diff --git a/backend/app.py b/backend/app.py index 25ee14e1..4bfd7b99 100644 --- a/backend/app.py +++ b/backend/app.py @@ -561,6 +561,37 @@ def calibrate_devices(): logger.error(f'设备校准失败: {e}') return jsonify({'success': False, 'error': str(e)}), 500 +@app.route('/api/devices/calibrate/imu', methods=['POST']) +def calibrate_imu(): + """校准IMU头部姿态传感器""" + try: + if not device_manager: + return jsonify({'success': False, 'error': '设备管理器未初始化'}), 500 + + if not device_manager.device_status.get('imu', False): + return jsonify({'success': False, 'error': 'IMU设备未连接'}), 400 + + # 执行IMU校准 + result = device_manager._calibrate_imu() + + if result.get('status') == 'success': + logger.info('IMU头部姿态校准成功') + return jsonify({ + 'success': True, + 'message': 'IMU头部姿态校准成功,正立状态已设为零位基准', + 'data': result + }) + else: + logger.error(f'IMU校准失败: {result.get("error", "未知错误")}') + return jsonify({ + 'success': False, + 'error': result.get('error', 'IMU校准失败') + }), 500 + + except Exception as e: + logger.error(f'IMU校准异常: {e}') + return jsonify({'success': False, 'error': str(e)}), 500 + # ==================== 视频推流API ==================== @app.route('/api/streaming/start', methods=['POST']) diff --git a/backend/device_manager.py b/backend/device_manager.py index 4521aa75..bdf58e0e 100644 --- a/backend/device_manager.py +++ b/backend/device_manager.py @@ -429,13 +429,25 @@ class DeviceManager: 'z': np.mean([s['gyro']['z'] for s in samples]) } + # 计算头部姿态零点偏移(正立状态为标准零位) + head_pose_offset = { + 'rotation': np.mean([s['head_pose']['rotation'] for s in samples if 'head_pose' in s]), + 'tilt': np.mean([s['head_pose']['tilt'] for s in samples if 'head_pose' in s]), + 'pitch': np.mean([s['head_pose']['pitch'] for s in samples if 'head_pose' in s]) + } + calibration = { 'status': 'success', 'accel_offset': accel_offset, 'gyro_offset': gyro_offset, + 'head_pose_offset': head_pose_offset, # 头部姿态零点偏移 'timestamp': datetime.now().isoformat() } + # 保存校准数据到设备实例 + if hasattr(self.imu_device, 'set_calibration'): + self.imu_device.set_calibration(calibration) + return calibration except Exception as e: @@ -1123,27 +1135,16 @@ class DeviceManager: # 从IMU设备读取数据 imu_data = self.imu_device.read_data() - if imu_data: - # 计算头部姿态角度(欧拉角) - # 这里使用简化的计算方法,实际应用中可能需要更复杂的姿态解算 - accel = imu_data['accel'] - gyro = imu_data['gyro'] + if imu_data and 'head_pose' in imu_data: + # 直接使用设备提供的头部姿态数据 + head_pose = imu_data['head_pose'] - # 计算俯仰角和横滚角(基于加速度计) - import math - pitch = math.atan2(accel['x'], math.sqrt(accel['y']**2 + accel['z']**2)) * 180 / math.pi - roll = math.atan2(accel['y'], math.sqrt(accel['x']**2 + accel['z']**2)) * 180 / math.pi - - # 偏航角需要磁力计数据,这里使用陀螺仪积分(简化处理) - yaw = gyro['z'] * 0.1 # 简化的偏航角计算 - - # 构建头部姿态数据 + # 构建完整的头部姿态数据 head_pose_data = { - 'roll': roll, - 'pitch': pitch, - 'yaw': yaw, - 'acceleration': accel, - 'gyroscope': gyro, + 'rotation': head_pose['rotation'], # 旋转角:左旋(-), 右旋(+) + 'tilt': head_pose['tilt'], # 倾斜角:左倾(-), 右倾(+) + 'pitch': head_pose['pitch'], # 俯仰角:俯角(-), 仰角(+) + 'temperature': imu_data.get('temperature', 25), 'timestamp': imu_data['timestamp'] } @@ -1176,32 +1177,58 @@ class DeviceManager: # 从压力传感器设备读取数据 pressure_data = self.pressure_device.read_data() - if pressure_data: - # 计算平衡相关指标 - left_pressure = pressure_data['left_foot'] - right_pressure = pressure_data['right_foot'] - total_pressure = left_pressure + right_pressure + if pressure_data and 'foot_pressure' in pressure_data: + foot_pressure = pressure_data['foot_pressure'] + + # 获取各区域压力值 + left_front = foot_pressure['left_front'] + left_rear = foot_pressure['left_rear'] + right_front = foot_pressure['right_front'] + right_rear = foot_pressure['right_rear'] + left_total = foot_pressure['left_total'] + right_total = foot_pressure['right_total'] + + # 计算总压力 + total_pressure = left_total + right_total # 计算平衡比例(左脚压力占总压力的比例) - balance_ratio = left_pressure / total_pressure if total_pressure > 0 else 0.5 + balance_ratio = left_total / total_pressure if total_pressure > 0 else 0.5 # 计算压力中心偏移 pressure_center_offset = (balance_ratio - 0.5) * 100 # 转换为百分比 - # 构建足部压力数据 - foot_pressure_data = { - 'left_foot_pressure': left_pressure, - 'right_foot_pressure': right_pressure, - 'total_pressure': total_pressure, - 'balance_ratio': balance_ratio, - 'pressure_center_offset': pressure_center_offset, - 'balance_status': 'balanced' if abs(pressure_center_offset) < 10 else 'unbalanced', + # 计算前后足压力分布 + left_front_ratio = left_front / left_total if left_total > 0 else 0.5 + right_front_ratio = right_front / right_total if right_total > 0 else 0.5 + + # 构建完整的足部压力数据 + complete_pressure_data = { + # 分区压力值 + 'pressure_zones': { + 'left_front': left_front, + 'left_rear': left_rear, + 'right_front': right_front, + 'right_rear': right_rear, + 'left_total': left_total, + 'right_total': right_total, + 'total_pressure': total_pressure + }, + # 平衡分析 + 'balance_analysis': { + 'balance_ratio': round(balance_ratio, 3), + 'pressure_center_offset': round(pressure_center_offset, 2), + 'balance_status': 'balanced' if abs(pressure_center_offset) < 10 else 'unbalanced', + 'left_front_ratio': round(left_front_ratio, 3), + 'right_front_ratio': round(right_front_ratio, 3) + }, + # 压力图片 + 'pressure_image': pressure_data.get('pressure_image', ''), 'timestamp': pressure_data['timestamp'] } # 通过WebSocket发送足部压力数据 self.socketio.emit('pressure_data', { - 'foot_pressure': foot_pressure_data, + 'foot_pressure': complete_pressure_data, 'timestamp': datetime.now().isoformat() }) @@ -1624,23 +1651,59 @@ class MockIMUDevice: def __init__(self): self.noise_level = 0.1 + self.calibration_data = None # 校准数据 + self.head_pose_offset = {'rotation': 0, 'tilt': 0, 'pitch': 0} # 头部姿态零点偏移 + + def set_calibration(self, calibration: Dict[str, Any]): + """设置校准数据""" + self.calibration_data = calibration + if 'head_pose_offset' in calibration: + self.head_pose_offset = calibration['head_pose_offset'] + + def apply_calibration(self, raw_data: Dict[str, Any]) -> Dict[str, Any]: + """应用校准数据""" + if not self.calibration_data: + return raw_data + + # 应用头部姿态零点校准 + if 'head_pose' in raw_data: + raw_data['head_pose']['rotation'] -= self.head_pose_offset['rotation'] + raw_data['head_pose']['tilt'] -= self.head_pose_offset['tilt'] + raw_data['head_pose']['pitch'] -= self.head_pose_offset['pitch'] + + return raw_data def read_data(self) -> Dict[str, Any]: """读取IMU数据""" - return { - 'accel': { - 'x': np.random.normal(0, self.noise_level), - 'y': np.random.normal(0, self.noise_level), - 'z': np.random.normal(9.8, self.noise_level) # 重力加速度 + # 生成头部姿态角度数据,角度范围(-90°, +90°) + # 使用正弦波模拟自然的头部运动,添加随机噪声 + import time + current_time = time.time() + + # 旋转角(左旋为负,右旋为正) + rotation_angle = 30 * np.sin(current_time * 0.5) + np.random.normal(0, self.noise_level * 5) + rotation_angle = np.clip(rotation_angle, -90, 90) + + # 倾斜角(左倾为负,右倾为正) + tilt_angle = 20 * np.sin(current_time * 0.3 + np.pi/4) + np.random.normal(0, self.noise_level * 5) + tilt_angle = np.clip(tilt_angle, -90, 90) + + # 俯仰角(俯角为负,仰角为正) + pitch_angle = 15 * np.sin(current_time * 0.7 + np.pi/2) + np.random.normal(0, self.noise_level * 5) + pitch_angle = np.clip(pitch_angle, -90, 90) + + # 生成原始数据 + raw_data = { + 'head_pose': { + 'rotation': rotation_angle, # 旋转角:左旋(-), 右旋(+) + 'tilt': tilt_angle, # 倾斜角:左倾(-), 右倾(+) + 'pitch': pitch_angle # 俯仰角:俯角(-), 仰角(+) }, - 'gyro': { - 'x': np.random.normal(0, self.noise_level), - 'y': np.random.normal(0, self.noise_level), - 'z': np.random.normal(0, self.noise_level) - }, - 'temperature': np.random.normal(25, 2), 'timestamp': datetime.now().isoformat() } + + # 应用校准并返回 + return self.apply_calibration(raw_data) class MockPressureDevice: @@ -1652,15 +1715,93 @@ class MockPressureDevice: def read_data(self) -> Dict[str, Any]: """读取压力数据""" - # 模拟轻微的左右脚压力差异 - left_pressure = self.base_pressure + np.random.normal(0, self.noise_level) - right_pressure = self.base_pressure + np.random.normal(0, self.noise_level) + # 模拟各个足部区域的压力值 + 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 { - 'left_foot': max(0, left_pressure), - 'right_foot': max(0, right_pressure), + '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() } + + def _generate_pressure_image(self, left_front, left_rear, right_front, right_rear) -> str: + """生成足部压力图片的base64数据""" + try: + import base64 + from io import BytesIO + import matplotlib.pyplot as plt + import matplotlib.patches as patches + + # 创建图形 + 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='white') + 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}") + # 返回一个简单的占位符base64图片 + return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==" class VideoStreamManager: diff --git a/frontend/src/renderer/src/services/api.js b/frontend/src/renderer/src/services/api.js index 8d2c1896..22c23e23 100644 --- a/frontend/src/renderer/src/services/api.js +++ b/frontend/src/renderer/src/services/api.js @@ -112,6 +112,11 @@ export const deviceAPI = { return api.post('/api/devices/calibrate') }, + // 校准IMU头部姿态传感器 + calibrateIMU() { + return api.post('/api/devices/calibrate/imu') + }, + // 测试设备 testDevice() { return api.post('/api/devices/test') diff --git a/frontend/src/renderer/src/views/Detection.vue b/frontend/src/renderer/src/views/Detection.vue index d0bccf91..41f0b015 100644 --- a/frontend/src/renderer/src/views/Detection.vue +++ b/frontend/src/renderer/src/views/Detection.vue @@ -30,7 +30,7 @@ --el-button-border-color: #409EFF; --el-button-border-color: transparent "> 保存数据 - +