孔检测算法更新,移除了一些不用的参数

This commit is contained in:
yiyi 2026-03-13 09:49:07 +08:00
parent 3842df9f9b
commit 5dc4feab00
28 changed files with 3571 additions and 2587 deletions

View File

@ -113,29 +113,15 @@ int DetectPresenter::DetectHoles(
detectionParams.angleThresholdPos = static_cast<float>(algorithmParams.detectionParam.angleThresholdPos);
detectionParams.angleThresholdNeg = static_cast<float>(algorithmParams.detectionParam.angleThresholdNeg);
detectionParams.minPitDepth = static_cast<float>(algorithmParams.detectionParam.minPitDepth);
detectionParams.angleStep = static_cast<float>(algorithmParams.detectionParam.angleStep);
detectionParams.maxScanRadius = static_cast<float>(algorithmParams.detectionParam.maxScanRadius);
detectionParams.clusterEps = static_cast<float>(algorithmParams.detectionParam.clusterEps);
detectionParams.clusterMinPoints = algorithmParams.detectionParam.clusterMinPoints;
detectionParams.minRadius = static_cast<float>(algorithmParams.detectionParam.minRadius);
detectionParams.maxRadius = static_cast<float>(algorithmParams.detectionParam.maxRadius);
detectionParams.expansionSize1 = algorithmParams.detectionParam.expansionSize1;
detectionParams.expansionSize2 = algorithmParams.detectionParam.expansionSize2;
detectionParams.validZThreshold = static_cast<float>(algorithmParams.detectionParam.validZThreshold);
detectionParams.minVTransitionPoints = algorithmParams.detectionParam.minVTransitionPoints;
detectionParams.cornerScale = static_cast<float>(algorithmParams.detectionParam.cornerScale);
detectionParams.cornerAngleThreshold = static_cast<float>(algorithmParams.detectionParam.cornerAngleThreshold);
detectionParams.jumpCornerTh_1 = static_cast<float>(algorithmParams.detectionParam.jumpCornerTh_1);
detectionParams.jumpCornerTh_2 = static_cast<float>(algorithmParams.detectionParam.jumpCornerTh_2);
detectionParams.minEndingGap = static_cast<float>(algorithmParams.detectionParam.minEndingGap);
detectionParams.minEndingGap_z = static_cast<float>(algorithmParams.detectionParam.minEndingGap_z);
// 映射 VrHoleFilterParam -> SHoleFilterParams
SHoleFilterParams filterParams;
filterParams.minHoleRadius = static_cast<float>(algorithmParams.filterParam.minHoleRadius);
filterParams.maxHoleRadius = static_cast<float>(algorithmParams.filterParam.maxHoleRadius);
filterParams.maxEccentricity = static_cast<float>(algorithmParams.filterParam.maxEccentricity);
filterParams.maxCornerRatio = static_cast<float>(algorithmParams.filterParam.maxCornerRatio);
filterParams.minAngularCoverage = static_cast<float>(algorithmParams.filterParam.minAngularCoverage);
filterParams.maxRadiusFitRatio = static_cast<float>(algorithmParams.filterParam.maxRadiusFitRatio);
filterParams.minQualityScore = static_cast<float>(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);

View File

@ -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",

View File

@ -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
// 构建日期

View File

@ -8,6 +8,7 @@
#include <QtCore/QStandardPaths>
#include <QtCore/QFile>
#include <cstring>
#include <cmath>
#include <QSettings>
#include <QFileDialog>
#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<QLineEdit*>(name);
QLineEdit* edit = findChild<QLineEdit*>(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<int>(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)

View File

@ -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<VrHandEyeCalibMatrix>& calibMatrixList);
bool SaveCurrentCalibMatrixToCache();

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>780</width>
<height>760</height>
<height>656</height>
</rect>
</property>
<property name="windowTitle">
@ -44,7 +44,7 @@
<x>40</x>
<y>80</y>
<width>701</width>
<height>611</height>
<height>491</height>
</rect>
</property>
<property name="font">
@ -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; }</string>
</property>
<property name="currentIndex">
<number>2</number>
<number>0</number>
</property>
<widget class="QWidget" name="tab_detection">
<attribute name="title">
@ -74,7 +74,7 @@ QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 3px 0 3px;
<x>10</x>
<y>10</y>
<width>671</width>
<height>561</height>
<height>411</height>
</rect>
</property>
<property name="font">
@ -89,9 +89,9 @@ QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 3px 0 3px;
<property name="geometry">
<rect>
<x>20</x>
<y>30</y>
<y>40</y>
<width>631</width>
<height>656</height>
<height>341</height>
</rect>
</property>
<layout class="QGridLayout">
@ -136,165 +136,55 @@ QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 3px 0 3px;
<widget class="QLineEdit" name="lineEdit_minPitDepth"/>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_angleStep">
<property name="text">
<string>径向扫描角度步长 (angleStep)</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="lineEdit_angleStep"/>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_maxScanRadius">
<property name="text">
<string>最大扫描半径 mm (maxScanRadius)</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLineEdit" name="lineEdit_maxScanRadius"/>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_clusterEps">
<property name="text">
<string>DBSCAN聚类半径 mm (clusterEps)</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QLineEdit" name="lineEdit_clusterEps"/>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_clusterMinPoints">
<property name="text">
<string>DBSCAN最小点数 (clusterMinPoints)</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QLineEdit" name="lineEdit_clusterMinPoints"/>
</item>
<item row="8" column="0">
<widget class="QLabel" name="label_minRadius">
<property name="text">
<string>最小孔半径 mm (minRadius)</string>
</property>
</widget>
</item>
<item row="8" column="1">
<item row="4" column="1">
<widget class="QLineEdit" name="lineEdit_minRadius"/>
</item>
<item row="9" column="0">
<item row="5" column="0">
<widget class="QLabel" name="label_maxRadius">
<property name="text">
<string>最大孔半径 mm (maxRadius)</string>
</property>
</widget>
</item>
<item row="9" column="1">
<item row="5" column="1">
<widget class="QLineEdit" name="lineEdit_maxRadius"/>
</item>
<item row="10" column="0">
<item row="6" column="0">
<widget class="QLabel" name="label_expansionSize1">
<property name="text">
<string>第一环扩展大小 (expansionSize1)</string>
</property>
</widget>
</item>
<item row="10" column="1">
<item row="6" column="1">
<widget class="QLineEdit" name="lineEdit_expansionSize1"/>
</item>
<item row="11" column="0">
<item row="7" column="0">
<widget class="QLabel" name="label_expansionSize2">
<property name="text">
<string>第二环扩展大小 (expansionSize2)</string>
</property>
</widget>
</item>
<item row="11" column="1">
<item row="7" column="1">
<widget class="QLineEdit" name="lineEdit_expansionSize2"/>
</item>
<item row="12" column="0">
<widget class="QLabel" name="label_validZThreshold">
<property name="text">
<string>有效Z值阈值 (validZThreshold)</string>
</property>
</widget>
</item>
<item row="12" column="1">
<widget class="QLineEdit" name="lineEdit_validZThreshold"/>
</item>
<item row="13" column="0">
<item row="8" column="0">
<widget class="QLabel" name="label_minVTransitionPoints">
<property name="text">
<string>V形最小过渡点数 (minVTransitionPoints)</string>
</property>
</widget>
</item>
<item row="13" column="1">
<item row="8" column="1">
<widget class="QLineEdit" name="lineEdit_minVTransitionPoints"/>
</item>
<item row="14" column="0">
<widget class="QLabel" name="label_cornerScale">
<property name="text">
<string>角点搜索距离 mm (cornerScale)</string>
</property>
</widget>
</item>
<item row="14" column="1">
<widget class="QLineEdit" name="lineEdit_cornerScale"/>
</item>
<item row="15" column="0">
<widget class="QLabel" name="label_cornerAngleThreshold">
<property name="text">
<string>角点角度阈值 (cornerAngleThreshold)</string>
</property>
</widget>
</item>
<item row="15" column="1">
<widget class="QLineEdit" name="lineEdit_cornerAngleThreshold"/>
</item>
<item row="16" column="0">
<widget class="QLabel" name="label_jumpCornerTh_1">
<property name="text">
<string>跳跃小角度阈值 (jumpCornerTh_1)</string>
</property>
</widget>
</item>
<item row="16" column="1">
<widget class="QLineEdit" name="lineEdit_jumpCornerTh_1"/>
</item>
<item row="17" column="0">
<widget class="QLabel" name="label_jumpCornerTh_2">
<property name="text">
<string>跳跃大角度阈值 (jumpCornerTh_2)</string>
</property>
</widget>
</item>
<item row="17" column="1">
<widget class="QLineEdit" name="lineEdit_jumpCornerTh_2"/>
</item>
<item row="18" column="0">
<widget class="QLabel" name="label_minEndingGap">
<property name="text">
<string>Y方向配对距离 mm (minEndingGap)</string>
</property>
</widget>
</item>
<item row="18" column="1">
<widget class="QLineEdit" name="lineEdit_minEndingGap"/>
</item>
<item row="19" column="0">
<widget class="QLabel" name="label_minEndingGap_z">
<property name="text">
<string>Z方向高度阈值 mm (minEndingGap_z)</string>
</property>
</widget>
</item>
<item row="19" column="1">
<widget class="QLineEdit" name="lineEdit_minEndingGap_z"/>
</item>
</layout>
</widget>
</widget>
@ -309,7 +199,7 @@ QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 3px 0 3px;
<x>10</x>
<y>10</y>
<width>671</width>
<height>561</height>
<height>421</height>
</rect>
</property>
<property name="font">
@ -326,118 +216,88 @@ QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 3px 0 3px;
<x>20</x>
<y>30</y>
<width>631</width>
<height>510</height>
<height>371</height>
</rect>
</property>
<layout class="QGridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_minHoleRadius">
<property name="text">
<string>最小孔半径 mm (minHoleRadius)</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="lineEdit_minHoleRadius"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_maxHoleRadius">
<property name="text">
<string>最大孔半径 mm (maxHoleRadius)</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="lineEdit_maxHoleRadius"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_maxEccentricity">
<property name="text">
<string>最大离心率 (maxEccentricity)</string>
</property>
</widget>
</item>
<item row="2" column="1">
<item row="0" column="1">
<widget class="QLineEdit" name="lineEdit_maxEccentricity"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_maxCornerRatio">
<property name="text">
<string>最大矩形度比率 (maxCornerRatio)</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="lineEdit_maxCornerRatio"/>
</item>
<item row="4" column="0">
<item row="1" column="0">
<widget class="QLabel" name="label_minAngularCoverage">
<property name="text">
<string>最小角度覆盖 (minAngularCoverage)</string>
</property>
</widget>
</item>
<item row="4" column="1">
<item row="1" column="1">
<widget class="QLineEdit" name="lineEdit_minAngularCoverage"/>
</item>
<item row="5" column="0">
<item row="2" column="0">
<widget class="QLabel" name="label_maxRadiusFitRatio">
<property name="text">
<string>最大半径拟合比率 (maxRadiusFitRatio)</string>
</property>
</widget>
</item>
<item row="5" column="1">
<item row="2" column="1">
<widget class="QLineEdit" name="lineEdit_maxRadiusFitRatio"/>
</item>
<item row="6" column="0">
<item row="3" column="0">
<widget class="QLabel" name="label_minQualityScore">
<property name="text">
<string>最小质量分数 (minQualityScore)</string>
</property>
</widget>
</item>
<item row="6" column="1">
<item row="3" column="1">
<widget class="QLineEdit" name="lineEdit_minQualityScore"/>
</item>
<item row="7" column="0">
<item row="4" column="0">
<widget class="QLabel" name="label_maxPlaneResidual">
<property name="text">
<string>最大平面残差 mm (maxPlaneResidual)</string>
</property>
</widget>
</item>
<item row="7" column="1">
<item row="4" column="1">
<widget class="QLineEdit" name="lineEdit_maxPlaneResidual"/>
</item>
<item row="8" column="0">
<item row="5" column="0">
<widget class="QLabel" name="label_maxAngularGap">
<property name="text">
<string>最大角度间隙 (maxAngularGap)</string>
</property>
</widget>
</item>
<item row="8" column="1">
<item row="5" column="1">
<widget class="QLineEdit" name="lineEdit_maxAngularGap"/>
</item>
<item row="9" column="0">
<item row="6" column="0">
<widget class="QLabel" name="label_minInlierRatio">
<property name="text">
<string>最小内点比率 (minInlierRatio)</string>
</property>
</widget>
</item>
<item row="9" column="1">
<item row="6" column="1">
<widget class="QLineEdit" name="lineEdit_minInlierRatio"/>
</item>
<item row="10" column="0">
<item row="7" column="0">
<widget class="QLabel" name="label_sortMode">
<property name="text">
<string>排序模式</string>
</property>
</widget>
</item>
<item row="10" column="1">
<item row="7" column="1">
<widget class="QComboBox" name="comboBox_sortMode">
<property name="styleSheet">
<string notr="true">QComboBox {
@ -472,7 +332,7 @@ QComboBox QAbstractItemView {
<x>10</x>
<y>10</y>
<width>671</width>
<height>561</height>
<height>421</height>
</rect>
</property>
<property name="font">
@ -610,7 +470,7 @@ QComboBox QAbstractItemView {
<x>10</x>
<y>10</y>
<width>671</width>
<height>561</height>
<height>411</height>
</rect>
</property>
<property name="font">
@ -925,7 +785,7 @@ QComboBox QAbstractItemView {
<property name="geometry">
<rect>
<x>2</x>
<y>700</y>
<y>590</y>
<width>776</width>
<height>44</height>
</rect>

View File

@ -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; // 最小内点比率
};
/**

View File

@ -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);

View File

@ -1,189 +1,207 @@
#ifndef WHEELMEASUREPRESENTER_H
#define WHEELMEASUREPRESENTER_H
#include "BasePresenter.h"
#include "IVrWheelMeasureConfig.h"
#include "IWheelMeasureStatus.h"
#include "CommonDialogCameraLevel.h"
#include <QImage>
#include <QString>
#include <QStringList>
/**
* @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<std::pair<EVzResultDataType, SVzLaserLineData>>& 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<std::pair<EVzResultDataType, SVzLaserLineData>>& 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<std::pair<EVzResultDataType, SVzLaserLineData>>& 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 <QImage>
#include <QString>
#include <QStringList>
#include <QMap>
/**
* @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<std::pair<EVzResultDataType, SVzLaserLineData>>& 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<std::pair<EVzResultDataType, SVzLaserLineData>>& 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<std::pair<EVzResultDataType, SVzLaserLineData>>& 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<int, WheelMeasureTCPProtocol::CameraMeasureResult> m_tcpResults; // TCP检测结果缓存
// 继续检测下一个设备
void continueSequentialDetection();
};
#endif // WHEELMEASUREPRESENTER_H

View File

@ -0,0 +1,125 @@
#ifndef WHEELMEASURETCPPROTOCOL_H
#define WHEELMEASURETCPPROTOCOL_H
#include <functional>
#include <vector>
#include <string>
#include <QString>
#include <QMutex>
#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<bool(int param)>;
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<CameraMeasureResult>& 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

View File

@ -0,0 +1,246 @@
#include "WheelMeasureTCPProtocol.h"
#include "VrLog.h"
#include <QStringList>
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<CameraMeasureResult>& 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<int>(result.centerDistance));
resultStr += "," + QString::number(static_cast<int>(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());
}
}

View File

@ -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 \

View File

@ -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
*/

View File

@ -2,141 +2,103 @@
#define HOLE_DETECTION_PARAMS_H
#include <cmath>
#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

File diff suppressed because it is too large Load Diff

View File

@ -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;
}

View File

@ -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

View File

@ -0,0 +1,145 @@
#ifndef BATCHVERIFYDIALOG_H
#define BATCHVERIFYDIALOG_H
#include <QDialog>
#include <QTableWidget>
#include <QPushButton>
#include <QProgressBar>
#include <QLabel>
#include <QTextEdit>
#include <QSpinBox>
#include <QDoubleSpinBox>
#include <vector>
#include <QString>
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<BatchVerifyItem>& 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<BatchVerifyItem> m_items;
QString m_currentDirectory;
bool m_isVerifying;
};
#endif // BATCHVERIFYDIALOG_H

View File

@ -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

View File

@ -0,0 +1,556 @@
#include "BatchVerifyDialog.h"
#include "IChessboardDetector.h"
#include "IHandEyeCalib.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGridLayout>
#include <QGroupBox>
#include <QFileDialog>
#include <QMessageBox>
#include <QHeaderView>
#include <QDir>
#include <QFileInfo>
#include <QSettings>
#include <QDateTime>
#include <QApplication>
#include <QImage>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <cmath>
#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));
}

View File

@ -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 <QMenuBar>
#include <QAction>
@ -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("打开批量验证工具");
}

2
Utils

@ -1 +1 @@
Subproject commit c812176d844d7a4d2883c30d3ff33f880a543d51
Subproject commit d6049e7846e2704813b2b30ab13b510cc86c7bea