949 lines
42 KiB
Python
949 lines
42 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
综合录制管理器
|
||
支持屏幕录制和足部视频录制
|
||
"""
|
||
|
||
import cv2
|
||
import numpy as np
|
||
import pyautogui
|
||
import threading
|
||
import time
|
||
from datetime import datetime
|
||
import os
|
||
import logging
|
||
import json
|
||
import base64
|
||
from pathlib import Path
|
||
from typing import Optional, Dict, Any, List
|
||
import sys
|
||
# 移除psutil导入,不再需要性能监控
|
||
import gc
|
||
|
||
try:
|
||
from .camera_manager import CameraManager
|
||
from .femtobolt_manager import FemtoBoltManager
|
||
from .pressure_manager import PressureManager
|
||
except ImportError:
|
||
from camera_manager import CameraManager
|
||
from femtobolt_manager import FemtoBoltManager
|
||
from pressure_manager import PressureManager
|
||
|
||
class RecordingManager:
|
||
def __init__(self, camera_manager: Optional[CameraManager] = None, db_manager=None,
|
||
femtobolt_manager: Optional[FemtoBoltManager] = None,
|
||
pressure_manager: Optional[PressureManager] = None):
|
||
"""
|
||
初始化录制管理器
|
||
|
||
Args:
|
||
camera_manager: 相机管理器实例
|
||
db_manager: 数据库管理器实例
|
||
femtobolt_manager: FemtoBolt深度相机管理器实例
|
||
pressure_manager: 压力传感器管理器实例
|
||
"""
|
||
self.camera_manager = camera_manager
|
||
self.db_manager = db_manager
|
||
self.femtobolt_manager = femtobolt_manager
|
||
self.pressure_manager = pressure_manager
|
||
|
||
# 录制状态
|
||
self.sync_recording = False
|
||
self.is_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.femtobolt_video_writer = None
|
||
|
||
# 录制线程
|
||
self.feet_recording_thread = None
|
||
self.screen_recording_thread = None
|
||
self.femtobolt_recording_thread = None
|
||
|
||
# 独立的录制参数配置
|
||
self.screen_fps = 25 # 屏幕录制帧率
|
||
self.camera_fps = 20 # 相机录制帧率
|
||
self.femtobolt_fps = 15 # FemtoBolt录制帧率
|
||
|
||
# 录制区域
|
||
self.screen_region = None
|
||
self.camera_region = None
|
||
self.femtobolt_region = None
|
||
|
||
# 屏幕尺寸
|
||
self.screen_size = pyautogui.size()
|
||
|
||
# 输出目录
|
||
self.screen_output_dir = None
|
||
self.camera_output_dir = None
|
||
self.femtobolt_output_dir = None
|
||
|
||
# 视频参数
|
||
self.MAX_FRAME_SIZE = (1280, 720) # 最大帧尺寸
|
||
|
||
# 独立的帧率控制参数
|
||
self.screen_current_fps = self.screen_fps
|
||
self.camera_current_fps = self.camera_fps
|
||
self.femtobolt_current_fps = self.femtobolt_fps
|
||
|
||
# 区域大小阈值配置 - 根据实际录制场景优化
|
||
self.SMALL_REGION_THRESHOLD = 400 * 300 # 小区域阈值 (120,000像素)
|
||
self.MEDIUM_REGION_THRESHOLD = 800 * 600 # 中等区域阈值 (480,000像素)
|
||
self.LARGE_REGION_THRESHOLD = 1600 * 900 # 大区域阈值 (1,440,000像素)
|
||
|
||
# 基于区域大小的帧率配置 - 大幅降低帧率以减小文件大小
|
||
self.fps_config = {
|
||
'small': {'screen': 12, 'camera': 25, 'femtobolt': 20}, # 小区域:低帧率
|
||
'medium': {'screen': 10, 'camera': 22, 'femtobolt': 18}, # 中等区域:更低帧率
|
||
'large': {'screen': 8, 'camera': 18, 'femtobolt': 15}, # 大区域:很低帧率
|
||
'xlarge': {'screen': 6, 'camera': 15, 'femtobolt': 12} # 超大区域:极低帧率
|
||
}
|
||
|
||
# 移除CPU监控和性能优化参数,使用固定帧率控制
|
||
|
||
# 录制同步控制
|
||
self.recording_sync_barrier = None # 同步屏障
|
||
self.recording_threads = {} # 录制线程字典
|
||
self.recording_start_sync = threading.Event() # 录制开始同步事件
|
||
self.global_recording_start_time = None # 全局录制开始时间
|
||
|
||
# 日志
|
||
self.logger = logging.getLogger(__name__)
|
||
|
||
self.logger.info("录制管理器初始化完成")
|
||
|
||
# 移除系统性能检查方法
|
||
|
||
def _calculate_region_size_category(self, region):
|
||
"""
|
||
根据录制区域大小计算区域类别
|
||
|
||
Args:
|
||
region: 录制区域 (x, y, width, height)
|
||
|
||
Returns:
|
||
str: 区域大小类别 ('small', 'medium', 'large', 'xlarge')
|
||
"""
|
||
if not region or len(region) != 4:
|
||
return 'medium' # 默认中等大小
|
||
|
||
_, _, width, height = region
|
||
area = width * height
|
||
|
||
if area <= self.SMALL_REGION_THRESHOLD:
|
||
return 'small'
|
||
elif area <= self.MEDIUM_REGION_THRESHOLD:
|
||
return 'medium'
|
||
elif area <= self.LARGE_REGION_THRESHOLD:
|
||
return 'large'
|
||
else:
|
||
return 'xlarge'
|
||
|
||
def _set_adaptive_fps_by_region(self, recording_type, region):
|
||
"""
|
||
根据录制区域大小设置自适应帧率
|
||
|
||
Args:
|
||
recording_type: 录制类型 ('screen', 'camera', 'femtobolt')
|
||
region: 录制区域 (x, y, width, height)
|
||
"""
|
||
size_category = self._calculate_region_size_category(region)
|
||
target_fps = self.fps_config[size_category][recording_type]
|
||
|
||
# 计算区域面积用于日志
|
||
_, _, width, height = region
|
||
area = width * height
|
||
|
||
if recording_type == 'screen':
|
||
self.screen_current_fps = target_fps
|
||
elif recording_type == 'camera':
|
||
self.camera_current_fps = target_fps
|
||
elif recording_type == 'femtobolt':
|
||
self.femtobolt_current_fps = target_fps
|
||
|
||
self.logger.info(f"{recording_type}录制区域解包成功: x={region[0]}, y={region[1]}, w={width}, h={height}")
|
||
self.logger.info(f"{recording_type}录制区域分析: 面积={area:,}像素, 类别={size_category}, 优化帧率={target_fps}fps")
|
||
|
||
# 如果是大区域,提示将启用性能优化
|
||
if size_category in ['large', 'xlarge']:
|
||
self.logger.info(f"{recording_type}大区域检测: 将启用降采样和压缩优化以提升性能")
|
||
|
||
# 移除动态性能调整方法,使用固定帧率控制
|
||
|
||
def _optimize_frame_for_large_region(self, frame, region, recording_type):
|
||
"""
|
||
为大区域录制优化帧数据
|
||
|
||
Args:
|
||
frame: 原始帧数据
|
||
region: 录制区域
|
||
recording_type: 录制类型
|
||
|
||
Returns:
|
||
优化后的帧数据
|
||
"""
|
||
if frame is None:
|
||
return None
|
||
|
||
size_category = self._calculate_region_size_category(region)
|
||
|
||
# 对所有区域进行优化处理以减小文件大小
|
||
_, _, width, height = region
|
||
|
||
# 根据区域大小进行不同程度的降采样
|
||
if size_category == 'xlarge': #screen录屏·超大区域
|
||
# 超大区域:降采样到50%,极大减小文件大小
|
||
new_width = int(width * 1)
|
||
new_height = int(height * 1)
|
||
quality = 95 # 较高质量压缩
|
||
elif size_category == 'large':
|
||
# 大区域:降采样到60%,显著优化文件大小
|
||
new_width = int(width * 1)
|
||
new_height = int(height * 1)
|
||
quality = 95 # 较高质量压缩
|
||
elif size_category == 'medium': #足部视频录屏·中等区域
|
||
# 中等区域:降采样到75%,适度优化
|
||
new_width = int(width * 1)
|
||
new_height = int(height * 1)
|
||
quality = 100 # 较高质量压缩
|
||
else: # small
|
||
# 小区域:降采样到85%,轻度优化
|
||
new_width = int(width * 1)
|
||
new_height = int(height * 1)
|
||
quality = 100 # 较高质量压缩
|
||
|
||
# 应用降采样
|
||
frame = cv2.resize(frame, (new_width, new_height), interpolation=cv2.INTER_AREA)
|
||
self.logger.debug(f"{recording_type}区域降采样({size_category}): {width}x{height} -> {new_width}x{new_height}")
|
||
|
||
# 应用激进的JPEG压缩以进一步减小文件大小
|
||
encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), quality]
|
||
_, encoded_frame = cv2.imencode('.jpg', frame, encode_param)
|
||
frame = cv2.imdecode(encoded_frame, cv2.IMREAD_COLOR)
|
||
|
||
# 重要:将帧尺寸调整回VideoWriter期望的原始尺寸
|
||
# 这样可以保持压缩优化的同时确保与VideoWriter兼容
|
||
frame = cv2.resize(frame, (width, height), interpolation=cv2.INTER_LINEAR)
|
||
|
||
return frame
|
||
|
||
def start_recording(self, session_id: str, patient_id: str, screen_location: List[int], camera_location: List[int], femtobolt_location: List[int], recording_types: List[str] = None) -> Dict[str, Any]:
|
||
"""
|
||
启动同步录制
|
||
|
||
Args:
|
||
session_id: 检测会话ID
|
||
patient_id: 患者ID
|
||
screen_location: 屏幕录制区域 [x, y, w, h]
|
||
camera_location: 相机录制区域 [x, y, w, h]
|
||
femtobolt_location: FemtoBolt录制区域 [x, y, w, h]
|
||
recording_types: 录制类型列表 ['screen', 'feet', 'femtobolt'],默认全部录制
|
||
|
||
Returns:
|
||
Dict: 录制启动状态和信息
|
||
"""
|
||
result = {
|
||
'success': False,
|
||
'session_id': session_id,
|
||
'patient_id': patient_id,
|
||
'recording_start_time': None,
|
||
'video_paths': {
|
||
'feet_video': None,
|
||
'screen_video': None,
|
||
'femtobolt_video': None
|
||
},
|
||
'message': ''
|
||
}
|
||
|
||
try:
|
||
# 检查是否已在录制
|
||
if self.sync_recording:
|
||
result['message'] = f'已在录制中,当前会话ID: {self.current_session_id}'
|
||
return result
|
||
|
||
# 设置默认录制类型
|
||
recording_types = ['screen', 'feet']
|
||
# recording_types = ['screen']
|
||
|
||
|
||
|
||
# 验证录制区域参数(仅对启用的录制类型进行验证)
|
||
if 'screen' in recording_types:
|
||
if not screen_location or not isinstance(screen_location, list) or len(screen_location) != 4:
|
||
result['success'] = False
|
||
result['message'] = '屏幕录制区域参数无效或缺失,必须是包含4个元素的数组[x, y, w, h]'
|
||
return result
|
||
|
||
if 'feet' in recording_types:
|
||
if not camera_location or not isinstance(camera_location, list) or len(camera_location) != 4:
|
||
result['success'] = False
|
||
result['message'] = '相机录制区域参数无效或缺失,必须是包含4个元素的数组[x, y, w, h]'
|
||
return result
|
||
|
||
if 'femtobolt' in recording_types:
|
||
if not femtobolt_location or not isinstance(femtobolt_location, list) or len(femtobolt_location) != 4:
|
||
result['success'] = False
|
||
result['message'] = 'FemtoBolt录制区域参数无效或缺失,必须是包含4个元素的数组[x, y, w, h]'
|
||
return result
|
||
|
||
# 设置录制参数
|
||
self.current_session_id = session_id
|
||
# self.logger.info(f'检测sessionID................: {self.current_session_id}')
|
||
self.current_patient_id = patient_id
|
||
self.screen_region = tuple(screen_location) # [x, y, w, h] -> (x, y, w, h)
|
||
self.camera_region = tuple(camera_location) # [x, y, w, h] -> (x, y, w, h)
|
||
self.femtobolt_region = tuple(femtobolt_location) # [x, y, w, h] -> (x, y, w, h)
|
||
|
||
# 根据录制区域大小设置自适应帧率
|
||
if 'screen' in recording_types:
|
||
self._set_adaptive_fps_by_region('screen', self.screen_region)
|
||
if 'feet' in recording_types:
|
||
self._set_adaptive_fps_by_region('camera', self.camera_region)
|
||
if 'femtobolt' in recording_types:
|
||
self._set_adaptive_fps_by_region('femtobolt', self.femtobolt_region)
|
||
|
||
# 设置录制同步
|
||
active_recording_count = len([t for t in recording_types if t in ['screen', 'feet', 'femtobolt']])
|
||
self.recording_sync_barrier = threading.Barrier(active_recording_count)
|
||
self.recording_start_sync.clear()
|
||
self.global_recording_start_time = None
|
||
|
||
self.recording_start_time = datetime.now()
|
||
db_base_path = os.path.join('data', 'patients', patient_id, session_id)
|
||
# 创建主存储目录
|
||
if getattr(sys, 'frozen', False):
|
||
# 打包后的exe文件路径
|
||
exe_dir = os.path.dirname(sys.executable)
|
||
base_path = os.path.join(exe_dir, 'data', 'patients', patient_id, session_id)
|
||
else:
|
||
base_path = os.path.join('data', 'patients', patient_id, session_id)
|
||
|
||
|
||
try:
|
||
# 设置目录权限
|
||
self._set_directory_permissions(base_path)
|
||
os.makedirs(base_path, exist_ok=True)
|
||
|
||
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')
|
||
femtobolt_video_path = os.path.join(base_path, 'femtobolt.mp4')
|
||
|
||
|
||
# 准备数据库更新信息,返回给调用方统一处理
|
||
result['database_updates'] = {
|
||
'session_id': session_id,
|
||
'status': 'recording',
|
||
'video_paths': {
|
||
'normal_video_path': os.path.join(db_base_path, 'feet.mp4'),
|
||
'screen_video_path': os.path.join(db_base_path, 'screen.mp4'),
|
||
'femtobolt_video_path': os.path.join(db_base_path, 'femtobolt.mp4')
|
||
}
|
||
}
|
||
self.logger.debug(f'数据库更新信息已准备 - 会话ID: {session_id}')
|
||
|
||
# 视频编码参数 - 使用浏览器兼容的H.264编解码器
|
||
# 优先使用H.264编码器以确保浏览器兼容性
|
||
try:
|
||
fourcc = cv2.VideoWriter_fourcc(*'avc1') # H.264编码器,浏览器兼容性最好
|
||
except:
|
||
try:
|
||
fourcc = cv2.VideoWriter_fourcc(*'H264') # 备选H.264编码器
|
||
except:
|
||
try:
|
||
fourcc = cv2.VideoWriter_fourcc(*'mp4v') # 备选编解码器
|
||
except:
|
||
fourcc = cv2.VideoWriter_fourcc(*'MJPG') # 最后备选
|
||
|
||
# 根据录制类型选择性地初始化视频写入器,使用各自的自适应帧率
|
||
self.screen_video_writer = None
|
||
self.femtobolt_video_writer = None
|
||
self.feet_video_writer = None
|
||
|
||
if 'screen' in recording_types:
|
||
self.screen_video_writer = cv2.VideoWriter(
|
||
screen_video_path, fourcc, self.screen_current_fps, (self.screen_region[2], self.screen_region[3])
|
||
)
|
||
self.logger.info(f'屏幕视频写入器使用帧率: {self.screen_current_fps}fps')
|
||
|
||
if 'femtobolt' in recording_types:
|
||
self.femtobolt_video_writer = cv2.VideoWriter(
|
||
femtobolt_video_path, fourcc, self.femtobolt_current_fps, (self.femtobolt_region[2], self.femtobolt_region[3])
|
||
)
|
||
self.logger.info(f'FemtoBolt视频写入器使用帧率: {self.femtobolt_current_fps}fps')
|
||
|
||
if 'feet' in recording_types:
|
||
self.feet_video_writer = cv2.VideoWriter(
|
||
feet_video_path, fourcc, self.camera_current_fps, (self.camera_region[2], self.camera_region[3])
|
||
)
|
||
self.logger.info(f'足部视频写入器使用帧率: {self.camera_current_fps}fps')
|
||
|
||
# 检查视频写入器状态(仅检查启用的录制类型)
|
||
# 检查足部视频写入器
|
||
if 'feet' in recording_types:
|
||
if self.feet_video_writer and self.feet_video_writer.isOpened():
|
||
self.logger.info(f'足部视频写入器初始化成功: {feet_video_path}')
|
||
else:
|
||
self.logger.error(f'足部视频写入器初始化失败: {feet_video_path}')
|
||
else:
|
||
self.logger.info('足部录制功能已禁用')
|
||
|
||
# 检查屏幕视频写入器
|
||
if 'screen' in recording_types:
|
||
if self.screen_video_writer and self.screen_video_writer.isOpened():
|
||
self.logger.info(f'屏幕视频写入器初始化成功: {screen_video_path}')
|
||
else:
|
||
self.logger.error(f'屏幕视频写入器初始化失败: {screen_video_path}')
|
||
else:
|
||
self.logger.info('屏幕录制功能已禁用')
|
||
|
||
# 检查FemtoBolt视频写入器
|
||
if 'femtobolt' in recording_types:
|
||
if self.femtobolt_video_writer and self.femtobolt_video_writer.isOpened():
|
||
self.logger.info(f'FemtoBolt视频写入器初始化成功: {femtobolt_video_path}')
|
||
else:
|
||
self.logger.error(f'FemtoBolt视频写入器初始化失败: {femtobolt_video_path}')
|
||
else:
|
||
self.logger.info('FemtoBolt录制功能已禁用')
|
||
|
||
# 重置停止事件
|
||
self.recording_stop_event.clear()
|
||
self.sync_recording = True
|
||
|
||
# 根据录制类型启动对应的录制线程
|
||
if 'feet' in recording_types and self.feet_video_writer and self.feet_video_writer.isOpened():
|
||
self.feet_recording_thread = threading.Thread(
|
||
target=self._generic_recording_thread,
|
||
args=('camera', self.camera_region, feet_video_path, self.feet_video_writer),
|
||
daemon=True,
|
||
name='FeetRecordingThread'
|
||
)
|
||
self.feet_recording_thread.start()
|
||
# self.logger.info(f'足部录制线程已启动 - 区域: {self.camera_region}, 输出文件: {feet_video_path}')
|
||
|
||
if 'screen' in recording_types and self.screen_video_writer and self.screen_video_writer.isOpened():
|
||
self.screen_recording_thread = threading.Thread(
|
||
target=self._generic_recording_thread,
|
||
args=('screen', self.screen_region, screen_video_path, self.screen_video_writer),
|
||
daemon=True,
|
||
name='ScreenRecordingThread'
|
||
)
|
||
self.screen_recording_thread.start()
|
||
# self.logger.info(f'屏幕录制线程已启动 - 区域: {self.screen_region}, 输出文件: {screen_video_path}')
|
||
|
||
if 'femtobolt' in recording_types and self.femtobolt_video_writer and self.femtobolt_video_writer.isOpened():
|
||
self.femtobolt_recording_thread = threading.Thread(
|
||
target=self._generic_recording_thread,
|
||
args=('femtobolt', self.femtobolt_region, femtobolt_video_path, self.femtobolt_video_writer),
|
||
daemon=True,
|
||
name='FemtoBoltRecordingThread'
|
||
)
|
||
self.femtobolt_recording_thread.start()
|
||
# self.logger.info(f'FemtoBolt录制线程已启动 - 区域: {self.femtobolt_region}, 输出文件: {femtobolt_video_path}')
|
||
|
||
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, session_id: str = None) -> Dict[str, Any]:
|
||
"""
|
||
停止录制
|
||
|
||
Args:
|
||
session_id: 会话ID,用于验证是否为当前录制会话
|
||
|
||
Returns:
|
||
Dict: 停止录制的结果
|
||
"""
|
||
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
|
||
|
||
# 记录停止时间,确保所有录制线程同时结束
|
||
recording_stop_time = time.time()
|
||
self.logger.info(f'开始停止录制,停止时间: {recording_stop_time}')
|
||
|
||
# 设置停止标志
|
||
self.sync_recording = False
|
||
self.recording_stop_event.set()
|
||
|
||
# 收集活跃的录制线程
|
||
active_threads = []
|
||
if hasattr(self, 'feet_recording_thread') and self.feet_recording_thread and self.feet_recording_thread.is_alive():
|
||
active_threads.append(('feet', self.feet_recording_thread))
|
||
if hasattr(self, 'screen_recording_thread') and self.screen_recording_thread and self.screen_recording_thread.is_alive():
|
||
active_threads.append(('screen', self.screen_recording_thread))
|
||
if hasattr(self, 'femtobolt_recording_thread') and self.femtobolt_recording_thread and self.femtobolt_recording_thread.is_alive():
|
||
active_threads.append(('femtobolt', self.femtobolt_recording_thread))
|
||
|
||
# 同时等待所有录制线程结束
|
||
self.logger.info(f'等待 {len(active_threads)} 个录制线程结束')
|
||
for thread_name, thread in active_threads:
|
||
thread.join(timeout=5.0)
|
||
if thread.is_alive():
|
||
self.logger.warning(f'{thread_name}录制线程未能在超时时间内结束')
|
||
else:
|
||
self.logger.info(f'{thread_name}录制线程已结束')
|
||
|
||
# 计算实际录制时长并记录详细信息
|
||
if self.global_recording_start_time:
|
||
actual_recording_duration = recording_stop_time - self.global_recording_start_time
|
||
self.logger.info(f'录制时长统计:')
|
||
self.logger.info(f' 全局开始时间: {self.global_recording_start_time}')
|
||
self.logger.info(f' 全局结束时间: {recording_stop_time}')
|
||
self.logger.info(f' 实际录制时长: {actual_recording_duration:.3f}秒')
|
||
|
||
# 记录各录制类型的预期帧数
|
||
for thread_name, thread in active_threads:
|
||
if thread_name == 'screen':
|
||
expected_frames = int(actual_recording_duration * self.screen_current_fps)
|
||
self.logger.info(f' 屏幕录制预期帧数: {expected_frames}帧 (帧率{self.screen_current_fps}fps)')
|
||
elif thread_name == 'feet':
|
||
expected_frames = int(actual_recording_duration * self.camera_current_fps)
|
||
self.logger.info(f' 足部录制预期帧数: {expected_frames}帧 (帧率{self.camera_current_fps}fps)')
|
||
elif thread_name == 'femtobolt':
|
||
expected_frames = int(actual_recording_duration * self.femtobolt_current_fps)
|
||
self.logger.info(f' FemtoBolt录制预期帧数: {expected_frames}帧 (帧率{self.femtobolt_current_fps}fps)')
|
||
|
||
# 清理视频写入器
|
||
self._cleanup_video_writers()
|
||
|
||
# 准备数据库更新信息,返回给调用方统一处理
|
||
if self.current_session_id:
|
||
result['database_updates'] = {
|
||
'session_id': self.current_session_id,
|
||
'status': 'recorded'
|
||
}
|
||
self.logger.info(f'数据库更新信息已准备 - 会话ID: {self.current_session_id}')
|
||
|
||
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 _generic_recording_thread(self, recording_type, region, output_file_name, video_writer):
|
||
"""
|
||
通用录制线程,支持屏幕、相机和FemtoBolt录制
|
||
|
||
Args:
|
||
recording_type: 录制类型 ('screen', 'camera', 'femtobolt')
|
||
region: 录制区域 (x, y, width, height)
|
||
output_file_name: 输出文件名
|
||
video_writer: 视频写入器对象
|
||
"""
|
||
try:
|
||
self.logger.info(f'{recording_type}录制线程启动 - 区域: {region}, 输出文件: {output_file_name}')
|
||
frame_count = 0
|
||
|
||
# 根据录制类型获取对应的自适应帧率
|
||
if recording_type == 'screen':
|
||
target_fps = self.screen_current_fps
|
||
elif recording_type == 'camera':
|
||
target_fps = self.camera_current_fps
|
||
elif recording_type == 'femtobolt':
|
||
target_fps = self.femtobolt_current_fps
|
||
else:
|
||
target_fps = 25 # 默认帧率
|
||
|
||
frame_interval = 1.0 / target_fps
|
||
last_frame_time = time.time()
|
||
|
||
self.logger.info(f'{recording_type}录制线程使用帧率: {target_fps}fps')
|
||
|
||
# 等待所有录制线程准备就绪
|
||
if self.recording_sync_barrier:
|
||
self.recording_sync_barrier.wait()
|
||
|
||
# 第一个到达的线程设置全局开始时间
|
||
if self.global_recording_start_time is None:
|
||
self.global_recording_start_time = time.time()
|
||
self.recording_start_sync.set()
|
||
else:
|
||
self.recording_start_sync.wait()
|
||
|
||
# 所有线程从相同时间点开始录制
|
||
recording_start_time = self.global_recording_start_time
|
||
|
||
if not video_writer or not video_writer.isOpened():
|
||
self.logger.error(f'{recording_type}视频写入器初始化失败: {output_file_name}')
|
||
return
|
||
|
||
# 验证并解包region参数
|
||
if not region or len(region) != 4:
|
||
self.logger.error(f'{recording_type}录制区域参数无效: {region}')
|
||
return
|
||
|
||
x, y, w, h = region
|
||
self.logger.info(f'{recording_type}录制区域解包成功: x={x}, y={y}, w={w}, h={h}')
|
||
|
||
while self.sync_recording and not self.recording_stop_event.is_set():
|
||
try:
|
||
current_time = time.time()
|
||
|
||
# 严格的帧率控制 - 确保按照设定的fps精确录制
|
||
elapsed_time = current_time - last_frame_time
|
||
if elapsed_time < frame_interval:
|
||
sleep_time = frame_interval - elapsed_time
|
||
time.sleep(sleep_time)
|
||
current_time = time.time() # 重新获取时间
|
||
|
||
frame = None
|
||
|
||
# 获取帧数据 - 从屏幕截图生成
|
||
screenshot = pyautogui.screenshot(region=(x, y, w, h))
|
||
frame = cv2.cvtColor(np.array(screenshot), cv2.COLOR_RGB2BGR)
|
||
frame = cv2.resize(frame, (w, h))
|
||
|
||
# 对所有区域录制进行优化以减小文件大小
|
||
frame = self._optimize_frame_for_large_region(frame, region, recording_type)
|
||
|
||
# 写入视频帧
|
||
if frame is not None:
|
||
video_writer.write(frame)
|
||
frame_count += 1
|
||
else:
|
||
self.logger.warning(f'{recording_type}获取帧失败,跳过此帧')
|
||
|
||
last_frame_time = current_time
|
||
|
||
except Exception as e:
|
||
self.logger.error(f'{recording_type}录制线程错误: {e}')
|
||
time.sleep(0.1)
|
||
|
||
# 计算录制统计信息
|
||
if self.global_recording_start_time:
|
||
total_recording_time = time.time() - self.global_recording_start_time
|
||
expected_frames = int(total_recording_time * target_fps)
|
||
if abs(frame_count - expected_frames) > target_fps * 0.1: # 如果帧数差异超过0.1秒的帧数
|
||
self.logger.warning(f'{recording_type}帧数异常: 实际{frame_count}帧 vs 预期{expected_frames}帧,差异{frame_count - expected_frames}帧')
|
||
else:
|
||
self.logger.info(f'{recording_type}录制线程结束,总帧数: {frame_count}')
|
||
|
||
except Exception as e:
|
||
self.logger.error(f'{recording_type}录制线程异常: {e}')
|
||
finally:
|
||
# 清理资源
|
||
if video_writer:
|
||
try:
|
||
video_writer.release()
|
||
self.logger.info(f'{recording_type}视频写入器已释放')
|
||
except Exception as e:
|
||
self.logger.error(f'释放{recording_type}视频写入器失败: {e}')
|
||
|
||
def _cleanup_video_writers(self):
|
||
"""清理视频写入器"""
|
||
try:
|
||
if hasattr(self, 'feet_video_writer') and self.feet_video_writer:
|
||
self.feet_video_writer.release()
|
||
self.feet_video_writer = None
|
||
self.logger.debug("足部视频写入器已清理")
|
||
|
||
if hasattr(self, 'screen_video_writer') and self.screen_video_writer:
|
||
self.screen_video_writer.release()
|
||
self.screen_video_writer = None
|
||
self.logger.debug("屏幕视频写入器已清理")
|
||
|
||
if hasattr(self, 'femtobolt_video_writer') and self.femtobolt_video_writer:
|
||
self.femtobolt_video_writer.release()
|
||
self.femtobolt_video_writer = None
|
||
self.logger.debug("FemtoBolt视频写入器已清理")
|
||
|
||
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.screen_region = 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.screen_region = (x, y, width, height)
|
||
self.logger.info(f"录制区域已设置: {self.screen_region}")
|
||
else:
|
||
self.logger.info("录制模式已设置: 全屏录制")
|
||
|
||
return True
|
||
|
||
def set_recording_regions(self, screen_region=None, camera_region=None, femtobolt_region=None):
|
||
"""
|
||
设置三个录制区域
|
||
|
||
Args:
|
||
screen_region: 屏幕录制区域 (x, y, width, height)
|
||
camera_region: 相机录制区域 (x, y, width, height)
|
||
femtobolt_region: FemtoBolt录制区域 (x, y, width, height)
|
||
"""
|
||
if self.sync_recording:
|
||
self.logger.warning("录制进行中,无法更改区域设置")
|
||
return False
|
||
|
||
self.screen_region = screen_region
|
||
self.camera_region = camera_region
|
||
self.femtobolt_region = femtobolt_region
|
||
|
||
self.logger.info(f'录制区域已设置:')
|
||
self.logger.info(f' 屏幕区域: {screen_region}')
|
||
self.logger.info(f' 相机区域: {camera_region}')
|
||
self.logger.info(f' FemtoBolt区域: {femtobolt_region}')
|
||
|
||
return True
|
||
|
||
def get_status(self):
|
||
"""获取录制状态"""
|
||
return {
|
||
'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,
|
||
'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 collect_detection_data(self, session_id: str, patient_id: str, detection_data: Dict[str, Any]) -> Dict[str, Any]:
|
||
"""
|
||
保存前端传入的检测数据和图片
|
||
|
||
Args:
|
||
session_id: 检测会话ID
|
||
patient_id: 患者ID
|
||
detection_data: 前端传入的检测数据,包含base64格式的图片数据
|
||
|
||
Returns:
|
||
Dict: 包含所有采集数据的字典,符合detection_data表结构
|
||
"""
|
||
# 生成采集时间戳
|
||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S_%f')[:-3] # 精确到毫秒
|
||
if getattr(sys, 'frozen', False):
|
||
# 打包后的exe文件路径
|
||
exe_dir = os.path.dirname(sys.executable)
|
||
data_dir = Path(os.path.join(exe_dir, 'data', 'patients', patient_id, session_id, timestamp))
|
||
else:
|
||
data_dir = Path(f'data/patients/{patient_id}/{session_id}/{timestamp}')
|
||
# 创建数据存储目录
|
||
data_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
# 设置目录权限为777(完全权限)
|
||
try:
|
||
import stat
|
||
os.chmod(str(data_dir), stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) # 777权限
|
||
self.logger.debug(f"已设置目录权限为777: {data_dir}")
|
||
except Exception as perm_error:
|
||
self.logger.warning(f"设置目录权限失败: {perm_error},但目录创建成功")
|
||
|
||
# 初始化数据字典
|
||
data = {
|
||
'session_id': session_id,
|
||
'head_pose': detection_data.get('head_pose'),
|
||
'body_pose': None,
|
||
'body_image': None,
|
||
'foot_data': detection_data.get('foot_data'),
|
||
'foot_data_image': None,
|
||
'foot_image': None,
|
||
'screen_image': None,
|
||
'timestamp': timestamp
|
||
}
|
||
|
||
try:
|
||
# 保存图片数据
|
||
image_fields = [
|
||
('body_image', 'body'),
|
||
('foot_image', 'foot'),
|
||
('foot_data_image', 'foot_data')
|
||
]
|
||
|
||
for field, prefix in image_fields:
|
||
base64_data = detection_data.get(field)
|
||
if base64_data:
|
||
try:
|
||
# 移除base64头部信息
|
||
if ';base64,' in base64_data:
|
||
base64_data = base64_data.split(';base64,')[1]
|
||
|
||
# 解码base64数据
|
||
image_data = base64.b64decode(base64_data)
|
||
|
||
# 生成图片文件名
|
||
filename = f'{prefix}_{timestamp}.jpg'
|
||
file_path = data_dir / filename
|
||
|
||
# 保存图片
|
||
with open(file_path, 'wb') as f:
|
||
f.write(image_data)
|
||
|
||
# 更新数据字典中的图片路径
|
||
|
||
data[field] = str(os.path.join('data', 'patients', patient_id, session_id, timestamp, filename))
|
||
self.logger.debug(f'{field}保存成功: {filename}')
|
||
|
||
except Exception as e:
|
||
self.logger.error(f'保存{field}失败: {e}')
|
||
# 屏幕截图
|
||
screen_image = self._capture_screen_image(data_dir,timestamp)
|
||
if screen_image:
|
||
data['screen_image'] = str(os.path.join('data', 'patients', patient_id, session_id, timestamp, screen_image))
|
||
|
||
self.logger.debug(f'数据保存完成: {session_id}, 时间戳: {timestamp}')
|
||
|
||
except Exception as e:
|
||
self.logger.error(f'数据保存失败: {e}')
|
||
|
||
return data
|
||
|
||
|
||
|
||
def _capture_screen_image(self, data_dir,timestamp) -> Optional[str]:
|
||
"""
|
||
采集屏幕截图,根据screen_region 进行截图
|
||
|
||
Args:
|
||
data_dir: 数据存储目录路径
|
||
|
||
Returns:
|
||
str: 截图文件的相对路径,失败返回None
|
||
"""
|
||
try:
|
||
# 截取屏幕
|
||
if self.screen_region:
|
||
# 使用指定区域截图
|
||
x, y, width, height = self.screen_region
|
||
screenshot = pyautogui.screenshot(region=(x, y, width, height))
|
||
else:
|
||
# 全屏截图
|
||
screenshot = pyautogui.screenshot()
|
||
|
||
# 保存截图
|
||
from pathlib import Path
|
||
screen_filename = f'screen_{timestamp}.jpg'
|
||
image_path = Path(data_dir) / screen_filename
|
||
screenshot.save(str(image_path), quality=95, optimize=True)
|
||
|
||
return screen_filename
|
||
|
||
except Exception as e:
|
||
self.logger.error(f'屏幕截图失败: {e}')
|
||
return None
|
||
|
||
|
||
|
||
|
||
# 保持向后兼容的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() |