626 lines
24 KiB
C++
626 lines
24 KiB
C++
#include "IprhParser.h"
|
||
#include "PerformanceLogger.h"
|
||
|
||
#include "ImpulseMultiChannelConverter.h"
|
||
|
||
#include <QFile>
|
||
#include <QTextStream>
|
||
#include <QDataStream>
|
||
#include <QDebug>
|
||
#include <QDir>
|
||
#include <QFileInfo>
|
||
#include <QRegularExpression>
|
||
#include <QStringList>
|
||
#include <QVector3D>
|
||
#include <QLocale>
|
||
#include <algorithm>
|
||
#include <cstring>
|
||
|
||
/**
|
||
* @brief 加载 Impulse 系列雷达 IPRH 数据(iprh头文件 + iprb纯二进制波形)
|
||
* @param iprhFilePath .iprh 文本配置头文件路径
|
||
* @param model 输出 GPR 全局数据模型
|
||
* @return true 加载解析成功;false 失败
|
||
* @note 存储结构:.iprh 存储全部仪器/测线参数;.iprb 无文件头,从头到尾全是 short16 振幅采样
|
||
*/
|
||
bool IprhParser::loadFromIprh(const QString &iprhFilePath, GPRDataModel &model) {
|
||
SCOPED_PERF_TIMER("Parser", "IprhParser::loadFromIprh");
|
||
|
||
model.clear();
|
||
model.header = GPRDataModel::Header{};
|
||
|
||
// 第一步:解析 iprh 文本头
|
||
if (!parseIprhHeader(iprhFilePath, model.header)) {
|
||
qDebug() << "Error: Failed to parse .iprh header file:" << iprhFilePath;
|
||
return false;
|
||
}
|
||
|
||
// 自动匹配同目录同名二进制数据文件 .iprb
|
||
QFileInfo iprhInfo(iprhFilePath);
|
||
QString iprbPath = iprhInfo.absolutePath() + "/" + iprhInfo.completeBaseName() + ".iprb";
|
||
|
||
if (!QFile::exists(iprbPath)) {
|
||
qDebug() << "Error: Matching .iprb binary file not found at:" << iprbPath;
|
||
return false;
|
||
}
|
||
|
||
// 如果头文件没有 LAST TRACE,从二进制文件大小推算
|
||
if (model.header.numTraces <= 0) {
|
||
QFile binaryFile(iprbPath);
|
||
if (binaryFile.open(QIODevice::ReadOnly)) {
|
||
qint64 fileSize = binaryFile.size();
|
||
qint64 traceBytes = static_cast<qint64>(model.header.samplesPerTrace) * sizeof(short);
|
||
if (traceBytes > 0) {
|
||
model.header.numTraces = static_cast<int>(fileSize / traceBytes);
|
||
qDebug() << "Inferred numTraces from binary size:" << model.header.numTraces;
|
||
}
|
||
binaryFile.close();
|
||
}
|
||
}
|
||
if (model.header.timeWindowNs <= 0.0 && model.header.timeIntervalNs > 0.0) {
|
||
model.header.timeWindowNs = model.header.samplesPerTrace * model.header.timeIntervalNs;
|
||
qDebug() << "Inferred timeWindowNs from samples * timeInterval:" << model.header.timeWindowNs;
|
||
}
|
||
|
||
if (model.header.numTraces <= 0) {
|
||
qDebug() << "Error: Unable to determine numTraces from header or binary file";
|
||
return false;
|
||
}
|
||
|
||
// 第二步:读取纯二进制波形
|
||
return loadIprbBinary(iprbPath, model);
|
||
}
|
||
|
||
/**
|
||
* @brief 解析 IPRH 文本头文件,提取雷达采集关键参数
|
||
* @param iprhFilePath iprh 文本文件路径
|
||
* @param header 待填充头部参数结构体
|
||
* @return 解析成功返回 true
|
||
*/
|
||
bool IprhParser::parseHeaderOnly(const QString &iprhFilePath, GPRDataModel::Header &header)
|
||
{
|
||
return parseIprhHeader(iprhFilePath, header);
|
||
}
|
||
|
||
bool IprhParser::parseIprhHeader(const QString &iprhFilePath, GPRDataModel::Header &header) {
|
||
SCOPED_PERF_TIMER("Parser", "IprhParser::parseIprhHeader");
|
||
|
||
QFile file(iprhFilePath);
|
||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||
qDebug() << "Error: Cannot open iprh file for read";
|
||
return false;
|
||
}
|
||
|
||
QTextStream in(&file);
|
||
|
||
while (!in.atEnd()) {
|
||
QString line = in.readLine().trimmed();
|
||
if (line.isEmpty()) continue;
|
||
|
||
int sepPos = line.indexOf(':');
|
||
if (sepPos == -1) continue;
|
||
|
||
QString key = line.left(sepPos).trimmed();
|
||
QString value = line.mid(sepPos + 1).trimmed();
|
||
|
||
header.rawParams[key] = value;
|
||
|
||
if (key == "SAMPLES") {
|
||
header.samplesPerTrace = extractInt(value);
|
||
} else if (key == "LAST TRACE") {
|
||
header.numTraces = extractInt(value);
|
||
} else if (key == "TIMEWINDOW") {
|
||
header.timeWindowNs = extractDouble(value);
|
||
} else if (key == "DISTANCE INTERVAL") {
|
||
header.distanceInc = extractDouble(value);
|
||
} else if (key == "TIME INTERVAL") {
|
||
header.timeIntervalNs = extractDouble(value);
|
||
} else if (key == "ANTENNAS") {
|
||
header.antennaFreq = extractDouble(value);
|
||
} else if (key == "ANTENNA") {
|
||
header.antennaType = value;
|
||
} else if (key == "DATE") {
|
||
header.date = value;
|
||
} else if (key == "START TIME" || key == "TIME") {
|
||
header.timeStr = value;
|
||
} else if (key == "CHANNELS" || key == "NUMBER_OF_CH") {
|
||
header.numberOfChannels = extractInt(value);
|
||
} else if (key == "CH_X_OFFSET" || key == "CH_OFFSET_X") {
|
||
// 单通道偏移量(Impulse 单通道文件)
|
||
if (!value.isEmpty()) {
|
||
header.chXOffsets.append(value.toFloat());
|
||
}
|
||
} else if (key == "CH_Y_OFFSET" || key == "CH_OFFSET_Y") {
|
||
if (!value.isEmpty()) {
|
||
header.chYOffsets.append(value.toFloat());
|
||
}
|
||
} else if (key == "CH_X_OFFSETS") {
|
||
// 兼容 Mala Mira 风格多值空格分隔
|
||
QStringList offsets = value.split(' ', Qt::SkipEmptyParts);
|
||
for (const QString &offset : offsets) {
|
||
header.chXOffsets.append(offset.toFloat());
|
||
}
|
||
} else if (key == "CH_Y_OFFSETS") {
|
||
QStringList offsets = value.split(' ', Qt::SkipEmptyParts);
|
||
for (const QString &offset : offsets) {
|
||
header.chYOffsets.append(offset.toFloat());
|
||
}
|
||
} else if (key == "UNITS") {
|
||
header.units = value;
|
||
} else if (key == "START POSITION") {
|
||
header.startPosition = extractDouble(value);
|
||
} else if (key == "STOP POSITION") {
|
||
header.stopPosition = extractDouble(value);
|
||
}
|
||
}
|
||
|
||
file.close();
|
||
|
||
if (header.samplesPerTrace <= 0) {
|
||
qDebug() << "Error: Invalid SAMPLES value in iprh file";
|
||
return false;
|
||
}
|
||
|
||
header.waveVelocity = 0.1;
|
||
|
||
qDebug() << "==== IPRH Header Parse Complete ===="
|
||
<< "\nSamples per trace:" << header.samplesPerTrace
|
||
<< "\nTotal traces:" << header.numTraces
|
||
<< "\nTime window(ns):" << header.timeWindowNs
|
||
<< "\nChannel count:" << header.numberOfChannels
|
||
<< "\nX offset array size:" << header.chXOffsets.size()
|
||
<< "\nY offset array size:" << header.chYOffsets.size();
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* @brief 读取纯 IPRB 二进制数据(无任何文件头,全连续 short16 振幅)
|
||
* @param iprbFilePath iprb 波形文件路径
|
||
* @param model 绑定头部参数,填充完整 traces 波形数组
|
||
* @return 读取成功 true
|
||
*/
|
||
bool IprhParser::loadIprbBinary(const QString &iprbFilePath, GPRDataModel &model) {
|
||
SCOPED_PERF_TIMER("Parser", "IprhParser::loadIprbBinary");
|
||
|
||
QFile file(iprbFilePath);
|
||
if (!file.open(QIODevice::ReadOnly)) {
|
||
qDebug() << "Error: Open iprb binary failed:" << iprbFilePath;
|
||
return false;
|
||
}
|
||
|
||
const int samplesPerTrace = model.header.samplesPerTrace;
|
||
const int totalTraceCount = model.header.numTraces;
|
||
|
||
const qint64 dataStartOffset = 0;
|
||
file.seek(dataStartOffset);
|
||
|
||
const qint64 singleTraceByteSize = samplesPerTrace * sizeof(short);
|
||
const qint64 fullExpectedBytes = totalTraceCount * singleTraceByteSize;
|
||
const qint64 realFileBytes = file.size();
|
||
|
||
if (realFileBytes < fullExpectedBytes) {
|
||
qDebug() << "Warning: IPRB file size smaller than theoretical data size!"
|
||
<< "Expected:" << fullExpectedBytes << "Actual:" << realFileBytes;
|
||
}
|
||
|
||
model.traces.reserve(totalTraceCount);
|
||
|
||
QByteArray traceBuffer;
|
||
traceBuffer.resize(singleTraceByteSize);
|
||
|
||
const int channelCnt = model.header.numberOfChannels > 0 ? model.header.numberOfChannels : 1;
|
||
model.channels = channelCnt;
|
||
model.tracesPerChannel = totalTraceCount / channelCnt;
|
||
|
||
if (model.header.distanceInc > 1e-6) {
|
||
model.totalDistance = static_cast<float>(model.tracesPerChannel * model.header.distanceInc);
|
||
} else {
|
||
model.totalDistance = static_cast<float>(model.header.stopPosition - model.header.startPosition);
|
||
}
|
||
|
||
for (int traceGlobalIdx = 0; traceGlobalIdx < totalTraceCount; ++traceGlobalIdx) {
|
||
if (file.atEnd()) {
|
||
qDebug() << "Warning: File ended early at global trace index" << traceGlobalIdx;
|
||
break;
|
||
}
|
||
|
||
qint64 readBytes = file.read(traceBuffer.data(), traceBuffer.size());
|
||
if (readBytes != traceBuffer.size()) {
|
||
qDebug() << "Warning: Trace" << traceGlobalIdx << "incomplete byte read";
|
||
break;
|
||
}
|
||
|
||
RadarTrace oneTrace;
|
||
oneTrace.amplitudes.resize(samplesPerTrace);
|
||
const short* rawShortBuf = reinterpret_cast<const short*>(traceBuffer.constData());
|
||
|
||
for (int s = 0; s < samplesPerTrace; s++) {
|
||
oneTrace.amplitudes[s] = rawShortBuf[s];
|
||
}
|
||
|
||
int chNo = traceGlobalIdx % channelCnt;
|
||
int traceInChIdx = traceGlobalIdx / channelCnt;
|
||
oneTrace.channelNumber = chNo;
|
||
|
||
float xOff = 0.0f;
|
||
float yOff = 0.0f;
|
||
if (chNo < model.header.chXOffsets.size()) xOff = model.header.chXOffsets[chNo];
|
||
if (chNo < model.header.chYOffsets.size()) yOff = model.header.chYOffsets[chNo];
|
||
|
||
float lineDist = static_cast<float>(model.header.startPosition + traceInChIdx * model.header.distanceInc);
|
||
oneTrace.position = QVector3D(xOff, lineDist - yOff, 0.0f);
|
||
|
||
model.traces.append(std::move(oneTrace));
|
||
}
|
||
|
||
file.close();
|
||
|
||
if (model.traces.isEmpty()) {
|
||
qDebug() << "Error: No valid traces loaded from iprb";
|
||
return false;
|
||
}
|
||
|
||
qDebug() << "IPRB Binary Load OK, total valid traces:" << model.traces.size();
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* @brief 加载 Impulse 多通道数据(每个通道一个 .iprh + .iprb)
|
||
* @param dirPath 数据所在目录
|
||
* @param baseName 测线基础名(如 "明星路_001")
|
||
* @param model 输出合并后的 GPR 数据模型
|
||
* @return true 加载合并成功
|
||
*
|
||
* 文件命名约定:baseName_A01.iprh / .iprb ... baseName_A14.iprh / .iprb
|
||
* 合并后 traces 按 Mala Mira 格式交错:trace0=ch0_pos0, trace1=ch1_pos0, ...
|
||
*/
|
||
bool IprhParser::convertImpulseMultiChannelToMala(const QString &dirPath,
|
||
const QString &baseName,
|
||
QString *radFilePath,
|
||
QString *errorMessage)
|
||
{
|
||
ImpulseMultiChannelConverter::ConversionPlan plan;
|
||
if (!ImpulseMultiChannelConverter::buildPlan(dirPath, baseName, plan, errorMessage)) {
|
||
if (errorMessage && errorMessage->isEmpty()) {
|
||
*errorMessage = QStringLiteral("多通道 Impulse 数据合并失败:%1").arg(baseName);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
ImpulseMultiChannelConverter::Options options;
|
||
options.overwriteExisting = true;
|
||
options.reuseExistingIfValid = false;
|
||
if (!ImpulseMultiChannelConverter::convertStreaming(plan, options, radFilePath, errorMessage)) {
|
||
return false;
|
||
}
|
||
|
||
qDebug() << "Impulse multi-channel converted to Mala Mira files:" << plan.outputRadPath << plan.outputRd3Path;
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* @brief 加载 Impulse 多通道数据(每个通道一个 .iprh + .iprb)
|
||
* @param dirPath 数据所在目录
|
||
* @param baseName 测线基础名(如 "明星路_001")
|
||
* @param model 输出合并后的 GPR 数据模型
|
||
* @return true 加载合并成功
|
||
*
|
||
* 文件命名约定:baseName_A01.iprh / .iprb ... baseName_A14.iprh / .iprb
|
||
* 合并后 traces 按 Mala Mira 格式交错:trace0=ch0_pos0, trace1=ch1_pos0, ...
|
||
*/
|
||
bool IprhParser::loadImpulseMultiChannel(const QString &dirPath,
|
||
const QString &baseName,
|
||
GPRDataModel &model)
|
||
{
|
||
SCOPED_PERF_TIMER("Parser", "IprhParser::loadImpulseMultiChannel");
|
||
|
||
model.clear();
|
||
model.header = GPRDataModel::Header{};
|
||
|
||
// 1. 发现所有通道文件并排序
|
||
QDir dir(dirPath);
|
||
dir.setFilter(QDir::Files | QDir::NoDotAndDotDot);
|
||
|
||
QRegularExpression reChannel(QStringLiteral("^%1_A(\\d+)\\.iprh$").arg(QRegularExpression::escape(baseName)));
|
||
QMap<int, QString> channelHeaders; // channelNum -> iprh path
|
||
|
||
for (const QFileInfo &fi : dir.entryInfoList()) {
|
||
QRegularExpressionMatch match = reChannel.match(fi.fileName());
|
||
if (match.hasMatch()) {
|
||
int chNum = match.captured(1).toInt();
|
||
QString iprhPath = fi.absoluteFilePath();
|
||
QString iprbPath = iprhPath;
|
||
iprbPath.replace(".iprh", ".iprb");
|
||
if (QFile::exists(iprbPath)) {
|
||
channelHeaders.insert(chNum, iprhPath);
|
||
} else {
|
||
qDebug() << "Missing .iprb for" << iprhPath;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (channelHeaders.isEmpty()) {
|
||
qDebug() << "No multi-channel .iprh/.iprb found for baseName:" << baseName;
|
||
return false;
|
||
}
|
||
|
||
const int channelCount = channelHeaders.size();
|
||
qDebug() << "Impulse multi-channel: found" << channelCount << "channels for" << baseName;
|
||
|
||
// 2. 解析第一个通道的头文件作为 master
|
||
auto it = channelHeaders.begin();
|
||
GPRDataModel::Header masterHeader;
|
||
if (!parseIprhHeader(it.value(), masterHeader)) {
|
||
qDebug() << "Failed to parse master header:" << it.value();
|
||
return false;
|
||
}
|
||
|
||
QVector<float> xOffsets;
|
||
QVector<float> yOffsets;
|
||
xOffsets.reserve(channelCount);
|
||
yOffsets.reserve(channelCount);
|
||
|
||
// 3. 验证各通道一致性并收集偏移量
|
||
QVector<int> channelTracesPerChannel;
|
||
channelTracesPerChannel.reserve(channelCount);
|
||
|
||
double antennaSeparation = 0.0;
|
||
|
||
for (auto cit = channelHeaders.begin(); cit != channelHeaders.end(); ++cit) {
|
||
int chNum = cit.key();
|
||
QString iprhPath = cit.value();
|
||
|
||
GPRDataModel::Header chHeader;
|
||
if (!parseIprhHeader(iprhPath, chHeader)) {
|
||
qDebug() << "Failed to parse channel header:" << iprhPath;
|
||
return false;
|
||
}
|
||
|
||
// 验证关键参数一致性
|
||
if (chHeader.samplesPerTrace != masterHeader.samplesPerTrace) {
|
||
qDebug() << "Inconsistent SAMPLES across channels";
|
||
return false;
|
||
}
|
||
if (qFuzzyCompare(chHeader.timeIntervalNs, masterHeader.timeIntervalNs) == false &&
|
||
chHeader.timeIntervalNs > 0 && masterHeader.timeIntervalNs > 0) {
|
||
qDebug() << "Warning: Inconsistent TIME INTERVAL across channels";
|
||
}
|
||
if (qFuzzyCompare(chHeader.distanceInc, masterHeader.distanceInc) == false &&
|
||
chHeader.distanceInc > 0 && masterHeader.distanceInc > 0) {
|
||
qDebug() << "Warning: Inconsistent DISTANCE INTERVAL across channels";
|
||
}
|
||
|
||
// 收集单通道偏移量
|
||
float xOff = 0.0f, yOff = 0.0f;
|
||
if (chHeader.rawParams.contains("CH_X_OFFSET")) {
|
||
xOff = chHeader.rawParams.value("CH_X_OFFSET").toFloat();
|
||
} else if (chHeader.rawParams.contains("CH_OFFSET_X")) {
|
||
xOff = chHeader.rawParams.value("CH_OFFSET_X").toFloat();
|
||
} else if (!chHeader.chXOffsets.isEmpty()) {
|
||
xOff = chHeader.chXOffsets.first();
|
||
}
|
||
if (chHeader.rawParams.contains("CH_Y_OFFSET")) {
|
||
yOff = chHeader.rawParams.value("CH_Y_OFFSET").toFloat();
|
||
} else if (chHeader.rawParams.contains("CH_OFFSET_Y")) {
|
||
yOff = chHeader.rawParams.value("CH_OFFSET_Y").toFloat();
|
||
} else if (!chHeader.chYOffsets.isEmpty()) {
|
||
yOff = chHeader.chYOffsets.first();
|
||
}
|
||
xOffsets.append(xOff);
|
||
yOffsets.append(yOff);
|
||
|
||
if (chHeader.rawParams.contains("ANTENNA SEPARATION")) {
|
||
antennaSeparation = chHeader.rawParams.value("ANTENNA SEPARATION").toDouble();
|
||
}
|
||
|
||
// 从 .iprb 大小计算该通道的道数
|
||
QString iprbPath = iprhPath;
|
||
iprbPath.replace(".iprh", ".iprb");
|
||
QFile iprbFile(iprbPath);
|
||
int tracesInThisChannel = 0;
|
||
if (iprbFile.open(QIODevice::ReadOnly)) {
|
||
qint64 fileSize = iprbFile.size();
|
||
qint64 traceBytes = static_cast<qint64>(chHeader.samplesPerTrace) * sizeof(short);
|
||
if (traceBytes > 0) {
|
||
tracesInThisChannel = static_cast<int>(fileSize / traceBytes);
|
||
}
|
||
iprbFile.close();
|
||
}
|
||
channelTracesPerChannel.append(tracesInThisChannel);
|
||
}
|
||
|
||
// 4. 以最小道数为准截断读取,容忍各通道微小差异
|
||
int tracesPerChannel = channelTracesPerChannel.isEmpty() ? 0 : *std::min_element(channelTracesPerChannel.begin(), channelTracesPerChannel.end());
|
||
for (int tc : channelTracesPerChannel) {
|
||
if (tc != tracesPerChannel) {
|
||
qDebug() << "Warning: Inconsistent trace count across channels. Using minimum:" << tracesPerChannel << "channel has:" << tc;
|
||
}
|
||
}
|
||
if (tracesPerChannel <= 0) {
|
||
qDebug() << "Error: No valid traces in any channel";
|
||
return false;
|
||
}
|
||
|
||
// 5. 如果偏移量全部为零,基于 ANTENNA SEPARATION 计算对称分布
|
||
bool allZeroOffsets = true;
|
||
for (float v : xOffsets) { if (qFuzzyIsNull(v) == false) { allZeroOffsets = false; break; } }
|
||
if (allZeroOffsets && antennaSeparation > 1e-6 && channelCount > 1) {
|
||
double totalWidth = (channelCount - 1) * antennaSeparation;
|
||
double startX = -totalWidth / 2.0;
|
||
for (int i = 0; i < channelCount; ++i) {
|
||
xOffsets[i] = static_cast<float>(startX + i * antennaSeparation);
|
||
}
|
||
qDebug() << "Computed symmetric X offsets from antenna separation:" << antennaSeparation;
|
||
}
|
||
|
||
// 6. 组装合并后的 Header
|
||
model.header = masterHeader;
|
||
model.header.numberOfChannels = channelCount;
|
||
model.header.numTraces = tracesPerChannel * channelCount;
|
||
model.header.chXOffsets = xOffsets;
|
||
model.header.chYOffsets = yOffsets;
|
||
if (model.header.timeWindowNs <= 0.0 && model.header.timeIntervalNs > 0.0) {
|
||
model.header.timeWindowNs = model.header.samplesPerTrace * model.header.timeIntervalNs;
|
||
}
|
||
|
||
// 7. 预分配 traces
|
||
model.traces.reserve(model.header.numTraces);
|
||
|
||
// 8. 读取每个通道的 .iprb 到临时数组
|
||
QVector<QVector<RadarTrace>> channelTraceArrays;
|
||
channelTraceArrays.resize(channelCount);
|
||
|
||
int chIdx = 0;
|
||
for (auto cit = channelHeaders.begin(); cit != channelHeaders.end(); ++cit, ++chIdx) {
|
||
QString iprhPath = cit.value();
|
||
QString iprbPath = iprhPath;
|
||
iprbPath.replace(".iprh", ".iprb");
|
||
|
||
QFile file(iprbPath);
|
||
if (!file.open(QIODevice::ReadOnly)) {
|
||
qDebug() << "Error: Cannot open" << iprbPath;
|
||
return false;
|
||
}
|
||
|
||
const int samplesPerTrace = model.header.samplesPerTrace;
|
||
const qint64 singleTraceByteSize = samplesPerTrace * sizeof(short);
|
||
QByteArray traceBuffer;
|
||
traceBuffer.resize(singleTraceByteSize);
|
||
|
||
channelTraceArrays[chIdx].reserve(tracesPerChannel);
|
||
|
||
for (int t = 0; t < tracesPerChannel; ++t) {
|
||
if (file.atEnd()) break;
|
||
qint64 readBytes = file.read(traceBuffer.data(), traceBuffer.size());
|
||
if (readBytes != traceBuffer.size()) break;
|
||
|
||
RadarTrace oneTrace;
|
||
oneTrace.amplitudes.resize(samplesPerTrace);
|
||
const short* rawShortBuf = reinterpret_cast<const short*>(traceBuffer.constData());
|
||
for (int s = 0; s < samplesPerTrace; s++) {
|
||
oneTrace.amplitudes[s] = rawShortBuf[s];
|
||
}
|
||
channelTraceArrays[chIdx].append(std::move(oneTrace));
|
||
}
|
||
file.close();
|
||
|
||
if (channelTraceArrays[chIdx].size() != tracesPerChannel) {
|
||
qDebug() << "Warning: Channel" << cit.key() << "has fewer traces than expected";
|
||
}
|
||
}
|
||
|
||
// 9. 按 Mala Mira 格式交错合并
|
||
model.channels = channelCount;
|
||
model.tracesPerChannel = tracesPerChannel;
|
||
for (int pos = 0; pos < tracesPerChannel; ++pos) {
|
||
for (int ch = 0; ch < channelCount; ++ch) {
|
||
if (pos < channelTraceArrays[ch].size()) {
|
||
RadarTrace trace = std::move(channelTraceArrays[ch][pos]);
|
||
trace.channelNumber = ch;
|
||
float xOff = (ch < xOffsets.size()) ? xOffsets[ch] : 0.0f;
|
||
float yOff = (ch < yOffsets.size()) ? yOffsets[ch] : 0.0f;
|
||
float lineDist = static_cast<float>(model.header.startPosition + pos * model.header.distanceInc);
|
||
trace.position = QVector3D(xOff, lineDist - yOff, 0.0f);
|
||
model.traces.append(std::move(trace));
|
||
}
|
||
}
|
||
}
|
||
|
||
if (model.header.distanceInc > 1e-6) {
|
||
model.totalDistance = static_cast<float>(model.tracesPerChannel * model.header.distanceInc);
|
||
} else {
|
||
model.totalDistance = static_cast<float>(model.header.stopPosition - model.header.startPosition);
|
||
}
|
||
|
||
qDebug() << "Impulse multi-channel load OK. Total traces:" << model.traces.size()
|
||
<< "Channels:" << channelCount << "Traces/Channel:" << tracesPerChannel;
|
||
return !model.traces.isEmpty();
|
||
}
|
||
|
||
bool IprhParser::writeMalaFiles(const QString &radFilePath,
|
||
const QString &rd3FilePath,
|
||
const GPRDataModel &model,
|
||
const QString &sourceBaseName,
|
||
QString *errorMessage)
|
||
{
|
||
QFile rd3File(rd3FilePath);
|
||
if (!rd3File.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
||
if (errorMessage) {
|
||
*errorMessage = QStringLiteral("无法写入转换后的 RD3 文件:%1").arg(rd3FilePath);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
QDataStream out(&rd3File);
|
||
out.setByteOrder(QDataStream::LittleEndian);
|
||
const int samplesPerTrace = model.header.samplesPerTrace;
|
||
for (const RadarTrace &trace : model.traces) {
|
||
for (int s = 0; s < samplesPerTrace; ++s) {
|
||
const qint16 sample = static_cast<qint16>(s < trace.amplitudes.size() ? trace.amplitudes[s] : 0);
|
||
out << sample;
|
||
}
|
||
}
|
||
rd3File.close();
|
||
|
||
QFile radFile(radFilePath);
|
||
if (!radFile.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) {
|
||
if (errorMessage) {
|
||
*errorMessage = QStringLiteral("无法写入转换后的 RAD 文件:%1").arg(radFilePath);
|
||
}
|
||
QFile::remove(rd3FilePath);
|
||
return false;
|
||
}
|
||
|
||
QTextStream rad(&radFile);
|
||
rad << "# Converted from Impulse multi-channel survey: " << sourceBaseName << '\n';
|
||
rad << "SAMPLES: " << model.header.samplesPerTrace << '\n';
|
||
rad << "LAST TRACE: " << model.traces.size() << '\n';
|
||
rad << "TIMEWINDOW: " << QLocale::c().toString(model.header.timeWindowNs, 'g', 12) << '\n';
|
||
rad << "TIME INTERVAL: " << QLocale::c().toString(model.header.timeIntervalNs, 'g', 12) << '\n';
|
||
rad << "DISTANCE INTERVAL: " << QLocale::c().toString(model.header.distanceInc, 'g', 12) << '\n';
|
||
rad << "ANTENNAS: " << QLocale::c().toString(model.header.antennaFreq, 'g', 12) << '\n';
|
||
if (!model.header.antennaType.isEmpty()) rad << "ANTENNA: " << model.header.antennaType << '\n';
|
||
if (!model.header.date.isEmpty()) rad << "DATE: " << model.header.date << '\n';
|
||
if (!model.header.timeStr.isEmpty()) rad << "TIME: " << model.header.timeStr << '\n';
|
||
rad << "NUMBER_OF_CH: " << qMax(1, model.header.numberOfChannels) << '\n';
|
||
rad << "CH_X_OFFSETS: " << formatFloatList(model.header.chXOffsets) << '\n';
|
||
rad << "CH_Y_OFFSETS: " << formatFloatList(model.header.chYOffsets) << '\n';
|
||
if (!model.header.units.isEmpty()) rad << "UNITS: " << model.header.units << '\n';
|
||
rad << "START POSITION: " << QLocale::c().toString(model.header.startPosition, 'g', 12) << '\n';
|
||
const double stopPosition = model.header.stopPosition > model.header.startPosition
|
||
? model.header.stopPosition
|
||
: model.header.startPosition + model.tracesPerChannel * model.header.distanceInc;
|
||
rad << "STOP POSITION: " << QLocale::c().toString(stopPosition, 'g', 12) << '\n';
|
||
radFile.close();
|
||
|
||
return true;
|
||
}
|
||
|
||
QString IprhParser::formatFloatList(const QVector<float> &values)
|
||
{
|
||
QStringList parts;
|
||
parts.reserve(values.size());
|
||
for (float value : values) {
|
||
parts.append(QLocale::c().toString(value, 'g', 10));
|
||
}
|
||
return parts.join(' ');
|
||
}
|
||
|
||
QString IprhParser::uniqueConvertedBasePath(const QString &dirPath, const QString &baseName)
|
||
{
|
||
return QDir(dirPath).absoluteFilePath(baseName + QStringLiteral("_mala_converted"));
|
||
}
|
||
|
||
double IprhParser::extractDouble(const QString &value) {
|
||
bool ok = false;
|
||
double res = value.toDouble(&ok);
|
||
return ok ? res : 0.0;
|
||
}
|
||
|
||
int IprhParser::extractInt(const QString &value) {
|
||
bool ok = false;
|
||
int res = value.toInt(&ok);
|
||
return ok ? res : 0;
|
||
}
|