This commit is contained in:
YangOne 2026-07-20 11:43:32 +08:00
commit 588b42932d
52 changed files with 771 additions and 362 deletions

View File

@ -8,11 +8,22 @@ ParkingSpaceGuide 使用一台平面相机、一台雷达和一块显示屏完
- 雷达:`Device/RsLidarDevice`
- 显示屏:`Device/LedDisplayDevice`
- 组播:`Nets/UDPClient`
- 2D 机型识别:`AppAlgo/AAPGS_model`RK3588 Linux AArch64
算法入口当前预留在
算法入口位于
`ParkingSpaceGuideApp/Presenter/Src/DetectPresenter.cpp`
机型识别由雷达算法按距离触发,相机软触发取图后调用 AAPGS 分类模型,
当前支持 `A320``B737`。应用默认从可执行文件同级的
`AAPGS_model/models/model.yaml` 加载模型;也可通过环境变量覆盖:
- `AAPGS_MODEL_CONFIG`:模型 YAML 的完整路径
- `AAPGS_MODEL_ROOT`AAPGS SDK 根目录
- `AAPGS_RUNTIME_LIBRARY``libyolo_runtime.so.2` 的完整路径
Windows 构建保留界面与业务流程,但不执行仅支持 RK3588 的 NPU 推理。
文档:
- 详细设计:`详细设计.md`

View File

