feat(io/gpr): 多通道 .iprb+.ord 装配 GprSurvey

assembleGprSurvey 把一条测线若干通道 .iprb(同名 .iprh)+.ord 装配为
geopro::core::GprSurvey:校验各通道 samples 一致、ntraces 取最小值对齐、
按 .ord 横偏 Y 升序重排通道(values 同步置换)、x0/z0=0、dx=道距、
dz=depthOfSample(1,h);通道数与 .ord 有效通道数不符抛 runtime_error。
索引 64 位。纯 C++17,零 Qt/VTK。
This commit is contained in:
gaozheng 2026-06-23 11:36:56 +08:00
parent 4a1fecb149
commit c15555dd8a
5 changed files with 235 additions and 1 deletions

View File

@ -1,6 +1,9 @@
add_library(geopro_io_gpr STATIC IprHeader.cpp IprbReader.cpp GprGeometry.cpp)
add_library(geopro_io_gpr STATIC
IprHeader.cpp IprbReader.cpp GprGeometry.cpp GprSurveyAssembler.cpp)
target_include_directories(geopro_io_gpr PUBLIC ${CMAKE_SOURCE_DIR}/src)
target_compile_features(geopro_io_gpr PUBLIC cxx_std_17)
# GprSurveyAssembler geopro::core::GprSurvey include
target_link_libraries(geopro_io_gpr PUBLIC geopro_core)
# C++17 Qt/VTK AUTOMOC/UIC/RCC
set_target_properties(geopro_io_gpr PROPERTIES AUTOMOC OFF AUTOUIC OFF AUTORCC OFF)

View File

@ -0,0 +1,114 @@
#include "io/gpr/GprSurveyAssembler.hpp"
#include <algorithm>
#include <cstddef>
#include <fstream>
#include <numeric>
#include <sstream>
#include <stdexcept>
#include "io/gpr/GprGeometry.hpp"
#include "io/gpr/IprHeader.hpp"
#include "io/gpr/IprbReader.hpp"
namespace geopro::io::gpr {
namespace {
// 把 .iprb 路径的扩展名替换为 .iprh取最后一个 '.' 之后)。
std::string toHeaderPath(const std::string& iprbPath) {
const std::size_t dot = iprbPath.find_last_of('.');
const std::size_t slash = iprbPath.find_last_of("/\\");
// 仅当 '.' 在最后一个路径分隔符之后才视为扩展名。
if (dot != std::string::npos &&
(slash == std::string::npos || dot > slash)) {
return iprbPath.substr(0, dot) + ".iprh";
}
return iprbPath + ".iprh";
}
std::string readFileText(const std::string& path) {
std::ifstream f(path, std::ios::binary);
if (!f) {
throw std::runtime_error("无法打开文件: " + path);
}
std::ostringstream ss;
ss << f.rdbuf();
return ss.str();
}
} // namespace
geopro::core::GprSurvey assembleGprSurvey(
const std::vector<std::string>& channelIprbPaths,
const std::string& ordPath) {
// 1. .ord -> 各通道横偏(文件序)。
const std::vector<double> channelY0 =
parseChannelXOffsets(readFileText(ordPath));
if (channelY0.size() != channelIprbPaths.size()) {
throw std::runtime_error(
"通道数不一致: .ord 有效通道数与 .iprb 路径数量不符");
}
const std::size_t nchan = channelIprbPaths.size();
if (nchan == 0) {
throw std::runtime_error("无通道可装配");
}
// 2. 各通道读 header + BScan。
std::vector<IprHeader> headers;
std::vector<BScan> scans;
headers.reserve(nchan);
scans.reserve(nchan);
for (const std::string& iprbPath : channelIprbPaths) {
const IprHeader h = parseIprHeader(readFileText(toHeaderPath(iprbPath)));
headers.push_back(h);
scans.push_back(readIprb(iprbPath, h));
}
// 3. 校验 samples 一致ntraces 取各通道 traces 最小值(对齐)。
const int samples = scans.front().samples;
std::int64_t minTraces = scans.front().traces;
for (std::size_t c = 0; c < nchan; ++c) {
if (scans[c].samples != samples) {
throw std::runtime_error("通道 samples 不一致");
}
minTraces = std::min(minTraces, scans[c].traces);
}
// 4. 由首通道 header 定标尺。
geopro::core::GprSurvey survey;
survey.samples = samples;
survey.ntraces = static_cast<int>(minTraces);
survey.x0 = 0.0;
survey.dx = headers.front().distanceInterval;
survey.z0 = 0.0;
survey.dz = (samples > 1) ? depthOfSample(1, headers.front()) : 0.0;
// 5. 按 Y 升序求置换order[c] = 升序第 c 位对应的原通道索引。
std::vector<std::size_t> order(nchan);
std::iota(order.begin(), order.end(), std::size_t{0});
std::stable_sort(order.begin(), order.end(),
[&](std::size_t a, std::size_t b) {
return channelY0[a] < channelY0[b];
});
survey.channelY.resize(nchan);
const std::size_t ntraces = static_cast<std::size_t>(survey.ntraces);
const std::size_t ns = static_cast<std::size_t>(samples);
survey.values.assign(nchan * ntraces * ns, 0.0);
for (std::size_t c = 0; c < nchan; ++c) {
const std::size_t src = order[c];
survey.channelY[c] = channelY0[src];
const BScan& bscan = scans[src];
for (std::size_t t = 0; t < ntraces; ++t) {
for (std::size_t s = 0; s < ns; ++s) {
survey.values[(c * ntraces + t) * ns + s] =
static_cast<double>(bscan.data[t * ns + s]);
}
}
}
return survey;
}
} // namespace geopro::io::gpr

