diff --git a/distance-judgement/.idea/.gitignore b/distance-judgement/.idea/.gitignore new file mode 100644 index 00000000..f649f0f6 --- /dev/null +++ b/distance-judgement/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/distance-judgement/.idea/inspectionProfiles/profiles_settings.xml b/distance-judgement/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 00000000..105ce2da --- /dev/null +++ b/distance-judgement/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/distance-judgement/.idea/misc.xml b/distance-judgement/.idea/misc.xml new file mode 100644 index 00000000..2e9d95a7 --- /dev/null +++ b/distance-judgement/.idea/misc.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/distance-judgement/.idea/modules.xml b/distance-judgement/.idea/modules.xml new file mode 100644 index 00000000..b7e21735 --- /dev/null +++ b/distance-judgement/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/distance-judgement/.idea/pythonProject2.iml b/distance-judgement/.idea/pythonProject2.iml new file mode 100644 index 00000000..d0876a78 --- /dev/null +++ b/distance-judgement/.idea/pythonProject2.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/distance-judgement/CAMERA_ICON_OVERLAP_FIX.md b/distance-judgement/CAMERA_ICON_OVERLAP_FIX.md new file mode 100644 index 00000000..20f9893f --- /dev/null +++ b/distance-judgement/CAMERA_ICON_OVERLAP_FIX.md @@ -0,0 +1,137 @@ +# 摄像头图标重叠问题修复报告 🔧 + +## 问题描述 + +在摄像头图标更新时,没有清除之前的图标,导致地图上出现图标重叠的现象。 + +## 问题根源分析 + +### 1. 固定摄像头视野扇形重叠 +- **问题位置**: `src/web_server.py` 第3730行附近 +- **原因**: 摄像头位置更新时,只更新了`cameraMarker`的位置,但没有同步更新`fixedCameraFOV`视野扇形 +- **表现**: 旧的视野扇形仍然显示在原位置,新的视野扇形在新位置,造成重叠 + +### 2. 移动设备朝向标记重叠 +- **问题位置**: `src/web_server.py` 第2491行附近 +- **原因**: 移动设备朝向更新时,`orientationMarker`是复合对象(包含`deviceMarker`和`viewSector`),但只简单调用了`map.remove()` +- **表现**: 设备标记和视野扇形没有被完全清除,导致重叠 + +### 3. 变量作用域问题 +- **问题位置**: `src/web_server.py` 第1647行 +- **原因**: `fixedCameraFOV`使用`const`声明,无法在其他函数中重新赋值 +- **影响**: 摄像头位置更新函数无法更新全局视野扇形引用 + +## 修复内容 + +### ✅ 修复1:自动配置时的视野扇形同步更新 +```javascript +// 🔧 修复:同步更新视野扇形位置,避免图标重叠 +if (fixedCameraFOV) { + // 移除旧的视野扇形 + map.remove(fixedCameraFOV); + + // 重新创建视野扇形在新位置 + const newFOV = createGeographicSector( + lng, lat, + result.data.camera_heading || config.CAMERA_HEADING, + config.CAMERA_FOV, + 100, // 100米检测范围 + '#2196F3' // 蓝色,与固定摄像头标记颜色匹配 + ); + map.add(newFOV); + + // 更新全局变量引用 + fixedCameraFOV = newFOV; +} +``` + +### ✅ 修复2:手动配置时的视野扇形同步更新 +```javascript +// 🔧 修复:手动配置时也要同步更新视野扇形 +// 同步更新视野扇形 +if (fixedCameraFOV) { + map.remove(fixedCameraFOV); + + const newFOV = createGeographicSector( + lng, lat, heading, config.CAMERA_FOV, + 100, '#2196F3' + ); + map.add(newFOV); + fixedCameraFOV = newFOV; +} +``` + +### ✅ 修复3:移动设备朝向标记的正确清除 +```javascript +// 🔧 修复:正确移除旧的视野扇形标记,避免重叠 +if (mobileDeviceMarkers[deviceId].orientationMarker) { + // orientationMarker是一个复合对象,包含deviceMarker和viewSector + const oldOrientation = mobileDeviceMarkers[deviceId].orientationMarker; + if (oldOrientation.deviceMarker) { + map.remove(oldOrientation.deviceMarker); + } + if (oldOrientation.viewSector) { + map.remove(oldOrientation.viewSector); + } +} +``` + +### ✅ 修复4:变量作用域调整 +```javascript +// 将 const 改为 var,允许重新赋值 +var fixedCameraFOV = createGeographicSector(...); +``` + +## 测试验证 + +修复后,以下操作不再出现图标重叠: + +1. **自动配置摄像头位置** - 视野扇形会同步移动到新位置 +2. **手动配置摄像头位置** - 视野扇形会同步更新位置和朝向 +3. **移动设备朝向更新** - 旧的设备标记和视野扇形会被完全清除 +4. **摄像头朝向变更** - 视野扇形会反映新的朝向角度 + +## 影响范围 + +✅ **已修复的功能**: +- 固定摄像头位置更新 +- 固定摄像头朝向更新 +- 移动设备位置更新 +- 移动设备朝向更新 +- 手动配置摄像头 + +✅ **无影响的功能**: +- 人员检测标记更新(原本就有正确的清除逻辑) +- 远程设备标记更新(原本就有正确的清除逻辑) +- 其他地图功能 + +## 技术细节 + +- **修改文件**: `src/web_server.py` +- **修改行数**: 约15行代码修改 +- **兼容性**: 完全向后兼容,不影响现有功能 +- **性能影响**: 无负面影响,实际上减少了地图上的冗余元素 + +## 📝 补充修复:重复无人机图标问题 + +### 问题描述 +用户反映地图上出现了2个无人机图标,但应该只有1个无人机图标和1个电脑图标。 + +### 根源分析 +移动设备同时显示了两个独立的🚁标记: +- `locationMarker`:GPS位置标记 +- `orientationMarker`:朝向标记(包含视野扇形) + +### ✅ 修复方案 +1. **移除重复的位置标记**:删除独立的`locationMarker` +2. **合并功能到朝向标记**:朝向标记同时承担位置和朝向显示 +3. **更新清除逻辑**:移除对`locationMarker`的引用 +4. **添加数据缓存**:为点击事件提供设备数据支持 + +### 🎯 修复后的效果 +- **固定摄像头(电脑端)**:💻电脑图标 + 蓝色视野扇形 +- **移动设备(移动端)**:🚁无人机图标 + 朝向箭头 + 橙色视野扇形 + +## 总结 + +通过这次修复,彻底解决了摄像头图标重叠的问题,确保地图上的标记状态与实际配置始终保持一致,提升了用户体验。同时解决了重复无人机图标的问题,让图标显示更加清晰和直观。 \ No newline at end of file diff --git a/distance-judgement/CAMERA_ORIENTATION_GUIDE.md b/distance-judgement/CAMERA_ORIENTATION_GUIDE.md new file mode 100644 index 00000000..4c628189 --- /dev/null +++ b/distance-judgement/CAMERA_ORIENTATION_GUIDE.md @@ -0,0 +1,236 @@ +# 摄像头朝向自动配置功能指南 🧭 + +## 功能概述 + +本系统现在支持自动获取设备位置和朝向,将本地摄像头设置为面朝使用者,实现智能的摄像头配置。 + +## 🎯 主要功能 + +### 1. 自动GPS定位 +- **Windows系统**: 使用Windows Location API获取精确GPS位置 +- **其他系统**: 使用IP地理定位作为备选方案 +- **精度**: GPS可达10米内,IP定位约10公里 + +### 2. 设备朝向检测 +- **桌面设备**: 使用默认朝向算法(假设用户面向屏幕) +- **移动设备**: 支持陀螺仪和磁力计朝向检测 +- **智能计算**: 自动计算摄像头应该面向用户的角度 + +### 3. 自动配置应用 +- **实时更新**: 自动更新配置文件和运行时参数 +- **地图同步**: 自动更新地图上的摄像头位置标记 +- **即时生效**: 配置立即应用到距离计算和人员定位 + +## 🚀 使用方法 + +### 方法一:启动时自动配置 + +```bash +python main_web.py +``` + +系统会检测到默认配置并询问是否自动配置: +``` +🤖 检测到摄像头使用默认配置 + 是否要自动配置摄像头位置和朝向? + • 输入 'y' - 立即自动配置 + • 输入 'n' - 跳过,使用Web界面配置 + • 直接回车 - 跳过自动配置 + +🔧 请选择 (y/n/回车): y +``` + +### 方法二:独立配置工具 + +```bash +# 完整自动配置 +python tools/auto_configure_camera.py + +# 仅测试GPS功能 +python tools/auto_configure_camera.py --test-gps + +# 仅测试朝向功能 +python tools/auto_configure_camera.py --test-heading +``` + +### 方法三:Web界面配置 + +1. 启动Web服务器:`python main_web.py` +2. 打开浏览器访问 `https://127.0.0.1:5000` +3. 在"🧭 自动位置配置"面板中: + - 点击"📍 获取位置"按钮 + - 点击"🧭 获取朝向"按钮 + - 点击"🤖 自动配置摄像头"按钮 + +## 📱 Web界面功能详解 + +### GPS位置获取 +```javascript +// 使用浏览器Geolocation API +navigator.geolocation.getCurrentPosition() +``` + +**支持的浏览器**: +- ✅ Chrome/Edge (推荐) +- ✅ Firefox +- ✅ Safari +- ❌ IE (不支持) + +**权限要求**: +- 首次使用需要授权位置权限 +- HTTPS环境下精度更高 +- 室外环境GPS信号更好 + +### 设备朝向检测 +```javascript +// 使用设备朝向API +window.addEventListener('deviceorientation', handleOrientation) +``` + +**支持情况**: +- 📱 **移动设备**: 完全支持(手机、平板) +- 💻 **桌面设备**: 有限支持(使用算法估算) +- 🍎 **iOS 13+**: 需要明确请求权限 + +## ⚙️ 技术实现 + +### 后端模块 + +#### 1. OrientationDetector (`src/orientation_detector.py`) +- GPS位置获取(多平台支持) +- 设备朝向检测 +- 摄像头朝向计算 +- 配置文件更新 + +#### 2. WebOrientationDetector (`src/web_orientation_detector.py`) +- Web API接口 +- 前后端数据同步 +- 实时状态管理 + +### 前端功能 + +#### JavaScript函数 +- `requestGPSPermission()` - GPS权限请求 +- `requestOrientationPermission()` - 朝向权限请求 +- `autoConfigureCamera()` - 自动配置执行 +- `manualConfiguration()` - 手动配置入口 + +#### API接口 +- `POST /api/orientation/auto_configure` - 自动配置 +- `POST /api/orientation/update_location` - 更新GPS +- `POST /api/orientation/update_heading` - 更新朝向 +- `GET /api/orientation/get_status` - 获取状态 + +## 🔧 配置原理 + +### 朝向计算逻辑 + +```python +def calculate_camera_heading_facing_user(self, user_heading: float) -> float: + """ + 计算摄像头朝向用户的角度 + 摄像头朝向 = (用户朝向 + 180°) % 360° + """ + camera_heading = (user_heading + 180) % 360 + return camera_heading +``` + +### 坐标转换 + +```python +def calculate_person_position(self, pixel_x, pixel_y, distance, frame_width, frame_height): + """ + 基于摄像头位置、朝向和距离计算人员GPS坐标 + 使用球面几何学进行精确计算 + """ + # 像素到角度转换 + horizontal_angle_per_pixel = self.camera_fov / frame_width + horizontal_offset_degrees = (pixel_x - center_x) * horizontal_angle_per_pixel + + # 计算实际方位角 + person_bearing = (self.camera_heading + horizontal_offset_degrees) % 360 + + # 球面坐标计算 + person_lat, person_lng = self._calculate_destination_point( + self.camera_lat, self.camera_lng, distance, person_bearing + ) +``` + +## 📋 系统要求 + +### 环境要求 +- Python 3.7+ +- 现代Web浏览器 +- 网络连接(GPS定位需要) + +### Windows特别要求 +```bash +# 安装Windows位置服务支持 +pip install winrt-runtime winrt-Windows.Devices.Geolocation +``` + +### 移动设备要求 +- HTTPS访问(GPS权限要求) +- 现代移动浏览器 +- 设备朝向传感器支持 + +## 🔍 故障排除 + +### GPS获取失败 +**常见原因**: +- 位置权限被拒绝 +- 网络连接问题 +- GPS信号不佳 + +**解决方案**: +1. 检查浏览器位置权限设置 +2. 移动到室外或窗边 +3. 使用IP定位作为备选 +4. 手动输入坐标 + +### 朝向检测失败 +**常见原因**: +- 设备不支持朝向传感器 +- 浏览器兼容性问题 +- 权限被拒绝 + +**解决方案**: +1. 使用支持的移动设备 +2. 更新到现代浏览器 +3. 允许设备朝向权限 +4. 使用手动配置 + +### 配置不生效 +**可能原因**: +- 配置文件写入失败 +- 权限不足 +- 模块导入错误 + +**解决方案**: +1. 检查文件写入权限 +2. 重启应用程序 +3. 查看控制台错误信息 + +## 💡 使用建议 + +### 最佳实践 +1. **首次配置**: 使用Web界面进行配置,可视化效果更好 +2. **定期更新**: 位置变化时重新配置 +3. **精度要求**: GPS环境下精度更高,室内可用IP定位 +4. **设备选择**: 移动设备朝向检测更准确 + +### 注意事项 +1. **隐私保护**: GPS数据仅用于本地配置,不会上传 +2. **网络要求**: 初次配置需要网络连接 +3. **兼容性**: 老旧浏览器可能不支持某些功能 +4. **精度限制**: 桌面设备朝向检测精度有限 + +## 📚 相关文档 + +- [MAP_USAGE_GUIDE.md](MAP_USAGE_GUIDE.md) - 地图功能使用指南 +- [MOBILE_GUIDE.md](MOBILE_GUIDE.md) - 移动端使用指南 +- [HTTPS_SETUP.md](HTTPS_SETUP.md) - HTTPS配置指南 + +--- + +🎯 **快速开始**: 运行 `python main_web.py`,选择自动配置,享受智能的摄像头定位体验! \ No newline at end of file diff --git a/distance-judgement/HTTPS_SETUP.md b/distance-judgement/HTTPS_SETUP.md new file mode 100644 index 00000000..9eca3614 --- /dev/null +++ b/distance-judgement/HTTPS_SETUP.md @@ -0,0 +1,99 @@ +# 🔒 HTTPS设置指南 + +## 概述 +本系统已升级支持HTTPS,解决摄像头权限问题。现代浏览器要求HTTPS才能访问摄像头等敏感设备。 + +## 🚀 快速启动 + +### 方法一:自动设置(推荐) +1. 在PyCharm中打开项目 +2. 直接运行 `main_web.py` +3. 系统会自动生成SSL证书并启动HTTPS服务器 + +### 方法二:手动安装依赖 +如果遇到cryptography库缺失: +```bash +pip install cryptography +``` + +## 📱 访问地址 + +启动后访问地址已升级为HTTPS: +- **本地访问**: https://127.0.0.1:5000 +- **手机访问**: https://你的IP:5000/mobile/mobile_client.html + +## 🔑 浏览器安全警告处理 + +### 桌面浏览器 +1. 访问 https://127.0.0.1:5000 +2. 看到"您的连接不是私密连接"警告 +3. 点击 **"高级"** +4. 点击 **"继续访问localhost(不安全)"** +5. 正常使用 + +### 手机浏览器 +1. 访问 https://你的IP:5000/mobile/mobile_client.html +2. 出现安全警告时,点击 **"高级"** 或 **"详细信息"** +3. 选择 **"继续访问"** 或 **"继续前往此网站"** +4. 正常使用摄像头功能 + +## 📂 文件结构 + +新增文件: +``` +ssl/ +├── cert.pem # SSL证书文件 +└── key.pem # 私钥文件 +``` + +## 🔧 技术说明 + +### SSL证书特性 +- **类型**: 自签名证书 +- **有效期**: 365天 +- **支持域名**: localhost, 127.0.0.1 +- **算法**: RSA-2048, SHA-256 + +### 摄像头权限要求 +- ✅ HTTPS环境 - 支持摄像头访问 +- ❌ HTTP环境 - 浏览器阻止摄像头访问 +- ⚠️ localhost - HTTP也可以,但IP访问必须HTTPS + +## 🐛 故障排除 + +### 问题1: cryptography库安装失败 +```bash +# Windows +pip install --upgrade pip +pip install cryptography + +# 如果还是失败,尝试: +pip install --only-binary=cryptography cryptography +``` + +### 问题2: 证书生成失败 +1. 检查ssl目录权限 +2. 重新运行程序,会自动重新生成 + +### 问题3: 手机无法访问 +1. 确保手机和电脑在同一网络 +2. 检查防火墙设置 +3. 在手机浏览器中接受安全证书 + +### 问题4: 摄像头仍然无法访问 +1. 确认使用HTTPS访问 +2. 检查浏览器摄像头权限设置 +3. 尝试不同浏览器(Chrome、Firefox等) + +## 📋 更新日志 + +### v2.0 - HTTPS升级 +- ✅ 自动SSL证书生成 +- ✅ 完整HTTPS支持 +- ✅ 摄像头权限兼容 +- ✅ 手机端HTTPS支持 +- ✅ 浏览器安全警告处理指南 + +## 🎯 下一步 + +完成HTTPS升级后,您的移动端摄像头功能将完全正常工作,不再受到浏览器安全限制的影响。 \ No newline at end of file diff --git a/distance-judgement/MAP_USAGE_GUIDE.md b/distance-judgement/MAP_USAGE_GUIDE.md new file mode 100644 index 00000000..bce1849f --- /dev/null +++ b/distance-judgement/MAP_USAGE_GUIDE.md @@ -0,0 +1,165 @@ +# 地图功能使用指南 🗺️ + +## 功能概述 + +本系统集成了高德地图API,可以实时在地图上显示: +- 📷 摄像头位置(蓝色标记) +- 👥 检测到的人员位置(红色标记) +- 📏 每个人员距离摄像头的距离 + +## 快速开始 + +### 1. 配置摄像头位置 📍 + +首先需要设置摄像头的地理位置: + +```bash +python setup_camera_location.py +``` + +按提示输入: +- 摄像头纬度(例:39.9042) +- 摄像头经度(例:116.4074) +- 摄像头朝向角度(0-360°,0为正北) +- 高德API Key(可选,用于更好的地图体验) + +### 2. 启动系统 🚀 + +```bash +python main.py +``` + +### 3. 查看地图 🗺️ + +在检测界面按 `m` 键打开地图,系统会自动在浏览器中显示实时地图。 + +## 操作说明 + +### 键盘快捷键 + +- `q` - 退出程序 +- `c` - 距离校准模式 +- `r` - 重置为默认参数 +- `s` - 保存当前帧截图 +- `m` - 打开地图显示 🗺️ +- `h` - 设置摄像头朝向 🧭 + +### 地图界面说明 + +- 🔵 **蓝色标记** - 摄像头位置 +- 🔴 **红色标记** - 检测到的人员位置 +- 📊 **信息面板** - 显示系统状态和统计信息 +- ⚡ **实时更新** - 地图每3秒自动刷新一次 + +## 坐标计算原理 + +系统通过以下步骤计算人员的地理坐标: + +1. **像素坐标获取** - 从YOLO检测结果获取人体在画面中的位置 +2. **角度计算** - 根据摄像头视场角计算人相对于摄像头中心的角度偏移 +3. **方位角计算** - 结合摄像头朝向,计算人相对于正北的绝对角度 +4. **地理坐标转换** - 使用球面几何学公式,根据距离和角度计算地理坐标 + +### 关键参数 + +- `CAMERA_FOV` - 摄像头视场角(默认60°) +- `CAMERA_HEADING` - 摄像头朝向角度(0°为正北) +- 距离计算基于已校准的距离测量算法 + +## 高德地图API配置 + +### 获取API Key + +1. 访问 [高德开放平台](https://lbs.amap.com/) +2. 注册并创建应用 +3. 获取Web服务API Key +4. 在配置中替换 `your_gaode_api_key_here` + +### API使用限制 + +- 免费配额:每日10万次调用 +- 超出配额后可能影响地图加载 +- 建议使用自己的API Key以确保稳定服务 + +## 精度优化建议 + +### 距离校准 📏 + +使用 `c` 键进入校准模式: +1. 让一个人站在已知距离处 +2. 输入实际距离 +3. 系统自动调整计算参数 + +### 朝向校准 🧭 + +使用 `h` 键设置准确朝向: +1. 确定摄像头实际朝向(使用指南针) +2. 输入角度(0°为正北,90°为正东) + +### 位置校准 📍 + +确保摄像头GPS坐标准确: +1. 使用手机GPS应用获取精确坐标 +2. 运行 `setup_camera_location.py` 更新配置 + +## 故障排除 + +### 地图无法打开 + +1. 检查网络连接 +2. 确认高德API Key配置正确 +3. 尝试手动访问生成的HTML文件 + +### 人员位置不准确 + +1. 重新校准距离参数 +2. 检查摄像头朝向设置 +3. 确认摄像头GPS坐标准确 + +### 地图显示异常 + +1. 刷新浏览器页面 +2. 清除浏览器缓存 +3. 检查JavaScript控制台错误信息 + +## 技术细节 + +### 坐标转换公式 + +系统使用WGS84坐标系和球面几何学公式: + +```python +# 球面距离计算 +lat2 = asin(sin(lat1) * cos(d/R) + cos(lat1) * sin(d/R) * cos(bearing)) +lng2 = lng1 + atan2(sin(bearing) * sin(d/R) * cos(lat1), cos(d/R) - sin(lat1) * sin(lat2)) +``` + +### 视场角映射 + +```python +# 像素到角度的转换 +horizontal_angle_per_pixel = camera_fov / frame_width +horizontal_offset = (pixel_x - center_x) * horizontal_angle_per_pixel +``` + +## 系统要求 + +- Python 3.7+ +- OpenCV 4.0+ +- 网络连接(地图加载) +- 现代浏览器(Chrome/Firefox/Edge) + +## 注意事项 + +⚠️ **重要提醒**: +- 本系统仅供技术研究使用 +- 实际部署需要考虑隐私保护 +- GPS坐标精度影响最终定位准确性 +- 距离计算基于单目视觉,存在一定误差 + +## 更新日志 + +- v1.0.0 - 基础地图显示功能 +- v1.1.0 - 添加实时人员位置标记 +- v1.2.0 - 优化坐标计算精度 +- v1.3.0 - 增加配置工具和用户指南 \ No newline at end of file diff --git a/distance-judgement/MOBILE_GUIDE.md b/distance-judgement/MOBILE_GUIDE.md new file mode 100644 index 00000000..ec650d7c --- /dev/null +++ b/distance-judgement/MOBILE_GUIDE.md @@ -0,0 +1,246 @@ +# 📱 手机连接功能使用指南 + +## 🚁 无人机战场态势感知系统 - 手机扩展功能 + +这个功能允许你使用手机作为移动侦察设备,将手机摄像头图像、GPS位置和设备信息实时传输到指挥中心,扩展战场态势感知能力。 + +## 🌟 功能特性 + +### 📡 数据传输 +- **实时视频流**: 传输手机摄像头画面到指挥中心 +- **GPS定位**: 自动获取和传输手机的精确位置 +- **设备状态**: 监控电池电量、信号强度等 +- **人体检测**: 在手机端进行AI人体检测 +- **地图集成**: 检测结果自动显示在指挥中心地图上 + +### 🛡️ 技术特点 +- **低延迟传输**: 优化的数据压缩和传输协议 +- **自动重连**: 网络中断后自动重新连接 +- **多设备支持**: 支持多台手机同时连接 +- **跨平台兼容**: 支持Android、iOS等主流移动设备 + +## 🚀 快速开始 + +### 1. 启动服务端 + +#### 方法一:使用Web模式(推荐) +```bash +python run.py +# 选择 "1. Web模式" +# 在Web界面中点击"启用手机模式" +``` + +#### 方法二:直接启动Web服务器 +```bash +python main_web.py +``` + +### 2. 配置网络连接 + +确保手机和电脑在同一网络环境下: +- **局域网连接**: 连接同一WiFi网络 +- **热点模式**: 电脑开启热点,手机连接 +- **有线网络**: 电脑有线连接,手机连WiFi + +### 3. 获取服务器IP地址 + +在电脑上查看IP地址: + +**Windows:** +```cmd +ipconfig +``` + +**Linux/Mac:** +```bash +ifconfig +# 或 +ip addr show +``` + +记下显示的IP地址(如 192.168.1.100) + +### 4. 手机端连接 + +#### 方法一:使用浏览器(推荐) +1. 打开手机浏览器 +2. 访问 `http://[服务器IP]:5000/mobile/mobile_client.html` +3. 例如:`http://192.168.1.100:5000/mobile/mobile_client.html` + +#### 方法二:直接访问HTML文件 +1. 将 `mobile/mobile_client.html` 复制到手机 +2. 在文件中修改服务器IP地址 +3. 用浏览器打开HTML文件 + +### 5. 开始传输 +1. 在手机页面中点击"开始传输" +2. 允许摄像头和位置权限 +3. 查看连接状态指示灯变绿 +4. 在指挥中心Web界面查看实时数据 + +## 📱 手机端界面说明 + +### 状态面板 +- **📍 GPS坐标**: 显示当前精确位置 +- **🔋 电池电量**: 实时电池状态 +- **🌐 连接状态**: 与服务器的连接状态 + +### 控制按钮 +- **📹 开始传输**: 启动数据传输 +- **⏹️ 停止传输**: 停止传输 +- **🔄 重连**: 重新连接服务器 + +### 统计信息 +- **📊 已发送帧数**: 传输的图像帧数量 +- **📈 数据量**: 累计传输的数据量 + +### 日志面板 +- 显示详细的操作日志和错误信息 +- 帮助诊断连接问题 + +## 🖥️ 服务端管理 + +### Web界面控制 +访问 `http://localhost:5000` 查看: +- **地图显示**: 实时显示手机位置和检测结果 +- **设备管理**: 查看连接的手机列表 +- **数据统计**: 查看传输统计信息 + +### API接口 +- `GET /api/mobile/devices` - 获取连接设备列表 +- `POST /api/mobile/toggle` - 切换手机模式开关 +- `POST /mobile/ping` - 手机连接测试 +- `POST /mobile/upload` - 接收手机数据 + +### 命令行监控 +服务器控制台会显示详细日志: +``` +📱 新设备连接: iPhone (mobile_12) +📍 设备 mobile_12 位置更新: (39.904200, 116.407400) +🎯 检测到 2 个人 +📍 手机检测人员 1: 距离5.2m, 坐标(39.904250, 116.407450) +``` + +## ⚙️ 高级配置 + +### 修改传输参数 + +在手机端HTML文件中可以调整: + +```javascript +// 修改服务器地址 +this.serverHost = '192.168.1.100'; +this.serverPort = 5000; + +// 修改传输频率(毫秒) +const interval = 1000; // 1秒传输一次 + +// 修改图像质量(0.1-1.0) +const frameData = this.canvas.toDataURL('image/jpeg', 0.5); +``` + +### 网络优化 + +**低带宽环境:** +- 降低图像质量 (0.3-0.5) +- 增加传输间隔 (2-5秒) +- 减小图像分辨率 + +**高质量需求:** +- 提高图像质量 (0.7-0.9) +- 减少传输间隔 (0.5-1秒) +- 使用更高分辨率 + +## 🔧 故障排除 + +### 常见问题 + +#### 1. 手机无法连接服务器 +- **检查网络**: 确保在同一网络 +- **检查IP地址**: 确认服务器IP正确 +- **检查防火墙**: 关闭防火墙或开放端口 +- **检查端口**: 确认5000端口未被占用 + +#### 2. 摄像头无法访问 +- **权限设置**: 在浏览器中允许摄像头权限 +- **HTTPS需求**: 某些浏览器需要HTTPS才能访问摄像头 +- **设备占用**: 关闭其他使用摄像头的应用 + +#### 3. GPS定位失败 +- **位置权限**: 允许浏览器访问位置信息 +- **网络连接**: 确保网络连接正常 +- **室内环境**: 移动到有GPS信号的位置 + +#### 4. 传输断开 +- **网络稳定性**: 检查WiFi信号强度 +- **服务器状态**: 确认服务器正常运行 +- **自动重连**: 等待自动重连或手动重连 + +### 调试方法 + +#### 手机端调试 +1. 打开浏览器开发者工具 (F12) +2. 查看Console面板的错误信息 +3. 检查Network面板的网络请求 + +#### 服务端调试 +1. 查看控制台输出的日志信息 +2. 使用 `python tests/test_system.py` 测试系统 +3. 检查网络连接和端口状态 + +## 🌐 网络配置示例 + +### 局域网配置 +``` +电脑 (192.168.1.100) ←→ 路由器 ←→ 手机 (192.168.1.101) +``` + +### 热点配置 +``` +电脑热点 (192.168.137.1) ←→ 手机 (192.168.137.2) +``` + +### 有线+WiFi配置 +``` +电脑 (有线: 192.168.1.100) ←→ 路由器 ←→ 手机 (WiFi: 192.168.1.101) +``` + +## 📊 性能建议 + +### 推荐配置 +- **网络**: WiFi 5GHz频段,带宽 ≥ 10Mbps +- **手机**: RAM ≥ 4GB,Android 8+ / iOS 12+ +- **服务器**: 双核CPU,RAM ≥ 4GB + +### 优化设置 +- **高质量模式**: 0.7质量,1秒间隔 +- **平衡模式**: 0.5质量,1秒间隔(推荐) +- **省流量模式**: 0.3质量,2秒间隔 + +## 🚁 实战应用场景 + +### 军用场景 +- **前线侦察**: 士兵携带手机进行前方侦察 +- **多点监控**: 多个观察点同时传输情报 +- **指挥决策**: 指挥部实时获取战场态势 + +### 民用场景 +- **安保监控**: 保安巡逻时实时传输画面 +- **应急救援**: 救援人员现场情况汇报 +- **活动监管**: 大型活动现场监控 + +### 技术演示 +- **远程教学**: 实地教学直播 +- **技术展示**: 产品演示和技术验证 + +--- + +## 📞 技术支持 + +如有问题,请: +1. 查看控制台日志信息 +2. 运行系统测试脚本 +3. 检查网络配置 +4. 参考故障排除指南 + +这个手机连接功能大大扩展了战场态势感知系统的应用场景,让移动侦察成为可能! \ No newline at end of file diff --git a/distance-judgement/__pycache__/config.cpython-311.pyc b/distance-judgement/__pycache__/config.cpython-311.pyc new file mode 100644 index 00000000..24bd06e8 Binary files /dev/null and b/distance-judgement/__pycache__/config.cpython-311.pyc differ diff --git a/distance-judgement/__pycache__/demo_map.cpython-311.pyc b/distance-judgement/__pycache__/demo_map.cpython-311.pyc new file mode 100644 index 00000000..fab59c43 Binary files /dev/null and b/distance-judgement/__pycache__/demo_map.cpython-311.pyc differ diff --git a/distance-judgement/__pycache__/distance_calculator.cpython-311.pyc b/distance-judgement/__pycache__/distance_calculator.cpython-311.pyc new file mode 100644 index 00000000..cc94bd0e Binary files /dev/null and b/distance-judgement/__pycache__/distance_calculator.cpython-311.pyc differ diff --git a/distance-judgement/__pycache__/main.cpython-311.pyc b/distance-judgement/__pycache__/main.cpython-311.pyc new file mode 100644 index 00000000..ee040a15 Binary files /dev/null and b/distance-judgement/__pycache__/main.cpython-311.pyc differ diff --git a/distance-judgement/__pycache__/main_web.cpython-311.pyc b/distance-judgement/__pycache__/main_web.cpython-311.pyc new file mode 100644 index 00000000..44b68aa7 Binary files /dev/null and b/distance-judgement/__pycache__/main_web.cpython-311.pyc differ diff --git a/distance-judgement/__pycache__/main_web.cpython-39.pyc b/distance-judgement/__pycache__/main_web.cpython-39.pyc new file mode 100644 index 00000000..71f0194d Binary files /dev/null and b/distance-judgement/__pycache__/main_web.cpython-39.pyc differ diff --git a/distance-judgement/__pycache__/person_detector.cpython-311.pyc b/distance-judgement/__pycache__/person_detector.cpython-311.pyc new file mode 100644 index 00000000..71709859 Binary files /dev/null and b/distance-judgement/__pycache__/person_detector.cpython-311.pyc differ diff --git a/distance-judgement/create_cert.bat b/distance-judgement/create_cert.bat new file mode 100644 index 00000000..0519ecba --- /dev/null +++ b/distance-judgement/create_cert.bat @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/distance-judgement/create_simple_cert.py b/distance-judgement/create_simple_cert.py new file mode 100644 index 00000000..d5dbd7a5 --- /dev/null +++ b/distance-judgement/create_simple_cert.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +使用OpenSSL命令行工具创建简单的自签名证书 +不依赖Python的cryptography库 +""" + +import os +import subprocess +import sys + +def create_ssl_dir(): + """创建ssl目录""" + if not os.path.exists("ssl"): + os.makedirs("ssl") + print("✅ 创建ssl目录") + +def create_certificate_with_openssl(): + """使用OpenSSL命令创建证书""" + print("🔑 使用OpenSSL创建自签名证书...") + + # 检查OpenSSL是否可用 + try: + subprocess.run(["openssl", "version"], check=True, capture_output=True) + except (subprocess.CalledProcessError, FileNotFoundError): + print("❌ OpenSSL未安装或不在PATH中") + print("📝 请安装OpenSSL或使用其他方法") + return False + + # 创建私钥 + key_cmd = [ + "openssl", "genrsa", + "-out", "ssl/key.pem", + "2048" + ] + + # 创建证书 + cert_cmd = [ + "openssl", "req", "-new", "-x509", + "-key", "ssl/key.pem", + "-out", "ssl/cert.pem", + "-days", "365", + "-subj", "/C=CN/ST=Beijing/L=Beijing/O=Distance System/CN=localhost" + ] + + try: + print(" 生成私钥...") + subprocess.run(key_cmd, check=True, capture_output=True) + + print(" 生成证书...") + subprocess.run(cert_cmd, check=True, capture_output=True) + + print("✅ SSL证书创建成功!") + print(" 🔑 私钥: ssl/key.pem") + print(" 📜 证书: ssl/cert.pem") + return True + + except subprocess.CalledProcessError as e: + print(f"❌ OpenSSL命令执行失败: {e}") + return False + +def create_certificate_manual(): + """提供手动创建证书的说明""" + print("📝 手动创建SSL证书说明:") + print() + print("方法1 - 使用在线工具:") + print(" 访问: https://www.selfsignedcertificate.com/") + print(" 下载证书文件并重命名为 cert.pem 和 key.pem") + print() + print("方法2 - 使用Git Bash (Windows):") + print(" 打开Git Bash,进入项目目录,执行:") + print(" openssl genrsa -out ssl/key.pem 2048") + print(" openssl req -new -x509 -key ssl/key.pem -out ssl/cert.pem -days 365") + print() + print("方法3 - 暂时使用HTTP:") + print(" 运行: python main_web.py") + print(" 注意: HTTP模式下手机摄像头可能无法使用") + +def main(): + """主函数""" + create_ssl_dir() + + # 检查证书是否已存在 + if os.path.exists("ssl/cert.pem") and os.path.exists("ssl/key.pem"): + print("✅ SSL证书已存在") + return + + print("🔍 尝试创建SSL证书...") + + # 尝试使用OpenSSL + if create_certificate_with_openssl(): + return + + # 提供手动创建说明 + create_certificate_manual() + + \ No newline at end of file diff --git a/distance-judgement/demo_mobile.py b/distance-judgement/demo_mobile.py new file mode 100644 index 00000000..1e36a4ee --- /dev/null +++ b/distance-judgement/demo_mobile.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +手机连接功能演示脚本 +展示如何使用手机作为移动侦察设备 +""" + +import time +import json +import base64 +import requests +from src import MobileConnector, config + +def demo_mobile_functionality(): + """演示手机连接功能""" + print("📱 手机连接功能演示") + print("=" * 60) + + print("🎯 演示内容:") + print("1. 启动手机连接服务器") + print("2. 模拟手机客户端连接") + print("3. 发送模拟数据") + print("4. 展示数据处理流程") + print() + + # 创建手机连接器 + mobile_connector = MobileConnector(port=8080) + + print("📱 正在启动手机连接服务器...") + if mobile_connector.start_server(): + print("✅ 手机连接服务器启动成功") + print(f"🌐 等待手机客户端连接到端口 8080") + print() + + print("📖 使用说明:") + print("1. 确保手机和电脑在同一网络") + print("2. 在手机浏览器中访问:") + print(" http://[电脑IP]:5000/mobile/mobile_client.html") + print("3. 或者直接打开 mobile/mobile_client.html 文件") + print("4. 点击'开始传输'按钮") + print() + + print("🔧 获取电脑IP地址的方法:") + print("Windows: ipconfig") + print("Linux/Mac: ifconfig 或 ip addr show") + print() + + # 设置回调函数来显示接收的数据 + def on_frame_received(device_id, frame, device): + print(f"📷 收到设备 {device_id[:8]} 的图像帧") + print(f" 分辨率: {frame.shape[1]}x{frame.shape[0]}") + print(f" 设备: {device.device_name}") + + def on_location_received(device_id, location, device): + lat, lng, accuracy = location + print(f"📍 收到设备 {device_id[:8]} 的位置信息") + print(f" 坐标: ({lat:.6f}, {lng:.6f})") + print(f" 精度: {accuracy}m") + + def on_device_event(event_type, device): + if event_type == 'device_connected': + print(f"📱 设备连接: {device.device_name} ({device.device_id[:8]})") + print(f" 电池: {device.battery_level}%") + elif event_type == 'device_disconnected': + print(f"📱 设备断开: {device.device_name} ({device.device_id[:8]})") + + # 注册回调函数 + mobile_connector.add_frame_callback(on_frame_received) + mobile_connector.add_location_callback(on_location_received) + mobile_connector.add_device_callback(on_device_event) + + print("⏳ 等待手机连接... (按 Ctrl+C 退出)") + + try: + # 监控连接状态 + while True: + time.sleep(5) + + # 显示统计信息 + stats = mobile_connector.get_statistics() + online_devices = mobile_connector.get_online_devices() + + if stats['online_devices'] > 0: + print(f"\n📊 连接统计:") + print(f" 在线设备: {stats['online_devices']}") + print(f" 接收帧数: {stats['frames_received']}") + print(f" 数据量: {stats['data_received_mb']:.2f} MB") + print(f" 平均帧率: {stats['avg_frames_per_second']:.1f} FPS") + + print(f"\n📱 在线设备:") + for device in online_devices: + print(f" • {device.device_name} ({device.device_id[:8]})") + print(f" 电池: {device.battery_level}%") + if device.current_location: + lat, lng, acc = device.current_location + print(f" 位置: ({lat:.6f}, {lng:.6f})") + else: + print("⏳ 等待设备连接...") + + except KeyboardInterrupt: + print("\n🔴 用户中断") + + finally: + mobile_connector.stop_server() + print("📱 手机连接服务器已停止") + + else: + print("❌ 手机连接服务器启动失败") + print("💡 可能的原因:") + print(" - 端口 8080 已被占用") + print(" - 网络权限问题") + print(" - 防火墙阻止连接") + +def test_mobile_api(): + """测试手机相关API""" + print("\n🧪 测试手机API接口") + print("=" * 40) + + base_url = "http://127.0.0.1:5000" + + try: + # 测试ping接口 + test_data = {"device_id": "test_device_123"} + response = requests.post(f"{base_url}/mobile/ping", + json=test_data, timeout=5) + + if response.status_code == 200: + data = response.json() + print("✅ Ping API测试成功") + print(f" 服务器时间: {data.get('server_time')}") + else: + print(f"❌ Ping API测试失败: HTTP {response.status_code}") + + except requests.exceptions.ConnectionError: + print("⚠️ 无法连接到Web服务器") + print("💡 请先启动Web服务器: python main_web.py") + + except Exception as e: + print(f"❌ API测试出错: {e}") + +def show_mobile_guide(): + """显示手机连接指南""" + print("\n📖 手机连接步骤指南") + print("=" * 40) + + print("1️⃣ 启动服务端:") + print(" python main_web.py") + print(" 或 python run.py (选择Web模式)") + print() + + print("2️⃣ 获取电脑IP地址:") + print(" Windows: 打开CMD,输入 ipconfig") + print(" Mac/Linux: 打开终端,输入 ifconfig") + print(" 记下IP地址,如: 192.168.1.100") + print() + + print("3️⃣ 手机端连接:") + print(" 方法1: 浏览器访问 http://[IP]:5000/mobile/mobile_client.html") + print(" 方法2: 直接打开 mobile/mobile_client.html 文件") + print() + + print("4️⃣ 开始传输:") + print(" • 允许摄像头和位置权限") + print(" • 点击'开始传输'按钮") + print(" • 查看连接状态指示灯") + print() + + print("5️⃣ 查看结果:") + print(" • 在电脑Web界面查看地图") + print(" • 观察实时检测结果") + print(" • 监控设备状态") + +if __name__ == "__main__": + print("🚁 无人机战场态势感知系统 - 手机连接演示") + print("=" * 60) + + while True: + print("\n选择演示内容:") + print("1. 📱 启动手机连接服务器") + print("2. 🧪 测试手机API接口") + print("3. 📖 查看连接指南") + print("0. ❌ 退出") + + try: + choice = input("\n请输入选择 (0-3): ").strip() + + if choice == "1": + demo_mobile_functionality() + elif choice == "2": + test_mobile_api() + elif choice == "3": + show_mobile_guide() + elif choice == "0": + print("👋 再见!") + break + else: + print("❌ 无效选择,请重新输入") + + except KeyboardInterrupt: + print("\n👋 再见!") + break + except Exception as e: + print(f"❌ 出错: {e}") + + input("\n按回车键继续...") \ No newline at end of file diff --git a/distance-judgement/get_ip.py b/distance-judgement/get_ip.py new file mode 100644 index 00000000..cfc745f9 --- /dev/null +++ b/distance-judgement/get_ip.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import socket + +def get_local_ip(): + """获取本机IP地址""" + try: + # 创建一个socket连接来获取本机IP + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("8.8.8.8", 80)) + ip = s.getsockname()[0] + s.close() + return ip + except: + try: + # 备用方法 + import subprocess + result = subprocess.run(['ipconfig'], capture_output=True, text=True, shell=True) + lines = result.stdout.split('\n') + for line in lines: + if 'IPv4' in line and '192.168' in line: + return line.split(':')[-1].strip() + except: + return '127.0.0.1' + +if __name__ == "__main__": + ip = get_local_ip() + print(f"🌐 服务器地址信息") + print(f"="*50) + print(f"本机IP地址: {ip}") + print(f"主页面地址: http://{ip}:5000/") + print(f"移动客户端: http://{ip}:5000/mobile/mobile_client.html") + print(f"GPS测试页面: http://{ip}:5000/mobile/gps_test.html") + print(f"设备选择测试: http://{ip}:5000/test_device_selector.html") + print(f"="*50) + print(f"�� 手机/平板请访问移动客户端地址!") \ No newline at end of file diff --git a/distance-judgement/main.py b/distance-judgement/main.py new file mode 100644 index 00000000..c1429b85 --- /dev/null +++ b/distance-judgement/main.py @@ -0,0 +1,261 @@ +import cv2 +import time +import numpy as np +from src import PersonDetector, DistanceCalculator, MapManager, config + +class RealTimePersonDistanceDetector: + def __init__(self): + self.detector = PersonDetector() + self.distance_calculator = DistanceCalculator() + self.cap = None + self.fps_counter = 0 + self.fps_time = time.time() + self.current_fps = 0 + + # 初始化地图管理器 + if config.ENABLE_MAP_DISPLAY: + self.map_manager = MapManager( + api_key=config.GAODE_API_KEY, + camera_lat=config.CAMERA_LATITUDE, + camera_lng=config.CAMERA_LONGITUDE + ) + self.map_manager.set_camera_position( + config.CAMERA_LATITUDE, + config.CAMERA_LONGITUDE, + config.CAMERA_HEADING + ) + print("🗺️ 地图管理器已初始化") + else: + self.map_manager = None + + def initialize_camera(self): + """初始化摄像头""" + self.cap = cv2.VideoCapture(config.CAMERA_INDEX) + if not self.cap.isOpened(): + raise Exception(f"无法开启摄像头 {config.CAMERA_INDEX}") + + # 设置摄像头参数 + self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, config.FRAME_WIDTH) + self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, config.FRAME_HEIGHT) + self.cap.set(cv2.CAP_PROP_FPS, config.FPS) + + # 获取实际设置的参数 + actual_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + actual_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + actual_fps = int(self.cap.get(cv2.CAP_PROP_FPS)) + + print(f"摄像头初始化成功:") + print(f" 分辨率: {actual_width}x{actual_height}") + print(f" 帧率: {actual_fps} FPS") + + def calculate_fps(self): + """计算实际帧率""" + self.fps_counter += 1 + current_time = time.time() + if current_time - self.fps_time >= 1.0: + self.current_fps = self.fps_counter + self.fps_counter = 0 + self.fps_time = current_time + + def draw_info_panel(self, frame, person_count=0): + """绘制信息面板""" + height, width = frame.shape[:2] + + # 绘制顶部信息栏 + info_height = 60 + cv2.rectangle(frame, (0, 0), (width, info_height), (0, 0, 0), -1) + + # 显示FPS + fps_text = f"FPS: {self.current_fps}" + cv2.putText(frame, fps_text, (10, 25), config.FONT, 0.6, (0, 255, 0), 2) + + # 显示人员计数 + person_text = f"Persons: {person_count}" + cv2.putText(frame, person_text, (150, 25), config.FONT, 0.6, (0, 255, 255), 2) + + # 显示模型信息 + model_text = self.detector.get_model_info() + cv2.putText(frame, model_text, (10, 45), config.FONT, 0.5, (255, 255, 255), 1) + + # 显示操作提示 + help_text = "Press 'q' to quit | 'c' to calibrate | 'r' to reset | 'm' to open map" + text_size = cv2.getTextSize(help_text, config.FONT, 0.5, 1)[0] + cv2.putText(frame, help_text, (width - text_size[0] - 10, 25), + config.FONT, 0.5, (255, 255, 0), 1) + + # 显示地图状态 + if self.map_manager: + map_status = "Map: ON" + cv2.putText(frame, map_status, (10, height - 10), + config.FONT, 0.5, (0, 255, 255), 1) + + return frame + + def calibrate_distance(self, detections): + """距离校准模式""" + if len(detections) == 0: + print("未检测到人体,无法校准") + return + + print("\n=== 距离校准模式 ===") + print("请确保画面中有一个人,并输入该人距离摄像头的真实距离") + + try: + real_distance = float(input("请输入真实距离(厘米): ")) + + # 使用第一个检测到的人进行校准 + detection = detections[0] + x1, y1, x2, y2, conf = detection + bbox_height = y2 - y1 + + # 更新参考参数 + config.REFERENCE_DISTANCE = real_distance + config.REFERENCE_HEIGHT_PIXELS = bbox_height + + # 重新初始化距离计算器 + self.distance_calculator = DistanceCalculator() + + print(f"校准完成!") + print(f"参考距离: {real_distance}cm") + print(f"参考像素高度: {bbox_height}px") + + except ValueError: + print("输入无效,校准取消") + except Exception as e: + print(f"校准失败: {e}") + + def process_frame(self, frame): + """处理单帧图像""" + # 检测人体 + detections = self.detector.detect_persons(frame) + + # 计算距离并更新地图位置 + distances = [] + if self.map_manager: + self.map_manager.clear_persons() + + for i, detection in enumerate(detections): + bbox = detection[:4] # [x1, y1, x2, y2] + x1, y1, x2, y2 = bbox + distance = self.distance_calculator.get_distance(bbox) + distance_str = self.distance_calculator.format_distance(distance) + distances.append(distance_str) + + # 更新地图上的人员位置 + if self.map_manager: + # 计算人体中心点 + center_x = (x1 + x2) / 2 + center_y = (y1 + y2) / 2 + + # 将距离从厘米转换为米 + distance_meters = distance / 100.0 + + # 添加到地图 + self.map_manager.add_person_position( + center_x, center_y, distance_meters, + frame.shape[1], frame.shape[0], # width, height + f"P{i+1}" + ) + + # 绘制检测结果 + frame = self.detector.draw_detections(frame, detections, distances) + + # 绘制信息面板 + frame = self.draw_info_panel(frame, len(detections)) + + # 计算FPS + self.calculate_fps() + + return frame, detections + + def run(self): + """运行主程序""" + try: + print("正在初始化...") + self.initialize_camera() + + print("系统启动成功!") + print("操作说明:") + print(" - 按 'q' 键退出程序") + print(" - 按 'c' 键进入距离校准模式") + print(" - 按 'r' 键重置为默认参数") + print(" - 按 's' 键保存当前帧") + if self.map_manager: + print(" - 按 'm' 键打开地图显示") + print(" - 按 'h' 键设置摄像头朝向") + print("\n开始实时检测...") + + frame_count = 0 + + while True: + ret, frame = self.cap.read() + if not ret: + print("无法读取摄像头画面") + break + + # 处理帧 + processed_frame, detections = self.process_frame(frame) + + # 显示结果 + cv2.imshow('Real-time Person Distance Detection', processed_frame) + + # 处理按键 + key = cv2.waitKey(1) & 0xFF + + if key == ord('q'): + print("用户退出程序") + break + elif key == ord('c'): + # 校准模式 + self.calibrate_distance(detections) + elif key == ord('r'): + # 重置参数 + print("重置为默认参数") + self.distance_calculator = DistanceCalculator() + elif key == ord('s'): + # 保存当前帧 + filename = f"capture_{int(time.time())}.jpg" + cv2.imwrite(filename, processed_frame) + print(f"已保存截图: {filename}") + elif key == ord('m') and self.map_manager: + # 打开地图显示 + print("正在打开地图...") + self.map_manager.open_map() + elif key == ord('h') and self.map_manager: + # 设置摄像头朝向 + try: + heading = float(input("请输入摄像头朝向角度 (0-360°, 0为正北): ")) + if 0 <= heading <= 360: + self.map_manager.update_camera_heading(heading) + else: + print("角度必须在0-360度之间") + except ValueError: + print("输入无效") + + frame_count += 1 + + except KeyboardInterrupt: + print("\n程序被用户中断") + except Exception as e: + print(f"程序运行出错: {e}") + finally: + self.cleanup() + + def cleanup(self): + """清理资源""" + if self.cap: + self.cap.release() + cv2.destroyAllWindows() + print("资源已清理,程序结束") + +def main(): + """主函数""" + print("=" * 50) + print("实时人体距离检测系统") + print("=" * 50) + + detector = RealTimePersonDistanceDetector() + detector.run() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/distance-judgement/main_web.py b/distance-judgement/main_web.py new file mode 100644 index 00000000..e2b698c0 --- /dev/null +++ b/distance-judgement/main_web.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +无人机战场态势感知系统 - Web版本 +先显示地图界面,通过按钮控制摄像头启动和显示 +""" + +import sys +import os +from src import WebServer, config + +def main(): + """主函数""" + global config # 声明 config 为全局变量 + + print("=" * 60) + print("🚁 无人机战场态势感知系统 - Web版本") + print("=" * 60) + print() + + # 检查配置 + print("📋 系统配置检查...") + print(f"📍 摄像头位置: ({config.CAMERA_LATITUDE:.6f}, {config.CAMERA_LONGITUDE:.6f})") + print(f"🧭 摄像头朝向: {config.CAMERA_HEADING}°") + print(f"🔑 API Key: {'已配置' if config.GAODE_API_KEY != 'your_gaode_api_key_here' else '未配置'}") + print() + + if config.GAODE_API_KEY == "your_gaode_api_key_here": + print("⚠️ 警告: 未配置高德地图API Key") + print(" 地图功能可能受限,建议运行 setup_camera_location.py 进行配置") + print() + + # 检查是否为默认配置,提供自动配置选项 + if (config.CAMERA_LATITUDE == 39.9042 and + config.CAMERA_LONGITUDE == 116.4074 and + config.CAMERA_HEADING == 0): + print("🤖 检测到摄像头使用默认配置") + print(" 是否要自动配置摄像头位置和朝向?") + print(" • 输入 'y' - 立即自动配置") + print(" • 输入 'n' - 跳过,使用Web界面配置") + print(" • 直接回车 - 跳过自动配置") + print() + + try: + choice = input("🔧 请选择 (y/n/回车): ").strip().lower() + + if choice == 'y': + print("\n🚀 启动自动配置...") + from src.orientation_detector import OrientationDetector + + detector = OrientationDetector() + result = detector.auto_configure_camera_location() + + if result['success']: + print(f"✅ 自动配置成功!") + print(f"📍 新位置: ({result['gps_location'][0]:.6f}, {result['gps_location'][1]:.6f})") + print(f"🧭 新朝向: {result['camera_heading']:.1f}°") + + apply_choice = input("\n🔧 是否应用此配置? (y/n): ").strip().lower() + if apply_choice == 'y': + detector.update_camera_config( + result['gps_location'], + result['camera_heading'] + ) + print("✅ 配置已应用!") + + # 重新加载配置模块 + import importlib + import src.config + importlib.reload(src.config) + + # 更新全局 config 变量 + config = src.config + else: + print("⏭️ 配置未应用,将使用原配置") + else: + print("❌ 自动配置失败,将使用默认配置") + print("💡 可以在Web界面启动后使用自动配置功能") + + print() + elif choice == 'n': + print("⏭️ 已跳过自动配置") + print("💡 提示: 系统启动后可在Web界面使用自动配置功能") + print() + else: + print("⏭️ 已跳过自动配置") + print() + + except KeyboardInterrupt: + print("\n⏭️ 已跳过自动配置") + print() + except Exception as e: + print(f"⚠️ 自动配置过程出错: {e}") + print("💡 将使用默认配置,可在Web界面手动配置") + print() + + # 系统介绍 + print("🎯 系统功能:") + print(" • 🗺️ 实时地图显示") + print(" • 📷 摄像头控制(Web界面)") + print(" • 👥 人员检测和定位") + print(" • 📏 距离测量") + print(" • 🌐 Web界面操作") + print() + + print("💡 使用说明:") + print(" 1. 系统启动后会自动打开浏览器") + print(" 2. 在地图界面点击 '启动视频侦察' 按钮") + print(" 3. 右上角会显示摄像头小窗口") + print(" 4. 检测到的人员会在地图上用红点标记") + print(" 5. 点击 '停止侦察' 按钮停止检测") + print() + + try: + # 创建并启动Web服务器 + print("🌐 正在启动Web服务器...") + web_server = WebServer() + + # 获取本机IP地址用于移动设备连接 + import socket + try: + # 连接到一个远程地址来获取本机IP + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("8.8.8.8", 80)) + local_ip = s.getsockname()[0] + s.close() + except: + local_ip = "127.0.0.1" + + # 启动服务器 + print("✅ 系统已启动!") + print(f"🔒 本地访问: https://127.0.0.1:5000") + print(f"🔒 手机/平板访问: https://{local_ip}:5000") + print(f"📱 手机客户端: https://{local_ip}:5000/mobile/mobile_client.html") + print("🔴 按 Ctrl+C 停止服务器") + print() + print("🔑 HTTPS注意事项:") + print(" • 首次访问会显示'您的连接不是私密连接'警告") + print(" • 点击'高级'->'继续访问localhost(不安全)'即可") + print(" • 手机访问时也需要点击'继续访问'") + print() + + # 尝试自动打开浏览器 + try: + import webbrowser + webbrowser.open('https://127.0.0.1:5000') + print("🌐 浏览器已自动打开") + except: + print("⚠️ 无法自动打开浏览器,请手动访问地址") + + print("-" * 60) + + # 运行服务器,绑定到所有网络接口,启用HTTPS + web_server.run(host='0.0.0.0', port=5000, debug=False, ssl_enabled=True) + + except KeyboardInterrupt: + print("\n🔴 用户中断程序") + except Exception as e: + print(f"❌ 程序运行出错: {e}") + print("💡 建议检查:") + print(" 1. 是否正确安装了所有依赖包") + print(" 2. 摄像头是否正常工作") + print(" 3. 网络连接是否正常") + sys.exit(1) + finally: + print("👋 程序已结束") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/distance-judgement/main_web_simple_https.py b/distance-judgement/main_web_simple_https.py new file mode 100644 index 00000000..77b91833 --- /dev/null +++ b/distance-judgement/main_web_simple_https.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +简化版HTTPS Web服务器 +使用Python内置ssl模块,无需额外依赖 +""" + +import ssl +import socket +from src.web_server import create_app +from get_ip import get_local_ip + +def create_simple_ssl_context(): + """创建简单的SSL上下文,使用自签名证书""" + context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + + # 检查是否存在SSL证书文件 + import os + cert_file = "ssl/cert.pem" + key_file = "ssl/key.pem" + + if not os.path.exists(cert_file) or not os.path.exists(key_file): + print("❌ SSL证书文件不存在") + print("📝 为了使用HTTPS,请选择以下选项之一:") + print(" 1. 安装cryptography库: pip install cryptography") + print(" 2. 使用HTTP版本: python main_web.py") + print(" 3. 手动创建SSL证书") + return None + + try: + context.load_cert_chain(cert_file, key_file) + return context + except Exception as e: + print(f"❌ 加载SSL证书失败: {e}") + return None + +def main(): + """启动简化版HTTPS服务器""" + print("🚀 启动简化版HTTPS服务器...") + + # 创建Flask应用 + app = create_app() + + # 获取本地IP + local_ip = get_local_ip() + + print(f"🌐 本地IP地址: {local_ip}") + print() + print("📱 访问地址:") + print(f" 桌面端: https://127.0.0.1:5000") + print(f" 手机端: https://{local_ip}:5000/mobile/mobile_client.html") + print() + print("⚠️ 如果看到安全警告,请点击 '高级' -> '继续访问'") + print() + + # 创建SSL上下文 + ssl_context = create_simple_ssl_context() + + if ssl_context is None: + print("🔄 回退到HTTP模式...") + print(f" 桌面端: http://127.0.0.1:5000") + print(f" 手机端: http://{local_ip}:5000/mobile/mobile_client.html") + app.run(host='0.0.0.0', port=5000, debug=True) + else: + print("🔒 HTTPS模式启动成功!") + app.run(host='0.0.0.0', port=5000, debug=True, ssl_context=ssl_context) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/distance-judgement/mobile/baidu_browser_test.html b/distance-judgement/mobile/baidu_browser_test.html new file mode 100644 index 00000000..4dca59f5 --- /dev/null +++ b/distance-judgement/mobile/baidu_browser_test.html @@ -0,0 +1,1442 @@ + + + + + + + 📱 百度浏览器摄像头测试 + + + + +

