fix(store): brickRange 用 hasRange 标志替代 (0,0) 哨兵

(0,0) 是合法值域(真实全零块,kBlank=INT16_MIN 非 0),旧实现用
(vmin==0&&vmax==0) 当未计算哨兵会误判,导致全零块每次 brickRange
都无谓解压重算,且 buildPyramid 后仍走惰性。

- BrickEntry 加 bool hasRange 显式标志
- brickRange: hasRange 真→直接返回;假→惰性算并就地缓存(mutable levels_)
- meta.json 序列化/反序列化带 hasRange(老 store 缺字段→false,惰性兼容)
- buildPyramid 回填值域时一并置 hasRange=true
- 补测试:真实全零块 brickRange 返回 (0,0) 不退化(金字塔/老 store 两路)
This commit is contained in:
gaozheng 2026-06-23 12:02:17 +08:00
parent c21226a3d7
commit 86e2b6b8a8
3 changed files with 78 additions and 9 deletions

View File

@ -194,7 +194,9 @@ ChunkedVolumeStore::ChunkedVolumeStore(const std::string& dir) : dir_(dir) {
be.bw = e.at("bw").get<int>();
be.bh = e.at("bh").get<int>();
be.bd = e.at("bd").get<int>();
// 老 store 无 min/max缺失时置 0brickRange(0,...) 会惰性算)。
// hasRange 显式标志:老 store 无此字段 → falsebrickRange 走惰性算)。
// 若有 min/max 字段则视为已算(兼容 buildPyramid 之前写的、只带 min/max 的 meta
be.hasRange = e.value("hasRange", e.contains("min") && e.contains("max"));
be.vmin = e.value("min", static_cast<std::int16_t>(0));
be.vmax = e.value("max", static_cast<std::int16_t>(0));
bricks_.push_back(be);
@ -236,6 +238,8 @@ ChunkedVolumeStore::ChunkedVolumeStore(const std::string& dir) : dir_(dir) {
be.bw = e.at("bw").get<int>();
be.bh = e.at("bh").get<int>();
be.bd = e.at("bd").get<int>();
// 降采样级由 buildPyramid 写入,恒带 min/max → hasRange 默认 true。
be.hasRange = e.value("hasRange", true);
be.vmin = e.value("min", kBlank);
be.vmax = e.value("max", kBlank);
lv.bricks.push_back(be);
@ -249,10 +253,13 @@ ChunkedVolumeStore::ChunkedVolumeStore(const std::string& dir) : dir_(dir) {
for (std::size_t i = 0; i < jb0.size(); ++i) {
const std::int16_t mn = jb0[i].value("min", kBlank);
const std::int16_t mx = jb0[i].value("max", kBlank);
const bool hr = jb0[i].value("hasRange", jb0[i].contains("min"));
levels_[0].bricks[i].vmin = mn;
levels_[0].bricks[i].vmax = mx;
levels_[0].bricks[i].hasRange = hr;
bricks_[i].vmin = mn;
bricks_[i].vmax = mx;
bricks_[i].hasRange = hr;
}
}
}
@ -322,18 +329,27 @@ std::vector<std::int16_t> ChunkedVolumeStore::readBrick(int level, int bx,
std::pair<std::int16_t, std::int16_t> ChunkedVolumeStore::brickRange(
int level, int bx, int by, int bz) const {
const Level& lv = levelAt(level);
if (level < 0 || level >= static_cast<int>(levels_.size())) {
throw std::out_of_range("ChunkedVolumeStore: level out of range");
}
Level& lv = levels_[static_cast<std::size_t>(level)]; // mutable可缓存惰性结果
if (bx < 0 || by < 0 || bz < 0 || bx >= lv.bx || by >= lv.by || bz >= lv.bz) {
throw std::out_of_range("ChunkedVolumeStore::brickRange: brick index out of range");
}
const BrickEntry& be =
BrickEntry& be =
lv.bricks.at(static_cast<std::size_t>(brickIndexAt(lv, bx, by, bz)));
// level 0 老 store 无存储 min/maxvmin==vmax==0 哨兵):惰性读块计算。
if (level == 0 && be.vmin == 0 && be.vmax == 0) {
return computeRange(readBrickFrom(lv, bx, by, bz));
}
// 已算buildPyramid 时算过、或之前惰性算并缓存)→ 直接返回。
// 注意:用 hasRange 显式标志而非 (0,0) 哨兵——(0,0) 是合法值域,且 kBlank!=0。
if (be.hasRange) {
return {be.vmin, be.vmax};
}
// 未算(老 store level 0惰性读块计算并就地缓存 + 置 hasRange。
const auto rng = computeRange(readBrickFrom(lv, bx, by, bz));
be.vmin = rng.first;
be.vmax = rng.second;
be.hasRange = true;
return rng;
}
void ChunkedVolumeStore::buildPyramid(int levels) {
// 参数 levels = 最高级索引level 1..levels 为降采样级);总级数(含 level 0
@ -467,6 +483,7 @@ void ChunkedVolumeStore::buildPyramid(int levels) {
be.bd = bd;
be.vmin = rng.first;
be.vmax = rng.second;
be.hasRange = true; // buildPyramid 现算现存,值域确定。
lv.bricks.push_back(be);
offset += clen;
}
@ -496,6 +513,7 @@ void ChunkedVolumeStore::buildPyramid(int levels) {
for (std::size_t i = 0; i < jb.size(); ++i) {
jb[i]["min"] = levels_[0].bricks[i].vmin;
jb[i]["max"] = levels_[0].bricks[i].vmax;
jb[i]["hasRange"] = levels_[0].bricks[i].hasRange;
}
}
}
@ -510,7 +528,8 @@ void ChunkedVolumeStore::buildPyramid(int levels) {
{"bh", be.bh},
{"bd", be.bd},
{"min", be.vmin},
{"max", be.vmax}});
{"max", be.vmax},
{"hasRange", be.hasRange}});
}
jlevels.push_back(json{{"nx", lv.nx},
{"ny", lv.ny},

View File

@ -87,6 +87,9 @@ class ChunkedVolumeStore {
std::int64_t compressedLen = 0;
int bw = 0, bh = 0, bd = 0;
std::int16_t vmin = 0, vmax = 0; // 块内 (min,max),跳过 kBlank全 blank=(kBlank,kBlank)
// 显式「值域已算」标志:替代 (0,0) 哨兵。(0,0) 是合法值域,不能当未计算用。
// false → brickRange 惰性读块计算并缓存true → 直接返回 (vmin,vmax)。
bool hasRange = false;
};
// 一个分辨率级:维度 + 块数 + 逐块索引 + 数据文件名。
@ -110,7 +113,8 @@ class ChunkedVolumeStore {
std::vector<BrickEntry> bricks_; // = level 0 索引兼容字段readBrick(bx,by,bz) 用)
int levelCount_ = 1;
std::vector<Level> levels_; // levels_[0] 即 level 0与 bricks_ 同源)
// mutablebrickRange 为 const但惰性算出值域后需就地缓存置 hasRange=true
mutable std::vector<Level> levels_; // levels_[0] 即 level 0与 bricks_ 同源)
};
} // namespace geopro::data

View File

@ -98,6 +98,52 @@ TEST(Pyramid, AllBlankBrickRange) {
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();