commit fcf16fe0d3e3712a6a8215da5841b19e75fad7b2 Author: Chen-qwq <293990434@qq.com> Date: Sat Feb 21 23:42:50 2026 +0800 init commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f7383c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +Python +__pycache__ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..bf88671 --- /dev/null +++ b/README.md @@ -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,并且相机已经正确连接到计算机。 diff --git a/arbitrator.py b/arbitrator.py new file mode 100644 index 0000000..c2f556e --- /dev/null +++ b/arbitrator.py @@ -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 + } diff --git a/auto_exposure.py b/auto_exposure.py new file mode 100644 index 0000000..d7f532d --- /dev/null +++ b/auto_exposure.py @@ -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 diff --git a/camera.py b/camera.py new file mode 100644 index 0000000..6978f7a --- /dev/null +++ b/camera.py @@ -0,0 +1,1006 @@ +# -*- coding: utf-8 -*- +""" +海康相机管理模块 + +本模块提供了完整的海康相机管理功能,包括: +1. 设备枚举和选择 +2. 相机打开和关闭 +3. 图像采集和显示 +4. 曝光、增益等参数控制 +5. 异常处理和重连机制 +6. 相机参数控制界面 + +前置条件: +1. 已安装海康 MVS SDK +2. 已配置 Python 环境 +3. 相机已正确连接 +""" + +import sys +import ctypes +import threading +import time +import os +import tkinter as tk +from tkinter import ttk + +# 添加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 Camera: + """ + 海康相机管理类 + + 功能: + - 设备枚举和选择 + - 相机打开和关闭 + - 图像采集和显示 + - 曝光、增益等参数控制 + - 异常处理和重连机制 + """ + + def __init__(self): + """ + 初始化相机管理器 + """ + # 初始化SDK + MvCamera.MV_CC_Initialize() + + 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 # 当前帧率 + self.last_frame = None # 最后一帧图像 + + def to_hex_str(self, num): + """ + 将返回的错误码转换为十六进制显示 + + Args: + num: 错误码 + + Returns: + str: 十六进制字符串 + """ + 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 + + def decoding_char(self, ctypes_char_array): + """ + 安全地从 ctypes 字符数组中解码出字符串 + + Args: + ctypes_char_array: ctypes字符数组 + + Returns: + str: 解码后的字符串 + """ + 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): + """ + 枚举相机设备 + + Returns: + int: 设备数量 + """ + 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): + """ + 打开相机设备 + + Args: + n_index: 设备索引 + + Returns: + int: 错误码 + """ + 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): + """ + 设置曝光时间(微秒) + + Args: + microseconds: 曝光时间(微秒) + + Returns: + int: 错误码 + """ + 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) + + Args: + db: 增益值(dB) + + Returns: + int: 错误码 + """ + 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): + """ + 设置帧率 + + Args: + fps: 帧率值 + + Returns: + int: 错误码 + """ + 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 + + Args: + x: X坐标 + y: Y坐标 + w: 宽度 + h: 高度 + + Returns: + int: 错误码 + """ + 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): + """ + 开始取流 + + Returns: + int: 错误码 + """ + 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): + """ + 停止取流 + + Returns: + int: 错误码 + """ + 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)) + + # 保存最后一帧图像 + self.last_frame = frame + + # 显示图像 + 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}") + + def get_frame(self): + """ + 获取当前帧图像 + + Returns: + tuple: (ret, frame) + """ + if not self.is_open or not self.is_grabbing: + return False, None + + return True, self.last_frame + + def __del__(self): + """ + 析构函数,确保资源释放 + """ + self.close_device() + # 反初始化SDK + try: + MvCamera.MV_CC_Finalize() + print("SDK反初始化成功") + except Exception as e: + print(f"SDK反初始化失败: {e}") + + +# SDK安装指南 +""" +海康 MVS 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 + +5. 安装依赖 + - pip install opencv-python + - pip install numpy + +6. 测试安装 + - 运行本脚本:python camera.py + - 查看是否能成功枚举设备 + +7. 常见问题 + - 找不到 MvCameraControl_class:检查 MvImport 目录是否正确 + - 相机无法打开:检查相机是否被其他程序占用 + - 图像获取失败:检查相机连接和参数设置 +""" + + +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__": + """ + 测试相机功能 + """ + print("海康相机测试程序") + print("按 Q 退出显示窗口") + print("按 Ctrl+C 退出程序") + + # 创建相机实例 + camera = Camera() + + try: + # 创建控制界面 + root = tk.Tk() + app = CameraControlPanel(root, camera) + + # 运行主循环 + root.mainloop() + + except KeyboardInterrupt: + print("\n用户中断程序") + except Exception as e: + print(f"程序异常: {e}") + finally: + # 清理资源 + camera.close_device() + print("程序退出") diff --git a/camera_control.py b/camera_control.py new file mode 100644 index 0000000..457eafa --- /dev/null +++ b/camera_control.py @@ -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() diff --git a/camera_hik.py b/camera_hik.py new file mode 100644 index 0000000..c6b5f41 --- /dev/null +++ b/camera_hik.py @@ -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}") diff --git a/config_panel.py b/config_panel.py new file mode 100644 index 0000000..c75cda1 --- /dev/null +++ b/config_panel.py @@ -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) diff --git a/detector.py b/detector.py new file mode 100644 index 0000000..2cf6daf --- /dev/null +++ b/detector.py @@ -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 diff --git a/dot_matrix_detector.py b/dot_matrix_detector.py new file mode 100644 index 0000000..e84f4c0 --- /dev/null +++ b/dot_matrix_detector.py @@ -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 diff --git a/main.py b/main.py new file mode 100644 index 0000000..352c5ff --- /dev/null +++ b/main.py @@ -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() diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..f91ff8b --- /dev/null +++ b/utils.py @@ -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 diff --git a/verify_camera.py b/verify_camera.py new file mode 100644 index 0000000..29b8d0f --- /dev/null +++ b/verify_camera.py @@ -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()