📱 百度浏览器摄像头测试

+

专门针对百度浏览器的摄像头API兼容性测试

+ + + + + + + + + + + +
+ +
点击按钮测试摄像头
+
+ +
+ +
+ 🚁 返回移动终端 +
+ + + + + \ No newline at end of file diff --git a/distance-judgement/mobile/browser_compatibility_guide.html b/distance-judgement/mobile/browser_compatibility_guide.html new file mode 100644 index 00000000..671aa68f --- /dev/null +++ b/distance-judgement/mobile/browser_compatibility_guide.html @@ -0,0 +1,410 @@ + + + + + + + 🌐 浏览器兼容性指南 + + + + +
+
+

🌐 浏览器兼容性指南

+

移动侦察终端摄像头功能兼容性说明与解决方案

+
+ +
+

📋 当前浏览器检测

+
+

正在检测您的浏览器兼容性...

+
+
+ +
+

🔍 "设备扫描失败: 浏览器不支持设备枚举功能" 问题说明

+ +
+

⚠️ 问题原因

+

这个错误表示您的浏览器不支持 navigator.mediaDevices.enumerateDevices() API,这个API用于列出可用的摄像头设备。

+
+ +
+

✅ 系统自动解决方案

+

我们的系统已经自动启用了兼容模式,为您提供以下设备选项:

