feat/vtk-3d-view #7
|
|
@ -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;缺失时置 0(brickRange(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/max(buildPyramid 写过),回填 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/max(vmin==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;
|
||||||
|
}
|
||||||
|
// 原 bricks(level 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
|
||||||
|
|
|
||||||
|
|
@ -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.bin,level 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
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,8 @@ target_link_libraries(geopro_tests PRIVATE geopro_data)
|
||||||
|
|
||||||
# store 层:ChunkedVolumeStore(GPR 三维体分块压缩落盘 round-trip + 边缘块 + 压缩生效)。
|
# store 层:ChunkedVolumeStore(GPR 三维体分块压缩落盘 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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue