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 328 additions and 18 deletions
Showing only changes of commit 5ffc784792 - Show all commits

View File

@ -54,6 +54,40 @@ int extent(int n, int b, int brick) {
return got < brick ? got : brick;
}
// 压缩一块 int16 体素 → qCompress 流write 与 StreamingVolumeWriter 共用DRY
QByteArray compressBrick(const std::vector<std::int16_t>& blk) {
const int rawBytes = static_cast<int>(blk.size() * sizeof(std::int16_t));
return qCompress(reinterpret_cast<const uchar*>(blk.data()), rawBytes);
}
// 把几何/量化元信息写入 meta json不含 bricks/levels 数组——write 与
// StreamingVolumeWriter::finalize 共用,确保 meta 结构逐字段一致DRY
void writeMetaGeometry(json& meta, int nx, int ny, int nz, int brick,
const std::array<double, 3>& origin,
const std::array<double, 3>& spacing,
const geopro::core::Quant& quant, double vminPhys,
double vmaxPhys) {
meta["nx"] = nx;
meta["ny"] = ny;
meta["nz"] = nz;
meta["brick"] = brick;
meta["origin"] = {origin[0], origin[1], origin[2]};
meta["spacing"] = {spacing[0], spacing[1], spacing[2]};
meta["quant"] = {{"scale", quant.scale}, {"offset", quant.offset}};
meta["vminPhys"] = vminPhys;
meta["vmaxPhys"] = vmaxPhys;
}
// 单块索引 jsonwrite 与 StreamingVolumeWriter::finalize 共用DRY
json brickIndexJson(std::int64_t offset, std::int64_t clen, int bw, int bh,
int bd) {
return json{{"offset", offset},
{"compressedLen", clen},
{"bw", bw},
{"bh", bh},
{"bd", bd}};
}
// 从体中拷出一块的 int16块内 i 最快、k 最慢,与体一致)。
std::vector<std::int16_t> sliceBrick(const geopro::core::ScalarVolumeI16& vol,
int bx, int by, int bz, int brick,
@ -101,20 +135,13 @@ void ChunkedVolumeStore::write(const std::string& dir,
const int bd = extent(nz, bz, brick);
auto raw = sliceBrick(vol, bx, by, bz, brick, bw, bh, bd);
const int rawBytes =
static_cast<int>(raw.size() * sizeof(std::int16_t));
const QByteArray compressed = qCompress(
reinterpret_cast<const uchar*>(raw.data()), rawBytes);
const QByteArray compressed = compressBrick(raw);
const std::int64_t clen = compressed.size();
data.write(compressed.constData(), clen);
if (!data) throw std::runtime_error("ChunkedVolumeStore: data.bin write failed");
bricks.push_back(json{{"offset", offset},
{"compressedLen", clen},
{"bw", bw},
{"bh", bh},
{"bd", bd}});
bricks.push_back(brickIndexJson(offset, clen, bw, bh, bd));
offset += clen;
}
}
@ -122,15 +149,8 @@ void ChunkedVolumeStore::write(const std::string& dir,
data.close();
json meta;
meta["nx"] = nx;
meta["ny"] = ny;
meta["nz"] = nz;
meta["brick"] = brick;
meta["origin"] = {b.origin[0], b.origin[1], b.origin[2]};
meta["spacing"] = {b.spacing[0], b.spacing[1], b.spacing[2]};
meta["quant"] = {{"scale", b.quant.scale}, {"offset", b.quant.offset}};
meta["vminPhys"] = b.vminPhys;
meta["vmaxPhys"] = b.vmaxPhys;
writeMetaGeometry(meta, nx, ny, nz, brick, b.origin, b.spacing, b.quant,
b.vminPhys, b.vmaxPhys);
meta["bricks"] = std::move(bricks);
std::ofstream metaOut((fs::path(dir) / kMetaFile).string(),
@ -545,4 +565,103 @@ void ChunkedVolumeStore::buildPyramid(int levels) {
if (!metaOut) throw std::runtime_error("ChunkedVolumeStore: meta.json write failed");
}
// ----------------------- StreamingVolumeWriter -----------------------
StreamingVolumeWriter::StreamingVolumeWriter(const std::string& dir,
const StoreMeta& meta)
: dir_(dir), meta_(meta) {
if (meta_.brick <= 0)
throw std::invalid_argument("StreamingVolumeWriter: brick must be > 0");
bricksX_ = ceilDiv(meta_.nx, meta_.brick);
bricksY_ = ceilDiv(meta_.ny, meta_.brick);
bricksZ_ = ceilDiv(meta_.nz, meta_.brick);
// 预分配固定顺序索引bz 最慢、bx 最快),与 write 的遍历顺序一致 → 同布局。
entries_.assign(static_cast<std::size_t>(bricksX_) * bricksY_ * bricksZ_,
Entry{});
fs::create_directories(fs::path(dir_));
// 截断式打开一次以建立空 data.binwriteBrick 用追加模式逐块写)。
std::ofstream init((fs::path(dir_) / kDataFile).string(),
std::ios::binary | std::ios::trunc);
if (!init)
throw std::runtime_error(
"StreamingVolumeWriter: cannot open data.bin for write");
}
void StreamingVolumeWriter::writeBrick(int bx, int by, int bz,
const std::vector<std::int16_t>& voxels) {
if (finalized_)
throw std::runtime_error(
"StreamingVolumeWriter: writeBrick after finalize");
if (bx < 0 || by < 0 || bz < 0 || bx >= bricksX_ || by >= bricksY_ ||
bz >= bricksZ_)
throw std::runtime_error("StreamingVolumeWriter: brick index out of range");
const std::size_t idx =
(static_cast<std::size_t>(bz) * bricksY_ + by) * bricksX_ + bx;
Entry& e = entries_[idx];
if (e.written)
throw std::runtime_error("StreamingVolumeWriter: brick written twice");
const int bw = extent(meta_.nx, bx, meta_.brick);
const int bh = extent(meta_.ny, by, meta_.brick);
const int bd = extent(meta_.nz, bz, meta_.brick);
const std::size_t expect = static_cast<std::size_t>(bw) * bh * bd;
if (voxels.size() != expect)
throw std::runtime_error("StreamingVolumeWriter: brick voxel count mismatch");
const QByteArray compressed = compressBrick(voxels);
const std::int64_t clen = compressed.size();
// 追加写 data.bin块按 writeBrick 调用顺序物理排布,索引记录各自偏移;
// finalize 再按固定顺序写 meta → readBrick 凭索引偏移定位,物理顺序无关)。
std::ofstream data((fs::path(dir_) / kDataFile).string(),
std::ios::binary | std::ios::app);
if (!data)
throw std::runtime_error(
"StreamingVolumeWriter: cannot open data.bin for append");
data.write(compressed.constData(), clen);
if (!data)
throw std::runtime_error("StreamingVolumeWriter: data.bin write failed");
e.offset = offset_;
e.compressedLen = clen;
e.bw = bw;
e.bh = bh;
e.bd = bd;
e.written = true;
offset_ += clen;
++written_;
}
void StreamingVolumeWriter::finalize() {
if (finalized_)
throw std::runtime_error("StreamingVolumeWriter: already finalized");
if (written_ != static_cast<std::int64_t>(entries_.size()))
throw std::runtime_error("StreamingVolumeWriter: missing bricks at finalize");
// 按固定顺序bz 最慢、bx 最快)输出索引,结构与 write 的 bricks 数组一致。
json bricks = json::array();
for (const Entry& e : entries_)
bricks.push_back(brickIndexJson(e.offset, e.compressedLen, e.bw, e.bh, e.bd));
json meta;
writeMetaGeometry(meta, meta_.nx, meta_.ny, meta_.nz, meta_.brick,
meta_.origin, meta_.spacing, meta_.quant, meta_.vminPhys,
meta_.vmaxPhys);
meta["bricks"] = std::move(bricks);
std::ofstream metaOut((fs::path(dir_) / kMetaFile).string(),
std::ios::trunc);
if (!metaOut)
throw std::runtime_error(
"StreamingVolumeWriter: cannot open meta.json for write");
metaOut << meta.dump(2);
if (!metaOut)
throw std::runtime_error("StreamingVolumeWriter: meta.json write failed");
finalized_ = true;
}
} // namespace geopro::data

View File

@ -115,6 +115,43 @@ class ChunkedVolumeStore {
int levelCount_ = 1;
// mutablebrickRange 为 const但惰性算出值域后需就地缓存置 hasRange=true
mutable std::vector<Level> levels_; // levels_[0] 即 level 0与 bricks_ 同源)
friend class StreamingVolumeWriter;
};
// 逐块增量写 level0 store不持整卷。块写入顺序任意但每块只写一次。
// 产出与 ChunkedVolumeStore::write(整卷) 逐 brick + meta 完全一致data.bin 为
// 逐块 qCompress 流(按 bz 最慢、bx 最快的固定顺序排布meta.json 结构同 write
// 故 ChunkedVolumeStore(dir)/readBrick 能照常读。偏移/长度全程 64 位。
class StreamingVolumeWriter {
public:
// 用 StoreMeta 定 dims/brick/origin/spacing/quant/vminmax与 write 一致的元信息)。
StreamingVolumeWriter(const std::string& dir, const StoreMeta& meta);
// 写一块voxels 为该块体素(大小=bw*bh*bd块内 i 最快,与 write 同布局)。
// bx/by/bz 为块索引;体素数不符或同块重复写 → 抛 std::runtime_error。
void writeBrick(int bx, int by, int bz,
const std::vector<std::int16_t>& voxels);
// 收尾:写 meta.json含所有已写块的索引。有缺块 → 抛 std::runtime_error。
void finalize();
private:
// 单块在 data.bin 中的索引(与 ChunkedVolumeStore::BrickEntry 子集对应)。
struct Entry {
std::int64_t offset = 0;
std::int64_t compressedLen = 0;
int bw = 0, bh = 0, bd = 0;
bool written = false;
};
std::string dir_;
StoreMeta meta_;
int bricksX_ = 0, bricksY_ = 0, bricksZ_ = 0;
std::vector<Entry> entries_; // 固定顺序索引bz 最慢、bx 最快)
std::int64_t offset_ = 0; // data.bin 当前追加偏移64 位)
std::int64_t written_ = 0; // 已写块计数
bool finalized_ = false;
};
} // namespace geopro::data

