From c48cc3eb639f3908286565fee246e75bbb2295af Mon Sep 17 00:00:00 2001 From: jingna <1264204245@qq.com> Date: Thu, 14 Aug 2025 11:33:34 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=B1=E5=BA=A6=E7=9B=B8=E6=9C=BA=E8=B6=B3?= =?UTF-8?q?=E9=83=A8=E5=8E=8B=E5=8A=9B=E8=A7=86=E9=A2=91=E6=8E=A8=E6=B5=81?= =?UTF-8?q?=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + backend/app.py | 8 +- backend/config.ini | 4 +- backend/device_manager.py | 961 ++++++++++++++++-- backend/dll/smitsense/SMiTSenseUsbWrapper.dll | Bin 0 -> 18944 bytes backend/testcamera.py | 2 +- backend/tests/SMiTSenseUsbWrapper.dll | Bin 0 -> 18944 bytes backend/tests/smitsense_demo.py | 194 ---- backend/tests/testsmit.py | 85 ++ config.ini | 2 +- frontend/src/renderer/src/views/Detection.vue | 18 +- 11 files changed, 958 insertions(+), 317 deletions(-) create mode 100644 backend/dll/smitsense/SMiTSenseUsbWrapper.dll create mode 100644 backend/tests/SMiTSenseUsbWrapper.dll delete mode 100644 backend/tests/smitsense_demo.py create mode 100644 backend/tests/testsmit.py 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 0000000000000000000000000000000000000000..04eb4e48a87e2a8a555e52e8525772641def850b GIT binary patch literal 18944 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..04eb4e48a87e2a8a555e52e8525772641def850b GIT binary patch literal 18944 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 literal 0 HcmV?d00001 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 + }}%