fix(3d): 异常截图配色与切面一致——取 widget 同源着色输出(非另建 LUT)

用户实测异常截图与切面渲染配色差异极大(切面暖色彩虹、截图偏冷蓝绿)。根因:selectedSliceColorImage
另建 buildLut(v->cs,vmin,vmax) 上色, 与屏幕切片 widget 的实际着色可能分歧(范围/血缘处理不同)。
改:SliceTool 暴露 coloredResliceImage() = widget->GetColorMap()->GetOutput()(屏幕切片所贴的同一
RGBA 像素, 逐像素一致, 外区 alpha=0); selectedSliceColorImage 改取它再双线性上采样到 2048。
captureAnomalyShotFromSlice 处理 RGBA → 外区透明(顺带消除截图蓝边)。导出图片同样受益(与屏幕一致)。

测试:439/439 通过
This commit is contained in:
gaozheng 2026-06-26 11:56:33 +08:00
parent d470dc8154
commit 4ae8286bb0
4 changed files with 39 additions and 24 deletions

View File

@ -177,15 +177,24 @@ bool captureAnomalyShotFromSlice(vtkImageData* colorImg, const double o[3], cons
const QRect crop = shape.boundingRect().toAlignedRect().intersected(QRect(0, 0, nx, ny)); const QRect crop = shape.boundingRect().toAlignedRect().intersected(QRect(0, 0, nx, ny));
if (crop.width() < 1 || crop.height() < 1) return false; if (crop.width() < 1 || crop.height() < 1) return false;
// 切片 RGB(vtk, j=0 在底) → QImage(顶左原点,翻行)。 // 切片着色图(vtk, j=0 在底) → QImage(顶左原点,翻行)。RGBA 保留外区透明(消除血缘外蓝边)。
QImage src(nx, ny, QImage::Format_RGB888); const int comps = colorImg->GetNumberOfScalarComponents();
const bool rgba = comps >= 4;
QImage src(nx, ny, rgba ? QImage::Format_RGBA8888 : QImage::Format_RGB888);
for (int j = 0; j < ny; ++j) { for (int j = 0; j < ny; ++j) {
uchar* row = src.scanLine(ny - 1 - j); uchar* row = src.scanLine(ny - 1 - j);
for (int i = 0; i < nx; ++i) { for (int i = 0; i < nx; ++i) {
const auto* px = static_cast<unsigned char*>(colorImg->GetScalarPointer(i, j, 0)); const auto* px = static_cast<unsigned char*>(colorImg->GetScalarPointer(i, j, 0));
row[i * 3] = px[0]; if (rgba) {
row[i * 3 + 1] = px[1]; row[i * 4] = px[0];
row[i * 3 + 2] = px[2]; row[i * 4 + 1] = px[1];
row[i * 4 + 2] = px[2];
row[i * 4 + 3] = px[3];
} else {
row[i * 3] = px[0];
row[i * 3 + 1] = px[1];
row[i * 3 + 2] = px[2];
}
} }
} }

View File

@ -297,37 +297,29 @@ vtkImageData* InteractionManager::selectedSliceImage() const {
} }
vtkSmartPointer<vtkImageData> InteractionManager::selectedSliceColorImage() const { vtkSmartPointer<vtkImageData> InteractionManager::selectedSliceColorImage() const {
vtkImageData* scalar = selectedSliceImage(); if (selected_ < 0 || selected_ >= static_cast<int>(slices_.size())) return nullptr;
if (scalar == nullptr) return nullptr; // 与屏幕切片**同源**的着色输出(widget 自己的 ColorMap 输出, 逐像素一致, RGBA 外区透明)。
// 原先另建 LUT 上色, 与屏幕配色可能不一致(用户实测异常截图与切面差异大) → 改取 widget 着色结果。
auto colored = slices_[static_cast<std::size_t>(selected_)]->coloredResliceImage();
if (colored == nullptr) return nullptr;
// 高清导出切片重采样像素维度受体素网格分辨率限制常仅几十px→ 先上采样到目标分辨率 // 高清化:切片重采样像素维度受体素分辨率限制(常仅几十px) → 上采样到目标分辨率(双线性, 与屏幕
// (最长边 kExportLongSide保持长宽比、插值再上色得到清晰大图。 // TextureInterpolateOn 同口径), 得清晰大图。对 RGBA 直接插值(色已定, 不再过 LUT)
constexpr int kExportLongSide = 2048; constexpr int kExportLongSide = 2048;
int dims[3]; int dims[3];
scalar->GetDimensions(dims); colored->GetDimensions(dims);
const int nx = dims[0], ny = dims[1]; const int nx = dims[0], ny = dims[1];
const int longest = std::max(nx, ny); const int longest = std::max(nx, ny);
double f = (longest > 0) ? static_cast<double>(kExportLongSide) / longest : 1.0; double f = (longest > 0) ? static_cast<double>(kExportLongSide) / longest : 1.0;
if (f < 1.0) f = 1.0; // 不缩小(已够大则原样) if (f < 1.0) f = 1.0; // 不缩小
vtkNew<vtkImageResize> resize; vtkNew<vtkImageResize> resize;
resize->SetInputData(scalar); resize->SetInputData(colored);
resize->SetResizeMethodToOutputDimensions(); resize->SetResizeMethodToOutputDimensions();
resize->SetOutputDimensions(std::max(1, static_cast<int>(nx * f)), resize->SetOutputDimensions(std::max(1, static_cast<int>(nx * f)),
std::max(1, static_cast<int>(ny * f)), 1); std::max(1, static_cast<int>(ny * f)), 1);
resize->Update(); resize->Update();
// 用与切片显示同一色阶 LUT 上色:取选中切片所属体的色阶(多体并发各体色阶不同)。
const VolumeImg* v = (selected_ >= 0 && selected_ < static_cast<int>(slices_.size()))
? volumeOf(slices_[static_cast<std::size_t>(selected_)]->volumeDsId())
: nullptr;
auto lut = v ? buildLut(v->cs, v->vmin, v->vmax) : buildLut(geopro::core::ColorScale{}, 0.0, 1.0);
vtkNew<vtkImageMapToColors> map;
map->SetInputConnection(resize->GetOutputPort());
map->SetLookupTable(lut);
map->SetOutputFormatToRGB();
map->Update();
auto out = vtkSmartPointer<vtkImageData>::New(); auto out = vtkSmartPointer<vtkImageData>::New();
out->DeepCopy(map->GetOutput()); // 深拷贝脱离 filter 生命周期 out->DeepCopy(resize->GetOutput()); // 脱离 filter 生命周期
return out; return out;
} }

