feat(render): DEM 地形+影像贴图(spec ④) + dd_slice 交互切片
- TerrainActor(buildTerrain): GDAL 读 dem.tif(高程)+ image.tif(影像); DEM CRS→4326→ GeoLocalFrame 配准成 vtkStructuredGrid warp 面; 影像经 GDAL 读像素(行翻转正立)作纹理, 按经纬→EPSG:3857→像素 算纹理坐标贴图(影像/DEM 异源 CRS 重投影对位)。影像读失败→按高程上色。 离屏 verify_terrain_3d.png 核对: 卫星影像正立贴微起伏面、配准对位。+2 单测。 - 注: 影像须 GDAL 读(vtkTIFFReader 对此压缩 TIFF 报错"reading the row")。 - dd_slice: 3D「视图详情」加「切片」图层 = vtkImagePlaneWidget 在体素 image 拖切面(spec M1-b)。 - 接入 app: 3D 浮层五图层(帘面/体素/切片/地形); repo.demPath()/imagePath(); PROJ 不可用则禁用。 - vcpkg 加 gdal(连带 hdf5/netcdf/geos 等, 已缓存); 全 40 测试绿; app 构建干净。 - 注: 地形/切片 Z 基准与帘面/体素纵向夸张未统一(spec M-3 待办); dem 低分辨率→起伏细微。
This commit is contained in:
parent
8466fe3a5a
commit
7007619bf2
|
|
@ -17,11 +17,11 @@
|
|||
- **左下 数据真实显示栏**(对齐原型):单击测线 → 列其采集批次(数据集,tab 数据/文件);单击采集批次 → 数据详情+异常列表+属性。启动自动选首测线+首数据集。
|
||||
- **中央 二维地图 / 三维视图**(两个**真内容**,非相机切换):
|
||||
- 二维地图 = `MapLineActor`:测线 `lat/lon` 轨迹**红线**俯视(浅底),像地图。
|
||||
- 三维视图 = `CurtainActor`:沿测线的**竖直断面墙**(分段色带,z 纵向夸张×3,沿弯曲测线弯)。中央工具条**仅**「二维地图/三维视图」。**3D 左上「视图详情」浮层**(对齐原型,仅 3D 显示):图层勾选 **帘面 / 体素**(体素=`buildVoxelFromScatters` 两交叉测线散点经 EPSG:4547 配准 IDW,与帘面同纵向夸张;PROJ 不可用则禁用)。
|
||||
- 三维视图 = `CurtainActor`:沿测线的**竖直断面墙**(分段色带,z 纵向夸张×3,沿弯曲测线弯)。中央工具条**仅**「二维地图/三维视图」。**3D 左上「视图详情」浮层**(对齐原型,仅 3D 显示)五图层勾选:**帘面 / 体素 / 切片 / 地形**(体素=`buildVoxelFromScatters` 散点经 EPSG:4547 配准 IDW;切片=`vtkImagePlaneWidget` 在体素 image 上交互拖切面=dd_slice;地形=`TerrainActor` GDAL 读 DEM 高程面 + 影像 EPSG:3857 重投影贴图;后三者需 PROJ,不可用则禁用)。
|
||||
- **下方 数据详情**:工具条「原数据/网格数据」切换 +「显示异常/显示电极/显示等值线」开关(对齐原型)。单击数据集 → 网格数据=`GridContourActor` 平面剖面(#18,colorBar 真实非均匀分段值上色,纵向夸张×1.5);原数据=`ScatterActor` 彩色方块散点(#17);显示异常=`AnomalyActor` dashed 折线叠加;显示电极=`ElectrodeActor` 顶边 ▼ 标记;显示等值线=#18 黑色等值线显隐(同纵向夸张对齐)。
|
||||
- **右上 异常列表**(对齐原型):单击数据集→列该数据集异常(颜色块+名称(类型)+派生「位置/深/尺寸」),勾选框显隐,与数据详情异常叠加联动(取消勾选→该异常虚线隐藏)。
|
||||
- **右下 属性**:名称/网格 nx×ny/vmin·vmax/异常数。
|
||||
- 单元测试累计 **38 个全绿**(core/data/net/render;含 Scatter 2 + Anomaly 4 + VoxelRegister 1 + Electrode 2、修复了陈旧的 Curtain mapper 类型断言);离屏 `verify_section/map/curtain_3d/scatter/section_anomaly(含电极▼)/voxel_top/voxel_3d.png` 均核对正确。
|
||||
- 单元测试累计 **40 个全绿**(core/data/net/render;含 Scatter 2 + Anomaly 4 + VoxelRegister 1 + Electrode 2 + Terrain 2、修复了陈旧的 Curtain mapper 类型断言);离屏 `verify_section/map/curtain_3d/scatter/section_anomaly(含电极▼)/voxel_top/voxel_3d/terrain_3d(DEM+影像贴图).png` 均核对正确。
|
||||
|
||||
## 2. 各 Phase 完成度
|
||||
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
| P1 | core(LocalFrame/模型/ColorScale/IDW/CrsTransform/GeoLocalFrame) | ✅ |
|
||||
| P2 | data(解析器/LocalSampleRepository)+ 对象树 | ✅ |
|
||||
| P3 | 登录(RsaEncryptor/ApiClient/AuthService/LoginWindow) | ✅(**Credential 记住免登录未做**) |
|
||||
| P4 | 渲染:render 层 + 二维地图(线)+ 三维视图(帘面)+ 数据详情(#18/#17/异常) | 🔶 **三视图 + 散点#17 + 异常叠加 已对; dd_voxel 引擎已验证(UI 未接,待 3D 图层控制)**;**DEM地形(需加gdal) / 底图瓦片 / dd_slice / 布局对齐原型(左下数据列表/右上异常列表/电极/3D图层浮层) 未做** |
|
||||
| P4 | 渲染:render 层 + 二维地图(线)+ 三维(帘面/体素/切片/地形)+ 数据详情(#18/#17/异常/电极) | ✅ **核心全达成**:三视图 + #17 + 异常 + dd_voxel + dd_slice + DEM地形影像 + 原型六面板。**剩**:底图瓦片(M1.5)、数值标签、Credential 免登录、Z 基准统一、dock 透视持久化 |
|
||||
|
||||
## 3. 构建约定(**机器本地**)
|
||||
|
||||
|
|
@ -40,7 +40,8 @@
|
|||
- **构建/测试经** `& cmd /c "D:\Git\lanbingtech\geopro\external\dev.bat <cmd>"`(**PowerShell 调**,Bash 下参数透传坏)。C: 极小→TEMP/构建全在 D:。
|
||||
- **app 构建前先** `taskkill /IM geopro_desktop.exe /F`(运行中 LNK1104 锁 exe)。
|
||||
- 部署:`D:\Qt\6.11.1\msvc2022_64\bin\windeployqt.exe --release <exe>`(找不到 ads dll 的警告无害);VTK/vcpkg dll 由 POST_BUILD `TARGET_RUNTIME_DLLS` 拷。
|
||||
- **离屏渲染验证**:`render_verify` 需 PATH 加 `external\vtk-install\bin` + `build\release\vcpkg_installed\x64-windows\bin` 再运行。
|
||||
- **离屏渲染验证**:`render_verify` 需 PATH 加 `external\vtk-install\bin` + `build\release\vcpkg_installed\x64-windows\bin`;且体素/地形需 `PROJ_DATA=...\vcpkg_installed\x64-windows\share\proj`、`GDAL_DATA=...\share\gdal` 再运行。
|
||||
- **GDAL**:已加 vcpkg `gdal`(首次编译久,连带 hdf5/netcdf/geos 等;已缓存)。app/测试运行时需 PROJ_DATA(app main() 已自动按候选路径设;部署须随包附带 proj/gdal 数据)。
|
||||
- **改源码用 Write 工具,勿用 PowerShell `Set-Content -Encoding UTF8`**(会把中文注释弄乱、断构建)。
|
||||
|
||||
## 4. 关键决策与已核实事实
|
||||
|
|
@ -68,7 +69,8 @@
|
|||
|
||||
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` 地形面 + 纹理。
|
||||
3. ~~**DEM/影像地形**~~ ✅ **已完成**(2026-06-08)。`render::buildTerrain`(GDAL 读 dem.tif 高程 + image.tif 影像;DEM CRS→4326→GeoLocalFrame 配准成 warp 面;影像 EPSG:3857→像素纹理坐标贴图)。离屏 `verify_terrain_3d.png` 卫星影像正确贴微起伏面、方向正立、配准对位。**注**:影像须用 **GDAL 读像素**(vtkTIFFReader 对此压缩 TIFF 报错);+2 单测;接入 app 3D「地形」图层。dem.tif 低分辨率→起伏细微。Z 基准与帘面/体素夸张未统一(spec M-3 待办)。
|
||||
- **dd_slice 交互切片** ✅ 已完成(同日):3D「切片」图层=`vtkImagePlaneWidget` 在体素 image 拖切面(spec M1-b);**注**:切片在 image 原始米坐标,与夸张体绘制有纵向比例差(M-3)。交互项待人工复核。
|
||||
4. **dd_voxel 回归**:✅ **已完成**(2026-06-08,CRS 已定 EPSG:4547)。`render::buildVoxelFromScatters`:散点 projX/Y→4547→4326→GeoLocalFrame 配准 + IDW(maxDist 裁剪)→ `buildVoxel`;离屏 `verify_voxel_top.png` 两臂支撑吻合 ref voxel_hslice、`verify_voxel_3d.png` profile1 片贴合帘面;+1 单测(VoxelRegister,需 PROJ_DATA)。**UI 已接入**(增量3):3D「视图详情」浮层「体素」图层勾选驱动;main() 自动设 PROJ_DATA(部署须随包附带 proj 数据);PROJ 不可用则该层禁用。**注**:仅 2 交叉线→薄十字片(15.9% 充填,半透明偏淡),可信满体需≥3线(设计 §10/§14)。**dd_slice 交互切片未做**(buildVoxel 已暴露 image 供 reslice widget)。
|
||||
5. **底图瓦片**(二维地图,天地图/Mapbox):M1.5。
|
||||
6. **Credential(QtKeychain)**:记住一个月免登录持久化(P3 Task2 未做)。
|
||||
|
|
@ -80,7 +82,7 @@
|
|||
- ✅ **增量2 左下「数据列表」+ 对象树到 TM 层**(2026-06-08,`panels/DatasetListPanel`;树 GS→TM 复选驱动中央, DS 移出树入数据列表 tab 数据/文件, DS 单击→详情+异常+属性, 启动自动选首测线/首数据集;待人工复核)。
|
||||
- ✅ **增量3 3D「视图详情」图层浮层**(2026-06-08,QFrame 浮于 QVTK 左上,仅 3D 显示;帘面/体素图层勾选;体素经此正经接入=之前移除的工具条开关的正确归宿;main() 设 PROJ_DATA;待人工复核浮层渲染+体素显隐)。
|
||||
- 🔶 **增量4 电极标记 + 工具条**(2026-06-08,`ElectrodeActor` 顶边 ▼ PNG 核对吻合; +「显示电极/显示等值线」开关;待人工复核)。**剩**:数值标签、色阶配置/滤波处理(M1.5/进阶)。
|
||||
- 渲染积木累计含 `ElectrodeActor`(顶边 ▼)。底图影像=DEM/底图任务(需 GDAL)。
|
||||
- 渲染积木累计含 `ElectrodeActor`(顶边 ▼)、`TerrainActor`(DEM 高程面 + 影像纹理,GDAL)。底图瓦片=M1.5。
|
||||
- 架构:新面板抽到 `src/app/panels/`(暂随 app 编译,如 login/),控制 main.cpp 体量;后续可升 `src/view/` 库。
|
||||
|
||||
## 7. 渲染验证手段(务必用)
|
||||
|
|
|
|||
|
|
@ -89,7 +89,13 @@
|
|||
- 剖面顶部电极▼标记(grid.lat/lon 或 x 轴电极位);可选数值标签。
|
||||
- 提交 `feat(view): 数据详情电极标记 + 工具条对齐原型`。
|
||||
|
||||
### (增量 5,可选)右下属性面板规范化 + 整体 dock 透视持久化
|
||||
### 增量 5:DEM 地形 + dd_slice ✅ 已完成(2026-06-08, backlog 项随原型 3D 图层一并落地)
|
||||
> `actors/TerrainActor`(GDAL 读 DEM/影像 → 重投影到 GeoLocalFrame → warp 面 + GDAL 读影像像素作纹理;
|
||||
> 影像 EPSG:3857 重投影对位)。离屏 `verify_terrain_3d.png` 核对吻合(卫星影像正立贴面)。+2 单测。
|
||||
> dd_slice = `vtkImagePlaneWidget` 在体素 image 拖切面。两者接入 3D「视图详情」浮层(地形/切片图层)。
|
||||
> 全 40 测试绿。vcpkg 加 gdal。**注**:影像须 GDAL 读(非 vtkTIFFReader);Z 基准统一(M-3)待办。
|
||||
|
||||
### (后续,可选)数值标签 / 色阶配置 / Credential 免登录 / dock 透视持久化 / 底图瓦片(M1.5)
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@
|
|||
// - 中央「二维地图 / 三维视图」:两个互斥视图(内容不同,不是同一物体换相机)。
|
||||
// 二维地图 = 对每个勾选数据集 buildSurveyLine(lat/lon 红线俯视,z=0)+ applyTop2D(浅底背景)。
|
||||
// 三维视图 = 勾选测线的 buildCurtain(竖直断面墙),actor SetScale(1,1,3) 纵向夸张 + applyFree3D(白底)。
|
||||
// 三维左上「视图详情」浮层(对齐原型):图层勾选 帘面 / 体素(dd_voxel,散点经 EPSG:4547 配准 IDW)。
|
||||
// 三维左上「视图详情」浮层(对齐原型):图层勾选 帘面 / 体素(dd_voxel,散点经 EPSG:4547 配准 IDW)
|
||||
// / 切片(dd_slice,vtkImagePlaneWidget 在体素 image 上交互拖切面) / 地形(DEM 高程面 + 影像纹理)。
|
||||
// 切视图 / 勾选变化 / 图层变化 → 重建对应内容。
|
||||
// - 下方「数据详情」:独立 QVTK 小视图 + 工具条「原数据 / 网格数据」切换 +「显示异常」开关(对齐原型)。
|
||||
// 单击某 DS → 显示该数据集:
|
||||
|
|
@ -55,6 +56,7 @@
|
|||
#include "panels/DatasetListPanel.hpp"
|
||||
|
||||
#include "CameraPreset.hpp"
|
||||
#include "ColorLutBuilder.hpp"
|
||||
#include "Scene.hpp"
|
||||
#include "VoxelFromScatters.hpp"
|
||||
#include "actors/AnomalyActor.hpp"
|
||||
|
|
@ -63,6 +65,7 @@
|
|||
#include "actors/GridContourActor.hpp"
|
||||
#include "actors/MapLineActor.hpp"
|
||||
#include "actors/ScatterActor.hpp"
|
||||
#include "actors/TerrainActor.hpp"
|
||||
|
||||
#include "geo/CrsTransform.hpp"
|
||||
#include "geo/GeoLocalFrame.hpp"
|
||||
|
|
@ -75,6 +78,9 @@
|
|||
|
||||
#include <QVTKOpenGLStereoWidget.h>
|
||||
#include <vtkGenericOpenGLRenderWindow.h>
|
||||
#include <vtkImagePlaneWidget.h>
|
||||
#include <vtkLookupTable.h>
|
||||
#include <vtkRenderWindowInteractor.h>
|
||||
#include <vtkRenderer.h>
|
||||
#include <vtkSmartPointer.h>
|
||||
|
||||
|
|
@ -180,6 +186,10 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
|||
// 三维图层显隐(由「视图详情」浮层控制)+ 项目 CRS→WGS84(体素配准)。
|
||||
auto showCurtain = std::make_shared<bool>(true); // 帘面,默认显示
|
||||
auto showVoxel = std::make_shared<bool>(false); // 体素,默认关
|
||||
auto showTerrain = std::make_shared<bool>(false); // 地形(DEM+影像),默认关
|
||||
auto showSlice = std::make_shared<bool>(false); // dd_slice 交互切片,默认关
|
||||
// 持久的切片 widget(挂 interactor,跨重建保活;rebuildCentral 据条件创建/拆除)。
|
||||
auto slicePlane = std::make_shared<vtkSmartPointer<vtkImagePlaneWidget>>();
|
||||
std::shared_ptr<geopro::core::CrsTransform> crs; // PROJ 失败→空→体素层无效(不崩)
|
||||
try {
|
||||
crs = std::make_shared<geopro::core::CrsTransform>(kProjectCrs, kWgs84);
|
||||
|
|
@ -226,13 +236,21 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
|||
chkCurtain->setChecked(true);
|
||||
auto* chkVoxel = new QCheckBox(QStringLiteral("体素(dd_voxel)"));
|
||||
chkVoxel->setChecked(false);
|
||||
if (!crs) { // PROJ 不可用 → 体素层禁用并提示
|
||||
chkVoxel->setEnabled(false);
|
||||
chkVoxel->setToolTip(QStringLiteral("PROJ 数据(proj.db)缺失,体素配准不可用"));
|
||||
auto* chkTerrain = new QCheckBox(QStringLiteral("地形(DEM+影像)"));
|
||||
chkTerrain->setChecked(false);
|
||||
auto* chkSlice = new QCheckBox(QStringLiteral("切片(dd_slice)"));
|
||||
chkSlice->setChecked(false);
|
||||
if (!crs) { // PROJ 不可用 → 体素/切片/地形层(都需配准)禁用并提示
|
||||
const QString tip = QStringLiteral("PROJ 数据(proj.db)缺失,配准不可用");
|
||||
chkVoxel->setEnabled(false); chkVoxel->setToolTip(tip);
|
||||
chkTerrain->setEnabled(false); chkTerrain->setToolTip(tip);
|
||||
chkSlice->setEnabled(false); chkSlice->setToolTip(tip);
|
||||
}
|
||||
layerLayout->addWidget(layerTitle);
|
||||
layerLayout->addWidget(chkCurtain);
|
||||
layerLayout->addWidget(chkVoxel);
|
||||
layerLayout->addWidget(chkSlice);
|
||||
layerLayout->addWidget(chkTerrain);
|
||||
layerPanel->setVisible(false); // 默认二维,不显示图层浮层
|
||||
|
||||
auto* vtkDock = new ads::CDockWidget(QStringLiteral("二维地图/三维视图"));
|
||||
|
|
@ -329,7 +347,10 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
|||
// 三维视图 = buildCurtain(断面墙)SetScale(1,1,kCurtainZScale) + applyFree3D(白底)。
|
||||
// frame/structure 全局共享;切视图/勾选变化都调用此函数重建当前视图。
|
||||
auto rebuildCentral = [scene, rendererPtr, renderWindowPtr, viewMode, &repo, frame, tree,
|
||||
structure, showCurtain, showVoxel, crs]() {
|
||||
structure, showCurtain, showVoxel, showTerrain, showSlice, slicePlane,
|
||||
crs]() {
|
||||
// 先拆除上次的切片 widget(独立于 scene actor,须显式关闭),再按条件重建。
|
||||
if (*slicePlane) { (*slicePlane)->Off(); *slicePlane = nullptr; }
|
||||
scene->clear();
|
||||
|
||||
const bool is2D = (*viewMode == ViewMode::Map2D);
|
||||
|
|
@ -367,15 +388,44 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
|||
if (ds.ddType == "dd_section") renderSection(ds.id);
|
||||
}
|
||||
|
||||
// 三维「体素」图层:两交叉测线散点经 CRS 配准 IDW 成体素(派生层),与帘面同纵向夸张。
|
||||
if (!is2D && *showVoxel && crs) {
|
||||
// 三维「体素 / 切片」图层:两交叉测线散点经 CRS 配准 IDW 成体素。
|
||||
// 体素=GPU 体绘制(与帘面同纵向夸张);切片=vtkImagePlaneWidget 在体素 image 上交互拖切面。
|
||||
// 注:切片 widget 作用于 image 原始米坐标(无 actor 夸张),与夸张后的体绘制存在纵向比例差
|
||||
// (spec M-3 Z 基准统一待办);切片本身演示 dd_slice 交互正确。
|
||||
if (!is2D && (*showVoxel || *showSlice) && crs) {
|
||||
const auto profs = repo.loadVoxelScatters();
|
||||
const auto vcs = repo.loadScatterColorScale("grid1");
|
||||
auto vr = geopro::render::buildVoxelFromScatters(profs, vcs, *crs, *frame);
|
||||
if (vr.valid()) {
|
||||
if (*showVoxel) {
|
||||
vr.volume->SetScale(1.0, 1.0, kCurtainZScale);
|
||||
rendererPtr->AddVolume(vr.volume);
|
||||
}
|
||||
vtkRenderWindowInteractor* interactor = renderWindowPtr->GetInteractor();
|
||||
if (*showSlice && interactor) {
|
||||
const std::vector<double> stops = vcs.stopValues();
|
||||
const double vmn = stops.size() >= 2 ? stops.front() : 0.0;
|
||||
const double vmx = stops.size() >= 2 ? stops.back() : 1.0;
|
||||
auto lut = geopro::render::buildLut(vcs, vmn, vmx, 256);
|
||||
int dims[3] = {1, 1, 1};
|
||||
vr.image->GetDimensions(dims);
|
||||
auto plane = vtkSmartPointer<vtkImagePlaneWidget>::New();
|
||||
plane->SetInteractor(interactor);
|
||||
plane->SetInputData(vr.image);
|
||||
plane->SetPlaneOrientationToXAxes();
|
||||
plane->SetSliceIndex(dims[0] / 2);
|
||||
plane->SetLookupTable(lut);
|
||||
plane->DisplayTextOn();
|
||||
plane->On();
|
||||
*slicePlane = plane;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 三维「地形」图层:GDAL 读 DEM(高程)+影像(EPSG:3857),重投影到世界系,warp 面 + 纹理。
|
||||
if (!is2D && *showTerrain && crs) {
|
||||
auto terr = geopro::render::buildTerrain(repo.demPath(), repo.imagePath(), *frame, 1.0);
|
||||
if (terr) scene->addActor(terr);
|
||||
}
|
||||
|
||||
if (is2D)
|
||||
|
|
@ -581,6 +631,16 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
|||
*showVoxel = on;
|
||||
rebuildCentral();
|
||||
});
|
||||
QObject::connect(chkTerrain, &QCheckBox::toggled, vtkWidget,
|
||||
[showTerrain, rebuildCentral](bool on) {
|
||||
*showTerrain = on;
|
||||
rebuildCentral();
|
||||
});
|
||||
QObject::connect(chkSlice, &QCheckBox::toggled, vtkWidget,
|
||||
[showSlice, rebuildCentral](bool on) {
|
||||
*showSlice = on;
|
||||
rebuildCentral();
|
||||
});
|
||||
|
||||
// ── 启动默认:测线已勾选,但 itemChanged 在 connect 之前触发故未渲染;这里重建一次中央内容。
|
||||
rebuildCentral();
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ constexpr const char* kScatterColorScaleFile = u8"剖面原数据的色阶数据
|
|||
constexpr const char* kVoxelScatterFile1 = u8"剖面原数据1.txt";
|
||||
constexpr const char* kVoxelScatterFile2 = u8"剖面原数据2.txt";
|
||||
constexpr const char* kAnomalyFile = u8"剖面网格数据1——对应的异常圈定数据.txt";
|
||||
constexpr const char* kDemFile = u8"dem.tif";
|
||||
constexpr const char* kImageFile = u8"image.tif";
|
||||
|
||||
// 校验 dsId,未知则抛错(输入边界验证)。
|
||||
void requireKnownDs(const std::string& dsId) {
|
||||
|
|
@ -99,6 +101,9 @@ ColorScale LocalSampleRepository::loadScatterColorScale(const std::string& dsId)
|
|||
return parseColorScale(readFile(kScatterColorScaleFile));
|
||||
}
|
||||
|
||||
std::string LocalSampleRepository::demPath() const { return dirUtf8_ + kDemFile; }
|
||||
std::string LocalSampleRepository::imagePath() const { return dirUtf8_ + kImageFile; }
|
||||
|
||||
std::vector<ScatterField> LocalSampleRepository::loadVoxelScatters() {
|
||||
std::vector<ScatterField> out;
|
||||
out.push_back(parseScatter(readFile(kVoxelScatterFile1)));
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ public:
|
|||
// 具体类专有方法(不进 IDatasetRepository 接口);散点点为不透明,alpha 量纲差异无影响。
|
||||
geopro::core::ColorScale loadScatterColorScale(const std::string& dsId);
|
||||
|
||||
// DEM/影像 GeoTIFF 的绝对路径(供 render::buildTerrain 经 GDAL 读)。
|
||||
std::string demPath() const;
|
||||
std::string imagePath() const;
|
||||
|
||||
// dd_voxel 输入:读两条交叉剖面散点(剖面原数据1.txt + 剖面原数据2.txt),
|
||||
// 返回两者,供体素插值合并。具体类专有方法(不进 IDatasetRepository 接口)。
|
||||
std::vector<geopro::core::ScatterField> loadVoxelScatters();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
find_package(VTK REQUIRED COMPONENTS CommonCore CommonDataModel FiltersGeometry FiltersModeling RenderingCore RenderingOpenGL2 RenderingVolumeOpenGL2 InteractionStyle InteractionWidgets)
|
||||
find_package(VTK REQUIRED COMPONENTS CommonCore CommonDataModel FiltersGeometry FiltersModeling RenderingCore RenderingOpenGL2 RenderingVolumeOpenGL2 InteractionStyle InteractionWidgets IOImage)
|
||||
find_package(GDAL CONFIG REQUIRED)
|
||||
add_library(geopro_render STATIC
|
||||
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 actors/ElectrodeActor.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 actors/ElectrodeActor.cpp actors/TerrainActor.cpp)
|
||||
target_include_directories(geopro_render PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(geopro_render PUBLIC geopro_core ${VTK_LIBRARIES})
|
||||
target_link_libraries(geopro_render PUBLIC geopro_core ${VTK_LIBRARIES} GDAL::GDAL)
|
||||
target_compile_features(geopro_render PUBLIC cxx_std_17)
|
||||
set_target_properties(geopro_render PROPERTIES AUTOMOC OFF AUTOUIC OFF AUTORCC OFF)
|
||||
vtk_module_autoinit(TARGETS geopro_render MODULES ${VTK_LIBRARIES})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,189 @@
|
|||
#include "actors/TerrainActor.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
#include <gdal.h>
|
||||
#include <gdal_priv.h>
|
||||
|
||||
#include <vtkFloatArray.h>
|
||||
#include <vtkImageData.h>
|
||||
#include <vtkNew.h>
|
||||
#include <vtkPointData.h>
|
||||
#include <vtkPoints.h>
|
||||
#include <vtkPolyData.h>
|
||||
#include <vtkPolyDataMapper.h>
|
||||
#include <vtkStructuredGrid.h>
|
||||
#include <vtkStructuredGridGeometryFilter.h>
|
||||
#include <vtkTexture.h>
|
||||
#include <vtkUnsignedCharArray.h>
|
||||
|
||||
#include "geo/CrsTransform.hpp"
|
||||
|
||||
namespace geopro::render {
|
||||
|
||||
namespace {
|
||||
|
||||
// 项目 CRS(DEM 无投影信息时的兜底)。
|
||||
constexpr const char* kFallbackCrs = "EPSG:4547";
|
||||
|
||||
struct Raster {
|
||||
int w = 0, h = 0;
|
||||
double gt[6] = {0, 1, 0, 0, 0, 1}; // 仿射: x=gt0+col*gt1+row*gt2, y=gt3+col*gt4+row*gt5
|
||||
std::string wkt;
|
||||
bool ok() const { return w > 0 && h > 0; }
|
||||
};
|
||||
|
||||
// 用 GDAL 读影像像素为 RGB 纹理(vtkTIFFReader 对压缩/分块 TIFF 不可靠,故走 GDAL)。
|
||||
// 行翻转使影像在 vtkImageData 中正立(配合 tcoord v=1-row/h)。读失败返回 nullptr。
|
||||
vtkSmartPointer<vtkTexture> readTexture(const std::string& path) {
|
||||
auto* ds = static_cast<GDALDataset*>(GDALOpen(path.c_str(), GA_ReadOnly));
|
||||
if (!ds) return nullptr;
|
||||
const int w = ds->GetRasterXSize(), h = ds->GetRasterYSize();
|
||||
const int nb = std::min(ds->GetRasterCount(), 3);
|
||||
if (w <= 0 || h <= 0 || nb < 1) { GDALClose(ds); return nullptr; }
|
||||
|
||||
std::vector<unsigned char> buf(static_cast<size_t>(w) * h * nb);
|
||||
// 交错读 RGB:pixelSpace=nb, lineSpace=w*nb, bandSpace=1。
|
||||
const CPLErr err =
|
||||
ds->RasterIO(GF_Read, 0, 0, w, h, buf.data(), w, h, GDT_Byte, nb, nullptr,
|
||||
nb, static_cast<GSpacing>(w) * nb, 1);
|
||||
GDALClose(ds);
|
||||
if (err != CE_None) return nullptr;
|
||||
|
||||
vtkNew<vtkImageData> img;
|
||||
img->SetDimensions(w, h, 1);
|
||||
vtkNew<vtkUnsignedCharArray> sc;
|
||||
sc->SetNumberOfComponents(3);
|
||||
sc->SetNumberOfTuples(static_cast<vtkIdType>(w) * h);
|
||||
for (int r = 0; r < h; ++r)
|
||||
for (int c = 0; c < w; ++c) {
|
||||
const size_t s = (static_cast<size_t>(r) * w + c) * nb;
|
||||
const vtkIdType d = static_cast<vtkIdType>(h - 1 - r) * w + c; // 行翻转→正立
|
||||
unsigned char rgb[3];
|
||||
rgb[0] = buf[s];
|
||||
rgb[1] = nb > 1 ? buf[s + 1] : buf[s];
|
||||
rgb[2] = nb > 2 ? buf[s + 2] : buf[s];
|
||||
sc->SetTypedTuple(d, rgb);
|
||||
}
|
||||
img->GetPointData()->SetScalars(sc);
|
||||
|
||||
auto tex = vtkSmartPointer<vtkTexture>::New();
|
||||
tex->SetInputData(img);
|
||||
tex->InterpolateOn();
|
||||
return tex;
|
||||
}
|
||||
|
||||
// 读栅格几何信息(不读像素)。
|
||||
Raster readGeo(GDALDataset* ds) {
|
||||
Raster r;
|
||||
if (!ds) return r;
|
||||
r.w = ds->GetRasterXSize();
|
||||
r.h = ds->GetRasterYSize();
|
||||
ds->GetGeoTransform(r.gt);
|
||||
const char* p = ds->GetProjectionRef();
|
||||
if (p) r.wkt = p;
|
||||
return r;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
vtkSmartPointer<vtkActor> buildTerrain(const std::string& demPath, const std::string& imagePath,
|
||||
const geopro::core::GeoLocalFrame& frame, double zScale)
|
||||
{
|
||||
GDALAllRegister();
|
||||
|
||||
auto* dem = static_cast<GDALDataset*>(GDALOpen(demPath.c_str(), GA_ReadOnly));
|
||||
if (!dem) return vtkSmartPointer<vtkActor>::New();
|
||||
const Raster dg = readGeo(dem);
|
||||
if (!dg.ok()) { GDALClose(dem); return vtkSmartPointer<vtkActor>::New(); }
|
||||
|
||||
// DEM 高程像素(float)。
|
||||
std::vector<float> elev(static_cast<size_t>(dg.w) * dg.h, 0.0F);
|
||||
GDALRasterBand* band = dem->GetRasterBand(1);
|
||||
int hasNoData = 0;
|
||||
const double noData = band->GetNoDataValue(&hasNoData);
|
||||
band->RasterIO(GF_Read, 0, 0, dg.w, dg.h, elev.data(), dg.w, dg.h, GDT_Float32, 0, 0);
|
||||
GDALClose(dem);
|
||||
|
||||
// 有效高程范围(忽略 nodata),nodata 填为最小有效高程使其平坦。
|
||||
float vmin = std::numeric_limits<float>::infinity();
|
||||
float vmax = -std::numeric_limits<float>::infinity();
|
||||
for (float v : elev)
|
||||
if (!(hasNoData && v == static_cast<float>(noData))) {
|
||||
vmin = std::min(vmin, v);
|
||||
vmax = std::max(vmax, v);
|
||||
}
|
||||
if (!(vmin <= vmax)) { vmin = vmax = 0.0F; }
|
||||
|
||||
// 坐标变换:DEM CRS → 4326;4326 → 3857(纹理坐标用)。
|
||||
const std::string demCrs = dg.wkt.empty() ? std::string(kFallbackCrs) : dg.wkt;
|
||||
geopro::core::CrsTransform demTo4326(demCrs, "EPSG:4326");
|
||||
geopro::core::CrsTransform llTo3857("EPSG:4326", "EPSG:3857");
|
||||
|
||||
// 影像几何(算纹理坐标);像素经 vtkTIFFReader 读为纹理。
|
||||
Raster ig;
|
||||
auto* img = static_cast<GDALDataset*>(GDALOpen(imagePath.c_str(), GA_ReadOnly));
|
||||
if (img) { ig = readGeo(img); GDALClose(img); }
|
||||
const bool hasImage = ig.ok();
|
||||
|
||||
// 结构化网格:点=世界局部米(E,N,elev*zScale),标量=高程;纹理坐标(若有影像)。
|
||||
vtkNew<vtkStructuredGrid> sgrid;
|
||||
sgrid->SetDimensions(dg.w, dg.h, 1);
|
||||
vtkNew<vtkPoints> points;
|
||||
points->SetNumberOfPoints(static_cast<vtkIdType>(dg.w) * dg.h);
|
||||
vtkNew<vtkFloatArray> sc;
|
||||
sc->SetName("elev");
|
||||
sc->SetNumberOfTuples(static_cast<vtkIdType>(dg.w) * dg.h);
|
||||
vtkNew<vtkFloatArray> tc;
|
||||
tc->SetName("tc");
|
||||
tc->SetNumberOfComponents(2);
|
||||
if (hasImage) tc->SetNumberOfTuples(static_cast<vtkIdType>(dg.w) * dg.h);
|
||||
|
||||
for (int j = 0; j < dg.h; ++j)
|
||||
for (int i = 0; i < dg.w; ++i) {
|
||||
const double demX = dg.gt[0] + (i + 0.5) * dg.gt[1] + (j + 0.5) * dg.gt[2];
|
||||
const double demY = dg.gt[3] + (i + 0.5) * dg.gt[4] + (j + 0.5) * dg.gt[5];
|
||||
const auto ll = demTo4326.forward(demX, demY); // (lon, lat)
|
||||
const auto local = frame.toLocal(ll.y, ll.x); // (E, N)
|
||||
const vtkIdType id = static_cast<vtkIdType>(j) * dg.w + i;
|
||||
float z = elev[static_cast<size_t>(j) * dg.w + i];
|
||||
if (hasNoData && z == static_cast<float>(noData)) z = vmin;
|
||||
points->SetPoint(id, local.x, local.y, z * zScale);
|
||||
sc->SetValue(id, z);
|
||||
if (hasImage) {
|
||||
const auto m = llTo3857.forward(ll.x, ll.y); // (mercX, mercY)
|
||||
const double col = (m.x - ig.gt[0]) / ig.gt[1];
|
||||
const double row = (m.y - ig.gt[3]) / ig.gt[5]; // gt5<0
|
||||
const double u = col / ig.w;
|
||||
const double v = 1.0 - row / ig.h; // 翻转使影像正立(按需核对)
|
||||
tc->SetTuple2(id, u, v);
|
||||
}
|
||||
}
|
||||
sgrid->SetPoints(points);
|
||||
sgrid->GetPointData()->SetScalars(sc);
|
||||
if (hasImage) sgrid->GetPointData()->SetTCoords(tc);
|
||||
|
||||
vtkNew<vtkStructuredGridGeometryFilter> geom;
|
||||
geom->SetInputData(sgrid);
|
||||
|
||||
vtkNew<vtkPolyDataMapper> mapper;
|
||||
mapper->SetInputConnection(geom->GetOutputPort());
|
||||
|
||||
auto actor = vtkSmartPointer<vtkActor>::New();
|
||||
actor->SetMapper(mapper);
|
||||
|
||||
auto tex = hasImage ? readTexture(imagePath) : nullptr;
|
||||
if (tex) {
|
||||
actor->SetTexture(tex);
|
||||
mapper->ScalarVisibilityOff(); // 用纹理,不用高程标量上色
|
||||
} else {
|
||||
mapper->SetScalarRange(vmin, vmax); // 退化:影像读失败/缺失 → 按高程上色
|
||||
}
|
||||
return actor;
|
||||
}
|
||||
|
||||
} // namespace geopro::render
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
#pragma once
|
||||
#include <string>
|
||||
|
||||
#include <vtkActor.h>
|
||||
#include <vtkSmartPointer.h>
|
||||
|
||||
#include "geo/GeoLocalFrame.hpp"
|
||||
|
||||
namespace geopro::render {
|
||||
|
||||
// DEM 地形 + 影像贴图(spec ④)。GDAL 读 dem.tif(高程栅格)与 image.tif(影像,EPSG:3857);
|
||||
// 各自按其 CRS → EPSG:4326 → GeoLocalFrame 局部米配准到世界系(与帘面/体素同系);
|
||||
// 高程作 z 起伏(vtkStructuredGrid 面),影像按经纬→3857→像素 算纹理坐标贴面。
|
||||
// 影像加载失败则退化为按高程上色(无纹理)。读不到 DEM 返回空 actor。
|
||||
//
|
||||
// 依赖 GDAL/PROJ;调用方运行时须有 PROJ_DATA。zScale 为高程纵向夸张(地形通常 1.0~数倍)。
|
||||
vtkSmartPointer<vtkActor> buildTerrain(const std::string& demPath,
|
||||
const std::string& imagePath,
|
||||
const geopro::core::GeoLocalFrame& frame,
|
||||
double zScale = 1.0);
|
||||
|
||||
} // namespace geopro::render
|
||||
|
|
@ -72,6 +72,8 @@ target_sources(geopro_tests PRIVATE render/test_scatter.cpp)
|
|||
target_sources(geopro_tests PRIVATE render/test_anomaly.cpp)
|
||||
# Electrode:buildElectrodes(剖面顶边朝下三角 ▼) 三角数/顶点位置/空安全。
|
||||
target_sources(geopro_tests PRIVATE render/test_electrode.cpp)
|
||||
# Terrain:buildTerrain(GDAL 读 dem/image + 重投影 → warp 面+纹理) 非空/缺文件安全(需 PROJ_DATA)。
|
||||
target_sources(geopro_tests PRIVATE render/test_terrain.cpp)
|
||||
target_link_libraries(geopro_tests PRIVATE geopro_render ${VTK_LIBRARIES})
|
||||
vtk_module_autoinit(TARGETS geopro_tests MODULES ${VTK_LIBRARIES})
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
#include <gtest/gtest.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <vtkActor.h>
|
||||
#include <vtkMapper.h>
|
||||
|
||||
#include "actors/TerrainActor.hpp"
|
||||
#include "geo/GeoLocalFrame.hpp"
|
||||
|
||||
using namespace geopro::core;
|
||||
|
||||
// 样本 DEM/影像目录(UTF-8 中文路径;源文件以 UTF-8 保存 + MSVC /utf-8 编译)。
|
||||
static const std::string kDir =
|
||||
u8"D:/Git/lanbingtech/geopro/docs/剖面网格数据的色阶数据2等文件/";
|
||||
|
||||
// buildTerrain: 真实 dem.tif + image.tif 经 GDAL 读 + 重投影 → 非空 actor 且有 mapper。
|
||||
// (需 PROJ_DATA + GDAL 运行时; tests CMake 注入 PROJ_DATA。)
|
||||
TEST(Terrain, BuildsTexturedSurfaceFromSampleDemImage) {
|
||||
GeoLocalFrame frame(22.546, 114.164); // 测区附近(香港)
|
||||
auto actor = geopro::render::buildTerrain(kDir + "dem.tif", kDir + "image.tif", frame, 1.0);
|
||||
ASSERT_NE(actor.GetPointer(), nullptr);
|
||||
ASSERT_NE(actor->GetMapper(), nullptr); // 成功读 DEM → 有 mapper(空 actor 无 mapper)
|
||||
}
|
||||
|
||||
// 不存在的 DEM → 安全返回空 actor(无 mapper),不崩。
|
||||
TEST(Terrain, MissingDemYieldsSafeActor) {
|
||||
GeoLocalFrame frame(22.546, 114.164);
|
||||
auto actor = geopro::render::buildTerrain("D:/no/such/dem.tif", "D:/no/such/img.tif", frame);
|
||||
ASSERT_NE(actor.GetPointer(), nullptr);
|
||||
EXPECT_EQ(actor->GetMapper(), nullptr);
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@
|
|||
#include "actors/GridContourActor.hpp"
|
||||
#include "actors/MapLineActor.hpp"
|
||||
#include "actors/ScatterActor.hpp"
|
||||
#include "actors/TerrainActor.hpp"
|
||||
#include "geo/CrsTransform.hpp"
|
||||
#include "geo/GeoLocalFrame.hpp"
|
||||
#include "parse/SampleParsers.hpp"
|
||||
|
|
@ -149,6 +150,16 @@ int main() {
|
|||
dims[0], dims[1], dims[2], profs[0].v.size() + profs[1].v.size());
|
||||
}
|
||||
|
||||
// 7) DEM 地形 + 影像贴图 — GDAL 读 + 重投影到世界系 + warp 面 + 纹理
|
||||
{
|
||||
auto terr = render::buildTerrain(dir + "dem.tif", dir + "image.tif", frame, 1.0);
|
||||
vtkNew<vtkRenderer> ren; ren->SetBackground(0.50, 0.60, 0.72);
|
||||
if (terr) ren->AddActor(terr);
|
||||
render::applyFree3D(ren);
|
||||
renderToPng(ren, (dir + "verify_terrain_3d.png").c_str(), 800, 600);
|
||||
std::printf("TERRAIN actor=%d\n", terr ? 1 : 0);
|
||||
}
|
||||
|
||||
std::printf("RENDER_VERIFY_DONE grid=%dx%d lat0=%.5f lon0=%.5f\n", g.nx(), g.ny(), lat0, lon0);
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
"description": "Geopro 3.0 desktop client (Qt6 + VTK9) - M1. 方案②-修订: Qt/VTK/ADS/QtKeychain 对接官方 MSVC Qt(不走 vcpkg); 仅非 Qt 依赖走 vcpkg, 按层递增。",
|
||||
"dependencies": [
|
||||
"eigen3",
|
||||
"gdal",
|
||||
"gtest",
|
||||
"nlohmann-json",
|
||||
"openssl",
|
||||
|
|
|
|||
Loading…
Reference in New Issue