diff --git a/tools/gpr_poc/main.cpp b/tools/gpr_poc/main.cpp index 68a1696..b61c5db 100644 --- a/tools/gpr_poc/main.cpp +++ b/tools/gpr_poc/main.cpp @@ -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 #include #include +#include #include #include #include @@ -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 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 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 prop = makeVariantProperty( + v, m.quant, cs, vmin, vmax, v.maxOpacity, + v.useGradientOpacity ? &gs : nullptr); + + vtkNew mapper; + mapper->SetInputData(lp.img.Get()); + mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode); + mapper->SetAutoAdjustSampleDistances(0); + mapper->SetInteractiveAdjustSampleDistances(0); + + auto volume = vtkSmartPointer::New(); + volume->SetMapper(mapper); + volume->SetProperty(prop); + + // 刚体摆放 + Z 夸张(一并烘进 UserTransform)。 + auto xf = vtkSmartPointer::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 [--preview] " + "[--exagg 8] [--level 1] [--spread M] [--shotDir ]\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 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 gpsList; + double minLat = std::numeric_limits::infinity(); + double minLon = std::numeric_limits::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:匹配 "*_.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 placements; + int lineIdx = 0; + for (const LineGps& lg : gpsList) { + // 轨迹 → 局部米(绕公共原点)。 + std::vector 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 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::New(); + if (!preview) rw->SetSize(winW, winH); + vtkNew ren; + ren->SetBackground(kViewDefaultVariant.bg[0], kViewDefaultVariant.bg[1], + kViewDefaultVariant.bg[2]); + rw->AddRenderer(ren); + + auto capWin = vtkSmartPointer::New(); + vtkOutputWindow::SetInstance(capWin); + + int placed = 0; + for (const LinePlacement& lp : placements) { + double vmin = 0, vmax = 0; + vtkSmartPointer 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 iren; + iren->SetRenderWindow(rw); + vtkNew 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 [--exagg 8] [--opacity 0.5] " "[--smoke] [--preview] [--near] [--variant N] [--gallery] " "[--frames 90]\n" + " gpr_poc view-all [--preview] " + "[--exagg 8] [--level 1] [--spread M] [--shotDir ]\n" " gpr_poc polish [--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";