孔检测HoleDetection 做了算法更新

This commit is contained in:
yiyi 2026-04-02 09:52:45 +08:00
parent 512c72b895
commit de7df9398d
21 changed files with 348 additions and 54 deletions

View File

@ -211,19 +211,21 @@ int HoleDetectionPresenter::InitAlgoParams()
xmlParams.detectionParam.maxRadius, xmlParams.detectionParam.maxRadius,
xmlParams.detectionParam.expansionSize1, xmlParams.detectionParam.expansionSize1,
xmlParams.detectionParam.expansionSize2); xmlParams.detectionParam.expansionSize2);
LOG_INFO("Loaded XML params - Detection: minVTransitionPoints=%d\n", LOG_INFO("Loaded XML params - Detection: minVTransitionPoints=%d, edgeBoundaryFilterDist=%.2f\n",
xmlParams.detectionParam.minVTransitionPoints); xmlParams.detectionParam.minVTransitionPoints,
xmlParams.detectionParam.edgeBoundaryFilterDist);
// 打印过滤参数 // 打印过滤参数
LOG_INFO("Loaded XML params - Filter: maxEccentricity=%.5f, minAngularCoverage=%.1f, maxRadiusFitRatio=%.2f\n", LOG_INFO("Loaded XML params - Filter: maxEccentricity=%.5f, minAngularCoverage=%.1f, maxRadiusFitRatio=%.2f\n",
xmlParams.filterParam.maxEccentricity, xmlParams.filterParam.maxEccentricity,
xmlParams.filterParam.minAngularCoverage, xmlParams.filterParam.minAngularCoverage,
xmlParams.filterParam.maxRadiusFitRatio); xmlParams.filterParam.maxRadiusFitRatio);
LOG_INFO("Loaded XML params - Filter: minQualityScore=%.2f, maxPlaneResidual=%.1f, maxAngularGap=%.1f, minInlierRatio=%.2f\n", LOG_INFO("Loaded XML params - Filter: minQualityScore=%.2f, maxPlaneResidual=%.1f, maxAngularGap=%.1f, minInlierRatio=%.2f, minHoleDepth=%.2f\n",
xmlParams.filterParam.minQualityScore, xmlParams.filterParam.minQualityScore,
xmlParams.filterParam.maxPlaneResidual, xmlParams.filterParam.maxPlaneResidual,
xmlParams.filterParam.maxAngularGap, xmlParams.filterParam.maxAngularGap,
xmlParams.filterParam.minInlierRatio); xmlParams.filterParam.minInlierRatio,
xmlParams.filterParam.minHoleDepth);
LOG_INFO("Loaded XML params - SortMode: %d\n", xmlParams.sortMode); LOG_INFO("Loaded XML params - SortMode: %d\n", xmlParams.sortMode);

View File

@ -5,7 +5,7 @@
#define HOLEDETECTION_APP_NAME "孔洞检测" #define HOLEDETECTION_APP_NAME "孔洞检测"
// 版本字符串 // 版本字符串
#define HOLEDETECTION_VERSION_STRING "1.1.0" #define HOLEDETECTION_VERSION_STRING "1.1.1"
#define HOLEDETECTION_BUILD_STRING "1" #define HOLEDETECTION_BUILD_STRING "1"
#define HOLEDETECTION_FULL_VERSION_STRING "V" HOLEDETECTION_VERSION_STRING "_" HOLEDETECTION_BUILD_STRING #define HOLEDETECTION_FULL_VERSION_STRING "V" HOLEDETECTION_VERSION_STRING "_" HOLEDETECTION_BUILD_STRING

View File

@ -1,3 +1,7 @@
# 1.1.1 2026-04-02
## build_1
1. 更新算法
# 1.1.0 2026-03-25 # 1.1.0 2026-03-25
## build_1 ## build_1
1. 算法优化 1. 算法优化

View File

