diff --git a/.gitignore b/.gitignore index 209932b..6bf0190 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,6 @@ docs/剖面网格数据的色阶数据2等文件.tar /installer/staging/ /installer/dist/ /installer/redist/ + +# ---- Radar sample data: keep .head/.cor (small text), ignore big .data binaries ---- +samples/**/*.data diff --git a/docs/superpowers/plans/2026-06-29-3d-radar-volume-ingest.md b/docs/superpowers/plans/2026-06-29-3d-radar-volume-ingest.md new file mode 100644 index 0000000..250e1e7 --- /dev/null +++ b/docs/superpowers/plans/2026-06-29-3d-radar-volume-ingest.md @@ -0,0 +1,1052 @@ +# 三维雷达体接入(规范化格式)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`(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 chXOffsets; // 每通道横向偏移(米); size==channels 时启用通道插值 + double dxBase = 1.0; // 道距(米) → dx = dxBase*stride + double dyWhenNotInterpolated = 1.0;// 未插值时 Y 间距(米) + double dz = 1.0; // 深度采样间距(米) + }; + using CubeSampler = std::function; + // 扫值域→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 +#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 +#include +#include "core/algo/GprVolumeBuilder.hpp" +namespace geopro::io::gpr { +struct RadarCubeDesc { + int channels = 0; int traces = 0; int samples = 0; + std::vector chXOffsets; + double dxBase = 1.0; double dyWhenNotInterpolated = 1.0; double dz = 1.0; +}; +using CubeSampler = std::function; +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 +#include +#include +#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 rows; + bool interpolated = false; + if (static_cast(d.chXOffsets.size()) == d.channels && targetDy > 0.0) { + rows = planChannelInterpolation(d.chXOffsets, targetDy); + interpolated = (static_cast(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(rows.size()); + + // 扫值域 → Quant(中点 offset, 64000 裕度)。 + double vmin = std::numeric_limits::infinity(); + double vmax = -std::numeric_limits::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 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 +#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 `/``,`#ifndef` 守卫。 +`src/io/gpr/NormalizedRadarReader.cpp`(解析 KEY:VALUE): +```cpp +#include "io/gpr/NormalizedRadarReader.hpp" +#include +#include +#include +#include +namespace geopro::io::gpr { +namespace { +std::map parseKv(const std::string& text) { + std::map 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& 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& 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(h.lastTrace / h.channels); + h.bits = static_cast(optD(kv, "BITS", 16)); + h.endianType = static_cast(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 readRadarDataCube(const std::string& dataPath, + const RadarHeader& h); + ``` +- Consumes: `RadarHeader`(Task 2)。 + +- [ ] **Step 1: 写失败测试** + +加到 `test_normalized_radar_reader.cpp`: +```cpp +#include +#include +#include +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(100 * t + 10 * c + s); + f.write(reinterpret_cast(&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 `)。`.cpp` 加: +```cpp +#include +#include +// ... +std::vector 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(h.lastTrace) * h.samples; + const std::uintmax_t expect = static_cast(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 cube(n); + std::ifstream f(dataPath, std::ios::binary); + if (!f) throw std::runtime_error("打开 .data 失败: " + dataPath); + f.read(reinterpret_cast(cube.data()), static_cast(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(v); + v = static_cast((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 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 parseRadarCor(const std::string& corText) { + std::vector 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 +#include +#include +#include +#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(t * 10 + c * 100 + s); + f.write(reinterpret_cast(&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 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(cube[(static_cast(t) * M + c) * N + s]); + }; + return assembleRadarVolume(d, sample, coarse, targetDy); +} +} // namespace geopro::io::gpr +``` +(`.cpp` 顶部 `#include `/``/``。)加 `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(t * 10 + c * 100 + s); + f.write(reinterpret_cast(&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 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 g; std::string err; + try { g = std::make_shared( + 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 ``/``/``/``(缺则补)。 + +- [ ] **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 渲染内核、切片/异常工具、三级树(下游零改动达成)。 diff --git a/docs/superpowers/specs/2026-06-29-3d-radar-volume-ingest-design.md b/docs/superpowers/specs/2026-06-29-3d-radar-volume-ingest-design.md new file mode 100644 index 0000000..144ca0d --- /dev/null +++ b/docs/superpowers/specs/2026-06-29-3d-radar-volume-ingest-design.md @@ -0,0 +1,198 @@ +# 三维雷达 DS 接入设计:规范化格式 → 体渲染 + 切片 + 异常 — 2026-06-29 + +> 负责人范围:**仅三维雷达的体渲染 + 切片 + 异常标注**(不含 2D 波列图详情页、不含二维雷达)。 +> 本 spec 基于精读现有架构(HEAD `main`)+ Explore 代理交叉验证 + 用真实 Mala 数据实测确诊。 +> 所有"数据事实"均已用 `D:/Downloads/MALA南同大道_rSlicer` 6 条测线核对,**无未验证假设**。 + +--- + +## 1. 背景与锁定决策 + +客户新增雷达数据类型,模型层级沿用 `项目 → GS → TM(TmObject) → DS列表`。三维雷达 TM 携带 +`头信息(.head)` + DS 列表 `= 雷达RTK轨迹(.cor,备份+快照) + 每频率 雷达3D-频率N(打标.index + 通道1..N data.data)`。 +数据体 = **K 切片(沿运动) × M 通道(sweep) × N 采样(深度)**(与 IDS/OpenGPR 手册一致)。 + +四项锁定决策(用户 2026-06-29 确认): + +| 维度 | 决策 | +|---|---| +| 职责范围 | 仅三维体 + 切片 + 异常;不做 2D 波列图详情、不做二维雷达 | +| 数据来源 | 后端未就绪 → 先读**本地已转换好的目录**(后端就绪后切按 DS 下载) | +| 输入格式 | 客户端**规范化 `.head/.data/.cor/.index`**(新写 reader) | +| 渲染规模 | 先**单线 / 下采样稠密体**打通 | + +--- + +## 2. 已验证的数据事实(★C++ reader 必须遵守,零假设) + +实测工具:`tools/radar_convert/malamira.py probe`(B-scan/C-scan 主序核对)。 + +1. **数据体主序 = position-major**:磁盘扫描顺序 = `(道0:通道0..M-1)(道1:通道0..M-1)…`,每 sweep 内 N 采样连续。 + 即 `flat.reshape(K, M, N)[道][通道][采样]`。probe 确诊:position-major 的 B-scan 出现连续直达波 + + 地层 + 双曲线绕射;channel-major 为竖条乱码(已排除)。 +2. **直接对应 geopro 体轴** `X=道(nx=K)` / `Y=通道(ny=M)` / `Z=采样(nz=N)`,**无需轴置换**——与现有 + `Gpr3dvVolumeBridge` 轴映射完全一致。⚠️ 但**磁盘主序(采样最快→通道→道)与体内存布局(道最快,`((k·ny+j)·nx+i)`, + `ScalarVolumeI16.hpp:58`)相反**,新 reader 必须**逐体素散射重排填值**(照 bridge 的 `vol.at(to,j,s)=`), + **严禁 memcpy/整块拷贝**。"无需转置"指轴语义,不是字节布局。 +3. **维度推导**:`M=NUMBER_OF_CH`、`N=SAMPLES`、`K=LAST_TRACE/NUMBER_OF_CH`(`.head` 的 `LAST_TRACE` + 是**总扫描数**=K×M,不是道数)。 +4. **数据类型**:int16 小端(`.rd3`/`bits=16`) / int32 小端(`.rd7`/`bits=32`)。`-32768` 是直达波饱和真实值,**非空值哨兵**。 +5. **`BITS` 公式 bug**:客户文档 §3.3 `BITS = 文件大小/LAST_TRACE/NUMBER_OF_CH×8` **漏了 SAMPLES 维、量纲错**。 + 正确:`bytes = 文件大小/(LAST_TRACE×SAMPLES)`,并与扩展名交叉校验。**须同步后端**(见 `tools/radar_convert/README.md`)。 +6. **ddCode(数据字典 DD0623 权威)**:三维雷达体 DS = **`dd_radar_3d`**(本次新增,形态=三维插值模型); + 轨迹 DS = **`dd_trajectory_data`**(保留,复用现成 `TrajectoryStrategy`/`TrajectoryMapView`,零改动); + `dd_voxel` **仅物探反演体(电阻率/速度)、不含雷达**;`dd_slice` 切片共用;`dd_radar_rtk_trajectory` 已删除。 + **雷达体不复用 `dd_voxel`。** + +--- + +## 3. 架构论点:DS 优先 + 只换最内层 reader + +### 3a. 数据层:只换最内层 reader(下游建体链复用) + +现有真实雷达体链路是分层的,**只有最内层 reader 绑定厂商格式**: + +``` +data::createRadarVolumeGrid [data/GprVolumeRepository 新] + ├ io::gpr::buildLineVolumeFromNormalized ← 新写:仅此层认规范化 .head/.data + └ data::builtI16ToVolumeGrid [data/GprVolumeRepository.cpp:11] int16→float 稠密体 +``` +(范本:现有 `createGprVolume`[`Api3dRepository.cpp:128`]→`createGprVolumeGrid`→`buildLineVolumeFromGpr3dv`(Impulse .iprb)。 +我们换最内层 reader,复用 `builtI16ToVolumeGrid` 及其下游。)**产 `core::BuiltI16`(轴 X=道/Y=通道/Z=采样)即可**, +这正是 `GprVolumeRepository.cpp:14` 注释写明的"数据层方案 A"。 + +### 3b. DS 优先:运行期路由按 `volumes_` 成员,不看 ddCode(关键发现,已验真代码) +桌面端体渲染的运行期分流**只认 `Api3dRepository::volumes_` map 是否含此 dsId**,与 ddCode/"vol-" 前缀无关: +- `isVolumeDataset(dsId)` = `volumes_.count(dsId)`(`Api3dRepository.cpp:105`); +- `VtkSceneController::addDatasetAsync`(:182) 按 `isVolumeDataset` 分流体素/帘面; +- `loadVolume(dsId)`(:341) 唯一查找键 = `volumes_` 的 dsId;命中 `cachedGrid` 直渲(:347)。 + +**所以 DS 优先落地 = 把一条"真实 radar DS id"为 key 的 `StoredVolume` 写进 `volumes_`**(携 `ddCode=dd_radar_3d` + `lineDir/prefix`), +则 `isVolumeDataset/addDatasetAsync/loadVolume` **零改动**自动按体渲染。三维分析栏 voxel 段内容来自 `volumeRows()`→ +`section("voxel")->setDatasets`(`main.cpp:602`,**绕过 `splitByCategory`**),故 `dimensionOf`/`CategoryConfig` **也无需改**—— +唯一要改 `volumeRows()`(:170) 把硬编码 `dd_voxel` 改成输出该 DS 的真实 ddCode。 + +### 完全复用、不碰的部分 +`render::buildVoxel`(GPU 体绘制)、勾选→`addDatasetAsync`→`loadVolume`→`addVolume` 路由、`isVolumeDataset`、 +切片(`SliceTool` updown 深度 C-scan / leftright·frontback B-scan + `InteractionManager`)、 +异常(`AnomalyDrawTool` 点/线/面 → `dd_anomaly` 挂体)、跨视图色阶联动、`builtI16ToVolumeGrid` 适配器、 +轨迹 `dd_trajectory_data` 详情视图。 + +--- + +## 4. 插件化转换层(已落地 Mala) + +转换 = "按雷达型号的插件",本地 Python 工具 = 未来服务端下发插件的原型,契约不变: + +``` +plugin_id : RADAR_TYPE_MALAMIRA # 对齐文档 §2.1 型号列表 +supports(fileset) -> bool +convert(lineDir, prefix, outDir) -> {head, data, cor} # 厂商原始 → 规范化 +``` + +- **`RADAR_TYPE_MALAMIRA`(已完成)**:`tools/radar_convert/malamira.py`。`.rad→.head`(§3.3)、 + `.rd3→.data`(§3.5 原样拷贝)、`.pos→.cor`(§2.2.2)。`info/convert/probe` 三命令。6 条线全部转换 + 校验通过。 + 与文档偏差 5 处见 `tools/radar_convert/README.md`(含 BITS 公式修正)。 +- **`RADAR_TYPE_IMPLUSE`(规划,暂不做)**:见 §7。 + +客户端职责(未来):设备对接/原始导入 → 按型号拉插件 → `convert` → 喂规范化 reader。 + +**契约两点待补(登记)**:(a) `convert` 返回 `{head,data,cor}` **未含 `.index`(打标)**,有打标的型号需扩约定; +(b) 规范化 `.data` 无扩展名,C++ reader 解析字节宽(2/4) **单点依赖 `.head` 的 `BITS` 字段** → 保证转换器 BITS 永远正确是隐性前提(`malamira.py` 的 `filesize==LAST_TRACE×SAMPLES×bytes` 断言已守住,见 §2.5)。 + +--- + +## 5. 新增 / 改动清单(C++ 侧,待实现) + +| # | 文件 | 动作 | +|---|---|---| +| ① | `src/io/gpr/NormalizedRadarReader.{hpp,cpp}` **新** | 纯函数:`parseHead(.head)→RadarHeader`;`readDataCube(.data,header)→cube`(按 `BITS`+`ENDIAN_TYPE` 解二进制,`reshape(K,M,N)` 主序已定);`parseCor(.cor)→vector`(先解析,世界配准后置) | +| ② | `src/io/gpr/NormalizedRadarVolumeBridge.{hpp,cpp}` **新** | `buildLineVolumeFromNormalized(lineDir,prefix,metrics,coarse,targetDy)→core::BuiltI16`。⚠️ **DRY**:`Gpr3dvVolumeBridge.cpp:133-197` 的值域扫描/`Quant`构造/逐体素填值/spacing 当前内联且耦合 Impulse 模型——动工时**先把这段抽成共享 helper**(签名接受抽象立方体访问器 `(c,t,s)→short` + 通道偏移/几何),两个 bridge 同调;`planChannelInterpolation`/`depthOfSample` 已是共享纯函数。**不要复制填体循环**(否则两份独立漂移) | +| ③ | `src/data/GprVolumeRepository.{hpp,cpp}` 改 | 加 `createRadarVolumeGrid(lineDir,prefix,coarse,targetDy)`(调②,复用 `builtI16ToVolumeGrid`)。**不动**现有 `createGprVolumeGrid` | +| ④ | `src/data/api/Api3dRepository.{cpp,hpp}` 改 | **DS 优先 + 懒加载后台建体**(3 处):(a) `StoredVolume`(:139) 加 `lineDir/linePrefix/coarse/ddCode/structParentId` 字段(`ddCode` 默认 `"dd_voxel"`,存量行为不变);(b) 新 `registerRadarDataset(lineDir,prefix,name,structParentId,coarse)`——**只存元数据、不建体**,以 `ddCode="dd_radar_3d"` 写进 `volumes_`,id=`"radar-"+(++counter)`,瞬时返回;(c) `loadVolume`(:347 后) 加 radar 分支:未命中 cachedGrid 且有 `linePrefix` → **后台线程** `createRadarVolumeGrid`(仿 `finalizeVolume:268-335` 的 `std::thread`+`QMetaObject::invokeMethod(qApp,...)` 回主线程;无 `QCoreApplication` 时退化同步,可单测)→ 建体 + 灰度色阶 + 缓存 + async `onOk`;(d) `volumeRows()`(:170) 输出 `sv.ddCode` + `sv.structParentId`。运行期 `isVolumeDataset/addDatasetAsync` 按 `volumes_` 成员 → **零改动**。**懒加载+后台建体同时消除 UI 冻结(评审 HIGH)与 spinner 永久转圈(评审 MEDIUM)**。⚠️ `dimensionOf`/`CategoryConfig`/`splitByCategory` **不需改**(voxel 段走 `volumeRows` 注入、绕过分类,见 §3b) | +| ⑤ | `src/app/main.cpp` + `src/app/panels/columns/CategorySection.cpp` 改 | (1) 导入入口「三维雷达→导入测线目录」:选目录+前缀 → `registerRadarDataset` → `refreshAnalysis` → `setItemChecked(true)`+`setItemBusy(true)`+`scrollItemToTop`(同 `generateVolume:978-980`;渲染异步、spinner 由 `datasetRendered` 撤)。(2) **放开两处 ddCode gate**给 `dd_radar_3d`:`main.cpp:1011` `detailRequested`→体属性;**`CategorySection.cpp:397`** 列表右键 `if (ddCode=="dd_voxel")` 块(「生成切片」+「色阶」**同在此一个块内**,**只改 :397 这一处**即同时放开二者,无独立 :403 gate)——**不改则 radar 体无切片入口、P0 验收②不可达 — 评审 CRITICAL**| +| ⑥ | 测试 | `tests/io/gpr/test_normalized_radar_reader.cpp`(喂 `tests/data/radar/` 截断样例,断言维度/字节序/通道插值/主序) | + +### 几何映射(关键算法,纯函数易测) +- **X(沿线 spacing)** = `DISTANCE_INTERVAL`(距离模式,本期唯一支持)。 +- **Y(通道横向)** = 复用 `GprGeometry::planChannelInterpolation(chXOffsets, targetDy)`(`GprGeometry.hpp:27`)—— + 通道偏移取自 `.head` 的 `CH_X_OFFSETS`(不读 `.ord`),线内插值、**绝不跨线**(默认 2.5cm)。 +- **Z(深度 spacing)** = `TIMEWINDOW/(SAMPLES-1) × 波速/2`;波速由 `DIELECTRIC` 求,`.rad` 无该字段时用默认波速。 +- **量化** → `core::BuiltI16`(复用 `core/algo/GprVolumeBuilder.hpp`)。 +- **`.cor`/`.index`** 本期只解析不深用:单线放原点沿 +X,切片/异常即可工作。逐道 GPS 世界配准 + 打标 → 多线阶段。 + +--- + +## 6. 分期 + +- **P0(本期)**:①–⑥,单线规范化体打通。**验收**:导入 `samples/radar/malamira_南同大道`(如 000 线)→ + 三维体段出现一条 **`dd_radar_3d`** DS → 勾选 → `loadVolume` 懒建体渲染 → 切 updown/leftright/frontback → 画点/线/面异常并挂体下。 +- **P1**:`.cor` 逐道 GPS 世界配准 + 高程、`.index` 打标、多线合一场景、轨迹 DS(`dd_trajectory_data`) 一并登记、DS 进左侧数据集树(挂 TM)。 +- **P2**:大体量 → 把 `gpr_poc` 的 `ChunkedVolumeStore`+`*VolumeSource` 核外/LOD 接进 app(碰 controller/view,单独立项)。 + +--- + +## 7. 双数据集测试策略 + Impulse 转换器决策 + +已有完整 POC 基于**另一套雷达数据 明星路(Impulse)**,交接见 `.superpowers/sdd/HANDOFF-2026-06-27-gpr-lod-slice.md`。 +明星路 数据在本地(`D:/Downloads/明星路`,14G,20 测线)。 + +**关键差异**:明星路 是 `RADAR_TYPE_IMPLUSE`——每线 **14 通道为 14 个独立 `.iprb` 二进制**(`_A01.._A14`,各 ~74MB)+ +逐通道 `.iprh` + `.ord` + `.gps/.time/.cor/.mrk`。Mala 则把 16 通道打进**一个** position-major `.rd3`。 +现有 C++ P1 链(`buildLineVolumeFromGpr3dv`)**能直读 `.iprb`**。 + +> ⚠️ **校正(评审 HIGH-2)**:明星路真正"经验证"的渲染是 **`gpr_poc` CLI 的核外 LOD 多线**路径,**不是** app 内 +> `createGprVolume→builtI16ToVolumeGrid→loadVolume→addVolume` 的**稠密单体**路径——后者的 **app 导入 UI 触发从未接** +> (HANDOFF-2026-06-27 §6 第 3 条:"数据层 `createGprVolume` 已就绪,但 UI 触发未接")。故 app 内稠密渲染对**两套数据都是首次**。 + +**决策:本期不建 Impulse→规范化 转换器。** 理由: +1. Impulse `.iprb` 原始二进制远比 Mala "改名+字段映射"复杂(14 个独立通道文件、需逆向二进制布局);且 P1 链含**处理** + (gain/filter),而规范化 `.data` 是**原始**——raw vs processed 语义需先厘清。高成本、低边际价值。 +2. 明星路 的 `.iprb` 已有现成 in-app 数据层入口(`createGprVolume`,只差接 UI 触发),复用成本低。 + +**双数据集测试(校正后表述)**: +- **Mala(16ch,规范化 reader 新路径)** + **明星路(14ch,现有 `.iprb` P1 路径)** 在 app 内**首次**稠密渲染, + 二者喂**同一条下游 render/slice/anomaly 链** → 互证下游对两种通道数/几何无关。**前置任务**:明星路 in-app 导入入口 + 同样需接(复用现成 `createGprVolume`,与清单⑤同形)。 +- 以 **`gpr_poc` CLI 的明星路成像作为离线对照基线**(那才是已验证的),不声称 in-app 已验证。 +- ⚠️ 注意:明星路是**处理后**数据、Mala 是**原始**数据 → 双测只验"管路通 + 几何/通道数无关",**不验"成像一致"**(二者有无增益滤波,视觉不可比)。 +- 把 **`RADAR_TYPE_IMPLUSE` 转换器列为规划的第二插件**,待规范化管线需吞并 Impulse(多半与服务端插件同步)时再做, + 届时一并解决 raw/processed 取舍。当前"插件产规范化→reader 吃规范化"契约**仅对 Mala 一家成立,属过渡态**。 + +--- + +## 8. 风险 / 待确认 + +1. **`.cor` 坐标语义**:`.pos` 是本地投影坐标(米),按文档直映入经纬度列、N/E/M 占位、解状态=4。 + 真实 CRS 未知;单线渲染不依赖 `.cor` 配准,多线阶段需后端给 `CRS_CODE`。 +2. **波速默认值**:Mala `.rad` 无 `DIELECTRIC` → Z 深度 spacing 用默认波速(建议介电常数≈9 / v≈0.1 m·ns⁻¹), + 仅影响深度标尺,不影响体结构。需确认默认值。 +3. **采集模式**:本期只支持**距离模式**(X 用 `DISTANCE_INTERVAL`);时间模式(`SCAN_SECOND`)推迟。 +4. **大体量 / 内存实算**:app 侧 `ScalarVolume` 是 `std::vector`(8 字节/体素,**比 int16 体放大 4×**,是真正天花板)。 + 单线最大线(K=3778, ny≈49@2.5cm, N=516):coarse=4 → nx=945 → 体素 23.9M → **≈182 MiB**(+中间 BuiltI16 48MB +VTK 副本,单线峰值 <0.5GB,**P0 安全**); + coarse=1 全分辨率 → **≈764 MB** 仅体(单线勉强,非 P0 默认)。**多线/全分辨率必走 P2 核外**(`ChunkedVolumeStore`,已有 gpr_poc 实现)。 + +--- + +## 9. 测试计划 + +- **转换器(已验证)**:`info` 维度校验 6/6 通过;`convert` 产物 `.data` 与源 `.rd3` 字节一致;`probe` 主序确诊。 +- **C++ reader 单测**:`tests/data/radar/` 放截断样例(position-major 可按字节前截 K' 道得合法小体); + 断言 K/M/N、字节序、`planChannelInterpolation` 行数、主序填值正确。 +- **bridge/repo 单测**:`createRadarVolumeGrid` 产 `VolumeGrid` 维度/spacing/vmin-vmax 合理;NaN 空值语义。 +- **联调(人工)**:`build.bat app` → 导入样例 → 渲染 + 三向切片 + 三类异常;对照 明星路 现有路径同场景。 +- 失败排查看桌面日志 `%LOCALAPPDATA%/Geomative/Geopro3/logs/geopro_*.log`。 + +--- + +## 10. 文件地图(现有锚点) + +- 数据体模型 `src/data/repo/I3dSceneRepository.hpp:19`(`VolumeGrid` 稠密 float) +- 渲染入口 `src/controller/I3dSceneView.hpp:45`(`addVolume`)/ `src/app/VtkSceneView.cpp:173` +- 体素工厂 `src/render/actors/VoxelActor.hpp:29/47`(`buildVoxel`/`buildVoxelI16`) +- GPR IO `src/io/gpr/Gpr3dvVolumeBridge.hpp:47`(轴映射范本)/ `GprGeometry.hpp:27,32`(通道插值/深度) +- 体进 app `src/data/api/Api3dRepository.cpp:128`(`createGprVolume`)/ `src/data/GprVolumeRepository.cpp:11,38` +- 切片/异常 `src/render/interact/SliceTool.hpp` / `InteractionManager.hpp:52,57` / `AnomalyDrawTool.hpp:33` +- 三级树 `src/app/panels/columns/CategoryAnalysisTab.hpp:28` / `CategoryConfig.hpp:23` +- 转换器 `tools/radar_convert/malamira.py` + `README.md`;样例 `samples/radar/malamira_南同大道/` +- 详情视图扩展(如未来做 2D 波列图,非本期)`docs/superpowers/specs/2026-06-28-dataset-detail-view-architecture-and-extension-guide.md` diff --git a/docs/superpowers/specs/2026-06-30-radar-import-process-render-pipeline.md b/docs/superpowers/specs/2026-06-30-radar-import-process-render-pipeline.md new file mode 100644 index 0000000..1c9fac0 --- /dev/null +++ b/docs/superpowers/specs/2026-06-30-radar-import-process-render-pipeline.md @@ -0,0 +1,115 @@ +# 三维雷达 导入→处理→渲染 全链路方案(结合 POC 评估) + +> 2026-06-30。本文把用户给出的雷达产品目标落成方案,并结合 POC(明星路 13G)已验证的资产, +> 评估复用/缺口、定关键架构缝与决策、排风险与分期。范围限定**三维雷达**(渲染/切片/异常 + +> 其上游的导入/处理管线);不含 2D 雷达图、不含后端反演链。 + +## 1. 用户目标(七步) + +1. 设备经 USB 接到用户电脑。 +2. 客户端「设备连接」功能:自动识别设备、打开 USB 存储,用户选文件导入。 +3. 另一导入分支:文件已在用户电脑,经**文件夹选择**导入。 +4. 导入过程按文件类型(不同型号雷达)**自动加载插件**,对数据做 **ds 标准化转换**。 +5. 导入完成自动形成 **项目 / GS / TM / ds** 结构(建 GS/TM 的「方案」待细化)。 +6. 数据集详情页:用**数据处理插件**处理原始数据,插件支持多种方法(用户勾选); + 另**固定加入两个客户端内置处理方法**:**插值**、**预渲染**(即为 LOD 做准备)。 + 处理后**存为新 ds**。 +7. VTK 视图:选三维雷达 ds 渲染、切片等;**所选 ds 可能是未处理原数据,也可能是处理后数据**(不同 ds)。 + +## 2. 关键决策(用户已拍板) + +- **D1 — 预渲染(LOD 烘焙)是可选的。** 默认勾选,但用户可取消。 + → **渲染路径必须同时支持「未预渲染」与「已预渲染」两类 ds**(不能假设所有大体都已烘焙 LOD)。 + → 采用**混合渲染源**(见 §4):原/插值 ds 走整卷源;预渲染 ds 走 LOD 源。 + +## 3. POC ↔ 目标 映射(复用 vs 缺口) + +**结论:算法基本齐(POC/app 已有标准化、插值、增益、LOD 引擎、渲染源抽象);缺的是三层"框架/管线" ++ 设备接入。** + +| 步骤 | 已有(POC/现状) | 缺口 | +|---|---|---| +| 1–2 设备 USB/存储 | 无 | **全新**:Windows 设备识别 + USB 盘浏览(与 POC 无关,纯平台 plumbing) | +| 3 文件夹导入 | 已有导入入口、`tools/radar_convert` malamira 转换器 | 文件夹选择 + 批量 | +| 4 按型号插件标准化 | 转换算法有(malamira→规范化 `.head/.data`、`RadarVolumeAssembler`、int16 量化) | **导入插件框架**(按文件类型注册 reader);现写死一种 | +| 5 项目/GS/TM/ds 结构 | ds 树(`sourceShowParentId` 派生嵌套)已在 | 自动建 GS/TM 的「方案」 | +| 6 处理插件 + 两内置 | 两内置**算法都有**:插值=`createRadarVolumeGrid` 通道插值(targetDy);预渲染=`ChunkedVolumeStore::write`+`buildPyramidStreaming`。增益(dewow/AGC/tpow)亦有 | **处理插件框架** + 「处理→存为新 ds」管线 + 多方法勾选 UI | +| 7 选 ds 渲染/切片 | **渲染源抽象 `IVolumeRenderSource`(整卷/LOD 多态,含 `sliceSource()`)** + 整卷渲染 + 切片 + 异常 | 把 app 雷达路径迁到 `IVolumeRenderSource`;LOD 源接进 app(Track D) | + +## 4. 架构缝:`IVolumeRenderSource`(已设计好,最低风险) + +POC 已建好渲染源抽象(`src/render/source/IVolumeRenderSource.hpp`):上层(控制器/`SliceTool`)只认此接口, +运行时在两种实现间切换: + +- **`WholeVolumeSource`(整卷)** —— 给**未预渲染**的原/插值 ds(小体,单纹理够用)。 +- **`ViewAdaptiveVolumeSource`(核外金字塔 LOD)** —— 给**已预渲染**的 ds(大体,按相机选层/选块重组单纹理)。 + +接口自带 `update(vtkCamera)`、`currentImages()`、**`sliceSource()`**(切片/异常的 reslice 基底也走它), +故"切片在两种源上都能切"是接口内建能力,不需两套切片代码。 + +> **D1 落到这里**:选 ds 渲染时按"该 ds 是否带 LOD store"路由到对应源。未预渲染 → 整卷源(现有内存 +> 体路径迁入即可);已预渲染 → LOD 源(Track D 接入)。 + +## 5. 处理与数据血缘模型 + +- 处理一律**产出新 ds**,挂在源 ds 下(复用现有派生树 `sourceShowParentId`): + ``` + 原始 ds ─[插值]→ 插值 ds ─[预渲染]→ 预渲染 ds(LOD store) + └─[增益/migration/…(可多选)]→ 处理 ds + ``` +- **两个内置处理方法**(client 自带、固定加入): + - **插值**:线内通道插值(读真实道偏移、目标横向间距如 2.5cm,**绝不跨线**)。算法=`createRadarVolumeGrid` 的 targetDy 路径。 + - **预渲染(LOD 烘焙)**:把体烘成 **`ChunkedVolumeStore` 分块金字塔**(int16 量化、64³ brick、qCompress、 + 逐级 2× 降采样、每块 min/max;流式 `buildPyramidStreaming` 不持整卷)。产出 = 一个 **store 目录**, + 不是普通稠密体 → 该 ds 须带「类型=LOD store + 路径」标记,供 §4 渲染路由。 +- 顺序:通常先插值再预渲染(烘焙插值后的体);模型支持任选基底(也可直接烘原始)。 + +## 6. 预渲染专用落盘格式(LOD 前置,已实现于 POC) + +`ChunkedVolumeStore`(一个目录,非单文件): +- `meta.json`:几何 + 量化(scale/offset) + 逐块索引(offset/压缩长/**每块 min/max**); +- `data.bin`:逐块 int16 → qCompress;块内 i 最快、k 最慢;偏移全 64 位(卷 >2GB); +- `data_L1.bin…`:金字塔各级(逐级 2× 降采样)。 + +构建:整卷 `write` 或流式 `StreamingVolumeWriter`(逐块写不持整卷)+ `buildPyramid(Streaming)`。 +渲染:`ViewAdaptiveVolumeSource` 打开 store,`update(相机)`→选层+选块→`readBrick`→重组单 `vtkImageData`, +**内存恒定、绕 16384 纹理墙**。 + +## 7. 需要新建的三块骨架 + +1. **插件框架(两类,别混)** + - **导入插件**(步4):按文件类型/型号 → 标准化成 ds 的 reader 注册表。 + - **处理插件**(步6):吃一个 ds → 产出新 ds 的 transform,可多选串联;两内置(插值、预渲染)即自带处理插件。 + - 待定:插件接口(输入 ds/参数 → 输出 ds)、发现/注册、进程内 DLL(ABI/崩溃隔离风险)vs 子进程。 +2. **「处理 → 新 ds」管线**:血缘落树、预渲染 ds 的 store 路径/缓存/失效/磁盘占用、重处理**异步+进度+可取消**。 +3. **设备/USB 接入**(步1–2):Windows 设备识别 + USB 盘浏览。最独立、与 POC 无关,可最后做;先跑通文件夹导入。 + +## 8. 风险排序 + +1. **中**:插件框架(架构骨架,影响步4/6,定义不好后面返工)。 +2. **中**:预渲染 ds 的渲染/切片路由(Track D 核心;但**引擎+缝已验证**,是"接线"风险非"能不能做"风险)。 +3. **低–中**:处理管线异步/进度/缓存(工程量明确)。 +4. **低**:设备 USB(plumbing,独立)。 + +## 9. 分期建议 + +- **P0 验证最高技术风险**:把 app 雷达渲染迁到 `IVolumeRenderSource`,使 + - 未预渲染 ds → `WholeVolumeSource`(迁现有内存体路径); + - 预渲染 ds → `ViewAdaptiveVolumeSource`; + 南同大道先烘一个小 store 验"选 ds→按是否预渲染路由→渲染+切片"全链路。**验通则整个方案立住。** +- **P1 插件框架**:先定**处理插件**接口(含两内置),跑通"原 ds→插值→预渲染→渲染";导入插件框架并行。 +- **P2 处理管线 UI/异步**:详情页多方法勾选、进度、新 ds 落树。 +- **P3 设备 USB**:最后接。 + +## 10. 现状基线(本轮已落地的交互/渲染精修,作为接入前的稳定底座) + +- 切片拾取**精确化**:光标射线 vs 切片真实矩形求交 + 可见数据(alpha)双判定,去除外扩(雷达+反演通用)。 +- 取消选中:点击体/空白/帘面即取消(精确"命中切片"判据)+ Esc 兜底。 +- 滚轮步长:按**沿法向体素间距 × N**(Shift 粗调),不随体长跳变。 +- 双击正视:缩放到切片(按面内尺寸+视角框住),不再"又小又远"。 +- 不透明度:各向异性体用特征尺度(门控;近立方反演维持原对角线)。 +- **B 方案视角导航**:#1 绕拾取点旋转(无选中时绕光标射线穿体中段点,不甩飞); + #2 沿线位置滑块(雷达专属,沿最长轴 dolly 到窗口;仅细长体显示)。 +- 雷达显示**增益模式**右键切换(AGC/保幅 tpow/关),纯显示重建、不动原始数据。 + +> 这些是**单内存体 + 渲染期采样距自适应**底座;多分辨率/视锥 LOD 仍属 §4/§9 的 Track D 接入范畴(未做)。 diff --git a/samples/radar/malamira_南同大道/README.md b/samples/radar/malamira_南同大道/README.md new file mode 100644 index 0000000..625291d --- /dev/null +++ b/samples/radar/malamira_南同大道/README.md @@ -0,0 +1,21 @@ +# 样例:MALA南同大道(RADAR_TYPE_MALAMIRA → 规范化格式) + +三维雷达联调/单测固定样例。由 `tools/radar_convert/malamira.py` 从原始 Mala Mira +数据转换而来(16 通道、516 采样、距离模式,6 条测线)。 + +- `*.head` / `*.cor`:**纳入 git**(小文本)。 +- `*.data`:**.gitignore 忽略**(每条 ~40–62MB,共 ~277MB,过大不入库)。需要时按下方命令重生成。 + +## 重新生成 .data(及全部规范化档) + +```bash +python tools/radar_convert/malamira.py convert \ + "D:/Downloads/MALA南同大道_rSlicer" \ + --out "samples/radar/malamira_南同大道" +``` + +原始数据来源:`D:/Downloads/MALA南同大道_rSlicer`(厂商 Mala rSlicer 导出,`.rad/.rd3/_G01.pos`)。 + +## 已验证数据事实(见 tools/radar_convert/README.md) +- `.data` 主序 = **position-major** `(K道, M通道, N采样)`,int16 小端,无需转置。 +- 维度:K∈[2333,3778],M=16,N=516;`K = LAST_TRACE / NUMBER_OF_CH`。 diff --git a/samples/radar/malamira_南同大道/南同大道_000.cor b/samples/radar/malamira_南同大道/南同大道_000.cor new file mode 100644 index 0000000..a6279fa --- /dev/null +++ b/samples/radar/malamira_南同大道/南同大道_000.cor @@ -0,0 +1,527 @@ +VERSION:1 +1 317.179340 N 472.759046 E 49.980000 M 4 +12 317.201303 N 472.700649 E 51.040000 M 4 +17 317.384393 N 473.183925 E 51.140000 M 4 +23 317.597198 N 473.749575 E 51.200000 M 4 +29 317.792284 N 474.255457 E 51.240000 M 4 +35 318.046986 N 474.867687 E 51.280000 M 4 +41 318.273818 N 475.403025 E 51.310000 M 4 +48 318.543100 N 475.999329 E 51.340000 M 4 +55 318.830654 N 476.625089 E 51.370000 M 4 +60 319.046043 N 477.118469 E 51.390000 M 4 +65 319.246667 N 477.600376 E 51.410000 M 4 +71 319.458364 N 478.140509 E 51.430000 M 4 +77 319.651051 N 478.641082 E 51.450000 M 4 +82 319.850383 N 479.176249 E 51.470000 M 4 +88 320.012986 N 479.663978 E 51.490000 M 4 +93 320.156763 N 480.190410 E 51.510000 M 4 +101 320.339483 N 480.949576 E 51.540000 M 4 +108 320.495626 N 481.651715 E 51.570000 M 4 +113 320.608581 N 482.152802 E 51.590000 M 4 +121 320.752912 N 482.893130 E 51.620000 M 4 +128 320.857007 N 483.611708 E 51.650000 M 4 +134 320.936555 N 484.149101 E 51.670000 M 4 +139 321.010566 N 484.662347 E 51.690000 M 4 +145 321.078671 N 485.236388 E 51.710000 M 4 +150 321.144377 N 485.778405 E 51.730000 M 4 +156 321.211744 N 486.372825 E 51.750000 M 4 +162 321.271174 N 486.920493 E 51.770000 M 4 +168 321.334665 N 487.508405 E 51.790000 M 4 +173 321.408491 N 488.041688 E 51.810000 M 4 +179 321.493207 N 488.605625 E 51.830000 M 4 +184 321.570171 N 489.118015 E 51.850000 M 4 +190 321.651565 N 489.665169 E 51.870000 M 4 +195 321.736650 N 490.163345 E 51.890000 M 4 +201 321.830410 N 490.703820 E 51.910000 M 4 +206 321.917156 N 491.205763 E 51.930000 M 4 +211 322.003164 N 491.757884 E 51.950000 M 4 +217 322.081051 N 492.279179 E 51.970000 M 4 +223 322.169273 N 492.859214 E 51.990000 M 4 +228 322.248452 N 493.432741 E 52.010000 M 4 +235 322.327447 N 494.097546 E 52.030000 M 4 +242 322.416592 N 494.750021 E 52.050000 M 4 +250 322.513489 N 495.501994 E 52.070000 M 4 +257 322.607618 N 496.247460 E 52.090000 M 4 +266 322.703962 N 497.114858 E 52.110000 M 4 +275 322.803443 N 497.962561 E 52.130000 M 4 +280 322.862135 N 498.476150 E 52.140000 M 4 +289 322.964385 N 499.396294 E 52.160000 M 4 +300 323.075125 N 500.446076 E 52.180000 M 4 +305 323.114068 N 500.948705 E 52.190000 M 4 +310 323.166485 N 501.465718 E 52.200000 M 4 +315 323.218717 N 501.993521 E 52.210000 M 4 +322 323.279255 N 502.624075 E 52.220000 M 4 +327 323.330749 N 503.176538 E 52.230000 M 4 +333 323.382981 N 503.739962 E 52.240000 M 4 +339 323.436874 N 504.314345 E 52.250000 M 4 +346 323.500181 N 504.996790 E 52.260000 M 4 +352 323.545399 N 505.590525 E 52.270000 M 4 +358 323.602984 N 506.183061 E 52.280000 M 4 +364 323.662784 N 506.758986 E 52.290000 M 4 +370 323.734949 N 507.422763 E 52.300000 M 4 +376 323.800839 N 507.989612 E 52.310000 M 4 +382 323.868759 N 508.555433 E 52.320000 M 4 +388 323.936311 N 509.118342 E 52.330000 M 4 +394 324.012537 N 509.770132 E 52.340000 M 4 +400 324.070121 N 510.323965 E 52.350000 M 4 +405 324.138964 N 510.879682 E 52.360000 M 4 +411 324.210023 N 511.431118 E 52.370000 M 4 +418 324.293262 N 512.071091 E 52.380000 M 4 +423 324.365058 N 512.618416 E 52.390000 M 4 +429 324.436854 N 513.161974 E 52.400000 M 4 +434 324.510312 N 513.702621 E 52.410000 M 4 +441 324.599088 N 514.329408 E 52.420000 M 4 +446 324.668854 N 514.855498 E 52.430000 M 4 +451 324.744342 N 515.387582 E 52.440000 M 4 +457 324.819091 N 515.916241 E 52.450000 M 4 +463 324.899931 N 516.526074 E 52.460000 M 4 +468 324.966744 N 517.039491 E 52.470000 M 4 +473 325.032450 N 517.540578 E 52.480000 M 4 +484 325.178626 N 518.604403 E 52.500000 M 4 +494 325.295457 N 519.583630 E 52.520000 M 4 +505 325.410811 N 520.655333 E 52.540000 M 4 +510 325.469872 N 521.155221 E 52.550000 M 4 +515 325.536869 N 521.657335 E 52.560000 M 4 +520 325.609219 N 522.164245 E 52.570000 M 4 +526 325.687660 N 522.761234 E 52.580000 M 4 +531 325.748382 N 523.274480 E 52.590000 M 4 +537 325.804860 N 523.798173 E 52.600000 M 4 +542 325.862075 N 524.328030 E 52.610000 M 4 +548 325.934425 N 524.949337 E 52.620000 M 4 +554 326.000500 N 525.487415 E 52.630000 M 4 +559 326.066575 N 526.031829 E 52.640000 M 4 +565 326.129696 N 526.582408 E 52.650000 M 4 +572 326.199831 N 527.231972 E 52.660000 M 4 +577 326.258893 N 527.792826 E 52.670000 M 4 +583 326.323122 N 528.361387 E 52.680000 M 4 +589 326.391042 N 528.933887 E 52.690000 M 4 +596 326.472620 N 529.610166 E 52.700000 M 4 +602 326.542386 N 530.196366 E 52.710000 M 4 +608 326.612706 N 530.788560 E 52.720000 M 4 +614 326.684502 N 531.385378 E 52.730000 M 4 +621 326.772356 N 532.087687 E 52.740000 M 4 +627 326.844521 N 532.692040 E 52.750000 M 4 +633 326.921670 N 533.302558 E 52.760000 M 4 +640 326.998265 N 533.915987 E 52.770000 M 4 +647 327.089810 N 534.636450 E 52.780000 M 4 +653 327.170835 N 535.256387 E 52.790000 M 4 +659 327.252413 N 535.878036 E 52.800000 M 4 +666 327.334729 N 536.501740 E 52.810000 M 4 +673 327.432734 N 537.231622 E 52.820000 M 4 +680 327.506191 N 537.853614 E 52.830000 M 4 +686 327.588693 N 538.481086 E 52.840000 M 4 +692 327.673039 N 539.109072 E 52.850000 M 4 +700 327.773443 N 539.842207 E 52.860000 M 4 +706 327.857975 N 540.469165 E 52.870000 M 4 +713 327.945275 N 541.094582 E 52.880000 M 4 +719 328.032390 N 541.718458 E 52.890000 M 4 +726 328.133532 N 542.444571 E 52.900000 M 4 +733 328.216402 N 543.059199 E 52.910000 M 4 +739 328.304994 N 543.677766 E 52.920000 M 4 +745 328.393586 N 544.294792 E 52.930000 M 4 +752 328.496020 N 545.018166 E 52.940000 M 4 +759 328.582213 N 545.638274 E 52.950000 M 4 +765 328.666006 N 546.259409 E 52.960000 M 4 +771 328.750353 N 546.881915 E 52.970000 M 4 +779 328.850018 N 547.606488 E 52.980000 M 4 +785 328.926060 N 548.225226 E 52.990000 M 4 +791 329.010037 N 548.847389 E 53.000000 M 4 +798 329.091985 N 549.470922 E 53.010000 M 4 +805 329.185560 N 550.200803 E 53.020000 M 4 +812 329.265292 N 550.827077 E 53.030000 M 4 +818 329.345948 N 551.454035 E 53.040000 M 4 +824 329.424758 N 552.083391 E 53.050000 M 4 +832 329.513719 N 552.820294 E 53.060000 M 4 +838 329.574441 N 553.450334 E 53.070000 M 4 +845 329.646976 N 554.087225 E 53.080000 M 4 +851 329.717295 N 554.727884 E 53.090000 M 4 +859 329.798320 N 555.479514 E 53.100000 M 4 +865 329.867717 N 556.126509 E 53.110000 M 4 +872 329.933422 N 556.775217 E 53.120000 M 4 +878 329.997098 N 557.426150 E 53.130000 M 4 +886 330.069817 N 558.187885 E 53.140000 M 4 +893 330.115405 N 558.837277 E 53.150000 M 4 +899 330.176681 N 559.491807 E 53.160000 M 4 +906 330.241463 N 560.148906 E 53.170000 M 4 +914 330.317874 N 560.916977 E 53.180000 M 4 +921 330.381549 N 561.575446 E 53.190000 M 4 +927 330.445225 N 562.233230 E 53.200000 M 4 +934 330.507239 N 562.890329 E 53.210000 M 4 +942 330.579220 N 563.653091 E 53.220000 M 4 +948 330.625361 N 564.298202 E 53.230000 M 4 +955 330.686637 N 564.943484 E 53.240000 M 4 +961 330.748652 N 565.582601 E 53.250000 M 4 +969 330.824693 N 566.321045 E 53.260000 M 4 +975 330.885969 N 566.945264 E 53.270000 M 4 +981 330.945584 N 567.561433 E 53.280000 M 4 +987 331.004460 N 568.169039 E 53.290000 M 4 +994 331.072934 N 568.873233 E 53.300000 M 4 +1000 331.122029 N 569.454809 E 53.310000 M 4 +1006 331.184597 N 570.047688 E 53.320000 M 4 +1012 331.244212 N 570.636457 E 53.330000 M 4 +1019 331.310102 N 571.320785 E 53.340000 M 4 +1025 331.369348 N 571.908526 E 53.350000 M 4 +1031 331.434315 N 572.497123 E 53.360000 M 4 +1037 331.502789 N 573.083666 E 53.370000 M 4 +1044 331.582706 N 573.770049 E 53.380000 M 4 +1050 331.641029 N 574.348371 E 53.390000 M 4 +1056 331.711165 N 574.942106 E 53.400000 M 4 +1062 331.783884 N 575.535328 E 53.410000 M 4 +1069 331.874506 N 576.233014 E 53.420000 M 4 +1075 331.953316 N 576.835483 E 53.430000 M 4 +1081 332.031203 N 577.442918 E 53.440000 M 4 +1088 332.106136 N 578.054806 E 53.450000 M 4 +1095 332.193990 N 578.775782 E 53.460000 M 4 +1101 332.257112 N 579.388355 E 53.470000 M 4 +1108 332.328723 N 580.020108 E 53.480000 M 4 +1114 332.394429 N 580.659911 E 53.490000 M 4 +1122 332.466041 N 581.415994 E 53.500000 M 4 +1128 332.527317 N 582.072065 E 53.510000 M 4 +1135 332.590069 N 582.733788 E 53.520000 M 4 +1142 332.655221 N 583.400477 E 53.530000 M 4 +1150 332.728125 N 584.185159 E 53.540000 M 4 +1156 332.777958 N 584.854075 E 53.550000 M 4 +1163 332.839049 N 585.535320 E 53.560000 M 4 +1170 332.904755 N 586.220333 E 53.570000 M 4 +1178 332.991501 N 587.022141 E 53.580000 M 4 +1185 333.071418 N 587.712463 E 53.590000 M 4 +1192 333.152443 N 588.405354 E 53.600000 M 4 +1200 333.232544 N 589.101156 E 53.610000 M 4 +1208 333.325012 N 589.915808 E 53.620000 M 4 +1215 333.401792 N 590.605787 E 53.630000 M 4 +1222 333.488722 N 591.308953 E 53.640000 M 4 +1229 333.578052 N 592.014345 E 53.650000 M 4 +1238 333.682517 N 592.839957 E 53.660000 M 4 +1245 333.771293 N 593.552542 E 53.670000 M 4 +1252 333.858224 N 594.267353 E 53.680000 M 4 +1259 333.944970 N 594.981651 E 53.690000 M 4 +1268 334.049065 N 595.815825 E 53.700000 M 4 +1275 334.125107 N 596.524643 E 53.710000 M 4 +1282 334.213699 N 597.244934 E 53.720000 M 4 +1290 334.298415 N 597.968136 E 53.730000 M 4 +1298 334.398819 N 598.813100 E 53.740000 M 4 +1306 334.486488 N 599.541440 E 53.750000 M 4 +1313 334.577110 N 600.268582 E 53.760000 M 4 +1320 334.667547 N 600.998634 E 53.770000 M 4 +1329 334.768874 N 601.853531 E 53.780000 M 4 +1336 334.840486 N 602.575877 E 53.790000 M 4 +1344 334.928708 N 603.312095 E 53.800000 M 4 +1351 335.019146 N 604.047970 E 53.810000 M 4 +1360 335.124164 N 604.907833 E 53.820000 M 4 +1368 335.210356 N 605.646619 E 53.830000 M 4 +1375 335.294150 N 606.386091 E 53.840000 M 4 +1383 335.379788 N 607.124364 E 53.850000 M 4 +1391 335.483514 N 607.987994 E 53.860000 M 4 +1399 335.558448 N 608.711711 E 53.870000 M 4 +1406 335.642795 N 609.447586 E 53.880000 M 4 +1414 335.724927 N 610.185002 E 53.890000 M 4 +1422 335.820532 N 611.045208 E 53.900000 M 4 +1430 335.903403 N 611.780740 E 53.910000 M 4 +1437 335.987380 N 612.515759 E 53.920000 M 4 +1445 336.069512 N 613.248895 E 53.930000 M 4 +1453 336.164379 N 614.103106 E 53.940000 M 4 +1461 336.230639 N 614.816718 E 53.950000 M 4 +1468 336.310002 N 615.543860 E 53.960000 M 4 +1475 336.387705 N 616.269802 E 53.970000 M 4 +1484 336.478511 N 617.114766 E 53.980000 M 4 +1491 336.555660 N 617.836599 E 53.990000 M 4 +1499 336.632255 N 618.557917 E 54.000000 M 4 +1506 336.707558 N 619.280264 E 54.010000 M 4 +1514 336.792458 N 620.120604 E 54.020000 M 4 +1522 336.849305 N 620.827708 E 54.030000 M 4 +1529 336.920917 N 621.545431 E 54.040000 M 4 +1536 336.992713 N 622.263325 E 54.050000 M 4 +1544 337.076875 N 623.094930 E 54.060000 M 4 +1552 337.150886 N 623.807002 E 54.070000 M 4 +1559 337.228035 N 624.519586 E 54.080000 M 4 +1566 337.304630 N 625.231658 E 54.090000 M 4 +1575 337.391007 N 626.062578 E 54.100000 M 4 +1582 337.450991 N 626.763004 E 54.110000 M 4 +1589 337.530354 N 627.471650 E 54.120000 M 4 +1596 337.613225 N 628.177385 E 54.130000 M 4 +1604 337.712890 N 629.000257 E 54.140000 M 4 +1612 337.793546 N 629.706677 E 54.150000 M 4 +1619 337.871987 N 630.412583 E 54.160000 M 4 +1626 337.953380 N 631.116262 E 54.170000 M 4 +1634 338.051939 N 631.934511 E 54.180000 M 4 +1641 338.119490 N 632.627059 E 54.190000 M 4 +1648 338.206051 N 633.327142 E 54.200000 M 4 +1655 338.291690 N 634.027739 E 54.210000 M 4 +1664 338.390802 N 634.845474 E 54.220000 M 4 +1671 338.477918 N 635.546242 E 54.230000 M 4 +1678 338.566509 N 636.244442 E 54.240000 M 4 +1685 338.655470 N 636.942813 E 54.250000 M 4 +1693 338.761596 N 637.757464 E 54.260000 M 4 +1700 338.835238 N 638.445560 E 54.270000 M 4 +1707 338.922907 N 639.144787 E 54.280000 M 4 +1714 339.009838 N 639.845213 E 54.290000 M 4 +1723 339.113010 N 640.665345 E 54.300000 M 4 +1730 339.207323 N 641.369538 E 54.310000 M 4 +1737 339.300714 N 642.067053 E 54.320000 M 4 +1744 339.387829 N 642.766109 E 54.330000 M 4 +1752 339.482512 N 643.582644 E 54.340000 M 4 +1759 339.541388 N 644.271083 E 54.350000 M 4 +1767 339.621306 N 644.971680 E 54.360000 M 4 +1774 339.706575 N 645.670736 E 54.370000 M 4 +1782 339.805134 N 646.492066 E 54.380000 M 4 +1789 339.880990 N 647.190780 E 54.390000 M 4 +1796 339.950203 N 647.890521 E 54.400000 M 4 +1803 340.018861 N 648.593515 E 54.410000 M 4 +1812 340.103577 N 649.415360 E 54.420000 M 4 +1819 340.155994 N 650.109963 E 54.430000 M 4 +1826 340.226314 N 650.819123 E 54.440000 M 4 +1833 340.294788 N 651.527255 E 54.450000 M 4 +1841 340.375443 N 652.353723 E 54.460000 M 4 +1849 340.452961 N 653.062712 E 54.470000 M 4 +1856 340.525865 N 653.780434 E 54.480000 M 4 +1863 340.593970 N 654.500897 E 54.490000 M 4 +1872 340.684961 N 655.337127 E 54.500000 M 4 +1879 340.734978 N 656.050739 E 54.510000 M 4 +1886 340.823755 N 656.763666 E 54.520000 M 4 +1893 340.909947 N 657.482759 E 54.530000 M 4 +1902 341.004814 N 658.324640 E 54.540000 M 4 +1909 341.089161 N 659.046130 E 54.550000 M 4 +1917 341.180706 N 659.774299 E 54.560000 M 4 +1924 341.272435 N 660.499214 E 54.570000 M 4 +1933 341.376162 N 661.346404 E 54.580000 M 4 +1940 341.434485 N 662.079539 E 54.590000 M 4 +1948 341.514217 N 662.822265 E 54.600000 M 4 +1955 341.597641 N 663.572012 E 54.610000 M 4 +1964 341.702475 N 664.445403 E 54.620000 M 4 +1972 341.793835 N 665.199089 E 54.630000 M 4 +1979 341.885564 N 665.956200 E 54.640000 M 4 +1987 341.975817 N 666.715537 E 54.650000 M 4 +1996 342.083973 N 667.601772 E 54.660000 M 4 +2004 342.157061 N 668.351690 E 54.670000 M 4 +2011 342.253590 N 669.111027 E 54.680000 M 4 +2019 342.345504 N 669.873618 E 54.690000 M 4 +2028 342.450891 N 670.759683 E 54.700000 M 4 +2036 342.540959 N 671.517307 E 54.710000 M 4 +2044 342.630843 N 672.274418 E 54.720000 M 4 +2051 342.721281 N 673.029302 E 54.730000 M 4 +2060 342.823346 N 673.909544 E 54.740000 M 4 +2068 342.892558 N 674.651927 E 54.750000 M 4 +2075 342.982442 N 675.405613 E 54.760000 M 4 +2083 343.072695 N 676.159127 E 54.770000 M 4 +2092 343.181589 N 677.036286 E 54.780000 M 4 +2099 343.277194 N 677.788602 E 54.790000 M 4 +2107 343.372430 N 678.534581 E 54.800000 M 4 +2115 343.469512 N 679.280561 E 54.810000 M 4 +2123 343.583390 N 680.146246 E 54.820000 M 4 +2131 343.664045 N 680.873558 E 54.830000 M 4 +2138 343.760942 N 681.613544 E 54.840000 M 4 +2146 343.854517 N 682.351817 E 54.850000 M 4 +2155 343.961566 N 683.209967 E 54.860000 M 4 +2162 344.052003 N 683.947212 E 54.870000 M 4 +2170 344.144102 N 684.683944 E 54.880000 M 4 +2177 344.236754 N 685.419819 E 54.890000 M 4 +2186 344.343987 N 686.280538 E 54.900000 M 4 +2193 344.423166 N 687.003912 E 54.910000 M 4 +2201 344.515449 N 687.742185 E 54.920000 M 4 +2208 344.607363 N 688.482341 E 54.930000 M 4 +2217 344.717918 N 689.348712 E 54.940000 M 4 +2224 344.812232 N 690.091780 E 54.950000 M 4 +2232 344.905807 N 690.837245 E 54.960000 M 4 +2240 344.996613 N 691.583909 E 54.970000 M 4 +2249 345.099601 N 692.456102 E 54.980000 M 4 +2256 345.176750 N 693.189580 E 54.990000 M 4 +2264 345.261835 N 693.941039 E 55.000000 M 4 +2271 345.345813 N 694.694211 E 55.010000 M 4 +2280 345.439942 N 695.574796 E 55.020000 M 4 +2288 345.518936 N 696.332420 E 55.030000 M 4 +2295 345.596823 N 697.090045 E 55.040000 M 4 +2303 345.672864 N 697.851437 E 55.050000 M 4 +2312 345.761641 N 698.741097 E 55.060000 M 4 +2320 345.824393 N 699.493071 E 55.070000 M 4 +2328 345.895820 N 700.257031 E 55.080000 M 4 +2335 345.967432 N 701.022534 E 55.090000 M 4 +2344 346.050671 N 701.916133 E 55.100000 M 4 +2352 346.121914 N 702.681464 E 55.110000 M 4 +2360 346.194448 N 703.448850 E 55.120000 M 4 +2368 346.267537 N 704.215893 E 55.130000 M 4 +2377 346.351514 N 705.110349 E 55.140000 M 4 +2384 346.414267 N 705.863864 E 55.150000 M 4 +2392 346.491046 N 706.627996 E 55.160000 M 4 +2400 346.568195 N 707.388360 E 55.170000 M 4 +2409 346.660847 N 708.276308 E 55.180000 M 4 +2416 346.741687 N 709.033762 E 55.190000 M 4 +2424 346.827326 N 709.786420 E 55.200000 M 4 +2432 346.919425 N 710.530173 E 55.210000 M 4 +2440 347.029980 N 711.387295 E 55.220000 M 4 +2448 347.120417 N 712.100565 E 55.230000 M 4 +2455 347.227650 N 712.824453 E 55.240000 M 4 +2462 347.343004 N 713.543032 E 55.250000 M 4 +2471 347.488073 N 714.375836 E 55.260000 M 4 +2478 347.622253 N 715.086195 E 55.270000 M 4 +2485 347.762893 N 715.793642 E 55.280000 M 4 +2493 347.910915 N 716.500918 E 55.290000 M 4 +2501 348.094004 N 717.327900 E 55.300000 M 4 +2508 348.254946 N 718.024387 E 55.310000 M 4 +2516 348.430469 N 718.741596 E 55.320000 M 4 +2523 348.614850 N 719.463429 E 55.330000 M 4 +2532 348.841313 N 720.310961 E 55.340000 M 4 +2540 349.040645 N 721.041356 E 55.350000 M 4 +2548 349.250681 N 721.776204 E 55.360000 M 4 +2555 349.471976 N 722.515162 E 55.370000 M 4 +2565 349.741258 N 723.381532 E 55.380000 M 4 +2572 349.977503 N 724.110557 E 55.390000 M 4 +2580 350.226114 N 724.859105 E 55.400000 M 4 +2588 350.485983 N 725.610393 E 55.410000 M 4 +2598 350.805283 N 726.483956 E 55.420000 M 4 +2606 351.088592 N 727.230449 E 55.430000 M 4 +2614 351.381499 N 727.980025 E 55.440000 M 4 +2622 351.679389 N 728.729429 E 55.450000 M 4 +2631 352.030803 N 729.603335 E 55.460000 M 4 +2639 352.332200 N 730.333045 E 55.470000 M 4 +2647 352.627137 N 731.084847 E 55.480000 M 4 +2656 352.915244 N 731.838019 E 55.490000 M 4 +2665 353.239896 N 732.721172 E 55.500000 M 4 +2673 353.509548 N 733.481194 E 55.510000 M 4 +2681 353.768494 N 734.242415 E 55.520000 M 4 +2689 354.021165 N 735.006204 E 55.530000 M 4 +2699 354.310935 N 735.896550 E 55.540000 M 4 +2707 354.552901 N 736.642358 E 55.550000 M 4 +2715 354.793575 N 737.409573 E 55.560000 M 4 +2723 355.022068 N 738.180384 E 55.570000 M 4 +2732 355.272525 N 739.083916 E 55.580000 M 4 +2741 355.475917 N 739.861577 E 55.590000 M 4 +2749 355.669158 N 740.640608 E 55.600000 M 4 +2757 355.855200 N 741.420838 E 55.610000 M 4 +2766 356.061546 N 742.332248 E 55.620000 M 4 +2774 356.224702 N 743.099805 E 55.630000 M 4 +2782 356.375124 N 743.885515 E 55.640000 M 4 +2790 356.514840 N 744.672938 E 55.650000 M 4 +2800 356.662493 N 745.592397 E 55.660000 M 4 +2808 356.776555 N 746.378278 E 55.670000 M 4 +2816 356.880466 N 747.160734 E 55.680000 M 4 +2823 356.972934 N 747.934456 E 55.690000 M 4 +2833 357.069831 N 748.833707 E 55.700000 M 4 +2840 357.141812 N 749.585338 E 55.710000 M 4 +2848 357.212501 N 750.350669 E 55.720000 M 4 +2856 357.283559 N 751.110520 E 55.730000 M 4 +2865 357.367721 N 751.992303 E 55.740000 M 4 +2872 357.436933 N 752.745646 E 55.750000 M 4 +2880 357.504485 N 753.494365 E 55.760000 M 4 +2887 357.570190 N 754.239659 E 55.770000 M 4 +2896 357.645309 N 755.104660 E 55.780000 M 4 +2903 357.704924 N 755.833514 E 55.790000 M 4 +2911 357.764354 N 756.566135 E 55.800000 M 4 +2918 357.821200 N 757.295332 E 55.810000 M 4 +2927 357.886721 N 758.139953 E 55.820000 M 4 +2934 357.942460 N 758.858360 E 55.830000 M 4 +2941 357.997830 N 759.570603 E 55.840000 M 4 +2948 358.055046 N 760.277022 E 55.850000 M 4 +2957 358.125735 N 761.092702 E 55.860000 M 4 +2964 358.189041 N 761.777201 E 55.870000 M 4 +2970 358.259361 N 762.458275 E 55.880000 M 4 +2977 358.332633 N 763.129588 E 55.890000 M 4 +2985 358.415688 N 763.900057 E 55.900000 M 4 +2992 358.485639 N 764.548935 E 55.910000 M 4 +2998 358.558727 N 765.188566 E 55.920000 M 4 +3004 358.633476 N 765.813641 E 55.930000 M 4 +3012 358.719669 N 766.525883 E 55.940000 M 4 +3018 358.788143 N 767.108658 E 55.950000 M 4 +3023 358.858647 N 767.686809 E 55.960000 M 4 +3029 358.926937 N 768.245780 E 55.970000 M 4 +3036 359.006116 N 768.877019 E 55.980000 M 4 +3041 359.073852 N 769.400712 E 55.990000 M 4 +3046 359.139557 N 769.909848 E 56.000000 M 4 +3057 359.277059 N 770.956719 E 56.020000 M 4 +3066 359.397212 N 771.852202 E 56.040000 M 4 +3075 359.527146 N 772.769264 E 56.060000 M 4 +3083 359.634933 N 773.557200 E 56.080000 M 4 +3091 359.749364 N 774.348390 E 56.100000 M 4 +3098 359.835741 N 774.998125 E 56.120000 M 4 +3105 359.922672 N 775.656594 E 56.140000 M 4 +3110 359.992438 N 776.199124 E 56.160000 M 4 +3115 360.058697 N 776.715624 E 56.180000 M 4 +3121 360.129017 N 777.278191 E 56.210000 M 4 +3127 360.204320 N 777.846752 E 56.250000 M 4 +3186 360.009972 N 776.423466 E 61.980000 M 4 +3192 360.070509 N 776.937054 E 62.050000 M 4 +3197 360.126618 N 777.453897 E 62.090000 M 4 +3202 360.182541 N 777.967999 E 62.120000 M 4 +3208 360.246955 N 778.566701 E 62.150000 M 4 +3215 360.316905 N 779.262503 E 62.180000 M 4 +3220 360.367292 N 779.763932 E 62.200000 M 4 +3226 360.421924 N 780.343111 E 62.220000 M 4 +3232 360.474340 N 780.922803 E 62.240000 M 4 +3239 360.532848 N 781.584526 E 62.260000 M 4 +3245 360.584711 N 782.207374 E 62.280000 M 4 +3252 360.635836 N 782.841525 E 62.300000 M 4 +3257 360.677732 N 783.391248 E 62.320000 M 4 +3263 360.719444 N 783.958096 E 62.340000 M 4 +3268 360.753220 N 784.490180 E 62.360000 M 4 +3274 360.797700 N 785.073982 E 62.380000 M 4 +3280 360.834429 N 785.627302 E 62.400000 M 4 +3286 360.879094 N 786.237306 E 62.420000 M 4 +3292 360.932803 N 786.809121 E 62.440000 M 4 +3298 360.998693 N 787.404911 E 62.460000 M 4 +3303 361.069013 N 787.933912 E 62.480000 M 4 +3309 361.162957 N 788.480210 E 62.500000 M 4 +3314 361.278865 N 788.975988 E 62.520000 M 4 +3319 361.439437 N 789.489063 E 62.540000 M 4 +3324 361.630279 N 789.953844 E 62.560000 M 4 +3330 361.888303 N 790.453904 E 62.580000 M 4 +3336 362.182870 N 790.915260 E 62.600000 M 4 +3342 362.574151 N 791.393057 E 62.620000 M 4 +3348 363.013234 N 791.800469 E 62.640000 M 4 +3355 363.570993 N 792.185446 E 62.660000 M 4 +3361 364.150716 N 792.483255 E 62.680000 M 4 +3368 364.816262 N 792.730545 E 62.700000 M 4 +3375 365.450247 N 792.903682 E 62.720000 M 4 +3382 366.160273 N 793.015511 E 62.740000 M 4 +3389 366.836339 N 793.055926 E 62.760000 M 4 +3396 367.560393 N 793.009517 E 62.780000 M 4 +3403 368.217449 N 792.898373 E 62.800000 M 4 +3410 368.906988 N 792.713077 E 62.820000 M 4 +3417 369.525655 N 792.494729 E 62.840000 M 4 +3424 370.155210 N 792.171232 E 62.860000 M 4 +3431 370.704110 N 791.821190 E 62.880000 M 4 +3438 371.257809 N 791.382439 E 62.900000 M 4 +3444 371.737866 N 790.943003 E 62.920000 M 4 +3451 372.199651 N 790.402871 E 62.940000 M 4 +3458 372.569337 N 789.858285 E 62.960000 M 4 +3465 372.911892 N 789.233039 E 62.980000 M 4 +3472 373.179328 N 788.642216 E 63.000000 M 4 +3479 373.390841 N 787.953778 E 63.020000 M 4 +3486 373.524836 N 787.296507 E 63.040000 M 4 +3493 373.606045 N 786.569195 E 63.060000 M 4 +3500 373.618596 N 785.905931 E 63.080000 M 4 +3507 373.540340 N 785.172967 E 63.100000 M 4 +3514 373.444181 N 784.513642 E 63.120000 M 4 +3521 373.329934 N 783.789412 E 63.140000 M 4 +3528 373.238389 N 783.122894 E 63.160000 M 4 +3536 373.112331 N 782.367153 E 63.180000 M 4 +3543 372.982950 N 781.654911 E 63.200000 M 4 +3551 372.825699 N 780.864577 E 63.220000 M 4 +3559 372.685429 N 780.134524 E 63.240000 M 4 +3567 372.524487 N 779.310454 E 63.260000 M 4 +3575 372.392338 N 778.531251 E 63.280000 M 4 +3584 372.272001 N 777.684575 E 63.300000 M 4 +3592 372.188577 N 776.942363 E 63.320000 M 4 +3600 372.082636 N 776.141583 E 63.340000 M 4 +3607 371.998842 N 775.414099 E 63.360000 M 4 +3615 371.910066 N 774.637466 E 63.380000 M 4 +3622 371.839193 N 773.944575 E 63.400000 M 4 +3630 371.757061 N 773.190204 E 63.420000 M 4 +3637 371.684157 N 772.504849 E 63.440000 M 4 +3644 371.612361 N 771.775823 E 63.460000 M 4 +3650 371.558652 N 771.131911 E 63.480000 M 4 +3658 371.494054 N 770.434910 E 63.500000 M 4 +3664 371.438499 N 769.804356 E 63.520000 M 4 +3671 371.379069 N 769.136468 E 63.540000 M 4 +3677 371.334219 N 768.544959 E 63.560000 M 4 +3683 371.265745 N 767.911836 E 63.580000 M 4 +3689 371.192657 N 767.347899 E 63.600000 M 4 +3695 371.119015 N 766.758274 E 63.620000 M 4 +3700 371.058477 N 766.239719 E 63.640000 M 4 +3706 370.990742 N 765.690852 E 63.660000 M 4 +3713 370.906210 N 764.982720 E 63.690000 M 4 +3720 370.841612 N 764.321168 E 63.720000 M 4 +3725 370.775722 N 763.772644 E 63.750000 M 4 +3732 370.720167 N 763.123252 E 63.790000 M 4 +3737 370.662583 N 762.589284 E 63.830000 M 4 +3743 370.613488 N 762.024662 E 63.900000 M 4 +3777 370.988711 N 759.407569 E 66.060000 M 4 +3778 370.993695 N 759.363044 E 66.110000 M 4 diff --git a/samples/radar/malamira_南同大道/南同大道_000.head b/samples/radar/malamira_南同大道/南同大道_000.head new file mode 100644 index 0000000..5789cfe --- /dev/null +++ b/samples/radar/malamira_南同大道/南同大道_000.head @@ -0,0 +1,34 @@ +DATE:2022-03-10 +START_TIME:10:46 +STOP_TIME: +UNITS:m +MODE:距离模式 +ANTENNAS:200 MHz shielded +FREQUENCY:5351.611816 +STACKS:2 +LAST_TRACE:60448 +POSITIVE_DIRECTION:1 +SAMPLES:516 +TIME_INTERVAL:0.000000 +TIMEWINDOW:96.419553 +DEPTH: +ZERO_POSITION: +DIELECTRIC: +SOIL_TYPE: +BITS:16 +MARK: +DISTANCE_INTERVAL:0.099194 +START_POSITION:0.000000 +STOP_POSITION:374.656262 +WHEEL_GPS: +WHEEL_CALIBRATION:84.4270000000 +SCAN_SECOND: +NUMBER_OF_CH:16 +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 +RTK_X_OFFSET: +RTK_Y_OFFSET:0.350 +RTK_Z_OFFSET: +GAIN: +FILTER: +SMOOTH: +ENDIAN_TYPE:1 diff --git a/samples/radar/malamira_南同大道/南同大道_001.cor b/samples/radar/malamira_南同大道/南同大道_001.cor new file mode 100644 index 0000000..025cd5a --- /dev/null +++ b/samples/radar/malamira_南同大道/南同大道_001.cor @@ -0,0 +1,335 @@ +VERSION:1 +1 353.073787 N 657.340790 E 70.990000 M 4 +6 352.968400 N 656.889195 E 71.210000 M 4 +12 352.865227 N 656.326114 E 71.260000 M 4 +20 352.732339 N 655.609419 E 71.300000 M 4 +26 352.607388 N 654.979550 E 71.330000 M 4 +32 352.505138 N 654.409448 E 71.350000 M 4 +38 352.405472 N 653.834208 E 71.370000 M 4 +45 352.293625 N 653.205537 E 71.390000 M 4 +52 352.181409 N 652.526518 E 71.410000 M 4 +60 352.051659 N 651.717346 E 71.430000 M 4 +69 351.922278 N 650.910572 E 71.450000 M 4 +74 351.843837 N 650.416678 E 71.460000 M 4 +82 351.710211 N 649.585243 E 71.480000 M 4 +93 351.561635 N 648.594200 E 71.500000 M 4 +102 351.433177 N 647.654191 E 71.520000 M 4 +113 351.288847 N 646.606635 E 71.540000 M 4 +123 351.151714 N 645.635800 E 71.560000 M 4 +129 351.082871 N 645.102004 E 71.570000 M 4 +135 351.000923 N 644.463571 E 71.580000 M 4 +141 350.930050 N 643.902888 E 71.590000 M 4 +147 350.861022 N 643.329532 E 71.600000 M 4 +153 350.791625 N 642.744360 E 71.610000 M 4 +160 350.708755 N 642.054894 E 71.620000 M 4 +166 350.640650 N 641.513391 E 71.630000 M 4 +172 350.569592 N 640.933870 E 71.640000 M 4 +178 350.501671 N 640.358973 E 71.650000 M 4 +185 350.423046 N 639.688174 E 71.660000 M 4 +191 350.354018 N 639.112249 E 71.670000 M 4 +197 350.284252 N 638.535126 E 71.680000 M 4 +203 350.215594 N 637.959372 E 71.690000 M 4 +210 350.132908 N 637.288402 E 71.700000 M 4 +215 350.071817 N 636.760085 E 71.710000 M 4 +221 350.003527 N 636.185531 E 71.720000 M 4 +227 349.933392 N 635.611318 E 71.730000 M 4 +234 349.845723 N 634.942574 E 71.740000 M 4 +240 349.770789 N 634.369903 E 71.750000 M 4 +246 349.701946 N 633.796547 E 71.760000 M 4 +252 349.635502 N 633.223363 E 71.770000 M 4 +259 349.552632 N 632.555132 E 71.780000 M 4 +264 349.491540 N 632.031782 E 71.790000 M 4 +270 349.415684 N 631.468359 E 71.800000 M 4 +276 349.336874 N 630.907847 E 71.810000 M 4 +283 349.241822 N 630.250406 E 71.820000 M 4 +288 349.159506 N 629.681503 E 71.830000 M 4 +294 349.075528 N 629.108489 E 71.840000 M 4 +300 348.984721 N 628.531708 E 71.850000 M 4 +307 348.873982 N 627.852175 E 71.860000 M 4 +313 348.787605 N 627.312385 E 71.870000 M 4 +319 348.691076 N 626.721218 E 71.880000 M 4 +325 348.597686 N 626.122860 E 71.890000 M 4 +333 348.489899 N 625.418837 E 71.900000 M 4 +339 348.392448 N 624.806436 E 71.910000 M 4 +346 348.295735 N 624.180334 E 71.920000 M 4 +352 348.200499 N 623.545840 E 71.930000 M 4 +360 348.094558 N 622.788730 E 71.940000 M 4 +366 348.016487 N 622.177698 E 71.950000 M 4 +373 347.929556 N 621.511865 E 71.960000 M 4 +380 347.837827 N 620.836100 E 71.970000 M 4 +389 347.729117 N 620.036004 E 71.980000 M 4 +396 347.638126 N 619.339346 E 71.990000 M 4 +403 347.548980 N 618.633098 E 72.000000 M 4 +410 347.460942 N 617.916916 E 72.010000 M 4 +419 347.359062 N 617.070583 E 72.020000 M 4 +426 347.278037 N 616.383515 E 72.030000 M 4 +434 347.187784 N 615.640104 E 72.040000 M 4 +442 347.100669 N 614.888302 E 72.050000 M 4 +451 347.000634 N 614.000525 E 72.060000 M 4 +459 346.915180 N 613.231256 E 72.070000 M 4 +467 346.830833 N 612.453423 E 72.080000 M 4 +475 346.745748 N 611.669597 E 72.090000 M 4 +485 346.645344 N 610.746028 E 72.100000 M 4 +492 346.568749 N 609.997138 E 72.110000 M 4 +501 346.482187 N 609.192932 E 72.120000 M 4 +509 346.393226 N 608.382219 E 72.130000 M 4 +519 346.288762 N 607.429537 E 72.140000 M 4 +527 346.195740 N 606.608549 E 72.150000 M 4 +536 346.101058 N 605.782081 E 72.160000 M 4 +544 346.006745 N 604.949447 E 72.170000 M 4 +554 345.894344 N 603.973304 E 72.180000 M 4 +563 345.804829 N 603.179716 E 72.190000 M 4 +571 345.706455 N 602.332184 E 72.200000 M 4 +580 345.606051 N 601.480199 E 72.210000 M 4 +591 345.489405 N 600.482306 E 72.220000 M 4 +599 345.387525 N 599.622443 E 72.230000 M 4 +608 345.285091 N 598.759327 E 72.240000 M 4 +617 345.183579 N 597.891072 E 72.250000 M 4 +628 345.062504 N 596.877424 E 72.260000 M 4 +636 344.967083 N 596.055751 E 72.270000 M 4 +645 344.860773 N 595.188011 E 72.280000 M 4 +654 344.752617 N 594.319414 E 72.290000 M 4 +665 344.629511 N 593.303882 E 72.300000 M 4 +674 344.520986 N 592.432717 E 72.310000 M 4 +683 344.412830 N 591.563264 E 72.320000 M 4 +692 344.300799 N 590.698264 E 72.330000 M 4 +702 344.172341 N 589.691123 E 72.340000 M 4 +711 344.072121 N 588.877328 E 72.350000 M 4 +719 343.966549 N 588.015752 E 72.360000 M 4 +728 343.859870 N 587.155205 E 72.370000 M 4 +739 343.736764 N 586.151832 E 72.380000 M 4 +748 343.630454 N 585.294709 E 72.390000 M 4 +757 343.525436 N 584.437244 E 72.400000 M 4 +765 343.422632 N 583.581834 E 72.410000 M 4 +776 343.303956 N 582.585482 E 72.420000 M 4 +784 343.207613 N 581.779051 E 72.430000 M 4 +793 343.106839 N 580.924497 E 72.440000 M 4 +802 343.004405 N 580.072512 E 72.450000 M 4 +812 342.886836 N 579.078044 E 72.460000 M 4 +821 342.789201 N 578.226059 E 72.470000 M 4 +830 342.692673 N 577.375102 E 72.480000 M 4 +838 342.595591 N 576.519863 E 72.490000 M 4 +849 342.480052 N 575.522826 E 72.500000 M 4 +857 342.389246 N 574.717765 E 72.510000 M 4 +866 342.292718 N 573.862526 E 72.520000 M 4 +875 342.199143 N 573.007287 E 72.530000 M 4 +885 342.092832 N 572.008880 E 72.540000 M 4 +894 342.003133 N 571.152614 E 72.550000 M 4 +903 341.914357 N 570.294464 E 72.560000 M 4 +911 341.827242 N 569.435800 E 72.570000 M 4 +922 341.726838 N 568.432598 E 72.580000 M 4 +930 341.650796 N 567.620686 E 72.590000 M 4 +939 341.568849 N 566.757056 E 72.600000 M 4 +948 341.486348 N 565.893940 E 72.610000 M 4 +958 341.390004 N 564.886628 E 72.620000 M 4 +967 341.307688 N 564.019573 E 72.630000 M 4 +976 341.227032 N 563.148578 E 72.640000 M 4 +985 341.146007 N 562.274673 E 72.650000 M 4 +996 341.048741 N 561.254860 E 72.660000 M 4 +1004 340.971592 N 560.427364 E 72.670000 M 4 +1013 340.882631 N 559.548321 E 72.680000 M 4 +1023 340.792563 N 558.665510 E 72.690000 M 4 +1033 340.688099 N 557.629599 E 72.700000 M 4 +1042 340.595631 N 556.738397 E 72.710000 M 4 +1052 340.500395 N 555.845996 E 72.720000 M 4 +1061 340.402575 N 554.950856 E 72.730000 M 4 +1072 340.285744 N 553.904841 E 72.740000 M 4 +1080 340.192538 N 553.054739 E 72.750000 M 4 +1090 340.087151 N 552.154461 E 72.760000 M 4 +1099 339.980102 N 551.253497 E 72.770000 M 4 +1110 339.853490 N 550.198920 E 72.780000 M 4 +1119 339.742935 N 549.291449 E 72.790000 M 4 +1129 339.629796 N 548.380210 E 72.800000 M 4 +1138 339.513150 N 547.467944 E 72.810000 M 4 +1149 339.377309 N 546.403091 E 72.820000 M 4 +1158 339.271737 N 545.537748 E 72.830000 M 4 +1168 339.155460 N 544.622742 E 72.840000 M 4 +1177 339.037707 N 543.705167 E 72.850000 M 4 +1188 338.897621 N 542.637231 E 72.860000 M 4 +1198 338.777469 N 541.721540 E 72.870000 M 4 +1207 338.658054 N 540.802938 E 72.880000 M 4 +1217 338.541962 N 539.884335 E 72.890000 M 4 +1228 338.411474 N 538.812290 E 72.900000 M 4 +1237 338.308670 N 537.941295 E 72.910000 M 4 +1246 338.199407 N 537.017042 E 72.920000 M 4 +1256 338.093835 N 536.091931 E 72.930000 M 4 +1267 337.977189 N 535.017146 E 72.940000 M 4 +1276 337.881769 N 534.103681 E 72.950000 M 4 +1286 337.786163 N 533.197237 E 72.960000 M 4 +1295 337.689820 N 532.296103 E 72.970000 M 4 +1306 337.577050 N 531.253170 E 72.980000 M 4 +1315 337.487350 N 530.417968 E 72.990000 M 4 +1324 337.393960 N 529.539267 E 73.000000 M 4 +1333 337.296693 N 528.666218 E 73.010000 M 4 +1343 337.180232 N 527.657365 E 73.020000 M 4 +1352 337.077429 N 526.801784 E 73.030000 M 4 +1361 336.972041 N 525.953566 E 73.040000 M 4 +1369 336.865178 N 525.110144 E 73.050000 M 4 +1379 336.739119 N 524.136055 E 73.060000 M 4 +1388 336.633731 N 523.357024 E 73.070000 M 4 +1396 336.523915 N 522.529186 E 73.080000 M 4 +1405 336.412067 N 521.703060 E 73.090000 M 4 +1415 336.281025 N 520.741815 E 73.100000 M 4 +1423 336.168440 N 519.919800 E 73.110000 M 4 +1432 336.053640 N 519.099839 E 73.120000 M 4 +1440 335.938101 N 518.281077 E 73.130000 M 4 +1450 335.802630 N 517.330108 E 73.140000 M 4 +1458 335.689675 N 516.570771 E 73.150000 M 4 +1466 335.576720 N 515.772902 E 73.160000 M 4 +1474 335.464504 N 514.989418 E 73.170000 M 4 +1484 335.333093 N 514.092736 E 73.180000 M 4 +1491 335.217370 N 513.339564 E 73.190000 M 4 +1499 335.093157 N 512.596496 E 73.200000 M 4 +1507 334.961192 N 511.864560 E 73.210000 M 4 +1516 334.795082 N 511.026275 E 73.220000 M 4 +1522 334.650936 N 510.364723 E 73.230000 M 4 +1530 334.487410 N 509.666353 E 73.240000 M 4 +1537 334.316502 N 508.980826 E 73.250000 M 4 +1545 334.112003 N 508.192033 E 73.260000 M 4 +1553 333.931681 N 507.525173 E 73.270000 M 4 +1560 333.746746 N 506.867047 E 73.280000 M 4 +1567 333.554797 N 506.216284 E 73.290000 M 4 +1575 333.325381 N 505.464825 E 73.300000 M 4 +1581 333.133617 N 504.876398 E 73.310000 M 4 +1588 332.934101 N 504.242248 E 73.320000 M 4 +1595 332.737538 N 503.612721 E 73.330000 M 4 +1602 332.514951 N 502.886607 E 73.340000 M 4 +1609 332.332784 N 502.269410 E 73.350000 M 4 +1615 332.159661 N 501.657864 E 73.360000 M 4 +1622 331.997058 N 501.050772 E 73.370000 M 4 +1629 331.820797 N 500.346749 E 73.380000 M 4 +1635 331.679419 N 499.798910 E 73.390000 M 4 +1641 331.537303 N 499.205689 E 73.400000 M 4 +1648 331.400540 N 498.619489 E 73.410000 M 4 +1655 331.248088 N 497.947149 E 73.420000 M 4 +1661 331.123136 N 497.382355 E 73.430000 M 4 +1666 331.002061 N 496.832804 E 73.440000 M 4 +1672 330.888184 N 496.298493 E 73.450000 M 4 +1678 330.764524 N 495.693455 E 73.460000 M 4 +1688 330.576082 N 494.735465 E 73.480000 M 4 +1694 330.488598 N 494.234206 E 73.490000 M 4 +1700 330.391147 N 493.650233 E 73.500000 M 4 +1705 330.311783 N 493.151201 E 73.510000 M 4 +1710 330.237957 N 492.652340 E 73.520000 M 4 +1715 330.170959 N 492.154849 E 73.530000 M 4 +1721 330.098794 N 491.575157 E 73.540000 M 4 +1731 329.995437 N 490.635833 E 73.560000 M 4 +1742 329.888019 N 489.570295 E 73.580000 M 4 +1752 329.808286 N 488.589013 E 73.600000 M 4 +1763 329.717849 N 487.534265 E 73.620000 M 4 +1772 329.633502 N 486.619943 E 73.640000 M 4 +1783 329.526454 N 485.583176 E 73.660000 M 4 +1793 329.428449 N 484.639057 E 73.680000 M 4 +1803 329.315310 N 483.626265 E 73.700000 M 4 +1812 329.212507 N 482.752359 E 73.720000 M 4 +1822 329.091431 N 481.763714 E 73.740000 M 4 +1832 328.988074 N 480.864635 E 73.760000 M 4 +1842 328.873089 N 479.906473 E 73.780000 M 4 +1850 328.770839 N 479.086683 E 73.800000 M 4 +1860 328.648657 N 478.158662 E 73.820000 M 4 +1868 328.550652 N 477.315068 E 73.840000 M 4 +1878 328.435852 N 476.418043 E 73.860000 M 4 +1886 328.331572 N 475.654939 E 73.880000 M 4 +1895 328.220463 N 474.774354 E 73.900000 M 4 +1903 328.112676 N 473.968779 E 73.920000 M 4 +1912 327.989940 N 473.098299 E 73.940000 M 4 +1920 327.882337 N 472.355573 E 73.960000 M 4 +1929 327.746866 N 471.487833 E 73.980000 M 4 +1937 327.621176 N 470.687566 E 74.000000 M 4 +1946 327.482198 N 469.820682 E 74.020000 M 4 +1954 327.357985 N 469.074360 E 74.040000 M 4 +1963 327.215684 N 468.196687 E 74.060000 M 4 +1972 327.086119 N 467.382892 E 74.080000 M 4 +1981 326.957660 N 466.503849 E 74.100000 M 4 +1988 326.857810 N 465.752047 E 74.120000 M 4 +1998 326.746701 N 464.878313 E 74.140000 M 4 +2006 326.648143 N 464.075991 E 74.160000 M 4 +2015 326.548662 N 463.211333 E 74.180000 M 4 +2022 326.463023 N 462.480082 E 74.200000 M 4 +2031 326.370555 N 461.641968 E 74.220000 M 4 +2039 326.284363 N 460.886056 E 74.240000 M 4 +2047 326.188942 N 460.089558 E 74.260000 M 4 +2054 326.113085 N 459.427321 E 74.280000 M 4 +2062 326.027262 N 458.675690 E 74.300000 M 4 +2068 325.945868 N 458.019962 E 74.320000 M 4 +2075 325.868535 N 457.367144 E 74.340000 M 4 +2083 325.776990 N 456.616884 E 74.370000 M 4 +2088 325.713499 N 456.094390 E 74.390000 M 4 +2095 325.629891 N 455.419652 E 74.420000 M 4 +2102 325.549420 N 454.751935 E 74.460000 M 4 +2109 325.467657 N 454.115558 E 74.500000 M 4 +2115 325.401398 N 453.546484 E 74.540000 M 4 +2121 325.324064 N 452.941617 E 74.580000 M 4 +2127 325.260389 N 452.397203 E 74.620000 M 4 +2133 325.185086 N 451.818709 E 74.660000 M 4 +2138 325.118458 N 451.303580 E 74.700000 M 4 +2144 325.046477 N 450.752658 E 74.740000 M 4 +2150 324.968590 N 450.132378 E 74.790000 M 4 +2156 324.906391 N 449.584197 E 74.830000 M 4 +2161 324.841424 N 449.067868 E 74.860000 M 4 +2167 324.772211 N 448.522769 E 74.890000 M 4 +2173 324.706506 N 447.933486 E 74.920000 M 4 +2180 324.619760 N 447.229293 E 74.950000 M 4 +2188 324.521939 N 446.459509 E 74.980000 M 4 +2196 324.433532 N 445.725175 E 75.010000 M 4 +2202 324.354538 N 445.151991 E 75.030000 M 4 +2207 324.279973 N 444.623503 E 75.050000 M 4 +2213 324.215006 N 444.098612 E 75.070000 M 4 +2218 324.136750 N 443.580571 E 75.090000 M 4 +2224 324.054618 N 443.029991 E 75.110000 M 4 +2229 323.978207 N 442.528562 E 75.130000 M 4 +2234 323.908072 N 442.032955 E 75.150000 M 4 +2239 323.831662 N 441.538204 E 75.170000 M 4 +2245 323.745285 N 440.994475 E 75.190000 M 4 +2250 323.664814 N 440.483455 E 75.210000 M 4 +2256 323.584158 N 439.957194 E 75.230000 M 4 +2261 323.497228 N 439.420829 E 75.250000 M 4 +2267 323.397377 N 438.828635 E 75.270000 M 4 +2273 323.303987 N 438.280111 E 75.290000 M 4 +2279 323.224070 N 437.729874 E 75.310000 M 4 +2284 323.137693 N 437.191111 E 75.330000 M 4 +2290 323.050024 N 436.618783 E 75.350000 M 4 +2296 322.972875 N 436.097830 E 75.370000 M 4 +2301 322.909199 N 435.584584 E 75.390000 M 4 +2306 322.834450 N 435.076304 E 75.410000 M 4 +2312 322.757301 N 434.526924 E 75.430000 M 4 +2317 322.691042 N 434.019843 E 75.450000 M 4 +2323 322.641948 N 433.507796 E 75.470000 M 4 +2328 322.576057 N 432.988213 E 75.490000 M 4 +2334 322.505184 N 432.415200 E 75.510000 M 4 +2339 322.445015 N 431.878835 E 75.530000 M 4 +2345 322.397028 N 431.335448 E 75.550000 M 4 +2351 322.335198 N 430.793431 E 75.570000 M 4 +2357 322.276875 N 430.208601 E 75.590000 M 4 +2362 322.220952 N 429.675490 E 75.610000 M 4 +2368 322.180901 N 429.153167 E 75.630000 M 4 +2373 322.130145 N 428.643689 E 75.650000 M 4 +2378 322.063517 N 428.106125 E 75.670000 M 4 +2386 321.937827 N 427.349186 E 75.700000 M 4 +2393 321.795158 N 426.707671 E 75.730000 M 4 +2400 321.570356 N 426.036358 E 75.760000 M 4 +2407 321.267852 N 425.470366 E 75.790000 M 4 +2414 320.834490 N 424.917218 E 75.820000 M 4 +2421 320.322688 N 424.475213 E 75.850000 M 4 +2428 319.740012 N 424.136474 E 75.880000 M 4 +2435 319.108057 N 423.819998 E 75.910000 M 4 +2443 318.415196 N 423.547020 E 75.940000 M 4 +2451 317.681545 N 423.384158 E 75.970000 M 4 +2457 317.082996 N 423.281578 E 75.990000 M 4 +2463 316.485555 N 423.237394 E 76.010000 M 4 +2470 315.810781 N 423.289113 E 76.030000 M 4 +2477 315.149295 N 423.366348 E 76.050000 M 4 +2485 314.418228 N 423.528354 E 76.070000 M 4 +2492 313.752313 N 423.752182 E 76.090000 M 4 +2500 313.075509 N 424.110272 E 76.110000 M 4 +2507 312.489142 N 424.474699 E 76.130000 M 4 +2514 311.918278 N 424.924410 E 76.150000 M 4 +2521 311.451510 N 425.368813 E 76.170000 M 4 +2528 311.022578 N 425.910829 E 76.190000 M 4 +2534 310.677070 N 426.397360 E 76.210000 M 4 +2540 310.360908 N 426.942973 E 76.230000 M 4 +2546 310.138137 N 427.436011 E 76.250000 M 4 +2552 309.975165 N 427.957135 E 76.270000 M 4 +2558 309.806471 N 428.539738 E 76.300000 M 4 +2590 312.454813 N 427.648879 E 77.810000 M 4 +2591 312.442077 N 427.651961 E 77.860000 M 4 diff --git a/samples/radar/malamira_南同大道/南同大道_001.head b/samples/radar/malamira_南同大道/南同大道_001.head new file mode 100644 index 0000000..9e32f00 --- /dev/null +++ b/samples/radar/malamira_南同大道/南同大道_001.head @@ -0,0 +1,34 @@ +DATE:2022-03-10 +START_TIME:10:48 +STOP_TIME: +UNITS:m +MODE:距离模式 +ANTENNAS:200 MHz shielded +FREQUENCY:5351.611816 +STACKS:2 +LAST_TRACE:41456 +POSITIVE_DIRECTION:1 +SAMPLES:516 +TIME_INTERVAL:0.000000 +TIMEWINDOW:96.419553 +DEPTH: +ZERO_POSITION: +DIELECTRIC: +SOIL_TYPE: +BITS:16 +MARK: +DISTANCE_INTERVAL:0.097424 +START_POSITION:0.000000 +STOP_POSITION:252.327351 +WHEEL_GPS: +WHEEL_CALIBRATION:84.4270000000 +SCAN_SECOND: +NUMBER_OF_CH:16 +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 +RTK_X_OFFSET: +RTK_Y_OFFSET:0.350 +RTK_Z_OFFSET: +GAIN: +FILTER: +SMOOTH: +ENDIAN_TYPE:1 diff --git a/samples/radar/malamira_南同大道/南同大道_002.cor b/samples/radar/malamira_南同大道/南同大道_002.cor new file mode 100644 index 0000000..1546199 --- /dev/null +++ b/samples/radar/malamira_南同大道/南同大道_002.cor @@ -0,0 +1,346 @@ +VERSION:1 +1 320.505224 N 474.820250 E 80.150000 M 4 +6 320.535862 N 475.242047 E 80.420000 M 4 +12 320.603598 N 475.800162 E 80.470000 M 4 +19 320.687944 N 476.438080 E 80.510000 M 4 +25 320.765647 N 477.048427 E 80.540000 M 4 +33 320.863467 N 477.741146 E 80.570000 M 4 +39 320.958334 N 478.306624 E 80.590000 M 4 +45 321.046372 N 478.856176 E 80.610000 M 4 +51 321.162649 N 479.497006 E 80.630000 M 4 +58 321.279110 N 480.146398 E 80.650000 M 4 +67 321.422149 N 480.924916 E 80.670000 M 4 +75 321.550607 N 481.696240 E 80.690000 M 4 +84 321.703613 N 482.604568 E 80.710000 M 4 +94 321.861048 N 483.510840 E 80.730000 M 4 +100 321.957207 N 484.071523 E 80.740000 M 4 +105 322.049859 N 484.582029 E 80.750000 M 4 +111 322.130145 N 485.104523 E 80.760000 M 4 +117 322.209693 N 485.646197 E 80.770000 M 4 +124 322.305114 N 486.304324 E 80.780000 M 4 +130 322.389645 N 486.892921 E 80.790000 M 4 +136 322.477684 N 487.503610 E 80.800000 M 4 +143 322.568306 N 488.136220 E 80.810000 M 4 +151 322.677015 N 488.905661 E 80.820000 M 4 +158 322.785540 N 489.594613 E 80.830000 M 4 +166 322.880223 N 490.297436 E 80.840000 M 4 +173 322.975459 N 491.012932 E 80.850000 M 4 +182 323.086752 N 491.842826 E 80.860000 M 4 +190 323.181435 N 492.548560 E 80.870000 M 4 +197 323.275010 N 493.251384 E 80.880000 M 4 +204 323.369692 N 493.952323 E 80.890000 M 4 +213 323.476002 N 494.771085 E 80.900000 M 4 +220 323.581021 N 495.475793 E 80.910000 M 4 +228 323.669797 N 496.172965 E 80.920000 M 4 +235 323.761342 N 496.868424 E 80.930000 M 4 +244 323.870236 N 497.677596 E 80.940000 M 4 +251 323.962335 N 498.369117 E 80.950000 M 4 +258 324.052587 N 499.057555 E 80.960000 M 4 +266 324.142287 N 499.745650 E 80.970000 M 4 +274 324.246013 N 500.545917 E 80.980000 M 4 +281 324.350662 N 501.228704 E 80.990000 M 4 +288 324.439254 N 501.898989 E 81.000000 M 4 +295 324.523785 N 502.565678 E 81.010000 M 4 +304 324.624005 N 503.349333 E 81.020000 M 4 +311 324.710566 N 504.022872 E 81.030000 M 4 +318 324.797128 N 504.702234 E 81.040000 M 4 +325 324.883505 N 505.386904 E 81.050000 M 4 +334 324.985755 N 506.198816 E 81.060000 M 4 +341 325.086159 N 506.905921 E 81.070000 M 4 +349 325.174012 N 507.620047 E 81.080000 M 4 +356 325.265188 N 508.337941 E 81.090000 M 4 +365 325.372052 N 509.173828 E 81.100000 M 4 +372 325.459905 N 509.880248 E 81.110000 M 4 +380 325.546651 N 510.581188 E 81.120000 M 4 +387 325.632844 N 511.279901 E 81.130000 M 4 +396 325.734909 N 512.095238 E 81.140000 M 4 +403 325.833467 N 512.793951 E 81.150000 M 4 +411 325.920029 N 513.489239 E 81.160000 M 4 +418 326.006406 N 514.186583 E 81.170000 M 4 +427 326.106441 N 515.003118 E 81.180000 M 4 +434 326.191157 N 515.710223 E 81.190000 M 4 +442 326.276057 N 516.424863 E 81.200000 M 4 +449 326.362619 N 517.147209 E 81.210000 M 4 +458 326.466714 N 517.997653 E 81.220000 M 4 +466 326.566011 N 518.736953 E 81.230000 M 4 +474 326.655341 N 519.478480 E 81.240000 M 4 +482 326.744671 N 520.227884 E 81.250000 M 4 +491 326.853011 N 521.110866 E 81.260000 M 4 +499 326.948432 N 521.872601 E 81.270000 M 4 +507 327.047360 N 522.642213 E 81.280000 M 4 +515 327.146656 N 523.416963 E 81.290000 M 4 +525 327.261457 N 524.324434 E 81.300000 M 4 +533 327.371458 N 525.111514 E 81.310000 M 4 +541 327.474077 N 525.895340 E 81.320000 M 4 +550 327.579095 N 526.679851 E 81.330000 M 4 +559 327.701647 N 527.599139 E 81.340000 M 4 +568 327.804820 N 528.389130 E 81.350000 M 4 +576 327.908915 N 529.178779 E 81.360000 M 4 +584 328.013010 N 529.973394 E 81.370000 M 4 +594 328.134455 N 530.901416 E 81.380000 M 4 +603 328.246856 N 531.705964 E 81.390000 M 4 +611 328.351320 N 532.509142 E 81.400000 M 4 +620 328.457077 N 533.315060 E 81.410000 M 4 +630 328.580183 N 534.260035 E 81.420000 M 4 +638 328.682801 N 535.075372 E 81.430000 M 4 +647 328.783205 N 535.897730 E 81.440000 M 4 +656 328.878811 N 536.722828 E 81.450000 M 4 +666 328.992504 N 537.694006 E 81.460000 M 4 +675 329.098075 N 538.537942 E 81.470000 M 4 +684 329.194419 N 539.379309 E 81.480000 M 4 +693 329.286518 N 540.224958 E 81.490000 M 4 +703 329.386183 N 541.215316 E 81.500000 M 4 +712 329.469607 N 542.063190 E 81.510000 M 4 +721 329.559307 N 542.915518 E 81.520000 M 4 +730 329.652882 N 543.771271 E 81.530000 M 4 +740 329.762329 N 544.772075 E 81.540000 M 4 +750 329.870670 N 545.638788 E 81.550000 M 4 +759 329.958339 N 546.506185 E 81.560000 M 4 +768 330.046561 N 547.367418 E 81.570000 M 4 +778 330.154717 N 548.373189 E 81.580000 M 4 +787 330.254937 N 549.239388 E 81.590000 M 4 +796 330.359955 N 550.101134 E 81.600000 M 4 +806 330.464235 N 550.967162 E 81.610000 M 4 +816 330.574975 N 551.984406 E 81.620000 M 4 +826 330.684607 N 552.861052 E 81.630000 M 4 +835 330.773937 N 553.738211 E 81.640000 M 4 +844 330.866589 N 554.618624 E 81.650000 M 4 +855 330.979175 N 555.647343 E 81.660000 M 4 +864 331.074965 N 556.531523 E 81.670000 M 4 +874 331.168171 N 557.418615 E 81.680000 M 4 +883 331.256209 N 558.309475 E 81.690000 M 4 +894 331.351814 N 559.353606 E 81.700000 M 4 +903 331.450188 N 560.253371 E 81.710000 M 4 +913 331.524568 N 561.154505 E 81.720000 M 4 +922 331.595811 N 562.059065 E 81.730000 M 4 +933 331.676651 N 563.119808 E 81.740000 M 4 +943 331.742356 N 564.034300 E 81.750000 M 4 +953 331.806216 N 564.952389 E 81.760000 M 4 +962 331.870999 N 565.872876 E 81.770000 M 4 +974 331.951654 N 566.949374 E 81.780000 M 4 +983 332.040800 N 567.874826 E 81.790000 M 4 +993 332.114442 N 568.802848 E 81.800000 M 4 +1003 332.190852 N 569.730527 E 81.810000 M 4 +1014 332.283505 N 570.815930 E 81.820000 M 4 +1024 332.366559 N 571.747205 E 81.830000 M 4 +1034 332.453859 N 572.679337 E 81.840000 M 4 +1044 332.544481 N 573.613181 E 81.850000 M 4 +1055 332.654483 N 574.700982 E 81.860000 M 4 +1065 332.766330 N 575.633285 E 81.870000 M 4 +1075 332.865811 N 576.568327 E 81.880000 M 4 +1085 332.967507 N 577.504227 E 81.890000 M 4 +1096 333.090059 N 578.596480 E 81.900000 M 4 +1106 333.200061 N 579.533578 E 81.910000 M 4 +1116 333.310247 N 580.471018 E 81.920000 M 4 +1126 333.420986 N 581.408630 E 81.930000 M 4 +1137 333.547968 N 582.505507 E 81.940000 M 4 +1147 333.672550 N 583.444146 E 81.950000 M 4 +1157 333.784767 N 584.384841 E 81.960000 M 4 +1167 333.895875 N 585.328617 E 81.970000 M 4 +1179 334.022119 N 586.432345 E 81.980000 M 4 +1189 334.127875 N 587.382115 E 81.990000 M 4 +1199 334.233816 N 588.333085 E 82.000000 M 4 +1209 334.340126 N 589.282684 E 82.010000 M 4 +1220 334.463417 N 590.387439 E 82.020000 M 4 +1230 334.579693 N 591.325222 E 82.030000 M 4 +1240 334.682312 N 592.262320 E 82.040000 M 4 +1250 334.785485 N 593.196335 E 82.050000 M 4 +1261 334.903792 N 594.284821 E 82.060000 M 4 +1271 335.003089 N 595.216439 E 82.070000 M 4 +1281 335.100539 N 596.146173 E 82.080000 M 4 +1291 335.197437 N 597.074366 E 82.090000 M 4 +1302 335.308730 N 598.153604 E 82.100000 M 4 +1312 335.419101 N 599.076830 E 82.110000 M 4 +1321 335.513968 N 599.998515 E 82.120000 M 4 +1331 335.609942 N 600.917289 E 82.130000 M 4 +1342 335.719575 N 601.982484 E 82.140000 M 4 +1352 335.811858 N 602.886873 E 82.150000 M 4 +1361 335.905064 N 603.788179 E 82.160000 M 4 +1371 336.000854 N 604.684176 E 82.170000 M 4 +1382 336.115469 N 605.727280 E 82.180000 M 4 +1391 336.226209 N 606.619167 E 82.190000 M 4 +1401 336.325506 N 607.513965 E 82.200000 M 4 +1410 336.424249 N 608.409620 E 82.210000 M 4 +1421 336.541633 N 609.457861 E 82.220000 M 4 +1431 336.643698 N 610.360195 E 82.230000 M 4 +1440 336.744656 N 611.269378 E 82.240000 M 4 +1450 336.847828 N 612.181473 E 82.250000 M 4 +1461 336.970011 N 613.253005 E 82.260000 M 4 +1471 337.088503 N 614.173320 E 82.270000 M 4 +1480 337.195367 N 615.103054 E 82.280000 M 4 +1490 337.302230 N 616.038439 E 82.290000 M 4 +1502 337.429581 N 617.139427 E 82.300000 M 4 +1512 337.537552 N 618.090225 E 82.310000 M 4 +1522 337.645155 N 619.045133 E 82.320000 M 4 +1532 337.752941 N 620.007063 E 82.330000 M 4 +1544 337.879000 N 621.136649 E 82.340000 M 4 +1554 338.002290 N 622.106114 E 82.350000 M 4 +1565 338.111000 N 623.086710 E 82.360000 M 4 +1575 338.220078 N 624.074157 E 82.370000 M 4 +1587 338.345584 N 625.233884 E 82.380000 M 4 +1598 338.454478 N 626.231948 E 82.390000 M 4 +1608 338.564110 N 627.234465 E 82.400000 M 4 +1619 338.673373 N 628.244345 E 82.410000 M 4 +1631 338.798878 N 629.427363 E 82.420000 M 4 +1642 338.917739 N 630.443580 E 82.430000 M 4 +1653 339.024787 N 631.463564 E 82.440000 M 4 +1664 339.130175 N 632.489542 E 82.450000 M 4 +1676 339.255126 N 633.691569 E 82.460000 M 4 +1687 339.364574 N 634.724911 E 82.470000 M 4 +1698 339.476421 N 635.763049 E 82.480000 M 4 +1709 339.595651 N 636.806152 E 82.490000 M 4 +1722 339.734998 N 638.029072 E 82.500000 M 4 +1733 339.868809 N 639.080225 E 82.510000 M 4 +1744 339.987485 N 640.136001 E 82.520000 M 4 +1755 340.113359 N 641.191778 E 82.530000 M 4 +1768 340.273194 N 642.431480 E 82.540000 M 4 +1780 340.414940 N 643.495477 E 82.550000 M 4 +1791 340.552996 N 644.570776 E 82.560000 M 4 +1802 340.678501 N 645.645390 E 82.570000 M 4 +1815 340.825047 N 646.908041 E 82.580000 M 4 +1827 340.962918 N 647.991731 E 82.590000 M 4 +1838 341.101896 N 649.079875 E 82.600000 M 4 +1850 341.245673 N 650.168703 E 82.610000 M 4 +1863 341.409937 N 651.444882 E 82.620000 M 4 +1875 341.548731 N 652.543129 E 82.630000 M 4 +1887 341.682357 N 653.648569 E 82.640000 M 4 +1898 341.816537 N 654.757092 E 82.650000 M 4 +1912 341.975264 N 656.051938 E 82.660000 M 4 +1924 342.124762 N 657.156693 E 82.670000 M 4 +1935 342.264848 N 658.273093 E 82.680000 M 4 +1947 342.404934 N 659.390349 E 82.690000 M 4 +1961 342.564768 N 660.697183 E 82.700000 M 4 +1973 342.699133 N 661.818378 E 82.710000 M 4 +1985 342.829436 N 662.944711 E 82.720000 M 4 +1996 342.963616 N 664.069502 E 82.730000 M 4 +2010 343.123266 N 665.383529 E 82.740000 M 4 +2022 343.274979 N 666.500443 E 82.750000 M 4 +2034 343.408790 N 667.620781 E 82.760000 M 4 +2046 343.540016 N 668.741463 E 82.770000 M 4 +2059 343.692099 N 670.047098 E 82.780000 M 4 +2071 343.824802 N 671.162642 E 82.790000 M 4 +2083 343.959720 N 672.275788 E 82.800000 M 4 +2095 344.097222 N 673.387564 E 82.810000 M 4 +2108 344.255580 N 674.682410 E 82.820000 M 4 +2120 344.410062 N 675.784082 E 82.830000 M 4 +2132 344.544242 N 676.887125 E 82.840000 M 4 +2143 344.678421 N 677.984687 E 82.850000 M 4 +2157 344.832165 N 679.257099 E 82.860000 M 4 +2168 344.960808 N 680.340961 E 82.870000 M 4 +2179 345.089450 N 681.418144 E 82.880000 M 4 +2191 345.216616 N 682.489162 E 82.890000 M 4 +2204 345.365931 N 683.729721 E 82.900000 M 4 +2215 345.514691 N 684.783099 E 82.910000 M 4 +2226 345.639089 N 685.833910 E 82.920000 M 4 +2237 345.760533 N 686.878212 E 82.930000 M 4 +2250 345.903203 N 688.088630 E 82.940000 M 4 +2260 346.022802 N 689.118547 E 82.950000 M 4 +2271 346.141847 N 690.143669 E 82.960000 M 4 +2282 346.259047 N 691.161941 E 82.970000 M 4 +2294 346.394887 N 692.341020 E 82.980000 M 4 +2305 346.530728 N 693.339427 E 82.990000 M 4 +2315 346.644421 N 694.337320 E 83.000000 M 4 +2326 346.757006 N 695.328191 E 83.010000 M 4 +2338 346.885280 N 696.475245 E 83.020000 M 4 +2348 346.996573 N 697.451732 E 83.030000 M 4 +2359 347.106206 N 698.422053 E 83.040000 M 4 +2369 347.215285 N 699.385181 E 83.050000 M 4 +2380 347.340420 N 700.501067 E 83.060000 M 4 +2390 347.469063 N 701.444844 E 83.070000 M 4 +2400 347.575927 N 702.386737 E 83.080000 M 4 +2410 347.680022 N 703.322294 E 83.090000 M 4 +2422 347.801098 N 704.405128 E 83.100000 M 4 +2431 347.903532 N 705.326128 E 83.110000 M 4 +2441 348.005413 N 706.240107 E 83.120000 M 4 +2450 348.107293 N 707.148948 E 83.130000 M 4 +2461 348.223385 N 708.200101 E 83.140000 M 4 +2471 348.342800 N 709.089933 E 83.150000 M 4 +2480 348.438590 N 709.975655 E 83.160000 M 4 +2489 348.531796 N 710.856410 E 83.170000 M 4 +2500 348.638475 N 711.875710 E 83.180000 M 4 +2509 348.729651 N 712.743108 E 83.190000 M 4 +2518 348.823595 N 713.603484 E 83.200000 M 4 +2527 348.918462 N 714.458209 E 83.210000 M 4 +2538 349.029940 N 715.446512 E 83.220000 M 4 +2547 349.154338 N 716.275720 E 83.230000 M 4 +2555 349.249020 N 717.108696 E 83.240000 M 4 +2564 349.344072 N 717.936363 E 83.250000 M 4 +2574 349.456104 N 718.892641 E 83.260000 M 4 +2583 349.550786 N 719.701642 E 83.270000 M 4 +2591 349.649344 N 720.508416 E 83.280000 M 4 +2600 349.750118 N 721.307141 E 83.290000 M 4 +2609 349.865471 N 722.231053 E 83.300000 M 4 +2618 349.995406 N 723.006145 E 83.310000 M 4 +2626 350.094334 N 723.783464 E 83.320000 M 4 +2634 350.196030 N 724.555131 E 83.330000 M 4 +2643 350.318028 N 725.442223 E 83.340000 M 4 +2651 350.418247 N 726.198306 E 83.350000 M 4 +2659 350.516437 N 726.948567 E 83.360000 M 4 +2667 350.611857 N 727.696601 E 83.370000 M 4 +2676 350.725919 N 728.571020 E 83.380000 M 4 +2684 350.851609 N 729.309635 E 83.390000 M 4 +2692 350.944815 N 730.064349 E 83.400000 M 4 +2700 351.034145 N 730.824371 E 83.410000 M 4 +2709 351.142116 N 731.712319 E 83.420000 M 4 +2718 351.241044 N 732.477821 E 83.430000 M 4 +2726 351.339233 N 733.241268 E 83.440000 M 4 +2734 351.435946 N 733.997180 E 83.450000 M 4 +2743 351.546501 N 734.872284 E 83.460000 M 4 +2750 351.671452 N 735.596001 E 83.470000 M 4 +2758 351.772041 N 736.335301 E 83.480000 M 4 +2766 351.871153 N 737.071176 E 83.490000 M 4 +2775 351.982631 N 737.926758 E 83.500000 M 4 +2783 352.074176 N 738.656125 E 83.510000 M 4 +2790 352.167567 N 739.381383 E 83.520000 M 4 +2798 352.262434 N 740.105271 E 83.530000 M 4 +2807 352.371881 N 740.942870 E 83.540000 M 4 +2814 352.497202 N 741.636960 E 83.550000 M 4 +2822 352.589854 N 742.346291 E 83.560000 M 4 +2829 352.680845 N 743.051854 E 83.570000 M 4 +2838 352.784941 N 743.869589 E 83.580000 M 4 +2845 352.872979 N 744.567275 E 83.590000 M 4 +2853 352.961571 N 745.261707 E 83.600000 M 4 +2860 353.049240 N 745.951857 E 83.610000 M 4 +2868 353.146137 N 746.750069 E 83.620000 M 4 +2875 353.261675 N 747.414189 E 83.630000 M 4 +2882 353.336609 N 748.094578 E 83.640000 M 4 +2890 353.408405 N 748.769830 E 83.650000 M 4 +2898 353.491091 N 749.553656 E 83.660000 M 4 +2905 353.561780 N 750.221373 E 83.670000 M 4 +2912 353.631731 N 750.885835 E 83.680000 M 4 +2919 353.697621 N 751.544989 E 83.690000 M 4 +2927 353.771263 N 752.310663 E 83.700000 M 4 +2934 353.862992 N 752.946868 E 83.710000 M 4 +2940 353.922792 N 753.595576 E 83.720000 M 4 +2947 353.981484 N 754.240858 E 83.730000 M 4 +2955 354.049035 N 754.990948 E 83.740000 M 4 +2962 354.105328 N 755.629551 E 83.750000 M 4 +2968 354.159221 N 756.263873 E 83.760000 M 4 +2975 354.213484 N 756.895113 E 83.770000 M 4 +2983 354.279189 N 757.627392 E 83.780000 M 4 +2989 354.359291 N 758.235683 E 83.790000 M 4 +2996 354.417060 N 758.851510 E 83.800000 M 4 +3002 354.475752 N 759.464083 E 83.810000 M 4 +3009 354.544780 N 760.170845 E 83.820000 M 4 +3016 354.603103 N 760.768177 E 83.830000 M 4 +3022 354.664194 N 761.359685 E 83.840000 M 4 +3028 354.725470 N 761.942974 E 83.850000 M 4 +3035 354.798559 N 762.611718 E 83.860000 M 4 +3041 354.879768 N 763.160585 E 83.870000 M 4 +3047 354.943812 N 763.716987 E 83.880000 M 4 +3053 355.009518 N 764.264655 E 83.890000 M 4 +3059 355.089989 N 764.890585 E 83.900000 M 4 +3065 355.160493 N 765.416847 E 83.910000 M 4 +3070 355.232289 N 765.931634 E 83.920000 M 4 +3075 355.303163 N 766.433064 E 83.930000 M 4 +3081 355.383634 N 767.001625 E 83.940000 M 4 +3091 355.532763 N 767.918857 E 83.960000 M 4 +3101 355.665651 N 768.860579 E 83.980000 M 4 +3110 355.769193 N 769.663414 E 84.000000 M 4 +3118 355.865905 N 770.454776 E 84.020000 M 4 +3125 355.965387 N 771.089611 E 84.040000 M 4 +3131 356.036260 N 771.704068 E 84.060000 M 4 +3139 356.114885 N 772.402268 E 84.090000 M 4 +3144 356.182990 N 772.921850 E 84.120000 M 4 +3154 356.120976 N 773.145336 E 85.080000 M 4 +3155 356.108241 N 773.148076 E 85.140000 M 4 diff --git a/samples/radar/malamira_南同大道/南同大道_002.head b/samples/radar/malamira_南同大道/南同大道_002.head new file mode 100644 index 0000000..33086c5 --- /dev/null +++ b/samples/radar/malamira_南同大道/南同大道_002.head @@ -0,0 +1,34 @@ +DATE:2022-03-10 +START_TIME:10:50 +STOP_TIME: +UNITS:m +MODE:距离模式 +ANTENNAS:200 MHz shielded +FREQUENCY:5351.611816 +STACKS:2 +LAST_TRACE:50480 +POSITIVE_DIRECTION:1 +SAMPLES:516 +TIME_INTERVAL:0.000000 +TIMEWINDOW:96.419553 +DEPTH: +ZERO_POSITION: +DIELECTRIC: +SOIL_TYPE: +BITS:16 +MARK: +DISTANCE_INTERVAL:0.095571 +START_POSITION:0.000000 +STOP_POSITION:301.431752 +WHEEL_GPS: +WHEEL_CALIBRATION:84.4270000000 +SCAN_SECOND: +NUMBER_OF_CH:16 +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 +RTK_X_OFFSET: +RTK_Y_OFFSET:0.350 +RTK_Z_OFFSET: +GAIN: +FILTER: +SMOOTH: +ENDIAN_TYPE:1 diff --git a/samples/radar/malamira_南同大道/南同大道_003.cor b/samples/radar/malamira_南同大道/南同大道_003.cor new file mode 100644 index 0000000..4277048 --- /dev/null +++ b/samples/radar/malamira_南同大道/南同大道_003.cor @@ -0,0 +1,245 @@ +VERSION:1 +1 351.615898 N 669.138257 E 91.260000 M 4 +7 351.568280 N 668.631689 E 91.460000 M 4 +13 351.512172 N 668.086077 E 91.500000 M 4 +18 351.454772 N 667.544745 E 91.530000 M 4 +25 351.381868 N 666.861787 E 91.560000 M 4 +31 351.321515 N 666.310351 E 91.580000 M 4 +37 351.263561 N 665.759087 E 91.600000 M 4 +44 351.196379 N 665.117744 E 91.620000 M 4 +50 351.134365 N 664.493012 E 91.640000 M 4 +59 351.057400 N 663.720659 E 91.660000 M 4 +67 350.976191 N 662.934436 E 91.680000 M 4 +77 350.879663 N 661.999221 E 91.700000 M 4 +87 350.791440 N 661.079249 E 91.720000 M 4 +92 350.741238 N 660.577648 E 91.730000 M 4 +98 350.678486 N 659.964733 E 91.740000 M 4 +104 350.620717 N 659.416551 E 91.750000 M 4 +110 350.561655 N 658.846106 E 91.760000 M 4 +116 350.500564 N 658.252542 E 91.770000 M 4 +124 350.427845 N 657.528655 E 91.780000 M 4 +131 350.367123 N 656.904608 E 91.790000 M 4 +138 350.299387 N 656.236378 E 91.800000 M 4 +145 350.229067 N 655.547254 E 91.810000 M 4 +154 350.145643 N 654.732603 E 91.820000 M 4 +161 350.073293 N 654.041253 E 91.830000 M 4 +168 350.004450 N 653.356411 E 91.840000 M 4 +175 349.935238 N 652.672083 E 91.850000 M 4 +184 349.852552 N 651.873015 E 91.860000 M 4 +191 349.789799 N 651.210094 E 91.870000 M 4 +198 349.719110 N 650.521827 E 91.880000 M 4 +205 349.647868 N 649.833731 E 91.890000 M 4 +214 349.565367 N 649.029355 E 91.900000 M 4 +221 349.494124 N 648.340232 E 91.910000 M 4 +228 349.421036 N 647.650252 E 91.920000 M 4 +236 349.348501 N 646.959588 E 91.930000 M 4 +244 349.265447 N 646.152642 E 91.940000 M 4 +251 349.205278 N 645.485611 E 91.950000 M 4 +258 349.134774 N 644.796316 E 91.960000 M 4 +266 349.065192 N 644.112502 E 91.970000 M 4 +274 348.985460 N 643.327820 E 91.980000 M 4 +281 348.916801 N 642.662158 E 91.990000 M 4 +288 348.847035 N 641.989475 E 92.000000 M 4 +295 348.777638 N 641.312854 E 92.010000 M 4 +304 348.700120 N 640.513615 E 92.020000 M 4 +311 348.635522 N 639.845042 E 92.030000 M 4 +318 348.562618 N 639.138793 E 92.040000 M 4 +326 348.483808 N 638.417988 E 92.050000 M 4 +335 348.386727 N 637.561551 E 92.060000 M 4 +343 348.304041 N 636.811461 E 92.070000 M 4 +351 348.218771 N 636.048185 E 92.080000 M 4 +359 348.131841 N 635.271723 E 92.090000 M 4 +369 348.031067 N 634.354833 E 92.100000 M 4 +377 347.944690 N 633.583337 E 92.110000 M 4 +385 347.851854 N 632.776049 E 92.120000 M 4 +394 347.756064 N 631.955061 E 92.130000 M 4 +404 347.644217 N 630.983884 E 92.140000 M 4 +413 347.549349 N 630.136523 E 92.150000 M 4 +422 347.451345 N 629.282996 E 92.160000 M 4 +431 347.347619 N 628.415085 E 92.170000 M 4 +442 347.218238 N 627.391333 E 92.180000 M 4 +451 347.107498 N 626.525990 E 92.190000 M 4 +461 346.992698 N 625.623656 E 92.200000 M 4 +471 346.879005 N 624.711219 E 92.210000 M 4 +482 346.745379 N 623.633693 E 92.220000 M 4 +492 346.627072 N 622.701390 E 92.230000 M 4 +502 346.503412 N 621.759155 E 92.240000 M 4 +512 346.374770 N 620.805103 E 92.250000 M 4 +524 346.223056 N 619.677229 E 92.260000 M 4 +534 346.101243 N 618.721978 E 92.270000 M 4 +545 345.973338 N 617.732648 E 92.280000 M 4 +555 345.841927 N 616.731501 E 92.290000 M 4 +568 345.683754 N 615.547627 E 92.300000 M 4 +579 345.546067 N 614.521820 E 92.310000 M 4 +590 345.407643 N 613.482655 E 92.320000 M 4 +601 345.268110 N 612.431503 E 92.330000 M 4 +614 345.104400 N 611.189060 E 92.340000 M 4 +625 344.970405 N 610.135510 E 92.350000 M 4 +637 344.830688 N 609.043428 E 92.360000 M 4 +648 344.689311 N 607.938502 E 92.370000 M 4 +662 344.523201 N 606.635778 E 92.380000 M 4 +674 344.382377 N 605.511329 E 92.390000 M 4 +686 344.244321 N 604.385682 E 92.400000 M 4 +698 344.108296 N 603.259863 E 92.410000 M 4 +712 343.951230 N 601.950118 E 92.420000 M 4 +723 343.827386 N 600.852384 E 92.430000 M 4 +735 343.695237 N 599.730675 E 92.440000 M 4 +747 343.565487 N 598.610850 E 92.450000 M 4 +761 343.415803 N 597.307613 E 92.460000 M 4 +773 343.287714 N 596.192240 E 92.470000 M 4 +784 343.162394 N 595.079950 E 92.480000 M 4 +796 343.035228 N 593.965434 E 92.490000 M 4 +810 342.888682 N 592.670416 E 92.500000 M 4 +821 342.773144 N 591.588781 E 92.510000 M 4 +833 342.648377 N 590.484197 E 92.520000 M 4 +845 342.522318 N 589.383039 E 92.530000 M 4 +858 342.373927 N 588.099324 E 92.540000 M 4 +870 342.245838 N 587.001933 E 92.550000 M 4 +881 342.113135 N 585.910193 E 92.560000 M 4 +893 341.979509 N 584.822735 E 92.570000 M 4 +906 341.822074 N 583.561969 E 92.580000 M 4 +917 341.694354 N 582.512015 E 92.590000 M 4 +929 341.558513 N 581.440141 E 92.600000 M 4 +940 341.421196 N 580.369636 E 92.610000 M 4 +953 341.258224 N 579.126338 E 92.620000 M 4 +964 341.119430 N 578.062512 E 92.630000 M 4 +976 340.975837 N 577.000399 E 92.640000 M 4 +987 340.831691 N 575.937430 E 92.650000 M 4 +1000 340.663182 N 574.697899 E 92.660000 M 4 +1011 340.529556 N 573.660961 E 92.670000 M 4 +1022 340.393715 N 572.596964 E 92.680000 M 4 +1034 340.262120 N 571.527830 E 92.690000 M 4 +1047 340.111883 N 570.275112 E 92.700000 M 4 +1058 339.984347 N 569.196730 E 92.710000 M 4 +1070 339.858842 N 568.112012 E 92.720000 M 4 +1081 339.734260 N 567.020615 E 92.730000 M 4 +1095 339.592513 N 565.738613 E 92.740000 M 4 +1106 339.474760 N 564.656977 E 92.750000 M 4 +1118 339.354423 N 563.542975 E 92.760000 M 4 +1130 339.231871 N 562.424006 E 92.770000 M 4 +1144 339.090862 N 561.112720 E 92.780000 M 4 +1156 338.968679 N 559.982105 E 92.790000 M 4 +1168 338.849080 N 558.843100 E 92.800000 M 4 +1180 338.731512 N 557.701697 E 92.810000 M 4 +1194 338.594010 N 556.366093 E 92.820000 M 4 +1206 338.471458 N 555.247637 E 92.830000 M 4 +1218 338.346691 N 554.102467 E 92.840000 M 4 +1230 338.217679 N 552.954556 E 92.850000 M 4 +1244 338.065597 N 551.614499 E 92.860000 M 4 +1256 337.929571 N 550.468472 E 92.870000 M 4 +1268 337.793730 N 549.321932 E 92.880000 M 4 +1280 337.653275 N 548.178474 E 92.890000 M 4 +1294 337.486797 N 546.847836 E 92.900000 M 4 +1306 337.348187 N 545.743937 E 92.910000 M 4 +1318 337.200350 N 544.610925 E 92.920000 M 4 +1330 337.054358 N 543.482880 E 92.930000 M 4 +1344 336.887879 N 542.173649 E 92.940000 M 4 +1356 336.748532 N 541.058448 E 92.950000 M 4 +1367 336.606416 N 539.950781 E 92.960000 M 4 +1379 336.465961 N 538.841745 E 92.970000 M 4 +1393 336.301328 N 537.547927 E 92.980000 M 4 +1404 336.174531 N 536.475196 E 92.990000 M 4 +1416 336.045150 N 535.382429 E 93.000000 M 4 +1427 335.917430 N 534.298738 E 93.010000 M 4 +1441 335.760364 N 533.041226 E 93.020000 M 4 +1452 335.608281 N 531.959932 E 93.030000 M 4 +1463 335.463212 N 530.883434 E 93.040000 M 4 +1475 335.319251 N 529.814985 E 93.050000 M 4 +1488 335.157386 N 528.576653 E 93.060000 M 4 +1499 335.015824 N 527.549647 E 93.070000 M 4 +1510 334.864110 N 526.497638 E 93.080000 M 4 +1521 334.712028 N 525.447684 E 93.090000 M 4 +1534 334.540381 N 524.228875 E 93.100000 M 4 +1545 334.395681 N 523.187483 E 93.110000 M 4 +1556 334.251350 N 522.150202 E 93.120000 M 4 +1567 334.106835 N 521.117716 E 93.130000 M 4 +1580 333.937403 N 519.920827 E 93.140000 M 4 +1590 333.797317 N 518.929100 E 93.150000 M 4 +1601 333.652617 N 517.909458 E 93.160000 M 4 +1612 333.512347 N 516.893926 E 93.170000 M 4 +1624 333.353435 N 515.716559 E 93.180000 M 4 +1635 333.218148 N 514.710104 E 93.190000 M 4 +1645 333.082677 N 513.707930 E 93.200000 M 4 +1656 332.948312 N 512.708667 E 93.210000 M 4 +1668 332.794753 N 511.546885 E 93.220000 M 4 +1679 332.669248 N 510.583414 E 93.230000 M 4 +1689 332.539129 N 509.589289 E 93.240000 M 4 +1700 332.407718 N 508.594650 E 93.250000 M 4 +1712 332.258404 N 507.433552 E 93.260000 M 4 +1722 332.129945 N 506.438228 E 93.270000 M 4 +1733 332.001672 N 505.442048 E 93.280000 M 4 +1744 331.873952 N 504.445354 E 93.290000 M 4 +1756 331.724453 N 503.282716 E 93.300000 M 4 +1766 331.605408 N 502.315306 E 93.310000 M 4 +1777 331.481010 N 501.317413 E 93.320000 M 4 +1787 331.358274 N 500.318321 E 93.330000 M 4 +1800 331.214312 N 499.153628 E 93.340000 M 4 +1810 331.092498 N 498.154023 E 93.350000 M 4 +1821 330.972161 N 497.155616 E 93.360000 M 4 +1831 330.852193 N 496.158579 E 93.370000 M 4 +1844 330.714507 N 494.995598 E 93.380000 M 4 +1854 330.599153 N 494.034525 E 93.390000 M 4 +1864 330.482692 N 493.043825 E 93.400000 M 4 +1875 330.367707 N 492.057406 E 93.410000 M 4 +1887 330.235188 N 490.910009 E 93.420000 M 4 +1897 330.121126 N 489.931468 E 93.430000 M 4 +1907 330.007249 N 488.957208 E 93.440000 M 4 +1918 329.894848 N 487.986202 E 93.450000 M 4 +1930 329.761960 N 486.857472 E 93.460000 M 4 +1939 329.655466 N 485.927738 E 93.470000 M 4 +1950 329.543249 N 484.971288 E 93.480000 M 4 +1960 329.431218 N 484.018949 E 93.490000 M 4 +1971 329.295561 N 482.912995 E 93.500000 M 4 +1981 329.181130 N 481.965964 E 93.510000 M 4 +1991 329.067991 N 481.026983 E 93.520000 M 4 +2001 328.951899 N 480.092967 E 93.530000 M 4 +2013 328.814213 N 479.003625 E 93.540000 M 4 +2022 328.700335 N 478.112423 E 93.550000 M 4 +2032 328.584058 N 477.197759 E 93.560000 M 4 +2041 328.467966 N 476.293885 E 93.570000 M 4 +2052 328.329542 N 475.255919 E 93.580000 M 4 +2062 328.209389 N 474.381499 E 93.590000 M 4 +2071 328.090159 N 473.523007 E 93.600000 M 4 +2080 327.971483 N 472.680783 E 93.610000 M 4 +2090 327.835088 N 471.721251 E 93.620000 M 4 +2098 327.714751 N 470.953009 E 93.630000 M 4 +2106 327.599767 N 470.168155 E 93.640000 M 4 +2115 327.486258 N 469.397344 E 93.650000 M 4 +2124 327.358354 N 468.514876 E 93.660000 M 4 +2132 327.255920 N 467.774034 E 93.670000 M 4 +2140 327.157177 N 467.044838 E 93.680000 M 4 +2147 327.060648 N 466.330541 E 93.690000 M 4 +2156 326.951385 N 465.512806 E 93.700000 M 4 +2163 326.863347 N 464.862386 E 93.710000 M 4 +2170 326.773094 N 464.183196 E 93.720000 M 4 +2177 326.687455 N 463.510855 E 93.730000 M 4 +2185 326.591666 N 462.736277 E 93.740000 M 4 +2192 326.513963 N 462.076095 E 93.750000 M 4 +2199 326.439768 N 461.420880 E 93.760000 M 4 +2206 326.369632 N 460.771146 E 93.770000 M 4 +2214 326.290638 N 460.017974 E 93.780000 M 4 +2220 326.231023 N 459.415676 E 93.790000 M 4 +2227 326.167717 N 458.778271 E 93.800000 M 4 +2234 326.104964 N 458.145490 E 93.810000 M 4 +2241 326.033168 N 457.411841 E 93.820000 M 4 +2248 325.975399 N 456.788308 E 93.830000 M 4 +2254 325.922428 N 456.170769 E 93.840000 M 4 +2261 325.870011 N 455.558881 E 93.850000 M 4 +2268 325.805598 N 454.852119 E 93.860000 M 4 +2274 325.758164 N 454.291435 E 93.870000 M 4 +2281 325.703164 N 453.699927 E 93.880000 M 4 +2287 325.647794 N 453.113727 E 93.890000 M 4 +2294 325.584303 N 452.440530 E 93.900000 M 4 +2300 325.530040 N 451.872312 E 93.910000 M 4 +2306 325.474117 N 451.314369 E 93.920000 M 4 +2311 325.415425 N 450.764817 E 93.930000 M 4 +2318 325.346397 N 450.137859 E 93.940000 M 4 +2329 325.238610 N 449.143905 E 93.960000 M 4 +2340 325.119380 N 448.100116 E 93.980000 M 4 +2349 325.015654 N 447.226039 E 94.000000 M 4 +2358 324.915250 N 446.391351 E 94.020000 M 4 +2364 324.840870 N 445.768674 E 94.040000 M 4 +2371 324.767966 N 445.168774 E 94.060000 M 4 +2377 324.691002 N 444.541815 E 94.090000 M 4 +2383 324.676791 N 444.146220 E 94.520000 M 4 +2384 324.679928 N 444.131664 E 94.570000 M 4 diff --git a/samples/radar/malamira_南同大道/南同大道_003.head b/samples/radar/malamira_南同大道/南同大道_003.head new file mode 100644 index 0000000..39fce2e --- /dev/null +++ b/samples/radar/malamira_南同大道/南同大道_003.head @@ -0,0 +1,34 @@ +DATE:2022-03-10 +START_TIME:10:52 +STOP_TIME: +UNITS:m +MODE:距离模式 +ANTENNAS:200 MHz shielded +FREQUENCY:5351.611816 +STACKS:2 +LAST_TRACE:38144 +POSITIVE_DIRECTION:1 +SAMPLES:516 +TIME_INTERVAL:0.000000 +TIMEWINDOW:96.419553 +DEPTH: +ZERO_POSITION: +DIELECTRIC: +SOIL_TYPE: +BITS:16 +MARK: +DISTANCE_INTERVAL:0.095224 +START_POSITION:0.000000 +STOP_POSITION:226.917805 +WHEEL_GPS: +WHEEL_CALIBRATION:84.4270000000 +SCAN_SECOND: +NUMBER_OF_CH:16 +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 +RTK_X_OFFSET: +RTK_Y_OFFSET:0.350 +RTK_Z_OFFSET: +GAIN: +FILTER: +SMOOTH: +ENDIAN_TYPE:1 diff --git a/samples/radar/malamira_南同大道/南同大道_004.cor b/samples/radar/malamira_南同大道/南同大道_004.cor new file mode 100644 index 0000000..f898dbe --- /dev/null +++ b/samples/radar/malamira_南同大道/南同大道_004.cor @@ -0,0 +1,313 @@ +VERSION:1 +1 319.500445 N 468.116541 E 103.040000 M 4 +7 319.535328 N 468.424626 E 103.500000 M 4 +12 319.612846 N 468.954826 E 103.560000 M 4 +19 319.706790 N 469.530750 E 103.600000 M 4 +25 319.803134 N 470.120718 E 103.630000 M 4 +32 319.933438 N 470.825768 E 103.660000 M 4 +38 320.037348 N 471.349289 E 103.680000 M 4 +45 320.152149 N 471.971110 E 103.700000 M 4 +51 320.260489 N 472.603548 E 103.720000 M 4 +59 320.409249 N 473.312879 E 103.740000 M 4 +67 320.556718 N 474.027519 E 103.760000 M 4 +76 320.738147 N 474.871455 E 103.780000 M 4 +85 320.908870 N 475.733887 E 103.800000 M 4 +96 321.136071 N 476.723217 E 103.820000 M 4 +106 321.362165 N 477.709122 E 103.840000 M 4 +112 321.482133 N 478.227334 E 103.850000 M 4 +119 321.624987 N 478.858402 E 103.860000 M 4 +125 321.744217 N 479.432615 E 103.870000 M 4 +131 321.879689 N 480.010937 E 103.880000 M 4 +137 322.021989 N 480.609981 E 103.890000 M 4 +145 322.194374 N 481.334725 E 103.900000 M 4 +152 322.350702 N 481.975383 E 103.910000 M 4 +159 322.513674 N 482.631797 E 103.920000 M 4 +167 322.683659 N 483.303110 E 103.930000 M 4 +175 322.883176 N 484.108857 E 103.940000 M 4 +183 323.044302 N 484.826065 E 103.950000 M 4 +191 323.214103 N 485.540363 E 103.960000 M 4 +198 323.382981 N 486.265620 E 103.970000 M 4 +208 323.577145 N 487.113324 E 103.980000 M 4 +215 323.743439 N 487.835157 E 103.990000 M 4 +223 323.906226 N 488.554763 E 104.000000 M 4 +231 324.065692 N 489.273342 E 104.010000 M 4 +240 324.242875 N 490.111113 E 104.020000 M 4 +248 324.380008 N 490.840651 E 104.030000 M 4 +255 324.518064 N 491.554606 E 104.040000 M 4 +263 324.654643 N 492.267362 E 104.050000 M 4 +272 324.806725 N 493.096913 E 104.060000 M 4 +279 324.930938 N 493.806587 E 104.070000 M 4 +287 325.050353 N 494.515918 E 104.080000 M 4 +294 325.164415 N 495.223023 E 104.090000 M 4 +303 325.293980 N 496.045381 E 104.100000 M 4 +311 325.395122 N 496.754883 E 104.110000 M 4 +318 325.499402 N 497.442636 E 104.120000 M 4 +325 325.602206 N 498.119258 E 104.130000 M 4 +333 325.721436 N 498.890925 E 104.140000 M 4 +340 325.824608 N 499.549737 E 104.150000 M 4 +347 325.929257 N 500.215056 E 104.160000 M 4 +354 326.033353 N 500.880717 E 104.170000 M 4 +363 326.158858 N 501.667797 E 104.180000 M 4 +370 326.257970 N 502.360174 E 104.190000 M 4 +378 326.366126 N 503.055120 E 104.200000 M 4 +385 326.477234 N 503.762225 E 104.210000 M 4 +394 326.609753 N 504.602564 E 104.220000 M 4 +402 326.724000 N 505.337755 E 104.230000 M 4 +410 326.839169 N 506.087844 E 104.240000 M 4 +418 326.953969 N 506.852661 E 104.250000 M 4 +427 327.089994 N 507.762872 E 104.260000 M 4 +436 327.203872 N 508.566907 E 104.270000 M 4 +445 327.326239 N 509.376592 E 104.280000 M 4 +453 327.448238 N 510.199806 E 104.290000 M 4 +464 327.590907 N 511.176464 E 104.300000 M 4 +473 327.713644 N 512.026736 E 104.310000 M 4 +482 327.835827 N 512.889510 E 104.320000 M 4 +491 327.957271 N 513.763245 E 104.330000 M 4 +502 328.096065 N 514.796073 E 104.340000 M 4 +512 328.206251 N 515.703373 E 104.350000 M 4 +521 328.320682 N 516.610673 E 104.360000 M 4 +531 328.434006 N 517.527734 E 104.370000 M 4 +542 328.563756 N 518.606458 E 104.380000 M 4 +552 328.672835 N 519.538419 E 104.390000 M 4 +562 328.782836 N 520.479627 E 104.400000 M 4 +572 328.899298 N 521.428199 E 104.410000 M 4 +584 329.043259 N 522.546311 E 104.420000 M 4 +594 329.159351 N 523.522455 E 104.430000 M 4 +605 329.284118 N 524.489351 E 104.440000 M 4 +615 329.415345 N 525.459672 E 104.450000 M 4 +627 329.570381 N 526.592512 E 104.460000 M 4 +637 329.709359 N 527.563690 E 104.470000 M 4 +647 329.845569 N 528.540690 E 104.480000 M 4 +658 329.983255 N 529.520601 E 104.490000 M 4 +670 330.141244 N 530.663716 E 104.500000 M 4 +680 330.268410 N 531.657670 E 104.510000 M 4 +691 330.402405 N 532.638952 E 104.520000 M 4 +701 330.537323 N 533.622630 E 104.530000 M 4 +713 330.691990 N 534.770370 E 104.540000 M 4 +724 330.821740 N 535.756789 E 104.550000 M 4 +734 330.946322 N 536.739097 E 104.560000 M 4 +745 331.073673 N 537.726373 E 104.570000 M 4 +757 331.217450 N 538.882161 E 104.580000 M 4 +768 331.334834 N 539.885363 E 104.590000 M 4 +778 331.453141 N 540.863219 E 104.600000 M 4 +788 331.564619 N 541.836451 E 104.610000 M 4 +800 331.698799 N 542.969977 E 104.620000 M 4 +810 331.825596 N 543.945264 E 104.630000 M 4 +821 331.968081 N 544.934765 E 104.640000 M 4 +831 332.100599 N 545.920842 E 104.650000 M 4 +844 332.240685 N 547.073376 E 104.660000 M 4 +854 332.349026 N 548.068186 E 104.670000 M 4 +864 332.465672 N 549.056489 E 104.680000 M 4 +875 332.593391 N 550.048388 E 104.690000 M 4 +887 332.748981 N 551.219075 E 104.700000 M 4 +898 332.875962 N 552.226730 E 104.710000 M 4 +909 332.991316 N 553.235754 E 104.720000 M 4 +919 333.101871 N 554.251457 E 104.730000 M 4 +932 333.231252 N 555.441839 E 104.740000 M 4 +943 333.335532 N 556.475695 E 104.750000 M 4 +954 333.450517 N 557.502358 E 104.760000 M 4 +965 333.567163 N 558.535872 E 104.770000 M 4 +977 333.702635 N 559.751085 E 104.780000 M 4 +989 333.816697 N 560.798641 E 104.790000 M 4 +1000 333.930020 N 561.852020 E 104.800000 M 4 +1011 334.045743 N 562.910023 E 104.810000 M 4 +1024 334.179554 N 564.144416 E 104.820000 M 4 +1035 334.285864 N 565.212009 E 104.830000 M 4 +1046 334.397342 N 566.263162 E 104.840000 M 4 +1057 334.509189 N 567.311574 E 104.850000 M 4 +1070 334.638201 N 568.532439 E 104.860000 M 4 +1081 334.746172 N 569.574344 E 104.870000 M 4 +1092 334.853221 N 570.615050 E 104.880000 M 4 +1103 334.958423 N 571.651646 E 104.890000 M 4 +1116 335.082452 N 572.857440 E 104.900000 M 4 +1127 335.178242 N 573.900373 E 104.910000 M 4 +1138 335.285475 N 574.928406 E 104.920000 M 4 +1148 335.394000 N 575.951987 E 104.930000 M 4 +1161 335.521535 N 577.138943 E 104.940000 M 4 +1172 335.633751 N 578.147796 E 104.950000 M 4 +1182 335.748736 N 579.147059 E 104.960000 M 4 +1193 335.865013 N 580.142897 E 104.970000 M 4 +1205 335.999193 N 581.298857 E 104.980000 M 4 +1215 336.108456 N 582.294866 E 104.990000 M 4 +1226 336.223256 N 583.273578 E 105.000000 M 4 +1236 336.341009 N 584.248694 E 105.010000 M 4 +1248 336.480911 N 585.382733 E 105.020000 M 4 +1258 336.601248 N 586.351684 E 105.030000 M 4 +1268 336.719739 N 587.322519 E 105.040000 M 4 +1279 336.837123 N 588.293183 E 105.050000 M 4 +1291 336.972411 N 589.427222 E 105.060000 M 4 +1301 337.076690 N 590.409531 E 105.070000 M 4 +1311 337.187430 N 591.384989 E 105.080000 M 4 +1322 337.295955 N 592.365757 E 105.090000 M 4 +1334 337.423306 N 593.516921 E 105.100000 M 4 +1344 337.530170 N 594.510533 E 105.110000 M 4 +1355 337.635557 N 595.508083 E 105.120000 M 4 +1366 337.739837 N 596.512826 E 105.130000 M 4 +1378 337.860913 N 597.691391 E 105.140000 M 4 +1389 337.960947 N 598.712061 E 105.150000 M 4 +1399 338.069657 N 599.732559 E 105.160000 M 4 +1410 338.181504 N 600.760250 E 105.170000 M 4 +1423 338.314023 N 601.966900 E 105.180000 M 4 +1434 338.429007 N 603.004181 E 105.190000 M 4 +1445 338.547868 N 604.046258 E 105.200000 M 4 +1456 338.669682 N 605.093471 E 105.210000 M 4 +1469 338.813644 N 606.320159 E 105.220000 M 4 +1480 338.931951 N 607.379189 E 105.230000 M 4 +1491 339.056164 N 608.437877 E 105.240000 M 4 +1503 339.181853 N 609.500161 E 105.250000 M 4 +1516 339.330429 N 610.742432 E 105.260000 M 4 +1527 339.460548 N 611.808312 E 105.270000 M 4 +1538 339.592882 N 612.877618 E 105.280000 M 4 +1550 339.724478 N 613.948464 E 105.290000 M 4 +1563 339.876930 N 615.199298 E 105.300000 M 4 +1574 340.000774 N 616.278023 E 105.310000 M 4 +1586 340.131816 N 617.354349 E 105.320000 M 4 +1597 340.263596 N 618.431019 E 105.330000 M 4 +1610 340.416971 N 619.683908 E 105.340000 M 4 +1622 340.544137 N 620.756467 E 105.350000 M 4 +1633 340.664474 N 621.830054 E 105.360000 M 4 +1644 340.777429 N 622.906723 E 105.370000 M 4 +1658 340.900350 N 624.165263 E 105.380000 M 4 +1669 340.990418 N 625.252208 E 105.390000 M 4 +1680 341.082517 N 626.336412 E 105.400000 M 4 +1692 341.171847 N 627.425754 E 105.410000 M 4 +1705 341.278711 N 628.700906 E 105.420000 M 4 +1717 341.371547 N 629.799154 E 105.430000 M 4 +1728 341.467337 N 630.899285 E 105.440000 M 4 +1740 341.565157 N 632.004553 E 105.450000 M 4 +1754 341.688632 N 633.298200 E 105.460000 M 4 +1766 341.784053 N 634.414087 E 105.470000 M 4 +1777 341.903098 N 635.527233 E 105.480000 M 4 +1789 342.021959 N 636.640037 E 105.490000 M 4 +1803 342.176072 N 637.944130 E 105.500000 M 4 +1815 342.305822 N 639.064641 E 105.510000 M 4 +1826 342.436126 N 640.179328 E 105.520000 M 4 +1838 342.552033 N 641.295728 E 105.530000 M 4 +1852 342.683444 N 642.600678 E 105.540000 M 4 +1864 342.773513 N 643.729066 E 105.550000 M 4 +1876 342.890712 N 644.854371 E 105.560000 M 4 +1888 343.013449 N 645.980019 E 105.570000 M 4 +1901 343.164793 N 647.282571 E 105.580000 M 4 +1913 343.304510 N 648.404965 E 105.590000 M 4 +1925 343.443119 N 649.528901 E 105.600000 M 4 +1937 343.584682 N 650.654035 E 105.610000 M 4 +1951 343.739533 N 651.967719 E 105.620000 M 4 +1963 343.854702 N 653.094051 E 105.630000 M 4 +1975 343.987959 N 654.224323 E 105.640000 M 4 +1987 344.126384 N 655.353053 E 105.650000 M 4 +2001 344.296184 N 656.672217 E 105.660000 M 4 +2013 344.439039 N 657.805914 E 105.670000 M 4 +2025 344.584846 N 658.939439 E 105.680000 M 4 +2037 344.730100 N 660.077589 E 105.690000 M 4 +2051 344.900454 N 661.407713 E 105.700000 M 4 +2063 345.026513 N 662.550315 E 105.710000 M 4 +2075 345.173428 N 663.694629 E 105.720000 M 4 +2087 345.321819 N 664.839628 E 105.730000 M 4 +2101 345.496234 N 666.177630 E 105.740000 M 4 +2113 345.645364 N 667.327767 E 105.750000 M 4 +2125 345.796339 N 668.477733 E 105.760000 M 4 +2138 345.947314 N 669.630096 E 105.770000 M 4 +2152 346.126159 N 670.974777 E 105.780000 M 4 +2164 346.260708 N 672.130736 E 105.790000 M 4 +2176 346.416851 N 673.287381 E 105.800000 M 4 +2189 346.572625 N 674.446081 E 105.810000 M 4 +2203 346.755345 N 675.799153 E 105.820000 M 4 +2215 346.912596 N 676.961791 E 105.830000 M 4 +2228 347.070215 N 678.126142 E 105.840000 M 4 +2240 347.222483 N 679.293062 E 105.850000 M 4 +2254 347.393760 N 680.655724 E 105.860000 M 4 +2267 347.517973 N 681.827439 E 105.870000 M 4 +2279 347.655475 N 683.002408 E 105.880000 M 4 +2292 347.788363 N 684.181144 E 105.890000 M 4 +2306 347.936385 N 685.559391 E 105.900000 M 4 +2319 348.059860 N 686.743436 E 105.910000 M 4 +2331 348.179090 N 687.931248 E 105.920000 M 4 +2344 348.294813 N 689.122315 E 105.930000 M 4 +2358 348.429361 N 690.515460 E 105.940000 M 4 +2371 348.531427 N 691.709781 E 105.950000 M 4 +2384 348.656193 N 692.907183 E 105.960000 M 4 +2396 348.786866 N 694.105100 E 105.970000 M 4 +2411 348.946147 N 695.504924 E 105.980000 M 4 +2424 349.085864 N 696.704725 E 105.990000 M 4 +2436 349.232594 N 697.903498 E 106.000000 M 4 +2449 349.385415 N 699.102442 E 106.010000 M 4 +2464 349.568135 N 700.500896 E 106.020000 M 4 +2476 349.715235 N 701.697614 E 106.030000 M 4 +2489 349.880237 N 702.898784 E 106.040000 M 4 +2502 350.048746 N 704.099955 E 106.050000 M 4 +2517 350.251030 N 705.500635 E 106.060000 M 4 +2530 350.428214 N 706.701463 E 106.070000 M 4 +2542 350.611857 N 707.903490 E 106.080000 M 4 +2555 350.796977 N 709.105174 E 106.090000 M 4 +2570 351.013843 N 710.502943 E 106.100000 M 4 +2583 351.186966 N 711.694181 E 106.110000 M 4 +2595 351.371532 N 712.883535 E 106.120000 M 4 +2608 351.556837 N 714.067409 E 106.130000 M 4 +2622 351.774440 N 715.437264 E 106.140000 M 4 +2635 351.960852 N 716.603328 E 106.150000 M 4 +2647 352.142281 N 717.762370 E 106.160000 M 4 +2659 352.320018 N 718.912335 E 106.170000 M 4 +2673 352.529132 N 720.247597 E 106.180000 M 4 +2685 352.697825 N 721.387459 E 106.190000 M 4 +2698 352.877777 N 722.524752 E 106.200000 M 4 +2709 353.058099 N 723.649544 E 106.210000 M 4 +2723 353.270719 N 724.943704 E 106.220000 M 4 +2735 353.460269 N 726.036814 E 106.230000 M 4 +2747 353.659416 N 727.128383 E 106.240000 M 4 +2758 353.851549 N 728.224575 E 106.250000 M 4 +2772 354.058817 N 729.494246 E 106.260000 M 4 +2783 354.214776 N 730.578793 E 106.270000 M 4 +2795 354.375348 N 731.648613 E 106.280000 M 4 +2806 354.538136 N 732.715521 E 106.290000 M 4 +2819 354.737098 N 733.949743 E 106.300000 M 4 +2830 354.901362 N 734.996614 E 106.310000 M 4 +2841 355.063042 N 736.035265 E 106.320000 M 4 +2852 355.223615 N 737.067237 E 106.330000 M 4 +2865 355.414087 N 738.270634 E 106.340000 M 4 +2876 355.566170 N 739.294044 E 106.350000 M 4 +2887 355.723236 N 740.307007 E 106.360000 M 4 +2897 355.879748 N 741.316716 E 106.370000 M 4 +2910 356.059146 N 742.492199 E 106.380000 M 4 +2920 356.208645 N 743.494373 E 106.390000 M 4 +2931 356.353714 N 744.492437 E 106.400000 M 4 +2941 356.495276 N 745.487418 E 106.410000 M 4 +2954 356.656956 N 746.643207 E 106.420000 M 4 +2964 356.780431 N 747.622262 E 106.430000 M 4 +2974 356.910735 N 748.604399 E 106.440000 M 4 +2985 357.036055 N 749.582427 E 106.450000 M 4 +2997 357.180571 N 750.716466 E 106.460000 M 4 +3007 357.299062 N 751.683019 E 106.470000 M 4 +3017 357.415155 N 752.644606 E 106.480000 M 4 +3027 357.526079 N 753.598316 E 106.490000 M 4 +3039 357.651215 N 754.701529 E 106.500000 M 4 +3049 357.745528 N 755.632976 E 106.510000 M 4 +3059 357.845194 N 756.559114 E 106.520000 M 4 +3068 357.941538 N 757.471551 E 106.530000 M 4 +3079 358.047294 N 758.520135 E 106.540000 M 4 +3089 358.133117 N 759.404658 E 106.550000 M 4 +3098 358.215249 N 760.273768 E 106.560000 M 4 +3107 358.295720 N 761.125240 E 106.570000 M 4 +3117 358.389111 N 762.096417 E 106.580000 M 4 +3125 358.456108 N 762.897711 E 106.590000 M 4 +3134 358.531596 N 763.687874 E 106.600000 M 4 +3142 358.607268 N 764.460397 E 106.610000 M 4 +3151 358.693091 N 765.335159 E 106.620000 M 4 +3159 358.764149 N 766.063670 E 106.630000 M 4 +3166 358.831331 N 766.770261 E 106.640000 M 4 +3173 358.893346 N 767.455788 E 106.650000 M 4 +3182 358.961635 N 768.226770 E 106.660000 M 4 +3188 359.010730 N 768.851674 E 106.670000 M 4 +3195 359.064808 N 769.463390 E 106.680000 M 4 +3201 359.115748 N 770.048220 E 106.690000 M 4 +3208 359.172964 N 770.699154 E 106.700000 M 4 +3213 359.220951 N 771.227984 E 106.710000 M 4 +3218 359.265247 N 771.731811 E 106.720000 M 4 +3229 359.351070 N 772.736554 E 106.740000 M 4 +3238 359.417514 N 773.546411 E 106.760000 M 4 +3246 359.488941 N 774.324928 E 106.780000 M 4 +3252 359.543019 N 774.944865 E 106.800000 M 4 +3258 359.591744 N 775.511885 E 106.820000 M 4 +3265 359.639363 N 776.139185 E 106.850000 M 4 +3271 359.689011 N 776.684456 E 106.880000 M 4 +3276 359.732569 N 777.210717 E 106.920000 M 4 +3281 359.757300 N 777.645015 E 107.070000 M 4 diff --git a/samples/radar/malamira_南同大道/南同大道_004.head b/samples/radar/malamira_南同大道/南同大道_004.head new file mode 100644 index 0000000..e67c39e --- /dev/null +++ b/samples/radar/malamira_南同大道/南同大道_004.head @@ -0,0 +1,34 @@ +DATE:2022-03-10 +START_TIME:10:54 +STOP_TIME: +UNITS:m +MODE:距离模式 +ANTENNAS:200 MHz shielded +FREQUENCY:5351.611816 +STACKS:2 +LAST_TRACE:52496 +POSITIVE_DIRECTION:1 +SAMPLES:516 +TIME_INTERVAL:0.000000 +TIMEWINDOW:96.419553 +DEPTH: +ZERO_POSITION: +DIELECTRIC: +SOIL_TYPE: +BITS:16 +MARK: +DISTANCE_INTERVAL:0.095304 +START_POSITION:0.000000 +STOP_POSITION:312.597339 +WHEEL_GPS: +WHEEL_CALIBRATION:84.4270000000 +SCAN_SECOND: +NUMBER_OF_CH:16 +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 +RTK_X_OFFSET: +RTK_Y_OFFSET:0.350 +RTK_Z_OFFSET: +GAIN: +FILTER: +SMOOTH: +ENDIAN_TYPE:1 diff --git a/samples/radar/malamira_南同大道/南同大道_005.cor b/samples/radar/malamira_南同大道/南同大道_005.cor new file mode 100644 index 0000000..0aa32e9 --- /dev/null +++ b/samples/radar/malamira_南同大道/南同大道_005.cor @@ -0,0 +1,342 @@ +VERSION:1 +1 324.297322 N 503.979203 E 139.800000 M 4 +7 324.256718 N 504.322737 E 140.200000 M 4 +12 324.396434 N 504.852766 E 140.270000 M 4 +18 324.513819 N 505.340837 E 140.310000 M 4 +24 324.607209 N 505.990744 E 140.350000 M 4 +31 324.726439 N 506.597665 E 140.380000 M 4 +38 324.842716 N 507.268635 E 140.410000 M 4 +44 324.893841 N 507.812365 E 140.430000 M 4 +49 324.984463 N 508.360546 E 140.450000 M 4 +56 325.087635 N 509.010795 E 140.470000 M 4 +63 325.191361 N 509.661729 E 140.490000 M 4 +71 325.270725 N 510.411133 E 140.510000 M 4 +79 325.384602 N 511.133137 E 140.530000 M 4 +87 325.509000 N 511.944535 E 140.550000 M 4 +95 325.621770 N 512.717572 E 140.570000 M 4 +104 325.712761 N 513.580517 E 140.590000 M 4 +113 325.840112 N 514.406129 E 140.610000 M 4 +118 325.912462 N 514.903963 E 140.620000 M 4 +128 326.032799 N 515.786945 E 140.640000 M 4 +138 326.176391 N 516.779871 E 140.660000 M 4 +148 326.294514 N 517.721079 E 140.680000 M 4 +159 326.463392 N 518.779938 E 140.700000 M 4 +164 326.533896 N 519.284108 E 140.710000 M 4 +170 326.602370 N 519.796840 E 140.720000 M 4 +175 326.672136 N 520.320190 E 140.730000 M 4 +182 326.773094 N 520.942867 E 140.740000 M 4 +188 326.828280 N 521.484883 E 140.750000 M 4 +193 326.927022 N 522.032551 E 140.760000 M 4 +199 327.013215 N 522.588782 E 140.770000 M 4 +206 327.096454 N 523.251875 E 140.780000 M 4 +212 327.160868 N 523.828484 E 140.790000 M 4 +218 327.229896 N 524.407834 E 140.800000 M 4 +224 327.303169 N 524.992493 E 140.810000 M 4 +232 327.395267 N 525.683842 E 140.820000 M 4 +238 327.427566 N 526.277920 E 140.830000 M 4 +244 327.495671 N 526.876964 E 140.840000 M 4 +250 327.560638 N 527.469158 E 140.850000 M 4 +258 327.636495 N 528.152972 E 140.860000 M 4 +264 327.707553 N 528.735233 E 140.870000 M 4 +270 327.780272 N 529.314412 E 140.880000 M 4 +276 327.851884 N 529.893590 E 140.890000 M 4 +283 327.932724 N 530.566958 E 140.900000 M 4 +289 327.957456 N 531.136033 E 140.910000 M 4 +295 328.024453 N 531.709731 E 140.920000 M 4 +301 328.092928 N 532.282231 E 140.930000 M 4 +308 328.171553 N 532.947207 E 140.940000 M 4 +314 328.238366 N 533.514398 E 140.950000 M 4 +320 328.304441 N 534.081418 E 140.960000 M 4 +326 328.371069 N 534.647239 E 140.970000 M 4 +333 328.448033 N 535.303481 E 140.980000 M 4 +338 328.473319 N 535.856287 E 140.990000 M 4 +344 328.536625 N 536.415943 E 141.000000 M 4 +350 328.597532 N 536.974913 E 141.010000 M 4 +357 328.665637 N 537.623278 E 141.020000 M 4 +363 328.725252 N 538.177454 E 141.030000 M 4 +368 328.786897 N 538.731116 E 141.040000 M 4 +374 328.848173 N 539.283407 E 141.050000 M 4 +381 328.920154 N 539.923895 E 141.060000 M 4 +387 328.947100 N 540.463342 E 141.070000 M 4 +392 329.012806 N 541.009640 E 141.080000 M 4 +398 329.080357 N 541.554397 E 141.090000 M 4 +405 329.162120 N 542.187178 E 141.100000 M 4 +410 329.234470 N 542.727482 E 141.110000 M 4 +416 329.308666 N 543.266587 E 141.120000 M 4 +422 329.382677 N 543.802781 E 141.130000 M 4 +428 329.470161 N 544.426657 E 141.140000 M 4 +434 329.503752 N 544.948808 E 141.150000 M 4 +439 329.578501 N 545.477467 E 141.160000 M 4 +445 329.653251 N 546.003215 E 141.170000 M 4 +451 329.743873 N 546.612705 E 141.180000 M 4 +457 329.823421 N 547.130232 E 141.190000 M 4 +462 329.903523 N 547.640910 E 141.200000 M 4 +467 329.980487 N 548.146792 E 141.210000 M 4 +474 330.069448 N 548.731450 E 141.220000 M 4 +484 330.180003 N 549.693723 E 141.240000 M 4 +494 330.343898 N 550.697780 E 141.260000 M 4 +503 330.471618 N 551.564493 E 141.280000 M 4 +513 330.563347 N 552.437714 E 141.300000 M 4 +520 330.586602 N 553.192256 E 141.320000 M 4 +529 330.636989 N 553.992865 E 141.340000 M 4 +536 330.668734 N 554.708361 E 141.360000 M 4 +544 330.679070 N 555.453655 E 141.380000 M 4 +551 330.607643 N 556.085922 E 141.400000 M 4 +557 330.548582 N 556.728465 E 141.420000 M 4 +563 330.477524 N 557.269796 E 141.440000 M 4 +569 330.380626 N 557.826369 E 141.460000 M 4 +574 330.217285 N 558.309988 E 141.480000 M 4 +580 330.050068 N 558.821522 E 141.500000 M 4 +587 329.796659 N 559.454303 E 141.530000 M 4 +593 329.506336 N 559.996148 E 141.560000 M 4 +599 329.281719 N 560.470006 E 141.590000 M 4 +606 328.978661 N 561.040108 E 141.630000 M 4 +612 328.768809 N 561.613807 E 141.670000 M 4 +619 328.545299 N 562.222783 E 141.710000 M 4 +624 328.412412 N 562.743736 E 141.740000 M 4 +630 328.319206 N 563.274278 E 141.770000 M 4 +636 328.202191 N 563.836674 E 141.800000 M 4 +642 328.143868 N 564.435547 E 141.830000 M 4 +649 328.134455 N 565.052744 E 141.860000 M 4 +655 328.124304 N 565.625415 E 141.890000 M 4 +661 328.168969 N 566.254942 E 141.920000 M 4 +668 328.201822 N 566.863233 E 141.950000 M 4 +674 328.281185 N 567.492589 E 141.980000 M 4 +680 328.370515 N 568.086324 E 142.010000 M 4 +687 328.450248 N 568.687594 E 142.040000 M 4 +693 328.574092 N 569.315580 E 142.070000 M 4 +700 328.736510 N 569.931407 E 142.100000 M 4 +706 328.901143 N 570.446194 E 142.130000 M 4 +711 329.110072 N 570.936321 E 142.160000 M 4 +718 329.388398 N 571.514472 E 142.200000 M 4 +723 329.652513 N 571.959559 E 142.230000 M 4 +729 329.957601 N 572.392316 E 142.260000 M 4 +736 330.395207 N 572.934675 E 142.300000 M 4 +742 330.780951 N 573.325647 E 142.330000 M 4 +748 331.181090 N 573.686477 E 142.360000 M 4 +753 331.607438 N 574.046280 E 142.390000 M 4 +759 332.004994 N 574.416359 E 142.420000 M 4 +766 332.495756 N 574.925495 E 142.460000 M 4 +772 332.843294 N 575.334790 E 142.490000 M 4 +778 333.141922 N 575.785015 E 142.520000 M 4 +784 333.431138 N 576.283191 E 142.550000 M 4 +790 333.676426 N 576.827091 E 142.580000 M 4 +796 333.836261 N 577.356606 E 142.610000 M 4 +802 333.995541 N 577.948629 E 142.640000 M 4 +808 334.082657 N 578.543049 E 142.670000 M 4 +815 334.153530 N 579.169665 E 142.700000 M 4 +821 334.193027 N 579.780868 E 142.730000 M 4 +828 334.172171 N 580.425807 E 142.760000 M 4 +835 334.156483 N 581.103799 E 142.790000 M 4 +842 334.126030 N 581.798916 E 142.820000 M 4 +849 334.068260 N 582.456529 E 142.850000 M 4 +854 334.032085 N 582.957445 E 142.870000 M 4 +862 333.952353 N 583.704623 E 142.900000 M 4 +870 333.825740 N 584.414810 E 142.930000 M 4 +875 333.746931 N 584.952203 E 142.950000 M 4 +881 333.674396 N 585.458427 E 142.970000 M 4 +887 333.585804 N 586.007123 E 142.990000 M 4 +892 333.538371 N 586.538522 E 143.010000 M 4 +898 333.481893 N 587.125749 E 143.030000 M 4 +904 333.457715 N 587.681124 E 143.050000 M 4 +910 333.416926 N 588.282394 E 143.070000 M 4 +916 333.420986 N 588.857120 E 143.090000 M 4 +923 333.447195 N 589.493839 E 143.110000 M 4 +929 333.478940 N 590.105214 E 143.130000 M 4 +937 333.475249 N 590.788343 E 143.150000 M 4 +944 333.499612 N 591.468218 E 143.170000 M 4 +952 333.523790 N 592.247592 E 143.190000 M 4 +960 333.554428 N 593.009155 E 143.210000 M 4 +969 333.558857 N 593.868504 E 143.230000 M 4 +977 333.597986 N 594.704563 E 143.250000 M 4 +983 333.621610 N 595.204794 E 143.260000 M 4 +992 333.671074 N 596.080583 E 143.280000 M 4 +1002 333.739548 N 597.041142 E 143.300000 M 4 +1011 333.775538 N 597.913678 E 143.320000 M 4 +1021 333.844012 N 598.860195 E 143.340000 M 4 +1030 333.916916 N 599.722969 E 143.360000 M 4 +1039 334.008830 N 600.644483 E 143.380000 M 4 +1048 334.056633 N 601.469067 E 143.400000 M 4 +1058 334.150392 N 602.378936 E 143.420000 M 4 +1066 334.243414 N 603.208487 E 143.440000 M 4 +1076 334.345479 N 604.098490 E 143.460000 M 4 +1084 334.406939 N 604.887111 E 143.480000 M 4 +1093 334.517310 N 605.755023 E 143.500000 M 4 +1101 334.628234 N 606.545699 E 143.520000 M 4 +1110 334.742850 N 607.363434 E 143.540000 M 4 +1117 334.783270 N 608.038856 E 143.560000 M 4 +1125 334.874261 N 608.776616 E 143.580000 M 4 +1132 334.959900 N 609.447415 E 143.600000 M 4 +1139 335.050337 N 610.177981 E 143.620000 M 4 +1147 335.103677 N 610.857343 E 143.640000 M 4 +1155 335.207588 N 611.674563 E 143.660000 M 4 +1164 335.311130 N 612.489729 E 143.680000 M 4 +1174 335.434604 N 613.434533 E 143.700000 M 4 +1183 335.524119 N 614.337209 E 143.720000 M 4 +1194 335.663282 N 615.393328 E 143.740000 M 4 +1199 335.725665 N 615.898525 E 143.750000 M 4 +1205 335.788049 N 616.414169 E 143.760000 M 4 +1210 335.850986 N 616.938375 E 143.770000 M 4 +1217 335.925366 N 617.560025 E 143.780000 M 4 +1222 335.953789 N 618.077381 E 143.790000 M 4 +1228 336.021710 N 618.626761 E 143.800000 M 4 +1234 336.090368 N 619.180594 E 143.810000 M 4 +1241 336.171208 N 619.832556 E 143.820000 M 4 +1247 336.241344 N 620.397863 E 143.830000 M 4 +1253 336.314986 N 620.968650 E 143.840000 M 4 +1259 336.388074 N 621.544746 E 143.850000 M 4 +1266 336.474266 N 622.222052 E 143.860000 M 4 +1272 336.507119 N 622.781194 E 143.870000 M 4 +1278 336.577070 N 623.371847 E 143.880000 M 4 +1284 336.648312 N 623.965068 E 143.890000 M 4 +1291 336.733766 N 624.661042 E 143.900000 M 4 +1298 336.808885 N 625.263511 E 143.910000 M 4 +1304 336.888064 N 625.869233 E 143.920000 M 4 +1310 336.967243 N 626.478039 E 143.930000 M 4 +1318 337.052143 N 627.193192 E 143.940000 M 4 +1324 337.089241 N 627.785900 E 143.950000 M 4 +1331 337.163252 N 628.406179 E 143.960000 M 4 +1337 337.240585 N 629.030055 E 143.970000 M 4 +1345 337.331023 N 629.761307 E 143.980000 M 4 +1351 337.407802 N 630.391176 E 143.990000 M 4 +1358 337.482552 N 631.020361 E 144.000000 M 4 +1365 337.550657 N 631.639099 E 144.010000 M 4 +1372 337.621530 N 632.337127 E 144.020000 M 4 +1378 337.648477 N 632.907058 E 144.030000 M 4 +1384 337.715290 N 633.490347 E 144.040000 M 4 +1390 337.782472 N 634.063531 E 144.050000 M 4 +1397 337.859621 N 634.719774 E 144.060000 M 4 +1403 337.922927 N 635.273778 E 144.070000 M 4 +1408 337.984203 N 635.816137 E 144.080000 M 4 +1414 338.041234 N 636.350961 E 144.090000 M 4 +1420 338.105647 N 636.964219 E 144.100000 M 4 +1431 338.182058 N 637.977867 E 144.120000 M 4 +1436 338.239458 N 638.494710 E 144.130000 M 4 +1443 338.306271 N 639.098378 E 144.140000 M 4 +1448 338.364040 N 639.615391 E 144.150000 M 4 +1454 338.420887 N 640.133432 E 144.160000 M 4 +1459 338.477918 N 640.650275 E 144.170000 M 4 +1465 338.546023 N 641.256169 E 144.180000 M 4 +1471 338.567432 N 641.757256 E 144.190000 M 4 +1476 338.630923 N 642.290025 E 144.200000 M 4 +1482 338.696444 N 642.828959 E 144.210000 M 4 +1489 338.773777 N 643.466706 E 144.220000 M 4 +1494 338.840221 N 644.019854 E 144.230000 M 4 +1500 338.908695 N 644.581394 E 144.240000 M 4 +1506 338.977354 N 645.148584 E 144.250000 M 4 +1513 339.054503 N 645.817328 E 144.260000 M 4 +1519 339.083110 N 646.369278 E 144.270000 M 4 +1525 339.152876 N 646.949998 E 144.280000 M 4 +1531 339.225042 N 647.534485 E 144.290000 M 4 +1538 339.310311 N 648.206654 E 144.300000 M 4 +1544 339.375279 N 648.764255 E 144.310000 M 4 +1550 339.441723 N 649.315348 E 144.320000 M 4 +1556 339.509274 N 649.866270 E 144.330000 M 4 +1562 339.591775 N 650.507613 E 144.340000 M 4 +1568 339.623520 N 651.027024 E 144.350000 M 4 +1573 339.691625 N 651.566815 E 144.360000 M 4 +1579 339.758992 N 652.103865 E 144.370000 M 4 +1586 339.835772 N 652.726028 E 144.380000 M 4 +1591 339.900001 N 653.255201 E 144.390000 M 4 +1597 339.970505 N 653.785229 E 144.400000 M 4 +1602 340.045808 N 654.311491 E 144.410000 M 4 +1609 340.133108 N 654.919954 E 144.420000 M 4 +1619 340.217455 N 655.922299 E 144.440000 M 4 +1625 340.268764 N 656.434004 E 144.450000 M 4 +1631 340.332624 N 657.030479 E 144.460000 M 4 +1636 340.394454 N 657.534649 E 144.470000 M 4 +1641 340.466065 N 658.031283 E 144.480000 M 4 +1647 340.546536 N 658.526034 E 144.490000 M 4 +1653 340.622947 N 659.099904 E 144.500000 M 4 +1662 340.679793 N 660.032549 E 144.520000 M 4 +1673 340.785919 N 661.079249 E 144.540000 M 4 +1684 340.914561 N 662.055050 E 144.560000 M 4 +1695 341.040989 N 663.133432 E 144.580000 M 4 +1705 341.127920 N 664.116939 E 144.600000 M 4 +1711 341.186797 N 664.633439 E 144.610000 M 4 +1717 341.258962 N 665.246184 E 144.620000 M 4 +1723 341.322822 N 665.779466 E 144.630000 M 4 +1728 341.389820 N 666.319770 E 144.640000 M 4 +1734 341.455894 N 666.866753 E 144.650000 M 4 +1741 341.535442 N 667.513406 E 144.660000 M 4 +1746 341.590628 N 668.044291 E 144.670000 M 4 +1752 341.659471 N 668.616448 E 144.680000 M 4 +1758 341.725176 N 669.194256 E 144.690000 M 4 +1766 341.805094 N 669.881325 E 144.700000 M 4 +1772 341.875782 N 670.477286 E 144.710000 M 4 +1778 341.955146 N 671.077700 E 144.720000 M 4 +1784 342.031187 N 671.677258 E 144.730000 M 4 +1792 342.120333 N 672.377341 E 144.740000 M 4 +1798 342.177364 N 672.942306 E 144.750000 M 4 +1804 342.251744 N 673.536041 E 144.760000 M 4 +1810 342.324832 N 674.116418 E 144.770000 M 4 +1817 342.411763 N 674.784477 E 144.780000 M 4 +1823 342.483744 N 675.349784 E 144.790000 M 4 +1829 342.554433 N 675.908584 E 144.800000 M 4 +1835 342.621061 N 676.460533 E 144.810000 M 4 +1841 342.697287 N 677.091772 E 144.820000 M 4 +1847 342.741029 N 677.589777 E 144.830000 M 4 +1852 342.806919 N 678.115524 E 144.840000 M 4 +1858 342.871702 N 678.634764 E 144.850000 M 4 +1864 342.942945 N 679.229185 E 144.860000 M 4 +1869 342.999053 N 679.730100 E 144.870000 M 4 +1879 343.111269 N 680.711895 E 144.890000 M 4 +1885 343.176606 N 681.270866 E 144.900000 M 4 +1895 343.266674 N 682.177823 E 144.920000 M 4 +1905 343.368739 N 683.173319 E 144.940000 M 4 +1914 343.462130 N 684.072398 E 144.960000 M 4 +1925 343.557181 N 685.028163 E 144.980000 M 4 +1933 343.623071 N 685.862166 E 145.000000 M 4 +1943 343.724029 N 686.782139 E 145.020000 M 4 +1952 343.817973 N 687.615457 E 145.040000 M 4 +1961 343.927237 N 688.503577 E 145.060000 M 4 +1969 344.015275 N 689.275587 E 145.080000 M 4 +1978 344.136350 N 690.134079 E 145.100000 M 4 +1986 344.244137 N 690.919104 E 145.120000 M 4 +1995 344.358568 N 691.766808 E 145.140000 M 4 +2003 344.429441 N 692.523062 E 145.160000 M 4 +2012 344.538705 N 693.381726 E 145.180000 M 4 +2021 344.648521 N 694.184048 E 145.200000 M 4 +2030 344.766459 N 695.064975 E 145.220000 M 4 +2038 344.850068 N 695.857706 E 145.240000 M 4 +2048 344.971882 N 696.756786 E 145.260000 M 4 +2056 345.083360 N 697.593358 E 145.280000 M 4 +2066 345.212371 N 698.503398 E 145.300000 M 4 +2075 345.295057 N 699.315995 E 145.320000 M 4 +2084 345.415948 N 700.229117 E 145.340000 M 4 +2093 345.530748 N 701.065861 E 145.360000 M 4 +2102 345.650163 N 701.963228 E 145.380000 M 4 +2111 345.722143 N 702.756302 E 145.400000 M 4 +2120 345.834729 N 703.632605 E 145.420000 M 4 +2128 345.931811 N 704.432186 E 145.440000 M 4 +2137 346.036091 N 705.284171 E 145.460000 M 4 +2145 346.099028 N 706.030322 E 145.480000 M 4 +2154 346.194264 N 706.851139 E 145.500000 M 4 +2161 346.282117 N 707.594720 E 145.520000 M 4 +2170 346.375323 N 708.387452 E 145.540000 M 4 +2177 346.426633 N 709.081370 E 145.560000 M 4 +2185 346.518178 N 709.844988 E 145.580000 M 4 +2192 346.596618 N 710.535653 E 145.600000 M 4 +2200 346.675982 N 711.267761 E 145.620000 M 4 +2207 346.722308 N 711.903282 E 145.640000 M 4 +2214 346.802041 N 712.606790 E 145.660000 M 4 +2221 346.868484 N 713.244708 E 145.680000 M 4 +2228 346.939173 N 713.919618 E 145.700000 M 4 +2234 346.979593 N 714.501879 E 145.720000 M 4 +2241 347.051205 N 715.143051 E 145.740000 M 4 +2247 347.117834 N 715.718804 E 145.760000 M 4 +2253 347.185754 N 716.324527 E 145.780000 M 4 +2258 347.219160 N 716.838972 E 145.800000 M 4 +2264 347.284497 N 717.408047 E 145.820000 M 4 +2270 347.341712 N 717.917011 E 145.840000 M 4 +2275 347.401327 N 718.448924 E 145.860000 M 4 +2282 347.459650 N 719.123662 E 145.890000 M 4 +2290 347.539937 N 719.825629 E 145.920000 M 4 +2296 347.587001 N 720.451902 E 145.950000 M 4 +2303 347.656582 N 721.061735 E 145.980000 M 4 +2308 347.717858 N 721.590052 E 146.010000 M 4 +2315 347.766215 N 722.213585 E 146.050000 M 4 +2320 347.828967 N 722.764678 E 146.090000 M 4 +2326 347.871418 N 723.292309 E 146.140000 M 4 +2332 347.919036 N 723.817200 E 146.220000 M 4 +2333 347.924942 N 723.934680 E 146.300000 M 4 diff --git a/samples/radar/malamira_南同大道/南同大道_005.head b/samples/radar/malamira_南同大道/南同大道_005.head new file mode 100644 index 0000000..9630475 --- /dev/null +++ b/samples/radar/malamira_南同大道/南同大道_005.head @@ -0,0 +1,34 @@ +DATE:2022-03-10 +START_TIME:11:02 +STOP_TIME: +UNITS:m +MODE:距离模式 +ANTENNAS:200 MHz shielded +FREQUENCY:5351.611816 +STACKS:2 +LAST_TRACE:37328 +POSITIVE_DIRECTION:1 +SAMPLES:516 +TIME_INTERVAL:0.000000 +TIMEWINDOW:96.419553 +DEPTH: +ZERO_POSITION: +DIELECTRIC: +SOIL_TYPE: +BITS:16 +MARK: +DISTANCE_INTERVAL:0.095987 +START_POSITION:0.000000 +STOP_POSITION:223.841500 +WHEEL_GPS: +WHEEL_CALIBRATION:84.4270000000 +SCAN_SECOND: +NUMBER_OF_CH:16 +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 +RTK_X_OFFSET: +RTK_Y_OFFSET:0.350 +RTK_Z_OFFSET: +GAIN: +FILTER: +SMOOTH: +ENDIAN_TYPE:1 diff --git a/src/app/DatasetDimension.cpp b/src/app/DatasetDimension.cpp index 91d85db..b4c1e2d 100644 --- a/src/app/DatasetDimension.cpp +++ b/src/app/DatasetDimension.cpp @@ -7,7 +7,7 @@ namespace { enum class Dim { D3, D2, Analysis, Other }; Dim dimOf(const std::string& c) { if (c == "dd_voxel" || c == "dd_Structual3D" || c == "dd_Property3D" || - c == "dd_section" || c == "dd_inversion_data") + c == "dd_section" || c == "dd_inversion_data" || c == "dd_radar_3d") return Dim::D3; if (c == "dd_slice") return Dim::Analysis; if (c == "dd_trajectory_data") return Dim::D2; diff --git a/src/app/VtkSceneView.cpp b/src/app/VtkSceneView.cpp index b9e10cc..11526e9 100644 --- a/src/app/VtkSceneView.cpp +++ b/src/app/VtkSceneView.cpp @@ -216,14 +216,22 @@ void VtkSceneView::addVolume(const std::string& dsId, const geopro::data::Volume volumeOwnerDs_ = dsId; volumes_[dsId] = VolumeRec{image, cs, vol.vmin, vol.vmax, volume}; // 多体并发:登记本体 image+actor - // G3 等值面:在值域高段(0.7)抽不透明实心异常体(参考图红块)。挂同一 dsProps_ → 随体一并移除。 - const double isoVal = vol.vmin + 0.7 * (vol.vmax - vol.vmin); - auto iso = geopro::render::buildIsosurface(image, cs, vol.vmin, vol.vmax, isoVal); - if (iso) { - iso->PickableOff(); // 不参与拾取(同体 actor,避免串选) - iso->SetVisibility(analysisMode2D_ ? 0 : 1); // 3D 内容:二维分析下隐藏 - scene_.addActor(iso); - dsProps_[dsId].push_back(iso); + // G3 等值面:在值域高段(0.7)抽不透明实心异常体(参考图红块)——【反演专属】。 + // 雷达体(registerRadarDataset 产的 "radar-" id)跳过:振幅体的 0.7 阈值面=强反射层, + // 既无地球物理含义、又是 SetOpacity(1.0) 实色 actor【不受体不透明度控制】(用户实测: + // 体不透明度调 0 仍见灰色实面=就是它)。"radar-" 是该体唯一生产者指定的稳定 id。 + // 注:impulse-GPR("vol-")同为振幅体、亦不该有等值面,但 "vol-" 与反演共用前缀, + // 待 ddCode 贯通 addVolume 后再统一按类型门控(见 spec §11)。 + const bool isRadarVolume = dsId.rfind("radar-", 0) == 0; + if (!isRadarVolume) { + const double isoVal = vol.vmin + 0.7 * (vol.vmax - vol.vmin); + auto iso = geopro::render::buildIsosurface(image, cs, vol.vmin, vol.vmax, isoVal); + if (iso) { + iso->PickableOff(); // 不参与拾取(同体 actor,避免串选) + iso->SetVisibility(analysisMode2D_ ? 0 : 1); // 3D 内容:二维分析下隐藏 + scene_.addActor(iso); + dsProps_[dsId].push_back(iso); + } } if (onVolumeChanged) onVolumeChanged(); } @@ -359,6 +367,39 @@ void VtkSceneView::applyCameraView(geopro::controller::ViewDir dir) { if (onCameraChanged) onCameraChanged(); // 相机变了 → 底图按新视锥重算覆盖 } +void VtkSceneView::focusAlongLongAxis(double t, double windowFrac) { + double b[6]; + if (!computeDataBounds(b) || !scene_.renderer()) return; + const double ex = b[1] - b[0], ey = b[3] - b[2], ez = b[5] - b[4]; + const int ax = (ex >= ey && ex >= ez) ? 0 : (ey >= ez ? 1 : 2); // 最长轴 + const double lo = b[2 * ax], hi = b[2 * ax + 1], len = hi - lo; + if (len <= 0.0) return; + if (t < 0.0) t = 0.0; + if (t > 1.0) t = 1.0; + if (windowFrac <= 0.0) windowFrac = 0.12; + const double half = 0.5 * windowFrac * len; + const double center = lo + t * len; + double sub[6] = {b[0], b[1], b[2], b[3], b[4], b[5]}; // 短轴满幅 + sub[2 * ax] = (center - half < lo) ? lo : center - half; // 长轴只取窗口段 + sub[2 * ax + 1] = (center + half > hi) ? hi : center + half; + scene_.renderer()->ResetCamera(sub); // 保持朝向,仅重定位+缩放到该窗口 + scene_.renderer()->ResetCameraClippingRange(); + if (renderWindow_) renderWindow_->Render(); + if (onCameraChanged) onCameraChanged(); // 底图随新视锥重算 +} + +double VtkSceneView::longAxisElongation() const { + double b[6]; + if (!computeDataBounds(b)) return 0.0; + const double ex = std::abs(b[1] - b[0]), ey = std::abs(b[3] - b[2]), ez = std::abs(b[5] - b[4]); + double mx = ex, mn = ex; + if (ey > mx) mx = ey; + if (ez > mx) mx = ez; + if (ey < mn) mn = ey; + if (ez < mn) mn = ez; + return (mn > 0.0) ? mx / mn : 0.0; +} + void VtkSceneView::zoom(double factor) { geopro::render::zoomBy(scene_.renderer(), factor); if (renderWindow_) renderWindow_->Render(); diff --git a/src/app/VtkSceneView.hpp b/src/app/VtkSceneView.hpp index 6b1c8a9..abd63d1 100644 --- a/src/app/VtkSceneView.hpp +++ b/src/app/VtkSceneView.hpp @@ -94,6 +94,13 @@ public: void setAnalysisMode2D(bool is2D); bool isAnalysisMode2D() const { return analysisMode2D_; } + // ── B 方案#2:沿线位置巡航(雷达超长测线)────────────────────────────────────── + // t∈[0,1] 沿数据【最长轴】定位;取景到该位置一段【窗口】(windowFrac=窗口占长轴比例), + // 保持当前朝向(ResetCamera 只重定位+缩放、不转向)→ 像滚动读长 radargram。短轴满幅、长轴只取一段。 + void focusAlongLongAxis(double t, double windowFrac); + // 数据包围盒长短轴比(max/min 跨度)。用于判是否细长(雷达)→ 决定沿线滑块显隐。无数据返回 0。 + double longAxisElongation() const; + // ── 二维分析改造 B 期:选中 2D 足迹沿高程 Z 拖动 ─────────────────────────────── // 仅二维分析下用。pickMapLineAt:在屏幕(x,y)拾取足迹(只考虑可见足迹,不被地形/底图干扰);命中则 // 选中(additive=Ctrl 多选切换,否则单选替换)并高亮,返回是否有选中(交互样式据此决定 Z 拖动/平移)。 diff --git a/src/app/main.cpp b/src/app/main.cpp index 52ece32..9d3a598 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -245,6 +245,38 @@ private: std::vector raiseAfter_; // 定位后再 raise 到 overlay 之上的常驻控件(工具条/提示) }; +// 底部条浮层定位器:把 overlay 钉在 host(QVTK 画布)底部、横向铺满(留边距)。 +// 用于 B 方案#2 雷达沿线位置滑块。仅外观,无 Q_OBJECT/moc。 +class BottomBarOverlay : public QObject { +public: + BottomBarOverlay(QWidget* overlay, QWidget* host, int margin = 12, int barHeight = 34) + : QObject(host), overlay_(overlay), host_(host), margin_(margin), barHeight_(barHeight) + { + host_->installEventFilter(this); + } + void reposition() + { + const QSize h = host_->size(); + const int w = std::max(160, h.width() - 2 * margin_); + overlay_->resize(w, barHeight_); + overlay_->move(margin_, std::max(0, h.height() - barHeight_ - margin_)); + if (overlay_->isVisible()) overlay_->raise(); // GL 子控件须 raise 才可见 + } + +protected: + bool eventFilter(QObject* obj, QEvent* e) override + { + if (obj == host_ && (e->type() == QEvent::Resize || e->type() == QEvent::Show)) reposition(); + return QObject::eventFilter(obj, e); + } + +private: + QWidget* overlay_; + QWidget* host_; + int margin_; + int barHeight_; +}; + // 取 vector 中位数(用于由测线 lat/lon 推世界系原点)。空则返回 0。 double median(std::vector v) { @@ -471,6 +503,39 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re elevHint->show(); elevHint->raise(); }); + // ── B 方案#2:雷达沿线位置滑块(超长测线巡航)──────────────────────────────────── + // 拖动 → 相机沿数据最长轴 dolly 到该位置的一段窗口(focusAlongLongAxis)。仅细长(雷达)体显示。 + auto* alongLineBar = new QWidget(vtkWidget); + alongLineBar->setObjectName(QStringLiteral("alongLineBar")); + auto* alongLineSlider = new QSlider(Qt::Horizontal, alongLineBar); + { + auto* lay = new QHBoxLayout(alongLineBar); + lay->setContentsMargins(12, 4, 12, 4); + lay->setSpacing(10); + auto* lbl = new QLabel(QStringLiteral("沿线位置"), alongLineBar); + lbl->setObjectName(QStringLiteral("alongLineLbl")); + alongLineSlider->setRange(0, 1000); + alongLineSlider->setValue(0); + lay->addWidget(lbl); + lay->addWidget(alongLineSlider, 1); + } + geopro::app::applyTokenizedStyleSheet( + alongLineBar, + QStringLiteral("QWidget#alongLineBar{background:#0E1A2D;" + "border:1px solid {{border/default}};border-radius:8px;}" + "QLabel#alongLineLbl{color:#E6ECF5;border:none;}")); + alongLineBar->hide(); // 默认隐藏,仅细长(雷达)体显示 + auto* alongLineOverlay = new BottomBarOverlay(alongLineBar, vtkWidget); + QObject::connect(alongLineSlider, &QSlider::valueChanged, vtkWidget, + [sceneView](int v) { sceneView->focusAlongLongAxis(v / 1000.0, 0.12); }); + // 显隐刷新:仅三维分析 + 细长(长短轴比≥4,即雷达)体时显示沿线滑块。 + auto refreshAlongLineBar = std::make_shared>( + [sceneView, alongLineBar, alongLineOverlay]() { + const bool show = !sceneView->isAnalysisMode2D() && sceneView->longAxisElongation() >= 4.0; + alongLineBar->setVisible(show); + if (show) alongLineOverlay->reposition(); + }); + if (auto* style = interactionMgr->pickStyle()) { // 命中可见足迹→选中(Ctrl 多选)并返回是否进入 Z 拖动;未命中(返回 false)→交互样式回退平移。 style->onPick2D = [sceneView](int x, int y, bool additive) { @@ -567,7 +632,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re }; // 体素变化(重建/清场)后把体素 image 推给 InteractionManager(切片基底),并调和已保存切片 + 异常。 - sceneView->onVolumeChanged = [interactionMgr, sceneView, syncSlices, refreshAnomalies]() { + sceneView->onVolumeChanged = [interactionMgr, sceneView, syncSlices, refreshAnomalies, + refreshAlongLineBar]() { // 多体并发:先移除 interactionMgr 中已不再渲染的体(关其切片),再 upsert 当前所有已渲染体 image。 for (const std::string& id : interactionMgr->volumeIds()) if (!sceneView->isVolumeRendered(id)) interactionMgr->removeVolumeImage(id); @@ -576,6 +642,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re kv.second.vmin, kv.second.vmax); syncSlices(); // 体到场/移除后调和各体下已勾选切片(多体并存) refreshAnomalies(); // 同步重载异常 actor + 刷新异常列表 + (*refreshAlongLineBar)(); // 体增删 → 据是否细长(雷达)刷新沿线滑块显隐(B#2) }; // ── 抽屉信号 → 控制器/交互(Task 7/12 接线)────────────────────────────── @@ -607,10 +674,15 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // 后下发控制器(setCheckedDatasets 全量 diff,须并集;否则一栏勾选会清掉另一栏的图元)。 auto checkedProfiles = std::make_shared(); auto checkedAnalysis = std::make_shared(); - auto pushChecked = [sceneCtrl, checkedProfiles, checkedAnalysis]() { + // 引导层(emptyState)隐藏器:emptyState 在后文(~1390)才创建,故用可后置赋值的回调转发, + // 让「三维分析栏勾选(体/切片)」这条渲染路径也能隐藏不透明引导层——否则它盖住已渲染的体 + // (雷达体由分析栏勾选触发渲染,但旧逻辑只在对象树勾选时隐藏引导层 → 体被盖住看不到)。 + auto setSceneEmptyVisible = std::make_shared>(); + auto pushChecked = [sceneCtrl, checkedProfiles, checkedAnalysis, setSceneEmptyVisible]() { QStringList all = *checkedProfiles; all += *checkedAnalysis; sceneCtrl->setCheckedDatasets(all); + if (*setSceneEmptyVisible) (*setSceneEmptyVisible)(all.isEmpty()); // 场景有内容→隐藏引导层 }; // ── VTK 视图切片右键菜单(设计 §2.3)────────────────────────────────────── @@ -980,6 +1052,66 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re analysisTab->scrollItemToTop(qid); // 新三维体行尽量滚到分析栏顶部 }); }); + // 本地导入三维雷达测线(后端未就绪的过渡入口):入口=三维体段头「+ 导入雷达测线」按钮(CategorySection) + // → analysisTab.radarImportRequested(impulse)。app 无原生菜单栏(menuBar 被 TopBar 经 setMenuWidget 占用), + // 故入口放可见的段头按钮。impulse=false 走规范化(.head/.data, 懒加载后台建体);true 走 Impulse(.iprb, eager)。 + QObject::connect( + analysisTab, &geopro::app::CategoryAnalysisTab::radarImportRequested, &window, + [&window, scene3dRepo, refreshAnalysis, analysisTab, vtkLoading](bool impulse) { + if (!impulse) { // 规范化 .head/.data → registerRadarDataset(dd_radar_3d, 懒加载后台建体) + 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 归属)。 + // coarse=1 全分辨率(沿线不抽稀):验收期要肉眼判读反射/双曲线/通道连续性, + // 不能被沿线抽稀糊掉。单线峰值内存 ~0.7–1.5GB(spec §8.4);若 OOM 退回 2。 + const std::string newId = scene3dRepo->registerRadarDataset( + dir.toLocal8Bit().toStdString(), prefix.toLocal8Bit().toStdString(), + prefix.toStdString(), /*structParentId=*/std::string(), /*coarse=*/1); + { const QSignalBlocker block(analysisTab); refreshAnalysis(); } // DS 进三维体段(不触发渲染) + const QString qid = QString::fromStdString(newId); + analysisTab->setItemChecked(qid, true); // 勾选 → addDatasetAsync → loadVolume 后台建体渲染 + analysisTab->setItemBusy(qid, true); // spinner; 渲染完成由 datasetRendered 撤 + analysisTab->scrollItemToTop(qid); + return; + } + // 明星路 Impulse(.iprb):复用现成 createGprVolume(eager 同步建体,预填 cachedGrid)。双数据集互证下游几何无关。 + 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体…")); + // 内层捕获 window 引用(非 [=] 值拷贝):QMainWindow 拷贝构造已删除,且 showToast 需非 const QWidget*。 + QTimer::singleShot(0, &window, [=, &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 永久转圈)。 + analysisTab->setItemChecked(qid, true); + analysisTab->scrollItemToTop(qid); + }); + }); // 任一数据集(剖面/体)异步加载开始 → 列表项复选框转等待 spinner;渲染完成 → 复原复选框。 // 覆盖非三维体:勾选剖面首次渲染较慢时也有等待反馈(用户反馈)。 QObject::connect(sceneCtrl, &geopro::controller::VtkSceneController::datasetLoading, analysisTab, @@ -1008,7 +1140,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re geopro::app::SlicePropertiesDialog dlg(name, sp, &window); dlg.exec(); } - } else if (ddCode == QStringLiteral("dd_voxel")) { + } else if (ddCode == QStringLiteral("dd_voxel") || ddCode == QStringLiteral("dd_radar_3d")) { geopro::data::Api3dRepository::VolumeInfo info; if (scene3dRepo->volumeInfo(dsId.toStdString(), info)) { geopro::app::VolumePropertiesDialog dlg(name, info, &window); @@ -1133,6 +1265,17 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re QMessageBox::warning(&window, QStringLiteral("导出"), QStringLiteral("导出失败。")); }); + // 雷达体显示增益模式切换(右键「增益模式」):仓储切模式+清缓存 → 控制器清缓存/旧色阶并(勾选中) + // 异步用新增益重建体重渲。0=关(原始) 1=AGC(显深部) 2=保幅 tpow(判振幅)。纯显示,不动原始 .data。 + QObject::connect( + analysisTab, &geopro::app::CategoryAnalysisTab::radarGainModeRequested, &window, + [scene3dRepo, sceneCtrl](const QString& qid, int mode) { + const auto m = (mode == 0) ? geopro::data::RadarGainMode::Off + : (mode == 2) ? geopro::data::RadarGainMode::Tpow + : geopro::data::RadarGainMode::Agc; + if (scene3dRepo->setRadarGainMode(qid.toStdString(), m)) + sceneCtrl->rebuildRadarVolume(qid.toStdString()); + }); // 色阶(三维体/切片):复刻原版「色阶配置」对话框,确定后体素 + 其切片随新色阶重渲染。 // 仅对当前已渲染的三维体生效(切片色阶继承体色阶,经 InteractionManager 重建)。 QObject::connect(analysisTab, &geopro::app::CategoryAnalysisTab::colorScaleRequested, &window, @@ -1277,11 +1420,12 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // 渲染 [VtkSceneView]。顺序:先 ①②(都不渲染),最后 ③ 收尾统一渲染。只翻可见标志、不清空/重建 → // 切换瞬时;地形+底图常驻。 QObject::connect(drawer, &geopro::app::ColumnDrawer::analysisModeChanged, &window, - [interactionMgr, sceneCtrl, sceneView, viewToolbar](bool is2D) { + [interactionMgr, sceneCtrl, sceneView, viewToolbar, refreshAlongLineBar](bool is2D) { interactionMgr->setMode2D(is2D); sceneCtrl->onAnalysisModeChanged(is2D); sceneView->setAnalysisMode2D(is2D); viewToolbar->setAnalysisMode2D(is2D); // 二维下禁用 6 向快捷视图 + (*refreshAlongLineBar)(); // 二维隐藏沿线滑块、三维细长体显示(B#2) }); // 首个真实剖面到达 → frame 重锚到数据 lat/lon 后,把选中的底图加载到数据所在位置 @@ -1373,8 +1517,10 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re auto* emptyCentering = new CenterOverlay(emptyState, vtkWidget); // 引导层定位后,把工具条与提示浮层 raise 到其上 → 工具条永在最上层(修:缩小渲染区时引导层挡工具条)。 - emptyCentering->setRaiseAfter({viewToolbar, anomalyHint, elevHint}); + emptyCentering->setRaiseAfter({viewToolbar, anomalyHint, elevHint, alongLineBar}); emptyCentering->reposition(); + // 引导层隐藏器就位(见 pushChecked 处声明):场景(剖面∪三维分析)有勾选 → 隐藏不透明引导层、露出渲染。 + *setSceneEmptyVisible = [emptyState](bool empty) { emptyState->setVisible(empty); }; auto* vtkDock = new ads::CDockWidget(QStringLiteral("VTK视图")); vtkDock->setWidget(centerWidget); @@ -1548,10 +1694,11 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re auto generation = std::make_shared(0); QObject::connect( objectTree, &geopro::app::ObjectTreePanel::checkedSourcesChanged, &window, - [&projectRepo, &nav, drawer, emptyState, generation, lastSourceRows, + [&projectRepo, &nav, drawer, emptyState, generation, lastSourceRows, checkedAnalysis, refreshAnalysis](const QList& sources) { const unsigned long long myGen = ++(*generation); - emptyState->setVisible(sources.isEmpty()); // 有勾选→隐藏引导层,露出中央渲染 + // 引导层隐藏 = 对象树无源 且 三维分析栏也无勾选(否则取消勾选对象树会盖住仍在渲染的雷达体)。 + emptyState->setVisible(sources.isEmpty() && checkedAnalysis->isEmpty()); if (sources.isEmpty()) { *lastSourceRows = {}; refreshAnalysis(); // 清空 5 段(客户端三维体仍驻留) + col2D diff --git a/src/app/panels/columns/CategoryAnalysisTab.cpp b/src/app/panels/columns/CategoryAnalysisTab.cpp index 7f5959c..2680571 100644 --- a/src/app/panels/columns/CategoryAnalysisTab.cpp +++ b/src/app/panels/columns/CategoryAnalysisTab.cpp @@ -47,11 +47,15 @@ CategoryAnalysisTab::CategoryAnalysisTab(geopro::data::DatasetFieldDictionary* d }); connect(sec, &CategorySection::generateVolumeRequested, this, &CategoryAnalysisTab::generateVolumeRequested); + connect(sec, &CategorySection::radarImportRequested, this, + &CategoryAnalysisTab::radarImportRequested); connect(sec, &CategorySection::detailRequested, this, &CategoryAnalysisTab::detailRequested); connect(sec, &CategorySection::deleteDatasetRequested, this, &CategoryAnalysisTab::deleteDatasetRequested); connect(sec, &CategorySection::sliceRequested, this, &CategoryAnalysisTab::sliceRequested); connect(sec, &CategorySection::colorScaleRequested, this, &CategoryAnalysisTab::colorScaleRequested); + connect(sec, &CategorySection::radarGainModeRequested, this, + &CategoryAnalysisTab::radarGainModeRequested); connect(sec, &CategorySection::sliceSaveRequested, this, &CategoryAnalysisTab::sliceSaveRequested); connect(sec, &CategorySection::sliceSaveAsRequested, this, &CategoryAnalysisTab::sliceSaveAsRequested); connect(sec, &CategorySection::sliceExportImageRequested, this, diff --git a/src/app/panels/columns/CategoryAnalysisTab.hpp b/src/app/panels/columns/CategoryAnalysisTab.hpp index 2f927d0..eb00c5a 100644 --- a/src/app/panels/columns/CategoryAnalysisTab.hpp +++ b/src/app/panels/columns/CategoryAnalysisTab.hpp @@ -39,11 +39,13 @@ public: signals: void checkedDatasetsChanged(const QStringList& dsIds); // 5 段勾选并集 void generateVolumeRequested(const QString& dsTypeCode, const QStringList& sourceDsIds); + void radarImportRequested(bool impulse); // 三维体段头「+导入雷达测线」(false=规范化, true=Impulse) void detailRequested(const QString& dsId, const QString& ddCode, const QString& name); void deleteDatasetRequested(const QString& dsId, const QString& ddCode); // 右键删除切片/异常 // ── 三维体段操作转发(迁自旧 Column3DAnalysis,全接)── void sliceRequested(geopro::render::interact::SliceAxis axis, const QString& volumeDsId); void colorScaleRequested(const QString& dsId); + void radarGainModeRequested(const QString& dsId, int mode); // 雷达体显示增益模式(0关/1AGC/2保幅) void sliceSaveRequested(const QString& dsId); void sliceSaveAsRequested(const QString& dsId); void sliceExportImageRequested(const QString& dsId); diff --git a/src/app/panels/columns/CategorySection.cpp b/src/app/panels/columns/CategorySection.cpp index 2d9544f..ef66c92 100644 --- a/src/app/panels/columns/CategorySection.cpp +++ b/src/app/panels/columns/CategorySection.cpp @@ -83,6 +83,32 @@ CategorySection::CategorySection(const CategorySpec& spec, geopro::data::Dataset }); hl->addWidget(gen); } + // 三维体段头「+ 导入雷达测线」(后端未就绪的本地过渡入口):弹出菜单选 规范化/Impulse。 + // 次级强调按钮样式同「+新增三维体」;点击发 radarImportRequested(impulse) → 上层走导入流程。 + if (spec_.id == "voxel") { + auto* imp = new QToolButton(headerRow); + imp->setText(QStringLiteral("+ 导入雷达测线")); + imp->setCursor(Qt::PointingHandCursor); + imp->setPopupMode(QToolButton::InstantPopup); + applyTokenizedStyleSheet( + imp, QStringLiteral( + "QToolButton{border:1px solid {{accent/primary}};border-radius:%1px;" + "color:{{accent/primary}};background:transparent;padding:%2px %3px;font-size:%4px;}" + "QToolButton::menu-indicator{image:none;width:0;}" + "QToolButton:hover{background:{{bg/selected}};}" + "QToolButton:pressed{background:{{bg/hover}};}") + .arg(radius::kSm) + .arg(scaledPx(space::kXxs)) + .arg(scaledPx(space::kMd)) + .arg(scaledPx(type::kCaption))); + auto* menu = new QMenu(imp); + menu->addAction(QStringLiteral("规范化测线目录(.head/.data)…"), this, + [this] { emit radarImportRequested(false); }); + menu->addAction(QStringLiteral("Impulse 测线目录(.iprb)…"), this, + [this] { emit radarImportRequested(true); }); + imp->setMenu(menu); + hl->addWidget(imp); + } root->addWidget(headerRow); body_ = new QWidget(this); @@ -394,13 +420,22 @@ void CategorySection::showContextMenu(const QPoint& pos) { QMenu menu(this); menu.addAction(QStringLiteral("详情"), this, [this, id, ddCode, name] { emit detailRequested(id, ddCode, name); }); - if (ddCode == QStringLiteral("dd_voxel")) { // 三维体 + if (ddCode == QStringLiteral("dd_voxel") || ddCode == QStringLiteral("dd_radar_3d")) { // 三维体 QMenu* sl = menu.addMenu(QStringLiteral("生成切片")); // id=被右键的三维体 dsId(切片建到该体上) sl->addAction(QStringLiteral("上下"), this, [this, id] { emit sliceRequested(SliceAxis::UpDown, id); }); sl->addAction(QStringLiteral("前后"), this, [this, id] { emit sliceRequested(SliceAxis::FrontBack, id); }); sl->addAction(QStringLiteral("左右"), this, [this, id] { emit sliceRequested(SliceAxis::LeftRight, id); }); sl->addAction(QStringLiteral("任意"), this, [this, id] { emit sliceRequested(SliceAxis::Oblique, id); }); menu.addAction(QStringLiteral("色阶"), this, [this, id] { emit colorScaleRequested(id); }); + if (ddCode == QStringLiteral("dd_radar_3d")) { // 雷达体:显示增益模式切换(纯显示,重建体) + QMenu* gm = menu.addMenu(QStringLiteral("增益模式")); + gm->addAction(QStringLiteral("AGC(显深部·找目标)"), this, + [this, id] { emit radarGainModeRequested(id, 1); }); + gm->addAction(QStringLiteral("保幅 tpow(判振幅·复核)"), this, + [this, id] { emit radarGainModeRequested(id, 2); }); + gm->addAction(QStringLiteral("关(原始振幅)"), this, + [this, id] { emit radarGainModeRequested(id, 0); }); + } } else if (ddCode == QStringLiteral("dd_slice")) { // 切片(列表中均为已保存=定稿锁定,无保存/另存) QMenu* ex = menu.addMenu(QStringLiteral("导出")); ex->addAction(QStringLiteral("图片"), this, [this, id] { emit sliceExportImageRequested(id); }); diff --git a/src/app/panels/columns/CategorySection.hpp b/src/app/panels/columns/CategorySection.hpp index 4de7528..1ce1714 100644 --- a/src/app/panels/columns/CategorySection.hpp +++ b/src/app/panels/columns/CategorySection.hpp @@ -51,11 +51,13 @@ signals: void checkedDatasetsChanged(const QStringList& dsIds); // 数据行勾选=渲染 void collapsedChanged(); // 折叠/展开切换 → 外层 CategoryAnalysisTab 重排各段 stretch void generateVolumeRequested(const QString& dsTypeCode, const QStringList& sourceDsIds); // 段头「+新增三维体」 + void radarImportRequested(bool impulse); // 三维体段头「+导入雷达测线」(false=规范化 .head/.data, true=Impulse .iprb) void detailRequested(const QString& dsId, const QString& ddCode, const QString& name); // 双击/右键=详情 void deleteDatasetRequested(const QString& dsId, const QString& ddCode); // 右键删除(切片/异常) // ── 三维体段右键操作(迁自旧 Column3DAnalysis,全接)── void sliceRequested(geopro::render::interact::SliceAxis axis, const QString& volumeDsId); // 体→生成切片(轴+目标体) void colorScaleRequested(const QString& dsId); // 体/切片→色阶 + void radarGainModeRequested(const QString& dsId, int mode); // 雷达体→显示增益模式(0关/1AGC/2保幅tpow) void sliceSaveRequested(const QString& dsId); // 切片→保存位姿 void sliceSaveAsRequested(const QString& dsId); // 切片→另存 void sliceExportImageRequested(const QString& dsId); // 切片→导出图片 diff --git a/src/controller/DatasetViewState.hpp b/src/controller/DatasetViewState.hpp index ad0b4ce..72b22f0 100644 --- a/src/controller/DatasetViewState.hpp +++ b/src/controller/DatasetViewState.hpp @@ -39,6 +39,10 @@ public: if (!scales_.contains(dsId)) scales_.insert(dsId, cs); } + // 清除某 ds 的色阶真源(不广播):体被以新参数重建(如雷达切增益模式→值域大变)时调, + // 让下次 seedColorScale 用重建后的新色阶,而非沿用旧窗口。 + void clearColorScale(const QString& dsId) { scales_.remove(dsId); } + signals: void colorScaleChanged(const QString& dsId); diff --git a/src/controller/VtkSceneController.cpp b/src/controller/VtkSceneController.cpp index c41ef4b..a2ec377 100644 --- a/src/controller/VtkSceneController.cpp +++ b/src/controller/VtkSceneController.cpp @@ -323,6 +323,18 @@ void VtkSceneController::setVolumeColorScale(const std::string& dsId, view_.renderIncremental(); } +void VtkSceneController::rebuildRadarVolume(const std::string& dsId) { + // 仓储已切增益模式并失效其 cachedGrid(setRadarGainMode)。此处: + // 1) 清控制器缓存(否则命中旧体);2) 清旧色阶真源(增益后值域大变,须用重建后新窗口); + // 3) 移除旧 actor;4) 若勾选中 → 异步用新增益重建体并重渲。 + volumeCache_.erase(dsId); + volumeScaleCache_.erase(dsId); + if (state_) state_->clearColorScale(QString::fromStdString(dsId)); + view_.removeDataset(dsId); + if (isChecked(dsId)) addDatasetAsync(dsId, rebuildGeneration_); + view_.renderIncremental(); +} + void VtkSceneController::setAxesMode(AxesMode mode) { axesMode_ = mode; rebuildInternal(); // 坐标轴随场景重建(clear 会移除旧坐标轴 prop) diff --git a/src/controller/VtkSceneController.hpp b/src/controller/VtkSceneController.hpp index 5a03f63..2521c0d 100644 --- a/src/controller/VtkSceneController.hpp +++ b/src/controller/VtkSceneController.hpp @@ -57,6 +57,9 @@ public slots: // 三维体透明度调节(工具条滑块):运行时更新已渲染体的不透明度,并作为后续新体默认(0~1)。 void setVolumeOpacity(double maxOpacity); void rebuild(); // 主题切换等外部触发的重渲染 + // 雷达体增益模式切换后重建:仓储已切模式+清缓存(setRadarGainMode),此处清控制器缓存/旧色阶 + // 并(若勾选中)异步用新增益重建体、重渲。 + void rebuildRadarVolume(const std::string& dsId); // 色阶编辑器「确定」:写入色阶真源(state_),经 colorScaleChanged 统一就地重着色(体/帘面 + 切片)。 // 兼容旧调用点;真正的重着色在 recolorDataset()。无 state_ 时退化为直连重建。 diff --git a/src/data/GprVolumeRepository.cpp b/src/data/GprVolumeRepository.cpp index a1b1277..12ca216 100644 --- a/src/data/GprVolumeRepository.cpp +++ b/src/data/GprVolumeRepository.cpp @@ -1,10 +1,14 @@ #include "data/GprVolumeRepository.hpp" +#include #include +#include #include +#include #include "core/model/ScalarVolumeI16.hpp" #include "io/gpr/Gpr3dvVolumeBridge.hpp" +#include "io/gpr/NormalizedRadarVolumeBridge.hpp" namespace geopro::data { @@ -17,20 +21,63 @@ VolumeGrid builtI16ToVolumeGrid(const geopro::core::BuiltI16& built) { out.vol = geopro::core::ScalarVolume(nx, ny, nz); out.origin = built.origin; out.spacing = built.spacing; - out.vmin = built.vminPhys; - out.vmax = built.vmaxPhys; - // 逐体素反量化(布局一致:i 最快、k 最慢)。 + // 逐体素反量化(布局一致:i 最快、k 最慢) + 同遍累计 int16 直方图(零额外遍历)。 // kBlank → NaN:下游 render::buildVoxel 把 NaN 映射到 [vmin,vmax] 外的哨兵 → - // 传递函数置全透明(与 float 路径空值语义一致)。 + // 传递函数置全透明(与 float 路径空值语义一致)。GPR 稠密体无 kBlank,直方图即全体素。 const std::vector& src = built.vol.data(); std::vector& dst = out.vol.data(); const double nan = std::numeric_limits::quiet_NaN(); + constexpr int kHistN = 65536; // int16 全域,桶 b ↔ 量化值 q = b - 32768 + std::vector hist(kHistN, 0); + std::uint64_t total = 0; for (std::size_t idx = 0; idx < src.size(); ++idx) { const std::int16_t q = src[idx]; - dst[idx] = (q == geopro::core::ScalarVolumeI16::kBlank) - ? nan - : built.quant.toPhys(q); + if (q == geopro::core::ScalarVolumeI16::kBlank) { + dst[idx] = nan; + continue; + } + dst[idx] = built.quant.toPhys(q); + ++hist[static_cast(q) + 32768]; + ++total; + } + + // 显示值域 = 【双极对称】99% 窗口(GPR 标准 B-scan),而非全 min/max、也非非对称分位。 + // GPR 振幅正负振荡:基线(零反射)应=中灰、强负=黑、强正=白,且窗口对称否则色调失衡。 + // 做法:以【中位数=基线】为中心向两侧等距扩张,直到覆盖 99% 样本 → ±A。强反射/饱和值 + // (原始数据实测有 int16 下限 -32768 的钳值)落在 1% 尾外 → clamp 到端色,结构铺开如实显示。 + // 对齐独立 Python radargram(双极对称 ±p99);用全值域=灰板,用非对称 2/98=过饱和+灰点偏移。 + // 退化(全同值)兜底回全值域。 + out.vmin = built.vminPhys; + out.vmax = built.vmaxPhys; + if (total > 0) { + // 中位数桶(基线≈零振幅)。 + int centerQ = 32767; + { + const double half = 0.5 * static_cast(total); + std::uint64_t cum = 0; + for (int b = 0; b < kHistN; ++b) { + cum += hist[b]; + if (static_cast(cum) > half) { centerQ = b - 32768; break; } + } + } + // 自中位数对称外扩,覆盖 99% → 半宽 A。 + const double need = 0.99 * static_cast(total); + const int ci = centerQ + 32768; + std::uint64_t cum = (ci >= 0 && ci < kHistN) ? hist[ci] : 0; + int A = 0; + for (int r = 1; r < kHistN; ++r) { + const int lo = ci - r, hi = ci + r; + if (lo >= 0 && lo < kHistN) cum += hist[lo]; + if (hi >= 0 && hi < kHistN) cum += hist[hi]; + if (static_cast(cum) >= need) { A = r; break; } + } + if (A > 0) { + const int loQ = std::max(-32768, centerQ - A); + const int hiQ = std::min(32767, centerQ + A); + out.vmin = built.quant.toPhys(static_cast(loQ)); + out.vmax = built.quant.toPhys(static_cast(hiQ)); + } } return out; } @@ -47,4 +94,83 @@ VolumeGrid createGprVolumeGrid(const std::string& lineDir, return builtI16ToVolumeGrid(built); } +namespace { +// 单道(沿深度 n)增益:先 dewow(减滑动均值去低频 DC),再按模式 AGC(除滑窗 RMS,显弱反射但抹相对幅) +// 或 Tpow(×(k+1)^幂,保幅补衰减)。前缀和 O(n)。 +void gainTraceInPlace(double* col, int n, RadarGainMode mode, int dewowWin, int agcWin, + double tpowPower) { + if (n <= 0 || mode == RadarGainMode::Off) return; + if (dewowWin > 1) { + const int h = dewowWin / 2; + std::vector ps(n + 1, 0.0); + for (int k = 0; k < n; ++k) ps[k + 1] = ps[k] + col[k]; + std::vector o(n); + for (int k = 0; k < n; ++k) { + const int lo = std::max(0, k - h), hi = std::min(n, k + h + 1); + o[k] = col[k] - (ps[hi] - ps[lo]) / (hi - lo); + } + for (int k = 0; k < n; ++k) col[k] = o[k]; + } + if (mode == RadarGainMode::Agc && agcWin > 1) { + const int h = agcWin / 2; + std::vector ps2(n + 1, 0.0); + for (int k = 0; k < n; ++k) ps2[k + 1] = ps2[k] + col[k] * col[k]; + for (int k = 0; k < n; ++k) { + const int lo = std::max(0, k - h), hi = std::min(n, k + h + 1); + const double rms = std::sqrt((ps2[hi] - ps2[lo]) / (hi - lo)) + 1e-6; + col[k] /= rms; + } + } else if (mode == RadarGainMode::Tpow && tpowPower > 0.0) { + for (int k = 0; k < n; ++k) + col[k] *= std::pow(static_cast(k + 1), tpowPower); // 保幅:随深度放大、不抹相对强弱 + } +} + +// 逐道显示增益 + 增益后重取双极对称 99% 窗口。纯显示:只改 app 渲染体 g.vol,原始 .data 不变。 +void applyRadarDisplayGain(VolumeGrid& g, RadarGainMode mode, int dewowWin, int agcWin, + double tpowPower) { + const int nx = g.vol.nx(), ny = g.vol.ny(), nz = g.vol.nz(); + if (nx <= 0 || ny <= 0 || nz <= 0) return; + std::vector& d = g.vol.data(); + auto idx = [nx, ny](int i, int j, int k) { + return (static_cast(k) * ny + j) * nx + i; + }; + std::vector col(nz); + for (int j = 0; j < ny; ++j) + for (int i = 0; i < nx; ++i) { + for (int k = 0; k < nz; ++k) col[k] = d[idx(i, j, k)]; + gainTraceInPlace(col.data(), nz, mode, dewowWin, agcWin, tpowPower); + for (int k = 0; k < nz; ++k) d[idx(i, j, k)] = col[k]; + } + // 增益后重取窗口(中位数中心、对称 99%),否则旧窗口与增益后量纲不符。 + std::vector a(d.begin(), d.end()); + if (!a.empty()) { + const std::size_t m = a.size() / 2; + std::nth_element(a.begin(), a.begin() + m, a.end()); + const double med = a[m]; + for (double& x : a) x = std::abs(x - med); + const std::size_t q = static_cast(0.99 * (a.size() - 1)); + std::nth_element(a.begin(), a.begin() + q, a.end()); + const double A = a[q] > 0.0 ? a[q] : 1.0; + g.vmin = med - A; + g.vmax = med + A; + } +} +} // namespace + +VolumeGrid createRadarVolumeGrid(const std::string& lineDir, + const std::string& linePrefix, int coarse, + double targetDy, RadarGainMode gainMode) { + // 走规范化测线链(io::gpr) 读 .head/.data → int16 量化体 → 反量化为 app 的 float 体。 + // 与 createGprVolumeGrid 同适配器(builtI16ToVolumeGrid),仅上游数据源不同。 + const geopro::core::BuiltI16 built = + geopro::io::gpr::buildLineVolumeFromNormalized(lineDir, linePrefix, coarse, + targetDy); + VolumeGrid g = builtI16ToVolumeGrid(built); + // 显示增益(纯显示):Agc 显深部弱反射 / Tpow 保幅。原始数据不变;窗口随增益重取。 + if (gainMode != RadarGainMode::Off) + applyRadarDisplayGain(g, gainMode, /*dewowWin=*/30, /*agcWin=*/50, /*tpowPower=*/1.5); + return g; +} + } // namespace geopro::data diff --git a/src/data/GprVolumeRepository.hpp b/src/data/GprVolumeRepository.hpp index 59f7ff8..4737e91 100644 --- a/src/data/GprVolumeRepository.hpp +++ b/src/data/GprVolumeRepository.hpp @@ -33,6 +33,25 @@ VolumeGrid createGprVolumeGrid(const std::string& lineDir, const std::string& linePrefix, int coarse = 4, double targetDy = 0.025); +// 走【规范化测线链】(io::gpr::buildLineVolumeFromNormalized) 建逐线雷达体并适配 +// 成 app 的 VolumeGrid。读 {lineDir}/{linePrefix}.head + .data(轴 X=道/Y=通道/ +// Z=采样)→ BuiltI16(int16+Quant) → builtI16ToVolumeGrid 反量化 → VolumeGrid。 +// 与 createGprVolumeGrid(P1/P2 链)同输出格式,下游 addVolume 无需改动;区别仅是 +// 上游数据源走规范化 .head/.data 而非 .iprh/.iprb。 +// coarse(≥1)沿测线下采样;targetDy(米,>0 启用)线内通道插值(读 .head CH_X_OFFSETS)。 +// 失败(加载失败/解析失败)→ 抛 std::runtime_error(由 io::gpr 链抛出,原样透传)。 +// 雷达显示增益模式(纯显示、逐道沿深度、不动原始 .data;窗口随增益重取): +// Off = 原始振幅; +// Agc = dewow + 滑窗 RMS 归一:最大化显出深部弱反射(找目标),但【抹相对振幅】; +// Tpow = dewow + ×时间^幂:保幅增益(补几何扩散/衰减),【保留相对强弱】——判振幅/复核异常用。 +// (审阅者点③:标深部目标前应在 Tpow 保幅下也确认,不只信 Agc。) +enum class RadarGainMode { Off, Agc, Tpow }; + +VolumeGrid createRadarVolumeGrid(const std::string& lineDir, + const std::string& linePrefix, int coarse = 4, + double targetDy = 0.025, + RadarGainMode gainMode = RadarGainMode::Off); + } // namespace geopro::data #endif // GEOPRO_DATA_GPRVOLUMEREPOSITORY_HPP diff --git a/src/data/api/Api3dRepository.cpp b/src/data/api/Api3dRepository.cpp index 5e16bac..5c4649c 100644 --- a/src/data/api/Api3dRepository.cpp +++ b/src/data/api/Api3dRepository.cpp @@ -29,6 +29,22 @@ namespace geopro::data { namespace { constexpr const char* kNotReady = "后端三维端点未就绪"; + +// 雷达中性灰度色阶。⚠ core::ColorScale 是【阶梯/分段常数】(colorAt 取下界 stop,不插值): +// 只放 3 个 stop(黑/灰/白) → 整个 [mid,vmax) 正振幅全塌成一级灰、[vmin,mid) 全黑,连续 GPR +// 数据被压成 3 级(深部 DC 渲成恒灰、丢层理)——实测确诊的真 bug。故铺 256 级平滑斜坡 +// black→white,colorAt 才能给出连续灰阶。(反演等值面仍用稀疏 stop 的阶梯,故意离散,不动。) +core::ColorScale radarGrayScale(double vmin, double vmax) { + core::ColorScale cs; + constexpr int kLevels = 256; + for (int i = 0; i < kLevels; ++i) { + const double f = static_cast(i) / (kLevels - 1); // 0..1 + const double val = vmin + (vmax - vmin) * f; + const auto g = static_cast(std::lround(f * 255.0)); + cs.addStop(val, core::Rgba{g, g, g, 255}); + } + return cs; +} } // namespace Api3dRepository::Api3dRepository(IAsyncDatasetRepository& dsRepo, @@ -40,7 +56,7 @@ DsDimension Api3dRepository::dimensionOf(const DsRow& ds) const { // TODO(P3): 与 LocalSample3dRepository 重复,宜提取共享映射(后续清理)。 const std::string& c = ds.ddCode; if (c == "dd_voxel" || c == "dd_Structual3D" || c == "dd_Property3D" || c == "dd_section" || - c == "dd_inversion_data") { + c == "dd_inversion_data" || c == "dd_radar_3d") { return DsDimension::Dim3D; } if (c == "dd_slice") return DsDimension::Analysis3D; @@ -130,12 +146,8 @@ std::string Api3dRepository::createGprVolume(const std::string& lineDir, const std::string& name, int coarse) { // 走 io::gpr 逐线管线(含线内通道插值)直接产体(抛异常透传给调用方)。 VolumeGrid grid = geopro::data::createGprVolumeGrid(lineDir, linePrefix, coarse); - // 简易灰度色阶(负→暗、零→灰、正→亮)覆盖体值域,使体素渲染可见。 - core::ColorScale scale; - const double mid = 0.5 * (grid.vmin + grid.vmax); - scale.addStop(grid.vmin, core::Rgba{20, 24, 40, 255}); - scale.addStop(mid, core::Rgba{140, 140, 150, 255}); - scale.addStop(grid.vmax, core::Rgba{235, 232, 220, 255}); + // 中性灰度(GPR 标准 B-scan),256 级连续——3-stop 会被 colorAt 阶梯压成 3 级(见 radarGrayScale)。 + const core::ColorScale scale = radarGrayScale(grid.vmin, grid.vmax); const std::string id = "vol-" + std::to_string(++volumeCounter_); StoredVolume sv; @@ -148,6 +160,34 @@ std::string Api3dRepository::createGprVolume(const std::string& lineDir, return id; } +std::string Api3dRepository::registerRadarDataset(const std::string& lineDir, + const std::string& linePrefix, + const std::string& name, + const std::string& structParentId, int coarse) { + // 只存元数据、不建体(懒建在首次 loadVolume 后台线程做并缓存)→ DS 优先、勾选才付出建体成本。 + 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; +} + +bool Api3dRepository::setRadarGainMode(const std::string& dsId, RadarGainMode mode) { + auto it = volumes_.find(dsId); + if (it == volumes_.end() || it->second.ddCode != "dd_radar_3d") return false; + if (it->second.gainMode == mode) return true; // 未变化也算成功(调用方可跳过重渲) + it->second.gainMode = mode; + it->second.cachedGrid.reset(); // 失效缓存体 → 下次 loadVolume 用新增益模式重建 + return true; +} + const VoxelGenerateRequest* Api3dRepository::lastVoxelRequest(const std::string& dsId) const { const auto it = volumes_.find(dsId); return (it != volumes_.end() && it->second.request) ? &*it->second.request : nullptr; @@ -167,9 +207,10 @@ std::vector Api3dRepository::volumeRows() const { DsRow r; r.id = id; r.dsName = sv.name; - r.ddCode = "dd_voxel"; + r.ddCode = sv.ddCode; // 雷达体="dd_radar_3d",其余 dd_voxel r.typeName = "三维体"; - r.structParentId = sv.request ? sv.request->structParentId : std::string(); // 结构归属(生成位置) + // 结构归属(生成位置):mock 请求体路径取 request->structParentId,雷达体路径取 sv.structParentId。 + r.structParentId = sv.request ? sv.request->structParentId : sv.structParentId; r.createTime = sv.createTime; rows.push_back(std::move(r)); } @@ -348,6 +389,51 @@ void Api3dRepository::loadVolume(const std::string& dsId, onOk(*sv.cachedGrid, sv.cachedScale); return; } + + if (!sv.linePrefix.empty()) { // 雷达体 DS:后台建体,避免阻塞 UI(与 finalizeVolume 同范式) + const std::string lineDir = sv.lineDir, linePrefix = sv.linePrefix; + const int coarse = sv.coarse; + const RadarGainMode gainMode = sv.gainMode; // 显示增益模式(右键可切,切时清缓存重建) + auto deliver = [this, dsId, onOk, onErr](std::shared_ptr g, std::string err) { + if (!g) { + onErr("Api3dRepository::loadVolume(radar): " + err); + return; + } + // 中性灰度,256 级连续(见 radarGrayScale:3-stop 会被 colorAt 阶梯压成 3 级)。 + const core::ColorScale scale = radarGrayScale(g->vmin, g->vmax); + 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, gainMode]() { + std::shared_ptr g; + std::string err; + try { + g = std::make_shared(geopro::data::createRadarVolumeGrid( + lineDir, linePrefix, coarse, /*targetDy=*/0.025, gainMode)); + } 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); // 具名变量(非结构化绑定)→ C++17 可被 lambda 捕获 + auto err = std::get<1>(r); + QMetaObject::invokeMethod( + qApp, [deliver, g, err]() mutable { deliver(std::move(g), std::move(err)); }, + Qt::QueuedConnection); + }).detach(); + return; + } + const VolumeBuildParams params = sv.params; // 拷贝:异步回调期间存储可能变动 if (params.sourceDatasetIds.empty()) { onErr("Api3dRepository::loadVolume: 三维体无源数据集"); diff --git a/src/data/api/Api3dRepository.hpp b/src/data/api/Api3dRepository.hpp index 6fe5416..e5df097 100644 --- a/src/data/api/Api3dRepository.hpp +++ b/src/data/api/Api3dRepository.hpp @@ -9,6 +9,7 @@ #include "dto/Vtk3dRequests.hpp" #include "geo/GeoLocalFrame.hpp" +#include "GprVolumeRepository.hpp" // RadarGainMode(雷达显示增益模式) #include "repo/I3dSceneRepository.hpp" #include "repo/VolumeBuildParams.hpp" @@ -48,6 +49,15 @@ public: // 返回新 dsId;失败抛 std::runtime_error(加载/立方体空,由 io::gpr 链透传)。 std::string createGprVolume(const std::string& lineDir, const std::string& linePrefix, const std::string& name, int coarse = 8); + // 登记一条规范化测线为 dd_radar_3d 体 DS:只存元数据(lineDir/prefix/coarse),【不立即建体】。 + // 首次 loadVolume 时在后台线程惰性建体并缓存(仿 finalizeVolume,避免阻塞 UI)。 + // id="radar-N";structParentId=结构归属(生成位置)。返回新 dsId。 + std::string registerRadarDataset(const std::string& lineDir, const std::string& linePrefix, + const std::string& name, const std::string& structParentId, + int coarse = 4); + // 切换雷达体显示增益模式(右键菜单):设新模式 + 清缓存体(强制下次 loadVolume 用新模式重建)。 + // 返回 true=是雷达体且已切换;false=非雷达体(忽略)。调用方随后清控制器缓存并重渲该 dsId。 + bool setRadarGainMode(const std::string& dsId, RadarGainMode mode); // 取回某三维体组装的请求体(测试/联调);非本 repo 创建或无 request 时返回 nullptr。 const VoxelGenerateRequest* lastVoxelRequest(const std::string& dsId) const; // 清空内存态三维体/切片/异常(切换项目时调;否则上个项目的体/切片/异常残留在新项目列表)。 @@ -144,6 +154,12 @@ private: std::optional pointCount; // 聚合散点数(finalizeVolume 时持久化,详情统计用) std::optional request; // 组装的真实请求体(createVolume(req) 路径填充) std::string createTime; // 创建时刻(mock,列表副标题/详情用) + // 雷达体 DS(registerRadarDataset 路径):只存元数据,loadVolume 懒建。 + std::string lineDir, linePrefix; // 规范化测线 .head/.data 所在目录 + 前缀 + std::string ddCode = "dd_voxel"; // 数据字典码(雷达体="dd_radar_3d",其余默认 dd_voxel) + std::string structParentId; // 结构归属(生成位置);雷达体无 request 时由此提供 + int coarse = 4; // 沿测线下采样因子(控内存) + RadarGainMode gainMode = RadarGainMode::Agc; // 显示增益模式(右键切换;默认 AGC 显深部) }; std::map volumes_; // dsId → 体 int volumeCounter_ = 0; diff --git a/src/data/repo/LocalSample3dRepository.cpp b/src/data/repo/LocalSample3dRepository.cpp index 096c212..17f3a7b 100644 --- a/src/data/repo/LocalSample3dRepository.cpp +++ b/src/data/repo/LocalSample3dRepository.cpp @@ -44,7 +44,7 @@ DsDimension LocalSample3dRepository::dimensionOf(const DsRow& ds) const { const std::string& c = ds.ddCode; // 真三维体 / 体素 / 帘面(dd_section/反演剖面摆成竖直帘面)入三维数据集。 if (c == "dd_voxel" || c == "dd_Structual3D" || c == "dd_Property3D" || c == "dd_section" || - c == "dd_inversion_data") { + c == "dd_inversion_data" || c == "dd_radar_3d") { return DsDimension::Dim3D; } // 切片:三维分析栏。 diff --git a/src/io/gpr/CMakeLists.txt b/src/io/gpr/CMakeLists.txt index 4afd503..0470637 100644 --- a/src/io/gpr/CMakeLists.txt +++ b/src/io/gpr/CMakeLists.txt @@ -1,6 +1,7 @@ add_library(geopro_io_gpr STATIC IprHeader.cpp IprbReader.cpp GprGeometry.cpp GprSurveyAssembler.cpp - GpsTrack.cpp) + GpsTrack.cpp NormalizedRadarReader.cpp + RadarVolumeAssembler.cpp NormalizedRadarVolumeBridge.cpp) target_include_directories(geopro_io_gpr PUBLIC ${CMAKE_SOURCE_DIR}/src) target_compile_features(geopro_io_gpr PUBLIC cxx_std_17) # GprSurveyAssembler 返回 geopro::core::GprSurvey(头文件内联,仅需 include 解析)。 diff --git a/src/io/gpr/Gpr3dvVolumeBridge.cpp b/src/io/gpr/Gpr3dvVolumeBridge.cpp index e3b7bcf..a6dceaf 100644 --- a/src/io/gpr/Gpr3dvVolumeBridge.cpp +++ b/src/io/gpr/Gpr3dvVolumeBridge.cpp @@ -1,8 +1,8 @@ #include "io/gpr/Gpr3dvVolumeBridge.hpp" +#include #include #include -#include #include #include @@ -13,7 +13,7 @@ #include "RadarProcessor.h" #include "core/model/ScalarVolumeI16.hpp" -#include "io/gpr/GprGeometry.hpp" // planChannelInterpolation +#include "io/gpr/RadarVolumeAssembler.hpp" // assembleRadarVolume(共享建体) namespace geopro::io::gpr { @@ -71,7 +71,6 @@ geopro::core::BuiltI16 buildLineVolumeFromGpr3dv(const std::string& lineDir, const std::string& linePrefix, BridgeMetrics* metricsOut, int coarse, double targetDy) { - const int stride = coarse > 1 ? coarse : 1; // 沿测线下采样步长(≥1) const QString dir = QString::fromLocal8Bit(lineDir.c_str()); const QString base = QString::fromLocal8Bit(linePrefix.c_str()); @@ -107,106 +106,48 @@ geopro::core::BuiltI16 buildLineVolumeFromGpr3dv(const std::string& lineDir, std::to_string(traces) + "/" + std::to_string(samples) + ")"); } - // 下采样后输出道数(向上取整保留末道附近):nxOut = ceil(traces/stride)。 - const int nxOut = (traces + stride - 1) / stride; - const int nx = nxOut; // X=道(沿测线,已按 stride 下采样) - const int nz = samples; // Z=样本(深度) - - // §1 线内通道插值:读各通道真实横向偏移(header.chXOffsets) → 规则网格化 Y 到 targetDy。 - // 绝不跨线;间距/通道数从数据来,不假设。退路(无偏移/未启用)= 逐通道 identity。 + // §1 线内通道插值偏移:读各通道真实横向偏移(header.chXOffsets)。绝不跨线;间距/ + // 通道数从数据来,不假设。空/不等长 → 不启用通道插值(helper 内退路=逐通道 identity)。 std::vector latOff; const auto& chx = processed.header.chXOffsets; if (chx.size() == channels) for (int c = 0; c < channels; ++c) latOff.push_back(static_cast(chx[c])); - std::vector rows; - bool interpolated = false; - if (static_cast(latOff.size()) == channels && targetDy > 0.0) { - rows = planChannelInterpolation(latOff, targetDy); - interpolated = (static_cast(rows.size()) != channels); - } - if (rows.empty()) - for (int c = 0; c < channels; ++c) rows.push_back({c, c, 0.0}); - const int ny = static_cast(rows.size()); // Y=通道(横向,可能已插值加密) - // 3) 扫处理后值域 → Quant(offset=中点,防溢出)。 + // 3) 组立方体描述 + 采样器(volumeData[通道][道][样本],越界取 0),调共享建体 helper。 + // 扫值域→Quant(中点 offset)→通道插值(规则化 Y 到 targetDy)→逐体素填→spacing 均在 + // helper 内完成(与后续规范化 .head/.data 路共用同一逻辑,零漂移)。 + const GPRDataModel::Header& h = processed.header; + RadarCubeDesc desc; + desc.channels = channels; + desc.traces = traces; + desc.samples = samples; + desc.chXOffsets = latOff; + desc.dxBase = h.distanceInc > 1e-9 ? h.distanceInc : 1.0; // 道距(米) + desc.dyWhenNotInterpolated = channelSpacingY(h, channels); // 原通道横距(米) + desc.dz = depthSpacingZ(h); // 深度采样距(米) + CubeSampler sample = [&processed](int c, int t, int s) -> double { + const auto& ch = processed.volumeData[c]; + if (t >= static_cast(ch.size())) return 0.0; + const auto& tr = ch[t]; + return s < static_cast(tr.size()) ? static_cast(tr[s]) : 0.0; + }; + const auto tFill = std::chrono::steady_clock::now(); - short rawMin = std::numeric_limits::max(); - short rawMax = std::numeric_limits::min(); - for (int c = 0; c < channels; ++c) { - const auto& chData = processed.volumeData[c]; - for (int t = 0; t < traces && t < chData.size(); ++t) { - const auto& trData = chData[t]; - for (int s = 0; s < samples && s < trData.size(); ++s) { - const short v = trData[s]; - if (v < rawMin) rawMin = v; - if (v > rawMax) rawMax = v; - } - } - } - if (rawMin > rawMax) { // 退化(理论不至):置零区间。 - rawMin = 0; - rawMax = 0; - } - const double vmin = static_cast(rawMin); - const double vmax = static_cast(rawMax); - - geopro::core::Quant quant; - // 量化到 int16 有效区间 [-32767, 32767](kBlank=-32768 保留),留两端裕度用 64000。 - quant.scale = (vmax > vmin) ? (vmax - vmin) / 64000.0 : 1.0; - quant.offset = 0.5 * (vmin + vmax); // 中点 → 防溢出 - - // 4) 逐 (输出行 j, trace, sample) 填体。每个输出行 = 两侧最近真实通道线性插值 - // (a==b 时即原通道)。GPR 立方体稠密(每体素有值),无空洞 → 不置 kBlank。 - // 沿测线按 stride 下采样:输出道 to → 源道 t = to*stride。 - geopro::core::BuiltI16 built; - built.vol = geopro::core::ScalarVolumeI16(nx, ny, nz); - for (int j = 0; j < ny; ++j) { - const auto& chA = processed.volumeData[rows[j].a]; - const auto& chB = processed.volumeData[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; - const bool hasA = t < static_cast(chA.size()); - const bool hasB = t < static_cast(chB.size()); - for (int s = 0; s < samples; ++s) { - const double va = - (hasA && s < static_cast(chA[t].size())) ? chA[t][s] : 0.0; - const double vb = - (hasB && s < static_cast(chB[t].size())) ? chB[t][s] : 0.0; - // X=输出道 to、Y=输出行 j、Z=样本 s。 - built.vol.at(to, j, s) = quant.toQ(wa * va + wb * vb); - } - } - } + geopro::core::BuiltI16 built = assembleRadarVolume(desc, sample, coarse, targetDy); const double fillMs = nowMs(tFill); - // 5) 几何:origin=0,spacing 按 X=道距 / Y=通道横距 / Z=深度采样距。 - const GPRDataModel::Header& h = processed.header; - // 下采样后相邻输出道在世界中跨 stride 个原始道距 → dx ×stride 保持真实尺度。 - const double dxBase = h.distanceInc > 1e-9 ? h.distanceInc : 1.0; - const double dx = dxBase * stride; - // 插值后 Y 已规则化到 targetDy 网格;否则用原通道横距。 - const double dy = interpolated ? targetDy : channelSpacingY(h, channels); - const double dz = depthSpacingZ(h); - - built.quant = quant; - built.origin = {0.0, 0.0, 0.0}; - built.spacing = {dx, dy, dz}; - built.vminPhys = vmin; - built.vmaxPhys = vmax; - if (metricsOut) { - metricsOut->nx = nx; - metricsOut->ny = ny; - metricsOut->nz = nz; + metricsOut->nx = built.vol.nx(); + metricsOut->ny = built.vol.ny(); + metricsOut->nz = built.vol.nz(); metricsOut->meanAbsBefore = meanBefore; metricsOut->meanAbsAfter = meanAfter; - metricsOut->vminPhys = vmin; - metricsOut->vmaxPhys = vmax; - metricsOut->dx = dx; - metricsOut->dy = dy; - metricsOut->dz = dz; + metricsOut->vminPhys = built.vminPhys; + metricsOut->vmaxPhys = built.vmaxPhys; + metricsOut->dx = built.spacing[0]; + metricsOut->dy = built.spacing[1]; + metricsOut->dz = built.spacing[2]; metricsOut->loadMs = loadMs; metricsOut->pipelineMs = pipelineMs; metricsOut->fillMs = fillMs; diff --git a/src/io/gpr/NormalizedRadarReader.cpp b/src/io/gpr/NormalizedRadarReader.cpp new file mode 100644 index 0000000..a3d360f --- /dev/null +++ b/src/io/gpr/NormalizedRadarReader.cpp @@ -0,0 +1,107 @@ +#include "io/gpr/NormalizedRadarReader.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +namespace geopro::io::gpr { +namespace { +std::map parseKv(const std::string& text) { + std::map 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& 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& 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(h.lastTrace / h.channels); + h.bits = static_cast(optD(kv, "BITS", 16)); + h.endianType = static_cast(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; +} +std::vector 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(h.lastTrace) * h.samples; + const std::uintmax_t expect = static_cast(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 cube(n); + std::ifstream f(dataPath, std::ios::binary); + if (!f) throw std::runtime_error("打开 .data 失败: " + dataPath); + f.read(reinterpret_cast(cube.data()), static_cast(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(v); + v = static_cast((u >> 8) | (u << 8)); + } + return cube; +} +std::vector parseRadarCor(const std::string& corText) { + std::vector 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; +} +} // namespace geopro::io::gpr diff --git a/src/io/gpr/NormalizedRadarReader.hpp b/src/io/gpr/NormalizedRadarReader.hpp new file mode 100644 index 0000000..d6fab03 --- /dev/null +++ b/src/io/gpr/NormalizedRadarReader.hpp @@ -0,0 +1,49 @@ +#ifndef GEOPRO_IO_GPR_NORMALIZED_RADAR_READER_HPP +#define GEOPRO_IO_GPR_NORMALIZED_RADAR_READER_HPP + +#include +#include +#include + +namespace geopro::io::gpr { + +// 规范化雷达 .head(KEY:VALUE 文本头,每行一项)解析结果。 +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 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); + +// 读规范化 .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 readRadarDataCube(const std::string& dataPath, + const RadarHeader& h); + +// 规范化 .cor 轨迹单点:序号/纬度/经度/高程/解状态。 +struct TracePos { int index = 0; double lat = 0, lon = 0, elev = 0; int solution = 0; }; + +// 解析 .cor:跳过 "VERSION:" 行,每行 [序号 纬度 N/S 经度 E/W 高程 M 解状态] +// (制表/空格分隔)。本期仅解析,为 P1 多线配准预留。 +std::vector parseRadarCor(const std::string& corText); + +} // namespace geopro::io::gpr + +#endif // GEOPRO_IO_GPR_NORMALIZED_RADAR_READER_HPP diff --git a/src/io/gpr/NormalizedRadarVolumeBridge.cpp b/src/io/gpr/NormalizedRadarVolumeBridge.cpp new file mode 100644 index 0000000..ca021d6 --- /dev/null +++ b/src/io/gpr/NormalizedRadarVolumeBridge.cpp @@ -0,0 +1,55 @@ +#include "io/gpr/NormalizedRadarVolumeBridge.hpp" + +#include +#include +#include +#include +#include +#include + +#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 cube = readRadarDataCube(data, h); + const int M = h.channels; + const int 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 && M > 1) + ? (h.chXOffsets.back() - h.chXOffsets.front()) / (M - 1) + : 1.0; + d.dz = depthSpacingZ(h) > 1e-12 ? depthSpacingZ(h) : 1.0; + + // position-major 立方体索引 ((t*M + c)*N + s),与 readRadarDataCube 一致。 + CubeSampler sample = [&cube, M, N](int c, int t, int s) { + return static_cast(cube[(static_cast(t) * M + c) * N + s]); + }; + + return assembleRadarVolume(d, sample, coarse, targetDy); +} + +} // namespace geopro::io::gpr diff --git a/src/io/gpr/NormalizedRadarVolumeBridge.hpp b/src/io/gpr/NormalizedRadarVolumeBridge.hpp new file mode 100644 index 0000000..128e89f --- /dev/null +++ b/src/io/gpr/NormalizedRadarVolumeBridge.hpp @@ -0,0 +1,20 @@ +#ifndef GEOPRO_IO_GPR_NORMALIZED_RADAR_VOLUME_BRIDGE_HPP +#define GEOPRO_IO_GPR_NORMALIZED_RADAR_VOLUME_BRIDGE_HPP + +#include + +#include "core/algo/GprVolumeBuilder.hpp" // geopro::core::BuiltI16 + +namespace geopro::io::gpr { + +// 读 {lineDir}/{linePrefix}.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); + +} // namespace geopro::io::gpr + +#endif // GEOPRO_IO_GPR_NORMALIZED_RADAR_VOLUME_BRIDGE_HPP diff --git a/src/io/gpr/RadarVolumeAssembler.cpp b/src/io/gpr/RadarVolumeAssembler.cpp new file mode 100644 index 0000000..9ccc1c6 --- /dev/null +++ b/src/io/gpr/RadarVolumeAssembler.cpp @@ -0,0 +1,68 @@ +#include "io/gpr/RadarVolumeAssembler.hpp" +#include +#include +#include +#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 rows; + bool interpolated = false; + if (static_cast(d.chXOffsets.size()) == d.channels && targetDy > 0.0) { + rows = planChannelInterpolation(d.chXOffsets, targetDy); + interpolated = (static_cast(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(rows.size()); + + // 扫值域 → Quant(中点 offset, 64000 裕度)。 + double vmin = std::numeric_limits::infinity(); + double vmax = -std::numeric_limits::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 diff --git a/src/io/gpr/RadarVolumeAssembler.hpp b/src/io/gpr/RadarVolumeAssembler.hpp new file mode 100644 index 0000000..676ebb3 --- /dev/null +++ b/src/io/gpr/RadarVolumeAssembler.hpp @@ -0,0 +1,17 @@ +#ifndef GEOPRO_IO_GPR_RADARVOLUMEASSEMBLER_HPP +#define GEOPRO_IO_GPR_RADARVOLUMEASSEMBLER_HPP +#include +#include +#include "core/algo/GprVolumeBuilder.hpp" +namespace geopro::io::gpr { +struct RadarCubeDesc { + int channels = 0; int traces = 0; int samples = 0; + std::vector chXOffsets; + double dxBase = 1.0; double dyWhenNotInterpolated = 1.0; double dz = 1.0; +}; +using CubeSampler = std::function; +geopro::core::BuiltI16 assembleRadarVolume(const RadarCubeDesc& desc, + const CubeSampler& sample, + int coarse, double targetDy); +} // namespace geopro::io::gpr +#endif diff --git a/src/render/actors/VoxelActor.cpp b/src/render/actors/VoxelActor.cpp index aeb43d2..eaa8d38 100644 --- a/src/render/actors/VoxelActor.cpp +++ b/src/render/actors/VoxelActor.cpp @@ -73,9 +73,25 @@ vtkSmartPointer assembleVolume(vtkImageData* img, std::min({std::abs(sp[0]), std::abs(sp[1]), std::abs(sp[2])}); // 最细体素维度 double bnd[6]; img->GetBounds(bnd); - const double diag = std::sqrt((bnd[1] - bnd[0]) * (bnd[1] - bnd[0]) + - (bnd[3] - bnd[2]) * (bnd[3] - bnd[2]) + - (bnd[5] - bnd[4]) * (bnd[5] - bnd[4])); // 包围盒对角(最长穿越路径) + const double ex = std::abs(bnd[1] - bnd[0]), ey = std::abs(bnd[3] - bnd[2]), + ez = std::abs(bnd[5] - bnd[4]); + const double diag = std::sqrt(ex * ex + ey * ey + ez * ez); + // 不透明度单位距离的尺度基准: + // - 【各向异性】体(细长,如雷达 375×1.4×5m):对角线被长轴主宰(375m)→ 单位距离过大 → + // 不透明度只在 100% 才实心、稍降即很透(用户实测)。改用【特征尺度=三轴几何平均 cbrt】, + // 对各向异性稳健。 + // - 【近立方】体(反演):维持原对角线,观感不变(门控:长短轴比 ≤ kAnisoRatio 走对角线)。 + constexpr double kAnisoRatio = 4.0; + const double maxE = std::max({ex, ey, ez}), minE = std::min({ex, ey, ez}); + const bool anisotropic = (minE > 0.0) && (maxE / minE > kAnisoRatio); + const double charLen = anisotropic ? std::cbrt(ex * ey * ez) : diag; + + // 大体(如雷达:24M 体素、深度采样距 mm 级 → 单条光线上千采样步)开启【交互期】采样距自适应: + // 旋转时 VTK 自动加大采样步(变粗)保流畅,停手即恢复设定的细采样距(0.3×minSp)出全质量帧。 + // 只是渲染期降采样、【绝不动数据】;切片/异常取自全分辨率体,保真不受影响。 + // 小体(反演,实测~7ms/帧)保持全程全质量,避免"停手补高清"的视觉突跳。 + constexpr vtkIdType kInteractiveLodVoxels = 4'000'000; + const int interactiveAdjust = (img->GetNumberOfPoints() > kInteractiveLodVoxels) ? 1 : 0; vtkSmartPointer mapper; if (mask && g_gpuVolumeSupported) { @@ -84,8 +100,8 @@ vtkSmartPointer assembleVolume(vtkImageData* img, gpu->SetInputData(img); gpu->SetMaskInput(mask); gpu->SetMaskTypeToBinary(); - gpu->SetAutoAdjustSampleDistances(0); // 全程全质量(GPU 直接 mapper 无交互降采样开关) - // 关了自适应必须显式给【细】采样距离,否则用粗默认值 → 看到一层层体素(分层伪影)。 + gpu->SetAutoAdjustSampleDistances(interactiveAdjust); // 大体:交互降采样保流畅,停手全质量;小体:全程全质量 + // 静止帧用【细】采样距离(0.3×minSp):否则用粗默认值 → 看到一层层体素(分层伪影)。 if (minSp > 0) gpu->SetSampleDistance(static_cast(0.3 * minSp)); // 抖动:用噪声纹理微扰每条光线的采样起点,消除规则采样面造成的「木纹/分层」伪影(VTK 官方此用途)。 gpu->SetUseJittering(1); @@ -94,9 +110,9 @@ vtkSmartPointer assembleVolume(vtkImageData* img, // SmartVolumeMapper:有 GPU 走 GPU ray cast,否则自动回退 CPU,避免无 GPU 时卡死/失败。 vtkNew sm; sm->SetInputData(img); - // 全程统一全质量(GPU 足够快, 实测 ~7ms/帧):关掉交互降采样, 避免"停手补高清"那一帧突跳停顿。 - sm->SetAutoAdjustSampleDistances(0); - sm->SetInteractiveAdjustSampleDistances(0); + // 大体交互降采样保流畅(停手恢复全质量);小体全程全质量(GPU 足够快, 实测 ~7ms/帧)避免突跳。 + sm->SetAutoAdjustSampleDistances(interactiveAdjust); + sm->SetInteractiveAdjustSampleDistances(interactiveAdjust); mapper = sm; } @@ -105,11 +121,10 @@ vtkSmartPointer assembleVolume(vtkImageData* img, prop->SetScalarOpacity(opacity); prop->SetInterpolationTypeToLinear(); prop->ShadeOff(); - // 不透明度单位距离 = 包围盒对角 × kOpacityUnitFraction:控制沿深度的累积速度,使色阶「不透明度」滑块 - // 有层次。取对角/10:100%(每单位=1.0)→沿体累积到≈实心、10% 很淡。太大(=整条对角)→100% 也偏透; - // 太小(=体素)→ 低不透明度也累积到全不透明。 + // 不透明度单位距离 = 尺度基准(charLen:各向异性=特征尺度 / 近立方=对角线) × kOpacityUnitFraction。 + // 控制累积速度使「不透明度」滑块有层次;细长体走特征尺度后不再"只有 100% 实心、99% 即很透"。 constexpr double kOpacityUnitFraction = 0.1; - if (diag > 0) prop->SetScalarOpacityUnitDistance(kOpacityUnitFraction * diag); + if (charLen > 0) prop->SetScalarOpacityUnitDistance(kOpacityUnitFraction * charLen); auto volume = vtkSmartPointer::New(); volume->SetMapper(mapper); diff --git a/src/render/interact/InteractionManager.cpp b/src/render/interact/InteractionManager.cpp index 0fd34d1..35b11d8 100644 --- a/src/render/interact/InteractionManager.cpp +++ b/src/render/interact/InteractionManager.cpp @@ -58,11 +58,22 @@ void InteractionManager::installStyle() { style_->onPick = [this](const Vec3& w) { onPicked(w); }; style_->onDoubleClick = [this](const Vec3& w) { onDoubleClicked(w); }; style_->onWheelStep = [this](int dir) { return onWheel(dir); }; + // Esc 取消选中:清选中+高亮 + 同步列表清选 + 重渲(拉近后点不到空白处取消时的可靠出口)。 + style_->onDeselect = [this]() { + if (selected_ < 0) return; + deselectSlice(); + if (onSliceSelectionChanged) onSliceSelectionChanged(std::string{}); + safeRender(); + }; + // 精确"命中切片"判定:光标射线 vs 切片真实矩形求交(点帘面/非切片物/边界外都不算)。 + style_->hitTestSlice = [this]() { return pointOnSlice(); }; // D39: 提供旋转中心 = 选中切片中心(有选中→true)。style 在按下拖动时据此绕选中切片旋转。 style_->getRotateCenter = [this](Vec3& c) { - if (selected_ < 0 || selected_ >= static_cast(slices_.size())) return false; - c = slices_[static_cast(selected_)]->center(); - return true; + if (selected_ >= 0 && selected_ < static_cast(slices_.size())) { + c = slices_[static_cast(selected_)]->center(); // 选中切片→绕其中心 + return true; + } + return rayVolumePivot(c); // 无选中→绕光标射线穿过的体中段点(不甩飞);无体命中→false(默认焦点) }; interactor_->SetInteractorStyle(style_); @@ -83,6 +94,8 @@ void InteractionManager::uninstallStyle() { style_->onDoubleClick = nullptr; style_->onWheelStep = nullptr; style_->getRotateCenter = nullptr; + style_->onDeselect = nullptr; + style_->hitTestSlice = nullptr; } // 摘除右键观察者(this 即将析构)。 if (interactor_ && rightBtnTag_ != 0) { @@ -470,6 +483,114 @@ int InteractionManager::nearestSlice(const Vec3& worldPoint) const { return idx; } +bool InteractionManager::cursorRay(double nearP[3], double dir[3]) const { + if (!interactor_ || !renderer_) return false; + const int* pos = interactor_->GetEventPosition(); + if (!pos) return false; + // 屏幕点在近/远裁剪面的世界坐标 → 连成视线(相机透视/正交均适用)。 + auto toWorld = [this](int x, int y, double z, double out[3]) { + renderer_->SetDisplayPoint(x, y, z); + renderer_->DisplayToWorld(); + double w[4]; + renderer_->GetWorldPoint(w); + const double iw = (w[3] != 0.0) ? 1.0 / w[3] : 1.0; + out[0] = w[0] * iw; + out[1] = w[1] * iw; + out[2] = w[2] * iw; + }; + double farP[3]; + toWorld(pos[0], pos[1], 0.0, nearP); + toWorld(pos[0], pos[1], 1.0, farP); + dir[0] = farP[0] - nearP[0]; + dir[1] = farP[1] - nearP[1]; + dir[2] = farP[2] - nearP[2]; + return true; +} + +bool InteractionManager::rayVolumePivot(Vec3& out) const { + double nearP[3], d[3]; + if (!cursorRay(nearP, d)) return false; + bool found = false; + double bestEnter = 0.0; + for (const auto& kv : volumes_) { + if (!kv.second.image) continue; + const std::array b = imageBounds(kv.second.image); + double tEnter = -1e300, tExit = 1e300; // slab 法求 ray∩包围盒 [tEnter,tExit] + bool ok = true; + for (int i = 0; i < 3; ++i) { + const double lo = b[2 * i], hi = b[2 * i + 1]; + if (std::abs(d[i]) < 1e-12) { + if (nearP[i] < lo || nearP[i] > hi) { + ok = false; + break; + } + } else { + double t1 = (lo - nearP[i]) / d[i], t2 = (hi - nearP[i]) / d[i]; + if (t1 > t2) { + const double tmp = t1; + t1 = t2; + t2 = tmp; + } + if (t1 > tEnter) tEnter = t1; + if (t2 < tExit) tExit = t2; + } + } + if (!ok || tExit < tEnter || tExit < 0.0) continue; + const double tin = (tEnter > 0.0 ? tEnter : 0.0); + if (!found || tin < bestEnter) { // 多体取最近命中 + bestEnter = tin; + const double tmid = 0.5 * (tin + tExit); // 体内中段点 → 稳定支点 + out = {nearP[0] + d[0] * tmid, nearP[1] + d[1] * tmid, nearP[2] + d[2] * tmid}; + found = true; + } + } + return found; +} + +bool InteractionManager::pointOnSlice() const { + double nearP[3], d[3]; + if (!cursorRay(nearP, d)) return false; + for (const auto& sp : slices_) { + double o[3], p1[3], p2[3]; + sp->planePoints(o, p1, p2); // 切片真实矩形:o + u·e1 + v·e2 (u,v∈[0,1]) + const double e1[3] = {p1[0] - o[0], p1[1] - o[1], p1[2] - o[2]}; + const double e2[3] = {p2[0] - o[0], p2[1] - o[1], p2[2] - o[2]}; + const double n[3] = {e1[1] * e2[2] - e1[2] * e2[1], e1[2] * e2[0] - e1[0] * e2[2], + e1[0] * e2[1] - e1[1] * e2[0]}; + const double denom = d[0] * n[0] + d[1] * n[1] + d[2] * n[2]; + if (std::abs(denom) < 1e-12) continue; // 视线平行于切面 + const double t = + ((o[0] - nearP[0]) * n[0] + (o[1] - nearP[1]) * n[1] + (o[2] - nearP[2]) * n[2]) / denom; + if (t < 0.0) continue; // 交点在相机后方 + const double h[3] = {nearP[0] + d[0] * t, nearP[1] + d[1] * t, nearP[2] + d[2] * t}; + const double hd[3] = {h[0] - o[0], h[1] - o[1], h[2] - o[2]}; + const double e1sq = e1[0] * e1[0] + e1[1] * e1[1] + e1[2] * e1[2]; + const double e2sq = e2[0] * e2[0] + e2[1] * e2[1] + e2[2] * e2[2]; + if (e1sq <= 0.0 || e2sq <= 0.0) continue; + const double u = (hd[0] * e1[0] + hd[1] * e1[1] + hd[2] * e1[2]) / e1sq; + const double v = (hd[0] * e2[0] + hd[1] * e2[1] + hd[2] * e2[2]) / e2sq; + if (u < 0.0 || u > 1.0 || v < 0.0 || v > 1.0) continue; // 不在切片矩形内 + // 矩形命中后还需该点处切片有【可见数据】(不透明)——切片矩形=体网格截面,常比可见数据大 + // (反演有大片空值网格、雷达边缘空采样)。落在矩形但透明(空值/外区)处不算"在切片上", + // 否则点 ds 边缘空白区(如截图左侧标尺处)仍误判命中 → 治用户实测的外扩。 + const vtkSmartPointer rgba = sp->coloredResliceImage(); + if (!rgba) return true; // 无着色图(理论不至) → 退化为矩形命中 + int dims[3]; + rgba->GetDimensions(dims); + if (dims[0] < 1 || dims[1] < 1 || rgba->GetNumberOfScalarComponents() < 4) + return true; // 无 alpha 通道 → 退化为矩形命中 + // 着色输出像素 (i,j) 沿 e1/e2 方向 → (u,v)·(dim-1)(vtkImagePlaneWidget reslice 轴约定)。 + int px = static_cast(u * (dims[0] - 1) + 0.5); + int py = static_cast(v * (dims[1] - 1) + 0.5); + px = px < 0 ? 0 : (px > dims[0] - 1 ? dims[0] - 1 : px); + py = py < 0 ? 0 : (py > dims[1] - 1 ? dims[1] - 1 : py); + const auto* pix = static_cast(rgba->GetScalarPointer(px, py, 0)); + if (pix && pix[3] > 10) return true; // alpha>阈值 = 该点切片可见 → 在切片上 + // 透明(空值/外区) → 不算在此切片上,继续看其它切片 + } + return false; +} + void InteractionManager::onPicked(const Vec3& worldPoint) { // 单击 = 选中命中切片;点在切片外(如点到体/帘面)→ 取消选中(idx=-1)。**不动相机**。 // 解决"选了切片无法取消":点击切片之外即清选中,滚轮恢复缩放(见 onWheel)。 @@ -497,7 +618,20 @@ void InteractionManager::faceSlice(int idx) { if (!cam) return; const Vec3 focal = slices_[static_cast(idx)]->center(); const Vec3 normal = slices_[static_cast(idx)]->normal(); - const double dist = cam->GetDistance(); // 保持当前观察距离 + // 缩放到切片:按切片【面内尺寸】(法向两侧两轴的跨度)+ 相机视角算"刚好框住"的距离, + // 而非沿用当前(拉远看整条线时可能几百米)距离——否则正视后切片又小又远(用户实测)。 + const VolumeImg* vol = volumeOf(slices_[static_cast(idx)]->volumeDsId()); + const std::array b = imageBounds(vol ? vol->image : nullptr); + const double ext[3] = {b[1] - b[0], b[3] - b[2], b[5] - b[4]}; + const double an[3] = {std::abs(normal[0]), std::abs(normal[1]), std::abs(normal[2])}; + const int ax = (an[0] >= an[1] && an[0] >= an[2]) ? 0 : (an[1] >= an[2] ? 1 : 2); // 法向主轴 + double inMax = 0.0; + for (int i = 0; i < 3; ++i) + if (i != ax) inMax = (ext[i] > inMax ? ext[i] : inMax); // 面内最大尺寸 + double dist = cam->GetDistance(); // 兜底(无体边界) + const double vaRad = cam->GetViewAngle() * 3.14159265358979323846 / 180.0; + if (inMax > 0.0 && vaRad > 0.0) + dist = (0.5 * inMax) / std::tan(0.5 * vaRad) * 1.1; // 框住面内最大尺寸 + 10% 余量 const FaceOnCamera face = faceOnCamera(focal, normal, dist); cam->SetFocalPoint(focal[0], focal[1], focal[2]); cam->SetPosition(face.position[0], face.position[1], face.position[2]); @@ -513,7 +647,17 @@ bool InteractionManager::onWheel(int dir) { // 配合 onPicked 的"点击切片外取消选中":取消后滚轮即恢复缩放,解决"选了切片无法缩放"。 // (不采用"悬停即推进":推进时鼠标难持续压在移动的切片上,且过敏感。) if (selected_ < 0 || selected_ >= static_cast(slices_.size())) return false; - const double step = wheelStep(imageBounds(selectedVolumeImage()), dir); // 选中切片所属体 + // 步长按切片【法向上的体素间距 × N】算:一格挪 N 个采样,与体总长无关——细长雷达体也不会 + // 因长轴 375m 而步太大/跳过。Shift=粗调(×10);超长轴粗定位另靠沿线滑块(后续)。 + const Vec3 n = slices_[static_cast(selected_)]->normal(); + std::array sp{1.0, 1.0, 1.0}; + if (vtkImageData* img = selectedVolumeImage()) { + double s[3]; + img->GetSpacing(s); + sp = {s[0], s[1], s[2]}; + } + const int voxels = (interactor_ && interactor_->GetShiftKey()) ? 20 : 2; // Shift=粗调 + const double step = wheelStep(sp, n, voxels, dir); slices_[static_cast(selected_)]->advance(step); safeRender(); return true; // 消费滚轮(推进选中切片,不缩放) diff --git a/src/render/interact/InteractionManager.hpp b/src/render/interact/InteractionManager.hpp index 244b64a..d32d0d6 100644 --- a/src/render/interact/InteractionManager.hpp +++ b/src/render/interact/InteractionManager.hpp @@ -142,6 +142,14 @@ private: // 找离世界点最近的切片索引;无切片返回 -1。 int nearestSlice(const Vec3& worldPoint) const; + // 精确判定:当前光标【射线】是否穿过某张切片真实矩形(origin/point1/point2)内。 + // 用射线-矩形求交(非带容差的 picker 点)→ 切片边界外不再误判命中(治外扩)。点帘面也判 false。 + bool pointOnSlice() const; + // 当前光标射线:近/远裁剪面世界点 → nearP + 方向 dir。无 interactor/renderer 返回 false。 + bool cursorRay(double nearP[3], double dir[3]) const; + // 旋转支点(B 方案#1):无选中切片时,取光标射线穿过的体【中段点】(进/出包围盒中点),多体取最近命中。 + // 绕它旋转→当前所视区域居中、不甩飞(治长体旋转丢目标)。无体命中返回 false(回退默认绕焦点)。 + bool rayVolumePivot(Vec3& out) const; // 在当前鼠标屏幕位置拾取 → 命中的切片索引;未命中切片返回 -1。 int pickSliceAtCursor() const; // 按 SliceTool 指针设为选中(widget 交互回调用:触碰即选中)。 diff --git a/src/render/interact/PickInteractorStyle.cpp b/src/render/interact/PickInteractorStyle.cpp index 533576a..7194ec6 100644 --- a/src/render/interact/PickInteractorStyle.cpp +++ b/src/render/interact/PickInteractorStyle.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -66,14 +67,17 @@ void PickInteractorStyle::OnLeftButtonDown() { return; } Vec3 world; - const bool hit = pickWorld(world); + const bool hit = pickWorld(world); // 仍用于取选中所需世界点(onPick) + // 命中切片【精确判定】:光标射线穿过某切片真实矩形内才算(不靠带容差的 picker 点)。 + // 边界外/帘面/其它物 → false → 抬键单击即取消。 + downHitSlice_ = hitTestSlice && hitTestSlice(); // 手动双击判定(GetRepeatCount 在 QVTK+Windows 不可靠,评审 M5): // 两次左键按下间隔 < 阈值且屏幕位置相近 → 双击。 const double now = nowMs(); const int* pos = iren ? iren->GetEventPosition() : nullptr; bool isDouble = false; - if (hit && pos && lastDownTime_ >= 0.0) { + if (downHitSlice_ && pos && lastDownTime_ >= 0.0) { // 仅命中切片才判双击(正视) const double dtMs = now - lastDownTime_; const int dx = pos[0] - lastDownPos_[0]; const int dy = pos[1] - lastDownPos_[1]; @@ -91,21 +95,24 @@ void PickInteractorStyle::OnLeftButtonDown() { lastDownTime_ = -1.0; // 重置,避免三击连判 return; // 不进入拖动旋转 } - if (hit) { - // 单击命中 → 选中所在切片(onPick 内仅选中, 不动相机)。 + if (downHitSlice_ && hit) { + // 单击命中【切片】→ 选中(onPick 内仅选中, 不动相机)。点帘面/非切片物/边界外不选中→抬键即取消。 if (onPick) onPick(world); } - // 不在按下时动相机(动相机=跳);绕选中物旋转在 Rotate() 内做(增量绕支点,不跳)。 + // 捕获本次拖动的旋转支点【一次】(在 onPick 选中之后取,故能用新选中切片):拖动中光标会移动, + // 不能每帧重取(否则支点漂)。选中切片→其中心;否则→光标射线穿过的体中段点;无→默认绕焦点。 + hasRotatePivot_ = (getRotateCenter && getRotateCenter(rotatePivot_)); + // 不在按下时动相机(动相机=跳);绕支点旋转在 Rotate() 内做(增量绕支点,不跳)。 Superclass::OnLeftButtonDown(); } void PickInteractorStyle::Rotate() { if (lock2D_) return; // 二维分析禁旋转(仅平移+缩放) - Vec3 c; - if (!this->CurrentRenderer || !getRotateCenter || !getRotateCenter(c)) { - Superclass::Rotate(); // 无选中物 → 默认绕焦点旋转 + if (!this->CurrentRenderer || !hasRotatePivot_) { + Superclass::Rotate(); // 无支点 → 默认绕焦点旋转 return; } + const Vec3 c = rotatePivot_; // 按下时已捕获、拖动中固定 auto* rwi = this->Interactor; auto* cam = this->CurrentRenderer->GetActiveCamera(); if (!rwi || !cam) return; @@ -114,8 +121,10 @@ void PickInteractorStyle::Rotate() { const int* size = this->CurrentRenderer->GetRenderWindow()->GetSize(); if (size[0] <= 0 || size[1] <= 0) return; // 与 TrackballCamera 同口径的角度映射。 + // 俯仰:本类绕 right=cross(DOP,up) 旋转,而 VTK 默认 Elevation 绕 cross(-DOP,up)=-right → + // 轴反向。故 elevation 取 +20(非 -20)抵消,使选中切片后上下方向与未选中(默认)时一致。 const double azimuth = dx * (-20.0 / size[0]) * this->MotionFactor; - const double elevation = dy * (-20.0 / size[1]) * this->MotionFactor; + const double elevation = dy * (20.0 / size[1]) * this->MotionFactor; double up[3], dop[3], right[3]; cam->GetViewUp(up); @@ -185,6 +194,17 @@ void PickInteractorStyle::OnLeftButtonUp() { if (onDrag2DEnd) onDrag2DEnd(); return; } + // 单击(抬键位移<阈值=非拖动)且按下未命中切片 → 取消选中(点空/点体;体 PickableOff 故点体也 hit=false)。 + // 拖空白旋转:抬键位移大 → 不取消,保留"绕选中切片旋转"。Esc 仍是完全拉近时的兜底。 + if (!lock2D_ && !downHitSlice_ && onDeselect) { + auto* iren = this->GetInteractor(); + const int* up = iren ? iren->GetEventPosition() : nullptr; + if (up) { + const int dx = up[0] - lastDownPos_[0], dy = up[1] - lastDownPos_[1]; + if (dx * dx + dy * dy <= kClickSlopPx2) onDeselect(); // 是单击 → 取消 + } + } + hasRotatePivot_ = false; // 拖动结束 → 清支点(下次按下重新捕获) Superclass::OnLeftButtonUp(); // 平移/旋转/缩放等由基类按 State 收尾 } @@ -205,4 +225,15 @@ void PickInteractorStyle::OnMouseWheelBackward() { Superclass::OnMouseWheelBackward(); } +void PickInteractorStyle::OnKeyPress() { + // Esc → 取消选中切片(拉近后满屏切片、点不到空白处取消时的可靠出口)。其它键走默认。 + auto* iren = this->GetInteractor(); + const char* sym = iren ? iren->GetKeySym() : nullptr; + if (sym && std::strcmp(sym, "Escape") == 0) { + if (onDeselect) onDeselect(); + return; + } + Superclass::OnKeyPress(); +} + } // namespace geopro::render::interact diff --git a/src/render/interact/PickInteractorStyle.hpp b/src/render/interact/PickInteractorStyle.hpp index 5c9b41f..81089be 100644 --- a/src/render/interact/PickInteractorStyle.hpp +++ b/src/render/interact/PickInteractorStyle.hpp @@ -30,6 +30,12 @@ public: // 取当前旋转中心(D39):有选中三维体/切片→填其中心、返回 true;否则 false(绕默认焦点)。 // 在"按下开始拖动"时调用一次,把焦点设到该中心(位置同步补偿,画面不变)→ 之后绕它旋转、不跳。 std::function getRotateCenter; + // 取消选中切片(Esc 键触发)。拉近后满屏切片、点不到空白处取消时的可靠出口。 + std::function onDeselect; + // 精确判定:当前光标【射线】是否穿过某张切片的真实矩形(origin/point1/point2)内。 + // 不靠带容差/夹取的 picker 命中点 → 切片边界外不再误判命中(用户实测的外扩)。 + // 点帘面/其它非切片物/边界外 → 返回 false → 单击即取消选中。 + std::function hitTestSlice; // 二维分析锁:开 → 左键拖动改为平移、禁旋转(仅平移+缩放);关 → 恢复三维拾取/旋转交互。 void setLock2D(bool on) { lock2D_ = on; } @@ -51,6 +57,7 @@ public: void OnLeftButtonDown() override; void OnMouseWheelForward() override; void OnMouseWheelBackward() override; + void OnKeyPress() override; // Esc → onDeselect(取消选中切片) // 绕选中物中心旋转(D39):有 getRotateCenter 时, 绕该中心增量旋转整个相机(位置+焦点+up), // 中心在世界/屏幕都不动→不跳; 否则回退默认(绕焦点)。 void Rotate() override; @@ -68,6 +75,15 @@ private: // 记上次左键按下时刻+屏幕位置,两次按下间隔 < kDoubleClickMs 且位置相近视为双击。 double lastDownTime_ = -1.0; // 单调时钟(毫秒),-1=无 int lastDownPos_[2] = {0, 0}; + // 左键按下时是否命中【切片】(精确:经 hitTestSlice 判点在切片矩形内,非"任意可拾取物")。 + // 抬键若为单击(无拖动)且未命中切片 → 取消选中(点空/点体/帘面/其它非切片物)。 + // 体 PickableOff、帘面虽可拾取但 hitTestSlice 判其非切片 → 都走取消。拖动则不取消(保留旋转)。 + bool downHitSlice_ = false; + + // 旋转支点:按下(拖动起点)时经 getRotateCenter 捕获一次,拖动中固定不漂(光标会动→不可每帧重取)。 + // 选中切片=其中心;否则=光标射线穿过的体中段点。无则 hasRotatePivot_=false→默认绕焦点。 + Vec3 rotatePivot_{}; + bool hasRotatePivot_ = false; // 二维分析模式:左键=平移、禁旋转(仅平移+缩放)。由 InteractionManager 在切 tab 时设。 bool lock2D_ = false; diff --git a/src/render/interact/SlicePlaneMath.cpp b/src/render/interact/SlicePlaneMath.cpp index 9def0c2..edf5815 100644 --- a/src/render/interact/SlicePlaneMath.cpp +++ b/src/render/interact/SlicePlaneMath.cpp @@ -56,10 +56,12 @@ Vec3 clampToBounds(const Vec3& origin, const std::array& b) { clamp1(origin[2], b[4], b[5])}; } -double wheelStep(const std::array& b, int dir) { - const double dx = b[1] - b[0], dy = b[3] - b[2], dz = b[5] - b[4]; - const double diag = std::sqrt(dx * dx + dy * dy + dz * dz); - const double mag = diag * 0.02; // 一次滚轮 ≈ 1/50 对角线 +double wheelStep(const std::array& spacing, const Vec3& normal, int voxels, int dir) { + // 沿法向的体素间距 = 各轴间距在法向上的投影绝对值和(轴向切片即取对应轴间距)。 + // 一格 = voxels 个采样,与体总长无关 → 超长轴(雷达沿线)也只挪几个采样、不跳过、不过冲。 + const double sn = std::abs(spacing[0] * normal[0]) + std::abs(spacing[1] * normal[1]) + + std::abs(spacing[2] * normal[2]); + const double mag = (sn > 0.0 ? sn : 1.0) * (voxels > 0 ? voxels : 1); return (dir >= 0 ? mag : -mag); } diff --git a/src/render/interact/SlicePlaneMath.hpp b/src/render/interact/SlicePlaneMath.hpp index 2beac40..49d741c 100644 --- a/src/render/interact/SlicePlaneMath.hpp +++ b/src/render/interact/SlicePlaneMath.hpp @@ -43,9 +43,10 @@ struct FaceOnCamera { }; FaceOnCamera faceOnCamera(const Vec3& focal, const Vec3& normal, double dist); -// 滚轮推进步长:取包围盒对角线长度的固定比例 × 方向(±1)。 -// 使一次滚轮在体内移动适中(约 1/50 对角线);dir>0 沿法向、dir<0 反向。 -double wheelStep(const std::array& bounds, int dir); +// 滚轮推进步长:按【沿切片法向的体素间距】× voxels × 方向(±1),即一格移动 voxels 个采样。 +// 与体总长无关(不会因长轴 375m 而步太大、也不因比例而跳过内容);spacing=体的三轴间距(含纵向夸张)。 +// voxels 小=细调;调用方可在 Shift 时传更大值做粗调。超长轴的粗定位另靠"沿线滑块",非滚轮。 +double wheelStep(const std::array& spacing, const Vec3& normal, int voxels, int dir); // 在切片中心列表中找离世界点最近的索引(按到平面的距离最小)。 // centers/normals 等长;空列表返回 -1。worldPoint 在哪张切片上→该索引。 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c5f4da8..e55bd49 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -226,11 +226,17 @@ target_sources(geopro_tests PRIVATE io/gpr/test_gpr_geometry.cpp) target_sources(geopro_tests PRIVATE io/gpr/test_gpr_survey_assembler.cpp) # GpsTrack:.gps 解析 + 经纬→局部米 + 沿轨迹里程插值/航向(G1 build-geo 基础,纯 C++17)。 target_sources(geopro_tests PRIVATE io/gpr/test_gps_track.cpp) +# NormalizedRadarReader:规范化 .head(KEY:VALUE) 解析(维度/字节序/通道偏移/波速/深度间距,纯 C++17)。 +target_sources(geopro_tests PRIVATE io/gpr/test_normalized_radar_reader.cpp) +# NormalizedRadarVolumeBridge:组合 reader(.head/.data) + assembleRadarVolume → BuiltI16(轴 X=道/Y=通道/Z=采样)。 +target_sources(geopro_tests PRIVATE io/gpr/test_normalized_radar_bridge.cpp) target_link_libraries(geopro_tests PRIVATE geopro_io_gpr) # Gpr3dvVolumeBridge(P2):gpr3dv 处理后立方体 → geopro 量化体(轴 X=道/Y=通道/Z=样本)。 # 链 geopro_gpr3dv_bridge(含 vendored gpr3dv + Qt)。 target_sources(geopro_tests PRIVATE io/gpr/test_gpr3dv_volume_bridge.cpp) +# RadarVolumeAssembler:格式无关共享建体(扫值域→Quant→通道插值→逐体素填→spacing)。 +target_sources(geopro_tests PRIVATE io/gpr/test_radar_volume_assembler.cpp) target_link_libraries(geopro_tests PRIVATE geopro_gpr3dv_bridge) add_subdirectory(spike) # spike S3: banded contour 渲染验证 diff --git a/tests/app/test_dataset_dimension.cpp b/tests/app/test_dataset_dimension.cpp index 72cbc22..6d5852d 100644 --- a/tests/app/test_dataset_dimension.cpp +++ b/tests/app/test_dataset_dimension.cpp @@ -14,14 +14,16 @@ TEST(DatasetDimension, SplitsByDdCode) { std::vector in{ row("a", "dd_section"), // 3D row("b", "dd_voxel"), // 3D + row("f", "dd_radar_3d"), // 3D(三维雷达体,spec §6.1) row("c", "dd_trajectory_data"), // 2D row("d", "dd_slice"), // Analysis row("e", "dd_unknownxyz"), // Other -> not in any bucket }; DimBuckets b = splitByDimension(in); - ASSERT_EQ(b.dim3D.size(), 2u); + ASSERT_EQ(b.dim3D.size(), 3u); EXPECT_EQ(b.dim3D[0].id, "a"); EXPECT_EQ(b.dim3D[1].id, "b"); + EXPECT_EQ(b.dim3D[2].id, "f"); ASSERT_EQ(b.dim2D.size(), 1u); EXPECT_EQ(b.dim2D[0].id, "c"); ASSERT_EQ(b.analysis.size(), 1u); diff --git a/tests/data/test_3d_repo.cpp b/tests/data/test_3d_repo.cpp index b6955f3..2785fa8 100644 --- a/tests/data/test_3d_repo.cpp +++ b/tests/data/test_3d_repo.cpp @@ -1,7 +1,14 @@ #include +#include +#include + +#include +#include +#include #include #include +#include #include "api/Api3dRepository.hpp" #include "geo/GeoLocalFrame.hpp" @@ -35,6 +42,8 @@ TEST(LocalSample3dRepo, DimensionOfMapsDdCode) { EXPECT_EQ(repo.dimensionOf(rowWith("dd_Property3D")), DsDimension::Dim3D); EXPECT_EQ(repo.dimensionOf(rowWith("dd_section")), DsDimension::Dim3D); EXPECT_EQ(repo.dimensionOf(rowWith("dd_inversion_data")), DsDimension::Dim3D); + // 三维雷达体(数据字典 DD0623 dd_radar_3d)→ 三维数据集(spec §6.1)。 + EXPECT_EQ(repo.dimensionOf(rowWith("dd_radar_3d")), DsDimension::Dim3D); EXPECT_EQ(repo.dimensionOf(rowWith("dd_slice")), DsDimension::Analysis3D); // 足迹型 → 二维:数据字典 DD0623 只 dd_trajectory_data 为统一通用轨迹「保留」; // 瞬变电磁/雷达通道/RTK 等轨迹型字典均「删除」→ 不再归 2D(落 Other)。 @@ -148,6 +157,17 @@ TEST(Api3dRepo, LoadMapLineNullHandleCallsOnError) { EXPECT_TRUE(errCalled); } +// dimensionOf(Api,与 LocalSample3dRepository 同口径):三维雷达体 dd_radar_3d → 三维(spec §6.1)。 +TEST(Api3dRepo, DimensionOfMapsDdRadar3dToDim3D) { + StubAsyncRepo dsRepo; + auto frame = std::make_shared(22.0, 114.0); + Api3dRepository repo(dsRepo, frame); + + EXPECT_EQ(repo.dimensionOf(rowWith("dd_radar_3d")), DsDimension::Dim3D); + EXPECT_EQ(repo.dimensionOf(rowWith("dd_voxel")), DsDimension::Dim3D); + EXPECT_EQ(repo.dimensionOf(rowWith("dd_unknown_xyz")), DsDimension::Other); +} + // volumeInfo:未知 dsId(非三维体)→ 返回 false,不弹空对话框。 TEST(Api3dRepo, VolumeInfoUnknownIdReturnsFalse) { StubAsyncRepo dsRepo; @@ -190,3 +210,82 @@ TEST(Api3dRepo, AnomalyRowsCarryMountAsParent) { EXPECT_EQ(rs->parentId, "slice-9"); // 挂切片 EXPECT_EQ(rs->typeName, "异常"); // typeName 空 → 回退"异常" } + +namespace { +// 写一条规范化测线合成数据(.head + .data);同 Task 6 createRadarVolumeGrid 用例口径: +// SAMPLES=3 / NUMBER_OF_CH=2 / LAST_TRACE=8 → X=道(4 段)、Y=通道(2)、Z=采样(3)。 +void writeSyntheticRadarLine(const std::filesystem::path& dir) { + namespace fs = std::filesystem; + 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(t * 10 + c * 100 + s); + f.write(reinterpret_cast(&v), 2); + } + } +} +} // namespace + +// registerRadarDataset:登记为 dd_radar_3d 体 DS(只存元数据、不建体)→ 仍被认作三维体, +// volumeRows 输出 ddCode="dd_radar_3d" + structParentId。 +TEST(Api3dRepo, RegisterRadarDatasetRoutesAsDdRadar3d) { + const auto dir = std::filesystem::temp_directory_path() / "api3d_radar_register"; + std::error_code ec; + std::filesystem::remove_all(dir, ec); + writeSyntheticRadarLine(dir); + + StubAsyncRepo dsRepo; + auto frame = std::make_shared(22.0, 114.0); + Api3dRepository repo(dsRepo, frame); + + 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"); + + std::filesystem::remove_all(dir, ec); +} + +// loadVolume:首次勾选时懒建雷达体(无 QCoreApplication → 同步交付;全量测试中若其它用例已建 +// QCoreApplication 单例则走异步,processEvents 排空队列交付)。回调收到有效 VolumeGrid。 +TEST(Api3dRepo, LoadVolumeBuildsRadarLazily) { + const auto dir = std::filesystem::temp_directory_path() / "api3d_radar_lazy"; + std::error_code ec; + std::filesystem::remove_all(dir, ec); + writeSyntheticRadarLine(dir); + + StubAsyncRepo dsRepo; + auto frame = std::make_shared(22.0, 114.0); + Api3dRepository repo(dsRepo, frame); + + const std::string id = repo.registerRadarDataset(dir.string(), "L", "测线L", "", 1); + bool got = false; + repo.loadVolume( + id, + [&](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; }); + // 全量测试单进程中其它用例(test_async_repo_dispatch/test_auth)会建持久 QCoreApplication 单例 → + // radar 分支走异步(std::thread + queued 交付),需驱动事件循环排空;无 app 时同步交付 got 已真。 + for (int i = 0; i < 200 && !got && QCoreApplication::instance(); ++i) + QCoreApplication::processEvents(QEventLoop::AllEvents, 10); + EXPECT_TRUE(got); + + std::filesystem::remove_all(dir, ec); +} diff --git a/tests/data/test_gpr_volume_repository.cpp b/tests/data/test_gpr_volume_repository.cpp index 58b8006..0b45bc1 100644 --- a/tests/data/test_gpr_volume_repository.cpp +++ b/tests/data/test_gpr_volume_repository.cpp @@ -57,12 +57,42 @@ TEST(GprVolumeRepositoryAdapter, DequantDimsSpacingAndBlank) { EXPECT_DOUBLE_EQ(g.spacing[1], 1.37); EXPECT_DOUBLE_EQ(g.spacing[2], 0.05); - // 物理值域 = BuiltI16 的 vminPhys/vmaxPhys。 - EXPECT_DOUBLE_EQ(g.vmin, 5.0); - EXPECT_DOUBLE_EQ(g.vmax, 20.0); + // 显示值域 = 双极对称窗口(以中位数为中心),非全 vminPhys/vmaxPhys。3 个有效体素 {9,12,20}: + // 中位数=12 → 窗口对称、【中点=中位数 12】(灰点落基线)。vmin(i - 500); + built.vol.at(1000, 0, 0) = 30000; + built.vol.at(1001, 0, 0) = 30000; + built.vol.at(1002, 0, 0) = -30000; + built.vol.at(1003, 0, 0) = -30000; + + const geopro::data::VolumeGrid g = geopro::data::builtI16ToVolumeGrid(built); + + // 对称 99% 窗口裁掉两端 0.4% 离群 → 显示窗落在结构范围(±500)内,远离 ±30000。 + EXPECT_GT(g.vmin, -1000.0); // 负向离群被裁 + EXPECT_LT(g.vmax, 1000.0); // 正向离群被裁 + EXPECT_NEAR(0.5 * (g.vmin + g.vmax), 0.0, 5.0); // 对称:中点≈基线(中位数≈0) + // 数据本身仍保留离群(只是显示窗收窄)——抽查离群体素反量化值未被改动。 + EXPECT_DOUBLE_EQ(g.vol.at(1000, 0, 0), 30000.0); +} + // 写一个合成通道:.iprh 文本头 + .iprb 纯 int16 波形([trace*samples + s],s 最快)。 // 与 test_gpr3dv_volume_bridge 同口径,确保 createGprVolumeGrid 走真 P1/P2 链。 void writeSyntheticChannel(const fs::path& iprhPath, int samples, int traces, @@ -153,4 +183,24 @@ TEST_F(GprVolumeRepositoryChainTest, ThrowsOnMissingLine) { std::runtime_error); } +// 规范化链(.head/.data → buildLineVolumeFromNormalized → 反量化)产 VolumeGrid。 +// 合成同 Task 5 桥接测试:SAMPLES=3/NUMBER_OF_CH=2/LAST_TRACE=8 → X=道(4 段)、 +// Y=通道(2)、Z=采样(3);coarse=1/targetDy=0 关下采样与通道插值,维度直读。 +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(t * 10 + c * 100 + s); + f.write(reinterpret_cast(&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); +} + } // namespace diff --git a/tests/io/gpr/test_normalized_radar_bridge.cpp b/tests/io/gpr/test_normalized_radar_bridge.cpp new file mode 100644 index 0000000..8e06d1f --- /dev/null +++ b/tests/io/gpr/test_normalized_radar_bridge.cpp @@ -0,0 +1,28 @@ +#include +#include +#include +#include +#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(t * 10 + c * 100 + s); + f.write(reinterpret_cast(&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 +} diff --git a/tests/io/gpr/test_normalized_radar_reader.cpp b/tests/io/gpr/test_normalized_radar_reader.cpp new file mode 100644 index 0000000..a41443f --- /dev/null +++ b/tests/io/gpr/test_normalized_radar_reader.cpp @@ -0,0 +1,87 @@ +#include +#include +#include +#include +#include "io/gpr/NormalizedRadarReader.hpp" +using namespace geopro::io::gpr; +namespace fs = std::filesystem; + +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); +} + +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(100 * t + 10 * c + s); + f.write(reinterpret_cast(&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(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); +} + +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); +} diff --git a/tests/io/gpr/test_radar_volume_assembler.cpp b/tests/io/gpr/test_radar_volume_assembler.cpp new file mode 100644 index 0000000..6afee13 --- /dev/null +++ b/tests/io/gpr/test_radar_volume_assembler.cpp @@ -0,0 +1,92 @@ +#include +#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 +} + +// ── 合成靶标:在【装配出的体】里验通道插值的几何忠实度 ────────────────────── +// 现有 test_gpr_geometry 只验 planChannelInterpolation 的【行规划】;这两个测试验 +// assembleRadarVolume 把规划【应用到体】是否正确——即用户要判断的"通道插值在体里 +// 对不对、会不会造缝"。 +// 布局:3 通道偏移 {0, 0.10, 0.20},targetDy=0.05 → ny=round(0.2/0.05)+1=5: +// 行 j0=ch0 / j1=blend(ch0,ch1,0.5) / j2=ch1 / j3=blend(ch1,ch2,0.5) / j4=ch2。 +namespace { +RadarCubeDesc make3ChDesc() { + RadarCubeDesc d; + d.channels = 3; d.traces = 2; d.samples = 3; + d.dxBase = 0.1; d.dz = 0.05; + d.chXOffsets = {0.0, 0.10, 0.20}; // 触发通道插值 + return d; +} +} // namespace + +// 平层反射(同一深度 s=2 全通道等值 500)穿过通道插值后【全部行仍等于 500】—— +// 不出现"插值行衰减/锯齿/横向缝"(用户最担心的 10cm 缝就是这条若失败)。 +TEST(RadarVolumeAssembler, FlatReflectorStaysFlatAcrossInterpolatedRows) { + const RadarCubeDesc d = make3ChDesc(); + // s==2:平层反射(全通道 500);s==0:逐通道阶梯(ch0=100/ch1=200/ch2=300)验混合;其余 0。 + auto sampler = [](int c, int /*t*/, int s) { + if (s == 2) return 500.0; + if (s == 0) return 100.0 * (c + 1); + return 0.0; + }; + const geopro::core::BuiltI16 b = assembleRadarVolume(d, sampler, /*coarse=*/1, /*targetDy=*/0.05); + ASSERT_EQ(b.vol.ny(), 5); // 3 通道 → 5 行(含 2 条插值) + ASSERT_EQ(b.vol.nx(), 2); + ASSERT_EQ(b.vol.nz(), 3); + EXPECT_DOUBLE_EQ(b.spacing[1], 0.05); // 插值后 dy=targetDy + // 平层在【每一行、每一道】都应保持 500(插值不破坏横向连续)。 + for (int j = 0; j < b.vol.ny(); ++j) + for (int to = 0; to < b.vol.nx(); ++to) + EXPECT_NEAR(b.quant.toPhys(b.vol.at(to, j, 2)), 500.0, b.quant.scale) + << "行 j=" << j << " 道 to=" << to << " 处平层被插值破坏"; +} + +// 插值行 = 相邻两通道的正确线性混合(j1 在 ch0=100/ch1=200 之间 wb=0.5 → 150); +// 纯通道行 = 原通道值(j0=100 / j2=200 / j4=300)。验"插值没造假峰、没错位"。 +TEST(RadarVolumeAssembler, InterpolatedRowIsCorrectLinearBlend) { + const RadarCubeDesc d = make3ChDesc(); + auto sampler = [](int c, int /*t*/, int s) { return s == 0 ? 100.0 * (c + 1) : 0.0; }; + const geopro::core::BuiltI16 b = assembleRadarVolume(d, sampler, /*coarse=*/1, /*targetDy=*/0.05); + ASSERT_EQ(b.vol.ny(), 5); + const double tol = b.quant.scale; + EXPECT_NEAR(b.quant.toPhys(b.vol.at(0, 0, 0)), 100.0, tol); // j0 = ch0 + EXPECT_NEAR(b.quant.toPhys(b.vol.at(0, 1, 0)), 150.0, tol); // j1 = blend(100,200,0.5) + EXPECT_NEAR(b.quant.toPhys(b.vol.at(0, 2, 0)), 200.0, tol); // j2 = ch1 + EXPECT_NEAR(b.quant.toPhys(b.vol.at(0, 3, 0)), 250.0, tol); // j3 = blend(200,300,0.5) + EXPECT_NEAR(b.quant.toPhys(b.vol.at(0, 4, 0)), 300.0, tol); // j4 = ch2 +} diff --git a/tests/render/test_slice_plane_math.cpp b/tests/render/test_slice_plane_math.cpp index 44dd2b2..4c082ae 100644 --- a/tests/render/test_slice_plane_math.cpp +++ b/tests/render/test_slice_plane_math.cpp @@ -95,17 +95,22 @@ TEST(SlicePlaneMath, FaceOnNormalizesNormal) { expectVec(cam.position, 0, 6, 0); } -// ── wheelStep:滚轮推进步长(按对角线比例 × 方向)── +// ── wheelStep:步长 = 沿法向体素间距 × voxels × 方向(spacing=三轴间距,X法向取X间距)── TEST(SlicePlaneMath, WheelStepForwardPositive) { - EXPECT_GT(wheelStep({0, 10, 0, 0, 0, 0}, +1), 0.0); + EXPECT_GT(wheelStep({0.1, 0.1, 0.05}, {1, 0, 0}, 2, +1), 0.0); } TEST(SlicePlaneMath, WheelStepBackwardNegative) { - EXPECT_LT(wheelStep({0, 10, 0, 0, 0, 0}, -1), 0.0); + EXPECT_LT(wheelStep({0.1, 0.1, 0.05}, {1, 0, 0}, 2, -1), 0.0); } -TEST(SlicePlaneMath, WheelStepScalesWithBounds) { - const double small = wheelStep({0, 10, 0, 0, 0, 0}, 1); - const double big = wheelStep({0, 100, 0, 0, 0, 0}, 1); - EXPECT_GT(big, small); // 体越大步长越大 +TEST(SlicePlaneMath, WheelStepScalesWithVoxels) { + const double fine = wheelStep({0.1, 0.1, 0.05}, {1, 0, 0}, 1, 1); + const double coarse = wheelStep({0.1, 0.1, 0.05}, {1, 0, 0}, 10, 1); + EXPECT_GT(coarse, fine); // voxels 越大步长越大(Shift 粗调) +} +// 只取法向那条轴的【间距】(非总长):长轴间距大也不影响 Z 法向步长;1 体素 = Z 间距 0.05。 +TEST(SlicePlaneMath, WheelStepUsesNormalAxisSpacing) { + const double zStep = wheelStep({100.0, 0.1, 0.05}, {0, 0, 1}, 1, 1); // Z 法向 → 取 Z 间距 0.05 + EXPECT_NEAR(zStep, 0.05, 1e-9); // 与 X 间距 100 无关 } // ── nearestPlane:找点所在切片(按到平面距离最小)── diff --git a/tools/radar_convert/README.md b/tools/radar_convert/README.md new file mode 100644 index 0000000..4855a35 --- /dev/null +++ b/tools/radar_convert/README.md @@ -0,0 +1,71 @@ +# radar_convert — 雷达原始数据 → 规范化格式 转换插件(本地原型) + +把厂商原始雷达数据转换成客户端**规范化格式** `.head / .data / .cor (/.index)`,供三维雷达 +渲染器消费。本目录是**未来"服务端下发转换插件"的本地原型**:今天的 Python 工具实现的 +`convert` 契约,将来由客户端按设备型号拉取对应插件执行,接口不变。 + +当前实现型号:**`RADAR_TYPE_MALAMIRA`**(Mala Mira rSlicer,`.rad + .rd3|.rd7 + _G01.pos`)。 + +--- + +## 插件契约(本地工具 = 未来插件,接口一致) + +``` +plugin_id : RADAR_TYPE_MALAMIRA +supports(fileset) -> bool # 据文件组成判断是否本插件可处理 +convert(lineDir, prefix, outDir) -> {head,data,cor} +``` + +- **输入**:一条测线三件套 `{prefix}.rad` + `{prefix}.rd3|.rd7` + `{prefix}_G01.pos`(轨迹可选)。 +- **输出**:规范化目录 `{prefix}.head` + `{prefix}.data` + `{prefix}.cor`。 +- 映射规则源自客户《雷达业务开发说明》§3.3(.rad→.head)/ §3.5(.rd3→.data)/ §2.2.2(.pos→.cor)。 + +## 用法 + +```bash +# 1) 列出目录内测线 + 维度一致性校验(不写文件) +python tools/radar_convert/malamira.py info + +# 2) 转换(--prefix 省略=全部测线) +python tools/radar_convert/malamira.py convert --out [--prefix 南同大道_000] + +# 3) probe:出图核对 .rd3 数据体主序(写一张 PNG 到 --out) +python tools/radar_convert/malamira.py probe --prefix 南同大道_000 --out +``` + +--- + +## 数据体维度与排列(★渲染器必读,已用真实数据核对) + +- 体维度:`K`(道/切片,沿运动) × `M`(通道) × `N`(采样/深度)。 + - `M = NUMBER_OF_CH`,`N = SAMPLES`,`K = LAST_TRACE / NUMBER_OF_CH`。 + - `.rad` 的 `LAST TRACE` 是**总扫描数**(=K×M),不是道数 K。`.head` 原样透传该值, + 渲染器按 `K = LAST_TRACE / NUMBER_OF_CH` 求道数。 +- **`.data` 主序 = position-major(已 probe 核对,MALA南同大道_000)**: + 磁盘扫描顺序 = `(道0: 通道0..M-1)(道1: 通道0..M-1)…`,每个 sweep 内 `N` 个采样连续。 + 即 `flat.reshape(K, M, N)[道][通道][采样]`。 + - 直接对应 geopro 体轴 `X=道(nx=K)`、`Y=通道(ny=M)`、`Z=采样(nz=N)`,**无需转置**。 + - 反例(错误主序):channel-major `reshape(M,K,N)` 的 B-scan 呈竖条乱码——probe 已排除。 +- 数据类型:`int16` 小端(`.rd3`)/ `int32` 小端(`.rd7`)。`.rd3` 中出现 `-32768` + 为直达波饱和的真实值,非空值哨兵。 + +--- + +## 与客户文档的偏差(实现时的取舍,便于后端对齐) + +| # | 项 | 文档 | 本工具 | 理由 | +|---|---|---|---|---| +| 1 | `BITS` 计算 | `文件大小/LAST_TRACE/NUMBER_OF_CH×8` | `bytes = 文件大小/(LAST_TRACE×SAMPLES)`,×8;并与扩展名(.rd3→16/.rd7→32)交叉校验 | 文档公式漏了 SAMPLES 维、量纲不符;本式对 6 条线均得 16,且 `文件大小==LAST_TRACE×SAMPLES×bytes` 严格成立 | +| 2 | `ENDIAN_TYPE` | §3.3 表:"留空" | 填 `1`(小端) | 同节要点 1 明确"通常小端即 1";渲染需确定字节序 | +| 3 | `WHEEL_CALIBRATION` | §3.3 表:"留空" | 透传 `.rad` 实际值 | `.rad` 有该字段,透传更忠实(不影响渲染) | +| 4 | `.cor` 坐标 | 北→纬度 / 东→经度 | 同文档直映,N/E/M 为占位标识,解状态=4 | `.pos` 是本地投影坐标(米)、非经纬度;CRS 未知。单线渲染不依赖 .cor 配准,多线阶段再处理 | +| 5 | `.index` | 可选打标文件 | 本数据集无打标 → 不产出 | 该目录仅有 FCODES.TXT(编码字典),无 .index 源 | + +--- + +## 已验证(MALA南同大道_rSlicer,6 条测线,2026-06-29) + +- `info`:6 条线全部通过 `文件大小 == LAST_TRACE×SAMPLES×bytes` 校验。 + K∈[2333,3778],M=16,N=516,bits=16,距离模式,dx≈0.095–0.099m,时窗 96.42ns,均含轨迹。 +- `convert`:6 条线 `.head/.data/.cor` 全部产出;`.data` 与源 `.rd3` 字节数严格一致。 +- `probe`:position-major B-scan 出现连续直达波 + 地层 + 双曲线绕射 → 主序确认。 diff --git a/tools/radar_convert/malamira.py b/tools/radar_convert/malamira.py new file mode 100644 index 0000000..13f368c --- /dev/null +++ b/tools/radar_convert/malamira.py @@ -0,0 +1,350 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +RADAR_TYPE_MALAMIRA 转换插件(本地原型)。 + +把 Mala Mira rSlicer 原始三件套(.rad + .rd3|.rd7 + _G01.pos)转换成客户端规范化 +格式(.head + .data + .cor),并提供 probe 子命令出图核对 .rd3 数据体主序。 + +本工具实现的 convert 契约 = 未来"服务端下发插件"的接口: + plugin_id : RADAR_TYPE_MALAMIRA + supports(fileset) -> bool + convert(lineDir, prefix, outDir) -> {head, data, cor} + +字段映射规则见客户《雷达业务开发说明》§3.3 / §2.2.2 / §3.5。与文档的少量偏差见 README.md。 +""" + +import argparse +import os +import shutil +import sys + +import numpy as np + +PLUGIN_ID = "RADAR_TYPE_MALAMIRA" + +# 规范化 .head 字段顺序(三维雷达,文档 §1.2.2)。 +HEAD_FIELD_ORDER = [ + "DATE", "START_TIME", "STOP_TIME", "UNITS", "MODE", "ANTENNAS", "FREQUENCY", + "STACKS", "LAST_TRACE", "POSITIVE_DIRECTION", "SAMPLES", "TIME_INTERVAL", + "TIMEWINDOW", "DEPTH", "ZERO_POSITION", "DIELECTRIC", "SOIL_TYPE", "BITS", + "MARK", "DISTANCE_INTERVAL", "START_POSITION", "STOP_POSITION", "WHEEL_GPS", + "WHEEL_CALIBRATION", "SCAN_SECOND", "NUMBER_OF_CH", "CH_X_OFFSETS", + "RTK_X_OFFSET", "RTK_Y_OFFSET", "RTK_Z_OFFSET", "GAIN", "FILTER", "SMOOTH", + "ENDIAN_TYPE", +] + + +# --------------------------------------------------------------------------- +# 解析 .rad +# --------------------------------------------------------------------------- +def parse_rad(rad_path): + """读 Mala .rad(ASCII,KEY:VALUE 行)→ dict(保留原始键,值去首尾空白)。""" + raw = {} + with open(rad_path, "r", encoding="utf-8", errors="replace") as f: + for line in f: + if ":" not in line: + continue + key, _, val = line.partition(":") + raw[key.strip()] = val.strip() + return raw + + +def _f(raw, key, default=None): + v = raw.get(key, "") + if v == "" or v is None: + return default + try: + return float(v) + except ValueError: + return default + + +def _i(raw, key, default=None): + v = _f(raw, key, None) + return int(round(v)) if v is not None else default + + +def compute_dims(raw, data_path): + """从 .rad + 数据文件大小推导体维度并做一致性校验。 + + 返回 dict:positions(K)=道/切片数, channels(M), samples(N), + last_trace(总扫描数=K*M), bits, bytes_per_sample。 + """ + samples = _i(raw, "SAMPLES") + channels = _i(raw, "NUMBER_OF_CH") + last_trace = _i(raw, "LAST TRACE") # Mala 中 = 总扫描数(道 * 通道) + if not samples or not channels or not last_trace: + raise ValueError( + "缺少 SAMPLES / NUMBER_OF_CH / LAST TRACE,无法推导维度: %s" % data_path) + + ext = os.path.splitext(data_path)[1].lower() + bytes_per_sample = 2 if ext == ".rd3" else 4 if ext == ".rd7" else None + if bytes_per_sample is None: + raise ValueError("未知数据扩展名(仅 .rd3/.rd7): %s" % data_path) + + filesize = os.path.getsize(data_path) + expect = last_trace * samples * bytes_per_sample + if filesize != expect: + raise ValueError( + "数据体大小不符: %s 实际 %d 字节, 期望 LAST_TRACE(%d)*SAMPLES(%d)*%d = %d" + % (data_path, filesize, last_trace, samples, bytes_per_sample, expect)) + + if last_trace % channels != 0: + raise ValueError( + "LAST_TRACE(%d) 不能被 NUMBER_OF_CH(%d) 整除,无法切分道/通道" + % (last_trace, channels)) + + positions = last_trace // channels + return { + "positions": positions, + "channels": channels, + "samples": samples, + "last_trace": last_trace, + "bits": bytes_per_sample * 8, + "bytes_per_sample": bytes_per_sample, + "filesize": filesize, + } + + +# --------------------------------------------------------------------------- +# .rad -> .head +# --------------------------------------------------------------------------- +def build_head(raw, dims): + """按 §3.3 把 .rad 映射成规范化 .head 字段 dict。无对应字段留空。""" + ch_y = raw.get("CH_Y_OFFSETS", "").split() + head = {k: "" for k in HEAD_FIELD_ORDER} + head.update({ + "DATE": raw.get("DATE", ""), + "START_TIME": raw.get("TIME", ""), + "UNITS": raw.get("UNITS", ""), + "MODE": "距离模式", # Mala 默认距离模式(§3.3 要点 4) + "ANTENNAS": raw.get("ANTENNAS", ""), + "FREQUENCY": raw.get("FREQUENCY", ""), + "STACKS": raw.get("STACKS", ""), + "LAST_TRACE": str(dims["last_trace"]), + "POSITIVE_DIRECTION": raw.get("POSITIVE DIRECTION", ""), + "SAMPLES": str(dims["samples"]), + "TIME_INTERVAL": raw.get("TIME INTERVAL", ""), + "TIMEWINDOW": raw.get("TIMEWINDOW", ""), + "BITS": str(dims["bits"]), + "DISTANCE_INTERVAL": raw.get("DISTANCE INTERVAL", ""), + "START_POSITION": raw.get("START POSITION", ""), + "STOP_POSITION": raw.get("STOP POSITION", ""), + "WHEEL_CALIBRATION": raw.get("WHEEL CALIBRATION", ""), + "NUMBER_OF_CH": str(dims["channels"]), + "CH_X_OFFSETS": raw.get("CH_X_OFFSETS", "").strip(), + "RTK_Y_OFFSET": ch_y[0] if ch_y else "", # §3.3:取 CH_Y_OFFSETS 首元素 + "ENDIAN_TYPE": "1", # Mala rd3 小端(§3.3 要点 1) + }) + return head + + +def write_head(head, out_path): + with open(out_path, "w", encoding="utf-8", newline="\n") as f: + for k in HEAD_FIELD_ORDER: + f.write("%s:%s\n" % (k, head.get(k, ""))) + + +# --------------------------------------------------------------------------- +# .pos -> .cor (§2.2.2 场景二) +# --------------------------------------------------------------------------- +def convert_pos_to_cor(pos_path, cor_path): + """.pos(本地坐标: 序号 北 东 高程) → .cor(序号 纬度 N 经度 E 高程 M 解状态=4)。 + + 注:.pos 为本地投影坐标(米),按文档直接映射 北→纬度 / 东→经度;N/E/M 为占位标识, + 解状态固定填 4(RTK Fixed)。单线渲染不依赖 .cor 做世界配准,多线阶段再用。 + """ + rows = [] + with open(pos_path, "r", encoding="utf-8", errors="replace") as f: + for line in f: + s = line.strip() + if not s or s.upper().startswith("UNITS"): + continue + parts = s.split() + if len(parts) < 4: + continue + idx = int(float(parts[0])) + north, east, elev = float(parts[1]), float(parts[2]), float(parts[3]) + rows.append((idx, north, east, elev)) + with open(cor_path, "w", encoding="utf-8", newline="\n") as f: + f.write("VERSION:1\n") + for idx, north, east, elev in rows: + f.write("%d\t%.6f\tN\t%.6f\tE\t%.6f\tM\t4\n" % (idx, north, east, elev)) + return len(rows) + + +# --------------------------------------------------------------------------- +# 测线发现 +# --------------------------------------------------------------------------- +def find_lines(line_dir): + """遍历目录,返回有效测线 [(prefix, rad, data, pos|None)](§3.2 抽取规则)。""" + out = [] + for name in sorted(os.listdir(line_dir)): + if not name.lower().endswith(".rad"): + continue + prefix = name[:-4] + rad = os.path.join(line_dir, name) + data = None + for ext in (".rd3", ".rd7"): + cand = os.path.join(line_dir, prefix + ext) + if os.path.exists(cand): + data = cand + break + if data is None: + print(" [跳过] %s 缺 .rd3/.rd7 数据文件" % prefix, file=sys.stderr) + continue + pos = os.path.join(line_dir, prefix + "_G01.pos") + out.append((prefix, rad, data, pos if os.path.exists(pos) else None)) + return out + + +# --------------------------------------------------------------------------- +# convert +# --------------------------------------------------------------------------- +def convert_line(prefix, rad, data, pos, out_dir): + raw = parse_rad(rad) + dims = compute_dims(raw, data) + os.makedirs(out_dir, exist_ok=True) + + head = build_head(raw, dims) + write_head(head, os.path.join(out_dir, prefix + ".head")) + shutil.copyfile(data, os.path.join(out_dir, prefix + ".data")) # §3.5 原样拷贝 + cor_n = convert_pos_to_cor(pos, os.path.join(out_dir, prefix + ".cor")) if pos else 0 + + print("[%s] 道(K)=%d 通道(M)=%d 采样(N)=%d bits=%d .data=%.1fMB .cor=%d点%s" + % (prefix, dims["positions"], dims["channels"], dims["samples"], + dims["bits"], dims["filesize"] / 1e6, cor_n, + "" if pos else " (无轨迹)")) + return dims + + +def cmd_convert(args): + if args.prefix: + rad = os.path.join(args.line_dir, args.prefix + ".rad") + data = None + for ext in (".rd3", ".rd7"): + if os.path.exists(os.path.join(args.line_dir, args.prefix + ext)): + data = os.path.join(args.line_dir, args.prefix + ext) + pos = os.path.join(args.line_dir, args.prefix + "_G01.pos") + convert_line(args.prefix, rad, data, pos if os.path.exists(pos) else None, + args.out) + else: + lines = find_lines(args.line_dir) + print("发现 %d 条测线,输出 → %s" % (len(lines), args.out)) + for prefix, rad, data, pos in lines: + convert_line(prefix, rad, data, pos, args.out) + + +# --------------------------------------------------------------------------- +# probe:核对 .rd3 数据体主序 +# --------------------------------------------------------------------------- +def load_flat(data_path, dims, endian="<"): + dt = np.dtype("%si%d" % (endian, dims["bytes_per_sample"])) + flat = np.fromfile(data_path, dtype=dt) + n = dims["last_trace"] * dims["samples"] + if flat.size != n: + raise ValueError("读到 %d 个样本,期望 %d" % (flat.size, n)) + return flat.astype(np.float32) + + +def _clip(img): + """按 99 分位绝对值裁剪对比度,返回 (img, vmax)。""" + v = np.percentile(np.abs(img), 99) or 1.0 + return img, v + + +def cmd_probe(args): + import matplotlib + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + raw = parse_rad(os.path.join(args.line_dir, args.prefix + ".rad")) + data = None + for ext in (".rd3", ".rd7"): + cand = os.path.join(args.line_dir, args.prefix + ext) + if os.path.exists(cand): + data = cand + dims = compute_dims(raw, data) + K, M, N = dims["positions"], dims["channels"], dims["samples"] + flat = load_flat(data, dims, "<" if args.endian == "little" else ">") + os.makedirs(args.out, exist_ok=True) + + print("[probe] %s K(道)=%d M(通道)=%d N(采样)=%d amp[min=%.0f max=%.0f mean|.|=%.1f]" + % (args.prefix, K, M, N, flat.min(), flat.max(), np.abs(flat).mean())) + + ch = args.channel + # H1: position-major sweeps 顺序 = (pos0:ch0..chM-1)(pos1:..) → reshape(K,M,N) + h1 = flat.reshape(K, M, N) + bscan_h1 = h1[:, ch, :].T # (N 采样 × K 道) + # H2: channel-major sweeps 顺序 = (ch0:pos0..posK-1)(ch1:..) → reshape(M,K,N) + h2 = flat.reshape(M, K, N) + bscan_h2 = h2[ch, :, :].T # (N 采样 × K 道) + # C-scan(H1 主序下某采样深度的 道×通道 平面) + cscan_h1 = h1[:, :, args.depth] # (K × M) + + panels = [ + ("H1 position-major B-scan ch%d" % ch, bscan_h1, "trace (K)", "sample (N)"), + ("H2 channel-major B-scan ch%d" % ch, bscan_h2, "trace (K)", "sample (N)"), + ("H1 C-scan @sample %d" % args.depth, cscan_h1, "channel (M)", "trace (K)"), + ] + fig, axes = plt.subplots(1, 3, figsize=(18, 6)) + for axp, (title, img, xl, yl) in zip(axes, panels): + _, vmax = _clip(img) + axp.imshow(img, aspect="auto", cmap="gray", vmin=-vmax, vmax=vmax, + interpolation="nearest") + axp.set_title(title) + axp.set_xlabel(xl) + axp.set_ylabel(yl) + fig.suptitle("%s %s -- main-order check: the coherent B-scan (layers/hyperbolas) is correct" + % (PLUGIN_ID, args.prefix), fontsize=12) + fig.tight_layout() + out_png = os.path.join(args.out, "probe_%s.png" % args.prefix) + fig.savefig(out_png, dpi=110) + print("[probe] 出图 → %s" % out_png) + + +# --------------------------------------------------------------------------- +def cmd_info(args): + lines = find_lines(args.line_dir) + print("目录 %s 发现 %d 条测线 (plugin=%s)" % (args.line_dir, len(lines), PLUGIN_ID)) + for prefix, rad, data, pos in lines: + raw = parse_rad(rad) + dims = compute_dims(raw, data) + print(" %-18s K=%-5d M=%-3d N=%-4d bits=%d dx=%s tw=%sns ch_x=%d个 轨迹=%s" + % (prefix, dims["positions"], dims["channels"], dims["samples"], + dims["bits"], raw.get("DISTANCE INTERVAL", "?"), + raw.get("TIMEWINDOW", "?"), + len(raw.get("CH_X_OFFSETS", "").split()), + "有" if pos else "无")) + + +def main(): + ap = argparse.ArgumentParser(description="RADAR_TYPE_MALAMIRA 转换插件(本地原型)") + sub = ap.add_subparsers(dest="cmd", required=True) + + p = sub.add_parser("info", help="列出目录内测线 + 维度校验") + p.add_argument("line_dir") + p.set_defaults(func=cmd_info) + + p = sub.add_parser("convert", help="转换为规范化 .head/.data/.cor") + p.add_argument("line_dir") + p.add_argument("--prefix", default=None, help="只转某条线(默认全部)") + p.add_argument("--out", required=True, help="输出目录") + p.set_defaults(func=cmd_convert) + + p = sub.add_parser("probe", help="出图核对 .rd3 数据体主序") + p.add_argument("line_dir") + p.add_argument("--prefix", required=True) + p.add_argument("--out", required=True) + p.add_argument("--channel", type=int, default=0) + p.add_argument("--depth", type=int, default=200, help="C-scan 取的采样深度") + p.add_argument("--endian", choices=["little", "big"], default="little") + p.set_defaults(func=cmd_probe) + + args = ap.parse_args() + args.func(args) + + +if __name__ == "__main__": + main()