feat(3d): 创建异常截图改相机重构图(方案A,frame-to-fit selection)

异常截图原为整窗口截图。改为业界 frame/zoom-to-fit selection 范式:
captureFramedRegionPng 把相机临时重新取景到圈定 worldPts 外扩区域(padFactor=1.4≈异常占画面~70%
带周边语境),视角方向不变仅推近/缩放(ResetCamera),后台缓冲+关交换截图屏幕不闪,截后还原相机。
点(零体积)/线面共面(某轴零厚度)用切片尺寸 0.25×min(e1,e2) 作框景半径兜底。
main 调用处从 worldPts 算世界包围盒 + 从切片 o/p1/p2 算兜底尺寸。

构建:app 链接通过
This commit is contained in:
gaozheng 2026-06-26 10:21:25 +08:00
parent 56e4b3a7ff
commit 75c1327aa4
3 changed files with 92 additions and 2 deletions

View File

@ -2,12 +2,15 @@
#include <fstream>
#include <vtkCamera.h>
#include <vtkDataArray.h>
#include <vtkImageData.h>
#include <vtkNew.h>
#include <vtkPNGWriter.h>
#include <vtkPointData.h>
#include <vtkRenderWindow.h>
#include <vtkRenderer.h>
#include <vtkRendererCollection.h>
#include <vtkWindowToImageFilter.h>
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<vtkWindowToImageFilter> 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<vtkPNGWriter> 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;

View File

@ -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);

View File

@ -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<ri::Vec3>& worldPts) {
volId, savedSliceId, normal, o, p1, p2](const std::vector<ri::Vec3>& 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);