pull/8/head
2793660631@qq.com 3 weeks ago
parent bcae26adb0
commit e7799487f4

Binary file not shown.

@ -1,32 +0,0 @@
# 应用环境
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

@ -1,73 +0,0 @@
# 智能战场医疗后送系统
基于无人机的智能战场医疗后送系统,用于快速、高效地运送医疗资源和伤员。
## 功能特点
- 实时伤员位置报告
- 医疗资源运送
- 无人机状态监控
- 简单易用的操作界面
## 系统要求
- 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

@ -1,3 +0,0 @@
"""
Config package initialization
"""

@ -1,110 +0,0 @@
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()

@ -1,29 +0,0 @@
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()

@ -1,14 +0,0 @@
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文件")

@ -1,168 +0,0 @@
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/")

@ -1,89 +0,0 @@
#!/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"

@ -1,350 +0,0 @@
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)

@ -1,9 +0,0 @@
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

@ -1,199 +0,0 @@
#!/usr/bin/env python3
import os
import sys
import time
import logging
import random
import threading
import signal
import argparse
# 添加项目根目录到Python路径
current_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.append(current_dir)
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)

@ -1,140 +0,0 @@
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("通信服务器已关闭")

@ -1,19 +0,0 @@
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

@ -1,65 +0,0 @@
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"<MedicalSupply {self.id}: {self.name}>"
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"<SupplyRequest {self.id}: {self.supply_id} for {self.target_id}>"
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
}

@ -1,103 +0,0 @@
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

@ -1,190 +0,0 @@
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()

@ -1,260 +0,0 @@
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 {}

@ -1,271 +0,0 @@
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

@ -1,131 +0,0 @@
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}")

@ -1,9 +0,0 @@
[
{
"x": 192.75340195819638,
"y": 269.1060189546518,
"width": 442.41011984021304,
"height": 100.79002079002078,
"class": "casualty"
}
]

@ -1,9 +0,0 @@
[
{
"x": 11.985121427837171,
"y": 117.62776025236593,
"width": 335.2926315789474,
"height": 184.41640378548897,
"class": "casualty"
}
]

@ -1,9 +0,0 @@
[
{
"x": 12.780651033233484,
"y": 53.81395348837209,
"width": 707.7720207253885,
"height": 412.9186046511628,
"class": "casualty"
}
]

@ -1,9 +0,0 @@
[
{
"x": 112.26251631820757,
"y": 376.3773436462995,
"width": 446.6321243523316,
"height": 147.99805825242714,
"class": "casualty"
}
]

@ -1,9 +0,0 @@
[
{
"x": 677.3609536917098,
"y": 894.9805757837389,
"width": 1138.0725388601036,
"height": 442.63106796116494,
"class": "casualty"
}
]

@ -1,9 +0,0 @@
[
{
"x": 194.732217611838,
"y": 293.69761911478724,
"width": 268.61626248216834,
"height": 167.63714902807772,
"class": "casualty"
}
]

@ -1,9 +0,0 @@
[
{
"x": 142.80639735506196,
"y": 558.0339801654405,
"width": 488.30242510699003,
"height": 295.6647791619479,
"class": "casualty"
}
]

@ -1,9 +0,0 @@
[
{
"x": 436.9429980658497,
"y": 254.91261424833132,
"width": 492.4352331606218,
"height": 341.12621359223294,
"class": "casualty"
}
]

@ -1,9 +0,0 @@
[
{
"x": 225.6994747497875,
"y": 360.23300615329185,
"width": 537.2020725388602,
"height": 142.60194174757282,
"class": "casualty"
}
]

@ -1,9 +0,0 @@
[
{
"x": 16.974595112047698,
"y": 179.43037974683543,
"width": 453.0442105263158,
"height": 135.56962025316457,
"class": "casualty"
}
]

@ -1,342 +0,0 @@
/* 全局样式 */
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;
}

@ -1,582 +0,0 @@
// 初始化应用
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(`
<div class="casualty-popup">
<h5>伤员ID: ${id}</h5>
<p>位置: ${lat.toFixed(6)}, ${lon.toFixed(6)}</p>
<button class="btn btn-sm btn-primary" onclick="requestMedicalSupply('${id}')">请求医疗物资</button>
</div>
`);
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(`
<div class="drone-popup">
<h5>无人机状态</h5>
<p>状态: ${data.status}</p>
<p>电量: ${data.battery}%</p>
</div>
`);
}
}
// 更新物资状态
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 = `
<div class="d-flex justify-content-between align-items-center">
<div>
<strong>${supply.name}</strong> (${supply.quantity})
</div>
<button class="btn btn-sm btn-outline-primary" onclick="requestSupply('${supply.id}')">
请求
</button>
</div>
<small class="text-muted">${supply.description}</small>
`;
supplyList.appendChild(supplyItem);
});
} else {
supplyList.innerHTML = '<p class="text-muted">没有可用物资</p>';
}
}
// 更新任务状态
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 = `
<div class="mission-item mission-${data.currentMission.status.toLowerCase()}">
<div class="d-flex justify-content-between">
<strong>${getMissionTypeLabel(data.currentMission.mission_type)}</strong>
<span class="badge bg-${getMissionStatusBadge(data.currentMission.status)}">
${data.currentMission.status}
</span>
</div>
<div>ID: ${data.currentMission.mission_id}</div>
<div>目标: (${data.currentMission.target_location[0].toFixed(6)},
${data.currentMission.target_location[1].toFixed(6)})</div>
</div>
`;
}
}
// 更新历史任务列表
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 = `
<div class="d-flex justify-content-between">
<strong>${getMissionTypeLabel(mission.mission_type)}</strong>
<span class="badge bg-${getMissionStatusBadge(mission.status)}">
${mission.status}
</span>
</div>
<div>ID: ${mission.mission_id}</div>
<small class="text-muted">${formatTime(mission.start_time)}</small>
`;
missionList.appendChild(missionItem);
});
} else {
missionList.innerHTML = '<p class="text-muted">没有历史任务</p>';
}
}
// 获取任务类型标签
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 = `
<div class="d-flex justify-content-between align-items-center">
<div>
<strong>伤员ID: ${id}</strong>
</div>
<button class="btn btn-sm btn-outline-primary" onclick="requestMedicalSupply('${id}')">
请求物资
</button>
</div>
<div>位置: ${position.lat.toFixed(6)}, ${position.lng.toFixed(6)}</div>
`;
casualtyList.appendChild(casualtyItem);
}
// 如果没有伤员,显示提示信息
if (Object.keys(casualtyMarkers).length === 0) {
casualtyList.innerHTML = '<p class="text-muted">没有伤员数据</p>';
}
}
// 加载医疗物资数据
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 = `
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i> 使
</div>
`;
// 显示模态框
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 = `
<div class="text-center p-3">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">正在加载...</span>
</div>
<p class="mt-2">正在进行伤员识别请稍候...</p>
</div>
`;
// 发送请求进行检测
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 = `
<div class="alert alert-danger">
<i class="fas fa-exclamation-circle me-2"></i> : ${error}
</div>
`;
});
}
// 从无人机摄像头进行检测
function detectFromDroneCamera() {
// 显示加载中...
document.getElementById('detection-result').innerHTML = `
<div class="text-center p-3">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">正在加载...</span>
</div>
<p class="mt-2">正在从无人机获取图像并进行伤员识别请稍候...</p>
</div>
`;
// 通过WebSocket请求无人机图像检测
socket.emit('detect_from_drone_camera', {});
}
// 处理检测结果
function handleDetectionResult(result) {
if (!result.success) {
document.getElementById('detection-result').innerHTML = `
<div class="alert alert-danger">
<i class="fas fa-exclamation-circle me-2"></i> : ${result.error || ''}
</div>
`;
return;
}
if (!result.detections || result.detections.length === 0) {
document.getElementById('detection-result').innerHTML = `
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle me-2"></i>
</div>
<img src="${result.result_path}" class="img-fluid mt-3 border rounded" alt="检测结果">
`;
return;
}
// 显示检测结果
let resultHTML = `
<div class="alert alert-success">
<i class="fas fa-check-circle me-2"></i> ! ${result.detections.length}
</div>
<div class="row">
<div class="col-md-8">
<img src="${result.result_path}" class="img-fluid border rounded" alt="检测结果">
</div>
<div class="col-md-4">
<h6>检测详情:</h6>
<ul class="list-group">
`;
// 添加每个检测结果
result.detections.forEach((detection, index) => {
resultHTML += `
<li class="list-group-item">
<div><strong>伤员 #${index + 1}</strong></div>
<div>置信度: ${(detection.confidence * 100).toFixed(2)}%</div>
</li>
`;
});
resultHTML += `
</ul>
</div>
</div>
`;
document.getElementById('detection-result').innerHTML = resultHTML;
}

@ -1,853 +0,0 @@
// 模型训练页面的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 = '<p class="text-muted" id="no-annotations">该图片暂无标注</p>';
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 = `
<div>
<span class="badge bg-success me-2">${index + 1}</span>
<span>${box.class}</span>
</div>
<div>
<small class="text-muted">
位置: (${Math.round(box.x)}, ${Math.round(box.y)})
尺寸: ${Math.round(box.width)}x${Math.round(box.height)}
</small>
</div>
`;
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 = `
<h6>${model.name}</h6>
<p class="mb-1"><small>训练时间: ${new Date(model.created_at).toLocaleString()}</small></p>
<p class="mb-1"><small>精度(mAP): ${model.map.toFixed(4)}</small></p>
<div class="d-flex justify-content-end">
<button class="btn btn-sm btn-primary deploy-model-btn" data-model="${model.name}">
<i class="fas fa-check-circle me-1"></i>
</button>
</div>
`;
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();
});

@ -1 +0,0 @@
This is a placeholder for a real YOLO model file

@ -1,9 +0,0 @@
# YOLOv5 数据集配置
path: src/ui/static/models\dataset
train: images/train
val: images/val
test: # 暂无测试集
# 类别
nc: 1 # 类别数量
names: ['casualty'] # 类别名称

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

@ -1 +0,0 @@
0 0.5519446158377372 0.6656271444784629 0.5898801597869507 0.20997920997920996

@ -1 +0,0 @@
0 0.3789692768297698 0.6640378548895899 0.7073684210526316 0.583596214511041

@ -1 +0,0 @@
0 0.4583333267449097 0.4874031007751938 0.8847150259067357 0.7732558139534884

@ -1 +0,0 @@
0 0.4194732231179667 0.844983813832107 0.5582901554404145 0.2776699029126213

@ -1 +0,0 @@
0 0.6085923941024227 0.8177993478126897 0.555699481865285 0.3242718446601941

@ -1 +0,0 @@
0 0.4700576412184603 0.8171346182442123 0.38373751783166904 0.36285097192224613

@ -1 +0,0 @@
0 0.5527965855836527 0.8003020065152091 0.6975748930099858 0.33522083805209507

@ -1 +0,0 @@
0 0.632556124672371 0.590938501450622 0.455958549222798 0.4737864077669902

@ -1 +0,0 @@
0 0.4576856583511274 0.599352745870942 0.49740932642487057 0.19805825242718447

@ -1 +0,0 @@
0 0.5137061189350329 0.7848101265822786 0.9557894736842105 0.4303797468354431

@ -1,170 +0,0 @@
{
"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
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 482 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 483 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 486 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save