From 75c1327aa43078dfeadfa8efdf4c65c92ffcb82a Mon Sep 17 00:00:00 2001 From: gaozheng Date: Fri, 26 Jun 2026 10:21:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(3d):=20=E5=88=9B=E5=BB=BA=E5=BC=82?= =?UTF-8?q?=E5=B8=B8=E6=88=AA=E5=9B=BE=E6=94=B9=E7=9B=B8=E6=9C=BA=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E5=9B=BE(=E6=96=B9=E6=A1=88A,frame-to-fit=20selection?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 异常截图原为整窗口截图。改为业界 frame/zoom-to-fit selection 范式: captureFramedRegionPng 把相机临时重新取景到圈定 worldPts 外扩区域(padFactor=1.4≈异常占画面~70% 带周边语境),视角方向不变仅推近/缩放(ResetCamera),后台缓冲+关交换截图屏幕不闪,截后还原相机。 点(零体积)/线面共面(某轴零厚度)用切片尺寸 0.25×min(e1,e2) 作框景半径兜底。 main 调用处从 worldPts 算世界包围盒 + 从切片 o/p1/p2 算兜底尺寸。 构建:app 链接通过 --- src/app/SliceExport.cpp | 65 +++++++++++++++++++++++++++++++++++++++++ src/app/SliceExport.hpp | 9 ++++++ src/app/main.cpp | 20 +++++++++++-- 3 files changed, 92 insertions(+), 2 deletions(-) diff --git a/src/app/SliceExport.cpp b/src/app/SliceExport.cpp index 66da84c..3e29ff5 100644 --- a/src/app/SliceExport.cpp +++ b/src/app/SliceExport.cpp @@ -2,12 +2,15 @@ #include +#include #include #include #include #include #include #include +#include +#include #include namespace geopro::app { @@ -41,6 +44,68 @@ bool captureRenderWindowPng(vtkRenderWindow* win, const std::string& path, int& return writer->GetErrorCode() == 0; } +bool captureFramedRegionPng(vtkRenderWindow* win, const double regionBounds[6], double padFactor, + double minExtent, const std::string& path, int& outW, int& outH) { + outW = outH = 0; + if (win == nullptr || path.empty()) return false; + vtkRenderer* ren = + win->GetRenderers() ? win->GetRenderers()->GetFirstRenderer() : nullptr; + vtkCamera* cam = ren ? ren->GetActiveCamera() : nullptr; + if (ren == nullptr || cam == nullptr) + return captureRenderWindowPng(win, path, outW, outH); // 无渲染器 → 退回整窗 + + // 1) 区域包围盒:minExtent 兜底(点零体积/共面零厚度) → padFactor 以中心外扩留边距。 + double b[6]; + for (int i = 0; i < 3; ++i) { + const double lo = regionBounds[2 * i], hi = regionBounds[2 * i + 1]; + const double c = 0.5 * (lo + hi); + double half = 0.5 * (hi - lo); + if (2.0 * half < minExtent) half = 0.5 * minExtent; // 退化轴兜底 + half *= padFactor; // 外扩边距 + b[2 * i] = c - half; + b[2 * i + 1] = c + half; + } + + // 2) 存相机现场(ResetCamera 改 position/focalPoint/clipping/parallelScale)。 + double pos[3], fp[3], up[3], clip[2]; + cam->GetPosition(pos); + cam->GetFocalPoint(fp); + cam->GetViewUp(up); + cam->GetClippingRange(clip); + const double va = cam->GetViewAngle(); + const double ps = cam->GetParallelScale(); + + // 3) 重构图:保持视角方向,仅推近/缩放框住外扩区域。 + ren->ResetCamera(b); + + // 4) 截图(后台缓冲 + 关交换 → 屏幕不闪)。 + vtkNew w2i; + w2i->SetInput(win); + w2i->ReadFrontBufferOff(); + w2i->Update(); + if (auto* img = w2i->GetOutput()) { + int dims[3]; + img->GetDimensions(dims); + outW = dims[0]; + outH = dims[1]; + } + vtkNew writer; + writer->SetFileName(path.c_str()); + writer->SetInputConnection(w2i->GetOutputPort()); + writer->Write(); + const bool ok = writer->GetErrorCode() == 0; + + // 5) 还原相机 + 重绘回原视图。 + cam->SetPosition(pos); + cam->SetFocalPoint(fp); + cam->SetViewUp(up); + cam->SetViewAngle(va); + cam->SetParallelScale(ps); + cam->SetClippingRange(clip); + win->Render(); + return ok; +} + bool exportSliceDat(vtkImageData* slice, const std::string& path) { if (slice == nullptr || path.empty()) return false; vtkDataArray* arr = slice->GetPointData() ? slice->GetPointData()->GetScalars() : nullptr; diff --git a/src/app/SliceExport.hpp b/src/app/SliceExport.hpp index 0325ba8..15ae84f 100644 --- a/src/app/SliceExport.hpp +++ b/src/app/SliceExport.hpp @@ -12,6 +12,15 @@ bool exportSliceImagePng(vtkImageData* colorImage, const std::string& path); // 截整个渲染窗口为 PNG(异常标识截图,需求 R88);成功返回 true,并填回截图像素宽高。 bool captureRenderWindowPng(vtkRenderWindow* win, const std::string& path, int& outW, int& outH); +// 「相机重构图」截图(方案A):把相机临时重新取景到 regionBounds(圈定范围)外扩后的区域, +// 使异常框在画面中央带周边语境,再截图、还原相机。业界 frame/zoom-to-fit selection 范式。 +// regionBounds: {xmin,xmax,ymin,ymax,zmin,zmax} 世界系圈定包围盒; +// padFactor: 以盒中心外扩的倍数(1.4≈异常占画面~70%); +// minExtent: 退化兜底(点=零体积、线/面共面=某轴零厚度)时各轴的最小世界尺寸。 +// 视角方向不变(仅推近/缩放);屏幕无闪(后台缓冲+关交换)。失败回退整窗截图。 +bool captureFramedRegionPng(vtkRenderWindow* win, const double regionBounds[6], double padFactor, + double minExtent, const std::string& path, int& outW, int& outH); + // 把切片重采样 2D 标量影像写为 .dat 文本网格(行=j、列=i,空格分隔,每格取标量首分量);成功返回 true。 bool exportSliceDat(vtkImageData* slice, const std::string& path); diff --git a/src/app/main.cpp b/src/app/main.cpp index 481de59..a1f396c 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -538,7 +538,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re anomalyDrawTool->start( o, normal, [&window, sceneView, scene3dRepo, renderWindowPtr, refreshAnomalies, refreshAnalysis, - volId, savedSliceId, normal, o](const std::vector& worldPts) { + volId, savedSliceId, normal, o, p1, p2](const std::vector& worldPts) { // 草稿异常:先临时渲染(让用户在对话框前看到所画,且截图含异常)。 geopro::core::Anomaly a; a.markType = geopro::core::AnomalyMarkType::Polygon; @@ -558,7 +558,23 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re const QString shot = QDir(QDir::tempPath()).filePath(QStringLiteral("geopro_anomaly_shot.png")); int sw = 0, sh = 0; - geopro::app::captureRenderWindowPng(renderWindowPtr, shot.toStdString(), sw, sh); + // 相机重构图(方案A):框住圈定 worldPts 外扩区域(异常居中带语境); + // 点/共面退化用切片尺寸兜底框景半径。 + double rb[6] = {worldPts[0][0], worldPts[0][0], worldPts[0][1], + worldPts[0][1], worldPts[0][2], worldPts[0][2]}; + for (const auto& p : worldPts) { + rb[0] = std::min(rb[0], p[0]); rb[1] = std::max(rb[1], p[0]); + rb[2] = std::min(rb[2], p[1]); rb[3] = std::max(rb[3], p[1]); + rb[4] = std::min(rb[4], p[2]); rb[5] = std::max(rb[5], p[2]); + } + auto vlen = [](double x, double y, double z) { + return std::sqrt(x * x + y * y + z * z); + }; + const double e1 = vlen(p1[0] - o[0], p1[1] - o[1], p1[2] - o[2]); + const double e2 = vlen(p2[0] - o[0], p2[1] - o[1], p2[2] - o[2]); + const double minExt = 0.25 * std::min(e1, e2); // 点/线退化框景半径 + geopro::app::captureFramedRegionPng(renderWindowPtr, rb, 1.4, minExt, + shot.toStdString(), sw, sh); geopro::app::AnomalySaveDialog dlg(shot, sw, sh, &window); if (dlg.exec() != QDialog::Accepted) { sceneView->removeAnomaly(draftId);