511 lines
16 KiB
Python
511 lines
16 KiB
Python
#!/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, Tuple
|
||
import logging
|
||
from collections import deque
|
||
import gc
|
||
|
||
try:
|
||
from .base_device import BaseDevice
|
||
from .utils.socket_manager import SocketManager
|
||
from .utils.config_manager import ConfigManager
|
||
except ImportError:
|
||
from base_device import BaseDevice
|
||
from utils.socket_manager import SocketManager
|
||
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')
|
||
|
||
# 流控制
|
||
self.streaming_thread = None
|
||
self.frame_cache = deque(maxlen=10)
|
||
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
|
||
|
||
# 重连机制
|
||
self.max_reconnect_attempts = 3
|
||
self.reconnect_delay = 2.0
|
||
|
||
# 设备标识和性能统计
|
||
self.device_id = f"camera_{self.device_index}"
|
||
self.performance_stats = {
|
||
'frames_processed': 0,
|
||
'actual_fps': 0,
|
||
'dropped_frames': 0
|
||
}
|
||
|
||
self.logger.info(f"相机管理器初始化完成 - 设备索引: {self.device_index}")
|
||
|
||
def initialize(self) -> bool:
|
||
"""
|
||
初始化相机设备
|
||
|
||
Returns:
|
||
bool: 初始化是否成功
|
||
"""
|
||
try:
|
||
self.logger.info(f"正在初始化相机设备 {self.device_index}...")
|
||
|
||
# 尝试多个后端
|
||
backends = [cv2.CAP_MSMF, cv2.CAP_DSHOW, cv2.CAP_ANY]
|
||
|
||
for backend in backends:
|
||
try:
|
||
self.cap = cv2.VideoCapture(self.device_index, backend)
|
||
if self.cap.isOpened():
|
||
self.logger.info(f"使用后端 {backend} 成功打开相机")
|
||
break
|
||
except Exception as e:
|
||
self.logger.warning(f"后端 {backend} 打开相机失败: {e}")
|
||
continue
|
||
else:
|
||
raise Exception("所有后端都无法打开相机")
|
||
|
||
# 设置相机属性
|
||
self._configure_camera()
|
||
|
||
# 验证相机是否正常工作
|
||
if not self._test_camera():
|
||
raise Exception("相机测试失败")
|
||
|
||
self.is_connected = 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'
|
||
})
|
||
|
||
self.logger.info("相机初始化成功")
|
||
return True
|
||
|
||
except Exception as e:
|
||
self.logger.error(f"相机初始化失败: {e}")
|
||
self.is_connected = False
|
||
if self.cap:
|
||
self.cap.release()
|
||
self.cap = None
|
||
return False
|
||
|
||
def _configure_camera(self):
|
||
"""
|
||
配置相机参数
|
||
"""
|
||
if not self.cap:
|
||
return
|
||
|
||
try:
|
||
# 设置FOURCC编码
|
||
if self.fourcc:
|
||
fourcc_code = cv2.VideoWriter_fourcc(*self.fourcc)
|
||
self.cap.set(cv2.CAP_PROP_FOURCC, fourcc_code)
|
||
|
||
# 设置分辨率
|
||
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, self.width)
|
||
self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, self.height)
|
||
|
||
# 设置帧率
|
||
self.cap.set(cv2.CAP_PROP_FPS, self.fps)
|
||
|
||
# 设置缓冲区大小
|
||
self.cap.set(cv2.CAP_PROP_BUFFERSIZE, self.buffer_size)
|
||
|
||
# 获取实际设置的值
|
||
actual_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
||
actual_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||
actual_fps = self.cap.get(cv2.CAP_PROP_FPS)
|
||
|
||
self.logger.info(f"相机配置 - 分辨率: {actual_width}x{actual_height}, FPS: {actual_fps}")
|
||
|
||
except Exception as e:
|
||
self.logger.warning(f"配置相机参数失败: {e}")
|
||
|
||
def _test_camera(self) -> bool:
|
||
"""
|
||
测试相机是否正常工作
|
||
|
||
Returns:
|
||
bool: 测试是否成功
|
||
"""
|
||
try:
|
||
ret, frame = self.cap.read()
|
||
if ret and frame is not None:
|
||
self.logger.info(f"相机测试成功 - 帧大小: {frame.shape}")
|
||
return True
|
||
else:
|
||
self.logger.error("相机测试失败 - 无法读取帧")
|
||
return False
|
||
except Exception as e:
|
||
self.logger.error(f"相机测试异常: {e}")
|
||
return False
|
||
|
||
def calibrate(self) -> bool:
|
||
"""
|
||
校准相机(对于普通相机,主要是验证连接和设置)
|
||
|
||
Returns:
|
||
bool: 校准是否成功
|
||
"""
|
||
try:
|
||
self.logger.info("开始相机校准...")
|
||
|
||
if not self.is_connected:
|
||
if not self.initialize():
|
||
return False
|
||
|
||
# 读取几帧来稳定相机
|
||
for i in range(5):
|
||
ret, frame = self.cap.read()
|
||
if not ret:
|
||
self.logger.warning(f"校准时读取第{i+1}帧失败")
|
||
|
||
self.logger.info("相机校准完成")
|
||
return True
|
||
|
||
except Exception as e:
|
||
self.logger.error(f"相机校准失败: {e}")
|
||
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():
|
||
self.streaming_thread.join(timeout=3.0)
|
||
|
||
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
|
||
|
||
while self.is_streaming:
|
||
try:
|
||
if not self.cap or not self.cap.isOpened():
|
||
if reconnect_attempts < self.max_reconnect_attempts:
|
||
self.logger.warning(f"相机连接丢失,尝试重连 ({reconnect_attempts + 1}/{self.max_reconnect_attempts})")
|
||
if self._reconnect():
|
||
reconnect_attempts = 0
|
||
continue
|
||
else:
|
||
reconnect_attempts += 1
|
||
time.sleep(self.reconnect_delay)
|
||
continue
|
||
else:
|
||
self.logger.error("相机重连失败次数过多,停止流")
|
||
break
|
||
|
||
ret, frame = self.cap.read()
|
||
|
||
if not ret or frame is None:
|
||
self.dropped_frames += 1
|
||
if self.dropped_frames > 10:
|
||
self.logger.warning(f"连续丢帧过多: {self.dropped_frames}")
|
||
self.dropped_frames = 0
|
||
time.sleep(0.01)
|
||
continue
|
||
|
||
# 重置丢帧计数
|
||
self.dropped_frames = 0
|
||
|
||
# 处理帧
|
||
processed_frame = self._process_frame(frame)
|
||
|
||
# 缓存帧
|
||
self.last_frame = processed_frame.copy()
|
||
self.frame_cache.append(processed_frame)
|
||
|
||
# 发送帧数据
|
||
self._send_frame_data(processed_frame)
|
||
|
||
# 更新统计
|
||
self._update_statistics()
|
||
|
||
# 内存管理
|
||
if self.frame_count % 30 == 0:
|
||
gc.collect()
|
||
|
||
except Exception as e:
|
||
self.logger.error(f"相机流处理异常: {e}")
|
||
time.sleep(0.1)
|
||
|
||
self.logger.info("相机流工作线程结束")
|
||
|
||
def _process_frame(self, frame: np.ndarray) -> np.ndarray:
|
||
"""
|
||
处理视频帧
|
||
|
||
Args:
|
||
frame: 原始帧
|
||
|
||
Returns:
|
||
np.ndarray: 处理后的帧
|
||
"""
|
||
try:
|
||
# 调整大小以优化传输
|
||
if frame.shape[1] > 640:
|
||
scale_factor = 640 / frame.shape[1]
|
||
new_width = 640
|
||
new_height = int(frame.shape[0] * scale_factor)
|
||
frame = cv2.resize(frame, (new_width, new_height))
|
||
|
||
return frame
|
||
|
||
except Exception as e:
|
||
self.logger.error(f"处理帧失败: {e}")
|
||
return frame
|
||
|
||
def _send_frame_data(self, frame: np.ndarray):
|
||
"""
|
||
发送帧数据
|
||
|
||
Args:
|
||
frame: 视频帧
|
||
"""
|
||
try:
|
||
# 编码为JPEG
|
||
encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 80]
|
||
_, buffer = cv2.imencode('.jpg', frame, encode_param)
|
||
|
||
# 转换为base64
|
||
frame_data = base64.b64encode(buffer).decode('utf-8')
|
||
|
||
# 发送数据
|
||
data = {
|
||
'timestamp': time.time(),
|
||
'frame_count': self.frame_count,
|
||
'image': frame_data,
|
||
'fps': self.actual_fps,
|
||
'device_id': self.device_id
|
||
}
|
||
|
||
self._socketio.emit('camera_frame', data, namespace='/devices')
|
||
|
||
except Exception as e:
|
||
self.logger.error(f"发送帧数据失败: {e}")
|
||
|
||
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:
|
||
self.cap.release()
|
||
|
||
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
|
||
|
||
def get_latest_frame(self) -> Optional[np.ndarray]:
|
||
"""
|
||
获取最新帧
|
||
|
||
Returns:
|
||
Optional[np.ndarray]: 最新帧,无帧返回None
|
||
"""
|
||
return self.last_frame.copy() if self.last_frame is not None else None
|
||
|
||
def disconnect(self):
|
||
"""
|
||
断开相机连接
|
||
"""
|
||
try:
|
||
self.stop_streaming()
|
||
|
||
if self.cap:
|
||
self.cap.release()
|
||
self.cap = None
|
||
|
||
self.is_connected = False
|
||
self.logger.info("相机已断开连接")
|
||
|
||
except Exception as e:
|
||
self.logger.error(f"断开相机连接失败: {e}")
|
||
|
||
def cleanup(self):
|
||
"""
|
||
清理资源
|
||
"""
|
||
try:
|
||
self.stop_streaming()
|
||
|
||
if self.cap:
|
||
self.cap.release()
|
||
self.cap = None
|
||
|
||
self.frame_cache.clear()
|
||
self.last_frame = None
|
||
|
||
super().cleanup()
|
||
self.logger.info("相机资源清理完成")
|
||
|
||
except Exception as e:
|
||
self.logger.error(f"清理相机资源失败: {e}") |