// GeoVolumeBuilder:按真实 RTK 几何把多条测线插值进统一路向网格。 // // 编排层(io_gpr + core 采样核 + store),故与 StreamingVolumeBuilder 一样编进 // geopro_store(不污染纯 geopro_core)。命名空间保持 geopro::core(接口契约)。 #include "core/algo/GeoVolumeBuilder.hpp" #include #include #include #include #include #include #include #include #include #include #include "core/algo/GprVolumeBuilder.hpp" // BuiltI16 #include "core/model/ScalarVolumeI16.hpp" // ScalarVolumeI16::kBlank, Quant #include "data/store/ChunkedVolumeStore.hpp" #include "io/gpr/GprGeometry.hpp" // parseChannelXOffsets, depthOfSample #include "io/gpr/GpsTrack.hpp" #include "io/gpr/IprHeader.hpp" #include "io/gpr/IprbReader.hpp" namespace fs = std::filesystem; namespace geopro::core { namespace { constexpr double kPi = 3.14159265358979323846; // 读 .iprh 文本 → 解析头(与 .iprb 同名)。 geopro::io::gpr::IprHeader readHeaderFor(const std::string& iprbPath) { fs::path h = fs::path(iprbPath).replace_extension(".iprh"); std::ifstream f(h); if (!f) throw std::runtime_error("GeoVolumeBuilder: 打不开 iprh " + h.string()); std::string text((std::istreambuf_iterator(f)), std::istreambuf_iterator()); return geopro::io::gpr::parseIprHeader(text); } // 一条线的标尺(轨迹局部米 + 通道横偏 + 头)。 struct LineScale { std::vector trackM; // 该线轨迹(局部米,旋转前) std::vector chanX; // 通道横偏(米,文件顺序) geopro::io::gpr::IprHeader header; std::int64_t totalTraces = 0; }; // 全线总道数 = min 通道(fileBytes/(samples*2))(与 assembler 口径一致)。 std::int64_t totalTracesOf(const std::vector& iprb, int samples) { std::int64_t minTr = std::numeric_limits::max(); const std::int64_t per = static_cast(samples) * 2; if (per <= 0) throw std::runtime_error("samples<=0"); for (const auto& p : iprb) { const std::int64_t bytes = static_cast(fs::file_size(p)); minTr = std::min(minTr, bytes / per); } return minTr; } } // namespace // ---- PCA 主轴角(可测纯函数)---- double principalAxisAngle(const std::vector& xs, const std::vector& ys) { const std::size_t n = std::min(xs.size(), ys.size()); if (n < 2) return 0.0; double mx = 0, my = 0; for (std::size_t i = 0; i < n; ++i) { mx += xs[i]; my += ys[i]; } mx /= static_cast(n); my /= static_cast(n); double sxx = 0, syy = 0, sxy = 0; for (std::size_t i = 0; i < n; ++i) { const double dx = xs[i] - mx, dy = ys[i] - my; sxx += dx * dx; syy += dy * dy; sxy += dx * dy; } sxx /= static_cast(n); syy /= static_cast(n); sxy /= static_cast(n); // 2×2 对称协方差矩阵最大特征向量方向 = 0.5*atan2(2*sxy, sxx-syy)。 if (std::abs(sxy) < 1e-15 && std::abs(sxx - syy) < 1e-15) return 0.0; return 0.5 * std::atan2(2.0 * sxy, sxx - syy); } GeoBuildResult buildGeoVolume(const std::vector& lines, const GeoGridSpec& spec, const std::string& outStoreDir, int pyramidLevels) { using geopro::io::gpr::interpAlongTrack; using geopro::io::gpr::lonLatToLocalM; using geopro::io::gpr::PosHeading; using geopro::io::gpr::XY; if (lines.empty()) throw std::runtime_error("buildGeoVolume: 无测线"); if (spec.cellXY <= 0 || spec.cellZ <= 0) throw std::runtime_error("buildGeoVolume: cell 必须 > 0"); // ---- 1) 各线 .gps → 局部米(共用原点 = 全体最小 lat/lon)---- std::vector tracks(lines.size()); double minLat = std::numeric_limits::infinity(); double minLon = std::numeric_limits::infinity(); for (std::size_t i = 0; i < lines.size(); ++i) { tracks[i] = geopro::io::gpr::parseGps(lines[i].gps); for (const auto& p : tracks[i].pts) { minLat = std::min(minLat, p.lat); minLon = std::min(minLon, p.lon); } } if (!std::isfinite(minLat) || !std::isfinite(minLon)) throw std::runtime_error("buildGeoVolume: 轨迹为空"); // 各线标尺:轨迹局部米 + 通道横偏 + 头 + 总道数。 std::vector scales(lines.size()); int samples = 0; geopro::io::gpr::IprHeader hdr0{}; for (std::size_t i = 0; i < lines.size(); ++i) { LineScale& sc = scales[i]; sc.trackM.reserve(tracks[i].pts.size()); for (const auto& p : tracks[i].pts) sc.trackM.push_back(lonLatToLocalM(p.lat, p.lon, minLat, minLon)); std::ifstream ordF(lines[i].ord); if (!ordF) throw std::runtime_error("buildGeoVolume: 打不开 ord " + lines[i].ord); std::string ordText((std::istreambuf_iterator(ordF)), std::istreambuf_iterator()); sc.chanX = geopro::io::gpr::parseChannelXOffsets(ordText); if (lines[i].iprb.empty()) throw std::runtime_error("buildGeoVolume: 线无 iprb"); sc.header = readHeaderFor(lines[i].iprb.front()); sc.totalTraces = totalTracesOf(lines[i].iprb, sc.header.samples); if (i == 0) { samples = sc.header.samples; hdr0 = sc.header; } } if (samples <= 0) throw std::runtime_error("buildGeoVolume: samples<=0"); // ---- 2) PCA 求路向主轴角,旋转使路向 = X'、横路 = Y' ---- // 收集全体通道平面点(trace 位置 + 通道横偏)求主轴。 std::vector px, py; for (std::size_t i = 0; i < scales.size(); ++i) { const LineScale& sc = scales[i]; const std::int64_t nt = sc.totalTraces; if (nt < 1 || sc.trackM.size() < 2) continue; // 稀疏采样道(每线最多 ~200 个采样点)求 PCA,省内存。 const std::int64_t stride = std::max(1, nt / 200); for (std::int64_t t = 0; t < nt; t += stride) { const double frac = nt > 1 ? static_cast(t) / (nt - 1) : 0.0; PosHeading ph = interpAlongTrack(sc.trackM, frac); px.push_back(ph.pos.x); py.push_back(ph.pos.y); } } const double rotRad = principalAxisAngle(px, py); const double cosR = std::cos(-rotRad), sinR = std::sin(-rotRad); auto rotate = [&](double x, double y, double& rx, double& ry) { rx = x * cosR - y * sinR; ry = x * sinR + y * cosR; }; // ---- 求旋转后包围盒(含通道横偏)→ 网格维度 + 原点 ---- double minX = std::numeric_limits::infinity(); double minY = std::numeric_limits::infinity(); double maxX = -std::numeric_limits::infinity(); double maxY = -std::numeric_limits::infinity(); for (std::size_t i = 0; i < scales.size(); ++i) { const LineScale& sc = scales[i]; const std::int64_t nt = sc.totalTraces; if (nt < 1 || sc.trackM.size() < 2 || sc.chanX.empty()) continue; const std::int64_t stride = std::max(1, nt / 200); for (std::int64_t t = 0; t < nt; t += stride) { const double frac = nt > 1 ? static_cast(t) / (nt - 1) : 0.0; PosHeading ph = interpAlongTrack(sc.trackM, frac); const double perpX = -ph.hy, perpY = ph.hx; // 垂直航向(左法向) for (double oc : {sc.chanX.front(), sc.chanX.back()}) { const double cx = ph.pos.x + oc * perpX; const double cy = ph.pos.y + oc * perpY; double rx, ry; rotate(cx, cy, rx, ry); minX = std::min(minX, rx); maxX = std::max(maxX, rx); minY = std::min(minY, ry); maxY = std::max(maxY, ry); } } } if (!std::isfinite(minX)) throw std::runtime_error("buildGeoVolume: 无有效几何"); // 深度范围:用首线头取最深样本深度。 const double maxDepth = geopro::io::gpr::depthOfSample(samples - 1, hdr0); auto cells = [](double range, double cell) { if (cell <= 0.0 || range <= 0.0) return 1; return static_cast(std::ceil(range / cell)) + 1; }; const int nx = cells(maxX - minX, spec.cellXY); // 沿路(长轴 X') const int ny = cells(maxY - minY, spec.cellXY); // 横路 Y' const int nz = cells(maxDepth, spec.cellZ); // 深度 Z const std::size_t cellCount = static_cast(nx) * ny * nz; // ---- 3) 逐线逐道逐通道逐样本 → grid sum/count(重叠均值)---- std::vector sum(cellCount, 0.0); std::vector cnt(cellCount, 0); auto cellIdx = [&](int gi, int gj, int gk) -> std::size_t { return (static_cast(gk) * ny + gj) * nx + gi; }; constexpr std::int64_t kChunk = 256; // 逐线分段读道,内存有界 double vmin = std::numeric_limits::infinity(); double vmax = -std::numeric_limits::infinity(); for (std::size_t i = 0; i < scales.size(); ++i) { const LineScale& sc = scales[i]; const std::int64_t nt = sc.totalTraces; if (nt < 1 || sc.trackM.size() < 2 || sc.chanX.empty()) continue; const int nChan = static_cast(sc.chanX.size()); // 各通道 .iprb 区间读(与 chanX 文件顺序对齐:iprb[c] ↔ chanX[c])。 const auto& iprbPaths = lines[i].iprb; if (static_cast(iprbPaths.size()) != nChan) { // 通道数与 .ord 有效通道不一致:按较小者对齐,避免越界。 } const int useChan = std::min(nChan, static_cast(iprbPaths.size())); for (std::int64_t t0 = 0; t0 < nt; t0 += kChunk) { const std::int64_t t1 = std::min(nt, t0 + kChunk); // 逐通道读该道段。 std::vector bs(useChan); for (int c = 0; c < useChan; ++c) bs[c] = geopro::io::gpr::readIprbRange(iprbPaths[c], sc.header, t0, t1); for (std::int64_t t = t0; t < t1; ++t) { const double frac = nt > 1 ? static_cast(t) / (nt - 1) : 0.0; PosHeading ph = interpAlongTrack(sc.trackM, frac); const double perpX = -ph.hy, perpY = ph.hx; const std::int64_t lt = t - t0; // BScan 内局部道索引 for (int c = 0; c < useChan; ++c) { const double oc = sc.chanX[c]; const double cx = ph.pos.x + oc * perpX; const double cy = ph.pos.y + oc * perpY; double rx, ry; rotate(cx, cy, rx, ry); const int gi = static_cast(std::lround((rx - minX) / spec.cellXY)); const int gj = static_cast(std::lround((ry - minY) / spec.cellXY)); if (gi < 0 || gi >= nx || gj < 0 || gj >= ny) continue; for (int s = 0; s < samples; ++s) { const double depth = geopro::io::gpr::depthOfSample(s, sc.header); const int gk = static_cast(std::lround(depth / spec.cellZ)); if (gk < 0 || gk >= nz) continue; const double v = static_cast( bs[c].data[static_cast(lt) * samples + s]); const std::size_t idx = cellIdx(gi, gj, gk); sum[idx] += v; ++cnt[idx]; } } } } } // ---- 4) 均值 + 求值域 ---- std::int64_t filled = 0; for (std::size_t idx = 0; idx < cellCount; ++idx) { if (cnt[idx] == 0) continue; const double mean = sum[idx] / cnt[idx]; sum[idx] = mean; // 复用 sum 存均值 vmin = std::min(vmin, mean); vmax = std::max(vmax, mean); ++filled; } if (!(vmin <= vmax)) { vmin = 0.0; vmax = 0.0; } // ---- 5) 量化 + 写 store + 金字塔 ---- Quant quant; quant.scale = (vmax > vmin) ? (vmax - vmin) / 64000.0 : 1.0; quant.offset = 0.5 * (vmin + vmax); ScalarVolumeI16 vol(nx, ny, nz); for (int gk = 0; gk < nz; ++gk) for (int gj = 0; gj < ny; ++gj) for (int gi = 0; gi < nx; ++gi) { const std::size_t idx = cellIdx(gi, gj, gk); vol.at(gi, gj, gk) = (cnt[idx] == 0) ? ScalarVolumeI16::kBlank : quant.toQ(sum[idx]); } BuiltI16 built; built.vol = std::move(vol); built.quant = quant; built.origin = {minX, minY, 0.0}; built.spacing = {spec.cellXY, spec.cellXY, spec.cellZ}; built.vminPhys = vmin; built.vmaxPhys = vmax; fs::create_directories(outStoreDir); geopro::data::ChunkedVolumeStore::write(outStoreDir, built); if (pyramidLevels > 0) { geopro::data::ChunkedVolumeStore store(outStoreDir); store.buildPyramid(pyramidLevels); } GeoBuildResult r; r.nx = nx; r.ny = ny; r.nz = nz; r.oxM = minX; r.oyM = minY; r.rotRad = rotRad; r.filled = filled; r.total = static_cast(cellCount); return r; } } // namespace geopro::core