diff --git a/App/HoleDetection/HoleDetectionApp/Presenter/Src/DetectPresenter.cpp b/App/HoleDetection/HoleDetectionApp/Presenter/Src/DetectPresenter.cpp index b08dd57..7a19b8e 100644 --- a/App/HoleDetection/HoleDetectionApp/Presenter/Src/DetectPresenter.cpp +++ b/App/HoleDetection/HoleDetectionApp/Presenter/Src/DetectPresenter.cpp @@ -113,29 +113,15 @@ int DetectPresenter::DetectHoles( detectionParams.angleThresholdPos = static_cast(algorithmParams.detectionParam.angleThresholdPos); detectionParams.angleThresholdNeg = static_cast(algorithmParams.detectionParam.angleThresholdNeg); detectionParams.minPitDepth = static_cast(algorithmParams.detectionParam.minPitDepth); - detectionParams.angleStep = static_cast(algorithmParams.detectionParam.angleStep); - detectionParams.maxScanRadius = static_cast(algorithmParams.detectionParam.maxScanRadius); - detectionParams.clusterEps = static_cast(algorithmParams.detectionParam.clusterEps); - detectionParams.clusterMinPoints = algorithmParams.detectionParam.clusterMinPoints; detectionParams.minRadius = static_cast(algorithmParams.detectionParam.minRadius); detectionParams.maxRadius = static_cast(algorithmParams.detectionParam.maxRadius); detectionParams.expansionSize1 = algorithmParams.detectionParam.expansionSize1; detectionParams.expansionSize2 = algorithmParams.detectionParam.expansionSize2; - detectionParams.validZThreshold = static_cast(algorithmParams.detectionParam.validZThreshold); detectionParams.minVTransitionPoints = algorithmParams.detectionParam.minVTransitionPoints; - detectionParams.cornerScale = static_cast(algorithmParams.detectionParam.cornerScale); - detectionParams.cornerAngleThreshold = static_cast(algorithmParams.detectionParam.cornerAngleThreshold); - detectionParams.jumpCornerTh_1 = static_cast(algorithmParams.detectionParam.jumpCornerTh_1); - detectionParams.jumpCornerTh_2 = static_cast(algorithmParams.detectionParam.jumpCornerTh_2); - detectionParams.minEndingGap = static_cast(algorithmParams.detectionParam.minEndingGap); - detectionParams.minEndingGap_z = static_cast(algorithmParams.detectionParam.minEndingGap_z); // 映射 VrHoleFilterParam -> SHoleFilterParams SHoleFilterParams filterParams; - filterParams.minHoleRadius = static_cast(algorithmParams.filterParam.minHoleRadius); - filterParams.maxHoleRadius = static_cast(algorithmParams.filterParam.maxHoleRadius); filterParams.maxEccentricity = static_cast(algorithmParams.filterParam.maxEccentricity); - filterParams.maxCornerRatio = static_cast(algorithmParams.filterParam.maxCornerRatio); filterParams.minAngularCoverage = static_cast(algorithmParams.filterParam.minAngularCoverage); filterParams.maxRadiusFitRatio = static_cast(algorithmParams.filterParam.maxRadiusFitRatio); filterParams.minQualityScore = static_cast(algorithmParams.filterParam.minQualityScore); @@ -147,24 +133,14 @@ int DetectPresenter::DetectHoles( LOG_INFO("[Algo Thread] DetectionParams: neighborCount=%d, angleThresholdPos=%.1f, angleThresholdNeg=%.1f, minPitDepth=%.1f\n", detectionParams.neighborCount, detectionParams.angleThresholdPos, detectionParams.angleThresholdNeg, detectionParams.minPitDepth); - LOG_INFO("[Algo Thread] DetectionParams: angleStep=%.1f, maxScanRadius=%.1f, clusterEps=%.1f, clusterMinPoints=%d\n", - detectionParams.angleStep, detectionParams.maxScanRadius, - detectionParams.clusterEps, detectionParams.clusterMinPoints); LOG_INFO("[Algo Thread] DetectionParams: minRadius=%.1f, maxRadius=%.1f, expansionSize1=%d, expansionSize2=%d\n", detectionParams.minRadius, detectionParams.maxRadius, detectionParams.expansionSize1, detectionParams.expansionSize2); - LOG_INFO("[Algo Thread] DetectionParams: validZThreshold=%.4f, minVTransitionPoints=%d\n", - detectionParams.validZThreshold, detectionParams.minVTransitionPoints); - LOG_INFO("[Algo Thread] DetectionParams: cornerScale=%.1f, cornerAngleThreshold=%.1f, jumpCornerTh_1=%.1f, jumpCornerTh_2=%.1f\n", - detectionParams.cornerScale, detectionParams.cornerAngleThreshold, - detectionParams.jumpCornerTh_1, detectionParams.jumpCornerTh_2); - LOG_INFO("[Algo Thread] DetectionParams: minEndingGap=%.1f, minEndingGap_z=%.1f\n", - detectionParams.minEndingGap, detectionParams.minEndingGap_z); + LOG_INFO("[Algo Thread] DetectionParams: minVTransitionPoints=%d\n", + detectionParams.minVTransitionPoints); - LOG_INFO("[Algo Thread] FilterParams: minHoleRadius=%.1f, maxHoleRadius=%.1f, maxEccentricity=%.5f\n", - filterParams.minHoleRadius, filterParams.maxHoleRadius, filterParams.maxEccentricity); - LOG_INFO("[Algo Thread] FilterParams: maxCornerRatio=%.2f, minAngularCoverage=%.1f, maxRadiusFitRatio=%.2f\n", - filterParams.maxCornerRatio, filterParams.minAngularCoverage, filterParams.maxRadiusFitRatio); + LOG_INFO("[Algo Thread] FilterParams: maxEccentricity=%.5f, minAngularCoverage=%.1f, maxRadiusFitRatio=%.2f\n", + filterParams.maxEccentricity, filterParams.minAngularCoverage, filterParams.maxRadiusFitRatio); LOG_INFO("[Algo Thread] FilterParams: minQualityScore=%.2f, maxPlaneResidual=%.1f, maxAngularGap=%.1f, minInlierRatio=%.2f\n", filterParams.minQualityScore, filterParams.maxPlaneResidual, filterParams.maxAngularGap, filterParams.minInlierRatio); diff --git a/App/HoleDetection/HoleDetectionApp/Presenter/Src/HoleDetectionPresenter.cpp b/App/HoleDetection/HoleDetectionApp/Presenter/Src/HoleDetectionPresenter.cpp index f77542a..c55becc 100644 --- a/App/HoleDetection/HoleDetectionApp/Presenter/Src/HoleDetectionPresenter.cpp +++ b/App/HoleDetection/HoleDetectionApp/Presenter/Src/HoleDetectionPresenter.cpp @@ -206,27 +206,17 @@ int HoleDetectionPresenter::InitAlgoParams() xmlParams.detectionParam.angleThresholdPos, xmlParams.detectionParam.angleThresholdNeg, xmlParams.detectionParam.minPitDepth); - LOG_INFO("Loaded XML params - Detection: angleStep=%.1f, maxScanRadius=%.1f, clusterEps=%.1f, clusterMinPoints=%d\n", - xmlParams.detectionParam.angleStep, - xmlParams.detectionParam.maxScanRadius, - xmlParams.detectionParam.clusterEps, - xmlParams.detectionParam.clusterMinPoints); LOG_INFO("Loaded XML params - Detection: minRadius=%.1f, maxRadius=%.1f, expansionSize1=%d, expansionSize2=%d\n", xmlParams.detectionParam.minRadius, xmlParams.detectionParam.maxRadius, xmlParams.detectionParam.expansionSize1, xmlParams.detectionParam.expansionSize2); - LOG_INFO("Loaded XML params - Detection: validZThreshold=%.4f, minVTransitionPoints=%d\n", - xmlParams.detectionParam.validZThreshold, + LOG_INFO("Loaded XML params - Detection: minVTransitionPoints=%d\n", xmlParams.detectionParam.minVTransitionPoints); // 打印过滤参数 - LOG_INFO("Loaded XML params - Filter: minHoleRadius=%.1f, maxHoleRadius=%.1f, maxEccentricity=%.5f\n", - xmlParams.filterParam.minHoleRadius, - xmlParams.filterParam.maxHoleRadius, - xmlParams.filterParam.maxEccentricity); - LOG_INFO("Loaded XML params - Filter: maxCornerRatio=%.2f, minAngularCoverage=%.1f, maxRadiusFitRatio=%.2f\n", - xmlParams.filterParam.maxCornerRatio, + LOG_INFO("Loaded XML params - Filter: maxEccentricity=%.5f, minAngularCoverage=%.1f, maxRadiusFitRatio=%.2f\n", + xmlParams.filterParam.maxEccentricity, xmlParams.filterParam.minAngularCoverage, xmlParams.filterParam.maxRadiusFitRatio); LOG_INFO("Loaded XML params - Filter: minQualityScore=%.2f, maxPlaneResidual=%.1f, maxAngularGap=%.1f, minInlierRatio=%.2f\n", diff --git a/App/HoleDetection/HoleDetectionApp/Version.h b/App/HoleDetection/HoleDetectionApp/Version.h index cc7d3f3..3a74af1 100644 --- a/App/HoleDetection/HoleDetectionApp/Version.h +++ b/App/HoleDetection/HoleDetectionApp/Version.h @@ -3,7 +3,7 @@ // 版本字符串 #define HOLEDETECTION_VERSION_STRING "1.0.0" -#define HOLEDETECTION_BUILD_STRING "2" +#define HOLEDETECTION_BUILD_STRING "3" #define HOLEDETECTION_FULL_VERSION_STRING "V" HOLEDETECTION_VERSION_STRING "_" HOLEDETECTION_BUILD_STRING // 构建日期 diff --git a/App/HoleDetection/HoleDetectionApp/dialogalgoarg.cpp b/App/HoleDetection/HoleDetectionApp/dialogalgoarg.cpp index 876b9f1..8544442 100644 --- a/App/HoleDetection/HoleDetectionApp/dialogalgoarg.cpp +++ b/App/HoleDetection/HoleDetectionApp/dialogalgoarg.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include "PathManager.h" @@ -89,6 +90,9 @@ void DialogAlgoarg::LoadConfigToUI() // 加载网络配置(PLC和机械臂服务端) LoadPlcRobotServerConfigToUI(configData.plcRobotServerConfig); + // 初始化标定矩阵缓存(必须在LoadCalibMatrixToUI之前调用) + m_calibMatrixCache = configData.handEyeCalibMatrixList; + // 加载手眼标定矩阵 LoadCalibMatrixToUI(); @@ -124,32 +128,18 @@ void DialogAlgoarg::LoadDetectionParamToUI(const VrHoleDetectionParam& param) ui->lineEdit_angleThresholdPos->setText(QString::number(param.angleThresholdPos)); ui->lineEdit_angleThresholdNeg->setText(QString::number(param.angleThresholdNeg)); ui->lineEdit_minPitDepth->setText(QString::number(param.minPitDepth)); - ui->lineEdit_angleStep->setText(QString::number(param.angleStep)); - ui->lineEdit_maxScanRadius->setText(QString::number(param.maxScanRadius)); - ui->lineEdit_clusterEps->setText(QString::number(param.clusterEps)); - ui->lineEdit_clusterMinPoints->setText(QString::number(param.clusterMinPoints)); ui->lineEdit_minRadius->setText(QString::number(param.minRadius)); ui->lineEdit_maxRadius->setText(QString::number(param.maxRadius)); ui->lineEdit_expansionSize1->setText(QString::number(param.expansionSize1)); ui->lineEdit_expansionSize2->setText(QString::number(param.expansionSize2)); - ui->lineEdit_validZThreshold->setText(QString::number(param.validZThreshold)); ui->lineEdit_minVTransitionPoints->setText(QString::number(param.minVTransitionPoints)); - ui->lineEdit_cornerScale->setText(QString::number(param.cornerScale)); - ui->lineEdit_cornerAngleThreshold->setText(QString::number(param.cornerAngleThreshold)); - ui->lineEdit_jumpCornerTh_1->setText(QString::number(param.jumpCornerTh_1)); - ui->lineEdit_jumpCornerTh_2->setText(QString::number(param.jumpCornerTh_2)); - ui->lineEdit_minEndingGap->setText(QString::number(param.minEndingGap)); - ui->lineEdit_minEndingGap_z->setText(QString::number(param.minEndingGap_z)); } void DialogAlgoarg::LoadFilterParamToUI(const VrHoleFilterParam& param) { if (!ui) return; - ui->lineEdit_minHoleRadius->setText(QString::number(param.minHoleRadius)); - ui->lineEdit_maxHoleRadius->setText(QString::number(param.maxHoleRadius)); ui->lineEdit_maxEccentricity->setText(QString::number(param.maxEccentricity)); - ui->lineEdit_maxCornerRatio->setText(QString::number(param.maxCornerRatio)); ui->lineEdit_minAngularCoverage->setText(QString::number(param.minAngularCoverage)); ui->lineEdit_maxRadiusFitRatio->setText(QString::number(param.maxRadiusFitRatio)); ui->lineEdit_minQualityScore->setText(QString::number(param.minQualityScore)); @@ -237,18 +227,6 @@ bool DialogAlgoarg::SaveDetectionParamFromUI(VrHoleDetectionParam& param) param.minPitDepth = ui->lineEdit_minPitDepth->text().toDouble(&ok); if (!ok) return false; - param.angleStep = ui->lineEdit_angleStep->text().toDouble(&ok); - if (!ok) return false; - - param.maxScanRadius = ui->lineEdit_maxScanRadius->text().toDouble(&ok); - if (!ok) return false; - - param.clusterEps = ui->lineEdit_clusterEps->text().toDouble(&ok); - if (!ok) return false; - - param.clusterMinPoints = ui->lineEdit_clusterMinPoints->text().toInt(&ok); - if (!ok) return false; - param.minRadius = ui->lineEdit_minRadius->text().toDouble(&ok); if (!ok) return false; @@ -261,30 +239,9 @@ bool DialogAlgoarg::SaveDetectionParamFromUI(VrHoleDetectionParam& param) param.expansionSize2 = ui->lineEdit_expansionSize2->text().toInt(&ok); if (!ok) return false; - param.validZThreshold = ui->lineEdit_validZThreshold->text().toDouble(&ok); - if (!ok) return false; - param.minVTransitionPoints = ui->lineEdit_minVTransitionPoints->text().toInt(&ok); if (!ok) return false; - param.cornerScale = ui->lineEdit_cornerScale->text().toDouble(&ok); - if (!ok) return false; - - param.cornerAngleThreshold = ui->lineEdit_cornerAngleThreshold->text().toDouble(&ok); - if (!ok) return false; - - param.jumpCornerTh_1 = ui->lineEdit_jumpCornerTh_1->text().toDouble(&ok); - if (!ok) return false; - - param.jumpCornerTh_2 = ui->lineEdit_jumpCornerTh_2->text().toDouble(&ok); - if (!ok) return false; - - param.minEndingGap = ui->lineEdit_minEndingGap->text().toDouble(&ok); - if (!ok) return false; - - param.minEndingGap_z = ui->lineEdit_minEndingGap_z->text().toDouble(&ok); - if (!ok) return false; - return true; } @@ -292,18 +249,9 @@ bool DialogAlgoarg::SaveFilterParamFromUI(VrHoleFilterParam& param) { bool ok = true; - param.minHoleRadius = ui->lineEdit_minHoleRadius->text().toDouble(&ok); - if (!ok) return false; - - param.maxHoleRadius = ui->lineEdit_maxHoleRadius->text().toDouble(&ok); - if (!ok) return false; - param.maxEccentricity = ui->lineEdit_maxEccentricity->text().toDouble(&ok); if (!ok) return false; - param.maxCornerRatio = ui->lineEdit_maxCornerRatio->text().toDouble(&ok); - if (!ok) return false; - param.minAngularCoverage = ui->lineEdit_minAngularCoverage->text().toDouble(&ok); if (!ok) return false; @@ -422,9 +370,13 @@ void DialogAlgoarg::InitCalibCameraComboBox() QLineEdit* DialogAlgoarg::GetCalibLineEdit(int row, int col) { - // 通过名称查找对应的QLineEdit + // 通过名称查找对应的QLineEdit (UI中使用 lineEdit_calib_row_col 格式) QString name = QString("lineEdit_calib_%1_%2").arg(row).arg(col); - return findChild(name); + QLineEdit* edit = findChild(name); + if (!edit) { + LOG_WARNING("LineEdit not found: %s\n", name.toStdString().c_str()); + } + return edit; } void DialogAlgoarg::LoadCalibMatrixToUI() @@ -435,36 +387,75 @@ void DialogAlgoarg::LoadCalibMatrixToUI() void DialogAlgoarg::LoadCalibMatrixForCamera(int cameraIndex) { + LOG_INFO("LoadCalibMatrixForCamera: cameraIndex=%d, cacheSize=%zu\n", + cameraIndex, m_calibMatrixCache.size()); + + // 检查索引是否有效 if (cameraIndex < 0 || cameraIndex >= static_cast(m_calibMatrixCache.size())) { - // 索引无效,显示单位矩阵 - VrHandEyeCalibMatrix defaultMatrix; - for (int i = 0; i < 16; i++) { - QLineEdit* edit = GetCalibLineEdit(i / 4, i % 4); - if (edit) { - edit->setText(QString::number(defaultMatrix.matrix[i], 'f', 6)); - } - } - // 默认旋转顺序 - int index = ui->comboBox_eulerOrder->findData(11); - if (index >= 0) ui->comboBox_eulerOrder->setCurrentIndex(index); + LOG_WARNING("Invalid camera index, loading identity matrix\n"); + LoadIdentityMatrixToUI(); return; } const VrHandEyeCalibMatrix& calibMatrix = m_calibMatrixCache[cameraIndex]; + // 打印矩阵前4个元素用于调试 + LOG_INFO("Matrix values: [0]=%.6f, [1]=%.6f, [2]=%.6f, [3]=%.6f\n", + calibMatrix.matrix[0], calibMatrix.matrix[1], + calibMatrix.matrix[2], calibMatrix.matrix[3]); + + // 直接加载矩阵数据到UI(不再检查是否为空) for (int i = 0; i < 16; i++) { QLineEdit* edit = GetCalibLineEdit(i / 4, i % 4); if (edit) { edit->setText(QString::number(calibMatrix.matrix[i], 'f', 6)); + } else { + LOG_WARNING("LineEdit not found for matrix[%d]\n", i); } } // 加载旋转顺序 int eulerOrder = calibMatrix.eulerOrder; + LOG_INFO("Loading eulerOrder: %d\n", eulerOrder); int index = ui->comboBox_eulerOrder->findData(eulerOrder); if (index >= 0) { ui->comboBox_eulerOrder->setCurrentIndex(index); + } else { + LOG_WARNING("EulerOrder %d not found in comboBox, using default\n", eulerOrder); + ui->comboBox_eulerOrder->setCurrentIndex(0); } + + LOG_INFO("LoadCalibMatrixForCamera completed\n"); +} + +void DialogAlgoarg::LoadIdentityMatrixToUI() +{ + LOG_INFO("LoadIdentityMatrixToUI called\n"); + + // 显示单位矩阵 + for (int i = 0; i < 16; i++) { + int row = i / 4; + int col = i % 4; + QLineEdit* edit = GetCalibLineEdit(row, col); + if (edit) { + // 对角线元素为1,其他为0 + double value = (row == col) ? 1.0 : 0.0; + edit->setText(QString::number(value, 'f', 6)); + } else { + LOG_WARNING("LineEdit not found for identity matrix[%d]\n", i); + } + } + + // 默认旋转顺序:外旋ZYX + int index = ui->comboBox_eulerOrder->findData(11); + if (index >= 0) { + ui->comboBox_eulerOrder->setCurrentIndex(index); + } else { + LOG_WARNING("Default eulerOrder 11 not found, using index 0\n"); + ui->comboBox_eulerOrder->setCurrentIndex(0); + } + + LOG_INFO("LoadIdentityMatrixToUI completed\n"); } void DialogAlgoarg::LoadCalibMatrixFromFile(const QString& filePath) diff --git a/App/HoleDetection/HoleDetectionApp/dialogalgoarg.h b/App/HoleDetection/HoleDetectionApp/dialogalgoarg.h index 4d411cf..7305fc8 100644 --- a/App/HoleDetection/HoleDetectionApp/dialogalgoarg.h +++ b/App/HoleDetection/HoleDetectionApp/dialogalgoarg.h @@ -50,6 +50,7 @@ private: void InitCalibCameraComboBox(); void LoadCalibMatrixToUI(); void LoadCalibMatrixForCamera(int cameraIndex); + void LoadIdentityMatrixToUI(); // 加载单位矩阵到UI void LoadCalibMatrixFromFile(const QString& filePath); bool SaveCalibMatrixToConfig(std::vector& calibMatrixList); bool SaveCurrentCalibMatrixToCache(); diff --git a/App/HoleDetection/HoleDetectionApp/dialogalgoarg.ui b/App/HoleDetection/HoleDetectionApp/dialogalgoarg.ui index a42b9b7..f80edee 100644 --- a/App/HoleDetection/HoleDetectionApp/dialogalgoarg.ui +++ b/App/HoleDetection/HoleDetectionApp/dialogalgoarg.ui @@ -7,7 +7,7 @@ 0 0 780 - 760 + 656 @@ -44,7 +44,7 @@ 40 80 701 - 611 + 491 @@ -62,7 +62,7 @@ QGroupBox { color: rgb(221, 225, 233); border: 1px solid rgb(100, 100, 100); bor QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 3px 0 3px; } - 2 + 0 @@ -74,7 +74,7 @@ QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 3px 0 3px; 10 10 671 - 561 + 411 @@ -89,9 +89,9 @@ QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 3px 0 3px; 20 - 30 + 40 631 - 656 + 341 @@ -136,165 +136,55 @@ QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 3px 0 3px; - - - 径向扫描角度步长 (angleStep) - - - - - - - - - - 最大扫描半径 mm (maxScanRadius) - - - - - - - - - - DBSCAN聚类半径 mm (clusterEps) - - - - - - - - - - DBSCAN最小点数 (clusterMinPoints) - - - - - - - 最小孔半径 mm (minRadius) - + - + 最大孔半径 mm (maxRadius) - + - + 第一环扩展大小 (expansionSize1) - + - + 第二环扩展大小 (expansionSize2) - + - - - - 有效Z值阈值 (validZThreshold) - - - - - - - + V形最小过渡点数 (minVTransitionPoints) - + - - - - 角点搜索距离 mm (cornerScale) - - - - - - - - - - 角点角度阈值 (cornerAngleThreshold) - - - - - - - - - - 跳跃小角度阈值 (jumpCornerTh_1) - - - - - - - - - - 跳跃大角度阈值 (jumpCornerTh_2) - - - - - - - - - - Y方向配对距离 mm (minEndingGap) - - - - - - - - - - Z方向高度阈值 mm (minEndingGap_z) - - - - - - @@ -309,7 +199,7 @@ QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 3px 0 3px; 10 10 671 - 561 + 421 @@ -326,118 +216,88 @@ QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 3px 0 3px; 20 30 631 - 510 + 371 - - - 最小孔半径 mm (minHoleRadius) - - - - - - - - - - 最大孔半径 mm (maxHoleRadius) - - - - - - - 最大离心率 (maxEccentricity) - + - - - - 最大矩形度比率 (maxCornerRatio) - - - - - - - + 最小角度覆盖 (minAngularCoverage) - + - + 最大半径拟合比率 (maxRadiusFitRatio) - + - + 最小质量分数 (minQualityScore) - + - + 最大平面残差 mm (maxPlaneResidual) - + - + 最大角度间隙 (maxAngularGap) - + - + 最小内点比率 (minInlierRatio) - + - + 排序模式 - + QComboBox { @@ -472,7 +332,7 @@ QComboBox QAbstractItemView { 10 10 671 - 561 + 421 @@ -610,7 +470,7 @@ QComboBox QAbstractItemView { 10 10 671 - 561 + 411 @@ -925,7 +785,7 @@ QComboBox QAbstractItemView { 2 - 700 + 590 776 44 diff --git a/App/HoleDetection/HoleDetectionConfig/Inc/IVrConfig.h b/App/HoleDetection/HoleDetectionConfig/Inc/IVrConfig.h index 3098c36..2cee28f 100644 --- a/App/HoleDetection/HoleDetectionConfig/Inc/IVrConfig.h +++ b/App/HoleDetection/HoleDetectionConfig/Inc/IVrConfig.h @@ -20,24 +20,11 @@ struct VrHoleDetectionParam double angleThresholdPos = 10.0; // 正角度阈值(度) double angleThresholdNeg = -10.0; // 负角度阈值(度) double minPitDepth = 1.0; // 最小凹坑深度(mm) - double angleStep = 1.0; // 径向扫描角度步长(度) - double maxScanRadius = 100.0; // 最大扫描半径(mm) - double clusterEps = 5.0; // DBSCAN聚类半径(mm) - int clusterMinPoints = 5; // DBSCAN最小点数 - double minRadius = 5.0; // 最小孔半径(mm) - double maxRadius = 50.0; // 最大孔半径(mm) - int expansionSize1 = 5; // 第一环扩展大小 - int expansionSize2 = 10; // 第二环扩展大小 - double validZThreshold = 0.0001; // 有效Z值阈值 + double minRadius = 2.0; // 最小孔半径(mm) + double maxRadius = 50.0; // 最大孔半径(mm) + int expansionSize1 = 5; // 第一环扩展大小 + int expansionSize2 = 10; // 第二环扩展大小 int minVTransitionPoints = 1; // V形端点间最小有效过渡点数 - - // 角点检测参数(cornerMethod) - double cornerScale = 2.0; // 前后搜索点距离(mm) - double cornerAngleThreshold = 45.0; // 最小角点角度变化(度) - double jumpCornerTh_1 = 10.0; // 跳跃检测小角度阈值(度) - double jumpCornerTh_2 = 30.0; // 跳跃检测大角度阈值(度) - double minEndingGap = 5.0; // Y方向跳跃配对距离阈值(mm) - double minEndingGap_z = 1.0; // Z方向跳跃验证高度阈值(mm) }; /** @@ -45,16 +32,13 @@ struct VrHoleDetectionParam */ struct VrHoleFilterParam { - double minHoleRadius = 1.0; // 最小孔半径(mm) - double maxHoleRadius = 10.0; // 最大孔半径(mm) double maxEccentricity = 0.99995; // 最大离心率 - double maxCornerRatio = 0.15; // 最大矩形度比率 double minAngularCoverage = 10.0; // 最小角度覆盖(度) - double maxRadiusFitRatio = 0.3; // 最大半径拟合比率 - double minQualityScore = 0.3; // 最小质量分数 + double maxRadiusFitRatio = 1.0; // 最大半径拟合比率 + double minQualityScore = 0.0; // 最小质量分数 double maxPlaneResidual = 10.0; // 最大平面残差(mm) double maxAngularGap = 90.0; // 最大角度间隙(度) - double minInlierRatio = 0.3; // 最小内点比率 + double minInlierRatio = 0.0; // 最小内点比率 }; /** diff --git a/App/HoleDetection/HoleDetectionConfig/Src/VrConfig.cpp b/App/HoleDetection/HoleDetectionConfig/Src/VrConfig.cpp index 4a2038c..fe47fec 100644 --- a/App/HoleDetection/HoleDetectionConfig/Src/VrConfig.cpp +++ b/App/HoleDetection/HoleDetectionConfig/Src/VrConfig.cpp @@ -103,14 +103,6 @@ int CVrConfig::LoadConfig(const std::string& filePath, ConfigResult& configResul configResult.algorithmParams.detectionParam.angleThresholdNeg = detectionParamElement->DoubleAttribute("angleThresholdNeg"); if (detectionParamElement->Attribute("minPitDepth")) configResult.algorithmParams.detectionParam.minPitDepth = detectionParamElement->DoubleAttribute("minPitDepth"); - if (detectionParamElement->Attribute("angleStep")) - configResult.algorithmParams.detectionParam.angleStep = detectionParamElement->DoubleAttribute("angleStep"); - if (detectionParamElement->Attribute("maxScanRadius")) - configResult.algorithmParams.detectionParam.maxScanRadius = detectionParamElement->DoubleAttribute("maxScanRadius"); - if (detectionParamElement->Attribute("clusterEps")) - configResult.algorithmParams.detectionParam.clusterEps = detectionParamElement->DoubleAttribute("clusterEps"); - if (detectionParamElement->Attribute("clusterMinPoints")) - configResult.algorithmParams.detectionParam.clusterMinPoints = detectionParamElement->IntAttribute("clusterMinPoints"); if (detectionParamElement->Attribute("minRadius")) configResult.algorithmParams.detectionParam.minRadius = detectionParamElement->DoubleAttribute("minRadius"); if (detectionParamElement->Attribute("maxRadius")) @@ -119,36 +111,16 @@ int CVrConfig::LoadConfig(const std::string& filePath, ConfigResult& configResul configResult.algorithmParams.detectionParam.expansionSize1 = detectionParamElement->IntAttribute("expansionSize1"); if (detectionParamElement->Attribute("expansionSize2")) configResult.algorithmParams.detectionParam.expansionSize2 = detectionParamElement->IntAttribute("expansionSize2"); - if (detectionParamElement->Attribute("validZThreshold")) - configResult.algorithmParams.detectionParam.validZThreshold = detectionParamElement->DoubleAttribute("validZThreshold"); if (detectionParamElement->Attribute("minVTransitionPoints")) configResult.algorithmParams.detectionParam.minVTransitionPoints = detectionParamElement->IntAttribute("minVTransitionPoints"); - if (detectionParamElement->Attribute("cornerScale")) - configResult.algorithmParams.detectionParam.cornerScale = detectionParamElement->DoubleAttribute("cornerScale"); - if (detectionParamElement->Attribute("cornerAngleThreshold")) - configResult.algorithmParams.detectionParam.cornerAngleThreshold = detectionParamElement->DoubleAttribute("cornerAngleThreshold"); - if (detectionParamElement->Attribute("jumpCornerTh_1")) - configResult.algorithmParams.detectionParam.jumpCornerTh_1 = detectionParamElement->DoubleAttribute("jumpCornerTh_1"); - if (detectionParamElement->Attribute("jumpCornerTh_2")) - configResult.algorithmParams.detectionParam.jumpCornerTh_2 = detectionParamElement->DoubleAttribute("jumpCornerTh_2"); - if (detectionParamElement->Attribute("minEndingGap")) - configResult.algorithmParams.detectionParam.minEndingGap = detectionParamElement->DoubleAttribute("minEndingGap"); - if (detectionParamElement->Attribute("minEndingGap_z")) - configResult.algorithmParams.detectionParam.minEndingGap_z = detectionParamElement->DoubleAttribute("minEndingGap_z"); } - // 解析孔洞过滤参数(10个) + // 解析孔洞过滤参数 XMLElement* filterParamElement = algoParamsElement->FirstChildElement("FilterParam"); if (filterParamElement) { - if (filterParamElement->Attribute("minHoleRadius")) - configResult.algorithmParams.filterParam.minHoleRadius = filterParamElement->DoubleAttribute("minHoleRadius"); - if (filterParamElement->Attribute("maxHoleRadius")) - configResult.algorithmParams.filterParam.maxHoleRadius = filterParamElement->DoubleAttribute("maxHoleRadius"); if (filterParamElement->Attribute("maxEccentricity")) configResult.algorithmParams.filterParam.maxEccentricity = filterParamElement->DoubleAttribute("maxEccentricity"); - if (filterParamElement->Attribute("maxCornerRatio")) - configResult.algorithmParams.filterParam.maxCornerRatio = filterParamElement->DoubleAttribute("maxCornerRatio"); if (filterParamElement->Attribute("minAngularCoverage")) configResult.algorithmParams.filterParam.minAngularCoverage = filterParamElement->DoubleAttribute("minAngularCoverage"); if (filterParamElement->Attribute("maxRadiusFitRatio")) @@ -400,30 +372,16 @@ bool CVrConfig::SaveConfig(const std::string& filePath, ConfigResult& configResu detectionParamElement->SetAttribute("angleThresholdPos", configResult.algorithmParams.detectionParam.angleThresholdPos); detectionParamElement->SetAttribute("angleThresholdNeg", configResult.algorithmParams.detectionParam.angleThresholdNeg); detectionParamElement->SetAttribute("minPitDepth", configResult.algorithmParams.detectionParam.minPitDepth); - detectionParamElement->SetAttribute("angleStep", configResult.algorithmParams.detectionParam.angleStep); - detectionParamElement->SetAttribute("maxScanRadius", configResult.algorithmParams.detectionParam.maxScanRadius); - detectionParamElement->SetAttribute("clusterEps", configResult.algorithmParams.detectionParam.clusterEps); - detectionParamElement->SetAttribute("clusterMinPoints", configResult.algorithmParams.detectionParam.clusterMinPoints); detectionParamElement->SetAttribute("minRadius", configResult.algorithmParams.detectionParam.minRadius); detectionParamElement->SetAttribute("maxRadius", configResult.algorithmParams.detectionParam.maxRadius); detectionParamElement->SetAttribute("expansionSize1", configResult.algorithmParams.detectionParam.expansionSize1); detectionParamElement->SetAttribute("expansionSize2", configResult.algorithmParams.detectionParam.expansionSize2); - detectionParamElement->SetAttribute("validZThreshold", configResult.algorithmParams.detectionParam.validZThreshold); detectionParamElement->SetAttribute("minVTransitionPoints", configResult.algorithmParams.detectionParam.minVTransitionPoints); - detectionParamElement->SetAttribute("cornerScale", configResult.algorithmParams.detectionParam.cornerScale); - detectionParamElement->SetAttribute("cornerAngleThreshold", configResult.algorithmParams.detectionParam.cornerAngleThreshold); - detectionParamElement->SetAttribute("jumpCornerTh_1", configResult.algorithmParams.detectionParam.jumpCornerTh_1); - detectionParamElement->SetAttribute("jumpCornerTh_2", configResult.algorithmParams.detectionParam.jumpCornerTh_2); - detectionParamElement->SetAttribute("minEndingGap", configResult.algorithmParams.detectionParam.minEndingGap); - detectionParamElement->SetAttribute("minEndingGap_z", configResult.algorithmParams.detectionParam.minEndingGap_z); algoParamsElement->InsertEndChild(detectionParamElement); // 添加孔洞过滤参数 XMLElement* filterParamElement = doc.NewElement("FilterParam"); - filterParamElement->SetAttribute("minHoleRadius", configResult.algorithmParams.filterParam.minHoleRadius); - filterParamElement->SetAttribute("maxHoleRadius", configResult.algorithmParams.filterParam.maxHoleRadius); filterParamElement->SetAttribute("maxEccentricity", configResult.algorithmParams.filterParam.maxEccentricity); - filterParamElement->SetAttribute("maxCornerRatio", configResult.algorithmParams.filterParam.maxCornerRatio); filterParamElement->SetAttribute("minAngularCoverage", configResult.algorithmParams.filterParam.minAngularCoverage); filterParamElement->SetAttribute("maxRadiusFitRatio", configResult.algorithmParams.filterParam.maxRadiusFitRatio); filterParamElement->SetAttribute("minQualityScore", configResult.algorithmParams.filterParam.minQualityScore); diff --git a/App/WheelMeasure/WheelMeasureApp/Presenter/Inc/WheelMeasurePresenter.h b/App/WheelMeasure/WheelMeasureApp/Presenter/Inc/WheelMeasurePresenter.h index b402204..4f2938a 100644 --- a/App/WheelMeasure/WheelMeasureApp/Presenter/Inc/WheelMeasurePresenter.h +++ b/App/WheelMeasure/WheelMeasureApp/Presenter/Inc/WheelMeasurePresenter.h @@ -1,189 +1,207 @@ -#ifndef WHEELMEASUREPRESENTER_H -#define WHEELMEASUREPRESENTER_H - -#include "BasePresenter.h" -#include "IVrWheelMeasureConfig.h" -#include "IWheelMeasureStatus.h" -#include "CommonDialogCameraLevel.h" - -#include -#include -#include - -/** - * @brief 车轮拱高测量Presenter - * 继承BasePresenter,负责相机控制、算法调用和结果处理 - */ -class WheelMeasurePresenter : public BasePresenter, - public IVrWheelMeasureConfigChangeNotify, - public ICameraLevelCalculator, - public ICameraLevelResultSaver -{ - Q_OBJECT - -signals: - void configUpdated(); - -public: - explicit WheelMeasurePresenter(QObject* parent = nullptr); - ~WheelMeasurePresenter(); - - // ============ 实现 BasePresenter 纯虚函数 ============ - - /** - * @brief 私有初始化(实现纯虚函数) - */ - int InitApp() override; - - /** - * @brief 初始化算法参数(实现纯虚函数) - */ - int InitAlgoParams() override; - - /** - * @brief 执行算法检测(实现纯虚函数) - */ - int ProcessAlgoDetection(std::vector>& detectionDataCache) override; - - /** - * @brief 获取检测数据类型(实现纯虚函数) - */ - EVzResultDataType GetDetectionDataType() override { - return keResultDataType_Position; - } - - /** - * @brief 相机状态变化通知(实现纯虚函数) - */ - void OnCameraStatusChanged(int cameraIndex, bool isConnected) override; - - /** - * @brief 工作状态变化通知(重写虚函数) - */ - void OnWorkStatusChanged(WorkStatus status) override; - - /** - * @brief 相机数量变化通知(重写虚函数) - */ - void OnCameraCountChanged(int count) override; - - /** - * @brief 状态文字更新通知(重写虚函数) - */ - void OnStatusUpdate(const std::string& statusMessage) override; - - /** - * @brief Modbus写寄存器回调(重写虚函数) - */ - void OnModbusWriteCallback(uint16_t startAddress, const uint16_t* data, uint16_t count) override; - - // ============ 实现 ICameraLevelCalculator 接口 ============ - - bool CalculatePlaneCalibration( - const std::vector>& scanData, - double planeCalib[9], - double& planeHeight, - double invRMatrix[9]) override; - - // ============ 实现 ICameraLevelResultSaver 接口 ============ - - bool SaveLevelingResults(double planeCalib[9], double planeHeight, double invRMatrix[9], - int cameraIndex, const QString& cameraName) override; - - bool LoadLevelingResults(int cameraIndex, const QString& cameraName, - double planeCalib[9], double& planeHeight, double invRMatrix[9]) override; - - // ============ IVrWheelMeasureConfigChangeNotify 接口实现 ============ - - void OnConfigChanged(const WheelMeasureConfigResult& configResult) override; - - // ============ 公共接口 ============ - - /** - * @brief 设置状态更新接口 - */ - void setStatusUpdate(IWheelMeasureStatus* statusUpdate) { m_statusUpdate = statusUpdate; } - - /** - * @brief 获取配置接口 - */ - IVrWheelMeasureConfig* GetConfig() const { return m_config; } - - /** - * @brief 获取配置结果 - */ - WheelMeasureConfigResult* GetConfigResult() { return &m_configResult; } - - /** - * @brief 获取相机名称列表 - */ - QStringList getCameraNames() const; - - /** - * @brief 重新检测 - */ - void ResetDetect(int cameraIndex = 0); - - /** - * @brief 启动所有相机检测(依次执行) - */ - void StartAllDetection(); - - /** - * @brief 停止所有相机检测(完成当前设备后停止) - */ - void StopAllDetection(); - - /** - * @brief 检查是否正在进行顺序检测 - */ - bool IsSequentialDetecting() const { return m_sequentialDetecting; } - - /** - * @brief 设置默认相机索引 - */ - void SetDefaultCameraIndex(int cameraIndex) { m_currentCameraIndex = cameraIndex; } - - /** - * @brief 获取当前默认相机索引 - */ - int GetDefaultCameraIndex() const { return m_currentCameraIndex; } - - /** - * @brief 静态相机状态回调函数 - */ - static void _StaticCameraNotify(EVzDeviceWorkStatus eStatus, void* pExtData, unsigned int nDataLength, void* pInfoParam); - -private: - // 初始化配置 - bool initializeConfig(const QString& configPath); - - // 初始化相机 - bool initializeCameras(); - - // 处理扫描数据 - void processScanData(std::vector>& detectionDataCache); - - // 根据相机索引获取调平参数 - WheelCameraPlaneCalibParam* getPlaneCalibParam(int cameraIndex); - - // 相机状态回调处理 - void _CameraNotify(EVzDeviceWorkStatus eStatus, void* pExtData, unsigned int nDataLength, void* pInfoParam); - -private: - IVrWheelMeasureConfig* m_config = nullptr; - WheelMeasureConfigResult m_configResult; - IWheelMeasureStatus* m_statusUpdate = nullptr; - int m_currentCameraIndex = 1; // 默认相机索引(1-based) - - // 顺序检测相关 - bool m_sequentialDetecting = false; // 是否正在顺序检测所有设备 - bool m_stopSequentialRequested = false; // 是否请求停止顺序检测 - int m_sequentialCurrentIndex = 0; // 当前顺序检测的设备索引(0-based) - int m_sequentialTotalCount = 0; // 需要顺序检测的设备总数 - - // 继续检测下一个设备 - void continueSequentialDetection(); -}; - -#endif // WHEELMEASUREPRESENTER_H +#ifndef WHEELMEASUREPRESENTER_H +#define WHEELMEASUREPRESENTER_H + +#include "BasePresenter.h" +#include "IVrWheelMeasureConfig.h" +#include "IWheelMeasureStatus.h" +#include "CommonDialogCameraLevel.h" +#include "WheelMeasureTCPProtocol.h" + +#include +#include +#include +#include + +/** + * @brief 车轮拱高测量Presenter + * 继承BasePresenter,负责相机控制、算法调用和结果处理 + */ +class WheelMeasurePresenter : public BasePresenter, + public IVrWheelMeasureConfigChangeNotify, + public ICameraLevelCalculator, + public ICameraLevelResultSaver +{ + Q_OBJECT + +signals: + void configUpdated(); + +public: + explicit WheelMeasurePresenter(QObject* parent = nullptr); + ~WheelMeasurePresenter(); + + // ============ 实现 BasePresenter 纯虚函数 ============ + + /** + * @brief 私有初始化(实现纯虚函数) + */ + int InitApp() override; + + /** + * @brief 初始化算法参数(实现纯虚函数) + */ + int InitAlgoParams() override; + + /** + * @brief 执行算法检测(实现纯虚函数) + */ + int ProcessAlgoDetection(std::vector>& detectionDataCache) override; + + /** + * @brief 获取检测数据类型(实现纯虚函数) + */ + EVzResultDataType GetDetectionDataType() override { + return keResultDataType_Position; + } + + /** + * @brief 相机状态变化通知(实现纯虚函数) + */ + void OnCameraStatusChanged(int cameraIndex, bool isConnected) override; + + /** + * @brief 工作状态变化通知(重写虚函数) + */ + void OnWorkStatusChanged(WorkStatus status) override; + + /** + * @brief 相机数量变化通知(重写虚函数) + */ + void OnCameraCountChanged(int count) override; + + /** + * @brief 状态文字更新通知(重写虚函数) + */ + void OnStatusUpdate(const std::string& statusMessage) override; + + /** + * @brief Modbus写寄存器回调(重写虚函数) + */ + void OnModbusWriteCallback(uint16_t startAddress, const uint16_t* data, uint16_t count) override; + + // ============ 实现 ICameraLevelCalculator 接口 ============ + + bool CalculatePlaneCalibration( + const std::vector>& scanData, + double planeCalib[9], + double& planeHeight, + double invRMatrix[9]) override; + + // ============ 实现 ICameraLevelResultSaver 接口 ============ + + bool SaveLevelingResults(double planeCalib[9], double planeHeight, double invRMatrix[9], + int cameraIndex, const QString& cameraName) override; + + bool LoadLevelingResults(int cameraIndex, const QString& cameraName, + double planeCalib[9], double& planeHeight, double invRMatrix[9]) override; + + // ============ IVrWheelMeasureConfigChangeNotify 接口实现 ============ + + void OnConfigChanged(const WheelMeasureConfigResult& configResult) override; + + // ============ 公共接口 ============ + + /** + * @brief 设置状态更新接口 + */ + void setStatusUpdate(IWheelMeasureStatus* statusUpdate) { m_statusUpdate = statusUpdate; } + + /** + * @brief 获取配置接口 + */ + IVrWheelMeasureConfig* GetConfig() const { return m_config; } + + /** + * @brief 获取配置结果 + */ + WheelMeasureConfigResult* GetConfigResult() { return &m_configResult; } + + /** + * @brief 获取相机名称列表 + */ + QStringList getCameraNames() const; + + /** + * @brief 重新检测 + */ + void ResetDetect(int cameraIndex = 0); + + /** + * @brief 启动所有相机检测(依次执行) + */ + void StartAllDetection(); + + /** + * @brief 停止所有相机检测(完成当前设备后停止) + */ + void StopAllDetection(); + + /** + * @brief 检查是否正在进行顺序检测 + */ + bool IsSequentialDetecting() const { return m_sequentialDetecting; } + + /** + * @brief 设置默认相机索引 + */ + void SetDefaultCameraIndex(int cameraIndex) { m_currentCameraIndex = cameraIndex; } + + /** + * @brief 获取当前默认相机索引 + */ + int GetDefaultCameraIndex() const { return m_currentCameraIndex; } + + /** + * @brief 获取TCP协议对象 + */ + WheelMeasureTCPProtocol* GetTCPProtocol() { return &m_tcpProtocol; } + + /** + * @brief 静态相机状态回调函数 + */ + static void _StaticCameraNotify(EVzDeviceWorkStatus eStatus, void* pExtData, unsigned int nDataLength, void* pInfoParam); + +private: + // 初始化配置 + bool initializeConfig(const QString& configPath); + + // 初始化相机 + bool initializeCameras(); + + // 处理扫描数据 + void processScanData(std::vector>& detectionDataCache); + + // 根据相机索引获取调平参数 + WheelCameraPlaneCalibParam* getPlaneCalibParam(int cameraIndex); + + // 相机状态回调处理 + void _CameraNotify(EVzDeviceWorkStatus eStatus, void* pExtData, unsigned int nDataLength, void* pInfoParam); + + // TCP检测触发回调 + bool onTCPDetectionTriggered(int param); + + // 发送TCP测量结果 + void sendTCPMeasureResults(); + +private: + IVrWheelMeasureConfig* m_config = nullptr; + WheelMeasureConfigResult m_configResult; + IWheelMeasureStatus* m_statusUpdate = nullptr; + int m_currentCameraIndex = 1; // 默认相机索引(1-based) + + // 顺序检测相关 + bool m_sequentialDetecting = false; // 是否正在顺序检测所有设备 + bool m_stopSequentialRequested = false; // 是否请求停止顺序检测 + int m_sequentialCurrentIndex = 0; // 当前顺序检测的设备索引(0-based) + int m_sequentialTotalCount = 0; // 需要顺序检测的设备总数 + + // TCP协议 + WheelMeasureTCPProtocol m_tcpProtocol; // TCP服务器协议 + bool m_tcpDetectionMode = false; // 是否为TCP触发的检测 + QMap m_tcpResults; // TCP检测结果缓存 + + // 继续检测下一个设备 + void continueSequentialDetection(); +}; + +#endif // WHEELMEASUREPRESENTER_H diff --git a/App/WheelMeasure/WheelMeasureApp/Presenter/Inc/WheelMeasureTCPProtocol.h b/App/WheelMeasure/WheelMeasureApp/Presenter/Inc/WheelMeasureTCPProtocol.h new file mode 100644 index 0000000..1400214 --- /dev/null +++ b/App/WheelMeasure/WheelMeasureApp/Presenter/Inc/WheelMeasureTCPProtocol.h @@ -0,0 +1,125 @@ +#ifndef WHEELMEASURETCPPROTOCOL_H +#define WHEELMEASURETCPPROTOCOL_H + +#include +#include +#include +#include +#include +#include "IYTCPServer.h" + +/** + * @brief 车轮测量TCP服务器协议 + * + * 协议格式: + * - 接收:start,100 (start命令,100是参数) + * - 发送:1,100,200;2,100,200;3,100,200;4,100,200 + * - 格式:相机ID,中心点到地面距离,轮眉到地面距离 + * - 多个相机结果用分号分隔 + * - 错误码: + * - 400:扫描/匹配失败 + * - 401:工件为空 + * - 示例:1,400;2,100,200;3,100,200;4,100,200 (相机1失败) + */ +class WheelMeasureTCPProtocol +{ +public: + /** + * @brief 单个相机的测量结果 + */ + struct CameraMeasureResult { + int cameraId = 0; // 相机ID (1-4) + int errorCode = 0; // 错误码:0-成功,400-扫描失败,401-工件为空 + double centerDistance = 0; // 中心点到地面距离 + double archDistance = 0; // 轮眉到地面距离 + }; + + /** + * @brief 检测触发回调函数类型 + * @param param 从start命令解析的参数 + * @return true-成功触发检测,false-失败 + */ + using DetectionTriggerCallback = std::function; + +public: + WheelMeasureTCPProtocol(); + ~WheelMeasureTCPProtocol(); + + /** + * @brief 初始化TCP服务器 + * @param port TCP端口号 + * @return 0-成功,其他-错误码 + */ + int Initialize(uint16_t port = 6800); + + /** + * @brief 反初始化,停止服务 + */ + void Deinitialize(); + + /** + * @brief 发送测量结果给客户端 + * @param results 所有相机的测量结果 + * @return 0-成功,其他-错误码 + */ + int SendMeasureResults(const std::vector& results); + + /** + * @brief 设置检测触发回调 + * @param callback 回调函数 + */ + void SetDetectionTriggerCallback(const DetectionTriggerCallback& callback); + + /** + * @brief 获取服务运行状态 + * @return true-运行中,false-已停止 + */ + bool IsRunning() const { return m_bServerRunning; } + + /** + * @brief 获取当前连接的客户端数量 + * @return 客户端数量 + */ + int GetClientCount() const; + +private: + /** + * @brief TCP客户端连接事件处理 + * @param pClient 客户端对象 + * @param eventType 事件类型 + */ + void OnTCPEvent(const TCPClient* pClient, TCPServerEventType eventType); + + /** + * @brief TCP数据接收处理 + * @param pClient 客户端对象 + * @param pData 数据指针 + * @param nLen 数据长度 + */ + void OnTCPDataReceived(const TCPClient* pClient, const char* pData, unsigned int nLen); + + /** + * @brief 解析命令 + * @param pClient 客户端对象 + * @param command 命令字符串 + */ + void ParseCommand(const TCPClient* pClient, const QString& command); + + /** + * @brief 发送数据给指定客户端 + * @param pClient 客户端对象,nullptr表示发送给所有客户端 + * @param data 数据字符串 + * @return true-成功,false-失败 + */ + bool SendData(const TCPClient* pClient, const QString& data); + +private: + IYTCPServer* m_pTCPServer; // TCP服务器实例 + bool m_bServerRunning; // 服务器运行状态 + uint16_t m_nPort; // TCP端口 + DetectionTriggerCallback m_detectionCallback; // 检测触发回调 + QMutex m_mutex; // 线程安全锁 + const TCPClient* m_pCurrentClient; // 当前请求的客户端 +}; + +#endif // WHEELMEASURETCPPROTOCOL_H diff --git a/App/WheelMeasure/WheelMeasureApp/Presenter/Src/WheelMeasurePresenter.cpp b/App/WheelMeasure/WheelMeasureApp/Presenter/Src/WheelMeasurePresenter.cpp index 20fbaa3..fd90298 100644 --- a/App/WheelMeasure/WheelMeasureApp/Presenter/Src/WheelMeasurePresenter.cpp +++ b/App/WheelMeasure/WheelMeasureApp/Presenter/Src/WheelMeasurePresenter.cpp @@ -1,898 +1,1000 @@ -#include "WheelMeasurePresenter.h" -#include "PathManager.h" -#include "VrLog.h" -#include "VrError.h" - -// wheelArchHeigthMeasure SDK -#include "wheelArchHeigthMeasure_Export.h" - -// PointCloudImageUtils -#include "PointCloudImageUtils.h" - -#include -#include -#include -#include -#include - -WheelMeasurePresenter::WheelMeasurePresenter(QObject* parent) - : BasePresenter(parent) -{ - // 创建配置接口 - IVrWheelMeasureConfig::CreateInstance(&m_config); - LOG_INFO("ALGO_VERSION: %s \n", wd_wheelArchHeigthMeasureVersion()); -} - -WheelMeasurePresenter::~WheelMeasurePresenter() -{ - // 清除状态回调,防止后续回调访问已销毁对象 - m_statusUpdate = nullptr; - - // 停止顺序检测 - m_stopSequentialRequested = true; - m_sequentialDetecting = false; - - // 处理待处理的 Qt 事件,确保 QueuedConnection 的回调不会访问已销毁对象 - QCoreApplication::processEvents(); - - if (m_config) { - delete m_config; - m_config = nullptr; - } -} - -int WheelMeasurePresenter::InitApp() -{ - LOG_INFO("WheelMeasurePresenter::InitApp() called\n"); - - SetWorkStatus(WorkStatus::InitIng); - - // 加载配置 - QString configPath = PathManager::GetInstance().GetConfigFilePath(); - if (!initializeConfig(configPath)) { - LOG_ERROR("Failed to initialize config from: %s\n", configPath.toStdString().c_str()); - if (m_statusUpdate) { - m_statusUpdate->OnErrorOccurred("配置文件加载失败"); - } - return ERR_CODE(DEV_CONFIG_ERR); - } - - // 初始化相机 - if (!initializeCameras()) { - LOG_ERROR("Failed to initialize cameras\n"); - if (m_statusUpdate) { - m_statusUpdate->OnErrorOccurred("相机初始化失败"); - } - } - - LOG_INFO("WheelMeasurePresenter::InitApp() completed\n"); - return SUCCESS; -} - -int WheelMeasurePresenter::InitAlgoParams() -{ - LOG_DEBUG("Initializing algorithm parameters\n"); - // 算法参数已在配置加载时初始化 - return SUCCESS; -} - -int WheelMeasurePresenter::ProcessAlgoDetection(std::vector>& detectionDataCache) -{ - LOG_INFO("ProcessAlgoDetection called, data lines: %zu\n", detectionDataCache.size()); - - // 处理检测数据 - processScanData(detectionDataCache); - - return SUCCESS; -} - -void WheelMeasurePresenter::OnCameraStatusChanged(int cameraIndex, bool isConnected) -{ - LOG_INFO("Camera[%d] status changed: %s\n", cameraIndex, isConnected ? "connected" : "disconnected"); - - if (!m_statusUpdate) return; - - // 从配置中获取相机名称(cameraIndex从1开始) - QString cameraName; - int enabledIndex = 0; - for (const auto& cameraConfig : m_configResult.cameras) { - if (cameraConfig.enabled) { - enabledIndex++; - if (enabledIndex == cameraIndex) { - cameraName = QString::fromStdString(cameraConfig.name); - break; - } - } - } - if (cameraName.isEmpty()) { - cameraName = QString("Camera%1").arg(cameraIndex); - } - - // 切换到主线程更新UI - QMetaObject::invokeMethod(this, [this, cameraName, isConnected]() { - if (!m_statusUpdate) return; - if (isConnected) { - m_statusUpdate->OnCameraConnected(cameraName); - m_statusUpdate->OnDeviceStatusChanged(cameraName, static_cast(DeviceStatus::Online)); - } else { - m_statusUpdate->OnCameraDisconnected(cameraName); - m_statusUpdate->OnDeviceStatusChanged(cameraName, static_cast(DeviceStatus::Offline)); - } - }, Qt::QueuedConnection); -} - -void WheelMeasurePresenter::OnWorkStatusChanged(WorkStatus status) -{ - // 写入Modbus工作状态 (地址1) - uint16_t statusValue = 0; - switch (status) { - case WorkStatus::InitIng: statusValue = 1; break; - case WorkStatus::Ready: statusValue = 2; break; - case WorkStatus::Working: statusValue = 3; break; - case WorkStatus::Detecting: statusValue = 3; break; - case WorkStatus::Completed: statusValue = 4; break; - case WorkStatus::Error: statusValue = 5; break; - default: statusValue = 0; break; - } - WriteModbusRegisters(1, &statusValue, 1); - - // 切换到主线程更新UI - QMetaObject::invokeMethod(this, [this, status]() { - if (m_statusUpdate) { - m_statusUpdate->OnWorkStatusChanged(status); - } - }, Qt::QueuedConnection); -} - -void WheelMeasurePresenter::OnCameraCountChanged(int count) -{ - if (!m_statusUpdate) return; - - QStringList cameraNames; - for (const auto& cameraConfig : m_configResult.cameras) { - if (cameraConfig.enabled) { - cameraNames.append(QString::fromStdString(cameraConfig.name)); - } - } - LOG_INFO("OnCameraCountChanged: count=%d, cameraNames=%d\n", count, cameraNames.size()); - - // 切换到主线程更新UI - QMetaObject::invokeMethod(this, [this, cameraNames]() { - if (m_statusUpdate) { - m_statusUpdate->OnNeedShowImageCount(cameraNames); - } - }, Qt::QueuedConnection); -} - -void WheelMeasurePresenter::OnStatusUpdate(const std::string& statusMessage) -{ - if (!m_statusUpdate) return; - - QString msg = QString::fromStdString(statusMessage); - // 切换到主线程更新UI - QMetaObject::invokeMethod(this, [this, msg]() { - if (m_statusUpdate) { - m_statusUpdate->OnStatusUpdate(msg); - } - }, Qt::QueuedConnection); -} - -bool WheelMeasurePresenter::initializeConfig(const QString& configPath) -{ - if (!m_config) { - LOG_ERROR("Config interface is null\n"); - return false; - } - - m_configResult = m_config->LoadConfig(configPath.toStdString()); - - // 设置调试参数到基类 - SetDebugParam(m_configResult.debugParam); - - // 设置配置改变通知 - m_config->SetConfigChangeNotify(this); - - LOG_INFO("Config loaded successfully, cameras: %zu\n", m_configResult.cameras.size()); - return true; -} - -bool WheelMeasurePresenter::initializeCameras() -{ - // 转换相机配置为DeviceInfo列表 - std::vector cameraList; - for (const auto& cameraConfig : m_configResult.cameras) { - if (!cameraConfig.enabled) { - LOG_INFO("Camera %s is disabled, skipping\n", cameraConfig.name.c_str()); - continue; - } - - DeviceInfo deviceInfo; - deviceInfo.index = cameraConfig.cameraIndex; - deviceInfo.name = cameraConfig.name; - deviceInfo.ip = cameraConfig.cameraIP; - cameraList.push_back(deviceInfo); - } - - // 调用基类InitCamera进行相机初始化(bRGB=false, bSwing=true) - int result = InitCamera(cameraList, false, true); - - LOG_INFO("Camera initialization completed. Connected cameras: %zu\n", m_vrEyeDeviceList.size()); - - return result == SUCCESS; -} - -QStringList WheelMeasurePresenter::getCameraNames() const -{ - QStringList names; - for (const auto& camera : m_vrEyeDeviceList) { - names.append(QString::fromStdString(camera.first)); - } - return names; -} - -void WheelMeasurePresenter::ResetDetect(int cameraIndex) -{ - StopDetection(); - - // 设置当前相机索引(从0-based转换为1-based) - m_currentCameraIndex = cameraIndex + 1; - LOG_INFO("ResetDetect: cameraIndex=%d, m_currentCameraIndex=%d\n", cameraIndex, m_currentCameraIndex); - - // 设置工作状态为检测中 - SetWorkStatus(WorkStatus::Working); - - // 清空数据 - ClearDetectionDataCache(); - - // 注意:不调用 OnClearMeasureData(),因为 mainwindow 中的 onDeviceClicked - // 已经调用了 clearDeviceResult(deviceName) 来清除指定设备的结果 - - // 重新开始检测(BasePresenter::StartDetection 期望 1-based 索引) - StartDetection(m_currentCameraIndex); -} - -void WheelMeasurePresenter::StartAllDetection() -{ - LOG_INFO("Starting sequential detection for all cameras\n"); - - // 计算启用的相机数量 - m_sequentialTotalCount = 0; - for (const auto& cameraConfig : m_configResult.cameras) { - if (cameraConfig.enabled) { - m_sequentialTotalCount++; - } - } - - if (m_sequentialTotalCount == 0) { - LOG_WARNING("No enabled cameras to detect\n"); - if (m_statusUpdate) { - m_statusUpdate->OnStatusUpdate(QString("没有可用的相机设备")); - } - return; - } - - // 初始化顺序检测状态 - m_sequentialDetecting = true; - m_stopSequentialRequested = false; - m_sequentialCurrentIndex = 0; - - // 清空之前的检测结果 - if (m_statusUpdate) { - m_statusUpdate->OnClearMeasureData(); - } - - LOG_INFO("Sequential detection started, total cameras: %d\n", m_sequentialTotalCount); - - // 开始检测第一个设备 - continueSequentialDetection(); -} - -void WheelMeasurePresenter::StopAllDetection() -{ - LOG_INFO("Stop sequential detection requested\n"); - - if (m_sequentialDetecting) { - // 设置停止标志,等待当前设备检测完成后停止 - m_stopSequentialRequested = true; - if (m_statusUpdate) { - m_statusUpdate->OnStatusUpdate(QString("正在完成当前设备检测,之后将停止...")); - } - } else { - // 如果不是顺序检测模式,直接停止 - StopDetection(); - } -} - -void WheelMeasurePresenter::continueSequentialDetection() -{ - // 检查是否应该停止 - if (m_stopSequentialRequested) { - LOG_INFO("Sequential detection stopped by user request\n"); - m_sequentialDetecting = false; - m_stopSequentialRequested = false; - if (m_statusUpdate) { - m_statusUpdate->OnStatusUpdate(QString("顺序检测已停止")); - } - SetWorkStatus(WorkStatus::Ready); - return; - } - - // 检查是否还有设备需要检测 - if (m_sequentialCurrentIndex >= m_sequentialTotalCount) { - LOG_INFO("Sequential detection completed, all %d cameras processed\n", m_sequentialTotalCount); - m_sequentialDetecting = false; - if (m_statusUpdate) { - m_statusUpdate->OnStatusUpdate(QString("所有 %1 个设备检测完成").arg(m_sequentialTotalCount)); - } - SetWorkStatus(WorkStatus::Completed); - return; - } - - // 获取当前要检测的相机索引(1-based) - int cameraIndex = m_sequentialCurrentIndex + 1; - m_currentCameraIndex = cameraIndex; - - // 获取相机名称 - QString cameraName; - int enabledIndex = 0; - for (const auto& cameraConfig : m_configResult.cameras) { - if (cameraConfig.enabled) { - enabledIndex++; - if (enabledIndex == cameraIndex) { - cameraName = QString::fromStdString(cameraConfig.name); - break; - } - } - } - - LOG_INFO("Starting detection for camera %d/%d: %s\n", - m_sequentialCurrentIndex + 1, m_sequentialTotalCount, - cameraName.toStdString().c_str()); - - if (m_statusUpdate) { - m_statusUpdate->OnStatusUpdate(QString("正在检测设备 %1/%2: %3") - .arg(m_sequentialCurrentIndex + 1) - .arg(m_sequentialTotalCount) - .arg(cameraName)); - } - - // 清空数据缓存 - ClearDetectionDataCache(); - - // 开始检测当前相机 - StartDetection(cameraIndex); -} - -void WheelMeasurePresenter::OnConfigChanged(const WheelMeasureConfigResult& configResult) -{ - LOG_INFO("Config changed notification received\n"); - m_configResult = configResult; - - // 更新基类调试参数 - SetDebugParam(m_configResult.debugParam); - - emit configUpdated(); -} - -void WheelMeasurePresenter::processScanData(std::vector>& detectionDataCache) -{ - LOG_INFO("Processing scan data, lines: %zu\n", detectionDataCache.size()); - - // 打印扫描数据统计信息 - int totalPoints = 0; - for (const auto& linePair : detectionDataCache) { - totalPoints += linePair.second.nPointCount; - } - LOG_INFO("Scan data statistics: %zu lines, %d total points\n", detectionDataCache.size(), totalPoints); - - // 打印调平参数 - LOG_INFO("========== PlaneCalib Parameters ==========\n"); - if (!m_configResult.planeCalibParams.empty()) { - for (const auto& calibParam : m_configResult.planeCalibParams) { - LOG_INFO("Camera[%d] %s: isCalibrated=%d, planeHeight=%.2f\n", - calibParam.cameraIndex, calibParam.cameraName.c_str(), - calibParam.isCalibrated ? 1 : 0, calibParam.planeHeight); - LOG_INFO(" planeCalib: [%.6f, %.6f, %.6f, %.6f, %.6f, %.6f, %.6f, %.6f, %.6f]\n", - calibParam.planeCalib[0], calibParam.planeCalib[1], calibParam.planeCalib[2], - calibParam.planeCalib[3], calibParam.planeCalib[4], calibParam.planeCalib[5], - calibParam.planeCalib[6], calibParam.planeCalib[7], calibParam.planeCalib[8]); - LOG_INFO(" invRMatrix: [%.6f, %.6f, %.6f, %.6f, %.6f, %.6f, %.6f, %.6f, %.6f]\n", - calibParam.invRMatrix[0], calibParam.invRMatrix[1], calibParam.invRMatrix[2], - calibParam.invRMatrix[3], calibParam.invRMatrix[4], calibParam.invRMatrix[5], - calibParam.invRMatrix[6], calibParam.invRMatrix[7], calibParam.invRMatrix[8]); - } - } else { - LOG_INFO("No PlaneCalib parameters loaded!\n"); - } - - // 打印算法参数 - LOG_INFO("========== Algorithm Parameters ==========\n"); - LOG_INFO("CornerParam: minEndingGap=%.2f, minEndingGap_z=%.2f, scale=%.2f, cornerTh=%.2f, jumpCornerTh_1=%.2f, jumpCornerTh_2=%.2f\n", - m_configResult.algorithmParams.cornerParam.minEndingGap, - m_configResult.algorithmParams.cornerParam.minEndingGap_z, - m_configResult.algorithmParams.cornerParam.scale, - m_configResult.algorithmParams.cornerParam.cornerTh, - m_configResult.algorithmParams.cornerParam.jumpCornerTh_1, - m_configResult.algorithmParams.cornerParam.jumpCornerTh_2); - - LOG_INFO("LineSegParam: segGapTh_y=%.2f, segGapTh_z=%.2f, maxDist=%.2f\n", - m_configResult.algorithmParams.lineSegParam.segGapTh_y, - m_configResult.algorithmParams.lineSegParam.segGapTh_z, - m_configResult.algorithmParams.lineSegParam.maxDist); - - LOG_INFO("OutlierFilterParam: continuityTh=%.2f, outlierTh=%.2f\n", - m_configResult.algorithmParams.filterParam.continuityTh, - m_configResult.algorithmParams.filterParam.outlierTh); - - LOG_INFO("TreeGrowParam: yDeviation_max=%.2f, zDeviation_max=%.2f, maxLineSkipNum=%d, maxSkipDistance=%.2f, minLTypeTreeLen=%.2f, minVTypeTreeLen=%.2f\n", - m_configResult.algorithmParams.growParam.yDeviation_max, - m_configResult.algorithmParams.growParam.zDeviation_max, - m_configResult.algorithmParams.growParam.maxLineSkipNum, - m_configResult.algorithmParams.growParam.maxSkipDistance, - m_configResult.algorithmParams.growParam.minLTypeTreeLen, - m_configResult.algorithmParams.growParam.minVTypeTreeLen); - - LOG_INFO("DebugParam: enableDebug=%d, saveDebugImage=%d, printDetailLog=%d\n", - m_configResult.debugParam.enableDebug, - m_configResult.debugParam.saveDebugImage, - m_configResult.debugParam.printDetailLog); - - LOG_INFO("============================================\n"); - - // ========== 调用 wheelArchHeigthMeasure SDK ========== - - // 1. 转换检测数据为SDK所需格式 - std::vector> scanLines; - size_t validPointCount = 0; - int convertResult = m_dataLoader.ConvertToSVzNL3DPosition(detectionDataCache, scanLines, &validPointCount); - - // 检查数据有效性 - if (convertResult != SUCCESS || scanLines.empty()) { - LOG_WARNING("Failed to convert data to XYZ format or no XYZ data available\n"); - return; - } - - // 检查线束数量和有效点数量 - const size_t MIN_SCAN_LINES = 200; - const size_t MIN_VALID_POINTS = 1000; - - if (scanLines.size() < MIN_SCAN_LINES || validPointCount < MIN_VALID_POINTS) { - LOG_ERROR("Insufficient data for detection: scan lines=%zu (min=%zu), valid points=%zu (min=%zu)\n", - scanLines.size(), MIN_SCAN_LINES, validPointCount, MIN_VALID_POINTS); - - if (m_statusUpdate) { - QString errorMsg = QString("数据不足: 线束=%1(最少%2), 有效点=%3(最少%4)") - .arg(scanLines.size()).arg(MIN_SCAN_LINES) - .arg(validPointCount).arg(MIN_VALID_POINTS); - m_statusUpdate->OnStatusUpdate(errorMsg); - m_statusUpdate->OnErrorOccurred(errorMsg); - } - - SetWorkStatus(WorkStatus::Error); - - // 如果正在进行顺序检测,继续检测下一个设备 - if (m_sequentialDetecting) { - m_sequentialCurrentIndex++; - QMetaObject::invokeMethod(this, [this]() { - continueSequentialDetection(); - }, Qt::QueuedConnection); - } - return; - } - - LOG_INFO("Data validation passed: scan lines=%zu, valid points=%zu\n", scanLines.size(), validPointCount); - - // 2. 准备算法参数 - SSG_cornerParam cornerParam; - cornerParam.minEndingGap = m_configResult.algorithmParams.cornerParam.minEndingGap; - cornerParam.minEndingGap_z = m_configResult.algorithmParams.cornerParam.minEndingGap_z; - cornerParam.scale = m_configResult.algorithmParams.cornerParam.scale; - cornerParam.cornerTh = m_configResult.algorithmParams.cornerParam.cornerTh; - cornerParam.jumpCornerTh_1 = m_configResult.algorithmParams.cornerParam.jumpCornerTh_1; - cornerParam.jumpCornerTh_2 = m_configResult.algorithmParams.cornerParam.jumpCornerTh_2; - - SSG_lineSegParam lineSegParam; - lineSegParam.segGapTh_y = m_configResult.algorithmParams.lineSegParam.segGapTh_y; - lineSegParam.segGapTh_z = m_configResult.algorithmParams.lineSegParam.segGapTh_z; - lineSegParam.maxDist = m_configResult.algorithmParams.lineSegParam.maxDist; - - SSG_outlierFilterParam filterParam; - filterParam.continuityTh = m_configResult.algorithmParams.filterParam.continuityTh; - filterParam.outlierTh = m_configResult.algorithmParams.filterParam.outlierTh; - - SSG_treeGrowParam growParam; - growParam.yDeviation_max = m_configResult.algorithmParams.growParam.yDeviation_max; - growParam.zDeviation_max = m_configResult.algorithmParams.growParam.zDeviation_max; - growParam.maxLineSkipNum = m_configResult.algorithmParams.growParam.maxLineSkipNum; - growParam.maxSkipDistance = m_configResult.algorithmParams.growParam.maxSkipDistance; - growParam.minLTypeTreeLen = m_configResult.algorithmParams.growParam.minLTypeTreeLen; - growParam.minVTypeTreeLen = m_configResult.algorithmParams.growParam.minVTypeTreeLen; - - // 3. 准备调平参数(使用当前相机的调平参数) - SSG_planeCalibPara groundCalibPara; - memset(&groundCalibPara, 0, sizeof(groundCalibPara)); - - // 初始化为单位矩阵 - groundCalibPara.planeCalib[0] = 1.0; - groundCalibPara.planeCalib[4] = 1.0; - groundCalibPara.planeCalib[8] = 1.0; - groundCalibPara.invRMatrix[0] = 1.0; - groundCalibPara.invRMatrix[4] = 1.0; - groundCalibPara.invRMatrix[8] = 1.0; - groundCalibPara.planeHeight = 0.0; - - // 查找当前相机的调平参数 - WheelCameraPlaneCalibParam* calibParam = getPlaneCalibParam(m_currentCameraIndex); - if (calibParam && calibParam->isCalibrated) { - for (int i = 0; i < 9; ++i) { - groundCalibPara.planeCalib[i] = calibParam->planeCalib[i]; - groundCalibPara.invRMatrix[i] = calibParam->invRMatrix[i]; - } - groundCalibPara.planeHeight = calibParam->planeHeight; - LOG_INFO("Using calibrated plane parameters for camera %d\n", m_currentCameraIndex); - LOG_INFO(" planeHeight: %.3f, errorCompensation: %.2f\n", calibParam->planeHeight, calibParam->errorCompensation); - - if(calibParam){ - // 计算调平使用的地面高度(加上该相机的误差补偿) - double adjustedPlaneHeight = groundCalibPara.planeHeight + calibParam->errorCompensation; - LOG_INFO(" adjustedPlaneHeight (with compensation): %.3f\n", adjustedPlaneHeight); - - for(size_t i = 0; i < scanLines.size(); i++){ - wd_horizonCamera_lineDataR(scanLines[i], calibParam->planeCalib, adjustedPlaneHeight); - } - } - } else { - LOG_WARN("No calibration data for camera %d, using default parameters\n", m_currentCameraIndex); - } - - - // 4. 调用算法 - int errCode = 0; - LOG_INFO("Calling wd_wheelArchHeigthMeasure...\n"); - - WD_wheelArchInfo wheelArchResult = wd_wheelArchHeigthMeasure( - scanLines, - cornerParam, - lineSegParam, - filterParam, - growParam, - groundCalibPara, - &errCode); - - LOG_INFO("Algorithm completed with errCode=%d\n", errCode); - - // 5. 处理算法结果 - WheelMeasureResult result; - - // 从配置中获取当前相机名称 - QString cameraName; - int enabledIndex = 0; - for (const auto& cameraConfig : m_configResult.cameras) { - if (cameraConfig.enabled) { - enabledIndex++; - if (enabledIndex == m_currentCameraIndex) { - cameraName = QString::fromStdString(cameraConfig.name); - break; - } - } - } - if (cameraName.isEmpty() && !m_vrEyeDeviceList.empty()) { - cameraName = QString::fromStdString(m_vrEyeDeviceList[0].first); - } - if (cameraName.isEmpty()) { - cameraName = QString("Camera%1").arg(m_currentCameraIndex); - } - - result.cameraName = cameraName; - result.aliasName = cameraName; - result.bResultValid = (errCode == 0); - - if (errCode == 0) { - LOG_INFO("========== Wheel Arch Measurement Result ==========\n"); - LOG_INFO("wheelArchPos: (%.3f, %.3f, %.3f)\n", - wheelArchResult.wheelArchPos.x, - wheelArchResult.wheelArchPos.y, - wheelArchResult.wheelArchPos.z); - LOG_INFO("wheelUpPos: (%.3f, %.3f, %.3f)\n", - wheelArchResult.wheelUpPos.x, - wheelArchResult.wheelUpPos.y, - wheelArchResult.wheelUpPos.z); - LOG_INFO("wheelDownPos: (%.3f, %.3f, %.3f)\n", - wheelArchResult.wheelDownPos.x, - wheelArchResult.wheelDownPos.y, - wheelArchResult.wheelDownPos.z); - LOG_INFO("archToCenterHeigth: %.3f archToGroundHeigth: %.3f mm\n", wheelArchResult.archToCenterHeigth, wheelArchResult.archToGroundHeigth); - LOG_INFO("==================================================\n"); - - if (m_statusUpdate) { - QString statusMsg = QString("轮眉高度: %1 mm, 到地面高度: %2 mm").arg(wheelArchResult.archToCenterHeigth).arg(wheelArchResult.archToGroundHeigth); - m_statusUpdate->OnStatusUpdate(statusMsg); - } - - // 填充测量结果数据 - WheelMeasureData measureData; - measureData.archToCenterHeight = wheelArchResult.archToCenterHeigth; - measureData.archToGroundHeight = wheelArchResult.archToGroundHeigth; - measureData.wheelArchPosX = wheelArchResult.wheelArchPos.x; - measureData.wheelArchPosY = wheelArchResult.wheelArchPos.y; - measureData.wheelArchPosZ = wheelArchResult.wheelArchPos.z; - measureData.wheelUpPosX = wheelArchResult.wheelUpPos.x; - measureData.wheelUpPosY = wheelArchResult.wheelUpPos.y; - measureData.wheelUpPosZ = wheelArchResult.wheelUpPos.z; - measureData.wheelDownPosX = wheelArchResult.wheelDownPos.x; - measureData.wheelDownPosY = wheelArchResult.wheelDownPos.y; - measureData.wheelDownPosZ = wheelArchResult.wheelDownPos.z; - measureData.timestamp = QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss"); - result.result.push_back(measureData); - - // 使用 PointCloudImageUtils 生成带检测结果的图像 - result.image = PointCloudImageUtils::GenerateWheelArchImage( - scanLines, - wheelArchResult.wheelArchPos, - wheelArchResult.wheelUpPos, - wheelArchResult.wheelDownPos, - wheelArchResult.archToCenterHeigth, - true); - } else { - LOG_ERROR("Algorithm failed with errCode=%d\n", errCode); - if (m_statusUpdate) { - m_statusUpdate->OnStatusUpdate(QString("算法检测失败,错误码: %1").arg(errCode)); - } - - // 生成仅有点云的图像(无检测结果) - SVzNL3DPoint emptyPoint = {0.0, 0.0, 0.0}; - result.image = PointCloudImageUtils::GenerateWheelArchImage( - scanLines, - emptyPoint, - emptyPoint, - emptyPoint, - 0.0, - false); - } - - result.bImageValid = !result.image.isNull(); - - // ========== 写入检测结果到 Modbus (地址2-25) ========== - // 辅助lambda: float转两个uint16_t (大端模式) - auto floatToUint16 = [](float value, uint16_t& high, uint16_t& low) { - uint32_t bits; - memcpy(&bits, &value, sizeof(float)); - high = static_cast((bits >> 16) & 0xFFFF); - low = static_cast(bits & 0xFFFF); - }; - - uint16_t modbusData[24]; - memset(modbusData, 0, sizeof(modbusData)); - - // 地址2: 设备序号 - modbusData[0] = static_cast(m_currentCameraIndex); - // 地址3: 结果有效 - modbusData[1] = (errCode == 0) ? 1 : 0; - - if (errCode == 0) { - // 地址4-5: 轮眉高度(到中心) - floatToUint16(static_cast(wheelArchResult.archToCenterHeigth), modbusData[2], modbusData[3]); - // 地址6-7: 轮眉X - floatToUint16(static_cast(wheelArchResult.wheelArchPos.x), modbusData[4], modbusData[5]); - // 地址8-9: 轮眉Y - floatToUint16(static_cast(wheelArchResult.wheelArchPos.y), modbusData[6], modbusData[7]); - // 地址10-11: 轮眉Z - floatToUint16(static_cast(wheelArchResult.wheelArchPos.z), modbusData[8], modbusData[9]); - // 地址12-13: 上点X - floatToUint16(static_cast(wheelArchResult.wheelUpPos.x), modbusData[10], modbusData[11]); - // 地址14-15: 上点Y - floatToUint16(static_cast(wheelArchResult.wheelUpPos.y), modbusData[12], modbusData[13]); - // 地址16-17: 上点Z - floatToUint16(static_cast(wheelArchResult.wheelUpPos.z), modbusData[14], modbusData[15]); - // 地址18-19: 下点X - floatToUint16(static_cast(wheelArchResult.wheelDownPos.x), modbusData[16], modbusData[17]); - // 地址20-21: 下点Y - floatToUint16(static_cast(wheelArchResult.wheelDownPos.y), modbusData[18], modbusData[19]); - // 地址22-23: 下点Z - floatToUint16(static_cast(wheelArchResult.wheelDownPos.z), modbusData[20], modbusData[21]); - // 地址24-25: 到地面高度 - floatToUint16(static_cast(wheelArchResult.archToGroundHeigth), modbusData[22], modbusData[23]); - } - - WriteModbusRegisters(2, modbusData, 24); - LOG_INFO("Modbus: 写入检测结果到地址2-25, 设备=%d, 有效=%d\n", m_currentCameraIndex, modbusData[1]); - - // 检测完成后清零"检测控制"(地址0) - uint16_t zero = 0; - WriteModbusRegisters(0, &zero, 1); - - // 直接调用回调,不使用信号槽 - if (m_statusUpdate) { - m_statusUpdate->OnMeasureResult(result); - } - - SetWorkStatus(WorkStatus::Completed); - - // 如果正在进行顺序检测,继续检测下一个设备 - if (m_sequentialDetecting) { - m_sequentialCurrentIndex++; - QMetaObject::invokeMethod(this, [this]() { - continueSequentialDetection(); - }, Qt::QueuedConnection); - } -} - -WheelCameraPlaneCalibParam* WheelMeasurePresenter::getPlaneCalibParam(int cameraIndex) -{ - for (auto& param : m_configResult.planeCalibParams) { - if (param.cameraIndex == cameraIndex) { - return ¶m; - } - } - return nullptr; -} - -// ============ ICameraLevelCalculator 接口实现 ============ - -bool WheelMeasurePresenter::CalculatePlaneCalibration( - const std::vector>& scanData, - double planeCalib[9], - double& planeHeight, - double invRMatrix[9]) -{ - LOG_INFO("CalculatePlaneCalibration called, scan lines: %zu\n", scanData.size()); - - // TODO: 调用调平算法库计算调平参数 - // 暂时返回单位矩阵 - planeCalib[0] = 1.0; planeCalib[1] = 0.0; planeCalib[2] = 0.0; - planeCalib[3] = 0.0; planeCalib[4] = 1.0; planeCalib[5] = 0.0; - planeCalib[6] = 0.0; planeCalib[7] = 0.0; planeCalib[8] = 1.0; - - invRMatrix[0] = 1.0; invRMatrix[1] = 0.0; invRMatrix[2] = 0.0; - invRMatrix[3] = 0.0; invRMatrix[4] = 1.0; invRMatrix[5] = 0.0; - invRMatrix[6] = 0.0; invRMatrix[7] = 0.0; invRMatrix[8] = 1.0; - - planeHeight = 0.0; - - return true; -} - -// ============ ICameraLevelResultSaver 接口实现 ============ - -bool WheelMeasurePresenter::SaveLevelingResults(double planeCalib[9], double planeHeight, double invRMatrix[9], - int cameraIndex, const QString& cameraName) -{ - LOG_INFO("SaveLevelingResults: Camera[%d] %s, planeHeight=%.2f\n", - cameraIndex, cameraName.toStdString().c_str(), planeHeight); - - // 查找或创建调平参数 - WheelCameraPlaneCalibParam* param = getPlaneCalibParam(cameraIndex); - if (!param) { - WheelCameraPlaneCalibParam newParam; - newParam.cameraIndex = cameraIndex; - newParam.cameraName = cameraName.toStdString(); - m_configResult.planeCalibParams.push_back(newParam); - param = &m_configResult.planeCalibParams.back(); - } - - // 更新调平参数 - param->isCalibrated = true; - param->planeHeight = planeHeight; - for (int i = 0; i < 9; ++i) { - param->planeCalib[i] = planeCalib[i]; - param->invRMatrix[i] = invRMatrix[i]; - } - - // 保存配置到文件 - QString configPath = PathManager::GetInstance().GetConfigFilePath(); - bool success = m_config->SaveConfig(configPath.toStdString(), m_configResult); - - if (success) { - LOG_INFO("Leveling results saved successfully\n"); - } else { - LOG_ERROR("Failed to save leveling results\n"); - } - - return success; -} - -bool WheelMeasurePresenter::LoadLevelingResults(int cameraIndex, const QString& cameraName, - double planeCalib[9], double& planeHeight, double invRMatrix[9]) -{ - LOG_INFO("LoadLevelingResults: Camera[%d] %s\n", cameraIndex, cameraName.toStdString().c_str()); - - WheelCameraPlaneCalibParam* param = getPlaneCalibParam(cameraIndex); - if (!param || !param->isCalibrated) { - LOG_WARN("No calibration data found for camera %d\n", cameraIndex); - return false; - } - - planeHeight = param->planeHeight; - for (int i = 0; i < 9; ++i) { - planeCalib[i] = param->planeCalib[i]; - invRMatrix[i] = param->invRMatrix[i]; - } - - LOG_INFO("Leveling results loaded: planeHeight=%.2f\n", planeHeight); - return true; -} - -// ============ 静态相机状态回调函数 ============ - -void WheelMeasurePresenter::_StaticCameraNotify(EVzDeviceWorkStatus eStatus, void* pExtData, unsigned int nDataLength, void* pInfoParam) -{ - // 从pInfoParam获取this指针,转换回WheelMeasurePresenter*类型 - WheelMeasurePresenter* pThis = reinterpret_cast(pInfoParam); - if (pThis) { - // 调用实例的非静态成员函数 - pThis->_CameraNotify(eStatus, pExtData, nDataLength, pInfoParam); - } -} - -void WheelMeasurePresenter::_CameraNotify(EVzDeviceWorkStatus eStatus, void* pExtData, unsigned int nDataLength, void* pInfoParam) -{ - LOG_DEBUG("[Camera Notify] received: status=%d\n", (int)eStatus); - - switch (eStatus) { - case EVzDeviceWorkStatus::keDeviceWorkStatus_Offline: - { - LOG_WARNING("[Camera Notify] Camera device offline/disconnected\n"); - - // 通知UI相机状态变更 - if (m_statusUpdate) { - m_statusUpdate->OnCameraDisconnected(QString("Camera")); - m_statusUpdate->OnStatusUpdate(QString("相机设备离线")); - } - break; - } - - case EVzDeviceWorkStatus::keDeviceWorkStatus_Eye_Reconnect: - { - LOG_INFO("[Camera Notify] Camera device reconnecting\n"); - - if (m_statusUpdate) { - m_statusUpdate->OnStatusUpdate(QString("相机设备重连中...")); - } - break; - } - - case EVzDeviceWorkStatus::keDeviceWorkStatus_Eye_Comming: - { - LOG_INFO("[Camera Notify] Camera device connected\n"); - - if (m_statusUpdate) { - m_statusUpdate->OnCameraConnected(QString("Camera")); - m_statusUpdate->OnStatusUpdate(QString("相机设备已连接")); - } - break; - } - - default: - LOG_DEBUG("[Camera Notify] Unhandled status: %d\n", (int)eStatus); - break; - } -} - -// ============ Modbus写寄存器回调处理 ============ - -void WheelMeasurePresenter::OnModbusWriteCallback(uint16_t startAddress, const uint16_t* data, uint16_t count) -{ - LOG_INFO("OnModbusWriteCallback: address=%d, count=%d\n", startAddress, count); - - if (!data || count == 0) { - return; - } - - // 地址0: 写1-4直接开始检测对应设备 - if (startAddress == 0) { - int deviceIndex = data[0]; - - // 检查设备索引是否有效 (1-4) - if (deviceIndex >= 1 && deviceIndex <= 4 && - deviceIndex <= static_cast(m_vrEyeDeviceList.size())) { - LOG_INFO("Modbus: 开始检测设备 %d\n", deviceIndex); - // 使用 QMetaObject::invokeMethod 在主线程执行 - QMetaObject::invokeMethod(this, [this, deviceIndex]() { - ResetDetect(deviceIndex - 1); // ResetDetect 期望 0-based 索引 - }, Qt::QueuedConnection); - } else { - LOG_WARNING("Modbus: 无效的设备索引: %d (有效范围: 1-%d)\n", - deviceIndex, static_cast(m_vrEyeDeviceList.size())); - } - } -} +#include "WheelMeasurePresenter.h" +#include "PathManager.h" +#include "VrLog.h" +#include "VrError.h" + +// wheelArchHeigthMeasure SDK +#include "wheelArchHeigthMeasure_Export.h" + +// PointCloudImageUtils +#include "PointCloudImageUtils.h" + +#include +#include +#include +#include +#include + +WheelMeasurePresenter::WheelMeasurePresenter(QObject* parent) + : BasePresenter(parent) +{ + // 创建配置接口 + IVrWheelMeasureConfig::CreateInstance(&m_config); + LOG_INFO("ALGO_VERSION: %s \n", wd_wheelArchHeigthMeasureVersion()); + + // 设置TCP检测触发回调 + m_tcpProtocol.SetDetectionTriggerCallback([this](int param) { + return this->onTCPDetectionTriggered(param); + }); +} + +WheelMeasurePresenter::~WheelMeasurePresenter() +{ + // 停止TCP服务器 + m_tcpProtocol.Deinitialize(); + + // 清除状态回调,防止后续回调访问已销毁对象 + m_statusUpdate = nullptr; + + // 停止顺序检测 + m_stopSequentialRequested = true; + m_sequentialDetecting = false; + + // 处理待处理的 Qt 事件,确保 QueuedConnection 的回调不会访问已销毁对象 + QCoreApplication::processEvents(); + + if (m_config) { + delete m_config; + m_config = nullptr; + } +} + +int WheelMeasurePresenter::InitApp() +{ + LOG_INFO("WheelMeasurePresenter::InitApp() called\n"); + + SetWorkStatus(WorkStatus::InitIng); + + // 加载配置 + QString configPath = PathManager::GetInstance().GetConfigFilePath(); + if (!initializeConfig(configPath)) { + LOG_ERROR("Failed to initialize config from: %s\n", configPath.toStdString().c_str()); + if (m_statusUpdate) { + m_statusUpdate->OnErrorOccurred("配置文件加载失败"); + } + return ERR_CODE(DEV_CONFIG_ERR); + } + + // 初始化相机 + if (!initializeCameras()) { + LOG_ERROR("Failed to initialize cameras\n"); + if (m_statusUpdate) { + m_statusUpdate->OnErrorOccurred("相机初始化失败"); + } + } + + // 初始化TCP服务器 + int tcpPort = 6800; // 默认端口 + int tcpResult = m_tcpProtocol.Initialize(tcpPort); + if (tcpResult != 0) { + LOG_ERROR("Failed to initialize TCP server on port %d, error code: %d\n", tcpPort, tcpResult); + if (m_statusUpdate) { + m_statusUpdate->OnStatusUpdate(QString("TCP服务器启动失败,端口: %1").arg(tcpPort)); + } + } else { + LOG_INFO("TCP server initialized successfully on port %d\n", tcpPort); + if (m_statusUpdate) { + m_statusUpdate->OnStatusUpdate(QString("TCP服务器已启动,端口: %1").arg(tcpPort)); + } + } + + LOG_INFO("WheelMeasurePresenter::InitApp() completed\n"); + return SUCCESS; +} + +int WheelMeasurePresenter::InitAlgoParams() +{ + LOG_DEBUG("Initializing algorithm parameters\n"); + // 算法参数已在配置加载时初始化 + return SUCCESS; +} + +int WheelMeasurePresenter::ProcessAlgoDetection(std::vector>& detectionDataCache) +{ + LOG_INFO("ProcessAlgoDetection called, data lines: %zu\n", detectionDataCache.size()); + + // 处理检测数据 + processScanData(detectionDataCache); + + return SUCCESS; +} + +void WheelMeasurePresenter::OnCameraStatusChanged(int cameraIndex, bool isConnected) +{ + LOG_INFO("Camera[%d] status changed: %s\n", cameraIndex, isConnected ? "connected" : "disconnected"); + + if (!m_statusUpdate) return; + + // 从配置中获取相机名称(cameraIndex从1开始) + QString cameraName; + int enabledIndex = 0; + for (const auto& cameraConfig : m_configResult.cameras) { + if (cameraConfig.enabled) { + enabledIndex++; + if (enabledIndex == cameraIndex) { + cameraName = QString::fromStdString(cameraConfig.name); + break; + } + } + } + if (cameraName.isEmpty()) { + cameraName = QString("Camera%1").arg(cameraIndex); + } + + // 切换到主线程更新UI + QMetaObject::invokeMethod(this, [this, cameraName, isConnected]() { + if (!m_statusUpdate) return; + if (isConnected) { + m_statusUpdate->OnCameraConnected(cameraName); + m_statusUpdate->OnDeviceStatusChanged(cameraName, static_cast(DeviceStatus::Online)); + } else { + m_statusUpdate->OnCameraDisconnected(cameraName); + m_statusUpdate->OnDeviceStatusChanged(cameraName, static_cast(DeviceStatus::Offline)); + } + }, Qt::QueuedConnection); +} + +void WheelMeasurePresenter::OnWorkStatusChanged(WorkStatus status) +{ + // 写入Modbus工作状态 (地址1) + uint16_t statusValue = 0; + switch (status) { + case WorkStatus::InitIng: statusValue = 1; break; + case WorkStatus::Ready: statusValue = 2; break; + case WorkStatus::Working: statusValue = 3; break; + case WorkStatus::Detecting: statusValue = 3; break; + case WorkStatus::Completed: statusValue = 4; break; + case WorkStatus::Error: statusValue = 5; break; + default: statusValue = 0; break; + } + WriteModbusRegisters(1, &statusValue, 1); + + // 切换到主线程更新UI + QMetaObject::invokeMethod(this, [this, status]() { + if (m_statusUpdate) { + m_statusUpdate->OnWorkStatusChanged(status); + } + }, Qt::QueuedConnection); +} + +void WheelMeasurePresenter::OnCameraCountChanged(int count) +{ + if (!m_statusUpdate) return; + + QStringList cameraNames; + for (const auto& cameraConfig : m_configResult.cameras) { + if (cameraConfig.enabled) { + cameraNames.append(QString::fromStdString(cameraConfig.name)); + } + } + LOG_INFO("OnCameraCountChanged: count=%d, cameraNames=%d\n", count, cameraNames.size()); + + // 切换到主线程更新UI + QMetaObject::invokeMethod(this, [this, cameraNames]() { + if (m_statusUpdate) { + m_statusUpdate->OnNeedShowImageCount(cameraNames); + } + }, Qt::QueuedConnection); +} + +void WheelMeasurePresenter::OnStatusUpdate(const std::string& statusMessage) +{ + if (!m_statusUpdate) return; + + QString msg = QString::fromStdString(statusMessage); + // 切换到主线程更新UI + QMetaObject::invokeMethod(this, [this, msg]() { + if (m_statusUpdate) { + m_statusUpdate->OnStatusUpdate(msg); + } + }, Qt::QueuedConnection); +} + +bool WheelMeasurePresenter::initializeConfig(const QString& configPath) +{ + if (!m_config) { + LOG_ERROR("Config interface is null\n"); + return false; + } + + m_configResult = m_config->LoadConfig(configPath.toStdString()); + + // 设置调试参数到基类 + SetDebugParam(m_configResult.debugParam); + + // 设置配置改变通知 + m_config->SetConfigChangeNotify(this); + + LOG_INFO("Config loaded successfully, cameras: %zu\n", m_configResult.cameras.size()); + return true; +} + +bool WheelMeasurePresenter::initializeCameras() +{ + // 转换相机配置为DeviceInfo列表 + std::vector cameraList; + for (const auto& cameraConfig : m_configResult.cameras) { + if (!cameraConfig.enabled) { + LOG_INFO("Camera %s is disabled, skipping\n", cameraConfig.name.c_str()); + continue; + } + + DeviceInfo deviceInfo; + deviceInfo.index = cameraConfig.cameraIndex; + deviceInfo.name = cameraConfig.name; + deviceInfo.ip = cameraConfig.cameraIP; + cameraList.push_back(deviceInfo); + } + + // 调用基类InitCamera进行相机初始化(bRGB=false, bSwing=true) + int result = InitCamera(cameraList, false, true); + + LOG_INFO("Camera initialization completed. Connected cameras: %zu\n", m_vrEyeDeviceList.size()); + + return result == SUCCESS; +} + +QStringList WheelMeasurePresenter::getCameraNames() const +{ + QStringList names; + for (const auto& camera : m_vrEyeDeviceList) { + names.append(QString::fromStdString(camera.first)); + } + return names; +} + +void WheelMeasurePresenter::ResetDetect(int cameraIndex) +{ + StopDetection(); + + // 设置当前相机索引(从0-based转换为1-based) + m_currentCameraIndex = cameraIndex + 1; + LOG_INFO("ResetDetect: cameraIndex=%d, m_currentCameraIndex=%d\n", cameraIndex, m_currentCameraIndex); + + // 设置工作状态为检测中 + SetWorkStatus(WorkStatus::Working); + + // 清空数据 + ClearDetectionDataCache(); + + // 注意:不调用 OnClearMeasureData(),因为 mainwindow 中的 onDeviceClicked + // 已经调用了 clearDeviceResult(deviceName) 来清除指定设备的结果 + + // 重新开始检测(BasePresenter::StartDetection 期望 1-based 索引) + StartDetection(m_currentCameraIndex); +} + +void WheelMeasurePresenter::StartAllDetection() +{ + LOG_INFO("Starting sequential detection for all cameras\n"); + + // 计算启用的相机数量 + m_sequentialTotalCount = 0; + for (const auto& cameraConfig : m_configResult.cameras) { + if (cameraConfig.enabled) { + m_sequentialTotalCount++; + } + } + + if (m_sequentialTotalCount == 0) { + LOG_WARNING("No enabled cameras to detect\n"); + if (m_statusUpdate) { + m_statusUpdate->OnStatusUpdate(QString("没有可用的相机设备")); + } + return; + } + + // 初始化顺序检测状态 + m_sequentialDetecting = true; + m_stopSequentialRequested = false; + m_sequentialCurrentIndex = 0; + + // 清空之前的检测结果 + if (m_statusUpdate) { + m_statusUpdate->OnClearMeasureData(); + } + + LOG_INFO("Sequential detection started, total cameras: %d\n", m_sequentialTotalCount); + + // 开始检测第一个设备 + continueSequentialDetection(); +} + +void WheelMeasurePresenter::StopAllDetection() +{ + LOG_INFO("Stop sequential detection requested\n"); + + if (m_sequentialDetecting) { + // 设置停止标志,等待当前设备检测完成后停止 + m_stopSequentialRequested = true; + if (m_statusUpdate) { + m_statusUpdate->OnStatusUpdate(QString("正在完成当前设备检测,之后将停止...")); + } + } else { + // 如果不是顺序检测模式,直接停止 + StopDetection(); + } +} + +void WheelMeasurePresenter::continueSequentialDetection() +{ + // 检查是否应该停止 + if (m_stopSequentialRequested) { + LOG_INFO("Sequential detection stopped by user request\n"); + m_sequentialDetecting = false; + m_stopSequentialRequested = false; + if (m_statusUpdate) { + m_statusUpdate->OnStatusUpdate(QString("顺序检测已停止")); + } + SetWorkStatus(WorkStatus::Ready); + return; + } + + // 检查是否还有设备需要检测 + if (m_sequentialCurrentIndex >= m_sequentialTotalCount) { + LOG_INFO("Sequential detection completed, all %d cameras processed\n", m_sequentialTotalCount); + m_sequentialDetecting = false; + if (m_statusUpdate) { + m_statusUpdate->OnStatusUpdate(QString("所有 %1 个设备检测完成").arg(m_sequentialTotalCount)); + } + SetWorkStatus(WorkStatus::Completed); + return; + } + + // 获取当前要检测的相机索引(1-based) + int cameraIndex = m_sequentialCurrentIndex + 1; + m_currentCameraIndex = cameraIndex; + + // 获取相机名称 + QString cameraName; + int enabledIndex = 0; + for (const auto& cameraConfig : m_configResult.cameras) { + if (cameraConfig.enabled) { + enabledIndex++; + if (enabledIndex == cameraIndex) { + cameraName = QString::fromStdString(cameraConfig.name); + break; + } + } + } + + LOG_INFO("Starting detection for camera %d/%d: %s\n", + m_sequentialCurrentIndex + 1, m_sequentialTotalCount, + cameraName.toStdString().c_str()); + + if (m_statusUpdate) { + m_statusUpdate->OnStatusUpdate(QString("正在检测设备 %1/%2: %3") + .arg(m_sequentialCurrentIndex + 1) + .arg(m_sequentialTotalCount) + .arg(cameraName)); + } + + // 清空数据缓存 + ClearDetectionDataCache(); + + // 开始检测当前相机 + StartDetection(cameraIndex); +} + +void WheelMeasurePresenter::OnConfigChanged(const WheelMeasureConfigResult& configResult) +{ + LOG_INFO("Config changed notification received\n"); + m_configResult = configResult; + + // 更新基类调试参数 + SetDebugParam(m_configResult.debugParam); + + emit configUpdated(); +} + +void WheelMeasurePresenter::processScanData(std::vector>& detectionDataCache) +{ + LOG_INFO("Processing scan data, lines: %zu\n", detectionDataCache.size()); + + // 打印扫描数据统计信息 + int totalPoints = 0; + for (const auto& linePair : detectionDataCache) { + totalPoints += linePair.second.nPointCount; + } + LOG_INFO("Scan data statistics: %zu lines, %d total points\n", detectionDataCache.size(), totalPoints); + + // 打印调平参数 + LOG_INFO("========== PlaneCalib Parameters ==========\n"); + if (!m_configResult.planeCalibParams.empty()) { + for (const auto& calibParam : m_configResult.planeCalibParams) { + LOG_INFO("Camera[%d] %s: isCalibrated=%d, planeHeight=%.2f\n", + calibParam.cameraIndex, calibParam.cameraName.c_str(), + calibParam.isCalibrated ? 1 : 0, calibParam.planeHeight); + LOG_INFO(" planeCalib: [%.6f, %.6f, %.6f, %.6f, %.6f, %.6f, %.6f, %.6f, %.6f]\n", + calibParam.planeCalib[0], calibParam.planeCalib[1], calibParam.planeCalib[2], + calibParam.planeCalib[3], calibParam.planeCalib[4], calibParam.planeCalib[5], + calibParam.planeCalib[6], calibParam.planeCalib[7], calibParam.planeCalib[8]); + LOG_INFO(" invRMatrix: [%.6f, %.6f, %.6f, %.6f, %.6f, %.6f, %.6f, %.6f, %.6f]\n", + calibParam.invRMatrix[0], calibParam.invRMatrix[1], calibParam.invRMatrix[2], + calibParam.invRMatrix[3], calibParam.invRMatrix[4], calibParam.invRMatrix[5], + calibParam.invRMatrix[6], calibParam.invRMatrix[7], calibParam.invRMatrix[8]); + } + } else { + LOG_INFO("No PlaneCalib parameters loaded!\n"); + } + + // 打印算法参数 + LOG_INFO("========== Algorithm Parameters ==========\n"); + LOG_INFO("CornerParam: minEndingGap=%.2f, minEndingGap_z=%.2f, scale=%.2f, cornerTh=%.2f, jumpCornerTh_1=%.2f, jumpCornerTh_2=%.2f\n", + m_configResult.algorithmParams.cornerParam.minEndingGap, + m_configResult.algorithmParams.cornerParam.minEndingGap_z, + m_configResult.algorithmParams.cornerParam.scale, + m_configResult.algorithmParams.cornerParam.cornerTh, + m_configResult.algorithmParams.cornerParam.jumpCornerTh_1, + m_configResult.algorithmParams.cornerParam.jumpCornerTh_2); + + LOG_INFO("LineSegParam: segGapTh_y=%.2f, segGapTh_z=%.2f, maxDist=%.2f\n", + m_configResult.algorithmParams.lineSegParam.segGapTh_y, + m_configResult.algorithmParams.lineSegParam.segGapTh_z, + m_configResult.algorithmParams.lineSegParam.maxDist); + + LOG_INFO("OutlierFilterParam: continuityTh=%.2f, outlierTh=%.2f\n", + m_configResult.algorithmParams.filterParam.continuityTh, + m_configResult.algorithmParams.filterParam.outlierTh); + + LOG_INFO("TreeGrowParam: yDeviation_max=%.2f, zDeviation_max=%.2f, maxLineSkipNum=%d, maxSkipDistance=%.2f, minLTypeTreeLen=%.2f, minVTypeTreeLen=%.2f\n", + m_configResult.algorithmParams.growParam.yDeviation_max, + m_configResult.algorithmParams.growParam.zDeviation_max, + m_configResult.algorithmParams.growParam.maxLineSkipNum, + m_configResult.algorithmParams.growParam.maxSkipDistance, + m_configResult.algorithmParams.growParam.minLTypeTreeLen, + m_configResult.algorithmParams.growParam.minVTypeTreeLen); + + LOG_INFO("DebugParam: enableDebug=%d, saveDebugImage=%d, printDetailLog=%d\n", + m_configResult.debugParam.enableDebug, + m_configResult.debugParam.saveDebugImage, + m_configResult.debugParam.printDetailLog); + + LOG_INFO("============================================\n"); + + // ========== 调用 wheelArchHeigthMeasure SDK ========== + + // 1. 转换检测数据为SDK所需格式 + std::vector> scanLines; + size_t validPointCount = 0; + int convertResult = m_dataLoader.ConvertToSVzNL3DPosition(detectionDataCache, scanLines, &validPointCount); + + // 检查数据有效性 + if (convertResult != SUCCESS || scanLines.empty()) { + LOG_WARNING("Failed to convert data to XYZ format or no XYZ data available\n"); + return; + } + + // 检查线束数量和有效点数量 + const size_t MIN_SCAN_LINES = 200; + const size_t MIN_VALID_POINTS = 1000; + + if (scanLines.size() < MIN_SCAN_LINES || validPointCount < MIN_VALID_POINTS) { + LOG_ERROR("Insufficient data for detection: scan lines=%zu (min=%zu), valid points=%zu (min=%zu)\n", + scanLines.size(), MIN_SCAN_LINES, validPointCount, MIN_VALID_POINTS); + + if (m_statusUpdate) { + QString errorMsg = QString("数据不足: 线束=%1(最少%2), 有效点=%3(最少%4)") + .arg(scanLines.size()).arg(MIN_SCAN_LINES) + .arg(validPointCount).arg(MIN_VALID_POINTS); + m_statusUpdate->OnStatusUpdate(errorMsg); + m_statusUpdate->OnErrorOccurred(errorMsg); + } + + SetWorkStatus(WorkStatus::Error); + + // 如果正在进行顺序检测,继续检测下一个设备 + if (m_sequentialDetecting) { + m_sequentialCurrentIndex++; + QMetaObject::invokeMethod(this, [this]() { + continueSequentialDetection(); + }, Qt::QueuedConnection); + } + return; + } + + LOG_INFO("Data validation passed: scan lines=%zu, valid points=%zu\n", scanLines.size(), validPointCount); + + // 2. 准备算法参数 + SSG_cornerParam cornerParam; + cornerParam.minEndingGap = m_configResult.algorithmParams.cornerParam.minEndingGap; + cornerParam.minEndingGap_z = m_configResult.algorithmParams.cornerParam.minEndingGap_z; + cornerParam.scale = m_configResult.algorithmParams.cornerParam.scale; + cornerParam.cornerTh = m_configResult.algorithmParams.cornerParam.cornerTh; + cornerParam.jumpCornerTh_1 = m_configResult.algorithmParams.cornerParam.jumpCornerTh_1; + cornerParam.jumpCornerTh_2 = m_configResult.algorithmParams.cornerParam.jumpCornerTh_2; + + SSG_lineSegParam lineSegParam; + lineSegParam.segGapTh_y = m_configResult.algorithmParams.lineSegParam.segGapTh_y; + lineSegParam.segGapTh_z = m_configResult.algorithmParams.lineSegParam.segGapTh_z; + lineSegParam.maxDist = m_configResult.algorithmParams.lineSegParam.maxDist; + + SSG_outlierFilterParam filterParam; + filterParam.continuityTh = m_configResult.algorithmParams.filterParam.continuityTh; + filterParam.outlierTh = m_configResult.algorithmParams.filterParam.outlierTh; + + SSG_treeGrowParam growParam; + growParam.yDeviation_max = m_configResult.algorithmParams.growParam.yDeviation_max; + growParam.zDeviation_max = m_configResult.algorithmParams.growParam.zDeviation_max; + growParam.maxLineSkipNum = m_configResult.algorithmParams.growParam.maxLineSkipNum; + growParam.maxSkipDistance = m_configResult.algorithmParams.growParam.maxSkipDistance; + growParam.minLTypeTreeLen = m_configResult.algorithmParams.growParam.minLTypeTreeLen; + growParam.minVTypeTreeLen = m_configResult.algorithmParams.growParam.minVTypeTreeLen; + + // 3. 准备调平参数(使用当前相机的调平参数) + SSG_planeCalibPara groundCalibPara; + memset(&groundCalibPara, 0, sizeof(groundCalibPara)); + + // 初始化为单位矩阵 + groundCalibPara.planeCalib[0] = 1.0; + groundCalibPara.planeCalib[4] = 1.0; + groundCalibPara.planeCalib[8] = 1.0; + groundCalibPara.invRMatrix[0] = 1.0; + groundCalibPara.invRMatrix[4] = 1.0; + groundCalibPara.invRMatrix[8] = 1.0; + groundCalibPara.planeHeight = 0.0; + + // 查找当前相机的调平参数 + WheelCameraPlaneCalibParam* calibParam = getPlaneCalibParam(m_currentCameraIndex); + if (calibParam && calibParam->isCalibrated) { + for (int i = 0; i < 9; ++i) { + groundCalibPara.planeCalib[i] = calibParam->planeCalib[i]; + groundCalibPara.invRMatrix[i] = calibParam->invRMatrix[i]; + } + groundCalibPara.planeHeight = calibParam->planeHeight; + LOG_INFO("Using calibrated plane parameters for camera %d\n", m_currentCameraIndex); + LOG_INFO(" planeHeight: %.3f, errorCompensation: %.2f\n", calibParam->planeHeight, calibParam->errorCompensation); + + if(calibParam){ + // 计算调平使用的地面高度(加上该相机的误差补偿) + double adjustedPlaneHeight = groundCalibPara.planeHeight + calibParam->errorCompensation; + LOG_INFO(" adjustedPlaneHeight (with compensation): %.3f\n", adjustedPlaneHeight); + + for(size_t i = 0; i < scanLines.size(); i++){ + wd_horizonCamera_lineDataR(scanLines[i], calibParam->planeCalib, adjustedPlaneHeight); + } + } + } else { + LOG_WARN("No calibration data for camera %d, using default parameters\n", m_currentCameraIndex); + } + + + // 4. 调用算法 + int errCode = 0; + LOG_INFO("Calling wd_wheelArchHeigthMeasure...\n"); + + WD_wheelArchInfo wheelArchResult = wd_wheelArchHeigthMeasure( + scanLines, + cornerParam, + lineSegParam, + filterParam, + growParam, + groundCalibPara, + &errCode); + + LOG_INFO("Algorithm completed with errCode=%d\n", errCode); + + // 5. 处理算法结果 + WheelMeasureResult result; + + // 从配置中获取当前相机名称 + QString cameraName; + int enabledIndex = 0; + for (const auto& cameraConfig : m_configResult.cameras) { + if (cameraConfig.enabled) { + enabledIndex++; + if (enabledIndex == m_currentCameraIndex) { + cameraName = QString::fromStdString(cameraConfig.name); + break; + } + } + } + if (cameraName.isEmpty() && !m_vrEyeDeviceList.empty()) { + cameraName = QString::fromStdString(m_vrEyeDeviceList[0].first); + } + if (cameraName.isEmpty()) { + cameraName = QString("Camera%1").arg(m_currentCameraIndex); + } + + result.cameraName = cameraName; + result.aliasName = cameraName; + result.bResultValid = (errCode == 0); + + if (errCode == 0) { + LOG_INFO("========== Wheel Arch Measurement Result ==========\n"); + LOG_INFO("wheelArchPos: (%.3f, %.3f, %.3f)\n", + wheelArchResult.wheelArchPos.x, + wheelArchResult.wheelArchPos.y, + wheelArchResult.wheelArchPos.z); + LOG_INFO("wheelUpPos: (%.3f, %.3f, %.3f)\n", + wheelArchResult.wheelUpPos.x, + wheelArchResult.wheelUpPos.y, + wheelArchResult.wheelUpPos.z); + LOG_INFO("wheelDownPos: (%.3f, %.3f, %.3f)\n", + wheelArchResult.wheelDownPos.x, + wheelArchResult.wheelDownPos.y, + wheelArchResult.wheelDownPos.z); + LOG_INFO("archToCenterHeigth: %.3f archToGroundHeigth: %.3f mm\n", wheelArchResult.archToCenterHeigth, wheelArchResult.archToGroundHeigth); + LOG_INFO("==================================================\n"); + + if (m_statusUpdate) { + QString statusMsg = QString("轮眉高度: %1 mm, 到地面高度: %2 mm").arg(wheelArchResult.archToCenterHeigth).arg(wheelArchResult.archToGroundHeigth); + m_statusUpdate->OnStatusUpdate(statusMsg); + } + + // 填充测量结果数据 + WheelMeasureData measureData; + measureData.archToCenterHeight = wheelArchResult.archToCenterHeigth; + measureData.archToGroundHeight = wheelArchResult.archToGroundHeigth; + measureData.wheelArchPosX = wheelArchResult.wheelArchPos.x; + measureData.wheelArchPosY = wheelArchResult.wheelArchPos.y; + measureData.wheelArchPosZ = wheelArchResult.wheelArchPos.z; + measureData.wheelUpPosX = wheelArchResult.wheelUpPos.x; + measureData.wheelUpPosY = wheelArchResult.wheelUpPos.y; + measureData.wheelUpPosZ = wheelArchResult.wheelUpPos.z; + measureData.wheelDownPosX = wheelArchResult.wheelDownPos.x; + measureData.wheelDownPosY = wheelArchResult.wheelDownPos.y; + measureData.wheelDownPosZ = wheelArchResult.wheelDownPos.z; + measureData.timestamp = QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss"); + result.result.push_back(measureData); + + // 使用 PointCloudImageUtils 生成带检测结果的图像 + result.image = PointCloudImageUtils::GenerateWheelArchImage( + scanLines, + wheelArchResult.wheelArchPos, + wheelArchResult.wheelUpPos, + wheelArchResult.wheelDownPos, + wheelArchResult.archToCenterHeigth, + true); + } else { + LOG_ERROR("Algorithm failed with errCode=%d\n", errCode); + if (m_statusUpdate) { + m_statusUpdate->OnStatusUpdate(QString("算法检测失败,错误码: %1").arg(errCode)); + } + + // 生成仅有点云的图像(无检测结果) + SVzNL3DPoint emptyPoint = {0.0, 0.0, 0.0}; + result.image = PointCloudImageUtils::GenerateWheelArchImage( + scanLines, + emptyPoint, + emptyPoint, + emptyPoint, + 0.0, + false); + } + + result.bImageValid = !result.image.isNull(); + + // ========== 写入检测结果到 Modbus (地址2-25) ========== + // 辅助lambda: float转两个uint16_t (大端模式) + auto floatToUint16 = [](float value, uint16_t& high, uint16_t& low) { + uint32_t bits; + memcpy(&bits, &value, sizeof(float)); + high = static_cast((bits >> 16) & 0xFFFF); + low = static_cast(bits & 0xFFFF); + }; + + uint16_t modbusData[24]; + memset(modbusData, 0, sizeof(modbusData)); + + // 地址2: 设备序号 + modbusData[0] = static_cast(m_currentCameraIndex); + // 地址3: 结果有效 + modbusData[1] = (errCode == 0) ? 1 : 0; + + if (errCode == 0) { + // 地址4-5: 轮眉高度(到中心) + floatToUint16(static_cast(wheelArchResult.archToCenterHeigth), modbusData[2], modbusData[3]); + // 地址6-7: 轮眉X + floatToUint16(static_cast(wheelArchResult.wheelArchPos.x), modbusData[4], modbusData[5]); + // 地址8-9: 轮眉Y + floatToUint16(static_cast(wheelArchResult.wheelArchPos.y), modbusData[6], modbusData[7]); + // 地址10-11: 轮眉Z + floatToUint16(static_cast(wheelArchResult.wheelArchPos.z), modbusData[8], modbusData[9]); + // 地址12-13: 上点X + floatToUint16(static_cast(wheelArchResult.wheelUpPos.x), modbusData[10], modbusData[11]); + // 地址14-15: 上点Y + floatToUint16(static_cast(wheelArchResult.wheelUpPos.y), modbusData[12], modbusData[13]); + // 地址16-17: 上点Z + floatToUint16(static_cast(wheelArchResult.wheelUpPos.z), modbusData[14], modbusData[15]); + // 地址18-19: 下点X + floatToUint16(static_cast(wheelArchResult.wheelDownPos.x), modbusData[16], modbusData[17]); + // 地址20-21: 下点Y + floatToUint16(static_cast(wheelArchResult.wheelDownPos.y), modbusData[18], modbusData[19]); + // 地址22-23: 下点Z + floatToUint16(static_cast(wheelArchResult.wheelDownPos.z), modbusData[20], modbusData[21]); + // 地址24-25: 到地面高度 + floatToUint16(static_cast(wheelArchResult.archToGroundHeigth), modbusData[22], modbusData[23]); + } + + WriteModbusRegisters(2, modbusData, 24); + LOG_INFO("Modbus: 写入检测结果到地址2-25, 设备=%d, 有效=%d\n", m_currentCameraIndex, modbusData[1]); + + // 检测完成后清零"检测控制"(地址0) + uint16_t zero = 0; + WriteModbusRegisters(0, &zero, 1); + + // 直接调用回调,不使用信号槽 + if (m_statusUpdate) { + m_statusUpdate->OnMeasureResult(result); + } + + SetWorkStatus(WorkStatus::Completed); + + // 如果是TCP触发的检测,缓存结果 + if (m_tcpDetectionMode) { + WheelMeasureTCPProtocol::CameraMeasureResult tcpResult; + tcpResult.cameraId = m_currentCameraIndex; + + if (errCode == 0) { + tcpResult.errorCode = 0; + tcpResult.centerDistance = wheelArchResult.archToCenterHeigth; + tcpResult.archDistance = wheelArchResult.archToGroundHeigth; + } else { + // 根据错误码映射 + if (errCode == -1 || errCode == -2) { + tcpResult.errorCode = 401; // 工件为空 + } else { + tcpResult.errorCode = 400; // 扫描/匹配失败 + } + } + + m_tcpResults[m_currentCameraIndex] = tcpResult; + LOG_INFO("TCP检测结果已缓存: 相机%d, 错误码=%d\n", tcpResult.cameraId, tcpResult.errorCode); + } + + // 如果正在进行顺序检测,继续检测下一个设备 + if (m_sequentialDetecting) { + m_sequentialCurrentIndex++; + QMetaObject::invokeMethod(this, [this]() { + continueSequentialDetection(); + }, Qt::QueuedConnection); + } else if (m_tcpDetectionMode) { + // TCP模式下,检查是否所有相机都检测完成 + if (m_tcpResults.size() >= m_sequentialTotalCount) { + sendTCPMeasureResults(); + } + } +} + +WheelCameraPlaneCalibParam* WheelMeasurePresenter::getPlaneCalibParam(int cameraIndex) +{ + for (auto& param : m_configResult.planeCalibParams) { + if (param.cameraIndex == cameraIndex) { + return ¶m; + } + } + return nullptr; +} + +// ============ ICameraLevelCalculator 接口实现 ============ + +bool WheelMeasurePresenter::CalculatePlaneCalibration( + const std::vector>& scanData, + double planeCalib[9], + double& planeHeight, + double invRMatrix[9]) +{ + LOG_INFO("CalculatePlaneCalibration called, scan lines: %zu\n", scanData.size()); + + // TODO: 调用调平算法库计算调平参数 + // 暂时返回单位矩阵 + planeCalib[0] = 1.0; planeCalib[1] = 0.0; planeCalib[2] = 0.0; + planeCalib[3] = 0.0; planeCalib[4] = 1.0; planeCalib[5] = 0.0; + planeCalib[6] = 0.0; planeCalib[7] = 0.0; planeCalib[8] = 1.0; + + invRMatrix[0] = 1.0; invRMatrix[1] = 0.0; invRMatrix[2] = 0.0; + invRMatrix[3] = 0.0; invRMatrix[4] = 1.0; invRMatrix[5] = 0.0; + invRMatrix[6] = 0.0; invRMatrix[7] = 0.0; invRMatrix[8] = 1.0; + + planeHeight = 0.0; + + return true; +} + +// ============ ICameraLevelResultSaver 接口实现 ============ + +bool WheelMeasurePresenter::SaveLevelingResults(double planeCalib[9], double planeHeight, double invRMatrix[9], + int cameraIndex, const QString& cameraName) +{ + LOG_INFO("SaveLevelingResults: Camera[%d] %s, planeHeight=%.2f\n", + cameraIndex, cameraName.toStdString().c_str(), planeHeight); + + // 查找或创建调平参数 + WheelCameraPlaneCalibParam* param = getPlaneCalibParam(cameraIndex); + if (!param) { + WheelCameraPlaneCalibParam newParam; + newParam.cameraIndex = cameraIndex; + newParam.cameraName = cameraName.toStdString(); + m_configResult.planeCalibParams.push_back(newParam); + param = &m_configResult.planeCalibParams.back(); + } + + // 更新调平参数 + param->isCalibrated = true; + param->planeHeight = planeHeight; + for (int i = 0; i < 9; ++i) { + param->planeCalib[i] = planeCalib[i]; + param->invRMatrix[i] = invRMatrix[i]; + } + + // 保存配置到文件 + QString configPath = PathManager::GetInstance().GetConfigFilePath(); + bool success = m_config->SaveConfig(configPath.toStdString(), m_configResult); + + if (success) { + LOG_INFO("Leveling results saved successfully\n"); + } else { + LOG_ERROR("Failed to save leveling results\n"); + } + + return success; +} + +bool WheelMeasurePresenter::LoadLevelingResults(int cameraIndex, const QString& cameraName, + double planeCalib[9], double& planeHeight, double invRMatrix[9]) +{ + LOG_INFO("LoadLevelingResults: Camera[%d] %s\n", cameraIndex, cameraName.toStdString().c_str()); + + WheelCameraPlaneCalibParam* param = getPlaneCalibParam(cameraIndex); + if (!param || !param->isCalibrated) { + LOG_WARN("No calibration data found for camera %d\n", cameraIndex); + return false; + } + + planeHeight = param->planeHeight; + for (int i = 0; i < 9; ++i) { + planeCalib[i] = param->planeCalib[i]; + invRMatrix[i] = param->invRMatrix[i]; + } + + LOG_INFO("Leveling results loaded: planeHeight=%.2f\n", planeHeight); + return true; +} + +// ============ 静态相机状态回调函数 ============ + +void WheelMeasurePresenter::_StaticCameraNotify(EVzDeviceWorkStatus eStatus, void* pExtData, unsigned int nDataLength, void* pInfoParam) +{ + // 从pInfoParam获取this指针,转换回WheelMeasurePresenter*类型 + WheelMeasurePresenter* pThis = reinterpret_cast(pInfoParam); + if (pThis) { + // 调用实例的非静态成员函数 + pThis->_CameraNotify(eStatus, pExtData, nDataLength, pInfoParam); + } +} + +void WheelMeasurePresenter::_CameraNotify(EVzDeviceWorkStatus eStatus, void* pExtData, unsigned int nDataLength, void* pInfoParam) +{ + LOG_DEBUG("[Camera Notify] received: status=%d\n", (int)eStatus); + + switch (eStatus) { + case EVzDeviceWorkStatus::keDeviceWorkStatus_Offline: + { + LOG_WARNING("[Camera Notify] Camera device offline/disconnected\n"); + + // 通知UI相机状态变更 + if (m_statusUpdate) { + m_statusUpdate->OnCameraDisconnected(QString("Camera")); + m_statusUpdate->OnStatusUpdate(QString("相机设备离线")); + } + break; + } + + case EVzDeviceWorkStatus::keDeviceWorkStatus_Eye_Reconnect: + { + LOG_INFO("[Camera Notify] Camera device reconnecting\n"); + + if (m_statusUpdate) { + m_statusUpdate->OnStatusUpdate(QString("相机设备重连中...")); + } + break; + } + + case EVzDeviceWorkStatus::keDeviceWorkStatus_Eye_Comming: + { + LOG_INFO("[Camera Notify] Camera device connected\n"); + + if (m_statusUpdate) { + m_statusUpdate->OnCameraConnected(QString("Camera")); + m_statusUpdate->OnStatusUpdate(QString("相机设备已连接")); + } + break; + } + + default: + LOG_DEBUG("[Camera Notify] Unhandled status: %d\n", (int)eStatus); + break; + } +} + +// ============ Modbus写寄存器回调处理 ============ + +void WheelMeasurePresenter::OnModbusWriteCallback(uint16_t startAddress, const uint16_t* data, uint16_t count) +{ + LOG_INFO("OnModbusWriteCallback: address=%d, count=%d\n", startAddress, count); + + if (!data || count == 0) { + return; + } + + // 地址0: 写1-4直接开始检测对应设备 + if (startAddress == 0) { + int deviceIndex = data[0]; + + // 检查设备索引是否有效 (1-4) + if (deviceIndex >= 1 && deviceIndex <= 4 && + deviceIndex <= static_cast(m_vrEyeDeviceList.size())) { + LOG_INFO("Modbus: 开始检测设备 %d\n", deviceIndex); + // 使用 QMetaObject::invokeMethod 在主线程执行 + QMetaObject::invokeMethod(this, [this, deviceIndex]() { + ResetDetect(deviceIndex - 1); // ResetDetect 期望 0-based 索引 + }, Qt::QueuedConnection); + } else { + LOG_WARNING("Modbus: 无效的设备索引: %d (有效范围: 1-%d)\n", + deviceIndex, static_cast(m_vrEyeDeviceList.size())); + } + } +} + +// ============ TCP协议回调处理 ============ + +bool WheelMeasurePresenter::onTCPDetectionTriggered(int param) +{ + LOG_INFO("TCP检测触发,参数: %d\n", param); + + // 清空之前的TCP结果缓存 + m_tcpResults.clear(); + m_tcpDetectionMode = true; + + // 启动所有相机的顺序检测 + QMetaObject::invokeMethod(this, [this]() { + StartAllDetection(); + }, Qt::QueuedConnection); + + return true; +} + +void WheelMeasurePresenter::sendTCPMeasureResults() +{ + LOG_INFO("发送TCP测量结果,共 %d 个相机\n", m_tcpResults.size()); + + // 构建结果向量(按相机ID排序) + std::vector results; + + // 按相机ID顺序添加结果 + for (int cameraId = 1; cameraId <= m_sequentialTotalCount; ++cameraId) { + if (m_tcpResults.contains(cameraId)) { + results.push_back(m_tcpResults[cameraId]); + } else { + // 如果某个相机没有结果,添加失败结果 + WheelMeasureTCPProtocol::CameraMeasureResult failResult; + failResult.cameraId = cameraId; + failResult.errorCode = 400; // 扫描失败 + results.push_back(failResult); + LOG_WARNING("相机 %d 没有检测结果,使用默认失败结果\n", cameraId); + } + } + + // 发送结果 + int sendResult = m_tcpProtocol.SendMeasureResults(results); + if (sendResult != 0) { + LOG_ERROR("发送TCP测量结果失败,错误码: %d\n", sendResult); + } else { + LOG_INFO("TCP测量结果发送成功\n"); + } + + // 清空缓存和标志 + m_tcpResults.clear(); + m_tcpDetectionMode = false; +} diff --git a/App/WheelMeasure/WheelMeasureApp/Presenter/Src/WheelMeasureTCPProtocol.cpp b/App/WheelMeasure/WheelMeasureApp/Presenter/Src/WheelMeasureTCPProtocol.cpp new file mode 100644 index 0000000..d794e8f --- /dev/null +++ b/App/WheelMeasure/WheelMeasureApp/Presenter/Src/WheelMeasureTCPProtocol.cpp @@ -0,0 +1,246 @@ +#include "WheelMeasureTCPProtocol.h" +#include "VrLog.h" +#include + +WheelMeasureTCPProtocol::WheelMeasureTCPProtocol() + : m_pTCPServer(nullptr) + , m_bServerRunning(false) + , m_nPort(6800) + , m_pCurrentClient(nullptr) +{ +} + +WheelMeasureTCPProtocol::~WheelMeasureTCPProtocol() +{ + Deinitialize(); +} + +int WheelMeasureTCPProtocol::Initialize(uint16_t port) +{ + LOG_INFO("初始化车轮测量TCP服务器,端口: %d\n", port); + + m_nPort = port; + + // 创建TCP服务器实例 + if (!VrCreatYTCPServer(&m_pTCPServer)) { + LOG_ERROR("创建TCP服务器实例失败\n"); + return -1; + } + + // 初始化TCP服务器 + if (!m_pTCPServer->Init(port)) { + LOG_ERROR("初始化TCP服务器失败,端口: %d\n", port); + delete m_pTCPServer; + m_pTCPServer = nullptr; + return -2; + } + + // 设置事件回调 + m_pTCPServer->SetEventCallback([this](const TCPClient* pClient, TCPServerEventType eventType) { + this->OnTCPEvent(pClient, eventType); + }); + + // 启动TCP服务器 + if (!m_pTCPServer->Start([this](const TCPClient* pClient, const char* pData, const unsigned int nLen) { + this->OnTCPDataReceived(pClient, pData, nLen); + })) { + LOG_ERROR("启动TCP服务器失败\n"); + m_pTCPServer->Close(); + delete m_pTCPServer; + m_pTCPServer = nullptr; + return -3; + } + + m_bServerRunning = true; + LOG_INFO("车轮测量TCP服务器启动成功,端口: %d\n", port); + return 0; +} + +void WheelMeasureTCPProtocol::Deinitialize() +{ + if (m_pTCPServer) { + LOG_INFO("停止车轮测量TCP服务器\n"); + + m_bServerRunning = false; + m_pCurrentClient = nullptr; + + m_pTCPServer->Stop(); + m_pTCPServer->Close(); + delete m_pTCPServer; + m_pTCPServer = nullptr; + + LOG_INFO("车轮测量TCP服务器已停止\n"); + } +} + +int WheelMeasureTCPProtocol::SendMeasureResults(const std::vector& results) +{ + if (!m_pTCPServer || !m_bServerRunning) { + LOG_ERROR("TCP服务器未运行\n"); + return -1; + } + + if (results.empty()) { + LOG_ERROR("测量结果为空\n"); + return -2; + } + + // 构建结果字符串:1,100,200;2,100,200;3,100,200;4,100,200 + // 或失败情况:1,400;2,100,200;3,100,200;4,100,200 + QString resultStr; + for (size_t i = 0; i < results.size(); ++i) { + const auto& result = results[i]; + + if (i > 0) { + resultStr += ";"; + } + + resultStr += QString::number(result.cameraId); + + if (result.errorCode != 0) { + // 失败情况:只发送相机ID和错误码 + resultStr += "," + QString::number(result.errorCode); + } else { + // 成功情况:发送相机ID、中心距离、轮眉距离 + resultStr += "," + QString::number(static_cast(result.centerDistance)); + resultStr += "," + QString::number(static_cast(result.archDistance)); + } + } + + LOG_INFO("发送测量结果: %s\n", resultStr.toStdString().c_str()); + + // 发送给当前请求的客户端 + QMutexLocker locker(&m_mutex); + bool success = SendData(m_pCurrentClient, resultStr); + + if (!success) { + LOG_ERROR("发送测量结果失败\n"); + return -3; + } + + return 0; +} + +void WheelMeasureTCPProtocol::SetDetectionTriggerCallback(const DetectionTriggerCallback& callback) +{ + m_detectionCallback = callback; +} + +int WheelMeasureTCPProtocol::GetClientCount() const +{ + // IYTCPServer 没有提供 GetClientCount 方法 + // 返回服务器是否运行的状态 + return m_bServerRunning ? 1 : 0; +} + +void WheelMeasureTCPProtocol::OnTCPEvent(const TCPClient* pClient, TCPServerEventType eventType) +{ + switch (eventType) { + case TCP_EVENT_CLIENT_CONNECTED: + { + LOG_INFO("客户端已连接: %s\n", pClient->m_szClientIP); + break; + } + + case TCP_EVENT_CLIENT_DISCONNECTED: + { + LOG_INFO("客户端已断开: %s\n", pClient->m_szClientIP); + + // 如果是当前请求的客户端断开,清除引用 + QMutexLocker locker(&m_mutex); + if (m_pCurrentClient == pClient) { + m_pCurrentClient = nullptr; + } + break; + } + + case TCP_EVENT_CLIENT_EXCEPTION: + { + LOG_WARNING("客户端异常: %s\n", pClient->m_szClientIP); + break; + } + + default: + break; + } +} + +void WheelMeasureTCPProtocol::OnTCPDataReceived(const TCPClient* pClient, const char* pData, unsigned int nLen) +{ + if (!pData || nLen == 0) { + return; + } + + // 转换为QString + QString command = QString::fromUtf8(pData, nLen).trimmed(); + + LOG_INFO("收到客户端命令: %s (来自 %s)\n", + command.toStdString().c_str(), + pClient->m_szClientIP); + + // 解析命令 + ParseCommand(pClient, command); +} + +void WheelMeasureTCPProtocol::ParseCommand(const TCPClient* pClient, const QString& command) +{ + // 命令格式:start,100 + QStringList parts = command.split(','); + + if (parts.isEmpty()) { + LOG_WARNING("收到空命令\n"); + return; + } + + QString cmd = parts[0].trimmed().toLower(); + + if (cmd == "start") { + // 解析参数 + int param = 100; // 默认值 + if (parts.size() > 1) { + bool ok = false; + int parsedParam = parts[1].trimmed().toInt(&ok); + if (ok) { + param = parsedParam; + } + } + + LOG_INFO("收到start命令,参数: %d\n", param); + + // 保存当前请求的客户端 + { + QMutexLocker locker(&m_mutex); + m_pCurrentClient = pClient; + } + + // 触发检测回调 + if (m_detectionCallback) { + bool success = m_detectionCallback(param); + if (!success) { + LOG_ERROR("触发检测失败\n"); + // 可以选择发送错误响应 + } + } else { + LOG_WARNING("检测回调未设置\n"); + } + } else { + LOG_WARNING("未知命令: %s\n", cmd.toStdString().c_str()); + } +} + +bool WheelMeasureTCPProtocol::SendData(const TCPClient* pClient, const QString& data) +{ + if (!m_pTCPServer || !m_bServerRunning) { + return false; + } + + QByteArray byteArray = data.toUtf8(); + + if (pClient) { + // 发送给指定客户端 + return m_pTCPServer->SendData(pClient, byteArray.constData(), byteArray.size()); + } else { + // 发送给所有客户端 + return m_pTCPServer->SendAllData(byteArray.constData(), byteArray.size()); + } +} diff --git a/App/WheelMeasure/WheelMeasureApp/WheelMeasureApp.pro b/App/WheelMeasure/WheelMeasureApp/WheelMeasureApp.pro index f8f3b3a..2ce6eb5 100644 --- a/App/WheelMeasure/WheelMeasureApp/WheelMeasureApp.pro +++ b/App/WheelMeasure/WheelMeasureApp/WheelMeasureApp.pro @@ -20,6 +20,7 @@ INCLUDEPATH += $$PWD/Presenter/Inc INCLUDEPATH += ../WheelMeasureConfig/Inc INCLUDEPATH += ../../../VrNets/TCPClient/Inc +INCLUDEPATH += ../../../VrNets/TCPServer/Inc INCLUDEPATH += ../../../Utils/VrUtils/Inc INCLUDEPATH += ../../../Device/VrEyeDevice/Inc INCLUDEPATH += ../../../Utils/VrCommon/Inc @@ -33,7 +34,7 @@ INCLUDEPATH += ../../../AppAlgo/wheelArchHeigthMeasure/Inc # Link libraries win32:CONFIG(debug, debug|release) { LIBS += -L../WheelMeasureConfig/debug -lWheelMeasureConfig - LIBS += -L../../../VrNets/debug -lVrTcpClient + LIBS += -L../../../VrNets/debug -lVrTcpClient -lVrTcpServer LIBS += -L../../../Utils/VrUtils/debug -lVrUtils LIBS += -L../../../Device/VrEyeDevice/debug -lVrEyeDevice LIBS += -L../../../AppUtils/UICommon/debug -lUICommon @@ -43,7 +44,7 @@ win32:CONFIG(debug, debug|release) { LIBS += -L../../../VrNets/debug -lVrModbus } else:win32:CONFIG(release, debug|release) { LIBS += -L../WheelMeasureConfig/release -lWheelMeasureConfig - LIBS += -L../../../VrNets/release -lVrTcpClient + LIBS += -L../../../VrNets/release -lVrTcpClient -lVrTcpServer LIBS += -L../../../Utils/VrUtils/release -lVrUtils LIBS += -L../../../Device/VrEyeDevice/release -lVrEyeDevice LIBS += -L../../../AppUtils/UICommon/release -lUICommon @@ -58,7 +59,7 @@ win32:CONFIG(debug, debug|release) { LIBS += -L../../../AppUtils/UICommon -lUICommon LIBS += -L../../../Utils/CloudUtils -lCloudUtils LIBS += -L../../../Device/VrEyeDevice -lVrEyeDevice - LIBS += -L../../../VrNets -lVrTcpClient + LIBS += -L../../../VrNets -lVrTcpClient -lVrTcpServer LIBS += -L../../../Utils/VrUtils -lVrUtils LIBS += -L../../../Module/ModbusTCPServer -lModbusTCPServer LIBS += -L../../../VrNets -lVrModbus @@ -79,7 +80,8 @@ SOURCES += \ widgets/DeviceStatusWidget.cpp \ widgets/ImageGridWidget.cpp \ widgets/MeasureResultListWidget.cpp \ - Presenter/Src/WheelMeasurePresenter.cpp + Presenter/Src/WheelMeasurePresenter.cpp \ + Presenter/Src/WheelMeasureTCPProtocol.cpp HEADERS += \ IWheelMeasureStatus.h \ @@ -91,7 +93,8 @@ HEADERS += \ widgets/DeviceStatusWidget.h \ widgets/ImageGridWidget.h \ widgets/MeasureResultListWidget.h \ - Presenter/Inc/WheelMeasurePresenter.h + Presenter/Inc/WheelMeasurePresenter.h \ + Presenter/Inc/WheelMeasureTCPProtocol.h FORMS += \ dialogalgoarg.ui \ diff --git a/AppAlgo/holeDetection/arm/debug/libHoleDetectionLib.so.1.0.0 b/AppAlgo/holeDetection/arm/debug/libHoleDetectionLib.so.1.0.0 index a287bd1..9c68401 100644 Binary files a/AppAlgo/holeDetection/arm/debug/libHoleDetectionLib.so.1.0.0 and b/AppAlgo/holeDetection/arm/debug/libHoleDetectionLib.so.1.0.0 differ diff --git a/AppAlgo/holeDetection/arm/release/libHoleDetectionLib.so.1.0.0 b/AppAlgo/holeDetection/arm/release/libHoleDetectionLib.so.1.0.0 index 5faefd7..c92f37b 100644 Binary files a/AppAlgo/holeDetection/arm/release/libHoleDetectionLib.so.1.0.0 and b/AppAlgo/holeDetection/arm/release/libHoleDetectionLib.so.1.0.0 differ diff --git a/AppAlgo/holeDetection/include/HoleDetection.h b/AppAlgo/holeDetection/include/HoleDetection.h index 5e1760a..5a5d75f 100644 --- a/AppAlgo/holeDetection/include/HoleDetection.h +++ b/AppAlgo/holeDetection/include/HoleDetection.h @@ -82,6 +82,14 @@ struct SHoleDetectionDebugCallbacks { */ void (*onHoleFitted)(const SHoleResult* hole, int holeIndex, void* userData); + /** + * @brief Called when segment endpoint pairs are detected + * @param segmentPairs Array of segment pairs (each pair has start and end points) + * @param count Number of segment pairs + * @param userData User-provided context pointer + */ + void (*onSegmentPairsDetected)(const SSegmentPair* segmentPairs, int count, void* userData); + /** * @brief User-provided context pointer, passed to all callbacks */ diff --git a/AppAlgo/holeDetection/include/HoleDetectionParams.h b/AppAlgo/holeDetection/include/HoleDetectionParams.h index a7b01d9..9dfafe1 100644 --- a/AppAlgo/holeDetection/include/HoleDetectionParams.h +++ b/AppAlgo/holeDetection/include/HoleDetectionParams.h @@ -2,141 +2,103 @@ #define HOLE_DETECTION_PARAMS_H #include -#include "../include/VZNL_Types.h" // Use types from VZNL_Types.h +#include "../include/VZNL_Types.h" // 使用 VZNL_Types.h 中的类型 /** - * @brief Sorting mode for detected holes + * @brief 检测到的孔洞排序模式 */ enum ESortMode { - keSortMode_None = 0, // No sorting - keSortMode_ByRadius = 1, // Sort by radius (largest first) - keSortMode_ByDepth = 2, // Sort by depth (deepest first) - keSortMode_ByQuality = 3 // Sort by quality score (highest first) + keSortMode_None = 0, // 不排序 + keSortMode_ByRadius = 1, // 按半径排序(最大的在前) + keSortMode_ByDepth = 2, // 按深度排序(最深的在前) + keSortMode_ByQuality = 3 // 按质量分数排序(最高的在前) }; /** - * @brief Detection parameters for hole detection algorithm + * @brief 孔洞检测算法的检测参数 */ struct SHoleDetectionParams { - // Pit detection parameters - int neighborCount; // Adjacent points for line connection (default: 3) - float angleThresholdPos; // Positive angle threshold in degrees (default: 70.0) - float angleThresholdNeg; // Negative angle threshold in degrees (default: -70.0) - float minPitDepth; // Minimum pit depth in mm (default: 5.0) + // 凹坑检测参数 + int neighborCount; // 线连接的相邻点数(默认值:3) + float angleThresholdPos; // 正角度阈值,单位:度(默认值:10.0) + float angleThresholdNeg; // 负角度阈值,单位:度(默认值:-10.0) + float minPitDepth; // 最小凹坑深度,单位:mm(默认值:1.0) - // Radial scanning parameters - float angleStep; // Angular step for radial scan in degrees (default: 1.0) - float maxScanRadius; // Maximum scan radius in mm (default: 100.0) + // 椭圆拟合参数 + float minRadius; // 最小孔洞半径,单位:mm(默认值:2.0) + float maxRadius; // 最大孔洞半径,单位:mm(默认值:50.0) - // Clustering parameters (DBSCAN) - float clusterEps; // DBSCAN epsilon in mm (default: 10.0) - int clusterMinPoints; // DBSCAN min points (default: 5) + // 平面拟合参数 + int expansionSize1; // 第一环扩展大小,单位:mm(默认值:5) + int expansionSize2; // 第二环扩展大小,单位:mm(默认值:10) - // Ellipse fitting parameters - float minRadius; // Minimum hole radius in mm (default: 5.0) - float maxRadius; // Maximum hole radius in mm (default: 50.0) + // V型检测参数 + int minVTransitionPoints; // V型端点之间的最小有效过渡点数(默认值:1) - // Plane fitting parameters - int expansionSize1; // First ring expansion in mm (default: 10.0) - int expansionSize2; // Second ring expansion in mm (default: 20.0) - - // Validation parameters - float validZThreshold; // Valid Z-value threshold (default: 1e-4) - - // V-type detection parameters - int minVTransitionPoints; // Minimum valid transition points between V-shape endpoints (default: 3) - - // Corner-based angle detection parameters (similar to cornerMethod) - float cornerScale; // Search distance for forward/backward points in mm (default: 5.0) - float cornerAngleThreshold; // Minimum corner angle change in degrees (default: 15.0) - float jumpCornerTh_1; // Small angle threshold for jump detection (default: 10.0) - float jumpCornerTh_2; // Large angle threshold for jump detection (default: 30.0) - float minEndingGap; // Y-direction distance threshold for jump pairing in mm (default: 5.0) - float minEndingGap_z; // Z-direction height threshold for jump validation in mm (default: 1.0) - - // Constructor with defaults + // 构造函数,设置默认值 SHoleDetectionParams() : neighborCount(3) , angleThresholdPos(10.0f) , angleThresholdNeg(-10.0f) , minPitDepth(1.0f) - , angleStep(1.0f) - , maxScanRadius(100.0f) - , clusterEps(5.0f) - , clusterMinPoints(5) - , minRadius(5.0f) + , minRadius(2.0f) , maxRadius(50.0f) , expansionSize1(5) , expansionSize2(10) - , validZThreshold(1e-4f) , minVTransitionPoints(1) - , cornerScale(2.0f) - , cornerAngleThreshold(45.0f) - , jumpCornerTh_1(10.0f) - , jumpCornerTh_2(30.0f) - , minEndingGap(5.0f) - , minEndingGap_z(1.0f) {} }; /** - * @brief Filter parameters for hole filtering + * @brief 孔洞过滤参数 */ struct SHoleFilterParams { - // Size range filtering - float minHoleRadius; // Minimum hole radius in mm (default: 5.0) - float maxHoleRadius; // Maximum hole radius in mm (default: 50.0) - // Quality threshold filtering - float maxEccentricity; // Maximum eccentricity (default: 0.5, standard e=sqrt(1-(b/a)^2)) + // 质量阈值过滤 + float maxEccentricity; // 最大离心率(默认值:0.99995,标准公式 e=sqrt(1-(b/a)^2)) - // Shape filtering (pre-fitting) - float maxCornerRatio; // Maximum corner ratio for rectangularity (default: 0.15) - // Higher = more rectangular. Set to 1.0 to disable. - float minAngularCoverage; // Minimum angular coverage in degrees (default: 300.0) - // Used to filter non-closed boundaries. Set to 0 to disable. - float maxRadiusFitRatio; // Maximum radiusVariance/radius ratio (default: 0.3) - // Measures how well points fit the circle. Set to 1.0 to disable. - float minQualityScore; // Minimum overall quality score (default: 0.3) - // Weighted combination of shape metrics. Set to 0 to disable. + // 形状过滤(拟合前) + float minAngularCoverage; // 最小角度覆盖范围,单位:度(默认值:10.0) + // 用于过滤非闭合边界。设置为 0 可禁用。 + float maxRadiusFitRatio; // 最大半径拟合比率 radiusVariance/radius(默认值:1.0) + // 衡量点与圆的拟合程度。设置为 1.0 可禁用。 + float minQualityScore; // 最小整体质量分数(默认值:0.0) + // 形状指标的加权组合。设置为 0 可禁用。 - // Planarity filtering (pre-projection) - float maxPlaneResidual; // Maximum point-to-plane residual in mm (default: 10.0) - // Rejects non-planar clusters (e.g. cliffs, step edges). - float maxAngularGap; // Maximum angular gap in degrees (default: 90.0) - // Rejects L-shaped or non-closed boundaries. - float minInlierRatio; // Minimum inlier ratio for ellipse fit (default: 0.7) - // Fraction of points within tolerance of fitted ellipse. + // 平面性过滤(投影前) + float maxPlaneResidual; // 最大点到平面残差,单位:mm(默认值:10.0) + // 拒绝非平面簇(例如悬崖、台阶边缘)。 + float maxAngularGap; // 最大角度间隙,单位:度(默认值:90.0) + // 拒绝 L 型或非闭合边界。 + float minInlierRatio; // 椭圆拟合的最小内点比率(默认值:0.0) + // 在拟合椭圆容差范围内的点的比例。 - // Constructor with defaults + // 构造函数,设置默认值 SHoleFilterParams() - : minHoleRadius(1.0f) - , maxHoleRadius(10.0f) - , maxEccentricity(0.99995f) - , maxCornerRatio(0.15f) + : maxEccentricity(0.99995f) , minAngularCoverage(10.f) - , maxRadiusFitRatio(0.3f) - , minQualityScore(0.3f) + , maxRadiusFitRatio(1.f) + , minQualityScore(0.f) , maxPlaneResidual(10.0f) , maxAngularGap(90.0f) - , minInlierRatio(0.3f) + , minInlierRatio(0.f) {} }; /** - * @brief Single hole detection result + * @brief 单个孔洞检测结果 * - * Note: SVzNL3DPointF and SVzNL2DPointF are defined in VZNL_Types.h + * 注意:SVzNL3DPointF 和 SVzNL2DPointF 在 VZNL_Types.h 中定义 */ struct SHoleResult { - SVzNL3DPointF center; // Hole center point (x, y, z) - SVzNL3DPointF normal; // Plane normal vector (unit length) - float radius; // Hole radius in mm - float depth; // Pit depth in mm - float eccentricity; // Eccentricity (0 = perfect circle) - float radiusVariance; // Radius variance in mm - float angularSpan; // Angular coverage in degrees - float qualityScore; // Quality score (0-1, higher = better) + SVzNL3DPointF center; // 孔洞中心点 (x, y, z) + SVzNL3DPointF normal; // 平面法向量(单位长度) + float radius; // 孔洞半径,单位:mm + float depth; // 凹坑深度,单位:mm + float eccentricity; // 离心率(0 = 完美圆形) + float radiusVariance; // 半径方差,单位:mm + float angularSpan; // 角度覆盖范围,单位:度 + float qualityScore; // 质量分数(0-1,越高越好) SHoleResult() : center() @@ -151,16 +113,16 @@ struct SHoleResult { }; /** - * @brief Multiple hole detection result + * @brief 多孔洞检测结果 * - * @note Memory management: The holes array is NOT automatically freed. - * Caller must call FreeMultiHoleResult() or manually delete[] holes. + * @note 内存管理:holes 数组不会自动释放。 + * 调用者必须调用 FreeMultiHoleResult() 或手动 delete[] holes。 */ struct SMultiHoleResult { - int holeCount; // Number of detected holes - SHoleResult* holes; // Array of hole results (caller must free) - int totalCandidates; // Total candidate holes before filtering - int filteredCount; // Number of holes filtered out + int holeCount; // 检测到的孔洞数量 + SHoleResult* holes; // 孔洞结果数组(调用者必须释放) + int totalCandidates; // 过滤前的候选孔洞总数 + int filteredCount; // 被过滤掉的孔洞数量 SMultiHoleResult() : holeCount(0) @@ -171,9 +133,9 @@ struct SMultiHoleResult { }; /** - * @brief Free memory allocated by DetectMultipleHoles + * @brief 释放 DetectMultipleHoles 分配的内存 * - * @param [in,out] result Result structure to free + * @param [in,out] result 要释放的结果结构 */ inline void FreeMultiHoleResult(SMultiHoleResult* result) { if (result != nullptr && result->holes != nullptr) { @@ -183,4 +145,31 @@ inline void FreeMultiHoleResult(SMultiHoleResult* result) { } } +/** + * @brief 线段端点对结构 + * + * 表示在线扫描中检测到的线段,包含起点和终点。 + * 注意:尽管名称为"PeakValley",但此结构存储的是线段端点, + * 不一定是峰值/谷值点。保留此命名是为了兼容性。 + */ +struct SSegmentPair { + SVzNLPointXYZ startPoint; // 线段起点 + SVzNLPointXYZ endPoint; // 线段终点 + int startRow; // 起点行索引 + int startCol; // 起点列索引 + int endRow; // 终点行索引 + int endCol; // 终点列索引 + float depth; // 起点和终点之间的深度差 + + SSegmentPair() + : startPoint() + , endPoint() + , startRow(0) + , startCol(0) + , endRow(0) + , endCol(0) + , depth(0.0f) + {} +}; + #endif // HOLE_DETECTION_PARAMS_H diff --git a/AppAlgo/holeDetection/windows/x64/Debug/HoleDetectionLib.dll b/AppAlgo/holeDetection/windows/x64/Debug/HoleDetectionLib.dll index aa82d6f..87b1bef 100644 Binary files a/AppAlgo/holeDetection/windows/x64/Debug/HoleDetectionLib.dll and b/AppAlgo/holeDetection/windows/x64/Debug/HoleDetectionLib.dll differ diff --git a/AppAlgo/holeDetection/windows/x64/Debug/HoleDetectionLib.pdb b/AppAlgo/holeDetection/windows/x64/Debug/HoleDetectionLib.pdb new file mode 100644 index 0000000..5d03022 Binary files /dev/null and b/AppAlgo/holeDetection/windows/x64/Debug/HoleDetectionLib.pdb differ diff --git a/AppAlgo/holeDetection/windows/x64/Release/HoleDetectionLib.dll b/AppAlgo/holeDetection/windows/x64/Release/HoleDetectionLib.dll index 299d5ce..bd89b6b 100644 Binary files a/AppAlgo/holeDetection/windows/x64/Release/HoleDetectionLib.dll and b/AppAlgo/holeDetection/windows/x64/Release/HoleDetectionLib.dll differ diff --git a/AppUtils/AppCommon/Src/BasePresenter.cpp b/AppUtils/AppCommon/Src/BasePresenter.cpp index 99e6920..5a64a53 100644 --- a/AppUtils/AppCommon/Src/BasePresenter.cpp +++ b/AppUtils/AppCommon/Src/BasePresenter.cpp @@ -1,909 +1,909 @@ -#include "BasePresenter.h" -#include "VrLog.h" -#include "VrError.h" -#include -#include -#include -#include -#include -#include -#include "PathManager.h" -#include "IYModbusTCPServer.h" - -BasePresenter::BasePresenter(QObject *parent) - : QObject(parent) - , m_currentCameraIndex(0) - , m_bCameraConnected(false) - , m_bAlgoDetectThreadRunning(false) - , m_pCameraReconnectTimer(nullptr) -{ - // 创建相机重连定时器 - m_pCameraReconnectTimer = new QTimer(this); - m_pCameraReconnectTimer->setInterval(2000); // 默认2秒 - connect(m_pCameraReconnectTimer, &QTimer::timeout, this, &BasePresenter::OnCameraReconnectTimer); -} - -BasePresenter::~BasePresenter() -{ - // 清除状态回调指针,防止后续回调访问 - m_pStatusCallback = nullptr; - - // 等待初始化线程完成 - if (m_initThread.joinable()) { - m_initThread.join(); - } - - // 处理待处理的 Qt 事件,确保 QueuedConnection 的回调执行时检查到 m_pStatusCallback 为空 - QCoreApplication::processEvents(); - - // 停止检测线程 - StopAlgoDetectThread(); - - // 停止重连定时器 - StopCameraReconnectTimer(); - - // 停止ModbusTCP服务器 - StopModbusServer(); - - // 清理相机设备 - for (size_t i = 0; i < m_vrEyeDeviceList.size(); ++i) { - auto& camera = m_vrEyeDeviceList[i]; - if (camera.second) { - camera.second->CloseDevice(); - delete camera.second; - camera.second = nullptr; - } - } - m_vrEyeDeviceList.clear(); - LOG_INFO("BasePresenter destructor finished\n"); -} - -int BasePresenter::Init() -{ - LOG_INFO("BasePresenter::Init()\n"); - - // 在后台线程中执行初始化 - m_initThread = std::thread([this]() { - int nRet = InitApp(); - if (nRet != SUCCESS) { - LOG_ERROR("InitApp failed: %d\n", nRet); - return; - } - - // 初始化算法参数 - nRet = InitAlgoParams(); - LOG_INFO("Algorithm parameters initialization result: %d\n", nRet); - - // 启动ModbusTCP服务(子类可通过重写 ShouldStartModbusServer() 跳过) - if (ShouldStartModbusServer()) { - int modbusRet = StartModbusServer(5020); - if (modbusRet == SUCCESS) { - LOG_INFO("ModbusTCP server started on port 5020\n"); - } else { - LOG_WARNING("Failed to start ModbusTCP server\n"); - } - } - - SetWorkStatus(WorkStatus::Ready); - }); - - return SUCCESS; -} - -int BasePresenter::StartDetection(int cameraIndex, bool isAuto) -{ - LOG_INFO("[BasePresenter] StartDetection - cameraIndex=%d, isAuto=%d\n", cameraIndex, isAuto); - - // 设置当前相机索引 - if (cameraIndex >= 0 && cameraIndex != -1) { - m_currentCameraIndex = cameraIndex; - } - int currentCamera = m_currentCameraIndex; - - // 检查相机列表是否为空 - if (m_vrEyeDeviceList.empty()) { - LOG_ERROR("[BasePresenter] No camera device found\n"); - return ERR_CODE(DEV_NOT_FIND); - } - - // 清空检测数据缓存 - ClearDetectionDataCache(); - - int nRet = SUCCESS; - - // 启动指定相机(cameraIndex为相机ID,从1开始编号) - int arrayIndex = currentCamera - 1; // 转换为数组索引(从0开始) - - // 检查相机是否连接 - if (arrayIndex < 0 || arrayIndex >= static_cast(m_vrEyeDeviceList.size()) || - m_vrEyeDeviceList[arrayIndex].second == nullptr) { - LOG_ERROR("[BasePresenter] Camera %d is not connected or invalid\n", currentCamera); - return ERR_CODE(DEV_NOT_FIND); - } - - SetWorkStatus(WorkStatus::Working); - - IVrEyeDevice* pDevice = m_vrEyeDeviceList[arrayIndex].second; - - // 获取数据类型(由子类决定) - EVzResultDataType eDataType = GetDetectionDataType(); - - // 设置状态回调 - VzNL_OnNotifyStatusCBEx statusCallback = GetCameraStatusCallback(); - pDevice->SetStatusCallback(statusCallback, this); - - // 获取检测回调函数(由子类提供) - VzNL_AutoOutputLaserLineExCB detectCallback = GetDetectionCallback(); - - // 开始检测 - nRet = pDevice->StartDetect(detectCallback, eDataType, this); - LOG_INFO("[BasePresenter] Camera %d start detection result: %d\n", currentCamera, nRet); - - if (nRet == SUCCESS) { - // 启动算法检测线程 - StartAlgoDetectThread(); - } - LOG_INFO("[BasePresenter] StartDetection finish \n"); - return nRet; -} - -int BasePresenter::StopDetection() -{ - LOG_INFO("[BasePresenter] StopDetection\n"); - - // 停止所有相机的检测 - for (size_t i = 0; i < m_vrEyeDeviceList.size(); ++i) { - IVrEyeDevice* pDevice = m_vrEyeDeviceList[i].second; - if (pDevice) { - int ret = pDevice->StopDetect(); - if (ret == 0) { - LOG_INFO("[BasePresenter] Camera %zu stop detection successfully\n", i + 1); - } else { - LOG_WARNING("[BasePresenter] Camera %zu stop detection failed, error code: %d\n", i + 1, ret); - } - } - } - - // 停止算法检测线程 - StopAlgoDetectThread(); - - return SUCCESS; -} - -int BasePresenter::GetDetectionDataCacheSize() const -{ - std::lock_guard lock(const_cast(m_detectionDataMutex)); - return static_cast(m_detectionDataCache.size()); -} - -int BasePresenter::SaveDetectionDataToFile(const std::string& filePath) -{ std::lock_guard lock(m_detectionDataMutex); - - if(m_detectionDataCache.empty()){ - LOG_WARNING("[BasePresenter] 检测数据缓存为空,无数据可保存\n"); - return ERR_CODE(DATA_ERR_INVALID); - } - - int lineNum = static_cast(m_detectionDataCache.size()); - float scanSpeed = 0.0f; - int maxTimeStamp = 0; - int clockPerSecond = 0; - - int result = m_dataLoader.SaveLaserScanData(filePath, m_detectionDataCache, lineNum, scanSpeed, maxTimeStamp, clockPerSecond); - - if (result == SUCCESS) { - LOG_INFO("[BasePresenter] 成功保存 %d 行检测数据到文件: %s\n", lineNum, filePath.c_str()); - } else { - LOG_ERROR("[BasePresenter] 保存检测数据失败,错误: %s\n", m_dataLoader.GetLastError().c_str()); - } - - return result; -} - -int BasePresenter::LoadDebugDataAndDetect(const std::string& filePath) -{ - SetWorkStatus(WorkStatus::Working); - LOG_INFO("[BasePresenter] Loading debug data from file: %s\n", filePath.c_str()); - - int lineNum = 0; - float scanSpeed = 0.0f; - int maxTimeStamp = 0; - int clockPerSecond = 0; - - int result = SUCCESS; - - // 1. 清空现有的检测数据缓存 - ClearDetectionDataCache(); - - std::string fileName = QFileInfo(QString::fromStdString(filePath)).fileName().toStdString(); - OnStatusUpdate(QString("加载文件:%1").arg(fileName.c_str()).toStdString()); - - // 2. 加载数据到缓存 - { - std::lock_guard lock(m_detectionDataMutex); - result = m_dataLoader.LoadLaserScanData(filePath, m_detectionDataCache, lineNum, scanSpeed, maxTimeStamp, clockPerSecond); - } - - if (result != SUCCESS) { - LOG_ERROR("[BasePresenter] 加载调试数据失败: %s\n", m_dataLoader.GetLastError().c_str()); - OnStatusUpdate("调试数据加载失败"); - return result; - } - - OnStatusUpdate(QString("成功加载 %1 行调试数据").arg(lineNum).toStdString()); - LOG_INFO("[BasePresenter] 成功加载 %d 行调试数据\n", lineNum); - - // 3. 执行检测任务 - result = DetectTask(); - - return result; -} - -void BasePresenter::SetCameraStatusCallback(VzNL_OnNotifyStatusCBEx fNotify, void* param) -{ - for (size_t i = 0; i < m_vrEyeDeviceList.size(); i++) { - IVrEyeDevice* pDevice = m_vrEyeDeviceList[i].second; - if (pDevice) { - pDevice->SetStatusCallback(fNotify, param); - LOG_DEBUG("[BasePresenter] Status callback set for camera %zu\n", i + 1); - } - } -} - -void BasePresenter::SetWorkStatus(WorkStatus status) -{ - if (m_currentWorkStatus != status) { - m_currentWorkStatus = status; - LOG_INFO("[BasePresenter] Work status changed to: %s\n", WorkStatusToString(status).c_str()); - - // 调用虚函数通知子类,子类可以在此调用UI回调 - OnWorkStatusChanged(status); - } -} - -// ============ InitCamera 完整实现 ============ -int BasePresenter::InitCamera(std::vector& cameraList, bool bRGB, bool bSwing) -{ - LOG_INFO("[BasePresenter] InitCamera\n"); - - m_bRGB = bRGB; - m_bSwing = bSwing; - - // 保存相机配置信息,用于重连尝试 - m_expectedList = cameraList; - - // 通知UI相机个数 - int cameraCount = cameraList.size(); - OnCameraCountChanged(cameraCount); - - LOG_INFO("[BasePresenter] init eyedevice list\n"); - // 初始化相机列表,预分配空间 - m_vrEyeDeviceList.resize(cameraCount, std::make_pair("", nullptr)); - for(int i = 0; i < cameraCount; i++) - { - m_vrEyeDeviceList[i] = std::make_pair(cameraList[i].name, nullptr); - } - - LOG_INFO("[BasePresenter] camera count : %d\n", cameraCount); - - // 尝试初始化所有相机 - bool allCamerasConnected = true; - - if(cameraCount > 0){ - // 循环打开所有配置的相机 - for (int i = 0; i < cameraCount; i++) { - int cameraIndex = i + 1; // 相机索引从1开始 - int nRet = OpenDevice(cameraIndex, cameraList[i].name.c_str(), cameraList[i].ip.c_str(), bRGB, bSwing); - - bool isConnected = (nRet == SUCCESS); - - // 通知相机状态变化 - OnCameraStatusChanged(cameraIndex, isConnected); - - if (!isConnected) { - allCamerasConnected = false; - LOG_WARNING("[BasePresenter] 相机%d (%s) 连接失败\n", cameraIndex, cameraList[i].name.c_str()); - } else { - LOG_INFO("[BasePresenter] 相机%d (%s) 连接成功\n", cameraIndex, cameraList[i].name.c_str()); - } - } - } else { - // 没有配置相机,创建一个默认项 - m_vrEyeDeviceList.resize(1, std::make_pair("", nullptr)); - DeviceInfo devInfo; - devInfo.index = 1; - devInfo.ip = ""; - devInfo.name = "相机"; - m_expectedList.push_back(devInfo); - - int nRet = OpenDevice(1, "相机", nullptr, bRGB, bSwing); - if (nRet != SUCCESS) { - allCamerasConnected = false; - } - - // 通知相机状态变化 - OnCameraStatusChanged(1, SUCCESS == nRet); - } - - // 检查连接状态 - int connectedCount = 0; - for (const auto& device : m_vrEyeDeviceList) { - if (device.second != nullptr) { - connectedCount++; - } - } - m_bCameraConnected = (connectedCount > 0); // 至少有一个相机连接成功 - - // 设置默认相机索引为第一个连接的相机 - m_currentCameraIndex = 1; // 默认从1开始 - for (int i = 0; i < static_cast(m_vrEyeDeviceList.size()); i++) { - if (m_vrEyeDeviceList[i].second != nullptr) { - m_currentCameraIndex = i + 1; // 找到第一个连接的相机 - break; - } - } - - LOG_INFO("[BasePresenter] 相机初始化完成: %d/%d 台相机连接成功, 默认相机索引: %d\n", - connectedCount, m_expectedList.size(), m_currentCameraIndex); - - // 如果不是所有期望的相机都连接成功,启动重连定时器 - if (!allCamerasConnected && !m_expectedList.empty()) { - LOG_INFO("[BasePresenter] 部分相机未连接 (%d/%d),启动重连定时器\n", connectedCount, m_expectedList.size()); - StartCameraReconnectTimer(); - } else if (allCamerasConnected) { - LOG_INFO("[BasePresenter] 所有相机连接成功\n"); - // 确保定时器停止 - StopCameraReconnectTimer(); - } else { - LOG_WARNING("[BasePresenter] 没有配置相机 (expectedCount=%d)\n", m_expectedList.size()); - } - - return SUCCESS; -} - -// ============ CreateDevice 默认实现 ============ -int BasePresenter::CreateDevice(IVrEyeDevice** ppDevice) -{ - if (!ppDevice) { - return ERR_CODE(DEV_ARG_INVAILD); - } - - // 默认创建VzNLSDK设备 - IVrEyeDevice::CreateObject(ppDevice); - if (*ppDevice) { - LOG_INFO("[BasePresenter] Created VzNL SDK device (default)\n"); - return SUCCESS; - } - - LOG_ERROR("[BasePresenter] Failed to create VzNL SDK device\n"); - return ERR_CODE(DEV_OPEN_ERR); -} - -// ============ OpenDevice 完整实现 ============ -int BasePresenter::OpenDevice(int cameraIndex, const char* cameraName, const char* cameraIp, bool bRGB, bool bSwing) -{ - LOG_INFO("[BasePresenter] OpenDevice - index %d (%s, %s)\n", - cameraIndex, cameraName, cameraIp ? cameraIp : "NULL"); - - // 1. 通过虚函数创建设备对象(子类可重写以创建不同类型的设备) - IVrEyeDevice* pDevice = nullptr; - int nCreateRet = CreateDevice(&pDevice); - if (nCreateRet != SUCCESS || !pDevice) { - LOG_ERROR("[BasePresenter] Failed to create device object, result: %d\n", nCreateRet); - return ERR_CODE(DEV_OPEN_ERR); - } - - // 2. 初始化设备 - int nRet = pDevice->InitDevice(); - if(nRet != SUCCESS){ - delete pDevice; - LOG_ERROR("[BasePresenter] InitDevice failed, error code: %d\n", nRet); - } - ERR_CODE_RETURN(nRet); - - // 3. 打开相机设备 - nRet = pDevice->OpenDevice(cameraIp, bRGB, bSwing); - LOG_INFO("[BasePresenter] OpenDevice camera %d (%s/%s) result: %d \n", cameraIndex, - bRGB ? "RGB" : "Normal", bSwing ? "Swing" : "Normal", nRet); - - // 4. 处理打开结果 - bool cameraConnected = (SUCCESS == nRet); - if(!cameraConnected){ - delete pDevice; // 释放失败的设备 - pDevice = nullptr; - } else { - - // 设置状态回调(调用子类提供的回调函数) - VzNL_OnNotifyStatusCBEx callback = GetCameraStatusCallback(); - nRet = pDevice->SetStatusCallback(callback, this); - LOG_DEBUG("[BasePresenter] SetStatusCallback result: %d\n", nRet); - if (nRet != SUCCESS) { - delete pDevice; - pDevice = nullptr; - } - } - LOG_DEBUG("[BasePresenter] Camera %d (%s) connected %s\n", cameraIndex, cameraName, cameraConnected ? "success" : "failed"); - - // 6. 存储到设备列表 - int arrIdx = cameraIndex - 1; - if(m_vrEyeDeviceList.size() > static_cast(arrIdx)){ - m_vrEyeDeviceList[arrIdx] = std::make_pair(cameraName, pDevice); - } else { - LOG_WARNING("[BasePresenter] Camera index %d out of range, list size: %zu\n", cameraIndex, m_vrEyeDeviceList.size()); - } - - return nRet; -} - -// ============ AlgoDetectThreadFunc 实现 ============ -void BasePresenter::AlgoDetectThreadFunc() -{ - LOG_INFO("[BasePresenter] 算法检测线程启动\n"); - - while(m_bAlgoDetectThreadRunning) - { - std::unique_lock lock(m_algoDetectMutex); - - // 等待检测触发(子类需要调用 m_algoDetectCondition.notify_one() 来触发) - m_algoDetectCondition.wait(lock); - - if(!m_bAlgoDetectThreadRunning){ - break; - } - - LOG_INFO("[BasePresenter] 检测线程被唤醒,开始执行检测任务\n"); - - // 执行检测任务 - int nRet = DetectTask(); - - if(nRet != SUCCESS){ - LOG_ERROR("[BasePresenter] 检测任务执行失败,错误码: %d\n", nRet); - } else { - LOG_INFO("[BasePresenter] 检测任务执行成功\n"); - } - } - - LOG_INFO("[BasePresenter] 算法检测线程退出\n"); -} - -// ============ DetectTask 实现 ============ -int BasePresenter::DetectTask() -{ - LOG_INFO("[BasePresenter] DetectTask - 开始执行检测任务\n"); - - // 获取调试参数 - VrDebugParam debugParam = GetDebugParam(); - - // 详细日志模式 - if (debugParam.enableDebug && debugParam.printDetailLog) { - LOG_INFO("[BasePresenter] 调试模式已启用\n"); - LOG_INFO("[BasePresenter] - savePointCloud: %s\n", debugParam.savePointCloud ? "true" : "false"); - LOG_INFO("[BasePresenter] - saveDebugImage: %s\n", debugParam.saveDebugImage ? "true" : "false"); - LOG_INFO("[BasePresenter] - debugOutputPath: %s\n", debugParam.debugOutputPath.c_str()); - } - - // 1. 验证检测数据缓存 - { - std::lock_guard lock(m_detectionDataMutex); - if (m_detectionDataCache.empty()) { - LOG_WARNING("[BasePresenter] 检测数据缓存为空\n"); - return ERR_CODE(DEV_DATA_INVALID); - } - LOG_INFO("[BasePresenter] 检测数据缓存大小: %zu\n", m_detectionDataCache.size()); - } - - // 2. 调试模式 - 保存点云数据 - if (debugParam.enableDebug && debugParam.savePointCloud) { - // 确定输出路径 - QString outputPath; - if (debugParam.debugOutputPath.empty()) { - // 默认使用应用程序目录下的 debug 子目录 - outputPath = QCoreApplication::applicationDirPath() + "/debug"; - } else { - outputPath = QString::fromStdString(debugParam.debugOutputPath); - } - - // 确保输出目录存在 - QDir dir(outputPath); - if (!dir.exists()) { - dir.mkpath("."); - } - - // 生成带时间戳的文件名 - QString timestamp = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss"); - QString fileName = QString("%1/pointcloud_%2.txt").arg(outputPath).arg(timestamp); - - if (debugParam.printDetailLog) { - LOG_INFO("[BasePresenter] 保存点云数据到: %s\n", fileName.toStdString().c_str()); - } - - // 保存点云数据 - int saveRet = SaveDetectionDataToFile(fileName.toStdString()); - if (saveRet != SUCCESS) { - LOG_WARNING("[BasePresenter] 保存点云数据失败,错误码: %d\n", saveRet); - } else { - LOG_INFO("[BasePresenter] 点云数据保存成功\n"); - } - } - - // 3. 调用子类实现的算法检测,传入缓存数据引用 - LOG_INFO("[BasePresenter] ProcessAlgoDetection 执行算法检测\n"); - int nRet = ProcessAlgoDetection(m_detectionDataCache); - LOG_INFO("[BasePresenter] ProcessAlgoDetection 执行结果: %d\n", nRet); - - SetWorkStatus(WorkStatus::Completed); - - LOG_INFO("[BasePresenter] DetectTask - 检测任务执行成功\n"); - return nRet; -} - -void BasePresenter::StartAlgoDetectThread() -{ - if (m_bAlgoDetectThreadRunning) { - LOG_WARNING("[BasePresenter] 算法检测线程已经在运行\n"); - return; - } - - m_bAlgoDetectThreadRunning = true; - - // 启动检测线程(不再detach,使用joinable线程) - m_algoDetectThread = std::thread(&BasePresenter::AlgoDetectThreadFunc, this); - - LOG_INFO("[BasePresenter] 算法检测线程已启动\n"); -} - -void BasePresenter::StopAlgoDetectThread() -{ - if (!m_bAlgoDetectThreadRunning) { - return; - } - - LOG_INFO("[BasePresenter] 正在停止算法检测线程...\n"); - - m_bAlgoDetectThreadRunning = false; - - // 唤醒可能在等待的线程 - m_algoDetectCondition.notify_all(); - - // 等待线程退出 - if (m_algoDetectThread.joinable()) { - m_algoDetectThread.join(); - } - - LOG_INFO("[BasePresenter] 算法检测线程已停止\n"); -} - -void BasePresenter::ClearDetectionDataCache() -{ - std::lock_guard lock(m_detectionDataMutex); - m_detectionDataCache.clear(); - LOG_DEBUG("[BasePresenter] 检测数据缓存已清空\n"); -} - -void BasePresenter::AddDetectionDataToCache(EVzResultDataType dataType, const SVzLaserLineData& laserData) -{ - std::lock_guard lock(m_detectionDataMutex); - m_detectionDataCache.push_back(std::make_pair(dataType, laserData)); -} - -// 通用的静态检测数据回调函数实现 -void BasePresenter::_StaticDetectionCallback(EVzResultDataType eDataType, SVzLaserLineData* pLaserLinePoint, void* pUserData) -{ - // 验证输入参数 - if (!pLaserLinePoint) { - LOG_WARNING("[BasePresenter Detection Callback] pLaserLinePoint is null\n"); - return; - } - - if (pLaserLinePoint->nPointCount <= 0) { - LOG_WARNING("[BasePresenter Detection Callback] Point count is zero or negative: %d\n", pLaserLinePoint->nPointCount); - return; - } - - if (!pLaserLinePoint->p3DPoint) { - LOG_WARNING("[BasePresenter Detection Callback] p3DPoint is null\n"); - return; - } - - // 获取 BasePresenter 实例指针 - BasePresenter* pThis = reinterpret_cast(pUserData); - if (!pThis) { - LOG_ERROR("[BasePresenter Detection Callback] pUserData is null\n"); - return; - } - - // 创建 SVzLaserLineData 副本 - SVzLaserLineData lineData; - memset(&lineData, 0, sizeof(SVzLaserLineData)); - - // 根据数据类型分配和复制点云数据 - if (eDataType == keResultDataType_Position) { - // 复制 SVzNL3DPosition 数据 - if (pLaserLinePoint->p3DPoint && pLaserLinePoint->nPointCount > 0) { - lineData.p3DPoint = new SVzNL3DPosition[pLaserLinePoint->nPointCount]; - if (lineData.p3DPoint) { - if(pLaserLinePoint->p3DPoint){ - memcpy(lineData.p3DPoint, pLaserLinePoint->p3DPoint, sizeof(SVzNL3DPosition) * pLaserLinePoint->nPointCount); - } else { - memset(lineData.p3DPoint, 0, sizeof(SVzNL3DPosition) * pLaserLinePoint->nPointCount); - } - } - lineData.p2DPoint = new SVzNL2DPosition[pLaserLinePoint->nPointCount]; - if (lineData.p2DPoint){ - if(pLaserLinePoint->p2DPoint) { - memcpy(lineData.p2DPoint, pLaserLinePoint->p2DPoint, sizeof(SVzNL2DPosition) * pLaserLinePoint->nPointCount); - } else { - memset(lineData.p2DPoint, 0, sizeof(SVzNL2DPosition) * pLaserLinePoint->nPointCount); - } - } - } - } else if (eDataType == keResultDataType_PointXYZRGBA) { - // 复制 SVzNLPointXYZRGBA 数据 - if (pLaserLinePoint->p3DPoint && pLaserLinePoint->nPointCount > 0) { - lineData.p3DPoint = new SVzNLPointXYZRGBA[pLaserLinePoint->nPointCount]; - if (lineData.p3DPoint) { - if(pLaserLinePoint->p3DPoint){ - memcpy(lineData.p3DPoint, pLaserLinePoint->p3DPoint, sizeof(SVzNLPointXYZRGBA) * pLaserLinePoint->nPointCount); - } else { - memset(lineData.p3DPoint, 0, sizeof(SVzNLPointXYZRGBA) * pLaserLinePoint->nPointCount); - } - } - lineData.p2DPoint = new SVzNL2DLRPoint[pLaserLinePoint->nPointCount]; - if (lineData.p2DPoint) { - if(pLaserLinePoint->p2DPoint) { - memcpy(lineData.p2DPoint, pLaserLinePoint->p2DPoint, sizeof(SVzNL2DLRPoint) * pLaserLinePoint->nPointCount); - } else { - memset(lineData.p2DPoint, 0, sizeof(SVzNL2DLRPoint) * pLaserLinePoint->nPointCount); - } - } - } - } - - // 复制其他字段 - lineData.nPointCount = pLaserLinePoint->nPointCount; - lineData.llTimeStamp = pLaserLinePoint->llTimeStamp; - lineData.llFrameIdx = pLaserLinePoint->llFrameIdx; - lineData.nEncodeNo = pLaserLinePoint->nEncodeNo; - lineData.fSwingAngle = pLaserLinePoint->fSwingAngle; - lineData.bEndOnceScan = pLaserLinePoint->bEndOnceScan; - - // 添加到检测数据缓存 - pThis->AddDetectionDataToCache(eDataType, lineData); -} - -// 通用的静态相机状态回调函数实现 -void BasePresenter::_StaticCameraStatusCallback(EVzDeviceWorkStatus eStatus, void* pExtData, unsigned int nDataLength, void* pInfoParam) -{ - LOG_DEBUG("[BasePresenter Camera Status Callback] received: status=%d\n", (int)eStatus); - - // 获取 BasePresenter 实例指针 - BasePresenter* pThis = reinterpret_cast(pInfoParam); - if (!pThis) { - LOG_ERROR("[BasePresenter Camera Status Callback] pInfoParam is null\n"); - return; - } - - switch (eStatus) { - case EVzDeviceWorkStatus::keDeviceWorkStatus_Offline: - { - LOG_WARNING("[BasePresenter Camera Status Callback] Camera device offline/disconnected\n"); - - // 更新相机连接状态 - pThis->m_bCameraConnected = false; - - // 通知子类相机状态变更(这里暂时通知相机1,实际应用中可能需要区分) - pThis->OnCameraStatusChanged(1, false); - break; - } - - case EVzDeviceWorkStatus::keDeviceWorkStatus_Eye_Reconnect: - { - LOG_INFO("[BasePresenter Camera Status Callback] Camera device online/connected\n"); - - // 更新相机连接状态 - pThis->m_bCameraConnected = true; - - // 通知子类相机状态变更 - pThis->OnCameraStatusChanged(1, true); - break; - } - - case EVzDeviceWorkStatus::keDeviceWorkStatus_Device_Swing_Finish: - { - LOG_INFO("[BasePresenter Camera Status Callback] Received scan finish signal from camera\n"); - - // 通知算法检测线程开始处理 - pThis->m_algoDetectCondition.notify_one(); - break; - } - - default: - break; - } -} - -// 相机一直重联 -void BasePresenter::StartCameraReconnectTimer() -{ - LOG_DEBUG("[BasePresenter] StartCameraReconnectTimer called\n"); - - // 使用QMetaObject::invokeMethod确保在正确的线程中操作定时器 - QMetaObject::invokeMethod(this, [this]() { - if (!m_pCameraReconnectTimer) { - return; - } - - if (m_pCameraReconnectTimer->isActive()) { - return; - } - - m_pCameraReconnectTimer->start(); - }, Qt::QueuedConnection); -} - -void BasePresenter::StopCameraReconnectTimer() -{ - // 直接停止定时器(析构时需要立即停止,不能用QueuedConnection) - if (m_pCameraReconnectTimer) { - m_pCameraReconnectTimer->stop(); - } -} - -// ============ OnCameraReconnectTimer 实现 ============ -void BasePresenter::OnCameraReconnectTimer() -{ -#ifdef _WIN32 - return; -#endif - // 调用子类实现的重连逻辑 - bool allConnected = TryReconnectCameras(); - - if (allConnected) { - LOG_INFO("[BasePresenter] 所有相机重连成功,停止定时器\n"); - StopCameraReconnectTimer(); - } -} - -// ============ TryReconnectCameras 默认实现 ============ -bool BasePresenter::TryReconnectCameras() -{ - LOG_DEBUG("[BasePresenter] TryReconnectCameras all %zd \n", m_expectedList.size()); - - bool allConnected = true; - int connectedCount = 0; - - // 遍历所有配置的相机,尝试重连失败的相机 - for (int i = 0; i < static_cast(m_expectedList.size()); i++) { - // 检查该位置的相机是否已连接 - if (i < static_cast(m_vrEyeDeviceList.size()) && m_vrEyeDeviceList[i].second != nullptr) { - // 相机已连接,跳过 - connectedCount++; - continue; - } - - // 尝试重连相机 - int cameraIndex = i + 1; // 相机索引从1开始 - const DeviceInfo& cameraInfo = m_expectedList[i]; - - LOG_DEBUG("[BasePresenter] 尝试重连相机 %d (%s, %s)\n", cameraIndex, cameraInfo.name.c_str(), cameraInfo.ip.c_str()); - - // 调用 OpenDevice 重连(使用初始化时的 RGB/Swing 参数) - int nRet = OpenDevice(cameraIndex, cameraInfo.name.c_str(), cameraInfo.ip.c_str(), m_bRGB, m_bSwing); - - OnCameraStatusChanged(cameraIndex, SUCCESS == nRet); - if (nRet == SUCCESS) { - LOG_INFO("[BasePresenter] 相机 %d (%s) 重连成功\n", cameraIndex, cameraInfo.name.c_str()); - connectedCount++; - } else { - LOG_DEBUG("[BasePresenter] 相机 %d (%s) 重连失败,错误码: %d\n", cameraIndex, cameraInfo.name.c_str(), nRet); - allConnected = false; - } - } - - // 更新相机连接状态 - m_bCameraConnected = (connectedCount > 0); - - // 更新默认相机索引为第一个连接的相机 - for (int i = 0; i < static_cast(m_vrEyeDeviceList.size()); i++) { - if (m_vrEyeDeviceList[i].second != nullptr) { - m_currentCameraIndex = i + 1; - break; - } - } - - LOG_INFO("[BasePresenter] 相机重连尝试完成: %d/%d 台相机已连接\n", connectedCount, m_expectedList.size()); - - return (connectedCount == m_expectedList.size() && allConnected); -} - -// ============ ModbusTCP 服务实现 ============ - -int BasePresenter::StartModbusServer(int port) -{ - LOG_INFO("[BasePresenter] 启动ModbusTCP服务器,端口: %d\n", port); - - // 如果已经运行,先停止 - if (m_modbusServer) { - StopModbusServer(); - } - - // 创建ModbusTCP服务器实例 - if (!IYModbusTCPServer::CreateInstance(&m_modbusServer)) { - LOG_ERROR("[BasePresenter] 创建ModbusTCP服务器实例失败\n"); - return ERR_CODE(DEV_OPEN_ERR); - } - - // 设置写寄存器回调 - m_modbusServer->setWriteRegistersCallback( - [this](uint8_t unitId, uint16_t startAddress, uint16_t quantity, const uint16_t* values) -> IYModbusTCPServer::ErrorCode { - int ret = this->OnModbusWriteRegisters(unitId, startAddress, quantity, values); - return (ret == 0) ? IYModbusTCPServer::ErrorCode::SUCCESS : IYModbusTCPServer::ErrorCode::SERVER_FAILURE; - } - ); - - // 设置连接状态回调 - m_modbusServer->setConnectionStatusCallback( - [this](bool isConnected) { - LOG_INFO("[BasePresenter] Modbus客户端%s\n", isConnected ? "已连接" : "已断开"); - // 通知子类 ModbusTCP 连接状态变化 - this->OnModbusServerStatusChanged(isConnected); - } - ); - - // 启动服务器 - int ret = m_modbusServer->start(port, 5); - if (ret != 0) { - LOG_ERROR("[BasePresenter] 启动ModbusTCP服务器失败,错误码: %d\n", ret); - delete m_modbusServer; - m_modbusServer = nullptr; - return ERR_CODE(DEV_OPEN_ERR); - } - - LOG_INFO("[BasePresenter] ModbusTCP服务器启动成功\n"); - return SUCCESS; -} - -void BasePresenter::StopModbusServer() -{ - if (m_modbusServer) { - m_modbusServer->stop(); - delete m_modbusServer; - m_modbusServer = nullptr; - } -} - -bool BasePresenter::IsModbusServerRunning() const -{ - return m_modbusServer != nullptr; -} - -int BasePresenter::WriteModbusRegisters(uint16_t startAddress, const uint16_t* data, uint16_t count) -{ - if (!m_modbusServer) { - LOG_WARNING("[BasePresenter] ModbusTCP服务器未运行\n"); - return ERR_CODE(DEV_NOT_FIND); - } - - if (!data || count == 0) { - LOG_WARNING("[BasePresenter] 无效的Modbus写入参数\n"); - return ERR_CODE(DEV_DATA_INVALID); - } - - // 转换为vector并写入 - std::vector values(data, data + count); - m_modbusServer->updateHoldingRegisters(startAddress, values); - - LOG_DEBUG("[BasePresenter] Modbus写入: 地址=%d, 数量=%d\n", startAddress, count); - return SUCCESS; -} - -int BasePresenter::OnModbusWriteRegisters(uint8_t unitId, uint16_t startAddress, - uint16_t quantity, const uint16_t* values) -{ - LOG_DEBUG("[BasePresenter] Modbus收到写寄存器: unitId=%d, 地址=%d, 数量=%d\n", unitId, startAddress, quantity); - - // 调用虚函数让子类处理 - OnModbusWriteCallback(startAddress, values, quantity); - - return 0; -} +#include "BasePresenter.h" +#include "VrLog.h" +#include "VrError.h" +#include +#include +#include +#include +#include +#include +#include "PathManager.h" +#include "IYModbusTCPServer.h" + +BasePresenter::BasePresenter(QObject *parent) + : QObject(parent) + , m_currentCameraIndex(0) + , m_bCameraConnected(false) + , m_bAlgoDetectThreadRunning(false) + , m_pCameraReconnectTimer(nullptr) +{ + // 创建相机重连定时器 + m_pCameraReconnectTimer = new QTimer(this); + m_pCameraReconnectTimer->setInterval(2000); // 默认2秒 + connect(m_pCameraReconnectTimer, &QTimer::timeout, this, &BasePresenter::OnCameraReconnectTimer); +} + +BasePresenter::~BasePresenter() +{ + // 清除状态回调指针,防止后续回调访问 + m_pStatusCallback = nullptr; + + // 等待初始化线程完成 + if (m_initThread.joinable()) { + m_initThread.join(); + } + + // 处理待处理的 Qt 事件,确保 QueuedConnection 的回调执行时检查到 m_pStatusCallback 为空 + QCoreApplication::processEvents(); + + // 停止检测线程 + StopAlgoDetectThread(); + + // 停止重连定时器 + StopCameraReconnectTimer(); + + // 停止ModbusTCP服务器 + StopModbusServer(); + + // 清理相机设备 + for (size_t i = 0; i < m_vrEyeDeviceList.size(); ++i) { + auto& camera = m_vrEyeDeviceList[i]; + if (camera.second) { + camera.second->CloseDevice(); + delete camera.second; + camera.second = nullptr; + } + } + m_vrEyeDeviceList.clear(); + LOG_INFO("BasePresenter destructor finished\n"); +} + +int BasePresenter::Init() +{ + LOG_INFO("BasePresenter::Init()\n"); + + // 在后台线程中执行初始化 + m_initThread = std::thread([this]() { + int nRet = InitApp(); + if (nRet != SUCCESS) { + LOG_ERROR("InitApp failed: %d\n", nRet); + return; + } + + // 初始化算法参数 + nRet = InitAlgoParams(); + LOG_INFO("Algorithm parameters initialization result: %d\n", nRet); + + // 启动ModbusTCP服务(子类可通过重写 ShouldStartModbusServer() 跳过) + if (ShouldStartModbusServer()) { + int modbusRet = StartModbusServer(5020); + if (modbusRet == SUCCESS) { + LOG_INFO("ModbusTCP server started on port 5020\n"); + } else { + LOG_WARNING("Failed to start ModbusTCP server\n"); + } + } + + SetWorkStatus(WorkStatus::Ready); + }); + + return SUCCESS; +} + +int BasePresenter::StartDetection(int cameraIndex, bool isAuto) +{ + LOG_INFO("[BasePresenter] StartDetection - cameraIndex=%d, isAuto=%d\n", cameraIndex, isAuto); + + // 设置当前相机索引 + if (cameraIndex >= 0 && cameraIndex != -1) { + m_currentCameraIndex = cameraIndex; + } + int currentCamera = m_currentCameraIndex; + + // 检查相机列表是否为空 + if (m_vrEyeDeviceList.empty()) { + LOG_ERROR("[BasePresenter] No camera device found\n"); + return ERR_CODE(DEV_NOT_FIND); + } + + // 清空检测数据缓存 + ClearDetectionDataCache(); + + int nRet = SUCCESS; + + // 启动指定相机(cameraIndex为相机ID,从1开始编号) + int arrayIndex = currentCamera - 1; // 转换为数组索引(从0开始) + + // 检查相机是否连接 + if (arrayIndex < 0 || arrayIndex >= static_cast(m_vrEyeDeviceList.size()) || + m_vrEyeDeviceList[arrayIndex].second == nullptr) { + LOG_ERROR("[BasePresenter] Camera %d is not connected or invalid\n", currentCamera); + return ERR_CODE(DEV_NOT_FIND); + } + + SetWorkStatus(WorkStatus::Working); + + IVrEyeDevice* pDevice = m_vrEyeDeviceList[arrayIndex].second; + + // 获取数据类型(由子类决定) + EVzResultDataType eDataType = GetDetectionDataType(); + + // 设置状态回调 + VzNL_OnNotifyStatusCBEx statusCallback = GetCameraStatusCallback(); + pDevice->SetStatusCallback(statusCallback, this); + + // 获取检测回调函数(由子类提供) + VzNL_AutoOutputLaserLineExCB detectCallback = GetDetectionCallback(); + + // 开始检测 + nRet = pDevice->StartDetect(detectCallback, eDataType, this); + LOG_INFO("[BasePresenter] Camera %d start detection result: %d\n", currentCamera, nRet); + + if (nRet == SUCCESS) { + // 启动算法检测线程 + StartAlgoDetectThread(); + } + LOG_INFO("[BasePresenter] StartDetection finish \n"); + return nRet; +} + +int BasePresenter::StopDetection() +{ + LOG_INFO("[BasePresenter] StopDetection\n"); + + // 停止所有相机的检测 + for (size_t i = 0; i < m_vrEyeDeviceList.size(); ++i) { + IVrEyeDevice* pDevice = m_vrEyeDeviceList[i].second; + if (pDevice) { + int ret = pDevice->StopDetect(); + if (ret == 0) { + LOG_INFO("[BasePresenter] Camera %zu stop detection successfully\n", i + 1); + } else { + LOG_WARNING("[BasePresenter] Camera %zu stop detection failed, error code: %d\n", i + 1, ret); + } + } + } + + // 停止算法检测线程 + StopAlgoDetectThread(); + + return SUCCESS; +} + +int BasePresenter::GetDetectionDataCacheSize() const +{ + std::lock_guard lock(const_cast(m_detectionDataMutex)); + return static_cast(m_detectionDataCache.size()); +} + +int BasePresenter::SaveDetectionDataToFile(const std::string& filePath) +{ std::lock_guard lock(m_detectionDataMutex); + + if(m_detectionDataCache.empty()){ + LOG_WARNING("[BasePresenter] 检测数据缓存为空,无数据可保存\n"); + return ERR_CODE(DATA_ERR_INVALID); + } + + int lineNum = static_cast(m_detectionDataCache.size()); + float scanSpeed = 0.0f; + int maxTimeStamp = 0; + int clockPerSecond = 0; + + int result = m_dataLoader.SaveLaserScanData(filePath, m_detectionDataCache, lineNum, scanSpeed, maxTimeStamp, clockPerSecond); + + if (result == SUCCESS) { + LOG_INFO("[BasePresenter] 成功保存 %d 行检测数据到文件: %s\n", lineNum, filePath.c_str()); + } else { + LOG_ERROR("[BasePresenter] 保存检测数据失败,错误: %s\n", m_dataLoader.GetLastError().c_str()); + } + + return result; +} + +int BasePresenter::LoadDebugDataAndDetect(const std::string& filePath) +{ + SetWorkStatus(WorkStatus::Working); + LOG_INFO("[BasePresenter] Loading debug data from file: %s\n", filePath.c_str()); + + int lineNum = 0; + float scanSpeed = 0.0f; + int maxTimeStamp = 0; + int clockPerSecond = 0; + + int result = SUCCESS; + + // 1. 清空现有的检测数据缓存 + ClearDetectionDataCache(); + + std::string fileName = QFileInfo(QString::fromStdString(filePath)).fileName().toStdString(); + OnStatusUpdate(QString("加载文件:%1").arg(fileName.c_str()).toStdString()); + + // 2. 加载数据到缓存 + { + std::lock_guard lock(m_detectionDataMutex); + result = m_dataLoader.LoadLaserScanData(filePath, m_detectionDataCache, lineNum, scanSpeed, maxTimeStamp, clockPerSecond); + } + + if (result != SUCCESS) { + LOG_ERROR("[BasePresenter] 加载调试数据失败: %s\n", m_dataLoader.GetLastError().c_str()); + OnStatusUpdate("调试数据加载失败"); + return result; + } + + OnStatusUpdate(QString("成功加载 %1 行调试数据").arg(lineNum).toStdString()); + LOG_INFO("[BasePresenter] 成功加载 %d 行调试数据\n", lineNum); + + // 3. 执行检测任务 + result = DetectTask(); + + return result; +} + +void BasePresenter::SetCameraStatusCallback(VzNL_OnNotifyStatusCBEx fNotify, void* param) +{ + for (size_t i = 0; i < m_vrEyeDeviceList.size(); i++) { + IVrEyeDevice* pDevice = m_vrEyeDeviceList[i].second; + if (pDevice) { + pDevice->SetStatusCallback(fNotify, param); + LOG_DEBUG("[BasePresenter] Status callback set for camera %zu\n", i + 1); + } + } +} + +void BasePresenter::SetWorkStatus(WorkStatus status) +{ + if (m_currentWorkStatus != status) { + m_currentWorkStatus = status; + LOG_INFO("[BasePresenter] Work status changed to: %s\n", WorkStatusToString(status).c_str()); + + // 调用虚函数通知子类,子类可以在此调用UI回调 + OnWorkStatusChanged(status); + } +} + +// ============ InitCamera 完整实现 ============ +int BasePresenter::InitCamera(std::vector& cameraList, bool bRGB, bool bSwing) +{ + LOG_INFO("[BasePresenter] InitCamera\n"); + + m_bRGB = bRGB; + m_bSwing = bSwing; + + // 保存相机配置信息,用于重连尝试 + m_expectedList = cameraList; + + // 通知UI相机个数 + int cameraCount = cameraList.size(); + OnCameraCountChanged(cameraCount); + + LOG_INFO("[BasePresenter] init eyedevice list\n"); + // 初始化相机列表,预分配空间 + m_vrEyeDeviceList.resize(cameraCount, std::make_pair("", nullptr)); + for(int i = 0; i < cameraCount; i++) + { + m_vrEyeDeviceList[i] = std::make_pair(cameraList[i].name, nullptr); + } + + LOG_INFO("[BasePresenter] camera count : %d\n", cameraCount); + + // 尝试初始化所有相机 + bool allCamerasConnected = true; + + if(cameraCount > 0){ + // 循环打开所有配置的相机 + for (int i = 0; i < cameraCount; i++) { + int cameraIndex = i + 1; // 相机索引从1开始 + int nRet = OpenDevice(cameraIndex, cameraList[i].name.c_str(), cameraList[i].ip.c_str(), bRGB, bSwing); + + bool isConnected = (nRet == SUCCESS); + + // 通知相机状态变化 + OnCameraStatusChanged(cameraIndex, isConnected); + + if (!isConnected) { + allCamerasConnected = false; + LOG_WARNING("[BasePresenter] 相机%d (%s) 连接失败\n", cameraIndex, cameraList[i].name.c_str()); + } else { + LOG_INFO("[BasePresenter] 相机%d (%s) 连接成功\n", cameraIndex, cameraList[i].name.c_str()); + } + } + } else { + // 没有配置相机,创建一个默认项 + m_vrEyeDeviceList.resize(1, std::make_pair("", nullptr)); + DeviceInfo devInfo; + devInfo.index = 1; + devInfo.ip = ""; + devInfo.name = "相机"; + m_expectedList.push_back(devInfo); + + int nRet = OpenDevice(1, "相机", nullptr, bRGB, bSwing); + if (nRet != SUCCESS) { + allCamerasConnected = false; + } + + // 通知相机状态变化 + OnCameraStatusChanged(1, SUCCESS == nRet); + } + + // 检查连接状态 + int connectedCount = 0; + for (const auto& device : m_vrEyeDeviceList) { + if (device.second != nullptr) { + connectedCount++; + } + } + m_bCameraConnected = (connectedCount > 0); // 至少有一个相机连接成功 + + // 设置默认相机索引为第一个连接的相机 + m_currentCameraIndex = 1; // 默认从1开始 + for (int i = 0; i < static_cast(m_vrEyeDeviceList.size()); i++) { + if (m_vrEyeDeviceList[i].second != nullptr) { + m_currentCameraIndex = i + 1; // 找到第一个连接的相机 + break; + } + } + + LOG_INFO("[BasePresenter] 相机初始化完成: %d/%d 台相机连接成功, 默认相机索引: %d\n", + connectedCount, m_expectedList.size(), m_currentCameraIndex); + + // 如果不是所有期望的相机都连接成功,启动重连定时器 + if (!allCamerasConnected && !m_expectedList.empty()) { + LOG_INFO("[BasePresenter] 部分相机未连接 (%d/%d),启动重连定时器\n", connectedCount, m_expectedList.size()); + StartCameraReconnectTimer(); + } else if (allCamerasConnected) { + LOG_INFO("[BasePresenter] 所有相机连接成功\n"); + // 确保定时器停止 + StopCameraReconnectTimer(); + } else { + LOG_WARNING("[BasePresenter] 没有配置相机 (expectedCount=%d)\n", m_expectedList.size()); + } + + return SUCCESS; +} + +// ============ CreateDevice 默认实现 ============ +int BasePresenter::CreateDevice(IVrEyeDevice** ppDevice) +{ + if (!ppDevice) { + return ERR_CODE(DEV_ARG_INVAILD); + } + + // 默认创建VzNLSDK设备 + IVrEyeDevice::CreateObject(ppDevice); + if (*ppDevice) { + LOG_INFO("[BasePresenter] Created VzNL SDK device (default)\n"); + return SUCCESS; + } + + LOG_ERROR("[BasePresenter] Failed to create VzNL SDK device\n"); + return ERR_CODE(DEV_OPEN_ERR); +} + +// ============ OpenDevice 完整实现 ============ +int BasePresenter::OpenDevice(int cameraIndex, const char* cameraName, const char* cameraIp, bool bRGB, bool bSwing) +{ + LOG_INFO("[BasePresenter] OpenDevice - index %d (%s, %s)\n", + cameraIndex, cameraName, cameraIp ? cameraIp : "NULL"); + + // 1. 通过虚函数创建设备对象(子类可重写以创建不同类型的设备) + IVrEyeDevice* pDevice = nullptr; + int nCreateRet = CreateDevice(&pDevice); + if (nCreateRet != SUCCESS || !pDevice) { + LOG_ERROR("[BasePresenter] Failed to create device object, result: %d\n", nCreateRet); + return ERR_CODE(DEV_OPEN_ERR); + } + + // 2. 初始化设备 + int nRet = pDevice->InitDevice(); + if(nRet != SUCCESS){ + delete pDevice; + LOG_ERROR("[BasePresenter] InitDevice failed, error code: %d\n", nRet); + } + ERR_CODE_RETURN(nRet); + + // 3. 打开相机设备 + nRet = pDevice->OpenDevice(cameraIp, bRGB, bSwing); + LOG_INFO("[BasePresenter] OpenDevice camera %d (%s/%s) result: %d \n", cameraIndex, + bRGB ? "RGB" : "Normal", bSwing ? "Swing" : "Normal", nRet); + + // 4. 处理打开结果 + bool cameraConnected = (SUCCESS == nRet); + if(!cameraConnected){ + delete pDevice; // 释放失败的设备 + pDevice = nullptr; + } else { + + // 设置状态回调(调用子类提供的回调函数) + VzNL_OnNotifyStatusCBEx callback = GetCameraStatusCallback(); + nRet = pDevice->SetStatusCallback(callback, this); + LOG_DEBUG("[BasePresenter] SetStatusCallback result: %d\n", nRet); + if (nRet != SUCCESS) { + delete pDevice; + pDevice = nullptr; + } + } + LOG_DEBUG("[BasePresenter] Camera %d (%s) connected %s\n", cameraIndex, cameraName, cameraConnected ? "success" : "failed"); + + // 6. 存储到设备列表 + int arrIdx = cameraIndex - 1; + if(m_vrEyeDeviceList.size() > static_cast(arrIdx)){ + m_vrEyeDeviceList[arrIdx] = std::make_pair(cameraName, pDevice); + } else { + LOG_WARNING("[BasePresenter] Camera index %d out of range, list size: %zu\n", cameraIndex, m_vrEyeDeviceList.size()); + } + + return nRet; +} + +// ============ AlgoDetectThreadFunc 实现 ============ +void BasePresenter::AlgoDetectThreadFunc() +{ + LOG_INFO("[BasePresenter] 算法检测线程启动\n"); + + while(m_bAlgoDetectThreadRunning) + { + std::unique_lock lock(m_algoDetectMutex); + + // 等待检测触发(子类需要调用 m_algoDetectCondition.notify_one() 来触发) + m_algoDetectCondition.wait(lock); + + if(!m_bAlgoDetectThreadRunning){ + break; + } + + LOG_INFO("[BasePresenter] 检测线程被唤醒,开始执行检测任务\n"); + + // 执行检测任务 + int nRet = DetectTask(); + + if(nRet != SUCCESS){ + LOG_ERROR("[BasePresenter] 检测任务执行失败,错误码: %d\n", nRet); + } else { + LOG_INFO("[BasePresenter] 检测任务执行成功\n"); + } + } + + LOG_INFO("[BasePresenter] 算法检测线程退出\n"); +} + +// ============ DetectTask 实现 ============ +int BasePresenter::DetectTask() +{ + LOG_INFO("[BasePresenter] DetectTask - 开始执行检测任务\n"); + + // 获取调试参数 + VrDebugParam debugParam = GetDebugParam(); + + // 详细日志模式 + if (debugParam.enableDebug && debugParam.printDetailLog) { + LOG_INFO("[BasePresenter] 调试模式已启用\n"); + LOG_INFO("[BasePresenter] - savePointCloud: %s\n", debugParam.savePointCloud ? "true" : "false"); + LOG_INFO("[BasePresenter] - saveDebugImage: %s\n", debugParam.saveDebugImage ? "true" : "false"); + LOG_INFO("[BasePresenter] - debugOutputPath: %s\n", debugParam.debugOutputPath.c_str()); + } + + // 1. 验证检测数据缓存 + { + std::lock_guard lock(m_detectionDataMutex); + if (m_detectionDataCache.empty()) { + LOG_WARNING("[BasePresenter] 检测数据缓存为空\n"); + return ERR_CODE(DEV_DATA_INVALID); + } + LOG_INFO("[BasePresenter] 检测数据缓存大小: %zu\n", m_detectionDataCache.size()); + } + + // 2. 调试模式 - 保存点云数据 + if (debugParam.enableDebug && debugParam.savePointCloud) { + // 确定输出路径 + QString outputPath; + if (debugParam.debugOutputPath.empty()) { + // 默认使用应用程序目录下的 debug 子目录 + outputPath = QCoreApplication::applicationDirPath() + "/debug"; + } else { + outputPath = QString::fromStdString(debugParam.debugOutputPath); + } + + // 确保输出目录存在 + QDir dir(outputPath); + if (!dir.exists()) { + dir.mkpath("."); + } + + // 生成带时间戳的文件名 + QString timestamp = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss"); + QString fileName = QString("%1/pointcloud_%2.txt").arg(outputPath).arg(timestamp); + + if (debugParam.printDetailLog) { + LOG_INFO("[BasePresenter] 保存点云数据到: %s\n", fileName.toStdString().c_str()); + } + + // 保存点云数据 + int saveRet = SaveDetectionDataToFile(fileName.toStdString()); + if (saveRet != SUCCESS) { + LOG_WARNING("[BasePresenter] 保存点云数据失败,错误码: %d\n", saveRet); + } else { + LOG_INFO("[BasePresenter] 点云数据保存成功\n"); + } + } + + // 3. 调用子类实现的算法检测,传入缓存数据引用 + LOG_INFO("[BasePresenter] ProcessAlgoDetection 执行算法检测\n"); + int nRet = ProcessAlgoDetection(m_detectionDataCache); + LOG_INFO("[BasePresenter] ProcessAlgoDetection 执行结果: %d\n", nRet); + + SetWorkStatus(WorkStatus::Completed); + + LOG_INFO("[BasePresenter] DetectTask - 检测任务执行成功\n"); + return nRet; +} + +void BasePresenter::StartAlgoDetectThread() +{ + if (m_bAlgoDetectThreadRunning) { + LOG_WARNING("[BasePresenter] 算法检测线程已经在运行\n"); + return; + } + + m_bAlgoDetectThreadRunning = true; + + // 启动检测线程(不再detach,使用joinable线程) + m_algoDetectThread = std::thread(&BasePresenter::AlgoDetectThreadFunc, this); + + LOG_INFO("[BasePresenter] 算法检测线程已启动\n"); +} + +void BasePresenter::StopAlgoDetectThread() +{ + if (!m_bAlgoDetectThreadRunning) { + return; + } + + LOG_INFO("[BasePresenter] 正在停止算法检测线程...\n"); + + m_bAlgoDetectThreadRunning = false; + + // 唤醒可能在等待的线程 + m_algoDetectCondition.notify_all(); + + // 等待线程退出 + if (m_algoDetectThread.joinable()) { + m_algoDetectThread.join(); + } + + LOG_INFO("[BasePresenter] 算法检测线程已停止\n"); +} + +void BasePresenter::ClearDetectionDataCache() +{ + std::lock_guard lock(m_detectionDataMutex); + m_detectionDataCache.clear(); + LOG_DEBUG("[BasePresenter] 检测数据缓存已清空\n"); +} + +void BasePresenter::AddDetectionDataToCache(EVzResultDataType dataType, const SVzLaserLineData& laserData) +{ + std::lock_guard lock(m_detectionDataMutex); + m_detectionDataCache.push_back(std::make_pair(dataType, laserData)); +} + +// 通用的静态检测数据回调函数实现 +void BasePresenter::_StaticDetectionCallback(EVzResultDataType eDataType, SVzLaserLineData* pLaserLinePoint, void* pUserData) +{ + // 验证输入参数 + if (!pLaserLinePoint) { + LOG_WARNING("[BasePresenter Detection Callback] pLaserLinePoint is null\n"); + return; + } + + if (pLaserLinePoint->nPointCount <= 0) { + LOG_WARNING("[BasePresenter Detection Callback] Point count is zero or negative: %d\n", pLaserLinePoint->nPointCount); + return; + } + + if (!pLaserLinePoint->p3DPoint) { + LOG_WARNING("[BasePresenter Detection Callback] p3DPoint is null\n"); + return; + } + + // 获取 BasePresenter 实例指针 + BasePresenter* pThis = reinterpret_cast(pUserData); + if (!pThis) { + LOG_ERROR("[BasePresenter Detection Callback] pUserData is null\n"); + return; + } + + // 创建 SVzLaserLineData 副本 + SVzLaserLineData lineData; + memset(&lineData, 0, sizeof(SVzLaserLineData)); + + // 根据数据类型分配和复制点云数据 + if (eDataType == keResultDataType_Position) { + // 复制 SVzNL3DPosition 数据 + if (pLaserLinePoint->p3DPoint && pLaserLinePoint->nPointCount > 0) { + lineData.p3DPoint = new SVzNL3DPosition[pLaserLinePoint->nPointCount]; + if (lineData.p3DPoint) { + if(pLaserLinePoint->p3DPoint){ + memcpy(lineData.p3DPoint, pLaserLinePoint->p3DPoint, sizeof(SVzNL3DPosition) * pLaserLinePoint->nPointCount); + } else { + memset(lineData.p3DPoint, 0, sizeof(SVzNL3DPosition) * pLaserLinePoint->nPointCount); + } + } + lineData.p2DPoint = new SVzNL2DPosition[pLaserLinePoint->nPointCount]; + if (lineData.p2DPoint){ + if(pLaserLinePoint->p2DPoint) { + memcpy(lineData.p2DPoint, pLaserLinePoint->p2DPoint, sizeof(SVzNL2DPosition) * pLaserLinePoint->nPointCount); + } else { + memset(lineData.p2DPoint, 0, sizeof(SVzNL2DPosition) * pLaserLinePoint->nPointCount); + } + } + } + } else if (eDataType == keResultDataType_PointXYZRGBA) { + // 复制 SVzNLPointXYZRGBA 数据 + if (pLaserLinePoint->p3DPoint && pLaserLinePoint->nPointCount > 0) { + lineData.p3DPoint = new SVzNLPointXYZRGBA[pLaserLinePoint->nPointCount]; + if (lineData.p3DPoint) { + if(pLaserLinePoint->p3DPoint){ + memcpy(lineData.p3DPoint, pLaserLinePoint->p3DPoint, sizeof(SVzNLPointXYZRGBA) * pLaserLinePoint->nPointCount); + } else { + memset(lineData.p3DPoint, 0, sizeof(SVzNLPointXYZRGBA) * pLaserLinePoint->nPointCount); + } + } + lineData.p2DPoint = new SVzNL2DLRPoint[pLaserLinePoint->nPointCount]; + if (lineData.p2DPoint) { + if(pLaserLinePoint->p2DPoint) { + memcpy(lineData.p2DPoint, pLaserLinePoint->p2DPoint, sizeof(SVzNL2DLRPoint) * pLaserLinePoint->nPointCount); + } else { + memset(lineData.p2DPoint, 0, sizeof(SVzNL2DLRPoint) * pLaserLinePoint->nPointCount); + } + } + } + } + + // 复制其他字段 + lineData.nPointCount = pLaserLinePoint->nPointCount; + lineData.llTimeStamp = pLaserLinePoint->llTimeStamp; + lineData.llFrameIdx = pLaserLinePoint->llFrameIdx; + lineData.nEncodeNo = pLaserLinePoint->nEncodeNo; + lineData.fSwingAngle = pLaserLinePoint->fSwingAngle; + lineData.bEndOnceScan = pLaserLinePoint->bEndOnceScan; + + // 添加到检测数据缓存 + pThis->AddDetectionDataToCache(eDataType, lineData); +} + +// 通用的静态相机状态回调函数实现 +void BasePresenter::_StaticCameraStatusCallback(EVzDeviceWorkStatus eStatus, void* pExtData, unsigned int nDataLength, void* pInfoParam) +{ + LOG_DEBUG("[BasePresenter Camera Status Callback] received: status=%d\n", (int)eStatus); + + // 获取 BasePresenter 实例指针 + BasePresenter* pThis = reinterpret_cast(pInfoParam); + if (!pThis) { + LOG_ERROR("[BasePresenter Camera Status Callback] pInfoParam is null\n"); + return; + } + + switch (eStatus) { + case EVzDeviceWorkStatus::keDeviceWorkStatus_Offline: + { + LOG_WARNING("[BasePresenter Camera Status Callback] Camera device offline/disconnected\n"); + + // 更新相机连接状态 + pThis->m_bCameraConnected = false; + + // 通知子类相机状态变更(这里暂时通知相机1,实际应用中可能需要区分) + pThis->OnCameraStatusChanged(1, false); + break; + } + + case EVzDeviceWorkStatus::keDeviceWorkStatus_Eye_Reconnect: + { + LOG_INFO("[BasePresenter Camera Status Callback] Camera device online/connected\n"); + + // 更新相机连接状态 + pThis->m_bCameraConnected = true; + + // 通知子类相机状态变更 + pThis->OnCameraStatusChanged(1, true); + break; + } + + case EVzDeviceWorkStatus::keDeviceWorkStatus_Device_Swing_Finish: + { + LOG_INFO("[BasePresenter Camera Status Callback] Received scan finish signal from camera\n"); + + // 通知算法检测线程开始处理 + pThis->m_algoDetectCondition.notify_one(); + break; + } + + default: + break; + } +} + +// 相机一直重联 +void BasePresenter::StartCameraReconnectTimer() +{ + LOG_DEBUG("[BasePresenter] StartCameraReconnectTimer called\n"); + + // 使用QMetaObject::invokeMethod确保在正确的线程中操作定时器 + QMetaObject::invokeMethod(this, [this]() { + if (!m_pCameraReconnectTimer) { + return; + } + + if (m_pCameraReconnectTimer->isActive()) { + return; + } + + m_pCameraReconnectTimer->start(); + }, Qt::QueuedConnection); +} + +void BasePresenter::StopCameraReconnectTimer() +{ + // 直接停止定时器(析构时需要立即停止,不能用QueuedConnection) + if (m_pCameraReconnectTimer) { + m_pCameraReconnectTimer->stop(); + } +} + +// ============ OnCameraReconnectTimer 实现 ============ +void BasePresenter::OnCameraReconnectTimer() +{ +#ifdef _WIN32 + return; +#endif + // 调用子类实现的重连逻辑 + bool allConnected = TryReconnectCameras(); + + if (allConnected) { + LOG_INFO("[BasePresenter] 所有相机重连成功,停止定时器\n"); + StopCameraReconnectTimer(); + } +} + +// ============ TryReconnectCameras 默认实现 ============ +bool BasePresenter::TryReconnectCameras() +{ + LOG_DEBUG("[BasePresenter] TryReconnectCameras all %zd \n", m_expectedList.size()); + + bool allConnected = true; + int connectedCount = 0; + + // 遍历所有配置的相机,尝试重连失败的相机 + for (int i = 0; i < static_cast(m_expectedList.size()); i++) { + // 检查该位置的相机是否已连接 + if (i < static_cast(m_vrEyeDeviceList.size()) && m_vrEyeDeviceList[i].second != nullptr) { + // 相机已连接,跳过 + connectedCount++; + continue; + } + + // 尝试重连相机 + int cameraIndex = i + 1; // 相机索引从1开始 + const DeviceInfo& cameraInfo = m_expectedList[i]; + + LOG_DEBUG("[BasePresenter] 尝试重连相机 %d (%s, %s)\n", cameraIndex, cameraInfo.name.c_str(), cameraInfo.ip.c_str()); + + // 调用 OpenDevice 重连(使用初始化时的 RGB/Swing 参数) + int nRet = OpenDevice(cameraIndex, cameraInfo.name.c_str(), cameraInfo.ip.c_str(), m_bRGB, m_bSwing); + + OnCameraStatusChanged(cameraIndex, SUCCESS == nRet); + if (nRet == SUCCESS) { + LOG_INFO("[BasePresenter] 相机 %d (%s) 重连成功\n", cameraIndex, cameraInfo.name.c_str()); + connectedCount++; + } else { + LOG_DEBUG("[BasePresenter] 相机 %d (%s) 重连失败,错误码: %d\n", cameraIndex, cameraInfo.name.c_str(), nRet); + allConnected = false; + } + } + + // 更新相机连接状态 + m_bCameraConnected = (connectedCount > 0); + + // 更新默认相机索引为第一个连接的相机 + for (int i = 0; i < static_cast(m_vrEyeDeviceList.size()); i++) { + if (m_vrEyeDeviceList[i].second != nullptr) { + m_currentCameraIndex = i + 1; + break; + } + } + + LOG_INFO("[BasePresenter] 相机重连尝试完成: %d/%d 台相机已连接\n", connectedCount, m_expectedList.size()); + + return (connectedCount == m_expectedList.size() && allConnected); +} + +// ============ ModbusTCP 服务实现 ============ + +int BasePresenter::StartModbusServer(int port) +{ + LOG_INFO("[BasePresenter] 启动ModbusTCP服务器,端口: %d\n", port); + + // 如果已经运行,先停止 + if (m_modbusServer) { + StopModbusServer(); + } + + // 创建ModbusTCP服务器实例 + if (!IYModbusTCPServer::CreateInstance(&m_modbusServer)) { + LOG_ERROR("[BasePresenter] 创建ModbusTCP服务器实例失败\n"); + return ERR_CODE(DEV_OPEN_ERR); + } + + // 设置写寄存器回调 + m_modbusServer->setWriteRegistersCallback( + [this](uint8_t unitId, uint16_t startAddress, uint16_t quantity, const uint16_t* values) -> IYModbusTCPServer::ErrorCode { + int ret = this->OnModbusWriteRegisters(unitId, startAddress, quantity, values); + return (ret == 0) ? IYModbusTCPServer::ErrorCode::SUCCESS : IYModbusTCPServer::ErrorCode::SERVER_FAILURE; + } + ); + + // 设置连接状态回调 + m_modbusServer->setConnectionStatusCallback( + [this](bool isConnected) { + LOG_INFO("[BasePresenter] Modbus客户端%s\n", isConnected ? "已连接" : "已断开"); + // 通知子类 ModbusTCP 连接状态变化 + this->OnModbusServerStatusChanged(isConnected); + } + ); + + // 启动服务器 + int ret = m_modbusServer->start(port, 5); + if (ret != 0) { + LOG_ERROR("[BasePresenter] 启动ModbusTCP服务器失败,错误码: %d\n", ret); + delete m_modbusServer; + m_modbusServer = nullptr; + return ERR_CODE(DEV_OPEN_ERR); + } + + LOG_INFO("[BasePresenter] ModbusTCP服务器启动成功\n"); + return SUCCESS; +} + +void BasePresenter::StopModbusServer() +{ + if (m_modbusServer) { + m_modbusServer->stop(); + delete m_modbusServer; + m_modbusServer = nullptr; + } +} + +bool BasePresenter::IsModbusServerRunning() const +{ + return m_modbusServer != nullptr; +} + +int BasePresenter::WriteModbusRegisters(uint16_t startAddress, const uint16_t* data, uint16_t count) +{ + if (!m_modbusServer) { + LOG_WARNING("[BasePresenter] ModbusTCP服务器未运行\n"); + return ERR_CODE(DEV_NOT_FIND); + } + + if (!data || count == 0) { + LOG_WARNING("[BasePresenter] 无效的Modbus写入参数\n"); + return ERR_CODE(DEV_DATA_INVALID); + } + + // 转换为vector并写入 + std::vector values(data, data + count); + m_modbusServer->updateHoldingRegisters(startAddress, values); + + LOG_DEBUG("[BasePresenter] Modbus写入: 地址=%d, 数量=%d\n", startAddress, count); + return SUCCESS; +} + +int BasePresenter::OnModbusWriteRegisters(uint8_t unitId, uint16_t startAddress, + uint16_t quantity, const uint16_t* values) +{ + LOG_DEBUG("[BasePresenter] Modbus收到写寄存器: unitId=%d, 地址=%d, 数量=%d\n", unitId, startAddress, quantity); + + // 调用虚函数让子类处理 + OnModbusWriteCallback(startAddress, values, quantity); + + return 0; +} diff --git a/AppUtils/UICommon/Src/DetectLogHelper.cpp b/AppUtils/UICommon/Src/DetectLogHelper.cpp index 323f37e..0f157f0 100644 --- a/AppUtils/UICommon/Src/DetectLogHelper.cpp +++ b/AppUtils/UICommon/Src/DetectLogHelper.cpp @@ -1,132 +1,132 @@ -#include "DetectLogHelper.h" - -DetectLogHelper::DetectLogHelper(QListView* listView, QObject *parent) - : QObject(parent) - , m_listView(listView) - , m_logModel(nullptr) - , m_lastLogCount(0) - , m_timestampFormat("hh:mm:ss") - , m_showTimestamp(true) - , m_deduplicationEnabled(true) -{ - if (m_listView) { - // 创建并设置 QStringListModel - m_logModel = new QStringListModel(this); - m_listView->setModel(m_logModel); - - // 设置 QListView 属性 - m_listView->setEditTriggers(QAbstractItemView::NoEditTriggers); - m_listView->setSelectionMode(QAbstractItemView::NoSelection); - m_listView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); - } - - initConnections(); -} - -DetectLogHelper::~DetectLogHelper() -{ -} - -void DetectLogHelper::initConnections() -{ - // 连接信号槽,支持跨线程更新 - connect(this, &DetectLogHelper::logUpdateRequested, - this, &DetectLogHelper::updateLogInUI, - Qt::QueuedConnection); - - connect(this, &DetectLogHelper::logClearRequested, - this, &DetectLogHelper::clearLogInUI, - Qt::QueuedConnection); -} - -void DetectLogHelper::appendLog(const QString& message) -{ - // 通过信号槽机制,确保在UI线程中更新 - emit logUpdateRequested(message); -} - -void DetectLogHelper::clearLog() -{ - // 通过信号槽机制,确保在UI线程中清空 - emit logClearRequested(); -} - -void DetectLogHelper::setTimestampFormat(const QString& format) -{ - m_timestampFormat = format; -} - -void DetectLogHelper::setShowTimestamp(bool show) -{ - m_showTimestamp = show; -} - -void DetectLogHelper::setDeduplicationEnabled(bool enable) -{ - m_deduplicationEnabled = enable; -} - -QStringListModel* DetectLogHelper::model() const -{ - return m_logModel; -} - -void DetectLogHelper::updateLogInUI(const QString& message) -{ - if (!m_logModel || !m_listView) return; - - // 获取当前数据 - QStringList logList = m_logModel->stringList(); - - // 构建日志条目 - QString logEntry; - - if (m_deduplicationEnabled && message == m_lastLogMessage && !logList.isEmpty()) { - // 相同消息,增加计数并替换最后一条 - m_lastLogCount++; - - if (m_showTimestamp) { - QString timestamp = QDateTime::currentDateTime().toString(m_timestampFormat); - logEntry = QString("[%1] %2 (x%3)").arg(timestamp).arg(message).arg(m_lastLogCount); - } else { - logEntry = QString("%1 (x%2)").arg(message).arg(m_lastLogCount); - } - - // 替换最后一条 - logList[logList.size() - 1] = logEntry; - } else { - // 新消息,重置计数 - m_lastLogMessage = message; - m_lastLogCount = 1; - - if (m_showTimestamp) { - QString timestamp = QDateTime::currentDateTime().toString(m_timestampFormat); - logEntry = QString("[%1] %2").arg(timestamp).arg(message); - } else { - logEntry = message; - } - - // 添加新的日志条目 - logList.append(logEntry); - } - - // 更新模型 - m_logModel->setStringList(logList); - - // 自动滚动到最底部 - if (!logList.isEmpty()) { - QModelIndex lastIndex = m_logModel->index(logList.size() - 1); - m_listView->scrollTo(lastIndex); - } -} - -void DetectLogHelper::clearLogInUI() -{ - if (m_logModel) { - m_logModel->setStringList(QStringList()); - } - - // 重置日志计数器 - m_lastLogMessage.clear(); - m_lastLogCount = 0; -} +#include "DetectLogHelper.h" + +DetectLogHelper::DetectLogHelper(QListView* listView, QObject *parent) + : QObject(parent) + , m_listView(listView) + , m_logModel(nullptr) + , m_lastLogCount(0) + , m_timestampFormat("hh:mm:ss") + , m_showTimestamp(true) + , m_deduplicationEnabled(true) +{ + if (m_listView) { + // 创建并设置 QStringListModel + m_logModel = new QStringListModel(this); + m_listView->setModel(m_logModel); + + // 设置 QListView 属性 + m_listView->setEditTriggers(QAbstractItemView::NoEditTriggers); + m_listView->setSelectionMode(QAbstractItemView::NoSelection); + m_listView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + } + + initConnections(); +} + +DetectLogHelper::~DetectLogHelper() +{ +} + +void DetectLogHelper::initConnections() +{ + // 连接信号槽,支持跨线程更新 + connect(this, &DetectLogHelper::logUpdateRequested, + this, &DetectLogHelper::updateLogInUI, + Qt::QueuedConnection); + + connect(this, &DetectLogHelper::logClearRequested, + this, &DetectLogHelper::clearLogInUI, + Qt::QueuedConnection); +} + +void DetectLogHelper::appendLog(const QString& message) +{ + // 通过信号槽机制,确保在UI线程中更新 + emit logUpdateRequested(message); +} + +void DetectLogHelper::clearLog() +{ + // 通过信号槽机制,确保在UI线程中清空 + emit logClearRequested(); +} + +void DetectLogHelper::setTimestampFormat(const QString& format) +{ + m_timestampFormat = format; +} + +void DetectLogHelper::setShowTimestamp(bool show) +{ + m_showTimestamp = show; +} + +void DetectLogHelper::setDeduplicationEnabled(bool enable) +{ + m_deduplicationEnabled = enable; +} + +QStringListModel* DetectLogHelper::model() const +{ + return m_logModel; +} + +void DetectLogHelper::updateLogInUI(const QString& message) +{ + if (!m_logModel || !m_listView) return; + + // 获取当前数据 + QStringList logList = m_logModel->stringList(); + + // 构建日志条目 + QString logEntry; + + if (m_deduplicationEnabled && message == m_lastLogMessage && !logList.isEmpty()) { + // 相同消息,增加计数并替换最后一条 + m_lastLogCount++; + + if (m_showTimestamp) { + QString timestamp = QDateTime::currentDateTime().toString(m_timestampFormat); + logEntry = QString("[%1] %2 (x%3)").arg(timestamp).arg(message).arg(m_lastLogCount); + } else { + logEntry = QString("%1 (x%2)").arg(message).arg(m_lastLogCount); + } + + // 替换最后一条 + logList[logList.size() - 1] = logEntry; + } else { + // 新消息,重置计数 + m_lastLogMessage = message; + m_lastLogCount = 1; + + if (m_showTimestamp) { + QString timestamp = QDateTime::currentDateTime().toString(m_timestampFormat); + logEntry = QString("[%1] %2").arg(timestamp).arg(message); + } else { + logEntry = message; + } + + // 添加新的日志条目 + logList.append(logEntry); + } + + // 更新模型 + m_logModel->setStringList(logList); + + // 自动滚动到最底部 + if (!logList.isEmpty()) { + QModelIndex lastIndex = m_logModel->index(logList.size() - 1); + m_listView->scrollTo(lastIndex); + } +} + +void DetectLogHelper::clearLogInUI() +{ + if (m_logModel) { + m_logModel->setStringList(QStringList()); + } + + // 重置日志计数器 + m_lastLogMessage.clear(); + m_lastLogCount = 0; +} diff --git a/Tools/CalibView/CalibView.pro b/Tools/CalibView/CalibView.pro index 0ba3ce4..6bc82b2 100644 --- a/Tools/CalibView/CalibView.pro +++ b/Tools/CalibView/CalibView.pro @@ -45,6 +45,7 @@ HEADERS += \ Inc/CalibViewMainWindow.h \ Inc/CalibDataWidget.h \ Inc/CalibResultWidget.h \ + Inc/BatchVerifyDialog.h \ ../RobotView/MainWindow.h \ ../VrEyeView/Inc/VrEyeViewWidget.h @@ -54,6 +55,7 @@ SOURCES += \ Src/CalibViewMainWindow.cpp \ Src/CalibDataWidget.cpp \ Src/CalibResultWidget.cpp \ + Src/BatchVerifyDialog.cpp \ ../RobotView/MainWindow.cpp \ ../VrEyeView/Src/VrEyeViewWidget.cpp diff --git a/Tools/CalibView/Inc/BatchVerifyDialog.h b/Tools/CalibView/Inc/BatchVerifyDialog.h new file mode 100644 index 0000000..f50a8e3 --- /dev/null +++ b/Tools/CalibView/Inc/BatchVerifyDialog.h @@ -0,0 +1,145 @@ +#ifndef BATCHVERIFYDIALOG_H +#define BATCHVERIFYDIALOG_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class IChessboardDetector; +class IHandEyeCalib; + +/** + * @brief 批量验证数据项 + */ +struct BatchVerifyItem +{ + QString leftImagePath; // 左目图像路径 + QString rightImagePath; // 右目图像路径 + double robotX, robotY, robotZ; // 机械臂坐标 + double robotRx, robotRy, robotRz; // 机械臂姿态 + + // 检测结果 + bool detected; + double camX, camY, camZ; // 相机检测到的坐标 + double camRx, camRy, camRz; // 相机检测到的姿态 + + // 验证结果 + double errorX, errorY, errorZ; // 误差 + double errorTotal; // 总误差 +}; + +/** + * @brief 批量验证对话框 + * 用于批量加载图像和机械臂坐标,进行标定验证 + */ +class BatchVerifyDialog : public QDialog +{ + Q_OBJECT + +public: + explicit BatchVerifyDialog(IChessboardDetector* detector, + IHandEyeCalib* calib, + QWidget* parent = nullptr); + ~BatchVerifyDialog() override; + + /** + * @brief 获取验证数据列表 + */ + const std::vector& getVerifyItems() const { return m_items; } + +private slots: + /** + * @brief 选择数据目录 + */ + void onSelectDirectory(); + + /** + * @brief 开始批量验证 + */ + void onStartVerify(); + + /** + * @brief 停止验证 + */ + void onStopVerify(); + + /** + * @brief 导出结果 + */ + void onExportResults(); + +private: + /** + * @brief 初始化界面 + */ + void setupUI(); + + /** + * @brief 扫描目录加载数据 + */ + bool scanDirectory(const QString& dirPath); + + /** + * @brief 加载机械臂坐标文件 + */ + bool loadRobotCoordinates(const QString& filePath); + + /** + * @brief 检测单个图像对 + */ + bool detectImagePair(BatchVerifyItem& item); + + /** + * @brief 计算验证误差 + */ + void calculateError(BatchVerifyItem& item); + + /** + * @brief 更新表格显示 + */ + void updateTable(); + + /** + * @brief 追加日志 + */ + void appendLog(const QString& message); + + // 检测器和标定实例 + IChessboardDetector* m_detector; + IHandEyeCalib* m_calib; + + // UI 控件 + QLabel* m_lblDirectory; + QPushButton* m_btnSelectDir; + QPushButton* m_btnStartVerify; + QPushButton* m_btnStopVerify; + QPushButton* m_btnExport; + QProgressBar* m_progressBar; + QTableWidget* m_tableResults; + QTextEdit* m_logEdit; + + // 标定板参数 + QSpinBox* m_sbPatternWidth; + QSpinBox* m_sbPatternHeight; + QDoubleSpinBox* m_sbSquareSize; + + // 相机内参 + QDoubleSpinBox* m_sbFx; + QDoubleSpinBox* m_sbFy; + QDoubleSpinBox* m_sbCx; + QDoubleSpinBox* m_sbCy; + + // 数据 + std::vector m_items; + QString m_currentDirectory; + bool m_isVerifying; +}; + +#endif // BATCHVERIFYDIALOG_H diff --git a/Tools/CalibView/Inc/CalibViewMainWindow.h b/Tools/CalibView/Inc/CalibViewMainWindow.h index 6f65cbd..bd3f3f4 100644 --- a/Tools/CalibView/Inc/CalibViewMainWindow.h +++ b/Tools/CalibView/Inc/CalibViewMainWindow.h @@ -107,6 +107,11 @@ private slots: void onChessboardDetected(double x, double y, double z, double rx, double ry, double rz); + /** + * @brief 打开批量验证对话框 + */ + void onOpenBatchVerify(); + private: /** * @brief 初始化界面 diff --git a/Tools/CalibView/Src/BatchVerifyDialog.cpp b/Tools/CalibView/Src/BatchVerifyDialog.cpp new file mode 100644 index 0000000..7e35e81 --- /dev/null +++ b/Tools/CalibView/Src/BatchVerifyDialog.cpp @@ -0,0 +1,556 @@ +#include "BatchVerifyDialog.h" +#include "IChessboardDetector.h" +#include "IHandEyeCalib.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +BatchVerifyDialog::BatchVerifyDialog(IChessboardDetector* detector, + IHandEyeCalib* calib, + QWidget* parent) + : QDialog(parent) + , m_detector(detector) + , m_calib(calib) + , m_lblDirectory(nullptr) + , m_btnSelectDir(nullptr) + , m_btnStartVerify(nullptr) + , m_btnStopVerify(nullptr) + , m_btnExport(nullptr) + , m_progressBar(nullptr) + , m_tableResults(nullptr) + , m_logEdit(nullptr) + , m_sbPatternWidth(nullptr) + , m_sbPatternHeight(nullptr) + , m_sbSquareSize(nullptr) + , m_sbFx(nullptr) + , m_sbFy(nullptr) + , m_sbCx(nullptr) + , m_sbCy(nullptr) + , m_isVerifying(false) +{ + setupUI(); + setWindowTitle("批量验证工具"); + resize(1000, 700); +} + +BatchVerifyDialog::~BatchVerifyDialog() +{ +} + +void BatchVerifyDialog::setupUI() +{ + QVBoxLayout* mainLayout = new QVBoxLayout(this); + + // 目录选择区域 + QGroupBox* dirGroup = new QGroupBox("数据目录", this); + QHBoxLayout* dirLayout = new QHBoxLayout(dirGroup); + m_lblDirectory = new QLabel("未选择目录", this); + m_btnSelectDir = new QPushButton("选择目录...", this); + connect(m_btnSelectDir, &QPushButton::clicked, this, &BatchVerifyDialog::onSelectDirectory); + dirLayout->addWidget(m_lblDirectory, 1); + dirLayout->addWidget(m_btnSelectDir); + mainLayout->addWidget(dirGroup); + + // 参数设置区域 + QGroupBox* paramGroup = new QGroupBox("检测参数", this); + QGridLayout* paramLayout = new QGridLayout(paramGroup); + + // 标定板参数 + paramLayout->addWidget(new QLabel("标定板宽度:", this), 0, 0); + m_sbPatternWidth = new QSpinBox(this); + m_sbPatternWidth->setRange(3, 20); + m_sbPatternWidth->setValue(11); + paramLayout->addWidget(m_sbPatternWidth, 0, 1); + + paramLayout->addWidget(new QLabel("标定板高度:", this), 0, 2); + m_sbPatternHeight = new QSpinBox(this); + m_sbPatternHeight->setRange(3, 20); + m_sbPatternHeight->setValue(8); + paramLayout->addWidget(m_sbPatternHeight, 0, 3); + + paramLayout->addWidget(new QLabel("方格尺寸(mm):", this), 0, 4); + m_sbSquareSize = new QDoubleSpinBox(this); + m_sbSquareSize->setRange(1, 100); + m_sbSquareSize->setDecimals(2); + m_sbSquareSize->setValue(15.0); + paramLayout->addWidget(m_sbSquareSize, 0, 5); + + // 相机内参 + paramLayout->addWidget(new QLabel("fx:", this), 1, 0); + m_sbFx = new QDoubleSpinBox(this); + m_sbFx->setRange(0, 10000); + m_sbFx->setDecimals(3); + m_sbFx->setValue(1000.0); + paramLayout->addWidget(m_sbFx, 1, 1); + + paramLayout->addWidget(new QLabel("fy:", this), 1, 2); + m_sbFy = new QDoubleSpinBox(this); + m_sbFy->setRange(0, 10000); + m_sbFy->setDecimals(3); + m_sbFy->setValue(1000.0); + paramLayout->addWidget(m_sbFy, 1, 3); + + paramLayout->addWidget(new QLabel("cx:", this), 1, 4); + m_sbCx = new QDoubleSpinBox(this); + m_sbCx->setRange(0, 5000); + m_sbCx->setDecimals(3); + m_sbCx->setValue(640.0); + paramLayout->addWidget(m_sbCx, 1, 5); + + paramLayout->addWidget(new QLabel("cy:", this), 2, 0); + m_sbCy = new QDoubleSpinBox(this); + m_sbCy->setRange(0, 5000); + m_sbCy->setDecimals(3); + m_sbCy->setValue(512.0); + paramLayout->addWidget(m_sbCy, 2, 1); + + mainLayout->addWidget(paramGroup); + + // 控制按钮 + QHBoxLayout* btnLayout = new QHBoxLayout(); + m_btnStartVerify = new QPushButton("开始验证", this); + m_btnStopVerify = new QPushButton("停止", this); + m_btnExport = new QPushButton("导出结果", this); + m_btnStartVerify->setEnabled(false); + m_btnStopVerify->setEnabled(false); + m_btnExport->setEnabled(false); + connect(m_btnStartVerify, &QPushButton::clicked, this, &BatchVerifyDialog::onStartVerify); + connect(m_btnStopVerify, &QPushButton::clicked, this, &BatchVerifyDialog::onStopVerify); + connect(m_btnExport, &QPushButton::clicked, this, &BatchVerifyDialog::onExportResults); + btnLayout->addWidget(m_btnStartVerify); + btnLayout->addWidget(m_btnStopVerify); + btnLayout->addWidget(m_btnExport); + btnLayout->addStretch(); + mainLayout->addLayout(btnLayout); + + // 进度条 + m_progressBar = new QProgressBar(this); + m_progressBar->setRange(0, 100); + m_progressBar->setValue(0); + mainLayout->addWidget(m_progressBar); + + // 结果表格 + m_tableResults = new QTableWidget(this); + m_tableResults->setColumnCount(13); + m_tableResults->setHorizontalHeaderLabels({ + "序号", "左目图像", "右目图像", + "机械臂X", "机械臂Y", "机械臂Z", + "检测X", "检测Y", "检测Z", + "误差X", "误差Y", "误差Z", "总误差" + }); + m_tableResults->horizontalHeader()->setStretchLastSection(true); + m_tableResults->setEditTriggers(QAbstractItemView::NoEditTriggers); + m_tableResults->setSelectionBehavior(QAbstractItemView::SelectRows); + mainLayout->addWidget(m_tableResults, 2); + + // 日志区域 + QGroupBox* logGroup = new QGroupBox("日志", this); + QVBoxLayout* logLayout = new QVBoxLayout(logGroup); + m_logEdit = new QTextEdit(this); + m_logEdit->setReadOnly(true); + m_logEdit->setMaximumHeight(150); + logLayout->addWidget(m_logEdit); + mainLayout->addWidget(logGroup); +} + +void BatchVerifyDialog::onSelectDirectory() +{ + QString dirPath = QFileDialog::getExistingDirectory( + this, "选择数据目录", m_currentDirectory); + + if (dirPath.isEmpty()) { + return; + } + + m_currentDirectory = dirPath; + m_lblDirectory->setText(dirPath); + + // 扫描目录 + if (scanDirectory(dirPath)) { + m_btnStartVerify->setEnabled(true); + appendLog(QString("成功加载 %1 组数据").arg(m_items.size())); + } else { + m_btnStartVerify->setEnabled(false); + appendLog("加载数据失败"); + } +} + +bool BatchVerifyDialog::scanDirectory(const QString& dirPath) +{ + m_items.clear(); + m_tableResults->setRowCount(0); + + QDir dir(dirPath); + if (!dir.exists()) { + QMessageBox::warning(this, "错误", "目录不存在"); + return false; + } + + // 查找 images 和 poses 子目录 + QDir imagesDir(dir.filePath("images")); + QDir posesDir(dir.filePath("poses")); + + if (!imagesDir.exists() || !posesDir.exists()) { + QMessageBox::warning(this, "错误", + "目录结构不正确\n请确保包含 images/ 和 poses/ 子目录"); + return false; + } + + // 查找所有 JSON 文件 + QStringList jsonFiles = posesDir.entryList(QStringList() << "s-*.json", QDir::Files, QDir::Name); + + if (jsonFiles.isEmpty()) { + QMessageBox::warning(this, "错误", "未找到位姿数据文件 (s-*.json)"); + return false; + } + + appendLog(QString("找到 %1 个位姿文件").arg(jsonFiles.size())); + + // 遍历每个 JSON 文件 + for (const QString& jsonFile : jsonFiles) { + // 提取样本 ID (例如 s-1.json -> s-1) + QString sampleId = QFileInfo(jsonFile).baseName(); + + // 构建图像路径 + QString leftImagePath = imagesDir.filePath(sampleId + "_L.png"); + QString rightImagePath = imagesDir.filePath(sampleId + "_R.png"); + + // 检查图像是否存在 + if (!QFile::exists(leftImagePath) || !QFile::exists(rightImagePath)) { + appendLog(QString("警告: 样本 %1 缺少图像文件").arg(sampleId)); + continue; + } + + // 加载 JSON 文件获取机械臂坐标 + QString jsonPath = posesDir.filePath(jsonFile); + QFile file(jsonPath); + if (!file.open(QIODevice::ReadOnly)) { + appendLog(QString("警告: 无法打开 %1").arg(jsonFile)); + continue; + } + + QByteArray jsonData = file.readAll(); + file.close(); + + QJsonDocument doc = QJsonDocument::fromJson(jsonData); + if (doc.isNull() || !doc.isObject()) { + appendLog(QString("警告: %1 不是有效的 JSON 文件").arg(jsonFile)); + continue; + } + + QJsonObject obj = doc.object(); + + // 读取 t_cp (机械臂末端位姿) + if (!obj.contains("t_cp")) { + appendLog(QString("警告: %1 缺少 t_cp 字段").arg(jsonFile)); + continue; + } + + QJsonObject tcpObj = obj["t_cp"].toObject(); + QJsonObject translation = tcpObj["translation_mm"].toObject(); + QJsonObject rotation = tcpObj["rotation_quat"].toObject(); + + // 创建验证项 + BatchVerifyItem item; + item.leftImagePath = leftImagePath; + item.rightImagePath = rightImagePath; + item.detected = false; + item.errorTotal = 0; + + // 读取位置 (mm) + item.robotX = translation["x"].toDouble(); + item.robotY = translation["y"].toDouble(); + item.robotZ = translation["z"].toDouble(); + + // 读取四元数 + double qw = rotation["w"].toDouble(); + double qx = rotation["x"].toDouble(); + double qy = rotation["y"].toDouble(); + double qz = rotation["z"].toDouble(); + + // 四元数转欧拉角 (ZYX顺序,单位:度) + // Roll (X轴旋转) + double sinr_cosp = 2.0 * (qw * qx + qy * qz); + double cosr_cosp = 1.0 - 2.0 * (qx * qx + qy * qy); + item.robotRx = std::atan2(sinr_cosp, cosr_cosp) * 180.0 / M_PI; + + // Pitch (Y轴旋转) + double sinp = 2.0 * (qw * qy - qz * qx); + if (std::abs(sinp) >= 1) + item.robotRy = std::copysign(M_PI / 2, sinp) * 180.0 / M_PI; + else + item.robotRy = std::asin(sinp) * 180.0 / M_PI; + + // Yaw (Z轴旋转) + double siny_cosp = 2.0 * (qw * qz + qx * qy); + double cosy_cosp = 1.0 - 2.0 * (qy * qy + qz * qz); + item.robotRz = std::atan2(siny_cosp, cosy_cosp) * 180.0 / M_PI; + + m_items.push_back(item); + } + + if (m_items.empty()) { + QMessageBox::warning(this, "错误", "未找到有效的数据"); + return false; + } + + updateTable(); + appendLog(QString("成功加载 %1 组数据").arg(m_items.size())); + return true; +} + +bool BatchVerifyDialog::loadRobotCoordinates(const QString& filePath) +{ + // 此函数已不再使用,因为坐标直接从 JSON 读取 + return true; +} + +void BatchVerifyDialog::onStartVerify() +{ + if (!m_detector) { + QMessageBox::warning(this, "错误", "检测器未初始化"); + return; + } + + m_isVerifying = true; + m_btnStartVerify->setEnabled(false); + m_btnStopVerify->setEnabled(true); + m_btnExport->setEnabled(false); + + appendLog("开始批量验证..."); + + // 设置检测参数 + m_detector->SetDetectionFlags(true, true, false); + + int successCount = 0; + int totalCount = m_items.size(); + + for (size_t i = 0; i < m_items.size(); ++i) { + if (!m_isVerifying) { + appendLog("验证已停止"); + break; + } + + appendLog(QString("正在验证第 %1/%2 组数据...").arg(i + 1).arg(totalCount)); + + if (detectImagePair(m_items[i])) { + calculateError(m_items[i]); + successCount++; + } + + // 更新进度 + m_progressBar->setValue((i + 1) * 100 / totalCount); + updateTable(); + + // 处理事件,保持界面响应 + QApplication::processEvents(); + } + + m_isVerifying = false; + m_btnStartVerify->setEnabled(true); + m_btnStopVerify->setEnabled(false); + m_btnExport->setEnabled(true); + + appendLog(QString("验证完成: 成功 %1/%2").arg(successCount).arg(totalCount)); +} + +void BatchVerifyDialog::onStopVerify() +{ + m_isVerifying = false; + appendLog("正在停止验证..."); +} + +bool BatchVerifyDialog::detectImagePair(BatchVerifyItem& item) +{ + // 加载左目图像 + QImage leftImage(item.leftImagePath); + if (leftImage.isNull()) { + appendLog(QString("错误: 无法加载左目图像 %1").arg(item.leftImagePath)); + return false; + } + + // 加载右目图像 + QImage rightImage(item.rightImagePath); + if (rightImage.isNull()) { + appendLog(QString("错误: 无法加载右目图像 %1").arg(item.rightImagePath)); + return false; + } + + // 准备相机内参 + CameraIntrinsics intrinsics; + intrinsics.fx = m_sbFx->value(); + intrinsics.fy = m_sbFy->value(); + intrinsics.cx = m_sbCx->value(); + intrinsics.cy = m_sbCy->value(); + + // 检测左目标定板 + QImage leftRgb = leftImage.convertToFormat(QImage::Format_RGB888); + ChessboardDetectResult leftResult; + int ret = m_detector->DetectChessboardWithPose( + leftRgb.bits(), + leftRgb.width(), + leftRgb.height(), + 3, + m_sbPatternWidth->value(), + m_sbPatternHeight->value(), + m_sbSquareSize->value(), + intrinsics, + leftResult); + + // 检测右目标定板 + QImage rightRgb = rightImage.convertToFormat(QImage::Format_RGB888); + ChessboardDetectResult rightResult; + int retRight = m_detector->DetectChessboardWithPose( + rightRgb.bits(), + rightRgb.width(), + rightRgb.height(), + 3, + m_sbPatternWidth->value(), + m_sbPatternHeight->value(), + m_sbSquareSize->value(), + intrinsics, + rightResult); + + if (ret == 0 && leftResult.detected && retRight == 0 && rightResult.detected) { + item.detected = true; + + if (leftResult.hasPose) { + item.camX = leftResult.center.x; + item.camY = leftResult.center.y; + item.camZ = leftResult.center.z; + item.camRx = leftResult.eulerAngles[0]; // Roll + item.camRy = leftResult.eulerAngles[1]; // Pitch + item.camRz = leftResult.eulerAngles[2]; // Yaw + } + + return true; + } else { + item.detected = false; + appendLog(QString("警告: 检测失败 - %1").arg(QFileInfo(item.leftImagePath).fileName())); + return false; + } +} + +void BatchVerifyDialog::calculateError(BatchVerifyItem& item) +{ + if (!item.detected) { + item.errorX = item.errorY = item.errorZ = item.errorTotal = 0; + return; + } + + // 计算位置误差 + item.errorX = item.camX - item.robotX; + item.errorY = item.camY - item.robotY; + item.errorZ = item.camZ - item.robotZ; + item.errorTotal = std::sqrt(item.errorX * item.errorX + + item.errorY * item.errorY + + item.errorZ * item.errorZ); +} + +void BatchVerifyDialog::updateTable() +{ + m_tableResults->setRowCount(m_items.size()); + + for (size_t i = 0; i < m_items.size(); ++i) { + const BatchVerifyItem& item = m_items[i]; + + m_tableResults->setItem(i, 0, new QTableWidgetItem(QString::number(i + 1))); + m_tableResults->setItem(i, 1, new QTableWidgetItem(QFileInfo(item.leftImagePath).fileName())); + m_tableResults->setItem(i, 2, new QTableWidgetItem(QFileInfo(item.rightImagePath).fileName())); + + m_tableResults->setItem(i, 3, new QTableWidgetItem(QString::number(item.robotX, 'f', 3))); + m_tableResults->setItem(i, 4, new QTableWidgetItem(QString::number(item.robotY, 'f', 3))); + m_tableResults->setItem(i, 5, new QTableWidgetItem(QString::number(item.robotZ, 'f', 3))); + + if (item.detected) { + m_tableResults->setItem(i, 6, new QTableWidgetItem(QString::number(item.camX, 'f', 3))); + m_tableResults->setItem(i, 7, new QTableWidgetItem(QString::number(item.camY, 'f', 3))); + m_tableResults->setItem(i, 8, new QTableWidgetItem(QString::number(item.camZ, 'f', 3))); + + m_tableResults->setItem(i, 9, new QTableWidgetItem(QString::number(item.errorX, 'f', 3))); + m_tableResults->setItem(i, 10, new QTableWidgetItem(QString::number(item.errorY, 'f', 3))); + m_tableResults->setItem(i, 11, new QTableWidgetItem(QString::number(item.errorZ, 'f', 3))); + m_tableResults->setItem(i, 12, new QTableWidgetItem(QString::number(item.errorTotal, 'f', 3))); + } else { + m_tableResults->setItem(i, 6, new QTableWidgetItem("未检测")); + m_tableResults->setItem(i, 7, new QTableWidgetItem("-")); + m_tableResults->setItem(i, 8, new QTableWidgetItem("-")); + m_tableResults->setItem(i, 9, new QTableWidgetItem("-")); + m_tableResults->setItem(i, 10, new QTableWidgetItem("-")); + m_tableResults->setItem(i, 11, new QTableWidgetItem("-")); + m_tableResults->setItem(i, 12, new QTableWidgetItem("-")); + } + } + + m_tableResults->resizeColumnsToContents(); +} + +void BatchVerifyDialog::onExportResults() +{ + QString fileName = QFileDialog::getSaveFileName( + this, "导出验证结果", "", + "CSV文件 (*.csv);;文本文件 (*.txt)"); + + if (fileName.isEmpty()) { + return; + } + + QFile file(fileName); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::warning(this, "错误", "无法创建文件"); + return; + } + + QTextStream out(&file); + out.setCodec("UTF-8"); + + // 写入表头 + out << "序号,左目图像,右目图像,机械臂X,机械臂Y,机械臂Z,检测X,检测Y,检测Z,误差X,误差Y,误差Z,总误差\n"; + + // 写入数据 + for (size_t i = 0; i < m_items.size(); ++i) { + const BatchVerifyItem& item = m_items[i]; + out << (i + 1) << "," + << QFileInfo(item.leftImagePath).fileName() << "," + << QFileInfo(item.rightImagePath).fileName() << "," + << item.robotX << "," << item.robotY << "," << item.robotZ << ","; + + if (item.detected) { + out << item.camX << "," << item.camY << "," << item.camZ << "," + << item.errorX << "," << item.errorY << "," << item.errorZ << "," + << item.errorTotal; + } else { + out << "未检测,-,-,-,-,-,-"; + } + out << "\n"; + } + + file.close(); + appendLog(QString("结果已导出到: %1").arg(fileName)); + QMessageBox::information(this, "成功", "验证结果已导出"); +} + +void BatchVerifyDialog::appendLog(const QString& message) +{ + QString timestamp = QDateTime::currentDateTime().toString("hh:mm:ss"); + m_logEdit->append(QString("[%1] %2").arg(timestamp).arg(message)); +} diff --git a/Tools/CalibView/Src/CalibViewMainWindow.cpp b/Tools/CalibView/Src/CalibViewMainWindow.cpp index 8aecc40..7118cbc 100644 --- a/Tools/CalibView/Src/CalibViewMainWindow.cpp +++ b/Tools/CalibView/Src/CalibViewMainWindow.cpp @@ -1,9 +1,11 @@ #include "CalibViewMainWindow.h" #include "CalibDataWidget.h" #include "CalibResultWidget.h" +#include "BatchVerifyDialog.h" #include "MainWindow.h" #include "VrEyeViewWidget.h" #include "../../SpinBoxPasteHelper.h" +#include "IChessboardDetector.h" #include #include @@ -259,6 +261,12 @@ void CalibViewMainWindow::createMenuBar() QAction* actVrEyeView = toolMenu->addAction("相机标定板检测(&V)"); connect(actVrEyeView, &QAction::triggered, this, &CalibViewMainWindow::onOpenVrEyeView); + toolMenu->addSeparator(); + + QAction* actBatchVerify = toolMenu->addAction("批量验证(&B)"); + actBatchVerify->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_B)); + connect(actBatchVerify, &QAction::triggered, this, &CalibViewMainWindow::onOpenBatchVerify); + // 帮助菜单 QMenu* helpMenu = menuBar()->addMenu("帮助(&H)"); @@ -739,3 +747,20 @@ void CalibViewMainWindow::onLoadCalibData() .arg(fileName).arg(saveTime).arg(typeNames[calibType])); updateStatusBar("标定数据已加载"); } + +void CalibViewMainWindow::onOpenBatchVerify() +{ + // 创建标定板检测器实例 + IChessboardDetector* detector = CreateChessboardDetectorInstance(); + if (!detector) { + QMessageBox::critical(this, "错误", "无法创建标定板检测器实例"); + return; + } + + // 创建批量验证对话框 + BatchVerifyDialog* dialog = new BatchVerifyDialog(detector, m_calib, this); + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->show(); + + appendLog("打开批量验证工具"); +} diff --git a/Utils b/Utils index c812176..d6049e7 160000 --- a/Utils +++ b/Utils @@ -1 +1 @@ -Subproject commit c812176d844d7a4d2883c30d3ff33f880a543d51 +Subproject commit d6049e7846e2704813b2b30ab13b510cc86c7bea