#include "data/store/ChunkedVolumeStore.hpp" #include "core/algo/GprVolumeBuilder.hpp" #include #include #include using namespace geopro::data; namespace { geopro::core::BuiltI16 makeRamp(int n) { geopro::core::BuiltI16 b; b.vol = geopro::core::ScalarVolumeI16(n, n, n); for (int k = 0; k < n; k++) for (int j = 0; j < n; j++) for (int i = 0; i < n; i++) b.vol.at(i, j, k) = (short)(i); // 沿 x 斜坡 b.quant = {1.0, 0.0}; b.origin = {{0, 0, 0}}; b.spacing = {{1, 1, 1}}; b.vminPhys = 0; b.vmaxPhys = n; return b; } } // namespace TEST(Pyramid, BuildsHalfResLevelsAndRanges) { auto dir = (std::filesystem::temp_directory_path() / "gpr_pyr").string(); std::filesystem::remove_all(dir); ChunkedVolumeStore::write(dir, makeRamp(64), 32); ChunkedVolumeStore s(dir); s.buildPyramid(2); // level 0(64³),1(32³),2(16³) EXPECT_GE(s.levels(), 3); int nx, ny, nz; s.dims(1, nx, ny, nz); EXPECT_EQ(nx, 32); EXPECT_EQ(ny, 32); EXPECT_EQ(nz, 32); // level0 块0 的 range:x 斜坡 0..31(brick32) → min=0,max=31 auto r0 = s.brickRange(0, 0, 0, 0); EXPECT_EQ(r0.first, 0); EXPECT_EQ(r0.second, 31); auto blk1 = s.readBrick(1, 0, 0, 0); // level1 块,降采样后非空 EXPECT_FALSE(blk1.empty()); std::filesystem::remove_all(dir); } TEST(Pyramid, Level0ReadCompatUnchanged) { auto dir = (std::filesystem::temp_directory_path() / "gpr_pyr_compat").string(); std::filesystem::remove_all(dir); ChunkedVolumeStore::write(dir, makeRamp(64), 32); ChunkedVolumeStore s(dir); s.buildPyramid(1); EXPECT_EQ(s.readBrick(0, 0, 0), s.readBrick(0, 0, 0, 0)); // 兼容重载等价 std::filesystem::remove_all(dir); } // 降采样语义:level1(0,0,0) 由 level0 每 2×2×2 平均得到。 // x 斜坡:level1 体素 i 来自 level0 的 (2i, 2i+1) 平均 = round((2i + 2i+1)/2) = 2i(取整向偶或截断, // 这里两值差 1,平均 2i+0.5,round→2i+1 或 2i 取决实现;仅校验单调与范围)。 TEST(Pyramid, DownsampledRangeWithinSource) { auto dir = (std::filesystem::temp_directory_path() / "gpr_pyr_ds").string(); std::filesystem::remove_all(dir); ChunkedVolumeStore::write(dir, makeRamp(64), 32); ChunkedVolumeStore s(dir); s.buildPyramid(2); // level1 维度 32³;以 brick32 仍是 1 块(覆盖 i=0..31 → 源 i=0..63)。 int nx, ny, nz; s.dims(1, nx, ny, nz); EXPECT_EQ(nx, 32); auto r1 = s.brickRange(1, 0, 0, 0); // 降采样值落在源范围 [0,63] 内,且块覆盖全 x → min≈0, max≈63 附近(round 后)。 EXPECT_GE(r1.first, 0); EXPECT_LE(r1.second, 63); EXPECT_LT(r1.first, r1.second); // 斜坡降采样后仍有跨度 // level1 块尺寸 = 32³。 auto blk1 = s.readBrick(1, 0, 0, 0); EXPECT_EQ(blk1.size(), 32u * 32 * 32); std::filesystem::remove_all(dir); } // 全 blank 块 → range 记 (kBlank,kBlank),降采样后该区域仍 blank。 TEST(Pyramid, AllBlankBrickRange) { auto dir = (std::filesystem::temp_directory_path() / "gpr_pyr_blank").string(); std::filesystem::remove_all(dir); geopro::core::BuiltI16 b; b.vol = geopro::core::ScalarVolumeI16(64, 64, 64); for (auto& v : b.vol.data()) v = geopro::core::ScalarVolumeI16::kBlank; b.quant = {1.0, 0.0}; b.origin = {{0, 0, 0}}; b.spacing = {{1, 1, 1}}; b.vminPhys = 0; b.vmaxPhys = 0; ChunkedVolumeStore::write(dir, b, 32); ChunkedVolumeStore s(dir); s.buildPyramid(2); auto r0 = s.brickRange(0, 0, 0, 0); EXPECT_EQ(r0.first, geopro::core::ScalarVolumeI16::kBlank); EXPECT_EQ(r0.second, geopro::core::ScalarVolumeI16::kBlank); auto r1 = s.brickRange(1, 0, 0, 0); EXPECT_EQ(r1.first, geopro::core::ScalarVolumeI16::kBlank); std::filesystem::remove_all(dir); } // 真实全零块(非 blank):brickRange 返回 (0,0) 且不退化为惰性。 // (0,0) 是合法值域,旧实现用 (vmin==0&&vmax==0) 当「未计算」哨兵会误判; // hasRange 标志修正后:buildPyramid 算出的全零块 hasRange=true,返回 (0,0)。 TEST(Pyramid, RealAllZeroBrickRangeIsZeroZeroNotDegenerate) { auto dir = (std::filesystem::temp_directory_path() / "gpr_pyr_zero").string(); std::filesystem::remove_all(dir); geopro::core::BuiltI16 b; b.vol = geopro::core::ScalarVolumeI16(64, 64, 64); for (auto& v : b.vol.data()) v = 0; // 真实 0(非 kBlank) b.quant = {1.0, 0.0}; b.origin = {{0, 0, 0}}; b.spacing = {{1, 1, 1}}; b.vminPhys = 0; b.vmaxPhys = 0; ChunkedVolumeStore::write(dir, b, 32); ChunkedVolumeStore s(dir); s.buildPyramid(1); auto r0 = s.brickRange(0, 0, 0, 0); EXPECT_EQ(r0.first, 0); EXPECT_EQ(r0.second, 0); // 合法 (0,0),不退化、不是 kBlank EXPECT_NE(r0.first, geopro::core::ScalarVolumeI16::kBlank); std::filesystem::remove_all(dir); } // 老 store 全零块(无金字塔):首次 brickRange 惰性算出 (0,0),不无限退化。 TEST(Pyramid, LegacyRealAllZeroBrickRangeIsZeroZero) { auto dir = (std::filesystem::temp_directory_path() / "gpr_pyr_zero_legacy").string(); std::filesystem::remove_all(dir); geopro::core::BuiltI16 b; b.vol = geopro::core::ScalarVolumeI16(64, 64, 64); for (auto& v : b.vol.data()) v = 0; b.quant = {1.0, 0.0}; b.origin = {{0, 0, 0}}; b.spacing = {{1, 1, 1}}; b.vminPhys = 0; b.vmaxPhys = 0; ChunkedVolumeStore::write(dir, b, 32); ChunkedVolumeStore s(dir); // 未 buildPyramid:老 store,块无 hasRange auto r0 = s.brickRange(0, 0, 0, 0); // 惰性算 → (0,0) EXPECT_EQ(r0.first, 0); EXPECT_EQ(r0.second, 0); std::filesystem::remove_all(dir); } // 老 store(未 buildPyramid):levels()==1;brickRange(0,...) 仍可惰性算。 TEST(Pyramid, LegacyStoreNoPyramidLevelsIsOne) { auto dir = (std::filesystem::temp_directory_path() / "gpr_pyr_legacy").string(); std::filesystem::remove_all(dir); ChunkedVolumeStore::write(dir, makeRamp(64), 32); ChunkedVolumeStore s(dir); // 未调用 buildPyramid EXPECT_EQ(s.levels(), 1); EXPECT_EQ(s.bricksX(), s.bricksX(0)); auto r0 = s.brickRange(0, 0, 0, 0); // 惰性算 EXPECT_EQ(r0.first, 0); EXPECT_EQ(r0.second, 31); std::filesystem::remove_all(dir); }