BodyBalanceEvaluation/backend/devices/camera_manager.py
2025-08-17 16:42:05 +08:00

511 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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}")