1053 lines
49 KiB
Markdown
1053 lines
49 KiB
Markdown
# 三维雷达体接入(规范化格式)Implementation Plan
|
||
|
||
> **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:** 让一条"规范化 `.head/.data/.cor`"三维雷达测线在桌面 app 里**登记成 `dd_radar_3d` DS**并渲染成体,复用现成切片/异常工具。
|
||
|
||
**Architecture:** 在稳定的 `core::BuiltI16` 汇聚点接入新格式——新写"规范化 reader + bridge"产出 `BuiltI16` → `builtI16ToVolumeGrid` → `createRadarVolumeGrid`;上层走 **DS 优先**:`registerRadarDataset` 把体以 `ddCode=dd_radar_3d` 登记进 `volumes_`(运行期路由只认 `volumes_` 成员,故 `isVolumeDataset`/`loadVolume`/`addDatasetAsync` 零改动)→ 勾选 → 渲染。先把现有 `Gpr3dvVolumeBridge` 的"扫值域→量化→通道插值→填体→spacing"抽成共享 helper(消 DRY),Impulse 与规范化两路同调。
|
||
|
||
**Tech Stack:** C++17、Qt6、VTK 9.6.2、GoogleTest/CTest、CMake(Ninja)、vcpkg。
|
||
|
||
## Global Constraints
|
||
|
||
- C++17;命名空间 `geopro::io::gpr` / `geopro::data`;类型 `PascalCase`、函数 `camelCase`、成员 `snake_`、常量 `kPascalCase`。
|
||
- 体轴映射固定:**X=道(nx=K) / Y=通道(ny=M) / Z=采样(nz=N)**。
|
||
- 数据体磁盘主序固定:**position-major**,扁平索引 `((t*M + c)*N + s)`(t=道, c=通道, s=采样;采样最快、道最慢)。⚠️ 与体内存布局(`((k*ny+j)*nx+i)`,道最快)相反 → **逐体素散射填值,严禁 memcpy/整块拷贝**。
|
||
- 量化:复用 `geopro::core::Quant`(`scale=(vmax-vmin)/64000`、`offset=中点`);GPR 体稠密无空洞 → **不置 `kBlank`**;不特判 `-32768`(饱和真实值,经 `toQ` 落普通区间)。
|
||
- `BITS` 真源 = `.head` 的 `BITS` 字段(P0 仅支持 16-bit;32-bit 抛 `not implemented`)。`ENDIAN_TYPE` 1=小端/2=大端。
|
||
- 通道插值复用纯函数 `geopro::io::gpr::planChannelInterpolation(offsets, targetDy)`(默认 `targetDy=0.025`),通道横向偏移取自 `.head` 的 `CH_X_OFFSETS`,**绝不跨线**。
|
||
- 内存:app 体是 `std::vector<double>`(int16 的 4×);单线默认 `coarse=4`(峰值 <0.5GB)。多线/全分辨率不在本计划(走 P2 核外)。
|
||
- 构建/测试:⚠️ **`cmake`/`ctest`/`cl` 不在 PATH** —— 必须经 `D:\Git\lanbingtech\geopro\build.bat`(自带 vcvars + VS cmake/ctest)。各 task 步骤里写的 `cmake --build ... && ctest -R ...` 是**意图**,实际跑:(1) **构建+全测**:PowerShell `D:\Git\lanbingtech\geopro\build.bat test`(构建 geopro_tests + ctest 全跑,~68s,**基线 444/444 全绿**——经 build.bat 时 proj.db 等都能找到);(2) **聚焦单测**(构建后快速复跑):`build/release/tests/geopro_tests.exe --gtest_filter=Pattern*`(雷达测试不依赖 proj.db,可直接跑);(3) **app**:`build.bat app`。**重链前关掉在跑的 app/exe**(LNK1104)。**别删** `build/release`。
|
||
- 提交:Conventional Commits(`feat:`/`refactor:`/`test:`),分支 `feat/3d-radar-volume-ingest`(已存在)。
|
||
- 失败排查:桌面日志 `%LOCALAPPDATA%/Geomative/Geopro3/logs/geopro_*.log`。
|
||
|
||
---
|
||
|
||
### Task 1: 抽出共享建体 helper `assembleRadarVolume`(消 HIGH-1 DRY)
|
||
|
||
把 `Gpr3dvVolumeBridge.cpp:132-197` 的"扫值域→Quant→通道插值→逐体素填→spacing"抽成格式无关的共享函数,Impulse 路改为调用它,行为不变(现有 gtest 兜底)。
|
||
|
||
**Files:**
|
||
- Create: `src/io/gpr/RadarVolumeAssembler.hpp`
|
||
- Create: `src/io/gpr/RadarVolumeAssembler.cpp`
|
||
- Modify: `src/io/gpr/Gpr3dvVolumeBridge.cpp:99-197`(dims 之后改为组 `RadarCubeDesc` + sampler 调 helper)
|
||
- Modify: `src/io/CMakeLists.txt`(加 `RadarVolumeAssembler.cpp`)
|
||
- Test: `tests/io/gpr/test_radar_volume_assembler.cpp`
|
||
- Modify: `tests/CMakeLists.txt`(加新测试源)
|
||
|
||
**Interfaces:**
|
||
- Produces:
|
||
```cpp
|
||
namespace geopro::io::gpr {
|
||
struct RadarCubeDesc {
|
||
int channels = 0; // M
|
||
int traces = 0; // K(源道数, 下采样前)
|
||
int samples = 0; // N
|
||
std::vector<double> chXOffsets; // 每通道横向偏移(米); size==channels 时启用通道插值
|
||
double dxBase = 1.0; // 道距(米) → dx = dxBase*stride
|
||
double dyWhenNotInterpolated = 1.0;// 未插值时 Y 间距(米)
|
||
double dz = 1.0; // 深度采样间距(米)
|
||
};
|
||
using CubeSampler = std::function<double(int c, int t, int s)>;
|
||
// 扫值域→Quant→通道插值(planChannelInterpolation)→逐体素填→spacing。轴 X=道/Y=通道/Z=样本。
|
||
// coarse≥1 沿道下采样(nxOut=ceil(K/coarse), dx×coarse);targetDy>0 启用通道插值。GPR 稠密不置 kBlank。
|
||
geopro::core::BuiltI16 assembleRadarVolume(const RadarCubeDesc& desc,
|
||
const CubeSampler& sample,
|
||
int coarse, double targetDy);
|
||
}
|
||
```
|
||
- Consumes: `geopro::core::{BuiltI16,Quant,ScalarVolumeI16}`、`planChannelInterpolation`。
|
||
|
||
- [ ] **Step 1: 写失败测试**
|
||
|
||
`tests/io/gpr/test_radar_volume_assembler.cpp`:
|
||
```cpp
|
||
#include <gtest/gtest.h>
|
||
#include "core/algo/GprVolumeBuilder.hpp"
|
||
#include "io/gpr/RadarVolumeAssembler.hpp"
|
||
|
||
using geopro::io::gpr::RadarCubeDesc;
|
||
using geopro::io::gpr::assembleRadarVolume;
|
||
|
||
// 2 道 × 3 通道 × 4 采样,值 = 100*c + 10*t + s。不插值(chXOffsets 空)、coarse=1。
|
||
TEST(RadarVolumeAssembler, AxisMapAndQuantRoundTrip) {
|
||
RadarCubeDesc d;
|
||
d.channels = 3; d.traces = 2; d.samples = 4;
|
||
d.dxBase = 0.1; d.dyWhenNotInterpolated = 0.5; d.dz = 0.05;
|
||
auto sampler = [](int c, int t, int s) { return 100.0 * c + 10.0 * t + s; };
|
||
|
||
const geopro::core::BuiltI16 b = assembleRadarVolume(d, sampler, /*coarse=*/1, /*targetDy=*/0.0);
|
||
|
||
EXPECT_EQ(b.vol.nx(), 2); // 道
|
||
EXPECT_EQ(b.vol.ny(), 3); // 通道(未插值=原通道数)
|
||
EXPECT_EQ(b.vol.nz(), 4); // 采样
|
||
EXPECT_DOUBLE_EQ(b.spacing[0], 0.1);
|
||
EXPECT_DOUBLE_EQ(b.spacing[1], 0.5);
|
||
EXPECT_DOUBLE_EQ(b.spacing[2], 0.05);
|
||
EXPECT_NEAR(b.vminPhys, 0.0, 1e-9); // c0,t0,s0
|
||
EXPECT_NEAR(b.vmaxPhys, 213.0, 1e-9); // c2,t1,s3 = 200+10+3
|
||
// 反量化对位:体素(道 t=1, 通道 c=2, 采样 s=3) 应≈213(量化误差内)。
|
||
const double recon = b.quant.toPhys(b.vol.at(1, 2, 3));
|
||
EXPECT_NEAR(recon, 213.0, b.quant.scale);
|
||
}
|
||
|
||
// coarse=2:4 道 → nxOut=2,dx×2。
|
||
TEST(RadarVolumeAssembler, CoarseDownsamplesTracesAndScalesDx) {
|
||
RadarCubeDesc d;
|
||
d.channels = 1; d.traces = 4; d.samples = 2; d.dxBase = 0.1;
|
||
auto sampler = [](int, int t, int s) { return 10.0 * t + s; };
|
||
const geopro::core::BuiltI16 b = assembleRadarVolume(d, sampler, /*coarse=*/2, 0.0);
|
||
EXPECT_EQ(b.vol.nx(), 2);
|
||
EXPECT_DOUBLE_EQ(b.spacing[0], 0.2);
|
||
EXPECT_NEAR(b.quant.toPhys(b.vol.at(1, 0, 0)), 20.0, b.quant.scale); // 输出道1 = 源道2
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: 跑测试确认失败**
|
||
|
||
Run: `cmake --build build/release --target geopro_tests && ctest --test-dir build/release -R RadarVolumeAssembler -V`
|
||
Expected: 编译失败(`RadarVolumeAssembler.hpp` 不存在)。
|
||
|
||
- [ ] **Step 3: 写 helper 实现**
|
||
|
||
`src/io/gpr/RadarVolumeAssembler.hpp`:
|
||
```cpp
|
||
#ifndef GEOPRO_IO_GPR_RADARVOLUMEASSEMBLER_HPP
|
||
#define GEOPRO_IO_GPR_RADARVOLUMEASSEMBLER_HPP
|
||
#include <functional>
|
||
#include <vector>
|
||
#include "core/algo/GprVolumeBuilder.hpp"
|
||
namespace geopro::io::gpr {
|
||
struct RadarCubeDesc {
|
||
int channels = 0; int traces = 0; int samples = 0;
|
||
std::vector<double> chXOffsets;
|
||
double dxBase = 1.0; double dyWhenNotInterpolated = 1.0; double dz = 1.0;
|
||
};
|
||
using CubeSampler = std::function<double(int c, int t, int s)>;
|
||
geopro::core::BuiltI16 assembleRadarVolume(const RadarCubeDesc& desc,
|
||
const CubeSampler& sample,
|
||
int coarse, double targetDy);
|
||
} // namespace geopro::io::gpr
|
||
#endif
|
||
```
|
||
`src/io/gpr/RadarVolumeAssembler.cpp`(搬 `Gpr3dvVolumeBridge.cpp:115-195` 的逻辑,数据访问换成 `sample(c,t,s)`):
|
||
```cpp
|
||
#include "io/gpr/RadarVolumeAssembler.hpp"
|
||
#include <cmath>
|
||
#include <limits>
|
||
#include <stdexcept>
|
||
#include "core/model/ScalarVolumeI16.hpp"
|
||
#include "io/gpr/GprGeometry.hpp" // planChannelInterpolation, ChannelInterpRow
|
||
namespace geopro::io::gpr {
|
||
|
||
geopro::core::BuiltI16 assembleRadarVolume(const RadarCubeDesc& d,
|
||
const CubeSampler& sample,
|
||
int coarse, double targetDy) {
|
||
if (d.channels <= 0 || d.traces <= 0 || d.samples <= 0)
|
||
throw std::runtime_error("assembleRadarVolume: 维度为空");
|
||
const int stride = coarse > 1 ? coarse : 1;
|
||
const int nxOut = (d.traces + stride - 1) / stride;
|
||
const int nz = d.samples;
|
||
|
||
// 通道插值方案(读 chXOffsets 规则化到 targetDy);退路=逐通道 identity。
|
||
std::vector<ChannelInterpRow> rows;
|
||
bool interpolated = false;
|
||
if (static_cast<int>(d.chXOffsets.size()) == d.channels && targetDy > 0.0) {
|
||
rows = planChannelInterpolation(d.chXOffsets, targetDy);
|
||
interpolated = (static_cast<int>(rows.size()) != d.channels);
|
||
}
|
||
if (rows.empty())
|
||
for (int c = 0; c < d.channels; ++c) rows.push_back({c, c, 0.0});
|
||
const int ny = static_cast<int>(rows.size());
|
||
|
||
// 扫值域 → Quant(中点 offset, 64000 裕度)。
|
||
double vmin = std::numeric_limits<double>::infinity();
|
||
double vmax = -std::numeric_limits<double>::infinity();
|
||
for (int c = 0; c < d.channels; ++c)
|
||
for (int t = 0; t < d.traces; ++t)
|
||
for (int s = 0; s < d.samples; ++s) {
|
||
const double v = sample(c, t, s);
|
||
if (v < vmin) vmin = v;
|
||
if (v > vmax) vmax = v;
|
||
}
|
||
if (!(vmin <= vmax)) { vmin = 0.0; vmax = 0.0; }
|
||
geopro::core::Quant quant;
|
||
quant.scale = (vmax > vmin) ? (vmax - vmin) / 64000.0 : 1.0;
|
||
quant.offset = 0.5 * (vmin + vmax);
|
||
|
||
// 逐(输出行 j, 输出道 to, 采样 s)填,散射写入(绝不 memcpy)。
|
||
geopro::core::BuiltI16 built;
|
||
built.vol = geopro::core::ScalarVolumeI16(nxOut, ny, nz);
|
||
for (int j = 0; j < ny; ++j) {
|
||
const int a = rows[j].a, b = rows[j].b;
|
||
const double wb = rows[j].wb, wa = 1.0 - wb;
|
||
for (int to = 0; to < nxOut; ++to) {
|
||
const int t = to * stride;
|
||
for (int s = 0; s < nz; ++s) {
|
||
const double va = sample(a, t, s);
|
||
const double vb = (b == a) ? va : sample(b, t, s);
|
||
built.vol.at(to, j, s) = quant.toQ(wa * va + wb * vb);
|
||
}
|
||
}
|
||
}
|
||
|
||
const double dy = interpolated ? targetDy : d.dyWhenNotInterpolated;
|
||
built.quant = quant;
|
||
built.origin = {0.0, 0.0, 0.0};
|
||
built.spacing = {d.dxBase * stride, dy, d.dz};
|
||
built.vminPhys = vmin;
|
||
built.vmaxPhys = vmax;
|
||
return built;
|
||
}
|
||
} // namespace geopro::io::gpr
|
||
```
|
||
在 `src/io/CMakeLists.txt` 现有 gpr 源清单里加 `gpr/RadarVolumeAssembler.cpp`(紧挨 `gpr/Gpr3dvVolumeBridge.cpp`)。在 `tests/CMakeLists.txt` 现有 `geopro_tests` 源清单里加 `io/gpr/test_radar_volume_assembler.cpp`(紧挨 `io/gpr/test_gpr3dv_volume_bridge.cpp`)。
|
||
|
||
- [ ] **Step 4: 跑测试确认通过**
|
||
|
||
Run: `cmake --build build/release --target geopro_tests && ctest --test-dir build/release -R RadarVolumeAssembler -V`
|
||
Expected: 2 个用例 PASS。
|
||
|
||
- [ ] **Step 5: Impulse 路改调 helper(保持现有 gtest 绿)**
|
||
|
||
把 `Gpr3dvVolumeBridge.cpp` 内联的"扫值域+Quant+填体+spacing"(约 :132-195)替换为:组 `RadarCubeDesc desc{channels, traces, samples, latOff, h.distanceInc>1e-9?h.distanceInc:1.0, channelSpacingY(h,channels), depthSpacingZ(h)}` + `CubeSampler sample = [&processed](int c,int t,int s){ const auto& ch=processed.volumeData[c]; if(t>=(int)ch.size()) return 0.0; const auto& tr=ch[t]; return s<(int)tr.size()? (double)tr[s] : 0.0; }`,调 `const auto built = assembleRadarVolume(desc, sample, coarse, targetDy);`,再回填 `metricsOut`(nx/ny/nz 取 `built.vol.*()`,dx/dy/dz 取 `built.spacing[*]`,vmin/vmax 取 `built.vminPhys/vmaxPhys`,before/after/load/pipeline 仍用原 Impulse 统计)。保留 `latOff`/`channelSpacingY`/`depthSpacingZ`/`meanAbsAmplitude` 不动。`#include "io/gpr/RadarVolumeAssembler.hpp"`。
|
||
|
||
- [ ] **Step 6: 跑现有 Impulse 测试确认无回归**
|
||
|
||
Run: `cmake --build build/release --target geopro_tests && ctest --test-dir build/release -R "Gpr3dv|RadarVolumeAssembler" -V`
|
||
Expected: `test_gpr3dv_volume_bridge` 全部仍 PASS + 新测试 PASS(行为不变)。
|
||
|
||
- [ ] **Step 7: Commit**
|
||
|
||
```bash
|
||
git add src/io/gpr/RadarVolumeAssembler.hpp src/io/gpr/RadarVolumeAssembler.cpp \
|
||
src/io/gpr/Gpr3dvVolumeBridge.cpp src/io/CMakeLists.txt \
|
||
tests/io/gpr/test_radar_volume_assembler.cpp tests/CMakeLists.txt
|
||
git commit -m "refactor(gpr): 抽出共享 assembleRadarVolume,Impulse 路改调(消填体 DRY)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 2: 规范化 `.head` 解析器
|
||
|
||
**Files:**
|
||
- Create: `src/io/gpr/NormalizedRadarReader.hpp`
|
||
- Create: `src/io/gpr/NormalizedRadarReader.cpp`
|
||
- Modify: `src/io/CMakeLists.txt`(加 `NormalizedRadarReader.cpp`)
|
||
- Test: `tests/io/gpr/test_normalized_radar_reader.cpp`
|
||
- Modify: `tests/CMakeLists.txt`(加新测试源)
|
||
|
||
**Interfaces:**
|
||
- Produces:
|
||
```cpp
|
||
namespace geopro::io::gpr {
|
||
struct RadarHeader {
|
||
int samples = 0; // N (SAMPLES)
|
||
int channels = 0; // M (NUMBER_OF_CH)
|
||
long lastTrace = 0; // 总扫描数=K*M (LAST_TRACE)
|
||
int traces = 0; // K = lastTrace/channels
|
||
int bits = 16; // 8/16/32 (BITS)
|
||
int endianType = 1; // 1 小端/2 大端 (ENDIAN_TYPE)
|
||
double distanceInterval = 1.0; // 道距 m (DISTANCE_INTERVAL)
|
||
double timeWindowNs = 0.0; // 时窗 ns (TIMEWINDOW)
|
||
double dielectric = 0.0; // 介电常数 (DIELECTRIC, 0=未知)
|
||
std::vector<double> chXOffsets; // 通道横向偏移 m (CH_X_OFFSETS)
|
||
};
|
||
// 解析 KEY:VALUE 文本头。缺 SAMPLES/NUMBER_OF_CH/LAST_TRACE 任一 → std::runtime_error。
|
||
// traces = lastTrace/channels(不整除抛错)。
|
||
RadarHeader parseRadarHead(const std::string& headText);
|
||
// 由 dielectric 求波速(m/ns): >0 时 0.2998/sqrt(eps),否则 0.1(默认)。
|
||
double waveVelocityMperNs(const RadarHeader& h);
|
||
// 深度采样间距(米): timeWindowNs/(samples-1) × 波速/2。samples<=1 → 0。
|
||
double depthSpacingZ(const RadarHeader& h);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 1: 写失败测试**
|
||
|
||
`tests/io/gpr/test_normalized_radar_reader.cpp`:
|
||
```cpp
|
||
#include <gtest/gtest.h>
|
||
#include "io/gpr/NormalizedRadarReader.hpp"
|
||
using namespace geopro::io::gpr;
|
||
|
||
TEST(NormalizedRadarHead, ParsesCoreFieldsAndDerivesTraces) {
|
||
const std::string head =
|
||
"SAMPLES:516\nNUMBER_OF_CH:16\nLAST_TRACE:60448\nBITS:16\nENDIAN_TYPE:1\n"
|
||
"DISTANCE_INTERVAL:0.099194\nTIMEWINDOW:96.419553\nDIELECTRIC:\n"
|
||
"CH_X_OFFSETS:0.080 0.160 0.240 0.320 0.400 0.480 0.560 0.640 0.720 0.800 "
|
||
"0.880 0.960 1.040 1.120 1.200 1.280\n";
|
||
const RadarHeader h = parseRadarHead(head);
|
||
EXPECT_EQ(h.samples, 516);
|
||
EXPECT_EQ(h.channels, 16);
|
||
EXPECT_EQ(h.lastTrace, 60448);
|
||
EXPECT_EQ(h.traces, 3778); // 60448/16
|
||
EXPECT_EQ(h.bits, 16);
|
||
EXPECT_EQ(h.endianType, 1);
|
||
EXPECT_DOUBLE_EQ(h.distanceInterval, 0.099194);
|
||
ASSERT_EQ(h.chXOffsets.size(), 16u);
|
||
EXPECT_DOUBLE_EQ(h.chXOffsets.front(), 0.080);
|
||
EXPECT_DOUBLE_EQ(h.chXOffsets.back(), 1.280);
|
||
}
|
||
|
||
TEST(NormalizedRadarHead, MissingRequiredFieldThrows) {
|
||
EXPECT_THROW(parseRadarHead("SAMPLES:516\nNUMBER_OF_CH:16\n"), std::runtime_error);
|
||
}
|
||
|
||
TEST(NormalizedRadarHead, DepthSpacingUsesDefaultVelocityWhenNoDielectric) {
|
||
const std::string head = "SAMPLES:516\nNUMBER_OF_CH:16\nLAST_TRACE:32\n"
|
||
"TIMEWINDOW:96.419553\nDIELECTRIC:\n";
|
||
const RadarHeader h = parseRadarHead(head);
|
||
EXPECT_NEAR(waveVelocityMperNs(h), 0.1, 1e-9); // 无介电 → 默认 0.1
|
||
const double dz = depthSpacingZ(h);
|
||
EXPECT_NEAR(dz, (96.419553 / 515.0) * 0.1 / 2.0, 1e-9);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: 跑测试确认失败**
|
||
|
||
Run: `cmake --build build/release --target geopro_tests 2>&1 | head`
|
||
Expected: 编译失败(头不存在)。
|
||
|
||
- [ ] **Step 3: 写实现**
|
||
|
||
`src/io/gpr/NormalizedRadarReader.hpp`:放上面 Interfaces 的结构体与三个函数声明,加 `#include <string>`/`<vector>`,`#ifndef` 守卫。
|
||
`src/io/gpr/NormalizedRadarReader.cpp`(解析 KEY:VALUE):
|
||
```cpp
|
||
#include "io/gpr/NormalizedRadarReader.hpp"
|
||
#include <cmath>
|
||
#include <map>
|
||
#include <sstream>
|
||
#include <stdexcept>
|
||
namespace geopro::io::gpr {
|
||
namespace {
|
||
std::map<std::string, std::string> parseKv(const std::string& text) {
|
||
std::map<std::string, std::string> kv;
|
||
std::istringstream in(text);
|
||
std::string line;
|
||
while (std::getline(in, line)) {
|
||
const auto pos = line.find(':');
|
||
if (pos == std::string::npos) continue;
|
||
std::string k = line.substr(0, pos), v = line.substr(pos + 1);
|
||
auto trim = [](std::string& s) {
|
||
const auto a = s.find_first_not_of(" \t\r\n");
|
||
const auto b = s.find_last_not_of(" \t\r\n");
|
||
s = (a == std::string::npos) ? "" : s.substr(a, b - a + 1);
|
||
};
|
||
trim(k); trim(v);
|
||
kv[k] = v;
|
||
}
|
||
return kv;
|
||
}
|
||
int reqInt(const std::map<std::string, std::string>& kv, const char* k) {
|
||
auto it = kv.find(k);
|
||
if (it == kv.end() || it->second.empty())
|
||
throw std::runtime_error(std::string("规范化 .head 缺字段: ") + k);
|
||
return std::stoi(it->second);
|
||
}
|
||
double optD(const std::map<std::string, std::string>& kv, const char* k, double dv) {
|
||
auto it = kv.find(k);
|
||
return (it == kv.end() || it->second.empty()) ? dv : std::stod(it->second);
|
||
}
|
||
} // namespace
|
||
|
||
RadarHeader parseRadarHead(const std::string& headText) {
|
||
const auto kv = parseKv(headText);
|
||
RadarHeader h;
|
||
h.samples = reqInt(kv, "SAMPLES");
|
||
h.channels = reqInt(kv, "NUMBER_OF_CH");
|
||
h.lastTrace = reqInt(kv, "LAST_TRACE");
|
||
if (h.channels <= 0 || h.lastTrace % h.channels != 0)
|
||
throw std::runtime_error("LAST_TRACE 不能被 NUMBER_OF_CH 整除");
|
||
h.traces = static_cast<int>(h.lastTrace / h.channels);
|
||
h.bits = static_cast<int>(optD(kv, "BITS", 16));
|
||
h.endianType = static_cast<int>(optD(kv, "ENDIAN_TYPE", 1));
|
||
h.distanceInterval = optD(kv, "DISTANCE_INTERVAL", 1.0);
|
||
h.timeWindowNs = optD(kv, "TIMEWINDOW", 0.0);
|
||
h.dielectric = optD(kv, "DIELECTRIC", 0.0);
|
||
auto it = kv.find("CH_X_OFFSETS");
|
||
if (it != kv.end() && !it->second.empty()) {
|
||
std::istringstream os(it->second);
|
||
double v;
|
||
while (os >> v) h.chXOffsets.push_back(v);
|
||
}
|
||
return h;
|
||
}
|
||
double waveVelocityMperNs(const RadarHeader& h) {
|
||
return h.dielectric > 0.0 ? 0.2998 / std::sqrt(h.dielectric) : 0.1;
|
||
}
|
||
double depthSpacingZ(const RadarHeader& h) {
|
||
if (h.samples <= 1 || h.timeWindowNs <= 0.0) return 0.0;
|
||
return (h.timeWindowNs / (h.samples - 1)) * waveVelocityMperNs(h) / 2.0;
|
||
}
|
||
} // namespace geopro::io::gpr
|
||
```
|
||
加 `src/io/CMakeLists.txt`:`gpr/NormalizedRadarReader.cpp`。加 `tests/CMakeLists.txt`:`io/gpr/test_normalized_radar_reader.cpp`。
|
||
|
||
- [ ] **Step 4: 跑测试确认通过**
|
||
|
||
Run: `cmake --build build/release --target geopro_tests && ctest --test-dir build/release -R NormalizedRadarHead -V`
|
||
Expected: 3 个用例 PASS。
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/io/gpr/NormalizedRadarReader.hpp src/io/gpr/NormalizedRadarReader.cpp \
|
||
src/io/CMakeLists.txt tests/io/gpr/test_normalized_radar_reader.cpp tests/CMakeLists.txt
|
||
git commit -m "feat(radar): 规范化 .head 解析(维度/字节序/通道偏移/深度间距)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 3: 规范化 `.data` 立方体读取(position-major,16-bit)
|
||
|
||
**Files:**
|
||
- Modify: `src/io/gpr/NormalizedRadarReader.hpp`(加 `readRadarDataCube` 声明)
|
||
- Modify: `src/io/gpr/NormalizedRadarReader.cpp`(加实现)
|
||
- Test: `tests/io/gpr/test_normalized_radar_reader.cpp`(加用例)
|
||
|
||
**Interfaces:**
|
||
- Produces:
|
||
```cpp
|
||
// 读规范化 .data → 扁平 int16 立方体,position-major 索引 ((t*M + c)*N + s)。
|
||
// 按 header.bits/endianType 解码;P0 仅 16-bit(32-bit 抛 not implemented)。
|
||
// 文件大小须 == lastTrace*samples*(bits/8),否则抛 std::runtime_error。
|
||
std::vector<std::int16_t> readRadarDataCube(const std::string& dataPath,
|
||
const RadarHeader& h);
|
||
```
|
||
- Consumes: `RadarHeader`(Task 2)。
|
||
|
||
- [ ] **Step 1: 写失败测试**
|
||
|
||
加到 `test_normalized_radar_reader.cpp`:
|
||
```cpp
|
||
#include <cstdint>
|
||
#include <filesystem>
|
||
#include <fstream>
|
||
namespace fs = std::filesystem;
|
||
|
||
TEST(NormalizedRadarData, ReadsPositionMajorCubeLittleEndian) {
|
||
// K=2 道, M=3 通道, N=2 采样; 值 v(t,c,s)=int16(100*t+10*c+s)。position-major 写。
|
||
fs::path dir = fs::temp_directory_path() / "radar_data_test";
|
||
fs::create_directories(dir);
|
||
const fs::path dp = dir / "L.data";
|
||
{
|
||
std::ofstream f(dp, std::ios::binary);
|
||
for (int t = 0; t < 2; ++t)
|
||
for (int c = 0; c < 3; ++c)
|
||
for (int s = 0; s < 2; ++s) {
|
||
std::int16_t v = static_cast<std::int16_t>(100 * t + 10 * c + s);
|
||
f.write(reinterpret_cast<const char*>(&v), sizeof(v)); // 小端(x86)
|
||
}
|
||
}
|
||
geopro::io::gpr::RadarHeader h;
|
||
h.samples = 2; h.channels = 3; h.lastTrace = 6; h.traces = 2; h.bits = 16; h.endianType = 1;
|
||
const auto cube = geopro::io::gpr::readRadarDataCube(dp.string(), h);
|
||
ASSERT_EQ(cube.size(), 2u * 3u * 2u);
|
||
auto at = [&](int t, int c, int s) { return cube[(size_t(t) * 3 + c) * 2 + s]; };
|
||
EXPECT_EQ(at(0, 0, 0), 0);
|
||
EXPECT_EQ(at(1, 2, 1), 121); // 100+20+1
|
||
EXPECT_EQ(at(0, 1, 0), 10);
|
||
}
|
||
|
||
TEST(NormalizedRadarData, WrongFileSizeThrows) {
|
||
fs::path dir = fs::temp_directory_path() / "radar_data_test";
|
||
fs::create_directories(dir);
|
||
const fs::path dp = dir / "bad.data";
|
||
{ std::ofstream f(dp, std::ios::binary); std::int16_t v = 0; f.write((char*)&v, 2); }
|
||
geopro::io::gpr::RadarHeader h;
|
||
h.samples = 2; h.channels = 3; h.lastTrace = 6; h.traces = 2; h.bits = 16;
|
||
EXPECT_THROW(geopro::io::gpr::readRadarDataCube(dp.string(), h), std::runtime_error);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: 跑测试确认失败**
|
||
|
||
Run: `cmake --build build/release --target geopro_tests 2>&1 | head`
|
||
Expected: `readRadarDataCube` 未声明,编译失败。
|
||
|
||
- [ ] **Step 3: 写实现**
|
||
|
||
`NormalizedRadarReader.hpp` 加声明(+`#include <cstdint>`)。`.cpp` 加:
|
||
```cpp
|
||
#include <fstream>
|
||
#include <filesystem>
|
||
// ...
|
||
std::vector<std::int16_t> readRadarDataCube(const std::string& dataPath,
|
||
const RadarHeader& h) {
|
||
if (h.bits != 16)
|
||
throw std::runtime_error("readRadarDataCube: 暂仅支持 16-bit(BITS=" +
|
||
std::to_string(h.bits) + " 待实现)");
|
||
const std::size_t n = static_cast<std::size_t>(h.lastTrace) * h.samples;
|
||
const std::uintmax_t expect = static_cast<std::uintmax_t>(n) * 2;
|
||
std::error_code ec;
|
||
const auto fsize = std::filesystem::file_size(dataPath, ec);
|
||
if (ec || fsize != expect)
|
||
throw std::runtime_error("规范化 .data 大小不符: " + dataPath);
|
||
std::vector<std::int16_t> cube(n);
|
||
std::ifstream f(dataPath, std::ios::binary);
|
||
if (!f) throw std::runtime_error("打开 .data 失败: " + dataPath);
|
||
f.read(reinterpret_cast<char*>(cube.data()), static_cast<std::streamsize>(expect));
|
||
if (!f) throw std::runtime_error("读 .data 失败: " + dataPath);
|
||
if (h.endianType == 2) // 大端 → 主机小端(x86),逐元素交换字节
|
||
for (auto& v : cube) {
|
||
const std::uint16_t u = static_cast<std::uint16_t>(v);
|
||
v = static_cast<std::int16_t>((u >> 8) | (u << 8));
|
||
}
|
||
return cube;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 跑测试确认通过**
|
||
|
||
Run: `cmake --build build/release --target geopro_tests && ctest --test-dir build/release -R NormalizedRadarData -V`
|
||
Expected: 2 个用例 PASS。
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/io/gpr/NormalizedRadarReader.hpp src/io/gpr/NormalizedRadarReader.cpp \
|
||
tests/io/gpr/test_normalized_radar_reader.cpp
|
||
git commit -m "feat(radar): 规范化 .data 立方体读取(position-major/16bit/字节序)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 4: 规范化 `.cor` 轨迹解析
|
||
|
||
本期只解析(单线渲染不依赖),为 P1 多线配准预留。
|
||
|
||
**Files:**
|
||
- Modify: `src/io/gpr/NormalizedRadarReader.hpp`(加 `TracePos` + `parseRadarCor`)
|
||
- Modify: `src/io/gpr/NormalizedRadarReader.cpp`
|
||
- Test: `tests/io/gpr/test_normalized_radar_reader.cpp`(加用例)
|
||
|
||
**Interfaces:**
|
||
- Produces:
|
||
```cpp
|
||
struct TracePos { int index = 0; double lat = 0, lon = 0, elev = 0; int solution = 0; };
|
||
// 解析 .cor:跳过 "VERSION:" 行,每行 [序号 纬度 N/S 经度 E/W 高程 M 解状态] (制表/空格分隔)。
|
||
std::vector<TracePos> parseRadarCor(const std::string& corText);
|
||
```
|
||
|
||
- [ ] **Step 1: 写失败测试**
|
||
|
||
```cpp
|
||
TEST(NormalizedRadarCor, ParsesRowsSkippingVersion) {
|
||
const std::string cor =
|
||
"VERSION:1\n"
|
||
"1\t317.179340\tN\t472.759046\tE\t49.980000\tM\t4\n"
|
||
"12\t317.201303\tN\t472.700649\tE\t51.040000\tM\t4\n";
|
||
const auto pts = geopro::io::gpr::parseRadarCor(cor);
|
||
ASSERT_EQ(pts.size(), 2u);
|
||
EXPECT_EQ(pts[0].index, 1);
|
||
EXPECT_DOUBLE_EQ(pts[0].lat, 317.179340);
|
||
EXPECT_DOUBLE_EQ(pts[0].lon, 472.759046);
|
||
EXPECT_DOUBLE_EQ(pts[1].elev, 51.040000);
|
||
EXPECT_EQ(pts[1].solution, 4);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: 跑测试确认失败**
|
||
|
||
Run: `cmake --build build/release --target geopro_tests 2>&1 | head`
|
||
Expected: `parseRadarCor` 未声明。
|
||
|
||
- [ ] **Step 3: 写实现**
|
||
|
||
`.hpp` 加 `TracePos`/`parseRadarCor`。`.cpp` 加:
|
||
```cpp
|
||
std::vector<TracePos> parseRadarCor(const std::string& corText) {
|
||
std::vector<TracePos> out;
|
||
std::istringstream in(corText);
|
||
std::string line;
|
||
while (std::getline(in, line)) {
|
||
if (line.empty() || line.rfind("VERSION", 0) == 0) continue;
|
||
std::istringstream ls(line);
|
||
TracePos p; std::string ns, ew, m;
|
||
if (ls >> p.index >> p.lat >> ns >> p.lon >> ew >> p.elev >> m >> p.solution)
|
||
out.push_back(p);
|
||
}
|
||
return out;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 跑测试确认通过**
|
||
|
||
Run: `cmake --build build/release --target geopro_tests && ctest --test-dir build/release -R NormalizedRadarCor -V`
|
||
Expected: PASS。
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/io/gpr/NormalizedRadarReader.hpp src/io/gpr/NormalizedRadarReader.cpp \
|
||
tests/io/gpr/test_normalized_radar_reader.cpp
|
||
git commit -m "feat(radar): 规范化 .cor 轨迹解析(P1 配准预留)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 5: 规范化体 bridge `buildLineVolumeFromNormalized`
|
||
|
||
组合 reader + `assembleRadarVolume`,产出 `BuiltI16`。
|
||
|
||
**Files:**
|
||
- Create: `src/io/gpr/NormalizedRadarVolumeBridge.hpp`
|
||
- Create: `src/io/gpr/NormalizedRadarVolumeBridge.cpp`
|
||
- Modify: `src/io/CMakeLists.txt`
|
||
- Test: `tests/io/gpr/test_normalized_radar_bridge.cpp`
|
||
- Modify: `tests/CMakeLists.txt`
|
||
|
||
**Interfaces:**
|
||
- Produces:
|
||
```cpp
|
||
namespace geopro::io::gpr {
|
||
// 读 {lineDir}/{prefix}.head + .data → assembleRadarVolume → BuiltI16(轴 X=道/Y=通道/Z=采样)。
|
||
// coarse 沿道下采样;targetDy 线内通道插值(读 .head CH_X_OFFSETS)。失败抛 std::runtime_error。
|
||
geopro::core::BuiltI16 buildLineVolumeFromNormalized(const std::string& lineDir,
|
||
const std::string& linePrefix,
|
||
int coarse = 4,
|
||
double targetDy = 0.025);
|
||
}
|
||
```
|
||
- Consumes: `parseRadarHead`/`readRadarDataCube`/`depthSpacingZ`(Task 2-3)、`assembleRadarVolume`(Task 1)。
|
||
|
||
- [ ] **Step 1: 写失败测试**
|
||
|
||
`tests/io/gpr/test_normalized_radar_bridge.cpp`:
|
||
```cpp
|
||
#include <gtest/gtest.h>
|
||
#include <cstdint>
|
||
#include <filesystem>
|
||
#include <fstream>
|
||
#include "core/algo/GprVolumeBuilder.hpp"
|
||
#include "io/gpr/NormalizedRadarVolumeBridge.hpp"
|
||
namespace fs = std::filesystem;
|
||
|
||
TEST(NormalizedRadarBridge, BuildsVolumeWithExpectedAxes) {
|
||
// K=4 道, M=2 通道, N=3 采样, 无通道偏移(不插值), coarse=1。
|
||
fs::path dir = fs::temp_directory_path() / "radar_bridge_test";
|
||
fs::create_directories(dir);
|
||
{ std::ofstream f(dir / "L.head");
|
||
f << "SAMPLES:3\nNUMBER_OF_CH:2\nLAST_TRACE:8\nBITS:16\nENDIAN_TYPE:1\n"
|
||
"DISTANCE_INTERVAL:0.1\nTIMEWINDOW:30\nDIELECTRIC:9\n"; }
|
||
{ std::ofstream f(dir / "L.data", std::ios::binary);
|
||
for (int t = 0; t < 4; ++t) for (int c = 0; c < 2; ++c) for (int s = 0; s < 3; ++s) {
|
||
std::int16_t v = static_cast<std::int16_t>(t * 10 + c * 100 + s);
|
||
f.write(reinterpret_cast<const char*>(&v), 2); } }
|
||
const auto b = geopro::io::gpr::buildLineVolumeFromNormalized(
|
||
(dir).string(), "L", /*coarse=*/1, /*targetDy=*/0.0); // targetDy=0 不插值
|
||
EXPECT_EQ(b.vol.nx(), 4); // 道
|
||
EXPECT_EQ(b.vol.ny(), 2); // 通道
|
||
EXPECT_EQ(b.vol.nz(), 3); // 采样
|
||
EXPECT_DOUBLE_EQ(b.spacing[0], 0.1); // dx=DISTANCE_INTERVAL
|
||
EXPECT_GT(b.spacing[2], 0.0); // dz 由 timewindow/dielectric 求得 >0
|
||
EXPECT_NEAR(b.quant.toPhys(b.vol.at(3, 1, 2)), 132.0, b.quant.scale); // t3c1s2=30+100+2
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: 跑测试确认失败**
|
||
|
||
Run: `cmake --build build/release --target geopro_tests 2>&1 | head`
|
||
Expected: 头不存在,编译失败。
|
||
|
||
- [ ] **Step 3: 写实现**
|
||
|
||
`src/io/gpr/NormalizedRadarVolumeBridge.hpp`:守卫 + 声明。`.cpp`:
|
||
```cpp
|
||
#include "io/gpr/NormalizedRadarVolumeBridge.hpp"
|
||
#include "io/gpr/NormalizedRadarReader.hpp"
|
||
#include "io/gpr/RadarVolumeAssembler.hpp"
|
||
namespace geopro::io::gpr {
|
||
geopro::core::BuiltI16 buildLineVolumeFromNormalized(const std::string& lineDir,
|
||
const std::string& linePrefix,
|
||
int coarse, double targetDy) {
|
||
const std::string head = lineDir + "/" + linePrefix + ".head";
|
||
const std::string data = lineDir + "/" + linePrefix + ".data";
|
||
std::string headText;
|
||
{ std::ifstream f(head); if (!f) throw std::runtime_error("打开 .head 失败: " + head);
|
||
std::stringstream ss; ss << f.rdbuf(); headText = ss.str(); }
|
||
const RadarHeader h = parseRadarHead(headText);
|
||
const std::vector<std::int16_t> cube = readRadarDataCube(data, h);
|
||
const int M = h.channels, N = h.samples;
|
||
RadarCubeDesc d;
|
||
d.channels = M; d.traces = h.traces; d.samples = N;
|
||
d.chXOffsets = h.chXOffsets;
|
||
d.dxBase = h.distanceInterval > 1e-9 ? h.distanceInterval : 1.0;
|
||
d.dyWhenNotInterpolated =
|
||
(h.chXOffsets.size() >= 2)
|
||
? (h.chXOffsets.back() - h.chXOffsets.front()) / (M - 1)
|
||
: 1.0;
|
||
d.dz = depthSpacingZ(h) > 1e-12 ? depthSpacingZ(h) : 1.0;
|
||
CubeSampler sample = [&cube, M, N](int c, int t, int s) {
|
||
return static_cast<double>(cube[(static_cast<std::size_t>(t) * M + c) * N + s]);
|
||
};
|
||
return assembleRadarVolume(d, sample, coarse, targetDy);
|
||
}
|
||
} // namespace geopro::io::gpr
|
||
```
|
||
(`.cpp` 顶部 `#include <fstream>`/`<sstream>`/`<stdexcept>`。)加 `src/io/CMakeLists.txt`:`gpr/NormalizedRadarVolumeBridge.cpp`;加 `tests/CMakeLists.txt`:`io/gpr/test_normalized_radar_bridge.cpp`。
|
||
|
||
- [ ] **Step 4: 跑测试确认通过**
|
||
|
||
Run: `cmake --build build/release --target geopro_tests && ctest --test-dir build/release -R NormalizedRadarBridge -V`
|
||
Expected: PASS。
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/io/gpr/NormalizedRadarVolumeBridge.hpp src/io/gpr/NormalizedRadarVolumeBridge.cpp \
|
||
src/io/CMakeLists.txt tests/io/gpr/test_normalized_radar_bridge.cpp tests/CMakeLists.txt
|
||
git commit -m "feat(radar): 规范化体 bridge buildLineVolumeFromNormalized"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 6: 数据层 `createRadarVolumeGrid`
|
||
|
||
**Files:**
|
||
- Modify: `src/data/GprVolumeRepository.hpp`(加声明)
|
||
- Modify: `src/data/GprVolumeRepository.cpp`(加实现)
|
||
- Test: `tests/data/test_gpr_volume_repository.cpp`(加用例)
|
||
|
||
**Interfaces:**
|
||
- Produces:
|
||
```cpp
|
||
// 规范化测线 → BuiltI16(buildLineVolumeFromNormalized) → 反量化 VolumeGrid。
|
||
VolumeGrid createRadarVolumeGrid(const std::string& lineDir, const std::string& linePrefix,
|
||
int coarse = 4, double targetDy = 0.025);
|
||
```
|
||
- Consumes: `buildLineVolumeFromNormalized`(Task 5)、`builtI16ToVolumeGrid`(现成)。
|
||
|
||
- [ ] **Step 1: 写失败测试**
|
||
|
||
加到 `tests/data/test_gpr_volume_repository.cpp`(仿现有 `createGprVolumeGrid` 测试,用 Task 5 同款合成 .head/.data):
|
||
```cpp
|
||
TEST(GprVolumeRepository, CreateRadarVolumeGridFromNormalized) {
|
||
fs::path dir = fs::temp_directory_path() / "radar_repo_test";
|
||
fs::create_directories(dir);
|
||
{ std::ofstream f(dir / "L.head");
|
||
f << "SAMPLES:3\nNUMBER_OF_CH:2\nLAST_TRACE:8\nBITS:16\nENDIAN_TYPE:1\n"
|
||
"DISTANCE_INTERVAL:0.1\nTIMEWINDOW:30\nDIELECTRIC:9\n"; }
|
||
{ std::ofstream f(dir / "L.data", std::ios::binary);
|
||
for (int t = 0; t < 4; ++t) for (int c = 0; c < 2; ++c) for (int s = 0; s < 3; ++s) {
|
||
std::int16_t v = static_cast<std::int16_t>(t * 10 + c * 100 + s);
|
||
f.write(reinterpret_cast<const char*>(&v), 2); } }
|
||
const auto grid = geopro::data::createRadarVolumeGrid(dir.string(), "L", 1, 0.0);
|
||
EXPECT_EQ(grid.vol.nx(), 4);
|
||
EXPECT_EQ(grid.vol.ny(), 2);
|
||
EXPECT_EQ(grid.vol.nz(), 3);
|
||
EXPECT_GT(grid.vmax, grid.vmin);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: 跑测试确认失败**
|
||
|
||
Run: `cmake --build build/release --target geopro_tests 2>&1 | head`
|
||
Expected: `createRadarVolumeGrid` 未声明。
|
||
|
||
- [ ] **Step 3: 写实现**
|
||
|
||
`GprVolumeRepository.hpp` 在 `createGprVolumeGrid` 声明后加 `createRadarVolumeGrid`(注释说明走规范化链)。`.cpp` 加:
|
||
```cpp
|
||
#include "io/gpr/NormalizedRadarVolumeBridge.hpp"
|
||
// ...
|
||
VolumeGrid createRadarVolumeGrid(const std::string& lineDir,
|
||
const std::string& linePrefix, int coarse,
|
||
double targetDy) {
|
||
const geopro::core::BuiltI16 built =
|
||
geopro::io::gpr::buildLineVolumeFromNormalized(lineDir, linePrefix, coarse, targetDy);
|
||
return builtI16ToVolumeGrid(built);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 跑测试确认通过**
|
||
|
||
Run: `cmake --build build/release --target geopro_tests && ctest --test-dir build/release -R "GprVolumeRepository" -V`
|
||
Expected: 新用例 + 现有用例全 PASS。
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/data/GprVolumeRepository.hpp src/data/GprVolumeRepository.cpp \
|
||
tests/data/test_gpr_volume_repository.cpp
|
||
git commit -m "feat(radar): 数据层 createRadarVolumeGrid(规范化→VolumeGrid)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 7: DS 优先登记 `registerRadarDataset` + `loadVolume` 懒加载后台建体
|
||
|
||
把规范化测线登记成一条 **`dd_radar_3d`** DS(不是游离 `dd_voxel` vol-N),**只存元数据、不建体**;首次勾选时 `loadVolume` 在**后台线程**建体(仿 `finalizeVolume`,避免 UI 冻结 + spinner 卡死)。运行期路由只认 `volumes_` 成员(已验:`isVolumeDataset`=`volumes_.count`、`loadVolume` 按 dsId 查、`addDatasetAsync` 按 `isVolumeDataset` 分流),故 `isVolumeDataset/addDatasetAsync` 零改动。
|
||
|
||
**Files:**
|
||
- Modify: `src/data/api/Api3dRepository.hpp`(`StoredVolume` 加字段 + 声明 `registerRadarDataset`)
|
||
- Modify: `src/data/api/Api3dRepository.cpp`(`registerRadarDataset` + `loadVolume` radar 分支 + `volumeRows()` 输出 `sv.ddCode`)
|
||
- Test: `tests/data/test_3d_repo.cpp`(加用例:登记为 `dd_radar_3d` + `loadVolume` 同步退化路径产有效体)
|
||
|
||
**Interfaces:**
|
||
- Produces:
|
||
```cpp
|
||
// 登记一条 dd_radar_3d 体 DS:只存元数据(lineDir/prefix/coarse),不建体。id="radar-"+(++counter)。
|
||
std::string registerRadarDataset(const std::string& lineDir, const std::string& linePrefix,
|
||
const std::string& name, const std::string& structParentId,
|
||
int coarse = 4);
|
||
```
|
||
- Consumes: `createRadarVolumeGrid`(Task 6)。
|
||
- `StoredVolume`(`Api3dRepository.hpp:139`) 新增字段:`std::string lineDir, linePrefix, ddCode = "dd_voxel", structParentId; int coarse = 4;`。
|
||
|
||
- [ ] **Step 1: 写失败测试**
|
||
|
||
加到 `tests/data/test_3d_repo.cpp`(用 Task 6 同款合成目录;构造 repo 照本文件现有用例的构造方式。gtest 无 `QCoreApplication` → `loadVolume` 走同步退化路径,回调即时触发):
|
||
```cpp
|
||
TEST(Api3dRepository, RegisterRadarDatasetRoutesAsDdRadar3d) {
|
||
// ...写合成 dir/L.head + L.data(同 Task 6)...
|
||
geopro::data::Api3dRepository repo(/*照本文件现有构造*/);
|
||
const std::string id = repo.registerRadarDataset(dir.string(), "L", "测线L",
|
||
/*structParentId=*/"tm-1", /*coarse=*/1);
|
||
EXPECT_FALSE(id.empty());
|
||
EXPECT_TRUE(repo.isVolumeDataset(id)); // 运行期按 volumes_ 成员判体 → 真(即便未建体)
|
||
const auto rows = repo.volumeRows();
|
||
ASSERT_FALSE(rows.empty());
|
||
EXPECT_EQ(rows.back().ddCode, "dd_radar_3d"); // 不是 dd_voxel
|
||
EXPECT_EQ(rows.back().structParentId, "tm-1");
|
||
}
|
||
|
||
TEST(Api3dRepository, LoadVolumeBuildsRadarLazily) {
|
||
// ...写合成 dir/L.head + L.data(同上)...
|
||
geopro::data::Api3dRepository repo(/*照本文件现有构造*/);
|
||
const std::string id = repo.registerRadarDataset(dir.string(), "L", "测线L", "", 1);
|
||
bool got = false;
|
||
repo.loadVolume(id,
|
||
[&](geopro::data::VolumeGrid g, geopro::core::ColorScale) {
|
||
got = true;
|
||
EXPECT_GT(g.vol.nx(), 0); EXPECT_GT(g.vol.ny(), 0); EXPECT_GT(g.vol.nz(), 0);
|
||
},
|
||
[&](const std::string& e) { FAIL() << e; });
|
||
EXPECT_TRUE(got); // 无 QCoreApplication → 同步交付
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: 跑测试确认失败**
|
||
|
||
Run: `cmake --build build/release --target geopro_tests 2>&1 | head`
|
||
Expected: `registerRadarDataset` 未声明。
|
||
|
||
- [ ] **Step 3: 写实现**
|
||
|
||
`Api3dRepository.hpp`:`StoredVolume`(:139 附近) 加 `std::string lineDir, linePrefix, ddCode = "dd_voxel", structParentId; int coarse = 4;`;在 `createGprVolume` 声明后加 `registerRadarDataset` 声明。
|
||
`.cpp` 加 `registerRadarDataset`(只存元数据,**不建体**):
|
||
```cpp
|
||
std::string Api3dRepository::registerRadarDataset(const std::string& lineDir,
|
||
const std::string& linePrefix,
|
||
const std::string& name,
|
||
const std::string& structParentId,
|
||
int coarse) {
|
||
const std::string id = "radar-" + std::to_string(++volumeCounter_);
|
||
StoredVolume sv;
|
||
sv.name = name;
|
||
sv.ddCode = "dd_radar_3d";
|
||
sv.lineDir = lineDir;
|
||
sv.linePrefix = linePrefix;
|
||
sv.coarse = coarse;
|
||
sv.structParentId = structParentId;
|
||
sv.createTime = QDateTime::currentDateTime()
|
||
.toString(QStringLiteral("yyyy-MM-dd HH:mm")).toStdString();
|
||
volumes_[id] = std::move(sv); // 不预填 cachedGrid → 懒建
|
||
return id;
|
||
}
|
||
```
|
||
`loadVolume`(`Api3dRepository.cpp:347` 的 `if (sv.cachedGrid)` 块之后、IDW 路径 `:351` 之前)插入 radar 分支(仿 `finalizeVolume:268-335` 后台线程范式,`#include "data/GprVolumeRepository.hpp"` 已在):
|
||
```cpp
|
||
if (!sv.linePrefix.empty()) { // radar DS:后台建体,避免阻塞 UI(与 finalizeVolume 同范式)
|
||
const std::string lineDir = sv.lineDir, linePrefix = sv.linePrefix;
|
||
const int coarse = sv.coarse;
|
||
auto deliver = [this, dsId, onOk, onErr](std::shared_ptr<VolumeGrid> g, std::string err) {
|
||
if (!g) { onErr("Api3dRepository::loadVolume(radar): " + err); return; }
|
||
core::ColorScale scale;
|
||
const double mid = 0.5 * (g->vmin + g->vmax);
|
||
scale.addStop(g->vmin, core::Rgba{20, 24, 40, 255});
|
||
scale.addStop(mid, core::Rgba{140, 140, 150, 255});
|
||
scale.addStop(g->vmax, core::Rgba{235, 232, 220, 255});
|
||
if (auto it2 = volumes_.find(dsId); it2 != volumes_.end()) {
|
||
it2->second.cachedGrid = *g; // 缓存 → 下次命中直渲
|
||
it2->second.cachedScale = scale;
|
||
}
|
||
onOk(*g, scale);
|
||
};
|
||
auto compute = [lineDir, linePrefix, coarse]() {
|
||
std::shared_ptr<VolumeGrid> g; std::string err;
|
||
try { g = std::make_shared<VolumeGrid>(
|
||
geopro::data::createRadarVolumeGrid(lineDir, linePrefix, coarse)); }
|
||
catch (const std::exception& e) { err = e.what(); }
|
||
return std::make_tuple(g, err);
|
||
};
|
||
if (!QCoreApplication::instance()) { // headless/单测 → 同步
|
||
auto r = compute(); deliver(std::get<0>(r), std::get<1>(r)); return;
|
||
}
|
||
std::thread([compute, deliver]() mutable {
|
||
auto r = compute();
|
||
auto g = std::get<0>(r); auto err = std::get<1>(r);
|
||
QMetaObject::invokeMethod(qApp,
|
||
[deliver, g, err]() mutable { deliver(std::move(g), std::move(err)); },
|
||
Qt::QueuedConnection);
|
||
}).detach();
|
||
return;
|
||
}
|
||
```
|
||
改 `volumeRows()`(`Api3dRepository.cpp:170` 附近):`r.ddCode = "dd_voxel";` → `r.ddCode = sv.ddCode;`,且 `r.structParentId = sv.request ? sv.request->structParentId : sv.structParentId;`(radar 无 request,用 sv.structParentId)。
|
||
|
||
- [ ] **Step 4: 跑测试确认通过**
|
||
|
||
Run: `cmake --build build/release --target geopro_tests && ctest --test-dir build/release -R "Api3dRepository" -V`
|
||
Expected: 2 个新用例 + 现有用例全 PASS(现有体 `sv.ddCode` 默认 `"dd_voxel"`、`linePrefix` 空不走 radar 分支,行为不变)。
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/data/api/Api3dRepository.hpp src/data/api/Api3dRepository.cpp tests/data/test_3d_repo.cpp
|
||
git commit -m "feat(radar): registerRadarDataset 登记 dd_radar_3d DS + loadVolume 懒加载后台建体"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 8: 本地导入入口 + `dd_radar_3d` 三处 ddCode gate 放开
|
||
|
||
导入登记 DS(瞬时,建体走 Task 7 的懒加载后台路径),并放开三处硬编码 `dd_voxel` 的 ddCode 判断让 radar 体可切片/调色阶/看详情。**评审 CRITICAL:列表右键「生成切片」是 UI 唯一切片入口,不放开 `CategorySection.cpp:397` 则 radar 体无法切片、P0 验收②不可达。**
|
||
|
||
**Files:**
|
||
- Modify: `src/app/main.cpp`(导入菜单动作 + `detailRequested` gate)
|
||
- Modify: `src/app/panels/columns/CategorySection.cpp:397`(右键「生成切片」+「色阶」同一 if 块的 gate)
|
||
|
||
**Interfaces:**
|
||
- Consumes: `scene3dRepo->registerRadarDataset`(Task 7)、`refreshAnalysis`、`analysisTab`、`window`。
|
||
|
||
- [ ] **Step 1: 加导入菜单动作**
|
||
|
||
在 `main.cpp` 中 `refreshAnalysis` 已定义、`analysisTab` 已存在处(紧接 `generateVolumeRequested` 连接块之后,约 :982 后)插入。**登记瞬时、不建体**——勾选后由 Task 7 的 `loadVolume` 后台建体,列表项 spinner 显进度,失败由 controller `loadFailed`→toast 兜底(`main.cpp:995`):
|
||
```cpp
|
||
// 本地导入三维雷达测线目录(后端未就绪的过渡入口):选目录+前缀 → registerRadarDataset(登记dd_radar_3d DS) → 勾选→后台建体渲染。
|
||
{
|
||
QMenu* radarMenu = window.menuBar()->addMenu(QStringLiteral("三维雷达"));
|
||
QAction* importAct = radarMenu->addAction(QStringLiteral("导入测线目录(本地)…"));
|
||
QObject::connect(importAct, &QAction::triggered, &window,
|
||
[&window, scene3dRepo, refreshAnalysis, analysisTab]() {
|
||
const QString dir = QFileDialog::getExistingDirectory(
|
||
&window, QStringLiteral("选择规范化三维雷达测线目录(含 *.head/*.data)"));
|
||
if (dir.isEmpty()) return;
|
||
bool ok = false;
|
||
const QString prefix = QInputDialog::getText(
|
||
&window, QStringLiteral("测线前缀"),
|
||
QStringLiteral("输入测线前缀(如 南同大道_000):"), QLineEdit::Normal, QString(), &ok);
|
||
if (!ok || prefix.isEmpty()) return;
|
||
// structParentId 暂空(P0 挂三维体段根;P1 接 TM 归属)。
|
||
const std::string newId = scene3dRepo->registerRadarDataset(
|
||
dir.toLocal8Bit().toStdString(), prefix.toLocal8Bit().toStdString(),
|
||
prefix.toStdString(), /*structParentId=*/std::string(), /*coarse=*/4);
|
||
{ const QSignalBlocker block(analysisTab); refreshAnalysis(); } // DS 进三维体段(不触发渲染)
|
||
const QString qid = QString::fromStdString(newId);
|
||
analysisTab->setItemChecked(qid, true); // 勾选 → addDatasetAsync → loadVolume 后台建体渲染
|
||
analysisTab->setItemBusy(qid, true); // spinner; 渲染完成由 datasetRendered 撤(main.cpp:987)
|
||
analysisTab->scrollItemToTop(qid);
|
||
});
|
||
}
|
||
```
|
||
确认 `main.cpp` 顶部已 include `<QMenu>`/`<QMenuBar>`/`<QFileDialog>`/`<QInputDialog>`(缺则补)。
|
||
|
||
- [ ] **Step 2: 放开列表右键「生成切片/色阶」gate(评审 CRITICAL)**
|
||
|
||
`src/app/panels/columns/CategorySection.cpp`:右键菜单的「生成切片」(:398-402) 与「色阶」(:403) **同在 `:397` 的 `if (ddCode == "dd_voxel")` 块内** → **只改 :397 这一处**判定即同时放开二者(无独立 :403 gate):
|
||
- `:397` `if (ddCode == QStringLiteral("dd_voxel"))` → `if (ddCode == QStringLiteral("dd_voxel") || ddCode == QStringLiteral("dd_radar_3d"))`
|
||
|
||
(这是 UI 唯一切片创建入口 `sliceRequested`→`main.cpp:1061`→`interactionMgr->addSlice`;不改则 radar 体右键无切片/色阶。`addSlice` 依赖的 volume image 由勾选→后台建体→`addVolume` 附着,与"先勾选渲染再切片"次序一致。)
|
||
|
||
- [ ] **Step 3: dd_radar_3d 双击详情路由**
|
||
|
||
`main.cpp` 的 `analysisTab` `detailRequested` 处理器(约 :1003-1013)目前只认 `dd_slice`/`dd_voxel`。把 `dd_voxel` 分支 `if (ddCode == QStringLiteral("dd_voxel"))` 改为 `if (ddCode == QStringLiteral("dd_voxel") || ddCode == QStringLiteral("dd_radar_3d"))`(`volumeInfo(dsId)` 对 radar DS 有效——它在 `volumes_` 里、loadVolume 后 `cachedGrid` 就绪 loaded=true)。
|
||
> 注(MEDIUM 显示瑕疵,本期可不处理):体属性对话框对 radar 会显示 `StoredVolume.params` 的默认 IDW 参数(cellXY/power…)与 pointCount=0,因 radar 不走 IDW。如需净化,给 `VolumePropertiesDialog` 加"radar 体隐藏插值参数段"分支,登记到 backlog。
|
||
|
||
- [ ] **Step 4: 构建 app**
|
||
|
||
Run: `build.bat app`(先关掉在跑的 app,避免 LNK1104)
|
||
Expected: 链接通过,生成 exe。
|
||
|
||
- [ ] **Step 5: 人工联调验收**
|
||
|
||
启动 app → 登录 → 菜单「三维雷达 → 导入测线目录(本地)」→ 选 `samples/radar/malamira_南同大道`、前缀 `南同大道_000` → 三维体段出现 `南同大道_000`(**ddCode=dd_radar_3d**)行、自动勾选、列表项转 spinner、后台建体完成后渲染出体(spinner 撤)。验:① 体可见;② **右键该行有「生成切片」**→切 updown/leftright/frontback 三向;③ 画点/线/面异常并保存、挂在体下;④ 双击该行弹体属性。日志看 `geopro_*.log` 无异常。
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git add src/app/main.cpp src/app/panels/columns/CategorySection.cpp
|
||
git commit -m "feat(radar): 导入入口 + dd_radar_3d 切片/色阶/详情 gate 放开"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 9: 双数据集互证(明星路 in-app 首次稠密渲染)
|
||
|
||
按评审 HIGH-2:明星路 in-app 渲染从未跑过,需接其导入入口(复用现成 `createGprVolume`,仍走 `.iprb` 路径、体 ddCode 仍为 `dd_voxel` vol-N——Impulse 归 `dd_radar_3d` 待其规范化插件就绪),与 Mala 同入口形态,证下游对 14ch/16ch 几何无关。**只验"管路通+几何无关",不验成像一致**(明星路处理后 vs Mala 原始)。
|
||
|
||
**Files:**
|
||
- Modify: `src/app/main.cpp`(Task 8 的 `radarMenu` 加第二个动作)
|
||
|
||
- [ ] **Step 1: 加 Impulse 导入动作**
|
||
|
||
在 Task 8 的 `radarMenu` 内追加:
|
||
```cpp
|
||
QAction* importImpulseAct = radarMenu->addAction(QStringLiteral("导入Impulse测线目录(.iprb)…"));
|
||
QObject::connect(importImpulseAct, &QAction::triggered, &window,
|
||
[&window, scene3dRepo, refreshAnalysis, analysisTab, vtkLoading]() {
|
||
const QString dir = QFileDialog::getExistingDirectory(
|
||
&window, QStringLiteral("选择 Impulse 测线目录(含 *.iprb/*.ord)"));
|
||
if (dir.isEmpty()) return;
|
||
bool ok = false;
|
||
const QString prefix = QInputDialog::getText(
|
||
&window, QStringLiteral("测线前缀"),
|
||
QStringLiteral("输入测线前缀(如 明星路_010):"), QLineEdit::Normal, QString(), &ok);
|
||
if (!ok || prefix.isEmpty()) return;
|
||
vtkLoading->showOver(QStringLiteral("正在建Impulse体…"));
|
||
QTimer::singleShot(0, &window, [=]() {
|
||
std::string newId;
|
||
try {
|
||
newId = scene3dRepo->createGprVolume(dir.toLocal8Bit().toStdString(),
|
||
prefix.toLocal8Bit().toStdString(),
|
||
prefix.toStdString(), /*coarse=*/8);
|
||
} catch (const std::exception& e) {
|
||
vtkLoading->hide();
|
||
geopro::app::showToast(&window,
|
||
QStringLiteral("建体失败:%1").arg(QString::fromLocal8Bit(e.what())));
|
||
return;
|
||
}
|
||
{ const QSignalBlocker block(analysisTab); refreshAnalysis(); }
|
||
vtkLoading->hide();
|
||
const QString qid = QString::fromStdString(newId);
|
||
// createGprVolume 预填 cachedGrid → setItemChecked 内 loadVolume 同步渲染、datasetRendered 自动撤 busy;
|
||
// 故此处【不要】再 setItemBusy(true)(否则 spinner 永久转圈 —— 评审 MEDIUM)。
|
||
analysisTab->setItemChecked(qid, true);
|
||
analysisTab->scrollItemToTop(qid);
|
||
});
|
||
});
|
||
```
|
||
> 注:明星路体经 `createGprVolume` 仍是 eager 同步建体(~74MB×14ch,建体期 UI 短冻,已用 `vtkLoading` 蒙版遮挡)。这是现有 `.iprb` 路径、本期不改;Mala 走 Task 7 懒加载后台路径不受影响。
|
||
|
||
- [ ] **Step 2: 构建 + 双数据集联调**
|
||
|
||
Run: `build.bat app`(先关 app)
|
||
验收:分别导入 `D:/Downloads/明星路`(前缀 `明星路_001`) 与 `samples/radar/malamira_南同大道`(前缀 `南同大道_000`),两体都能渲染 + 切片 + 异常。以 `gpr_poc` CLI 的明星路成像为离线对照基线,确认 in-app 明星路体几何合理(不要求与 Mala 成像可比)。
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add src/app/main.cpp
|
||
git commit -m "feat(radar): Impulse 测线本地导入入口(双数据集互证下游)"
|
||
```
|
||
|
||
---
|
||
|
||
## 完成判据
|
||
- `geopro_tests` 全绿(含新 reader/assembler/bridge/repo 用例 + 现有 Gpr3dv/Api3d 无回归)。
|
||
- app 内导入 Mala 规范化测线 → 渲染 + 三向切片 + 三类异常 OK。
|
||
- app 内导入明星路 Impulse 测线 → 渲染 OK(双数据集互证)。
|
||
- 未触碰 controller/view 渲染内核、切片/异常工具、三级树(下游零改动达成)。
|