#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); } // ----------------------- 流式金字塔对拍 ----------------------- namespace { // 体素随 (i,j,k) 变化,确保逐块对拍真正区分块内容(非全相同),含 blank 散点。 geopro::core::BuiltI16 makeVaried(int nx, int ny, int nz) { geopro::core::BuiltI16 b; b.vol = geopro::core::ScalarVolumeI16(nx, ny, nz); for (int k = 0; k < nz; ++k) for (int j = 0; j < ny; ++j) for (int i = 0; i < nx; ++i) { const int v = (i * 131 + j * 17 + k * 7) % 251; // 散点 blank,验降采样 blank 混合(部分 blank 取非 blank 均值)。 b.vol.at(i, j, k) = ((i + j + k) % 13 == 0) ? geopro::core::ScalarVolumeI16::kBlank : static_cast(v); } b.quant = {0.5, -3.0}; b.origin = {{10.0, 20.0, 30.0}}; b.spacing = {{2.0, 3.0, 4.0}}; b.vminPhys = -3.0; b.vmaxPhys = 122.0; return b; } // 流式 vs 重组整卷逐块对拍(dims + 每块体素 + min/max + hasRange + levels 数)。 void expectStreamingMatchesInRam(const geopro::core::BuiltI16& b, int brick, int levels) { const auto dirA = (std::filesystem::temp_directory_path() / "gpr_pyr_match_A").string(); const auto dirB = (std::filesystem::temp_directory_path() / "gpr_pyr_match_B").string(); std::filesystem::remove_all(dirA); std::filesystem::remove_all(dirB); ChunkedVolumeStore::write(dirA, b, brick); { ChunkedVolumeStore a(dirA); a.buildPyramid(levels); // 金标准(重组整卷) } ChunkedVolumeStore::write(dirB, b, brick); { ChunkedVolumeStore bb(dirB); bb.buildPyramidStreaming(levels); // 流式 } ChunkedVolumeStore A(dirA), B(dirB); ASSERT_EQ(A.levels(), B.levels()); for (int L = 0; L < A.levels(); ++L) { int anx, any, anz, bnx, bny, bnz; A.dims(L, anx, any, anz); B.dims(L, bnx, bny, bnz); EXPECT_EQ(anx, bnx) << "level " << L << " nx"; EXPECT_EQ(any, bny) << "level " << L << " ny"; EXPECT_EQ(anz, bnz) << "level " << L << " nz"; ASSERT_EQ(A.bricksX(L), B.bricksX(L)); ASSERT_EQ(A.bricksY(L), B.bricksY(L)); ASSERT_EQ(A.bricksZ(L), B.bricksZ(L)); for (int bz = 0; bz < A.bricksZ(L); ++bz) for (int by = 0; by < A.bricksY(L); ++by) for (int bx = 0; bx < A.bricksX(L); ++bx) { EXPECT_EQ(A.readBrick(L, bx, by, bz), B.readBrick(L, bx, by, bz)) << "voxels mismatch L=" << L << " (" << bx << "," << by << "," << bz << ")"; EXPECT_EQ(A.brickRange(L, bx, by, bz), B.brickRange(L, bx, by, bz)) << "range mismatch L=" << L << " (" << bx << "," << by << "," << bz << ")"; } } std::filesystem::remove_all(dirA); std::filesystem::remove_all(dirB); } } // namespace // 整除维度(128,brick64 → 2×2×2 满邻块):流式与重组整卷逐块一致。 TEST(Pyramid, StreamingMatchesInRam) { expectStreamingMatchesInRam(makeVaried(128, 128, 128), 64, 2); } // 非整除/奇数维度(100、127),验边缘块 + 奇数维降采样一致。 TEST(Pyramid, StreamingMatchesInRamNonDivisible) { expectStreamingMatchesInRam(makeVaried(100, 100, 100), 64, 2); expectStreamingMatchesInRam(makeVaried(127, 127, 127), 64, 3); } // 非立方 + 小 brick(多级、边缘块更碎):流式与重组整卷逐块一致。 TEST(Pyramid, StreamingMatchesInRamAnisotropicSmallBrick) { expectStreamingMatchesInRam(makeVaried(70, 33, 50), 32, 3); } // 全 blank 体流式降采样:各级仍全 blank(min/max=kBlank),与重组整卷一致。 TEST(Pyramid, StreamingMatchesInRamAllBlank) { geopro::core::BuiltI16 b; b.vol = geopro::core::ScalarVolumeI16(70, 70, 70); 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; expectStreamingMatchesInRam(b, 32, 2); } // 流式不破坏 level0:兼容重载等价。 TEST(Pyramid, StreamingLevel0ReadCompatUnchanged) { auto dir = (std::filesystem::temp_directory_path() / "gpr_pyr_s_compat").string(); std::filesystem::remove_all(dir); ChunkedVolumeStore::write(dir, makeRamp(64), 32); ChunkedVolumeStore s(dir); s.buildPyramidStreaming(1); EXPECT_EQ(s.readBrick(0, 0, 0), s.readBrick(0, 0, 0, 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); }