feat/vtk-3d-view #7

Merged
gaozheng merged 301 commits from feat/vtk-3d-view into main 2026-06-27 18:43:52 +08:00
4 changed files with 499 additions and 13 deletions
Showing only changes of commit 687edfeca1 - Show all commits

View File

@ -3,12 +3,16 @@
#include <QByteArray> #include <QByteArray>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <algorithm>
#include <cmath>
#include <cstring> #include <cstring>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <limits>
#include <stdexcept> #include <stdexcept>
#include "core/algo/GprVolumeBuilder.hpp" // geopro::core::BuiltI16 #include "core/algo/GprVolumeBuilder.hpp" // geopro::core::BuiltI16
#include "model/ScalarVolumeI16.hpp" // geopro::core::ScalarVolumeI16::kBlank
namespace geopro::data { namespace geopro::data {
@ -19,8 +23,31 @@ namespace fs = std::filesystem;
constexpr const char* kMetaFile = "meta.json"; constexpr const char* kMetaFile = "meta.json";
constexpr const char* kDataFile = "data.bin"; constexpr const char* kDataFile = "data.bin";
constexpr std::int16_t kBlank = geopro::core::ScalarVolumeI16::kBlank;
std::string levelDataFile(int level) {
return level == 0 ? std::string(kDataFile)
: "data_L" + std::to_string(level) + ".bin";
}
int ceilDiv(int n, int brick) { return (n + brick - 1) / brick; } int ceilDiv(int n, int brick) { return (n + brick - 1) / brick; }
// 块内 (min,max),跳过 kBlank全 blank → (kBlank,kBlank)。
std::pair<std::int16_t, std::int16_t> computeRange(
const std::vector<std::int16_t>& blk) {
std::int16_t lo = std::numeric_limits<std::int16_t>::max();
std::int16_t hi = std::numeric_limits<std::int16_t>::min();
bool any = false;
for (std::int16_t v : blk) {
if (v == kBlank) continue;
any = true;
if (v < lo) lo = v;
if (v > hi) hi = v;
}
if (!any) return {kBlank, kBlank};
return {lo, hi};
}
// 块尺寸(边缘块 < brick第 b 块沿该轴的体素数。 // 块尺寸(边缘块 < brick第 b 块沿该轴的体素数。
int extent(int n, int b, int brick) { int extent(int n, int b, int brick) {
const int got = n - b * brick; const int got = n - b * brick;
@ -167,30 +194,118 @@ ChunkedVolumeStore::ChunkedVolumeStore(const std::string& dir) : dir_(dir) {
be.bw = e.at("bw").get<int>(); be.bw = e.at("bw").get<int>();
be.bh = e.at("bh").get<int>(); be.bh = e.at("bh").get<int>();
be.bd = e.at("bd").get<int>(); be.bd = e.at("bd").get<int>();
// 老 store 无 min/max缺失时置 0brickRange(0,...) 会惰性算)。
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); bricks_.push_back(be);
} }
// 始终从 bricks_ 重建 level 0与兼容索引同源复用 data.bin
levels_.clear();
Level lv0;
lv0.nx = meta_.nx;
lv0.ny = meta_.ny;
lv0.nz = meta_.nz;
lv0.bx = bricksX_;
lv0.by = bricksY_;
lv0.bz = bricksZ_;
lv0.dataFile = kDataFile;
lv0.bricks = bricks_;
levels_.push_back(std::move(lv0));
// 若 meta.json 含金字塔buildPyramid 写入的 levels 数组),加载附加级。
// levels[0] 与上面的 level 0 一致跳过levels[1..] 各自独立数据文件。
if (meta.contains("levels") && meta.at("levels").is_array()) {
const auto& la = meta.at("levels");
for (std::size_t L = 1; L < la.size(); ++L) {
const auto& je = la[L];
Level lv;
lv.nx = je.at("nx").get<int>();
lv.ny = je.at("ny").get<int>();
lv.nz = je.at("nz").get<int>();
lv.bx = ceilDiv(lv.nx, meta_.brick);
lv.by = ceilDiv(lv.ny, meta_.brick);
lv.bz = ceilDiv(lv.nz, meta_.brick);
lv.dataFile = je.value("dataFile", levelDataFile(static_cast<int>(L)));
const auto& jb = je.at("bricks");
lv.bricks.reserve(jb.size());
for (const auto& e : jb) {
BrickEntry be;
be.offset = e.at("offset").get<std::int64_t>();
be.compressedLen = e.at("compressedLen").get<std::int64_t>();
be.bw = e.at("bw").get<int>();
be.bh = e.at("bh").get<int>();
be.bd = e.at("bd").get<int>();
be.vmin = e.value("min", kBlank);
be.vmax = e.value("max", kBlank);
lv.bricks.push_back(be);
}
levels_.push_back(std::move(lv));
}
// 若 meta 含 level0 的 min/maxbuildPyramid 写过),回填 level 0 与 bricks_。
if (!la.empty() && la[0].contains("bricks")) {
const auto& jb0 = la[0].at("bricks");
if (jb0.size() == levels_[0].bricks.size()) {
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);
levels_[0].bricks[i].vmin = mn;
levels_[0].bricks[i].vmax = mx;
bricks_[i].vmin = mn;
bricks_[i].vmax = mx;
}
}
}
}
levelCount_ = static_cast<int>(levels_.size());
} }
std::vector<std::int16_t> ChunkedVolumeStore::readBrick(int bx, int by, std::vector<std::int16_t> ChunkedVolumeStore::readBrick(int bx, int by,
int bz) const { int bz) const {
if (bx < 0 || by < 0 || bz < 0 || bx >= bricksX_ || by >= bricksY_ || return readBrick(0, bx, by, bz);
bz >= bricksZ_) { }
// ----------------------- 金字塔实现 -----------------------
const ChunkedVolumeStore::Level& ChunkedVolumeStore::levelAt(int level) const {
if (level < 0 || level >= static_cast<int>(levels_.size())) {
throw std::out_of_range("ChunkedVolumeStore: level out of range");
}
return levels_[static_cast<std::size_t>(level)];
}
int ChunkedVolumeStore::bricksX(int level) const { return levelAt(level).bx; }
int ChunkedVolumeStore::bricksY(int level) const { return levelAt(level).by; }
int ChunkedVolumeStore::bricksZ(int level) const { return levelAt(level).bz; }
void ChunkedVolumeStore::dims(int level, int& nx, int& ny, int& nz) const {
const Level& lv = levelAt(level);
nx = lv.nx;
ny = lv.ny;
nz = lv.nz;
}
std::vector<std::int16_t> ChunkedVolumeStore::readBrickFrom(const Level& lv,
int bx, int by,
int bz) const {
if (bx < 0 || by < 0 || bz < 0 || bx >= lv.bx || by >= lv.by || bz >= lv.bz) {
throw std::out_of_range("ChunkedVolumeStore::readBrick: brick index out of range"); throw std::out_of_range("ChunkedVolumeStore::readBrick: brick index out of range");
} }
const BrickEntry& be = bricks_.at(static_cast<std::size_t>(brickIndex(bx, by, bz))); const BrickEntry& be =
lv.bricks.at(static_cast<std::size_t>(brickIndexAt(lv, bx, by, bz)));
std::ifstream data((fs::path(dir_) / kDataFile).string(), std::ios::binary); std::ifstream data((fs::path(dir_) / lv.dataFile).string(), std::ios::binary);
if (!data) throw std::runtime_error("ChunkedVolumeStore: cannot open data.bin"); if (!data) throw std::runtime_error("ChunkedVolumeStore: cannot open level data file");
data.seekg(static_cast<std::streamoff>(be.offset), std::ios::beg); data.seekg(static_cast<std::streamoff>(be.offset), std::ios::beg);
QByteArray compressed; QByteArray compressed;
compressed.resize(static_cast<int>(be.compressedLen)); compressed.resize(static_cast<int>(be.compressedLen));
data.read(compressed.data(), be.compressedLen); data.read(compressed.data(), be.compressedLen);
if (!data) throw std::runtime_error("ChunkedVolumeStore: data.bin read failed"); if (!data) throw std::runtime_error("ChunkedVolumeStore: level data read failed");
const QByteArray raw = qUncompress(compressed); const QByteArray raw = qUncompress(compressed);
const std::size_t count = const std::size_t count = static_cast<std::size_t>(be.bw) * be.bh * be.bd;
static_cast<std::size_t>(be.bw) * be.bh * be.bd;
if (static_cast<std::size_t>(raw.size()) != count * sizeof(std::int16_t)) { if (static_cast<std::size_t>(raw.size()) != count * sizeof(std::int16_t)) {
throw std::runtime_error("ChunkedVolumeStore: decompressed size mismatch"); throw std::runtime_error("ChunkedVolumeStore: decompressed size mismatch");
} }
@ -200,4 +315,215 @@ std::vector<std::int16_t> ChunkedVolumeStore::readBrick(int bx, int by,
return out; return out;
} }
std::vector<std::int16_t> ChunkedVolumeStore::readBrick(int level, int bx,
int by, int bz) const {
return readBrickFrom(levelAt(level), bx, by, bz);
}
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 (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 =
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));
}
return {be.vmin, be.vmax};
}
void ChunkedVolumeStore::buildPyramid(int levels) {
// 参数 levels = 最高级索引level 1..levels 为降采样级);总级数(含 level 0
// = levels + 1。levels<=0 视为仅 level 0。
const int target = levels < 0 ? 1 : levels + 1;
const int brick = meta_.brick;
// 1) 重建所有级到内存(整卷重组 → 逐级 2× 平均降采样)。
// level 0 直接从已存块重组,避免依赖外部 BuiltI16。
struct DenseLevel {
int nx, ny, nz;
std::vector<std::int16_t> vox; // i 最快、k 最慢
};
auto idxAt = [](int nx, int ny, int i, int j, int k) -> std::size_t {
return (static_cast<std::size_t>(k) * ny + j) * nx + i;
};
// 重组 level 0 整卷。
DenseLevel d0;
d0.nx = meta_.nx;
d0.ny = meta_.ny;
d0.nz = meta_.nz;
d0.vox.assign(static_cast<std::size_t>(d0.nx) * d0.ny * d0.nz, kBlank);
const Level& lv0 = levels_[0];
for (int bz = 0; bz < lv0.bz; ++bz) {
for (int by = 0; by < lv0.by; ++by) {
for (int bx = 0; bx < lv0.bx; ++bx) {
const auto blk = readBrickFrom(lv0, bx, by, bz);
const BrickEntry& be =
lv0.bricks[static_cast<std::size_t>(brickIndexAt(lv0, bx, by, bz))];
const int i0 = bx * brick, j0 = by * brick, k0 = bz * brick;
std::size_t w = 0;
for (int kk = 0; kk < be.bd; ++kk)
for (int jj = 0; jj < be.bh; ++jj)
for (int ii = 0; ii < be.bw; ++ii)
d0.vox[idxAt(d0.nx, d0.ny, i0 + ii, j0 + jj, k0 + kk)] = blk[w++];
}
}
}
std::vector<DenseLevel> dense;
dense.push_back(std::move(d0));
for (int L = 1; L < target; ++L) {
const DenseLevel& src = dense.back();
DenseLevel dst;
dst.nx = std::max(1, (src.nx + 1) / 2);
dst.ny = std::max(1, (src.ny + 1) / 2);
dst.nz = std::max(1, (src.nz + 1) / 2);
dst.vox.assign(static_cast<std::size_t>(dst.nx) * dst.ny * dst.nz, kBlank);
for (int k = 0; k < dst.nz; ++k) {
for (int j = 0; j < dst.ny; ++j) {
for (int i = 0; i < dst.nx; ++i) {
// 2×2×2 邻域非 blank 平均round全 blank → kBlank。
long sum = 0;
int cnt = 0;
for (int dk = 0; dk < 2; ++dk) {
const int sk = 2 * k + dk;
if (sk >= src.nz) continue;
for (int dj = 0; dj < 2; ++dj) {
const int sj = 2 * j + dj;
if (sj >= src.ny) continue;
for (int di = 0; di < 2; ++di) {
const int si = 2 * i + di;
if (si >= src.nx) continue;
const std::int16_t v =
src.vox[idxAt(src.nx, src.ny, si, sj, sk)];
if (v == kBlank) continue;
sum += v;
++cnt;
}
}
}
if (cnt > 0) {
const long avg = std::lround(static_cast<double>(sum) / cnt);
dst.vox[idxAt(dst.nx, dst.ny, i, j, k)] =
static_cast<std::int16_t>(avg);
}
}
}
}
dense.push_back(std::move(dst));
}
// 2) 逐级落盘(分块 qCompress+ 计算每块 min/max重建 levels_。
std::vector<Level> rebuilt;
rebuilt.reserve(dense.size());
for (std::size_t L = 0; L < dense.size(); ++L) {
const DenseLevel& dl = dense[L];
Level lv;
lv.nx = dl.nx;
lv.ny = dl.ny;
lv.nz = dl.nz;
lv.bx = ceilDiv(dl.nx, brick);
lv.by = ceilDiv(dl.ny, brick);
lv.bz = ceilDiv(dl.nz, brick);
lv.dataFile = levelDataFile(static_cast<int>(L));
std::ofstream data((fs::path(dir_) / lv.dataFile).string(),
std::ios::binary | std::ios::trunc);
if (!data) throw std::runtime_error("ChunkedVolumeStore: cannot open level data for write");
std::int64_t offset = 0;
for (int bz = 0; bz < lv.bz; ++bz) {
for (int by = 0; by < lv.by; ++by) {
for (int bx = 0; bx < lv.bx; ++bx) {
const int bw = std::min(brick, dl.nx - bx * brick);
const int bh = std::min(brick, dl.ny - by * brick);
const int bd = std::min(brick, dl.nz - bz * brick);
std::vector<std::int16_t> blk(static_cast<std::size_t>(bw) * bh * bd);
const int i0 = bx * brick, j0 = by * brick, k0 = bz * brick;
std::size_t w = 0;
for (int kk = 0; kk < bd; ++kk)
for (int jj = 0; jj < bh; ++jj)
for (int ii = 0; ii < bw; ++ii)
blk[w++] = dl.vox[idxAt(dl.nx, dl.ny, i0 + ii, j0 + jj, k0 + kk)];
const int rawBytes =
static_cast<int>(blk.size() * sizeof(std::int16_t));
const QByteArray compressed = qCompress(
reinterpret_cast<const uchar*>(blk.data()), rawBytes);
const std::int64_t clen = compressed.size();
data.write(compressed.constData(), clen);
if (!data) throw std::runtime_error("ChunkedVolumeStore: level data write failed");
const auto rng = computeRange(blk);
BrickEntry be;
be.offset = offset;
be.compressedLen = clen;
be.bw = bw;
be.bh = bh;
be.bd = bd;
be.vmin = rng.first;
be.vmax = rng.second;
lv.bricks.push_back(be);
offset += clen;
}
}
}
data.close();
rebuilt.push_back(std::move(lv));
}
levels_ = std::move(rebuilt);
levelCount_ = static_cast<int>(levels_.size());
// 兼容字段level 0 索引(含新算的 min/max回写 bricks_。
bricks_ = levels_[0].bricks;
// 3) 重写 meta.json保留所有原字段含原 bricks追加/覆盖 levels 数组。
const fs::path metaPath = fs::path(dir_) / kMetaFile;
json meta;
{
std::ifstream in(metaPath.string());
if (!in) throw std::runtime_error("ChunkedVolumeStore: cannot open meta.json");
in >> meta;
}
// 原 brickslevel 0 兼容索引)补上 min/max保持 readBrick/老读取不变。
if (meta.contains("bricks") && meta.at("bricks").is_array()) {
auto& jb = meta.at("bricks");
if (jb.size() == levels_[0].bricks.size()) {
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;
}
}
}
json jlevels = json::array();
for (std::size_t L = 0; L < levels_.size(); ++L) {
const Level& lv = levels_[L];
json jbricks = json::array();
for (const auto& be : lv.bricks) {
jbricks.push_back(json{{"offset", be.offset},
{"compressedLen", be.compressedLen},
{"bw", be.bw},
{"bh", be.bh},
{"bd", be.bd},
{"min", be.vmin},
{"max", be.vmax}});
}
jlevels.push_back(json{{"nx", lv.nx},
{"ny", lv.ny},
{"nz", lv.nz},
{"dataFile", lv.dataFile},
{"bricks", std::move(jbricks)}});
}
meta["levels"] = std::move(jlevels);
std::ofstream metaOut(metaPath.string(), std::ios::trunc);
if (!metaOut) throw std::runtime_error("ChunkedVolumeStore: cannot open meta.json for write");
metaOut << meta.dump(2);
if (!metaOut) throw std::runtime_error("ChunkedVolumeStore: meta.json write failed");
}
} // namespace geopro::data } // namespace geopro::data

