The first commit

This commit is contained in:
qupengwei
2026-01-04 16:51:58 +08:00
parent 684a923cda
commit 9b2a6bf423
107 changed files with 35063 additions and 0 deletions

74
.gitignore vendored Normal file
View File

@@ -0,0 +1,74 @@
# Build directories
# 忽略所有build目录包括根目录和子目录
build/
# 注意:项目只在 image_capture/build/ 目录下构建
# 根目录的 build/ 文件夹应该被忽略,如果存在可以安全删除
bin/
lib/
!camport3/lib/
image_capture/src/images_template/
image_capture/build_debug
# CMake generated files
CMakeCache.txt
CMakeFiles/
cmake_install.cmake
Makefile
*.cmake
# Visual Studio files
.vs/
*.vcxproj
*.vcxproj.filters
*.vcxproj.user
*.sln
*.suo
*.user
*.sdf
*.opensdf
# Qt autogen files
*_autogen/
.qt/
ui_*.h
moc_*.cpp
qrc_*.cpp
# Compiled files
*.o
*.obj
*.exe
*.a
*.lib
!image_capture/camera_sdk/lib/**/*.lib
# Saved images
*.png
*.jpg
*.jpeg
*.ply
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
# Temporary files
*.tmp
*.temp
*.log
compile_commands.json.tmp*
# OS files
.DS_Store
Thumbs.db
*.gif
.cache/
!image_capture/camera_sdk/
!image_capture/camera_sdk/lib/
!image_capture/cmake/*.cmake

View File

@@ -0,0 +1,221 @@
# CMake 配置文档
本文档总结了 `image_capture` 项目的 CMake 构建系统配置。
---
## 目录结构
```
image_capture/
├── CMakeLists.txt # 主构建配置文件
└── cmake/ # CMake 模块目录
├── CompilerOptions.cmake # 编译器选项配置
├── Dependencies.cmake # 外部依赖管理
└── PercipioSDK.cmake # 相机 SDK 配置
```
---
## 主配置文件:[CMakeLists.txt](file:///d:/Git/stereo_warehouse_inspection/image_capture/CMakeLists.txt)
### 基本信息
- **CMake 最低版本**: 3.10
- **项目名称**: `image_capture`
- **编程语言**: C++
- **构建生成器**: Visual Studio 17 2022 (MSVC)
### 输出目录
```cmake
CMAKE_RUNTIME_OUTPUT_DIRECTORY = ${CMAKE_BINARY_DIR}/bin/Release # 可执行文件
CMAKE_LIBRARY_OUTPUT_DIRECTORY = ${CMAKE_BINARY_DIR}/lib/Release # 动态库
CMAKE_ARCHIVE_OUTPUT_DIRECTORY = ${CMAKE_BINARY_DIR}/lib/Release # 静态库
```
### 模块化设计
项目采用模块化的 CMake 配置,通过 `cmake/` 目录下的三个模块文件组织:
1. **CompilerOptions.cmake** - 编译器和全局设置
2. **Dependencies.cmake** - Qt6、OpenCV、Open3D 依赖
3. **PercipioSDK.cmake** - 图漾相机 SDK 配置
### 库和可执行文件
#### 1. Algorithm Library (`algorithm_lib`)
**类型**: 静态库
**源文件**:
- `src/algorithm/core/detection_base.cpp`
- `src/algorithm/core/detection_result.cpp`
- `src/algorithm/utils/image_processor.cpp`
- `src/algorithm/detections/slot_occupancy_detection.cpp`
- `src/algorithm/detections/pallet_offset_detection.cpp`
- `src/algorithm/detections/beam_rack_deflection_detection.cpp`
- `src/algorithm/detections/visual_inventory_detection.cpp`
- `src/algorithm/detections/visual_inventory_end_detection.cpp`
**包含路径**:
- `src`
- `third_party/percipio/common` (修复 json11.hpp 引用)
**依赖**: OpenCV, Open3D
#### 2. Main Executable (`image_capture`)
**类型**: 可执行文件
**主要源文件**:
- `src/main.cpp`
- `src/camera/ty_multi_camera_capture.cpp`
- `src/camera/mvs_multi_camera_capture.cpp`
- `src/device/device_manager.cpp`
- `src/redis/redis_communicator.cpp`
- `src/task/task_manager.cpp`
- `src/vision/vision_controller.cpp`
- `src/common/log_manager.cpp`
- `src/common/config_manager.cpp`
- `src/gui/mainwindow.cpp` / `.h` / `.ui`
**链接的库**:
- `algorithm_lib` (项目内部算法库)
- `cpp_api_lib` (相机 SDK C++ API 封装)
- `tycam` (相机 SDK 动态库)
- `${OpenCV_LIBS}` (OpenCV 库)
- `Open3D::Open3D` (Open3D 库)
- `Qt6::Core``Qt6::Widgets` (Qt 框架)
- `MvCameraControl.lib` (海康 MVS SDK)
### 测试配置
- **选项**: `BUILD_TESTS` (默认 ON)
- **测试目录**: `tests/` (通过 `add_subdirectory` 添加)
---
## CMake 模块详解
### 1. [CompilerOptions.cmake](file:///d:/Git/stereo_warehouse_inspection/image_capture/cmake/CompilerOptions.cmake)
#### C++ 标准
- **标准**: C++17
- **要求**: 必须支持
#### Qt 自动化工具
```cmake
CMAKE_AUTOMOC ON # 自动 Meta-Object Compiler
CMAKE_AUTORCC ON # 自动 Resource Compiler
CMAKE_AUTOUIC ON # 自动 UI Compiler
```
#### 编译器优化选项 (MSVC)
**Release 模式** (默认):
```cmake
/O2 # 优化速度
/Ob2 # 内联任何合适的函数
/Oi # 启用内建函数
/Ot # 代码速度优先
/Oy # 省略帧指针
/GL # 全局程序优化
```
**Debug 模式**:
```cmake
/Od # 禁用优化
/Zi # 生成完整调试信息
```
#### 其他设置
- **定义**: `OPENCV_DEPENDENCIES`
- **compile_commands.json**: 自动生成(用于 IDE 智能提示)
---
### 2. [Dependencies.cmake](file:///d:/Git/stereo_warehouse_inspection/image_capture/cmake/Dependencies.cmake)
#### Qt6 配置
```cmake
find_package(Qt6 REQUIRED COMPONENTS Widgets)
```
#### OpenCV 配置
```cmake
find_package(OpenCV REQUIRED)
```
#### Open3D 配置
```cmake
find_package(Open3D REQUIRED)
```
用于点云处理和算法运算。
---
### 3. [PercipioSDK.cmake](file:///d:/Git/stereo_warehouse_inspection/image_capture/cmake/PercipioSDK.cmake)
#### 相机 SDK 路径配置
```cmake
CAMPORT3_ROOT = ${CMAKE_CURRENT_SOURCE_DIR}/camera_sdk
CAMPORT3_LIB_DIR = ${CAMPORT3_ROOT}/lib/win/x64
```
#### 导入 tycam 动态库
```cmake
add_library(tycam SHARED IMPORTED)
```
#### C++ API 封装库 (`cpp_api_lib`)
**类型**: 静态库
**源文件**:
`camera_sdk/sample_v2/cpp/*`, `camera_sdk/common/*`
**依赖**: OpenCV
---
## 构建流程
### 配置项目
```bash
cd image_capture/build
cmake ..
```
可选参数:
```bash
-DOpenCV_DIR=<path> # 指定 OpenCV 路径
-DQt6_DIR=<path> # 指定 Qt6 路径
-DOpen3D_DIR=<path> # 指定 Open3D 路径
```
### 编译项目
```bash
cmake --build . --config Release
# 或
cmake --build . --config Debug
```
---
## 依赖项总结
| 依赖项 | 版本要求 | 用途 |
|--------|---------|------|
| CMake | ≥ 3.10 | 构建系统 |
| C++ | C++17 | 编程语言标准 |
| Qt6 | Widgets 组件 | GUI 框架 |
| OpenCV | 4.x | 图像处理 |
| Open3D | 0.17+ | 3D点云处理 |
| Percipio SDK | tycam.dll | 相机驱动 |
| MSVC | VS2022 (v143) | 编译器 |
---
## 维护建议
1. **环境一致性**: 确保所有依赖项Qt, OpenCV, Open3D都是使用 MSVC 编译的 x64 版本。
2. **DLL 管理**: 运行时确保所有必要的 DLL 都在可执行文件目录下。
3. **版本检测**: 保持 Open3D 和 OpenCV 版本的一致性,避免 ABI 冲突。
---
*文档更新时间: 2025-12-19*

View File

@@ -0,0 +1,306 @@
# 项目架构及调用关系文档
## 1. 系统概述
本系统是一个基于立体视觉的仓库巡检图像采集与处理系统。它集成了图漾(Percipio)工业相机SDK和海康(MVS)相机SDK进行多相机图像采集使用OpenCV进行图像处理Qt6作为用户界面框架并通过Redis与外部系统如机器人控制系统进行通信和任务调度。
系统主要功能包括:
- 多相机同步采集(深度图与彩色图)
- 实时图像预览与状态监控
- 基于Redis的任务触发与结果上报
- 多种检测算法(货位占用、横梁/立柱变形、托盘偏差等)
- 系统配置管理与日志记录
## 2. 目录结构说明
```text
scripts/ # 批处理脚本 (数据库配置、模拟任务等)
image_capture/
└── src/
├── algorithm/ # 核心算法库
│ ├── core/ # 算法基类与结果定义 (DetectionBase, DetectionResult)
│ ├── detections/ # 具体检测算法实现 (SlotOccupancy, BeamRackDeflection等)
│ └── utils/ # 图像处理工具 (ImageProcessor)
├── camera/ # 相机驱动层
│ ├── ty_multi_camera_capture.cpp/h # 图漾(Percipio) 3D相机封装
│ └── mvs_multi_camera_capture.cpp/h # 海康(MVS) 2D相机封装
├── common/ # 通用设施
│ ├── config_manager.cpp/h # 配置管理单例
│ ├── log_manager.cpp/h # 日志管理
│ └── log_streambuf.h # std::cout重定向到GUI
├── device/ # 硬件设备管理
│ └── device_manager.cpp/h # 相机设备单例管理
├── gui/ # 用户界面
│ └── mainwindow.cpp/h/ui # 主窗口实现 (集成Settings Tab)
├── redis/ # 通信模块
│ └── redis_communicator.cpp/h # Redis客户端封装
├── task/ # 任务调度
│ └── task_manager.cpp/h # 任务分发与执行逻辑
├── vision/ # 系统控制
│ └── vision_controller.cpp/h # 顶层控制器协调Redis与Task
├── common_types.h # 通用数据类型 (Point3D, CameraIntrinsics)
└── main.cpp # 程序入口
```
## 3. 核心架构设计
系统采用分层架构设计,各模块职责明确:
- **展示层 (GUI)**: `MainWindow` 负责界面显示、手动控制、参数配置及日志展示。
- **控制层 (Controller)**: `VisionController` 作为系统级控制器,负责服务的启动/停止,协调 `RedisCommunicator``TaskManager`
- **业务逻辑层 (Task/Manager)**: `TaskManager` 解析任务指令,`DeviceManager` 管理硬件资源。
- **算法层 (Algorithm)**: 提供具体的视觉检测功能,继承自 `DetectionBase`
- **驱动层 (Driver)**: `CameraCapture` 封装底层SDK调用。
### 系统分层架构图
```mermaid
graph TB
subgraph Presentation ["展示层 (Presentation)"]
direction TB
GUI[MainWindow]
end
subgraph Control ["控制层 (Control)"]
VC[VisionController]
end
subgraph Business ["业务逻辑层 (Business Logic)"]
direction TB
TM[TaskManager]
DM[DeviceManager]
end
subgraph Algorithm ["算法层 (Algorithm)"]
direction TB
DB[DetectionBase]
Det[Concrete Detections<br/>(Slot, Beam, etc.)]
end
subgraph Infrastructure ["基础设施层 (Infrastructure)"]
direction TB
Cam[CameraCapture]
Redis[RedisCommunicator]
Conf[ConfigManager]
end
%% 层级调用关系
GUI --> VC
VC --> TM
VC --> Redis
TM --> DM
TM --> DB
DB <|-- Det
DM --> Cam
DM --> MVS[MvsMultiCameraCapture]
%% 跨层辅助调用
GUI -.-> Conf
TM -.-> Conf
style Presentation fill:#e1f5fe,stroke:#01579b
style Control fill:#e8f5e9,stroke:#2e7d32
style Business fill:#fff3e0,stroke:#ef6c00
style Algorithm fill:#f3e5f5,stroke:#7b1fa2
style Infrastructure fill:#eceff1,stroke:#455a64
```
### 系统类图
```mermaid
classDiagram
class MainWindow {
+VisionController visionController_
+updateImage()
+onSaveSettings()
}
class VisionController {
+RedisCommunicator redis_comm_
+TaskManager task_manager_
+start()
+stop()
}
class DeviceManager {
<<Singleton>>
+CameraCapture camera_capture_
+initialize()
+computePointCloud()
}
class TaskManager {
+executeTask()
-algorithms_ map
}
class CameraCapture {
+getLatestImages()
+computePointCloud()
+start()
-captureThreadFunc()
}
class RedisCommunicator {
+connect()
+listenForTasks()
+publishResult()
}
class ConfigManager {
<<Singleton>>
+loadConfig()
+saveConfig()
}
MainWindow --> VisionController : 只有与管理
VisionController --> RedisCommunicator : 使用
VisionController --> TaskManager : 使用
VisionController ..> DeviceManager : 依赖(全局)
TaskManager ..> DeviceManager : 获取图像/点云
DeviceManager --> CameraCapture : 拥有
MainWindow ..> ConfigManager : 读写配置
TaskManager ..> ConfigManager : 读取参数
```
## 4. 关键模块详解
### 4.1 GUI与主入口 (MainWindow)
- **职责**: 程序的主要入口负责UI渲染、用户交互、参数配置及系统状态反馈。
- **调用关系**:
- 初始化时创建 `VisionController`
- 通过 `QTimer` 定期从 `DeviceManager` 获取图像更新界面。
- **Settings Tab**: 直接在 `MainWindow` 中实现,提供 "Beam/Rack Deflection", "Pallet Offset" 等算法参数配置界面。
- 通过 `ConfigManager` 加载和保存配置项包括ROI点坐标和各类阈值。
### 4.2 视觉控制器 (VisionController)
- **职责**: 系统的"大脑"不依赖于GUI运行设计上支持无头模式
- **流程**:
1. `initialize()`: 连接Redis。
2. `start()`: 启动Redis监听线程。
3. `onTaskReceived()`: 当Redis收到任务时转发给 `TaskManager`
### 4.3 任务管理 (TaskManager)
- **职责**: 解析Redis下发的JSON指令选择合适的算法执行。
- **工作流**:
1. 接收任务ID和参数。
2.`DeviceManager` 获取当前最新的一帧图像(深度+彩色)。
3. **点云生成**: 对于需要3D数据的任务Flag 2/3调用 `DeviceManager::computePointCloud()` 生成点云。
4. 根据任务类型实例化或调用相应的 `DetectionBase` 子类。
5. 执行 `detect()`,传入图像和点云数据。
6. 将结果打包为JSON通过回调或直接通过 `RedisCommunicator` 返回。
### 4.4 设备管理 (DeviceManager)
- **职责**: 硬件资源的全局访问点(单例模式)。
- **封装**: 内部持有 `CameraCapture` 实例,确保相机资源全生命周期只被初始化一次。
- **功能**:
- 提供线程安全的图像获取接口 `getLatestImages()`
- 提供点云计算接口 `computePointCloud()`利用SDK内部参数生成高精度点云。
### 4.5 相机驱动 (CameraCapture)
- **实现**: `ty_multi_camera_capture.cpp`
- **机制**:
- 为每个相机开启独立采集线程。
- 维护内部帧缓冲区。
- 将SDK的 `TYImage` 转换为 OpenCV `cv::Mat`
- **点云优化**: 内部集成 `TYMapDepthImageToPoint3d`利用相机标定参数直接计算3D点云消除畸变。
### 4.6 配置管理 (ConfigManager)
- **职责**: 管理 `config.json` 文件,集中管理系统配置。
- **管理内容**:
- Redis 连接信息。
- 算法阈值 (Beam/Rack, Pallet Offset 等)。
- ROI (Region of Interest) 坐标点。
- 系统通用参数 (最小/最大深度等)。
- **特性**: 单例模式,支持热加载(部分参数)和持久化保存。程序启动时由 `MainWindow` 加载确保算法使用持久化的用户设置。GUI中的Settings Tab直接操作此模块。
## 5. 系统执行与数据流
### 5.1 初始化流程
1. `main()` 启动 `QApplication`
2. `MainWindow` 构造:
- 初始化UI。
- **调用 `ConfigManager::getInstance().loadConfig()` 加载本地配置。**
- 调用 `DeviceManager::getInstance().initialize()` 初始化相机。
- 创建并初始化 `VisionController`(连接 Redis但暂不启动监听
- 启动定时器调用 `updateImage()` 刷新界面显示。
3. 启动设备采集:调用 `DeviceManager::startAll()`
4. 设备启动成功后再调用 `VisionController::start()` 开启 Redis 监听,确保任务到来时设备已就绪。
### 5.2 自动任务执行流 (Redis触发)
```mermaid
sequenceDiagram
participant Redis
participant RC as RedisCommunicator
participant VC as VisionController
participant TM as TaskManager
participant DM as DeviceManager
participant Algo as DetectionAlgorithm
Redis->>RC: Publish Task (JSON)
RC->>VC: onTaskReceived(data)
VC->>TM: executeTask(data)
activate TM
TM->>DM: getLatestImages()
DM-->>TM: depth_img, color_img
TM->>DM: computePointCloud(depth)
DM-->>TM: point_cloud (vector<Point3D>)
TM->>Algo: execute(images, point_cloud)
activate Algo
Algo-->>TM: DetectionResult
deactivate Algo
TM->>TM: processResult()
TM->>RC: writeString(key, value)
RC->>Redis: Set Key-Value
deactivate TM
```
1. **外部触发**: Redis 发布任务消息。
2. **接收**: `RedisCommunicator` 监听到消息,触发回调。
3. **调度**: `VisionController` 调用 `TaskManager::executeTask()`
4. **获取数据**: `TaskManager``DeviceManager` 获取最新帧。
5. **算法处理**: 调用相应算法(如 `SlotOccupancyDetection::detect`)。
6. **结果反馈**: 结果封装成JSON通过 `RedisCommunicator` 写入Redis结果队列。
7. **任务复位**: 结果写入完成后,将 `vision_task_flag``0``vision_task_side`/`vision_task_time` 置空,避免程序重启后被旧任务自动触发。
### 5.3 实时监控执行流 (GUI)
```mermaid
sequenceDiagram
participant Timer as QTimer
participant MainWin as MainWindow
participant DM as DeviceManager
Timer->>MainWin: timeout()
activate MainWin
MainWin->>DM: getLatestImages()
DM-->>MainWin: depth_img, color_img
MainWin->>MainWin: Convert to QImage
MainWin->>MainWin: update QLabel
deactivate MainWin
```
1. **定时刷新**: `MainWindow``QTimer` 触发 `updateImage()`
2. **数据拉取**: 调用 `DeviceManager::getInstance().getLatestImages()`
3. **渲染**: 将OpenCV Mat 转换为 QImage 并显示在 `QLabel` 上。
- 深度图进行伪彩色处理以便观察。
- 自适应窗口大小缩放。
## 6. 异常处理与日志
- **日志**: 使用 `LogManager``spdlog` (如果集成) 或标准输出。
- **重定向**: `LogStreamBuf``std::cout/cerr` 重定向到GUI的日志窗口方便现场调试。
- **错误恢复**: 相机掉线重连机制(在驱动层实现或计划中)。
## 7. 编译与构建
- **工具**: CMake
- **依赖**: Qt6, OpenCV 4.x, Percipio SDK, (Redis库通常被封装或作为源码包含)
- **平台**: Windows (MSVC/MinGW)

View File

@@ -0,0 +1,133 @@
# 项目功能类调用关系说明 (Project Class Interaction Documentation)
本主要介绍 `image_capture` 项目核心功能类之间的调用关系、数据流向以及模块划分。
## 1. 核心模块概览 (Core Modules Overview)
系统主要由以下几个核心模块组成:
* **GUI 模块 (`MainWindow`)**: 程序的入口与界面显示,负责系统初始化。
* **Vision 控制器 (`VisionController`)**: 系统的核心中枢,协调通信与任务管理。
* **任务管理 (`TaskManager`)**: 负责具体的业务逻辑执行、算法调度和结果处理。
* **设备管理 (`DeviceManager`)**: 负责相机等硬件设备的统一管理(单例模式)。
* **通信模块 (`RedisCommunicator`)**: 负责与外部系统(如 WMS通过 Redis 交互。
* **算法模块 (`DetectionBase` 及其子类)**: 具体的图像处理算法。
## 2. 类调用关系图 (Class Interaction Diagram)
```mermaid
classDiagram
class MainWindow {
+VisionController vision_controller
+init()
}
class VisionController {
-shared_ptr<RedisCommunicator> redis_comm
-shared_ptr<TaskManager> task_manager
+start()
+stop()
-onTaskReceived()
}
class RedisCommunicator {
+startListening()
+writeDetectionResult()
+setTaskCallback()
}
class TaskManager {
-queue<RedisTaskData> task_queue
-map detectors
+handleTask()
-executeDetectionTask()
-getDetector(flag)
}
class DeviceManager {
<<Singleton>>
+getInstance()
+getLatestImages()
+startAll()
}
class DetectionBase {
<<Abstract>>
+execute(depth, color, ...)
}
class ConcreteDetection {
+execute()
}
MainWindow --> VisionController : 拥有并管理
VisionController --> RedisCommunicator : 管理 (监听/发送)
VisionController --> TaskManager : 分发任务
RedisCommunicator --> VisionController : 回调通知 (Callback)
TaskManager ..> DeviceManager : 获取图像数据 (Dependency)
TaskManager --> DetectionBase : 调用算法
DetectionBase <|-- ConcreteDetection : 继承
```
## 3. 详细调用流程 (Detailed Call Flow)
### 3.1 系统初始化与启动 (Initialization & Startup)
1. **Entry Point**: `main.cpp` 创建 `QApplication` 并实例化 `MainWindow`
2. **MainWindow**:
* 构造函数中初始化界面。
* 调用 `DeviceManager::getInstance().initialize()` 扫描并初始化相机设备。
* 实例化 `VisionController` 成员变量。
* 调用 `VisionController::initialize()`,配置 Redis 连接参数。
* 调用 `VisionController::start()` 启动后台服务。
3. **VisionController**:
*`start()` 中调用 `RedisCommunicator::startListening()` 开启监听线程。
### 3.2 任务触发与执行 (Task Trigger & Execution)
当 Redis 中 `vision_task_flag` 发生变化时,流程如下:
1. **RedisCommunicator**:
* 监听线程检测到 Flag 变化。
* 通过回调函数 `VisionController::onTaskReceived` 通知控制器。
2. **VisionController**:
* `onTaskReceived` 将接收到的 `RedisTaskData` 传递给 `TaskManager::handleTask`
3. **TaskManager**:
* `handleTask` 将任务推入内部的任务队列 `task_queue_`
* 工作线程 `taskExecutionThreadFunc` 从队列中取出任务。
* **获取图像**: 调用 `DeviceManager::getInstance().getLatestImages(...)` 获取当前最新的深度图和彩色图。
* **选择算法**: 根据任务 Flag 调用 `getDetector(flag)` 获取对应的算法实例(如 `PalletOffsetDetection`)。
* **执行算法**: 调用 `detector->execute(depth_img, color_img, ...)` 进行计算。
* **结果封装**: 将算法返回的数据填充到 `DetectionResult` 结构体中。
### 3.3 结果处理 (Result Handling)
算法执行完成后:
1. **TaskManager**:
* 调用 `processResult(result)`
* 该函数会格式化结果为 JSON 字符串,并计算报警/警告状态。
* 调用 `redis_result_comm_->writeDetectionResult(json)` 将结果写入 Redis。
2. **RedisCommunicator**:
* 执行 Redis SET 命令,将 JSON 数据写入指定的 Key。
## 4. 关键类说明 (Key Class Descriptions)
### VisionController (`src/vision/vision_controller.h`)
* **职责**: 作为系统的外观Facade对外提供统一的 start/stop 接口,对内协调 Redis 和 TaskManager。
* **特点**: 它是 MainWindow 唯一直接交互的非 GUI 业务类。
### DeviceManager (`src/device/device_manager.h`)
* **职责**: 屏蔽底层相机 SDKPercipio / MVS的差异提供统一的图像获取接口。
* **模式**: 单例模式 (Singleton)。确保系统中只有一份硬件控制实例。
### TaskManager (`src/task/task_manager.h`)
* **职责**: 真正的“大脑”。负责任务的缓冲(队列)、图像获取、算法调度和结果回传。
* **并发**: 拥有独立的执行线程,避免阻塞 Redis 监听线程或 GUI 线程。
### RedisCommunicator (`src/redis/redis_communicator.h`)
* **职责**: 封装 Redis 的底层 socket 操作,提供易用的读写接口和异步监听机制。
### DetectionBase (`src/algorithm/core/detection_base.h`)
* **职责**: 定义所有检测算法的统一接口 `execute`
* **扩展**: 新增算法只需继承此类并在 `TaskManager` 中注册即可。
---
*文档生成时间: 2025-12-29*

View File

@@ -0,0 +1,247 @@
cmake_minimum_required(VERSION 3.10)
# 支持 MSVC
# 注意:配置 CMake 时请选择合适的生成器(例如 "Visual Studio 17 2022"
project(image_capture LANGUAGES CXX)
if(NOT MSVC)
message(FATAL_ERROR "This project requires MSVC (Visual Studio) compiler. Please use a Visual Studio generator (e.g., -G \"Visual Studio 17 2022\").")
endif()
# ============================================================================
# 输出目录
# ============================================================================
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
# 生成 compile_commands.json 文件,供 IntelliSense 使用
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# ============================================================================
# CMake 模块路径
# ============================================================================
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
include(CompilerOptions)
# ============================================================================
# 依赖项 (Qt6, OpenCV)
# ============================================================================
include(Dependencies)
# ============================================================================
# 相机 SDK 配置
# ============================================================================
include(PercipioSDK)
# ============================================================================
# 算法库
# ============================================================================
add_library(algorithm_lib STATIC
src/algorithm/core/detection_base.cpp
src/algorithm/core/detection_result.cpp
src/algorithm/utils/image_processor.cpp
src/algorithm/detections/slot_occupancy/slot_occupancy_detection.cpp
src/algorithm/detections/pallet_offset/pallet_offset_detection.cpp
src/algorithm/detections/beam_rack_deflection/beam_rack_deflection_detection.cpp
src/algorithm/detections/visual_inventory/visual_inventory_detection.cpp
)
target_link_libraries(algorithm_lib PUBLIC
${OpenCV_LIBS}
Open3D::Open3D
Qt6::Core
${HALCON_LIBRARIES}
)
target_include_directories(algorithm_lib PUBLIC
${HALCON_INCLUDE_DIRS}
${OpenCV_INCLUDE_DIRS}
src
${CMAKE_CURRENT_SOURCE_DIR}/third_party/percipio/common
)
target_link_directories(algorithm_lib PUBLIC ${OpenCV_LIB_DIRS})
# ============================================================================
# 主可执行文件
# ============================================================================
set(SOURCES
src/main.cpp
src/camera/ty_multi_camera_capture.cpp
src/camera/mvs_multi_camera_capture.cpp
src/device/device_manager.cpp
src/redis/redis_communicator.cpp
src/task/task_manager.cpp
src/vision/vision_controller.cpp
src/common/log_manager.cpp
src/common/config_manager.cpp
src/gui/mainwindow.cpp
src/gui/mainwindow.h
src/gui/mainwindow.ui
src/gui/settings_widget.cpp
src/gui/settings_widget.h
)
add_executable(${PROJECT_NAME} WIN32 ${SOURCES})
target_include_directories(${PROJECT_NAME} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_SOURCE_DIR}/third_party/mvs/Includes
${OpenCV_INCLUDE_DIRS}
${CMAKE_CURRENT_BINARY_DIR} # Qt AUTOUIC 生成的头文件
)
target_link_libraries(${PROJECT_NAME} PRIVATE
algorithm_lib
cpp_api_lib
tycam
${OpenCV_LIBS}
Qt6::Core
Qt6::Widgets
ws2_32
${CMAKE_CURRENT_SOURCE_DIR}/third_party/mvs/Libraries/win64/MvCameraControl.lib
)
target_link_directories(${PROJECT_NAME} PRIVATE ${OpenCV_LIB_DIRS})
if(Open3D_RUNTIME_DLLS)
foreach(DLL_FILE ${Open3D_RUNTIME_DLLS})
get_filename_component(DLL_NAME "${DLL_FILE}" NAME)
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${DLL_FILE}"
"$<TARGET_FILE_DIR:${PROJECT_NAME}>"
COMMENT "Copying runtime dependency: ${DLL_NAME}"
)
endforeach()
endif()
# Copy tycam.dll to executable directory
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${CAMPORT3_LIB_DIR}/tycam.dll"
"$<TARGET_FILE_DIR:${PROJECT_NAME}>"
COMMENT "Copying tycam.dll to executable directory"
)
# Copy Halcon DLLs
if(HALCON_ROOT)
set(HALCON_BIN_DIR "${HALCON_ROOT}/bin/x64-win64")
# Verify directory exists
if(EXISTS "${HALCON_BIN_DIR}")
set(HALCON_DLLS "halcon.dll" "halconcpp.dll")
foreach(DLL_NAME ${HALCON_DLLS})
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${HALCON_BIN_DIR}/${DLL_NAME}"
"$<TARGET_FILE_DIR:${PROJECT_NAME}>"
COMMENT "Copying Halcon DLL: ${DLL_NAME}"
)
endforeach()
else()
message(WARNING "Halcon bin directory not found at: ${HALCON_BIN_DIR}. DLLs will not be copied.")
endif()
endif()
# ============================================================================
# 工具链
# ============================================================================
add_executable(slot_algo_tuner WIN32
src/tools/slot_algo_tuner/main.cpp
src/tools/slot_algo_tuner/tuner_widget.cpp
src/tools/slot_algo_tuner/tuner_widget.h
)
target_include_directories(slot_algo_tuner PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
${OpenCV_INCLUDE_DIRS}
${CMAKE_CURRENT_BINARY_DIR}
)
target_link_libraries(slot_algo_tuner PRIVATE
${OpenCV_LIBS}
Qt6::Core
Qt6::Widgets
)
target_link_directories(slot_algo_tuner PRIVATE ${OpenCV_LIB_DIRS})
add_executable(calibration_tool WIN32
src/tools/calibration_tool/main.cpp
src/tools/calibration_tool/calibration_widget.cpp
src/tools/calibration_tool/calibration_widget.h
)
target_include_directories(calibration_tool PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
${OpenCV_INCLUDE_DIRS}
${CMAKE_CURRENT_BINARY_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/third_party/percipio/include
${CMAKE_CURRENT_SOURCE_DIR}/third_party/mvs/Includes
)
target_link_libraries(calibration_tool PRIVATE
${OpenCV_LIBS}
Qt6::Core
Qt6::Widgets
Open3D::Open3D
tycam
)
target_compile_definitions(calibration_tool PRIVATE NOMINMAX)
target_link_directories(calibration_tool PRIVATE ${OpenCV_LIB_DIRS})
# Intrinsic Dumper Tool
add_executable(intrinsic_dumper
src/tools/intrinsic_dumper/main.cpp
)
target_include_directories(intrinsic_dumper PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
${OpenCV_INCLUDE_DIRS}
${CMAKE_CURRENT_BINARY_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/third_party/percipio/include
)
target_link_libraries(intrinsic_dumper PRIVATE
Qt6::Core
tycam
)
# Reference Generator (Teach Tool)
add_executable(generate_reference
src/tools/generate_reference/main.cpp
src/device/device_manager.cpp
src/camera/ty_multi_camera_capture.cpp
src/camera/mvs_multi_camera_capture.cpp
src/common/log_manager.cpp
src/common/config_manager.cpp
)
target_include_directories(generate_reference PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
${OpenCV_INCLUDE_DIRS}
${CMAKE_CURRENT_BINARY_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/third_party/percipio/include
${CMAKE_CURRENT_SOURCE_DIR}/third_party/mvs/Includes
)
target_link_libraries(generate_reference PRIVATE
algorithm_lib
cpp_api_lib
${OpenCV_LIBS}
Qt6::Core
Qt6::Widgets
tycam
${CMAKE_CURRENT_SOURCE_DIR}/third_party/mvs/Libraries/win64/MvCameraControl.lib
)
target_link_directories(generate_reference PRIVATE ${OpenCV_LIB_DIRS})

View File

@@ -0,0 +1,32 @@
# C++ Standard
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Output Directories
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
# Generate compile_commands.json
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# Definitions
add_definitions(-DOPENCV_DEPENDENCIES)
# Qt6 Setup (Global)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
# Compiler Specific Options
if(MSVC)
# MSVC specific options
add_compile_options(/utf-8) # Fix C4819 encoding warning
add_compile_options(/W3) # Warning level 3
add_compile_options(/MP) # Multi-processor compilation
add_definitions(-D_CRT_SECURE_NO_WARNINGS) # Suppress C4996 deprecated warnings
add_compile_options($<$<CONFIG:Release>:/O2>) # Maximize speed
add_compile_options($<$<CONFIG:Release>:/Ob2>) # Inline function expansion
endif()

View File

@@ -0,0 +1,129 @@
# Qt6
if(NOT Qt6_DIR AND NOT ENV{Qt6_DIR} AND NOT CMAKE_PREFIX_PATH)
message(WARNING "Qt6 not found in environment. Please set CMAKE_PREFIX_PATH or Qt6_DIR.")
endif()
find_package(Qt6 REQUIRED COMPONENTS Widgets)
# OpenCV
if(DEFINED ENV{OpenCV_DIR})
set(OpenCV_DIR $ENV{OpenCV_DIR})
message(STATUS "Using OpenCV_DIR from environment: ${OpenCV_DIR}")
elseif(NOT OpenCV_DIR)
message(STATUS "OpenCV_DIR not set, trying to find OpenCV in standard locations...")
set(LEGACY_OPENCV_PATH "D:/enviroments/OPencv4.55/OPencv4.55_MSVC/opencv/build/x64/vc15/lib")
if(EXISTS ${LEGACY_OPENCV_PATH})
set(OpenCV_DIR ${LEGACY_OPENCV_PATH})
message(STATUS "Found legacy OpenCV path: ${OpenCV_DIR}")
endif()
endif()
find_package(OpenCV REQUIRED)
message(STATUS "OpenCV found: ${OpenCV_VERSION}")
message(STATUS "OpenCV libraries: ${OpenCV_LIBS}")
message(STATUS "OpenCV include dirs: ${OpenCV_INCLUDE_DIRS}")
# Open3D
# Open3D
if(DEFINED ENV{Open3D_DIR})
set(Open3D_DIR $ENV{Open3D_DIR})
message(STATUS "Using Open3D_DIR from environment: ${Open3D_DIR}")
elseif(NOT Open3D_DIR)
# Default to 0.18 Release
set(DEFAULT_OPEN3D_PATH "D:/enviroments/Open3d/open3d-devel-windows-amd64-0.18.0-release/CMake")
# Debug path: D:/enviroments/Open3d/open3d-devel-windows-amd64-0.18.0-debug/CMake
if(EXISTS ${DEFAULT_OPEN3D_PATH})
set(Open3D_DIR ${DEFAULT_OPEN3D_PATH})
message(STATUS "Using default Open3D path: ${Open3D_DIR}")
endif()
endif()
find_package(Open3D REQUIRED)
message(STATUS "Open3D found: ${Open3D_VERSION}")
message(STATUS "Open3D DIR: ${Open3D_DIR}")
# Find Open3D DLL and dependencies (TBB)
# Adjust ROOT calculation based on where Config is found.
get_filename_component(DIR_NAME "${Open3D_DIR}" NAME)
if("${DIR_NAME}" STREQUAL "CMake")
# Structure: root/CMake/Open3DConfig.cmake -> root is up one level
get_filename_component(Open3D_ROOT "${Open3D_DIR}/.." ABSOLUTE)
else()
# Assume standard install: root/lib/cmake/Open3D/Open3DConfig.cmake -> root is up 3 levels
get_filename_component(Open3D_ROOT "${Open3D_DIR}/../../.." ABSOLUTE)
endif()
set(Open3D_BIN_DIR "${Open3D_ROOT}/bin")
set(Open3D_RUNTIME_DLLS "")
find_file(Open3D_DLL NAMES Open3D.dll PATHS ${Open3D_BIN_DIR} NO_DEFAULT_PATH)
if(Open3D_DLL)
list(APPEND Open3D_RUNTIME_DLLS ${Open3D_DLL})
message(STATUS "Found Open3D DLL: ${Open3D_DLL}")
else()
message(WARNING "Open3D DLL not found in ${Open3D_BIN_DIR}. You might need to add it to your PATH manually.")
endif()
# Find TBB DLLs (tbb.dll or tbb12_debug.dll etc)
# We glob for tbb*.dll but filter based on build type to avoid mixing runtimes
file(GLOB TBB_ALL_DLLS "${Open3D_BIN_DIR}/tbb*.dll")
set(TBB_DLLS ${TBB_ALL_DLLS})
# Filter out debug DLLs (ending in _debug.dll or d.dll)
list(FILTER TBB_DLLS EXCLUDE REGEX ".*(_debug|d)\\.dll$")
if(NOT TBB_DLLS)
# If no release DLLs found, check if we only have debug ones
if(TBB_ALL_DLLS)
message(WARNING "Only Debug TBB DLLs found in ${Open3D_BIN_DIR}. Release build might crash due to ABI mismatch!")
# Fallback: copy everything (dangerous but better than nothing?)
set(TBB_DLLS ${TBB_ALL_DLLS})
else()
message(WARNING "No TBB DLLs found in ${Open3D_BIN_DIR}.")
endif()
endif()
if(TBB_DLLS)
list(APPEND Open3D_RUNTIME_DLLS ${TBB_DLLS})
message(STATUS "Found TBB DLLs: ${TBB_DLLS}")
endif()
# Halcon
# Force usage of the user known path if possible, or fallback to environment
set(USER_PROVIDED_HALCON_ROOT "C:/Users/cve/AppData/Local/Programs/MVTec/HALCON-23.11-Progress")
if(EXISTS "${USER_PROVIDED_HALCON_ROOT}")
set(HALCON_ROOT "${USER_PROVIDED_HALCON_ROOT}")
message(STATUS "Using user provided HALCON_ROOT: ${HALCON_ROOT}")
elseif(DEFINED ENV{HALCONROOT})
set(HALCON_ROOT $ENV{HALCONROOT})
file(TO_CMAKE_PATH "${HALCON_ROOT}" HALCON_ROOT)
message(STATUS "Using HALCON_ROOT from environment: ${HALCON_ROOT}")
else()
message(WARNING "HALCONROOT not found.")
endif()
if(HALCON_ROOT)
set(HALCON_INCLUDE_DIRS
"${HALCON_ROOT}/include"
"${HALCON_ROOT}/include/halconcpp"
)
if(WIN32)
set(HALCON_LIB_DIR "${HALCON_ROOT}/lib/x64-win64")
if(NOT EXISTS "${HALCON_LIB_DIR}")
set(HALCON_LIB_DIR "${HALCON_ROOT}/lib")
endif()
set(HALCON_LIBRARIES
"${HALCON_LIB_DIR}/halcon.lib"
"${HALCON_LIB_DIR}/halconcpp.lib"
)
endif()
message(STATUS "Halcon include: ${HALCON_INCLUDE_DIRS}")
message(STATUS "Halcon libs: ${HALCON_LIBRARIES}")
endif()

View File

@@ -0,0 +1,55 @@
# Camera SDK Paths
set(CAMPORT3_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/third_party/percipio)
set(CAMPORT3_LIB_DIR ${CAMPORT3_ROOT}/lib/win/x64)
# Import tycam library (MinGW)
add_library(tycam SHARED IMPORTED)
if(EXISTS ${CAMPORT3_LIB_DIR}/libtycam.dll.a)
set_target_properties(tycam PROPERTIES
IMPORTED_LOCATION ${CAMPORT3_LIB_DIR}/tycam.dll
IMPORTED_IMPLIB ${CAMPORT3_LIB_DIR}/libtycam.dll.a
)
message(STATUS "Using libtycam.dll.a (MinGW compatible)")
elseif(EXISTS ${CAMPORT3_LIB_DIR}/tycam.lib)
set_target_properties(tycam PROPERTIES
IMPORTED_LOCATION ${CAMPORT3_LIB_DIR}/tycam.dll
IMPORTED_IMPLIB ${CAMPORT3_LIB_DIR}/tycam.lib
)
message(STATUS "Using tycam.lib (may require conversion to .dll.a if linking fails)")
else()
message(FATAL_ERROR "Neither libtycam.dll.a nor tycam.lib found in ${CAMPORT3_LIB_DIR}")
endif()
# Static API Library Sources
set(CPP_API_SOURCES
${CAMPORT3_ROOT}/sample_v2/cpp/Device.cpp
${CAMPORT3_ROOT}/sample_v2/cpp/Frame.cpp
${CAMPORT3_ROOT}/common/MatViewer.cpp
${CAMPORT3_ROOT}/common/TYThread.cpp
${CAMPORT3_ROOT}/common/crc32.cpp
${CAMPORT3_ROOT}/common/json11.cpp
${CAMPORT3_ROOT}/common/ParametersParse.cpp
${CAMPORT3_ROOT}/common/huffman.cpp
${CAMPORT3_ROOT}/common/ImageSpeckleFilter.cpp
${CAMPORT3_ROOT}/common/DepthInpainter.cpp
)
add_library(cpp_api_lib STATIC ${CPP_API_SOURCES})
target_include_directories(cpp_api_lib PUBLIC
${CAMPORT3_ROOT}/include
${CAMPORT3_ROOT}/sample_v2/hpp
${CAMPORT3_ROOT}/common
${OpenCV_INCLUDE_DIRS}
)
# Fix for MinGW: Ensure standard C++ headers are found
if(MINGW)
target_include_directories(cpp_api_lib SYSTEM PUBLIC
${CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES}
)
endif()
target_link_libraries(cpp_api_lib PUBLIC ${OpenCV_LIBS})
target_link_directories(cpp_api_lib PUBLIC ${OpenCV_LIB_DIRS})

130
image_capture/config.json Normal file
View File

@@ -0,0 +1,130 @@
{
"redis": {
"host": "127.0.0.1",
"port": 6379,
"db": 0
},
"cameras": {
"depth_enabled": true,
"color_enabled": true,
"mapping": [
{
"id": "camera_0",
"index": 0
},
{
"id": "camera_1",
"index": 1
},
{
"id": "camera_2",
"index": 2
},
{
"id": "camera_3",
"index": 3
}
]
},
"vision": {
"save_path": "./images",
"log_level": 1
},
"algorithms": {
"beam_rack_deflection": {
"beam_roi_points": [
{
"x": 100,
"y": 50
},
{
"x": 540,
"y": 80
},
{
"x": 540,
"y": 280
},
{
"x": 100,
"y": 280
}
],
"rack_roi_points": [
{
"x": 50,
"y": 50
},
{
"x": 150,
"y": 50
},
{
"x": 150,
"y": 430
},
{
"x": 50,
"y": 430
}
],
"beam_thresholds": {
"A": -10.0,
"B": -5.0,
"C": 5.0,
"D": 10.0
},
"rack_thresholds": {
"A": -6.0,
"B": -3.0,
"C": 3.0,
"D": 6.0
}
},
"pallet_offset": {
"offset_lat_mm_thresholds": {
"A": -20.0,
"B": -10.0,
"C": 10.0,
"D": 20.0
},
"offset_lon_mm_thresholds": {
"A": -20.0,
"B": -10.0,
"C": 10.0,
"D": 20.0
},
"rotation_angle_thresholds": {
"A": -5.0,
"B": -2.5,
"C": 2.5,
"D": 5.0
},
"hole_def_mm_left_thresholds": {
"A": -8.0,
"B": -4.0,
"C": 4.0,
"D": 8.0
},
"hole_def_mm_right_thresholds": {
"A": -8.0,
"B": -4.0,
"C": 4.0,
"D": 8.0
}
},
"slot_occupancy": {
"depth_threshold_mm": 100.0,
"confidence_threshold": 0.8
},
"visual_inventory": {
"barcode_confidence_threshold": 0.7,
"roi_enabled": true
},
"general": {
"min_depth_mm": 800.0,
"max_depth_mm": 3000.0,
"sample_points": 50
}
}
}

6
image_capture/note.md Normal file
View File

@@ -0,0 +1,6 @@
# 确保在 image_capture 目录下
cd d:\Git\stereo_warehouse_inspection\image_capture
Remove-Item -Recurse -Force build
# 使用 Visual Studio 生成器重新配置项目: 指定 -G "Visual Studio 17 2022" (根据你的VS版本调整通常是 16 2019 或 17 2022)。
cmake -G "Visual Studio 17 2022" -A x64 -B build
cmake --build build --config Release

View File

@@ -0,0 +1,172 @@
#include "detection_base.h"
#include "../detections/beam_rack_deflection/beam_rack_deflection_detection.h"
#include "../detections/pallet_offset/pallet_offset_detection.h"
#include "../detections/slot_occupancy/slot_occupancy_detection.h"
#include "../detections/visual_inventory/visual_inventory_detection.h"
#include "detection_result.h"
#include <chrono>
#include <ctime>
#include <iomanip>
#include <opencv2/opencv.hpp>
#include <sstream>
/**
* @brief 获取当前时间戳字符串
*/
static std::string getCurrentTimeString() {
auto now = std::chrono::system_clock::now();
auto time_t = std::chrono::system_clock::to_time_t(now);
std::tm *tm = std::localtime(&time_t);
std::stringstream ss;
ss << std::put_time(tm, "%Y-%m-%d %H:%M:%S");
return ss.str();
}
// ========== SlotOccupancyDetection ==========
bool SlotOccupancyDetection::execute(const cv::Mat &depth_img,
const cv::Mat &color_img,
const std::string &side,
DetectionResult &result,
const std::vector<Point3D> *point_cloud,
int beam_length) {
result.result_type = 1;
result.result_status = "fail";
// 调用算法进行检测
SlotOccupancyResult algo_result;
if (!SlotOccupancyAlgorithm::detect(depth_img, color_img, side,
algo_result)) {
std::cout
<< "[Detection] SlotOccupancy: Detection failed (Algorithm error)."
<< std::endl;
result.result_status = "fail";
result.last_update_time = getCurrentTimeString();
return false;
}
// 将算法结果填充到 DetectionResult
result.slot_occupied = algo_result.slot_occupied;
result.result_status = algo_result.success ? "success" : "fail";
result.last_update_time = getCurrentTimeString();
// 日志输出到界面 (UI Log)
std::cout << "[Detection] SlotOccupancy Result: "
<< (result.slot_occupied ? "Occupied (有货)" : "Empty (无货)")
<< std::endl;
return algo_result.success;
}
// ========== PalletOffsetDetection ==========
bool PalletOffsetDetection::execute(const cv::Mat &depth_img,
const cv::Mat &color_img,
const std::string &side,
DetectionResult &result,
const std::vector<Point3D> *point_cloud,
int beam_length) {
result.result_type = 2;
result.result_status = "fail";
// 调用算法进行检测
PalletOffsetResult algo_result;
if (!PalletOffsetAlgorithm::detect(depth_img, color_img, side, algo_result,
point_cloud)) {
result.result_status = "fail";
result.last_update_time = getCurrentTimeString();
return false;
}
// 将算法结果填充到 DetectionResult
result.offset_lat_mm_value = algo_result.offset_lat_mm_value;
result.offset_lon_mm_value = algo_result.offset_lon_mm_value;
result.rotation_angle_value = algo_result.rotation_angle_value;
result.hole_def_mm_left_value = algo_result.hole_def_mm_left_value;
result.hole_def_mm_right_value = algo_result.hole_def_mm_right_value;
result.offset_lat_mm_threshold = algo_result.offset_lat_mm_threshold;
result.offset_lon_mm_threshold = algo_result.offset_lon_mm_threshold;
result.rotation_angle_threshold = algo_result.rotation_angle_threshold;
result.hole_def_mm_left_threshold = algo_result.hole_def_mm_left_threshold;
result.hole_def_mm_right_threshold = algo_result.hole_def_mm_right_threshold;
result.offset_lat_mm_warning_alarm = algo_result.offset_lat_mm_warning_alarm;
result.offset_lon_mm_warning_alarm = algo_result.offset_lon_mm_warning_alarm;
result.rotation_angle_warning_alarm =
algo_result.rotation_angle_warning_alarm;
result.hole_def_mm_left_warning_alarm =
algo_result.hole_def_mm_left_warning_alarm;
result.hole_def_mm_right_warning_alarm =
algo_result.hole_def_mm_right_warning_alarm;
result.result_status = algo_result.success ? "success" : "fail";
result.last_update_time = getCurrentTimeString();
return algo_result.success;
}
// ========== BeamRackDeflectionDetection ==========
bool BeamRackDeflectionDetection::execute(
const cv::Mat &depth_img, const cv::Mat &color_img, const std::string &side,
DetectionResult &result, const std::vector<Point3D> *point_cloud,
int beam_length) {
result.result_type = 3;
result.result_status = "fail";
// Select ROI based on beam_length
std::vector<cv::Point2i> beam_roi;
if (beam_length == 2180) {
beam_roi = BeamRackDeflectionAlgorithm::BEAM_ROI_2180;
} else if (beam_length == 1380) {
beam_roi = BeamRackDeflectionAlgorithm::BEAM_ROI_1380;
}
// 调用算法进行检测
BeamRackDeflectionResult algo_result;
if (!BeamRackDeflectionAlgorithm::detect(
depth_img, color_img, side, algo_result, point_cloud, beam_roi)) {
result.result_status = "fail";
result.last_update_time = getCurrentTimeString();
return false;
}
// 将算法结果填充到 DetectionResult
result.beam_def_mm_value = algo_result.beam_def_mm_value;
result.rack_def_mm_value = algo_result.rack_def_mm_value;
result.beam_def_mm_threshold = algo_result.beam_def_mm_threshold;
result.rack_def_mm_threshold = algo_result.rack_def_mm_threshold;
result.beam_def_mm_warning_alarm = algo_result.beam_def_mm_warning_alarm;
result.rack_def_mm_warning_alarm = algo_result.rack_def_mm_warning_alarm;
result.result_status = algo_result.success ? "success" : "fail";
result.last_update_time = getCurrentTimeString();
return algo_result.success;
}
// ========== VisualInventoryDetection ==========
bool VisualInventoryDetection::execute(const cv::Mat &depth_img,
const cv::Mat &color_img,
const std::string &side,
DetectionResult &result,
const std::vector<Point3D> *point_cloud,
int beam_length) {
result.result_type = 4;
result.result_status = "fail";
// 调用算法进行检测
VisualInventoryResult algo_result;
if (!VisualInventoryAlgorithm::detect(depth_img, color_img, side,
algo_result)) {
result.result_status = "fail";
result.last_update_time = getCurrentTimeString();
return false;
}
// 将算法结果填充到 DetectionResult
result.result_barcodes = algo_result.result_barcodes;
result.result_status = algo_result.success ? "success" : "fail";
result.last_update_time = getCurrentTimeString();
return algo_result.success;
}

