geopro/tests/render/test_view_adaptive_source.cpp

296 lines
11 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ViewAdaptiveVolumeSource(C2) headless 测试:用 C1 selectLod 选层选区,从分块
// 存储重组当前视野区域为【单张 VTK_SHORT vtkImageData】各轴 ≤16384世界
// origin/spacing 按 level+exagg。核心 updateView(CameraView,VolumeView) 不需真
// vtkCamera/GL 上下文——构造 vtkImageData 不需渲染管线。
//
// 验:远观粗层 / 近观细层 / 各轴 ≤16384 / VTK_SHORT / 重组体素与 store 对应
// level+区间位置一致(不错位)/ empty 情形空。
#include "render/source/ViewAdaptiveVolumeSource.hpp"
#include "core/algo/GprVolumeBuilder.hpp"
#include "data/store/ChunkedVolumeStore.hpp"
#include "lod/ViewAdaptiveLodPolicy.hpp"
#include <vtkImageData.h>
#include <vtkPointData.h>
#include <vtkShortArray.h>
#include <cmath>
#include <filesystem>
#include <gtest/gtest.h>
using namespace geopro;
using geopro::render::CameraView;
using geopro::render::VolumeView;
namespace {
// 造一个含金字塔的 store值 = 全局 (i+j+k)%1000便于校验块定位非 64 整除
// 维度以含边缘块。返回 store 目录。与 test_outofcore_source 同口径。
std::string makePyramidStore(const std::string& dir, int nx, int ny, int nz,
double ox, double oy, double oz, double dx,
double dy, double dz, int brick, int levels) {
std::filesystem::remove_all(dir);
core::BuiltI16 b;
b.vol = 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<short>((i + j + k) % 1000);
b.quant = {1.0, 0.0};
b.origin = {{ox, oy, oz}};
b.spacing = {{dx, dy, dz}};
b.vminPhys = 0;
b.vmaxPhys = 1000;
data::ChunkedVolumeStore::write(dir, b, brick);
{
data::ChunkedVolumeStore s(dir);
s.buildPyramid(levels);
}
return dir;
}
// 由 store meta + levels + exagg 填 VolumeView与生产 volumeViewOf 同口径)。
VolumeView volumeViewOf(const data::StoreMeta& m, int levels, double exagg) {
VolumeView v{};
v.nx = m.nx;
v.ny = m.ny;
v.nz = m.nz;
v.brick = m.brick;
v.levels = levels;
v.origin[0] = m.origin[0];
v.origin[1] = m.origin[1];
v.origin[2] = m.origin[2];
// C1 约定spacing 已含 exagg 于 y/z。
v.spacing[0] = m.spacing[0];
v.spacing[1] = m.spacing[1] * exagg;
v.spacing[2] = m.spacing[2] * exagg;
v.exagg = exagg;
return v;
}
// 相机:从 +X 看体中心(用 level0 物理范围,未含 exagg 于 X
CameraView lookFromX(const data::StoreMeta& m, double dist, double fovYDeg = 30.0,
int viewportH = 1080) {
CameraView c{};
const double cx = m.origin[0] + 0.5 * m.nx * m.spacing[0];
const double cy = m.origin[1] + 0.5 * m.ny * m.spacing[1];
const double cz = m.origin[2] + 0.5 * m.nz * m.spacing[2];
c.focal[0] = cx;
c.focal[1] = cy;
c.focal[2] = cz;
c.pos[0] = cx + dist;
c.pos[1] = cy;
c.pos[2] = cz;
c.up[0] = 0;
c.up[1] = 0;
c.up[2] = 1;
c.fovYDeg = fovYDeg;
c.aspect = 1.0;
c.viewportH = viewportH;
return c;
}
} // namespace
// ── 远观:选粗层、单图、各轴 ≤16384、VTK_SHORT ───────────────────────────────
TEST(ViewAdaptiveVolumeSource, FarViewCoarseLevelSingleTexture) {
const auto dir =
(std::filesystem::temp_directory_path() / "gpr_va_far").string();
// 200×80×60, brick=64 → 含边缘块。3 级金字塔。
makePyramidStore(dir, 200, 80, 60, 1, 2, 3, 0.5, 0.5, 0.2, 64, 3);
render::ViewAdaptiveVolumeSource src(dir, /*exagg*/ 1.0);
EXPECT_EQ(src.meta().nx, 200);
const VolumeView vol = volumeViewOf(src.meta(), src.levelCount(), 1.0);
const CameraView far = lookFromX(src.meta(), 8000.0);
src.updateView(far, vol);
auto imgs = src.currentImages();
ASSERT_EQ(imgs.size(), 1u);
ASSERT_NE(imgs[0].Get(), nullptr);
EXPECT_EQ(imgs[0]->GetScalarType(), VTK_SHORT);
int d[3];
imgs[0]->GetDimensions(d);
EXPECT_LE(d[0], 16384);
EXPECT_LE(d[1], 16384);
EXPECT_LE(d[2], 16384);
EXPECT_GT(d[0], 0);
EXPECT_GT(d[1], 0);
EXPECT_GT(d[2], 0);
EXPECT_GT(src.lastLevel(), 0); // 远 → 粗
}
// ── 近观:选最细层(0)、单图、仍 ≤16384 ─────────────────────────────────────
TEST(ViewAdaptiveVolumeSource, NearViewFineLevel) {
const auto dir =
(std::filesystem::temp_directory_path() / "gpr_va_near").string();
makePyramidStore(dir, 200, 80, 60, 1, 2, 3, 0.5, 0.5, 0.2, 64, 3);
render::ViewAdaptiveVolumeSource src(dir, 1.0);
const VolumeView vol = volumeViewOf(src.meta(), src.levelCount(), 1.0);
const CameraView near = lookFromX(src.meta(), 8.0, 20.0, 1080);
src.updateView(near, vol);
auto imgs = src.currentImages();
ASSERT_EQ(imgs.size(), 1u);
EXPECT_EQ(src.lastLevel(), 0); // 近 → 最细
int d[3];
imgs[0]->GetDimensions(d);
EXPECT_LE(d[0], 16384);
EXPECT_LE(d[1], 16384);
EXPECT_LE(d[2], 16384);
}
// ── 重组体素与 store 对应 level/区间位置一致(不错位)──────────────────────
// 远观选某粗 level重组单图后抽查若干体素用世界坐标反推该 level 体素索引,
// 与 store.readBrick(level,...) 对应块的同一体素值逐一相等 → 证明位置正确。
TEST(ViewAdaptiveVolumeSource, ReconstructedVoxelsMatchStore) {
const auto dir =
(std::filesystem::temp_directory_path() / "gpr_va_match").string();
makePyramidStore(dir, 200, 80, 60, 1, 2, 3, 0.5, 0.5, 0.2, 64, 3);
render::ViewAdaptiveVolumeSource src(dir, 1.0);
const VolumeView vol = volumeViewOf(src.meta(), src.levelCount(), 1.0);
const CameraView far = lookFromX(src.meta(), 8000.0);
src.updateView(far, vol);
auto imgs = src.currentImages();
ASSERT_EQ(imgs.size(), 1u);
vtkImageData* img = imgs[0];
const int level = src.lastLevel();
data::ChunkedVolumeStore store(dir);
const data::StoreMeta& m = src.meta();
const int brick = m.brick;
const double sc = static_cast<double>(1 << level);
int dimLx = 0, dimLy = 0, dimLz = 0;
store.dims(level, dimLx, dimLy, dimLz);
int d[3];
img->GetDimensions(d);
double org[3], sp[3];
img->GetOrigin(org);
img->GetSpacing(sp);
// spacing 应 = meta.spacing × 2^levelexagg=1
EXPECT_DOUBLE_EQ(sp[0], m.spacing[0] * sc);
EXPECT_DOUBLE_EQ(sp[1], m.spacing[1] * sc);
EXPECT_DOUBLE_EQ(sp[2], m.spacing[2] * sc);
auto* arr = vtkShortArray::SafeDownCast(img->GetPointData()->GetScalars());
ASSERT_NE(arr, nullptr);
// 该图覆盖的 level 体素起点:由 origin 反推(应整数)。
const long gi0 = std::lround((org[0] - m.origin[0]) / sp[0]);
const long gj0 = std::lround((org[1] - m.origin[1]) / sp[1]);
const long gk0 = std::lround((org[2] - m.origin[2]) / sp[2]);
EXPECT_GE(gi0, 0);
EXPECT_GE(gj0, 0);
EXPECT_GE(gk0, 0);
// 取该 level 单块校验:读 store 块 (b*,0,0),把块内某体素与图中同位置对比。
auto storeVoxelAt = [&](int gi, int gj, int gk) -> std::int16_t {
const int bx = gi / brick, by = gj / brick, bz = gk / brick;
const std::vector<std::int16_t> raw = store.readBrick(level, bx, by, bz);
const int i0 = bx * brick, j0 = by * brick, k0 = bz * brick;
const int bw = std::min(brick, dimLx - i0);
const int bh = std::min(brick, dimLy - j0);
const int li = gi - i0, lj = gj - j0, lk = gk - k0;
const std::size_t w =
(static_cast<std::size_t>(lk) * bh + lj) * bw + li;
return raw[w];
};
// 抽查图内若干位置(含非角点、跨块),逐一与 store 对应 level 体素相等。
int checked = 0;
for (int lk : {0, d[2] / 2, d[2] - 1}) {
for (int lj : {0, d[1] / 2, d[1] - 1}) {
for (int li : {0, d[0] / 2, d[0] - 1}) {
const long gi = gi0 + li, gj = gj0 + lj, gk = gk0 + lk;
if (gi >= dimLx || gj >= dimLy || gk >= dimLz) continue;
const vtkIdType id =
(static_cast<vtkIdType>(lk) * d[1] + lj) * d[0] + li;
const std::int16_t got = arr->GetValue(id);
const std::int16_t want =
storeVoxelAt(static_cast<int>(gi), static_cast<int>(gj),
static_cast<int>(gk));
EXPECT_EQ(got, want)
<< "体素错位 at local(" << li << "," << lj << "," << lk
<< ") level=" << level;
++checked;
}
}
}
EXPECT_GT(checked, 0);
}
// ── 世界 origin按 level+exagg 偏移正确exagg≠1 校验 y/z 烘焙)─────────────
TEST(ViewAdaptiveVolumeSource, WorldOriginSpacingWithExagg) {
const auto dir =
(std::filesystem::temp_directory_path() / "gpr_va_exagg").string();
makePyramidStore(dir, 200, 80, 60, 1, 2, 3, 0.5, 0.5, 0.2, 64, 3);
const double exagg = 4.0;
render::ViewAdaptiveVolumeSource src(dir, exagg);
const VolumeView vol = volumeViewOf(src.meta(), src.levelCount(), exagg);
// 远观整体,区间从 0 起 → origin 应正好 = meta.origin区间起点 0
const CameraView far = lookFromX(src.meta(), 8000.0);
src.updateView(far, vol);
auto imgs = src.currentImages();
ASSERT_EQ(imgs.size(), 1u);
const data::StoreMeta& m = src.meta();
const int level = src.lastLevel();
const double sc = static_cast<double>(1 << level);
double sp[3], org[3];
imgs[0]->GetSpacing(sp);
imgs[0]->GetOrigin(org);
// spacingx 不夸张y/z ×exagg。
EXPECT_DOUBLE_EQ(sp[0], m.spacing[0] * sc);
EXPECT_DOUBLE_EQ(sp[1], m.spacing[1] * sc * exagg);
EXPECT_DOUBLE_EQ(sp[2], m.spacing[2] * sc * exagg);
// 区间从 0 起 → origin = meta.originy/z 偏移为 0×… 仍是 origin
EXPECT_DOUBLE_EQ(org[0], m.origin[0]);
EXPECT_DOUBLE_EQ(org[1], m.origin[1]);
EXPECT_DOUBLE_EQ(org[2], m.origin[2]);
}
// ── empty体完全在视锥外 → currentImages 空 ───────────────────────────────
TEST(ViewAdaptiveVolumeSource, EmptyWhenBehindCamera) {
const auto dir =
(std::filesystem::temp_directory_path() / "gpr_va_empty").string();
makePyramidStore(dir, 200, 80, 60, 1, 2, 3, 0.5, 0.5, 0.2, 64, 3);
render::ViewAdaptiveVolumeSource src(dir, 1.0);
const VolumeView vol = volumeViewOf(src.meta(), src.levelCount(), 1.0);
CameraView c = lookFromX(src.meta(), 1000.0);
c.focal[0] = c.pos[0] + 1000.0; // 视线朝 +X体在身后 → 视锥外
src.updateView(c, vol);
EXPECT_TRUE(src.currentImages().empty());
EXPECT_EQ(src.sliceSource(), nullptr);
}
// ── 单层 store无金字塔也能重组level 恒 0───────────────────────────
TEST(ViewAdaptiveVolumeSource, SingleLevelStore) {
const auto dir =
(std::filesystem::temp_directory_path() / "gpr_va_1lvl").string();
// levels=0 → 仅 level 0。
makePyramidStore(dir, 128, 64, 48, 0, 0, 0, 1.0, 1.0, 1.0, 64, 0);
render::ViewAdaptiveVolumeSource src(dir, 1.0);
EXPECT_EQ(src.levelCount(), 1);
const VolumeView vol = volumeViewOf(src.meta(), src.levelCount(), 1.0);
const CameraView c = lookFromX(src.meta(), 500.0);
src.updateView(c, vol);
auto imgs = src.currentImages();
ASSERT_EQ(imgs.size(), 1u);
EXPECT_EQ(src.lastLevel(), 0);
EXPECT_EQ(imgs[0]->GetScalarType(), VTK_SHORT);
}