diff --git a/backend/app.py b/backend/app.py
index 060fa54a..5b09c4c7 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('应用初始化完成')
@@ -142,31 +136,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'])
@@ -720,7 +689,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格式
@@ -743,11 +712,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 bf12bf99..cd9d20c1 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 d6a395b7..51951e9e 100644
--- a/backend/device_manager.py
+++ b/backend/device_manager.py
@@ -66,12 +66,8 @@ class DeviceManager:
# 推流状态和线程
self.camera_streaming = False
self.femtobolt_streaming = False
- self.imu_streaming = False
- self.pressure_streaming = False
self.camera_streaming_thread = None
self.femtobolt_streaming_thread = None
- self.imu_thread = None
- self.pressure_thread = None
self.streaming_stop_event = threading.Event()
# 全局帧缓存机制
@@ -248,22 +244,17 @@ class DeviceManager:
self.femtobolt_config.synchronized_images_only = True
# 视效范围参数示例,假设SDK支持depth_range_min和depth_range_max
- # 启动FemtoBolt设备(直接尝试启动,失败时优雅处理)
+ # 直接尝试启动设备(pykinect_azure库没有设备数量检测API)
+ logger.info('准备启动FemtoBolt设备...')
+
+ # 启动FemtoBolt设备
logger.info('尝试启动FemtoBolt设备...')
- try:
- self.femtobolt_camera = pykinect.start_device(config=self.femtobolt_config)
- if self.femtobolt_camera:
- self.device_status['femtobolt'] = True
- logger.info('✓ FemtoBolt深度相机初始化成功!')
- else:
- logger.warning('FemtoBolt设备启动失败:设备返回None(可能未连接设备)')
- self.femtobolt_camera = None
- self.device_status['femtobolt'] = False
- except BaseException as device_error:
- logger.warning(f'FemtoBolt设备启动失败: {device_error}')
- logger.info('这通常表示没有连接FemtoBolt设备,系统将继续运行但不包含深度相机功能')
- self.femtobolt_camera = None
- self.device_status['femtobolt'] = False
+ self.femtobolt_camera = pykinect.start_device(config=self.femtobolt_config)
+ if self.femtobolt_camera:
+ self.device_status['femtobolt'] = True
+ logger.info('✓ FemtoBolt深度相机初始化成功!')
+ else:
+ raise Exception('设备启动返回None')
except Exception as e:
logger.warning(f'FemtoBolt深度相机初始化失败: {e}')
@@ -368,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']:
@@ -410,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),
@@ -507,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:
@@ -523,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 = {
@@ -572,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}')
@@ -628,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:
@@ -789,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
@@ -900,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深度相机推流线程"""
@@ -1144,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', {
@@ -1221,7 +856,9 @@ class DeviceManager:
head_pose_data = {
'rotation': head_pose['rotation'], # 旋转角:左旋(-), 右旋(+)
'tilt': head_pose['tilt'], # 倾斜角:左倾(-), 右倾(+)
- 'pitch': head_pose['pitch'], # 俯仰角:俯角(-), 仰角(+)
+ 'pitch': head_pose['pitch'], # 俯仰角:俯角(-), 仰角(+)
+
+ 'temperature': imu_data.get('temperature', 25),
'timestamp': imu_data['timestamp']
}
@@ -1267,14 +904,6 @@ class DeviceManager:
# 计算总压力
total_pressure = left_total + right_total
- # 计算各区域压力百分比
- left_front_percent = (left_front / total_pressure * 100) if total_pressure > 0 else 0
- left_rear_percent = (left_rear / total_pressure * 100) if total_pressure > 0 else 0
- right_front_percent = (right_front / total_pressure * 100) if total_pressure > 0 else 0
- right_rear_percent = (right_rear / total_pressure * 100) if total_pressure > 0 else 0
- left_total_percent = (left_total / total_pressure * 100) if total_pressure > 0 else 0
- right_total_percent = (right_total / total_pressure * 100) if total_pressure > 0 else 0
-
# 计算平衡比例(左脚压力占总压力的比例)
balance_ratio = left_total / total_pressure if total_pressure > 0 else 0.5
@@ -1287,15 +916,15 @@ class DeviceManager:
# 构建完整的足部压力数据
complete_pressure_data = {
- # 分区压力百分比
+ # 分区压力值
'pressure_zones': {
- 'left_front': round(left_front_percent, 1),
- 'left_rear': round(left_rear_percent, 1),
- 'right_front': round(right_front_percent,1),
- 'right_rear': round(right_rear_percent, 1),
- 'left_total': round(left_total_percent, 1),
- 'right_total': round(right_total_percent, 1),
- 'total_pressure': 100.0 # 总压力百分比始终为100%
+ 'left_front': left_front,
+ 'left_rear': left_rear,
+ 'right_front': right_front,
+ 'right_rear': right_rear,
+ 'left_total': left_total,
+ 'right_total': right_total,
+ 'total_pressure': total_pressure
},
# 平衡分析
'balance_analysis': {
@@ -1329,6 +958,7 @@ class DeviceManager:
logger.info('压力传感器足部压力数据推流线程已结束')
def start_recording(self, session_id: str, patient_id: str) -> Dict[str, Any]:
+ video_manager=VideoStreamManager()
"""启动同步录制
Args:
@@ -1376,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
@@ -1391,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)
@@ -1403,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(
@@ -1430,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(
@@ -1457,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'] = '同步录制已启动'
@@ -1516,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:
@@ -1528,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:
@@ -1558,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}')
@@ -1599,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}次)")
@@ -1625,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):
"""屏幕录制线程"""
@@ -1718,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设备"""
@@ -1879,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))
@@ -1929,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=="
@@ -1940,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
@@ -2067,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)
@@ -2098,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:
@@ -2114,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': '使用视频监控视频源(激进实时模式)'})
@@ -2155,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 # 重置错误计数
@@ -2192,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:
# 非阻塞方式放入队列,如果队列满了就丢弃旧帧
@@ -2478,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'
diff --git a/frontend/src/renderer/src/assets/svg/u125.svg b/frontend/src/renderer/src/assets/svg/u125.svg
new file mode 100644
index 00000000..c34c2cc3
--- /dev/null
+++ b/frontend/src/renderer/src/assets/svg/u125.svg
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/frontend/src/renderer/src/assets/svg/u14.svg b/frontend/src/renderer/src/assets/svg/u14.svg
new file mode 100644
index 00000000..03f39ab4
--- /dev/null
+++ b/frontend/src/renderer/src/assets/svg/u14.svg
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/frontend/src/renderer/src/assets/svg/u155.svg b/frontend/src/renderer/src/assets/svg/u155.svg
new file mode 100644
index 00000000..2654638d
--- /dev/null
+++ b/frontend/src/renderer/src/assets/svg/u155.svg
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/frontend/src/renderer/src/assets/svg/u164.svg b/frontend/src/renderer/src/assets/svg/u164.svg
new file mode 100644
index 00000000..5f3b3988
--- /dev/null
+++ b/frontend/src/renderer/src/assets/svg/u164.svg
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/frontend/src/renderer/src/assets/svg/u58.svg b/frontend/src/renderer/src/assets/svg/u58.svg
new file mode 100644
index 00000000..c2bf1820
--- /dev/null
+++ b/frontend/src/renderer/src/assets/svg/u58.svg
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/frontend/src/renderer/src/assets/svg/u67.svg b/frontend/src/renderer/src/assets/svg/u67.svg
new file mode 100644
index 00000000..fc3452ee
--- /dev/null
+++ b/frontend/src/renderer/src/assets/svg/u67.svg
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/frontend/src/renderer/src/assets/svg/u7.svg b/frontend/src/renderer/src/assets/svg/u7.svg
new file mode 100644
index 00000000..c34790d2
--- /dev/null
+++ b/frontend/src/renderer/src/assets/svg/u7.svg
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/frontend/src/renderer/src/assets/u45.png b/frontend/src/renderer/src/assets/u45.png
new file mode 100644
index 00000000..e64eb882
Binary files /dev/null and b/frontend/src/renderer/src/assets/u45.png differ
diff --git a/frontend/src/renderer/src/views/Dashboard.vue b/frontend/src/renderer/src/views/Dashboard.vue
index 0e926886..85ca187e 100644
--- a/frontend/src/renderer/src/views/Dashboard.vue
+++ b/frontend/src/renderer/src/views/Dashboard.vue
@@ -34,21 +34,21 @@
-
-
-
-
-
-
+
+
+
+
+
+
- 未处理
- 已处理
+ 未处理
+ 已处理
-
-
+
+
- 删除
+ 删除
@@ -981,14 +981,14 @@ function delClick(id) {
display: flex;
justify-content: center;
box-sizing: border-box;
- padding-top: 10px;
+ padding-top: 13px;
width: 100%;
}
.basic-info-text {
width: 33%;
text-align: center;
- font-size: 14px;
+ font-size: 20px;
}
.basic-info-textcolor {
@@ -1088,11 +1088,14 @@ function delClick(id) {
:deep(.el-table th .cell){
color: #30F3FF ;
font-weight: 400;
- font-size: 14px;
+ font-size: 18px;
}
:deep(.el-table--border .el-table__inner-wrapper){
border-right: 1px solid #434343;
}
+:deep(.el-table .cell){
+ font-size: 18px;
+}
\ No newline at end of file