View File

@@ -0,0 +1,116 @@
#pragma once
#include <string>
struct DetectionResult;
namespace cv {
class Mat;
}
#include "../../common_types.h"
/**
* @brief 检测任务基类
*
* 所有检测任务都继承自此类,实现统一的接口
*/
class DetectionBase {
public:
DetectionBase() {}
virtual ~DetectionBase() {}
/**
* 执行检测任务
* @param depth_img 深度图像(可选)
* @param color_img 彩色图像(可选)
* @param side 货架侧("left"或"right"
* @param result [输出] 检测结果
* @param point_cloud [可选] 点云数据
* @return 是否检测成功
*/
virtual bool execute(const cv::Mat &depth_img, const cv::Mat &color_img,
const std::string &side, DetectionResult &result,
const std::vector<Point3D> *point_cloud = nullptr,
int beam_length = 0) = 0;
/**
* 获取任务类型对应flag值
*/
virtual int getTaskType() const = 0;
/**
* 获取任务名称
*/
virtual std::string getTaskName() const = 0;
};
/**
* @brief Task 1: 货位有无检测
*/
class SlotOccupancyDetection : public DetectionBase {
public:
SlotOccupancyDetection() {}
virtual ~SlotOccupancyDetection() {}
bool execute(const cv::Mat &depth_img, const cv::Mat &color_img,
const std::string &side, DetectionResult &result,
const std::vector<Point3D> *point_cloud = nullptr,
int beam_length = 0) override;
int getTaskType() const override { return 1; }
std::string getTaskName() const override { return "SlotOccupancyDetection"; }
};
/**
* @brief Task 2: 托盘位置偏移检测 - 插孔变形检测(取货时)
*/
class PalletOffsetDetection : public DetectionBase {
public:
PalletOffsetDetection() {}
virtual ~PalletOffsetDetection() {}
bool execute(const cv::Mat &depth_img, const cv::Mat &color_img,
const std::string &side, DetectionResult &result,
const std::vector<Point3D> *point_cloud = nullptr,
int beam_length = 0) override;
int getTaskType() const override { return 2; }
std::string getTaskName() const override { return "PalletOffsetDetection"; }
};
/**
* @brief Task 3: 横梁变形检测 - 货架立柱变形检测(放货时)
*/
class BeamRackDeflectionDetection : public DetectionBase {
public:
BeamRackDeflectionDetection() {}
virtual ~BeamRackDeflectionDetection() {}
bool execute(const cv::Mat &depth_img, const cv::Mat &color_img,
const std::string &side, DetectionResult &result,
const std::vector<Point3D> *point_cloud = nullptr,
int beam_length = 0) override;
int getTaskType() const override { return 3; }
std::string getTaskName() const override {
return "BeamRackDeflectionDetection";
}
};
/**
* @brief Task 4: 视觉盘点(扫码)
*/
class VisualInventoryDetection : public DetectionBase {
public:
VisualInventoryDetection() {}
virtual ~VisualInventoryDetection() {}
bool execute(const cv::Mat &depth_img, const cv::Mat &color_img,
const std::string &side, DetectionResult &result,
const std::vector<Point3D> *point_cloud = nullptr,
int beam_length = 0) override;
int getTaskType() const override { return 4; }
std::string getTaskName() const override {
return "VisualInventoryDetection";
}
};

View File

@@ -0,0 +1,184 @@
#include "detection_result.h"
#include <iostream>
#include <sstream>
std::string DetectionResult::toJson() const {
// TODO: 使用JSON库如nlohmann/json生成JSON字符串
// 当前使用简单的字符串拼接方式
std::ostringstream oss;
oss << "{";
// 基础字段
oss << "\"result_status\":\"" << result_status << "\",";
oss << "\"result_type\":" << result_type << ",";
oss << "\"last_update_time\":\"" << last_update_time << "\"";
// Flag 1
if (result_type == 1) {
oss << ",\"slot_occupied\":" << (slot_occupied ? "true" : "false");
}
// Flag 2
if (result_type == 2) {
oss << ",\"offset_lat_mm_value\":" << offset_lat_mm_value;
if (!offset_lat_mm_threshold.empty()) {
oss << ",\"offset_lat_mm_threshold\":" << offset_lat_mm_threshold;
}
if (!offset_lat_mm_warning_alarm.empty()) {
oss << ",\"offset_lat_mm_warning_alarm\":" << offset_lat_mm_warning_alarm;
}
oss << ",\"offset_lon_mm_value\":" << offset_lon_mm_value;
if (!offset_lon_mm_threshold.empty()) {
oss << ",\"offset_lon_mm_threshold\":" << offset_lon_mm_threshold;
}
if (!offset_lon_mm_warning_alarm.empty()) {
oss << ",\"offset_lon_mm_warning_alarm\":" << offset_lon_mm_warning_alarm;
}
oss << ",\"hole_def_mm_left_value\":" << hole_def_mm_left_value;
if (!hole_def_mm_left_threshold.empty()) {
oss << ",\"hole_def_mm_left_threshold\":" << hole_def_mm_left_threshold;
}
if (!hole_def_mm_left_warning_alarm.empty()) {
oss << ",\"hole_def_mm_left_warning_alarm\":"
<< hole_def_mm_left_warning_alarm;
}
oss << ",\"hole_def_mm_right_value\":" << hole_def_mm_right_value;
if (!hole_def_mm_right_threshold.empty()) {
oss << ",\"hole_def_mm_right_threshold\":" << hole_def_mm_right_threshold;
}
if (!hole_def_mm_right_warning_alarm.empty()) {
oss << ",\"hole_def_mm_right_warning_alarm\":"
<< hole_def_mm_right_warning_alarm;
}
oss << ",\"rotation_angle_value\":" << rotation_angle_value;
if (!rotation_angle_threshold.empty()) {
oss << ",\"rotation_angle_threshold\":" << rotation_angle_threshold;
}
if (!rotation_angle_warning_alarm.empty()) {
oss << ",\"rotation_angle_warning_alarm\":"
<< rotation_angle_warning_alarm;
}
}
// Flag 3
if (result_type == 3) {
oss << ",\"beam_def_mm_value\":" << beam_def_mm_value;
if (!beam_def_mm_threshold.empty()) {
oss << ",\"beam_def_mm_threshold\":" << beam_def_mm_threshold;
}
if (!beam_def_mm_warning_alarm.empty()) {
oss << ",\"beam_def_mm_warning_alarm\":" << beam_def_mm_warning_alarm;
}
oss << ",\"rack_def_mm_value\":" << rack_def_mm_value;
if (!rack_def_mm_threshold.empty()) {
oss << ",\"rack_def_mm_threshold\":" << rack_def_mm_threshold;
}
if (!rack_def_mm_warning_alarm.empty()) {
oss << ",\"rack_def_mm_warning_alarm\":" << rack_def_mm_warning_alarm;
}
}
// Flag 4 & 5
if (result_type == 4 || result_type == 5) {
if (!result_barcodes.empty()) {
oss << ",\"result_barcodes\":" << result_barcodes;
}
}
oss << "}";
return oss.str();
}
bool DetectionResult::fromJson(const std::string &json_str) {
// TODO: 使用JSON库解析JSON字符串
// 当前实现为占位符
std::cerr << "[DetectionResult] TODO: Implement JSON parsing" << std::endl;
return false;
}
std::map<std::string, std::string> DetectionResult::toMap() const {
std::map<std::string, std::string> m;
// Helper to convert float to string
auto floatToStr = [](float val) { return std::to_string(val); };
// Helper to convert bool to string "true"/"false"
auto boolToStr = [](bool val) { return val ? "true" : "false"; };
// 基础字段 (总是写入)
m["result_status"] = result_status;
m["result_type"] = std::to_string(result_type);
m["last_update_time"] = last_update_time;
// Flag 1: 货位有无
if (result_type == 1) {
m["slot_occupied"] = boolToStr(slot_occupied);
}
// Flag 2: 托盘检测
if (result_type == 2) {
m["offset_lat_mm_value"] = floatToStr(offset_lat_mm_value);
m["offset_lat_mm_threshold"] =
offset_lat_mm_threshold.empty() ? "{}" : offset_lat_mm_threshold;
m["offset_lat_mm_warning_alarm"] = offset_lat_mm_warning_alarm.empty()
? "{}"
: offset_lat_mm_warning_alarm;
m["offset_lon_mm_value"] = floatToStr(offset_lon_mm_value);
m["offset_lon_mm_threshold"] =
offset_lon_mm_threshold.empty() ? "{}" : offset_lon_mm_threshold;
m["offset_lon_mm_warning_alarm"] = offset_lon_mm_warning_alarm.empty()
? "{}"
: offset_lon_mm_warning_alarm;
m["hole_def_mm_left_value"] = floatToStr(hole_def_mm_left_value);
m["hole_def_mm_left_threshold"] =
hole_def_mm_left_threshold.empty() ? "{}" : hole_def_mm_left_threshold;
m["hole_def_mm_left_warning_alarm"] = hole_def_mm_left_warning_alarm.empty()
? "{}"
: hole_def_mm_left_warning_alarm;
m["hole_def_mm_right_value"] = floatToStr(hole_def_mm_right_value);
m["hole_def_mm_right_threshold"] = hole_def_mm_right_threshold.empty()
? "{}"
: hole_def_mm_right_threshold;
m["hole_def_mm_right_warning_alarm"] =
hole_def_mm_right_warning_alarm.empty()
? "{}"
: hole_def_mm_right_warning_alarm;
m["rotation_angle_value"] = floatToStr(rotation_angle_value);
m["rotation_angle_threshold"] =
rotation_angle_threshold.empty() ? "{}" : rotation_angle_threshold;
m["rotation_angle_warning_alarm"] = rotation_angle_warning_alarm.empty()
? "{}"
: rotation_angle_warning_alarm;
}
// Flag 3: 横梁/立柱检测
if (result_type == 3) {
m["beam_def_mm_value"] = floatToStr(beam_def_mm_value);
m["beam_def_mm_threshold"] =
beam_def_mm_threshold.empty() ? "{}" : beam_def_mm_threshold;
m["beam_def_mm_warning_alarm"] =
beam_def_mm_warning_alarm.empty() ? "{}" : beam_def_mm_warning_alarm;
m["rack_def_mm_value"] = floatToStr(rack_def_mm_value);
m["rack_def_mm_threshold"] =
rack_def_mm_threshold.empty() ? "{}" : rack_def_mm_threshold;
m["rack_def_mm_warning_alarm"] =
rack_def_mm_warning_alarm.empty() ? "{}" : rack_def_mm_warning_alarm;
}
// Flag 4 & 5: 视觉盘点 & 结束
if (result_type == 4 || result_type == 5) {
m["result_barcodes"] = result_barcodes.empty() ? "{}" : result_barcodes;
}
return m;
}

