diff --git a/docs/superpowers/STATUS.md b/docs/superpowers/STATUS.md index aa78c49..536ec1d 100644 --- a/docs/superpowers/STATUS.md +++ b/docs/superpowers/STATUS.md @@ -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`(剖面原数据自带色阶,范围/分段与网格色阶不同)。 diff --git a/docs/superpowers/plans/2026-06-07-m1-phase4-render.md b/docs/superpowers/plans/2026-06-07-m1-phase4-render.md index 54cea83..ee37075 100644 --- a/docs/superpowers/plans/2026-06-07-m1-phase4-render.md +++ b/docs/superpowers/plans/2026-06-07-m1-phase4-render.md @@ -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 核对(本会话教训)。 diff --git a/src/app/main.cpp b/src/app/main.cpp index 5ba5a61..a83618b 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -3,6 +3,8 @@ // - 中央「二维地图 / 三维视图」:两个互斥视图(内容不同,不是同一物体换相机)。 // 二维地图 = 对每个勾选数据集 buildSurveyLine(lat/lon 红线俯视,z=0)+ applyTop2D(浅底背景)。 // 三维视图 = 对每个勾选数据集 buildCurtain(竖直断面墙),actor SetScale(1,1,3) 纵向夸张 + applyFree3D(白底)。 +// 工具条「体素」开关(仅三维有效)= 两交叉测线散点经 EPSG:4547→GeoLocalFrame 配准 IDW 成体素, +// 与帘面同纵向夸张叠加(派生产物,非对象树节点;输入仅 2 线→薄十字片,可信满体需≥3线)。 // 切视图 / 勾选变化 → 按当前勾选集重建对应内容。 // - 下方「数据详情」:独立 QVTK 小视图 + 工具条「原数据 / 网格数据」切换 +「显示异常」开关(对齐原型)。 // 单击某 DS → 显示该数据集: @@ -22,7 +24,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -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 +#include +#include #include #include @@ -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::Map2D); + // 体素显隐 + 项目 CRS→WGS84 变换(体素配准用)。PROJ 失败则置空 → 体素开关无效(不崩)。 + auto showVoxel = std::make_shared(false); + std::shared_ptr crs; + try { + crs = std::make_shared(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 + 登录编排 AuthService(RSA 公钥从 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"); diff --git a/src/render/CMakeLists.txt b/src/render/CMakeLists.txt index 84736e5..b6ef9ba 100644 --- a/src/render/CMakeLists.txt +++ b/src/render/CMakeLists.txt @@ -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) diff --git a/src/render/VoxelFromScatters.cpp b/src/render/VoxelFromScatters.cpp new file mode 100644 index 0000000..3a48667 --- /dev/null +++ b/src/render/VoxelFromScatters.cpp @@ -0,0 +1,99 @@ +#include "VoxelFromScatters.hpp" + +#include +#include +#include +#include + +#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(ext / cell) + 1; + if (n < 1) n = 1; + if (n > kMaxDim) n = kMaxDim; + return n; +} + +} // namespace + +VoxelResult buildVoxelFromScatters(const std::vector& 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 stops = cs.stopValues(); + if (stops.size() >= 2) { + vmin = stops.front(); vmax = stops.back(); + } else { + vmin = std::numeric_limits::infinity(); + vmax = -std::numeric_limits::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 diff --git a/src/render/VoxelFromScatters.hpp b/src/render/VoxelFromScatters.hpp new file mode 100644 index 0000000..c98945b --- /dev/null +++ b/src/render/VoxelFromScatters.hpp @@ -0,0 +1,37 @@ +#pragma once +#include + +#include +#include +#include + +#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 volume; + vtkSmartPointer 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 取负一致)。 +// IDW:maxDist 裁剪约束插值域(两交叉测线→十字片,设计 §10);NaN 留空→体绘制透明。 +// crs 须为「项目 CRS(如 EPSG:4547) → EPSG:4326」;frame 与帘面/地图共用以保证空间配准。 +// 输入不足(无点)返回 valid()==false 的空结果。 +VoxelResult buildVoxelFromScatters(const std::vector& 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 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0f4ca68..885d6e2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -62,6 +62,8 @@ find_package(VTK REQUIRED COMPONENTS CommonCore CommonDataModel RenderingCore) target_sources(geopro_tests PRIVATE render/test_color_lut.cpp) # dd_voxel:buildVoxel(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) # Curtain:buildCurtain(Grid+GeoLocalFrame->vtkStructuredGrid 帘面) 非空 actor + 点数=nx*ny。 target_sources(geopro_tests PRIVATE render/test_curtain.cpp) # Scatter(#17):buildScatter(ScatterField+ColorScale->vtkPolyData 彩色散点) 点数/verts/上色/y取负。 diff --git a/tests/render/test_voxel_register.cpp b/tests/render/test_voxel_register.cpp new file mode 100644 index 0000000..a709ef3 --- /dev/null +++ b/tests/render/test_voxel_register.cpp @@ -0,0 +1,71 @@ +#include + +#include + +#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_DATA:tests 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 米 + // p1:projX 递增(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); + // p2:projY 递增(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); +} diff --git a/tests/spike/render_verify.cpp b/tests/spike/render_verify.cpp index 5af30e6..5bc15f2 100644 --- a/tests/spike/render_verify.cpp +++ b/tests/spike/render_verify.cpp @@ -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 + 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 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 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 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; }