geopro/tests/data/store/test_pyramid.cpp

275 lines
10 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.

#include "data/store/ChunkedVolumeStore.hpp"
#include "core/algo/GprVolumeBuilder.hpp"
#include <gtest/gtest.h>
#include <filesystem>
#include <cstdint>
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<std::int16_t>(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
// 整除维度128brick64 → 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 体流式降采样:各级仍全 blankmin/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);
}