init commit
This commit is contained in:
commit
fcf16fe0d3
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Python
|
||||||
|
__pycache__
|
||||||
295
README.md
Normal file
295
README.md
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
# 海康工业相机激光检测系统
|
||||||
|
|
||||||
|
## 项目概述
|
||||||
|
|
||||||
|
本项目是一个基于海康工业相机(MV-CS016-10UC)的激光检测系统,主要用于检测绿色激光点和点阵灯盘。系统采用多线程架构,集成了相机管理、参数配置、检测算法和可视化界面,能够实时识别和跟踪激光目标。
|
||||||
|
本项目使用Trae的SOLO模式生成
|
||||||
|
|
||||||
|
## 系统功能
|
||||||
|
|
||||||
|
### 核心功能
|
||||||
|
|
||||||
|
1. **双模式激光检测**
|
||||||
|
- 模式A:单点/稀疏光点检测(适用于绿色激光笔)
|
||||||
|
- 模式B:点阵灯盘检测(适用于LED阵列)
|
||||||
|
- 智能仲裁:自动选择最优检测模式
|
||||||
|
|
||||||
|
2. **相机管理**
|
||||||
|
- 设备枚举和选择
|
||||||
|
- 相机参数控制(曝光时间、增益、帧率)
|
||||||
|
- 图像采集和显示
|
||||||
|
- 异常处理和重连机制
|
||||||
|
|
||||||
|
3. **参数配置**
|
||||||
|
- 实时参数调节界面
|
||||||
|
- HSV颜色空间配置
|
||||||
|
- 形态学操作参数设置
|
||||||
|
- ROI(感兴趣区域)设置
|
||||||
|
|
||||||
|
4. **性能优化**
|
||||||
|
- 自动曝光调节
|
||||||
|
- 交替帧策略
|
||||||
|
- 多线程架构
|
||||||
|
|
||||||
|
5. **可视化界面**
|
||||||
|
- 实时检测结果显示
|
||||||
|
- 双模检测状态条
|
||||||
|
- 操作日志记录
|
||||||
|
- 用户交互警告
|
||||||
|
|
||||||
|
### 技术特点
|
||||||
|
|
||||||
|
- **海康MVS SDK集成**:完整的SDK初始化、设备管理和参数控制
|
||||||
|
- **OpenCV图像处理**:高效的图像采集、转换和显示
|
||||||
|
- **多线程架构**:实时图像采集和处理
|
||||||
|
- **智能算法**:基于HSV颜色空间和形态学操作的目标检测
|
||||||
|
- **用户友好**:直观的参数调节界面和实时状态显示
|
||||||
|
|
||||||
|
## 安装和配置
|
||||||
|
|
||||||
|
### 1. 硬件要求
|
||||||
|
|
||||||
|
- 海康工业相机(推荐:MV-CS016-10UC)
|
||||||
|
- 计算机(Windows 10/11,64位)
|
||||||
|
- USB 3.0或GigE网络接口
|
||||||
|
|
||||||
|
### 2. 软件要求
|
||||||
|
|
||||||
|
- Python 3.7+
|
||||||
|
- 海康MVS SDK
|
||||||
|
- OpenCV
|
||||||
|
- NumPy
|
||||||
|
- Tkinter(Python标准库)
|
||||||
|
|
||||||
|
### 3. SDK安装
|
||||||
|
|
||||||
|
1. **下载MVS SDK**
|
||||||
|
- 访问海康官网:https://www.hikvision.com/cn/
|
||||||
|
- 搜索 "MVS SDK" 或 "机器视觉 SDK"
|
||||||
|
- 下载对应操作系统的最新版本
|
||||||
|
|
||||||
|
2. **安装SDK**
|
||||||
|
- 运行安装程序,按照默认设置安装
|
||||||
|
- 安装路径建议使用默认路径
|
||||||
|
|
||||||
|
3. **配置环境变量**
|
||||||
|
- 将SDK安装目录下的bin文件夹添加到系统PATH环境变量
|
||||||
|
- 例如:C:\Program Files\MVS\Runtime\bin\win64_x64
|
||||||
|
|
||||||
|
4. **配置Python环境**
|
||||||
|
- 复制SDK安装目录下的Python绑定文件
|
||||||
|
- 从:C:\Program Files\MVS\Development\Samples\Python\MvImport
|
||||||
|
- 到:项目目录\Python\MvImport
|
||||||
|
|
||||||
|
### 4. 依赖安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install opencv-python
|
||||||
|
pip install numpy
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用指南
|
||||||
|
|
||||||
|
### 主程序
|
||||||
|
|
||||||
|
**功能**:双模式激光检测系统的主程序,集成了相机管理、参数配置、检测算法和可视化界面。
|
||||||
|
|
||||||
|
**运行方式**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**操作流程**:
|
||||||
|
|
||||||
|
1. 启动程序后,系统会枚举连接的相机设备
|
||||||
|
2. 输入要打开的相机索引
|
||||||
|
3. 程序会自动初始化相机并开始取流
|
||||||
|
4. 同时启动参数控制面板
|
||||||
|
5. 系统会自动检测激光目标并显示结果
|
||||||
|
6. 按 `Q` 键退出显示窗口
|
||||||
|
7. 按 `Ctrl+C` 退出程序
|
||||||
|
|
||||||
|
**参数控制**:
|
||||||
|
- 曝光时间:控制相机曝光时长(微秒)
|
||||||
|
- 增益:控制相机信号增益(dB)
|
||||||
|
- 帧率:控制相机采集帧率(fps)
|
||||||
|
- ROI:设置感兴趣区域,提高处理速度
|
||||||
|
- HSV:调节颜色检测范围
|
||||||
|
- 形态学:设置形态学操作参数
|
||||||
|
- 几何参数:设置目标检测的几何条件
|
||||||
|
|
||||||
|
### 相机测试程序
|
||||||
|
|
||||||
|
**功能**:用于测试相机基本功能的验证程序,包括设备枚举、图像采集和显示。
|
||||||
|
|
||||||
|
**运行方式**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python verify_camera.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**操作流程**:
|
||||||
|
|
||||||
|
1. 启动程序后,系统会枚举连接的相机设备
|
||||||
|
2. 输入要打开的相机索引
|
||||||
|
3. 程序会打开相机并开始取流
|
||||||
|
4. 显示相机采集的实时图像
|
||||||
|
5. 按 `Q` 键退出显示窗口
|
||||||
|
6. 按 `Ctrl+C` 退出程序
|
||||||
|
|
||||||
|
**用途**:
|
||||||
|
- 验证相机连接是否正常
|
||||||
|
- 测试相机基本功能
|
||||||
|
- 检查图像质量
|
||||||
|
- 排查相机相关问题
|
||||||
|
|
||||||
|
### 相机控制程序
|
||||||
|
|
||||||
|
**功能**:用于相机参数调节的独立控制界面程序。
|
||||||
|
|
||||||
|
**运行方式**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python camera_control.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**操作流程**:
|
||||||
|
|
||||||
|
1. 启动程序后,系统会自动初始化相机
|
||||||
|
2. 点击 "初始化相机" 按钮枚举设备
|
||||||
|
3. 点击 "打开相机" 按钮打开选中的设备
|
||||||
|
4. 点击 "开始取流" 按钮开始采集图像
|
||||||
|
5. 使用滑块调节曝光时间、增益和帧率
|
||||||
|
6. 点击 "停止取流" 按钮停止采集
|
||||||
|
7. 点击 "关闭相机" 按钮关闭设备
|
||||||
|
|
||||||
|
**用途**:
|
||||||
|
- 快速调节相机参数
|
||||||
|
- 测试不同参数组合的效果
|
||||||
|
- 监控相机状态
|
||||||
|
|
||||||
|
## 代码结构
|
||||||
|
|
||||||
|
```
|
||||||
|
Dart/
|
||||||
|
├── main.py # 主程序:双模式激光检测系统
|
||||||
|
├── camera.py # 相机管理模块
|
||||||
|
├── camera_control.py # 相机控制界面
|
||||||
|
├── camera_hik.py # 海康相机实现
|
||||||
|
├── verify_camera.py # 相机验证程序
|
||||||
|
├── detector.py # 单点光斑检测器
|
||||||
|
├── dot_matrix_detector.py # 点阵灯盘检测器
|
||||||
|
├── arbitrator.py # 模式仲裁器
|
||||||
|
├── config_panel.py # 参数控制面板
|
||||||
|
├── auto_exposure.py # 自动曝光调节
|
||||||
|
├── utils.py # 工具函数
|
||||||
|
├── Python/
|
||||||
|
│ └── MvImport/ # 海康MVS SDK Python绑定
|
||||||
|
└── README.md # 项目说明文档
|
||||||
|
```
|
||||||
|
|
||||||
|
### 核心文件说明
|
||||||
|
|
||||||
|
| 文件 | 主要功能 | 说明 |
|
||||||
|
|------|----------|------|
|
||||||
|
| main.py | 双模式激光检测系统 | 集成所有模块,实现完整的检测流程 |
|
||||||
|
| camera.py | 相机管理 | 完整的相机管理功能和控制界面 |
|
||||||
|
| verify_camera.py | 相机验证 | 用于测试相机基本功能的独立程序 |
|
||||||
|
| detector.py | 单点光斑检测 | 基于HSV颜色空间的激光点检测 |
|
||||||
|
| dot_matrix_detector.py | 点阵灯盘检测 | 基于形态学操作的LED阵列检测 |
|
||||||
|
| arbitrator.py | 模式仲裁 | 智能选择最优检测模式 |
|
||||||
|
| config_panel.py | 参数配置 | 实时参数调节界面 |
|
||||||
|
|
||||||
|
## 常见问题和解决方案
|
||||||
|
|
||||||
|
### 1. 相机连接问题
|
||||||
|
|
||||||
|
**问题**:未找到设备
|
||||||
|
**解决方案**:
|
||||||
|
- 检查相机是否正确连接
|
||||||
|
- 检查USB/GigE接口是否正常
|
||||||
|
- 检查MVS SDK是否正确安装
|
||||||
|
- 检查设备管理器中是否识别到相机
|
||||||
|
|
||||||
|
**问题**:相机无法打开(错误码:2147483648)
|
||||||
|
**解决方案**:
|
||||||
|
- 关闭其他可能占用相机的程序
|
||||||
|
- 重启计算机
|
||||||
|
- 重新安装MVS SDK
|
||||||
|
- 检查相机驱动是否正确安装
|
||||||
|
|
||||||
|
### 2. 图像采集问题
|
||||||
|
|
||||||
|
**问题**:获取图像失败
|
||||||
|
**解决方案**:
|
||||||
|
- 检查相机连接
|
||||||
|
- 检查相机是否被其他程序占用
|
||||||
|
- 重启相机
|
||||||
|
- 调整相机参数
|
||||||
|
|
||||||
|
**问题**:图像显示异常
|
||||||
|
**解决方案**:
|
||||||
|
- 检查像素格式设置
|
||||||
|
- 调整曝光时间和增益
|
||||||
|
- 检查OpenCV安装
|
||||||
|
|
||||||
|
### 3. 检测算法问题
|
||||||
|
|
||||||
|
**问题**:激光点检测不到
|
||||||
|
**解决方案**:
|
||||||
|
- 调整HSV颜色范围
|
||||||
|
- 调整曝光时间和增益
|
||||||
|
- 检查激光光源是否正常
|
||||||
|
- 调整形态学操作参数
|
||||||
|
|
||||||
|
**问题**:误检测较多
|
||||||
|
**解决方案**:
|
||||||
|
- 缩小HSV颜色范围
|
||||||
|
- 增加形态学操作的开运算
|
||||||
|
- 调整几何参数阈值
|
||||||
|
- 设置ROI减小检测范围
|
||||||
|
|
||||||
|
### 4. 性能问题
|
||||||
|
|
||||||
|
**问题**:帧率过低
|
||||||
|
**解决方案**:
|
||||||
|
- 增大ROI范围
|
||||||
|
- 降低图像分辨率
|
||||||
|
- 启用交替帧策略
|
||||||
|
- 关闭不必要的显示窗口
|
||||||
|
|
||||||
|
**问题**:程序卡顿
|
||||||
|
**解决方案**:
|
||||||
|
- 减少同时显示的窗口数量
|
||||||
|
- 降低处理复杂度
|
||||||
|
- 检查计算机性能
|
||||||
|
|
||||||
|
## 系统要求
|
||||||
|
|
||||||
|
### 硬件要求
|
||||||
|
|
||||||
|
- CPU:Intel Core i5或更高
|
||||||
|
- 内存:8GB或更高
|
||||||
|
- 存储:10GB可用空间
|
||||||
|
- 接口:USB 3.0或GigE网络接口
|
||||||
|
|
||||||
|
### 软件要求
|
||||||
|
|
||||||
|
- 操作系统:Windows 10/11 64位
|
||||||
|
- Python:3.7或更高版本
|
||||||
|
- 海康MVS SDK:最新版本
|
||||||
|
- OpenCV:4.0或更高版本
|
||||||
|
- NumPy:1.18或更高版本
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
本项目仅供学习和研究使用,未经授权不得用于商业用途。
|
||||||
|
|
||||||
|
## 联系方式
|
||||||
|
|
||||||
|
如有问题或建议,请联系项目维护人员。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**注意**:使用本系统前,请确保已经正确安装和配置海康MVS SDK,并且相机已经正确连接到计算机。
|
||||||
334
arbitrator.py
Normal file
334
arbitrator.py
Normal file
@ -0,0 +1,334 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
双模式激光识别系统仲裁器
|
||||||
|
|
||||||
|
核心功能:
|
||||||
|
1. 双轨并行处理模式A和模式B的检测结果
|
||||||
|
2. 智能仲裁决策,选择最优模式
|
||||||
|
3. 边界情况处理和冲突解决
|
||||||
|
4. 性能优化策略
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
import time
|
||||||
|
|
||||||
|
class ModeArbitrator:
|
||||||
|
def __init__(self):
|
||||||
|
"""初始化仲裁器"""
|
||||||
|
# 工作模式
|
||||||
|
self.work_mode = 'auto' # auto, forced_single, forced_lamp, dual_display
|
||||||
|
|
||||||
|
# 算法使能状态
|
||||||
|
self.enable_mode_a = True
|
||||||
|
self.enable_mode_b = True
|
||||||
|
|
||||||
|
# 历史状态
|
||||||
|
self.last_mode = None # 上一帧使用的模式
|
||||||
|
self.last_a_score = 0
|
||||||
|
self.last_b_score = 0
|
||||||
|
self.consecutive_mode_count = 0 # 连续使用同一模式的帧数
|
||||||
|
|
||||||
|
# 模式失效检测
|
||||||
|
self.mode_a_failure_count = 0
|
||||||
|
self.mode_b_failure_count = 0
|
||||||
|
self.max_failure_count = 30 # 连续无检测的最大帧数
|
||||||
|
|
||||||
|
# 性能优化
|
||||||
|
self.use_alternating_frames = False # 是否使用交替帧策略
|
||||||
|
self.current_frame_parity = 0 # 帧奇偶性
|
||||||
|
|
||||||
|
# 调试信息
|
||||||
|
self.debug_info = {
|
||||||
|
'mode_a_score': 0,
|
||||||
|
'mode_b_score': 0,
|
||||||
|
'decision_reason': '',
|
||||||
|
'processing_time': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
def set_work_mode(self, mode):
|
||||||
|
"""设置工作模式"""
|
||||||
|
self.work_mode = mode
|
||||||
|
|
||||||
|
def set_algorithm_enable(self, enable_a, enable_b):
|
||||||
|
"""设置算法使能状态"""
|
||||||
|
self.enable_mode_a = enable_a
|
||||||
|
self.enable_mode_b = enable_b
|
||||||
|
|
||||||
|
def set_performance_optimization(self, use_alternating):
|
||||||
|
"""设置性能优化策略"""
|
||||||
|
self.use_alternating_frames = use_alternating
|
||||||
|
|
||||||
|
def calculate_mode_a_score(self, results, frame=None):
|
||||||
|
"""计算模式A的场景适配分"""
|
||||||
|
if not results:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# 高置信度检测物数量(1-3个为佳)
|
||||||
|
count_score = 0
|
||||||
|
if 1 <= len(results) <= 3:
|
||||||
|
count_score = 1.0
|
||||||
|
elif len(results) > 3:
|
||||||
|
count_score = max(0, 1.0 - (len(results) - 3) * 0.2)
|
||||||
|
else:
|
||||||
|
count_score = 0.5
|
||||||
|
|
||||||
|
# 单个目标圆度和面积
|
||||||
|
circularity_scores = []
|
||||||
|
area_scores = []
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
# 圆度得分
|
||||||
|
circularity = result[5] # result格式: (cx, cy, w, h, area, circularity)
|
||||||
|
circ_score = min(circularity / 0.8, 1.0) if circularity > 0.5 else 0
|
||||||
|
circularity_scores.append(circ_score)
|
||||||
|
|
||||||
|
# 面积得分(适中为佳)
|
||||||
|
area = result[4]
|
||||||
|
if 100 <= area <= 2000:
|
||||||
|
area_score = 1.0
|
||||||
|
elif area < 100:
|
||||||
|
area_score = max(0, area / 100)
|
||||||
|
else:
|
||||||
|
area_score = max(0, 1.0 - (area - 2000) / 3000)
|
||||||
|
area_scores.append(area_score)
|
||||||
|
|
||||||
|
# 平均圆度和面积得分
|
||||||
|
avg_circularity_score = np.mean(circularity_scores) if circularity_scores else 0
|
||||||
|
avg_area_score = np.mean(area_scores) if area_scores else 0
|
||||||
|
|
||||||
|
# 离散度系数(目标越分散分越低)
|
||||||
|
dispersion_coefficient = 1.0
|
||||||
|
if len(results) > 1:
|
||||||
|
# 计算所有点之间的平均距离
|
||||||
|
distances = []
|
||||||
|
for i in range(len(results)):
|
||||||
|
for j in range(i + 1, len(results)):
|
||||||
|
dx = results[i][0] - results[j][0]
|
||||||
|
dy = results[i][1] - results[j][1]
|
||||||
|
distances.append(np.sqrt(dx*dx + dy*dy))
|
||||||
|
if distances:
|
||||||
|
avg_distance = np.mean(distances)
|
||||||
|
# 距离越大,离散度越高,得分越低
|
||||||
|
dispersion_coefficient = max(0.5, 1.0 - min(avg_distance / 300, 0.5))
|
||||||
|
|
||||||
|
# 总分计算
|
||||||
|
total_score = (
|
||||||
|
len(results) * 0.3 +
|
||||||
|
avg_circularity_score * 0.4 +
|
||||||
|
avg_area_score * 0.2 +
|
||||||
|
count_score * 0.1
|
||||||
|
) * dispersion_coefficient
|
||||||
|
|
||||||
|
return total_score * 100 # 转换为0-100的分数
|
||||||
|
|
||||||
|
def calculate_mode_b_score(self, results, frame=None):
|
||||||
|
"""计算模式B的场景适配分"""
|
||||||
|
if not results:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# 取置信度最高的结果
|
||||||
|
best_result = max(results, key=lambda x: x['confidence'])
|
||||||
|
|
||||||
|
# 拟合圆置信度
|
||||||
|
confidence_score = min(best_result['confidence'] / 70, 1.0) # 70分为满分
|
||||||
|
|
||||||
|
# 检测到的LED颗粒数
|
||||||
|
led_count = best_result['led_count']
|
||||||
|
led_score = min(np.log(max(led_count, 10)) / np.log(50), 1.0) # 10-50个LED
|
||||||
|
|
||||||
|
# 圆度合理性
|
||||||
|
circularity = best_result['circularity']
|
||||||
|
circularity_score = 0
|
||||||
|
if 0.4 <= circularity <= 0.8:
|
||||||
|
circularity_score = 1.0
|
||||||
|
elif circularity < 0.4:
|
||||||
|
circularity_score = max(0, circularity / 0.4)
|
||||||
|
else:
|
||||||
|
circularity_score = max(0, 1.0 - (circularity - 0.8) / 0.2)
|
||||||
|
|
||||||
|
# 总分计算
|
||||||
|
total_score = (
|
||||||
|
confidence_score * 0.5 +
|
||||||
|
led_score * 0.3 +
|
||||||
|
circularity_score * 0.2
|
||||||
|
) * 100 # 转换为0-100的分数
|
||||||
|
|
||||||
|
return total_score
|
||||||
|
|
||||||
|
def arbitrate(self, mode_a_results, mode_b_results, frame=None):
|
||||||
|
"""
|
||||||
|
智能仲裁决策
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mode_a_results: 模式A的检测结果
|
||||||
|
mode_b_results: 模式B的检测结果
|
||||||
|
frame: 原始帧(可选,用于预筛选)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
selected_mode: 选中的模式 ('mode_a', 'mode_b', None)
|
||||||
|
selected_results: 选中的结果
|
||||||
|
debug_info: 调试信息
|
||||||
|
"""
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
# 重置调试信息
|
||||||
|
self.debug_info = {
|
||||||
|
'mode_a_score': 0,
|
||||||
|
'mode_b_score': 0,
|
||||||
|
'decision_reason': '',
|
||||||
|
'processing_time': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检查使能状态
|
||||||
|
if not self.enable_mode_a and not self.enable_mode_b:
|
||||||
|
self.debug_info['decision_reason'] = 'Both algorithms disabled'
|
||||||
|
self.debug_info['processing_time'] = time.time() - start_time
|
||||||
|
return None, None, self.debug_info
|
||||||
|
|
||||||
|
# 强制模式
|
||||||
|
if self.work_mode == 'forced_single' and self.enable_mode_a:
|
||||||
|
self.debug_info['decision_reason'] = 'Forced single point mode'
|
||||||
|
self.debug_info['processing_time'] = time.time() - start_time
|
||||||
|
return 'mode_a', mode_a_results, self.debug_info
|
||||||
|
|
||||||
|
if self.work_mode == 'forced_lamp' and self.enable_mode_b:
|
||||||
|
self.debug_info['decision_reason'] = 'Forced lamp disk mode'
|
||||||
|
self.debug_info['processing_time'] = time.time() - start_time
|
||||||
|
return 'mode_b', mode_b_results, self.debug_info
|
||||||
|
|
||||||
|
# 双模显示
|
||||||
|
if self.work_mode == 'dual_display':
|
||||||
|
self.debug_info['decision_reason'] = 'Dual display mode'
|
||||||
|
self.debug_info['processing_time'] = time.time() - start_time
|
||||||
|
return 'dual', (mode_a_results, mode_b_results), self.debug_info
|
||||||
|
|
||||||
|
# 性能优化:交替帧策略
|
||||||
|
if self.use_alternating_frames:
|
||||||
|
self.current_frame_parity = 1 - self.current_frame_parity
|
||||||
|
if self.current_frame_parity == 0 and self.enable_mode_a:
|
||||||
|
self.debug_info['decision_reason'] = 'Alternating frame: mode A'
|
||||||
|
self.debug_info['processing_time'] = time.time() - start_time
|
||||||
|
return 'mode_a', mode_a_results, self.debug_info
|
||||||
|
elif self.current_frame_parity == 1 and self.enable_mode_b:
|
||||||
|
self.debug_info['decision_reason'] = 'Alternating frame: mode B'
|
||||||
|
self.debug_info['processing_time'] = time.time() - start_time
|
||||||
|
return 'mode_b', mode_b_results, self.debug_info
|
||||||
|
|
||||||
|
# 计算得分
|
||||||
|
a_score = 0
|
||||||
|
b_score = 0
|
||||||
|
|
||||||
|
if self.enable_mode_a:
|
||||||
|
a_score = self.calculate_mode_a_score(mode_a_results, frame)
|
||||||
|
self.debug_info['mode_a_score'] = a_score
|
||||||
|
|
||||||
|
# 模式失效检测
|
||||||
|
if not mode_a_results:
|
||||||
|
self.mode_a_failure_count += 1
|
||||||
|
else:
|
||||||
|
self.mode_a_failure_count = 0
|
||||||
|
|
||||||
|
if self.enable_mode_b:
|
||||||
|
b_score = self.calculate_mode_b_score(mode_b_results, frame)
|
||||||
|
self.debug_info['mode_b_score'] = b_score
|
||||||
|
|
||||||
|
# 模式失效检测
|
||||||
|
if not mode_b_results:
|
||||||
|
self.mode_b_failure_count += 1
|
||||||
|
else:
|
||||||
|
self.mode_b_failure_count = 0
|
||||||
|
|
||||||
|
# 自动仲裁逻辑
|
||||||
|
selected_mode = None
|
||||||
|
selected_results = None
|
||||||
|
|
||||||
|
# 单模式使能情况
|
||||||
|
if self.enable_mode_a and not self.enable_mode_b:
|
||||||
|
selected_mode = 'mode_a'
|
||||||
|
selected_results = mode_a_results
|
||||||
|
self.debug_info['decision_reason'] = 'Only mode A enabled'
|
||||||
|
elif self.enable_mode_b and not self.enable_mode_a:
|
||||||
|
selected_mode = 'mode_b'
|
||||||
|
selected_results = mode_b_results
|
||||||
|
self.debug_info['decision_reason'] = 'Only mode B enabled'
|
||||||
|
else:
|
||||||
|
# 双模式仲裁
|
||||||
|
if b_score > a_score * 1.2:
|
||||||
|
selected_mode = 'mode_b'
|
||||||
|
selected_results = mode_b_results
|
||||||
|
self.debug_info['decision_reason'] = f'Lamp mode score ({b_score:.1f}) > single mode ({a_score:.1f}) * 1.2'
|
||||||
|
elif a_score > b_score * 1.5:
|
||||||
|
selected_mode = 'mode_a'
|
||||||
|
selected_results = mode_a_results
|
||||||
|
self.debug_info['decision_reason'] = f'Single mode score ({a_score:.1f}) > lamp mode ({b_score:.1f}) * 1.5'
|
||||||
|
else:
|
||||||
|
# 保持上一帧决策(迟滞效应)
|
||||||
|
if self.last_mode:
|
||||||
|
selected_mode = self.last_mode
|
||||||
|
selected_results = mode_a_results if self.last_mode == 'mode_a' else mode_b_results
|
||||||
|
self.debug_info['decision_reason'] = f'Hysteresis:保持{"单点" if self.last_mode == "mode_a" else "灯盘"}模式'
|
||||||
|
else:
|
||||||
|
# 首次决策
|
||||||
|
if a_score > b_score:
|
||||||
|
selected_mode = 'mode_a'
|
||||||
|
selected_results = mode_a_results
|
||||||
|
self.debug_info['decision_reason'] = 'First frame: single mode'
|
||||||
|
else:
|
||||||
|
selected_mode = 'mode_b'
|
||||||
|
selected_results = mode_b_results
|
||||||
|
self.debug_info['decision_reason'] = 'First frame: lamp mode'
|
||||||
|
|
||||||
|
# 更新历史状态
|
||||||
|
if selected_mode == self.last_mode:
|
||||||
|
self.consecutive_mode_count += 1
|
||||||
|
else:
|
||||||
|
self.consecutive_mode_count = 1
|
||||||
|
|
||||||
|
self.last_mode = selected_mode
|
||||||
|
self.last_a_score = a_score
|
||||||
|
self.last_b_score = b_score
|
||||||
|
|
||||||
|
# 处理模式失效
|
||||||
|
if selected_mode == 'mode_a' and self.mode_a_failure_count >= self.max_failure_count:
|
||||||
|
if self.enable_mode_b:
|
||||||
|
selected_mode = 'mode_b'
|
||||||
|
selected_results = mode_b_results
|
||||||
|
self.debug_info['decision_reason'] = 'Mode A failed, switching to mode B'
|
||||||
|
elif selected_mode == 'mode_b' and self.mode_b_failure_count >= self.max_failure_count:
|
||||||
|
if self.enable_mode_a:
|
||||||
|
selected_mode = 'mode_a'
|
||||||
|
selected_results = mode_a_results
|
||||||
|
self.debug_info['decision_reason'] = 'Mode B failed, switching to mode A'
|
||||||
|
|
||||||
|
# 计算处理时间
|
||||||
|
self.debug_info['processing_time'] = time.time() - start_time
|
||||||
|
|
||||||
|
return selected_mode, selected_results, self.debug_info
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
"""重置仲裁器状态"""
|
||||||
|
self.last_mode = None
|
||||||
|
self.last_a_score = 0
|
||||||
|
self.last_b_score = 0
|
||||||
|
self.consecutive_mode_count = 0
|
||||||
|
self.mode_a_failure_count = 0
|
||||||
|
self.mode_b_failure_count = 0
|
||||||
|
self.current_frame_parity = 0
|
||||||
|
self.debug_info = {
|
||||||
|
'mode_a_score': 0,
|
||||||
|
'mode_b_score': 0,
|
||||||
|
'decision_reason': '',
|
||||||
|
'processing_time': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_debug_info(self):
|
||||||
|
"""获取调试信息"""
|
||||||
|
return self.debug_info
|
||||||
|
|
||||||
|
def get_mode_failure_status(self):
|
||||||
|
"""获取模式失效状态"""
|
||||||
|
return {
|
||||||
|
'mode_a_failed': self.mode_a_failure_count >= self.max_failure_count,
|
||||||
|
'mode_b_failed': self.mode_b_failure_count >= self.max_failure_count,
|
||||||
|
'mode_a_failure_count': self.mode_a_failure_count,
|
||||||
|
'mode_b_failure_count': self.mode_b_failure_count
|
||||||
|
}
|
||||||
21
auto_exposure.py
Normal file
21
auto_exposure.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
def auto_exposure_adjust(frame, current_exp):
|
||||||
|
"""
|
||||||
|
根据画面亮度自动降低曝光
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: BGR格式的图像
|
||||||
|
current_exp: 当前曝光时间(微秒)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
new_exp: 新的曝光时间(微秒)
|
||||||
|
adjusted: 是否进行了调整
|
||||||
|
"""
|
||||||
|
mean_val = np.mean(cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY))
|
||||||
|
if mean_val > 250 and current_exp > 1000:
|
||||||
|
new_exp = max(100, current_exp * 0.8) # 降低20%
|
||||||
|
return int(new_exp), True
|
||||||
|
return current_exp, False
|
||||||
367
camera_control.py
Normal file
367
camera_control.py
Normal file
@ -0,0 +1,367 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
相机控制界面
|
||||||
|
|
||||||
|
本模块提供了一个基于Tkinter的相机控制界面,用于调节相机的曝光时间、增益等参数。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from camera import Camera
|
||||||
|
|
||||||
|
class CameraControlPanel:
|
||||||
|
"""
|
||||||
|
相机控制面板类
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, root, camera):
|
||||||
|
"""
|
||||||
|
初始化相机控制面板
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root: 父窗口
|
||||||
|
camera: 相机实例
|
||||||
|
"""
|
||||||
|
self.root = root
|
||||||
|
self.camera = camera
|
||||||
|
self.root.title("相机控制面板")
|
||||||
|
self.root.geometry("500x400")
|
||||||
|
self.root.resizable(True, True)
|
||||||
|
|
||||||
|
# 创建主框架
|
||||||
|
self.main_frame = ttk.Frame(self.root, padding="10")
|
||||||
|
self.main_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
# 相机状态
|
||||||
|
self.status_var = tk.StringVar(value="未初始化")
|
||||||
|
|
||||||
|
# 曝光时间变量
|
||||||
|
self.exp_var = tk.DoubleVar(value=5000.0)
|
||||||
|
self.exp_var.trace("w", self.on_exp_change)
|
||||||
|
|
||||||
|
# 增益变量
|
||||||
|
self.gain_var = tk.DoubleVar(value=0.0)
|
||||||
|
self.gain_var.trace("w", self.on_gain_change)
|
||||||
|
|
||||||
|
# 帧率变量
|
||||||
|
self.fps_var = tk.DoubleVar(value=30.0)
|
||||||
|
self.fps_var.trace("w", self.on_fps_change)
|
||||||
|
|
||||||
|
# 状态标签
|
||||||
|
self.create_status_section()
|
||||||
|
|
||||||
|
# 相机控制
|
||||||
|
self.create_control_section()
|
||||||
|
|
||||||
|
# 参数调节
|
||||||
|
self.create_param_section()
|
||||||
|
|
||||||
|
# 日志区域
|
||||||
|
self.create_log_section()
|
||||||
|
|
||||||
|
# 初始化相机
|
||||||
|
self.init_camera()
|
||||||
|
|
||||||
|
def create_status_section(self):
|
||||||
|
"""
|
||||||
|
创建状态显示区域
|
||||||
|
"""
|
||||||
|
status_frame = ttk.LabelFrame(self.main_frame, text="相机状态", padding="10")
|
||||||
|
status_frame.pack(fill=tk.X, pady=5)
|
||||||
|
|
||||||
|
status_label = ttk.Label(status_frame, textvariable=self.status_var, font=("Arial", 12, "bold"))
|
||||||
|
status_label.pack(anchor=tk.W)
|
||||||
|
|
||||||
|
def create_control_section(self):
|
||||||
|
"""
|
||||||
|
创建相机控制区域
|
||||||
|
"""
|
||||||
|
control_frame = ttk.LabelFrame(self.main_frame, text="相机控制", padding="10")
|
||||||
|
control_frame.pack(fill=tk.X, pady=5)
|
||||||
|
|
||||||
|
# 按钮容器
|
||||||
|
button_frame = ttk.Frame(control_frame)
|
||||||
|
button_frame.pack(fill=tk.X)
|
||||||
|
|
||||||
|
# 初始化按钮
|
||||||
|
self.init_button = ttk.Button(button_frame, text="初始化相机", command=self.init_camera)
|
||||||
|
self.init_button.pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
|
# 打开按钮
|
||||||
|
self.open_button = ttk.Button(button_frame, text="打开相机", command=self.open_camera, state=tk.DISABLED)
|
||||||
|
self.open_button.pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
|
# 开始取流按钮
|
||||||
|
self.start_button = ttk.Button(button_frame, text="开始取流", command=self.start_grabbing, state=tk.DISABLED)
|
||||||
|
self.start_button.pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
|
# 停止取流按钮
|
||||||
|
self.stop_button = ttk.Button(button_frame, text="停止取流", command=self.stop_grabbing, state=tk.DISABLED)
|
||||||
|
self.stop_button.pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
|
# 关闭按钮
|
||||||
|
self.close_button = ttk.Button(button_frame, text="关闭相机", command=self.close_camera, state=tk.DISABLED)
|
||||||
|
self.close_button.pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
|
def create_param_section(self):
|
||||||
|
"""
|
||||||
|
创建参数调节区域
|
||||||
|
"""
|
||||||
|
param_frame = ttk.LabelFrame(self.main_frame, text="参数调节", padding="10")
|
||||||
|
param_frame.pack(fill=tk.X, pady=5)
|
||||||
|
|
||||||
|
# 曝光时间
|
||||||
|
exp_frame = ttk.Frame(param_frame)
|
||||||
|
exp_frame.pack(fill=tk.X, pady=5)
|
||||||
|
|
||||||
|
exp_label = ttk.Label(exp_frame, text="曝光时间 (μs):")
|
||||||
|
exp_label.pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
|
exp_scale = ttk.Scale(exp_frame, from_=100.0, to=50000.0, orient=tk.HORIZONTAL, variable=self.exp_var, length=200)
|
||||||
|
exp_scale.pack(side=tk.LEFT, padx=5, expand=True, fill=tk.X)
|
||||||
|
|
||||||
|
exp_entry = ttk.Entry(exp_frame, textvariable=self.exp_var, width=10)
|
||||||
|
exp_entry.pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
|
# 增益
|
||||||
|
gain_frame = ttk.Frame(param_frame)
|
||||||
|
gain_frame.pack(fill=tk.X, pady=5)
|
||||||
|
|
||||||
|
gain_label = ttk.Label(gain_frame, text="增益 (dB):")
|
||||||
|
gain_label.pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
|
gain_scale = ttk.Scale(gain_frame, from_=0.0, to=48.0, orient=tk.HORIZONTAL, variable=self.gain_var, length=200)
|
||||||
|
gain_scale.pack(side=tk.LEFT, padx=5, expand=True, fill=tk.X)
|
||||||
|
|
||||||
|
gain_entry = ttk.Entry(gain_frame, textvariable=self.gain_var, width=10)
|
||||||
|
gain_entry.pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
|
# 帧率
|
||||||
|
fps_frame = ttk.Frame(param_frame)
|
||||||
|
fps_frame.pack(fill=tk.X, pady=5)
|
||||||
|
|
||||||
|
fps_label = ttk.Label(fps_frame, text="帧率 (fps):")
|
||||||
|
fps_label.pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
|
fps_scale = ttk.Scale(fps_frame, from_=1.0, to=60.0, orient=tk.HORIZONTAL, variable=self.fps_var, length=200)
|
||||||
|
fps_scale.pack(side=tk.LEFT, padx=5, expand=True, fill=tk.X)
|
||||||
|
|
||||||
|
fps_entry = ttk.Entry(fps_frame, textvariable=self.fps_var, width=10)
|
||||||
|
fps_entry.pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
|
def create_log_section(self):
|
||||||
|
"""
|
||||||
|
创建日志显示区域
|
||||||
|
"""
|
||||||
|
log_frame = ttk.LabelFrame(self.main_frame, text="日志", padding="10")
|
||||||
|
log_frame.pack(fill=tk.BOTH, expand=True, pady=5)
|
||||||
|
|
||||||
|
# 创建文本框
|
||||||
|
self.log_text = tk.Text(log_frame, height=10, wrap=tk.WORD)
|
||||||
|
self.log_text.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
# 添加滚动条
|
||||||
|
scrollbar = ttk.Scrollbar(self.log_text, command=self.log_text.yview)
|
||||||
|
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||||
|
self.log_text.config(yscrollcommand=scrollbar.set)
|
||||||
|
|
||||||
|
def log(self, message):
|
||||||
|
"""
|
||||||
|
添加日志信息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: 日志消息
|
||||||
|
"""
|
||||||
|
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
log_message = f"[{timestamp}] {message}\n"
|
||||||
|
self.log_text.insert(tk.END, log_message)
|
||||||
|
self.log_text.see(tk.END)
|
||||||
|
|
||||||
|
def init_camera(self):
|
||||||
|
"""
|
||||||
|
初始化相机
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.log("正在初始化相机...")
|
||||||
|
self.status_var.set("初始化中")
|
||||||
|
|
||||||
|
# 枚举设备
|
||||||
|
device_count = self.camera.enum_devices()
|
||||||
|
if device_count == 0:
|
||||||
|
self.log("未找到相机设备")
|
||||||
|
self.status_var.set("未找到设备")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.log(f"找到 {device_count} 台相机设备")
|
||||||
|
self.status_var.set("就绪")
|
||||||
|
|
||||||
|
# 启用打开按钮
|
||||||
|
self.open_button.config(state=tk.NORMAL)
|
||||||
|
self.log("相机初始化成功")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.log(f"初始化失败: {e}")
|
||||||
|
self.status_var.set("初始化失败")
|
||||||
|
|
||||||
|
def open_camera(self):
|
||||||
|
"""
|
||||||
|
打开相机
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.log("正在打开相机...")
|
||||||
|
self.status_var.set("打开中")
|
||||||
|
|
||||||
|
# 打开设备(默认使用索引0)
|
||||||
|
ret = self.camera.open_device(0)
|
||||||
|
if ret != 0:
|
||||||
|
self.log(f"打开相机失败,错误码: {ret}")
|
||||||
|
self.status_var.set("打开失败")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.log("相机打开成功")
|
||||||
|
self.status_var.set("已打开")
|
||||||
|
|
||||||
|
# 更新参数显示
|
||||||
|
self.exp_var.set(self.camera.current_exp)
|
||||||
|
self.gain_var.set(self.camera.current_gain)
|
||||||
|
self.fps_var.set(self.camera.current_fps)
|
||||||
|
|
||||||
|
# 启用控制按钮
|
||||||
|
self.start_button.config(state=tk.NORMAL)
|
||||||
|
self.close_button.config(state=tk.NORMAL)
|
||||||
|
self.open_button.config(state=tk.DISABLED)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.log(f"打开相机失败: {e}")
|
||||||
|
self.status_var.set("打开失败")
|
||||||
|
|
||||||
|
def start_grabbing(self):
|
||||||
|
"""
|
||||||
|
开始取流
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.log("正在开始取流...")
|
||||||
|
self.status_var.set("取流中")
|
||||||
|
|
||||||
|
ret = self.camera.start_grabbing()
|
||||||
|
if ret != 0:
|
||||||
|
self.log(f"开始取流失败,错误码: {ret}")
|
||||||
|
self.status_var.set("取流失败")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.log("开始取流成功")
|
||||||
|
self.status_var.set("正在取流")
|
||||||
|
|
||||||
|
# 更新按钮状态
|
||||||
|
self.start_button.config(state=tk.DISABLED)
|
||||||
|
self.stop_button.config(state=tk.NORMAL)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.log(f"开始取流失败: {e}")
|
||||||
|
self.status_var.set("取流失败")
|
||||||
|
|
||||||
|
def stop_grabbing(self):
|
||||||
|
"""
|
||||||
|
停止取流
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.log("正在停止取流...")
|
||||||
|
|
||||||
|
ret = self.camera.stop_grabbing()
|
||||||
|
if ret != 0:
|
||||||
|
self.log(f"停止取流失败,错误码: {ret}")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.log("停止取流成功")
|
||||||
|
self.status_var.set("已打开")
|
||||||
|
|
||||||
|
# 更新按钮状态
|
||||||
|
self.stop_button.config(state=tk.DISABLED)
|
||||||
|
self.start_button.config(state=tk.NORMAL)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.log(f"停止取流失败: {e}")
|
||||||
|
|
||||||
|
def close_camera(self):
|
||||||
|
"""
|
||||||
|
关闭相机
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.log("正在关闭相机...")
|
||||||
|
|
||||||
|
self.camera.close_device()
|
||||||
|
|
||||||
|
self.log("相机关闭成功")
|
||||||
|
self.status_var.set("已关闭")
|
||||||
|
|
||||||
|
# 更新按钮状态
|
||||||
|
self.close_button.config(state=tk.DISABLED)
|
||||||
|
self.stop_button.config(state=tk.DISABLED)
|
||||||
|
self.start_button.config(state=tk.DISABLED)
|
||||||
|
self.open_button.config(state=tk.NORMAL)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.log(f"关闭相机失败: {e}")
|
||||||
|
|
||||||
|
def on_exp_change(self, *args):
|
||||||
|
"""
|
||||||
|
曝光时间变化回调
|
||||||
|
"""
|
||||||
|
if self.camera.is_open:
|
||||||
|
try:
|
||||||
|
exp_value = self.exp_var.get()
|
||||||
|
ret = self.camera.set_exposure_time(exp_value)
|
||||||
|
if ret == 0:
|
||||||
|
self.log(f"曝光时间设置为: {exp_value} μs")
|
||||||
|
except Exception as e:
|
||||||
|
self.log(f"设置曝光时间失败: {e}")
|
||||||
|
|
||||||
|
def on_gain_change(self, *args):
|
||||||
|
"""
|
||||||
|
增益变化回调
|
||||||
|
"""
|
||||||
|
if self.camera.is_open:
|
||||||
|
try:
|
||||||
|
gain_value = self.gain_var.get()
|
||||||
|
ret = self.camera.set_gain(gain_value)
|
||||||
|
if ret == 0:
|
||||||
|
self.log(f"增益设置为: {gain_value} dB")
|
||||||
|
except Exception as e:
|
||||||
|
self.log(f"设置增益失败: {e}")
|
||||||
|
|
||||||
|
def on_fps_change(self, *args):
|
||||||
|
"""
|
||||||
|
帧率变化回调
|
||||||
|
"""
|
||||||
|
if self.camera.is_open:
|
||||||
|
try:
|
||||||
|
fps_value = self.fps_var.get()
|
||||||
|
ret = self.camera.set_frame_rate(fps_value)
|
||||||
|
if ret == 0:
|
||||||
|
self.log(f"帧率设置为: {fps_value} fps")
|
||||||
|
except Exception as e:
|
||||||
|
self.log(f"设置帧率失败: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
"""
|
||||||
|
测试相机控制面板
|
||||||
|
"""
|
||||||
|
import tkinter as tk
|
||||||
|
|
||||||
|
# 创建根窗口
|
||||||
|
root = tk.Tk()
|
||||||
|
|
||||||
|
# 创建相机实例
|
||||||
|
camera = Camera()
|
||||||
|
|
||||||
|
# 创建控制面板
|
||||||
|
app = CameraControlPanel(root, camera)
|
||||||
|
|
||||||
|
# 运行主循环
|
||||||
|
try:
|
||||||
|
root.mainloop()
|
||||||
|
finally:
|
||||||
|
# 确保关闭相机
|
||||||
|
camera.close_device()
|
||||||
460
camera_hik.py
Normal file
460
camera_hik.py
Normal file
@ -0,0 +1,460 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import sys
|
||||||
|
import ctypes
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
|
||||||
|
# 添加SDK路径
|
||||||
|
sys.path.append('Python/MvImport')
|
||||||
|
from MvCameraControl_class import *
|
||||||
|
from MvErrorDefine_const import *
|
||||||
|
from CameraParams_header import *
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
# 枚举值定义
|
||||||
|
MV_EXPOSURE_AUTO_MODE_OFF = 0
|
||||||
|
MV_GAIN_AUTO_MODE_OFF = 0
|
||||||
|
MV_BALANCE_WHITE_AUTO_OFF = 0
|
||||||
|
|
||||||
|
class CameraHik:
|
||||||
|
def __init__(self):
|
||||||
|
self.cam = None
|
||||||
|
self.device_list = MV_CC_DEVICE_INFO_LIST()
|
||||||
|
self.is_open = False
|
||||||
|
self.is_grabbing = False
|
||||||
|
self.b_exit = False
|
||||||
|
self.work_thread = None
|
||||||
|
self.current_exp = 5000.0 # 当前曝光时间(微秒)
|
||||||
|
self.current_gain = 0.0 # 当前增益(dB)
|
||||||
|
self.current_fps = 30.0 # 当前帧率
|
||||||
|
|
||||||
|
# 将返回的错误码转换为十六进制显示
|
||||||
|
def to_hex_str(self, num):
|
||||||
|
cha_dic = {10: 'a', 11: 'b', 12: 'c', 13: 'd', 14: 'e', 15: 'f'}
|
||||||
|
hex_str = ""
|
||||||
|
if num < 0:
|
||||||
|
num = num + 2 ** 32
|
||||||
|
while num >= 16:
|
||||||
|
digit = num % 16
|
||||||
|
hex_str = cha_dic.get(digit, str(digit)) + hex_str
|
||||||
|
num //= 16
|
||||||
|
hex_str = cha_dic.get(num, str(num)) + hex_str
|
||||||
|
return hex_str
|
||||||
|
|
||||||
|
# Decoding Characters
|
||||||
|
def decoding_char(self, ctypes_char_array):
|
||||||
|
"""
|
||||||
|
安全地从 ctypes 字符数组中解码出字符串。
|
||||||
|
适用于 Python 2.x 和 3.x,以及 32/64 位环境。
|
||||||
|
"""
|
||||||
|
byte_str = memoryview(ctypes_char_array).tobytes()
|
||||||
|
|
||||||
|
# 在第一个空字符处截断
|
||||||
|
null_index = byte_str.find(b'\x00')
|
||||||
|
if null_index != -1:
|
||||||
|
byte_str = byte_str[:null_index]
|
||||||
|
|
||||||
|
# 多编码尝试解码
|
||||||
|
for encoding in ['gbk', 'utf-8', 'latin-1']:
|
||||||
|
try:
|
||||||
|
return byte_str.decode(encoding)
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 如果所有编码都失败,使用替换策略
|
||||||
|
return byte_str.decode('latin-1', errors='replace')
|
||||||
|
|
||||||
|
# 枚举相机
|
||||||
|
def enum_devices(self):
|
||||||
|
self.device_list = MV_CC_DEVICE_INFO_LIST()
|
||||||
|
n_layer_type = (MV_GIGE_DEVICE | MV_USB_DEVICE)
|
||||||
|
ret = MvCamera.MV_CC_EnumDevices(n_layer_type, self.device_list)
|
||||||
|
if ret != 0:
|
||||||
|
print(f"枚举设备失败! ret = :{self.to_hex_str(ret)}")
|
||||||
|
return ret
|
||||||
|
|
||||||
|
if self.device_list.nDeviceNum == 0:
|
||||||
|
print("未找到设备")
|
||||||
|
return ret
|
||||||
|
print(f"找到 {self.device_list.nDeviceNum} 台设备!")
|
||||||
|
|
||||||
|
dev_list = []
|
||||||
|
target_model_found = False
|
||||||
|
|
||||||
|
for i in range(0, self.device_list.nDeviceNum):
|
||||||
|
mvcc_dev_info = cast(self.device_list.pDeviceInfo[i], POINTER(MV_CC_DEVICE_INFO)).contents
|
||||||
|
if mvcc_dev_info.nTLayerType == MV_GIGE_DEVICE:
|
||||||
|
print(f"\ngige device: [{i}]")
|
||||||
|
user_defined_name = self.decoding_char(mvcc_dev_info.SpecialInfo.stGigEInfo.chUserDefinedName)
|
||||||
|
model_name = self.decoding_char(mvcc_dev_info.SpecialInfo.stGigEInfo.chModelName)
|
||||||
|
print(f"device user define name: {user_defined_name}")
|
||||||
|
print(f"device model name: {model_name}")
|
||||||
|
|
||||||
|
# 检查是否为目标型号
|
||||||
|
if "MV-CS016-10UC" in model_name:
|
||||||
|
print("✓ 找到目标相机型号: MV-CS016-10UC")
|
||||||
|
target_model_found = True
|
||||||
|
|
||||||
|
nip1 = ((mvcc_dev_info.SpecialInfo.stGigEInfo.nCurrentIp & 0xff000000) >> 24)
|
||||||
|
nip2 = ((mvcc_dev_info.SpecialInfo.stGigEInfo.nCurrentIp & 0x00ff0000) >> 16)
|
||||||
|
nip3 = ((mvcc_dev_info.SpecialInfo.stGigEInfo.nCurrentIp & 0x0000ff00) >> 8)
|
||||||
|
nip4 = (mvcc_dev_info.SpecialInfo.stGigEInfo.nCurrentIp & 0x000000ff)
|
||||||
|
print(f"current ip: {nip1}.{nip2}.{nip3}.{nip4} ")
|
||||||
|
dev_list.append(f"[{i}]GigE: {user_defined_name} {model_name}({nip1}.{nip2}.{nip3}.{nip4})")
|
||||||
|
elif mvcc_dev_info.nTLayerType == MV_USB_DEVICE:
|
||||||
|
print(f"\nu3v device: [{i}]")
|
||||||
|
user_defined_name = self.decoding_char(mvcc_dev_info.SpecialInfo.stUsb3VInfo.chUserDefinedName)
|
||||||
|
model_name = self.decoding_char(mvcc_dev_info.SpecialInfo.stUsb3VInfo.chModelName)
|
||||||
|
print(f"device user define name: {user_defined_name}")
|
||||||
|
print(f"device model name: {model_name}")
|
||||||
|
|
||||||
|
# 检查是否为目标型号
|
||||||
|
if "MV-CS016-10UC" in model_name:
|
||||||
|
print("✓ 找到目标相机型号: MV-CS016-10UC")
|
||||||
|
target_model_found = True
|
||||||
|
|
||||||
|
str_serial_number = ""
|
||||||
|
for per in mvcc_dev_info.SpecialInfo.stUsb3VInfo.chSerialNumber:
|
||||||
|
if per == 0:
|
||||||
|
break
|
||||||
|
str_serial_number = str_serial_number + chr(per)
|
||||||
|
print(f"user serial number: {str_serial_number}")
|
||||||
|
dev_list.append(f"[{i}]USB: {user_defined_name} {model_name}({str_serial_number})")
|
||||||
|
|
||||||
|
# 打印设备列表
|
||||||
|
print("\n设备列表:")
|
||||||
|
for i, dev in enumerate(dev_list):
|
||||||
|
print(f"{i}: {dev}")
|
||||||
|
|
||||||
|
# 提示用户是否找到目标型号
|
||||||
|
if not target_model_found:
|
||||||
|
print("\n⚠️ 警告: 未找到目标相机型号 MV-CS016-10UC")
|
||||||
|
print("⚠️ 系统仍将尝试使用找到的设备,但可能无法正常工作")
|
||||||
|
|
||||||
|
return self.device_list.nDeviceNum
|
||||||
|
|
||||||
|
# 打开相机
|
||||||
|
def open_device(self, n_index):
|
||||||
|
if self.is_open:
|
||||||
|
print("相机已经打开!")
|
||||||
|
return MV_E_CALLORDER
|
||||||
|
|
||||||
|
if n_index < 0 or n_index >= self.device_list.nDeviceNum:
|
||||||
|
print("请选择有效的相机索引!")
|
||||||
|
return MV_E_CALLORDER
|
||||||
|
|
||||||
|
# 选择设备并创建句柄
|
||||||
|
st_device_list = cast(self.device_list.pDeviceInfo[n_index], POINTER(MV_CC_DEVICE_INFO)).contents
|
||||||
|
self.cam = MvCamera()
|
||||||
|
|
||||||
|
# 句柄占用保护
|
||||||
|
ret = self.cam.MV_CC_CreateHandle(st_device_list)
|
||||||
|
if ret != 0:
|
||||||
|
print(f"创建设备句柄失败! ret = {self.to_hex_str(ret)}")
|
||||||
|
# 尝试清理可能的占用
|
||||||
|
try:
|
||||||
|
self.cam.MV_CC_DestroyHandle()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return ret
|
||||||
|
|
||||||
|
# 打开设备
|
||||||
|
ret = self.cam.MV_CC_OpenDevice()
|
||||||
|
if ret != 0:
|
||||||
|
print(f"打开设备失败! ret = {self.to_hex_str(ret)}")
|
||||||
|
try:
|
||||||
|
self.cam.MV_CC_DestroyHandle()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return ret
|
||||||
|
|
||||||
|
print("设备打开成功!")
|
||||||
|
self.is_open = True
|
||||||
|
|
||||||
|
# 设置触发模式为off
|
||||||
|
ret = self.cam.MV_CC_SetEnumValue("TriggerMode", MV_TRIGGER_MODE_OFF)
|
||||||
|
if ret != 0:
|
||||||
|
print(f"设置触发模式失败! ret = {self.to_hex_str(ret)}")
|
||||||
|
|
||||||
|
# 设置像素格式为 BGR8
|
||||||
|
ret = self.cam.MV_CC_SetEnumValue("PixelFormat", PixelType_Gvsp_BGR8_Packed)
|
||||||
|
if ret != 0:
|
||||||
|
print(f"设置像素格式失败! ret = {self.to_hex_str(ret)}")
|
||||||
|
|
||||||
|
# 设置连续采集模式
|
||||||
|
ret = self.cam.MV_CC_SetEnumValue("AcquisitionMode", 2) # Continuous
|
||||||
|
if ret != 0:
|
||||||
|
print(f"设置连续采集模式失败! ret = {self.to_hex_str(ret)}")
|
||||||
|
|
||||||
|
# 防过曝设置
|
||||||
|
self.setup_anti_overexposure()
|
||||||
|
|
||||||
|
return MV_OK
|
||||||
|
|
||||||
|
# 防过曝参数设置
|
||||||
|
def setup_anti_overexposure(self):
|
||||||
|
"""设置防过曝参数(点阵灯盘专用)"""
|
||||||
|
# 关闭自动曝光,设置初始曝光时间(微秒)
|
||||||
|
ret = self.cam.MV_CC_SetEnumValue("ExposureAuto", MV_EXPOSURE_AUTO_MODE_OFF)
|
||||||
|
if ret != 0:
|
||||||
|
print(f"设置曝光自动模式失败! ret = {self.to_hex_str(ret)}")
|
||||||
|
|
||||||
|
# 点阵灯盘初始曝光时间:3000-5000微秒
|
||||||
|
initial_exp = 4000.0
|
||||||
|
ret = self.cam.MV_CC_SetFloatValue("ExposureTime", initial_exp)
|
||||||
|
if ret != 0:
|
||||||
|
print(f"设置曝光时间失败! ret = {self.to_hex_str(ret)}")
|
||||||
|
else:
|
||||||
|
self.current_exp = initial_exp
|
||||||
|
print(f"初始曝光时间设置为: {initial_exp}μs")
|
||||||
|
|
||||||
|
# 关闭自动增益
|
||||||
|
ret = self.cam.MV_CC_SetEnumValue("GainAuto", MV_GAIN_AUTO_MODE_OFF)
|
||||||
|
if ret != 0:
|
||||||
|
print(f"设置增益自动模式失败! ret = {self.to_hex_str(ret)}")
|
||||||
|
|
||||||
|
ret = self.cam.MV_CC_SetFloatValue("Gain", 0.0)
|
||||||
|
if ret != 0:
|
||||||
|
print(f"设置增益失败! ret = {self.to_hex_str(ret)}")
|
||||||
|
else:
|
||||||
|
self.current_gain = 0.0
|
||||||
|
print("初始增益设置为: 0dB")
|
||||||
|
|
||||||
|
# 关闭自动白平衡(识别绿色LED时必需)
|
||||||
|
ret = self.cam.MV_CC_SetEnumValue("BalanceWhiteAuto", MV_BALANCE_WHITE_AUTO_OFF)
|
||||||
|
if ret != 0:
|
||||||
|
print(f"设置白平衡自动模式失败! ret = {self.to_hex_str(ret)}")
|
||||||
|
else:
|
||||||
|
print("自动白平衡已关闭")
|
||||||
|
|
||||||
|
# 实时调节接口
|
||||||
|
def set_exposure_time(self, microseconds):
|
||||||
|
"""设置曝光时间(微秒)"""
|
||||||
|
if not self.is_open:
|
||||||
|
return MV_E_CALLORDER
|
||||||
|
|
||||||
|
ret = self.cam.MV_CC_SetFloatValue("ExposureTime", microseconds)
|
||||||
|
if ret != 0:
|
||||||
|
print(f"设置曝光时间失败! ret = {self.to_hex_str(ret)}")
|
||||||
|
return ret
|
||||||
|
|
||||||
|
self.current_exp = microseconds
|
||||||
|
return MV_OK
|
||||||
|
|
||||||
|
def set_gain(self, db):
|
||||||
|
"""设置增益(dB)"""
|
||||||
|
if not self.is_open:
|
||||||
|
return MV_E_CALLORDER
|
||||||
|
|
||||||
|
ret = self.cam.MV_CC_SetFloatValue("Gain", db)
|
||||||
|
if ret != 0:
|
||||||
|
print(f"设置增益失败! ret = {self.to_hex_str(ret)}")
|
||||||
|
return ret
|
||||||
|
|
||||||
|
self.current_gain = db
|
||||||
|
return MV_OK
|
||||||
|
|
||||||
|
def set_frame_rate(self, fps):
|
||||||
|
"""设置帧率"""
|
||||||
|
if not self.is_open:
|
||||||
|
return MV_E_CALLORDER
|
||||||
|
|
||||||
|
ret = self.cam.MV_CC_SetFloatValue("AcquisitionFrameRate", fps)
|
||||||
|
if ret != 0:
|
||||||
|
print(f"设置帧率失败! ret = {self.to_hex_str(ret)}")
|
||||||
|
return ret
|
||||||
|
|
||||||
|
self.current_fps = fps
|
||||||
|
return MV_OK
|
||||||
|
|
||||||
|
def set_roi(self, x, y, w, h):
|
||||||
|
"""设置ROI"""
|
||||||
|
if not self.is_open:
|
||||||
|
return MV_E_CALLORDER
|
||||||
|
|
||||||
|
# 停止取流
|
||||||
|
if self.is_grabbing:
|
||||||
|
self.stop_grabbing()
|
||||||
|
|
||||||
|
# 设置ROI参数
|
||||||
|
ret = self.cam.MV_CC_SetIntValue("OffsetX", x)
|
||||||
|
if ret != 0:
|
||||||
|
print(f"设置OffsetX失败! ret = {self.to_hex_str(ret)}")
|
||||||
|
return ret
|
||||||
|
|
||||||
|
ret = self.cam.MV_CC_SetIntValue("OffsetY", y)
|
||||||
|
if ret != 0:
|
||||||
|
print(f"设置OffsetY失败! ret = {self.to_hex_str(ret)}")
|
||||||
|
return ret
|
||||||
|
|
||||||
|
ret = self.cam.MV_CC_SetIntValue("Width", w)
|
||||||
|
if ret != 0:
|
||||||
|
print(f"设置Width失败! ret = {self.to_hex_str(ret)}")
|
||||||
|
return ret
|
||||||
|
|
||||||
|
ret = self.cam.MV_CC_SetIntValue("Height", h)
|
||||||
|
if ret != 0:
|
||||||
|
print(f"设置Height失败! ret = {self.to_hex_str(ret)}")
|
||||||
|
return ret
|
||||||
|
|
||||||
|
# 重新开始取流
|
||||||
|
if self.is_grabbing:
|
||||||
|
self.start_grabbing()
|
||||||
|
|
||||||
|
return MV_OK
|
||||||
|
|
||||||
|
# 开始取流
|
||||||
|
def start_grabbing(self):
|
||||||
|
if not self.is_open:
|
||||||
|
print("相机未打开!")
|
||||||
|
return MV_E_CALLORDER
|
||||||
|
|
||||||
|
if self.is_grabbing:
|
||||||
|
print("已经开始取流!")
|
||||||
|
return MV_E_CALLORDER
|
||||||
|
|
||||||
|
self.b_exit = False
|
||||||
|
ret = self.cam.MV_CC_StartGrabbing()
|
||||||
|
if ret != 0:
|
||||||
|
print(f"开始取流失败! ret = {self.to_hex_str(ret)}")
|
||||||
|
return ret
|
||||||
|
|
||||||
|
self.is_grabbing = True
|
||||||
|
print("开始取流成功!")
|
||||||
|
|
||||||
|
# 启动取图线程
|
||||||
|
self.work_thread = threading.Thread(target=self.work_thread_func)
|
||||||
|
self.work_thread.daemon = True
|
||||||
|
self.work_thread.start()
|
||||||
|
|
||||||
|
return MV_OK
|
||||||
|
|
||||||
|
# 停止取流
|
||||||
|
def stop_grabbing(self):
|
||||||
|
if not self.is_open:
|
||||||
|
print("相机未打开!")
|
||||||
|
return MV_E_CALLORDER
|
||||||
|
|
||||||
|
if not self.is_grabbing:
|
||||||
|
print("未开始取流!")
|
||||||
|
return MV_E_CALLORDER
|
||||||
|
|
||||||
|
self.b_exit = True
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
ret = self.cam.MV_CC_StopGrabbing()
|
||||||
|
if ret != 0:
|
||||||
|
print(f"停止取流失败! ret = {self.to_hex_str(ret)}")
|
||||||
|
return ret
|
||||||
|
|
||||||
|
self.is_grabbing = False
|
||||||
|
print("停止取流成功!")
|
||||||
|
|
||||||
|
return MV_OK
|
||||||
|
|
||||||
|
# 关闭相机
|
||||||
|
def close_device(self):
|
||||||
|
if self.is_grabbing:
|
||||||
|
self.stop_grabbing()
|
||||||
|
|
||||||
|
if self.is_open:
|
||||||
|
ret = self.cam.MV_CC_CloseDevice()
|
||||||
|
if ret != 0:
|
||||||
|
print(f"关闭设备失败! ret = {self.to_hex_str(ret)}")
|
||||||
|
|
||||||
|
# 销毁句柄
|
||||||
|
self.cam.MV_CC_DestroyHandle()
|
||||||
|
self.is_open = False
|
||||||
|
print("设备关闭成功!")
|
||||||
|
|
||||||
|
# 取图线程函数
|
||||||
|
def work_thread_func(self):
|
||||||
|
st_out_frame = MV_FRAME_OUT()
|
||||||
|
memset(byref(st_out_frame), 0, sizeof(st_out_frame))
|
||||||
|
|
||||||
|
timeout_count = 0 # 超时计数器
|
||||||
|
max_timeout_count = 3 # 最大超时次数
|
||||||
|
|
||||||
|
while not self.b_exit:
|
||||||
|
# 获取图像
|
||||||
|
ret = self.cam.MV_CC_GetImageBuffer(st_out_frame, 1000)
|
||||||
|
|
||||||
|
if ret == 0:
|
||||||
|
timeout_count = 0 # 重置超时计数器
|
||||||
|
# 打印图像信息
|
||||||
|
print(f"获取一帧图像: 宽度[{st_out_frame.stFrameInfo.nWidth}], 高度[{st_out_frame.stFrameInfo.nHeight}], 帧数[{st_out_frame.stFrameInfo.nFrameNum}]")
|
||||||
|
|
||||||
|
# 转换为 OpenCV 格式
|
||||||
|
try:
|
||||||
|
# 计算数据大小
|
||||||
|
data_size = st_out_frame.stFrameInfo.nWidth * st_out_frame.stFrameInfo.nHeight * 3
|
||||||
|
|
||||||
|
# 从缓冲区复制数据
|
||||||
|
frame_data = np.ctypeslib.as_array(st_out_frame.pBufAddr, shape=(data_size,))
|
||||||
|
frame = frame_data.reshape((st_out_frame.stFrameInfo.nHeight, st_out_frame.stFrameInfo.nWidth, 3))
|
||||||
|
|
||||||
|
# 显示图像
|
||||||
|
cv2.imshow("Camera", frame)
|
||||||
|
key = cv2.waitKey(1) & 0xFF
|
||||||
|
if key == ord('q'):
|
||||||
|
self.b_exit = True
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f"处理图像失败: {e}")
|
||||||
|
finally:
|
||||||
|
# 释放缓存
|
||||||
|
try:
|
||||||
|
self.cam.MV_CC_FreeImageBuffer(st_out_frame)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"释放图像缓存失败: {e}")
|
||||||
|
else:
|
||||||
|
print(f"获取图像失败! ret = {self.to_hex_str(ret)}")
|
||||||
|
timeout_count += 1
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
# 连续超时3次,触发重连
|
||||||
|
if timeout_count >= max_timeout_count:
|
||||||
|
print("连续超时,尝试重连相机...")
|
||||||
|
self._reconnect_camera()
|
||||||
|
timeout_count = 0
|
||||||
|
|
||||||
|
cv2.destroyAllWindows()
|
||||||
|
|
||||||
|
def _reconnect_camera(self):
|
||||||
|
"""重连相机"""
|
||||||
|
try:
|
||||||
|
# 停止取流
|
||||||
|
if self.is_grabbing:
|
||||||
|
print("停止取流...")
|
||||||
|
self.stop_grabbing()
|
||||||
|
|
||||||
|
# 关闭设备
|
||||||
|
print("关闭设备...")
|
||||||
|
self.close_device()
|
||||||
|
|
||||||
|
# 重新枚举设备
|
||||||
|
print("重新枚举设备...")
|
||||||
|
device_count = self.enum_devices()
|
||||||
|
if device_count == 0:
|
||||||
|
print("重连失败:未找到设备")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 重新打开设备(默认使用索引0)
|
||||||
|
print("重新打开设备...")
|
||||||
|
ret = self.open_device(0)
|
||||||
|
if ret != 0:
|
||||||
|
print(f"重连失败:打开设备失败! ret = {self.to_hex_str(ret)}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 重新开始取流
|
||||||
|
print("重新开始取流...")
|
||||||
|
ret = self.start_grabbing()
|
||||||
|
if ret != 0:
|
||||||
|
print(f"重连失败:开始取流失败! ret = {self.to_hex_str(ret)}")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("相机重连成功!")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"重连相机失败: {e}")
|
||||||
680
config_panel.py
Normal file
680
config_panel.py
Normal file
@ -0,0 +1,680 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
参数控制面板配置文件
|
||||||
|
|
||||||
|
参数说明:
|
||||||
|
|
||||||
|
1. 曝光时间 (exposure)
|
||||||
|
- 范围:100-50000微秒
|
||||||
|
- 作用:控制相机传感器的曝光时间
|
||||||
|
- 效果:
|
||||||
|
- 增大:画面变亮,激光点更明显,但可能过曝
|
||||||
|
- 减小:画面变暗,减少过曝,激光点更清晰
|
||||||
|
- 建议值:5000微秒(激光检测默认值),光线暗时可适当增加
|
||||||
|
|
||||||
|
2. 增益 (gain)
|
||||||
|
- 范围:0-24dB
|
||||||
|
- 作用:放大传感器信号,增强画面亮度
|
||||||
|
- 效果:
|
||||||
|
- 增大:画面变亮,噪声也会增加
|
||||||
|
- 减小:画面变暗,噪声减少
|
||||||
|
- 建议值:0dB(激光检测默认值),仅在光线极暗时使用
|
||||||
|
|
||||||
|
3. 帧率 (fps)
|
||||||
|
- 范围:1-120 FPS
|
||||||
|
- 作用:控制相机采集图像的频率
|
||||||
|
- 效果:
|
||||||
|
- 增大:画面流畅度提高,CPU占用增加
|
||||||
|
- 减小:画面流畅度降低,CPU占用减少
|
||||||
|
- 建议值:30 FPS(平衡流畅度和性能)
|
||||||
|
|
||||||
|
4. ROI设置 (roi_x, roi_y, roi_width, roi_height)
|
||||||
|
- 作用:设置感兴趣区域,仅处理指定区域的图像
|
||||||
|
- 效果:
|
||||||
|
- 减小ROI:提高处理速度,减少CPU占用
|
||||||
|
- 增大ROI:处理范围更广,CPU占用增加
|
||||||
|
- 建议值:默认全画幅(0, 0, 640, 480),激光点固定时可缩小ROI
|
||||||
|
|
||||||
|
5. HSV调节
|
||||||
|
- H通道 (H_low, H_high):控制颜色范围
|
||||||
|
- 范围:0-255
|
||||||
|
- 作用:识别绿色激光的颜色范围
|
||||||
|
- 建议值:35-85(绿色激光默认范围)
|
||||||
|
- S通道 (S_low, S_high):控制饱和度范围
|
||||||
|
- 范围:0-255
|
||||||
|
- 作用:区分激光点和背景
|
||||||
|
- 建议值:80-255(过滤低饱和度背景)
|
||||||
|
- V通道 (V_low, V_high):控制亮度范围
|
||||||
|
- 范围:0-255
|
||||||
|
- 作用:区分激光点和暗背景
|
||||||
|
- 建议值:80-255(过滤暗背景)
|
||||||
|
"""
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk
|
||||||
|
import threading
|
||||||
|
import queue
|
||||||
|
|
||||||
|
class ConfigPanel:
|
||||||
|
def __init__(self, update_callback):
|
||||||
|
"""
|
||||||
|
初始化参数控制面板
|
||||||
|
|
||||||
|
Args:
|
||||||
|
update_callback: 参数更新回调函数
|
||||||
|
"""
|
||||||
|
self.update_callback = update_callback
|
||||||
|
self.root = None
|
||||||
|
self.queue = queue.Queue()
|
||||||
|
self.thread = None
|
||||||
|
|
||||||
|
# 默认参数
|
||||||
|
self.defaults = {
|
||||||
|
'exposure': 5000, # 曝光时间(微秒)
|
||||||
|
'gain': 0, # 增益(dB)
|
||||||
|
'fps': 60, # 帧率(FPS)
|
||||||
|
'roi_x': 0, # ROI起始X坐标
|
||||||
|
'roi_y': 0, # ROI起始Y坐标
|
||||||
|
'roi_width': 640, # ROI宽度
|
||||||
|
'roi_height': 480, # ROI高度
|
||||||
|
'h_low': 35, # H通道最小值
|
||||||
|
'h_high': 85, # H通道最大值
|
||||||
|
's_low': 80, # S通道最小值
|
||||||
|
's_high': 255, # S通道最大值
|
||||||
|
'v_low': 80, # V通道最小值
|
||||||
|
'v_high': 255 # V通道最大值
|
||||||
|
}
|
||||||
|
|
||||||
|
# 当前参数
|
||||||
|
self.current = self.defaults.copy()
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""启动参数控制面板线程"""
|
||||||
|
self.thread = threading.Thread(target=self._run)
|
||||||
|
self.thread.daemon = True
|
||||||
|
self.thread.start()
|
||||||
|
|
||||||
|
def _run(self):
|
||||||
|
"""运行Tkinter主循环"""
|
||||||
|
self.root = tk.Tk()
|
||||||
|
self.root.title("参数控制面板")
|
||||||
|
self.root.geometry("400x600")
|
||||||
|
|
||||||
|
# 创建主框架
|
||||||
|
main_frame = ttk.Frame(self.root, padding="10")
|
||||||
|
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
# 创建标签页
|
||||||
|
notebook = ttk.Notebook(main_frame)
|
||||||
|
notebook.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
# 曝光增益页面
|
||||||
|
exp_gain_frame = ttk.Frame(notebook, padding="10")
|
||||||
|
notebook.add(exp_gain_frame, text="曝光增益")
|
||||||
|
|
||||||
|
# 帧率页面
|
||||||
|
fps_frame = ttk.Frame(notebook, padding="10")
|
||||||
|
notebook.add(fps_frame, text="帧率")
|
||||||
|
|
||||||
|
# ROI页面
|
||||||
|
roi_frame = ttk.Frame(notebook, padding="10")
|
||||||
|
notebook.add(roi_frame, text="ROI设置")
|
||||||
|
|
||||||
|
# HSV页面
|
||||||
|
hsv_frame = ttk.Frame(notebook, padding="10")
|
||||||
|
notebook.add(hsv_frame, text="HSV调节")
|
||||||
|
|
||||||
|
# 形态学参数页面
|
||||||
|
morph_frame = ttk.Frame(notebook, padding="10")
|
||||||
|
notebook.add(morph_frame, text="形态学参数")
|
||||||
|
|
||||||
|
# 几何参数页面
|
||||||
|
geometry_frame = ttk.Frame(notebook, padding="10")
|
||||||
|
notebook.add(geometry_frame, text="几何参数")
|
||||||
|
|
||||||
|
# 检测模式页面
|
||||||
|
mode_frame = ttk.Frame(notebook, padding="10")
|
||||||
|
notebook.add(mode_frame, text="检测模式")
|
||||||
|
|
||||||
|
# 曝光滑块
|
||||||
|
self._create_exposure_controls(exp_gain_frame)
|
||||||
|
|
||||||
|
# 增益滑块
|
||||||
|
self._create_gain_controls(exp_gain_frame)
|
||||||
|
|
||||||
|
# 帧率滑块
|
||||||
|
self._create_fps_controls(fps_frame)
|
||||||
|
|
||||||
|
# ROI设置
|
||||||
|
self._create_roi_controls(roi_frame)
|
||||||
|
|
||||||
|
# HSV调节
|
||||||
|
self._create_hsv_controls(hsv_frame)
|
||||||
|
|
||||||
|
# 形态学参数设置
|
||||||
|
self._create_morph_controls(morph_frame)
|
||||||
|
|
||||||
|
# 几何参数设置
|
||||||
|
self._create_geometry_controls(geometry_frame)
|
||||||
|
|
||||||
|
# 检测模式设置
|
||||||
|
self._create_mode_controls(mode_frame)
|
||||||
|
|
||||||
|
# 启动队列处理
|
||||||
|
self.root.after(100, self._process_queue)
|
||||||
|
|
||||||
|
# 运行主循环
|
||||||
|
self.root.mainloop()
|
||||||
|
|
||||||
|
def _create_exposure_controls(self, parent):
|
||||||
|
"""创建曝光控制滑块"""
|
||||||
|
# 曝光时间说明
|
||||||
|
ttk.Label(parent, text="曝光时间 (100-50000μs):").pack(pady=5)
|
||||||
|
ttk.Label(parent, text="作用: 控制相机传感器曝光时间", font=('Arial', 8), foreground='gray').pack(pady=2)
|
||||||
|
ttk.Label(parent, text="增大: 画面变亮,激光点更明显,但可能过曝", font=('Arial', 8), foreground='gray').pack(pady=1)
|
||||||
|
ttk.Label(parent, text="减小: 画面变暗,减少过曝,激光点更清晰", font=('Arial', 8), foreground='gray').pack(pady=1)
|
||||||
|
ttk.Label(parent, text="建议值: 5000μs(激光检测默认值)", font=('Arial', 8), foreground='green').pack(pady=2)
|
||||||
|
|
||||||
|
def on_exposure_change(value):
|
||||||
|
self.current['exposure'] = int(float(value))
|
||||||
|
self.update_callback('exposure', int(float(value)))
|
||||||
|
|
||||||
|
exposure_scale = ttk.Scale(
|
||||||
|
parent,
|
||||||
|
from_=100,
|
||||||
|
to=50000,
|
||||||
|
orient=tk.HORIZONTAL,
|
||||||
|
length=300,
|
||||||
|
command=on_exposure_change
|
||||||
|
)
|
||||||
|
exposure_scale.set(self.defaults['exposure'])
|
||||||
|
exposure_scale.pack(pady=5)
|
||||||
|
|
||||||
|
self.exposure_var = tk.StringVar(value=str(self.defaults['exposure']))
|
||||||
|
ttk.Entry(parent, textvariable=self.exposure_var, width=10).pack(pady=5)
|
||||||
|
|
||||||
|
def update_exposure_from_entry():
|
||||||
|
try:
|
||||||
|
value = int(self.exposure_var.get())
|
||||||
|
if 100 <= value <= 50000:
|
||||||
|
exposure_scale.set(value)
|
||||||
|
on_exposure_change(value)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
ttk.Button(parent, text="应用", command=update_exposure_from_entry).pack(pady=5)
|
||||||
|
|
||||||
|
def _create_gain_controls(self, parent):
|
||||||
|
"""创建增益控制滑块"""
|
||||||
|
# 增益说明
|
||||||
|
ttk.Label(parent, text="增益 (0-24dB):").pack(pady=5)
|
||||||
|
ttk.Label(parent, text="作用: 放大传感器信号,增强画面亮度", font=('Arial', 8), foreground='gray').pack(pady=2)
|
||||||
|
ttk.Label(parent, text="增大: 画面变亮,噪声也会增加", font=('Arial', 8), foreground='gray').pack(pady=1)
|
||||||
|
ttk.Label(parent, text="减小: 画面变暗,噪声减少", font=('Arial', 8), foreground='gray').pack(pady=1)
|
||||||
|
ttk.Label(parent, text="建议值: 0dB(激光检测默认值),仅在光线极暗时使用", font=('Arial', 8), foreground='green').pack(pady=2)
|
||||||
|
|
||||||
|
def on_gain_change(value):
|
||||||
|
gain_value = float(value) / 10.0
|
||||||
|
self.current['gain'] = gain_value
|
||||||
|
self.update_callback('gain', gain_value)
|
||||||
|
|
||||||
|
gain_scale = ttk.Scale(
|
||||||
|
parent,
|
||||||
|
from_=0,
|
||||||
|
to=240,
|
||||||
|
orient=tk.HORIZONTAL,
|
||||||
|
length=300,
|
||||||
|
command=on_gain_change
|
||||||
|
)
|
||||||
|
gain_scale.set(self.defaults['gain'] * 10)
|
||||||
|
gain_scale.pack(pady=5)
|
||||||
|
|
||||||
|
self.gain_var = tk.StringVar(value=str(self.defaults['gain']))
|
||||||
|
ttk.Entry(parent, textvariable=self.gain_var, width=10).pack(pady=5)
|
||||||
|
|
||||||
|
def update_gain_from_entry():
|
||||||
|
try:
|
||||||
|
value = float(self.gain_var.get())
|
||||||
|
if 0 <= value <= 24:
|
||||||
|
gain_scale.set(value * 10)
|
||||||
|
on_gain_change(value * 10)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
ttk.Button(parent, text="应用", command=update_gain_from_entry).pack(pady=5)
|
||||||
|
|
||||||
|
def _create_fps_controls(self, parent):
|
||||||
|
"""创建帧率控制滑块"""
|
||||||
|
# 帧率说明
|
||||||
|
ttk.Label(parent, text="帧率 (1-120 FPS):").pack(pady=5)
|
||||||
|
ttk.Label(parent, text="作用: 控制相机采集图像的频率", font=('Arial', 8), foreground='gray').pack(pady=2)
|
||||||
|
ttk.Label(parent, text="增大: 画面流畅度提高,CPU占用增加", font=('Arial', 8), foreground='gray').pack(pady=1)
|
||||||
|
ttk.Label(parent, text="减小: 画面流畅度降低,CPU占用减少", font=('Arial', 8), foreground='gray').pack(pady=1)
|
||||||
|
ttk.Label(parent, text="建议值: 30 FPS(平衡流畅度和性能)", font=('Arial', 8), foreground='green').pack(pady=2)
|
||||||
|
|
||||||
|
def on_fps_change(value):
|
||||||
|
self.current['fps'] = int(float(value))
|
||||||
|
self.update_callback('fps', int(float(value)))
|
||||||
|
|
||||||
|
fps_scale = ttk.Scale(
|
||||||
|
parent,
|
||||||
|
from_=1,
|
||||||
|
to=120,
|
||||||
|
orient=tk.HORIZONTAL,
|
||||||
|
length=300,
|
||||||
|
command=on_fps_change
|
||||||
|
)
|
||||||
|
fps_scale.set(self.defaults['fps'])
|
||||||
|
fps_scale.pack(pady=5)
|
||||||
|
|
||||||
|
self.fps_var = tk.StringVar(value=str(self.defaults['fps']))
|
||||||
|
ttk.Entry(parent, textvariable=self.fps_var, width=10).pack(pady=5)
|
||||||
|
|
||||||
|
def update_fps_from_entry():
|
||||||
|
try:
|
||||||
|
value = int(self.fps_var.get())
|
||||||
|
if 1 <= value <= 120:
|
||||||
|
fps_scale.set(value)
|
||||||
|
on_fps_change(value)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
ttk.Button(parent, text="应用", command=update_fps_from_entry).pack(pady=5)
|
||||||
|
|
||||||
|
def _create_roi_controls(self, parent):
|
||||||
|
"""创建ROI控制输入框"""
|
||||||
|
# ROI设置说明
|
||||||
|
ttk.Label(parent, text="ROI设置:").pack(pady=5)
|
||||||
|
ttk.Label(parent, text="作用: 设置感兴趣区域,仅处理指定区域的图像", font=('Arial', 8), foreground='gray').pack(pady=2)
|
||||||
|
ttk.Label(parent, text="减小ROI: 提高处理速度,减少CPU占用", font=('Arial', 8), foreground='gray').pack(pady=1)
|
||||||
|
ttk.Label(parent, text="增大ROI: 处理范围更广,CPU占用增加", font=('Arial', 8), foreground='gray').pack(pady=1)
|
||||||
|
ttk.Label(parent, text="建议值: 默认全画幅(0, 0, 640, 480)", font=('Arial', 8), foreground='green').pack(pady=2)
|
||||||
|
|
||||||
|
# ROI输入框
|
||||||
|
roi_frame = ttk.Frame(parent)
|
||||||
|
roi_frame.pack(fill=tk.X, pady=5)
|
||||||
|
|
||||||
|
# X坐标
|
||||||
|
ttk.Label(roi_frame, text="X:").grid(row=0, column=0, padx=5)
|
||||||
|
self.roi_x_var = tk.StringVar(value=str(self.defaults['roi_x']))
|
||||||
|
ttk.Entry(roi_frame, textvariable=self.roi_x_var, width=10).grid(row=0, column=1, padx=5)
|
||||||
|
|
||||||
|
# Y坐标
|
||||||
|
ttk.Label(roi_frame, text="Y:").grid(row=0, column=2, padx=5)
|
||||||
|
self.roi_y_var = tk.StringVar(value=str(self.defaults['roi_y']))
|
||||||
|
ttk.Entry(roi_frame, textvariable=self.roi_y_var, width=10).grid(row=0, column=3, padx=5)
|
||||||
|
|
||||||
|
# Width
|
||||||
|
ttk.Label(roi_frame, text="Width:").grid(row=1, column=0, padx=5)
|
||||||
|
self.roi_width_var = tk.StringVar(value=str(self.defaults['roi_width']))
|
||||||
|
ttk.Entry(roi_frame, textvariable=self.roi_width_var, width=10).grid(row=1, column=1, padx=5)
|
||||||
|
|
||||||
|
# Height
|
||||||
|
ttk.Label(roi_frame, text="Height:").grid(row=1, column=2, padx=5)
|
||||||
|
self.roi_height_var = tk.StringVar(value=str(self.defaults['roi_height']))
|
||||||
|
ttk.Entry(roi_frame, textvariable=self.roi_height_var, width=10).grid(row=1, column=3, padx=5)
|
||||||
|
|
||||||
|
def apply_roi():
|
||||||
|
try:
|
||||||
|
x = int(self.roi_x_var.get())
|
||||||
|
y = int(self.roi_y_var.get())
|
||||||
|
width = int(self.roi_width_var.get())
|
||||||
|
height = int(self.roi_height_var.get())
|
||||||
|
|
||||||
|
if x >= 0 and y >= 0 and width > 0 and height > 0:
|
||||||
|
self.current['roi_x'] = x
|
||||||
|
self.current['roi_y'] = y
|
||||||
|
self.current['roi_width'] = width
|
||||||
|
self.current['roi_height'] = height
|
||||||
|
self.update_callback('roi', (x, y, width, height))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
ttk.Button(parent, text="应用ROI", command=apply_roi).pack(pady=10)
|
||||||
|
|
||||||
|
def _create_hsv_controls(self, parent):
|
||||||
|
"""创建HSV控制滑块"""
|
||||||
|
# HSV调节说明
|
||||||
|
ttk.Label(parent, text="HSV范围调节:").pack(pady=5)
|
||||||
|
ttk.Label(parent, text="作用: 微调绿色激光的识别范围", font=('Arial', 8), foreground='gray').pack(pady=2)
|
||||||
|
|
||||||
|
# H通道说明
|
||||||
|
ttk.Label(parent, text="H通道: 控制颜色范围(绿色激光默认35-85)", font=('Arial', 8), foreground='gray').pack(pady=2)
|
||||||
|
|
||||||
|
# S通道说明
|
||||||
|
ttk.Label(parent, text="S通道: 控制饱和度范围(过滤低饱和度背景)", font=('Arial', 8), foreground='gray').pack(pady=2)
|
||||||
|
|
||||||
|
# V通道说明
|
||||||
|
ttk.Label(parent, text="V通道: 控制亮度范围(过滤暗背景)", font=('Arial', 8), foreground='gray').pack(pady=2)
|
||||||
|
ttk.Label(parent, text="建议值: H(35-85), S(80-255), V(80-255)", font=('Arial', 8), foreground='green').pack(pady=2)
|
||||||
|
|
||||||
|
# H通道
|
||||||
|
h_frame = ttk.Frame(parent)
|
||||||
|
h_frame.pack(fill=tk.X, pady=5)
|
||||||
|
|
||||||
|
ttk.Label(h_frame, text="H_low:").grid(row=0, column=0, padx=5)
|
||||||
|
self.h_low_var = tk.StringVar(value=str(self.defaults['h_low']))
|
||||||
|
ttk.Entry(h_frame, textvariable=self.h_low_var, width=5).grid(row=0, column=1, padx=5)
|
||||||
|
|
||||||
|
ttk.Label(h_frame, text="H_high:").grid(row=0, column=2, padx=5)
|
||||||
|
self.h_high_var = tk.StringVar(value=str(self.defaults['h_high']))
|
||||||
|
ttk.Entry(h_frame, textvariable=self.h_high_var, width=5).grid(row=0, column=3, padx=5)
|
||||||
|
|
||||||
|
# S通道
|
||||||
|
s_frame = ttk.Frame(parent)
|
||||||
|
s_frame.pack(fill=tk.X, pady=5)
|
||||||
|
|
||||||
|
ttk.Label(s_frame, text="S_low:").grid(row=0, column=0, padx=5)
|
||||||
|
self.s_low_var = tk.StringVar(value=str(self.defaults['s_low']))
|
||||||
|
ttk.Entry(s_frame, textvariable=self.s_low_var, width=5).grid(row=0, column=1, padx=5)
|
||||||
|
|
||||||
|
ttk.Label(s_frame, text="S_high:").grid(row=0, column=2, padx=5)
|
||||||
|
self.s_high_var = tk.StringVar(value=str(self.defaults['s_high']))
|
||||||
|
ttk.Entry(s_frame, textvariable=self.s_high_var, width=5).grid(row=0, column=3, padx=5)
|
||||||
|
|
||||||
|
# V通道
|
||||||
|
v_frame = ttk.Frame(parent)
|
||||||
|
v_frame.pack(fill=tk.X, pady=5)
|
||||||
|
|
||||||
|
ttk.Label(v_frame, text="V_low:").grid(row=0, column=0, padx=5)
|
||||||
|
self.v_low_var = tk.StringVar(value=str(self.defaults['v_low']))
|
||||||
|
ttk.Entry(v_frame, textvariable=self.v_low_var, width=5).grid(row=0, column=1, padx=5)
|
||||||
|
|
||||||
|
ttk.Label(v_frame, text="V_high:").grid(row=0, column=2, padx=5)
|
||||||
|
self.v_high_var = tk.StringVar(value=str(self.defaults['v_high']))
|
||||||
|
ttk.Entry(v_frame, textvariable=self.v_high_var, width=5).grid(row=0, column=3, padx=5)
|
||||||
|
|
||||||
|
def apply_hsv():
|
||||||
|
try:
|
||||||
|
h_low = int(self.h_low_var.get())
|
||||||
|
h_high = int(self.h_high_var.get())
|
||||||
|
s_low = int(self.s_low_var.get())
|
||||||
|
s_high = int(self.s_high_var.get())
|
||||||
|
v_low = int(self.v_low_var.get())
|
||||||
|
v_high = int(self.v_high_var.get())
|
||||||
|
|
||||||
|
# 验证范围
|
||||||
|
if all(0 <= val <= 255 for val in [h_low, h_high, s_low, s_high, v_low, v_high]):
|
||||||
|
self.current['h_low'] = h_low
|
||||||
|
self.current['h_high'] = h_high
|
||||||
|
self.current['s_low'] = s_low
|
||||||
|
self.current['s_high'] = s_high
|
||||||
|
self.current['v_low'] = v_low
|
||||||
|
self.current['v_high'] = v_high
|
||||||
|
self.update_callback('hsv', (h_low, h_high, s_low, s_high, v_low, v_high))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
ttk.Button(parent, text="应用HSV", command=apply_hsv).pack(pady=10)
|
||||||
|
|
||||||
|
def _process_queue(self):
|
||||||
|
"""处理队列中的消息"""
|
||||||
|
try:
|
||||||
|
while not self.queue.empty():
|
||||||
|
msg = self.queue.get_nowait()
|
||||||
|
# 处理消息
|
||||||
|
if msg['type'] == 'update_value':
|
||||||
|
param = msg['param']
|
||||||
|
value = msg['value']
|
||||||
|
if param in self.current:
|
||||||
|
self.current[param] = value
|
||||||
|
except queue.Empty:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
self.root.after(100, self._process_queue)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""停止参数控制面板"""
|
||||||
|
if self.root:
|
||||||
|
try:
|
||||||
|
# 使用after方法在Tkinter主线程中执行销毁操作
|
||||||
|
self.root.after(0, lambda: self._safe_destroy())
|
||||||
|
# 给Tkinter一些时间来处理销毁操作
|
||||||
|
import time
|
||||||
|
time.sleep(0.1)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"停止控制面板时出错: {e}")
|
||||||
|
|
||||||
|
def _safe_destroy(self):
|
||||||
|
"""安全销毁Tkinter窗口"""
|
||||||
|
if self.root:
|
||||||
|
try:
|
||||||
|
self.root.quit()
|
||||||
|
self.root.destroy()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _create_morph_controls(self, parent):
|
||||||
|
"""创建形态学参数控制面板"""
|
||||||
|
# 形态学参数说明
|
||||||
|
ttk.Label(parent, text="形态学参数设置:").pack(pady=5)
|
||||||
|
ttk.Label(parent, text="作用: 控制LED点阵的聚合效果", font=('Arial', 8), foreground='gray').pack(pady=2)
|
||||||
|
|
||||||
|
# 闭运算核大小
|
||||||
|
ttk.Label(parent, text="闭运算核大小 (5-50):").pack(pady=5)
|
||||||
|
def on_kernel_size_change(value):
|
||||||
|
kernel_size = int(float(value))
|
||||||
|
open_kernel = 5 # 固定开运算核大小
|
||||||
|
self.update_callback('morph', (kernel_size, open_kernel))
|
||||||
|
|
||||||
|
kernel_scale = ttk.Scale(
|
||||||
|
parent,
|
||||||
|
from_=5,
|
||||||
|
to=50,
|
||||||
|
orient=tk.HORIZONTAL,
|
||||||
|
length=300,
|
||||||
|
command=on_kernel_size_change
|
||||||
|
)
|
||||||
|
kernel_scale.set(20) # 默认值
|
||||||
|
kernel_scale.pack(pady=5)
|
||||||
|
|
||||||
|
# 开运算核大小
|
||||||
|
ttk.Label(parent, text="开运算核大小 (3-15):").pack(pady=5)
|
||||||
|
def on_open_kernel_change(value):
|
||||||
|
open_kernel = int(float(value))
|
||||||
|
kernel_size = 20 # 固定闭运算核大小
|
||||||
|
self.update_callback('morph', (kernel_size, open_kernel))
|
||||||
|
|
||||||
|
open_kernel_scale = ttk.Scale(
|
||||||
|
parent,
|
||||||
|
from_=3,
|
||||||
|
to=15,
|
||||||
|
orient=tk.HORIZONTAL,
|
||||||
|
length=300,
|
||||||
|
command=on_open_kernel_change
|
||||||
|
)
|
||||||
|
open_kernel_scale.set(5) # 默认值
|
||||||
|
open_kernel_scale.pack(pady=5)
|
||||||
|
|
||||||
|
def _create_geometry_controls(self, parent):
|
||||||
|
"""创建几何参数控制面板"""
|
||||||
|
# 几何参数说明
|
||||||
|
ttk.Label(parent, text="几何参数设置:").pack(pady=5)
|
||||||
|
ttk.Label(parent, text="作用: 控制灯盘的几何特征过滤", font=('Arial', 8), foreground='gray').pack(pady=2)
|
||||||
|
|
||||||
|
# 最小灯盘面积
|
||||||
|
ttk.Label(parent, text="最小灯盘面积 (100-5000):").pack(pady=5)
|
||||||
|
def on_min_area_change(value):
|
||||||
|
min_area = int(float(value))
|
||||||
|
min_leds = 10 # 默认值
|
||||||
|
circularity = 0.4 # 默认值
|
||||||
|
self.update_callback('geometry', (min_area, min_leds, circularity))
|
||||||
|
|
||||||
|
min_area_scale = ttk.Scale(
|
||||||
|
parent,
|
||||||
|
from_=100,
|
||||||
|
to=5000,
|
||||||
|
orient=tk.HORIZONTAL,
|
||||||
|
length=300,
|
||||||
|
command=on_min_area_change
|
||||||
|
)
|
||||||
|
min_area_scale.set(500) # 默认值
|
||||||
|
min_area_scale.pack(pady=5)
|
||||||
|
|
||||||
|
# 最小LED数量
|
||||||
|
ttk.Label(parent, text="最小LED数量 (5-50):").pack(pady=5)
|
||||||
|
def on_min_leds_change(value):
|
||||||
|
min_leds = int(float(value))
|
||||||
|
min_area = 500 # 默认值
|
||||||
|
circularity = 0.4 # 默认值
|
||||||
|
self.update_callback('geometry', (min_area, min_leds, circularity))
|
||||||
|
|
||||||
|
min_leds_scale = ttk.Scale(
|
||||||
|
parent,
|
||||||
|
from_=5,
|
||||||
|
to=50,
|
||||||
|
orient=tk.HORIZONTAL,
|
||||||
|
length=300,
|
||||||
|
command=on_min_leds_change
|
||||||
|
)
|
||||||
|
min_leds_scale.set(10) # 默认值
|
||||||
|
min_leds_scale.pack(pady=5)
|
||||||
|
|
||||||
|
# 圆度阈值
|
||||||
|
ttk.Label(parent, text="圆度阈值 (0.1-1.0):").pack(pady=5)
|
||||||
|
def on_circularity_change(value):
|
||||||
|
circularity = float(value)
|
||||||
|
min_area = 500 # 默认值
|
||||||
|
min_leds = 10 # 默认值
|
||||||
|
self.update_callback('geometry', (min_area, min_leds, circularity))
|
||||||
|
|
||||||
|
circularity_scale = ttk.Scale(
|
||||||
|
parent,
|
||||||
|
from_=0.1,
|
||||||
|
to=1.0,
|
||||||
|
orient=tk.HORIZONTAL,
|
||||||
|
length=300,
|
||||||
|
command=on_circularity_change
|
||||||
|
)
|
||||||
|
circularity_scale.set(0.4) # 默认值
|
||||||
|
circularity_scale.pack(pady=5)
|
||||||
|
|
||||||
|
def _create_mode_controls(self, parent):
|
||||||
|
"""创建双模式控制界面"""
|
||||||
|
# 双模式说明
|
||||||
|
ttk.Label(parent, text="双模式激光识别系统设置:").pack(pady=5)
|
||||||
|
ttk.Label(parent, text="作用: 配置单点光斑和点阵灯盘双模式检测", font=('Arial', 8), foreground='gray').pack(pady=2)
|
||||||
|
|
||||||
|
# 算法使能区
|
||||||
|
ttk.Label(parent, text="算法使能:").pack(pady=5)
|
||||||
|
|
||||||
|
enable_a_var = tk.BooleanVar(value=True)
|
||||||
|
enable_b_var = tk.BooleanVar(value=True)
|
||||||
|
|
||||||
|
def on_enable_change():
|
||||||
|
enable_a = enable_a_var.get()
|
||||||
|
enable_b = enable_b_var.get()
|
||||||
|
# 确保至少启用一项
|
||||||
|
if not enable_a and not enable_b:
|
||||||
|
enable_b_var.set(True)
|
||||||
|
enable_b = True
|
||||||
|
self.update_callback('algorithm_enable', (enable_a, enable_b))
|
||||||
|
|
||||||
|
# 启用单点光斑检测
|
||||||
|
ttk.Checkbutton(
|
||||||
|
parent,
|
||||||
|
text="启用单点光斑检测 (模式A)",
|
||||||
|
variable=enable_a_var,
|
||||||
|
command=on_enable_change
|
||||||
|
).pack(pady=2, anchor=tk.W)
|
||||||
|
|
||||||
|
# 启用点阵灯盘检测
|
||||||
|
ttk.Checkbutton(
|
||||||
|
parent,
|
||||||
|
text="启用点阵灯盘检测 (模式B)",
|
||||||
|
variable=enable_b_var,
|
||||||
|
command=on_enable_change
|
||||||
|
).pack(pady=2, anchor=tk.W)
|
||||||
|
|
||||||
|
# 工作模式选择
|
||||||
|
ttk.Label(parent, text="工作模式:").pack(pady=5)
|
||||||
|
|
||||||
|
work_mode_var = tk.StringVar(value='auto')
|
||||||
|
|
||||||
|
def on_work_mode_change():
|
||||||
|
mode = work_mode_var.get()
|
||||||
|
self.update_callback('work_mode', mode)
|
||||||
|
|
||||||
|
# 全自动仲裁
|
||||||
|
ttk.Radiobutton(
|
||||||
|
parent,
|
||||||
|
text="全自动仲裁 (系统自动选择最优结果)",
|
||||||
|
variable=work_mode_var,
|
||||||
|
value='auto',
|
||||||
|
command=on_work_mode_change
|
||||||
|
).pack(pady=2, anchor=tk.W)
|
||||||
|
|
||||||
|
# 强制单点模式
|
||||||
|
ttk.Radiobutton(
|
||||||
|
parent,
|
||||||
|
text="强制单点模式 (仅运行模式A)",
|
||||||
|
variable=work_mode_var,
|
||||||
|
value='forced_single',
|
||||||
|
command=on_work_mode_change
|
||||||
|
).pack(pady=2, anchor=tk.W)
|
||||||
|
|
||||||
|
# 强制灯盘模式
|
||||||
|
ttk.Radiobutton(
|
||||||
|
parent,
|
||||||
|
text="强制灯盘模式 (仅运行模式B)",
|
||||||
|
variable=work_mode_var,
|
||||||
|
value='forced_lamp',
|
||||||
|
command=on_work_mode_change
|
||||||
|
).pack(pady=2, anchor=tk.W)
|
||||||
|
|
||||||
|
# 双模显示
|
||||||
|
ttk.Radiobutton(
|
||||||
|
parent,
|
||||||
|
text="双模显示 (同时显示两种模式结果)",
|
||||||
|
variable=work_mode_var,
|
||||||
|
value='dual_display',
|
||||||
|
command=on_work_mode_change
|
||||||
|
).pack(pady=2, anchor=tk.W)
|
||||||
|
|
||||||
|
# 性能优化设置
|
||||||
|
ttk.Label(parent, text="性能优化:").pack(pady=5)
|
||||||
|
|
||||||
|
use_alternating_var = tk.BooleanVar(value=False)
|
||||||
|
|
||||||
|
def on_performance_change():
|
||||||
|
use_alternating = use_alternating_var.get()
|
||||||
|
self.update_callback('performance_optimization', use_alternating)
|
||||||
|
|
||||||
|
ttk.Checkbutton(
|
||||||
|
parent,
|
||||||
|
text="使用交替帧策略 (减少CPU占用)",
|
||||||
|
variable=use_alternating_var,
|
||||||
|
command=on_performance_change
|
||||||
|
).pack(pady=2, anchor=tk.W)
|
||||||
|
|
||||||
|
# 手动干预按钮
|
||||||
|
ttk.Label(parent, text="手动干预:").pack(pady=5)
|
||||||
|
|
||||||
|
button_frame = ttk.Frame(parent)
|
||||||
|
button_frame.pack(fill=tk.X, pady=5)
|
||||||
|
|
||||||
|
ttk.Button(
|
||||||
|
button_frame,
|
||||||
|
text="锁定当前模式",
|
||||||
|
command=lambda: self.update_callback('lock_mode', True)
|
||||||
|
).pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
|
ttk.Button(
|
||||||
|
button_frame,
|
||||||
|
text="重置仲裁",
|
||||||
|
command=lambda: self.update_callback('reset_arbiter', True)
|
||||||
|
).pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
|
# 状态显示区域
|
||||||
|
ttk.Label(parent, text="系统状态:").pack(pady=5)
|
||||||
|
|
||||||
|
self.status_var = tk.StringVar(value="就绪")
|
||||||
|
ttk.Label(parent, textvariable=self.status_var, font=('Arial', 10), foreground='green').pack(pady=2)
|
||||||
|
|
||||||
|
self.mode_a_status_var = tk.StringVar(value="模式A: 未检测")
|
||||||
|
ttk.Label(parent, textvariable=self.mode_a_status_var, font=('Arial', 9)).pack(pady=1, anchor=tk.W)
|
||||||
|
|
||||||
|
self.mode_b_status_var = tk.StringVar(value="模式B: 未检测")
|
||||||
|
ttk.Label(parent, textvariable=self.mode_b_status_var, font=('Arial', 9)).pack(pady=1, anchor=tk.W)
|
||||||
|
|
||||||
|
self.decision_var = tk.StringVar(value="决策: 等待输入")
|
||||||
|
ttk.Label(parent, textvariable=self.decision_var, font=('Arial', 9), foreground='blue').pack(pady=1, anchor=tk.W)
|
||||||
68
detector.py
Normal file
68
detector.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
class GreenLaserDetector:
|
||||||
|
def __init__(self):
|
||||||
|
"""初始化绿色激光检测器"""
|
||||||
|
# 针对短曝光激光点优化:降低最小识别面积
|
||||||
|
self.min_area = 50
|
||||||
|
self.max_area = 5000
|
||||||
|
|
||||||
|
# 默认HSV参数
|
||||||
|
self.h_low = 35
|
||||||
|
self.h_high = 85
|
||||||
|
self.s_low = 80
|
||||||
|
self.s_high = 255
|
||||||
|
self.v_low = 80
|
||||||
|
self.v_high = 255
|
||||||
|
|
||||||
|
def set_hsv_range(self, h_low, h_high, s_low, s_high, v_low, v_high):
|
||||||
|
"""设置HSV颜色范围"""
|
||||||
|
self.h_low = h_low
|
||||||
|
self.h_high = h_high
|
||||||
|
self.s_low = s_low
|
||||||
|
self.s_high = s_high
|
||||||
|
self.v_low = v_low
|
||||||
|
self.v_high = v_high
|
||||||
|
|
||||||
|
def detect(self, frame):
|
||||||
|
"""
|
||||||
|
检测绿色激光点
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: BGR格式的图像
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
results: 检测结果列表,每个元素为(cx, cy, w, h, area, circularity)
|
||||||
|
mask: 二值掩码图像
|
||||||
|
"""
|
||||||
|
# 转HSV
|
||||||
|
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
|
||||||
|
|
||||||
|
# 绿色范围(需根据实际激光调整)
|
||||||
|
lower_green = np.array([self.h_low, self.s_low, self.v_low])
|
||||||
|
upper_green = np.array([self.h_high, self.s_high, self.v_high])
|
||||||
|
mask = cv2.inRange(hsv, lower_green, upper_green)
|
||||||
|
|
||||||
|
# 形态学去噪
|
||||||
|
kernel = np.ones((5,5), np.uint8)
|
||||||
|
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
|
||||||
|
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
|
||||||
|
|
||||||
|
# 查找轮廓
|
||||||
|
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for cnt in contours:
|
||||||
|
area = cv2.contourArea(cnt)
|
||||||
|
if self.min_area < area < self.max_area:
|
||||||
|
# 圆形度检测
|
||||||
|
perimeter = cv2.arcLength(cnt, True)
|
||||||
|
if perimeter > 0:
|
||||||
|
circularity = 4 * np.pi * area / (perimeter ** 2)
|
||||||
|
if circularity > 0.7: # 圆度阈值
|
||||||
|
x, y, w, h = cv2.boundingRect(cnt)
|
||||||
|
cx, cy = x + w//2, y + h//2
|
||||||
|
results.append((cx, cy, w, h, area, circularity))
|
||||||
|
return results, mask
|
||||||
344
dot_matrix_detector.py
Normal file
344
dot_matrix_detector.py
Normal file
@ -0,0 +1,344 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
点阵灯盘检测算法
|
||||||
|
|
||||||
|
核心功能:
|
||||||
|
1. 预处理与离散点聚合
|
||||||
|
2. 连通域分析与圆拟合
|
||||||
|
3. 多灯盘分离
|
||||||
|
4. 后处理过滤
|
||||||
|
"""
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
from sklearn.cluster import DBSCAN
|
||||||
|
|
||||||
|
class DotMatrixDetector:
|
||||||
|
def __init__(self):
|
||||||
|
"""初始化点阵灯盘检测器"""
|
||||||
|
# 默认HSV参数(针对高亮绿色LED)
|
||||||
|
self.h_low = 35 # H通道最小值(绿色起始)
|
||||||
|
self.h_high = 85 # H通道最大值(绿色结束)
|
||||||
|
self.s_low = 60 # S通道最小值(饱和度阈值,降低以适应LED)
|
||||||
|
self.s_high = 255 # S通道最大值
|
||||||
|
self.v_low = 200 # V通道最小值(亮度阈值,提高以过滤环境光)
|
||||||
|
self.v_high = 255 # V通道最大值
|
||||||
|
|
||||||
|
# 形态学参数
|
||||||
|
self.morph_kernel_size = 20 # 闭运算核大小(略大于LED点阵间距)
|
||||||
|
self.morph_open_kernel = 5 # 开运算核大小(去除噪点)
|
||||||
|
|
||||||
|
# 几何参数
|
||||||
|
self.min_disk_area = 500 # 最小灯盘面积(像素)
|
||||||
|
self.min_led_count = 10 # 最小LED数量
|
||||||
|
self.circularity_threshold = 0.4 # 圆度阈值(点阵灯盘较宽松)
|
||||||
|
|
||||||
|
# 检测模式
|
||||||
|
self.detection_mode = 'least_squares' # least_squares, min_enclosing, dbscan
|
||||||
|
|
||||||
|
# DBSCAN参数
|
||||||
|
self.dbscan_eps = 30 # 空间距离阈值
|
||||||
|
self.dbscan_min_samples = 5 # 最小样本数
|
||||||
|
|
||||||
|
def set_hsv_range(self, h_low, h_high, s_low, s_high, v_low, v_high):
|
||||||
|
"""设置HSV颜色范围"""
|
||||||
|
self.h_low = h_low
|
||||||
|
self.h_high = h_high
|
||||||
|
self.s_low = s_low
|
||||||
|
self.s_high = s_high
|
||||||
|
self.v_low = v_low
|
||||||
|
self.v_high = v_high
|
||||||
|
|
||||||
|
def set_morph_parameters(self, kernel_size, open_kernel):
|
||||||
|
"""设置形态学参数"""
|
||||||
|
self.morph_kernel_size = kernel_size
|
||||||
|
self.morph_open_kernel = open_kernel
|
||||||
|
|
||||||
|
def set_geometry_parameters(self, min_area, min_leds, circularity):
|
||||||
|
"""设置几何参数"""
|
||||||
|
self.min_disk_area = min_area
|
||||||
|
self.min_led_count = min_leds
|
||||||
|
self.circularity_threshold = circularity
|
||||||
|
|
||||||
|
def set_detection_mode(self, mode):
|
||||||
|
"""设置检测模式"""
|
||||||
|
self.detection_mode = mode
|
||||||
|
|
||||||
|
def detect(self, frame):
|
||||||
|
"""
|
||||||
|
检测点阵灯盘
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: BGR格式的图像
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
results: 检测结果列表,每个元素包含圆心坐标、半径、置信度等
|
||||||
|
mask: 二值掩码图像
|
||||||
|
raw_leds: 原始LED点集
|
||||||
|
aggregated_mask: 形态学聚合后的掩码
|
||||||
|
"""
|
||||||
|
# 1. 预处理:颜色空间转换
|
||||||
|
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
|
||||||
|
|
||||||
|
# 2. 颜色阈值分割
|
||||||
|
lower_green = np.array([self.h_low, self.s_low, self.v_low])
|
||||||
|
upper_green = np.array([self.h_high, self.s_high, self.v_high])
|
||||||
|
mask = cv2.inRange(hsv, lower_green, upper_green)
|
||||||
|
|
||||||
|
# 3. 形态学操作:闭运算(聚合离散点)
|
||||||
|
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (self.morph_kernel_size, self.morph_kernel_size))
|
||||||
|
aggregated_mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
|
||||||
|
|
||||||
|
# 4. 形态学操作:开运算(去除噪点)
|
||||||
|
open_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (self.morph_open_kernel, self.morph_open_kernel))
|
||||||
|
aggregated_mask = cv2.morphologyEx(aggregated_mask, cv2.MORPH_OPEN, open_kernel)
|
||||||
|
|
||||||
|
# 5. 提取原始LED点
|
||||||
|
raw_leds = self._extract_raw_leds(mask)
|
||||||
|
|
||||||
|
# 6. 检测灯盘
|
||||||
|
results = []
|
||||||
|
|
||||||
|
if self.detection_mode == 'dbscan' and len(raw_leds) >= self.min_led_count:
|
||||||
|
# 使用DBSCAN聚类检测多灯盘
|
||||||
|
results = self._detect_with_dbscan(raw_leds)
|
||||||
|
else:
|
||||||
|
# 使用连通域分析
|
||||||
|
results = self._detect_with_contours(aggregated_mask, mask)
|
||||||
|
|
||||||
|
# 7. 后处理:非极大值抑制
|
||||||
|
results = self._non_maximum_suppression(results)
|
||||||
|
|
||||||
|
return results, mask, raw_leds, aggregated_mask
|
||||||
|
|
||||||
|
def _extract_raw_leds(self, mask):
|
||||||
|
"""提取原始LED点"""
|
||||||
|
# 查找连通域
|
||||||
|
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||||
|
|
||||||
|
leds = []
|
||||||
|
for contour in contours:
|
||||||
|
# 计算中心点
|
||||||
|
M = cv2.moments(contour)
|
||||||
|
if M['m00'] > 0:
|
||||||
|
cx = int(M['m10'] / M['m00'])
|
||||||
|
cy = int(M['m01'] / M['m00'])
|
||||||
|
leds.append((cx, cy))
|
||||||
|
|
||||||
|
return np.array(leds)
|
||||||
|
|
||||||
|
def _detect_with_contours(self, aggregated_mask, raw_mask):
|
||||||
|
"""基于连通域的灯盘检测"""
|
||||||
|
results = []
|
||||||
|
|
||||||
|
# 查找轮廓
|
||||||
|
contours, _ = cv2.findContours(aggregated_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||||
|
|
||||||
|
for i, contour in enumerate(contours):
|
||||||
|
# 计算轮廓面积
|
||||||
|
area = cv2.contourArea(contour)
|
||||||
|
if area < self.min_disk_area:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 在原始掩码中提取该连通域内的LED点
|
||||||
|
led_points = self._extract_leds_in_contour(contour, raw_mask)
|
||||||
|
if len(led_points) < self.min_led_count:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 根据检测模式拟合圆
|
||||||
|
if self.detection_mode == 'min_enclosing':
|
||||||
|
center, radius = cv2.minEnclosingCircle(led_points)
|
||||||
|
cx, cy = center
|
||||||
|
else: # least_squares
|
||||||
|
center, radius = self._fit_circle_least_squares(led_points)
|
||||||
|
if center is None:
|
||||||
|
continue
|
||||||
|
cx, cy = center
|
||||||
|
|
||||||
|
# 计算圆度
|
||||||
|
circularity = self._calculate_circularity(contour, radius)
|
||||||
|
if circularity < self.circularity_threshold:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 计算置信度
|
||||||
|
confidence = self._calculate_confidence(len(led_points), circularity, radius)
|
||||||
|
|
||||||
|
# 添加结果
|
||||||
|
results.append({
|
||||||
|
'id': i,
|
||||||
|
'center': (cx, cy),
|
||||||
|
'radius': radius,
|
||||||
|
'confidence': confidence,
|
||||||
|
'led_count': len(led_points),
|
||||||
|
'circularity': circularity,
|
||||||
|
'contour': contour,
|
||||||
|
'led_points': led_points
|
||||||
|
})
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def _detect_with_dbscan(self, led_points):
|
||||||
|
"""使用DBSCAN聚类检测多灯盘"""
|
||||||
|
results = []
|
||||||
|
|
||||||
|
if len(led_points) < self.min_led_count:
|
||||||
|
return results
|
||||||
|
|
||||||
|
# DBSCAN聚类
|
||||||
|
db = DBSCAN(eps=self.dbscan_eps, min_samples=self.dbscan_min_samples).fit(led_points)
|
||||||
|
labels = db.labels_
|
||||||
|
|
||||||
|
# 处理每个簇
|
||||||
|
unique_labels = set(labels)
|
||||||
|
for i, label in enumerate(unique_labels):
|
||||||
|
if label == -1: # 噪声点
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 提取簇中的点
|
||||||
|
cluster_points = led_points[labels == label]
|
||||||
|
if len(cluster_points) < self.min_led_count:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 拟合圆
|
||||||
|
center, radius = self._fit_circle_least_squares(cluster_points)
|
||||||
|
if center is None:
|
||||||
|
continue
|
||||||
|
cx, cy = center
|
||||||
|
|
||||||
|
# 计算置信度
|
||||||
|
circularity = self._calculate_cluster_circularity(cluster_points, center, radius)
|
||||||
|
confidence = self._calculate_confidence(len(cluster_points), circularity, radius)
|
||||||
|
|
||||||
|
# 添加结果
|
||||||
|
results.append({
|
||||||
|
'id': i,
|
||||||
|
'center': (cx, cy),
|
||||||
|
'radius': radius,
|
||||||
|
'confidence': confidence,
|
||||||
|
'led_count': len(cluster_points),
|
||||||
|
'circularity': circularity,
|
||||||
|
'led_points': cluster_points
|
||||||
|
})
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def _extract_leds_in_contour(self, contour, mask):
|
||||||
|
"""提取轮廓内的LED点"""
|
||||||
|
# 创建掩码
|
||||||
|
contour_mask = np.zeros_like(mask)
|
||||||
|
cv2.drawContours(contour_mask, [contour], -1, 255, -1)
|
||||||
|
|
||||||
|
# 与原始掩码相交
|
||||||
|
combined_mask = cv2.bitwise_and(mask, contour_mask)
|
||||||
|
|
||||||
|
# 提取点
|
||||||
|
return self._extract_raw_leds(combined_mask)
|
||||||
|
|
||||||
|
def _fit_circle_least_squares(self, points):
|
||||||
|
"""最小二乘拟合圆"""
|
||||||
|
if len(points) < 3:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
# 最小二乘拟合
|
||||||
|
x = points[:, 0]
|
||||||
|
y = points[:, 1]
|
||||||
|
|
||||||
|
# 构建矩阵
|
||||||
|
A = np.column_stack((x, y, np.ones_like(x)))
|
||||||
|
b = x**2 + y**2
|
||||||
|
|
||||||
|
# 求解线性方程组
|
||||||
|
try:
|
||||||
|
x_opt = np.linalg.lstsq(A, b, rcond=None)[0]
|
||||||
|
cx = x_opt[0] / 2
|
||||||
|
cy = x_opt[1] / 2
|
||||||
|
radius = np.sqrt(x_opt[2] + cx**2 + cy**2)
|
||||||
|
return (cx, cy), radius
|
||||||
|
except:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def _calculate_circularity(self, contour, radius):
|
||||||
|
"""计算圆度"""
|
||||||
|
area = cv2.contourArea(contour)
|
||||||
|
expected_area = np.pi * radius**2
|
||||||
|
if expected_area == 0:
|
||||||
|
return 0
|
||||||
|
return area / expected_area
|
||||||
|
|
||||||
|
def _calculate_cluster_circularity(self, points, center, radius):
|
||||||
|
"""计算聚类点集的圆度"""
|
||||||
|
if len(points) < 3:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# 计算点到圆心的距离
|
||||||
|
distances = np.sqrt((points[:, 0] - center[0])**2 + (points[:, 1] - center[1])**2)
|
||||||
|
|
||||||
|
# 计算距离的标准差
|
||||||
|
std_distance = np.std(distances)
|
||||||
|
if radius == 0:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# 圆度:标准差越小,圆度越高
|
||||||
|
circularity = 1.0 / (1.0 + std_distance / radius)
|
||||||
|
return circularity
|
||||||
|
|
||||||
|
def _calculate_confidence(self, led_count, circularity, radius):
|
||||||
|
"""计算置信度"""
|
||||||
|
# LED数量得分
|
||||||
|
led_score = min(led_count / 50, 1.0)
|
||||||
|
|
||||||
|
# 圆度得分
|
||||||
|
circ_score = min(circularity / 0.7, 1.0)
|
||||||
|
|
||||||
|
# 综合得分
|
||||||
|
confidence = (led_score * 0.6 + circ_score * 0.4) * 100
|
||||||
|
return confidence
|
||||||
|
|
||||||
|
def _non_maximum_suppression(self, results, iou_threshold=0.5):
|
||||||
|
"""非极大值抑制"""
|
||||||
|
if not results:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# 按置信度排序
|
||||||
|
results.sort(key=lambda x: x['confidence'], reverse=True)
|
||||||
|
|
||||||
|
filtered_results = []
|
||||||
|
while results:
|
||||||
|
current = results.pop(0)
|
||||||
|
filtered_results.append(current)
|
||||||
|
|
||||||
|
# 过滤重叠的结果
|
||||||
|
results = [r for r in results if not self._is_overlapping(current, r, iou_threshold)]
|
||||||
|
|
||||||
|
return filtered_results
|
||||||
|
|
||||||
|
def _is_overlapping(self, result1, result2, threshold):
|
||||||
|
"""判断两个圆是否重叠"""
|
||||||
|
cx1, cy1 = result1['center']
|
||||||
|
r1 = result1['radius']
|
||||||
|
cx2, cy2 = result2['center']
|
||||||
|
r2 = result2['radius']
|
||||||
|
|
||||||
|
# 计算圆心距
|
||||||
|
distance = np.sqrt((cx1 - cx2)**2 + (cy1 - cy2)**2)
|
||||||
|
|
||||||
|
# 计算重叠面积
|
||||||
|
if distance >= r1 + r2:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if distance <= abs(r1 - r2):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 计算重叠区域
|
||||||
|
a = (r1**2 - r2**2 + distance**2) / (2 * distance)
|
||||||
|
b = distance - a
|
||||||
|
h = np.sqrt(r1**2 - a**2)
|
||||||
|
|
||||||
|
area1 = r1**2 * np.arccos(a / r1) - a * h
|
||||||
|
area2 = r2**2 * np.arccos(b / r2) - b * h
|
||||||
|
overlap_area = area1 + area2
|
||||||
|
|
||||||
|
# 计算IoU
|
||||||
|
total_area = np.pi * (r1**2 + r2**2) - overlap_area
|
||||||
|
iou = overlap_area / total_area
|
||||||
|
|
||||||
|
return iou > threshold
|
||||||
437
main.py
Normal file
437
main.py
Normal file
@ -0,0 +1,437 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
from camera_hik import CameraHik
|
||||||
|
from dot_matrix_detector import DotMatrixDetector
|
||||||
|
from detector import GreenLaserDetector
|
||||||
|
from arbitrator import ModeArbitrator
|
||||||
|
from config_panel import ConfigPanel
|
||||||
|
from auto_exposure import auto_exposure_adjust
|
||||||
|
from MvCameraControl_class import *
|
||||||
|
|
||||||
|
class DualModeLaserDetectionSystem:
|
||||||
|
def __init__(self):
|
||||||
|
"""初始化双模式激光识别系统"""
|
||||||
|
self.camera = CameraHik()
|
||||||
|
self.detector_a = GreenLaserDetector() # 模式A:单点光斑检测
|
||||||
|
self.detector_b = DotMatrixDetector() # 模式B:点阵灯盘检测
|
||||||
|
self.arbiter = ModeArbitrator() # 仲裁器
|
||||||
|
self.config_panel = None
|
||||||
|
self.fps_counter = 0
|
||||||
|
self.fps_start_time = time.time()
|
||||||
|
self.fps = 0
|
||||||
|
self.current_mode = None
|
||||||
|
self.is_mode_locked = False
|
||||||
|
|
||||||
|
def on_config_update(self, param, value):
|
||||||
|
"""
|
||||||
|
处理参数更新回调
|
||||||
|
|
||||||
|
Args:
|
||||||
|
param: 参数名称
|
||||||
|
value: 参数值
|
||||||
|
"""
|
||||||
|
if param == 'exposure':
|
||||||
|
self.camera.set_exposure_time(value)
|
||||||
|
elif param == 'gain':
|
||||||
|
self.camera.set_gain(value)
|
||||||
|
elif param == 'fps':
|
||||||
|
self.camera.set_frame_rate(value)
|
||||||
|
elif param == 'roi':
|
||||||
|
x, y, width, height = value
|
||||||
|
self.camera.set_roi(x, y, width, height)
|
||||||
|
elif param == 'hsv':
|
||||||
|
h_low, h_high, s_low, s_high, v_low, v_high = value
|
||||||
|
# 同时更新两个检测器的HSV范围
|
||||||
|
self.detector_a.set_hsv_range(h_low, h_high, s_low, s_high, v_low, v_high)
|
||||||
|
self.detector_b.set_hsv_range(h_low, h_high, s_low, s_high, v_low, v_high)
|
||||||
|
elif param == 'morph':
|
||||||
|
kernel_size, open_kernel = value
|
||||||
|
self.detector_b.set_morph_parameters(kernel_size, open_kernel)
|
||||||
|
elif param == 'geometry':
|
||||||
|
min_area, min_leds, circularity = value
|
||||||
|
self.detector_b.set_geometry_parameters(min_area, min_leds, circularity)
|
||||||
|
elif param == 'detection_mode':
|
||||||
|
self.detector_b.set_detection_mode(value)
|
||||||
|
elif param == 'algorithm_enable':
|
||||||
|
enable_a, enable_b = value
|
||||||
|
self.arbiter.set_algorithm_enable(enable_a, enable_b)
|
||||||
|
elif param == 'work_mode':
|
||||||
|
self.arbiter.set_work_mode(value)
|
||||||
|
elif param == 'performance_optimization':
|
||||||
|
use_alternating = value
|
||||||
|
self.arbiter.set_performance_optimization(use_alternating)
|
||||||
|
elif param == 'lock_mode':
|
||||||
|
self.is_mode_locked = value
|
||||||
|
elif param == 'reset_arbiter':
|
||||||
|
self.arbiter.reset()
|
||||||
|
self.is_mode_locked = False
|
||||||
|
self.current_mode = None
|
||||||
|
|
||||||
|
def work_thread_func(self):
|
||||||
|
"""工作线程函数,集成双模式激光检测和可视化"""
|
||||||
|
st_out_frame = MV_FRAME_OUT()
|
||||||
|
memset(byref(st_out_frame), 0, sizeof(st_out_frame))
|
||||||
|
|
||||||
|
while not self.camera.b_exit:
|
||||||
|
# 获取图像
|
||||||
|
ret = self.camera.cam.MV_CC_GetImageBuffer(st_out_frame, 1000)
|
||||||
|
|
||||||
|
if ret == 0:
|
||||||
|
try:
|
||||||
|
# 计算数据大小
|
||||||
|
width = st_out_frame.stFrameInfo.nWidth
|
||||||
|
height = st_out_frame.stFrameInfo.nHeight
|
||||||
|
data_size = width * height * 3
|
||||||
|
|
||||||
|
# 从缓冲区复制数据
|
||||||
|
frame_data = np.ctypeslib.as_array(st_out_frame.pBufAddr, shape=(data_size,))
|
||||||
|
frame = frame_data.reshape((height, width, 3))
|
||||||
|
|
||||||
|
# 自动曝光调节
|
||||||
|
new_exp, adjusted = auto_exposure_adjust(frame, self.camera.current_exp)
|
||||||
|
if adjusted:
|
||||||
|
self.camera.set_exposure_time(new_exp)
|
||||||
|
|
||||||
|
# 双模式并行检测
|
||||||
|
# 模式A:单点光斑检测
|
||||||
|
mode_a_results, mask_a = self.detector_a.detect(frame)
|
||||||
|
|
||||||
|
# 模式B:点阵灯盘检测
|
||||||
|
mode_b_results, mask_b, raw_leds_b, aggregated_mask_b = self.detector_b.detect(frame)
|
||||||
|
|
||||||
|
# 智能仲裁决策
|
||||||
|
if self.is_mode_locked and self.current_mode:
|
||||||
|
# 锁定模式,使用当前模式结果
|
||||||
|
selected_mode = self.current_mode
|
||||||
|
if selected_mode == 'mode_a':
|
||||||
|
selected_results = mode_a_results
|
||||||
|
else:
|
||||||
|
selected_results = mode_b_results
|
||||||
|
debug_info = {
|
||||||
|
'mode_a_score': 0,
|
||||||
|
'mode_b_score': 0,
|
||||||
|
'decision_reason': '模式已锁定',
|
||||||
|
'processing_time': 0
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# 使用仲裁器决策
|
||||||
|
selected_mode, selected_results, debug_info = self.arbiter.arbitrate(
|
||||||
|
mode_a_results, mode_b_results, frame
|
||||||
|
)
|
||||||
|
self.current_mode = selected_mode
|
||||||
|
|
||||||
|
# 绘制检测结果
|
||||||
|
frame_with_results = self._draw_dual_mode_results(
|
||||||
|
frame.copy(),
|
||||||
|
selected_mode,
|
||||||
|
selected_results,
|
||||||
|
mode_a_results,
|
||||||
|
mode_b_results,
|
||||||
|
raw_leds_b
|
||||||
|
)
|
||||||
|
|
||||||
|
# 用户交互容错:过度调节警告
|
||||||
|
frame_with_results = self._add_user_warnings(frame_with_results)
|
||||||
|
|
||||||
|
# 计算FPS
|
||||||
|
self.fps_counter += 1
|
||||||
|
current_time = time.time()
|
||||||
|
if current_time - self.fps_start_time >= 1.0:
|
||||||
|
self.fps = self.fps_counter / (current_time - self.fps_start_time)
|
||||||
|
self.fps_counter = 0
|
||||||
|
self.fps_start_time = current_time
|
||||||
|
|
||||||
|
# 绘制状态栏
|
||||||
|
frame_with_status = self._draw_dual_mode_status_bar(
|
||||||
|
frame_with_results,
|
||||||
|
self.camera.current_exp,
|
||||||
|
self.camera.current_gain,
|
||||||
|
selected_mode,
|
||||||
|
len(mode_a_results) if mode_a_results else 0,
|
||||||
|
len(mode_b_results) if mode_b_results else 0,
|
||||||
|
debug_info,
|
||||||
|
self.fps
|
||||||
|
)
|
||||||
|
|
||||||
|
# 显示图像
|
||||||
|
cv2.imshow("Camera", frame_with_status)
|
||||||
|
|
||||||
|
# 显示掩码(可选)
|
||||||
|
if selected_mode == 'mode_a' or selected_mode == 'dual':
|
||||||
|
cv2.imshow("Mode A Mask", mask_a)
|
||||||
|
if selected_mode == 'mode_b' or selected_mode == 'dual':
|
||||||
|
cv2.imshow("Mode B Mask", mask_b)
|
||||||
|
if aggregated_mask_b is not None:
|
||||||
|
cv2.imshow("Mode B Aggregated", aggregated_mask_b)
|
||||||
|
|
||||||
|
# 检查退出键
|
||||||
|
key = cv2.waitKey(1) & 0xFF
|
||||||
|
if key == ord('q'):
|
||||||
|
self.camera.b_exit = True
|
||||||
|
break
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"处理图像失败: {e}")
|
||||||
|
finally:
|
||||||
|
# 释放缓存
|
||||||
|
self.camera.cam.MV_CC_FreeImageBuffer(st_out_frame)
|
||||||
|
else:
|
||||||
|
print(f"获取图像失败! ret = {self.camera.to_hex_str(ret)}")
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
cv2.destroyAllWindows()
|
||||||
|
|
||||||
|
def _draw_dual_mode_results(self, frame, selected_mode, selected_results, mode_a_results, mode_b_results, raw_leds_b):
|
||||||
|
"""绘制双模式检测结果"""
|
||||||
|
# 绘制模式A结果(单点光斑)
|
||||||
|
if selected_mode == 'mode_a' or selected_mode == 'dual':
|
||||||
|
if selected_mode == 'dual':
|
||||||
|
# 双模显示:显示所有模式A结果
|
||||||
|
display_results = mode_a_results
|
||||||
|
else:
|
||||||
|
# 单模式显示:显示选中的模式A结果
|
||||||
|
display_results = selected_results
|
||||||
|
|
||||||
|
for i, result in enumerate(display_results):
|
||||||
|
cx, cy, w, h, area, circularity = result
|
||||||
|
|
||||||
|
# 绘制绿色细线矩形框
|
||||||
|
cv2.rectangle(frame, (int(cx - w//2), int(cy - h//2)),
|
||||||
|
(int(cx + w//2), int(cy + h//2)), (0, 255, 0), 1)
|
||||||
|
|
||||||
|
# 绘制绿色小圆点和十字线
|
||||||
|
cv2.circle(frame, (int(cx), int(cy)), 3, (0, 255, 0), -1)
|
||||||
|
cross_size = int(w * 0.25)
|
||||||
|
cv2.line(frame, (int(cx) - cross_size, int(cy)), (int(cx) + cross_size, int(cy)), (0, 255, 0), 1)
|
||||||
|
cv2.line(frame, (int(cx), int(cy) - cross_size), (int(cx), int(cy) + cross_size), (0, 255, 0), 1)
|
||||||
|
|
||||||
|
# 绘制白色标签
|
||||||
|
info_text = f"SP: ID:{i} ({int(cx)},{int(cy)})"
|
||||||
|
cv2.putText(frame, info_text, (int(cx - w//2), int(cy - h//2) - 10),
|
||||||
|
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
|
||||||
|
|
||||||
|
# 绘制模式B结果(点阵灯盘)
|
||||||
|
if selected_mode == 'mode_b' or selected_mode == 'dual':
|
||||||
|
if selected_mode == 'dual':
|
||||||
|
# 双模显示:显示所有模式B结果
|
||||||
|
display_results = mode_b_results
|
||||||
|
else:
|
||||||
|
# 单模式显示:显示选中的模式B结果
|
||||||
|
display_results = selected_results
|
||||||
|
|
||||||
|
# 绘制原始LED点(半透明蓝色小点)
|
||||||
|
for point in raw_leds_b:
|
||||||
|
x, y = point
|
||||||
|
cv2.circle(frame, (int(x), int(y)), 2, (255, 0, 0), -1)
|
||||||
|
|
||||||
|
for result in display_results:
|
||||||
|
# 绘制青色粗线圆形
|
||||||
|
cx, cy = result['center']
|
||||||
|
radius = result['radius']
|
||||||
|
cv2.circle(frame, (int(cx), int(cy)), int(radius), (255, 255, 0), 2)
|
||||||
|
|
||||||
|
# 绘制青色十字准星
|
||||||
|
cross_size = int(radius) # 十字准星大小与半径成比例
|
||||||
|
cv2.line(frame, (int(cx) - cross_size, int(cy)), (int(cx) + cross_size, int(cy)), (255, 255, 0), 2)
|
||||||
|
cv2.line(frame, (int(cx), int(cy) - cross_size), (int(cx), int(cy) + cross_size), (255, 255, 0), 2)
|
||||||
|
|
||||||
|
# 绘制黄色标签
|
||||||
|
info_text = f"LD: ID:{result['id']} ({int(cx)},{int(cy)}) R:{int(radius)} N:{result['led_count']}"
|
||||||
|
cv2.putText(frame, info_text, (int(cx) - 100, int(cy) - int(radius) - 10),
|
||||||
|
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 255), 1)
|
||||||
|
|
||||||
|
return frame
|
||||||
|
|
||||||
|
def _draw_dual_mode_status_bar(self, frame, exposure, gain, selected_mode, mode_a_count, mode_b_count, debug_info, fps):
|
||||||
|
"""绘制双模式状态栏"""
|
||||||
|
# 计算状态栏高度
|
||||||
|
status_bar_height = 100
|
||||||
|
status_bar = np.zeros((status_bar_height, frame.shape[1], 3), dtype=np.uint8)
|
||||||
|
|
||||||
|
# 左上角:当前模式大字体标识
|
||||||
|
if selected_mode == 'mode_a':
|
||||||
|
mode_text = "MODE: AUTO-SINGLE"
|
||||||
|
mode_color = (0, 255, 0) # 绿色
|
||||||
|
elif selected_mode == 'mode_b':
|
||||||
|
mode_text = "MODE: AUTO-LAMP"
|
||||||
|
mode_color = (255, 255, 0) # 青色
|
||||||
|
elif selected_mode == 'dual':
|
||||||
|
mode_text = "MODE: DUAL DISPLAY"
|
||||||
|
mode_color = (0, 255, 255) # 蓝绿色
|
||||||
|
else:
|
||||||
|
mode_text = "MODE: NO TARGET"
|
||||||
|
mode_color = (128, 128, 128) # 灰色
|
||||||
|
|
||||||
|
cv2.putText(status_bar, mode_text, (20, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, mode_color, 2)
|
||||||
|
|
||||||
|
# 右上角:FPS和相机参数
|
||||||
|
camera_text = f"FPS: {fps:.1f} | Exposure: {exposure:.0f}μs | Gain: {gain:.1f}dB"
|
||||||
|
cv2.putText(status_bar, camera_text, (frame.shape[1] - 450, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 1)
|
||||||
|
|
||||||
|
# 中间:决策理由
|
||||||
|
decision_text = debug_info.get('decision_reason', 'No decision')
|
||||||
|
cv2.putText(status_bar, f"Decision: {decision_text}", (20, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 1)
|
||||||
|
|
||||||
|
# 底部:两种模式的实时置信度条
|
||||||
|
# 模式A置信度条
|
||||||
|
a_score = debug_info.get('mode_a_score', 0)
|
||||||
|
a_percentage = min(a_score, 100)
|
||||||
|
cv2.rectangle(status_bar, (20, 75), (320, 95), (50, 50, 50), -1)
|
||||||
|
cv2.rectangle(status_bar, (20, 75), (20 + int(a_percentage * 3), 95), (0, 255, 0), -1)
|
||||||
|
cv2.putText(status_bar, f"Mode A: {a_percentage:.1f}% ({mode_a_count} targets)", (25, 90), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
|
||||||
|
|
||||||
|
# 模式B置信度条
|
||||||
|
b_score = debug_info.get('mode_b_score', 0)
|
||||||
|
b_percentage = min(b_score, 100)
|
||||||
|
cv2.rectangle(status_bar, (340, 75), (640, 95), (50, 50, 50), -1)
|
||||||
|
cv2.rectangle(status_bar, (340, 75), (340 + int(b_percentage * 3), 95), (255, 255, 0), -1)
|
||||||
|
cv2.putText(status_bar, f"Mode B: {b_percentage:.1f}% ({mode_b_count} targets)", (345, 90), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
|
||||||
|
|
||||||
|
# 将状态栏添加到图像顶部
|
||||||
|
frame_with_status = np.vstack((status_bar, frame))
|
||||||
|
|
||||||
|
return frame_with_status
|
||||||
|
|
||||||
|
def _handle_visual_faults(self, results, frame):
|
||||||
|
"""视觉算法容错处理"""
|
||||||
|
# 处理部分遮挡情况
|
||||||
|
filtered_results = []
|
||||||
|
for result in results:
|
||||||
|
# 检查LED数量是否足够(即使部分遮挡)
|
||||||
|
if result['led_count'] >= self.detector.min_led_count * 0.7: # 允许30%遮挡
|
||||||
|
filtered_results.append(result)
|
||||||
|
|
||||||
|
# 处理多灯盘重叠情况
|
||||||
|
if len(filtered_results) > 1:
|
||||||
|
# 检查是否有重叠的灯盘
|
||||||
|
overlap_pairs = []
|
||||||
|
for i in range(len(filtered_results)):
|
||||||
|
for j in range(i + 1, len(filtered_results)):
|
||||||
|
if self._is_overlapping(filtered_results[i], filtered_results[j]):
|
||||||
|
overlap_pairs.append((i, j))
|
||||||
|
|
||||||
|
# 如果有重叠,保留置信度高的结果
|
||||||
|
if overlap_pairs:
|
||||||
|
# 按置信度排序
|
||||||
|
filtered_results.sort(key=lambda x: x['confidence'], reverse=True)
|
||||||
|
# 只保留前N个不重叠的结果
|
||||||
|
non_overlapping = []
|
||||||
|
for result in filtered_results:
|
||||||
|
overlapping = False
|
||||||
|
for existing in non_overlapping:
|
||||||
|
if self._is_overlapping(result, existing):
|
||||||
|
overlapping = True
|
||||||
|
break
|
||||||
|
if not overlapping:
|
||||||
|
non_overlapping.append(result)
|
||||||
|
filtered_results = non_overlapping
|
||||||
|
|
||||||
|
return filtered_results
|
||||||
|
|
||||||
|
def _is_overlapping(self, result1, result2):
|
||||||
|
"""判断两个灯盘是否重叠"""
|
||||||
|
cx1, cy1 = result1['center']
|
||||||
|
r1 = result1['radius']
|
||||||
|
cx2, cy2 = result2['center']
|
||||||
|
r2 = result2['radius']
|
||||||
|
|
||||||
|
distance = np.sqrt((cx1 - cx2)**2 + (cy1 - cy2)**2)
|
||||||
|
return distance < r1 + r2 * 0.5 # 允许部分重叠
|
||||||
|
|
||||||
|
def _add_user_warnings(self, frame):
|
||||||
|
"""添加用户交互警告"""
|
||||||
|
# 计算画面平均亮度
|
||||||
|
gray_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
||||||
|
mean_brightness = np.mean(gray_frame)
|
||||||
|
|
||||||
|
# 曝光过度警告
|
||||||
|
if mean_brightness > 250:
|
||||||
|
cv2.putText(frame, "⚠️ 曝光过度! 请降低曝光时间", (10, frame.shape[0] - 40),
|
||||||
|
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
|
||||||
|
|
||||||
|
# 曝光不足警告
|
||||||
|
elif mean_brightness < 10:
|
||||||
|
cv2.putText(frame, "⚠️ 曝光不足! 请增加曝光时间", (10, frame.shape[0] - 40),
|
||||||
|
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
|
||||||
|
|
||||||
|
# 增益过高警告
|
||||||
|
if self.camera.current_gain > 12:
|
||||||
|
cv2.putText(frame, "⚠️ 增益过高! 可能产生噪声", (10, frame.shape[0] - 10),
|
||||||
|
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
|
||||||
|
|
||||||
|
return frame
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""启动系统"""
|
||||||
|
print("双模式激光识别系统启动")
|
||||||
|
print("按 Q 退出显示窗口")
|
||||||
|
print("按 Ctrl+C 退出程序")
|
||||||
|
print("系统特性:")
|
||||||
|
print("- 模式A: 单点/稀疏光点检测(绿色激光笔)")
|
||||||
|
print("- 模式B: 点阵灯盘检测(LED阵列)")
|
||||||
|
print("- 智能仲裁: 自动选择最优检测模式")
|
||||||
|
print("- 性能优化: 支持交替帧策略")
|
||||||
|
|
||||||
|
# 初始化 SDK
|
||||||
|
MvCamera.MV_CC_Initialize()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 枚举设备
|
||||||
|
device_count = self.camera.enum_devices()
|
||||||
|
if device_count == 0:
|
||||||
|
print("未找到设备,程序退出!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 选择设备
|
||||||
|
n_index = int(input("请输入要打开的相机索引: "))
|
||||||
|
|
||||||
|
# 打开设备
|
||||||
|
ret = self.camera.open_device(n_index)
|
||||||
|
if ret != 0:
|
||||||
|
print("打开设备失败,程序退出!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 替换工作线程函数
|
||||||
|
original_work_thread = self.camera.work_thread_func
|
||||||
|
self.camera.work_thread_func = self.work_thread_func
|
||||||
|
|
||||||
|
# 开始取流
|
||||||
|
ret = self.camera.start_grabbing()
|
||||||
|
if ret != 0:
|
||||||
|
print("开始取流失败,程序退出!")
|
||||||
|
self.camera.close_device()
|
||||||
|
return
|
||||||
|
|
||||||
|
# 启动参数控制面板
|
||||||
|
self.config_panel = ConfigPanel(self.on_config_update)
|
||||||
|
self.config_panel.start()
|
||||||
|
|
||||||
|
# 等待用户操作
|
||||||
|
while True:
|
||||||
|
time.sleep(1)
|
||||||
|
if not self.camera.is_grabbing:
|
||||||
|
break
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n用户中断程序")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"程序异常: {e}")
|
||||||
|
finally:
|
||||||
|
# 清理资源
|
||||||
|
if self.config_panel:
|
||||||
|
self.config_panel.stop()
|
||||||
|
|
||||||
|
if self.camera.is_open:
|
||||||
|
self.camera.close_device()
|
||||||
|
|
||||||
|
# 反初始化 SDK
|
||||||
|
MvCamera.MV_CC_Finalize()
|
||||||
|
print("SDK反初始化成功")
|
||||||
|
print("程序退出")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
system = DualModeLaserDetectionSystem()
|
||||||
|
system.start()
|
||||||
60
utils.py
Normal file
60
utils.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
def draw_detection_results(frame, results):
|
||||||
|
"""
|
||||||
|
在图像上绘制检测结果
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: BGR格式的图像
|
||||||
|
results: 检测结果列表,每个元素为(cx, cy, w, h, area, circularity)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
frame: 绘制后的图像
|
||||||
|
"""
|
||||||
|
for result in results:
|
||||||
|
cx, cy, w, h, area, circularity = result
|
||||||
|
|
||||||
|
# 计算矩形框坐标
|
||||||
|
x = cx - w // 2
|
||||||
|
y = cy - h // 2
|
||||||
|
|
||||||
|
# 绘制绿色矩形框
|
||||||
|
cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
|
||||||
|
|
||||||
|
# 绘制十字准星
|
||||||
|
cv2.line(frame, (cx - 10, cy), (cx + 10, cy), (0, 255, 0), 2)
|
||||||
|
cv2.line(frame, (cx, cy - 10), (cx, cy + 10), (0, 255, 0), 2)
|
||||||
|
|
||||||
|
# 绘制坐标文字
|
||||||
|
text = f"({cx}, {cy})"
|
||||||
|
cv2.putText(frame, text, (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
|
||||||
|
|
||||||
|
return frame
|
||||||
|
|
||||||
|
def draw_status_bar(frame, exposure_time, gain, detected_count, fps):
|
||||||
|
"""
|
||||||
|
在图像上绘制状态栏信息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: BGR格式的图像
|
||||||
|
exposure_time: 当前曝光时间(微秒)
|
||||||
|
gain: 当前增益(dB)
|
||||||
|
detected_count: 检测到的目标数
|
||||||
|
fps: 当前帧率
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
frame: 绘制后的图像
|
||||||
|
"""
|
||||||
|
# 在图像顶部添加状态栏
|
||||||
|
status_bar = np.zeros((30, frame.shape[1], 3), dtype=np.uint8)
|
||||||
|
|
||||||
|
# 绘制信息文字
|
||||||
|
text = f"Exposure: {exposure_time:.0f}μs | Gain: {gain:.1f}dB | Detected: {detected_count} | FPS: {fps:.1f}"
|
||||||
|
cv2.putText(status_bar, text, (10, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
|
||||||
|
|
||||||
|
# 将状态栏添加到图像顶部
|
||||||
|
frame_with_status = np.vstack((status_bar, frame))
|
||||||
|
|
||||||
|
return frame_with_status
|
||||||
342
verify_camera.py
Normal file
342
verify_camera.py
Normal file
@ -0,0 +1,342 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import sys
|
||||||
|
import ctypes
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
|
||||||
|
# 添加SDK路径
|
||||||
|
sys.path.append('Python/MvImport')
|
||||||
|
from MvCameraControl_class import *
|
||||||
|
from MvErrorDefine_const import *
|
||||||
|
from CameraParams_header import *
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
# 初始化全局变量
|
||||||
|
global deviceList
|
||||||
|
deviceList = MV_CC_DEVICE_INFO_LIST()
|
||||||
|
global cam
|
||||||
|
cam = MvCamera()
|
||||||
|
global nSelCamIndex
|
||||||
|
nSelCamIndex = 0
|
||||||
|
global isOpen
|
||||||
|
isOpen = False
|
||||||
|
global isGrabbing
|
||||||
|
isGrabbing = False
|
||||||
|
global bExit
|
||||||
|
bExit = False
|
||||||
|
|
||||||
|
# 将返回的错误码转换为十六进制显示
|
||||||
|
def ToHexStr(num):
|
||||||
|
chaDic = {10: 'a', 11: 'b', 12: 'c', 13: 'd', 14: 'e', 15: 'f'}
|
||||||
|
hexStr = ""
|
||||||
|
if num < 0:
|
||||||
|
num = num + 2 ** 32
|
||||||
|
while num >= 16:
|
||||||
|
digit = num % 16
|
||||||
|
hexStr = chaDic.get(digit, str(digit)) + hexStr
|
||||||
|
num //= 16
|
||||||
|
hexStr = chaDic.get(num, str(num)) + hexStr
|
||||||
|
return hexStr
|
||||||
|
|
||||||
|
# Decoding Characters
|
||||||
|
def decoding_char(ctypes_char_array):
|
||||||
|
"""
|
||||||
|
安全地从 ctypes 字符数组中解码出字符串。
|
||||||
|
适用于 Python 2.x 和 3.x,以及 32/64 位环境。
|
||||||
|
"""
|
||||||
|
byte_str = memoryview(ctypes_char_array).tobytes()
|
||||||
|
|
||||||
|
# 在第一个空字符处截断
|
||||||
|
null_index = byte_str.find(b'\x00')
|
||||||
|
if null_index != -1:
|
||||||
|
byte_str = byte_str[:null_index]
|
||||||
|
|
||||||
|
# 多编码尝试解码
|
||||||
|
for encoding in ['gbk', 'utf-8', 'latin-1']:
|
||||||
|
try:
|
||||||
|
return byte_str.decode(encoding)
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 如果所有编码都失败,使用替换策略
|
||||||
|
return byte_str.decode('latin-1', errors='replace')
|
||||||
|
|
||||||
|
# 枚举相机
|
||||||
|
def enum_devices():
|
||||||
|
global deviceList
|
||||||
|
|
||||||
|
deviceList = MV_CC_DEVICE_INFO_LIST()
|
||||||
|
n_layer_type = (MV_GIGE_DEVICE | MV_USB_DEVICE)
|
||||||
|
ret = MvCamera.MV_CC_EnumDevices(n_layer_type, deviceList)
|
||||||
|
if ret != 0:
|
||||||
|
print(f"枚举设备失败! ret = :{ToHexStr(ret)}")
|
||||||
|
return ret
|
||||||
|
|
||||||
|
if deviceList.nDeviceNum == 0:
|
||||||
|
print("未找到设备")
|
||||||
|
return ret
|
||||||
|
print(f"找到 {deviceList.nDeviceNum} 台设备!")
|
||||||
|
|
||||||
|
devList = []
|
||||||
|
for i in range(0, deviceList.nDeviceNum):
|
||||||
|
mvcc_dev_info = cast(deviceList.pDeviceInfo[i], POINTER(MV_CC_DEVICE_INFO)).contents
|
||||||
|
if mvcc_dev_info.nTLayerType == MV_GIGE_DEVICE:
|
||||||
|
print(f"\ngige device: [{i}]")
|
||||||
|
user_defined_name = decoding_char(mvcc_dev_info.SpecialInfo.stGigEInfo.chUserDefinedName)
|
||||||
|
model_name = decoding_char(mvcc_dev_info.SpecialInfo.stGigEInfo.chModelName)
|
||||||
|
print(f"device user define name: {user_defined_name}")
|
||||||
|
print(f"device model name: {model_name}")
|
||||||
|
|
||||||
|
nip1 = ((mvcc_dev_info.SpecialInfo.stGigEInfo.nCurrentIp & 0xff000000) >> 24)
|
||||||
|
nip2 = ((mvcc_dev_info.SpecialInfo.stGigEInfo.nCurrentIp & 0x00ff0000) >> 16)
|
||||||
|
nip3 = ((mvcc_dev_info.SpecialInfo.stGigEInfo.nCurrentIp & 0x0000ff00) >> 8)
|
||||||
|
nip4 = (mvcc_dev_info.SpecialInfo.stGigEInfo.nCurrentIp & 0x000000ff)
|
||||||
|
print(f"current ip: {nip1}.{nip2}.{nip3}.{nip4} ")
|
||||||
|
devList.append(f"[{i}]GigE: {user_defined_name} {model_name}({nip1}.{nip2}.{nip3}.{nip4})")
|
||||||
|
elif mvcc_dev_info.nTLayerType == MV_USB_DEVICE:
|
||||||
|
print(f"\nu3v device: [{i}]")
|
||||||
|
user_defined_name = decoding_char(mvcc_dev_info.SpecialInfo.stUsb3VInfo.chUserDefinedName)
|
||||||
|
model_name = decoding_char(mvcc_dev_info.SpecialInfo.stUsb3VInfo.chModelName)
|
||||||
|
print(f"device user define name: {user_defined_name}")
|
||||||
|
print(f"device model name: {model_name}")
|
||||||
|
|
||||||
|
strSerialNumber = ""
|
||||||
|
for per in mvcc_dev_info.SpecialInfo.stUsb3VInfo.chSerialNumber:
|
||||||
|
if per == 0:
|
||||||
|
break
|
||||||
|
strSerialNumber = strSerialNumber + chr(per)
|
||||||
|
print(f"user serial number: {strSerialNumber}")
|
||||||
|
devList.append(f"[{i}]USB: {user_defined_name} {model_name}({strSerialNumber})")
|
||||||
|
|
||||||
|
# 打印设备列表
|
||||||
|
print("\n设备列表:")
|
||||||
|
for i, dev in enumerate(devList):
|
||||||
|
print(f"{i}: {dev}")
|
||||||
|
|
||||||
|
return deviceList.nDeviceNum
|
||||||
|
|
||||||
|
# 打开相机
|
||||||
|
def open_device(nIndex):
|
||||||
|
global cam
|
||||||
|
global deviceList
|
||||||
|
global isOpen
|
||||||
|
|
||||||
|
if isOpen:
|
||||||
|
print("相机已经打开!")
|
||||||
|
return MV_E_CALLORDER
|
||||||
|
|
||||||
|
if nIndex < 0 or nIndex >= deviceList.nDeviceNum:
|
||||||
|
print("请选择有效的相机索引!")
|
||||||
|
return MV_E_CALLORDER
|
||||||
|
|
||||||
|
# 选择设备并创建句柄
|
||||||
|
stDeviceList = cast(deviceList.pDeviceInfo[nIndex], POINTER(MV_CC_DEVICE_INFO)).contents
|
||||||
|
cam = MvCamera()
|
||||||
|
ret = cam.MV_CC_CreateHandle(stDeviceList)
|
||||||
|
if ret != 0:
|
||||||
|
print(f"创建设备句柄失败! ret = {ToHexStr(ret)}")
|
||||||
|
cam.MV_CC_DestroyHandle()
|
||||||
|
return ret
|
||||||
|
|
||||||
|
# 打开设备
|
||||||
|
ret = cam.MV_CC_OpenDevice()
|
||||||
|
if ret != 0:
|
||||||
|
print(f"打开设备失败! ret = {ToHexStr(ret)}")
|
||||||
|
cam.MV_CC_DestroyHandle()
|
||||||
|
return ret
|
||||||
|
|
||||||
|
print("设备打开成功!")
|
||||||
|
isOpen = True
|
||||||
|
|
||||||
|
# 设置触发模式为off
|
||||||
|
ret = cam.MV_CC_SetEnumValue("TriggerMode", MV_TRIGGER_MODE_OFF)
|
||||||
|
if ret != 0:
|
||||||
|
print(f"设置触发模式失败! ret = {ToHexStr(ret)}")
|
||||||
|
|
||||||
|
# 设置像素格式为 BGR8
|
||||||
|
ret = cam.MV_CC_SetEnumValue("PixelFormat", PixelType_Gvsp_BGR8_Packed)
|
||||||
|
if ret != 0:
|
||||||
|
print(f"设置像素格式失败! ret = {ToHexStr(ret)}")
|
||||||
|
|
||||||
|
# 设置连续采集模式
|
||||||
|
ret = cam.MV_CC_SetEnumValue("AcquisitionMode", 2) # Continuous
|
||||||
|
if ret != 0:
|
||||||
|
print(f"设置连续采集模式失败! ret = {ToHexStr(ret)}")
|
||||||
|
|
||||||
|
return MV_OK
|
||||||
|
|
||||||
|
# 开始取流
|
||||||
|
def start_grabbing():
|
||||||
|
global cam
|
||||||
|
global isGrabbing
|
||||||
|
global bExit
|
||||||
|
|
||||||
|
if not isOpen:
|
||||||
|
print("相机未打开!")
|
||||||
|
return MV_E_CALLORDER
|
||||||
|
|
||||||
|
if isGrabbing:
|
||||||
|
print("已经开始取流!")
|
||||||
|
return MV_E_CALLORDER
|
||||||
|
|
||||||
|
bExit = False
|
||||||
|
ret = cam.MV_CC_StartGrabbing()
|
||||||
|
if ret != 0:
|
||||||
|
print(f"开始取流失败! ret = {ToHexStr(ret)}")
|
||||||
|
return ret
|
||||||
|
|
||||||
|
isGrabbing = True
|
||||||
|
print("开始取流成功!")
|
||||||
|
|
||||||
|
# 启动取图线程
|
||||||
|
global hThreadHandle
|
||||||
|
hThreadHandle = threading.Thread(target=work_thread)
|
||||||
|
hThreadHandle.daemon = True
|
||||||
|
hThreadHandle.start()
|
||||||
|
|
||||||
|
return MV_OK
|
||||||
|
|
||||||
|
# 停止取流
|
||||||
|
def stop_grabbing():
|
||||||
|
global cam
|
||||||
|
global isGrabbing
|
||||||
|
global bExit
|
||||||
|
|
||||||
|
if not isOpen:
|
||||||
|
print("相机未打开!")
|
||||||
|
return MV_E_CALLORDER
|
||||||
|
|
||||||
|
if not isGrabbing:
|
||||||
|
print("未开始取流!")
|
||||||
|
return MV_E_CALLORDER
|
||||||
|
|
||||||
|
bExit = True
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
ret = cam.MV_CC_StopGrabbing()
|
||||||
|
if ret != 0:
|
||||||
|
print(f"停止取流失败! ret = {ToHexStr(ret)}")
|
||||||
|
return ret
|
||||||
|
|
||||||
|
isGrabbing = False
|
||||||
|
print("停止取流成功!")
|
||||||
|
|
||||||
|
return MV_OK
|
||||||
|
|
||||||
|
# 关闭相机
|
||||||
|
def close_device():
|
||||||
|
global cam
|
||||||
|
global isOpen
|
||||||
|
global isGrabbing
|
||||||
|
|
||||||
|
if isGrabbing:
|
||||||
|
stop_grabbing()
|
||||||
|
|
||||||
|
if isOpen:
|
||||||
|
ret = cam.MV_CC_CloseDevice()
|
||||||
|
if ret != 0:
|
||||||
|
print(f"关闭设备失败! ret = {ToHexStr(ret)}")
|
||||||
|
|
||||||
|
# 销毁句柄
|
||||||
|
cam.MV_CC_DestroyHandle()
|
||||||
|
isOpen = False
|
||||||
|
print("设备关闭成功!")
|
||||||
|
|
||||||
|
# 取图线程函数
|
||||||
|
def work_thread():
|
||||||
|
global cam
|
||||||
|
global bExit
|
||||||
|
|
||||||
|
stOutFrame = MV_FRAME_OUT()
|
||||||
|
memset(byref(stOutFrame), 0, sizeof(stOutFrame))
|
||||||
|
|
||||||
|
while not bExit:
|
||||||
|
# 获取图像
|
||||||
|
ret = cam.MV_CC_GetImageBuffer(stOutFrame, 1000)
|
||||||
|
|
||||||
|
if ret == 0:
|
||||||
|
# 打印图像信息
|
||||||
|
print(f"获取一帧图像: 宽度[{stOutFrame.stFrameInfo.nWidth}], 高度[{stOutFrame.stFrameInfo.nHeight}], 帧数[{stOutFrame.stFrameInfo.nFrameNum}]")
|
||||||
|
|
||||||
|
# 转换为 OpenCV 格式
|
||||||
|
try:
|
||||||
|
# 计算数据大小
|
||||||
|
data_size = stOutFrame.stFrameInfo.nWidth * stOutFrame.stFrameInfo.nHeight * 3
|
||||||
|
|
||||||
|
# 从缓冲区复制数据
|
||||||
|
frame_data = np.ctypeslib.as_array(stOutFrame.pBufAddr, shape=(data_size,))
|
||||||
|
frame_data = frame_data.reshape((stOutFrame.stFrameInfo.nHeight, stOutFrame.stFrameInfo.nWidth, 3))
|
||||||
|
|
||||||
|
# 显示图像
|
||||||
|
cv2.imshow("Camera", frame_data)
|
||||||
|
key = cv2.waitKey(1) & 0xFF
|
||||||
|
if key == ord('q'):
|
||||||
|
bExit = True
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f"处理图像失败: {e}")
|
||||||
|
finally:
|
||||||
|
# 释放缓存
|
||||||
|
cam.MV_CC_FreeImageBuffer(stOutFrame)
|
||||||
|
else:
|
||||||
|
print(f"获取图像失败! ret = {ToHexStr(ret)}")
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
cv2.destroyAllWindows()
|
||||||
|
|
||||||
|
# 主函数
|
||||||
|
def main():
|
||||||
|
print("海康相机验证程序")
|
||||||
|
print("按 Q 退出显示窗口")
|
||||||
|
print("按 Ctrl+C 退出程序")
|
||||||
|
|
||||||
|
# 初始化 SDK
|
||||||
|
MvCamera.MV_CC_Initialize()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 枚举设备
|
||||||
|
device_count = enum_devices()
|
||||||
|
if device_count == 0:
|
||||||
|
print("未找到设备,程序退出!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 选择设备
|
||||||
|
nIndex = int(input("请输入要打开的相机索引: "))
|
||||||
|
|
||||||
|
# 打开设备
|
||||||
|
ret = open_device(nIndex)
|
||||||
|
if ret != MV_OK:
|
||||||
|
print("打开设备失败,程序退出!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 开始取流
|
||||||
|
ret = start_grabbing()
|
||||||
|
if ret != MV_OK:
|
||||||
|
print("开始取流失败,程序退出!")
|
||||||
|
close_device()
|
||||||
|
return
|
||||||
|
|
||||||
|
# 等待用户操作
|
||||||
|
while True:
|
||||||
|
time.sleep(1)
|
||||||
|
if not isGrabbing:
|
||||||
|
break
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n用户中断程序")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"程序异常: {e}")
|
||||||
|
finally:
|
||||||
|
# 清理资源
|
||||||
|
if isOpen:
|
||||||
|
close_device()
|
||||||
|
|
||||||
|
# 反初始化 SDK
|
||||||
|
MvCamera.MV_CC_Finalize()
|
||||||
|
print("SDK反初始化成功")
|
||||||
|
print("程序退出")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
Reference in New Issue
Block a user