From d70590cbefe9719ec69f910693b218d5694bbaae Mon Sep 17 00:00:00 2001 From: gaozheng Date: Thu, 25 Jun 2026 09:06:15 +0800 Subject: [PATCH] =?UTF-8?q?feat(data):=20=E9=80=90=E7=BA=BF=20GPR=20?= =?UTF-8?q?=E4=BD=93=E5=8F=8D=E9=87=8F=E5=8C=96=E9=80=82=E9=85=8D=E5=99=A8?= =?UTF-8?q?=20+=20GprVolumeRepository?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增数据层方案A纯数据通道,把逐线 GPR 处理后 int16 量化体(io::gpr buildLineVolumeFromGpr3dv 产 BuiltI16)反量化成 app 渲染链吃的稠密 float 体 (data::VolumeGrid),使真实雷达三维体可经现成 loadVolume->addVolume->buildVoxel 显示(替 mock),零碰并行会话 UI/树/异常热文件。 - builtI16ToVolumeGrid:纯函数适配器,逐体素 Quant::toPhys 反量化, kBlank 空值哨兵->NaN(下游 buildVoxel 映射透明),origin/spacing/vmin-vmax 搬运。 - createGprVolumeGrid(lineDir,linePrefix,coarse=4):走 P1/P2 链建逐线体->适配。 - 单测:适配器逐值反量化/维度/spacing/origin/kBlank->NaN;全链合成多通道 .iprb 走真链产出有效 VolumeGrid;缺线抛异常。434 测试全过,无回归。 --- src/data/CMakeLists.txt | 6 +- src/data/GprVolumeRepository.cpp | 48 +++++++ src/data/GprVolumeRepository.hpp | 35 +++++ tests/CMakeLists.txt | 3 + tests/data/test_gpr_volume_repository.cpp | 156 ++++++++++++++++++++++ 5 files changed, 246 insertions(+), 2 deletions(-) create mode 100644 src/data/GprVolumeRepository.cpp create mode 100644 src/data/GprVolumeRepository.hpp create mode 100644 tests/data/test_gpr_volume_repository.cpp diff --git a/src/data/CMakeLists.txt b/src/data/CMakeLists.txt index e245aeb..58d4560 100644 --- a/src/data/CMakeLists.txt +++ b/src/data/CMakeLists.txt @@ -18,9 +18,11 @@ add_library(geopro_data STATIC api/ApiDatasetCommandRepository.cpp api/Api3dRepository.cpp api/DatasetLoadHandles.cpp - api/NavRequest.cpp) + api/NavRequest.cpp + GprVolumeRepository.cpp) target_include_directories(geopro_data PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(geopro_data PUBLIC geopro_core geopro_net Qt6::Core PRIVATE nlohmann_json::nlohmann_json) +# geopro_gpr3dv_bridge:逐线 GPR 体(BuiltI16)来源,GprVolumeRepository 反量化为 VolumeGrid。 +target_link_libraries(geopro_data PUBLIC geopro_core geopro_net Qt6::Core geopro_gpr3dv_bridge PRIVATE nlohmann_json::nlohmann_json) target_compile_features(geopro_data PUBLIC cxx_std_17) set_target_properties(geopro_data PROPERTIES AUTOMOC ON AUTOUIC OFF AUTORCC OFF) diff --git a/src/data/GprVolumeRepository.cpp b/src/data/GprVolumeRepository.cpp new file mode 100644 index 0000000..fd9f88a --- /dev/null +++ b/src/data/GprVolumeRepository.cpp @@ -0,0 +1,48 @@ +#include "data/GprVolumeRepository.hpp" + +#include +#include + +#include "core/model/ScalarVolumeI16.hpp" +#include "io/gpr/Gpr3dvVolumeBridge.hpp" + +namespace geopro::data { + +VolumeGrid builtI16ToVolumeGrid(const geopro::core::BuiltI16& built) { + const int nx = built.vol.nx(); + const int ny = built.vol.ny(); + const int nz = built.vol.nz(); + + VolumeGrid out; + 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 最慢)。 + // kBlank → NaN:下游 render::buildVoxel 把 NaN 映射到 [vmin,vmax] 外的哨兵 → + // 传递函数置全透明(与 float 路径空值语义一致)。 + const std::vector& src = built.vol.data(); + std::vector& dst = out.vol.data(); + const double nan = std::numeric_limits::quiet_NaN(); + 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); + } + return out; +} + +VolumeGrid createGprVolumeGrid(const std::string& lineDir, + const std::string& linePrefix, int coarse) { + // 走 P1/P2 链(io::gpr)得处理后 int16 量化体 → 反量化为 app 的 float 体。 + // metricsOut 传 nullptr:repository 只产数据,度量留给 gpr_poc CLI。 + const geopro::core::BuiltI16 built = + geopro::io::gpr::buildLineVolumeFromGpr3dv(lineDir, linePrefix, + /*metricsOut=*/nullptr, coarse); + return builtI16ToVolumeGrid(built); +} + +} // namespace geopro::data diff --git a/src/data/GprVolumeRepository.hpp b/src/data/GprVolumeRepository.hpp new file mode 100644 index 0000000..054fb2a --- /dev/null +++ b/src/data/GprVolumeRepository.hpp @@ -0,0 +1,35 @@ +#ifndef GEOPRO_DATA_GPRVOLUMEREPOSITORY_HPP +#define GEOPRO_DATA_GPRVOLUMEREPOSITORY_HPP + +#include + +#include "core/algo/GprVolumeBuilder.hpp" // geopro::core::BuiltI16 +#include "repo/I3dSceneRepository.hpp" // geopro::data::VolumeGrid + +namespace geopro::data { + +// 把逐线 GPR 处理后量化体(int16 BuiltI16)反量化成 app 渲染链吃的稠密 float 体 +// (VolumeGrid),使真实雷达三维体可经现成 loadVolume→addVolume→buildVoxel 显示。 +// +// 数据层方案 A(纯数据层,零 UI/render/controller 改动): +// io::gpr::buildLineVolumeFromGpr3dv(P1/P2 链) → core::BuiltI16(int16+Quant) +// → builtI16ToVolumeGrid(反量化) → data::VolumeGrid(float)。 +// 与现 mock(Api3dRepository 反演剖面 IDW)同格式输出,故下游 addVolume 无需任何改动。 + +// 纯函数适配器:BuiltI16(int16 量化体) → VolumeGrid(float 稠密体)。 +// - 逐体素 Quant::toPhys 反量化;kBlank(空值哨兵)→ NaN(下游 buildVoxel 映射为透明)。 +// - origin/spacing 原样搬运;vmin/vmax 用 BuiltI16 的物理值域。 +// - 布局一致(i 最快、k 最慢),两体可逐体素对位拷贝。 +VolumeGrid builtI16ToVolumeGrid(const geopro::core::BuiltI16& built); + +// 走 P1/P2 链建逐线 GPR 体并适配成 app 的 VolumeGrid。 +// lineDir/linePrefix 同 gpr3dv-smoke / build-line(如 "D:/Downloads/明星路", "明星路_001")。 +// coarse(下采样因子,≥1):沿测线(道/X 轴)每 coarse 道取 1,省内存;横向/深度保全分辨率。 +// 稠密 VolumeGrid 全内存,长线需较大 coarse 控内存(默认 4 = build-line POC 档)。 +// 失败(加载失败/立方体为空)→ 抛 std::runtime_error(由 io::gpr 链抛出,原样透传)。 +VolumeGrid createGprVolumeGrid(const std::string& lineDir, + const std::string& linePrefix, int coarse = 4); + +} // namespace geopro::data + +#endif // GEOPRO_DATA_GPRVOLUMEREPOSITORY_HPP diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 71e235f..efe5575 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -54,6 +54,9 @@ target_sources(geopro_tests PRIVATE data/test_dataset_load_handles.cpp) target_sources(geopro_tests PRIVATE data/test_async_repo_dispatch.cpp) # NavRequest 离线单测(QVariant payload: done/failed/abort 闸门)。 target_sources(geopro_tests PRIVATE data/test_nav_request.cpp) +# GprVolumeRepository:逐线 GPR int16 量化体(BuiltI16)→ app 渲染链 float 体(VolumeGrid)。 +# 纯适配器逐值反量化 + 全链(合成多通道 .iprb 走真 P1/P2)产出有效 VolumeGrid。 +target_sources(geopro_tests PRIVATE data/test_gpr_volume_repository.cpp) target_link_libraries(geopro_tests PRIVATE geopro_data) # store 层:ChunkedVolumeStore(GPR 三维体分块压缩落盘 round-trip + 边缘块 + 压缩生效)。 diff --git a/tests/data/test_gpr_volume_repository.cpp b/tests/data/test_gpr_volume_repository.cpp new file mode 100644 index 0000000..58b8006 --- /dev/null +++ b/tests/data/test_gpr_volume_repository.cpp @@ -0,0 +1,156 @@ +// GprVolumeRepository:逐线 GPR int16 量化体 → app 渲染链 float 体(VolumeGrid)。 +// 1) builtI16ToVolumeGrid 纯适配器:维度/反量化值/spacing/origin/vmin-vmax/kBlank→NaN。 +// 2) createGprVolumeGrid 全链:合成多通道 .iprb 走真 P1/P2 链 → 反量化体维度/spacing 自洽。 + +#include + +#include +#include +#include +#include +#include + +#include "core/algo/GprVolumeBuilder.hpp" +#include "core/model/ScalarVolumeI16.hpp" +#include "data/GprVolumeRepository.hpp" + +namespace fs = std::filesystem; + +namespace { + +// 适配器单测:手搭一个 2x1x2 的 BuiltI16(已知 quant/spacing/origin) → 校验反量化逐值。 +TEST(GprVolumeRepositoryAdapter, DequantDimsSpacingAndBlank) { + geopro::core::BuiltI16 built; + built.vol = geopro::core::ScalarVolumeI16(2, 1, 2); + // Quant: phys = q*scale + offset = q*0.5 + 10。 + built.quant.scale = 0.5; + built.quant.offset = 10.0; + built.origin = {1.0, 2.0, 3.0}; + built.spacing = {0.2, 1.37, 0.05}; + built.vminPhys = 5.0; + built.vmaxPhys = 20.0; + + // 填 4 个体素:3 个正常值 + 1 个 kBlank。 + built.vol.at(0, 0, 0) = 4; // phys = 4*0.5+10 = 12 + built.vol.at(1, 0, 0) = -2; // phys = -2*0.5+10 = 9 + built.vol.at(0, 0, 1) = 20; // phys = 20*0.5+10 = 20 + built.vol.at(1, 0, 1) = geopro::core::ScalarVolumeI16::kBlank; // → NaN + + const geopro::data::VolumeGrid g = geopro::data::builtI16ToVolumeGrid(built); + + // 维度。 + EXPECT_EQ(g.vol.nx(), 2); + EXPECT_EQ(g.vol.ny(), 1); + EXPECT_EQ(g.vol.nz(), 2); + + // 反量化逐值。 + EXPECT_DOUBLE_EQ(g.vol.at(0, 0, 0), 12.0); + EXPECT_DOUBLE_EQ(g.vol.at(1, 0, 0), 9.0); + EXPECT_DOUBLE_EQ(g.vol.at(0, 0, 1), 20.0); + EXPECT_TRUE(std::isnan(g.vol.at(1, 0, 1))); // kBlank → NaN(下游透明) + + // origin/spacing 原样搬运。 + EXPECT_DOUBLE_EQ(g.origin[0], 1.0); + EXPECT_DOUBLE_EQ(g.origin[1], 2.0); + EXPECT_DOUBLE_EQ(g.origin[2], 3.0); + EXPECT_DOUBLE_EQ(g.spacing[0], 0.2); + 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); + EXPECT_TRUE(g.valid()); +} + +// 写一个合成通道:.iprh 文本头 + .iprb 纯 int16 波形([trace*samples + s],s 最快)。 +// 与 test_gpr3dv_volume_bridge 同口径,确保 createGprVolumeGrid 走真 P1/P2 链。 +void writeSyntheticChannel(const fs::path& iprhPath, int samples, int traces, + std::int16_t base, double chYOffset, + double distanceInterval, double timeWindowNs, + double soilVelocity, int channels) { + std::ofstream h(iprhPath); + h << "SAMPLES: " << samples << "\n"; + h << "LAST TRACE: " << (traces - 1) << "\n"; + h << "CHANNELS: " << channels << "\n"; + h << "TIMEWINDOW: " << timeWindowNs << "\n"; + h << "SOIL VELOCITY: " << soilVelocity << "\n"; + h << "DISTANCE INTERVAL: " << distanceInterval << "\n"; + h << "CH_Y_OFFSET: " << chYOffset << "\n"; + h.close(); + + fs::path iprbPath = iprhPath; + iprbPath.replace_extension(".iprb"); + std::ofstream b(iprbPath, std::ios::binary); + for (int t = 0; t < traces; ++t) { + for (int s = 0; s < samples; ++s) { + const std::int16_t v = static_cast(base + t + s); + b.write(reinterpret_cast(&v), sizeof(v)); + } + } +} + +class GprVolumeRepositoryChainTest : public ::testing::Test { + protected: + void SetUp() override { + dir_ = fs::temp_directory_path() / "gpr_volume_repo_test"; + std::error_code ec; + fs::remove_all(dir_, ec); + fs::create_directories(dir_); + } + void TearDown() override { + std::error_code ec; + fs::remove_all(dir_, ec); + } + fs::path dir_; +}; + +TEST_F(GprVolumeRepositoryChainTest, FullChainProducesValidVolumeGrid) { + const int samples = 64; + const int traces = 40; + const int channels = 2; + const double dxHeader = 0.05; + const double timeWindowNs = 100.0; + const double soilVel = 0.1; + + writeSyntheticChannel(dir_ / "syn_001_A01.iprh", samples, traces, + /*base=*/100, /*chYOffset=*/-0.5, dxHeader, timeWindowNs, + soilVel, channels); + writeSyntheticChannel(dir_ / "syn_001_A02.iprh", samples, traces, + /*base=*/300, /*chYOffset=*/0.5, dxHeader, timeWindowNs, + soilVel, channels); + + // coarse=2:沿测线下采样,dx ×2 保形。 + geopro::data::VolumeGrid g; + ASSERT_NO_THROW( + { g = geopro::data::createGprVolumeGrid(dir_.string(), "syn_001", 2); }); + + // 维度:Y=通道数;X/Z 正。 + EXPECT_EQ(g.vol.ny(), channels); + EXPECT_GT(g.vol.nx(), 0); + EXPECT_GT(g.vol.nz(), 0); + + // spacing:X=道距×coarse、Y=通道横距(跨度1.0/(2-1)=1.0)、Z=深度采样距>0。 + EXPECT_DOUBLE_EQ(g.spacing[0], dxHeader * 2); + EXPECT_NEAR(g.spacing[1], 1.0, 1e-6); + EXPECT_GT(g.spacing[2], 0.0); + + // origin=0;值域自洽(vmin<=vmax,搬自 BuiltI16.vminPhys/vmaxPhys,处理后极小合成 + // 数据可能退化为相等区间 → 与 io::gpr 桥接同口径,不强求严格 <)。 + EXPECT_DOUBLE_EQ(g.origin[0], 0.0); + EXPECT_DOUBLE_EQ(g.origin[1], 0.0); + EXPECT_DOUBLE_EQ(g.origin[2], 0.0); + EXPECT_LE(g.vmin, g.vmax); + + // 稠密体:抽查角点为有限值(非 NaN)——GPR 立方体每体素有值,反量化后无空洞。 + EXPECT_TRUE(std::isfinite(g.vol.at(0, 0, 0))); + EXPECT_TRUE(std::isfinite(g.vol.at(g.vol.nx() - 1, g.vol.ny() - 1, + g.vol.nz() - 1))); +} + +TEST_F(GprVolumeRepositoryChainTest, ThrowsOnMissingLine) { + EXPECT_THROW(geopro::data::createGprVolumeGrid(dir_.string(), "nope", 1), + std::runtime_error); +} + +} // namespace