207 lines
8.5 KiB
C++
207 lines
8.5 KiB
C++
// 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);
|
||
|
||
// 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);
|
||
}
|
||
|
||
// 规范化链(.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
|