@ -319,6 +319,7 @@ void DialogAlgoarg::LoadDetectionParamToUI(const VrHoleDetectionParam& param)
ui->lineEdit_expansionSize1->setText(QString::number(param.expansionSize1)); ui->lineEdit_expansionSize1->setText(QString::number(param.expansionSize1));
ui->lineEdit_expansionSize2->setText(QString::number(param.expansionSize2)); ui->lineEdit_expansionSize2->setText(QString::number(param.expansionSize2));
ui->lineEdit_minVTransitionPoints->setText(QString::number(param.minVTransitionPoints)); ui->lineEdit_minVTransitionPoints->setText(QString::number(param.minVTransitionPoints));
ui->lineEdit_edgeBoundaryFilterDist->setText(QString::number(param.edgeBoundaryFilterDist));
} }
void DialogAlgoarg::LoadFilterParamToUI(const VrHoleFilterParam& param) void DialogAlgoarg::LoadFilterParamToUI(const VrHoleFilterParam& param)
@ -332,6 +333,7 @@ void DialogAlgoarg::LoadFilterParamToUI(const VrHoleFilterParam& param)
ui->lineEdit_maxPlaneResidual->setText(QString::number(param.maxPlaneResidual)); ui->lineEdit_maxPlaneResidual->setText(QString::number(param.maxPlaneResidual));
ui->lineEdit_maxAngularGap->setText(QString::number(param.maxAngularGap)); ui->lineEdit_maxAngularGap->setText(QString::number(param.maxAngularGap));
ui->lineEdit_minInlierRatio->setText(QString::number(param.minInlierRatio)); ui->lineEdit_minInlierRatio->setText(QString::number(param.minInlierRatio));
ui->lineEdit_minHoleDepth->setText(QString::number(param.minHoleDepth));
} }
void DialogAlgoarg::LoadSortModeToUI(int sortMode) void DialogAlgoarg::LoadSortModeToUI(int sortMode)
@ -377,6 +379,9 @@ bool DialogAlgoarg::SaveDetectionParamFromUI(VrHoleDetectionParam& param)
param.minVTransitionPoints = ui->lineEdit_minVTransitionPoints->text().toInt(&ok); param.minVTransitionPoints = ui->lineEdit_minVTransitionPoints->text().toInt(&ok);
if (!ok) return false; if (!ok) return false;
param.edgeBoundaryFilterDist = ui->lineEdit_edgeBoundaryFilterDist->text().toDouble(&ok);
if (!ok) return false;
return true; return true;
} }
@ -405,6 +410,9 @@ bool DialogAlgoarg::SaveFilterParamFromUI(VrHoleFilterParam& param)
param.minInlierRatio = ui->lineEdit_minInlierRatio->text().toDouble(&ok); param.minInlierRatio = ui->lineEdit_minInlierRatio->text().toDouble(&ok);
if (!ok) return false; if (!ok) return false;
param.minHoleDepth = ui->lineEdit_minHoleDepth->text().toDouble(&ok);
if (!ok) return false;
return true; return true;
} }

View File

@ -182,6 +182,16 @@ QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 3px 0 3px;
<item row="8" column="1"> <item row="8" column="1">
<widget class="QLineEdit" name="lineEdit_minVTransitionPoints"/> <widget class="QLineEdit" name="lineEdit_minVTransitionPoints"/>
</item> </item>
<item row="9" column="0">
<widget class="QLabel" name="label_edgeBoundaryFilterDist">
<property name="text">
<string>边缘过滤距离 (mm):</string>
</property>
</widget>
</item>
<item row="9" column="1">
<widget class="QLineEdit" name="lineEdit_edgeBoundaryFilterDist"/>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>
@ -296,13 +306,23 @@ QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 3px 0 3px;
<widget class="QLineEdit" name="lineEdit_minInlierRatio"/> <widget class="QLineEdit" name="lineEdit_minInlierRatio"/>
</item> </item>
<item row="7" column="0"> <item row="7" column="0">
<widget class="QLabel" name="label_minHoleDepth">
<property name="text">
<string>最小孔深 (mm):</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QLineEdit" name="lineEdit_minHoleDepth"/>
</item>
<item row="8" column="0">
<widget class="QLabel" name="label_sortMode"> <widget class="QLabel" name="label_sortMode">
<property name="text"> <property name="text">
<string>排序模式:</string> <string>排序模式:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="7" column="1"> <item row="8" column="1">
<widget class="QComboBox" name="comboBox_sortMode"/> <widget class="QComboBox" name="comboBox_sortMode"/>
</item> </item>
</layout> </layout>

View File

