孔检测算法更新,移除了一些不用的参数
This commit is contained in:
parent
3842df9f9b
commit
5dc4feab00
@ -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);
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
// 构建日期
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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; // 最小内点比率
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
File diff suppressed because it is too large
Load Diff
@ -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());
|
||||
}
|
||||
}
|
||||
@ -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 \
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -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
|
||||
*/
|
||||
|
||||
@ -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
|
||||
|
||||
Binary file not shown.
BIN
AppAlgo/holeDetection/windows/x64/Debug/HoleDetectionLib.pdb
Normal file
BIN
AppAlgo/holeDetection/windows/x64/Debug/HoleDetectionLib.pdb
Normal file
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
145
Tools/CalibView/Inc/BatchVerifyDialog.h
Normal file
145
Tools/CalibView/Inc/BatchVerifyDialog.h
Normal 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
|
||||
@ -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 初始化界面
|
||||
|
||||
556
Tools/CalibView/Src/BatchVerifyDialog.cpp
Normal file
556
Tools/CalibView/Src/BatchVerifyDialog.cpp
Normal 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));
|
||||
}
|
||||
@ -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
2
Utils
@ -1 +1 @@
|
||||
Subproject commit c812176d844d7a4d2883c30d3ff33f880a543d51
|
||||
Subproject commit d6049e7846e2704813b2b30ab13b510cc86c7bea
|
||||
Loading…
x
Reference in New Issue
Block a user