feat(render): dd_voxel 回归 — 散点经 EPSG:4547 配准到世界系成体素 + 3D 接入

- buildVoxelFromScatters(VoxelFromScatters): 两交叉测线散点(projX/Y) 经
  CrsTransform(EPSG:4547→4326) → GeoLocalFrame 局部米 + 深度(-ylist) 配准到帘面/地图
  同世界系, IDW(maxDist 裁剪 NaN 留空) → buildVoxel; 暴露 image 供后续 dd_slice。
- 离屏核对: verify_voxel_top.png 两臂支撑吻合 Python 真值 voxel_hslice;
  verify_voxel_3d.png profile1 片贴合帘面(同系配准正确)。
- 接入 app: 中央工具条「体素」开关(仅 3D 有效, 默认关), 与帘面同纵向夸张叠加。
  main() 按候选路径自动设 PROJ_DATA(部署须随包附带 proj 数据)。
- 新增 VoxelRegister 单测(需 PROJ_DATA, tests CMake 已注入); 全 36 测试绿。
- 注: 仅 2 交叉线→薄十字片(15.9% 充填), 体绘制半透明偏淡(低不透明度固有);
  可信满体需≥3线(设计 §10/§14)。dd_slice 交互切片未做。
This commit is contained in:
gaozheng 2026-06-08 09:05:27 +08:00
parent 2d39a3af26
commit 9b77d07359
9 changed files with 314 additions and 9 deletions

View File