View File

@ -4,6 +4,7 @@
#include <array> #include <array>
#include <cstdint> #include <cstdint>
#include <string> #include <string>
#include <utility>
#include <vector> #include <vector>
#include "model/ScalarVolumeI16.hpp" // geopro::core::Quant #include "model/ScalarVolumeI16.hpp" // geopro::core::Quant
@ -43,29 +44,73 @@ class ChunkedVolumeStore {
const StoreMeta& meta() const { return meta_; } const StoreMeta& meta() const { return meta_; }
// --- level 0 兼容接口(语义不变,= 全分辨率级)---
int bricksX() const { return bricksX_; } int bricksX() const { return bricksX_; }
int bricksY() const { return bricksY_; } int bricksY() const { return bricksY_; }
int bricksZ() const { return bricksZ_; } int bricksZ() const { return bricksZ_; }
// 读单块 → 还原 int16 vector。返回长度 = bw*bh*bd内部块 = brick³边缘块更小 // 读单块 → 还原 int16 vector。返回长度 = bw*bh*bd内部块 = brick³边缘块更小
// == readBrick(0, bx, by, bz)。
std::vector<std::int16_t> readBrick(int bx, int by, int bz) const; std::vector<std::int16_t> readBrick(int bx, int by, int bz) const;
// --- 金字塔(多分辨率 LOD---
// 在已 write 的 store 目录上构建金字塔level 0=全分辨率(已存),
// level 1..levels 逐级 2× 降采样(维度 ceil(n/2)),故总级数 = levels+1。
// 同时为所有 level含 0计算并存每块 (min,max)(跳过 kBlank。结果写回
// meta.json + 各级数据文件level 0 复用现有 data.binlevel L 写
// data_L<level>.bin。levels<=0 视为无金字塔(仅 level 0
void buildPyramid(int levels);
// 总层数(含 level 0未建金字塔时 = 1。
int levels() const { return levelCount_; }
// 各级块数bricksX() == bricksX(0) 保持兼容。
int bricksX(int level) const;
int bricksY(int level) const;
int bricksZ(int level) const;
// 各级体素维度。
void dims(int level, int& nx, int& ny, int& nz) const;
// 读某级单块 → 还原 int16 vector。level 0 与兼容重载等价。
std::vector<std::int16_t> readBrick(int level, int bx, int by, int bz) const;
// 每块 (min,max),跳过 kBlank全 blank 块返回 (kBlank,kBlank)。
// 对未建金字塔的 level 0惰性读块计算。
std::pair<std::int16_t, std::int16_t> brickRange(int level, int bx, int by,
int bz) const;
private: private:
// 单块在 data.bin 中的位置与未压缩尺寸。 // 单块在所属级数据文件中的位置、未压缩尺寸与值域
struct BrickEntry { struct BrickEntry {
std::int64_t offset = 0; std::int64_t offset = 0;
std::int64_t compressedLen = 0; std::int64_t compressedLen = 0;
int bw = 0, bh = 0, bd = 0; int bw = 0, bh = 0, bd = 0;
std::int16_t vmin = 0, vmax = 0; // 块内 (min,max),跳过 kBlank全 blank=(kBlank,kBlank)
}; };
int brickIndex(int bx, int by, int bz) const { // 一个分辨率级:维度 + 块数 + 逐块索引 + 数据文件名。
return (bz * bricksY_ + by) * bricksX_ + bx; struct Level {
int nx = 0, ny = 0, nz = 0;
int bx = 0, by = 0, bz = 0; // 块数
std::string dataFile;
std::vector<BrickEntry> bricks;
};
int brickIndexAt(const Level& lv, int bx, int by, int bz) const {
return (bz * lv.by + by) * lv.bx + bx;
} }
const Level& levelAt(int level) const;
std::vector<std::int16_t> readBrickFrom(const Level& lv, int bx, int by,
int bz) const;
std::string dir_; std::string dir_;
StoreMeta meta_; StoreMeta meta_;
int bricksX_ = 0, bricksY_ = 0, bricksZ_ = 0; int bricksX_ = 0, bricksY_ = 0, bricksZ_ = 0; // = level 0 块数(兼容字段)
std::vector<BrickEntry> bricks_; std::vector<BrickEntry> bricks_; // = level 0 索引兼容字段readBrick(bx,by,bz) 用)
int levelCount_ = 1;
std::vector<Level> levels_; // levels_[0] 即 level 0与 bricks_ 同源)
}; };
} // namespace geopro::data } // namespace geopro::data

View File

@ -56,6 +56,8 @@ target_link_libraries(geopro_tests PRIVATE geopro_data)
# store ChunkedVolumeStoreGPR round-trip + + # store ChunkedVolumeStoreGPR round-trip + +
target_sources(geopro_tests PRIVATE data/store/test_chunked_volume_store.cpp) target_sources(geopro_tests PRIVATE data/store/test_chunked_volume_store.cpp)
# store LOD + min/max level0 store
target_sources(geopro_tests PRIVATE data/store/test_pyramid.cpp)
target_link_libraries(geopro_tests PRIVATE geopro_store) target_link_libraries(geopro_tests PRIVATE geopro_store)
# net RSA OpenSSL / find_package # net RSA OpenSSL / find_package

View File

@ -0,0 +1,113 @@
#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);
}
// 老 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);
}