diff --git a/.gitignore b/.gitignore index 2e4afe99..d4991bb6 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,7 @@ build/ # 前端构建输出 frontend/src/renderer/dist/ frontend/src/renderer/dist-electron/ +backend/data/patients/ # 临时文件 *.tmp @@ -21413,3 +21414,4 @@ frontend/src/renderer/dist-electron/win-unpacked/resources/backend/BodyBalanceBa frontend/src/renderer/dist-electron/win-unpacked/resources/backend/BodyBalanceBackend/dll/smitsense/SMiTSenseUsb-F3.0d.dll frontend/src/renderer/dist-electron/win-unpacked/resources/backend/BodyBalanceBackend/dll/smitsense/SMiTSenseUsbWrapper.dll frontend/src/renderer/dist-electron/win-unpacked/resources/backend/BodyBalanceBackend/dll/smitsense/Wrapper.dll +backend/data/patients/202508060001/20250820102556/feet.mp4 diff --git a/backend/app.py b/backend/app.py index aaed2598..f0e701f2 100644 --- a/backend/app.py +++ b/backend/app.py @@ -28,6 +28,8 @@ sys.path.append(os.path.dirname(os.path.abspath(__file__))) # 导入自定义模块 from database import DatabaseManager from device_manager import DeviceManager, VideoStreamManager +from devices.screen_recorder import RecordingManager +from devices.camera_manager import CameraManager from utils import config as app_config # 确定日志文件路径 @@ -108,6 +110,8 @@ device_manager = None current_detection = None detection_thread = None video_stream_manager = None +recording_manager = None +camera_manager = None @@ -160,6 +164,14 @@ def init_app(): # 初始化设备管理器(不自动初始化设备) device_manager = DeviceManager(db_manager) + + # 初始化相机管理器 + global camera_manager, recording_manager + camera_manager = CameraManager() + + # 初始化录制管理器 + recording_manager = RecordingManager(camera_manager=camera_manager, db_manager=db_manager) + if socketio is not None: logger.info('SocketIO已启用') device_manager.set_socketio(socketio) # 设置WebSocket连接 @@ -701,7 +713,7 @@ def start_detection(): # 开始同步录制 recording_response = None try: - recording_response = device_manager.start_recording(session_id, patient_id) + recording_response = recording_manager.start_recording(session_id, patient_id) except Exception as rec_e: logger.error(f'开始同步录制失败: {rec_e}') @@ -716,9 +728,9 @@ def start_detection(): def stop_detection(session_id): """停止检测""" try: - if not db_manager or not device_manager: - logger.error('数据库管理器或设备管理器未初始化') - return jsonify({'success': False, 'error': '数据库管理器或设备管理器未初始化'}), 500 + if not db_manager or not recording_manager: + logger.error('数据库管理器或录制管理器未初始化') + return jsonify({'success': False, 'error': '数据库管理器或录制管理器未初始化'}), 500 if not session_id: logger.error('缺少会话ID') @@ -749,15 +761,17 @@ def stop_detection(session_id): 'success': False, 'message': f'视频数据解码失败: {str(e)}' }), 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}') - restrt=device_manager.stop_recording(session_id, video_data_base64=video_bytes) - logger.error(restrt) + # 使用新的录制管理器停止录制 + stop_result = recording_manager.stop_recording() + logger.info(f'录制停止结果: {stop_result}') + + # 处理前端传来的视频数据(如果需要保存) + if video_bytes: + # 可以在这里添加保存前端视频数据的逻辑 + logger.info(f'接收到前端视频数据,大小: {len(video_bytes)} 字节') + except Exception as rec_e: logger.error(f'停止同步录制失败: {rec_e}', exc_info=True) raise diff --git a/backend/device_manager.py b/backend/device_manager.py index 38f5b793..62b92d0f 100644 --- a/backend/device_manager.py +++ b/backend/device_manager.py @@ -1400,7 +1400,6 @@ class DeviceManager: 'recording_start_time': None, 'video_paths': { 'feet_video': None, - 'body_video': None, 'screen_video': None }, 'message': '' @@ -1457,10 +1456,10 @@ class DeviceManager: # 定义视频文件路径 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.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 # 更新数据库中的视频路径 @@ -1472,7 +1471,7 @@ 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) logger.debug(f'数据库视频路径更新成功 - 会话ID: {session_id}') @@ -1510,63 +1509,6 @@ class DeviceManager: # 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}') - - # 确保图像数据类型正确 - if frame1.dtype != np.uint8: - logger.warning(f'身体帧数据类型不是uint8: {frame1.dtype},将进行转换') - - 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}') - # 尝试使用默认分辨率重新初始化 - logger.info('尝试使用默认分辨率重新初始化身体视频写入器') - self.body_video_writer = cv2.VideoWriter( - body_video_path, fourcc, fps, (288, 576) # 默认分辨率 - ) - if self.body_video_writer.isOpened(): - logger.info(f'身体视频写入器使用默认分辨率初始化成功: {body_video_path}') - else: - logger.error(f'身体视频写入器使用默认分辨率初始化仍然失败: {body_video_path}') - else: - logger.warning('无法从缓存获取FemtoBolt帧数据,使用默认分辨率初始化身体视频写入器') - self.body_video_writer = cv2.VideoWriter( - body_video_path, fourcc, fps, (288, 576) # 默认分辨率 - ) - if self.body_video_writer.isOpened(): - logger.info(f'身体视频写入器使用默认分辨率初始化成功: {body_video_path}') - else: - logger.error(f'身体视频写入器使用默认分辨率初始化失败: {body_video_path}') - # FemtoBolt默认分辨率 - # 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( # screen_video_path, fourcc, fps, (1920, 1080) @@ -1583,14 +1525,7 @@ class DeviceManager: name='FeetRecordingThread' ) self.feet_recording_thread.start() - - # if self.body_video_writer: - # self.body_recording_thread = threading.Thread( - # target=self._body_recording_thread, - # daemon=True, - # name='BodyRecordingThread' - # ) - # self.body_recording_thread.start() + # #屏幕录制 # if self.screen_video_writer: # self.screen_recording_thread = threading.Thread( diff --git a/backend/devices/camera_manager.py b/backend/devices/camera_manager.py index e6fded32..ad6326ed 100644 --- a/backend/devices/camera_manager.py +++ b/backend/devices/camera_manager.py @@ -81,6 +81,12 @@ class CameraManager(BaseDevice): 'actual_fps': 0, 'dropped_frames': 0 } + + # 全局帧缓存(用于录制) + self.global_frame_cache = {} + self.frame_cache_lock = threading.Lock() + self.max_cache_size = 10 + self.cache_timeout = 5.0 # 5秒超时 # OpenCV优化开关 try: @@ -328,12 +334,15 @@ class CameraManager(BaseDevice): # 重置丢帧计数 self.dropped_frames = 0 + # 保存原始帧到全局缓存(用于录制) + self._save_frame_to_cache(frame, 'camera') + # 处理帧(降采样以优化传输负载) processed_frame = self._process_frame(frame) # 缓存帧(不复制,减少内存占用) - # self.last_frame = processed_frame - # self.frame_cache.append(processed_frame) + self.last_frame = processed_frame + self.frame_cache.append(processed_frame) # 发送帧数据 self._send_frame_data(processed_frame) @@ -568,8 +577,89 @@ class CameraManager(BaseDevice): self.frame_cache.clear() self.last_frame = None + # 清理全局帧缓存 + with self.frame_cache_lock: + self.global_frame_cache.clear() + super().cleanup() self.logger.info("相机资源清理完成") except Exception as e: self.logger.error(f"清理相机资源失败: {e}") + + 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'): + """从缓存获取最新帧""" + try: + with self.frame_cache_lock: + if frame_type not in self.global_frame_cache: + self.logger.debug(f'缓存中不存在帧类型: {frame_type}, 可用类型: {list(self.global_frame_cache.keys())}') + return None, None + + if not self.global_frame_cache[frame_type]: + self.logger.debug(f'帧类型 {frame_type} 的缓存为空') + return None, None + + # 清理过期帧 + self._cleanup_expired_frames() + + if not self.global_frame_cache[frame_type]: + self.logger.debug(f'清理过期帧后,帧类型 {frame_type} 的缓存为空') + return None, None + + # 获取最新帧 + latest_timestamp = max(self.global_frame_cache[frame_type].keys()) + frame_data = self.global_frame_cache[frame_type][latest_timestamp] + + return frame_data['frame'].copy(), frame_data['timestamp'] + + except Exception as e: + self.logger.error(f'从缓存获取帧失败: {e}') + return None, None + + def _cleanup_expired_frames(self): + """清理过期的缓存帧""" + try: + current_time = time.time() + + for frame_type in list(self.global_frame_cache.keys()): + expired_keys = [] + for timestamp in self.global_frame_cache[frame_type].keys(): + if current_time - timestamp > self.cache_timeout: + expired_keys.append(timestamp) + + # 删除过期帧 + for key in expired_keys: + del self.global_frame_cache[frame_type][key] + + except Exception as e: + self.logger.error(f'清理过期帧失败: {e}') diff --git a/backend/devices/screen_recorder.py b/backend/devices/screen_recorder.py index 478c839e..7e3479e4 100644 --- a/backend/devices/screen_recorder.py +++ b/backend/devices/screen_recorder.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ -屏幕录制工具 -支持录制当前屏幕并保存为视频文件 +综合录制管理器 +支持屏幕录制和足部视频录制 """ import cv2 @@ -12,286 +12,500 @@ import threading import time from datetime import datetime import os +import logging +from typing import Optional, Dict, Any -class ScreenRecorder: - def __init__(self, output_dir="recordings", fps=20, quality=80, region=None): +try: + from .camera_manager import CameraManager +except ImportError: + from camera_manager import CameraManager + +class RecordingManager: + def __init__(self, camera_manager: Optional[CameraManager] = None, db_manager=None): """ - 初始化屏幕录制器 + 初始化录制管理器 Args: - output_dir (str): 输出目录 - fps (int): 帧率 - quality (int): 视频质量 (1-100) - region (tuple): 录制区域 (x, y, width, height),None表示全屏录制 + camera_manager: 相机管理器实例 + db_manager: 数据库管理器实例 """ - self.output_dir = output_dir - self.fps = fps - self.quality = quality - self.recording = False - self.paused = False - self.video_writer = None - self.thread = None - self.region = region + self.camera_manager = camera_manager + self.db_manager = db_manager - # 创建输出目录 - if not os.path.exists(output_dir): - os.makedirs(output_dir) + # 录制状态 + self.sync_recording = False + self.recording_stop_event = threading.Event() - # 获取屏幕尺寸 + # 会话信息 + self.current_session_id = None + self.current_patient_id = None + self.recording_start_time = None + + # 视频写入器 + self.feet_video_writer = None + self.screen_video_writer = None + + # 录制线程 + self.feet_recording_thread = None + self.screen_recording_thread = None + + # 屏幕录制参数 + self.screen_fps = 20 + self.screen_region = None self.screen_size = pyautogui.size() - print(f"屏幕尺寸: {self.screen_size}") - # 设置录制区域 - if self.region: - x, y, width, height = self.region - # 确保区域在屏幕范围内 - x = max(0, min(x, self.screen_size[0] - 1)) - y = max(0, min(y, self.screen_size[1] - 1)) - width = min(width, self.screen_size[0] - x) - height = min(height, self.screen_size[1] - y) - self.region = (x, y, width, height) - self.record_size = (width, height) - print(f"录制区域: {self.region}") - else: - self.record_size = self.screen_size - print("录制模式: 全屏录制") + # 视频参数 + self.MAX_FRAME_SIZE = (1280, 720) # 最大帧尺寸 + + # 日志 + self.logger = logging.getLogger(__name__) + + self.logger.info("录制管理器初始化完成") - def start_recording(self, filename=None): + def start_recording(self, session_id: str, patient_id: str) -> Dict[str, Any]: """ - 开始录制 + 启动同步录制 Args: - filename (str): 输出文件名,如果为None则自动生成 - """ - if self.recording: - print("录制已在进行中") - return - - if filename is None: - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - filename = f"screen_record_{timestamp}.mp4" - - self.output_path = os.path.join(self.output_dir, filename) - - # 设置视频编码器 - fourcc = cv2.VideoWriter_fourcc(*'mp4v') - self.video_writer = cv2.VideoWriter( - self.output_path, - fourcc, - self.fps, - self.record_size - ) - - self.recording = True - self.paused = False - - # 在新线程中开始录制 - self.thread = threading.Thread(target=self._record_loop) - self.thread.daemon = True - self.thread.start() - - print(f"开始录制: {self.output_path}") - - def _record_loop(self): - """ - 录制循环 - """ - while self.recording: - if not self.paused: - # 截取屏幕 - if self.region: - # 区域录制 - x, y, width, height = self.region - screenshot = pyautogui.screenshot(region=(x, y, width, height)) - else: - # 全屏录制 - screenshot = pyautogui.screenshot() - - # 转换为numpy数组 - frame = np.array(screenshot) - - # 转换颜色格式 (RGB -> BGR) - frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) - - # 写入视频文件 - self.video_writer.write(frame) + session_id: 检测会话ID + patient_id: 患者ID - # 控制帧率 - time.sleep(1.0 / self.fps) - - def pause_recording(self): + Returns: + Dict: 录制启动状态和信息 """ - 暂停录制 - """ - if not self.recording: - print("当前没有在录制") - return + result = { + 'success': False, + 'session_id': session_id, + 'patient_id': patient_id, + 'recording_start_time': None, + 'video_paths': { + 'feet_video': None, + 'screen_video': None + }, + 'message': '' + } - self.paused = not self.paused - status = "暂停" if self.paused else "继续" - print(f"录制{status}") + try: + # 检查是否已在录制 + if self.sync_recording: + result['message'] = f'已在录制中,当前会话ID: {self.current_session_id}' + return result + + # 设置录制参数 + self.current_session_id = session_id + self.current_patient_id = patient_id + self.recording_start_time = datetime.now() + + # 创建存储目录 + base_path = os.path.join('data', 'patients', patient_id, session_id) + try: + os.makedirs(base_path, exist_ok=True) + self.logger.info(f'录制目录创建成功: {base_path}') + + # 设置目录权限 + self._set_directory_permissions(base_path) + + except Exception as dir_error: + self.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') + screen_video_path = os.path.join(base_path, 'screen.mp4') + + result['video_paths']['feet_video'] = feet_video_path + result['video_paths']['screen_video'] = screen_video_path + + # 更新数据库中的视频路径 + if self.db_manager: + try: + # 更新会话状态为录制中 + if not self.db_manager.update_session_status(session_id, 'recording'): + self.logger.error(f'更新会话状态为录制中失败 - 会话ID: {session_id}') + + # 更新视频文件路径 + self.db_manager.update_session_normal_video_path(session_id, feet_video_path) + self.db_manager.update_session_screen_video_path(session_id, screen_video_path) + + self.logger.debug(f'数据库视频路径更新成功 - 会话ID: {session_id}') + except Exception as db_error: + self.logger.error(f'更新数据库视频路径失败: {db_error}') + + # 视频编码参数 + fourcc = cv2.VideoWriter_fourcc(*'mp4v') + fps = 30 + + # 初始化足部视频写入器 + if self.camera_manager and self.camera_manager.is_connected: + target_width, target_height = self.MAX_FRAME_SIZE + self.feet_video_writer = cv2.VideoWriter( + feet_video_path, fourcc, fps, (target_width, target_height) + ) + + if self.feet_video_writer.isOpened(): + self.logger.info(f'脚部视频写入器初始化成功: {feet_video_path}') + else: + self.logger.error(f'脚部视频写入器初始化失败: {feet_video_path}') + else: + self.logger.warning('相机设备未启用,跳过脚部视频写入器初始化') + + # 初始化屏幕录制写入器 + record_size = self.screen_region[2:4] if self.screen_region else self.screen_size + self.screen_video_writer = cv2.VideoWriter( + screen_video_path, fourcc, self.screen_fps, record_size + ) + + if self.screen_video_writer.isOpened(): + self.logger.info(f'屏幕视频写入器初始化成功: {screen_video_path}') + else: + self.logger.error(f'屏幕视频写入器初始化失败: {screen_video_path}') + + # 重置停止事件 + self.recording_stop_event.clear() + self.sync_recording = True + + # 启动录制线程 + if self.feet_video_writer: + self.feet_recording_thread = threading.Thread( + target=self._feet_recording_thread, + daemon=True, + name='FeetRecordingThread' + ) + self.feet_recording_thread.start() + + if self.screen_video_writer: + self.screen_recording_thread = threading.Thread( + target=self._screen_recording_thread, + daemon=True, + name='ScreenRecordingThread' + ) + self.screen_recording_thread.start() + + result['success'] = True + result['recording_start_time'] = self.recording_start_time.isoformat() + result['message'] = '同步录制已启动' + + self.logger.info(f'同步录制已启动 - 会话ID: {session_id}, 患者ID: {patient_id}') + + except Exception as e: + self.logger.error(f'启动同步录制失败: {e}') + result['message'] = f'启动录制失败: {str(e)}' + # 清理已创建的写入器 + self._cleanup_video_writers() + + return result - def stop_recording(self): + def stop_recording(self, session_id: str = None) -> Dict[str, Any]: """ 停止录制 - """ - if not self.recording: - print("当前没有在录制") - return - - self.recording = False - self.paused = False - - # 等待录制线程结束 - if self.thread: - self.thread.join() - - # 释放视频写入器 - if self.video_writer: - self.video_writer.release() - self.video_writer = None - - print(f"录制完成: {self.output_path}") - - def set_region(self, region): - """ - 设置录制区域 Args: - region (tuple): 录制区域 (x, y, width, height),None表示全屏录制 + session_id: 会话ID,用于验证是否为当前录制会话 + + Returns: + Dict: 停止录制的结果 """ - if self.recording: - print("录制进行中,无法更改区域设置") + result = { + 'success': False, + 'session_id': self.current_session_id, + 'message': '' + } + + try: + # 验证会话ID + if session_id and session_id != self.current_session_id: + result['message'] = f'会话ID不匹配: 期望 {self.current_session_id}, 收到 {session_id}' + return result + + if not self.sync_recording: + result['message'] = '当前没有进行录制' + return result + + # 设置停止标志 + self.sync_recording = False + self.recording_stop_event.set() + + # 等待录制线程结束 + if self.feet_recording_thread and self.feet_recording_thread.is_alive(): + self.feet_recording_thread.join(timeout=5.0) + + if self.screen_recording_thread and self.screen_recording_thread.is_alive(): + self.screen_recording_thread.join(timeout=5.0) + + # 清理视频写入器 + self._cleanup_video_writers() + + # 更新数据库状态 + if self.db_manager and self.current_session_id: + try: + self.db_manager.update_session_status(self.current_session_id, 'completed') + self.logger.info(f'会话状态已更新为完成 - 会话ID: {self.current_session_id}') + except Exception as db_error: + self.logger.error(f'更新数据库状态失败: {db_error}') + + result['success'] = True + result['message'] = '录制已停止' + + self.logger.info(f'录制已停止 - 会话ID: {self.current_session_id}') + + # 重置会话信息 + self.current_session_id = None + self.current_patient_id = None + self.recording_start_time = None + + except Exception as e: + self.logger.error(f'停止录制失败: {e}') + result['message'] = f'停止录制失败: {str(e)}' + + return result + + def _feet_recording_thread(self): + """足部视频录制线程""" + consecutive_failures = 0 + max_consecutive_failures = 10 + recording_frame_count = 0 + + self.logger.info(f"足部录制线程已启动 - 会话ID: {self.current_session_id}") + self.logger.info(f"视频写入器状态: {self.feet_video_writer.isOpened() if self.feet_video_writer else 'None'}") + + try: + # 使用与屏幕录制相同的帧率控制 + target_fps = 30 # 目标帧率 + frame_interval = 1.0 / target_fps + last_frame_time = time.time() + + while self.sync_recording and not self.recording_stop_event.is_set(): + current_time = time.time() + + # 检查是否到了下一帧的时间 + if current_time - last_frame_time >= frame_interval: + if self.feet_video_writer: + # 从相机管理器的全局缓存获取最新帧 + frame, frame_timestamp = self.camera_manager._get_latest_frame_from_cache('camera') + + if frame is not None: + self.logger.debug(f"成功获取帧 - 尺寸: {frame.shape}, 数据类型: {frame.dtype}, 时间戳: {frame_timestamp}") + + # 检查视频写入器状态 + if not self.feet_video_writer.isOpened(): + self.logger.error(f"脚部视频写入器已关闭,无法写入帧 - 会话ID: {self.current_session_id}") + break + + try: + # 调整帧尺寸到目标大小 + resized_frame = cv2.resize(frame, self.MAX_FRAME_SIZE) + + # 写入录制文件 + write_success = self.feet_video_writer.write(resized_frame) + + if write_success is False: + self.logger.error(f"视频帧写入返回False - 可能写入失败") + consecutive_failures += 1 + else: + consecutive_failures = 0 + recording_frame_count += 1 + + except Exception as write_error: + self.logger.error(f"写入脚部视频帧异常: {write_error}") + consecutive_failures += 1 + if consecutive_failures >= 10: + self.logger.error("连续写入失败次数过多,停止录制") + break + else: + # 如果没有获取到帧,写入上一帧或黑色帧来保持帧率 + consecutive_failures += 1 + if consecutive_failures <= 3: + self.logger.warning(f"录制线程无法从缓存获取帧 (连续失败{consecutive_failures}次)") + elif consecutive_failures == max_consecutive_failures: + self.logger.error(f"录制线程连续失败{max_consecutive_failures}次,可能缓存无数据或推流已停止") + + last_frame_time = current_time + else: + self.logger.error("足部视频写入器未初始化") + break + + # 短暂休眠避免CPU占用过高 + time.sleep(0.01) + + # 检查连续失败情况 + if consecutive_failures >= max_consecutive_failures: + self.logger.error(f"连续失败次数达到上限({max_consecutive_failures}),停止录制") + break + + except Exception as e: + self.logger.error(f'足部录制线程异常: {e}') + finally: + self.logger.info(f"足部录制线程已结束 - 会话ID: {self.current_session_id}, 总录制帧数: {recording_frame_count}") + # 确保视频写入器被正确关闭 + if self.feet_video_writer: + self.feet_video_writer.release() + self.feet_video_writer = None + self.logger.debug("足部视频写入器已释放") + + def _screen_recording_thread(self): + """屏幕录制线程""" + self.logger.info(f"屏幕录制线程已启动 - 会话ID: {self.current_session_id}") + recording_frame_count = 0 + + try: + # 使用与足部录制相同的帧率控制 + target_fps = 30 # 目标帧率 + frame_interval = 1.0 / target_fps + last_frame_time = time.time() + + while self.sync_recording and not self.recording_stop_event.is_set(): + current_time = time.time() + + # 检查是否到了下一帧的时间 + if current_time - last_frame_time >= frame_interval: + try: + # 截取屏幕 + if self.screen_region: + x, y, width, height = self.screen_region + screenshot = pyautogui.screenshot(region=(x, y, width, height)) + else: + screenshot = pyautogui.screenshot() + + # 转换为numpy数组 + frame = np.array(screenshot) + + # 转换颜色格式 (RGB -> BGR) + frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) + + # 写入视频文件 + if self.screen_video_writer and self.screen_video_writer.isOpened(): + self.screen_video_writer.write(frame) + recording_frame_count += 1 + + last_frame_time = current_time + + except Exception as e: + self.logger.error(f"屏幕录制异常: {e}") + + # 短暂休眠避免CPU占用过高 + time.sleep(0.01) + + except Exception as e: + self.logger.error(f'屏幕录制线程异常: {e}') + finally: + self.logger.info(f"屏幕录制线程已结束 - 会话ID: {self.current_session_id}, 总录制帧数: {recording_frame_count}") + # 确保视频写入器被正确关闭 + if self.screen_video_writer: + self.screen_video_writer.release() + self.screen_video_writer = None + self.logger.debug("屏幕视频写入器已释放") + + def _cleanup_video_writers(self): + """清理视频写入器""" + try: + if self.feet_video_writer: + self.feet_video_writer.release() + self.feet_video_writer = None + self.logger.debug("足部视频写入器已清理") + + if self.screen_video_writer: + self.screen_video_writer.release() + self.screen_video_writer = None + self.logger.debug("屏幕视频写入器已清理") + + except Exception as e: + self.logger.error(f"清理视频写入器失败: {e}") + + def _set_directory_permissions(self, path): + """设置目录权限""" + try: + import subprocess + import platform + + if platform.system() == 'Windows': + try: + # 为Users用户组授予完全控制权限 + subprocess.run([ + 'icacls', path, '/grant', 'Users:(OI)(CI)F' + ], check=True, capture_output=True, text=True) + + # 为Everyone用户组授予完全控制权限 + subprocess.run([ + 'icacls', path, '/grant', 'Everyone:(OI)(CI)F' + ], check=True, capture_output=True, text=True) + + self.logger.info(f"已设置Windows目录权限(Users和Everyone完全控制): {path}") + except subprocess.CalledProcessError as icacls_error: + self.logger.warning(f"Windows权限设置失败: {icacls_error}") + else: + self.logger.info(f"已设置目录权限为777: {path}") + + except Exception as perm_error: + self.logger.warning(f"设置目录权限失败: {perm_error},但目录创建成功") + + def set_screen_region(self, region): + """设置屏幕录制区域""" + if self.sync_recording: + self.logger.warning("录制进行中,无法更改区域设置") return False - self.region = region + self.screen_region = region - # 重新计算录制区域 - if self.region: - x, y, width, height = self.region + if self.screen_region: + x, y, width, height = self.screen_region # 确保区域在屏幕范围内 x = max(0, min(x, self.screen_size[0] - 1)) y = max(0, min(y, self.screen_size[1] - 1)) width = min(width, self.screen_size[0] - x) height = min(height, self.screen_size[1] - y) - self.region = (x, y, width, height) - self.record_size = (width, height) - print(f"录制区域已设置: {self.region}") + self.screen_region = (x, y, width, height) + self.logger.info(f"录制区域已设置: {self.screen_region}") else: - self.record_size = self.screen_size - print("录制模式已设置: 全屏录制") + self.logger.info("录制模式已设置: 全屏录制") return True def get_status(self): - """ - 获取录制状态 - - Returns: - dict: 包含录制状态信息的字典 - """ + """获取录制状态""" return { - 'recording': self.recording, - 'paused': self.paused, - 'output_path': getattr(self, 'output_path', None), + 'recording': self.sync_recording, + 'session_id': self.current_session_id, + 'patient_id': self.current_patient_id, + 'recording_start_time': self.recording_start_time.isoformat() if self.recording_start_time else None, 'screen_size': self.screen_size, - 'record_size': self.record_size, - 'region': self.region, - 'fps': self.fps + 'screen_region': self.screen_region, + 'screen_fps': self.screen_fps, + 'feet_writer_active': self.feet_video_writer is not None and self.feet_video_writer.isOpened() if self.feet_video_writer else False, + 'screen_writer_active': self.screen_video_writer is not None and self.screen_video_writer.isOpened() if self.screen_video_writer else False } -def main(): - """ - 主函数 - 命令行界面 - """ - recorder = ScreenRecorder() - - print("\n=== 屏幕录制工具 ===") - print("命令:") - print(" start [filename] - 开始录制") - print(" pause - 暂停/恢复录制") - print(" stop - 停止录制") - print(" status - 查看状态") - print(" region x y w h - 设置录制区域 (x, y, 宽度, 高度)") - print(" fullscreen - 设置全屏录制") - print(" center w h - 设置居中区域录制 (宽度, 高度)") - print(" quit - 退出程序") - print("\n输入命令:") - - while True: - try: - command = input("> ").strip().split() - - if not command: - continue - - cmd = command[0].lower() - - if cmd == "start": - filename = command[1] if len(command) > 1 else None - recorder.start_recording(filename) - - elif cmd == "pause": - recorder.pause_recording() - - elif cmd == "stop": - recorder.stop_recording() - - elif cmd == "status": - status = recorder.get_status() - print(f"录制状态: {'进行中' if status['recording'] else '已停止'}") - print(f"暂停状态: {'是' if status['paused'] else '否'}") - print(f"输出文件: {status['output_path']}") - print(f"屏幕尺寸: {status['screen_size']}") - print(f"录制尺寸: {status['record_size']}") - print(f"录制区域: {status['region'] if status['region'] else '全屏'}") - print(f"帧率: {status['fps']} FPS") - - elif cmd == "region": - if len(command) != 5: - print("用法: region x y width height") - continue - try: - x, y, w, h = map(int, command[1:5]) - if recorder.set_region((x, y, w, h)): - print(f"录制区域已设置: ({x}, {y}, {w}, {h})") - except ValueError: - print("请输入有效的数字") - - elif cmd == "fullscreen": - if recorder.set_region(None): - print("已设置为全屏录制") - - elif cmd == "center": - if len(command) != 3: - print("用法: center width height") - continue - try: - w, h = map(int, command[1:3]) - screen_w, screen_h = recorder.screen_size - x = (screen_w - w) // 2 - y = (screen_h - h) // 2 - if recorder.set_region((x, y, w, h)): - print(f"居中录制区域已设置: ({x}, {y}, {w}, {h})") - except ValueError: - print("请输入有效的数字") - - elif cmd == "quit": - if recorder.recording: - recorder.stop_recording() - print("程序退出") - break - - else: - print("未知命令") - - except KeyboardInterrupt: - if recorder.recording: - recorder.stop_recording() - print("\n程序退出") - break - except Exception as e: - print(f"错误: {e}") -if __name__ == "__main__": - main() \ No newline at end of file +# 保持向后兼容的ScreenRecorder类 +class ScreenRecorder: + def __init__(self, output_dir="recordings", fps=20, quality=80, region=None): + """向后兼容的屏幕录制器""" + self.recording_manager = RecordingManager() + self.recording_manager.screen_fps = fps + self.recording_manager.set_screen_region(region) + self.output_dir = output_dir + + # 创建输出目录 + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + def start_recording(self, filename=None): + """开始录制""" + if filename is None: + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + filename = f"screen_record_{timestamp}" + + # 使用文件名作为会话ID + session_id = filename + patient_id = "default" + + return self.recording_manager.start_recording(session_id, patient_id) + + def stop_recording(self): + """停止录制""" + return self.recording_manager.stop_recording() + + def get_status(self): + """获取状态""" + return self.recording_manager.get_status() \ No newline at end of file diff --git a/backend/devices/utils/config.ini b/backend/devices/utils/config.ini index 71c7e20d..5cf905ba 100644 --- a/backend/devices/utils/config.ini +++ b/backend/devices/utils/config.ini @@ -15,7 +15,7 @@ backup_interval = 24 max_backups = 7 [CAMERA] -device_index = 0 +device_index = 3 width = 1280 height = 720 fps = 30 @@ -30,7 +30,7 @@ depth_range_max = 1700 [DEVICES] imu_device_type = real imu_port = COM3 -imu_baudrate = 115200 +imu_baudrate = 9600 pressure_device_type = real pressure_use_mock = False pressure_port = COM5 diff --git a/backend/main.py b/backend/main.py index 7d01d907..ce83025e 100644 --- a/backend/main.py +++ b/backend/main.py @@ -25,29 +25,36 @@ sys.path.append(os.path.dirname(os.path.abspath(__file__))) # 导入模块 from database import DatabaseManager from utils import config as app_config - -# 导入设备管理器 -try: - from devices.camera_manager import CameraManager - from devices.imu_manager import IMUManager - from devices.pressure_manager import PressureManager - from devices.femtobolt_manager import FemtoBoltManager - from devices.device_coordinator import DeviceCoordinator - from devices.utils.config_manager import ConfigManager -except ImportError: - # 如果上面的导入失败,尝试直接导入 - from camera_manager import CameraManager - import imu_manager - import pressure_manager - import femtobolt_manager - import device_coordinator - from utils import config_manager +from devices.camera_manager import CameraManager +from devices.imu_manager import IMUManager +from devices.pressure_manager import PressureManager +from devices.femtobolt_manager import FemtoBoltManager +from devices.device_coordinator import DeviceCoordinator +from devices.screen_recorder import RecordingManager +from devices.utils.config_manager import ConfigManager +# # 导入设备管理器 +# try: +# from devices.camera_manager import CameraManager +# from devices.imu_manager import IMUManager +# from devices.pressure_manager import PressureManager +# from devices.femtobolt_manager import FemtoBoltManager +# from devices.device_coordinator import DeviceCoordinator +# from devices.screen_recorder import RecordingManager +# from devices.utils.config_manager import ConfigManager +# except ImportError: +# # 如果上面的导入失败,尝试直接导入 +# # from camera_manager import CameraManager +# import imu_manager +# import pressure_manager +# import femtobolt_manager +# import device_coordinator +# from utils import config_manager - IMUManager = imu_manager.IMUManager - PressureManager = pressure_manager.PressureManager - FemtoBoltManager = femtobolt_manager.FemtoBoltManager - DeviceCoordinator = device_coordinator.DeviceCoordinator - ConfigManager = config_manager.ConfigManager +# IMUManager = imu_manager.IMUManager +# PressureManager = pressure_manager.PressureManager +# FemtoBoltManager = femtobolt_manager.FemtoBoltManager +# DeviceCoordinator = device_coordinator.DeviceCoordinator +# ConfigManager = config_manager.ConfigManager class AppServer: @@ -222,6 +229,14 @@ class AppServer: self.device_coordinator = DeviceCoordinator(self.socketio) self.logger.info('设备协调器初始化完成') + # 初始化录制管理器 + self.logger.info('正在初始化录制管理器...') + self.recording_manager = RecordingManager( + camera_manager=self.device_managers['camera'], + db_manager=self.db_manager + ) + self.logger.info('录制管理器初始化完成') + # 启动Flask应用 host = self.host port = self.port @@ -891,7 +906,7 @@ class AppServer: # 开始同步录制 recording_response = None try: - recording_response = self.device_coordinator.start_recording(session_id, patient_id) + recording_response = self.recording_manager.start_recording(session_id, patient_id) except Exception as rec_e: self.logger.error(f'开始同步录制失败: {rec_e}') @@ -937,8 +952,8 @@ class AppServer: }), 400 # 停止同步录制,传递视频数据 try: - restrt = self.device_coordinator.stop_recording(session_id, video_data_base64=video_bytes) - self.logger.error(restrt) + restrt = self.recording_manager.stop_recording(session_id) + self.logger.info(f'停止录制结果: {restrt}') except Exception as rec_e: self.logger.error(f'停止同步录制失败: {rec_e}', exc_info=True) raise diff --git a/frontend/src/renderer/src/views/Detection.vue b/frontend/src/renderer/src/views/Detection.vue index 7ac8b691..a426526d 100644 --- a/frontend/src/renderer/src/views/Detection.vue +++ b/frontend/src/renderer/src/views/Detection.vue @@ -1621,28 +1621,6 @@ async function saveDetectionData() { try { screenshotLoading.value = true - - // 显示进度提示 - ElMessage.info('正在生成截图...') - - // 获取要截图的DOM元素 - const element = document.getElementById('detectare') - if (!element) { - throw new Error('未找到截图区域') - } - - // 使用html2canvas进行截图 - const canvas = await html2canvas(element, { - useCORS: true, - allowTaint: true, - backgroundColor: '#ffffff', - scale: 1, - logging: false - }) - - // 将canvas转换为base64 - const base64Image = canvas.toDataURL('image/png') - // 显示保存进度 ElMessage.info('正在保存截图...') @@ -1656,7 +1634,6 @@ async function saveDetectionData() { patientId: patientInfo.value.id, patientName: patientInfo.value.name, sessionId: patientInfo.value.sessionId, - imageData: base64Image, head_pose: {}, body_pose: {}, foot_data: {} @@ -1664,7 +1641,7 @@ async function saveDetectionData() { // 显示成功消息和文件路径 ElMessage.success({ - message: `截图保存成功!文件路径: ${result.filepath}`, + message: `截图保存成功!`, duration: 5000 })