+
    +
  • 📱 默认摄像头 - 使用系统默认摄像头
  • +
  • 📹 后置摄像头 - 尝试使用后置摄像头
  • +
  • 🤳 前置摄像头 - 尝试使用前置摄像头
  • +
+

您可以通过设备选择器逐个测试这些选项,找到适合的摄像头配置。

+
+
+ +
+

📱 浏览器兼容性列表

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
浏览器getUserMediaenumerateDevicesPermissions API总体支持
Chrome 53+✅ 完全支持✅ 完全支持✅ 完全支持推荐
Firefox 36+✅ 完全支持✅ 完全支持⚠️ 部分支持推荐
Safari 11+✅ 完全支持✅ 完全支持❌ 不支持⚠️ 基本可用
Edge 17+✅ 完全支持✅ 完全支持✅ 完全支持推荐
旧版浏览器⚠️ 需要前缀❌ 不支持❌ 不支持⚠️ 兼容模式
+
+ +
+

🔧 解决方案与建议

+ +

1. 最佳解决方案 - 升级浏览器

+
+

推荐使用以下现代浏览器:

+
    +
  • 🌐 Chrome 版本 53 或更高
  • +
  • 🦊 Firefox 版本 36 或更高
  • +
  • 🧭 Safari 版本 11 或更高(iOS/macOS)
  • +
  • Edge 版本 17 或更高
  • +
+
+ +

2. 兼容模式使用方法

+
+

如果无法升级浏览器,请按以下步骤操作:

+
    +
  1. 忽略"设备扫描失败"的提示
  2. +
  3. 点击"📷 选择设备"按钮
  4. +
  5. 在设备列表中选择"默认摄像头"、"后置摄像头"或"前置摄像头"
  6. +
  7. 点击"使用选中设备"测试摄像头功能
  8. +
  9. 如果某个选项不工作,尝试其他选项
  10. +
+
+ +

3. 移动设备特别说明

+
+

移动设备用户请注意:

+
    +
  • 📱 Android:建议使用 Chrome 浏览器
  • +
  • 🍎 iOS:建议使用 Safari 浏览器
  • +
  • 🔒 确保在 HTTPS 环境下访问(已自动配置)
  • +
  • 🎥 允许摄像头权限访问
  • +
+
+
+ +
+

🚨 常见问题排除

