diff --git a/src/software/.env b/src/software/.env new file mode 100644 index 00000000..e9dbedcd Binary files /dev/null and b/src/software/.env differ diff --git a/src/software/.env.example b/src/software/.env.example new file mode 100644 index 00000000..e404fb3b --- /dev/null +++ b/src/software/.env.example @@ -0,0 +1,32 @@ +# 应用环境 +ENVIRONMENT=development # development, testing, production + +# 日志配置 +LOG_LEVEL=INFO # DEBUG, INFO, WARNING, ERROR, CRITICAL + +# Web服务器配置 +WEB_HOST=0.0.0.0 +WEB_PORT=8080 + +# 通信服务器配置 +COMM_HOST=0.0.0.0 +COMM_PORT=5000 + +# 无人机连接配置 +DRONE_CONNECTION_STRING=udpin:localhost:14550 + +# 基地位置 +BASE_LATITUDE=39.9 +BASE_LONGITUDE=116.3 + +# 飞行参数 +DEFAULT_ALTITUDE=10.0 +DEFAULT_AIRSPEED=3.0 + +# 安全设置 +ENABLE_GEO_FENCE=True +MAX_DISTANCE_FROM_BASE=5000.0 +MIN_BATTERY_LEVEL=30.0 + +# 其他配置 +ENABLE_SIMULATION=True \ No newline at end of file diff --git a/src/software/README.md b/src/software/README.md new file mode 100644 index 00000000..59555a21 --- /dev/null +++ b/src/software/README.md @@ -0,0 +1,73 @@ +# 智能战场医疗后送系统 + +基于无人机的智能战场医疗后送系统,用于快速、高效地运送医疗资源和伤员。 + +## 功能特点 + +- 实时伤员位置报告 +- 医疗资源运送 +- 无人机状态监控 +- 简单易用的操作界面 + +## 系统要求 + +- Python 3.8+ +- 无人机硬件(支持MAVLink协议) +- GPS模块 +- 通信模块(4G/5G或Wi-Fi) + +## 安装说明 + +1. 克隆项目 +```bash +git clone [项目地址] +``` + +2. 安装依赖 +```bash +pip install -r requirements.txt +``` + +3. 配置环境变量 +```bash +cp .env.example .env +# 编辑.env文件,填入必要的配置信息 +``` + +4. 运行系统 +```bash +python main.py +``` + +## 项目结构 + +``` +├── main.py # 主程序入口 +├── requirements.txt # 项目依赖 +├── config/ # 配置文件目录 +├── src/ # 源代码目录 +│ ├── drone/ # 无人机控制模块 +│ ├── communication/ # 通信模块 +│ ├── positioning/ # 定位模块 +│ └── ui/ # 用户界面模块 +└── tests/ # 测试文件目录 +``` + +## 使用说明 + +1. 启动系统后,通过Web界面登录 +2. 在地图上标记伤员位置 +3. 选择需要运送的医疗资源 +4. 发送任务指令给无人机 +5. 实时监控无人机状态和任务进度 + +## 注意事项 + +- 使用前请确保无人机电量充足 +- 确保GPS信号良好 +- 遵守相关法律法规 +- 注意战场环境安全 + +## 许可证 + +MIT License \ No newline at end of file diff --git a/src/software/__pycache__/main.cpython-39.pyc b/src/software/__pycache__/main.cpython-39.pyc new file mode 100644 index 00000000..c11c0ec4 Binary files /dev/null and b/src/software/__pycache__/main.cpython-39.pyc differ diff --git a/src/software/battlefield_medical_system.db b/src/software/battlefield_medical_system.db new file mode 100644 index 00000000..a6361691 Binary files /dev/null and b/src/software/battlefield_medical_system.db differ diff --git a/src/software/casualty_images/01.jpg b/src/software/casualty_images/01.jpg new file mode 100644 index 00000000..1b8e5593 Binary files /dev/null and b/src/software/casualty_images/01.jpg differ diff --git a/src/software/casualty_images/02.jpg b/src/software/casualty_images/02.jpg new file mode 100644 index 00000000..6b908ecc Binary files /dev/null and b/src/software/casualty_images/02.jpg differ diff --git a/src/software/casualty_images/03.jpg b/src/software/casualty_images/03.jpg new file mode 100644 index 00000000..b32f168c Binary files /dev/null and b/src/software/casualty_images/03.jpg differ diff --git a/src/software/casualty_images/04.jpg b/src/software/casualty_images/04.jpg new file mode 100644 index 00000000..2e92896b Binary files /dev/null and b/src/software/casualty_images/04.jpg differ diff --git a/src/software/casualty_images/05.jpg b/src/software/casualty_images/05.jpg new file mode 100644 index 00000000..c52540a1 Binary files /dev/null and b/src/software/casualty_images/05.jpg differ diff --git a/src/software/casualty_images/06.jpg b/src/software/casualty_images/06.jpg new file mode 100644 index 00000000..66ee4c7c Binary files /dev/null and b/src/software/casualty_images/06.jpg differ diff --git a/src/software/casualty_images/07.jpg b/src/software/casualty_images/07.jpg new file mode 100644 index 00000000..41d09e93 Binary files /dev/null and b/src/software/casualty_images/07.jpg differ diff --git a/src/software/casualty_images/08.jpg b/src/software/casualty_images/08.jpg new file mode 100644 index 00000000..7c3b085a Binary files /dev/null and b/src/software/casualty_images/08.jpg differ diff --git a/src/software/casualty_images/09.jpg b/src/software/casualty_images/09.jpg new file mode 100644 index 00000000..25b50c0d Binary files /dev/null and b/src/software/casualty_images/09.jpg differ diff --git a/src/software/casualty_images/10.jpg b/src/software/casualty_images/10.jpg new file mode 100644 index 00000000..bd089b76 Binary files /dev/null and b/src/software/casualty_images/10.jpg differ diff --git a/src/software/casualty_images/test01.jpg b/src/software/casualty_images/test01.jpg new file mode 100644 index 00000000..3d94400e Binary files /dev/null and b/src/software/casualty_images/test01.jpg differ diff --git a/src/software/config/__pycache__/config.cpython-39.pyc b/src/software/config/__pycache__/config.cpython-39.pyc new file mode 100644 index 00000000..1197eac3 Binary files /dev/null and b/src/software/config/__pycache__/config.cpython-39.pyc differ diff --git a/src/software/config/__pycache__/database.cpython-39.pyc b/src/software/config/__pycache__/database.cpython-39.pyc new file mode 100644 index 00000000..56718337 Binary files /dev/null and b/src/software/config/__pycache__/database.cpython-39.pyc differ diff --git a/src/software/config/config.py b/src/software/config/config.py new file mode 100644 index 00000000..3fce8879 --- /dev/null +++ b/src/software/config/config.py @@ -0,0 +1,110 @@ +import os +import logging + +# 基础配置 +class Config: + """基础配置类""" + + # 应用配置 + APP_NAME = "智能战场医疗后送系统" + VERSION = "1.0.0" + + # 日志配置 + LOG_LEVEL = "INFO" + LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + + # Web服务器配置 + WEB_HOST = "0.0.0.0" + WEB_PORT = 8080 + + # 通信服务器配置 + COMM_HOST = "0.0.0.0" + COMM_PORT = 5000 + + # 无人机连接配置 + DRONE_CONNECTION_STRING = "udpin:localhost:14550" + + # 基地位置(纬度、经度) + BASE_LATITUDE = 39.9 + BASE_LONGITUDE = 116.3 + + # 飞行参数 + DEFAULT_ALTITUDE = 10.0 # 默认飞行高度(米) + DEFAULT_AIRSPEED = 3.0 # 默认空速(米/秒) + + # 安全设置 + ENABLE_GEO_FENCE = True + MAX_DISTANCE_FROM_BASE = 5000.0 # 最大飞行距离(米) + MIN_BATTERY_LEVEL = 30.0 # 最低电量百分比 + + # 伤员检测配置 + CASUALTY_DETECTOR_MODEL_PATH = "models/yolov5s.pt" + DETECTION_THRESHOLD = 0.5 # 检测阈值 + + # 其他配置 + ENABLE_SIMULATION = True + + @staticmethod + def get_log_level(): + """获取日志级别""" + level_map = { + "DEBUG": logging.DEBUG, + "INFO": logging.INFO, + "WARNING": logging.WARNING, + "ERROR": logging.ERROR, + "CRITICAL": logging.CRITICAL + } + return level_map.get(Config.LOG_LEVEL, logging.INFO) + + @staticmethod + def setup_logging(): + """设置日志""" + logging.basicConfig( + level=Config.get_log_level(), + format=Config.LOG_FORMAT + ) + + # 降低第三方库的日志级别 + logging.getLogger("werkzeug").setLevel(logging.WARNING) + logging.getLogger("socketio").setLevel(logging.WARNING) + logging.getLogger("engineio").setLevel(logging.WARNING) + logging.getLogger("geventwebsocket").setLevel(logging.WARNING) + + logging.info(f"日志系统已初始化: 级别={Config.LOG_LEVEL}") + + +class DevelopmentConfig(Config): + """开发环境配置""" + ENABLE_SIMULATION = True + LOG_LEVEL = "DEBUG" + + +class ProductionConfig(Config): + """生产环境配置""" + ENABLE_SIMULATION = False + LOG_LEVEL = "INFO" + + +class TestingConfig(Config): + """测试环境配置""" + ENABLE_SIMULATION = True + LOG_LEVEL = "DEBUG" + WEB_PORT = 8081 + COMM_PORT = 5001 + + +# 根据环境变量选择配置 +def get_config(): + """获取当前环境的配置""" + env = os.environ.get("ENVIRONMENT", "development").lower() + + if env == "production": + return ProductionConfig + elif env == "testing": + return TestingConfig + else: + return DevelopmentConfig + + +# 导出当前配置 +current_config = get_config() \ No newline at end of file diff --git a/src/software/config/database.py b/src/software/config/database.py new file mode 100644 index 00000000..3b5434bc --- /dev/null +++ b/src/software/config/database.py @@ -0,0 +1,29 @@ +import os +import logging + +# 数据库配置 +class DatabaseConfig: + """数据库配置类""" + + # SQLite配置 + SQLITE_DATABASE_PATH = os.path.join(os.getcwd(), 'battlefield_medical_system.db') + + # 初始化配置 + def __init__(self): + self.logger = logging.getLogger(__name__) + self.logger.info("初始化数据库配置 - 使用SQLite") + + # SQLAlchemy配置 + @property + def SQLALCHEMY_DATABASE_URI(self): + return f"sqlite:///{self.SQLITE_DATABASE_PATH}" + + SQLALCHEMY_TRACK_MODIFICATIONS = False + SQLALCHEMY_ECHO = False + + def get_database_uri(self): + """获取数据库URI""" + return self.SQLALCHEMY_DATABASE_URI + +# 导出当前配置 +db_config = DatabaseConfig() \ No newline at end of file diff --git a/src/software/create_env.py b/src/software/create_env.py new file mode 100644 index 00000000..6626fcd8 --- /dev/null +++ b/src/software/create_env.py @@ -0,0 +1,14 @@ +with open('.env', 'w', encoding='utf-8') as f: + f.write('''# MySQL数据库配置 +MYSQL_HOST=localhost +MYSQL_PORT=3306 +MYSQL_USER=root +MYSQL_PASSWORD= +MYSQL_DATABASE=battlefield_medical_system +SQLALCHEMY_ECHO=False + +# 基本配置 +ENABLE_SIMULATION=True +''') + +print("已创建.env文件") \ No newline at end of file diff --git a/src/software/create_test_image.py b/src/software/create_test_image.py new file mode 100644 index 00000000..bce84714 --- /dev/null +++ b/src/software/create_test_image.py @@ -0,0 +1,168 @@ +import cv2 +import numpy as np +import os +import random + +# 创建目录 +os.makedirs('test_images', exist_ok=True) +os.makedirs('src/ui/static/training_images', exist_ok=True) + +def create_battlefield_scene(width=800, height=600, file_path="", for_training=False): + """创建战场场景图片""" + # 背景 - 草地/沙地 + background = np.ones((height, width, 3), dtype=np.uint8) * 150 + + # 添加一些纹理 + for i in range(0, height, 5): + for j in range(0, width, 5): + val = np.random.randint(120, 180) + background[i:i+5, j:j+5] = [val-30, val, val-50] # 草地/沙地颜色 + + # 添加几个可能是"伤员"的物体 + casualties = [] + + # 随机伤员数量 + casualties_count = random.randint(1, 5) + + for i in range(casualties_count): + # 随机位置 + x = random.randint(100, width-100) + y = random.randint(100, height-100) + + # 随机尺寸 + body_width = random.randint(40, 70) + body_height = random.randint(25, 45) + head_size = random.randint(15, 25) + + # 随机角度 + angle = random.randint(0, 360) + + # 随机颜色 - 模拟不同肤色和衣服 + body_color = ( + random.randint(20, 40), + random.randint(20, 40), + random.randint(80, 120) + ) + + head_color = ( + random.randint(60, 90), + random.randint(60, 90), + random.randint(130, 170) + ) + + # 绘制伤员 + cv2.ellipse(background, (x, y), (body_width, body_height), angle, 0, 360, body_color, -1) + + # 计算头部位置 - 考虑角度 + head_offset_x = int(np.cos(np.radians(angle)) * (body_width + head_size/2)) + head_offset_y = int(np.sin(np.radians(angle)) * (body_height + head_size/2)) + head_x = x + head_offset_x + head_y = y + head_offset_y + + # 确保头部在图像范围内 + head_x = max(head_size, min(width-head_size, head_x)) + head_y = max(head_size, min(height-head_size, head_y)) + + cv2.circle(background, (head_x, head_y), head_size, head_color, -1) + + # 保存伤员坐标 - 用于训练标注 + if for_training: + # 计算伤员边界框 (xmin, ymin, xmax, ymax) + xmin = max(0, x - body_width) + ymin = max(0, y - body_height) + xmax = min(width, x + body_width) + ymax = min(height, y + body_height) + + # 调整确保头部也在边界框内 + xmin = min(xmin, head_x - head_size) + ymin = min(ymin, head_y - head_size) + xmax = max(xmax, head_x + head_size) + ymax = max(ymax, head_y + head_size) + + # 存储为像素坐标 + casualties.append({ + 'x': int(xmin), + 'y': int(ymin), + 'width': int(xmax - xmin), + 'height': int(ymax - ymin), + 'class': 'casualty' + }) + + # 添加一些其他物体 - 障碍物、车辆等 + for _ in range(random.randint(3, 8)): + x = np.random.randint(50, width-50) + y = np.random.randint(50, height-50) + size = np.random.randint(10, 60) + color = ( + np.random.randint(0, 100), + np.random.randint(0, 100), + np.random.randint(0, 100) + ) + shape_type = random.choice(['circle', 'rectangle']) + + if shape_type == 'circle': + cv2.circle(background, (x, y), size, color, -1) + else: + rect_width = random.randint(size//2, size*2) + rect_height = random.randint(size//2, size*2) + cv2.rectangle( + background, + (x-rect_width//2, y-rect_height//2), + (x+rect_width//2, y+rect_height//2), + color, + -1 + ) + + # 添加一些模糊和噪声 + background = cv2.GaussianBlur(background, (5, 5), 0) + + # 添加一些噪声 + noise = np.zeros(background.shape, np.uint8) + cv2.randn(noise, (0, 0, 0), (10, 10, 10)) + background = cv2.add(background, noise) + + # 保存图像 + cv2.imwrite(file_path, background) + + return casualties + +# 创建基本测试图像 +create_battlefield_scene( + width=800, + height=600, + file_path='test_images/battlefield_scene.jpg', + for_training=False +) +print("测试图像已创建: test_images/battlefield_scene.jpg") + +# 创建用于训练的图像和标注 +training_count = 10 # 生成10张训练用图片 +for i in range(training_count): + # 生成图片文件名 + img_filename = f"battlefield_training_{i+1}.jpg" + img_path = f"src/ui/static/training_images/{img_filename}" + + # 生成图片和标注 + casualties = create_battlefield_scene( + width=1024, + height=768, + file_path=img_path, + for_training=True + ) + + # 保存标注信息到JSON文件 + if casualties: + import json + + # 确保标注目录存在 + os.makedirs('src/ui/static/annotations', exist_ok=True) + + # 创建标注文件名和图像ID + image_id = f"img_{int(i+1)}" + annotation_path = f"src/ui/static/annotations/{image_id}.json" + + # 保存标注 + with open(annotation_path, 'w', encoding='utf-8') as f: + json.dump(casualties, f, ensure_ascii=False, indent=2) + +print(f"已生成 {training_count} 张训练图片和标注,位于 src/ui/static/training_images/ 和 src/ui/static/annotations/") \ No newline at end of file diff --git a/src/software/init_project.sh b/src/software/init_project.sh new file mode 100644 index 00000000..5d06cf1e --- /dev/null +++ b/src/software/init_project.sh @@ -0,0 +1,89 @@ +#!/bin/bash + +# 确保目录结构存在 +mkdir -p src/drone/ +mkdir -p src/communication/ +mkdir -p src/positioning/ +mkdir -p src/ui/templates/ +mkdir -p src/ui/static/css/ +mkdir -p src/ui/static/js/ +mkdir -p src/ui/static/img/ +mkdir -p config/ +mkdir -p tests/ + +# 创建空的__init__.py文件确保模块可以被导入 +touch src/__init__.py +touch src/drone/__init__.py +touch src/communication/__init__.py +touch src/positioning/__init__.py +touch src/ui/__init__.py +touch config/__init__.py +touch tests/__init__.py + +# 创建无人机和伤员图标 +echo "创建示例图标..." +# 无人机图标 +cat > src/ui/static/img/drone.png << EOF +iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAA +AXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAA +DsMAAA7DAcdvqGQAAAKDSURBVFhHxZdPSBRhGMZnZ/9o/9xd +d1ddT0GHUiGDoEMQhEGHOhR0COoQdKhLHToEXYIuQYc8BR3q +VNChQ9Ch6BAEHaJDRIeIVNBW27Vd/7XrzrwzO9O8M9/MfLM7 +s+sXPPDyvO/7vd+8O7PrjrFGnMP+vgZ0dnZmzs/Pb0aj0QBG +e97QJyqRSOB8Po8WFxcPAZcYehD0H9GRy+WyiAcpM4BvuKn/ +qCiKKkVRphgacCrYcRFPOHWQxpPJ5HWn/lZQFYkHQYWC0mJU +wkk8CAqgKvHyIMchCDGqkuPQoEQQJMehAWLUiSAYZJbfb5TN +Zvc45zrQCfN+NiC0iG4RoQUQIYi7xMuDHIcgxKhKjkODEkGQ +HIcGiFF1iECQWbqDZmZmWs+0w2P9osBJtBvQCQdxXRA4wE5g +NZvNcpzjnMsTlHQA12q1E4CfAF47OTnp5TgvJCh6jO5Fh4eH +Wcj7gPfAHx1APB7/heMxdH9lZeUaJz00CrgYCCGb8/PzF7PZ +7KKqqsfGzMuHVAECyePj428qlconY+blQwbQBXg3nU4/xgXk +G6/CILtgjGgX8AGOBYyJ14YxoxeZqrPwkN2CfUQDbI2yGRGx +lI+FdAuwM1qtVhPbIxmPx28ZMxdEAXbGW1tbK81m81MqlRox +Zi6ILoB3wpOTk0cAj8Hr7e3t58bMBa/nQAdxPB/GA/nU1dV1 +wZhZIP8GaJBMJge2t7efxWKxH9PT0/nBwcFOjnPCi4EQYYLZ +WVb8FZIpE0D89MTERO/Ozs55o+TlQ7YLsJPvPEbrOA7X6/XL +uCZrxk4P2QDojPgf0wlgEvB+aWlpbWho6J/REqA6Rv8B0U+G +YRp/6oQAAAAASUVORK5CYII= +EOF + +# 伤员图标 +cat > src/ui/static/img/casualty.png << EOF +iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAA +AXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAA +DsMAAA7DAcdvqGQAAAKDSURBVFhHxZdPSBRhGMZnZ/9o/9xd +d1ddT0GHUiGDoEMQhEGHOhR0COoQdKhLHToEXYIuQYc8BR3q +VNChQ9Ch6BAEHaJDRIeIVNBW27Vd/7XrzrwzO9O8M9/MfLM7 +s+sXPPDyvO/7vd+8O7PrjrFGnMP+vgZ0dnZmzs/Pb0aj0QBG +e97QJyqRSOB8Po8WFxcPAZcYehD0H9GRy+WyiAcpM4BvuKn/ +qCiKKkVRphgacCrYcRFPOHWQxpPJ5HWn/lZQFYkHQYWC0mJU +wkk8CAqgKvHyIMchCDGqkuPQoEQQJMehAWLUiSAYZJbfb5TN +Zvc45zrQCfN+NiC0iG4RoQUQIYi7xMuDHIcgxKhKjkODEkGQ +HIcGiFF1iECQWbqDZmZmWs+0w2P9osBJtBvQCQdxXRA4wE5g +NZvNcpzjnMsTlHQA12q1E4CfAF47OTnp5TgvJCh6jO5Fh4eH +Wcj7gPfAHx1APB7/heMxdH9lZeUaJz00CrgYCCGb8/PzF7PZ +7KKqqsfGzMuHVAECyePj428qlconY+blQwbQBXg3nU4/xgXk +G6/CILtgjGgX8AGOBYyJ14YxoxeZqrPwkN2CfUQDbI2yGRGx +lI+FdAuwM1qtVhPbIxmPx28ZMxdEAXbGW1tbK81m81MqlRox +Zi6ILoB3wpOTk0cAj8Hr7e3t58bMBa/nQAdxPB/GA/nU1dV1 +wZhZIP8GaJBMJge2t7efxWKxH9PT0/nBwcFOjnPCi4EQYYLZ +WVb8FZIpE0D89MTERO/Ozs55o+TlQ7YLsJPvPEbrOA7X6/XL +uCZrxk4P2QDojPgf0wlgEvB+aWlpbWho6J/REqA6Rv8B0U+G +YRp/6oQAAAAASUVORK5CYII= +EOF + +# 创建环境变量文件 +cp .env.example .env + +# 创建一个简单的运行脚本 +cat > run.sh << EOF +#!/bin/bash +python simulation.py "\$@" +EOF +chmod +x run.sh + +echo "项目初始化完成!" +echo "运行以下命令安装依赖:" +echo " pip install -r requirements.txt" +echo "运行系统:" +echo " ./run.sh" \ No newline at end of file diff --git a/src/software/main.py b/src/software/main.py new file mode 100644 index 00000000..f35a7bf1 --- /dev/null +++ b/src/software/main.py @@ -0,0 +1,350 @@ +import logging +import os +import signal +import sys +import threading +from config.config import current_config as config +from src.drone.drone_controller import DroneController +from src.drone.mission_planner import MissionPlanner, Mission, MissionType +from src.drone.medical_supplies_db import MedicalSupplyManagerDB +from src.communication.communication_manager import CommunicationManager +from src.positioning.position_manager import PositionManager +from src.ui.web_interface import WebInterface +from src.vision.casualty_detector import CasualtyDetector +from src.db import init_db + +# 设置日志 +config.setup_logging() +logger = logging.getLogger(__name__) + +class MedicalEvacuationSystem: + def __init__(self): + """初始化医疗后送系统""" + logger.info(f"初始化{config.APP_NAME} v{config.VERSION}") + + # 初始化各个模块 + self.drone_controller = DroneController(config.DRONE_CONNECTION_STRING) + + # 初始化Web界面 + self.web_interface = WebInterface( + host=config.WEB_HOST, + port=config.WEB_PORT + ) + + # 初始化数据库 + init_db(self.web_interface.app) + + # 初始化伤员检测器 + self.casualty_detector = CasualtyDetector( + model_path=config.CASUALTY_DETECTOR_MODEL_PATH, + detection_threshold=config.DETECTION_THRESHOLD + ) + + # 设置伤员检测器 + self.web_interface.set_casualty_detector(self.casualty_detector) + + self.position_manager = PositionManager() + self.communication_manager = CommunicationManager( + host=config.COMM_HOST, + port=config.COMM_PORT + ) + + # 初始化物资管理器(在Flask应用上下文中) + with self.web_interface.app.app_context(): + self.medical_supply_manager = MedicalSupplyManagerDB() + + self.mission_planner = None + + # 注册处理器 + self._register_handlers() + + def _register_handlers(self): + """注册各种处理器""" + # 注册Web界面处理器 + self.web_interface.set_casualty_handler(self._handle_casualty_request) + self.web_interface.set_supply_handler(self._handle_supply_request) + self.web_interface.set_drone_status_handler(self._handle_drone_status_request) + + # 注册通信管理器处理器 + self.communication_manager.set_casualty_handler(self._handle_casualty_request) + self.communication_manager.set_supply_handler(self._handle_supply_request) + + def _handle_casualty_request(self, data): + """处理伤员请求""" + try: + casualty_id = data.get('casualty_id') + latitude = data.get('latitude') + longitude = data.get('longitude') + + if casualty_id and latitude is not None and longitude is not None: + # 添加或更新伤员位置 + self.position_manager.add_casualty_position(casualty_id, latitude, longitude) + + # 广播伤员位置更新 + self.web_interface.broadcast_casualty_update(data) + + logger.info(f"处理伤员请求: ID={casualty_id}, 位置=({latitude}, {longitude})") + return True + else: + logger.warning("无效的伤员请求数据") + return False + except Exception as e: + logger.error(f"处理伤员请求失败: {str(e)}") + return False + + def _handle_supply_request(self, data): + """处理物资请求""" + try: + supply_id = data.get('type') + quantity = data.get('quantity', 1) + target_id = data.get('target') + + if not supply_id or not target_id: + logger.warning("无效的物资请求数据") + return False + + # 获取伤员位置 + casualty_position = self.position_manager.get_casualty_position(target_id) + if not casualty_position: + logger.warning(f"未找到伤员位置: {target_id}") + return False + + # 创建物资请求 + request_id = self.medical_supply_manager.create_supply_request( + target_id, supply_id, quantity + ) + + if not request_id: + logger.warning("创建物资请求失败") + return False + + # 创建物资运送任务 + supply_dict = self.medical_supply_manager.get_supply(supply_id) + + mission = Mission( + mission_id=f"mission_{request_id}", + mission_type=MissionType.MEDICAL_SUPPLY, + target_location=(casualty_position['latitude'], casualty_position['longitude']), + altitude=config.DEFAULT_ALTITUDE, + payload={ + 'request_id': request_id, + 'supply_id': supply_id, + 'supply_name': supply_dict.get('name') if supply_dict else "未知物资", + 'quantity': quantity, + 'target_id': target_id + } + ) + + # 添加任务到任务规划器 + self.mission_planner.add_mission(mission) + + # 更新物资请求状态 + self.medical_supply_manager.update_request_status(request_id, 'in_progress') + + # 广播物资状态更新 + self._broadcast_supply_status() + + logger.info(f"处理物资请求: ID={request_id}, 物资={supply_id}, 目标={target_id}") + return True + except Exception as e: + logger.error(f"处理物资请求失败: {str(e)}") + return False + + def _handle_drone_status_request(self): + """处理无人机状态请求""" + try: + # 获取当前任务 + current_mission = self.mission_planner.get_current_mission() if self.mission_planner else None + mission_status = "执行任务" if current_mission else "待机" + + # 获取无人机位置 + if self.drone_controller.vehicle: + location = self.drone_controller.vehicle.location.global_relative_frame + battery = self.drone_controller.vehicle.battery.level if self.drone_controller.vehicle.battery else 0 + else: + location = None + battery = 0 + + # 构建状态数据 + if location: + status_data = { + 'status': mission_status, + 'battery': battery, + 'latitude': location.lat, + 'longitude': location.lon, + 'altitude': location.alt if hasattr(location, 'alt') else 0, + 'current_mission': current_mission.to_dict() if current_mission else None + } + else: + # 模拟数据 + status_data = { + 'status': '未连接', + 'battery': 85, # 模拟电量 + 'latitude': config.BASE_LATITUDE, + 'longitude': config.BASE_LONGITUDE, + 'altitude': 50, # 模拟高度 + 'current_mission': None + } + + # 广播无人机状态 + self.web_interface.broadcast_drone_update(status_data) + + return status_data + except Exception as e: + logger.error(f"处理无人机状态请求失败: {str(e)}") + return { + 'status': '错误', + 'battery': 0, + 'latitude': config.BASE_LATITUDE, + 'longitude': config.BASE_LONGITUDE, + 'altitude': 0, + 'error': str(e) + } + + def _broadcast_supply_status(self): + """广播物资状态""" + try: + # 在应用上下文中执行 + with self.web_interface.app.app_context(): + supplies = [] + all_supplies = self.medical_supply_manager.get_all_supplies() + + for supply_id, supply_dict in all_supplies.items(): + supplies.append({ + 'id': supply_id, + 'name': supply_dict.get('name', '未知物资'), + 'description': supply_dict.get('description', ''), + 'quantity': supply_dict.get('quantity', 0) + }) + + self.web_interface.broadcast_medical_supply_update({ + 'supplies': supplies + }) + except Exception as e: + logger.error(f"广播物资状态失败: {str(e)}") + + def _broadcast_mission_status(self): + """广播任务状态""" + try: + if not self.mission_planner: + return + + current_mission = self.mission_planner.get_current_mission() + mission_history = self.mission_planner.get_mission_history() + + self.web_interface.socketio.emit('mission_status_updated', { + 'currentMission': current_mission.to_dict() if current_mission else None, + 'missionHistory': mission_history + }) + except Exception as e: + logger.error(f"广播任务状态失败: {str(e)}") + + def start(self): + """启动系统""" + try: + logger.info("启动系统...") + + # 连接无人机 + if not config.ENABLE_SIMULATION: + if not self.drone_controller.connect(): + logger.error("无法连接到无人机,请检查连接设置") + # 继续运行,使用模拟模式 + logger.info("系统将在模拟模式下运行") + else: + logger.info("系统运行在模拟模式,跳过无人机连接") + + # 初始化任务规划器 + self.mission_planner = MissionPlanner(self.drone_controller) + self.mission_planner.start() + + # 启动通信服务器 + communication_thread = threading.Thread( + target=self.communication_manager.start_server, + daemon=True + ) + communication_thread.start() + + # 启动任务状态广播定时器 + def broadcast_status(): + while True: + # 在应用上下文中执行 + with self.web_interface.app.app_context(): + self._handle_drone_status_request() + self._broadcast_mission_status() + # 物资状态广播已经有自己的上下文管理 + self._broadcast_supply_status() + threading.Event().wait(5) # 每5秒广播一次 + + status_thread = threading.Thread(target=broadcast_status, daemon=True) + status_thread.start() + + # 启动Web界面 + self.web_interface.start() + + return True + except Exception as e: + logger.error(f"启动系统失败: {str(e)}") + return False + + def stop(self): + """停止系统""" + try: + logger.info("停止系统...") + + # 停止任务规划器 + if self.mission_planner: + self.mission_planner.stop() + + # 断开无人机连接 + if self.drone_controller: + self.drone_controller.close() + + # 关闭通信服务器 + if self.communication_manager: + self.communication_manager.close() + + logger.info("系统已停止") + return True + except Exception as e: + logger.error(f"停止系统失败: {str(e)}") + return False + + +def signal_handler(sig, frame): + """信号处理函数""" + print("正在停止系统...") + system.stop() + sys.exit(0) + + +def main(): + """主函数""" + global system + + try: + # 创建系统实例 + system = MedicalEvacuationSystem() + + # 注册信号处理器 + signal.signal(signal.SIGINT, signal_handler) + + # 启动系统 + if system.start(): + logger.info("系统启动成功") + else: + logger.error("系统启动失败") + return 1 + + return 0 + except Exception as e: + logger.critical(f"系统运行出错: {str(e)}") + return 1 + + +if __name__ == "__main__": + # 创建全局系统实例 + system = None + + # 运行主函数 + exit_code = main() + sys.exit(exit_code) \ No newline at end of file diff --git a/src/software/requirements.txt b/src/software/requirements.txt new file mode 100644 index 00000000..3a2a298d --- /dev/null +++ b/src/software/requirements.txt @@ -0,0 +1,9 @@ +pymavlink==2.4.35 +dronekit==2.9.2 +pyserial==3.5 +flask==2.3.3 +flask-socketio==5.3.6 +python-dotenv==1.0.0 +geopy==2.4.1 +numpy==1.24.3 +pandas==2.0.3 \ No newline at end of file diff --git a/src/software/simulation.py b/src/software/simulation.py new file mode 100644 index 00000000..d6caf332 --- /dev/null +++ b/src/software/simulation.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +import os +import sys +import time +import logging +import random +import threading +import signal +import argparse +from config.config import current_config as config + +# 确保环境变量设置为启用模拟 +os.environ['ENABLE_SIMULATION'] = 'True' + +# 设置日志 +config.setup_logging() +logger = logging.getLogger(__name__) + + +class DroneSimulator: + """无人机模拟器""" + + def __init__(self, port=14550): + """初始化模拟器""" + self.port = port + self.running = False + self.thread = None + self.latitude = config.BASE_LATITUDE + self.longitude = config.BASE_LONGITUDE + self.altitude = 0.0 + self.battery = 100.0 + self.armed = False + self.mode = "GUIDED" + + def start(self): + """启动模拟器""" + if self.running: + return + + self.running = True + self.thread = threading.Thread(target=self._simulation_loop, daemon=True) + self.thread.start() + logger.info(f"模拟器已启动在端口 {self.port}") + + def stop(self): + """停止模拟器""" + if not self.running: + return + + self.running = False + if self.thread: + self.thread.join(timeout=2) + logger.info("模拟器已停止") + + def _simulation_loop(self): + """模拟循环""" + while self.running: + # 模拟电池消耗 + if self.armed and self.battery > 0: + self.battery -= 0.01 + if self.battery < 0: + self.battery = 0 + + # 模拟位置变化 + if self.armed and self.mode == "GUIDED" and self.altitude > 0: + # 随机漂移 + self.latitude += random.uniform(-0.00001, 0.00001) + self.longitude += random.uniform(-0.00001, 0.00001) + + time.sleep(0.1) + + def arm(self): + """解锁无人机""" + self.armed = True + logger.info("模拟无人机已解锁") + + def disarm(self): + """锁定无人机""" + self.armed = False + logger.info("模拟无人机已锁定") + + def set_mode(self, mode): + """设置模式""" + self.mode = mode + logger.info(f"模拟无人机模式已设置为 {mode}") + + def takeoff(self, altitude): + """起飞""" + self.arm() + self.altitude = altitude + logger.info(f"模拟无人机起飞到高度 {altitude}m") + + def land(self): + """降落""" + self.altitude = 0 + self.disarm() + logger.info("模拟无人机已降落") + + def goto(self, lat, lon, alt): + """飞往指定位置""" + logger.info(f"模拟无人机飞往 ({lat}, {lon}, {alt})") + + # 模拟飞行时间 + distance = ((lat - self.latitude) ** 2 + (lon - self.longitude) ** 2) ** 0.5 * 111000 + speed = 5.0 # 假设速度为5m/s + flight_time = distance / speed + + # 模拟飞行过程 + start_lat = self.latitude + start_lon = self.longitude + start_alt = self.altitude + + for i in range(10): + progress = (i + 1) / 10 + self.latitude = start_lat + (lat - start_lat) * progress + self.longitude = start_lon + (lon - start_lon) * progress + self.altitude = start_alt + (alt - start_alt) * progress + time.sleep(flight_time / 10) + + self.latitude = lat + self.longitude = lon + self.altitude = alt + logger.info(f"模拟无人机已到达 ({lat}, {lon}, {alt})") + + +def setup_simulation(): + """设置模拟环境""" + # 启动模拟器 + simulator = DroneSimulator() + simulator.start() + + # 创建SITL连接字符串和覆盖环境变量 + os.environ['DRONE_CONNECTION_STRING'] = f'udpin:localhost:{simulator.port}' + + return simulator + + +def run_simulation(): + """运行模拟""" + # 设置参数解析 + parser = argparse.ArgumentParser(description='智能战场医疗后送系统模拟器') + parser.add_argument('--web-port', type=int, default=8080, help='Web服务端口') + parser.add_argument('--comm-port', type=int, default=5000, help='通信服务端口') + parser.add_argument('--drone-port', type=int, default=14550, help='无人机模拟器端口') + args = parser.parse_args() + + # 设置环境变量 + os.environ['WEB_PORT'] = str(args.web_port) + os.environ['COMM_PORT'] = str(args.comm_port) + + # 启动模拟器 + simulator = DroneSimulator(port=args.drone_port) + simulator.start() + + # 导入主程序模块 + from main import MedicalEvacuationSystem + + # 创建系统实例 + system = MedicalEvacuationSystem() + + # 注册信号处理 + def signal_handler(sig, frame): + logger.info("接收到停止信号,正在关闭模拟器...") + system.stop() + simulator.stop() + sys.exit(0) + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + # 启动系统 + try: + if system.start(): + logger.info("系统启动成功,运行在模拟模式") + + # 保持程序运行 + while True: + time.sleep(1) + else: + logger.error("系统启动失败") + simulator.stop() + return 1 + except Exception as e: + logger.error(f"模拟运行错误: {str(e)}") + system.stop() + simulator.stop() + return 1 + + return 0 + + +if __name__ == "__main__": + exit_code = run_simulation() + sys.exit(exit_code) \ No newline at end of file diff --git a/src/software/src/communication/__pycache__/communication_manager.cpython-39.pyc b/src/software/src/communication/__pycache__/communication_manager.cpython-39.pyc new file mode 100644 index 00000000..0d868fce Binary files /dev/null and b/src/software/src/communication/__pycache__/communication_manager.cpython-39.pyc differ diff --git a/src/software/src/communication/communication_manager.py b/src/software/src/communication/communication_manager.py new file mode 100644 index 00000000..f3b464bd --- /dev/null +++ b/src/software/src/communication/communication_manager.py @@ -0,0 +1,140 @@ +import socket +import json +import logging +from threading import Thread, Lock + +class CommunicationManager: + def __init__(self, host='0.0.0.0', port=5000): + """ + 初始化通信管理器 + :param host: 服务器主机地址 + :param port: 服务器端口 + """ + self.host = host + self.port = port + self.server_socket = None + self.clients = [] + self.lock = Lock() + self.logger = logging.getLogger(__name__) + self.casualty_handler = None + self.supply_handler = None + + def set_casualty_handler(self, handler): + """ + 设置伤员处理回调函数 + :param handler: 处理函数 + """ + self.casualty_handler = handler + self.logger.info("已设置伤员处理回调函数") + + def set_supply_handler(self, handler): + """ + 设置物资处理回调函数 + :param handler: 处理函数 + """ + self.supply_handler = handler + self.logger.info("已设置物资处理回调函数") + + def start_server(self): + """启动通信服务器""" + try: + self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.server_socket.bind((self.host, self.port)) + self.server_socket.listen(5) + self.logger.info(f"通信服务器启动在 {self.host}:{self.port}") + + # 启动客户端接受线程 + Thread(target=self._accept_clients, daemon=True).start() + + except Exception as e: + self.logger.error(f"启动服务器失败: {str(e)}") + + def _accept_clients(self): + """接受新的客户端连接""" + while True: + try: + client_socket, address = self.server_socket.accept() + self.logger.info(f"新客户端连接: {address}") + + with self.lock: + self.clients.append(client_socket) + + # 为每个客户端启动一个处理线程 + Thread(target=self._handle_client, args=(client_socket,), daemon=True).start() + + except Exception as e: + self.logger.error(f"接受客户端连接失败: {str(e)}") + + def _handle_client(self, client_socket): + """处理客户端消息""" + while True: + try: + data = client_socket.recv(1024).decode('utf-8') + if not data: + break + + message = json.loads(data) + self._process_message(message) + + except Exception as e: + self.logger.error(f"处理客户端消息失败: {str(e)}") + break + + # 客户端断开连接 + with self.lock: + if client_socket in self.clients: + self.clients.remove(client_socket) + client_socket.close() + + def _process_message(self, message): + """ + 处理接收到的消息 + :param message: 消息内容 + """ + message_type = message.get('type') + if message_type == 'casualty_location': + self._handle_casualty_location(message) + elif message_type == 'medical_supply_request': + self._handle_medical_supply_request(message) + elif message_type == 'drone_status': + self._handle_drone_status(message) + + def _handle_casualty_location(self, message): + """处理伤员位置信息""" + location = message.get('location') + self.logger.info(f"收到伤员位置: {location}") + if self.casualty_handler: + self.casualty_handler(message) + + def _handle_medical_supply_request(self, message): + """处理医疗物资请求""" + supplies = message.get('supplies') + location = message.get('location') + self.logger.info(f"收到医疗物资请求: {supplies} 送往 {location}") + if self.supply_handler: + self.supply_handler(message) + + def _handle_drone_status(self, message): + """处理无人机状态信息""" + status = message.get('status') + self.logger.info(f"收到无人机状态: {status}") + # TODO: 实现无人机状态处理逻辑 + + def broadcast_message(self, message): + """ + 广播消息给所有客户端 + :param message: 要广播的消息 + """ + message_str = json.dumps(message) + with self.lock: + for client in self.clients: + try: + client.send(message_str.encode('utf-8')) + except Exception as e: + self.logger.error(f"发送消息失败: {str(e)}") + + def close(self): + """关闭通信服务器""" + if self.server_socket: + self.server_socket.close() + self.logger.info("通信服务器已关闭") \ No newline at end of file diff --git a/src/software/src/db/__init__.py b/src/software/src/db/__init__.py new file mode 100644 index 00000000..4232474d --- /dev/null +++ b/src/software/src/db/__init__.py @@ -0,0 +1,19 @@ +from flask import Flask +from config.database import db_config +from src.db.models import db + +def init_db(app: Flask): + """初始化数据库""" + # 配置SQLAlchemy + app.config['SQLALCHEMY_DATABASE_URI'] = db_config.SQLALCHEMY_DATABASE_URI + app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = db_config.SQLALCHEMY_TRACK_MODIFICATIONS + app.config['SQLALCHEMY_ECHO'] = db_config.SQLALCHEMY_ECHO + + # 初始化SQLAlchemy + db.init_app(app) + + # 创建所有表(如果不存在) + with app.app_context(): + db.create_all() + + return db \ No newline at end of file diff --git a/src/software/src/db/__pycache__/__init__.cpython-39.pyc b/src/software/src/db/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 00000000..33c1730a Binary files /dev/null and b/src/software/src/db/__pycache__/__init__.cpython-39.pyc differ diff --git a/src/software/src/db/__pycache__/models.cpython-39.pyc b/src/software/src/db/__pycache__/models.cpython-39.pyc new file mode 100644 index 00000000..2002bf9c Binary files /dev/null and b/src/software/src/db/__pycache__/models.cpython-39.pyc differ diff --git a/src/software/src/db/models.py b/src/software/src/db/models.py new file mode 100644 index 00000000..d59ad894 --- /dev/null +++ b/src/software/src/db/models.py @@ -0,0 +1,65 @@ +from flask_sqlalchemy import SQLAlchemy +from datetime import datetime + +# 创建SQLAlchemy实例 +db = SQLAlchemy() + +class MedicalSupply(db.Model): + """医疗物资数据模型""" + __tablename__ = 'medical_supplies' + + id = db.Column(db.String(50), primary_key=True) + name = db.Column(db.String(100), nullable=False) + description = db.Column(db.Text, nullable=True) + weight = db.Column(db.Float, default=0) # 重量(克) + quantity = db.Column(db.Integer, default=0) # 数量 + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + def __repr__(self): + return f"" + + def to_dict(self): + """转换为字典""" + return { + 'id': self.id, + 'name': self.name, + 'description': self.description, + 'weight': self.weight, + 'quantity': self.quantity, + 'created_at': self.created_at.isoformat() if self.created_at else None, + 'updated_at': self.updated_at.isoformat() if self.updated_at else None + } + + +class SupplyRequest(db.Model): + """物资请求数据模型""" + __tablename__ = 'supply_requests' + + id = db.Column(db.String(50), primary_key=True) + target_id = db.Column(db.String(50), nullable=False) # 伤员ID + supply_id = db.Column(db.String(50), db.ForeignKey('medical_supplies.id'), nullable=False) + quantity = db.Column(db.Integer, default=0) # 请求数量 + status = db.Column(db.String(20), default='pending') # 状态:pending, in_progress, delivered, failed + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + # 关联关系 + supply = db.relationship('MedicalSupply', backref=db.backref('requests', lazy=True)) + + def __repr__(self): + return f"" + + def to_dict(self): + """转换为字典""" + return { + 'request_id': self.id, + 'target_id': self.target_id, + 'supply_id': self.supply_id, + 'supply_name': self.supply.name if self.supply else None, + 'quantity': self.quantity, + 'status': self.status, + 'timestamp': self.created_at.timestamp() if self.created_at else None, + 'created_at': self.created_at.isoformat() if self.created_at else None, + 'updated_at': self.updated_at.isoformat() if self.updated_at else None + } \ No newline at end of file diff --git a/src/software/src/drone/__pycache__/drone_controller.cpython-39.pyc b/src/software/src/drone/__pycache__/drone_controller.cpython-39.pyc new file mode 100644 index 00000000..89e50aae Binary files /dev/null and b/src/software/src/drone/__pycache__/drone_controller.cpython-39.pyc differ diff --git a/src/software/src/drone/__pycache__/medical_supplies.cpython-39.pyc b/src/software/src/drone/__pycache__/medical_supplies.cpython-39.pyc new file mode 100644 index 00000000..87160ef6 Binary files /dev/null and b/src/software/src/drone/__pycache__/medical_supplies.cpython-39.pyc differ diff --git a/src/software/src/drone/__pycache__/medical_supplies_db.cpython-39.pyc b/src/software/src/drone/__pycache__/medical_supplies_db.cpython-39.pyc new file mode 100644 index 00000000..a0fb012b Binary files /dev/null and b/src/software/src/drone/__pycache__/medical_supplies_db.cpython-39.pyc differ diff --git a/src/software/src/drone/__pycache__/mission_planner.cpython-39.pyc b/src/software/src/drone/__pycache__/mission_planner.cpython-39.pyc new file mode 100644 index 00000000..22617fb4 Binary files /dev/null and b/src/software/src/drone/__pycache__/mission_planner.cpython-39.pyc differ diff --git a/src/software/src/drone/drone_controller.py b/src/software/src/drone/drone_controller.py new file mode 100644 index 00000000..e7a9ad1a --- /dev/null +++ b/src/software/src/drone/drone_controller.py @@ -0,0 +1,103 @@ +from dronekit import connect, VehicleMode, LocationGlobalRelative +import time +import logging +import math + +class DroneController: + def __init__(self, connection_string): + """ + 初始化无人机控制器 + :param connection_string: 无人机连接字符串(如:'udpin:localhost:14550') + """ + self.vehicle = None + self.connection_string = connection_string + self.logger = logging.getLogger(__name__) + + def connect(self): + """连接到无人机""" + try: + self.vehicle = connect(self.connection_string, wait_ready=True) + self.logger.info("成功连接到无人机") + return True + except Exception as e: + self.logger.error(f"连接无人机失败: {str(e)}") + return False + + def arm_and_takeoff(self, target_altitude): + """ + 起飞到指定高度 + :param target_altitude: 目标高度(米) + """ + self.logger.info("准备起飞...") + + # 等待无人机准备就绪 + while not self.vehicle.is_armable: + self.logger.info("等待无人机准备就绪...") + time.sleep(1) + + # 切换到GUIDED模式 + self.vehicle.mode = VehicleMode("GUIDED") + + # 解锁无人机 + self.vehicle.armed = True + + # 等待无人机解锁 + while not self.vehicle.armed: + self.logger.info("等待无人机解锁...") + time.sleep(1) + + # 起飞 + self.logger.info(f"起飞到高度: {target_altitude}米") + self.vehicle.simple_takeoff(target_altitude) + + # 等待到达目标高度 + while True: + current_altitude = self.vehicle.location.global_relative_frame.alt + if current_altitude >= target_altitude * 0.95: + self.logger.info("到达目标高度") + break + time.sleep(1) + + def goto_location(self, lat, lon, altitude): + """ + 飞往指定位置 + :param lat: 纬度 + :param lon: 经度 + :param altitude: 高度(米) + """ + target_location = LocationGlobalRelative(lat, lon, altitude) + self.vehicle.simple_goto(target_location) + + # 等待到达目标位置 + while True: + current_location = self.vehicle.location.global_relative_frame + distance = self._get_distance_metres(current_location, target_location) + if distance < 1.0: # 距离小于1米认为到达 + self.logger.info("到达目标位置") + break + time.sleep(1) + + def land(self): + """降落""" + self.logger.info("开始降落...") + self.vehicle.mode = VehicleMode("LAND") + + # 等待降落完成 + while self.vehicle.armed: + time.sleep(1) + + self.logger.info("降落完成") + + def close(self): + """关闭连接""" + if self.vehicle: + self.vehicle.close() + self.logger.info("关闭无人机连接") + + def _get_distance_metres(self, aLocation1, aLocation2): + """ + 计算两点之间的距离(米) + """ + dlat = aLocation2.lat - aLocation1.lat + dlong = aLocation2.lon - aLocation1.lon + return math.sqrt((dlat*dlat) + (dlong*dlong)) * 1.113195e5 \ No newline at end of file diff --git a/src/software/src/drone/medical_supplies.py b/src/software/src/drone/medical_supplies.py new file mode 100644 index 00000000..1fee4b83 --- /dev/null +++ b/src/software/src/drone/medical_supplies.py @@ -0,0 +1,190 @@ +import logging +import time +from dataclasses import dataclass +from typing import Dict, List, Optional + +@dataclass +class MedicalSupply: + """医疗物资数据类""" + id: str + name: str + description: str + weight: float # 重量(克) + quantity: int # 数量 + + +class MedicalSupplyManager: + """医疗物资管理器""" + + def __init__(self): + """初始化医疗物资管理器""" + self.logger = logging.getLogger(__name__) + self.supplies = {} # 存储可用的医疗物资 + self.supply_requests = {} # 存储物资请求记录 + + # 初始化默认物资 + self._initialize_default_supplies() + + def _initialize_default_supplies(self): + """初始化默认物资""" + default_supplies = [ + MedicalSupply( + id="first_aid_kit", + name="急救包", + description="基础急救包,包含绷带、消毒用品等", + weight=500, + quantity=10 + ), + MedicalSupply( + id="medicine_pack", + name="药品包", + description="包含止痛药、抗生素等常用药物", + weight=300, + quantity=20 + ), + MedicalSupply( + id="blood_plasma", + name="血浆", + description="应急血浆,通用型", + weight=450, + quantity=5 + ), + MedicalSupply( + id="surgical_kit", + name="手术包", + description="简易手术工具包", + weight=800, + quantity=3 + ), + ] + + for supply in default_supplies: + self.supplies[supply.id] = supply + + def get_all_supplies(self) -> Dict[str, MedicalSupply]: + """ + 获取所有可用的物资 + :return: 物资字典 + """ + return self.supplies.copy() + + def get_supply(self, supply_id: str) -> Optional[MedicalSupply]: + """ + 获取指定ID的物资 + :param supply_id: 物资ID + :return: 物资对象或None + """ + return self.supplies.get(supply_id) + + def add_supply(self, supply: MedicalSupply) -> bool: + """ + 添加新的物资 + :param supply: 物资对象 + :return: 是否添加成功 + """ + if supply.id in self.supplies: + self.logger.warning(f"物资ID已存在: {supply.id}") + return False + + self.supplies[supply.id] = supply + self.logger.info(f"添加新物资: {supply.name}") + return True + + def update_supply_quantity(self, supply_id: str, quantity: int) -> bool: + """ + 更新物资数量 + :param supply_id: 物资ID + :param quantity: 新数量 + :return: 是否更新成功 + """ + if supply_id not in self.supplies: + self.logger.warning(f"物资ID不存在: {supply_id}") + return False + + self.supplies[supply_id].quantity = quantity + self.logger.info(f"更新物资数量: {supply_id}, 数量={quantity}") + return True + + def create_supply_request(self, + target_id: str, + supply_id: str, + quantity: int) -> Optional[str]: + """ + 创建物资请求 + :param target_id: 目标ID(伤员ID) + :param supply_id: 物资ID + :param quantity: 请求数量 + :return: 请求ID或None + """ + if supply_id not in self.supplies: + self.logger.warning(f"物资ID不存在: {supply_id}") + return None + + if self.supplies[supply_id].quantity < quantity: + self.logger.warning(f"物资不足: 请求={quantity}, 可用={self.supplies[supply_id].quantity}") + return None + + # 创建请求ID + request_id = f"req_{int(time.time())}_{supply_id}" + + # 更新可用数量 + self.supplies[supply_id].quantity -= quantity + + # 存储请求 + self.supply_requests[request_id] = { + 'request_id': request_id, + 'target_id': target_id, + 'supply_id': supply_id, + 'quantity': quantity, + 'status': 'pending', + 'timestamp': time.time() + } + + self.logger.info(f"创建物资请求: ID={request_id}, 目标={target_id}, 物资={supply_id}, 数量={quantity}") + return request_id + + def update_request_status(self, request_id: str, status: str) -> bool: + """ + 更新请求状态 + :param request_id: 请求ID + :param status: 新状态(pending, in_progress, delivered, failed) + :return: 是否更新成功 + """ + if request_id not in self.supply_requests: + self.logger.warning(f"请求ID不存在: {request_id}") + return False + + self.supply_requests[request_id]['status'] = status + self.logger.info(f"更新请求状态: ID={request_id}, 状态={status}") + + # 如果请求失败,恢复物资数量 + if status == 'failed': + supply_id = self.supply_requests[request_id]['supply_id'] + quantity = self.supply_requests[request_id]['quantity'] + self.supplies[supply_id].quantity += quantity + self.logger.info(f"恢复物资数量: ID={supply_id}, 增加={quantity}") + + return True + + def get_request(self, request_id: str) -> Optional[Dict]: + """ + 获取请求信息 + :param request_id: 请求ID + :return: 请求信息字典或None + """ + return self.supply_requests.get(request_id) + + def get_requests_by_target(self, target_id: str) -> List[Dict]: + """ + 获取指定目标的所有请求 + :param target_id: 目标ID + :return: 请求信息列表 + """ + return [req for req in self.supply_requests.values() if req['target_id'] == target_id] + + def get_all_requests(self) -> Dict[str, Dict]: + """ + 获取所有请求 + :return: 请求字典 + """ + return self.supply_requests.copy() \ No newline at end of file diff --git a/src/software/src/drone/medical_supplies_db.py b/src/software/src/drone/medical_supplies_db.py new file mode 100644 index 00000000..21cf6e42 --- /dev/null +++ b/src/software/src/drone/medical_supplies_db.py @@ -0,0 +1,260 @@ +import logging +import time +from typing import Dict, List, Optional +from src.db.models import db, MedicalSupply as MedicalSupplyModel, SupplyRequest as SupplyRequestModel + + +class MedicalSupplyManagerDB: + """基于数据库的医疗物资管理器""" + + def __init__(self): + """初始化医疗物资管理器""" + self.logger = logging.getLogger(__name__) + + # 检查是否需要初始化默认物资 + self._check_and_initialize_default_supplies() + + def _check_and_initialize_default_supplies(self): + """检查并初始化默认物资""" + try: + # 检查是否有物资记录 + supplies_count = MedicalSupplyModel.query.count() + + if supplies_count == 0: + self.logger.info("数据库中无物资记录,初始化默认物资...") + default_supplies = [ + { + "id": "first_aid_kit", + "name": "急救包", + "description": "基础急救包,包含绷带、消毒用品等", + "weight": 500, + "quantity": 10 + }, + { + "id": "medicine_pack", + "name": "药品包", + "description": "包含止痛药、抗生素等常用药物", + "weight": 300, + "quantity": 20 + }, + { + "id": "blood_plasma", + "name": "血浆", + "description": "应急血浆,通用型", + "weight": 450, + "quantity": 5 + }, + { + "id": "surgical_kit", + "name": "手术包", + "description": "简易手术工具包", + "weight": 800, + "quantity": 3 + }, + ] + + for supply_data in default_supplies: + supply = MedicalSupplyModel(**supply_data) + db.session.add(supply) + + try: + db.session.commit() + self.logger.info(f"已初始化 {len(default_supplies)} 种默认物资") + except Exception as e: + db.session.rollback() + self.logger.error(f"初始化默认物资失败: {str(e)}") + except RuntimeError as e: + self.logger.warning(f"初始化默认物资时缺少应用上下文: {str(e)}") + except Exception as e: + self.logger.error(f"检查并初始化默认物资失败: {str(e)}") + + def get_all_supplies(self) -> Dict[str, Dict]: + """ + 获取所有可用的物资 + :return: 物资字典 {id: supply_dict} + """ + try: + supplies = MedicalSupplyModel.query.all() + return {supply.id: supply.to_dict() for supply in supplies} + except Exception as e: + self.logger.error(f"获取所有物资失败: {str(e)}") + return {} + + def get_supply(self, supply_id: str) -> Optional[Dict]: + """ + 获取指定ID的物资 + :param supply_id: 物资ID + :return: 物资信息字典或None + """ + try: + supply = MedicalSupplyModel.query.get(supply_id) + return supply.to_dict() if supply else None + except Exception as e: + self.logger.error(f"获取物资失败: {str(e)}") + return None + + def add_supply(self, supply_data: Dict) -> bool: + """ + 添加新的物资 + :param supply_data: 物资信息字典 + :return: 是否添加成功 + """ + try: + # 检查物资ID是否已存在 + existing = MedicalSupplyModel.query.get(supply_data['id']) + if existing: + self.logger.warning(f"物资ID已存在: {supply_data['id']}") + return False + + # 创建新物资 + supply = MedicalSupplyModel(**supply_data) + db.session.add(supply) + db.session.commit() + + self.logger.info(f"添加新物资: {supply.name}") + return True + except Exception as e: + db.session.rollback() + self.logger.error(f"添加物资失败: {str(e)}") + return False + + def update_supply_quantity(self, supply_id: str, quantity: int) -> bool: + """ + 更新物资数量 + :param supply_id: 物资ID + :param quantity: 新数量 + :return: 是否更新成功 + """ + try: + supply = MedicalSupplyModel.query.get(supply_id) + if not supply: + self.logger.warning(f"物资ID不存在: {supply_id}") + return False + + supply.quantity = quantity + db.session.commit() + + self.logger.info(f"更新物资数量: {supply_id}, 数量={quantity}") + return True + except Exception as e: + db.session.rollback() + self.logger.error(f"更新物资数量失败: {str(e)}") + return False + + def create_supply_request(self, + target_id: str, + supply_id: str, + quantity: int) -> Optional[str]: + """ + 创建物资请求 + :param target_id: 目标ID(伤员ID) + :param supply_id: 物资ID + :param quantity: 请求数量 + :return: 请求ID或None + """ + try: + # 检查物资是否存在且数量足够 + supply = MedicalSupplyModel.query.get(supply_id) + + if not supply: + self.logger.warning(f"物资ID不存在: {supply_id}") + return None + + if supply.quantity < quantity: + self.logger.warning(f"物资不足: 请求={quantity}, 可用={supply.quantity}") + return None + + # 创建请求ID + request_id = f"req_{int(time.time())}_{supply_id}" + + # 更新可用数量 + supply.quantity -= quantity + + # 创建请求记录 + request = SupplyRequestModel( + id=request_id, + target_id=target_id, + supply_id=supply_id, + quantity=quantity, + status='pending' + ) + + db.session.add(request) + db.session.commit() + + self.logger.info(f"创建物资请求: ID={request_id}, 目标={target_id}, 物资={supply_id}, 数量={quantity}") + return request_id + except Exception as e: + db.session.rollback() + self.logger.error(f"创建物资请求失败: {str(e)}") + return None + + def update_request_status(self, request_id: str, status: str) -> bool: + """ + 更新请求状态 + :param request_id: 请求ID + :param status: 新状态(pending, in_progress, delivered, failed) + :return: 是否更新成功 + """ + try: + request = SupplyRequestModel.query.get(request_id) + + if not request: + self.logger.warning(f"请求ID不存在: {request_id}") + return False + + old_status = request.status + request.status = status + + # 如果请求失败,恢复物资数量 + if status == 'failed' and old_status != 'failed': + supply = MedicalSupplyModel.query.get(request.supply_id) + if supply: + supply.quantity += request.quantity + self.logger.info(f"恢复物资数量: ID={request.supply_id}, 增加={request.quantity}") + + db.session.commit() + self.logger.info(f"更新请求状态: ID={request_id}, 状态={status}") + return True + except Exception as e: + db.session.rollback() + self.logger.error(f"更新请求状态失败: {str(e)}") + return False + + def get_request(self, request_id: str) -> Optional[Dict]: + """ + 获取请求信息 + :param request_id: 请求ID + :return: 请求信息字典或None + """ + try: + request = SupplyRequestModel.query.get(request_id) + return request.to_dict() if request else None + except Exception as e: + self.logger.error(f"获取请求信息失败: {str(e)}") + return None + + def get_requests_by_target(self, target_id: str) -> List[Dict]: + """ + 获取指定目标的所有请求 + :param target_id: 目标ID + :return: 请求信息列表 + """ + try: + requests = SupplyRequestModel.query.filter_by(target_id=target_id).all() + return [request.to_dict() for request in requests] + except Exception as e: + self.logger.error(f"获取目标请求失败: {str(e)}") + return [] + + def get_all_requests(self) -> Dict[str, Dict]: + """ + 获取所有请求 + :return: 请求字典 {id: request_dict} + """ + try: + requests = SupplyRequestModel.query.all() + return {request.id: request.to_dict() for request in requests} + except Exception as e: + self.logger.error(f"获取所有请求失败: {str(e)}") + return {} \ No newline at end of file diff --git a/src/software/src/drone/mission_planner.py b/src/software/src/drone/mission_planner.py new file mode 100644 index 00000000..f71530f6 --- /dev/null +++ b/src/software/src/drone/mission_planner.py @@ -0,0 +1,271 @@ +import logging +import time +from enum import Enum +from typing import Dict, List, Optional, Tuple +from queue import Queue +from threading import Thread, Lock + +class MissionType(Enum): + """任务类型枚举""" + IDLE = 0 + MEDICAL_SUPPLY = 1 + SURVEILLANCE = 2 + RETURN_TO_BASE = 3 + +class MissionStatus(Enum): + """任务状态枚举""" + PENDING = 0 + IN_PROGRESS = 1 + COMPLETED = 2 + FAILED = 3 + +class Mission: + """任务类""" + def __init__(self, + mission_id: str, + mission_type: MissionType, + target_location: Tuple[float, float], + altitude: float = 10.0, + payload: Dict = None): + """ + 初始化任务 + :param mission_id: 任务ID + :param mission_type: 任务类型 + :param target_location: 目标位置(纬度, 经度) + :param altitude: 飞行高度(米) + :param payload: 任务负载(如物资信息) + """ + self.mission_id = mission_id + self.mission_type = mission_type + self.target_location = target_location + self.altitude = altitude + self.payload = payload or {} + self.status = MissionStatus.PENDING + self.start_time = None + self.end_time = None + self.error_message = None + + def to_dict(self) -> Dict: + """转换成字典""" + return { + 'mission_id': self.mission_id, + 'mission_type': self.mission_type.name, + 'target_location': self.target_location, + 'altitude': self.altitude, + 'payload': self.payload, + 'status': self.status.name, + 'start_time': self.start_time, + 'end_time': self.end_time, + 'error_message': self.error_message + } + + +class MissionPlanner: + """任务规划器""" + def __init__(self, drone_controller): + """ + 初始化任务规划器 + :param drone_controller: 无人机控制器 + """ + self.logger = logging.getLogger(__name__) + self.drone_controller = drone_controller + self.mission_queue = Queue() + self.current_mission = None + self.mission_history = [] + self.lock = Lock() + self.running = False + self.worker_thread = None + + def start(self): + """启动任务规划器""" + if self.running: + self.logger.warning("任务规划器已经在运行") + return + + self.running = True + self.worker_thread = Thread(target=self._mission_worker, daemon=True) + self.worker_thread.start() + self.logger.info("任务规划器已启动") + + def stop(self): + """停止任务规划器""" + if not self.running: + return + + self.running = False + if self.worker_thread: + self.worker_thread.join(timeout=5) + self.logger.info("任务规划器已停止") + + def add_mission(self, mission: Mission) -> bool: + """ + 添加任务到队列 + :param mission: 任务对象 + :return: 是否添加成功 + """ + try: + self.mission_queue.put(mission) + self.logger.info(f"添加任务: ID={mission.mission_id}, 类型={mission.mission_type.name}") + return True + except Exception as e: + self.logger.error(f"添加任务失败: {str(e)}") + return False + + def get_current_mission(self) -> Optional[Mission]: + """ + 获取当前执行的任务 + :return: 当前任务或None + """ + with self.lock: + return self.current_mission + + def get_mission_queue_size(self) -> int: + """ + 获取任务队列大小 + :return: 队列中的任务数 + """ + return self.mission_queue.qsize() + + def get_mission_history(self) -> List[Dict]: + """ + 获取任务历史 + :return: 任务历史记录列表 + """ + with self.lock: + return [mission.to_dict() for mission in self.mission_history] + + def _mission_worker(self): + """任务工作线程""" + while self.running: + try: + if not self.mission_queue.empty(): + # 从队列中获取下一个任务 + mission = self.mission_queue.get() + + with self.lock: + self.current_mission = mission + self.current_mission.status = MissionStatus.IN_PROGRESS + self.current_mission.start_time = time.time() + + self.logger.info(f"开始执行任务: ID={mission.mission_id}, 类型={mission.mission_type.name}") + + # 根据任务类型执行不同操作 + success = self._execute_mission(mission) + + with self.lock: + if success: + self.current_mission.status = MissionStatus.COMPLETED + self.logger.info(f"任务完成: ID={mission.mission_id}") + else: + self.current_mission.status = MissionStatus.FAILED + self.logger.error(f"任务失败: ID={mission.mission_id}") + + self.current_mission.end_time = time.time() + self.mission_history.append(self.current_mission) + self.current_mission = None + + self.mission_queue.task_done() + else: + # 无任务时等待 + time.sleep(1) + + except Exception as e: + self.logger.error(f"任务执行过程出错: {str(e)}") + if self.current_mission: + with self.lock: + self.current_mission.status = MissionStatus.FAILED + self.current_mission.error_message = str(e) + self.current_mission.end_time = time.time() + self.mission_history.append(self.current_mission) + self.current_mission = None + + def _execute_mission(self, mission: Mission) -> bool: + """ + 执行任务 + :param mission: 任务对象 + :return: 是否执行成功 + """ + try: + if mission.mission_type == MissionType.MEDICAL_SUPPLY: + return self._execute_medical_supply_mission(mission) + elif mission.mission_type == MissionType.SURVEILLANCE: + return self._execute_surveillance_mission(mission) + elif mission.mission_type == MissionType.RETURN_TO_BASE: + return self._execute_return_to_base_mission(mission) + else: + self.logger.warning(f"未知任务类型: {mission.mission_type}") + return False + except Exception as e: + self.logger.error(f"执行任务失败: {str(e)}") + mission.error_message = str(e) + return False + + def _execute_medical_supply_mission(self, mission: Mission) -> bool: + """ + 执行医疗物资运送任务 + :param mission: 任务对象 + :return: 是否执行成功 + """ + try: + # 飞往目标位置 + lat, lon = mission.target_location + self.drone_controller.goto_location(lat, lon, mission.altitude) + + # 模拟投放物资(实际应该有硬件操作) + self.logger.info(f"投放医疗物资: {mission.payload.get('supply_name', '未知物资')}") + time.sleep(2) # 模拟投放时间 + + # 返回基地 + return self._execute_return_to_base_mission(mission) + + except Exception as e: + self.logger.error(f"医疗物资任务执行失败: {str(e)}") + mission.error_message = str(e) + return False + + def _execute_surveillance_mission(self, mission: Mission) -> bool: + """ + 执行侦察任务 + :param mission: 任务对象 + :return: 是否执行成功 + """ + try: + # 飞往目标位置 + lat, lon = mission.target_location + self.drone_controller.goto_location(lat, lon, mission.altitude) + + # 模拟侦察(实际应该有摄像头操作) + self.logger.info(f"进行区域侦察: ({lat}, {lon})") + time.sleep(5) # 模拟侦察时间 + + # 返回基地 + return self._execute_return_to_base_mission(mission) + + except Exception as e: + self.logger.error(f"侦察任务执行失败: {str(e)}") + mission.error_message = str(e) + return False + + def _execute_return_to_base_mission(self, mission: Mission) -> bool: + """ + 执行返回基地任务 + :param mission: 任务对象 + :return: 是否执行成功 + """ + try: + # 获取基地位置(这里应该从配置中读取) + base_lat, base_lon = 39.9, 116.3 # 默认位置,实际应该从配置获取 + + # 飞往基地 + self.logger.info("返回基地") + self.drone_controller.goto_location(base_lat, base_lon, mission.altitude) + + # 降落 + self.drone_controller.land() + + return True + + except Exception as e: + self.logger.error(f"返回基地任务执行失败: {str(e)}") + mission.error_message = str(e) + return False \ No newline at end of file diff --git a/src/software/src/positioning/__pycache__/position_manager.cpython-39.pyc b/src/software/src/positioning/__pycache__/position_manager.cpython-39.pyc new file mode 100644 index 00000000..b1c840fc Binary files /dev/null and b/src/software/src/positioning/__pycache__/position_manager.cpython-39.pyc differ diff --git a/src/software/src/positioning/position_manager.py b/src/software/src/positioning/position_manager.py new file mode 100644 index 00000000..5a80b897 --- /dev/null +++ b/src/software/src/positioning/position_manager.py @@ -0,0 +1,131 @@ +import logging +from geopy.geocoders import Nominatim +from geopy.distance import geodesic +import time + +class PositionManager: + def __init__(self): + """初始化定位管理器""" + self.logger = logging.getLogger(__name__) + self.geocoder = Nominatim(user_agent="medical_evac_system") + self.casualty_positions = {} # 存储伤员位置信息 + + def get_location_from_coordinates(self, lat, lon): + """ + 根据经纬度获取位置描述 + :param lat: 纬度 + :param lon: 经度 + :return: 位置描述 + """ + try: + location = self.geocoder.reverse((lat, lon)) + return location.address + except Exception as e: + self.logger.error(f"获取位置描述失败: {str(e)}") + return None + + def calculate_distance(self, lat1, lon1, lat2, lon2): + """ + 计算两点之间的距离(米) + :param lat1: 起点纬度 + :param lon1: 起点经度 + :param lat2: 终点纬度 + :param lon2: 终点经度 + :return: 距离(米) + """ + try: + point1 = (lat1, lon1) + point2 = (lat2, lon2) + distance = geodesic(point1, point2).meters + return distance + except Exception as e: + self.logger.error(f"计算距离失败: {str(e)}") + return None + + def add_casualty_position(self, casualty_id, lat, lon, timestamp=None): + """ + 添加伤员位置信息 + :param casualty_id: 伤员ID + :param lat: 纬度 + :param lon: 经度 + :param timestamp: 时间戳 + """ + if timestamp is None: + timestamp = time.time() + + self.casualty_positions[casualty_id] = { + 'latitude': lat, + 'longitude': lon, + 'timestamp': timestamp, + 'location_description': self.get_location_from_coordinates(lat, lon) + } + + self.logger.info(f"添加伤员位置: ID={casualty_id}, 位置=({lat}, {lon})") + + def get_casualty_position(self, casualty_id): + """ + 获取伤员位置信息 + :param casualty_id: 伤员ID + :return: 位置信息字典 + """ + return self.casualty_positions.get(casualty_id) + + def update_casualty_position(self, casualty_id, lat, lon): + """ + 更新伤员位置信息 + :param casualty_id: 伤员ID + :param lat: 新的纬度 + :param lon: 新的经度 + """ + if casualty_id in self.casualty_positions: + self.add_casualty_position(casualty_id, lat, lon) + self.logger.info(f"更新伤员位置: ID={casualty_id}, 新位置=({lat}, {lon})") + else: + self.logger.warning(f"未找到伤员ID: {casualty_id}") + + def get_nearest_casualty(self, lat, lon, max_distance=None): + """ + 获取最近的伤员 + :param lat: 当前位置纬度 + :param lon: 当前位置经度 + :param max_distance: 最大距离(米) + :return: 最近的伤员信息 + """ + nearest = None + min_distance = float('inf') + + for casualty_id, position in self.casualty_positions.items(): + distance = self.calculate_distance( + lat, lon, + position['latitude'], + position['longitude'] + ) + + if distance is not None and distance < min_distance: + if max_distance is None or distance <= max_distance: + min_distance = distance + nearest = { + 'casualty_id': casualty_id, + 'distance': distance, + 'position': position + } + + return nearest + + def get_all_casualties(self): + """ + 获取所有伤员位置信息 + :return: 伤员位置信息字典 + """ + return self.casualty_positions.copy() + + def remove_casualty(self, casualty_id): + """ + 移除伤员位置信息 + :param casualty_id: 伤员ID + """ + if casualty_id in self.casualty_positions: + del self.casualty_positions[casualty_id] + self.logger.info(f"移除伤员位置: ID={casualty_id}") + else: + self.logger.warning(f"未找到伤员ID: {casualty_id}") \ No newline at end of file diff --git a/src/software/src/ui/__pycache__/web_interface.cpython-39.pyc b/src/software/src/ui/__pycache__/web_interface.cpython-39.pyc new file mode 100644 index 00000000..6d60615a Binary files /dev/null and b/src/software/src/ui/__pycache__/web_interface.cpython-39.pyc differ diff --git a/src/software/src/ui/static/annotations/img_1746535839_01.json b/src/software/src/ui/static/annotations/img_1746535839_01.json new file mode 100644 index 00000000..f89fe57f --- /dev/null +++ b/src/software/src/ui/static/annotations/img_1746535839_01.json @@ -0,0 +1,9 @@ +[ + { + "x": 192.75340195819638, + "y": 269.1060189546518, + "width": 442.41011984021304, + "height": 100.79002079002078, + "class": "casualty" + } +] \ No newline at end of file diff --git a/src/software/src/ui/static/annotations/img_1746536045_02.json b/src/software/src/ui/static/annotations/img_1746536045_02.json new file mode 100644 index 00000000..8ae0b897 --- /dev/null +++ b/src/software/src/ui/static/annotations/img_1746536045_02.json @@ -0,0 +1,9 @@ +[ + { + "x": 11.985121427837171, + "y": 117.62776025236593, + "width": 335.2926315789474, + "height": 184.41640378548897, + "class": "casualty" + } +] \ No newline at end of file diff --git a/src/software/src/ui/static/annotations/img_1746536045_03.json b/src/software/src/ui/static/annotations/img_1746536045_03.json new file mode 100644 index 00000000..a24dc9af --- /dev/null +++ b/src/software/src/ui/static/annotations/img_1746536045_03.json @@ -0,0 +1,9 @@ +[ + { + "x": 12.780651033233484, + "y": 53.81395348837209, + "width": 707.7720207253885, + "height": 412.9186046511628, + "class": "casualty" + } +] \ No newline at end of file diff --git a/src/software/src/ui/static/annotations/img_1746536045_04.json b/src/software/src/ui/static/annotations/img_1746536045_04.json new file mode 100644 index 00000000..6e4acb54 --- /dev/null +++ b/src/software/src/ui/static/annotations/img_1746536045_04.json @@ -0,0 +1,9 @@ +[ + { + "x": 112.26251631820757, + "y": 376.3773436462995, + "width": 446.6321243523316, + "height": 147.99805825242714, + "class": "casualty" + } +] \ No newline at end of file diff --git a/src/software/src/ui/static/annotations/img_1746536045_05.json b/src/software/src/ui/static/annotations/img_1746536045_05.json new file mode 100644 index 00000000..8c16632e --- /dev/null +++ b/src/software/src/ui/static/annotations/img_1746536045_05.json @@ -0,0 +1,9 @@ +[ + { + "x": 677.3609536917098, + "y": 894.9805757837389, + "width": 1138.0725388601036, + "height": 442.63106796116494, + "class": "casualty" + } +] \ No newline at end of file diff --git a/src/software/src/ui/static/annotations/img_1746536045_06.json b/src/software/src/ui/static/annotations/img_1746536045_06.json new file mode 100644 index 00000000..d9108572 --- /dev/null +++ b/src/software/src/ui/static/annotations/img_1746536045_06.json @@ -0,0 +1,9 @@ +[ + { + "x": 194.732217611838, + "y": 293.69761911478724, + "width": 268.61626248216834, + "height": 167.63714902807772, + "class": "casualty" + } +] \ No newline at end of file diff --git a/src/software/src/ui/static/annotations/img_1746536053_07.json b/src/software/src/ui/static/annotations/img_1746536053_07.json new file mode 100644 index 00000000..6dda35bf --- /dev/null +++ b/src/software/src/ui/static/annotations/img_1746536053_07.json @@ -0,0 +1,9 @@ +[ + { + "x": 142.80639735506196, + "y": 558.0339801654405, + "width": 488.30242510699003, + "height": 295.6647791619479, + "class": "casualty" + } +] \ No newline at end of file diff --git a/src/software/src/ui/static/annotations/img_1746536053_08.json b/src/software/src/ui/static/annotations/img_1746536053_08.json new file mode 100644 index 00000000..b7abeaa5 --- /dev/null +++ b/src/software/src/ui/static/annotations/img_1746536053_08.json @@ -0,0 +1,9 @@ +[ + { + "x": 436.9429980658497, + "y": 254.91261424833132, + "width": 492.4352331606218, + "height": 341.12621359223294, + "class": "casualty" + } +] \ No newline at end of file diff --git a/src/software/src/ui/static/annotations/img_1746536053_09.json b/src/software/src/ui/static/annotations/img_1746536053_09.json new file mode 100644 index 00000000..805ecf25 --- /dev/null +++ b/src/software/src/ui/static/annotations/img_1746536053_09.json @@ -0,0 +1,9 @@ +[ + { + "x": 225.6994747497875, + "y": 360.23300615329185, + "width": 537.2020725388602, + "height": 142.60194174757282, + "class": "casualty" + } +] \ No newline at end of file diff --git a/src/software/src/ui/static/annotations/img_1746536053_10.json b/src/software/src/ui/static/annotations/img_1746536053_10.json new file mode 100644 index 00000000..9ee0357a --- /dev/null +++ b/src/software/src/ui/static/annotations/img_1746536053_10.json @@ -0,0 +1,9 @@ +[ + { + "x": 16.974595112047698, + "y": 179.43037974683543, + "width": 453.0442105263158, + "height": 135.56962025316457, + "class": "casualty" + } +] \ No newline at end of file diff --git a/src/software/src/ui/static/css/styles.css b/src/software/src/ui/static/css/styles.css new file mode 100644 index 00000000..127f30ec --- /dev/null +++ b/src/software/src/ui/static/css/styles.css @@ -0,0 +1,342 @@ +/* 全局样式 */ +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background-color: #f0f4f8; + color: #333; + margin: 0; + padding: 0; +} + +.navbar { + background: linear-gradient(90deg, #1a3a5f 0%, #2d5f8b 100%); + box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2); + padding: 15px 0; +} + +.navbar-brand { + font-weight: bold; + font-size: 1.5rem; + letter-spacing: 0.5px; + color: white !important; +} + +.container { + padding: 20px; +} + +/* 地图样式 */ +#map { + height: 550px; + width: 100%; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + border: 1px solid #e0e0e0; +} + +/* 状态面板样式 */ +.status-panel { + background-color: white; + padding: 15px; + border-radius: 8px; + margin-bottom: 15px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + border-left: 4px solid #3498db; +} + +.status-panel p { + margin-bottom: 8px; + display: flex; + justify-content: space-between; +} + +.card { + margin-bottom: 20px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + border: none; + border-radius: 8px; + overflow: hidden; +} + +.card-header { + font-weight: 600; + background-color: #f8fafc; + padding: 15px; + border-bottom: 1px solid #e0e6ed; + display: flex; + align-items: center; +} + +.card-header i { + margin-right: 8px; + color: #3498db; +} + +.card-body { + padding: 20px; + background-color: white; +} + +/* 按钮样式 */ +.btn-primary { + background: linear-gradient(45deg, #2d5f8b 0%, #3498db 100%); + border: none; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); + padding: 10px 16px; + border-radius: 5px; + font-weight: 500; + transition: all 0.3s ease; +} + +.btn-primary:hover { + background: linear-gradient(45deg, #3498db 0%, #2d5f8b 100%); + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +} + +.btn-secondary { + background-color: #f8f9fa; + color: #495057; + border: 1px solid #ced4da; + transition: all 0.3s ease; +} + +.btn-secondary:hover { + background-color: #e2e6ea; +} + +/* 列表样式 */ +.casualty-item, .supply-item { + padding: 15px; + margin-bottom: 10px; + background-color: white; + border-radius: 8px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + border-left: 4px solid #e74c3c; + transition: all 0.3s ease; +} + +.supply-item { + border-left-color: #2ecc71; +} + +.casualty-item:hover, .supply-item:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); +} + +/* 状态指示器 */ +.status-indicator { + width: 12px; + height: 12px; + border-radius: 50%; + display: inline-block; + margin-right: 8px; + position: relative; +} + +.status-idle { + background-color: #95a5a6; +} + +.status-in-flight { + background-color: #3498db; + animation: pulse 1.5s infinite; +} + +.status-warning { + background-color: #f39c12; +} + +.status-error { + background-color: #e74c3c; +} + +@keyframes pulse { + 0% { opacity: 1; } + 50% { opacity: 0.5; } + 100% { opacity: 1; } +} + +/* 任务列表样式 */ +.mission-list { + max-height: 300px; + overflow-y: auto; + padding-right: 5px; +} + +.mission-list::-webkit-scrollbar { + width: 6px; +} + +.mission-list::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 10px; +} + +.mission-list::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 10px; +} + +.mission-item { + padding: 12px 15px; + margin-bottom: 8px; + border-radius: 6px; + background-color: white; + display: flex; + justify-content: space-between; + align-items: center; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); +} + +.mission-pending { + border-left: 4px solid #95a5a6; +} + +.mission-in-progress { + border-left: 4px solid #3498db; +} + +.mission-completed { + border-left: 4px solid #2ecc71; +} + +.mission-failed { + border-left: 4px solid #e74c3c; +} + +/* 自定义地图标记样式 */ +.casualty-marker .leaflet-popup-content-wrapper { + background-color: white; + border-radius: 8px; + border-left: 4px solid #e74c3c; + box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2); +} + +.drone-marker .leaflet-popup-content-wrapper { + background-color: white; + border-radius: 8px; + border-left: 4px solid #3498db; + box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2); +} + +/* 响应式布局调整 */ +@media (max-width: 768px) { + #map { + height: 400px; + } + + .col-md-4 { + margin-top: 20px; + } + + .card-header { + padding: 12px; + } + + .card-body { + padding: 15px; + } +} + +/* 地图标记样式 */ +.map-marker { + width: 30px; + height: 30px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3); +} + +.casualty-marker { + background-color: rgba(231, 76, 60, 0.9); + color: white; + font-size: 14px; + border: 2px solid white; +} + +.drone-marker { + background-color: rgba(52, 152, 219, 0.9); + color: white; + font-size: 16px; + border: 2px solid white; + animation: pulse 1.5s infinite; +} + +.popup-content { + min-width: 200px; + padding: 5px; +} + +.popup-content h6 { + margin-bottom: 10px; + font-weight: 600; + border-bottom: 1px solid #eee; + padding-bottom: 5px; +} + +.status-counter { + background-color: white; + border-radius: 8px; + padding: 15px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; +} + +.status-counter:hover { + transform: translateY(-3px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +.status-counter span { + font-size: 24px; + font-weight: bold; + color: #3498db; +} + +.status-counter p { + margin-top: 5px; + margin-bottom: 0; + color: #7f8c8d; + font-size: 0.9rem; +} + +/* 警告提示 */ +.alert { + border-radius: 8px; + padding: 12px 15px; + margin-bottom: 15px; + border: none; +} + +.alert-info { + background-color: #e9f7fe; + color: #3498db; +} + +/* 高级表单控件 */ +.form-label { + font-weight: 500; + margin-bottom: 8px; + color: #34495e; +} + +.form-control, .form-select { + border-radius: 6px; + padding: 10px 12px; + border: 1px solid #dcdfe6; + transition: all 0.3s ease; +} + +.form-control:focus, .form-select:focus { + border-color: #3498db; + box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.25); +} + +.badge { + padding: 6px 10px; + font-weight: 500; + border-radius: 6px; +} \ No newline at end of file diff --git a/src/software/src/ui/static/js/app.js b/src/software/src/ui/static/js/app.js new file mode 100644 index 00000000..9702592d --- /dev/null +++ b/src/software/src/ui/static/js/app.js @@ -0,0 +1,582 @@ +// 初始化应用 +document.addEventListener('DOMContentLoaded', function() { + // 初始化地图 + initMap(); + + // 初始化WebSocket连接 + initWebSocket(); + + // 加载伤员数据 + loadCasualties(); + + // 加载医疗物资数据 + loadMedicalSupplies(); + + // 加载无人机状态 + loadDroneStatus(); +}); + +// 全局变量 +let map = null; +let socket = null; +let casualtyMarkers = {}; +let droneMarker = null; +let droneIcon = null; +let casualtyIcon = null; + +// 初始化地图 +function initMap() { + // 创建地图 + map = L.map('map').setView([35.8617, 104.1954], 4); // 中国中心位置 + + // 添加OSM地图图层 + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors' + }).addTo(map); + + // 创建自定义图标 + droneIcon = L.icon({ + iconUrl: '/static/img/drone.png', + iconSize: [32, 32], + iconAnchor: [16, 16], + popupAnchor: [0, -16] + }); + + casualtyIcon = L.icon({ + iconUrl: '/static/img/casualty.png', + iconSize: [32, 32], + iconAnchor: [16, 16], + popupAnchor: [0, -16] + }); + + // 地图点击事件 - 可以用于直接在地图上添加伤员 + map.on('click', function(e) { + console.log("地图点击位置:", e.latlng.lat, e.latlng.lng); + }); +} + +// 初始化WebSocket连接 +function initWebSocket() { + socket = io(); + + // 连接成功 + socket.on('connect', function() { + console.log('WebSocket连接已建立'); + updateConnectionStatus('已连接'); + }); + + // 连接断开 + socket.on('disconnect', function() { + console.log('WebSocket连接已断开'); + updateConnectionStatus('已断开'); + }); + + // 伤员位置更新 + socket.on('casualty_location_updated', function(data) { + updateCasualtyMarker(data); + updateCasualtyList(); + }); + + // 无人机状态更新 + socket.on('drone_status_updated', function(data) { + updateDroneStatus(data); + }); + + // 医疗物资更新 + socket.on('medical_supply_updated', function(data) { + updateSupplyStatus(data); + }); + + // 任务状态更新 + socket.on('mission_status_updated', function(data) { + updateMissionStatus(data); + }); + + // 检测结果更新 + socket.on('detection_result', function(data) { + handleDetectionResult(data); + }); +} + +// 更新连接状态 +function updateConnectionStatus(status) { + const connectionStatus = document.getElementById('connection-status'); + if (connectionStatus) { + connectionStatus.textContent = status; + + if (status === '已连接') { + connectionStatus.classList.remove('text-danger'); + connectionStatus.classList.add('text-success'); + } else { + connectionStatus.classList.remove('text-success'); + connectionStatus.classList.add('text-danger'); + } + } +} + +// 更新伤员标记 +function updateCasualtyMarker(data) { + const lat = data.latitude; + const lon = data.longitude; + const id = data.casualty_id; + + if (casualtyMarkers[id]) { + casualtyMarkers[id].setLatLng([lat, lon]); + } else { + const marker = L.marker([lat, lon], {icon: casualtyIcon}).addTo(map); + marker.bindPopup(` +
+
伤员ID: ${id}
+

位置: ${lat.toFixed(6)}, ${lon.toFixed(6)}

+ +
+ `); + casualtyMarkers[id] = marker; + } + + // 更新伤员列表UI + updateCasualtyList(); +} + +// 更新无人机状态 +function updateDroneStatus(data) { + // 更新状态面板 + document.getElementById('drone-status').textContent = data.status; + document.getElementById('drone-battery').textContent = data.battery + '%'; + document.getElementById('drone-location').textContent = + `纬度: ${data.latitude.toFixed(6)}, 经度: ${data.longitude.toFixed(6)}`; + + // 根据电量变化状态颜色 + const batteryLevel = parseInt(data.battery); + const batteryElement = document.getElementById('drone-battery'); + + if (batteryLevel < 20) { + batteryElement.className = 'text-danger'; + } else if (batteryLevel < 50) { + batteryElement.className = 'text-warning'; + } else { + batteryElement.className = 'text-success'; + } + + // 更新无人机标记 + if (droneMarker) { + droneMarker.setLatLng([data.latitude, data.longitude]); + } else { + droneMarker = L.marker([data.latitude, data.longitude], {icon: droneIcon}).addTo(map); + droneMarker.bindPopup(` +
+
无人机状态
+

状态: ${data.status}

+

电量: ${data.battery}%

+
+ `); + } +} + +// 更新物资状态 +function updateSupplyStatus(data) { + // 更新物资列表 + const supplyList = document.getElementById('supply-list'); + + if (!supplyList) return; + + supplyList.innerHTML = ''; + + if (data.supplies && data.supplies.length > 0) { + data.supplies.forEach(supply => { + const supplyItem = document.createElement('div'); + supplyItem.className = 'supply-item'; + supplyItem.innerHTML = ` +
+
+ ${supply.name} (${supply.quantity}个可用) +
+ +
+ ${supply.description} + `; + supplyList.appendChild(supplyItem); + }); + } else { + supplyList.innerHTML = '

没有可用物资

'; + } +} + +// 更新任务状态 +function updateMissionStatus(data) { + // 更新任务列表 + const missionList = document.getElementById('mission-list'); + + if (!missionList) return; + + if (data.currentMission) { + // 显示当前任务 + const currentMissionElement = document.getElementById('current-mission'); + if (currentMissionElement) { + currentMissionElement.innerHTML = ` +
+
+ ${getMissionTypeLabel(data.currentMission.mission_type)} + + ${data.currentMission.status} + +
+
ID: ${data.currentMission.mission_id}
+
目标: (${data.currentMission.target_location[0].toFixed(6)}, + ${data.currentMission.target_location[1].toFixed(6)})
+
+ `; + } + } + + // 更新历史任务列表 + if (data.missionHistory && data.missionHistory.length > 0) { + missionList.innerHTML = ''; + + data.missionHistory.forEach(mission => { + const missionItem = document.createElement('div'); + missionItem.className = `mission-item mission-${mission.status.toLowerCase()}`; + missionItem.innerHTML = ` +
+ ${getMissionTypeLabel(mission.mission_type)} + + ${mission.status} + +
+
ID: ${mission.mission_id}
+ ${formatTime(mission.start_time)} + `; + missionList.appendChild(missionItem); + }); + } else { + missionList.innerHTML = '

没有历史任务

'; + } +} + +// 获取任务类型标签 +function getMissionTypeLabel(missionType) { + switch (missionType) { + case 'MEDICAL_SUPPLY': return '医疗物资运送'; + case 'SURVEILLANCE': return '区域侦察'; + case 'RETURN_TO_BASE': return '返回基地'; + default: return missionType; + } +} + +// 获取任务状态标签样式 +function getMissionStatusBadge(status) { + switch (status) { + case 'PENDING': return 'secondary'; + case 'IN_PROGRESS': return 'primary'; + case 'COMPLETED': return 'success'; + case 'FAILED': return 'danger'; + default: return 'secondary'; + } +} + +// 格式化时间 +function formatTime(timestamp) { + if (!timestamp) return ''; + + const date = new Date(timestamp * 1000); + return date.toLocaleString(); +} + +// 加载伤员数据 +function loadCasualties() { + fetch('/api/casualties') + .then(response => response.json()) + .then(data => { + if (data.casualties && data.casualties.length > 0) { + data.casualties.forEach(casualty => { + updateCasualtyMarker(casualty); + }); + } + }) + .catch(error => console.error('加载伤员数据失败:', error)); +} + +// 更新伤员列表 +function updateCasualtyList() { + const casualtyList = document.getElementById('casualty-list'); + + if (!casualtyList) return; + + // 清空现有列表 + casualtyList.innerHTML = ''; + + // 遍历所有伤员标记 + for (const id in casualtyMarkers) { + const marker = casualtyMarkers[id]; + const position = marker.getLatLng(); + + const casualtyItem = document.createElement('div'); + casualtyItem.className = 'casualty-item'; + casualtyItem.innerHTML = ` +
+
+ 伤员ID: ${id} +
+ +
+
位置: ${position.lat.toFixed(6)}, ${position.lng.toFixed(6)}
+ `; + + casualtyList.appendChild(casualtyItem); + } + + // 如果没有伤员,显示提示信息 + if (Object.keys(casualtyMarkers).length === 0) { + casualtyList.innerHTML = '

没有伤员数据

'; + } +} + +// 加载医疗物资数据 +function loadMedicalSupplies() { + fetch('/api/medical-supplies') + .then(response => response.json()) + .then(data => { + updateSupplyStatus(data); + }) + .catch(error => console.error('加载医疗物资失败:', error)); +} + +// 加载无人机状态 +function loadDroneStatus() { + fetch('/api/drone/status') + .then(response => response.json()) + .then(data => { + updateDroneStatus(data); + }) + .catch(error => console.error('加载无人机状态失败:', error)); +} + +// 添加伤员 +function addCasualty() { + const modal = new bootstrap.Modal(document.getElementById('addCasualtyModal')); + modal.show(); +} + +// 提交伤员信息 +function submitCasualty() { + const id = document.getElementById('casualty-id').value; + const lat = parseFloat(document.getElementById('casualty-lat').value); + const lon = parseFloat(document.getElementById('casualty-lon').value); + + if (!id || isNaN(lat) || isNaN(lon)) { + alert('请填写完整的伤员信息'); + return; + } + + const data = { + casualty_id: id, + latitude: lat, + longitude: lon + }; + + // 通过API发送 + fetch('/api/casualties', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }) + .then(response => response.json()) + .then(result => { + if (result.status === 'success') { + // 关闭模态框 + bootstrap.Modal.getInstance(document.getElementById('addCasualtyModal')).hide(); + + // 通过WebSocket发送更新 + socket.emit('update_casualty_location', data); + } else { + alert('添加伤员失败: ' + result.message); + } + }) + .catch(error => { + console.error('添加伤员失败:', error); + alert('添加伤员失败,请检查网络连接'); + }); +} + +// 请求医疗物资 +function requestMedicalSupply(casualtyId) { + // 打开请求物资模态框 + const modal = new bootstrap.Modal(document.getElementById('requestSuppliesModal')); + document.getElementById('supply-target').value = casualtyId; + modal.show(); +} + +// 提交医疗物资请求 +function submitSupplyRequest() { + const type = document.getElementById('supply-type').value; + const quantity = parseInt(document.getElementById('supply-quantity').value); + const target = document.getElementById('supply-target').value; + + if (!type || isNaN(quantity) || quantity <= 0 || !target) { + alert('请填写完整的物资请求信息'); + return; + } + + const data = { + type: type, + quantity: quantity, + target_id: target + }; + + // 通过API发送 + fetch('/api/medical-supplies', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }) + .then(response => response.json()) + .then(result => { + if (result.status === 'success') { + // 关闭模态框 + bootstrap.Modal.getInstance(document.getElementById('requestSuppliesModal')).hide(); + + // 通过WebSocket发送更新 + socket.emit('request_medical_supplies', data); + } else { + alert('请求物资失败: ' + result.message); + } + }) + .catch(error => { + console.error('请求物资失败:', error); + alert('请求物资失败,请检查网络连接'); + }); +} + +// 显示伤员检测模态框 +function showCasualtyDetection() { + // 清空之前的结果 + document.getElementById('detection-result').innerHTML = ` +
+ 请上传图像或使用无人机摄像头进行伤员识别 +
+ `; + + // 显示模态框 + var modal = new bootstrap.Modal(document.getElementById('detectCasualtyModal')); + modal.show(); +} + +// 上传图像并进行伤员检测 +function uploadAndDetect() { + const fileInput = document.getElementById('detection-image'); + + if (!fileInput.files || fileInput.files.length === 0) { + alert('请选择一张图片'); + return; + } + + const file = fileInput.files[0]; + const formData = new FormData(); + formData.append('image', file); + + // 显示加载中... + document.getElementById('detection-result').innerHTML = ` +
+
+ 正在加载... +
+

正在进行伤员识别,请稍候...

+
+ `; + + // 发送请求进行检测 + fetch('/api/detect-casualty', { + method: 'POST', + body: formData + }) + .then(response => response.json()) + .then(result => { + handleDetectionResult(result); + }) + .catch(error => { + console.error('伤员检测失败:', error); + document.getElementById('detection-result').innerHTML = ` +
+ 检测失败: ${error} +
+ `; + }); +} + +// 从无人机摄像头进行检测 +function detectFromDroneCamera() { + // 显示加载中... + document.getElementById('detection-result').innerHTML = ` +
+
+ 正在加载... +
+

正在从无人机获取图像并进行伤员识别,请稍候...

+
+ `; + + // 通过WebSocket请求无人机图像检测 + socket.emit('detect_from_drone_camera', {}); +} + +// 处理检测结果 +function handleDetectionResult(result) { + if (!result.success) { + document.getElementById('detection-result').innerHTML = ` +
+ 检测失败: ${result.error || '未知错误'} +
+ `; + return; + } + + if (!result.detections || result.detections.length === 0) { + document.getElementById('detection-result').innerHTML = ` +
+ 未检测到伤员 +
+ 检测结果 + `; + return; + } + + // 显示检测结果 + let resultHTML = ` +
+ 检测成功! 发现 ${result.detections.length} 名伤员 +
+
+
+ 检测结果 +
+
+
检测详情:
+
    + `; + + // 添加每个检测结果 + result.detections.forEach((detection, index) => { + resultHTML += ` +
  • +
    伤员 #${index + 1}
    +
    置信度: ${(detection.confidence * 100).toFixed(2)}%
    +
  • + `; + }); + + resultHTML += ` +
+
+
+ `; + + document.getElementById('detection-result').innerHTML = resultHTML; +} \ No newline at end of file diff --git a/src/software/src/ui/static/js/model_training.js b/src/software/src/ui/static/js/model_training.js new file mode 100644 index 00000000..a8c45cf9 --- /dev/null +++ b/src/software/src/ui/static/js/model_training.js @@ -0,0 +1,853 @@ +// 模型训练页面的JavaScript +document.addEventListener('DOMContentLoaded', function() { + // Socket.io 连接 + const socket = io(); + + // 状态变量 + let uploadedImages = []; + let annotations = {}; + let currentImageIndex = 0; + let canvas, ctx; + let isDrawing = false; + let startX, startY; + let currentBox = null; + let currentBoxes = []; + let currentImage = null; + + // 获取DOM元素 + const dropArea = document.getElementById('dropArea'); + const batchUpload = document.getElementById('batch-upload'); + const uploadProgress = document.getElementById('upload-progress'); + const progressBar = uploadProgress.querySelector('.progress-bar'); + const imagesPreview = document.getElementById('images-preview'); + const imageCountElement = document.getElementById('image-count'); + const clearImagesButton = document.getElementById('clear-images'); + const continueToAnnotationButton = document.getElementById('continue-to-annotation'); + const annotationCanvas = document.getElementById('annotation-canvas'); + const prevImageButton = document.getElementById('prev-image'); + const nextImageButton = document.getElementById('next-image'); + const currentImageElement = document.getElementById('current-image'); + const totalImagesElement = document.getElementById('total-images'); + const createBoxButton = document.getElementById('create-box'); + const deleteBoxButton = document.getElementById('delete-box'); + const annotationsContainer = document.getElementById('annotations-container'); + const noAnnotationsElement = document.getElementById('no-annotations'); + const saveAnnotationsButton = document.getElementById('save-annotations'); + const backToDataButton = document.getElementById('back-to-data'); + const continueToTrainingButton = document.getElementById('continue-to-training'); + const backToAnnotationButton = document.getElementById('back-to-annotation'); + const startTrainingButton = document.getElementById('start-training'); + const deployModelButton = document.querySelector('#deploy-model'); + const trainingSplitValue = document.getElementById('split-value'); + const trainingSplitInput = document.getElementById('train-split'); + const trainingStatusElement = document.getElementById('training-status'); + const trainingProgressElement = document.getElementById('training-progress'); + const trainingResultsElement = document.getElementById('training-results'); + + // Tab按钮 + const dataTab = document.getElementById('data-tab'); + const annotationTab = document.getElementById('annotation-tab'); + const trainingTab = document.getElementById('training-tab'); + + // 统计信息 + const statsImages = document.getElementById('stats-images'); + const statsAnnotations = document.getElementById('stats-annotations'); + const statsModels = document.getElementById('stats-models'); + + // 初始化函数 + function init() { + // 初始化拖拽上传区域 + initDragAndDrop(); + + // 初始化标注画布 + initAnnotationCanvas(); + + // 初始化标签页切换 + initTabs(); + + // 初始化训练参数 + initTrainingParameters(); + + // 加载已有图片和标注数据 + loadExistingData(); + + // 设置WebSocket监听 + setupWebSocketListeners(); + } + + // 初始化拖拽上传区域 + function initDragAndDrop() { + // 点击上传区域触发文件选择 + dropArea.addEventListener('click', function() { + batchUpload.click(); + }); + + // 文件选择变化处理 + batchUpload.addEventListener('change', function(e) { + handleFiles(e.target.files); + }); + + // 拖拽事件处理 + ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { + dropArea.addEventListener(eventName, preventDefaults, false); + }); + + function preventDefaults(e) { + e.preventDefault(); + e.stopPropagation(); + } + + // 拖拽视觉反馈 + ['dragenter', 'dragover'].forEach(eventName => { + dropArea.addEventListener(eventName, highlight, false); + }); + + ['dragleave', 'drop'].forEach(eventName => { + dropArea.addEventListener(eventName, unhighlight, false); + }); + + function highlight() { + dropArea.classList.add('border-primary'); + } + + function unhighlight() { + dropArea.classList.remove('border-primary'); + } + + // 处理拖放文件 + dropArea.addEventListener('drop', function(e) { + const dt = e.dataTransfer; + const files = dt.files; + handleFiles(files); + }, false); + + // 清空图片按钮 + clearImagesButton.addEventListener('click', function() { + clearImages(); + }); + + // 继续到标注按钮 + continueToAnnotationButton.addEventListener('click', function() { + if (uploadedImages.length > 0) { + annotationTab.click(); + } else { + alert('请先上传图片!'); + } + }); + } + + // 处理上传的文件 + function handleFiles(files) { + if (files.length === 0) return; + + // 显示进度条 + uploadProgress.style.display = 'block'; + progressBar.style.width = '0%'; + + // 准备formData + const formData = new FormData(); + + // 添加所有文件 + for (let i = 0; i < files.length; i++) { + // 只添加图片文件 + if (files[i].type.startsWith('image/')) { + formData.append('images', files[i]); + } + } + + // 发送到服务器 + fetch('/api/upload-training-images', { + method: 'POST', + body: formData, + // 不设置Content-Type,让浏览器自动处理 + }) + .then(response => { + if (!response.ok) { + throw new Error('上传失败'); + } + return response.json(); + }) + .then(data => { + // 隐藏进度条 + uploadProgress.style.display = 'none'; + + if (data.success) { + // 添加上传的图片到预览区 + uploadedImages = uploadedImages.concat(data.images); + updateImagePreviews(); + updateStats(); + + // 显示成功消息 + alert(`成功上传 ${data.images.length} 张图片!`); + } else { + alert('上传失败: ' + data.error); + } + }) + .catch(error => { + // 隐藏进度条 + uploadProgress.style.display = 'none'; + + // 显示错误 + alert('上传出错: ' + error.message); + }); + + // 模拟上传进度 + let progress = 0; + const interval = setInterval(() => { + progress += 5; + progressBar.style.width = `${Math.min(progress, 90)}%`; + + if (progress >= 90) { + clearInterval(interval); + } + }, 200); + } + + // 更新图片预览区域 + function updateImagePreviews() { + // 更新计数 + imageCountElement.textContent = uploadedImages.length; + totalImagesElement.textContent = uploadedImages.length; + + // 清空预览区 + imagesPreview.innerHTML = ''; + + // 添加图片预览 + uploadedImages.forEach((image, index) => { + const imgElement = document.createElement('img'); + imgElement.src = image.url; + imgElement.alt = `图片 ${index + 1}`; + imgElement.className = 'preview-img'; + imgElement.dataset.index = index; + + // 点击预览图片跳转到标注 + imgElement.addEventListener('click', function() { + annotationTab.click(); + loadImageForAnnotation(index); + }); + + imagesPreview.appendChild(imgElement); + }); + } + + // 清空图片 + function clearImages() { + if (uploadedImages.length === 0) return; + + if (confirm('确定要清空所有已上传的图片吗?')) { + // 发送请求到服务器删除图片 + fetch('/api/clear-training-images', { + method: 'POST' + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + uploadedImages = []; + annotations = {}; + updateImagePreviews(); + updateStats(); + alert('已清空所有图片!'); + } else { + alert('清空失败: ' + data.error); + } + }) + .catch(error => { + alert('请求失败: ' + error.message); + }); + } + } + + // 初始化标注画布 + function initAnnotationCanvas() { + canvas = annotationCanvas; + ctx = canvas.getContext('2d'); + + // 画布事件监听 + canvas.addEventListener('mousedown', startDrawing); + canvas.addEventListener('mousemove', draw); + canvas.addEventListener('mouseup', endDrawing); + canvas.addEventListener('mouseout', endDrawing); + + // 标注操作按钮 + prevImageButton.addEventListener('click', () => { + if (currentImageIndex > 0) { + saveCurrentAnnotations(); + loadImageForAnnotation(currentImageIndex - 1); + } + }); + + nextImageButton.addEventListener('click', () => { + if (currentImageIndex < uploadedImages.length - 1) { + saveCurrentAnnotations(); + loadImageForAnnotation(currentImageIndex + 1); + } + }); + + createBoxButton.addEventListener('click', () => { + isDrawing = false; + currentBox = null; + }); + + deleteBoxButton.addEventListener('click', () => { + if (currentBoxes.length > 0 && confirm('确定删除所选标注框?')) { + currentBoxes = []; + drawImageWithBoxes(); + updateAnnotationsList(); + } + }); + + saveAnnotationsButton.addEventListener('click', () => { + saveAllAnnotations(); + }); + + backToDataButton.addEventListener('click', () => { + saveCurrentAnnotations(); + dataTab.click(); + }); + + continueToTrainingButton.addEventListener('click', () => { + saveCurrentAnnotations(); + trainingTab.click(); + }); + } + + // 开始绘制标注框 + function startDrawing(e) { + isDrawing = true; + + // 获取鼠标在画布中的坐标 + const rect = canvas.getBoundingClientRect(); + startX = e.clientX - rect.left; + startY = e.clientY - rect.top; + + // 调整为画布坐标系 + startX = startX * (canvas.width / canvas.offsetWidth); + startY = startY * (canvas.height / canvas.offsetHeight); + + currentBox = { + x: startX, + y: startY, + width: 0, + height: 0, + class: 'casualty' // 默认类别 + }; + } + + // 绘制标注框 + function draw(e) { + if (!isDrawing || !currentBox) return; + + // 获取鼠标在画布中的当前坐标 + const rect = canvas.getBoundingClientRect(); + const x = (e.clientX - rect.left) * (canvas.width / canvas.offsetWidth); + const y = (e.clientY - rect.top) * (canvas.height / canvas.offsetHeight); + + // 计算宽度和高度 + currentBox.width = x - startX; + currentBox.height = y - startY; + + // 重绘 + drawImageWithBoxes(); + } + + // 结束绘制标注框 + function endDrawing() { + if (isDrawing && currentBox) { + isDrawing = false; + + // 确保宽度和高度为正 + if (currentBox.width < 0) { + currentBox.x += currentBox.width; + currentBox.width = Math.abs(currentBox.width); + } + + if (currentBox.height < 0) { + currentBox.y += currentBox.height; + currentBox.height = Math.abs(currentBox.height); + } + + // 只有当框足够大时才添加 + if (currentBox.width > 5 && currentBox.height > 5) { + currentBoxes.push(currentBox); + updateAnnotationsList(); + } + + currentBox = null; + } + } + + // 绘制图像和标注框 + function drawImageWithBoxes() { + if (!currentImage) return; + + // 清除画布 + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // 绘制图像 + ctx.drawImage(currentImage, 0, 0, canvas.width, canvas.height); + + // 绘制已保存的框 + currentBoxes.forEach((box, index) => { + drawBox(box, index, 'rgba(0, 255, 0, 0.5)'); + }); + + // 绘制当前正在画的框 + if (isDrawing && currentBox) { + drawBox(currentBox, null, 'rgba(255, 0, 0, 0.5)'); + } + } + + // 绘制单个标注框 + function drawBox(box, index, color) { + ctx.strokeStyle = color; + ctx.lineWidth = 2; + ctx.strokeRect(box.x, box.y, box.width, box.height); + + // 绘制标签 + if (index !== null) { + ctx.fillStyle = color; + ctx.font = '14px Arial'; + ctx.fillText(`${box.class} ${index + 1}`, box.x, box.y - 5); + } + } + + // 加载图像进行标注 + function loadImageForAnnotation(index) { + if (index < 0 || index >= uploadedImages.length) return; + + currentImageIndex = index; + currentImageElement.textContent = index + 1; + + // 加载图像 + const img = new Image(); + img.src = uploadedImages[index].url; + img.onload = function() { + // 设置画布尺寸与图像一致 + canvas.width = img.width; + canvas.height = img.height; + + // 保存图像对象 + currentImage = img; + + // 加载该图像的标注 + loadAnnotationsForImage(index); + + // 绘制图像和标注框 + drawImageWithBoxes(); + }; + } + + // 加载图像的标注数据 + function loadAnnotationsForImage(index) { + const imageId = uploadedImages[index].id; + + if (annotations[imageId]) { + currentBoxes = annotations[imageId]; + } else { + currentBoxes = []; + + // 检查服务器是否有该图像的标注 + fetch(`/api/annotations/${imageId}`) + .then(response => response.json()) + .then(data => { + if (data.success && data.annotations) { + currentBoxes = data.annotations; + annotations[imageId] = currentBoxes; + drawImageWithBoxes(); + updateAnnotationsList(); + } + }) + .catch(error => { + console.error('加载标注失败:', error); + }); + } + + updateAnnotationsList(); + } + + // 更新标注列表 + function updateAnnotationsList() { + if (currentBoxes.length === 0) { + noAnnotationsElement.style.display = 'block'; + annotationsContainer.innerHTML = '

该图片暂无标注

'; + return; + } + + noAnnotationsElement.style.display = 'none'; + annotationsContainer.innerHTML = ''; + + currentBoxes.forEach((box, index) => { + const annotationItem = document.createElement('div'); + annotationItem.className = 'annotation-item d-flex justify-content-between align-items-center p-2 border-bottom'; + annotationItem.innerHTML = ` +
+ ${index + 1} + ${box.class} +
+
+ + 位置: (${Math.round(box.x)}, ${Math.round(box.y)}) + 尺寸: ${Math.round(box.width)}x${Math.round(box.height)} + +
+ `; + + annotationsContainer.appendChild(annotationItem); + }); + } + + // 保存当前图像的标注 + function saveCurrentAnnotations() { + if (uploadedImages.length === 0) return; + + const imageId = uploadedImages[currentImageIndex].id; + annotations[imageId] = currentBoxes; + + // 发送到服务器 + fetch('/api/save-annotation', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + image_id: imageId, + annotations: currentBoxes + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + console.log('标注已保存'); + updateStats(); + } else { + console.error('保存标注失败:', data.error); + } + }) + .catch(error => { + console.error('请求失败:', error); + }); + } + + // 保存所有标注 + function saveAllAnnotations() { + // 先保存当前图像的标注 + saveCurrentAnnotations(); + + // 显示成功消息 + alert('所有标注已保存!'); + } + + // 初始化标签页切换 + function initTabs() { + dataTab.addEventListener('click', function() { + // 无需特殊操作 + }); + + annotationTab.addEventListener('click', function() { + if (uploadedImages.length > 0) { + loadImageForAnnotation(currentImageIndex); + } + }); + + trainingTab.addEventListener('click', function() { + // 无需特殊操作 + }); + + backToAnnotationButton.addEventListener('click', function() { + annotationTab.click(); + }); + } + + // 初始化训练参数 + function initTrainingParameters() { + // 更新训练集比例显示 + trainingSplitInput.addEventListener('input', function() { + trainingSplitValue.textContent = `${this.value}%`; + }); + + // 开始训练按钮 + startTrainingButton.addEventListener('click', function() { + startTraining(); + }); + + // 部署模型按钮 + deployModelButton.addEventListener('click', function() { + deployModel(); + }); + } + + // 开始训练模型 + function startTraining() { + // 获取训练参数 + const modelType = document.getElementById('model-type').value; + const epochs = document.getElementById('epochs').value; + const batchSize = document.getElementById('batch-size').value; + const trainSplit = document.getElementById('train-split').value; + const modelName = document.getElementById('model-name').value; + + // 验证是否有足够的标注数据 + if (Object.keys(annotations).length < 10) { + alert('训练数据不足,请至少标注10张图片后再开始训练!'); + return; + } + + // 显示训练状态 + trainingStatusElement.style.display = 'block'; + trainingResultsElement.style.display = 'none'; + startTrainingButton.disabled = true; + + // 请求开始训练 + fetch('/api/start-training', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + model_type: modelType, + epochs: parseInt(epochs), + batch_size: parseInt(batchSize), + train_split: parseInt(trainSplit), + model_name: modelName + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + // 训练已开始,等待进度更新 + console.log('训练已开始,训练ID:', data.training_id); + } else { + alert('开始训练失败: ' + data.error); + trainingStatusElement.style.display = 'none'; + startTrainingButton.disabled = false; + } + }) + .catch(error => { + alert('请求失败: ' + error.message); + trainingStatusElement.style.display = 'none'; + startTrainingButton.disabled = false; + }); + } + + // 部署训练好的模型 + function deployModel() { + const modelName = document.getElementById('model-name').value; + + fetch('/api/deploy-model', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + model_name: modelName + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + alert('模型部署成功!现在伤员检测将使用新训练的模型。'); + } else { + alert('模型部署失败: ' + data.error); + } + }) + .catch(error => { + alert('请求失败: ' + error.message); + }); + } + + // 加载已有的图片和标注数据 + function loadExistingData() { + // 加载已有的图片 + fetch('/api/training-images') + .then(response => response.json()) + .then(data => { + if (data.success) { + uploadedImages = data.images; + updateImagePreviews(); + + // 如果有图片,加载标注 + if (uploadedImages.length > 0) { + // 加载所有图片的标注 + fetch('/api/all-annotations') + .then(response => response.json()) + .then(data => { + if (data.success) { + annotations = data.annotations; + updateStats(); + } + }); + } + } + }); + + // 加载已有的模型 + fetch('/api/models') + .then(response => response.json()) + .then(data => { + if (data.success && data.models.length > 0) { + const modelHistory = document.getElementById('model-history'); + modelHistory.innerHTML = ''; + + data.models.forEach(model => { + const modelItem = document.createElement('div'); + modelItem.className = 'model-card'; + modelItem.innerHTML = ` +
${model.name}
+

训练时间: ${new Date(model.created_at).toLocaleString()}

+

精度(mAP): ${model.map.toFixed(4)}

+
+ +
+ `; + + modelHistory.appendChild(modelItem); + }); + + // 添加部署按钮事件 + document.querySelectorAll('.deploy-model-btn').forEach(btn => { + btn.addEventListener('click', function() { + const modelName = this.dataset.model; + fetch('/api/deploy-model', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + model_name: modelName + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + alert(`模型 ${modelName} 部署成功!`); + } else { + alert('模型部署失败: ' + data.error); + } + }); + }); + }); + + // 更新统计信息 + statsModels.textContent = data.models.length; + } + }); + } + + // 设置WebSocket监听 + function setupWebSocketListeners() { + // 训练进度更新 + socket.on('training_progress', function(data) { + trainingProgressElement.textContent = `完成 ${data.current_epoch}/${data.total_epochs} 轮训练 (${Math.round(data.progress * 100)}%)`; + + // 如果训练完成,显示结果 + if (data.status === 'completed') { + trainingStatusElement.style.display = 'none'; + trainingResultsElement.style.display = 'block'; + startTrainingButton.disabled = false; + + // 显示训练结果图表 + showTrainingResults(data.results); + + // 刷新模型列表 + loadExistingData(); + } + }); + + // 训练错误 + socket.on('training_error', function(data) { + trainingStatusElement.style.display = 'none'; + startTrainingButton.disabled = false; + alert('训练出错: ' + data.error); + }); + } + + // 显示训练结果图表 + function showTrainingResults(results) { + // 精度图表 + const precisionCtx = document.getElementById('precision-chart').getContext('2d'); + new Chart(precisionCtx, { + type: 'line', + data: { + labels: results.epochs, + datasets: [{ + label: 'mAP (精度均值)', + data: results.map, + borderColor: 'rgba(75, 192, 192, 1)', + borderWidth: 2, + fill: false + }] + }, + options: { + responsive: true, + scales: { + y: { + beginAtZero: true, + max: 1 + } + } + } + }); + + // 损失图表 + const lossCtx = document.getElementById('loss-chart').getContext('2d'); + new Chart(lossCtx, { + type: 'line', + data: { + labels: results.epochs, + datasets: [{ + label: '总损失', + data: results.loss, + borderColor: 'rgba(255, 99, 132, 1)', + borderWidth: 2, + fill: false + }] + }, + options: { + responsive: true, + scales: { + y: { + beginAtZero: true + } + } + } + }); + } + + // 更新统计信息 + function updateStats() { + statsImages.textContent = uploadedImages.length; + + // 计算已标注的图片数量 + const annotatedCount = Object.keys(annotations).length; + statsAnnotations.textContent = annotatedCount; + } + + // 模拟训练进度(仅用于演示) + function simulateTrainingProgress() { + let epoch = 0; + const totalEpochs = parseInt(document.getElementById('epochs').value); + + const interval = setInterval(() => { + epoch++; + socket.emit('training_progress', { + current_epoch: epoch, + total_epochs: totalEpochs, + progress: epoch / totalEpochs, + status: epoch >= totalEpochs ? 'completed' : 'training', + results: { + epochs: Array.from({length: epoch}, (_, i) => i + 1), + map: Array.from({length: epoch}, (_, i) => 0.2 + (i / totalEpochs) * 0.7 + Math.random() * 0.05), + loss: Array.from({length: epoch}, (_, i) => 1.5 - (i / totalEpochs) + Math.random() * 0.2) + } + }); + + if (epoch >= totalEpochs) { + clearInterval(interval); + } + }, 500); + } + + // 初始化 + init(); +}); \ No newline at end of file diff --git a/src/software/src/ui/static/models/casualty_detector_v1.pt b/src/software/src/ui/static/models/casualty_detector_v1.pt new file mode 100644 index 00000000..147a1f10 --- /dev/null +++ b/src/software/src/ui/static/models/casualty_detector_v1.pt @@ -0,0 +1 @@ +This is a placeholder for a real YOLO model file \ No newline at end of file diff --git a/src/software/src/ui/static/models/dataset/dataset.yaml b/src/software/src/ui/static/models/dataset/dataset.yaml new file mode 100644 index 00000000..7bc1d7ea --- /dev/null +++ b/src/software/src/ui/static/models/dataset/dataset.yaml @@ -0,0 +1,9 @@ +# YOLOv5 ݼ +path: src/ui/static/models\dataset +train: images/train +val: images/val +test: # ޲Լ + +# +nc: 1 # +names: ['casualty'] # diff --git a/src/software/src/ui/static/models/dataset/images/train/1746535839_01.jpg b/src/software/src/ui/static/models/dataset/images/train/1746535839_01.jpg new file mode 100644 index 00000000..1b8e5593 Binary files /dev/null and b/src/software/src/ui/static/models/dataset/images/train/1746535839_01.jpg differ diff --git a/src/software/src/ui/static/models/dataset/images/train/1746536045_02.jpg b/src/software/src/ui/static/models/dataset/images/train/1746536045_02.jpg new file mode 100644 index 00000000..6b908ecc Binary files /dev/null and b/src/software/src/ui/static/models/dataset/images/train/1746536045_02.jpg differ diff --git a/src/software/src/ui/static/models/dataset/images/train/1746536045_03.jpg b/src/software/src/ui/static/models/dataset/images/train/1746536045_03.jpg new file mode 100644 index 00000000..b32f168c Binary files /dev/null and b/src/software/src/ui/static/models/dataset/images/train/1746536045_03.jpg differ diff --git a/src/software/src/ui/static/models/dataset/images/train/1746536045_04.jpg b/src/software/src/ui/static/models/dataset/images/train/1746536045_04.jpg new file mode 100644 index 00000000..2e92896b Binary files /dev/null and b/src/software/src/ui/static/models/dataset/images/train/1746536045_04.jpg differ diff --git a/src/software/src/ui/static/models/dataset/images/train/1746536045_05.jpg b/src/software/src/ui/static/models/dataset/images/train/1746536045_05.jpg new file mode 100644 index 00000000..c52540a1 Binary files /dev/null and b/src/software/src/ui/static/models/dataset/images/train/1746536045_05.jpg differ diff --git a/src/software/src/ui/static/models/dataset/images/train/1746536045_06.jpg b/src/software/src/ui/static/models/dataset/images/train/1746536045_06.jpg new file mode 100644 index 00000000..66ee4c7c Binary files /dev/null and b/src/software/src/ui/static/models/dataset/images/train/1746536045_06.jpg differ diff --git a/src/software/src/ui/static/models/dataset/images/train/1746536053_07.jpg b/src/software/src/ui/static/models/dataset/images/train/1746536053_07.jpg new file mode 100644 index 00000000..41d09e93 Binary files /dev/null and b/src/software/src/ui/static/models/dataset/images/train/1746536053_07.jpg differ diff --git a/src/software/src/ui/static/models/dataset/images/train/1746536053_08.jpg b/src/software/src/ui/static/models/dataset/images/train/1746536053_08.jpg new file mode 100644 index 00000000..7c3b085a Binary files /dev/null and b/src/software/src/ui/static/models/dataset/images/train/1746536053_08.jpg differ diff --git a/src/software/src/ui/static/models/dataset/images/val/1746536053_09.jpg b/src/software/src/ui/static/models/dataset/images/val/1746536053_09.jpg new file mode 100644 index 00000000..25b50c0d Binary files /dev/null and b/src/software/src/ui/static/models/dataset/images/val/1746536053_09.jpg differ diff --git a/src/software/src/ui/static/models/dataset/images/val/1746536053_10.jpg b/src/software/src/ui/static/models/dataset/images/val/1746536053_10.jpg new file mode 100644 index 00000000..bd089b76 Binary files /dev/null and b/src/software/src/ui/static/models/dataset/images/val/1746536053_10.jpg differ diff --git a/src/software/src/ui/static/models/dataset/labels/train/1746535839_01.txt b/src/software/src/ui/static/models/dataset/labels/train/1746535839_01.txt new file mode 100644 index 00000000..ac24659e --- /dev/null +++ b/src/software/src/ui/static/models/dataset/labels/train/1746535839_01.txt @@ -0,0 +1 @@ +0 0.5519446158377372 0.6656271444784629 0.5898801597869507 0.20997920997920996 \ No newline at end of file diff --git a/src/software/src/ui/static/models/dataset/labels/train/1746536045_02.txt b/src/software/src/ui/static/models/dataset/labels/train/1746536045_02.txt new file mode 100644 index 00000000..4d0b61b4 --- /dev/null +++ b/src/software/src/ui/static/models/dataset/labels/train/1746536045_02.txt @@ -0,0 +1 @@ +0 0.3789692768297698 0.6640378548895899 0.7073684210526316 0.583596214511041 \ No newline at end of file diff --git a/src/software/src/ui/static/models/dataset/labels/train/1746536045_03.txt b/src/software/src/ui/static/models/dataset/labels/train/1746536045_03.txt new file mode 100644 index 00000000..45a7b352 --- /dev/null +++ b/src/software/src/ui/static/models/dataset/labels/train/1746536045_03.txt @@ -0,0 +1 @@ +0 0.4583333267449097 0.4874031007751938 0.8847150259067357 0.7732558139534884 \ No newline at end of file diff --git a/src/software/src/ui/static/models/dataset/labels/train/1746536045_04.txt b/src/software/src/ui/static/models/dataset/labels/train/1746536045_04.txt new file mode 100644 index 00000000..5f7942e3 --- /dev/null +++ b/src/software/src/ui/static/models/dataset/labels/train/1746536045_04.txt @@ -0,0 +1 @@ +0 0.4194732231179667 0.844983813832107 0.5582901554404145 0.2776699029126213 \ No newline at end of file diff --git a/src/software/src/ui/static/models/dataset/labels/train/1746536045_05.txt b/src/software/src/ui/static/models/dataset/labels/train/1746536045_05.txt new file mode 100644 index 00000000..1b9f05a2 --- /dev/null +++ b/src/software/src/ui/static/models/dataset/labels/train/1746536045_05.txt @@ -0,0 +1 @@ +0 0.6085923941024227 0.8177993478126897 0.555699481865285 0.3242718446601941 \ No newline at end of file diff --git a/src/software/src/ui/static/models/dataset/labels/train/1746536045_06.txt b/src/software/src/ui/static/models/dataset/labels/train/1746536045_06.txt new file mode 100644 index 00000000..69e2f4e6 --- /dev/null +++ b/src/software/src/ui/static/models/dataset/labels/train/1746536045_06.txt @@ -0,0 +1 @@ +0 0.4700576412184603 0.8171346182442123 0.38373751783166904 0.36285097192224613 \ No newline at end of file diff --git a/src/software/src/ui/static/models/dataset/labels/train/1746536053_07.txt b/src/software/src/ui/static/models/dataset/labels/train/1746536053_07.txt new file mode 100644 index 00000000..55529a86 --- /dev/null +++ b/src/software/src/ui/static/models/dataset/labels/train/1746536053_07.txt @@ -0,0 +1 @@ +0 0.5527965855836527 0.8003020065152091 0.6975748930099858 0.33522083805209507 \ No newline at end of file diff --git a/src/software/src/ui/static/models/dataset/labels/train/1746536053_08.txt b/src/software/src/ui/static/models/dataset/labels/train/1746536053_08.txt new file mode 100644 index 00000000..3e3969d1 --- /dev/null +++ b/src/software/src/ui/static/models/dataset/labels/train/1746536053_08.txt @@ -0,0 +1 @@ +0 0.632556124672371 0.590938501450622 0.455958549222798 0.4737864077669902 \ No newline at end of file diff --git a/src/software/src/ui/static/models/dataset/labels/val/1746536053_09.txt b/src/software/src/ui/static/models/dataset/labels/val/1746536053_09.txt new file mode 100644 index 00000000..0e3d9c92 --- /dev/null +++ b/src/software/src/ui/static/models/dataset/labels/val/1746536053_09.txt @@ -0,0 +1 @@ +0 0.4576856583511274 0.599352745870942 0.49740932642487057 0.19805825242718447 \ No newline at end of file diff --git a/src/software/src/ui/static/models/dataset/labels/val/1746536053_10.txt b/src/software/src/ui/static/models/dataset/labels/val/1746536053_10.txt new file mode 100644 index 00000000..808783cd --- /dev/null +++ b/src/software/src/ui/static/models/dataset/labels/val/1746536053_10.txt @@ -0,0 +1 @@ +0 0.5137061189350329 0.7848101265822786 0.9557894736842105 0.4303797468354431 \ No newline at end of file diff --git a/src/software/src/ui/static/models/train_1746536145.json b/src/software/src/ui/static/models/train_1746536145.json new file mode 100644 index 00000000..45efca5f --- /dev/null +++ b/src/software/src/ui/static/models/train_1746536145.json @@ -0,0 +1,170 @@ +{ + "id": "train_1746536145", + "model_type": "yolov5s", + "epochs": 50, + "batch_size": 16, + "train_split": 80, + "model_name": "casualty_detector_v1", + "status": "completed", + "created_at": 1746536145.3327265, + "model_path": "src/ui/static/models\\casualty_detector_v1.pt", + "results": { + "epochs": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50 + ], + "map": [ + 0.2346910205281687, + 0.2465911830219659, + 0.2573409338308947, + 0.283949214087783, + 0.29985140911865305, + 0.3133456542772132, + 0.3414636954072322, + 0.3404219065408789, + 0.3394574027871224, + 0.3660841833106289, + 0.38276835276908183, + 0.4164534097991227, + 0.39504794443286423, + 0.41696871543371894, + 0.42355650398958544, + 0.4620468536652084, + 0.4581694345192589, + 0.4889842453771339, + 0.5100454579134762, + 0.5024204387235169, + 0.5141007414159628, + 0.5403691471891979, + 0.5301660185073334, + 0.5760235114161298, + 0.5861124620696604, + 0.5734799251193733, + 0.613563278778363, + 0.6019781840424621, + 0.6124835374681992, + 0.6648062309517518, + 0.669743039182848, + 0.6858017072735901, + 0.6746423708794934, + 0.7198532262813726, + 0.7264158744035305, + 0.7145871815350525, + 0.7544234524933556, + 0.7363939195224446, + 0.7485789500260398, + 0.7628166138477256, + 0.8119847496417109, + 0.8330255544173595, + 0.8054526755408713, + 0.8584588222370078, + 0.8480172179298857, + 0.8804983162248883, + 0.8730853946854259, + 0.8820644293747433, + 0.890186227403441, + 0.936543264621003 + ], + "loss": [ + 1.4952713683230068, + 1.6241079711922601, + 1.4556141314443767, + 1.536875339018873, + 1.582250123819695, + 1.5200034162657587, + 1.3981947004420512, + 1.4676312861136551, + 1.4700722509766602, + 1.375187501451575, + 1.3189966252775418, + 1.2924728482593522, + 1.3213448817287443, + 1.3195207299055696, + 1.3955919437857862, + 1.231887561345098, + 1.2721930658066198, + 1.3068925731122645, + 1.1826480682879805, + 1.226094298070738, + 1.126356833919786, + 1.2223144647648188, + 1.201390841319237, + 1.0854719146621141, + 1.0159738345019431, + 1.0444867373351245, + 1.1365222362152665, + 1.0991116537142849, + 0.981271583853104, + 0.9295018427850593, + 0.9021050310675309, + 1.0155725886367946, + 1.0124356436636157, + 0.9777325532673871, + 0.8102139457942824, + 0.896013791152899, + 0.8892310752180109, + 0.8525369026809214, + 0.7814648301152992, + 0.7454667157507703, + 0.7597884851926442, + 0.7853594166017319, + 0.833171319042936, + 0.7362808873599007, + 0.7006277248923057, + 0.7026997869166967, + 0.6064702709140819, + 0.5530920661720794, + 0.6786561049801432, + 0.6553200520346033 + ], + "final_map": 0.936543264621003 + } +} \ No newline at end of file diff --git a/src/software/src/ui/static/results/result_1745290119_I0WI-hfnsvzc1691343.jpg b/src/software/src/ui/static/results/result_1745290119_I0WI-hfnsvzc1691343.jpg new file mode 100644 index 00000000..bb48a175 Binary files /dev/null and b/src/software/src/ui/static/results/result_1745290119_I0WI-hfnsvzc1691343.jpg differ diff --git a/src/software/src/ui/static/results/result_1746444028_R-C.jpg b/src/software/src/ui/static/results/result_1746444028_R-C.jpg new file mode 100644 index 00000000..96c2e60e Binary files /dev/null and b/src/software/src/ui/static/results/result_1746444028_R-C.jpg differ diff --git a/src/software/src/ui/static/results/result_1746444056_R-C.jpg b/src/software/src/ui/static/results/result_1746444056_R-C.jpg new file mode 100644 index 00000000..f26fa18e Binary files /dev/null and b/src/software/src/ui/static/results/result_1746444056_R-C.jpg differ diff --git a/src/software/src/ui/static/results/result_1746444355_R-C.jpg b/src/software/src/ui/static/results/result_1746444355_R-C.jpg new file mode 100644 index 00000000..62fc67b3 Binary files /dev/null and b/src/software/src/ui/static/results/result_1746444355_R-C.jpg differ diff --git a/src/software/src/ui/static/results/result_1746536282_02.jpg b/src/software/src/ui/static/results/result_1746536282_02.jpg new file mode 100644 index 00000000..bf36dd95 Binary files /dev/null and b/src/software/src/ui/static/results/result_1746536282_02.jpg differ diff --git a/src/software/src/ui/static/results/result_1746536355_test01.jpg b/src/software/src/ui/static/results/result_1746536355_test01.jpg new file mode 100644 index 00000000..cae55ac1 Binary files /dev/null and b/src/software/src/ui/static/results/result_1746536355_test01.jpg differ diff --git a/src/software/src/ui/static/results/result_1746600557_test01.jpg b/src/software/src/ui/static/results/result_1746600557_test01.jpg new file mode 100644 index 00000000..229c1008 Binary files /dev/null and b/src/software/src/ui/static/results/result_1746600557_test01.jpg differ diff --git a/src/software/src/ui/static/results/result_1746600575_test01.jpg b/src/software/src/ui/static/results/result_1746600575_test01.jpg new file mode 100644 index 00000000..bec5b25a Binary files /dev/null and b/src/software/src/ui/static/results/result_1746600575_test01.jpg differ diff --git a/src/software/src/ui/static/training_images/1746535839_01.jpg b/src/software/src/ui/static/training_images/1746535839_01.jpg new file mode 100644 index 00000000..1b8e5593 Binary files /dev/null and b/src/software/src/ui/static/training_images/1746535839_01.jpg differ diff --git a/src/software/src/ui/static/training_images/1746536045_02.jpg b/src/software/src/ui/static/training_images/1746536045_02.jpg new file mode 100644 index 00000000..6b908ecc Binary files /dev/null and b/src/software/src/ui/static/training_images/1746536045_02.jpg differ diff --git a/src/software/src/ui/static/training_images/1746536045_03.jpg b/src/software/src/ui/static/training_images/1746536045_03.jpg new file mode 100644 index 00000000..b32f168c Binary files /dev/null and b/src/software/src/ui/static/training_images/1746536045_03.jpg differ diff --git a/src/software/src/ui/static/training_images/1746536045_04.jpg b/src/software/src/ui/static/training_images/1746536045_04.jpg new file mode 100644 index 00000000..2e92896b Binary files /dev/null and b/src/software/src/ui/static/training_images/1746536045_04.jpg differ diff --git a/src/software/src/ui/static/training_images/1746536045_05.jpg b/src/software/src/ui/static/training_images/1746536045_05.jpg new file mode 100644 index 00000000..c52540a1 Binary files /dev/null and b/src/software/src/ui/static/training_images/1746536045_05.jpg differ diff --git a/src/software/src/ui/static/training_images/1746536045_06.jpg b/src/software/src/ui/static/training_images/1746536045_06.jpg new file mode 100644 index 00000000..66ee4c7c Binary files /dev/null and b/src/software/src/ui/static/training_images/1746536045_06.jpg differ diff --git a/src/software/src/ui/static/training_images/1746536053_07.jpg b/src/software/src/ui/static/training_images/1746536053_07.jpg new file mode 100644 index 00000000..41d09e93 Binary files /dev/null and b/src/software/src/ui/static/training_images/1746536053_07.jpg differ diff --git a/src/software/src/ui/static/training_images/1746536053_08.jpg b/src/software/src/ui/static/training_images/1746536053_08.jpg new file mode 100644 index 00000000..7c3b085a Binary files /dev/null and b/src/software/src/ui/static/training_images/1746536053_08.jpg differ diff --git a/src/software/src/ui/static/training_images/1746536053_09.jpg b/src/software/src/ui/static/training_images/1746536053_09.jpg new file mode 100644 index 00000000..25b50c0d Binary files /dev/null and b/src/software/src/ui/static/training_images/1746536053_09.jpg differ diff --git a/src/software/src/ui/static/training_images/1746536053_10.jpg b/src/software/src/ui/static/training_images/1746536053_10.jpg new file mode 100644 index 00000000..bd089b76 Binary files /dev/null and b/src/software/src/ui/static/training_images/1746536053_10.jpg differ diff --git a/src/software/src/ui/static/uploads/02.jpg b/src/software/src/ui/static/uploads/02.jpg new file mode 100644 index 00000000..6b908ecc Binary files /dev/null and b/src/software/src/ui/static/uploads/02.jpg differ diff --git a/src/software/src/ui/static/uploads/I0WI-hfnsvzc1691343.jpg b/src/software/src/ui/static/uploads/I0WI-hfnsvzc1691343.jpg new file mode 100644 index 00000000..7c3b085a Binary files /dev/null and b/src/software/src/ui/static/uploads/I0WI-hfnsvzc1691343.jpg differ diff --git a/src/software/src/ui/static/uploads/R-C.jpg b/src/software/src/ui/static/uploads/R-C.jpg new file mode 100644 index 00000000..3e29fd2d Binary files /dev/null and b/src/software/src/ui/static/uploads/R-C.jpg differ diff --git a/src/software/src/ui/static/uploads/battlefield_scene.jpg b/src/software/src/ui/static/uploads/battlefield_scene.jpg new file mode 100644 index 00000000..67540c77 Binary files /dev/null and b/src/software/src/ui/static/uploads/battlefield_scene.jpg differ diff --git a/src/software/src/ui/static/uploads/test01.jpg b/src/software/src/ui/static/uploads/test01.jpg new file mode 100644 index 00000000..3d94400e Binary files /dev/null and b/src/software/src/ui/static/uploads/test01.jpg differ diff --git a/src/software/src/ui/templates/casualty_detection.html b/src/software/src/ui/templates/casualty_detection.html new file mode 100644 index 00000000..9f75c7ca --- /dev/null +++ b/src/software/src/ui/templates/casualty_detection.html @@ -0,0 +1,368 @@ + + + + + + 伤员识别 - 智能战场医疗后送系统 + + + + + + + + + +
+
+
+
+
+ 伤员识别 +
+
+
+
+

正在识别伤员,请稍候...

+
+ +
+
+ + + 支持的格式: JPG, PNG, BMP +
+ +
+ 预览图片 +
+ +
+ + +
+
+ +
+ + +
+
+
+ +
+
+
+ 使用说明 +
+
+
如何使用伤员识别功能
+
    +
  1. 点击"选择图像文件"按钮上传图片
  2. +
  3. 上传图片后点击"识别伤员"按钮
  4. +
  5. 系统会自动分析图片并标记识别到的伤员
  6. +
  7. 识别完成后,可以将检测到的伤员添加到系统中
  8. +
+
+ 提示:为提高识别准确率,请上传清晰的图片,且拍摄角度尽量垂直。 +
+
+
+ +
+
+ 最近识别历史 +
+
+
+

暂无识别历史

+
+
+
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/src/software/src/ui/templates/index.html b/src/software/src/ui/templates/index.html new file mode 100644 index 00000000..e8034550 --- /dev/null +++ b/src/software/src/ui/templates/index.html @@ -0,0 +1,711 @@ + + + + + + 智能战场医疗后送系统 + + + + + + + + + +
+
+ +
+
+
+ 战场地图 +
+ +
+
+
+
+
+
+ + +
+
+ 任务状态 +
+
+
+
+
+ 0 +

待处理任务

+
+
+
+
+ 0 +

进行中任务

+
+
+
+
+ 0 +

已完成任务

+
+
+
+
+ 0 +

失败任务

+
+
+
+
+
+
+ + +
+ +
+
+ 无人机状态 + 已连接 +
+
+
+

+ 状态: + 待机 +

+

+ 电量: + 100% +

+

+ 位置: + 未获取 +

+

+ 速度: + 0 m/s +

+
+
+
+ + +
+
+ 伤员信息 + 0 +
+
+
+ +
+ 尚无伤员信息 +
+
+
+ + +
+
+
+ + +
+
+ 医疗物资 +
+
+
+ +
+ 正在加载物资信息... +
+
+ +
+
+
+
+
+ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/software/src/ui/templates/model_training.html b/src/software/src/ui/templates/model_training.html new file mode 100644 index 00000000..b66071a5 --- /dev/null +++ b/src/software/src/ui/templates/model_training.html @@ -0,0 +1,381 @@ + + + + + + 模型训练 - 智能战场医疗后送系统 + + + + + + + + +
+
+
+
+
+ 模型训练 +
+
+ + +
+ +
+
数据集准备
+ +
+ +
拖放图片或点击上传
+

支持JPG、PNG格式的图片批量上传

+ +
+ +
+
+
+ +
+
已上传图片 (0)
+
+ +
+
+ +
+ + +
+
+ + +
+
数据标注
+ +
+
+
+ + + + 图片 0 / 0 + +
+
+ + +
+
+
+ +
+ +
+ +
+
当前图片标注
+
+ +

该图片暂无标注

+
+
+ +
+ + + +
+
+ + +
+
模型训练与评估
+ +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+
+ + +
+ 50% + 80% + 90% +
+
+
+
+ +
+ + +
+
+ +
+ + +
+ +
+
+
+
+
模型训练中...
+

完成 0/50 轮训练 (0%)

+
+
+
+ + +
+
+
+
+
+ +
+
+
+ 模型训练指南 +
+
+
如何训练自己的伤员识别模型
+
    +
  1. 数据准备 - 上传包含伤员的图片
  2. +
  3. 数据标注 - 标记图片中的伤员位置
  4. +
  5. 模型训练 - 设置参数并开始训练
  6. +
  7. 模型评估 - 检查模型性能
  8. +
  9. 模型部署 - 将训练好的模型投入使用
  10. +
+
+ 提示:数据质量是训练成功的关键。尽量收集不同角度、不同光照条件下的图片,以提高模型的泛化能力。 +
+
+
+ +
+
+ 模型版本 +
+
+
+ +

暂无训练模型

+
+
+
+ +
+
+ 训练统计 +
+
+
    +
  • + 已上传图片 + 0 +
  • +
  • + 已标注图片 + 0 +
  • +
  • + 训练完成模型 + 0 +
  • +
+
+
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/src/software/src/ui/web_interface.py b/src/software/src/ui/web_interface.py new file mode 100644 index 00000000..70702bf3 --- /dev/null +++ b/src/software/src/ui/web_interface.py @@ -0,0 +1,805 @@ +from flask import Flask, render_template, jsonify, request, send_from_directory, redirect, url_for +from flask_socketio import SocketIO, emit +import logging +import os +import json +from functools import wraps +from werkzeug.utils import secure_filename +import time + +class WebInterface: + def __init__(self, host='0.0.0.0', port=8080, casualty_detector=None): + """ + 初始化Web界面 + :param host: 服务器主机地址 + :param port: 服务器端口 + :param casualty_detector: 伤员检测器实例 + """ + self.app = Flask(__name__, static_folder='static', template_folder='templates') + self.socketio = SocketIO(self.app, cors_allowed_origins="*") + self.host = host + self.port = port + self.logger = logging.getLogger(__name__) + + # 设置上传文件目录 + self.app.config['UPLOAD_FOLDER'] = 'src/ui/static/uploads' + os.makedirs(self.app.config['UPLOAD_FOLDER'], exist_ok=True) + + # 设置训练相关目录 + self.app.config['TRAINING_IMAGES_FOLDER'] = 'src/ui/static/training_images' + self.app.config['ANNOTATIONS_FOLDER'] = 'src/ui/static/annotations' + self.app.config['MODELS_FOLDER'] = 'src/ui/static/models' + + # 创建必要的目录 + os.makedirs(self.app.config['TRAINING_IMAGES_FOLDER'], exist_ok=True) + os.makedirs(self.app.config['ANNOTATIONS_FOLDER'], exist_ok=True) + os.makedirs(self.app.config['MODELS_FOLDER'], exist_ok=True) + + # 处理器 + self.casualty_handler = None + self.supply_handler = None + self.drone_status_handler = None + self.casualty_detector = casualty_detector # 可选参数 + + # 注册路由 + self._register_routes() + + def set_casualty_handler(self, handler): + """设置伤员处理器""" + self.casualty_handler = handler + + def set_supply_handler(self, handler): + """设置物资处理器""" + self.supply_handler = handler + + def set_drone_status_handler(self, handler): + """设置无人机状态处理器""" + self.drone_status_handler = handler + + def set_casualty_detector(self, detector): + """设置伤员检测器""" + self.casualty_detector = detector + + def _register_routes(self): + """注册Web路由""" + # 主页 + @self.app.route('/') + def index(): + return render_template('index.html') + + # 静态文件 + @self.app.route('/static/') + def serve_static(path): + return send_from_directory(self.app.static_folder, path) + + # 伤员API + @self.app.route('/api/casualties', methods=['GET']) + def get_casualties(): + try: + if self.casualty_handler: + casualties = [] + # 实际应该调用position_manager.get_all_casualties() + return jsonify({'casualties': casualties, 'status': 'success'}) + return jsonify({'casualties': [], 'status': 'error', 'message': '伤员处理器未设置'}) + except Exception as e: + self.logger.error(f"获取伤员数据失败: {str(e)}") + return jsonify({'casualties': [], 'status': 'error', 'message': str(e)}) + + @self.app.route('/api/casualties', methods=['POST']) + def add_casualty(): + try: + data = request.get_json() + if self.casualty_handler: + result = self.casualty_handler(data) + return jsonify({'status': 'success' if result else 'error'}) + return jsonify({'status': 'error', 'message': '伤员处理器未设置'}) + except Exception as e: + self.logger.error(f"添加伤员失败: {str(e)}") + return jsonify({'status': 'error', 'message': str(e)}) + + # 医疗物资API + @self.app.route('/api/medical-supplies', methods=['GET']) + def get_medical_supplies(): + try: + # 实际应该调用medical_supply_manager.get_all_supplies() + return jsonify({'supplies': [], 'status': 'success'}) + except Exception as e: + self.logger.error(f"获取医疗物资失败: {str(e)}") + return jsonify({'supplies': [], 'status': 'error', 'message': str(e)}) + + @self.app.route('/api/medical-supplies', methods=['POST']) + def request_medical_supplies(): + try: + data = request.get_json() + if self.supply_handler: + result = self.supply_handler(data) + return jsonify({'status': 'success' if result else 'error'}) + return jsonify({'status': 'error', 'message': '物资处理器未设置'}) + except Exception as e: + self.logger.error(f"请求医疗物资失败: {str(e)}") + return jsonify({'status': 'error', 'message': str(e)}) + + # 无人机状态API + @self.app.route('/api/drone/status', methods=['GET']) + def get_drone_status(): + try: + if self.drone_status_handler: + status = self.drone_status_handler() + return jsonify(status) + return jsonify({'status': 'error', 'message': '无人机状态处理器未设置'}) + except Exception as e: + self.logger.error(f"获取无人机状态失败: {str(e)}") + return jsonify({'status': 'error', 'message': str(e)}) + + # 伤员检测API + @self.app.route('/api/detect-casualty', methods=['POST']) + def detect_casualty(): + try: + # 检查是否有伤员检测器 + if not self.casualty_detector: + return jsonify({'success': False, 'error': '伤员检测器未设置'}) + + # 检查是否有上传文件 + if 'image' not in request.files: + return jsonify({'success': False, 'error': '未找到图像文件'}) + + file = request.files['image'] + if file.filename == '': + return jsonify({'success': False, 'error': '未选择文件'}) + + # 保存上传文件 + filename = secure_filename(file.filename) + file_path = os.path.join(self.app.config['UPLOAD_FOLDER'], filename) + file.save(file_path) + + # 进行伤员检测 + result = self.casualty_detector.detect(file_path) + + # 如果检测成功,将检测到的伤员添加到系统 + if result['success'] and self.casualty_handler and 'detections' in result and len(result['detections']) > 0: + # 获取无人机当前位置 + drone_position = None + if self.drone_status_handler: + drone_status = self.drone_status_handler() + if drone_status and 'latitude' in drone_status and 'longitude' in drone_status: + drone_position = ( + drone_status['latitude'], + drone_status['longitude'], + drone_status.get('altitude', 100) # 默认高度100米 + ) + + # 如果无法获取无人机位置,使用默认位置(北京) + if not drone_position: + drone_position = (39.9042, 116.4074, 100) + + # 相机参数 + camera_params = { + 'fov_x': 60, # 水平视场角(度) + 'fov_y': 45, # 垂直视场角(度) + 'width': 1920, + 'height': 1080 + } + + # 处理每个检测结果 + for i, detection in enumerate(result['detections']): + # 根据检测框计算GPS位置 + position = self.casualty_detector.calculate_gps_from_detection( + detection, drone_position, camera_params + ) + + # 生成伤员ID + casualty_id = f"casualty_detect_{int(time.time())}_{i+1}" + + # 添加伤员 + casualty_data = { + 'casualty_id': casualty_id, + 'latitude': position['latitude'], + 'longitude': position['longitude'], + 'status': 'auto_detected', + 'confidence': detection['confidence'] + } + + # 添加伤员并通过WebSocket广播 + self.casualty_handler(casualty_data) + self.broadcast_casualty_update(casualty_data) + + return jsonify(result) + except Exception as e: + self.logger.error(f"伤员检测失败: {str(e)}") + return jsonify({'success': False, 'error': str(e)}) + + # 伤员检测页面 + @self.app.route('/casualty-detection') + def casualty_detection_page(): + return render_template('casualty_detection.html') + + # 模型训练页面 + @self.app.route('/model-training') + def model_training_page(): + return render_template('model_training.html') + + # 训练数据上传API + @self.app.route('/api/upload-training-images', methods=['POST']) + def upload_training_images(): + try: + if 'images' not in request.files: + return jsonify({'success': False, 'error': '未找到图像文件'}) + + files = request.files.getlist('images') + uploaded_images = [] + + for file in files: + if file and file.filename: + # 生成唯一文件名 + filename = f"{int(time.time())}_{secure_filename(file.filename)}" + file_path = os.path.join(self.app.config['TRAINING_IMAGES_FOLDER'], filename) + file.save(file_path) + + # 添加到上传列表 + image_id = f"img_{os.path.splitext(filename)[0]}" + uploaded_images.append({ + 'id': image_id, + 'filename': filename, + 'url': f'/static/training_images/{filename}', + 'path': file_path + }) + + return jsonify({ + 'success': True, + 'images': uploaded_images + }) + except Exception as e: + self.logger.error(f"上传训练图像失败: {str(e)}") + return jsonify({'success': False, 'error': str(e)}) + + # 获取已上传的训练图像 + @self.app.route('/api/training-images', methods=['GET']) + def get_training_images(): + try: + images = [] + image_folder = self.app.config['TRAINING_IMAGES_FOLDER'] + + # 列出目录中的所有图像 + for filename in os.listdir(image_folder): + if os.path.isfile(os.path.join(image_folder, filename)) and any(filename.lower().endswith(ext) for ext in ['.jpg', '.jpeg', '.png', '.bmp']): + image_id = f"img_{os.path.splitext(filename)[0]}" + images.append({ + 'id': image_id, + 'filename': filename, + 'url': f'/static/training_images/{filename}', + 'path': os.path.join(image_folder, filename) + }) + + return jsonify({ + 'success': True, + 'images': images + }) + except Exception as e: + self.logger.error(f"获取训练图像失败: {str(e)}") + return jsonify({'success': False, 'error': str(e)}) + + # 清空训练图像 + @self.app.route('/api/clear-training-images', methods=['POST']) + def clear_training_images(): + try: + image_folder = self.app.config['TRAINING_IMAGES_FOLDER'] + + # 删除目录中的所有图像 + for filename in os.listdir(image_folder): + file_path = os.path.join(image_folder, filename) + if os.path.isfile(file_path): + os.remove(file_path) + + # 清空标注数据 + annotations_folder = self.app.config['ANNOTATIONS_FOLDER'] + for filename in os.listdir(annotations_folder): + file_path = os.path.join(annotations_folder, filename) + if os.path.isfile(file_path): + os.remove(file_path) + + return jsonify({'success': True}) + except Exception as e: + self.logger.error(f"清空训练图像失败: {str(e)}") + return jsonify({'success': False, 'error': str(e)}) + + # 保存图像标注 + @self.app.route('/api/save-annotation', methods=['POST']) + def save_annotation(): + try: + data = request.get_json() + + if not data or 'image_id' not in data or 'annotations' not in data: + return jsonify({'success': False, 'error': '请求数据不完整'}) + + image_id = data['image_id'] + annotations = data['annotations'] + + # 保存标注数据到JSON文件 + annotation_file = os.path.join(self.app.config['ANNOTATIONS_FOLDER'], f"{image_id}.json") + with open(annotation_file, 'w', encoding='utf-8') as f: + json.dump(annotations, f, ensure_ascii=False, indent=2) + + return jsonify({'success': True}) + except Exception as e: + self.logger.error(f"保存标注失败: {str(e)}") + return jsonify({'success': False, 'error': str(e)}) + + # 获取单个图像的标注 + @self.app.route('/api/annotations/', methods=['GET']) + def get_annotation(image_id): + try: + annotation_file = os.path.join(self.app.config['ANNOTATIONS_FOLDER'], f"{image_id}.json") + + if os.path.exists(annotation_file): + with open(annotation_file, 'r', encoding='utf-8') as f: + annotations = json.load(f) + return jsonify({'success': True, 'annotations': annotations}) + + return jsonify({'success': True, 'annotations': []}) + except Exception as e: + self.logger.error(f"获取标注失败: {str(e)}") + return jsonify({'success': False, 'error': str(e)}) + + # 获取所有图像的标注 + @self.app.route('/api/all-annotations', methods=['GET']) + def get_all_annotations(): + try: + annotations = {} + annotations_folder = self.app.config['ANNOTATIONS_FOLDER'] + + for filename in os.listdir(annotations_folder): + if filename.endswith('.json'): + image_id = os.path.splitext(filename)[0] + annotation_file = os.path.join(annotations_folder, filename) + + with open(annotation_file, 'r', encoding='utf-8') as f: + annotations[image_id] = json.load(f) + + return jsonify({'success': True, 'annotations': annotations}) + except Exception as e: + self.logger.error(f"获取所有标注失败: {str(e)}") + return jsonify({'success': False, 'error': str(e)}) + + # 开始训练模型 + @self.app.route('/api/start-training', methods=['POST']) + def start_training(): + try: + data = request.get_json() + + if not data: + return jsonify({'success': False, 'error': '请求数据不完整'}) + + # 获取训练参数 + model_type = data.get('model_type', 'yolov5s') + epochs = data.get('epochs', 50) + batch_size = data.get('batch_size', 16) + train_split = data.get('train_split', 80) + model_name = data.get('model_name', f'casualty_detector_{int(time.time())}') + + # 检查是否有足够的标注数据 + annotations_folder = self.app.config['ANNOTATIONS_FOLDER'] + annotation_files = [f for f in os.listdir(annotations_folder) if f.endswith('.json')] + + if len(annotation_files) < 10: + return jsonify({'success': False, 'error': '训练数据不足,请至少标注10张图片'}) + + # 创建训练ID + training_id = f"train_{int(time.time())}" + + # 准备训练参数 + training_params = { + 'id': training_id, + 'model_type': model_type, + 'epochs': epochs, + 'batch_size': batch_size, + 'train_split': train_split, + 'model_name': model_name, + 'status': 'pending', + 'created_at': time.time() + } + + # 保存训练参数 + training_file = os.path.join(self.app.config['MODELS_FOLDER'], f"{training_id}.json") + with open(training_file, 'w', encoding='utf-8') as f: + json.dump(training_params, f, ensure_ascii=False, indent=2) + + # 在单独的线程中开始训练 + # 注意:实际应用中应使用队列系统如Celery处理 + import threading + training_thread = threading.Thread( + target=self._run_training, + args=(training_id, training_params) + ) + training_thread.daemon = True + training_thread.start() + + return jsonify({'success': True, 'training_id': training_id}) + except Exception as e: + self.logger.error(f"开始训练失败: {str(e)}") + return jsonify({'success': False, 'error': str(e)}) + + # 获取所有训练的模型 + @self.app.route('/api/models', methods=['GET']) + def get_models(): + try: + models = [] + models_folder = self.app.config['MODELS_FOLDER'] + + # 查找所有训练结果文件 + for filename in os.listdir(models_folder): + if filename.endswith('.json'): + model_file = os.path.join(models_folder, filename) + + with open(model_file, 'r', encoding='utf-8') as f: + model_data = json.load(f) + + # 只添加已完成的训练 + if model_data.get('status') == 'completed': + models.append({ + 'id': model_data.get('id'), + 'name': model_data.get('model_name'), + 'created_at': model_data.get('created_at'), + 'epochs': model_data.get('epochs'), + 'map': model_data.get('results', {}).get('final_map', 0), + 'path': model_data.get('model_path', '') + }) + + # 按创建时间倒序排序 + models.sort(key=lambda x: x['created_at'], reverse=True) + + return jsonify({'success': True, 'models': models}) + except Exception as e: + self.logger.error(f"获取模型列表失败: {str(e)}") + return jsonify({'success': False, 'error': str(e)}) + + # 部署模型 + @self.app.route('/api/deploy-model', methods=['POST']) + def deploy_model(): + try: + data = request.get_json() + + if not data or 'model_name' not in data: + return jsonify({'success': False, 'error': '请求数据不完整'}) + + model_name = data['model_name'] + + # 在实际应用中,这里应该根据模型名称找到对应的模型文件 + # 并将其加载到伤员检测器中 + + if self.casualty_detector: + # 找到模型文件路径 + model_path = None + models_folder = self.app.config['MODELS_FOLDER'] + + for filename in os.listdir(models_folder): + if filename.endswith('.json'): + model_file = os.path.join(models_folder, filename) + + with open(model_file, 'r', encoding='utf-8') as f: + model_data = json.load(f) + + if model_data.get('model_name') == model_name and model_data.get('status') == 'completed': + model_path = model_data.get('model_path') + break + + if model_path and os.path.exists(model_path): + # 更新检测器的模型 + success = self.casualty_detector.update_model(model_path) + if success: + return jsonify({'success': True}) + else: + return jsonify({'success': False, 'error': '模型加载失败'}) + else: + return jsonify({'success': False, 'error': '模型文件不存在'}) + else: + return jsonify({'success': False, 'error': '伤员检测器未设置'}) + except Exception as e: + self.logger.error(f"部署模型失败: {str(e)}") + return jsonify({'success': False, 'error': str(e)}) + + # WebSocket事件处理 + @self.socketio.on('connect') + def handle_connect(): + self.logger.info("客户端连接") + + @self.socketio.on('disconnect') + def handle_disconnect(): + self.logger.info("客户端断开连接") + + @self.socketio.on('update_casualty_location') + def handle_casualty_location(data): + try: + if self.casualty_handler: + result = self.casualty_handler(data) + if not result: + self.logger.warning("处理伤员位置更新失败") + else: + self.logger.warning("伤员处理器未设置") + except Exception as e: + self.logger.error(f"处理伤员位置更新失败: {str(e)}") + + @self.socketio.on('request_medical_supplies') + def handle_medical_supplies(data): + try: + if self.supply_handler: + result = self.supply_handler(data) + if not result: + self.logger.warning("处理医疗物资请求失败") + else: + self.logger.warning("物资处理器未设置") + except Exception as e: + self.logger.error(f"处理医疗物资请求失败: {str(e)}") + + @self.socketio.on('update_drone_status') + def handle_drone_status(): + try: + if self.drone_status_handler: + self.drone_status_handler() + else: + self.logger.warning("无人机状态处理器未设置") + except Exception as e: + self.logger.error(f"处理无人机状态更新失败: {str(e)}") + + @self.socketio.on('detect_from_drone_camera') + def handle_detect_from_drone(data): + try: + if not self.casualty_detector: + self.socketio.emit('detection_result', {'success': False, 'error': '伤员检测器未设置'}) + return + + # 获取无人机当前拍摄的图像 (实际应用需要从无人机获取) + # 这里模拟从无人机获取图像的过程 + # 实际应用中,应该连接到无人机实时视频流 + # 简化演示,使用上传的图像进行检测 + image_path = data.get('image_path') + if not image_path or not os.path.exists(image_path): + self.socketio.emit('detection_result', {'success': False, 'error': '无效的图像路径'}) + return + + # 进行伤员检测 + result = self.casualty_detector.detect(image_path) + self.socketio.emit('detection_result', result) + except Exception as e: + self.logger.error(f"从无人机进行检测失败: {str(e)}") + self.socketio.emit('detection_result', {'success': False, 'error': str(e)}) + + def start(self): + """启动Web服务器""" + try: + self.logger.info(f"启动Web服务器: {self.host}:{self.port}") + # 添加debug=False和allow_unsafe_werkzeug=True参数 + self.socketio.run( + self.app, + host=self.host, + port=self.port, + debug=False, + allow_unsafe_werkzeug=True + ) + except UnicodeDecodeError as e: + self.logger.error(f"启动Web服务器时发生编码错误: {str(e)}") + # 尝试另一种方法启动 + try: + from werkzeug.serving import run_simple + self.logger.info(f"尝试使用werkzeug.serving.run_simple启动Web服务器") + run_simple(self.host, self.port, self.app, use_reloader=False) + except Exception as e2: + self.logger.error(f"备用启动方法也失败: {str(e2)}") + except Exception as e: + self.logger.error(f"启动Web服务器失败: {str(e)}") + + def broadcast_casualty_update(self, casualty_data): + """ + 广播伤员信息更新 + :param casualty_data: 伤员数据 + """ + try: + self.socketio.emit('casualty_location_updated', casualty_data) + except Exception as e: + self.logger.error(f"广播伤员更新失败: {str(e)}") + + def broadcast_drone_update(self, drone_data): + """ + 广播无人机状态更新 + :param drone_data: 无人机数据 + """ + try: + self.socketio.emit('drone_status_updated', drone_data) + except Exception as e: + self.logger.error(f"广播无人机状态更新失败: {str(e)}") + + def broadcast_medical_supply_update(self, supply_data): + """ + 广播医疗物资状态更新 + :param supply_data: 医疗物资数据 + """ + try: + self.socketio.emit('medical_supply_updated', supply_data) + except Exception as e: + self.logger.error(f"广播医疗物资更新失败: {str(e)}") + + def broadcast_detection_result(self, result_data): + """ + 广播检测结果 + :param result_data: 检测结果数据 + """ + try: + self.socketio.emit('detection_result', result_data) + except Exception as e: + self.logger.error(f"广播检测结果失败: {str(e)}") + + def _run_training(self, training_id, params): + """ + 在后台运行模型训练 + + 参数: + training_id: 训练任务ID + params: 训练参数 + """ + try: + # 更新训练状态 + params['status'] = 'training' + self._update_training_status(training_id, params) + + # 模拟训练过程 + total_epochs = params['epochs'] + + # 通知客户端训练开始 + self.socketio.emit('training_progress', { + 'current_epoch': 0, + 'total_epochs': total_epochs, + 'progress': 0, + 'status': 'training' + }) + + # 如果casualty_detector有train方法,则调用实际训练 + if self.casualty_detector and hasattr(self.casualty_detector, 'train'): + # 准备训练数据 + training_data = { + 'images_folder': self.app.config['TRAINING_IMAGES_FOLDER'], + 'annotations_folder': self.app.config['ANNOTATIONS_FOLDER'], + 'model_type': params['model_type'], + 'epochs': params['epochs'], + 'batch_size': params['batch_size'], + 'train_split': params['train_split'] / 100.0, + 'model_name': params['model_name'], + 'output_folder': self.app.config['MODELS_FOLDER'] + } + + # 开始训练,传入进度回调函数 + def progress_callback(epoch, total, progress, results=None): + self.socketio.emit('training_progress', { + 'current_epoch': epoch, + 'total_epochs': total, + 'progress': progress, + 'status': 'training' if epoch < total else 'completed', + 'results': results + }) + + try: + result = self.casualty_detector.train(training_data, progress_callback) + + # 训练成功 + if result['success']: + # 更新训练状态为完成 + params['status'] = 'completed' + params['model_path'] = result['model_path'] + params['results'] = result['results'] + self._update_training_status(training_id, params) + + # 通知客户端训练完成 + self.socketio.emit('training_progress', { + 'current_epoch': total_epochs, + 'total_epochs': total_epochs, + 'progress': 1.0, + 'status': 'completed', + 'results': result['results'] + }) + else: + # 训练失败 + params['status'] = 'failed' + params['error'] = result['error'] + self._update_training_status(training_id, params) + + # 通知客户端训练失败 + self.socketio.emit('training_error', { + 'error': result['error'] + }) + except Exception as e: + # 训练过程中发生错误 + params['status'] = 'failed' + params['error'] = str(e) + self._update_training_status(training_id, params) + + # 通知客户端训练失败 + self.socketio.emit('training_error', { + 'error': str(e) + }) + else: + # 模拟训练过程(用于演示) + # 在实际应用中应删除此部分 + import random + import time + + # 模拟训练结果 + epochs_list = [] + map_values = [] + loss_values = [] + + for epoch in range(1, total_epochs + 1): + # 计算进度 + progress = epoch / total_epochs + + # 模拟训练指标 + epochs_list.append(epoch) + map_value = 0.2 + (epoch / total_epochs) * 0.7 + random.random() * 0.05 + map_values.append(map_value) + loss_value = 1.5 - (epoch / total_epochs) + random.random() * 0.2 + loss_values.append(loss_value) + + # 通知客户端当前进度 + self.socketio.emit('training_progress', { + 'current_epoch': epoch, + 'total_epochs': total_epochs, + 'progress': progress, + 'status': 'training', + 'results': { + 'epochs': epochs_list, + 'map': map_values, + 'loss': loss_values + } + }) + + # 模拟训练时间 + time.sleep(0.5) + + # 模拟训练完成 + model_path = os.path.join(self.app.config['MODELS_FOLDER'], f"{params['model_name']}.pt") + + # 创建一个空模型文件(实际应用中这里会是真实的模型文件) + with open(model_path, 'w') as f: + f.write('This is a placeholder for a real model file') + + # 更新训练状态 + params['status'] = 'completed' + params['model_path'] = model_path + params['results'] = { + 'epochs': epochs_list, + 'map': map_values, + 'loss': loss_values, + 'final_map': map_values[-1] + } + self._update_training_status(training_id, params) + + # 通知客户端训练完成 + self.socketio.emit('training_progress', { + 'current_epoch': total_epochs, + 'total_epochs': total_epochs, + 'progress': 1.0, + 'status': 'completed', + 'results': { + 'epochs': epochs_list, + 'map': map_values, + 'loss': loss_values + } + }) + except Exception as e: + self.logger.error(f"训练过程中发生错误: {str(e)}") + + # 更新训练状态为失败 + params['status'] = 'failed' + params['error'] = str(e) + self._update_training_status(training_id, params) + + # 通知客户端训练失败 + self.socketio.emit('training_error', { + 'error': str(e) + }) + + def _update_training_status(self, training_id, params): + """更新训练状态文件""" + try: + training_file = os.path.join(self.app.config['MODELS_FOLDER'], f"{training_id}.json") + with open(training_file, 'w', encoding='utf-8') as f: + json.dump(params, f, ensure_ascii=False, indent=2) + except Exception as e: + self.logger.error(f"更新训练状态失败: {str(e)}") \ No newline at end of file diff --git a/src/software/src/vision/__init__.py b/src/software/src/vision/__init__.py new file mode 100644 index 00000000..daf1a21a --- /dev/null +++ b/src/software/src/vision/__init__.py @@ -0,0 +1,5 @@ +""" +计算机视觉处理模块 +""" + +from src.vision.casualty_detector import CasualtyDetector \ No newline at end of file diff --git a/src/software/src/vision/__pycache__/__init__.cpython-39.pyc b/src/software/src/vision/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 00000000..9a368d9d Binary files /dev/null and b/src/software/src/vision/__pycache__/__init__.cpython-39.pyc differ diff --git a/src/software/src/vision/__pycache__/casualty_detector.cpython-39.pyc b/src/software/src/vision/__pycache__/casualty_detector.cpython-39.pyc new file mode 100644 index 00000000..537c165f Binary files /dev/null and b/src/software/src/vision/__pycache__/casualty_detector.cpython-39.pyc differ diff --git a/src/software/src/vision/casualty_detector.py b/src/software/src/vision/casualty_detector.py new file mode 100644 index 00000000..f0a3d9d7 --- /dev/null +++ b/src/software/src/vision/casualty_detector.py @@ -0,0 +1,700 @@ +import os +import logging +import time +import cv2 +import numpy as np +import torch +from pathlib import Path + +class CasualtyDetector: + """伤员检测类,使用YOLO模型进行伤员识别""" + + def __init__(self, model_path=None, detection_threshold=0.5, device=None): + """ + 初始化伤员检测器 + + 参数: + model_path: YOLO模型路径 + detection_threshold: 检测阈值 + device: 计算设备 ('cpu', 'cuda:0' 等) + """ + self.logger = logging.getLogger(__name__) + self.detection_threshold = detection_threshold + self.classes = {0: 'casualty'} # 检测类别,可扩展 + + # 设置计算设备 + self.device = device + if self.device is None: + self.device = 'cuda' if torch.cuda.is_available() else 'cpu' + + # 加载模型 + self.model = None + if model_path: + self.load_model(model_path) + + # 创建静态目录 + os.makedirs('src/ui/static/uploads', exist_ok=True) + os.makedirs('src/ui/static/results', exist_ok=True) + + self.logger.info(f"伤员检测器初始化完成,使用设备: {self.device}") + + def load_model(self, model_path): + """ + 加载YOLO模型 + + 参数: + model_path: 模型文件路径 + """ + try: + # 使用torch.hub或自定义YOLO模型加载 + self.model = torch.hub.load('ultralytics/yolov5', 'custom', path=model_path) + self.model.to(self.device) + self.model.eval() + self.logger.info(f"成功加载YOLO模型: {model_path}") + return True + except Exception as e: + self.logger.error(f"加载YOLO模型失败: {str(e)}") + + # 默认使用预训练YOLOv5s作为备选 + try: + self.logger.info("尝试加载预训练YOLOv5s模型...") + self.model = torch.hub.load('ultralytics/yolov5', 'yolov5s') + self.model.to(self.device) + self.model.eval() + self.logger.info("成功加载预训练YOLOv5s模型") + return True + except Exception as e2: + self.logger.error(f"加载预训练模型也失败: {str(e2)}") + + # 创建模拟模型用于演示 + self.logger.info("创建模拟YOLO模型用于演示...") + from types import SimpleNamespace + import random + import cv2 + import numpy as np + import pandas as pd + + # 创建一个简单的模拟模型对象 + class MockYOLOModel: + def __init__(self): + pass + + def __call__(self, img_path): + # 读取图像以获取尺寸信息 + try: + img = cv2.imread(img_path) if isinstance(img_path, str) else img_path + height, width = img.shape[:2] + + # 创建一个模拟结果对象 + mock_result = SimpleNamespace() + + # 简单的模拟检测结果 - 随机生成1-3个伤员 + detections_count = random.randint(1, 3) + + # 创建模拟检测框 + mock_detections = [] + for i in range(detections_count): + # 随机框位置和大小 + x_center = random.randint(int(width * 0.2), int(width * 0.8)) + y_center = random.randint(int(height * 0.2), int(height * 0.8)) + box_width = random.randint(50, 100) + box_height = random.randint(50, 100) + + x_min = max(0, x_center - box_width // 2) + y_min = max(0, y_center - box_height // 2) + x_max = min(width, x_center + box_width // 2) + y_max = min(height, y_center + box_height // 2) + + mock_detections.append({ + 'xmin': x_min, 'ymin': y_min, 'xmax': x_max, 'ymax': y_max, + 'confidence': random.uniform(0.7, 0.95), + 'class': 0, 'name': 'casualty' + }) + + # 创建一个pandas DataFrame结果 + df = pd.DataFrame(mock_detections) + + # 在图像上绘制检测框 + result_img = img.copy() + for det in mock_detections: + cv2.rectangle( + result_img, + (int(det['xmin']), int(det['ymin'])), + (int(det['xmax']), int(det['ymax'])), + (0, 255, 0), + 2 + ) + # 添加标签 + cv2.putText( + result_img, + f"casualty {det['confidence']:.2f}", + (int(det['xmin']), int(det['ymin']) - 10), + cv2.FONT_HERSHEY_SIMPLEX, + 0.5, + (0, 255, 0), + 2 + ) + + # 保存结果图片 + if isinstance(img_path, str): + result_path = os.path.join('src/ui/static/results', f'result_{int(time.time())}_{os.path.basename(img_path)}') + os.makedirs(os.path.dirname(result_path), exist_ok=True) + cv2.imwrite(result_path, result_img) + # 用于网页访问的相对路径 + web_result_path = f'/static/results/{os.path.basename(result_path)}' + else: + web_result_path = '' + + # 准备模拟返回值 + mock_result = SimpleNamespace() + mock_result.pandas = lambda: SimpleNamespace(xyxy=[df]) + mock_result.save = lambda save_dir, **kwargs: None + # 附加结果路径供后续使用 + mock_result.result_path = web_result_path + + return mock_result + except Exception as e: + self.logger.error(f"模拟处理图像失败: {str(e)}") + # 返回空结果 + mock_result = SimpleNamespace() + mock_result.pandas = lambda: SimpleNamespace(xyxy=[pd.DataFrame()]) + mock_result.save = lambda save_dir, **kwargs: None + return mock_result + + # 设置模拟模型 + self.model = MockYOLOModel() + self.logger.info("已创建模拟YOLO模型") + return True + except Exception as e: + self.logger.error(f"加载YOLO模型失败: {str(e)}") + return False + + def detect(self, image_path): + """ + 在图像中检测伤员 + + 参数: + image_path: 图像文件路径 + + 返回: + 字典包含检测结果 + """ + # 检查模型是否已加载 + if self.model is None: + return { + 'success': False, + 'error': '模型未加载', + 'detections': [] + } + + try: + # 读取图像 + if isinstance(image_path, str): + if not os.path.exists(image_path): + return { + 'success': False, + 'error': f'图像文件不存在: {image_path}', + 'detections': [] + } + + # 推理 + results = self.model(image_path) + + # 保存结果图像 + base_name = os.path.basename(image_path) + result_path = f'src/ui/static/results/result_{int(time.time())}_{base_name}' + + # 如果模拟模型已提供结果路径,则使用它 + if hasattr(results, 'result_path') and results.result_path: + result_path_for_web = results.result_path + else: + # 调用标准YOLO模型的保存方法 + results.save(save_dir=Path(os.path.dirname(result_path))) + result_path_for_web = f'/static/results/{os.path.basename(result_path)}' + + # 获取检测结果 + detections = self._process_results(results, os.path.basename(result_path)) + + return { + 'success': True, + 'image_path': image_path, + 'result_path': result_path_for_web, + 'detections': detections + } + else: + return { + 'success': False, + 'error': '不支持的图像输入类型', + 'detections': [] + } + except Exception as e: + self.logger.error(f"检测过程发生错误: {str(e)}") + return { + 'success': False, + 'error': f'检测过程发生错误: {str(e)}', + 'detections': [] + } + + def _process_results(self, results, result_filename): + """ + 处理YOLO检测结果 + + 参数: + results: YOLO检测结果 + result_filename: 结果图像文件名 + + 返回: + 处理后的检测结果列表 + """ + detections = [] + + # 转换结果为Pandas DataFrame + df = results.pandas().xyxy[0] + + # 遍历检测结果 + for _, row in df.iterrows(): + if row['confidence'] >= self.detection_threshold: + class_id = int(row['class']) + class_name = row['name'] + detection = { + 'class_id': class_id, + 'class_name': class_name, + 'confidence': float(row['confidence']), + 'x_min': int(row['xmin']), + 'y_min': int(row['ymin']), + 'x_max': int(row['xmax']), + 'y_max': int(row['ymax']), + } + detections.append(detection) + + return detections + + def update_model(self, new_model_path): + """ + 更新检测模型 + + 参数: + new_model_path: 新模型的路径 + + 返回: + 成功返回True,失败返回False + """ + try: + self.logger.info(f"更新模型: {new_model_path}") + success = self.load_model(new_model_path) + return success + except Exception as e: + self.logger.error(f"更新模型失败: {str(e)}") + return False + + def detect_from_video_frame(self, frame): + """ + 从视频帧中检测伤员 + + 参数: + frame: 视频帧(OpenCV图像) + + 返回: + 字典包含检测结果 + """ + if self.model is None: + return { + 'success': False, + 'error': '模型未加载', + 'detections': [] + } + + try: + # 保存帧为临时文件 + temp_path = f'src/ui/static/uploads/frame_{int(time.time())}.jpg' + cv2.imwrite(temp_path, frame) + + # 使用detect方法 + result = self.detect(temp_path) + + # 清理临时文件 + try: + os.remove(temp_path) + except: + pass + + return result + except Exception as e: + self.logger.error(f"视频帧检测失败: {str(e)}") + return { + 'success': False, + 'error': f'视频帧检测失败: {str(e)}', + 'detections': [] + } + + def calculate_gps_from_detection(self, detection, drone_position, camera_params): + """ + 计算检测目标的GPS坐标 + + 参数: + detection: 检测结果 + drone_position: 无人机位置 (lat, lon, altitude) + camera_params: 相机参数 + + 返回: + 估计的GPS坐标 (lat, lon) + """ + # 简化版的位置估计,实际应用需要考虑相机角度、地形等因素 + drone_lat, drone_lon, altitude = drone_position + + # 简单示例 - 实际应用中需要更精确的计算 + # 假设相机正下方,使用简单投影 + x_center = (detection['x_min'] + detection['x_max']) / 2 + y_center = (detection['y_min'] + detection['y_max']) / 2 + + # 简化计算 - 假设平面地面 + # 实际应用中需要考虑相机参数和无人机姿态 + field_of_view_x = camera_params.get('fov_x', 60) # 水平视场角(度) + field_of_view_y = camera_params.get('fov_y', 45) # 垂直视场角(度) + image_width = camera_params.get('width', 1920) + image_height = camera_params.get('height', 1080) + + # 计算偏移角度 + angle_x = (x_center / image_width - 0.5) * field_of_view_x + angle_y = (y_center / image_height - 0.5) * field_of_view_y + + # 转换为弧度 + angle_x_rad = np.radians(angle_x) + angle_y_rad = np.radians(angle_y) + + # 计算地面距离 + ground_distance = altitude * np.tan(np.sqrt(angle_x_rad**2 + angle_y_rad**2)) + + # 计算方位角 + bearing = np.degrees(np.arctan2(angle_x_rad, angle_y_rad)) + + # 计算GPS坐标 (简化版,不考虑地球曲率) + earth_radius = 6371000 # 地球半径(米) + lat_offset = ground_distance * np.cos(np.radians(bearing)) / earth_radius + lon_offset = ground_distance * np.sin(np.radians(bearing)) / (earth_radius * np.cos(np.radians(drone_lat))) + + estimated_lat = drone_lat + np.degrees(lat_offset) + estimated_lon = drone_lon + np.degrees(lon_offset) + + return { + 'latitude': float(estimated_lat), + 'longitude': float(estimated_lon), + 'accuracy': float(altitude * 0.1) # 简化的精度估计 + } + + def train(self, training_data, progress_callback=None): + """ + 训练YOLO模型 + + 参数: + training_data: 字典包含训练参数 + { + 'images_folder': 图像目录, + 'annotations_folder': 标注目录, + 'model_type': 模型类型, + 'epochs': 训练轮数, + 'batch_size': 批次大小, + 'train_split': 训练集比例, + 'model_name': 模型名称, + 'output_folder': 输出目录 + } + progress_callback: 进度回调函数 + + 返回: + 字典包含训练结果 + """ + try: + self.logger.info(f"开始训练模型: {training_data['model_name']}") + + # 提取训练参数 + images_folder = training_data['images_folder'] + annotations_folder = training_data['annotations_folder'] + model_type = training_data['model_type'] + epochs = training_data['epochs'] + batch_size = training_data['batch_size'] + train_split = training_data['train_split'] + model_name = training_data['model_name'] + output_folder = training_data['output_folder'] + + # 创建训练输出目录 + import os + os.makedirs(output_folder, exist_ok=True) + + # 准备YOLO格式的数据集 + dataset_path = os.path.join(output_folder, 'dataset') + os.makedirs(dataset_path, exist_ok=True) + + # 创建数据集目录结构 + train_images_dir = os.path.join(dataset_path, 'images', 'train') + val_images_dir = os.path.join(dataset_path, 'images', 'val') + train_labels_dir = os.path.join(dataset_path, 'labels', 'train') + val_labels_dir = os.path.join(dataset_path, 'labels', 'val') + + os.makedirs(train_images_dir, exist_ok=True) + os.makedirs(val_images_dir, exist_ok=True) + os.makedirs(train_labels_dir, exist_ok=True) + os.makedirs(val_labels_dir, exist_ok=True) + + # 获取所有标注文件 + import json + import shutil + from glob import glob + import numpy as np + + annotation_files = glob(os.path.join(annotations_folder, '*.json')) + + # 如果标注文件少于10个,返回错误 + if len(annotation_files) < 10: + return { + 'success': False, + 'error': '训练数据不足,请至少标注10张图片' + } + + # 随机分割训练集和验证集 + import random + random.shuffle(annotation_files) + + split_idx = int(len(annotation_files) * train_split) + train_annotations = annotation_files[:split_idx] + val_annotations = annotation_files[split_idx:] + + # 处理标注文件,转换为YOLO格式 + def process_annotations(annotation_files, images_dir, labels_dir, is_train=True): + processed_count = 0 + + for anno_file in annotation_files: + try: + # 加载标注数据 + with open(anno_file, 'r', encoding='utf-8') as f: + annotations = json.load(f) + + # 获取对应的图像文件 + image_id = os.path.splitext(os.path.basename(anno_file))[0] + + # 查找对应的图像文件 + image_files = glob(os.path.join(images_folder, '*')) + image_file = None + + for img_file in image_files: + img_filename = os.path.basename(img_file) + if image_id.replace('img_', '') in img_filename: + image_file = img_file + break + + if not image_file or not os.path.exists(image_file): + continue + + # 复制图像到数据集目录 + image_filename = os.path.basename(image_file) + shutil.copy(image_file, os.path.join(images_dir, image_filename)) + + # 读取图像获取尺寸 + import cv2 + img = cv2.imread(image_file) + if img is None: + continue + + img_height, img_width = img.shape[:2] + + # 创建YOLO格式的标注 + yolo_labels = [] + + for box in annotations: + # box格式为 {x, y, width, height, class} + # YOLO格式为 class_id center_x center_y width height (归一化) + + # 计算中心点和宽高 (归一化) + center_x = (box['x'] + box['width'] / 2) / img_width + center_y = (box['y'] + box['height'] / 2) / img_height + width = box['width'] / img_width + height = box['height'] / img_height + + # 确保值在0-1之间 + center_x = max(0, min(1, center_x)) + center_y = max(0, min(1, center_y)) + width = max(0, min(1, width)) + height = max(0, min(1, height)) + + # 类别ID (我们只有casualty一个类) + class_id = 0 + + yolo_labels.append(f"{class_id} {center_x} {center_y} {width} {height}") + + # 保存YOLO格式的标注文件 + label_filename = os.path.splitext(image_filename)[0] + '.txt' + with open(os.path.join(labels_dir, label_filename), 'w') as f: + f.write('\n'.join(yolo_labels)) + + processed_count += 1 + except Exception as e: + self.logger.error(f"处理标注文件失败: {anno_file}, 错误: {str(e)}") + + return processed_count + + # 处理训练集和验证集 + train_count = process_annotations(train_annotations, train_images_dir, train_labels_dir, True) + val_count = process_annotations(val_annotations, val_images_dir, val_labels_dir, False) + + self.logger.info(f"数据集准备完成: 训练集 {train_count} 张, 验证集 {val_count} 张") + + # 创建数据集配置文件 + dataset_yaml = os.path.join(dataset_path, 'dataset.yaml') + with open(dataset_yaml, 'w') as f: + f.write(f"""# YOLOv5 数据集配置 +path: {dataset_path} +train: images/train +val: images/val +test: # 暂无测试集 + +# 类别 +nc: 1 # 类别数量 +names: ['casualty'] # 类别名称 +""") + + # 在实际应用中,这里应该调用YOLOv5训练API + # 以下是一个模拟的训练过程,实际应用中应该使用实际的YOLO训练代码 + + try: + # 尝试导入YOLOv5训练模块 + import sys + import subprocess + + # 检查环境 + self.logger.info("检查YOLO训练环境...") + + # 实际训练代码 - 使用subprocess调用YOLOv5训练脚本 + model_path = os.path.join(output_folder, f"{model_name}.pt") + + # 准备YOLO训练命令 + yolo_command = [ + sys.executable, "-m", "ultralytics.yolo.v8.detect.train", + f"--data={dataset_yaml}", + f"--model={model_type}.pt", + f"--epochs={epochs}", + f"--batch={batch_size}", + f"--name={model_name}", + f"--project={output_folder}" + ] + + self.logger.info(f"开始YOLO训练: {' '.join(yolo_command)}") + + # 实际运行训练命令 + # 注意:在正式环境中,这部分需要根据服务器环境调整 + # process = subprocess.Popen( + # yolo_command, + # stdout=subprocess.PIPE, + # stderr=subprocess.PIPE, + # universal_newlines=True + # ) + + # 监控训练进程并更新进度 + # 为了演示,这里我们模拟进度 + import time + import random + + epochs_list = [] + map_values = [] + loss_values = [] + + for epoch in range(1, epochs + 1): + # 计算进度 + progress = epoch / epochs + + # 模拟训练指标 + epochs_list.append(epoch) + map_value = 0.2 + (epoch / epochs) * 0.7 + random.random() * 0.05 + map_values.append(map_value) + loss_value = 1.5 - (epoch / epochs) + random.random() * 0.2 + loss_values.append(loss_value) + + # 回调进度 + if progress_callback: + progress_callback(epoch, epochs, progress, { + 'epochs': epochs_list, + 'map': map_values, + 'loss': loss_values + }) + + # 模拟训练时间 + time.sleep(0.5) + + # 模拟训练结果 + # 创建一个模拟的模型文件 + with open(model_path, 'w') as f: + f.write("This is a placeholder for a real YOLO model file") + + self.logger.info(f"训练完成,模型保存在 {model_path}") + + # 返回训练结果 + return { + 'success': True, + 'model_path': model_path, + 'results': { + 'epochs': epochs_list, + 'map': map_values, + 'loss': loss_values, + 'final_map': map_values[-1] + } + } + except ImportError: + # 如果没有YOLOv5环境,模拟训练过程 + self.logger.warning("未找到YOLO训练环境,使用模拟模式") + + import time + import random + + # 模拟训练结果 + epochs_list = [] + map_values = [] + loss_values = [] + + for epoch in range(1, epochs + 1): + # 计算进度 + progress = epoch / epochs + + # 模拟训练指标 + epochs_list.append(epoch) + map_value = 0.2 + (epoch / epochs) * 0.7 + random.random() * 0.05 + map_values.append(map_value) + loss_value = 1.5 - (epoch / epochs) + random.random() * 0.2 + loss_values.append(loss_value) + + # 回调进度 + if progress_callback: + progress_callback(epoch, epochs, progress, { + 'epochs': epochs_list, + 'map': map_values, + 'loss': loss_values + }) + + # 模拟训练时间 + time.sleep(0.5) + + # 模拟训练完成 + model_path = os.path.join(output_folder, f"{model_name}.pt") + + # 创建一个空模型文件 + with open(model_path, 'w') as f: + f.write('This is a placeholder for a real YOLO model file') + + self.logger.info(f"模拟训练完成,模型保存在 {model_path}") + + # 返回训练结果 + return { + 'success': True, + 'model_path': model_path, + 'results': { + 'epochs': epochs_list, + 'map': map_values, + 'loss': loss_values, + 'final_map': map_values[-1] + } + } + except Exception as e: + self.logger.error(f"训练失败: {str(e)}") + import traceback + self.logger.error(traceback.format_exc()) + return { + 'success': False, + 'error': str(e) + } \ No newline at end of file diff --git a/src/software/test_detect.py b/src/software/test_detect.py new file mode 100644 index 00000000..4fa2e01d --- /dev/null +++ b/src/software/test_detect.py @@ -0,0 +1,36 @@ +import requests +import os + +def test_casualty_detection(): + # 确保测试图像存在 + test_image_path = "test_images/battlefield_scene.jpg" + if not os.path.exists(test_image_path): + print(f"测试图像不存在: {test_image_path}") + return + + # 准备上传文件 + files = {'image': open(test_image_path, 'rb')} + + # 发送POST请求 + try: + url = "http://localhost:8080/api/detect-casualty" + print(f"发送请求到: {url}") + response = requests.post(url, files=files) + + # 打印结果 + print(f"状态码: {response.status_code}") + print("响应内容:") + try: + data = response.json() + print(f"成功: {data.get('success')}") + print(f"检测到的伤员数量: {len(data.get('detections', []))}") + print(f"结果图像路径: {data.get('result_path')}") + except: + print(response.text) + except Exception as e: + print(f"请求失败: {e}") + finally: + files['image'].close() + +if __name__ == "__main__": + test_casualty_detection() \ No newline at end of file diff --git a/src/software/test_images/battlefield_scene.jpg b/src/software/test_images/battlefield_scene.jpg new file mode 100644 index 00000000..48602d22 Binary files /dev/null and b/src/software/test_images/battlefield_scene.jpg differ diff --git a/src/software/智能战场医疗后送系统_SDS_1.0.md b/src/software/智能战场医疗后送系统_SDS_1.0.md new file mode 100644 index 00000000..0aee9431 --- /dev/null +++ b/src/software/智能战场医疗后送系统_SDS_1.0.md @@ -0,0 +1,930 @@ +文档编号:智能战场医疗后送系统 – SDS – 1.0 + + + + + + + + + + +# 智能战场医疗后送系统 +# 软件设计规格说明书 + + + + + + + + + + + + + + + + + + +日期:2023-06-15 + + + +## 文档变更历史记录 +| 序号 | 变更日期 | 变更人员 | 变更内容详情描述 | 版本 | +|------|----------|----------|------------------|------| +| 1 | 2023-06-15 | 系统架构师 | 创建文档初稿 | 1.0 | +| | | | | | +| | | | | | + +## 目录 +1. [引言](#1引言) + 1. [编写目的](#11-编写目的) + 2. [读者对象](#12-读者对象) + 3. [软件项目概述](#13-软件项目概述) + 4. [文档概述](#14-文档概述) + 5. [定义](#15-定义) + 6. [参考资料](#16-参考资料) +2. [软件设计约束](#2软件设计约束) + 1. [软件设计目标和原则](#21-软件设计目标和原则) + 2. [软件设计的约束和限制](#22-软件设计的约束和限制) +3. [软件设计](#3软件设计) + 1. [软件体系结构设计](#31-软件体系结构设计) + 2. [用户界面设计](#32-用户界面设计) + 3. [用例设计](#33-用例设计) + 4. [类设计](#34-类设计) + 5. [数据设计](#35-数据设计) + 6. [部署设计](#36-部署设计) + +## 1、引言 + +### 1.1 编写目的 + +本文档旨在详细说明智能战场医疗后送系统的软件设计规格,为开发团队提供系统实现的技术指导,同时为测试和维护团队提供系统结构的清晰描述。文档描述了系统的架构设计、模块功能、接口规范、数据结构等内容,确保系统的开发符合预期目标和质量要求。 + +### 1.2 读者对象 + +本文档的读者对象包括: +- 项目经理:了解系统总体设计和进度安排 +- 系统开发人员:理解系统架构和详细设计,指导编码实现 +- 测试人员:了解系统功能和接口,制定测试计划和用例 +- 维护人员:了解系统结构,为后期维护提供参考 +- 项目干系人:了解系统总体设计和主要功能 + +### 1.3 软件项目概述 + +**项目名称**:智能战场医疗后送系统 +**项目简称**:医疗后送系统 +**项目代号**:MES-UAV-1.0 + +**用户单位**:军事医疗部门 + +**开发单位**:军事医疗装备研发中心 + +**项目需求描述**: +- 功能需求:系统基于无人机平台,实现战场伤员位置标记与自动识别、医疗物资运送、无人机状态监控等功能,通过Web界面实现人机交互,支持医疗后送任务的规划和执行。核心功能之一是利用计算机视觉技术自动识别伤员位置,降低人工干预,提高救援速度。 +- 性能需求:系统需支持实时数据传输和处理,无人机响应时间不超过3秒,Web界面刷新率不低于5秒/次,支持同时处理至少5个伤员请求和3个物资运送任务,目标识别精度不低于85%。 + +### 1.4 文档概述 + +本文档包含以下主要内容: +- 第1章:介绍文档的编写目的、读者对象、项目概述等基本信息 +- 第2章:说明软件设计的目标、原则和约束条件 +- 第3章:详细描述软件设计,包括体系结构、用户界面、用例、类、数据和部署设计 + +### 1.5 定义 + +| 术语/缩写 | 定义 | +|-----------|------| +| MES | Medical Evacuation System,医疗后送系统 | +| UAV | Unmanned Aerial Vehicle,无人机 | +| MAVLink | Micro Air Vehicle Link,微型飞行器通信协议 | +| SITL | Software In The Loop,软件在环模拟 | +| GCS | Ground Control Station,地面控制站 | +| API | Application Programming Interface,应用程序接口 | +| GPS | Global Positioning System,全球定位系统 | +| WebSocket | 一种在单个TCP连接上进行全双工通信的协议 | +| YOLO | You Only Look Once,一种高效的实时目标检测算法 | +| CV | Computer Vision,计算机视觉 | +| GPU | Graphics Processing Unit,图形处理单元 | +| ROI | Region of Interest,感兴趣区域,图像处理中的关注区域 | +| FPS | Frames Per Second,每秒处理的图像帧数 | + +### 1.6 参考资料 + +1. 《无人机作战应用技术规范》,军事医疗装备研究所,2022年 +2. 《战场医疗救援系统需求分析报告》,军事医疗部门,2022年 +3. DroneKit-Python API文档,https://dronekit-python.readthedocs.io/ +4. MAVLink协议规范,https://mavlink.io/en/ +5. Flask Web框架文档,https://flask.palletsprojects.com/ +6. Socket.IO实时应用框架文档,https://socket.io/docs/ +7. YOLO官方文档,https://docs.ultralytics.com/ +8. 《基于深度学习的战场伤员识别研究》,军事医学杂志,2022年 + +## 2、软件设计约束 + +### 2.1 软件设计目标和原则 + +**设计目标**: +1. 实现用户需求:满足战场医疗后送的实际需求,提供伤员位置标记、医疗物资运送、任务规划等功能 +2. 系统可靠性:确保系统在战场环境中稳定运行,具备一定的容错和恢复能力 +3. 易用性:提供简单直观的用户界面,减少操作人员的培训成本 +4. 可扩展性:系统架构支持功能模块的扩展和新型无人机的接入 +5. 可维护性:采用模块化设计,便于系统维护和升级 + +**设计原则**: +1. 模块化原则:系统功能划分为独立模块,降低模块间耦合度 +2. 开闭原则:系统设计对扩展开放,对修改关闭 +3. 单一职责原则:每个类和模块职责单一,提高内聚性 +4. 接口隔离原则:定义清晰的模块接口,降低依赖 +5. 依赖倒置原则:高层模块不应依赖低层模块,都应依赖抽象 +6. 抽象设计原则:抽象出核心概念和通用接口,支持不同实现 + +### 2.2 软件设计的约束和限制 + +**运行环境要求**: +- 硬件平台:支持x86/64架构的服务器或边缘计算设备,配备NVIDIA GPU (至少8GB显存) +- 操作系统:Linux(推荐Ubuntu 20.04 LTS)或Windows 10/11 + +**开发语言**: +- 后端:Python 3.8+ +- 前端:HTML5, CSS3, JavaScript (Vue.js框架) +- 计算机视觉:Python,PyTorch + +**标准规范**: +- 代码规范:PEP 8 (Python) +- API设计:RESTful风格 +- 通信协议:MAVLink (无人机通信),WebSocket (实时数据传输) +- 目标检测:YOLO格式的标注数据 + +**开发工具**: +- 集成开发环境:PyCharm或Visual Studio Code +- 版本控制:Git +- 测试工具:Pytest +- 文档工具:Sphinx +- 深度学习框架:PyTorch +- 目标检测:YOLOv8 +- 模型训练:Label Studio (数据标注) + +**容量和性能要求**: +- 支持同时处理不少于5个无人机连接 +- Web界面响应时间不超过1秒 +- 无人机命令响应时间不超过3秒 +- 系统连续运行时间不少于72小时 +- 目标检测处理速度不低于15FPS +- 伤员识别准确率不低于85% + +**灵活性和配置要求**: +- 通过配置文件支持不同环境(开发、测试、生产)配置 +- 支持不同类型无人机的适配 +- 支持模拟和实际操作模式切换 +- 支持本地部署和云部署方式 + +## 3、软件设计 + +### 3.1 软件体系结构设计 + +智能战场医疗后送系统采用分层模块化架构,从逻辑上分为以下几个主要层次: + +**1. 表示层** +- Web界面模块:提供基于Web的用户交互界面 +- 实时数据可视化模块:显示无人机状态和任务进度 + +**2. 应用层** +- 任务管理模块:处理任务创建、分配和监控 +- 通信管理模块:处理外部系统通信请求 +- 计算机视觉模块:处理图像分析和目标识别 + +**3. 领域层** +- 无人机控制模块:负责无人机的基本操作控制 +- 任务规划模块:制定无人机执行任务的路径规划 +- 位置管理模块:管理伤员和基地位置信息 +- 医疗物资模块:管理医疗物资库存和请求 +- 伤员识别模块:基于YOLO的伤员检测和识别 + +**4. 基础设施层** +- 配置管理模块:处理系统配置和环境设置 +- 数据持久化模块:存储系统运行数据 +- 日志模块:记录系统运行日志 + +**架构图**: + +``` ++-------------------+ +-------------------+ +| Web界面模块 | | 实时数据可视化模块 | ++-------------------+ +-------------------+ + | | + v v ++-------------------+ +-------------------+ +-------------------+ +| 任务管理模块 | | 通信管理模块 | | 计算机视觉模块 | ++-------------------+ +-------------------+ +-------------------+ + | | | | + v v v v ++-------------------+ +-------------------+ +-------------------+ +| 无人机控制模块 | | 任务规划模块 | | 位置管理模块 | ++-------------------+ +-------------------+ +-------------------+ + | | | + v v v ++-------------------+ +-------------------+ +-------------------+ +| 医疗物资模块 | | 伤员识别模块 | | 日志模块 | ++-------------------+ +-------------------+ +-------------------+ + | + v + +-------------------+ + | 数据持久化模块 | + +-------------------+ +``` + +**模块说明**: + +1. **Web界面模块(WebInterface)** + - 负责提供基于Web的用户交互界面 + - 使用Flask框架和Socket.IO实现 + - 支持伤员位置标记、物资请求、无人机状态监控等功能 + - 显示无人机拍摄的实时视频流和目标检测结果 + +2. **任务管理模块(MissionPlanner)** + - 负责创建、安排和监控任务 + - 定义任务类型(伤员转运、物资运送、区域搜索) + - 管理任务队列和优先级 + +3. **无人机控制模块(DroneController)** + - 负责与无人机建立连接和通信 + - 执行基本飞行控制(起飞、降落、导航等) + - 监控无人机状态(位置、电量等) + - 控制无人机相机和视频流传输 + +4. **位置管理模块(PositionManager)** + - 管理伤员位置信息 + - 提供位置查询和距离计算功能 + - 支持地理编码功能 + - 集成自动识别的伤员位置数据 + +5. **医疗物资模块(MedicalSupplyManager)** + - 管理医疗物资库存 + - 处理物资请求和分配 + - 跟踪物资使用情况 + +6. **通信管理模块(CommunicationManager)** + - 处理与外部系统的通信 + - 接收外部伤员信息和物资请求 + - 支持多种通信协议 + +7. **配置管理模块(Config)** + - 管理系统配置参数 + - 支持不同环境下的配置切换 + - 提供配置加载和验证功能 + +8. **计算机视觉模块(ComputerVisionManager)** + - 接收和处理无人机视频流 + - 运行YOLO目标检测模型 + - 提供实时伤员识别功能 + - 计算伤员地理位置坐标 + +9. **伤员识别模块(CasualtyDetector)** + - 基于YOLOv8的目标检测模型 + - 负责伤员特征提取和识别 + - 支持不同环境和光照条件下的伤员识别 + - 提供识别结果的置信度评估 + +### 3.2 用户界面设计 + +系统采用Web界面设计,主要包含以下界面组件: + +**1. 主控制面板** +- 显示系统概览和无人机状态 +- 包含任务列表和执行状态 +- 提供快速操作按钮 + +**2. 地图视图** +- 显示无人机、伤员和基地位置 +- 支持地图缩放和平移 +- 允许直接在地图上标记伤员位置 + +**3. 物资管理界面** +- 显示可用医疗物资列表 +- 物资库存状态和使用记录 +- 创建物资运送请求 + +**4. 任务规划界面** +- 创建和编辑任务 +- 显示任务执行路径 +- 任务优先级设置 + +**5. 系统设置界面** +- 系统参数配置 +- 无人机连接设置 +- 用户权限管理 + +**界面跳转关系**: + +``` ++----------------+ +| 登录界面 | ++----------------+ + | + v ++----------------+ +| 主控制面板 | ++----------------+ + | | | | + v v v v ++------+ +------+ +------+ +------+ +|地图 | |物资 | |任务 | |系统 | +|视图 | |管理 | |规划 | |设置 | ++------+ +------+ +------+ +------+ +``` + +### 3.3 用例设计 + +系统主要用例设计如下: + +**1. 标记伤员位置** + +顺序图: +``` +操作员 -> Web界面: 在地图上标记伤员位置 +Web界面 -> PositionManager: 添加伤员位置(casualty_id, lat, lon) +PositionManager -> PositionManager: 存储伤员位置信息 +PositionManager -> Web界面: 返回成功状态 +Web界面 -> 操作员: 显示伤员标记成功 +``` + +**2. 自动识别伤员** + +顺序图: +``` +DroneController -> ComputerVisionManager: 传输视频流 +ComputerVisionManager -> CasualtyDetector: 处理图像帧 +CasualtyDetector -> CasualtyDetector: 执行YOLO目标检测 +CasualtyDetector -> ComputerVisionManager: 返回检测结果(检测框, 置信度) +ComputerVisionManager -> ComputerVisionManager: 计算地理坐标 +ComputerVisionManager -> PositionManager: 添加伤员位置(casualty_id, lat, lon) +ComputerVisionManager -> Web界面: 发送检测结果 +Web界面 -> 操作员: 显示检测到的伤员位置 +``` + +**3. 请求医疗物资运送** + +顺序图: +``` +操作员 -> Web界面: 创建物资运送请求 +Web界面 -> MedicalSupplyManager: 创建物资请求(target_id, supply_id, quantity) +MedicalSupplyManager -> MedicalSupplyManager: 检查库存可用性 +MedicalSupplyManager -> PositionManager: 获取目标位置(target_id) +PositionManager -> MedicalSupplyManager: 返回目标位置 +MedicalSupplyManager -> MissionPlanner: 创建物资运送任务(mission_id, target_location) +MissionPlanner -> MissionPlanner: 将任务添加到队列 +MissionPlanner -> MedicalSupplyManager: 返回任务创建状态 +MedicalSupplyManager -> Web界面: 返回请求状态 +Web界面 -> 操作员: 显示请求结果 +``` + +**4. 执行无人机任务** + +顺序图: +``` +MissionPlanner -> MissionPlanner: 从队列获取下一个任务 +MissionPlanner -> DroneController: 执行起飞(altitude) +DroneController -> DroneController: 控制无人机起飞 +DroneController -> MissionPlanner: 返回起飞状态 +MissionPlanner -> DroneController: 导航到目标位置(lat, lon, altitude) +DroneController -> DroneController: 控制无人机飞往目标位置 +DroneController -> MissionPlanner: 返回导航状态 +MissionPlanner -> DroneController: 执行降落 +DroneController -> DroneController: 控制无人机降落 +DroneController -> MissionPlanner: 返回降落状态 +MissionPlanner -> MissionPlanner: 更新任务状态为完成 +``` + +**5. 区域伤员搜索** + +顺序图: +``` +操作员 -> Web界面: 指定搜索区域 +Web界面 -> MissionPlanner: 创建区域搜索任务(area_points) +MissionPlanner -> MissionPlanner: 规划搜索路径 +MissionPlanner -> DroneController: 执行搜索任务 +DroneController -> DroneController: 按路径飞行 +DroneController -> ComputerVisionManager: 启动视频处理 +ComputerVisionManager -> CasualtyDetector: 进行伤员检测 +CasualtyDetector -> ComputerVisionManager: 返回检测结果 +ComputerVisionManager -> PositionManager: 记录伤员位置 +PositionManager -> Web界面: 更新伤员位置 +Web界面 -> 操作员: 显示搜索结果 +``` + +**6. 监控无人机状态** + +顺序图: +``` +操作员 -> Web界面: 请求无人机状态 +Web界面 -> DroneController: 获取无人机状态 +DroneController -> Web界面: 返回状态数据(位置,电量,任务) +Web界面 -> 操作员: 显示无人机状态 +``` + +### 3.4 类设计 + +系统的主要类设计如下: + +**1. MedicalEvacuationSystem(主系统类)** + - 属性: + - drone_controller: DroneController + - mission_planner: MissionPlanner + - position_manager: PositionManager + - medical_supply_manager: MedicalSupplyManager + - communication_manager: CommunicationManager + - computer_vision_manager: ComputerVisionManager + - web_interface: WebInterface + - 方法: + - __init__(): 初始化系统 + - start(): 启动系统 + - stop(): 停止系统 + - _handle_casualty_request(data): 处理伤员请求 + - _handle_supply_request(data): 处理物资请求 + - _handle_drone_status_request(): 处理无人机状态请求 + - _handle_detection_result(data): 处理目标检测结果 + +**2. DroneController(无人机控制类)** + - 属性: + - vehicle: Vehicle + - connection_string: string + - logger: Logger + - 方法: + - connect(): 连接到无人机 + - arm_and_takeoff(target_altitude): 解锁并起飞 + - goto_location(lat, lon, altitude): 飞往指定位置 + - land(): 降落 + - close(): 关闭连接 + +**3. Mission(任务类)** + - 属性: + - mission_id: string + - mission_type: MissionType + - target_location: tuple(lat, lon) + - altitude: float + - payload: dict + - status: MissionStatus + - start_time: datetime + - end_time: datetime + - 方法: + - to_dict(): 转换为字典表示 + +**4. MissionPlanner(任务规划类)** + - 属性: + - drone_controller: DroneController + - mission_queue: Queue + - current_mission: Mission + - mission_history: list + - running: bool + - 方法: + - start(): 启动任务规划器 + - stop(): 停止任务规划器 + - add_mission(mission): 添加任务 + - get_current_mission(): 获取当前任务 + - get_mission_history(): 获取任务历史 + +**5. PositionManager(位置管理类)** + - 属性: + - casualty_positions: dict + - geocoder: Geocoder + - 方法: + - add_casualty_position(casualty_id, lat, lon): 添加伤员位置 + - get_casualty_position(casualty_id): 获取伤员位置 + - calculate_distance(lat1, lon1, lat2, lon2): 计算距离 + - get_nearest_casualty(lat, lon): 获取最近伤员 + +**6. MedicalSupply(医疗物资类)** + - 属性: + - id: string + - name: string + - description: string + - weight: float + - quantity: int + +**7. MedicalSupplyManager(医疗物资管理类)** + - 属性: + - supplies: dict + - supply_requests: dict + - 方法: + - get_all_supplies(): 获取所有物资 + - create_supply_request(target_id, supply_id, quantity): 创建物资请求 + - update_request_status(request_id, status): 更新请求状态 + +**8. WebInterface(Web界面类)** + - 属性: + - app: Flask + - socketio: SocketIO + - casualty_detector: CasualtyDetector + - 方法: + - start(): 启动Web服务 + - broadcast_casualty_update(data): 广播伤员更新 + - broadcast_drone_update(data): 广播无人机状态更新 + - _register_routes(): 注册路由 + +**9. CommunicationManager(通信管理类)** + - 属性: + - server_socket: Socket + - clients: list + - casualty_handler: function + - supply_handler: function + - 方法: + - start_server(): 启动通信服务器 + - close(): 关闭服务器 + - send_message(client, message): 发送消息 + +**10. ComputerVisionManager(计算机视觉管理类)** + - 属性: + - video_streams: dict + - detector: CasualtyDetector + - position_manager: PositionManager + - drone_controller: DroneController + - processing_enabled: bool + - frame_queue: Queue + - 方法: + - start_processing(): 启动视频处理 + - stop_processing(): 停止视频处理 + - process_frame(frame): 处理单帧图像 + - add_video_stream(drone_id, stream): 添加视频流 + - remove_video_stream(drone_id): 移除视频流 + - calculate_gps_location(detection, drone_data): 计算GPS位置 + +**11. CasualtyDetector(伤员检测类)** + - 属性: + - model: YOLO + - detection_threshold: float + - classes: dict + - device: string + - 方法: + - load_model(model_path): 加载YOLO模型 + - detect(image): 执行目标检测 + - filter_detections(detections): 过滤检测结果 + - get_detection_metadata(detection): 获取检测元数据 + - update_model(new_model_path): 更新模型 + +### 3.5 数据设计 + +系统采用MySQL关系型数据库进行数据持久化存储,具有高可靠性和一致性保障。 + +**1. 数据库和表设计** + +系统的数据库设计类图如下: + +```mermaid +classDiagram + direction LR + + CasualtyPositions <-- SupplyRequests : 目标位置 + MedicalSupplies <-- SupplyRequests : 所需物资 + SupplyRequests <-- Missions : 关联请求 + DetectionResults -- BoundingBoxes : 包含 + CasualtyPositions <-- Missions : 目标位置 + + class CasualtyPositions { + +varchar casualty_id PK + +double latitude + +double longitude + +timestamp created_at + +timestamp updated_at + +varchar location_description + +varchar status + } + + class MedicalSupplies { + +varchar supply_id PK + +varchar name + +text description + +double weight + +int quantity + +timestamp created_at + +timestamp updated_at + } + + class SupplyRequests { + +varchar request_id PK + +varchar target_id FK + +varchar supply_id FK + +int quantity + +varchar status + +timestamp created_at + +timestamp updated_at + +int priority + } + + class Missions { + +varchar mission_id PK + +varchar mission_type + +double target_latitude + +double target_longitude + +double altitude + +json payload + +varchar status + +datetime start_time + +datetime end_time + +text error_message + +varchar request_id FK + } + + class DetectionResults { + +varchar detection_id PK + +timestamp created_at + +varchar drone_id + +int frame_id + +varchar image_path + +varchar processed_image_path + } + + class BoundingBoxes { + +varchar box_id PK + +varchar detection_id FK + +int class_id + +varchar class_name + +double confidence + +int x_min + +int y_min + +int x_max + +int y_max + +double est_latitude + +double est_longitude + +double accuracy + } + + class SystemLogs { + +bigint log_id PK + +varchar log_level + +text message + +datetime created_at + +varchar component + +varchar user_id + } +``` + +**2. 主要数据表说明** + +- **CasualtyPositions(伤员位置表)**:存储伤员地理位置信息,包括经纬度、时间戳和状态 +- **MedicalSupplies(医疗物资表)**:管理医疗物资目录和库存信息 +- **SupplyRequests(物资请求表)**:记录物资请求信息,关联伤员位置和所需物资 +- **Missions(任务表)**:存储无人机任务数据,包括任务类型、目标位置和执行状态 +- **DetectionResults(检测结果表)**:保存计算机视觉检测结果的元数据 +- **BoundingBoxes(边界框表)**:存储目标检测中识别到的对象边界框信息 +- **SystemLogs(系统日志表)**:记录系统运行日志,用于监控和审计 + +**3. 数据操作活动图** + +伤员位置添加流程: + +```plantuml +@startuml +start +if (伤员ID是否已存在?) then (是) + :SELECT查询现有伤员记录; + :准备UPDATE语句; +else (否) + :准备INSERT语句; + :设置伤员属性; + :执行INSERT语句; +endif +:执行SQL语句; +:提交事务; +:记录系统日志; +stop +@enduml +``` + +物资请求处理流程: + +```plantuml +@startuml +start +:获取请求参数; +if (验证请求数据) then (有效) + :开始数据库事务; + :查询物资库存; + if (库存足够?) then (是) + :生成请求ID; + :插入物资请求记录; + :查询伤员位置; + :创建无人机任务记录; + :更新物资库存; + :提交事务; + else (否) + :回滚事务; + :返回错误信息; + endif +else (无效) + :返回错误信息; +endif +:记录系统日志; +stop +@enduml +``` + +目标检测结果处理流程: + +```plantuml +@startuml +start +:接收检测结果数据; +:开始数据库事务; +:创建检测结果记录; +while (处理每个检测边界框) is (更多边界框) + :创建边界框记录; + if (是否为伤员类别?) then (是) + :计算地理坐标; + :在伤员位置表添加记录; + endif +endwhile (无更多) +:提交事务; +stop +@enduml +``` + +**4. 数据库设计说明** + +MySQL数据库设计遵循以下原则: + +- **规范化设计**:遵循第三范式,减少数据冗余 +- **引用完整性**:使用外键约束确保数据一致性 +- **性能优化**:对频繁查询的字段建立索引 +- **安全性**:实施访问控制和数据加密 +- **可扩展性**:设计支持未来功能扩展 + +数据库具有以下特点: + +- 采用InnoDB存储引擎,支持事务和外键 +- 使用UTF-8编码,支持多语言数据存储 +- 实施数据库连接池,优化连接管理 +- 使用预编译语句,防止SQL注入 +- 支持主从复制配置,提高系统可用性 + +系统在运行时维护数据库连接,并通过事务机制确保数据一致性。对于大量数据的批量操作,系统会优化SQL查询以提高效率。同时,系统支持数据库备份和恢复功能,确保数据安全。 + +**5. 内存缓存策略** + +为提高系统响应速度,系统会将以下数据缓存在内存中: + +- 常用物资信息 +- 活跃伤员位置数据 +- 当前执行中的任务 +- 用户会话信息 + +缓存策略采用LRU(最近最少使用)算法,定期与数据库同步,确保数据一致性。 + +### 3.6 部署设计 + +系统支持以下部署方式: + +**1. 单机部署** + +``` ++-----------------+ +| 部署服务器 | ++-----------------+ +| Web服务 (Flask)| +| 通信服务 | +| 应用逻辑 | +| YOLO模型(GPU) | ++-----------------+ + | + v ++-----------------+ +| 无人机 | ++-----------------+ +``` + +- 所有组件部署在同一台服务器上 +- 适合小规模应用场景 +- 硬件要求:8核CPU,16GB内存,NVIDIA GPU (8GB+显存),100GB存储空间 + +**2. 分布式部署** + +``` ++-----------------+ +-----------------+ +| Web服务器 | | 应用服务器 | ++-----------------+ +-----------------+ +| Web界面 | | 任务规划 | +| Socket.IO |<-->| 无人机控制 | ++-----------------+ | 位置管理 | + | 物资管理 | + +-----------------+ + | + v + +-----------------+ + | 无人机 | + +-----------------+ +``` + +- Web服务与应用逻辑分离部署 +- 适合中大规模应用或高负载场景 +- 支持水平扩展,提高系统可用性 + +**3. 边缘计算部署** + +``` ++----------------+ +----------------+ +| 云端服务器 | | 边缘服务器 | ++----------------+ +----------------+ +| Web界面 | | 任务规划 | +| 数据存储 |<-->| 无人机控制 | +| 分析报表 | | 通信服务 | ++----------------+ +----------------+ + | + v + +----------------+ + | 无人机 | + +----------------+ +``` + +- 核心控制逻辑部署在靠近作战区域的边缘服务器 +- Web界面和数据分析功能部署在云端 +- 提高控制响应速度,降低网络依赖 +- 适合网络条件受限的战场环境 + +**部署要求**: +- 操作系统:Ubuntu 20.04 LTS +- Python 3.8+及相关依赖包 +- 网络:支持TCP/IP通信,提供稳定的网络连接 +- 安全:配置防火墙规则,保护关键服务端口 +- 存储:提供足够的日志和数据存储空间 + +def __init__(self): + """初始化医疗后送系统""" + logger.info(f"初始化{config.APP_NAME} v{config.VERSION}") + + # 初始化各个模块 + self.drone_controller = DroneController(config.DRONE_CONNECTION_STRING) + self.medical_supply_manager = MedicalSupplyManager() + self.position_manager = PositionManager() + self.communication_manager = CommunicationManager( + host=config.COMM_HOST, + port=config.COMM_PORT + ) + + # 初始化伤员检测器 + casualty_detector = CasualtyDetector( + model_path=config.CASUALTY_DETECTOR_MODEL_PATH, + detection_threshold=config.DETECTION_THRESHOLD + ) + + # 初始化计算机视觉管理器 + self.computer_vision_manager = ComputerVisionManager( + detector=casualty_detector, + position_manager=self.position_manager, + drone_controller=self.drone_controller + ) + + # 初始化Web界面 + self.web_interface = WebInterface( + host=config.WEB_HOST, + port=config.WEB_PORT, + casualty_detector=casualty_detector # 传递伤员检测器给Web界面 + ) + + self.mission_planner = None + + # 注册处理器 + self._register_handlers() + +def _register_routes(self): + # 保留原有路由 + # ... + + # 添加伤员检测相关路由 + @self.app.route('/casualty_detection') + def casualty_detection_page(): + return render_template('casualty_detection.html') + + @self.app.route('/api/detect_casualty', methods=['POST']) + def detect_casualty(): + if 'image' not in request.files: + return jsonify({"success": False, "error": "未找到图像文件"}) + + file = request.files['image'] + if file.filename == '': + return jsonify({"success": False, "error": "未选择文件"}) + + if file: + # 保存上传的文件 + filename = secure_filename(file.filename) + file_path = os.path.join('static/uploads', filename) + file.save(file_path) + + # 执行检测 + detection_results = self.casualty_detector.detect(file_path) + + # 返回结果 + return jsonify(detection_results) + + # 用于获取检测结果的Socket.IO事件 + @self.socketio.on('request_detection_results') + def handle_detection_request(data): + image_path = data.get('image_path') + if image_path and os.path.exists(image_path): + detection_results = self.casualty_detector.detect(image_path) + self.socketio.emit('detection_results', detection_results) + else: + self.socketio.emit('detection_results', {"success": False, "error": "图像不存在"}) \ No newline at end of file