BodyBalanceEvaluation/backend/devices/camera_manager.py

1076 lines
42 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
普通相机管理器
负责普通USB摄像头的连接配置和数据采集
"""
import cv2
import threading
import time
import base64
import numpy as np
from typing import Optional, Dict, Any
import logging
2025-08-21 12:04:14 +08:00
import queue
import gc
try:
from .base_device import BaseDevice
from .utils.config_manager import ConfigManager
except ImportError:
from base_device import BaseDevice
from utils.config_manager import ConfigManager
class CameraManager(BaseDevice):
"""普通相机管理器"""
def __init__(self, socketio, config_manager: Optional[ConfigManager] = None):
"""
初始化相机管理器
Args:
socketio: SocketIO实例
config_manager: 配置管理器实例
"""
# 配置管理
self.config_manager = config_manager or ConfigManager()
config = self.config_manager.get_device_config('camera')
super().__init__("camera", config)
# 保存socketio实例
self._socketio = socketio
# 相机相关属性
self.cap = None
self.device_index = config.get('device_index', 0)
self.width = config.get('width', 1280)
self.height = config.get('height', 720)
self.fps = config.get('fps', 30)
self.buffer_size = config.get('buffer_size', 1)
self.fourcc = config.get('fourcc', 'MJPG')
# OpenCV后端配置 (DirectShow性能最佳)
backend_name = config.get('backend', 'directshow').lower()
self.backend_map = {
'directshow': cv2.CAP_DSHOW,
'dshow': cv2.CAP_DSHOW,
'msmf': cv2.CAP_MSMF,
'any': cv2.CAP_ANY
}
self.preferred_backend = self.backend_map.get(backend_name, cv2.CAP_DSHOW)
self.backend_name = backend_name
2025-08-18 18:30:49 +08:00
# 额外可调的降采样宽度(不改变外部配置语义,仅内部优化传输)
self._tx_max_width = int(config.get('tx_max_width', 640))
# 流控制
self.streaming_thread = None
2025-08-18 18:30:49 +08:00
# 减小缓存长度保留最近2帧即可避免累计占用
2025-08-21 12:04:14 +08:00
self.frame_cache = queue.Queue(maxsize=int(config.get('frame_cache_len', 2)))
self.last_frame = None
self.frame_count = 0
self.dropped_frames = 0
# 性能监控
self.fps_counter = 0
self.fps_start_time = time.time()
self.actual_fps = 0
# 重连与断连检测机制(-1 表示无限重连)
self.max_reconnect_attempts = int(config.get('max_reconnect_attempts', -1))
self.reconnect_delay = float(config.get('reconnect_delay', 2.0))
self.read_fail_threshold = int(config.get('read_fail_threshold', 30))
self._last_connected_state = None
# 设备标识和性能统计
self.device_id = f"camera_{self.device_index}"
self.performance_stats = {
'frames_processed': 0,
'actual_fps': 0,
'dropped_frames': 0
}
2025-08-20 10:30:51 +08:00
2025-08-21 12:04:14 +08:00
# 全局帧队列(用于录制)
self.frame_queue = queue.Queue(maxsize=10) # 最大长度10自动丢弃旧帧
# 属性缓存机制 - 避免重复设置相同属性值
self._property_cache = {}
self._cache_enabled = True
2025-08-18 18:30:49 +08:00
# OpenCV优化开关和性能设置
2025-08-18 18:30:49 +08:00
try:
cv2.setUseOptimized(True)
# 设置OpenCV线程数以提高性能
cv2.setNumThreads(4) # 使用4个线程
2025-08-18 18:30:49 +08:00
except Exception:
pass
self.logger.info(f"相机管理器初始化完成 - 设备索引: {self.device_index}")
def _set_property_optimized(self, prop, value):
"""
优化的属性设置方法使用缓存避免重复设置
Args:
prop: OpenCV属性常量
value: 属性值
Returns:
bool: 是否实际执行了设置操作
"""
if not self.cap:
return False
# 检查缓存,避免重复设置相同值
if self._cache_enabled and prop in self._property_cache:
if self._property_cache[prop] == value:
return False # 值未改变,跳过设置
# 执行属性设置
result = self.cap.set(prop, value)
# 更新缓存
if self._cache_enabled:
self._property_cache[prop] = value
return True
def initialize(self) -> bool:
"""
初始化相机设备
Returns:
bool: 初始化是否成功
"""
start_time = time.time()
try:
self.logger.info(f"正在初始化相机设备...")
# 使用构造函数中已加载的配置,避免并发读取配置文件
config_time = time.time()
self.logger.info(f"使用已加载配置: device_index={self.device_index}, resolution={self.width}x{self.height}, fps={self.fps} (耗时: {(config_time - start_time)*1000:.1f}ms)")
# 使用配置的后端,如果失败则尝试其他后端
if self.preferred_backend == cv2.CAP_DSHOW:
backends = [cv2.CAP_DSHOW, cv2.CAP_MSMF, cv2.CAP_ANY]
elif self.preferred_backend == cv2.CAP_MSMF:
backends = [cv2.CAP_MSMF, cv2.CAP_DSHOW, cv2.CAP_ANY]
else:
backends = [self.preferred_backend, cv2.CAP_DSHOW, cv2.CAP_MSMF, cv2.CAP_ANY]
camera_open_time = time.time()
for backend in backends:
backend_start = time.time()
try:
# 快速打开相机,减少超时等待
self.cap = cv2.VideoCapture(self.device_index, backend)
# 设置较短的超时时间以加快检测
if hasattr(cv2, 'CAP_PROP_OPEN_TIMEOUT_MSEC'):
self.cap.set(cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, 3000) # 3秒超时
if self.cap.isOpened():
backend_time = (time.time() - backend_start) * 1000
self.logger.info(f"使用后端 {backend} 成功打开相机 (耗时: {backend_time:.1f}ms)")
break
except Exception as e:
backend_time = (time.time() - backend_start) * 1000
self.logger.warning(f"后端 {backend} 打开相机失败: {e} (耗时: {backend_time:.1f}ms)")
continue
else:
total_open_time = (time.time() - camera_open_time) * 1000
self.logger.warning(f"所有后端都无法打开相机,相机设备不可用 (总耗时: {total_open_time:.1f}ms)")
return False
total_open_time = (time.time() - camera_open_time) * 1000
self.logger.info(f"相机打开完成 (总耗时: {total_open_time:.1f}ms)")
# 设置相机属性
config_start = time.time()
self._configure_camera()
config_time = (time.time() - config_start) * 1000
self.logger.info(f"相机配置完成 (耗时: {config_time:.1f}ms)")
# 验证相机是否正常工作(优化测试过程)
test_start = time.time()
if not self._test_camera():
test_time = (time.time() - test_start) * 1000
self.logger.warning(f"相机测试失败,相机设备不可用 (耗时: {test_time:.1f}ms)")
return False
test_time = (time.time() - test_start) * 1000
self.logger.info(f"相机测试完成 (耗时: {test_time:.1f}ms)")
# 使用set_connected方法来正确启动连接监控线程
self.set_connected(True)
self._last_connected_state = True
self._device_info.update({
'device_index': self.device_index,
'resolution': f"{self.width}x{self.height}",
'fps': self.fps,
'backend': self.cap.getBackendName() if hasattr(self.cap, 'getBackendName') else 'Unknown'
})
total_time = (time.time() - start_time) * 1000
self.logger.info(f"相机初始化成功 (总耗时: {total_time:.1f}ms)")
return True
except Exception as e:
error_msg = f"相机初始化失败: {e}"
# 提供更详细的错误信息和解决建议
if "所有后端都无法打开相机" in str(e):
error_msg += f"\n可能原因:\n1. 相机设备索引 {self.device_index} 不存在或被其他程序占用\n2. 相机驱动程序未正确安装\n3. 相机硬件连接问题\n建议:检查相机连接状态,尝试更换设备索引或重启相机设备"
elif "Camera index out of range" in str(e):
error_msg += f"\n相机设备索引 {self.device_index} 超出范围,请检查可用的相机设备"
elif "backend" in str(e).lower():
error_msg += "\n相机后端初始化失败,可能是驱动程序问题"
self.logger.error(error_msg)
self.is_connected = False
if self.cap:
2025-08-18 18:30:49 +08:00
try:
self.cap.release()
except Exception:
pass
self.cap = None
return False
def _configure_camera(self):
"""
配置相机参数
"""
if not self.cap:
return
try:
# 批量设置相机属性以提高效率
config_start = time.time()
# 设置缓冲区大小(优先设置,减少延迟)
buffer_start = time.time()
try:
self.cap.set(cv2.CAP_PROP_BUFFERSIZE, min(self.buffer_size, 1)) # 使用最小缓冲区减少延迟
except Exception:
pass
buffer_time = (time.time() - buffer_start) * 1000
self.logger.debug(f"缓冲区设置耗时: {buffer_time:.1f}ms")
# 性能优化设置:禁用可能导致延迟的自动功能
optimization_start = time.time()
try:
# 禁用自动曝光以减少处理时间
self.cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0.25) # 手动曝光模式
# 禁用自动白平衡
self.cap.set(cv2.CAP_PROP_AUTO_WB, 0)
# 设置较低的曝光值以减少延迟
self.cap.set(cv2.CAP_PROP_EXPOSURE, -6)
except Exception as e:
self.logger.debug(f"设置性能优化参数时出现警告: {e}")
optimization_time = (time.time() - optimization_start) * 1000
self.logger.debug(f"性能优化设置耗时: {optimization_time:.1f}ms")
# 激进优化:跳过非关键属性设置,只设置必要属性
resolution_start = time.time()
# 优先级属性设置:只设置最关键的属性
critical_properties = [
(cv2.CAP_PROP_FRAME_WIDTH, self.width),
(cv2.CAP_PROP_FRAME_HEIGHT, self.height)
]
# 可选属性(在某些情况下可以跳过)
optional_properties = [
(cv2.CAP_PROP_FPS, self.fps)
]
batch_start = time.time()
actual_sets = 0
skipped_sets = 0
# 设置关键属性
for prop, value in critical_properties:
try:
if self._set_property_optimized(prop, value):
actual_sets += 1
else:
skipped_sets += 1
except Exception as e:
self.logger.debug(f"设置属性 {prop} 失败: {e}")
skipped_sets += 1
# 条件设置可选属性(如果时间允许)
current_time = (time.time() - batch_start) * 1000
if current_time < 1000: # 如果已用时间少于1秒才设置FPS
for prop, value in optional_properties:
try:
if self._set_property_optimized(prop, value):
actual_sets += 1
else:
skipped_sets += 1
except Exception as e:
self.logger.debug(f"设置可选属性 {prop} 失败: {e}")
skipped_sets += 1
else:
self.logger.debug(f"跳过FPS设置以节省时间 (已耗时: {current_time:.1f}ms)")
skipped_sets += len(optional_properties)
batch_time = (time.time() - batch_start) * 1000
resolution_time = (time.time() - resolution_start) * 1000
self.logger.debug(f"优化属性设置耗时: {batch_time:.1f}ms, 实际设置: {actual_sets}, 跳过: {skipped_sets}, 总计: {resolution_time:.1f}ms")
# 将原来的单独计时变量设为批量时间的一部分用于兼容性
width_time = batch_time * 0.5 # 估算宽度设置占比
height_time = batch_time * 0.4 # 估算高度设置占比
fps_time = batch_time * 0.1 # 估算帧率设置占比(可能被跳过)
# 设置FOURCC编码如果需要
if self.fourcc:
fourcc_start = time.time()
fourcc_code = cv2.VideoWriter_fourcc(*self.fourcc)
self.cap.set(cv2.CAP_PROP_FOURCC, fourcc_code)
fourcc_time = (time.time() - fourcc_start) * 1000
self.logger.debug(f"FOURCC设置耗时: {fourcc_time:.1f}ms")
# 激进优化:延迟验证机制 - 只在必要时验证
verification_start = time.time()
# 检查是否需要验证(基于配置或调试需求)
need_verification = self.logger.isEnabledFor(logging.DEBUG) or getattr(self, '_force_verification', False)
if need_verification:
# 只读取关键属性进行验证
batch_read_start = time.time()
try:
actual_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
actual_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
# FPS验证可选因为某些相机不支持精确FPS设置
actual_fps = self.cap.get(cv2.CAP_PROP_FPS) if actual_sets > 2 else self.fps
batch_read_time = (time.time() - batch_read_start) * 1000
self.logger.debug(f"验证读取耗时: {batch_read_time:.1f}ms")
except Exception as e:
# 验证失败不影响主流程
actual_width, actual_height, actual_fps = self.width, self.height, self.fps
batch_read_time = (time.time() - batch_read_start) * 1000
self.logger.debug(f"属性验证失败,使用默认值: {e}")
else:
# 跳过验证,使用设置值
actual_width, actual_height, actual_fps = self.width, self.height, self.fps
batch_read_time = 0.0
self.logger.debug("跳过属性验证以节省时间")
verification_time = (time.time() - verification_start) * 1000
# 为兼容性保留单独计时变量
width_read_time = batch_read_time * 0.4
height_read_time = batch_read_time * 0.4
fps_read_time = batch_read_time * 0.2
self.logger.debug(f"延迟验证耗时: {batch_read_time:.1f}ms, 总计: {verification_time:.1f}ms")
total_config_time = (time.time() - config_start) * 1000
self.logger.info(f"相机配置完成 - 分辨率: {actual_width}x{actual_height}, FPS: {actual_fps}")
self.logger.info(f"配置耗时统计 - 缓冲区: {buffer_time:.1f}ms, 优化设置: {optimization_time:.1f}ms, 分辨率: {resolution_time:.1f}ms, 帧率: {fps_time:.1f}ms, 验证: {verification_time:.1f}ms, 总计: {total_config_time:.1f}ms")
self.logger.debug(f"配置详情 - 分辨率设置: {resolution_time:.1f}ms, FPS设置: {fps_time:.1f}ms, 验证: {verification_time:.1f}ms, 总计: {total_config_time:.1f}ms")
except Exception as e:
self.logger.warning(f"配置相机参数失败: {e}")
def _test_camera(self) -> bool:
"""
测试相机是否正常工作
Returns:
bool: 测试是否成功
"""
try:
# 快速测试:只读取一帧进行验证
read_start = time.time()
ret, frame = self.cap.read()
read_time = (time.time() - read_start) * 1000
if ret and frame is not None:
# 基本帧验证
if len(frame.shape) >= 2 and frame.shape[0] > 0 and frame.shape[1] > 0:
self.logger.info(f"相机测试成功 - 帧大小: {frame.shape}, 读取耗时: {read_time:.1f}ms")
# 清理测试帧内存
del frame
return True
else:
self.logger.error(f"相机测试失败 - 帧数据无效: {frame.shape if frame is not None else 'None'}")
return False
else:
self.logger.error(f"相机测试失败 - 无法读取帧, 读取耗时: {read_time:.1f}ms")
return False
except Exception as e:
self.logger.error(f"相机测试异常: {e}")
return False
def calibrate(self) -> bool:
"""
校准相机对于普通相机主要是验证连接和设置
Returns:
bool: 校准是否成功
"""
calibrate_start = time.time()
try:
self.logger.info("开始相机校准...")
if not self.is_connected:
init_start = time.time()
if not self.initialize():
return False
init_time = (time.time() - init_start) * 1000
self.logger.info(f"校准中初始化完成 (耗时: {init_time:.1f}ms)")
# 优化减少稳定帧数量只读取2帧来稳定相机
stabilize_start = time.time()
stable_frames = 2 # 减少从5帧到2帧
for i in range(stable_frames):
frame_start = time.time()
ret, frame = self.cap.read()
frame_time = (time.time() - frame_start) * 1000
if not ret:
self.logger.warning(f"校准时读取第{i+1}帧失败 (耗时: {frame_time:.1f}ms)")
else:
# 立即释放帧内存
if frame is not None:
del frame
self.logger.debug(f"校准帧{i+1}读取成功 (耗时: {frame_time:.1f}ms)")
stabilize_time = (time.time() - stabilize_start) * 1000
total_time = (time.time() - calibrate_start) * 1000
self.logger.info(f"相机校准完成 - 稳定化耗时: {stabilize_time:.1f}ms, 总耗时: {total_time:.1f}ms")
return True
except Exception as e:
total_time = (time.time() - calibrate_start) * 1000
self.logger.error(f"相机校准失败: {e} (耗时: {total_time:.1f}ms)")
return False
def start_streaming(self) -> bool:
"""
开始数据流推送
Returns:
bool: 启动是否成功
"""
if self.is_streaming:
self.logger.warning("相机流已在运行")
return True
if not self.is_connected:
if not self.initialize():
return False
try:
self.is_streaming = True
self.streaming_thread = threading.Thread(
target=self._streaming_worker,
name=f"Camera-{self.device_index}-Stream",
daemon=True
)
self.streaming_thread.start()
self.logger.info("相机流启动成功")
return True
except Exception as e:
self.logger.error(f"启动相机流失败: {e}")
self.is_streaming = False
return False
def stop_streaming(self) -> bool:
"""
停止数据流推送
Returns:
bool: 停止是否成功
"""
try:
self.is_streaming = False
if self.streaming_thread and self.streaming_thread.is_alive():
2025-08-18 18:30:49 +08:00
# 等待线程退出
self.streaming_thread.join(timeout=3.0)
2025-08-18 18:30:49 +08:00
self.streaming_thread = None
self.logger.info("相机流已停止")
return True
except Exception as e:
self.logger.error(f"停止相机流失败: {e}")
return False
def _streaming_worker(self):
"""
流处理工作线程
"""
self.logger.info("相机流工作线程启动")
reconnect_attempts = 0
consecutive_read_failures = 0
2025-08-18 18:30:49 +08:00
# 基于目标FPS的简单节拍器防止无上限地读取/编码/发送导致对象堆积
frame_interval = 1.0 / max(self.fps, 1)
next_tick = time.time()
while self.is_streaming:
2025-08-18 18:30:49 +08:00
loop_start = time.time()
try:
# 如果设备未打开,进入重连流程
if not self.cap or not self.cap.isOpened():
# 仅在状态变化时广播一次断连状态
if self._last_connected_state is not False:
try:
self._socketio.emit('camera_status', {
'status': 'disconnected',
'device_id': self.device_id,
'timestamp': time.time()
}, namespace='/devices')
except Exception:
pass
self._last_connected_state = False
# 无限重连max_reconnect_attempts == -1否则按次数重试
if self.max_reconnect_attempts == -1 or reconnect_attempts < self.max_reconnect_attempts:
self.logger.warning(f"相机连接丢失,尝试重连 ({'' if self.max_reconnect_attempts == -1 else reconnect_attempts + 1}/{self.max_reconnect_attempts if self.max_reconnect_attempts != -1 else ''})")
if not self.is_streaming:
break
if self._reconnect():
reconnect_attempts = 0
consecutive_read_failures = 0
# 广播恢复
try:
self._socketio.emit('camera_status', {
'status': 'connected',
'device_id': self.device_id,
'timestamp': time.time()
}, namespace='/devices')
except Exception:
pass
self._last_connected_state = True
continue
else:
reconnect_attempts += 1
time.sleep(self.reconnect_delay)
continue
else:
# 超过次数也不退出线程,降频重试,防止永久停机
self.logger.error("相机重连失败次数过多,进入降频重试模式")
time.sleep(max(self.reconnect_delay, 5.0))
# 重置计数以便继续尝试
reconnect_attempts = 0
continue
ret, frame = self.cap.read()
if not ret or frame is None:
consecutive_read_failures += 1
self.dropped_frames += 1
if consecutive_read_failures >= getattr(self, 'read_fail_threshold', 30):
self.logger.warning(f"连续读帧失败 {consecutive_read_failures} 次,执行相机软复位并进入重连")
try:
if self.cap:
try:
self.cap.release()
except Exception:
pass
self.cap = None
self.is_connected = False
except Exception:
pass
# 进入下一轮循环会走到未打开分支
consecutive_read_failures = 0
time.sleep(self.reconnect_delay)
continue
if self.dropped_frames > 10:
self.logger.warning(f"连续丢帧过多: {self.dropped_frames}")
2025-08-18 18:30:49 +08:00
# 仅在异常情况下触发一次GC避免高频强制GC
try:
gc.collect()
except Exception:
pass
self.dropped_frames = 0
2025-08-18 18:30:49 +08:00
# 防止空转占满CPU
time.sleep(0.02)
continue
# 读帧成功,重置失败计数
consecutive_read_failures = 0
self.dropped_frames = 0
# 更新心跳时间,防止连接监控线程判定为超时
self.update_heartbeat()
2025-08-21 12:04:14 +08:00
# 保存原始帧到队列(用于录制)
try:
self.frame_queue.put_nowait({
'frame': frame.copy(),
'timestamp': time.time()
})
except queue.Full:
# 队列满时丢弃最旧的帧,添加新帧
try:
self.frame_queue.get_nowait() # 移除最旧的帧
self.frame_queue.put_nowait({
'frame': frame.copy(),
'timestamp': time.time()
})
except queue.Empty:
pass # 队列为空,忽略
2025-08-20 10:30:51 +08:00
2025-08-18 18:30:49 +08:00
# 处理帧(降采样以优化传输负载)
processed_frame = self._process_frame(frame)
2025-08-18 18:30:49 +08:00
# 发送帧数据
self._send_frame_data(processed_frame)
# 更新统计
self._update_statistics()
2025-08-18 18:30:49 +08:00
# 主动释放局部引用帮助GC更快识别可回收对象
del frame
# 限速保证不超过目标FPS减小发送端积压
now = time.time()
# 下一个tick基于固定间隔前移避免误差累积
next_tick += frame_interval
sleep_time = next_tick - now
if sleep_time > 0:
time.sleep(sleep_time)
else:
# 如果处理耗时超过间隔,纠正节拍器,避免持续为负
next_tick = now
except Exception as e:
self.logger.error(f"相机流处理异常: {e}")
2025-08-18 18:30:49 +08:00
# 小退避,避免异常情况下空转
time.sleep(0.05)
self.logger.info("相机流工作线程结束")
def _process_frame(self, frame: np.ndarray) -> np.ndarray:
"""
处理视频帧
Args:
frame: 原始帧
Returns:
np.ndarray: 处理后的帧
"""
try:
2025-08-18 18:30:49 +08:00
# 调整大小以优化传输(使用 INTER_AREA 质量好且更省内存/CPU
h, w = frame.shape[:2]
if w > self._tx_max_width:
scale = self._tx_max_width / float(w)
new_w = self._tx_max_width
new_h = int(h * scale)
frame = cv2.resize(frame, (new_w, new_h), interpolation=cv2.INTER_AREA)
return frame
except Exception as e:
self.logger.error(f"处理帧失败: {e}")
return frame
def _send_frame_data(self, frame: np.ndarray):
"""
发送帧数据
Args:
frame: 视频帧
"""
2025-08-18 18:30:49 +08:00
# 将临时对象局部化,并在 finally 中删除引用,加速回收
buffer = None
frame_bytes = None
frame_data = None
try:
# 编码为JPEG
encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 80]
2025-08-18 18:30:49 +08:00
ok, buffer = cv2.imencode('.jpg', frame, encode_param)
if not ok or buffer is None:
self.logger.warning("帧JPEG编码失败")
return
2025-08-18 18:30:49 +08:00
# 转换为bytes再做base64减少中间numpy对象的长生命周期
frame_bytes = buffer.tobytes()
frame_data = base64.b64encode(frame_bytes).decode('utf-8')
# 发送数据
data = {
'timestamp': time.time(),
'frame_count': self.frame_count,
'image': frame_data,
'fps': self.actual_fps,
'device_id': self.device_id
}
2025-08-17 16:42:05 +08:00
self._socketio.emit('camera_frame', data, namespace='/devices')
except Exception as e:
self.logger.error(f"发送帧数据失败: {e}")
2025-08-18 18:30:49 +08:00
finally:
# 显式删除临时大对象的引用,避免在高吞吐下堆积
del buffer
del frame_bytes
del frame_data
def _update_statistics(self):
"""
更新性能统计
"""
self.frame_count += 1
self.fps_counter += 1
# 每秒计算一次实际FPS
current_time = time.time()
if current_time - self.fps_start_time >= 1.0:
self.actual_fps = self.fps_counter / (current_time - self.fps_start_time)
self.fps_counter = 0
self.fps_start_time = current_time
# 更新性能统计
self.performance_stats.update({
'frames_processed': self.frame_count,
'actual_fps': round(self.actual_fps, 2),
'dropped_frames': self.dropped_frames
})
def _reconnect(self) -> bool:
"""
重新连接相机
Returns:
bool: 重连是否成功
"""
try:
if self.cap:
2025-08-18 18:30:49 +08:00
try:
self.cap.release()
except Exception:
pass
self.cap = None
time.sleep(1.0) # 等待设备释放
return self.initialize()
except Exception as e:
self.logger.error(f"相机重连失败: {e}")
return False
def get_status(self) -> Dict[str, Any]:
"""
获取设备状态
Returns:
Dict[str, Any]: 设备状态信息
"""
status = super().get_status()
status.update({
'device_index': self.device_index,
'resolution': f"{self.width}x{self.height}",
'target_fps': self.fps,
'actual_fps': self.actual_fps,
'frame_count': self.frame_count,
'dropped_frames': self.dropped_frames,
'has_frame': self.last_frame is not None
})
return status
def capture_image(self, save_path: Optional[str] = None) -> Optional[np.ndarray]:
"""
捕获单张图像
Args:
save_path: 保存路径可选
Returns:
Optional[np.ndarray]: 捕获的图像失败返回None
"""
try:
if not self.is_connected or not self.cap:
self.logger.error("相机未连接")
return None
ret, frame = self.cap.read()
if not ret or frame is None:
self.logger.error("捕获图像失败")
return None
if save_path:
cv2.imwrite(save_path, frame)
self.logger.info(f"图像已保存到: {save_path}")
return frame
except Exception as e:
self.logger.error(f"捕获图像异常: {e}")
return None
2025-08-21 12:04:14 +08:00
def disconnect(self):
"""
断开相机连接
"""
try:
self.stop_streaming()
if self.cap:
2025-08-18 18:30:49 +08:00
try:
self.cap.release()
except Exception:
pass
self.cap = None
self.is_connected = False
self.logger.info("相机已断开连接")
except Exception as e:
self.logger.error(f"断开相机连接失败: {e}")
def reload_config(self) -> bool:
"""
重新加载设备配置
Returns:
bool: 重新加载是否成功
"""
try:
self.logger.info("正在重新加载相机配置...")
# 获取最新配置
config = self.config_manager.get_device_config('camera')
# 更新配置属性
self.device_index = config.get('device_index', 0)
self.width = config.get('width', 1280)
self.height = config.get('height', 720)
self.fps = config.get('fps', 30)
self.buffer_size = config.get('buffer_size', 1)
self.fourcc = config.get('fourcc', 'MJPG')
self._tx_max_width = int(config.get('tx_max_width', 640))
# 新增:动态更新重连/阈值配置
self.max_reconnect_attempts = int(config.get('max_reconnect_attempts', self.max_reconnect_attempts))
self.reconnect_delay = float(config.get('reconnect_delay', self.reconnect_delay))
self.read_fail_threshold = int(config.get('read_fail_threshold', self.read_fail_threshold))
# 更新帧缓存队列大小
frame_cache_len = int(config.get('frame_cache_len', 2))
if frame_cache_len != self.frame_cache.maxsize:
# 清空旧队列
while not self.frame_cache.empty():
try:
self.frame_cache.get_nowait()
except queue.Empty:
break
# 创建新队列
self.frame_cache = queue.Queue(maxsize=frame_cache_len)
# 更新设备信息
self.device_id = f"camera_{self.device_index}"
self.logger.info(f"相机配置重新加载成功 - 设备索引: {self.device_index}, 分辨率: {self.width}x{self.height}, FPS: {self.fps}")
return True
except Exception as e:
self.logger.error(f"重新加载相机配置失败: {e}")
return False
2025-09-10 09:13:21 +08:00
def check_hardware_connection(self) -> bool:
"""
检查相机硬件连接状态
Returns:
bool: 相机是否物理连接
"""
try:
if not self.cap:
# 如果相机实例不存在,尝试重新创建
return self._attempt_device_reconnection()
if not self.cap.isOpened():
# 相机未打开,尝试重连
return self._attempt_device_reconnection()
# 尝试读取一帧来验证连接
try:
ret, frame = self.cap.read()
if ret and frame is not None:
# 立即释放帧内存
del frame
return True
else:
# 读取失败,可能设备已断开
return self._attempt_device_reconnection()
except Exception:
# 读取异常,尝试重连
return self._attempt_device_reconnection()
2025-09-10 09:13:21 +08:00
except Exception as e:
self.logger.debug(f"检查相机硬件连接时发生异常: {e}")
return False
def _attempt_device_reconnection(self) -> bool:
"""
尝试重新连接相机设备
Returns:
bool: 重连是否成功
"""
try:
self.logger.info("检测到相机设备断开,尝试重新连接...")
# 清理旧的相机实例
if self.cap:
try:
self.cap.release()
except Exception as e:
self.logger.debug(f"清理旧相机实例时出错: {e}")
self.cap = None
# 等待设备释放
time.sleep(0.5)
# 重新初始化相机
if self.initialize():
self._notify_status_change(True)
# 重连成功后,确保数据流正在运行
if not self.is_streaming:
self.logger.info("重连成功,启动相机数据流")
self.start_streaming()
# 更新设备信息
self._device_info.update({
'device_index': self.device_index,
'resolution': f"{self.width}x{self.height}",
'fps': self.fps,
'backend': self.cap.getBackendName() if hasattr(self.cap, 'getBackendName') else 'Unknown'
})
return True
else:
self.logger.warning("相机设备重连失败")
return False
except Exception as e:
self.logger.error(f"相机设备重连过程中出错: {e}")
self.cap = None
return False
def cleanup(self):
"""
清理资源
"""
try:
2025-09-10 09:13:21 +08:00
self.logger.info("开始清理相机资源")
# 清理监控线程
self._cleanup_monitoring()
# 停止流
if self.is_streaming:
self.stop_streaming()
# 断开连接
self.disconnect()
2025-08-21 12:04:14 +08:00
# 清理帧缓存
while not self.frame_cache.empty():
try:
self.frame_cache.get_nowait()
except queue.Empty:
break
2025-09-10 09:13:21 +08:00
# 清理全局帧队列
2025-08-21 12:04:14 +08:00
while not self.frame_queue.empty():
try:
self.frame_queue.get_nowait()
except queue.Empty:
break
2025-08-20 10:30:51 +08:00
2025-09-10 09:13:21 +08:00
self.last_frame = None
super().cleanup()
self.logger.info("相机资源清理完成")
except Exception as e:
2025-09-10 09:13:21 +08:00
self.logger.error(f"清理相机资源时发生错误: {e}")
2025-08-20 10:30:51 +08:00
def _save_frame_to_cache(self, frame, frame_type='camera'):
"""保存帧到全局缓存"""
try:
with self.frame_cache_lock:
current_time = time.time()
# 清理过期帧
self._cleanup_expired_frames()
# 如果缓存已满,移除最旧的帧
if frame_type in self.global_frame_cache and len(self.global_frame_cache[frame_type]) >= self.max_cache_size:
oldest_key = min(self.global_frame_cache[frame_type].keys())
del self.global_frame_cache[frame_type][oldest_key]
# 初始化帧类型缓存
if frame_type not in self.global_frame_cache:
self.global_frame_cache[frame_type] = {}
# 保存帧(深拷贝避免引用问题)
frame_data = {
'frame': frame.copy(),
'timestamp': current_time,
'frame_id': len(self.global_frame_cache[frame_type])
}
self.global_frame_cache[frame_type][current_time] = frame_data
except Exception as e:
self.logger.error(f'保存帧到缓存失败: {e}')
def _get_latest_frame_from_cache(self, frame_type='camera'):
2025-08-21 12:04:14 +08:00
"""从队列获取最新帧"""
2025-08-20 10:30:51 +08:00
try:
2025-08-21 12:04:14 +08:00
if self.frame_queue.empty():
self.logger.debug('帧队列为空')
return None, None
2025-08-20 10:30:51 +08:00
2025-08-21 12:04:14 +08:00
# 获取队列中的所有帧,保留最新的一个
frames = []
while not self.frame_queue.empty():
try:
frames.append(self.frame_queue.get_nowait())
except queue.Empty:
break
if not frames:
return None, None
# 获取最新帧(最后一个)
latest_frame = frames[-1]
# 将最新帧重新放回队列
try:
self.frame_queue.put_nowait(latest_frame)
except queue.Full:
pass # 队列满时忽略
return latest_frame['frame'].copy(), latest_frame['timestamp']
2025-08-20 10:30:51 +08:00
except Exception as e:
2025-08-21 12:04:14 +08:00
self.logger.error(f'从队列获取帧失败: {e}')
return None, None