feat:博清焊缝算法更新; 应用做点云预处理,扫描结束进行最终检测

This commit is contained in:
YangSn 2026-08-26 14:26:35 +08:00
parent a27176fd9d
commit 379782908a
28 changed files with 1338 additions and 33 deletions

View File

@ -1,6 +1,7 @@
#pragma once
#include <QString>
#include <mutex>
#include <utility>
#include <vector>
@ -16,6 +17,14 @@ public:
DetectPresenter();
static QString GetAlgoVersion();
void ResetIncrementalData();
bool ProcessDetectionLine(EVzResultDataType dataType,
const SVzLaserLineData& lineData,
const VrAlgorithmParams& algorithmParams,
const SSG_planeCalibPara& groundCalibParam,
LaserDataLoader& dataLoader);
bool HasIncrementalData() const;
int DetectWeldSeam(
int cameraIndex,
std::vector<std::pair<EVzResultDataType, SVzLaserLineData>>& laserLines,
@ -24,4 +33,9 @@ public:
const VrDebugParam& debugParam,
LaserDataLoader& dataLoader,
DetectionResult& detectionResult);
private:
mutable std::mutex m_incrementalMutex;
std::vector<std::vector<SVzNL3DPosition>> m_incrementalScanLines;
std::vector<std::vector<SSG_basicFeatureGap>> m_incrementalLineValleys;
};

View File

@ -58,6 +58,10 @@ protected:
int InitAlgoParams() override;
int ProcessAlgoDetection(
std::vector<std::pair<EVzResultDataType, SVzLaserLineData>>& detectionDataCache) override;
bool OnDetectionData(EVzResultDataType dataType,
SVzLaserLineData* laserData) override;
bool HasPendingDetectionData() const override;
void ClearDetectionDataCache() override;
EVzResultDataType GetDetectionDataType() override { return keResultDataType_Position; }
VzNL_OnNotifyStatusCBEx GetCameraStatusCallback() override;
void OnCameraStatusChanged(int cameraIndex, bool isConnected) override;

View File