@ -20,12 +20,13 @@ struct VrHoleDetectionParam
int neighborCount = 3; // 相邻点连接数 int neighborCount = 3; // 相邻点连接数
double angleThresholdPos = 30.0; // 正角度阈值(度) double angleThresholdPos = 30.0; // 正角度阈值(度)
double angleThresholdNeg = -30.0; // 负角度阈值(度) double angleThresholdNeg = -30.0; // 负角度阈值(度)
double minPitDepth = 1.0; // 最小凹坑深度mm double minPitDepth = 1.0; // 最小凹坑深度mm
double minRadius = 2.0; // 最小孔半径mm double minRadius = 2.0; // 最小孔半径mm
double maxRadius = 50.0; // 最大孔半径mm double maxRadius = 50.0; // 最大孔半径mm
int expansionSize1 = 5; // 第一环扩展大小 int expansionSize1 = 5; // 第一环扩展大小
int expansionSize2 = 10; // 第二环扩展大小 int expansionSize2 = 10; // 第二环扩展大小
int minVTransitionPoints = 1; // V形端点间最小有效过渡点数 int minVTransitionPoints = 1; // V形端点间最小有效过渡点数
double edgeBoundaryFilterDist = 0.0; // 边缘过滤距离mm
}; };
/** /**
@ -40,6 +41,7 @@ struct VrHoleFilterParam
double maxPlaneResidual = 10.0; // 最大平面残差mm double maxPlaneResidual = 10.0; // 最大平面残差mm
double maxAngularGap = 90.0; // 最大角度间隙(度) double maxAngularGap = 90.0; // 最大角度间隙(度)
double minInlierRatio = 0.0; // 最小内点比率 double minInlierRatio = 0.0; // 最小内点比率
double minHoleDepth = 2.5; // 最小孔深mm
}; };
/** /**

View File

@ -15,6 +15,7 @@ SHoleDetectionParams ToAlgoParam(const VrHoleDetectionParam& param)
algo.expansionSize1 = param.expansionSize1; algo.expansionSize1 = param.expansionSize1;
algo.expansionSize2 = param.expansionSize2; algo.expansionSize2 = param.expansionSize2;
algo.minVTransitionPoints = param.minVTransitionPoints; algo.minVTransitionPoints = param.minVTransitionPoints;
algo.edgeBoundaryFilterDist = static_cast<float>(param.edgeBoundaryFilterDist);
return algo; return algo;
} }
@ -28,6 +29,7 @@ SHoleFilterParams ToAlgoParam(const VrHoleFilterParam& param)
algo.maxPlaneResidual = static_cast<float>(param.maxPlaneResidual); algo.maxPlaneResidual = static_cast<float>(param.maxPlaneResidual);
algo.maxAngularGap = static_cast<float>(param.maxAngularGap); algo.maxAngularGap = static_cast<float>(param.maxAngularGap);
algo.minInlierRatio = static_cast<float>(param.minInlierRatio); algo.minInlierRatio = static_cast<float>(param.minInlierRatio);
algo.minHoleDepth = static_cast<float>(param.minHoleDepth);
return algo; return algo;
} }
@ -46,15 +48,16 @@ void LogAlgoParams(const std::string& logTag,
LOG_INFO("%s DetectionParams: angleThresholdPos=%.1f, angleThresholdNeg=%.1f, minPitDepth=%.1f\n", LOG_INFO("%s DetectionParams: angleThresholdPos=%.1f, angleThresholdNeg=%.1f, minPitDepth=%.1f\n",
logTag.c_str(), detectionParams.angleThresholdPos, logTag.c_str(), detectionParams.angleThresholdPos,
detectionParams.angleThresholdNeg, detectionParams.minPitDepth); detectionParams.angleThresholdNeg, detectionParams.minPitDepth);
LOG_INFO("%s DetectionParams: minRadius=%.1f, maxRadius=%.1f, expansionSize1=%d, expansionSize2=%d, minVTransitionPoints=%d\n", LOG_INFO("%s DetectionParams: minRadius=%.1f, maxRadius=%.1f, expansionSize1=%d, expansionSize2=%d, minVTransitionPoints=%d, edgeBoundaryFilterDist=%.2f\n",
logTag.c_str(), detectionParams.minRadius, detectionParams.maxRadius, logTag.c_str(), detectionParams.minRadius, detectionParams.maxRadius,
detectionParams.expansionSize1, detectionParams.expansionSize2, detectionParams.minVTransitionPoints); detectionParams.expansionSize1, detectionParams.expansionSize2,
detectionParams.minVTransitionPoints, detectionParams.edgeBoundaryFilterDist);
LOG_INFO("%s FilterParams: maxEccentricity=%.5f, minAngularCoverage=%.1f, maxRadiusFitRatio=%.2f\n", LOG_INFO("%s FilterParams: maxEccentricity=%.5f, minAngularCoverage=%.1f, maxRadiusFitRatio=%.2f\n",
logTag.c_str(), filterParams.maxEccentricity, filterParams.minAngularCoverage, filterParams.maxRadiusFitRatio); logTag.c_str(), filterParams.maxEccentricity, filterParams.minAngularCoverage, filterParams.maxRadiusFitRatio);
LOG_INFO("%s FilterParams: minQualityScore=%.2f, maxPlaneResidual=%.1f, maxAngularGap=%.1f, minInlierRatio=%.2f\n", LOG_INFO("%s FilterParams: minQualityScore=%.2f, maxPlaneResidual=%.1f, maxAngularGap=%.1f, minInlierRatio=%.2f, minHoleDepth=%.2f\n",
logTag.c_str(), filterParams.minQualityScore, filterParams.maxPlaneResidual, logTag.c_str(), filterParams.minQualityScore, filterParams.maxPlaneResidual,
filterParams.maxAngularGap, filterParams.minInlierRatio); filterParams.maxAngularGap, filterParams.minInlierRatio, filterParams.minHoleDepth);
} }
} // namespace AlgoParamConverter } // namespace AlgoParamConverter

View File

@ -84,6 +84,8 @@ int CVrConfig::LoadConfig(const std::string& filePath, ConfigResult& configResul
configResult.algorithmParams.detectionParam.expansionSize2 = detectionParamElement->IntAttribute("expansionSize2"); configResult.algorithmParams.detectionParam.expansionSize2 = detectionParamElement->IntAttribute("expansionSize2");
if (detectionParamElement->Attribute("minVTransitionPoints")) if (detectionParamElement->Attribute("minVTransitionPoints"))
configResult.algorithmParams.detectionParam.minVTransitionPoints = detectionParamElement->IntAttribute("minVTransitionPoints"); configResult.algorithmParams.detectionParam.minVTransitionPoints = detectionParamElement->IntAttribute("minVTransitionPoints");
if (detectionParamElement->Attribute("edgeBoundaryFilterDist"))
configResult.algorithmParams.detectionParam.edgeBoundaryFilterDist = detectionParamElement->DoubleAttribute("edgeBoundaryFilterDist");
} }
// 解析孔洞过滤参数 // 解析孔洞过滤参数
@ -104,6 +106,8 @@ int CVrConfig::LoadConfig(const std::string& filePath, ConfigResult& configResul
configResult.algorithmParams.filterParam.maxAngularGap = filterParamElement->DoubleAttribute("maxAngularGap"); configResult.algorithmParams.filterParam.maxAngularGap = filterParamElement->DoubleAttribute("maxAngularGap");
if (filterParamElement->Attribute("minInlierRatio")) if (filterParamElement->Attribute("minInlierRatio"))
configResult.algorithmParams.filterParam.minInlierRatio = filterParamElement->DoubleAttribute("minInlierRatio"); configResult.algorithmParams.filterParam.minInlierRatio = filterParamElement->DoubleAttribute("minInlierRatio");
if (filterParamElement->Attribute("minHoleDepth"))
configResult.algorithmParams.filterParam.minHoleDepth = filterParamElement->DoubleAttribute("minHoleDepth");
} }
// 解析排序模式 // 解析排序模式
@ -196,6 +200,7 @@ bool CVrConfig::SaveConfig(const std::string& filePath, ConfigResult& configResu
detectionParamElement->SetAttribute("expansionSize1", configResult.algorithmParams.detectionParam.expansionSize1); detectionParamElement->SetAttribute("expansionSize1", configResult.algorithmParams.detectionParam.expansionSize1);
detectionParamElement->SetAttribute("expansionSize2", configResult.algorithmParams.detectionParam.expansionSize2); detectionParamElement->SetAttribute("expansionSize2", configResult.algorithmParams.detectionParam.expansionSize2);
detectionParamElement->SetAttribute("minVTransitionPoints", configResult.algorithmParams.detectionParam.minVTransitionPoints); detectionParamElement->SetAttribute("minVTransitionPoints", configResult.algorithmParams.detectionParam.minVTransitionPoints);
detectionParamElement->SetAttribute("edgeBoundaryFilterDist", configResult.algorithmParams.detectionParam.edgeBoundaryFilterDist);
algoParamsElement->InsertEndChild(detectionParamElement); algoParamsElement->InsertEndChild(detectionParamElement);
// 添加孔洞过滤参数 // 添加孔洞过滤参数
@ -207,6 +212,7 @@ bool CVrConfig::SaveConfig(const std::string& filePath, ConfigResult& configResu
filterParamElement->SetAttribute("maxPlaneResidual", configResult.algorithmParams.filterParam.maxPlaneResidual); filterParamElement->SetAttribute("maxPlaneResidual", configResult.algorithmParams.filterParam.maxPlaneResidual);
filterParamElement->SetAttribute("maxAngularGap", configResult.algorithmParams.filterParam.maxAngularGap); filterParamElement->SetAttribute("maxAngularGap", configResult.algorithmParams.filterParam.maxAngularGap);
filterParamElement->SetAttribute("minInlierRatio", configResult.algorithmParams.filterParam.minInlierRatio); filterParamElement->SetAttribute("minInlierRatio", configResult.algorithmParams.filterParam.minInlierRatio);
filterParamElement->SetAttribute("minHoleDepth", configResult.algorithmParams.filterParam.minHoleDepth);
algoParamsElement->InsertEndChild(filterParamElement); algoParamsElement->InsertEndChild(filterParamElement);
// 添加排序模式 // 添加排序模式

View File

@ -12,11 +12,13 @@
minPitDepth="1.0" angleStep="1.0" maxScanRadius="100.0" clusterEps="5.0" minPitDepth="1.0" angleStep="1.0" maxScanRadius="100.0" clusterEps="5.0"
clusterMinPoints="5" minRadius="5.0" maxRadius="50.0" expansionSize1="5" clusterMinPoints="5" minRadius="5.0" maxRadius="50.0" expansionSize1="5"
expansionSize2="10" validZThreshold="0.0001" minVTransitionPoints="1" expansionSize2="10" validZThreshold="0.0001" minVTransitionPoints="1"
edgeBoundaryFilterDist="0.0"
cornerScale="2.0" cornerAngleThreshold="45.0" jumpCornerTh_1="10.0" cornerScale="2.0" cornerAngleThreshold="45.0" jumpCornerTh_1="10.0"
jumpCornerTh_2="30.0" minEndingGap="5.0" minEndingGap_z="1.0"/> jumpCornerTh_2="30.0" minEndingGap="5.0" minEndingGap_z="1.0"/>
<FilterParam minHoleRadius="1.0" maxHoleRadius="10.0" maxEccentricity="0.99995" <FilterParam minHoleRadius="1.0" maxHoleRadius="10.0" maxEccentricity="0.99995"
maxCornerRatio="0.15" minAngularCoverage="10.0" maxRadiusFitRatio="0.3" maxCornerRatio="0.15" minAngularCoverage="10.0" maxRadiusFitRatio="0.3"
minQualityScore="0.3" maxPlaneResidual="10.0" maxAngularGap="90.0" minInlierRatio="0.3"/> minQualityScore="0.3" maxPlaneResidual="10.0" maxAngularGap="90.0" minInlierRatio="0.3"
minHoleDepth="2.5"/>
<SortMode value="0"/> <SortMode value="0"/>
<PlaneCalibParams> <PlaneCalibParams>
<CameraCalibParam cameraIndex="1" cameraName="Camera1" isCalibrated="false" planeHeight="-1.0" <CameraCalibParam cameraIndex="1" cameraName="Camera1" isCalibrated="false" planeHeight="-1.0"

View File

@ -1,41 +1,133 @@
#include "mainwindow.h" #include "mainwindow.h"
#include "SingleInstanceManager.h" #include "SingleInstanceManager.h"
#include "AuthView.h" #include "AuthView.h"
#include "AuthManager.h"
#include "HolePitPositionPresenter.h"
#include "IYHolePitPositionStatus.h"
#include "VrLog.h"
#include <QApplication> #include <QApplication>
#include <QIcon> #include <QIcon>
#include <QMessageBox> #include <QMessageBox>
#include <QObject> #include <QObject>
#include <cstdio> #include <cstdio>
#include <cstring>
#ifdef Q_OS_UNIX
#include <cstdlib>
#include <sys/stat.h>
#endif
// 无桌面模式下的状态监听器,将状态信息输出到日志
class HeadlessStatusListener : public IYHolePitPositionStatus
{
public:
void OnStatusUpdate(const std::string& msg) override {
LOG_INFO("[Headless] Status: %s\n", msg.c_str());
}
void OnDetectionResult(const HolePitPositionResult& result) override {
LOG_INFO("[Headless] DetectionResult received, errorCode=%d\n", result.errorCode);
}
void OnCamera1StatusChanged(bool connected) override {
LOG_INFO("[Headless] Camera1: %s\n", connected ? "connected" : "disconnected");
}
void OnCamera2StatusChanged(bool connected) override {
LOG_INFO("[Headless] Camera2: %s\n", connected ? "connected" : "disconnected");
}
void OnRobotConnectionChanged(bool connected) override {
LOG_INFO("[Headless] Robot: %s\n", connected ? "connected" : "disconnected");
}
void OnSerialConnectionChanged(bool connected) override {
LOG_INFO("[Headless] Serial: %s\n", connected ? "connected" : "disconnected");
}
void OnCameraCountChanged(int count) override {
LOG_INFO("[Headless] CameraCount: %d\n", count);
}
void OnWorkStatusChanged(WorkStatus status) override {
LOG_INFO("[Headless] WorkStatus changed: %d\n", static_cast<int>(status));
}
};
#ifdef Q_OS_UNIX
// 检测设备文件是否存在
static bool deviceExists(const char* path)
{
struct stat st;
return stat(path, &st) == 0;
}
// 检测是否有可用的显示设备X11、Wayland、framebuffer、DRM 任意一种即可)
static bool detectDisplay()
{
// X11 / Wayland 环境变量
const char* display = std::getenv("DISPLAY");
const char* waylandDisp = std::getenv("WAYLAND_DISPLAY");
if ((display && strlen(display) > 0) ||
(waylandDisp && strlen(waylandDisp) > 0)) {
return true;
}
// Framebuffer嵌入式 ARM 常见)
if (deviceExists("/dev/fb0")) {
return true;
}
return false;
}
#endif
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
bool hasDisplay = true;
#ifdef Q_OS_UNIX
hasDisplay = detectDisplay();
if (!hasDisplay) {
qputenv("QT_QPA_PLATFORM", "offscreen");
fprintf(stderr, "[Main] No display detected, running in headless mode.\n");
fflush(stderr);
}
#endif
QApplication a(argc, argv); QApplication a(argc, argv);
// 设置应用程序图标
a.setWindowIcon(QIcon(":/common/resource/logo.png"));
// 使用 SingleInstanceManager 检查单例
SingleInstanceManager instanceManager("HolePitPositionApp_SingleInstance_Key"); SingleInstanceManager instanceManager("HolePitPositionApp_SingleInstance_Key");
if (instanceManager.isAnotherInstanceRunning()) { if (hasDisplay) {
// 已有实例在运行,显示提示信息并退出 // ---- 有桌面:完整 UI 模式 ----
QMessageBox::information(nullptr, a.setWindowIcon(QIcon(":/common/resource/logo.png"));
QObject::tr("应用程序已运行"),
QObject::tr("坑孔定位检测应用程序已经在运行中,请勿重复启动!"), if (instanceManager.isAnotherInstanceRunning()) {
QMessageBox::Ok); QMessageBox::information(nullptr,
return 0; QObject::tr("应用程序已运行"),
QObject::tr("坑孔定位检测应用程序已经在运行中,请勿重复启动!"),
QMessageBox::Ok);
return 0;
}
if (!AuthView::CheckAndShow(nullptr)) {
return 0;
}
MainWindow w;
w.show();
return a.exec();
} else {
// ---- 无桌面:只启动业务逻辑 ----
if (instanceManager.isAnotherInstanceRunning()) {
return 0;
}
// 无UI模式下直接检查授权有效性
if (!AuthManager::CheckLicenseValid()) {
fprintf(stderr, "[Main] Authorization check failed, exiting.\n");
fflush(stderr);
return 0;
}
static HeadlessStatusListener listener;
static HolePitPositionPresenter presenter;
presenter.SetStatusCallback<IYHolePitPositionStatus>(&listener);
presenter.Init();
return a.exec();
} }
// 检查授权,无授权则显示授权对话框
if (!AuthView::CheckAndShow(nullptr)) {
// 用户取消授权或授权失败,退出程序
return 0;
}
// 没有其他实例运行,正常启动应用程序
MainWindow w;
w.show();
return a.exec();
} }

View File

@ -5,17 +5,19 @@
#include "ErrorCodes.h" #include "ErrorCodes.h"
#include "../include/VZNL_Types.h" #include "../include/VZNL_Types.h"
// Export macro for Windows DLL // Export macro for shared library
#ifdef _WIN32 #ifdef _WIN32
#ifdef HOLE_DETECTION_EXPORTS #ifdef HOLE_DETECTION_EXPORTS
#define HOLE_DETECTION_API __declspec(dllexport) #define HOLE_DETECTION_API __declspec(dllexport)
#else #else
#define HOLE_DETECTION_API __declspec(dllimport) #define HOLE_DETECTION_API __declspec(dllimport)
#endif #endif
#elif defined(__GNUC__) || defined(__clang__)
#define HOLE_DETECTION_API __attribute__((visibility("default")))
#else #else
#define HOLE_DETECTION_API #ifdef HOLE_DETECTION_EXPORTS
#define HOLE_DETECTION_API __attribute__((visibility("default")))
#else
#define HOLE_DETECTION_API
#endif
#endif #endif
/** /**
@ -82,6 +84,16 @@ struct SClusterInfo {
int count; // Number of points in this cluster int count; // Number of points in this cluster
}; };
/**
* @brief Plane segmentation information for debug callbacks
*/
struct SPlaneSegmentInfo {
float normalX, normalY, normalZ; // 平面法向量 (归一化)
int pointCount; // 该平面的点数
const int* pointIndices; // 点索引数组 (在原始grid中的索引)
float normalAngleDeg; // 法向量与参考方向的夹角 (度)
};
/** /**
* @brief Debug callbacks for visualization and debugging * @brief Debug callbacks for visualization and debugging
* *
@ -149,10 +161,47 @@ struct SHoleDetectionDebugCallbacks {
void* userData void* userData
); );
/**
* @brief Called after RANSAC plane segmentation and normal filtering
* @param planes Array of plane segment information
* @param planeCount Number of planes
* @param totalPointCount Total number of points in the grid (rows * cols)
* @param userData User-provided context pointer
*/
void (*onPlanesSegmented)(
const SPlaneSegmentInfo* planes,
int planeCount,
int totalPointCount,
void* userData
);
/**
* @brief Called after masked points grid is built for a plane
* @param points Masked point array (grid format, rows * cols)
* @param rows Row count
* @param cols Column count
* @param planeIndex Index of the current plane (0-based)
* @param userData User-provided context pointer
*/
void (*onMaskedPointsReady)(
const SVzNLPointXYZ* points,
int rows,
int cols,
int planeIndex,
void* userData
);
/** /**
* @brief User-provided context pointer, passed to all callbacks * @brief User-provided context pointer, passed to all callbacks
*/ */
void* userData; void* userData;
/**
* @brief If non-null, each cluster's boundary points are saved as a CSV file
* in this directory before fitting. Files are named cluster_0.csv, cluster_1.csv, ...
* Fields: row,col,x,y,z
*/
const char* clusterOutputDir;
}; };
/** /**
@ -200,12 +249,4 @@ HOLE_DETECTION_API int DetectMultipleHoles(
*/ */
HOLE_DETECTION_API void PrintDetectionResults(const SMultiHoleResult& result, bool verbose = true); HOLE_DETECTION_API void PrintDetectionResults(const SMultiHoleResult& result, bool verbose = true);
/**
* @brief Print a single hole result to console
*
* @param [in] hole Single hole result
* @param [in] index Hole index (for display purposes)
*/
HOLE_DETECTION_API void PrintHoleResult(const SHoleResult& hole, int index);
#endif // HOLE_DETECTION_H #endif // HOLE_DETECTION_H