View File

@ -0,0 +1,28 @@
#ifndef GEOPRO_IO_GPR_GPRSURVEYASSEMBLER_HPP
#define GEOPRO_IO_GPR_GPRSURVEYASSEMBLER_HPP
#include <string>
#include <vector>
#include "core/model/GprSurvey.hpp"
namespace geopro::io::gpr {
// 把真实 GPR 一条测线的若干通道 .iprb(同目录同名 .iprh) + .ord 装配成 GprSurvey。
//
// channelIprbPaths该线各通道 .iprb 路径,每个同目录有同名 .iprh(扩展名替换为 .iprh)。
// ordPath.ord 文件路径,提供各有效通道横偏(Y)。
//
// 规则:
// - .ord 有效通道数须与 channelIprbPaths 数量一致(否则抛 std::runtime_error)
// - 各通道 samples 须相等(否则抛)ntraces = 各通道 traces 最小值(对齐)
// - x0=0, dx=header.distanceIntervalz0=0, dz=depthOfSample(1,h)(samples<=1 则 0)
// - channelY 按 Y 升序排序values 通道维同步重排;
// - values[(c*ntraces+t)*samples+s] = 该通道 BScan(t,s) 值(int16->double)。
geopro::core::GprSurvey assembleGprSurvey(
const std::vector<std::string>& channelIprbPaths,
const std::string& ordPath);
} // namespace geopro::io::gpr
#endif // GEOPRO_IO_GPR_GPRSURVEYASSEMBLER_HPP

View File

@ -195,6 +195,8 @@ target_link_libraries(geopro_tests PRIVATE geopro_controller Qt6::Test)
target_sources(geopro_tests PRIVATE io/gpr/test_ipr_header.cpp)
target_sources(geopro_tests PRIVATE io/gpr/test_iprb_reader.cpp)
target_sources(geopro_tests PRIVATE io/gpr/test_gpr_geometry.cpp)
# GprSurveyAssembler .iprb + .ord -> GprSurveysamples /traces /Y
target_sources(geopro_tests PRIVATE io/gpr/test_gpr_survey_assembler.cpp)
target_link_libraries(geopro_tests PRIVATE geopro_io_gpr)
add_subdirectory(spike) # spike S3: banded contour

View File