View File

@ -58,6 +58,8 @@ target_link_libraries(geopro_tests PRIVATE geopro_data)
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)
# store StreamingVolumeWriter level0 write +meta
target_sources(geopro_tests PRIVATE data/store/test_streaming_write.cpp)
target_link_libraries(geopro_tests PRIVATE geopro_store)
# net RSA OpenSSL / find_package

View File

@ -0,0 +1,152 @@
#include "data/store/ChunkedVolumeStore.hpp"
#include "core/algo/GprVolumeBuilder.hpp"
#include <gtest/gtest.h>
#include <filesystem>
#include <cstdint>
#include <vector>
using namespace geopro::data;
namespace {
// 非常量体:体素随 (i,j,k) 变化,确保逐块对拍真正区分块内容(非全相同)。
geopro::core::BuiltI16 makeBuilt(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)
b.vol.at(i, j, k) =
static_cast<std::int16_t>((i * 131 + j * 17 + k * 7) % 251);
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;
}
int ceilDiv(int n, int brick) { return (n + brick - 1) / brick; }
int extent(int n, int b, int brick) {
const int got = n - b * brick;
return got < brick ? got : brick;
}
// 从整卷切出 (bx,by,bz) 块体素(块内 i 最快、k 最慢,与 write 同布局)。
std::vector<std::int16_t> sliceBrickFrom(const geopro::core::BuiltI16& b, int bx,
int by, int bz, int brick) {
const auto& vol = b.vol;
const int bw = extent(vol.nx(), bx, brick);
const int bh = extent(vol.ny(), by, brick);
const int bd = extent(vol.nz(), bz, brick);
std::vector<std::int16_t> out(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)
out[w++] = vol.at(i0 + ii, j0 + jj, k0 + kk);
return out;
}
std::string tmp(const char* name) {
return (std::filesystem::temp_directory_path() / name).string();
}
} // namespace
// 核心验收:流式逐块写 vs 非流式整卷 write逐块 readBrick + meta 完全一致。
TEST(StreamingVolumeWriter, MatchesNonStreamingWrite) {
auto b = makeBuilt(100, 40, 30); // 100/40/30 均非整除 64 → 含边缘块
const int brick = 64;
auto dirA = tmp("swA"), dirB = tmp("swB");
std::filesystem::remove_all(dirA);
std::filesystem::remove_all(dirB);
ChunkedVolumeStore::write(dirA, b, brick); // 金标准
StoreMeta m = ChunkedVolumeStore::readMeta(dirA); // 复用同一 meta 元信息
const int bX = ceilDiv(m.nx, brick);
const int bY = ceilDiv(m.ny, brick);
const int bZ = ceilDiv(m.nz, brick);
{
StreamingVolumeWriter w(dirB, m);
// 故意打乱写入顺序(与 write 的固定遍历顺序不同),验证顺序无关。
for (int bx = 0; bx < bX; ++bx)
for (int by = 0; by < bY; ++by)
for (int bz = 0; bz < bZ; ++bz)
w.writeBrick(bx, by, bz, sliceBrickFrom(b, bx, by, bz, brick));
w.finalize();
}
ChunkedVolumeStore A(dirA), B(dirB);
for (int bz = 0; bz < bZ; ++bz)
for (int by = 0; by < bY; ++by)
for (int bx = 0; bx < bX; ++bx)
EXPECT_EQ(A.readBrick(bx, by, bz), B.readBrick(bx, by, bz))
<< "brick mismatch at " << bx << "," << by << "," << bz;
EXPECT_EQ(B.meta().nx, A.meta().nx);
EXPECT_EQ(B.meta().ny, A.meta().ny);
EXPECT_EQ(B.meta().nz, A.meta().nz);
EXPECT_EQ(B.meta().brick, A.meta().brick);
EXPECT_EQ(B.meta().origin, A.meta().origin);
EXPECT_EQ(B.meta().spacing, A.meta().spacing);
EXPECT_EQ(B.meta().quant.scale, A.meta().quant.scale);
EXPECT_EQ(B.meta().quant.offset, A.meta().quant.offset);
EXPECT_EQ(B.meta().vminPhys, A.meta().vminPhys);
EXPECT_EQ(B.meta().vmaxPhys, A.meta().vmaxPhys);
std::filesystem::remove_all(dirA);
std::filesystem::remove_all(dirB);
}
// 同一块重复写 → 抛异常(约定:每块只写一次)。
TEST(StreamingVolumeWriter, DuplicateBrickThrows) {
auto b = makeBuilt(70, 30, 20);
auto dirA = tmp("swDup");
std::filesystem::remove_all(dirA);
ChunkedVolumeStore::write(dirA, b, 64);
StoreMeta m = ChunkedVolumeStore::readMeta(dirA);
auto dirB = tmp("swDupB");
std::filesystem::remove_all(dirB);
StreamingVolumeWriter w(dirB, m);
w.writeBrick(0, 0, 0, sliceBrickFrom(b, 0, 0, 0, 64));
EXPECT_THROW(w.writeBrick(0, 0, 0, sliceBrickFrom(b, 0, 0, 0, 64)),
std::runtime_error);
std::filesystem::remove_all(dirA);
std::filesystem::remove_all(dirB);
}
// 缺块 finalize → 抛异常(约定:所有块必须写齐)。
TEST(StreamingVolumeWriter, MissingBrickFinalizeThrows) {
auto b = makeBuilt(70, 30, 20); // bX=2,bY=1,bZ=1 → 共 2 块
auto dirA = tmp("swMiss");
std::filesystem::remove_all(dirA);
ChunkedVolumeStore::write(dirA, b, 64);
StoreMeta m = ChunkedVolumeStore::readMeta(dirA);
auto dirB = tmp("swMissB");
std::filesystem::remove_all(dirB);
StreamingVolumeWriter w(dirB, m);
w.writeBrick(0, 0, 0, sliceBrickFrom(b, 0, 0, 0, 64)); // 只写 1 块,缺 (1,0,0)
EXPECT_THROW(w.finalize(), std::runtime_error);
std::filesystem::remove_all(dirA);
std::filesystem::remove_all(dirB);
}
// 块体素大小不符bw*bh*bd 不匹配)→ 抛异常。
TEST(StreamingVolumeWriter, WrongVoxelCountThrows) {
auto b = makeBuilt(70, 30, 20);
auto dirA = tmp("swSize");
std::filesystem::remove_all(dirA);
ChunkedVolumeStore::write(dirA, b, 64);
StoreMeta m = ChunkedVolumeStore::readMeta(dirA);
auto dirB = tmp("swSizeB");
std::filesystem::remove_all(dirB);
StreamingVolumeWriter w(dirB, m);
std::vector<std::int16_t> bad(10); // 远小于 64*30*20
EXPECT_THROW(w.writeBrick(0, 0, 0, bad), std::runtime_error);
std::filesystem::remove_all(dirA);
std::filesystem::remove_all(dirB);
}