View File

@@ -0,0 +1,96 @@
#pragma once
#include <string>
#include <map>
// TODO: 添加nlohmann/json库依赖
// 临时使用简单的JSON字符串表示后续替换为nlohmann::json
// 为了简化这里使用std::string存储JSON字符串
// 实际实现时应该使用nlohmann::json或类似的JSON库
using JsonValue = std::string; // 临时定义实际应使用nlohmann::json
/**
* @brief 检测结果数据结构
*
* 包含所有检测任务的结果数据根据任务类型flag填充相应字段
*/
struct DetectionResult {
// 基础字段
std::string result_status; // "success" 或 "fail"
int result_type; // 对应 vision_task_flag1~5
std::string last_update_time; // "YYYY-MM-DD HH:MM:SS"
// Flag 1: 货位有无检测
bool slot_occupied; // 货位是否有托盘/货物
// Flag 2: 托盘位置偏移检测 - 插孔变形检测(取货时)
// 左右偏移量
float offset_lat_mm_value;
JsonValue offset_lat_mm_threshold; // {"A": -5.0, "B": -3.0, "C": 3.0, "D": 5.0}
JsonValue offset_lat_mm_warning_alarm; // {"warning": false, "alarm": false}
// 前后偏移量
float offset_lon_mm_value;
JsonValue offset_lon_mm_threshold;
JsonValue offset_lon_mm_warning_alarm;
// 左侧插孔变形
float hole_def_mm_left_value;
JsonValue hole_def_mm_left_threshold;
JsonValue hole_def_mm_left_warning_alarm;
// 右侧插孔变形
float hole_def_mm_right_value;
JsonValue hole_def_mm_right_threshold;
JsonValue hole_def_mm_right_warning_alarm;
// 托盘整体旋转角度
float rotation_angle_value;
JsonValue rotation_angle_threshold;
JsonValue rotation_angle_warning_alarm;
// Flag 3: 横梁变形检测 - 货架立柱变形检测(放货时)
// 横梁弯曲量
float beam_def_mm_value;
JsonValue beam_def_mm_threshold;
JsonValue beam_def_mm_warning_alarm;
// 立柱弯曲量
float rack_def_mm_value;
JsonValue rack_def_mm_threshold;
JsonValue rack_def_mm_warning_alarm;
// Flag 4: 视觉盘点(扫码)
JsonValue result_barcodes; // {"A01":["BOX111","BOX112"], "A02":["BOX210"]}
DetectionResult()
: result_status("fail")
, result_type(0)
, slot_occupied(false)
, offset_lat_mm_value(0.0f)
, offset_lon_mm_value(0.0f)
, hole_def_mm_left_value(0.0f)
, hole_def_mm_right_value(0.0f)
, rotation_angle_value(0.0f)
, beam_def_mm_value(0.0f)
, rack_def_mm_value(0.0f)
{
}
/**
* 转换为JSON字符串
*/
std::string toJson() const;
/**
* 从JSON字符串解析
*/
bool fromJson(const std::string& json_str);
/**
* 转换为Key-Value Map
* 用于分别写入Redis各个Key
*/
std::map<std::string, std::string> toMap() const;
};

View File

@@ -0,0 +1,932 @@
#include "beam_rack_deflection_detection.h"
#include "../../../common/config_manager.h"
#define DEBUG_ROI_SELECTION // 启用交互式ROI选择调试模式
#include <algorithm>
#include <cmath>
#include <iostream>
#include <string>
#include <vector>
#include <Eigen/Dense>
#include <QCoreApplication>
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
//====================
// 步骤1默认ROI点定义
//====================
// 定义默认ROI点四个点左上、右上、右下、左下
// 横梁ROI默认点示例值可根据实际场景调整
const std::vector<cv::Point2i>
BeamRackDeflectionAlgorithm::DEFAULT_BEAM_ROI_POINTS = {
cv::Point2i(100, 50), // 左上
cv::Point2i(540, 80), // 右上
cv::Point2i(540, 280), // 右下
cv::Point2i(100, 280) // 左下
};
// 2180mm 横梁 ROI (Placeholder - Same as Default for now)
const std::vector<cv::Point2i> BeamRackDeflectionAlgorithm::BEAM_ROI_2180 = {
cv::Point2i(100, 50), cv::Point2i(540, 80), cv::Point2i(540, 280),
cv::Point2i(100, 280)};
// 1380mm 横梁 ROI (Placeholder - Same as Default for now)
const std::vector<cv::Point2i> BeamRackDeflectionAlgorithm::BEAM_ROI_1380 = {
cv::Point2i(100, 50), cv::Point2i(540, 80), cv::Point2i(540, 280),
cv::Point2i(100, 280)};
//====================
// 步骤2立柱ROI默认点定义
//====================
// 立柱ROI默认点示例值可根据实际场景调整
const std::vector<cv::Point2i>
BeamRackDeflectionAlgorithm::DEFAULT_RACK_ROI_POINTS = {
cv::Point2i(50, 50), // 左上
cv::Point2i(150, 50), // 右上
cv::Point2i(150, 430), // 右下
cv::Point2i(50, 430) // 左下
};
//====================
// 步骤3横梁阈值默认值定义
//====================
// 定义默认阈值四个值A负方向报警, B负方向警告, C正方向警告, D正方向报警
// 横梁阈值默认值(示例值,可根据实际需求调整)
const std::vector<float> BeamRackDeflectionAlgorithm::DEFAULT_BEAM_THRESHOLDS =
{
-50.0f, // A: 负方向报警阈值 (横梁Y+方向忽略)
-30.0f, // B: 负方向警告阈值 (横梁Y+方向忽略)
30.0f, // C: 正方向警告阈值 (>30mm)
50.0f // D: 正方向报警阈值 (>50mm)
};
//====================
// 步骤4立柱阈值默认值定义
//====================
// 立柱阈值默认值(示例值,可根据实际需求调整)
const std::vector<float> BeamRackDeflectionAlgorithm::DEFAULT_RACK_THRESHOLDS =
{
-50.0f, // A: 负方向报警阈值 (对称参考)
-30.0f, // B: 负方向警告阈值 (对称参考)
30.0f, // C: 正方向警告阈值 (绝对值 > 30mm)
50.0f // D: 正方向报警阈值 (绝对值 > 50mm)
};
//====================
// 步骤5加载标定参数
//====================
bool BeamRackDeflectionAlgorithm::loadCalibration(Eigen::Matrix4d &transform) {
// 在当前目录查找 calibration_result_*.json 文件
QDir dir = QDir::current();
QStringList filters;
filters << "calibration_result_*.json";
dir.setNameFilters(filters);
QFileInfoList list = dir.entryInfoList(QDir::Files, QDir::Time); // 按时间排序
if (list.empty()) {
std::cerr << "[BeamRackDeflectionAlgorithm] Warning: No calibration file "
"found. Using Identity."
<< std::endl;
transform = Eigen::Matrix4d::Identity();
return false;
}
// 使用最新的文件
QString filePath = list.first().absoluteFilePath();
std::cout << "[BeamRackDeflectionAlgorithm] Loading calibration from: "
<< filePath.toStdString() << std::endl;
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly)) {
std::cerr << "[BeamRackDeflectionAlgorithm] Error: Could not open file."
<< std::endl;
transform = Eigen::Matrix4d::Identity();
return false;
}
QByteArray data = file.readAll();
QJsonDocument doc = QJsonDocument::fromJson(data);
if (doc.isNull()) {
std::cerr << "[BeamRackDeflectionAlgorithm] Error: Invalid JSON."
<< std::endl;
transform = Eigen::Matrix4d::Identity();
return false;
}
QJsonObject root = doc.object();
if (root.contains("transformation_matrix")) {
QJsonArray arr = root["transformation_matrix"].toArray();
if (arr.size() == 16) {
for (int i = 0; i < 4; ++i) {
for (int j = 0; j < 4; ++j) {
transform(i, j) = arr[i * 4 + j].toDouble();
}
}
return true;
}
}
std::cerr << "[BeamRackDeflectionAlgorithm] Error: transformation_matrix "
"missing or invalid."
<< std::endl;
transform = Eigen::Matrix4d::Identity();
return false;
}
//====================
// 步骤6横梁和立柱变形检测主函数
//====================
bool BeamRackDeflectionAlgorithm::detect(
const cv::Mat &depth_img, const cv::Mat &color_img, const std::string &side,
BeamRackDeflectionResult &result, const std::vector<Point3D> *point_cloud,
const std::vector<cv::Point2i> &beam_roi_points,
const std::vector<cv::Point2i> &rack_roi_points,
const std::vector<float> &beam_thresholds,
const std::vector<float> &rack_thresholds) {
// 算法启用开关
const bool USE_ALGORITHM = true;
if (USE_ALGORITHM) {
// --- 真实算法逻辑 ---
// 6.1 初始化结果
result.success = false;
result.beam_def_mm_value = 0.0f;
result.rack_def_mm_value = 0.0f;
// 6.2 验证深度图
if (depth_img.empty()) {
std::cerr << "[BeamRackDeflectionAlgorithm] ERROR: Depth image empty!"
<< std::endl;
return false;
}
// 6.3 检查点云
if (!point_cloud || point_cloud->empty()) {
std::cerr
<< "[BeamRackDeflectionAlgorithm] ERROR: Point cloud empty or null!"
<< std::endl;
return false;
}
// 6.4 加载标定参数
Eigen::Matrix4d transform;
loadCalibration(transform);
// 6.5 转换点云并按ROI组织
// 注意:假设点云与深度图分辨率匹配(行优先)
// 如果点云只是有效点的列表而没有结构我们无法轻松映射2D ROI
// 但通常标准会保持 size = width * height
if (point_cloud->size() != depth_img.cols * depth_img.rows) {
std::cerr << "[BeamRackDeflectionAlgorithm] Warning: Point cloud size "
"mismatch. Assuming organized."
<< std::endl;
}
int width = depth_img.cols;
int height = depth_img.rows;
std::vector<Eigen::Vector3d> beam_points_3d;
std::vector<Eigen::Vector3d> rack_points_3d;
// 6.6 辅助函数检查点是否在ROI内
auto isInRoi = [](const std::vector<cv::Point2i> &roi, int x, int y) {
if (roi.size() < 3)
return false;
return cv::pointPolygonTest(roi, cv::Point2f((float)x, (float)y),
false) >= 0;
};
// 6.7 确定实际使用的ROI使用默认值或自定义值
std::vector<cv::Point2i> actual_beam_roi =
beam_roi_points.empty() ? DEFAULT_BEAM_ROI_POINTS : beam_roi_points;
std::vector<cv::Point2i> actual_rack_roi =
rack_roi_points.empty() ? DEFAULT_RACK_ROI_POINTS : rack_roi_points;
// 6.8 交互式ROI选择调试模式
#ifdef DEBUG_ROI_SELECTION
// 辅助lambda函数用于4点ROI选择
auto selectPolygonROI =
[&](const std::string &winName,
const cv::Mat &bg_img) -> std::vector<cv::Point2i> {
std::vector<cv::Point> clicks;
std::string fullWinName = winName + " (Click 4 points)";
cv::namedWindow(fullWinName, cv::WINDOW_AUTOSIZE);
cv::setMouseCallback(
fullWinName,
[](int event, int x, int y, int flags, void *userdata) {
auto *points = static_cast<std::vector<cv::Point> *>(userdata);
if (event == cv::EVENT_LBUTTONDOWN) {
if (points->size() < 4) {
points->push_back(cv::Point(x, y));
std::cout << "Clicked: (" << x << ", " << y << ")" << std::endl;
}
}
},
&clicks);
while (clicks.size() < 4) {
cv::Mat display = bg_img.clone();
for (size_t i = 0; i < clicks.size(); ++i) {
cv::circle(display, clicks[i], 4, cv::Scalar(0, 0, 255), -1);
if (i > 0)
cv::line(display, clicks[i - 1], clicks[i], cv::Scalar(0, 255, 0),
2);
}
cv::imshow(fullWinName, display);
int key = cv::waitKey(10);
if (key == 27)
return {}; // ESC键取消
}
// 闭合多边形可视化
cv::Mat final_display = bg_img.clone();
for (size_t i = 0; i < clicks.size(); ++i) {
cv::circle(final_display, clicks[i], 4, cv::Scalar(0, 0, 255), -1);
if (i > 0)
cv::line(final_display, clicks[i - 1], clicks[i],
cv::Scalar(0, 255, 0), 2);
}
cv::line(final_display, clicks.back(), clicks.front(),
cv::Scalar(0, 255, 0), 2);
cv::imshow(fullWinName, final_display);
cv::waitKey(500); // Show for a bit
cv::destroyWindow(fullWinName);
// Convert to Point2i
std::vector<cv::Point2i> result;
for (const auto &p : clicks)
result.push_back(p);
return result;
};
static bool showed_debug_warning = false;
if (!showed_debug_warning) {
std::cout << "[BeamRackDeflectionAlgorithm] DEBUG INFO: Interactive "
"Rectified ROI Selection Enabled."
<< std::endl;
showed_debug_warning = true;
}
if (!depth_img.empty()) {
// --- 矫正逻辑 ---
cv::Mat display_img;
cv::normalize(depth_img, display_img, 0, 255, cv::NORM_MINMAX, CV_8U);
cv::cvtColor(display_img, display_img, cv::COLOR_GRAY2BGR);
// 尝试加载内参以进行矫正
cv::Mat H = cv::Mat::eye(3, 3, CV_64F);
bool can_rectify = false;
QDir dir_curr = QDir::current();
QStringList filters;
filters << "intrinsics_*.json";
dir_curr.setNameFilters(filters);
QFileInfoList list = dir_curr.entryInfoList(QDir::Files, QDir::Time);
if (!list.empty()) {
QFile i_file(list.first().absoluteFilePath());
if (i_file.open(QIODevice::ReadOnly)) {
QJsonDocument i_doc = QJsonDocument::fromJson(i_file.readAll());
if (!i_doc.isNull() && i_doc.object().contains("depth")) {
QJsonObject d_obj = i_doc.object()["depth"].toObject();
if (d_obj.contains("intrinsic")) {
QJsonArray i_arr = d_obj["intrinsic"].toArray();
if (i_arr.size() >= 9) {
double fx = i_arr[0].toDouble();
double fy = i_arr[4].toDouble();
double cx = i_arr[2].toDouble();
double cy = i_arr[5].toDouble();
Eigen::Matrix3d K;
K << fx, 0, cx, 0, fy, cy, 0, 0, 1;
Eigen::Matrix3d R = transform.block<3, 3>(0, 0);
// 单应性矩阵 H = K * R * K_inv
// 这将图像变换为仿佛相机已按 R 旋转
Eigen::Matrix3d H_eig = K * R * K.inverse();
for (int r = 0; r < 3; ++r)
for (int c = 0; c < 3; ++c)
H.at<double>(r, c) = H_eig(r, c);
can_rectify = true;
std::cout << "[BeamRackDeflectionAlgorithm] Intrinsics loaded. "
"Rectification enabled."
<< std::endl;
}
}
}
}
}
cv::Mat warp_img;
cv::Mat H_final = H.clone(); // 复制原始 H 以开始
if (can_rectify) {
// 1. 计算变换后的角点以找到新的边界框
std::vector<cv::Point2f> corners = {
cv::Point2f(0, 0), cv::Point2f((float)width, 0),
cv::Point2f((float)width, (float)height),
cv::Point2f(0, (float)height)};
std::vector<cv::Point2f> warped_corners;
cv::perspectiveTransform(corners, warped_corners, H);
cv::Rect bbox = cv::boundingRect(warped_corners);
// 2. 创建平移矩阵以将图像移入视野
cv::Mat T = cv::Mat::eye(3, 3, CV_64F);
T.at<double>(0, 2) = -bbox.x;
T.at<double>(1, 2) = -bbox.y;
// 3. 更新单应性矩阵
H_final = T * H;
// 4. 使用新尺寸和 H 进行变换
cv::warpPerspective(display_img, warp_img, H_final, bbox.size());
std::cout << "[BeamRackDeflectionAlgorithm] Rectified Image Size: "
<< bbox.width << "x" << bbox.height << std::endl;
} else {
std::cout << "[BeamRackDeflectionAlgorithm] Warning: Intrinsics not "
"found. Showing unrectified image."
<< std::endl;
warp_img = display_img.clone();
}
// --- 选择横梁 ROI ---
std::cout << "[BeamRackDeflectionAlgorithm] Please click 4 points for "
"BEAM ROI..."
<< std::endl;
auto beam_poly_visual = selectPolygonROI("Select BEAM", warp_img);
// 如果已矫正,则映射回原始坐标
if (beam_poly_visual.size() == 4) {
if (can_rectify) {
std::vector<cv::Point2f> src, dst;
for (auto p : beam_poly_visual)
src.push_back(cv::Point2f(p.x, p.y));
cv::perspectiveTransform(src, dst,
H_final.inv()); // Use H_final.inv()
actual_beam_roi.clear();
for (auto p : dst)
actual_beam_roi.push_back(
cv::Point2i(std::round(p.x), std::round(p.y)));
} else {
actual_beam_roi = beam_poly_visual;
}
std::cout << "[BeamRackDeflectionAlgorithm] Beam ROI Updated."
<< std::endl;
}
// --- 选择立柱 ROI ---
std::cout << "[BeamRackDeflectionAlgorithm] Please click 4 points for "
"RACK ROI..."
<< std::endl;
auto rack_poly_visual = selectPolygonROI("Select RACK", warp_img);
if (rack_poly_visual.size() == 4) {
if (can_rectify) {
std::vector<cv::Point2f> src, dst;
for (auto p : rack_poly_visual)
src.push_back(cv::Point2f(p.x, p.y));
cv::perspectiveTransform(src, dst,
H_final.inv()); // Use H_final.inv()
actual_rack_roi.clear();
for (auto p : dst)
actual_rack_roi.push_back(
cv::Point2i(std::round(p.x), std::round(p.y)));
} else {
actual_rack_roi = rack_poly_visual;
}
std::cout << "[BeamRackDeflectionAlgorithm] Rack ROI Updated."
<< std::endl;
}
}
#endif
// ============================================
cv::Rect beam_bbox = cv::boundingRect(actual_beam_roi);
cv::Rect rack_bbox = cv::boundingRect(actual_rack_roi);
// 处理横梁 ROI 区域
float max_beam_deflection = 0.0f;
float max_rack_deflection = 0.0f;
auto process_roi = [&](const cv::Rect &bbox,
const std::vector<cv::Point2i> &poly,
std::vector<Eigen::Vector3d> &out_pts) {
int start_x = std::max(0, bbox.x);
int end_x = std::min(width, bbox.x + bbox.width);
int start_y = std::max(0, bbox.y);
int end_y = std::min(height, bbox.y + bbox.height);
for (int y = start_y; y < end_y; ++y) {
for (int x = start_x; x < end_x; ++x) {
if (!isInRoi(poly, x, y))
continue;
int idx = y * width + x;
if (idx >= point_cloud->size())
continue;
const Point3D &pt = (*point_cloud)[idx];
if (pt.z <= 0.0f || std::isnan(pt.x))
continue;
// Transform
Eigen::Vector4d p(pt.x, pt.y, pt.z, 1.0);
Eigen::Vector4d p_trans = transform * p;
out_pts.emplace_back(p_trans.head<3>());
}
}
};
process_roi(beam_bbox, actual_beam_roi, beam_points_3d);
process_roi(rack_bbox, actual_rack_roi, rack_points_3d);
// ===========================================
// FIX: 自动旋转矫正 (PCA)
// 解决 "基准线不水平" 的问题,确保横梁水平,立柱垂直
// 通过将数据旋转到水平/垂直,基准线(连接端点)将变为水平/垂直。
// 从而使变形量(点到线的距离)等于 Y 轴(横梁)或 X 轴(立柱)的偏差。
// ===========================================
auto correctRotation = [](std::vector<Eigen::Vector3d> &points,
bool is_beam) {
if (points.size() < 10)
return;
// 1. Convert to cv::Mat for PCA (Only use X, Y)
int n = points.size();
cv::Mat data(n, 2, CV_64F);
for (int i = 0; i < n; ++i) {
data.at<double>(i, 0) = points[i].x();
data.at<double>(i, 1) = points[i].y();
}
// 2. Perform PCA
cv::PCA pca(data, cv::Mat(), cv::PCA::DATA_AS_ROW);
// 3. Get primary eigenvector (direction of max variance)
// Eigenvectors are stored in rows. Row 0 is the primary vector.
cv::Point2d eigen_vec(pca.eigenvectors.at<double>(0, 0),
pca.eigenvectors.at<double>(0, 1));
// 4. Calculate angle relative to desired axis
// Beam (is_beam=true): Should align with X-axis (1, 0)
// Rack (is_beam=false): Should align with Y-axis (0, 1)
double angle = std::atan2(eigen_vec.y, eigen_vec.x); // Angle of the data
double rotation_angle = 0.0;
if (is_beam) {
// Target: Horizontal (0 degrees)
rotation_angle = -angle;
} else {
// Target: Vertical (90 degrees or PI/2)
rotation_angle = (CV_PI / 2.0) - angle;
}
// Normalize to -PI ~ PI
while (rotation_angle > CV_PI)
rotation_angle -= 2 * CV_PI;
while (rotation_angle < -CV_PI)
rotation_angle += 2 * CV_PI;
// Safety check: Don't rotate if angle is suspicious huge (> 45 deg)
// unless confident For now, we trust PCA for standard slight tilts (< 30
// deg).
std::cout << "[BeamRackDeflectionAlgorithm] Correcting "
<< (is_beam ? "Beam" : "Rack")
<< " Rotation: " << rotation_angle * 180.0 / CV_PI << " deg."
<< std::endl;
// 5. Apply Rotation
double c = std::cos(rotation_angle);
double s = std::sin(rotation_angle);
// Center of rotation: PCA mean
double cx = pca.mean.at<double>(0);
double cy = pca.mean.at<double>(1);
for (int i = 0; i < n; ++i) {
double x = points[i].x() - cx;
double y = points[i].y() - cy;
double x_new = x * c - y * s;
double y_new = x * s + y * c;
points[i].x() = x_new + cx;
points[i].y() = y_new + cy;
// Z unchanged
}
};
// Apply corrections
correctRotation(beam_points_3d, true);
correctRotation(rack_points_3d, false);
// ===========================================
// 6.9 计算变形量
// 分箱(切片)方法辅助函数
auto calculate_deflection_binned = [&](std::vector<Eigen::Vector3d> &points,
bool is_beam_y_check,
const std::string &label) -> float {
if (points.empty())
return 0.0f;
// 1. 沿主轴排序点
std::sort(points.begin(), points.end(),
[is_beam_y_check](const Eigen::Vector3d &a,
const Eigen::Vector3d &b) {
return is_beam_y_check ? (a.x() < b.x()) : (a.y() < b.y());
});
// 2. 分箱
int num_bins = 50;
if (points.size() < 100)
num_bins = 10; // Reduce bins for small sets
double min_u = is_beam_y_check ? points.front().x() : points.front().y();
double max_u = is_beam_y_check ? points.back().x() : points.back().y();
// 可视化辅助
#ifdef DEBUG_ROI_SELECTION
int viz_w = 800;
int viz_h = 400;
cv::Mat viz_img = cv::Mat::zeros(viz_h, viz_w, CV_8UC3);
double disp_min_u = min_u;
double disp_max_u = max_u;
double min_v = 1e9, max_v = -1e9;
auto map_u = [&](double u) -> int {
return (int)((u - disp_min_u) / (disp_max_u - disp_min_u) *
(viz_w - 40) +
20);
};
// Will define map_v later after range finding
#endif
std::vector<Eigen::Vector3d> raw_centroids;
std::vector<int> counts;
double range_min = min_u;
double range_max = max_u;
double bin_step = (range_max - range_min) / num_bins;
if (bin_step < 1.0)
return 0.0f;
auto it = points.begin();
double avg_pts_per_bin = 0;
int filled_bins = 0;
for (int i = 0; i < num_bins; ++i) {
double bin_start = range_min + i * bin_step;
double bin_end = bin_start + bin_step;
std::vector<Eigen::Vector3d> bin_pts;
while (it != points.end()) {
double val = is_beam_y_check ? it->x() : it->y();
// double val_v = is_beam_y_check ? it->y() : it->x();
if (val > bin_end)
break;
bin_pts.push_back(*it);
++it;
}
if (!bin_pts.empty()) {
// Robust Centroid (Trimmed Mean)
std::sort(bin_pts.begin(), bin_pts.end(),
[is_beam_y_check](const Eigen::Vector3d &a,
const Eigen::Vector3d &b) {
double val_a = is_beam_y_check ? a.y() : a.x(); // V axis
double val_b = is_beam_y_check ? b.y() : b.x();
return val_a < val_b;
});
size_t n = bin_pts.size();
size_t start = (size_t)(n * 0.25);
size_t end = (size_t)(n * 0.75);
if (end <= start) {
start = 0;
end = n;
}
Eigen::Vector3d sum(0, 0, 0);
int count = 0;
for (size_t k = start; k < end; ++k) {
sum += bin_pts[k];
count++;
}
if (count > 0) {
raw_centroids.push_back(sum / count);
counts.push_back(bin_pts.size());
avg_pts_per_bin += bin_pts.size();
filled_bins++;
}
}
}
if (filled_bins < 2)
return 0.0f;
avg_pts_per_bin /= filled_bins;
// --- 2.1 Bin Filtering (Remove Noise) ---
// Filter out bins with significantly low density (e.g. < 20% of average)
std::vector<Eigen::Vector3d> bin_centroids;
for (size_t i = 0; i < raw_centroids.size(); ++i) {
if (counts[i] > avg_pts_per_bin * 0.2) {
bin_centroids.push_back(raw_centroids[i]);
// Track V range for filtered points
double v =
is_beam_y_check ? raw_centroids[i].y() : raw_centroids[i].x();
#ifdef DEBUG_ROI_SELECTION
if (v < min_v)
min_v = v;
if (v > max_v)
max_v = v;
#endif
}
}
if (bin_centroids.size() < 2) {
std::cerr << "[BeamRack] Filtered bins too few." << std::endl;
return 0.0f;
}
#ifdef DEBUG_ROI_SELECTION
// Adjust V range
double v_range = max_v - min_v;
if (v_range < 1.0)
v_range = 10.0;
min_v -= v_range * 0.5; // More margin
max_v += v_range * 0.5;
auto map_v = [&](double v) -> int {
return (int)((v - min_v) / (max_v - min_v) * (viz_h - 40) + 20);
};
// Draw Points
for (size_t i = 0; i < bin_centroids.size(); ++i) {
double u =
is_beam_y_check ? bin_centroids[i].x() : bin_centroids[i].y();
double v =
is_beam_y_check ? bin_centroids[i].y() : bin_centroids[i].x();
cv::circle(viz_img, cv::Point(map_u(u), map_v(v)), 3,
cv::Scalar(255, 255, 0), -1); // Cyan Centroids
}
#endif
// --- 3. Robust Baseline Fitting (Support Line) ---
// Instead of simple endpoints, fit a line to "valid support regions"
// Support Regions: First 15% and Last 15% of VALID centroids.
std::vector<Eigen::Vector3d> support_points;
int support_count = (int)(bin_centroids.size() * 0.15);
if (support_count < 2)
support_count = 2; // At least 2 points at each end
if (support_count * 2 > bin_centroids.size())
support_count = bin_centroids.size() / 2;
for (int i = 0; i < support_count; ++i)
support_points.push_back(bin_centroids[i]);
for (int i = 0; i < support_count; ++i)
support_points.push_back(bin_centroids[bin_centroids.size() - 1 - i]);
// Fit Line to Support Points (Least Squares)
// Model: v = m * u + c (since rotated, m should be close to 0)
double sum_u = 0, sum_v = 0, sum_uv = 0, sum_uu = 0;
int N = support_points.size();
for (const auto &p : support_points) {
double u = is_beam_y_check ? p.x() : p.y();
double v = is_beam_y_check ? p.y() : p.x();
sum_u += u;
sum_v += v;
sum_uv += u * v;
sum_uu += u * u;
}
double slope = 0, intercept = 0;
double denom = N * sum_uu - sum_u * sum_u;
if (std::abs(denom) > 1e-6) {
slope = (N * sum_uv - sum_u * sum_v) / denom;
intercept = (sum_v - slope * sum_u) / N;
} else {
// Vertical line? Should not happen after rotation. Fallback average.
slope = 0;
intercept = sum_v / N;
}
std::cout << "[BeamRack] Baseline Fit: slope=" << slope
<< ", intercept=" << intercept << " (Support Pts: " << N << ")"
<< std::endl;
// --- 4. Calculate Max Deflection ---
double max_def = 0.0;
Eigen::Vector3d max_pt;
double max_theoretical_v = 0;
for (const auto &p : bin_centroids) {
double u = is_beam_y_check ? p.x() : p.y();
double v = is_beam_y_check ? p.y() : p.x();
double theoretical_v = slope * u + intercept;
double def = 0;
if (is_beam_y_check) {
// Beam: Y+ is down. Deflection = ActualY - TheoreticalY
// We rotated data, so Y+ might still be relevant if rotation was just
// alignment. Assuming standard coords: Sag (Down) is Y decreasing? Or
// increasing? In Camera Coords: Y is DOWN. So Sag is INCREASING Y.
// Deflection = v - theoretical_v. Positive = Down.
def = v - theoretical_v;
} else {
// Rack: Deflection is absolute distance
def = std::abs(v - theoretical_v);
}
if (def > max_def) {
max_def = def;
max_pt = p;
max_theoretical_v = theoretical_v;
}
}
// Robust Average of Max Region (Top 3)
// ... (Simplified: use raw max for now, or implement top-k avg if
// preferred) Sticking to Max for simplicity as requested, but previous
// code used Average. Let's reimplement Top 3 Average roughly around max
// peak? Actually, just returning max_def is cleaner for "maximum sag".
#ifdef DEBUG_ROI_SELECTION
// Draw Baseline
double u_start = disp_min_u;
double v_start = slope * u_start + intercept;
double u_end = disp_max_u;
double v_end = slope * u_end + intercept;
cv::line(viz_img, cv::Point(map_u(u_start), map_v(v_start)),
cv::Point(map_u(u_end), map_v(v_end)), cv::Scalar(0, 255, 0), 2);
// Draw Max Deflection
if (max_def != 0.0) {
double u_d = is_beam_y_check ? max_pt.x() : max_pt.y();
double v_d = is_beam_y_check ? max_pt.y() : max_pt.x();
cv::line(viz_img, cv::Point(map_u(u_d), map_v(v_d)),
cv::Point(map_u(u_d), map_v(max_theoretical_v)),
cv::Scalar(0, 0, 255), 2);
cv::putText(viz_img, "Max: " + std::to_string(max_def),
cv::Point(viz_w / 2, 50), cv::FONT_HERSHEY_SIMPLEX, 0.8,
cv::Scalar(0, 0, 255), 2);
}
cv::imshow("Robust Deflection: " + label, viz_img);
cv::waitKey(100);
#endif
return (float)max_def;
};
// 6.10 Run Calculation logic
// --- 横梁变形Y+ 方向)---
max_beam_deflection =
calculate_deflection_binned(beam_points_3d, true, "Beam");
// --- 立柱变形X 方向)---
max_rack_deflection =
calculate_deflection_binned(rack_points_3d, false, "Rack");
// 存储结果
result.beam_def_mm_value = max_beam_deflection;
result.rack_def_mm_value = max_rack_deflection;
std::cout << "[BeamRackDeflectionAlgorithm] Results: Beam="
<< max_beam_deflection << "mm, Rack=" << max_rack_deflection
<< "mm"
<< " (Beam Points: " << beam_points_3d.size()
<< ", Rack Points: " << rack_points_3d.size() << ")" << std::endl;
// 使用默认或提供的阈值
// std::vector<float> actual_beam_thresh = beam_thresholds.empty() ?
// DEFAULT_BEAM_THRESHOLDS : beam_thresholds; // OLD std::vector<float>
// actual_rack_thresh = rack_thresholds.empty() ? DEFAULT_RACK_THRESHOLDS :
// rack_thresholds; // OLD
// NEW: Use ConfigManager
std::vector<float> actual_beam_thresh =
ConfigManager::getInstance().getBeamThresholds();
std::vector<float> actual_rack_thresh =
ConfigManager::getInstance().getRackThresholds();
// Fallback if empty (should not happen with getBeamThresholds defaults)
if (actual_beam_thresh.size() < 4)
actual_beam_thresh = DEFAULT_BEAM_THRESHOLDS;
if (actual_rack_thresh.size() < 4)
actual_rack_thresh = DEFAULT_RACK_THRESHOLDS;
// 制作 json 阈值字符串的辅助函数
auto make_json_thresh = [](const std::vector<float> &t) {
return "{\"A\":" + std::to_string(t[0]) +
",\"B\":" + std::to_string(t[1]) +
",\"C\":" + std::to_string(t[2]) +
",\"D\":" + std::to_string(t[3]) + "}";
};
if (actual_beam_thresh.size() >= 4) {
result.beam_def_mm_threshold = make_json_thresh(actual_beam_thresh);
}
if (actual_rack_thresh.size() >= 4) {
result.rack_def_mm_threshold = make_json_thresh(actual_rack_thresh);
}
// 检查状态
// 横梁正值为向下Y+)。检查 C 和 D。
// 负值为向上Y-)。检查 A 和 B
// 用户要求:“横梁由于货物仅向下弯曲...Y 正方向)”
// 所以我们主要使用 max_beam_deflection检查 C警告和 D报警这是
// >0。 遗留阈值具有负值,可能用于范围检查。 我们将假设标准 [A(neg),
// B(neg), C(pos), D(pos)] 格式。
bool beam_warn = (max_beam_deflection >= actual_beam_thresh[2]); // > C
bool beam_alrm = (max_beam_deflection >= actual_beam_thresh[3]); // > D
// Rack: Bends Left or Right. We took Abs() -> always positive.
// So we check against C and D.
bool rack_warn = (max_rack_deflection >= actual_rack_thresh[2]);
bool rack_alrm = (max_rack_deflection >= actual_rack_thresh[3]);
auto make_json_status = [](bool w, bool a) {
return "{\"warning\":" + std::string(w ? "true" : "false") +
",\"alarm\":" + std::string(a ? "true" : "false") + "}";
};
result.beam_def_mm_warning_alarm = make_json_status(beam_warn, beam_alrm);
result.rack_def_mm_warning_alarm = make_json_status(rack_warn, rack_alrm);
// 标记为成功
result.success = true;
#ifdef DEBUG_ROI_SELECTION
std::cout << "[BeamRackDeflectionAlgorithm] Press ANY KEY to close graphs "
"and continue..."
<< std::endl;
cv::waitKey(0);
cv::destroyAllWindows();
#endif
return result.success;
} else {
// --- 模拟数据逻辑 ---
std::cout << "[BeamRackDeflectionAlgorithm] Using FAKE DATA implementation "
"(Switch OFF)."
<< std::endl;
result.beam_def_mm_value = 5.5f; // 模拟横梁弯曲
result.rack_def_mm_value = 2.2f; // 模拟立柱弯曲
result.success = true;
// 设置模拟阈值
// std::vector<float> actual_beam_thresh = beam_thresholds.empty() ?
// DEFAULT_BEAM_THRESHOLDS : beam_thresholds; std::vector<float>
// actual_rack_thresh = rack_thresholds.empty() ? DEFAULT_RACK_THRESHOLDS :
// rack_thresholds;
std::vector<float> actual_beam_thresh =
ConfigManager::getInstance().getBeamThresholds();
std::vector<float> actual_rack_thresh =
ConfigManager::getInstance().getRackThresholds();
if (actual_beam_thresh.size() < 4)
actual_beam_thresh = DEFAULT_BEAM_THRESHOLDS;
if (actual_rack_thresh.size() < 4)
actual_rack_thresh = DEFAULT_RACK_THRESHOLDS;
auto make_json_thresh = [](const std::vector<float> &t) {
return "{\"A\":" + std::to_string(t[0]) +
",\"B\":" + std::to_string(t[1]) +
",\"C\":" + std::to_string(t[2]) +
",\"D\":" + std::to_string(t[3]) + "}";
};
if (actual_beam_thresh.size() >= 4)
result.beam_def_mm_threshold = make_json_thresh(actual_beam_thresh);
if (actual_rack_thresh.size() >= 4)
result.rack_def_mm_threshold = make_json_thresh(actual_rack_thresh);
// 设置模拟警告/报警状态
result.beam_def_mm_warning_alarm = "{\"warning\":false,\"alarm\":false}";
result.rack_def_mm_warning_alarm = "{\"warning\":false,\"alarm\":false}";
return result.success;
}
}

