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
1 changed files with 342 additions and 0 deletions
Showing only changes of commit 0a0d3ba017 - Show all commits

View File

@ -36,6 +36,7 @@
#include "data/store/ChunkedVolumeStore.hpp" #include "data/store/ChunkedVolumeStore.hpp"
#include "io/gpr/Gpr3dvVolumeBridge.hpp" #include "io/gpr/Gpr3dvVolumeBridge.hpp"
#include "io/gpr/GprSurveyAssembler.hpp" #include "io/gpr/GprSurveyAssembler.hpp"
#include "io/gpr/GpsTrack.hpp"
#include "io/gpr/IprHeader.hpp" #include "io/gpr/IprHeader.hpp"
#include "render/actors/VoxelActor.hpp" #include "render/actors/VoxelActor.hpp"
#include "render/source/OutOfCoreSource.hpp" #include "render/source/OutOfCoreSource.hpp"
@ -76,6 +77,7 @@
#include <vtkShortArray.h> #include <vtkShortArray.h>
#include <vtkSmartPointer.h> #include <vtkSmartPointer.h>
#include <vtkSmartVolumeMapper.h> #include <vtkSmartVolumeMapper.h>
#include <vtkTransform.h>
#include <vtkUnsignedCharArray.h> #include <vtkUnsignedCharArray.h>
#include <vtkVolume.h> #include <vtkVolume.h>
#include <vtkWindowToImageFilter.h> #include <vtkWindowToImageFilter.h>
@ -3674,6 +3676,343 @@ int cmdViewGallery(const std::string& dir, int frames,
return rc; return rc;
} }
// ============================================================================
// view-all全部独立体按真实 GPS 位置/朝向摆进同一 3D 场景一起渲测区全貌Task P7
// ============================================================================
//
// 对应客户端「选多个 ds 一起生成三维」:每条线是独立 coarse 体(线局部坐标 X=沿测线、
// Y=通道横向、Z=深度origin≈0本命令把它们按各自 .gps 真实位置/航向摆进同一世界框:
// - 公共世界原点 = 全体 .gps 最小经纬(同一世界框);
// - 每条线刚体变换:平移到该线 .gps 起点局部米 + 绕竖直 Z 轴转该线航向角(起→止主方向),
// 使体局部 X沿测线对齐真实航向、Y横向随之垂直、Z深度保持竖直
// - 深度 Z 用 exagg 夸张(只 Z
// - 每条体加载为一张整卷 vtkImageDatacoarse 体小,按 --level 选层整体上纹理,不走 LOD
// 套上该线 vtkTransform全加进同一 renderer 一起渲。
// 传函/配色用 P4 默认醒目版var4增强灰度 + 实体包络 + 梯度门 + 光照),逐体按 2/98 分位标定。
//
// --preview 离屏出俯视(top) + 斜视(oblique)两张图展示 20 条并排成测区;否则开真窗口可转可缩。
// 一条线在世界中的摆放(刚体变换 + 该线已加载的整卷体)。
struct LinePlacement {
std::string name; // 明星路_NNN
vtkSmartPointer<vtkImageData> img; // 该线整卷 VTK_SHORT线局部坐标
geopro::data::StoreMeta meta; // 量化/几何
double startX = 0, startY = 0; // 起点局部米(相对公共原点)
double headingDeg = 0; // 航向角(度,相对 +X 东向,逆时针)
double spreadX = 0, spreadY = 0; // 可选横向铺开偏移(垂直航向,--spread>0 时)
};
// 由该线整卷体 + 公共世界原点 + 航向 + Z 夸张 → vtkVolume套刚体变换 + 传函)。
// 变换合成VTK先加先内层作用于点的顺序为 后加先施Translate→RotateZ→Scale(1,1,exagg)
// 即点先按 exagg 拉伸 Z局部再绕 Z 旋到真实航向,再平移到世界起点。
vtkSmartPointer<vtkVolume> makePlacedVolume(const LinePlacement& lp, double exagg,
double& vminOut, double& vmaxOut) {
const GalleryVariant& v = kViewDefaultVariant; // P4 默认醒目版var4
const geopro::data::StoreMeta& m = lp.meta;
// 逐体 2/98 分位标定(裁离群,自适应该体值域;退化则回退全量化域)。
double vmin = m.vminPhys, vmax = m.vmaxPhys;
const ScalarPercentiles pc =
sampleScalarPercentiles(lp.img.Get(), m.quant, 0.02, 0.98);
if (pc.samples > 0) {
vmin = pc.lo;
vmax = pc.hi;
}
vminOut = vmin;
vmaxOut = vmax;
const geopro::core::ColorScale cs = pickColor(v.color, vmin, vmax);
GradStats gs;
if (v.useGradientOpacity) gs = sampleGradientMagnitude(lp.img.Get());
vtkSmartPointer<vtkVolumeProperty> prop = makeVariantProperty(
v, m.quant, cs, vmin, vmax, v.maxOpacity,
v.useGradientOpacity ? &gs : nullptr);
vtkNew<vtkSmartVolumeMapper> mapper;
mapper->SetInputData(lp.img.Get());
mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
mapper->SetAutoAdjustSampleDistances(0);
mapper->SetInteractiveAdjustSampleDistances(0);
auto volume = vtkSmartPointer<vtkVolume>::New();
volume->SetMapper(mapper);
volume->SetProperty(prop);
// 刚体摆放 + Z 夸张(一并烘进 UserTransform
auto xf = vtkSmartPointer<vtkTransform>::New();
xf->PostMultiply();
xf->Scale(1.0, 1.0, exagg); // 只 Z 夸张(局部深度)
xf->RotateZ(lp.headingDeg); // 绕竖直轴转航向
// 平移到世界起点(+ 可选横向铺开偏移,垂直航向,让重叠的同路多趟可分辨)。
xf->Translate(lp.startX + lp.spreadX, lp.startY + lp.spreadY, 0.0);
volume->SetUserTransform(xf);
return volume;
}
int cmdViewAll(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2);
if (a.positional.size() < 2) {
std::cerr << "用法: gpr_poc view-all <storesDir> <gpsDir> [--preview] "
"[--exagg 8] [--level 1] [--spread M] [--shotDir <dir>]\n"
"例: gpr_poc view-all tmp/lines_all D:/Downloads/明星路 "
"--preview --exagg 8\n";
return 2;
}
const std::string storesDir = a.positional[0];
const std::string gpsDir = a.positional[1];
const double exagg = std::stod(a.get("exagg", "8"));
// --level每条体取金字塔哪一层整渲。默认 1L1 ~5664×7×398/体20 体内存/纹理可控)。
const int level = std::stoi(a.get("level", "1"));
// --spread M每条线沿垂直自身航向方向额外横向偏移 index*M 米,把「同一条路重复多趟、
// 真实位置高度重叠」的体在视觉上铺开成可分辨的并排测区M=0=纯真实位置,默认 0
const double spread = std::stod(a.get("spread", "0"));
const bool preview = a.kv.count("preview") > 0;
const std::string shotDir = a.get("shotDir", storesDir);
std::cout << "[view-all] storesDir=" << storesDir << " gpsDir=" << gpsDir
<< " exagg=" << exagg << " level=" << level << " spread=" << spread
<< (preview ? " [PREVIEW 离屏俯视+斜视出图]" : " [真窗口可交互]")
<< "\n";
// 离屏闸门不可渲机不产假结果preview/真窗口都需 GL
std::cout << "[view-all] 离屏闸门复检...\n";
if (cmdOffscreenSmoke() != 0) {
std::cout << "[view-all] 闸门失败,中止。\n";
return 1;
}
// 1) 发现 storesDir 下所有 明星路_NNN 体目录(含 meta.json按名排序。
std::vector<std::string> storeNames;
for (const auto& e : fs::directory_iterator(storesDir)) {
if (!e.is_directory()) continue;
if (!fs::exists(e.path() / "meta.json")) continue;
storeNames.push_back(e.path().filename().string());
}
std::sort(storeNames.begin(), storeNames.end());
std::cout << "[view-all] 发现体目录数=" << storeNames.size() << "\n";
if (storeNames.empty()) {
std::cerr << "[view-all] 错误: storesDir 下未发现任何含 meta.json 的体目录\n";
return 1;
}
// 2) 各线 .gps按目录名末段 _NNN 匹配),先全解析定公共世界原点(最小经纬)。
struct LineGps {
std::string name;
std::string num; // NNN
std::string gpsPath;
geopro::io::gpr::GpsTrack track;
};
std::vector<LineGps> gpsList;
double minLat = std::numeric_limits<double>::infinity();
double minLon = std::numeric_limits<double>::infinity();
for (const std::string& nm : storeNames) {
const std::size_t us = nm.find_last_of('_');
if (us == std::string::npos) {
std::cerr << "[view-all] 跳过 " << nm << ":名无 _NNN 后缀,无法配 .gps\n";
continue;
}
const std::string num = nm.substr(us + 1);
// .gps匹配 "*_<num>.gps"。
std::string gpsPath;
for (const auto& e : fs::directory_iterator(gpsDir)) {
if (!e.is_regular_file()) continue;
if (e.path().extension().string() != ".gps") continue;
const std::string stem = e.path().stem().string();
const std::size_t s2 = stem.find_last_of('_');
if (s2 != std::string::npos && stem.substr(s2 + 1) == num) {
gpsPath = e.path().string();
break;
}
}
if (gpsPath.empty()) {
std::cerr << "[view-all] 跳过 " << nm << ":缺 .gpsgpsDir 无 *_" << num
<< ".gps\n";
continue;
}
geopro::io::gpr::GpsTrack tr = geopro::io::gpr::parseGps(gpsPath);
if (tr.pts.size() < 2) {
std::cerr << "[view-all] 跳过 " << nm << ".gps 轨迹点 <2无法定位/航向)\n";
continue;
}
for (const auto& p : tr.pts) {
minLat = std::min(minLat, p.lat);
minLon = std::min(minLon, p.lon);
}
gpsList.push_back({nm, num, gpsPath, std::move(tr)});
}
if (gpsList.empty()) {
std::cerr << "[view-all] 错误: 无任何线同时具备体与可用 .gps\n";
return 1;
}
std::cout << "[view-all] 公共世界原点(最小经纬) lat0=" << minLat
<< " lon0=" << minLon << " (共 " << gpsList.size() << " 线参与)\n";
if (spread <= 0.0) {
std::cout << "[view-all] 提示: --spread=0 用纯真实 GPS 位置;本工区为同一条路重复多趟、"
"横向仅约数十米,真实位置下多趟高度重叠会叠成一条带。"
"如需把各趟铺开成可分辨的并排测区,加 --spread 60。\n";
}
// 3) 逐线:算起点局部米 + 航向 + 加载整卷体 → LinePlacement。
std::vector<LinePlacement> placements;
int lineIdx = 0;
for (const LineGps& lg : gpsList) {
// 轨迹 → 局部米(绕公共原点)。
std::vector<geopro::io::gpr::XY> trackM;
trackM.reserve(lg.track.pts.size());
for (const auto& p : lg.track.pts)
trackM.push_back(
geopro::io::gpr::lonLatToLocalM(p.lat, p.lon, minLat, minLon));
const geopro::io::gpr::XY& start = trackM.front();
// 航向:起→止主方向(直接首尾向量,稳健于逐点抖动)。
const geopro::io::gpr::XY& end = trackM.back();
const double hx = end.x - start.x, hy = end.y - start.y;
const double headingDeg = std::atan2(hy, hx) * 180.0 / 3.14159265358979323846;
// 垂直航向的左法向单位向量 (-hy,hx)/|h|,用于 --spread 横向铺开。
const double hlen = std::hypot(hx, hy);
double spreadX = 0, spreadY = 0;
if (spread > 0.0 && hlen > 0.0) {
const double off = lineIdx * spread;
spreadX = (-hy / hlen) * off;
spreadY = (hx / hlen) * off;
}
// 加载该线整卷体(按 --level夹到该 store 实际层数)。
const std::string storePath = (fs::path(storesDir) / lg.name).string();
geopro::data::ChunkedVolumeStore store(storePath);
const int lv = std::max(0, std::min(level, store.levels() - 1));
vtkSmartPointer<vtkImageData> img =
buildLevelImage(store, lv, store.meta());
LinePlacement lp;
lp.name = lg.name;
lp.img = img;
lp.meta = store.meta();
lp.startX = start.x;
lp.startY = start.y;
lp.headingDeg = headingDeg;
lp.spreadX = spreadX;
lp.spreadY = spreadY;
++lineIdx;
std::cout << "[view-all] " << lg.name << " level=" << lv << " 维度="
<< img->GetDimensions()[0] << "x" << img->GetDimensions()[1]
<< "x" << img->GetDimensions()[2] << " 起点局部米=(" << start.x
<< ", " << start.y << ") 航向=" << headingDeg << "°\n";
placements.push_back(std::move(lp));
}
std::cout << "[view-all] 加载并定位线数=" << placements.size() << "\n";
// 4) 同一 renderer 加全部 placed volume。
const int winW = 1400, winH = 900;
auto rw = preview ? makeOffscreenWindow(winW, winH)
: vtkSmartPointer<vtkRenderWindow>::New();
if (!preview) rw->SetSize(winW, winH);
vtkNew<vtkRenderer> ren;
ren->SetBackground(kViewDefaultVariant.bg[0], kViewDefaultVariant.bg[1],
kViewDefaultVariant.bg[2]);
rw->AddRenderer(ren);
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
vtkOutputWindow::SetInstance(capWin);
int placed = 0;
for (const LinePlacement& lp : placements) {
double vmin = 0, vmax = 0;
vtkSmartPointer<vtkVolume> vol = makePlacedVolume(lp, exagg, vmin, vmax);
ren->AddVolume(vol);
++placed;
}
std::cout << "[view-all] 已加入场景体数=" << placed << "\n";
// 渲一帧 → 验非空 + 闸门纹理错。
ren->ResetCamera();
rw->Render();
if (capWin->textureError()) {
std::cerr << "[view-all] 警告: 检测到 3D 纹理维度错误(某体超 GL 上限),"
"可调小 --level 增大 level 取更粗层。\n";
}
if (preview) {
fs::create_directories(shotDir);
// 俯视(top):相机沿 -Z 俯看 XY 平面(看 20 条在测区平面如何并排铺开)。
{
ren->ResetCamera();
vtkCamera* cam = ren->GetActiveCamera();
double fp[3], pos[3];
cam->GetFocalPoint(fp);
const double* b = ren->ComputeVisiblePropBounds();
const double span = std::max({b[1] - b[0], b[3] - b[2], b[5] - b[4]});
cam->SetPosition(fp[0], fp[1], fp[2] + span * 2.0);
cam->SetFocalPoint(fp[0], fp[1], fp[2]);
cam->SetViewUp(0, 1, 0);
ren->ResetCameraClippingRange();
rw->Render();
const std::string p = (fs::path(shotDir) / "view-all-top.png").string();
savePng(rw.Get(), p);
std::cout << "[view-all] 俯视图存: " << p << "\n";
}
// 斜视(oblique):默认 var4 取景El45/Az30 风格),看测区三维起伏。
{
ren->ResetCamera();
vtkCamera* cam = ren->GetActiveCamera();
cam->Elevation(35.0);
cam->Azimuth(30.0);
ren->ResetCameraClippingRange();
rw->Render();
const std::string p =
(fs::path(shotDir) / "view-all-oblique.png").string();
savePng(rw.Get(), p);
std::cout << "[view-all] 斜视图存: " << p << "\n";
}
// fps斜视取景预热后连渲计时真实多体共场景 fps不编造
rw->Render();
Stopwatch sw;
const int frames = 60;
for (int f = 0; f < frames; ++f) rw->Render();
const double ms = sw.elapsedMs();
const double fps = ms > 0 ? frames * 1000.0 / ms : 0.0;
const vtkIdType nb = countNonBlackPixels(rw.Get(), winW, winH);
std::cout << "\n=== view-all --preview 测区全貌(多体共场景)===\n";
std::cout << "参与线数 : " << placements.size() << "\n";
std::cout << "level : " << level << "\n";
std::cout << "exagg(Z) : " << exagg << "\n";
std::cout << "spread(横向铺开): " << spread << " m (0=纯真实位置)\n";
std::cout << "非黑像素 : " << nb << " / " << (winW * winH) << "\n";
std::cout << "fps(" << frames << "帧连渲) : " << fps << "\n";
std::cout << "俯视图 : " << (fs::path(shotDir) / "view-all-top.png").string()
<< "\n";
std::cout << "斜视图 : "
<< (fs::path(shotDir) / "view-all-oblique.png").string() << "\n";
writeMetricLine(
"view-all,lines=" + std::to_string(placements.size()) +
",level=" + std::to_string(level) + ",exagg=" + std::to_string(exagg) +
",spread=" + std::to_string(spread) +
",nonBlack=" + std::to_string(nb) + ",fps=" + std::to_string(fps));
return nb > 0 ? 0 : 1;
}
// 真窗口:可旋转/缩放。
rw->SetWindowName("gpr_poc view-all —— 20 条独立体按真实 GPS 摆成测区");
vtkNew<vtkRenderWindowInteractor> iren;
iren->SetRenderWindow(rw);
vtkNew<vtkInteractorStyleTrackballCamera> style;
iren->SetInteractorStyle(style);
ren->ResetCamera();
vtkCamera* cam = ren->GetActiveCamera();
cam->Elevation(35.0);
cam->Azimuth(30.0);
ren->ResetCameraClippingRange();
std::cout << "[view-all] 打开真窗口。左键旋转 / 滚轮缩放 / q 退出。\n";
iren->Initialize();
rw->Render();
iren->Start();
std::cout << "[view-all] 窗口关闭,退出。\n";
return 0;
}
int cmdView(int argc, char** argv) { int cmdView(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2); const Args a = parseArgs(argc, argv, 2);
if (a.positional.empty()) { if (a.positional.empty()) {
@ -4561,6 +4900,8 @@ void usage() {
" gpr_poc view <storeDir> [--exagg 8] [--opacity 0.5] " " gpr_poc view <storeDir> [--exagg 8] [--opacity 0.5] "
"[--smoke] [--preview] [--near] [--variant N] [--gallery] " "[--smoke] [--preview] [--near] [--variant N] [--gallery] "
"[--frames 90]\n" "[--frames 90]\n"
" gpr_poc view-all <storesDir> <gpsDir> [--preview] "
"[--exagg 8] [--level 1] [--spread M] [--shotDir <dir>]\n"
" gpr_poc polish <storeDir> [--exagg 8] [--frames 90] " " gpr_poc polish <storeDir> [--exagg 8] [--frames 90] "
"[--localBricks 4]\n"; "[--localBricks 4]\n";
} }
@ -4594,6 +4935,7 @@ int main(int argc, char** argv) {
if (cmd == "tune") return cmdTune(argc, argv); if (cmd == "tune") return cmdTune(argc, argv);
if (cmd == "fps-budget") return cmdFpsBudget(argc, argv); if (cmd == "fps-budget") return cmdFpsBudget(argc, argv);
if (cmd == "view") return cmdView(argc, argv); if (cmd == "view") return cmdView(argc, argv);
if (cmd == "view-all") return cmdViewAll(argc, argv);
if (cmd == "polish") return cmdPolish(argc, argv); if (cmd == "polish") return cmdPolish(argc, argv);
} catch (const std::exception& e) { } catch (const std::exception& e) {
std::cerr << "错误: " << e.what() << "\n"; std::cerr << "错误: " << e.what() << "\n";