#include "IprhParser.h" #include "PerformanceLogger.h" #include "ImpulseMultiChannelConverter.h" #include #include #include #include #include #include #include #include #include #include #include #include /** * @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(model.header.samplesPerTrace) * sizeof(short); if (traceBytes > 0) { model.header.numTraces = static_cast(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(model.tracesPerChannel * model.header.distanceInc); } else { model.totalDistance = static_cast(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(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(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 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 xOffsets; QVector yOffsets; xOffsets.reserve(channelCount); yOffsets.reserve(channelCount); // 3. 验证各通道一致性并收集偏移量 QVector 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(chHeader.samplesPerTrace) * sizeof(short); if (traceBytes > 0) { tracesInThisChannel = static_cast(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(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> 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(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(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(model.tracesPerChannel * model.header.distanceInc); } else { model.totalDistance = static_cast(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(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 &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; }