View File

@@ -0,0 +1,134 @@
#pragma once
#include "../../../common_types.h"
#include <Eigen/Dense>
#include <opencv2/opencv.hpp>
#include <string>
/**
* @brief 四边形ROI结构四个点定义
*/
struct QuadrilateralROI {
cv::Point2i points[4]; // 四个点:左上、右上、右下、左下(按顺序)
QuadrilateralROI() {
for (int i = 0; i < 4; ++i) {
points[i] = cv::Point2i(0, 0);
}
}
QuadrilateralROI(const cv::Point2i &pt0, const cv::Point2i &pt1,
const cv::Point2i &pt2, const cv::Point2i &pt3) {
points[0] = pt0;
points[1] = pt1;
points[2] = pt2;
points[3] = pt3;
}
bool isValid() const {
// 检查是否有有效的点不全为0
for (int i = 0; i < 4; ++i) {
if (points[i].x > 0 || points[i].y > 0) {
return true;
}
}
return false;
}
cv::Rect getBoundingRect() const {
if (!isValid()) {
return cv::Rect();
}
int min_x = points[0].x, max_x = points[0].x;
int min_y = points[0].y, max_y = points[0].y;
for (int i = 1; i < 4; ++i) {
min_x = std::min(min_x, points[i].x);
max_x = std::max(max_x, points[i].x);
min_y = std::min(min_y, points[i].y);
max_y = std::max(max_y, points[i].y);
}
return cv::Rect(min_x, min_y, max_x - min_x, max_y - min_y);
}
};
/**
* @brief 横梁变形检测算法结果
*/
struct BeamRackDeflectionResult {
// 变形量
float beam_def_mm_value; // 横梁弯曲量mm
float rack_def_mm_value; // 立柱弯曲量mm
// 阈值JSON字符串
std::string beam_def_mm_threshold;
std::string rack_def_mm_threshold;
// 警告和报警信号JSON字符串
std::string beam_def_mm_warning_alarm;
std::string rack_def_mm_warning_alarm;
bool success; // 算法是否执行成功
BeamRackDeflectionResult()
: beam_def_mm_value(0.0f), rack_def_mm_value(0.0f), success(false) {}
};
/**
* @brief 横梁变形检测算法
*
* 检测横梁和货架立柱的变形
*/
class BeamRackDeflectionAlgorithm {
public:
// 默认ROI点定义四个点左上、右上、右下、左下
// 横梁ROI默认点
static const std::vector<cv::Point2i> DEFAULT_BEAM_ROI_POINTS;
// 立柱ROI默认点
static const std::vector<cv::Point2i> DEFAULT_RACK_ROI_POINTS;
// 默认阈值定义四个值A负方向报警, B负方向警告, C正方向警告, D正方向报警
// 横梁阈值默认值
static const std::vector<float> DEFAULT_BEAM_THRESHOLDS;
// 立柱阈值默认值
static const std::vector<float> DEFAULT_RACK_THRESHOLDS;
// 2180mm 横梁 ROI (Placeholder)
static const std::vector<cv::Point2i> BEAM_ROI_2180;
// 1380mm 横梁 ROI (Placeholder)
static const std::vector<cv::Point2i> BEAM_ROI_1380;
// ... (keep class definition)
/**
* 执行横梁变形检测(使用深度图方案)
* @param depth_img 深度图像
* @param color_img 彩色图像
* @param side 货架侧("left"或"right"
* @param result [输出] 检测结果
* @param point_cloud [可选] 点云数据
* @param beam_roi_points
* 横梁ROI的四个点左上、右上、右下、左下为空时使用默认值
* @param rack_roi_points
* 立柱ROI的四个点左上、右上、右下、左下为空时使用默认值
* @param beam_thresholds 横梁阈值四个值[A,B,C,D],为空时使用默认值
* @param rack_thresholds 立柱阈值四个值[A,B,C,D],为空时使用默认值
* @return 是否检测成功
*/
static bool
detect(const cv::Mat &depth_img, const cv::Mat &color_img,
const std::string &side, BeamRackDeflectionResult &result,
const std::vector<Point3D> *point_cloud = nullptr,
const std::vector<cv::Point2i> &beam_roi_points =
std::vector<cv::Point2i>(),
const std::vector<cv::Point2i> &rack_roi_points =
std::vector<cv::Point2i>(),
const std::vector<float> &beam_thresholds = std::vector<float>(),
const std::vector<float> &rack_thresholds = std::vector<float>());
private:
static bool loadCalibration(Eigen::Matrix4d &transform);
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,104 @@
#pragma once
#include <string>
#include <vector>
#include <opencv2/core.hpp>
#include <Eigen/Dense>
#include "../../../common_types.h"
namespace cv { class Mat; }
/**
* @brief 托盘位置偏移检测算法结果
*/
struct PalletOffsetResult {
// 位置偏移量 (相对于参考得出的世界坐标系下的差异)
float offset_lat_mm_value; // 左右偏移量mm- X轴 World
float offset_lon_mm_value; // 前后偏移量mm- Z轴 World
float rotation_angle_value; // 旋转角度(度)- 绕 Y轴 World
// 插孔变形
float hole_def_mm_left_value; // 左侧插孔变形mm
float hole_def_mm_right_value; // 右侧插孔变形mm
// 绝对坐标 (用于生成参考模板)
float abs_x;
float abs_y;
float abs_z;
// 个体插孔坐标 (可选用于调试或Reference生成)
Point3D left_hole_pos;
Point3D right_hole_pos;
// 阈值JSON字符串
std::string offset_lat_mm_threshold;
std::string offset_lon_mm_threshold;
std::string rotation_angle_threshold;
std::string hole_def_mm_left_threshold;
std::string hole_def_mm_right_threshold;
// 警告和报警信号JSON字符串
std::string offset_lat_mm_warning_alarm;
std::string offset_lon_mm_warning_alarm;
std::string rotation_angle_warning_alarm;
std::string hole_def_mm_left_warning_alarm;
std::string hole_def_mm_right_warning_alarm;
bool success; // 算法是否执行成功
PalletOffsetResult()
: offset_lat_mm_value(0.0f)
, offset_lon_mm_value(0.0f)
, rotation_angle_value(0.0f)
, hole_def_mm_left_value(0.0f)
, hole_def_mm_right_value(0.0f)
, abs_x(0.0f), abs_y(0.0f), abs_z(0.0f)
, success(false)
{}
};
/**
* @brief 托盘位置偏移检测算法
*
* 检测托盘位置偏移和插孔变形
* 核心逻辑:
* 1. 纯深度图输入,不依赖点云。
* 2. 交互式 ROI 选择(当未提供 ROI 时)。
* 3. 2D 特征检测 + 稀疏 3D 转换(利用内参和标定矩阵)。
* 4. 世界坐标系下的偏移与变形计算。
*/
class PalletOffsetAlgorithm {
public:
/**
* @brief 执行托盘位置偏移检测
*
* @param depth_img 深度图像 (CV_16U or CV_32F)
* @param color_img 彩色图像 (仅用于显示,可选)
* @param side 货架侧("left"或"right"
* @param result [输出] 检测结果
* @param point_cloud [可选] 点云数据 (若为空,则使用 depth + intrinsics 计算)
* @param roi_points [可选] ROI区域若为空则触发交互式选择
* @param intrinsics [可选] 相机内参,用于 2D->3D 转换。若为0则尝试自动获取。
* @return 是否检测成功
*/
static bool detect(const cv::Mat& depth_img,
const cv::Mat& color_img,
const std::string& side,
PalletOffsetResult& result,
const std::vector<Point3D>* point_cloud = nullptr,
const std::vector<cv::Point2i>& roi_points = {},
const CameraIntrinsics& intrinsics = CameraIntrinsics(),
const cv::Mat* calib_mat_override = nullptr);
private:
/**
* @brief 从 JSON 文件加载标定矩阵
*/
static bool loadCalibration(Eigen::Matrix4d& transform);
/**
* @brief 交互式 ROI 选择
*/
static std::vector<cv::Point2i> selectPolygonROI(const cv::Mat& visual_img);
};

View File

@@ -0,0 +1,261 @@
#include "slot_occupancy_detection.h"
#include <iostream>
#include <mutex>
#include <opencv2/opencv.hpp>
#include <vector>
//====================
// 步骤1配置参数
//====================
namespace Config {
// 基准图文件相对路径列表 (按顺序尝试)
const std::vector<std::string> TEMPLATE_PATHS = {
"src\\images_template\\temp.bmp",
"..\\src\\images_template\\temp.bmp",
"..\\..\\src\\images_template\\temp.bmp",
"..\\..\\..\\src\\images_template\\temp.bmp",
"..\\..\\..\\..\\src\\images_template\\temp.bmp",
"d:\\Git\\stereo_warehouse_inspection\\image_capture\\src\\images_"
"template\\temp.bmp"};
// 差异阈值:当前像素与背景像素相差多少算“有变化” (0-255)
// 建议:如果环境光稳定,设为 20-30如果光照波动大设为 40-50
const int DIFF_THRESHOLD = 28;
// 面积阈值:差异像素总数超过多少算“有货”
// 建议:根据 ROI 大小调整,通常设为 ROI 面积的 5% - 10%
const int AREA_THRESHOLD = 1000000;
// 高斯模糊核大小 (必须是奇数)
const int BLUR_SIZE = 7;
// 目标工作分辨率 (相机分辨率)
const cv::Size TARGET_SIZE(4024, 3036);
// ROI (感兴趣区域) - 默认值
const cv::Rect ROI_DEFAULT(1400, 600, 1200, 1800);
} // namespace Config
//====================
// 步骤2算法上下文用于管理静态资源
//====================
class SlotAlgoContext {
public:
SlotAlgoContext() : initialized_(false) {}
// 初始化:加载并预处理基准图
// 返回值:是否初始化成功
bool ensureInitialized() {
std::lock_guard<std::mutex> lock(mutex_);
if (initialized_)
return true;
cv::Mat raw_ref;
// 1. 尝试加载基准图
for (const auto &path : Config::TEMPLATE_PATHS) {
raw_ref = cv::imread(path, cv::IMREAD_GRAYSCALE);
if (!raw_ref.empty()) {
std::cout << "[SlotAlgo] Loaded template from: " << path << std::endl;
break;
}
}
if (raw_ref.empty()) {
std::cerr << "[SlotAlgo] CRITICAL: Failed to load template image."
<< std::endl;
return false;
}
// 2. 尺寸对齐 (Resize)
// 只有当尺寸不匹配时才执行 Resize确保 ref_img_processed_ 始终是
// TARGET_SIZE
if (raw_ref.size() != Config::TARGET_SIZE) {
std::cout << "[SlotAlgo] Resizing template from " << raw_ref.cols << "x"
<< raw_ref.rows << " to " << Config::TARGET_SIZE.width << "x"
<< Config::TARGET_SIZE.height << std::endl;
cv::resize(raw_ref, ref_img_processed_, Config::TARGET_SIZE);
} else {
ref_img_processed_ = raw_ref;
}
// 3. 预处理:高斯模糊
// 提前对整张基准图进行模糊,避免每帧对 ROI 进行模糊,减少计算量
// (注:如果内存紧张,可以只存原图,但为了速度建议存模糊后的图)
cv::GaussianBlur(ref_img_processed_, ref_img_processed_,
cv::Size(Config::BLUR_SIZE, Config::BLUR_SIZE), 0);
// 4. 初始化形态学核
morph_kernel_ = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(5, 5));
std::cout << "[SlotAlgo] Initialization complete. Reference size: "
<< ref_img_processed_.cols << "x" << ref_img_processed_.rows
<< std::endl;
initialized_ = true;
return true;
}
// 获取处理后的基准图 (只读引用)
const cv::Mat &getRefImage() const { return ref_img_processed_; }
// 获取形态学核
const cv::Mat &getMorphKernel() const { return morph_kernel_; }
bool isInitialized() const { return initialized_; }
private:
std::mutex mutex_;
bool initialized_;
cv::Mat ref_img_processed_; // 存储 Resize 并 Blur 后的基准图
cv::Mat morph_kernel_;
};
// 全局静态上下文实例
static SlotAlgoContext g_algo_context;
//====================
// 步骤3辅助函数
//====================
static cv::Rect getSafeROI(const cv::Rect &request_roi, int img_width,
int img_height) {
cv::Rect roi = request_roi;
roi.x = std::max(0, roi.x);
roi.y = std::max(0, roi.y);
roi.width = std::min(roi.width, img_width - roi.x);
roi.height = std::min(roi.height, img_height - roi.y);
return roi;
}
//====================
// 步骤4核心算法实现
//====================
bool SlotOccupancyAlgorithm::detect(const cv::Mat &depth_img,
const cv::Mat &color_img,
const std::string &side,
SlotOccupancyResult &result) {
// 算法启用开关
const bool USE_ALGORITHM = false;
if (USE_ALGORITHM) {
// --- 真实算法逻辑 ---
// 初始化结果
result.success = false;
result.slot_occupied = false;
// 1. 确保算法所需的资源已加载 (懒加载模式)
if (!g_algo_context.ensureInitialized()) {
std::cerr << "[SlotAlgo] Algorithm not initialized, skipping detection."
<< std::endl;
return false;
}
// 2. 输入检查
if (color_img.empty()) {
std::cerr << "[SlotAlgo] Input image is empty." << std::endl;
return false;
}
try {
// 3. 准备当前帧灰度图 (高效转换)
cv::Mat curr_gray;
if (color_img.channels() == 3) {
cv::cvtColor(color_img, curr_gray, cv::COLOR_BGR2GRAY);
} else {
// 如果已经是灰度图,直接引用,避免拷贝
curr_gray = color_img;
}
// 验证输入尺寸 (假设输入应该匹配目标分辨率)
if (curr_gray.size() != Config::TARGET_SIZE) {
// 如果输入尺寸不对,这里选择报错或者 Resize
// 鉴于这是一个工业场景,分辨率突变通常是异常,这里建议打印警告如果必须处理则
// Resize 但为了效率,我们尽量避免每帧 Resize。
// 如果确实不一样,这里做一个临时 Resize 以保证程序不崩,但会影响性能
// std::cout << "[SlotAlgo] Warning: Input size mismatch. Resizing..."
// << std::endl; 暂时不处理 resize依靠 getSafeROI 防止崩坏,或者在 ROI
// 截取时会出错
}
// 4. 确定 ROI
// 实际应用中根据 side 选择 ROI; 目前使用默认
cv::Rect roi =
getSafeROI(Config::ROI_DEFAULT, curr_gray.cols, curr_gray.rows);
// 5. 截取 ROI
// 直接从 input 和 cached reference 中截取,无需 clone
cv::Mat img_roi = curr_gray(roi);
const cv::Mat &ref_full = g_algo_context.getRefImage();
// 确保 ref_full 够大覆盖 ROI (理论上 init 中已经 resize 到了 TARGET_SIZE)
// 双重保险
cv::Rect ref_roi_rect = getSafeROI(roi, ref_full.cols, ref_full.rows);
if (ref_roi_rect != roi) {
std::cerr << "[SlotAlgo] Error: Reference image size mismatch with ROI."
<< std::endl;
return false;
}
cv::Mat ref_roi = ref_full(ref_roi_rect);
// 6. 图像处理 pipeline
// 只对当前帧 ROI 做高斯模糊 (基准图已经预处理过了)
cv::Mat img_roi_blurred;
cv::GaussianBlur(img_roi, img_roi_blurred,
cv::Size(Config::BLUR_SIZE, Config::BLUR_SIZE), 0);
// 绝对差分
cv::Mat diff;
cv::absdiff(img_roi_blurred, ref_roi, diff);
// 二值化
cv::Mat mask;
cv::threshold(diff, mask, Config::DIFF_THRESHOLD, 255, cv::THRESH_BINARY);
// 形态学滤波 (去除噪点)
cv::morphologyEx(mask, mask, cv::MORPH_OPEN,
g_algo_context.getMorphKernel());
// 7. 统计判定
int non_zero_pixels = cv::countNonZero(mask);
// 可选:仅在状态变化时打印,避免刷屏
// std::cout << "[SlotAlgo] Diff pixels: " << non_zero_pixels <<
// std::endl;
if (non_zero_pixels > Config::AREA_THRESHOLD) {
result.slot_occupied = true;
} else {
result.slot_occupied = false;
}
result.success = true;
// std::cout << "[SlotAlgo] Result: " << (result.slot_occupied ?
// "Occupied" : "Empty") << std::endl;
} catch (const cv::Exception &e) {
std::cerr << "[SlotAlgo] OpenCV Exception: " << e.what() << std::endl;
result.success = false;
return false;
} catch (...) {
std::cerr << "[SlotAlgo] Unknown Exception during detection."
<< std::endl;
result.success = false;
return false;
}
return result.success;
} else {
// --- 模拟数据逻辑 ---
std::cout << "[SlotOccupancyAlgorithm] Using FAKE DATA implementation "
"(Switch OFF)."
<< std::endl;
result.slot_occupied = false; // 模拟无货
result.success = true;
return result.success;
}
}

View File

@@ -0,0 +1,37 @@
#pragma once
#include <string>
namespace cv { class Mat; }
/**
* @brief 货位有无检测算法结果
*/
struct SlotOccupancyResult {
bool slot_occupied; // 货位是否有托盘/货物
bool success; // 算法是否执行成功
SlotOccupancyResult() : slot_occupied(false), success(false) {}
};
/**
* @brief 货位有无检测算法
*
* 分析深度图或彩色图,判断货位是否有托盘/货物
*/
class SlotOccupancyAlgorithm {
public:
/**
* 执行货位有无检测
* @param depth_img 深度图像
* @param color_img 彩色图像
* @param side 货架侧("left"或"right"
* @param result [输出] 检测结果
* @return 是否检测成功
*/
static bool detect(const cv::Mat& depth_img,
const cv::Mat& color_img,
const std::string& side,
SlotOccupancyResult& result);
};

View File

@@ -0,0 +1,129 @@
#include "visual_inventory_detection.h"
#include "HalconCpp.h"
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace HalconCpp;
// Helper to convert cv::Mat to Halcon HImage
HImage MatToHImage(const cv::Mat &image) {
HImage hImage;
if (image.empty())
return hImage;
cv::Mat gray;
if (image.channels() == 3) {
cv::cvtColor(image, gray, cv::COLOR_BGR2GRAY);
} else {
gray = image.clone();
}
// Fix: Create a copy of the data to ensure it persists beyond function scope
// GenImage1 with "byte" type expects the data to remain valid
void *data_copy = new unsigned char[gray.total()];
memcpy(data_copy, gray.data, gray.total());
try {
hImage.GenImage1("byte", gray.cols, gray.rows, data_copy);
} catch (...) {
delete[] static_cast<unsigned char *>(data_copy);
throw;
}
// Note: The data_copy will be managed by Halcon's HImage
// We don't delete it here as Halcon takes ownership
return hImage;
}
bool VisualInventoryAlgorithm::detect(const cv::Mat &depth_img,
const cv::Mat &color_img,
const std::string &side,
VisualInventoryResult &result) {
result.success = false;
try {
if (color_img.empty()) {
std::cerr << "[VisualInventoryAlgorithm] Error: Empty image input."
<< std::endl;
return false;
}
// 1. Convert to HImage
HImage hImage = MatToHImage(color_img);
// 2. Setup Halcon QR Code Model
HDataCode2D dataCode2d;
dataCode2d.CreateDataCode2dModel("QR Code", HTuple(), HTuple());
dataCode2d.SetDataCode2dParam("default_parameters", "enhanced_recognition");
HTuple resultHandles, decodedDataStrings;
// 3. Detect
// stop_after_result_num: 100 ensures we get up to 100 codes
HXLDCont symbolXLDs =
dataCode2d.FindDataCode2d(hImage, "stop_after_result_num", 100,
&resultHandles, &decodedDataStrings);
// 4. Transform Results to JSON
// Format: {"A01":["BOX111","BOX112"], "A02":["BOX210"]}
// Since we don't have position information, group all codes under a generic
// key
std::string json_barcodes = "\"" + side + "\":[";
Hlong count = decodedDataStrings.Length();
for (Hlong i = 0; i < count; i++) {
if (i > 0)
json_barcodes += ",";
// Access string from HTuple using S() which returns const char*
HTuple s = decodedDataStrings[i];
std::string code = std::string(s.S());
// Save raw code for deduplication
result.codes.push_back(code);
// Escape special characters in JSON strings
// Replace backslashes first, then quotes
size_t pos = 0;
while ((pos = code.find('\\', pos)) != std::string::npos) {
code.replace(pos, 1, "\\\\");
pos += 2;
}
pos = 0;
while ((pos = code.find('"', pos)) != std::string::npos) {
code.replace(pos, 1, "\\\"");
pos += 2;
}
json_barcodes += "\"" + code + "\"";
}
json_barcodes += "]";
result.result_barcodes = "{" + json_barcodes + "}";
result.success = true;
std::cout << "[VisualInventoryAlgorithm] Side: " << side
<< " | Detected: " << count << " codes." << std::endl;
} catch (HException &except) {
std::cerr << "[VisualInventoryAlgorithm] Halcon Exception: "
<< except.ErrorMessage().Text() << std::endl;
result.result_barcodes = "{\"" + side +
"\":[], \"error\":\"Halcon Exception: " +
std::string(except.ErrorMessage().Text()) + "\"}";
result.success = false;
} catch (std::exception &e) {
std::cerr << "[VisualInventoryAlgorithm] Exception: " << e.what()
<< std::endl;
result.result_barcodes =
"{\"" + side + "\":[], \"error\":\"" + std::string(e.what()) + "\"}";
result.success = false;
} catch (...) {
std::cerr
<< "[VisualInventoryAlgorithm] Unknown Exception during detection."
<< std::endl;
result.result_barcodes =
"{\"" + side + "\":[], \"error\":\"Unknown exception\"}";
result.success = false;
}
return result.success;
}

View File

@@ -0,0 +1,40 @@
#pragma once
#include <string>
#include <vector>
namespace cv {
class Mat;
}
/**
* @brief 视觉盘点检测算法结果
*/
struct VisualInventoryResult {
std::string
result_barcodes; // 条码扫描结果JSON: {"left":["BOX111","BOX112"]} 或
// 错误时: {"left":[], "error":"error message"}
std::vector<std::string> codes; // 原始条码列表,便于去重
bool success; // 算法是否执行成功
VisualInventoryResult() : success(false) {}
};
/**
* @brief 视觉盘点检测算法
*
* 识别货位位置并扫描条码
*/
class VisualInventoryAlgorithm {
public:
/**
* 执行视觉盘点检测
* @param depth_img 深度图像
* @param color_img 彩色图像
* @param side 货架侧("left"或"right"
* @param result [输出] 检测结果
* @return 是否检测成功
*/
static bool detect(const cv::Mat &depth_img, const cv::Mat &color_img,
const std::string &side, VisualInventoryResult &result);
};

View File

@@ -0,0 +1,150 @@
#include "image_processor.h"
#include <opencv2/opencv.hpp>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <chrono>
#include <ctime>
#include <filesystem>
// ========== ImageProcessor 类实现 ==========
/**
* 构造函数
*/
ImageProcessor::ImageProcessor() {}
/**
* 处理深度图像
*
* @param depth_img 输入的深度图像16位无符号整数CV_16U类型
* @return cv::Mat 处理后的深度图(已应用伪彩色映射)
*/
cv::Mat ImageProcessor::processDepthImage(const cv::Mat& depth_img) {
if (depth_img.empty())
return cv::Mat();
// 确保输入是16位深度图
cv::Mat depthMap;
if (depth_img.type() == CV_16U) {
depthMap = depth_img;
} else {
// 如果不是16位尝试转换或返回空
return cv::Mat();
}
// 创建掩码标记无效深度值0值表示无效/无数据)
cv::Mat invalid_mask = (depthMap == 0);
// 性能优化避免不必要的clone直接使用depthMap的视图
// 只有在需要修改时才创建副本
cv::Mat depthProcessed;
// 检查是否有无效值需要处理
int invalid_count = cv::countNonZero(invalid_mask);
if (invalid_count > 0) {
// 有无效值,需要创建副本并修改
depthProcessed = depthMap.clone();
// 将无效值设置为一个很大的值,这样在归一化时会被排除
depthProcessed.setTo(65535, invalid_mask);
} else {
// 没有无效值,直接使用原图(避免不必要的复制)
depthProcessed = depthMap;
}
// 计算有效深度值的范围(排除无效值)
double minVal, maxVal;
cv::minMaxLoc(depthProcessed, &minVal, &maxVal, nullptr, nullptr,
~invalid_mask);
// 如果所有值都无效,返回黑色图像
if (maxVal == 0 || minVal == 65535) {
cv::Mat blackImg = cv::Mat::zeros(depthMap.size(), CV_8UC3);
return blackImg;
}
// 如果所有有效深度值都相同maxVal == minVal避免除零错误
// 返回一个统一颜色的深度图中等灰色对应JET色图的中间值
if (maxVal == minVal) {
// 创建单通道灰度图有效区域设置为中等灰度值128对应JET色图的中间颜色
cv::Mat grayMat = cv::Mat::zeros(depthMap.size(), CV_8UC1);
grayMat.setTo(128, ~invalid_mask);
// 应用伪彩色映射
cv::Mat uniformImg;
cv::applyColorMap(grayMat, uniformImg, cv::COLORMAP_JET);
// 确保无效区域保持黑色
uniformImg.setTo(cv::Scalar(0, 0, 0), invalid_mask);
return uniformImg;
}
// 归一化有效深度值到0-255范围
cv::Mat depthVis;
depthProcessed.convertTo(depthVis, CV_8U, 255.0 / (maxVal - minVal),
-minVal * 255.0 / (maxVal - minVal));
// 将无效区域设置为0黑色
depthVis.setTo(0, invalid_mask);
// 应用伪彩色映射以提高可视性JET色图蓝色=近,红色=远)
cv::applyColorMap(depthVis, depthVis, cv::COLORMAP_JET);
// 确保无效区域保持黑色(伪彩色映射后可能改变,需要再次设置)
depthVis.setTo(cv::Scalar(0, 0, 0), invalid_mask);
return depthVis;
}
/**
* 保存图像到文件
*
* @param depth_img 深度图像(可选)
* @param color_img 彩色图像(可选)
* @param frame_num 帧编号,用于文件命名
* @param save_dir 保存目录(可选,默认为当前目录)
*/
void ImageProcessor::saveImages(const cv::Mat& depth_img, const cv::Mat& color_img,
int frame_num, const std::string& save_dir) {
// 创建保存目录(如果指定了且不存在)
std::string actual_dir = save_dir.empty() ? "." : save_dir;
if (!save_dir.empty()) {
try {
std::filesystem::create_directories(actual_dir);
} catch (const std::exception& e) {
std::cerr << "[Save] Failed to create directory: " << e.what() << std::endl;
actual_dir = "."; // 回退到当前目录
}
}
// 获取当前系统时间用于文件命名
auto now = std::chrono::system_clock::now();
auto time_t = std::chrono::system_clock::to_time_t(now);
std::tm* tm = std::localtime(&time_t);
char time_str[64];
std::strftime(time_str, sizeof(time_str), "%Y%m%d_%H%M%S", tm);
// 保存深度图
if (!depth_img.empty()) {
std::stringstream depth_filename;
depth_filename << actual_dir << "/depth_" << time_str << "_frame"
<< std::setfill('0') << std::setw(6) << frame_num << ".png";
if (cv::imwrite(depth_filename.str(), depth_img)) {
std::cout << "[Save] Depth image saved: " << depth_filename.str() << std::endl;
} else {
std::cerr << "[Save] Failed to save depth image: " << depth_filename.str() << std::endl;
}
}
// 保存彩色图
if (!color_img.empty()) {
std::stringstream color_filename;
color_filename << actual_dir << "/color_" << time_str << "_frame"
<< std::setfill('0') << std::setw(6) << frame_num << ".png";
if (cv::imwrite(color_filename.str(), color_img)) {
std::cout << "[Save] Color image saved: " << color_filename.str() << std::endl;
} else {
std::cerr << "[Save] Failed to save color image: " << color_filename.str() << std::endl;
}
}
}