@ -0,0 +1,87 @@
#include "io/gpr/GprSurveyAssembler.hpp"
#include <gtest/gtest.h>
#include <fstream>
#include <filesystem>
#include <cstdint>
#include <stdexcept>
#include <vector>
using namespace geopro::io::gpr;
namespace {
void writeText(const std::string& p, const std::string& s) {
std::ofstream f(p);
f << s;
}
void writeI16(const std::string& p, const std::vector<int16_t>& v) {
std::ofstream f(p, std::ios::binary);
f.write(reinterpret_cast<const char*>(v.data()),
static_cast<std::streamsize>(v.size() * sizeof(int16_t)));
}
// samples=2, lastTrace=1 -> traces=2; CHANNELS 仅供头解析,不影响装配。
const char* HDR =
"SAMPLES: 2\nLAST TRACE: 1\nCHANNELS: 2\nTIMEWINDOW: 4.0\n"
"SOIL VELOCITY: 100.000000\nDISTANCE INTERVAL: 0.05\n";
} // namespace
TEST(GprSurveyAssembler, AssemblesTwoChannels) {
auto d = (std::filesystem::temp_directory_path() / "gpr_asm").string();
std::filesystem::create_directories(d);
// 通道 A(横偏 1.0, 值 10,11, 12,13), 通道 B(横偏 0.0, 值 20..23)。
// B 的 Y 更小, 用于验证按 Y 升序重排。
writeText(d + "/A.iprh", HDR);
writeI16(d + "/A.iprb", {10, 11, 12, 13}); // [trace*samples+s]
writeText(d + "/B.iprh", HDR);
writeI16(d + "/B.iprb", {20, 21, 22, 23});
writeText(d + "/x.ord", "0 1.0 -1.5 1\n1 0.0 -1.5 1\n"); // A->1.0, B->0.0
auto s = assembleGprSurvey({d + "/A.iprb", d + "/B.iprb"}, d + "/x.ord");
EXPECT_EQ(s.samples, 2);
EXPECT_EQ(s.ntraces, 2);
EXPECT_NEAR(s.x0, 0.0, 1e-9);
EXPECT_NEAR(s.z0, 0.0, 1e-9);
EXPECT_NEAR(s.dx, 0.05, 1e-9);
// depthOfSample(1,h) = 1e8 * (1 * 4/(2-1) * 1e-9) / 2 = 0.2
EXPECT_NEAR(s.dz, 0.2, 1e-6);
ASSERT_EQ(s.channelY.size(), 2u);
EXPECT_NEAR(s.channelY[0], 0.0, 1e-9); // 升序: B(0.0) 在前
EXPECT_NEAR(s.channelY[1], 1.0, 1e-9); // A(1.0) 在后
// 升序后通道0=B, 通道1=A
EXPECT_NEAR(s.at(0, 0, 0), 20.0, 1e-9); // B 的 (t0,s0)
EXPECT_NEAR(s.at(0, 1, 1), 23.0, 1e-9); // B 的 (t1,s1)
EXPECT_NEAR(s.at(1, 0, 0), 10.0, 1e-9); // A 的 (t0,s0)
EXPECT_NEAR(s.at(1, 1, 1), 13.0, 1e-9); // A 的 (t1,s1)
std::filesystem::remove_all(d);
}
TEST(GprSurveyAssembler, ThrowsWhenChannelCountMismatch) {
auto d = (std::filesystem::temp_directory_path() / "gpr_asm_mismatch").string();
std::filesystem::create_directories(d);
writeText(d + "/A.iprh", HDR);
writeI16(d + "/A.iprb", {10, 11, 12, 13});
// .ord 含 2 个有效通道, 但只传 1 个 iprb 路径 -> 抛错。
writeText(d + "/x.ord", "0 1.0 -1.5 1\n1 0.0 -1.5 1\n");
EXPECT_THROW(assembleGprSurvey({d + "/A.iprb"}, d + "/x.ord"),
std::runtime_error);
std::filesystem::remove_all(d);
}
TEST(GprSurveyAssembler, AlignsTracesToMinimum) {
auto d = (std::filesystem::temp_directory_path() / "gpr_asm_align").string();
std::filesystem::create_directories(d);
// 通道 A: traces=2 (lastTrace=1); 通道 B: traces=3 (lastTrace=2)。
// ntraces 应对齐为 min=2。
const char* HDR3 =
"SAMPLES: 2\nLAST TRACE: 2\nCHANNELS: 2\nTIMEWINDOW: 4.0\n"
"SOIL VELOCITY: 100.000000\nDISTANCE INTERVAL: 0.05\n";
writeText(d + "/A.iprh", HDR);
writeI16(d + "/A.iprb", {10, 11, 12, 13}); // 2 道
writeText(d + "/B.iprh", HDR3);
writeI16(d + "/B.iprb", {20, 21, 22, 23, 24, 25}); // 3 道
writeText(d + "/x.ord", "0 0.0 -1.5 1\n1 1.0 -1.5 1\n"); // A->0.0, B->1.0
auto s = assembleGprSurvey({d + "/A.iprb", d + "/B.iprb"}, d + "/x.ord");
EXPECT_EQ(s.ntraces, 2);
EXPECT_EQ(s.samples, 2);
// A(Y=0.0)=通道0, B(Y=1.0)=通道1; B 的第3道(t=2)被对齐丢弃。
EXPECT_NEAR(s.at(1, 1, 0), 22.0, 1e-9); // B 的 (t1,s0)
std::filesystem::remove_all(d);
}