+ +
+
  • + + 完全无法访问摄像头 +
    检查浏览器是否支持getUserMedia,尝试升级浏览器或使用HTTPS访问 +
  • +
  • + + 无法枚举设备但能使用摄像头 +
    正常现象,使用兼容模式的默认设备选项即可 +
  • +
  • + + 权限被拒绝 +
    检查浏览器权限设置,清除网站数据后重新允许权限 +
  • +
  • + + 摄像头被占用 +
    关闭其他使用摄像头的应用程序或浏览器标签页 +
  • +
    +
    + +
    +

    🧪 测试工具

    +

    使用以下工具测试您的浏览器兼容性和摄像头功能:

    +
    + 📷 摄像头权限测试 + 🚁 返回移动终端 + +
    +
    +
    + + + + + \ No newline at end of file diff --git a/distance-judgement/mobile/camera_permission_test.html b/distance-judgement/mobile/camera_permission_test.html new file mode 100644 index 00000000..b2995a9e --- /dev/null +++ b/distance-judgement/mobile/camera_permission_test.html @@ -0,0 +1,504 @@ + + + + + + + 📷 摄像头权限测试 + + + + +
    +
    +

    📷 摄像头权限测试工具

    +

    全面测试摄像头权限获取方法的正确性

    +
    + +
    +

    🔍 1. 浏览器兼容性检查

    +
    等待测试...
    + +
    + +
    +

    🔐 2. 权限状态查询

    +
    等待测试...
    + +
    + +
    +

    📱 3. 设备枚举测试

    +
    等待测试...
    + +
    + +
    +

    🎥 4. 摄像头访问测试

    +
    等待测试...
    + + + +
    + +
    +

    📋 测试日志

    +
    + +
    +
    + + + + + \ No newline at end of file diff --git a/distance-judgement/mobile/gps_test.html b/distance-judgement/mobile/gps_test.html new file mode 100644 index 00000000..b5ebcf08 --- /dev/null +++ b/distance-judgement/mobile/gps_test.html @@ -0,0 +1,312 @@ + + + + + + + GPS连接测试 + + + + +
    +

    📍 GPS连接测试工具

    + +
    + 当前状态: +
    初始化中...
    +
    + +
    + GPS坐标: +
    等待获取...
    +
    + +
    + 服务器连接: +
    未测试
    +
    + +
    + + + + +
    + +
    + ⚠️ 重要提示:
    + • 现代浏览器在HTTP模式下可能限制GPS访问
    + • 请确保允许浏览器访问位置信息
    + • 在室外或窗边可获得更好的GPS信号
    + • 首次访问需要用户授权位置权限 +
    + +

    📋 操作日志

    +
    +
    + + + + + \ No newline at end of file diff --git a/distance-judgement/mobile/legacy_browser_help.html b/distance-judgement/mobile/legacy_browser_help.html new file mode 100644 index 00000000..59da0502 --- /dev/null +++ b/distance-judgement/mobile/legacy_browser_help.html @@ -0,0 +1,247 @@ + + + + + + + 📱 旧版浏览器使用指南 + + + + +
    +
    +

    📱 旧版浏览器使用指南

    +

    移动侦察终端兼容模式使用说明

    +
    + +
    +

    ⚠️ 检测结果

    +

    您的浏览器兼容性较低,但系统已自动启用兼容模式。请按照以下步骤操作:

    +
    + +
    +

    🔧 使用步骤

    + +
    + 1 + 返回主页面 +
    关闭此页面,返回移动侦察终端主界面 +
    + +
    + 2 + 查看系统状态 +
    确认页面显示"兼容模式:已为您的浏览器启用兼容支持" +
    + +
    + 3 + 选择摄像头设备 +
    点击页面中的"📷 选择设备"按钮 +
    + +
    + 4 + 测试设备选项 +
    在弹窗中选择以下任一设备进行测试: +
    +
    📱 默认摄像头 - 系统自动选择
    +
    📹 后置摄像头 - 优先使用后置
    +
    🤳 前置摄像头 - 优先使用前置
    +
    +
    + +
    + 5 + 启动摄像头 +
    选择设备后点击"✅ 使用选择的设备" +
    + +
    + 6 + 允许权限 +
    当浏览器弹出权限请求时,点击"允许" +
    + +
    + 7 + 开始使用 +
    摄像头启动成功后,点击"📹 开始传输" +
    +
    + +
    +

    🚨 常见问题

    + +

    Q: 权限被拒绝怎么办?

    +

    A: 清除浏览器数据,重新访问页面并允许权限

    + +

    Q: 某个设备选项不工作?

    +

    A: 尝试其他设备选项,通常至少有一个会工作

    + +

    Q: 完全无法使用摄像头?

    +

    A: 考虑升级浏览器或换用现代浏览器

    +
    + +
    +

    🌐 推荐浏览器

    +

    为获得最佳体验,建议升级到以下浏览器:

    + +
    + +
    +

    ✅ 重要提醒

    +

    兼容模式虽然功能有限,但基本的摄像头录制和GPS定位功能仍然可用。请耐心按步骤操作。

    +
    + +
    + 🚁 返回移动终端 + 📋 详细兼容性说明 +
    + +
    +

    技术说明:您的浏览器缺少现代Web API支持,但我们通过以下方式提供兼容:

    + +
    +
    + + + + + \ No newline at end of file diff --git a/distance-judgement/mobile/mobile_client.html b/distance-judgement/mobile/mobile_client.html new file mode 100644 index 00000000..b323ffbf --- /dev/null +++ b/distance-judgement/mobile/mobile_client.html @@ -0,0 +1,2874 @@ + + + + + + + 🚁 移动侦察终端 + + + + +
    + +
    +
    +

    🚁 移动侦察终端

    +
    正在初始化...
    + +
    + +
    +
    + 📍 GPS坐标 + 获取中... +
    +
    + 🧭 设备朝向 + 获取中... +
    +
    + 🔋 电池电量 + -- +
    +
    + 📶 信号强度 + -- +
    +
    + 🌐 连接状态 + 离线 +
    +
    + +
    +
    + 📹 实时视频监控 + 准备就绪 +
    + +
    + 点击"开始传输"启动视频监控 +
    +
    + +
    + + + + + + + + + + + +
    + + + +
    +
    +
    📊 已发送帧数
    +
    0
    +
    +
    +
    📈 数据量
    +
    0 KB
    +
    +
    + + +
    +

    🚀 性能优化建议

    +
    +
    📶 网络良好: 可选择 5-10 FPS + 高质量(70-90%)
    +
    📱 网络一般: 建议 2-5 FPS + 中质量(50-70%)
    +
    🐌 网络较慢: 选择 1-2 FPS + 低质量(30-50%)
    +
    💡 系统会自动监控网络状况并给出调整建议
    +
    +
    + +
    +
    系统初始化中...
    +
    + +
    +

    📍 GPS权限说明

    +

    如果GPS获取失败,请确保:

    + +
    +
    + + + + + \ No newline at end of file diff --git a/distance-judgement/mobile/permission_guide.html b/distance-judgement/mobile/permission_guide.html new file mode 100644 index 00000000..7078d29a --- /dev/null +++ b/distance-judgement/mobile/permission_guide.html @@ -0,0 +1,430 @@ + + + + + + + 📱 权限设置指南 + + + + +
    +

    📱 权限设置指南

    + +
    +

    📊 当前权限状态

    +
    + 📍 GPS定位权限 +
    +
    +
    + 📷 摄像头权限 +
    +
    +
    正在检查权限状态...
    +
    + +
    +

    🎯 第1步:GPS定位权限

    +

    为了在地图上显示您的位置,需要获取GPS定位权限:

    + +
    + +
    +
    + +
    +

    📷 第2步:摄像头权限

    +

    为了拍摄和传输视频,需要获取摄像头访问权限:

    + +
    + +
    +
    + +
    +

    🔧 不同浏览器的权限设置方法:

    + +
    + 📱 Safari (iOS): +
      +
    • 设置 → Safari → 摄像头/麦克风 → 允许
    • +
    • 设置 → 隐私与安全性 → 定位服务 → Safari → 使用App期间
    • +
    +
    + +
    + 🤖 Chrome (Android): +
      +
    • 点击地址栏左侧的🔒或ℹ️图标
    • +
    • 设置权限为"允许"
    • +
    • 或在设置 → 网站设置中调整
    • +
    +
    + +
    + 🖥️ 桌面浏览器: +
      +
    • 点击地址栏的🔒图标
    • +
    • 将摄像头和位置权限设为"允许"
    • +
    • 刷新页面使设置生效
    • +
    +
    +
    + +
    +

    ⚠️ 常见问题解决:

    +

    GPS获取失败:

    + +

    摄像头无法访问:

    + +
    + +
    + 🧪 权限测试页面 + +
    + + +
    + + + + + \ No newline at end of file diff --git a/distance-judgement/requirements.txt b/distance-judgement/requirements.txt new file mode 100644 index 00000000..ce615fd4 --- /dev/null +++ b/distance-judgement/requirements.txt @@ -0,0 +1,14 @@ +opencv-python==4.8.1.78 +ultralytics==8.0.196 +numpy==1.24.3 +torch==2.0.1 +torchvision==0.15.2 +matplotlib==3.7.2 +pillow==10.0.0 +requests==2.31.0 +flask==2.3.3 +cryptography>=3.4.8 + +# Windows系统位置服务支持(仅Windows) +winrt-runtime>=1.0.0; sys_platform == "win32" +winrt-Windows.Devices.Geolocation>=1.0.0; sys_platform == "win32" \ No newline at end of file diff --git a/distance-judgement/run.py b/distance-judgement/run.py new file mode 100644 index 00000000..ae118b81 --- /dev/null +++ b/distance-judgement/run.py @@ -0,0 +1,97 @@ +1#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +无人机战场态势感知系统 - 启动脚本 +让用户选择运行模式 +""" + +import sys +import os + +def show_menu(): + """显示菜单""" + print("=" * 60) + print("🚁 无人机战场态势感知系统") + print("=" * 60) + print() + print("请选择运行模式:") + print() + print("1. 🌐 Web模式 (推荐)") + print(" • 地图作为主界面") + print(" • 通过浏览器操作") + print(" • 可视化程度更高") + print(" • 支持远程访问") + print() + print("2. 🖥️ 传统模式") + print(" • 直接显示摄像头画面") + print(" • 键盘快捷键操作") + print(" • 性能更好") + print(" • 适合本地使用") + print() + print("3. ⚙️ 配置摄像头位置") + print(" • 设置GPS坐标") + print(" • 配置朝向角度") + print(" • 设置API Key") + print() + print("4. 🧪 运行系统测试") + print(" • 检查各模块状态") + print(" • 验证系统功能") + print() + print("0. ❌ 退出") + print() + +def main(): + """主函数""" + while True: + show_menu() + try: + choice = input("请输入选择 (0-4): ").strip() + + if choice == "1": + print("\n🌐 启动Web模式...") + import main_web + main_web.main() + break + + elif choice == "2": + print("\n🖥️ 启动传统模式...") + import main + main.main() + break + + elif choice == "3": + print("\n⚙️ 配置摄像头位置...") + import sys + sys.path.append('tools') + import setup_camera_location + setup_camera_location.main() + print("\n配置完成,请重新选择运行模式") + input("按回车键继续...") + + elif choice == "4": + print("\n🧪 运行系统测试...") + import sys + sys.path.append('tests') + import test_system + test_system.main() + print("\n测试完成") + input("按回车键继续...") + + elif choice == "0": + print("\n👋 再见!") + sys.exit(0) + + else: + print("\n❌ 无效选择,请重新输入") + input("按回车键继续...") + + except KeyboardInterrupt: + print("\n\n👋 再见!") + sys.exit(0) + except Exception as e: + print(f"\n❌ 运行出错: {e}") + input("按回车键继续...") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/distance-judgement/src/__init__.py b/distance-judgement/src/__init__.py new file mode 100644 index 00000000..02675b6d --- /dev/null +++ b/distance-judgement/src/__init__.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +实时人体距离检测系统 - 核心模块包 + +包含以下模块: +- config: 配置文件 +- person_detector: 人体检测模块 +- distance_calculator: 距离计算模块 +""" + +__version__ = "1.0.0" +__author__ = "Distance Detection System" + +# 导入核心模块 +from .config import * +from .person_detector import PersonDetector +from .distance_calculator import DistanceCalculator +from .map_manager import MapManager +from .web_server import WebServer +from .mobile_connector import MobileConnector, MobileDevice +from .orientation_detector import OrientationDetector +from .web_orientation_detector import WebOrientationDetector + +__all__ = [ + 'PersonDetector', + 'DistanceCalculator', + 'MapManager', + 'WebServer', + 'MobileConnector', + 'MobileDevice', + 'CAMERA_INDEX', + 'FRAME_WIDTH', + 'FRAME_HEIGHT', + 'FPS', + 'MODEL_PATH', + 'CONFIDENCE_THRESHOLD', + 'IOU_THRESHOLD', + 'KNOWN_PERSON_HEIGHT', + 'FOCAL_LENGTH', + 'REFERENCE_DISTANCE', + 'REFERENCE_HEIGHT_PIXELS', + 'FONT', + 'FONT_SCALE', + 'FONT_THICKNESS', + 'BOX_COLOR', + 'TEXT_COLOR', + 'TEXT_BG_COLOR', + 'PERSON_CLASS_ID' +] \ No newline at end of file diff --git a/distance-judgement/src/__pycache__/__init__.cpython-311.pyc b/distance-judgement/src/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 00000000..8c6971c7 Binary files /dev/null and b/distance-judgement/src/__pycache__/__init__.cpython-311.pyc differ diff --git a/distance-judgement/src/__pycache__/__init__.cpython-39.pyc b/distance-judgement/src/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 00000000..8a3f9798 Binary files /dev/null and b/distance-judgement/src/__pycache__/__init__.cpython-39.pyc differ diff --git a/distance-judgement/src/__pycache__/config.cpython-311.pyc b/distance-judgement/src/__pycache__/config.cpython-311.pyc new file mode 100644 index 00000000..ec3f976d Binary files /dev/null and b/distance-judgement/src/__pycache__/config.cpython-311.pyc differ diff --git a/distance-judgement/src/__pycache__/config.cpython-39.pyc b/distance-judgement/src/__pycache__/config.cpython-39.pyc new file mode 100644 index 00000000..7804005d Binary files /dev/null and b/distance-judgement/src/__pycache__/config.cpython-39.pyc differ diff --git a/distance-judgement/src/__pycache__/distance_calculator.cpython-311.pyc b/distance-judgement/src/__pycache__/distance_calculator.cpython-311.pyc new file mode 100644 index 00000000..1624b4b5 Binary files /dev/null and b/distance-judgement/src/__pycache__/distance_calculator.cpython-311.pyc differ diff --git a/distance-judgement/src/__pycache__/map_manager.cpython-311.pyc b/distance-judgement/src/__pycache__/map_manager.cpython-311.pyc new file mode 100644 index 00000000..fcc437f7 Binary files /dev/null and b/distance-judgement/src/__pycache__/map_manager.cpython-311.pyc differ diff --git a/distance-judgement/src/__pycache__/mobile_connector.cpython-311.pyc b/distance-judgement/src/__pycache__/mobile_connector.cpython-311.pyc new file mode 100644 index 00000000..b1cbff01 Binary files /dev/null and b/distance-judgement/src/__pycache__/mobile_connector.cpython-311.pyc differ diff --git a/distance-judgement/src/__pycache__/orientation_detector.cpython-311.pyc b/distance-judgement/src/__pycache__/orientation_detector.cpython-311.pyc new file mode 100644 index 00000000..33a8bec8 Binary files /dev/null and b/distance-judgement/src/__pycache__/orientation_detector.cpython-311.pyc differ diff --git a/distance-judgement/src/__pycache__/person_detector.cpython-311.pyc b/distance-judgement/src/__pycache__/person_detector.cpython-311.pyc new file mode 100644 index 00000000..b41347d6 Binary files /dev/null and b/distance-judgement/src/__pycache__/person_detector.cpython-311.pyc differ diff --git a/distance-judgement/src/__pycache__/web_orientation_detector.cpython-311.pyc b/distance-judgement/src/__pycache__/web_orientation_detector.cpython-311.pyc new file mode 100644 index 00000000..9ad2f9a6 Binary files /dev/null and b/distance-judgement/src/__pycache__/web_orientation_detector.cpython-311.pyc differ diff --git a/distance-judgement/src/__pycache__/web_server.cpython-311.pyc b/distance-judgement/src/__pycache__/web_server.cpython-311.pyc new file mode 100644 index 00000000..072b8632 Binary files /dev/null and b/distance-judgement/src/__pycache__/web_server.cpython-311.pyc differ diff --git a/distance-judgement/src/config.py b/distance-judgement/src/config.py new file mode 100644 index 00000000..309eda0f --- /dev/null +++ b/distance-judgement/src/config.py @@ -0,0 +1,40 @@ +# 配置文件 +import cv2 + +# 摄像头设置 +CAMERA_INDEX = 0 # 默认摄像头索引 +FRAME_WIDTH = 640 +FRAME_HEIGHT = 480 +FPS = 30 + +# YOLO模型设置 +MODEL_PATH = 'yolov8n.pt' # YOLOv8 nano模型 +CONFIDENCE_THRESHOLD = 0.5 +IOU_THRESHOLD = 0.45 + +# 距离计算参数 +# 这些参数需要根据实际摄像头和场景进行标定 +KNOWN_PERSON_HEIGHT = 170 # 假设平均人身高170cm +FOCAL_LENGTH = 500 # 焦距参数,需要校准 +REFERENCE_DISTANCE = 200 # 参考距离(cm) +REFERENCE_HEIGHT_PIXELS = 300 # 在参考距离下人体框的像素高度 + +# 显示设置 +FONT = cv2.FONT_HERSHEY_SIMPLEX +FONT_SCALE = 0.7 +FONT_THICKNESS = 2 +BOX_COLOR = (0, 255, 0) # 绿色框 +TEXT_COLOR = (255, 255, 255) # 白色文字 +TEXT_BG_COLOR = (0, 0, 0) # 黑色背景 + +# 人体类别ID(COCO数据集中person的类别ID是0) +PERSON_CLASS_ID = 0 + +# 地图配置 +GAODE_API_KEY = "3dcf7fa331c70e62d4683cf40fffc443" # 需要替换为真实的高德API key +CAMERA_LATITUDE = 28.262339630314234 # 摄像头纬度 +CAMERA_LONGITUDE = 113.04752581515713 # 摄像头经度 +CAMERA_HEADING = 180 # 摄像头朝向角度 +CAMERA_FOV = 60 # 摄像头视场角度 +ENABLE_MAP_DISPLAY = True # 是否启用地图显示 +MAP_AUTO_REFRESH = True # 地图是否自动刷新 \ No newline at end of file diff --git a/distance-judgement/src/distance_calculator.py b/distance-judgement/src/distance_calculator.py new file mode 100644 index 00000000..3f1702fc --- /dev/null +++ b/distance-judgement/src/distance_calculator.py @@ -0,0 +1,206 @@ +import numpy as np +import math +from . import config + +class DistanceCalculator: + def __init__(self): + self.focal_length = config.FOCAL_LENGTH + self.known_height = config.KNOWN_PERSON_HEIGHT + self.reference_distance = config.REFERENCE_DISTANCE + self.reference_height_pixels = config.REFERENCE_HEIGHT_PIXELS + + def calculate_distance_by_height(self, bbox_height): + """ + 根据人体框高度计算距离 + 使用相似三角形原理:距离 = (已知高度 × 焦距) / 像素高度 + """ + if bbox_height <= 0: + return 0 + + # 使用参考距离和参考像素高度来校准 + distance = (self.reference_distance * self.reference_height_pixels) / bbox_height + return max(distance, 30) # 最小距离限制为30cm + + def calculate_distance_by_focal_length(self, bbox_height): + """ + 使用焦距公式计算距离 + 距离 = (真实高度 × 焦距) / 像素高度 + """ + if bbox_height <= 0: + return 0 + + distance = (self.known_height * self.focal_length) / bbox_height + return max(distance, 30) # 最小距离限制为30cm + + def calibrate_focal_length(self, known_distance, measured_height_pixels): + """ + 标定焦距 + 焦距 = (像素高度 × 真实距离) / 真实高度 + """ + self.focal_length = (measured_height_pixels * known_distance) / self.known_height + print(f"焦距已标定为: {self.focal_length:.2f}") + + def get_distance(self, bbox): + """ + 根据边界框计算距离 + bbox: [x1, y1, x2, y2] + """ + x1, y1, x2, y2 = bbox + bbox_height = y2 - y1 + bbox_width = x2 - x1 + + # 使用高度计算距离(更准确) + distance = self.calculate_distance_by_height(bbox_height) + + return distance + + def format_distance(self, distance): + """ + 格式化距离显示 + """ + if distance < 100: + return f"{distance:.1f}cm" + else: + return f"{distance/100:.1f}m" + + def calculate_person_gps_position(self, camera_lat, camera_lng, camera_heading, + bbox, distance_meters, frame_width, frame_height, + camera_fov=60): + """ + 🎯 核心算法:根据摄像头GPS位置、朝向、人体检测框计算人员真实GPS坐标 + + Args: + camera_lat: 摄像头纬度 + camera_lng: 摄像头经度 + camera_heading: 摄像头朝向角度 (0=正北, 90=正东) + bbox: 人体检测框 [x1, y1, x2, y2] + distance_meters: 人员距离摄像头的距离(米) + frame_width: 画面宽度(像素) + frame_height: 画面高度(像素) + camera_fov: 摄像头水平视场角(度) + + Returns: + (person_lat, person_lng): 人员GPS坐标 + """ + x1, y1, x2, y2 = bbox + + # 计算人体检测框中心点 + person_center_x = (x1 + x2) / 2 + person_center_y = (y1 + y2) / 2 + + # 计算人员相对于画面中心的偏移角度 + frame_center_x = frame_width / 2 + horizontal_offset_pixels = person_center_x - frame_center_x + + # 将像素偏移转换为角度偏移 + horizontal_angle_per_pixel = camera_fov / frame_width + horizontal_offset_degrees = horizontal_offset_pixels * horizontal_angle_per_pixel + + # 计算人员相对于正北的实际方位角 + person_bearing = (camera_heading + horizontal_offset_degrees) % 360 + + # 使用球面几何计算人员GPS坐标 + person_lat, person_lng = self._calculate_destination_point( + camera_lat, camera_lng, distance_meters, person_bearing + ) + + return person_lat, person_lng + + def _calculate_destination_point(self, lat, lng, distance, bearing): + """ + 🌍 球面几何计算:根据起点坐标、距离和方位角计算目标点坐标 + + Args: + lat: 起点纬度 + lng: 起点经度 + distance: 距离(米) + bearing: 方位角(度,0=正北) + + Returns: + (target_lat, target_lng): 目标点坐标 + """ + # 地球半径(米) + R = 6371000 + + # 转换为弧度 + lat1 = math.radians(lat) + lng1 = math.radians(lng) + bearing_rad = math.radians(bearing) + + # 球面几何计算目标点坐标 + lat2 = math.asin( + math.sin(lat1) * math.cos(distance / R) + + math.cos(lat1) * math.sin(distance / R) * math.cos(bearing_rad) + ) + + lng2 = lng1 + math.atan2( + math.sin(bearing_rad) * math.sin(distance / R) * math.cos(lat1), + math.cos(distance / R) - math.sin(lat1) * math.sin(lat2) + ) + + return math.degrees(lat2), math.degrees(lng2) + + def is_person_in_camera_fov(self, camera_lat, camera_lng, camera_heading, + person_lat, person_lng, camera_fov=60, max_distance=100): + """ + 🔍 检查人员是否在摄像头视野范围内 + + Args: + camera_lat: 摄像头纬度 + camera_lng: 摄像头经度 + camera_heading: 摄像头朝向角度 + person_lat: 人员纬度 + person_lng: 人员经度 + camera_fov: 摄像头视场角(度) + max_distance: 最大检测距离(米) + + Returns: + bool: 是否在视野内 + """ + # 计算人员相对于摄像头的距离和方位角 + distance, bearing = self._calculate_distance_and_bearing( + camera_lat, camera_lng, person_lat, person_lng + ) + + # 检查距离是否在范围内 + if distance > max_distance: + return False + + # 计算人员方位角与摄像头朝向的角度差 + angle_diff = abs(bearing - camera_heading) + if angle_diff > 180: + angle_diff = 360 - angle_diff + + # 检查是否在视场角范围内 + return angle_diff <= camera_fov / 2 + + def _calculate_distance_and_bearing(self, lat1, lng1, lat2, lng2): + """ + 🧭 计算两点间距离和方位角 + + Returns: + (distance_meters, bearing_degrees): 距离(米)和方位角(度) + """ + # 转换为弧度 + lat1_rad = math.radians(lat1) + lng1_rad = math.radians(lng1) + lat2_rad = math.radians(lat2) + lng2_rad = math.radians(lng2) + + # 计算距离 (Haversine公式) + dlat = lat2_rad - lat1_rad + dlng = lng2_rad - lng1_rad + + a = (math.sin(dlat/2)**2 + + math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(dlng/2)**2) + c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a)) + distance = 6371000 * c # 地球半径6371km + + # 计算方位角 + y = math.sin(dlng) * math.cos(lat2_rad) + x = (math.cos(lat1_rad) * math.sin(lat2_rad) - + math.sin(lat1_rad) * math.cos(lat2_rad) * math.cos(dlng)) + bearing = math.atan2(y, x) + bearing_degrees = (math.degrees(bearing) + 360) % 360 + + return distance, bearing_degrees \ No newline at end of file diff --git a/distance-judgement/src/map_manager.py b/distance-judgement/src/map_manager.py new file mode 100644 index 00000000..f6570be5 --- /dev/null +++ b/distance-judgement/src/map_manager.py @@ -0,0 +1,242 @@ +import requests +import json +import math +import webbrowser +import os +from typing import List, Tuple, Dict +import time + +class MapManager: + """高德地图管理器 - 处理地图显示和坐标标记""" + + def __init__(self, api_key: str = None, camera_lat: float = None, camera_lng: float = None): + self.api_key = api_key or "your_gaode_api_key_here" # 需要替换为真实的API key + self.camera_lat = camera_lat or 39.9042 # 默认北京天安门坐标 + self.camera_lng = camera_lng or 116.4074 + self.camera_heading = 0 # 摄像头朝向角度(正北为0度) + self.camera_fov = 60 # 摄像头视场角度 + self.persons_positions = [] # 人员位置列表 + self.map_html_path = "person_tracking_map.html" + + def set_camera_position(self, lat: float, lng: float, heading: float = 0): + """设置摄像头位置和朝向""" + self.camera_lat = lat + self.camera_lng = lng + self.camera_heading = heading + print(f"📍 摄像头位置已设置: ({lat:.6f}, {lng:.6f}), 朝向: {heading}°") + + def calculate_person_position(self, pixel_x: float, pixel_y: float, distance: float, + frame_width: int, frame_height: int) -> Tuple[float, float]: + """根据人在画面中的像素位置和距离,计算真实地理坐标""" + # 将像素坐标转换为相对角度 + horizontal_angle_per_pixel = self.camera_fov / frame_width + + # 计算人相对于摄像头中心的角度偏移 + center_x = frame_width / 2 + horizontal_offset_degrees = (pixel_x - center_x) * horizontal_angle_per_pixel + + # 计算人相对于摄像头的实际角度 + person_bearing = (self.camera_heading + horizontal_offset_degrees) % 360 + + # 将距离和角度转换为地理坐标偏移 + person_lat, person_lng = self._calculate_destination_point( + self.camera_lat, self.camera_lng, distance, person_bearing + ) + + return person_lat, person_lng + + def _calculate_destination_point(self, lat: float, lng: float, distance: float, bearing: float) -> Tuple[float, float]: + """根据起点坐标、距离和方位角计算目标点坐标,使用球面几何学计算""" + # 地球半径(米) + R = 6371000 + + # 转换为弧度 + lat1 = math.radians(lat) + lng1 = math.radians(lng) + bearing_rad = math.radians(bearing) + + # 计算目标点坐标 + lat2 = math.asin( + math.sin(lat1) * math.cos(distance / R) + + math.cos(lat1) * math.sin(distance / R) * math.cos(bearing_rad) + ) + + lng2 = lng1 + math.atan2( + math.sin(bearing_rad) * math.sin(distance / R) * math.cos(lat1), + math.cos(distance / R) - math.sin(lat1) * math.sin(lat2) + ) + + return math.degrees(lat2), math.degrees(lng2) + + def add_person_position(self, pixel_x: float, pixel_y: float, distance: float, + frame_width: int, frame_height: int, person_id: str = None): + """添加人员位置""" + lat, lng = self.calculate_person_position(pixel_x, pixel_y, distance, frame_width, frame_height) + + person_info = { + 'id': person_id or f"person_{len(self.persons_positions) + 1}", + 'lat': lat, + 'lng': lng, + 'distance': distance, + 'timestamp': time.time(), + 'pixel_x': pixel_x, + 'pixel_y': pixel_y + } + + self.persons_positions.append(person_info) + + # 只保留最近10秒的数据 + current_time = time.time() + self.persons_positions = [ + p for p in self.persons_positions + if current_time - p['timestamp'] < 10 + ] + + return lat, lng + + def clear_persons(self): + """清空人员位置""" + self.persons_positions = [] + + def add_person_at_coordinates(self, lat: float, lng: float, person_id: str, + distance: float = 0, source: str = "manual"): + """直接在指定GPS坐标添加人员标记""" + person_data = { + 'id': person_id, + 'lat': lat, + 'lng': lng, + 'distance': distance, + 'timestamp': time.time(), + 'source': source # 标记数据来源(如设备ID) + } + + # 添加到人员数据列表 + self.persons_positions.append(person_data) + + # 只保留最近10秒的数据 + current_time = time.time() + self.persons_positions = [ + p for p in self.persons_positions + if current_time - p['timestamp'] < 10 + ] + + return lat, lng + + def get_persons_data(self) -> List[Dict]: + """获取当前人员数据""" + return self.persons_positions + + def generate_map_html(self) -> str: + """生成高德地图HTML页面""" + persons_data_json = json.dumps(self.persons_positions) + + html_content = f""" + + + + 实时人员位置追踪系统 🚁 + + + + + +
    +
    +

    🚁 无人机战场态势感知

    +
    ● 摄像头在线
    +
    📍 坐标: {self.camera_lat:.6f}, {self.camera_lng:.6f}
    +
    🧭 朝向: {self.camera_heading}°
    +
    👥 检测到: {len(self.persons_positions)} 人
    +
    + 🔴 红点 = 人员位置
    + 📷 蓝点 = 摄像头位置
    + ⚡ 实时更新 +
    +
    + + + +""" + + # 保存HTML文件 + with open(self.map_html_path, 'w', encoding='utf-8') as f: + f.write(html_content) + + return self.map_html_path + + def open_map(self): + """在浏览器中打开地图""" + html_path = self.generate_map_html() + file_url = f"file://{os.path.abspath(html_path)}" + webbrowser.open(file_url) + print(f"🗺️ 地图已在浏览器中打开: {html_path}") + + def update_camera_heading(self, new_heading: float): + """更新摄像头朝向""" + self.camera_heading = new_heading + print(f"🧭 摄像头朝向已更新: {new_heading}°") \ No newline at end of file diff --git a/distance-judgement/src/mobile_connector.py b/distance-judgement/src/mobile_connector.py new file mode 100644 index 00000000..cd4d6354 --- /dev/null +++ b/distance-judgement/src/mobile_connector.py @@ -0,0 +1,303 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +手机连接器模块 +用于接收手机传送的摄像头图像、GPS位置和设备信息 +""" + +import cv2 +import numpy as np +import json +import time +import threading +from datetime import datetime +import base64 +import socket +import struct +from typing import Dict, List, Optional, Tuple, Callable +from . import config + +class MobileDevice: + """移动设备信息类""" + def __init__(self, device_id: str, device_name: str): + self.device_id = device_id + self.device_name = device_name + self.last_seen = time.time() + self.is_online = True + self.current_location = None # (lat, lng, accuracy) + self.battery_level = 100 + self.signal_strength = 100 + self.camera_info = {} + self.connection_info = {} + + def update_status(self, data: dict): + """更新设备状态""" + self.last_seen = time.time() + self.is_online = True + + if 'gps' in data: + self.current_location = ( + data['gps'].get('latitude'), + data['gps'].get('longitude'), + data['gps'].get('accuracy', 0) + ) + + if 'battery' in data: + self.battery_level = data['battery'] + + if 'signal' in data: + self.signal_strength = data['signal'] + + if 'camera_info' in data: + self.camera_info = data['camera_info'] + + def is_location_valid(self) -> bool: + """检查GPS位置是否有效""" + if not self.current_location: + return False + lat, lng, _ = self.current_location + return lat is not None and lng is not None and -90 <= lat <= 90 and -180 <= lng <= 180 + +class MobileConnector: + """手机连接器主类""" + + def __init__(self, port: int = 8080): + self.port = port + self.server_socket = None + self.is_running = False + self.devices = {} # device_id -> MobileDevice + self.frame_callbacks = [] # 帧数据回调函数列表 + self.location_callbacks = [] # 位置数据回调函数列表 + self.device_callbacks = [] # 设备状态回调函数列表 + self.client_threads = [] + + # 统计信息 + self.total_frames_received = 0 + self.total_data_received = 0 + self.start_time = time.time() + + def add_frame_callback(self, callback: Callable): + """添加帧数据回调函数""" + self.frame_callbacks.append(callback) + + def add_location_callback(self, callback: Callable): + """添加位置数据回调函数""" + self.location_callbacks.append(callback) + + def add_device_callback(self, callback: Callable): + """添加设备状态回调函数""" + self.device_callbacks.append(callback) + + def start_server(self): + """启动服务器""" + try: + self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.server_socket.bind(('0.0.0.0', self.port)) + self.server_socket.listen(5) + self.is_running = True + + print(f"📱 手机连接服务器启动成功,端口: {self.port}") + print(f"🌐 等待手机客户端连接...") + + # 启动服务器监听线程 + server_thread = threading.Thread(target=self._server_loop, daemon=True) + server_thread.start() + + # 启动设备状态监控线程 + monitor_thread = threading.Thread(target=self._device_monitor, daemon=True) + monitor_thread.start() + + return True + + except Exception as e: + print(f"❌ 启动服务器失败: {e}") + return False + + def stop_server(self): + """停止服务器""" + self.is_running = False + if self.server_socket: + self.server_socket.close() + + # 清理客户端连接 + for thread in self.client_threads: + if thread.is_alive(): + thread.join(timeout=1.0) + + print("📱 手机连接服务器已停止") + + def _server_loop(self): + """服务器主循环""" + while self.is_running: + try: + client_socket, address = self.server_socket.accept() + print(f"📱 新的手机客户端连接: {address}") + + # 为每个客户端创建处理线程 + client_thread = threading.Thread( + target=self._handle_client, + args=(client_socket, address), + daemon=True + ) + client_thread.start() + self.client_threads.append(client_thread) + + except Exception as e: + if self.is_running: + print(f"⚠️ 服务器接受连接时出错: {e}") + break + + def _handle_client(self, client_socket, address): + """处理客户端连接""" + device_id = None + try: + while self.is_running: + # 接收数据长度 + length_data = self._recv_all(client_socket, 4) + if not length_data: + break + + data_length = struct.unpack('!I', length_data)[0] + + # 接收JSON数据 + json_data = self._recv_all(client_socket, data_length) + if not json_data: + break + + try: + data = json.loads(json_data.decode('utf-8')) + device_id = data.get('device_id') + + if device_id: + self._process_mobile_data(device_id, data, address) + self.total_data_received += len(json_data) + + except json.JSONDecodeError as e: + print(f"⚠️ JSON解析错误: {e}") + continue + + except Exception as e: + print(f"⚠️ 处理客户端 {address} 时出错: {e}") + finally: + client_socket.close() + if device_id and device_id in self.devices: + self.devices[device_id].is_online = False + print(f"📱 设备 {device_id} 已断开连接") + + def _recv_all(self, socket, length): + """接收指定长度的数据""" + data = b'' + while len(data) < length: + packet = socket.recv(length - len(data)) + if not packet: + return None + data += packet + return data + + def _process_mobile_data(self, device_id: str, data: dict, address): + """处理手机发送的数据""" + # 更新或创建设备信息 + if device_id not in self.devices: + device_name = data.get('device_name', f'Mobile-{device_id[:8]}') + self.devices[device_id] = MobileDevice(device_id, device_name) + print(f"📱 新设备注册: {device_name} ({device_id[:8]})") + + # 触发设备状态回调 + for callback in self.device_callbacks: + try: + callback('device_connected', self.devices[device_id]) + except Exception as e: + print(f"⚠️ 设备回调错误: {e}") + + device = self.devices[device_id] + device.update_status(data) + device.connection_info = {'address': address} + + # 处理图像数据 + if 'frame' in data: + try: + frame_data = base64.b64decode(data['frame']) + frame = cv2.imdecode( + np.frombuffer(frame_data, np.uint8), + cv2.IMREAD_COLOR + ) + + if frame is not None: + self.total_frames_received += 1 + + # 触发帧数据回调 + for callback in self.frame_callbacks: + try: + callback(device_id, frame, device) + except Exception as e: + print(f"⚠️ 帧回调错误: {e}") + + except Exception as e: + print(f"⚠️ 图像数据处理错误: {e}") + + # 处理GPS位置数据 + if 'gps' in data and device.is_location_valid(): + for callback in self.location_callbacks: + try: + callback(device_id, device.current_location, device) + except Exception as e: + print(f"⚠️ 位置回调错误: {e}") + + def _device_monitor(self): + """设备状态监控""" + while self.is_running: + try: + current_time = time.time() + offline_devices = [] + + for device_id, device in self.devices.items(): + # 超过30秒没有数据认为离线 + if current_time - device.last_seen > 30: + if device.is_online: + device.is_online = False + offline_devices.append(device_id) + + # 通知离线设备 + for device_id in offline_devices: + print(f"📱 设备 {device_id[:8]} 已离线") + for callback in self.device_callbacks: + try: + callback('device_disconnected', self.devices[device_id]) + except Exception as e: + print(f"⚠️ 设备回调错误: {e}") + + time.sleep(5) # 每5秒检查一次 + + except Exception as e: + print(f"⚠️ 设备监控错误: {e}") + time.sleep(5) + + def get_online_devices(self) -> List[MobileDevice]: + """获取在线设备列表""" + return [device for device in self.devices.values() if device.is_online] + + def get_device_by_id(self, device_id: str) -> Optional[MobileDevice]: + """根据ID获取设备""" + return self.devices.get(device_id) + + def get_statistics(self) -> dict: + """获取连接统计信息""" + online_count = len(self.get_online_devices()) + total_count = len(self.devices) + uptime = time.time() - self.start_time + + return { + 'online_devices': online_count, + 'total_devices': total_count, + 'frames_received': self.total_frames_received, + 'data_received_mb': self.total_data_received / (1024 * 1024), + 'uptime_seconds': uptime, + 'avg_frames_per_second': self.total_frames_received / uptime if uptime > 0 else 0 + } + + def send_command_to_device(self, device_id: str, command: dict): + """向指定设备发送命令(预留接口)""" + # TODO: 实现向手机发送控制命令的功能 + pass \ No newline at end of file diff --git a/distance-judgement/src/orientation_detector.py b/distance-judgement/src/orientation_detector.py new file mode 100644 index 00000000..d220e20c --- /dev/null +++ b/distance-judgement/src/orientation_detector.py @@ -0,0 +1,295 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +设备朝向检测模块 +用于自动获取设备的GPS位置和朝向信息 +""" + +import requests +import time +import json +import math +from typing import Tuple, Optional, Dict +from . import config + + +class OrientationDetector: + """设备朝向检测器""" + + def __init__(self): + self.current_location = None # (lat, lng, accuracy) + self.current_heading = None # 设备朝向角度 + self.last_update = 0 + self.gps_cache_duration = 300 # GPS缓存5分钟 + + def get_current_gps_location(self) -> Optional[Tuple[float, float, float]]: + """ + 获取当前设备的GPS位置 + 返回: (纬度, 经度, 精度) 或 None + """ + try: + # 首先尝试使用系统API (需要安装相关库) + location = self._get_system_gps() + if location: + return location + + # 如果系统API不可用,使用IP地理定位作为备选 + location = self._get_ip_geolocation() + if location: + print("🌐 使用IP地理定位获取位置(精度较低)") + return location + + return None + + except Exception as e: + print(f"❌ GPS位置获取失败: {e}") + return None + + def _get_system_gps(self) -> Optional[Tuple[float, float, float]]: + """尝试使用系统GPS API获取位置""" + try: + # 在Windows上可以使用Windows Location API + # 这里提供一个框架,实际实现需要根据操作系统选择合适的API + import platform + system = platform.system() + + if system == "Windows": + return self._get_windows_location() + elif system == "Darwin": # macOS + return self._get_macos_location() + elif system == "Linux": + return self._get_linux_location() + + except ImportError: + print("💡 系统定位API不可用,将使用IP定位") + + return None + + def _get_windows_location(self) -> Optional[Tuple[float, float, float]]: + """Windows系统GPS定位""" + try: + # 使用Windows Location API + import winrt.windows.devices.geolocation as geo + + locator = geo.Geolocator() + # 设置期望精度 + locator.desired_accuracy = geo.PositionAccuracy.HIGH + + print("🔍 正在获取Windows系统GPS位置...") + + # 获取位置信息(同步方式) + position = locator.get_geoposition_async().get() + + lat = position.coordinate.point.position.latitude + lng = position.coordinate.point.position.longitude + accuracy = position.coordinate.accuracy + + print(f"✅ Windows GPS获取成功: ({lat:.6f}, {lng:.6f}), 精度: ±{accuracy:.0f}m") + return (lat, lng, accuracy) + + except Exception as e: + print(f"⚠️ Windows GPS API失败: {e}") + return None + + def _get_macos_location(self) -> Optional[Tuple[float, float, float]]: + """macOS系统GPS定位""" + try: + # macOS可以使用Core Location框架 + # 这里提供一个基本框架 + print("💡 macOS GPS定位需要额外配置,建议使用IP定位") + return None + + except Exception as e: + print(f"⚠️ macOS GPS API失败: {e}") + return None + + def _get_linux_location(self) -> Optional[Tuple[float, float, float]]: + """Linux系统GPS定位""" + try: + # Linux可以使用gpsd或NetworkManager + print("💡 Linux GPS定位需要额外配置,建议使用IP定位") + return None + + except Exception as e: + print(f"⚠️ Linux GPS API失败: {e}") + return None + + def _get_ip_geolocation(self) -> Optional[Tuple[float, float, float]]: + """使用IP地址进行地理定位""" + try: + print("🌐 正在使用IP地理定位...") + + # 使用免费的IP地理定位服务 + response = requests.get("http://ip-api.com/json/", timeout=10) + + if response.status_code == 200: + data = response.json() + + if data.get('status') == 'success': + lat = float(data.get('lat', 0)) + lng = float(data.get('lon', 0)) + accuracy = 10000 # IP定位精度通常在10km左右 + + city = data.get('city', '未知') + region = data.get('regionName', '未知') + country = data.get('country', '未知') + + print(f"✅ IP定位成功: {city}, {region}, {country}") + print(f"📍 位置: ({lat:.6f}, {lng:.6f}), 精度: ±{accuracy:.0f}m") + + return (lat, lng, accuracy) + + except Exception as e: + print(f"❌ IP地理定位失败: {e}") + + return None + + def get_device_heading(self) -> Optional[float]: + """ + 获取设备朝向(磁力计方向) + 返回: 角度 (0-360度,0为正北) 或 None + """ + try: + # 桌面设备通常没有磁力计,返回默认朝向 + # 可以根据摄像头位置或用户设置来确定朝向 + print("💡 桌面设备朝向检测有限,使用默认朝向") + + # 假设用户面向屏幕,摄像头朝向用户 + # 如果摄像头在屏幕上方,那么朝向就是用户的相反方向 + default_heading = 180.0 # 假设用户面向南方,摄像头朝向北方 + + return default_heading + + except Exception as e: + print(f"❌ 设备朝向检测失败: {e}") + return None + + def calculate_camera_heading_facing_user(self, user_heading: float) -> float: + """ + 计算摄像头朝向用户的角度 + + Args: + user_heading: 用户朝向角度 (0-360度) + + Returns: + 摄像头应该设置的朝向角度 + """ + # 摄像头朝向用户,即朝向用户相反的方向 + camera_heading = (user_heading + 180) % 360 + return camera_heading + + def auto_configure_camera_location(self) -> Dict: + """ + 自动配置摄像头位置和朝向 + + Returns: + 配置信息字典 + """ + result = { + 'success': False, + 'gps_location': None, + 'device_heading': None, + 'camera_heading': None, + 'method': None, + 'accuracy': None + } + + print("🚀 开始自动配置摄像头位置和朝向...") + + # 1. 获取GPS位置 + gps_location = self.get_current_gps_location() + if not gps_location: + print("❌ 无法获取GPS位置,自动配置失败") + return result + + lat, lng, accuracy = gps_location + result['gps_location'] = (lat, lng) + result['accuracy'] = accuracy + + # 2. 获取设备朝向 + device_heading = self.get_device_heading() + if device_heading is None: + print("⚠️ 无法获取设备朝向,使用默认朝向") + device_heading = 0.0 # 默认朝北 + + result['device_heading'] = device_heading + + # 3. 计算摄像头朝向(朝向用户) + camera_heading = self.calculate_camera_heading_facing_user(device_heading) + result['camera_heading'] = camera_heading + + # 4. 确定配置方法 + if accuracy < 100: + result['method'] = 'GPS' + else: + result['method'] = 'IP定位' + + result['success'] = True + + print(f"✅ 自动配置完成:") + print(f"📍 GPS位置: ({lat:.6f}, {lng:.6f})") + print(f"🧭 设备朝向: {device_heading:.1f}°") + print(f"📷 摄像头朝向: {camera_heading:.1f}°") + print(f"🎯 定位方法: {result['method']}") + print(f"📏 定位精度: ±{accuracy:.0f}m") + + return result + + def update_camera_config(self, gps_location: Tuple[float, float], camera_heading: float): + """ + 更新摄像头配置文件 + + Args: + gps_location: (纬度, 经度) + camera_heading: 摄像头朝向角度 + """ + try: + from tools.setup_camera_location import update_config_file + + lat, lng = gps_location + + # 更新配置文件 + update_config_file(lat, lng, camera_heading) + + # 同时更新运行时配置 + config.CAMERA_LATITUDE = lat + config.CAMERA_LONGITUDE = lng + config.CAMERA_HEADING = camera_heading + + print(f"✅ 摄像头配置已更新") + print(f"📍 新位置: ({lat:.6f}, {lng:.6f})") + print(f"🧭 新朝向: {camera_heading:.1f}°") + + except Exception as e: + print(f"❌ 配置更新失败: {e}") + + +def main(): + """测试函数""" + print("🧭 设备朝向检测器测试") + print("=" * 50) + + detector = OrientationDetector() + + # 测试自动配置 + result = detector.auto_configure_camera_location() + + if result['success']: + print("\n🎯 是否应用此配置? (y/n): ", end="") + choice = input().strip().lower() + + if choice == 'y': + detector.update_camera_config( + result['gps_location'], + result['camera_heading'] + ) + print("✅ 配置已应用") + else: + print("⏭️ 配置未应用") + else: + print("❌ 自动配置失败") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/distance-judgement/src/person_detector.py b/distance-judgement/src/person_detector.py new file mode 100644 index 00000000..cb84178b --- /dev/null +++ b/distance-judgement/src/person_detector.py @@ -0,0 +1,100 @@ +import cv2 +import numpy as np +from ultralytics import YOLO +from . import config + +class PersonDetector: + def __init__(self): + self.model = None + self.load_model() + + def load_model(self): + """加载YOLO模型""" + try: + self.model = YOLO(config.MODEL_PATH) + print(f"YOLO模型加载成功: {config.MODEL_PATH}") + except Exception as e: + print(f"模型加载失败: {e}") + print("正在下载YOLOv8n模型...") + self.model = YOLO('yolov8n.pt') # 会自动下载 + + def detect_persons(self, frame): + """ + 检测图像中的人体 + 返回: 检测结果列表,每个结果包含 [x1, y1, x2, y2, confidence] + """ + if self.model is None: + return [] + + try: + # 使用YOLO进行检测 + results = self.model(frame, verbose=False) + + persons = [] + for result in results: + boxes = result.boxes + if boxes is not None: + for box in boxes: + # 获取类别、置信度和坐标 + cls = int(box.cls[0]) + conf = float(box.conf[0]) + + # 只保留人体检测结果 + if cls == config.PERSON_CLASS_ID and conf >= config.CONFIDENCE_THRESHOLD: + # 获取边界框坐标 + x1, y1, x2, y2 = box.xyxy[0].cpu().numpy() + persons.append([int(x1), int(y1), int(x2), int(y2), conf]) + + return persons + + except Exception as e: + print(f"检测过程中出错: {e}") + return [] + + def draw_detections(self, frame, detections, distances): + """ + 在图像上绘制检测结果和距离信息 + """ + for i, detection in enumerate(detections): + x1, y1, x2, y2, conf = detection + + # 绘制边界框 + cv2.rectangle(frame, (x1, y1), (x2, y2), config.BOX_COLOR, 2) + + # 准备显示文本 + person_id = f"Person #{i+1}" + distance_text = f"Distance: {distances[i]}" if i < len(distances) else "Distance: N/A" + conf_text = f"Conf: {conf:.2f}" + + # 计算文本位置 + text_y = y1 - 35 if y1 - 35 > 20 else y1 + 20 + + # 绘制人员ID文本背景和文字 + id_text_size = cv2.getTextSize(person_id, config.FONT, config.FONT_SCALE, config.FONT_THICKNESS)[0] + cv2.rectangle(frame, (x1, text_y - id_text_size[1] - 5), + (x1 + id_text_size[0] + 10, text_y + 5), (255, 0, 0), -1) + cv2.putText(frame, person_id, (x1 + 5, text_y), + config.FONT, config.FONT_SCALE, config.TEXT_COLOR, config.FONT_THICKNESS) + + # 绘制距离文本背景和文字 + distance_text_y = text_y + 25 + distance_text_size = cv2.getTextSize(distance_text, config.FONT, config.FONT_SCALE, config.FONT_THICKNESS)[0] + cv2.rectangle(frame, (x1, distance_text_y - distance_text_size[1] - 5), + (x1 + distance_text_size[0] + 10, distance_text_y + 5), config.TEXT_BG_COLOR, -1) + cv2.putText(frame, distance_text, (x1 + 5, distance_text_y), + config.FONT, config.FONT_SCALE, config.TEXT_COLOR, config.FONT_THICKNESS) + + # 绘制置信度文本(在框的右上角) + conf_text_size = cv2.getTextSize(conf_text, config.FONT, config.FONT_SCALE - 0.2, config.FONT_THICKNESS)[0] + cv2.rectangle(frame, (x2 - conf_text_size[0] - 10, y1), + (x2, y1 + conf_text_size[1] + 10), config.TEXT_BG_COLOR, -1) + cv2.putText(frame, conf_text, (x2 - conf_text_size[0] - 5, y1 + conf_text_size[1] + 5), + config.FONT, config.FONT_SCALE - 0.2, config.TEXT_COLOR, config.FONT_THICKNESS) + + return frame + + def get_model_info(self): + """获取模型信息""" + if self.model: + return f"YOLO Model: {config.MODEL_PATH}" + return "Model not loaded" \ No newline at end of file diff --git a/distance-judgement/src/web_orientation_detector.py b/distance-judgement/src/web_orientation_detector.py new file mode 100644 index 00000000..7d004305 --- /dev/null +++ b/distance-judgement/src/web_orientation_detector.py @@ -0,0 +1,335 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Web端朝向检测器 +提供Web API接口用于获取GPS位置和设备朝向信息 +""" + +from flask import Blueprint, jsonify, request +import json +import time +from typing import Dict, Optional, Tuple +from . import config +from .orientation_detector import OrientationDetector + + +class WebOrientationDetector: + """Web端朝向检测器""" + + def __init__(self): + self.orientation_detector = OrientationDetector() + self.current_web_location = None + self.current_web_heading = None + self.last_web_update = 0 + + # 创建Blueprint + self.blueprint = Blueprint('orientation', __name__) + self.setup_routes() + + def setup_routes(self): + """设置Web API路由""" + + @self.blueprint.route('/api/orientation/auto_configure', methods=['POST']) + def auto_configure_from_web(): + """从Web端自动配置摄像头位置和朝向""" + try: + data = request.get_json() or {} + print(f"🔍 收到自动配置请求: {data}") + + # 支持两种数据格式 + # 新格式: {gps_location: [lat, lng], user_heading: heading, apply_config: true} + # 旧格式: {gps: {...}, orientation: {...}} + + if 'gps_location' in data: + # 新格式处理 + gps_location = data.get('gps_location') + user_heading = data.get('user_heading', 0) + apply_config = data.get('apply_config', True) + + if not gps_location or len(gps_location) < 2: + return jsonify({ + "success": False, + "error": "GPS位置数据格式错误" + }) + + lat, lng = float(gps_location[0]), float(gps_location[1]) + + # 验证坐标范围 + if not (-90 <= lat <= 90) or not (-180 <= lng <= 180): + return jsonify({ + "success": False, + "error": "GPS坐标范围不正确" + }) + + # 计算摄像头朝向 + if user_heading is not None: + # 计算摄像头朝向(朝向用户方向) + camera_heading = (user_heading + 180) % 360 + else: + camera_heading = 0.0 + + print(f"📍 处理GPS位置: ({lat:.6f}, {lng:.6f})") + print(f"🧭 用户朝向: {user_heading}°, 摄像头朝向: {camera_heading}°") + + if apply_config: + # 应用配置 + self.orientation_detector.update_camera_config((lat, lng), camera_heading) + print(f"✅ 配置已应用到系统") + + return jsonify({ + "success": True, + "message": "摄像头位置和朝向已自动配置", + "gps_location": [lat, lng], + "user_heading": user_heading, + "camera_heading": camera_heading, + "applied": apply_config + }) + + else: + # 旧格式处理 + gps_data = data.get('gps') + orientation_data = data.get('orientation') + + if not gps_data: + # 如果前端没有提供GPS,尝试后端获取 + result = self.orientation_detector.auto_configure_camera_location() + else: + # 使用前端提供的数据 + result = self.process_web_data(gps_data, orientation_data) + + if result['success']: + # 应用配置 + self.orientation_detector.update_camera_config( + result['gps_location'], + result['camera_heading'] + ) + + return jsonify({ + "success": True, + "message": "摄像头位置和朝向已自动配置", + **result + }) + else: + return jsonify({ + "success": False, + "error": result.get('error', '自动配置失败') + }) + + except Exception as e: + print(f"❌ 自动配置异常: {e}") + import traceback + traceback.print_exc() + return jsonify({ + "success": False, + "error": f"配置失败: {str(e)}" + }) + + @self.blueprint.route('/api/orientation/update_location', methods=['POST']) + def update_location(): + """更新GPS位置信息""" + try: + data = request.get_json() + + if not data or 'latitude' not in data or 'longitude' not in data: + return jsonify({ + "status": "error", + "message": "缺少位置信息" + }) + + lat = float(data['latitude']) + lng = float(data['longitude']) + accuracy = float(data.get('accuracy', 1000)) + + # 验证坐标范围 + if not (-90 <= lat <= 90) or not (-180 <= lng <= 180): + return jsonify({ + "status": "error", + "message": "坐标范围不正确" + }) + + # 更新位置信息 + self.current_web_location = (lat, lng, accuracy) + self.last_web_update = time.time() + + print(f"📍 Web GPS更新: ({lat:.6f}, {lng:.6f}), 精度: ±{accuracy:.0f}m") + + return jsonify({ + "status": "success", + "message": "位置信息已更新" + }) + + except Exception as e: + return jsonify({ + "status": "error", + "message": f"位置更新失败: {str(e)}" + }) + + @self.blueprint.route('/api/orientation/update_heading', methods=['POST']) + def update_heading(): + """更新设备朝向信息""" + try: + data = request.get_json() + + if not data or 'heading' not in data: + return jsonify({ + "status": "error", + "message": "缺少朝向信息" + }) + + heading = float(data['heading']) + + # 标准化角度到0-360范围 + heading = heading % 360 + + # 更新朝向信息 + self.current_web_heading = heading + self.last_web_update = time.time() + + print(f"🧭 Web朝向更新: {heading:.1f}°") + + return jsonify({ + "status": "success", + "message": "朝向信息已更新" + }) + + except Exception as e: + return jsonify({ + "status": "error", + "message": f"朝向更新失败: {str(e)}" + }) + + @self.blueprint.route('/api/orientation/get_status') + def get_orientation_status(): + """获取当前朝向状态""" + try: + current_time = time.time() + + # 检查数据是否过期(30秒) + web_data_fresh = (current_time - self.last_web_update) < 30 + + status = { + "web_location": self.current_web_location, + "web_heading": self.current_web_heading, + "web_data_fresh": web_data_fresh, + "last_update": self.last_web_update, + "current_config": { + "latitude": config.CAMERA_LATITUDE, + "longitude": config.CAMERA_LONGITUDE, + "heading": config.CAMERA_HEADING + } + } + + return jsonify({ + "status": "success", + "data": status + }) + + except Exception as e: + return jsonify({ + "status": "error", + "message": f"状态获取失败: {str(e)}" + }) + + @self.blueprint.route('/api/orientation/apply_config', methods=['POST']) + def apply_config(): + """应用当前的位置和朝向配置""" + try: + if not self.current_web_location: + return jsonify({ + "status": "error", + "message": "没有可用的位置信息" + }) + + lat, lng, accuracy = self.current_web_location + + # 使用Web朝向或默认朝向 + if self.current_web_heading is not None: + # 计算摄像头朝向(朝向用户) + camera_heading = self.orientation_detector.calculate_camera_heading_facing_user( + self.current_web_heading + ) + else: + # 使用默认朝向 + camera_heading = 0.0 + + # 应用配置 + self.orientation_detector.update_camera_config((lat, lng), camera_heading) + + return jsonify({ + "status": "success", + "message": "配置已应用", + "data": { + "latitude": lat, + "longitude": lng, + "camera_heading": camera_heading, + "accuracy": accuracy + } + }) + + except Exception as e: + return jsonify({ + "status": "error", + "message": f"配置应用失败: {str(e)}" + }) + + def process_web_data(self, gps_data: Dict, orientation_data: Optional[Dict] = None) -> Dict: + """ + 处理来自Web端的GPS和朝向数据 + + Args: + gps_data: GPS数据 {'latitude': float, 'longitude': float, 'accuracy': float} + orientation_data: 朝向数据 {'heading': float} (可选) + + Returns: + 配置结果字典 + """ + result = { + 'success': False, + 'gps_location': None, + 'device_heading': None, + 'camera_heading': None, + 'method': 'Web', + 'accuracy': None + } + + try: + # 处理GPS数据 + lat = float(gps_data['latitude']) + lng = float(gps_data['longitude']) + accuracy = float(gps_data.get('accuracy', 1000)) + + # 验证坐标 + if not (-90 <= lat <= 90) or not (-180 <= lng <= 180): + raise ValueError("坐标范围不正确") + + result['gps_location'] = (lat, lng) + result['accuracy'] = accuracy + + # 处理朝向数据 + device_heading = 0.0 # 默认朝向 + if orientation_data and 'heading' in orientation_data: + device_heading = float(orientation_data['heading']) % 360 + + result['device_heading'] = device_heading + + # 计算摄像头朝向(面向用户) + camera_heading = self.orientation_detector.calculate_camera_heading_facing_user(device_heading) + result['camera_heading'] = camera_heading + + result['success'] = True + + print(f"✅ Web数据处理完成:") + print(f"📍 GPS位置: ({lat:.6f}, {lng:.6f})") + print(f"🧭 设备朝向: {device_heading:.1f}°") + print(f"📷 摄像头朝向: {camera_heading:.1f}°") + print(f"📏 定位精度: ±{accuracy:.0f}m") + + except Exception as e: + print(f"❌ Web数据处理失败: {e}") + + return result + + def get_blueprint(self): + """获取Flask Blueprint""" + return self.blueprint \ No newline at end of file diff --git a/distance-judgement/src/web_server.py b/distance-judgement/src/web_server.py new file mode 100644 index 00000000..6be0db32 --- /dev/null +++ b/distance-judgement/src/web_server.py @@ -0,0 +1,3961 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from flask import Flask, jsonify, request +import threading +import time +import cv2 +import base64 +import numpy as np +from . import config +from .person_detector import PersonDetector +from .distance_calculator import DistanceCalculator +from .map_manager import MapManager +from .mobile_connector import MobileConnector +from .web_orientation_detector import WebOrientationDetector + +class WebServer: + """Web服务器类,管理地图界面和摄像头控制""" + + def __init__(self): + self.app = Flask(__name__) + self.map_manager = MapManager( + api_key=config.GAODE_API_KEY, + camera_lat=config.CAMERA_LATITUDE, + camera_lng=config.CAMERA_LONGITUDE + ) + + # 摄像头相关 + self.camera_active = False + self.cap = None + self.detector = None + self.distance_calculator = None + self.camera_thread = None + self.current_frame = None + self.detection_data = [] + + # 手机连接相关 + self.mobile_connector = MobileConnector(port=8080) + self.mobile_frames = {} # device_id -> latest frame + self.mobile_mode = False # 是否启用手机模式 + + # 🌟 移动端实时数据存储 + self.mobile_locations = {} # device_id -> latest location + self.mobile_orientations = {} # device_id -> latest orientation + + # 朝向检测相关 + self.web_orientation_detector = WebOrientationDetector() + + # 设置路由 + self.setup_routes() + + # 设置手机连接器回调 + self.setup_mobile_callbacks() + + # 注册朝向检测API + self.app.register_blueprint(self.web_orientation_detector.get_blueprint()) + + # 设置安全头部 + self.setup_security_headers() + + def setup_security_headers(self): + """设置HTTP安全头部""" + @self.app.after_request + def add_security_headers(response): + """为所有响应添加安全头部""" + response.headers['X-Content-Type-Options'] = 'nosniff' + response.headers['X-Frame-Options'] = 'SAMEORIGIN' + response.headers['X-XSS-Protection'] = '1; mode=block' + response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin' + return response + + def setup_routes(self): + """设置Flask路由""" + + @self.app.route('/') + def index(): + """主页面 - 显示地图和控制界面""" + return self.generate_main_page() + + @self.app.route('/mobile/mobile_client.html') + def mobile_client(): + """手机端客户端页面""" + return self.serve_mobile_client() + + @self.app.route('/mobile/gps_test.html') + def gps_test(): + """GPS测试页面""" + return self.serve_gps_test() + + @self.app.route('/mobile/permission_guide.html') + def permission_guide(): + """权限设置指南页面""" + return self.serve_permission_guide() + + @self.app.route('/mobile/baidu_browser_test.html') + def baidu_browser_test(): + """百度浏览器测试页面""" + return self.serve_baidu_browser_test() + + @self.app.route('/mobile/camera_permission_test.html') + def camera_permission_test(): + """摄像头权限测试页面""" + return self.serve_camera_permission_test() + + @self.app.route('/mobile/browser_compatibility_guide.html') + def browser_compatibility_guide(): + """浏览器兼容性指南页面""" + return self.serve_browser_compatibility_guide() + + @self.app.route('/mobile/legacy_browser_help.html') + def legacy_browser_help(): + """旧版浏览器帮助页面""" + return self.serve_legacy_browser_help() + + @self.app.route('/mobile/harmonyos_camera_fix.html') + def harmonyos_camera_fix(): + """鸿蒙系统摄像头修复页面""" + return self.serve_harmonyos_camera_fix() + + @self.app.route('/test_device_selector.html') + def test_device_selector(): + """设备选择器测试页面""" + try: + with open('test_device_selector.html', 'r', encoding='utf-8') as f: + return f.read() + except FileNotFoundError: + return "测试页面未找到", 404 + + @self.app.route('/api/start_camera', methods=['POST']) + def start_camera(): + """启动摄像头""" + try: + print("🔍 API调用: /api/start_camera") + if not self.camera_active: + print("📷 摄像头当前状态: 离线,正在启动...") + self.start_camera_detection() + # 等待一下检查启动是否成功 + time.sleep(0.5) + if self.camera_active: + print("✅ 摄像头启动成功") + return jsonify({"status": "success", "message": "摄像头已启动", "camera_active": True}) + else: + print("❌ 摄像头启动失败") + return jsonify({"status": "error", "message": "摄像头启动失败,请检查设备连接"}) + else: + print("⚠️ 摄像头已在运行中") + return jsonify({"status": "warning", "message": "摄像头已在运行中", "camera_active": True}) + except Exception as e: + print(f"❌ 摄像头启动异常: {e}") + import traceback + traceback.print_exc() + return jsonify({"status": "error", "message": f"启动失败: {str(e)}"}) + + @self.app.route('/api/stop_camera', methods=['POST']) + def stop_camera(): + """停止摄像头""" + try: + self.stop_camera_detection() + return jsonify({"status": "success", "message": "摄像头已停止"}) + except Exception as e: + return jsonify({"status": "error", "message": f"停止失败: {str(e)}"}) + + @self.app.route('/api/get_persons_data') + def get_persons_data(): + """获取人员检测数据""" + data = self.map_manager.get_persons_data() + print(f"🌐 API调用 /api/get_persons_data 返回 {len(data)} 个人员数据") + return jsonify(data) + + @self.app.route('/api/get_mobile_devices_data') + def get_mobile_devices_data(): + """获取移动端设备的实时位置和朝向数据""" + devices_data = [] + current_time = time.time() + + # 获取所有活跃的移动设备数据 + for device_id in set(list(self.mobile_locations.keys()) + list(self.mobile_orientations.keys())): + device_info = {} + + # 获取位置数据 + location_data = self.mobile_locations.get(device_id) + if location_data and (current_time - location_data['timestamp']) < 30: # 30秒内的数据有效 + device_info['location'] = { + 'latitude': location_data['latitude'], + 'longitude': location_data['longitude'], + 'accuracy': location_data['accuracy'], + 'timestamp': location_data['timestamp'] + } + + # 获取朝向数据 + orientation_data = self.mobile_orientations.get(device_id) + if orientation_data and (current_time - orientation_data['timestamp']) < 30: # 30秒内的数据有效 + device_info['orientation'] = { + 'heading': orientation_data['heading'], + 'tilt': orientation_data['tilt'], + 'roll': orientation_data['roll'], + 'timestamp': orientation_data['timestamp'] + } + + # 只有有数据的设备才加入返回列表 + if device_info: + device_info['device_id'] = device_id + devices_data.append(device_info) + + return jsonify(devices_data) + + @self.app.route('/api/debug_info') + def debug_info(): + """获取调试信息""" + debug_data = { + "camera_active": self.camera_active, + "persons_count": len(self.map_manager.get_persons_data()), + "persons_data": self.map_manager.get_persons_data(), + "detection_count": len(self.detection_data) if self.detection_data else 0, + "camera_position": { + "lat": config.CAMERA_LATITUDE, + "lng": config.CAMERA_LONGITUDE, + "heading": config.CAMERA_HEADING + } + } + return jsonify(debug_data) + + @self.app.route('/api/get_camera_frame') + def get_camera_frame(): + """获取当前摄像头帧(base64编码)""" + if self.current_frame is not None: + _, buffer = cv2.imencode('.jpg', self.current_frame) + frame_base64 = base64.b64encode(buffer).decode('utf-8') + return jsonify({ + "status": "success", + "frame": f"data:image/jpeg;base64,{frame_base64}", + "active": self.camera_active + }) + else: + return jsonify({ + "status": "no_frame", + "frame": None, + "active": self.camera_active + }) + + # 手机端API路由 + @self.app.route('/mobile/ping', methods=['POST']) + def mobile_ping(): + """手机端连接测试""" + try: + data = request.get_json() + device_id = data.get('device_id') + return jsonify({"status": "success", "server_time": time.time(), "device_id": device_id}) + except Exception as e: + return jsonify({"status": "error", "message": str(e)}), 400 + + @self.app.route('/mobile/upload', methods=['POST']) + def mobile_upload(): + """接收手机端数据""" + try: + data = request.get_json() + self.process_mobile_data(data) + return jsonify({"status": "success", "timestamp": time.time()}) + except Exception as e: + print(f"⚠️ 处理手机数据错误: {e}") + return jsonify({"status": "error", "message": str(e)}), 400 + + @self.app.route('/api/mobile/realtime_data', methods=['POST']) + def mobile_realtime_data(): + """接收移动端实时GPS和朝向数据""" + try: + data = request.get_json() or {} + device_id = data.get('device_id') + data_type = data.get('type') + payload = data.get('data', {}) + + if not device_id or not data_type: + return jsonify({ + "status": "error", + "message": "设备ID和数据类型不能为空" + }), 400 + + # 处理不同类型的数据 + if data_type == 'location': + return self.handle_mobile_location_data(device_id, payload) + elif data_type == 'orientation': + return self.handle_mobile_orientation_data(device_id, payload) + else: + return jsonify({ + "status": "error", + "message": f"不支持的数据类型: {data_type}" + }), 400 + + except Exception as e: + print(f"❌ 移动端实时数据处理错误: {e}") + return jsonify({ + "status": "error", + "message": f"数据处理失败: {str(e)}" + }), 500 + + @self.app.route('/api/mobile/devices') + def get_mobile_devices(): + """获取连接的手机设备列表""" + devices = [] + + # 首先添加Socket连接的设备 + for device in self.mobile_connector.get_online_devices(): + devices.append({ + "device_id": device.device_id, + "device_name": device.device_name, + "battery_level": device.battery_level, + "is_online": device.is_online, + "last_seen": device.last_seen, + "location": device.current_location + }) + + # 然后添加HTTP API连接的设备 + current_time = time.time() + for device_id, frame_data in self.mobile_frames.items(): + # 检查是否已经在Socket设备列表中 + device_exists = any(d["device_id"] == device_id for d in devices) + if not device_exists: + device_info = frame_data.get('device_info', {}) + last_seen = frame_data.get('timestamp', current_time) + + # 设备被认为在线如果最后一次见到的时间在30秒内 + is_online = (current_time - last_seen) < 30 + + location = None + gps_data = device_info.get('gps') + if gps_data and gps_data.get('latitude') and gps_data.get('longitude'): + location = { + "lat": gps_data['latitude'], + "lng": gps_data['longitude'], + "accuracy": gps_data.get('accuracy', 0) + } + + devices.append({ + "device_id": device_id, + "device_name": device_info.get('device_name', f'HTTP-Device-{device_id[:8]}'), + "battery_level": device_info.get('battery', 100), + "is_online": is_online, + "last_seen": last_seen, + "location": location, + "connection_type": "HTTP" + }) + + return jsonify(devices) + + @self.app.route('/api/mobile/toggle', methods=['POST']) + def toggle_mobile_mode(): + """切换手机模式""" + try: + if not self.mobile_mode: + # 启动手机模式 + if self.mobile_connector.start_server(): + self.mobile_mode = True + # 停止本地摄像头 + if self.camera_active: + self.stop_camera_detection() + return jsonify({"status": "success", "message": "手机模式已启动", "mobile_mode": True}) + else: + return jsonify({"status": "error", "message": "手机服务器启动失败"}) + else: + # 停止手机模式 + self.mobile_connector.stop_server() + self.mobile_mode = False + self.mobile_frames.clear() + self.map_manager.clear_persons() + return jsonify({"status": "success", "message": "手机模式已停止", "mobile_mode": False}) + except Exception as e: + return jsonify({"status": "error", "message": str(e)}) + + @self.app.route('/api/connect_remote_device', methods=['POST']) + def connect_remote_device(): + """连接到远程设备""" + try: + data = request.get_json() + device_id = data.get('device_id') + client_id = data.get('client_id') + + # 检查设备是否存在 - 首先检查Socket连接的设备 + target_device = None + for device in self.mobile_connector.get_online_devices(): + if device.device_id == device_id: + target_device = device + break + + # 如果在Socket设备中没找到,检查HTTP连接的设备 + if not target_device and device_id in self.mobile_frames: + frame_data = self.mobile_frames[device_id] + device_info = frame_data.get('device_info', {}) + current_time = time.time() + last_seen = frame_data.get('timestamp', current_time) + + # 检查设备是否在线(30秒内有数据) + if (current_time - last_seen) < 30: + # 创建一个临时设备对象用于响应 + target_device = { + 'device_id': device_id, + 'device_name': device_info.get('device_name', f'HTTP-Device-{device_id[:8]}'), + 'battery_level': device_info.get('battery', 100), + 'current_location': None + } + + # 如果有GPS数据,添加位置信息 + gps_data = device_info.get('gps') + if gps_data and gps_data.get('latitude') and gps_data.get('longitude'): + target_device['current_location'] = { + "lat": gps_data['latitude'], + "lng": gps_data['longitude'], + "accuracy": gps_data.get('accuracy', 0) + } + + if not target_device: + return jsonify({"status": "error", "message": "目标设备未找到或已离线"}), 404 + + # 建立连接关系 + connection_info = { + "client_id": client_id, + "device_id": device_id, + "connected_at": time.time(), + "status": "connected" + } + + # 存储连接信息(这里可以扩展为更复杂的连接管理) + if not hasattr(self, 'remote_connections'): + self.remote_connections = {} + self.remote_connections[f"{client_id}->{device_id}"] = connection_info + + # 处理不同格式的设备数据 + if isinstance(target_device, dict): + # HTTP连接的设备(字典格式) + device_name = target_device['device_name'] + device_info = { + "device_id": target_device['device_id'], + "device_name": target_device['device_name'], + "battery_level": target_device['battery_level'], + "location": target_device['current_location'] + } + else: + # Socket连接的设备(对象格式) + device_name = target_device.device_name + device_info = { + "device_id": target_device.device_id, + "device_name": target_device.device_name, + "battery_level": target_device.battery_level, + "location": target_device.current_location + } + + return jsonify({ + "status": "success", + "message": f"已连接到设备 {device_name}", + "device_info": device_info + }) + + except Exception as e: + return jsonify({"status": "error", "message": str(e)}), 500 + + @self.app.route('/api/remote_device/stream/') + def get_remote_device_stream(device_id): + """获取远程设备的视频流""" + try: + # 获取指定设备的最新帧 + if device_id in self.mobile_frames: + frame_data = self.mobile_frames[device_id] + device_info = frame_data.get('device_info', {}) + + # 准备返回数据,包含GPS信息 + response_data = { + "status": "success", + "frame": f"data:image/jpeg;base64,{frame_data.get('frame')}", + "timestamp": frame_data.get('timestamp'), + "device_info": device_info + } + + # 如果有GPS数据,加入响应中 + gps_data = device_info.get('gps') + if gps_data and gps_data.get('latitude') and gps_data.get('longitude'): + response_data['gps'] = { + "lat": gps_data['latitude'], + "lng": gps_data['longitude'], + "accuracy": gps_data.get('accuracy', 0) + } + + return jsonify(response_data) + else: + return jsonify({ + "status": "no_frame", + "message": "设备无视频数据" + }), 404 + except Exception as e: + return jsonify({"status": "error", "message": str(e)}), 500 + + @self.app.route('/favicon.ico') + def favicon(): + return '', 204 # 返回空内容,状态码204表示无内容 + + def draw_info_panel(self, frame, person_count=0): + """绘制信息面板 - 完全复制main.py的逻辑""" + height, width = frame.shape[:2] + + # 绘制顶部信息栏 + info_height = 60 + cv2.rectangle(frame, (0, 0), (width, info_height), (0, 0, 0), -1) + + # 显示FPS(Web版本不需要实时FPS,显示固定文本) + fps_text = "Web Mode" + cv2.putText(frame, fps_text, (10, 25), config.FONT, 0.6, (0, 255, 0), 2) + + # 显示人员计数 + person_text = f"Persons: {person_count}" + cv2.putText(frame, person_text, (150, 25), config.FONT, 0.6, (0, 255, 255), 2) + + # 显示模型信息 + if hasattr(self, 'person_detector') and self.person_detector: + model_text = self.person_detector.get_model_info() + else: + model_text = "YOLO Model: Loading..." + cv2.putText(frame, model_text, (10, 45), config.FONT, 0.5, (255, 255, 255), 1) + + # 显示时间信息 + time_text = f"Time: {time.strftime('%H:%M:%S')}" + cv2.putText(frame, time_text, (width - 150, 25), config.FONT, 0.5, (255, 255, 0), 1) + + # 显示地图状态 + map_status = "Map: ON" + cv2.putText(frame, map_status, (10, height - 10), + config.FONT, 0.5, (0, 255, 255), 1) + + # 显示摄像头信息 + camera_info = f"Camera: Local ({config.CAMERA_LATITUDE:.4f}, {config.CAMERA_LONGITUDE:.4f})" + cv2.putText(frame, camera_info, (width - 400, height - 10), + config.FONT, 0.4, (255, 255, 255), 1) + + return frame + + def draw_backend_info_panel(self, frame, person_count=0, fps=0): + """绘制后端摄像头的信息面板 - 显示实时FPS""" + height, width = frame.shape[:2] + + # 绘制顶部信息栏 + info_height = 60 + cv2.rectangle(frame, (0, 0), (width, info_height), (0, 0, 0), -1) + + # 显示实时FPS + fps_text = f"FPS: {fps}" + cv2.putText(frame, fps_text, (10, 25), config.FONT, 0.6, (0, 255, 0), 2) + + # 显示人员计数 + person_text = f"Persons: {person_count}" + cv2.putText(frame, person_text, (150, 25), config.FONT, 0.6, (0, 255, 255), 2) + + # 显示模型信息 + if hasattr(self, 'detector') and self.detector: + model_text = self.detector.get_model_info() + else: + model_text = "YOLO Model: Loading..." + cv2.putText(frame, model_text, (10, 45), config.FONT, 0.5, (255, 255, 255), 1) + + # 显示时间信息 + time_text = f"Time: {time.strftime('%H:%M:%S')}" + cv2.putText(frame, time_text, (width - 150, 25), config.FONT, 0.5, (255, 255, 0), 1) + + # 显示地图状态 + map_status = "Map: ON" + cv2.putText(frame, map_status, (10, height - 10), + config.FONT, 0.5, (0, 255, 255), 1) + + # 显示摄像头信息 + camera_info = f"Camera: Backend ({config.CAMERA_LATITUDE:.4f}, {config.CAMERA_LONGITUDE:.4f})" + cv2.putText(frame, camera_info, (width - 450, height - 10), + config.FONT, 0.4, (255, 255, 255), 1) + + # 显示操作提示 + help_text = "Real-time Detection Mode" + cv2.putText(frame, help_text, (width - 250, 45), + config.FONT, 0.5, (255, 255, 0), 1) + + return frame + + def setup_mobile_callbacks(self): + """设置手机连接器回调函数""" + self.mobile_connector.add_frame_callback(self.on_mobile_frame) + self.mobile_connector.add_location_callback(self.on_mobile_location) + self.mobile_connector.add_device_callback(self.on_mobile_device_event) + + def process_mobile_data(self, data): + """处理手机端发送的数据""" + device_id = data.get('device_id') + if not device_id: + raise ValueError("缺少device_id") + + # 处理图像数据 + if 'frame' in data: + try: + # 存储base64图像数据(用于远程设备流) + self.mobile_frames[device_id] = { + 'frame': data['frame'], + 'timestamp': data.get('timestamp', time.time()), + 'device_info': { + 'device_name': data.get('device_name', 'Unknown'), + 'battery': data.get('battery', 100), + 'gps': data.get('gps') + } + } + + # 解码base64图像用于检测 + frame_data = base64.b64decode(data['frame']) + frame = cv2.imdecode(np.frombuffer(frame_data, np.uint8), cv2.IMREAD_COLOR) + + if frame is not None: + + # 如果启用了检测器,进行人体检测 + if not self.detector: + self.detector = PersonDetector() + self.distance_calculator = DistanceCalculator() + + # 处理GPS和设备信息 + gps_data = data.get('gps') + if gps_data and gps_data.get('latitude') and gps_data.get('longitude'): + self.process_mobile_detection(device_id, frame, gps_data) + + print(f"📱 收到设备 {device_id[:8]} 的图像数据") + + except Exception as e: + print(f"⚠️ 处理手机图像数据错误: {e}") + raise + + def process_mobile_detection(self, device_id, frame, gps_data): + """处理手机图像的人体检测""" + try: + # 存储移动设备的最新GPS位置 + if gps_data: + print(f"📍 设备 {device_id[:8]} GPS位置: ({gps_data['latitude']:.6f}, {gps_data['longitude']:.6f})") + + # 更新设备在移动连接器中的位置信息 + if hasattr(self.mobile_connector, 'update_device_location'): + self.mobile_connector.update_device_location(device_id, gps_data['latitude'], gps_data['longitude']) + + # 创建帧的副本用于绘制检测框 + processed_frame = frame.copy() + + # 检测人体 + detections = self.detector.detect_persons(frame) + + if len(detections) > 0: + print(f"📱 设备 {device_id[:8]} 检测到 {len(detections)} 个人") + + # 清除地图上该设备的旧标记 + self.map_manager.clear_persons() + + for i, detection in enumerate(detections): + bbox = detection[:4] # [x1, y1, x2, y2] + x1, y1, x2, y2 = bbox + + # 绘制检测框 + cv2.rectangle(processed_frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) + + # 计算距离(基于手机摄像头) + distance = self.distance_calculator.get_distance(bbox) + distance_meters = distance / 100.0 + + # 绘制距离标签 + label = f"Person {i+1}: {distance_meters:.1f}m" + label_size = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2)[0] + cv2.rectangle(processed_frame, (int(x1), int(y1-label_size[1]-10)), + (int(x1+label_size[0]), int(y1)), (0, 255, 0), -1) + cv2.putText(processed_frame, label, (int(x1), int(y1-5)), + cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 2) + + # 🎯 使用精确的GPS坐标计算 + # 获取设备朝向信息 + device_heading = 0 # 默认朝向正北 + if device_id in self.mobile_orientations: + orientation_data = self.mobile_orientations[device_id] + if time.time() - orientation_data['timestamp'] < 30: # 30秒内的朝向数据有效 + device_heading = orientation_data['heading'] + + # 使用距离计算器的精确算法计算人员GPS坐标 + frame_height, frame_width = frame.shape[:2] + person_lat, person_lng = self.distance_calculator.calculate_person_gps_position( + camera_lat=gps_data['latitude'], + camera_lng=gps_data['longitude'], + camera_heading=device_heading, + bbox=bbox, + distance_meters=distance_meters, + frame_width=frame_width, + frame_height=frame_height, + camera_fov=60 # 假设移动设备摄像头视场角为60度 + ) + + # 🔍 检查人员是否在摄像头视野范围内 + in_fov = self.distance_calculator.is_person_in_camera_fov( + camera_lat=gps_data['latitude'], + camera_lng=gps_data['longitude'], + camera_heading=device_heading, + person_lat=person_lat, + person_lng=person_lng, + camera_fov=60, + max_distance=50 # 最大检测距离50米 + ) + + if in_fov: + # 添加到地图 + self.map_manager.add_person_at_coordinates( + person_lat, person_lng, + f"Mobile-P{i+1}", + distance_meters, + device_id + ) + print(f"📍 手机检测人员 {i+1}: 距离{distance_meters:.1f}m, 坐标({person_lat:.6f}, {person_lng:.6f}) ✅视野内") + else: + print(f"⚠️ 手机检测人员 {i+1}: 距离{distance_meters:.1f}m, 坐标({person_lat:.6f}, {person_lng:.6f}) ❌超出视野") + else: + # 即使没有检测到人员,也要清除旧标记,避免误导 + print(f"📱 设备 {device_id[:8]} 未检测到人员") + + # 绘制信息面板 + processed_frame = self.draw_backend_info_panel(processed_frame, len(detections), fps=60) + + # 在顶部添加移动设备标识 + device_label = f"Mobile Device: {device_id[:8]}" + cv2.putText(processed_frame, device_label, (10, 80), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 0), 2) + + # 更新当前帧供前端显示 + self.current_frame = processed_frame + + # 标记摄像头为活跃状态 + self.camera_active = True + + except Exception as e: + print(f"⚠️ 手机检测处理错误: {e}") + + def on_mobile_frame(self, device_id, frame, device): + """手机帧数据回调""" + # 这个方法会被MobileConnector调用,但我们已经在process_mobile_data中处理了 + pass + + def on_mobile_location(self, device_id, location, device): + """手机位置数据回调""" + lat, lng, accuracy = location + print(f"📍 设备 {device_id[:8]} 位置更新: ({lat:.6f}, {lng:.6f})") + + def on_mobile_device_event(self, event_type, device): + """手机设备事件回调""" + if event_type == 'device_connected': + print(f"📱 新设备连接: {device.device_name} ({device.device_id[:8]})") + elif event_type == 'device_disconnected': + print(f"📱 设备断开: {device.device_name} ({device.device_id[:8]})") + # 清理该设备的数据 + if device.device_id in self.mobile_frames: + del self.mobile_frames[device.device_id] + # 🌟 清理实时数据 + if device.device_id in self.mobile_locations: + del self.mobile_locations[device.device_id] + if device.device_id in self.mobile_orientations: + del self.mobile_orientations[device.device_id] + + def handle_mobile_location_data(self, device_id, location_data): + """处理移动端GPS位置数据""" + try: + latitude = location_data.get('latitude') + longitude = location_data.get('longitude') + accuracy = location_data.get('accuracy', 100) + timestamp = location_data.get('timestamp', time.time()) + + if latitude is None or longitude is None: + return jsonify({ + "status": "error", + "message": "GPS坐标数据不完整" + }), 400 + + # 存储位置数据 + self.mobile_locations[device_id] = { + 'latitude': latitude, + 'longitude': longitude, + 'accuracy': accuracy, + 'timestamp': timestamp + } + + print(f"📍 设备 {device_id[:8]} GPS更新: ({latitude:.6f}, {longitude:.6f}) ±{accuracy}m") + + return jsonify({ + "status": "success", + "message": "GPS位置数据已接收" + }) + + except Exception as e: + print(f"❌ GPS数据处理错误: {e}") + return jsonify({ + "status": "error", + "message": f"GPS数据处理失败: {str(e)}" + }), 500 + + def handle_mobile_orientation_data(self, device_id, orientation_data): + """处理移动端设备朝向数据""" + try: + heading = orientation_data.get('heading') + tilt = orientation_data.get('tilt') + roll = orientation_data.get('roll') + timestamp = orientation_data.get('timestamp', time.time()) + + if heading is None: + return jsonify({ + "status": "error", + "message": "朝向数据不完整" + }), 400 + + # 存储朝向数据 + self.mobile_orientations[device_id] = { + 'heading': heading, + 'tilt': tilt, + 'roll': roll, + 'timestamp': timestamp + } + + print(f"🧭 设备 {device_id[:8]} 朝向更新: {heading:.1f}° (倾斜:{tilt:.1f}°)") + + return jsonify({ + "status": "success", + "message": "朝向数据已接收" + }) + + except Exception as e: + print(f"❌ 朝向数据处理错误: {e}") + return jsonify({ + "status": "error", + "message": f"朝向数据处理失败: {str(e)}" + }), 500 + + def serve_mobile_client(self): + """提供手机端客户端页面""" + try: + with open('mobile/mobile_client.html', 'r', encoding='utf-8') as f: + return f.read() + except FileNotFoundError: + return """ + + 文件未找到 + +

    ❌ 手机端客户端文件未找到

    +

    请确保 mobile/mobile_client.html 文件存在

    +

    或者直接访问该文件的完整路径

    + + + """, 404 + + def serve_gps_test(self): + """提供GPS测试页面""" + try: + with open('mobile/gps_test.html', 'r', encoding='utf-8') as f: + return f.read() + except FileNotFoundError: + return """ + + 文件未找到 + +

    ❌ GPS测试页面未找到

    +

    请确保 mobile/gps_test.html 文件存在

    + + + """, 404 + + def serve_permission_guide(self): + """提供权限设置指南页面""" + try: + with open('mobile/permission_guide.html', 'r', encoding='utf-8') as f: + return f.read() + except FileNotFoundError: + return """ + + 文件未找到 + +

    ❌ 权限指南页面未找到

    +

    请确保 mobile/permission_guide.html 文件存在

    + + + """, 404 + + def serve_baidu_browser_test(self): + """提供百度浏览器测试页面""" + try: + with open('mobile/baidu_browser_test.html', 'r', encoding='utf-8') as f: + return f.read() + except FileNotFoundError: + return """ + + 文件未找到 + +

    ❌ 百度浏览器测试页面未找到

    +

    请确保 mobile/baidu_browser_test.html 文件存在

    + 返回移动客户端 + + + """, 404 + + def serve_camera_permission_test(self): + """提供摄像头权限测试页面""" + try: + with open('mobile/camera_permission_test.html', 'r', encoding='utf-8') as f: + return f.read() + except FileNotFoundError: + return """ + + 文件未找到 + +

    ❌ 摄像头权限测试页面未找到

    +

    请确保 mobile/camera_permission_test.html 文件存在

    + 返回移动客户端 + + + """, 404 + + def serve_browser_compatibility_guide(self): + """提供浏览器兼容性指南页面""" + try: + with open('mobile/browser_compatibility_guide.html', 'r', encoding='utf-8') as f: + return f.read() + except FileNotFoundError: + return """ + + 文件未找到 + +

    ❌ 浏览器兼容性指南页面未找到

    +

    请确保 mobile/browser_compatibility_guide.html 文件存在

    + 返回移动客户端 + + + """, 404 + + def serve_legacy_browser_help(self): + """提供旧版浏览器帮助页面""" + try: + with open('mobile/legacy_browser_help.html', 'r', encoding='utf-8') as f: + return f.read() + except FileNotFoundError: + return """ + + 文件未找到 + +

    ❌ 旧版浏览器帮助页面未找到

    +

    请确保 mobile/legacy_browser_help.html 文件存在

    + 返回移动客户端 + + + """, 404 + + def serve_harmonyos_camera_fix(self): + """提供鸿蒙系统摄像头修复页面""" + try: + with open('mobile/harmonyos_camera_fix.html', 'r', encoding='utf-8') as f: + return f.read() + except FileNotFoundError: + return """ + + 文件未找到 + +

    ❌ 鸿蒙系统摄像头修复页面未找到

    +

    请确保 mobile/harmonyos_camera_fix.html 文件存在

    + 返回移动客户端 + + + """, 404 + + def start_camera_detection(self): + """启动摄像头检测""" + if self.camera_active: + print("⚠️ 摄像头已在运行中") + return + + try: + print("=" * 60) + print(f"🔍 开始摄像头检测初始化...") + print(f"🔍 当前时间: {time.strftime('%Y-%m-%d %H:%M:%S')}") + print(f"🔍 配置的摄像头索引: {config.CAMERA_INDEX}") + print(f"🔍 当前camera_active状态: {self.camera_active}") + + # 检查OpenCV是否正常 + print(f"🔍 OpenCV版本: {cv2.__version__}") + + # 检查系统摄像头 + print("🔍 检查系统可用摄像头...") + available_cameras = [] + for i in range(10): + test_cap = cv2.VideoCapture(i) + if test_cap.isOpened(): + available_cameras.append(i) + test_cap.release() + print(f" ✅ 找到摄像头索引: {i}") + else: + test_cap.release() + + if not available_cameras: + raise Exception("系统中没有找到任何可用的摄像头设备") + + print(f"🔍 可用摄像头索引: {available_cameras}") + + # 初始化摄像头 + print(f"🔍 正在尝试打开摄像头索引: {config.CAMERA_INDEX}") + self.cap = cv2.VideoCapture(config.CAMERA_INDEX) + print(f"🔍 VideoCapture对象创建完成: {self.cap}") + print(f"🔍 cap.isOpened(): {self.cap.isOpened()}") + + # 等待摄像头初始化 + time.sleep(1) + + if not self.cap.isOpened(): + print(f"❌ 初始摄像头索引 {config.CAMERA_INDEX} 打开失败") + # 尝试其他摄像头索引 + success = False + for i in available_cameras: + print(f"🔍 尝试备用摄像头索引: {i}") + if self.cap: + self.cap.release() + self.cap = cv2.VideoCapture(i) + time.sleep(0.8) # 增加等待时间 + print(f"🔍 索引 {i} isOpened(): {self.cap.isOpened()}") + if self.cap.isOpened(): + print(f"✅ 成功打开备用摄像头索引: {i}") + success = True + break + else: + print(f"❌ 索引 {i} 打开失败") + self.cap.release() + + if not success: + raise Exception(f"无法打开任何摄像头,尝试了索引: {available_cameras}") + else: + print(f"✅ 初始摄像头索引 {config.CAMERA_INDEX} 打开成功") + + # 设置摄像头参数 + print(f"📐 设置摄像头参数: {config.FRAME_WIDTH}x{config.FRAME_HEIGHT}@{config.FPS}fps") + self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, config.FRAME_WIDTH) + self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, config.FRAME_HEIGHT) + self.cap.set(cv2.CAP_PROP_FPS, config.FPS) + + # 测试读取一帧 + print("🔍 测试摄像头帧读取...") + for attempt in range(3): + ret, test_frame = self.cap.read() + print(f"🔍 读取尝试 {attempt + 1}: ret={ret}, frame={'有效' if test_frame is not None else '无效'}") + if ret and test_frame is not None: + print(f"✅ 摄像头测试成功,实际分辨率: {test_frame.shape[1]}x{test_frame.shape[0]}") + break + time.sleep(0.5) + else: + raise Exception("摄像头无法读取图像帧,连续3次尝试都失败") + + # 初始化检测器 + print("🤖 初始化YOLO检测器...") + try: + self.detector = PersonDetector() + print("✅ PersonDetector初始化完成") + self.distance_calculator = DistanceCalculator() + print("✅ DistanceCalculator初始化完成") + except Exception as e: + print(f"❌ 检测器初始化失败: {e}") + raise e + + # 启动检测线程 + print("🔍 设置camera_active = True") + self.camera_active = True + print(f"🔍 当前camera_active状态: {self.camera_active}") + + print("🔍 创建摄像头检测线程...") + self.camera_thread = threading.Thread(target=self.camera_detection_loop, name="CameraDetectionThread") + self.camera_thread.daemon = True + + print("🔍 启动摄像头检测线程...") + self.camera_thread.start() + print(f"🔍 线程是否存活: {self.camera_thread.is_alive()}") + + # 等待线程启动 + time.sleep(1) + print(f"🔍 启动后camera_active状态: {self.camera_active}") + print(f"🔍 线程启动后状态: {self.camera_thread.is_alive()}") + + print("🎯 摄像头检测线程启动完成") + print("=" * 60) + + except Exception as e: + self.camera_active = False + if hasattr(self, 'cap') and self.cap: + self.cap.release() + print(f"❌ 摄像头启动失败: {e}") + raise e + + def stop_camera_detection(self): + """停止摄像头检测""" + self.camera_active = False + if self.camera_thread: + self.camera_thread.join(timeout=2) + if self.cap: + self.cap.release() + self.current_frame = None + self.detection_data = [] + self.map_manager.clear_persons() + print("📷 摄像头检测已停止") + + def camera_detection_loop(self): + """摄像头检测循环""" + fps_counter = 0 + fps_time = time.time() + current_fps = 0 + frame_count = 0 + + print("=" * 50) + print("🔍 摄像头检测循环已启动") + print(f"🔍 线程名称: {threading.current_thread().name}") + print(f"🔍 启动时camera_active: {self.camera_active}") + print(f"🔍 启动时cap对象: {self.cap}") + print(f"🔍 启动时cap.isOpened(): {self.cap.isOpened() if self.cap else 'cap为None'}") + print("=" * 50) + + while self.camera_active and self.cap: + try: + # 详细的读帧调试 + if frame_count % 100 == 0: # 每100帧输出一次详细状态 + print(f"🔍 循环状态检查 - 帧{frame_count}: camera_active={self.camera_active}, cap={self.cap is not None}") + + ret, frame = self.cap.read() + if not ret or frame is None: + print(f"❌ 无法读取摄像头画面 - 帧{frame_count}: ret={ret}, frame={'有效' if frame is not None else '无效'}") + print(f"🔍 当前cap状态: {self.cap}") + print(f"🔍 cap.isOpened(): {self.cap.isOpened() if self.cap else 'cap为None'}") + break + + frame_count += 1 + + # 检测人体 + try: + detections = self.detector.detect_persons(frame) + except Exception as detect_error: + print(f"❌ 人体检测错误 - 帧{frame_count}: {detect_error}") + detections = [] + + # 添加调试输出 (每30帧输出一次状态,避免刷屏) + if frame_count % 30 == 0 or len(detections) > 0: + print(f"📹 帧 {frame_count}: 检测到 {len(detections)} 个人 (FPS: {current_fps})") + if len(detections) > 0: + print(f"🎯 检测详情: {[f'person_{i}_{det[4]:.2f}' for i, det in enumerate(detections)]}") + + # 计算距离并更新地图位置 + self.map_manager.clear_persons() + distances = [] + + for i, detection in enumerate(detections): + bbox = detection[:4] # [x1, y1, x2, y2] + x1, y1, x2, y2 = bbox + distance = self.distance_calculator.get_distance(bbox) + distance_str = self.distance_calculator.format_distance(distance) + distances.append(distance_str) + + # 计算人体中心点 + center_x = (x1 + x2) / 2 + center_y = (y1 + y2) / 2 + + # 将距离从厘米转换为米 + distance_meters = distance / 100.0 + + # 🎯 使用精确的GPS坐标计算 - 固定摄像头 + frame_height, frame_width = frame.shape[:2] + lat, lng = self.distance_calculator.calculate_person_gps_position( + camera_lat=config.CAMERA_LATITUDE, + camera_lng=config.CAMERA_LONGITUDE, + camera_heading=config.CAMERA_HEADING, + bbox=bbox, + distance_meters=distance_meters, + frame_width=frame_width, + frame_height=frame_height, + camera_fov=config.CAMERA_FOV + ) + + # 🔍 检查人员是否在摄像头视野范围内 + in_fov = self.distance_calculator.is_person_in_camera_fov( + camera_lat=config.CAMERA_LATITUDE, + camera_lng=config.CAMERA_LONGITUDE, + camera_heading=config.CAMERA_HEADING, + person_lat=lat, + person_lng=lng, + camera_fov=config.CAMERA_FOV, + max_distance=100 # 最大检测距离100米 + ) + + if in_fov: + # 添加到地图 + self.map_manager.add_person_at_coordinates( + lat, lng, f"P{i+1}", distance_meters, "fixed_camera" + ) + + if len(detections) > 0: # 只在有检测时输出详细信息 + if in_fov: + print(f"📍 人员 {i+1}: 像素位置({center_x:.1f}, {center_y:.1f}), 距离{distance_meters:.1f}m, 坐标({lat:.6f}, {lng:.6f}) ✅视野内") + else: + print(f"⚠️ 人员 {i+1}: 像素位置({center_x:.1f}, {center_y:.1f}), 距离{distance_meters:.1f}m, 坐标({lat:.6f}, {lng:.6f}) ❌超出视野") + + # 绘制检测结果(完整的检测框、距离、置信度) + frame = self.detector.draw_detections(frame, detections, distances) + + # 计算FPS + fps_counter += 1 + current_time = time.time() + if current_time - fps_time >= 1.0: + current_fps = fps_counter + fps_counter = 0 + fps_time = current_time + + # 添加完整的信息面板 - 带FPS信息 + frame = self.draw_backend_info_panel(frame, len(detections), current_fps) + + # 保存当前帧 + try: + self.current_frame = frame.copy() + self.detection_data = detections + + # 每100帧验证一次帧保存 + if frame_count % 100 == 0: + print(f"🔍 帧保存状态 - 帧{frame_count}: current_frame={'已保存' if self.current_frame is not None else '保存失败'}") + + except Exception as save_error: + print(f"❌ 帧保存错误 - 帧{frame_count}: {save_error}") + + # 控制帧率 + time.sleep(1/30) # 30 FPS + + except Exception as e: + print(f"❌ 检测循环错误 - 帧{frame_count}: {e}") + import traceback + traceback.print_exc() + break + + print(f"🛑 摄像头检测循环已结束 - 总处理帧数: {frame_count}") + print(f"🔍 结束时camera_active: {self.camera_active}") + print(f"🔍 结束时cap状态: {self.cap}") + print("=" * 50) + + def generate_main_page(self): + """生成主页面HTML""" + html_template = f""" + + + + 🚁 无人机战场态势感知系统 + + + + + +
    + +
    +

    🚁 系统控制台

    +
    +
    + 📍 摄像头位置: + {config.CAMERA_LATITUDE:.4f}, {config.CAMERA_LONGITUDE:.4f} +
    +
    + 🧭 朝向角度: + {config.CAMERA_HEADING}° +
    +
    + 📷 摄像头状态: + 离线 +
    +
    + 👥 检测人数: + 0 +
    +
    + + +
    + + + + + +
    + + +
    +
    + + + + + + +
    + +
    +
    📹 实时视频监控
    +
    + +
    等待视频流...
    +
    +
    + +
    +
    + 💻固定指挥中心 +
    +
    + 🚁移动无人机 +
    +
    + 🔶无人机视野 +
    +
    + 🧑‍🌾检测目标 +
    +
    + +
    + + +
    +
    +

    📷 选择视频设备

    + + +
    +

    📱 本地设备

    +
    +
    正在扫描本地设备...
    +
    +
    + + +
    +

    🌐 远程设备

    +
    +
    暂无远程设备连接
    +
    +
    + +
    + + + +
    +
    +
    + + + + + + +""" + return html_template + + def run(self, host='127.0.0.1', port=5000, debug=False, ssl_enabled=True): + """启动Web服务器""" + print("🌐 启动Web服务器...") + + ssl_context = None + protocol = "http" + + if ssl_enabled: + import os + cert_file = "ssl/cert.pem" + key_file = "ssl/key.pem" + + if os.path.exists(cert_file) and os.path.exists(key_file): + ssl_context = (cert_file, key_file) + protocol = "https" + print("🔒 HTTPS模式已启用") + else: + print("⚠️ SSL证书文件不存在,正在生成...") + self.generate_ssl_certificate() + if os.path.exists(cert_file) and os.path.exists(key_file): + ssl_context = (cert_file, key_file) + protocol = "https" + print("🔒 HTTPS模式已启用") + else: + print("❌ SSL证书生成失败,使用HTTP模式") + ssl_enabled = False + + print(f"📍 访问地址: {protocol}://{host}:{port}") + if ssl_enabled: + print("🔑 注意: 自签名证书会显示安全警告,点击'高级'->'继续访问'即可") + print("🚁 无人机战场态势感知系统已就绪") + + try: + self.app.run(host=host, port=port, debug=debug, threaded=True, ssl_context=ssl_context) + except KeyboardInterrupt: + print("\n🔴 服务器已停止") + self.stop_camera_detection() + self.stop_camera_detection() + + def generate_ssl_certificate(self): + """生成自签名SSL证书""" + try: + import os + import datetime + import ipaddress + from cryptography import x509 + from cryptography.x509.oid import NameOID + from cryptography.hazmat.primitives import hashes, serialization + from cryptography.hazmat.primitives.asymmetric import rsa + + # 创建ssl目录 + ssl_dir = "ssl" + if not os.path.exists(ssl_dir): + os.makedirs(ssl_dir) + + print("🔑 正在生成SSL证书...") + + # 生成私钥 + private_key = rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + ) + + # 创建证书主体 + subject = issuer = x509.Name([ + x509.NameAttribute(NameOID.COUNTRY_NAME, "CN"), + x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Beijing"), + x509.NameAttribute(NameOID.LOCALITY_NAME, "Beijing"), + x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Distance Judgement System"), + x509.NameAttribute(NameOID.COMMON_NAME, "localhost"), + ]) + + # 生成证书 + cert = x509.CertificateBuilder().subject_name( + subject + ).issuer_name( + issuer + ).public_key( + private_key.public_key() + ).serial_number( + x509.random_serial_number() + ).not_valid_before( + datetime.datetime.utcnow() + ).not_valid_after( + datetime.datetime.utcnow() + datetime.timedelta(days=365) + ).add_extension( + x509.SubjectAlternativeName([ + x509.DNSName("localhost"), + x509.DNSName("127.0.0.1"), + x509.IPAddress(ipaddress.IPv4Address("127.0.0.1")), + ]), + critical=False, + ).sign(private_key, hashes.SHA256()) + + # 保存私钥 + key_path = os.path.join(ssl_dir, "key.pem") + with open(key_path, "wb") as f: + f.write(private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption() + )) + + # 保存证书 + cert_path = os.path.join(ssl_dir, "cert.pem") + with open(cert_path, "wb") as f: + f.write(cert.public_bytes(serialization.Encoding.PEM)) + + print(f"✅ SSL证书已生成:") + print(f" 🔑 私钥: {key_path}") + print(f" 📜 证书: {cert_path}") + print(f" 📅 有效期: 365天") + + except ImportError: + print("❌ 缺少cryptography库,请先安装: pip install cryptography") + except Exception as e: + print(f"❌ 生成SSL证书失败: {e}") diff --git a/distance-judgement/ssl/cert.pem b/distance-judgement/ssl/cert.pem new file mode 100644 index 00000000..903d0bf8 --- /dev/null +++ b/distance-judgement/ssl/cert.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDiTCCAnGgAwIBAgIUD45qB5JkkfGfRqN8cZTJ1Q2TE14wDQYJKoZIhvcNAQEL +BQAwaTELMAkGA1UEBhMCQ04xEDAOBgNVBAgMB0JlaWppbmcxEDAOBgNVBAcMB0Jl +aWppbmcxIjAgBgNVBAoMGURpc3RhbmNlIEp1ZGdlbWVudCBTeXN0ZW0xEjAQBgNV +BAMMCWxvY2FsaG9zdDAeFw0yNTA2MjkwODQ2MTRaFw0yNjA2MjkwODQ2MTRaMGkx +CzAJBgNVBAYTAkNOMRAwDgYDVQQIDAdCZWlqaW5nMRAwDgYDVQQHDAdCZWlqaW5n +MSIwIAYDVQQKDBlEaXN0YW5jZSBKdWRnZW1lbnQgU3lzdGVtMRIwEAYDVQQDDAls +b2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3u/JfTd1P +/62wGwE0vAEOOPh0Zxn+lCssp0K9axWTfrvp0oWErcyGCVp+E+QjFOPyf0ocw7BX +31O5UoJtOCYHACutXvp+Vd2YFxptXYU+CN/qj4MF+n28U7AwUiWPqSOy9/IMcdOl +IfDKkSHCLWmUtNC8ot5eG/mYxqDVLZfI3Carclw/hwIYBa18YnaYG0xYM+G13Xpp +yP5itRXLGS8I4GpTCoYFlPq0n+rW81sWNQjw3RmK4t1dF2AWhuDc5nYvRZdf4Qhk +ovwW9n48fRaTfsUDylTVZ9RgmSo3KRWmw8DDCo4rlTtOS4x7fd1l6m1JPgPWg9bX +9Qbz17wGGoUdAgMBAAGjKTAnMCUGA1UdEQQeMByCCWxvY2FsaG9zdIIJMTI3LjAu +MC4xhwR/AAABMA0GCSqGSIb3DQEBCwUAA4IBAQBEneYvDdzdvv65rHUA9UKJzBGs +4+j5ZYhCTl0E1HCVxWVHtheUmpUUTlXd0q40NayD0fqt+Cak+0gxKoh8vj1jceKU +EO2OSMx7GIEETF1DU2mvaEHvlgLC5YC72DzirGrM+e4VXIIf7suvmcvAw42IGMtw +xzEZANYeVY87LYVtJQ0Uw11j2C3dKdQJpEFhldWYwlaLYU6jhtkkiybAa7ZAI1AQ +mL+02Y+IQ2sNOuVL7ltqoo0b5BmD4MXjn0wjcy/ARNlq7LxQcvm9UKQCFWtgPGNh +qP8BBUq2pbJJFoxgjQYqAAL7tbdimWElBXwiOEESAjjIC8l/YG4s8QKWhGcq +-----END CERTIFICATE----- diff --git a/distance-judgement/ssl/key.pem b/distance-judgement/ssl/key.pem new file mode 100644 index 00000000..930da7c3 --- /dev/null +++ b/distance-judgement/ssl/key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC3u/JfTd1P/62w +GwE0vAEOOPh0Zxn+lCssp0K9axWTfrvp0oWErcyGCVp+E+QjFOPyf0ocw7BX31O5 +UoJtOCYHACutXvp+Vd2YFxptXYU+CN/qj4MF+n28U7AwUiWPqSOy9/IMcdOlIfDK +kSHCLWmUtNC8ot5eG/mYxqDVLZfI3Carclw/hwIYBa18YnaYG0xYM+G13XppyP5i +tRXLGS8I4GpTCoYFlPq0n+rW81sWNQjw3RmK4t1dF2AWhuDc5nYvRZdf4QhkovwW +9n48fRaTfsUDylTVZ9RgmSo3KRWmw8DDCo4rlTtOS4x7fd1l6m1JPgPWg9bX9Qbz +17wGGoUdAgMBAAECggEAAJVp+AexNkHRez5xCFrg2XQp+yW7ifWRiM4RbN0xPs0Y +ZJ1BgcwnOTIX7+Q5LdrS2CBitB7zixzCG1qgj2K7nhYg0MJo+pynepOmvNBAyrUa +dP1fCF0eXevqc37zGM5w+lpg6aTxw5ByOJtaNOqfikN4QLNBU6GSwA/Hkm8NP56J +ZtVBfGE/inq4pyoFxLBwfGgYn9sRoo4AgPaUYiCFL7s4CXpkrFAg86sxkt0ak6pa +9Hj9nVIcYdhNlEfvO53pnmU3KeXEGUVaE5CtxATEuYfTqNfb2+CBAUAkd1JTzC6P +YLZC1WnrajC9LbblDgWvKQ2ItuNxPcCQOEgQl0IVRwKBgQDf74VeEaCAzQwY48q8 +/RiuJfCc/C7zAHNk4LuYalWSRFaMfciJSfWHNi2UhTuTYiYdg7rSfdrwLOJg/gz0 +c/H9k5SPwObFP0iXSY7FRsfviA5BJIe5xHyMNs0upiO9bmPA0X948esk4dCaUwWz +TleMHlFSf7gk5sOsL7utYPqF0wKBgQDSCtHnXEaVCzoSrpuw9sEZnNIAqqfPOmfg +OYwjz2yp89X4i/N1Lp15oe2vyfGNF4TzRl5kcGwv534om3PjMF9j4ANgy7BCdAx2 +5YXtoCull8lFd5ansBKM6BYtN/YWABTywxkFxMrR+f7gg7L8ywopGomyyyGc/hX6 +4UWaRQdDTwKBgAzt/31W9zV4oWIuhN40nuAvQJ1P0kYlmIQSlcJPIXG4kGa8PH/w +zURpVGhm6PGxkRHTMU5GBgYoEUoYYRccOrSxeLp0IN7ysHZLwPqTA6hI6snIGi4X +sjlGUMKIxTeC0C+p6PpKvZD7mNfQQ1v/Af8NIRTqWu+Gg3XFq8hu+QgRAoGBAMYh ++MFTFS2xKnXHCgyTp7G+cYa5dJSRlr0368838lwbLGNJuT133IqJSkpBp78dSYem +gJIkTpmduC8b/OR5k/IFtYoQelMlX0Ck4II4ThPlq7IAzjeeatFKeOjs2hEEwL4D +dc4wRdZvCZPGCAhYi1wcsXncDfgm4psG934/0UsXAoGAf1mWndfCOtj3/JqjcAKz +cCpfdwgFnTt0U3SNZ5FMXZ4oCRXcDiKN7VMJg6ZtxCxLgAXN92eF/GdMotIFd0ou +6xXLJzIp0XPc1uh5+VPOEjpqtl/ByURge0sshzce53mrhx6ixgAb2qWBJH/cNmIK +VKGQWzXu+zbojPTSWzJltA0= +-----END PRIVATE KEY----- diff --git a/distance-judgement/test_device_selector.html b/distance-judgement/test_device_selector.html new file mode 100644 index 00000000..0e7e4a6f --- /dev/null +++ b/distance-judgement/test_device_selector.html @@ -0,0 +1,392 @@ + + + + + + + 设备选择器测试 + + + + +
    +

    🧪 设备选择器测试

    + +
    +
    + 📹 视频设备 + +
    +
    + 点击"选择设备"开始使用摄像头 +
    +
    + + + + +
    +
    系统初始化中...
    +
    +
    + + + + + \ No newline at end of file diff --git a/distance-judgement/test_network.py b/distance-judgement/test_network.py new file mode 100644 index 00000000..755a9466 --- /dev/null +++ b/distance-judgement/test_network.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +网络连接测试脚本 +帮助诊断手机/平板连接问题 +""" + +import socket +import subprocess +import sys +import threading +import time +from http.server import HTTPServer, SimpleHTTPRequestHandler + +def get_local_ip(): + """获取本机IP地址""" + try: + # 方法1: 连接到远程地址获取本地IP + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("8.8.8.8", 80)) + ip = s.getsockname()[0] + s.close() + return ip + except: + try: + # 方法2: 获取主机名对应的IP + hostname = socket.gethostname() + ip = socket.gethostbyname(hostname) + if ip.startswith("127."): + return None + return ip + except: + return None + +def test_port(host, port): + """测试端口是否可访问""" + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(5) + result = sock.connect_ex((host, port)) + sock.close() + return result == 0 + except: + return False + +def start_test_server(port=8888): + """启动测试HTTP服务器""" + class TestHandler(SimpleHTTPRequestHandler): + def do_GET(self): + self.send_response(200) + self.send_header('Content-type', 'text/html; charset=utf-8') + self.end_headers() + html = """ + 网络测试 + +

    ✅ 网络连接测试成功!

    +

    如果您能看到这个页面,说明网络连接正常。

    +

    测试时间: %s

    +

    您的IP: %s

    +

    刷新测试

    + + """ % (time.strftime('%Y-%m-%d %H:%M:%S'), self.client_address[0]) + self.wfile.write(html.encode('utf-8')) + + def log_message(self, format, *args): + print(f"📱 测试访问: {self.client_address[0]} - {format % args}") + + try: + server = HTTPServer(('0.0.0.0', port), TestHandler) + print(f"🧪 测试服务器已启动,端口: {port}") + server.serve_forever() + except Exception as e: + print(f"❌ 测试服务器启动失败: {e}") + +def main(): + print("=" * 60) + print("🔍 网络连接诊断工具") + print("=" * 60) + print() + + # 1. 获取IP地址 + print("📍 1. 获取网络IP地址...") + local_ip = get_local_ip() + if local_ip: + print(f"✅ 本机IP地址: {local_ip}") + else: + print("❌ 无法获取IP地址,请检查网络连接") + return + + # 2. 检查常用端口 + print("\n🔌 2. 检查端口状态...") + ports_to_test = [5000, 8080, 8888] + for port in ports_to_test: + if test_port('127.0.0.1', port): + print(f"⚠️ 端口 {port} 已被占用") + else: + print(f"✅ 端口 {port} 可用") + + # 3. 显示连接信息 + print(f"\n📱 3. 移动设备连接信息:") + print(f" 主服务器地址: http://{local_ip}:5000") + print(f" 手机客户端: http://{local_ip}:5000/mobile/mobile_client.html") + print(f" 测试地址: http://{local_ip}:8888") + + # 4. 防火墙检查提示 + print(f"\n🛡️ 4. 防火墙设置提示:") + print(" 如果平板无法连接,请检查Windows防火墙设置:") + print(" 1. 打开 Windows 安全中心") + print(" 2. 点击 防火墙和网络保护") + print(" 3. 点击 允许应用通过防火墙") + print(" 4. 确保 Python 程序被允许通过防火墙") + print(" 或者临时关闭防火墙进行测试") + + # 5. 网络检查命令 + print(f"\n🔧 5. 网络检查命令:") + print(f" 在平板上ping测试: ping {local_ip}") + print(f" 在电脑上查看网络: ipconfig") + print(f" 检查防火墙状态: netsh advfirewall show allprofiles") + + # 6. 启动测试服务器 + print(f"\n🧪 6. 启动网络测试服务器...") + print(f" 请在平板浏览器访问: http://{local_ip}:8888") + print(" 如果能看到测试页面,说明网络连接正常") + print(" 按 Ctrl+C 停止测试") + print() + + try: + start_test_server(8888) + except KeyboardInterrupt: + print("\n👋 测试结束") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/distance-judgement/tests/test_system.py b/distance-judgement/tests/test_system.py new file mode 100644 index 00000000..b56d5fbd --- /dev/null +++ b/distance-judgement/tests/test_system.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +系统综合测试脚本 +用于验证各个模块是否正常工作 +""" + +import cv2 +import numpy as np +import sys +import traceback +import requests +import json +import time + +def test_opencv(): + """测试OpenCV是否正常工作""" + print("=" * 50) + print("测试 OpenCV...") + try: + print(f"OpenCV 版本: {cv2.__version__}") + + # 测试摄像头 + cap = cv2.VideoCapture(0) + if cap.isOpened(): + print("✓ 摄像头可以正常打开") + ret, frame = cap.read() + if ret: + print(f"✓ 摄像头可以正常读取画面,分辨率: {frame.shape[1]}x{frame.shape[0]}") + else: + print("✗ 无法从摄像头读取画面") + cap.release() + else: + print("✗ 无法打开摄像头") + + return True + except Exception as e: + print(f"✗ OpenCV 测试失败: {e}") + return False + +def test_yolo_model(): + """测试YOLO模型是否正常工作""" + print("=" * 50) + print("测试 YOLO 模型...") + try: + from ultralytics import YOLO + print("✓ ultralytics 库导入成功") + + # 尝试加载模型 + model = YOLO('yolov8n.pt') + print("✓ YOLOv8n 模型加载成功") + + # 创建一个测试图像 + test_image = np.zeros((640, 480, 3), dtype=np.uint8) + results = model(test_image, verbose=False) + print("✓ YOLO 推理测试成功") + + return True + except Exception as e: + print(f"✗ YOLO 模型测试失败: {e}") + traceback.print_exc() + return False + +def test_modules(): + """测试自定义模块""" + print("=" * 50) + print("测试自定义模块...") + try: + # 测试配置模块 + from src import config + print("✓ config 模块导入成功") + + # 测试距离计算模块 + from src import DistanceCalculator + calculator = DistanceCalculator() + test_bbox = [100, 100, 200, 400] # 测试边界框 + distance = calculator.get_distance(test_bbox) + print(f"✓ distance_calculator 模块测试成功,测试距离: {calculator.format_distance(distance)}") + + # 测试人体检测模块 + from src import PersonDetector + detector = PersonDetector() + print("✓ person_detector 模块导入成功") + + # 测试地图管理器 + from src import MapManager + map_manager = MapManager( + api_key=config.GAODE_API_KEY, + camera_lat=config.CAMERA_LATITUDE, + camera_lng=config.CAMERA_LONGITUDE + ) + print("✓ map_manager 模块导入成功") + + # 测试手机连接器 + from src import MobileConnector + mobile_connector = MobileConnector(port=8081) # 使用不同端口避免冲突 + print("✓ mobile_connector 模块导入成功") + + return True + except Exception as e: + print(f"✗ 自定义模块测试失败: {e}") + traceback.print_exc() + return False + +def test_flask(): + """测试Flask是否安装""" + print("=" * 50) + print("测试 Flask 环境...") + try: + import flask + print(f"✓ Flask 导入成功,版本: {flask.__version__}") + + # 测试Web服务器模块 + from src import WebServer + print("✓ WebServer 模块导入成功") + + return True + except ImportError: + print("✗ Flask 未安装,Web功能不可用") + return False + except Exception as e: + print(f"✗ Flask 测试失败: {e}") + return False + +def test_web_apis(base_url="http://127.0.0.1:5000"): + """测试Web API接口(仅在服务器运行时测试)""" + print("=" * 50) + print("测试Web API接口...") + + try: + # 测试主页面 + response = requests.get(f"{base_url}/", timeout=5) + if response.status_code == 200: + print("✓ 主页面访问正常") + + # 测试人员数据API + response = requests.get(f"{base_url}/api/get_persons_data", timeout=5) + if response.status_code == 200: + data = response.json() + print(f"✓ 人员数据API正常: {len(data)} 个人员") + + # 测试调试信息API + response = requests.get(f"{base_url}/api/debug_info", timeout=5) + if response.status_code == 200: + data = response.json() + print(f"✓ 调试信息API正常") + print(f" 摄像头状态: {'在线' if data.get('camera_active') else '离线'}") + + # 测试手机相关API + response = requests.get(f"{base_url}/api/mobile/devices", timeout=5) + if response.status_code == 200: + devices = response.json() + print(f"✓ 手机设备API正常: {len(devices)} 个设备") + + # 测试手机端页面 + response = requests.get(f"{base_url}/mobile/mobile_client.html", timeout=5) + if response.status_code == 200: + print(f"✓ 手机端页面可访问") + + return True + else: + print("✗ Web服务器未响应") + return False + + except requests.exceptions.ConnectionError: + print("⚠️ Web服务器未运行,跳过API测试") + return True # 不算失败,因为服务器可能没启动 + except Exception as e: + print(f"✗ Web API测试失败: {e}") + return False + +def main(): + """主测试函数""" + print("🔧 开始系统综合测试...") + print("测试将验证所有必要的组件是否正常工作") + + tests = [ + ("OpenCV", test_opencv), + ("YOLO模型", test_yolo_model), + ("自定义模块", test_modules), + ("Flask环境", test_flask), + ("Web API", test_web_apis), + ] + + results = {} + + for test_name, test_func in tests: + print(f"\n🧪 开始测试: {test_name}") + results[test_name] = test_func() + + # 显示测试结果摘要 + print("\n" + "=" * 50) + print("📊 测试结果摘要:") + print("=" * 50) + + passed = 0 + total = len(tests) + + for test_name, result in results.items(): + status = "✓ 通过" if result else "✗ 失败" + print(f"{test_name:<15}: {status}") + if result: + passed += 1 + + print(f"\n总体结果: {passed}/{total} 测试通过") + + if passed >= total - 1: # 允许Web API测试失败(因为可能没启动服务器) + print("🎉 系统基本功能正常!") + print("\n📖 使用建议:") + print(" 1. 运行 'python run.py' 选择运行模式") + print(" 2. 使用Web模式获得最佳体验") + print(" 3. 配置摄像头位置获得准确的地图显示") + else: + print("⚠️ 系统存在问题,请检查相关组件") + print("\n🔧 建议操作:") + print(" 1. 重新运行 'python tools/install.py' 安装依赖") + print(" 2. 检查requirements.txt中的依赖版本") + print(" 3. 确认摄像头设备连接正常") + + print("\n测试完成!") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/distance-judgement/tools/__pycache__/setup_camera_location.cpython-311.pyc b/distance-judgement/tools/__pycache__/setup_camera_location.cpython-311.pyc new file mode 100644 index 00000000..815e9cf5 Binary files /dev/null and b/distance-judgement/tools/__pycache__/setup_camera_location.cpython-311.pyc differ diff --git a/distance-judgement/tools/auto_configure_camera.py b/distance-judgement/tools/auto_configure_camera.py new file mode 100644 index 00000000..5657c1db --- /dev/null +++ b/distance-judgement/tools/auto_configure_camera.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +摄像头自动配置工具 +自动获取设备位置和朝向,设置摄像头参数 +""" + +import sys +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from src.orientation_detector import OrientationDetector + + +def main(): + """主函数""" + print("=" * 60) + print("🤖 摄像头自动配置工具") + print("=" * 60) + print() + + print("🎯 功能说明:") + print(" • 自动获取当前设备的GPS位置") + print(" • 自动检测设备朝向") + print(" • 计算摄像头应该面向用户的角度") + print(" • 自动更新系统配置文件") + print() + + print("⚠️ 注意事项:") + print(" • 请确保设备连接到互联网") + print(" • Windows系统可能需要开启位置服务") + print(" • 桌面设备朝向检测精度有限") + print() + + try: + # 创建朝向检测器 + detector = OrientationDetector() + + # 执行自动配置 + result = detector.auto_configure_camera_location() + + if result['success']: + print() + print("✅ 自动配置成功!") + print("📊 配置详情:") + print(f" 📍 GPS位置: {result['gps_location'][0]:.6f}, {result['gps_location'][1]:.6f}") + print(f" 🧭 设备朝向: {result['device_heading']:.1f}°") + print(f" 📷 摄像头朝向: {result['camera_heading']:.1f}°") + print(f" 🎯 定位方法: {result['method']}") + print(f" 📏 定位精度: ±{result['accuracy']:.0f}m") + print() + + # 询问是否应用配置 + while True: + choice = input("🔧 是否应用此配置? (y/n/r): ").strip().lower() + + if choice == 'y': + # 应用配置 + detector.update_camera_config( + result['gps_location'], + result['camera_heading'] + ) + print("✅ 配置已应用到系统!") + break + + elif choice == 'n': + print("⏭️ 配置未应用") + break + + elif choice == 'r': + # 重新检测 + print("\n🔄 重新检测...") + result = detector.auto_configure_camera_location() + if not result['success']: + print("❌ 重新检测失败") + break + + print("📊 新的配置详情:") + print(f" 📍 GPS位置: {result['gps_location'][0]:.6f}, {result['gps_location'][1]:.6f}") + print(f" 🧭 设备朝向: {result['device_heading']:.1f}°") + print(f" 📷 摄像头朝向: {result['camera_heading']:.1f}°") + print(f" 🎯 定位方法: {result['method']}") + print(f" 📏 定位精度: ±{result['accuracy']:.0f}m") + print() + + else: + print("❌ 请输入 y(应用)/n(取消)/r(重新检测)") + + else: + print("❌ 自动配置失败") + print("💡 建议:") + print(" 1. 检查网络连接") + print(" 2. 使用手动配置: python tools/setup_camera_location.py") + print(" 3. 或在Web界面中手动设置") + + except KeyboardInterrupt: + print("\n🔴 用户取消操作") + + except Exception as e: + print(f"❌ 配置过程出错: {e}") + print("💡 建议使用手动配置工具") + + finally: + print("\n👋 配置工具结束") + + +def test_gps_only(): + """仅测试GPS定位功能""" + print("🧪 GPS定位测试") + print("-" * 30) + + detector = OrientationDetector() + location = detector.get_current_gps_location() + + if location: + lat, lng, accuracy = location + print(f"✅ GPS测试成功:") + print(f" 📍 位置: {lat:.6f}, {lng:.6f}") + print(f" 📏 精度: ±{accuracy:.0f}m") + else: + print("❌ GPS测试失败") + + +def test_heading_only(): + """仅测试朝向检测功能""" + print("🧪 朝向检测测试") + print("-" * 30) + + detector = OrientationDetector() + heading = detector.get_device_heading() + + if heading is not None: + print(f"✅ 朝向测试成功:") + print(f" 🧭 设备朝向: {heading:.1f}°") + + # 计算摄像头朝向 + camera_heading = detector.calculate_camera_heading_facing_user(heading) + print(f" 📷 摄像头朝向: {camera_heading:.1f}°") + else: + print("❌ 朝向测试失败") + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description='摄像头自动配置工具') + parser.add_argument('--test-gps', action='store_true', help='仅测试GPS功能') + parser.add_argument('--test-heading', action='store_true', help='仅测试朝向功能') + + args = parser.parse_args() + + if args.test_gps: + test_gps_only() + elif args.test_heading: + test_heading_only() + else: + main() \ No newline at end of file diff --git a/distance-judgement/tools/generate_ssl_cert.py b/distance-judgement/tools/generate_ssl_cert.py new file mode 100644 index 00000000..0180b0af --- /dev/null +++ b/distance-judgement/tools/generate_ssl_cert.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +生成自签名SSL证书用于HTTPS服务 +""" + +import os +import datetime +import ipaddress +from cryptography import x509 +from cryptography.x509.oid import NameOID +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import rsa + +def generate_ssl_certificate(): + """生成自签名SSL证书""" + + # 创建ssl目录 + ssl_dir = "ssl" + if not os.path.exists(ssl_dir): + os.makedirs(ssl_dir) + + print("🔑 正在生成SSL证书...") + + # 生成私钥 + private_key = rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + ) + + # 创建证书主体 + subject = issuer = x509.Name([ + x509.NameAttribute(NameOID.COUNTRY_NAME, "CN"), + x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Beijing"), + x509.NameAttribute(NameOID.LOCALITY_NAME, "Beijing"), + x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Distance Judgement System"), + x509.NameAttribute(NameOID.COMMON_NAME, "localhost"), + ]) + + # 生成证书 + cert = x509.CertificateBuilder().subject_name( + subject + ).issuer_name( + issuer + ).public_key( + private_key.public_key() + ).serial_number( + x509.random_serial_number() + ).not_valid_before( + datetime.datetime.utcnow() + ).not_valid_after( + datetime.datetime.utcnow() + datetime.timedelta(days=365) + ).add_extension( + x509.SubjectAlternativeName([ + x509.DNSName("localhost"), + x509.DNSName("127.0.0.1"), + x509.IPAddress(ipaddress.IPv4Address("127.0.0.1")), + x509.IPAddress(ipaddress.IPv4Address("0.0.0.0")), + ]), + critical=False, + ).sign(private_key, hashes.SHA256()) + + # 保存私钥 + key_path = os.path.join(ssl_dir, "key.pem") + with open(key_path, "wb") as f: + f.write(private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption() + )) + + # 保存证书 + cert_path = os.path.join(ssl_dir, "cert.pem") + with open(cert_path, "wb") as f: + f.write(cert.public_bytes(serialization.Encoding.PEM)) + + print(f"✅ SSL证书已生成:") + print(f" 🔑 私钥: {key_path}") + print(f" 📜 证书: {cert_path}") + print(f" 📅 有效期: 365天") + print() + print("⚠️ 注意: 这是自签名证书,浏览器会显示安全警告") + print(" 点击 '高级' -> '继续访问localhost(不安全)' 即可") + +if __name__ == "__main__": + try: + generate_ssl_certificate() + except ImportError: + print("❌ 缺少cryptography库,正在尝试安装...") + import subprocess + import sys + subprocess.check_call([sys.executable, "-m", "pip", "install", "cryptography"]) + print("✅ cryptography库安装完成,重新生成证书...") + generate_ssl_certificate() + except Exception as e: + print(f"❌ 生成SSL证书失败: {e}") \ No newline at end of file diff --git a/distance-judgement/tools/install.py b/distance-judgement/tools/install.py new file mode 100644 index 00000000..2f83a295 --- /dev/null +++ b/distance-judgement/tools/install.py @@ -0,0 +1,264 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +自动安装脚本 +自动安装所需依赖并验证环境 +""" + +import subprocess +import sys +import time +import os + +def print_header(title): + """打印标题""" + print("\n" + "=" * 60) + print(f" {title}") + print("=" * 60) + +def run_command(command, description): + """运行命令并显示结果""" + print(f"\n🔄 {description}...") + try: + result = subprocess.run(command, shell=True, capture_output=True, text=True) + if result.returncode == 0: + print(f"✅ {description} 成功完成") + return True + else: + print(f"❌ {description} 失败") + print(f"错误信息: {result.stderr}") + return False + except Exception as e: + print(f"❌ {description} 执行异常: {e}") + return False + +def check_python_version(): + """检查Python版本""" + print_header("检查Python版本") + + version = sys.version_info + print(f"当前Python版本: {version.major}.{version.minor}.{version.micro}") + + if version.major < 3 or (version.major == 3 and version.minor < 8): + print("❌ Python版本过低,需要Python 3.8或更高版本") + return False + else: + print("✅ Python版本满足要求") + return True + +def install_requirements(): + """安装依赖包""" + print_header("安装依赖包") + + if not os.path.exists("requirements.txt"): + print("❌ requirements.txt 文件不存在") + return False + + # 升级pip + if not run_command(f"{sys.executable} -m pip install --upgrade pip", "升级pip"): + print("⚠️ pip升级失败,继续安装依赖") + + # 安装基础依赖 + success1 = run_command(f"{sys.executable} -m pip install -r requirements.txt", "安装基础依赖包") + + # 安装Flask(Web功能所需) + success2 = install_flask() + + return success1 and success2 + +def install_flask(): + """安装Flask及其依赖""" + print("\n🔧 安装Flask(Web功能支持)...") + + try: + # 检查是否已安装 + import flask + print(f"✅ Flask已安装,版本: {flask.__version__}") + return True + except ImportError: + pass + + # 尝试使用清华镜像源 + result = subprocess.run([ + sys.executable, "-m", "pip", "install", + "flask==2.3.3", + "-i", "https://pypi.tuna.tsinghua.edu.cn/simple/", + "--trusted-host", "pypi.tuna.tsinghua.edu.cn" + ], capture_output=True, text=True, timeout=300) + + if result.returncode == 0: + print("✅ Flask安装成功!") + return True + else: + # 尝试官方源 + result = subprocess.run([ + sys.executable, "-m", "pip", "install", "flask==2.3.3" + ], capture_output=True, text=True, timeout=300) + + if result.returncode == 0: + print("✅ Flask安装成功!") + return True + else: + print(f"❌ Flask安装失败: {result.stderr}") + return False + +def verify_installation(): + """验证安装""" + print_header("验证安装") + + modules_to_check = [ + ("cv2", "OpenCV"), + ("numpy", "NumPy"), + ("ultralytics", "Ultralytics"), + ("torch", "PyTorch"), + ("flask", "Flask"), + ] + + all_success = True + + for module, name in modules_to_check: + try: + __import__(module) + print(f"✅ {name} 安装成功") + except ImportError: + print(f"❌ {name} 安装失败") + all_success = False + + return all_success + +def download_yolo_model(): + """预下载YOLO模型""" + print_header("下载YOLO模型") + + try: + from ultralytics import YOLO + print("🔄 正在下载YOLOv8n模型,请稍候...") + model = YOLO('yolov8n.pt') + print("✅ YOLOv8n模型下载成功") + + # 测试模块结构 + try: + from src import PersonDetector, DistanceCalculator + print("✅ 重构后的模块结构测试成功") + except ImportError as e: + print(f"⚠️ 模块结构测试失败: {e}") + + return True + except Exception as e: + print(f"❌ YOLO模型下载失败: {e}") + return False + +def run_test(): + """运行测试""" + print_header("运行系统测试") + + if os.path.exists("test_modules.py"): + return run_command(f"{sys.executable} test_modules.py", "运行系统测试") + else: + print("⚠️ 测试脚本不存在,跳过测试") + return True + +def create_desktop_shortcut(): + """创建桌面快捷方式(Windows)""" + try: + import platform + if platform.system() == "Windows": + print_header("创建桌面快捷方式") + + desktop_path = os.path.join(os.path.expanduser("~"), "Desktop") + if os.path.exists(desktop_path): + shortcut_content = f""" +@echo off +cd /d "{os.getcwd()}" +python run.py +pause +""" + shortcut_path = os.path.join(desktop_path, "人体距离检测.bat") + with open(shortcut_path, "w", encoding="gbk") as f: + f.write(shortcut_content) + print(f"✅ 桌面快捷方式已创建: {shortcut_path}") + return True + else: + print("⚠️ 未找到桌面路径") + return False + except Exception as e: + print(f"⚠️ 创建快捷方式失败: {e}") + return False + +def main(): + """主安装函数""" + print("🚀 人体距离检测系统 - 自动安装程序") + print("此程序将自动安装所需依赖并配置环境") + + steps = [ + ("检查Python版本", check_python_version), + ("安装依赖包", install_requirements), + ("验证安装", verify_installation), + ("下载YOLO模型", download_yolo_model), + ("运行系统测试", run_test), + ] + + # 可选步骤 + optional_steps = [ + ("创建桌面快捷方式", create_desktop_shortcut), + ] + + print(f"\n📋 安装计划:") + for i, (name, _) in enumerate(steps, 1): + print(f" {i}. {name}") + + print(f"\n可选步骤:") + for i, (name, _) in enumerate(optional_steps, 1): + print(f" {i}. {name}") + + input("\n按Enter键开始安装...") + + start_time = time.time() + success_count = 0 + total_steps = len(steps) + + # 执行主要安装步骤 + for i, (name, func) in enumerate(steps, 1): + print(f"\n📦 步骤 {i}/{total_steps}: {name}") + + if func(): + success_count += 1 + print(f"✅ 步骤 {i} 完成") + else: + print(f"❌ 步骤 {i} 失败") + + # 询问是否继续 + choice = input("是否继续安装?(y/n): ").lower() + if choice != 'y': + print("安装已取消") + return + + # 执行可选步骤 + for name, func in optional_steps: + choice = input(f"\n是否执行: {name}?(y/n): ").lower() + if choice == 'y': + func() + + # 显示安装结果 + elapsed_time = time.time() - start_time + print_header("安装完成") + + print(f"✅ 安装步骤: {success_count}/{total_steps} 完成") + print(f"⏱️ 总耗时: {elapsed_time:.1f}秒") + + if success_count == total_steps: + print("🎉 所有步骤都已成功完成!") + print("\n📖 使用指南:") + print(" 1. 运行 'python run.py' 启动系统") + print(" 2. 选择运行模式(Web或传统模式)") + print(" 3. 如需配置摄像头位置,运行 setup_camera_location.py") + print(" 4. 如遇问题,运行 test_modules.py 进行诊断") + else: + print("⚠️ 部分步骤未成功,请检查错误信息") + print("💡 如需帮助,请查看README.md文档") + + print("\n安装程序结束!") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/distance-judgement/tools/setup_camera_location.py b/distance-judgement/tools/setup_camera_location.py new file mode 100644 index 00000000..31a7f0e0 --- /dev/null +++ b/distance-judgement/tools/setup_camera_location.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +摄像头地理位置配置工具 +用于设置摄像头的经纬度坐标和朝向角度 +""" + +import os +import sys + +def update_config_file(lat, lng, heading, api_key=None): + """更新配置文件中的摄像头位置信息""" + config_path = "src/config.py" + + # 读取配置文件 + with open(config_path, 'r', encoding='utf-8') as f: + content = f.read() + + # 更新纬度 + if "CAMERA_LATITUDE = " in content: + lines = content.split('\n') + for i, line in enumerate(lines): + if line.startswith('CAMERA_LATITUDE = '): + lines[i] = f'CAMERA_LATITUDE = {lat} # 摄像头纬度' + break + content = '\n'.join(lines) + + # 更新经度 + if "CAMERA_LONGITUDE = " in content: + lines = content.split('\n') + for i, line in enumerate(lines): + if line.startswith('CAMERA_LONGITUDE = '): + lines[i] = f'CAMERA_LONGITUDE = {lng} # 摄像头经度' + break + content = '\n'.join(lines) + + # 更新朝向 + if "CAMERA_HEADING = " in content: + lines = content.split('\n') + for i, line in enumerate(lines): + if line.startswith('CAMERA_HEADING = '): + lines[i] = f'CAMERA_HEADING = {heading} # 摄像头朝向角度' + break + content = '\n'.join(lines) + + # 更新API key(如果提供) + if api_key and 'GAODE_API_KEY = ' in content: + lines = content.split('\n') + for i, line in enumerate(lines): + if line.startswith('GAODE_API_KEY = '): + lines[i] = f'GAODE_API_KEY = "{api_key}" # 高德地图API密钥' + break + content = '\n'.join(lines) + + # 写回配置文件 + with open(config_path, 'w', encoding='utf-8') as f: + f.write(content) + +def main(): + print("=" * 60) + print("🚁 无人机摄像头地理位置配置工具") + print("=" * 60) + print() + + print("📍 请设置摄像头的地理位置信息") + print("提示: 可以通过高德地图等应用获取准确的经纬度坐标") + print() + + try: + # 获取纬度 + lat = float(input("请输入摄像头纬度 (例: 39.9042): ")) + if not (-90 <= lat <= 90): + raise ValueError("纬度必须在-90到90之间") + + # 获取经度 + lng = float(input("请输入摄像头经度 (例: 116.4074): ")) + if not (-180 <= lng <= 180): + raise ValueError("经度必须在-180到180之间") + + # 获取朝向 + heading = float(input("请输入摄像头朝向角度 (0-360°, 0为正北): ")) + if not (0 <= heading <= 360): + raise ValueError("朝向角度必须在0到360之间") + + # 可选:设置高德API Key + print("\n🔑 高德地图API Key设置 (可选)") + print("如果您有高德开放平台的API Key,请输入以获得更好的地图体验") + api_key = input("请输入高德API Key (留空跳过): ").strip() + + # 更新配置 + update_config_file(lat, lng, heading, api_key if api_key else None) + + print("\n✅ 配置更新成功!") + print(f"📍 摄像头位置: ({lat:.6f}, {lng:.6f})") + print(f"🧭 朝向角度: {heading}°") + if api_key: + print("🔑 API Key 已设置") + + print("\n🎯 建议使用步骤:") + print("1. 运行 python run.py 启动系统") + print("2. 选择Web模式获得最佳体验") + print("3. 按 'm' 键打开地图查看效果") + print("4. 按 'c' 键校准距离参数以提高精度") + + except ValueError as e: + print(f"❌ 输入错误: {e}") + print("请重新运行程序并输入正确的数值") + sys.exit(1) + except Exception as e: + print(f"❌ 配置失败: {e}") + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/distance-judgement/tools/ssl/cert.pem b/distance-judgement/tools/ssl/cert.pem new file mode 100644 index 00000000..786e3616 --- /dev/null +++ b/distance-judgement/tools/ssl/cert.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDjzCCAnegAwIBAgIUJK9wxusX1FTV1FbBSRVlZUwUdmcwDQYJKoZIhvcNAQEL +BQAwaTELMAkGA1UEBhMCQ04xEDAOBgNVBAgMB0JlaWppbmcxEDAOBgNVBAcMB0Jl +aWppbmcxIjAgBgNVBAoMGURpc3RhbmNlIEp1ZGdlbWVudCBTeXN0ZW0xEjAQBgNV +BAMMCWxvY2FsaG9zdDAeFw0yNTA2MjkwODQ2MDNaFw0yNjA2MjkwODQ2MDNaMGkx +CzAJBgNVBAYTAkNOMRAwDgYDVQQIDAdCZWlqaW5nMRAwDgYDVQQHDAdCZWlqaW5n +MSIwIAYDVQQKDBlEaXN0YW5jZSBKdWRnZW1lbnQgU3lzdGVtMRIwEAYDVQQDDAls +b2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCEb/6AFLiJ +18UpEH5DKJdiXBAzc/c9ECzU/4k+ZJpp9Hs2SfhZhRvDpTw90dZReQfh2squyLcy +DhyWTF+7nedSX4AaU1zQSwdQR/J+0T4fCH23gwxcSWRXA2yUrIgjgCtfc3wSzU+X +eeA9ntjBAv+Axf9PPwMiqtxmWjNCc8YmDr24L0oBWwBSX0lsIB6cmTBOhwkAI5J5 +DWcHaIi/jPjojwJu6z8mGIPmaOBqf12ZJfMB1u3YUHysZYidVuA3TaR/Dx07tZJk +yd/RT8sJYJ4VdiA7ZxOynrKh/z84fqpKl2xzuCHGIMJFsyTyKmsYNux0h0lCKGNL +dLotTjYC8ravAgMBAAGjLzAtMCsGA1UdEQQkMCKCCWxvY2FsaG9zdIIJMTI3LjAu +MC4xhwR/AAABhwQAAAAAMA0GCSqGSIb3DQEBCwUAA4IBAQAaU/fpR8g5mUmFjdco +/0vkcoUxH4mO3w8cbVcRO513KKmV3mLQim+sNkL+mdEXSsHDdoiz/rjhTD6i9LW4 +qrQCIvYPJHtFr2SEdOJWLHsPMaOv86DnF0ufEIB22SmnFAFa75PN35p08JZoWiUk +19RmC5gXn2G32eGRfwir9a+sB9lS4Q0MfmSdK8myb32JmuXkFWJgB5jtzEsVDX3q +RpLVBlM7CIisX9+EfrjJVeaj5EnlLeFayHEnyuRBFy2k4mqdhdMOFxdmaqmTtmS+ +TFrmCiGGKU74HLmGr4m10ZBkL5hhw/7XtGqTDMzKLmPXf62j1HoJhhdzVH2QbbRy +QnR2 +-----END CERTIFICATE----- diff --git a/distance-judgement/tools/ssl/key.pem b/distance-judgement/tools/ssl/key.pem new file mode 100644 index 00000000..60d75a6a --- /dev/null +++ b/distance-judgement/tools/ssl/key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCEb/6AFLiJ18Up +EH5DKJdiXBAzc/c9ECzU/4k+ZJpp9Hs2SfhZhRvDpTw90dZReQfh2squyLcyDhyW +TF+7nedSX4AaU1zQSwdQR/J+0T4fCH23gwxcSWRXA2yUrIgjgCtfc3wSzU+XeeA9 +ntjBAv+Axf9PPwMiqtxmWjNCc8YmDr24L0oBWwBSX0lsIB6cmTBOhwkAI5J5DWcH +aIi/jPjojwJu6z8mGIPmaOBqf12ZJfMB1u3YUHysZYidVuA3TaR/Dx07tZJkyd/R +T8sJYJ4VdiA7ZxOynrKh/z84fqpKl2xzuCHGIMJFsyTyKmsYNux0h0lCKGNLdLot +TjYC8ravAgMBAAECggEAC9+MhggViUoeY3GWmEfF1qwhSbOeWUufcVMdh0n2rAQe +nb3g9Ymg9RfVwEcVO0WqBr4aSLQ29FZeirz7IjNkXzavoeySWBw54iEpJOR2eMrG +lpK5o3Zy9/gXHncfV2twuAR+/aKJfa+QAoZAsYEmzfEyU/T2v39o9gYlLVJ608OC +iyb3xRsiidnonRR7pCIX2ghI/GJcKFZmYbc2g4hehBz6zBN7Xu7t26sUrdJd9tT2 +wWIEz0pH2Iutwiy4mlfkqJ+dezSZPCRXxLHbq2RRKn/17YNiCvTjBpsX83FxcwKR +6XlIabWMNJ6EOvNGtwufXAUwrieHq6uPFx5mKHfIoQKBgQC6WWk0IyZOFyuqTNBq +2axuXkxmU+xjfbu4NyFXxoNmyiR6VkK4tpcj7nV2ZN6gdA8pCOjZiQM9NA3GYPPv +eOcTvgIL16cE4EdrkE+Jv+UF7SAhToPbrnBF9y2GN5FBk9L2tvDDeF0qcXzyIleK +9dJYqoAxssCUIhASb5AsCoo6oQKBgQC18BqB1Ucp9tz7juRP+rT81zESJCEGpfu1 +TPOiZgjkr6jR5xsvFHOBtbnXkCi7hir1Wo9kRTujsL58Pu+vA7M6ZWjpGIBtdfmw +fSUZmt+hW+V6O1K8WQRFQgErM3PJNBN6l/mLh9Lj39tyeFrrA1WBhtx4mVot4DTC +ds9CVb0/TwKBgCXWX8kpVe7HP6N9o1f+yMdEOGkSo030Srh14TxMX4PwiYWZnESb +NociNRGMG7QivK1NVNJOwqybtCxSpVU7jFfy3cF/0TbpPzc0/yFuKFeStVJt+dIS +UlOyg7jb8Y+KL2zO6oYWG3yxvHgBxxq9HS/Jtuvgar/pRrAnnPOEVFrhAoGAHVwx +6uHQKiV8Y9wbXAzJSEQx1wudiMUgaZGRf5OXu8/dHoJ9EIvsV/JLm03YROrR4+ZJ +XZUOmsva8ZH2e/fM5I+Y7oTVtNRlBuYrJoansBJ0ZdVM9LgoyERui9oxxTZyLkZ4 +LtwsXDmz4DUr9uEC23Q3//4/X0ffO8KQj9PmRmECgYBR3YU15qledPzD2n5CtqKD +EiVkB1TRZPq46bJFTZ/WhwvIOOrSDb4u7aYP4DzW7kzZt+uRiAHNfrY5AE29681c +llt1kr+MrAbX0CdqYUWJoT0Z8Svuw083m9O0EPAZMiYT73izgcvlvvG5MT9uHkQB +q6LmyYRBH1NLxYz0aFvY+w== +-----END PRIVATE KEY----- diff --git a/distance-judgement/yolov8n.pt b/distance-judgement/yolov8n.pt new file mode 100644 index 00000000..d61ef50d Binary files /dev/null and b/distance-judgement/yolov8n.pt differ