feat/vtk-3d-view #7
|
|
@ -36,6 +36,7 @@
|
|||
#include "data/store/ChunkedVolumeStore.hpp"
|
||||
#include "io/gpr/Gpr3dvVolumeBridge.hpp"
|
||||
#include "io/gpr/GprSurveyAssembler.hpp"
|
||||
#include "io/gpr/GpsTrack.hpp"
|
||||
#include "io/gpr/IprHeader.hpp"
|
||||
#include "render/actors/VoxelActor.hpp"
|
||||
#include "render/source/OutOfCoreSource.hpp"
|
||||
|
|
@ -76,6 +77,7 @@
|
|||
#include <vtkShortArray.h>
|
||||
#include <vtkSmartPointer.h>
|
||||
#include <vtkSmartVolumeMapper.h>
|
||||
#include <vtkTransform.h>
|
||||
#include <vtkUnsignedCharArray.h>
|
||||
#include <vtkVolume.h>
|
||||
#include <vtkWindowToImageFilter.h>
|
||||
|
|
@ -3674,6 +3676,343 @@ int cmdViewGallery(const std::string& dir, int frames,
|
|||
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);
|
||||
// - 每条体加载为一张整卷 vtkImageData(coarse 体小,按 --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:每条体取金字塔哪一层整渲。默认 1(L1 ~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 << ":缺 .gps(gpsDir 无 *_" << 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) {
|
||||
const Args a = parseArgs(argc, argv, 2);
|
||||
if (a.positional.empty()) {
|
||||
|
|
@ -4561,6 +4900,8 @@ void usage() {
|
|||
" gpr_poc view <storeDir> [--exagg 8] [--opacity 0.5] "
|
||||
"[--smoke] [--preview] [--near] [--variant N] [--gallery] "
|
||||
"[--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] "
|
||||
"[--localBricks 4]\n";
|
||||
}
|
||||
|
|
@ -4594,6 +4935,7 @@ int main(int argc, char** argv) {
|
|||
if (cmd == "tune") return cmdTune(argc, argv);
|
||||
if (cmd == "fps-budget") return cmdFpsBudget(argc, argv);
|
||||
if (cmd == "view") return cmdView(argc, argv);
|
||||
if (cmd == "view-all") return cmdViewAll(argc, argv);
|
||||
if (cmd == "polish") return cmdPolish(argc, argv);
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "错误: " << e.what() << "\n";
|
||||
|
|
|
|||
Loading…
Reference in New Issue