@ -37,6 +37,9 @@ INCLUDEPATH += $$PWD/../../../Nets/UDPClient/Inc
INCLUDEPATH += $$PWD/../../../Utils/VrCommon/Inc
INCLUDEPATH += $$PWD/../../../Utils/VrUtils/Inc
AAPGS_ROOT = $$clean_path($$PWD/../../../AppAlgo/AAPGS_model)
INCLUDEPATH += $$AAPGS_ROOT/include
win32 {
POCO_ROOT = $$PWD/../../../SDK/poco/Windows
} else:unix:!macx {
@ -61,6 +64,7 @@ win32 {
SOURCES += \
Presenter/Src/ConfigManager.cpp \
Presenter/Src/ParkingSpaceGuidePresenter.cpp \
Presenter/Src/AapgsModelClassifier.cpp \
Presenter/Src/DetectPresenter.cpp \
Presenter/Src/ParkingStatusHttpServer.cpp \
main.cpp \
@ -70,6 +74,7 @@ SOURCES += \
HEADERS += \
Presenter/Inc/ConfigManager.h \
Presenter/Inc/ParkingSpaceGuidePresenter.h \
Presenter/Inc/AapgsModelClassifier.h \
Presenter/Inc/DetectPresenter.h \
Presenter/Inc/ParkingStatusHttpServer.h \
mainwindow.h \
@ -156,6 +161,20 @@ win32 {
unix {
target.path = /usr/lib
aapgsRuntime.path = /usr/lib/AAPGS_model/lib
aapgsRuntime.files = \
$$AAPGS_ROOT/lib/libyolo_runtime.so.2 \
$$AAPGS_ROOT/lib/libyolo_backend_cls.so \
$$AAPGS_ROOT/lib/librknnrt.so \
$$AAPGS_ROOT/lib/librga.so
aapgsModels.path = /usr/lib/AAPGS_model/models
aapgsModels.files = \
$$AAPGS_ROOT/models/model.yaml \
$$AAPGS_ROOT/models/model.rknn
INSTALLS += aapgsRuntime aapgsModels
}
!isEmpty(target.path): INSTALLS += target

View File

@ -1,6 +1,8 @@
#ifndef PARKINGSPACEGUIDE_DETECTPRESENTER_H
#define PARKINGSPACEGUIDE_DETECTPRESENTER_H
#include <memory>
#include <QByteArray>
#include <QImage>
#include <QString>
@ -8,6 +10,8 @@
#include "IRsLidarDevice.h"
#include "IYParkingSpaceGuideStatus.h"
class AapgsModelClassifier;
enum class AirplanePresenceState
{
NotDetected,
@ -51,8 +55,11 @@ struct ModelRecognitionResult
class DetectPresenter
{
public:
DetectPresenter() = default;
~DetectPresenter() = default;
DetectPresenter();
~DetectPresenter();
DetectPresenter(const DetectPresenter&) = delete;
DetectPresenter& operator=(const DetectPresenter&) = delete;
static QString GetAlgoVersion();
@ -69,6 +76,9 @@ public:
int RecognizeModel2D(const QImage& frame,
const QByteArray& recognitionContext,
ModelRecognitionResult& result);
private:
std::unique_ptr<AapgsModelClassifier> m_modelClassifier;
};
#endif // PARKINGSPACEGUIDE_DETECTPRESENTER_H

View File

@ -3,6 +3,15 @@
#include <algorithm>
#include <cmath>
#include "AapgsModelClassifier.h"
DetectPresenter::DetectPresenter()
: m_modelClassifier(std::make_unique<AapgsModelClassifier>())
{
}
DetectPresenter::~DetectPresenter() = default;
bool ModelRecognitionResult::IsVerified(double minimumConfidence) const
{
if (!std::isfinite(minimumConfidence)) {
@ -22,7 +31,7 @@ bool ModelRecognitionResult::IsVerified(double minimumConfidence) const
QString DetectPresenter::GetAlgoVersion()
{
return QStringLiteral("车引导预留算法接口 版本0.1");
return QStringLiteral("机引导算法接口 0.1 / AAPGS_model 1.0.0");
}
int DetectPresenter::DetectAirplanePresence(const RsCloudData& cloud,
@ -81,7 +90,7 @@ int DetectPresenter::DetectParkingSpaceGuide(const RsCloudData& cloud,
info.guideStateCode = 0;
info.guideText = QStringLiteral("预留算法接口");
result.parkingSpaceInfoList.push_back(info);
result.message = QStringLiteral("引导预留算法接口");
result.message = QStringLiteral("引导预留算法接口");
const double modelVerifyDistance = algorithmParams.calibrationParam.modelVerifyDistance;
const bool isInModelVerifyRange = modelVerifyDistance >= 0.0 &&
@ -110,10 +119,25 @@ int DetectPresenter::RecognizeModel2D(const QImage& frame,
const QByteArray& recognitionContext,
ModelRecognitionResult& result)
{
Q_UNUSED(frame);
Q_UNUSED(recognitionContext);
result = ModelRecognitionResult();
result.message = QStringLiteral("机型识别算法接口未实现");
if (!m_modelClassifier) {
result.message = QStringLiteral("AAPGS机型识别器未初始化");
return -1;
}
AapgsModelClassifier::Classification classification;
QString errorMessage;
if (!m_modelClassifier->Classify(frame, classification, errorMessage)) {
result.message = errorMessage.trimmed().isEmpty()
? QStringLiteral("AAPGS机型识别失败")
: errorMessage.trimmed();
return -1;
}
result.modelType = classification.modelType;
result.confidence = classification.confidence;
result.verified = true;
return 0;
}

View File

@ -137,7 +137,7 @@ int ParkingSpaceGuidePresenter::Init()
{
m_reconfiguring.store(false);
NotifyWorkStatus(WorkStatus::InitIng);
NotifyStatus("引导初始化中");
NotifyStatus("引导初始化中");
int ret = InitConfig();
if (ret != 0) {
@ -192,7 +192,7 @@ int ParkingSpaceGuidePresenter::Init()
m_initialized = true;
NotifyWorkStatus(WorkStatus::Ready);
NotifyStatus("引导初始化完成");
NotifyStatus("引导初始化完成");
return 0;
}
@ -229,7 +229,7 @@ bool ParkingSpaceGuidePresenter::StartDetection(int cameraIndex)
}
if (m_detectionRunning.load()) {
NotifyStatus("引导实时检测已在运行");
NotifyStatus("引导实时检测已在运行");
return true;
}
@ -259,12 +259,12 @@ bool ParkingSpaceGuidePresenter::StartDetection(int cameraIndex)
m_detectionRunning.store(false);
m_stopDetectionRequested.store(true);
NotifyWorkStatus(WorkStatus::Error);
NotifyStatus("启动停引导实时检测线程失败");
NotifyStatus("启动停引导实时检测线程失败");
return false;
}
NotifyWorkStatus(WorkStatus::Working);
NotifyStatus("引导实时检测已开始");
NotifyStatus("引导实时检测已开始");
return true;
}
@ -298,19 +298,19 @@ bool ParkingSpaceGuidePresenter::TriggerDetection(int cameraIndex)
}
NotifyWorkStatus(WorkStatus::Detecting);
NotifyStatus("引导单次检测已开始");
NotifyStatus("引导单次检测已开始");
bool success = false;
ParkingGuideAlgorithmState algorithmState;
try {
success = DetectOnce(cloudFrame->cloud, algorithmState);
} catch (const std::exception& e) {
NotifyStatus(std::string("引导单次检测异常:") + e.what());
NotifyStatus(std::string("引导单次检测异常:") + e.what());
} catch (...) {
NotifyStatus("引导单次检测发生未知异常");
NotifyStatus("引导单次检测发生未知异常");
}
NotifyWorkStatus(success ? WorkStatus::Completed : WorkStatus::Error);
NotifyStatus(success ? "引导单次检测完成"
: "引导单次检测失败");
NotifyStatus(success ? "引导单次检测完成"
: "引导单次检测失败");
return success;
}
@ -335,11 +335,11 @@ bool ParkingSpaceGuidePresenter::DetectOnce(const RsCloudData& cloud,
if (ret != 0) {
result.errorCode = ret;
if (result.message.isEmpty()) {
result.message = QStringLiteral("引导检测失败");
result.message = QStringLiteral("引导检测失败");
}
} else {
if (result.parkingSpaceInfoList.empty() && result.message.isEmpty()) {
result.message = QStringLiteral("无停引导结果");
result.message = QStringLiteral("无停引导结果");
}
if (control.needModelRecognition) {
@ -385,9 +385,17 @@ bool ParkingSpaceGuidePresenter::DetectOnce(const RsCloudData& cloud,
info.confidence = control.nextState.modelConfidence;
}
} else {
const QString reason = modelResult.message.trimmed().isEmpty()
? QStringLiteral("机型识别结果未通过验证")
: modelResult.message;
QString reason = modelResult.message.trimmed();
if (reason.isEmpty() && modelRet == 0) {
reason = QStringLiteral(
"机型识别置信度不足:%1实际%2阈值%3")
.arg(modelResult.modelType)
.arg(modelResult.confidence, 0, 'f', 3)
.arg(minimumConfidence, 0, 'f', 3);
}
if (reason.isEmpty()) {
reason = QStringLiteral("机型识别结果未通过验证");
}
appendRecognitionFailure(reason);
}
}
@ -608,10 +616,10 @@ void ParkingSpaceGuidePresenter::DetectionLoop()
DetectOnce(cloudFrame->cloud, algorithmState);
} catch (const std::exception& e) {
PublishErrorResult(-1,
QStringLiteral("引导检测异常:%1")
QStringLiteral("引导检测异常:%1")
.arg(QString::fromUtf8(e.what())));
} catch (...) {
PublishErrorResult(-1, QStringLiteral("引导检测发生未知异常"));
PublishErrorResult(-1, QStringLiteral("引导检测发生未知异常"));
}
}
@ -639,7 +647,7 @@ int ParkingSpaceGuidePresenter::StopDetection()
if (wasRunning) {
NotifyWorkStatus(WorkStatus::Ready);
NotifyStatus("引导实时检测已停止");
NotifyStatus("引导实时检测已停止");
}
return 0;
}

View File

@ -1,7 +1,7 @@
#ifndef PARKINGSPACEGUIDE_VERSION_H
#define PARKINGSPACEGUIDE_VERSION_H
#define PARKINGSPACEGUIDE_APP_NAME "停引导"
#define PARKINGSPACEGUIDE_APP_NAME "停引导"
#define PARKINGSPACEGUIDE_VERSION_STRING "0.1.0"
#define PARKINGSPACEGUIDE_BUILD_STRING "1"
#define PARKINGSPACEGUIDE_FULL_VERSION_STRING "版本" PARKINGSPACEGUIDE_VERSION_STRING "_" PARKINGSPACEGUIDE_BUILD_STRING

View File

@ -23,9 +23,9 @@ DialogAlgoArg::DialogAlgoArg(QWidget* parent)
, ui(new Ui::DialogAlgoArg)
{
ui->setupUi(this);
setWindowTitle(QStringLiteral("引导参数设置"));
setWindowTitle(QStringLiteral("引导参数设置"));
if (ui->label_title) {
ui->label_title->setText(QStringLiteral("引导参数设置"));
ui->label_title->setText(QStringLiteral("引导参数设置"));
}
BuildUi();
connect(ui->tabWidget, &QTabWidget::currentChanged, this, [this](int index) {
@ -101,7 +101,7 @@ void DialogAlgoArg::ShowTabGroup(ConfigPage page)
void DialogAlgoArg::UpdateTitle(ConfigPage page)
{
QString title = QStringLiteral("引导参数设置");
QString title = QStringLiteral("引导参数设置");
switch (page) {
case ConfigPage::Detection:
title = QStringLiteral("检测参数设置");

View File

@ -35,7 +35,7 @@
<string notr="true">color: rgb(221, 225, 233);</string>
</property>
<property name="text">
<string>停引导参数设置</string>
<string>停引导参数设置</string>
</property>
</widget>
<widget class="QTabWidget" name="tabWidget">

View File

@ -17,7 +17,7 @@ int main(int argc, char* argv[])
if (instanceManager.isAnotherInstanceRunning()) {
QMessageBox messageBox;
messageBox.setWindowTitle(QObject::tr("应用已运行"));
messageBox.setText(QObject::tr("引导程序已在运行。"));
messageBox.setText(QObject::tr("引导程序已在运行。"));
messageBox.addButton(QObject::tr("确定"), QMessageBox::AcceptRole);
messageBox.exec();
return 0;

View File

@ -8,7 +8,9 @@
#include <QFileDialog>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QHBoxLayout>
#include <QIcon>
#include <QLabel>
#include <QListWidgetItem>
#include <QMetaObject>
#include <QPixmap>
@ -107,6 +109,39 @@ QString FormatNumber(double value, const QString& unit, int precision = 2)
return QStringLiteral("%1%2").arg(value, 0, 'f', precision).arg(unit);
}
void ConfigureResultWidget(SingleTargetResultWidget* resultWidget)
{
if (!resultWidget) {
return;
}
// Keep this presentation local to ParkingSpaceGuide; UICommon defaults are shared by other apps.
auto* mainLayout = qobject_cast<QVBoxLayout*>(resultWidget->layout());
if (mainLayout && mainLayout->count() > 0) {
auto* headerLayout = qobject_cast<QHBoxLayout*>(mainLayout->itemAt(0)->layout());
if (headerLayout && headerLayout->count() == 2) {
headerLayout->setSpacing(12);
headerLayout->setStretch(0, 0);
headerLayout->setStretch(1, 0);
headerLayout->addStretch(1);
}
}
resultWidget->setStyleSheet(
resultWidget->styleSheet()
+ QStringLiteral(
"#singleTargetTitle { font-size: 28px; }"
"#singleTargetStatus { font-size: 34px; font-weight: 700; padding: 0; }"
"#singleTargetEmpty { font-size: 28px; }"
"#singleTargetFieldName { font-size: 24px; }"
"#singleTargetFieldValue { font-size: 26px; }"));
auto* statusLabel = resultWidget->findChild<QLabel*>(QStringLiteral("singleTargetStatus"));
if (statusLabel) {
statusLabel->setStyleSheet(QStringLiteral("background-color: transparent;"));
}
}
} // namespace
MainWindow::MainWindow(QWidget* parent)
@ -114,9 +149,9 @@ MainWindow::MainWindow(QWidget* parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
setWindowTitle(QStringLiteral("引导"));
setWindowTitle(QStringLiteral("引导"));
if (ui->label) {
ui->label->setText(QStringLiteral("引导"));
ui->label->setText(QStringLiteral("引导"));
}
#ifdef _WIN32
@ -288,6 +323,7 @@ void MainWindow::addDetectionResult(const DetectionResult& result)
resultWidget->setTitle(tr("当前状态"));
resultWidget->setStatus(GuideStateName(ParkingGuideState::Unknown), false);
resultWidget->setEmptyText(tr("暂无停机状态结果"));
ConfigureResultWidget(resultWidget);
return;
}
@ -305,6 +341,7 @@ void MainWindow::addDetectionResult(const DetectionResult& result)
{tr("偏转角"), FormatNumber(info.angle, tr(""))},
{tr("速度"), FormatNumber(info.aircraftSpeed, tr("米/秒"), 2)}
});
ConfigureResultWidget(resultWidget);
}
void MainWindow::OnStatusUpdate(const std::string& statusMessage)
@ -452,7 +489,7 @@ void MainWindow::on_btn_start_clicked()
}
clearDetectionLog();
updateStatusLog(tr("开始停引导检测"));
updateStatusLog(tr("开始停引导检测"));
if (!m_presenter->StartDetection(-1)) {
updateStatusLog(tr("启动检测失败"));
}
@ -686,7 +723,7 @@ void MainWindow::onSaveDetectionData()
}
const std::string timeStamp = CVrDateUtils::GetNowTime();
const QString fileName = QString("引导_%1_%2.txt")
const QString fileName = QString("引导_%1_%2.txt")
.arg(m_presenter->GetDetectIndex())
.arg(QString::fromStdString(timeStamp));
const QString fullPath = QDir(dirPath).filePath(fileName);

View File

@ -11,7 +11,7 @@
</rect>
</property>
<property name="windowTitle">
<string>停引导</string>
<string>停引导</string>
</property>
<property name="autoFillBackground">
<bool>false</bool>
@ -75,7 +75,7 @@ color: rgb(239, 241, 245);</string>
background-color: rgba(255, 255, 255, 0);</string>
</property>
<property name="text">
<string>停引导</string>
<string>停引导</string>
</property>
</widget>
<widget class="QListWidget" name="detect_result_list">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

View File

@ -5,6 +5,7 @@
<COM Name="COM11" Remarks="" Baund="9600" Parity="0" StopBit="0" DataBit="0" FlowCtrl="0" TransMode="0" CharType="0" BreakTime="10" />
<COM Name="COM13" Remarks="" Baund="9600" Parity="0" StopBit="0" DataBit="0" FlowCtrl="0" TransMode="0" CharType="0" BreakTime="10" />
<COM Name="COM9" Remarks="" Baund="9600" Parity="0" StopBit="0" DataBit="0" FlowCtrl="0" TransMode="0" CharType="0" BreakTime="10" />
<COM Name="COM1" Remarks="" Baund="9600" Parity="0" StopBit="0" DataBit="0" FlowCtrl="0" TransMode="0" CharType="0" BreakTime="10" />
<NET Name="PLC_NET" Remarks="" DesIP="127.0.0.1" LocalPortID="502" DesPortID="0" LinkMode="1" LinkResetTime="0" LinkHoldTime="6000" TransMode="2" CharType="0" MaxAsynNum="15" />
</PORT_LIST>
<DEVICE_LIST>
@ -13,7 +14,7 @@
<DATA Name="拍照" BLOCK="2" PrtcTYPE="1" Unit="--" ShowType="0" Addr="1001" Gain="1" Size="1" BitOffset="0" BitNum="16" Range="--" Color="" ByteOder="0" WordOder="0" IntervalTime="0" IsShow="1" IsBRead="1" IsBWrite="0">
<TIMEOUT Time="2000" ResendCount="0" />
<CURVE IsShow="0" />
<VALUE Stategy="0" TimeBngrWay="0" Coef_a="101" Coef_b="0" Coef_c="0" Coef_d="0" Coef_e="0" ResetStep="0" XInvl="1000" MaxRadom="0" MinRadom="0" MaxValue="0" MinValue="0" />
<VALUE Stategy="0" TimeBngrWay="0" Coef_a="201" Coef_b="0" Coef_c="0" Coef_d="0" Coef_e="0" ResetStep="0" XInvl="1000" MaxRadom="0" MinRadom="0" MaxValue="0" MinValue="0" />
</DATA>
<DATA Name="完成" BLOCK="2" PrtcTYPE="1" Unit="--" ShowType="0" Addr="1003" Gain="1" Size="1" BitOffset="0" BitNum="16" Range="--" Color="" ByteOder="0" WordOder="0" IntervalTime="0" IsShow="1" IsBRead="1" IsBWrite="0">
<TIMEOUT Time="2000" ResendCount="0" />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@ -42,15 +42,17 @@
### 4.2 产品型号与拍照触发值D1000
| 序号 | 产品型号 | 触发相机1 | 触发相机2 | 当前状态 |
|------|----------|-----------|-----------|----------|
| 1 | 4WE6 | 1 | 2 | 已实现 |
| 2 | Z2S6 | 101 | 102 | 已实现 |
| 3 | Z2FS6_30_2 | 201 | 202 | 已实现 |
| 4 | DB10 | 301 | 302 | 已实现 |
| 5 | DB20 | 401 | 402 | 已实现 |
|序号| 产品型号 | 算法接口 | 触发相机1 | 触发相机2 | 高度 |
|----|------|--------------|-----------|-----------|------|
| 1 | 4WE6 | `API_1` | 1 | 2 | 47 |
| 2 | Z2S6 | `API_2` | 101 | 102 | 47 |
| 3 | Z2FS6_30_2 | `API_2` | 201 | 202 | 28 |
| 4 | DB10 | `API_3` | 301 | 302 | 60 |
| 5 | DB20 | `API_3` | 401 | 402 | 60 |
五种工件分别读取各自独立配置的检测参数。
> **编号说明**:产品序号即软件工件编号,配置、界面、触发协议和算法分派统一使用 1-5不再转换为算法 test 的历史工件编号。`API_1``API_2``API_3` 分别对应三套工件孔定位接口。
五种工件分别读取其软件工件编号对应的独立检测参数和工具参数。
### 4.3 检测结果状态码D1002

View File

@ -4,14 +4,24 @@
#include <string>
#include <functional>
#include <vector>
#include <cstdint>
#include <QImage>
#include <QMetaType>
// 使用 AppCommon 提供的通用接口和类型
#include "IVisionApplicationStatus.h"
// 算法 workpieceType 的高16位表示方向1=反向0=正向。
inline bool IsReversedWorkpieceType(int workpieceType)
{
const std::uint32_t packedType = static_cast<std::uint32_t>(workpieceType);
return ((packedType >> 16) & 0xFFFFu) == 1u;
}
// 孔位置结构体
struct HolePosition : public PositionData<double> {
int workpieceType = 0; // 算法原始工件类型高16位为方向1=反向0=正向
// 默认构造函数
HolePosition() : PositionData<double>() {}
@ -30,7 +40,7 @@ struct HolePosition : public PositionData<double> {
// 工件孔检测结果结构体
struct WorkpieceHoleDetectionResult : public DetectionResultData<HolePosition> {
int workpieceType = 0; // 工件类型
int workpieceType = 0; // 第一个工件的算法原始类型包含高16位方向
// 默认构造函数
WorkpieceHoleDetectionResult() = default;

View File

@ -33,7 +33,6 @@ public:
const VrDebugParam& debugParam,
LaserDataLoader& dataLoader,
const double clibMatrix[16],
int eulerOrder,
int dirVectorInvert,
int poseOutputOrder,
WorkpieceHoleDetectionResult& detectionResult);

View File

@ -4,21 +4,30 @@
namespace WorkpieceProduct
{
enum class AlgorithmApi
{
Api1,
Api2,
Api3
};
struct ProductInfo
{
int number;
const char* model;
bool detectionSupported;
AlgorithmApi algorithmApi;
int triggerBase; // D1000 触发值 = triggerBase + cameraIndex
};
inline const ProductInfo* Products()
{
static const ProductInfo products[] = {
{1, "4WE6", true},
{2, "Z2S6", true},
{3, "Z2FS6_30_2", true},
{4, "DB10", true},
{5, "DB20", true}
{1, "4WE6", true, AlgorithmApi::Api1, 0},
{2, "Z2S6", true, AlgorithmApi::Api2, 100},
{3, "Z2FS6_30_2", true, AlgorithmApi::Api2, 200},
{4, "DB10", true, AlgorithmApi::Api3, 300},
{5, "DB20", true, AlgorithmApi::Api3, 400}
};
return products;
}
@ -41,7 +50,11 @@ inline const ProductInfo* Find(int workpieceNumber)
inline int TriggerValue(int workpieceNumber, int cameraIndex)
{
return (workpieceNumber - 1) * 100 + cameraIndex;
const ProductInfo* product = Find(workpieceNumber);
if (!product || cameraIndex < 1 || cameraIndex > 2) {
return -1;
}
return product->triggerBase + cameraIndex;
}
inline bool DecodeTriggerValue(int triggerValue, int& workpieceNumber, int& cameraIndex)

View File

@ -1,13 +1,57 @@
#include "DetectPresenter.h"
#include "workpieceHolePositioning_Export.h"
#include "SG_baseAlgo_Export.h"
#include "SG_errCode.h"
#include "AlgoParamConverter.h"
#include "IHandEyeCalib.h"
#include "WorkpieceProduct.h"
#include <cmath>
#include <fstream>
#include <memory>
namespace {
HECEulerOrder ToHandEyeEulerOrder(int eulerOrder)
{
switch (eulerOrder) {
case 10: return HECEulerOrder::XYZ;
case 11: return HECEulerOrder::ZYX;
case 12: return HECEulerOrder::ZXY;
case 13: return HECEulerOrder::YXZ;
case 14: return HECEulerOrder::YZX;
case 15: return HECEulerOrder::XZY;
default:
LOG_WARNING("Unsupported tool euler order %d, fallback to 11 (ZYX)\n", eulerOrder);
return HECEulerOrder::ZYX;
}
}
void ApplyToolRotationToEyeAxes(IHandEyeCalib& handEyeCalib,
HECEulerOrder eulerOrder,
const VrToolParam& toolParam,
std::vector<HECPoint3D>& axes)
{
if (axes.size() != 3 ||
(std::fabs(toolParam.rotX) < 1e-9 &&
std::fabs(toolParam.rotY) < 1e-9 &&
std::fabs(toolParam.rotZ) < 1e-9)) {
return;
}
HECRotationMatrix toolRotation;
handEyeCalib.EulerToRotationMatrix(
HECEulerAngles::fromDegrees(toolParam.rotX, toolParam.rotY, toolParam.rotZ),
eulerOrder,
toolRotation);
const std::vector<HECPoint3D> originalAxes = axes;
for (int column = 0; column < 3; ++column) {
axes[column] = (originalAxes[0] * toolRotation.at(0, column)
+ originalAxes[1] * toolRotation.at(1, column)
+ originalAxes[2] * toolRotation.at(2, column)).normalized();
}
}
bool HasValidZPoint(const std::vector<SVzNL3DPosition>& line)
{
for (const SVzNL3DPosition& point : line) {
@ -64,7 +108,6 @@ int DetectPresenter::DetectWorkpieceHole(
const VrDebugParam& debugParam,
LaserDataLoader& dataLoader,
const double clibMatrix[16],
int eulerOrder,
int dirVectorInvert,
int poseOutputOrder,
WorkpieceHoleDetectionResult& detectionResult)
@ -80,6 +123,8 @@ int DetectPresenter::DetectWorkpieceHole(
LOG_ERROR("Algorithm profile is missing for workpiece %d\n", workpieceNumber);
return ERR_CODE(DEV_CONFIG_ERR);
}
const VrToolParam& toolParam = profile->toolParam;
const HECEulerOrder toolEulerOrder = ToHandEyeEulerOrder(toolParam.eulerOrder);
// 获取当前相机的校准参数
VrCameraPlaneCalibParam cameraCalibParamValue;
@ -136,6 +181,10 @@ int DetectPresenter::DetectWorkpieceHole(
growParam.maxLineSkipNum, growParam.yDeviation_max,
growParam.maxSkipDistance, growParam.zDeviation_max,
growParam.minLTypeTreeLen, growParam.minVTypeTreeLen);
LOG_INFO("Workpiece %d tool params: eulerOrder=%d, rot=(%.3f,%.3f,%.3f), offset=(%.3f,%.3f,%.3f)\n",
workpieceNumber, toolParam.eulerOrder,
toolParam.rotX, toolParam.rotY, toolParam.rotZ,
toolParam.offsetX, toolParam.offsetY, toolParam.offsetZ);
// 准备平面校准参数
SSG_planeCalibPara groundCalibPara = AlgoParamConverter::ToAlgoPlaneCalibParam(cameraCalibParam);
@ -162,22 +211,26 @@ int DetectPresenter::DetectWorkpieceHole(
std::vector<WD_workpieceInfo> workpiecePositioning;
const char* algorithmName = nullptr;
switch (workpieceNumber) {
case 1:
const WorkpieceProduct::ProductInfo* product = WorkpieceProduct::Find(workpieceNumber);
if (!product || !product->detectionSupported) {
LOG_ERROR("No product or detection algorithm for software workpiece %d\n", workpieceNumber);
return ERR_CODE(FUN_UNSUPPORT);
}
switch (product->algorithmApi) {
case WorkpieceProduct::AlgorithmApi::Api1:
algorithmName = "wd_workpieceHolePositioning";
wd_workpieceHolePositioning(
algorithmData, workpiecePara, lineSegPara, filterParam, growParam,
groundCalibPara, workpiecePositioning, &errCode);
break;
case 2:
case 5:
case WorkpieceProduct::AlgorithmApi::Api2:
algorithmName = "wd_workpieceHolePositioning_2";
wd_workpieceHolePositioning_2(
algorithmData, workpiecePara, lineSegPara, filterParam, growParam,
groundCalibPara, workpiecePositioning, &errCode);
break;
case 3:
case 4:
case WorkpieceProduct::AlgorithmApi::Api3:
algorithmName = "wd_workpieceHolePositioning_3";
wd_workpieceHolePositioning_3(
algorithmData, workpiecePara, workpieceParam.bigHoleDiameter,
@ -185,7 +238,7 @@ int DetectPresenter::DetectWorkpieceHole(
workpiecePositioning, &errCode);
break;
default:
LOG_ERROR("No detection algorithm for workpiece %d\n", workpieceNumber);
LOG_ERROR("Unsupported algorithm API for software workpiece %d\n", workpieceNumber);
return ERR_CODE(FUN_UNSUPPORT);
}
@ -213,6 +266,12 @@ int DetectPresenter::DetectWorkpieceHole(
if (i == 0) {
detectionResult.workpieceType = workpiece.workpieceType;
}
if (IsReversedWorkpieceType(workpiece.workpieceType)) {
// 算法执行成功,但反向工件属于异常物料;保留结果继续生成可视化图片。
detectionResult.errorCode = SX_ERR_UNKNOWN_OBJECT;
LOG_WARNING("Detected reversed workpiece %zu (workpieceType=%d)\n",
i + 1, workpiece.workpieceType);
}
// 六轴转换:位置+姿态(转换到机械臂坐标系)
HECPoint3D eyeCenter(workpiece.center.x, workpiece.center.y, workpiece.center.z);
@ -242,6 +301,9 @@ int DetectPresenter::DetectWorkpieceHole(
break;
}
// 工具姿态补偿在 Eye 坐标系内合成,再进入手眼旋转链。
ApplyToolRotationToEyeAxes(*handEyeCalib, toolEulerOrder, toolParam, dirVectors_eye);
// 1. 位置转换R * P_eye + T
HECPoint3D ptRobot;
handEyeCalib->TransformPoint(calibResult.R, calibResult.T, eyeCenter, ptRobot);
@ -264,15 +326,17 @@ int DetectPresenter::DetectWorkpieceHole(
R_pose[2][1] = dirVectors_robot[1].z;
R_pose[2][2] = dirVectors_robot[2].z;
// 4. 使用算法库的 rotationMatrixToEulerZYX 转换为欧拉角(返回角度)
SSG_EulerAngles robotRpy = rotationMatrixToEulerZYX(R_pose);
// 4. 保持既有 ZYX 输出分解约定,确保零工具补偿时输出完全兼容。
// toolParam.eulerOrder 只控制上面的工具补偿旋转合成顺序。
const SSG_EulerAngles robotRpy = rotationMatrixToEulerZYX(R_pose);
// 将机器人坐标系下的位姿添加到positions列表
// 根据 poseOutputOrder 重映射欧拉角输出顺序
HolePosition centerRobotPos;
centerRobotPos.x = ptRobot.x;
centerRobotPos.y = ptRobot.y;
centerRobotPos.z = ptRobot.z;
centerRobotPos.workpieceType = workpiece.workpieceType;
centerRobotPos.x = ptRobot.x + toolParam.offsetX;
centerRobotPos.y = ptRobot.y + toolParam.offsetY;
centerRobotPos.z = ptRobot.z + toolParam.offsetZ;
switch (poseOutputOrder) {
case POSE_ORDER_RPY:
centerRobotPos.roll = robotRpy.roll;
@ -315,9 +379,9 @@ int DetectPresenter::DetectWorkpieceHole(
if(debugParam.enableDebug && debugParam.printDetailLog){
LOG_INFO("[Algo Thread] Direction vectors (eye): X=(%.3f,%.3f,%.3f), Y=(%.3f,%.3f,%.3f), Z=(%.3f,%.3f,%.3f)\n",
workpiece.x_dir.x, workpiece.x_dir.y, workpiece.x_dir.z,
workpiece.y_dir.x, workpiece.y_dir.y, workpiece.y_dir.z,
workpiece.z_dir.x, workpiece.z_dir.y, workpiece.z_dir.z);
dirVectors_eye[0].x, dirVectors_eye[0].y, dirVectors_eye[0].z,
dirVectors_eye[1].x, dirVectors_eye[1].y, dirVectors_eye[1].z,
dirVectors_eye[2].x, dirVectors_eye[2].y, dirVectors_eye[2].z);
LOG_INFO("[Algo Thread] Direction vectors (robot): X=(%.3f,%.3f,%.3f), Y=(%.3f,%.3f,%.3f), Z=(%.3f,%.3f,%.3f)\n",
dirVectors_robot[0].x, dirVectors_robot[0].y, dirVectors_robot[0].z,
dirVectors_robot[1].x, dirVectors_robot[1].y, dirVectors_robot[1].z,
@ -325,22 +389,25 @@ int DetectPresenter::DetectWorkpieceHole(
LOG_INFO("[Algo Thread] Center Eye Coords: X=%.3f, Y=%.3f, Z=%.3f\n",
workpiece.center.x, workpiece.center.y, workpiece.center.z);
LOG_INFO("[Algo Thread] Center Robot Coords: X=%.3f, Y=%.3f, Z=%.3f, R=%.4f, P=%.4f, Y=%.4f\n",
ptRobot.x, ptRobot.y, ptRobot.z,
centerRobotPos.x, centerRobotPos.y, centerRobotPos.z,
centerRobotPos.roll, centerRobotPos.pitch, centerRobotPos.yaw);
}
}
// 使用 PointCloudCanvas 生成点云图像(灰色底图 + 红色孔位标记 + 中心点编号)
// 使用 PointCloudCanvas 生成点云图像(正向红点、反向蓝点 + 中心点编号)
{
PointCloudCanvas canvas = PointCloudCanvas::Create(xyzData);
for (size_t i = 0; i < workpiecePositioning.size(); i++) {
const WD_workpieceInfo& workpiece = workpiecePositioning[i];
// 红色小圆点标记每个孔
const QColor markerColor = IsReversedWorkpieceType(workpiece.workpieceType)
? QColor(0, 0, 255)
: QColor(255, 0, 0);
// 小圆点标记每个孔
for (size_t j = 0; j < workpiece.holes.size(); j++) {
canvas.drawPoint(workpiece.holes[j].x, workpiece.holes[j].y, QColor(255, 0, 0), 4);
canvas.drawPoint(workpiece.holes[j].x, workpiece.holes[j].y, markerColor, 4);
}
// 中心点标记 + 白色编号
canvas.drawPoint(workpiece.center.x, workpiece.center.y, QColor(255, 0, 0), 4);
canvas.drawPoint(workpiece.center.x, workpiece.center.y, markerColor, 4);
canvas.drawText(workpiece.center.x, workpiece.center.y,
QString("%1").arg(i + 1), Qt::white, 14);
}

View File

@ -62,6 +62,14 @@ const char* GetPLCStatusDescription(int statusCode)
}
}
bool ContainsReversedWorkpiece(const WorkpieceHoleDetectionResult& result)
{
return std::any_of(result.positions.begin(), result.positions.end(),
[](const HolePosition& position) {
return IsReversedWorkpieceType(position.workpieceType);
});
}
} // namespace
void ConfigChangeListenerProxy::OnSystemConfigChanged(const SystemConfig& config)
@ -390,16 +398,6 @@ int WorkpieceHolePresenter::ProcessAlgoDetection(std::vector<std::pair<EVzResult
ConfigResult configResult = m_pConfigManager->GetConfigResult();
VrDebugParam debugParam = configResult.debugParam;
// 获取旋转顺序配置(从当前相机对应的标定矩阵获取)
int eulerOrder = 11; // 默认外旋ZYX
int matrixIdx = m_currentCameraIndex - 1;
if (matrixIdx >= 0 && matrixIdx < static_cast<int>(configResult.handEyeCalibMatrixList.size())) {
eulerOrder = configResult.handEyeCalibMatrixList[matrixIdx].eulerOrder;
} else if (!configResult.handEyeCalibMatrixList.empty()) {
eulerOrder = configResult.handEyeCalibMatrixList[0].eulerOrder;
}
LOG_INFO("[Algo Thread] Using euler order: %d\n", eulerOrder);
// 获取方向向量反向配置
int dirVectorInvert = configResult.plcRobotServerConfig.dirVectorInvert;
LOG_INFO("[Algo Thread] Using dir vector invert: %d\n", dirVectorInvert);
@ -412,12 +410,22 @@ int WorkpieceHolePresenter::ProcessAlgoDetection(std::vector<std::pair<EVzResult
int nRet = m_pDetectPresenter->DetectWorkpieceHole(workpieceNumber, m_currentCameraIndex, detectionDataCache,
algorithmParams, debugParam, m_dataLoader,
currentClibMatrix.clibMatrix, eulerOrder, dirVectorInvert,
currentClibMatrix.clibMatrix, dirVectorInvert,
poseOutputOrder, detectionResult);
const bool hasReversedWorkpiece = ContainsReversedWorkpiece(detectionResult);
// 根据项目类型选择处理方式
if (GetStatusCallback<IYWorkpieceHoleStatus>()) {
QString err = QString("错误:%1").arg(nRet);
if (auto pStatus = GetStatusCallback<IYWorkpieceHoleStatus>()) pStatus->OnStatusUpdate(QString("检测%1").arg(SUCCESS == nRet ? "成功": err).toStdString());
QString detectionStatus;
if (nRet != SUCCESS) {
detectionStatus = QString("检测错误:%1").arg(nRet);
} else if (hasReversedWorkpiece) {
detectionStatus = "检测异常:工件反向";
} else {
detectionStatus = "检测成功";
}
if (auto pStatus = GetStatusCallback<IYWorkpieceHoleStatus>()) {
pStatus->OnStatusUpdate(detectionStatus.toStdString());
}
}
LOG_INFO("[Algo Thread] wd_workpieceHolePositioning detected %zu workpieces time : %.2f ms\n", detectionResult.positions.size(), oTimeUtils.GetElapsedTimeInMilliSec());
@ -435,7 +443,9 @@ int WorkpieceHolePresenter::ProcessAlgoDetection(std::vector<std::pair<EVzResult
}
// 更新状态
QString statusMsg = QString("检测完成,发现%1个工件").arg(detectionResult.positions.size());
QString statusMsg = hasReversedWorkpiece
? QString("检测完成,发现%1个工件其中包含反向工件").arg(detectionResult.positions.size())
: QString("检测完成,发现%1个工件").arg(detectionResult.positions.size());
if (auto pStatus = GetStatusCallback<IYWorkpieceHoleStatus>()) pStatus->OnStatusUpdate(statusMsg.toStdString());
// 发送检测结果给TCP客户端
@ -895,7 +905,9 @@ void WorkpieceHolePresenter::SendCoordinateDataToPLC(const WorkpieceHoleDetectio
if (auto pStatus = GetStatusCallback<IYWorkpieceHoleStatus>()) {
switch (plcStatus) {
case PLCModbusClient::RESULT_ABNORMAL_MATERIAL:
pStatus->OnStatusUpdate("检测结果为异常物料已通知PLC");
pStatus->OnStatusUpdate(ContainsReversedWorkpiece(result)
? "检测到反向工件已按异常物料通知PLC"
: "检测结果为异常物料已通知PLC");
break;
case PLCModbusClient::RESULT_NO_PRODUCT:
pStatus->OnStatusUpdate("检测结果为无产品已通知PLC");
@ -926,7 +938,7 @@ void WorkpieceHolePresenter::SendCoordinateDataToPLC(const WorkpieceHoleDetectio
int poseOutputOrder = configResult.plcRobotServerConfig.poseOutputOrder;
LOG_INFO("Using pose output order: %d\n", poseOutputOrder);
// 协议单点输出改为最后一个工件。
// 协议单点输出使用最后一个工件。
const auto& pos = result.positions.back();
PLCModbusClient::CoordinateData coord;

View File

@ -5,8 +5,8 @@
#define WORKPIECEHOLE_APP_NAME "工件孔定位"
// 版本字符串
#define WORKPIECEHOLE_VERSION_STRING "1.2.1"
#define WORKPIECEHOLE_BUILD_STRING "1"
#define WORKPIECEHOLE_VERSION_STRING "1.2.2"
#define WORKPIECEHOLE_BUILD_STRING "2"
#define WORKPIECEHOLE_FULL_VERSION_STRING "V" WORKPIECEHOLE_VERSION_STRING "_" WORKPIECEHOLE_BUILD_STRING
// 构建日期

View File

@ -1,3 +1,8 @@
# 1.2.2
## build_1 2026-07-14
1. 工件3和工件5 反了。
2. 每个工件有独立的工具参数调整
# 1.2.1
## build_1 2026-07-14
1. 更新算法包含1-5工件算法

View File

@ -13,6 +13,7 @@
#include "PathManager.h"
#include "StyledMessageBox.h"
#include "HandEyeCalibWidget.h"
#include "ToolExtrinsicWidget.h"
#include "WorkpieceProduct.h"
#include "VrLog.h"
@ -24,6 +25,7 @@ DialogAlgoarg::DialogAlgoarg(ConfigManager* configManager, QWidget *parent)
, m_currentWorkpieceNumber(1)
, m_isSwitchingWorkpieceProfile(true)
, m_handEyeCalibWidget(nullptr)
, m_toolExtrinsicWidget(nullptr)
{
try {
ui->setupUi(this);
@ -52,6 +54,9 @@ DialogAlgoarg::DialogAlgoarg(ConfigManager* configManager, QWidget *parent)
// 初始化工件参数组下拉框
InitWorkpieceProfileComboBox();
// 初始化工具参数 tab工具参数由顶部工件选择器驱动
InitToolExtrinsicTab();
// 从配置文件加载数据到界面
LoadConfigToUI();
m_isSwitchingWorkpieceProfile = false;
@ -156,17 +161,39 @@ void DialogAlgoarg::LoadCurrentWorkpieceProfileToUI()
LoadLineSegParamToUI(profile.lineSegParam);
LoadFilterParamToUI(profile.filterParam);
LoadGrowParamToUI(profile.growParam);
if (m_toolExtrinsicWidget) {
m_toolExtrinsicWidget->setData(
profile.toolParam.eulerOrder,
profile.toolParam.rotX, profile.toolParam.rotY, profile.toolParam.rotZ,
profile.toolParam.offsetX, profile.toolParam.offsetY, profile.toolParam.offsetZ);
}
}
bool DialogAlgoarg::SaveCurrentWorkpieceProfileFromUI()
{
if (m_toolExtrinsicWidget && !m_toolExtrinsicWidget->hasAcceptableInput()) {
return false;
}
VrWorkpieceAlgorithmProfile& profile =
m_algorithmParams.GetOrCreateWorkpieceProfile(m_currentWorkpieceNumber);
return SaveWorkpieceHoleParamFromUI(profile.workpieceHoleParam) &&
SaveLineSegParamFromUI(profile.lineSegParam) &&
SaveFilterParamFromUI(profile.filterParam) &&
SaveGrowParamFromUI(profile.growParam);
if (!SaveWorkpieceHoleParamFromUI(profile.workpieceHoleParam) ||
!SaveLineSegParamFromUI(profile.lineSegParam) ||
!SaveFilterParamFromUI(profile.filterParam) ||
!SaveGrowParamFromUI(profile.growParam)) {
return false;
}
if (m_toolExtrinsicWidget) {
m_toolExtrinsicWidget->getData(
profile.toolParam.eulerOrder,
profile.toolParam.rotX, profile.toolParam.rotY, profile.toolParam.rotZ,
profile.toolParam.offsetX, profile.toolParam.offsetY, profile.toolParam.offsetZ);
}
return true;
}
void DialogAlgoarg::on_comboBox_workpieceProfile_currentIndexChanged(int index)
@ -243,8 +270,9 @@ bool DialogAlgoarg::SaveConfigFromUI()
}
try {
// 获取当前配置
SystemConfig systemConfig = m_pConfigManager->GetConfig();
// 基于当前配置构造候选值;落盘失败时恢复运行时配置。
const SystemConfig previousSystemConfig = m_pConfigManager->GetConfig();
SystemConfig systemConfig = previousSystemConfig;
VrAlgorithmParams& algoParams = systemConfig.configResult.algorithmParams;
// 先暂存当前工件,再一次性保存所有工件参数组。
@ -300,7 +328,12 @@ bool DialogAlgoarg::SaveConfigFromUI()
return false;
}
return m_pConfigManager->SaveConfigToFile(m_configFilePath.toStdString());
if (!m_pConfigManager->SaveConfigToFile(m_configFilePath.toStdString())) {
m_pConfigManager->UpdateFullConfig(previousSystemConfig);
return false;
}
return true;
} catch (const std::exception& e) {
return false;
@ -407,6 +440,21 @@ void DialogAlgoarg::on_btn_camer_cancel_clicked()
reject();
}
// ========== 工具参数相关实现 ==========
void DialogAlgoarg::InitToolExtrinsicTab()
{
if (!ui || !ui->verticalLayout_toolExtrinsicHost || m_toolExtrinsicWidget) {
return;
}
m_toolExtrinsicWidget = new ToolExtrinsicWidget(this);
m_toolExtrinsicWidget->setCameraSelectorVisible(false);
m_toolExtrinsicWidget->setTargetOffsetVisible(true);
m_toolExtrinsicWidget->setAxialAngleRangeVisible(false);
ui->verticalLayout_toolExtrinsicHost->addWidget(m_toolExtrinsicWidget);
}
// ========== 手眼标定相关实现 ==========
void DialogAlgoarg::InitHandEyeCalibTab()

View File

@ -7,6 +7,7 @@
#include "ConfigManager.h"
class HandEyeCalibWidget;
class ToolExtrinsicWidget;
namespace Ui {
class DialogAlgoarg;
@ -59,6 +60,9 @@ private:
void InitHandEyeCalibTab();
void InitEulerOrderComboBox();
// 工具参数
void InitToolExtrinsicTab();
private:
Ui::DialogAlgoarg *ui;
ConfigManager* m_pConfigManager;
@ -69,6 +73,9 @@ private:
// 手眼标定共享控件
HandEyeCalibWidget* m_handEyeCalibWidget;
// 工具参数共享控件
ToolExtrinsicWidget* m_toolExtrinsicWidget;
};
#endif // DIALOGALGOARG_H

View File

@ -774,6 +774,12 @@ QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 3px 0 3px;
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_toolExtrinsic">
<attribute name="title">
<string>工具参数</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_toolExtrinsicHost"/>
</widget>
<widget class="QWidget" name="tab_network">
<attribute name="title">
<string>网络配置</string>

View File

@ -434,7 +434,7 @@ int MainWindow::selectWorkpieceForDetection()
layout.setContentsMargins(20, 20, 20, 20);
layout.setSpacing(12);
QLabel header(tr("序号 产品型号"), &dialog);
QLabel header(tr("显示序号 - 产品型号"), &dialog);
layout.addWidget(&header);
QListWidget productList(&dialog);
@ -446,7 +446,7 @@ int MainWindow::selectWorkpieceForDetection()
const WorkpieceProduct::ProductInfo* products = WorkpieceProduct::Products();
for (int i = 0; i < WorkpieceProduct::ProductCount(); ++i) {
const WorkpieceProduct::ProductInfo& product = products[i];
QString text = QString("%1 %2")
QString text = tr("%1 - %2")
.arg(product.number)
.arg(QString::fromLatin1(product.model));
if (!product.detectionSupported) {

View File

@ -26,7 +26,7 @@ struct VrOutlierFilterParam
*/
struct VrLineSegParam
{
double distScale = 3.0; // 计算方向角的窗口比例因子
double distScale = 10.0; // 计算方向角的窗口比例因子
double segGapTh_y = 15.0; // Y方向间隔阈值大于此值认为是分段
double segGapTh_z = 0.0; // Z方向间隔阈值大于此值认为是分段
};
@ -51,7 +51,7 @@ struct VrWorkpieceHoleParam
{
int workpieceType = 0; // 工件类型
double holeDiameter = 6.0; // 孔直径 (mm)
double bigHoleDiameter = 33.0; // 大孔直径 (mm)工件3算法使用
double bigHoleDiameter = 33.0; // 大孔直径 (mm)API_3 算法使用
double holeDist_L = 40.0; // 孔间距_长 (mm)
double holeDist_W = 32.0; // 孔间距_宽 (mm)
double xLen = 44.0; // x轴方向长度用于绘制平面 (mm)
@ -59,6 +59,22 @@ struct VrWorkpieceHoleParam
double H = 47.0; // 高度 (mm)
};
/**
* @brief
*
* 姿
*/
struct VrToolParam
{
int eulerOrder = 11; // 欧拉角旋转顺序,默认外旋 ZYX
double rotX = 0.0; // 工具姿态 X 轴补偿 (deg)
double rotY = 0.0; // 工具姿态 Y 轴补偿 (deg)
double rotZ = 0.0; // 工具姿态 Z 轴补偿 (deg)
double offsetX = 0.0; // 目标点 X 方向偏移 (mm)
double offsetY = 0.0; // 目标点 Y 方向偏移 (mm)
double offsetZ = 0.0; // 目标点 Z 方向偏移 (mm)
};
/**
* @brief PLC Modbus
*
@ -198,6 +214,7 @@ struct VrWorkpieceAlgorithmProfile
VrLineSegParam lineSegParam;
VrOutlierFilterParam filterParam;
VrTreeGrowParam growParam;
VrToolParam toolParam;
VrWorkpieceAlgorithmProfile() = default;
@ -220,7 +237,7 @@ struct VrWorkpieceAlgorithmProfile
workpieceHoleParam.yLen = 70.0;
workpieceHoleParam.H = 47.0;
lineSegParam.distScale = 3.0;
lineSegParam.distScale = 10.0;
lineSegParam.segGapTh_y = 15.0;
lineSegParam.segGapTh_z = 0.0;
@ -234,13 +251,29 @@ struct VrWorkpieceAlgorithmProfile
growParam.minLTypeTreeLen = 2.0;
growParam.minVTypeTreeLen = 2.0;
if (number == 3 || number == 4) {
toolParam = VrToolParam();
// 参数编号始终采用软件产品顺序,不依赖算法 test 的历史工件编号。
switch (number) {
case 1: // 4WE6 / API_1 / 原 test1
break;
case 2: // Z2S6 / API_2 / 原 test2
workpieceHoleParam.holeDiameter = 8.0;
break;
case 3: // Z2FS6_30_2 / API_2 / 原 test5
workpieceHoleParam.H = 28.0;
lineSegParam.distScale = 14.0;
growParam.minLTypeTreeLen = 1.5;
growParam.minVTypeTreeLen = 1.5;
break;
case 4: // DB10 / API_3 / 原 test3
case 5: // DB20 / API_3 / 原 test4
workpieceHoleParam.H = 60.0;
lineSegParam.distScale = 30.0;
lineSegParam.segGapTh_y = 40.0;
} else if (number == 5) {
workpieceHoleParam.holeDiameter = 10.0;
workpieceHoleParam.H = 28.0;
break;
default:
break;
}
}
};

View File

@ -10,6 +10,23 @@ using namespace tinyxml2;
namespace {
constexpr int kSoftwareProductNumberingVersion = 2;
int ToSoftwareWorkpieceNumber(int storedNumber, bool usesSoftwareProductNumbering)
{
if (usesSoftwareProductNumbering) {
return storedNumber;
}
// 旧配置按算法 test 编号保存;运行时统一转换为软件产品编号。
switch (storedNumber) {
case 3: return 4;
case 4: return 5;
case 5: return 3;
default: return storedNumber;
}
}
void LoadWorkpieceProfileParams(
XMLElement* parentElement,
VrWorkpieceAlgorithmProfile& profile)
@ -75,6 +92,25 @@ void LoadWorkpieceProfileParams(
if (growParamElement->Attribute("minVTypeTreeLen"))
profile.growParam.minVTypeTreeLen = growParamElement->DoubleAttribute("minVTypeTreeLen");
}
XMLElement* toolParamElement = parentElement->FirstChildElement("ToolParam");
if (toolParamElement)
{
if (toolParamElement->Attribute("eulerOrder"))
profile.toolParam.eulerOrder = toolParamElement->IntAttribute("eulerOrder");
if (toolParamElement->Attribute("rotX"))
profile.toolParam.rotX = toolParamElement->DoubleAttribute("rotX");
if (toolParamElement->Attribute("rotY"))
profile.toolParam.rotY = toolParamElement->DoubleAttribute("rotY");
if (toolParamElement->Attribute("rotZ"))
profile.toolParam.rotZ = toolParamElement->DoubleAttribute("rotZ");
if (toolParamElement->Attribute("offsetX"))
profile.toolParam.offsetX = toolParamElement->DoubleAttribute("offsetX");
if (toolParamElement->Attribute("offsetY"))
profile.toolParam.offsetY = toolParamElement->DoubleAttribute("offsetY");
if (toolParamElement->Attribute("offsetZ"))
profile.toolParam.offsetZ = toolParamElement->DoubleAttribute("offsetZ");
}
}
void SaveWorkpieceProfileParams(
@ -116,6 +152,16 @@ void SaveWorkpieceProfileParams(
growParamElement->SetAttribute("minLTypeTreeLen", profile.growParam.minLTypeTreeLen);
growParamElement->SetAttribute("minVTypeTreeLen", profile.growParam.minVTypeTreeLen);
profileElement->InsertEndChild(growParamElement);
XMLElement* toolParamElement = doc.NewElement("ToolParam");
toolParamElement->SetAttribute("eulerOrder", profile.toolParam.eulerOrder);
toolParamElement->SetAttribute("rotX", profile.toolParam.rotX);
toolParamElement->SetAttribute("rotY", profile.toolParam.rotY);
toolParamElement->SetAttribute("rotZ", profile.toolParam.rotZ);
toolParamElement->SetAttribute("offsetX", profile.toolParam.offsetX);
toolParamElement->SetAttribute("offsetY", profile.toolParam.offsetY);
toolParamElement->SetAttribute("offsetZ", profile.toolParam.offsetZ);
profileElement->InsertEndChild(toolParamElement);
}
} // namespace
@ -149,6 +195,14 @@ int CVrConfig::LoadConfig(const std::string& filePath, ConfigResult& configResul
return LOAD_CONFIG_INVALID_FORMAT;
}
const int productNumberingVersion =
root->IntAttribute("productNumberingVersion", 1);
const bool usesSoftwareProductNumbering =
productNumberingVersion >= kSoftwareProductNumberingVersion;
if (!usesSoftwareProductNumbering) {
LOG_INFO("Migrating legacy workpiece profiles to software product numbering\n");
}
// 解析摄像头列表
ConfigXmlUtils::LoadCameraList(root, configResult.cameraList);
@ -181,7 +235,10 @@ int CVrConfig::LoadConfig(const std::string& filePath, ConfigResult& configResul
XMLElement* profileElement = algoParamsElement->FirstChildElement("WorkpieceParamGroup");
if (profileElement) {
while (profileElement) {
const int workpieceNumber = profileElement->IntAttribute("workpieceNumber", 0);
const int storedWorkpieceNumber =
profileElement->IntAttribute("workpieceNumber", 0);
const int workpieceNumber = ToSoftwareWorkpieceNumber(
storedWorkpieceNumber, usesSoftwareProductNumbering);
VrWorkpieceAlgorithmProfile* workpieceProfile =
configResult.algorithmParams.FindWorkpieceProfile(workpieceNumber);
if (workpieceProfile) {
@ -265,6 +322,7 @@ bool CVrConfig::SaveConfig(const std::string& filePath, ConfigResult& configResu
// 创建根元素
XMLElement* root = doc.NewElement("WorkpieceHoleConfig");
root->SetAttribute("productNumberingVersion", kSoftwareProductNumberingVersion);
doc.InsertEndChild(root);
// 添加摄像头列表

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<WorkpieceHoleConfig>
<WorkpieceHoleConfig productNumberingVersion="2">
<Cameras>
<Camera name="Camera1" ip="192.168.1.100"/>
</Cameras>
@ -10,32 +10,37 @@
<WorkpieceParamGroup workpieceNumber="1">
<WorkpieceHoleParam workpieceType="0" holeDiameter="6.0" bigHoleDiameter="33.0" holeDist_L="40.0" holeDist_W="32.0" xLen="44.0" yLen="70.0" H="47.0"/>
<FilterParam continuityTh="20.0" outlierTh="5.0"/>
<LineSegParam distScale="3.0" segGapTh_y="15.0" segGapTh_z="0.0"/>
<LineSegParam distScale="10.0" segGapTh_y="15.0" segGapTh_z="0.0"/>
<GrowParam maxLineSkipNum="2" yDeviation_max="4.0" maxSkipDistance="0.0" zDeviation_max="10.0" minLTypeTreeLen="2.0" minVTypeTreeLen="2.0"/>
<ToolParam eulerOrder="11" rotX="0.0" rotY="0.0" rotZ="0.0" offsetX="0.0" offsetY="0.0" offsetZ="0.0"/>
</WorkpieceParamGroup>
<WorkpieceParamGroup workpieceNumber="2">
<WorkpieceHoleParam workpieceType="0" holeDiameter="6.0" bigHoleDiameter="33.0" holeDist_L="40.0" holeDist_W="32.0" xLen="44.0" yLen="70.0" H="47.0"/>
<WorkpieceHoleParam workpieceType="0" holeDiameter="8.0" bigHoleDiameter="33.0" holeDist_L="40.0" holeDist_W="32.0" xLen="44.0" yLen="70.0" H="47.0"/>
<FilterParam continuityTh="5.0" outlierTh="5.0"/>
<LineSegParam distScale="3.0" segGapTh_y="15.0" segGapTh_z="0.0"/>
<LineSegParam distScale="10.0" segGapTh_y="15.0" segGapTh_z="0.0"/>
<GrowParam maxLineSkipNum="2" yDeviation_max="4.0" maxSkipDistance="0.0" zDeviation_max="10.0" minLTypeTreeLen="2.0" minVTypeTreeLen="2.0"/>
<ToolParam eulerOrder="11" rotX="0.0" rotY="0.0" rotZ="0.0" offsetX="0.0" offsetY="0.0" offsetZ="0.0"/>
</WorkpieceParamGroup>
<WorkpieceParamGroup workpieceNumber="3">
<WorkpieceHoleParam workpieceType="0" holeDiameter="6.0" bigHoleDiameter="33.0" holeDist_L="40.0" holeDist_W="32.0" xLen="44.0" yLen="70.0" H="60.0"/>
<WorkpieceHoleParam workpieceType="0" holeDiameter="6.0" bigHoleDiameter="33.0" holeDist_L="40.0" holeDist_W="32.0" xLen="44.0" yLen="70.0" H="28.0"/>
<FilterParam continuityTh="5.0" outlierTh="5.0"/>
<LineSegParam distScale="30.0" segGapTh_y="40.0" segGapTh_z="0.0"/>
<GrowParam maxLineSkipNum="2" yDeviation_max="4.0" maxSkipDistance="0.0" zDeviation_max="10.0" minLTypeTreeLen="2.0" minVTypeTreeLen="2.0"/>
<LineSegParam distScale="14.0" segGapTh_y="15.0" segGapTh_z="0.0"/>
<GrowParam maxLineSkipNum="2" yDeviation_max="4.0" maxSkipDistance="0.0" zDeviation_max="10.0" minLTypeTreeLen="1.5" minVTypeTreeLen="1.5"/>
<ToolParam eulerOrder="11" rotX="0.0" rotY="0.0" rotZ="0.0" offsetX="0.0" offsetY="0.0" offsetZ="0.0"/>
</WorkpieceParamGroup>
<WorkpieceParamGroup workpieceNumber="4">
<WorkpieceHoleParam workpieceType="0" holeDiameter="6.0" bigHoleDiameter="33.0" holeDist_L="40.0" holeDist_W="32.0" xLen="44.0" yLen="70.0" H="60.0"/>
<FilterParam continuityTh="5.0" outlierTh="5.0"/>
<LineSegParam distScale="30.0" segGapTh_y="40.0" segGapTh_z="0.0"/>
<GrowParam maxLineSkipNum="2" yDeviation_max="4.0" maxSkipDistance="0.0" zDeviation_max="10.0" minLTypeTreeLen="2.0" minVTypeTreeLen="2.0"/>
<ToolParam eulerOrder="11" rotX="0.0" rotY="0.0" rotZ="0.0" offsetX="0.0" offsetY="0.0" offsetZ="0.0"/>
</WorkpieceParamGroup>
<WorkpieceParamGroup workpieceNumber="5">
<WorkpieceHoleParam workpieceType="0" holeDiameter="10.0" bigHoleDiameter="33.0" holeDist_L="40.0" holeDist_W="32.0" xLen="44.0" yLen="70.0" H="28.0"/>
<WorkpieceHoleParam workpieceType="0" holeDiameter="6.0" bigHoleDiameter="33.0" holeDist_L="40.0" holeDist_W="32.0" xLen="44.0" yLen="70.0" H="60.0"/>
<FilterParam continuityTh="5.0" outlierTh="5.0"/>
<LineSegParam distScale="3.0" segGapTh_y="15.0" segGapTh_z="0.0"/>
<LineSegParam distScale="30.0" segGapTh_y="40.0" segGapTh_z="0.0"/>
<GrowParam maxLineSkipNum="2" yDeviation_max="4.0" maxSkipDistance="0.0" zDeviation_max="10.0" minLTypeTreeLen="2.0" minVTypeTreeLen="2.0"/>
<ToolParam eulerOrder="11" rotX="0.0" rotY="0.0" rotZ="0.0" offsetX="0.0" offsetY="0.0" offsetZ="0.0"/>
</WorkpieceParamGroup>
<PlaneCalibParams>
<CameraCalibParam cameraIndex="1" cameraName="Camera1" isCalibrated="false" planeHeight="-1.0"

View File

@ -12,6 +12,7 @@
#define SG_ERR_SCAN_DIR_NOT_SUPPORTED -1009
#define SX_ERR_ZERO_OBJECTS -1010
#define SX_ERR_NULL_GROUND_PLANE -1011
#define SX_ERR_ZERO_2D_OBJECTS -1012
//BQ_workpiece
#define SX_ERR_INVLD_VTREE_NUM -2001

View File

@ -189,7 +189,7 @@ void _outputWorkpieceInfo(char* fileName, std::vector< WD_workpieceInfo>& workpi
int number = (int)workpiecePositioning.size();
for (int i = 0; i < number; i++)
{
sprintf_s(dataStr, 250, "工件_%d", i + 1);
sprintf_s(dataStr, 250, "工件_%d: type=%x", i + 1, workpiecePositioning[i].workpieceType);
sw << dataStr << std::endl;
int holeNumber = (int)workpiecePositioning[i].holes.size();
for (int j = 0; j < holeNumber; j++)
@ -947,11 +947,15 @@ void _XOYprojection(
if (i == 0)
{
rgb = { 255, 0, 0 };
if((holes[i].workpieceType&0xffff0000) > 0)//方向为反
rgb = { 255, 0, 255 };
size = int(3.0/scale);
}
else
{
rgb = { 255, 255, 0 };
if ((holes[i].workpieceType & 0xffff0000) > 0)//方向为反
rgb = { 0, 255, 255 };
size = int(3.0 / scale);
}
WD_workpieceInfo& a_hole = holes[i];
@ -1569,7 +1573,7 @@ void TuoPuFa_holePosition_test3(void)
};
SVzNLRange fileIdx[TPF3_TEST_GROUP] = {
{1,10}, {1, 10}, {1,10}, {1,10}, {1, 10},
{1,11}, {1, 10}, {1,10}, {1,10}, {1, 10},
{1,10}, {1, 10},
};
@ -1635,7 +1639,7 @@ void TuoPuFa_holePosition_test3(void)
for (int fidx = fileIdx[grp].nMin; fidx <= fileIdx[grp].nMax; fidx++)
{
//fidx = 2;
//fidx = 3;
char _scan_file[256];
sprintf_s(_scan_file, "%s%d_LaserData_Jh26B074.txt", dataPath[grp], fidx);
std::vector<std::vector< SVzNL3DPosition>> scanLines;
@ -2736,11 +2740,11 @@ int main()
TuoPuFa_holePosition_test5();
else if (keSG_拓普发孔定位_工件全测试 == testMode)
{
TuoPuFa_holePosition_test();
TuoPuFa_holePosition_test2();
//TuoPuFa_holePosition_test();
//TuoPuFa_holePosition_test2();
TuoPuFa_holePosition_test3();
TuoPuFa_holePosition_test4();
TuoPuFa_holePosition_test5();
//TuoPuFa_holePosition_test5();
}
else if (keSG_华航孔定位 == testMode)
HuaHang_holePosition_test();

View File

@ -90,6 +90,9 @@ public:
double& axialAngleMin, double& axialAngleMax) const;
int currentCameraIndex() const;
bool hasAcceptableInput() const;
// 单条目编辑场景可隐藏控件内置选择器,由外部选择器驱动 setData/getData。
void setCameraSelectorVisible(bool visible);
void setTargetOffsetVisible(bool visible);
void setAxialAngleRangeVisible(bool visible);
@ -108,6 +111,7 @@ private:
Ui::ToolExtrinsicWidget* ui = nullptr;
QGroupBox* m_groupExtrinsic;
QLabel* m_labelCamera;
QComboBox* m_comboCamera;
QComboBox* m_comboEulerOrder;
QLineEdit* m_editRotX;

View File

@ -10,6 +10,7 @@ ToolExtrinsicWidget::ToolExtrinsicWidget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::ToolExtrinsicWidget)
, m_groupExtrinsic(nullptr)
, m_labelCamera(nullptr)
, m_comboCamera(nullptr)
, m_comboEulerOrder(nullptr)
, m_editRotX(nullptr)
@ -206,6 +207,29 @@ int ToolExtrinsicWidget::currentCameraIndex() const
return m_comboCamera->currentData().toInt();
}
bool ToolExtrinsicWidget::hasAcceptableInput() const
{
const auto isAcceptable = [](const QLineEdit* edit) {
return !edit || edit->hasAcceptableInput();
};
return m_comboEulerOrder && m_comboEulerOrder->currentIndex() >= 0
&& isAcceptable(m_editRotX)
&& isAcceptable(m_editRotY)
&& isAcceptable(m_editRotZ)
&& isAcceptable(m_editAxialAngleMin)
&& isAcceptable(m_editAxialAngleMax)
&& isAcceptable(m_editOffsetX)
&& isAcceptable(m_editOffsetY)
&& isAcceptable(m_editOffsetZ);
}
void ToolExtrinsicWidget::setCameraSelectorVisible(bool visible)
{
if (m_labelCamera) m_labelCamera->setVisible(visible);
if (m_comboCamera) m_comboCamera->setVisible(visible);
}
void ToolExtrinsicWidget::setTargetOffsetVisible(bool visible)
{
if (m_labelTargetOffset) m_labelTargetOffset->setVisible(visible);
@ -332,6 +356,7 @@ void ToolExtrinsicWidget::setupUI()
}
m_groupExtrinsic = ui->groupExtrinsic;
m_labelCamera = ui->labelCamera;
m_comboCamera = ui->comboCamera;
m_comboEulerOrder = ui->comboEulerOrder;
m_editRotX = ui->editRotX;

2
Device

@ -1 +1 @@
Subproject commit 76d33caae885f1b47eec264f65f8e5f76cc131d0
Subproject commit 1136a52250ae2595291f231492c770b29fe3354f

View File

@ -17,7 +17,7 @@
| 11 | 铁路隧道槽道测量 | TunnelChannel | 1.0.0.3 |
| 12 | 螺杆定位 | ScrewPosition | 1.2.4.1 |
| 13 | 包裹拆线位置定位 | BagThreadPosition | 1.0.0.4 |
| 14 | 工件孔定位 | WorkpieceHole | 1.2.1.1 |
| 14 | 工件孔定位 | WorkpieceHole | 1.2.2.2 |
| 15 | 轮胎孔定位 | TireHolePose | 1.0.2.1 |
| 16 | 坑孔定位 | HolePitPosition | 无 |
| 17 | 钢筋焊缝定位 | RodWeldSeam | 1.0.0.1 |