feat/vtk-3d-view #7
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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「几何缓冲 + 按掩膜裁剪栅格」:点→圆、线→胶囊带、面→外扩多边形;缓冲外透明。
|
||||
// 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。
|
||||
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();
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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 灌入 points(x, -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue