feat/vtk-3d-view #7

Merged
gaozheng merged 301 commits from feat/vtk-3d-view into main 2026-06-27 18:43:52 +08:00
6 changed files with 164 additions and 25 deletions
Showing only changes of commit 04af569b7d - Show all commits

View File

@ -37,6 +37,7 @@ AnomalySaveDialog::AnomalySaveDialog(const QString& screenshotPath, int shotW, i
form->addRow(formkit::editLabel(QStringLiteral("名称")), name_);
type_ = new EmptyAwareComboBox();
type_->setPlaceholderText(QStringLiteral("请选择异常类型")); // 空(如该形态平台无类型)时显灰占位+「暂无数据」
formkit::capField(type_);
form->addRow(formkit::editLabel(QStringLiteral("异常类型")), type_);
// 选中类型变化 → 拉其平台样式(legend),使保存的异常按平台类型样式渲染。
@ -89,9 +90,9 @@ void AnomalySaveDialog::loadStyleForCurrent() {
const QString typeId = type_->currentData().toString();
if (typeId.isEmpty()) return;
QPointer<AnomalySaveDialog> self(this);
cmdRepo_->getExceptionTypeDetail(typeId, [self](bool ok, QJsonObject data, const QString&) {
cmdRepo_->getExceptionTypeDetail(typeId, [self](bool ok, QJsonObject detail, const QString&) {
if (!self || !ok) return;
const QJsonObject lg = data.value(QStringLiteral("legend")).toObject();
const QJsonObject lg = detail.value(QStringLiteral("legend")).toObject();
// 按形态(1点/2线/3面)从 legend 派生样式:点用 pointColor线/面用 polyline*。
if (self->remarkSourceType_ == 1) {
self->styleColor_ = lg.value(QStringLiteral("pointColor")).toString();

View File

@ -1,7 +1,18 @@
#include "SliceExport.hpp"
#include <algorithm>
#include <cmath>
#include <fstream>
#include <QColor>
#include <QImage>
#include <QPainter>
#include <QPainterPath>
#include <QPainterPathStroker>
#include <QPointF>
#include <QPolygonF>
#include <QRect>
#include <vtkCamera.h>
#include <vtkDataArray.h>
#include <vtkImageData.h>
@ -106,6 +117,107 @@ bool captureFramedRegionPng(vtkRenderWindow* win, const double regionBounds[6],
return ok;
}
bool captureAnomalyShotFromSlice(vtkImageData* colorImg, const double o[3], const double p1[3],
const double p2[3],
const std::vector<std::array<double, 3>>& worldPts, int markType,
const std::string& outlineHex, const std::string& path, int& outW,
int& outH) {
outW = outH = 0;
if (colorImg == nullptr || worldPts.empty() || path.empty()) return false;
int dims[3];
colorImg->GetDimensions(dims);
const int nx = dims[0], ny = dims[1];
if (nx < 2 || ny < 2) return false;
// 平面两轴(image i↔e1=p1-o, j↔e2=p2-o);世界点 → 归一(u,v) → 像素(QImage 顶左原点,需翻 j)。
const double e1[3] = {p1[0] - o[0], p1[1] - o[1], p1[2] - o[2]};
const double e2[3] = {p2[0] - o[0], p2[1] - o[1], p2[2] - o[2]};
const double L1 = e1[0] * e1[0] + e1[1] * e1[1] + e1[2] * e1[2];
const double L2 = e2[0] * e2[0] + e2[1] * e2[1] + e2[2] * e2[2];
if (L1 < 1e-12 || L2 < 1e-12) return false;
QPolygonF poly;
poly.reserve(static_cast<int>(worldPts.size()));
for (const auto& P : worldPts) {
const double d[3] = {P[0] - o[0], P[1] - o[1], P[2] - o[2]};
const double u = (d[0] * e1[0] + d[1] * e1[1] + d[2] * e1[2]) / L1;
const double v = (d[0] * e2[0] + d[1] * e2[1] + d[2] * e2[2]) / L2;
poly << QPointF(u * (nx - 1), (ny - 1) - v * (ny - 1)); // 翻 j 到 QImage 坐标
}
// 缓冲半径:异常包围盒对角的 15%,最小取图长边 4%(点/小异常也有可见外扩)。
const QRectF pb = poly.boundingRect();
const double diag = std::hypot(pb.width(), pb.height());
const double buffer = std::max(0.04 * std::max(nx, ny), 0.15 * diag);
// 按形态构 buffer 后的裁剪形状:点→圆、线→胶囊带、面→外扩多边形(填充 描边)。
QPainterPath shape;
if (markType == 1 || poly.size() == 1) {
shape.addEllipse(poly.first(), buffer, buffer);
} else if (markType == 2) {
QPainterPath line;
line.moveTo(poly.first());
for (int i = 1; i < poly.size(); ++i) line.lineTo(poly[i]);
QPainterPathStroker st;
st.setWidth(2.0 * buffer);
st.setCapStyle(Qt::RoundCap);
st.setJoinStyle(Qt::RoundJoin);
shape = st.createStroke(line);
} else {
QPainterPath fill;
fill.addPolygon(poly);
fill.closeSubpath();
QPainterPath outline = fill;
QPainterPathStroker st;
st.setWidth(2.0 * buffer);
st.setJoinStyle(Qt::RoundJoin);
shape = fill.united(st.createStroke(outline)); // 向外扩 buffer
}
// 裁剪区 = 形状包围盒(夹到图内)。
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);
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<unsigned char*>(colorImg->GetScalarPointer(i, j, 0));
row[i * 3] = px[0];
row[i * 3 + 1] = px[1];
row[i * 3 + 2] = px[2];
}
}
// 输出buffer 形状内贴剖面像素(外透明),再描异常轮廓。
QImage out(crop.size(), QImage::Format_ARGB32);
out.fill(Qt::transparent);
QPainter pr(&out);
pr.setRenderHint(QPainter::Antialiasing, true);
pr.translate(-crop.topLeft());
pr.setClipPath(shape);
pr.drawImage(0, 0, src);
pr.setClipping(false);
QColor oc(QString::fromStdString(outlineHex));
if (!oc.isValid()) oc = QColor(255, 48, 48);
QPen pen(oc);
pen.setWidthF(2.0);
pr.setPen(pen);
pr.setBrush(Qt::NoBrush);
if (markType == 1 || poly.size() == 1)
pr.drawEllipse(poly.first(), 4.0, 4.0); // 点:小标记
else if (markType == 2)
pr.drawPolyline(poly);
else
pr.drawPolygon(poly);
pr.end();
if (!out.save(QString::fromStdString(path), "PNG")) return false;
outW = out.width();
outH = out.height();
return true;
}
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

@ -1,5 +1,7 @@
#pragma once
#include <array>
#include <string>
#include <vector>
class vtkImageData;
class vtkRenderWindow;
@ -21,6 +23,17 @@ bool captureRenderWindowPng(vtkRenderWindow* win, const std::string& path, int&
bool captureFramedRegionPng(vtkRenderWindow* win, const double regionBounds[6], double padFactor,
double minExtent, const std::string& path, int& outW, int& outH);
// 异常截图(正确做法):**只从切片那张 2D 剖面彩图**上,按异常几何**向外缓冲(buffer)一圈后裁剪**输出。
// 业界范式 = GIS「几何缓冲 + 按掩膜裁剪栅格」:点→圆、线→胶囊带、面→外扩多边形;缓冲外透明。
// colorImgselectedSliceColorImage() 的剖面 RGB 图o/p1/p2该切片平面三点(image i↔p1-o, j↔p2-o)
// worldPts异常顶点(世界系,落在该平面)markType1点/2线/3面outlineHex在裁图上描异常轮廓的颜色。
// 成功返回 true填回输出像素宽高。失败(无图/几何退化)返回 false调用方可回退。
bool captureAnomalyShotFromSlice(vtkImageData* colorImg, const double o[3], const double p1[3],
const double p2[3],
const std::vector<std::array<double, 3>>& worldPts, int markType,
const std::string& outlineHex, const std::string& path, int& outW,
int& outH);
// 把切片重采样 2D 标量影像写为 .dat 文本网格(行=j、列=i空格分隔每格取标量首分量成功返回 true。
bool exportSliceDat(vtkImageData* slice, const std::string& path);

View File

@ -545,8 +545,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
const std::string savedSliceId = interactionMgr->selectedSliceDsId();
anomalyDrawTool->start(
mode, o, normal,
[&window, &cmdRepo, &nav, sceneView, scene3dRepo, renderWindowPtr, refreshAnomalies,
refreshAnalysis, volId, savedSliceId, normal, o, p1, p2,
[&window, &cmdRepo, &nav, interactionMgr, sceneView, scene3dRepo, renderWindowPtr,
refreshAnomalies, refreshAnalysis, volId, savedSliceId, normal, o, p1, p2,
shape](const std::vector<ri::Vec3>& worldPts) {
// 草稿异常:先临时渲染(让用户在对话框前看到所画,且截图含异常)。
geopro::core::Anomaly a;
@ -563,27 +563,37 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
a.id = draftId;
sceneView->addAnomaly(a);
renderWindowPtr->Render();
// 截图(含异常)→ 临时文件。
const QString shot =
QDir(QDir::tempPath()).filePath(QStringLiteral("geopro_anomaly_shot.png"));
int sw = 0, sh = 0;
// 相机重构图(方案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]);
// 截图(正确做法):只从切片那张 2D 剖面彩图、按异常几何向外缓冲一圈裁剪(GIS buffer+掩膜)。
std::vector<std::array<double, 3>> wpts;
wpts.reserve(worldPts.size());
for (const auto& p : worldPts) wpts.push_back({p[0], p[1], p[2]});
vtkSmartPointer<vtkImageData> sliceColor = interactionMgr->selectedSliceColorImage();
const double oo[3] = {o[0], o[1], o[2]};
const double pp1[3] = {p1[0], p1[1], p1[2]};
const double pp2[3] = {p2[0], p2[1], p2[2]};
bool shotOk = sliceColor && geopro::app::captureAnomalyShotFromSlice(
sliceColor, oo, pp1, pp2, wpts, shape,
a.lineColor, shot.toStdString(), sw, sh);
if (!shotOk) { // 回退:无切片图时退回相机框景(整窗外扩),至少有图。
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);
}
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);
// 异常类型按标注形态(shape=1点/2线/3面)拉对应平台类型,与平台一致。
geopro::app::AnomalySaveDialog dlg(shot, sw, sh, &cmdRepo,
nav.currentProjectId(), shape, &window);

View File

@ -19,8 +19,8 @@ namespace {
// 虚线点画图案(16 位)与重复因子dashed 异常用。
constexpr int kDashPattern = 0xF0F0;
constexpr int kDashRepeat = 1;
// Point 型异常的方块点像素边长
constexpr float kPointSize = 8.0F;
// Point 型异常的小球像素直径RenderPointsAsSpheres 下为球径)
constexpr float kPointSize = 13.0F;
// 把一个异常的 localPts 灌入 pointsx, -y, 0深度取负与 #18 同坐标系)。
void fillPoints2D(vtkPoints* points, const geopro::core::Anomaly& a)
@ -82,6 +82,7 @@ vtkSmartPointer<vtkActor> buildActor(vtkPoints* points, std::size_t n,
actor->GetProperty()->SetColor(c.r / 255.0, c.g / 255.0, c.b / 255.0);
if (asPoints) {
actor->GetProperty()->SetPointSize(kPointSize);
actor->GetProperty()->SetRenderPointsAsSpheres(true); // 点异常渲染为小球(非扁平方点)
} else {
actor->GetProperty()->SetLineWidth(a.lineWidth > 0.0 ? a.lineWidth : 1.0);
if (a.dashed) {

View File

@ -169,7 +169,8 @@ void AnomalyDrawTool::updatePreview() {
preview_->SetMapper(mapper);
preview_->GetProperty()->SetColor(1.0, 0.9, 0.0); // 亮黄
preview_->GetProperty()->SetLineWidth(2.0);
preview_->GetProperty()->SetPointSize(9.0); // 醒目圆点
preview_->GetProperty()->SetPointSize(mode_ == DrawMode::Point ? 16.0 : 9.0); // 点模式更醒目
preview_->GetProperty()->SetRenderPointsAsSpheres(true); // 顶点渲染为小球(图钉感)
renderer_->AddActor(preview_);
interactor_->Render();
}
@ -178,7 +179,8 @@ void AnomalyDrawTool::updateRubber() {
if (!renderer_) return;
if (rubber_) renderer_->RemoveViewProp(rubber_);
rubber_ = nullptr;
if (pts_.empty() || !hasCursor_) {
// 点模式:单点标注,不拉末点→光标的橡皮筋线(否则点完还甩出一条线,用户反馈)。
if (mode_ == DrawMode::Point || pts_.empty() || !hasCursor_) {
if (interactor_) interactor_->Render();
return;
}