geopro/tests/data/test_gpr_volume_repository.cpp

207 lines
8.5 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// GprVolumeRepository逐线 GPR int16 量化体 → app 渲染链 float 体(VolumeGrid)。
// 1) builtI16ToVolumeGrid 纯适配器:维度/反量化值/spacing/origin/vmin-vmax/kBlank→NaN。
// 2) createGprVolumeGrid 全链:合成多通道 .iprb 走真 P1/P2 链 → 反量化体维度/spacing 自洽。
#include <gtest/gtest.h>
#include <cmath>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <string>
#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);
// 显示值域 = 双极对称窗口(以中位数为中心),非全 vminPhys/vmaxPhys。3 个有效体素 {9,12,20}
// 中位数=12 → 窗口对称、【中点=中位数 12】(灰点落基线)。vmin<vmax。
EXPECT_LT(g.vmin, g.vmax);
EXPECT_NEAR(0.5 * (g.vmin + g.vmax), 12.0, 1e-9); // 对称中点=中位数(基线→中灰)
EXPECT_TRUE(g.valid());
}
// 双极对称显示窗口:强离群(模拟原始 GPR 首波/路面/int16 饱和钳值)落在 1% 尾外应被裁剪,
// 窗口对称且中点落基线(中位数),而非被 ±极值撑满(否则结构压成中灰"灰板"或过饱和)。
TEST(GprVolumeRepositoryAdapter, RobustDisplayRangeClipsOutliers) {
geopro::core::BuiltI16 built;
built.vol = geopro::core::ScalarVolumeI16(1004, 1, 1);
built.quant.scale = 1.0; // phys = q
built.quant.offset = 0.0;
built.origin = {0.0, 0.0, 0.0};
built.spacing = {0.1, 0.1, 0.05};
built.vminPhys = -30000.0; // 全值域(含离群)——不应被采用为显示值域
built.vmaxPhys = 30000.0;
// 1000 个"结构"体素 q=-500..499(基线≈0) + 各 2 个 ±30000 强离群(共 1004离群 0.4% < 1% 尾)。
for (int i = 0; i < 1000; ++i)
built.vol.at(i, 0, 0) = static_cast<std::int16_t>(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,
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<std::int16_t>(base + t + s);
b.write(reinterpret_cast<const char*>(&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);
// spacingX=道距×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);
}
// 规范化链(.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<std::int16_t>(t * 10 + c * 100 + s);
f.write(reinterpret_cast<const char*>(&v), 2); } }
const auto grid = geopro::data::createRadarVolumeGrid(dir.string(), "L", 1, 0.0);
EXPECT_EQ(grid.vol.nx(), 4);
EXPECT_EQ(grid.vol.ny(), 2);
EXPECT_EQ(grid.vol.nz(), 3);
EXPECT_GT(grid.vmax, grid.vmin);
}
} // namespace