feat/vtk-3d-view #7
|
|
@ -0,0 +1,383 @@
|
|||
# GPR 三维体 POC(B & C 双方案)实现计划
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 用真实 13G 雷达数据为 B(整卷上 GPU)与 C(分块+金字塔+核外)两套对等方案做 POC,验证技术可行性并挖出 spec 未预见的阻塞;POC 代码即生产地基与接口实现,不返工。
|
||||
|
||||
**Architecture:** 共用地基(解析/几何/结构化建体/int16 量化体/分块存储)+ `IVolumeRenderSource` 渲染接缝;`WholeVolumeSource`(B) 与 `OutOfCoreSource`(C) 是接口下两个永久并存实现,用户运行时按数据规模切换。落盘从第一天就分块,B 的裸分块格式是 C 金字塔/核外的基座。
|
||||
|
||||
**Tech Stack:** C++17, Qt6, VTK 9.6(`RenderingVolumeOpenGL2` GPU ray cast,自带 `vtkzlib`),GoogleTest,nlohmann-json(sidecar),现有 `src/{core,data,render,app}` 分层。
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- **dtype**:雷达体走 **int16**(`vtkShortArray`),不污染反演剖面的 double 主路径(`ScalarVolume` 保持 `std::vector<double>`,`src/core/model/Field.hpp:8-26`)。
|
||||
- **量化**:物理值 ↔ int16 经 `scale/offset`,**必须贯穿**传递函数采样、色阶 LUT(`src/render/interact/SliceTool.cpp:37`)、取值/详情反量化(见 spec B §3.5)。
|
||||
- **落盘**:**不用 `vtkHDFWriter`**(VTK 9.6 写不了 `vtkImageData`,记忆 `vtk96-hdfwriter-no-imagedata`)。用裸 int16 分块 + sidecar(json) + 逐块 `vtkzlib`。
|
||||
- **渲染接缝**:上层(场景/切片)只面向 `IVolumeRenderSource`;B/C 是其两个实现。
|
||||
- **结构化建体**:X(沿线)/Z(深度) 规则落格,仅 Y(14 通道) 向 1D 插值;**不**对雷达用全 3D 散点 IDW(现有 `IdwInterpolator` 无空间索引暴力,`src/core/algo/IdwInterpolator.cpp:15-33`)。
|
||||
- **真实数据判定**:POC 用 `D:\Downloads\明星路`(450MHz/14通道/821采样/~45306道/线/20线/int16/13.6GB)。POC 过 = 在该真实数据上跑通并达标,不许避重就轻。
|
||||
- **测试数据头实证**:`.iprh` 文本键值(`SAMPLES/LAST TRACE/CHANNELS/TIMEWINDOW/SOIL VELOCITY/DISTANCE INTERVAL`);`.iprb` = `int16[samples × traces]`,`samples×traces×2 == 文件大小`;`.ord` = 通道横向偏移(14 有效)。
|
||||
|
||||
---
|
||||
|
||||
## 文件结构(决定分解与复用)
|
||||
|
||||
```
|
||||
src/io/gpr/ ← 新增:雷达 IO(共用地基)
|
||||
IprHeader.{hpp,cpp} 解析 .iprh → 结构体
|
||||
IprbReader.{hpp,cpp} 读 .iprb int16 B-scan(mmap/分块读)
|
||||
GprGeometry.{hpp,cpp} .ord 通道偏移 + .gps/.cor 逐道经纬 + 深度轴
|
||||
GprSurvey.{hpp,cpp} 一个工区 = 线[]×通道[] + 几何(建体输入)
|
||||
src/core/model/
|
||||
ScalarVolumeI16.hpp int16 体 + Quant{scale,offset} (新增,与 ScalarVolume 并列)
|
||||
src/core/algo/
|
||||
GprVolumeBuilder.{hpp,cpp} 结构化建体:X/Z 落格 + Y 向 1D 插值 → ScalarVolumeI16
|
||||
src/data/store/
|
||||
ChunkedVolumeStore.{hpp,cpp} 分块 int16 + zlib + sidecar;读/写/按块取(B/C 共用,C 加金字塔)
|
||||
src/render/source/
|
||||
IVolumeRenderSource.hpp 渲染接缝(接口)
|
||||
WholeVolumeSource.{hpp,cpp} B:读全块 → 1 个 vtkImageData
|
||||
OutOfCoreSource.{hpp,cpp} C:金字塔 + brick 分页 → 工作集
|
||||
src/render/actors/
|
||||
VoxelActor.cpp Modify:buildVoxel 增 int16 重载 + 量化域传函
|
||||
tests/io/gpr/ tests/core/ tests/data/store/ ← 对应测试
|
||||
tools/gpr_poc/ ← POC 度量台(建体/加载/显存/fps 探针 + CLI)
|
||||
```
|
||||
|
||||
POC 度量统一进 `tools/gpr_poc`(建体耗时、输出维度、落盘体积/压缩比、加载耗时、显存、切片/体绘制 fps),B/C 用同一套指标对照。
|
||||
|
||||
> **POC vs 生产**:Task 1–6(地基)+ 7–8、10–11(接口/存储)是**生产代码,走 TDD、有完整代码**。Task 9、12–13 是**可行性探针**:给出明确实验、被测未知、通过/失败判据与度量,不预先杜撰我们正要验证的 VTK 核外内部实现——这是 POC 的本质,强行写"完整代码"等于造假。
|
||||
|
||||
---
|
||||
|
||||
## Phase 0 — 地基(共用,生产级 TDD)
|
||||
|
||||
### Task 1: .iprh 头解析
|
||||
|
||||
**Files:**
|
||||
- Create: `src/io/gpr/IprHeader.hpp`, `src/io/gpr/IprHeader.cpp`
|
||||
- Test: `tests/io/gpr/test_ipr_header.cpp`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `struct IprHeader { int samples; long lastTrace; int channels; double timeWindowNs; double soilVelocity; double distanceInterval; };` + `IprHeader parseIprHeader(const std::string& text);`
|
||||
|
||||
- [ ] **Step 1: 写失败测试**
|
||||
```cpp
|
||||
#include "io/gpr/IprHeader.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
using geopro::io::gpr::parseIprHeader;
|
||||
TEST(IprHeader, ParsesKeyFieldsFromRealSample) {
|
||||
const std::string t =
|
||||
"SAMPLES: 821\nLAST TRACE: 45305\nCHANNELS: 14\n"
|
||||
"TIMEWINDOW: 160.352\nSOIL VELOCITY: 100.000000\nDISTANCE INTERVAL: 0.049084\n";
|
||||
auto h = parseIprHeader(t);
|
||||
EXPECT_EQ(h.samples, 821);
|
||||
EXPECT_EQ(h.lastTrace, 45305);
|
||||
EXPECT_EQ(h.channels, 14);
|
||||
EXPECT_DOUBLE_EQ(h.timeWindowNs, 160.352);
|
||||
EXPECT_DOUBLE_EQ(h.soilVelocity, 100.0);
|
||||
EXPECT_NEAR(h.distanceInterval, 0.049084, 1e-9);
|
||||
}
|
||||
```
|
||||
- [ ] **Step 2: 运行确认失败** — `ctest -R IprHeader`,预期 编译/链接失败(未定义)。
|
||||
- [ ] **Step 3: 最小实现** — `parseIprHeader` 逐行 `key: value` 拆分,按字段名填结构体;缺字段抛 `std::runtime_error`。
|
||||
- [ ] **Step 4: 运行确认通过** — `ctest -R IprHeader`,预期 PASS。
|
||||
- [ ] **Step 5: 提交** — `git commit -m "feat(gpr): parse .iprh header fields"`
|
||||
|
||||
### Task 2: .iprb B-scan 读取
|
||||
|
||||
**Files:**
|
||||
- Create: `src/io/gpr/IprbReader.{hpp,cpp}`
|
||||
- Test: `tests/io/gpr/test_iprb_reader.cpp`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `IprHeader`
|
||||
- Produces: `struct BScan { int samples; long traces; std::vector<int16_t> data; /* [trace*samples + s] */ };` + `BScan readIprb(const std::string& path, const IprHeader& h);`(校验 `samples*traces*2 == fileSize`)
|
||||
|
||||
- [ ] **Step 1: 写失败测试**(用临时文件造 4 道×3 采样 int16)
|
||||
```cpp
|
||||
TEST(IprbReader, ReadsInt16AndValidatesSize) {
|
||||
// 写 tmp:samples=3, traces=4 → 24 bytes
|
||||
std::vector<int16_t> raw{0,1,2, 10,11,12, 20,21,22, 30,31,32};
|
||||
auto path = writeTmp(raw); // helper
|
||||
geopro::io::gpr::IprHeader h{}; h.samples=3; h.lastTrace=3; // traces=lastTrace+1=4
|
||||
auto b = geopro::io::gpr::readIprb(path, h);
|
||||
EXPECT_EQ(b.samples, 3); EXPECT_EQ(b.traces, 4);
|
||||
EXPECT_EQ(b.data[1*3 + 2], 12); // 第1道第2采样
|
||||
}
|
||||
```
|
||||
- [ ] **Step 2: 运行确认失败**。
|
||||
- [ ] **Step 3: 最小实现** — `traces = lastTrace+1`;读全文件为 int16;不匹配大小抛错。
|
||||
- [ ] **Step 4: 通过**。
|
||||
- [ ] **Step 5: 提交** — `git commit -m "feat(gpr): read .iprb int16 b-scan with size check"`
|
||||
|
||||
### Task 3: 几何(通道偏移 + 逐道经纬 + 深度轴)
|
||||
|
||||
**Files:**
|
||||
- Create: `src/io/gpr/GprGeometry.{hpp,cpp}`
|
||||
- Test: `tests/io/gpr/test_gpr_geometry.cpp`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces:
|
||||
- `std::vector<double> parseChannelXOffsets(const std::string& ordText);`(取第 4 列==1 的有效通道横偏,明星路应得 14 个 -0.686..+0.686)
|
||||
- `double depthOfSample(int s, const IprHeader& h);`(`= s * (timeWindowNs/(samples-1)) * soilVelocity*1e-9/2`,单位米;soilVelocity 100 m/µs = 1e8 m/s)
|
||||
|
||||
- [ ] **Step 1: 写失败测试**
|
||||
```cpp
|
||||
TEST(GprGeometry, ParsesActiveChannelOffsets) {
|
||||
const std::string ord = "0 -0.686000 -1.5 1\n1 -0.581000 -1.5 1\n14 0 -1.5 0\n";
|
||||
auto xs = geopro::io::gpr::parseChannelXOffsets(ord);
|
||||
EXPECT_EQ(xs.size(), 2u); // 仅 2 个有效(末列=1)
|
||||
EXPECT_NEAR(xs[0], -0.686, 1e-6);
|
||||
}
|
||||
TEST(GprGeometry, DepthOfLastSampleMatchesPhysics) {
|
||||
geopro::io::gpr::IprHeader h{}; h.samples=821; h.timeWindowNs=160.352; h.soilVelocity=1e8;
|
||||
EXPECT_NEAR(geopro::io::gpr::depthOfSample(820, h), 8.0, 0.05); // ~8m
|
||||
}
|
||||
```
|
||||
> 注:`soilVelocity` 单位换算在 Task 1 读入时统一成 m/s(100 m/µs = 1e8 m/s),在此基础上测试。
|
||||
- [ ] **Step 2: 失败**。
|
||||
- [ ] **Step 3: 实现** — `.ord` 按空白拆列、末列=="1" 收集第 2 列;`depthOfSample` 按公式。
|
||||
- [ ] **Step 4: 通过**。
|
||||
- [ ] **Step 5: 提交** — `git commit -m "feat(gpr): channel offsets + depth axis geometry"`
|
||||
|
||||
### Task 4: int16 量化体类型
|
||||
|
||||
**Files:**
|
||||
- Create: `src/core/model/ScalarVolumeI16.hpp`
|
||||
- Test: `tests/core/test_scalar_volume_i16.cpp`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces:
|
||||
```cpp
|
||||
struct Quant { double scale = 1.0; double offset = 0.0;
|
||||
int16_t toQ(double v) const; // round((v-offset)/scale),钳到[INT16_MIN+1,INT16_MAX]
|
||||
double toPhys(int16_t q) const; }; // q*scale+offset
|
||||
class ScalarVolumeI16 { // 行优先 idx=((k*ny+j)*nx+i),与 vtkImageData 一致
|
||||
ScalarVolumeI16(int nx,int ny,int nz);
|
||||
int16_t& at(int i,int j,int k); int nx()const; ...; std::vector<int16_t>& data();
|
||||
static constexpr int16_t kBlank = INT16_MIN; }; // 空值哨兵→透明
|
||||
```
|
||||
|
||||
- [ ] **Step 1: 写失败测试**(量化往返 + 索引布局 + blank)
|
||||
```cpp
|
||||
TEST(ScalarVolumeI16, QuantRoundTripAndLayout) {
|
||||
geopro::core::Quant q{0.5, -10.0};
|
||||
EXPECT_EQ(q.toQ(-10.0), 0); EXPECT_NEAR(q.toPhys(q.toQ(3.0)), 3.0, 0.25);
|
||||
geopro::core::ScalarVolumeI16 v(2,2,2);
|
||||
v.at(1,0,1) = 7; EXPECT_EQ(v.data()[(1*2+0)*2+1], 7);
|
||||
}
|
||||
```
|
||||
- [ ] **Step 2: 失败** → **Step 3: 实现** → **Step 4: 通过**。
|
||||
- [ ] **Step 5: 提交** — `git commit -m "feat(core): int16 scalar volume + quantization"`
|
||||
|
||||
### Task 5: 结构化建体 GprVolumeBuilder
|
||||
|
||||
**Files:**
|
||||
- Create: `src/core/algo/GprVolumeBuilder.{hpp,cpp}`
|
||||
- Test: `tests/core/test_gpr_volume_builder.cpp`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `GprSurvey`(线×通道 BScan + 几何)、`GridSpec`(复用 `src/core/algo/IInterpolator.hpp:7-13`)
|
||||
- Produces: `struct BuiltI16 { ScalarVolumeI16 vol; Quant quant; std::array<double,3> origin, spacing; double vminPhys, vmaxPhys; };`
|
||||
`BuiltI16 buildGprVolume(const GprSurvey& s, const GridSpec& spec);`
|
||||
- **算法**:X(沿线)/Z(深度) 最近邻或线性落格(道已规则);**Y 向**对落在该 (x,z) 的 14 通道值做 1D 线性插值填充横向网格;maxDist/无覆盖 → `kBlank`。量化 scale/offset 由全体 min/max 定。
|
||||
|
||||
- [ ] **Step 1: 写失败测试**(2 通道、各 1 道×2 采样的人造 survey,验横向中点插值 + 维度)
|
||||
```cpp
|
||||
TEST(GprVolumeBuilder, InterpolatesAcrossChannelsOnly) {
|
||||
auto s = makeTwoChannelSurvey(/*ch0 val=0, ch1 val=100, 横偏 0 和 1m*/);
|
||||
geopro::core::GridSpec spec{/*nx=*/3,/*ny=*/1,/*nz=*/1, 0,0,0, 0.5,1,1, 2.0, 9.9};
|
||||
auto b = geopro::core::buildGprVolume(s, spec);
|
||||
EXPECT_NEAR(b.quant.toPhys(b.vol.at(1,0,0)), 50.0, 1.0); // 横向中点≈50
|
||||
}
|
||||
```
|
||||
- [ ] **Step 2: 失败** → **Step 3: 实现**(先单线程,循环结构留可并行)→ **Step 4: 通过**。
|
||||
- [ ] **Step 5: 提交** — `git commit -m "feat(core): structured GPR volume builder (Y-only interp)"`
|
||||
|
||||
### Task 6: 分块存储 ChunkedVolumeStore(B/C 共用基座)
|
||||
|
||||
**Files:**
|
||||
- Create: `src/data/store/ChunkedVolumeStore.{hpp,cpp}`
|
||||
- Test: `tests/data/store/test_chunked_volume_store.cpp`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces:
|
||||
```cpp
|
||||
struct StoreMeta { int nx,ny,nz; int brick; // e.g. 64
|
||||
std::array<double,3> origin, spacing; Quant quant; double vminPhys,vmaxPhys; };
|
||||
class ChunkedVolumeStore {
|
||||
static void write(const std::string& dir, const BuiltI16& b, int brick=64); // 分块+zlib+sidecar.json
|
||||
static StoreMeta readMeta(const std::string& dir);
|
||||
std::vector<int16_t> readBrick(int bx,int by,int bz) const; // 解压单块
|
||||
// Task 10 追加:pyramid 层;Task 11 用 readBrick 做工作集
|
||||
};
|
||||
```
|
||||
- **格式**:`meta.json`(StoreMeta + 分块索引/偏移/压缩长度) + `data.bin`(逐块 zlib 压缩流)。zlib 用 VTK 自带 `vtkzlib` 或直接 zlib C API。
|
||||
|
||||
- [ ] **Step 1: 写失败测试**(write→readMeta→readBrick 往返 + 压缩后小于原始)
|
||||
```cpp
|
||||
TEST(ChunkedVolumeStore, RoundTripBrickAndCompresses) {
|
||||
auto b = makeBuilt(128,128,128, /*可压缩模式*/);
|
||||
geopro::data::ChunkedVolumeStore::write(tmpDir, b, 64);
|
||||
auto m = geopro::data::ChunkedVolumeStore::readMeta(tmpDir);
|
||||
EXPECT_EQ(m.nx, 128); EXPECT_EQ(m.brick, 64);
|
||||
geopro::data::ChunkedVolumeStore s(tmpDir);
|
||||
auto blk = s.readBrick(0,0,0);
|
||||
EXPECT_EQ(blk.size(), 64u*64*64);
|
||||
EXPECT_LT(fileSize(tmpDir+"/data.bin"), 128u*128*128*2); // 压缩生效
|
||||
}
|
||||
```
|
||||
- [ ] **Step 2: 失败** → **Step 3: 实现** → **Step 4: 通过**。
|
||||
- [ ] **Step 5: 提交** — `git commit -m "feat(data): chunked int16 volume store (zlib + sidecar)"`
|
||||
|
||||
### Task 7: VoxelActor int16 重载 + 量化域传递函数
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/render/actors/VoxelActor.cpp`(增 int16 重载,参照现 double 版 `:41-79`)
|
||||
- Test: `tests/render/test_voxel_i16_smoke.cpp`(无窗渲染冒烟 / 或断言 image 标量类型与传函控制点在量化域)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `vtkSmartPointer<vtkVolume> buildVoxelI16(const ScalarVolumeI16& vol, const Quant& q, const ColorScale& cs, double ox,..,dz, vtkSmartPointer<vtkImageData>& outImage);`
|
||||
- **要点**:`vtkShortArray` 填值;传函/不透明度在**量化域 qmin/qmax** 加点(`q.toQ(vminPhys)`..`q.toQ(vmaxPhys)`),`kBlank→0` 不透明;`vtkSmartVolumeMapper`。
|
||||
|
||||
- [ ] **Step 1: 写失败测试**(构造小 int16 体,断言 `outImage->GetScalarType()==VTK_SHORT` 且传函在量化域采样不崩)。
|
||||
- [ ] **Step 2: 失败** → **Step 3: 实现** → **Step 4: 通过**。
|
||||
- [ ] **Step 5: 提交** — `git commit -m "feat(render): int16 voxel actor with quantized transfer fn"`
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — POC-B(整卷上 GPU)
|
||||
|
||||
### Task 8: IVolumeRenderSource + WholeVolumeSource(B)
|
||||
|
||||
**Files:**
|
||||
- Create: `src/render/source/IVolumeRenderSource.hpp`, `src/render/source/WholeVolumeSource.{hpp,cpp}`
|
||||
- Test: `tests/render/test_whole_volume_source.cpp`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces:
|
||||
```cpp
|
||||
class IVolumeRenderSource { public: virtual ~IVolumeRenderSource()=default;
|
||||
virtual StoreMeta meta() const = 0;
|
||||
virtual void update(const Camera& cam) = 0; // B:首次载全量;C:按相机换块
|
||||
virtual std::vector<vtkSmartPointer<vtkImageData>> currentProps() const = 0; // B:1 个;C:工作集
|
||||
virtual vtkImageData* sliceSource() const = 0; }; // 供 SliceTool reslice
|
||||
class WholeVolumeSource : public IVolumeRenderSource { // 读全块拼 1 个 int16 vtkImageData
|
||||
explicit WholeVolumeSource(const std::string& storeDir); };
|
||||
```
|
||||
|
||||
- [ ] **Step 1: 写失败测试**(从 Task 6 写出的 store 构造 WholeVolumeSource,`currentProps().size()==1`,维度==meta)。
|
||||
- [ ] **Step 2: 失败** → **Step 3: 实现**(遍历所有 brick 填进整卷 image)→ **Step 4: 通过**。
|
||||
- [ ] **Step 5: 提交** — `git commit -m "feat(render): IVolumeRenderSource + whole-volume source (B)"`
|
||||
|
||||
### Task 9: POC-B 真实数据度量(探针,含通过判据)
|
||||
|
||||
**Files:**
|
||||
- Create: `tools/gpr_poc/main.cpp`(CLI:`gpr_poc build|renderB <明星路目录>`)、`tools/gpr_poc/Probe.{hpp,cpp}`(计时/显存/fps)
|
||||
- 复用:Task 1–8 全部。
|
||||
|
||||
**被验证的未知 / 阻塞点:** ①int16 GPU ray cast 在真机正常出图;②真实建体耗时与输出体积;③整卷 5~10GB 加载耗时、显存峰值;④切片拖动 fps、体绘制 fps;⑤超显存时 `vtkSmartVolumeMapper` 的 `LowResResample`(`MaxMemoryInBytes`) 自动降质观感。
|
||||
|
||||
- [ ] **Step 1:** `gpr_poc build 明星路` → 跑 Task 1–6,输出:建体耗时、`nx×ny×nz`、落盘体积、压缩比。记录。
|
||||
- [ ] **Step 2:** `gpr_poc renderB` → WholeVolumeSource 上 `vtkSmartVolumeMapper`,量化传函着色,真窗口显示。
|
||||
- [ ] **Step 3:** Probe 采集:加载耗时、显存峰值、切片拖动 fps(脚本化相机/切片移动)、体绘制旋转 fps。
|
||||
- [ ] **Step 4:** 设 `MaxMemoryInBytes` 低于体大小,验证 `LowResResample` 自动降质路径出图。
|
||||
- [ ] **Step 5:** 写 `docs/superpowers/plans/poc-results-B.md`:指标表 + 结论。
|
||||
- **B 通过判据**:真实数据建体可完成且体积/耗时可接受;整卷在目标显存内出图;**切片拖动 ≥ 可用帧率(目标 ≥30fps)**;超显存时 LowRes 兜底可用。任一硬阻塞(如 GPU 不吃 short、显存必爆且 LowRes 不可接受)→ 记为 B 的落地风险并反馈 spec。
|
||||
- [ ] **Step 6: 提交** — `git commit -m "test(gpr): POC-B real-data metrics harness + results"`
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — POC-C(分块+金字塔+核外,含最小真实分页器)
|
||||
|
||||
### Task 10: 金字塔生成(ChunkedVolumeStore 增量)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/data/store/ChunkedVolumeStore.{hpp,cpp}`(加 LOD 层 + 每块 min/max)
|
||||
- Test: `tests/data/store/test_pyramid.cpp`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `void buildPyramid(int levels);`(逐级 2× 降采样,每层独立分块 + 每块 `int16 min,max` 存 meta);`std::vector<int16_t> readBrick(int level,int bx,int by,int bz);`;`std::pair<int16_t,int16_t> brickRange(int level,int bx,int by,int bz);`
|
||||
|
||||
- [ ] **Step 1: 写失败测试**(建 1 层金字塔,断言 level1 维度≈半、brickRange 命中真实 min/max)。
|
||||
- [ ] **Step 2: 失败** → **Step 3: 实现** → **Step 4: 通过**。
|
||||
- [ ] **Step 5: 提交** — `git commit -m "feat(data): volume pyramid + per-brick min/max"`
|
||||
|
||||
### Task 11: brick 分页器(LRU 工作集,生产级 TDD)
|
||||
|
||||
**Files:**
|
||||
- Create: `src/render/source/BrickPager.{hpp,cpp}`
|
||||
- Test: `tests/render/test_brick_pager.cpp`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces:
|
||||
```cpp
|
||||
class BrickPager { // 内存恒定:驻留 ≤ budgetBricks 个解压块
|
||||
BrickPager(const ChunkedVolumeStore& store, size_t budgetBricks);
|
||||
void requestVisible(const std::vector<BrickId>& visible, int level); // 载入缺失、LRU 淘汰
|
||||
const std::vector<int16_t>* get(BrickId id, int level) const; // 命中返回,未命中 nullptr
|
||||
size_t residentCount() const; };
|
||||
```
|
||||
|
||||
- [ ] **Step 1: 写失败测试**(budget=4,请求 6 块 → residentCount==4,最早的被淘汰,命中/未命中正确)。
|
||||
- [ ] **Step 2: 失败** → **Step 3: 实现**(LRU + 从 store 解压载入)→ **Step 4: 通过**。
|
||||
- [ ] **Step 5: 提交** — `git commit -m "feat(render): bounded-memory brick pager (LRU)"`
|
||||
|
||||
### Task 12: OutOfCoreSource(C)— 最高风险探针:核外体绘制
|
||||
|
||||
**Files:**
|
||||
- Create: `src/render/source/OutOfCoreSource.{hpp,cpp}`(实现 `IVolumeRenderSource`)
|
||||
- 复用:Task 10/11。
|
||||
|
||||
**被验证的未知 / 阻塞点(C 的命门,必须正面撞):**
|
||||
1. **VTK 能否渲染"动态换入换出的块工作集"为体**——把 BrickPager 的工作集作为多个 `vtkImageData` 喂给 `vtkMultiBlockVolumeMapper`(注意其"试图全量加载"语义,须只喂视野块)或多个 `vtkVolume` 叠加;验证可行性与正确性。
|
||||
2. **块边接缝**:相邻 brick 渲染交界是否可见;试 `vtkMultiBlockVolumeMapper` 的抖动是否压得住。
|
||||
3. **LOD 切换**:相机拉远用粗层、拉近换细层,切换是否闪烁/可接受。
|
||||
4. **热路径解压**:拖动每帧换块时 zlib 解压是否拖垮帧率(CPU 瓶颈)。
|
||||
|
||||
> 本任务**不预写完整实现**——它就是要发现上面四点的真实结论。步骤是受控实验,产物是"能跑的最小版本 + 实测结论"。
|
||||
|
||||
- [ ] **Step 1:** `update(cam)` 内:算视野相交 brick + 选 LOD → `BrickPager::requestVisible` → `currentProps()` 返回工作集 image。
|
||||
- [ ] **Step 2:** 用 `vtkMultiBlockVolumeMapper`(或 N×`vtkVolume`)渲染工作集,量化传函复用 Task 7。先静态相机出图,确认正确。
|
||||
- [ ] **Step 3:** 接相机移动 → 动态换块;Probe 测 residentCount/内存恒定、换块 fps。
|
||||
- [ ] **Step 4:** 逐项记录未知 1–4 的实测结论(接缝截图、LOD 切换录屏指标、解压占帧时间)。
|
||||
- [ ] **Step 5:** 写 `poc-results-C.md`:四个未知的结论 + 是否构成阻塞 + 缓解手段。
|
||||
- **C 通过判据**:工作集体绘制能正确出图且**内存恒定**;接缝/闪烁/解压三项**各自有可接受方案或明确缓解**(不可接受则记为阻塞并反馈 spec C,这正是 POC 的目的)。
|
||||
- [ ] **Step 6: 提交** — `git commit -m "test(gpr): POC-C out-of-core volume render probe + results"`
|
||||
|
||||
### Task 13: 切片核外 + B/C 切换贯通
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/render/source/OutOfCoreSource.cpp`(`sliceSource()`:只读切面相交块拼子体供 reslice)
|
||||
- 复用现有 `SliceTool`(`src/render/interact/SliceTool.cpp`)对 source 给出的 image 切片。
|
||||
|
||||
**被验证的未知:** 切片只读相交块时的内存/fps;同一 `IVolumeRenderSource` 下 B↔C 运行时切换无缝。
|
||||
|
||||
- [ ] **Step 1:** `OutOfCoreSource::sliceSource()` 按当前切面算相交 brick → 拼最小子体 image。
|
||||
- [ ] **Step 2:** `SliceTool` 对该 image reslice,拖动切片测 fps、内存。
|
||||
- [ ] **Step 3:** POC 台加 `renderC` 与 `--source whole|ooc` 开关,验证同一上层代码切两实现。
|
||||
- [ ] **Step 4:** 补 `poc-results-C.md`:切片核外指标 + 切换验证。
|
||||
- [ ] **Step 5: 提交** — `git commit -m "feat(render): slice out-of-core + runtime B/C source switch"`
|
||||
|
||||
---
|
||||
|
||||
## Self-Review(对照 spec 检查)
|
||||
|
||||
**1. spec 覆盖**
|
||||
- spec B:int16 路径(Task 4/7)、结构化建体(Task 5)、裸分块落盘(Task 6)、量化贯穿(Task 4/7/Global)、整卷渲染(Task 8/9)、LowResResample(Task 9) ✓
|
||||
- spec C:分块(Task 6)、金字塔+min/max(Task 10)、brick 分页(Task 11)、核外体绘制(Task 12)、切片核外(Task 13)、B/C 切换(Task 13) ✓
|
||||
- 共有地基:.iprb/.iprh 解析(Task 1/2)、几何/配准(Task 3)、GPR→体(Task 5) ✓
|
||||
- **缺口(POC 不覆盖,明确记录)**:ddCode 接入数据集树/UI、后端持久化对接、异常/色阶编辑器接线——POC 阶段不做(spec A §2 / B §6 列为成品阶段),不影响复用。
|
||||
|
||||
**2. 占位符扫描**:地基任务(1–8,10,11)均有完整测试代码与实现描述;POC 探针(9,12,13)按设计**有意不写杜撰实现**,代之以受控实验 + 通过判据(已在任务内注明本质)。无 "TODO/TBD/适当处理" 类空话。
|
||||
|
||||
**3. 类型一致性**:`ScalarVolumeI16`/`Quant`/`BuiltI16`/`StoreMeta`/`IVolumeRenderSource`/`BrickPager` 在定义任务与消费任务间签名一致;`buildGprVolume`/`buildVoxelI16`/`ChunkedVolumeStore::{write,readMeta,readBrick}`/`requestVisible` 全程同名。
|
||||
|
||||
---
|
||||
|
||||
## 关键风险(开工即知)
|
||||
|
||||
- **Task 12 是月级生产工程的"最小探针"**:POC 只需证可行 + 撞出阻塞,不追求生产质量分页器;但若未知 1(VTK 渲染动态工作集)撞墙,是 C 的根本阻塞,须立刻反馈 spec C 并评估替代(OpenVDS / 自建 GL)。
|
||||
- **真实 13G 在合理分辨率下可能装得进显存** → B 顺过、C 的核外价值要靠"细分辨率/拼全路段大体"的真实配置才能压出(Task 9/12 的网格参数留 CLI 可调,用同一份真实数据调出超显存体来考验 C)。
|
||||
- **量化贯穿**漏一处即色阶/读数错(Global 约束 + Task 4/7),Self-Review 已盯。
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
# GPR 三维体 · 方案 A:整卷上纹理,不用金字塔(复用现有管线,最简基线)
|
||||
|
||||
- 日期:2026-06-23
|
||||
- 范围:把 GPR(探地雷达)阵列数据插值成三维体并在 VTK 中渲染/切片,**直接复用现有剖面三维体管线**,整卷一次性进显存,不做分块/金字塔/核外。
|
||||
- 定位:三选一中的**最小改动基线**。用于评估"现有架构原样接雷达,能做到什么、卡在哪"。
|
||||
- **⚠ 评审结论(2026-06-23,opus):A 不应作为独立交付步,建议并入 B。** 唯一值得从 A 单独先做的是"三方案共有的地基"(§2)。`double`+400³+暴力 IDW 三条硬约束使 A 产出的 GPR 体既无业务分辨率(沿线被强制粗化到 5.5m vs 物理 5cm)、又无法落盘秒开。详见 §5/§6。
|
||||
- 测试数据(明星路):450MHz 阵列 GPR,14 通道,每道 821 采样,单线 ~45306 道,20 线,int16,合计 13.6GB;路长 2223m,测幅 1.37m,深 ~8m。
|
||||
|
||||
---
|
||||
|
||||
## 1. 设计意图
|
||||
|
||||
不引入任何新渲染/存储机制。把雷达数据**喂进现有体素管线**,让它走和反演剖面三维体完全一样的路:
|
||||
|
||||
```
|
||||
.iprb/.iprh → PointSet → core::IdwInterpolator → ScalarVolume(double)
|
||||
→ data::VolumeGrid → render::buildVoxel → vtkSmartVolumeMapper(整卷进显存)
|
||||
切片:render::interact::SliceTool(vtkImagePlaneWidget / vtkImageReslice,CPU 重采样)
|
||||
持久化:VolumeBuildParams(参数必存)+ 可选明细缓存(现内存 mock)
|
||||
```
|
||||
|
||||
现有落点(实证):
|
||||
- `core::ScalarVolume` = `std::vector<double>`,行优先(`src/core/model/Field.hpp:8-26`)。
|
||||
- `render::buildVoxel` → `vtkImageData`+`vtkDoubleArray`+`vtkSmartVolumeMapper`,整卷上传(`src/render/actors/VoxelActor.cpp:41-79`)。
|
||||
- 体素维度上限 `kMaxVolumeDim = 400`(`src/core/algo/VolumeBuilder.hpp:8`)。
|
||||
- 切片 CPU 重采样(`src/render/interact/SliceTool.cpp:24-39`)。
|
||||
- 插值 `IdwInterpolator`,单线程三重循环,且**无空间索引——每体素全点集线性扫描,O(体素数×点数) 暴力**(`src/core/algo/IdwInterpolator.cpp:15-33`,评审实证)。雷达级点集下即便 400³ 也是分钟级甚至卡死,不只是"偏慢"。
|
||||
- 持久化 `Api3dRepository::StoredVolume`,纯内存(`src/data/api/Api3dRepository.hpp:112-119`),重算逻辑已就绪(`Api3dRepository.cpp:212-225`)。
|
||||
- **注意(评审)**:现有 `loadVolume` 的散点来源硬绑 `loadSection`/`appendGridPoints`(ERT 反演帘面,`Api3dRepository.cpp:146-171`),**雷达没有现成喂入路径**。"复用现有管线"实际仍须新写 GPR→`buildVolume` 接入(§2 已含),§1 流程图的"原样复用"措辞偏乐观。
|
||||
|
||||
---
|
||||
|
||||
## 2. 新增工作(雷达接入,三方案共有的地基)
|
||||
|
||||
A/B/C 都绕不开这块,A 用最朴素实现:
|
||||
|
||||
1. **`.iprb`/`.iprh` 解析器**(新):`.iprh` 文本头取 `SAMPLES/LAST TRACE/CHANNELS/TIMEWINDOW/SOIL VELOCITY/DISTANCE INTERVAL`;`.iprb` 读 int16 B-scan(`samples × traces`,校验 `samples×traces×2 == 文件大小`)。
|
||||
2. **地理配准**:`.ord` 取 14 通道横向偏移;`.gps`/`.cor` 取每道经纬度/RTK;深度 = `time × soilVelocity / 2`。
|
||||
3. **GPR→PointSet 适配器**:把"14 通道 × N 道 × 821 采样"摊成 `PointSet{x,y,z,v}`(局部坐标)。**注意横向只有 14 个真实样本**,是稀疏维。
|
||||
4. **数据集接入**:新增 ddCode(如 `dd_gpr_volume`),在维度分类(`Api3dRepository.cpp:30-45`、`LocalSample3dRepository.cpp:43-58`)归 3D;DTO 解析器放 `src/data/dto/`。
|
||||
|
||||
---
|
||||
|
||||
## 3. 关键约束与后果(A 的硬边界)
|
||||
|
||||
现有管线是 **double + 400³ 上限**。这两条直接决定 A 能做什么:
|
||||
|
||||
| 约束 | 数值 | 后果 |
|
||||
|---|---|---|
|
||||
| 标量 dtype | `double`(8 字节/体素) | 同样体素数,内存是 int16 的 **4 倍** |
|
||||
| 维度上限 | 400³ | 整卷 ≤ 400³×8 ≈ **512MB**;放不下全路段 |
|
||||
| 整卷进显存 | 一次性 | 体大小受限于显存 |
|
||||
| IDW | 单线程 + 无空间索引暴力 | 大点集插值分钟级/卡死(明星路单线 ~5亿采样点级) |
|
||||
|
||||
> **`fitAxis` 行为(评审实证 `VolumeBuilder.cpp:16-26`)**:格数超 400 时**不裁剪范围**,而是 `outCell=ext/(400-1)` 把 400 格摊满整个包络。所以 A 不是"丢掉远端",而是"强制粗化"——沿线细节被低分辨率抹平。
|
||||
|
||||
**全路段在 A 下做不到原始分辨率。** 明星路需 ~22000(沿线)×270(横)×400(深),远超 400³。在 A 下只能:
|
||||
- **重度降采样到 ≤400³**:沿线 2223m/400 ≈ 5.5m 网格 → 沿线细节全毁;或
|
||||
- **按单条测线/短段分别建小体**(单线降到 400³ 仍偏粗),多体并排显示(类似现有 2D 足迹平铺)。**但**(评审):20 体 × 400³ × double ≈ 10GB 整卷同驻显存,比单大体更易爆显存;且各体独立 `GridSpec`/origin,**跨体的全路段连续切片做不到**(用户想沿全路一刀切无法实现)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 持久化(沿用 2026-06-17 §7 策略)
|
||||
|
||||
- **必存**:`VolumeBuildParams`(源数据引用 + 插值模型/参数 + 色阶)+ `GridSpec`(origin/spacing/dims,锚定切片/异常坐标)。
|
||||
- **可选明细**:`ScalarVolume`(double)。A 阶段仍是内存 mock(`StoredVolume.cachedGrid`),**未真实落盘**——这是 A 与用户"保存插值后体"诉求的**主要差距**。
|
||||
- 用户要的两种保存:①参数 ②插值后明细 —— A 的 ②目前只有内存缓存,需补一段最朴素的 double 体落盘(raw + sidecar)才算满足;但 double 全路段落盘巨大,不实用。
|
||||
|
||||
---
|
||||
|
||||
## 5. 评估
|
||||
|
||||
**优点**
|
||||
- 改动最小:渲染/切片/异常/详情**全部现成**,只加雷达解析与适配。
|
||||
- 路径已验证,风险低,可最快出"雷达能进三维场景"的可见效果。
|
||||
|
||||
**缺点/限制**
|
||||
- `double` + 400³ → **撑不起全路段原始分辨率**,只能粗览或分段小体。
|
||||
- 明细落盘不实用(double 体积过大),用户"算一次秒开"诉求难真正成立。
|
||||
- 单线程 IDW 在雷达量级偏慢。
|
||||
- 把结构化的雷达数据(沿线/深度本就规则)当无结构散点做 3D IDW,**算力浪费**(见方案 B 的结构化插值优化)。
|
||||
|
||||
**适用**
|
||||
- 单条短测线 / 粗分辨率概览 / 快速打通链路的第一步。
|
||||
- **不适合**作为全路段完整体验的最终方案。
|
||||
|
||||
---
|
||||
|
||||
## 6. 工作量与落地顺序
|
||||
|
||||
1. `.iprb`/`.iprh` 解析 + 地理配准 + GPR→PointSet(地基,~中)。
|
||||
2. 雷达 ddCode 接入维度分类 + DTO(~小)。
|
||||
3. 直接复用 `IdwInterpolator`/`buildVoxel`/`SliceTool`,按 ≤400³ 降采样建体(~小)。
|
||||
4. (可选)double 明细落盘最朴素实现(~小,但不推荐用于全路段)。
|
||||
|
||||
**结论(修订,评审定)**:**no-go(作为独立交付步),并入 B。** A 没有任何 B 不需要的独立资产,渲染/切片/异常/持久化骨架 A、B 共享,地基(§2)也共享。`double`+400³+暴力 IDW 三条硬约束使 A 的 GPR 产物既无业务分辨率、又无法落盘秒开,连"最小基线该兑现的可用产物"都达不到。
|
||||
- **唯一抽出先做的独立里程碑 = §2 共有地基**(`.iprb`/`.iprh` 解析 + 14 通道配准 + GPR→PointSet + ddCode 接入),A/B/C 都要。
|
||||
- "复用 double+400³ 管线建退化体"这一步**不单独交付**,直接在 B 的 int16+结构化建体上落地,避免做一遍注定被 B 推翻的降级体。
|
||||
- **全路段完整体验走 B。**
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
# GPR 三维体 · 方案 B:全路段 int16 整卷上 GPU(升级现有管线,推荐)
|
||||
|
||||
- 日期:2026-06-23
|
||||
- 范围:把全路段 GPR 按**物理分辨率(5~10cm)**插值成**单个 int16 体(~5~10GB)**,整卷传成 GPU 3D 纹理,切片/体绘制都丝滑。**不做金字塔/核外**,靠"右尺寸 + int16"让单体进显存。
|
||||
- 定位(2026-06-23 用户定):**与 C 对等的两条已承诺路线之一,两者都做、用户运行时按需切换**。B 走"整卷进显存"路线(在现有管线上有针对性升级),适合能装进显存的体;超显存的体走 C。经同一 `IVolumeRenderSource` 接口切换。
|
||||
- **✅ 评审结论(2026-06-23,opus):Go(条件式)。** 体积测算、int16+结构化插值、渲染/切片复用均成立。**开工前必改 3 项**:①落盘方案(VTKHDF Writer 写不了 ImageData,必须改裸分块,见 §3);②量化贯穿传递函数/色阶/反量化(见 §3.5);③一个 vtkShortArray→GPU 体绘制的小验证 spike。
|
||||
- 测试数据(明星路):同方案 A。物理分辨率依据:450MHz、土速 0.1m/ns → 波长 λ≈0.22m、垂向分辨率 ≈5cm;网格细过 ~5cm 即过采样。
|
||||
|
||||
---
|
||||
|
||||
## 1. 设计意图
|
||||
|
||||
A 的瓶颈是 `double` + 400³ 上限,撑不起全路段。B 针对性拆掉这两条,**保持"整卷进显存"这一最省力的渲染架构不变**:
|
||||
|
||||
```
|
||||
.iprb/.iprh → 结构化建体(仅横向插值)→ ScalarVolumeI16(int16)
|
||||
→ vtkImageData + vtkShortArray → vtkSmartVolumeMapper(整卷进显存)
|
||||
切片:复用 SliceTool(reslice 对 int16 image 同样工作)
|
||||
持久化:VolumeBuildParams + int16 明细【真实落盘 + 分块压缩】
|
||||
```
|
||||
|
||||
体积测算(明星路全路段,依据真实头文件):
|
||||
|
||||
| 网格 (横×纵×深) | 体素数 | int16 体积 | 进显存? |
|
||||
|---|---|---|---|
|
||||
| 10cm×10cm×2cm | 22230×270×400 ≈ 2.4G | **4.8GB** | 12GB+ 显卡可 |
|
||||
| 10cm×10cm×原生821 | ≈ 4.9G | **9.8GB** | 16~24GB 显卡可 |
|
||||
| 5cm×5cm×5cm | 44460×540×160 ≈ 3.8G | **7.7GB** | 16GB+ 显卡可 |
|
||||
|
||||
**关键:5~10GB 全部在单显卡可承载区间——不需要金字塔/核外。** 那个 39TB 是 cm 级横向过采样的产物,物理无意义。
|
||||
|
||||
---
|
||||
|
||||
## 2. 三处核心升级(相对 A)
|
||||
|
||||
### 2.1 dtype:引入 int16 体(4× 内存削减)
|
||||
- 现 `ScalarVolume` 全仓库是 `double`(`Field.hpp:8-26`),直接改全局风险大。**方案**:新增并行的 `ScalarVolumeI16`(`std::vector<int16_t>` + 同样行优先布局 + 量化标定 `scale/offset` 把物理值映射到 int16),雷达走 int16 路径,反演剖面仍走 double。
|
||||
- 渲染:`buildVoxel` 增加 int16 重载 → `vtkImageData` + `vtkShortArray`。**评审已证实** GPU 体绘制原生支持 short(`vtkSmartVolumeMapper`→`vtkOpenGLGPUVolumeRayCastMapper`→`vtkVolumeTexture` 走 GL 16-bit 整型纹理)。NaN/空值改用 int16 哨兵(如 `INT16_MIN`)+ 不透明度传递函数透明(与现 `VoxelActor.cpp:23-24,68-72` 同构)。
|
||||
- 收益:同体素数内存/显存/磁盘 = double 的 1/4,是"让全路段进显存"的关键杠杆。雷达原始本就是 int16,**无精度损失**。
|
||||
- **适配面比"加个重载"大(评审 HIGH)**:`ScalarVolume`(double) 被 `VolumeGrid`/`buildVoxel`/`finalizeVolume`/`Api3dRepository`(`StoredVolume.cachedGrid`、`loadVolume` 回调签名、`VolumeInfo` 统计) 一路引用。int16 体需让这些**要么模板化、要么并行一套带量化 meta 的变体**。隔离方向(雷达 int16 / 反演剖面仍 double)对,但工作量按"中"算偏乐观,§6 已上调。
|
||||
|
||||
### 2.2 维度上限:由物理分辨率决定,拆掉 400³ 死值
|
||||
- 移除/放宽 `kMaxVolumeDim=400`(`VolumeBuilder.hpp:8`),改为按 `cellXY/cellZ` 与场景范围算出 dims,并加**显存预算守卫**(建体前估算 `nx·ny·nz·2B`,**并留余量**:实际还要叠加传递函数纹理 + 颜色/深度 FBO,按裸标量算偏紧——评审 MEDIUM)。
|
||||
- **显存探测无可靠跨厂商 API(评审 MEDIUM)**:OpenGL 无统一"可用显存"查询。实践只能 try-upload-on-fail 或留保守阈值。
|
||||
- **免费兜底(评审发现,spec 原漏报)**:`vtkSmartVolumeMapper` 自带 `MaxMemoryInBytes` + `LowResResample`(`vtkSmartVolumeMapper.h:194-211,373-379`),体超显存时**自动降采样重采样到可容纳**——等于"概览体"免费实现。目标机显存小时优先用它 + 按区域细化,**仍在 B 框架内,不必转 C**。
|
||||
- 默认网格由雷达物理分辨率给(横 5~10cm、深 2~5cm),不让用户填出过采样网格。
|
||||
|
||||
### 2.3 插值:结构化建体,不做 3D 散点 IDW
|
||||
- **重要架构洞察**:雷达数据沿测线(X)、深度(Z)**本就是规则密采样**,只有横向(Y)的 14 通道是稀疏的。所以"插值成体"≠ 3D 无结构散点插值,而是:
|
||||
- X、Z 方向按道距/采样直接落格(重采样/最近邻,廉价);
|
||||
- **只在 Y 方向对 14 通道做 1D 插值**填充横向空隙。
|
||||
- 这比 A 复用的全 3D IDW **快一两个数量级**,且单线程可接受;若仍慢,Y 向插值天然可并行(QtConcurrent/std::thread,按 X 切片并行)。
|
||||
- 保留 `IInterpolator` 抽象,新增 `GprStructuredBuilder` 实现,与 IDW 并列。
|
||||
|
||||
---
|
||||
|
||||
## 3. 持久化(真正满足"算一次、之后秒开")
|
||||
|
||||
用户要两种保存,B 把第二种做实:
|
||||
|
||||
- **方式一(参数档)**:`VolumeBuildParams`(源 .iprb 引用 + 建体参数 + 色阶 + 量化 scale/offset)+ `GridSpec`。小、可复算、详情面板展示。
|
||||
- **方式二(明细缓存,升级为真实落盘)**:int16 体 **分块写盘 + 逐块压缩**:
|
||||
- **⚠ 不能用 `vtkHDFWriter`(评审 CRITICAL,两个评审独立证实)**:VTK 9.6 的 `vtkHDFWriter` **写不了 `vtkImageData`/规则体**——它只支持 PolyData/UnstructuredGrid/Partitioned/MultiBlock(`vtkHDFWriter.h:6-9,232-235`,无 ImageData 写重载)。`vtkHDFReader` 能**读** ImageData,但 Writer 不能**写**,读写不对称。"补个 IOHdf 组件就能 VTKHDF 原生落盘体"的说法**错误**。
|
||||
- **首选(改正后)**:**自定义 raw int16 分块 + sidecar(GridSpec/量化 scale·offset/vmin·vmax/分块索引) + 逐块 zlib(VTK 自带 `vtkzlib`,无需新依赖)**。分块布局从一开始就设计好,C 的"切片核外"可几乎免费复用同一格式。
|
||||
- 备选:直接用底层 `vtkhdf5` C API 自写 chunked dataset(绕过 `vtkHDFWriter`),获得 HDF5 生态兼容;成本高于 raw 分块。
|
||||
- 不引入独立 zstd/blosc(vcpkg 未含;如需更高压缩比再加)。
|
||||
- **加载**:有明细 → 读盘(可 mmap)→ 整卷上显存;无明细 → 按参数后台线程重算落缓存(复用现有重算逻辑 `Api3dRepository.cpp:212-225`,从 mock 升级为真实落盘)。
|
||||
- **后台重算不阻塞 UI(评审 MEDIUM)**:现 `loadVolume` 回调**在主线程**(mock 同步)。改为工作线程建体/重算后,回调要**跨线程编组**回 UI(Qt 信号 / `QMetaObject::invokeMethod`),这是线程模型改动,需显式设计。
|
||||
- **加载耗时别承诺"秒开"(评审 LOW)**:5~10GB 上传 GPU(约 1~5s) + 压缩明细解压,实际**约 10s 量级**。明星路单体压缩后约 2~6GB,读盘+解压秒~十秒级——比每次重算快得多,用户"算一次之后快读"诉求成立。
|
||||
|
||||
### 3.5 量化贯穿(评审 HIGH,正确性问题,必做)
|
||||
int16 渲染标量是量化域 `q = round((v_phys - offset)/scale)`,不是物理值。必须把量化贯穿全链,否则色阶/读数全错:
|
||||
- **传递函数 / 不透明度**:现 `VoxelActor.cpp:62-72` 用物理 `vmin/vmax` 加控制点 → int16 路径必须改成在**量化域 `qmin/qmax`** 采样。
|
||||
- **切片色阶 LUT**:`buildLut(cs,vmin,vmax)`(`SliceTool.cpp:37`)同理喂量化域。
|
||||
- **反量化显示**:取值光标 / 异常详情 / 数据详情面板展示给用户的值必须 `v_phys = q*scale + offset` 反量化回物理量。
|
||||
- `scale/offset` 存入 `VolumeBuildParams` 并随 `VolumeInfo` 传递。
|
||||
|
||||
---
|
||||
|
||||
## 4. 渲染与交互(基本复用,验证为主)
|
||||
|
||||
- 整卷 `vtkSmartVolumeMapper`(现有),int16 image 直接喂;确认 9.6 的 GPU ray cast 对 short 标量正常。
|
||||
- 切片 `SliceTool`(`vtkImagePlaneWidget`/`vtkImageReslice`)对 int16 image 同样工作(CPU reslice 与 dtype 无关);丝滑度由"整卷已在显存"保证。
|
||||
- 异常/详情/色阶:复用现有 3D 分析栏链路。
|
||||
|
||||
---
|
||||
|
||||
## 5. 评估
|
||||
|
||||
**优点**
|
||||
- **全路段完整连续体 + 最好体验**,切片/体绘制丝滑,且**不必上金字塔/核外**。
|
||||
- 复用现有渲染/切片/异常/详情,主要新增 = int16 路径 + 结构化建体 + 真实落盘。
|
||||
- int16 + 结构化插值同时解决"内存/显存/磁盘大"和"插值慢"。
|
||||
- 明细真实落盘,"算一次秒开"成立。
|
||||
|
||||
**缺点/风险(评审分级)**
|
||||
- **CRITICAL(已在 §3 修正)**:`vtkHDFWriter` 写不了 ImageData → 落盘改裸 int16 分块+zlib。有现成退路,非方案推翻,但落盘是"自写格式"的中等工程,非"补组件"。
|
||||
- **HIGH**:量化未贯穿传递函数/色阶/反量化会导致颜色与读数错(§3.5 已补设计)。
|
||||
- **HIGH**:int16 适配面被低估(`VolumeGrid`/`loadVolume` 回调/`StoredVolume`/`VolumeInfo` 均需带量化 meta),非单点重载。
|
||||
- **MEDIUM**:后台重算从主线程改工作线程,跨线程回调编组需设计;显存无可靠查询、预算按裸标量偏紧;结构化落格假设道近似等距,GPS 抖动需沿弧长重采样。
|
||||
- **LOW**:5~10GB 加载约 10s 级,UX 别承诺"秒开"。
|
||||
- 单巨体无部分加载:打开即载全量。
|
||||
- int16 路径是对核心类型的扩展,需谨慎不污染 double 主路径(用并行类型隔离)。
|
||||
|
||||
**适用**
|
||||
- 当前明星路这一档(及绝大多数单路段工程)。**这是默认推荐。**
|
||||
|
||||
---
|
||||
|
||||
## 6. 工作量与落地顺序
|
||||
|
||||
0. **【开工前】验证 spike(~半天)**:`vtkShortArray` 填小 `vtkImageData` → `vtkSmartVolumeMapper` 跑通 GPU ray cast + 量化域传递函数,确认 GPU 路径与颜色正确。
|
||||
1. 地基(同 A §2):`.iprb`/`.iprh` 解析 + 配准 + 接入(~中)。
|
||||
2. `ScalarVolumeI16` + `buildVoxel` int16 重载 + 哨兵透明 + **量化贯穿传递函数/LUT/反量化(§3.5)**(~中大,评审上调:适配面比单点重载大)。
|
||||
3. `GprStructuredBuilder`(X/Z 落格 + Y 向插值,可并行;GPS 抖动需沿弧长重采样)替代全 3D IDW(~中)。
|
||||
4. 显存预算守卫(留 FBO/传函余量)+ `LowResResample` 概览兜底 + 物理分辨率默认网格(~小)。
|
||||
5. 明细真实落盘(**raw int16 分块 + sidecar + zlib,不用 vtkHDFWriter**)+ 后台重算(**含跨线程回调编组**)(~中大,评审上调:自写分块格式 + 线程模型)。
|
||||
6. 渲染/切片对 int16 的验证(~小)。
|
||||
|
||||
**结论(评审:Go 条件式 / 用户定:与 C 都做)**:B 用"右尺寸 + int16 + 结构化插值"在现有架构上拿到全路段完整体验。**前置条件**:§3 落盘章节已从 VTKHDF 改为裸分块、§3.5 量化设计已补、第 0 步 spike 通过——满足后即可进入实现。B 与 C 经同一 `IVolumeRenderSource` 并存,用户按数据规模在两者间切换(能进显存走 B、超显存走 C),落盘格式两者共用(B 的裸分块是 C 分块/金字塔的基座)。
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
# GPR 三维体 · 方案 C:分块 + 金字塔 + 核外(应对超大数据量)
|
||||
|
||||
- 日期:2026-06-23
|
||||
- 范围:当单个三维体在**合理分辨率下仍超显存/内存**时(几十公里测线、多工区合并、或必须超精网格),采用业界处理 TB 级体的标准架构:**分块(bricking) + 多分辨率金字塔(LOD) + 逐块压缩 + 核外按需加载(out-of-core)**。
|
||||
- 定位(2026-06-23 用户定):**与 B 对等的两条已承诺路线之一,两者都做、用户运行时按需切换**(不是 B 的兜底/预案)。对标地震(OpenVDS/ZGY)、数字病理/显微(OME-Zarr)。B 适合能整卷进显存的体、C 适合超显存/超大范围的体,由用户按数据选择,经同一 `IVolumeRenderSource` 接口切换。
|
||||
- POC(用户定):C 的 POC **含"最小但真实的核外分页器"**,正面验证最高风险点(分页器在 VTK 上可行性、块边接缝、LOD 闪烁、热路径解压),"POC 过 ⇒ 可落地"。
|
||||
- 前置:地基(`.iprb` 解析/配准/接入) 与 int16 + 结构化建体 与方案 B 共用。
|
||||
- **⚠ 评审结论(2026-06-23,opus)+ 用户决策**:**Go——C 是已承诺路线,与 B 都做。** 架构对标业界无误。开工注意:①`vtkHDFWriter` 写不了规则体(与 B 同源 CRITICAL,§2.2 已改正为裸 HDF5/分块);②整卷核外分页器无 VTK 开箱基础(CRITICAL,月级,**POC 即用最小真实分页器正面验证**);③`vtkSmartVolumeMapper` 自带 `LowResResample` 仅作 C 内的降质兜底手段,不替代 C。落地顺序:裸分块格式 → 切片核外 → 整卷核外分页器。
|
||||
|
||||
---
|
||||
|
||||
## 1. C 的适用场景(用户在 B/C 间按需选择的依据,非"门槛")
|
||||
|
||||
C 与 B 并存,用户对某个体选 C 而非 B,典型是:
|
||||
- 合理分辨率(5~10cm)下单体 int16 体积 **超过本机显存**;
|
||||
- 测线长一个数量级(几十 km)或多工区拼接成连续大体;
|
||||
- 需要在内存/显存恒定下浏览任意大的体。
|
||||
|
||||
B 与 C 同为成品、经同一 `IVolumeRenderSource` 切换;小体走 B(整卷最省力)、大体走 C(核外不爆内存),**由用户按数据选**。
|
||||
|
||||
---
|
||||
|
||||
## 2. 架构
|
||||
|
||||
```
|
||||
建体(int16) → 分块(brick 64³/128³) → 逐块压缩 + 每块 min/max
|
||||
→ 多分辨率金字塔(全分辨率 / 1/2 / 1/4 / 1/8 …)
|
||||
→ 写入分块格式文件(离线/后台一次)
|
||||
渲染 → 核外分页器:按相机视野 + LOD 选块 → 解压载入显存 → 相机移动换入换出
|
||||
内存/显存只驻留当前所需块(数 GB),与总体积无关
|
||||
切片 → 只读切面相交的块(最便宜的子集);等值面靠每块 min/max 剔除
|
||||
交互 → 拖动用粗 LOD,停下加载全分辨率;沿拖动方向预取
|
||||
```
|
||||
|
||||
### 2.1 存储格式(分块 + 金字塔 + 压缩)
|
||||
- **⚠ 不能依赖 `vtkHDFWriter`(评审 CRITICAL,与 B 同源)**:VTK 9.6 的 `vtkHDFWriter` **写不了 `vtkImageData`/规则体**(`vtkHDFWriter.h:6-9,232-235`,仅 PolyData/UnstructuredGrid/composite);且**无规则体的多分辨率 overview**(头文件唯一多级机制是 AMR 层级 `vtkHDFReader.h:203-209`,非金字塔)。所以"补 IOHdf 组件即可 VTKHDF 落盘 + overview"**不成立**——原 spec 的"待验证"实为"基本不支持"。
|
||||
- **首选(改正后)**:**裸 `vtkhdf5` C API 自写 chunked dataset**(HDF5 原生 chunking + zlib 逐块压缩 + 随机访问),金字塔层作为多个 HDF5 dataset **自行组织**;或自定义 brick 文件(裸分块 + 索引),每块 zlib + 头存 min/max + LOD 偏移表。**与 B 的落盘层统一**(B 本就要改成裸分块,正好一并设计成可分块/可多级)。
|
||||
- 不引入独立 zstd/blosc(vcpkg 未含);如压缩比不足再评估加入。
|
||||
|
||||
### 2.2 渲染端核外分页(C 的真正难点)
|
||||
- **VTK 不开箱提供大体的 out-of-core GPU 体绘制(评审证实)。** 注意两个相关但不够用的开箱件:
|
||||
- `vtkMultiBlockVolumeMapper` **存在但不是分页器**(`vtkMultiBlockVolumeMapper.h:14-21`:试图"同时加载所有块",仅 GPU 分配失败才退化逐块重载)——它给的是"多块同时渲染 + 抖动抗块边接缝",**没有按视野换入换出/LOD 选块/预取**。要复用它做渲染层,须在喂数据前自己完成"只放视野块"的筛选。
|
||||
- `vtkSmartVolumeMapper` 的 `LowResResample`(见 B §2.2)是"自动降质看全貌",**不是核外**——它是 C 之前的免费兜底,不是 C 的实现。
|
||||
- 整卷核外分页器须**从零自建**(选块/LOD/LRU/解压/换入换出/预取)。三条路:
|
||||
1. **切片优先(推荐先做)**:切片只需读相交的块,复用 `vtkImageReslice` 对"当前块集合"重采样。**这条最易落地**,能先拿到"超大体看切片"的能力,不碰整卷核外。
|
||||
2. **自建 LOD + brick 分页**:在 `vtkSmartVolumeMapper` 之上,把视野内块按 LOD 作为多个 `vtkImageData`/`vtkMultiBlockDataSet` 动态加载/淘汰。整卷透明体绘制走这条,**工作量最大**。
|
||||
3. **集成 OpenVDS**(地震库):能力最全但**重依赖**(vcpkg 未含,需自带 + 适配 VTK),适配成本高。
|
||||
- 建议:**先做 1(切片核外),整卷体绘制核外(2)列为后续**。
|
||||
|
||||
### 2.3 建体/金字塔流水线
|
||||
- 复用方案 B 的 int16 + 结构化建体产出全分辨率体 → 分块 → 逐级降采样建金字塔 → 逐块压缩落盘。
|
||||
- **离线/后台执行一次**,结果即持久化产物(C 的"保存插值后体"天然就是这个分块金字塔文件)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 持久化
|
||||
|
||||
- C 的存储格式**本身就是方式二(明细缓存)**:分块 + 金字塔 + 压缩,是"算一次、之后秒开"的载体,且读取只碰视野块、内存可控。
|
||||
- 方式一(参数档 `VolumeBuildParams`+`GridSpec`)仍保留,用于复算/详情/校验。
|
||||
- 切片/异常坐标仍锚定 `GridSpec`(与 A/B 一致),保证跨 LOD 一致。
|
||||
|
||||
---
|
||||
|
||||
## 4. 评估
|
||||
|
||||
**优点**
|
||||
- **不设规模上限**:任意大小(几十 GB ~ TB)皆可,内存/显存恒定在数 GB。
|
||||
- 切片只读相交块、等值面块级剔除、拖动 LOD 降级 —— 大体交互可做到流畅。
|
||||
- 与业界(地震/医学)成熟路径同构,可借鉴现成设计。
|
||||
|
||||
**缺点/风险(评审分级)**
|
||||
- **CRITICAL**:`vtkHDFWriter` 写不了规则体 → 落盘须自写裸 HDF5/分块(§2.1 已改),是中大件,非"补组件"。
|
||||
- **CRITICAL**:整卷核外分页器**无 VTK 开箱基础**,从零自建(选块/LOD/LRU/预取),月级且风险集中;或集成重依赖 OpenVDS。
|
||||
- **HIGH**:VTKHDF 无规则体多分辨率 overview,金字塔须全自管。
|
||||
- **HIGH**:压缩块解压在**交互热路径**(拖动每帧换块解压),CPU 可能成瓶颈——不止 IO 放大。
|
||||
- **MEDIUM**:LOD 降采样的半像素偏移 → 异常拾取**跨层落点漂移**("GridSpec 锚定保证一致"未覆盖此情形)。
|
||||
- **MEDIUM**:块边接缝(MultiBlock 抖动可部分缓解)/ LOD 切换闪烁(需 morphing/淡入自解决)。
|
||||
- 复杂度高 → 维护成本与缺陷面大。
|
||||
- **依赖断点(评审)**:C 声称"复用 B 的落盘",而 B 那层须先从 VTKHDF 改成裸分块 HDF5——C 的"地基已就绪"前提依赖 B 先这么做。
|
||||
|
||||
**适用**
|
||||
- 仅当数据真正超出方案 B 的单显卡承载。**当前明星路用不到。**
|
||||
|
||||
---
|
||||
|
||||
## 5. 工作量与落地顺序(仅在需要时启动)
|
||||
|
||||
1. 地基 + int16 + 结构化建体(与 B 共用,若已做则复用)。
|
||||
2. 分块格式 + 逐块压缩 + 每块 min/max(VTKHDF 或自定义)(~中大)。
|
||||
3. 多分辨率金字塔生成流水线(后台一次)(~中)。
|
||||
4. **切片核外**:按切面读相交块重采样(~中)—— 先交付这条。
|
||||
5. 整卷体绘制核外分页器 + LOD 拖动降级 + 预取(~大,后续)。
|
||||
6. 显存/内存缓存与淘汰策略(~中)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 与 A/B 的关系
|
||||
|
||||
- 能力:A ⊂ B ⊂ C;成本同序递增。
|
||||
- A、B 共享渲染/切片现有架构;**C 是不同的存储+渲染架构**(分块+核外),是 B 撞到显存天花板后的演进,不是平行替代。
|
||||
- **推荐策略**:现在做 B 满足明星路与多数工程;把 C 的"分块格式 + 切片核外"作为**预案**,待出现真正超大数据再启动;整卷体绘制核外列为最后一档。
|
||||
|
||||
> **切片核外"最易落地"有隐藏前提(评审)**:它要求分块格式**已做完**才能"读相交块"。所以"先交付切片核外"≠ 可跳过分块——§5 顺序(先分块格式再切片核外)正确,别误读为切片核外是独立小工程。
|
||||
|
||||
**结论(用户定:Go,C 与 B 都做)**:C 的总体架构成立、对标 OpenVDS/ZGY/OME-Zarr 无误,是与 B 对等的已承诺成品;工程最重、渲染端整卷核外非开箱、落盘须裸 HDF5(已改)。
|
||||
- **落盘与 B 统一**:裸分块格式(不依赖 vtkHDFWriter),B 落盘本就改裸分块,C 的分块/切片核外在同一格式上增量获得。
|
||||
- **整卷核外分页器**是 C 的最高风险件(两个 CRITICAL + 热路径解压 HIGH,月级),**POC 即以"最小真实分页器"正面验证**,确保"过了能落地"。
|
||||
- **B/C 切换**:经 `IVolumeRenderSource` 运行时切换,用户按数据选;`LowResResample` 是 C 内的降质手段,不替代 C 也不替代用户选择。
|
||||
Loading…
Reference in New Issue