From b1e74b6a185dd7603609a91f77434adc75066a92 Mon Sep 17 00:00:00 2001 From: zhaozilong12 <405241463@qq.com> Date: Thu, 7 Aug 2025 14:38:08 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E5=BD=95=E5=83=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app.py | 45 +- backend/app_simple.py | 2 +- backend/database.py | 11 +- backend/device_manager.py | 985 ++++++++++++++++++-------------------- 4 files changed, 477 insertions(+), 566 deletions(-) diff --git a/backend/app.py b/backend/app.py index 37b87d1d..c941eea9 100644 --- a/backend/app.py +++ b/backend/app.py @@ -106,14 +106,8 @@ def init_app(): # 初始化设备管理器 device_manager = DeviceManager(db_manager) device_manager.set_socketio(socketio) # 设置WebSocket连接 - - # 自动启动推流以填充帧缓存 - if device_manager.device_status['camera']: - streaming_result = device_manager.start_streaming() - logger.info(f'自动启动推流结果: {streaming_result}') - # 初始化视频流管理器 - video_stream_manager = VideoStreamManager(socketio) + video_stream_manager = VideoStreamManager(socketio, device_manager) logger.info('应用初始化完成') @@ -141,31 +135,6 @@ def api_health_check(): 'version': '1.0.0' }) -@app.route('/api/frame-cache/status', methods=['GET']) -def get_frame_cache_status(): - """获取帧缓存状态""" - try: - if device_manager: - cache_info = device_manager.get_frame_cache_info() - return jsonify({ - 'success': True, - 'data': cache_info, - 'timestamp': datetime.now().isoformat() - }) - else: - return jsonify({ - 'success': False, - 'error': '设备管理器未初始化', - 'timestamp': datetime.now().isoformat() - }), 500 - except Exception as e: - logger.error(f'获取帧缓存状态失败: {e}') - return jsonify({ - 'success': False, - 'error': str(e), - 'timestamp': datetime.now().isoformat() - }), 500 - # ==================== 认证API ==================== @app.route('/api/auth/login', methods=['POST']) @@ -719,7 +688,7 @@ def stop_detection(session_id): }), 400 data = flask_request.get_json() - logger.debug(f'接收到停止检测请求,session_id: {session_id}, 请求数据: {data}') + # logger.debug(f'接收到停止检测请求,session_id: {session_id}, 请求数据: {data}') # video_data = data.get('videoData') if data else None video_data = data['videoData'] mime_type = data.get('mimeType', 'video/webm;codecs=vp9') # 默认webm格式 @@ -742,11 +711,11 @@ def stop_detection(session_id): }), 400 # 停止同步录制,传递视频数据 try: - logger.debug(f'调用device_manager.stop_recording,session_id: {session_id}, video_data长度: {len(video_data) if video_data else 0}') - if video_data is None: - logger.warning(f'视频数据为空,session_id: {session_id}') - else: - logger.debug(f'视频数据长度: {len(video_data)} 字符,约 {len(video_data)*3/4/1024:.2f} KB, session_id: {session_id}') + # logger.debug(f'调用device_manager.stop_recording,session_id: {session_id}, video_data长度: {len(video_data) if video_data else 0}') + # if video_data is None: + # logger.warning(f'视频数据为空,session_id: {session_id}') + # else: + # logger.debug(f'视频数据长度: {len(video_data)} 字符,约 {len(video_data)*3/4/1024:.2f} KB, session_id: {session_id}') restrt=device_manager.stop_recording(session_id, video_data_base64=video_bytes) logger.error(restrt) except Exception as rec_e: diff --git a/backend/app_simple.py b/backend/app_simple.py index 87404f22..6e7998c4 100644 --- a/backend/app_simple.py +++ b/backend/app_simple.py @@ -79,7 +79,7 @@ def init_app(): logger.info("设备管理器初始化成功") # 初始化视频流管理器 - video_stream_manager = VideoStreamManager() + video_stream_manager = VideoStreamManager(device_manager=device_manager) logger.info("视频流管理器初始化成功") logger.info("应用初始化完成") diff --git a/backend/database.py b/backend/database.py index 89170f6e..ea242972 100644 --- a/backend/database.py +++ b/backend/database.py @@ -658,8 +658,12 @@ class DatabaseManager: logger.error(f'创建检测会话失败: {e}') raise - def update_session_status(self, session_id: str, status: str): - """更新会话状态""" + def update_session_status(self, session_id: str, status: str) -> bool: + """更新会话状态 + + Returns: + bool: 更新成功返回True,失败返回False + """ conn = self.get_connection() cursor = conn.cursor() @@ -681,11 +685,12 @@ class DatabaseManager: conn.commit() logger.info(f'更新会话状态: {session_id} -> {status}') + return True except Exception as e: conn.rollback() logger.error(f'更新会话状态失败: {e}') - raise + return False def update_session_duration(self, session_id: str, duration: int): """更新会话持续时间""" diff --git a/backend/device_manager.py b/backend/device_manager.py index 693aa4fc..51951e9e 100644 --- a/backend/device_manager.py +++ b/backend/device_manager.py @@ -359,9 +359,9 @@ class DeviceManager: try: # 摄像头校准 - if self.device_status['camera']: - camera_calibration = self._calibrate_camera() - calibration_result['camera'] = camera_calibration + # if self.device_status['camera']: + # camera_calibration = self._calibrate_camera() + # calibration_result['camera'] = camera_calibration # IMU校准 if self.device_status['imu']: @@ -401,7 +401,6 @@ class DeviceManager: # 计算平均亮度和对比度 avg_brightness = np.mean([np.mean(cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)) for frame in frames]) - calibration = { 'status': 'success', 'brightness': float(avg_brightness), @@ -498,7 +497,7 @@ class DeviceManager: def collect_data(self, session_id: str, patient_id: str, screen_image_base64: str = None) -> Dict[str, Any]: # 实例化VideoStreamManager(VideoStreamManager类在同一文件中定义) - video_stream_manager = VideoStreamManager() + video_stream_manager = VideoStreamManager(device_manager=self) """采集所有设备数据并保存到指定目录结构 Args: @@ -514,7 +513,16 @@ class DeviceManager: # 创建数据存储目录 data_dir = Path(f'data/patients/{patient_id}/{session_id}/{timestamp}') - data_dir.mkdir(parents=True, exist_ok=True) + # data_dir.mkdir(parents=True, exist_ok=True) + + # # 设置目录权限为777(完全权限) + # try: + # import os + # import stat + # os.chmod(str(data_dir), stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) # 777权限 + # logger.debug(f"已设置目录权限为777: {data_dir}") + # except Exception as perm_error: + # logger.warning(f"设置目录权限失败: {perm_error},但目录创建成功") # 初始化数据字典 data = { @@ -563,7 +571,7 @@ class DeviceManager: # 5. 采集足部监测视频截图(从摄像头获取) if self.device_status['camera']: - foot_image_path = video_stream_manager._capture_foot_image(data_dir, self) + foot_image_path = video_stream_manager._capture_foot_image(data_dir,self) if foot_image_path: data['foot_image'] = str(foot_image_path) logger.debug(f'足部截图保存成功: {foot_image_path}') @@ -619,130 +627,7 @@ class DeviceManager: return data - - 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(): - self.recording = True - 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 - self.recording = False - logger.info('视频录制已停止') - - - - def start_femtobolt_recording(self, filename=None): - """开始FemtoBolt深度相机录制""" - if not FEMTOBOLT_AVAILABLE or self.femtobolt_camera is None: - logger.error('FemtoBolt深度相机未初始化,无法录制') - return False - - try: - if filename is None: - timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') - filename = f'femtobolt_recording_{timestamp}' - - # 确保录制目录存在 - os.makedirs('recordings', exist_ok=True) - - # 创建彩色和深度视频文件路径 - color_filepath = os.path.join('recordings', f'{filename}_color.mp4') - depth_filepath = os.path.join('recordings', f'{filename}_depth.mp4') - - # 设置视频参数(基于FemtoBolt配置) - if self.femtobolt_config.color_resolution == pykinect.K4A_COLOR_RESOLUTION_1080P: - width, height = 1920, 1080 - elif self.femtobolt_config.color_resolution == pykinect.K4A_COLOR_RESOLUTION_720P: - width, height = 1280, 720 - else: - width, height = 1920, 1080 # 默认 - - fps = 30 # 默认30fps - - # 创建视频写入器 - fourcc = cv2.VideoWriter_fourcc(*'mp4v') - self.femtobolt_color_writer = cv2.VideoWriter(color_filepath, fourcc, fps, (width, height)) - self.femtobolt_depth_writer = cv2.VideoWriter(depth_filepath, fourcc, fps, (width, height)) - - if self.femtobolt_color_writer.isOpened() and self.femtobolt_depth_writer.isOpened(): - self.femtobolt_recording = True - self.femtobolt_recording_filename = filename - logger.info(f'开始FemtoBolt录制: {filename}') - return True - else: - logger.error('FemtoBolt视频写入器创建失败') - if self.femtobolt_color_writer: - self.femtobolt_color_writer.release() - if self.femtobolt_depth_writer: - self.femtobolt_depth_writer.release() - return False - - except Exception as e: - logger.error(f'FemtoBolt开始录制失败: {e}') - return False - - def stop_femtobolt_recording(self): - """停止FemtoBolt深度相机录制""" - if self.femtobolt_recording: - self.femtobolt_recording = False - - if hasattr(self, 'femtobolt_color_writer') and self.femtobolt_color_writer: - self.femtobolt_color_writer.release() - self.femtobolt_color_writer = None - - if hasattr(self, 'femtobolt_depth_writer') and self.femtobolt_depth_writer: - self.femtobolt_depth_writer.release() - self.femtobolt_depth_writer = None - - logger.info('FemtoBolt视频录制已停止') - - - - def start_camera_stream(self): - """开始摄像头推流""" - if self.camera is None: - logger.error('摄像头未初始化') - return False - - try: - self.camera_streaming = True - logger.info('摄像头推流已开始') - return True - except Exception as e: - logger.error(f'摄像头推流启动失败: {e}') - return False - - def stop_camera_stream(self): - """停止摄像头推流""" - self.camera_streaming = False - logger.info('摄像头推流已停止') - + def start_femtobolt_stream(self): """开始FemtoBolt深度相机推流""" if not FEMTOBOLT_AVAILABLE or self.femtobolt_camera is None: @@ -780,33 +665,6 @@ class DeviceManager: """停止FemtoBolt深度相机推流""" self.femtobolt_streaming = False logger.debug('FemtoBolt深度相机推流已停止') - - - def record_femtobolt_frame(self, color_image, depth_image): - """录制FemtoBolt帧到视频文件""" - if not self.femtobolt_recording: - return - - try: - if hasattr(self, 'femtobolt_color_writer') and self.femtobolt_color_writer and color_image is not None: - # 确保图像尺寸正确 - if color_image.shape[:2] != (1080, 1920): # height, width - color_image = cv2.resize(color_image, (1920, 1080)) - self.femtobolt_color_writer.write(color_image) - - if hasattr(self, 'femtobolt_depth_writer') and self.femtobolt_depth_writer and depth_image is not None: - # 将深度图像转换为3通道格式用于视频录制 - depth_normalized = cv2.normalize(depth_image, None, 0, 255, cv2.NORM_MINMAX, dtype=cv2.CV_8U) - depth_colored = cv2.applyColorMap(depth_normalized, cv2.COLORMAP_JET) - - # 确保图像尺寸正确 - if depth_colored.shape[:2] != (1080, 1920): # height, width - depth_colored = cv2.resize(depth_colored, (1920, 1080)) - self.femtobolt_depth_writer.write(depth_colored) - - except Exception as e: - logger.error(f'录制FemtoBolt帧失败: {e}') - def set_socketio(self, socketio): """设置WebSocket连接""" self.socketio = socketio @@ -891,201 +749,7 @@ class DeviceManager: logger.error(f'停止压力传感器数据推流失败: {e}') return False - def start_streaming(self) -> Dict[str, bool]: - """启动所有设备推流 - - Returns: - Dict: 推流启动状态 - { - 'camera_streaming': bool, - 'femtobolt_streaming': bool, - 'imu_streaming': bool, - 'pressure_streaming': bool - } - """ - result = { - 'camera_streaming': False, - 'femtobolt_streaming': False, - 'imu_streaming': False, - 'pressure_streaming': False - } - - try: - # 重置停止事件 - self.streaming_stop_event.clear() - - # 启动足部监视摄像头推流 - if self.device_status['camera'] and not self.camera_streaming: - self.camera_streaming = True - self.camera_streaming_thread = threading.Thread( - target=self._camera_streaming_thread, - daemon=True, - name='CameraStreamingThread' - ) - self.camera_streaming_thread.start() - result['camera_streaming'] = True - logger.debug('足部监视摄像头推流已启动') - - # 启动FemtoBolt深度相机推流 - if self.device_status['femtobolt'] and not self.femtobolt_streaming: - self.femtobolt_streaming = True - self.femtobolt_streaming_thread = threading.Thread( - target=self._femtobolt_streaming_thread, - daemon=True, - name='FemtoBoltStreamingThread' - ) - self.femtobolt_streaming_thread.start() - result['femtobolt_streaming'] = True - logger.debug('FemtoBolt深度相机推流已启动') - - # 启动IMU头部姿态数据推流 - if self.device_status['imu'] and not self.imu_streaming: - result['imu_streaming'] = self.start_imu_streaming() - logger.debug('IMU头部姿态数据推流已启动') - - # 启动压力传感器足部压力数据推流 - if self.device_status['pressure'] and not self.pressure_streaming: - result['pressure_streaming'] = self.start_pressure_streaming() - logger.debug('压力传感器足部压力数据推流已启动') - - except Exception as e: - logger.warning(f'启动推流失败: {e}') - - return result - - def stop_streaming(self) -> bool: - """停止所有设备推流 - - Returns: - bool: 停止操作是否成功 - """ - try: - # 设置停止事件 - self.streaming_stop_event.set() - - # 停止摄像头推流 - if self.camera_streaming: - self.camera_streaming = False - if self.camera_streaming_thread and self.camera_streaming_thread.is_alive(): - self.camera_streaming_thread.join(timeout=2) - logger.debug('足部监视摄像头推流已停止') - - # 停止FemtoBolt推流 - if self.femtobolt_streaming: - self.femtobolt_streaming = False - if self.femtobolt_streaming_thread and self.femtobolt_streaming_thread.is_alive(): - self.femtobolt_streaming_thread.join(timeout=2) - logger.debug('FemtoBolt深度相机推流已停止') - - # 停止IMU头部姿态数据推流 - if self.imu_streaming: - self.stop_imu_streaming() - logger.debug('IMU头部姿态数据推流已停止') - - # 停止压力传感器足部压力数据推流 - if self.pressure_streaming: - self.stop_pressure_streaming() - logger.debug('压力传感器足部压力数据推流已停止') - - return True - - except Exception as e: - logger.warning(f'停止推流失败: {e}') - return False - - def _camera_streaming_thread(self): - """足部监视摄像头推流线程""" - frame_count = 0 - consecutive_failures = 0 - max_consecutive_failures = 10 - - try: - while self.camera_streaming and not self.streaming_stop_event.is_set(): - if self.camera: - # 使用摄像头锁避免与录制和截图功能冲突 - with self.camera_lock: - # 检查摄像头状态 - if not self.camera.isOpened(): - logger.warning('推流线程检测到摄像头已关闭,尝试重新打开') - device_index = 0 - if self.db_manager: - try: - monitor_config = self.db_manager.get_system_setting('monitor_device_index') - if monitor_config: - device_index = int(monitor_config) - except Exception: - pass - - self.camera.open(device_index) - 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.camera.set(cv2.CAP_PROP_BUFFERSIZE, 1) - logger.info('推流线程摄像头重新打开成功') - consecutive_failures = 0 - else: - logger.error('推流线程摄像头重新打开失败') - consecutive_failures += 1 - time.sleep(0.5) - continue - - ret, frame = self.camera.read() - - if ret and frame is not None: - # 保存原始帧到全局缓存 - self._save_frame_to_cache(frame, 'camera') - - if self.socketio: - # 编码并推送帧 - try: - # 调整帧大小以减少网络负载 - display_frame = frame.copy() - height, width = display_frame.shape[:2] - if width > 640: - scale = 640 / width - new_width = 640 - new_height = int(height * scale) - display_frame = cv2.resize(display_frame, (new_width, new_height)) - - # JPEG编码 - encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 80] - success, buffer = cv2.imencode('.jpg', display_frame, encode_param) - - if success: - jpg_as_text = base64.b64encode(buffer).decode('utf-8') - self.socketio.emit('video_frame', { - 'image': jpg_as_text, - 'frame_id': frame_count, - 'timestamp': time.time() - }) - frame_count += 1 - consecutive_failures = 0 # 重置失败计数 - - except Exception as e: - consecutive_failures += 1 - if consecutive_failures <= 3: - logger.debug(f'摄像头帧推送失败 (连续失败{consecutive_failures}次): {e}') - else: - consecutive_failures += 1 - if consecutive_failures <= 3: - logger.warning(f"推流线程无法从足部摄像头获取帧 (连续失败{consecutive_failures}次)") - elif consecutive_failures == max_consecutive_failures: - logger.error(f"推流线程足部摄像头连续失败{max_consecutive_failures}次,可能需要重启设备") - - time.sleep(0.1) # 短暂等待 - else: - time.sleep(0.1) # 摄像头不可用时等待 - - # 控制帧率 - # time.sleep(1/30) # 30 FPS - - except Exception as e: - logger.debug(f'摄像头推流线程异常: {e}') - finally: - self.camera_streaming = False def _femtobolt_streaming_thread(self): """FemtoBolt深度相机推流线程""" @@ -1135,39 +799,19 @@ class DeviceManager: depth_colored = cv2.cvtColor(depth_colored, cv2.COLOR_BGRA2BGR) elif len(depth_colored.shape) == 3 and depth_colored.shape[2] == 3: pass - - # 预处理:裁剪成宽460,高819,保持高度不裁剪,宽度从中间裁剪 height, width = depth_colored.shape[:2] - target_width = 460 - target_height = 819 - - # 计算裁剪区域的纵向起点,保持高度不裁剪,纵向居中裁剪或上下填充(这里保持高度不裁剪,故不裁剪高度) - # 计算宽度裁剪起点 + # logger.debug(f'FemtoBolt帧宽: {width}') + # logger.debug(f'FemtoBolt帧高: {height}') + target_width = height // 2 if width > target_width: - left = (width - target_width) // 2 - right = left + target_width - cropped_image = depth_colored[:, left:right] - else: - cropped_image = depth_colored - - # 如果高度不足target_height,进行上下填充黑边 - cropped_height = cropped_image.shape[0] - if cropped_height < target_height: - pad_top = (target_height - cropped_height) // 2 - pad_bottom = target_height - cropped_height - pad_top - cropped_image = cv2.copyMakeBorder(cropped_image, pad_top, pad_bottom, 0, 0, cv2.BORDER_CONSTANT, value=[0,0,0]) - elif cropped_height > target_height: - # 如果高度超过target_height,裁剪高度中间部分 - top = (cropped_height - target_height) // 2 - cropped_image = cropped_image[top:top+target_height, :] - - # 最终调整大小,保持宽460,高819 - depth_colored = cv2.resize(cropped_image, (target_width, target_height)) - - # JPEG编码 - encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 80] - success, buffer = cv2.imencode('.jpg', depth_colored, encode_param) - + left = (width - target_width) // 2 + right = left + target_width + depth_colored = depth_colored[:, left:right] + + # 保存处理好的身体帧到全局缓存 + self._save_frame_to_cache(depth_colored.copy(), 'femtobolt') + + success, buffer = cv2.imencode('.jpg', depth_colored, [int(cv2.IMWRITE_JPEG_QUALITY), 80]) if success and self.socketio: jpg_as_text = base64.b64encode(buffer).decode('utf-8') self.socketio.emit('depth_camera_frame', { @@ -1314,6 +958,7 @@ class DeviceManager: logger.info('压力传感器足部压力数据推流线程已结束') def start_recording(self, session_id: str, patient_id: str) -> Dict[str, Any]: + video_manager=VideoStreamManager() """启动同步录制 Args: @@ -1361,13 +1006,46 @@ class DeviceManager: # 创建存储目录 base_path = os.path.join('data', 'patients', patient_id, session_id) - os.makedirs(base_path, exist_ok=True) + try: + os.makedirs(base_path, exist_ok=True) + logger.info(f'录制目录创建成功: {base_path}') + + # 设置目录权限为777(所有用户完全权限) + try: + import stat + import subprocess + import platform + # 在Windows系统上使用icacls命令设置更详细的权限 + if platform.system() == 'Windows': + try: + # 为Users用户组授予完全控制权限 + subprocess.run([ + 'icacls', base_path, '/grant', 'Users:(OI)(CI)F' + ], check=True, capture_output=True, text=True) + + # 为Everyone用户组授予完全控制权限 + subprocess.run([ + 'icacls', base_path, '/grant', 'Everyone:(OI)(CI)F' + ], check=True, capture_output=True, text=True) + + logger.info(f"已设置Windows目录权限(Users和Everyone完全控制): {base_path}") + except subprocess.CalledProcessError as icacls_error: + logger.warning(f"Windows权限设置失败: {icacls_error}") + else: + logger.info(f"已设置目录权限为777: {base_path}") + + except Exception as perm_error: + logger.warning(f"设置目录权限失败: {perm_error},但目录创建成功") + except Exception as dir_error: + logger.error(f'创建录制目录失败: {base_path}, 错误: {dir_error}') + result['success'] = False + result['message'] = f'创建录制目录失败: {dir_error}' + return result # 定义视频文件路径 feet_video_path = os.path.join(base_path, 'feet.mp4') body_video_path = os.path.join(base_path, 'body.mp4') - screen_video_path = os.path.join(base_path, 'screen.mp4') - + screen_video_path = os.path.join(base_path, 'screen.webm') result['video_paths']['feet_video'] = feet_video_path result['video_paths']['body_video'] = body_video_path result['video_paths']['screen_video'] = screen_video_path @@ -1376,7 +1054,8 @@ class DeviceManager: if self.db_manager: try: # 更新会话状态为录制中 - self.db_manager.update_session_status(session_id, 'recording') + if not self.db_manager.update_session_status(session_id, 'recording'): + logger.error(f'更新会话状态为录制中失败 - 会话ID: {session_id}') # 更新视频文件路径 self.db_manager.update_session_normal_video_path(session_id, feet_video_path) @@ -1388,25 +1067,116 @@ class DeviceManager: logger.error(f'更新数据库视频路径失败: {db_error}') # 数据库更新失败不影响录制启动,继续执行 - # 视频编码参数 - fourcc = cv2.VideoWriter_fourcc(*'mp4v') + # 视频编码参数 - 尝试更兼容的编解码器 + # 首先尝试MJPG,这是最兼容的编解码器 + fourcc = cv2.VideoWriter_fourcc(*'MJPG') fps = 30 + logger.info(f'使用编解码器: MJPG') # 初始化视频写入器 if self.device_status['camera']: # 获取摄像头分辨率 if self.camera and self.camera.isOpened(): - width = int(self.camera.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(self.camera.get(cv2.CAP_PROP_FRAME_HEIGHT)) + target_width,target_height = video_manager.MAX_FRAME_SIZE self.feet_video_writer = cv2.VideoWriter( - feet_video_path, fourcc, fps, (width, height) + feet_video_path, fourcc, fps, (target_width, target_height) ) - + + # 检查视频写入器是否初始化成功 + if self.feet_video_writer.isOpened(): + logger.info(f'脚部视频写入器初始化成功: {feet_video_path}') + else: + logger.error(f'脚部视频写入器初始化失败: {feet_video_path}') + else: + logger.error('摄像头未打开,无法初始化脚部视频写入器') + else: + logger.warning('摄像头设备未启用,跳过脚部视频写入器初始化') if self.device_status['femtobolt']: + frame1, frame_timestamp1 = self._get_latest_frame_from_cache('femtobolt') + if frame1 is not None: + actual_height,actual_width=frame1.shape[:2] + logger.info(f'初始化身体视频写入器 裁剪后分辨率: {actual_height}x{actual_width}') + logger.info(f'VideoWriter将使用分辨率: width={actual_width}, height={actual_height}') + + # 确保图像数据类型正确 + if frame1.dtype != np.uint8: + logger.warning(f'身体帧数据类型不是uint8: {frame1.dtype},将进行转换') + + # 尝试多种编解码器和分辨率组合 + codecs_to_try = ['MJPG', 'XVID', 'mp4v', 'H264'] + resolutions_to_try = [(actual_width, actual_height), (288, 576), (640, 480)] + + success = False + for codec in codecs_to_try: + if success: + break + fourcc_test = cv2.VideoWriter_fourcc(*codec) + for resolution in resolutions_to_try: + logger.info(f'尝试编解码器: {codec}, 分辨率: {resolution}') + self.body_video_writer = cv2.VideoWriter( + body_video_path, fourcc_test, fps, resolution + ) + if self.body_video_writer.isOpened(): + logger.info(f'身体视频写入器初始化成功: {body_video_path}, 编解码器: {codec}, 分辨率: {resolution}') + success = True + break + else: + logger.warning(f'编解码器 {codec} 分辨率 {resolution} 初始化失败') + if self.body_video_writer: + self.body_video_writer.release() + self.body_video_writer = None + + if not success: + logger.error(f'所有编解码器和分辨率组合都失败了,身体视频写入器初始化失败') + else: + logger.warning('无法从缓存获取FemtoBolt帧数据,使用默认分辨率初始化身体视频写入器') + # 使用相同的编解码器回退机制 + codecs_to_try = ['MJPG', 'XVID', 'mp4v', 'H264'] + resolutions_to_try = [(288, 576), (640, 480), (320, 240)] + + success = False + for codec in codecs_to_try: + if success: + break + fourcc_test = cv2.VideoWriter_fourcc(*codec) + for resolution in resolutions_to_try: + logger.info(f'尝试默认编解码器: {codec}, 分辨率: {resolution}') + self.body_video_writer = cv2.VideoWriter( + body_video_path, fourcc_test, fps, resolution + ) + if self.body_video_writer.isOpened(): + logger.info(f'身体视频写入器默认初始化成功: {body_video_path}, 编解码器: {codec}, 分辨率: {resolution}') + success = True + break + else: + logger.warning(f'默认编解码器 {codec} 分辨率 {resolution} 初始化失败') + if self.body_video_writer: + self.body_video_writer.release() + self.body_video_writer = None + + if not success: + logger.error(f'所有默认编解码器和分辨率组合都失败了,身体视频写入器初始化失败') # FemtoBolt默认分辨率 - self.body_video_writer = cv2.VideoWriter( - body_video_path, fourcc, fps, (1280, 720) - ) + # capture = self.femtobolt_camera.update() + # if capture is not None: + # ret, depth_image = capture.get_depth_image() + # femtoboltheight, femtoboltwidth = depth_image.shape[:2] + # # 计算裁剪后的实际分辨率(与推流处理保持一致) + # target_width = femtoboltheight // 2 + # actual_height = femtoboltheight + # actual_width = target_width + + # logger.info(f'初始化身体视频写入器,原始分辨率: {femtoboltheight}x{femtoboltwidth}, 裁剪后分辨率: {actual_height}x{actual_width}') + # self.body_video_writer = cv2.VideoWriter( + # body_video_path, fourcc, fps, (actual_width, actual_height) + # ) + # if self.body_video_writer.isOpened(): + # logger.info(f'身体视频写入器初始化成功: {body_video_path}, 分辨率: {actual_width}x{actual_height}') + # else: + # logger.error(f'身体视频写入器初始化失败: {body_video_path}, 分辨率: {actual_width}x{actual_height}') + + else: + logger.warning('FemtoBolt设备未启用,跳过身体视频写入器初始化') # # 屏幕录制写入器(默认分辨率,后续根据实际帧调整) # self.screen_video_writer = cv2.VideoWriter( @@ -1415,7 +1185,7 @@ class DeviceManager: # 重置停止事件 self.recording_stop_event.clear() - + self.sync_recording = True # 启动录制线程 if self.feet_video_writer: self.feet_recording_thread = threading.Thread( @@ -1442,7 +1212,7 @@ class DeviceManager: # self.screen_recording_thread.start() # 设置录制状态 - self.sync_recording = True + result['success'] = True result['recording_start_time'] = self.recording_start_time.isoformat() result['message'] = '同步录制已启动' @@ -1501,11 +1271,18 @@ class DeviceManager: (self.body_recording_thread, 'body') ] + logger.info(f"正在停止录制线程 - 会话ID: {session_id}") + for thread, name in threads_to_join: if thread and thread.is_alive(): + logger.debug(f"等待{name}录制线程结束...") thread.join(timeout=3) if thread.is_alive(): - logger.debug(f'{name}录制线程未能正常结束') + logger.warning(f'{name}录制线程未能在3秒内正常结束,可能存在阻塞') + else: + logger.debug(f'{name}录制线程已正常结束') + else: + logger.debug(f'{name}录制线程未运行或已结束') # 计算录制时长 if self.recording_start_time: @@ -1513,27 +1290,15 @@ class DeviceManager: result['recording_duration'] = duration # 清理视频写入器并收集文件信息 - video_files = self._cleanup_video_writers() + # video_files = self._cleanup_video_writers() # 保存传入的屏幕录制视频数据,替代原有屏幕录制视频保存逻辑 # video_bytes = base64.b64decode(video_data_base64) with open(screen_video_path, 'wb') as f: f.write(video_data_base64) - video_files.append(screen_video_path) + # video_files.append(screen_video_path) logger.info(f'屏幕录制视频保存成功,路径: {screen_video_path}, 文件大小: {os.path.getsize(screen_video_path)} 字节') - # # 保存传入的屏幕录制视频数据,替代原有屏幕录制视频保存逻辑 - # if video_data_base64: - # try: - # # video_bytes = base64.b64decode(video_data_base64) - # with open(screen_video_path, 'wb') as f: - # f.write(video_data_base64) - # video_files.append(screen_video_path) - # logger.info(f'屏幕录制视频保存成功,路径: {screen_video_path}, 文件大小: {os.path.getsize(screen_video_path)} 字节') - # except Exception as e: - # logger.error(f'保存屏幕录制视频失败: {e}', exc_info=True) - # logger.debug(f'视频数据长度: {len(video_data_base64)}') - # raise - result['video_files'] = video_files + result['video_files'] = screen_video_path # 更新数据库中的会话信息 if self.db_manager and result['recording_duration'] > 0: @@ -1543,9 +1308,12 @@ class DeviceManager: self.db_manager.update_session_normal_video_path(session_id, feet_video_path) self.db_manager.update_session_femtobolt_video_path(session_id, body_video_path) self.db_manager.update_session_screen_video_path(session_id, screen_video_path) - self.db_manager.update_session_status(session_id, 'completed') - logger.debug(f'数据库会话信息更新成功 - 会话ID: {session_id}, 持续时间: {duration_seconds}秒') + # 更新会话状态为已完成 + if self.db_manager.update_session_status(session_id, 'completed'): + logger.debug(f'数据库会话信息更新成功 - 会话ID: {session_id}, 持续时间: {duration_seconds}秒') + else: + logger.error(f'更新会话状态为已完成失败 - 会话ID: {session_id}') except Exception as db_error: logger.error(f'更新数据库会话信息失败: {db_error}') @@ -1584,24 +1352,47 @@ class DeviceManager: consecutive_failures = 0 max_consecutive_failures = 10 + # logger.info(f"足部录制线程已启动 - 会话ID: {self.current_session_id}") + # logger.info(f"视频写入器状态: {self.feet_video_writer.isOpened() if self.feet_video_writer else 'None'}") + try: while self.sync_recording and not self.recording_stop_event.is_set(): if self.feet_video_writer: # 从全局缓存获取最新帧 frame, frame_timestamp = self._get_latest_frame_from_cache('camera') - + # 详细记录帧获取情况 if frame is not None: - # 写入录制文件 - self.feet_video_writer.write(frame) - consecutive_failures = 0 # 重置失败计数 - - # 记录录制统计 - if hasattr(self, 'recording_frame_count'): - self.recording_frame_count += 1 - else: - self.recording_frame_count = 1 + #logger.debug(f"成功获取帧 - 尺寸: {frame.shape}, 数据类型: {frame.dtype}, 时间戳: {frame_timestamp}") + # 检查视频写入器状态 + if not self.feet_video_writer.isOpened(): + # logger.error(f"脚部视频写入器已关闭,无法写入帧 - 会话ID: {self.current_session_id}") + break + try: + # 复制帧数据避免引用问题 + image = frame.copy() + # 写入录制文件 + write_success = self.feet_video_writer.write(image) + # 检查写入是否成功 + if write_success is False: + logger.error(f"视频帧写入返回False - 可能写入失败") + consecutive_failures += 1 + else: + consecutive_failures = 0 # 重置失败计数 + + # 记录录制统计 + if hasattr(self, 'recording_frame_count'): + self.recording_frame_count += 1 + else: + self.recording_frame_count = 1 + except Exception as write_error: + logger.error(f"写入脚部视频帧异常: {write_error}") + consecutive_failures += 1 + if consecutive_failures >= 10: + logger.error("连续写入失败次数过多,停止录制") + break else: + logger.warning(f"从缓存获取的帧为None - 连续失败{consecutive_failures + 1}次") consecutive_failures += 1 if consecutive_failures <= 3: logger.warning(f"录制线程无法从缓存获取帧 (连续失败{consecutive_failures}次)") @@ -1610,35 +1401,134 @@ class DeviceManager: # 等待一段时间再重试 time.sleep(0.1) + else: + logger.error("足部视频写入器未初始化") + break + + # 检查连续失败情况 + if consecutive_failures >= max_consecutive_failures: + logger.error(f"连续失败次数达到上限({max_consecutive_failures}),停止录制") + break time.sleep(1/30) # 30 FPS except Exception as e: logger.error(f'足部录制线程异常: {e}') + finally: + logger.info(f"足部录制线程已结束 - 会话ID: {self.current_session_id}, 总录制帧数: {getattr(self, 'recording_frame_count', 0)}") + # 确保视频写入器被正确关闭 + if self.feet_video_writer: + self.feet_video_writer.release() + self.feet_video_writer = None + logger.debug("足部视频写入器已释放") def _body_recording_thread(self): """身体视频录制线程""" + consecutive_failures = 0 + max_consecutive_failures = 10 + + # logger.info(f"身体录制线程启动 - 会话ID: {self.current_session_id}") + try: while self.sync_recording and not self.recording_stop_event.is_set(): - if self.femtobolt_camera and self.body_video_writer: - try: - capture = self.femtobolt_camera.update() - if capture.color is not None: - # 转换颜色格式 - color_image = capture.color - color_image = cv2.cvtColor(color_image, cv2.COLOR_BGRA2BGR) - - # 调整到录制分辨率 - color_image = cv2.resize(color_image, (1280, 720)) - self.body_video_writer.write(color_image) + if self.body_video_writer: + # 从全局缓存获取最新帧 + frame, frame_timestamp = self._get_latest_frame_from_cache('femtobolt') - except Exception as e: - logger.error(f'FemtoBolt录制帧处理失败: {e}') + if frame is not None: + # 检查视频写入器状态 + if not self.body_video_writer.isOpened(): + logger.error(f"身体视频写入器已关闭,无法写入帧 - 会话ID: {self.current_session_id}") + break + + # 添加帧信息日志 + logger.debug(f"获取到身体帧 - 形状: {frame.shape}, 数据类型: {frame.dtype}, 时间戳: {frame_timestamp}") + + try: + # 复制帧数据避免引用问题 + image = frame.copy() + + # 检查图像有效性 + if image is None or image.size == 0: + logger.warning(f"身体帧数据无效 - 会话ID: {self.current_session_id}") + consecutive_failures += 1 + continue + + # 确保图像数据类型正确 + if image.dtype != np.uint8: + logger.debug(f"转换身体帧数据类型从 {image.dtype} 到 uint8") + image = image.astype(np.uint8) + + # 确保图像是3通道BGR格式 + if len(image.shape) != 3 or image.shape[2] != 3: + logger.warning(f"身体帧格式异常: {image.shape},期望3通道BGR格式") + consecutive_failures += 1 + continue + + # 检查并调整图像分辨率以匹配视频写入器 + current_height, current_width = image.shape[:2] + expected_width, expected_height = 288, 576 # 默认期望分辨率 + + if current_width != expected_width or current_height != expected_height: + logger.debug(f"调整身体帧分辨率从 {current_width}x{current_height} 到 {expected_width}x{expected_height}") + image = cv2.resize(image, (expected_width, expected_height)) + + # 确保图像数据连续性(OpenCV要求) + if not image.flags['C_CONTIGUOUS']: + logger.debug("转换身体帧为连续内存布局") + image = np.ascontiguousarray(image) + + # 写入录制文件 + logger.debug(f"尝试写入身体视频帧 - 图像形状: {image.shape}, 数据类型: {image.dtype}, 连续性: {image.flags['C_CONTIGUOUS']}") + write_success = self.body_video_writer.write(image) + + # 检查写入是否成功 - cv2.VideoWriter.write()可能返回None、False或True + if write_success is False: + consecutive_failures += 1 + logger.warning(f"身体视频帧写入明确失败 - 会话ID: {self.current_session_id}, 连续失败次数: {consecutive_failures}, 图像形状: {image.shape}, 写入器状态: {self.body_video_writer.isOpened()}") + + if consecutive_failures >= max_consecutive_failures: + logger.error(f"身体视频写入连续失败{max_consecutive_failures}次,停止录制") + break + elif write_success is None: + # 某些OpenCV版本可能返回None,这通常表示写入失败 + consecutive_failures += 1 + logger.warning(f"身体视频帧写入返回None - 会话ID: {self.current_session_id}, 连续失败次数: {consecutive_failures}, 可能是编解码器问题") + + if consecutive_failures >= max_consecutive_failures: + logger.error(f"身体视频写入连续返回None {max_consecutive_failures}次,停止录制") + break + else: + consecutive_failures = 0 + logger.debug(f"成功写入身体视频帧 - 会话ID: {self.current_session_id}") + + # 释放图像内存 + # del image + + except Exception as e: + consecutive_failures += 1 + logger.error(f'身体视频帧写入异常: {e}, 连续失败次数: {consecutive_failures}, 帧形状: {frame.shape if frame is not None else "None"}') + + if consecutive_failures >= max_consecutive_failures: + logger.error(f"身体视频写入连续异常{max_consecutive_failures}次,停止录制") + break + else: + # 没有可用帧,短暂等待 + logger.debug(f"未获取到身体帧,等待中... - 会话ID: {self.current_session_id}") + time.sleep(0.01) + continue + else: + logger.warning(f"身体视频写入器未初始化 - 会话ID: {self.current_session_id}") + time.sleep(0.1) + continue + # 控制录制帧率 time.sleep(1/30) # 30 FPS except Exception as e: logger.error(f'身体录制线程异常: {e}') + finally: + logger.info(f"身体录制线程结束 - 会话ID: {self.current_session_id}") def _screen_recording_thread(self): """屏幕录制线程""" @@ -1703,67 +1593,95 @@ class DeviceManager: return video_files - def __del__(self): - """析构函数,确保资源被正确释放""" + def _save_frame_to_cache(self, frame, frame_type='camera'): + """保存帧到全局缓存""" try: - self.cleanup() + import time + with self.frame_cache_lock: + current_time = time.time() + + # 清理过期帧 + self._cleanup_expired_frames() + + # 如果缓存已满,移除最旧的帧 + if frame_type in self.frame_cache and len(self.frame_cache[frame_type]) >= self.max_cache_size: + oldest_key = min(self.frame_cache[frame_type].keys()) + del self.frame_cache[frame_type][oldest_key] + + # 初始化帧类型缓存 + if frame_type not in self.frame_cache: + self.frame_cache[frame_type] = {} + + # 保存帧(深拷贝避免引用问题) + frame_data = { + 'frame': frame.copy(), + 'timestamp': current_time, + 'frame_id': len(self.frame_cache[frame_type]) + } + + self.frame_cache[frame_type][current_time] = frame_data + # logger.debug(f'成功保存帧到缓存: {frame_type}, 缓存大小: {len(self.frame_cache[frame_type])}, 帧尺寸: {frame.shape}') + except Exception as e: - logger.error(f'析构函数清理资源失败: {e}') + logger.error(f'保存帧到缓存失败: {e}') - def cleanup(self): - """清理资源""" + def _get_latest_frame_from_cache(self, frame_type='camera'): + """从缓存获取最新帧""" try: - # 停止推流 - self.stop_streaming() - - # 停止录制 - if self.sync_recording: - self.stop_recording(self.current_session_id) - - - - # 使用锁保护摄像头释放 - with self.camera_lock: - if self.camera: - self.camera.release() - self.camera = None - logger.debug('摄像头资源已释放') - - if hasattr(self, 'video_writer') and self.video_writer: - self.video_writer.release() - - # 清理FemtoBolt录像写入器 - if hasattr(self, 'femtobolt_color_writer') and self.femtobolt_color_writer: - self.femtobolt_color_writer.release() - - if hasattr(self, 'femtobolt_depth_writer') and self.femtobolt_depth_writer: - self.femtobolt_depth_writer.release() - - # 清理同步录制写入器 - if self.feet_video_writer: - self.feet_video_writer.release() - if self.body_video_writer: - self.body_video_writer.release() - if self.screen_video_writer: - self.screen_video_writer.release() - - if self.femtobolt_camera: - self.femtobolt_camera = None - - # 清理帧缓存 - try: - with self.frame_cache_lock: - self.frame_cache.clear() - logger.debug('帧缓存已清理') - except Exception as cache_error: - logger.error(f'清理帧缓存失败: {cache_error}') - - logger.debug('设备资源已清理') - + import time + with self.frame_cache_lock: + # logger.debug(f'尝试从缓存获取帧: {frame_type}') + + if frame_type not in self.frame_cache: + logger.debug(f'缓存中不存在帧类型: {frame_type}, 可用类型: {list(self.frame_cache.keys())}') + return None, None + + if not self.frame_cache[frame_type]: + logger.debug(f'帧类型 {frame_type} 的缓存为空') + return None, None + + # 清理过期帧 + self._cleanup_expired_frames() + + if not self.frame_cache[frame_type]: + logger.debug(f'清理过期帧后,帧类型 {frame_type} 的缓存为空') + return None, None + + # 获取最新帧 + latest_timestamp = max(self.frame_cache[frame_type].keys()) + frame_data = self.frame_cache[frame_type][latest_timestamp] + + current_time = time.time() + frame_age = current_time - frame_data['timestamp'] + # logger.debug(f'成功获取最新帧: {frame_type}, 帧龄: {frame_age:.2f}秒, 缓存大小: {len(self.frame_cache[frame_type])}') + + return frame_data['frame'].copy(), frame_data['timestamp'] + except Exception as e: - logger.error(f'清理设备资源失败: {e}') - - + logger.error(f'从缓存获取帧失败: {e}') + return None, None + + def _cleanup_expired_frames(self): + """清理过期的缓存帧""" + try: + import time + current_time = time.time() + + for frame_type in list(self.frame_cache.keys()): + expired_keys = [] + for timestamp in self.frame_cache[frame_type].keys(): + if current_time - timestamp > self.cache_timeout: + expired_keys.append(timestamp) + + # 删除过期帧 + for key in expired_keys: + del self.frame_cache[frame_type][key] + + if expired_keys: + logger.debug(f'清理了 {len(expired_keys)} 个过期帧: {frame_type}') + + except Exception as e: + logger.error(f'清理过期帧失败: {e}') class MockIMUDevice: """模拟IMU设备""" @@ -1864,8 +1782,16 @@ class MockPressureDevice: try: import base64 from io import BytesIO + import matplotlib + matplotlib.use('Agg') # 设置非交互式后端,避免Tkinter错误 import matplotlib.pyplot as plt import matplotlib.patches as patches + import logging + + # 临时禁用PIL的调试日志 + pil_logger = logging.getLogger('PIL') + original_level = pil_logger.level + pil_logger.setLevel(logging.WARNING) # 创建图形 fig, ax = plt.subplots(1, 1, figsize=(6, 8)) @@ -1914,9 +1840,17 @@ class MockPressureDevice: image_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8') plt.close(fig) + # 恢复PIL的日志级别 + pil_logger.setLevel(original_level) + return f"data:image/png;base64,{image_base64}" except Exception as e: + # 确保在异常情况下也恢复PIL的日志级别 + try: + pil_logger.setLevel(original_level) + except: + pass logger.warning(f"生成压力图片失败: {e}") # 返回一个简单的占位符base64图片 return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==" @@ -1925,8 +1859,9 @@ class MockPressureDevice: class VideoStreamManager: """视频推流管理器""" - def __init__(self, socketio=None): + def __init__(self, socketio=None, device_manager=None): self.socketio = socketio + self.device_manager = device_manager self.device_index = None self.video_thread = None self.video_running = False @@ -2052,7 +1987,7 @@ class VideoStreamManager: def generate_test_frame(self, frame_count): """生成测试帧""" - width, height = 640, 480 + width, height = self.MAX_FRAME_SIZE # 创建黑色背景 frame = np.zeros((height, width, 3), dtype=np.uint8) @@ -2083,7 +2018,7 @@ class VideoStreamManager: error_count = 0 use_test_mode = False last_frame_time = time.time() - + width,height=self.MAX_FRAME_SIZE logger.debug(f'开始生成视频监控帧,设备号: {self.device_index}') try: @@ -2099,8 +2034,8 @@ class VideoStreamManager: 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) + cap.set(cv2.CAP_PROP_FRAME_WIDTH, width) + cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height) logger.debug('视频监控流已打开,开始推送帧(激进实时模式)') if self.socketio: self.socketio.emit('video_status', {'status': 'started', 'message': '使用视频监控视频源(激进实时模式)'}) @@ -2140,8 +2075,8 @@ class VideoStreamManager: 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) + cap.set(cv2.CAP_PROP_FRAME_WIDTH, width) + cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height) continue error_count = 0 # 重置错误计数 @@ -2177,6 +2112,15 @@ class VideoStreamManager: continue try: + # 保存帧到全局缓存 + if self.device_manager: + self.device_manager._save_frame_to_cache(frame.copy(), 'camera') + # 每1000帧记录一次缓存保存状态 + if frame_count % 1000 == 0: + logger.debug(f"视频推流已保存第 {frame_count} 帧到全局缓存") + else: + logger.warning("VideoStreamManager未关联DeviceManager,无法保存帧到缓存") + # 将帧放入队列进行异步处理 try: # 非阻塞方式放入队列,如果队列满了就丢弃旧帧 @@ -2463,35 +2407,28 @@ class VideoStreamManager: logger.error(f'足部压力数据采集失败: {e}') return None - def _capture_foot_image(self, data_dir: Path, device_manager) -> Optional[str]: + def _capture_foot_image(self, data_dir: Path, device_manager=None) -> Optional[str]: """采集足部监测视频截图(从全局缓存获取)""" try: image = None - # 检查是否有device_manager实例 - if device_manager is not None: - logger.info('正在从全局缓存获取最新图像...') - - # 从全局缓存获取最新帧 - frame, frame_timestamp = device_manager._get_latest_frame_from_cache('camera') - - if frame is not None: - # 使用缓存中的图像 - image = frame.copy() # 复制帧数据避免引用问题 - current_time = time.time() - frame_age = current_time - frame_timestamp if frame_timestamp else 0 - logger.info(f'成功获取缓存图像,尺寸: {image.shape},帧龄: {frame_age:.2f}秒') - else: - logger.warning('缓存中无可用图像,使用模拟图像') - image = np.zeros((480, 640, 3), dtype=np.uint8) - cv2.rectangle(image, (50, 50), (590, 430), (0, 255, 0), 2) - cv2.putText(image, 'No Cached Frame', (120, 250), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2) + # 直接使用self获取缓存帧 + logger.info('正在从全局缓存获取最新图像...') + + # 从全局缓存获取最新帧 + frame, frame_timestamp = device_manager._get_latest_frame_from_cache('camera') + #frame, frame_count = self.frame_queue.get(timeout=1) + if frame is not None: + # 使用缓存中的图像 + image = frame.copy() # 复制帧数据避免引用问题 + current_time = time.time() + frame_age = current_time - frame_timestamp if frame_timestamp else 0 + logger.info(f'成功获取缓存图像,尺寸: {image.shape},帧龄: {frame_age:.2f}秒') else: - logger.warning('设备管理器不可用,使用模拟图像') - # 使用模拟图像作为备用 + logger.warning('缓存中无可用图像,使用模拟图像') image = np.zeros((480, 640, 3), dtype=np.uint8) cv2.rectangle(image, (50, 50), (590, 430), (0, 255, 0), 2) - cv2.putText(image, 'Device Manager N/A', (100, 250), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2) + cv2.putText(image, 'No Cached Frame', (120, 250), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2) # 保存图片 image_path = data_dir / 'foot_image.jpg'