feat(gpr): build-line 健壮支持任意测线 + 新增 build-all 批量建体(磁盘守护/coarse 下采样)

- 桥接 buildLineVolumeFromGpr3dv 增 coarse 参数:沿测线(道/X 轴)按步长下采样,
  spacing.x ×stride 保持真实世界尺度;通道/样本保留全分辨率。默认 coarse=1 全分辨率,
  对现有调用与测试零影响。
- build-line 增 --coarse F;单线建体核心抽出 buildOneLine,加体维度退化守护
  (短桩线 nx<2 等报因跳过不落盘),整条 try/catch 不崩。
- 新增 build-all <lineDir> --outBase <baseDir> [--levels N] [--coarse F]
  [--minFreeGB G]:扫 *_A<NN>.iprh 发现全部测线,逐条建到 baseDir/<line>/;
  每条建前查磁盘剩余,低于阈值(默认 3G)停并报已建哪些;单条异常捕获跳过不中断批量;
  末尾打印各线状态(成功/跳过+因/维度/大小)与合计占用。
- 通道/道/样本全从数据读,不写死;不破坏现有 gpr/bridge 测试(12/12 通过)。
This commit is contained in:
gaozheng 2026-06-25 08:42:31 +08:00
parent 4330e12c3e
commit 4e2bdc3b81
3 changed files with 202 additions and 32 deletions

View File