View File

@@ -0,0 +1,63 @@
#pragma once
// OpenCV图像处理模块头文件
// 负责深度图的处理(伪彩色映射)、显示和保存
// 注意此模块不依赖SDK只使用OpenCV标准类型
// 注意:彩色图的颜色空间转换已在图像采集层完成,此处不再处理
#include <string>
namespace cv { class Mat; }
/**
* 图像处理器类
*
* 功能说明:
* - 处理深度图(归一化、伪彩色映射)
* - 保存图像到文件
*
* 注意彩色图的颜色空间转换已在图像采集层camera层完成
* 统一输出BGR格式此处不再需要处理
*
* 设计原则:
* - 此模块完全独立于SDK只使用OpenCV的cv::Mat类型
* - SDK到OpenCV的转换应在图像采集层camera层完成
* - 图像显示由GUI层MainWindow负责使用Qt的QLabel分别显示
*/
class ImageProcessor
{
public:
/**
* 构造函数
*/
ImageProcessor();
/**
* 处理深度图像
*
* @param depth_img 输入的深度图像16位无符号整数CV_16U类型
* @return cv::Mat 处理后的深度图已应用伪彩色映射BGR格式
*
* 功能说明:
* - 将深度值归一化到0-255范围
* - 应用JET伪彩色映射蓝色=近,红色=远)
* - 处理无效深度值0值
*/
cv::Mat processDepthImage(const cv::Mat& depth_img);
/**
* 保存图像到文件
*
* @param depth_img 深度图像(可选)
* @param color_img 彩色图像(可选)
* @param frame_num 帧编号,用于文件命名
* @param save_dir 保存目录(可选,默认为当前目录)
*
* 功能说明:
* - 获取当前时间戳用于文件命名
* - 保存深度图和彩色图到文件
*/
void saveImages(const cv::Mat& depth_img, const cv::Mat& color_img,
int frame_num, const std::string& save_dir = "");
};

View File

@@ -0,0 +1,306 @@
/**
* @file mvs_multi_camera_capture.cpp
* @brief 海康 MVS 相机采集实现文件
*
* 此文件包含了 MvsMultiCameraCapture 类的完整实现
* - 封装 MVS SDK (MvCameraControl.h),管理多相机采集
* - 将 SDK 的原始帧数据转换为 OpenCV 的 cv::Mat 格式
* - 管理采集线程和缓冲区
*
* 设计说明:
* - 每个相机使用独立的采集线程,避免阻塞
* - 使用线程安全的缓冲区存储最新图像
* - 统一输出 BGR 格式的彩色图
*/
#include "mvs_multi_camera_capture.h"
#include "MvCameraControl.h"
#include <iostream>
#include <chrono>
#include <cstring>
/**
* @brief 构造函数
* 初始化运行标志和状态
*/
MvsMultiCameraCapture::MvsMultiCameraCapture() : running_(false), initialized_(false) {}
/**
* @brief 析构函数
* 确保在对象销毁时正确停止所有采集线程和相机,并清理资源
*/
MvsMultiCameraCapture::~MvsMultiCameraCapture() {
stop();
// 清理句柄
for (auto& cam : cameras_) {
if (cam.handle) {
MV_CC_DestroyHandle(cam.handle);
cam.handle = nullptr;
}
}
}
/**
* @brief 初始化相机
*
* 此函数完成以下工作:
* 1. 枚举所有连接的 GenTL GigE 和 USB 设备
* 2. 创建并打开相机句柄
* 3. 配置相机参数(如触发模式、包大小等)
* 4. 初始化图像缓冲区
*
* @return true 初始化成功且至少找到一个设备, false 失败
*/
bool MvsMultiCameraCapture::initialize() {
if (initialized_) return true;
MV_CC_DEVICE_INFO_LIST stDeviceList;
memset(&stDeviceList, 0, sizeof(MV_CC_DEVICE_INFO_LIST));
// 枚举 GenTL GigE 和 USB 设备
int nRet = MV_CC_EnumDevices(MV_GIGE_DEVICE | MV_USB_DEVICE, &stDeviceList);
if (MV_OK != nRet) {
std::cerr << "[MVS] EnumDevices failed: " << std::hex << nRet << std::endl;
return false;
}
if (stDeviceList.nDeviceNum == 0) {
std::cout << "[MVS] No devices found." << std::endl;
return false;
}
std::cout << "[MVS] Found " << stDeviceList.nDeviceNum << " devices." << std::endl;
for (unsigned int i = 0; i < stDeviceList.nDeviceNum; i++) {
MV_CC_DEVICE_INFO* pDeviceInfo = stDeviceList.pDeviceInfo[i];
if (NULL == pDeviceInfo) continue;
CameraInfo camInfo;
camInfo.index = static_cast<int>(cameras_.size());
// 获取序列号
if (pDeviceInfo->nTLayerType == MV_GIGE_DEVICE) {
camInfo.serial_number = std::string((char*)pDeviceInfo->SpecialInfo.stGigEInfo.chSerialNumber);
} else if (pDeviceInfo->nTLayerType == MV_USB_DEVICE) {
camInfo.serial_number = std::string((char*)pDeviceInfo->SpecialInfo.stUsb3VInfo.chSerialNumber);
}
// 创建句柄
nRet = MV_CC_CreateHandle(&camInfo.handle, pDeviceInfo);
if (MV_OK != nRet) {
std::cerr << "[MVS] CreateHandle failed for device " << i << std::endl;
continue;
}
// 打开设备
nRet = MV_CC_OpenDevice(camInfo.handle);
if (MV_OK != nRet) {
std::cerr << "[MVS] OpenDevice failed for device " << i << std::endl;
MV_CC_DestroyHandle(camInfo.handle);
continue;
}
// 确保触发模式为 OFF 以进行连续采集
nRet = MV_CC_SetEnumValue(camInfo.handle, "TriggerMode", MV_TRIGGER_MODE_OFF);
if (MV_OK != nRet) {
std::cerr << "[MVS] Warning: Failed to set TriggerMode to Off. Ret = " << std::hex << nRet << std::endl;
}
// 检查 GigE 的最佳包大小并设置
if (pDeviceInfo->nTLayerType == MV_GIGE_DEVICE) {
int nPacketSize = MV_CC_GetOptimalPacketSize(camInfo.handle);
if (nPacketSize > 0) {
MV_CC_SetIntValue(camInfo.handle, "GevSCPSPacketSize", nPacketSize);
}
}
cameras_.push_back(camInfo);
buffers_.push_back(std::make_shared<ImageBuffer>());
// 记录相机分辨率
MVCC_INTVALUE stWidth = {0};
MVCC_INTVALUE stHeight = {0};
int nRetW = MV_CC_GetIntValue(camInfo.handle, "Width", &stWidth);
int nRetH = MV_CC_GetIntValue(camInfo.handle, "Height", &stHeight);
std::cout << "[MVS] Initialized camera " << camInfo.index << ": " << camInfo.serial_number;
if (MV_OK == nRetW && MV_OK == nRetH) {
std::cout << " (Resolution: " << stWidth.nCurValue << "x" << stHeight.nCurValue << ")";
}
std::cout << std::endl;
}
initialized_ = true;
return !cameras_.empty();
}
/**
* @brief 开始采集
* 启动所有相机的抓图,并为每个相机创建一个采集线程
* @return true 启动成功, false 失败
*/
bool MvsMultiCameraCapture::start() {
if (!initialized_ || running_) return false;
running_ = true;
for (const auto& cam : cameras_) {
// 开始抓取
int nRet = MV_CC_StartGrabbing(cam.handle);
if (MV_OK != nRet) {
std::cerr << "[MVS] StartGrabbing failed for camera " << cam.index << std::endl;
}
threads_.emplace_back(&MvsMultiCameraCapture::captureThreadFunc, this, cam.index);
}
return true;
}
/**
* @brief 停止采集
* 停止所有采集线程和相机抓图
*/
void MvsMultiCameraCapture::stop() {
running_ = false;
for (auto& t : threads_) {
if (t.joinable()) t.join();
}
threads_.clear();
for (const auto& cam : cameras_) {
MV_CC_StopGrabbing(cam.handle);
}
}
/**
* @brief 获取相机 ID (序列号)
* @param camera_index 相机索引
* @return 相机序列号
*/
std::string MvsMultiCameraCapture::getCameraId(int camera_index) const {
if (camera_index >= 0 && camera_index < cameras_.size()) {
return cameras_[camera_index].serial_number;
}
return "";
}
/**
* @brief 获取指定相机的最新图像
* 从线程安全的缓冲区中读取最新图像数据
*
* @param camera_index 相机索引
* @param[out] image 输出图像
* @param[out] fps 当前帧率
* @return true 成功获取, false 索引无效或无新图像
*/
bool MvsMultiCameraCapture::getLatestImage(int camera_index, cv::Mat& image, double& fps) {
if (camera_index < 0 || camera_index >= buffers_.size()) return false;
auto& buffer = buffers_[camera_index];
std::lock_guard<std::mutex> lock(buffer->mtx);
if (buffer->image.empty()) return false;
image = buffer->image.clone();
fps = buffer->fps;
buffer->updated = false;
return true;
}
/**
* @brief 转换为 OpenCV Mat 格式
*
* 将 SDK 返回的帧数据转换为 OpenCV 的 BGR8 Mat。
*
* @param handle 相机句柄
* @param pFrame MVS 帧信息结构体指针 (MV_FRAME_OUT*)
* @param pUser 用户数据 (未使用)
* @return cv::Mat 转换后的 OpenCV 图像
*/
cv::Mat MvsMultiCameraCapture::convertToMat(void* handle, void* pFrame, void* pUser) {
// pFrame 在 captureThreadFunc 中传入的是 MV_FRAME_OUT* 指针
MV_FRAME_OUT* stFrameOut = (MV_FRAME_OUT*)pFrame;
MV_FRAME_OUT_INFO_EX* stUserInfo = stFrameOut->stFrameInfo.enPixelType == 0 ? nullptr : &stFrameOut->stFrameInfo;
if (!handle || !stUserInfo) return cv::Mat();
cv::Mat image;
MV_CC_PIXEL_CONVERT_PARAM stConvertParam = {0};
stConvertParam.nWidth = stUserInfo->nWidth;
stConvertParam.nHeight = stUserInfo->nHeight;
stConvertParam.pSrcData = stFrameOut->pBufAddr;
stConvertParam.nSrcDataLen = stUserInfo->nFrameLen;
stConvertParam.enSrcPixelType = stUserInfo->enPixelType;
stConvertParam.enDstPixelType = PixelType_Gvsp_BGR8_Packed; // 转换为 OpenCV 的 BGR8
stConvertParam.nDstBufferSize = stUserInfo->nWidth * stUserInfo->nHeight * 3;
// 分配目标缓冲区
image.create(stUserInfo->nHeight, stUserInfo->nWidth, CV_8UC3);
stConvertParam.pDstBuffer = image.data;
int nRet = MV_CC_ConvertPixelType(handle, &stConvertParam);
if (MV_OK != nRet) {
std::cerr << "[MVS] ConvertPixelType failed: " << std::hex << nRet << std::endl;
return cv::Mat();
}
return image;
}
/**
* @brief 采集线程函数
*
* 每个相机的独立工作线程:
* 1. 循环调用 MV_CC_GetImageBuffer 获取图像
* 2. 调用 convertToMat 转换为 cv::Mat
* 3. 更新线程安全缓冲区
* 4. 计算并更新 FPS
*
* @param camera_index 相机索引
*/
void MvsMultiCameraCapture::captureThreadFunc(int camera_index) {
auto& cam = cameras_[camera_index];
auto& buffer = buffers_[camera_index];
MV_FRAME_OUT stFrameOut;
memset(&stFrameOut, 0, sizeof(MV_FRAME_OUT));
auto start_time = std::chrono::steady_clock::now();
int frame_count = 0;
while (running_) {
// 获取图像缓冲区,超时 1000ms
int nRet = MV_CC_GetImageBuffer(cam.handle, &stFrameOut, 1000);
if (MV_OK == nRet) {
try {
// 传递 stFrameOut 指针进行转换
cv::Mat image = convertToMat(cam.handle, &stFrameOut, nullptr);
if (!image.empty()) {
std::lock_guard<std::mutex> lock(buffer->mtx);
buffer->image = image;
buffer->updated = true;
frame_count++;
auto now = std::chrono::steady_clock::now();
double elapsed = std::chrono::duration_cast<std::chrono::seconds>(now - start_time).count();
if (elapsed >= 1.0) {
buffer->fps = frame_count / elapsed;
frame_count = 0;
start_time = now;
// std::cout << "[MVS] Cam " << camera_index << " FPS: " << buffer->fps << std::endl;
}
}
} catch (const std::exception& e) {
std::cerr << "[MVS] Exception in conversion: " << e.what() << std::endl;
}
// 释放图像缓冲区
MV_CC_FreeImageBuffer(cam.handle, &stFrameOut);
} else {
// 如果触发器正在等待,超时是预期的,但我们设置的是连续采集。
std::cerr << "[MVS] GetImageBuffer failed: " << std::hex << nRet << std::endl;
}
}
}

View File

@@ -0,0 +1,122 @@
#pragma once
#include <opencv2/opencv.hpp>
#include <vector>
#include <string>
#include <atomic>
#include <thread>
#include <mutex>
#include <memory>
/**
* @file mvs_multi_camera_capture.h
* @brief 海康 MVS 相机采集类定义
*/
/**
* @brief 图像缓冲区结构体
* 存储从相机采集到的最新图像及相关元数据
*/
struct ImageBuffer {
cv::Mat image; ///< 存储图像数据 (BGR格式)
std::mutex mtx; ///< 互斥锁,保证多线程访问安全
bool updated = false; ///< 标记图像是否已更新
double fps = 0.0; ///< 当前帧率
};
/**
* @brief 相机信息结构体
* 存储相机的句柄和序列号等信息
*/
struct CameraInfo {
void* handle = nullptr; ///< MVS SDK 相机句柄
std::string serial_number; ///< 相机序列号
int index = -1; ///< 相机索引
};
/**
* @brief MVS 多相机采集类
*
* 功能说明:
* - 封装海康 MVS SDK管理多相机采集
* - 将 SDK 原始图像转换为 OpenCV Mat 格式
* - 管理采集线程和缓冲区
* - 提供最新的图像数据供上层调用
*/
class MvsMultiCameraCapture {
public:
MvsMultiCameraCapture();
~MvsMultiCameraCapture();
/**
* @brief 初始化相机
* 枚举并打开所有连接的 GenTL GigE 和 USB 设备
* @return true 初始化成功且至少找到一个设备, false 失败
*/
bool initialize();
/**
* @brief 开始采集
* 启动所有相机的抓图,并开启采集线程
* @return true 启动成功, false 失败
*/
bool start();
/**
* @brief 停止采集
* 停止抓图并关闭所有线程
*/
void stop();
/**
* @brief 获取相机数量
* @return 已初始化的相机数量
*/
int getCameraCount() const { return static_cast<int>(cameras_.size()); }
/**
* @brief 获取指定相机的最新图像
* @param camera_index 相机索引
* @param[out] image 输出图像 (BGR格式)
* @param[out] fps 当前帧率
* @return true 成功获取, false 索引无效或无新图像
*/
bool getLatestImage(int camera_index, cv::Mat& image, double& fps);
/**
* @brief 获取相机 ID (序列号)
* @param camera_index 相机索引
* @return 相机序列号字符串
*/
std::string getCameraId(int camera_index) const;
/**
* @brief 检查是否正在运行
* @return true 运行中, false 已停止
*/
bool isRunning() const { return running_; }
private:
/**
* @brief 采集线程函数
* 每个相机运行在独立的线程中,持续从 SDK 获取图像
* @param camera_index 相机索引
*/
void captureThreadFunc(int camera_index);
/**
* @brief 转换为 OpenCV Mat 格式
* 使用 SDK 的 MV_CC_ConvertPixelType 函数将原始数据转换为 BGR8 格式
* @param handle 相机句柄
* @param pFrame 帧数据指针 (MV_FRAME_OUT*)
* @param pUser 用户数据 (保留,未使用)
* @return cv::Mat 转换后的图像
*/
static cv::Mat convertToMat(void* handle, void* pFrame, void* pUser);
std::vector<CameraInfo> cameras_; ///< 相机列表
std::vector<std::shared_ptr<ImageBuffer>> buffers_; ///< 缓冲区列表
std::vector<std::thread> threads_; ///< 采集线程列表
std::atomic<bool> running_; ///< 运行状态标志
bool initialized_ = false; ///< 初始化状态标志
};

View File

