geopro/src/core/algo/GeoVolumeBuilder.cpp

336 lines
12 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// GeoVolumeBuilder按真实 RTK 几何把多条测线插值进统一路向网格。
//
// 编排层io_gpr + core 采样核 + store故与 StreamingVolumeBuilder 一样编进
// geopro_store不污染纯 geopro_core。命名空间保持 geopro::core接口契约
#include "core/algo/GeoVolumeBuilder.hpp"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <limits>
#include <stdexcept>
#include <vector>
#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<char>(f)),
std::istreambuf_iterator<char>());
return geopro::io::gpr::parseIprHeader(text);
}
// 一条线的标尺(轨迹局部米 + 通道横偏 + 头)。
struct LineScale {
std::vector<geopro::io::gpr::XY> trackM; // 该线轨迹(局部米,旋转前)
std::vector<double> chanX; // 通道横偏(米,文件顺序)
geopro::io::gpr::IprHeader header;
std::int64_t totalTraces = 0;
};
// 全线总道数 = min 通道(fileBytes/(samples*2))(与 assembler 口径一致)。
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;
if (per <= 0) throw std::runtime_error("samples<=0");
for (const auto& p : iprb) {
const std::int64_t bytes = static_cast<std::int64_t>(fs::file_size(p));
minTr = std::min(minTr, bytes / per);
}
return minTr;
}
} // namespace
// ---- PCA 主轴角(可测纯函数)----
double principalAxisAngle(const std::vector<double>& xs,
const std::vector<double>& 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<double>(n);
my /= static_cast<double>(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<double>(n);
syy /= static_cast<double>(n);
sxy /= static_cast<double>(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<GeoLineInput>& 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<geopro::io::gpr::GpsTrack> tracks(lines.size());
double minLat = std::numeric_limits<double>::infinity();
double minLon = std::numeric_limits<double>::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<LineScale> 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<char>(ordF)),
std::istreambuf_iterator<char>());
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<double> 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<std::int64_t>(1, nt / 200);
for (std::int64_t t = 0; t < nt; t += stride) {
const double frac = nt > 1 ? static_cast<double>(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<double>::infinity();
double minY = std::numeric_limits<double>::infinity();
double maxX = -std::numeric_limits<double>::infinity();
double maxY = -std::numeric_limits<double>::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<std::int64_t>(1, nt / 200);
for (std::int64_t t = 0; t < nt; t += stride) {
const double frac = nt > 1 ? static_cast<double>(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<int>(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<std::size_t>(nx) * ny * nz;
// ---- 3) 逐线逐道逐通道逐样本 → grid sum/count重叠均值----
std::vector<double> sum(cellCount, 0.0);
std::vector<std::uint16_t> cnt(cellCount, 0);
auto cellIdx = [&](int gi, int gj, int gk) -> std::size_t {
return (static_cast<std::size_t>(gk) * ny + gj) * nx + gi;
};
constexpr std::int64_t kChunk = 256; // 逐线分段读道,内存有界
double vmin = std::numeric_limits<double>::infinity();
double vmax = -std::numeric_limits<double>::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<int>(sc.chanX.size());
// 各通道 .iprb 区间读(与 chanX 文件顺序对齐iprb[c] ↔ chanX[c])。
const auto& iprbPaths = lines[i].iprb;
if (static_cast<int>(iprbPaths.size()) != nChan) {
// 通道数与 .ord 有效通道不一致:按较小者对齐,避免越界。
}
const int useChan = std::min<int>(nChan, static_cast<int>(iprbPaths.size()));
for (std::int64_t t0 = 0; t0 < nt; t0 += kChunk) {
const std::int64_t t1 = std::min(nt, t0 + kChunk);
// 逐通道读该道段。
std::vector<geopro::io::gpr::BScan> 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<double>(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<int>(std::lround((rx - minX) / spec.cellXY));
const int gj = static_cast<int>(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<int>(std::lround(depth / spec.cellZ));
if (gk < 0 || gk >= nz) continue;
const double v = static_cast<double>(
bs[c].data[static_cast<std::size_t>(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<std::int64_t>(cellCount);
return r;
}
} // namespace geopro::core