6835 lines
314 KiB
C++
6835 lines
314 KiB
C++
// gpr_poc —— POC-B headless 度量 CLI。
|
||
//
|
||
// 串起整条地基:发现 14 通道 .iprb + .ord → assembleGprSurvey → buildGprVolume
|
||
// → ChunkedVolumeStore::write → buildPyramid → WholeVolumeSource(load),
|
||
// 在真实/合成数据上输出可测的真实指标(耗时/维度/体积/压缩比/加载/峰值内存)。
|
||
//
|
||
// 子命令:
|
||
// gpr_poc build <dir> [--line 001] [--cellXY 0.2] [--cellZ 0.05] [--out <storeDir>] [--levels 2]
|
||
// gpr_poc load <storeDir>
|
||
// gpr_poc selftest
|
||
// gpr_poc offscreen-smoke —— 离屏 GL 闸门冒烟
|
||
// gpr_poc renderB <storeDir> [--frames 120] —— 离屏体绘制/切片 fps 基准
|
||
|
||
#include <algorithm>
|
||
#include <chrono>
|
||
#include <cmath>
|
||
#include <cstdint>
|
||
#include <cstdlib>
|
||
#include <filesystem>
|
||
#include <fstream>
|
||
#include <iostream>
|
||
#include <limits>
|
||
#include <map>
|
||
#include <memory>
|
||
#include <set>
|
||
#include <string>
|
||
#include <thread>
|
||
#include <vector>
|
||
|
||
#include "Probe.hpp"
|
||
|
||
#include "core/algo/GeoVolumeBuilder.hpp"
|
||
#include "core/algo/GprVolumeBuilder.hpp"
|
||
#include "core/algo/IInterpolator.hpp"
|
||
#include "core/model/GprSurvey.hpp"
|
||
#include "core/model/ColorScale.hpp"
|
||
#include "core/model/ScalarVolumeI16.hpp"
|
||
#include "data/store/ChunkedVolumeStore.hpp"
|
||
#include "io/gpr/Gpr3dvSurveyVolumeBridge.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"
|
||
#include "render/interact/SlicePlaneMath.hpp"
|
||
#include "render/interact/SliceTool.hpp"
|
||
#include "render/source/ViewAdaptiveVolumeSource.hpp"
|
||
#include "render/source/WholeVolumeSource.hpp"
|
||
|
||
// P9: WGS84 → CGCS2000 高斯-克吕格精确平面坐标(vendored 3DGPRViewer,零 Qt)。
|
||
#include "CoordinateTransform.h"
|
||
|
||
// ---- VTK 离屏渲染 ----
|
||
#include <vtkActor.h>
|
||
#include <vtkCamera.h>
|
||
#include <vtkCubeSource.h>
|
||
#include <vtkGPUVolumeRayCastMapper.h>
|
||
#include <vtkMultiVolume.h>
|
||
#include <vtkOpenGLGPUVolumeRayCastMapper.h>
|
||
#include <vtkOpenGLRenderWindow.h>
|
||
#include <vtkTextureUnitManager.h>
|
||
#include <vtkImageActor.h>
|
||
#include <vtkImageData.h>
|
||
#include <vtkImageMapToColors.h>
|
||
#include <vtkImageMapper3D.h>
|
||
#include <vtkImageReslice.h>
|
||
#include <vtkLookupTable.h>
|
||
#include <vtkColorTransferFunction.h>
|
||
#include <vtkMultiBlockDataSet.h>
|
||
#include <vtkMultiBlockVolumeMapper.h>
|
||
#include <vtkNew.h>
|
||
#include <vtkPiecewiseFunction.h>
|
||
#include <vtkVolumeProperty.h>
|
||
#include <vtkObjectFactory.h>
|
||
#include <vtkOutputWindow.h>
|
||
#include <vtkPNGWriter.h>
|
||
#include <vtkPointData.h>
|
||
#include <vtkImageAppendComponents.h>
|
||
#include <vtkImageBlend.h>
|
||
#include <vtkImageThreshold.h>
|
||
#include <vtkOutlineFilter.h>
|
||
#include <vtkPlaneSource.h>
|
||
#include <vtkPolyDataMapper.h>
|
||
#include <vtkProperty.h>
|
||
#include <vtkRenderWindow.h>
|
||
#include <vtkRenderWindowInteractor.h>
|
||
#include <vtkInteractorStyleTrackballCamera.h>
|
||
#include <vtkCallbackCommand.h>
|
||
#include <vtkScalarBarActor.h>
|
||
#include <vtkTextActor.h>
|
||
#include <vtkTextProperty.h>
|
||
#include <vtkRenderer.h>
|
||
#include <vtkShortArray.h>
|
||
#include <vtkSmartPointer.h>
|
||
#include <vtkSmartVolumeMapper.h>
|
||
#include <vtkTransform.h>
|
||
#include <vtkUnsignedCharArray.h>
|
||
#include <vtkVolume.h>
|
||
#include <vtkWindowToImageFilter.h>
|
||
|
||
#ifdef _WIN32
|
||
#include <windows.h> // SetConsoleOutputCP(修中文控制台 GBK 乱码)
|
||
#endif
|
||
|
||
namespace fs = std::filesystem;
|
||
using geopro::tools::Probe;
|
||
using geopro::tools::Stopwatch;
|
||
|
||
namespace {
|
||
|
||
constexpr int kChannels = 14;
|
||
|
||
// ---- 命令行参数解析(极简 --key value)----
|
||
struct Args {
|
||
std::map<std::string, std::string> kv;
|
||
std::vector<std::string> positional;
|
||
|
||
std::string get(const std::string& key, const std::string& def) const {
|
||
auto it = kv.find(key);
|
||
return it != kv.end() ? it->second : def;
|
||
}
|
||
};
|
||
|
||
Args parseArgs(int argc, char** argv, int start) {
|
||
Args a;
|
||
for (int i = start; i < argc; ++i) {
|
||
std::string tok = argv[i];
|
||
if (tok.rfind("--", 0) == 0) {
|
||
// 下一个 token 也是 --xxx(或本 token 是末尾)→ 本 token 是无值布尔旗标,
|
||
// 存空串(避免把后面的旗标误当成本旗标的值,如 --preview --near)。
|
||
const bool hasValue =
|
||
(i + 1 < argc) && std::string(argv[i + 1]).rfind("--", 0) != 0;
|
||
a.kv[tok.substr(2)] = hasValue ? argv[i + 1] : "";
|
||
if (hasValue) ++i;
|
||
} else {
|
||
a.positional.push_back(tok);
|
||
}
|
||
}
|
||
return a;
|
||
}
|
||
|
||
// 把一行指标追加写入 last-metrics.txt(与可执行同目录的工具源目录无关,
|
||
// 写到当前工作目录便于汇总;CSV 风格一行)。
|
||
void writeMetricLine(const std::string& line) {
|
||
std::ofstream f("last-metrics.txt", std::ios::app);
|
||
if (f) f << line << "\n";
|
||
}
|
||
|
||
// 发现某线 14 通道 .iprb(按通道号 A01..A14 排序)+ 该线 .ord。
|
||
struct LineFiles {
|
||
std::vector<std::string> iprb; // 已按通道号排序
|
||
std::string ord;
|
||
};
|
||
|
||
LineFiles discoverLine(const std::string& dir, const std::string& line) {
|
||
LineFiles lf;
|
||
std::map<int, std::string> byChannel; // 通道号 → 路径(自动按号排序)
|
||
std::string ordPath;
|
||
|
||
for (const auto& e : fs::directory_iterator(dir)) {
|
||
if (!e.is_regular_file()) continue;
|
||
const std::string name = e.path().filename().string();
|
||
const std::string ext = e.path().extension().string();
|
||
|
||
// .ord:优先匹配本线(含 "_<line>" 且以 .ord 结尾),否则记下工区任一 .ord 作兜底。
|
||
if (ext == ".ord") {
|
||
if (name.find("_" + line + ".") != std::string::npos) {
|
||
ordPath = e.path().string();
|
||
} else if (ordPath.empty()) {
|
||
ordPath = e.path().string();
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// .iprb:匹配 "*_<line>_A<NN>.iprb"。
|
||
if (ext != ".iprb") continue;
|
||
const std::string tag = "_" + line + "_A";
|
||
const std::size_t pos = name.find(tag);
|
||
if (pos == std::string::npos) continue;
|
||
const std::size_t numStart = pos + tag.size();
|
||
std::size_t numEnd = numStart;
|
||
while (numEnd < name.size() &&
|
||
std::isdigit(static_cast<unsigned char>(name[numEnd]))) {
|
||
++numEnd;
|
||
}
|
||
if (numEnd == numStart) continue;
|
||
const int ch = std::stoi(name.substr(numStart, numEnd - numStart));
|
||
byChannel[ch] = e.path().string();
|
||
}
|
||
|
||
for (const auto& [ch, path] : byChannel) lf.iprb.push_back(path);
|
||
lf.ord = ordPath;
|
||
return lf;
|
||
}
|
||
|
||
// 由 survey 推 GridSpec:X 沿测线,Y 跨通道,Z 深度。
|
||
geopro::core::GridSpec specFromSurvey(const geopro::core::GprSurvey& s,
|
||
double cellXY, double cellZ) {
|
||
geopro::core::GridSpec spec{};
|
||
|
||
const double rangeX =
|
||
(s.ntraces > 1) ? (s.ntraces - 1) * s.dx : 0.0;
|
||
const double y0 = s.channelY.empty() ? 0.0 : s.channelY.front();
|
||
const double y1 = s.channelY.empty() ? 0.0 : s.channelY.back();
|
||
const double rangeY = y1 - y0;
|
||
const double rangeZ =
|
||
(s.samples > 1) ? (s.samples - 1) * s.dz : 0.0;
|
||
|
||
auto cells = [](double range, double cell) {
|
||
if (cell <= 0.0) return 1;
|
||
return static_cast<int>(std::ceil(range / cell)) + 1;
|
||
};
|
||
|
||
spec.ox = s.x0;
|
||
spec.oy = y0;
|
||
spec.oz = s.z0;
|
||
spec.dx = cellXY;
|
||
spec.dy = cellXY;
|
||
spec.dz = cellZ;
|
||
spec.nx = cells(rangeX, cellXY);
|
||
spec.ny = cells(rangeY, cellXY);
|
||
spec.nz = cells(rangeZ, cellZ);
|
||
spec.power = 2.0;
|
||
spec.maxDist = cellXY * 2.0;
|
||
return spec;
|
||
}
|
||
|
||
// 落盘 data.bin 体积(所有 data*.bin 之和,含金字塔各级)。
|
||
std::int64_t storeDataBytes(const std::string& dir) {
|
||
std::int64_t total = 0;
|
||
for (const auto& e : fs::directory_iterator(dir)) {
|
||
if (!e.is_regular_file()) continue;
|
||
const std::string name = e.path().filename().string();
|
||
if (name.rfind("data", 0) == 0 &&
|
||
e.path().extension().string() == ".bin") {
|
||
total += static_cast<std::int64_t>(e.file_size());
|
||
}
|
||
}
|
||
return total;
|
||
}
|
||
|
||
int cmdBuild(int argc, char** argv) {
|
||
const Args a = parseArgs(argc, argv, 2);
|
||
if (a.positional.empty()) {
|
||
std::cerr << "用法: gpr_poc build <dir> [--line 001] [--cellXY 0.2] "
|
||
"[--cellZ 0.05] [--out <storeDir>] [--levels 2]\n";
|
||
return 2;
|
||
}
|
||
const std::string dir = a.positional[0];
|
||
const std::string line = a.get("line", "001");
|
||
const double cellXY = std::stod(a.get("cellXY", "0.2"));
|
||
const double cellZ = std::stod(a.get("cellZ", "0.05"));
|
||
const int levels = std::stoi(a.get("levels", "2"));
|
||
const std::string out =
|
||
a.get("out", (fs::temp_directory_path() / ("gpr_store_" + line)).string());
|
||
|
||
std::cout << "[build] dir=" << dir << " line=" << line
|
||
<< " cellXY=" << cellXY << " cellZ=" << cellZ
|
||
<< " levels=" << levels << " out=" << out << "\n";
|
||
|
||
const LineFiles lf = discoverLine(dir, line);
|
||
std::cout << "[build] 发现通道数=" << lf.iprb.size()
|
||
<< " ord=" << (lf.ord.empty() ? "(无)" : lf.ord) << "\n";
|
||
if (lf.iprb.size() != static_cast<std::size_t>(kChannels)) {
|
||
std::cerr << "[build] 警告: 通道数 != " << kChannels
|
||
<< "(仍按发现数继续)\n";
|
||
}
|
||
if (lf.iprb.empty() || lf.ord.empty()) {
|
||
std::cerr << "[build] 错误: 未发现 .iprb 或 .ord\n";
|
||
return 1;
|
||
}
|
||
|
||
// 1) 装配
|
||
Stopwatch swAsm;
|
||
geopro::core::GprSurvey survey =
|
||
geopro::io::gpr::assembleGprSurvey(lf.iprb, lf.ord);
|
||
const double asmMs = swAsm.elapsedMs();
|
||
std::cout << "[build] 装配完成 ntraces=" << survey.ntraces
|
||
<< " samples=" << survey.samples
|
||
<< " channels=" << survey.channelY.size()
|
||
<< " dx=" << survey.dx << " dz=" << survey.dz << "\n";
|
||
|
||
// 2) 建体
|
||
const geopro::core::GridSpec spec = specFromSurvey(survey, cellXY, cellZ);
|
||
std::cout << "[build] GridSpec nx=" << spec.nx << " ny=" << spec.ny
|
||
<< " nz=" << spec.nz << " dx=" << spec.dx << " dy=" << spec.dy
|
||
<< " dz=" << spec.dz << " maxDist=" << spec.maxDist << "\n";
|
||
|
||
Stopwatch swBuild;
|
||
geopro::core::BuiltI16 built = geopro::core::buildGprVolume(survey, spec);
|
||
const double buildMs = swBuild.elapsedMs();
|
||
|
||
const std::int64_t nx = built.vol.nx(), ny = built.vol.ny(), nz = built.vol.nz();
|
||
const std::int64_t voxels = nx * ny * nz;
|
||
const std::int64_t rawBytes = voxels * 2; // int16
|
||
|
||
// 3) 落盘 + 金字塔
|
||
fs::create_directories(out);
|
||
Stopwatch swWrite;
|
||
geopro::data::ChunkedVolumeStore::write(out, built);
|
||
const double writeMs = swWrite.elapsedMs();
|
||
|
||
Stopwatch swPyr;
|
||
{
|
||
geopro::data::ChunkedVolumeStore store(out);
|
||
store.buildPyramid(levels);
|
||
}
|
||
const double pyrMs = swPyr.elapsedMs();
|
||
|
||
const std::int64_t dataBytes = storeDataBytes(out);
|
||
const double ratio =
|
||
dataBytes > 0 ? static_cast<double>(rawBytes) / dataBytes : 0.0;
|
||
const double peak = Probe::peakMemMB();
|
||
|
||
std::cout << "\n=== build 指标 ===\n";
|
||
std::cout << "装配耗时(ms) : " << asmMs << "\n";
|
||
std::cout << "建体耗时(ms) : " << buildMs << "\n";
|
||
std::cout << "落盘耗时(ms) : " << writeMs << "\n";
|
||
std::cout << "金字塔耗时(ms) : " << pyrMs << "\n";
|
||
std::cout << "体维度 : " << nx << " x " << ny << " x " << nz << "\n";
|
||
std::cout << "体素数 : " << voxels << "\n";
|
||
std::cout << "原始体积(B) : " << rawBytes << " ("
|
||
<< rawBytes / (1024.0 * 1024.0) << " MB)\n";
|
||
std::cout << "data.bin(B) : " << dataBytes << " ("
|
||
<< dataBytes / (1024.0 * 1024.0) << " MB)\n";
|
||
std::cout << "压缩比 : " << ratio << " x\n";
|
||
std::cout << "峰值内存(MB) : " << peak << "\n";
|
||
|
||
writeMetricLine(
|
||
"build,line=" + line + ",cellXY=" + std::to_string(cellXY) +
|
||
",cellZ=" + std::to_string(cellZ) + ",nx=" + std::to_string(nx) +
|
||
",ny=" + std::to_string(ny) + ",nz=" + std::to_string(nz) +
|
||
",voxels=" + std::to_string(voxels) +
|
||
",rawB=" + std::to_string(rawBytes) +
|
||
",dataB=" + std::to_string(dataBytes) +
|
||
",ratio=" + std::to_string(ratio) + ",asmMs=" + std::to_string(asmMs) +
|
||
",buildMs=" + std::to_string(buildMs) +
|
||
",writeMs=" + std::to_string(writeMs) +
|
||
",pyrMs=" + std::to_string(pyrMs) + ",peakMB=" + std::to_string(peak));
|
||
return 0;
|
||
}
|
||
|
||
// ============================================================================
|
||
// build-stream:多线合并流式建大体(Track B 总验收)
|
||
// ============================================================================
|
||
//
|
||
// 把工区目录下全部测线(各 14 通道 .iprb + 该线 .ord)流式建成【一个连续合并大体】:
|
||
// 1) 扫目录发现所有线号;
|
||
// 2) 定合并网格:沿 X 顺序排列(线 i 接在线 i-1 之后,按 brick 对齐对齐到 64 格边界),
|
||
// Y/Z 取各线最大值——退路近似(report 标注),证明大体流式建得出来且内存有界;
|
||
// 3) 全局量化:单遍扫所有线所有 slab 定全局 vmin/vmax(一次只持一个 64 道 slab);
|
||
// 4) 单个 StreamingVolumeWriter 跨线逐 slab 逐 brick 写(各线落在合并网格对应 X 区域),
|
||
// 全程不持整卷/不持整线 survey;
|
||
// 5) buildPyramidStreaming → finalize。
|
||
//
|
||
// 复用 buildGprVolumeStreaming 的 slab/采样核机制(sampleGprPoint + StreamingVolumeWriter),
|
||
// 仅在 X 方向把多线拼到同一 store。
|
||
|
||
constexpr int kStreamBrick = 64; // 与 StreamingVolumeWriter/Store 内部 brick 一致
|
||
|
||
int ceilDivInt(int n, int b) { return (n + b - 1) / b; }
|
||
int extentOf(int n, int b, int brick) {
|
||
const int got = n - b * brick;
|
||
return got < brick ? got : brick;
|
||
}
|
||
|
||
// 发现工区内全部线号(三位零填充,如 "001"):扫 .ord,取 "*_<NNN>.ord" 的 NNN。
|
||
std::vector<std::string> discoverLines(const std::string& dir) {
|
||
std::vector<std::string> lines;
|
||
for (const auto& e : fs::directory_iterator(dir)) {
|
||
if (!e.is_regular_file()) continue;
|
||
if (e.path().extension().string() != ".ord") continue;
|
||
const std::string stem = e.path().stem().string(); // 明星路_001
|
||
const std::size_t us = stem.find_last_of('_');
|
||
if (us == std::string::npos) continue;
|
||
const std::string num = stem.substr(us + 1);
|
||
bool allDigit = !num.empty();
|
||
for (char c : num)
|
||
if (!std::isdigit(static_cast<unsigned char>(c))) allDigit = false;
|
||
if (allDigit) lines.push_back(num);
|
||
}
|
||
std::sort(lines.begin(), lines.end());
|
||
return lines;
|
||
}
|
||
|
||
// 一条线的几何 + 道距 + 全线总道数(不持整线:1 道 slab 取标尺,header 取总道数)。
|
||
struct LineGeom {
|
||
int samples = 0;
|
||
std::int64_t totalTraces = 0;
|
||
double dx = 1.0, dz = 1.0;
|
||
std::vector<double> channelY; // 升序
|
||
int nx = 0, ny = 0, nz = 0; // 该线在合并网格下的体素维度(X 未对齐 brick)
|
||
};
|
||
|
||
// 全线总道数 = min 通道(fileBytes/(samples*2)),与 assembleGprSurvey 对齐口径一致。
|
||
std::int64_t totalTracesOf(const std::vector<std::string>& iprb, int samples) {
|
||
std::int64_t minTr = std::numeric_limits<std::int64_t>::max();
|
||
const std::int64_t per = static_cast<std::int64_t>(samples) * 2;
|
||
for (const auto& p : iprb) {
|
||
const std::int64_t bytes = static_cast<std::int64_t>(fs::file_size(p));
|
||
if (per <= 0) throw std::runtime_error("samples<=0");
|
||
minTr = std::min(minTr, bytes / per);
|
||
}
|
||
return minTr;
|
||
}
|
||
|
||
int cmdBuildStream(int argc, char** argv) {
|
||
const Args a = parseArgs(argc, argv, 2);
|
||
if (a.positional.empty()) {
|
||
std::cerr << "用法: gpr_poc build-stream <dir> [--cellXY 0.05] "
|
||
"[--cellZ 0.05] [--out <storeDir>] [--levels 3] "
|
||
"[--sliceXBricks 8] [--maxLines N]\n";
|
||
return 2;
|
||
}
|
||
const std::string dir = a.positional[0];
|
||
const double cellXY = std::stod(a.get("cellXY", "0.05"));
|
||
const double cellZ = std::stod(a.get("cellZ", "0.05"));
|
||
const int levels = std::stoi(a.get("levels", "3"));
|
||
int sliceXBricks = std::stoi(a.get("sliceXBricks", "8"));
|
||
if (sliceXBricks <= 0) sliceXBricks = 1;
|
||
const int maxLines = std::stoi(a.get("maxLines", "0")); // 0=全部
|
||
const std::string out =
|
||
a.get("out", (fs::temp_directory_path() / "gpr_store_merged").string());
|
||
|
||
std::cout << "[build-stream] dir=" << dir << " cellXY=" << cellXY
|
||
<< " cellZ=" << cellZ << " levels=" << levels
|
||
<< " sliceXBricks=" << sliceXBricks << " out=" << out << "\n";
|
||
|
||
// 1) 发现线号。
|
||
std::vector<std::string> lineNos = discoverLines(dir);
|
||
if (maxLines > 0 && static_cast<int>(lineNos.size()) > maxLines)
|
||
lineNos.resize(maxLines);
|
||
std::cout << "[build-stream] 发现测线数=" << lineNos.size() << "\n";
|
||
if (lineNos.empty()) {
|
||
std::cerr << "[build-stream] 错误: 未发现任何 .ord 测线\n";
|
||
return 1;
|
||
}
|
||
|
||
Stopwatch swTotal;
|
||
|
||
// 2) 各线文件 + 几何(1 道 slab 取标尺,不持整线)。
|
||
std::vector<LineFiles> files;
|
||
std::vector<LineGeom> geom;
|
||
files.reserve(lineNos.size());
|
||
geom.reserve(lineNos.size());
|
||
for (const std::string& ln : lineNos) {
|
||
LineFiles lf = discoverLine(dir, ln);
|
||
if (lf.iprb.empty() || lf.ord.empty()) {
|
||
std::cerr << "[build-stream] 警告: 线 " << ln << " 缺 iprb/ord,跳过\n";
|
||
continue;
|
||
}
|
||
LineGeom g;
|
||
// 1 道 slab:取 dx/dz/samples/channelY(内存只随 1 道)。
|
||
const geopro::core::GprSurvey s0 =
|
||
geopro::io::gpr::assembleGprSurveySlab(lf.iprb, lf.ord, 0, 1);
|
||
g.samples = s0.samples;
|
||
g.dx = s0.dx;
|
||
g.dz = s0.dz;
|
||
g.channelY = s0.channelY;
|
||
g.totalTraces = totalTracesOf(lf.iprb, g.samples);
|
||
|
||
// 该线在合并网格下维度(X/Z 落格,Y 跨通道):与 specFromSurvey 同式。
|
||
const double rangeX = (g.totalTraces > 1) ? (g.totalTraces - 1) * g.dx : 0.0;
|
||
const double rangeY =
|
||
g.channelY.empty() ? 0.0 : (g.channelY.back() - g.channelY.front());
|
||
const double rangeZ = (g.samples > 1) ? (g.samples - 1) * g.dz : 0.0;
|
||
auto cells = [](double range, double cell) {
|
||
if (cell <= 0.0) return 1;
|
||
return static_cast<int>(std::ceil(range / cell)) + 1;
|
||
};
|
||
g.nx = cells(rangeX, cellXY);
|
||
g.ny = cells(rangeY, cellXY);
|
||
g.nz = cells(rangeZ, cellZ);
|
||
|
||
std::cout << "[build-stream] 线 " << ln << " 通道=" << lf.iprb.size()
|
||
<< " 道数=" << g.totalTraces << " samples=" << g.samples
|
||
<< " nx=" << g.nx << " ny=" << g.ny << " nz=" << g.nz << "\n";
|
||
files.push_back(std::move(lf));
|
||
geom.push_back(std::move(g));
|
||
}
|
||
if (files.empty()) {
|
||
std::cerr << "[build-stream] 错误: 无可用测线\n";
|
||
return 1;
|
||
}
|
||
|
||
// 3) 合并网格(沿 X 顺序排列;各线 X 起点对齐到 brick 边界)。
|
||
// 每线占 [xBrickOffset, xBrickOffset + ceil(nx/brick)) 的 brick 列。
|
||
std::vector<int> xBrickOffset(files.size());
|
||
int mergedBx = 0, mergedNy = 0, mergedNz = 0;
|
||
for (std::size_t i = 0; i < files.size(); ++i) {
|
||
xBrickOffset[i] = mergedBx;
|
||
mergedBx += ceilDivInt(geom[i].nx, kStreamBrick);
|
||
mergedNy = std::max(mergedNy, geom[i].ny);
|
||
mergedNz = std::max(mergedNz, geom[i].nz);
|
||
}
|
||
const int mergedNx = mergedBx * kStreamBrick; // 末线 brick 对齐后整宽
|
||
const int bY = ceilDivInt(mergedNy, kStreamBrick);
|
||
const int bZ = ceilDivInt(mergedNz, kStreamBrick);
|
||
|
||
std::cout << "[build-stream] 合并网格 nx=" << mergedNx << " ny=" << mergedNy
|
||
<< " nz=" << mergedNz << " (bX=" << mergedBx << " bY=" << bY
|
||
<< " bZ=" << bZ << ")\n";
|
||
|
||
// 每线网格 spec(origin 沿 X 平移到该线 brick 起点的世界 X)。
|
||
auto specForLine = [&](std::size_t i) {
|
||
geopro::core::GridSpec spec{};
|
||
spec.ox = xBrickOffset[i] * kStreamBrick * cellXY; // 该线在合并体的世界 X 起点
|
||
spec.oy = geom[i].channelY.empty() ? 0.0 : geom[i].channelY.front();
|
||
spec.oz = 0.0;
|
||
spec.dx = cellXY;
|
||
spec.dy = cellXY;
|
||
spec.dz = cellZ;
|
||
spec.nx = geom[i].nx;
|
||
spec.ny = geom[i].ny;
|
||
spec.nz = geom[i].nz;
|
||
spec.power = 2.0;
|
||
spec.maxDist = cellXY * 2.0;
|
||
return spec;
|
||
};
|
||
|
||
// 4) 全局量化:单遍扫所有线所有 slab(一次只持一个 64 道 slab)。
|
||
std::cout << "[build-stream] 扫全局量化区间...\n";
|
||
Stopwatch swScan;
|
||
double vmin = std::numeric_limits<double>::infinity();
|
||
double vmax = -std::numeric_limits<double>::infinity();
|
||
constexpr std::int64_t kScanChunk = 64;
|
||
for (std::size_t i = 0; i < files.size(); ++i) {
|
||
const std::int64_t total = geom[i].totalTraces;
|
||
for (std::int64_t t0 = 0; t0 < total; t0 += kScanChunk) {
|
||
const std::int64_t t1 = std::min(total, t0 + kScanChunk);
|
||
const auto slab = geopro::io::gpr::assembleGprSurveySlab(
|
||
files[i].iprb, files[i].ord, t0, t1);
|
||
for (double v : slab.values) {
|
||
if (std::isnan(v)) continue;
|
||
if (v < vmin) vmin = v;
|
||
if (v > vmax) vmax = v;
|
||
}
|
||
}
|
||
}
|
||
if (!(vmin <= vmax)) {
|
||
vmin = 0.0;
|
||
vmax = 0.0;
|
||
}
|
||
const double scanMs = swScan.elapsedMs();
|
||
std::cout << "[build-stream] 全局值域 [" << vmin << ", " << vmax << "] 扫描 "
|
||
<< scanMs << "ms\n";
|
||
|
||
geopro::core::Quant quant;
|
||
quant.scale = (vmax > vmin) ? (vmax - vmin) / 64000.0 : 1.0;
|
||
quant.offset = 0.5 * (vmin + vmax);
|
||
|
||
// 5) 合并 StoreMeta + 单个 StreamingVolumeWriter。
|
||
geopro::data::StoreMeta meta;
|
||
meta.nx = mergedNx;
|
||
meta.ny = mergedNy;
|
||
meta.nz = mergedNz;
|
||
meta.brick = kStreamBrick;
|
||
meta.origin = {0.0, 0.0, 0.0};
|
||
meta.spacing = {cellXY, cellXY, cellZ};
|
||
meta.quant = quant;
|
||
meta.vminPhys = vmin;
|
||
meta.vmaxPhys = vmax;
|
||
|
||
fs::create_directories(out);
|
||
geopro::data::StreamingVolumeWriter writer(out, meta);
|
||
|
||
// 6) 跨线逐 slab 逐 brick 写。每线在自己的 brick 列区间内沿 X 分 slab;
|
||
// 合并网格 brick (mergedBx, by, bz):
|
||
// - 落在某线列区间内且该线有覆盖 → sampleGprPoint(线局部索引);
|
||
// - 否则 → blank(线间填充 + 该线 ny/nz 之外的合并余量)。
|
||
std::cout << "[build-stream] 流式写合并大体...\n";
|
||
Stopwatch swBuild;
|
||
for (std::size_t i = 0; i < files.size(); ++i) {
|
||
const geopro::core::GridSpec spec = specForLine(i);
|
||
const int lineBx = ceilDivInt(geom[i].nx, kStreamBrick);
|
||
const int lineBy = ceilDivInt(geom[i].ny, kStreamBrick);
|
||
const int lineBz = ceilDivInt(geom[i].nz, kStreamBrick);
|
||
const std::int64_t total = geom[i].totalTraces;
|
||
const double surveyDx = geom[i].dx > 0.0 ? geom[i].dx : 1.0;
|
||
|
||
// 沿 X 分 slab(brick 对齐),每 slab 含 sliceXBricks 个 X brick。
|
||
for (int bcol = 0; bcol < lineBx; bcol += sliceXBricks) {
|
||
const int bxEnd = std::min(lineBx, bcol + sliceXBricks);
|
||
const int gx0 = bcol * kStreamBrick;
|
||
const int gx1 = std::min(spec.nx, bxEnd * kStreamBrick);
|
||
|
||
// 该 slab 网格 X 列 → 全局道范围(夹到 [0,total)),可能全越界。
|
||
std::int64_t t0 = std::numeric_limits<std::int64_t>::max();
|
||
std::int64_t t1 = std::numeric_limits<std::int64_t>::min();
|
||
for (int gi = gx0; gi < gx1; ++gi) {
|
||
const double worldX = gi * cellXY; // 线局部世界 X(spec.ox 已含偏移,但落格用线内 x0=0)
|
||
const std::int64_t g = std::llround(worldX / surveyDx);
|
||
if (g < 0 || g >= total) continue;
|
||
t0 = std::min(t0, g);
|
||
t1 = std::max(t1, g);
|
||
}
|
||
const bool hasTraces = (t0 <= t1);
|
||
geopro::core::GprSurvey slab;
|
||
// 线局部 spec:x0=0 落格(与 assembleGprSurveySlab 的 x0=t0*dx 对齐靠 worldX)。
|
||
geopro::core::GridSpec localSpec = spec;
|
||
localSpec.ox = 0.0; // 采样核用线局部坐标
|
||
if (hasTraces) {
|
||
slab = geopro::io::gpr::assembleGprSurveySlab(files[i].iprb,
|
||
files[i].ord, t0, t1 + 1);
|
||
}
|
||
|
||
// 写该 slab 覆盖的合并 brick:X 列 [bcol,bxEnd) → 合并列 +xBrickOffset[i],
|
||
// Y/Z 全程(含该线 ny/nz 之外的合并余量 → blank)。
|
||
for (int bz = 0; bz < bZ; ++bz) {
|
||
for (int by = 0; by < bY; ++by) {
|
||
for (int lbx = bcol; lbx < bxEnd; ++lbx) {
|
||
const int mbx = xBrickOffset[i] + lbx; // 合并 brick X 索引
|
||
const int bw = extentOf(mergedNx, mbx, kStreamBrick);
|
||
const int bh = extentOf(mergedNy, by, kStreamBrick);
|
||
const int bd = extentOf(mergedNz, bz, kStreamBrick);
|
||
std::vector<std::int16_t> voxels(
|
||
static_cast<std::size_t>(bw) * bh * bd);
|
||
// 该 brick 是否落在线自身覆盖范围内(线 brick 网格内)。
|
||
const bool inLine =
|
||
(lbx < lineBx && by < lineBy && bz < lineBz && hasTraces);
|
||
if (!inLine) {
|
||
std::fill(voxels.begin(), voxels.end(),
|
||
geopro::core::ScalarVolumeI16::kBlank);
|
||
} else {
|
||
const int i0 = lbx * kStreamBrick, j0 = by * kStreamBrick,
|
||
k0 = bz * kStreamBrick;
|
||
std::size_t wi = 0;
|
||
for (int kk = 0; kk < bd; ++kk) {
|
||
for (int jj = 0; jj < bh; ++jj) {
|
||
for (int ii = 0; ii < bw; ++ii) {
|
||
const int gi = i0 + ii, gj = j0 + jj, gk = k0 + kk;
|
||
// 线网格之外的余格(合并 brick 比线 brick 大)→ blank。
|
||
if (gi >= spec.nx || gj >= spec.ny || gk >= spec.nz) {
|
||
voxels[wi++] = geopro::core::ScalarVolumeI16::kBlank;
|
||
} else {
|
||
voxels[wi++] = geopro::core::sampleGprPoint(
|
||
slab, localSpec, gi, gj, gk, quant);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
writer.writeBrick(mbx, by, bz, voxels);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
std::cout << "[build-stream] 线 " << lineNos[i] << " 写入完成 ("
|
||
<< (i + 1) << "/" << files.size()
|
||
<< ") 峰值内存(MB)=" << Probe::peakMemMB() << "\n";
|
||
}
|
||
writer.finalize();
|
||
const double buildMs = swBuild.elapsedMs();
|
||
|
||
// 7) 流式金字塔。
|
||
std::cout << "[build-stream] 流式金字塔 levels=" << levels << "...\n";
|
||
Stopwatch swPyr;
|
||
{
|
||
geopro::data::ChunkedVolumeStore store(out);
|
||
store.buildPyramidStreaming(levels);
|
||
}
|
||
const double pyrMs = swPyr.elapsedMs();
|
||
|
||
const std::int64_t voxels =
|
||
static_cast<std::int64_t>(mergedNx) * mergedNy * mergedNz;
|
||
const std::int64_t rawBytes = voxels * 2;
|
||
const std::int64_t dataBytes = storeDataBytes(out);
|
||
const double ratio =
|
||
dataBytes > 0 ? static_cast<double>(rawBytes) / dataBytes : 0.0;
|
||
const double totalMs = swTotal.elapsedMs();
|
||
const double peak = Probe::peakMemMB();
|
||
|
||
std::cout << "\n=== build-stream 指标(多线合并大体)===\n";
|
||
std::cout << "合并方式 : 沿 X 顺序排列(退路近似,brick 对齐)\n";
|
||
std::cout << "测线数 : " << files.size() << "\n";
|
||
std::cout << "合并体维度 : " << mergedNx << " x " << mergedNy << " x "
|
||
<< mergedNz << "\n";
|
||
std::cout << "体素数 : " << voxels << "\n";
|
||
std::cout << "原始体积(B) : " << rawBytes << " ("
|
||
<< rawBytes / (1024.0 * 1024.0) << " MB)\n";
|
||
std::cout << "data.bin(B) : " << dataBytes << " ("
|
||
<< dataBytes / (1024.0 * 1024.0) << " MB)\n";
|
||
std::cout << "压缩比 : " << ratio << " x\n";
|
||
std::cout << "扫量化耗时(ms) : " << scanMs << "\n";
|
||
std::cout << "建体耗时(ms) : " << buildMs << "\n";
|
||
std::cout << "金字塔耗时(ms) : " << pyrMs << "\n";
|
||
std::cout << "总耗时(ms) : " << totalMs << "\n";
|
||
std::cout << "峰值内存(MB) : " << peak << "\n";
|
||
|
||
writeMetricLine(
|
||
"build-stream,lines=" + std::to_string(files.size()) +
|
||
",cellXY=" + std::to_string(cellXY) + ",cellZ=" + std::to_string(cellZ) +
|
||
",nx=" + std::to_string(mergedNx) + ",ny=" + std::to_string(mergedNy) +
|
||
",nz=" + std::to_string(mergedNz) + ",voxels=" + std::to_string(voxels) +
|
||
",rawB=" + std::to_string(rawBytes) +
|
||
",dataB=" + std::to_string(dataBytes) +
|
||
",ratio=" + std::to_string(ratio) + ",scanMs=" + std::to_string(scanMs) +
|
||
",buildMs=" + std::to_string(buildMs) +
|
||
",pyrMs=" + std::to_string(pyrMs) +
|
||
",totalMs=" + std::to_string(totalMs) +
|
||
",peakMB=" + std::to_string(peak));
|
||
return 0;
|
||
}
|
||
|
||
// ============================================================================
|
||
// build-geo:按真实 RTK 几何把多线插值进统一路向网格(Task G1)
|
||
// ============================================================================
|
||
//
|
||
// 消除 build-stream 顺序拼接的退化扁带:各线 .gps RTK 轨迹 → 经纬 → 局部米 →
|
||
// PCA 路向旋转 → 道按里程均匀分布定位、14 通道横偏垂直航向摆放 → 全部插进统一
|
||
// 路向网格(≈4472×43×81)重叠取均值 → 量化写 ChunkedVolumeStore + 金字塔。
|
||
int cmdBuildGeo(int argc, char** argv) {
|
||
const Args a = parseArgs(argc, argv, 2);
|
||
if (a.positional.empty()) {
|
||
std::cerr << "用法: gpr_poc build-geo <dir> [--cellXY 0.5] [--cellZ 0.1] "
|
||
"[--out <storeDir>] [--levels 4] [--maxLines N] "
|
||
"[--curvilinear]\n";
|
||
return 2;
|
||
}
|
||
const std::string dir = a.positional[0];
|
||
const double cellXY = std::stod(a.get("cellXY", "0.5"));
|
||
const double cellZ = std::stod(a.get("cellZ", "0.1"));
|
||
const int levels = std::stoi(a.get("levels", "4"));
|
||
const int maxLines = std::stoi(a.get("maxLines", "0"));
|
||
const bool curvilinear = a.kv.count("curvilinear") > 0; // 曲线版(G2)
|
||
const std::string out =
|
||
a.get("out", (fs::temp_directory_path() / "gpr_store_geo").string());
|
||
|
||
std::cout << "[build-geo] dir=" << dir << " cellXY=" << cellXY
|
||
<< " cellZ=" << cellZ << " levels=" << levels
|
||
<< " mode=" << (curvilinear ? "曲线(中心线)" : "PCA")
|
||
<< " out=" << out << "\n";
|
||
|
||
// 发现线号 → 各线 iprb/ord(复用 discoverLine)+ .gps(按线号匹配)。
|
||
std::vector<std::string> lineNos = discoverLines(dir);
|
||
if (maxLines > 0 && static_cast<int>(lineNos.size()) > maxLines)
|
||
lineNos.resize(maxLines);
|
||
std::cout << "[build-geo] 发现测线数=" << lineNos.size() << "\n";
|
||
if (lineNos.empty()) {
|
||
std::cerr << "[build-geo] 错误: 未发现任何 .ord 测线\n";
|
||
return 1;
|
||
}
|
||
|
||
std::vector<geopro::core::GeoLineInput> lines;
|
||
for (const std::string& ln : lineNos) {
|
||
const LineFiles lf = discoverLine(dir, ln);
|
||
if (lf.iprb.empty() || lf.ord.empty()) {
|
||
std::cerr << "[build-geo] 警告: 线 " << ln << " 缺 iprb/ord,跳过\n";
|
||
continue;
|
||
}
|
||
// .gps:匹配 "*_<ln>.gps"。
|
||
std::string gps;
|
||
for (const auto& e : fs::directory_iterator(dir)) {
|
||
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 us = stem.find_last_of('_');
|
||
if (us != std::string::npos && stem.substr(us + 1) == ln) {
|
||
gps = e.path().string();
|
||
break;
|
||
}
|
||
}
|
||
if (gps.empty()) {
|
||
std::cerr << "[build-geo] 警告: 线 " << ln << " 缺 .gps,跳过\n";
|
||
continue;
|
||
}
|
||
geopro::core::GeoLineInput in;
|
||
in.iprb = lf.iprb;
|
||
in.ord = lf.ord;
|
||
in.gps = gps;
|
||
lines.push_back(std::move(in));
|
||
}
|
||
if (lines.empty()) {
|
||
std::cerr << "[build-geo] 错误: 无可用测线\n";
|
||
return 1;
|
||
}
|
||
std::cout << "[build-geo] 可用测线数=" << lines.size() << "\n";
|
||
|
||
fs::create_directories(out);
|
||
Stopwatch sw;
|
||
const geopro::core::GeoGridSpec spec{cellXY, cellZ};
|
||
const geopro::core::GeoBuildResult r =
|
||
geopro::core::buildGeoVolume(lines, spec, out, levels, curvilinear);
|
||
const double buildMs = sw.elapsedMs();
|
||
|
||
const double fillRate =
|
||
r.total > 0 ? static_cast<double>(r.filled) / r.total : 0.0;
|
||
const double rotDeg = r.rotRad * 180.0 / 3.14159265358979323846;
|
||
const std::int64_t dataBytes = storeDataBytes(out);
|
||
const double peak = Probe::peakMemMB();
|
||
|
||
std::cout << "\n=== build-geo 指标(真实 RTK 几何统一路向体)===\n";
|
||
std::cout << "网格模式 : " << (curvilinear ? "曲线(中心线展开)" : "PCA 旋转")
|
||
<< "\n";
|
||
std::cout << "测线数 : " << lines.size() << "\n";
|
||
std::cout << "体维度 : " << r.nx << " x " << r.ny << " x " << r.nz
|
||
<< "\n";
|
||
std::cout << "总 cell : " << r.total << "\n";
|
||
std::cout << "非空 cell : " << r.filled << "\n";
|
||
std::cout << "填充率 : " << (fillRate * 100.0) << " %\n";
|
||
std::cout << "路向旋转角 : " << rotDeg << " ° (" << r.rotRad << " rad)\n";
|
||
std::cout << "网格原点(m) : (" << r.oxM << ", " << r.oyM << ")\n";
|
||
std::cout << "data.bin(B) : " << dataBytes << " ("
|
||
<< dataBytes / (1024.0 * 1024.0) << " MB)\n";
|
||
std::cout << "建体总耗时(ms) : " << buildMs << "\n";
|
||
std::cout << "峰值内存(MB) : " << peak << "\n";
|
||
|
||
writeMetricLine(
|
||
"build-geo,lines=" + std::to_string(lines.size()) +
|
||
",cellXY=" + std::to_string(cellXY) + ",cellZ=" + std::to_string(cellZ) +
|
||
",nx=" + std::to_string(r.nx) + ",ny=" + std::to_string(r.ny) +
|
||
",nz=" + std::to_string(r.nz) + ",total=" + std::to_string(r.total) +
|
||
",filled=" + std::to_string(r.filled) +
|
||
",fillRate=" + std::to_string(fillRate) +
|
||
",rotDeg=" + std::to_string(rotDeg) +
|
||
",buildMs=" + std::to_string(buildMs) +
|
||
",peakMB=" + std::to_string(peak));
|
||
return 0;
|
||
}
|
||
|
||
// ============================================================================
|
||
// build-line:走 gpr3dv(P1) 处理链→处理后立方体→geopro 量化体→分块+金字塔(P2)
|
||
// ============================================================================
|
||
//
|
||
// 与 build/build-stream/build-geo 不同:本命令不走 geopro 自家解析+采样核建体,
|
||
// 而是【复用 vendored gpr3dv 的读+处理链】(算法零改动)产出处理后立方体
|
||
// volumeData[通道][道][样本],再桥接(Gpr3dvVolumeBridge)成 geopro int16 量化体
|
||
// (轴 X=道/Y=通道/Z=样本),落 ChunkedVolumeStore + buildPyramid。原样渲(14 格 Y
|
||
// 薄体),不做横向插值加密。
|
||
// 磁盘剩余空间(GB):查 path 所在卷可用字节。失败(路径不存在等)→ -1(视为未知)。
|
||
double freeSpaceGB(const std::string& path) {
|
||
std::error_code ec;
|
||
// 用已存在的父目录查(out 目录可能还没建)。
|
||
fs::path p = fs::absolute(path, ec);
|
||
while (!p.empty() && !fs::exists(p, ec)) p = p.parent_path();
|
||
if (p.empty()) p = fs::current_path(ec);
|
||
const fs::space_info si = fs::space(p, ec);
|
||
if (ec) return -1.0;
|
||
return static_cast<double>(si.available) / (1024.0 * 1024.0 * 1024.0);
|
||
}
|
||
|
||
// 单线建体结果(供 build-all 汇总,不编造)。ok=false 时 reason 给清晰原因。
|
||
struct LineBuildResult {
|
||
std::string prefix;
|
||
bool ok = false;
|
||
std::string reason; // 失败/跳过原因
|
||
std::int64_t nx = 0, ny = 0, nz = 0;
|
||
std::int64_t dataBytes = 0;
|
||
};
|
||
|
||
// 单线建体核心:gpr3dv 处理链 → 桥接量化体(可 coarse 下采样) → 落盘 + 金字塔。
|
||
// 异常(加载失败/立方体空/短桩线维度退化)由调用方捕获,不在此中断批量。
|
||
LineBuildResult buildOneLine(const std::string& lineDir,
|
||
const std::string& linePrefix,
|
||
const std::string& out, int levels, int coarse,
|
||
double targetDy) {
|
||
LineBuildResult r;
|
||
r.prefix = linePrefix;
|
||
|
||
std::cout << "[build-line] lineDir=" << lineDir << " linePrefix=" << linePrefix
|
||
<< " levels=" << levels << " coarse=" << coarse << " out=" << out
|
||
<< "\n";
|
||
|
||
// 1) gpr3dv 处理链 → 处理后立方体 → 桥接量化体(coarse 沿测线下采样)。
|
||
Stopwatch swBridge;
|
||
geopro::io::gpr::BridgeMetrics bm;
|
||
geopro::core::BuiltI16 built = geopro::io::gpr::buildLineVolumeFromGpr3dv(
|
||
lineDir, linePrefix, &bm, coarse, targetDy);
|
||
const double bridgeMs = swBridge.elapsedMs();
|
||
|
||
const std::int64_t nx = built.vol.nx(), ny = built.vol.ny(),
|
||
nz = built.vol.nz();
|
||
r.nx = nx;
|
||
r.ny = ny;
|
||
r.nz = nz;
|
||
// 短桩线/退化体守护:维度过小无法建有意义的金字塔/体绘制 → 报因跳过(不落盘)。
|
||
if (nx < 2 || ny < 1 || nz < 2) {
|
||
r.ok = false;
|
||
r.reason = "体维度退化(道×通道×样本=" + std::to_string(nx) + "x" +
|
||
std::to_string(ny) + "x" + std::to_string(nz) +
|
||
"),无法建可看体,跳过";
|
||
std::cerr << "[build-line] " << linePrefix << " " << r.reason << "\n";
|
||
return r;
|
||
}
|
||
const std::int64_t voxels = nx * ny * nz;
|
||
const std::int64_t rawBytes = voxels * 2; // int16
|
||
|
||
std::cout << "[build-line] 处理前后平均绝对幅值: " << bm.meanAbsBefore << " → "
|
||
<< bm.meanAbsAfter << " (处理"
|
||
<< (std::abs(bm.meanAbsAfter - bm.meanAbsBefore) > 1e-9 ? "已生效"
|
||
: "未变化")
|
||
<< ")\n";
|
||
std::cout << "[build-line] 体维度(道×通道×样本) = " << nx << " x " << ny
|
||
<< " x " << nz << "\n";
|
||
std::cout << "[build-line] 世界 spacing dx=" << bm.dx << " dy=" << bm.dy
|
||
<< " dz=" << bm.dz << " (m)\n";
|
||
|
||
// 2) 落盘 + 金字塔(道很长 → 需 levels≥2 使最粗层≤16384)。
|
||
fs::create_directories(out);
|
||
Stopwatch swWrite;
|
||
geopro::data::ChunkedVolumeStore::write(out, built);
|
||
const double writeMs = swWrite.elapsedMs();
|
||
|
||
Stopwatch swPyr;
|
||
{
|
||
geopro::data::ChunkedVolumeStore store(out);
|
||
store.buildPyramid(levels);
|
||
}
|
||
const double pyrMs = swPyr.elapsedMs();
|
||
|
||
const std::int64_t dataBytes = storeDataBytes(out);
|
||
r.dataBytes = dataBytes;
|
||
const double ratio =
|
||
dataBytes > 0 ? static_cast<double>(rawBytes) / dataBytes : 0.0;
|
||
const double peak = Probe::peakMemMB();
|
||
|
||
std::cout << "\n=== build-line 指标(gpr3dv 处理后真三维体)===\n";
|
||
std::cout << "桥接耗时(ms) : " << bridgeMs << " (含 读 " << bm.loadMs
|
||
<< " + 处理 " << bm.pipelineMs << " + 量化填体 " << bm.fillMs
|
||
<< ")\n";
|
||
std::cout << "落盘耗时(ms) : " << writeMs << "\n";
|
||
std::cout << "金字塔耗时(ms) : " << pyrMs << "\n";
|
||
std::cout << "体维度 : " << nx << " x " << ny << " x " << nz << "\n";
|
||
std::cout << "体素数 : " << voxels << "\n";
|
||
std::cout << "处理后值域 : [" << bm.vminPhys << ", " << bm.vmaxPhys
|
||
<< "] 量化 scale=" << built.quant.scale
|
||
<< " offset=" << built.quant.offset << "\n";
|
||
std::cout << "世界 spacing : dx=" << bm.dx << " dy=" << bm.dy
|
||
<< " dz=" << bm.dz << " (m)\n";
|
||
std::cout << "原始体积(B) : " << rawBytes << " ("
|
||
<< rawBytes / (1024.0 * 1024.0) << " MB)\n";
|
||
std::cout << "data.bin(B) : " << dataBytes << " ("
|
||
<< dataBytes / (1024.0 * 1024.0) << " MB)\n";
|
||
std::cout << "压缩比 : " << ratio << " x\n";
|
||
std::cout << "峰值内存(MB) : " << peak << "\n";
|
||
|
||
writeMetricLine(
|
||
"build-line,prefix=" + linePrefix + ",coarse=" + std::to_string(coarse) +
|
||
",nx=" + std::to_string(nx) + ",ny=" + std::to_string(ny) +
|
||
",nz=" + std::to_string(nz) + ",voxels=" + std::to_string(voxels) +
|
||
",vmin=" + std::to_string(bm.vminPhys) +
|
||
",vmax=" + std::to_string(bm.vmaxPhys) +
|
||
",dx=" + std::to_string(bm.dx) + ",dy=" + std::to_string(bm.dy) +
|
||
",dz=" + std::to_string(bm.dz) + ",rawB=" + std::to_string(rawBytes) +
|
||
",dataB=" + std::to_string(dataBytes) +
|
||
",ratio=" + std::to_string(ratio) +
|
||
",bridgeMs=" + std::to_string(bridgeMs) +
|
||
",writeMs=" + std::to_string(writeMs) +
|
||
",pyrMs=" + std::to_string(pyrMs) + ",peakMB=" + std::to_string(peak));
|
||
|
||
r.ok = true;
|
||
return r;
|
||
}
|
||
|
||
// ess-stat:实测一条线密体的"空体素跳过(ESS)潜力"——按块(默认 16³)算 min/max,统计有多少块
|
||
// 整段值落在"近零背景带"(在 V 形不透明传函里≈透明)内 → 这些块 ESS 会整块跳过。空块占比≈ESS
|
||
// 提速潜力。零依赖、跑在真实数据上,是上不上 OSPRay/ESS 的关键决策数。
|
||
int cmdEssStat(int argc, char** argv) {
|
||
const Args a = parseArgs(argc, argv, 2);
|
||
if (a.positional.size() < 2) {
|
||
std::cerr << "用法: gpr_poc ess-stat <lineDir> <linePrefix> [--coarse 8] "
|
||
"[--targetDy 0.025] [--block 16]\n";
|
||
return 2;
|
||
}
|
||
const std::string lineDir = a.positional[0];
|
||
const std::string prefix = a.positional[1];
|
||
const int coarse = std::stoi(a.get("coarse", "8"));
|
||
const double targetDy = std::stod(a.get("targetDy", "0.025"));
|
||
const int B = std::max(1, std::stoi(a.get("block", "16")));
|
||
|
||
geopro::io::gpr::BridgeMetrics bm;
|
||
geopro::core::BuiltI16 built =
|
||
geopro::io::gpr::buildLineVolumeFromGpr3dv(lineDir, prefix, &bm, coarse,
|
||
targetDy);
|
||
const auto& vol = built.vol;
|
||
const int nx = vol.nx(), ny = vol.ny(), nz = vol.nz();
|
||
const double center = built.quant.offset;
|
||
const double half =
|
||
std::max(1e-9, 0.5 * (built.vmaxPhys - built.vminPhys));
|
||
std::cout << "[ess-stat] " << prefix << " 体维度=" << nx << "x" << ny << "x"
|
||
<< nz << " 值域=[" << built.vminPhys << "," << built.vmaxPhys
|
||
<< "] 块=" << B << "³\n";
|
||
|
||
// 几个"透明带半宽"阈值(相对半值域):背景带越宽,可跳块越多(但太宽会跳掉弱反射)。
|
||
const double taus[] = {0.05, 0.10, 0.15, 0.20, 0.30};
|
||
constexpr int NT = 5;
|
||
long total = 0, skip[NT] = {0, 0, 0, 0, 0};
|
||
// 同时统计"近零体素占比"(单体素 < 0.1 半值域)作直觉参考。
|
||
long voxNear = 0, voxTotal = 0;
|
||
|
||
for (int z0 = 0; z0 < nz; z0 += B)
|
||
for (int y0 = 0; y0 < ny; y0 += B)
|
||
for (int x0 = 0; x0 < nx; x0 += B) {
|
||
short mn = std::numeric_limits<short>::max();
|
||
short mx = std::numeric_limits<short>::min();
|
||
const int x1 = std::min(x0 + B, nx), y1 = std::min(y0 + B, ny),
|
||
z1 = std::min(z0 + B, nz);
|
||
for (int z = z0; z < z1; ++z)
|
||
for (int y = y0; y < y1; ++y)
|
||
for (int x = x0; x < x1; ++x) {
|
||
const short v = vol.at(x, y, z);
|
||
if (v < mn) mn = v;
|
||
if (v > mx) mx = v;
|
||
const double p = built.quant.toPhys(v);
|
||
++voxTotal;
|
||
if (std::abs(p - center) < 0.10 * half) ++voxNear;
|
||
}
|
||
++total;
|
||
const double pmn = built.quant.toPhys(mn);
|
||
const double pmx = built.quant.toPhys(mx);
|
||
const double dev = std::max(std::abs(pmn - center), std::abs(pmx - center));
|
||
for (int t = 0; t < NT; ++t)
|
||
if (dev < taus[t] * half) ++skip[t];
|
||
}
|
||
|
||
std::cout << "[ess-stat] 近零体素占比(<0.1 半值域) = "
|
||
<< (voxTotal ? 100.0 * voxNear / voxTotal : 0.0) << "%\n";
|
||
std::cout << "[ess-stat] 可跳块占比(块内全段落在透明带) / 对应 ESS 理论提速:\n";
|
||
for (int t = 0; t < NT; ++t) {
|
||
const double frac = total ? static_cast<double>(skip[t]) / total : 0.0;
|
||
const double speedup = frac < 0.999 ? 1.0 / (1.0 - frac) : 999.0;
|
||
std::cout << " 透明带半宽 " << static_cast<int>(taus[t] * 100)
|
||
<< "% 半值域 → 可跳块 " << 100.0 * frac << "% → 理论上限 ~"
|
||
<< speedup << "×\n";
|
||
}
|
||
std::cout << "[ess-stat] 注:理论上限=1/(1-可跳块占比),实际 OSPRay 还有 SIMD/光线相干等增益,"
|
||
"但非空块(反射层)仍要采。可跳块>80% 即 ESS 大幅值得上。\n";
|
||
return 0;
|
||
}
|
||
|
||
vtkSmartPointer<vtkRenderWindow> makeOffscreenWindow(int w, int h); // 前置声明(定义在后)
|
||
|
||
// passcost:确诊 P11/P12"各线独立 mapper + LOD 仍 0.5fps"卡在哪。
|
||
// N 个独立 vtkSmartVolumeMapper(GPU),每个渲一个 size³ 体,测离屏稳态 fps vs N。
|
||
// --overlap 0:N 个体【铺开】不重叠(光线各穿 ~1 个)→ 隔离"每遍固定开销"(嫌疑2);
|
||
// --overlap 1:N 个体【叠在一起】重叠(光线穿 N 个)→ 隔离"重叠采样"(嫌疑3)。
|
||
// 铺开随 N 线性掉=固定开销(嫌疑2,最坏);铺开不掉、叠加掉=重叠(嫌疑3);都不掉=选区没调小(嫌疑1,最好)。
|
||
int cmdPassCost(int argc, char** argv) {
|
||
const Args a = parseArgs(argc, argv, 0);
|
||
const int S = std::stoi(a.get("size", "64")); // 每体边长(模拟 LOD 削小后的小区)
|
||
const bool overlap = std::stoi(a.get("overlap", "0")) != 0;
|
||
const int W = 1400, H = 900;
|
||
|
||
// 合成一个 S³ 体(含非平凡图案,避免被早终止/空跳优化掉)。
|
||
auto img = vtkSmartPointer<vtkImageData>::New();
|
||
img->SetDimensions(S, S, S);
|
||
img->SetSpacing(1.0, 1.0, 1.0);
|
||
vtkNew<vtkShortArray> arr;
|
||
arr->SetName("v");
|
||
arr->SetNumberOfTuples(static_cast<vtkIdType>(S) * S * S);
|
||
for (int z = 0; z < S; ++z)
|
||
for (int y = 0; y < S; ++y)
|
||
for (int x = 0; x < S; ++x) {
|
||
const double v = 200.0 * std::sin(0.3 * x) * std::cos(0.3 * z) + 300.0;
|
||
arr->SetValue((static_cast<vtkIdType>(z) * S + y) * S + x,
|
||
static_cast<short>(v));
|
||
}
|
||
img->GetPointData()->SetScalars(arr);
|
||
|
||
auto prop = vtkSmartPointer<vtkVolumeProperty>::New();
|
||
vtkNew<vtkColorTransferFunction> col;
|
||
col->AddRGBPoint(0, 0.1, 0.1, 0.2);
|
||
col->AddRGBPoint(600, 0.95, 0.95, 0.9);
|
||
vtkNew<vtkPiecewiseFunction> op;
|
||
op->AddPoint(0, 0.0);
|
||
op->AddPoint(300, 0.05);
|
||
op->AddPoint(600, 0.4);
|
||
prop->SetColor(col);
|
||
prop->SetScalarOpacity(op);
|
||
prop->SetInterpolationTypeToLinear();
|
||
|
||
std::cout << "=== passcost:N 个独立 GPU mapper × " << S << "³ 体 ("
|
||
<< (overlap ? "叠在一起/重叠" : "铺开/不重叠") << ") ===\n";
|
||
std::cout << "N fps(离屏稳态)\n";
|
||
for (int N : {1, 3, 5, 10, 20}) {
|
||
auto rw = makeOffscreenWindow(W, H);
|
||
vtkNew<vtkRenderer> ren;
|
||
ren->SetBackground(0.05, 0.05, 0.08);
|
||
rw->AddRenderer(ren);
|
||
std::vector<vtkSmartPointer<vtkSmartVolumeMapper>> ms;
|
||
std::vector<vtkSmartPointer<vtkVolume>> vs;
|
||
for (int n = 0; n < N; ++n) {
|
||
auto m = vtkSmartPointer<vtkSmartVolumeMapper>::New();
|
||
m->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
|
||
m->SetInputData(img);
|
||
auto v = vtkSmartPointer<vtkVolume>::New();
|
||
v->SetMapper(m);
|
||
v->SetProperty(prop);
|
||
// 铺开:沿 X 排开 S 间距(不重叠);重叠:全堆在原点。
|
||
if (!overlap) v->SetPosition(static_cast<double>(n) * S * 1.05, 0, 0);
|
||
ren->AddVolume(v);
|
||
ms.push_back(m);
|
||
vs.push_back(v);
|
||
}
|
||
ren->ResetCamera();
|
||
rw->Render(); // 预热(编译着色器/上传纹理)
|
||
rw->Render();
|
||
Stopwatch sw;
|
||
const int F = 30;
|
||
for (int f = 0; f < F; ++f) rw->Render();
|
||
const double ms_per = sw.elapsedMs();
|
||
const double fps = ms_per > 0 ? F * 1000.0 / ms_per : 0.0;
|
||
std::cout << N << " " << fps << "\n";
|
||
}
|
||
std::cout << "判读:铺开随 N 线性掉=每遍固定开销(嫌疑2,最坏,逼架构取舍);"
|
||
"铺开不掉/叠加掉=重叠(嫌疑3);都不怎么掉=0.5fps 是选区没调小(嫌疑1,最好,可修)。\n";
|
||
return 0;
|
||
}
|
||
|
||
int cmdBuildLine(int argc, char** argv) {
|
||
const Args a = parseArgs(argc, argv, 2);
|
||
if (a.positional.size() < 2) {
|
||
std::cerr << "用法: gpr_poc build-line <lineDir> <linePrefix> "
|
||
"--out <storeDir> [--levels 3] [--coarse F]\n"
|
||
"例: gpr_poc build-line \"D:/Downloads/明星路\" 明星路_001 "
|
||
"--out tmp/line001_proc --levels 3 --coarse 4\n";
|
||
return 2;
|
||
}
|
||
const std::string lineDir = a.positional[0];
|
||
const std::string linePrefix = a.positional[1];
|
||
const int levels = std::stoi(a.get("levels", "3"));
|
||
const int coarse = std::stoi(a.get("coarse", "1"));
|
||
const double targetDy = std::stod(a.get("targetDy", "0.025"));
|
||
const std::string out =
|
||
a.get("out", (fs::temp_directory_path() / "gpr_store_line").string());
|
||
|
||
try {
|
||
const LineBuildResult r =
|
||
buildOneLine(lineDir, linePrefix, out, levels, coarse, targetDy);
|
||
if (!r.ok) {
|
||
std::cerr << "[build-line] 跳过: " << r.reason << "\n";
|
||
return 1;
|
||
}
|
||
return 0;
|
||
} catch (const std::exception& e) {
|
||
std::cerr << "[build-line] 失败(" << linePrefix << "): " << e.what() << "\n";
|
||
return 1;
|
||
}
|
||
}
|
||
|
||
// build-all:发现目录下所有测线(_Axx 分组),逐条 build-line 到 baseDir/<lineName>/。
|
||
// 磁盘守护:每条建前查可用空间,低于阈值(默认 3GB)即停并报已建哪些。
|
||
// 短桩线/异常单条捕获并跳过(报因),不中断其余。
|
||
int cmdBuildAll(int argc, char** argv) {
|
||
const Args a = parseArgs(argc, argv, 2);
|
||
if (a.positional.empty() || !a.kv.count("outBase")) {
|
||
std::cerr << "用法: gpr_poc build-all <lineDir> --outBase <baseDir> "
|
||
"[--levels 3] [--coarse F] [--minFreeGB 3]\n"
|
||
"例: gpr_poc build-all \"D:/Downloads/明星路\" "
|
||
"--outBase tmp/lines_all --levels 3 --coarse 4\n";
|
||
return 2;
|
||
}
|
||
const std::string lineDir = a.positional[0];
|
||
const std::string outBase = a.get("outBase", "");
|
||
const int levels = std::stoi(a.get("levels", "3"));
|
||
const int coarse = std::stoi(a.get("coarse", "1"));
|
||
const double minFreeGB = std::stod(a.get("minFreeGB", "3"));
|
||
const double targetDy = std::stod(a.get("targetDy", "0.025"));
|
||
|
||
// 1) 发现所有测线前缀:扫 *_<line>_A<NN>.iprh,取 "<...>_<line>" 部分(去 _A<NN>)。
|
||
std::set<std::string> prefixSet;
|
||
std::error_code ec;
|
||
for (const auto& e : fs::directory_iterator(lineDir, ec)) {
|
||
if (!e.is_regular_file()) continue;
|
||
const std::string name = e.path().filename().string();
|
||
if (e.path().extension().string() != ".iprh") continue;
|
||
// 找 "_A<NN>" 通道后缀,截断得测线前缀。
|
||
const std::size_t pos = name.rfind("_A");
|
||
if (pos == std::string::npos) continue;
|
||
std::size_t d = pos + 2;
|
||
while (d < name.size() && std::isdigit(static_cast<unsigned char>(name[d])))
|
||
++d;
|
||
if (d == pos + 2) continue; // _A 后无数字 → 非通道文件
|
||
prefixSet.insert(name.substr(0, pos));
|
||
}
|
||
if (prefixSet.empty()) {
|
||
std::cerr << "[build-all] 未在 " << lineDir << " 发现任何测线(*_A<NN>.iprh)\n";
|
||
return 1;
|
||
}
|
||
std::vector<std::string> prefixes(prefixSet.begin(), prefixSet.end());
|
||
std::cout << "[build-all] 发现 " << prefixes.size() << " 条测线,outBase="
|
||
<< outBase << " levels=" << levels << " coarse=" << coarse
|
||
<< " minFreeGB=" << minFreeGB << "\n";
|
||
|
||
fs::create_directories(outBase, ec);
|
||
|
||
// 2) 逐条建:磁盘守护 → buildOneLine(单条 try/catch)。
|
||
std::vector<LineBuildResult> results;
|
||
bool stoppedByDisk = false;
|
||
for (const std::string& prefix : prefixes) {
|
||
const double freeGB = freeSpaceGB(outBase);
|
||
std::cout << "\n[build-all] --- " << prefix << " --- 剩余磁盘 "
|
||
<< freeGB << " GB\n";
|
||
if (freeGB >= 0.0 && freeGB < minFreeGB) {
|
||
std::cerr << "[build-all] 磁盘守护触发: 剩余 " << freeGB << " GB < "
|
||
<< minFreeGB << " GB,停止,未建 " << prefix << " 及其后。\n";
|
||
stoppedByDisk = true;
|
||
break;
|
||
}
|
||
const std::string out = (fs::path(outBase) / prefix).string();
|
||
LineBuildResult r;
|
||
r.prefix = prefix;
|
||
try {
|
||
r = buildOneLine(lineDir, prefix, out, levels, coarse, targetDy);
|
||
} catch (const std::exception& e) {
|
||
r.ok = false;
|
||
r.reason = std::string("异常: ") + e.what();
|
||
std::cerr << "[build-all] " << prefix << " 失败: " << e.what()
|
||
<< "(跳过,继续)\n";
|
||
}
|
||
results.push_back(r);
|
||
}
|
||
|
||
// 3) 汇总。
|
||
std::cout << "\n=== build-all 汇总 ===\n";
|
||
int okCount = 0;
|
||
std::int64_t totalBytes = 0;
|
||
for (const auto& r : results) {
|
||
if (r.ok) {
|
||
++okCount;
|
||
totalBytes += r.dataBytes;
|
||
std::cout << " [OK] " << r.prefix << " 维度=" << r.nx << "x" << r.ny
|
||
<< "x" << r.nz << " data="
|
||
<< r.dataBytes / (1024.0 * 1024.0) << " MB\n";
|
||
} else {
|
||
std::cout << " [跳过] " << r.prefix << " 原因=" << r.reason << "\n";
|
||
}
|
||
}
|
||
std::cout << "成功 " << okCount << "/" << prefixes.size()
|
||
<< " 条,合计 data=" << totalBytes / (1024.0 * 1024.0 * 1024.0)
|
||
<< " GB,剩余磁盘 " << freeSpaceGB(outBase) << " GB\n";
|
||
if (stoppedByDisk)
|
||
std::cout << "注意: 因磁盘守护提前停止,部分测线未建。\n";
|
||
return 0;
|
||
}
|
||
|
||
// ============================================================================
|
||
// build-survey-line / build-survey-all:测绘级精确坐标逐线世界对齐体(Task P8)
|
||
// ============================================================================
|
||
//
|
||
// 与 build-line(线局部坐标 X=道/Y=通道/Z=样本,origin≈0)不同:本路径走 P8 测绘级桥接
|
||
// (Gpr3dvSurveyVolumeBridge)——用 vendored 3DGPRViewer 的精确坐标/轨迹/网格代码
|
||
// (CoordinateTransform/TrajectoryCalculator/CScanGridder,算法零改动),让单线严格按
|
||
// CGCS2000 大地坐标、逐通道逐道跟 GPS 轨迹建成【世界轴对齐体】(跟路的弯)。体 meta.origin
|
||
// 为真实 CGCS2000 世界米,多线共享同一参考系 → view-survey-all 直接按 origin 摆放即精确就位。
|
||
|
||
// 按线前缀(如 "明星路_001")在 dir 下找匹配的 .gps(尾段 _NNN 相同)。空串=未找到。
|
||
std::string findGpsForPrefix(const std::string& dir, const std::string& prefix) {
|
||
const std::size_t us = prefix.find_last_of('_');
|
||
if (us == std::string::npos) return "";
|
||
const std::string num = prefix.substr(us + 1);
|
||
std::error_code ec;
|
||
for (const auto& e : fs::directory_iterator(dir, ec)) {
|
||
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)
|
||
return e.path().string();
|
||
}
|
||
return "";
|
||
}
|
||
|
||
// 单线测绘级建体核心:P8 桥接 → 世界对齐量化体 → 落盘 + 金字塔。
|
||
// 异常(加载失败/GPS 无效/体空/退化)由调用方捕获,不在此中断批量。
|
||
LineBuildResult buildOneSurveyLine(const std::string& lineDir,
|
||
const std::string& linePrefix,
|
||
const std::string& gpsPath,
|
||
const std::string& out, int levels,
|
||
int coarse, double cellSizeM,
|
||
double searchRadiusM) {
|
||
LineBuildResult r;
|
||
r.prefix = linePrefix;
|
||
|
||
std::cout << "[build-survey-line] lineDir=" << lineDir
|
||
<< " linePrefix=" << linePrefix << " gps=" << gpsPath
|
||
<< " levels=" << levels << " coarse=" << coarse
|
||
<< " cellSize=" << cellSizeM << " out=" << out << "\n";
|
||
|
||
Stopwatch swBridge;
|
||
geopro::io::gpr::SurveyBridgeMetrics bm;
|
||
geopro::core::BuiltI16 built = geopro::io::gpr::buildLineVolumeSurvey(
|
||
lineDir, linePrefix, gpsPath, &bm, coarse, cellSizeM, searchRadiusM);
|
||
const double bridgeMs = swBridge.elapsedMs();
|
||
|
||
const std::int64_t nx = built.vol.nx(), ny = built.vol.ny(),
|
||
nz = built.vol.nz();
|
||
r.nx = nx;
|
||
r.ny = ny;
|
||
r.nz = nz;
|
||
if (nx < 2 || ny < 2 || nz < 2) {
|
||
r.ok = false;
|
||
r.reason = "世界体维度退化(东×北×深=" + std::to_string(nx) + "x" +
|
||
std::to_string(ny) + "x" + std::to_string(nz) +
|
||
"),无法建可看体,跳过";
|
||
std::cerr << "[build-survey-line] " << linePrefix << " " << r.reason << "\n";
|
||
return r;
|
||
}
|
||
const std::int64_t voxels = nx * ny * nz;
|
||
const std::int64_t rawBytes = voxels * 2;
|
||
const double fillRate =
|
||
bm.totalCells > 0
|
||
? static_cast<double>(bm.filledCells) / bm.totalCells
|
||
: 0.0;
|
||
|
||
std::cout << "[build-survey-line] 处理前后平均绝对幅值: " << bm.meanAbsBefore
|
||
<< " → " << bm.meanAbsAfter << "\n";
|
||
std::cout << "[build-survey-line] RTK点=" << bm.rtkPoints
|
||
<< " 通道=" << bm.channels << " CGCS带号=" << bm.cgcsZone
|
||
<< " 中央经线=" << bm.centralMeridianDeg << "°\n";
|
||
std::cout << "[build-survey-line] 世界体维度(东×北×深) = " << nx << " x " << ny
|
||
<< " x " << nz << " 非空体素=" << bm.filledCells << " ("
|
||
<< (fillRate * 100.0) << "%)\n";
|
||
std::cout << "[build-survey-line] CGCS2000 世界 origin=(" << std::fixed
|
||
<< bm.originX << ", " << bm.originY << ") spacing cell=" << bm.cellSizeM
|
||
<< " dz=" << bm.dz << " (m)\n";
|
||
|
||
fs::create_directories(out);
|
||
Stopwatch swWrite;
|
||
geopro::data::ChunkedVolumeStore::write(out, built);
|
||
const double writeMs = swWrite.elapsedMs();
|
||
|
||
Stopwatch swPyr;
|
||
{
|
||
geopro::data::ChunkedVolumeStore store(out);
|
||
store.buildPyramid(levels);
|
||
}
|
||
const double pyrMs = swPyr.elapsedMs();
|
||
|
||
const std::int64_t dataBytes = storeDataBytes(out);
|
||
r.dataBytes = dataBytes;
|
||
const double ratio =
|
||
dataBytes > 0 ? static_cast<double>(rawBytes) / dataBytes : 0.0;
|
||
const double peak = Probe::peakMemMB();
|
||
|
||
std::cout << "\n=== build-survey-line 指标(测绘级 CGCS2000 世界对齐体)===\n";
|
||
std::cout << "桥接耗时(ms) : " << bridgeMs << " (读 " << bm.loadMs
|
||
<< " + 处理 " << bm.pipelineMs << " + GPS轨迹 " << bm.trajMs
|
||
<< " + 网格量化 " << bm.gridMs << ")\n";
|
||
std::cout << "落盘耗时(ms) : " << writeMs << "\n";
|
||
std::cout << "金字塔耗时(ms) : " << pyrMs << "\n";
|
||
std::cout << "世界体维度 : " << nx << " x " << ny << " x " << nz << "\n";
|
||
std::cout << "体素数 : " << voxels << " 非空 " << bm.filledCells
|
||
<< " (" << (fillRate * 100.0) << "%)\n";
|
||
std::cout << "处理后值域 : [" << bm.vminPhys << ", " << bm.vmaxPhys
|
||
<< "] 量化 scale=" << built.quant.scale
|
||
<< " offset=" << built.quant.offset << "\n";
|
||
std::cout << "CGCS2000 origin: (" << std::fixed << bm.originX << ", "
|
||
<< bm.originY << ") (东, 北) 米\n";
|
||
std::cout << "世界 spacing : cell=" << bm.cellSizeM << " dz=" << bm.dz
|
||
<< " (m)\n";
|
||
std::cout << "data.bin(B) : " << dataBytes << " ("
|
||
<< dataBytes / (1024.0 * 1024.0) << " MB) 压缩比 " << ratio
|
||
<< " x\n";
|
||
std::cout << "峰值内存(MB) : " << peak << "\n";
|
||
|
||
writeMetricLine(
|
||
"build-survey-line,prefix=" + linePrefix +
|
||
",coarse=" + std::to_string(coarse) + ",nx=" + std::to_string(nx) +
|
||
",ny=" + std::to_string(ny) + ",nz=" + std::to_string(nz) +
|
||
",voxels=" + std::to_string(voxels) +
|
||
",filled=" + std::to_string(bm.filledCells) +
|
||
",zone=" + std::to_string(bm.cgcsZone) +
|
||
",originX=" + std::to_string(bm.originX) +
|
||
",originY=" + std::to_string(bm.originY) +
|
||
",cell=" + std::to_string(bm.cellSizeM) + ",dz=" + std::to_string(bm.dz) +
|
||
",vmin=" + std::to_string(bm.vminPhys) +
|
||
",vmax=" + std::to_string(bm.vmaxPhys) +
|
||
",dataB=" + std::to_string(dataBytes) +
|
||
",bridgeMs=" + std::to_string(bridgeMs) +
|
||
",peakMB=" + std::to_string(peak));
|
||
|
||
r.ok = true;
|
||
return r;
|
||
}
|
||
|
||
int cmdBuildSurveyLine(int argc, char** argv) {
|
||
const Args a = parseArgs(argc, argv, 2);
|
||
if (a.positional.size() < 2) {
|
||
std::cerr << "用法: gpr_poc build-survey-line <lineDir> <linePrefix> "
|
||
"--out <storeDir> [--levels 3] [--coarse F] [--cell 0.05] "
|
||
"[--radius 0.5] [--gps <path>]\n"
|
||
"例: gpr_poc build-survey-line \"D:/Downloads/明星路\" "
|
||
"明星路_001 --out tmp/survey001 --levels 3 --coarse 4\n";
|
||
return 2;
|
||
}
|
||
const std::string lineDir = a.positional[0];
|
||
const std::string linePrefix = a.positional[1];
|
||
const int levels = std::stoi(a.get("levels", "3"));
|
||
const int coarse = std::stoi(a.get("coarse", "1"));
|
||
const double cellSizeM = std::stod(a.get("cell", "0"));
|
||
const double radiusM = std::stod(a.get("radius", "0"));
|
||
std::string gps = a.get("gps", "");
|
||
if (gps.empty()) gps = findGpsForPrefix(lineDir, linePrefix);
|
||
const std::string out = a.get(
|
||
"out", (fs::temp_directory_path() / "gpr_store_survey_line").string());
|
||
|
||
if (gps.empty()) {
|
||
std::cerr << "[build-survey-line] 失败: 未找到 " << linePrefix
|
||
<< " 的 .gps(可用 --gps 指定)\n";
|
||
return 1;
|
||
}
|
||
try {
|
||
const LineBuildResult r = buildOneSurveyLine(
|
||
lineDir, linePrefix, gps, out, levels, coarse, cellSizeM, radiusM);
|
||
if (!r.ok) {
|
||
std::cerr << "[build-survey-line] 跳过: " << r.reason << "\n";
|
||
return 1;
|
||
}
|
||
return 0;
|
||
} catch (const std::exception& e) {
|
||
std::cerr << "[build-survey-line] 失败(" << linePrefix << "): " << e.what()
|
||
<< "\n";
|
||
return 1;
|
||
}
|
||
}
|
||
|
||
// build-survey-all:发现所有测线 → 逐条走 P8 测绘级路径建世界对齐体到 baseDir/<lineName>/。
|
||
// 各线 .gps 自动按尾段 _NNN 匹配;缺 .gps 的线跳过报因。磁盘守护同 build-all。
|
||
int cmdBuildSurveyAll(int argc, char** argv) {
|
||
const Args a = parseArgs(argc, argv, 2);
|
||
if (a.positional.empty() || !a.kv.count("outBase")) {
|
||
std::cerr << "用法: gpr_poc build-survey-all <lineDir> --outBase <baseDir> "
|
||
"[--levels 3] [--coarse F] [--cell 0.05] [--radius 0.5] "
|
||
"[--minFreeGB 3]\n"
|
||
"例: gpr_poc build-survey-all \"D:/Downloads/明星路\" "
|
||
"--outBase tmp/survey_all --levels 3 --coarse 4\n";
|
||
return 2;
|
||
}
|
||
const std::string lineDir = a.positional[0];
|
||
const std::string outBase = a.get("outBase", "");
|
||
const int levels = std::stoi(a.get("levels", "3"));
|
||
const int coarse = std::stoi(a.get("coarse", "1"));
|
||
const double cellSizeM = std::stod(a.get("cell", "0"));
|
||
const double radiusM = std::stod(a.get("radius", "0"));
|
||
const double minFreeGB = std::stod(a.get("minFreeGB", "3"));
|
||
|
||
// 发现所有测线前缀(扫 *_A<NN>.iprh,截 _A<NN> 得前缀),与 build-all 同口径。
|
||
std::set<std::string> prefixSet;
|
||
std::error_code ec;
|
||
for (const auto& e : fs::directory_iterator(lineDir, ec)) {
|
||
if (!e.is_regular_file()) continue;
|
||
if (e.path().extension().string() != ".iprh") continue;
|
||
const std::string name = e.path().filename().string();
|
||
const std::size_t pos = name.rfind("_A");
|
||
if (pos == std::string::npos) continue;
|
||
std::size_t d = pos + 2;
|
||
while (d < name.size() && std::isdigit(static_cast<unsigned char>(name[d])))
|
||
++d;
|
||
if (d == pos + 2) continue;
|
||
prefixSet.insert(name.substr(0, pos));
|
||
}
|
||
if (prefixSet.empty()) {
|
||
std::cerr << "[build-survey-all] 未在 " << lineDir
|
||
<< " 发现任何测线(*_A<NN>.iprh)\n";
|
||
return 1;
|
||
}
|
||
std::vector<std::string> prefixes(prefixSet.begin(), prefixSet.end());
|
||
std::cout << "[build-survey-all] 发现 " << prefixes.size()
|
||
<< " 条测线,outBase=" << outBase << " levels=" << levels
|
||
<< " coarse=" << coarse << " minFreeGB=" << minFreeGB << "\n";
|
||
|
||
fs::create_directories(outBase, ec);
|
||
|
||
std::vector<LineBuildResult> results;
|
||
bool stoppedByDisk = false;
|
||
for (const std::string& prefix : prefixes) {
|
||
const double freeGB = freeSpaceGB(outBase);
|
||
std::cout << "\n[build-survey-all] --- " << prefix << " --- 剩余磁盘 "
|
||
<< freeGB << " GB\n";
|
||
if (freeGB >= 0.0 && freeGB < minFreeGB) {
|
||
std::cerr << "[build-survey-all] 磁盘守护触发: 剩余 " << freeGB << " GB < "
|
||
<< minFreeGB << " GB,停止,未建 " << prefix << " 及其后。\n";
|
||
stoppedByDisk = true;
|
||
break;
|
||
}
|
||
const std::string out = (fs::path(outBase) / prefix).string();
|
||
LineBuildResult r;
|
||
r.prefix = prefix;
|
||
const std::string gps = findGpsForPrefix(lineDir, prefix);
|
||
if (gps.empty()) {
|
||
r.ok = false;
|
||
r.reason = "缺 .gps,跳过";
|
||
std::cerr << "[build-survey-all] " << prefix << " 缺 .gps,跳过\n";
|
||
results.push_back(r);
|
||
continue;
|
||
}
|
||
try {
|
||
r = buildOneSurveyLine(lineDir, prefix, gps, out, levels, coarse,
|
||
cellSizeM, radiusM);
|
||
} catch (const std::exception& e) {
|
||
r.ok = false;
|
||
r.reason = std::string("异常: ") + e.what();
|
||
std::cerr << "[build-survey-all] " << prefix << " 失败: " << e.what()
|
||
<< "(跳过,继续)\n";
|
||
}
|
||
results.push_back(r);
|
||
}
|
||
|
||
std::cout << "\n=== build-survey-all 汇总(测绘级 CGCS2000 世界对齐体) ===\n";
|
||
int okCount = 0;
|
||
std::int64_t totalBytes = 0;
|
||
for (const auto& r : results) {
|
||
if (r.ok) {
|
||
++okCount;
|
||
totalBytes += r.dataBytes;
|
||
std::cout << " [OK] " << r.prefix << " 维度(东×北×深)=" << r.nx << "x"
|
||
<< r.ny << "x" << r.nz << " data="
|
||
<< r.dataBytes / (1024.0 * 1024.0) << " MB\n";
|
||
} else {
|
||
std::cout << " [跳过] " << r.prefix << " 原因=" << r.reason << "\n";
|
||
}
|
||
}
|
||
std::cout << "成功 " << okCount << "/" << prefixes.size()
|
||
<< " 条,合计 data=" << totalBytes / (1024.0 * 1024.0 * 1024.0)
|
||
<< " GB,剩余磁盘 " << freeSpaceGB(outBase) << " GB\n";
|
||
if (stoppedByDisk)
|
||
std::cout << "注意: 因磁盘守护提前停止,部分测线未建。\n";
|
||
return 0;
|
||
}
|
||
|
||
int cmdLoad(int argc, char** argv) {
|
||
const Args a = parseArgs(argc, argv, 2);
|
||
if (a.positional.empty()) {
|
||
std::cerr << "用法: gpr_poc load <storeDir>\n";
|
||
return 2;
|
||
}
|
||
const std::string dir = a.positional[0];
|
||
std::cout << "[load] storeDir=" << dir << "\n";
|
||
|
||
Stopwatch sw;
|
||
geopro::render::WholeVolumeSource src(dir);
|
||
const double loadMs = sw.elapsedMs();
|
||
|
||
const auto& m = src.meta();
|
||
const std::int64_t voxels =
|
||
static_cast<std::int64_t>(m.nx) * m.ny * m.nz;
|
||
const std::int64_t wholeBytes = voxels * 2; // VTK_SHORT
|
||
const double peak = Probe::peakMemMB();
|
||
|
||
std::cout << "\n=== load 指标 ===\n";
|
||
std::cout << "加载耗时(ms) : " << loadMs << "\n";
|
||
std::cout << "整卷维度 : " << m.nx << " x " << m.ny << " x " << m.nz
|
||
<< "\n";
|
||
std::cout << "整卷字节(B) : " << wholeBytes << " ("
|
||
<< wholeBytes / (1024.0 * 1024.0) << " MB)\n";
|
||
std::cout << "峰值内存(MB) : " << peak << "\n";
|
||
|
||
writeMetricLine("load,dir=" + dir + ",nx=" + std::to_string(m.nx) +
|
||
",ny=" + std::to_string(m.ny) +
|
||
",nz=" + std::to_string(m.nz) +
|
||
",wholeB=" + std::to_string(wholeBytes) +
|
||
",loadMs=" + std::to_string(loadMs) +
|
||
",peakMB=" + std::to_string(peak));
|
||
return 0;
|
||
}
|
||
|
||
// ---- selftest:合成极小数据走完整 build→load 管线 ----
|
||
|
||
// 写一个极小通道的 .iprb + .iprh(samples 采样、traces 道,值 = base + t + s)。
|
||
void writeSyntheticChannel(const fs::path& iprbPath, int samples, int traces,
|
||
std::int16_t base) {
|
||
const fs::path iprhPath =
|
||
fs::path(iprbPath).replace_extension(".iprh");
|
||
std::ofstream h(iprhPath);
|
||
h << "SAMPLES: " << samples << "\n";
|
||
h << "LAST TRACE: " << (traces - 1) << "\n";
|
||
h << "CHANNELS: 2\n";
|
||
h << "TIMEWINDOW: 100.0\n";
|
||
h << "SOIL VELOCITY: 100.0\n"; // m/µs → ×1e6 → 1e8 m/s
|
||
h << "DISTANCE INTERVAL: 0.05\n";
|
||
h.close();
|
||
|
||
std::ofstream b(iprbPath, std::ios::binary);
|
||
// 布局 [trace*samples + s],s 最快。
|
||
for (int t = 0; t < traces; ++t) {
|
||
for (int s = 0; s < samples; ++s) {
|
||
const std::int16_t v =
|
||
static_cast<std::int16_t>(base + t + s);
|
||
b.write(reinterpret_cast<const char*>(&v), sizeof(v));
|
||
}
|
||
}
|
||
}
|
||
|
||
int cmdSelftest() {
|
||
std::cout << "[selftest] 构造极小合成 survey(2 通道)...\n";
|
||
const fs::path tmp =
|
||
fs::temp_directory_path() / "gpr_poc_selftest";
|
||
std::error_code ec;
|
||
fs::remove_all(tmp, ec);
|
||
fs::create_directories(tmp);
|
||
|
||
const int samples = 8;
|
||
const int traces = 12;
|
||
|
||
// 2 通道 .iprb/.iprh + .ord(末列==1 标记有效通道,第 2 列为横偏 Y)。
|
||
writeSyntheticChannel(tmp / "syn_001_A01.iprb", samples, traces,
|
||
/*base=*/100);
|
||
writeSyntheticChannel(tmp / "syn_001_A02.iprb", samples, traces,
|
||
/*base=*/200);
|
||
{
|
||
std::ofstream ord(tmp / "syn_001.ord");
|
||
ord << "0 0.000000 -1.5 1\n";
|
||
ord << "1 1.000000 -1.5 1\n";
|
||
}
|
||
|
||
const std::vector<std::string> iprb = {
|
||
(tmp / "syn_001_A01.iprb").string(),
|
||
(tmp / "syn_001_A02.iprb").string()};
|
||
const std::string ord = (tmp / "syn_001.ord").string();
|
||
|
||
bool ok = true;
|
||
auto check = [&](bool cond, const std::string& msg) {
|
||
if (!cond) {
|
||
std::cerr << "[selftest] FAIL: " << msg << "\n";
|
||
ok = false;
|
||
}
|
||
};
|
||
|
||
try {
|
||
// 装配
|
||
geopro::core::GprSurvey survey =
|
||
geopro::io::gpr::assembleGprSurvey(iprb, ord);
|
||
check(survey.ntraces == traces, "ntraces");
|
||
check(survey.samples == samples, "samples");
|
||
check(survey.channelY.size() == 2, "channels");
|
||
// channelY 升序:A01 偏移 0.0 在前,A02 偏移 1.0 在后。
|
||
check(survey.channelY.front() < survey.channelY.back(), "channelY 升序");
|
||
|
||
// 建体:cellXY 取通道间距 1.0 → ny=2;cellZ 较细确保 nz>1。
|
||
const double cellXY = 1.0;
|
||
const double cellZ = std::max(survey.dz, 1e-12);
|
||
const geopro::core::GridSpec spec =
|
||
specFromSurvey(survey, cellXY, cellZ);
|
||
std::cout << "[selftest] GridSpec " << spec.nx << "x" << spec.ny << "x"
|
||
<< spec.nz << " dz=" << spec.dz << "\n";
|
||
check(spec.ny == 2, "ny==2");
|
||
|
||
geopro::core::BuiltI16 built =
|
||
geopro::core::buildGprVolume(survey, spec);
|
||
check(built.vol.nx() == spec.nx, "built nx");
|
||
check(built.vol.ny() == spec.ny, "built ny");
|
||
check(built.vol.nz() == spec.nz, "built nz");
|
||
|
||
// 落盘 + 金字塔
|
||
const std::string store = (tmp / "store").string();
|
||
fs::create_directories(store);
|
||
geopro::data::ChunkedVolumeStore::write(store, built, /*brick=*/4);
|
||
{
|
||
geopro::data::ChunkedVolumeStore s(store);
|
||
s.buildPyramid(1);
|
||
check(s.levels() == 2, "金字塔层数==2");
|
||
}
|
||
|
||
// 加载整卷,校验维度一致
|
||
geopro::render::WholeVolumeSource src(store);
|
||
check(src.meta().nx == spec.nx, "load nx");
|
||
check(src.meta().ny == spec.ny, "load ny");
|
||
check(src.meta().nz == spec.nz, "load nz");
|
||
|
||
// 某体素值合理性:x0/y0 角点应有非 blank 量化值(落格命中首道首通道)。
|
||
const std::int16_t q = built.vol.at(0, 0, 0);
|
||
check(q != geopro::core::ScalarVolumeI16::kBlank, "(0,0,0) 非 blank");
|
||
} catch (const std::exception& e) {
|
||
std::cerr << "[selftest] 异常: " << e.what() << "\n";
|
||
ok = false;
|
||
}
|
||
|
||
fs::remove_all(tmp, ec);
|
||
std::cout << "[selftest] " << (ok ? "PASS" : "FAIL") << "\n";
|
||
return ok ? 0 : 1;
|
||
}
|
||
|
||
// ============================================================================
|
||
// 离屏 GPU 渲染基准(POC-B)
|
||
// ============================================================================
|
||
|
||
// 捕获 VTK 错误输出的 OutputWindow:用于侦测体绘制时 vtkVolumeTexture 报的
|
||
// "Invalid texture dimensions" / "MAX_3D_TEXTURE_SIZE" —— 一旦出现,说明整卷
|
||
// 单张 3D 纹理上传失败,体绘制 fps 无意义,必须如实标 INVALID(绝不当真上报)。
|
||
class CapturingOutputWindow : public vtkOutputWindow {
|
||
public:
|
||
static CapturingOutputWindow* New();
|
||
vtkTypeMacro(CapturingOutputWindow, vtkOutputWindow);
|
||
|
||
void DisplayText(const char* txt) override {
|
||
if (txt) {
|
||
const std::string s(txt);
|
||
captured_ += s;
|
||
if (s.find("texture dimensions") != std::string::npos ||
|
||
s.find("MAX_3D_TEXTURE_SIZE") != std::string::npos) {
|
||
textureError_ = true;
|
||
}
|
||
// 纹理【单元数】超限(multi-volume 一个包挂太多体)——与 3D 纹理【尺寸】超限区分,
|
||
// 供 view-all 自动减小每包体数(K)退避,直到本机硬件不再报错。
|
||
if (s.find("number of textures") != std::string::npos ||
|
||
s.find("Hardware does not support the number") != std::string::npos) {
|
||
textureCountError_ = true;
|
||
}
|
||
}
|
||
// 仍透传到 stderr,便于人工查看。
|
||
if (txt) std::cerr << txt;
|
||
}
|
||
|
||
bool textureError() const { return textureError_; }
|
||
bool textureCountError() const { return textureCountError_; }
|
||
void resetTextureCountError() { textureCountError_ = false; }
|
||
const std::string& captured() const { return captured_; }
|
||
|
||
private:
|
||
std::string captured_;
|
||
bool textureError_ = false;
|
||
bool textureCountError_ = false;
|
||
};
|
||
vtkStandardNewMacro(CapturingOutputWindow);
|
||
|
||
// 创建一个离屏 vtkRenderWindow(VTK9.6:SetShowWindow(false)+OffScreenRenderingOn)。
|
||
vtkSmartPointer<vtkRenderWindow> makeOffscreenWindow(int w, int h) {
|
||
auto rw = vtkSmartPointer<vtkRenderWindow>::New();
|
||
rw->SetOffScreenRendering(1);
|
||
rw->SetShowWindow(false);
|
||
rw->SetSize(w, h);
|
||
return rw;
|
||
}
|
||
|
||
// 闸门:最小离屏渲染冒烟。返回 0=OK,非 0=离屏 GL 起不来(BLOCKED_OFFSCREEN)。
|
||
// 流程:离屏窗口 → 加一个 cube actor → Render() → 读回像素,确认非全黑/读得到。
|
||
int cmdOffscreenSmoke() {
|
||
std::cout << "[offscreen-smoke] 创建离屏 vtkRenderWindow...\n";
|
||
try {
|
||
auto rw = makeOffscreenWindow(256, 256);
|
||
|
||
vtkNew<vtkRenderer> ren;
|
||
ren->SetBackground(0.1, 0.1, 0.2);
|
||
rw->AddRenderer(ren);
|
||
|
||
vtkNew<vtkCubeSource> cube;
|
||
cube->SetXLength(1.0);
|
||
cube->SetYLength(1.0);
|
||
cube->SetZLength(1.0);
|
||
|
||
vtkNew<vtkPolyDataMapper> mapper;
|
||
mapper->SetInputConnection(cube->GetOutputPort());
|
||
|
||
vtkNew<vtkActor> actor;
|
||
actor->SetMapper(mapper);
|
||
actor->GetProperty()->SetColor(1.0, 0.6, 0.2);
|
||
ren->AddActor(actor);
|
||
ren->ResetCamera();
|
||
|
||
// Render():若 GL 上下文创建失败,VTK 会输出错误(多数返回,少数抛)。
|
||
rw->Render();
|
||
|
||
// 读回像素验证:取整窗 RGB,确认能读到且非全 0。
|
||
const int* sz = rw->GetSize();
|
||
const int w = sz[0], h = sz[1];
|
||
if (w <= 0 || h <= 0) {
|
||
std::cout << "[offscreen-smoke] FAIL: 窗口尺寸为 0(上下文未建立)\n";
|
||
std::cout << "STATUS=BLOCKED_OFFSCREEN\n";
|
||
return 1;
|
||
}
|
||
|
||
auto pixels = vtkSmartPointer<vtkUnsignedCharArray>::New();
|
||
// GetRGBACharPixelData(x0,y0,x1,y1,front,arr):front=1 读前缓冲。
|
||
const int ok =
|
||
rw->GetRGBACharPixelData(0, 0, w - 1, h - 1, /*front=*/1, pixels);
|
||
if (ok == 0 || pixels->GetNumberOfTuples() == 0) {
|
||
std::cout << "[offscreen-smoke] FAIL: 读不到像素\n";
|
||
std::cout << "STATUS=BLOCKED_OFFSCREEN\n";
|
||
return 1;
|
||
}
|
||
|
||
// 统计非背景像素(cube 应渲出橙色,存在像素 R 通道明显高于背景)。
|
||
vtkIdType nonBlack = 0;
|
||
const vtkIdType n = pixels->GetNumberOfTuples();
|
||
for (vtkIdType i = 0; i < n; ++i) {
|
||
const double r = pixels->GetComponent(i, 0);
|
||
const double g = pixels->GetComponent(i, 1);
|
||
const double b = pixels->GetComponent(i, 2);
|
||
if (r > 80 || g > 80 || b > 80) ++nonBlack;
|
||
}
|
||
|
||
const char* caps = rw->ReportCapabilities();
|
||
std::cout << "[offscreen-smoke] 读回像素 " << n << " 个,非背景像素 "
|
||
<< nonBlack << "\n";
|
||
std::cout << "[offscreen-smoke] GL 能力:\n"
|
||
<< (caps ? caps : "(无)") << "\n";
|
||
|
||
if (nonBlack == 0) {
|
||
std::cout << "[offscreen-smoke] FAIL: 渲染结果全为背景(actor 未画出)\n";
|
||
std::cout << "STATUS=BLOCKED_OFFSCREEN\n";
|
||
return 1;
|
||
}
|
||
|
||
std::cout << "[offscreen-smoke] OK:离屏 GL 可用,可继续真实基准。\n";
|
||
std::cout << "STATUS=OK\n";
|
||
return 0;
|
||
} catch (const std::exception& e) {
|
||
std::cout << "[offscreen-smoke] FAIL: 异常 " << e.what() << "\n";
|
||
std::cout << "STATUS=BLOCKED_OFFSCREEN\n";
|
||
return 1;
|
||
}
|
||
}
|
||
|
||
// 体绘制 fps:每帧绕 azimuth 旋相机再 Render(),避免被驱动优化成空渲染。
|
||
double benchVolumeFps(vtkRenderWindow* rw, vtkRenderer* ren, int frames) {
|
||
ren->ResetCamera();
|
||
vtkCamera* cam = ren->GetActiveCamera();
|
||
rw->Render(); // 预热一帧(首帧含上传显存/编译 shader,不计时)
|
||
Stopwatch sw;
|
||
for (int f = 0; f < frames; ++f) {
|
||
cam->Azimuth(360.0 / frames); // 每帧转一点,扫满一圈
|
||
rw->Render();
|
||
}
|
||
const double ms = sw.elapsedMs();
|
||
return ms > 0.0 ? frames * 1000.0 / ms : 0.0;
|
||
}
|
||
|
||
// 切片扫描 fps:沿 K 轴(深度)逐偏移 reslice 取轴向切面 + 纹理渲染,每帧推进偏移。
|
||
double benchSliceFps(vtkRenderWindow* rw, vtkRenderer* ren,
|
||
vtkImageData* full, vtkLookupTable* lut, int frames) {
|
||
// reslice:固定轴向(XY 平面),沿 Z 改变 ResliceAxesOrigin 扫过整卷。
|
||
vtkNew<vtkImageReslice> reslice;
|
||
reslice->SetInputData(full);
|
||
reslice->SetOutputDimensionality(2);
|
||
reslice->SetInterpolationModeToLinear();
|
||
|
||
vtkNew<vtkImageMapToColors> colorize;
|
||
colorize->SetLookupTable(lut);
|
||
colorize->SetInputConnection(reslice->GetOutputPort());
|
||
|
||
vtkNew<vtkImageActor> imgActor;
|
||
imgActor->GetMapper()->SetInputConnection(colorize->GetOutputPort());
|
||
ren->AddViewProp(imgActor);
|
||
ren->ResetCamera();
|
||
|
||
double bounds[6];
|
||
full->GetBounds(bounds);
|
||
const double zMin = bounds[4], zMax = bounds[5];
|
||
const double ox = 0.5 * (bounds[0] + bounds[1]);
|
||
const double oy = 0.5 * (bounds[2] + bounds[3]);
|
||
|
||
rw->Render(); // 预热
|
||
Stopwatch sw;
|
||
for (int f = 0; f < frames; ++f) {
|
||
const double t = static_cast<double>(f) / std::max(1, frames - 1);
|
||
const double z = zMin + (zMax - zMin) * t;
|
||
reslice->SetResliceAxesOrigin(ox, oy, z);
|
||
reslice->Modified();
|
||
rw->Render();
|
||
}
|
||
const double ms = sw.elapsedMs();
|
||
return ms > 0.0 ? frames * 1000.0 / ms : 0.0;
|
||
}
|
||
|
||
// 由 ColorScale 物理区间建 256 级 VTK LUT(切片纹理着色用,与体绘制色阶同源)。
|
||
vtkSmartPointer<vtkLookupTable> makeLut(const geopro::core::ColorScale& cs,
|
||
double vmin, double vmax) {
|
||
auto lut = vtkSmartPointer<vtkLookupTable>::New();
|
||
const int n = 256;
|
||
lut->SetNumberOfTableValues(n);
|
||
lut->SetRange(vmin, vmax);
|
||
for (int i = 0; i < n; ++i) {
|
||
const double v = vmin + (vmax - vmin) * i / (n - 1);
|
||
const auto c = cs.colorAt(v);
|
||
lut->SetTableValue(i, c.r / 255.0, c.g / 255.0, c.b / 255.0, 1.0);
|
||
}
|
||
lut->Build();
|
||
return lut;
|
||
}
|
||
|
||
// 简单蓝-白-红色阶(与 test_color_scale 同款最简构造)。
|
||
geopro::core::ColorScale makeColorScale(double vmin, double vmax) {
|
||
geopro::core::ColorScale cs;
|
||
const double mid = 0.5 * (vmin + vmax);
|
||
cs.addStop(vmin, geopro::core::Rgba{0, 0, 255, 255});
|
||
cs.addStop(mid, geopro::core::Rgba{255, 255, 255, 255});
|
||
cs.addStop(vmax, geopro::core::Rgba{255, 0, 0, 255});
|
||
return cs;
|
||
}
|
||
|
||
// ============================================================================
|
||
// 视觉调优共享构件(Task 12d ①)
|
||
// ============================================================================
|
||
//
|
||
// 结构化配色:地震/雷达体常用的「结构色阶」——深蓝(强负)→青→白(零)→黄→红(强正),
|
||
// 比单纯蓝-白-红更易拉开正负反射层次。值域用数据 vmin/vmax,无需手调控制点。
|
||
geopro::core::ColorScale makeStructuralColorScale(double vmin, double vmax) {
|
||
geopro::core::ColorScale cs;
|
||
const double span = (vmax > vmin) ? (vmax - vmin) : 1.0;
|
||
auto at = [&](double t) { return vmin + span * t; };
|
||
cs.addStop(at(0.00), geopro::core::Rgba{0, 0, 140, 255}); // 深蓝
|
||
cs.addStop(at(0.25), geopro::core::Rgba{0, 160, 220, 255}); // 青
|
||
cs.addStop(at(0.50), geopro::core::Rgba{245, 245, 245, 255}); // 白(零附近)
|
||
cs.addStop(at(0.75), geopro::core::Rgba{250, 190, 30, 255}); // 黄
|
||
cs.addStop(at(1.00), geopro::core::Rgba{170, 0, 0, 255}); // 暗红
|
||
return cs;
|
||
}
|
||
|
||
// 地震高对比色阶(seismic 红-白-蓝):两端饱和亮色(强正=亮红、强负=亮蓝),
|
||
// 零附近白。比 structural 更亮、对比更狠,正负反射一眼分开。
|
||
// 前置声明(实现在 polish 段):梯度幅值分位统计,供 C4 画廊的梯度不透明度标定。
|
||
struct GradStats {
|
||
double median = 0, p75 = 0, p90 = 0, p99 = 0, mx = 0;
|
||
std::size_t samples = 0;
|
||
};
|
||
GradStats sampleGradientMagnitude(vtkImageData* img);
|
||
|
||
// 标量分位标定(P3 修可见性核心):扫该体实际体素值分布,取 2%/98% 分位作色阶/
|
||
// 不透明度的物理端点,裁掉离群。处理后体值多集中在 ±窄带、少量离群到 ±9000,若按
|
||
// 全量化域(meta.vminPhys/vmaxPhys=±9249)映射 → 窄带信号落近透明区 → 整体近黑。
|
||
// 返回物理单位的 {lo, hi}(已按 quant.toPhys 反算)。前置声明,实现在 polish 段。
|
||
struct ScalarPercentiles {
|
||
double lo = 0.0, hi = 0.0; // 物理单位(2% / 98% 分位)
|
||
std::size_t samples = 0;
|
||
};
|
||
ScalarPercentiles sampleScalarPercentiles(vtkImageData* img,
|
||
const geopro::core::Quant& q,
|
||
double pLo, double pHi);
|
||
|
||
geopro::core::ColorScale makeSeismicColorScale(double vmin, double vmax) {
|
||
geopro::core::ColorScale cs;
|
||
const double span = (vmax > vmin) ? (vmax - vmin) : 1.0;
|
||
auto at = [&](double t) { return vmin + span * t; };
|
||
cs.addStop(at(0.00), geopro::core::Rgba{30, 60, 255, 255}); // 亮蓝(强负)
|
||
cs.addStop(at(0.30), geopro::core::Rgba{120, 180, 255, 255}); // 浅蓝
|
||
cs.addStop(at(0.50), geopro::core::Rgba{255, 255, 255, 255}); // 白(零)
|
||
cs.addStop(at(0.70), geopro::core::Rgba{255, 170, 120, 255}); // 浅橙
|
||
cs.addStop(at(1.00), geopro::core::Rgba{255, 40, 30, 255}); // 亮红(强正)
|
||
return cs;
|
||
}
|
||
|
||
// jet 类高饱和色阶(蓝-青-绿-黄-红):全程高亮高饱和,最大化色彩动态范围,
|
||
// 弱信号也能映到鲜明色相,适合「一眼铺满层次」的取向。
|
||
geopro::core::ColorScale makeJetColorScale(double vmin, double vmax) {
|
||
geopro::core::ColorScale cs;
|
||
const double span = (vmax > vmin) ? (vmax - vmin) : 1.0;
|
||
auto at = [&](double t) { return vmin + span * t; };
|
||
cs.addStop(at(0.00), geopro::core::Rgba{0, 0, 200, 255}); // 蓝
|
||
cs.addStop(at(0.25), geopro::core::Rgba{0, 200, 255, 255}); // 青
|
||
cs.addStop(at(0.50), geopro::core::Rgba{0, 230, 60, 255}); // 绿
|
||
cs.addStop(at(0.75), geopro::core::Rgba{255, 230, 0, 255}); // 黄
|
||
cs.addStop(at(1.00), geopro::core::Rgba{255, 30, 0, 255}); // 红
|
||
return cs;
|
||
}
|
||
|
||
// 调亮版 seismic:与 makeSeismicColorScale 同红-白-蓝走向,但把蓝端提亮、整体抬白,
|
||
// 弱信号也落在更亮的色域(强负不再是深蓝、零附近纯白、强正亮红),整体更醒目不发暗。
|
||
geopro::core::ColorScale makeBrightSeismicColorScale(double vmin, double vmax) {
|
||
geopro::core::ColorScale cs;
|
||
const double span = (vmax > vmin) ? (vmax - vmin) : 1.0;
|
||
auto at = [&](double t) { return vmin + span * t; };
|
||
cs.addStop(at(0.00), geopro::core::Rgba{70, 130, 255, 255}); // 亮蓝(强负)
|
||
cs.addStop(at(0.30), geopro::core::Rgba{170, 210, 255, 255}); // 浅亮蓝
|
||
cs.addStop(at(0.50), geopro::core::Rgba{255, 255, 255, 255}); // 纯白(零)
|
||
cs.addStop(at(0.70), geopro::core::Rgba{255, 200, 150, 255}); // 亮浅橙
|
||
cs.addStop(at(1.00), geopro::core::Rgba{255, 80, 60, 255}); // 亮红(强正)
|
||
return cs;
|
||
}
|
||
|
||
// 增强灰度:黑→白单调,但中段抬亮、两端拉满,弱反射也落在中亮灰,层界面/竖纹对比醒目。
|
||
// GPR 内部水平层叠/基底反射用灰度往往最干净直读(不被多色相干扰)。
|
||
geopro::core::ColorScale makeGrayEnhancedColorScale(double vmin, double vmax) {
|
||
geopro::core::ColorScale cs;
|
||
const double span = (vmax > vmin) ? (vmax - vmin) : 1.0;
|
||
auto at = [&](double t) { return vmin + span * t; };
|
||
cs.addStop(at(0.00), geopro::core::Rgba{20, 25, 40, 255}); // 强负:近黑带冷调
|
||
cs.addStop(at(0.30), geopro::core::Rgba{120, 125, 135, 255}); // 中负:中灰(抬亮)
|
||
cs.addStop(at(0.50), geopro::core::Rgba{180, 182, 188, 255}); // 零附近:亮灰
|
||
cs.addStop(at(0.70), geopro::core::Rgba{225, 222, 210, 255}); // 中正:暖亮灰
|
||
cs.addStop(at(1.00), geopro::core::Rgba{255, 252, 240, 255}); // 强正:近白
|
||
return cs;
|
||
}
|
||
|
||
// 背景压制强度(--bgSuppress,0..1):0=原观感(近零背景压低但可见);越大→近零背景越
|
||
// 透明 + 中心透明死区越宽,只留中/强反射层 → "压背景、突出反射"(业界标准传函做法)。
|
||
// 注意:压太狠会连带抹掉弱异常(与 ESS 阈值同取舍);本系统弱异常靠切片抓,三维体压背景风险低。
|
||
double gBgSuppress = 0.0;
|
||
|
||
// 「实体感」不透明度包络(Task 12d gallery):与 structural 双端斜坡不同,这里让
|
||
// 中高值段普遍可见——背景(近零)仍压低但不归零,中高段从 floorOpacity 平滑升到
|
||
// maxOpacity,使体读起来像半透明实心块、内部层次(而非只剩两端薄壳)可见。
|
||
// floorOpacity:近零背景的最低不透明度(0.05~0.12,压住但不消失)
|
||
// maxOpacity :强反射端的不透明度峰值(0.85 时近实心)
|
||
// midOpacity :中值段(半幅处)的不透明度(0.3~0.5,决定「半透明实心」观感)
|
||
// gBgSuppress :见上,压低 floor + 加宽中心透明死区。
|
||
vtkSmartPointer<vtkVolumeProperty> makeSolidVolumeProperty(
|
||
const geopro::core::Quant& q, const geopro::core::ColorScale& cs,
|
||
double vminPhys, double vmaxPhys, double floorOpacity, double midOpacity,
|
||
double maxOpacity) {
|
||
constexpr int kTransferSamples = 64;
|
||
if (vminPhys >= vmaxPhys) vmaxPhys = vminPhys + 1.0;
|
||
const double qminD = static_cast<double>(q.toQ(vminPhys));
|
||
const double qmaxD = static_cast<double>(q.toQ(vmaxPhys));
|
||
|
||
vtkNew<vtkColorTransferFunction> color;
|
||
for (int t = 0; t < kTransferSamples; ++t) {
|
||
const double qd = qminD + (qmaxD - qminD) * t / (kTransferSamples - 1);
|
||
const auto qvLevel = static_cast<std::int16_t>(std::lround(qd));
|
||
const double phys = q.toPhys(qvLevel);
|
||
const auto c = cs.colorAt(phys);
|
||
color->AddRGBPoint(qd, c.r / 255.0, c.g / 255.0, c.b / 255.0);
|
||
}
|
||
|
||
// 不透明度:V 形(中段=零附近背景=floor,正负两端=max),但全程 ≥floor 且中值
|
||
// 段≈mid → 整体半透明实心、内部层次可见,而非两端薄壳。
|
||
// --bgSuppress F:F>0 时把近零背景 floor 压到 floor*(1-F),并在中心开 ±(F*0.45*half) 的
|
||
// 全透明死区 → 压背景、突出中/强反射(F=0 即原观感)。
|
||
const double bg = std::clamp(gBgSuppress, 0.0, 1.0);
|
||
const double cFloor = floorOpacity * (1.0 - bg); // 背景压低(F→1 趋 0)
|
||
const double dead = bg * 0.45; // 中心透明死区半宽(占 half 的比例)
|
||
vtkNew<vtkPiecewiseFunction> opacity;
|
||
opacity->AddPoint(
|
||
static_cast<double>(geopro::core::ScalarVolumeI16::kBlank), 0.0);
|
||
const double qmid = 0.5 * (qminD + qmaxD);
|
||
const double half = 0.5 * (qmaxD - qminD);
|
||
opacity->AddPoint(qminD, maxOpacity); // 强负反射:近实心
|
||
opacity->AddPoint(qmid - 0.55 * half, midOpacity); // 中负段:半透明实心
|
||
if (dead > 0.0) {
|
||
opacity->AddPoint(qmid - dead * half, 0.0); // 死区左沿:透明
|
||
opacity->AddPoint(qmid, 0.0); // 中心背景:透明
|
||
opacity->AddPoint(qmid + dead * half, 0.0); // 死区右沿:透明
|
||
} else {
|
||
opacity->AddPoint(qmid, cFloor); // 近零背景:压低但可见
|
||
}
|
||
opacity->AddPoint(qmid + 0.55 * half, midOpacity); // 中正段:半透明实心
|
||
opacity->AddPoint(qmaxD, maxOpacity); // 强正反射:近实心
|
||
|
||
auto prop = vtkSmartPointer<vtkVolumeProperty>::New();
|
||
prop->SetColor(color);
|
||
prop->SetScalarOpacity(opacity);
|
||
prop->SetInterpolationTypeToLinear();
|
||
prop->ShadeOff();
|
||
return prop;
|
||
}
|
||
|
||
// 参数化量化域传函:与 makeI16VolumeProperty 同逻辑,但 kMaxOpacity 可由 --opacity 控。
|
||
// 不透明度调高时光线提前终止,fps 近乎中性甚至更快(探针认知,报告打印前后对照证实)。
|
||
vtkSmartPointer<vtkVolumeProperty> makeTunedVolumeProperty(
|
||
const geopro::core::Quant& q, const geopro::core::ColorScale& cs,
|
||
double vminPhys, double vmaxPhys, double maxOpacity,
|
||
bool structuralOpacity = true) {
|
||
constexpr int kTransferSamples = 64;
|
||
if (vminPhys >= vmaxPhys) vmaxPhys = vminPhys + 1.0;
|
||
const double qminD = static_cast<double>(q.toQ(vminPhys));
|
||
const double qmaxD = static_cast<double>(q.toQ(vmaxPhys));
|
||
|
||
vtkNew<vtkColorTransferFunction> color;
|
||
for (int t = 0; t < kTransferSamples; ++t) {
|
||
const double qd = qminD + (qmaxD - qminD) * t / (kTransferSamples - 1);
|
||
const auto qvLevel = static_cast<std::int16_t>(std::lround(qd));
|
||
const double phys = q.toPhys(qvLevel);
|
||
const auto c = cs.colorAt(phys);
|
||
color->AddRGBPoint(qd, c.r / 255.0, c.g / 255.0, c.b / 255.0);
|
||
}
|
||
|
||
// 不透明度:
|
||
// - 原始(structuralOpacity=false):线性单斜坡 [qmin,qmax]→[0,maxOpacity],
|
||
// 与 VoxelActor 默认一致,作调优前对照基线。
|
||
// - 调优(structuralOpacity=true):双端斜坡。GPR/地震体值多集中在零附近(背景),
|
||
// 强反射在正负两端;线性单斜坡会让占多数的近零背景填满体、遮住结构。改为
|
||
// 「中段(零附近)透明 + 正负两端不透明」——抑制背景、凸显强反射层,截面结构才看得出。
|
||
vtkNew<vtkPiecewiseFunction> opacity;
|
||
opacity->AddPoint(
|
||
static_cast<double>(geopro::core::ScalarVolumeI16::kBlank), 0.0);
|
||
if (structuralOpacity) {
|
||
const double qmid = 0.5 * (qminD + qmaxD);
|
||
const double half = 0.5 * (qmaxD - qminD);
|
||
opacity->AddPoint(qminD, maxOpacity); // 强负反射:不透明
|
||
opacity->AddPoint(qmid - 0.30 * half, 0.0); // 近零背景:透明
|
||
opacity->AddPoint(qmid + 0.30 * half, 0.0);
|
||
opacity->AddPoint(qmaxD, maxOpacity); // 强正反射:不透明
|
||
} else {
|
||
opacity->AddPoint(qminD, 0.0);
|
||
opacity->AddPoint(qmaxD, maxOpacity);
|
||
}
|
||
|
||
auto prop = vtkSmartPointer<vtkVolumeProperty>::New();
|
||
prop->SetColor(color);
|
||
prop->SetScalarOpacity(opacity);
|
||
prop->SetInterpolationTypeToLinear();
|
||
prop->ShadeOff();
|
||
return prop;
|
||
}
|
||
|
||
// 由预构建 VTK_SHORT 图像建一个「视觉调优」体:自定义不透明度 + 垂向夸张。
|
||
// 垂向夸张用 vtkVolume::SetScale(1, exagg, exagg) 缩放跨通道(Y)与深度(Z)两薄轴,
|
||
// 不改图像数据;体物理极扁(X≈2.2km vs Y≈1.5m/Z≈8m),放大薄轴截面结构才看得出。
|
||
vtkSmartPointer<vtkVolume> buildTunedVolume(vtkImageData* shortImg,
|
||
const geopro::core::Quant& q,
|
||
const geopro::core::ColorScale& cs,
|
||
double vminPhys, double vmaxPhys,
|
||
double maxOpacity, double exagg,
|
||
bool structuralOpacity = true) {
|
||
vtkNew<vtkSmartVolumeMapper> mapper;
|
||
mapper->SetInputData(shortImg);
|
||
mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
|
||
mapper->SetAutoAdjustSampleDistances(0);
|
||
mapper->SetInteractiveAdjustSampleDistances(0);
|
||
|
||
auto prop = makeTunedVolumeProperty(q, cs, vminPhys, vmaxPhys, maxOpacity,
|
||
structuralOpacity);
|
||
|
||
auto volume = vtkSmartPointer<vtkVolume>::New();
|
||
volume->SetMapper(mapper);
|
||
volume->SetProperty(prop);
|
||
volume->SetScale(1.0, exagg, exagg); // 垂向夸张:放大 Y/Z 薄轴
|
||
return volume;
|
||
}
|
||
|
||
int cmdRenderB(int argc, char** argv) {
|
||
const Args a = parseArgs(argc, argv, 2);
|
||
if (a.positional.empty()) {
|
||
std::cerr << "用法: gpr_poc renderB <storeDir> [--frames 120]\n";
|
||
return 2;
|
||
}
|
||
const std::string dir = a.positional[0];
|
||
const int frames = std::stoi(a.get("frames", "120"));
|
||
std::cout << "[renderB] storeDir=" << dir << " frames=" << frames << "\n";
|
||
|
||
// 闸门复检:renderB 前先确认离屏可用(避免在不可渲染机上跑出假数据)。
|
||
std::cout << "[renderB] 离屏闸门复检...\n";
|
||
if (cmdOffscreenSmoke() != 0) {
|
||
std::cout << "[renderB] 闸门失败,中止,不产出 fps。\n";
|
||
return 1;
|
||
}
|
||
|
||
// 1) 加载整卷(VTK_SHORT)。
|
||
Stopwatch swLoad;
|
||
geopro::render::WholeVolumeSource src(dir);
|
||
const double loadMs = swLoad.elapsedMs();
|
||
const auto& m = src.meta();
|
||
|
||
const std::int64_t voxels =
|
||
static_cast<std::int64_t>(m.nx) * m.ny * m.nz;
|
||
const std::int64_t wholeBytes = voxels * 2; // VTK_SHORT
|
||
std::cout << "[renderB] 整卷 " << m.nx << "x" << m.ny << "x" << m.nz
|
||
<< " 体素=" << voxels << " 字节=" << wholeBytes << " ("
|
||
<< wholeBytes / (1024.0 * 1024.0) << " MB),加载 " << loadMs
|
||
<< "ms\n";
|
||
|
||
auto images = src.currentImages();
|
||
if (images.empty() || !images.front()) {
|
||
std::cerr << "[renderB] 错误: currentImages 为空\n";
|
||
return 1;
|
||
}
|
||
vtkImageData* shortImg = images.front().Get();
|
||
|
||
// 色阶用 meta 的物理区间。
|
||
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
|
||
const geopro::core::ColorScale cs = makeColorScale(vmin, vmax);
|
||
|
||
// 2) 体绘制(离屏)。
|
||
auto rw = makeOffscreenWindow(1024, 768);
|
||
vtkNew<vtkRenderer> ren;
|
||
ren->SetBackground(0.0, 0.0, 0.0);
|
||
rw->AddRenderer(ren);
|
||
|
||
vtkSmartPointer<vtkVolume> volume =
|
||
geopro::render::buildVoxelI16FromImage(shortImg, m.quant, cs, vmin, vmax);
|
||
ren->AddVolume(volume);
|
||
|
||
// 装上捕获式 OutputWindow:拦截体绘制时的 3D 纹理维度错误。
|
||
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
|
||
vtkOutputWindow::SetInstance(capWin);
|
||
|
||
std::cout << "[renderB] 体绘制基准(" << frames << " 帧旋转相机)...\n";
|
||
const double volFpsRaw = benchVolumeFps(rw, ren, frames);
|
||
|
||
const bool textureErr = capWin->textureError();
|
||
vtkOutputWindow::SetInstance(nullptr); // 还原默认输出窗口
|
||
|
||
// 进显存判据:SmartVolumeMapper 实际用的渲染模式(2=GPURenderMode)。
|
||
int renderMode = -1;
|
||
bool lowResResample = false;
|
||
if (auto* svm =
|
||
vtkSmartVolumeMapper::SafeDownCast(volume->GetMapper())) {
|
||
renderMode = svm->GetLastUsedRenderMode();
|
||
// 大体可能触发降质重采样(GPU 显存不足时 SmartVolumeMapper 走低分辨率)。
|
||
lowResResample = (svm->GetInteractiveAdjustSampleDistances() == 0 &&
|
||
renderMode != vtkSmartVolumeMapper::GPURenderMode);
|
||
}
|
||
const bool onGpu = (renderMode == vtkSmartVolumeMapper::GPURenderMode);
|
||
|
||
// 任一维度超过 GL_MAX_3D_TEXTURE_SIZE(本机实测 16384)→ 整卷无法成单张 3D 纹理。
|
||
constexpr int kMax3DTexObserved = 16384;
|
||
const bool dimOversize =
|
||
(m.nx > kMax3DTexObserved || m.ny > kMax3DTexObserved ||
|
||
m.nz > kMax3DTexObserved);
|
||
// 体绘制 fps 是否可信:上传成功(无纹理错误且未超限)才算真实整卷体绘制帧率。
|
||
const bool volFpsValid = !textureErr && !dimOversize;
|
||
const double volFps = volFpsValid ? volFpsRaw : -1.0;
|
||
|
||
std::cout << "[renderB] 体绘制 raw_fps=" << volFpsRaw
|
||
<< " 渲染模式=" << renderMode << (onGpu ? "(GPU)" : "(非GPU)")
|
||
<< " 纹理维度错误=" << (textureErr ? "是" : "否")
|
||
<< " 超 16384=" << (dimOversize ? "是" : "否") << "\n";
|
||
if (!volFpsValid) {
|
||
std::cout << "[renderB] 警告: 整卷未能成单张 3D 纹理(X=" << m.nx
|
||
<< " > " << kMax3DTexObserved
|
||
<< "),体绘制 fps 无意义 → 标 INVALID。\n";
|
||
}
|
||
|
||
// 3) 切片扫描(离屏,沿 Z 扫整卷)。
|
||
vtkNew<vtkRenderer> ren2;
|
||
ren2->SetBackground(0.0, 0.0, 0.0);
|
||
auto rw2 = makeOffscreenWindow(1024, 768);
|
||
rw2->AddRenderer(ren2);
|
||
vtkSmartPointer<vtkLookupTable> lut = makeLut(cs, vmin, vmax);
|
||
|
||
std::cout << "[renderB] 切片扫描基准(" << frames << " 帧沿 Z 推进)...\n";
|
||
const double sliceFps =
|
||
benchSliceFps(rw2, ren2, src.sliceSource(), lut, frames);
|
||
std::cout << "[renderB] 切片 fps=" << sliceFps << "\n";
|
||
|
||
const double peak = Probe::peakMemMB();
|
||
const std::string vram = "N/A"; // VTK 安装未带 GLEW 头,无法直查 NVX 显存
|
||
|
||
// 4) 汇总打印。
|
||
const std::string volFpsStr =
|
||
volFpsValid ? std::to_string(volFps) : "INVALID(整卷超 3D 纹理上限)";
|
||
|
||
std::cout << "\n=== renderB GPU 指标 ===\n";
|
||
std::cout << "离屏闸门 : OK\n";
|
||
std::cout << "体维度 : " << m.nx << " x " << m.ny << " x " << m.nz
|
||
<< "\n";
|
||
std::cout << "体素数 : " << voxels << "\n";
|
||
std::cout << "整卷字节(B) : " << wholeBytes << " ("
|
||
<< wholeBytes / (1024.0 * 1024.0) << " MB)\n";
|
||
std::cout << "体绘制 fps : " << volFpsStr << "\n";
|
||
if (!volFpsValid) {
|
||
std::cout << " (raw_fps=" << volFpsRaw
|
||
<< " 为空纹理渲染,X=" << m.nx << " > 16384,不可信)\n";
|
||
}
|
||
std::cout << "切片扫描 fps : " << sliceFps << " (2D 纹理,无 3D 上限约束)\n";
|
||
std::cout << "渲染模式 : " << renderMode
|
||
<< (onGpu ? " (GPU 路径)" : " (非 GPU)") << "\n";
|
||
std::cout << "整卷进显存 : "
|
||
<< (volFpsValid && onGpu ? "是(单张 3D 纹理)"
|
||
: "否(超 GL_MAX_3D_TEXTURE_SIZE 16384)")
|
||
<< "\n";
|
||
std::cout << "降质重采样 : " << (lowResResample ? "是" : "否") << "\n";
|
||
std::cout << "GPU 显存 : " << vram << "\n";
|
||
std::cout << "进程峰值内存(MB): " << peak << "\n";
|
||
|
||
writeMetricLine(
|
||
"renderB,dir=" + dir + ",nx=" + std::to_string(m.nx) +
|
||
",ny=" + std::to_string(m.ny) + ",nz=" + std::to_string(m.nz) +
|
||
",voxels=" + std::to_string(voxels) +
|
||
",wholeB=" + std::to_string(wholeBytes) +
|
||
",volFps=" + volFpsStr +
|
||
",volFpsRaw=" + std::to_string(volFpsRaw) +
|
||
",volFpsValid=" + std::to_string(volFpsValid ? 1 : 0) +
|
||
",sliceFps=" + std::to_string(sliceFps) +
|
||
",renderMode=" + std::to_string(renderMode) +
|
||
",onGpu=" + std::to_string(onGpu ? 1 : 0) +
|
||
",loadMs=" + std::to_string(loadMs) +
|
||
",peakMB=" + std::to_string(peak));
|
||
return 0;
|
||
}
|
||
|
||
// ============================================================================
|
||
// 核外分块体绘制基准(POC-C,命门探针)
|
||
// ============================================================================
|
||
|
||
// 量化域传函(与 VoxelActor::buildVoxelI16FromImage 同逻辑):颜色对每量化级 qv 用
|
||
// q.toPhys(qv) 反查 ColorScale;不透明度 kBlank→0、[qmin,qmax] 线性到 kMaxOpacity。
|
||
// MultiBlock 全块共用同一 vtkVolumeProperty(挂在单个 vtkVolume 上)。
|
||
vtkSmartPointer<vtkVolumeProperty> makeI16VolumeProperty(
|
||
const geopro::core::Quant& q, const geopro::core::ColorScale& cs,
|
||
double vminPhys, double vmaxPhys) {
|
||
constexpr int kTransferSamples = 64;
|
||
constexpr double kMaxOpacity = 0.15;
|
||
if (vminPhys >= vmaxPhys) vmaxPhys = vminPhys + 1.0;
|
||
|
||
const double qminD = static_cast<double>(q.toQ(vminPhys));
|
||
const double qmaxD = static_cast<double>(q.toQ(vmaxPhys));
|
||
|
||
vtkNew<vtkColorTransferFunction> color;
|
||
for (int t = 0; t < kTransferSamples; ++t) {
|
||
const double qd = qminD + (qmaxD - qminD) * t / (kTransferSamples - 1);
|
||
const auto qvLevel = static_cast<std::int16_t>(std::lround(qd));
|
||
const double phys = q.toPhys(qvLevel);
|
||
const auto c = cs.colorAt(phys);
|
||
color->AddRGBPoint(qd, c.r / 255.0, c.g / 255.0, c.b / 255.0);
|
||
}
|
||
|
||
vtkNew<vtkPiecewiseFunction> opacity;
|
||
opacity->AddPoint(
|
||
static_cast<double>(geopro::core::ScalarVolumeI16::kBlank), 0.0);
|
||
opacity->AddPoint(qminD, 0.0);
|
||
opacity->AddPoint(qmaxD, kMaxOpacity);
|
||
|
||
auto prop = vtkSmartPointer<vtkVolumeProperty>::New();
|
||
prop->SetColor(color);
|
||
prop->SetScalarOpacity(opacity);
|
||
prop->SetInterpolationTypeToLinear();
|
||
prop->ShadeOff();
|
||
return prop;
|
||
}
|
||
|
||
// 由当前工作集图像组装 vtkMultiBlockDataSet(每块一个 vtkImageData)。
|
||
vtkSmartPointer<vtkMultiBlockDataSet> makeMultiBlock(
|
||
const std::vector<vtkSmartPointer<vtkImageData>>& imgs) {
|
||
auto mb = vtkSmartPointer<vtkMultiBlockDataSet>::New();
|
||
mb->SetNumberOfBlocks(static_cast<unsigned int>(imgs.size()));
|
||
for (unsigned int i = 0; i < imgs.size(); ++i) {
|
||
mb->SetBlock(i, imgs[i].Get());
|
||
}
|
||
return mb;
|
||
}
|
||
|
||
int cmdRenderC(int argc, char** argv) {
|
||
const Args a = parseArgs(argc, argv, 2);
|
||
if (a.positional.empty()) {
|
||
std::cerr << "用法: gpr_poc renderC <storeDir> [--budget 64] [--frames 120]\n";
|
||
return 2;
|
||
}
|
||
const std::string dir = a.positional[0];
|
||
const std::size_t budget =
|
||
static_cast<std::size_t>(std::stoul(a.get("budget", "64")));
|
||
const int frames = std::stoi(a.get("frames", "120"));
|
||
std::cout << "[renderC] storeDir=" << dir << " budget=" << budget
|
||
<< " frames=" << frames << "\n";
|
||
|
||
// 闸门复检:不可渲染机不产假 fps。
|
||
std::cout << "[renderC] 离屏闸门复检...\n";
|
||
if (cmdOffscreenSmoke() != 0) {
|
||
std::cout << "[renderC] 闸门失败,中止,不产出 fps。\n";
|
||
return 1;
|
||
}
|
||
|
||
// 1) 核外源(读 meta + 建 pager,不载整卷)。
|
||
Stopwatch swLoad;
|
||
geopro::render::OutOfCoreSource src(dir, budget);
|
||
const double loadMs = swLoad.elapsedMs();
|
||
const auto& m = src.meta();
|
||
const std::int64_t voxels =
|
||
static_cast<std::int64_t>(m.nx) * m.ny * m.nz;
|
||
|
||
const int winW = 1024, winH = 768;
|
||
src.setAspect(static_cast<double>(winW) / winH);
|
||
|
||
std::cout << "[renderC] 体 " << m.nx << "x" << m.ny << "x" << m.nz
|
||
<< " 体素=" << voxels << " (整卷 X=" << m.nx
|
||
<< " > 16384 → renderB INVALID),源构造 " << loadMs << "ms\n";
|
||
|
||
// 色阶用 meta 物理区间。
|
||
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
|
||
const geopro::core::ColorScale cs = makeColorScale(vmin, vmax);
|
||
vtkSmartPointer<vtkVolumeProperty> prop =
|
||
makeI16VolumeProperty(m.quant, cs, vmin, vmax);
|
||
|
||
// 2) 离屏 + MultiBlock 体绘制。
|
||
auto rw = makeOffscreenWindow(winW, winH);
|
||
vtkNew<vtkRenderer> ren;
|
||
ren->SetBackground(0.0, 0.0, 0.0);
|
||
rw->AddRenderer(ren);
|
||
|
||
vtkNew<vtkMultiBlockVolumeMapper> mapper;
|
||
mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
|
||
|
||
auto volume = vtkSmartPointer<vtkVolume>::New();
|
||
volume->SetMapper(mapper);
|
||
volume->SetProperty(prop);
|
||
ren->AddVolume(volume);
|
||
|
||
// 装捕获式 OutputWindow:拦截每块上传时的 3D 纹理维度错误(应无,因块 ≤64³)。
|
||
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
|
||
vtkOutputWindow::SetInstance(capWin);
|
||
|
||
// 相机:先以全体定向(看整卷),首帧 update 选出工作集后再 ResetCamera 到
|
||
// 实际驻留块的 mapper 包围盒(budget<视野总块时工作集只覆盖体的一部分,框住它
|
||
// 才能确证核外体绘制真渲出;这是 budget 受限下的诚实测法,报告说明)。
|
||
ren->ResetCamera(m.origin[0], m.origin[0] + m.nx * m.spacing[0],
|
||
m.origin[1], m.origin[1] + m.ny * m.spacing[1],
|
||
m.origin[2], m.origin[2] + m.nz * m.spacing[2]);
|
||
vtkCamera* cam = ren->GetActiveCamera();
|
||
|
||
auto refreshBlocks = [&]() {
|
||
src.update(cam);
|
||
auto imgs = src.currentImages();
|
||
auto mb = makeMultiBlock(imgs);
|
||
mapper->SetInputDataObject(mb);
|
||
mapper->Update();
|
||
return imgs.size();
|
||
};
|
||
|
||
const std::size_t warmBlocks = refreshBlocks();
|
||
// 用工作集(mapper)实际包围盒重置相机,框住驻留块。
|
||
{
|
||
double b[6];
|
||
mapper->GetBounds(b);
|
||
if (b[0] <= b[1]) {
|
||
ren->ResetCamera(b);
|
||
} else {
|
||
ren->ResetCamera();
|
||
}
|
||
}
|
||
rw->Render(); // 预热(上传显存 + 编译 shader,不计时)
|
||
|
||
{
|
||
double b[6];
|
||
mapper->GetBounds(b);
|
||
std::cout << "[renderC] 工作集包围盒 x[" << b[0] << "," << b[1] << "] y["
|
||
<< b[2] << "," << b[3] << "] z[" << b[4] << "," << b[5] << "]\n";
|
||
}
|
||
|
||
std::cout << "[renderC] 预热:level=" << src.lastLevel()
|
||
<< " 视野块=" << src.lastVisibleCount() << "/"
|
||
<< src.lastLevelBrickTotal()
|
||
<< " 驻留=" << src.residentCount() << " 渲染块=" << warmBlocks
|
||
<< "\n";
|
||
|
||
std::size_t maxResident = src.residentCount();
|
||
std::size_t sumBlocks = 0;
|
||
|
||
// 3a) 静态工作集体绘制 fps:工作集固定(不每帧换块),只旋相机 + Render。
|
||
// 隔离"纯 GPU MultiBlock 体绘制"成本(剔除分块换页/解压/重建 mapper 开销),
|
||
// 直接对照 renderB 整卷 fps,回答未知 #6(真实体绘制 fps)。
|
||
std::cout << "[renderC] 静态工作集体绘制基准(" << frames << " 帧旋相机)...\n";
|
||
Stopwatch swStatic;
|
||
for (int f = 0; f < frames; ++f) {
|
||
cam->Azimuth(360.0 / frames);
|
||
rw->Render(); // 工作集不变,仅旋转
|
||
}
|
||
const double staticMs = swStatic.elapsedMs();
|
||
const double staticFps = staticMs > 0 ? frames * 1000.0 / staticMs : 0.0;
|
||
std::cout << "[renderC] 静态工作集 fps=" << staticFps << "\n";
|
||
|
||
// 3b) 动态换页体绘制 fps:每帧 update(cam)(重选 LOD/视野块,含 qUncompress 解压
|
||
// 换入的块 + 重建 MultiBlock)+ Render。回答未知 #4(热路径解压是否拖垮 fps)
|
||
// 与 #5(内存恒定)。同时累计 update 耗时占比。
|
||
std::cout << "[renderC] 动态换页体绘制基准(" << frames << " 帧旋相机)...\n";
|
||
double updateMsTotal = 0.0;
|
||
Stopwatch swDyn;
|
||
for (int f = 0; f < frames; ++f) {
|
||
cam->Azimuth(360.0 / frames);
|
||
Stopwatch swU;
|
||
const std::size_t blocks = refreshBlocks(); // update + 重建 MultiBlock
|
||
updateMsTotal += swU.elapsedMs();
|
||
sumBlocks += blocks;
|
||
maxResident = std::max(maxResident, src.residentCount());
|
||
rw->Render();
|
||
}
|
||
const double dynMs = swDyn.elapsedMs();
|
||
const double dynFps = dynMs > 0 ? frames * 1000.0 / dynMs : 0.0;
|
||
const double rawFps = dynFps; // 主报告口径:含换页的真实交互 fps
|
||
std::cout << "[renderC] 动态换页 fps=" << dynFps
|
||
<< " (其中 update/换页/重建 平均 " << (updateMsTotal / frames)
|
||
<< " ms/帧)\n";
|
||
|
||
const bool textureErr = capWin->textureError();
|
||
vtkOutputWindow::SetInstance(nullptr);
|
||
|
||
// 4) 正确性判据:渲出非空像素(非全背景)。
|
||
auto pixels = vtkSmartPointer<vtkUnsignedCharArray>::New();
|
||
rw->GetRGBACharPixelData(0, 0, winW - 1, winH - 1, /*front=*/1, pixels);
|
||
vtkIdType nonBlack = 0;
|
||
const vtkIdType npx = pixels->GetNumberOfTuples();
|
||
for (vtkIdType i = 0; i < npx; ++i) {
|
||
if (pixels->GetComponent(i, 0) > 10 || pixels->GetComponent(i, 1) > 10 ||
|
||
pixels->GetComponent(i, 2) > 10) {
|
||
++nonBlack;
|
||
}
|
||
}
|
||
const bool renderedNonEmpty = (nonBlack > 0);
|
||
|
||
// 渲染模式(MultiBlock 内部每块一个 SmartVolumeMapper;此处取一块代表性查询)。
|
||
// MultiBlock 不直接暴露 LastUsedRenderMode,故以纹理无错 + 非空像素为体绘制真出证据。
|
||
const bool volFpsValid = !textureErr && renderedNonEmpty;
|
||
|
||
const double peak = Probe::peakMemMB();
|
||
const double avgBlocks =
|
||
frames > 0 ? static_cast<double>(sumBlocks) / frames : 0.0;
|
||
|
||
std::cout << "\n=== renderC 核外体绘制指标 ===\n";
|
||
std::cout << "离屏闸门 : OK\n";
|
||
std::cout << "体维度 : " << m.nx << " x " << m.ny << " x " << m.nz
|
||
<< " (整卷 X 超 16384,renderB=INVALID)\n";
|
||
std::cout << "体素数 : " << voxels << "\n";
|
||
std::cout << "budget(块) : " << budget << "\n";
|
||
std::cout << "峰值驻留(块) : " << maxResident
|
||
<< (maxResident <= budget ? " (≤budget,内存恒定 OK)"
|
||
: " (!! 超 budget)")
|
||
<< "\n";
|
||
std::cout << "末帧 level : " << src.lastLevel() << "\n";
|
||
std::cout << "末帧视野块/总块 : " << src.lastVisibleCount() << " / "
|
||
<< src.lastLevelBrickTotal() << "\n";
|
||
std::cout << "平均渲染块/帧 : " << avgBlocks << "\n";
|
||
std::cout << "纹理维度错误 : " << (textureErr ? "是(!!)" : "否") << "\n";
|
||
std::cout << "渲出非空像素 : " << (renderedNonEmpty ? "是" : "否(!!)")
|
||
<< " (非背景像素=" << nonBlack << ")\n";
|
||
std::cout << "静态工作集 fps : "
|
||
<< (volFpsValid ? std::to_string(staticFps)
|
||
: std::string("INVALID(纹理错或空渲染)"))
|
||
<< " (纯 GPU MultiBlock 体绘制)\n";
|
||
std::cout << "动态换页 fps : "
|
||
<< (volFpsValid ? std::to_string(dynFps)
|
||
: std::string("INVALID(纹理错或空渲染)"))
|
||
<< " (含每帧 update/解压/重建 mapper)\n";
|
||
std::cout << " 换页均耗时/帧 : " << (updateMsTotal / frames) << " ms\n";
|
||
std::cout << "进程峰值内存(MB) : " << peak << "\n";
|
||
std::cout << "源构造耗时(ms) : " << loadMs << "\n";
|
||
std::cout << "对照 renderB : 整卷 INVALID(超 3D 纹理上限);renderC "
|
||
<< (volFpsValid ? "真渲出 ✔" : "未渲出 ✘") << "\n";
|
||
|
||
writeMetricLine(
|
||
"renderC,dir=" + dir + ",nx=" + std::to_string(m.nx) +
|
||
",ny=" + std::to_string(m.ny) + ",nz=" + std::to_string(m.nz) +
|
||
",voxels=" + std::to_string(voxels) +
|
||
",budget=" + std::to_string(budget) +
|
||
",maxResident=" + std::to_string(maxResident) +
|
||
",lastLevel=" + std::to_string(src.lastLevel()) +
|
||
",lastVisible=" + std::to_string(src.lastVisibleCount()) +
|
||
",lastLevelTotal=" + std::to_string(src.lastLevelBrickTotal()) +
|
||
",avgBlocks=" + std::to_string(avgBlocks) +
|
||
",textureErr=" + std::to_string(textureErr ? 1 : 0) +
|
||
",nonBlack=" + std::to_string(nonBlack) +
|
||
",volFpsValid=" + std::to_string(volFpsValid ? 1 : 0) +
|
||
",staticFps=" + (volFpsValid ? std::to_string(staticFps) : "INVALID") +
|
||
",dynFps=" + (volFpsValid ? std::to_string(dynFps) : "INVALID") +
|
||
",updateMsPerFrame=" + std::to_string(updateMsTotal / frames) +
|
||
",rawFps=" + std::to_string(rawFps) +
|
||
",loadMs=" + std::to_string(loadMs) +
|
||
",peakMB=" + std::to_string(peak));
|
||
return volFpsValid ? 0 : 1;
|
||
}
|
||
|
||
// ============================================================================
|
||
// 单 mapper SetPartitions 整卷体绘制基准(POC-C-partitioned,去风险探针)
|
||
// ============================================================================
|
||
//
|
||
// 验"对的架构":整卷喂【单个】vtkGPUVolumeRayCastMapper(其 OpenGL 实现 =
|
||
// vtkOpenGLGPUVolumeRayCastMapper),用 SetPartitions(ceil(nx/16384),...) 让同一
|
||
// mapper 内部把体沿轴分区上传(每区 ≤16384 绕过 GL_MAX_3D_TEXTURE_SIZE),一次
|
||
// ray cast。对照 9c 整卷单 SmartVolumeMapper(INVALID,纹理墙) 与 12 MultiBlock
|
||
// (每块一 mapper,9.5 静态/1.45 换页)。
|
||
//
|
||
// 双闸(同 9c,绝不把空纹理假帧率当性能):
|
||
// ① CapturingOutputWindow 捕获 3D 纹理维度错误;
|
||
// ② 真实回读像素,统计非背景像素 → 非空才算真渲出。
|
||
int cmdRenderCPartitioned(int argc, char** argv) {
|
||
const Args a = parseArgs(argc, argv, 2);
|
||
if (a.positional.empty()) {
|
||
std::cerr
|
||
<< "用法: gpr_poc renderC-partitioned <storeDir> [--frames 120]\n";
|
||
return 2;
|
||
}
|
||
const std::string dir = a.positional[0];
|
||
const int frames = std::stoi(a.get("frames", "120"));
|
||
std::cout << "[renderC-partitioned] storeDir=" << dir << " frames=" << frames
|
||
<< "\n";
|
||
|
||
// 闸门复检:不可渲染机不产假 fps。
|
||
std::cout << "[renderC-partitioned] 离屏闸门复检...\n";
|
||
if (cmdOffscreenSmoke() != 0) {
|
||
std::cout << "[renderC-partitioned] 闸门失败,中止,不产出 fps。\n";
|
||
return 1;
|
||
}
|
||
|
||
// 1) WholeVolumeSource 重组整卷 VTK_SHORT image(常驻内存,约 400MB)。
|
||
Stopwatch swLoad;
|
||
geopro::render::WholeVolumeSource src(dir);
|
||
const double loadMs = swLoad.elapsedMs();
|
||
const auto& m = src.meta();
|
||
|
||
const std::int64_t voxels =
|
||
static_cast<std::int64_t>(m.nx) * m.ny * m.nz;
|
||
const std::int64_t wholeBytes = voxels * 2; // VTK_SHORT
|
||
std::cout << "[renderC-partitioned] 整卷 " << m.nx << "x" << m.ny << "x"
|
||
<< m.nz << " 体素=" << voxels << " 字节=" << wholeBytes << " ("
|
||
<< wholeBytes / (1024.0 * 1024.0) << " MB),加载 " << loadMs
|
||
<< "ms\n";
|
||
|
||
auto images = src.currentImages();
|
||
if (images.empty() || !images.front()) {
|
||
std::cerr << "[renderC-partitioned] 错误: currentImages 为空\n";
|
||
return 1;
|
||
}
|
||
vtkImageData* shortImg = images.front().Get();
|
||
|
||
// 2) 分区数:任一轴 > 16384 → ceil(dim/16384) 个分区,其余轴 1。
|
||
constexpr int kMax3DTex = 16384;
|
||
auto partCount = [](int dim) {
|
||
return static_cast<unsigned short>((dim + kMax3DTex - 1) / kMax3DTex);
|
||
};
|
||
const unsigned short px = partCount(m.nx);
|
||
const unsigned short py = partCount(m.ny);
|
||
const unsigned short pz = partCount(m.nz);
|
||
std::cout << "[renderC-partitioned] SetPartitions(" << px << "," << py << ","
|
||
<< pz << ") 每区上限 ≤" << kMax3DTex << " (沿线 " << m.nx << "/"
|
||
<< px << "=" << (m.nx + px - 1) / px << ")\n";
|
||
|
||
// 3) 量化域传函(复用现有 makeI16VolumeProperty:qmin/qmax + kBlank 透明)。
|
||
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
|
||
const geopro::core::ColorScale cs = makeColorScale(vmin, vmax);
|
||
vtkSmartPointer<vtkVolumeProperty> prop =
|
||
makeI16VolumeProperty(m.quant, cs, vmin, vmax);
|
||
|
||
// 4) 离屏 + 单个 GPU ray cast mapper + SetPartitions。
|
||
const int winW = 1024, winH = 768;
|
||
auto rw = makeOffscreenWindow(winW, winH);
|
||
vtkNew<vtkRenderer> ren;
|
||
ren->SetBackground(0.0, 0.0, 0.0);
|
||
rw->AddRenderer(ren);
|
||
|
||
// vtkGPUVolumeRayCastMapper 抽象基类无 SetPartitions(在 OpenGL 实现上);
|
||
// 直接建 OpenGL 具体类(工厂默认产物同此),喂【整卷单 image】不预切块。
|
||
vtkNew<vtkOpenGLGPUVolumeRayCastMapper> mapper;
|
||
mapper->SetInputData(shortImg);
|
||
mapper->SetPartitions(px, py, pz);
|
||
|
||
auto volume = vtkSmartPointer<vtkVolume>::New();
|
||
volume->SetMapper(mapper);
|
||
volume->SetProperty(prop);
|
||
ren->AddVolume(volume);
|
||
|
||
// 装捕获式 OutputWindow:拦截分区上传时的 3D 纹理维度错误。
|
||
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
|
||
vtkOutputWindow::SetInstance(capWin);
|
||
|
||
// 相机:用 mapper 实际包围盒定向(整卷,非工作集);体极扁长(44476:29:162),
|
||
// ResetCamera 全体后再倾斜抬高视角,让薄维度可见(否则边缘视角近乎不可见)。
|
||
{
|
||
double b[6];
|
||
mapper->GetBounds(b);
|
||
if (b[0] <= b[1]) {
|
||
ren->ResetCamera(b);
|
||
} else {
|
||
ren->ResetCamera();
|
||
}
|
||
}
|
||
vtkCamera* cam = ren->GetActiveCamera();
|
||
cam->Elevation(30.0); // 抬高,避免纯边缘视角看不到薄板
|
||
cam->Azimuth(30.0);
|
||
ren->ResetCameraClippingRange();
|
||
|
||
// 每帧旋相机 + Render 测 fps;同时多帧采样非背景像素取最大值
|
||
// (区分"真渲不出"与"末帧恰好边缘视角空"——后者只是采样时机)。
|
||
auto countNonBlack = [&]() -> vtkIdType {
|
||
auto px = vtkSmartPointer<vtkUnsignedCharArray>::New();
|
||
rw->GetRGBACharPixelData(0, 0, winW - 1, winH - 1, /*front=*/1, px);
|
||
vtkIdType nb = 0;
|
||
const vtkIdType np = px->GetNumberOfTuples();
|
||
for (vtkIdType i = 0; i < np; ++i) {
|
||
if (px->GetComponent(i, 0) > 10 || px->GetComponent(i, 1) > 10 ||
|
||
px->GetComponent(i, 2) > 10) {
|
||
++nb;
|
||
}
|
||
}
|
||
return nb;
|
||
};
|
||
|
||
std::cout << "[renderC-partitioned] 单 mapper 整卷体绘制基准(" << frames
|
||
<< " 帧旋相机)...\n";
|
||
rw->Render(); // 预热(分区上传 + 编译 shader,不计时)
|
||
vtkIdType maxNonBlack = countNonBlack();
|
||
const int sampleEvery = std::max(1, frames / 8);
|
||
Stopwatch swBench;
|
||
for (int f = 0; f < frames; ++f) {
|
||
cam->Azimuth(360.0 / frames);
|
||
rw->Render();
|
||
if (f % sampleEvery == 0) {
|
||
maxNonBlack = std::max(maxNonBlack, countNonBlack());
|
||
}
|
||
}
|
||
const double benchMs = swBench.elapsedMs();
|
||
const double volFpsRaw =
|
||
benchMs > 0.0 ? frames * 1000.0 / benchMs : 0.0;
|
||
|
||
const bool textureErr = capWin->textureError();
|
||
vtkOutputWindow::SetInstance(nullptr);
|
||
|
||
// 5) 正确性判据:整个旋转扫描中的最大非背景像素(非空才算真渲出)。
|
||
const vtkIdType nonBlack = maxNonBlack;
|
||
const bool renderedNonEmpty = (nonBlack > 0);
|
||
|
||
// 双闸:无纹理错 + 非空像素 → fps 可信。
|
||
const bool volFpsValid = !textureErr && renderedNonEmpty;
|
||
const double volFps = volFpsValid ? volFpsRaw : -1.0;
|
||
const double peak = Probe::peakMemMB();
|
||
const bool interactive = volFpsValid && volFps >= 15.0;
|
||
|
||
const std::string volFpsStr =
|
||
volFpsValid ? std::to_string(volFps)
|
||
: std::string("INVALID(纹理错或空渲染)");
|
||
|
||
std::cout << "\n=== renderC-partitioned 单 mapper SetPartitions 指标 ===\n";
|
||
std::cout << "离屏闸门 : OK\n";
|
||
std::cout << "体维度 : " << m.nx << " x " << m.ny << " x " << m.nz
|
||
<< "\n";
|
||
std::cout << "体素数 : " << voxels << "\n";
|
||
std::cout << "整卷字节(B) : " << wholeBytes << " ("
|
||
<< wholeBytes / (1024.0 * 1024.0) << " MB)\n";
|
||
std::cout << "分区数(px,py,pz) : " << px << "," << py << "," << pz << "\n";
|
||
std::cout << "纹理维度错误 : " << (textureErr ? "是(!!)" : "否") << "\n";
|
||
std::cout << "渲出非空像素 : " << (renderedNonEmpty ? "是" : "否(!!)")
|
||
<< " (非背景像素=" << nonBlack << ")\n";
|
||
std::cout << "体绘制 fps : " << volFpsStr << "\n";
|
||
if (!volFpsValid) {
|
||
std::cout << " (raw_fps=" << volFpsRaw << " 不可信)\n";
|
||
}
|
||
std::cout << "达交互级(≥15fps) : "
|
||
<< (interactive ? "是 ✔" : "否 ✘") << "\n";
|
||
std::cout << "进程峰值内存(MB) : " << peak << "\n";
|
||
std::cout << "源构造耗时(ms) : " << loadMs << "\n";
|
||
std::cout << "对照 renderB : 整卷单 SmartVolumeMapper=INVALID(纹理墙);"
|
||
"renderC MultiBlock=9.5 静态/1.45 换页;本探针="
|
||
<< (volFpsValid ? volFpsStr + "fps" : "INVALID") << "\n";
|
||
|
||
writeMetricLine(
|
||
"renderC-partitioned,dir=" + dir + ",nx=" + std::to_string(m.nx) +
|
||
",ny=" + std::to_string(m.ny) + ",nz=" + std::to_string(m.nz) +
|
||
",voxels=" + std::to_string(voxels) +
|
||
",wholeB=" + std::to_string(wholeBytes) +
|
||
",px=" + std::to_string(px) + ",py=" + std::to_string(py) +
|
||
",pz=" + std::to_string(pz) +
|
||
",textureErr=" + std::to_string(textureErr ? 1 : 0) +
|
||
",nonBlack=" + std::to_string(nonBlack) +
|
||
",volFpsValid=" + std::to_string(volFpsValid ? 1 : 0) +
|
||
",volFps=" + volFpsStr + ",volFpsRaw=" + std::to_string(volFpsRaw) +
|
||
",interactive=" + std::to_string(interactive ? 1 : 0) +
|
||
",loadMs=" + std::to_string(loadMs) + ",peakMB=" + std::to_string(peak));
|
||
|
||
// 写报告文件(覆盖式,含对照表)。
|
||
{
|
||
const fs::path repo =
|
||
fs::path("docs") / "superpowers" / "plans" / "poc-results-C.md";
|
||
fs::create_directories(repo.parent_path());
|
||
std::ofstream rf(repo.string());
|
||
if (rf) {
|
||
rf << "# POC-C 单 mapper SetPartitions 整卷体绘制探针结果\n\n";
|
||
rf << "## 体\n";
|
||
rf << "- 维度: " << m.nx << " x " << m.ny << " x " << m.nz << " (体素 "
|
||
<< voxels << ")\n";
|
||
rf << "- 整卷字节: " << wholeBytes << " B ("
|
||
<< wholeBytes / (1024.0 * 1024.0) << " MB, VTK_SHORT)\n";
|
||
rf << "- store: " << dir << "\n\n";
|
||
rf << "## 单 mapper SetPartitions\n";
|
||
rf << "- mapper: vtkOpenGLGPUVolumeRayCastMapper (整卷单 image,不预切块)\n";
|
||
rf << "- 分区数: SetPartitions(" << px << ", " << py << ", " << pz
|
||
<< ") 每区上限 ≤" << kMax3DTex << "\n";
|
||
rf << "- 纹理维度错误: " << (textureErr ? "是" : "否") << "\n";
|
||
rf << "- 渲出非空像素: " << (renderedNonEmpty ? "是" : "否") << " (非背景像素 "
|
||
<< nonBlack << ")\n";
|
||
rf << "- 体绘制 fps: " << volFpsStr << "\n";
|
||
rf << "- 达交互级(≥15fps): " << (interactive ? "是" : "否") << "\n";
|
||
rf << "- 进程峰值内存: " << peak << " MB\n";
|
||
rf << "- 源构造耗时: " << loadMs << " ms\n\n";
|
||
rf << "## 对照表\n\n";
|
||
rf << "| 路径 | 是否渲出 | fps |\n";
|
||
rf << "|---|---|---|\n";
|
||
rf << "| renderB 整卷单 SmartVolumeMapper | INVALID(纹理墙) | — |\n";
|
||
rf << "| renderC MultiBlock(每块一 mapper) | 渲出 | 9.5 静态/1.45 换页 |\n";
|
||
rf << "| renderC-partitioned 单 mapper SetPartitions | "
|
||
<< (volFpsValid ? "渲出" : "未渲出") << " | "
|
||
<< (volFpsValid ? volFpsStr : std::string("INVALID")) << " |\n\n";
|
||
rf << "## 判据结论\n";
|
||
if (volFpsValid && interactive) {
|
||
rf << "单 mapper SetPartitions 整卷体绘制【真渲出且达交互级】(" << volFps
|
||
<< " fps ≥15)。C production 路线钉死可行。\n";
|
||
} else if (volFpsValid) {
|
||
rf << "单 mapper SetPartitions 整卷体绘制【真渲出但未达交互级】(" << volFps
|
||
<< " fps <15)。VTK 这条路天花板暴露,需评估 OpenVDS/自建 GL。\n";
|
||
} else {
|
||
rf << "单 mapper SetPartitions 整卷体绘制【未真渲出】(纹理错="
|
||
<< (textureErr ? "是" : "否") << ",非空像素="
|
||
<< (renderedNonEmpty ? "是" : "否")
|
||
<< ")。SetPartitions 未能绕过纹理墙,如实记录。\n";
|
||
}
|
||
std::cout << "[renderC-partitioned] 报告写入 " << repo.string() << "\n";
|
||
}
|
||
}
|
||
|
||
return volFpsValid ? 0 : 1;
|
||
}
|
||
|
||
// ============================================================================
|
||
// LOD-fps 探针(POC-C 最后一根链子,Task 12c)
|
||
// ============================================================================
|
||
//
|
||
// 12b 已证整卷全分辨率 ray cast(2.08 亿体素)~10fps 是硬上限,fps 杠杆只有 LOD
|
||
// (渲更少体素)。本探针在【真实金字塔 store】上验四件事,全离屏、双闸防假帧率:
|
||
// (a) 粗层概览 fps:level2 整卷(单轴 <16384 → 单 SmartVolumeMapper)。
|
||
// (b) 全分辨率局部 fps:level0 一段 brick 列(沿线局部)。
|
||
// (c) LOD 切换动态过渡:相机从远观(level2)逐步拉近到近观局部(level0),跨越
|
||
// LOD 切换那一下逐帧记帧耗时,标切换帧尖峰/stall。
|
||
// (d) 截图:lod-overview.png / lod-fullres-local.png / lod-transition-mid.png。
|
||
//
|
||
// 双闸(同 9c,绝不把空纹理假帧率当性能):
|
||
// ① CapturingOutputWindow 捕获 3D 纹理维度错误;
|
||
// ② 真实回读像素,统计非背景像素 → 非空才算真渲出。
|
||
|
||
// 把金字塔某 level 重组成整卷 VTK_SHORT vtkImageData(逻辑同 WholeVolumeSource,
|
||
// 但按 level 维度 + spacing×2^level,使物理范围与 level0 一致)。
|
||
vtkSmartPointer<vtkImageData> buildLevelImage(
|
||
const geopro::data::ChunkedVolumeStore& store, int level,
|
||
const geopro::data::StoreMeta& m) {
|
||
int nx = 0, ny = 0, nz = 0;
|
||
store.dims(level, nx, ny, nz);
|
||
const int brick = m.brick;
|
||
const double sc = static_cast<double>(1 << level); // 2^level
|
||
|
||
auto img = vtkSmartPointer<vtkImageData>::New();
|
||
img->SetDimensions(nx, ny, nz);
|
||
img->SetOrigin(m.origin[0], m.origin[1], m.origin[2]);
|
||
img->SetSpacing(m.spacing[0] * sc, m.spacing[1] * sc, m.spacing[2] * sc);
|
||
|
||
vtkNew<vtkShortArray> arr;
|
||
arr->SetName("v");
|
||
arr->SetNumberOfTuples(static_cast<vtkIdType>(nx) * ny * nz);
|
||
|
||
for (int bz = 0; bz < store.bricksZ(level); ++bz) {
|
||
for (int by = 0; by < store.bricksY(level); ++by) {
|
||
for (int bx = 0; bx < store.bricksX(level); ++bx) {
|
||
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 = (nx - i0 < brick) ? (nx - i0) : brick;
|
||
const int bh = (ny - j0 < brick) ? (ny - j0) : brick;
|
||
const int bd = (nz - k0 < brick) ? (nz - k0) : brick;
|
||
std::size_t w = 0;
|
||
for (int kk = 0; kk < bd; ++kk) {
|
||
const vtkIdType gk = static_cast<vtkIdType>(k0 + kk);
|
||
for (int jj = 0; jj < bh; ++jj) {
|
||
const vtkIdType gj = static_cast<vtkIdType>(j0 + jj);
|
||
vtkIdType id = (gk * ny + gj) * nx + i0;
|
||
for (int ii = 0; ii < bw; ++ii) arr->SetValue(id++, raw[w++]);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
img->GetPointData()->SetScalars(arr);
|
||
return img;
|
||
}
|
||
|
||
// 取 level0 一段 brick 列 [bx0, bx0+bxCount) × 全 Y × 全 Z 重组成局部整卷
|
||
// VTK_SHORT image(X 维 = bxCount*brick ≤ 几百,远 <16384,单 3D 纹理)。
|
||
// Origin 沿 X 偏移到该段起点,spacing 用 level0 原值。
|
||
vtkSmartPointer<vtkImageData> buildLocalLevel0Image(
|
||
const geopro::data::ChunkedVolumeStore& store,
|
||
const geopro::data::StoreMeta& m, int bx0, int bxCount) {
|
||
const int brick = m.brick;
|
||
const int nx0 = m.nx, ny0 = m.ny, nz0 = m.nz;
|
||
const int totBx = store.bricksX(0);
|
||
bx0 = std::max(0, std::min(bx0, totBx - 1));
|
||
bxCount = std::max(1, std::min(bxCount, totBx - bx0));
|
||
|
||
const int i0Global = bx0 * brick;
|
||
const int localNx = std::min(bxCount * brick, nx0 - i0Global);
|
||
|
||
auto img = vtkSmartPointer<vtkImageData>::New();
|
||
img->SetDimensions(localNx, ny0, nz0);
|
||
img->SetOrigin(m.origin[0] + i0Global * m.spacing[0], m.origin[1],
|
||
m.origin[2]);
|
||
img->SetSpacing(m.spacing[0], m.spacing[1], m.spacing[2]);
|
||
|
||
vtkNew<vtkShortArray> arr;
|
||
arr->SetName("v");
|
||
arr->SetNumberOfTuples(static_cast<vtkIdType>(localNx) * ny0 * nz0);
|
||
|
||
for (int bz = 0; bz < store.bricksZ(0); ++bz) {
|
||
for (int by = 0; by < store.bricksY(0); ++by) {
|
||
for (int bx = bx0; bx < bx0 + bxCount; ++bx) {
|
||
const std::vector<std::int16_t> raw = store.readBrick(0, bx, by, bz);
|
||
const int gi0 = bx * brick, j0 = by * brick, k0 = bz * brick;
|
||
const int li0 = gi0 - i0Global; // 局部 X 起点
|
||
const int bw = (nx0 - gi0 < brick) ? (nx0 - gi0) : brick;
|
||
const int bh = (ny0 - j0 < brick) ? (ny0 - j0) : brick;
|
||
const int bd = (nz0 - k0 < brick) ? (nz0 - k0) : brick;
|
||
std::size_t w = 0;
|
||
for (int kk = 0; kk < bd; ++kk) {
|
||
const vtkIdType gk = static_cast<vtkIdType>(k0 + kk);
|
||
for (int jj = 0; jj < bh; ++jj) {
|
||
const vtkIdType gj = static_cast<vtkIdType>(j0 + jj);
|
||
vtkIdType id = (gk * ny0 + gj) * localNx + li0;
|
||
for (int ii = 0; ii < bw; ++ii) arr->SetValue(id++, raw[w++]);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
img->GetPointData()->SetScalars(arr);
|
||
return img;
|
||
}
|
||
|
||
// 统计当前窗口前缓冲非背景像素(>10 任一通道)。
|
||
vtkIdType countNonBlackPixels(vtkRenderWindow* rw, int w, int h) {
|
||
auto px = vtkSmartPointer<vtkUnsignedCharArray>::New();
|
||
rw->GetRGBACharPixelData(0, 0, w - 1, h - 1, /*front=*/1, px);
|
||
vtkIdType nb = 0;
|
||
const vtkIdType np = px->GetNumberOfTuples();
|
||
for (vtkIdType i = 0; i < np; ++i) {
|
||
if (px->GetComponent(i, 0) > 10 || px->GetComponent(i, 1) > 10 ||
|
||
px->GetComponent(i, 2) > 10) {
|
||
++nb;
|
||
}
|
||
}
|
||
return nb;
|
||
}
|
||
|
||
// 离屏窗口截图 → PNG。
|
||
void savePng(vtkRenderWindow* rw, const std::string& path) {
|
||
rw->Render();
|
||
vtkNew<vtkWindowToImageFilter> w2i;
|
||
w2i->SetInput(rw);
|
||
w2i->SetInputBufferTypeToRGB();
|
||
w2i->ReadFrontBufferOff();
|
||
w2i->Update();
|
||
vtkNew<vtkPNGWriter> writer;
|
||
writer->SetFileName(path.c_str());
|
||
writer->SetInputConnection(w2i->GetOutputPort());
|
||
writer->Write();
|
||
}
|
||
|
||
// 画面平均亮度(0~255):取前缓冲 RGB 求 luma 均值。Task 12d gallery 报告用,
|
||
// 量化「整体偏暗 vs 变亮」——背景占多数,故这是含背景的全屏均亮(横向对比有效)。
|
||
double meanBrightness(vtkRenderWindow* rw, int w, int h) {
|
||
auto px = vtkSmartPointer<vtkUnsignedCharArray>::New();
|
||
rw->GetRGBACharPixelData(0, 0, w - 1, h - 1, /*front=*/1, px);
|
||
const vtkIdType np = px->GetNumberOfTuples();
|
||
if (np == 0) return 0.0;
|
||
double sum = 0.0;
|
||
for (vtkIdType i = 0; i < np; ++i) {
|
||
const double r = px->GetComponent(i, 0);
|
||
const double g = px->GetComponent(i, 1);
|
||
const double b = px->GetComponent(i, 2);
|
||
sum += 0.299 * r + 0.587 * g + 0.114 * b;
|
||
}
|
||
return sum / static_cast<double>(np);
|
||
}
|
||
|
||
int cmdRenderLOD(int argc, char** argv) {
|
||
const Args a = parseArgs(argc, argv, 2);
|
||
if (a.positional.empty()) {
|
||
std::cerr << "用法: gpr_poc renderLOD <storeDir> [--frames 120]\n";
|
||
return 2;
|
||
}
|
||
const std::string dir = a.positional[0];
|
||
const int frames = std::stoi(a.get("frames", "120"));
|
||
std::cout << "[renderLOD] storeDir=" << dir << " frames=" << frames << "\n";
|
||
|
||
// 闸门复检:不可渲染机不产假 fps。
|
||
std::cout << "[renderLOD] 离屏闸门复检...\n";
|
||
if (cmdOffscreenSmoke() != 0) {
|
||
std::cout << "[renderLOD] 闸门失败,中止,不产出 fps。\n";
|
||
return 1;
|
||
}
|
||
|
||
geopro::data::ChunkedVolumeStore store(dir);
|
||
const geopro::data::StoreMeta& m = store.meta();
|
||
const int totLevels = store.levels();
|
||
std::cout << "[renderLOD] level0=" << m.nx << "x" << m.ny << "x" << m.nz
|
||
<< " 总层数=" << totLevels << "\n";
|
||
if (totLevels < 3) {
|
||
std::cout << "[renderLOD] 警告: 金字塔层数 <3(需 build --levels 3)。\n";
|
||
}
|
||
|
||
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
|
||
const geopro::core::ColorScale cs = makeColorScale(vmin, vmax);
|
||
|
||
const fs::path shotDir =
|
||
fs::path("docs") / "superpowers" / "plans" / "poc-lod-shots";
|
||
fs::create_directories(shotDir);
|
||
|
||
const int winW = 1024, winH = 768;
|
||
|
||
// 共用一个捕获式 OutputWindow,贯穿三段渲染。
|
||
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
|
||
vtkOutputWindow::SetInstance(capWin);
|
||
|
||
// ---- (a) 粗层概览 fps:level2 整卷 ----
|
||
const int ovLevel = std::min(2, totLevels - 1);
|
||
std::cout << "[renderLOD] (a) 建 level" << ovLevel << " 整卷 image...\n";
|
||
vtkSmartPointer<vtkImageData> ovImg = buildLevelImage(store, ovLevel, m);
|
||
int ovNx, ovNy, ovNz;
|
||
store.dims(ovLevel, ovNx, ovNy, ovNz);
|
||
|
||
auto rwOv = makeOffscreenWindow(winW, winH);
|
||
vtkNew<vtkRenderer> renOv;
|
||
renOv->SetBackground(0.0, 0.0, 0.0);
|
||
rwOv->AddRenderer(renOv);
|
||
vtkSmartPointer<vtkVolume> ovVol =
|
||
geopro::render::buildVoxelI16FromImage(ovImg.Get(), m.quant, cs, vmin,
|
||
vmax);
|
||
renOv->AddVolume(ovVol);
|
||
// 先测 fps(benchVolumeFps 内部会 ResetCamera + 旋满一圈)。
|
||
const double ovFps = benchVolumeFps(rwOv.Get(), renOv, frames);
|
||
// 截图前重设一个利于人眼的取景:整线物理纵横比极扁(~2200m×1.5m×8m),俯视角
|
||
// 看宽面才能呈现整条带(而非边缘线)。
|
||
renOv->ResetCamera();
|
||
renOv->GetActiveCamera()->Elevation(55.0);
|
||
renOv->GetActiveCamera()->Azimuth(20.0);
|
||
renOv->ResetCameraClippingRange();
|
||
rwOv->Render();
|
||
const vtkIdType ovNonBlack = countNonBlackPixels(rwOv.Get(), winW, winH);
|
||
savePng(rwOv.Get(), (shotDir / "lod-overview.png").string());
|
||
std::cout << "[renderLOD] (a) 概览 fps=" << ovFps << " 非空像素=" << ovNonBlack
|
||
<< " (level" << ovLevel << " " << ovNx << "x" << ovNy << "x" << ovNz
|
||
<< ")\n";
|
||
|
||
// ---- (b) 全分辨率局部 fps:level0 一段 brick 列 ----
|
||
const int totBx = store.bricksX(0);
|
||
const int localBx = std::min(4, totBx); // 4 brick 列 ≈ 256 体素宽
|
||
const int bx0 = std::max(0, totBx / 2 - localBx / 2); // 取沿线中段
|
||
std::cout << "[renderLOD] (b) 建 level0 局部 image (brick列 [" << bx0 << ","
|
||
<< (bx0 + localBx) << ") / " << totBx << ")...\n";
|
||
vtkSmartPointer<vtkImageData> locImg =
|
||
buildLocalLevel0Image(store, m, bx0, localBx);
|
||
int locDims[3];
|
||
locImg->GetDimensions(locDims);
|
||
|
||
auto rwLoc = makeOffscreenWindow(winW, winH);
|
||
vtkNew<vtkRenderer> renLoc;
|
||
renLoc->SetBackground(0.0, 0.0, 0.0);
|
||
rwLoc->AddRenderer(renLoc);
|
||
vtkSmartPointer<vtkVolume> locVol =
|
||
geopro::render::buildVoxelI16FromImage(locImg.Get(), m.quant, cs, vmin,
|
||
vmax);
|
||
renLoc->AddVolume(locVol);
|
||
const double locFps = benchVolumeFps(rwLoc.Get(), renLoc, frames);
|
||
// 截图取景:局部块(256×29×162)斜俯视,呈现全分辨率细节供与概览对比。
|
||
renLoc->ResetCamera();
|
||
renLoc->GetActiveCamera()->Elevation(35.0);
|
||
renLoc->GetActiveCamera()->Azimuth(25.0);
|
||
renLoc->ResetCameraClippingRange();
|
||
rwLoc->Render();
|
||
const vtkIdType locNonBlack = countNonBlackPixels(rwLoc.Get(), winW, winH);
|
||
savePng(rwLoc.Get(), (shotDir / "lod-fullres-local.png").string());
|
||
std::cout << "[renderLOD] (b) 局部 fps=" << locFps << " 非空像素="
|
||
<< locNonBlack << " (level0 局部 " << locDims[0] << "x" << locDims[1]
|
||
<< "x" << locDims[2] << ")\n";
|
||
|
||
// ---- (c) LOD 切换动态过渡 ----
|
||
// 同一窗口:相机从远观(看整卷,用 level2 概览体)逐步 dolly 拉近,到一半处
|
||
// 跨越 LOD 切换——把体从 level2 整卷换成 level0 局部体(重设 mapper 输入/相机
|
||
// 目标),逐帧记帧耗时,标切换帧尖峰。
|
||
std::cout << "[renderLOD] (c) LOD 切换动态过渡(" << frames << " 帧 dolly)...\n";
|
||
auto rwTr = makeOffscreenWindow(winW, winH);
|
||
vtkNew<vtkRenderer> renTr;
|
||
renTr->SetBackground(0.0, 0.0, 0.0);
|
||
rwTr->AddRenderer(renTr);
|
||
|
||
// 远观体 = level2 概览(新建一份,避免与 (a) 共享 actor 状态)。
|
||
vtkSmartPointer<vtkVolume> farVol =
|
||
geopro::render::buildVoxelI16FromImage(ovImg.Get(), m.quant, cs, vmin,
|
||
vmax);
|
||
// 近观体 = level0 局部(复用 (b) 的 image)。
|
||
vtkSmartPointer<vtkVolume> nearVol =
|
||
geopro::render::buildVoxelI16FromImage(locImg.Get(), m.quant, cs, vmin,
|
||
vmax);
|
||
|
||
renTr->AddVolume(farVol);
|
||
renTr->ResetCamera(); // 框住整卷(level2 与 level0 物理范围一致)
|
||
vtkCamera* camTr = renTr->GetActiveCamera();
|
||
camTr->Elevation(20.0);
|
||
renTr->ResetCameraClippingRange();
|
||
rwTr->Render(); // 预热远观
|
||
|
||
// dolly 目标:从当前(远)拉近到局部段中心。
|
||
double locCenter[3];
|
||
locImg->GetCenter(locCenter);
|
||
const int switchFrame = frames / 2;
|
||
const double dollyPerFrame =
|
||
std::pow(6.0, 1.0 / std::max(1, switchFrame)); // 切换前累计 dolly≈6×
|
||
|
||
std::vector<double> frameMs(frames, 0.0);
|
||
bool switched = false;
|
||
double switchStallMs = 0.0;
|
||
|
||
for (int f = 0; f < frames; ++f) {
|
||
Stopwatch swF;
|
||
if (f == switchFrame && !switched) {
|
||
// —— LOD 切换那一下 ——:换体 + 把相机焦点移到局部段中心。
|
||
renTr->RemoveVolume(farVol);
|
||
renTr->AddVolume(nearVol);
|
||
camTr->SetFocalPoint(locCenter[0], locCenter[1], locCenter[2]);
|
||
renTr->ResetCameraClippingRange();
|
||
switched = true;
|
||
}
|
||
// 渐进拉近(切换前 dolly 进;切换后继续推近 + 轻微环绕,逐步框满局部块)。
|
||
camTr->Dolly(switched ? 1.04 : dollyPerFrame);
|
||
if (switched) camTr->Azimuth(0.5);
|
||
renTr->ResetCameraClippingRange();
|
||
rwTr->Render();
|
||
frameMs[f] = swF.elapsedMs();
|
||
if (f == switchFrame) switchStallMs = frameMs[f];
|
||
// 切换后推近一小段再截“过渡中间帧”,使局部块已明显呈现(而非切换瞬间仍很远)。
|
||
if (f == switchFrame + (frames - switchFrame) / 3) {
|
||
savePng(rwTr.Get(), (shotDir / "lod-transition-mid.png").string());
|
||
}
|
||
}
|
||
|
||
// 过渡帧耗时统计:平均、最大、切换帧、切换帧相对邻帧的尖峰倍数。
|
||
double sum = 0, mx = 0;
|
||
for (double v : frameMs) {
|
||
sum += v;
|
||
mx = std::max(mx, v);
|
||
}
|
||
const double avgMs = frames > 0 ? sum / frames : 0.0;
|
||
const double preMs =
|
||
switchFrame > 0 ? frameMs[switchFrame - 1] : avgMs;
|
||
const double spikeRatio = preMs > 0 ? switchStallMs / preMs : 0.0;
|
||
// 可感知卡顿判据(绝对耗时为准,尖峰倍数仅作次级信号):当两端帧耗时是亚毫秒
|
||
// 时,一次性换体的 ~9ms 抖动倍数虽大但仍 <1 个 60Hz 帧(16.7ms),人眼不可感。
|
||
// 故:切换帧 >1 个 60Hz 帧(16.7ms)才记“轻微”,>2 帧(33ms)记“可感知卡顿”。
|
||
constexpr double kFrame60Ms = 1000.0 / 60.0; // 16.7ms
|
||
const bool perceptibleStall = switchStallMs > 2.0 * kFrame60Ms; // >33ms
|
||
const bool minorHitch =
|
||
!perceptibleStall && switchStallMs > kFrame60Ms; // 16.7~33ms 轻微
|
||
const vtkIdType trNonBlack = countNonBlackPixels(rwTr.Get(), winW, winH);
|
||
|
||
const bool textureErr = capWin->textureError();
|
||
vtkOutputWindow::SetInstance(nullptr);
|
||
|
||
// 双闸:无纹理错 + 三段均渲出非空像素。
|
||
const bool renderedNonEmpty =
|
||
(ovNonBlack > 0) && (locNonBlack > 0) && (trNonBlack > 0);
|
||
const bool valid = !textureErr && renderedNonEmpty;
|
||
|
||
const double ovFpsV = valid ? ovFps : -1.0;
|
||
const double locFpsV = valid ? locFps : -1.0;
|
||
const bool ovInteractive = valid && ovFps >= 15.0;
|
||
const bool locInteractive = valid && locFps >= 15.0;
|
||
const double peak = Probe::peakMemMB();
|
||
|
||
const char* stallTxt =
|
||
perceptibleStall ? "可感知卡顿" : (minorHitch ? "轻微抖动(<2帧)" : "无");
|
||
std::cout << "[renderLOD] (c) 过渡帧耗时 avg=" << avgMs << "ms max=" << mx
|
||
<< "ms 切换帧=" << switchStallMs << "ms (邻帧 " << preMs << "ms, 尖峰 "
|
||
<< spikeRatio << "×) 卡顿=" << stallTxt << "\n";
|
||
|
||
std::cout << "\n=== renderLOD LOD-fps 探针指标 ===\n";
|
||
std::cout << "离屏闸门 : OK\n";
|
||
std::cout << "纹理维度错误 : " << (textureErr ? "是(!!)" : "否") << "\n";
|
||
std::cout << "三段均渲出非空 : " << (renderedNonEmpty ? "是" : "否(!!)")
|
||
<< " (概览=" << ovNonBlack << " 局部=" << locNonBlack
|
||
<< " 过渡=" << trNonBlack << ")\n";
|
||
std::cout << "(a) 粗层概览 fps : "
|
||
<< (valid ? std::to_string(ovFpsV) : std::string("INVALID"))
|
||
<< " (level" << ovLevel << " " << ovNx << "x" << ovNy << "x" << ovNz
|
||
<< ") 交互级=" << (ovInteractive ? "是 ✔" : "否 ✘") << "\n";
|
||
std::cout << "(b) 全分辨率局部fps: "
|
||
<< (valid ? std::to_string(locFpsV) : std::string("INVALID"))
|
||
<< " (level0 局部 " << locDims[0] << "x" << locDims[1] << "x"
|
||
<< locDims[2] << ") 交互级=" << (locInteractive ? "是 ✔" : "否 ✘")
|
||
<< "\n";
|
||
std::cout << "(c) 过渡平均/最大 : " << avgMs << " / " << mx << " ms\n";
|
||
std::cout << " 切换帧耗时 : " << switchStallMs << " ms (邻帧 " << preMs
|
||
<< " ms, 尖峰 " << spikeRatio << "×)\n";
|
||
std::cout << " 可感知卡顿 : " << stallTxt
|
||
<< (perceptibleStall ? " ✘" : " ✔") << " (判据:切换帧 >33ms 才记卡顿"
|
||
"; 1 帧 60Hz=16.7ms)\n";
|
||
std::cout << "进程峰值内存(MB) : " << peak << "\n";
|
||
std::cout << "截图 : " << shotDir.string()
|
||
<< " (lod-overview / lod-fullres-local / lod-transition-mid)\n";
|
||
|
||
writeMetricLine(
|
||
"renderLOD,dir=" + dir + ",totLevels=" + std::to_string(totLevels) +
|
||
",ovLevel=" + std::to_string(ovLevel) +
|
||
",ovDims=" + std::to_string(ovNx) + "x" + std::to_string(ovNy) + "x" +
|
||
std::to_string(ovNz) +
|
||
",ovFps=" + (valid ? std::to_string(ovFpsV) : "INVALID") +
|
||
",ovNonBlack=" + std::to_string(ovNonBlack) +
|
||
",locDims=" + std::to_string(locDims[0]) + "x" +
|
||
std::to_string(locDims[1]) + "x" + std::to_string(locDims[2]) +
|
||
",locFps=" + (valid ? std::to_string(locFpsV) : "INVALID") +
|
||
",locNonBlack=" + std::to_string(locNonBlack) +
|
||
",trAvgMs=" + std::to_string(avgMs) + ",trMaxMs=" + std::to_string(mx) +
|
||
",switchMs=" + std::to_string(switchStallMs) +
|
||
",switchSpike=" + std::to_string(spikeRatio) +
|
||
",stall=" + std::to_string(perceptibleStall ? 1 : 0) +
|
||
",trNonBlack=" + std::to_string(trNonBlack) +
|
||
",textureErr=" + std::to_string(textureErr ? 1 : 0) +
|
||
",valid=" + std::to_string(valid ? 1 : 0) +
|
||
",peakMB=" + std::to_string(peak));
|
||
|
||
// 写 poc-results-C.md 的 LOD 段(追加,不覆盖 renderC-partitioned 段)。
|
||
{
|
||
const fs::path repo =
|
||
fs::path("docs") / "superpowers" / "plans" / "poc-results-C.md";
|
||
fs::create_directories(repo.parent_path());
|
||
std::ofstream rf(repo.string(), std::ios::app);
|
||
if (rf) {
|
||
rf << "\n\n# POC-C LOD-fps 探针结果(Task 12c)\n\n";
|
||
rf << "金字塔 store: " << dir << "(level0=" << m.nx << "x" << m.ny << "x"
|
||
<< m.nz << ",总 " << totLevels << " 层)\n\n";
|
||
rf << "| 项 | 维度 | 结果 |\n|---|---|---|\n";
|
||
rf << "| (a) 粗层概览 fps | level" << ovLevel << " " << ovNx << "x" << ovNy
|
||
<< "x" << ovNz << " | " << (valid ? std::to_string(ovFpsV) : "INVALID")
|
||
<< " fps " << (ovInteractive ? "(交互级)" : "(未达交互级)") << " |\n";
|
||
rf << "| (b) 全分辨率局部 fps | level0 局部 " << locDims[0] << "x"
|
||
<< locDims[1] << "x" << locDims[2] << " | "
|
||
<< (valid ? std::to_string(locFpsV) : "INVALID") << " fps "
|
||
<< (locInteractive ? "(交互级)" : "(未达交互级)") << " |\n";
|
||
rf << "| (c) LOD 切换过渡 | 切换帧 " << switchFrame << "/" << frames
|
||
<< " | 平均 " << avgMs << "ms,切换帧 " << switchStallMs << "ms(尖峰 "
|
||
<< spikeRatio << "×),"
|
||
<< (perceptibleStall ? "可感知卡顿"
|
||
: (minorHitch ? "轻微抖动" : "无可感知卡顿"))
|
||
<< " |\n\n";
|
||
rf << "- 卡顿判据:切换帧绝对耗时 >33ms(2 个 60Hz 帧)才记可感知卡顿;"
|
||
"16.7~33ms 记轻微抖动;亚毫秒基线下尖峰倍数大但绝对值低不算卡顿。\n";
|
||
rf << "- 双闸:纹理维度错误=" << (textureErr ? "是" : "否")
|
||
<< ";三段均渲出非空像素=" << (renderedNonEmpty ? "是" : "否")
|
||
<< "(概览 " << ovNonBlack << " / 局部 " << locNonBlack << " / 过渡 "
|
||
<< trNonBlack << ")。\n";
|
||
rf << "- 截图(人眼判“概览糊→拉近清晰”):docs/superpowers/plans/poc-lod-shots/"
|
||
"lod-overview.png、lod-fullres-local.png、lod-transition-mid.png\n";
|
||
rf << "- 进程峰值内存: " << peak << " MB\n\n";
|
||
rf << "## 判据结论\n";
|
||
if (valid && ovInteractive && locInteractive && !perceptibleStall) {
|
||
rf << "粗层概览 + 全分辨率局部【都达交互级】且切换【无不可接受卡顿】→ "
|
||
"LOD-based C 路线钉死可行。\n";
|
||
} else if (valid && ovInteractive && !locInteractive) {
|
||
rf << "粗层快但全分辨率局部仍慢 → VTK 体绘制有真实天花板,记录,"
|
||
"评估 OpenVDS/自建 GL。\n";
|
||
} else if (valid && perceptibleStall) {
|
||
rf << "两端 fps 可接受但切换卡顿明显(切换帧 " << switchStallMs
|
||
<< "ms)→ 为后续 morphing/淡入提供依据。\n";
|
||
} else if (!valid) {
|
||
rf << "双闸未过(纹理错或空渲染)→ 数字不可信,如实标 INVALID。\n";
|
||
} else {
|
||
rf << "部分达标,详见上表。\n";
|
||
}
|
||
rf << "\n**最低配未验声明**:本探针仅在本机(RTX 3060)跑得上限数字,"
|
||
"最低配机器未验证,需用户在目标机跑或提供型号。\n";
|
||
}
|
||
std::cout << "[renderLOD] 报告追加写入 " << repo.string() << "\n";
|
||
}
|
||
|
||
return valid ? 0 : 1;
|
||
}
|
||
|
||
// ============================================================================
|
||
// ① 视觉调优:出一帧能看结构的图 + 调优前后 fps 对照(Task 12d)
|
||
// ============================================================================
|
||
//
|
||
// 在【真实金字塔 store】上对局部段(level0 一段 brick 列)与粗层概览(level2 整卷)
|
||
// 各跑两遍体绘制 fps:调优前(默认色阶 0.15 不透明度 无夸张) vs 调优后(结构色阶 +
|
||
// --opacity + --exagg 垂向夸张),离屏存 lod-tuned-local.png / lod-tuned-overview.png,
|
||
// 并打印前后 fps 对照——证实「视觉调优对 fps 近乎中性」这一探针认知。双闸防假帧率。
|
||
int cmdTune(int argc, char** argv) {
|
||
const Args a = parseArgs(argc, argv, 2);
|
||
if (a.positional.empty()) {
|
||
std::cerr << "用法: gpr_poc tune <storeDir> [--opacity 0.5] [--exagg 8] "
|
||
"[--frames 120] [--localBricks 4]\n";
|
||
return 2;
|
||
}
|
||
const std::string dir = a.positional[0];
|
||
const double opacity = std::stod(a.get("opacity", "0.5"));
|
||
const double exagg = std::stod(a.get("exagg", "8"));
|
||
const int frames = std::stoi(a.get("frames", "120"));
|
||
const int localBricks = std::stoi(a.get("localBricks", "4"));
|
||
std::cout << "[tune] storeDir=" << dir << " opacity=" << opacity
|
||
<< " exagg=" << exagg << " frames=" << frames << "\n";
|
||
|
||
std::cout << "[tune] 离屏闸门复检...\n";
|
||
if (cmdOffscreenSmoke() != 0) {
|
||
std::cout << "[tune] 闸门失败,中止。\n";
|
||
return 1;
|
||
}
|
||
|
||
geopro::data::ChunkedVolumeStore store(dir);
|
||
const geopro::data::StoreMeta& m = store.meta();
|
||
const int totLevels = store.levels();
|
||
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
|
||
|
||
const geopro::core::ColorScale csPlain = makeColorScale(vmin, vmax);
|
||
const geopro::core::ColorScale csTuned = makeStructuralColorScale(vmin, vmax);
|
||
|
||
const fs::path shotDir =
|
||
fs::path("docs") / "superpowers" / "plans" / "poc-lod-shots";
|
||
fs::create_directories(shotDir);
|
||
const int winW = 1024, winH = 768;
|
||
|
||
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
|
||
vtkOutputWindow::SetInstance(capWin);
|
||
|
||
// ---- 局部段:level0 一段 brick 列(沿线中段)----
|
||
const int totBx = store.bricksX(0);
|
||
const int localBx = std::min(localBricks, totBx);
|
||
const int bx0 = std::max(0, totBx / 2 - localBx / 2);
|
||
vtkSmartPointer<vtkImageData> locImg =
|
||
buildLocalLevel0Image(store, m, bx0, localBx);
|
||
int locDims[3];
|
||
locImg->GetDimensions(locDims);
|
||
|
||
// 调优前局部 fps(默认色阶 0.15 无夸张)。
|
||
auto rwA = makeOffscreenWindow(winW, winH);
|
||
vtkNew<vtkRenderer> renA;
|
||
renA->SetBackground(0.0, 0.0, 0.0);
|
||
rwA->AddRenderer(renA);
|
||
vtkSmartPointer<vtkVolume> volA =
|
||
buildTunedVolume(locImg.Get(), m.quant, csPlain, vmin, vmax, 0.15, 1.0,
|
||
/*structuralOpacity=*/false); // 原始线性单斜坡基线
|
||
renA->AddVolume(volA);
|
||
const double locFpsBefore = benchVolumeFps(rwA.Get(), renA, frames);
|
||
|
||
// 调优后局部 fps(结构色阶 + opacity + exagg)。
|
||
auto rwB = makeOffscreenWindow(winW, winH);
|
||
vtkNew<vtkRenderer> renB;
|
||
renB->SetBackground(0.04, 0.04, 0.08); // 深蓝灰背景,衬托体
|
||
rwB->AddRenderer(renB);
|
||
vtkSmartPointer<vtkVolume> volB =
|
||
buildTunedVolume(locImg.Get(), m.quant, csTuned, vmin, vmax, opacity,
|
||
exagg);
|
||
renB->AddVolume(volB);
|
||
const double locFpsAfter = benchVolumeFps(rwB.Get(), renB, frames);
|
||
// 调优后取景:夸张后块更"立体",斜俯视呈现截面层次;Zoom 拉近填满画面。
|
||
renB->ResetCamera();
|
||
renB->GetActiveCamera()->Elevation(28.0);
|
||
renB->GetActiveCamera()->Azimuth(30.0);
|
||
renB->GetActiveCamera()->Zoom(1.7);
|
||
renB->ResetCameraClippingRange();
|
||
rwB->Render();
|
||
const vtkIdType locNonBlack = countNonBlackPixels(rwB.Get(), winW, winH);
|
||
savePng(rwB.Get(), (shotDir / "lod-tuned-local.png").string());
|
||
|
||
// ---- 概览:level2 整卷(接受它就是细带)----
|
||
const int ovLevel = std::min(2, totLevels - 1);
|
||
vtkSmartPointer<vtkImageData> ovImg = buildLevelImage(store, ovLevel, m);
|
||
auto rwO = makeOffscreenWindow(winW, winH);
|
||
vtkNew<vtkRenderer> renO;
|
||
renO->SetBackground(0.04, 0.04, 0.08);
|
||
rwO->AddRenderer(renO);
|
||
vtkSmartPointer<vtkVolume> volO =
|
||
buildTunedVolume(ovImg.Get(), m.quant, csTuned, vmin, vmax, opacity,
|
||
exagg);
|
||
renO->AddVolume(volO);
|
||
const double ovFpsAfter = benchVolumeFps(rwO.Get(), renO, frames);
|
||
renO->ResetCamera();
|
||
renO->GetActiveCamera()->Elevation(50.0);
|
||
renO->GetActiveCamera()->Azimuth(20.0);
|
||
renO->ResetCameraClippingRange();
|
||
rwO->Render();
|
||
const vtkIdType ovNonBlack = countNonBlackPixels(rwO.Get(), winW, winH);
|
||
savePng(rwO.Get(), (shotDir / "lod-tuned-overview.png").string());
|
||
|
||
const bool textureErr = capWin->textureError();
|
||
vtkOutputWindow::SetInstance(nullptr);
|
||
const bool valid =
|
||
!textureErr && locNonBlack > 0 && ovNonBlack > 0;
|
||
|
||
const double dropPct =
|
||
locFpsBefore > 0 ? (locFpsBefore - locFpsAfter) / locFpsBefore * 100.0
|
||
: 0.0;
|
||
|
||
std::cout << "\n=== tune 视觉调优指标 ===\n";
|
||
std::cout << "局部段维度 : " << locDims[0] << "x" << locDims[1] << "x"
|
||
<< locDims[2] << " (level0)\n";
|
||
std::cout << "调优前局部 fps : "
|
||
<< (valid ? std::to_string(locFpsBefore) : "INVALID")
|
||
<< " (默认蓝白红, 不透明度 0.15, 无夸张)\n";
|
||
std::cout << "调优后局部 fps : "
|
||
<< (valid ? std::to_string(locFpsAfter) : "INVALID")
|
||
<< " (结构色阶, 不透明度 " << opacity << ", 夸张 " << exagg
|
||
<< "x)\n";
|
||
std::cout << "fps 变化 : " << dropPct
|
||
<< "% (正=变慢/负=变快; 探针预期近乎中性)\n";
|
||
std::cout << "调优后概览 fps : "
|
||
<< (valid ? std::to_string(ovFpsAfter) : "INVALID") << " (level"
|
||
<< ovLevel << ")\n";
|
||
std::cout << "双闸 : 纹理错=" << (textureErr ? "是" : "否")
|
||
<< " 局部非空=" << locNonBlack << " 概览非空=" << ovNonBlack
|
||
<< " → " << (valid ? "可信" : "INVALID") << "\n";
|
||
std::cout << "截图 : " << shotDir.string()
|
||
<< " (lod-tuned-local.png / lod-tuned-overview.png)\n";
|
||
|
||
writeMetricLine(
|
||
"tune,dir=" + dir + ",opacity=" + std::to_string(opacity) +
|
||
",exagg=" + std::to_string(exagg) +
|
||
",locDims=" + std::to_string(locDims[0]) + "x" +
|
||
std::to_string(locDims[1]) + "x" + std::to_string(locDims[2]) +
|
||
",locFpsBefore=" + (valid ? std::to_string(locFpsBefore) : "INVALID") +
|
||
",locFpsAfter=" + (valid ? std::to_string(locFpsAfter) : "INVALID") +
|
||
",dropPct=" + std::to_string(dropPct) +
|
||
",ovFpsAfter=" + (valid ? std::to_string(ovFpsAfter) : "INVALID") +
|
||
",locNonBlack=" + std::to_string(locNonBlack) +
|
||
",ovNonBlack=" + std::to_string(ovNonBlack) +
|
||
",valid=" + std::to_string(valid ? 1 : 0));
|
||
return valid ? 0 : 1;
|
||
}
|
||
|
||
// ============================================================================
|
||
// ② fps 预算:递增全分辨率(level0)窗口找「每帧体素预算」(Task 12d)
|
||
// ============================================================================
|
||
//
|
||
// 对递增的 level0 brick 列段(4,16,64,128,256 brick,可 --bricks 覆盖)各重组成
|
||
// 局部整卷 image 跑体绘制 fps,输出表 brick数/体素数/fps,找出 fps 跌破 30 的体素
|
||
// 阈值 = production LOD 每帧渲染的全分辨率块数上限。双闸防假帧率。
|
||
int cmdFpsBudget(int argc, char** argv) {
|
||
const Args a = parseArgs(argc, argv, 2);
|
||
if (a.positional.empty()) {
|
||
std::cerr << "用法: gpr_poc fps-budget <storeDir> [--frames 90] "
|
||
"[--bricks 4,16,64,128,256]\n";
|
||
return 2;
|
||
}
|
||
const std::string dir = a.positional[0];
|
||
const int frames = std::stoi(a.get("frames", "90"));
|
||
const double opacity = std::stod(a.get("opacity", "0.5"));
|
||
const double exagg = std::stod(a.get("exagg", "8"));
|
||
|
||
// 解析 brick 段列表(逗号分隔)。
|
||
std::vector<int> brickSteps;
|
||
{
|
||
const std::string raw = a.get("bricks", "4,16,64,128,256");
|
||
std::string cur;
|
||
for (char ch : raw) {
|
||
if (ch == ',') {
|
||
if (!cur.empty()) brickSteps.push_back(std::stoi(cur));
|
||
cur.clear();
|
||
} else {
|
||
cur.push_back(ch);
|
||
}
|
||
}
|
||
if (!cur.empty()) brickSteps.push_back(std::stoi(cur));
|
||
}
|
||
|
||
std::cout << "[fps-budget] storeDir=" << dir << " frames=" << frames << "\n";
|
||
std::cout << "[fps-budget] 离屏闸门复检...\n";
|
||
if (cmdOffscreenSmoke() != 0) {
|
||
std::cout << "[fps-budget] 闸门失败,中止,不产出 fps。\n";
|
||
return 1;
|
||
}
|
||
|
||
geopro::data::ChunkedVolumeStore store(dir);
|
||
const geopro::data::StoreMeta& m = store.meta();
|
||
const int totBx = store.bricksX(0);
|
||
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
|
||
const geopro::core::ColorScale cs = makeStructuralColorScale(vmin, vmax);
|
||
|
||
std::cout << "[fps-budget] level0=" << m.nx << "x" << m.ny << "x" << m.nz
|
||
<< " 总 brick列=" << totBx << " brick=" << m.brick << "\n";
|
||
|
||
struct Row {
|
||
int bricks;
|
||
long long voxels;
|
||
double fps;
|
||
bool valid;
|
||
};
|
||
std::vector<Row> rows;
|
||
|
||
constexpr double kTargetFps = 30.0;
|
||
long long budgetVoxels = -1; // fps 跌破 30 前的最大体素数
|
||
int budgetBricks = -1;
|
||
long long firstBelowVoxels = -1;
|
||
int firstBelowBricks = -1;
|
||
|
||
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
|
||
vtkOutputWindow::SetInstance(capWin);
|
||
|
||
for (int nb : brickSteps) {
|
||
const int localBx = std::min(nb, totBx);
|
||
if (localBx <= 0) continue;
|
||
const int bx0 = std::max(0, totBx / 2 - localBx / 2);
|
||
vtkSmartPointer<vtkImageData> img =
|
||
buildLocalLevel0Image(store, m, bx0, localBx);
|
||
int d[3];
|
||
img->GetDimensions(d);
|
||
const long long voxels =
|
||
static_cast<long long>(d[0]) * d[1] * d[2];
|
||
|
||
auto rw = makeOffscreenWindow(1024, 768);
|
||
vtkNew<vtkRenderer> ren;
|
||
ren->SetBackground(0.0, 0.0, 0.0);
|
||
rw->AddRenderer(ren);
|
||
vtkSmartPointer<vtkVolume> vol =
|
||
buildTunedVolume(img.Get(), m.quant, cs, vmin, vmax, opacity, exagg);
|
||
ren->AddVolume(vol);
|
||
|
||
const double fps = benchVolumeFps(rw.Get(), ren, frames);
|
||
// 双闸:纹理无错 + 该段渲出非空像素。
|
||
ren->ResetCamera();
|
||
rw->Render();
|
||
const vtkIdType nonBlack = countNonBlackPixels(rw.Get(), 1024, 768);
|
||
const bool valid = !capWin->textureError() && nonBlack > 0;
|
||
|
||
rows.push_back({localBx, voxels, fps, valid});
|
||
std::cout << "[fps-budget] brick=" << localBx << " (" << d[0] << "x" << d[1]
|
||
<< "x" << d[2] << ") 体素=" << voxels << " fps="
|
||
<< (valid ? std::to_string(fps) : "INVALID")
|
||
<< " 非空=" << nonBlack << "\n";
|
||
|
||
if (valid) {
|
||
if (fps >= kTargetFps) {
|
||
if (voxels > budgetVoxels) {
|
||
budgetVoxels = voxels;
|
||
budgetBricks = localBx;
|
||
}
|
||
} else if (firstBelowVoxels < 0) {
|
||
firstBelowVoxels = voxels;
|
||
firstBelowBricks = localBx;
|
||
}
|
||
}
|
||
}
|
||
|
||
const bool textureErr = capWin->textureError();
|
||
vtkOutputWindow::SetInstance(nullptr);
|
||
const double peak = Probe::peakMemMB();
|
||
|
||
std::cout << "\n=== fps-budget 每帧体素预算表 ===\n";
|
||
std::cout << "| brick段 | 维度体素数 | 体绘制 fps | ≥30 |\n";
|
||
std::cout << "|---|---|---|---|\n";
|
||
for (const auto& r : rows) {
|
||
std::cout << "| " << r.bricks << " | " << r.voxels << " | "
|
||
<< (r.valid ? std::to_string(r.fps) : std::string("INVALID"))
|
||
<< " | " << (r.valid && r.fps >= kTargetFps ? "是" : "否")
|
||
<< " |\n";
|
||
}
|
||
std::cout << "\n每帧体素预算(fps≥30 上限) : "
|
||
<< (budgetVoxels >= 0 ? std::to_string(budgetVoxels) +
|
||
" 体素 (" + std::to_string(budgetBricks) +
|
||
" brick列)"
|
||
: std::string("未触达(所有测点均 ≥30)"))
|
||
<< "\n";
|
||
std::cout << "首个跌破 30 的窗口 : "
|
||
<< (firstBelowVoxels >= 0
|
||
? std::to_string(firstBelowVoxels) + " 体素 (" +
|
||
std::to_string(firstBelowBricks) + " brick列)"
|
||
: std::string("无(测点未跌破; 需更大 --bricks)"))
|
||
<< "\n";
|
||
std::cout << "纹理维度错误 : " << (textureErr ? "是(!!)" : "否")
|
||
<< "\n";
|
||
std::cout << "进程峰值内存(MB) : " << peak << "\n";
|
||
|
||
// 落 last-metrics + 追加写 poc-results-C.md。
|
||
for (const auto& r : rows) {
|
||
writeMetricLine(
|
||
"fps-budget,dir=" + dir + ",bricks=" + std::to_string(r.bricks) +
|
||
",voxels=" + std::to_string(r.voxels) +
|
||
",fps=" + (r.valid ? std::to_string(r.fps) : "INVALID") +
|
||
",valid=" + std::to_string(r.valid ? 1 : 0));
|
||
}
|
||
|
||
{
|
||
const fs::path repo =
|
||
fs::path("docs") / "superpowers" / "plans" / "poc-results-C.md";
|
||
fs::create_directories(repo.parent_path());
|
||
std::ofstream rf(repo.string(), std::ios::app);
|
||
if (rf) {
|
||
rf << "\n\n# POC-C fps 预算探针结果(Task 12d ②)\n\n";
|
||
rf << "金字塔 store: " << dir << "(level0=" << m.nx << "x" << m.ny << "x"
|
||
<< m.nz << ",brick=" << m.brick << ")\n\n";
|
||
rf << "递增 level0 局部窗口(沿线中段 brick 列)体绘制 fps:\n\n";
|
||
rf << "| brick段 | 体素数 | 体绘制 fps | ≥30fps |\n|---|---|---|---|\n";
|
||
for (const auto& r : rows) {
|
||
rf << "| " << r.bricks << " | " << r.voxels << " | "
|
||
<< (r.valid ? std::to_string(r.fps) : "INVALID") << " | "
|
||
<< (r.valid && r.fps >= kTargetFps ? "是" : "否") << " |\n";
|
||
}
|
||
rf << "\n- **每帧体素预算(fps≥30 上限)**: "
|
||
<< (budgetVoxels >= 0
|
||
? std::to_string(budgetVoxels) + " 体素(" +
|
||
std::to_string(budgetBricks) + " brick 列)"
|
||
: "未触达,所有测点 ≥30fps")
|
||
<< "\n";
|
||
rf << "- 首个跌破 30 的窗口: "
|
||
<< (firstBelowVoxels >= 0
|
||
? std::to_string(firstBelowVoxels) + " 体素(" +
|
||
std::to_string(firstBelowBricks) + " brick 列)"
|
||
: "无(需更大 --bricks 段触达天花板)")
|
||
<< "\n";
|
||
rf << "- 双闸:纹理维度错误=" << (textureErr ? "是" : "否")
|
||
<< ";每段均按非空像素校验。\n";
|
||
rf << "- production LOD 应把【每帧渲染的全分辨率块】卡在此预算以内。\n";
|
||
rf << "- **本机 RTX 3060 上限数;最低配需用户在目标机跑 fps-budget/view。**\n";
|
||
}
|
||
std::cout << "[fps-budget] 报告追加写入 " << repo.string() << "\n";
|
||
}
|
||
|
||
return textureErr ? 1 : 0;
|
||
}
|
||
|
||
// ============================================================================
|
||
// 视觉调参画廊(Task 12d gallery):view --preview --variant N
|
||
// ============================================================================
|
||
//
|
||
// 同一局部段(沿线中段 kViewDefaultLocalBricks 列全分辨率) + 同一相机框法
|
||
// (ResetCamera→Elevation/Azimuth→Zoom),只换「不透明度包络 / 配色 / 取景角度 /
|
||
// 背景」四组视觉参数,各存一张 PNG 供控制方挑选。fps 对视觉调参近乎中性,每组实测验证。
|
||
//
|
||
// 注:交互窗口(无 flag 的 view)默认即采用 var4(kViewDefaultVariant)——配色/不透明度
|
||
// 包络/取景/exagg/背景全部走同一份 var4 参数,故「交互默认画面 == view-var4」。
|
||
enum class OpacityProfile {
|
||
kSolid, // V 形实体感:中高值段普遍可见,半透明实心块
|
||
kStructural, // 现有双端斜坡:仅正负两端不透明(对照基线)
|
||
};
|
||
enum class ColorChoice {
|
||
kStructural,
|
||
kSeismic,
|
||
kJet,
|
||
kBrightSeismic, // 调亮版 seismic
|
||
kGrayEnhanced, // 增强灰度
|
||
};
|
||
|
||
struct GalleryVariant {
|
||
const char* name; // 文件名后缀:view-<name>.png
|
||
OpacityProfile profile;
|
||
ColorChoice color;
|
||
double floorOpacity; // 近零背景不透明度(kSolid 用)
|
||
double midOpacity; // 中值段不透明度(kSolid 用)
|
||
double maxOpacity; // 两端峰值不透明度
|
||
double exagg; // 垂向夸张
|
||
double elevation; // ResetCamera 后 Elevation
|
||
double azimuth; // 再 Azimuth
|
||
double zoom; // 再 Zoom 填满画面
|
||
double bg[3]; // 背景 RGB
|
||
const char* desc; // 报告用中文说明
|
||
// ---- C4 视觉调优(梯度不透明度 + 光照),默认关 → 不改既有变体行为 ----
|
||
bool useGradientOpacity = false; // SetGradientOpacity:均匀层透明、界面/异常显形
|
||
bool useShade = false; // SetShade:层界面带立体明暗
|
||
// ---- P4 调亮/调清晰 ----
|
||
// 梯度门松弛度 0~1:0=严格(均匀层全透、偏暗) 1=宽松(均匀层保留底不透明、更亮更满)。
|
||
// 宽松时降低梯度阈值并抬高「低梯度区」的不透明度地板,让横向层叠/基底反射等弱结构保留。
|
||
double gradGateRelax = 0.0;
|
||
double ambient = 0.30; // 光照环境项(别太低,否则体面偏暗);useShade 时生效。
|
||
};
|
||
|
||
// P4 调亮/调清晰:4 组对照(暗版基线 / 提亮 / 高对比 / 灰度增强)。
|
||
// 同一局部段、同一斜穿取景(El45/Az30)。P3 默认(seismic+严格梯度门+低 ambient)整体
|
||
// 偏暗、均匀层被门全透成空。本组在「消雾」与「够亮够满」间放宽门控、抬 ambient、换更亮
|
||
// 配色,让横向层叠/竖纹/基底反射醒目。所有端点按该体 2/98 分位自适应(runGalleryVariant
|
||
// 内标定),非写死单一数据。末项 = kViewDefaultVariant → 交互窗口默认取最清晰醒目组。
|
||
//
|
||
// 字段:floorOpacity/midOpacity/maxOpacity(V 形标量包络)、gradGateRelax(梯度门松弛
|
||
// 0~1)、ambient(光照环境项)。提亮组抬高 floor/mid 让均匀层保留底不透明、抬 ambient
|
||
// 防体面发暗、放宽梯度门保留弱结构。
|
||
const GalleryVariant kGalleryVariants[] = {
|
||
// var1:暗版基线(= P3 默认)——seismic + 严格梯度门(relax0) + 低 ambient0.3 + 暗背景。
|
||
// 均匀层几乎全透、整体偏暗偏空,仅作对照。
|
||
{"var1", OpacityProfile::kSolid, ColorChoice::kSeismic,
|
||
0.04, 0.30, 0.60, 8.0, 45.0, 30.0, 1.5, {0.07, 0.08, 0.11},
|
||
"暗版基线(P3默认):seismic+严格梯度门+低ambient+暗背景,均匀层近全透、偏暗偏空",
|
||
/*useGradientOpacity=*/true, /*useShade=*/true,
|
||
/*gradGateRelax=*/0.0, /*ambient=*/0.30},
|
||
// var2:提亮——调亮版 seismic + 放宽梯度门(relax0.6) + 抬 floor/mid/max + 高 ambient
|
||
// + 略亮背景。均匀层保留底不透明、横向层叠透出、整体明显更亮更实。
|
||
{"var2", OpacityProfile::kSolid, ColorChoice::kBrightSeismic,
|
||
0.10, 0.45, 0.75, 8.0, 45.0, 30.0, 1.5, {0.12, 0.13, 0.17},
|
||
"提亮:调亮版seismic+放宽梯度门(relax0.6)+抬不透明度(floor0.10/mid0.45/max0.75)"
|
||
"+高ambient0.5,均匀层保留、层叠透出、明显更亮",
|
||
/*useGradientOpacity=*/true, /*useShade=*/true,
|
||
/*gradGateRelax=*/0.6, /*ambient=*/0.50},
|
||
// var3:高对比——jet 高饱和配色 + 放宽梯度门(relax0.5) + 抬不透明度 + ambient0.45 +
|
||
// 暗背景衬高饱和。弱信号也映到鲜明色相,层界面/异常对比最狠。
|
||
{"var3", OpacityProfile::kSolid, ColorChoice::kJet,
|
||
0.08, 0.42, 0.78, 8.0, 45.0, 30.0, 1.5, {0.04, 0.04, 0.07},
|
||
"高对比:jet高饱和+放宽梯度门(relax0.5)+抬不透明度+ambient0.45+暗背景衬色,"
|
||
"弱信号映鲜明色相、层界面对比最狠",
|
||
/*useGradientOpacity=*/true, /*useShade=*/true,
|
||
/*gradGateRelax=*/0.5, /*ambient=*/0.45},
|
||
// var4:灰度增强(默认)——增强灰度 + 放宽梯度门(relax0.7) + 抬不透明度 + 高 ambient
|
||
// + 中性背景。GPR 内部水平层叠/竖纹/基底反射用增强灰度最干净直读,选作交互默认。
|
||
{"var4", OpacityProfile::kSolid, ColorChoice::kGrayEnhanced,
|
||
0.12, 0.48, 0.80, 8.0, 45.0, 30.0, 1.5, {0.14, 0.15, 0.18},
|
||
"灰度增强(默认/综合最佳):增强灰度+放宽梯度门(relax0.7)+抬不透明度"
|
||
"(floor0.12/mid0.48/max0.80)+高ambient0.5+中性背景,层叠/竖纹/基底反射干净醒目"
|
||
" → 交互窗口默认",
|
||
/*useGradientOpacity=*/true, /*useShade=*/true,
|
||
/*gradGateRelax=*/0.7, /*ambient=*/0.50},
|
||
};
|
||
|
||
// 交互窗口(无 flag 的 view)的默认视觉变体 = var4(kGalleryVariants 末项)。
|
||
// 交互默认与 view-var4 走同一份参数 → 二者画面一致(DRY,不复制粘贴漂移)。
|
||
const GalleryVariant& kViewDefaultVariant =
|
||
kGalleryVariants[sizeof(kGalleryVariants) / sizeof(kGalleryVariants[0]) - 1];
|
||
|
||
geopro::core::ColorScale pickColor(ColorChoice c, double vmin, double vmax) {
|
||
switch (c) {
|
||
case ColorChoice::kSeismic: return makeSeismicColorScale(vmin, vmax);
|
||
case ColorChoice::kJet: return makeJetColorScale(vmin, vmax);
|
||
case ColorChoice::kBrightSeismic:
|
||
return makeBrightSeismicColorScale(vmin, vmax);
|
||
case ColorChoice::kGrayEnhanced:
|
||
return makeGrayEnhancedColorScale(vmin, vmax);
|
||
case ColorChoice::kStructural:
|
||
default: return makeStructuralColorScale(vmin, vmax);
|
||
}
|
||
}
|
||
|
||
// 按变体的不透明度包络建体属性(gallery / 交互默认共用,DRY)。kSolid 走 V 形实体
|
||
// 包络(floor/mid/max),kStructural 走双端斜坡(仅 maxOpacity)。
|
||
//
|
||
// C4 视觉调优:若变体开了 useGradientOpacity 且传入了实测梯度统计 gs,再叠加
|
||
// 1) 梯度不透明度(SetGradientOpacity):低梯度(均匀层)→透明、高梯度(界面/异常)→显形,
|
||
// 让射线穿透均匀雾、内部界面浮出(阈值按 gs 的 median/p90/p99 标定,不靠猜);
|
||
// 2) 光照(useShade → SetShade + Ambient/Diffuse/Specular):层界面带立体明暗。
|
||
vtkSmartPointer<vtkVolumeProperty> makeVariantProperty(
|
||
const GalleryVariant& v, const geopro::core::Quant& q,
|
||
const geopro::core::ColorScale& cs, double vmin, double vmax,
|
||
double maxOpacity, const GradStats* gs = nullptr) {
|
||
vtkSmartPointer<vtkVolumeProperty> prop;
|
||
if (v.profile == OpacityProfile::kSolid) {
|
||
prop = makeSolidVolumeProperty(q, cs, vmin, vmax, v.floorOpacity,
|
||
v.midOpacity, maxOpacity);
|
||
} else {
|
||
prop = makeTunedVolumeProperty(q, cs, vmin, vmax, maxOpacity);
|
||
}
|
||
|
||
// 梯度不透明度:均匀层(低梯度)半透/透明,层界面/异常边缘(高梯度)显形。阈值按实测
|
||
// 分布,并按 gradGateRelax 松弛:
|
||
// - relax=0(严格,暗版):median→0、p90→0.5、p99→0.9,均匀层全透 → 偏暗偏空。
|
||
// - relax>0(调亮):低梯度地板抬到 floorG(均匀层保留底不透明、更亮更满),阈值
|
||
// 整体左移(更早升起),让横向层叠/基底反射等弱结构保留可见。
|
||
// 无 gs(未测)则跳过。
|
||
if (v.useGradientOpacity && gs != nullptr && gs->samples > 0) {
|
||
const double relax = std::clamp(v.gradGateRelax, 0.0, 1.0);
|
||
const double floorG = 0.30 * relax; // 低梯度区不透明度地板(均匀层保留度)
|
||
const double midG = 0.5 + 0.25 * relax;
|
||
const double hiG = 0.9;
|
||
// 阈值左移:松弛越大,门越早从低梯度升起(结构保留越多)。
|
||
const double tLo = std::max(1.0, gs->median * (1.0 - 0.7 * relax));
|
||
const double tMid = std::max(2.0, gs->p90 * (1.0 - 0.6 * relax));
|
||
const double tHi = std::max(3.0, gs->p99 * (1.0 - 0.4 * relax));
|
||
vtkNew<vtkPiecewiseFunction> grad;
|
||
grad->AddPoint(0.0, floorG);
|
||
grad->AddPoint(tLo, floorG);
|
||
grad->AddPoint(tMid, midG);
|
||
grad->AddPoint(tHi, hiG);
|
||
prop->SetGradientOpacity(grad);
|
||
}
|
||
|
||
// 光照:ShadeOn + Ambient/Diffuse,保留立体明暗;Specular 压到 0.05(近乎关)避免
|
||
// 旋转时视角相关的高光在体表游走形成「移动白斑」。Ambient 由变体控(别太低否则偏暗)。
|
||
if (v.useShade) {
|
||
prop->ShadeOn();
|
||
prop->SetAmbient(v.ambient);
|
||
prop->SetDiffuse(0.7);
|
||
prop->SetSpecular(0.05);
|
||
prop->SetSpecularPower(10.0);
|
||
}
|
||
return prop;
|
||
}
|
||
|
||
// ============================================================================
|
||
// ③ view:真窗口可交互(给用户肉眼测 + 最低配机跑)(Task 12d)
|
||
// ============================================================================
|
||
//
|
||
// 真 vtkRenderWindow + vtkRenderWindowInteractor(TrackballCamera),挂
|
||
// OutOfCoreSource:相机变化时 source.update(camera) 重选 LOD/视野块再渲(确保
|
||
// 拖动/缩放时 LOD 真切换);屏幕左上角 vtkTextActor 实时显示 fps + 当前 level。
|
||
// 默认取景对准局部段 + 默认垂向夸张/不透明度(同 ①)。
|
||
//
|
||
// 离屏 smoke:--smoke 时不开真窗口,只离屏建管线 + 渲一帧 + 验非空像素,确保不崩。
|
||
|
||
// 整卷单张 3D 纹理的轴上限(同 renderLOD/renderB 实测 GL_MAX_3D_TEXTURE_SIZE)。
|
||
constexpr int kViewMax3DTex = 16384;
|
||
|
||
// 单纹理统一渲染(Task 12d-singletex):
|
||
//
|
||
// 交互 view 不再用 vtkMultiBlockVolumeMapper + budget 分块(缺块、个位数 fps)。
|
||
// 任何相机位置都只渲【一张 vtkImageData + 单个 vtkSmartVolumeMapper】,与 --preview
|
||
// 走完全同一条产单图 + 同一 mapper 的路径,保证一致、高 fps(与预览 184fps 同档)。
|
||
//
|
||
// LOD 选层规则(拉近变细、拉远变粗):
|
||
// - 远观/中景(相机选中粗层)→ 升到最细的「整卷各轴 ≤16384」层(本数据 L2:11119、
|
||
// L3:5560),整卷重组成一张纹理,任何缩放都显示完整体,绝不缺块。
|
||
// - 拉近(相机选中 level0/1,X 超 16384 无法整卷成单纹理)→ 取当前视野在 level0 的
|
||
// X 子区域(沿线裁一段,使子体各轴 ≤16384)重组一张纹理。
|
||
// 两条都用现成 buildLevelImage / buildLocalLevel0Image 产单图 → 单 SmartVolumeMapper。
|
||
|
||
// view 的每帧回调共享状态(挂到 interactor 的 EndInteraction 上)。
|
||
//
|
||
// 渲染源 = ViewAdaptiveVolumeSource(C2):每次交互结束 source->update(cam) 用 C1
|
||
// selectLod 选层选区 → 从分块存储重组【当前视野区域单图】→ 喂单 SmartVolumeMapper。
|
||
// 退掉旧 POC 简化路径(viewPickLevel/wholeVolumeLevelFor/viewLocalBrickRange/缓存
|
||
// 三件套/MultiBlock 分块全部由 C1+C2 承担)。
|
||
struct ViewState {
|
||
geopro::render::ViewAdaptiveVolumeSource* source = nullptr;
|
||
vtkSmartVolumeMapper* mapper = nullptr; // 高清层:单 SmartVolumeMapper(叠在底图上)
|
||
vtkSmartVolumeMapper* baseMapper = nullptr; // 常驻底图层 mapper(用于按高清块挖空 cropping)
|
||
vtkRenderer* ren = nullptr;
|
||
vtkCamera* cam = nullptr;
|
||
vtkTextActor* fpsText = nullptr;
|
||
vtkRenderWindow* rw = nullptr;
|
||
double exagg = 8.0;
|
||
int lastLevel = -1;
|
||
std::string dir; // store 目录:首帧直读 level0 局部段(类 gallery),绕开 LOD 选粗层
|
||
// 预建的首帧高清段(level0 沿线中段):cmdView 已为分位标定建好,直接复用喂 mapper,
|
||
// 避免在 viewSetupDefaultFrame 内重复读盘。空则该函数再按 dir 直读。
|
||
vtkSmartPointer<vtkImageData> seedSegImg;
|
||
// 持有当前高清单图引用,避免被释放(mapper 仅持裸指针)。
|
||
vtkSmartPointer<vtkImageData> currentImg;
|
||
// 回调防重入:回调内部会 Render(),若 Render 又触发观察者回调会无限递归。
|
||
bool inCb = false;
|
||
};
|
||
|
||
// C3-8 底图按高清块挖空:用 vtkVolumeMapper 的 Cropping 在【底图 mapper】上裁掉高清块
|
||
// 覆盖的那一块,使任意时刻 = 底图(高清区外) + 高清(高清区内),无空间重叠 → 不再双渲发白。
|
||
//
|
||
// 裁剪平面用高清单图的【模型坐标包围盒】(GetBounds)。底图与高清两 actor 用同一份
|
||
// SetScale(1,1,exagg),且高清单图自带绝对世界 origin(buildLocalLevel0Image 沿 X 偏移),
|
||
// 与底图同坐标系 → 高清块的模型盒平面直接作底图 cropping 平面即对齐(两层 scale 一致)。
|
||
//
|
||
// CroppingRegionFlags:6 个平面把空间分成 3×3×3=27 区,中心区(盒内)= bit 0x0002000
|
||
// (VTK_CROP_SUBVOLUME)。要「渲盒外、挖掉盒内」→ 全 27 区减中心区 = 0x7ffffff & ~0x0002000。
|
||
void viewSyncBaseCropping(ViewState* st) {
|
||
if (st->baseMapper == nullptr) return;
|
||
if (st->currentImg == nullptr) { // 高清未就绪:底图不裁剪,全渲(绝不空白)
|
||
st->baseMapper->SetCropping(0);
|
||
return;
|
||
}
|
||
double b[6];
|
||
st->currentImg->GetBounds(b); // 模型坐标盒(含绝对 X origin),与底图同系
|
||
st->baseMapper->SetCroppingRegionPlanes(b[0], b[1], b[2], b[3], b[4], b[5]);
|
||
st->baseMapper->SetCroppingRegionFlags(0x7ffffff & ~VTK_CROP_SUBVOLUME);
|
||
st->baseMapper->SetCropping(1);
|
||
}
|
||
|
||
// C3-2 非阻塞拉取:把最新已就绪单图喂 mapper(若有新结果)。不阻塞主线程——
|
||
// 后台 builder 没新结果就沿用上一帧(拖动跟手的关键)。返回 1=喂了新图,0=无变化。
|
||
std::size_t viewPickLatest(ViewState* st) {
|
||
auto imgs = st->source->currentImages(); // 内部 takeLatest(非阻塞)
|
||
if (imgs.empty() || imgs[0] == nullptr) return 0; // 无新结果:保留上一帧
|
||
if (imgs[0] == st->currentImg) return 0; // 同一张:无需重喂
|
||
st->currentImg = imgs[0];
|
||
st->lastLevel = st->source->lastLevel();
|
||
st->mapper->SetInputData(st->currentImg);
|
||
st->mapper->Update();
|
||
viewSyncBaseCropping(st); // 高清块换位 → 同步更新底图挖空盒,保持无缝无重叠
|
||
return 1;
|
||
}
|
||
|
||
// 单纹理刷新(C3-2 异步):source->update(cam) 只【提交目标】(非阻塞),随后非阻塞
|
||
// 拉一次最新就绪。拖动中主线程不被重组卡住——新纹理由后台备好、下一帧/定时器换上。
|
||
// 返回当前喂入的块数(1=有就绪单图,0=尚无就绪/视锥外)。
|
||
std::size_t viewRefreshSingle(ViewState* st) {
|
||
st->source->update(st->cam); // 提交目标,立即返回
|
||
viewPickLatest(st);
|
||
return st->currentImg != nullptr ? 1 : 0;
|
||
}
|
||
|
||
// 阻塞式刷新:提交目标后【轮询到就绪】再返回(带超时)。仅用于 preview/smoke/默认
|
||
// 取景这类「需要保证拿到一张图」的离屏/初始化场景——交互路径绝不用此(会卡主线程)。
|
||
std::size_t viewRefreshBlocking(ViewState* st, int maxTries = 3000,
|
||
int sleepMs = 2) {
|
||
st->source->update(st->cam); // 提交目标
|
||
for (int i = 0; i < maxTries; ++i) {
|
||
if (viewPickLatest(st)) return 1;
|
||
if (st->currentImg != nullptr) return 1; // 已有上一就绪且无新结果
|
||
std::this_thread::sleep_for(std::chrono::milliseconds(sleepMs));
|
||
}
|
||
return st->currentImg != nullptr ? 1 : 0;
|
||
}
|
||
|
||
// interactor 回调:每次交互(旋转/缩放)结束后重选 LOD + 刷新 fps 文本。
|
||
//
|
||
// fps 修复(Task 12d-fix3):之前用 frameTimer(上次回调到本次的墙钟)算 fps,把
|
||
// 用户思考/不动的空闲时间也算进去,显示的是「空闲间隔」(如 0.2fps),不可信。改为
|
||
// 松手时连渲 kFpsProbeFrames 帧、累计「实际 Render 耗时」取均值,得到真实渲染帧率。
|
||
void viewOnInteract(vtkObject*, unsigned long, void* clientData, void*) {
|
||
auto* st = static_cast<ViewState*>(clientData);
|
||
// 防重入:本回调内部会 st->rw->Render(),若该 Render 再触发观察者进本回调
|
||
// 将无限递归。已在回调中则直接返回(双保险)。
|
||
if (st->inCb) return;
|
||
st->inCb = true;
|
||
// EndInteraction 时重选 LOD + 重组单图(仅松手触发一次,避免拖动中卡)。
|
||
const std::size_t blocks = viewRefreshSingle(st);
|
||
st->ren->ResetCameraClippingRange();
|
||
const int lvl = st->lastLevel;
|
||
|
||
// 真实渲染帧率:连渲若干帧,只累计 Render() 本身耗时(不含空闲)。首帧含切换后
|
||
// 的纹理上传/shader 编译,故跑 kFpsProbeFrames 帧取均值更可信。
|
||
constexpr int kFpsProbeFrames = 3;
|
||
Stopwatch swR;
|
||
for (int i = 0; i < kFpsProbeFrames; ++i) st->rw->Render();
|
||
const double renderMs = swR.elapsedMs() / kFpsProbeFrames;
|
||
const double fps = renderMs > 0 ? 1000.0 / renderMs : 0.0;
|
||
|
||
char buf[256];
|
||
std::snprintf(buf, sizeof(buf),
|
||
"fps: %.1f | LOD level: %d | blocks: %zu | exagg: %.0fx",
|
||
fps, lvl, blocks, st->exagg);
|
||
st->fpsText->SetInput(buf);
|
||
st->lastLevel = lvl;
|
||
st->rw->Render(); // 末帧带上更新后的 fps 文本
|
||
st->inCb = false;
|
||
}
|
||
|
||
// C3-2 拖动跟手核心:交互进行中(旋转/缩放每次相机变化)只【提交目标】(非阻塞),
|
||
// 绝不在主线程重组——主线程立刻继续响应输入,画面用上一张已就绪纹理(跟手)。
|
||
void viewOnInteracting(vtkObject*, unsigned long, void* clientData, void*) {
|
||
auto* st = static_cast<ViewState*>(clientData);
|
||
st->source->update(st->cam); // 提交最新视野目标,立即返回(supersede 旧目标)
|
||
}
|
||
|
||
// C3-2 定时器:周期性非阻塞拉取后台已就绪的新纹理换上 → 拖动中/松手后新 LOD 备好
|
||
// 即自然显示,主线程从不被重组卡住。无新结果则什么也不做(不重渲、不抖)。
|
||
void viewOnTimer(vtkObject* caller, unsigned long, void* clientData, void*) {
|
||
auto* st = static_cast<ViewState*>(clientData);
|
||
if (st->inCb) return; // 与 fps 探针回调互斥,避免重入
|
||
if (viewPickLatest(st)) {
|
||
st->ren->ResetCameraClippingRange();
|
||
st->rw->Render(); // 仅在确有新纹理时重渲
|
||
}
|
||
(void)caller;
|
||
}
|
||
|
||
// 默认取景宽度:沿测线取约 256 道(=4 brick 列×64)的一段作首帧局部段。整线横截面
|
||
// 相对长度 1:34,框整卷只会看到一条隐形细带;框这个局部段,层状结构才充满视野
|
||
// (用户可再滚轮拉远看整体——细带是物理真实,拉近看细节)。段越宽 X 越细长、截面
|
||
// 越填不满画面;256 道是 ① cmdTune 出 lod-tuned-local.png(有清晰层状结构)的取景,
|
||
// 沿用之以保证首帧同等可读。
|
||
constexpr int kViewDefaultLocalBricks = 4;
|
||
|
||
// 建立 view 的「默认取景」:把 level0 一段局部体(沿线中段)整卷单块喂 mapper,再
|
||
// ResetCamera 到该局部段(actor 已 SetScale(1,exagg,exagg)),置相机为能看出层状
|
||
// 结构的角度。真窗口 / --smoke / --preview 三条路径共用此函数 → 渲的是同一画面。
|
||
// 取景角度(Elevation/Azimuth/Zoom)取自 kViewDefaultVariant(var4),与 view-var4 一致。
|
||
//
|
||
// 返回喂给 mapper 的块数(=1)。同步更新 st->lastLevel=0(默认即全分辨率局部段)。
|
||
std::size_t viewSetupDefaultFrame(ViewState* st, vtkRenderer* ren) {
|
||
geopro::render::ViewAdaptiveVolumeSource& source = *st->source;
|
||
const geopro::data::StoreMeta& m = source.meta();
|
||
|
||
// 首帧默认取景:先把相机放到沿线中段一个局部窗口(~kViewDefaultLocalBricks 列)
|
||
// 正前方近观 → 触发 C1 选 level0 + 视野子区间,C2 重组该视野单图。先用「局部段
|
||
// 包围盒」ResetCamera 把相机框到该段,再 source->update(cam) 让 C1/C2 选区重组。
|
||
const int brick = m.brick;
|
||
const int totBricksX = (m.nx + brick - 1) / brick;
|
||
const int localBx = std::min(kViewDefaultLocalBricks, totBricksX);
|
||
const int bx0 = std::max(0, totBricksX / 2 - localBx / 2); // 沿线中段
|
||
// 该局部段世界 X 范围(level0,brick 列 [bx0, bx0+localBx))。把首帧相机框到这一段
|
||
// (而非整卷)是 P3 修复 #2 的关键:相机近观局部段 → C1 selectLod 选 level0 局部子区,
|
||
// C2 重组该段单图,framing 该段 → 14×796 截面 + 沿线一段充满视野(类厚 B-scan)。
|
||
// 框整卷则 selectLod 选最粗层(整条 45305 细带)、看着空白。
|
||
const double segX0 = m.origin[0] + bx0 * brick * m.spacing[0];
|
||
const double segX1 =
|
||
m.origin[0] + std::min(m.nx, (bx0 + localBx) * brick) * m.spacing[0];
|
||
// 段(exagg 后)世界尺寸与中心 + 包围球半径。X 取该段宽(非整卷),Y/Z 全幅(薄轴)。
|
||
const double wx = std::max(1.0, segX1 - segX0);
|
||
const double wy = std::max(1.0, m.ny * m.spacing[1] * st->exagg);
|
||
const double wz = std::max(1.0, m.nz * m.spacing[2] * st->exagg);
|
||
const double cx = 0.5 * (segX0 + segX1);
|
||
const double cy = m.origin[1] + 0.5 * wy;
|
||
const double cz = m.origin[2] + 0.5 * wz;
|
||
const double radius = 0.5 * std::sqrt(wx * wx + wy * wy + wz * wz);
|
||
|
||
// 相机从 +Y 看段中心(看进【X-Z 宽面=B-scan 墙】),距离 = 半径/tan(半视角)×余量。
|
||
// 段几何 = X≈12.6m 宽 × Y≈1.5m 薄(跨通道) × Z 深(exagg 后高)。exagg 只夸张深度(Z),
|
||
// Y 仍真实极薄 → 若从 +X(沿线)看只见薄前缘、近空。改从 +Y 俯看宽 X-Z 面: GPR 水平
|
||
// 分层沿 X 铺开、随 Z 叠层,这一面才读得出内部结构。整条概览靠用户滚轮拉远(细带几何必然)。
|
||
st->cam = ren->GetActiveCamera();
|
||
const double fovY = st->cam->GetViewAngle();
|
||
const double halfAngle = 0.5 * fovY * 3.14159265358979 / 180.0;
|
||
const double tanH = std::max(1e-3, std::tan(halfAngle));
|
||
const double dist = radius / tanH * 1.4; // 1.4:留余量含 aspect/边缘
|
||
st->cam->SetFocalPoint(cx, cy, cz);
|
||
st->cam->SetPosition(cx, cy + dist, cz); // +Y 视点 → 正对 X-Z 宽面
|
||
st->cam->SetViewUp(0, 0, 1); // Z 朝上(深度向下)
|
||
ren->ResetCameraClippingRange();
|
||
|
||
// 首帧高清段直读(P3 修复 #2 核心):异步 LOD 源在「框一段」的视距下仍会选最粗层
|
||
// (整条 45305 细带 → 看着近黑),不可取。改为【直接从 store 读 level0 沿线中段子体】
|
||
// (与 gallery 的 buildLocalLevel0Image 同一直读路径,非 LOD 算法),喂高清 mapper —
|
||
// 保证首帧就是「全分辨率一段」的清晰块体。后续交互仍由异步源接管(用户拉远/拖动按
|
||
// 视距正常选层)。退化(读不到段)再回退异步阻塞刷新。
|
||
std::size_t blocks = 0;
|
||
{
|
||
vtkSmartPointer<vtkImageData> locImg = st->seedSegImg; // cmdView 预建,优先复用
|
||
if (locImg == nullptr && !st->dir.empty()) { // 退化:按 dir 直读
|
||
geopro::data::ChunkedVolumeStore store(st->dir);
|
||
locImg = buildLocalLevel0Image(store, m, bx0, localBx);
|
||
}
|
||
if (locImg != nullptr) {
|
||
st->currentImg = locImg;
|
||
st->lastLevel = 0;
|
||
st->mapper->SetInputData(locImg);
|
||
st->mapper->Update();
|
||
viewSyncBaseCropping(st); // 底图按该段挖空,无缝叠加
|
||
blocks = 1;
|
||
}
|
||
}
|
||
if (blocks == 0) blocks = viewRefreshBlocking(st); // 退化:回退异步源
|
||
|
||
// 框住【局部段】:无参 ResetCamera 会按场景全部 actor(含常驻整卷底图,45305 长)的
|
||
// 包围盒框 → 整条细带、截面填不满 → 看着空白。改为只框高清段(currentImg=level0 沿
|
||
// 线中段子体)的包围盒,使 14×796 截面+沿线一段充满视野(类厚 B-scan);整条概览靠用户
|
||
// 滚轮拉远(细带是 1:34 几何必然,非 bug)。Z 轴按 actor 的 SetScale(1,1,exagg) 同步夸张。
|
||
if (st->currentImg != nullptr) {
|
||
double b[6];
|
||
st->currentImg->GetBounds(b); // 高清段模型坐标盒(含绝对 X origin)
|
||
b[4] *= st->exagg; // Z 下界随深度夸张
|
||
b[5] *= st->exagg; // Z 上界随深度夸张
|
||
ren->ResetCamera(b); // 只框该段 → 段充满画面
|
||
} else {
|
||
ren->ResetCamera(); // 退化(无高清段):回退全场景框
|
||
}
|
||
// 取景角度:默认相机已置 +Y 正对 X-Z 宽面。var4 的 El45/Az30 是为 gallery 的 +X /
|
||
// Y 也夸张几何调的,套到这里(+Y 视、Y 真实极薄)会让宽面强烈斜退、大片黑。改用为本
|
||
// +Y B-scan 几何调的小角度(El/Az 各 ~15-18°)留 3D 立体感而宽面仍基本正对,Zoom 拉足
|
||
// 让 X-Z 面填满。仅默认/交互/preview 取景,不动 var4 gallery 参数。
|
||
constexpr double kDefaultFrameElevation = 18.0; // 轻俯,见顶面薄边显层叠
|
||
constexpr double kDefaultFrameAzimuth = 15.0; // 轻偏,宽面仍基本正对
|
||
constexpr double kDefaultFrameZoom = 1.1; // ResetCamera 已贴合该段,只略收边距
|
||
st->cam = ren->GetActiveCamera();
|
||
st->cam->Elevation(kDefaultFrameElevation);
|
||
st->cam->Azimuth(kDefaultFrameAzimuth);
|
||
st->cam->Zoom(kDefaultFrameZoom);
|
||
ren->ResetCameraClippingRange();
|
||
return blocks;
|
||
}
|
||
|
||
// 渲一组画廊变体并存 PNG,报告 结构像素 / 平均亮度 / fps。返回 0=OK。
|
||
// shotDirOverride 非空 → PNG 存到该目录(P4 让 gallery 出图落在 store 同目录,便于对照)。
|
||
int runGalleryVariant(const std::string& dir, const GalleryVariant& v,
|
||
int frames, const std::string& shotDirOverride = "") {
|
||
const int winW = 1280, winH = 800;
|
||
geopro::data::ChunkedVolumeStore store(dir);
|
||
const geopro::data::StoreMeta& m = store.meta();
|
||
|
||
auto rw = makeOffscreenWindow(winW, winH);
|
||
vtkNew<vtkRenderer> ren;
|
||
ren->SetBackground(v.bg[0], v.bg[1], v.bg[2]);
|
||
rw->AddRenderer(ren);
|
||
|
||
// 局部段(沿线中段,同 viewSetupDefaultFrame 的取段法)。先建体 → 实测梯度分布
|
||
// 用于 C4 梯度不透明度标定(仅开了 useGradientOpacity 的变体需要)。
|
||
const int totBx = store.bricksX(0);
|
||
const int localBx = std::min(kViewDefaultLocalBricks, totBx);
|
||
const int bx0 = std::max(0, totBx / 2 - localBx / 2);
|
||
vtkSmartPointer<vtkImageData> locImg =
|
||
buildLocalLevel0Image(store, m, bx0, localBx);
|
||
|
||
// P3 传函分位标定:色阶/不透明度端点按该局部段【实际值 2%/98% 分位】裁离群。
|
||
double vmin = m.vminPhys, vmax = m.vmaxPhys;
|
||
{
|
||
const ScalarPercentiles pc =
|
||
sampleScalarPercentiles(locImg.Get(), m.quant, 0.02, 0.98);
|
||
if (pc.samples > 0) {
|
||
vmin = pc.lo;
|
||
vmax = pc.hi;
|
||
std::cout << "[gallery " << v.name << "] 传函分位标定(样本 " << pc.samples
|
||
<< "): 2%=" << vmin << " 98%=" << vmax << " (全域 ["
|
||
<< m.vminPhys << ", " << m.vmaxPhys << "])\n";
|
||
}
|
||
}
|
||
const geopro::core::ColorScale cs = pickColor(v.color, vmin, vmax);
|
||
|
||
GradStats gs;
|
||
if (v.useGradientOpacity) {
|
||
gs = sampleGradientMagnitude(locImg.Get());
|
||
std::cout << "[gallery " << v.name << "] 梯度幅值分布(量化域,样本 " << gs.samples
|
||
<< "): median=" << gs.median << " p90=" << gs.p90
|
||
<< " p99=" << gs.p99 << " max=" << gs.mx << "\n";
|
||
}
|
||
|
||
vtkSmartPointer<vtkVolumeProperty> prop = makeVariantProperty(
|
||
v, m.quant, cs, vmin, vmax, v.maxOpacity,
|
||
v.useGradientOpacity ? &gs : nullptr);
|
||
|
||
vtkNew<vtkMultiBlockVolumeMapper> mapper;
|
||
mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
|
||
auto volume = vtkSmartPointer<vtkVolume>::New();
|
||
volume->SetMapper(mapper);
|
||
volume->SetProperty(prop);
|
||
volume->SetScale(1.0, v.exagg, v.exagg);
|
||
ren->AddVolume(volume);
|
||
|
||
std::vector<vtkSmartPointer<vtkImageData>> one{locImg};
|
||
auto mb = makeMultiBlock(one);
|
||
mapper->SetInputDataObject(mb);
|
||
mapper->Update();
|
||
|
||
ren->ResetCamera();
|
||
vtkCamera* cam = ren->GetActiveCamera();
|
||
cam->Elevation(v.elevation);
|
||
cam->Azimuth(v.azimuth);
|
||
cam->Zoom(v.zoom);
|
||
ren->ResetCameraClippingRange();
|
||
|
||
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
|
||
vtkOutputWindow::SetInstance(capWin);
|
||
rw->Render();
|
||
|
||
const fs::path shotDir =
|
||
shotDirOverride.empty()
|
||
? fs::path("docs") / "superpowers" / "plans" / "poc-lod-shots"
|
||
: fs::path(shotDirOverride);
|
||
fs::create_directories(shotDir);
|
||
const std::string pngPath =
|
||
(shotDir / (std::string("view-") + v.name + ".png")).string();
|
||
savePng(rw.Get(), pngPath);
|
||
|
||
// 结构像素:任一通道 >50(排除暗背景),度量「确有体结构」。
|
||
auto countStructPixels = [&]() -> vtkIdType {
|
||
auto px = vtkSmartPointer<vtkUnsignedCharArray>::New();
|
||
rw->GetRGBACharPixelData(0, 0, winW - 1, winH - 1, /*front=*/1, px);
|
||
vtkIdType n = 0;
|
||
const vtkIdType np = px->GetNumberOfTuples();
|
||
for (vtkIdType i = 0; i < np; ++i) {
|
||
if (px->GetComponent(i, 0) > 50 || px->GetComponent(i, 1) > 50 ||
|
||
px->GetComponent(i, 2) > 50) {
|
||
++n;
|
||
}
|
||
}
|
||
return n;
|
||
};
|
||
const vtkIdType structPx = countStructPixels();
|
||
const double bright = meanBrightness(rw.Get(), winW, winH);
|
||
|
||
rw->Render(); // 预热再测 fps
|
||
Stopwatch sw;
|
||
for (int f = 0; f < frames; ++f) {
|
||
cam->Azimuth(360.0 / frames);
|
||
rw->Render();
|
||
}
|
||
const double ms = sw.elapsedMs();
|
||
const double fps = ms > 0 ? frames * 1000.0 / ms : 0.0;
|
||
const bool texErr = capWin->textureError();
|
||
vtkOutputWindow::SetInstance(nullptr);
|
||
const bool ok = !texErr && structPx > 0;
|
||
|
||
std::cout << "\n--- gallery " << v.name << " ---\n";
|
||
std::cout << "参数 : " << v.desc << "\n";
|
||
std::cout << "存图 : " << pngPath << "\n";
|
||
std::cout << "结构像素(>50) : " << structPx << " / " << (winW * winH)
|
||
<< " (" << (100.0 * structPx / (winW * winH)) << "%)\n";
|
||
std::cout << "平均亮度(0-255) : " << bright << "\n";
|
||
std::cout << "真实 fps : " << (ok ? std::to_string(fps) : "INVALID")
|
||
<< " (" << frames << " 帧旋相机)\n";
|
||
std::cout << "结果 : " << (ok ? "OK" : "FAIL") << "\n";
|
||
|
||
writeMetricLine(
|
||
"view-gallery," + std::string(v.name) + ",dir=" + dir +
|
||
",profile=" + (v.profile == OpacityProfile::kSolid ? "solid" : "struct") +
|
||
",floor=" + std::to_string(v.floorOpacity) +
|
||
",mid=" + std::to_string(v.midOpacity) +
|
||
",max=" + std::to_string(v.maxOpacity) +
|
||
",exagg=" + std::to_string(v.exagg) +
|
||
",el=" + std::to_string(v.elevation) +
|
||
",az=" + std::to_string(v.azimuth) + ",zoom=" + std::to_string(v.zoom) +
|
||
",structPx=" + std::to_string(structPx) +
|
||
",bright=" + std::to_string(bright) +
|
||
",fps=" + (ok ? std::to_string(fps) : "INVALID") +
|
||
",png=" + pngPath);
|
||
return ok ? 0 : 1;
|
||
}
|
||
|
||
// view --gallery:依次渲全部 4 组变体。shotDir 空 → 默认 docs/.../poc-lod-shots;
|
||
// 否则存到 shotDir(P4:默认传 store 目录,4 张图落在 tmp/line001_proc)。
|
||
int cmdViewGallery(const std::string& dir, int frames,
|
||
const std::string& shotDir = "") {
|
||
std::cout << "[view --gallery] storeDir=" << dir << " frames=" << frames
|
||
<< " shotDir=" << (shotDir.empty() ? "(默认)" : shotDir)
|
||
<< "\n[view --gallery] 离屏闸门复检...\n";
|
||
if (cmdOffscreenSmoke() != 0) {
|
||
std::cout << "[view --gallery] 闸门失败,中止。\n";
|
||
return 1;
|
||
}
|
||
int rc = 0;
|
||
for (const auto& v : kGalleryVariants) {
|
||
if (runGalleryVariant(dir, v, frames, shotDir) != 0) rc = 1;
|
||
}
|
||
const std::string outDesc =
|
||
shotDir.empty() ? "docs/superpowers/plans/poc-lod-shots" : shotDir;
|
||
std::cout << "\n[view --gallery] 完成,4 张图存于 " << outDesc
|
||
<< "/view-var{1..4}.png\n";
|
||
return rc;
|
||
}
|
||
|
||
// buildLineProperty 定义在 view-all 段(本函数之后)→ 前置声明,供 slice 渲染体用。
|
||
vtkSmartPointer<vtkVolumeProperty> buildLineProperty(
|
||
const geopro::data::StoreMeta& m, vtkImageData* basis, double sharedVmin,
|
||
double sharedVmax);
|
||
|
||
// ============================================================================
|
||
// slice:复用桌面端 SliceTool(同一份 geopro_render 代码,非重写)在 GPR 体上切片
|
||
// ============================================================================
|
||
// 加载一条线的体图 → 开真窗口 → 挂 SliceTool(4 种 axis:updown=深度切片C-scan/
|
||
// frontback=纵向剖面radargram/leftright=横向剖面/oblique=任意斜切)。切面可拖动/旋转/滚轮推进。
|
||
int cmdSlice(int argc, char** argv) {
|
||
using geopro::render::interact::SliceAxis;
|
||
using geopro::render::interact::SliceTool;
|
||
const Args a = parseArgs(argc, argv, 2); // 跳过 exe[0] + 命令名[1]
|
||
if (a.positional.empty()) {
|
||
std::cerr << "用法: gpr_poc slice <storeDir> [--axis updown|frontback|leftright|oblique] "
|
||
"[--exagg 8] [--bgSuppress 0.5]\n"
|
||
" 渲染半透明三维体 + 切面(在体内切,移动看不同剖面,同桌面端)。\n"
|
||
" updown=深度切片C-scan(水平面), frontback=纵向剖面radargram(沿线竖直面),\n"
|
||
" leftright=横向剖面(垂直线竖直面), oblique=任意角度斜切。\n"
|
||
" bgSuppress 越大体越透(更易看清体内切面)。\n";
|
||
return 2;
|
||
}
|
||
const std::string storeDir = a.positional[0];
|
||
const std::string axisStr = a.get("axis", "frontback");
|
||
const double exagg = std::stod(a.get("exagg", "8"));
|
||
const std::string shot = a.get("shot", ""); // 非空=离屏出图验证(不开真窗口)
|
||
|
||
geopro::render::ViewAdaptiveVolumeSource source(storeDir, /*exagg=*/1.0);
|
||
source.setAspect(1400.0 / 900.0);
|
||
source.setViewportHeight(900);
|
||
vtkImageData* base = source.baseImage();
|
||
if (base == nullptr) {
|
||
std::cerr << "[slice] 底图为空,无法切片(store 有效?)\n";
|
||
return 1;
|
||
}
|
||
const geopro::data::StoreMeta& meta = source.meta();
|
||
|
||
// 色阶与体绘制同口径(2/98 分位 + pickColor),保证切片配色与三维体一致。
|
||
const GalleryVariant& v = kViewDefaultVariant;
|
||
double vmin = meta.vminPhys, vmax = meta.vmaxPhys;
|
||
const ScalarPercentiles pc =
|
||
sampleScalarPercentiles(base, meta.quant, 0.02, 0.98);
|
||
if (pc.samples > 0) {
|
||
vmin = pc.lo;
|
||
vmax = pc.hi;
|
||
}
|
||
const geopro::core::ColorScale cs = pickColor(v.color, vmin, vmax);
|
||
|
||
// 把 exagg 烤进 Z spacing(显示用,与桌面端"VE 烤入 origin/spacing"同口径)。
|
||
// ShallowCopy 不动 source 的底图;SliceTool 持非拥有指针,img 在本函数作用域内保活。
|
||
auto img = vtkSmartPointer<vtkImageData>::New();
|
||
img->ShallowCopy(base);
|
||
double sp[3];
|
||
img->GetSpacing(sp);
|
||
img->SetSpacing(sp[0], sp[1], sp[2] * exagg);
|
||
|
||
SliceAxis axis = SliceAxis::UpDown;
|
||
if (axisStr == "frontback") axis = SliceAxis::FrontBack;
|
||
else if (axisStr == "leftright") axis = SliceAxis::LeftRight;
|
||
else if (axisStr == "oblique") axis = SliceAxis::Oblique;
|
||
|
||
vtkNew<vtkRenderer> ren;
|
||
ren->SetBackground(0.06, 0.06, 0.09);
|
||
vtkNew<vtkRenderWindow> rw;
|
||
rw->AddRenderer(ren);
|
||
rw->SetSize(1400, 900);
|
||
rw->SetWindowName("gpr_poc slice —— 三维体 + 切面(复用桌面端 SliceTool)");
|
||
if (!shot.empty()) rw->SetOffScreenRendering(1); // 验证模式:离屏
|
||
|
||
// 【关键】渲染三维体本身——桌面端切片是"在渲染出的三维体上切",切面在体内切、移动看不同剖面。
|
||
// 体设半透明(bgSuppress 默认压背景)→ 能透过体看见体内的切面。
|
||
gBgSuppress = std::clamp(std::stod(a.get("bgSuppress", "0.5")), 0.0, 1.0);
|
||
vtkSmartPointer<vtkVolumeProperty> volProp = buildLineProperty(meta, base, vmin, vmax);
|
||
vtkNew<vtkGPUVolumeRayCastMapper> volMapper;
|
||
volMapper->SetInputData(img);
|
||
vtkNew<vtkVolume> vol;
|
||
vol->SetMapper(volMapper);
|
||
vol->SetProperty(volProp);
|
||
ren->AddVolume(vol);
|
||
|
||
// 体包围盒轮廓(上下文:看切面在体里的位置)。
|
||
vtkNew<vtkOutlineFilter> outline;
|
||
outline->SetInputData(img);
|
||
vtkNew<vtkPolyDataMapper> omap;
|
||
omap->SetInputConnection(outline->GetOutputPort());
|
||
vtkNew<vtkActor> oact;
|
||
oact->SetMapper(omap);
|
||
oact->GetProperty()->SetColor(0.4, 0.4, 0.5);
|
||
ren->AddActor(oact);
|
||
|
||
vtkNew<vtkRenderWindowInteractor> iren;
|
||
iren->SetRenderWindow(rw);
|
||
vtkNew<vtkInteractorStyleTrackballCamera> style;
|
||
iren->SetInteractorStyle(style);
|
||
iren->Initialize(); // SliceTool 构造即 On(),须先有活 interactor
|
||
|
||
// 复用桌面端 SliceTool(geopro_render 同一份代码)。
|
||
SliceTool slice(img, iren, axis, cs, vmin, vmax);
|
||
|
||
int d[3];
|
||
img->GetDimensions(d);
|
||
std::cout << "[slice] axis=" << axisStr << " 体维度=" << d[0] << "x" << d[1]
|
||
<< "x" << d[2] << " 值域=[" << vmin << "," << vmax << "]\n";
|
||
|
||
// 相机正对切面(沿法向看,否则极扁的体侧视成一条线、看不到切片纹理)。
|
||
double bnd[6];
|
||
img->GetBounds(bnd);
|
||
const double cx = 0.5 * (bnd[0] + bnd[1]), cy = 0.5 * (bnd[2] + bnd[3]),
|
||
cz = 0.5 * (bnd[4] + bnd[5]);
|
||
vtkCamera* cam = ren->GetActiveCamera();
|
||
cam->SetFocalPoint(cx, cy, cz);
|
||
if (axis == SliceAxis::UpDown) { // X-Y 面,法向 Z → 俯视
|
||
cam->SetPosition(cx, cy, cz + 1.0);
|
||
cam->SetViewUp(0, 1, 0);
|
||
} else if (axis == SliceAxis::LeftRight) { // Y-Z 面,法向 X → 侧视
|
||
cam->SetPosition(cx + 1.0, cy, cz);
|
||
cam->SetViewUp(0, 0, 1);
|
||
} else { // FrontBack/Oblique:X-Z 面,法向 Y → 正视(radargram)
|
||
cam->SetPosition(cx, cy - 1.0, cz);
|
||
cam->SetViewUp(0, 0, 1);
|
||
}
|
||
ren->ResetCamera(); // 沿该朝向拟合距离框住体
|
||
if (!shot.empty()) {
|
||
rw->Render();
|
||
savePng(rw, shot);
|
||
std::cout << "[slice] 离屏出图: " << shot << "(axis=" << axisStr << ")\n";
|
||
return 0;
|
||
}
|
||
std::cout << "[slice] 打开真窗口(半透明三维体 + 切面)。左键旋转 / 滚轮缩放 / "
|
||
"拖切面移动看不同剖面 / q 退出。\n";
|
||
rw->Render();
|
||
iren->Start();
|
||
// 干净拆除:先 Off 切面 widget,再 Finalize GL 上下文,避免关窗时 widget 在死上下文上
|
||
// 重建 shader 报错(depth-blit / texture 告警)。
|
||
slice.close();
|
||
rw->Finalize();
|
||
std::cout << "[slice] 窗口关闭,退出。\n";
|
||
return 0;
|
||
}
|
||
|
||
// ============================================================================
|
||
// C-scan 深度切片(--slice):在 20 条合成体上加"水平深度切面"——每条线贡献它在该深度的
|
||
// 水平幅值图,按各自世界变换摆进场景 → 拼成"整条路某深度的俯视图"(GPR 找管线/空洞的主力视图)。
|
||
// 深度方向(Z)在"绕竖直 Z 旋转 + 平移"的摆放下不变,故一个世界深度面 = 各线同一 z 切片,
|
||
// 直接用 vtkImageActor(显示 z=k 切片) + SetUserTransform(线的世界变换) 摆位,无需斜切 reslice。
|
||
// 键盘 Up/Down 改深度 → 扫过不同深度。逐线真实数据、不合并、空隙透明。
|
||
// ============================================================================
|
||
// 全局世界切面:定义【一个】世界轴对齐的薄板(slab),把【每条线】用各自 worldInv 作 ResliceTransform
|
||
// reslice 到这【同一个世界面】上采样(覆盖外=透明),再 blend 合成一张 → 真正一整片切面。
|
||
// 整片沿扫描轴移动 = 所有线被同一个面同步扫过(无论朝向)。
|
||
// depth(updown):沿 world Z 扫、面=X-Y;cross(leftright):沿 world Y 扫、面=X-Z(横穿所有线);
|
||
// radargram(frontback):沿 world X 扫、面=Y-Z。
|
||
struct GlobalSlice {
|
||
// 竖直剖面(横切/顺路):全局面 reslice 各线 → 同一世界面。
|
||
std::vector<vtkSmartPointer<vtkImageReslice>> reslices;
|
||
// 深度(水平):逐线整张水平切面(共面、全覆盖、原生分辨率)。lineActors 非空即此模式。
|
||
std::vector<vtkSmartPointer<vtkImageActor>> lineActors;
|
||
std::vector<vtkSmartPointer<vtkTransform>> winv; // 各线 world→local(算深度 z 索引)
|
||
std::vector<std::array<double, 3>> lorg, lspc;
|
||
std::vector<std::array<int, 3>> ldim;
|
||
int sweepAxis = 1; // world 扫描轴 0=X 1=Y 2=Z
|
||
int inA = 0, inB = 2; // 面内两个 world 轴
|
||
double sweepWorld = 0, sweepMin = 0, sweepMax = 1;
|
||
double footLo[3] = {0, 0, 0}, footHi[3] = {0, 0, 0}; // footprint AABB(取景用)
|
||
vtkSmartPointer<vtkPlaneSource> planeSrc; // 可见的切面矩形(淡),让"面"看得见
|
||
vtkRenderWindow* rw = nullptr;
|
||
std::vector<vtkVolume*> volumes;
|
||
double volUnit = 20.0;
|
||
bool volOn = true;
|
||
|
||
void applySlice() {
|
||
if (!lineActors.empty()) { // 深度逐线模式:每线设 z=k 那一片(共面、全覆盖)
|
||
for (std::size_t i = 0; i < lineActors.size(); ++i) {
|
||
double W[3] = {0.5 * (footLo[0] + footHi[0]), 0.5 * (footLo[1] + footHi[1]),
|
||
0.5 * (footLo[2] + footHi[2])};
|
||
W[sweepAxis] = sweepWorld;
|
||
double L[3];
|
||
winv[i]->TransformPoint(W, L);
|
||
const int k = std::clamp(
|
||
static_cast<int>(std::lround((L[2] - lorg[i][2]) / lspc[i][2])), 0,
|
||
ldim[i][2] - 1);
|
||
lineActors[i]->SetDisplayExtent(0, ldim[i][0] - 1, 0, ldim[i][1] - 1, k, k);
|
||
}
|
||
} else {
|
||
for (auto& rs : reslices) {
|
||
double o[3];
|
||
rs->GetOutputOrigin(o);
|
||
o[sweepAxis] = sweepWorld; // 整片世界面沿扫描轴移动(所有线同步)
|
||
rs->SetOutputOrigin(o);
|
||
}
|
||
}
|
||
if (planeSrc) { // 同步移动可见的切面矩形
|
||
double o[3] = {footLo[0], footLo[1], footLo[2]};
|
||
o[sweepAxis] = sweepWorld;
|
||
double p1[3] = {o[0], o[1], o[2]}, p2[3] = {o[0], o[1], o[2]};
|
||
p1[inA] = footHi[inA];
|
||
p2[inB] = footHi[inB];
|
||
planeSrc->SetOrigin(o);
|
||
planeSrc->SetPoint1(p1);
|
||
planeSrc->SetPoint2(p2);
|
||
}
|
||
if (rw) rw->Render();
|
||
}
|
||
void applyVolume() {
|
||
for (auto* v : volumes) {
|
||
if (v == nullptr) continue;
|
||
v->SetVisibility(volOn ? 1 : 0);
|
||
if (v->GetProperty())
|
||
v->GetProperty()->SetScalarOpacityUnitDistance(volUnit);
|
||
}
|
||
if (rw) rw->Render();
|
||
}
|
||
};
|
||
|
||
void cscanOnKey(vtkObject* caller, unsigned long, void* cd, void*) {
|
||
auto* st = static_cast<GlobalSlice*>(cd);
|
||
auto* iren = static_cast<vtkRenderWindowInteractor*>(caller);
|
||
const std::string key = iren->GetKeySym() ? iren->GetKeySym() : "";
|
||
const double step = (st->sweepMax - st->sweepMin) * 0.02;
|
||
if (key == "Up") {
|
||
st->sweepWorld = std::min(st->sweepMax, st->sweepWorld + step);
|
||
st->applySlice();
|
||
} else if (key == "Down") {
|
||
st->sweepWorld = std::max(st->sweepMin, st->sweepWorld - step);
|
||
st->applySlice();
|
||
} else if (key == "v" || key == "V") {
|
||
st->volOn = !st->volOn;
|
||
st->applyVolume();
|
||
} else if (key == "bracketright") {
|
||
st->volUnit *= 1.5;
|
||
st->applyVolume();
|
||
} else if (key == "bracketleft") {
|
||
st->volUnit = std::max(0.5, st->volUnit / 1.5);
|
||
st->applyVolume();
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// view-all:全部独立体按精确 CGCS2000 坐标/朝向摆进同一 3D 场景一起渲(测区全貌)(P7→P9)
|
||
// ============================================================================
|
||
//
|
||
// 对应客户端「选多个 ds 一起生成三维」:每条线是独立密实 coarse 体(线局部坐标 X=沿测线、
|
||
// Y=通道横向、Z=深度,origin≈0),本命令把它们按各自 .gps 真实位置/航向摆进同一世界框。
|
||
//
|
||
// P9 升级:起点投影从 lonLatToLocalM(简化等距投影)换成 CoordinateTransform::wgs84ToCgcs2000
|
||
// (CGCS2000 高斯-克吕格 3°带,与 P8 测绘级桥接同口径),公共带号取首条线首点经度推断,
|
||
// 全线共用同一带号 → 同一参考系;公共世界原点 = 全体 CGCS2000 最小东/北。
|
||
// - 每条线刚体变换:平移到该线 .gps 起点 CGCS2000 局部米 + 绕竖直 Z 轴转该线航向角
|
||
// (起→止主方向,CGCS2000 系),使体局部 X(沿测线)对齐真实航向、Y(横向)垂直、Z 竖直;
|
||
// - 深度 Z 用 exagg 夸张(只 Z);
|
||
// - 每条体加载为一张整卷 vtkImageData(密实体小,按 --level 选层整体上纹理,不走 LOD),
|
||
// 套上该线 vtkTransform,全加进同一 renderer 一起渲。
|
||
// 传函/配色用 P4 默认醒目版(var4:增强灰度 + 实体包络 + 梯度门 + 光照),逐体按 2/98 分位标定。
|
||
//
|
||
// --preview 离屏出俯视(top) + 斜视(oblique)两张图展示 20 条并排成测区;否则开真窗口可转可缩。
|
||
|
||
// 一条线在世界中的摆放(刚体变换 + 该线的视野自适应引擎源)。
|
||
//
|
||
// P11:每条线不再整卷加载固定层,而是各持一个 ViewAdaptiveVolumeSource(LOD+视锥
|
||
// 裁剪+异步重组引擎,与单条 view 完全同款)。引擎 exagg=1.0(不烘几何),垂向夸张/
|
||
// 航向/平移全由世界变换 T 承担(与单条 view 把 exagg 放 actor SetScale 同理)。每帧
|
||
// 把世界相机逆变换到该线局部帧喂引擎 → selectLod 选层选区(视锥外→引擎不提交)。
|
||
struct PlacedSource {
|
||
std::string name; // 明星路_NNN
|
||
std::unique_ptr<geopro::render::ViewAdaptiveVolumeSource> source;
|
||
geopro::data::StoreMeta meta; // 量化/几何
|
||
double startX = 0, startY = 0; // 起点局部米(相对公共原点)
|
||
double headingDeg = 0; // 航向角(度,相对 +X 东向)
|
||
double spreadX = 0, spreadY = 0; // 可选横向铺开偏移
|
||
|
||
vtkSmartPointer<vtkTransform> world; // T:Scale(1,1,exagg)→RotateZ→Translate
|
||
vtkSmartPointer<vtkTransform> worldInv; // T⁻¹(相机逆变换到局部帧)
|
||
vtkSmartPointer<vtkVolumeProperty> prop; // 逐线 2/98 分位标定的传函
|
||
// 单遍合成(方案 A):每条线仍是【独立插值】的体,但 20 条不再各自一个 mapper(=20 遍
|
||
// ray-cast,物理 20× 卡死),而是全部作为【同一个 vtkGPUVolumeRayCastMapper 的不同
|
||
// 端口】注册进一个 vtkMultiVolume → 单遍 ray-cast 一次性合成(重叠也只穿一遍)。
|
||
// ps.volume 只承载该端口的世界变换 T + 逐线传函(不再持自己的 mapper);该端口的
|
||
// vtkImageData 由引擎按相机选的 LOD 决定,经 multiMapper->SetInputDataObject(port,img) 换上。
|
||
vtkSmartPointer<vtkVolume> volume; // 该线的体(套 T + 逐线 prop)
|
||
vtkSmartPointer<vtkImageData> currentImg; // 持当前单图引用(mapper 仅持裸指针)
|
||
|
||
// LOD 中心架构(确诊后):每条线【独立】自己的 mapper(passcost 证明 N 遍开销温和),
|
||
// 配视野 LOD 引擎换区 + 停手才重建。弃 multi-volume 单遍(它为单遍关掉了 LOD→渲整卷大贴图→卡)。
|
||
vtkSmartPointer<vtkGPUVolumeRayCastMapper> mapper; // 每线独立 GPU mapper(弃 multi-volume 单遍)
|
||
vtkGPUVolumeRayCastMapper* multiMapper = nullptr; // 旧 multi-volume 残留(保编译,不再用)
|
||
vtkMultiVolume* multiVol = nullptr; // 旧残留
|
||
int port = 0; // 旧残留
|
||
|
||
vtkSmartPointer<vtkImageData> fullYImg; // §3:全 Y 密度底图(通道 LOD 抽平面的源)
|
||
int yStride = 1; // §3:当前通道维 LOD stride(1=全密度)
|
||
|
||
double worldBounds[6] = {0, 0, 0, 0, 0, 0}; // 该线(含 T+底图盒)的世界 AABB(取景用)
|
||
};
|
||
|
||
// 由 起点+航向+Z 夸张 → 世界刚体变换 T。
|
||
// 合成顺序(VTK PostMultiply:先加先施于点):Scale(1,1,exagg)→RotateZ→Translate,
|
||
// 即点先按 exagg 拉伸 Z(局部深度),再绕竖直轴转真实航向,再平移到世界起点。
|
||
vtkSmartPointer<vtkTransform> makeLineTransform(double startX, double startY,
|
||
double headingDeg,
|
||
double spreadX, double spreadY,
|
||
double exagg) {
|
||
auto xf = vtkSmartPointer<vtkTransform>::New();
|
||
xf->PostMultiply();
|
||
xf->Scale(1.0, 1.0, exagg);
|
||
xf->RotateZ(headingDeg);
|
||
xf->Translate(startX + spreadX, startY + spreadY, 0.0);
|
||
return xf;
|
||
}
|
||
|
||
// 逐线传函:从该线常驻底图(整卷代表)实测 2/98 分位标定色阶/不透明度端点 + 梯度门,
|
||
// 与单条 view 的传函标定同口径(底图非空恒可标定;退化回退全量化域)。
|
||
vtkSmartPointer<vtkVolumeProperty> buildLineProperty(
|
||
const geopro::data::StoreMeta& m, vtkImageData* basis,
|
||
double sharedVmin = 0.0, double sharedVmax = 0.0) {
|
||
const GalleryVariant& v = kViewDefaultVariant; // P4 默认醒目版(var4)
|
||
double vmin = m.vminPhys, vmax = m.vmaxPhys;
|
||
if (sharedVmin < sharedVmax) {
|
||
// #6:传入【统一物理幅值范围】→ 同一幅值在所有线上映射到同一颜色(跨线可比)。
|
||
vmin = sharedVmin;
|
||
vmax = sharedVmax;
|
||
} else if (basis != nullptr) {
|
||
const ScalarPercentiles pc =
|
||
sampleScalarPercentiles(basis, m.quant, 0.02, 0.98);
|
||
if (pc.samples > 0) {
|
||
vmin = pc.lo;
|
||
vmax = pc.hi;
|
||
}
|
||
}
|
||
const geopro::core::ColorScale cs = pickColor(v.color, vmin, vmax);
|
||
GradStats gs;
|
||
if (v.useGradientOpacity && basis != nullptr) gs = sampleGradientMagnitude(basis);
|
||
return makeVariantProperty(v, m.quant, cs, vmin, vmax, v.maxOpacity,
|
||
(v.useGradientOpacity && gs.samples > 0) ? &gs
|
||
: nullptr);
|
||
}
|
||
|
||
// 根因验证用:把一张体沿 Y(通道维)抽半 → 同范围、同 X/Z/origin,仅 Y 平面数减半、
|
||
// Y spacing 翻倍。这正是"通道 LOD 换密度"的等价改动(同范围、不同 Y 密度),用来确定
|
||
// 就地换端口贴图在 multi-volume 里能否算对。
|
||
vtkSmartPointer<vtkImageData> downsampleY(vtkImageData* in) {
|
||
int d[3];
|
||
in->GetDimensions(d);
|
||
double sp[3], org[3];
|
||
in->GetSpacing(sp);
|
||
in->GetOrigin(org);
|
||
const int ny2 = std::max(1, (d[1] + 1) / 2);
|
||
auto out = vtkSmartPointer<vtkImageData>::New();
|
||
out->SetDimensions(d[0], ny2, d[2]);
|
||
out->SetOrigin(org);
|
||
out->SetSpacing(sp[0], sp[1] * 2.0, sp[2]); // Y 间距翻倍(平面数减半)
|
||
vtkNew<vtkShortArray> arr;
|
||
arr->SetName("v");
|
||
arr->SetNumberOfTuples(static_cast<vtkIdType>(d[0]) * ny2 * d[2]);
|
||
auto* inArr = vtkShortArray::SafeDownCast(in->GetPointData()->GetScalars());
|
||
for (int k = 0; k < d[2]; ++k)
|
||
for (int j2 = 0; j2 < ny2; ++j2) {
|
||
const int j = std::min(j2 * 2, d[1] - 1);
|
||
for (int i = 0; i < d[0]; ++i) {
|
||
const vtkIdType idIn =
|
||
(static_cast<vtkIdType>(k) * d[1] + j) * d[0] + i;
|
||
const vtkIdType idOut =
|
||
(static_cast<vtkIdType>(k) * ny2 + j2) * d[0] + i;
|
||
arr->SetValue(idOut, inArr ? inArr->GetValue(idIn) : 0);
|
||
}
|
||
}
|
||
out->GetPointData()->SetScalars(arr);
|
||
return out;
|
||
}
|
||
|
||
// §3 通道维 LOD:沿 Y(通道维)按 stride 抽平面 → 同范围、保包围盒(ny'×spY×stride≈ny×spY),
|
||
// stride=1 即原图。stride∈{1,2,4,8} → 显示 全/半/1-4/1-8 通道密度。bbox 不变 → multi-volume
|
||
// 就地换不破坏(已 --swapTest 验证)。stride==1 时直接返回原图(零拷贝)。
|
||
vtkSmartPointer<vtkImageData> subsampleYStride(vtkImageData* in, int stride) {
|
||
if (in == nullptr || stride <= 1) return in;
|
||
int d[3];
|
||
in->GetDimensions(d);
|
||
double sp[3], org[3];
|
||
in->GetSpacing(sp);
|
||
in->GetOrigin(org);
|
||
const int ny2 = std::max(1, (d[1] + stride - 1) / stride);
|
||
auto out = vtkSmartPointer<vtkImageData>::New();
|
||
out->SetDimensions(d[0], ny2, d[2]);
|
||
out->SetOrigin(org);
|
||
out->SetSpacing(sp[0], sp[1] * stride, sp[2]); // Y 间距 ×stride → 跨度不变
|
||
vtkNew<vtkShortArray> arr;
|
||
arr->SetName("v");
|
||
arr->SetNumberOfTuples(static_cast<vtkIdType>(d[0]) * ny2 * d[2]);
|
||
auto* inArr = vtkShortArray::SafeDownCast(in->GetPointData()->GetScalars());
|
||
for (int k = 0; k < d[2]; ++k)
|
||
for (int j2 = 0; j2 < ny2; ++j2) {
|
||
const int j = std::min(j2 * stride, d[1] - 1);
|
||
for (int i = 0; i < d[0]; ++i) {
|
||
const vtkIdType idIn =
|
||
(static_cast<vtkIdType>(k) * d[1] + j) * d[0] + i;
|
||
const vtkIdType idOut =
|
||
(static_cast<vtkIdType>(k) * ny2 + j2) * d[0] + i;
|
||
arr->SetValue(idOut, inArr ? inArr->GetValue(idIn) : 0);
|
||
}
|
||
}
|
||
out->GetPointData()->SetScalars(arr);
|
||
return out;
|
||
}
|
||
|
||
// 根因验证用:把一张体沿 X 裁掉后半 → 范围/包围盒改变(模拟引擎换"子区域"),用来确认
|
||
// "改包围盒的换图"才是破坏 multi-volume 的真凶。
|
||
vtkSmartPointer<vtkImageData> cropXhalf(vtkImageData* in) {
|
||
int d[3];
|
||
in->GetDimensions(d);
|
||
double sp[3], org[3];
|
||
in->GetSpacing(sp);
|
||
in->GetOrigin(org);
|
||
const int nx2 = std::max(1, d[0] / 2);
|
||
auto out = vtkSmartPointer<vtkImageData>::New();
|
||
out->SetDimensions(nx2, d[1], d[2]);
|
||
out->SetOrigin(org); // 保 origin,但 X 范围缩到一半 → 包围盒变
|
||
out->SetSpacing(sp);
|
||
vtkNew<vtkShortArray> arr;
|
||
arr->SetName("v");
|
||
arr->SetNumberOfTuples(static_cast<vtkIdType>(nx2) * d[1] * d[2]);
|
||
auto* inArr = vtkShortArray::SafeDownCast(in->GetPointData()->GetScalars());
|
||
for (int k = 0; k < d[2]; ++k)
|
||
for (int j = 0; j < d[1]; ++j)
|
||
for (int i = 0; i < nx2; ++i) {
|
||
const vtkIdType idIn = (static_cast<vtkIdType>(k) * d[1] + j) * d[0] + i;
|
||
const vtkIdType idOut = (static_cast<vtkIdType>(k) * d[1] + j) * nx2 + i;
|
||
arr->SetValue(idOut, inArr ? inArr->GetValue(idIn) : 0);
|
||
}
|
||
out->GetPointData()->SetScalars(arr);
|
||
return out;
|
||
}
|
||
|
||
// #6 色标图例:按【统一物理幅值范围】+ 默认配色建一个屏幕右侧色阶条(含刻度),让用户
|
||
// 知道颜色代表多大的幅值。配色与体属性同源(pickColor),故图例颜色与体内颜色一致。
|
||
vtkSmartPointer<vtkScalarBarActor> buildScalarBar(double vmin, double vmax) {
|
||
const GalleryVariant& v = kViewDefaultVariant;
|
||
const geopro::core::ColorScale cs = pickColor(v.color, vmin, vmax);
|
||
auto lut = vtkSmartPointer<vtkColorTransferFunction>::New();
|
||
constexpr int N = 64;
|
||
for (int t = 0; t < N; ++t) {
|
||
const double phys = vmin + (vmax - vmin) * t / (N - 1);
|
||
const auto c = cs.colorAt(phys);
|
||
lut->AddRGBPoint(phys, c.r / 255.0, c.g / 255.0, c.b / 255.0);
|
||
}
|
||
auto bar = vtkSmartPointer<vtkScalarBarActor>::New();
|
||
bar->SetLookupTable(lut);
|
||
bar->SetTitle("Amplitude"); // ASCII(默认字体无 CJK,避免乱码)
|
||
bar->SetNumberOfLabels(5);
|
||
bar->SetMaximumWidthInPixels(80);
|
||
bar->GetTitleTextProperty()->SetColor(1, 1, 1);
|
||
bar->GetLabelTextProperty()->SetColor(1, 1, 1);
|
||
bar->SetPosition(0.91, 0.28);
|
||
bar->SetWidth(0.08);
|
||
bar->SetHeight(0.46);
|
||
return bar;
|
||
}
|
||
|
||
// 把世界相机参数逆变换到某线局部帧(T⁻¹):pos/focal 是点(含平移逆),up 是方向
|
||
// (仅旋转逆,TransformVector 不含平移)。再调引擎 updateView 选层选区(视锥外→引擎
|
||
// 内部 selectLod 判 empty → 不提交,保留上一就绪/无图)。
|
||
// 诊断开关:置 1 时跳过引擎选层/换图,全程只渲各线 baseImage(粗整卷)。
|
||
// 用于隔离"首帧连续→引擎换图后断开/消失"——若 baseOnly 始终连续,则根因是引擎换图。
|
||
bool gViewAllBaseOnly = false;
|
||
|
||
// 根因验证开关:就地换端口贴图后施加何种"失效/重算"策略,看能否让 multi-volume 算对。
|
||
// 0=不做(基线,已知会断开) 1=multiVol.Modified() 2=重 SetVolume+Modified
|
||
// 3=2 + mapper.Modified() 4=3 + 强制 multiVol.GetBounds() 重算
|
||
int gViewAllSwapFix = 0;
|
||
|
||
// §3 通道维 LOD:按相机距离选 Y stride(远疏近密),保包围盒就地换 Y 平面子集。--chanLod 0 关。
|
||
bool gChanLod = true;
|
||
|
||
void viewAllSubmitOneLine(PlacedSource& ps, vtkCamera* worldCam,
|
||
double aspect, int viewportH) {
|
||
if (gViewAllBaseOnly || worldCam == nullptr) return;
|
||
double wp[3], wf[3], wu[3];
|
||
worldCam->GetPosition(wp);
|
||
worldCam->GetFocalPoint(wf);
|
||
worldCam->GetViewUp(wu);
|
||
|
||
geopro::render::CameraView c{};
|
||
ps.worldInv->TransformPoint(wp, c.pos);
|
||
ps.worldInv->TransformPoint(wf, c.focal);
|
||
ps.worldInv->TransformVector(wu, c.up);
|
||
c.fovYDeg = worldCam->GetViewAngle();
|
||
c.aspect = aspect;
|
||
c.viewportH = viewportH;
|
||
|
||
// 引擎 exagg=1.0:局部帧几何无夸张(夸张在 T 里),故喂引擎 volumeView 默认即可。
|
||
ps.source->setAspect(aspect);
|
||
ps.source->setViewportHeight(viewportH);
|
||
ps.source->updateView(c, geopro::render::VolumeView{
|
||
ps.meta.nx, ps.meta.ny, ps.meta.nz,
|
||
ps.meta.brick, ps.source->levelCount(),
|
||
{ps.meta.origin[0], ps.meta.origin[1],
|
||
ps.meta.origin[2]},
|
||
{ps.meta.spacing[0], ps.meta.spacing[1],
|
||
ps.meta.spacing[2]},
|
||
1.0});
|
||
}
|
||
|
||
// 非阻塞拉取该线后台已就绪的引擎单图(视野 LOD 选区),换上该线【自己的 mapper】
|
||
// (无新结果→沿用上一帧)。返回 1=换上新图。引擎选 LOD(远→粗 whole,近→细局部小区)。
|
||
// 各线独立 mapper → 换"改包围盒的子区域"安全(无 multi-volume 可破坏)。
|
||
int viewAllPickOneLine(PlacedSource& ps) {
|
||
if (gViewAllBaseOnly) return 0; // 诊断:不换图,保持 baseImage
|
||
auto imgs = ps.source->currentImages(); // 内部 takeLatest(非阻塞)
|
||
if (imgs.empty() || imgs[0] == nullptr) return 0;
|
||
if (imgs[0] == ps.currentImg) return 0;
|
||
ps.currentImg = imgs[0];
|
||
if (ps.mapper) {
|
||
ps.mapper->SetInputData(ps.currentImg);
|
||
ps.mapper->Update();
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
// view-all 每帧驱动共享状态(挂 interactor 回调)。
|
||
struct ViewAllState {
|
||
std::vector<PlacedSource>* lines = nullptr;
|
||
std::vector<vtkSmartPointer<vtkGPUVolumeRayCastMapper>>* mappers = nullptr;
|
||
vtkRenderer* ren = nullptr;
|
||
vtkCamera* cam = nullptr;
|
||
vtkRenderWindow* rw = nullptr;
|
||
vtkTextActor* fpsText = nullptr;
|
||
double aspect = 1400.0 / 900.0;
|
||
int viewportH = 900;
|
||
double dragImgSample = 4.0; // 交互态屏幕降采样倍率(--maxImgSample)
|
||
double sampleDist = 0.3; // 静止态沿光线步长(世界米,--sampleDist;越大越快越糙)
|
||
double dragSampleMul = 3.0; // 交互态沿光线步长再放大倍数(--dragSampleMul)
|
||
bool lowQ = false; // 当前是否处于低质(交互)态
|
||
int idleTicks = 0; // 连续无交互的定时器 tick 数(>=阈值 → 恢复全质量)
|
||
bool inCb = false;
|
||
};
|
||
|
||
// 交互态(拖动/滚轮):屏幕降采样(1/dragImgSample) + 沿光线步长加粗(sampleDist×dragSampleMul)。
|
||
// 关键:AutoAdjust 只降屏幕、不降沿光线步长;20 个体每步采 20 次,沿光线步长才是大头,
|
||
// 故交互时必须手动把沿光线步长也加粗(否则拖动仍卡)。
|
||
void viewAllSetWheelCoarse(ViewAllState* st) {
|
||
if (!st->mappers) return;
|
||
for (auto& mm : *st->mappers) {
|
||
mm->SetImageSampleDistance(st->dragImgSample);
|
||
mm->SetSampleDistance(static_cast<float>(st->sampleDist * st->dragSampleMul));
|
||
}
|
||
st->lowQ = true;
|
||
}
|
||
|
||
// 静止态:全屏幕分辨率 + 沿光线步长回到 sampleDist(清晰)。AutoAdjust 全程关(手动控)。
|
||
void viewAllRestoreAdaptive(ViewAllState* st) {
|
||
if (!st->mappers) return;
|
||
for (auto& mm : *st->mappers) {
|
||
mm->SetImageSampleDistance(1.0);
|
||
mm->SetSampleDistance(static_cast<float>(st->sampleDist));
|
||
}
|
||
st->lowQ = false;
|
||
}
|
||
|
||
// §3 通道维 LOD:按相机距离/场景对角线选 Y stride(远→疏省、近→全密度)。
|
||
int viewAllChanStride(vtkRenderer* ren) {
|
||
double b[6];
|
||
ren->ComputeVisiblePropBounds(b);
|
||
const double diag = std::sqrt((b[1] - b[0]) * (b[1] - b[0]) +
|
||
(b[3] - b[2]) * (b[3] - b[2]) +
|
||
(b[5] - b[4]) * (b[5] - b[4]));
|
||
const double dist = ren->GetActiveCamera()->GetDistance();
|
||
const double r = diag > 1e-9 ? dist / diag : 1.0;
|
||
if (r > 1.4) return 8; // 远 → 半通道密度(7 平面/56)
|
||
if (r > 0.8) return 4; // 中 → 原始通道(14)
|
||
if (r > 0.45) return 2; // 近 → 原始+1(28)
|
||
return 1; // 最近 → 全插值(56)
|
||
}
|
||
|
||
// 按当前相机选 stride,对各线就地换 Y 平面子集(保包围盒 → multi-volume 不破坏)。
|
||
// 返回换图线数(>0 需重渲)。
|
||
int viewAllApplyChanLod(ViewAllState* st) {
|
||
if (!gChanLod) return 0;
|
||
const int stride = viewAllChanStride(st->ren);
|
||
int changed = 0;
|
||
for (PlacedSource& ps : *st->lines) {
|
||
if (ps.fullYImg == nullptr || ps.yStride == stride) continue;
|
||
ps.yStride = stride;
|
||
ps.currentImg = subsampleYStride(ps.fullYImg, stride);
|
||
if (ps.multiMapper)
|
||
ps.multiMapper->SetInputDataObject(ps.port, ps.currentImg);
|
||
++changed; // 保包围盒换图,无需失效重算
|
||
}
|
||
return changed;
|
||
}
|
||
|
||
// 对所有线提交引擎目标(非阻塞,按当前相机各自选 LOD)。
|
||
// 方案 A 起:20 条共享【单个】vtkGPUVolumeRayCastMapper 单遍合成,重叠也只穿一遍 →
|
||
// 不再需要视锥裁剪削 20× 开销(且各线 AABB 都覆盖全路、互相重叠,裁剪本就几乎无效)。
|
||
void viewAllSubmitTargets(ViewAllState* st) {
|
||
for (PlacedSource& ps : *st->lines)
|
||
viewAllSubmitOneLine(ps, st->cam, st->aspect, st->viewportH);
|
||
}
|
||
|
||
// 拖动进行中(InteractionEvent):质量交给 AutoAdjust 自适应(够快就清晰,不够才降)。
|
||
// 拖动中:进入交互态低质(屏幕 + 沿光线步长都加粗)+ 重置裁剪范围(防旋转裁掉)。绝不重建纹理。
|
||
void viewAllOnInteracting(vtkObject*, unsigned long, void* clientData, void*) {
|
||
auto* st = static_cast<ViewAllState*>(clientData);
|
||
if (!st->lowQ) viewAllSetWheelCoarse(st); // 进入交互态低质(拖动跟手)
|
||
st->idleTicks = 0;
|
||
if (st->ren) st->ren->ResetCameraClippingRange();
|
||
}
|
||
|
||
// 滚轮缩放(MouseWheelForward/BackwardEvent):立刻固定低分辨率(AutoAdjust 单帧来不及
|
||
// 自适应,故手动降)。高优先级先于 style 的 Dolly+Render → 滚轮那一帧即低质快渲。
|
||
void viewAllOnWheel(vtkObject*, unsigned long, void* clientData, void*) {
|
||
auto* st = static_cast<ViewAllState*>(clientData);
|
||
viewAllSetWheelCoarse(st);
|
||
st->idleTicks = 0;
|
||
if (st->ren) st->ren->ResetCameraClippingRange();
|
||
}
|
||
|
||
// 定时器:滚轮停手约 idle 后恢复自适应 + 全质量重渲。
|
||
void viewAllOnTimer(vtkObject*, unsigned long, void* clientData, void*) {
|
||
auto* st = static_cast<ViewAllState*>(clientData);
|
||
if (st->inCb) return;
|
||
if (st->lowQ && ++st->idleTicks >= 3) { // ~3×定时器周期无交互 → 恢复
|
||
viewAllRestoreAdaptive(st);
|
||
viewAllApplyChanLod(st); // §3:滚轮缩放停手后按新距离重选通道密度
|
||
st->ren->ResetCameraClippingRange();
|
||
st->rw->Render();
|
||
}
|
||
}
|
||
|
||
// 拖动结束(EndInteractionEvent):恢复自适应(静止 AutoAdjust→全质量)+ 刷新 fps。
|
||
void viewAllOnInteract(vtkObject*, unsigned long, void* clientData, void*) {
|
||
auto* st = static_cast<ViewAllState*>(clientData);
|
||
if (st->inCb) return;
|
||
st->inCb = true;
|
||
viewAllRestoreAdaptive(st);
|
||
viewAllApplyChanLod(st); // §3:拖动结束后按当前距离重选通道密度
|
||
st->idleTicks = 0;
|
||
st->ren->ResetCameraClippingRange();
|
||
|
||
Stopwatch swR;
|
||
st->rw->Render(); // 一帧全质量;其耗时即静止 fps
|
||
const double fps = swR.elapsedMs() > 0 ? 1000.0 / swR.elapsedMs() : 0.0;
|
||
char buf[256];
|
||
std::snprintf(buf, sizeof(buf),
|
||
"fps(静止): %.1f | lines: %d | 单遍合成(multi-volume)", fps,
|
||
static_cast<int>(st->lines->size()));
|
||
if (st->fpsText) st->fpsText->SetInput(buf);
|
||
st->rw->Render();
|
||
st->inCb = false;
|
||
}
|
||
|
||
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);
|
||
// --maxPerPass N:每个 vtkMultiVolume 单遍合成最多挂几条体(受 GPU 每着色器纹理单元
|
||
// 上限约束,每体约吃 4 个单元)。0=自动按 GPU 纹理单元数推断(推荐)。超过的线分成
|
||
// 多个 multi-volume 包,每包一遍 → 总遍数=ceil(N/K)(远少于"每线一遍"的 N 遍)。
|
||
const int maxPerPassArg = std::stoi(a.get("maxPerPass", "0"));
|
||
// LOD 中心架构(passcost 确诊后):各线独立 mapper → 引擎换 LOD 子区【安全】(无 multi-volume
|
||
// 可破坏)。故视野 LOD【默认开】,单帧渲染量随视野走、与 20 条总量解耦。--baseOnly 仅诊断用
|
||
// (锁定整卷底图、关引擎换图)。
|
||
gViewAllBaseOnly = a.kv.count("baseOnly") != 0;
|
||
gViewAllSwapFix = std::stoi(a.get("swapFix", "0")); // 旧 multi-volume 换图诊断(已无意义,保留不碍)
|
||
// 引擎金字塔已逐级降 Y,无需单独抽 Y 平面 → 通道维 LOD【默认关】(--chanLod 1 可重开)。
|
||
gChanLod = std::stoi(a.get("chanLod", "0")) != 0;
|
||
// --bgSuppress F(0..1):压背景、突出反射层(传函压低近零背景 + 中心透明死区)。
|
||
// 0=原观感(背景可见);0.3~0.6 让总览干净;过大(>0.7)会连带抹掉弱反射(弱异常靠切片抓)。
|
||
gBgSuppress = std::clamp(std::stod(a.get("bgSuppress", "0")), 0.0, 1.0);
|
||
// --maxImgSample F:拖动态屏幕采样距离上限(AutoAdjust 拖动时最多降到 1/F 分辨率,
|
||
// 越大越快越糊;松手自动恢复全质量)。默认 4。拉近卡顿可调大(如 8)。
|
||
// LOD 已扛住性能(概览/拉近都够快)→ 拖动【默认不降质,保持清晰】。降质纯 opt-in 兜底:
|
||
// --maxImgSample F(拖动屏幕降到 1/F,默认 1=不降)、--dragSampleMul M(拖动步长×M,默认 1=不变)。
|
||
// 仅当低端机拖动仍卡时才调大它们。
|
||
const double maxImgSample = std::stod(a.get("maxImgSample", "1"));
|
||
// --sampleDist D:静止态沿光线步长(世界米)。通道插值后 Y 很密,自动步长会算得过细→巨卡,
|
||
// 故固定一个合理值。越大越快越糙;卡时调大(0.5/1.0),太糊/丢层调小(0.15)。
|
||
const double sampleDist = std::stod(a.get("sampleDist", "0.3"));
|
||
const double dragSampleMul = std::stod(a.get("dragSampleMul", "1"));
|
||
|
||
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;
|
||
// P9:CGCS2000 公共参考。带号由首条可用线首点经度推断(3°带),全线共用同一带号 →
|
||
// 同一高斯-克吕格平面参考系;公共原点取全体投影后最小东/北(数值减到较小,避免大坐标精度损失)。
|
||
constexpr double kDeg2Rad = 3.14159265358979323846 / 180.0;
|
||
int cgcsZone = 0;
|
||
bool zoneSet = false;
|
||
double minEast = std::numeric_limits<double>::infinity();
|
||
double minNorth = 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;
|
||
}
|
||
// 带号:首条可用线首点经度推断(与 P8 Gpr3dvSurveyVolumeBridge 同口径),全线共用。
|
||
if (!zoneSet) {
|
||
cgcsZone = static_cast<int>(std::lround(tr.pts.front().lon / 3.0));
|
||
zoneSet = true;
|
||
}
|
||
for (const auto& p : tr.pts) {
|
||
double cx = 0.0, cy = 0.0; // cx=北(northing), cy=东(easting,含带号)
|
||
CoordinateTransform::wgs84ToCgcs2000(p.lat * kDeg2Rad, p.lon * kDeg2Rad,
|
||
cgcsZone, cx, cy);
|
||
minEast = std::min(minEast, cy);
|
||
minNorth = std::min(minNorth, cx);
|
||
}
|
||
gpsList.push_back({nm, num, gpsPath, std::move(tr)});
|
||
}
|
||
if (gpsList.empty()) {
|
||
std::cerr << "[view-all] 错误: 无任何线同时具备体与可用 .gps\n";
|
||
return 1;
|
||
}
|
||
std::cout << "[view-all] CGCS2000 带号=" << cgcsZone
|
||
<< " 公共世界原点(最小东/北)=(" << std::fixed << minEast << ", "
|
||
<< minNorth << ")" << std::defaultfloat << " (共 " << gpsList.size()
|
||
<< " 线参与)\n";
|
||
if (spread <= 0.0) {
|
||
std::cout << "[view-all] 提示: --spread=0 用纯真实 GPS 位置;本工区为同一条路重复多趟、"
|
||
"横向仅约数十米,真实位置下多趟高度重叠会叠成一条带。"
|
||
"如需把各趟铺开成可分辨的并排测区,加 --spread 60。\n";
|
||
}
|
||
|
||
// 3) 逐线:算起点局部米 + 航向 + 建 ViewAdaptiveVolumeSource 引擎 → PlacedSource。
|
||
// P11:不再整卷固定层加载(撞 GL 16384 纹理墙),改为每线一个视野自适应引擎,
|
||
// 引擎恒产 ≤16384 单纹理(LOD+视野选区),exagg/航向/平移由世界变换 T 承担。
|
||
const int winW = 1400, winH = 900;
|
||
const double aspect = static_cast<double>(winW) / winH;
|
||
std::vector<PlacedSource> lines;
|
||
int lineIdx = 0;
|
||
for (const LineGps& lg : gpsList) {
|
||
// 轨迹 → CGCS2000 局部米(投影到公共带号后减公共原点)。XY 约定 x=东、y=北。
|
||
std::vector<geopro::io::gpr::XY> trackM;
|
||
trackM.reserve(lg.track.pts.size());
|
||
for (const auto& p : lg.track.pts) {
|
||
double cx = 0.0, cy = 0.0; // cx=北, cy=东
|
||
CoordinateTransform::wgs84ToCgcs2000(p.lat * kDeg2Rad, p.lon * kDeg2Rad,
|
||
cgcsZone, cx, cy);
|
||
trackM.push_back(geopro::io::gpr::XY{cy - minEast, cx - minNorth});
|
||
}
|
||
|
||
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;
|
||
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;
|
||
}
|
||
++lineIdx;
|
||
|
||
const std::string storePath = (fs::path(storesDir) / lg.name).string();
|
||
|
||
PlacedSource ps;
|
||
ps.name = lg.name;
|
||
// 引擎 exagg=1.0:垂向夸张放进世界变换 T(与单条 view 把 exagg 放 actor 同理)。
|
||
ps.source = std::make_unique<geopro::render::ViewAdaptiveVolumeSource>(
|
||
storePath, /*exagg=*/1.0);
|
||
ps.source->setAspect(aspect);
|
||
ps.source->setViewportHeight(winH);
|
||
ps.meta = ps.source->meta();
|
||
ps.startX = start.x;
|
||
ps.startY = start.y;
|
||
ps.headingDeg = headingDeg;
|
||
ps.spreadX = spreadX;
|
||
ps.spreadY = spreadY;
|
||
|
||
// 世界变换 T + 逆变换(相机逆变换到局部帧)。
|
||
ps.world = makeLineTransform(start.x, start.y, headingDeg, spreadX, spreadY,
|
||
exagg);
|
||
ps.worldInv = vtkSmartPointer<vtkTransform>::New();
|
||
ps.worldInv->DeepCopy(ps.world);
|
||
ps.worldInv->Inverse();
|
||
|
||
// 逐线传函(从常驻底图标定)。P12:每线只建【一个】体(套世界变换 T),
|
||
// 起步喂粗底图(小且不空),引擎备好更合适 LOD 单图后整图换上。
|
||
ps.prop = buildLineProperty(ps.meta, ps.source->baseImage());
|
||
|
||
// LOD 中心:每条线建【自己的】GPU mapper,起步喂 baseImage,引擎选区后换上。
|
||
// 手动控质(与旧 multi-volume 同口径):AutoAdjust 关、静止步长 sampleDist。
|
||
ps.mapper = vtkSmartPointer<vtkGPUVolumeRayCastMapper>::New();
|
||
ps.mapper->SetAutoAdjustSampleDistances(0);
|
||
ps.mapper->SetImageSampleDistance(1.0);
|
||
ps.mapper->SetSampleDistance(static_cast<float>(sampleDist));
|
||
ps.volume = vtkSmartPointer<vtkVolume>::New();
|
||
if (ps.source->baseImage() != nullptr) {
|
||
ps.currentImg = ps.source->baseImage(); // 起步即有图(不空白)
|
||
ps.fullYImg = ps.source->baseImage();
|
||
ps.mapper->SetInputData(ps.currentImg);
|
||
ps.mapper->Update();
|
||
}
|
||
ps.volume->SetMapper(ps.mapper);
|
||
ps.volume->SetProperty(ps.prop);
|
||
ps.volume->SetUserTransform(ps.world);
|
||
|
||
// 该线世界 AABB(底图模型盒经 T 变换的 8 角包络)→ 视锥裁剪用。
|
||
if (ps.source->baseImage() != nullptr) {
|
||
double mb[6];
|
||
ps.source->baseImage()->GetBounds(mb);
|
||
double lo[3] = {std::numeric_limits<double>::infinity(),
|
||
std::numeric_limits<double>::infinity(),
|
||
std::numeric_limits<double>::infinity()};
|
||
double hi[3] = {-std::numeric_limits<double>::infinity(),
|
||
-std::numeric_limits<double>::infinity(),
|
||
-std::numeric_limits<double>::infinity()};
|
||
for (int cx = 0; cx < 2; ++cx)
|
||
for (int cy = 0; cy < 2; ++cy)
|
||
for (int cz = 0; cz < 2; ++cz) {
|
||
double in[3] = {mb[cx], mb[2 + cy], mb[4 + cz]}, out[3];
|
||
ps.world->TransformPoint(in, out);
|
||
for (int d = 0; d < 3; ++d) {
|
||
lo[d] = std::min(lo[d], out[d]);
|
||
hi[d] = std::max(hi[d], out[d]);
|
||
}
|
||
}
|
||
ps.worldBounds[0] = lo[0]; ps.worldBounds[1] = hi[0];
|
||
ps.worldBounds[2] = lo[1]; ps.worldBounds[3] = hi[1];
|
||
ps.worldBounds[4] = lo[2]; ps.worldBounds[5] = hi[2];
|
||
}
|
||
|
||
int bd[3] = {0, 0, 0};
|
||
if (ps.source->baseImage()) ps.source->baseImage()->GetDimensions(bd);
|
||
std::cout << "[view-all] " << lg.name << " 引擎底图 level="
|
||
<< ps.source->baseLevel() << " 底图维度=" << bd[0] << "x" << bd[1]
|
||
<< "x" << bd[2] << " 起点局部米=(" << start.x << ", " << start.y
|
||
<< ") 航向=" << headingDeg << "°\n";
|
||
lines.push_back(std::move(ps));
|
||
}
|
||
std::cout << "[view-all] 加载并定位线数=" << lines.size()
|
||
<< "(每线一个 ViewAdaptiveVolumeSource 引擎)\n";
|
||
|
||
// 诊断(--overlapStat):实测一条俯视光线真实穿几个体——把各线世界 AABB 投到 X-Y 平面,
|
||
// 在公共 footprint 上撒细网格,统计每格被几条线的 AABB 覆盖(=该处俯视光线穿的体数)。
|
||
// 纯几何、不渲染。验证"20× 重叠"是真是假(§7.1)。
|
||
if (a.kv.count("overlapStat") > 0) {
|
||
double mnx = 1e30, mny = 1e30, mxx = -1e30, mxy = -1e30;
|
||
for (const auto& ps : lines) {
|
||
mnx = std::min(mnx, ps.worldBounds[0]); mxx = std::max(mxx, ps.worldBounds[1]);
|
||
mny = std::min(mny, ps.worldBounds[2]); mxy = std::max(mxy, ps.worldBounds[3]);
|
||
}
|
||
const int G = 400; // 网格分辨率
|
||
const double sx = (mxx - mnx) / G, sy = (mxy - mny) / G;
|
||
long covered = 0, sumCov = 0, maxCov = 0;
|
||
std::vector<long> hist(static_cast<std::size_t>(lines.size()) + 1, 0);
|
||
for (int j = 0; j < G; ++j)
|
||
for (int i = 0; i < G; ++i) {
|
||
const double cx = mnx + (i + 0.5) * sx, cy = mny + (j + 0.5) * sy;
|
||
int cov = 0;
|
||
for (const auto& ps : lines)
|
||
if (cx >= ps.worldBounds[0] && cx <= ps.worldBounds[1] &&
|
||
cy >= ps.worldBounds[2] && cy <= ps.worldBounds[3])
|
||
++cov;
|
||
if (cov > 0) { ++covered; sumCov += cov; maxCov = std::max<long>(maxCov, cov); }
|
||
++hist[cov];
|
||
}
|
||
std::cout << "\n=== overlapStat(俯视光线穿体数,纯几何)===\n";
|
||
std::cout << "footprint X=[" << mnx << "," << mxx << "] Y=[" << mny << "," << mxy
|
||
<< "] (米)\n";
|
||
std::cout << "有体覆盖格的【平均重叠层数】= "
|
||
<< (covered ? double(sumCov) / covered : 0.0)
|
||
<< " 最大重叠 = " << maxCov << " / 共 " << lines.size() << " 条\n";
|
||
std::cout << "重叠层数分布(覆盖格中占比):\n";
|
||
for (std::size_t k = 1; k < hist.size(); ++k)
|
||
if (hist[k] > 0)
|
||
std::cout << " 穿 " << k << " 个体: " << (covered ? 100.0 * hist[k] / covered : 0.0)
|
||
<< "%\n";
|
||
std::cout << "结论:平均/最大若 ~2–3 而非 ~20,则'20×重叠'是误判,瓶颈在别处(密 Y 细采样)。\n";
|
||
return 0;
|
||
}
|
||
|
||
// #6 统一传函:汇总各线底图 2/98 分位 → 一套【公共物理幅值范围】,重建各线属性使
|
||
// 同一幅值跨线映射同色(可比)。各线仍用自己的 quant(量化差异由各自 quant 吸收)。
|
||
double sumLo = 0.0, sumHi = 0.0;
|
||
int nRange = 0;
|
||
for (PlacedSource& ps : lines) {
|
||
vtkImageData* b = ps.source->baseImage();
|
||
if (b == nullptr) continue;
|
||
const ScalarPercentiles pc =
|
||
sampleScalarPercentiles(b, ps.meta.quant, 0.02, 0.98);
|
||
if (pc.samples > 0) {
|
||
sumLo += pc.lo;
|
||
sumHi += pc.hi;
|
||
++nRange;
|
||
}
|
||
}
|
||
double shVmin = 0.0, shVmax = 0.0;
|
||
if (nRange > 0 && sumLo / nRange < sumHi / nRange) {
|
||
shVmin = sumLo / nRange;
|
||
shVmax = sumHi / nRange;
|
||
for (PlacedSource& ps : lines) {
|
||
ps.prop = buildLineProperty(ps.meta, ps.source->baseImage(), shVmin, shVmax);
|
||
ps.volume->SetProperty(ps.prop);
|
||
}
|
||
std::cout << "[view-all] #6 统一幅值范围 [" << shVmin << ", " << shVmax
|
||
<< "](跨线一致 + 色标图例)\n";
|
||
}
|
||
|
||
// 4) 单个共享 vtkGPUVolumeRayCastMapper + vtkMultiVolume:全部线作为端口单遍合成。
|
||
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);
|
||
|
||
// #6 色标图例(右侧色阶条 + 幅值刻度)。仅在拿到公共幅值范围时加。
|
||
if (shVmin < shVmax) ren->AddViewProp(buildScalarBar(shVmin, shVmax));
|
||
|
||
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
|
||
vtkOutputWindow::SetInstance(capWin);
|
||
|
||
// 方案 A 核心:把多条【各自独立插值】的体作为同一个 vtkGPUVolumeRayCastMapper 的不同
|
||
// 端口注册进一个 vtkMultiVolume → 单遍 ray-cast 一次性合成(含重叠),消除"N 体=N 遍
|
||
// ray-cast"的物理 N× 开销;每条线仍各保留自己的世界变换 T 与逐线传函(满足"分开插值、
|
||
// 合并渲染")。
|
||
//
|
||
// 硬约束:单个 multi-volume 同时挂的体数受 GPU 每着色器纹理单元上限制约(每体约吃 4 个
|
||
// 单元:体标量 + 颜色 TF + 不透明 TF + 梯度)。超限会报"Hardware does not support the
|
||
// number of textures"并悄悄丢体。故按 K 条/包分组:每包一个 multi-volume 单遍合成,
|
||
// 总遍数=ceil(N/K)(远少于每线一遍的 N 遍)。实测本机 K=7(32 单元 GPU)。
|
||
//
|
||
// 先空渲一帧建好 GL 上下文,再查 GPU 纹理单元数推断【起始】K(每包体数)。
|
||
rw->Render();
|
||
int K = maxPerPassArg;
|
||
const bool autoK = (maxPerPassArg <= 0);
|
||
if (autoK) {
|
||
int units = 0;
|
||
if (auto* oglrw = vtkOpenGLRenderWindow::SafeDownCast(rw)) {
|
||
if (auto* tum = oglrw->GetTextureUnitManager())
|
||
units = tum->GetNumberOfTextureUnits();
|
||
}
|
||
K = units > 8 ? (units - 4) / 4 : 1; // 起始【估计】:留 4 单元、每体 ~4 单元
|
||
if (K < 1) K = 1;
|
||
std::cout << "[view-all] GPU 纹理单元=" << units << " → 起始每包体数 K=" << K
|
||
<< "(仅估计;下面按真实硬件告警自动退避。--maxPerPass 可锁定)\n";
|
||
} else {
|
||
std::cout << "[view-all] 每包体数 K=" << K << "(--maxPerPass 锁定,不自动退避)\n";
|
||
}
|
||
|
||
// LOD 中心装配:各线独立 GPU mapper(已在上面建好、喂了 baseImage),直接把每条线的 volume
|
||
// 加进同一 renderer。无 multi-volume → 无纹理单元上限、无分包、无退避(passcost 证明 N 遍开销
|
||
// 温和:20 条独立 mapper 铺开 78fps)。每帧实际渲染量由视野 LOD(引擎选区)压住、与 20 条总量解耦。
|
||
(void)K;
|
||
(void)autoK;
|
||
std::vector<vtkSmartPointer<vtkGPUVolumeRayCastMapper>> mappers;
|
||
mappers.reserve(lines.size());
|
||
for (PlacedSource& ps : lines) {
|
||
if (ps.mapper) mappers.push_back(ps.mapper);
|
||
ren->AddVolume(ps.volume);
|
||
}
|
||
ren->ResetCamera();
|
||
rw->Render();
|
||
std::cout << "[view-all] 已加入场景线数=" << lines.size()
|
||
<< "(各线独立 GPU mapper + 视野 LOD,弃 multi-volume 单遍;单帧渲染量随视野走)\n";
|
||
|
||
// --slice:全局世界切面——定义一个世界轴对齐薄板,每条线用 worldInv 作 ResliceTransform
|
||
// reslice 到这【同一个面】上采样(覆盖外透明),blend 合成一张 → 真正一整片切面,整体扫过去。
|
||
GlobalSlice gs;
|
||
const bool sliceOn = a.kv.count("slice") > 0;
|
||
vtkSmartPointer<vtkImageActor> sliceActor;
|
||
if (sliceOn) {
|
||
gs.rw = rw.Get();
|
||
const std::string sax = a.get("slice", "");
|
||
int inPlane[2];
|
||
if (sax == "frontback") { // 顺路 radargram:沿 world X 扫,面=Y-Z
|
||
gs.sweepAxis = 0; inPlane[0] = 1; inPlane[1] = 2;
|
||
} else if (sax == "leftright") { // 横切断面:沿 world Y(沿路) 扫,面=X-Z(横穿所有线)
|
||
gs.sweepAxis = 1; inPlane[0] = 0; inPlane[1] = 2;
|
||
} else { // updown 深度 C-scan:沿 world Z 扫,面=X-Y
|
||
gs.sweepAxis = 2; inPlane[0] = 0; inPlane[1] = 1;
|
||
}
|
||
gs.inA = inPlane[0];
|
||
gs.inB = inPlane[1];
|
||
// footprint:全线世界 AABB 并集。
|
||
double lo[3] = {1e30, 1e30, 1e30}, hi[3] = {-1e30, -1e30, -1e30};
|
||
for (const PlacedSource& ps : lines)
|
||
for (int d = 0; d < 3; ++d) {
|
||
lo[d] = std::min(lo[d], ps.worldBounds[d * 2]);
|
||
hi[d] = std::max(hi[d], ps.worldBounds[d * 2 + 1]);
|
||
}
|
||
gs.sweepMin = lo[gs.sweepAxis];
|
||
gs.sweepMax = hi[gs.sweepAxis];
|
||
const double atFrac = std::clamp(std::stod(a.get("sliceAt", "0.5")), 0.0, 1.0);
|
||
gs.sweepWorld = lo[gs.sweepAxis] + atFrac * (hi[gs.sweepAxis] - lo[gs.sweepAxis]);
|
||
for (int d = 0; d < 3; ++d) { gs.footLo[d] = lo[d]; gs.footHi[d] = hi[d]; }
|
||
const double SENT = -32768.0; // 覆盖外 sentinel(int16 数据 ±7417 之外)
|
||
const geopro::core::ColorScale sliceCs =
|
||
pickColor(kViewDefaultVariant.color, shVmin, shVmax);
|
||
vtkSmartPointer<vtkLookupTable> lut = makeLut(sliceCs, shVmin, shVmax);
|
||
|
||
if (gs.sweepAxis == 2) {
|
||
// ── 深度 C-scan:逐线整张水平切面(深度共面→拼成完整 C-scan,全覆盖、原生分辨率)──
|
||
for (PlacedSource& ps : lines) {
|
||
vtkImageData* b = ps.source->baseImage();
|
||
if (b == nullptr) continue;
|
||
int dd[3];
|
||
double oo[3], ss[3];
|
||
b->GetDimensions(dd);
|
||
b->GetOrigin(oo);
|
||
b->GetSpacing(ss);
|
||
auto col = vtkSmartPointer<vtkImageMapToColors>::New();
|
||
col->SetLookupTable(lut);
|
||
col->SetInputData(b);
|
||
col->SetOutputFormatToRGBA();
|
||
col->Update();
|
||
auto act = vtkSmartPointer<vtkImageActor>::New();
|
||
act->GetMapper()->SetInputConnection(col->GetOutputPort());
|
||
act->SetUserTransform(ps.world); // 整张水平片摆进世界(共面)
|
||
ren->AddActor(act);
|
||
gs.lineActors.push_back(act);
|
||
gs.winv.push_back(ps.worldInv);
|
||
gs.lorg.push_back({oo[0], oo[1], oo[2]});
|
||
gs.lspc.push_back({ss[0], ss[1], ss[2]});
|
||
gs.ldim.push_back({dd[0], dd[1], dd[2]});
|
||
if (ps.volume) gs.volumes.push_back(ps.volume.Get());
|
||
}
|
||
} else {
|
||
// ── 横切/顺路(竖直面):全局面 reslice 各线到同一世界面 + blend(共面、可见整片)──
|
||
// 输出薄板:面内铺满 footprint(目标 0.1m、每轴上限 2000),扫描轴 1 片@sweepWorld。
|
||
const double targetSp = 0.1;
|
||
double osp[3], oorg[3];
|
||
int oext[6];
|
||
for (int d = 0; d < 3; ++d) {
|
||
if (d == gs.sweepAxis) {
|
||
osp[d] = 1.0; oorg[d] = gs.sweepWorld; oext[d * 2] = 0; oext[d * 2 + 1] = 0;
|
||
} else {
|
||
const double span = std::max(1e-3, hi[d] - lo[d]);
|
||
const int Nd = std::clamp(static_cast<int>(span / targetSp), 64, 2000);
|
||
osp[d] = span / Nd;
|
||
oorg[d] = lo[d];
|
||
oext[d * 2] = 0; oext[d * 2 + 1] = Nd - 1;
|
||
}
|
||
}
|
||
auto blend = vtkSmartPointer<vtkImageBlend>::New();
|
||
blend->SetBlendModeToNormal();
|
||
for (PlacedSource& ps : lines) {
|
||
vtkImageData* b = ps.source->baseImage();
|
||
if (b == nullptr) continue;
|
||
auto rs = vtkSmartPointer<vtkImageReslice>::New();
|
||
rs->SetInputData(b);
|
||
rs->SetResliceTransform(ps.worldInv);
|
||
rs->SetInterpolationModeToNearestNeighbor();
|
||
rs->SetBackgroundLevel(SENT);
|
||
rs->SetOutputDimensionality(3);
|
||
rs->SetOutputSpacing(osp);
|
||
rs->SetOutputOrigin(oorg);
|
||
rs->SetOutputExtent(oext);
|
||
auto rgb = vtkSmartPointer<vtkImageMapToColors>::New();
|
||
rgb->SetLookupTable(lut);
|
||
rgb->SetInputConnection(rs->GetOutputPort());
|
||
rgb->SetOutputFormatToRGB();
|
||
auto th = vtkSmartPointer<vtkImageThreshold>::New();
|
||
th->SetInputConnection(rs->GetOutputPort());
|
||
th->ThresholdBetween(SENT - 0.5, SENT + 0.5);
|
||
th->SetInValue(0);
|
||
th->SetOutValue(255);
|
||
th->SetOutputScalarTypeToUnsignedChar();
|
||
auto app = vtkSmartPointer<vtkImageAppendComponents>::New();
|
||
app->AddInputConnection(rgb->GetOutputPort());
|
||
app->AddInputConnection(th->GetOutputPort());
|
||
blend->AddInputConnection(app->GetOutputPort());
|
||
gs.reslices.push_back(rs);
|
||
if (ps.volume) gs.volumes.push_back(ps.volume.Get());
|
||
}
|
||
sliceActor = vtkSmartPointer<vtkImageActor>::New();
|
||
sliceActor->GetMapper()->SetInputConnection(blend->GetOutputPort());
|
||
blend->Update();
|
||
sliceActor->SetDisplayExtent(oext[0], oext[1], oext[2], oext[3], oext[4], oext[5]);
|
||
ren->AddActor(sliceActor);
|
||
}
|
||
gs.applySlice();
|
||
gs.applyVolume();
|
||
const char* axisName = gs.sweepAxis == 2
|
||
? "深度 C-scan(逐线整片,全覆盖原生分辨率,沿深度扫)"
|
||
: gs.sweepAxis == 1
|
||
? "横切断面(全局面,沿路扫,整片横穿所有线)"
|
||
: "顺路 radargram(全局面,沿横向扫)";
|
||
const std::size_t nslice =
|
||
gs.lineActors.empty() ? gs.reslices.size() : gs.lineActors.size();
|
||
std::cout << "[view-all] --slice:切片已加(" << nslice << " 线," << axisName
|
||
<< ")。↑↓整片扫过 / [ ]调体透明度 / v 体显隐。\n";
|
||
}
|
||
|
||
ViewAllState st;
|
||
st.lines = &lines;
|
||
st.mappers = &mappers;
|
||
st.ren = ren.Get();
|
||
st.rw = rw.Get();
|
||
st.aspect = aspect;
|
||
st.viewportH = winH;
|
||
st.dragImgSample = maxImgSample;
|
||
st.sampleDist = sampleDist;
|
||
st.dragSampleMul = dragSampleMul;
|
||
|
||
// 首帧:ResetCamera 框全测区 → 概览(各线选粗 LOD 底图)。提交引擎目标 + 阻塞拉首图。
|
||
ren->ResetCamera();
|
||
st.cam = ren->GetActiveCamera();
|
||
viewAllSubmitTargets(&st);
|
||
// 概览阻塞拉一次(保证首帧高清就绪,离屏/真窗口都从有图起步)。
|
||
for (PlacedSource& ps : lines) {
|
||
for (int tries = 0; tries < 200; ++tries) {
|
||
if (viewAllPickOneLine(ps)) break;
|
||
if (ps.currentImg != nullptr) break;
|
||
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
||
}
|
||
}
|
||
viewAllApplyChanLod(&st); // §3:首帧按概览距离选通道密度(远→疏)
|
||
rw->Render();
|
||
if (capWin->textureError()) {
|
||
std::cerr << "[view-all] 警告: 仍检测到 3D 纹理维度错误(不应发生,引擎契约 "
|
||
"≤16384)。\n";
|
||
}
|
||
|
||
// 根因验证(--swapTest):确定性复现"通道 LOD 换密度"。before=全 Y 渲一帧存图;
|
||
// 然后把每条线端口就地换成 Y 抽半的图(同范围、不同 Y 密度)+ 施加 --swapFix 策略;
|
||
// after 再渲一帧存图。对比 before/after:after 若仍连续/在位 → 就地换贴图能算对。
|
||
if (a.kv.count("swapTest") > 0) {
|
||
const std::string sd = a.get("shotDir", "tmp/swaptest");
|
||
fs::create_directories(sd);
|
||
ren->ResetCamera();
|
||
ren->GetActiveCamera()->Elevation(30);
|
||
ren->GetActiveCamera()->Azimuth(25);
|
||
ren->ResetCameraClippingRange();
|
||
rw->Render();
|
||
savePng(rw.Get(), (fs::path(sd) / "before-fullY.png").string());
|
||
for (PlacedSource& ps : lines) {
|
||
if (ps.currentImg == nullptr) continue;
|
||
int dIn[3];
|
||
ps.currentImg->GetDimensions(dIn);
|
||
// --swapMode x = 改包围盒(X 裁半,模拟引擎子区域);否则 y = 保包围盒(改 Y 密度,通道 LOD)。
|
||
auto dY = (a.get("swapMode", "y") == "x") ? cropXhalf(ps.currentImg)
|
||
: downsampleY(ps.currentImg);
|
||
int dOut[3];
|
||
dY->GetDimensions(dOut);
|
||
ps.currentImg = dY;
|
||
ps.multiMapper->SetInputDataObject(ps.port, dY);
|
||
if (gViewAllSwapFix >= 2) ps.multiVol->SetVolume(ps.volume, ps.port);
|
||
ps.multiVol->Modified();
|
||
if (gViewAllSwapFix >= 3) ps.multiMapper->Modified();
|
||
if (gViewAllSwapFix >= 4) ps.multiVol->GetBounds();
|
||
std::cout << "[swapTest] " << ps.name << " Y " << dIn[1] << "→" << dOut[1]
|
||
<< "\n";
|
||
}
|
||
ren->ResetCameraClippingRange();
|
||
rw->Render();
|
||
savePng(rw.Get(), (fs::path(sd) / "after-halfY.png").string());
|
||
std::cout << "[swapTest] swapFix=" << gViewAllSwapFix
|
||
<< " 出图: " << (fs::path(sd) / "before-fullY.png").string()
|
||
<< " / after-halfY.png(after 连续在位=能算对)\n";
|
||
return 0;
|
||
}
|
||
|
||
if (preview) {
|
||
fs::create_directories(shotDir);
|
||
|
||
// (A) 概览俯视(top):相机沿 -Z 俯看 XY 平面(看 20 条在测区平面铺开)。
|
||
{
|
||
ren->ResetCamera();
|
||
vtkCamera* cam = ren->GetActiveCamera();
|
||
double fp[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();
|
||
st.cam = cam;
|
||
viewAllSubmitTargets(&st);
|
||
for (PlacedSource& ps : lines)
|
||
for (int t = 0; t < 100 && ps.currentImg == nullptr; ++t) {
|
||
if (viewAllPickOneLine(ps)) break;
|
||
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
||
}
|
||
viewAllApplyChanLod(&st);
|
||
if (!lines.empty()) {
|
||
int d[3] = {0, 0, 0};
|
||
if (lines[0].currentImg) lines[0].currentImg->GetDimensions(d);
|
||
std::cout << "[chanLod] 俯视(远) stride=" << lines[0].yStride
|
||
<< " 线0 Y=" << d[1] << "\n";
|
||
}
|
||
rw->Render();
|
||
savePng(rw.Get(), (fs::path(shotDir) / "view-all-top.png").string());
|
||
std::cout << "[view-all] 俯视图存: "
|
||
<< (fs::path(shotDir) / "view-all-top.png").string() << "\n";
|
||
}
|
||
|
||
// (B) 概览斜视(oblique):var4 取景 + 概览 fps(全部可见、各选粗 LOD)。
|
||
int ovVisible = 0, ovCulled = 0;
|
||
double fpsOverview = 0.0;
|
||
{
|
||
ren->ResetCamera();
|
||
vtkCamera* cam = ren->GetActiveCamera();
|
||
cam->Elevation(35.0);
|
||
cam->Azimuth(30.0);
|
||
ren->ResetCameraClippingRange();
|
||
st.cam = cam;
|
||
viewAllSubmitTargets(&st);
|
||
for (PlacedSource& ps : lines) {
|
||
for (int t = 0; t < 100 && ps.currentImg == nullptr; ++t) {
|
||
if (viewAllPickOneLine(ps)) break;
|
||
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
||
}
|
||
++ovVisible;
|
||
}
|
||
rw->Render();
|
||
savePng(rw.Get(),
|
||
(fs::path(shotDir) / "view-all-oblique.png").string());
|
||
std::cout << "[view-all] 斜视图存: "
|
||
<< (fs::path(shotDir) / "view-all-oblique.png").string() << "\n";
|
||
|
||
rw->SetDesiredUpdateRate(15.0); // 拖动态:降采样
|
||
rw->Render();
|
||
Stopwatch sw;
|
||
const int frames = 60;
|
||
for (int f = 0; f < frames; ++f) {
|
||
cam->Azimuth(360.0 / frames);
|
||
rw->Render();
|
||
}
|
||
fpsOverview = sw.elapsedMs() > 0 ? frames * 1000.0 / sw.elapsedMs() : 0.0;
|
||
}
|
||
|
||
// (C) 拉近一段:把焦点对准测区中段一条线的世界中心、近距正对该段 → 大部分线出
|
||
// 视锥被裁掉,当前线只渲可见段的合适 LOD。报拉近 fps(与概览对比)。
|
||
// fps 探针用小幅 azimuth 摆动(±6°,模拟拉近态轻微转动观察),而非整周 orbit
|
||
// ——整周 orbit 会把全测区转回视野使裁剪失效,不代表真实"拉近看一段"的交互。
|
||
int nearVisible = 0, nearCulled = 0;
|
||
double fpsNear = 0.0;
|
||
{
|
||
// 选中段一条线(取参与线的中位)作拉近目标,焦点置其世界 AABB 中心。
|
||
const PlacedSource& tgt = lines[lines.size() / 2];
|
||
const double cx = 0.5 * (tgt.worldBounds[0] + tgt.worldBounds[1]);
|
||
const double cy = 0.5 * (tgt.worldBounds[2] + tgt.worldBounds[3]);
|
||
const double cz = 0.5 * (tgt.worldBounds[4] + tgt.worldBounds[5]);
|
||
// 该段(含 exagg 后)世界跨度 → 近距视距(贴该段,使其充满视野、邻线出视锥)。
|
||
const double segLen = std::max({tgt.worldBounds[1] - tgt.worldBounds[0],
|
||
tgt.worldBounds[3] - tgt.worldBounds[2],
|
||
tgt.worldBounds[5] - tgt.worldBounds[4]});
|
||
vtkCamera* cam = ren->GetActiveCamera();
|
||
const double fovY = cam->GetViewAngle();
|
||
const double tanH =
|
||
std::max(1e-3, std::tan(0.5 * fovY * 3.14159265358979 / 180.0));
|
||
// 近距:只贴该段横截面尺度(取较短轴 ~该段 Y/Z 跨度的 1/4),使邻行线出视锥。
|
||
const double shortSpan = std::max(
|
||
1.0, std::min(tgt.worldBounds[3] - tgt.worldBounds[2],
|
||
tgt.worldBounds[5] - tgt.worldBounds[4]));
|
||
const double dist = (0.25 * shortSpan) / tanH * 1.4;
|
||
cam->SetFocalPoint(cx, cy, cz);
|
||
cam->SetPosition(cx, cy + dist, cz + 0.3 * dist); // 斜上方近观该段
|
||
cam->SetViewUp(0, 0, 1);
|
||
ren->ResetCameraClippingRange();
|
||
st.cam = cam;
|
||
(void)segLen;
|
||
viewAllSubmitTargets(&st);
|
||
for (PlacedSource& ps : lines) {
|
||
for (int t = 0; t < 120 && ps.currentImg == nullptr; ++t) {
|
||
if (viewAllPickOneLine(ps)) break;
|
||
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
||
}
|
||
++nearVisible;
|
||
}
|
||
viewAllApplyChanLod(&st);
|
||
if (!lines.empty()) {
|
||
int d[3] = {0, 0, 0};
|
||
if (lines[0].currentImg) lines[0].currentImg->GetDimensions(d);
|
||
std::cout << "[chanLod] 拉近(近) stride=" << lines[0].yStride
|
||
<< " 线0 Y=" << d[1] << "\n";
|
||
}
|
||
rw->Render();
|
||
savePng(rw.Get(), (fs::path(shotDir) / "view-all-near.png").string());
|
||
std::cout << "[view-all] 拉近图存: "
|
||
<< (fs::path(shotDir) / "view-all-near.png").string() << "\n";
|
||
|
||
// --slice 验证图:相机沿扫描轴正对切面(看整片切面 face-on)。
|
||
if (sliceOn) {
|
||
const int sa = gs.sweepAxis;
|
||
const int a = (sa == 0) ? 1 : 0; // 面内轴 1
|
||
const int b = (sa == 2) ? 1 : 2; // 面内轴 2
|
||
double ctr[3];
|
||
for (int d = 0; d < 3; ++d) ctr[d] = 0.5 * (gs.footLo[d] + gs.footHi[d]);
|
||
ctr[sa] = gs.sweepWorld;
|
||
const double extA = std::max(1.0, gs.footHi[a] - gs.footLo[a]);
|
||
const double extB = std::max(1.0, gs.footHi[b] - gs.footLo[b]);
|
||
const double dist = 0.6 * std::max(extA, extB) / tanH * 1.4;
|
||
double pos[3] = {ctr[0], ctr[1], ctr[2]};
|
||
pos[sa] += dist; // 沿扫描轴退后,正对切面
|
||
cam->SetFocalPoint(ctr[0], ctr[1], ctr[2]);
|
||
cam->SetPosition(pos[0], pos[1], pos[2]);
|
||
// 俯视(深度,沿Z看)用 Y 朝上;竖直剖面用 Z 朝上(避免 up 与视向平行退化)。
|
||
if (sa == 2) cam->SetViewUp(0, 1, 0);
|
||
else cam->SetViewUp(0, 0, 1);
|
||
ren->ResetCameraClippingRange();
|
||
rw->SetDesiredUpdateRate(0.5);
|
||
gs.volOn = false; // 验证:隐去体,只看切面本身
|
||
gs.applyVolume();
|
||
rw->Render();
|
||
savePng(rw.Get(), (fs::path(shotDir) / "view-all-slice.png").string());
|
||
std::cout << "[view-all] 切面正视图存: "
|
||
<< (fs::path(shotDir) / "view-all-slice.png").string() << "\n";
|
||
}
|
||
|
||
rw->SetDesiredUpdateRate(15.0); // 拖动态:降采样
|
||
rw->Render();
|
||
Stopwatch sw;
|
||
const int frames = 60;
|
||
for (int f = 0; f < frames; ++f) {
|
||
cam->Azimuth(f % 2 == 0 ? 0.4 : -0.4); // 小幅摆动(不转回全测区)
|
||
rw->Render(); // 单遍合成,无需逐帧重提交(拖动只渲已有纹理)
|
||
}
|
||
fpsNear = sw.elapsedMs() > 0 ? frames * 1000.0 / sw.elapsedMs() : 0.0;
|
||
rw->SetDesiredUpdateRate(0.5);
|
||
}
|
||
|
||
const vtkIdType nb = countNonBlackPixels(rw.Get(), winW, winH);
|
||
const bool texErr = capWin->textureError();
|
||
vtkOutputWindow::SetInstance(nullptr);
|
||
|
||
std::cout << "\n=== view-all --preview 测区全貌(多体共场景,引擎 LOD+视锥裁剪)===\n";
|
||
std::cout << "参与线数 : " << lines.size() << "\n";
|
||
std::cout << "exagg(Z) : " << exagg << "\n";
|
||
std::cout << "spread(横向铺开) : " << spread << " m (0=纯真实位置)\n";
|
||
std::cout << "纹理维度错误 : " << (texErr ? "是(!!)" : "否(引擎契约 ≤16384)")
|
||
<< "\n";
|
||
std::cout << "概览可见/裁剪线 : " << ovVisible << " / " << ovCulled << "\n";
|
||
std::cout << "概览 fps(60帧旋) : " << fpsOverview << "\n";
|
||
std::cout << "拉近可见/裁剪线 : " << nearVisible << " / " << nearCulled
|
||
<< " (视锥裁剪生效:裁掉屏外线)\n";
|
||
std::cout << "拉近 fps(60帧旋) : " << fpsNear << "\n";
|
||
const double speedup = fpsOverview > 0 ? fpsNear / fpsOverview : 0.0;
|
||
std::cout << "拉近/概览 fps 比 : " << speedup << "x\n";
|
||
std::cout << "末帧非黑像素 : " << nb << " / " << (winW * winH) << "\n";
|
||
std::cout << "俯视图 : "
|
||
<< (fs::path(shotDir) / "view-all-top.png").string() << "\n";
|
||
std::cout << "斜视图 : "
|
||
<< (fs::path(shotDir) / "view-all-oblique.png").string() << "\n";
|
||
std::cout << "拉近图 : "
|
||
<< (fs::path(shotDir) / "view-all-near.png").string() << "\n";
|
||
|
||
writeMetricLine(
|
||
"view-all,lines=" + std::to_string(lines.size()) +
|
||
",exagg=" + std::to_string(exagg) + ",spread=" + std::to_string(spread) +
|
||
",ovVisible=" + std::to_string(ovVisible) +
|
||
",ovCulled=" + std::to_string(ovCulled) +
|
||
",nearVisible=" + std::to_string(nearVisible) +
|
||
",nearCulled=" + std::to_string(nearCulled) +
|
||
",fpsOverview=" + std::to_string(fpsOverview) +
|
||
",fpsNear=" + std::to_string(fpsNear) +
|
||
",nonBlack=" + std::to_string(nb) +
|
||
",texErr=" + std::to_string(texErr ? 1 : 0));
|
||
return (nb > 0 && !texErr) ? 0 : 1;
|
||
}
|
||
|
||
// 真窗口:可旋转/缩放(每线引擎 LOD + 视锥裁剪 + 拖动降采样)。
|
||
vtkOutputWindow::SetInstance(nullptr);
|
||
rw->SetWindowName("gpr_poc view-all —— 多体单遍合成(multi-volume)/逐线LOD/拖动降采样");
|
||
|
||
// 屏幕左上角 fps + 可见/裁剪线数文本。
|
||
vtkNew<vtkTextActor> fpsText;
|
||
fpsText->SetInput("fps: -- | visible lines: -- | culled: --");
|
||
fpsText->GetTextProperty()->SetFontSize(20);
|
||
fpsText->GetTextProperty()->SetColor(1.0, 1.0, 0.4);
|
||
fpsText->SetDisplayPosition(12, winH - 30);
|
||
ren->AddViewProp(fpsText);
|
||
st.fpsText = fpsText.Get();
|
||
|
||
vtkNew<vtkRenderWindowInteractor> iren;
|
||
iren->SetRenderWindow(rw);
|
||
vtkNew<vtkInteractorStyleTrackballCamera> style;
|
||
iren->SetInteractorStyle(style);
|
||
iren->SetDesiredUpdateRate(15.0); // 拖动态:mapper 降采样
|
||
iren->SetStillUpdateRate(0.5); // 静止态:全质量
|
||
|
||
vtkNew<vtkCallbackCommand> cbInteract;
|
||
cbInteract->SetCallback(viewAllOnInteracting);
|
||
cbInteract->SetClientData(&st);
|
||
iren->AddObserver(vtkCommand::InteractionEvent, cbInteract);
|
||
|
||
vtkNew<vtkCallbackCommand> cbEnd;
|
||
cbEnd->SetCallback(viewAllOnInteract);
|
||
cbEnd->SetClientData(&st);
|
||
iren->AddObserver(vtkCommand::EndInteractionEvent, cbEnd);
|
||
|
||
vtkNew<vtkCallbackCommand> cbTimer;
|
||
cbTimer->SetCallback(viewAllOnTimer);
|
||
cbTimer->SetClientData(&st);
|
||
iren->AddObserver(vtkCommand::TimerEvent, cbTimer);
|
||
|
||
// 滚轮缩放:高优先级(1.0)先于 style 的 Dolly+Render → 该帧即按低质渲染。
|
||
vtkNew<vtkCallbackCommand> cbWheelF;
|
||
cbWheelF->SetCallback(viewAllOnWheel);
|
||
cbWheelF->SetClientData(&st);
|
||
iren->AddObserver(vtkCommand::MouseWheelForwardEvent, cbWheelF, 1.0);
|
||
vtkNew<vtkCallbackCommand> cbWheelB;
|
||
cbWheelB->SetCallback(viewAllOnWheel);
|
||
cbWheelB->SetClientData(&st);
|
||
iren->AddObserver(vtkCommand::MouseWheelBackwardEvent, cbWheelB, 1.0);
|
||
|
||
// --slice:键盘 Up/Down 改 C-scan 深度(高优先级,先于 style 处理)。
|
||
vtkNew<vtkCallbackCommand> cbKey;
|
||
if (sliceOn) {
|
||
cbKey->SetCallback(cscanOnKey);
|
||
cbKey->SetClientData(&gs);
|
||
iren->AddObserver(vtkCommand::KeyPressEvent, cbKey, 1.0);
|
||
}
|
||
|
||
std::cout << "[view-all] 打开真窗口。左键旋转 / 滚轮缩放 / q 退出"
|
||
<< (sliceOn ? " / ↑↓改切片深度 / [ ]调体透明度 / v 体显隐" : "")
|
||
<< "。\n";
|
||
iren->Initialize();
|
||
iren->CreateRepeatingTimer(33); // ~30Hz 非阻塞拉取后台就绪纹理
|
||
rw->Render();
|
||
iren->Start();
|
||
std::cout << "[view-all] 窗口关闭,退出。\n";
|
||
return 0;
|
||
}
|
||
|
||
// ============================================================================
|
||
// view-survey-all:测绘级世界对齐体直接按 CGCS2000 origin 摆放渲染(Task P8)
|
||
// ============================================================================
|
||
//
|
||
// 与 view-all(线局部体 + .gps 起点+航向刚体近似摆放)不同:本命令的体已是测绘级世界对齐
|
||
// 体(build-survey-all 产出,meta.origin=CGCS2000 世界米,轴 X=东/Y=北/Z=深)。故【无需 .gps、
|
||
// 无需航向旋转】——直接按各体 meta.origin 平移摆进同一世界框即精确就位(跟路的弯)。
|
||
// 取公共参考原点(全体 origin 最小东/北)平移到近零局部框,保 VTK 数值稳定且相对位置严格不变。
|
||
|
||
// 由测绘级世界对齐体 + 公共参考原点 + Z 夸张 → vtkVolume(平移到世界位,无旋转)。
|
||
vtkSmartPointer<vtkVolume> makeSurveyPlacedVolume(
|
||
vtkImageData* img, const geopro::data::StoreMeta& m, double refX,
|
||
double refY, double exagg, double& vminOut, double& vmaxOut) {
|
||
const GalleryVariant& v = kViewDefaultVariant;
|
||
|
||
double vmin = m.vminPhys, vmax = m.vmaxPhys;
|
||
const ScalarPercentiles pc =
|
||
sampleScalarPercentiles(img, 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(img);
|
||
vtkSmartPointer<vtkVolumeProperty> prop = makeVariantProperty(
|
||
v, m.quant, cs, vmin, vmax, v.maxOpacity,
|
||
v.useGradientOpacity ? &gs : nullptr);
|
||
|
||
// img 的 origin 是 meta.origin(CGCS 世界米,东向 ~4e7 量级)。直接把 image origin 减去公共
|
||
// 参考原点落到近零局部框——避免 VTK 内部 float32 在 ~4e7 绝对坐标上丢精度(±1m);相对位置严格不变。
|
||
double iorg[3];
|
||
img->GetOrigin(iorg);
|
||
img->SetOrigin(iorg[0] - refX, iorg[1] - refY, iorg[2]);
|
||
|
||
vtkNew<vtkSmartVolumeMapper> mapper;
|
||
mapper->SetInputData(img);
|
||
mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
|
||
mapper->SetAutoAdjustSampleDistances(0);
|
||
mapper->SetInteractiveAdjustSampleDistances(0);
|
||
|
||
auto volume = vtkSmartPointer<vtkVolume>::New();
|
||
volume->SetMapper(mapper);
|
||
volume->SetProperty(prop);
|
||
|
||
// 体已落近零世界框;仅 Z 夸张(局部深度)。
|
||
auto xf = vtkSmartPointer<vtkTransform>::New();
|
||
xf->PostMultiply();
|
||
xf->Scale(1.0, 1.0, exagg);
|
||
volume->SetUserTransform(xf);
|
||
return volume;
|
||
}
|
||
|
||
int cmdViewSurveyAll(int argc, char** argv) {
|
||
const Args a = parseArgs(argc, argv, 2);
|
||
if (a.positional.empty()) {
|
||
std::cerr << "用法: gpr_poc view-survey-all <storesDir> [--preview] "
|
||
"[--exagg 8] [--level 1] [--shotDir <dir>]\n"
|
||
"例: gpr_poc view-survey-all tmp/survey_all --preview "
|
||
"--exagg 8\n";
|
||
return 2;
|
||
}
|
||
const std::string storesDir = a.positional[0];
|
||
const double exagg = std::stod(a.get("exagg", "8"));
|
||
const int level = std::stoi(a.get("level", "1"));
|
||
const bool preview = a.kv.count("preview") > 0;
|
||
const std::string shotDir = a.get("shotDir", storesDir);
|
||
|
||
std::cout << "[view-survey-all] storesDir=" << storesDir << " exagg=" << exagg
|
||
<< " level=" << level
|
||
<< (preview ? " [PREVIEW 离屏俯视+斜视出图]" : " [真窗口可交互]")
|
||
<< "\n";
|
||
|
||
std::cout << "[view-survey-all] 离屏闸门复检...\n";
|
||
if (cmdOffscreenSmoke() != 0) {
|
||
std::cout << "[view-survey-all] 闸门失败,中止。\n";
|
||
return 1;
|
||
}
|
||
|
||
// 1) 发现体目录(含 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-survey-all] 发现体目录数=" << storeNames.size() << "\n";
|
||
if (storeNames.empty()) {
|
||
std::cerr << "[view-survey-all] 错误: 未发现任何含 meta.json 的体目录\n";
|
||
return 1;
|
||
}
|
||
|
||
// 2) 逐体读 meta,先定公共参考原点(全体 origin 最小东/北)。
|
||
struct SurveyPlaced {
|
||
std::string name;
|
||
vtkSmartPointer<vtkImageData> img;
|
||
geopro::data::StoreMeta meta;
|
||
};
|
||
std::vector<SurveyPlaced> placed;
|
||
double refX = std::numeric_limits<double>::infinity();
|
||
double refY = std::numeric_limits<double>::infinity();
|
||
for (const std::string& nm : storeNames) {
|
||
const std::string storePath = (fs::path(storesDir) / nm).string();
|
||
geopro::data::ChunkedVolumeStore store(storePath);
|
||
const geopro::data::StoreMeta m = store.meta();
|
||
refX = std::min(refX, m.origin[0]);
|
||
refY = std::min(refY, m.origin[1]);
|
||
const int lv = std::max(0, std::min(level, store.levels() - 1));
|
||
vtkSmartPointer<vtkImageData> img = buildLevelImage(store, lv, m);
|
||
std::cout << "[view-survey-all] " << nm << " level=" << lv << " 维度="
|
||
<< img->GetDimensions()[0] << "x" << img->GetDimensions()[1] << "x"
|
||
<< img->GetDimensions()[2] << " CGCS origin=(" << std::fixed
|
||
<< m.origin[0] << ", " << m.origin[1] << ")\n";
|
||
placed.push_back({nm, img, m});
|
||
}
|
||
if (placed.empty()) {
|
||
std::cerr << "[view-survey-all] 错误: 无可用体\n";
|
||
return 1;
|
||
}
|
||
std::cout << "[view-survey-all] 公共参考原点(最小东/北)=(" << std::fixed << refX
|
||
<< ", " << refY << ") (共 " << placed.size() << " 体)\n";
|
||
|
||
// 3) 同一 renderer 加全部世界对齐体(按 origin-ref 摆放,无旋转)。
|
||
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 added = 0;
|
||
for (const SurveyPlaced& sp : placed) {
|
||
double vmin = 0, vmax = 0;
|
||
vtkSmartPointer<vtkVolume> vol = makeSurveyPlacedVolume(
|
||
sp.img.Get(), sp.meta, refX, refY, exagg, vmin, vmax);
|
||
ren->AddVolume(vol);
|
||
++added;
|
||
}
|
||
std::cout << "[view-survey-all] 已加入场景体数=" << added << "\n";
|
||
|
||
ren->ResetCamera();
|
||
rw->Render();
|
||
if (capWin->textureError()) {
|
||
std::cerr << "[view-survey-all] 警告: 检测到 3D 纹理维度错误(某体超 GL 上限),"
|
||
"可增大 --level 取更粗层。\n";
|
||
}
|
||
|
||
if (preview) {
|
||
fs::create_directories(shotDir);
|
||
{ // 俯视(top):看 20 条按真实 CGCS 位置铺成测区(含路的弯)。
|
||
ren->ResetCamera();
|
||
vtkCamera* cam = ren->GetActiveCamera();
|
||
double fp[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-survey-all-top.png").string();
|
||
savePng(rw.Get(), p);
|
||
std::cout << "[view-survey-all] 俯视图存: " << p << "\n";
|
||
}
|
||
{ // 斜视(oblique)。
|
||
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-survey-all-oblique.png").string();
|
||
savePng(rw.Get(), p);
|
||
std::cout << "[view-survey-all] 斜视图存: " << p << "\n";
|
||
}
|
||
|
||
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-survey-all --preview 测区全貌(测绘级世界对齐体)===\n";
|
||
std::cout << "参与体数 : " << placed.size() << "\n";
|
||
std::cout << "level : " << level << "\n";
|
||
std::cout << "exagg(Z) : " << exagg << "\n";
|
||
std::cout << "公共参考原点 : (" << std::fixed << refX << ", " << refY
|
||
<< ") CGCS2000 米\n";
|
||
std::cout << "非黑像素 : " << nb << " / " << (winW * winH) << "\n";
|
||
std::cout << "fps(" << frames << "帧连渲) : " << fps << "\n";
|
||
std::cout << "俯视图 : "
|
||
<< (fs::path(shotDir) / "view-survey-all-top.png").string()
|
||
<< "\n";
|
||
std::cout << "斜视图 : "
|
||
<< (fs::path(shotDir) / "view-survey-all-oblique.png").string()
|
||
<< "\n";
|
||
|
||
writeMetricLine("view-survey-all,bodies=" + std::to_string(placed.size()) +
|
||
",level=" + std::to_string(level) +
|
||
",exagg=" + std::to_string(exagg) +
|
||
",nonBlack=" + std::to_string(nb) +
|
||
",fps=" + std::to_string(fps));
|
||
return nb > 0 ? 0 : 1;
|
||
}
|
||
|
||
rw->SetWindowName("gpr_poc view-survey-all —— 测绘级 CGCS2000 世界对齐体");
|
||
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-survey-all] 打开真窗口。左键旋转 / 滚轮缩放 / q 退出。\n";
|
||
iren->Initialize();
|
||
rw->Render();
|
||
iren->Start();
|
||
std::cout << "[view-survey-all] 窗口关闭,退出。\n";
|
||
return 0;
|
||
}
|
||
|
||
int cmdView(int argc, char** argv) {
|
||
const Args a = parseArgs(argc, argv, 2);
|
||
if (a.positional.empty()) {
|
||
std::cerr << "用法: gpr_poc view <storeDir> [--exagg 8] [--opacity 0.5] "
|
||
"[--budget 64] [--smoke] [--preview] [--base] [--variant N] "
|
||
"[--gallery] [--frames 90]\n";
|
||
return 2;
|
||
}
|
||
const std::string dir = a.positional[0];
|
||
// 交互/preview/smoke 默认视觉参数 = kViewDefaultVariant(var4):配色/不透明度包络/
|
||
// exagg/背景全部走 var4,故默认画面 == view-var4(DRY,与画廊同源)。命令行 --exagg /
|
||
// --opacity 若用户显式传则覆盖 var4 对应值,否则用 var4 的 exagg / maxOpacity。
|
||
const GalleryVariant& dv = kViewDefaultVariant;
|
||
const double exagg =
|
||
a.kv.count("exagg") ? std::stod(a.get("exagg", "8")) : dv.exagg;
|
||
const double opacity = a.kv.count("opacity")
|
||
? std::stod(a.get("opacity", "0.5"))
|
||
: dv.maxOpacity;
|
||
const std::size_t budget =
|
||
static_cast<std::size_t>(std::stoul(a.get("budget", "64")));
|
||
const int frames = std::stoi(a.get("frames", "90"));
|
||
auto hasFlag = [&](const char* name) {
|
||
return a.kv.count(name) > 0 ||
|
||
std::find(a.positional.begin(), a.positional.end(),
|
||
std::string("--") + name) != a.positional.end();
|
||
};
|
||
const bool smoke = hasFlag("smoke");
|
||
const bool preview = hasFlag("preview");
|
||
// C3-8 验收用:--preview --shots 额外从【真 view 场景(base+hires+cropping)】多旋转角
|
||
// 离屏出图,用于人工核对「路细长不胖 / 拼接无缝无白 / 旋转无移动白斑」。只加图,不改
|
||
// 默认行为(无 --shots 时与原 preview 完全一致)。
|
||
const bool shots = hasFlag("shots");
|
||
// C3-6 底图预览:--preview --base(或 --base)模拟「交互态:只渲常驻粗底图」——
|
||
// 隐去高清叠加层、只渲整卷最粗 ≤16384 层单纹理 → 整卷概览、盖全、绝不空白。
|
||
const bool basePreview = hasFlag("base");
|
||
// 拉近预览(Task 12d-singletex):--preview --variant near(或 --near)走与真窗口
|
||
// 完全相同的单纹理拉近路径(viewRefreshSingle 选 level0 局部子区域),供控制方 Read
|
||
// 验证「拉近后」单图非空、完整、fps 高。
|
||
const bool nearPreview =
|
||
hasFlag("near") || (a.kv.count("variant") && a.get("variant", "") == "near");
|
||
|
||
// 画廊出图目录:--out <dir> 显式指定;否则默认存到 store 目录(P4:gallery 落在
|
||
// tmp/line001_proc,便于控制方就地对照)。
|
||
const std::string galleryShotDir = a.get("out", dir);
|
||
|
||
// 画廊模式(Task 12d):渲 4 组视觉调参图供挑选。优先于其余路径。
|
||
if (hasFlag("gallery")) {
|
||
return cmdViewGallery(dir, frames, galleryShotDir);
|
||
}
|
||
// 单变体:view --preview --variant N(N=1..4),只渲第 N 组(near 不走此路)。
|
||
if (preview && a.kv.count("variant") && !nearPreview) {
|
||
const int vi = std::stoi(a.get("variant", "1"));
|
||
const int n = static_cast<int>(sizeof(kGalleryVariants) /
|
||
sizeof(kGalleryVariants[0]));
|
||
if (vi < 1 || vi > n) {
|
||
std::cerr << "[view] --variant 需在 1.." << n << " 之间\n";
|
||
return 2;
|
||
}
|
||
std::cout << "[view] storeDir=" << dir << " 单变体 variant=" << vi << "\n";
|
||
if (cmdOffscreenSmoke() != 0) return 1;
|
||
return runGalleryVariant(dir, kGalleryVariants[vi - 1], frames,
|
||
galleryShotDir);
|
||
}
|
||
|
||
std::cout << "[view] storeDir=" << dir << " exagg=" << exagg
|
||
<< " opacity=" << opacity << " budget=" << budget
|
||
<< (preview ? " [PREVIEW 离屏存图+测fps]"
|
||
: (smoke ? " [SMOKE 离屏]" : " [真窗口交互]"))
|
||
<< "\n";
|
||
|
||
// preview/smoke 走离屏。
|
||
const bool offscreen = smoke || preview;
|
||
const int winW = 1280, winH = 800;
|
||
|
||
// 单纹理统一路径(Task 12d-singletex):交互 view 只用 ChunkedVolumeStore 产
|
||
// 单图 + 单 SmartVolumeMapper,不再用 OutOfCoreSource/BrickPager/MultiBlock 分块。
|
||
(void)budget; // 交互 view 不再用 budget 分块(保留参数以兼容旧命令行)。
|
||
// 渲染源 = ViewAdaptiveVolumeSource(C2)。exagg 走 actor 的 SetScale(不烘进
|
||
// 几何,避免与 SetScale 重复夸张),故源构造 exagg=1.0。视口高/宽高比注入给
|
||
// C1 选层(分辨率密度 + 视锥裁剪)。
|
||
geopro::render::ViewAdaptiveVolumeSource source(dir, /*exagg=*/1.0);
|
||
source.setViewportHeight(winH);
|
||
source.setAspect(static_cast<double>(winW) / winH);
|
||
const auto& m = source.meta();
|
||
// P3 首帧高清段(沿线中段 level0):直读 store 建该段单图——既作传函分位标定的基准
|
||
// (该段实际值,对比比整卷底图更punchy),又供 viewSetupDefaultFrame 直接喂高清 mapper
|
||
// (绕开异步 LOD 在「框一段」视距下仍选最粗层的问题)。同 gallery 的 buildLocalLevel0Image
|
||
// 直读路径,非 LOD 算法。读不到则回退底图/全域。
|
||
vtkSmartPointer<vtkImageData> segImg;
|
||
{
|
||
geopro::data::ChunkedVolumeStore store(dir);
|
||
const int totBx = store.bricksX(0);
|
||
const int localBx = std::min(kViewDefaultLocalBricks, totBx);
|
||
const int bx0 = std::max(0, totBx / 2 - localBx / 2);
|
||
segImg = buildLocalLevel0Image(store, m, bx0, localBx);
|
||
}
|
||
// P3 传函分位标定:色阶/不透明度端点按【实际值 2%/98% 分位】(物理域),裁离群。
|
||
// 处理后体值多集中 ±窄带、少量离群 ±9000,按 meta 全域(±9249)映射 → 窄带近透明 → 全黑。
|
||
// 基准用常驻底图(整卷代表):band 较窄 → 窄带信号映到更饱和色、可见度更高;且代表整线
|
||
// 典型信号、不受单段局部高能离群影响。底图缺失再回退首帧段。退化(无样本)回退全域。
|
||
double vmin = m.vminPhys, vmax = m.vmaxPhys;
|
||
vtkImageData* pcBasis =
|
||
source.baseImage() != nullptr ? source.baseImage() : segImg.Get();
|
||
if (pcBasis != nullptr) {
|
||
const ScalarPercentiles pc =
|
||
sampleScalarPercentiles(pcBasis, m.quant, 0.02, 0.98);
|
||
if (pc.samples > 0) {
|
||
vmin = pc.lo;
|
||
vmax = pc.hi;
|
||
std::cout << "[view] 传函分位标定("
|
||
<< (source.baseImage() != nullptr ? "底图" : "局部段")
|
||
<< ",样本 " << pc.samples << "): 2%=" << vmin << " 98%=" << vmax
|
||
<< " (全域 [" << m.vminPhys << ", " << m.vmaxPhys
|
||
<< "] → 裁离群)\n";
|
||
}
|
||
}
|
||
// 配色/不透明度包络取自 var4:seismic + V 形实体包络(floor/mid + opacity 作峰值)。
|
||
const geopro::core::ColorScale cs = pickColor(dv.color, vmin, vmax);
|
||
// C4:默认变体(var4)开了梯度不透明度 → 从常驻底图实测梯度分布标定阈值。底图恒非空。
|
||
GradStats dvGs;
|
||
if (dv.useGradientOpacity && source.baseImage() != nullptr) {
|
||
dvGs = sampleGradientMagnitude(source.baseImage());
|
||
std::cout << "[view] 梯度幅值分布(底图,量化域,样本 " << dvGs.samples
|
||
<< "): median=" << dvGs.median << " p90=" << dvGs.p90
|
||
<< " p99=" << dvGs.p99 << "\n";
|
||
}
|
||
vtkSmartPointer<vtkVolumeProperty> prop = makeVariantProperty(
|
||
dv, m.quant, cs, vmin, vmax, opacity,
|
||
dv.useGradientOpacity ? &dvGs : nullptr);
|
||
|
||
// 渲染窗口:preview/smoke 走离屏,否则真窗口。
|
||
vtkSmartPointer<vtkRenderWindow> rw;
|
||
if (offscreen) {
|
||
rw = makeOffscreenWindow(winW, winH);
|
||
} else {
|
||
rw = vtkSmartPointer<vtkRenderWindow>::New();
|
||
rw->SetSize(winW, winH);
|
||
rw->SetWindowName("gpr_poc view —— 核外 LOD 体绘制 (滚轮缩放切 LOD, 左键旋转)");
|
||
}
|
||
|
||
vtkNew<vtkRenderer> ren;
|
||
ren->SetBackground(dv.bg[0], dv.bg[1], dv.bg[2]); // var4 略亮冷灰背景
|
||
rw->AddRenderer(ren);
|
||
|
||
// C3-6 常驻粗底图层(底图永不空白的命脉):第一个 vtkVolume = 整卷最粗
|
||
// 「各轴 ≤16384」层单纹理(source.baseImage(),构造时主线程一次建成、永远持有)。
|
||
// 它【永远在场、永远渲染、绝不释放、绝不被异步路径触碰】→ 任何相机/任何运动中都
|
||
// 盖住整个体 → 拖动/缩放绝不空白。高清层(下方 mapper)叠在其上、就绪后局部覆盖。
|
||
vtkNew<vtkSmartVolumeMapper> baseMapper;
|
||
baseMapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
|
||
baseMapper->SetAutoAdjustSampleDistances(1);
|
||
baseMapper->SetInteractiveAdjustSampleDistances(1);
|
||
auto baseVolume = vtkSmartPointer<vtkVolume>::New();
|
||
if (source.baseImage() != nullptr) { // 退化 store 防护(理论恒非空)
|
||
baseMapper->SetInputData(source.baseImage()); // 常驻输入,永不改
|
||
baseMapper->Update();
|
||
baseVolume->SetMapper(baseMapper);
|
||
baseVolume->SetProperty(prop); // 与高清层共用传函(同配色/不透明度)
|
||
baseVolume->SetScale(1.0, 1.0, exagg); // 垂向夸张只放大深度(Z);横向路宽(Y)不动 → 与高清层空间对齐
|
||
ren->AddVolume(baseVolume); // 先加底图 → 底层常渲
|
||
}
|
||
|
||
// 高清叠加层:单 vtkSmartVolumeMapper(GPU 光线投射,整张 3D 纹理),与 --preview /
|
||
// gallery 同一 mapper 类型。叠在底图之上:currentImages 就绪后摆到对应世界位置局部
|
||
// 覆盖底图;没就绪则无输入(只显底图,不空)。运动中高清滞后由底图兜底,绝不空白。
|
||
vtkNew<vtkSmartVolumeMapper> mapper;
|
||
mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
|
||
// C3-5:交互式采样距离自适应(修长板填屏 ray-march 慢的关键)。POC 当初为离屏
|
||
// 基准把这两个关掉(0/0)以求恒定全质量,但交互场景必须【开启】:拖动时按渲染窗口的
|
||
// DesiredUpdateRate 拉大 SampleDistance(降采样→快)、停下恢复小步长(全质量→可慢)。
|
||
// 长板填屏慢的根因是每像素沿超长轴海量采样,LOD/异步都不缩短 ray-march 长度,只有
|
||
// 拖动期降采样才把交互态降到可跟手帧率。
|
||
mapper->SetAutoAdjustSampleDistances(1);
|
||
mapper->SetInteractiveAdjustSampleDistances(1);
|
||
|
||
auto volume = vtkSmartPointer<vtkVolume>::New();
|
||
volume->SetMapper(mapper);
|
||
volume->SetProperty(prop);
|
||
volume->SetScale(1.0, 1.0, exagg); // 垂向夸张只放大深度(Z);横向路宽(Y)保持真实比例(修 GPR 路被压胖)
|
||
ren->AddVolume(volume); // 后加高清 → 叠在底图上
|
||
|
||
// 屏幕左上角实时 fps 文本。
|
||
vtkNew<vtkTextActor> fpsText;
|
||
fpsText->SetInput("fps: -- | LOD level: --");
|
||
fpsText->GetTextProperty()->SetFontSize(20);
|
||
fpsText->GetTextProperty()->SetColor(1.0, 1.0, 0.4);
|
||
fpsText->SetDisplayPosition(12, winH - 30);
|
||
ren->AddViewProp(fpsText);
|
||
|
||
// 捕获式 OutputWindow(拦截块上传纹理错)。
|
||
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
|
||
vtkOutputWindow::SetInstance(capWin);
|
||
|
||
ViewState st;
|
||
st.source = &source;
|
||
st.mapper = mapper.Get();
|
||
st.baseMapper = baseMapper.Get(); // 供 viewSyncBaseCropping 按高清块挖空底图
|
||
st.ren = ren.Get();
|
||
st.fpsText = fpsText.Get();
|
||
st.rw = rw.Get();
|
||
st.exagg = exagg;
|
||
st.dir = dir; // 供首帧直读 level0 局部段(绕开 LOD 选粗层)
|
||
st.seedSegImg = segImg; // cmdView 已为分位标定建好的首帧高清段,直接复用
|
||
|
||
// 相机初始定向(修复 1):默认框「局部段」而非整卷。整线横截面 1:34,框整卷
|
||
// 即便 exagg=8 也是一条隐形细带(看着空白);改为对准沿线中段一个 ~768 道窗口
|
||
// 的全分辨率局部体 → 开窗第一帧就看到一段有层状结构的体。三路径共用此取景。
|
||
std::size_t warm = viewSetupDefaultFrame(&st, ren);
|
||
rw->Render();
|
||
|
||
// C3-6 底图预览(模拟「交互态:只渲常驻粗底图」):隐去高清叠加层、把相机框到整卷
|
||
// 包围盒(exagg 后),只渲整卷最粗 ≤16384 层单纹理 → 整卷概览、盖全、非空。证明
|
||
// 拖动/缩放时即使高清全部缺位,底图也独立盖住整个体(永不空白的命脉)。
|
||
if (preview && basePreview) {
|
||
volume->SetVisibility(0); // 隐去高清层 → 只剩常驻底图
|
||
baseMapper->SetCropping(0); // 高清隐了 → 底图取消挖空,整卷全渲(证明永不空白)
|
||
// 框整卷(无参 ResetCamera 按场景中可见 actor 包围盒;高清隐了 → 框底图全卷)。
|
||
ren->ResetCamera();
|
||
st.cam = ren->GetActiveCamera();
|
||
st.cam->Elevation(kViewDefaultVariant.elevation);
|
||
st.cam->Azimuth(kViewDefaultVariant.azimuth);
|
||
ren->ResetCameraClippingRange();
|
||
rw->Render();
|
||
|
||
const fs::path shotDir =
|
||
fs::path("docs") / "superpowers" / "plans" / "poc-lod-shots";
|
||
fs::create_directories(shotDir);
|
||
const std::string pngPath = (shotDir / "view-base.png").string();
|
||
savePng(rw.Get(), pngPath);
|
||
|
||
auto countStructPx = [&]() -> vtkIdType {
|
||
auto px = vtkSmartPointer<vtkUnsignedCharArray>::New();
|
||
rw->GetRGBACharPixelData(0, 0, winW - 1, winH - 1, /*front=*/1, px);
|
||
vtkIdType n = 0;
|
||
const vtkIdType np = px->GetNumberOfTuples();
|
||
for (vtkIdType i = 0; i < np; ++i) {
|
||
if (px->GetComponent(i, 0) > 50 || px->GetComponent(i, 1) > 50 ||
|
||
px->GetComponent(i, 2) > 50) {
|
||
++n;
|
||
}
|
||
}
|
||
return n;
|
||
};
|
||
const vtkIdType baseStruct = countStructPx();
|
||
int bd[3] = {0, 0, 0};
|
||
if (source.baseImage()) source.baseImage()->GetDimensions(bd);
|
||
const bool texErrB = capWin->textureError();
|
||
vtkOutputWindow::SetInstance(nullptr);
|
||
const bool okB = !texErrB && source.baseImage() != nullptr && baseStruct > 0;
|
||
|
||
std::cout << "\n=== view --preview --base 常驻粗底图验证(整卷概览)===\n";
|
||
std::cout << "底图 level : " << source.baseLevel()
|
||
<< "(整卷最粗 ≤16384 层)\n";
|
||
std::cout << "底图维度 : " << bd[0] << " x " << bd[1] << " x " << bd[2]
|
||
<< "\n";
|
||
std::cout << "存图 : " << pngPath << "\n";
|
||
std::cout << "结构像素(>50) : " << baseStruct << " / " << (winW * winH)
|
||
<< " (" << (100.0 * baseStruct / (winW * winH)) << "%)\n";
|
||
std::cout << "纹理维度错误 : " << (texErrB ? "是(!!)" : "否") << "\n";
|
||
std::cout << "base 结果 : "
|
||
<< (okB ? "OK ✔ 底图盖全非空" : "FAIL ✘") << "\n";
|
||
|
||
writeMetricLine(
|
||
"view-base,dir=" + dir + ",baseLevel=" +
|
||
std::to_string(source.baseLevel()) + ",bx=" + std::to_string(bd[0]) +
|
||
",by=" + std::to_string(bd[1]) + ",bz=" + std::to_string(bd[2]) +
|
||
",structPixels=" + std::to_string(baseStruct) +
|
||
",ok=" + std::to_string(okB ? 1 : 0) + ",png=" + pngPath);
|
||
return okB ? 0 : 1;
|
||
}
|
||
|
||
// 拉近预览:在默认取景基础上拉近相机,再走阻塞刷新(与真窗口缩放后完全相同的
|
||
// 单纹理选区路径,level0 局部子区域),轮询到就绪验证「拉近后」单图非空、完整。
|
||
if (nearPreview) {
|
||
st.cam->Dolly(2.5); // 拉近
|
||
ren->ResetCameraClippingRange();
|
||
warm = viewRefreshBlocking(&st);
|
||
rw->Render();
|
||
}
|
||
|
||
std::cout << "[view] 预热(" << (nearPreview ? "拉近局部段" : "默认局部段")
|
||
<< "): level=" << st.lastLevel << " 渲染块=" << warm << "\n";
|
||
|
||
const vtkIdType nonBlack = countNonBlackPixels(rw.Get(), winW, winH);
|
||
const bool textureErr = capWin->textureError();
|
||
const bool renderedOk = !textureErr && nonBlack > 0;
|
||
|
||
if (preview) {
|
||
// 修复 2:用与真窗口完全相同的默认相机/source/exagg/传函(viewSetupDefaultFrame
|
||
// 已建好),离屏渲一帧存图 → 控制方先 Read 确认开窗默认画面非空、有结构。
|
||
const fs::path shotDir =
|
||
fs::path("docs") / "superpowers" / "plans" / "poc-lod-shots";
|
||
fs::create_directories(shotDir);
|
||
const std::string pngPath =
|
||
(shotDir / (nearPreview ? "view-near.png" : "view-default.png"))
|
||
.string();
|
||
savePng(rw.Get(), pngPath);
|
||
// C3-8:多旋转角出图(同一真 view 场景:base+hires 共用 SetScale(1,1,exagg) + 底图
|
||
// 按高清块 cropping 挖空)。从默认相机起取若干 (azimuth,elevation) 离屏存图,供人工
|
||
// 核对路细长比例 / 拼接无缝 / 旋转无移动白斑。无 --shots 不执行。
|
||
if (shots) {
|
||
const struct {
|
||
const char* name;
|
||
double az;
|
||
double el;
|
||
} kAngles[] = {
|
||
{"view-shot-az0", 0.0, 0.0}, {"view-shot-az30", 30.0, 0.0},
|
||
{"view-shot-az60", 60.0, 0.0}, {"view-shot-az-30", -30.0, 0.0},
|
||
{"view-shot-el20", 0.0, 20.0}, {"view-shot-az45el15", 45.0, 15.0},
|
||
};
|
||
for (const auto& s : kAngles) {
|
||
st.cam->Azimuth(s.az);
|
||
st.cam->Elevation(s.el);
|
||
ren->ResetCameraClippingRange();
|
||
rw->Render();
|
||
const std::string sp = (shotDir / (std::string(s.name) + ".png")).string();
|
||
savePng(rw.Get(), sp);
|
||
std::cout << "[view] 旋转角出图: " << sp << " (az=" << s.az
|
||
<< " el=" << s.el << ")\n";
|
||
st.cam->Elevation(-s.el); // 复位到默认朝向,下一角从默认起算
|
||
st.cam->Azimuth(-s.az);
|
||
}
|
||
}
|
||
// 结构像素计数:背景为深蓝灰(R/G≈10,B≈20),countNonBlackPixels(>10) 会把整屏
|
||
// 背景都算「非空」,对验证「画面有结构」无意义。改为只数明显亮于背景的像素
|
||
// (任一通道 >50),作为「确有渲出的体结构」的诚实判据。
|
||
auto countStructPixels = [&]() -> vtkIdType {
|
||
auto px = vtkSmartPointer<vtkUnsignedCharArray>::New();
|
||
rw->GetRGBACharPixelData(0, 0, winW - 1, winH - 1, /*front=*/1, px);
|
||
vtkIdType n = 0;
|
||
const vtkIdType np = px->GetNumberOfTuples();
|
||
for (vtkIdType i = 0; i < np; ++i) {
|
||
if (px->GetComponent(i, 0) > 50 || px->GetComponent(i, 1) > 50 ||
|
||
px->GetComponent(i, 2) > 50) {
|
||
++n;
|
||
}
|
||
}
|
||
return n;
|
||
};
|
||
const vtkIdType defStruct = countStructPixels();
|
||
|
||
// C3-5:两态 fps 对照。AutoAdjustSampleDistances 据渲染窗口的 DesiredUpdateRate
|
||
// 决定 SampleDistance——高 DesiredUpdateRate=拖动态(大步长/降采样/快),低=静止态
|
||
// (小步长/全质量/可慢)。离屏无真交互,故显式设 DesiredUpdateRate 模拟两态,旋相机
|
||
// 测真实 Render() 帧率,量化拖动降采样提速。
|
||
//
|
||
// ① 全质量静态:StillUpdateRate(慢档),mapper 用最小 SampleDistance。
|
||
rw->SetDesiredUpdateRate(0.5); // 静止目标帧率(慢)→全质量
|
||
rw->Render(); // 预热一帧(管线热 + 采样距离按此档生效)
|
||
Stopwatch swStill;
|
||
for (int f = 0; f < frames; ++f) {
|
||
st.cam->Azimuth(360.0 / frames);
|
||
rw->Render();
|
||
}
|
||
const double msStill = swStill.elapsedMs();
|
||
const double fps = msStill > 0 ? frames * 1000.0 / msStill : 0.0;
|
||
|
||
// ② 交互态(降采样):高 DesiredUpdateRate(拖动目标 15fps)→ mapper 拉大 SampleDistance。
|
||
rw->SetDesiredUpdateRate(15.0); // 拖动目标帧率→降采样
|
||
rw->Render(); // 预热一帧让降采样步长生效
|
||
Stopwatch swInteract;
|
||
for (int f = 0; f < frames; ++f) {
|
||
st.cam->Azimuth(360.0 / frames);
|
||
rw->Render();
|
||
}
|
||
const double msInteract = swInteract.elapsedMs();
|
||
const double fpsInteract =
|
||
msInteract > 0 ? frames * 1000.0 / msInteract : 0.0;
|
||
rw->SetDesiredUpdateRate(0.5); // 复位
|
||
|
||
const bool texErr2 = capWin->textureError();
|
||
vtkOutputWindow::SetInstance(nullptr);
|
||
const bool ok = !texErr2 && defStruct > 0;
|
||
|
||
std::cout << "\n=== view --preview 离屏默认视角验证 ===\n";
|
||
std::cout << "默认局部段维度 : " << kViewDefaultLocalBricks
|
||
<< " brick 列(沿线中段) level0\n";
|
||
std::cout << "存图 : " << pngPath << "\n";
|
||
std::cout << "结构像素(>50) : " << defStruct << " / " << (winW * winH)
|
||
<< " (" << (100.0 * defStruct / (winW * winH))
|
||
<< "%, 已排除深蓝灰背景)\n";
|
||
std::cout << "纹理维度错误 : " << (texErr2 ? "是(!!)" : "否") << "\n";
|
||
const double speedup = fps > 0 ? fpsInteract / fps : 0.0;
|
||
std::cout << "全质量静态 fps : " << (ok ? std::to_string(fps) : "INVALID")
|
||
<< " (" << frames << " 帧旋相机, DesiredUpdateRate=0.5)\n";
|
||
std::cout << "交互态降采样 fps: "
|
||
<< (ok ? std::to_string(fpsInteract) : "INVALID") << " (" << frames
|
||
<< " 帧旋相机, DesiredUpdateRate=15)\n";
|
||
std::cout << "拖动态提速 : " << (ok ? std::to_string(speedup) : "INVALID")
|
||
<< "x (交互态/全质量)\n";
|
||
std::cout << "交互级(≥15fps) : "
|
||
<< (ok ? (fpsInteract >= 15.0 ? "是 ✔" : "否(未达拖动目标)")
|
||
: "INVALID")
|
||
<< "\n";
|
||
std::cout << "preview 结果 : "
|
||
<< (ok ? "OK ✔ 默认视角有结构" : "FAIL ✘") << "\n";
|
||
|
||
writeMetricLine(
|
||
"view-preview,dir=" + dir + ",exagg=" + std::to_string(exagg) +
|
||
",opacity=" + std::to_string(opacity) +
|
||
",localBricks=" + std::to_string(kViewDefaultLocalBricks) +
|
||
",structPixels=" + std::to_string(defStruct) +
|
||
",fpsStill=" + (ok ? std::to_string(fps) : "INVALID") +
|
||
",fpsInteract=" + (ok ? std::to_string(fpsInteract) : "INVALID") +
|
||
",speedup=" + (ok ? std::to_string(speedup) : "INVALID") +
|
||
",textureErr=" + std::to_string(texErr2 ? 1 : 0) +
|
||
",ok=" + std::to_string(ok ? 1 : 0) +
|
||
",png=" + pngPath);
|
||
return ok ? 0 : 1;
|
||
}
|
||
|
||
if (smoke) {
|
||
// 离屏 smoke:模拟一次缩放 → 验 LOD 切换 + 不崩(单纹理路径)。
|
||
const int lvlNear = st.lastLevel;
|
||
st.cam->Dolly(0.02); // 大幅拉远 → 期望切到粗 LOD(整卷粗层单纹理)
|
||
ren->ResetCameraClippingRange();
|
||
const std::size_t blocksFar = viewRefreshBlocking(&st);
|
||
const int lvlFar = st.lastLevel;
|
||
rw->Render();
|
||
st.cam->Dolly(50.0); // 拉近回来 → 期望切回细 LOD(level0 局部子区域)
|
||
ren->ResetCameraClippingRange();
|
||
viewRefreshBlocking(&st);
|
||
const int lvlNear2 = st.lastLevel;
|
||
rw->Render();
|
||
const vtkIdType nb2 = countNonBlackPixels(rw.Get(), winW, winH);
|
||
|
||
vtkOutputWindow::SetInstance(nullptr);
|
||
const bool lodSwitched = (lvlFar != lvlNear) || (lvlNear2 != lvlFar);
|
||
const bool ok = renderedOk && nb2 > 0 && !capWin->textureError();
|
||
std::cout << "\n=== view --smoke 离屏冒烟 ===\n";
|
||
std::cout << "近观 level=" << lvlNear << " → 拉远 level=" << lvlFar
|
||
<< " → 再拉近 level=" << lvlNear2 << "\n";
|
||
std::cout << "LOD 随缩放切换 : " << (lodSwitched ? "是 ✔" : "否(测点档位未跨界)")
|
||
<< " (blocksFar=" << blocksFar << ")\n";
|
||
std::cout << "纹理维度错误 : " << (textureErr ? "是(!!)" : "否") << "\n";
|
||
std::cout << "渲出非空像素 : " << (renderedOk ? "是" : "否(!!)")
|
||
<< " (近=" << nonBlack << " 远拉近=" << nb2 << ")\n";
|
||
std::cout << "smoke 结果 : " << (ok ? "OK ✔ 不崩" : "FAIL ✘") << "\n";
|
||
return ok ? 0 : 1;
|
||
}
|
||
|
||
vtkOutputWindow::SetInstance(nullptr);
|
||
if (!renderedOk) {
|
||
std::cout << "[view] 警告: 首帧未渲出非空像素(纹理错=" << textureErr
|
||
<< ");窗口仍开,供人工排查。\n";
|
||
}
|
||
|
||
// 真窗口交互:TrackballCamera + 每次交互结束重选 LOD + 刷 fps 文本。
|
||
vtkNew<vtkRenderWindowInteractor> iren;
|
||
iren->SetRenderWindow(rw);
|
||
vtkNew<vtkInteractorStyleTrackballCamera> style;
|
||
iren->SetInteractorStyle(style);
|
||
|
||
// C3-5:交互/静止目标帧率。interactor 在交互(拖动)中把渲染窗口 DesiredUpdateRate
|
||
// 拉到 DesiredUpdateRate(15)→mapper 自适应降采样(快、跟手);松手后落回 StillUpdateRate
|
||
// (0.5)→恢复小步长全质量。配合上面 mapper 的 AutoAdjust/InteractiveAdjust 才生效。
|
||
iren->SetDesiredUpdateRate(15.0);
|
||
iren->SetStillUpdateRate(0.5);
|
||
|
||
vtkNew<vtkCallbackCommand> cb;
|
||
cb->SetCallback(viewOnInteract);
|
||
cb->SetClientData(&st);
|
||
// EndInteraction:旋转/缩放松手后提交新目标 + 刷 fps(仅松手触发一次,不自激)。
|
||
// 注意:绝不可在 rw 的 EndEvent 上注册——回调内部 Render() 会再触发 EndEvent
|
||
// 形成无限递归重渲(窗口卡死、fps≈0)。fps 文本在松手时刷新即可。
|
||
iren->AddObserver(vtkCommand::EndInteractionEvent, cb);
|
||
|
||
// C3-2:拖动进行中持续提交目标(非阻塞),主线程不被重组卡住 → 跟手。
|
||
vtkNew<vtkCallbackCommand> cbInteract;
|
||
cbInteract->SetCallback(viewOnInteracting);
|
||
cbInteract->SetClientData(&st);
|
||
iren->AddObserver(vtkCommand::InteractionEvent, cbInteract);
|
||
|
||
// C3-2:周期定时器非阻塞拉取后台已就绪纹理换上(新 LOD 备好即显示,拖动不卡)。
|
||
vtkNew<vtkCallbackCommand> cbTimer;
|
||
cbTimer->SetCallback(viewOnTimer);
|
||
cbTimer->SetClientData(&st);
|
||
iren->AddObserver(vtkCommand::TimerEvent, cbTimer);
|
||
|
||
std::cout << "[view] 打开真窗口。左键旋转 / 滚轮缩放(切 LOD) / q 退出。\n";
|
||
iren->Initialize();
|
||
iren->CreateRepeatingTimer(33); // ~30Hz 拉取后台就绪纹理(不阻塞主线程)
|
||
rw->Render();
|
||
iren->Start();
|
||
|
||
std::cout << "[view] 窗口关闭,退出。\n";
|
||
return 0;
|
||
}
|
||
|
||
// ============================================================================
|
||
// 12d-polish:梯度不透明度 + 光照 打磨探针(验证"体内部白雾"能否靠打磨解决)
|
||
// ============================================================================
|
||
//
|
||
// 当前体绘制对道路 GPR 水平层数据,体中间是均匀白雾、只有端面有层次。本探针在同一
|
||
// 全分辨率(level0)局部段 + 同一「看进体内部」视角(斜穿俯视,视线穿过体内部而非只看
|
||
// 端面)下渲 3 张离屏对比图,验证:给体加【梯度不透明度】(均匀区透明、层界面显出) +
|
||
// 【光照/明暗】能否让内部层状结构"浮"出来:
|
||
// polish-a-value.png 基线:按数值的不透明度(V形包络),无梯度不透明度、无光照
|
||
// polish-b-grad.png + 梯度不透明度(SetGradientOpacity)
|
||
// polish-c-grad-shade.png + 梯度不透明度 + 光照(ShadeOn, Ambient/Diffuse/Specular)
|
||
//
|
||
// 梯度不透明度的 piecewise 按【实际梯度幅值分布】标定阈值(不靠猜):先在量化域逐体素
|
||
// 采样 6 邻居中心差分梯度幅值,取分位数(median / p90)作斜坡控制点。
|
||
|
||
// 量化域标量不透明度峰值。基线(a)用 0.15(与默认体绘制同档→均匀积分白雾);开了梯度
|
||
// 不透明度(b/c)后均匀区被梯度门压成透明,可放心把标量峰值提到 0.6,让【层界面】这类
|
||
// 高梯度处的净不透明度(标量×梯度)足够高、层面真正"浮"成实面,而非仍是淡影。
|
||
constexpr double kPolishMaxOpacityFog = 0.15; // a:基线白雾
|
||
constexpr double kPolishMaxOpacityGrad = 0.6; // b/c:梯度门控后可提高
|
||
|
||
// 在 VTK_SHORT 局部体上采样梯度幅值分布(量化域,中心差分),返回有序的若干分位数。
|
||
// 跳过 kBlank 体素及其邻居(空值不参与梯度)。返回 {median, p75, p90, p99, max}。
|
||
// (GradStats 已在文件上方前置声明,供 C4 画廊共用。)
|
||
GradStats sampleGradientMagnitude(vtkImageData* img) {
|
||
int dims[3];
|
||
img->GetDimensions(dims);
|
||
const int nx = dims[0], ny = dims[1], nz = dims[2];
|
||
auto* arr = vtkShortArray::SafeDownCast(img->GetPointData()->GetScalars());
|
||
GradStats gs;
|
||
if (!arr || nx < 3 || ny < 3 || nz < 3) return gs;
|
||
const std::int16_t blank = geopro::core::ScalarVolumeI16::kBlank;
|
||
auto at = [&](int i, int j, int k) -> std::int16_t {
|
||
const vtkIdType id = (static_cast<vtkIdType>(k) * ny + j) * nx + i;
|
||
return arr->GetValue(id);
|
||
};
|
||
std::vector<double> mags;
|
||
mags.reserve(static_cast<std::size_t>(nx) * ny * nz / 8 + 1);
|
||
// 步长抽样:大体不必逐体素,间隔取样即可代表分布(≤~50万样本)。
|
||
const vtkIdType total = static_cast<vtkIdType>(nx - 2) * (ny - 2) * (nz - 2);
|
||
const int stride = static_cast<int>(std::max<vtkIdType>(1, total / 500000));
|
||
vtkIdType counter = 0;
|
||
for (int k = 1; k < nz - 1; ++k) {
|
||
for (int j = 1; j < ny - 1; ++j) {
|
||
for (int i = 1; i < nx - 1; ++i) {
|
||
if ((counter++ % stride) != 0) continue;
|
||
const std::int16_t c = at(i, j, k);
|
||
if (c == blank) continue;
|
||
const std::int16_t xm = at(i - 1, j, k), xp = at(i + 1, j, k);
|
||
const std::int16_t ym = at(i, j - 1, k), yp = at(i, j + 1, k);
|
||
const std::int16_t zm = at(i, j, k - 1), zp = at(i, j, k + 1);
|
||
if (xm == blank || xp == blank || ym == blank || yp == blank ||
|
||
zm == blank || zp == blank) {
|
||
continue;
|
||
}
|
||
const double gx = 0.5 * (xp - xm);
|
||
const double gy = 0.5 * (yp - ym);
|
||
const double gz = 0.5 * (zp - zm);
|
||
mags.push_back(std::sqrt(gx * gx + gy * gy + gz * gz));
|
||
}
|
||
}
|
||
}
|
||
if (mags.empty()) return gs;
|
||
std::sort(mags.begin(), mags.end());
|
||
auto q = [&](double p) {
|
||
const std::size_t idx = static_cast<std::size_t>(
|
||
std::min<double>(mags.size() - 1, p * (mags.size() - 1)));
|
||
return mags[idx];
|
||
};
|
||
gs.median = q(0.50);
|
||
gs.p75 = q(0.75);
|
||
gs.p90 = q(0.90);
|
||
gs.p99 = q(0.99);
|
||
gs.mx = mags.back();
|
||
gs.samples = mags.size();
|
||
return gs;
|
||
}
|
||
|
||
// 标量分位标定实现(P3):步长抽样该 VTK_SHORT 体的非空体素值,排序取 pLo/pHi 分位,
|
||
// 用 quant.toPhys 反算成物理端点。失败/退化(无样本或 lo>=hi)返回 samples=0,由调用方
|
||
// 回退到 meta 全量化域。仿 sampleGradientMagnitude 的 stride/blank 处理,自适应该体值域。
|
||
ScalarPercentiles sampleScalarPercentiles(vtkImageData* img,
|
||
const geopro::core::Quant& q,
|
||
double pLo, double pHi) {
|
||
ScalarPercentiles out;
|
||
if (!img) return out;
|
||
int dims[3];
|
||
img->GetDimensions(dims);
|
||
const vtkIdType npts =
|
||
static_cast<vtkIdType>(dims[0]) * dims[1] * dims[2];
|
||
auto* arr = vtkShortArray::SafeDownCast(img->GetPointData()->GetScalars());
|
||
if (!arr || npts <= 0) return out;
|
||
const std::int16_t blank = geopro::core::ScalarVolumeI16::kBlank;
|
||
std::vector<std::int16_t> vals;
|
||
vals.reserve(static_cast<std::size_t>(npts) / 4 + 1);
|
||
// 步长抽样:~50 万样本足以代表分布(与梯度采样同档)。
|
||
const vtkIdType stride = std::max<vtkIdType>(1, npts / 500000);
|
||
for (vtkIdType id = 0; id < npts; id += stride) {
|
||
const std::int16_t v = arr->GetValue(id);
|
||
if (v == blank) continue;
|
||
vals.push_back(v);
|
||
}
|
||
if (vals.empty()) return out;
|
||
std::sort(vals.begin(), vals.end());
|
||
auto pick = [&](double p) {
|
||
const std::size_t idx = static_cast<std::size_t>(
|
||
std::min<double>(vals.size() - 1, p * (vals.size() - 1)));
|
||
return static_cast<double>(vals[idx]);
|
||
};
|
||
const double qLo = pick(pLo);
|
||
const double qHi = pick(pHi);
|
||
if (!(qLo < qHi)) return out; // 退化(常数体)→ 调用方回退全域
|
||
out.lo = q.toPhys(static_cast<std::int16_t>(std::lround(qLo)));
|
||
out.hi = q.toPhys(static_cast<std::int16_t>(std::lround(qHi)));
|
||
out.samples = vals.size();
|
||
return out;
|
||
}
|
||
|
||
// 标量不透明度:V 形包络(与 makeSolidVolumeProperty 同思路,floor/mid/max),三图共用,
|
||
// 保证唯一变量是梯度不透明度 / 光照。
|
||
void setPolishScalarOpacity(vtkVolumeProperty* prop, const geopro::core::Quant& q,
|
||
double vminPhys, double vmaxPhys, double maxOpacity) {
|
||
if (vminPhys >= vmaxPhys) vmaxPhys = vminPhys + 1.0;
|
||
const double qminD = static_cast<double>(q.toQ(vminPhys));
|
||
const double qmaxD = static_cast<double>(q.toQ(vmaxPhys));
|
||
const double qmid = 0.5 * (qminD + qmaxD);
|
||
const double half = 0.5 * (qmaxD - qminD);
|
||
const double floorOp = 0.4 * maxOpacity; // 中段背景按峰值比例压低(V 形)
|
||
vtkNew<vtkPiecewiseFunction> opacity;
|
||
opacity->AddPoint(
|
||
static_cast<double>(geopro::core::ScalarVolumeI16::kBlank), 0.0);
|
||
opacity->AddPoint(qminD, maxOpacity);
|
||
opacity->AddPoint(qmid - 0.55 * half, floorOp);
|
||
opacity->AddPoint(qmid, 0.2 * maxOpacity);
|
||
opacity->AddPoint(qmid + 0.55 * half, floorOp);
|
||
opacity->AddPoint(qmaxD, maxOpacity);
|
||
prop->SetScalarOpacity(opacity);
|
||
}
|
||
|
||
// 颜色传函(量化域,seismic 红白蓝,与其余探针同思路)。
|
||
void setPolishColor(vtkVolumeProperty* prop, const geopro::core::Quant& q,
|
||
const geopro::core::ColorScale& cs, double vminPhys,
|
||
double vmaxPhys) {
|
||
constexpr int kSamples = 64;
|
||
if (vminPhys >= vmaxPhys) vmaxPhys = vminPhys + 1.0;
|
||
const double qminD = static_cast<double>(q.toQ(vminPhys));
|
||
const double qmaxD = static_cast<double>(q.toQ(vmaxPhys));
|
||
vtkNew<vtkColorTransferFunction> color;
|
||
for (int t = 0; t < kSamples; ++t) {
|
||
const double qd = qminD + (qmaxD - qminD) * t / (kSamples - 1);
|
||
const auto qv = static_cast<std::int16_t>(std::lround(qd));
|
||
const auto c = cs.colorAt(q.toPhys(qv));
|
||
color->AddRGBPoint(qd, c.r / 255.0, c.g / 255.0, c.b / 255.0);
|
||
}
|
||
prop->SetColor(color);
|
||
}
|
||
|
||
// 共用:把局部体喂单 SmartVolumeMapper,按一个「看进体内部」的相机取景渲一帧 + 存 PNG,
|
||
// 报告 结构像素 / 平均亮度 / fps。mode: 0=value 基线 1=+grad 2=+grad+shade。
|
||
int renderPolishOne(vtkImageData* locImg, const geopro::core::Quant& quant,
|
||
const geopro::core::ColorScale& cs, double vmin, double vmax,
|
||
const GradStats& gs, int mode, double exagg,
|
||
const std::string& pngPath, int frames, double elevation,
|
||
double azimuth, double zoom) {
|
||
const int winW = 1280, winH = 800;
|
||
auto rw = makeOffscreenWindow(winW, winH);
|
||
vtkNew<vtkRenderer> ren;
|
||
ren->SetBackground(0.05, 0.05, 0.09);
|
||
rw->AddRenderer(ren);
|
||
|
||
vtkNew<vtkSmartVolumeMapper> mapper;
|
||
mapper->SetInputData(locImg);
|
||
mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
|
||
mapper->SetAutoAdjustSampleDistances(0);
|
||
mapper->SetInteractiveAdjustSampleDistances(0);
|
||
|
||
auto prop = vtkSmartPointer<vtkVolumeProperty>::New();
|
||
setPolishColor(prop, quant, cs, vmin, vmax);
|
||
const double scalarMax =
|
||
(mode == 0) ? kPolishMaxOpacityFog : kPolishMaxOpacityGrad;
|
||
setPolishScalarOpacity(prop, quant, vmin, vmax, scalarMax);
|
||
prop->SetInterpolationTypeToLinear();
|
||
|
||
// 梯度不透明度(mode>=1):梯度小(均匀区)→透明、梯度大(层界面)→不透明。
|
||
// 阈值按实测分布:median 处仍接近 0(压住均匀积分雾),p90 升到 0.5,p99 到 0.9。
|
||
if (mode >= 1) {
|
||
vtkNew<vtkPiecewiseFunction> grad;
|
||
grad->AddPoint(0.0, 0.0);
|
||
grad->AddPoint(std::max(1.0, gs.median), 0.0);
|
||
grad->AddPoint(std::max(2.0, gs.p90), 0.5);
|
||
grad->AddPoint(std::max(3.0, gs.p99), 0.9);
|
||
prop->SetGradientOpacity(grad);
|
||
}
|
||
|
||
// 光照(mode>=2):ShadeOn + Ambient/Diffuse/Specular,让层界面带立体明暗。
|
||
if (mode >= 2) {
|
||
prop->ShadeOn();
|
||
prop->SetAmbient(0.3);
|
||
prop->SetDiffuse(0.7);
|
||
prop->SetSpecular(0.2);
|
||
prop->SetSpecularPower(10.0);
|
||
} else {
|
||
prop->ShadeOff();
|
||
}
|
||
|
||
auto volume = vtkSmartPointer<vtkVolume>::New();
|
||
volume->SetMapper(mapper);
|
||
volume->SetProperty(prop);
|
||
volume->SetScale(1.0, exagg, exagg); // 垂向夸张:薄轴放大,截面结构才看得出
|
||
ren->AddVolume(volume);
|
||
|
||
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
|
||
vtkOutputWindow::SetInstance(capWin);
|
||
|
||
// 「看进体内部」取景:斜穿俯视——较大 Elevation 让视线从上方斜穿过体内部(而非只看
|
||
// 端面),配合垂向夸张后体呈板状,俯视看穿层间。Zoom 填满画面。
|
||
ren->ResetCamera();
|
||
vtkCamera* cam = ren->GetActiveCamera();
|
||
cam->Elevation(elevation);
|
||
cam->Azimuth(azimuth);
|
||
cam->Zoom(zoom);
|
||
ren->ResetCameraClippingRange();
|
||
rw->Render();
|
||
|
||
// 结构像素:任一通道 >50(排除暗背景)。
|
||
auto countStructPixels = [&]() -> vtkIdType {
|
||
auto px = vtkSmartPointer<vtkUnsignedCharArray>::New();
|
||
rw->GetRGBACharPixelData(0, 0, winW - 1, winH - 1, /*front=*/1, px);
|
||
vtkIdType n = 0;
|
||
const vtkIdType np = px->GetNumberOfTuples();
|
||
for (vtkIdType i = 0; i < np; ++i) {
|
||
if (px->GetComponent(i, 0) > 50 || px->GetComponent(i, 1) > 50 ||
|
||
px->GetComponent(i, 2) > 50) {
|
||
++n;
|
||
}
|
||
}
|
||
return n;
|
||
};
|
||
// 高于背景像素:背景为 (0.05,0.05,0.09)≈RGB(13,13,23),阈值 35 干净地把渲出的体
|
||
// 结构与背景分开(区别于结构像素>50:>35 能纳入梯度门控后偏暗但确有的层面)。
|
||
auto countAboveBg = [&]() -> vtkIdType {
|
||
auto px = vtkSmartPointer<vtkUnsignedCharArray>::New();
|
||
rw->GetRGBACharPixelData(0, 0, winW - 1, winH - 1, /*front=*/1, px);
|
||
vtkIdType n = 0;
|
||
const vtkIdType np = px->GetNumberOfTuples();
|
||
for (vtkIdType i = 0; i < np; ++i) {
|
||
if (px->GetComponent(i, 0) > 35 || px->GetComponent(i, 1) > 35 ||
|
||
px->GetComponent(i, 2) > 35) {
|
||
++n;
|
||
}
|
||
}
|
||
return n;
|
||
};
|
||
const vtkIdType structPx = countStructPixels();
|
||
const vtkIdType aboveBg = countAboveBg();
|
||
const double bright = meanBrightness(rw.Get(), winW, winH);
|
||
|
||
savePng(rw.Get(), pngPath);
|
||
|
||
// fps:旋相机 frames 帧(保持大俯角,绕 Azimuth)。
|
||
rw->Render();
|
||
Stopwatch sw;
|
||
for (int f = 0; f < frames; ++f) {
|
||
cam->Azimuth(360.0 / frames);
|
||
rw->Render();
|
||
}
|
||
const double ms = sw.elapsedMs();
|
||
const double fps = ms > 0 ? frames * 1000.0 / ms : 0.0;
|
||
const bool texErr = capWin->textureError();
|
||
vtkOutputWindow::SetInstance(nullptr);
|
||
// 有效判据:无纹理错 + 渲出高于背景的像素(>35)。结构像素(>50)仅作亮度强弱度量,
|
||
// 不作有效门——梯度门控后层面偏暗但确有渲出,不应误判为空。
|
||
const bool ok = !texErr && aboveBg > 0;
|
||
|
||
const char* label =
|
||
mode == 0 ? "a-value(基线)"
|
||
: (mode == 1 ? "b-grad(+梯度不透明度)" : "c-grad-shade(+梯度+光照)");
|
||
std::cout << "\n--- polish " << label << " ---\n";
|
||
std::cout << "存图 : " << pngPath << "\n";
|
||
std::cout << "高于背景像素(>35): " << aboveBg << " / " << (winW * winH) << " ("
|
||
<< (100.0 * aboveBg / (winW * winH)) << "%)\n";
|
||
std::cout << "结构像素(>50) : " << structPx << " / " << (winW * winH) << " ("
|
||
<< (100.0 * structPx / (winW * winH)) << "%)\n";
|
||
std::cout << "平均亮度(0-255) : " << bright << "\n";
|
||
std::cout << "真实 fps : " << (ok ? std::to_string(fps) : "INVALID")
|
||
<< " (" << frames << " 帧旋相机)\n";
|
||
std::cout << "结果 : " << (ok ? "OK" : "FAIL(纹理错或空渲染)") << "\n";
|
||
|
||
writeMetricLine(
|
||
"polish," + std::string(mode == 0 ? "a-value" : mode == 1 ? "b-grad"
|
||
: "c-grad-shade") +
|
||
",aboveBg=" + std::to_string(aboveBg) +
|
||
",structPx=" + std::to_string(structPx) +
|
||
",bright=" + std::to_string(bright) +
|
||
",fps=" + (ok ? std::to_string(fps) : "INVALID") +
|
||
",texErr=" + std::to_string(texErr ? 1 : 0) + ",png=" + pngPath);
|
||
return ok ? 0 : 1;
|
||
}
|
||
|
||
int cmdPolish(int argc, char** argv) {
|
||
const Args a = parseArgs(argc, argv, 2);
|
||
if (a.positional.empty()) {
|
||
std::cerr << "用法: gpr_poc polish <storeDir> [--exagg 8] [--frames 90] "
|
||
"[--localBricks 4]\n";
|
||
return 2;
|
||
}
|
||
const std::string dir = a.positional[0];
|
||
const double exagg = std::stod(a.get("exagg", "8"));
|
||
const int frames = std::stoi(a.get("frames", "90"));
|
||
const int localBricks = std::stoi(a.get("localBricks", "4"));
|
||
// 「看进体内部」取景:斜穿俯视。默认 El45/Az30/Zoom1.5(视线从上方斜穿层间,
|
||
// 既不是纯端面也不至于过陡退化成边缘线)。可命令行覆盖以微调。
|
||
const double elevation = std::stod(a.get("elevation", "45"));
|
||
const double azimuth = std::stod(a.get("azimuth", "30"));
|
||
const double zoom = std::stod(a.get("zoom", "1.5"));
|
||
std::cout << "[polish] storeDir=" << dir << " exagg=" << exagg
|
||
<< " frames=" << frames << " localBricks=" << localBricks << "\n";
|
||
|
||
std::cout << "[polish] 离屏闸门复检...\n";
|
||
if (cmdOffscreenSmoke() != 0) {
|
||
std::cout << "[polish] 闸门失败,中止。\n";
|
||
return 1;
|
||
}
|
||
|
||
geopro::data::ChunkedVolumeStore store(dir);
|
||
const geopro::data::StoreMeta& m = store.meta();
|
||
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
|
||
const geopro::core::ColorScale cs = makeSeismicColorScale(vmin, vmax);
|
||
|
||
// 全分辨率 level0 局部段(沿线中段),三图共用同一体。
|
||
const int totBx = store.bricksX(0);
|
||
const int localBx = std::min(localBricks, totBx);
|
||
const int bx0 = std::max(0, totBx / 2 - localBx / 2);
|
||
vtkSmartPointer<vtkImageData> locImg =
|
||
buildLocalLevel0Image(store, m, bx0, localBx);
|
||
int locDims[3];
|
||
locImg->GetDimensions(locDims);
|
||
std::cout << "[polish] level0 局部段 " << locDims[0] << "x" << locDims[1] << "x"
|
||
<< locDims[2] << " (brick列 [" << bx0 << "," << (bx0 + localBx)
|
||
<< ") / " << totBx << ")\n";
|
||
|
||
// 标定梯度不透明度阈值:采样实际梯度幅值分布。
|
||
const GradStats gs = sampleGradientMagnitude(locImg.Get());
|
||
std::cout << "[polish] 梯度幅值分布(量化域,样本 " << gs.samples
|
||
<< "): median=" << gs.median << " p75=" << gs.p75
|
||
<< " p90=" << gs.p90 << " p99=" << gs.p99 << " max=" << gs.mx
|
||
<< "\n";
|
||
std::cout << "[polish] 梯度不透明度 piecewise: grad<=" << std::max(1.0, gs.median)
|
||
<< "→0.0 grad=" << std::max(2.0, gs.p90) << "→0.5 grad>="
|
||
<< std::max(3.0, gs.p99) << "→0.9\n";
|
||
|
||
const fs::path shotDir =
|
||
fs::path("docs") / "superpowers" / "plans" / "poc-lod-shots";
|
||
fs::create_directories(shotDir);
|
||
|
||
int rc = 0;
|
||
rc |= renderPolishOne(locImg.Get(), m.quant, cs, vmin, vmax, gs, /*mode=*/0,
|
||
exagg, (shotDir / "polish-a-value.png").string(), frames,
|
||
elevation, azimuth, zoom);
|
||
rc |= renderPolishOne(locImg.Get(), m.quant, cs, vmin, vmax, gs, /*mode=*/1,
|
||
exagg, (shotDir / "polish-b-grad.png").string(), frames,
|
||
elevation, azimuth, zoom);
|
||
rc |= renderPolishOne(locImg.Get(), m.quant, cs, vmin, vmax, gs, /*mode=*/2,
|
||
exagg, (shotDir / "polish-c-grad-shade.png").string(),
|
||
frames, elevation, azimuth, zoom);
|
||
|
||
std::cout << "\n[polish] 完成,3 张对比图存于 " << shotDir.string()
|
||
<< " (polish-a-value / polish-b-grad / polish-c-grad-shade)\n";
|
||
return rc;
|
||
}
|
||
|
||
void usage() {
|
||
std::cerr << "gpr_poc —— POC-B headless 度量 CLI\n"
|
||
" gpr_poc build <dir> [--line 001] [--cellXY 0.2] "
|
||
"[--cellZ 0.05] [--out <storeDir>] [--levels 2]\n"
|
||
" gpr_poc build-stream <dir> [--cellXY 0.05] [--cellZ 0.05] "
|
||
"[--out <storeDir>] [--levels 3] [--sliceXBricks 8] "
|
||
"[--maxLines N]\n"
|
||
" gpr_poc build-geo <dir> [--cellXY 0.5] [--cellZ 0.1] "
|
||
"[--out <storeDir>] [--levels 4] [--maxLines N] [--curvilinear]\n"
|
||
" gpr_poc build-line <lineDir> <linePrefix> --out <storeDir> "
|
||
"[--levels 3] [--coarse F]\n"
|
||
" gpr_poc build-all <lineDir> --outBase <baseDir> "
|
||
"[--levels 3] [--coarse F] [--minFreeGB 3]\n"
|
||
" gpr_poc build-survey-line <lineDir> <linePrefix> --out "
|
||
"<storeDir> [--levels 3] [--coarse F] [--cell 0.05] "
|
||
"[--radius 0.5] [--gps <path>]\n"
|
||
" gpr_poc build-survey-all <lineDir> --outBase <baseDir> "
|
||
"[--levels 3] [--coarse F] [--cell 0.05] [--radius 0.5] "
|
||
"[--minFreeGB 3]\n"
|
||
" gpr_poc load <storeDir>\n"
|
||
" gpr_poc selftest\n"
|
||
" gpr_poc offscreen-smoke\n"
|
||
" gpr_poc renderB <storeDir> [--frames 120]\n"
|
||
" gpr_poc renderC <storeDir> [--budget 64] [--frames 120]\n"
|
||
" gpr_poc renderC-partitioned <storeDir> [--frames 120]\n"
|
||
" gpr_poc renderLOD <storeDir> [--frames 120]\n"
|
||
" gpr_poc tune <storeDir> [--opacity 0.5] [--exagg 8] "
|
||
"[--frames 120] [--localBricks 4]\n"
|
||
" gpr_poc fps-budget <storeDir> [--frames 90] "
|
||
"[--bricks 4,16,64,128,256]\n"
|
||
" 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 view-survey-all <storesDir> [--preview] [--exagg 8] "
|
||
"[--level 1] [--shotDir <dir>]\n"
|
||
" gpr_poc polish <storeDir> [--exagg 8] [--frames 90] "
|
||
"[--localBricks 4]\n";
|
||
}
|
||
|
||
} // namespace
|
||
|
||
int main(int argc, char** argv) {
|
||
#ifdef _WIN32
|
||
// Windows 控制台默认 GBK,会把 UTF-8 中文输出显示为乱码。设为 UTF-8 码页修复。
|
||
SetConsoleOutputCP(CP_UTF8);
|
||
#endif
|
||
if (argc < 2) {
|
||
usage();
|
||
return 2;
|
||
}
|
||
const std::string cmd = argv[1];
|
||
try {
|
||
if (cmd == "build") return cmdBuild(argc, argv);
|
||
if (cmd == "build-stream") return cmdBuildStream(argc, argv);
|
||
if (cmd == "build-geo") return cmdBuildGeo(argc, argv);
|
||
if (cmd == "build-line") return cmdBuildLine(argc, argv);
|
||
if (cmd == "build-all") return cmdBuildAll(argc, argv);
|
||
if (cmd == "ess-stat") return cmdEssStat(argc, argv);
|
||
if (cmd == "passcost") return cmdPassCost(argc, argv);
|
||
if (cmd == "slice") return cmdSlice(argc, argv);
|
||
if (cmd == "build-survey-line") return cmdBuildSurveyLine(argc, argv);
|
||
if (cmd == "build-survey-all") return cmdBuildSurveyAll(argc, argv);
|
||
if (cmd == "load") return cmdLoad(argc, argv);
|
||
if (cmd == "selftest") return cmdSelftest();
|
||
if (cmd == "offscreen-smoke") return cmdOffscreenSmoke();
|
||
if (cmd == "renderB") return cmdRenderB(argc, argv);
|
||
if (cmd == "renderC") return cmdRenderC(argc, argv);
|
||
if (cmd == "renderC-partitioned")
|
||
return cmdRenderCPartitioned(argc, argv);
|
||
if (cmd == "renderLOD") return cmdRenderLOD(argc, 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 == "view-survey-all") return cmdViewSurveyAll(argc, argv);
|
||
if (cmd == "polish") return cmdPolish(argc, argv);
|
||
} catch (const std::exception& e) {
|
||
std::cerr << "错误: " << e.what() << "\n";
|
||
return 1;
|
||
}
|
||
usage();
|
||
return 2;
|
||
}
|