@@ -0,0 +1,950 @@
/**
* @file ty_multi_camera_capture.cpp
* @brief TY相机采集实现文件
*
* 此文件包含了 CameraCapture 类的完整实现
* - 封装TY相机SDK管理多相机采集
* - 将SDK的TYImage转换为OpenCV的cv::Mat格式
* - 管理采集线程和缓冲区
* - 输出原始cv::Mat格式的图像供上层使用
*
* 设计说明:
* - 每个相机使用独立的采集线程,避免阻塞
* - 使用线程安全的缓冲区存储最新图像
* - 使用clone()确保数据安全,避免悬空指针
* - 统一输出BGR格式的彩色图便于上层处理
*/
#include "ty_multi_camera_capture.h"
#include "TYCoordinateMapper.h"
#include <chrono>
#include <iostream>
#ifdef _WIN32
#include <windows.h>
#endif
/**
* @brief 构造函数
*
* 初始化所有成员变量为默认值:初始化列表的方式初始化成员变量,避免在构造函数体中初始化,提高代码可读性。
* - streams_configured_: 流未配置
* - depth_enabled_: 深度流未启用
* - color_enabled_: 彩色流未启用
* - running_: 未运行状态
*/
CameraCapture::CameraCapture()
: streams_configured_(false), depth_enabled_(false), color_enabled_(false),
running_(false) {}
/**
* @brief 析构函数
*
* 确保在对象销毁时正确停止所有采集线程和相机
* 调用stop()来清理资源,避免资源泄漏
*/
CameraCapture::~CameraCapture() { stop(); }
/**
* @brief 初始化并配置相机
*
* 此函数完成以下工作:
* 1. 清理现有资源(如果之前已初始化)
* 2. 查询并打开所有可用的相机设备
* 3. 为每个相机配置深度流和彩色流
* 4. 创建图像处理器和缓冲区
*
* @param enable_depth 是否启用深度流true表示启用深度图采集
* @param enable_color 是否启用彩色流true表示启用彩色图采集
* @return true 初始化成功false 初始化失败(无设备或打开失败)
*
* @note 如果部分相机打开失败只要至少有一个相机成功打开函数仍返回true
*/
bool CameraCapture::initialize(bool enable_depth, bool enable_color) {
// 设置控制台代码页为UTF-8确保中文正确显示
#ifdef _WIN32
SetConsoleOutputCP(65001); // UTF-8代码页
SetConsoleCP(65001); // 设置输入代码页也为UTF-8
#endif
// 清理现有资源,确保重新初始化时状态干净
stop();
cameras_.clear();
depth_processers_.clear();
color_processers_.clear();
camera_running_.clear();
buffers_.clear();
calib_infos_.clear();
has_calib_info_.clear();
// 通过TY SDK的上下文查询设备列表
// TYContext是单例模式获取全局唯一的上下文实例
auto &context = TYContext::getInstance();
auto device_list = context.queryDeviceList();
// 检查是否找到设备-检查device_list是否为空或者设备数量为0
if (!device_list || device_list->empty()) {
std::cerr << "[CameraCapture] No devices found!" << std::endl;
return false;
}
// 获取所有可用设备数量,使用所有找到的设备
int device_count = device_list->devCount();
std::cout << "[CameraCapture] Found " << device_count
<< " device(s), will use all available devices" << std::endl;
// 遍历设备列表,逐个打开所有可用相机
for (int i = 0; i < device_count; i++) {
// 获取设备信息包含设备ID等
auto device_info = device_list->getDeviceInfo(i);
if (!device_info) {
std::cerr << "[CameraCapture] Failed to get camera device info! " << i << std::endl;
continue; // 跳过此设备,继续处理下一个
}
std::cout << "[CameraCapture] Preparing to open camera! " << i << ": "
<< device_info->id() << std::endl;
// 创建FastCamera对象SDK提供的相机封装类
auto camera = std::make_shared<FastCamera>();
// 使用设备ID打开相机
TY_STATUS status = camera->open(device_info->id());
// 检查打开是否成功
if (status != TY_STATUS_OK) {
std::cerr << "[CameraCapture] Failed to open camera! " << i << ": "
<< device_info->id() << std::endl;
continue; // 打开失败,跳过此设备
} else {
std::cout << "[CameraCapture] Successfully opened camera! " << i << ": "
<< device_info->id() << std::endl;
}
// 成功打开,添加到相机列表
cameras_.push_back(camera);
camera_running_.push_back(false); // 初始状态为未运行
// 图像处理器稍后在配置流时创建,这里先占位
depth_processers_.push_back(nullptr);
color_processers_.push_back(nullptr);
// 获取并保存标定信息
TY_CAMERA_CALIB_INFO calib_info;
TY_STATUS calib_status = TYGetStruct(camera->handle(), TY_COMPONENT_DEPTH_CAM, TY_STRUCT_CAM_CALIB_DATA,
&calib_info, sizeof(calib_info));
if (calib_status == TY_STATUS_OK) {
calib_infos_.push_back(calib_info);
has_calib_info_.push_back(true);
std::cout << "[CameraCapture] Camera " << i << " calibration info fetched." << std::endl;
} else {
calib_infos_.push_back(TY_CAMERA_CALIB_INFO());
has_calib_info_.push_back(false);
std::cerr << "[CameraCapture] Camera " << i << " failed to fetch calibration info: " << calib_status << std::endl;
}
}
// 检查是否至少成功打开一个相机
if (cameras_.empty()) {
std::cerr << "[CameraCapture] No cameras opened successfully!" << std::endl;
return false;
}
// ========== 配置流 ==========
// 保存流配置标志,供后续使用
depth_enabled_ = enable_depth;
color_enabled_ = enable_color;
// 为每个已打开的相机配置流
for (size_t i = 0; i < cameras_.size(); i++) {
auto &camera = cameras_[i];
// 启用深度流
if (enable_depth) {
// 调用SDK接口启用深度流
TY_STATUS status = camera->stream_enable(FastCamera::stream_depth);
if (status != TY_STATUS_OK) {
std::cerr << "[CameraCapture] Camera " << i << " failed to enable depth stream"
<< std::endl;
} else {
// 创建深度图像处理器
// ImageProcesser用于处理SDK返回的原始图像数据
std::string depth_win_name = "depth_" + std::to_string(i);
depth_processers_[i] =
std::make_shared<ImageProcesser>(depth_win_name.c_str());
std::cout << "[CameraCapture] Camera " << i << " depth stream enabled"
<< std::endl;
}
}
// 启用彩色流
if (enable_color) {
// 调用SDK接口启用彩色流
TY_STATUS status = camera->stream_enable(FastCamera::stream_color);
if (status != TY_STATUS_OK) {
std::cerr << "[CameraCapture] Camera " << i << " failed to enable color stream"
<< std::endl;
} else {
// 创建彩色图像处理器
std::string color_win_name = "color_" + std::to_string(i);
color_processers_[i] =
std::make_shared<ImageProcesser>(color_win_name.c_str());
std::cout << "[CameraCapture] Camera " << i << " color stream enabled"
<< std::endl;
}
}
}
// ========== 设置分辨率 ==========
// 为每个相机设置深度图和彩色图分辨率为640x480
for (size_t i = 0; i < cameras_.size(); i++) {
auto &camera = cameras_[i];
TY_DEV_HANDLE hDevice = camera->handle();
if (hDevice == 0) {
std::cerr << "[CameraCapture] Camera " << i << " handle is invalid, skip resolution setting"
<< std::endl;
continue;
}
// 设置深度图分辨率为1280x960使用图像模式
if (enable_depth) {
// 方法1尝试使用图像模式推荐同时设置分辨率和格式
TY_IMAGE_MODE depth_mode = TY_IMAGE_MODE_DEPTH16_1280x960;
TY_STATUS status = TYSetEnum(hDevice, TY_COMPONENT_DEPTH_CAM, TY_ENUM_IMAGE_MODE, depth_mode);
if (status != TY_STATUS_OK) {
// 方法2如果图像模式不支持回退到单独设置宽高
std::cerr << "[CameraCapture] Camera " << i << " failed to set depth image mode 1280x960, trying width/height: "
<< status << "(" << TYErrorString(status) << ")" << std::endl;
status = TYSetInt(hDevice, TY_COMPONENT_DEPTH_CAM, TY_INT_WIDTH, 1280);
if (status == TY_STATUS_OK) {
status = TYSetInt(hDevice, TY_COMPONENT_DEPTH_CAM, TY_INT_HEIGHT, 960);
}
if (status != TY_STATUS_OK) {
std::cerr << "[CameraCapture] Camera " << i << " failed to set depth resolution: "
<< status << "(" << TYErrorString(status) << ")" << std::endl;
} else {
std::cout << "[CameraCapture] Camera " << i << " depth resolution set to 1280x960 (via width/height)"
<< std::endl;
}
} else {
std::cout << "[CameraCapture] Camera " << i << " depth resolution set to 1280x960"
<< std::endl;
}
}
// 设置彩色图分辨率为1280x960使用YUYV格式
if (enable_color) {
// 方法1尝试使用YUYV格式的1280x960图像模式推荐
TY_IMAGE_MODE color_mode = TY_IMAGE_MODE_YUYV_1280x960;
TY_STATUS status = TYSetEnum(hDevice, TY_COMPONENT_RGB_CAM, TY_ENUM_IMAGE_MODE, color_mode);
if (status != TY_STATUS_OK) {
// 方法2如果YUYV模式不支持尝试其他格式
std::cerr << "[CameraCapture] Camera " << i << " failed to set YUYV_1280x960 mode, trying alternatives: "
<< status << "(" << TYErrorString(status) << ")" << std::endl;
// 尝试RGB格式
color_mode = TY_IMAGE_MODE_RGB_1280x960;
status = TYSetEnum(hDevice, TY_COMPONENT_RGB_CAM, TY_ENUM_IMAGE_MODE, color_mode);
if (status != TY_STATUS_OK) {
// 方法3如果图像模式都不支持回退到单独设置宽高
std::cerr << "[CameraCapture] Camera " << i << " failed to set RGB_1280x960 mode, trying width/height: "
<< status << "(" << TYErrorString(status) << ")" << std::endl;
status = TYSetInt(hDevice, TY_COMPONENT_RGB_CAM, TY_INT_WIDTH, 1280);
if (status == TY_STATUS_OK) {
status = TYSetInt(hDevice, TY_COMPONENT_RGB_CAM, TY_INT_HEIGHT, 960);
}
if (status != TY_STATUS_OK) {
std::cerr << "[CameraCapture] Camera " << i << " failed to set color resolution: "
<< status << "(" << TYErrorString(status) << ")" << std::endl;
} else {
std::cout << "[CameraCapture] Camera " << i << " color resolution set to 1280x960 (via width/height)"
<< std::endl;
}
} else {
std::cout << "[CameraCapture] Camera " << i << " color resolution set to 1280x960 (RGB format)"
<< std::endl;
}
} else {
std::cout << "[CameraCapture] Camera " << i << " color resolution set to 1280x960 YUYV"
<< std::endl;
}
}
}
// ========== 设置帧率 ==========
// 根据相机技术参数640x480分辨率下
// - 深度图19 fps
// - RGB图YUYV格式25 fps
// 使用连续模式以获得最高帧率
for (size_t i = 0; i < cameras_.size(); i++) {
auto &camera = cameras_[i];
TY_DEV_HANDLE hDevice = camera->handle();
if (hDevice == 0) {
std::cerr << "[CameraCapture] Camera " << i << " handle is invalid, skip frame rate setting"
<< std::endl;
continue;
}
// 方法1使用连续模式TY_TRIGGER_MODE_OFF让相机以最大帧率连续采集
// 640x480分辨率下连续模式应该能达到深度19fpsRGB(YUYV)25fps
TY_TRIGGER_PARAM trigger_param;
trigger_param.mode = TY_TRIGGER_MODE_OFF; // 连续模式,不使用触发
trigger_param.fps = 0; // 连续模式下fps参数无效
trigger_param.rsvd = 0;
TY_STATUS status = TYSetStruct(hDevice, TY_COMPONENT_DEVICE, TY_STRUCT_TRIGGER_PARAM,
&trigger_param, sizeof(trigger_param));
if (status != TY_STATUS_OK) {
std::cerr << "[CameraCapture] Camera " << i << " failed to set trigger mode (continuous): "
<< status << "(" << TYErrorString(status) << ")" << std::endl;
// 方法2如果连续模式不支持尝试使用周期性触发模式
// 根据技术参数640x480下RGB可达25fps深度可达19fps
// 设置为25fps以匹配RGB的最高帧率
trigger_param.mode = TY_TRIGGER_MODE_M_PER; // 主模式,周期性触发
trigger_param.fps = 25; // 设置帧率为25fps匹配RGB YUYV格式的最高帧率
trigger_param.rsvd = 0;
status = TYSetStruct(hDevice, TY_COMPONENT_DEVICE, TY_STRUCT_TRIGGER_PARAM,
&trigger_param, sizeof(trigger_param));
if (status != TY_STATUS_OK) {
std::cerr << "[CameraCapture] Camera " << i << " failed to set trigger mode (25fps): "
<< status << "(" << TYErrorString(status) << ")" << std::endl;
} else {
std::cout << "[CameraCapture] Camera " << i << " frame rate set to 25fps (trigger mode)"
<< std::endl;
}
} else {
std::cout << "[CameraCapture] Camera " << i << " set to continuous mode"
<< std::endl;
}
}
// 标记流已配置
streams_configured_ = true;
// ========== 创建图像缓冲区 ==========
// 为每个相机创建一个独立的图像缓冲区
// 缓冲区用于存储采集线程获取的最新图像,供上层读取
for (size_t i = 0; i < cameras_.size(); i++) {
buffers_.push_back(std::make_shared<ImageBuffer>());
}
std::cout << "[CameraCapture] Initialization complete! Total " << cameras_.size() << " camera(s)"
<< std::endl;
return true;
}
/**
* @brief 启动采集
*
* 此函数完成以下工作:
* 1. 检查相机和流配置状态
* 2. 启动所有相机的数据流
* 3. 为每个相机创建独立的采集线程
*
* @return true 启动成功false 启动失败(无相机或流未配置或启动失败)
*
* @note 如果部分相机启动失败函数返回false但已启动的相机需要手动停止
* @note 每个相机使用独立的线程,避免相互阻塞
*/
bool CameraCapture::start() {
// 检查是否已经在运行
if (running_) {
std::cout << "[CameraCapture] System already running" << std::endl;
return true;
}
// 检查是否有相机
if (cameras_.empty()) {
std::cerr << "[CameraCapture] No cameras to start!" << std::endl;
return false;
}
// 检查流是否已配置必须先调用initialize
if (!streams_configured_) {
std::cerr << "[CameraCapture] Streams not configured!" << std::endl;
return false;
}
// ========== 启动所有相机 ==========
bool all_started = true;
for (size_t i = 0; i < cameras_.size(); i++) {
auto &camera = cameras_[i];
// 调用SDK接口启动相机数据流
TY_STATUS status = camera->start();
if (status == TY_STATUS_OK) {
camera_running_[i] = true; // 标记相机为运行状态
std::cout << "[CameraCapture] Camera " << i << " started" << std::endl;
} else {
camera_running_[i] = false;
std::cerr << "[CameraCapture] Camera " << i << " failed to start" << std::endl;
all_started = false; // 记录有相机启动失败
}
}
// 如果有相机启动失败返回false
if (!all_started) {
return false;
}
// ========== 启动采集线程 ==========
// 设置运行标志,采集线程会检查此标志来决定是否开启
running_ = true;
// 为每个相机创建独立的采集线程
// 线程函数captureThreadFunc
// 参数this指针指向调用 start() 的 CameraCapture
// 对象和相机索引、static_cast类型转换操作符
for (size_t i = 0; i < cameras_.size(); i++) {
capture_threads_.emplace_back(&CameraCapture::captureThreadFunc, this,
static_cast<int>(i));
}
return true;
}
/**
* @brief 停止采集
*
* 此函数完成以下工作:
* 1. 设置运行标志为false通知采集线程退出
* 2. 等待所有采集线程结束join
* 3. 停止所有相机的数据流
*
* @note 此函数是线程安全的,可以在任何线程中调用
* @note 析构函数会自动调用此函数,确保资源正确释放
*/
void CameraCapture::stop() {
// 设置运行标志为false通知所有采集线程退出循环
running_ = false;
// 等待所有采集线程结束
// join()会阻塞直到线程执行完毕,确保线程安全退出
for (auto &t : capture_threads_) {
if (t.joinable()) {
t.join();
}
}
capture_threads_.clear(); // 清空线程列表
// 停止所有相机的数据流
for (size_t i = 0; i < cameras_.size(); i++) {
if (camera_running_[i] && cameras_[i]) {
cameras_[i]->stop(); // 调用SDK接口停止相机
camera_running_[i] = false; // 标记相机为停止状态
}
}
}
/**
* @brief 获取相机数量
*
* @return 当前已初始化的相机数量
*/
int CameraCapture::getCameraCount() const {
return static_cast<int>(cameras_.size());
}
/**
* @brief 获取指定相机的设备ID
*
* @param index 相机索引从0开始
* @return 相机设备ID字符串如果索引无效则返回空字符串
*
* @note 此函数会重新查询设备列表确保返回最新的设备ID
*/
std::string CameraCapture::getCameraId(int index) const {
// 检查索引有效性
if (index < 0 || index >= static_cast<int>(cameras_.size())) {
return "";
}
// 重新查询设备列表获取ID
// 注意这里使用SDK的设备列表而不是内部存储确保ID是最新的
auto &context = TYContext::getInstance();
auto device_list = context.queryDeviceList();
if (!device_list || index >= device_list->devCount()) {
return "";
}
auto device_info = device_list->getDeviceInfo(index);
if (!device_info) {
return "";
}
return std::string(device_info->id());
}
/**
* @brief 获取指定相机的最新图像
*
* 从线程安全的缓冲区中读取最新采集的图像数据
*
* @param camera_index 相机索引从0开始
* @param depth [输出] 深度图CV_16U格式包含原始深度值单位毫米
* @param color [输出] 彩色图BGR格式CV_8UC3类型
* @param fps [输出] 当前帧率(帧/秒)
* @return true 成功获取图像false 索引无效或缓冲区为空
*
* @note 此函数是线程安全的,使用互斥锁保护缓冲区访问
* @note 如果某个图像流未启用或尚未采集到数据对应的Mat将为空
* @note 使用copyTo()复制数据,确保返回的图像数据独立于缓冲区
*/
bool CameraCapture::getLatestImages(int camera_index, cv::Mat &depth,
cv::Mat &color, double &fps) {
// 检查索引有效性
if (camera_index < 0 || camera_index >= static_cast<int>(buffers_.size())) {
return false;
}
auto buffer = buffers_[camera_index];
// 使用互斥锁保护缓冲区访问,确保线程安全
// lock_guard自动管理锁的获取和释放
std::lock_guard<std::mutex> lock(buffer->mtx);
// 复制深度图数据
if (!buffer->depth.empty()) {
buffer->depth.copyTo(depth); // 深拷贝,确保数据独立
} else {
depth = cv::Mat(); // 如果为空返回空Mat
}
// 复制彩色图数据
if (!buffer->color.empty()) {
buffer->color.copyTo(color); // 深拷贝,确保数据独立
} else {
color = cv::Mat(); // 如果为空返回空Mat
}
// 复制FPS值
fps = buffer->fps;
return true;
}
/**
* @brief 检查是否正在运行
*
* @return true 正在运行false 已停止
*/
bool CameraCapture::isRunning() const { return running_; }
/**
* @brief 获取指定相机的深度相机内参
*
* 从图漾相机SDK获取深度相机的内参fx, fy, cx, cy
* 内参存储在相机的标定数据中通过TYGetStruct API获取
*
* @param camera_index 相机索引从0开始
* @param fx [输出] 焦距x像素单位
* @param fy [输出] 焦距y像素单位
* @param cx [输出] 主点x坐标像素单位
* @param cy [输出] 主点y坐标像素单位
* @return true 成功获取内参false 索引无效或获取失败
*
* @note 内参矩阵格式为3x3
* | fx 0 cx |
* | 0 fy cy |
* | 0 0 1 |
* @note 此函数需要在相机初始化后调用initialize之后
*/
bool CameraCapture::getDepthCameraIntrinsics(int camera_index, float& fx, float& fy, float& cx, float& cy) {
// 检查索引有效性
if (camera_index < 0 || camera_index >= static_cast<int>(cameras_.size())) {
std::cerr << "[CameraCapture] Invalid camera index: " << camera_index << std::endl;
return false;
}
// 检查相机是否已打开
auto camera = cameras_[camera_index];
if (!camera) {
std::cerr << "[CameraCapture] Camera " << camera_index << " is not opened" << std::endl;
return false;
}
// 获取相机设备句柄
TY_DEV_HANDLE hDevice = camera->handle();
if (hDevice == 0) {
std::cerr << "[CameraCapture] Camera " << camera_index << " handle is invalid" << std::endl;
return false;
}
// 获取深度相机的内参
TY_CAMERA_INTRINSIC intrinsic;
TY_STATUS status = TYGetStruct(hDevice, TY_COMPONENT_DEPTH_CAM, TY_STRUCT_CAM_INTRINSIC,
&intrinsic, sizeof(intrinsic));
if (status != TY_STATUS_OK) {
std::cerr << "[CameraCapture] Failed to get depth camera intrinsics for camera "
<< camera_index << ", error: " << status << "(" << TYErrorString(status) << ")" << std::endl;
return false;
}
// 内参矩阵是3x3按行主序存储
// data[0] = fx, data[1] = 0, data[2] = cx
// data[3] = 0, data[4] = fy, data[5] = cy
// data[6] = 0, data[7] = 0, data[8] = 1
fx = intrinsic.data[0]; // fx
fy = intrinsic.data[4]; // fy
cx = intrinsic.data[2]; // cx
cy = intrinsic.data[5]; // cy
std::cout << "[CameraCapture] Camera " << camera_index
<< " depth intrinsics: fx=" << fx << ", fy=" << fy
<< ", cx=" << cx << ", cy=" << cy << std::endl;
return true;
}
/**
* @brief 采集线程函数
*
* 这是每个相机独立运行的采集线程的主函数
* 主要工作:
* 1. 从SDK获取原始帧数据TYFrame
* 2. 提取深度图和彩色图TYImage
* 3. 使用图像处理器处理原始数据(如果需要)
* 4. 转换为OpenCV格式cv::Mat
* 5. 进行颜色空间转换统一为BGR
* 6. 计算帧率
* 7. 更新线程安全的缓冲区
*
* @param camera_index 相机索引,标识此线程负责哪个相机
*
* @note 此函数运行在独立的线程中,每个相机一个线程
* @note 使用异常处理确保线程异常不会导致程序崩溃
* @note 使用超时机制避免长时间阻塞
* @note 缓冲区更新使用互斥锁保护,确保线程安全
*/
void CameraCapture::captureThreadFunc(int camera_index) {
// 检查相机索引有效性
if (camera_index < 0 || camera_index >= static_cast<int>(cameras_.size())) {
return;
}
// 获取此相机对应的缓冲区
auto buffer = buffers_[camera_index];
if (!buffer) {
std::cerr << "[CameraCapture] Camera " << camera_index << " buffer invalid"
<< std::endl;
return;
}
// 初始化帧计数和FPS计算相关变量
int frame_count = 0;
auto start_time =
std::chrono::steady_clock::now(); // 记录开始时间用于计算FPS
int consecutive_timeouts = 0; // 连续超时计数,用于检测相机是否异常
// 使用try-catch捕获异常确保线程异常不会导致程序崩溃
try {
// 主循环:持续采集图像直到停止标志被设置或相机停止运行
while (running_ && camera_running_[camera_index]) {
// 再次检查相机索引有效性(防止在运行过程中相机被移除)
if (camera_index >= static_cast<int>(cameras_.size()) ||
!cameras_[camera_index]) {
break;
}
// 从相机获取帧数据超时时间500ms
// tryGetFrames是非阻塞的如果500ms内没有新帧返回nullptr
// 注意根据实际测试单帧处理时间约155ms加上相机采集时间500ms是合理的超时值
// 如果相机帧率很低(<2fps可以适当增加到1000ms
auto frame = cameras_[camera_index]->tryGetFrames(500);
if (!frame) {
// 获取帧失败(超时或错误)
consecutive_timeouts++;
// 如果连续超时超过10次输出警告并短暂休眠
// 这样可以减少错误日志的噪音同时避免CPU占用过高
if (consecutive_timeouts == 10) {
std::cerr << "[CameraCapture] Camera " << camera_index
<< " consecutive timeout 10 times, may be low frame rate or connection issue" << std::endl;
}
if (consecutive_timeouts > 10) {
// 连续超时超过10次后每次超时都休眠100ms避免CPU空转
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
continue; // 继续下一次循环
}
// 成功获取帧,重置超时计数
consecutive_timeouts = 0;
frame_count++; // 帧计数加1
// 记录帧获取时间,用于性能分析
auto frame_start_time = std::chrono::steady_clock::now();
// ========== 处理深度图 ==========
cv::Mat depthMat;
auto depth_img = frame->depthImage(); // 从帧中提取深度图
if (depth_img) {
// 如果深度流已启用,使用图像处理器处理原始数据
auto depth_processer = depth_processers_[camera_index];
if (depth_processer) {
// parse()处理原始TYImage数据可能进行格式转换或校正
depth_processer->parse(depth_img);
// image()返回处理后的TYImage
depth_img = depth_processer->image();
}
// 将TYImage转换为OpenCV的Mat格式
// TYImageToMat内部使用clone(),确保数据安全
depthMat = TYImageToMat(depth_img);
}
// ========== 处理彩色图 ==========
cv::Mat colorMat;
auto color_img = frame->colorImage(); // 从帧中提取彩色图
if (color_img) {
// 如果彩色流已启用,使用图像处理器处理原始数据
auto color_processer = color_processers_[camera_index];
if (color_processer) {
// parse()处理原始TYImage数据
color_processer->parse(color_img);
// image()返回处理后的TYImage
color_img = color_processer->image();
}
// 将TYImage转换为OpenCV的Mat格式
cv::Mat rawColorMat = TYImageToMat(color_img);
if (!rawColorMat.empty()) {
// 获取像素格式标识,用于确定颜色空间转换方式
int pixel_format = getPixelFormatId(color_img->pixelFormat());
// 性能优化根据像素格式进行颜色空间转换统一输出BGR格式
// 优化直接转换到目标Mat避免中间变量
if (pixel_format == 1) {
// RGB格式转换为BGR
cv::cvtColor(rawColorMat, colorMat, cv::COLOR_RGB2BGR);
} else if (pixel_format == 2 || pixel_format == 3) {
// YUYV或YVYU格式YUV422转换为BGR
// 注意YVYU和YUYV使用相同的转换代码
cv::cvtColor(rawColorMat, colorMat, cv::COLOR_YUV2BGR_YUYV);
} else {
// BGR格式或其他直接使用假设已经是BGR
// 性能优化使用move语义避免不必要的复制
colorMat = std::move(rawColorMat);
}
}
}
// ========== 计算FPS ==========
// 使用已采集的帧数和经过的时间计算平均帧率
auto current_time = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
current_time - start_time)
.count();
double fps = 0.0;
if (elapsed > 0) {
// FPS = 帧数 * 1000 / 经过的毫秒数
fps = (frame_count * 1000.0) / elapsed;
}
// ========== 更新缓冲区 ==========
// 使用互斥锁保护缓冲区,确保线程安全
// 注意这里使用独立的lock_guard作用域确保锁在更新完成后立即释放
{
std::lock_guard<std::mutex> lock(buffer->mtx);
// 更新深度图缓冲区
// 性能优化使用swap代替copyTo避免数据复制直接交换指针
if (!depthMat.empty()) {
// 使用swap交换数据这是零拷贝操作只交换内部指针
std::swap(buffer->depth, depthMat);
} else {
// 如果深度图为空,清空缓冲区
buffer->depth = cv::Mat();
}
// 更新彩色图缓冲区
// 性能优化使用swap代替copyTo避免数据复制
if (!colorMat.empty()) {
// 使用swap交换数据这是零拷贝操作只交换内部指针
std::swap(buffer->color, colorMat);
} else {
// 如果彩色图为空,清空缓冲区
buffer->color = cv::Mat();
}
// 更新FPS和帧计数
buffer->fps = fps;
buffer->frame_count = frame_count;
buffer->updated = true; // 标记缓冲区已更新
}
// lock_guard在这里自动释放锁
// 性能监控每100帧输出一次处理时间可选用于调试
if (frame_count % 100 == 0) {
auto frame_end_time = std::chrono::steady_clock::now();
auto frame_process_time =
std::chrono::duration_cast<std::chrono::milliseconds>(
frame_end_time - frame_start_time)
.count();
// if (frame_process_time > 50) { // 如果单帧处理时间超过50ms输出警告
// std::cout << "[CameraCapture] 相机 " << camera_index
// << " 单帧处理时间: " << frame_process_time << "ms"
// << std::endl;
// }
}
} // while循环结束
} catch (const std::exception &e) {
// 捕获标准异常,记录错误信息
std::cerr << "[CameraCapture] Camera " << camera_index << " capture thread exception: "
<< e.what() << std::endl;
} catch (...) {
// 捕获所有其他异常(非标准异常)
std::cerr << "[CameraCapture] Camera " << camera_index
<< " capture thread unknown exception" << std::endl;
}
// 线程函数结束,线程自动退出
}
/**
* @brief 将TYImage转换为OpenCV的Mat格式
*
* 此函数将SDK的TYImage格式转换为OpenCV的cv::Mat格式
* 关键点:
* 1. 根据像素格式确定OpenCV的Mat类型
* 2. 创建临时Mat包装原始缓冲区零拷贝视图
* 3. 使用clone()创建数据副本,确保数据安全
*
* @param img SDK的TYImage智能指针
* @return cv::Mat OpenCV格式的图像矩阵如果输入无效则返回空Mat
*
* @note 使用clone()创建数据副本是必要的,因为:
* - TYImage的数据可能在frame对象销毁后失效
* - 如果不clone返回的Mat会引用已释放的内存导致悬空指针
* - clone()虽然增加内存和CPU开销但确保了数据安全
*
* @note 支持的像素格式:
* - TY_PIXEL_FORMAT_DEPTH16: 16位深度图 -> CV_16U
* - TY_PIXEL_FORMAT_RGB: RGB彩色图 -> CV_8UC3
* - TY_PIXEL_FORMAT_BGR: BGR彩色图 -> CV_8UC3
* - TY_PIXEL_FORMAT_MONO: 单色图 -> CV_8U
* - TY_PIXEL_FORMAT_YUYV/YVYU: YUV422格式 -> CV_8UC2
*/
cv::Mat CameraCapture::TYImageToMat(const std::shared_ptr<TYImage> &img) {
// 检查输入有效性
if (!img || !img->buffer())
return cv::Mat();
// 根据SDK的像素格式确定OpenCV的Mat数据类型
int type = -1;
switch (img->pixelFormat()) {
case TY_PIXEL_FORMAT_DEPTH16:
type = CV_16U; // 16位无符号整数用于深度值
break;
case TY_PIXEL_FORMAT_RGB:
type = CV_8UC3; // 8位无符号整数3通道RGB
break;
case TY_PIXEL_FORMAT_MONO:
type = CV_8U; // 8位无符号整数单通道灰度
break;
case TY_PIXEL_FORMAT_YVYU:
case TY_PIXEL_FORMAT_YUYV:
type = CV_8UC2; // 8位无符号整数2通道YUV422
break;
case TY_PIXEL_FORMAT_BGR:
type = CV_8UC3; // 8位无符号整数3通道BGR
break;
default:
type = CV_8U; // 默认单通道
break;
}
// 创建临时Mat对象直接包装原始缓冲区零拷贝
// 注意:这只是创建一个视图,不复制数据
// 参数:高度、宽度、数据类型、原始数据指针
cv::Mat tempMat(img->height(), img->width(), type, img->buffer());
// 使用clone()创建数据副本
// 这是关键步骤确保返回的Mat拥有独立的数据副本
// 即使原始的TYImage被销毁返回的Mat仍然有效
// 虽然会增加内存和CPU开销但这是确保数据安全的必要代价
return tempMat.clone();
}
/**
* @brief 获取像素格式标识
*
* 将SDK的像素格式枚举转换为简单的整数标识
* 用于后续的颜色空间转换判断
*
* @param pixel_format SDK的像素格式枚举值
* @return 像素格式标识:
* - 0: BGR格式或默认
* - 1: RGB格式
* - 2: YUYV格式YUV422
* - 3: YVYU格式YUV422
*
* @note 此函数用于简化颜色空间转换的判断逻辑
*/
int CameraCapture::getPixelFormatId(TY_PIXEL_FORMAT pixel_format) {
switch (pixel_format) {
case TY_PIXEL_FORMAT_RGB:
return 1; // RGB格式
case TY_PIXEL_FORMAT_YUYV:
return 2; // YUYV格式YUV422
case TY_PIXEL_FORMAT_YVYU:
return 3; // YVYU格式YUV422
case TY_PIXEL_FORMAT_BGR:
default:
return 0; // BGR格式或默认
}
}
/**
* @brief 利用SDK生成点云
* @param camera_index 相机索引
* @param depth_img 深度图
* @param out_points 输出点云
* @return 是否成功
*/
bool CameraCapture::computePointCloud(int camera_index, const cv::Mat& depth_img, std::vector<Point3D>& out_points) {
if (camera_index < 0 || camera_index >= static_cast<int>(cameras_.size())) {
return false;
}
if (!has_calib_info_[camera_index]) {
std::cerr << "[CameraCapture] No calibration info for camera " << camera_index << std::endl;
return false;
}
if (depth_img.empty()) {
return false;
}
// Check for valid intrinsics to prevent division by zero crash
float fx = calib_infos_[camera_index].intrinsic.data[0];
float fy = calib_infos_[camera_index].intrinsic.data[4];
if (std::abs(fx) < 1e-6 || std::abs(fy) < 1e-6) {
std::cerr << "[CameraCapture] Invalid intrinsics for camera " << camera_index
<< " (fx=" << fx << ", fy=" << fy << "). Cannot compute point cloud." << std::endl;
return false;
}
// 调整输出容器大小
out_points.resize(depth_img.cols * depth_img.rows);
// TY_VECT_3F {float x, y, z} 与 Point3D {float x, y, z} 内存布局兼容
// 直接使用 SDK 函数生成点云
TY_VECT_3F* p3d = reinterpret_cast<TY_VECT_3F*>(out_points.data());
TY_STATUS status = TYMapDepthImageToPoint3d(&calib_infos_[camera_index],
depth_img.cols, depth_img.rows,
(const uint16_t*)depth_img.data,
p3d);
if (status != TY_STATUS_OK) {
std::cerr << "[CameraCapture] TYMapDepthImageToPoint3d failed: " << status << std::endl;
return false;
}
return true;
}

View File

@@ -0,0 +1,159 @@
#pragma once
#include "Frame.hpp"
#include "Device.hpp"
#include "TYApi.h"
#include <opencv2/opencv.hpp>
#include <vector>
#include <memory>
#include <thread>
#include <atomic>
#include <mutex>
#include <string>
#include "../common_types.h"
using namespace percipio_layer;
/**
* @brief CameraCapture
* 图像采集层负责从SDK获取图像并转换为OpenCV格式
*
* 功能说明:
* - 封装TY相机SDK管理多相机采集
* - 将SDK的TYImage转换为OpenCV的cv::Mat格式
* - 管理采集线程和缓冲区
* - 输出原始cv::Mat格式的图像供上层使用
*
* 设计原则:
* - 此模块属于图像采集层可以依赖SDK
* - 输出标准OpenCV格式实现SDK与算法层的隔离
* - 不进行图像处理(如伪彩色映射等),只负责采集和格式转换
*/
class CameraCapture
{
public:
/**
* @brief 图像缓冲区结构
* 存储原始采集的图像数据cv::Mat格式
*/
struct ImageBuffer {
cv::Mat depth; // 原始深度图CV_16U格式
cv::Mat color; // 原始彩色图BGR格式
double fps = 0.0; // 当前帧率
int frame_count = 0; // 帧计数
std::mutex mtx; // 互斥锁
std::atomic<bool> updated{false}; // 更新标志、std::atomic 确保在多线程环境中对 updated 的操作是原子的,不会发生竞争条件。
};
CameraCapture();
~CameraCapture();
/**
* 初始化并配置相机
* @param enable_depth 是否启用深度流
* @param enable_color 是否启用彩色流
* @return 是否成功
*/
bool initialize(bool enable_depth = true, bool enable_color = true);
/**
* 启动采集
* @return 是否成功
*/
bool start();
/**
* 停止采集
*/
void stop();
/**
* 获取相机数量
* @return 相机数量
*/
int getCameraCount() const;
/**
* 获取相机ID
* @param index 相机索引
* @return 相机ID字符串
*/
std::string getCameraId(int index) const;
/**
* 获取指定相机的最新图像
* @param camera_index 相机索引
* @param depth 输出的深度图CV_16U格式原始深度值
* @param color 输出的彩色图BGR格式
* @param fps 输出的帧率
* @return 是否成功获取到图像
*/
bool getLatestImages(int camera_index, cv::Mat& depth, cv::Mat& color, double& fps);
/**
* 检查是否正在运行
* @return 是否运行中
*/
bool isRunning() const;
/**
* 获取指定相机的深度相机内参
* @param camera_index 相机索引
* @param fx [输出] 焦距x
* @param fy [输出] 焦距y
* @param cx [输出] 主点x
* @param cy [输出] 主点y
* @return 是否成功获取内参
*/
// Added method for depth camera intrinsics
bool getDepthCameraIntrinsics(int camera_index, float& fx, float& fy, float& cx, float& cy);
/**
* @brief 利用SDK生成点云
* @param camera_index 相机索引
* @param depth_img 深度图
* @param out_points 输出点云
* @return 是否成功
*/
bool computePointCloud(int camera_index, const cv::Mat& depth_img, std::vector<Point3D>& out_points);
private:
/**
* 采集线程函数
* @param camera_index 相机索引
*/
void captureThreadFunc(int camera_index);
/**
* 将TYImage转换为OpenCV的Mat格式
* @param img 输入的TYImage智能指针
* @return cv::Mat OpenCV格式的图像矩阵
*/
static cv::Mat TYImageToMat(const std::shared_ptr<TYImage> &img);
/**
* 获取像素格式标识,用于颜色空间转换
* @param pixel_format SDK的像素格式枚举
* @return 像素格式标识0: BGR, 1: RGB, 2: YUYV, 3: YVYU
*/
static int getPixelFormatId(TY_PIXEL_FORMAT pixel_format);
// SDK相关成员原MultiCameraCapture的功能
std::vector<std::shared_ptr<FastCamera>> cameras_; // 相机对象列表
std::vector<std::shared_ptr<ImageProcesser>> depth_processers_; // 深度图像处理器
std::vector<std::shared_ptr<ImageProcesser>> color_processers_; // 彩色图像处理器
std::vector<bool> camera_running_; // 相机运行状态
bool streams_configured_; // 流是否已配置
bool depth_enabled_; // 是否启用深度流
bool color_enabled_; // 是否启用彩色流
// 采集线程和缓冲区
std::vector<std::shared_ptr<ImageBuffer>> buffers_; // 图像缓冲区
std::vector<std::thread> capture_threads_; // 采集线程
std::atomic<bool> running_; // 运行标志
// 标定信息
std::vector<TY_CAMERA_CALIB_INFO> calib_infos_;
std::vector<bool> has_calib_info_;
};

View File