View File

@ -6,6 +6,7 @@
#include <vtkCallbackCommand.h> #include <vtkCallbackCommand.h>
#include <vtkCommand.h> #include <vtkCommand.h>
#include <vtkImageData.h> #include <vtkImageData.h>
#include <vtkImageMapToColors.h>
#include <vtkImagePlaneWidget.h> #include <vtkImagePlaneWidget.h>
#include <vtkLookupTable.h> #include <vtkLookupTable.h>
#include <vtkProperty.h> #include <vtkProperty.h>
@ -170,6 +171,16 @@ vtkImageData* SliceTool::reslicedOutput() const {
return widget_ ? widget_->GetResliceOutput() : nullptr; return widget_ ? widget_->GetResliceOutput() : nullptr;
} }
vtkSmartPointer<vtkImageData> SliceTool::coloredResliceImage() const {
if (!widget_) return nullptr;
vtkImageMapToColors* cm = widget_->GetColorMap(); // widget 内部把 reslice 经 LUT 上色 → 纹理
if (cm == nullptr) return nullptr;
cm->Update();
auto out = vtkSmartPointer<vtkImageData>::New();
out->DeepCopy(cm->GetOutput()); // 即屏幕切片所贴像素(RGBA, 外区 alpha=0)
return out;
}
double SliceTool::distanceToPlane(const Vec3& p) const { double SliceTool::distanceToPlane(const Vec3& p) const {
const Vec3 c = center(); const Vec3 c = center();
const Vec3 n = normal(); const Vec3 n = normal();

View File

@ -77,6 +77,9 @@ public:
// 当前切面重采样得到的 2D 标量影像(导出 dat 用widget 已释放则 nullptr。 // 当前切面重采样得到的 2D 标量影像(导出 dat 用widget 已释放则 nullptr。
vtkImageData* reslicedOutput() const; vtkImageData* reslicedOutput() const;
// 与屏幕切片纹理同源的着色输出(widget 自己的 ColorMap 输出, RGBA, 逐像素一致, 外区透明)。
// 异常截图/导出用它而非另建 LUT避免与屏幕配色不一致(用户实测差异大)。
vtkSmartPointer<vtkImageData> coloredResliceImage() const;
// 关闭Off() 并解除 interactor 绑定(幂等)。 // 关闭Off() 并解除 interactor 绑定(幂等)。
void close(); void close();