diff --git a/README.md b/README.md index 7d4677d2..cdb0baba 100644 --- a/README.md +++ b/README.md @@ -1,458 +1,129 @@ # 身体平衡评估系统 -一个基于多传感器融合技术的专业身体平衡评估与分析系统,为用户提供准确的平衡能力评估和康复指导。 +一个基于多传感器融合的身体平衡评估与分析系统,提供姿态检测、视频录制、数据采集与评估报告等功能。 ## 系统特性 -### 🎯 核心功能 -- **实时姿态检测**: 基于MediaPipe的高精度人体姿态识别 -- **多传感器融合**: 整合摄像头、IMU传感器和压力传感器数据 -- **智能分析引擎**: 多维度平衡能力评估算法 -- **实时视频流**: WebSocket实时视频传输和显示 -- **检测数据采集**: 一键采集当前检测状态的数据快照 -- **视频录制功能**: 支持检测过程的视频录制和回放 -- **可视化报告**: 直观的数据图表和分析报告 -- **历史数据管理**: 完整的检测记录存储和对比分析 +- 实时姿态检测与数据采集 +- 多设备管理(摄像头、IMU、压力传感器) +- 视频录制与文件存储规范化 +- 本地数据库管理与历史记录 +- 静态文件HTTP访问映射 +- 开发与打包环境兼容 +- 数据安全:本地存储,保护用户隐私 -### 🔧 技术特点 -- **现代化架构**: Vue 3 + Python Flask 前后端分离 -- **实时通信**: WebSocket 实时数据传输和视频流 -- **多媒体支持**: 集成视频录制、截图和数据采集功能 -- **跨平台支持**: Windows、macOS、Linux -- **模块化设计**: 清晰的目录结构,易于扩展和维护 -- **数据安全**: 本地存储,保护用户隐私 -- **开发友好**: 独立的前后端开发环境,支持热重载 -- **部署简化**: 一键安装和启动脚本,降低部署复杂度 - -## 系统架构 +## 目录结构(当前) ``` -身体平衡评估系统/ -├── backend/ # 后端服务 -│ ├── main.py # 主启动脚本 -│ ├── app.py # Flask 主应用 -│ ├── database.py # 数据库管理 -│ ├── device_manager.py # 设备管理 -│ ├── detection_engine.py # 检测引擎 -│ ├── data_processor.py # 数据处理 -│ ├── utils.py # 工具函数 -│ └── requirements.txt # Python 依赖 -├── frontend/ # 前端应用 -│ └── src/renderer/ # 前端源码 -│ ├── src/ -│ │ ├── views/ # 页面组件 -│ │ ├── stores/ # 状态管理 -│ │ ├── services/ # API 服务 -│ │ └── router/ # 路由配置 -│ ├── package.json # Node.js 依赖 -│ └── vite.config.js # 构建配置 -├── data/ # 数据目录 -├── logs/ # 日志目录 -├── venv/ # Python 虚拟环境 -├── install.bat # 安装脚本 -├── start_dev.bat # 开发模式启动脚本 -└── start_prod.bat # 生产模式启动脚本 +BodyBalanceEvaluation/ +├── backend/ +│ ├── main.py # AppServer 后端入口(推荐) +│ ├── app.py # 备用后端入口(历史版本) +│ ├── database.py # 数据库管理 +│ ├── device_manager.py # 设备统一管理(旧版) +│ ├── check_monitor_status.py # 设备连接状态检查脚本 +│ ├── build_app.py # 打包相关脚本 +│ ├── config.ini # 后端配置文件(可选) +│ ├── data/ # 默认数据目录(开发环境) +│ ├── devices/ +│ │ ├── camera_manager.py +│ │ ├── screen_recorder.py +│ │ ├── imu_manager.py +│ │ ├── pressure_manager.py +│ │ ├── femtobolt_manager.py +│ │ ├── device_coordinator.py +│ │ └── utils/ +│ │ └── config_manager.py # 配置管理器(设备侧) +│ ├── requirements.txt # 运行依赖 +│ └── requirements_build.txt # 打包依赖 +├── frontend/ +│ └── src/renderer/ # Electron + 前端资源 +├── config.ini # 顶层配置(可选,优先就近) +├── data/ +│ └── patients/ # 示例数据目录 +└── README.md ``` -## 快速开始 +## 安装与运行 -### 环境要求 +### 后端(开发) -- **Python**: 3.8 或更高版本 -- **Node.js**: 16.0 或更高版本 (开发模式) -- **操作系统**: Windows 10/11, macOS 10.15+, Ubuntu 18.04+ +- 创建并激活虚拟环境 + - `python -m venv venv` + - `venv\Scripts\activate`(Windows) +- 安装依赖 + - `pip install -r backend/requirements.txt` +- 启动后端服务(推荐入口) + - `python backend/main.py --host 0.0.0.0 --port 5000 --debug` -### 安装步骤 +说明:`main.py` 使用 `AppServer` 类,启动时会初始化 `ConfigManager`、数据库管理器、设备协调器及相关路由。 -#### 方式一:一键安装 (Windows 推荐) -1. **克隆项目** - ```bash - git clone - cd BodyBalanceEvaluation - ``` +### 前端与打包 -2. **运行安装脚本** - ```bash - install.bat - ``` - - 安装脚本会自动完成: - - 检查 Python 和 Node.js 环境 - - 创建 Python 虚拟环境 - - 安装后端依赖 - - 安装前端依赖 - - 创建必要的目录结构 +- Electron 在 `frontend/src/renderer/main/main.js` 中启动本地静态服务器,并在打包环境下拉起后端可执行文件: + - `resources/backend/BodyBalanceBackend/BodyBalanceBackend.exe` +- 前端调用后端 API 与静态文件路由相互独立。 -#### 方式二:手动安装 +## 配置说明 -1. **克隆项目** - ```bash - git clone - cd BodyBalanceEvaluation - ``` +系统配置由设备侧 `ConfigManager` 统一读取(`backend/devices/utils/config_manager.py`)。常用节: -2. **创建虚拟环境** - ```bash - python -m venv venv - venv\Scripts\activate # Windows - # source venv/bin/activate # macOS/Linux - ``` +- `[FILEPATH]` + - `path`:文件存储根目录。支持绝对路径或相对路径。 + - 绝对路径示例:`D:/BodyCheck/file` + - 相对路径示例:`data` 或 `../data`(相对 `main.py` 所在目录或打包 `exe` 同级目录) +- `[DATABASE]` + - `path`:数据库文件路径(支持相对/绝对)。 -3. **安装 Python 依赖** - ```bash - pip install -r backend/requirements.txt - ``` +示例(`backend/config.ini` 或项目根 `config.ini`): -4. **安装前端依赖** (开发模式) - ```bash - cd frontend/src/renderer - npm install - cd ../../.. - ``` +``` +[FILEPATH] +path = D:/BodyCheck/file -### 启动应用程序 - - **Windows 用户 (推荐)**: - ```bash - # 一键安装 (首次使用) - install.bat - - # 开发模式 - start_dev.bat - - # 生产模式 - start_prod.bat - ``` - - **手动启动**: - ```bash - # 激活虚拟环境 - venv\Scripts\activate - - # 进入后端目录 - cd backend - - # 开发模式 - python main.py --mode development - - # 生产模式 - python main.py --mode production - ``` - -### 命令行参数 - -```bash -cd backend -python main.py [选项] - -选项: - --mode {development,production} 运行模式 (默认: development) - --host HOST 服务器主机 (默认: 127.0.0.1) - --port PORT 服务器端口 (默认: 5000) - --no-browser 不自动打开浏览器 - --log-level {DEBUG,INFO,WARNING,ERROR} 日志级别 (默认: INFO) +[DATABASE] +path = data/body_balance.db ``` -## 使用指南 +## 静态文件访问 -### 1. 系统设置 +- 后端提供静态文件映射路由(`backend/main.py`): + - `GET /` + - 访问路径会在服务端拼接至 `[FILEPATH].path` 指定的存储根目录下。 + - 例如:配置 `path = D:/BodyCheck/file`,则访问 `http://host:port/202508190001/20251014111329/video_111331358/screen.mp4` + 会映射到 `D:/BodyCheck/file/202508190001/20251014111329/video_111331358/screen.mp4`。 +- 安全校验: + - 路径规范化与越界拦截(拒绝 `..`、绝对路径、UNC 路径)。 + - 仅允许访问 `[FILEPATH].path` 根目录内的资源。 + - 视频类文件设置正确的 `Content-Type` 与 `Accept-Ranges`(流式播放友好)。 -首次使用前,请进入「系统设置」页面配置: +注:如果需要固定前缀路由(如 `/data/`),可以在 `main.py` 中将路由前缀改为 `/data`,逻辑保持不变。 -- **设备配置**: 选择摄像头、配置串口设备 -- **检测参数**: 设置默认检测时长、采样频率等 -- **数据管理**: 配置数据存储路径和清理策略 +## 录制与数据存储 -### 2. 患者管理 +- `screen_recorder.py` 在类初始化时注入 `ConfigManager`,并在录制/采集流程中统一从 `[FILEPATH].path` 读取文件根目录。 +- 目录规范建议:`///`,业务层可按需要扩展子目录与文件命名。 +- 当配置为相对路径时,开发环境相对 `backend/` 目录,打包环境相对 `exe` 同级目录。 -- 添加患者基本信息(姓名、年龄、性别等) -- 记录患者病史和康复目标 -- 管理患者档案和检测记录 +## 主要 API(摘录) -### 3. 姿态检测 +- `GET /health`:健康检查 +- `POST /api/detection/start`:开始检测(创建会话) +- `POST /api/detection//stop`:停止检测并保存录制内容 +- `GET /`:静态文件映射至 `[FILEPATH].path` -1. **选择患者**: 从患者列表中选择或新建患者 -2. **设备准备**: 确保摄像头和传感器正常连接 -3. **开始检测**: 点击开始按钮进行实时检测 -4. **实时监控**: 通过实时视频流观察检测过程 -5. **数据采集**: 在检测过程中点击"检测数据采集"按钮获取当前状态数据 -6. **视频录制**: 使用"开始录制"/"停止录制"按钮记录检测过程 -7. **停止检测**: 完成检测并自动计算检测时长 -8. **查看结果**: 检测完成后查看分析结果和建议 +说明:更多设备管理、状态广播与检测过程接口,参见 `backend/main.py` 内路由与 SocketIO 事件注册。 -### 4. 数据分析 +## 开发建议 -- **单次分析**: 查看单次检测的详细数据和图表 -- **对比分析**: 比较多次检测结果,观察变化趋势 -- **报告生成**: 生成 PDF 格式的专业评估报告 -- **数据导出**: 导出原始数据用于进一步分析 +- 统一通过 `ConfigManager` 访问配置,避免硬编码路径。 +- 读取与写入文件路径时使用 `os.path.normpath/abspath/realpath` 进行规范化。 +- 对外提供文件访问统一通过静态路由,确保安全校验与越界防护。 +- 开发与打包环境下路径基础目录不同,尽量通过配置与规范化处理屏蔽差异。 -### 5. 历史记录 +## 数据安全 -- 浏览所有历史检测记录 -- 按患者、日期、状态等条件筛选 -- 批量导出和删除操作 -- 时间线视图查看检测历史 - -## 设备支持 - -### 摄像头 -- USB 摄像头 (推荐 1080p 30fps) -- 内置摄像头 -- 网络摄像头 - -### IMU 传感器 -- 支持串口通信的 9 轴 IMU -- 波特率: 9600, 115200, 230400 -- 数据格式: 加速度、陀螺仪、磁力计 - -### 压力传感器 -- 多点压力传感器阵列 -- 串口通信接口 -- 支持 1-16 个传感器点 - -## 开发指南 - -### 后端开发 - -后端使用 Flask 框架,主要模块: - -- `main.py`: 主启动脚本和进程管理 -- `app.py`: 主应用和 API 路由 -- `database.py`: SQLite 数据库操作 -- `device_manager.py`: 硬件设备管理和视频流处理 -- `detection_engine.py`: 检测算法引擎 -- `data_processor.py`: 数据分析和处理 - -### 主要API端点 - -- `POST /api/detection/start`: 开始检测会话 -- `POST /api/detection/{session_id}/stop`: 停止检测会话 -- `POST /api/detection/{session_id}/collect`: 采集检测数据 -- `WebSocket /ws`: 实时数据和视频流传输 - -### 前端开发 - -前端使用 Vue 3 + Element Plus,主要特性: - -- **组合式 API**: 使用 Vue 3 Composition API -- **状态管理**: Pinia 状态管理库 -- **UI 组件**: Element Plus 组件库 -- **图表库**: ECharts 数据可视化 -- **构建工具**: Vite 快速构建 - -### 添加新功能 - -1. **后端 API**: - ```python - @app.route('/api/new-feature', methods=['POST']) - def new_feature(): - # 实现新功能逻辑 - return jsonify({'status': 'success'}) - ``` - -2. **前端服务**: - ```javascript - // frontend/src/renderer/src/services/api.js - export const newFeatureAPI = { - doSomething: (data) => api.post('/new-feature', data) - } - ``` - -3. **前端组件**: - ```vue - - - - ``` - -### 项目结构优势 - -新的项目结构带来以下优势: - -1. **清晰分离**: 前后端代码完全分离,便于团队协作开发 -2. **独立部署**: 前后端可以独立部署和扩展 -3. **开发效率**: 前后端可以并行开发,提高开发效率 -4. **维护性**: 模块化结构便于代码维护和功能扩展 -5. **版本管理**: 前后端可以独立进行版本控制 -6. **技术栈**: 前后端可以选择最适合的技术栈 - -### 开发环境配置 - -**后端开发**: -```bash -# 激活虚拟环境 -venv\Scripts\activate - -# 进入后端目录 -cd backend - -# 启动开发服务器 -python main.py --mode development --log-level DEBUG -``` -python debug_server.py - -**前端开发**: -```bash -# 进入前端目录 -cd frontend/src/renderer - -# 启动开发服务器 -npm run dev -``` - -**同时开发** (推荐): -```bash -# 使用开发脚本同时启动前后端 -start_dev.bat -``` - -## 故障排除 - -### 常见问题 - -**Q: 摄像头无法识别** -A: 检查摄像头连接,确保没有被其他应用占用,在设备设置中刷新摄像头列表。 - -**Q: 传感器连接失败** -A: 确认串口设置正确,检查设备驱动是否安装,尝试不同的波特率设置。 - -**Q: 前端页面无法加载** -A: 检查后端服务是否正常启动,确认防火墙设置,查看浏览器控制台错误信息。 - -**Q: 检测结果不准确** -A: 确保设备已正确校准,检查环境光线条件,调整检测参数设置。 - -**Q: 视频录制失败** -A: 检查磁盘空间是否充足,确认录制权限设置,查看后端日志中的错误信息。 - -**Q: WebSocket连接断开** -A: 检查网络连接稳定性,确认防火墙未阻止WebSocket连接,尝试刷新页面重新连接。 - -### 日志查看 - -系统日志保存在 `logs/` 目录下: - -- `app.log`: 应用程序主日志 -- `device.log`: 设备管理日志 -- `detection.log`: 检测引擎日志 -- `error.log`: 错误日志 - -### 性能优化 - -1. **硬件要求**: - - CPU: Intel i5 或同等性能 - - 内存: 8GB RAM - - 存储: 10GB 可用空间 - -2. **软件优化**: - - 关闭不必要的后台程序 - - 使用 SSD 存储提高 I/O 性能 - - 定期清理历史数据和日志 - -## 数据格式 - -### 检测数据结构 - -```json -{ - "session_id": "uuid", - "patient_id": "patient_uuid", - "timestamp": "2024-01-01T12:00:00Z", - "duration": 60, - "data": { - "camera": { - "landmarks": [...], - "confidence": 0.95 - }, - "imu": { - "acceleration": [x, y, z], - "gyroscope": [x, y, z], - "magnetometer": [x, y, z] - }, - "pressure": { - "sensors": [p1, p2, p3, p4], - "center_of_pressure": [x, y] - } - }, - "recording": { - "video_path": "path/to/video.mp4", - "screenshots": ["path/to/screenshot1.png"] - } -} -``` - -### 分析结果格式 - -```json -{ - "session_id": "uuid", - "analysis_time": "2024-01-01T12:01:00Z", - "overall_assessment": "good", - "balance_score": 85, - "posture_score": 78, - "metrics": { - "sway_area": 2.5, - "sway_velocity": 1.2, - "postural_stability": 0.85 - }, - "recommendations": [ - "建议加强核心肌群训练", - "注意保持正确站姿" - ] -} -``` - -## 许可证 - -本项目采用 MIT 许可证。详见 [LICENSE](LICENSE) 文件。 - -## 贡献指南 - -欢迎贡献代码!请遵循以下步骤: - -1. Fork 本仓库 -2. 创建特性分支 (`git checkout -b feature/AmazingFeature`) -3. 提交更改 (`git commit -m 'Add some AmazingFeature'`) -4. 推送到分支 (`git push origin feature/AmazingFeature`) -5. 开启 Pull Request - -## 支持 - -如果您遇到问题或有建议,请: - -- 查看 [FAQ](docs/FAQ.md) -- 提交 [Issue](issues) -- 发送邮件至: support@example.com - -## 更新日志 - -### v1.2.0 (2024-01-20) -- **检测功能增强**: 新增检测数据采集功能,支持实时数据快照 -- **视频录制**: 集成视频录制功能,支持检测过程的完整记录 -- **实时视频流**: 优化WebSocket视频传输,提供流畅的实时监控 -- **API优化**: 重构检测相关API,使用RESTful设计模式 -- **用户体验**: 改进检测界面,添加录制控制和数据采集按钮 -- **生命周期管理**: 完善组件生命周期处理,确保资源正确释放 - -### v1.1.0 (2024-01-15) -- **项目重构**: 前后端完全分离,优化项目结构 -- **新增脚本**: 添加一键安装和启动脚本 (install.bat, start_dev.bat, start_prod.bat) -- **开发体验**: 改进开发环境配置,支持独立的前后端开发 -- **文档更新**: 完善 README 文档,添加详细的安装和使用说明 -- **路径优化**: 统一使用虚拟环境,规范化依赖管理 - -### v1.0.0 (2024-01-01) -- 初始版本发布 -- 基础检测功能 -- 患者管理系统 -- 数据分析和报告生成 - ---- - -**身体平衡评估系统** - 专业的平衡能力评估解决方案 \ No newline at end of file +- 所有数据采用本地存储,避免敏感信息外泄。 +- 静态文件访问包含越界保护,限制访问至配置的存储根目录内。 +- 建议对患者身份信息进行匿名化处理(如ID映射)。 \ No newline at end of file diff --git a/backend/app.py b/backend/app.py deleted file mode 100644 index 66dfca7f..00000000 --- a/backend/app.py +++ /dev/null @@ -1,1221 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -平衡体态检测系统 - 后端服务 -基于Flask的本地API服务 -""" - -import os -import sys -import json -import time -import threading -from datetime import datetime -from flask import Flask, jsonify, send_file -from flask import request as flask_request -from flask_cors import CORS -import sqlite3 -import logging -from pathlib import Path -from flask_socketio import SocketIO, emit -import cv2 -import configparser -import os - -# 添加当前目录到Python路径 -sys.path.append(os.path.dirname(os.path.abspath(__file__))) - -# 导入自定义模块 -from database import DatabaseManager -from device_manager import DeviceManager, VideoStreamManager -from devices.screen_recorder import RecordingManager -from devices.camera_manager import CameraManager -from utils import config as app_config - -# 确定日志文件路径 -if getattr(sys, 'frozen', False): - # 如果是打包后的exe,日志文件在exe同目录下的logs文件夹 - log_dir = os.path.join(os.path.dirname(sys.executable), 'logs') -else: - # 如果是开发环境,使用当前目录的logs文件夹 - log_dir = 'logs' - -# 确保日志目录存在 -os.makedirs(log_dir, exist_ok=True) -log_file = os.path.join(log_dir, 'backend.log') - -# 配置日志 -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - handlers=[ - logging.FileHandler(log_file, encoding='utf-8'), - logging.StreamHandler() - ] -) -logger = logging.getLogger(__name__) - -# 创建Flask应用 -app = Flask(__name__) -app.config['SECRET_KEY'] = 'body-balance-detection-system-2024' - -# 初始化SocketIO -try: - socketio = SocketIO( - app, - cors_allowed_origins='*', - async_mode='threading', - logger=False, - engineio_logger=False, - ping_timeout=60, - ping_interval=25, - manage_session=False, - always_connect=False, - transports=['polling'], # 只使用polling,避免打包环境websocket问题 - allow_upgrades=False, # 禁用升级到websocket - cookie=None # 禁用cookie - ) - - logger.info('SocketIO初始化成功') -except Exception as e: - logger.error(f'SocketIO初始化失败: {e}') - socketio = None - -import logging -logging.getLogger('socketio').setLevel(logging.WARNING) -logging.getLogger('engineio').setLevel(logging.WARNING) - -# 启用CORS支持 -CORS(app, origins='*', supports_credentials=True, allow_headers=['Content-Type', 'Authorization'], methods=['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']) - -# 注册Blueprint(如需要可在此处添加) - -# 读取RTSP配置 -config = configparser.ConfigParser() - -# 确定配置文件路径 -if getattr(sys, 'frozen', False): - # 如果是打包后的exe,配置文件在exe同目录下 - config_path = os.path.join(os.path.dirname(sys.executable), 'config.ini') -else: - # 如果是开发环境,配置文件在上级目录 - config_path = os.path.join(os.path.dirname(__file__), 'config.ini') - -config.read(config_path, encoding='utf-8') -device_index = config.get('CAMERA', 'device_index', fallback=None) - -# 全局变量 -db_manager = None -device_manager = None -current_detection = None -detection_thread = None -video_stream_manager = None -recording_manager = None -camera_manager = None - - - -def init_app(): - """初始化应用""" - global db_manager, device_manager, video_stream_manager - - try: - # 确定基础目录 - if getattr(sys, 'frozen', False): - # 如果是打包后的exe,使用exe同目录 - base_dir = os.path.dirname(sys.executable) - else: - # 如果是开发环境,使用当前脚本目录 - base_dir = os.path.dirname(os.path.abspath(__file__)) - - # 创建必要的目录 - logs_dir = os.path.join(base_dir, 'logs') - data_dir = os.path.join(base_dir, 'data') - os.makedirs(logs_dir, exist_ok=True) - os.makedirs(data_dir, exist_ok=True) - - # 从配置文件读取数据库路径 - db_path_config = app_config.get('DATABASE', 'path', 'data/body_balance.db') - # 如果是相对路径,基于基础目录解析 - if not os.path.isabs(db_path_config): - # 如果配置路径以 'backend/' 开头,去掉这个前缀 - if db_path_config.startswith('backend/'): - db_path_config = db_path_config[8:] # 去掉 'backend/' 前缀 - db_path = os.path.join(base_dir, db_path_config) - else: - db_path = db_path_config - - # 确保数据库目录存在 - db_dir = os.path.dirname(db_path) - os.makedirs(db_dir, exist_ok=True) - - # 输出数据库路径信息 - print(f"\n=== 系统初始化 ===") - print(f"数据库配置路径: {db_path_config}") - print(f"数据库实际路径: {db_path}") - print(f"数据库目录: {db_dir}") - print(f"当前工作目录: {os.getcwd()}") - print(f"数据库文件存在: {'是' if os.path.exists(db_path) else '否'}") - print(f"==================\n") - - # 初始化数据库 - db_manager = DatabaseManager(db_path) - db_manager.init_database() - - # 初始化设备管理器(不自动初始化设备) - device_manager = DeviceManager(db_manager, recording_manager) - - # 初始化相机管理器 - global camera_manager, recording_manager - camera_manager = CameraManager() - - # 初始化录制管理器 - recording_manager = RecordingManager(camera_manager=camera_manager, db_manager=db_manager) - - if socketio is not None: - 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 - - # 可选:在后台线程中初始化设备,避免阻塞应用启动 - def init_devices_async(): - try: - - device_manager._init_devices() - logger.info('后台设备初始化完成') - except Exception as e: - logger.error(f'后台设备初始化失败: {e}') - - # 启动后台设备初始化线程 - device_init_thread = threading.Thread(target=init_devices_async, daemon=True) - device_init_thread.start() - socketio.run( - app, - host='0.0.0.0', # 允许所有IP访问 - port=5000, - debug=True, - use_reloader=False, # 禁用热重载以避免FemtoBolt设备资源冲突 - log_output=True, # 输出详细日志 - allow_unsafe_werkzeug=True - ) - logger.info('应用初始化完成') - - except Exception as e: - logger.error(f'应用初始化失败: {e}') - logger.warning('部分功能可能不可用,但服务将继续运行') - # 不再抛出异常,让应用继续运行 - -# ==================== 基础API ==================== - -@app.route('/health', methods=['GET']) -def health_check(): - """健康检查接口""" - return jsonify({ - 'status': 'healthy', - 'timestamp': datetime.now().isoformat(), - 'version': '1.0.0' - }) - -@app.route('/test-socketio') -def test_socketio(): - """SocketIO连接测试页面""" - return send_file('test_socketio_connection.html') - -@app.route('/api/health', methods=['GET']) -def api_health_check(): - """API健康检查""" - return jsonify({ - 'status': 'ok', - 'timestamp': datetime.now().isoformat(), - 'version': '1.0.0' - }) - -# ==================== 认证API ==================== - -@app.route('/api/auth/login', methods=['POST']) -def login(): - """用户登录""" - try: - data = flask_request.get_json() - username = data.get('username') - password = data.get('password') - if not username or not password: - return jsonify({ - 'success': False, - 'message': '用户名或密码不能为空' - }), 400 - - # 使用数据库验证用户 - user = db_manager.authenticate_user(username, password) - - if user: - # 检查用户是否已激活 - if not user['is_active']: - return jsonify({ - 'success': False, - 'message': '账户未激活,请联系管理员审核' - }), 403 - - # 构建用户数据 - user_data = { - 'id': user['id'], - 'username': user['username'], - 'name': user['name'], - 'role': 'admin' if user['user_type'] == 'admin' else 'user', - 'user_type': user['user_type'], - 'avatar': '' - } - - # 生成token(实际项目中应使用JWT等安全token) - token = f"token_{username}_{int(time.time())}" - - logger.info(f'用户 {username} 登录成功') - - return jsonify({ - 'success': True, - 'data': { - 'token': token, - 'user': user_data - }, - 'message': '登录成功' - }) - else: - logger.warning(f'用户 {username} 登录失败:用户名或密码错误') - return jsonify({ - 'success': False, - 'message': '用户名或密码错误' - }), 401 - - except Exception as e: - logger.error(f'登录失败: {e}') - return jsonify({'success': False, 'message': '登录失败'}), 500 - - -@app.route('/api/auth/register', methods=['POST']) -def register(): - """用户注册""" - try: - data = flask_request.get_json() - username = data.get('username') - password = data.get('password') - name = data.get('name') or data.get('email', '') - phone = data.get('phone') - - if not username or not password: - return jsonify({ - 'success': False, - 'message': '用户名和密码不能为空' - }), 400 - - if len(password) < 6: - return jsonify({ - 'success': False, - 'message': '密码长度不能少于6位' - }), 400 - - # 构建用户数据字典 - user_data = { - 'username': username, - 'password': password, - 'name': name, - 'phone': phone - } - - # 使用数据库注册用户 - result = db_manager.register_user(user_data) - - if result['success']: - logger.info(f'用户 {username} 注册成功,等待管理员审核') - return jsonify({ - 'success': True, - 'message': '注册成功,请等待管理员审核后登录' - }) - else: - return jsonify({ - 'success': False, - 'message': result['message'] - }), 400 - - except Exception as e: - logger.error(f'注册失败: {e}') - return jsonify({'success': False, 'message': '注册失败'}), 500 - -@app.route('/api/auth/logout', methods=['POST']) -def logout(): - """用户退出""" - try: - return jsonify({ - 'success': True, - 'message': '退出成功' - }) - except Exception as e: - logger.error(f'退出失败: {e}') - return jsonify({'success': False, 'message': '退出失败'}), 500 - -@app.route('/api/auth/verify', methods=['GET']) -def verify_token(): - """验证token""" - try: - # 简单的token验证(实际项目中应验证JWT等) - auth_header = flask_request.headers.get('Authorization') - if auth_header and auth_header.startswith('Bearer '): - token = auth_header.split(' ')[1] - # 这里可以添加真实的token验证逻辑 - return jsonify({ - 'success': True, - 'data': {'valid': True} - }) - else: - return jsonify({ - 'success': True, - 'data': {'valid': True} # 暂时总是返回有效 - }) - except Exception as e: - logger.error(f'验证token失败: {e}') - return jsonify({'success': False, 'message': '验证失败'}), 500 - -@app.route('/api/auth/forgot-password', methods=['POST']) -def forgot_password(): - """忘记密码 - 根据用户名和手机号找回密码""" - try: - data = flask_request.get_json() - username = data.get('username') - phone = data.get('phone') - - if not username: - return jsonify({ - 'success': False, - 'error': '请输入用户名' - }), 400 - - if not phone: - return jsonify({ - 'success': False, - 'error': '请输入手机号码' - }), 400 - - # 验证手机号格式 - import re - phone_pattern = r'^1[3-9]\d{9}$' - if not re.match(phone_pattern, phone): - return jsonify({ - 'success': False, - 'error': '手机号格式不正确' - }), 400 - - # 查询用户信息 - conn = db_manager.get_connection() - cursor = conn.cursor() - - cursor.execute(''' - SELECT username, password, phone FROM users - WHERE username = ? AND phone = ? - ''', (username, phone)) - - user = cursor.fetchone() - - if user: - # 用户存在且手机号匹配,返回数据库中存储的实际密码 - actual_password = user['password'] - - logger.info(f'用户 {username} 密码查询成功') - - return jsonify({ - 'success': True, - 'password': actual_password, # 返回数据库中存储的实际密码 - 'message': '密码找回成功' - }) - else: - # 检查用户是否存在 - cursor.execute('SELECT username FROM users WHERE username = ?', (username,)) - user_exists = cursor.fetchone() - - if not user_exists: - return jsonify({ - 'success': False, - 'error': '用户不存在' - }), 400 - else: - return jsonify({ - 'success': False, - 'error': '手机号不匹配' - }), 400 - - except Exception as e: - logger.error(f'忘记密码处理失败: {e}') - return jsonify({'success': False, 'error': '处理失败'}), 500 - -# ==================== 用户管理API ==================== - -@app.route('/api/users', methods=['GET']) -def get_users(): - """获取用户列表(管理员功能)""" - try: - # 这里应该验证管理员权限 - page = int(flask_request.args.get('page', 1)) - size = int(flask_request.args.get('size', 10)) - status = flask_request.args.get('status') # active, inactive, all - - users = db_manager.get_users(page, size, status) - total = db_manager.get_users_count(status) - - return jsonify({ - 'success': True, - 'data': { - 'users': users, - 'total': total, - 'page': page, - 'size': size - } - }) - - except Exception as e: - logger.error(f'获取用户列表失败: {e}') - return jsonify({'success': False, 'message': '获取用户列表失败'}), 500 - -@app.route('/api/users//approve', methods=['POST']) -def approve_user(user_id): - """审核用户(管理员功能)""" - try: - # 这里应该验证管理员权限 - data = flask_request.get_json() - approve = data.get('approve', True) - - result = db_manager.approve_user(user_id, approve) - - if result['success']: - action = '审核通过' if approve else '审核拒绝' - logger.info(f'用户 {user_id} {action}') - return jsonify({ - 'success': True, - 'message': f'用户{action}成功' - }) - else: - return jsonify({ - 'success': False, - 'message': result['message'] - }), 400 - - except Exception as e: - logger.error(f'审核用户失败: {e}') - return jsonify({'success': False, 'message': '审核用户失败'}), 500 - -@app.route('/api/users/', methods=['DELETE']) -def delete_user(user_id): - """删除用户(管理员功能)""" - try: - # 这里应该验证管理员权限 - result = db_manager.delete_user(user_id) - - if result['success']: - logger.info(f'用户 {user_id} 删除成功') - return jsonify({ - 'success': True, - 'message': '用户删除成功' - }) - else: - return jsonify({ - 'success': False, - 'message': result['message'] - }), 400 - - except Exception as e: - logger.error(f'删除用户失败: {e}') - return jsonify({'success': False, 'message': '删除用户失败'}), 500 - - -@app.route('/api/system/info', methods=['GET']) -def get_system_info(): - """获取系统信息""" - try: - info = { - 'version': '1.0.0', - 'start_time': datetime.now().isoformat(), - 'database_status': 'connected' if db_manager else 'disconnected', - 'device_count': len(device_manager.get_connected_devices()) if device_manager else 0 - } - return jsonify({'success': True, 'data': info}) - except Exception as e: - logger.error(f'获取系统信息失败: {e}') - return jsonify({'success': False, 'error': str(e)}), 500 - - -# ==================== 患者管理API ==================== - -@app.route('/api/patients', methods=['GET']) -def get_patients(): - """获取患者列表""" - try: - page = int(flask_request.args.get('page', 1)) - size = int(flask_request.args.get('size', 10)) - keyword = flask_request.args.get('keyword', '') - - patients = db_manager.get_patients(page, size, keyword) - total = db_manager.get_patients_count(keyword) - - return jsonify({ - 'success': True, - 'data': { - 'patients': patients, - 'total': total, - 'page': page, - 'size': size - } - }) - except Exception as e: - logger.error(f'获取患者列表失败: {e}') - return jsonify({'success': False, 'error': str(e)}), 500 - -@app.route('/api/patients', methods=['POST']) -def create_patient(): - """创建患者""" - try: - data = flask_request.get_json() - patient_id = db_manager.create_patient(data) - return jsonify({ - 'success': True, - 'data': {'patient_id': patient_id}, - 'message': '患者创建成功' - }) - except Exception as e: - logger.error(f'创建患者失败: {e}') - return jsonify({'success': False, 'error': str(e)}), 500 - -@app.route('/api/patients/', methods=['PUT']) -def update_patient(patient_id): - """更新患者信息""" - try: - data = flask_request.get_json() - db_manager.update_patient(patient_id, data) - return jsonify({'success': True, 'message': '患者信息更新成功'}) - except Exception as e: - logger.error(f'更新患者信息失败: {e}') - return jsonify({'success': False, 'error': str(e)}), 500 - -@app.route('/api/patients/', methods=['GET']) -def get_patient(patient_id): - """获取单个患者信息""" - try: - patient = db_manager.get_patient(patient_id) - if patient: - return jsonify({'success': True, 'data': patient}) - else: - return jsonify({'success': False, 'error': '患者不存在'}), 404 - except Exception as e: - logger.error(f'获取患者信息失败: {e}') - return jsonify({'success': False, 'error': str(e)}), 500 - -@app.route('/api/patients/', methods=['DELETE']) -def delete_patient(patient_id): - """删除患者""" - try: - db_manager.delete_patient(patient_id) - return jsonify({'success': True, 'message': '患者删除成功'}) - except Exception as e: - logger.error(f'删除患者失败: {e}') - return jsonify({'success': False, 'error': str(e)}), 500 - -# ==================== 设备管理API ==================== - -@app.route('/api/devices/status', methods=['GET']) -def get_device_status(): - """获取设备状态""" - try: - if not device_manager: - return jsonify({'camera': False, 'imu': False, 'pressure': False}) - - status = device_manager.get_device_status() - return jsonify(status) - except Exception as e: - logger.error(f'获取设备状态失败: {e}') - return jsonify({'camera': False, 'imu': False, 'pressure': False}) - -@app.route('/api/devices/refresh', methods=['POST']) -def refresh_devices(): - """刷新设备连接""" - try: - if device_manager: - device_manager.refresh_devices() - return jsonify({'success': True, 'message': '设备已刷新'}) - except Exception as e: - logger.error(f'刷新设备失败: {e}') - return jsonify({'success': False, 'error': str(e)}), 500 - -@app.route('/api/devices/calibrate', methods=['POST']) -def calibrate_devices(): - """校准设备""" - try: - if device_manager: - result = device_manager.calibrate_devices() - return jsonify({'success': True, 'data': result}) - return jsonify({'success': False, 'error': '设备管理器未初始化'}), 500 - except Exception as e: - 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/detection/start', methods=['POST']) -def start_detection(): - """开始检测""" - try: - if not db_manager or not device_manager: - return jsonify({'success': False, 'error': '数据库管理器或设备管理器未初始化'}), 500 - - data = flask_request.get_json() - patient_id = data.get('patient_id') - creator_id = data.get('creator_id') - if not patient_id or not creator_id: - return jsonify({'success': False, 'error': '缺少患者ID或创建人ID'}), 400 - - # 调用create_detection_session方法,settings传空字典 - session_id = db_manager.create_detection_session(patient_id, settings={}, creator_id=creator_id) - - # 开始同步录制 - recording_response = None - try: - recording_response = recording_manager.start_recording(session_id, patient_id) - except Exception as rec_e: - logger.error(f'开始同步录制失败: {rec_e}') - - start_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - - return jsonify({'success': True, 'session_id': session_id, 'detectionStartTime': start_time, 'recording': recording_response}) - except Exception as e: - logger.error(f'开始检测失败: {e}') - return jsonify({'success': False, 'error': str(e)}), 500 - -@app.route('/api/detection//stop', methods=['POST']) -def stop_detection(session_id): - """停止检测""" - try: - if not db_manager or not recording_manager: - logger.error('数据库管理器或录制管理器未初始化') - return jsonify({'success': False, 'error': '数据库管理器或录制管理器未初始化'}), 500 - - if not session_id: - logger.error('缺少会话ID') - return jsonify({ - 'success': False, - 'error': '缺少会话ID' - }), 400 - - data = flask_request.get_json() - # logger.debug(f'接收到停止检测请求,session_id: {session_id}, 请求数据: {data}') - # video_data = data.get('videoData') if data else None - video_data = data['videoData'] - mime_type = data.get('mimeType', 'video/webm;codecs=vp9') # 默认webm格式 - import base64 - # 验证base64视频数据格式 - if not video_data.startswith('data:video/'): - return jsonify({ - 'success': False, - 'message': '无效的视频数据格式' - }), 400 - try: - header, encoded = video_data.split(',', 1) - video_bytes = base64.b64decode(encoded) - # with open(r'D:/111.webm', 'wb') as f: - # f.write(video_bytes) - except Exception as e: - return jsonify({ - 'success': False, - 'message': f'视频数据解码失败: {str(e)}' - }), 400 - # 停止同步录制 - try: - # 使用新的录制管理器停止录制 - stop_result = recording_manager.stop_recording() - logger.info(f'录制停止结果: {stop_result}') - - # 处理前端传来的视频数据(如果需要保存) - if video_bytes: - # 可以在这里添加保存前端视频数据的逻辑 - logger.info(f'接收到前端视频数据,大小: {len(video_bytes)} 字节') - - except Exception as rec_e: - logger.error(f'停止同步录制失败: {rec_e}', exc_info=True) - raise - - # 更新会话状态为已完成 - success = db_manager.update_session_status(session_id, 'completed') - - if success: - logger.info(f'检测会话已停止 - 会话ID: {session_id}') - return jsonify({ - 'success': True, - 'message': '检测已停止' - }) - else: - logger.error('停止检测失败,更新会话状态失败') - return jsonify({ - 'success': False, - 'error': '停止检测失败' - }), 500 - - except Exception as e: - logger.error(f'停止检测失败: {e}', exc_info=True) - return jsonify({'success': False, 'error': str(e)}), 500 - -@app.route('/api/detection//status', methods=['GET']) -def get_detection_status(session_id): - """获取检测状态""" - try: - if not db_manager: - return jsonify({'success': False, 'error': '数据库管理器未初始化'}), 500 - - if not session_id: - return jsonify({ - 'success': False, - 'error': '缺少会话ID' - }), 400 - - # 获取会话数据 - session_data = db_manager.get_session_data(session_id) - - if session_data: - return jsonify({ - 'success': True, - 'data': session_data - }) - else: - return jsonify({ - 'success': False, - 'error': '会话不存在' - }), 404 - - except Exception as e: - logger.error(f'获取检测状态失败: {e}') - return jsonify({'success': False, 'error': str(e)}), 500 - -@app.route('/api/detection//save-info', methods=['POST']) -def save_session_info(session_id): - """保存会话信息(诊断、处理、建议、状态)""" - try: - if not db_manager: - return jsonify({'success': False, 'error': '数据库管理器未初始化'}), 500 - - if not session_id: - return jsonify({ - 'success': False, - 'error': '缺少会话ID' - }), 400 - - # 获取请求数据 - data = flask_request.get_json() or {} - diagnosis_info = data.get('diagnosis_info') - treatment_info = data.get('treatment_info') - suggestion_info = data.get('suggestion_info') - status = data.get('status') - - # 验证至少提供一个要更新的字段 - if not any([diagnosis_info, treatment_info, suggestion_info, status]): - return jsonify({ - 'success': False, - 'error': '至少需要提供一个要更新的字段(diagnosis_info, treatment_info, suggestion_info, status)' - }), 400 - - # 调用数据库管理器的批量更新方法 - db_manager.update_session_all_info( - session_id=session_id, - diagnosis_info=diagnosis_info, - treatment_info=treatment_info, - suggestion_info=suggestion_info, - status=status - ) - - # 构建更新信息反馈 - updated_fields = [] - if diagnosis_info is not None: - updated_fields.append('诊断信息') - if treatment_info is not None: - updated_fields.append('处理信息') - if suggestion_info is not None: - updated_fields.append('建议信息') - if status is not None: - updated_fields.append(f'状态({status})') - - logger.info(f'会话信息保存成功: {session_id}, 更新字段: {", ".join(updated_fields)}') - - return jsonify({ - 'success': True, - 'message': f'会话信息保存成功,更新字段: {", ".join(updated_fields)}', - 'data': { - 'session_id': session_id, - 'updated_fields': updated_fields - } - }) - - except Exception as e: - logger.error(f'保存会话信息失败: {e}') - return jsonify({'success': False, 'error': str(e)}), 500 - -@app.route('/api/detection//collect', methods=['POST']) -def collect_detection_data(session_id): - """采集检测数据""" - try: - if not db_manager: - return jsonify({'success': False, 'error': '数据库管理器未初始化'}), 500 - - if not device_manager: - return jsonify({'success': False, 'error': '设备管理器未初始化'}), 500 - - # 获取请求数据 - data = flask_request.get_json() or {} - patient_id = data.get('patient_id') - - # 如果没有提供patient_id,从会话信息中获取 - if not patient_id: - session_data = db_manager.get_session_data(session_id) - if not session_data: - return jsonify({ - 'success': False, - 'error': '检测会话不存在' - }), 404 - patient_id = session_data.get('patient_id') - - if not patient_id: - return jsonify({ - 'success': False, - 'error': '无法获取患者ID' - }), 400 - - # 调用录制管理器采集数据 - collected_data = recording_manager.collect_detection_data( - session_id=session_id, - patient_id=patient_id - ) - - # 将采集的数据保存到数据库 - if collected_data: - db_manager.save_detection_data(session_id, collected_data) - logger.info(f'检测数据采集并保存成功: {session_id}') - - return jsonify({ - 'success': True, - 'data': { - 'session_id': session_id, - 'timestamp': collected_data.get('timestamp'), - 'data_collected': bool(collected_data) - }, - 'message': '数据采集成功' - }) - - except Exception as e: - logger.error(f'采集检测数据失败: {e}') - return jsonify({'success': False, 'error': str(e)}), 500 - - -@app.route('/api/history/sessions', methods=['GET']) -def get_detection_sessions(): - """获取检测会话历史""" - try: - page = int(flask_request.args.get('page', 1)) - size = int(flask_request.args.get('size', 10)) - patient_id = flask_request.args.get('patient_id') - - sessions = db_manager.get_detection_sessions(page, size, patient_id) - total = db_manager.get_sessions_count(patient_id) - - return jsonify({ - 'success': True, - 'data': { - 'sessions': sessions, - 'total': total, - 'page': page, - 'size': size - } - }) - - except Exception as e: - logger.error(f'获取检测历史失败: {e}') - return jsonify({'success': False, 'error': str(e)}), 500 - - -# ==================== WebSocket 事件处理 ==================== - -# 只有当socketio不为None时才注册事件处理器 -if socketio is not None: - # 简单的测试事件处理器 - @socketio.on('connect') - def handle_connect(): - # print('CLIENT CONNECTED!!!', flush=True) # 控制台打印测试已关闭 - logger.info('客户端已连接') - return True - - @socketio.on('disconnect') - def handle_disconnect(): - # print('CLIENT DISCONNECTED!!!', flush=True) # 控制台打印测试已关闭 - logger.info('客户端已断开连接') - - # 原始的start_video处理逻辑(暂时注释) - @socketio.on('start_video_stream') - def handle_start_video(data=None): - try: - results = {'status': 'success', 'cameras': {}} - - logger.info(f'video_stream_manager状态: {video_stream_manager is not None}') - logger.info(f'device_manager状态: {device_manager is not None}') - - # 启动视频流管理器(普通摄像头) - if video_stream_manager: - 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'[TIMING] start_video_stream返回(耗时: {(time.time()-t_vs)*1000:.2f}ms): {video_result}') - results['cameras']['normal'] = video_result - else: - logger.error('视频流管理器未初始化') - results['cameras']['normal'] = {'status': 'error', 'message': '视频流管理器未初始化'} - - # 启动FemtoBolt深度相机 - if device_manager: - logger.info('正在启动FemtoBolt深度相机...') - femtobolt_result = device_manager.start_femtobolt_stream() - logger.info(f'FemtoBolt启动结果: {femtobolt_result}') - if femtobolt_result: - results['cameras']['femtobolt'] = {'status': 'success', 'message': 'FemtoBolt深度相机推流已启动'} - else: - results['cameras']['femtobolt'] = {'status': 'error', 'message': 'FemtoBolt深度相机启动失败'} - else: - logger.error('设备管理器未初始化') - results['cameras']['femtobolt'] = {'status': 'error', 'message': '设备管理器未初始化'} - - # 检查是否有任何相机启动成功 - success_count = sum(1 for cam_result in results['cameras'].values() if cam_result.get('status') == 'success') - if success_count == 0: - results['status'] = 'error' - results['message'] = '所有相机启动失败' - elif success_count < len(results['cameras']): - results['status'] = 'partial' - results['message'] = f'{success_count}/{len(results["cameras"])}个相机启动成功' - else: - results['message'] = '所有相机启动成功' - - logger.info(f'发送video_status事件: {results}') - try: - emit('video_status', results) - except Exception as emit_error: - logger.error(f'发送video_status事件失败: {emit_error}') - - except Exception as e: - logger.error(f'启动视频流失败: {e}', exc_info=True) - try: - emit('video_status', {'status': 'error', 'message': f'启动失败: {str(e)}'}) - except Exception as emit_error: - logger.error(f'发送错误状态失败: {emit_error}') - -@socketio.on('stop_video_stream') -def handle_stop_video(data=None): - logger.info(f'收到stop_video事件,数据: {data}') - - try: - results = {'status': 'success', 'cameras': {}} - - # 停止视频流管理器(普通摄像头) - if video_stream_manager: - video_result = video_stream_manager.stop_video_stream() - results['cameras']['normal'] = video_result - else: - results['cameras']['normal'] = {'status': 'error', 'message': '视频流管理器未初始化'} - - # 停止FemtoBolt深度相机 - if device_manager: - device_manager.stop_femtobolt_stream() - results['cameras']['femtobolt'] = {'status': 'success', 'message': 'FemtoBolt深度相机推流已停止'} - else: - results['cameras']['femtobolt'] = {'status': 'error', 'message': '设备管理器未初始化'} - - # 检查是否有任何相机停止成功 - success_count = sum(1 for cam_result in results['cameras'].values() if cam_result.get('status') == 'success') - if success_count == 0: - results['status'] = 'error' - results['message'] = '所有相机停止失败' - elif success_count < len(results['cameras']): - results['status'] = 'partial' - results['message'] = f'{success_count}/{len(results["cameras"])}个相机停止成功' - else: - results['message'] = '所有相机停止成功' - - try: - emit('video_status', results) - except Exception as emit_error: - logger.error(f'发送video_status事件失败: {emit_error}') - - except Exception as e: - logger.error(f'停止视频流失败: {e}') - try: - emit('video_status', {'status': 'error', 'message': f'停止失败: {str(e)}'}) - except Exception as emit_error: - logger.error(f'发送错误状态失败: {emit_error}') - -@socketio.on('start_imu_streaming') -def handle_start_imu_streaming(data=None): - """启动IMU头部姿态数据推流""" - try: - if device_manager: - result = device_manager.start_imu_streaming() - if result: - try: - emit('imu_status', {'status': 'success', 'message': 'IMU头部姿态数据推流已启动'}) - except Exception as emit_error: - logger.error(f'发送IMU状态失败: {emit_error}') - logger.info('IMU头部姿态数据推流已启动') - else: - try: - emit('imu_status', {'status': 'error', 'message': 'IMU头部姿态数据推流启动失败'}) - except Exception as emit_error: - logger.error(f'发送IMU状态失败: {emit_error}') - logger.error('IMU头部姿态数据推流启动失败') - else: - try: - emit('imu_status', {'status': 'error', 'message': '设备管理器未初始化'}) - except Exception as emit_error: - logger.error(f'发送IMU状态失败: {emit_error}') - logger.error('设备管理器未初始化') - except Exception as e: - logger.error(f'启动IMU数据推流失败: {e}') - try: - emit('imu_status', {'status': 'error', 'message': f'启动失败: {str(e)}'}) - except Exception as emit_error: - logger.error(f'发送IMU状态失败: {emit_error}') - -@socketio.on('stop_imu_streaming') -def handle_stop_imu_streaming(data=None): - """停止IMU头部姿态数据推流""" - try: - if device_manager: - result = device_manager.stop_imu_streaming() - if result: - try: - emit('imu_status', {'status': 'success', 'message': 'IMU头部姿态数据推流已停止'}) - except Exception as emit_error: - logger.error(f'发送IMU状态失败: {emit_error}') - logger.info('IMU头部姿态数据推流已停止') - else: - try: - emit('imu_status', {'status': 'error', 'message': 'IMU头部姿态数据推流停止失败'}) - except Exception as emit_error: - logger.error(f'发送IMU状态失败: {emit_error}') - logger.error('IMU头部姿态数据推流停止失败') - else: - try: - emit('imu_status', {'status': 'error', 'message': '设备管理器未初始化'}) - except Exception as emit_error: - logger.error(f'发送IMU状态失败: {emit_error}') - logger.error('设备管理器未初始化') - except Exception as e: - logger.error(f'停止IMU数据推流失败: {e}') - try: - emit('imu_status', {'status': 'error', 'message': f'停止失败: {str(e)}'}) - except Exception as emit_error: - logger.error(f'发送IMU状态失败: {emit_error}') - -@socketio.on('start_pressure_streaming') -def handle_start_pressure_streaming(data=None): - """启动压力传感器足部压力数据推流""" - try: - if device_manager: - result = device_manager.start_pressure_streaming() - if result: - emit('pressure_status', {'status': 'success', 'message': '压力传感器足部压力数据推流已启动'}) - logger.info('压力传感器足部压力数据推流已启动') - else: - emit('pressure_status', {'status': 'error', 'message': '压力传感器足部压力数据推流启动失败'}) - logger.error('压力传感器足部压力数据推流启动失败') - else: - emit('pressure_status', {'status': 'error', 'message': '设备管理器未初始化'}) - logger.error('设备管理器未初始化') - except Exception as e: - logger.error(f'启动压力传感器数据推流失败: {e}') - emit('pressure_status', {'status': 'error', 'message': f'启动失败: {str(e)}'}) - -@socketio.on('stop_pressure_streaming') -def handle_stop_pressure_streaming(data=None): - """停止压力传感器足部压力数据推流""" - try: - if device_manager: - result = device_manager.stop_pressure_streaming() - if result: - emit('pressure_status', {'status': 'success', 'message': '压力传感器足部压力数据推流已停止'}) - logger.info('压力传感器足部压力数据推流已停止') - else: - emit('pressure_status', {'status': 'error', 'message': '压力传感器足部压力数据推流停止失败'}) - logger.error('压力传感器足部压力数据推流停止失败') - else: - emit('pressure_status', {'status': 'error', 'message': '设备管理器未初始化'}) - logger.error('设备管理器未初始化') - except Exception as e: - logger.error(f'停止压力传感器数据推流失败: {e}') - emit('pressure_status', {'status': 'error', 'message': f'停止失败: {str(e)}'}) - -# ==================== 错误处理 ==================== - -@app.errorhandler(404) -def not_found(error): - return jsonify({'success': False, 'error': 'API接口不存在'}), 404 - -@app.errorhandler(500) -def internal_error(error): - return jsonify({'success': False, 'error': '服务器内部错误'}), 500 - -if __name__ == '__main__': - import argparse - - # 解析命令行参数 - parser = argparse.ArgumentParser(description='Body Balance Evaluation System Backend') - parser.add_argument('--host', default=None, help='Host address to bind to') - parser.add_argument('--port', type=int, default=None, help='Port number to bind to') - parser.add_argument('--debug', action='store_true', help='Enable debug mode') - args = parser.parse_args() - - try: - # 初始化应用 - init_app() - - except KeyboardInterrupt: - logger.info('服务被用户中断') - except Exception as e: - logger.error(f'服务启动失败: {e}') - sys.exit(1) - finally: - logger.info('后端服务已停止') \ No newline at end of file diff --git a/backend/app.spec b/backend/app.spec deleted file mode 100644 index a89d3d93..00000000 --- a/backend/app.spec +++ /dev/null @@ -1,118 +0,0 @@ - -# -*- mode: python ; coding: utf-8 -*- - -block_cipher = None - -a = Analysis( - ['main.py'], - pathex=['D:/Trae_space/pyKinectAzure'], - binaries=[ - # FemtoBolt相关库文件 - ('dll/femtobolt/k4a.dll', 'dll/femtobolt'), # K4A动态库 - ('dll/femtobolt/k4arecord.dll', 'dll/femtobolt'), # K4A录制库 - ('dll/femtobolt/depthengine_2_0.dll', 'dll/femtobolt'), # 深度引擎 - ('dll/femtobolt/OrbbecSDK.dll', 'dll/femtobolt'), # Orbbec SDK - ('dll/femtobolt/k4a.lib', 'dll/femtobolt'), # K4A静态库 - ('dll/femtobolt/k4arecord.lib', 'dll/femtobolt'), # K4A录制静态库 - ('dll/femtobolt/k4arecorder.exe', 'dll/femtobolt'), # K4A录制工具 - ('dll/femtobolt/k4aviewer.exe', 'dll/femtobolt'), # K4A查看器 - # SMiTSense相关库文件 - ('dll/smitsense/SMiTSenseUsb-F3.0.dll', 'dll/smitsense'), # SMiTSense传感器库 - ('dll/smitsense/Wrapper.dll', 'dll/smitsense'), # SMiTSense传感器库包装类 - ], - hiddenimports=[ - 'flask', - 'flask_socketio', - 'flask_cors', - 'cv2', - 'numpy', - 'pandas', - 'scipy', - 'matplotlib', - 'seaborn', - 'sklearn', - 'PIL', - 'reportlab', - 'sqlite3', - 'configparser', - 'logging', - 'threading', - 'queue', - 'base64', - 'psutil', - 'pykinect_azure', - 'pykinect_azure.k4a', - 'pykinect_azure.k4abt', - 'pykinect_azure.k4arecord', - 'pykinect_azure.pykinect', - 'pykinect_azure.utils', - 'pykinect_azure._k4a', - 'pykinect_azure._k4abt', - 'pyserial', - 'requests', - 'yaml', - 'click', - 'colorama', - 'tqdm', - 'database', - 'device_manager', - 'utils', - 'eventlet', - 'socketio', - 'engineio', - 'engineio.async_drivers.threading', - 'engineio.async_drivers.eventlet', - 'engineio.async_eventlet', - 'socketio.async_eventlet', - 'greenlet', - 'gevent', - 'gevent.socket', - 'gevent.select', - 'dns', - 'dns.resolver', - 'dns.reversename', - 'dns.e164', - ], - hookspath=[], - hooksconfig={}, - runtime_hooks=[], - excludes=[], - win_no_prefer_redirects=False, - win_private_assemblies=False, - cipher=block_cipher, - noarchive=False, -) - -pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) - -exe = EXE( - pyz, - a.scripts, - [], - exclude_binaries=True, - name='BodyBalanceBackend', - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - upx_exclude=[], - runtime_tmpdir=None, - console=True, - disable_windowed_traceback=False, - argv_emulation=False, - target_arch=None, - codesign_identity=None, - entitlements_file=None, - icon=None -) - -coll = COLLECT( - exe, - a.binaries, - a.zipfiles, - a.datas, - strip=False, - upx=True, - upx_exclude=[], - name='BodyBalanceBackend' -) diff --git a/backend/check_monitor_status.py b/backend/check_monitor_status.py deleted file mode 100644 index 2fe2e0e3..00000000 --- a/backend/check_monitor_status.py +++ /dev/null @@ -1,186 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -检查设备连接监控线程状态的测试脚本 -""" - -import sys -import os -import threading -import time -from typing import Dict, Any - -# 添加项目路径 -sys.path.append(os.path.dirname(os.path.abspath(__file__))) - -from devices.utils.config_manager import ConfigManager -from devices.camera_manager import CameraManager -from devices.imu_manager import IMUManager -from devices.pressure_manager import PressureManager -from devices.femtobolt_manager import FemtoBoltManager - -class MockCameraManager(CameraManager): - """模拟摄像头管理器,用于测试监控线程""" - - def __init__(self, socketio, config_manager): - super().__init__(socketio, config_manager) - self._mock_hardware_connected = True - - def check_hardware_connection(self) -> bool: - """模拟硬件连接检查""" - return self._mock_hardware_connected - - def set_mock_hardware_status(self, connected: bool): - """设置模拟硬件连接状态""" - self._mock_hardware_connected = connected - -def check_device_monitor_status(device_manager, device_name: str): - """ - 检查单个设备的监控线程状态 - - Args: - device_manager: 设备管理器实例 - device_name: 设备名称 - """ - print(f"\n=== {device_name.upper()} 设备监控状态检查 ===") - - # 检查基本状态 - print(f"设备连接状态: {device_manager.is_connected}") - print(f"设备流状态: {device_manager.is_streaming}") - - # 检查监控线程相关属性 - if hasattr(device_manager, '_connection_monitor_thread'): - monitor_thread = device_manager._connection_monitor_thread - print(f"监控线程对象: {monitor_thread}") - - if monitor_thread: - print(f"监控线程存活状态: {monitor_thread.is_alive()}") - print(f"监控线程名称: {monitor_thread.name}") - print(f"监控线程守护状态: {monitor_thread.daemon}") - else: - print("监控线程对象: None") - else: - print("设备管理器没有监控线程属性") - - # 检查监控停止事件 - if hasattr(device_manager, '_monitor_stop_event'): - stop_event = device_manager._monitor_stop_event - print(f"监控停止事件: {stop_event}") - print(f"监控停止事件状态: {stop_event.is_set()}") - else: - print("设备管理器没有监控停止事件属性") - - # 检查监控配置 - if hasattr(device_manager, '_connection_check_interval'): - print(f"连接检查间隔: {device_manager._connection_check_interval}秒") - - if hasattr(device_manager, '_connection_timeout'): - print(f"连接超时时间: {device_manager._connection_timeout}秒") - - # 检查心跳时间 - if hasattr(device_manager, '_last_heartbeat'): - last_heartbeat = device_manager._last_heartbeat - current_time = time.time() - heartbeat_age = current_time - last_heartbeat - print(f"上次心跳时间: {time.ctime(last_heartbeat)}") - print(f"心跳间隔: {heartbeat_age:.2f}秒前") - -def main(): - """ - 主函数:检查所有设备的监控状态 - """ - print("设备连接监控状态检查工具") - print("=" * 50) - - try: - # 初始化配置管理器 - config_manager = ConfigManager() - - # 创建模拟设备管理器实例 - mock_camera = MockCameraManager(None, config_manager) - - print("\n=== 初始状态检查 ===") - check_device_monitor_status(mock_camera, 'mock_camera') - - print("\n=== 系统线程信息 ===") - active_threads = threading.active_count() - print(f"当前活跃线程数: {active_threads}") - - print("\n活跃线程列表:") - for thread in threading.enumerate(): - print(f" - {thread.name} (守护: {thread.daemon}, 存活: {thread.is_alive()})") - - # 测试连接监控启动 - print("\n=== 测试连接监控启动 ===") - - print("\n1. 设置模拟硬件为连接状态...") - mock_camera.set_mock_hardware_status(True) - - print("\n2. 设置设备为连接状态...") - mock_camera.set_connected(True) - time.sleep(0.5) # 等待线程启动 - - print("\n连接后的监控状态:") - check_device_monitor_status(mock_camera, 'mock_camera') - - print("\n=== 系统线程信息 (启动监控后) ===") - active_threads = threading.active_count() - print(f"当前活跃线程数: {active_threads}") - - print("\n活跃线程列表:") - for thread in threading.enumerate(): - print(f" - {thread.name} (守护: {thread.daemon}, 存活: {thread.is_alive()})") - - print("\n3. 等待3秒观察监控线程工作...") - time.sleep(3) - - print("\n监控运行中的状态:") - check_device_monitor_status(mock_camera, 'mock_camera') - - print("\n4. 模拟硬件断开...") - mock_camera.set_mock_hardware_status(False) - time.sleep(6) # 等待监控检测到断开(检查间隔是5秒) - - print("\n硬件断开后的监控状态:") - check_device_monitor_status(mock_camera, 'mock_camera') - - print("\n5. 重新连接测试...") - mock_camera.set_mock_hardware_status(True) - mock_camera.set_connected(True) - time.sleep(0.5) - - print("\n重新连接后的监控状态:") - check_device_monitor_status(mock_camera, 'mock_camera') - - print("\n6. 手动断开连接...") - mock_camera.set_connected(False) - time.sleep(0.5) - - print("\n手动断开后的监控状态:") - check_device_monitor_status(mock_camera, 'mock_camera') - - print("\n=== 最终系统线程信息 ===") - active_threads = threading.active_count() - print(f"当前活跃线程数: {active_threads}") - - print("\n活跃线程列表:") - for thread in threading.enumerate(): - print(f" - {thread.name} (守护: {thread.daemon}, 存活: {thread.is_alive()})") - - except Exception as e: - print(f"检查过程中发生错误: {e}") - import traceback - traceback.print_exc() - - finally: - # 清理资源 - print("\n=== 清理资源 ===") - try: - if 'mock_camera' in locals(): - mock_camera.cleanup() - print("mock_camera 设备资源已清理") - except Exception as e: - print(f"清理资源时发生错误: {e}") - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/backend/config.ini b/backend/config.ini index db7df1ff..e3274995 100644 --- a/backend/config.ini +++ b/backend/config.ini @@ -33,8 +33,8 @@ algorithm_type = plt color_resolution = 1080P depth_mode = NFOV_2X2BINNED camera_fps = 20 -depth_range_min = 800 -depth_range_max = 1200 +depth_range_min = 1200 +depth_range_max = 1600 fps = 15 synchronized_images_only = False diff --git a/backend/device_manager.py b/backend/device_manager.py deleted file mode 100644 index b975af28..00000000 --- a/backend/device_manager.py +++ /dev/null @@ -1,3635 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -设备管理模块 -负责摄像头、IMU传感器和压力传感器的连接和数据采集 -以及视频推流功能 -""" - -import cv2 -import numpy as np -import time -import threading -import json -import queue -import base64 -import gc -import os -import psutil -import configparser -from datetime import datetime -from pathlib import Path -from typing import Dict, List, Optional, Any, Tuple -from concurrent.futures import ThreadPoolExecutor -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 - import matplotlib.pyplot as plt - MATPLOTLIB_AVAILABLE = True -except ImportError: - MATPLOTLIB_AVAILABLE = False - print("警告: matplotlib库未安装,将使用默认深度图渲染") - -# 数据库管理 -# from backend.app import get_detection_sessions -from database import DatabaseManager - -# FemtoBolt深度相机支持 -try: - import pykinect_azure as pykinect - # 重新启用FemtoBolt功能,使用正确的Orbbec SDK K4A Wrapper路径 - FEMTOBOLT_AVAILABLE = True - print("信息: pykinect_azure库已安装,FemtoBolt深度相机功能已启用") - print("使用Orbbec SDK K4A Wrapper以确保与FemtoBolt设备的兼容性") -except ImportError: - FEMTOBOLT_AVAILABLE = False - print("警告: pykinect_azure库未安装,FemtoBolt深度相机功能将不可用") - print("请使用以下命令安装: pip install pykinect_azure") - -logger = logging.getLogger(__name__) - -class DeviceManager: - """设备管理器""" - - def __init__(self, db_manager: DatabaseManager = None): - self.camera = None - self.femtobolt_camera = None - self.imu_device = None - self.pressure_device = None - self.device_status = { - 'camera': False, - 'femtobolt': False, - 'imu': False, - 'pressure': False - } - self.calibration_data = {} - self.data_lock = threading.Lock() - self.camera_lock = threading.Lock() # 摄像头访问锁 - self.latest_data = {} - - # 数据库连接 - self.db_manager = db_manager - - # 推流状态和线程 - self.camera_streaming = False - self.femtobolt_streaming = False - self.imu_streaming = False - self.pressure_streaming = False - self.camera_streaming_thread = None - self.femtobolt_streaming_thread = None - self.imu_thread = None - self.pressure_thread = None - self.streaming_stop_event = threading.Event() - - # 全局帧缓存机制 - self.frame_cache = {} - self.frame_cache_lock = threading.RLock() # 可重入锁 - self.max_cache_size = 10 # 最大缓存帧数 - self.cache_timeout = 5.0 # 缓存超时时间(秒) - - # 同步录制状态 - self.sync_recording = False - self.current_session_id = None - self.current_patient_id = None - self.recording_start_time = None - - # 三路视频录制器 - self.feet_video_writer = None - self.body_video_writer = None - self.screen_video_writer = None - - # 录制线程和控制 - self.feet_recording_thread = None - self.body_recording_thread = None - self.screen_recording_thread = None - self.recording_stop_event = threading.Event() - - # 屏幕录制队列 - self.screen_frame_queue = queue.Queue(maxsize=100) - - # 兼容旧版录制状态 - self.recording = False - self.video_writer = None - - # FemtoBolt相机相关 - self.femtobolt_config = None - self.femtobolt_recording = False - self.femtobolt_color_writer = None - self.femtobolt_depth_writer = None - - # WebSocket连接(用于推流) - self.socketio = None - - # 延迟设备初始化,避免启动时阻塞 - # self._init_devices() # 注释掉自动初始化,改为按需初始化 - - - - def _init_devices(self): - - """初始化所有设备""" - # 分别初始化各个设备,单个设备失败不影响其他设备 - # try: - # self._init_camera() - # except Exception as e: - # logger.error(f'摄像头初始化失败: {e}') - - try: - if FEMTOBOLT_AVAILABLE: - self._init_femtobolt_camera() - except Exception as e: - logger.error(f'FemtoBolt深度相机初始化失败: {e}') - - try: - logger.error('IMU传感器初始化!!!!!!!!!!!!!!!!') - self._init_imu() - except Exception as e: - logger.error(f'IMU传感器初始化失败: {e}') - - try: - self._init_pressure_sensor() - except Exception as e: - logger.error(f'压力传感器初始化失败: {e}') - - logger.info('设备初始化完成') - - def _init_camera(self): - """初始化足部监视摄像头""" - try: - # 从数据库读取摄像头设备索引配置 - device_index = 0 # 默认值 - if self.db_manager: - try: - monitor_config = self.db_manager.get_system_setting('monitor_device_index') - if monitor_config: - device_index = int(monitor_config) - logger.info(f'从数据库读取摄像头设备索引: {device_index}') - else: - logger.info('数据库中未找到monitor_device_index配置,使用默认值0') - except Exception as e: - logger.warning(f'读取摄像头设备索引配置失败,使用默认值0: {e}') - else: - logger.warning('数据库管理器未初始化,使用默认摄像头索引0') - - - self.device_status['camera'] = True - except Exception as e: - logger.error(f'摄像头初始化异常: {e}') - self.camera = None - - def _init_femtobolt_camera(self): - """初始化FemtoBolt深度相机""" - if not FEMTOBOLT_AVAILABLE: - logger.warning('FemtoBolt深度相机库未安装,跳过初始化') - self.femtobolt_camera = None - self.device_status['femtobolt'] = False - return - - try: - # 初始化pykinect_azure库(优先使用指定SDK路径) - # 首先尝试手动指定路径(优先级最高) - sdk_paths = self._get_femtobolt_sdk_paths() - for sdk_path in sdk_paths: - if os.path.exists(sdk_path): - try: - pykinect.initialize_libraries(track_body=False, module_k4a_path=sdk_path) - logger.info(f'✓ 成功使用FemtoBolt SDK: {sdk_path}') - break - except Exception as e: - logger.warning(f'✗ FemtoBolt SDK路径失败: {sdk_path} - {e}') - continue - # 配置FemtoBolt设备参数 - self.femtobolt_config = pykinect.default_configuration - # logger.info('FemtoBolt配置参数。。。。。。。。。。。。。。。。。') - # logger.warning(pykinect.default_configuration) - # 从config.ini读取配置 - import configparser - config = configparser.ConfigParser() - config.read(os.path.join(os.path.dirname(__file__), 'config.ini')) - # color_res_str = config.get('DEFAULT', 'femtobolt_color_resolution', fallback='1080P') - # depth_range_min = config.getint('DEFAULT', 'femtobolt_depth_range_min', fallback=500) - # depth_range_max = config.getint('DEFAULT', 'femtobolt_depth_range_max', fallback=4500) - - # # 解析分辨率配置,分为宽度和高度 - # resolution_map = { - # '1024x1024': (1024, 1024), - # '1920x1080': (1920, 1080), - # '1280x720': (1280, 720), - # '720x720': (720, 720) - # } - # width, height = resolution_map.get(color_res_str, (1920, 1080)) - # 假设SDK支持设置宽高参数,示例代码如下(需根据实际SDK调整) - # if hasattr(self.femtobolt_config, 'color_resolution_width') and hasattr(self.femtobolt_config, 'color_resolution_height'): - # self.femtobolt_config.color_resolution_width = width - # self.femtobolt_config.color_resolution_height = height - # else: - # logger.info('FemtoBolt存在分辨率参数。。。。。。。。。。。。。。。。。') - # # 兼容原有枚举设置 - # if color_res_str == '720P': - # self.femtobolt_config.color_resolution = pykinect.K4A_COLOR_RESOLUTION_720P - # elif color_res_str == '1080P': - # self.femtobolt_config.color_resolution = pykinect.K4A_COLOR_RESOLUTION_1080P - # else: - # self.femtobolt_config.color_resolution = pykinect.K4A_COLOR_RESOLUTION_1080P - - self.femtobolt_config.depth_mode = pykinect.K4A_DEPTH_MODE_NFOV_UNBINNED - # self.femtobolt_config.depth_mode = pykinect.K4A_DEPTH_MODE_NFOV_UNBINNED - self.femtobolt_config.camera_fps = pykinect.K4A_FRAMES_PER_SECOND_15 - self.femtobolt_config.synchronized_images_only = False - self.femtobolt_config.color_resolution = 0 - # 视效范围参数示例,假设SDK支持depth_range_min和depth_range_max - - # 直接尝试启动设备(pykinect_azure库没有设备数量检测API) - # logger.info('准备启动FemtoBolt设备...') - - # 启动FemtoBolt设备 - # logger.info(f'尝试启动FemtoBolt设备...,参数详情是{self.femtobolt_config}') - - - self.femtobolt_camera = pykinect.start_device(config=self.femtobolt_config) - if self.femtobolt_camera: - self.device_status['femtobolt'] = True - logger.info('✓ FemtoBolt深度相机初始化成功!') - else: - # 设备启动失败,但不抛出异常,允许应用继续运行 - logger.warning('FemtoBolt设备启动返回None,可能设备未连接或被占用') - self.femtobolt_camera = None - self.device_status['femtobolt'] = False - return # 直接返回,不抛出异常 - - except Exception as e: - logger.warning(f'FemtoBolt深度相机初始化失败: {e}') - logger.warning('FemtoBolt深度相机功能将不可用,但不影响其他功能') - logger.warning('可能的解决方案:') - logger.warning('1. 检查FemtoBolt设备是否正确连接并被识别') - logger.warning('2. 安装Orbbec官方的K4A兼容驱动程序') - logger.warning('3. 确保没有其他应用程序占用设备') - logger.warning('4. 尝试重新插拔设备或重启计算机') - logger.warning('5. 考虑使用Orbbec原生SDK而非Azure Kinect SDK') - self.femtobolt_camera = None - self.device_status['femtobolt'] = False - # 不再抛出异常,让系统继续运行其他功能 - - def _get_femtobolt_sdk_paths(self) -> List[str]: - """获取FemtoBolt SDK可能的路径列表""" - import platform - sdk_paths = [] - if platform.system() == "Windows": - # 优先使用Orbbec SDK K4A Wrapper(与azure_kinect_image_example.py一致) - base_dir = os.path.dirname(os.path.abspath(__file__)) - dll_path = os.path.join(base_dir, "dll","femtobolt","bin", "k4a.dll") - sdk_paths.append(dll_path) - return sdk_paths - - def _init_imu(self): - """初始化IMU传感器""" - logger.info('开始初始化IMU传感器...') - try: - # 从config.ini读取串口配置 - config = configparser.ConfigParser() - # 优先读取根目录config.ini,否则读取backend/config.ini - root_config_path = os.path.join(os.path.dirname(__file__), 'config.ini') - app_root_config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config.ini') - logger.debug(f'尝试读取配置文件: {root_config_path}, {app_root_config_path}') - - read_files = config.read([app_root_config_path, root_config_path], encoding='utf-8') - logger.debug(f'成功读取的配置文件: {read_files}') - - if not read_files: - logger.warning('未能读取到config.ini,将使用默认串口配置COM7@9600') - - imu_port = config.get('DEVICES', 'imu_port', fallback='COM7') - imu_baudrate = config.getint('DEVICES', 'imu_baudrate', fallback=9600) - logger.info(f'从配置文件读取IMU串口配置: {imu_port}@{imu_baudrate}') - - # 初始化真实IMU设备 - logger.debug('创建RealIMUDevice实例...') - self.imu_device = RealIMUDevice(port=imu_port, baudrate=imu_baudrate) - - # 测试读取数据 - logger.debug('测试IMU设备数据读取...') - test_data = self.imu_device.read_data() - logger.debug(f'IMU设备测试数据: {test_data}') - - self.device_status['imu'] = True - logger.info(f'IMU传感器初始化成功(真实设备): {imu_port}@{imu_baudrate}') - except Exception as e: - logger.error(f'IMU传感器初始化失败: {e}', exc_info=True) - self.imu_device = None - self.device_status['imu'] = False - - 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('压力传感器初始化成功(模拟)') - except Exception as e: - logger.error(f'压力传感器初始化失败: {e}') - self.pressure_device = None - - def get_device_status(self) -> Dict[str, bool]: - """获取设备状态""" - return self.device_status.copy() - - def get_connected_devices(self) -> List[str]: - """获取已连接的设备列表""" - return [device for device, status in self.device_status.items() if status] - - def refresh_devices(self): - """刷新设备连接""" - logger.info('刷新设备连接...') - - # 使用锁保护摄像头重新初始化 - with self.camera_lock: - if self.camera: - self.camera.release() - self.camera = None - - self._init_devices() - - def calibrate_devices(self) -> Dict[str, Any]: - """校准设备""" - calibration_result = {} - - try: - # 摄像头校准 - # if self.device_status['camera']: - # camera_calibration = self._calibrate_camera() - # calibration_result['camera'] = camera_calibration - - # IMU校准 - if self.device_status['imu']: - imu_calibration = self._calibrate_imu() - calibration_result['imu'] = imu_calibration - - # 压力传感器校准 - if self.device_status['pressure']: - pressure_calibration = self._calibrate_pressure() - calibration_result['pressure'] = pressure_calibration - - self.calibration_data = calibration_result - logger.info('设备校准完成') - - except Exception as e: - logger.error(f'设备校准失败: {e}') - raise - - return calibration_result - - def _calibrate_camera(self) -> Dict[str, Any]: - """校准摄像头""" - if not self.camera or not self.camera.isOpened(): - return {'status': 'failed', 'error': '摄像头未连接'} - - try: - # 获取几帧图像进行校准 - frames = [] - for _ in range(10): - ret, frame = self.camera.read() - if ret: - frames.append(frame) - time.sleep(0.1) - - if not frames: - return {'status': 'failed', 'error': '无法获取图像'} - - # 计算平均亮度和对比度 - avg_brightness = np.mean([np.mean(cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)) for frame in frames]) - calibration = { - 'status': 'success', - 'brightness': float(avg_brightness), - 'resolution': (int(self.camera.get(cv2.CAP_PROP_FRAME_WIDTH)), - int(self.camera.get(cv2.CAP_PROP_FRAME_HEIGHT))), - 'fps': float(self.camera.get(cv2.CAP_PROP_FPS)), - 'timestamp': datetime.now().isoformat() - } - - return calibration - - except Exception as e: - return {'status': 'failed', 'error': str(e)} - - def _calibrate_imu(self) -> Dict[str, Any]: - """标准校准:采样较多帧,计算稳定零点偏移""" - if not self.imu_device: - return {'status': 'failed', 'error': 'IMU设备未连接'} - try: - samples = [] - for _ in range(100): - 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) - if not samples: - return {'status': 'failed', 'error': '无法获取IMU数据进行校准'} - head_pose_offset = { - 'rotation': float(np.mean([s['rotation'] for s in samples])), - 'tilt': float(np.mean([s['tilt'] for s in samples])), - 'pitch': float(np.mean([s['pitch'] for s in samples])) - } - calibration = { - 'status': 'success', - '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: - return {'status': 'failed', 'error': str(e)} - - def _quick_calibrate_imu(self) -> Dict[str, Any]: - """快速校准:采样少量帧,以当前姿态为零点(用于每次推流启动)""" - if not self.imu_device: - return {'status': 'failed', 'error': 'IMU设备未连接'} - try: - samples = [] - for _ in range(10): # 少量采样,加快启动 - 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) - if not samples: - return {'status': 'failed', 'error': '无法获取IMU数据进行快速校准'} - head_pose_offset = { - 'rotation': float(np.median([s['rotation'] for s in samples])), - 'tilt': float(np.median([s['tilt'] for s in samples])), - 'pitch': float(np.median([s['pitch'] for s in samples])) - } - calibration = { - 'status': 'success', - '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: - return {'status': 'failed', 'error': str(e)} - - def _calibrate_pressure(self) -> Dict[str, Any]: - """校准压力传感器""" - if not self.pressure_device: - return {'status': 'failed', 'error': '压力传感器未连接'} - - try: - # 收集零压力数据 - samples = [] - for _ in range(50): - data = self.pressure_device.read_data() - samples.append(data) - time.sleep(0.02) - - # 计算零点偏移 - zero_offset = { - 'left_foot': np.mean([s['left_foot'] for s in samples]), - 'right_foot': np.mean([s['right_foot'] for s in samples]) - } - - calibration = { - 'status': 'success', - 'zero_offset': zero_offset, - 'timestamp': datetime.now().isoformat() - } - - return calibration - - except Exception as e: - return {'status': 'failed', 'error': str(e)} - - def collect_data(self, session_id: str, patient_id: str, screen_image_base64: str = None) -> Dict[str, Any]: - # 实例化VideoStreamManager(VideoStreamManager类在同一文件中定义) - video_stream_manager = VideoStreamManager(device_manager=self) - """采集所有设备数据并保存到指定目录结构 - - Args: - session_id: 检测会话ID - patient_id: 患者ID - screen_image_base64: 前端界面截图的base64数据 - - Returns: - Dict: 包含所有采集数据的字典,符合detection_data表结构 - """ - # 生成采集时间戳 - timestamp = datetime.now().strftime('%Y%m%d_%H%M%S_%f')[:-3] # 精确到毫秒 - - # 创建数据存储目录 - data_dir = Path(f'data/patients/{patient_id}/{session_id}/{timestamp}') - data_dir.mkdir(parents=True, exist_ok=True) - - # 设置目录权限为777(完全权限) - try: - import os - import stat - os.chmod(str(data_dir), stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) # 777权限 - logger.debug(f"已设置目录权限为777: {data_dir}") - except Exception as perm_error: - logger.warning(f"设置目录权限失败: {perm_error},但目录创建成功") - - # 初始化数据字典 - data = { - 'session_id': session_id, - 'head_pose': None, - 'body_pose': None, - 'body_image': None, - 'foot_data': None, - 'foot_image': None, - 'foot_data_image': None, - 'screen_image': None, - 'timestamp': timestamp - } - - try: - # # 1. 采集头部姿态数据(从IMU设备获取) - # if self.device_status['imu']: - # head_pose_data = self._collect_head_pose_data() - # if head_pose_data: - # data['head_pose'] = json.dumps(head_pose_data) - # logger.debug(f'头部姿态数据采集成功: {session_id}') - - # # 2. 采集身体姿态数据(从FemtoBolt深度相机获取) - # if self.device_status['femtobolt']: - # body_pose_data = self._collect_body_pose_data() - # if body_pose_data: - # data['body_pose'] = json.dumps(body_pose_data) - # logger.debug(f'身体姿态数据采集成功: {session_id}') - - # 3. 采集身体视频截图(从FemtoBolt深度相机获取) - if self.device_status['femtobolt']: - try: - body_image_path = video_stream_manager._capture_body_image(data_dir, self) - if body_image_path: - data['body_image'] = str(body_image_path) - logger.debug(f'身体截图保存成功: {body_image_path}') - except Exception as e: - logger.error(f'调用_video_stream_manager._capture_body_image异常: {e}') - - # # 4. 采集足部压力数据(从压力传感器获取) - # if self.device_status['pressure']: - # foot_data = self._collect_foot_pressure_data() - # if foot_data: - # data['foot_data'] = json.dumps(foot_data) - # logger.debug(f'足部压力数据采集成功: {session_id}') - - # 5. 采集足部监测视频截图(从摄像头获取) - if self.device_status['camera']: - foot_image_path = video_stream_manager._capture_foot_image(data_dir,self) - if foot_image_path: - data['foot_image'] = str(foot_image_path) - logger.debug(f'足部截图保存成功: {foot_image_path}') - - # # 6. 生成足底压力数据图(从压力传感器数据生成) - # if self.device_status['pressure']: - # foot_data_image_path = self._generate_foot_pressure_image(data_dir) - # if foot_data_image_path: - # data['foot_data_image'] = str(foot_data_image_path) - # logger.debug(f'足底压力数据图生成成功: {foot_data_image_path}') - - # 7. 保存屏幕录制截图(从前端传入的base64数据) - if screen_image_base64: - try: - # logger.debug(f'屏幕截图保存.................{screen_image_base64}') - # 保存屏幕截图的base64数据为图片文件 - screen_image_path = None - if screen_image_base64: - try: - if screen_image_base64.startswith('data:image/'): - base64_data = screen_image_base64.split(',')[1] - else: - base64_data = screen_image_base64 - image_data = base64.b64decode(base64_data) - image_path = data_dir / 'screen_image.png' - with open(image_path, 'wb') as f: - f.write(image_data) - abs_image_path = image_path.resolve() - abs_cwd = Path.cwd().resolve() - screen_image_path = str(abs_image_path.relative_to(abs_cwd)) - logger.debug(f'屏幕截图保存成功: {screen_image_path}') - except Exception as e: - logger.error(f'屏幕截图保存失败: {e}') - import traceback - logger.error(traceback.format_exc()) - - if screen_image_path: - data['screen_image'] = str(screen_image_path) - logger.debug(f'屏幕截图保存成功: {screen_image_path}') - except Exception as e: - logger.error(f'屏幕截图保存失败: {e}') - import traceback - logger.error(traceback.format_exc()) - - # 更新最新数据 - with self.data_lock: - self.latest_data = data.copy() - - logger.debug(f'数据采集完成: {session_id}, 时间戳: {timestamp}') - - except Exception as e: - logger.error(f'数据采集失败: {e}') - - return data - - - def start_femtobolt_stream(self): - """开始FemtoBolt深度相机推流""" - if not FEMTOBOLT_AVAILABLE or self.femtobolt_camera is None: - logger.error('FemtoBolt深度相机未初始化') - return False - - try: - # 检查是否已经在推流 - if self.femtobolt_streaming: - logger.warning('FemtoBolt深度相机推流已在运行') - return True - - # 重置停止事件 - self.streaming_stop_event.clear() - - # 设置推流标志 - self.femtobolt_streaming = True - - # 启动推流线程 - self.femtobolt_streaming_thread = threading.Thread( - target=self._femtobolt_streaming_thread, - daemon=True, - name='FemtoBoltStreamingThread' - ) - self.femtobolt_streaming_thread.start() - - # logger.info('FemtoBolt深度相机推流已开始') - return True - except Exception as e: - logger.error(f'FemtoBolt深度相机推流启动失败: {e}') - self.femtobolt_streaming = False - return False - - def stop_femtobolt_stream(self): - """停止FemtoBolt深度相机推流""" - self.femtobolt_streaming = False - logger.debug('FemtoBolt深度相机推流已停止') - def set_socketio(self, socketio): - """设置WebSocket连接""" - self.socketio = socketio - - def start_imu_streaming(self): - """启动IMU头部姿态数据推流""" - try: - if self.imu_streaming: - logger.warning('IMU数据推流已在运行') - return True - - if not self.imu_device: - logger.error('IMU设备未初始化') - return False - - # 在启动推流前进行快速零点校准(自动以当前姿态为基准) - logger.info('正在进行IMU零点校准...') - calibration_result = self._quick_calibrate_imu() - if calibration_result.get('status') == 'success': - logger.info(f'IMU零点校准完成: {calibration_result["head_pose_offset"]}') - else: - logger.warning(f'IMU零点校准失败,将使用默认零偏移: {calibration_result.get("error", "未知错误")}') - - self.imu_streaming = True - self.imu_thread = threading.Thread(target=self._imu_streaming_thread, daemon=True) - self.imu_thread.start() - logger.info('IMU头部姿态数据推流已启动') - return True - - except Exception as e: - logger.error(f'启动IMU数据推流失败: {e}') - self.imu_streaming = False - return False - - def stop_imu_streaming(self): - """停止IMU头部姿态数据推流""" - try: - if not self.imu_streaming: - logger.warning('IMU数据推流未运行') - return True - - self.imu_streaming = False - if self.imu_thread and self.imu_thread.is_alive(): - self.imu_thread.join(timeout=2) - - logger.info('IMU头部姿态数据推流已停止') - return True - - except Exception as e: - logger.error(f'停止IMU数据推流失败: {e}') - return False - - def start_pressure_streaming(self): - """启动压力传感器足部压力数据推流""" - try: - if self.pressure_streaming: - 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() - logger.info('压力传感器足部压力数据推流已启动') - return True - - except Exception as e: - logger.error(f'启动压力传感器数据推流失败: {e}') - self.pressure_streaming = False - return False - - def stop_pressure_streaming(self): - """停止压力传感器足部压力数据推流""" - try: - if not self.pressure_streaming: - logger.warning('压力传感器数据推流未运行') - return True - - 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 - - except Exception as e: - logger.error(f'停止压力传感器数据推流失败: {e}') - return False - - # def _femtobolt_streaming_thread(self): - # import matplotlib - # matplotlib.use("Agg") # 无GUI后端 - # import matplotlib.pyplot as plt - # from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas - # from matplotlib.colors import LinearSegmentedColormap - - # frame_count = 0 - - # try: - # # 读取一次配置,避免每帧IO - # config = configparser.ConfigParser() - # config.read('config.ini', encoding='utf-8') - # 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 - - # # 如果可以用matplotlib,提前初始化绘图对象 - # if MATPLOTLIB_AVAILABLE and depth_range_min is not None and depth_range_max is not None: - # colors = ['fuchsia', 'red', 'yellow', 'lime', 'cyan', 'blue'] * 4 - # mcmap = LinearSegmentedColormap.from_list("custom_cmap", colors) - - # # 创建独立figure和axes - # fig, ax = plt.subplots(figsize=(7, 7)) - # canvas = FigureCanvas(fig) - - # # 灰色背景(假设分辨率不会超过) - # max_h, max_w = 1080, 1920 - # background = np.ones((max_h, max_w)) * 0.5 - # 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.set_axis_off() - # plt.tight_layout(pad=0) - - # contour = None # 用于保存等高线对象 - - # 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: - # if MATPLOTLIB_AVAILABLE and depth_range_min is not None and depth_range_max is not None: - # # 数据过滤 - # depth_image = depth_image.copy() - # depth_image[(depth_image > depth_range_max) | - # (depth_image < depth_range_min)] = 0 - # depth_masked = np.ma.masked_equal(depth_image, 0) - - # # 删除旧等高线 - # if contour: - # for coll in contour.collections: - # coll.remove() - - # # 绘制新等高线 - # contour = ax.contourf( - # depth_masked, - # levels=200, - # cmap=mcmap, - # vmin=depth_range_min, - # vmax=depth_range_max, - # origin='upper', - # zorder=2 - # ) - - # # 渲染到numpy - # canvas.draw() - # img = np.frombuffer(canvas.tostring_rgb(), dtype=np.uint8) - # img = img.reshape(fig.canvas.get_width_height()[::-1] + (3,)) - # depth_colored = img - # else: - # # 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 = cv2.applyColorMap(depth_normalized, cv2.COLORMAP_JET) - # mask_outside = (depth_image < depth_range_min) | (depth_image > depth_range_max) - # depth_colored[mask_outside] = [0, 0, 0] - - # # 裁剪 - # height, width = depth_colored.shape[:2] - # target_width = height // 2 - # if width > target_width: - # left = (width - target_width) // 2 - # right = left + target_width - # depth_colored = depth_colored[:, left:right] - - # # 缓存帧 - # self._save_frame_to_cache(depth_colored.copy(), 'femtobolt') - - # # 发送 - # success, buffer = cv2.imencode('.jpg', depth_colored, [int(cv2.IMWRITE_JPEG_QUALITY), 80]) - # if success and self.socketio: - # jpg_as_text = base64.b64encode(buffer).decode('utf-8') - # self.socketio.emit('depth_camera_frame', { - # 'image': jpg_as_text, - # 'frame_id': frame_count, - # 'timestamp': time.time() - # }) - # frame_count += 1 - # 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 _femtobolt_streaming_thread(self): - # """FemtoBolt深度相机推流线程(优化版本)""" - # frame_count = 0 - # import matplotlib - # matplotlib.use("Agg") # 使用无GUI的Agg后端,加速渲染 - # import matplotlib.pyplot as plt - # from matplotlib.colors import LinearSegmentedColormap - # from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas - # try: - # # 读取深度范围配置(只读一次) - # 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 - - # # 如果启用matplotlib模式,提前准备绘图对象 - # if MATPLOTLIB_AVAILABLE and depth_range_min is not None and depth_range_max is not None: - # colors = ['fuchsia', 'red', 'yellow', 'lime', 'cyan', 'blue'] * 4 - # mcmap = LinearSegmentedColormap.from_list("custom_cmap", colors) - - # fig, ax = plt.subplots(figsize=(7, 7)) - # canvas = FigureCanvas(fig) - - # background = np.ones((720, 1280)) * 0.5 # 假设最大分辨率,后面裁剪 - # bg_img = ax.imshow(background, origin='lower', cmap='gray', alpha=0.3) - # ax.grid(True, which='both', axis='both', color='white', linestyle='-', linewidth=1, zorder=0) - - # contour = None # 等高线对象占位 - # ax.set_axis_off() - # plt.tight_layout(pad=0) - - # 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: - # if MATPLOTLIB_AVAILABLE and depth_range_min is not None and depth_range_max is not None: - # # 过滤范围外值 - # depth_image = depth_image.copy() - # depth_image[(depth_image > depth_range_max) | (depth_image < depth_range_min)] = 0 - # depth_masked = np.ma.masked_equal(depth_image, 0) - - # # 清理旧的等高线 - # if contour: - # for coll in contour.collections: - # coll.remove() - - # # 绘制新的等高线 - # contour = ax.contourf( - # depth_masked, levels=200, cmap=mcmap, - # vmin=depth_range_min, vmax=depth_range_max, - # origin='upper', zorder=2 - # ) - - # # 渲染到 numpy - # canvas.draw() - # img = np.frombuffer(canvas.tostring_rgb(), dtype=np.uint8) - # img = img.reshape(fig.canvas.get_width_height()[::-1] + (3,)) - # depth_colored = img - # else: - # # 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 = cv2.applyColorMap(depth_normalized, cv2.COLORMAP_JET) - # mask_outside = (depth_image < depth_range_min) | (depth_image > depth_range_max) - # depth_colored[mask_outside] = [0, 0, 0] - - # # 裁剪 - # height, width = depth_colored.shape[:2] - # target_width = height // 2 - # if width > target_width: - # left = (width - target_width) // 2 - # right = left + target_width - # depth_colored = depth_colored[:, left:right] - - # # 保存到缓存 - # self._save_frame_to_cache(depth_colored.copy(), 'femtobolt') - - # # 编码并推送 - # success, buffer = cv2.imencode('.jpg', depth_colored, [int(cv2.IMWRITE_JPEG_QUALITY), 80]) - # if success and self.socketio: - # jpg_as_text = base64.b64encode(buffer).decode('utf-8') - # self.socketio.emit('depth_camera_frame', { - # 'image': jpg_as_text, - # 'frame_id': frame_count, - # 'timestamp': time.time() - # }) - # frame_count += 1 - # else: - # time.sleep(0.01) - # except Exception as e: - # logger.debug(f'FemtoBolt帧推送失败: {e}') - # time.sleep(0.1) - - # time.sleep(1 / 30) # 控制帧率 - - # except Exception as e: - # 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: - # 获取FemtoBolt帧 - capture = self.femtobolt_camera.update() - # 检查capture是否有效并获取彩色深度图像 - if capture is not None: - ret, depth_image = capture.get_depth_image() - height2, width2 = depth_image.shape[:2] - # logger.debug(f'FemtoBolt原始帧宽: {width2}') - # logger.debug(f'FemtoBolt原始帧高: {height2}') - - if ret and depth_image is not None: - # 读取config.ini中的深度范围配置 - 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 - # 使用matplotlib渲染深度图,参考display_x.py - 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 已经是 np.uint16 格式 - depth_image = depth_image.copy() - depth_image[depth_image > depth_range_max] = 0 - depth_image[depth_image < depth_range_min] = 0 - # 归一化到 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) - depth_normalized = ((depth_normalized - depth_range_min) / (depth_range_max - depth_range_min) * 255).astype(np.uint8) - depth_colored = cv2.applyColorMap(depth_normalized, cv2.COLORMAP_JET) - mask_outside = (depth_image < depth_range_min) | (depth_image > depth_range_max) - depth_colored[mask_outside] = [0, 0, 0] - - height, width = depth_colored.shape[:2] - # logger.debug(f'FemtoBolt帧宽: {width}') - # logger.debug(f'FemtoBolt帧高: {height}') - target_width = height // 2 - if width > target_width: - left = (width - target_width) // 2 - right = left + target_width - depth_colored = depth_colored[:, left:right] - height1, width1 = depth_colored.shape[:2] - # logger.debug(f'FemtoBolt帧裁剪完以后得宽: {width1}') - # logger.debug(f'FemtoBolt帧裁剪完以后得宽: {height1}') - # 保存处理好的身体帧到全局缓存 - self._save_frame_to_cache(depth_colored.copy(), 'femtobolt') - - success, buffer = cv2.imencode('.jpg', depth_colored, [int(cv2.IMWRITE_JPEG_QUALITY), 80]) - if success and self.socketio: - jpg_as_text = base64.b64encode(buffer).decode('utf-8') - self.socketio.emit('depth_camera_frame', { - 'image': jpg_as_text, - 'frame_id': frame_count, - 'timestamp': time.time() - }) - frame_count += 1 - - 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 _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头部姿态数据推流线程已启动') - - try: - loop_count = 0 - while self.imu_streaming and self.socketio: - try: - loop_count += 1 - # 从IMU设备读取数据 - imu_data = self.imu_device.read_data() - - if imu_data and 'head_pose' in imu_data: - # 直接使用设备提供的头部姿态数据,减少数据包装 - head_pose = imu_data['head_pose'] - # logger.warning(f'推送数据{head_pose}') - # 优化:直接发送最精简的数据格式,避免重复时间戳 - rotation = head_pose.get('rotation') - tilt = head_pose.get('tilt') - pitch = head_pose.get('pitch') - try: - rotation = round(float(rotation), 2) if rotation is not None else rotation - except Exception: - pass - try: - tilt = round(float(tilt), 2) if tilt is not None else tilt - except Exception: - pass - try: - pitch = round(float(pitch), 2) if pitch is not None else pitch - except Exception: - pass - self.socketio.emit('imu_data', { - 'rotation': rotation, # 旋转角:左旋(-), 右旋(+) - 'tilt': tilt, # 倾斜角:左倾(-), 右倾(+) - 'pitch': pitch, # 俯仰角:俯角(-), 仰角(+) - }) - - # 优化:提高数据发送频率到30Hz,降低延时 - time.sleep(0.033) - - except Exception as e: - # 减少异常日志的详细程度 - logger.warning(f'IMU数据推流异常: {e}') - time.sleep(0.033) - - except Exception as e: - logger.error(f'IMU推流线程异常: {e}', exc_info=True) - finally: - logger.info('IMU头部姿态数据推流线程已结束') - - def _pressure_streaming_thread(self): - """压力传感器足部压力数据推流线程""" - logger.info('压力传感器足部压力数据推流线程已启动') - - try: - while self.pressure_streaming and self.socketio: - try: - # 从压力传感器设备读取数据 - pressure_data = self.pressure_device.read_data() - - if pressure_data and 'foot_pressure' in pressure_data: - foot_pressure = pressure_data['foot_pressure'] - # logger.error(f"压力传感器数据{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_total / total_pressure if total_pressure > 0 else 0.5 - - # 计算压力中心偏移 - pressure_center_offset = (balance_ratio - 0.5) * 100 # 转换为百分比 - - # 计算前后足压力分布 - 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': complete_pressure_data, - 'timestamp': datetime.now().isoformat() - }) - - # 控制数据发送频率(20Hz) - time.sleep(0.05) - - except Exception as e: - logger.error(f'压力传感器数据推流异常: {e}') - time.sleep(0.1) - - except Exception as e: - logger.error(f'压力传感器推流线程异常: {e}') - finally: - logger.info('压力传感器足部压力数据推流线程已结束') - - def start_recording(self, session_id: str, patient_id: str) -> Dict[str, Any]: - video_manager=VideoStreamManager() - """启动同步录制 - - Args: - session_id: 检测会话ID - patient_id: 患者ID - - Returns: - Dict: 录制启动状态和信息 - { - 'success': bool, - 'session_id': str, - 'patient_id': str, - 'recording_start_time': str, - 'video_paths': { - 'feet_video': str, - 'body_video': str, - 'screen_video': str - }, - 'message': str - } - """ - result = { - 'success': False, - 'session_id': session_id, - 'patient_id': patient_id, - 'recording_start_time': None, - 'video_paths': { - 'feet_video': None, - 'screen_video': None - }, - 'message': '' - } - - try: - # 检查是否已在录制 - if self.sync_recording: - result['message'] = f'已在录制中,当前会话ID: {self.current_session_id}' - return result - - # 设置录制参数 - self.current_session_id = session_id - self.current_patient_id = patient_id - self.recording_start_time = datetime.now() - - # 创建存储目录 - base_path = os.path.join('data', 'patients', patient_id, session_id) - try: - os.makedirs(base_path, exist_ok=True) - logger.info(f'录制目录创建成功: {base_path}') - - # 设置目录权限为777(所有用户完全权限) - try: - import stat - import subprocess - import platform - # 在Windows系统上使用icacls命令设置更详细的权限 - if platform.system() == 'Windows': - try: - # 为Users用户组授予完全控制权限 - subprocess.run([ - 'icacls', base_path, '/grant', 'Users:(OI)(CI)F' - ], check=True, capture_output=True, text=True) - - # 为Everyone用户组授予完全控制权限 - subprocess.run([ - 'icacls', base_path, '/grant', 'Everyone:(OI)(CI)F' - ], check=True, capture_output=True, text=True) - - logger.info(f"已设置Windows目录权限(Users和Everyone完全控制): {base_path}") - except subprocess.CalledProcessError as icacls_error: - logger.warning(f"Windows权限设置失败: {icacls_error}") - else: - logger.info(f"已设置目录权限为777: {base_path}") - - except Exception as perm_error: - logger.warning(f"设置目录权限失败: {perm_error},但目录创建成功") - except Exception as dir_error: - logger.error(f'创建录制目录失败: {base_path}, 错误: {dir_error}') - result['success'] = False - result['message'] = f'创建录制目录失败: {dir_error}' - return result - - # 定义视频文件路径 - feet_video_path = os.path.join(base_path, 'feet.mp4') - - screen_video_path = os.path.join(base_path, 'screen.webm') - result['video_paths']['feet_video'] = feet_video_path - - result['video_paths']['screen_video'] = screen_video_path - - # 更新数据库中的视频路径 - if self.db_manager: - try: - # 更新会话状态为录制中 - if not self.db_manager.update_session_status(session_id, 'recording'): - logger.error(f'更新会话状态为录制中失败 - 会话ID: {session_id}') - - # 更新视频文件路径 - self.db_manager.update_session_normal_video_path(session_id, feet_video_path) - - self.db_manager.update_session_screen_video_path(session_id, screen_video_path) - - logger.debug(f'数据库视频路径更新成功 - 会话ID: {session_id}') - except Exception as db_error: - logger.error(f'更新数据库视频路径失败: {db_error}') - # 数据库更新失败不影响录制启动,继续执行 - - # 视频编码参数 - fourcc = cv2.VideoWriter_fourcc(*'mp4v') - fps = 30 - - # 初始化视频写入器 - if self.device_status['camera']: - target_width,target_height = video_manager.MAX_FRAME_SIZE - self.feet_video_writer = cv2.VideoWriter(feet_video_path, fourcc, fps, (target_width, target_height)) - - # 检查视频写入器是否初始化成功 - if self.feet_video_writer.isOpened(): - logger.info(f'脚部视频写入器初始化成功: {feet_video_path}') - else: - logger.error(f'脚部视频写入器初始化失败: {feet_video_path}') - # # 获取摄像头分辨率 - # if self.camera and self.camera.isOpened(): - # target_width,target_height = video_manager.MAX_FRAME_SIZE - # self.feet_video_writer = cv2.VideoWriter( - # feet_video_path, fourcc, fps, (target_width, target_height) - # ) - - # # 检查视频写入器是否初始化成功 - # if self.feet_video_writer.isOpened(): - # logger.info(f'脚部视频写入器初始化成功: {feet_video_path}') - # else: - # logger.error(f'脚部视频写入器初始化失败: {feet_video_path}') - # else: - # logger.error('摄像头未打开,无法初始化脚部视频写入器') - else: - logger.warning('摄像头设备未启用,跳过脚部视频写入器初始化') - # # 屏幕录制写入器(默认分辨率,后续根据实际帧调整) - # self.screen_video_writer = cv2.VideoWriter( - # screen_video_path, fourcc, fps, (1920, 1080) - # ) - - # 重置停止事件 - self.recording_stop_event.clear() - self.sync_recording = True - # 启动录制线程 - if self.feet_video_writer: - self.feet_recording_thread = threading.Thread( - target=self._feet_recording_thread, - daemon=True, - name='FeetRecordingThread' - ) - self.feet_recording_thread.start() - - # #屏幕录制 - # if self.screen_video_writer: - # self.screen_recording_thread = threading.Thread( - # target=self._screen_recording_thread, - # daemon=True, - # name='ScreenRecordingThread' - # ) - # self.screen_recording_thread.start() - - # 设置录制状态 - - result['success'] = True - result['recording_start_time'] = self.recording_start_time.isoformat() - result['message'] = '同步录制已启动' - - logger.debug(f'同步录制已启动 - 会话ID: {session_id}, 患者ID: {patient_id}') - - except Exception as e: - logger.error(f'启动同步录制失败: {e}') - result['message'] = f'启动录制失败: {str(e)}' - # 清理已创建的写入器 - self._cleanup_video_writers() - - return result - - def stop_recording(self, session_id: str, video_data_base64) -> Dict[str, Any]: - """停止同步录制 - - Args: - session_id: 检测会话ID - video_data_base64: 屏幕录制视频的base64编码数据,可选 - - Returns: - Dict: 录制停止状态和信息 - """ - result = { - 'success': False, - 'session_id': session_id, - 'recording_duration': 0, - 'video_files': [], - 'message': '' - } - - try: - # 检查录制状态 - if not self.sync_recording: - result['message'] = '当前没有进行录制' - return result - - if self.current_session_id != session_id: - result['message'] = f'会话ID不匹配,当前录制会话: {self.current_session_id}' - return result - - # 设置停止事件 - self.recording_stop_event.set() - session_data = self.db_manager.get_session_data(session_id) - base_path = os.path.join('data', 'patients', session_data['patient_id'], session_id) - - # 定义视频文件路径 - feet_video_path = os.path.join(base_path, 'feet.mp4') - body_video_path = os.path.join(base_path, 'body.mp4') - screen_video_path = os.path.join(base_path, 'screen.webm') - - # 等待录制线程结束 - threads_to_join = [ - (self.feet_recording_thread, 'feet'), - (self.body_recording_thread, 'body') - ] - - logger.info(f"正在停止录制线程 - 会话ID: {session_id}") - - for thread, name in threads_to_join: - if thread and thread.is_alive(): - logger.debug(f"等待{name}录制线程结束...") - thread.join(timeout=3) - if thread.is_alive(): - logger.warning(f'{name}录制线程未能在3秒内正常结束,可能存在阻塞') - else: - logger.debug(f'{name}录制线程已正常结束') - else: - logger.debug(f'{name}录制线程未运行或已结束') - - # 计算录制时长 - if self.recording_start_time: - duration = (datetime.now() - self.recording_start_time).total_seconds() - result['recording_duration'] = duration - - # 清理视频写入器并收集文件信息 - # video_files = self._cleanup_video_writers() - # 保存传入的屏幕录制视频数据,替代原有屏幕录制视频保存逻辑 - # video_bytes = base64.b64decode(video_data_base64) - with open(screen_video_path, 'wb') as f: - f.write(video_data_base64) - # video_files.append(screen_video_path) - logger.info(f'屏幕录制视频保存成功,路径: {screen_video_path}, 文件大小: {os.path.getsize(screen_video_path)} 字节') - - result['video_files'] = screen_video_path - - # 更新数据库中的会话信息 - if self.db_manager and result['recording_duration'] > 0: - try: - duration_seconds = int(result['recording_duration']) - self.db_manager.update_session_duration(session_id, duration_seconds) - self.db_manager.update_session_normal_video_path(session_id, feet_video_path) - self.db_manager.update_session_femtobolt_video_path(session_id, body_video_path) - self.db_manager.update_session_screen_video_path(session_id, screen_video_path) - - # 更新会话状态为已完成 - if self.db_manager.update_session_status(session_id, 'completed'): - logger.debug(f'数据库会话信息更新成功 - 会话ID: {session_id}, 持续时间: {duration_seconds}秒') - else: - logger.error(f'更新会话状态为已完成失败 - 会话ID: {session_id}') - except Exception as db_error: - logger.error(f'更新数据库会话信息失败: {db_error}') - - # 重置录制状态 - self.sync_recording = False - self.current_session_id = None - self.current_patient_id = None - self.recording_start_time = None - - result['success'] = True - result['message'] = '同步录制已停止' - - logger.debug(f'同步录制已停止 - 会话ID: {session_id}, 录制时长: {result["recording_duration"]:.2f}秒') - - except Exception as e: - logger.error(f'停止同步录制失败: {e}', exc_info=True) - result['message'] = f'停止录制失败: {str(e)}' - - return result - - def add_screen_frame(self, frame_data: str): - """添加屏幕录制帧 - - Args: - frame_data: base64编码的屏幕截图数据 - """ - if self.sync_recording and not self.screen_frame_queue.full(): - try: - self.screen_frame_queue.put(frame_data, block=False) - except: - # 队列满时丢弃帧 - pass - - def _feet_recording_thread(self): - """足部视频录制线程""" - consecutive_failures = 0 - max_consecutive_failures = 10 - - # logger.info(f"足部录制线程已启动 - 会话ID: {self.current_session_id}") - logger.info(f"视频写入器状态: {self.feet_video_writer.isOpened() if self.feet_video_writer else 'None'}") - - try: - while self.sync_recording and not self.recording_stop_event.is_set(): - if self.feet_video_writer: - # 从全局缓存获取最新帧 - frame, frame_timestamp = self._get_latest_frame_from_cache('camera') - # 详细记录帧获取情况 - if frame is not None: - logger.debug(f"成功获取帧 - 尺寸: {frame.shape}, 数据类型: {frame.dtype}, 时间戳: {frame_timestamp}") - # 检查视频写入器状态 - if not self.feet_video_writer.isOpened(): - logger.error(f"脚部视频写入器已关闭,无法写入帧 - 会话ID: {self.current_session_id}") - break - try: - # 复制帧数据避免引用问题 - image = frame.copy() - # 写入录制文件 - write_success = self.feet_video_writer.write(image) - # 检查写入是否成功 - if write_success is False: - logger.error(f"视频帧写入返回False - 可能写入失败") - consecutive_failures += 1 - else: - consecutive_failures = 0 # 重置失败计数 - - # 记录录制统计 - if hasattr(self, 'recording_frame_count'): - self.recording_frame_count += 1 - else: - self.recording_frame_count = 1 - except Exception as write_error: - logger.error(f"写入脚部视频帧异常: {write_error}") - consecutive_failures += 1 - if consecutive_failures >= 10: - logger.error("连续写入失败次数过多,停止录制") - break - - else: - logger.warning(f"从缓存获取的帧为None - 连续失败{consecutive_failures + 1}次") - consecutive_failures += 1 - if consecutive_failures <= 3: - logger.warning(f"录制线程无法从缓存获取帧 (连续失败{consecutive_failures}次)") - elif consecutive_failures == max_consecutive_failures: - logger.error(f"录制线程连续失败{max_consecutive_failures}次,可能缓存无数据或推流已停止") - - # 等待一段时间再重试 - time.sleep(0.1) - else: - logger.error("足部视频写入器未初始化") - break - - # 检查连续失败情况 - if consecutive_failures >= max_consecutive_failures: - logger.error(f"连续失败次数达到上限({max_consecutive_failures}),停止录制") - break - - time.sleep(1/30) # 30 FPS - - except Exception as e: - logger.error(f'足部录制线程异常: {e}') - finally: - logger.info(f"足部录制线程已结束 - 会话ID: {self.current_session_id}, 总录制帧数: {getattr(self, 'recording_frame_count', 0)}") - # 确保视频写入器被正确关闭 - if self.feet_video_writer: - self.feet_video_writer.release() - self.feet_video_writer = None - logger.debug("足部视频写入器已释放") - - def _body_recording_thread(self): - """身体视频录制线程""" - consecutive_failures = 0 - max_consecutive_failures = 10 - - # logger.info(f"身体录制线程启动 - 会话ID: {self.current_session_id}") - - try: - while self.sync_recording and not self.recording_stop_event.is_set(): - if self.body_video_writer: - # 从全局缓存获取最新帧 - frame, frame_timestamp = self._get_latest_frame_from_cache('femtobolt') - - if frame is not None: - # 检查视频写入器状态 - if not self.body_video_writer.isOpened(): - logger.error(f"身体视频写入器已关闭,无法写入帧 - 会话ID: {self.current_session_id}") - break - - # 添加帧信息日志 - logger.debug(f"获取到身体帧 - 形状: {frame.shape}, 数据类型: {frame.dtype}, 时间戳: {frame_timestamp}") - - try: - # 复制帧数据避免引用问题 - image = frame.copy() - - # 检查图像有效性 - if image is None or image.size == 0: - logger.warning(f"身体帧数据无效 - 会话ID: {self.current_session_id}") - consecutive_failures += 1 - continue - - # 确保图像数据类型正确 - if image.dtype != np.uint8: - logger.debug(f"转换身体帧数据类型从 {image.dtype} 到 uint8") - image = image.astype(np.uint8) - - # 确保图像是3通道BGR格式 - if len(image.shape) != 3 or image.shape[2] != 3: - logger.warning(f"身体帧格式异常: {image.shape},期望3通道BGR格式") - consecutive_failures += 1 - continue - - # 检查并调整图像分辨率以匹配视频写入器 - current_height, current_width = image.shape[:2] - expected_width, expected_height = 288, 576 # 默认期望分辨率 - - if current_width != expected_width or current_height != expected_height: - logger.debug(f"调整身体帧分辨率从 {current_width}x{current_height} 到 {expected_width}x{expected_height}") - image = cv2.resize(image, (expected_width, expected_height)) - - # 确保图像数据连续性(OpenCV要求) - if not image.flags['C_CONTIGUOUS']: - logger.debug("转换身体帧为连续内存布局") - image = np.ascontiguousarray(image) - - # 写入录制文件 - logger.debug(f"尝试写入身体视频帧 - 图像形状: {image.shape}, 数据类型: {image.dtype}, 连续性: {image.flags['C_CONTIGUOUS']}") - write_success = self.body_video_writer.write(image) - - # 检查写入是否成功 - cv2.VideoWriter.write()可能返回None、False或True - if write_success is False: - consecutive_failures += 1 - logger.warning(f"身体视频帧写入明确失败 - 会话ID: {self.current_session_id}, 连续失败次数: {consecutive_failures}, 图像形状: {image.shape}, 写入器状态: {self.body_video_writer.isOpened()}") - - if consecutive_failures >= max_consecutive_failures: - logger.error(f"身体视频写入连续失败{max_consecutive_failures}次,停止录制") - break - elif write_success is None: - # 某些OpenCV版本可能返回None,这通常表示写入失败 - consecutive_failures += 1 - logger.warning(f"身体视频帧写入返回None - 会话ID: {self.current_session_id}, 连续失败次数: {consecutive_failures}, 可能是编解码器问题") - - if consecutive_failures >= max_consecutive_failures: - logger.error(f"身体视频写入连续返回None {max_consecutive_failures}次,停止录制") - break - else: - consecutive_failures = 0 - logger.debug(f"成功写入身体视频帧 - 会话ID: {self.current_session_id}") - - # 释放图像内存 - # del image - - except Exception as e: - consecutive_failures += 1 - logger.error(f'身体视频帧写入异常: {e}, 连续失败次数: {consecutive_failures}, 帧形状: {frame.shape if frame is not None else "None"}') - - if consecutive_failures >= max_consecutive_failures: - logger.error(f"身体视频写入连续异常{max_consecutive_failures}次,停止录制") - break - else: - # 没有可用帧,短暂等待 - logger.debug(f"未获取到身体帧,等待中... - 会话ID: {self.current_session_id}") - time.sleep(0.01) - continue - else: - logger.warning(f"身体视频写入器未初始化 - 会话ID: {self.current_session_id}") - time.sleep(0.1) - continue - - # 控制录制帧率 - time.sleep(1/30) # 30 FPS - - except Exception as e: - logger.error(f'身体录制线程异常: {e}') - finally: - logger.info(f"身体录制线程结束 - 会话ID: {self.current_session_id}") - - def _screen_recording_thread(self): - """屏幕录制线程""" - try: - while self.sync_recording and not self.recording_stop_event.is_set(): - try: - # 从队列获取屏幕帧 - frame_data = self.screen_frame_queue.get(timeout=1) - - # 解码base64图像 - image_data = base64.b64decode(frame_data) - nparr = np.frombuffer(image_data, np.uint8) - frame = cv2.imdecode(nparr, cv2.IMREAD_COLOR) - - if frame is not None and self.screen_video_writer: - # 调整到录制分辨率 - frame = cv2.resize(frame, (1920, 1080)) - self.screen_video_writer.write(frame) - - except queue.Empty: - continue - except Exception as e: - logger.error(f'屏幕录制帧处理失败: {e}') - - except Exception as e: - logger.error(f'屏幕录制线程异常: {e}') - - def _cleanup_video_writers(self) -> List[str]: - """清理视频写入器并返回文件列表""" - video_files = [] - - try: - if self.feet_video_writer: - self.feet_video_writer.release() - self.feet_video_writer = None - if self.current_patient_id and self.current_session_id: - feet_path = os.path.join('data', 'patients', self.current_patient_id, - self.current_session_id, 'feet.mp4') - if os.path.exists(feet_path): - video_files.append(feet_path) - - if self.body_video_writer: - self.body_video_writer.release() - self.body_video_writer = None - if self.current_patient_id and self.current_session_id: - body_path = os.path.join('data', 'patients', self.current_patient_id, - self.current_session_id, 'body.mp4') - if os.path.exists(body_path): - video_files.append(body_path) - - if self.screen_video_writer: - self.screen_video_writer.release() - self.screen_video_writer = None - if self.current_patient_id and self.current_session_id: - screen_path = os.path.join('data', 'patients', self.current_patient_id, - self.current_session_id, 'screen.mp4') - if os.path.exists(screen_path): - video_files.append(screen_path) - - except Exception as e: - logger.error(f'清理视频写入器失败: {e}') - - return video_files - - def _save_frame_to_cache(self, frame, frame_type='camera'): - """保存帧到全局缓存""" - try: - import time - with self.frame_cache_lock: - current_time = time.time() - - # 清理过期帧 - self._cleanup_expired_frames() - - # 如果缓存已满,移除最旧的帧 - if frame_type in self.frame_cache and len(self.frame_cache[frame_type]) >= self.max_cache_size: - oldest_key = min(self.frame_cache[frame_type].keys()) - del self.frame_cache[frame_type][oldest_key] - - # 初始化帧类型缓存 - if frame_type not in self.frame_cache: - self.frame_cache[frame_type] = {} - - # 保存帧(深拷贝避免引用问题) - frame_data = { - 'frame': frame.copy(), - 'timestamp': current_time, - 'frame_id': len(self.frame_cache[frame_type]) - } - - self.frame_cache[frame_type][current_time] = frame_data - # logger.debug(f'成功保存帧到缓存: {frame_type}, 缓存大小: {len(self.frame_cache[frame_type])}, 帧尺寸: {frame.shape}') - - except Exception as e: - logger.error(f'保存帧到缓存失败: {e}') - - def _get_latest_frame_from_cache(self, frame_type='camera'): - """从缓存获取最新帧""" - try: - import time - with self.frame_cache_lock: - # logger.debug(f'尝试从缓存获取帧: {frame_type}') - - if frame_type not in self.frame_cache: - logger.debug(f'缓存中不存在帧类型: {frame_type}, 可用类型: {list(self.frame_cache.keys())}') - return None, None - - if not self.frame_cache[frame_type]: - logger.debug(f'帧类型 {frame_type} 的缓存为空') - return None, None - - # 清理过期帧 - self._cleanup_expired_frames() - - if not self.frame_cache[frame_type]: - logger.debug(f'清理过期帧后,帧类型 {frame_type} 的缓存为空') - return None, None - - # 获取最新帧 - latest_timestamp = max(self.frame_cache[frame_type].keys()) - frame_data = self.frame_cache[frame_type][latest_timestamp] - - current_time = time.time() - frame_age = current_time - frame_data['timestamp'] - # logger.debug(f'成功获取最新帧: {frame_type}, 帧龄: {frame_age:.2f}秒, 缓存大小: {len(self.frame_cache[frame_type])}') - - return frame_data['frame'].copy(), frame_data['timestamp'] - - except Exception as e: - logger.error(f'从缓存获取帧失败: {e}') - return None, None - - def _cleanup_expired_frames(self): - """清理过期的缓存帧""" - try: - import time - current_time = time.time() - - for frame_type in list(self.frame_cache.keys()): - expired_keys = [] - for timestamp in self.frame_cache[frame_type].keys(): - if current_time - timestamp > self.cache_timeout: - expired_keys.append(timestamp) - - # 删除过期帧 - for key in expired_keys: - del self.frame_cache[frame_type][key] - - # if expired_keys: - # logger.debug(f'清理了 {len(expired_keys)} 个过期帧: {frame_type}') - - except Exception as e: - logger.error(f'清理过期帧失败: {e}') -class RealIMUDevice: - """真实IMU设备,通过串口读取姿态数据""" - def __init__(self, port, baudrate): - self.port = port - self.baudrate = baudrate - self.ser = None - self.buffer = bytearray() - self.calibration_data = None - self.head_pose_offset = {'rotation': 0, 'tilt': 0, 'pitch': 0} - self.last_data = { - 'roll': 0.0, - 'pitch': 0.0, - 'yaw': 0.0, - 'temperature': 25.0 - } - logger.debug(f'RealIMUDevice 初始化: port={self.port}, baudrate={self.baudrate}') - self._connect() - - def _connect(self): - try: - logger.debug(f'尝试打开串口: {self.port} @ {self.baudrate}') - self.ser = serial.Serial(self.port, self.baudrate, timeout=1) - if hasattr(self.ser, 'reset_input_buffer'): - try: - self.ser.reset_input_buffer() - logger.debug('已清空串口输入缓冲区') - except Exception as e: - logger.debug(f'重置串口输入缓冲区失败: {e}') - logger.info(f'IMU设备连接成功: {self.port} @ {self.baudrate}bps') - except Exception as e: - # logger.error(f'IMU设备连接失败: {e}', exc_info=True) - self.ser = None - - 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'] - logger.debug(f'应用IMU校准数据: {self.head_pose_offset}') - - def apply_calibration(self, raw_data: Dict[str, Any]) -> Dict[str, Any]: - """应用校准:将当前姿态减去初始偏移,得到相对于初始姿态的变化量""" - if not raw_data or 'head_pose' not in raw_data: - return raw_data - - # 应用校准偏移 - calibrated_data = raw_data.copy() - head_pose = raw_data['head_pose'].copy() - - # 减去基准值(零点偏移) - head_pose['rotation'] = head_pose['rotation'] - self.head_pose_offset['rotation'] - head_pose['tilt'] = head_pose['tilt'] - self.head_pose_offset['tilt'] - head_pose['pitch'] = head_pose['pitch'] - self.head_pose_offset['pitch'] - - calibrated_data['head_pose'] = head_pose - return calibrated_data - - @staticmethod - def _checksum(data: bytes) -> int: - return sum(data[:-1]) & 0xFF - - def _parse_packet(self, data: bytes) -> Optional[Dict[str, float]]: - if len(data) != 11: - logger.debug(f'无效数据包长度: {len(data)}') - return None - if data[0] != 0x55: - logger.debug(f'错误的包头: 0x{data[0]:02X}') - return None - if self._checksum(data) != data[-1]: - logger.debug(f'校验和错误: 期望{self._checksum(data):02X}, 实际{data[-1]:02X}') - return None - packet_type = data[1] - vals = [int.from_bytes(data[i:i+2], 'little', signed=True) for i in range(2, 10, 2)] - if packet_type == 0x53: # 姿态角,单位0.01° - pitchl, rxl, yawl, temp = vals # 注意这里 vals 已经是有符号整数 - # 使用第一段代码里的比例系数 - k_angle = 180.0 - roll = -round(rxl / 32768.0 * k_angle,2) - pitch = -round(pitchl / 32768.0 * k_angle,2) - yaw = -round(yawl / 32768.0 * k_angle,2) - temp = temp / 100.0 - self.last_data = { - 'roll': roll, - 'pitch': pitch, - 'yaw': yaw, - 'temperature': temp - } - # logger.debug(f'解析姿态角包: roll={roll}, pitch={pitch}, yaw={yaw}, temp={temp}') - return self.last_data - else: - # logger.debug(f'忽略的数据包类型: 0x{packet_type:02X}') - return None - - 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() - return { - 'head_pose': { - 'rotation': self.last_data['yaw'], - 'tilt': self.last_data['roll'], - 'pitch': self.last_data['pitch'] - }, - 'temperature': self.last_data['temperature'], - 'timestamp': datetime.now().isoformat() - } - try: - bytes_waiting = self.ser.in_waiting - if bytes_waiting: - # logger.debug(f'串口缓冲区待读字节: {bytes_waiting}') - chunk = self.ser.read(bytes_waiting) - # logger.debug(f'读取到字节: {len(chunk)}') - self.buffer.extend(chunk) - while len(self.buffer) >= 11: - if self.buffer[0] != 0x55: - dropped = self.buffer.pop(0) - logger.debug(f'丢弃无效字节: 0x{dropped:02X}') - continue - packet = bytes(self.buffer[:11]) - parsed = self._parse_packet(packet) - del self.buffer[:11] - if parsed is not None: - raw = { - 'head_pose': { - 'rotation': parsed['yaw'], # rotation = roll - 'tilt': parsed['roll'], # tilt = yaw - 'pitch': parsed['pitch'] # pitch = pitch - }, - 'temperature': parsed['temperature'], - 'timestamp': datetime.now().isoformat() - } - # logger.debug(f'映射后的头部姿态: {raw}') - return self.apply_calibration(raw) if apply_calibration else raw - raw = { - 'head_pose': { - 'rotation': self.last_data['yaw'], - 'tilt': self.last_data['roll'], - 'pitch': self.last_data['pitch'] - }, - 'temperature': self.last_data['temperature'], - 'timestamp': datetime.now().isoformat() - } - return self.apply_calibration(raw) if apply_calibration else raw - except Exception as e: - logger.error(f'IMU数据读取异常: {e}', exc_info=True) - raw = { - 'head_pose': { - 'rotation': self.last_data['yaw'], - 'tilt': self.last_data['roll'], - 'pitch': self.last_data['pitch'] - }, - 'temperature': self.last_data['temperature'], - 'timestamp': datetime.now().isoformat() - } - return self.apply_calibration(raw) if apply_calibration else raw - - def __del__(self): - try: - if self.ser and getattr(self.ser, 'is_open', False): - self.ser.close() - logger.info('IMU设备串口已关闭') - except Exception: - pass - -class MockIMUDevice: - """模拟IMU设备""" - - 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 raw_data or 'head_pose' not in raw_data: - return raw_data - - calibrated_data = raw_data.copy() - head_pose = raw_data['head_pose'].copy() - head_pose['rotation'] = head_pose['rotation'] - self.head_pose_offset['rotation'] - head_pose['tilt'] = head_pose['tilt'] - self.head_pose_offset['tilt'] - head_pose['pitch'] = head_pose['pitch'] - self.head_pose_offset['pitch'] - calibrated_data['head_pose'] = head_pose - return calibrated_data - - def read_data(self, apply_calibration: bool = True) -> Dict[str, Any]: - """读取IMU数据""" - # 生成头部姿态角度数据,角度范围(-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 # 俯仰角:俯角(-), 仰角(+) - }, - 'timestamp': datetime.now().isoformat() - } - - # 应用校准并返回 - 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': round(left_front_pct), - 'left_rear': round(left_rear_pct), - 'right_front': round(right_front_pct), - 'right_rear': round(right_rear_pct), - 'left_total': round(left_total_pct), - 'right_total': round(right_total_pct), - 'total_pressure': round(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]: - """读取压力数据,模拟基于矩阵数据的真实设备行为""" - 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_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: - 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 - from io import BytesIO - - # 临时禁用PIL的调试日志 - pil_logger = logging.getLogger('PIL') - original_level = pil_logger.level - pil_logger.setLevel(logging.WARNING) - - # 创建图形 - fig, ax = plt.subplots(1, 1, figsize=(6, 8)) - 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) - - # 恢复PIL的日志级别 - pil_logger.setLevel(original_level) - - return f"data:image/png;base64,{image_base64}" - - except Exception as e: - # 确保在异常情况下也恢复PIL的日志级别 - try: - pil_logger.setLevel(original_level) - except: - pass - logger.warning(f"生成压力图片失败: {e}") - # 返回一个简单的占位符base64图片 - return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==" - - 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: - """视频推流管理器""" - - def __init__(self, socketio=None, device_manager=None): - self.socketio = socketio - self.device_manager = device_manager - self.device_index = None - self.video_thread = None - self.video_running = False - - # # 用于异步编码的线程池和队列 - self.encoding_executor = ThreadPoolExecutor(max_workers=2) - self.frame_queue = queue.Queue(maxsize=1) # 只保留最新的一帧 - - # 内存优化配置 - self.frame_skip_counter = 0 - self.FRAME_SKIP_RATIO = 1 # 每3帧发送1帧,减少网络和内存压力 - self.MAX_FRAME_SIZE = (640, 480) # 进一步减小帧尺寸以节省内存 - self.MAX_MEMORY_USAGE = 200 * 1024 * 1024 # 200MB内存限制 - self.memory_check_counter = 0 - # 移除了MEMORY_CHECK_INTERVAL,改为每30帧检查一次内存 - - # 读取RTSP配置 - self._load_rtsp_config() - - 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 - - end_time = time.time() - logger.info(f'[TIMING] RTSP配置加载完成,设备号: {self.device_index} - 耗时: {(end_time - start_time) * 1000:.2f}ms') - except Exception as 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): - """获取当前进程内存使用量(字节)""" - try: - process = psutil.Process(os.getpid()) - return process.memory_info().rss - except: - return 0 - - def async_encode_frame(self, frame, frame_count): - """异步编码帧 - 内存优化版本""" - try: - # 内存检查 - self.memory_check_counter += 1 - if self.memory_check_counter >= self.MEMORY_CHECK_INTERVAL: - self.memory_check_counter = 0 - current_memory = self.get_memory_usage() - if current_memory > self.MAX_MEMORY_USAGE: - logger.warning(f"内存使用过高: {current_memory / 1024 / 1024:.2f}MB,强制清理") - gc.collect() - # 如果内存仍然过高,跳过此帧 - if self.get_memory_usage() > self.MAX_MEMORY_USAGE: - del frame - return - - # 更激进的图像尺寸压缩以节省内存 - height, width = frame.shape[:2] - target_width, target_height = self.MAX_FRAME_SIZE - - if width > target_width or height > target_height: - # 计算缩放比例,保持宽高比 - scale_w = target_width / width - scale_h = target_height / height - scale = min(scale_w, scale_h) - - new_width = int(width * scale) - new_height = int(height * scale) - - # 使用更快的插值方法减少CPU使用 - frame = cv2.resize(frame, (new_width, new_height), interpolation=cv2.INTER_AREA) - self.device_manager._save_frame_to_cache(frame, 'camera') - # 优化JPEG编码参数:优先考虑速度和内存 - encode_param = [ - int(cv2.IMWRITE_JPEG_QUALITY), 50, # 进一步降低质量以减少内存使用 - int(cv2.IMWRITE_JPEG_OPTIMIZE), 1, # 启用优化 - int(cv2.IMWRITE_JPEG_PROGRESSIVE), 0 # 禁用渐进式以减少内存 - ] - - success, buffer = cv2.imencode('.jpg', frame, encode_param) - if not success: - logger.error('图像编码失败') - return - - # 立即释放frame内存 - del frame - - jpg_as_text = base64.b64encode(buffer).decode('utf-8') - - # 立即释放buffer内存 - del buffer - # 发送数据 - if self.socketio: - self.socketio.emit('video_frame', { - 'image': jpg_as_text, - 'frame_id': frame_count, - 'timestamp': time.time() - }) - - # 立即释放base64字符串 - del jpg_as_text - - except Exception as e: - logger.error(f'异步编码帧失败: {e}') - finally: - # 定期强制垃圾回收 - if self.memory_check_counter % 10 == 0: - gc.collect() - - def frame_encoding_worker(self): - """帧编码工作线程""" - while self.video_running: - try: - # 从队列获取帧 - frame, frame_count = self.frame_queue.get(timeout=1) - - # 提交到线程池进行异步编码 - self.encoding_executor.submit(self.async_encode_frame, frame, frame_count) - except queue.Empty: - continue - except Exception as e: - logger.error(f'帧编码工作线程异常: {e}') - - def generate_test_frame(self, frame_count): - """生成测试帧""" - width, height = self.MAX_FRAME_SIZE - - # 创建黑色背景 - frame = np.zeros((height, width, 3), dtype=np.uint8) - - # 添加动态元素 - timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3] - - # 添加时间戳 - cv2.putText(frame, timestamp, (10, 90), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 2) - - # 添加帧计数 - cv2.putText(frame, f'TEST Frame: {frame_count}', (10, 120), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 2) - - # 添加移动的圆形 - center_x = int(320 + 200 * np.sin(frame_count * 0.1)) - center_y = int(240 + 100 * np.cos(frame_count * 0.1)) - cv2.circle(frame, (center_x, center_y), 30, (255, 0, 0), -1) - - # 添加变化的矩形 - rect_size = int(50 + 30 * np.sin(frame_count * 0.05)) - cv2.rectangle(frame, (500, 200), (500 + rect_size, 200 + rect_size), (0, 0, 255), -1) - - return frame - - 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.info(f'[TIMING] 进入generate_video_frames - {datetime.now().strftime("%H:%M:%S.%f")[:-3]}') - - try: - 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: - # 设置相机属性(逐项记录耗时与是否成功) - 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': 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) - # encoding_thread.daemon = True - # encoding_thread.start() - - while self.video_running: - if use_test_mode: - # 使用测试模式生成帧 - frame = self.generate_test_frame(frame_count) - ret = True - else: - # 使用视频监控流,添加帧跳过机制减少延迟 - ret, frame = cap.read() - if not ret: - error_count += 1 - logger.debug(f'视频监控读取帧失败(第{error_count}次),尝试重连...') - if 'cap' in locals(): - cap.release() - - if error_count > 5: - logger.debug('视频监控连接失败次数过多,切换到测试模式') - use_test_mode = True - if self.socketio: - self.socketio.emit('video_status', {'status': 'switched', 'message': '已切换到测试视频源'}) - continue - - # 立即重连,不等待 - cap = cv2.VideoCapture(self.device_index) - if cap.isOpened(): - # 重连时应用相同的激进实时设置 - cap.set(cv2.CAP_PROP_BUFFERSIZE, 0) - cap.set(cv2.CAP_PROP_FPS, 60) - cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('M', 'J', 'P', 'G')) - cap.set(cv2.CAP_PROP_FRAME_WIDTH, width) - cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height) - continue - - error_count = 0 # 重置错误计数 - - # 内存优化的帧跳过策略 - # 减少跳帧数量,避免过度内存使用 - skip_count = 0 - while skip_count < 3: # 减少到最多跳过3帧 - temp_ret, temp_frame = cap.read() - if temp_ret: - # 立即释放之前的帧 - if 'frame' in locals(): - del frame - frame = temp_frame - skip_count += 1 - else: - break - - # 降低帧率以减少内存压力 - current_time = time.time() - if current_time - last_frame_time < 1/20: # 降低到20fps最大频率 - continue - last_frame_time = current_time - - frame_count += 1 - - # 实现帧跳过以减少内存和网络压力 - self.frame_skip_counter += 1 - - if self.frame_skip_counter % (self.FRAME_SKIP_RATIO + 1) != 0: - # 跳过此帧,立即释放内存 - del frame - continue - - try: - # 直接在主循环中执行帧处理逻辑(替代异步工作线程) - - # 内存检查 - self.memory_check_counter += 1 - if self.memory_check_counter % 30 == 0: - memory_usage = psutil.virtual_memory().percent - if memory_usage > 85: - logger.warning(f'内存使用率过高: {memory_usage}%,跳过当前帧') - del frame - continue - - # 按照MAX_FRAME_SIZE裁剪帧 - cropped_frame = frame.copy() - width, height = self.MAX_FRAME_SIZE - if cropped_frame.shape[1] > width or cropped_frame.shape[0] > height: - # 计算裁剪区域(居中裁剪) - start_x = max(0, (cropped_frame.shape[1] - width) // 2) - start_y = max(0, (cropped_frame.shape[0] - height) // 2) - end_x = min(cropped_frame.shape[1], start_x + width) - end_y = min(cropped_frame.shape[0], start_y + height) - cropped_frame = cropped_frame[start_y:end_y, start_x:end_x] - - # 保存帧到全局缓存 - if self.device_manager: - self.device_manager._save_frame_to_cache(cropped_frame, 'camera') - # 每1000帧记录一次缓存保存状态 - # if frame_count % 1000 == 0: - # logger.debug(f"视频推流已保存第 {frame_count} 帧到全局缓存") - else: - logger.warning("VideoStreamManager未关联DeviceManager,无法保存帧到缓存") - - # JPEG编码和socketio发送 - try: - # 使用较低的JPEG质量以节省内存 - encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 70] - result, buffer = cv2.imencode('.jpg', cropped_frame, encode_param) - - if result: - # 转换为base64字符串 - jpg_as_text = base64.b64encode(buffer).decode('utf-8') - - # 立即释放buffer内存 - del buffer - - # 发送数据 - if self.socketio: - self.socketio.emit('video_frame', { - 'image': jpg_as_text, - '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 - - except Exception as e: - logger.error(f'帧编码失败: {e}') - - # 立即释放帧内存 - del frame - del cropped_frame - - if frame_count % 60 == 0: # 每60帧记录一次 - - # 定期强制垃圾回收 - gc.collect() - - except Exception as e: - logger.error(f'帧队列处理失败: {e}') - - except Exception as e: - # logger.error(f'监控视频推流异常: {e}') - if self.socketio: - self.socketio.emit('video_status', {'status': 'error', 'message': f'推流异常: {str(e)}'}) - finally: - if 'cap' in locals(): - cap.release() - self.video_running = False - - - def start_video_stream(self): - """启动视频监控推流""" - try: - if self.video_thread and self.video_thread.is_alive(): - logger.warning('视频监控线程已在运行') - return {'status': 'already_running', 'message': '视频监控已在运行'} - - 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'[TIMING] 视频监控线程启动失败: {e}') - return {'status': 'error', 'message': f'视频监控线程启动失败: {str(e)}'} - - def stop_video_stream(self): - """停止视频监控推流""" - try: - self.video_running = False - logger.info('视频监控推流已停止') - return {'status': 'stopped', 'message': '视频监控推流已停止'} - - except Exception as e: - logger.error(f'停止视频监控推流失败: {e}') - return {'status': 'error', 'message': f'停止失败: {str(e)}'} - - def is_streaming(self): - """检查是否正在推流""" - return self.video_running - - def get_stream_status(self): - """获取推流状态""" - return { - 'running': self.video_running, - 'device_index': self.device_index, - 'thread_alive': self.video_thread.is_alive() if self.video_thread else False - } - - - def _collect_head_pose_data(self) -> Dict[str, Any]: - """采集头部姿态数据(从IMU设备获取)""" - try: - # 模拟IMU头部姿态数据 - head_pose = { - 'roll': np.random.uniform(-30, 30), - 'pitch': np.random.uniform(-30, 30), - 'yaw': np.random.uniform(-180, 180), - 'acceleration': { - 'x': np.random.uniform(-2, 2), - 'y': np.random.uniform(-2, 2), - 'z': np.random.uniform(8, 12) - }, - 'gyroscope': { - 'x': np.random.uniform(-5, 5), - 'y': np.random.uniform(-5, 5), - 'z': np.random.uniform(-5, 5) - }, - 'timestamp': datetime.now().isoformat() - } - return head_pose - except Exception as e: - logger.error(f'头部姿态数据采集失败: {e}') - return None - - def _collect_body_pose_data(self) -> Dict[str, Any]: - """采集身体姿态数据(从FemtoBolt深度相机获取)""" - try: - # 模拟身体姿态关键点数据 - body_pose = { - 'keypoints': { - 'head': {'x': 320, 'y': 100, 'confidence': 0.95}, - 'neck': {'x': 320, 'y': 150, 'confidence': 0.92}, - 'left_shoulder': {'x': 280, 'y': 180, 'confidence': 0.88}, - 'right_shoulder': {'x': 360, 'y': 180, 'confidence': 0.90}, - 'left_elbow': {'x': 250, 'y': 220, 'confidence': 0.85}, - 'right_elbow': {'x': 390, 'y': 220, 'confidence': 0.87}, - 'left_wrist': {'x': 220, 'y': 260, 'confidence': 0.82}, - 'right_wrist': {'x': 420, 'y': 260, 'confidence': 0.84}, - 'spine': {'x': 320, 'y': 250, 'confidence': 0.93}, - 'left_hip': {'x': 300, 'y': 350, 'confidence': 0.89}, - 'right_hip': {'x': 340, 'y': 350, 'confidence': 0.91}, - 'left_knee': {'x': 290, 'y': 450, 'confidence': 0.86}, - 'right_knee': {'x': 350, 'y': 450, 'confidence': 0.88}, - 'left_ankle': {'x': 285, 'y': 550, 'confidence': 0.83}, - 'right_ankle': {'x': 355, 'y': 550, 'confidence': 0.85} - }, - 'balance_score': np.random.uniform(0.6, 1.0), - 'center_of_mass': {'x': 320, 'y': 350}, - 'timestamp': datetime.now().isoformat() - } - return body_pose - except Exception as e: - logger.error(f'身体姿态数据采集失败: {e}') - return None - - def _capture_body_image(self, data_dir: Path, device_manager) -> Optional[str]: - """采集身体视频截图(从FemtoBolt深度相机获取)""" - try: - image = None - - # 检查是否有device_manager实例且FemtoBolt深度相机可用 - if (device_manager is not None and - FEMTOBOLT_AVAILABLE and - hasattr(device_manager, 'femtobolt_camera') and - device_manager.femtobolt_camera is not None): - - # 从FemtoBolt深度相机获取真实图像 - logger.info('正在从FemtoBolt深度相机获取身体图像...') - capture = device_manager.femtobolt_camera.update() - - if capture is not None: - # 获取深度图像 - ret, depth_image = capture.get_depth_image() - if ret and depth_image is not None: - # 读取config.ini中的深度范围配置 - 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 - - # 优化深度图彩色映射,范围外用黑色,区间内用Jet模型从蓝色到黄色到红色渐变 - if depth_range_min is not None and depth_range_max is not None: - # 归一化深度值到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进行伪彩色映射 - depth_colored = cv2.applyColorMap(depth_normalized, cv2.COLORMAP_JET) - - # 范围外用黑色 - mask_outside = (depth_image < depth_range_min) | (depth_image > depth_range_max) - depth_colored[mask_outside] = [0, 0, 0] # BGR黑色 - else: - # 如果没有配置,使用默认伪彩色映射 - depth_colored = cv2.convertScaleAbs(depth_image, alpha=0.03) - depth_colored = cv2.applyColorMap(depth_colored, cv2.COLORMAP_JET) - - # 转换颜色格式(如果需要) - if len(depth_colored.shape) == 3 and depth_colored.shape[2] == 4: - depth_colored = cv2.cvtColor(depth_colored, cv2.COLOR_BGRA2BGR) - elif len(depth_colored.shape) == 3 and depth_colored.shape[2] == 3: - pass - - # 预处理:裁剪成宽460,高819,保持高度不裁剪,宽度从中间裁剪 - height, width = depth_colored.shape[:2] - target_width = 460 - target_height = 819 - - # 计算宽度裁剪起点 - if width > target_width: - left = (width - target_width) // 2 - right = left + target_width - cropped_image = depth_colored[:, left:right] - else: - cropped_image = depth_colored - - # 如果高度不足target_height,进行上下填充黑边 - cropped_height = cropped_image.shape[0] - if cropped_height < target_height: - pad_top = (target_height - cropped_height) // 2 - pad_bottom = target_height - cropped_height - pad_top - cropped_image = cv2.copyMakeBorder(cropped_image, pad_top, pad_bottom, 0, 0, cv2.BORDER_CONSTANT, value=[0,0,0]) - elif cropped_height > target_height: - # 如果高度超过target_height,裁剪高度中间部分 - top = (cropped_height - target_height) // 2 - cropped_image = cropped_image[top:top+target_height, :] - - # 最终调整大小,保持宽460,高819 - image = cv2.resize(cropped_image, (target_width, target_height)) - - logger.info(f'成功获取FemtoBolt深度图像,尺寸: {image.shape}') - else: - logger.warning('无法从FemtoBolt获取深度图像,使用模拟图像') - # 使用模拟图像作为备用 - image = np.zeros((819, 460, 3), dtype=np.uint8) - cv2.rectangle(image, (50, 50), (410, 769), (0, 255, 0), 2) - cv2.putText(image, 'FemtoBolt Unavailable', (75, 400), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2) - else: - logger.warning('FemtoBolt capture为None,使用模拟图像') - # 使用模拟图像作为备用 - image = np.zeros((819, 460, 3), dtype=np.uint8) - cv2.rectangle(image, (50, 50), (410, 769), (0, 255, 0), 2) - cv2.putText(image, 'Capture Failed', (120, 400), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2) - else: - logger.warning('FemtoBolt深度相机不可用,使用模拟图像') - # 使用模拟图像作为备用 - image = np.zeros((819, 460, 3), dtype=np.uint8) - cv2.rectangle(image, (50, 50), (410, 769), (0, 255, 0), 2) - cv2.putText(image, 'Camera Not Available', (60, 400), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2) - - # 保存图片 - image_path = data_dir / 'body_image.jpg' - cv2.imwrite(str(image_path), image) - logger.info(f'身体图像已保存到: {image_path}') - - return image_path - except Exception as e: - logger.error(f'身体截图保存失败: {e}') - return None - - def _collect_foot_pressure_data(self) -> Dict[str, Any]: - """采集足部压力数据(从压力传感器获取)""" - try: - # 模拟压力传感器数据 - pressure_data = { - 'left_foot': { - 'heel': np.random.uniform(0, 100), - 'arch': np.random.uniform(0, 50), - 'ball': np.random.uniform(0, 80), - 'toes': np.random.uniform(0, 60), - 'total_pressure': 0 - }, - 'right_foot': { - 'heel': np.random.uniform(0, 100), - 'arch': np.random.uniform(0, 50), - 'ball': np.random.uniform(0, 80), - 'toes': np.random.uniform(0, 60), - 'total_pressure': 0 - }, - 'balance_ratio': 0, - 'timestamp': datetime.now().isoformat() - } - - # 计算总压力和平衡比例 - left_total = sum(pressure_data['left_foot'][key] for key in ['heel', 'arch', 'ball', 'toes']) - right_total = sum(pressure_data['right_foot'][key] for key in ['heel', 'arch', 'ball', 'toes']) - - pressure_data['left_foot']['total_pressure'] = left_total - pressure_data['right_foot']['total_pressure'] = right_total - - if left_total + right_total > 0: - pressure_data['balance_ratio'] = left_total / (left_total + right_total) - - return pressure_data - except Exception as e: - logger.error(f'足部压力数据采集失败: {e}') - return None - - def _capture_foot_image(self, data_dir: Path, device_manager=None) -> Optional[str]: - """采集足部监测视频截图(从全局缓存获取)""" - try: - image = None - - # 直接使用self获取缓存帧 - logger.info('正在从全局缓存获取最新图像...') - - # 从全局缓存获取最新帧 - frame, frame_timestamp = device_manager._get_latest_frame_from_cache('camera') - #frame, frame_count = self.frame_queue.get(timeout=1) - if frame is not None: - # 使用缓存中的图像 - image = frame.copy() # 复制帧数据避免引用问题 - current_time = time.time() - frame_age = current_time - frame_timestamp if frame_timestamp else 0 - logger.info(f'成功获取缓存图像,尺寸: {image.shape},帧龄: {frame_age:.2f}秒') - else: - logger.warning('缓存中无可用图像,使用模拟图像') - image = np.zeros((480, 640, 3), dtype=np.uint8) - cv2.rectangle(image, (50, 50), (590, 430), (0, 255, 0), 2) - cv2.putText(image, 'No Cached Frame', (120, 250), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2) - - # 保存图片 - image_path = data_dir / 'foot_image.jpg' - cv2.imwrite(str(image_path), image) - logger.info(f'足部图像已保存到: {image_path}') - - return image_path - except Exception as e: - logger.error(f'足部截图保存失败: {e}') - # 即使出错也要保存一个模拟图像 - try: - image = np.zeros((480, 640, 3), dtype=np.uint8) - cv2.rectangle(image, (50, 50), (590, 430), (255, 0, 0), 2) - cv2.putText(image, 'Error Occurred', (180, 250), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2) - image_path = data_dir / 'foot_image.jpg' - cv2.imwrite(str(image_path), image) - return image_path - except Exception: - return None - - def _generate_foot_pressure_image(self, data_dir: Path) -> Optional[str]: - """生成足底压力数据图(从压力传感器数据生成)""" - try: - # 创建压力分布热力图 - fig_width, fig_height = 400, 600 - pressure_map = np.zeros((fig_height, fig_width, 3), dtype=np.uint8) - - # 模拟左脚压力分布 - left_foot_x = fig_width // 4 - left_foot_y = fig_height // 2 - - # 模拟右脚压力分布 - right_foot_x = 3 * fig_width // 4 - right_foot_y = fig_height // 2 - - # 绘制压力点(用不同颜色表示压力大小) - for i in range(20): - x = np.random.randint(left_foot_x - 50, left_foot_x + 50) - y = np.random.randint(left_foot_y - 100, left_foot_y + 100) - pressure = np.random.randint(0, 255) - cv2.circle(pressure_map, (x, y), 5, (0, pressure, 255 - pressure), -1) - - x = np.random.randint(right_foot_x - 50, right_foot_x + 50) - y = np.random.randint(right_foot_y - 100, right_foot_y + 100) - pressure = np.random.randint(0, 255) - cv2.circle(pressure_map, (x, y), 5, (0, pressure, 255 - pressure), -1) - - # 保存图片 - image_path = data_dir / 'foot_data_image.jpg' - cv2.imwrite(str(image_path), pressure_map) - - return str(image_path.relative_to(Path.cwd())) - except Exception as e: - logger.error(f'足底压力数据图生成失败: {e}') - return None - - def _save_screen_image(self, data_dir: Path, screen_image_base64: str) -> Optional[str]: - """保存屏幕录制截图(从前端传入的base64数据)""" - try: - # 解码base64数据 - if screen_image_base64.startswith('data:image/'): - # 移除data:image/jpeg;base64,前缀 - base64_data = screen_image_base64.split(',')[1] - else: - base64_data = screen_image_base64 - - # 解码并保存图片 - image_data = base64.b64decode(base64_data) - image_path = data_dir / 'screen_image.jpg' - - with open(image_path, 'wb') as f: - f.write(image_data) - - return str(image_path.relative_to(Path.cwd())) - except Exception as e: - logger.error(f'屏幕截图保存失败: {e}') - return None \ No newline at end of file diff --git a/backend/simple_camera_test.py b/backend/simple_camera_test.py deleted file mode 100644 index 4cfeb7e0..00000000 --- a/backend/simple_camera_test.py +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -简化的相机性能测试 -""" - -import sys -import os -import time -import logging - -# 添加项目路径 -sys.path.append(os.path.dirname(os.path.abspath(__file__))) - -from devices.utils.config_manager import ConfigManager -from devices.camera_manager import CameraManager - -# 设置日志级别 -logging.basicConfig( - level=logging.DEBUG, - format='%(asctime)s - %(levelname)s - %(message)s' -) - -def test_camera_init_time(): - """ - 测试相机初始化时间 - """ - print("相机初始化性能测试") - print("=" * 50) - - try: - # 创建管理器 - config_manager = ConfigManager() - camera_manager = CameraManager(None, config_manager) - - print("\n开始相机初始化测试...") - - # 记录总时间 - total_start = time.time() - - # 执行初始化 - success = camera_manager.initialize() - - total_time = (time.time() - total_start) * 1000 - - print(f"\n初始化结果: {'成功' if success else '失败'}") - print(f"总耗时: {total_time:.1f}ms ({total_time/1000:.1f}秒)") - - # 性能评估 - if total_time < 1000: - print("性能评级: 优秀 ⭐⭐⭐ (< 1秒)") - elif total_time < 3000: - print("性能评级: 良好 ⭐⭐ (< 3秒)") - elif total_time < 5000: - print("性能评级: 一般 ⭐ (< 5秒)") - else: - print("性能评级: 需要优化 ❌ (> 5秒)") - - if success: - # 测试校准 - print("\n测试校准性能...") - calibrate_start = time.time() - calibrate_success = camera_manager.calibrate() - calibrate_time = (time.time() - calibrate_start) * 1000 - - print(f"校准结果: {'成功' if calibrate_success else '失败'}") - print(f"校准耗时: {calibrate_time:.1f}ms") - - # 测试首帧获取 - if camera_manager.cap: - print("\n测试首帧获取...") - frame_start = time.time() - ret, frame = camera_manager.cap.read() - frame_time = (time.time() - frame_start) * 1000 - - if ret and frame is not None: - print(f"首帧获取成功 - 耗时: {frame_time:.1f}ms, 帧大小: {frame.shape}") - del frame - else: - print(f"首帧获取失败 - 耗时: {frame_time:.1f}ms") - - # 获取设备信息 - print("\n设备信息:") - device_info = camera_manager.get_device_info() - for key, value in device_info.items(): - if key in ['width', 'height', 'fps', 'fourcc']: - print(f" {key}: {value}") - - # 清理 - camera_manager.cleanup() - - except Exception as e: - print(f"\n❌ 测试失败: {e}") - import traceback - traceback.print_exc() - -def analyze_performance_bottlenecks(): - """ - 分析性能瓶颈 - """ - print("\n" + "=" * 50) - print("性能瓶颈分析") - print("=" * 50) - - print("\n根据测试结果,主要性能瓶颈可能包括:") - print("1. 相机打开 (CAP_PROP设置) - 通常耗时3-4秒") - print("2. 分辨率设置 - 可能耗时5-6秒") - print("3. FPS设置 - 可能耗时2-3秒") - print("4. 首帧读取 - 通常耗时300-400ms") - - print("\n优化建议:") - print("• 使用更快的相机后端 (如DirectShow)") - print("• 减少不必要的属性设置") - print("• 使用较低的分辨率进行初始化") - print("• 启用OpenCV优化") - print("• 设置合适的缓冲区大小") - -def main(): - print("相机启动性能优化测试") - print("目标: 将启动时间从10+秒优化到3秒以内") - - # 执行测试 - test_camera_init_time() - - # 分析结果 - analyze_performance_bottlenecks() - - print("\n" + "=" * 50) - print("测试完成") - print("=" * 50) - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/backend/test_avoid_duplicate_init.py b/backend/test_avoid_duplicate_init.py deleted file mode 100644 index b19ef323..00000000 --- a/backend/test_avoid_duplicate_init.py +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -测试避免重复初始化功能 -""" - -import requests -import time -import json - -def test_avoid_duplicate_initialization(): - """ - 测试避免重复初始化功能 - """ - base_url = "http://localhost:5000" - - print("=== 测试避免重复初始化功能 ===") - - # 1. 获取初始设备状态 - print("\n1. 获取初始设备状态") - try: - response = requests.get(f"{base_url}/api/devices/status") - if response.status_code == 200: - data = response.json() - print(f"设备状态: {json.dumps(data, indent=2, ensure_ascii=False)}") - else: - print(f"获取设备状态失败: {response.status_code}") - except Exception as e: - print(f"请求失败: {e}") - - # 2. 第一次启动设备数据推送 - print("\n2. 第一次启动设备数据推送") - try: - response = requests.post(f"{base_url}/api/devices/start_push") - if response.status_code == 200: - data = response.json() - print(f"第一次启动结果: {json.dumps(data, indent=2, ensure_ascii=False)}") - else: - print(f"第一次启动失败: {response.status_code}") - except Exception as e: - print(f"请求失败: {e}") - - # 等待一段时间 - print("\n等待5秒...") - time.sleep(5) - - # 3. 第二次启动设备数据推送(应该避免重复初始化) - print("\n3. 第二次启动设备数据推送(测试避免重复初始化)") - try: - response = requests.post(f"{base_url}/api/devices/start_push") - if response.status_code == 200: - data = response.json() - print(f"第二次启动结果: {json.dumps(data, indent=2, ensure_ascii=False)}") - else: - print(f"第二次启动失败: {response.status_code}") - except Exception as e: - print(f"请求失败: {e}") - - # 4. 再次获取设备状态 - print("\n4. 获取最终设备状态") - try: - response = requests.get(f"{base_url}/api/devices/status") - if response.status_code == 200: - data = response.json() - print(f"最终设备状态: {json.dumps(data, indent=2, ensure_ascii=False)}") - else: - print(f"获取设备状态失败: {response.status_code}") - except Exception as e: - print(f"请求失败: {e}") - - print("\n=== 测试完成 ===") - print("请查看终端日志,确认第二次启动时是否显示'已连接,跳过初始化'的消息") - -if __name__ == "__main__": - test_avoid_duplicate_initialization() \ No newline at end of file diff --git a/backend/test_backend_optimization.py b/backend/test_backend_optimization.py deleted file mode 100644 index 48b8330a..00000000 --- a/backend/test_backend_optimization.py +++ /dev/null @@ -1,235 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -OpenCV后端优化验证脚本 -测试DirectShow后端相对于MSMF的性能提升 -""" - -import sys -import os -import time -import logging - -# 添加项目路径 -sys.path.append(os.path.dirname(os.path.abspath(__file__))) - -from devices.camera_manager import CameraManager -from devices.utils.config_manager import ConfigManager - -# 配置日志 -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(levelname)s - %(message)s' -) -logger = logging.getLogger(__name__) - -def test_backend_performance(backend_name, test_name): - """ - 测试指定后端的性能 - - Args: - backend_name: 后端名称 (directshow, msmf) - test_name: 测试名称 - - Returns: - dict: 性能数据 - """ - print(f"\n{'='*60}") - print(f"📷 测试 {test_name} 后端性能") - print(f"{'='*60}") - - # 创建配置管理器并设置后端 - config_manager = ConfigManager() - - # 获取原始配置 - original_config = config_manager.get_device_config('camera') - - # 设置测试后端 - test_config = { - 'backend': backend_name - } - config_manager.set_camera_config(test_config) - - try: - # 创建相机管理器 - camera = CameraManager(None, config_manager) - - # 测试初始化性能 - start_time = time.time() - success = camera.initialize() - total_time = (time.time() - start_time) * 1000 - - if success: - print(f"✅ 相机初始化成功: {total_time:.1f}ms") - - # 获取实际后端信息 - if camera.cap: - backend_info = camera.cap.getBackendName() if hasattr(camera.cap, 'getBackendName') else 'Unknown' - actual_width = int(camera.cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - actual_height = int(camera.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - actual_fps = camera.cap.get(cv2.CAP_PROP_FPS) - print(f"🎯 实际后端: {backend_info}") - print(f"📐 实际分辨率: {actual_width}x{actual_height}@{actual_fps:.1f}fps") - - # 测试首帧获取 - frame_start = time.time() - ret, frame = camera.cap.read() if camera.cap else (False, None) - frame_time = (time.time() - frame_start) * 1000 - - if ret and frame is not None: - print(f"🖼️ 首帧获取: {frame_time:.1f}ms, 帧大小: {frame.shape}") - else: - print(f"❌ 首帧获取失败") - frame_time = -1 - - # 测试连续帧性能 - print(f"🎬 测试连续帧获取性能...") - frame_times = [] - for i in range(10): - frame_start = time.time() - ret, frame = camera.cap.read() if camera.cap else (False, None) - if ret: - frame_times.append((time.time() - frame_start) * 1000) - time.sleep(0.01) # 小延迟 - - if frame_times: - avg_frame_time = sum(frame_times) / len(frame_times) - min_frame_time = min(frame_times) - max_frame_time = max(frame_times) - print(f"📈 连续帧性能: 平均 {avg_frame_time:.1f}ms, 最快 {min_frame_time:.1f}ms, 最慢 {max_frame_time:.1f}ms") - else: - avg_frame_time = -1 - print(f"❌ 连续帧测试失败") - - # 清理资源 - camera.cleanup() - print(f"🧹 相机资源已释放") - - return { - 'backend': backend_name, - 'success': True, - 'init_time': total_time, - 'first_frame_time': frame_time, - 'avg_frame_time': avg_frame_time, - 'backend_info': backend_info if camera.cap else 'Unknown', - 'resolution': f"{actual_width}x{actual_height}@{actual_fps:.1f}fps" if camera.cap else "未知" - } - else: - print(f"❌ 初始化失败") - return { - 'backend': backend_name, - 'success': False, - 'init_time': total_time, - 'first_frame_time': -1, - 'avg_frame_time': -1, - 'backend_info': 'Failed', - 'resolution': "失败" - } - - except Exception as e: - print(f"❌ 测试异常: {e}") - return { - 'backend': backend_name, - 'success': False, - 'init_time': -1, - 'first_frame_time': -1, - 'avg_frame_time': -1, - 'backend_info': 'Exception', - 'resolution': "异常", - 'error': str(e) - } - finally: - # 恢复原始配置 - try: - restore_config = { - 'backend': original_config['backend'] - } - config_manager.set_camera_config(restore_config) - except Exception as e: - print(f"⚠️ 恢复配置失败: {e}") - -def main(): - """ - 主函数:测试不同后端的性能 - """ - print("\n" + "="*80) - print("🚀 OpenCV后端性能优化验证") - print("="*80) - - # 测试用例 - test_cases = [ - ('directshow', 'DirectShow (推荐)'), - ('msmf', 'MSMF (默认)') - ] - - results = [] - - # 执行测试 - for backend, name in test_cases: - result = test_backend_performance(backend, name) - results.append(result) - time.sleep(1) # 测试间隔 - - # 汇总结果 - print(f"\n\n{'='*80}") - print(f"📊 后端性能优化验证汇总") - print(f"{'='*80}") - - # 表格头 - print(f"{'后端':<12} {'状态':<8} {'初始化':<12} {'首帧':<12} {'连续帧':<12} {'实际后端':<15}") - print("-" * 80) - - successful_results = [r for r in results if r['success']] - - for result in results: - status = "✅成功" if result['success'] else "❌失败" - init_time = f"{result['init_time']:.1f}ms" if result['init_time'] > 0 else "失败" - first_frame = f"{result['first_frame_time']:.1f}ms" if result['first_frame_time'] > 0 else "失败" - avg_frame = f"{result['avg_frame_time']:.1f}ms" if result['avg_frame_time'] > 0 else "失败" - backend_info = result['backend_info'][:14] if len(result['backend_info']) > 14 else result['backend_info'] - - print(f"{result['backend']:<12} {status:<8} {init_time:<12} {first_frame:<12} {avg_frame:<12} {backend_info:<15}") - - # 性能分析 - if len(successful_results) >= 2: - print(f"\n📈 性能分析:") - - # 找到最快的后端 - fastest = min(successful_results, key=lambda x: x['init_time']) - slowest = max(successful_results, key=lambda x: x['init_time']) - - print(f"🏆 最快后端: {fastest['backend']} - {fastest['init_time']:.1f}ms") - print(f"🐌 最慢后端: {slowest['backend']} - {slowest['init_time']:.1f}ms") - - if fastest['init_time'] > 0 and slowest['init_time'] > 0: - improvement = ((slowest['init_time'] - fastest['init_time']) / slowest['init_time']) * 100 - print(f"💡 性能提升: {improvement:.1f}% (使用最快后端)") - - print(f"\n📋 详细性能对比:") - for result in successful_results: - if result != fastest: - slowdown = ((result['init_time'] - fastest['init_time']) / fastest['init_time']) * 100 - print(f" {result['backend']}: 比最快后端慢 {slowdown:.1f}% ({result['init_time']:.1f}ms vs {fastest['init_time']:.1f}ms)") - - print(f"\n🎯 建议:") - if successful_results: - fastest = min(successful_results, key=lambda x: x['init_time']) - print(f"✅ 推荐使用 {fastest['backend']} 后端以获得最佳性能") - print(f"📝 配置建议: 在配置文件中设置 backend = {fastest['backend']}") - - if fastest['init_time'] > 5000: - print(f"⚠️ 性能评级: 需要优化 (> 5秒)") - elif fastest['init_time'] > 2000: - print(f"⚠️ 性能评级: 一般 (2-5秒)") - else: - print(f"✅ 性能评级: 良好 (< 2秒)") - else: - print(f"❌ 所有后端测试失败,请检查相机连接") - - print(f"\n{'='*80}") - print(f"测试完成") - print(f"{'='*80}") - -if __name__ == "__main__": - import cv2 # 需要导入cv2用于相机操作 - main() \ No newline at end of file diff --git a/backend/test_camera_analysis.py b/backend/test_camera_analysis.py deleted file mode 100644 index 3de1214a..00000000 --- a/backend/test_camera_analysis.py +++ /dev/null @@ -1,143 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -深入分析相机设备的行为 -""" - -import cv2 -import numpy as np -import time - -def analyze_camera_frame(frame, device_index): - """ - 分析相机帧的特征 - """ - print(f"\n=== 设备 {device_index} 帧分析 ===") - print(f"帧形状: {frame.shape}") - print(f"数据类型: {frame.dtype}") - print(f"数据范围: {frame.min()} - {frame.max()}") - - # 检查是否是纯色帧(可能是虚拟设备) - unique_colors = len(np.unique(frame.reshape(-1, frame.shape[-1]), axis=0)) - print(f"唯一颜色数量: {unique_colors}") - - # 检查帧的统计信息 - mean_values = np.mean(frame, axis=(0, 1)) - std_values = np.std(frame, axis=(0, 1)) - print(f"各通道均值: {mean_values}") - print(f"各通道标准差: {std_values}") - - # 检查是否是静态帧 - if np.all(std_values < 1.0): - print("⚠️ 这可能是一个静态/虚拟帧(标准差很小)") - - # 检查是否是纯黑帧 - if np.all(mean_values < 10): - print("⚠️ 这可能是一个黑色帧") - - # 检查帧的变化 - return frame - -def test_camera_devices(): - """ - 测试多个相机设备并比较帧内容 - """ - print("=== 相机设备详细分析 ===") - - devices_to_test = [0, 1] - frames = {} - - for device_index in devices_to_test: - print(f"\n--- 测试设备 {device_index} ---") - - try: - cap = cv2.VideoCapture(device_index, cv2.CAP_DSHOW) - - if cap.isOpened(): - print(f"设备 {device_index}: 成功打开") - - # 获取设备属性 - width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - fps = cap.get(cv2.CAP_PROP_FPS) - fourcc = int(cap.get(cv2.CAP_PROP_FOURCC)) - - print(f"分辨率: {width}x{height}") - print(f"帧率: {fps}") - print(f"编码: {fourcc}") - - # 读取多帧进行分析 - frames_list = [] - for i in range(5): - ret, frame = cap.read() - if ret and frame is not None: - frames_list.append(frame.copy()) - if i == 0: # 只分析第一帧 - frames[device_index] = analyze_camera_frame(frame, device_index) - else: - print(f"第{i+1}帧读取失败") - break - - # 检查帧间变化 - if len(frames_list) > 1: - diff = cv2.absdiff(frames_list[0], frames_list[-1]) - total_diff = np.sum(diff) - print(f"首末帧差异总和: {total_diff}") - - if total_diff < 1000: # 阈值可调整 - print("⚠️ 帧内容几乎没有变化,可能是虚拟设备") - else: - print("✓ 帧内容有变化,可能是真实相机") - - else: - print(f"设备 {device_index}: 无法打开") - - cap.release() - - except Exception as e: - print(f"设备 {device_index} 测试异常: {e}") - - # 比较不同设备的帧 - if 0 in frames and 1 in frames: - print("\n=== 设备间帧比较 ===") - diff = cv2.absdiff(frames[0], frames[1]) - total_diff = np.sum(diff) - print(f"设备0和设备1帧差异总和: {total_diff}") - - if total_diff < 1000: - print("⚠️ 两个设备的帧几乎相同,设备1可能是设备0的镜像或虚拟设备") - else: - print("✓ 两个设备的帧不同,可能是独立的相机") - -def check_system_cameras(): - """ - 检查系统中可用的相机设备 - """ - print("\n=== 系统相机设备检查 ===") - - available_cameras = [] - - # 测试前10个设备索引 - for i in range(10): - cap = cv2.VideoCapture(i, cv2.CAP_DSHOW) - if cap.isOpened(): - ret, _ = cap.read() - if ret: - available_cameras.append(i) - print(f"设备 {i}: 可用") - else: - print(f"设备 {i}: 打开但无法读取") - else: - print(f"设备 {i}: 不可用") - cap.release() - - # 避免测试太多设备 - if len(available_cameras) >= 3: - break - - print(f"\n发现 {len(available_cameras)} 个可用相机设备: {available_cameras}") - return available_cameras - -if __name__ == "__main__": - check_system_cameras() - test_camera_devices() \ No newline at end of file diff --git a/backend/test_camera_disconnect.py b/backend/test_camera_disconnect.py deleted file mode 100644 index 8d37978a..00000000 --- a/backend/test_camera_disconnect.py +++ /dev/null @@ -1,194 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -相机断开连接测试脚本 -测试相机USB拔出时是否能正常检测设备断连并发送socket信息 -""" - -import time -import threading -import logging -from devices.camera_manager import CameraManager -from devices.utils.config_manager import ConfigManager -from unittest.mock import Mock - -# 配置日志 -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' -) -logger = logging.getLogger(__name__) - -class MockSocketIO: - """模拟SocketIO用于测试""" - - def __init__(self): - self.events = [] - self.lock = threading.Lock() - - def emit(self, event, data, namespace=None): - """记录发送的事件""" - with self.lock: - self.events.append({ - 'event': event, - 'data': data, - 'namespace': namespace, - 'timestamp': time.time() - }) - logger.info(f"Socket事件: {event} -> {data} (namespace: {namespace})") - - def get_events(self): - """获取所有事件""" - with self.lock: - return self.events.copy() - - def clear_events(self): - """清空事件记录""" - with self.lock: - self.events.clear() - -def test_camera_disconnect_detection(): - """ - 测试相机断开连接检测功能 - """ - logger.info("="*60) - logger.info("开始测试相机断开连接检测功能") - logger.info("="*60) - - # 创建模拟SocketIO - mock_socketio = MockSocketIO() - - # 创建配置管理器 - config_manager = ConfigManager() - - # 创建相机管理器 - camera_manager = CameraManager(mock_socketio, config_manager) - - try: - # 1. 初始化相机 - logger.info("\n步骤1: 初始化相机设备") - if not camera_manager.initialize(): - logger.error("相机初始化失败,无法进行测试") - return False - - logger.info(f"相机初始化成功 - 连接状态: {camera_manager.is_connected}") - - # 2. 启动数据流 - logger.info("\n步骤2: 启动相机数据流") - if not camera_manager.start_streaming(): - logger.error("相机数据流启动失败") - return False - - logger.info("相机数据流启动成功") - - # 3. 等待一段时间让系统稳定 - logger.info("\n步骤3: 等待系统稳定 (5秒)") - time.sleep(5) - - # 清空之前的事件记录 - mock_socketio.clear_events() - - # 4. 提示用户拔出USB - logger.info("\n步骤4: 请拔出相机USB连接线") - logger.info("等待30秒来检测断开连接...") - - start_time = time.time() - disconnect_detected = False - - # 监控30秒 - while time.time() - start_time < 30: - # 检查连接状态 - if camera_manager.is_connected: - logger.debug(f"相机仍然连接中... (已等待 {time.time() - start_time:.1f}秒)") - else: - logger.info(f"检测到相机断开连接! (耗时 {time.time() - start_time:.1f}秒)") - disconnect_detected = True - break - - time.sleep(1) - - # 5. 分析结果 - logger.info("\n步骤5: 分析测试结果") - - if disconnect_detected: - logger.info("✓ 相机断开连接检测: 成功") - else: - logger.warning("✗ 相机断开连接检测: 失败 (30秒内未检测到断开)") - - # 检查Socket事件 - events = mock_socketio.get_events() - disconnect_events = [e for e in events if 'status' in str(e.get('data', {})) and 'disconnect' in str(e.get('data', {})).lower()] - - if disconnect_events: - logger.info(f"✓ Socket断开通知: 成功 (发送了 {len(disconnect_events)} 个断开事件)") - for event in disconnect_events: - logger.info(f" - 事件: {event['event']}, 数据: {event['data']}") - else: - logger.warning("✗ Socket断开通知: 失败 (未发送断开事件)") - - # 6. 测试重连机制 - if disconnect_detected: - logger.info("\n步骤6: 测试重连机制") - logger.info("请重新插入相机USB连接线") - logger.info("等待30秒来检测重新连接...") - - start_time = time.time() - reconnect_detected = False - - while time.time() - start_time < 30: - if camera_manager.is_connected: - logger.info(f"检测到相机重新连接! (耗时 {time.time() - start_time:.1f}秒)") - reconnect_detected = True - break - - time.sleep(1) - - if reconnect_detected: - logger.info("✓ 相机重连检测: 成功") - else: - logger.warning("✗ 相机重连检测: 失败 (30秒内未检测到重连)") - - # 7. 显示所有Socket事件 - logger.info("\n步骤7: 所有Socket事件记录") - all_events = mock_socketio.get_events() - if all_events: - for i, event in enumerate(all_events, 1): - logger.info(f" {i}. 事件: {event['event']}, 数据: {event['data']}, 时间: {time.strftime('%H:%M:%S', time.localtime(event['timestamp']))}") - else: - logger.info(" 无Socket事件记录") - - return disconnect_detected - - except Exception as e: - logger.error(f"测试过程中发生异常: {e}") - return False - - finally: - # 清理资源 - try: - camera_manager.stop_streaming() - camera_manager.disconnect() - logger.info("测试资源清理完成") - except Exception as e: - logger.error(f"清理资源时发生异常: {e}") - -def main(): - """ - 主函数 - """ - logger.info("相机断开连接测试脚本") - logger.info("此脚本将测试相机USB拔出时的断连检测和Socket通知功能") - logger.info("") - - # 运行测试 - success = test_camera_disconnect_detection() - - logger.info("\n" + "="*60) - if success: - logger.info("测试完成: 相机断开连接检测功能正常") - else: - logger.info("测试完成: 相机断开连接检测功能存在问题") - logger.info("="*60) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/backend/test_camera_full.py b/backend/test_camera_full.py deleted file mode 100644 index eab1654c..00000000 --- a/backend/test_camera_full.py +++ /dev/null @@ -1,212 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -完整的相机断开连接测试脚本 -模拟主程序的完整流程 -""" - -import sys -import os -sys.path.append(os.path.dirname(os.path.abspath(__file__))) - -import time -import threading -import logging -from datetime import datetime -from devices.camera_manager import CameraManager -from devices.utils.config_manager import ConfigManager - -# 配置日志 -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' -) -logger = logging.getLogger(__name__) - -class MockSocketIO: - """模拟SocketIO用于测试""" - - def __init__(self): - self.events = [] - self.lock = threading.Lock() - - def emit(self, event, data, namespace=None): - """记录发送的事件""" - with self.lock: - self.events.append({ - 'event': event, - 'data': data, - 'namespace': namespace, - 'timestamp': time.time() - }) - logger.info(f"Socket事件: {event} -> {data} (namespace: {namespace})") - - def get_events(self): - """获取所有事件""" - with self.lock: - return self.events.copy() - - def clear_events(self): - """清空事件记录""" - with self.lock: - self.events.clear() - -class MockAppServer: - """模拟主程序服务器""" - - def __init__(self): - self.socketio = MockSocketIO() - self.logger = logger - self.device_managers = {} - - def broadcast_device_status(self, device_name: str, is_connected: bool): - """广播单个设备状态""" - if self.socketio: - try: - status_data = { - 'device_type': device_name, - 'status': is_connected, - 'timestamp': datetime.now().isoformat() - } - self.socketio.emit('device_status', status_data, namespace='/devices') - self.logger.info(f'广播设备状态: {device_name} -> {"已连接" if is_connected else "未连接"}') - except Exception as e: - self.logger.error(f'广播设备状态失败: {e}') - - def _on_device_status_change(self, device_name: str, is_connected: bool): - """设备状态变化回调函数""" - self.logger.info(f'设备状态变化: {device_name} -> {"已连接" if is_connected else "未连接"}') - self.broadcast_device_status(device_name, is_connected) - -def test_camera_disconnect_with_socket(): - """测试相机断开连接和Socket通知""" - logger.info("="*60) - logger.info("开始测试相机断开连接和Socket通知功能") - logger.info("="*60) - - # 创建模拟服务器 - app_server = MockAppServer() - - try: - # 创建配置管理器 - config_manager = ConfigManager() - - # 创建相机管理器 - camera_manager = CameraManager(app_server.socketio, config_manager) - app_server.device_managers['camera'] = camera_manager - - # 添加状态变化回调(模拟主程序的回调注册) - camera_manager.add_status_change_callback(app_server._on_device_status_change) - - # 1. 测试初始化 - logger.info("\n步骤1: 初始化相机设备") - if camera_manager.initialize(): - logger.info(f"✓ 相机初始化成功 - 连接状态: {camera_manager.is_connected}") - else: - logger.warning("✗ 相机初始化失败") - return False - - # 清空初始化时的事件 - app_server.socketio.clear_events() - - # 2. 启动数据流(可选) - logger.info("\n步骤2: 启动相机数据流") - try: - if camera_manager.start_streaming(app_server.socketio): - logger.info("✓ 相机数据流启动成功") - time.sleep(2) # 让数据流稳定 - else: - logger.warning("✗ 相机数据流启动失败") - except Exception as e: - logger.warning(f"数据流启动异常: {e}") - - # 3. 监控连接状态变化 - logger.info("\n步骤3: 监控连接状态变化 (30秒)") - logger.info("请在此期间拔出相机USB连接线来测试断开检测...") - - start_time = time.time() - last_status = camera_manager.is_connected - disconnect_detected = False - - while time.time() - start_time < 30: - current_status = camera_manager.is_connected - - if current_status != last_status: - elapsed_time = time.time() - start_time - logger.info(f"检测到状态变化: {'连接' if current_status else '断开'} (耗时: {elapsed_time:.1f}秒)") - last_status = current_status - - if not current_status: - logger.info("✓ 成功检测到相机断开!") - disconnect_detected = True - time.sleep(2) # 等待事件处理完成 - break - - time.sleep(0.5) - - # 4. 检查Socket事件 - logger.info("\n步骤4: 检查Socket事件") - events = app_server.socketio.get_events() - - if events: - logger.info(f"✓ 共记录到 {len(events)} 个Socket事件:") - disconnect_events = 0 - for i, event in enumerate(events, 1): - logger.info(f" {i}. 事件: {event['event']}, 数据: {event['data']}, 命名空间: {event['namespace']}") - if event['event'] == 'device_status' and event['data'].get('status') == False: - disconnect_events += 1 - - if disconnect_events > 0: - logger.info(f"✓ 检测到 {disconnect_events} 个设备断开事件") - else: - logger.warning("✗ 未检测到设备断开事件") - else: - logger.warning("✗ 未记录到任何Socket事件") - - # 5. 测试结果总结 - logger.info("\n步骤5: 测试结果总结") - - if disconnect_detected: - logger.info("✓ 硬件断开检测: 成功") - else: - logger.warning("✗ 硬件断开检测: 失败 (30秒内未检测到断开)") - - if events and any(e['event'] == 'device_status' and e['data'].get('status') == False for e in events): - logger.info("✓ Socket断开通知: 成功") - else: - logger.warning("✗ Socket断开通知: 失败") - - return disconnect_detected and len(events) > 0 - - except Exception as e: - logger.error(f"测试过程中发生异常: {e}") - import traceback - traceback.print_exc() - return False - - finally: - # 清理资源 - try: - if 'camera_manager' in locals(): - camera_manager.stop_streaming() - camera_manager.disconnect() - logger.info("测试资源清理完成") - except Exception as e: - logger.error(f"清理资源时发生异常: {e}") - -def main(): - """主函数""" - logger.info("相机断开连接完整测试脚本") - logger.info("此脚本将模拟主程序的完整流程,测试相机USB拔出时的断连检测和Socket通知功能") - - success = test_camera_disconnect_with_socket() - - logger.info("\n" + "="*60) - if success: - logger.info("测试完成: 相机断开连接检测和Socket通知功能正常") - else: - logger.info("测试完成: 相机断开连接检测和Socket通知功能存在问题") - logger.info("="*60) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/backend/test_camera_performance.py b/backend/test_camera_performance.py deleted file mode 100644 index 9568a1a8..00000000 --- a/backend/test_camera_performance.py +++ /dev/null @@ -1,217 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -相机启动性能测试脚本 -""" - -import sys -import os -import time -import logging -from typing import Dict, Any - -# 添加项目路径 -sys.path.append(os.path.dirname(os.path.abspath(__file__))) - -from devices.utils.config_manager import ConfigManager -from devices.camera_manager import CameraManager - -# 设置日志级别为DEBUG以查看详细信息 -logging.basicConfig( - level=logging.DEBUG, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' -) - -def test_camera_startup_performance(): - """ - 测试相机启动性能 - """ - print("=" * 60) - print("相机启动性能测试") - print("=" * 60) - - try: - # 初始化配置管理器 - print("\n1. 初始化配置管理器...") - config_start = time.time() - config_manager = ConfigManager() - config_time = (time.time() - config_start) * 1000 - print(f"配置管理器初始化完成 (耗时: {config_time:.1f}ms)") - - # 创建相机管理器 - print("\n2. 创建相机管理器...") - manager_start = time.time() - camera_manager = CameraManager(None, config_manager) - manager_time = (time.time() - manager_start) * 1000 - print(f"相机管理器创建完成 (耗时: {manager_time:.1f}ms)") - - # 测试多次初始化以获得平均性能 - print("\n3. 执行相机初始化性能测试...") - test_rounds = 3 - init_times = [] - - for i in range(test_rounds): - print(f"\n--- 第 {i+1} 轮测试 ---") - - # 如果之前已连接,先断开 - if camera_manager.is_connected: - disconnect_start = time.time() - camera_manager.disconnect() - disconnect_time = (time.time() - disconnect_start) * 1000 - print(f"断开连接耗时: {disconnect_time:.1f}ms") - time.sleep(0.5) # 等待设备完全断开 - - # 执行初始化 - init_start = time.time() - success = camera_manager.initialize() - init_time = (time.time() - init_start) * 1000 - - if success: - print(f"✓ 初始化成功 (总耗时: {init_time:.1f}ms)") - init_times.append(init_time) - - # 测试校准性能 - calibrate_start = time.time() - calibrate_success = camera_manager.calibrate() - calibrate_time = (time.time() - calibrate_start) * 1000 - - if calibrate_success: - print(f"✓ 校准成功 (耗时: {calibrate_time:.1f}ms)") - else: - print(f"✗ 校准失败 (耗时: {calibrate_time:.1f}ms)") - - # 测试第一帧获取时间 - if camera_manager.cap: - first_frame_start = time.time() - ret, frame = camera_manager.cap.read() - first_frame_time = (time.time() - first_frame_start) * 1000 - - if ret and frame is not None: - print(f"✓ 首帧获取成功 (耗时: {first_frame_time:.1f}ms, 帧大小: {frame.shape})") - del frame # 释放内存 - else: - print(f"✗ 首帧获取失败 (耗时: {first_frame_time:.1f}ms)") - else: - print(f"✗ 初始化失败 (耗时: {init_time:.1f}ms)") - - time.sleep(1) # 测试间隔 - - # 性能统计 - print("\n" + "=" * 60) - print("性能统计结果") - print("=" * 60) - - if init_times: - avg_init_time = sum(init_times) / len(init_times) - min_init_time = min(init_times) - max_init_time = max(init_times) - - print(f"初始化性能统计 ({len(init_times)} 次成功测试):") - print(f" 平均耗时: {avg_init_time:.1f}ms") - print(f" 最快耗时: {min_init_time:.1f}ms") - print(f" 最慢耗时: {max_init_time:.1f}ms") - - # 性能评估 - if avg_init_time < 1000: # 1秒以内 - print(f" 性能评级: 优秀 ⭐⭐⭐") - elif avg_init_time < 3000: # 3秒以内 - print(f" 性能评级: 良好 ⭐⭐") - elif avg_init_time < 5000: # 5秒以内 - print(f" 性能评级: 一般 ⭐") - else: - print(f" 性能评级: 需要优化 ❌") - else: - print("❌ 所有初始化测试都失败了") - - # 获取设备信息 - if camera_manager.is_connected: - print("\n设备信息:") - device_info = camera_manager.get_device_info() - for key, value in device_info.items(): - print(f" {key}: {value}") - - # 清理资源 - print("\n4. 清理资源...") - cleanup_start = time.time() - camera_manager.cleanup() - cleanup_time = (time.time() - cleanup_start) * 1000 - print(f"资源清理完成 (耗时: {cleanup_time:.1f}ms)") - - except Exception as e: - print(f"\n❌ 测试过程中发生错误: {e}") - import traceback - traceback.print_exc() - -def test_streaming_startup(): - """ - 测试流媒体启动性能 - """ - print("\n" + "=" * 60) - print("流媒体启动性能测试") - print("=" * 60) - - try: - config_manager = ConfigManager() - camera_manager = CameraManager(None, config_manager) - - # 初始化相机 - print("\n1. 初始化相机...") - if not camera_manager.initialize(): - print("❌ 相机初始化失败,无法进行流媒体测试") - return - - # 测试流媒体启动 - print("\n2. 启动流媒体...") - streaming_start = time.time() - streaming_success = camera_manager.start_streaming() - streaming_time = (time.time() - streaming_start) * 1000 - - if streaming_success: - print(f"✓ 流媒体启动成功 (耗时: {streaming_time:.1f}ms)") - - # 等待几秒钟收集帧数据 - print("\n3. 收集性能数据...") - time.sleep(3) - - # 获取统计信息 - stats = camera_manager.get_stats() - print(f"\n流媒体性能统计:") - for key, value in stats.items(): - print(f" {key}: {value}") - - # 停止流媒体 - print("\n4. 停止流媒体...") - stop_start = time.time() - camera_manager.stop_streaming() - stop_time = (time.time() - stop_start) * 1000 - print(f"✓ 流媒体停止完成 (耗时: {stop_time:.1f}ms)") - else: - print(f"❌ 流媒体启动失败 (耗时: {streaming_time:.1f}ms)") - - # 清理 - camera_manager.cleanup() - - except Exception as e: - print(f"\n❌ 流媒体测试过程中发生错误: {e}") - import traceback - traceback.print_exc() - -def main(): - """ - 主函数 - """ - print("相机性能测试工具") - print("测试目标:优化相机启动时间,目标从10+秒降低到3秒以内") - - # 执行基本启动性能测试 - test_camera_startup_performance() - - # 执行流媒体启动性能测试 - test_streaming_startup() - - print("\n" + "=" * 60) - print("测试完成!") - print("=" * 60) - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/backend/test_camera_simple.py b/backend/test_camera_simple.py deleted file mode 100644 index b7778d8c..00000000 --- a/backend/test_camera_simple.py +++ /dev/null @@ -1,148 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -简化的相机断开连接测试脚本 -""" - -import sys -import os -sys.path.append(os.path.dirname(os.path.abspath(__file__))) - -import time -import threading -import logging -from devices.camera_manager import CameraManager -from devices.utils.config_manager import ConfigManager - -# 配置日志 -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' -) -logger = logging.getLogger(__name__) - -class MockSocketIO: - """模拟SocketIO用于测试""" - - def __init__(self): - self.events = [] - self.lock = threading.Lock() - - def emit(self, event, data, namespace=None): - """记录发送的事件""" - with self.lock: - self.events.append({ - 'event': event, - 'data': data, - 'namespace': namespace, - 'timestamp': time.time() - }) - logger.info(f"Socket事件: {event} -> {data} (namespace: {namespace})") - - def get_events(self): - """获取所有事件""" - with self.lock: - return self.events.copy() - -def test_camera_connection(): - """测试相机连接和断开检测""" - logger.info("="*60) - logger.info("开始测试相机连接和断开检测功能") - logger.info("="*60) - - # 创建模拟SocketIO - mock_socketio = MockSocketIO() - - try: - # 创建配置管理器 - config_manager = ConfigManager() - - # 创建相机管理器 - camera_manager = CameraManager(mock_socketio, config_manager) - - # 添加状态变化回调 - def status_callback(device_name, is_connected): - logger.info(f"状态回调: {device_name} -> {'连接' if is_connected else '断开'}") - - camera_manager.add_status_change_callback(status_callback) - - # 1. 测试初始化 - logger.info("\n步骤1: 初始化相机设备") - if camera_manager.initialize(): - logger.info(f"✓ 相机初始化成功 - 连接状态: {camera_manager.is_connected}") - else: - logger.warning("✗ 相机初始化失败") - return False - - # 2. 测试硬件连接检查 - logger.info("\n步骤2: 测试硬件连接检查") - hardware_connected = camera_manager.check_hardware_connection() - logger.info(f"硬件连接状态: {hardware_connected}") - - # 3. 启动连接监控 - logger.info("\n步骤3: 启动连接监控") - camera_manager._start_connection_monitor() - logger.info("连接监控已启动") - - # 4. 监控连接状态变化 - logger.info("\n步骤4: 监控连接状态 (30秒)") - logger.info("请在此期间拔出相机USB连接线来测试断开检测...") - - start_time = time.time() - last_status = camera_manager.is_connected - - while time.time() - start_time < 30: - current_status = camera_manager.is_connected - - if current_status != last_status: - logger.info(f"检测到状态变化: {'连接' if current_status else '断开'} (耗时: {time.time() - start_time:.1f}秒)") - last_status = current_status - - if not current_status: - logger.info("✓ 成功检测到相机断开!") - break - - time.sleep(1) - - # 5. 检查Socket事件 - logger.info("\n步骤5: 检查Socket事件") - events = mock_socketio.get_events() - - if events: - logger.info(f"共记录到 {len(events)} 个Socket事件:") - for i, event in enumerate(events, 1): - logger.info(f" {i}. {event['event']} -> {event['data']}") - else: - logger.info("未记录到Socket事件") - - return True - - except Exception as e: - logger.error(f"测试过程中发生异常: {e}") - return False - - finally: - # 清理资源 - try: - if 'camera_manager' in locals(): - camera_manager._stop_connection_monitor() - camera_manager.disconnect() - logger.info("测试资源清理完成") - except Exception as e: - logger.error(f"清理资源时发生异常: {e}") - -def main(): - """主函数""" - logger.info("相机断开连接测试脚本") - - success = test_camera_connection() - - logger.info("\n" + "="*60) - if success: - logger.info("测试完成: 相机断开连接检测功能测试完成") - else: - logger.info("测试完成: 相机断开连接检测功能存在问题") - logger.info("="*60) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/backend/test_opencv_backends.py b/backend/test_opencv_backends.py deleted file mode 100644 index 8a3f5db9..00000000 --- a/backend/test_opencv_backends.py +++ /dev/null @@ -1,305 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -OpenCV后端性能测试脚本 -测试不同OpenCV后端(DirectShow vs MSMF)的性能差异 -""" - -import sys -import os -import time -import logging -import cv2 - -# 添加项目路径 -sys.path.append(os.path.dirname(os.path.abspath(__file__))) - -# 配置日志 -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(levelname)s - %(message)s' -) -logger = logging.getLogger(__name__) - -def test_opencv_backend(backend_name, backend_id, device_index=0, width=1280, height=720, fps=30): - """ - 测试指定OpenCV后端的性能 - - Args: - backend_name: 后端名称 - backend_id: 后端ID - device_index: 设备索引 - width: 宽度 - height: 高度 - fps: 帧率 - - Returns: - dict: 性能数据 - """ - print(f"\n{'='*70}") - print(f"测试 {backend_name} 后端 (ID: {backend_id})") - print(f"分辨率: {width}x{height}, FPS: {fps}") - print(f"{'='*70}") - - result = { - 'backend_name': backend_name, - 'backend_id': backend_id, - 'success': False, - 'init_time': -1, - 'config_time': -1, - 'first_frame_time': -1, - 'total_time': -1, - 'actual_resolution': 'N/A', - 'error': None - } - - cap = None - - try: - # 1. 测试相机初始化时间 - print(f"📷 初始化相机 (后端: {backend_name})...") - init_start = time.time() - - # 创建VideoCapture对象并指定后端 - cap = cv2.VideoCapture(device_index, backend_id) - - if not cap.isOpened(): - print(f"❌ 无法打开相机 (后端: {backend_name})") - result['error'] = f"无法打开相机 (后端: {backend_name})" - return result - - init_time = (time.time() - init_start) * 1000 - result['init_time'] = init_time - print(f"✅ 相机初始化成功: {init_time:.1f}ms") - - # 2. 测试配置时间 - print(f"⚙️ 配置相机参数...") - config_start = time.time() - - # 设置分辨率和帧率 - cap.set(cv2.CAP_PROP_FRAME_WIDTH, width) - cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height) - cap.set(cv2.CAP_PROP_FPS, fps) - - # 设置缓冲区大小 - cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) - - # 性能优化设置 - try: - cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0.25) # 手动曝光 - cap.set(cv2.CAP_PROP_AUTO_WB, 0) # 禁用自动白平衡 - cap.set(cv2.CAP_PROP_EXPOSURE, -6) # 设置曝光值 - except Exception as e: - print(f"⚠️ 性能优化设置警告: {e}") - - config_time = (time.time() - config_start) * 1000 - result['config_time'] = config_time - print(f"✅ 配置完成: {config_time:.1f}ms") - - # 3. 获取实际分辨率 - actual_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - actual_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - actual_fps = cap.get(cv2.CAP_PROP_FPS) - result['actual_resolution'] = f"{actual_width}x{actual_height}@{actual_fps:.1f}fps" - print(f"🎯 实际参数: {actual_width}x{actual_height}, FPS: {actual_fps:.1f}") - - # 4. 测试首帧获取时间 - print(f"🖼️ 获取首帧...") - frame_start = time.time() - - ret, frame = cap.read() - - if ret and frame is not None: - first_frame_time = (time.time() - frame_start) * 1000 - result['first_frame_time'] = first_frame_time - print(f"✅ 首帧获取成功: {first_frame_time:.1f}ms, 帧大小: {frame.shape}") - else: - print(f"❌ 首帧获取失败") - result['error'] = "首帧获取失败" - return result - - # 5. 计算总时间 - total_time = init_time + config_time + first_frame_time - result['total_time'] = total_time - result['success'] = True - - print(f"📊 总耗时: {total_time:.1f}ms ({total_time/1000:.2f}秒)") - - # 6. 测试连续帧获取性能 - print(f"🎬 测试连续帧获取性能...") - frame_times = [] - test_frames = 10 - - for i in range(test_frames): - frame_start = time.time() - ret, frame = cap.read() - if ret: - frame_time = (time.time() - frame_start) * 1000 - frame_times.append(frame_time) - else: - break - - if frame_times: - avg_frame_time = sum(frame_times) / len(frame_times) - max_frame_time = max(frame_times) - min_frame_time = min(frame_times) - print(f"📈 连续帧性能: 平均 {avg_frame_time:.1f}ms, 最快 {min_frame_time:.1f}ms, 最慢 {max_frame_time:.1f}ms") - result['avg_frame_time'] = avg_frame_time - result['max_frame_time'] = max_frame_time - result['min_frame_time'] = min_frame_time - - return result - - except Exception as e: - print(f"❌ 测试异常: {e}") - result['error'] = str(e) - return result - - finally: - if cap: - cap.release() - print(f"🧹 相机资源已释放") - -def get_available_backends(): - """ - 获取可用的OpenCV后端 - - Returns: - list: 可用后端列表 - """ - backends = [ - ('DirectShow', cv2.CAP_DSHOW), - ('MSMF', cv2.CAP_MSMF), - ('V4L2', cv2.CAP_V4L2), - ('GStreamer', cv2.CAP_GSTREAMER), - ('Any', cv2.CAP_ANY) - ] - - available_backends = [] - - for name, backend_id in backends: - try: - # 尝试创建VideoCapture对象 - cap = cv2.VideoCapture(0, backend_id) - if cap.isOpened(): - available_backends.append((name, backend_id)) - cap.release() - except Exception: - pass - - return available_backends - -def main(): - """ - 主测试函数 - """ - print("🚀 OpenCV后端性能测试") - print(f"OpenCV版本: {cv2.__version__}") - - # 获取可用后端 - print("\n🔍 检测可用的OpenCV后端...") - available_backends = get_available_backends() - - if not available_backends: - print("❌ 未找到可用的相机后端") - return - - print(f"✅ 找到 {len(available_backends)} 个可用后端:") - for name, backend_id in available_backends: - print(f" - {name} (ID: {backend_id})") - - # 测试参数 - test_params = { - 'device_index': 0, - 'width': 1280, - 'height': 720, - 'fps': 30 - } - - print(f"\n📋 测试参数: {test_params['width']}x{test_params['height']}@{test_params['fps']}fps") - - # 执行测试 - results = [] - - for backend_name, backend_id in available_backends: - result = test_opencv_backend( - backend_name, - backend_id, - **test_params - ) - results.append(result) - - # 等待一下,避免设备冲突 - time.sleep(2) - - # 输出汇总结果 - print(f"\n{'='*90}") - print("📈 OpenCV后端性能测试汇总") - print(f"{'='*90}") - - print(f"{'后端':<12} {'状态':<8} {'初始化':<10} {'配置':<10} {'首帧':<10} {'总计':<10} {'实际分辨率':<20}") - print("-" * 90) - - successful_results = [] - - for result in results: - status = "✅成功" if result['success'] else "❌失败" - init_time = f"{result['init_time']:.1f}ms" if result['init_time'] > 0 else "N/A" - config_time = f"{result['config_time']:.1f}ms" if result['config_time'] > 0 else "N/A" - frame_time = f"{result['first_frame_time']:.1f}ms" if result['first_frame_time'] > 0 else "N/A" - total_time = f"{result['total_time']:.1f}ms" if result['total_time'] > 0 else "N/A" - - print(f"{result['backend_name']:<12} {status:<8} {init_time:<10} {config_time:<10} {frame_time:<10} {total_time:<10} {result['actual_resolution']:<20}") - - if result['success']: - successful_results.append(result) - - # 性能分析 - if len(successful_results) >= 2: - print(f"\n📊 性能分析:") - - # 找到最快和最慢的后端 - fastest = min(successful_results, key=lambda x: x['total_time']) - slowest = max(successful_results, key=lambda x: x['total_time']) - - print(f"🏆 最快后端: {fastest['backend_name']} - {fastest['total_time']:.1f}ms") - print(f"🐌 最慢后端: {slowest['backend_name']} - {slowest['total_time']:.1f}ms") - - if slowest['total_time'] > fastest['total_time']: - improvement = ((slowest['total_time'] - fastest['total_time']) / slowest['total_time']) * 100 - print(f"💡 性能提升: {improvement:.1f}% (使用最快后端)") - - # 详细对比 - print(f"\n📋 详细性能对比:") - for result in successful_results: - if result != fastest: - if result['total_time'] > fastest['total_time']: - slower = ((result['total_time'] - fastest['total_time']) / fastest['total_time']) * 100 - print(f" {result['backend_name']}: 比最快后端慢 {slower:.1f}% ({result['total_time']:.1f}ms vs {fastest['total_time']:.1f}ms)") - - elif len(successful_results) == 1: - result = successful_results[0] - print(f"\n📊 只有一个后端测试成功: {result['backend_name']} - {result['total_time']:.1f}ms") - - # 推荐建议 - print(f"\n🎯 建议:") - if successful_results: - fastest = min(successful_results, key=lambda x: x['total_time']) - print(f"✅ 推荐使用 {fastest['backend_name']} 后端以获得最佳性能") - print(f"📝 配置建议: 在相机初始化时指定后端 cv2.VideoCapture(device_index, cv2.CAP_{fastest['backend_name'].upper()})") - - if fastest['total_time'] < 3000: - print(f"🚀 性能评级: 优秀 (< 3秒)") - elif fastest['total_time'] < 5000: - print(f"⚡ 性能评级: 良好 (< 5秒)") - else: - print(f"⚠️ 性能评级: 需要优化 (> 5秒)") - else: - print(f"❌ 所有后端测试都失败了,请检查相机连接和驱动") - - print(f"\n{'='*90}") - print("测试完成") - print(f"{'='*90}") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/backend/test_opencv_behavior.py b/backend/test_opencv_behavior.py deleted file mode 100644 index b28cea02..00000000 --- a/backend/test_opencv_behavior.py +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -测试OpenCV VideoCapture的行为 -验证当设备索引不存在时VideoCapture的表现 -""" - -import cv2 -import time - -def test_video_capture_behavior(): - """ - 测试不同设备索引的VideoCapture行为 - """ - print("=== OpenCV VideoCapture 行为测试 ===") - print(f"OpenCV版本: {cv2.__version__}") - print() - - # 测试不同的设备索引 - test_indices = [0, 1, 2, 3, -1] - backends = [cv2.CAP_DSHOW, cv2.CAP_MSMF, cv2.CAP_ANY] - backend_names = ['CAP_DSHOW', 'CAP_MSMF', 'CAP_ANY'] - - for device_index in test_indices: - print(f"\n--- 测试设备索引 {device_index} ---") - - for backend, backend_name in zip(backends, backend_names): - print(f"\n后端: {backend_name}") - - try: - start_time = time.time() - cap = cv2.VideoCapture(device_index, backend) - open_time = (time.time() - start_time) * 1000 - - print(f" VideoCapture创建: 成功 (耗时: {open_time:.1f}ms)") - print(f" isOpened(): {cap.isOpened()}") - - if cap.isOpened(): - # 尝试读取帧 - start_time = time.time() - ret, frame = cap.read() - read_time = (time.time() - start_time) * 1000 - - print(f" read()返回值: ret={ret}") - if ret and frame is not None: - print(f" 帧形状: {frame.shape}") - print(f" 读取耗时: {read_time:.1f}ms") - else: - print(f" 读取失败 (耗时: {read_time:.1f}ms)") - - # 获取一些属性 - try: - width = cap.get(cv2.CAP_PROP_FRAME_WIDTH) - height = cap.get(cv2.CAP_PROP_FRAME_HEIGHT) - fps = cap.get(cv2.CAP_PROP_FPS) - print(f" 分辨率: {int(width)}x{int(height)}") - print(f" 帧率: {fps}") - except Exception as e: - print(f" 获取属性失败: {e}") - else: - print(" 相机未打开") - - cap.release() - - except Exception as e: - print(f" 异常: {e}") - - print("\n=== 测试完成 ===") - -def test_specific_case(): - """ - 专门测试device_index=1的情况 - """ - print("\n=== 专门测试 device_index=1 ===") - - try: - # 使用DSHOW后端(Windows默认) - cap = cv2.VideoCapture(1, cv2.CAP_DSHOW) - print(f"VideoCapture(1, CAP_DSHOW) 创建成功") - print(f"isOpened(): {cap.isOpened()}") - - if cap.isOpened(): - print("相机显示为已打开,但这可能是虚假的") - - # 尝试多次读取 - for i in range(3): - print(f"\n第{i+1}次读取:") - start_time = time.time() - ret, frame = cap.read() - read_time = (time.time() - start_time) * 1000 - - print(f" ret: {ret}") - print(f" frame is None: {frame is None}") - print(f" 耗时: {read_time:.1f}ms") - - if ret and frame is not None: - print(f" 帧形状: {frame.shape}") - print(f" 帧数据类型: {frame.dtype}") - print(f" 帧数据范围: {frame.min()} - {frame.max()}") - else: - print(" 读取失败或帧为空") - break - else: - print("相机未打开") - - cap.release() - - except Exception as e: - print(f"异常: {e}") - -if __name__ == "__main__": - test_video_capture_behavior() - test_specific_case() \ No newline at end of file diff --git a/backend/test_reconnection.py b/backend/test_reconnection.py deleted file mode 100644 index ceffe85b..00000000 --- a/backend/test_reconnection.py +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -设备重连机制测试脚本 -测试设备断开后的自动重连功能 -""" - -import time -import threading -from devices.camera_manager import CameraManager -from devices.imu_manager import IMUManager -from devices.femtobolt_manager import FemtoBoltManager -from devices.pressure_manager import PressureManager -import logging - -# 配置日志 -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' -) - -class MockSocketIO: - """模拟SocketIO用于测试""" - def emit(self, event, data): - print(f"[SocketIO] 发送事件: {event}, 数据: {data}") - -def test_device_reconnection(device_manager, device_name): - """测试设备重连机制""" - print(f"\n=== 测试 {device_name} 重连机制 ===") - - # 初始化设备 - print(f"1. 初始化 {device_name} 设备...") - success = device_manager.initialize() - print(f" 初始化结果: {'成功' if success else '失败'}") - - if success: - print(f" 设备连接状态: {'已连接' if device_manager.is_connected else '未连接'}") - - # 等待一段时间让连接稳定 - print("2. 等待连接稳定...") - time.sleep(3) - - # 模拟设备断开 - print("3. 模拟设备断开连接...") - device_manager.disconnect() - print(f" 断开后连接状态: {'已连接' if device_manager.is_connected else '未连接'}") - - # 等待一段时间 - print("4. 等待重连机制触发...") - time.sleep(5) - - # 尝试重新连接 - print("5. 尝试重新连接...") - reconnect_success = device_manager.initialize() - print(f" 重连结果: {'成功' if reconnect_success else '失败'}") - print(f" 重连后连接状态: {'已连接' if device_manager.is_connected else '未连接'}") - - # 清理 - device_manager.disconnect() - - print(f"=== {device_name} 重连测试完成 ===\n") - return success - -def main(): - """主测试函数""" - print("开始设备重连机制测试...") - - # 创建模拟SocketIO - mock_socketio = MockSocketIO() - - # 测试相机重连 - print("\n测试相机重连机制...") - camera_manager = CameraManager(mock_socketio) - test_device_reconnection(camera_manager, "相机") - - # 测试IMU重连 - print("\n测试IMU重连机制...") - imu_manager = IMUManager(mock_socketio) - test_device_reconnection(imu_manager, "IMU") - - # 测试FemtoBolt重连 - print("\n测试FemtoBolt重连机制...") - femtobolt_manager = FemtoBoltManager(mock_socketio) - test_device_reconnection(femtobolt_manager, "FemtoBolt") - - # 测试压力传感器重连 - print("\n测试压力传感器重连机制...") - pressure_manager = PressureManager(mock_socketio) - test_device_reconnection(pressure_manager, "压力传感器") - - print("\n所有设备重连测试完成!") - print("\n注意事项:") - print("1. 某些设备可能需要物理连接才能成功初始化") - print("2. 重连机制的效果取决于设备的实际可用性") - print("3. 观察日志中的连接监控线程启动和停止信息") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/backend/test_resolution_performance.py b/backend/test_resolution_performance.py deleted file mode 100644 index 78fdb51a..00000000 --- a/backend/test_resolution_performance.py +++ /dev/null @@ -1,214 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -分辨率性能测试脚本 -测试不同分辨率下相机配置的性能差异 -""" - -import sys -import os -import time -import logging - -# 添加项目路径 -sys.path.append(os.path.dirname(os.path.abspath(__file__))) - -from devices.camera_manager import CameraManager -from devices.utils.config_manager import ConfigManager - -# 配置日志 -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(levelname)s - %(message)s' -) -logger = logging.getLogger(__name__) - -def test_resolution_performance(width, height, test_name): - """ - 测试指定分辨率的性能 - - Args: - width: 宽度 - height: 高度 - test_name: 测试名称 - - Returns: - dict: 性能数据 - """ - print(f"\n{'='*60}") - print(f"测试 {test_name}: {width}x{height}") - print(f"{'='*60}") - - # 创建配置管理器并设置分辨率 - config_manager = ConfigManager() - - # 获取原始配置 - original_config = config_manager.get_device_config('camera') - - # 临时设置测试分辨率 - test_config = { - 'width': width, - 'height': height - } - config_manager.set_camera_config(test_config) - - try: - # 创建相机管理器 - camera = CameraManager(None, config_manager) - - # 测试初始化性能 - start_time = time.time() - success = camera.initialize() - total_time = (time.time() - start_time) * 1000 - - if success: - print(f"✅ 初始化成功") - print(f"📊 总耗时: {total_time:.1f}ms ({total_time/1000:.1f}秒)") - - # 获取实际分辨率 - if camera.cap: - actual_width = int(camera.cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - actual_height = int(camera.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - print(f"🎯 实际分辨率: {actual_width}x{actual_height}") - - # 测试首帧获取 - frame_start = time.time() - ret, frame = camera.cap.read() if camera.cap else (False, None) - frame_time = (time.time() - frame_start) * 1000 - - if ret and frame is not None: - print(f"🖼️ 首帧获取: {frame_time:.1f}ms, 帧大小: {frame.shape}") - else: - print(f"❌ 首帧获取失败") - frame_time = -1 - - # 清理资源 - camera.cleanup() - - return { - 'resolution': f"{width}x{height}", - 'success': True, - 'total_time': total_time, - 'frame_time': frame_time, - 'actual_resolution': f"{actual_width}x{actual_height}" if camera.cap else "未知" - } - else: - print(f"❌ 初始化失败") - return { - 'resolution': f"{width}x{height}", - 'success': False, - 'total_time': total_time, - 'frame_time': -1, - 'actual_resolution': "失败" - } - - except Exception as e: - print(f"❌ 测试异常: {e}") - return { - 'resolution': f"{width}x{height}", - 'success': False, - 'total_time': -1, - 'frame_time': -1, - 'actual_resolution': "异常", - 'error': str(e) - } - finally: - # 恢复原始配置 - try: - restore_config = { - 'width': original_config['width'], - 'height': original_config['height'] - } - config_manager.set_camera_config(restore_config) - except Exception as e: - print(f"⚠️ 恢复配置失败: {e}") - -def main(): - """ - 主测试函数 - """ - print("🚀 开始分辨率性能测试") - - # 测试不同分辨率 - test_cases = [ - (1280, 720, "当前分辨率"), - (640, 480, "标准VGA"), - (320, 240, "QVGA小分辨率"), - (160, 120, "极小分辨率") - ] - - results = [] - - for width, height, name in test_cases: - result = test_resolution_performance(width, height, name) - results.append(result) - - # 等待一下,避免设备冲突 - time.sleep(1) - - # 输出汇总结果 - print(f"\n{'='*80}") - print("📈 性能测试汇总") - print(f"{'='*80}") - - print(f"{'分辨率':<15} {'状态':<8} {'初始化耗时':<12} {'首帧耗时':<10} {'实际分辨率':<15}") - print("-" * 80) - - successful_results = [] - - for result in results: - status = "✅成功" if result['success'] else "❌失败" - init_time = f"{result['total_time']:.1f}ms" if result['total_time'] > 0 else "N/A" - frame_time = f"{result['frame_time']:.1f}ms" if result['frame_time'] > 0 else "N/A" - - print(f"{result['resolution']:<15} {status:<8} {init_time:<12} {frame_time:<10} {result['actual_resolution']:<15}") - - if result['success'] and result['total_time'] > 0: - successful_results.append(result) - - # 性能分析 - if len(successful_results) >= 2: - print(f"\n📊 性能分析:") - - # 找到最快和最慢的 - fastest = min(successful_results, key=lambda x: x['total_time']) - slowest = max(successful_results, key=lambda x: x['total_time']) - - print(f"🏆 最快配置: {fastest['resolution']} - {fastest['total_time']:.1f}ms") - print(f"🐌 最慢配置: {slowest['resolution']} - {slowest['total_time']:.1f}ms") - - if slowest['total_time'] > fastest['total_time']: - improvement = ((slowest['total_time'] - fastest['total_time']) / slowest['total_time']) * 100 - print(f"💡 性能提升: {improvement:.1f}% (使用最小分辨率)") - - # 基准对比 - baseline = next((r for r in successful_results if "1280x720" in r['resolution']), None) - if baseline: - print(f"\n📋 相对于当前分辨率(1280x720)的性能对比:") - for result in successful_results: - if result != baseline: - if result['total_time'] < baseline['total_time']: - improvement = ((baseline['total_time'] - result['total_time']) / baseline['total_time']) * 100 - print(f" {result['resolution']}: 快 {improvement:.1f}% ({result['total_time']:.1f}ms vs {baseline['total_time']:.1f}ms)") - else: - degradation = ((result['total_time'] - baseline['total_time']) / baseline['total_time']) * 100 - print(f" {result['resolution']}: 慢 {degradation:.1f}% ({result['total_time']:.1f}ms vs {baseline['total_time']:.1f}ms)") - - print(f"\n🎯 建议:") - if successful_results: - fastest = min(successful_results, key=lambda x: x['total_time']) - if fastest['total_time'] < 3000: # 小于3秒 - print(f"✅ 推荐使用 {fastest['resolution']} 以获得最佳性能") - else: - print(f"⚠️ 即使最快的分辨率 {fastest['resolution']} 仍需 {fastest['total_time']:.1f}ms") - print(f" 建议考虑其他优化方案(如更换相机后端)") - else: - print(f"❌ 所有测试都失败了,请检查相机连接") - - print(f"\n{'='*80}") - print("测试完成") - print(f"{'='*80}") - -if __name__ == "__main__": - import cv2 # 在这里导入cv2,避免在函数中导入 - main() \ No newline at end of file diff --git a/backend/testcamera.py b/backend/testcamera.py deleted file mode 100644 index 263025dc..00000000 --- a/backend/testcamera.py +++ /dev/null @@ -1,126 +0,0 @@ - -import cv2 - -class CameraViewer: - def __init__(self, device_index=0): - self.device_index = device_index - self.window_name = "Camera Viewer" - - def start_stream(self): - cap = cv2.VideoCapture(self.device_index) - if not cap.isOpened(): - print(f"无法打开摄像头设备 {self.device_index}") - return - - cv2.namedWindow(self.window_name, cv2.WINDOW_NORMAL) - - while True: - ret, frame = cap.read() - if not ret: - print("无法获取视频帧") - break - - cv2.imshow(self.window_name, frame) - - if cv2.waitKey(1) & 0xFF == ord('q'): - break - - cap.release() - cv2.destroyAllWindows() - -if __name__ == "__main__": - # 修改这里的数字可以切换不同摄像头设备 - viewer = CameraViewer(device_index=3) - viewer.start_stream() - -# import ctypes -# from ctypes import c_int, c_uint16, c_uint8, c_char, c_char_p, Structure, POINTER, byref - -# # 设备结构体,对应wrapper中FPMS_DEVICE_C -# class FPMS_DEVICE_C(Structure): -# _pack_ = 1 -# _fields_ = [ -# ("mn", c_uint16), -# ("sn", c_char * 64), -# ("fwVersion", c_uint16), -# ("protoVer", c_uint8), -# ("pid", c_uint16), -# ("vid", c_uint16), -# ("rows", c_uint16), -# ("cols", c_uint16), -# ] - -# # 加载DLL -# dll_path = r"D:\BodyBalanceEvaluation\backend\SMiTSenseUsbWrapper.dll" -# dll = ctypes.windll.LoadLibrary(dll_path) - -# # 函数原型声明 - -# # int fpms_usb_init_c(int debugFlag); -# dll.fpms_usb_init_c.argtypes = [c_int] -# dll.fpms_usb_init_c.restype = c_int - -# dll.fpms_usb_get_device_list_c.argtypes = [POINTER(FPMS_DEVICE_C), c_int] -# dll.fpms_usb_get_device_list_c.restype = c_int - -# dll.fpms_usb_open_c.argtypes = [POINTER(FPMS_DEVICE_C), POINTER(ctypes.c_void_p)] -# dll.fpms_usb_open_c.restype = c_int - -# # int fpms_usb_read_frame_c(void* handle, uint16_t* frame); -# dll.fpms_usb_read_frame_c.argtypes = [ctypes.c_void_p, POINTER(c_uint16)] -# dll.fpms_usb_read_frame_c.restype = c_int - -# # int fpms_usb_close_c(void* handle); -# dll.fpms_usb_close_c.argtypes = [ctypes.c_void_p] -# dll.fpms_usb_close_c.restype = c_int - -# # 其他函数如果需要可以类似声明 - -# def main(): -# # 初始化 -# ret = dll.fpms_usb_init_c(0) -# print(f"fpms_usb_init_c 返回值: {ret}") -# if ret != 0: -# print("初始化失败") -# return - -# MAX_DEVICES = 8 -# devices = (FPMS_DEVICE_C * MAX_DEVICES)() # 创建数组 -# count = dll.fpms_usb_get_device_list_c(devices, MAX_DEVICES) -# print(f"设备数量: {count}") -# if count <= 0: -# print("未找到设备或错误") -# return - -# for i in range(count): -# dev = devices[i] -# print(f"设备{i}: mn={dev.mn}, sn={dev.sn.decode(errors='ignore').rstrip(chr(0))}, fwVersion={dev.fwVersion}") - -# # 打开第一个设备 -# handle = ctypes.c_void_p() -# ret = dll.fpms_usb_open_c(byref(devices[0]), byref(handle)) -# print(f"fpms_usb_open_c 返回值: {ret}") -# if ret != 0: -# print("打开设备失败") -# return - -# # 假设帧大小是 rows * cols -# rows = devices[0].rows -# cols = devices[0].cols -# frame_size = rows * cols -# frame_buffer = (c_uint16 * frame_size)() - -# ret = dll.fpms_usb_read_frame_c(handle, frame_buffer) -# print(f"fpms_usb_read_frame_c 返回值: {ret}") -# if ret == 0: -# # 打印前10个数据看看 -# print("帧数据前10个点:", list(frame_buffer[:10])) -# else: -# print("读取帧失败") - -# # 关闭设备 -# ret = dll.fpms_usb_close_c(handle) -# print(f"fpms_usb_close_c 返回值: {ret}") - -# if __name__ == "__main__": -# main() diff --git a/config.ini b/config.ini index 32af1805..798e2872 100644 --- a/config.ini +++ b/config.ini @@ -35,7 +35,7 @@ chart_dpi = 300 export_format = csv [SECURITY] -secret_key = 332fe6a0e5b58a60e61eeee09cad362a7c47051202db7fa334256c2527371ecf +secret_key = 74d2f5ad774b449e6958cc5d30d77411c3560c9d0279e48154a847b744688989 session_timeout = 3600 max_login_attempts = 5 diff --git a/frontend/src/renderer/dist-electron-install/win-unpacked/resources/backend/Log/OrbbecSDK.log.txt b/frontend/src/renderer/dist-electron-install/win-unpacked/resources/backend/Log/OrbbecSDK.log.txt new file mode 100644 index 00000000..18664128 Binary files /dev/null and b/frontend/src/renderer/dist-electron-install/win-unpacked/resources/backend/Log/OrbbecSDK.log.txt differ diff --git a/frontend/src/renderer/dist-electron-install/win-unpacked/resources/config.ini b/frontend/src/renderer/dist-electron-install/win-unpacked/resources/config.ini new file mode 100644 index 00000000..e1d5f9f2 --- /dev/null +++ b/frontend/src/renderer/dist-electron-install/win-unpacked/resources/config.ini @@ -0,0 +1,41 @@ +[APP] +name = Body Balance Evaluation System +version = 1.0.0 +debug = false +log_level = INFO + +[SERVER] +host = 0.0.0.0 +port = 5000 +cors_origins = * + +[DATABASE] +path = backend/data/body_balance.db +backup_interval = 24 +max_backups = 7 + +[DEVICES] +camera_index = 0 +camera_width = 640 +camera_height = 480 +camera_fps = 30 +imu_port = COM3 +pressure_port = COM4 + +[DETECTION] +default_duration = 60 +sampling_rate = 30 +balance_threshold = 0.2 +posture_threshold = 5.0 + +[DATA_PROCESSING] +filter_window = 5 +outlier_threshold = 2.0 +chart_dpi = 300 +export_format = csv + +[SECURITY] +secret_key = c4939b252df4fff97f62644697ab798d7c0ccff8a8d9a592d0ffeb7675a44f92 +session_timeout = 3600 +max_login_attempts = 5 + diff --git a/设备管理优化方案.md b/设备管理优化方案.md deleted file mode 100644 index 2f7c849c..00000000 --- a/设备管理优化方案.md +++ /dev/null @@ -1,301 +0,0 @@ -# 设备管理优化方案 - -## 1. 现状分析 - -### 1.1 当前架构问题 - -当前的 `device_manager.py` 文件(3694行)存在以下问题: - -1. **单一职责原则违反**:一个类管理四种不同类型的设备 -2. **代码耦合度高**:设备间相互依赖,一个设备故障可能影响其他设备 -3. **维护困难**:代码量庞大,修改一个设备功能可能影响其他设备 -4. **性能瓶颈**:所有设备共享同一个推流线程池,资源竞争严重 -5. **扩展性差**:添加新设备类型需要修改核心管理器 -6. **测试复杂**:单元测试需要模拟所有设备 - -### 1.2 当前设备类型 - -- **FemtoBolt深度相机**:负责身体姿态检测和深度图像采集 -- **普通相机**:负责足部监控视频流 -- **IMU传感器**:负责头部姿态数据采集 -- **压力板传感器**:负责足底压力数据采集 - -## 2. 优化方案设计 - -### 2.1 架构设计原则 - -1. **单一职责原则**:每个设备类只负责自身的管理 -2. **开闭原则**:对扩展开放,对修改封闭 -3. **依赖倒置原则**:依赖抽象而非具体实现 -4. **接口隔离原则**:设备间通过标准接口通信 - -### 2.2 目标架构 - -``` -设备管理系统 -├── 抽象基类 (BaseDevice) -├── FemtoBolt深度相机管理器 (FemtoBoltManager) -├── 普通相机管理器 (CameraManager) -├── IMU传感器管理器 (IMUManager) -├── 压力板管理器 (PressureManager) -└── 设备协调器 (DeviceCoordinator) -``` - -### 2.3 文件结构 - -``` -backend/devices/ -├── __init__.py -├── base_device.py # 抽象基类 -├── femtobolt_manager.py # FemtoBolt深度相机管理 -├── camera_manager.py # 普通相机管理 -├── imu_manager.py # IMU传感器管理 -├── pressure_manager.py # 压力板管理 -├── device_coordinator.py # 设备协调器 -└── utils/ - ├── __init__.py - ├── socket_manager.py # Socket连接管理 - └── config_manager.py # 配置管理 -``` - -## 3. 详细设计 - -### 3.1 抽象基类设计 - -```python -# base_device.py -from abc import ABC, abstractmethod -from typing import Dict, Any, Optional -import threading -import logging - -class BaseDevice(ABC): - """设备抽象基类""" - - def __init__(self, device_name: str, config: Dict[str, Any]): - self.device_name = device_name - self.config = config - self.is_connected = False - self.is_streaming = False - self.socket_namespace = f"/{device_name}" - self.logger = logging.getLogger(f"device.{device_name}") - self._lock = threading.RLock() - - @abstractmethod - def initialize(self) -> bool: - """初始化设备""" - pass - - @abstractmethod - def calibrate(self) -> Dict[str, Any]: - """校准设备""" - pass - - @abstractmethod - def start_streaming(self, socketio) -> bool: - """启动数据推流""" - pass - - @abstractmethod - def stop_streaming(self) -> bool: - """停止数据推流""" - pass - - @abstractmethod - def get_status(self) -> Dict[str, Any]: - """获取设备状态""" - pass - - @abstractmethod - def cleanup(self) -> None: - """清理资源""" - pass -``` - -### 3.2 FemtoBolt深度相机管理器 - -```python -# femtobolt_manager.py -class FemtoBoltManager(BaseDevice): - """FemtoBolt深度相机管理器""" - - def __init__(self, config: Dict[str, Any]): - super().__init__("femtobolt", config) - self.camera = None - self.streaming_thread = None - self.frame_cache = {} - - def initialize(self) -> bool: - """初始化FemtoBolt深度相机""" - try: - # FemtoBolt初始化逻辑 - return True - except Exception as e: - self.logger.error(f"FemtoBolt初始化失败: {e}") - return False - - def start_streaming(self, socketio) -> bool: - """启动深度图像推流""" - # 独立的Socket.IO命名空间 - # 独立的推流线程 - pass -``` - -### 3.3 设备协调器 - -```python -# device_coordinator.py -class DeviceCoordinator: - """设备协调器 - 管理所有设备的生命周期""" - - def __init__(self): - self.devices = {} - self.socketio = None - - def register_device(self, device: BaseDevice): - """注册设备""" - self.devices[device.device_name] = device - - def initialize_all(self) -> Dict[str, bool]: - """初始化所有设备""" - results = {} - for name, device in self.devices.items(): - results[name] = device.initialize() - return results - - def start_all_streaming(self) -> Dict[str, bool]: - """启动所有设备推流""" - results = {} - for name, device in self.devices.items(): - if device.is_connected: - results[name] = device.start_streaming(self.socketio) - return results -``` - -## 4. 优势分析 - -### 4.1 性能优势 - -1. **并行处理**:每个设备独立的Socket.IO命名空间,减少数据传输冲突 -2. **资源隔离**:每个设备独立的线程池,避免资源竞争 -3. **内存优化**:设备级别的缓存管理,减少内存占用 -4. **故障隔离**:单个设备故障不影响其他设备运行 - -### 4.2 开发优势 - -1. **代码可维护性**:每个设备类代码量控制在500-800行 -2. **团队协作**:不同开发者可以并行开发不同设备 -3. **单元测试**:每个设备可以独立测试 -4. **版本控制**:设备功能变更影响范围小 - -### 4.3 扩展优势 - -1. **新设备接入**:只需实现BaseDevice接口 -2. **功能扩展**:设备功能扩展不影响其他设备 -3. **配置管理**:每个设备独立配置文件 -4. **部署灵活**:可以选择性部署某些设备 - -## 5. 劣势分析 - -### 5.1 复杂性增加 - -1. **架构复杂度**:从单一类变为多类协作 -2. **通信开销**:设备间通信需要额外的协调机制 -3. **状态同步**:多设备状态同步复杂度增加 - -### 5.2 开发成本 - -1. **重构工作量**:需要大量重构现有代码 -2. **测试工作量**:需要重新设计集成测试 -3. **文档更新**:需要更新相关文档和API - -### 5.3 运维复杂度 - -1. **监控复杂**:需要监控多个独立服务 -2. **故障排查**:跨设备问题排查难度增加 -3. **配置管理**:多个配置文件管理复杂 - -## 6. 实施方案 - -### 6.1 分阶段实施 - -#### 第一阶段:基础架构搭建(1-2周) -- 创建抽象基类和工具类 -- 设计Socket.IO命名空间方案 -- 搭建设备协调器框架 - -#### 第二阶段:设备迁移(3-4周) -- 按优先级迁移设备:Camera → IMU → Pressure → FemtoBolt -- 每个设备迁移后进行充分测试 -- 保持向后兼容性 - -#### 第三阶段:优化和集成(1-2周) -- 性能优化和内存管理 -- 集成测试和压力测试 -- 文档更新和代码审查 - -### 6.2 风险控制 - -1. **渐进式迁移**:保留原有代码作为备份 -2. **功能开关**:通过配置控制使用新旧架构 -3. **充分测试**:每个阶段都进行完整测试 -4. **回滚方案**:准备快速回滚到原架构的方案 - -### 6.3 Socket.IO命名空间设计 - -```javascript -// 前端连接示例 -const cameraSocket = io('/camera'); -const femtoboltSocket = io('/femtobolt'); -const imuSocket = io('/imu'); -const pressureSocket = io('/pressure'); - -// 独立的事件监听 -cameraSocket.on('video_frame', handleCameraFrame); -femtoboltSocket.on('depth_frame', handleDepthFrame); -imuSocket.on('imu_data', handleIMUData); -pressureSocket.on('pressure_data', handlePressureData); -``` - -## 7. 性能预期 - -### 7.1 性能提升预期 - -- **并发处理能力**:提升40-60% -- **内存使用效率**:降低20-30% -- **故障恢复时间**:减少50-70% -- **开发效率**:提升30-50% - -### 7.2 资源消耗 - -- **CPU使用**:可能增加5-10%(多线程开销) -- **内存使用**:减少20-30%(更好的缓存管理) -- **网络带宽**:基本持平(优化的数据传输) - -## 8. 结论和建议 - -### 8.1 可行性评估 - -**高度可行** - 该优化方案在技术上完全可行,且能显著改善系统的可维护性和性能。 - -### 8.2 推荐实施 - -**强烈推荐** - 考虑到当前代码的复杂度和未来的扩展需求,建议尽快实施该优化方案。 - -### 8.3 关键成功因素 - -1. **充分的测试**:确保每个阶段都有完整的测试覆盖 -2. **团队协作**:需要前后端团队密切配合 -3. **渐进式实施**:避免一次性大规模重构的风险 -4. **性能监控**:实施过程中持续监控系统性能 - -### 8.4 后续优化方向 - -1. **微服务化**:将设备管理器进一步拆分为独立的微服务 -2. **容器化部署**:使用Docker容器化部署各个设备服务 -3. **负载均衡**:为高负载设备添加负载均衡机制 -4. **监控告警**:建立完善的设备监控和告警系统 - ---- - -*本优化方案基于对现有代码的深入分析,结合软件工程最佳实践制定。实施过程中应根据实际情况灵活调整。* \ No newline at end of file