@@ -0,0 +1,734 @@
#include "config_manager.h"
#include <fstream>
#include <iostream>
#include <sstream>
ConfigManager &ConfigManager::getInstance() {
static ConfigManager instance;
return instance;
}
ConfigManager::ConfigManager() {
// 默认配置
config_json_ = json11::Json::object{};
}
bool ConfigManager::loadConfig(const std::string &config_path) {
std::lock_guard<std::mutex> lock(mutex_);
std::ifstream file(config_path);
if (!file.is_open()) {
std::cerr << "ConfigManager: Failed to open config file: " << config_path
<< std::endl;
return false;
}
std::stringstream buffer;
buffer << file.rdbuf();
std::string content = buffer.str();
std::string err;
config_json_ = json11::Json::parse(content, err);
if (!err.empty()) {
std::cerr << "ConfigManager: Failed to parse JSON: " << err << std::endl;
return false;
}
std::cout << "ConfigManager: Successfully loaded config from " << config_path
<< std::endl;
return true;
}
bool ConfigManager::saveConfig(const std::string &config_path) {
std::lock_guard<std::mutex> lock(mutex_);
std::string json_str = config_json_.dump();
std::ofstream file(config_path);
if (!file.is_open()) {
std::cerr << "ConfigManager: Failed to open config file for writing: " << config_path
<< std::endl;
return false;
}
file << json_str;
file.close();
std::cout << "ConfigManager: Successfully saved config to " << config_path << std::endl;
return true;
}
// --- Accessors & Setters ---
std::string ConfigManager::getRedisHost() const {
std::lock_guard<std::mutex> lock(mutex_);
if (config_json_["redis"].is_object()) {
return config_json_["redis"]["host"].string_value();
}
return "127.0.0.1"; // Default
}
int ConfigManager::getRedisPort() const {
std::lock_guard<std::mutex> lock(mutex_);
if (config_json_["redis"].is_object()) {
int port = config_json_["redis"]["port"].int_value();
return port > 0 ? port : 6379;
}
return 6379;
}
int ConfigManager::getRedisDb() const {
std::lock_guard<std::mutex> lock(mutex_);
if (config_json_["redis"].is_object()) {
return config_json_["redis"]["db"].int_value();
}
return 0;
}
bool ConfigManager::isDepthEnabled() const {
std::lock_guard<std::mutex> lock(mutex_);
if (config_json_["cameras"].is_object()) {
auto val = config_json_["cameras"]["depth_enabled"];
if (val.is_bool())
return val.bool_value();
}
return true; // Default
}
bool ConfigManager::isColorEnabled() const {
std::lock_guard<std::mutex> lock(mutex_);
if (config_json_["cameras"].is_object()) {
auto val = config_json_["cameras"]["color_enabled"];
if (val.is_bool())
return val.bool_value();
}
return true; // Default
}
std::vector<ConfigManager::CameraMapping>
ConfigManager::getCameraMappings() const {
std::lock_guard<std::mutex> lock(mutex_);
std::vector<CameraMapping> mappings;
if (config_json_["cameras"].is_object() &&
config_json_["cameras"]["mapping"].is_array()) {
for (const auto &item : config_json_["cameras"]["mapping"].array_items()) {
CameraMapping m;
m.id = item["id"].string_value();
m.index = item["index"].int_value();
mappings.push_back(m);
}
}
return mappings;
}
std::string ConfigManager::getSavePath() const {
std::lock_guard<std::mutex> lock(mutex_);
if (config_json_["vision"].is_object()) {
return config_json_["vision"]["save_path"].string_value();
}
return "./";
}
int ConfigManager::getLogLevel() const {
std::lock_guard<std::mutex> lock(mutex_);
if (config_json_["vision"].is_object()) {
return config_json_["vision"]["log_level"].int_value();
}
return 0;
}
// --- Algorithm Config Accessors ---
// Beam/Rack Deflection - ROI Points
std::vector<cv::Point2i> ConfigManager::getBeamROIPoints() const {
std::lock_guard<std::mutex> lock(mutex_);
std::vector<cv::Point2i> points;
if (config_json_["algorithms"].is_object() &&
config_json_["algorithms"]["beam_rack_deflection"].is_object() &&
config_json_["algorithms"]["beam_rack_deflection"]["beam_roi_points"].is_array()) {
const auto& points_array = config_json_["algorithms"]["beam_rack_deflection"]["beam_roi_points"].array_items();
for (const auto& point : points_array) {
if (point.is_object()) {
int x = point["x"].int_value();
int y = point["y"].int_value();
points.push_back(cv::Point2i(x, y));
}
}
}
// Default values if not configured
if (points.empty()) {
points = {
cv::Point2i(100, 50),
cv::Point2i(540, 80),
cv::Point2i(540, 280),
cv::Point2i(100, 280)
};
}
return points;
}
std::vector<cv::Point2i> ConfigManager::getRackROIPoints() const {
std::lock_guard<std::mutex> lock(mutex_);
std::vector<cv::Point2i> points;
if (config_json_["algorithms"].is_object() &&
config_json_["algorithms"]["beam_rack_deflection"].is_object() &&
config_json_["algorithms"]["beam_rack_deflection"]["rack_roi_points"].is_array()) {
const auto& points_array = config_json_["algorithms"]["beam_rack_deflection"]["rack_roi_points"].array_items();
for (const auto& point : points_array) {
if (point.is_object()) {
int x = point["x"].int_value();
int y = point["y"].int_value();
points.push_back(cv::Point2i(x, y));
}
}
}
// Default values if not configured
if (points.empty()) {
points = {
cv::Point2i(50, 50),
cv::Point2i(150, 50),
cv::Point2i(150, 430),
cv::Point2i(50, 430)
};
}
return points;
}
// Beam/Rack Deflection - Thresholds
std::vector<float> ConfigManager::getBeamThresholds() const {
std::lock_guard<std::mutex> lock(mutex_);
std::vector<float> thresholds;
if (config_json_["algorithms"].is_object() &&
config_json_["algorithms"]["beam_rack_deflection"].is_object() &&
config_json_["algorithms"]["beam_rack_deflection"]["beam_thresholds"].is_object()) {
const auto& thresh = config_json_["algorithms"]["beam_rack_deflection"]["beam_thresholds"];
thresholds.push_back(static_cast<float>(thresh["A"].number_value()));
thresholds.push_back(static_cast<float>(thresh["B"].number_value()));
thresholds.push_back(static_cast<float>(thresh["C"].number_value()));
thresholds.push_back(static_cast<float>(thresh["D"].number_value()));
}
// Default values if not configured
if (thresholds.size() != 4) {
thresholds = {-10.0f, -5.0f, 5.0f, 10.0f};
}
return thresholds;
}
std::vector<float> ConfigManager::getRackThresholds() const {
std::lock_guard<std::mutex> lock(mutex_);
std::vector<float> thresholds;
if (config_json_["algorithms"].is_object() &&
config_json_["algorithms"]["beam_rack_deflection"].is_object() &&
config_json_["algorithms"]["beam_rack_deflection"]["rack_thresholds"].is_object()) {
const auto& thresh = config_json_["algorithms"]["beam_rack_deflection"]["rack_thresholds"];
thresholds.push_back(static_cast<float>(thresh["A"].number_value()));
thresholds.push_back(static_cast<float>(thresh["B"].number_value()));
thresholds.push_back(static_cast<float>(thresh["C"].number_value()));
thresholds.push_back(static_cast<float>(thresh["D"].number_value()));
}
// Default values if not configured
if (thresholds.size() != 4) {
thresholds = {-6.0f, -3.0f, 3.0f, 6.0f};
}
return thresholds;
}
// Pallet Offset - Thresholds
std::vector<float> ConfigManager::getPalletOffsetLatThresholds() const {
std::lock_guard<std::mutex> lock(mutex_);
std::vector<float> thresholds;
if (config_json_["algorithms"].is_object() &&
config_json_["algorithms"]["pallet_offset"].is_object() &&
config_json_["algorithms"]["pallet_offset"]["offset_lat_mm_thresholds"].is_object()) {
const auto& thresh = config_json_["algorithms"]["pallet_offset"]["offset_lat_mm_thresholds"];
thresholds.push_back(static_cast<float>(thresh["A"].number_value()));
thresholds.push_back(static_cast<float>(thresh["B"].number_value()));
thresholds.push_back(static_cast<float>(thresh["C"].number_value()));
thresholds.push_back(static_cast<float>(thresh["D"].number_value()));
}
if (thresholds.size() != 4) {
thresholds = {-20.0f, -10.0f, 10.0f, 20.0f};
}
return thresholds;
}
std::vector<float> ConfigManager::getPalletOffsetLonThresholds() const {
std::lock_guard<std::mutex> lock(mutex_);
std::vector<float> thresholds;
if (config_json_["algorithms"].is_object() &&
config_json_["algorithms"]["pallet_offset"].is_object() &&
config_json_["algorithms"]["pallet_offset"]["offset_lon_mm_thresholds"].is_object()) {
const auto& thresh = config_json_["algorithms"]["pallet_offset"]["offset_lon_mm_thresholds"];
thresholds.push_back(static_cast<float>(thresh["A"].number_value()));
thresholds.push_back(static_cast<float>(thresh["B"].number_value()));
thresholds.push_back(static_cast<float>(thresh["C"].number_value()));
thresholds.push_back(static_cast<float>(thresh["D"].number_value()));
}
if (thresholds.size() != 4) {
thresholds = {-20.0f, -10.0f, 10.0f, 20.0f};
}
return thresholds;
}
std::vector<float> ConfigManager::getPalletRotationAngleThresholds() const {
std::lock_guard<std::mutex> lock(mutex_);
std::vector<float> thresholds;
if (config_json_["algorithms"].is_object() &&
config_json_["algorithms"]["pallet_offset"].is_object() &&
config_json_["algorithms"]["pallet_offset"]["rotation_angle_thresholds"].is_object()) {
const auto& thresh = config_json_["algorithms"]["pallet_offset"]["rotation_angle_thresholds"];
thresholds.push_back(static_cast<float>(thresh["A"].number_value()));
thresholds.push_back(static_cast<float>(thresh["B"].number_value()));
thresholds.push_back(static_cast<float>(thresh["C"].number_value()));
thresholds.push_back(static_cast<float>(thresh["D"].number_value()));
}
if (thresholds.size() != 4) {
thresholds = {-5.0f, -2.5f, 2.5f, 5.0f};
}
return thresholds;
}
std::vector<float> ConfigManager::getPalletHoleDefLeftThresholds() const {
std::lock_guard<std::mutex> lock(mutex_);
std::vector<float> thresholds;
if (config_json_["algorithms"].is_object() &&
config_json_["algorithms"]["pallet_offset"].is_object() &&
config_json_["algorithms"]["pallet_offset"]["hole_def_mm_left_thresholds"].is_object()) {
const auto& thresh = config_json_["algorithms"]["pallet_offset"]["hole_def_mm_left_thresholds"];
thresholds.push_back(static_cast<float>(thresh["A"].number_value()));
thresholds.push_back(static_cast<float>(thresh["B"].number_value()));
thresholds.push_back(static_cast<float>(thresh["C"].number_value()));
thresholds.push_back(static_cast<float>(thresh["D"].number_value()));
}
if (thresholds.size() != 4) {
thresholds = {-8.0f, -4.0f, 4.0f, 8.0f};
}
return thresholds;
}
std::vector<float> ConfigManager::getPalletHoleDefRightThresholds() const {
std::lock_guard<std::mutex> lock(mutex_);
std::vector<float> thresholds;
if (config_json_["algorithms"].is_object() &&
config_json_["algorithms"]["pallet_offset"].is_object() &&
config_json_["algorithms"]["pallet_offset"]["hole_def_mm_right_thresholds"].is_object()) {
const auto& thresh = config_json_["algorithms"]["pallet_offset"]["hole_def_mm_right_thresholds"];
thresholds.push_back(static_cast<float>(thresh["A"].number_value()));
thresholds.push_back(static_cast<float>(thresh["B"].number_value()));
thresholds.push_back(static_cast<float>(thresh["C"].number_value()));
thresholds.push_back(static_cast<float>(thresh["D"].number_value()));
}
if (thresholds.size() != 4) {
thresholds = {-8.0f, -4.0f, 4.0f, 8.0f};
}
return thresholds;
}
// Slot Occupancy
float ConfigManager::getSlotOccupancyDepthThreshold() const {
std::lock_guard<std::mutex> lock(mutex_);
if (config_json_["algorithms"].is_object() &&
config_json_["algorithms"]["slot_occupancy"].is_object()) {
return static_cast<float>(config_json_["algorithms"]["slot_occupancy"]["depth_threshold_mm"].number_value());
}
return 100.0f; // Default
}
float ConfigManager::getSlotOccupancyConfidenceThreshold() const {
std::lock_guard<std::mutex> lock(mutex_);
if (config_json_["algorithms"].is_object() &&
config_json_["algorithms"]["slot_occupancy"].is_object()) {
return static_cast<float>(config_json_["algorithms"]["slot_occupancy"]["confidence_threshold"].number_value());
}
return 0.8f; // Default
}
// Visual Inventory
float ConfigManager::getVisualInventoryBarcodeConfidence() const {
std::lock_guard<std::mutex> lock(mutex_);
if (config_json_["algorithms"].is_object() &&
config_json_["algorithms"]["visual_inventory"].is_object()) {
return static_cast<float>(config_json_["algorithms"]["visual_inventory"]["barcode_confidence_threshold"].number_value());
}
return 0.7f; // Default
}
bool ConfigManager::getVisualInventoryROIEnabled() const {
std::lock_guard<std::mutex> lock(mutex_);
if (config_json_["algorithms"].is_object() &&
config_json_["algorithms"]["visual_inventory"].is_object()) {
return config_json_["algorithms"]["visual_inventory"]["roi_enabled"].bool_value();
}
return true; // Default
}
// General Algorithm Parameters
float ConfigManager::getAlgorithmMinDepth() const {
std::lock_guard<std::mutex> lock(mutex_);
if (config_json_["algorithms"].is_object() &&
config_json_["algorithms"]["general"].is_object()) {
return static_cast<float>(config_json_["algorithms"]["general"]["min_depth_mm"].number_value());
}
return 800.0f; // Default
}
float ConfigManager::getAlgorithmMaxDepth() const {
std::lock_guard<std::mutex> lock(mutex_);
if (config_json_["algorithms"].is_object() &&
config_json_["algorithms"]["general"].is_object()) {
return static_cast<float>(config_json_["algorithms"]["general"]["max_depth_mm"].number_value());
}
return 3000.0f; // Default
}
int ConfigManager::getAlgorithmSamplePoints() const {
std::lock_guard<std::mutex> lock(mutex_);
if (config_json_["algorithms"].is_object() &&
config_json_["algorithms"]["general"].is_object()) {
return config_json_["algorithms"]["general"]["sample_points"].int_value();
}
return 50; // Default
}
// Beam/Rack Deflection - Setters
void ConfigManager::setBeamROIPoints(const std::vector<cv::Point2i>& points) {
std::lock_guard<std::mutex> lock(mutex_);
// Note: json11 is immutable, we need to reconstruct the object or use a mutable json library.
// For this project using json11, we have to rebuild the part of the json tree.
// This is a bit expensive but config saving is rare.
// Actually, to make it easier with json11, we might need to parse, modify and dump if we want to keep comments?
// But json11 parser doesn't keep comments.
// Let's just modify the internal map if possible or rebuild.
// json11::Json is const. We need to cast it away or rebuild the whole structure?
// Rebuilding is safer.
// Implementation Note: Since json11 is immutable, proper way is to create new Json objects.
// For simplicity in this context, we will use a "deep update" strategy helper if we had one.
// But here we need to do it manually.
// Let's cheat a bit and use const_cast for the "value" if it was a simpler lib, but json11 uses shared_ptr...
// Okay, we will use a temporary mutable map approach for the 'algorithms' section.
// Helper to get mutable map from Json object
auto get_mutable_map = [](const json11::Json& j) -> json11::Json::object {
return j.object_items();
};
json11::Json::object root_map = get_mutable_map(config_json_);
json11::Json::object algo_map = get_mutable_map(root_map["algorithms"]);
json11::Json::object beam_rack_map = get_mutable_map(algo_map["beam_rack_deflection"]);
std::vector<json11::Json> points_json;
for(const auto& p : points) {
points_json.push_back(json11::Json::object{{"x", p.x}, {"y", p.y}});
}
beam_rack_map["beam_roi_points"] = points_json;
algo_map["beam_rack_deflection"] = beam_rack_map;
root_map["algorithms"] = algo_map;
config_json_ = root_map;
}
void ConfigManager::setRackROIPoints(const std::vector<cv::Point2i>& points) {
std::lock_guard<std::mutex> lock(mutex_);
json11::Json::object root_map = config_json_.object_items();
json11::Json::object algo_map = root_map["algorithms"].object_items();
json11::Json::object beam_rack_map = algo_map["beam_rack_deflection"].object_items();
std::vector<json11::Json> points_json;
for(const auto& p : points) {
points_json.push_back(json11::Json::object{{"x", p.x}, {"y", p.y}});
}
beam_rack_map["rack_roi_points"] = points_json;
algo_map["beam_rack_deflection"] = beam_rack_map;
root_map["algorithms"] = algo_map;
config_json_ = root_map;
}
void ConfigManager::setBeamThresholds(const std::vector<float>& thresholds) {
if(thresholds.size() < 4) return;
std::lock_guard<std::mutex> lock(mutex_);
json11::Json::object root_map = config_json_.object_items();
json11::Json::object algo_map = root_map["algorithms"].object_items();
json11::Json::object beam_rack_map = algo_map["beam_rack_deflection"].object_items();
beam_rack_map["beam_thresholds"] = json11::Json::object{
{"A", thresholds[0]}, {"B", thresholds[1]}, {"C", thresholds[2]}, {"D", thresholds[3]}
};
algo_map["beam_rack_deflection"] = beam_rack_map;
root_map["algorithms"] = algo_map;
config_json_ = root_map;
}
void ConfigManager::setRackThresholds(const std::vector<float>& thresholds) {
if(thresholds.size() < 4) return;
std::lock_guard<std::mutex> lock(mutex_);
json11::Json::object root_map = config_json_.object_items();
json11::Json::object algo_map = root_map["algorithms"].object_items();
json11::Json::object beam_rack_map = algo_map["beam_rack_deflection"].object_items();
beam_rack_map["rack_thresholds"] = json11::Json::object{
{"A", thresholds[0]}, {"B", thresholds[1]}, {"C", thresholds[2]}, {"D", thresholds[3]}
};
algo_map["beam_rack_deflection"] = beam_rack_map;
root_map["algorithms"] = algo_map;
config_json_ = root_map;
}
// Pallet Offset Setters
void ConfigManager::setPalletOffsetLatThresholds(const std::vector<float>& thresholds) {
if(thresholds.size() < 4) return;
std::lock_guard<std::mutex> lock(mutex_);
json11::Json::object root_map = config_json_.object_items();
json11::Json::object algo_map = root_map["algorithms"].object_items();
json11::Json::object pallet_map = algo_map["pallet_offset"].object_items();
pallet_map["offset_lat_mm_thresholds"] = json11::Json::object{
{"A", thresholds[0]}, {"B", thresholds[1]}, {"C", thresholds[2]}, {"D", thresholds[3]}
};
algo_map["pallet_offset"] = pallet_map;
root_map["algorithms"] = algo_map;
config_json_ = root_map;
}
void ConfigManager::setPalletOffsetLonThresholds(const std::vector<float>& thresholds) {
if(thresholds.size() < 4) return;
std::lock_guard<std::mutex> lock(mutex_);
json11::Json::object root_map = config_json_.object_items();
json11::Json::object algo_map = root_map["algorithms"].object_items();
json11::Json::object pallet_map = algo_map["pallet_offset"].object_items();
pallet_map["offset_lon_mm_thresholds"] = json11::Json::object{
{"A", thresholds[0]}, {"B", thresholds[1]}, {"C", thresholds[2]}, {"D", thresholds[3]}
};
algo_map["pallet_offset"] = pallet_map;
root_map["algorithms"] = algo_map;
config_json_ = root_map;
}
void ConfigManager::setPalletRotationAngleThresholds(const std::vector<float>& thresholds) {
if(thresholds.size() < 4) return;
std::lock_guard<std::mutex> lock(mutex_);
json11::Json::object root_map = config_json_.object_items();
json11::Json::object algo_map = root_map["algorithms"].object_items();
json11::Json::object pallet_map = algo_map["pallet_offset"].object_items();
pallet_map["rotation_angle_thresholds"] = json11::Json::object{
{"A", thresholds[0]}, {"B", thresholds[1]}, {"C", thresholds[2]}, {"D", thresholds[3]}
};
algo_map["pallet_offset"] = pallet_map;
root_map["algorithms"] = algo_map;
config_json_ = root_map;
}
void ConfigManager::setPalletHoleDefLeftThresholds(const std::vector<float>& thresholds) {
if(thresholds.size() < 4) return;
std::lock_guard<std::mutex> lock(mutex_);
json11::Json::object root_map = config_json_.object_items();
json11::Json::object algo_map = root_map["algorithms"].object_items();
json11::Json::object pallet_map = algo_map["pallet_offset"].object_items();
pallet_map["hole_def_mm_left_thresholds"] = json11::Json::object{
{"A", thresholds[0]}, {"B", thresholds[1]}, {"C", thresholds[2]}, {"D", thresholds[3]}
};
algo_map["pallet_offset"] = pallet_map;
root_map["algorithms"] = algo_map;
config_json_ = root_map;
}
void ConfigManager::setPalletHoleDefRightThresholds(const std::vector<float>& thresholds) {
if(thresholds.size() < 4) return;
std::lock_guard<std::mutex> lock(mutex_);
json11::Json::object root_map = config_json_.object_items();
json11::Json::object algo_map = root_map["algorithms"].object_items();
json11::Json::object pallet_map = algo_map["pallet_offset"].object_items();
pallet_map["hole_def_mm_right_thresholds"] = json11::Json::object{
{"A", thresholds[0]}, {"B", thresholds[1]}, {"C", thresholds[2]}, {"D", thresholds[3]}
};
algo_map["pallet_offset"] = pallet_map;
root_map["algorithms"] = algo_map;
config_json_ = root_map;
}
// Slot Occupancy Setters
void ConfigManager::setSlotOccupancyDepthThreshold(float value) {
std::lock_guard<std::mutex> lock(mutex_);
json11::Json::object root_map = config_json_.object_items();
json11::Json::object algo_map = root_map["algorithms"].object_items();
json11::Json::object slot_map = algo_map["slot_occupancy"].object_items();
slot_map["depth_threshold_mm"] = value;
algo_map["slot_occupancy"] = slot_map;
root_map["algorithms"] = algo_map;
config_json_ = root_map;
}
void ConfigManager::setSlotOccupancyConfidenceThreshold(float value) {
std::lock_guard<std::mutex> lock(mutex_);
json11::Json::object root_map = config_json_.object_items();
json11::Json::object algo_map = root_map["algorithms"].object_items();
json11::Json::object slot_map = algo_map["slot_occupancy"].object_items();
slot_map["confidence_threshold"] = value;
algo_map["slot_occupancy"] = slot_map;
root_map["algorithms"] = algo_map;
config_json_ = root_map;
}
// Visual Inventory Setters
void ConfigManager::setVisualInventoryBarcodeConfidence(float value) {
std::lock_guard<std::mutex> lock(mutex_);
json11::Json::object root_map = config_json_.object_items();
json11::Json::object algo_map = root_map["algorithms"].object_items();
json11::Json::object vis_map = algo_map["visual_inventory"].object_items();
vis_map["barcode_confidence_threshold"] = value;
algo_map["visual_inventory"] = vis_map;
root_map["algorithms"] = algo_map;
config_json_ = root_map;
}
void ConfigManager::setVisualInventoryROIEnabled(bool value) {
std::lock_guard<std::mutex> lock(mutex_);
json11::Json::object root_map = config_json_.object_items();
json11::Json::object algo_map = root_map["algorithms"].object_items();
json11::Json::object vis_map = algo_map["visual_inventory"].object_items();
vis_map["roi_enabled"] = value;
algo_map["visual_inventory"] = vis_map;
root_map["algorithms"] = algo_map;
config_json_ = root_map;
}
// General Setters
void ConfigManager::setAlgorithmMinDepth(float value) {
std::lock_guard<std::mutex> lock(mutex_);
json11::Json::object root_map = config_json_.object_items();
json11::Json::object algo_map = root_map["algorithms"].object_items();
json11::Json::object gen_map = algo_map["general"].object_items();
gen_map["min_depth_mm"] = value;
algo_map["general"] = gen_map;
root_map["algorithms"] = algo_map;
config_json_ = root_map;
}
void ConfigManager::setAlgorithmMaxDepth(float value) {
std::lock_guard<std::mutex> lock(mutex_);
json11::Json::object root_map = config_json_.object_items();
json11::Json::object algo_map = root_map["algorithms"].object_items();
json11::Json::object gen_map = algo_map["general"].object_items();
gen_map["max_depth_mm"] = value;
algo_map["general"] = gen_map;
root_map["algorithms"] = algo_map;
config_json_ = root_map;
}
void ConfigManager::setAlgorithmSamplePoints(int value) {
std::lock_guard<std::mutex> lock(mutex_);
json11::Json::object root_map = config_json_.object_items();
json11::Json::object algo_map = root_map["algorithms"].object_items();
json11::Json::object gen_map = algo_map["general"].object_items();
gen_map["sample_points"] = value;
algo_map["general"] = gen_map;
root_map["algorithms"] = algo_map;
config_json_ = root_map;
}
// Generic access
std::string ConfigManager::getString(const std::string &key,
const std::string &default_value) const {
std::lock_guard<std::mutex> lock(mutex_);
// Simple top-level access, or implement dot notation parsing if needed.
// For now assuming top level.
if (config_json_[key].is_string()) {
return config_json_[key].string_value();
}
return default_value;
}
int ConfigManager::getInt(const std::string &key, int default_value) const {
std::lock_guard<std::mutex> lock(mutex_);
if (config_json_[key].is_number()) {
return config_json_[key].int_value();
}
return default_value;
}
bool ConfigManager::getBool(const std::string &key, bool default_value) const {
std::lock_guard<std::mutex> lock(mutex_);
if (config_json_[key].is_bool()) {
return config_json_[key].bool_value();
}
return default_value;
}

View File

@@ -0,0 +1,124 @@
#pragma once
#include "json11.hpp"
#include <map>
#include <mutex>
#include <string>
#include <vector>
#include <opencv2/core.hpp>
/**
* @brief ConfigManager
* 全局配置管理器,单例模式
* 负责加载和提供系统配置参数
*/
class ConfigManager {
public:
static ConfigManager &getInstance();
// 禁止拷贝
ConfigManager(const ConfigManager &) = delete;
ConfigManager &operator=(const ConfigManager &) = delete;
/**
* 加载配置文件
* @param config_path 配置文件路径,默认在当前目录查找 config.json
* @return 是否成功加载
*/
bool loadConfig(const std::string &config_path = "config.json");
/**
* 保存配置文件
* @param config_path 配置文件路径,默认在当前目录查找 config.json
* @return 是否成功保存
*/
bool saveConfig(const std::string &config_path = "config.json");
// --- Accessors & Setters ---
// Redis Config
std::string getRedisHost() const;
int getRedisPort() const;
int getRedisDb() const;
// Camera Config
bool isDepthEnabled() const;
bool isColorEnabled() const;
struct CameraMapping {
std::string id;
int index;
};
std::vector<CameraMapping> getCameraMappings() const;
// Vision/Global Config
std::string getSavePath() const;
int getLogLevel() const;
// Algorithm Config - Beam/Rack Deflection
std::vector<cv::Point2i> getBeamROIPoints() const;
void setBeamROIPoints(const std::vector<cv::Point2i>& points);
std::vector<cv::Point2i> getRackROIPoints() const;
void setRackROIPoints(const std::vector<cv::Point2i>& points);
std::vector<float> getBeamThresholds() const; // Returns [A, B, C, D]
void setBeamThresholds(const std::vector<float>& thresholds);
std::vector<float> getRackThresholds() const; // Returns [A, B, C, D]
void setRackThresholds(const std::vector<float>& thresholds);
// Algorithm Config - Pallet Offset
std::vector<float> getPalletOffsetLatThresholds() const;
void setPalletOffsetLatThresholds(const std::vector<float>& thresholds);
std::vector<float> getPalletOffsetLonThresholds() const;
void setPalletOffsetLonThresholds(const std::vector<float>& thresholds);
std::vector<float> getPalletRotationAngleThresholds() const;
void setPalletRotationAngleThresholds(const std::vector<float>& thresholds);
std::vector<float> getPalletHoleDefLeftThresholds() const;
void setPalletHoleDefLeftThresholds(const std::vector<float>& thresholds);
std::vector<float> getPalletHoleDefRightThresholds() const;
void setPalletHoleDefRightThresholds(const std::vector<float>& thresholds);
// Algorithm Config - Slot Occupancy
float getSlotOccupancyDepthThreshold() const;
void setSlotOccupancyDepthThreshold(float value);
float getSlotOccupancyConfidenceThreshold() const;
void setSlotOccupancyConfidenceThreshold(float value);
// Algorithm Config - Visual Inventory
float getVisualInventoryBarcodeConfidence() const;
void setVisualInventoryBarcodeConfidence(float value);
bool getVisualInventoryROIEnabled() const;
void setVisualInventoryROIEnabled(bool value);
// Algorithm Config - General
float getAlgorithmMinDepth() const;
void setAlgorithmMinDepth(float value);
float getAlgorithmMaxDepth() const;
void setAlgorithmMaxDepth(float value);
int getAlgorithmSamplePoints() const;
void setAlgorithmSamplePoints(int value);
// Generic access (for dynamic access)
std::string getString(const std::string &key,
const std::string &default_value = "") const;
int getInt(const std::string &key, int default_value = 0) const;
bool getBool(const std::string &key, bool default_value = false) const;
private:
ConfigManager();
~ConfigManager() = default;
json11::Json config_json_;
mutable std::mutex mutex_;
};

View File

@@ -0,0 +1,104 @@
#include "log_manager.h"
#include <iostream>
LogManager &LogManager::getInstance() {
static LogManager instance;
return instance;
}
void LogManager::setCallback(LogCallback callback) {
std::lock_guard<std::mutex> lock(mutex_);
callback_ = callback;
}
#include "config_manager.h"
#include <cstdarg>
#include <cstdio>
#include <ctime>
#include <iomanip>
#include <sstream>
#include <vector>
void LogManager::logFormat(LogLevel level, const char *fmt, ...) {
// 1. Level Filter
if (static_cast<int>(level) < ConfigManager::getInstance().getLogLevel()) {
return;
}
// 2. Format Message
va_list args;
va_start(args, fmt);
// Determine required size
va_list args_copy;
va_copy(args_copy, args);
int size = std::vsnprintf(nullptr, 0, fmt, args_copy);
va_end(args_copy);
if (size < 0) {
va_end(args);
return; // Encoding error
}
std::vector<char> buffer(size + 1);
std::vsnprintf(buffer.data(), buffer.size(), fmt, args);
va_end(args);
std::string message(buffer.data(), size);
// 3. Delegate
logInternal(level, message);
}
void LogManager::logInternal(LogLevel level, const std::string &message) {
// 1. Add Timestamp and Level Prefix
const char *levelStr = "[INFO] ";
bool isError = false;
switch (level) {
case LogLevel::DEBUG:
levelStr = "[DEBUG] ";
break;
case LogLevel::INFO:
levelStr = "[INFO] ";
break;
case LogLevel::WARNING:
levelStr = "[WARN] ";
break;
case LogLevel::ERROR:
levelStr = "[ERROR] ";
isError = true;
break;
}
auto t = std::time(nullptr);
auto tm = *std::localtime(&t);
std::ostringstream oss;
oss << std::put_time(&tm, "[%Y-%m-%d %H:%M:%S] ") << levelStr << message
<< "\n";
std::string formattedMsg = oss.str();
// 2. Output
std::lock_guard<std::mutex> lock(mutex_);
if (callback_) {
callback_(formattedMsg, isError);
} else {
if (isError) {
std::cerr << formattedMsg;
} else {
std::cout << formattedMsg;
}
}
}
// 兼容旧接口
void LogManager::log(const std::string &message, bool isError) {
logInternal(isError ? LogLevel::ERROR : LogLevel::INFO, message);
}
void LogManager::clearCallback() {
std::lock_guard<std::mutex> lock(mutex_);
callback_ = nullptr;
}

View File

