From 4ae8286bb07d6bcbc4c6a03b55f1ca241e5873e7 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Fri, 26 Jun 2026 11:56:33 +0800 Subject: [PATCH] =?UTF-8?q?fix(3d):=20=E5=BC=82=E5=B8=B8=E6=88=AA=E5=9B=BE?= =?UTF-8?q?=E9=85=8D=E8=89=B2=E4=B8=8E=E5=88=87=E9=9D=A2=E4=B8=80=E8=87=B4?= =?UTF-8?q?=E2=80=94=E2=80=94=E5=8F=96=20widget=20=E5=90=8C=E6=BA=90?= =?UTF-8?q?=E7=9D=80=E8=89=B2=E8=BE=93=E5=87=BA(=E9=9D=9E=E5=8F=A6?= =?UTF-8?q?=E5=BB=BA=20LUT)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户实测异常截图与切面渲染配色差异极大(切面暖色彩虹、截图偏冷蓝绿)。根因:selectedSliceColorImage 另建 buildLut(v->cs,vmin,vmax) 上色, 与屏幕切片 widget 的实际着色可能分歧(范围/血缘处理不同)。 改:SliceTool 暴露 coloredResliceImage() = widget->GetColorMap()->GetOutput()(屏幕切片所贴的同一 RGBA 像素, 逐像素一致, 外区 alpha=0); selectedSliceColorImage 改取它再双线性上采样到 2048。 captureAnomalyShotFromSlice 处理 RGBA → 外区透明(顺带消除截图蓝边)。导出图片同样受益(与屏幕一致)。 测试:439/439 通过 --- src/app/SliceExport.cpp | 19 ++++++++++---- src/render/interact/InteractionManager.cpp | 30 ++++++++-------------- src/render/interact/SliceTool.cpp | 11 ++++++++ src/render/interact/SliceTool.hpp | 3 +++ 4 files changed, 39 insertions(+), 24 deletions(-) diff --git a/src/app/SliceExport.cpp b/src/app/SliceExport.cpp index 1c10a56..92d85e4 100644 --- a/src/app/SliceExport.cpp +++ b/src/app/SliceExport.cpp @@ -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)); if (crop.width() < 1 || crop.height() < 1) return false; - // 切片 RGB(vtk, j=0 在底) → QImage(顶左原点,翻行)。 - QImage src(nx, ny, QImage::Format_RGB888); + // 切片着色图(vtk, j=0 在底) → QImage(顶左原点,翻行)。RGBA 保留外区透明(消除血缘外蓝边)。 + 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) { uchar* row = src.scanLine(ny - 1 - j); for (int i = 0; i < nx; ++i) { const auto* px = static_cast(colorImg->GetScalarPointer(i, j, 0)); - row[i * 3] = px[0]; - row[i * 3 + 1] = px[1]; - row[i * 3 + 2] = px[2]; + if (rgba) { + row[i * 4] = px[0]; + 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]; + } } } diff --git a/src/render/interact/InteractionManager.cpp b/src/render/interact/InteractionManager.cpp index dffece6..5291f8c 100644 --- a/src/render/interact/InteractionManager.cpp +++ b/src/render/interact/InteractionManager.cpp @@ -297,37 +297,29 @@ vtkImageData* InteractionManager::selectedSliceImage() const { } vtkSmartPointer InteractionManager::selectedSliceColorImage() const { - vtkImageData* scalar = selectedSliceImage(); - if (scalar == nullptr) return nullptr; + if (selected_ < 0 || selected_ >= static_cast(slices_.size())) return nullptr; + // 与屏幕切片**同源**的着色输出(widget 自己的 ColorMap 输出, 逐像素一致, RGBA 外区透明)。 + // 原先另建 LUT 上色, 与屏幕配色可能不一致(用户实测异常截图与切面差异大) → 改取 widget 着色结果。 + auto colored = slices_[static_cast(selected_)]->coloredResliceImage(); + if (colored == nullptr) return nullptr; - // 高清导出:切片重采样像素维度受体素网格分辨率限制(常仅几十px)→ 先上采样到目标分辨率 - // (最长边 kExportLongSide,保持长宽比、插值),再上色,得到清晰大图。 + // 高清化:切片重采样像素维度受体素分辨率限制(常仅几十px) → 上采样到目标分辨率(双线性, 与屏幕 + // TextureInterpolateOn 同口径), 得清晰大图。对 RGBA 直接插值(色已定, 不再过 LUT)。 constexpr int kExportLongSide = 2048; int dims[3]; - scalar->GetDimensions(dims); + colored->GetDimensions(dims); const int nx = dims[0], ny = dims[1]; const int longest = std::max(nx, ny); double f = (longest > 0) ? static_cast(kExportLongSide) / longest : 1.0; - if (f < 1.0) f = 1.0; // 不缩小(已够大则原样) + if (f < 1.0) f = 1.0; // 不缩小 vtkNew resize; - resize->SetInputData(scalar); + resize->SetInputData(colored); resize->SetResizeMethodToOutputDimensions(); resize->SetOutputDimensions(std::max(1, static_cast(nx * f)), std::max(1, static_cast(ny * f)), 1); resize->Update(); - - // 用与切片显示同一色阶 LUT 上色:取选中切片所属体的色阶(多体并发各体色阶不同)。 - const VolumeImg* v = (selected_ >= 0 && selected_ < static_cast(slices_.size())) - ? volumeOf(slices_[static_cast(selected_)]->volumeDsId()) - : nullptr; - auto lut = v ? buildLut(v->cs, v->vmin, v->vmax) : buildLut(geopro::core::ColorScale{}, 0.0, 1.0); - vtkNew map; - map->SetInputConnection(resize->GetOutputPort()); - map->SetLookupTable(lut); - map->SetOutputFormatToRGB(); - map->Update(); auto out = vtkSmartPointer::New(); - out->DeepCopy(map->GetOutput()); // 深拷贝脱离 filter 生命周期 + out->DeepCopy(resize->GetOutput()); // 脱离 filter 生命周期 return out; } diff --git a/src/render/interact/SliceTool.cpp b/src/render/interact/SliceTool.cpp index fe282c5..26052f2 100644 --- a/src/render/interact/SliceTool.cpp +++ b/src/render/interact/SliceTool.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -170,6 +171,16 @@ vtkImageData* SliceTool::reslicedOutput() const { return widget_ ? widget_->GetResliceOutput() : nullptr; } +vtkSmartPointer SliceTool::coloredResliceImage() const { + if (!widget_) return nullptr; + vtkImageMapToColors* cm = widget_->GetColorMap(); // widget 内部把 reslice 经 LUT 上色 → 纹理 + if (cm == nullptr) return nullptr; + cm->Update(); + auto out = vtkSmartPointer::New(); + out->DeepCopy(cm->GetOutput()); // 即屏幕切片所贴像素(RGBA, 外区 alpha=0) + return out; +} + double SliceTool::distanceToPlane(const Vec3& p) const { const Vec3 c = center(); const Vec3 n = normal(); diff --git a/src/render/interact/SliceTool.hpp b/src/render/interact/SliceTool.hpp index 8075566..277c9b4 100644 --- a/src/render/interact/SliceTool.hpp +++ b/src/render/interact/SliceTool.hpp @@ -77,6 +77,9 @@ public: // 当前切面重采样得到的 2D 标量影像(导出 dat 用);widget 已释放则 nullptr。 vtkImageData* reslicedOutput() const; + // 与屏幕切片纹理同源的着色输出(widget 自己的 ColorMap 输出, RGBA, 逐像素一致, 外区透明)。 + // 异常截图/导出用它而非另建 LUT,避免与屏幕配色不一致(用户实测差异大)。 + vtkSmartPointer coloredResliceImage() const; // 关闭:Off() 并解除 interactor 绑定(幂等)。 void close();