@ -16,10 +16,10 @@
- **左 对象树**:GS→TM→DS,复选框勾选 → 控制中央显示哪些测线。
- **中央 二维地图 / 三维视图**(两个**真内容**,非相机切换):
- 二维地图 = `MapLineActor`:测线 `lat/lon` 轨迹**红线**俯视(浅底),像地图。
- 三维视图 = `CurtainActor`:沿测线的**竖直断面墙**(分段色带,z 纵向夸张×3,沿弯曲测线弯)。
- 三维视图 = `CurtainActor`:沿测线的**竖直断面墙**(分段色带,z 纵向夸张×3,沿弯曲测线弯)。工具条「体素」开关(仅 3D)= `buildVoxelFromScatters`:两交叉测线散点经 **EPSG:4547→GeoLocalFrame** 配准 + IDW 成体素(十字片),与帘面同纵向夸张叠加(派生产物;2 线→薄十字片,可信满体需≥3线)。
- **下方 数据详情**:工具条「原数据/网格数据」切换 +「显示异常」开关(对齐原型命名)。单击数据集 → 网格数据=`GridContourActor` 平面剖面(#18,colorBar 真实非均匀分段值上色,纵向夸张×1.5);原数据=`ScatterActor` 彩色方块散点(#17,x=距离/y=深度取负,用散点自带色阶);显示异常=`AnomalyActor` 在上图叠加异常 dashed 折线(同纵向夸张对齐)。
- **右 属性**:名称/网格 nx×ny/vmin·vmax。
- 单元测试累计 **35 个全绿**(core/data/net/render;含 Scatter 2 + Anomaly 4、修复了陈旧的 Curtain mapper 类型断言);离屏 `verify_section/map/curtain_3d/scatter/section_anomaly.png` 均核对正确(scatter 吻合 ref_17、异常折线位置吻合 ref_18)。
- 单元测试累计 **36 个全绿**(core/data/net/render;含 Scatter 2 + Anomaly 4 + VoxelRegister 1、修复了陈旧的 Curtain mapper 类型断言);离屏 `verify_section/map/curtain_3d/scatter/section_anomaly/voxel_top/voxel_3d.png` 均核对正确(scatter 吻合 ref_17、异常吻合 ref_18、体素 footprint 吻合 ref voxel_hslice 的两臂支撑)。
## 2. 各 Phase 完成度
@ -29,7 +29,7 @@
| P1 | core(LocalFrame/模型/ColorScale/IDW/CrsTransform/GeoLocalFrame) | ✅ |
| P2 | data(解析器/LocalSampleRepository)+ 对象树 | ✅ |
| P3 | 登录(RsaEncryptor/ApiClient/AuthService/LoginWindow) | ✅(**Credential 记住免登录未做**) |
| P4 | 渲染:render 层 + 二维地图(线)+ 三维视图(帘面)+ 数据详情(#18/#17/异常) | 🔶 **核心三视图 + 散点#17 + 异常叠加 已对**;**DEM地形 / dd_voxel回归 / 底图瓦片 未做;布局对齐原型(左下数据列表/右上异常列表/电极/底图)未做** |
| P4 | 渲染:render 层 + 二维地图(线)+ 三维视图(帘面/体素)+ 数据详情(#18/#17/异常) | 🔶 **三视图 + 散点#17 + 异常叠加 + dd_voxel回归 已对**;**DEM地形(需加gdal) / 底图瓦片 未做;交互切片(dd_slice)未做;布局对齐原型(左下数据列表/右上异常列表/电极/底图)未做** |
## 3. 构建约定(**机器本地**
@ -67,7 +67,7 @@
1. ~~**散点 #17**:`ScatterActor`(剖面原数据 2597 点彩色散点),数据详情"原数据"视图~~**已完成**(2026-06-08,离屏 PNG 核对吻合 Python 真值,接入数据详情「反演剖面/原数据」切换;app 待人工登录肉眼复核交互)。
2. ~~**异常叠加**:`AnomalyActor`(markType 点/线/面)~~**已完成**(2026-06-08,叠加在数据详情 #18/#17 上,「显示异常」开关默认开;离屏 `verify_section_anomaly.png` 折线位置吻合 ref_18;样本 3 异常均 markType=2 dashed;app 待人工登录复核)。**注**:dashed 点画在 VTK OpenGL2 下偏弱(几乎实线),几何/颜色/位置正确,纯观感项可后续调。
3. **DEM/影像地形**:加 vcpkg `gdal`;GDAL 读 dem.tif/image.tif;**影像 EPSG:3857 必须 PROJ 重投影到世界系**;`vtkWarpScalar` 地形面 + 纹理。
4. **dd_voxel 回归**:需先确认项目 CRS,使散点 projX/Y 能转到 lat/lon 世界系,与帘面配准;VoxelActor 已就绪
4. ~~**dd_voxel 回归**~~**已完成**(2026-06-08,CRS 已定 EPSG:4547)。`render::buildVoxelFromScatters`:散点 projX/Y→4547→4326→GeoLocalFrame 配准 + IDW(maxDist 裁剪)→ `buildVoxel`;接入 app 3D 视图「体素」开关(默认关,与帘面同纵向夸张)。离屏 `verify_voxel_top.png` 两臂支撑吻合 ref voxel_hslice、`verify_voxel_3d.png` profile1 片贴合帘面;+1 单测(VoxelRegister)。**注**:仅 2 交叉线→薄十字片(15.9% 充填),体绘制半透明偏淡(低不透明度固有);可信满体需≥3线(设计 §10/§14)。**dd_slice 交互切片未做**(buildVoxel 已暴露 image 供后续 reslice widget)。app 待人工登录复核 + 运行时须 PROJ_DATA(main() 已按候选路径自动设;部署须随包附带 proj 数据)
5. **底图瓦片**(二维地图,天地图/Mapbox):M1.5。
6. **Credential(QtKeychain)**:记住一个月免登录持久化(P3 Task2 未做)。
7. 多测线:当前样本仅 1 条 dd_section(grid1);多条共存机制已就绪,加数据即叠加。
@ -94,5 +94,5 @@ cmd /c "...\external\dev.bat cmake --build build/release --target render_verify"
- 计划:`plans/` 下 phase0-4 + `2026-06-07-m1-view-redesign.md`(正确视图模型)+ spike-report
- 环境:`../ENV_SETUP_Windows.md`
- 验证脚本:`tools/validate_samples.py`(#17/#18 真值)、`tools/validate_voxel.py`(voxel)、`tests/spike/render_verify.cpp`(app 渲染积木离屏核对)
- 渲染积木(render 层):`MapLineActor`(测线线)、`CurtainActor`(断面墙)、`GridContourActor`(#18 平面)、`ScatterActor`(#17 散点)、`AnomalyActor`(异常 点/线/面)、`VoxelActor`(体,未接 UI)、`ColorLutBuilder`、`CameraPreset`、`Scene`;`core::GeoLocalFrame`(经纬→局部米)。
- 渲染积木(render 层):`MapLineActor`(测线线)、`CurtainActor`(断面墙)、`GridContourActor`(#18 平面)、`ScatterActor`(#17 散点)、`AnomalyActor`(异常 点/线/面)、`VoxelActor`(`buildVoxel` 体绘制)、`buildVoxelFromScatters`(散点→CRS配准→IDW→体素,VoxelFromScatters.hpp)、`ColorLutBuilder`、`CameraPreset`、`Scene`;`core::GeoLocalFrame`(经纬→局部米)、`core::CrsTransform`(PROJ,EPSG:4547↔4326)。
- 散点 #17 专属色阶经 `LocalSampleRepository::loadScatterColorScale`(剖面原数据自带色阶,范围/分段与网格色阶不同)。

View File

@ -4,7 +4,7 @@
> **⚠️ 状态(2026-06-07,务必先读 `2026-06-07-m1-view-redesign.md` + STATUS.md):**
> - **Task 1(render 层 + 相机预设)**:✅ 已完成。但中央"单场景平躺剖面 + 2D/3D 仅换相机"的做法**已被 view-redesign 取代** —— 现在中央是 **二维地图(测线线)/ 三维视图(竖直帘面)两种内容**(详见 view-redesign 的"实施结果")。
> - **Task 2(dd_voxel 体绘制+切片)**:🔶 VoxelActor 代码完成,但**已从 UI 移除/搁置**(散点 projX/Y 真实 CRS 未确认,无法与 lat/lon 帘面配准)。**CRS 确认后再回归**
> - **Task 2(dd_voxel 体绘制+切片)**:🔶 **体绘制+回归 ✅ 已完成**(2026-06-08,CRS 已实证定 EPSG:4547)。`buildVoxelFromScatters`(散点→4547→4326→GeoLocalFrame 配准 + IDW)→ `buildVoxel`;接入 app 3D「体素」开关。离屏 PNG 两臂支撑吻合 ref voxel_hslice、profile1 片贴合帘面;+1 单测。**dd_slice 交互切片 ⬜ 未做**(image 已暴露供 reslice widget)
> - **Task 3(散点#17 + 异常叠加)**:✅ **已完成**(2026-06-08)。散点#17=`ScatterActor`(吻合 ref_17);异常叠加=`AnomalyActor`(点/线/面,叠在数据详情#18/#17,「显示异常」开关,吻合 ref_18)。数据详情切换按原型命名「原数据/网格数据」。+6 单测,全 35 测试绿;app 待人工登录复核。布局对齐原型其余项见 STATUS §6.10。
> - **Task 4(DEM/影像地形)**:⬜ 未做(P4 下次,需先确认 CRS + 加 gdal)。
> 渲染必须用 `tests/spike/render_verify.cpp` 离屏 PNG 核对(本会话教训)。

View File

@ -3,6 +3,8 @@
// - 中央「二维地图 / 三维视图」:两个互斥视图(内容不同,不是同一物体换相机)。
// 二维地图 = 对每个勾选数据集 buildSurveyLinelat/lon 红线俯视z=0+ applyTop2D浅底背景
// 三维视图 = 对每个勾选数据集 buildCurtain竖直断面墙actor SetScale(1,1,3) 纵向夸张 + applyFree3D白底
// 工具条「体素」开关(仅三维有效)= 两交叉测线散点经 EPSG:4547→GeoLocalFrame 配准 IDW 成体素,
// 与帘面同纵向夸张叠加(派生产物,非对象树节点;输入仅 2 线→薄十字片可信满体需≥3线
// 切视图 / 勾选变化 → 按当前勾选集重建对应内容。
// - 下方「数据详情」:独立 QVTK 小视图 + 工具条「原数据 / 网格数据」切换 +「显示异常」开关(对齐原型)。
// 单击某 DS → 显示该数据集:
@ -22,7 +24,9 @@
#include <QActionGroup>
#include <QApplication>
#include <QDialog>
#include <QFile>
#include <QLabel>
#include <QStringList>
#include <QMainWindow>
#include <QSurfaceFormat>
#include <QToolBar>
@ -44,15 +48,19 @@
#include "CameraPreset.hpp"
#include "Scene.hpp"
#include "VoxelFromScatters.hpp"
#include "actors/AnomalyActor.hpp"
#include "actors/CurtainActor.hpp"
#include "actors/GridContourActor.hpp"
#include "actors/MapLineActor.hpp"
#include "actors/ScatterActor.hpp"
#include "geo/CrsTransform.hpp"
#include "geo/GeoLocalFrame.hpp"
#include <algorithm>
#include <exception>
#include <memory>
#include <vector>
#include <QVTKOpenGLStereoWidget.h>
@ -124,6 +132,10 @@ constexpr float kScatterPointSize = 4.0F;
constexpr double kCurtainZScale = 3.0;
constexpr double kDetailYScale = 1.5;
// 项目 CRS(实证确定,STATUS §4):散点 projX/Y -> 经纬,再经 GeoLocalFrame 配准体素到世界系。
constexpr const char* kProjectCrs = "EPSG:4547"; // CGCS2000 / 3-degree GK CM 114E
constexpr const char* kWgs84 = "EPSG:4326";
// 在给定 QMainWindow 上构建 M1 工作台。
// repo 生命周期须覆盖到事件循环结束(由调用方保证)。
void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& repo)
@ -150,6 +162,15 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
// 当前视图模式(全局共享,切视图/勾选时据此重建内容)。默认二维地图。
auto viewMode = std::make_shared<ViewMode>(ViewMode::Map2D);
// 体素显隐 + 项目 CRS→WGS84 变换(体素配准用)。PROJ 失败则置空 → 体素开关无效(不崩)。
auto showVoxel = std::make_shared<bool>(false);
std::shared_ptr<geopro::core::CrsTransform> crs;
try {
crs = std::make_shared<geopro::core::CrsTransform>(kProjectCrs, kWgs84);
} catch (const std::exception&) {
crs.reset();
}
auto* dockManager = new ads::CDockManager(&window);
window.setCentralWidget(dockManager);
@ -170,6 +191,11 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
viewGroup->addAction(act2D);
viewGroup->addAction(act3D);
act2D->setChecked(true); // 默认二维地图
// 「体素」开关:仅三维视图有效,叠加 dd_voxel 体绘制(两交叉测线配准成十字片)。默认关。
viewToolBar->addSeparator();
auto* actVoxel = viewToolBar->addAction(QStringLiteral("体素"));
actVoxel->setCheckable(true);
actVoxel->setChecked(false);
centerLayout->addWidget(viewToolBar);
centerLayout->addWidget(vtkWidget, 1);
@ -239,7 +265,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
// 二维地图 = buildSurveyLine红线俯视浅底背景+ applyTop2D。
// 三维视图 = buildCurtain断面墙SetScale(1,1,kCurtainZScale) + applyFree3D白底
// frame 全局共享;切视图/勾选变化都调用此函数重建当前视图。
auto rebuildCentral = [scene, rendererPtr, renderWindowPtr, viewMode, &repo, frame, tree]() {
auto rebuildCentral = [scene, rendererPtr, renderWindowPtr, viewMode, &repo, frame, tree,
showVoxel, crs]() {
scene->clear();
const bool is2D = (*viewMode == ViewMode::Map2D);
@ -273,6 +300,18 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
}
}
// 三维视图 + 体素开关:两交叉测线散点经 CRS 配准 IDW 成体素(十字片)
// 与帘面同纵向夸张对齐叠加(派生产物,非对象树节点)。
if (!is2D && *showVoxel && crs) {
const auto profs = repo.loadVoxelScatters();
const auto vcs = repo.loadScatterColorScale("grid1");
auto vr = geopro::render::buildVoxelFromScatters(profs, vcs, *crs, *frame);
if (vr.valid()) {
vr.volume->SetScale(1.0, 1.0, kCurtainZScale); // 与帘面同纵向夸张
rendererPtr->AddVolume(vr.volume);
}
}
if (is2D)
geopro::render::applyTop2D(rendererPtr);
else
@ -389,6 +428,11 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
*viewMode = ViewMode::View3D;
rebuildCentral();
});
// 「体素」开关:切换 dd_voxel 叠加 → 重建中央(仅三维视图可见)。
QObject::connect(actVoxel, &QAction::toggled, vtkWidget, [showVoxel, rebuildCentral](bool on) {
*showVoxel = on;
rebuildCentral();
});
// ── 启动默认dd_section 已勾选,但 itemChanged 在 connect 之前触发故未渲染。
// 这里 connect 之后主动按默认视图(二维地图)重建一次中央内容。
@ -403,6 +447,24 @@ int main(int argc, char* argv[])
QSurfaceFormat::setDefaultFormat(QVTKOpenGLStereoWidget::defaultFormat());
QApplication app(argc, argv);
// PROJ 数据(proj.db)定位:体素配准的 CrsTransform 需要。优先已设环境变量;
// 否则按 exe 旁 / 构建目录候选设置。部署时须随包附带 proj 数据并设此变量。
if (qEnvironmentVariableIsEmpty("PROJ_DATA")) {
const QString appDir = QCoreApplication::applicationDirPath();
const QStringList candidates = {
appDir + "/proj",
appDir + "/../../vcpkg_installed/x64-windows/share/proj",
QStringLiteral(
"D:/Git/lanbingtech/geopro/build/release/vcpkg_installed/x64-windows/share/proj"),
};
for (const auto& c : candidates) {
if (QFile::exists(c + "/proj.db")) {
qputenv("PROJ_DATA", c.toUtf8());
break;
}
}
}
// 网络层:共享会话 ApiClient + 登录编排 AuthServiceRSA 公钥从 resources 读取)。
geopro::net::ApiClient api(QStringLiteral("http://tenant.geomative.cn/pop-api"));
const std::string pem = readPem("D:/Git/lanbingtech/geopro/resources/rsa_public_key.pem");

View File

@ -1,6 +1,6 @@
find_package(VTK REQUIRED COMPONENTS CommonCore CommonDataModel FiltersGeometry FiltersModeling RenderingCore RenderingOpenGL2 RenderingVolumeOpenGL2 InteractionStyle InteractionWidgets)
add_library(geopro_render STATIC
Scene.cpp ColorLutBuilder.cpp CameraPreset.cpp actors/GridContourActor.cpp actors/VoxelActor.cpp actors/CurtainActor.cpp actors/MapLineActor.cpp actors/ScatterActor.cpp actors/AnomalyActor.cpp)
Scene.cpp ColorLutBuilder.cpp CameraPreset.cpp VoxelFromScatters.cpp actors/GridContourActor.cpp actors/VoxelActor.cpp actors/CurtainActor.cpp actors/MapLineActor.cpp actors/ScatterActor.cpp actors/AnomalyActor.cpp)
target_include_directories(geopro_render PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(geopro_render PUBLIC geopro_core ${VTK_LIBRARIES})
target_compile_features(geopro_render PUBLIC cxx_std_17)

View File

@ -0,0 +1,99 @@
#include "VoxelFromScatters.hpp"
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <limits>
#include "actors/VoxelActor.hpp"
#include "algo/IdwInterpolator.hpp"
namespace geopro::render {
namespace {
// 体素网格各维上限:防止退化输入(如坐标异常)撑出超大体拖垮内存/渲染。
constexpr int kMaxDim = 400;
// 钳制维度到 [1, kMaxDim]。
int clampDim(double ext, double cell)
{
int n = static_cast<int>(ext / cell) + 1;
if (n < 1) n = 1;
if (n > kMaxDim) n = kMaxDim;
return n;
}
} // namespace
VoxelResult buildVoxelFromScatters(const std::vector<geopro::core::ScatterField>& profiles,
const geopro::core::ColorScale& cs,
const geopro::core::CrsTransform& crs,
const geopro::core::GeoLocalFrame& frame,
double cellXY,
double cellZ,
double power,
double maxDist)
{
// 1) 配准所有点到世界局部米 + 深度,组装 IDW 输入点集。
geopro::core::PointSet pts;
for (const auto& s : profiles) {
const std::size_t n = s.v.size();
if (s.projX.size() < n || s.projY.size() < n || s.y.size() < n) continue; // 字段不齐跳过
for (std::size_t i = 0; i < n; ++i) {
const auto ll = crs.forward(s.projX[i], s.projY[i]); // (lon, lat)always_xy
const auto local = frame.toLocal(ll.y, ll.x); // (x East, y North) 米
pts.x.push_back(local.x);
pts.y.push_back(local.y);
pts.z.push_back(-s.y[i]); // 深度向下z 取负(与帘面一致)
pts.v.push_back(s.v[i]);
}
}
if (pts.v.empty()) return VoxelResult{};
// 2) 由点集包络定 GridSpec(角点对齐,与 IDW/vtkImageData 一致)。
double minx = pts.x[0], maxx = pts.x[0];
double miny = pts.y[0], maxy = pts.y[0];
double minz = pts.z[0], maxz = pts.z[0];
for (std::size_t i = 1; i < pts.v.size(); ++i) {
minx = std::min(minx, pts.x[i]); maxx = std::max(maxx, pts.x[i]);
miny = std::min(miny, pts.y[i]); maxy = std::max(maxy, pts.y[i]);
minz = std::min(minz, pts.z[i]); maxz = std::max(maxz, pts.z[i]);
}
geopro::core::GridSpec spec{};
spec.ox = minx; spec.oy = miny; spec.oz = minz;
spec.dx = cellXY; spec.dy = cellXY; spec.dz = cellZ;
spec.nx = clampDim(maxx - minx, cellXY);
spec.ny = clampDim(maxy - miny, cellXY);
spec.nz = clampDim(maxz - minz, cellZ);
spec.power = power;
spec.maxDist = maxDist;
// 3) IDW → ScalarVolume(maxDist 外 NaN 留空)。
const geopro::core::IdwInterpolator idw;
const geopro::core::ScalarVolume vol = idw.interpolate(pts, spec);
// 4) 色阶范围:优先 colorBar 真实分段值,否则数据实测。
double vmin, vmax;
const std::vector<double> stops = cs.stopValues();
if (stops.size() >= 2) {
vmin = stops.front(); vmax = stops.back();
} else {
vmin = std::numeric_limits<double>::infinity();
vmax = -std::numeric_limits<double>::infinity();
for (double v : vol.data()) {
if (std::isnan(v)) continue;
vmin = std::min(vmin, v); vmax = std::max(vmax, v);
}
if (!(vmin < vmax)) { vmin = 0.0; vmax = 1.0; }
}
// 5) 体绘制 + 暴露 image(供切片)。
VoxelResult out;
out.volume = buildVoxel(vol, cs, spec.ox, spec.oy, spec.oz, spec.dx, spec.dy, spec.dz, vmin,
vmax, out.image);
return out;
}
} // namespace geopro::render

View File

@ -0,0 +1,37 @@
#pragma once
#include <vector>
#include <vtkImageData.h>
#include <vtkSmartPointer.h>
#include <vtkVolume.h>
#include "geo/CrsTransform.hpp"
#include "geo/GeoLocalFrame.hpp"
#include "model/ColorScale.hpp"
#include "model/Field.hpp"
namespace geopro::render {
// dd_voxel 体素结果:体绘制 volume + 内部 image(含 origin/spacing供交互切片)。
struct VoxelResult {
vtkSmartPointer<vtkVolume> volume;
vtkSmartPointer<vtkImageData> image;
bool valid() const { return volume != nullptr && image != nullptr; }
};
// 把若干测线散点(GIS projX/projY + 深度 ylist)配准到 GeoLocalFrame 世界系并 IDW 成体素。
// 配准:(projX,projY) --crs(项目CRS→EPSG:4326)--> (lon,lat) --frame--> 局部米(x East,y North);
// 垂向 z = -ylist(深度向下,与帘面 z 取负一致)。
// IDWmaxDist 裁剪约束插值域(两交叉测线→十字片,设计 §10)NaN 留空→体绘制透明。
// crs 须为「项目 CRS(如 EPSG:4547) → EPSG:4326」frame 与帘面/地图共用以保证空间配准。
// 输入不足(无点)返回 valid()==false 的空结果。
VoxelResult buildVoxelFromScatters(const std::vector<geopro::core::ScatterField>& profiles,
const geopro::core::ColorScale& cs,
const geopro::core::CrsTransform& crs,
const geopro::core::GeoLocalFrame& frame,
double cellXY = 1.0,
double cellZ = 0.5,
double power = 2.0,
double maxDist = 4.0);
} // namespace geopro::render

View File

@ -62,6 +62,8 @@ find_package(VTK REQUIRED COMPONENTS CommonCore CommonDataModel RenderingCore)
target_sources(geopro_tests PRIVATE render/test_color_lut.cpp)
# dd_voxelbuildVoxel(ScalarVolume->vtkImageData->GPU 体绘制) + dims
target_sources(geopro_tests PRIVATE render/test_voxel_build.cpp)
# dd_voxel buildVoxelFromScatters(散点 projX/Y -EPSG:4547-> 世界系 + IDW) +充填( PROJ_DATA)
target_sources(geopro_tests PRIVATE render/test_voxel_register.cpp)
# CurtainbuildCurtain(Grid+GeoLocalFrame->vtkStructuredGrid 帘面) actor + =nx*ny
target_sources(geopro_tests PRIVATE render/test_curtain.cpp)
# Scatter(#17)buildScatter(ScatterField+ColorScale->vtkPolyData 彩色散点) /verts//y

View File

@ -0,0 +1,71 @@
#include <gtest/gtest.h>
#include <vtkImageData.h>
#include "VoxelFromScatters.hpp"
#include "geo/CrsTransform.hpp"
#include "geo/GeoLocalFrame.hpp"
#include "model/ColorScale.hpp"
#include "model/Field.hpp"
using namespace geopro::core;
// buildVoxelFromScatters: 两条剖面散点(项目 CRS EPSG:4547) 配准到 GeoLocalFrame 世界系
// + IDW 成体素。验证valid、image dims/spacing 合理、原点附近格被充填。
// (需 PROJ_DATAtests CMake 已注入环境变量。)
TEST(VoxelRegister, BuildsRegisteredVoxelFromTwoProfiles) {
// 香港测区附近(EPSG:4547 CM114E)的两组点,构造交叉两臂。
// profile1 沿 East 方向、profile2 沿 North 方向,深度 0..-10。
const double px0 = 516870.0, py0 = 2494270.0;
ScatterField p1, p2;
for (int i = 0; i < 11; ++i) {
const double t = i; // 0..10 米
// p1projX 递增(East 向)
p1.projX.push_back(px0 + t);
p1.projY.push_back(py0);
p1.y.push_back(t); // ylist=深度
p1.v.push_back(100.0 + t);
// p2projY 递增(North 向)
p2.projX.push_back(px0);
p2.projY.push_back(py0 + t);
p2.y.push_back(t);
p2.v.push_back(200.0 + t);
}
ColorScale cs;
cs.addStop(100.0, Rgba{0, 0, 255, 255});
cs.addStop(210.0, Rgba{255, 0, 0, 255});
CrsTransform crs("EPSG:4547", "EPSG:4326");
// 世界系原点取测区附近经纬(由 px0/py0 反算)。
const auto ll0 = crs.forward(px0, py0); // (lon, lat)
GeoLocalFrame frame(ll0.y, ll0.x);
auto vr = geopro::render::buildVoxelFromScatters({p1, p2}, cs, crs, frame,
/*cellXY*/ 1.0, /*cellZ*/ 1.0,
/*power*/ 2.0, /*maxDist*/ 3.0);
ASSERT_TRUE(vr.valid());
int dims[3];
vr.image->GetDimensions(dims);
// 两臂各 ~10m → 水平 ~10x10深度 0..-10 → ~11。合理上界检查。
EXPECT_GE(dims[0], 5);
EXPECT_GE(dims[1], 5);
EXPECT_GE(dims[2], 5);
EXPECT_LT(dims[0], 50);
EXPECT_LT(dims[1], 50);
EXPECT_LT(dims[2], 50);
double sp[3];
vr.image->GetSpacing(sp);
EXPECT_DOUBLE_EQ(sp[0], 1.0);
EXPECT_DOUBLE_EQ(sp[2], 1.0);
// 体内应有被充填(非哨兵)的格:哨兵 = vmin-1 = 99。统计 > 99.5 的格数 > 0。
int filled = 0;
for (int k = 0; k < dims[2]; ++k)
for (int j = 0; j < dims[1]; ++j)
for (int i = 0; i < dims[0]; ++i)
if (vr.image->GetScalarComponentAsDouble(i, j, k, 0) > 99.5) ++filled;
EXPECT_GT(filled, 0);
}

View File

@ -17,14 +17,18 @@
#include "CameraPreset.hpp"
#include "ColorLutBuilder.hpp"
#include "VoxelFromScatters.hpp"
#include "actors/AnomalyActor.hpp"
#include "actors/CurtainActor.hpp"
#include "actors/GridContourActor.hpp"
#include "actors/AnomalyActor.hpp"
#include "actors/MapLineActor.hpp"
#include "actors/ScatterActor.hpp"
#include "geo/CrsTransform.hpp"
#include "geo/GeoLocalFrame.hpp"
#include "parse/SampleParsers.hpp"
#include <vtkVolume.h>
static std::string slurp(const char* p) {
std::ifstream f(p);
std::stringstream s; s << f.rdbuf(); return s.str();
@ -111,6 +115,36 @@ int main() {
std::printf("ANOMALY n=%zu\n", anomalies.size());
}
// 6) dd_voxel — 两交叉剖面散点经 EPSG:4547→4326→GeoLocalFrame 配准 + IDW 成体素,
// 叠一条帘面(grid1,同系)做空间参照; 透视核对"十字片"与帘面配准(profile1 片应贴合帘面)。
{
std::vector<core::ScatterField> profs;
profs.push_back(data::parseScatter(slurp((dir + "scatter.json").c_str())));
profs.push_back(data::parseScatter(slurp((dir + "scatter2.json").c_str())));
core::ColorScale scs = data::parseColorScale(slurp((dir + "scatter_colorbar.json").c_str()));
core::CrsTransform crs("EPSG:4547", "EPSG:4326");
auto vr = render::buildVoxelFromScatters(profs, scs, crs, frame);
// 灰底使半透明体绘制可见(白底会冲淡)。3D体素 + 帘面(同系)核对 profile1 片贴合帘面。
vtkNew<vtkRenderer> ren; ren->SetBackground(0.22, 0.22, 0.26);
if (vr.valid()) ren->AddVolume(vr.volume);
auto curt = render::buildCurtain(g, cs, frame); // 原始米(不夸张)与体素同系
if (curt) ren->AddActor(curt);
render::applyFree3D(ren);
renderToPng(ren, (dir + "verify_voxel_3d.png").c_str(), 700, 500);
// 俯视:体素 footprint 应呈两测线相交的"X"(十字片)。
vtkNew<vtkRenderer> renTop; renTop->SetBackground(0.22, 0.22, 0.26);
if (vr.valid()) renTop->AddVolume(vr.volume);
render::applyTop2D(renTop);
renderToPng(renTop, (dir + "verify_voxel_top.png").c_str(), 600, 500);
int dims[3] = {0, 0, 0};
if (vr.valid()) vr.image->GetDimensions(dims);
std::printf("VOXEL valid=%d dims=%dx%dx%d pts=%zu\n", vr.valid() ? 1 : 0,
dims[0], dims[1], dims[2], profs[0].v.size() + profs[1].v.size());
}
std::printf("RENDER_VERIFY_DONE grid=%dx%d lat0=%.5f lon0=%.5f\n", g.nx(), g.ny(), lat0, lon0);
return 0;
}