View File

@ -2,7 +2,7 @@
#define HOLE_DETECTION_PARAMS_H #define HOLE_DETECTION_PARAMS_H
#include <cmath> #include <cmath>
#include "VZNL_Types.h" // 使用 VZNL_Types.h 中定义的类型 #include "../include/VZNL_Types.h" // 使用 VZNL_Types.h 中定义的类型
/** /**
* @brief * @brief
@ -33,6 +33,9 @@ struct SHoleDetectionParams {
// 当曲率角未超过 angleThreshold 时,若前后参考点间的 // 当曲率角未超过 angleThreshold 时,若前后参考点间的
// 净 Z 坡度角超过此阈值,则将该点判定为下降或上升趋势。 // 净 Z 坡度角超过此阈值,则将该点判定为下降或上升趋势。
// 设置为 0 可禁用坡度补充判断。 // 设置为 0 可禁用坡度补充判断。
float edgeBoundaryFilterDist; // 边缘过滤距离单位mm默认值0.0,即不过滤)
// pair 中任意端点到首/尾有效线(行扫描取 y列扫描取 x
// 的距离小于此值时,该 pair 将被剔除。
// 构造函数,设置默认值 // 构造函数,设置默认值
SHoleDetectionParams() SHoleDetectionParams()
@ -51,6 +54,7 @@ struct SHoleDetectionParams {
, minFeatureSpan(2.0f) , minFeatureSpan(2.0f)
, residualSmoothWindow(5) , residualSmoothWindow(5)
, slopeAngleThreshold(3.0f) , slopeAngleThreshold(3.0f)
, edgeBoundaryFilterDist(0.0f)
{} {}
}; };
@ -78,6 +82,10 @@ struct SHoleFilterParams {
float minInlierRatio; // 圆拟合的最小内点比率默认值0.0 float minInlierRatio; // 圆拟合的最小内点比率默认值0.0
// 表示落在拟合圆容差范围内的点所占比例。 // 表示落在拟合圆容差范围内的点所占比例。
// 深度验证
float minHoleDepth; // 圆孔内部最大 Z 与参考平面 Z 的最小差值单位mm默认值2.5
// 若差值小于此阈值则认为是假孔,返回失败。<=0 表示不过滤。
// 构造函数,设置默认值 // 构造函数,设置默认值
SHoleFilterParams() SHoleFilterParams()
: maxEccentricity(0.99995f) : maxEccentricity(0.99995f)
@ -87,6 +95,7 @@ struct SHoleFilterParams {
, maxPlaneResidual(10.0f) , maxPlaneResidual(10.0f)
, maxAngularGap(90.0f) , maxAngularGap(90.0f)
, minInlierRatio(0.f) , minInlierRatio(0.f)
, minHoleDepth(2.5f)
{} {}
}; };

View File

@ -282,4 +282,109 @@ int ComputeRowZDiffFluctuation(
int* errCode int* errCode
); );
// ========== RANSAC 平面分割 ==========
/**
* @brief RANSAC平面分割参数
*/
struct RansacPlaneSegmentationParams {
float distanceThreshold; // 点到平面距离阈值 (mm), 建议 0.5-2.0
int maxIterations; // RANSAC最大迭代次数, 建议 100-1000
int minPlanePoints; // 最小平面点数, 建议 50-200
int maxPlanes; // 最大提取平面数量, 建议 3-10
float growthZThreshold; // 区域生长时相邻点Z差阈值 (mm), 建议 0.5-2.0
float minPlaneRatio; // 平面最小点数占比 (相对最大平面), 建议 0.05-0.2
RansacPlaneSegmentationParams()
: distanceThreshold(1.0f)
, maxIterations(500)
, minPlanePoints(100)
, maxPlanes(5)
, growthZThreshold(1.0f)
, minPlaneRatio(0.1f)
{}
};
/**
* @brief
*/
struct NormalFilterParams {
float maxAngleDeg; // 法向量与参考方向的最大夹角 (度), 建议 30-60
float refNx, refNy, refNz; // 参考法向量方向 (默认Z轴)
float maxDistFromPlane; // 点到平面的最大允许距离;超出则从 pointIndices 中移除;<=0 表示不过滤
NormalFilterParams()
: maxAngleDeg(45.0f)
, refNx(0.0f)
, refNy(0.0f)
, refNz(1.0f)
, maxDistFromPlane(2.0f)
{}
};
/**
* @brief
*/
struct PlaneSegment {
float a, b, c, d; // 平面方程 ax + by + cz + d = 0 (a,b,c 已归一化)
std::vector<int> pointIndices; // 属于该平面的点索引 (在原始grid中的索引)
int pointCount; // 点数
float normalAngleDeg; // 与参考法向的夹角 (度)
PlaneSegment()
: a(0.0f), b(0.0f), c(1.0f), d(0.0f)
, pointCount(0)
, normalAngleDeg(0.0f)
{}
};
/**
* @brief 使RANSAC方法迭代分割多个平面
*
* :
* 1.
* 2. RANSAC:
* a. 3
* b.
* c.
* d.
* 3.
*
* @param points (grid格式, rows*cols)
* @param rows
* @param cols
* @param params RANSAC参数
* @param outPlanes
* @param errCode
* @return
*/
int SegmentPlanesByRansac(
const SVzNLPointXYZ* points,
int rows,
int cols,
const RansacPlaneSegmentationParams& params,
std::vector<PlaneSegment>& outPlanes,
int* errCode
);
/**
* @brief
*
* 1.
* 2. pointIndices maxDistFromPlane
* maxDistFromPlane <= 0
*
* @param planes ()
* @param params
* @param points
* @param pointCount
* @return
*/
int FilterPlanesByNormal(
std::vector<PlaneSegment>& planes,
const NormalFilterParams& params,
const SVzNLPointXYZ* points,
int pointCount
);
#endif // PLANE_SEGMENTATION_H #endif // PLANE_SEGMENTATION_H

2
Utils

@ -1 +1 @@
Subproject commit 7631f8aadce030e71321af4a4d6c9c2bf8be5b87 Subproject commit 5e978eaea4f7891cc2f4a9ac871598cbb3178f50