#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 设备管理模块 负责摄像头、IMU传感器和压力传感器的连接和数据采集 以及视频推流功能 """ import cv2 import numpy as np import time import threading import json import queue import base64 import gc import os import psutil import configparser from datetime import datetime from typing import Dict, List, Optional, Any from concurrent.futures import ThreadPoolExecutor import logging logger = logging.getLogger(__name__) class DeviceManager: """设备管理器""" def __init__(self): self.camera = None self.imu_device = None self.pressure_device = None self.device_status = { 'camera': False, 'imu': False, 'pressure': False } self.calibration_data = {} self.data_lock = threading.Lock() self.latest_data = {} # 初始化设备 self._init_devices() def _init_devices(self): """初始化所有设备""" try: self._init_camera() self._init_imu() self._init_pressure_sensor() logger.info('设备初始化完成') except Exception as e: logger.error(f'设备初始化失败: {e}') def _init_camera(self): """初始化摄像头""" try: # 尝试连接默认摄像头 self.camera = cv2.VideoCapture(0) if self.camera.isOpened(): # 设置摄像头参数 self.camera.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) self.camera.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) self.camera.set(cv2.CAP_PROP_FPS, 30) self.device_status['camera'] = True logger.info('摄像头初始化成功') else: logger.warning('摄像头连接失败') self.camera = None except Exception as e: logger.error(f'摄像头初始化异常: {e}') self.camera = None def _init_imu(self): """初始化IMU传感器""" try: # 这里应该连接实际的IMU设备 # 目前使用模拟数据 self.imu_device = MockIMUDevice() self.device_status['imu'] = True logger.info('IMU传感器初始化成功(模拟)') except Exception as e: logger.error(f'IMU传感器初始化失败: {e}') self.imu_device = None def _init_pressure_sensor(self): """初始化压力传感器""" try: # 这里应该连接实际的压力传感器 # 目前使用模拟数据 self.pressure_device = MockPressureDevice() self.device_status['pressure'] = True logger.info('压力传感器初始化成功(模拟)') except Exception as e: logger.error(f'压力传感器初始化失败: {e}') self.pressure_device = None def get_device_status(self) -> Dict[str, bool]: """获取设备状态""" return self.device_status.copy() def get_connected_devices(self) -> List[str]: """获取已连接的设备列表""" return [device for device, status in self.device_status.items() if status] def refresh_devices(self): """刷新设备连接""" logger.info('刷新设备连接...') # 重新初始化所有设备 if self.camera: self.camera.release() self._init_devices() def calibrate_devices(self) -> Dict[str, Any]: """校准设备""" calibration_result = {} try: # 摄像头校准 if self.device_status['camera']: camera_calibration = self._calibrate_camera() calibration_result['camera'] = camera_calibration # IMU校准 if self.device_status['imu']: imu_calibration = self._calibrate_imu() calibration_result['imu'] = imu_calibration # 压力传感器校准 if self.device_status['pressure']: pressure_calibration = self._calibrate_pressure() calibration_result['pressure'] = pressure_calibration self.calibration_data = calibration_result logger.info('设备校准完成') except Exception as e: logger.error(f'设备校准失败: {e}') raise return calibration_result def _calibrate_camera(self) -> Dict[str, Any]: """校准摄像头""" if not self.camera or not self.camera.isOpened(): return {'status': 'failed', 'error': '摄像头未连接'} try: # 获取几帧图像进行校准 frames = [] for _ in range(10): ret, frame = self.camera.read() if ret: frames.append(frame) time.sleep(0.1) if not frames: return {'status': 'failed', 'error': '无法获取图像'} # 计算平均亮度和对比度 avg_brightness = np.mean([np.mean(cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)) for frame in frames]) calibration = { 'status': 'success', 'brightness': float(avg_brightness), 'resolution': (int(self.camera.get(cv2.CAP_PROP_FRAME_WIDTH)), int(self.camera.get(cv2.CAP_PROP_FRAME_HEIGHT))), 'fps': float(self.camera.get(cv2.CAP_PROP_FPS)), 'timestamp': datetime.now().isoformat() } return calibration except Exception as e: return {'status': 'failed', 'error': str(e)} def _calibrate_imu(self) -> Dict[str, Any]: """校准IMU传感器""" if not self.imu_device: return {'status': 'failed', 'error': 'IMU设备未连接'} try: # 收集静态数据进行零点校准 samples = [] for _ in range(100): data = self.imu_device.read_data() samples.append(data) time.sleep(0.01) # 计算零点偏移 accel_offset = { 'x': np.mean([s['accel']['x'] for s in samples]), 'y': np.mean([s['accel']['y'] for s in samples]), 'z': np.mean([s['accel']['z'] for s in samples]) - 9.8 # 重力补偿 } gyro_offset = { 'x': np.mean([s['gyro']['x'] for s in samples]), 'y': np.mean([s['gyro']['y'] for s in samples]), 'z': np.mean([s['gyro']['z'] for s in samples]) } calibration = { 'status': 'success', 'accel_offset': accel_offset, 'gyro_offset': gyro_offset, 'timestamp': datetime.now().isoformat() } return calibration except Exception as e: return {'status': 'failed', 'error': str(e)} def _calibrate_pressure(self) -> Dict[str, Any]: """校准压力传感器""" if not self.pressure_device: return {'status': 'failed', 'error': '压力传感器未连接'} try: # 收集零压力数据 samples = [] for _ in range(50): data = self.pressure_device.read_data() samples.append(data) time.sleep(0.02) # 计算零点偏移 zero_offset = { 'left_foot': np.mean([s['left_foot'] for s in samples]), 'right_foot': np.mean([s['right_foot'] for s in samples]) } calibration = { 'status': 'success', 'zero_offset': zero_offset, 'timestamp': datetime.now().isoformat() } return calibration except Exception as e: return {'status': 'failed', 'error': str(e)} def collect_data(self) -> Dict[str, Any]: """采集所有设备数据""" data = {} timestamp = datetime.now().isoformat() try: # 采集摄像头数据 if self.device_status['camera']: camera_data = self._collect_camera_data() if camera_data: data['camera'] = camera_data # 采集IMU数据 if self.device_status['imu']: imu_data = self._collect_imu_data() if imu_data: data['imu'] = imu_data # 采集压力传感器数据 if self.device_status['pressure']: pressure_data = self._collect_pressure_data() if pressure_data: data['pressure'] = pressure_data # 添加时间戳 data['timestamp'] = timestamp # 更新最新数据 with self.data_lock: self.latest_data = data.copy() except Exception as e: logger.error(f'数据采集失败: {e}') return data def _collect_camera_data(self) -> Optional[Dict[str, Any]]: """采集摄像头数据""" if not self.camera or not self.camera.isOpened(): return None try: ret, frame = self.camera.read() if not ret: return None # 进行姿态检测(这里应该集成实际的姿态检测算法) pose_data = self._detect_pose(frame) return { 'frame_available': True, 'frame_size': frame.shape, 'pose_data': pose_data, 'timestamp': datetime.now().isoformat() } except Exception as e: logger.error(f'摄像头数据采集失败: {e}') return None def _detect_pose(self, frame: np.ndarray) -> Dict[str, Any]: """姿态检测(模拟实现)""" # 这里应该集成实际的姿态检测算法,如MediaPipe或OpenPose # 目前返回模拟数据 return { 'center_of_gravity': { 'x': np.random.normal(0, 5), 'y': np.random.normal(0, 5) }, 'body_angle': { 'pitch': np.random.normal(0, 2), 'roll': np.random.normal(0, 2), 'yaw': np.random.normal(0, 1) }, 'confidence': np.random.uniform(0.8, 1.0) } def _collect_imu_data(self) -> Optional[Dict[str, Any]]: """采集IMU数据""" if not self.imu_device: return None try: raw_data = self.imu_device.read_data() # 应用校准偏移 if 'imu' in self.calibration_data: calibration = self.calibration_data['imu'] if calibration.get('status') == 'success': accel_offset = calibration.get('accel_offset', {}) gyro_offset = calibration.get('gyro_offset', {}) # 校正加速度数据 raw_data['accel']['x'] -= accel_offset.get('x', 0) raw_data['accel']['y'] -= accel_offset.get('y', 0) raw_data['accel']['z'] -= accel_offset.get('z', 0) # 校正陀螺仪数据 raw_data['gyro']['x'] -= gyro_offset.get('x', 0) raw_data['gyro']['y'] -= gyro_offset.get('y', 0) raw_data['gyro']['z'] -= gyro_offset.get('z', 0) return raw_data except Exception as e: logger.error(f'IMU数据采集失败: {e}') return None def _collect_pressure_data(self) -> Optional[Dict[str, Any]]: """采集压力传感器数据""" if not self.pressure_device: return None try: raw_data = self.pressure_device.read_data() # 应用校准偏移 if 'pressure' in self.calibration_data: calibration = self.calibration_data['pressure'] if calibration.get('status') == 'success': zero_offset = calibration.get('zero_offset', {}) raw_data['left_foot'] -= zero_offset.get('left_foot', 0) raw_data['right_foot'] -= zero_offset.get('right_foot', 0) # 计算重心位置 total_pressure = raw_data['left_foot'] + raw_data['right_foot'] if total_pressure > 0: center_of_pressure = { 'x': (raw_data['right_foot'] - raw_data['left_foot']) / total_pressure * 100, 'y': 0 # 简化模型 } else: center_of_pressure = {'x': 0, 'y': 0} raw_data['center_of_pressure'] = center_of_pressure raw_data['total_pressure'] = total_pressure return raw_data except Exception as e: logger.error(f'压力传感器数据采集失败: {e}') return None def get_latest_data(self) -> Dict[str, Any]: """获取最新采集的数据""" with self.data_lock: return self.latest_data.copy() def start_video_recording(self, output_path: str) -> bool: """开始视频录制""" if not self.camera or not self.camera.isOpened(): return False try: # 获取摄像头参数 width = int(self.camera.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int(self.camera.get(cv2.CAP_PROP_FRAME_HEIGHT)) fps = int(self.camera.get(cv2.CAP_PROP_FPS)) # 创建视频写入器 fourcc = cv2.VideoWriter_fourcc(*'mp4v') self.video_writer = cv2.VideoWriter(output_path, fourcc, fps, (width, height)) if self.video_writer.isOpened(): logger.info(f'开始视频录制: {output_path}') return True else: logger.error('视频写入器创建失败') return False except Exception as e: logger.error(f'开始视频录制失败: {e}') return False def stop_video_recording(self): """停止视频录制""" if hasattr(self, 'video_writer') and self.video_writer: self.video_writer.release() self.video_writer = None logger.info('视频录制已停止') def cleanup(self): """清理资源""" try: if self.camera: self.camera.release() if hasattr(self, 'video_writer') and self.video_writer: self.video_writer.release() logger.info('设备资源已清理') except Exception as e: logger.error(f'清理设备资源失败: {e}') class MockIMUDevice: """模拟IMU设备""" def __init__(self): self.noise_level = 0.1 def read_data(self) -> Dict[str, Any]: """读取IMU数据""" return { 'accel': { 'x': np.random.normal(0, self.noise_level), 'y': np.random.normal(0, self.noise_level), 'z': np.random.normal(9.8, self.noise_level) # 重力加速度 }, 'gyro': { 'x': np.random.normal(0, self.noise_level), 'y': np.random.normal(0, self.noise_level), 'z': np.random.normal(0, self.noise_level) }, 'temperature': np.random.normal(25, 2), 'timestamp': datetime.now().isoformat() } class MockPressureDevice: """模拟压力传感器设备""" def __init__(self): self.base_pressure = 500 # 基础压力值 self.noise_level = 10 def read_data(self) -> Dict[str, Any]: """读取压力数据""" # 模拟轻微的左右脚压力差异 left_pressure = self.base_pressure + np.random.normal(0, self.noise_level) right_pressure = self.base_pressure + np.random.normal(0, self.noise_level) return { 'left_foot': max(0, left_pressure), 'right_foot': max(0, right_pressure), 'timestamp': datetime.now().isoformat() } class VideoStreamManager: """视频推流管理器""" def __init__(self, socketio=None): self.socketio = socketio self.rtsp_url = None self.rtsp_thread = None self.rtsp_running = False # 用于异步编码的线程池和队列 self.encoding_executor = ThreadPoolExecutor(max_workers=2) self.frame_queue = queue.Queue(maxsize=1) # 只保留最新的一帧 # 内存优化配置 self.frame_skip_counter = 0 self.FRAME_SKIP_RATIO = 1 # 每3帧发送1帧,减少网络和内存压力 self.MAX_FRAME_SIZE = (640, 480) # 进一步减小帧尺寸以节省内存 self.MAX_MEMORY_USAGE = 200 * 1024 * 1024 # 200MB内存限制 self.memory_check_counter = 0 self.MEMORY_CHECK_INTERVAL = 50 # 每50帧检查一次内存 # 读取RTSP配置 self._load_rtsp_config() def _load_rtsp_config(self): """加载RTSP配置""" try: config = configparser.ConfigParser() config_path = os.path.join(os.path.dirname(__file__), 'config.ini') config.read(config_path, encoding='utf-8') self.rtsp_url = config.get('CAMERA', 'rtsp_url', fallback=None) logger.info(f'RTSP配置加载完成: {self.rtsp_url}') except Exception as e: logger.error(f'加载RTSP配置失败: {e}') self.rtsp_url = None def get_memory_usage(self): """获取当前进程内存使用量(字节)""" try: process = psutil.Process(os.getpid()) return process.memory_info().rss except: return 0 def async_encode_frame(self, frame, frame_count): """异步编码帧 - 内存优化版本""" try: # 内存检查 self.memory_check_counter += 1 if self.memory_check_counter >= self.MEMORY_CHECK_INTERVAL: self.memory_check_counter = 0 current_memory = self.get_memory_usage() if current_memory > self.MAX_MEMORY_USAGE: logger.warning(f"内存使用过高: {current_memory / 1024 / 1024:.2f}MB,强制清理") gc.collect() # 如果内存仍然过高,跳过此帧 if self.get_memory_usage() > self.MAX_MEMORY_USAGE: del frame return # 更激进的图像尺寸压缩以节省内存 height, width = frame.shape[:2] target_width, target_height = self.MAX_FRAME_SIZE if width > target_width or height > target_height: # 计算缩放比例,保持宽高比 scale_w = target_width / width scale_h = target_height / height scale = min(scale_w, scale_h) new_width = int(width * scale) new_height = int(height * scale) # 使用更快的插值方法减少CPU使用 frame = cv2.resize(frame, (new_width, new_height), interpolation=cv2.INTER_AREA) # 优化JPEG编码参数:优先考虑速度和内存 encode_param = [ int(cv2.IMWRITE_JPEG_QUALITY), 50, # 进一步降低质量以减少内存使用 int(cv2.IMWRITE_JPEG_OPTIMIZE), 1, # 启用优化 int(cv2.IMWRITE_JPEG_PROGRESSIVE), 0 # 禁用渐进式以减少内存 ] success, buffer = cv2.imencode('.jpg', frame, encode_param) if not success: logger.error('图像编码失败') return # 立即释放frame内存 del frame jpg_as_text = base64.b64encode(buffer).decode('utf-8') # 立即释放buffer内存 del buffer # 发送数据 if self.socketio: self.socketio.emit('rtsp_frame', { 'image': jpg_as_text, 'frame_id': frame_count, 'timestamp': time.time() }) # 立即释放base64字符串 del jpg_as_text except Exception as e: logger.error(f'异步编码帧失败: {e}') finally: # 定期强制垃圾回收 if self.memory_check_counter % 10 == 0: gc.collect() def frame_encoding_worker(self): """帧编码工作线程""" while self.rtsp_running: try: # 从队列获取帧 frame, frame_count = self.frame_queue.get(timeout=1) # 提交到线程池进行异步编码 self.encoding_executor.submit(self.async_encode_frame, frame, frame_count) except queue.Empty: continue except Exception as e: logger.error(f'帧编码工作线程异常: {e}') def generate_test_frame(self, frame_count): """生成测试帧""" width, height = 640, 480 # 创建黑色背景 frame = np.zeros((height, width, 3), dtype=np.uint8) # 添加动态元素 timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3] # 添加时间戳 cv2.putText(frame, timestamp, (10, 90), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 2) # 添加帧计数 cv2.putText(frame, f'TEST Frame: {frame_count}', (10, 120), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 2) # 添加移动的圆形 center_x = int(320 + 200 * np.sin(frame_count * 0.1)) center_y = int(240 + 100 * np.cos(frame_count * 0.1)) cv2.circle(frame, (center_x, center_y), 30, (255, 0, 0), -1) # 添加变化的矩形 rect_size = int(50 + 30 * np.sin(frame_count * 0.05)) cv2.rectangle(frame, (500, 200), (500 + rect_size, 200 + rect_size), (0, 0, 255), -1) return frame def generate_rtsp_frames(self): """生成RTSP帧""" frame_count = 0 error_count = 0 use_test_mode = False last_frame_time = time.time() logger.info(f'开始生成RTSP帧,URL: {self.rtsp_url}') try: cap = cv2.VideoCapture(self.rtsp_url) if not cap.isOpened(): logger.warning(f'无法打开RTSP流: {self.rtsp_url},切换到测试模式') use_test_mode = True if self.socketio: self.socketio.emit('rtsp_status', {'status': 'started', 'message': '使用测试视频源'}) else: # 最激进的实时优化设置 cap.set(cv2.CAP_PROP_BUFFERSIZE, 0) # 完全禁用缓冲区 cap.set(cv2.CAP_PROP_FPS, 60) # 提高帧率到60fps cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('M', 'J', 'P', 'G')) # MJPEG编码 # 设置更低的分辨率以减少处理时间 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) logger.info('RTSP流已打开,开始推送帧(激进实时模式)') if self.socketio: self.socketio.emit('rtsp_status', {'status': 'started', 'message': '使用RTSP视频源(激进实时模式)'}) self.rtsp_running = True # 启动帧编码工作线程 encoding_thread = threading.Thread(target=self.frame_encoding_worker) encoding_thread.daemon = True encoding_thread.start() while self.rtsp_running: if use_test_mode: # 使用测试模式生成帧 frame = self.generate_test_frame(frame_count) ret = True else: # 使用RTSP流,添加帧跳过机制减少延迟 ret, frame = cap.read() if not ret: error_count += 1 logger.warning(f'RTSP读取帧失败(第{error_count}次),尝试重连...') if 'cap' in locals(): cap.release() if error_count > 5: logger.warning('RTSP连接失败次数过多,切换到测试模式') use_test_mode = True if self.socketio: self.socketio.emit('rtsp_status', {'status': 'switched', 'message': '已切换到测试视频源'}) continue # 立即重连,不等待 cap = cv2.VideoCapture(self.rtsp_url) if cap.isOpened(): # 重连时应用相同的激进实时设置 cap.set(cv2.CAP_PROP_BUFFERSIZE, 0) cap.set(cv2.CAP_PROP_FPS, 60) cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('M', 'J', 'P', 'G')) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) continue error_count = 0 # 重置错误计数 # 内存优化的帧跳过策略 # 减少跳帧数量,避免过度内存使用 skip_count = 0 while skip_count < 3: # 减少到最多跳过3帧 temp_ret, temp_frame = cap.read() if temp_ret: # 立即释放之前的帧 if 'frame' in locals(): del frame frame = temp_frame skip_count += 1 else: break # 降低帧率以减少内存压力 current_time = time.time() if current_time - last_frame_time < 1/20: # 降低到20fps最大频率 continue last_frame_time = current_time frame_count += 1 # 实现帧跳过以减少内存和网络压力 self.frame_skip_counter += 1 if self.frame_skip_counter % (self.FRAME_SKIP_RATIO + 1) != 0: # 跳过此帧,立即释放内存 del frame continue try: # 将帧放入队列进行异步处理 try: # 非阻塞方式放入队列,如果队列满了就丢弃旧帧 self.frame_queue.put_nowait((frame.copy(), frame_count)) except queue.Full: # 队列满了,清空队列并放入新帧 try: old_frame, _ = self.frame_queue.get_nowait() del old_frame # 立即释放旧帧内存 except queue.Empty: pass self.frame_queue.put_nowait((frame.copy(), frame_count)) # 立即释放原始帧内存 del frame if frame_count % 60 == 0: # 每60帧记录一次 logger.info(f'已推送 {frame_count} 帧到编码队列,跳过率: {self.FRAME_SKIP_RATIO}/{self.FRAME_SKIP_RATIO+1}') # 定期强制垃圾回收 gc.collect() except Exception as e: logger.error(f'帧队列处理失败: {e}') except Exception as e: logger.error(f'RTSP推流异常: {e}') if self.socketio: self.socketio.emit('rtsp_status', {'status': 'error', 'message': f'推流异常: {str(e)}'}) finally: if 'cap' in locals(): cap.release() self.rtsp_running = False logger.info(f'RTSP推流结束,总共推送了 {frame_count} 帧') def start_rtsp_stream(self): """启动RTSP推流""" try: if self.rtsp_thread and self.rtsp_thread.is_alive(): logger.warning('RTSP线程已在运行') return {'status': 'already_running', 'message': 'RTSP已在运行'} if not self.rtsp_url: logger.error('RTSP URL未配置') return {'status': 'error', 'message': 'RTSP URL未配置'} logger.info(f'启动RTSP线程,URL: {self.rtsp_url}') self.rtsp_thread = threading.Thread(target=self.generate_rtsp_frames) self.rtsp_thread.daemon = True self.rtsp_thread.start() logger.info('RTSP线程已启动') return {'status': 'started', 'message': 'RTSP推流已启动'} except Exception as e: logger.error(f'启动RTSP失败: {e}') return {'status': 'error', 'message': f'启动失败: {str(e)}'} def stop_rtsp_stream(self): """停止RTSP推流""" try: self.rtsp_running = False logger.info('RTSP推流已停止') return {'status': 'stopped', 'message': 'RTSP推流已停止'} except Exception as e: logger.error(f'停止RTSP失败: {e}') return {'status': 'error', 'message': f'停止失败: {str(e)}'} def is_streaming(self): """检查是否正在推流""" return self.rtsp_running def get_stream_status(self): """获取推流状态""" return { 'running': self.rtsp_running, 'rtsp_url': self.rtsp_url, 'thread_alive': self.rtsp_thread.is_alive() if self.rtsp_thread else False } def cleanup(self): """清理资源""" try: self.rtsp_running = False if self.rtsp_thread and self.rtsp_thread.is_alive(): self.rtsp_thread.join(timeout=2) self.encoding_executor.shutdown(wait=False) # 清空队列 while not self.frame_queue.empty(): try: frame, _ = self.frame_queue.get_nowait() del frame except queue.Empty: break logger.info('视频推流资源已清理') except Exception as e: logger.error(f'清理视频推流资源失败: {e}')