@@ -0,0 +1,80 @@
#pragma once
#include <functional>
#include <memory>
#include <mutex>
#include <string>
/**
* @brief 全局日志管理器(单例模式)
*
* 用于将标准输出cout/cerr重定向到Qt日志系统
* 任何模块都可以通过LogManager输出日志这些日志会被重定向到MainWindow的日志文本框
*/
// 日志级别枚举
enum class LogLevel { DEBUG = 0, INFO = 1, WARNING = 2, ERROR = 3 };
/**
* @brief 全局日志管理器(单例模式)
*
* 用于将标准输出cout/cerr重定向到Qt日志系统
* 支持格式化输出和日志级别过滤
*/
class LogManager {
public:
// 日志回调函数类型
using LogCallback =
std::function<void(const std::string &message, bool isError)>;
/**
* @brief 获取单例实例
*/
static LogManager &getInstance();
/**
* @brief 设置日志回调函数
* @param callback 回调函数,接收日志消息和错误标志
*/
void setCallback(LogCallback callback);
/**
* @brief 格式化并输出日志消息
* @param level 日志级别
* @param fmt 格式化字符串 (printf style)
* @param ... 可变参数
*/
void logFormat(LogLevel level, const char *fmt, ...);
/**
* @brief 兼容旧接口的日志输出
* @param message 日志消息
* @param isError 是否为错误消息
*/
void log(const std::string &message, bool isError = false);
/**
* @brief 清除回调函数
*/
void clearCallback();
private:
LogManager() = default;
~LogManager() = default;
// 内部实际执行日志输出的方法
void logInternal(LogLevel level, const std::string &message);
LogCallback callback_;
std::mutex mutex_; // 保护回调函数的线程安全
};
// 宏定义以便于使用
#define LOG_DEBUG(fmt, ...) \
LogManager::getInstance().logFormat(LogLevel::DEBUG, fmt, ##__VA_ARGS__)
#define LOG_INFO(fmt, ...) \
LogManager::getInstance().logFormat(LogLevel::INFO, fmt, ##__VA_ARGS__)
#define LOG_WARN(fmt, ...) \
LogManager::getInstance().logFormat(LogLevel::WARNING, fmt, ##__VA_ARGS__)
#define LOG_ERROR(fmt, ...) \
LogManager::getInstance().logFormat(LogLevel::ERROR, fmt, ##__VA_ARGS__)

View File

@@ -0,0 +1,39 @@
#pragma once
#include <streambuf>
#include <string>
#include "log_manager.h"
/**
* @brief 自定义streambuf用于重定向cout/cerr到LogManager
*/
class LogStreamBuf : public std::streambuf {
public:
LogStreamBuf(bool isError) : isError_(isError), buffer_() {}
protected:
virtual int_type overflow(int_type c) override {
if (c != EOF) {
buffer_ += static_cast<char>(c);
if (c == '\n') {
// 遇到换行符,输出完整行
LogManager::getInstance().log(buffer_, isError_);
buffer_.clear();
}
}
return c;
}
virtual int sync() override {
if (!buffer_.empty()) {
LogManager::getInstance().log(buffer_, isError_);
buffer_.clear();
}
return 0;
}
private:
bool isError_;
std::string buffer_;
};

View File

@@ -0,0 +1,27 @@
#pragma once
#include <vector>
/**
* @brief 点云数据结构
* 表示一个三维点
*/
struct Point3D {
float x, y, z;
Point3D() : x(0), y(0), z(0) {}
Point3D(float x, float y, float z) : x(x), y(y), z(z) {}
};
/**
* @brief 相机内参结构
*/
struct CameraIntrinsics {
float fx; // 焦距x
float fy; // 焦距y
float cx; // 主点x
float cy; // 主点y
CameraIntrinsics() : fx(0), fy(0), cx(0), cy(0) {}
CameraIntrinsics(float fx, float fy, float cx, float cy)
: fx(fx), fy(fy), cx(cx), cy(cy) {}
};

View File

@@ -0,0 +1,300 @@
/**
* @file device_manager.cpp
* @brief 设备管理器实现文件
*
* 此文件实现了DeviceManager类的完整功能
* - 设备初始化(扫描和配置相机)
* - 设备启动和停止
* - 图像获取接口
* - 设备信息查询
*
* 设计说明:
* - DeviceManager是对CameraCapture的封装提供统一的设备管理接口
* - 不涉及业务逻辑,只负责设备层的管理
* - 使用智能指针管理CameraCapture自动释放资源
*/
#include "device_manager.h"
#include "../camera/ty_multi_camera_capture.h"
#include "../camera/mvs_multi_camera_capture.h"
#include <iostream>
/**
* @brief 获取单例实例
*
* @return DeviceManager单例引用-DeviceManager&返回的是实例的引用
*/
DeviceManager& DeviceManager::getInstance() {
static DeviceManager instance; // C++11保证线程安全的单例
return instance;
}
/**
* @brief 构造函数(私有)
*
* 初始化设备管理器,设置初始状态为未初始化
*/
DeviceManager::DeviceManager() : initialized_(false) {}
/**
* @brief 析构函数
*
* 确保在对象销毁时正确停止所有设备
* 调用stopAll()清理资源
*/
DeviceManager::~DeviceManager() {
stopAll();
}
/**
* @brief 初始化并扫描设备
*
* 初始化相机采集模块,扫描并配置所有可用的相机设备
*
* @param enable_depth 是否启用深度流true表示启用深度图采集
* @param enable_color 是否启用彩色流true表示启用彩色图采集
* @return 发现的设备数量0表示初始化失败或未找到设备
*
* @note 如果已经初始化,直接返回当前设备数量(避免重复初始化)
* @note 初始化失败时capture_会被重置为nullptr
*/
int DeviceManager::initialize(bool enable_depth, bool enable_color) {
// 如果已经初始化,直接返回当前设备数量
if (initialized_) {
return getDeviceCount();
}
int total_count = 0;
// 创建深度相机采集对象
capture_ = std::make_shared<CameraCapture>();
// 初始化深度相机采集(扫描设备、配置流)
if (capture_->initialize(enable_depth, enable_color)) {
total_count += capture_->getCameraCount();
std::cout << "[DeviceManager] Initialized " << capture_->getCameraCount() << " depth camera(s)" << std::endl;
} else {
std::cerr << "[DeviceManager] Failed to initialize depth cameras" << std::endl;
capture_.reset(); // 重置智能指针,释放资源
}
// 初始化MVS 2D相机
mvs_cameras_ = std::make_unique<MvsMultiCameraCapture>();
if (mvs_cameras_->initialize()) {
total_count += mvs_cameras_->getCameraCount();
std::cout << "[DeviceManager] Initialized " << mvs_cameras_->getCameraCount() << " 2D camera(s)" << std::endl;
} else {
std::cout << "[DeviceManager] No 2D cameras found or initialization failed" << std::endl;
mvs_cameras_.reset();
}
// 获取设备数量并标记为已初始化
initialized_ = true;
std::cout << "[DeviceManager] Total devices initialized: " << total_count << std::endl;
return total_count;
}
/**
* @brief 启动所有设备
*
* 启动所有相机的数据采集
*
* @return true 启动成功false 启动失败capture_为空或启动失败
*
* @note 必须先调用initialize()初始化设备
*/
bool DeviceManager::startAll() {
bool success = true;
// 启动深度相机
if (capture_) {
if (!capture_->start()) {
success = false;
std::cerr << "[DeviceManager] Failed to start depth cameras" << std::endl;
}
}
// 启动2D相机
if (mvs_cameras_) {
if (!mvs_cameras_->start()) {
success = false;
std::cerr << "[DeviceManager] Failed to start 2D cameras" << std::endl;
}
}
return success;
}
/**
* @brief 停止所有设备
*
* 停止所有相机的数据采集
*
* @note 此函数是幂等的,可以安全地多次调用
*/
void DeviceManager::stopAll() {
if (capture_) {
capture_->stop();
}
}
/**
* @brief 获取设备数量
*
* @return 当前已初始化的设备数量0表示未初始化或无设备
*/
int DeviceManager::getDeviceCount() const {
int count = 0;
if (capture_) {
count += capture_->getCameraCount();
}
if (mvs_cameras_) {
count += mvs_cameras_->getCameraCount();
}
return count;
}
int DeviceManager::getDepthCameraCount() const {
return capture_ ? capture_->getCameraCount() : 0;
}
/**
* @brief 获取设备ID
*
* @param index 设备索引从0开始
* @return 设备ID字符串如果索引无效或未初始化则返回空字符串
*/
std::string DeviceManager::getDeviceId(int index) const {
int percipio_count = capture_ ? capture_->getCameraCount() : 0;
if (index < percipio_count) {
return capture_->getCameraId(index);
}
int mvs_index = index - percipio_count;
if (mvs_cameras_ && mvs_index >= 0 && mvs_index < mvs_cameras_->getCameraCount()) {
return "2D-" + mvs_cameras_->getCameraId(mvs_index);
}
return "";
}
/**
* @brief 获取指定设备的最新图像
*
* 从设备缓冲区中获取最新采集的图像数据
*
* @param device_index 设备索引从0开始
* @param depth [输出] 深度图CV_16U格式包含原始深度值单位毫米
* @param color [输出] 彩色图BGR格式CV_8UC3类型
* @param fps [输出] 当前帧率(帧/秒)
* @return true 成功获取图像false 获取失败(设备未初始化、索引无效、缓冲区为空)
*
* @note 此函数是线程安全的,使用互斥锁保护缓冲区访问
* @note 如果某个图像流未启用或尚未采集到数据对应的Mat将为空
*/
bool DeviceManager::getLatestImages(int device_index, cv::Mat& depth, cv::Mat& color, double& fps) {
int percipio_count = capture_ ? capture_->getCameraCount() : 0;
// 深度相机
if (device_index < percipio_count) {
if (!capture_) return false;
return capture_->getLatestImages(device_index, depth, color, fps);
}
// 2D相机
int mvs_index = device_index - percipio_count;
if (mvs_cameras_ && mvs_index >= 0 && mvs_index < mvs_cameras_->getCameraCount()) {
depth = cv::Mat(); // 2D相机没有深度图
return mvs_cameras_->getLatestImage(mvs_index, color, fps);
}
return false;
}
/**
* @brief 检查是否正在运行
*
* @return true 正在运行false 已停止或未初始化
*/
bool DeviceManager::isRunning() const {
bool anyScaleRunning = capture_ && capture_->isRunning();
bool anyMVSRunning = mvs_cameras_ && mvs_cameras_->isRunning();
return anyScaleRunning || anyMVSRunning;
}
/**
* @brief 获取指定设备的深度相机内参
*
* 从相机SDK获取深度相机的内参fx, fy, cx, cy
* 内参存储在相机的标定数据中
*
* @param device_index 设备索引从0开始
* @param cy [输出] 主点y坐标像素单位
* @return 是否成功获取内参
*/
bool DeviceManager::getDepthCameraIntrinsics(int device_index, float& fx, float& fy, float& cx, float& cy) {
if (!capture_) return false;
// 只有深度相机有内参
int percipio_count = capture_->getCameraCount();
if (device_index < percipio_count) {
return capture_->getDepthCameraIntrinsics(device_index, fx, fy, cx, cy);
}
return false;
}
/**
* @brief 利用SDK生成点云
* @param device_index 设备索引
* @param depth_img 深度图
* @param out_points 输出点云
* @return 是否成功
*/
bool DeviceManager::computePointCloud(int device_index, const cv::Mat& depth_img, std::vector<Point3D>& out_points) {
if (!capture_) return false;
// 只有深度相机可以生成点云
int percipio_count = capture_->getCameraCount();
if (device_index < percipio_count) {
return capture_->computePointCloud(device_index, depth_img, out_points);
}
return false;
}
int DeviceManager::get2DCameraCount() const {
return mvs_cameras_ ? mvs_cameras_->getCameraCount() : 0;
}
bool DeviceManager::get2DCameraImage(int camera_index, cv::Mat& image, double& fps) {
if (!mvs_cameras_) {
return false;
}
return mvs_cameras_->getLatestImage(camera_index, image, fps);
}
std::string DeviceManager::get2DCameraId(int camera_index) const {
if (!mvs_cameras_) {
return "";
}
return mvs_cameras_->getCameraId(camera_index);
}

View File

@@ -0,0 +1,154 @@
#pragma once
// #include "../camera/ty_multi_camera_capture.h" -> Moved to cpp
// #include "../camera/mvs_multi_camera_capture.h" -> Moved to cpp
#include <opencv2/opencv.hpp>
#include <vector>
class CameraCapture;
class MvsMultiCameraCapture;
#include <memory>
#include <string>
#include "../common_types.h"
/**
* @brief DeviceManager
* 设备管理器Device Manager负责管理硬件设备相机、读码器等
*
* 采用单例模式,确保全局只有一个设备管理器实例
* 任何模块都可以通过getInstance()访问设备
*
* 功能说明:
* - 管理相机采集设备的初始化、启动、停止
* - 管理读码器设备的初始化、启动、停止
* - 提供设备访问接口(获取图像、设备信息等)
* - 支持未来扩展其他设备类型
*
* 职责范围:
* - 设备生命周期管理(初始化、启动、停止)
* - 设备数据获取(图像、设备信息)
* - 不涉及业务逻辑(任务管理、结果处理等)
*/
class DeviceManager {
public:
/**
* 获取单例实例
* @return DeviceManager单例引用
*/
static DeviceManager& getInstance();
// 禁止拷贝和赋值
DeviceManager(const DeviceManager&) = delete;
DeviceManager& operator=(const DeviceManager&) = delete;
~DeviceManager();
/**
* 初始化并扫描设备
* @param enable_depth 是否启用深度流
* @param enable_color 是否启用彩色流
* @return 发现的设备数量
*/
int initialize(bool enable_depth = true, bool enable_color = true);
/**
* 启动所有设备
* @return 是否成功
*/
bool startAll();
/**
* 停止所有设备
*/
void stopAll();
/**
* 获取设备数量
* @return 设备数量
*/
int getDeviceCount() const;
/**
* 获取设备ID
* @param index 设备索引
* @return 设备ID字符串
*/
std::string getDeviceId(int index) const;
/**
* 获取指定设备的最新图像
* @param device_index 设备索引
* @param depth 输出的深度图
* @param color 输出的彩色图
* @param fps 输出的帧率
* @return 是否成功获取到图像
*/
bool getLatestImages(int device_index, cv::Mat& depth, cv::Mat& color, double& fps);
/**
* 检查是否正在运行
* @return 是否运行中
*/
bool isRunning() const;
/**
* 获取指定设备的深度相机内参
* @param device_index 设备索引
* @param fx [输出] 焦距x
* @param fy [输出] 焦距y
* @param cx [输出] 主点x
* @param cy [输出] 主点y
* @return 是否成功获取内参
*/
bool getDepthCameraIntrinsics(int device_index, float& fx, float& fy, float& cx, float& cy);
/**
* @brief 利用SDK生成点云
* @param device_index 设备索引
* @param depth_img 深度图
* @param out_points 输出点云
* @return 是否成功
*/
bool computePointCloud(int device_index, const cv::Mat& depth_img, std::vector<Point3D>& out_points);
/**
* 获取深度相机数量
* @return 深度相机数量
*/
int getDepthCameraCount() const;
/**
* 获取2D (MVS)相机数量
* @return 2D相机数量
*/
int get2DCameraCount() const;
/**
* 获取2D相机图像
* @param camera_index 2D相机索引从0开始
* @param image 输出的彩色图
* @param fps 输出的帧率
* @return 是否成功
*/
bool get2DCameraImage(int camera_index, cv::Mat& image, double& fps);
/**
* 获取2D相机ID
* @param camera_index 2D相机索引
* @return 相机ID字符串
*/
std::string get2DCameraId(int camera_index) const;
private:
DeviceManager(); // 私有构造函数,确保单例
std::shared_ptr<CameraCapture> capture_; // Percipio深度相机采集对象
std::unique_ptr<MvsMultiCameraCapture> mvs_cameras_; // MVS 2D相机采集对象
bool initialized_; // 是否已初始化
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,106 @@
#pragma once
#include <QMainWindow>
#include <QLabel>
#include <QPushButton>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QTimer>
#include <QPlainTextEdit>
#include <QTextStream>
#include <QTabWidget>
#include <QDoubleSpinBox>
#include <QSpinBox>
#include <QFormLayout>
#include <QGroupBox>
#include <QScrollArea>
#include <QScrollArea>
#include <memory>
#include <vector>
#include <streambuf>
#include <fstream>
#include <opencv2/core/mat.hpp>
// Forward declarations
class SettingsWidget;
class ImageProcessor;
class VisionController;
class LogStreamBuf;
// 必须在QT_BEGIN_NAMESPACE之前包含确保MOC能看到完整类型定义
#include "../common/log_streambuf.h"
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
void onStartCapture();
void onStopCapture();
void onSaveImage();
void onSavePointCloud();
void updateImage();
private:
// Helper to reduce redundancy in save functions
bool prepareCapturedData(cv::Mat& depth, cv::Mat& color, QString& timestamp);
Ui::MainWindow *ui;
std::shared_ptr<VisionController> visionController_; // Vision系统控制器Redis监控和算法触发
std::vector<std::shared_ptr<ImageProcessor>> processors_;
// Supports max 4 depth cameras
static const int MAX_DEPTH_CAMERAS = 4;
static const int MAX_2D_CAMERAS = 5;
QLabel* depthImageLabels_[MAX_DEPTH_CAMERAS];
QLabel* depthInfoLabels_[MAX_DEPTH_CAMERAS];
// 2D camera display control array
QLabel* twoDImageLabels_[MAX_2D_CAMERAS];
QLabel* twoDInfoLabels_[MAX_2D_CAMERAS];
void update2DDisplay();
void update2DCameraDisplay(int camera_index, const cv::Mat& image, double fps);
// 深度图信息标签显示相机编号、FPS等
QPushButton* startButton_;
QPushButton* stopButton_;
QPushButton* saveButton_;
QPushButton* savePointCloudButton_;
QPlainTextEdit* logTextEdit_;
QTimer* updateTimer_;
bool isCapturing_;
int currentDeviceIndex_;
// 日志重定向相关
std::unique_ptr<LogStreamBuf> coutBuf_;
std::unique_ptr<LogStreamBuf> cerrBuf_;
std::streambuf* originalCout_;
std::streambuf* originalCerr_;
QImage cvMatToQImage(const cv::Mat& mat);
void updateDepthDisplay();
void updateCameraDisplay(int cameraIndex);
void appendLog(const QString& message);
// 辅助函数:简化代码
void startImageDisplay(); // 启动图像显示(更新按钮状态和定时器)
void stopImageDisplay(); // 停止图像显示(更新按钮状态、停止定时器、清空显示)
// Settings tab
// Settings Widget
SettingsWidget* settingsWidget_;
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,305 @@
#include "settings_widget.h"
#include "../common/config_manager.h"
#include <QSpinBox>
#include <QDoubleSpinBox>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QTabWidget>
#include <QPushButton>
#include <QGroupBox>
#include <QFormLayout>
#include <QLabel>
#include <QScrollArea>
#include <QMessageBox>
SettingsWidget::SettingsWidget(QWidget *parent) : QWidget(parent) {
setupUi();
loadSettings();
}
void SettingsWidget::setupUi() {
auto mainLayout = new QVBoxLayout(this);
auto tabWidget = new QTabWidget(this);
tabWidget->addTab(createBeamRackTab(), "Beam/Rack Deflection");
tabWidget->addTab(createPalletOffsetTab(), "Pallet Offset");
tabWidget->addTab(createOtherAlgorithmsTab(), "Other Algorithms");
tabWidget->addTab(createGeneralTab(), "General");
mainLayout->addWidget(tabWidget);
auto buttonLayout = new QHBoxLayout();
buttonLayout->addStretch();
auto saveButton = new QPushButton("Save Settings", this);
saveButton->setMinimumSize(120, 35);
QFont font;
font.setPointSize(12);
saveButton->setFont(font);
connect(saveButton, &QPushButton::clicked, this, &SettingsWidget::saveSettings);
buttonLayout->addWidget(saveButton);
mainLayout->addLayout(buttonLayout);
}
QWidget* SettingsWidget::createBeamRackTab() {
auto widget = new QWidget(this);
auto layout = new QVBoxLayout(widget);
auto roiGroup = new QGroupBox("感兴趣区域点", this);
auto roiLayout = new QGridLayout(roiGroup);
roiLayout->addWidget(new QLabel("", this), 0, 0);
roiLayout->addWidget(new QLabel("横梁 X", this), 0, 1);
roiLayout->addWidget(new QLabel("横梁 Y", this), 0, 2);
roiLayout->addWidget(new QLabel("立柱 X", this), 0, 3);
roiLayout->addWidget(new QLabel("立柱 Y", this), 0, 4);
const QStringList pointNames = {"左上", "右上", "右下", "左下"};
for (int i = 0; i < 4; ++i) {
beamRoiX_[i] = new QSpinBox(this); beamRoiX_[i]->setRange(0, 5000);
beamRoiY_[i] = new QSpinBox(this); beamRoiY_[i]->setRange(0, 5000);
rackRoiX_[i] = new QSpinBox(this); rackRoiX_[i]->setRange(0, 5000);
rackRoiY_[i] = new QSpinBox(this); rackRoiY_[i]->setRange(0, 5000);
roiLayout->addWidget(new QLabel(pointNames[i], this), i+1, 0);
roiLayout->addWidget(beamRoiX_[i], i+1, 1);
roiLayout->addWidget(beamRoiY_[i], i+1, 2);
roiLayout->addWidget(rackRoiX_[i], i+1, 3);
roiLayout->addWidget(rackRoiY_[i], i+1, 4);
}
layout->addWidget(roiGroup);
auto threshGroup = new QGroupBox("阈值 (mm)", this);
auto threshLayout = new QFormLayout(threshGroup);
auto createDoubleSpin = [this](QDoubleSpinBox*& box) {
box = new QDoubleSpinBox(this);
box->setRange(-1000.0, 1000.0);
box->setSingleStep(0.1);
box->setDecimals(1);
};
createDoubleSpin(beamThresholdA_);
createDoubleSpin(beamThresholdB_);
createDoubleSpin(beamThresholdC_);
createDoubleSpin(beamThresholdD_);
createDoubleSpin(rackThresholdA_);
createDoubleSpin(rackThresholdB_);
createDoubleSpin(rackThresholdC_);
createDoubleSpin(rackThresholdD_);
threshLayout->addRow("横梁负向报警 (A):", beamThresholdA_);
threshLayout->addRow("横梁负向预警 (B):", beamThresholdB_);
threshLayout->addRow("横梁正向预警 (C):", beamThresholdC_);
threshLayout->addRow("横梁正向报警 (D):", beamThresholdD_);
threshLayout->addRow("立柱负向报警 (A):", rackThresholdA_);
threshLayout->addRow("立柱负向预警 (B):", rackThresholdB_);
threshLayout->addRow("立柱正向预警 (C):", rackThresholdC_);
threshLayout->addRow("立柱正向报警 (D):", rackThresholdD_);
layout->addWidget(threshGroup);
layout->addStretch();
return widget;
}
QWidget* SettingsWidget::createPalletOffsetTab() {
auto widget = new QWidget(this);
auto scroll = new QScrollArea(widget);
auto contentWidget = new QWidget();
auto layout = new QVBoxLayout(contentWidget);
auto createThreshGroup = [this](const QString& title, QDoubleSpinBox*& A, QDoubleSpinBox*& B, QDoubleSpinBox*& C, QDoubleSpinBox*& D) {
auto group = new QGroupBox(title, this);
auto flo = new QFormLayout(group);
auto createDS = [this](QDoubleSpinBox*& box) {
box = new QDoubleSpinBox(this);
box->setRange(-1000.0, 1000.0);
box->setSingleStep(0.1);
box->setDecimals(1);
};
createDS(A); createDS(B); createDS(C); createDS(D);
flo->addRow("低位报警 (A):", A);
flo->addRow("低位预警 (B):", B);
flo->addRow("高位预警 (C):", C);
flo->addRow("高位报警 (D):", D);
return group;
};
layout->addWidget(createThreshGroup("横向偏移 (mm)", palletLatA_, palletLatB_, palletLatC_, palletLatD_));
layout->addWidget(createThreshGroup("纵向偏移 (mm)", palletLonA_, palletLonB_, palletLonC_, palletLonD_));
layout->addWidget(createThreshGroup("旋转角度 (deg)", palletRotA_, palletRotB_, palletRotC_, palletRotD_));
layout->addWidget(createThreshGroup("左孔变形 (mm)", palletHoleLeftA_, palletHoleLeftB_, palletHoleLeftC_, palletHoleLeftD_));
layout->addWidget(createThreshGroup("右孔变形 (mm)", palletHoleRightA_, palletHoleRightB_, palletHoleRightC_, palletHoleRightD_));
layout->addStretch();
auto mainLayout = new QVBoxLayout(widget);
scroll->setWidget(contentWidget);
scroll->setWidgetResizable(true);
mainLayout->addWidget(scroll);
return widget;
}
QWidget* SettingsWidget::createOtherAlgorithmsTab() {
auto widget = new QWidget(this);
auto layout = new QVBoxLayout(widget);
auto slotGroup = new QGroupBox("库位占用", this);
auto slotLayout = new QFormLayout(slotGroup);
slotDepthThreshold_ = new QDoubleSpinBox(this);
slotDepthThreshold_->setRange(0.0, 10000.0);
slotConfidenceThreshold_ = new QDoubleSpinBox(this);
slotConfidenceThreshold_->setRange(0.0, 1.0);
slotConfidenceThreshold_->setSingleStep(0.05);
slotLayout->addRow("深度阈值 (mm):", slotDepthThreshold_);
slotLayout->addRow("置信度阈值:", slotConfidenceThreshold_);
layout->addWidget(slotGroup);
auto visGroup = new QGroupBox("视觉盘点", this);
auto visLayout = new QFormLayout(visGroup);
visualBarcodeConfidence_ = new QDoubleSpinBox(this);
visualBarcodeConfidence_->setRange(0.0, 1.0);
visualBarcodeConfidence_->setSingleStep(0.05);
visLayout->addRow("条码置信度:", visualBarcodeConfidence_);
layout->addWidget(visGroup);
layout->addStretch();
return widget;
}
QWidget* SettingsWidget::createGeneralTab() {
auto widget = new QWidget(this);
auto layout = new QFormLayout(widget);
minDepth_ = new QDoubleSpinBox(this);
minDepth_->setRange(0.0, 10000.0);
maxDepth_ = new QDoubleSpinBox(this);
maxDepth_->setRange(0.0, 10000.0);
samplePoints_ = new QSpinBox(this);
samplePoints_->setRange(1, 1000);
layout->addRow("最小深度 (mm):", minDepth_);
layout->addRow("最大深度 (mm):", maxDepth_);
layout->addRow("采样点数:", samplePoints_);
return widget;
}
void SettingsWidget::loadSettings() {
auto& config = ConfigManager::getInstance();
auto beamPoints = config.getBeamROIPoints();
for(int i=0; i<4 && i<beamPoints.size(); ++i) {
beamRoiX_[i]->setValue(beamPoints[i].x);
beamRoiY_[i]->setValue(beamPoints[i].y);
}
auto rackPoints = config.getRackROIPoints();
for(int i=0; i<4 && i<rackPoints.size(); ++i) {
rackRoiX_[i]->setValue(rackPoints[i].x);
rackRoiY_[i]->setValue(rackPoints[i].y);
}
auto beamT = config.getBeamThresholds();
if(beamT.size() >= 4) {
beamThresholdA_->setValue(beamT[0]);
beamThresholdB_->setValue(beamT[1]);
beamThresholdC_->setValue(beamT[2]);
beamThresholdD_->setValue(beamT[3]);
}
auto rackT = config.getRackThresholds();
if(rackT.size() >= 4) {
rackThresholdA_->setValue(rackT[0]);
rackThresholdB_->setValue(rackT[1]);
rackThresholdC_->setValue(rackT[2]);
rackThresholdD_->setValue(rackT[3]);
}
auto setThresh = [](std::vector<float> v, QDoubleSpinBox* a, QDoubleSpinBox* b, QDoubleSpinBox* c, QDoubleSpinBox* d) {
if(v.size() >= 4) {
a->setValue(v[0]); b->setValue(v[1]); c->setValue(v[2]); d->setValue(v[3]);
}
};
setThresh(config.getPalletOffsetLatThresholds(), palletLatA_, palletLatB_, palletLatC_, palletLatD_);
setThresh(config.getPalletOffsetLonThresholds(), palletLonA_, palletLonB_, palletLonC_, palletLonD_);
setThresh(config.getPalletRotationAngleThresholds(), palletRotA_, palletRotB_, palletRotC_, palletRotD_);
setThresh(config.getPalletHoleDefLeftThresholds(), palletHoleLeftA_, palletHoleLeftB_, palletHoleLeftC_, palletHoleLeftD_);
setThresh(config.getPalletHoleDefRightThresholds(), palletHoleRightA_, palletHoleRightB_, palletHoleRightC_, palletHoleRightD_);
slotDepthThreshold_->setValue(config.getSlotOccupancyDepthThreshold());
slotConfidenceThreshold_->setValue(config.getSlotOccupancyConfidenceThreshold());
visualBarcodeConfidence_->setValue(config.getVisualInventoryBarcodeConfidence());
minDepth_->setValue(config.getAlgorithmMinDepth());
maxDepth_->setValue(config.getAlgorithmMaxDepth());
samplePoints_->setValue(config.getAlgorithmSamplePoints());
}
void SettingsWidget::saveSettings() {
auto& config = ConfigManager::getInstance();
std::vector<cv::Point2i> beamPts, rackPts;
for(int i=0; i<4; ++i) {
beamPts.push_back(cv::Point2i(beamRoiX_[i]->value(), beamRoiY_[i]->value()));
rackPts.push_back(cv::Point2i(rackRoiX_[i]->value(), rackRoiY_[i]->value()));
}
config.setBeamROIPoints(beamPts);
config.setRackROIPoints(rackPts);
config.setBeamThresholds({
(float)beamThresholdA_->value(), (float)beamThresholdB_->value(),
(float)beamThresholdC_->value(), (float)beamThresholdD_->value()
});
config.setRackThresholds({
(float)rackThresholdA_->value(), (float)rackThresholdB_->value(),
(float)rackThresholdC_->value(), (float)rackThresholdD_->value()
});
config.setPalletOffsetLatThresholds({
(float)palletLatA_->value(), (float)palletLatB_->value(),
(float)palletLatC_->value(), (float)palletLatD_->value()
});
config.setPalletOffsetLonThresholds({
(float)palletLonA_->value(), (float)palletLonB_->value(),
(float)palletLonC_->value(), (float)palletLonD_->value()
});
config.setPalletRotationAngleThresholds({
(float)palletRotA_->value(), (float)palletRotB_->value(),
(float)palletRotC_->value(), (float)palletRotD_->value()
});
config.setPalletHoleDefLeftThresholds({
(float)palletHoleLeftA_->value(), (float)palletHoleLeftB_->value(),
(float)palletHoleLeftC_->value(), (float)palletHoleLeftD_->value()
});
config.setPalletHoleDefRightThresholds({
(float)palletHoleRightA_->value(), (float)palletHoleRightB_->value(),
(float)palletHoleRightC_->value(), (float)palletHoleRightD_->value()
});
config.setSlotOccupancyDepthThreshold((float)slotDepthThreshold_->value());
config.setSlotOccupancyConfidenceThreshold((float)slotConfidenceThreshold_->value());
config.setVisualInventoryBarcodeConfidence((float)visualBarcodeConfidence_->value());
config.setAlgorithmMinDepth((float)minDepth_->value());
config.setAlgorithmMaxDepth((float)maxDepth_->value());
config.setAlgorithmSamplePoints(samplePoints_->value());
if (config.saveConfig()) {
QMessageBox::information(this, "Success", "Configuration saved successfully.");
emit settingsSaved();
} else {
QMessageBox::critical(this, "Error", "Failed to save configuration.");
}
}

View File

@@ -0,0 +1,75 @@
#pragma once
#include <QWidget>
#include <vector>
class QSpinBox;
class QDoubleSpinBox;
class SettingsWidget : public QWidget {
Q_OBJECT
public:
explicit SettingsWidget(QWidget *parent = nullptr);
~SettingsWidget() = default;
public slots:
void loadSettings();
void saveSettings();
signals:
void settingsSaved();
private:
void setupUi();
QWidget* createBeamRackTab();
QWidget* createPalletOffsetTab();
QWidget* createOtherAlgorithmsTab();
QWidget* createGeneralTab();
// Beam/Rack Deflection
QSpinBox* beamRoiX_[4];
QSpinBox* beamRoiY_[4];
QSpinBox* rackRoiX_[4];
QSpinBox* rackRoiY_[4];
QDoubleSpinBox* beamThresholdA_;
QDoubleSpinBox* beamThresholdB_;
QDoubleSpinBox* beamThresholdC_;
QDoubleSpinBox* beamThresholdD_;
QDoubleSpinBox* rackThresholdA_;
QDoubleSpinBox* rackThresholdB_;
QDoubleSpinBox* rackThresholdC_;
QDoubleSpinBox* rackThresholdD_;
// Pallet Offset
QDoubleSpinBox* palletLatA_;
QDoubleSpinBox* palletLatB_;
QDoubleSpinBox* palletLatC_;
QDoubleSpinBox* palletLatD_;
QDoubleSpinBox* palletLonA_;
QDoubleSpinBox* palletLonB_;
QDoubleSpinBox* palletLonC_;
QDoubleSpinBox* palletLonD_;