diff --git a/.gitignore b/.gitignore index 3b25994e..d08eca98 100644 --- a/.gitignore +++ b/.gitignore @@ -10020,3 +10020,4 @@ node_modules/date-fns/subQuarters/index.js node_modules/date-fns/subQuarters/index.js.flow Log/OrbbecSDK.log.txt Log/OrbbecSDK.log.txt +backend/Log/OrbbecSDK.log.txt diff --git a/backend/app.py b/backend/app.py index cf1c767b..4a928f38 100644 --- a/backend/app.py +++ b/backend/app.py @@ -164,7 +164,10 @@ def init_app(): logger.info('SocketIO已启用') device_manager.set_socketio(socketio) # 设置WebSocket连接 # 初始化视频流管理器 + t_vsm = time.time() + logger.info(f'[TIMING] 准备创建VideoStreamManager - {datetime.now().strftime("%H:%M:%S.%f")[:-3]}') video_stream_manager = VideoStreamManager(socketio, device_manager) + logger.info(f'[TIMING] VideoStreamManager创建完成,耗时: {(time.time()-t_vsm)*1000:.2f}ms') else: logger.info('SocketIO未启用,跳过WebSocket相关初始化') video_stream_manager = None @@ -1058,9 +1061,10 @@ if socketio is not None: # 启动视频流管理器(普通摄像头) if video_stream_manager: - logger.info('正在启动视频流管理器...') + t_vs = time.time() + logger.info(f'[TIMING] 即将调用start_video_stream - {datetime.now().strftime("%H:%M:%S.%f")[:-3]}') video_result = video_stream_manager.start_video_stream() - logger.info(f'视频流管理器启动结果: {video_result}') + logger.info(f'[TIMING] start_video_stream返回(耗时: {(time.time()-t_vs)*1000:.2f}ms): {video_result}') results['cameras']['normal'] = video_result else: logger.error('视频流管理器未初始化') diff --git a/backend/config.ini b/backend/config.ini index 9e0d5470..480fd2ba 100644 --- a/backend/config.ini +++ b/backend/config.ini @@ -16,11 +16,11 @@ backup_interval = 24 max_backups = 7 [DEVICES] -camera_index = 0 +camera_index = 3 camera_width = 640 camera_height = 480 camera_fps = 30 -imu_port = COM4 +imu_port = COM8 imu_baudrate = 9600 pressure_port = COM4 diff --git a/backend/device_manager.py b/backend/device_manager.py index 35739779..724f32cd 100644 --- a/backend/device_manager.py +++ b/backend/device_manager.py @@ -26,6 +26,10 @@ import logging # 添加串口通信支持 import serial +# SMiTSense DLL支持 +import ctypes +from ctypes import Structure, c_int, c_float, c_char_p, c_void_p, c_uint32, c_uint16, POINTER, byref + # matplotlib相关导入(用于深度图渲染) try: from matplotlib.colors import LinearSegmentedColormap @@ -43,7 +47,7 @@ from database import DatabaseManager try: import pykinect_azure as pykinect # 重新启用FemtoBolt功能,使用正确的Orbbec SDK K4A Wrapper路径 - FEMTOBOLT_AVAILABLE = False + FEMTOBOLT_AVAILABLE = True print("信息: pykinect_azure库已安装,FemtoBolt深度相机功能已启用") print("使用Orbbec SDK K4A Wrapper以确保与FemtoBolt设备的兼容性") except ImportError: @@ -134,10 +138,10 @@ class DeviceManager: """初始化所有设备""" # 分别初始化各个设备,单个设备失败不影响其他设备 - try: - self._init_camera() - except Exception as e: - logger.error(f'摄像头初始化失败: {e}') + # try: + # self._init_camera() + # except Exception as e: + # logger.error(f'摄像头初始化失败: {e}') try: if FEMTOBOLT_AVAILABLE: @@ -156,7 +160,7 @@ class DeviceManager: except Exception as e: logger.error(f'压力传感器初始化失败: {e}') - logger.info('设备初始化完成(部分设备可能初始化失败)') + logger.info('设备初始化完成') def _init_camera(self): """初始化足部监视摄像头""" @@ -298,7 +302,7 @@ class DeviceManager: logger.warning('未能读取到config.ini,将使用默认串口配置COM7@9600') imu_port = config.get('DEVICES', 'imu_port', fallback='COM7') - imu_baudrate = config.getint('DEVICES', 'baudrate', fallback=9600) + imu_baudrate = config.getint('DEVICES', 'imu_baudrate', fallback=9600) logger.info(f'从配置文件读取IMU串口配置: {imu_port}@{imu_baudrate}') # 初始化真实IMU设备 @@ -320,8 +324,16 @@ class DeviceManager: def _init_pressure_sensor(self): """初始化压力传感器""" try: - # 这里应该连接实际的压力传感器 - # 目前使用模拟数据 + # 优先尝试连接真实设备 + try: + self.pressure_device = RealPressureDevice() + self.device_status['pressure'] = True + logger.info('压力传感器初始化成功(真实设备)') + return + except Exception as real_e: + logger.warning(f'真实压力传感器初始化失败,使用模拟设备。原因: {real_e}') + + # 回退到模拟设备 self.pressure_device = MockPressureDevice() self.device_status['pressure'] = True logger.info('压力传感器初始化成功(模拟)') @@ -418,7 +430,7 @@ class DeviceManager: try: samples = [] for _ in range(100): - data = self.imu_device.read_data() + data = self.imu_device.read_data(apply_calibration=False) if data and 'head_pose' in data: samples.append(data['head_pose']) time.sleep(0.01) @@ -447,7 +459,7 @@ class DeviceManager: try: samples = [] for _ in range(10): # 少量采样,加快启动 - data = self.imu_device.read_data() + data = self.imu_device.read_data(apply_calibration=False) if data and 'head_pose' in data: samples.append(data['head_pose']) time.sleep(0.01) @@ -658,7 +670,7 @@ class DeviceManager: ) self.femtobolt_streaming_thread.start() - logger.info('FemtoBolt深度相机推流已开始') + # logger.info('FemtoBolt深度相机推流已开始') return True except Exception as e: logger.error(f'FemtoBolt深度相机推流启动失败: {e}') @@ -728,10 +740,27 @@ class DeviceManager: logger.warning('压力传感器数据推流已在运行') return True + # 确保设备已初始化(懒加载+自动重连) + if not self.pressure_device: + try: + self._init_pressure_sensor() + except Exception as init_e: + logger.error(f'压力传感器设备初始化失败: {init_e}') + return False + else: + # 如果是真实设备且未连接,尝试重连 + try: + if hasattr(self.pressure_device, 'is_connected') and not getattr(self.pressure_device, 'is_connected', True): + logger.info('检测到压力设备未连接,尝试重新初始化...') + self._init_pressure_sensor() + except Exception as reinit_e: + logger.warning(f'压力设备重连失败: {reinit_e}') + + # 再次确认 if not self.pressure_device: logger.error('压力传感器设备未初始化') return False - + self.pressure_streaming = True self.pressure_thread = threading.Thread(target=self._pressure_streaming_thread, daemon=True) self.pressure_thread.start() @@ -753,6 +782,13 @@ class DeviceManager: self.pressure_streaming = False if self.pressure_thread and self.pressure_thread.is_alive(): self.pressure_thread.join(timeout=2) + + # 关闭压力设备连接 + if self.pressure_device and hasattr(self.pressure_device, 'close'): + try: + self.pressure_device.close() + except Exception as close_e: + logger.warning(f'关闭压力设备连接失败: {close_e}') logger.info('压力传感器足部压力数据推流已停止') return True @@ -986,7 +1022,7 @@ class DeviceManager: # finally: # self.femtobolt_streaming = False - def _femtobolt_streaming_thread(self): + # def _femtobolt_streaming_thread(self): """FemtoBolt深度相机推流线程""" frame_count = 0 try: @@ -1015,36 +1051,41 @@ class DeviceManager: depth_range_max = None # 使用matplotlib渲染深度图,参考display_x.py if MATPLOTLIB_AVAILABLE and depth_range_min is not None and depth_range_max is not None: - # 在子线程中切换到非交互后端,避免GUI警告 - import matplotlib - try: - if matplotlib.get_backend().lower() != 'agg': - matplotlib.use('Agg', force=True) - logger.debug('切换matplotlib后端为Agg以适配子线程渲染') - except Exception: - pass + import numpy as np + import cv2 + # 假设 depth_image 已经是 np.uint16 格式 + depth_image = depth_image.copy() depth_image[depth_image > depth_range_max] = 0 depth_image[depth_image < depth_range_min] = 0 - background = np.ones_like(depth_image) * 0.5 - depth_masked = np.ma.masked_equal(depth_image, 0) - colors = ['fuchsia', 'red', 'yellow', 'lime', 'cyan', 'blue', - 'fuchsia', 'red', 'yellow', 'lime', 'cyan', 'blue', - 'fuchsia', 'red', 'yellow', 'lime', 'cyan', 'blue', - 'fuchsia', 'red', 'yellow', 'lime', 'cyan', 'blue'] - mcmap = LinearSegmentedColormap.from_list("custom_cmap", colors) - fig = plt.figure(figsize=(width2/100, height2/100), dpi=100) - ax = fig.add_subplot(111) - ax.imshow(background, origin='lower', cmap='gray', alpha=0.3) - ax.grid(True, which='both', axis='both', color='white', linestyle='-', linewidth=1, zorder=0) - ax.contourf(depth_masked, levels=200, cmap=mcmap, vmin=depth_range_min, vmax=depth_range_max, origin='upper', zorder=2) - fig.tight_layout(pad=0) - try: - fig.canvas.draw() - img = np.frombuffer(fig.canvas.tostring_rgb(), dtype=np.uint8) - img = img.reshape(fig.canvas.get_width_height()[::-1] + (3,)) - depth_colored = img - finally: - plt.close(fig) + # 归一化到 0-255 + depth_normalized = np.clip(depth_image, depth_range_min, depth_range_max) + depth_normalized = ((depth_normalized - depth_range_min) /(depth_range_max - depth_range_min) * 255).astype(np.uint8) + # 用 OpenCV 生成彩色映射(用 COLORMAP_JET 或自定义 LUT 代替 LinearSegmentedColormap) + # === 对比度增强 === + alpha = 1.5 # 对比度增益 (>1 增强对比,1.5 比较明显) + beta = 0 # 亮度偏移 + depth_normalized = cv2.convertScaleAbs(depth_normalized, alpha=alpha, beta=beta) + # 可选:伽马校正,增强中间层次感 + gamma = 0.8 # <1 提亮暗部, >1 压暗暗部 + look_up_table = np.array([((i / 255.0) ** gamma) * 255 for i in range(256)]).astype("uint8") + depth_normalized = cv2.LUT(depth_normalized, look_up_table) + depth_colored = cv2.applyColorMap(depth_normalized, cv2.COLORMAP_JET) + # 创建灰色背景 + rows, cols = depth_colored.shape[:2] + background = np.ones_like(depth_colored, dtype=np.uint8) * 128 # 灰色 + # 画网格(和 matplotlib 网格类似) + rows, cols = depth_colored.shape[:2] + grid_color = (255, 255, 255) # 白色 + line_thickness = 1 + grid_bg = np.zeros_like(depth_colored) + cell_size = 50 # 可以根据原 contourf 分辨率调 + for x in range(0, cols, cell_size): + cv2.line(grid_bg, (x, 0), (x, rows), grid_color, line_thickness) + for y in range(0, rows, cell_size): + cv2.line(grid_bg, (0, y), (cols, y), grid_color, line_thickness) + bg_with_grid = background.copy() + mask_grid = (grid_bg.sum(axis=2) > 0) + depth_colored[mask_grid] = grid_bg[mask_grid] else: # 如果没有matplotlib则使用原有OpenCV伪彩色映射 depth_normalized = np.clip(depth_image, depth_range_min, depth_range_max) @@ -1093,10 +1134,117 @@ class DeviceManager: logger.debug(f'FemtoBolt推流线程异常: {e}') finally: self.femtobolt_streaming = False - + def _femtobolt_streaming_thread(self): + """FemtoBolt深度相机推流线程""" + frame_count = 0 + try: + while self.femtobolt_streaming and not self.streaming_stop_event.is_set(): + if self.femtobolt_camera and self.socketio: + try: + capture = self.femtobolt_camera.update() + if capture is not None: + ret, depth_image = capture.get_depth_image() + if ret and depth_image is not None: + import configparser + config = configparser.ConfigParser() + config.read('config.ini') + try: + depth_range_min = int(config.get('DEFAULT', 'femtobolt_depth_range_min', fallback='1400')) + depth_range_max = int(config.get('DEFAULT', 'femtobolt_depth_range_max', fallback='1900')) + except Exception: + depth_range_min = None + depth_range_max = None + + if MATPLOTLIB_AVAILABLE and depth_range_min is not None and depth_range_max is not None: + import numpy as np + import cv2 + depth_image = depth_image.copy() + + # === 生成灰色背景 + 白色网格 === + rows, cols = depth_image.shape[:2] + background = np.ones((rows, cols, 3), dtype=np.uint8) * 128 + cell_size = 50 + grid_color = (255, 255, 255) + grid_bg = np.zeros_like(background) + for x in range(0, cols, cell_size): + cv2.line(grid_bg, (x, 0), (x, rows), grid_color, 1) + for y in range(0, rows, cell_size): + cv2.line(grid_bg, (0, y), (cols, y), grid_color, 1) + mask_grid = (grid_bg.sum(axis=2) > 0) + background[mask_grid] = grid_bg[mask_grid] + + # === 处理深度图满足区间的部分 === + depth_clipped = depth_image.copy() + depth_clipped[depth_clipped < depth_range_min] = 0 + depth_clipped[depth_clipped > depth_range_max] = 0 + depth_normalized = np.clip(depth_clipped, depth_range_min, depth_range_max) + depth_normalized = ((depth_normalized - depth_range_min) / (depth_range_max - depth_range_min) * 255).astype(np.uint8) + + # 对比度和伽马校正 + alpha, beta, gamma = 1.5, 0, 0.8 + depth_normalized = cv2.convertScaleAbs(depth_normalized, alpha=alpha, beta=beta) + lut = np.array([((i / 255.0) ** gamma) * 255 for i in range(256)]).astype("uint8") + depth_normalized = cv2.LUT(depth_normalized, lut) + + # 伪彩色 + depth_colored = cv2.applyColorMap(depth_normalized, cv2.COLORMAP_JET) + + # 将有效深度覆盖到灰色背景上 + mask_valid = (depth_clipped > 0) + for c in range(3): + background[:, :, c][mask_valid] = depth_colored[:, :, c][mask_valid] + + depth_colored_final = background + + else: + # 没有matplotlib则使用原OpenCV伪彩色 + depth_normalized = np.clip(depth_image, depth_range_min, depth_range_max) + depth_normalized = ((depth_normalized - depth_range_min) / (depth_range_max - depth_range_min) * 255).astype(np.uint8) + depth_colored_final = cv2.applyColorMap(depth_normalized, cv2.COLORMAP_JET) + mask_outside = (depth_image < depth_range_min) | (depth_image > depth_range_max) + depth_colored_final[mask_outside] = [128, 128, 128] # 灰色 + + # 裁剪宽度 + height, width = depth_colored_final.shape[:2] + target_width = height // 2 + if width > target_width: + left = (width - target_width) // 2 + right = left + target_width + depth_colored_final = depth_colored_final[:, left:right] + + # 保存到缓存 + self._save_frame_to_cache(depth_colored_final.copy(), 'femtobolt') + + # 推送SocketIO + success, buffer = cv2.imencode('.jpg', depth_colored_final, [int(cv2.IMWRITE_JPEG_QUALITY), 80]) + if success and self.socketio: + import base64, time + jpg_as_text = base64.b64encode(buffer).decode('utf-8') + self.socketio.emit('depth_camera_frame', { + 'image': jpg_as_text, + 'frame_id': frame_count, + 'timestamp': time.time() + }) + frame_count += 1 + else: + time.sleep(0.01) + else: + time.sleep(0.01) + + except Exception as e: + logger.debug(f'FemtoBolt帧推送失败: {e}') + time.sleep(0.1) + + time.sleep(1/30) # 30 FPS + + except Exception as e: + logger.debug(f'FemtoBolt推流线程异常: {e}') + finally: + self.femtobolt_streaming = False + def _imu_streaming_thread(self): """IMU头部姿态数据推流线程""" - logger.info('IMU头部姿态数据推流线程已启动') + # logger.info('IMU头部姿态数据推流线程已启动') try: loop_count = 0 @@ -1925,7 +2073,7 @@ class DeviceManager: logger.error(f'清理过期帧失败: {e}') class RealIMUDevice: """真实IMU设备,通过串口读取姿态数据""" - def __init__(self, port: str = 'COM4', baudrate: int = 9600): + def __init__(self, port, baudrate): self.port = port self.baudrate = baudrate self.ser = None @@ -2015,7 +2163,7 @@ class RealIMUDevice: # logger.debug(f'忽略的数据包类型: 0x{packet_type:02X}') return None - def read_data(self) -> Dict[str, Any]: + def read_data(self, apply_calibration: bool = True) -> Dict[str, Any]: if not self.ser or not getattr(self.ser, 'is_open', False): logger.warning('IMU串口未连接,尝试重新连接...') self._connect() @@ -2054,7 +2202,7 @@ class RealIMUDevice: 'timestamp': datetime.now().isoformat() } # logger.debug(f'映射后的头部姿态: {raw}') - return self.apply_calibration(raw) + return self.apply_calibration(raw) if apply_calibration else raw raw = { 'head_pose': { 'rotation': self.last_data['yaw'], @@ -2064,7 +2212,7 @@ class RealIMUDevice: 'temperature': self.last_data['temperature'], 'timestamp': datetime.now().isoformat() } - return self.apply_calibration(raw) + return self.apply_calibration(raw) if apply_calibration else raw except Exception as e: logger.error(f'IMU数据读取异常: {e}', exc_info=True) raw = { @@ -2076,7 +2224,7 @@ class RealIMUDevice: 'temperature': self.last_data['temperature'], 'timestamp': datetime.now().isoformat() } - return self.apply_calibration(raw) + return self.apply_calibration(raw) if apply_calibration else raw def __del__(self): try: @@ -2113,7 +2261,7 @@ class MockIMUDevice: calibrated_data['head_pose'] = head_pose return calibrated_data - def read_data(self) -> Dict[str, Any]: + def read_data(self, apply_calibration: bool = True) -> Dict[str, Any]: """读取IMU数据""" # 生成头部姿态角度数据,角度范围(-90°, +90°) # 使用正弦波模拟自然的头部运动,添加随机噪声 @@ -2143,55 +2291,572 @@ class MockIMUDevice: } # 应用校准并返回 - return self.apply_calibration(raw_data) + return self.apply_calibration(raw_data) if apply_calibration else raw_data + + +class RealPressureDevice: + """真实SMiTSense压力传感器设备""" + + def __init__(self, dll_path=None): + """初始化SMiTSense压力传感器 + + Args: + dll_path: DLL文件路径,如果为None则使用默认路径 + """ + self.dll = None + self.device_handle = None + self.is_connected = False + self.rows = 0 + self.cols = 0 + self.frame_size = 0 + self.buf = None + + # 设置DLL路径 - 使用正确的DLL文件名 + if dll_path is None: + # 尝试多个可能的DLL文件名 + dll_candidates = [ + os.path.join(os.path.dirname(__file__), 'dll', 'smitsense', 'SMiTSenseUsbWrapper.dll'), + os.path.join(os.path.dirname(__file__), 'dll', 'smitsense', 'SMiTSenseUsb-F3.0.dll') + ] + dll_path = None + for candidate in dll_candidates: + if os.path.exists(candidate): + dll_path = candidate + break + + if dll_path is None: + raise FileNotFoundError(f"未找到SMiTSense DLL文件,检查路径: {dll_candidates}") + + self.dll_path = dll_path + logger.info(f'初始化真实压力传感器设备,DLL路径: {dll_path}') + + try: + self._load_dll() + self._initialize_device() + except Exception as e: + logger.error(f'压力传感器初始化失败: {e}') + # 如果真实设备初始化失败,可以选择降级为模拟设备 + raise + + def _load_dll(self): + """加载SMiTSense DLL并设置函数签名""" + try: + if not os.path.exists(self.dll_path): + raise FileNotFoundError(f"DLL文件未找到: {self.dll_path}") + + # 加载DLL + self.dll = ctypes.WinDLL(self.dll_path) + logger.info(f"成功加载DLL: {self.dll_path}") + + # 设置函数签名(基于testsmit.py的工作代码) + self.dll.SMiTSenseUsb_Init.argtypes = [ctypes.c_int] + self.dll.SMiTSenseUsb_Init.restype = ctypes.c_int + + self.dll.SMiTSenseUsb_ScanDevices.argtypes = [ctypes.POINTER(ctypes.c_int)] + self.dll.SMiTSenseUsb_ScanDevices.restype = ctypes.c_int + + self.dll.SMiTSenseUsb_OpenAndStart.argtypes = [ + ctypes.c_int, + ctypes.POINTER(ctypes.c_uint16), + ctypes.POINTER(ctypes.c_uint16) + ] + self.dll.SMiTSenseUsb_OpenAndStart.restype = ctypes.c_int + + self.dll.SMiTSenseUsb_GetLatestFrame.argtypes = [ + ctypes.POINTER(ctypes.c_uint16), + ctypes.c_int + ] + self.dll.SMiTSenseUsb_GetLatestFrame.restype = ctypes.c_int + + self.dll.SMiTSenseUsb_StopAndClose.argtypes = [] + self.dll.SMiTSenseUsb_StopAndClose.restype = ctypes.c_int + + logger.info("DLL函数签名设置完成") + + except Exception as e: + logger.error(f"加载DLL失败: {e}") + raise + + def _initialize_device(self): + """初始化设备连接""" + try: + # 初始化USB连接 + ret = self.dll.SMiTSenseUsb_Init(0) + if ret != 0: + raise RuntimeError(f"USB初始化失败: {ret}") + + # 扫描设备 + count = ctypes.c_int() + ret = self.dll.SMiTSenseUsb_ScanDevices(ctypes.byref(count)) + if ret != 0 or count.value == 0: + raise RuntimeError(f"设备扫描失败或未找到设备: {ret}, count: {count.value}") + + logger.info(f"发现 {count.value} 个SMiTSense设备") + + # 打开并启动第一个设备 + rows = ctypes.c_uint16() + cols = ctypes.c_uint16() + ret = self.dll.SMiTSenseUsb_OpenAndStart(0, ctypes.byref(rows), ctypes.byref(cols)) + if ret != 0: + raise RuntimeError(f"设备启动失败: {ret}") + + self.rows = rows.value + self.cols = cols.value + self.frame_size = self.rows * self.cols + self.buf_type = ctypes.c_uint16 * self.frame_size + self.buf = self.buf_type() + self.is_connected = True + + logger.info(f"SMiTSense压力传感器初始化成功: {self.rows}行 x {self.cols}列") + + except Exception as e: + logger.error(f"设备初始化失败: {e}") + raise + + def read_data(self) -> Dict[str, Any]: + """读取压力数据并转换为与MockPressureDevice兼容的格式""" + try: + if not self.is_connected or not self.dll: + logger.error("设备未连接") + return self._get_empty_data() + + # 读取原始压力数据 + ret = self.dll.SMiTSenseUsb_GetLatestFrame(self.buf, self.frame_size) + if ret != 0: + logger.warning(f"读取数据帧失败: {ret}") + return self._get_empty_data() + + # 转换为numpy数组 + raw_data = np.frombuffer(self.buf, dtype=np.uint16).reshape((self.rows, self.cols)) + + # 计算足部区域压力 (基于传感器的实际布局) + foot_zones = self._calculate_foot_pressure_zones(raw_data) + + # 生成压力图像 + pressure_image_base64 = self._generate_pressure_image( + foot_zones['left_front'], + foot_zones['left_rear'], + foot_zones['right_front'], + foot_zones['right_rear'], + raw_data + ) + + return { + 'foot_pressure': { + 'left_front': round(foot_zones['left_front'], 2), + 'left_rear': round(foot_zones['left_rear'], 2), + 'right_front': round(foot_zones['right_front'], 2), + 'right_rear': round(foot_zones['right_rear'], 2), + 'left_total': round(foot_zones['left_total'], 2), + 'right_total': round(foot_zones['right_total'], 2) + }, + 'pressure_image': pressure_image_base64, + 'timestamp': datetime.now().isoformat() + } + + except Exception as e: + logger.error(f"读取压力数据异常: {e}") + return self._get_empty_data() + + def _calculate_foot_pressure_zones(self, raw_data): + """计算足部区域压力,返回百分比: + - 左足、右足:相对于双足总压的百分比 + - 左前、左后:相对于左足总压的百分比 + - 右前、右后:相对于右足总压的百分比 + 基于原始矩阵按行列各等分为四象限(上半部为前、下半部为后,左半部为左、右半部为右)。 + """ + try: + # 防护:空数据 + if raw_data is None: + raise ValueError("raw_data is None") + + # 转为浮点以避免 uint16 溢出 + rd = np.asarray(raw_data, dtype=np.float64) + rows, cols = rd.shape if rd.ndim == 2 else (0, 0) + if rows == 0 or cols == 0: + raise ValueError("raw_data has invalid shape") + + # 行列对半分(上=前,下=后;左=左,右=右) + mid_r = rows // 2 + mid_c = cols // 2 + + # 四象限求和 + left_front = float(np.sum(rd[:mid_r, :mid_c], dtype=np.float64)) + left_rear = float(np.sum(rd[mid_r:, :mid_c], dtype=np.float64)) + right_front = float(np.sum(rd[:mid_r, mid_c:], dtype=np.float64)) + right_rear = float(np.sum(rd[mid_r:, mid_c:], dtype=np.float64)) + + # 绝对总压 + left_total_abs = left_front + left_rear + right_total_abs = right_front + right_rear + total_abs = left_total_abs + right_total_abs + + # 左右足占比(相对于双足总压) + left_total_pct = float((left_total_abs / total_abs * 100) if total_abs > 0 else 0) + right_total_pct = float((right_total_abs / total_abs * 100) if total_abs > 0 else 0) + + # 前后占比(相对于各自单足总压) + left_front_pct = float((left_front / left_total_abs * 100) if left_total_abs > 0 else 0) + left_rear_pct = float((left_rear / left_total_abs * 100) if left_total_abs > 0 else 0) + right_front_pct = float((right_front / right_total_abs * 100) if right_total_abs > 0 else 0) + right_rear_pct = float((right_rear / right_total_abs * 100) if right_total_abs > 0 else 0) + + return { + 'left_front': int(left_front_pct), + 'left_rear': int(left_rear_pct), + 'right_front': int(right_front_pct), + 'right_rear': int(right_rear_pct), + 'left_total': int(left_total_pct), + 'right_total': int(right_total_pct), + 'total_pressure': int(total_abs) + } + except Exception as e: + logger.error(f"计算足部区域压力异常: {e}") + return { + 'left_front': 0, 'left_rear': 0, 'right_front': 0, 'right_rear': 0, + 'left_total': 0, 'right_total': 0, 'total_pressure': 0 + } + + def _generate_pressure_image(self, left_front, left_rear, right_front, right_rear, raw_data=None) -> str: + """生成足部压力图片的base64数据""" + try: + if MATPLOTLIB_AVAILABLE and raw_data is not None: + # 使用原始数据生成更详细的热力图 + return self._generate_heatmap_image(raw_data) + else: + # 降级到简单的区域显示图 + return self._generate_simple_pressure_image(left_front, left_rear, right_front, right_rear) + + except Exception as e: + logger.warning(f"生成压力图片失败: {e}") + return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==" + def _generate_heatmap_image(self, raw_data) -> str: + """生成基于原始数据的热力图(OpenCV实现,固定范围映射,效果与matplotlib一致)""" + try: + import cv2 + import numpy as np + import base64 + from io import BytesIO + from PIL import Image + + # 固定映射范围(与 matplotlib vmin/vmax 一致) + vmin, vmax = 0, 1000 + norm_data = np.clip((raw_data - vmin) / (vmax - vmin) * 255, 0, 255).astype(np.uint8) + + # 应用 jet 颜色映射 + heatmap = cv2.applyColorMap(norm_data, cv2.COLORMAP_JET) + + # OpenCV 生成的是 BGR,转成 RGB + heatmap_rgb = cv2.cvtColor(heatmap, cv2.COLOR_BGR2RGB) + + # 转成 Pillow Image + img = Image.fromarray(heatmap_rgb) + + # 输出为 Base64 PNG + buffer = BytesIO() + img.save(buffer, format="PNG") + buffer.seek(0) + image_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8") + + return f"data:image/png;base64,{image_base64}" + + except Exception as e: + logger.warning(f"生成热力图失败: {e}") + return self._generate_simple_pressure_image(0, 0, 0, 0) + # def _generate_heatmap_image(self, raw_data) -> str: + # """生成基于原始数据的热力图""" + # try: + # import matplotlib + # matplotlib.use('Agg') + # import matplotlib.pyplot as plt + # from io import BytesIO + + # # 参考 tests/testsmit.py 的渲染方式:使用 jet 色图、nearest 插值、固定范围并关闭坐标轴 + # fig, ax = plt.subplots() + # im = ax.imshow(raw_data, cmap='jet', interpolation='nearest', vmin=0, vmax=1000) + # ax.axis('off') + + # # 紧凑布局并导出为 base64 + # from io import BytesIO + # buffer = BytesIO() + # plt.savefig(buffer, format='png', bbox_inches='tight', dpi=100, pad_inches=0, facecolor='black') + # buffer.seek(0) + # image_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8') + # plt.close(fig) + + # return f"data:image/png;base64,{image_base64}" + + # except Exception as e: + # logger.warning(f"生成热力图失败: {e}") + # return self._generate_simple_pressure_image(0, 0, 0, 0) + + def _generate_simple_pressure_image(self, left_front, left_rear, right_front, right_rear) -> str: + """生成简单的足部压力区域图""" + try: + import matplotlib + matplotlib.use('Agg') + import matplotlib.pyplot as plt + import matplotlib.patches as patches + from io import BytesIO + + # 创建图形 + fig, ax = plt.subplots(1, 1, figsize=(6, 8)) + ax.set_xlim(0, 10) + ax.set_ylim(0, 12) + ax.set_aspect('equal') + ax.axis('off') + + # 定义颜色映射 + max_pressure = max(left_front, left_rear, right_front, right_rear) + if max_pressure > 0: + left_front_color = plt.cm.Reds(left_front / max_pressure) + left_rear_color = plt.cm.Reds(left_rear / max_pressure) + right_front_color = plt.cm.Reds(right_front / max_pressure) + right_rear_color = plt.cm.Reds(right_rear / max_pressure) + else: + left_front_color = left_rear_color = right_front_color = right_rear_color = 'lightgray' + + # 绘制足部区域 + left_front_rect = patches.Rectangle((1, 6), 2, 4, linewidth=1, edgecolor='black', facecolor=left_front_color) + left_rear_rect = patches.Rectangle((1, 2), 2, 4, linewidth=1, edgecolor='black', facecolor=left_rear_color) + right_front_rect = patches.Rectangle((7, 6), 2, 4, linewidth=1, edgecolor='black', facecolor=right_front_color) + right_rear_rect = patches.Rectangle((7, 2), 2, 4, linewidth=1, edgecolor='black', facecolor=right_rear_color) + + ax.add_patch(left_front_rect) + ax.add_patch(left_rear_rect) + ax.add_patch(right_front_rect) + ax.add_patch(right_rear_rect) + + # 添加标签 + ax.text(2, 8, f'{left_front:.1f}', ha='center', va='center', fontsize=10, weight='bold') + ax.text(2, 4, f'{left_rear:.1f}', ha='center', va='center', fontsize=10, weight='bold') + ax.text(8, 8, f'{right_front:.1f}', ha='center', va='center', fontsize=10, weight='bold') + ax.text(8, 4, f'{right_rear:.1f}', ha='center', va='center', fontsize=10, weight='bold') + + ax.text(2, 0.5, '左足', ha='center', va='center', fontsize=12, weight='bold') + ax.text(8, 0.5, '右足', ha='center', va='center', fontsize=12, weight='bold') + + # 保存为base64 + buffer = BytesIO() + plt.savefig(buffer, format='png', bbox_inches='tight', dpi=100, facecolor='black') + buffer.seek(0) + image_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8') + plt.close(fig) + + return f"data:image/png;base64,{image_base64}" + + except Exception as e: + logger.warning(f"生成简单压力图片失败: {e}") + return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==" + + def _get_empty_data(self): + """返回空的压力数据""" + return { + 'foot_pressure': { + 'left_front': 0.0, + 'left_rear': 0.0, + 'right_front': 0.0, + 'right_rear': 0.0, + 'left_total': 0.0, + 'right_total': 0.0 + }, + 'pressure_image': "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", + 'timestamp': datetime.now().isoformat() + } + + def close(self): + """显式关闭压力传感器连接""" + try: + if self.is_connected and self.dll: + self.dll.SMiTSenseUsb_StopAndClose() + self.is_connected = False + logger.info('SMiTSense压力传感器连接已关闭') + except Exception as e: + logger.error(f'关闭压力传感器连接异常: {e}') + + def __del__(self): + """析构函数,确保资源清理""" + self.close() class MockPressureDevice: - """模拟压力传感器设备""" + """模拟压力传感器设备,模拟真实SMiTSense设备的行为""" def __init__(self): self.base_pressure = 500 # 基础压力值 self.noise_level = 10 + self.rows = 4 # 模拟传感器矩阵行数 + self.cols = 4 # 模拟传感器矩阵列数 + self.time_offset = np.random.random() * 10 # 随机时间偏移,让每个实例的波形不同 def read_data(self) -> Dict[str, Any]: - """读取压力数据""" - # 模拟各个足部区域的压力值 - left_front = max(0, self.base_pressure * 0.6 + np.random.normal(0, self.noise_level)) - left_rear = max(0, self.base_pressure * 0.4 + np.random.normal(0, self.noise_level)) - right_front = max(0, self.base_pressure * 0.6 + np.random.normal(0, self.noise_level)) - right_rear = max(0, self.base_pressure * 0.4 + np.random.normal(0, self.noise_level)) - - # 计算总压力 - left_total = left_front + left_rear - right_total = right_front + right_rear - - # 生成模拟的足部压力图片(base64格式) - pressure_image_base64 = self._generate_pressure_image(left_front, left_rear, right_front, right_rear) - - return { - 'foot_pressure': { - 'left_front': round(left_front, 2), # 左前足压力 - 'left_rear': round(left_rear, 2), # 左后足压力 - 'right_front': round(right_front, 2), # 右前足压力 - 'right_rear': round(right_rear, 2), # 右后足压力 - 'left_total': round(left_total, 2), # 左足总压力 - 'right_total': round(right_total, 2) # 右足总压力 - }, - 'pressure_image': pressure_image_base64, # 足部压力图片(base64格式) - 'timestamp': datetime.now().isoformat() - } + """读取压力数据,模拟基于矩阵数据的真实设备行为""" + try: + # 生成模拟的传感器矩阵数据 + raw_data = self._generate_simulated_matrix_data() + + # 使用与真实设备相同的计算逻辑 + foot_zones = self._calculate_foot_pressure_zones(raw_data) + + # 生成压力图像 + pressure_image_base64 = self._generate_pressure_image( + foot_zones['left_front'], + foot_zones['left_rear'], + foot_zones['right_front'], + foot_zones['right_rear'], + raw_data + ) + + return { + 'foot_pressure': { + 'left_front': round(foot_zones['left_front'], 2), + 'left_rear': round(foot_zones['left_rear'], 2), + 'right_front': round(foot_zones['right_front'], 2), + 'right_rear': round(foot_zones['right_rear'], 2), + 'left_total': round(foot_zones['left_total'], 2), + 'right_total': round(foot_zones['right_total'], 2) + }, + 'pressure_image': pressure_image_base64, + 'timestamp': datetime.now().isoformat() + } + + except Exception as e: + logger.error(f"模拟压力设备读取数据异常: {e}") + return self._get_empty_data() - def _generate_pressure_image(self, left_front, left_rear, right_front, right_rear) -> str: - return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==" + def _generate_simulated_matrix_data(self): + """生成模拟的传感器矩阵数据,模拟真实的足部压力分布""" + import time + current_time = time.time() + self.time_offset + + # 创建4x4的传感器矩阵 + matrix_data = np.zeros((self.rows, self.cols)) + + # 模拟动态的压力分布,使用正弦波叠加噪声 + for i in range(self.rows): + for j in range(self.cols): + # 基础压力值,根据传感器位置不同 + base_value = self.base_pressure * (0.3 + 0.7 * np.random.random()) + + # 添加时间变化(模拟人体重心变化) + time_variation = np.sin(current_time * 0.5 + i * 0.5 + j * 0.3) * 0.3 + + # 添加噪声 + noise = np.random.normal(0, self.noise_level) + + # 确保压力值非负 + matrix_data[i, j] = max(0, base_value * (1 + time_variation) + noise) + + return matrix_data + + def _calculate_foot_pressure_zones(self, raw_data): + """计算足部区域压力,返回百分比: + - 左足、右足:相对于双足总压的百分比 + - 左前、左后:相对于左足总压的百分比 + - 右前、右后:相对于右足总压的百分比 + 基于原始矩阵按行列各等分为四象限(上半部为前、下半部为后,左半部为左、右半部为右)。 + """ + try: + # 防护:空数据 + if raw_data is None: + raise ValueError("raw_data is None") + + # 转为浮点以避免 uint16 溢出 + rd = np.asarray(raw_data, dtype=np.float64) + rows, cols = rd.shape if rd.ndim == 2 else (0, 0) + if rows == 0 or cols == 0: + raise ValueError("raw_data has invalid shape") + + # 行列对半分(上=前,下=后;左=左,右=右) + mid_r = rows // 2 + mid_c = cols // 2 + + # 四象限求和 + left_front = float(np.sum(rd[:mid_r, :mid_c], dtype=np.float64)) + left_rear = float(np.sum(rd[mid_r:, :mid_c], dtype=np.float64)) + right_front = float(np.sum(rd[:mid_r, mid_c:], dtype=np.float64)) + right_rear = float(np.sum(rd[mid_r:, mid_c:], dtype=np.float64)) + + # 绝对总压 + left_total_abs = left_front + left_rear + right_total_abs = right_front + right_rear + total_abs = left_total_abs + right_total_abs + + # 左右足占比(相对于双足总压) + left_total_pct = float((left_total_abs / total_abs * 100) if total_abs > 0 else 0) + right_total_pct = float((right_total_abs / total_abs * 100) if total_abs > 0 else 0) + + # 前后占比(相对于各自单足总压) + left_front_pct = float((left_front / left_total_abs * 100) if left_total_abs > 0 else 0) + left_rear_pct = float((left_rear / left_total_abs * 100) if left_total_abs > 0 else 0) + right_front_pct = float((right_front / right_total_abs * 100) if right_total_abs > 0 else 0) + right_rear_pct = float((right_rear / right_total_abs * 100) if right_total_abs > 0 else 0) + + return { + 'left_front': left_front_pct, + 'left_rear': left_rear_pct, + 'right_front': right_front_pct, + 'right_rear': right_rear_pct, + 'left_total': left_total_pct, + 'right_total': right_total_pct, + 'total_pressure': float(total_abs) + } + except Exception as e: + logger.error(f"计算足部区域压力异常: {e}") + return { + 'left_front': 0, 'left_rear': 0, 'right_front': 0, 'right_rear': 0, + 'left_total': 0, 'right_total': 0, 'total_pressure': 0 + } + + def _generate_pressure_image(self, left_front, left_rear, right_front, right_rear, raw_data=None) -> str: """生成足部压力图片的base64数据""" try: - import base64 + if MATPLOTLIB_AVAILABLE and raw_data is not None: + # 使用原始数据生成更详细的热力图 + return self._generate_heatmap_image(raw_data) + else: + # 降级到简单的区域显示图 + return self._generate_simple_pressure_image(left_front, left_rear, right_front, right_rear) + + except Exception as e: + logger.warning(f"生成模拟压力图片失败: {e}") + return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==" + + def _generate_heatmap_image(self, raw_data) -> str: + """生成基于原始数据的热力图""" + try: + import matplotlib + matplotlib.use('Agg') + import matplotlib.pyplot as plt from io import BytesIO + + # 参考 tests/testsmit.py 的渲染方式:使用 jet 色图、nearest 插值、固定范围并关闭坐标轴 + fig, ax = plt.subplots() + im = ax.imshow(raw_data, cmap='jet', interpolation='nearest', vmin=0, vmax=1000) + ax.axis('off') + + # 紧凑布局并导出为 base64 + from io import BytesIO + buffer = BytesIO() + plt.savefig(buffer, format='png', bbox_inches='tight', dpi=100, pad_inches=0) + buffer.seek(0) + image_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8') + plt.close(fig) + + return f"data:image/png;base64,{image_base64}" + + except Exception as e: + logger.warning(f"生成热力图失败: {e}") + return self._generate_simple_pressure_image(0, 0, 0, 0) + + def _generate_simple_pressure_image(self, left_front, left_rear, right_front, right_rear) -> str: + """生成简单的足部压力区域图""" + try: import matplotlib matplotlib.use('Agg') # 设置非交互式后端,避免Tkinter错误 import matplotlib.pyplot as plt import matplotlib.patches as patches - import logging + from io import BytesIO # 临时禁用PIL的调试日志 pil_logger = logging.getLogger('PIL') @@ -2259,6 +2924,21 @@ class MockPressureDevice: logger.warning(f"生成压力图片失败: {e}") # 返回一个简单的占位符base64图片 return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==" + + def _get_empty_data(self): + """返回空的压力数据""" + return { + 'foot_pressure': { + 'left_front': 0.0, + 'left_rear': 0.0, + 'right_front': 0.0, + 'right_rear': 0.0, + 'left_total': 0.0, + 'right_total': 0.0 + }, + 'pressure_image': "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", + 'timestamp': datetime.now().isoformat() + } class VideoStreamManager: @@ -2288,15 +2968,21 @@ class VideoStreamManager: def _load_rtsp_config(self): """加载RTSP配置""" + start_time = time.time() + logger.info(f'[TIMING] 开始加载RTSP配置 - {datetime.now().strftime("%H:%M:%S.%f")[:-3]}') + try: config = configparser.ConfigParser() config_path = os.path.join(os.path.dirname(__file__), 'config.ini') config.read(config_path, encoding='utf-8') device_index_str = config.get('DEVICES', 'camera_index', fallback='0') self.device_index = int(device_index_str) if device_index_str else 0 - logger.info(f'视频监控设备配置加载完成,设备号: {self.device_index}') + + end_time = time.time() + logger.info(f'[TIMING] RTSP配置加载完成,设备号: {self.device_index} - 耗时: {(end_time - start_time) * 1000:.2f}ms') except Exception as e: - logger.error(f'视频监控设备配置失败: {e}') + end_time = time.time() + logger.error(f'[TIMING] 视频监控设备配置失败: {e} - 耗时: {(end_time - start_time) * 1000:.2f}ms') self.device_index = None def get_memory_usage(self): @@ -2419,32 +3105,91 @@ class VideoStreamManager: def generate_video_frames(self): """生成视频监控帧""" + t0 = time.time() frame_count = 0 error_count = 0 use_test_mode = False + first_frame_sent = False last_frame_time = time.time() width,height=self.MAX_FRAME_SIZE - logger.debug(f'开始生成视频监控帧,设备号: {self.device_index}') + # logger.info(f'[TIMING] 进入generate_video_frames - {datetime.now().strftime("%H:%M:%S.%f")[:-3]}') try: - cap = cv2.VideoCapture(self.device_index) - if not cap.isOpened(): - logger.debug(f'无法打开视频监控流: {self.device_index},切换到测试模式') + t_open_start = time.time() + # logger.info(f'[TIMING] 开始打开VideoCapture({self.device_index})') + + # 依次尝试不同后端,选择最快可用的(Windows推荐优先MSMF,然后DSHOW) + backends = [ + (cv2.CAP_MSMF, 'MSMF'), + (cv2.CAP_DSHOW, 'DSHOW'), + (cv2.CAP_ANY, 'ANY') + ] + cap = None + selected_backend = None + for api, name in backends: + try: + t_try = time.time() + logger.info(f'[TIMING] 尝试后端: {name}') + tmp = cv2.VideoCapture(self.device_index, api) + create_ms = (time.time() - t_try) * 1000 + # logger.info(f'[TIMING] 后端{name} 创建VideoCapture耗时: {create_ms:.2f}ms') + if tmp.isOpened(): + cap = tmp + selected_backend = name + # logger.info(f'[TIMING] 选择后端{name} 打开成功') + break + else: + tmp.release() + logger.info(f'[TIMING] 后端{name} 打开失败') + except Exception as e: + logger.warning(f'[TIMING] 后端{name} 异常: {e}') + + # logger.info(f'[TIMING] VideoCapture对象创建耗时: {(time.time()-t_open_start)*1000:.2f}ms(选用后端: {selected_backend})') + + t_open_check = time.time() + if cap is None or not cap.isOpened(): + logger.warning(f'[TIMING] 无法打开视频监控流: {self.device_index},切换到测试模式(isOpened检查耗时: {(time.time()-t_open_check)*1000:.2f}ms)') use_test_mode = True if self.socketio: self.socketio.emit('video_status', {'status': 'started', 'message': '使用测试视频源'}) else: - # 最激进的实时优化设置 - cap.set(cv2.CAP_PROP_BUFFERSIZE, 0) # 完全禁用缓冲区 - cap.set(cv2.CAP_PROP_FPS, 60) # 提高帧率到60fps - cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('M', 'J', 'P', 'G')) # MJPEG编码 - # 设置更低的分辨率以减少处理时间 - cap.set(cv2.CAP_PROP_FRAME_WIDTH, width) - cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height) + # 设置相机属性(逐项记录耗时与是否成功) + total_set_start = time.time() + + # 先设置编码 + t_prop = time.time() + ok_fourcc = cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('M','J','P','G')) + # logger.info(f'[TIMING] 设置FOURCC=MJPG 返回: {ok_fourcc} 耗时: {(time.time()-t_prop)*1000:.2f}ms') + + # 再设置分辨率 + t_prop = time.time() + ok_w = cap.set(cv2.CAP_PROP_FRAME_WIDTH, width) + # logger.info(f'[TIMING] 设置宽度={width} 返回: {ok_w} 耗时: {(time.time()-t_prop)*1000:.2f}ms') + + t_prop = time.time() + ok_h = cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height) + # logger.info(f'[TIMING] 设置高度={height} 返回: {ok_h} 耗时: {(time.time()-t_prop)*1000:.2f}ms') + + # 最后设置帧率和缓冲 + t_prop = time.time() + ok_fps = cap.set(cv2.CAP_PROP_FPS, 30) # 先用30fps更兼容 + # logger.info(f'[TIMING] 设置FPS=30 返回: {ok_fps} 耗时: {(time.time()-t_prop)*1000:.2f}ms') + + t_prop = time.time() + ok_buf = cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 使用极小缓冲区(不支持的后端会忽略) + # logger.info(f'[TIMING] 设置BUFFERSIZE=1 返回: {ok_buf} 耗时: {(time.time()-t_prop)*1000:.2f}ms') + + # logger.info(f'[TIMING] 设置相机属性耗时: {(time.time()-total_set_start)*1000:.2f}ms') + + # 拉一帧,触发真实初始化 + t_first_read = time.time() + warmup_ok, _ = cap.read() + # logger.info(f'[TIMING] 首帧读取耗时: {(time.time()-t_first_read)*1000:.2f}ms, 成功: {warmup_ok}') if self.socketio: - self.socketio.emit('video_status', {'status': 'started', 'message': '使用视频监控视频源(激进实时模式)'}) + self.socketio.emit('video_status', {'status': 'started', 'message': f'使用视频监控视频源({selected_backend or "unknown"})'}) self.video_running = True + # logger.info(f'[TIMING] generate_video_frames初始化总耗时: {(time.time()-t0)*1000:.2f}ms') # # 启动帧编码工作线程 # encoding_thread = threading.Thread(target=self.frame_encoding_worker) @@ -2567,6 +3312,9 @@ class VideoStreamManager: 'frame_id': frame_count, 'timestamp': time.time() }) + if not first_frame_sent: + first_frame_sent = True + # logger.info(f'[TIMING] 第一帧已发送 - 总耗时: {(time.time()-t0)*1000:.2f}ms') # 立即释放base64字符串 del jpg_as_text @@ -2602,22 +3350,19 @@ class VideoStreamManager: if self.video_thread and self.video_thread.is_alive(): logger.warning('视频监控线程已在运行') return {'status': 'already_running', 'message': '视频监控已在运行'} - # logger.error(f'视频监控相机未配置2222222222{self.device_index}') - # if not self.device_index: - # logger.error('视频监控相机未配置') - # return {'status': 'error', 'message': '视频监控相机未配置'} - - logger.info(f'视频启动监控线程,设备号: {self.device_index}') - self.video_thread = threading.Thread(target=self.generate_video_frames) - self.video_thread.daemon = True - self.video_thread.start() - self.video_running = True - logger.info('视频监控线程已启动') + t_start = time.time() + logger.info(f'[TIMING] 准备启动视频监控线程,设备号: {self.device_index} - {datetime.now().strftime("%H:%M:%S.%f")[:-3]}') + self.video_thread = threading.Thread(target=self.generate_video_frames, name='VideoStreamThread') + self.video_thread.daemon = True + self.video_thread.start() + self.video_running = True + # logger.info(f'[TIMING] 视频监控线程创建完成,耗时: {(time.time()-t_start)*1000:.2f}ms') + return {'status': 'started', 'message': '视频监控线程已启动'} except Exception as e: - logger.error(f'视频监控线程启动失败: {e}') + logger.error(f'[TIMING] 视频监控线程启动失败: {e}') return {'status': 'error', 'message': f'视频监控线程启动失败: {str(e)}'} def stop_video_stream(self): diff --git a/backend/dll/smitsense/SMiTSenseUsbWrapper.dll b/backend/dll/smitsense/SMiTSenseUsbWrapper.dll new file mode 100644 index 00000000..04eb4e48 Binary files /dev/null and b/backend/dll/smitsense/SMiTSenseUsbWrapper.dll differ diff --git a/backend/testcamera.py b/backend/testcamera.py index d71e229c..263025dc 100644 --- a/backend/testcamera.py +++ b/backend/testcamera.py @@ -30,7 +30,7 @@ class CameraViewer: if __name__ == "__main__": # 修改这里的数字可以切换不同摄像头设备 - viewer = CameraViewer(device_index=0) + viewer = CameraViewer(device_index=3) viewer.start_stream() # import ctypes diff --git a/backend/tests/SMiTSenseUsbWrapper.dll b/backend/tests/SMiTSenseUsbWrapper.dll new file mode 100644 index 00000000..04eb4e48 Binary files /dev/null and b/backend/tests/SMiTSenseUsbWrapper.dll differ diff --git a/backend/tests/smitsense_demo.py b/backend/tests/smitsense_demo.py deleted file mode 100644 index d2a508fa..00000000 --- a/backend/tests/smitsense_demo.py +++ /dev/null @@ -1,194 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -SMiTSense足部压力传感器简单演示程序 -展示如何使用SMiTSenseUsb-F3.0.dll进行基本的压力数据采集 -""" - -import sys -import os -import time -from test_smitsense_dll import SMiTSensePressureSensor - -def simple_pressure_monitor(): - """ - 简单的压力监控演示 - """ - print("SMiTSense足部压力传感器演示") - print("=" * 40) - - # 创建传感器实例 - sensor = SMiTSensePressureSensor() - - try: - # 初始化 - print("正在初始化传感器...") - if not sensor.initialize(): - print("❌ 初始化失败") - return - - # 查找设备 - print("正在查找设备...") - devices = sensor.get_device_list() - if not devices: - print("❌ 未找到SMiTSense设备") - print("请检查:") - print("1. 设备是否正确连接") - print("2. 驱动程序是否已安装") - print("3. 设备是否被其他程序占用") - return - - print(f"✅ 找到 {len(devices)} 个设备") - - # 连接第一个设备 - print("正在连接设备...") - if not sensor.connect(devices[0]): - print("❌ 连接失败") - return - - print("✅ 设备连接成功") - - # 获取灵敏度 - sensitivity = sensor.get_sensitivity() - if sensitivity is not None: - print(f"当前灵敏度: {sensitivity}") - - # 开始监控压力数据 - print("\n开始压力监控(按Ctrl+C停止)...") - print("-" * 60) - print("时间\t\t左前\t左后\t右前\t右后\t总压力") - print("-" * 60) - - count = 0 - while True: - # 读取压力数据 - pressure_data = sensor.read_pressure_data() - - if pressure_data: - # 转换为足部区域数据 - foot_zones = sensor.get_foot_pressure_zones(pressure_data) - - if foot_zones: - timestamp = time.strftime("%H:%M:%S") - print(f"{timestamp}\t{foot_zones['left_front']:.1f}\t" - f"{foot_zones['left_rear']:.1f}\t" - f"{foot_zones['right_front']:.1f}\t" - f"{foot_zones['right_rear']:.1f}\t" - f"{foot_zones['total_pressure']:.1f}") - - # 简单的平衡分析 - if foot_zones['total_pressure'] > 10: # 有压力时才分析 - left_ratio = foot_zones['left_total'] / foot_zones['total_pressure'] - if left_ratio < 0.4: - balance_status = "右倾" - elif left_ratio > 0.6: - balance_status = "左倾" - else: - balance_status = "平衡" - - if count % 10 == 0: # 每10次显示一次平衡状态 - print(f"\t\t\t\t\t\t\t平衡状态: {balance_status}") - else: - print(f"{time.strftime('%H:%M:%S')}\t数据处理失败") - else: - print(f"{time.strftime('%H:%M:%S')}\t无数据") - - count += 1 - time.sleep(0.2) # 5Hz采样率 - - except KeyboardInterrupt: - print("\n\n用户停止监控") - except Exception as e: - print(f"\n❌ 监控过程中发生错误: {e}") - finally: - # 断开连接 - print("正在断开设备连接...") - sensor.disconnect() - print("✅ 设备已断开") - -def test_dll_availability(): - """ - 测试DLL文件是否可用 - """ - print("检查DLL文件可用性...") - - dll_path = os.path.join(os.path.dirname(__file__), 'SMiTSenseUsb-F3.0.dll') - - if not os.path.exists(dll_path): - print(f"❌ DLL文件不存在: {dll_path}") - return False - - try: - sensor = SMiTSensePressureSensor(dll_path) - print("✅ DLL文件加载成功") - - # 测试基本函数调用 - result = sensor.initialize() - print(f"✅ 初始化函数调用成功,返回值: {result}") - - return True - except Exception as e: - print(f"❌ DLL测试失败: {e}") - return False - -def show_usage(): - """ - 显示使用说明 - """ - print(""" -SMiTSense足部压力传感器使用说明 -================================= - -1. 硬件准备: - - 确保SMiTSense足部压力传感器已正确连接到USB端口 - - 安装相应的设备驱动程序 - - 确保设备未被其他程序占用 - -2. 软件准备: - - 确保SMiTSenseUsb-F3.0.dll文件在当前目录中 - - 安装Python 3.6+ - - 确保ctypes库可用(Python标准库) - -3. 运行程序: - python smitsense_demo.py - -4. 功能说明: - - 自动检测并连接SMiTSense设备 - - 实时显示足部各区域压力数据 - - 简单的平衡状态分析 - - 支持Ctrl+C安全退出 - -5. 数据说明: - - 左前/左后/右前/右后:对应足部四个区域的压力值 - - 总压力:所有传感器压力值的总和 - - 平衡状态:基于左右脚压力分布的简单分析 - -6. 故障排除: - - 如果提示"未找到设备",请检查硬件连接和驱动 - - 如果提示"DLL加载失败",请检查DLL文件路径 - - 如果数据异常,请检查传感器校准状态 - -7. 集成到现有系统: - - 可以将SMiTSensePressureSensor类集成到device_manager.py中 - - 替换现有的MockPressureDevice模拟设备 - - 实现真实的压力传感器数据采集 -""") - -if __name__ == "__main__": - if len(sys.argv) > 1 and sys.argv[1] == "--help": - show_usage() - sys.exit(0) - - print("SMiTSense足部压力传感器演示程序") - print("使用 --help 参数查看详细使用说明") - print() - - # 首先测试DLL可用性 - if not test_dll_availability(): - print("\n请检查DLL文件和依赖项后重试") - sys.exit(1) - - print() - - # 运行压力监控演示 - simple_pressure_monitor() \ No newline at end of file diff --git a/backend/tests/testsmit.py b/backend/tests/testsmit.py new file mode 100644 index 00000000..d6a9757e --- /dev/null +++ b/backend/tests/testsmit.py @@ -0,0 +1,85 @@ +import ctypes +import time +import numpy as np +import cv2 + +# === DLL 加载 === +dll = ctypes.WinDLL(r"D:\BodyBalanceEvaluation\backend\tests\SMiTSenseUsbWrapper.dll") + +dll.SMiTSenseUsb_Init.argtypes = [ctypes.c_int] +dll.SMiTSenseUsb_Init.restype = ctypes.c_int + +dll.SMiTSenseUsb_ScanDevices.argtypes = [ctypes.POINTER(ctypes.c_int)] +dll.SMiTSenseUsb_ScanDevices.restype = ctypes.c_int + +dll.SMiTSenseUsb_OpenAndStart.argtypes = [ + ctypes.c_int, + ctypes.POINTER(ctypes.c_uint16), + ctypes.POINTER(ctypes.c_uint16) +] +dll.SMiTSenseUsb_OpenAndStart.restype = ctypes.c_int + +dll.SMiTSenseUsb_GetLatestFrame.argtypes = [ + ctypes.POINTER(ctypes.c_uint16), + ctypes.c_int +] +dll.SMiTSenseUsb_GetLatestFrame.restype = ctypes.c_int + +dll.SMiTSenseUsb_StopAndClose.argtypes = [] +dll.SMiTSenseUsb_StopAndClose.restype = ctypes.c_int + +# === 初始化设备 === +ret = dll.SMiTSenseUsb_Init(0) +if ret != 0: + raise RuntimeError(f"Init failed: {ret}") + +count = ctypes.c_int() +ret = dll.SMiTSenseUsb_ScanDevices(ctypes.byref(count)) +if ret != 0 or count.value == 0: + raise RuntimeError("No devices found") + +rows = ctypes.c_uint16() +cols = ctypes.c_uint16() +ret = dll.SMiTSenseUsb_OpenAndStart(0, ctypes.byref(rows), ctypes.byref(cols)) +if ret != 0: + raise RuntimeError("OpenAndStart failed") + +rows, cols = rows.value, cols.value +frame_size = rows * cols +buf_type = ctypes.c_uint16 * frame_size +buf = buf_type() + +print(f"实时采集: {rows} 行 x {cols} 列") + +# 固定映射范围(和原来 matplotlib 一样) +vmin, vmax = 0, 1000 + +try: + while True: + ret = dll.SMiTSenseUsb_GetLatestFrame(buf, frame_size) + if ret == 0: + data = np.frombuffer(buf, dtype=np.uint16).reshape((rows, cols)) + + # 固定范围映射到 0-255 + norm_data = np.clip((data - vmin) / (vmax - vmin) * 255, 0, 255).astype(np.uint8) + + # 颜色映射(jet) + heatmap = cv2.applyColorMap(norm_data, cv2.COLORMAP_JET) + + # 放大显示(保持 nearest 效果) + heatmap = cv2.resize(heatmap, (cols * 4, rows * 4), interpolation=cv2.INTER_NEAREST) + + # 显示 + cv2.imshow("SMiTSense 足底压力分布", heatmap) + + if cv2.waitKey(1) & 0xFF == 27: # ESC 退出 + break + else: + print("读取数据帧失败") + time.sleep(0.05) # 20 FPS +except KeyboardInterrupt: + pass +finally: + dll.SMiTSenseUsb_StopAndClose() + cv2.destroyAllWindows() + print("设备已关闭") diff --git a/config.ini b/config.ini index 691600d7..1f8f5e91 100644 --- a/config.ini +++ b/config.ini @@ -19,7 +19,7 @@ camera_index = 0 camera_width = 640 camera_height = 480 camera_fps = 30 -imu_port = COM3 +imu_port = COM8 pressure_port = COM4 [DETECTION] diff --git a/frontend/src/renderer/src/views/Detection.vue b/frontend/src/renderer/src/views/Detection.vue index 5b4ebadd..ca0b37f6 100644 --- a/frontend/src/renderer/src/views/Detection.vue +++ b/frontend/src/renderer/src/views/Detection.vue @@ -204,16 +204,16 @@
-
- 左足总压力 - {{ footPressure.left_total - }}% + style="display: flex;justify-content: center;margin-top: 8px;font-size: 18px;width: 470px;margin-left: -85px;"> +
+
左足总压力
+
{{ footPressure.left_total + }}%
-
- 右足总压力 - {{ footPressure.right_total - }}% +
+
右足总压力
+
{{ footPressure.right_total + }}%