275 lines
10 KiB
C++
275 lines
10 KiB
C++
#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
|
||
|
||
// 整除维度(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);
|
||
}
|