feat/vtk-3d-view #7
|
|
@ -37,6 +37,7 @@ AnomalySaveDialog::AnomalySaveDialog(const QString& screenshotPath, int shotW, i
|
||||||
form->addRow(formkit::editLabel(QStringLiteral("名称")), name_);
|
form->addRow(formkit::editLabel(QStringLiteral("名称")), name_);
|
||||||
|
|
||||||
type_ = new EmptyAwareComboBox();
|
type_ = new EmptyAwareComboBox();
|
||||||
|
type_->setPlaceholderText(QStringLiteral("请选择异常类型")); // 空(如该形态平台无类型)时显灰占位+「暂无数据」
|
||||||
formkit::capField(type_);
|
formkit::capField(type_);
|
||||||
form->addRow(formkit::editLabel(QStringLiteral("异常类型")), type_);
|
form->addRow(formkit::editLabel(QStringLiteral("异常类型")), type_);
|
||||||
// 选中类型变化 → 拉其平台样式(legend),使保存的异常按平台类型样式渲染。
|
// 选中类型变化 → 拉其平台样式(legend),使保存的异常按平台类型样式渲染。
|
||||||
|
|
@ -89,9 +90,9 @@ void AnomalySaveDialog::loadStyleForCurrent() {
|
||||||
const QString typeId = type_->currentData().toString();
|
const QString typeId = type_->currentData().toString();
|
||||||
if (typeId.isEmpty()) return;
|
if (typeId.isEmpty()) return;
|
||||||
QPointer<AnomalySaveDialog> self(this);
|
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;
|
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*。
|
// 按形态(1点/2线/3面)从 legend 派生样式:点用 pointColor;线/面用 polyline*。
|
||||||
if (self->remarkSourceType_ == 1) {
|
if (self->remarkSourceType_ == 1) {
|
||||||
self->styleColor_ = lg.value(QStringLiteral("pointColor")).toString();
|
self->styleColor_ = lg.value(QStringLiteral("pointColor")).toString();
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,18 @@
|
||||||
#include "SliceExport.hpp"
|
#include "SliceExport.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
|
||||||
|
#include <QColor>
|
||||||
|
#include <QImage>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPainterPath>
|
||||||
|
#include <QPainterPathStroker>
|
||||||
|
#include <QPointF>
|
||||||
|
#include <QPolygonF>
|
||||||
|
#include <QRect>
|
||||||
|
|
||||||
#include <vtkCamera.h>
|
#include <vtkCamera.h>
|
||||||
#include <vtkDataArray.h>
|
#include <vtkDataArray.h>
|
||||||
#include <vtkImageData.h>
|
#include <vtkImageData.h>
|
||||||
|
|
@ -106,6 +117,107 @@ bool captureFramedRegionPng(vtkRenderWindow* win, const double regionBounds[6],
|
||||||
return ok;
|
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) {
|
bool exportSliceDat(vtkImageData* slice, const std::string& path) {
|
||||||
if (slice == nullptr || path.empty()) return false;
|
if (slice == nullptr || path.empty()) return false;
|
||||||
vtkDataArray* arr = slice->GetPointData() ? slice->GetPointData()->GetScalars() : nullptr;
|
vtkDataArray* arr = slice->GetPointData() ? slice->GetPointData()->GetScalars() : nullptr;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
#include <array>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
class vtkImageData;
|
class vtkImageData;
|
||||||
class vtkRenderWindow;
|
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,
|
bool captureFramedRegionPng(vtkRenderWindow* win, const double regionBounds[6], double padFactor,
|
||||||
double minExtent, const std::string& path, int& outW, int& outH);
|
double minExtent, const std::string& path, int& outW, int& outH);
|
||||||
|
|
||||||
|
// 异常截图(正确做法):**只从切片那张 2D 剖面彩图**上,按异常几何**向外缓冲(buffer)一圈后裁剪**输出。
|
||||||
|
// 业界范式 = GIS「几何缓冲 + 按掩膜裁剪栅格」:点→圆、线→胶囊带、面→外扩多边形;缓冲外透明。
|
||||||
|
// colorImg:selectedSliceColorImage() 的剖面 RGB 图;o/p1/p2:该切片平面三点(image i↔p1-o, j↔p2-o);
|
||||||
|
// worldPts:异常顶点(世界系,落在该平面);markType:1点/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。
|
// 把切片重采样 2D 标量影像写为 .dat 文本网格(行=j、列=i,空格分隔,每格取标量首分量);成功返回 true。
|
||||||
bool exportSliceDat(vtkImageData* slice, const std::string& path);
|
bool exportSliceDat(vtkImageData* slice, const std::string& path);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -545,8 +545,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
const std::string savedSliceId = interactionMgr->selectedSliceDsId();
|
const std::string savedSliceId = interactionMgr->selectedSliceDsId();
|
||||||
anomalyDrawTool->start(
|
anomalyDrawTool->start(
|
||||||
mode, o, normal,
|
mode, o, normal,
|
||||||
[&window, &cmdRepo, &nav, sceneView, scene3dRepo, renderWindowPtr, refreshAnomalies,
|
[&window, &cmdRepo, &nav, interactionMgr, sceneView, scene3dRepo, renderWindowPtr,
|
||||||
refreshAnalysis, volId, savedSliceId, normal, o, p1, p2,
|
refreshAnomalies, refreshAnalysis, volId, savedSliceId, normal, o, p1, p2,
|
||||||
shape](const std::vector<ri::Vec3>& worldPts) {
|
shape](const std::vector<ri::Vec3>& worldPts) {
|
||||||
// 草稿异常:先临时渲染(让用户在对话框前看到所画,且截图含异常)。
|
// 草稿异常:先临时渲染(让用户在对话框前看到所画,且截图含异常)。
|
||||||
geopro::core::Anomaly a;
|
geopro::core::Anomaly a;
|
||||||
|
|
@ -563,12 +563,21 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
a.id = draftId;
|
a.id = draftId;
|
||||||
sceneView->addAnomaly(a);
|
sceneView->addAnomaly(a);
|
||||||
renderWindowPtr->Render();
|
renderWindowPtr->Render();
|
||||||
// 截图(含异常)→ 临时文件。
|
|
||||||
const QString shot =
|
const QString shot =
|
||||||
QDir(QDir::tempPath()).filePath(QStringLiteral("geopro_anomaly_shot.png"));
|
QDir(QDir::tempPath()).filePath(QStringLiteral("geopro_anomaly_shot.png"));
|
||||||
int sw = 0, sh = 0;
|
int sw = 0, sh = 0;
|
||||||
// 相机重构图(方案A):框住圈定 worldPts 外扩区域(异常居中带语境);
|
// 截图(正确做法):只从切片那张 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],
|
double rb[6] = {worldPts[0][0], worldPts[0][0], worldPts[0][1],
|
||||||
worldPts[0][1], worldPts[0][2], worldPts[0][2]};
|
worldPts[0][1], worldPts[0][2], worldPts[0][2]};
|
||||||
for (const auto& p : worldPts) {
|
for (const auto& p : worldPts) {
|
||||||
|
|
@ -581,9 +590,10 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
};
|
};
|
||||||
const double e1 = vlen(p1[0] - o[0], p1[1] - o[1], p1[2] - o[2]);
|
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 e2 = vlen(p2[0] - o[0], p2[1] - o[1], p2[2] - o[2]);
|
||||||
const double minExt = 0.25 * std::min(e1, e2); // 点/线退化框景半径
|
const double minExt = 0.25 * std::min(e1, e2);
|
||||||
geopro::app::captureFramedRegionPng(renderWindowPtr, rb, 1.4, minExt,
|
geopro::app::captureFramedRegionPng(renderWindowPtr, rb, 1.4, minExt,
|
||||||
shot.toStdString(), sw, sh);
|
shot.toStdString(), sw, sh);
|
||||||
|
}
|
||||||
// 异常类型按标注形态(shape=1点/2线/3面)拉对应平台类型,与平台一致。
|
// 异常类型按标注形态(shape=1点/2线/3面)拉对应平台类型,与平台一致。
|
||||||
geopro::app::AnomalySaveDialog dlg(shot, sw, sh, &cmdRepo,
|
geopro::app::AnomalySaveDialog dlg(shot, sw, sh, &cmdRepo,
|
||||||
nav.currentProjectId(), shape, &window);
|
nav.currentProjectId(), shape, &window);
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,8 @@ namespace {
|
||||||
// 虚线点画图案(16 位)与重复因子;dashed 异常用。
|
// 虚线点画图案(16 位)与重复因子;dashed 异常用。
|
||||||
constexpr int kDashPattern = 0xF0F0;
|
constexpr int kDashPattern = 0xF0F0;
|
||||||
constexpr int kDashRepeat = 1;
|
constexpr int kDashRepeat = 1;
|
||||||
// Point 型异常的方块点像素边长。
|
// Point 型异常的小球像素直径(RenderPointsAsSpheres 下为球径)。
|
||||||
constexpr float kPointSize = 8.0F;
|
constexpr float kPointSize = 13.0F;
|
||||||
|
|
||||||
// 把一个异常的 localPts 灌入 points(x, -y, 0:深度取负,与 #18 同坐标系)。
|
// 把一个异常的 localPts 灌入 points(x, -y, 0:深度取负,与 #18 同坐标系)。
|
||||||
void fillPoints2D(vtkPoints* points, const geopro::core::Anomaly& a)
|
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);
|
actor->GetProperty()->SetColor(c.r / 255.0, c.g / 255.0, c.b / 255.0);
|
||||||
if (asPoints) {
|
if (asPoints) {
|
||||||
actor->GetProperty()->SetPointSize(kPointSize);
|
actor->GetProperty()->SetPointSize(kPointSize);
|
||||||
|
actor->GetProperty()->SetRenderPointsAsSpheres(true); // 点异常渲染为小球(非扁平方点)
|
||||||
} else {
|
} else {
|
||||||
actor->GetProperty()->SetLineWidth(a.lineWidth > 0.0 ? a.lineWidth : 1.0);
|
actor->GetProperty()->SetLineWidth(a.lineWidth > 0.0 ? a.lineWidth : 1.0);
|
||||||
if (a.dashed) {
|
if (a.dashed) {
|
||||||
|
|
|
||||||
|
|
@ -169,7 +169,8 @@ void AnomalyDrawTool::updatePreview() {
|
||||||
preview_->SetMapper(mapper);
|
preview_->SetMapper(mapper);
|
||||||
preview_->GetProperty()->SetColor(1.0, 0.9, 0.0); // 亮黄
|
preview_->GetProperty()->SetColor(1.0, 0.9, 0.0); // 亮黄
|
||||||
preview_->GetProperty()->SetLineWidth(2.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_);
|
renderer_->AddActor(preview_);
|
||||||
interactor_->Render();
|
interactor_->Render();
|
||||||
}
|
}
|
||||||
|
|
@ -178,7 +179,8 @@ void AnomalyDrawTool::updateRubber() {
|
||||||
if (!renderer_) return;
|
if (!renderer_) return;
|
||||||
if (rubber_) renderer_->RemoveViewProp(rubber_);
|
if (rubber_) renderer_->RemoveViewProp(rubber_);
|
||||||
rubber_ = nullptr;
|
rubber_ = nullptr;
|
||||||
if (pts_.empty() || !hasCursor_) {
|
// 点模式:单点标注,不拉末点→光标的橡皮筋线(否则点完还甩出一条线,用户反馈)。
|
||||||
|
if (mode_ == DrawMode::Point || pts_.empty() || !hasCursor_) {
|
||||||
if (interactor_) interactor_->Render();
|
if (interactor_) interactor_->Render();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue