feat: 优化双相机布局和调试数据存储

fix(螺杆定位): 修改TCP协议只输出一个目标点
This commit is contained in:
杰仔 2026-04-25 19:01:54 +08:00
parent 0331f3f319
commit ad5a7f40b5
29 changed files with 350 additions and 42 deletions

View File

@ -71,15 +71,15 @@ RX = 89.211
格式:
```text
PointNum_AX1_AY1_AZ1_A1_B1_C1_X1_Y1_Z1_A1_B1_C1/AX2_AY2_AZ2_A2_B2_C2_X2_Y2_Z2_A2_B2_C2/\r\n
PointNum_AX_AY_AZ_A_B_C_X_Y_Z_A_B_C\r\n
```
每个目标由 12 个浮点组成:先 **接近点** (`AX/AY/AZ + A/B/C`),再 **目标点** (`X/Y/Z + A/B/C`),两者姿态完全相同。
次只发送一个目标(第一个),由 12 个浮点组成:先 **接近点** (`AX/AY/AZ + A/B/C`),再 **目标点** (`X/Y/Z + A/B/C`),两者姿态完全相同。
示例:
```text
2_90.500_200.300_50.100_15.500_-2.300_45.000_100.500_200.300_50.100_15.500_-2.300_45.000/110.000_210.500_48.000_-5.000_1.200_30.000_120.000_210.500_48.000_-5.000_1.200_30.000/\r\n
1_90.500_200.300_50.100_15.500_-2.300_45.000_100.500_200.300_50.100_15.500_-2.300_45.000\r\n
0\r\n
```

View File

@ -41,8 +41,9 @@ public:
// Protocol:
// request : T1_X_Y_Z_A_B_C\r\n
// response: PointNum_AX1_AY1_AZ1_A1_B1_C1_X1_Y1_Z1_A1_B1_C1/.../\r\n
// 每个目标 12 个浮点:先接近点(AX/AY/AZ + A/B/C),后目标点(X/Y/Z + A/B/C),两者姿态相同
// response: PointNum_AX_AY_AZ_A_B_C_X_Y_Z_A_B_C\r\n
// PointNum 为检测到的目标数量,每次只发送第一个目标的 12 个浮点
// 先接近点(AX/AY/AZ + A/B/C),后目标点(X/Y/Z + A/B/C),两者姿态相同
int SendTextResult(const std::string& text, const TCPClient* pClient = nullptr);
private:

View File

