diff --git a/src/io/gpr/CMakeLists.txt b/src/io/gpr/CMakeLists.txt index 15cf3fb..a66381b 100644 --- a/src/io/gpr/CMakeLists.txt +++ b/src/io/gpr/CMakeLists.txt @@ -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) diff --git a/src/io/gpr/GprSurveyAssembler.cpp b/src/io/gpr/GprSurveyAssembler.cpp new file mode 100644 index 0000000..041fce5 --- /dev/null +++ b/src/io/gpr/GprSurveyAssembler.cpp @@ -0,0 +1,114 @@ +#include "io/gpr/GprSurveyAssembler.hpp" + +#include +#include +#include +#include +#include +#include + +#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& channelIprbPaths, + const std::string& ordPath) { + // 1. .ord -> 各通道横偏(文件序)。 + const std::vector 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 headers; + std::vector 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(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 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(survey.ntraces); + const std::size_t ns = static_cast(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(bscan.data[t * ns + s]); + } + } + } + + return survey; +} + +} // namespace geopro::io::gpr diff --git a/src/io/gpr/GprSurveyAssembler.hpp b/src/io/gpr/GprSurveyAssembler.hpp new file mode 100644 index 0000000..d0da002 --- /dev/null +++ b/src/io/gpr/GprSurveyAssembler.hpp @@ -0,0 +1,28 @@ +#ifndef GEOPRO_IO_GPR_GPRSURVEYASSEMBLER_HPP +#define GEOPRO_IO_GPR_GPRSURVEYASSEMBLER_HPP + +#include +#include + +#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.distanceInterval;z0=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& channelIprbPaths, + const std::string& ordPath); + +} // namespace geopro::io::gpr + +#endif // GEOPRO_IO_GPR_GPRSURVEYASSEMBLER_HPP diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 97462ab..6e4da3d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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 -> GprSurvey(samples 校验/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 渲染验证 diff --git a/tests/io/gpr/test_gpr_survey_assembler.cpp b/tests/io/gpr/test_gpr_survey_assembler.cpp new file mode 100644 index 0000000..a997649 --- /dev/null +++ b/tests/io/gpr/test_gpr_survey_assembler.cpp @@ -0,0 +1,87 @@ +#include "io/gpr/GprSurveyAssembler.hpp" +#include +#include +#include +#include +#include +#include +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& v) { + std::ofstream f(p, std::ios::binary); + f.write(reinterpret_cast(v.data()), + static_cast(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); +}