@ -67,7 +67,9 @@ double nowMs(std::chrono::steady_clock::time_point t0) {
geopro::core::BuiltI16 buildLineVolumeFromGpr3dv(const std::string& lineDir, geopro::core::BuiltI16 buildLineVolumeFromGpr3dv(const std::string& lineDir,
const std::string& linePrefix, const std::string& linePrefix,
BridgeMetrics* metricsOut) { BridgeMetrics* metricsOut,
int coarse) {
const int stride = coarse > 1 ? coarse : 1; // 沿测线下采样步长(≥1)
const QString dir = QString::fromLocal8Bit(lineDir.c_str()); const QString dir = QString::fromLocal8Bit(lineDir.c_str());
const QString base = QString::fromLocal8Bit(linePrefix.c_str()); const QString base = QString::fromLocal8Bit(linePrefix.c_str());
@ -103,7 +105,9 @@ geopro::core::BuiltI16 buildLineVolumeFromGpr3dv(const std::string& lineDir,
std::to_string(traces) + "/" + std::to_string(traces) + "/" +
std::to_string(samples) + ")"); std::to_string(samples) + ")");
} }
const int nx = traces; // X=道(沿测线) // 下采样后输出道数(向上取整保留末道附近)nxOut = ceil(traces/stride)。
const int nxOut = (traces + stride - 1) / stride;
const int nx = nxOut; // X=道(沿测线,已按 stride 下采样)
const int ny = channels; // Y=通道(横向) const int ny = channels; // Y=通道(横向)
const int nz = samples; // Z=样本(深度) const int nz = samples; // Z=样本(深度)
@ -135,17 +139,19 @@ geopro::core::BuiltI16 buildLineVolumeFromGpr3dv(const std::string& lineDir,
quant.offset = 0.5 * (vmin + vmax); // 中点 → 防溢出 quant.offset = 0.5 * (vmin + vmax); // 中点 → 防溢出
// 4) 逐 (ch,trace,sample) 填体。GPR 立方体为稠密体(每体素有值),无空洞 → 不置 kBlank。 // 4) 逐 (ch,trace,sample) 填体。GPR 立方体为稠密体(每体素有值),无空洞 → 不置 kBlank。
// 沿测线按 stride 下采样:输出道 to → 源道 t = to*stride。
geopro::core::BuiltI16 built; geopro::core::BuiltI16 built;
built.vol = geopro::core::ScalarVolumeI16(nx, ny, nz); built.vol = geopro::core::ScalarVolumeI16(nx, ny, nz);
for (int c = 0; c < channels; ++c) { for (int c = 0; c < channels; ++c) {
const auto& chData = processed.volumeData[c]; const auto& chData = processed.volumeData[c];
for (int t = 0; t < traces; ++t) { for (int to = 0; to < nxOut; ++to) {
const bool hasTrace = t < chData.size(); const int t = to * stride;
const bool hasTrace = t < static_cast<int>(chData.size());
for (int s = 0; s < samples; ++s) { for (int s = 0; s < samples; ++s) {
short v = 0; short v = 0;
if (hasTrace && s < chData[t].size()) v = chData[t][s]; if (hasTrace && s < static_cast<int>(chData[t].size())) v = chData[t][s];
// X=道 t、Y=通道 c、Z=样本 s。 // X=输出道 to、Y=通道 c、Z=样本 s。
built.vol.at(t, c, s) = quant.toQ(static_cast<double>(v)); built.vol.at(to, c, s) = quant.toQ(static_cast<double>(v));
} }
} }
} }
@ -153,7 +159,9 @@ geopro::core::BuiltI16 buildLineVolumeFromGpr3dv(const std::string& lineDir,
// 5) 几何origin=0spacing 按 X=道距 / Y=通道横距 / Z=深度采样距。 // 5) 几何origin=0spacing 按 X=道距 / Y=通道横距 / Z=深度采样距。
const GPRDataModel::Header& h = processed.header; const GPRDataModel::Header& h = processed.header;
const double dx = h.distanceInc > 1e-9 ? h.distanceInc : 1.0; // 下采样后相邻输出道在世界中跨 stride 个原始道距 → dx ×stride 保持真实尺度。
const double dxBase = h.distanceInc > 1e-9 ? h.distanceInc : 1.0;
const double dx = dxBase * stride;
const double dy = channelSpacingY(h, channels); const double dy = channelSpacingY(h, channels);
const double dz = depthSpacingZ(h); const double dz = depthSpacingZ(h);

View File

@ -38,10 +38,13 @@ struct BridgeMetrics {
// 走 P1 链得处理后立方体 → 量化映射成 geopro BuiltI16(轴 X=道/Y=通道/Z=样本)。 // 走 P1 链得处理后立方体 → 量化映射成 geopro BuiltI16(轴 X=道/Y=通道/Z=样本)。
// lineDir/linePrefix 同 gpr3dv-smoke(如 "D:/Downloads/明星路", "明星路_001")。 // lineDir/linePrefix 同 gpr3dv-smoke(如 "D:/Downloads/明星路", "明星路_001")。
// metricsOut 非空时回填维度/量化/spacing/耗时(供 CLI 报告,不编造)。 // metricsOut 非空时回填维度/量化/spacing/耗时(供 CLI 报告,不编造)。
// coarse(下采样因子≥1):沿测线(道/X 轴)每 coarse 道取 1spacing.x ×coarse 保形;
// 通道/样本(横向/深度)保留全分辨率。coarse≤1 即全分辨率。磁盘紧张时省空间用。
// 失败(加载失败/立方体为空) → 抛 std::runtime_error。 // 失败(加载失败/立方体为空) → 抛 std::runtime_error。
geopro::core::BuiltI16 buildLineVolumeFromGpr3dv(const std::string& lineDir, geopro::core::BuiltI16 buildLineVolumeFromGpr3dv(const std::string& lineDir,
const std::string& linePrefix, const std::string& linePrefix,
BridgeMetrics* metricsOut); BridgeMetrics* metricsOut,
int coarse = 1);
} // namespace geopro::io::gpr } // namespace geopro::io::gpr

View File

@ -20,6 +20,7 @@
#include <fstream> #include <fstream>
#include <iostream> #include <iostream>
#include <map> #include <map>
#include <set>
#include <string> #include <string>
#include <thread> #include <thread>
#include <vector> #include <vector>
@ -808,33 +809,60 @@ int cmdBuildGeo(int argc, char** argv) {
// volumeData[通道][道][样本],再桥接(Gpr3dvVolumeBridge)成 geopro int16 量化体 // volumeData[通道][道][样本],再桥接(Gpr3dvVolumeBridge)成 geopro int16 量化体
// (轴 X=道/Y=通道/Z=样本),落 ChunkedVolumeStore + buildPyramid。原样渲(14 格 Y // (轴 X=道/Y=通道/Z=样本),落 ChunkedVolumeStore + buildPyramid。原样渲(14 格 Y
// 薄体),不做横向插值加密。 // 薄体),不做横向插值加密。
int cmdBuildLine(int argc, char** argv) { // 磁盘剩余空间(GB):查 path 所在卷可用字节。失败(路径不存在等)→ -1(视为未知)。
const Args a = parseArgs(argc, argv, 2); double freeSpaceGB(const std::string& path) {
if (a.positional.size() < 2) { std::error_code ec;
std::cerr << "用法: gpr_poc build-line <lineDir> <linePrefix> " // 用已存在的父目录查(out 目录可能还没建)。
"--out <storeDir> [--levels 3]\n" fs::path p = fs::absolute(path, ec);
"例: gpr_poc build-line \"D:/Downloads/明星路\" 明星路_001 " while (!p.empty() && !fs::exists(p, ec)) p = p.parent_path();
"--out tmp/line001_proc --levels 3\n"; if (p.empty()) p = fs::current_path(ec);
return 2; 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);
} }
const std::string lineDir = a.positional[0];
const std::string linePrefix = a.positional[1]; // 单线建体结果(供 build-all 汇总,不编造)。ok=false 时 reason 给清晰原因。
const int levels = std::stoi(a.get("levels", "3")); struct LineBuildResult {
const std::string out = std::string prefix;
a.get("out", (fs::temp_directory_path() / "gpr_store_line").string()); 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) {
LineBuildResult r;
r.prefix = linePrefix;
std::cout << "[build-line] lineDir=" << lineDir << " linePrefix=" << linePrefix std::cout << "[build-line] lineDir=" << lineDir << " linePrefix=" << linePrefix
<< " levels=" << levels << " out=" << out << "\n"; << " levels=" << levels << " coarse=" << coarse << " out=" << out
<< "\n";
// 1) gpr3dv 处理链 → 处理后立方体 → 桥接量化体。 // 1) gpr3dv 处理链 → 处理后立方体 → 桥接量化体(coarse 沿测线下采样)
Stopwatch swBridge; Stopwatch swBridge;
geopro::io::gpr::BridgeMetrics bm; geopro::io::gpr::BridgeMetrics bm;
geopro::core::BuiltI16 built = geopro::core::BuiltI16 built = geopro::io::gpr::buildLineVolumeFromGpr3dv(
geopro::io::gpr::buildLineVolumeFromGpr3dv(lineDir, linePrefix, &bm); lineDir, linePrefix, &bm, coarse);
const double bridgeMs = swBridge.elapsedMs(); const double bridgeMs = swBridge.elapsedMs();
const std::int64_t nx = built.vol.nx(), ny = built.vol.ny(), const std::int64_t nx = built.vol.nx(), ny = built.vol.ny(),
nz = built.vol.nz(); 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 voxels = nx * ny * nz;
const std::int64_t rawBytes = voxels * 2; // int16 const std::int64_t rawBytes = voxels * 2; // int16
@ -848,7 +876,7 @@ int cmdBuildLine(int argc, char** argv) {
std::cout << "[build-line] 世界 spacing dx=" << bm.dx << " dy=" << bm.dy std::cout << "[build-line] 世界 spacing dx=" << bm.dx << " dy=" << bm.dy
<< " dz=" << bm.dz << " (m)\n"; << " dz=" << bm.dz << " (m)\n";
// 2) 落盘 + 金字塔(道很长 45305>16384 → 需 levels≥2 使最粗层≤16384)。 // 2) 落盘 + 金字塔(道很长 → 需 levels≥2 使最粗层≤16384)。
fs::create_directories(out); fs::create_directories(out);
Stopwatch swWrite; Stopwatch swWrite;
geopro::data::ChunkedVolumeStore::write(out, built); geopro::data::ChunkedVolumeStore::write(out, built);
@ -862,6 +890,7 @@ int cmdBuildLine(int argc, char** argv) {
const double pyrMs = swPyr.elapsedMs(); const double pyrMs = swPyr.elapsedMs();
const std::int64_t dataBytes = storeDataBytes(out); const std::int64_t dataBytes = storeDataBytes(out);
r.dataBytes = dataBytes;
const double ratio = const double ratio =
dataBytes > 0 ? static_cast<double>(rawBytes) / dataBytes : 0.0; dataBytes > 0 ? static_cast<double>(rawBytes) / dataBytes : 0.0;
const double peak = Probe::peakMemMB(); const double peak = Probe::peakMemMB();
@ -887,9 +916,9 @@ int cmdBuildLine(int argc, char** argv) {
std::cout << "峰值内存(MB) : " << peak << "\n"; std::cout << "峰值内存(MB) : " << peak << "\n";
writeMetricLine( writeMetricLine(
"build-line,prefix=" + linePrefix + ",nx=" + std::to_string(nx) + "build-line,prefix=" + linePrefix + ",coarse=" + std::to_string(coarse) +
",ny=" + std::to_string(ny) + ",nz=" + std::to_string(nz) + ",nx=" + std::to_string(nx) + ",ny=" + std::to_string(ny) +
",voxels=" + std::to_string(voxels) + ",nz=" + std::to_string(nz) + ",voxels=" + std::to_string(voxels) +
",vmin=" + std::to_string(bm.vminPhys) + ",vmin=" + std::to_string(bm.vminPhys) +
",vmax=" + std::to_string(bm.vmaxPhys) + ",vmax=" + std::to_string(bm.vmaxPhys) +
",dx=" + std::to_string(bm.dx) + ",dy=" + std::to_string(bm.dy) + ",dx=" + std::to_string(bm.dx) + ",dy=" + std::to_string(bm.dy) +
@ -899,6 +928,133 @@ int cmdBuildLine(int argc, char** argv) {
",bridgeMs=" + std::to_string(bridgeMs) + ",bridgeMs=" + std::to_string(bridgeMs) +
",writeMs=" + std::to_string(writeMs) + ",writeMs=" + std::to_string(writeMs) +
",pyrMs=" + std::to_string(pyrMs) + ",peakMB=" + std::to_string(peak)); ",pyrMs=" + std::to_string(pyrMs) + ",peakMB=" + std::to_string(peak));
r.ok = true;
return r;
}
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 std::string out =
a.get("out", (fs::temp_directory_path() / "gpr_store_line").string());
try {
const LineBuildResult r =
buildOneLine(lineDir, linePrefix, out, levels, coarse);
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"));
// 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);
} 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; return 0;
} }
@ -4388,7 +4544,9 @@ void usage() {
" gpr_poc build-geo <dir> [--cellXY 0.5] [--cellZ 0.1] " " gpr_poc build-geo <dir> [--cellXY 0.5] [--cellZ 0.1] "
"[--out <storeDir>] [--levels 4] [--maxLines N] [--curvilinear]\n" "[--out <storeDir>] [--levels 4] [--maxLines N] [--curvilinear]\n"
" gpr_poc build-line <lineDir> <linePrefix> --out <storeDir> " " gpr_poc build-line <lineDir> <linePrefix> --out <storeDir> "
"[--levels 3]\n" "[--levels 3] [--coarse F]\n"
" gpr_poc build-all <lineDir> --outBase <baseDir> "
"[--levels 3] [--coarse F] [--minFreeGB 3]\n"
" gpr_poc load <storeDir>\n" " gpr_poc load <storeDir>\n"
" gpr_poc selftest\n" " gpr_poc selftest\n"
" gpr_poc offscreen-smoke\n" " gpr_poc offscreen-smoke\n"
@ -4424,6 +4582,7 @@ int main(int argc, char** argv) {
if (cmd == "build-stream") return cmdBuildStream(argc, argv); if (cmd == "build-stream") return cmdBuildStream(argc, argv);
if (cmd == "build-geo") return cmdBuildGeo(argc, argv); if (cmd == "build-geo") return cmdBuildGeo(argc, argv);
if (cmd == "build-line") return cmdBuildLine(argc, argv); if (cmd == "build-line") return cmdBuildLine(argc, argv);
if (cmd == "build-all") return cmdBuildAll(argc, argv);
if (cmd == "load") return cmdLoad(argc, argv); if (cmd == "load") return cmdLoad(argc, argv);
if (cmd == "selftest") return cmdSelftest(); if (cmd == "selftest") return cmdSelftest();
if (cmd == "offscreen-smoke") return cmdOffscreenSmoke(); if (cmd == "offscreen-smoke") return cmdOffscreenSmoke();