@ -2,6 +2,7 @@
#include "AlgorithmParamConverter.h"
#include "PointCloudImageUtils.h"
#include "SG_baseAlgo_Export.h"
#include "SX_lapWeldDetection_Export.h"
#include "VrDateUtils.h"
#include "VrError.h"
@ -13,19 +14,6 @@
#include <QDir>
#include <algorithm>
SG_APISHARED_EXPORT void wd_getLineVFlaws(
std::vector<SVzNL3DPosition>& line,
int lineIndex,
SWD_valleyParam valleyParam,
std::vector<SSG_basicFeatureGap>& valleys);
SG_APISHARED_EXPORT void sg_lineGapsGrowing(
int lineIndex,
bool isLastLine,
std::vector<SSG_basicFeatureGap>& features,
std::vector<SSG_gapFeatureTree>& trees,
SSG_treeGrowParam growParam);
namespace {
int ValidPointCount(const std::vector<SVzNL3DPosition>& line)
@ -105,6 +93,49 @@ DetectPresenter::DetectPresenter()
LOG_INFO("lapWeldDetection version: %s\n", wd_weldSeamAlgoVersion());
}
void DetectPresenter::ResetIncrementalData()
{
std::lock_guard<std::mutex> lock(m_incrementalMutex);
m_incrementalScanLines.clear();
m_incrementalLineValleys.clear();
}
bool DetectPresenter::HasIncrementalData() const
{
std::lock_guard<std::mutex> lock(m_incrementalMutex);
return !m_incrementalScanLines.empty();
}
bool DetectPresenter::ProcessDetectionLine(
EVzResultDataType dataType,
const SVzLaserLineData& lineData,
const VrAlgorithmParams& algorithmParams,
const SSG_planeCalibPara& groundCalibParam,
LaserDataLoader& dataLoader)
{
std::vector<std::pair<EVzResultDataType, SVzLaserLineData>> oneLine;
oneLine.emplace_back(dataType, lineData);
std::vector<std::vector<SVzNL3DPosition>> converted;
if (dataLoader.ConvertToSVzNL3DPosition(oneLine, converted) != SUCCESS ||
converted.empty() || converted.front().empty() ||
ValidPointCount(converted.front()) == 0) {
return false;
}
std::vector<SVzNL3DPosition> scanLine = std::move(converted.front());
sx_lineDataR(scanLine, groundCalibParam.planeCalib, -1.0);
const WeldSeamAlgorithmParams params =
AlgorithmParamConverter::ToAlgorithmParams(algorithmParams);
std::vector<SSG_basicFeatureGap> valleys;
std::lock_guard<std::mutex> lock(m_incrementalMutex);
const int lineIndex = static_cast<int>(m_incrementalScanLines.size());
wd_getLineVFlaws(scanLine, lineIndex, params.valleyParam,
params.detectFlawType, valleys);
m_incrementalScanLines.push_back(std::move(scanLine));
m_incrementalLineValleys.push_back(std::move(valleys));
return true;
}
QString DetectPresenter::GetAlgoVersion()
{
return QString::fromUtf8(wd_weldSeamAlgoVersion());
@ -119,17 +150,24 @@ int DetectPresenter::DetectWeldSeam(
LaserDataLoader& dataLoader,
DetectionResult& detectionResult)
{
if (laserLines.empty()) {
detectionResult.message = QStringLiteral("扫描数据为空");
return ERR_CODE(DEV_DATA_INVALID);
}
std::vector<std::vector<SVzNL3DPosition>> scanLines;
const int convertResult = dataLoader.ConvertToSVzNL3DPosition(laserLines, scanLines);
RemoveEmptyEdgeLines(scanLines);
if (convertResult != SUCCESS || scanLines.empty()) {
detectionResult.message = QStringLiteral("扫描数据转换失败");
return ERR_CODE(DEV_DATA_INVALID);
std::vector<std::vector<SSG_basicFeatureGap>> preprocessedValleys;
const bool useIncrementalData = HasIncrementalData();
if (useIncrementalData) {
std::lock_guard<std::mutex> lock(m_incrementalMutex);
scanLines = m_incrementalScanLines;
preprocessedValleys = m_incrementalLineValleys;
if (scanLines.empty() || scanLines.size() != preprocessedValleys.size()) {
detectionResult.message = QStringLiteral("扫描数据为空");
return ERR_CODE(DEV_DATA_INVALID);
}
} else {
const int convertResult = dataLoader.ConvertToSVzNL3DPosition(laserLines, scanLines);
RemoveEmptyEdgeLines(scanLines);
if (convertResult != SUCCESS || scanLines.empty()) {
detectionResult.message = QStringLiteral("扫描数据转换失败");
return ERR_CODE(DEV_DATA_INVALID);
}
}
const WeldSeamAlgorithmParams params = AlgorithmParamConverter::ToAlgorithmParams(algorithmParams);
@ -157,16 +195,22 @@ int DetectPresenter::DetectWeldSeam(
}
CVrTimeUtils timer;
for (auto& line : scanLines) {
sx_lineDataR(line, groundCalibParam.planeCalib, -1.0);
if (!useIncrementalData) {
for (auto& line : scanLines) {
sx_lineDataR(line, groundCalibParam.planeCalib, -1.0);
}
}
std::vector<SSG_gapFeatureTree> trees;
trees.reserve(scanLines.size());
for (size_t lineIndex = 0; lineIndex < scanLines.size(); ++lineIndex) {
std::vector<SSG_basicFeatureGap> lineValleys;
wd_getLineVFlaws(scanLines[lineIndex], static_cast<int>(lineIndex),
params.valleyParam, lineValleys);
if (useIncrementalData) {
lineValleys = preprocessedValleys[lineIndex];
} else {
wd_getLineVFlaws(scanLines[lineIndex], static_cast<int>(lineIndex),
params.valleyParam, params.detectFlawType, lineValleys);
}
const bool isLastLine = lineIndex + 1 == scanLines.size();
sg_lineGapsGrowing(static_cast<int>(lineIndex), isLastLine,
lineValleys, trees, params.treeGrowParam);

View File

@ -85,6 +85,8 @@ int WeldSeamPresenter::InitAlgoParams()
p.treeGrowParam.maxLineSkipNum, p.treeGrowParam.yDeviationMax,
p.treeGrowParam.maxSkipDistance, p.treeGrowParam.zDeviationMax,
p.treeGrowParam.minLTypeTreeLen, p.treeGrowParam.minVTypeTreeLen);
LOG_INFO("WeldSeam flaw type=%d\n",
static_cast<int>(p.weldSeamParam.detectFlawType));
return SUCCESS;
}
@ -179,7 +181,7 @@ QString WeldSeamPresenter::StopDynamicScanAndDetect()
// StopDetect 成功后才检查缓存。空扫描仍属于已受理的 STOP 请求:
// 先让调用方返回 STOP_ACK,再在 Presenter 线程异步发布 RESULT_NG。
if (GetDetectionDataCacheSize() <= 0) {
if (!HasPendingDetectionData()) {
UpdateModbusStatus(2);
OnStatusUpdate("动态扫描已结束,未采集到有效数据");
QMetaObject::invokeMethod(this, [this]() {
@ -240,6 +242,13 @@ int WeldSeamPresenter::ProcessAlgoDetection(
int code = ERR_CODE(DEV_NOT_FIND);
if (m_pDetectPresenter && m_pConfigManager) {
const ConfigResult config = m_pConfigManager->GetConfigResult();
// Live WeldSeam scans are preprocessed line-by-line. The base cache is
// only populated in debug mode for persistence, so release that copy
// before asking DetectPresenter to consume its incremental data.
if (m_pDetectPresenter->HasIncrementalData() && !detectionDataCache.empty()) {
m_dataLoader.FreeLaserScanData(detectionDataCache);
detectionDataCache.clear();
}
code = m_pDetectPresenter->DetectWeldSeam(
m_currentCameraIndex, detectionDataCache, config.algorithmParams,
GetCameraCalibParam(m_currentCameraIndex), config.debugParam, m_dataLoader, result);
@ -264,6 +273,39 @@ int WeldSeamPresenter::ProcessAlgoDetection(
return code;
}
bool WeldSeamPresenter::OnDetectionData(EVzResultDataType dataType,
SVzLaserLineData* laserData)
{
if (!laserData || !m_pDetectPresenter || !m_pConfigManager ||
dataType != keResultDataType_Position) {
return false;
}
const ConfigResult config = m_pConfigManager->GetConfigResult();
const bool processed = m_pDetectPresenter->ProcessDetectionLine(
dataType, *laserData, config.algorithmParams,
GetCameraCalibParam(m_currentCameraIndex), m_dataLoader);
if (!processed) {
LOG_WARNING("WeldSeam failed to preprocess one point-cloud line\n");
return true;
}
// Keep the original line only when debug persistence is enabled. In the
// normal path BasePresenter immediately drops its temporary callback data.
return !config.debugParam.enableDebug;
}
bool WeldSeamPresenter::HasPendingDetectionData() const
{
return m_pDetectPresenter && m_pDetectPresenter->HasIncrementalData();
}
void WeldSeamPresenter::ClearDetectionDataCache()
{
BasePresenter::ClearDetectionDataCache();
if (m_pDetectPresenter) m_pDetectPresenter->ResetIncrementalData();
}
int WeldSeamPresenter::LoadAndDetect(const QString& fileName)
{
ScanState expected = ScanState::Ready;

View File

@ -2,7 +2,7 @@
#define WELDSEAM_VERSION_H
#define WELDSEAM_APP_NAME "焊缝定位"
#define WELDSEAM_VERSION_STRING "1.0.0"
#define WELDSEAM_VERSION_STRING "1.0.1"
#define WELDSEAM_BUILD_STRING "1"
#define WELDSEAM_FULL_VERSION_STRING "V" WELDSEAM_VERSION_STRING "_" WELDSEAM_BUILD_STRING

View File

@ -1,6 +1,8 @@
# WeldSeam 版本记录
# 1.0.1
## build_1
1. 更新算法版本,增加了焊缝类型
## 1.0.0
# 1.0.0
- 使用 VzNLSDK 非摆动动态扫描模式。
- 实现 `START_SCAN` / `STOP_SCAN` TCP 交互流程。

View File

@ -4,7 +4,8 @@ QT += core gui network serialport
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++17
# OpenCV 3.2 still uses std::random_shuffle, which was removed in C++17.
CONFIG += c++14
# 项目名称
TARGET = WeldSeamApp
@ -148,6 +149,8 @@ else:unix:!macx: {
# 焊缝检测算法(lapWeldDetection)
INCLUDEPATH += ../../../AppAlgo/lapWeldDetection/Inc
# OpenCV 3.2 headers required by SG_baseAlgo_Export.h
INCLUDEPATH += $$PWD/../../../SDK/OpenCV320/include
win32:CONFIG(release, debug|release): {
LIBS += -L$$PWD/../../../AppAlgo/lapWeldDetection/Windows/x64/Release -llapWeldDetection -lbaseAlgorithm

View File

@ -11,6 +11,7 @@
#include <QIntValidator>
#include <QLineEdit>
#include <QMessageBox>
#include <QComboBox>
#include <cmath>
#include <cstring>
@ -34,6 +35,10 @@ DialogAlgoArg::DialogAlgoArg(QWidget* parent)
{
ui->setupUi(this);
setWindowTitle(QStringLiteral("焊缝定位 - 算法参数设置"));
ui->comboDetectFlawType->addItem(QStringLiteral("未知/全部"), 0);
ui->comboDetectFlawType->addItem(QStringLiteral("凹陷"), 1);
ui->comboDetectFlawType->addItem(QStringLiteral("凸起"), 2);
ui->comboDetectFlawType->addItem(QStringLiteral("凹陷+凸起"), 3);
InitNumericEditors();
InitCommunicationWidgets();
}
@ -149,6 +154,9 @@ void DialogAlgoArg::Load()
SetDoubleText(ui->editMinLTypeTreeLen, p.treeGrowParam.minLTypeTreeLen);
SetDoubleText(ui->editMinVTypeTreeLen, p.treeGrowParam.minVTypeTreeLen);
SetIntText(ui->editWeldRefPoints, p.weldSeamParam.weldRefPoints);
const int flawType = static_cast<int>(p.weldSeamParam.detectFlawType);
const int flawTypeIndex = ui->comboDetectFlawType->findData(flawType);
ui->comboDetectFlawType->setCurrentIndex(flawTypeIndex >= 0 ? flawTypeIndex : 0);
SetIntText(ui->editTcpPort, config.tcpPort);
LoadCommunicationConfig();
}
@ -225,6 +233,14 @@ bool DialogAlgoArg::Save()
p.treeGrowParam.minVTypeTreeLen)) return false;
if (!TryGetInt(ui->editWeldRefPoints, QStringLiteral("焊缝中间参考点数"),
p.weldSeamParam.weldRefPoints)) return false;
const int flawType = ui->comboDetectFlawType->currentData().toInt();
if (flawType < static_cast<int>(keWD_FlawType_Uknown) ||
flawType > static_cast<int>(keSG_FlawType_Dep_Prot)) {
StyledMessageBox::warning(this, QStringLiteral("参数错误"),
QStringLiteral("焊缝类型无效。"));
return false;
}
p.weldSeamParam.detectFlawType = static_cast<EWD_FlawType>(flawType);
int tcpPort = 0;
if (!TryGetInt(ui->editTcpPort, QStringLiteral("TCP 服务端口"), tcpPort)) return false;

View File

@ -140,6 +140,23 @@ QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 3px 0 3px;
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_detectFlawType">
<property name="text">
<string>焊缝类型:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="comboDetectFlawType">
<property name="minimumSize">
<size>
<width>200</width>
<height>35</height>
</size>
</property>
</widget>
</item>
</layout>
</widget>
</item>

View File

@ -9,6 +9,7 @@ struct WeldSeamAlgorithmParams
SWD_valleyParam valleyParam{};
SSG_treeGrowParam treeGrowParam{};
int weldRefPoints = 3;
EWD_FlawType detectFlawType = keWD_FlawType_Uknown;
};
class AlgorithmParamConverter

View File

@ -7,6 +7,7 @@
#include "VrCommonConfig.h"
#include "VrHandEyeCalibConfig.h"
#include "SG_baseDataType.h"
// 参数默认值与 AppAlgo/lapWeldDetection/lapWeldDetection_test.cpp
// 中 BQ_thinWeldSeamTest 的 BQ_getWeldSeamPose 调用保持一致。
@ -30,6 +31,7 @@ struct VrTreeGrowParam
struct VrWeldSeamParam
{
int weldRefPoints = 3;
EWD_FlawType detectFlawType = keWD_FlawType_Uknown;
};
struct VrAlgorithmParams

View File

@ -14,5 +14,6 @@ WeldSeamAlgorithmParams AlgorithmParamConverter::ToAlgorithmParams(const VrAlgor
result.treeGrowParam.minLTypeTreeLen = params.treeGrowParam.minLTypeTreeLen;
result.treeGrowParam.minVTypeTreeLen = params.treeGrowParam.minVTypeTreeLen;
result.weldRefPoints = params.weldSeamParam.weldRefPoints;
result.detectFlawType = params.weldSeamParam.detectFlawType;
return result;
}

View File

@ -33,6 +33,12 @@ void LoadAlgorithmParams(XMLElement* algoElement, VrAlgorithmParams& params)
if (XMLElement* e = algoElement->FirstChildElement("WeldSeamParam")) {
e->QueryIntAttribute("weldRefPoints", &params.weldSeamParam.weldRefPoints);
int detectFlawType = static_cast<int>(params.weldSeamParam.detectFlawType);
if (e->QueryIntAttribute("detectFlawType", &detectFlawType) == XML_SUCCESS &&
detectFlawType >= static_cast<int>(keWD_FlawType_Uknown) &&
detectFlawType <= static_cast<int>(keSG_FlawType_Dep_Prot)) {
params.weldSeamParam.detectFlawType = static_cast<EWD_FlawType>(detectFlawType);
}
}
// 解析多相机平面校准参数
@ -58,6 +64,8 @@ void SaveAlgorithmParams(XMLDocument& doc, XMLElement* algoElement, const VrAlgo
XMLElement* weldElement = doc.NewElement("WeldSeamParam");
weldElement->SetAttribute("weldRefPoints", params.weldSeamParam.weldRefPoints);
weldElement->SetAttribute("detectFlawType",
static_cast<int>(params.weldSeamParam.detectFlawType));
algoElement->InsertEndChild(weldElement);
// 多相机平面校准参数

View File

@ -9,7 +9,7 @@
<TreeGrowParam maxLineSkipNum="50" yDeviationMax="1"
maxSkipDistance="30" zDeviationMax="5"
minLTypeTreeLen="50" minVTypeTreeLen="50"/>
<WeldSeamParam weldRefPoints="3"/>
<WeldSeamParam weldRefPoints="3" detectFlawType="0"/>
<PlaneCalibParams/>
</AlgorithmParams>

File diff suppressed because it is too large Load Diff

View File

@ -6,6 +6,14 @@
#define PI 3.14159265358979323846
typedef enum
{
keWD_FlawType_Uknown = 0,
keWD_FlawType_Depression, //凹陷
keWD_FlawType_Protrusion, //凸起
keSG_FlawType_Dep_Prot, //凹陷和凸起混合
} EWD_FlawType;
// 定义欧拉角结构体(单位:度)
typedef struct{
double roll; // 绕X轴旋转(横滚)
@ -90,6 +98,12 @@ typedef struct
int end;
}SWD_Interval; //区间
typedef struct SWD_IndexedValue
{
int idx;
double value;
}SWD_IndexedValue;
typedef struct
{
bool validFlag; //指示结果是否有效