@ -132,16 +132,18 @@ void ScrewPositionPresenter::_SendDetectionResultToTCP(const DetectionResult& re
int count = static_cast<int>(result.positions.size());
std::string resultText = std::to_string(count);
for (const auto& pos : result.positions) {
// 每次只发送一个目标,取第一个
if (!result.positions.empty()) {
const auto& pos = result.positions.front();
double angle1 = 0.0;
double angle2 = 0.0;
double angle3 = 0.0;
ReorderPoseAngles(pos.roll, pos.pitch, pos.yaw, poseOutputOrder, angle1, angle2, angle3);
// 每组输出 12 个浮点:先接近点(X/Y/Z/A/B/C),后目标点(X/Y/Z/A/B/C),姿态相同
// 输出 12 个浮点:先接近点(AX/AY/AZ + A/B/C),后目标点(X/Y/Z + A/B/C),姿态相同
char buf[512];
std::snprintf(buf, sizeof(buf),
"_%.3f_%.3f_%.3f_%.3f_%.3f_%.3f_%.3f_%.3f_%.3f_%.3f_%.3f_%.3f/",
"_%.3f_%.3f_%.3f_%.3f_%.3f_%.3f_%.3f_%.3f_%.3f_%.3f_%.3f_%.3f",
pos.approachX, pos.approachY, pos.approachZ, angle1, angle2, angle3,
pos.x, pos.y, pos.z, angle1, angle2, angle3);
resultText += buf;

View File

@ -3,8 +3,8 @@
#define SCREWPOSITION_APP_NAME "螺杆定位"
#define SCREWPOSITION_VERSION_STRING "1.1.5"
#define SCREWPOSITION_BUILD_STRING "3"
#define SCREWPOSITION_VERSION_STRING "1.1.6"
#define SCREWPOSITION_BUILD_STRING "1"
#define SCREWPOSITION_FULL_VERSION_STRING "V" SCREWPOSITION_VERSION_STRING "_" SCREWPOSITION_BUILD_STRING
// 获取版本信息的便捷函数

View File

@ -1,6 +1,12 @@
# 1.1.5 2026-04-19
# 1.1.6 2026-04-25
## build_1
1. 双相机
# 1.1.5 2026-04-19
## build_1-5
1. 增加目标点的偏移并且协议更新
2. 目标只输出一个目标
3. 调试姿态转换
# 1.1.4 2026-04-19
## build_1

View File

@ -28,6 +28,7 @@
#include <QGraphicsView>
#include <QPushButton>
#include "ScrewPositionPresenter.h"
#include "ResultListLayoutHelper.h"
#include "Version.h"
#include "IVrUtils.h"
@ -399,7 +400,7 @@ void MainWindow::OnCameraCountChanged(int cameraCount)
{
// 设置设备状态widget中的相机数量
if (m_deviceStatusWidget) {
m_deviceStatusWidget->setCameraCount(cameraCount);
m_deviceStatusWidget->setCameraCount(cameraCount, true);
// 设置相机名称(从配置文件中读取)
if (m_presenter) {
@ -415,6 +416,7 @@ void MainWindow::OnCameraCountChanged(int cameraCount)
}
}
// 如果只有一个相机,更新状态消息
if (cameraCount < 2) {
updateStatusLog(tr("系统使用单相机模式"));

View File

@ -3,7 +3,7 @@
#define WHEELMEASURE_APP_NAME "轮眉高度测量"
#define WHEELMEASURE_VERSION_STRING "1.1.5"
#define WHEELMEASURE_VERSION_STRING "1.1.6"
#define WHEELMEASURE_BUILD_STRING "1"
#define WHEELMEASURE_FULL_VERSION_STRING "V" WHEELMEASURE_VERSION_STRING "_" WHEELMEASURE_BUILD_STRING

View File

@ -1,3 +1,7 @@
# 1.1.6
## build_1 2026-04-25
1. 优化存储数据
# 1.1.5
## build_1 2026-04-20
1. 补偿系数

View File

@ -152,3 +152,6 @@ else:unix:!macx: {
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
DISTFILES += \
Version.md

View File

@ -4,6 +4,11 @@
#include <vector>
#include <opencv2/opencv.hpp>
//向量叉乘
SG_APISHARED_EXPORT SVzNL3DPoint vec3_cross(const SVzNL3DPoint& a, const SVzNL3DPoint& b);
//逆时针旋转时 θ > 0 ;顺时针旋转时 θ < 0
SG_APISHARED_EXPORT SVzNL3DPoint wd_rotate2D(const SVzNL3DPoint& pt, const double angle);
//滤除离群点:z跳变门限方法大于门限视为不连续根据连续段点数量判断噪声
SG_APISHARED_EXPORT void sg_lineDataRemoveOutlier(
@ -872,3 +877,15 @@ SG_APISHARED_EXPORT void scanLinesSmooting3x3(
std::vector< std::vector<SVzNL3DPosition>>& gridDataInput,
std::vector< std::vector<SVzNL3DPosition>>& smoothingData
);
SG_APISHARED_EXPORT void WD_getHoleInfo(
std::vector< std::vector<SVzNL3DPosition>>& scanLines,
const SSG_lineSegParam lineSegPara,
const SSG_outlierFilterParam filterParam,
const SSG_treeGrowParam growParam,
const double valieCommonNumRatio,
std::vector<SWD_segFeatureTree>& segTrees_v,
std::vector<SWD_segFeatureTree>& segTrees_h,
std::vector<SSG_intPair>& validObjects
);

View File

@ -44,3 +44,6 @@
//拓普发工件定位
#define SX_ERR_UNKNOWN_OBJECT -2501
//配天工件定位
#define SX_ERR_UNKNOWN_PLATE_DIR -2601

View File

@ -35,13 +35,15 @@ HEADERS += \
Inc/BasePresenter.h \
Inc/VrCommonConfig.h \
Inc/ProtocolCommon.h \
Inc/TCPServerProtocol.h
Inc/TCPServerProtocol.h \
Inc/DebugDataSaver.h
# 源文件
SOURCES += \
Src/SingleInstanceManager.cpp \
Src/BasePresenter.cpp \
Src/TCPServerProtocol.cpp
Src/TCPServerProtocol.cpp \
Src/DebugDataSaver.cpp
# Windows平台库链接
win32:CONFIG(release, debug|release): {

View File

@ -15,6 +15,7 @@
#include "VrCommonConfig.h" // 包含公共配置结构体
#include "LaserDataLoader.h" // 激光数据加载器
#include "IVisionApplicationStatus.h" // 工作状态枚举
#include "DebugDataSaver.h" // 调试数据异步存储
// 前向声明
class IYModbusTCPServer;
@ -573,6 +574,9 @@ protected:
// 调试参数
VrDebugParam m_debugParam;
// 调试数据异步存储
DebugDataSaver m_debugDataSaver;
private:
// 启动ModbusTCP服务在Init中调用
int StartModbusServer(int port = 5020);

View File

@ -0,0 +1,60 @@
#ifndef DEBUG_DATA_SAVER_H
#define DEBUG_DATA_SAVER_H
#include <string>
#include <vector>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <atomic>
#include "VZNL_Types.h"
class DebugDataSaver
{
public:
explicit DebugDataSaver(int maxThreads = 4);
~DebugDataSaver();
// 异步保存点云数据(内部深拷贝数据,不阻塞调用线程)
// timestamp: 检测时刻的时间戳,格式 YYYYMMDD_HHmmss
// 调用方需在持有数据锁时调用DeepCopyData 会在调用线程完成深拷贝
void SaveAsync(const std::string& basePath,
const std::string& timestamp,
const std::vector<std::pair<EVzResultDataType, SVzLaserLineData>>& data,
const std::string& deviceName = "");
// 停止所有工作线程(等待队列中任务完成后退出)
void Stop();
private:
struct SaveTask {
std::string filePath;
std::string deviceName;
std::vector<std::pair<EVzResultDataType, SVzLaserLineData>> data;
};
void WorkerThread();
// 深拷贝检测数据(含原始指针数组的完整拷贝)
static std::vector<std::pair<EVzResultDataType, SVzLaserLineData>> DeepCopyData(
const std::vector<std::pair<EVzResultDataType, SVzLaserLineData>>& src);
// 释放深拷贝的数据
static void FreeData(std::vector<std::pair<EVzResultDataType, SVzLaserLineData>>& data);
// 生成带日期目录的完整保存路径: basePath/YYYYMMDD/pointcloud[_deviceName]_YYYYMMDD_HHmmss_NNN.txt
// timestamp: 检测时刻的时间戳,格式 YYYYMMDD_HHmmss
// deviceName: 设备名称(为空时不包含在文件名中)
std::string GenerateSavePath(const std::string& basePath, const std::string& timestamp, const std::string& deviceName = "");
int m_maxThreads;
std::vector<std::thread> m_workers;
std::queue<SaveTask> m_taskQueue;
std::mutex m_queueMutex;
std::condition_variable m_queueCondition;
std::atomic<bool> m_running;
std::atomic<int> m_taskCounter{0};
};
#endif // DEBUG_DATA_SAVER_H

View File

@ -39,6 +39,9 @@ BasePresenter::~BasePresenter()
// 停止检测线程
StopAlgoDetectThread();
// 停止调试数据异步存储
m_debugDataSaver.Stop();
// 释放检测数据缓存中的动态内存
ClearDetectionDataCache();
@ -282,12 +285,13 @@ int BasePresenter::InitCamera(std::vector<DeviceInfo>& cameraList, bool bRGB, bo
LOG_INFO("[BasePresenter] init eyedevice list\n");
// 初始化相机列表,预分配空间
m_vrEyeDeviceList.resize(cameraCount, std::make_pair("", nullptr));
LOG_INFO("[BasePresenter] camera count : %d\n", cameraCount);
for(int i = 0; i < cameraCount; i++)
{
m_vrEyeDeviceList[i] = std::make_pair(cameraList[i].name, nullptr);
LOG_INFO("[BasePresenter] camera %d name : %s, ip : %s\n", i, cameraList[i].name.c_str(), cameraList[i].ip.c_str());
}
LOG_INFO("[BasePresenter] camera count : %d\n", cameraCount);
// 尝试初始化所有相机
bool allCamerasConnected = true;
@ -496,37 +500,41 @@ int BasePresenter::DetectTask()
LOG_INFO("[BasePresenter] 检测数据缓存大小: %zu\n", m_detectionDataCache.size());
}
// 2. 调试模式 - 保存点云数据
// 2. 调试模式 - 异步保存点云数据
if (debugParam.enableDebug && debugParam.savePointCloud) {
// 确定输出路径
QString outputPath;
if (debugParam.debugOutputPath.empty()) {
// 默认使用应用程序目录下的 debug 子目录
outputPath = QCoreApplication::applicationDirPath() + "/debug";
} else {
outputPath = QString::fromStdString(debugParam.debugOutputPath);
}
// 确保输出目录存在
// 确保输出目录存在
QDir dir(outputPath);
if (!dir.exists()) {
dir.mkpath(".");
}
// 生成带时间戳的文件名
QString timestamp = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss");
QString fileName = QString("%1/pointcloud_%2.txt").arg(outputPath).arg(timestamp);
// 在检测时刻生成时间戳,确保文件名反映实际检测时间
std::string timestamp = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss").toStdString();
if (debugParam.printDetailLog) {
LOG_INFO("[BasePresenter] 保存点云数据到: %s\n", fileName.toStdString().c_str());
// 获取当前相机名称,用于生成更直观的文件名
std::string deviceName;
int arrayIndex = m_currentCameraIndex - 1;
LOG_INFO("[BasePresenter] currentCameraIndex: %d, arrayIndex: %d\n", m_currentCameraIndex, arrayIndex);
if (arrayIndex >= 0 && arrayIndex < static_cast<int>(m_vrEyeDeviceList.size())) {
deviceName = m_vrEyeDeviceList[arrayIndex].first;
}
// 保存点云数据
int saveRet = SaveDetectionDataToFile(fileName.toStdString());
if (saveRet != SUCCESS) {
LOG_WARNING("[BasePresenter] 保存点云数据失败,错误码: %d\n", saveRet);
} else {
LOG_INFO("[BasePresenter] 点云数据保存成功\n");
if (debugParam.printDetailLog) {
LOG_INFO("[BasePresenter] 提交点云数据异步保存任务,路径: %s时间: %s设备: %s\n", outputPath.toStdString().c_str(), timestamp.c_str(), deviceName.c_str());
}
// 深拷贝数据后异步保存,不阻塞检测流程
{
std::lock_guard<std::mutex> lock(m_detectionDataMutex);
m_debugDataSaver.SaveAsync(outputPath.toStdString(), timestamp, m_detectionDataCache, deviceName);
}
}

View File

@ -0,0 +1,185 @@
#include "DebugDataSaver.h"
#include "LaserDataLoader.h"
#include "VrLog.h"
#include "VrError.h"
#include <QDir>
#include <QString>
#include <cstring>
DebugDataSaver::DebugDataSaver(int maxThreads)
: m_maxThreads(std::max(1, std::min(maxThreads, 4)))
, m_running(true)
{
for (int i = 0; i < m_maxThreads; ++i) {
m_workers.emplace_back(&DebugDataSaver::WorkerThread, this);
}
LOG_INFO("[DebugDataSaver] 已启动 %d 个存储线程\n", m_maxThreads);
}
DebugDataSaver::~DebugDataSaver()
{
Stop();
}
void DebugDataSaver::Stop()
{
if (!m_running.exchange(false)) {
return;
}
m_queueCondition.notify_all();
for (auto& worker : m_workers) {
if (worker.joinable()) {
worker.join();
}
}
m_workers.clear();
// 释放队列中未处理的任务
while (!m_taskQueue.empty()) {
FreeData(m_taskQueue.front().data);
m_taskQueue.pop();
}
LOG_INFO("[DebugDataSaver] 已停止\n");
}
void DebugDataSaver::SaveAsync(const std::string& basePath, const std::string& timestamp, const std::vector<std::pair<EVzResultDataType, SVzLaserLineData>>& data, const std::string& deviceName)
{
if (data.empty() || !m_running) return;
SaveTask task;
task.deviceName = deviceName;
task.filePath = GenerateSavePath(basePath, timestamp, deviceName);
task.data = DeepCopyData(data);
{
std::lock_guard<std::mutex> lock(m_queueMutex);
m_taskQueue.push(std::move(task));
}
m_queueCondition.notify_one();
}
void DebugDataSaver::WorkerThread()
{
LaserDataLoader dataLoader;
while (true) {
SaveTask task;
{
std::unique_lock<std::mutex> lock(m_queueMutex);
m_queueCondition.wait(lock, [this] {
return !m_taskQueue.empty() || !m_running;
});
if (!m_running && m_taskQueue.empty()) {
break;
}
if (m_taskQueue.empty()) {
continue;
}
task = std::move(m_taskQueue.front());
m_taskQueue.pop();
}
int lineNum = static_cast<int>(task.data.size());
float scanSpeed = 0.0f;
int maxTimeStamp = 0;
int clockPerSecond = 0;
int result = dataLoader.SaveLaserScanData(task.filePath, task.data, lineNum, scanSpeed, maxTimeStamp, clockPerSecond);
if (result == SUCCESS) {
LOG_INFO("[DebugDataSaver] 保存成功: %s (%d行)\n", task.filePath.c_str(), lineNum);
} else {
LOG_ERROR("[DebugDataSaver] 保存失败: %s, 错误: %s\n", task.filePath.c_str(), dataLoader.GetLastError().c_str());
}
FreeData(task.data);
}
}
std::vector<std::pair<EVzResultDataType, SVzLaserLineData>> DebugDataSaver::DeepCopyData(
const std::vector<std::pair<EVzResultDataType, SVzLaserLineData>>& src)
{
std::vector<std::pair<EVzResultDataType, SVzLaserLineData>> dst;
dst.reserve(src.size());
for (const auto& item : src) {
EVzResultDataType dataType = item.first;
const SVzLaserLineData& srcLine = item.second;
SVzLaserLineData dstLine;
memset(&dstLine, 0, sizeof(SVzLaserLineData));
if (srcLine.nPointCount > 0) {
if (dataType == keResultDataType_Position) {
dstLine.p3DPoint = new SVzNL3DPosition[srcLine.nPointCount];
if (srcLine.p3DPoint) {
memcpy(dstLine.p3DPoint, srcLine.p3DPoint, sizeof(SVzNL3DPosition) * srcLine.nPointCount);
}
dstLine.p2DPoint = new SVzNL2DPosition[srcLine.nPointCount];
if (srcLine.p2DPoint) {
memcpy(dstLine.p2DPoint, srcLine.p2DPoint, sizeof(SVzNL2DPosition) * srcLine.nPointCount);
}
} else if (dataType == keResultDataType_PointXYZRGBA) {
dstLine.p3DPoint = new SVzNLPointXYZRGBA[srcLine.nPointCount];
if (srcLine.p3DPoint) {
memcpy(dstLine.p3DPoint, srcLine.p3DPoint, sizeof(SVzNLPointXYZRGBA) * srcLine.nPointCount);
}
dstLine.p2DPoint = new SVzNL2DLRPoint[srcLine.nPointCount];
if (srcLine.p2DPoint) {
memcpy(dstLine.p2DPoint, srcLine.p2DPoint, sizeof(SVzNL2DLRPoint) * srcLine.nPointCount);
}
}
}
dstLine.nPointCount = srcLine.nPointCount;
dstLine.llTimeStamp = srcLine.llTimeStamp;
dstLine.llFrameIdx = srcLine.llFrameIdx;
dstLine.nEncodeNo = srcLine.nEncodeNo;
dstLine.fSwingAngle = srcLine.fSwingAngle;
dstLine.bEndOnceScan = srcLine.bEndOnceScan;
dst.push_back(std::make_pair(dataType, dstLine));
}
return dst;
}
void DebugDataSaver::FreeData(std::vector<std::pair<EVzResultDataType, SVzLaserLineData>>& data)
{
LaserDataLoader loader;
loader.FreeLaserScanData(data);
data.clear();
}
std::string DebugDataSaver::GenerateSavePath(const std::string& basePath, const std::string& timestamp, const std::string& deviceName)
{
// 从时间戳提取日期部分作为子文件夹名前8位YYYYMMDD
std::string dateFolder = timestamp.substr(0, 8);
QString fullPath = QString::fromStdString(basePath) + "/" + QString::fromStdString(dateFolder);
QDir dir(fullPath);
if (!dir.exists()) {
dir.mkpath(".");
}
int taskId = m_taskCounter.fetch_add(1);
QString fileName;
if (!deviceName.empty()) {
fileName = QString("%1/%2_%3_%4.txt").arg(fullPath)
.arg(taskId, 4, 10, QChar('0'))
.arg(QString::fromStdString(deviceName))
.arg(QString::fromStdString(timestamp));
} else {
fileName = QString("%1/pointcloud_%2_%3.txt").arg(fullPath)
.arg(QString::fromStdString(timestamp))
.arg(taskId, 4, 10, QChar('0'));
}
return fileName.toStdString();
}

View File

@ -40,7 +40,8 @@ public:
void setCamera2Name(const QString& name);
// 设置相机数量(用于控制相机二的显示和布局方向)
void setCameraCount(int cameraCount);
// cameraCount>=2 时 horizontal 有效true=横向不调整布局false=纵向(默认)
void setCameraCount(int cameraCount, bool horizontal = false);
// 设置机械臂是否可见(默认可见)
void setRobotVisible(bool visible);
@ -76,6 +77,9 @@ private:
// 相机数量
int m_cameraCount;
// 双相机布局方向true=横向false=纵向)
bool m_horizontalLayout;
// 相机是否可点击
bool m_cameraClickable;

View File

@ -13,6 +13,7 @@ DeviceStatusWidget::DeviceStatusWidget(QWidget *parent)
, m_camera2Name("")
, m_robotVisible(true)
, m_cameraCount(1)
, m_horizontalLayout(false)
, m_cameraClickable(true)
{
ui->setupUi(this);
@ -235,13 +236,14 @@ void DeviceStatusWidget::updateSerialStatus(bool isConnected)
}
// 设置相机数量(用于控制相机二的显示和布局方向)
void DeviceStatusWidget::setCameraCount(int cameraCount)
void DeviceStatusWidget::setCameraCount(int cameraCount, bool horizontal)
{
LOG_DEBUG("setCameraCount cameraCount: %d \n", cameraCount);
LOG_DEBUG("setCameraCount cameraCount: %d, horizontal: %d\n", cameraCount, horizontal);
// 检查是否在主线程中
if (QThread::currentThread() == this->thread()) {
m_cameraCount = cameraCount;
m_horizontalLayout = horizontal;
// 如果相机数量小于2隐藏相机二相关的UI元素
bool showCamera2 = (cameraCount >= 2);
@ -261,12 +263,15 @@ void DeviceStatusWidget::setCameraCount(int cameraCount)
}
}
// 更新布局
// 横向布局时不需要调整布局
if (!horizontal) {
updateLayout();
}
} else {
// 在其他线程中,使用队列方式确保线程安全
QMetaObject::invokeMethod(this, [this, cameraCount]() {
QMetaObject::invokeMethod(this, [this, cameraCount, horizontal]() {
m_cameraCount = cameraCount;
m_horizontalLayout = horizontal;
// 如果相机数量小于2隐藏相机二相关的UI元素
bool showCamera2 = (cameraCount >= 2);
@ -286,8 +291,10 @@ void DeviceStatusWidget::setCameraCount(int cameraCount)
}
}
// 更新布局
// 横向布局时不需要调整布局
if (!horizontal) {
updateLayout();
}
}, Qt::QueuedConnection);
}
}

View File

@ -8,15 +8,15 @@
| 2 | 皮带撕裂 | BeltTearingServer | 2.0.8.1 |
| 3 | 孔洞检测 | HoleDetection | 1.1.6.2 |
| 4 | 棒材定位 | RodAndBarPosition | 1.1.0.1 |
| 5 | 车轮拱高测量 | WheelMeasure | 1.1.5.1 |
| 5 | 车轮拱高测量 | WheelMeasure | 1.1.6.1 |
| 6 | 定子定位 | StatorPosition | 1.0.0.1 |
| 7 | 焊接 | LapWeld | 1.0.0.1 |
| 8 | 工件定位 | Workpiece | 1.0.3.0 |
| 9 | 颗粒尺寸检测 | ParticleSize | 1.0.0.0 |
| 10 | 双目标记检测 | BinocularMarkServer | 1.0.0.4 |
| 11 | 铁路隧道槽道测量 | TunnelChannel | 1.0.0.3 |
| 12 | 螺杆定位 | ScrewPosition | 1.1.5.3 |
| 12 | 螺杆定位 | ScrewPosition | 1.1.6.1 |
| 13 | 包裹拆线位置定位 | BagThreadPosition | 1.0.0.4 |
| 14 | 工件孔定位 | WorkpieceHole | 1.1.3.1 |
| 14 | 工件孔定位 | WorkpieceHole | 1.1.3.2 |
| 16 | 坑孔定位 | HolePitPosition | 无 |

2
Utils

@ -1 +1 @@
Subproject commit 598cd176f5390520aa400dbe8cb7cc7b7a213b91
Subproject commit c088ad39c4b0ee5d41d9a6ec2ca5f1efdaea9bf6