Merge pull request '视觉处理' (#6) from zhangyang into main

main
pkv3rwifo 1 week ago
commit 7d2e906199

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.11 (yolo8)" />
</component>
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (yolo8)" project-jdk-type="Python SDK" />
</project>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/pythonProject2.iml" filepath="$PROJECT_DIR$/.idea/pythonProject2.iml" />
</modules>
</component>
</project>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

@ -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. **添加数据缓存**:为点击事件提供设备数据支持
### 🎯 修复后的效果
- **固定摄像头(电脑端)**:💻电脑图标 + 蓝色视野扇形
- **移动设备(移动端)**:🚁无人机图标 + 朝向箭头 + 橙色视野扇形
## 总结
通过这次修复,彻底解决了摄像头图标重叠的问题,确保地图上的标记状态与实际配置始终保持一致,提升了用户体验。同时解决了重复无人机图标的问题,让图标显示更加清晰和直观。

@ -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`,选择自动配置,享受智能的摄像头定位体验!

@ -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升级后您的移动端摄像头功能将完全正常工作不再受到浏览器安全限制的影响。

@ -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 - 增加配置工具和用户指南

@ -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 ≥ 4GBAndroid 8+ / iOS 12+
- **服务器**: 双核CPURAM ≥ 4GB
### 优化设置
- **高质量模式**: 0.7质量1秒间隔
- **平衡模式**: 0.5质量1秒间隔推荐
- **省流量模式**: 0.3质量2秒间隔
## 🚁 实战应用场景
### 军用场景
- **前线侦察**: 士兵携带手机进行前方侦察
- **多点监控**: 多个观察点同时传输情报
- **指挥决策**: 指挥部实时获取战场态势
### 民用场景
- **安保监控**: 保安巡逻时实时传输画面
- **应急救援**: 救援人员现场情况汇报
- **活动监管**: 大型活动现场监控
### 技术演示
- **远程教学**: 实地教学直播
- **技术展示**: 产品演示和技术验证
---
## 📞 技术支持
如有问题,请:
1. 查看控制台日志信息
2. 运行系统测试脚本
3. 检查网络配置
4. 参考故障排除指南
这个手机连接功能大大扩展了战场态势感知系统的应用场景,让移动侦察成为可能!

@ -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()

@ -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按回车键继续...")

@ -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"<EFBFBD><EFBFBD> 手机/平板请访问移动客户端地址!")

@ -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()

@ -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()

@ -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()

File diff suppressed because it is too large Load Diff

@ -0,0 +1,410 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🌐 浏览器兼容性指南</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
min-height: 100vh;
padding: 20px;
line-height: 1.6;
}
.container {
max-width: 900px;
margin: 0 auto;
}
.header {
text-align: center;
padding: 30px 0;
border-bottom: 2px solid rgba(255, 255, 255, 0.2);
margin-bottom: 30px;
}
.section {
background: rgba(0, 0, 0, 0.3);
border-radius: 15px;
padding: 25px;
margin-bottom: 20px;
}
.section h3 {
color: #4CAF50;
margin-bottom: 15px;
font-size: 20px;
}
.compatibility-table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
overflow: hidden;
}
.compatibility-table th,
.compatibility-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.compatibility-table th {
background: rgba(0, 0, 0, 0.3);
font-weight: bold;
}
.support-yes {
color: #4CAF50;
}
.support-partial {
color: #FF9800;
}
.support-no {
color: #f44336;
}
.solution-box {
background: rgba(76, 175, 80, 0.2);
border-left: 4px solid #4CAF50;
padding: 15px;
margin: 15px 0;
border-radius: 0 8px 8px 0;
}
.warning-box {
background: rgba(255, 152, 0, 0.2);
border-left: 4px solid #FF9800;
padding: 15px;
margin: 15px 0;
border-radius: 0 8px 8px 0;
}
.error-box {
background: rgba(244, 67, 54, 0.2);
border-left: 4px solid #f44336;
padding: 15px;
margin: 15px 0;
border-radius: 0 8px 8px 0;
}
.btn {
display: inline-block;
padding: 12px 24px;
background: #4CAF50;
color: white;
text-decoration: none;
border-radius: 8px;
font-weight: bold;
margin: 5px;
transition: background 0.3s;
}
.btn:hover {
background: #45a049;
}
.btn-secondary {
background: #2196F3;
}
.btn-secondary:hover {
background: #1976D2;
}
.feature-list {
list-style: none;
padding: 0;
}
.feature-list li {
padding: 8px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.feature-list li:last-child {
border-bottom: none;
}
.status-indicator {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 10px;
}
.status-ok {
background: #4CAF50;
}
.status-warning {
background: #FF9800;
}
.status-error {
background: #f44336;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🌐 浏览器兼容性指南</h1>
<p>移动侦察终端摄像头功能兼容性说明与解决方案</p>
</div>
<div class="section">
<h3>📋 当前浏览器检测</h3>
<div id="browserInfo">
<p>正在检测您的浏览器兼容性...</p>
</div>
</div>
<div class="section">
<h3>🔍 "设备扫描失败: 浏览器不支持设备枚举功能" 问题说明</h3>
<div class="warning-box">
<h4>⚠️ 问题原因</h4>
<p>这个错误表示您的浏览器不支持 <code>navigator.mediaDevices.enumerateDevices()</code> API这个API用于列出可用的摄像头设备。</p>
</div>
<div class="solution-box">
<h4>✅ 系统自动解决方案</h4>
<p>我们的系统已经自动启用了兼容模式,为您提供以下设备选项:</p>
<ul style="margin: 10px 0 0 20px;">
<li>📱 <strong>默认摄像头</strong> - 使用系统默认摄像头</li>
<li>📹 <strong>后置摄像头</strong> - 尝试使用后置摄像头</li>
<li>🤳 <strong>前置摄像头</strong> - 尝试使用前置摄像头</li>
</ul>
<p style="margin-top: 10px;">您可以通过设备选择器逐个测试这些选项,找到适合的摄像头配置。</p>
</div>
</div>
<div class="section">
<h3>📱 浏览器兼容性列表</h3>
<table class="compatibility-table">
<thead>
<tr>
<th>浏览器</th>
<th>getUserMedia</th>
<th>enumerateDevices</th>
<th>Permissions API</th>
<th>总体支持</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Chrome 53+</strong></td>
<td class="support-yes">✅ 完全支持</td>
<td class="support-yes">✅ 完全支持</td>
<td class="support-yes">✅ 完全支持</td>
<td class="support-yes"><strong>推荐</strong></td>
</tr>
<tr>
<td><strong>Firefox 36+</strong></td>
<td class="support-yes">✅ 完全支持</td>
<td class="support-yes">✅ 完全支持</td>
<td class="support-partial">⚠️ 部分支持</td>
<td class="support-yes"><strong>推荐</strong></td>
</tr>
<tr>
<td><strong>Safari 11+</strong></td>
<td class="support-yes">✅ 完全支持</td>
<td class="support-yes">✅ 完全支持</td>
<td class="support-no">❌ 不支持</td>
<td class="support-partial">⚠️ 基本可用</td>
</tr>
<tr>
<td><strong>Edge 17+</strong></td>
<td class="support-yes">✅ 完全支持</td>
<td class="support-yes">✅ 完全支持</td>
<td class="support-yes">✅ 完全支持</td>
<td class="support-yes"><strong>推荐</strong></td>
</tr>
<tr>
<td><strong>旧版浏览器</strong></td>
<td class="support-partial">⚠️ 需要前缀</td>
<td class="support-no">❌ 不支持</td>
<td class="support-no">❌ 不支持</td>
<td class="support-partial">⚠️ 兼容模式</td>
</tr>
</tbody>
</table>
</div>
<div class="section">
<h3>🔧 解决方案与建议</h3>
<h4>1. 最佳解决方案 - 升级浏览器</h4>
<div class="solution-box">
<p><strong>推荐使用以下现代浏览器:</strong></p>
<ul style="margin: 10px 0 0 20px;">
<li>🌐 <strong>Chrome</strong> 版本 53 或更高</li>
<li>🦊 <strong>Firefox</strong> 版本 36 或更高</li>
<li>🧭 <strong>Safari</strong> 版本 11 或更高iOS/macOS</li>
<li><strong>Edge</strong> 版本 17 或更高</li>
</ul>
</div>
<h4>2. 兼容模式使用方法</h4>
<div class="warning-box">
<p><strong>如果无法升级浏览器,请按以下步骤操作:</strong></p>
<ol style="margin: 10px 0 0 20px;">
<li>忽略"设备扫描失败"的提示</li>
<li>点击"📷 选择设备"按钮</li>
<li>在设备列表中选择"默认摄像头"、"后置摄像头"或"前置摄像头"</li>
<li>点击"使用选中设备"测试摄像头功能</li>
<li>如果某个选项不工作,尝试其他选项</li>
</ol>
</div>
<h4>3. 移动设备特别说明</h4>
<div class="solution-box">
<p><strong>移动设备用户请注意:</strong></p>
<ul style="margin: 10px 0 0 20px;">
<li>📱 <strong>Android</strong>:建议使用 Chrome 浏览器</li>
<li>🍎 <strong>iOS</strong>:建议使用 Safari 浏览器</li>
<li>🔒 确保在 HTTPS 环境下访问(已自动配置)</li>
<li>🎥 允许摄像头权限访问</li>
</ul>
</div>
</div>
<div class="section">
<h3>🚨 常见问题排除</h3>
<div class="feature-list">
<li>
<span class="status-indicator status-error"></span>
<strong>完全无法访问摄像头</strong>
<br><small>检查浏览器是否支持getUserMedia尝试升级浏览器或使用HTTPS访问</small>
</li>
<li>
<span class="status-indicator status-warning"></span>
<strong>无法枚举设备但能使用摄像头</strong>
<br><small>正常现象,使用兼容模式的默认设备选项即可</small>
</li>
<li>
<span class="status-indicator status-warning"></span>
<strong>权限被拒绝</strong>
<br><small>检查浏览器权限设置,清除网站数据后重新允许权限</small>
</li>
<li>
<span class="status-indicator status-error"></span>
<strong>摄像头被占用</strong>
<br><small>关闭其他使用摄像头的应用程序或浏览器标签页</small>
</li>
</div>
</div>
<div class="section">
<h3>🧪 测试工具</h3>
<p>使用以下工具测试您的浏览器兼容性和摄像头功能:</p>
<div style="margin-top: 20px;">
<a href="camera_permission_test.html" class="btn">📷 摄像头权限测试</a>
<a href="mobile_client.html" class="btn btn-secondary">🚁 返回移动终端</a>
<button class="btn" onclick="testCurrentBrowser()">🔍 重新检测浏览器</button>
</div>
</div>
</div>
<script>
function testCurrentBrowser() {
const browserInfo = document.getElementById('browserInfo');
const compatibility = {
mediaDevices: !!navigator.mediaDevices,
getUserMedia: !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia),
enumerateDevices: !!(navigator.mediaDevices && navigator.mediaDevices.enumerateDevices),
permissions: !!navigator.permissions,
isSecure: location.protocol === 'https:' || location.hostname === 'localhost',
userAgent: navigator.userAgent
};
// 检测浏览器类型
let browserName = 'Unknown Browser';
let browserVersion = 'Unknown Version';
if (navigator.userAgent.includes('Chrome') && !navigator.userAgent.includes('Edg')) {
browserName = 'Chrome';
const match = navigator.userAgent.match(/Chrome\/(\d+)/);
if (match) browserVersion = match[1];
} else if (navigator.userAgent.includes('Firefox')) {
browserName = 'Firefox';
const match = navigator.userAgent.match(/Firefox\/(\d+)/);
if (match) browserVersion = match[1];
} else if (navigator.userAgent.includes('Safari') && !navigator.userAgent.includes('Chrome')) {
browserName = 'Safari';
const match = navigator.userAgent.match(/Version\/(\d+)/);
if (match) browserVersion = match[1];
} else if (navigator.userAgent.includes('Edg')) {
browserName = 'Edge';
const match = navigator.userAgent.match(/Edg\/(\d+)/);
if (match) browserVersion = match[1];
}
// 生成检测结果
let resultHtml = `
<h4>🔍 检测结果</h4>
<p><strong>浏览器:</strong> ${browserName} ${browserVersion}</p>
<div style="margin: 15px 0;">
`;
const features = [
{ name: 'MediaDevices API', supported: compatibility.mediaDevices, critical: true },
{ name: 'getUserMedia方法', supported: compatibility.getUserMedia, critical: true },
{ name: '设备枚举功能', supported: compatibility.enumerateDevices, critical: false },
{ name: '权限查询API', supported: compatibility.permissions, critical: false },
{ name: 'HTTPS安全环境', supported: compatibility.isSecure, critical: true }
];
features.forEach(feature => {
const status = feature.supported ?
'<span class="status-indicator status-ok"></span>✅ 支持' :
'<span class="status-indicator status-error"></span>❌ 不支持';
const importance = feature.critical ? ' (必需)' : ' (可选)';
resultHtml += `<div style="margin: 8px 0;">${status} <strong>${feature.name}</strong>${importance}</div>`;
});
resultHtml += '</div>';
// 给出建议
const criticalIssues = features.filter(f => f.critical && !f.supported);
if (criticalIssues.length === 0) {
if (compatibility.enumerateDevices) {
resultHtml += '<div class="solution-box"><strong>✅ 您的浏览器完全兼容!</strong><br>可以正常使用所有摄像头功能。</div>';
} else {
resultHtml += '<div class="warning-box"><strong>⚠️ 基本兼容</strong><br>摄像头功能正常,但需要使用兼容模式进行设备选择。</div>';
}
} else {
resultHtml += `<div class="error-box"><strong>❌ 兼容性问题</strong><br>检测到 ${criticalIssues.length} 个关键功能不支持,建议升级浏览器。</div>`;
}
browserInfo.innerHTML = resultHtml;
}
// 页面加载时自动检测
window.onload = function () {
testCurrentBrowser();
};
</script>
</body>
</html>

@ -0,0 +1,504 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>📷 摄像头权限测试</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
}
.header {
text-align: center;
padding: 20px 0;
border-bottom: 2px solid rgba(255, 255, 255, 0.2);
margin-bottom: 30px;
}
.test-section {
background: rgba(0, 0, 0, 0.3);
border-radius: 15px;
padding: 25px;
margin-bottom: 20px;
}
.test-title {
font-size: 18px;
margin-bottom: 15px;
color: #4CAF50;
}
.test-result {
margin: 10px 0;
padding: 10px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.1);
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
margin: 5px;
background: #4CAF50;
color: white;
}
.btn:hover {
background: #45a049;
}
.btn:disabled {
background: #666;
cursor: not-allowed;
}
.video-container {
position: relative;
background: #000;
border-radius: 10px;
overflow: hidden;
margin: 20px 0;
max-height: 400px;
}
#testVideo {
width: 100%;
height: auto;
display: block;
}
.log-area {
background: rgba(0, 0, 0, 0.5);
border-radius: 10px;
padding: 15px;
height: 300px;
overflow-y: auto;
font-family: monospace;
font-size: 14px;
line-height: 1.6;
}
.log-entry {
margin-bottom: 5px;
}
.log-success {
color: #4CAF50;
}
.log-error {
color: #f44336;
}
.log-warning {
color: #FF9800;
}
.log-info {
color: #2196F3;
}
.permission-status {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-weight: bold;
margin-left: 10px;
}
.status-granted {
background: #4CAF50;
}
.status-denied {
background: #f44336;
}
.status-prompt {
background: #FF9800;
}
.status-unknown {
background: #666;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📷 摄像头权限测试工具</h1>
<p>全面测试摄像头权限获取方法的正确性</p>
</div>
<div class="test-section">
<h3 class="test-title">🔍 1. 浏览器兼容性检查</h3>
<div id="compatibilityResult" class="test-result">等待测试...</div>
<button class="btn" onclick="testCompatibility()">开始检查</button>
</div>
<div class="test-section">
<h3 class="test-title">🔐 2. 权限状态查询</h3>
<div id="permissionResult" class="test-result">等待测试...</div>
<button class="btn" onclick="checkPermissionStatus()">检查权限状态</button>
</div>
<div class="test-section">
<h3 class="test-title">📱 3. 设备枚举测试</h3>
<div id="deviceResult" class="test-result">等待测试...</div>
<button class="btn" onclick="enumerateDevices()">枚举设备</button>
</div>
<div class="test-section">
<h3 class="test-title">🎥 4. 摄像头访问测试</h3>
<div id="cameraResult" class="test-result">等待测试...</div>
<div class="video-container" style="display: none;" id="videoContainer">
<video id="testVideo" autoplay muted playsinline></video>
</div>
<button class="btn" onclick="testCameraAccess()">请求摄像头权限</button>
<button class="btn" onclick="stopCamera()" style="background: #f44336;">停止摄像头</button>
</div>
<div class="test-section">
<h3 class="test-title">📋 测试日志</h3>
<div id="logArea" class="log-area"></div>
<button class="btn" onclick="clearLog()" style="background: #666;">清空日志</button>
</div>
</div>
<script>
let currentStream = null;
let permissionChangeListener = null;
function log(message, type = 'info') {
const logArea = document.getElementById('logArea');
const timestamp = new Date().toLocaleTimeString();
const entry = document.createElement('div');
entry.className = `log-entry log-${type}`;
entry.textContent = `${timestamp} - ${message}`;
logArea.appendChild(entry);
logArea.scrollTop = logArea.scrollHeight;
}
function clearLog() {
document.getElementById('logArea').innerHTML = '';
}
function updateResult(elementId, content, type = 'info') {
const element = document.getElementById(elementId);
element.innerHTML = content;
element.className = `test-result log-${type}`;
}
// 1. 浏览器兼容性检查
function testCompatibility() {
log('开始浏览器兼容性检查...', 'info');
const checks = [
{
name: 'MediaDevices API',
test: () => !!navigator.mediaDevices,
required: true
},
{
name: 'getUserMedia方法',
test: () => !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia),
required: true
},
{
name: 'enumerateDevices方法',
test: () => !!(navigator.mediaDevices && navigator.mediaDevices.enumerateDevices),
required: false
},
{
name: 'Permissions API',
test: () => !!navigator.permissions,
required: false
},
{
name: 'HTTPS环境',
test: () => location.protocol === 'https:' || location.hostname === 'localhost',
required: true
}
];
let resultHtml = '<h4>兼容性检查结果:</h4>';
let allPassed = true;
checks.forEach(check => {
const passed = check.test();
const status = passed ? '✅' : (check.required ? '❌' : '⚠️');
const statusText = passed ? '支持' : '不支持';
resultHtml += `<div>${status} ${check.name}: ${statusText}</div>`;
if (!passed && check.required) {
allPassed = false;
}
log(`${check.name}: ${statusText}`, passed ? 'success' : (check.required ? 'error' : 'warning'));
});
if (allPassed) {
resultHtml += '<div style="color: #4CAF50; margin-top: 10px;"><strong>✅ 浏览器完全兼容摄像头功能</strong></div>';
log('浏览器兼容性检查通过', 'success');
} else {
resultHtml += '<div style="color: #f44336; margin-top: 10px;"><strong>❌ 浏览器不兼容,请使用现代浏览器</strong></div>';
log('浏览器兼容性检查失败', 'error');
}
updateResult('compatibilityResult', resultHtml, allPassed ? 'success' : 'error');
}
// 2. 权限状态查询
async function checkPermissionStatus() {
log('开始权限状态查询...', 'info');
try {
if (!navigator.permissions) {
updateResult('permissionResult', '❌ 浏览器不支持权限查询API', 'warning');
log('浏览器不支持权限查询API', 'warning');
return;
}
const result = await navigator.permissions.query({ name: 'camera' });
const statusMap = {
'granted': { text: '已授权', color: 'success', icon: '✅' },
'denied': { text: '已拒绝', color: 'error', icon: '❌' },
'prompt': { text: '需要询问', color: 'warning', icon: '⚠️' }
};
const status = statusMap[result.state] || { text: '未知', color: 'info', icon: '❓' };
let resultHtml = `
<h4>权限状态查询结果:</h4>
<div>${status.icon} 摄像头权限状态: <span class="permission-status status-${result.state}">${status.text}</span></div>
`;
// 添加权限变化监听
if (permissionChangeListener) {
result.removeEventListener('change', permissionChangeListener);
}
permissionChangeListener = () => {
log(`权限状态变化: ${result.state}`, 'info');
checkPermissionStatus(); // 重新检查
};
result.addEventListener('change', permissionChangeListener);
resultHtml += '<div style="margin-top: 10px;">✅ 已设置权限变化监听</div>';
updateResult('permissionResult', resultHtml, status.color);
log(`摄像头权限状态: ${result.state}`, status.color);
} catch (error) {
const errorMsg = `权限查询失败: ${error.message}`;
updateResult('permissionResult', `❌ ${errorMsg}`, 'error');
log(errorMsg, 'error');
}
}
// 3. 设备枚举测试
async function enumerateDevices() {
log('开始设备枚举测试...', 'info');
try {
if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
updateResult('deviceResult', '❌ 浏览器不支持设备枚举功能', 'error');
log('浏览器不支持设备枚举功能', 'error');
return;
}
const devices = await navigator.mediaDevices.enumerateDevices();
const videoDevices = devices.filter(device => device.kind === 'videoinput');
let resultHtml = `<h4>设备枚举结果:</h4>`;
resultHtml += `<div>📹 发现 ${videoDevices.length} 个视频设备</div>`;
if (videoDevices.length === 0) {
resultHtml += '<div style="color: #f44336;">❌ 未找到任何视频设备</div>';
log('未找到任何视频设备', 'error');
} else {
videoDevices.forEach((device, index) => {
const label = device.label || `摄像头 ${index + 1} (需要权限显示详细信息)`;
resultHtml += `<div>📱 设备 ${index + 1}: ${label}</div>`;
log(`设备 ${index + 1}: ${label} (ID: ${device.deviceId.substr(0, 8)}...)`, 'info');
});
// 检查是否有设备标签
const hasLabels = videoDevices.some(device => device.label);
if (!hasLabels) {
resultHtml += '<div style="color: #FF9800; margin-top: 10px;">⚠️ 设备标签为空,可能需要先获取摄像头权限</div>';
log('设备标签为空,需要摄像头权限才能显示详细信息', 'warning');
}
}
updateResult('deviceResult', resultHtml, videoDevices.length > 0 ? 'success' : 'error');
log(`设备枚举完成,找到 ${videoDevices.length} 个视频设备`, 'success');
} catch (error) {
const errorMsg = `设备枚举失败: ${error.message}`;
updateResult('deviceResult', `❌ ${errorMsg}`, 'error');
log(errorMsg, 'error');
}
}
// 4. 摄像头访问测试
async function testCameraAccess() {
log('开始摄像头访问测试...', 'info');
try {
// 先停止之前的流
if (currentStream) {
currentStream.getTracks().forEach(track => track.stop());
currentStream = null;
}
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
throw new Error('浏览器不支持摄像头访问功能');
}
const constraints = {
video: {
facingMode: 'environment', // 优先使用后置摄像头
width: { ideal: 640 },
height: { ideal: 480 },
frameRate: { ideal: 30, max: 30 }
},
audio: false
};
log('正在请求摄像头权限...', 'info');
currentStream = await navigator.mediaDevices.getUserMedia(constraints);
const video = document.getElementById('testVideo');
video.srcObject = currentStream;
// 等待视频开始播放
await new Promise((resolve, reject) => {
video.onloadedmetadata = () => {
log('视频流准备就绪', 'success');
resolve();
};
video.onerror = reject;
setTimeout(() => reject(new Error('视频加载超时')), 10000);
});
// 显示视频容器
document.getElementById('videoContainer').style.display = 'block';
// 获取实际的视频配置
const track = currentStream.getVideoTracks()[0];
const settings = track.getSettings();
let resultHtml = `
<h4>摄像头访问成功!</h4>
<div>✅ 摄像头权限已获取</div>
<div>📹 分辨率: ${settings.width}x${settings.height}</div>
<div>🎯 帧率: ${settings.frameRate}fps</div>
<div>📱 设备: ${track.label || '未知设备'}</div>
`;
if (settings.facingMode) {
resultHtml += `<div>📷 摄像头方向: ${settings.facingMode}</div>`;
}
updateResult('cameraResult', resultHtml, 'success');
log(`摄像头访问成功: ${track.label || '未知设备'}`, 'success');
log(`视频配置: ${settings.width}x${settings.height}@${settings.frameRate}fps`, 'info');
} catch (error) {
let errorMsg = error.message;
let errorType = 'error';
// 详细的错误分析
if (error.name === 'NotAllowedError') {
errorMsg = '摄像头权限被拒绝';
errorType = 'error';
} else if (error.name === 'NotFoundError') {
errorMsg = '未找到可用的摄像头设备';
errorType = 'error';
} else if (error.name === 'NotSupportedError') {
errorMsg = '浏览器不支持摄像头功能';
errorType = 'error';
} else if (error.name === 'NotReadableError') {
errorMsg = '摄像头被其他应用占用';
errorType = 'error';
} else if (error.name === 'OverconstrainedError') {
errorMsg = '摄像头不支持请求的配置';
errorType = 'warning';
} else if (error.name === 'SecurityError') {
errorMsg = '安全限制请在HTTPS环境下访问';
errorType = 'error';
}
let resultHtml = `<h4>摄像头访问失败</h4><div>❌ ${errorMsg}</div>`;
// 提供解决建议
if (error.name === 'NotAllowedError') {
resultHtml += `
<div style="margin-top: 10px;">
<strong>💡 解决方案:</strong><br>
1. 点击浏览器地址栏的摄像头图标或锁图标<br>
2. 选择"允许"摄像头权限<br>
3. 刷新页面重试
</div>
`;
}
updateResult('cameraResult', resultHtml, errorType);
log(`摄像头访问失败: ${errorMsg}`, errorType);
}
}
// 停止摄像头
function stopCamera() {
if (currentStream) {
currentStream.getTracks().forEach(track => track.stop());
currentStream = null;
document.getElementById('videoContainer').style.display = 'none';
updateResult('cameraResult', '<h4>摄像头已停止</h4><div>📱 视频流已释放</div>', 'info');
log('摄像头已停止,视频流已释放', 'info');
} else {
log('没有活跃的摄像头流', 'warning');
}
}
// 页面加载时自动运行兼容性检查
window.onload = function () {
log('摄像头权限测试工具已加载', 'success');
testCompatibility();
};
// 页面卸载时清理资源
window.onbeforeunload = function () {
stopCamera();
};
</script>
</body>
</html>

@ -0,0 +1,312 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GPS连接测试</title>
<style>
body {
font-family: Arial, sans-serif;
padding: 20px;
background: #f0f0f0;
color: #333;
}
.container {
max-width: 600px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.status-box {
padding: 15px;
margin: 10px 0;
border-radius: 5px;
font-weight: bold;
}
.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.info {
background: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.warning {
background: #fff3cd;
color: #856404;
border: 1px solid #ffeaa7;
}
button {
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
margin: 5px;
}
button:hover {
background: #0056b3;
}
button:disabled {
background: #6c757d;
cursor: not-allowed;
}
.log {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 5px;
padding: 10px;
height: 200px;
overflow-y: auto;
font-family: monospace;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<h1>📍 GPS连接测试工具</h1>
<div class="status-box info">
<strong>当前状态:</strong>
<div id="status">初始化中...</div>
</div>
<div class="status-box" id="gpsBox">
<strong>GPS坐标</strong>
<div id="gpsCoords">等待获取...</div>
</div>
<div class="status-box" id="connectionBox">
<strong>服务器连接:</strong>
<div id="connectionStatus">未测试</div>
</div>
<div style="text-align: center; margin: 20px 0;">
<button onclick="requestGPS()">请求GPS权限</button>
<button onclick="testConnection()">测试服务器连接</button>
<button onclick="sendTestData()" id="sendBtn" disabled>发送测试数据</button>
<button onclick="clearLog()">清空日志</button>
</div>
<div class="warning">
<strong>⚠️ 重要提示:</strong><br>
• 现代浏览器在HTTP模式下可能限制GPS访问<br>
• 请确保允许浏览器访问位置信息<br>
• 在室外或窗边可获得更好的GPS信号<br>
• 首次访问需要用户授权位置权限
</div>
<h3>📋 操作日志</h3>
<div class="log" id="logArea"></div>
</div>
<script>
let currentPosition = null;
let serverConnected = false;
const serverHost = window.location.hostname;
const serverPort = window.location.port || 5000;
const serverProtocol = window.location.protocol;
const baseURL = `${serverProtocol}//${serverHost}:${serverPort}`;
function log(message, type = 'info') {
const logArea = document.getElementById('logArea');
const timestamp = new Date().toLocaleTimeString();
const entry = `[${timestamp}] ${message}\n`;
logArea.textContent += entry;
logArea.scrollTop = logArea.scrollHeight;
console.log(`[${type}] ${message}`);
}
function updateStatus(message, type = 'info') {
const statusDiv = document.getElementById('status');
statusDiv.textContent = message;
statusDiv.style.color = type === 'success' ? '#155724' :
type === 'error' ? '#721c24' : '#0c5460';
}
function updateGPSBox(message, type = 'info') {
const gpsBox = document.getElementById('gpsBox');
document.getElementById('gpsCoords').textContent = message;
gpsBox.className = `status-box ${type}`;
}
function updateConnectionBox(message, type = 'info') {
const connBox = document.getElementById('connectionBox');
document.getElementById('connectionStatus').textContent = message;
connBox.className = `status-box ${type}`;
}
function requestGPS() {
log('开始请求GPS权限...');
updateStatus('正在请求GPS权限...');
if (!('geolocation' in navigator)) {
log('❌ 设备不支持GPS定位', 'error');
updateGPSBox('设备不支持GPS', 'error');
return;
}
const options = {
enableHighAccuracy: true,
timeout: 15000,
maximumAge: 10000
};
navigator.geolocation.getCurrentPosition(
(position) => {
currentPosition = {
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy,
timestamp: Date.now()
};
const gpsText = `${position.coords.latitude.toFixed(6)}, ${position.coords.longitude.toFixed(6)}`;
const accuracyText = `精度: ${position.coords.accuracy.toFixed(0)}m`;
log(`✅ GPS获取成功: ${gpsText} (${accuracyText})`, 'success');
updateGPSBox(`${gpsText}\n${accuracyText}`, 'success');
updateStatus('GPS权限获取成功', 'success');
document.getElementById('sendBtn').disabled = !serverConnected;
},
(error) => {
let errorMsg = '';
switch (error.code) {
case error.PERMISSION_DENIED:
errorMsg = '用户拒绝了位置访问请求';
break;
case error.POSITION_UNAVAILABLE:
errorMsg = '位置信息不可用';
break;
case error.TIMEOUT:
errorMsg = '位置获取超时';
break;
default:
errorMsg = '未知位置错误';
break;
}
log(`❌ GPS获取失败: ${errorMsg}`, 'error');
updateGPSBox(`获取失败: ${errorMsg}`, 'error');
updateStatus('GPS权限获取失败', 'error');
if (error.code === error.PERMISSION_DENIED) {
log('💡 请在浏览器中允许位置访问权限', 'info');
}
},
options
);
}
async function testConnection() {
log('开始测试服务器连接...');
updateStatus('正在测试服务器连接...');
try {
const testData = {
device_id: 'test_device_' + Date.now(),
test: true
};
const response = await fetch(`${baseURL}/mobile/ping`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(testData)
});
if (response.ok) {
const data = await response.json();
log(`✅ 服务器连接成功: ${baseURL}`, 'success');
updateConnectionBox(`连接成功: ${baseURL}`, 'success');
updateStatus('服务器连接正常', 'success');
serverConnected = true;
document.getElementById('sendBtn').disabled = !currentPosition;
} else {
throw new Error(`HTTP ${response.status}`);
}
} catch (error) {
log(`❌ 服务器连接失败: ${error.message}`, 'error');
updateConnectionBox(`连接失败: ${error.message}`, 'error');
updateStatus('服务器连接失败', 'error');
serverConnected = false;
}
}
async function sendTestData() {
if (!currentPosition || !serverConnected) {
log('❌ 请先获取GPS并测试服务器连接', 'error');
return;
}
log('发送测试数据到服务器...');
updateStatus('正在发送测试数据...');
try {
const testData = {
device_id: 'gps_test_' + Date.now(),
device_name: 'GPS测试设备',
timestamp: Date.now(),
gps: currentPosition,
battery: 100,
test_mode: true
};
const response = await fetch(`${baseURL}/mobile/upload`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(testData)
});
if (response.ok) {
log('✅ 测试数据发送成功', 'success');
updateStatus('测试数据发送成功', 'success');
} else {
throw new Error(`HTTP ${response.status}`);
}
} catch (error) {
log(`❌ 测试数据发送失败: ${error.message}`, 'error');
updateStatus('测试数据发送失败', 'error');
}
}
function clearLog() {
document.getElementById('logArea').textContent = '';
}
// 页面加载时自动初始化
window.onload = function () {
log('GPS连接测试工具已加载');
log(`服务器地址: ${baseURL}`);
log(`协议: ${serverProtocol.replace(':', '')}, 主机: ${serverHost}, 端口: ${serverPort}`);
updateStatus('就绪 - 点击按钮开始测试');
};
</script>
</body>
</html>

@ -0,0 +1,247 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>📱 旧版浏览器使用指南</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
color: white;
min-height: 100vh;
padding: 15px;
line-height: 1.6;
}
.container {
max-width: 600px;
margin: 0 auto;
}
.header {
text-align: center;
padding: 20px 0;
margin-bottom: 20px;
border-bottom: 2px solid rgba(255, 255, 255, 0.3);
}
.section {
background: rgba(0, 0, 0, 0.3);
border-radius: 10px;
padding: 20px;
margin-bottom: 15px;
}
.section h3 {
color: #ffeb3b;
margin-bottom: 10px;
}
.step {
background: rgba(255, 255, 255, 0.1);
padding: 15px;
margin: 10px 0;
border-radius: 8px;
border-left: 4px solid #4CAF50;
}
.step-number {
background: #4CAF50;
color: white;
width: 25px;
height: 25px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: bold;
margin-right: 10px;
}
.warning {
background: rgba(244, 67, 54, 0.2);
border: 2px solid #f44336;
border-radius: 8px;
padding: 15px;
margin: 15px 0;
}
.success {
background: rgba(76, 175, 80, 0.2);
border: 2px solid #4CAF50;
border-radius: 8px;
padding: 15px;
margin: 15px 0;
}
.btn {
display: inline-block;
background: #4CAF50;
color: white;
padding: 12px 20px;
text-decoration: none;
border-radius: 6px;
font-weight: bold;
margin: 5px;
text-align: center;
}
.btn-secondary {
background: #2196F3;
}
.device-option {
background: rgba(255, 255, 255, 0.15);
border-radius: 8px;
padding: 12px;
margin: 8px 0;
cursor: pointer;
transition: background 0.3s;
}
.device-option:hover {
background: rgba(255, 255, 255, 0.25);
}
.browser-list {
list-style: none;
padding: 0;
}
.browser-list li {
padding: 8px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}
.browser-list li:last-child {
border-bottom: none;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📱 旧版浏览器使用指南</h1>
<p>移动侦察终端兼容模式使用说明</p>
</div>
<div class="warning">
<h4>⚠️ 检测结果</h4>
<p>您的浏览器兼容性较低,但系统已自动启用兼容模式。请按照以下步骤操作:</p>
</div>
<div class="section">
<h3>🔧 使用步骤</h3>
<div class="step">
<span class="step-number">1</span>
<strong>返回主页面</strong>
<br>关闭此页面,返回移动侦察终端主界面
</div>
<div class="step">
<span class="step-number">2</span>
<strong>查看系统状态</strong>
<br>确认页面显示"兼容模式:已为您的浏览器启用兼容支持"
</div>
<div class="step">
<span class="step-number">3</span>
<strong>选择摄像头设备</strong>
<br>点击页面中的"📷 选择设备"按钮
</div>
<div class="step">
<span class="step-number">4</span>
<strong>测试设备选项</strong>
<br>在弹窗中选择以下任一设备进行测试:
<div style="margin-top: 10px;">
<div class="device-option">📱 默认摄像头 - 系统自动选择</div>
<div class="device-option">📹 后置摄像头 - 优先使用后置</div>
<div class="device-option">🤳 前置摄像头 - 优先使用前置</div>
</div>
</div>
<div class="step">
<span class="step-number">5</span>
<strong>启动摄像头</strong>
<br>选择设备后点击"✅ 使用选择的设备"
</div>
<div class="step">
<span class="step-number">6</span>
<strong>允许权限</strong>
<br>当浏览器弹出权限请求时,点击"允许"
</div>
<div class="step">
<span class="step-number">7</span>
<strong>开始使用</strong>
<br>摄像头启动成功后,点击"📹 开始传输"
</div>
</div>
<div class="section">
<h3>🚨 常见问题</h3>
<p><strong>Q: 权限被拒绝怎么办?</strong></p>
<p>A: 清除浏览器数据,重新访问页面并允许权限</p>
<p style="margin-top: 15px;"><strong>Q: 某个设备选项不工作?</strong></p>
<p>A: 尝试其他设备选项,通常至少有一个会工作</p>
<p style="margin-top: 15px;"><strong>Q: 完全无法使用摄像头?</strong></p>
<p>A: 考虑升级浏览器或换用现代浏览器</p>
</div>
<div class="section">
<h3>🌐 推荐浏览器</h3>
<p>为获得最佳体验,建议升级到以下浏览器:</p>
<ul class="browser-list">
<li>🌐 <strong>Chrome 53+</strong> - 完全支持所有功能</li>
<li>🦊 <strong>Firefox 36+</strong> - 良好的兼容性</li>
<li>🧭 <strong>Safari 11+</strong> - iOS/macOS用户推荐</li>
<li><strong>Edge 17+</strong> - Windows用户推荐</li>
</ul>
</div>
<div class="success">
<h4>✅ 重要提醒</h4>
<p>兼容模式虽然功能有限但基本的摄像头录制和GPS定位功能仍然可用。请耐心按步骤操作。</p>
</div>
<div style="text-align: center; margin: 30px 0;">
<a href="mobile_client.html" class="btn">🚁 返回移动终端</a>
<a href="browser_compatibility_guide.html" class="btn btn-secondary">📋 详细兼容性说明</a>
</div>
<div
style="background: rgba(0,0,0,0.3); padding: 15px; border-radius: 8px; margin-top: 20px; font-size: 12px; color: #ccc;">
<p><strong>技术说明:</strong>您的浏览器缺少现代Web API支持但我们通过以下方式提供兼容</p>
<ul style="margin: 10px 0; padding-left: 20px;">
<li>使用传统getUserMedia API (webkit/moz前缀)</li>
<li>提供预定义设备配置代替设备枚举</li>
<li>简化权限检查流程</li>
<li>降级使用基础功能</li>
</ul>
</div>
</div>
<script>
// 简单的页面加载提示
window.onload = function () {
console.log('旧版浏览器帮助页面已加载');
};
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

@ -0,0 +1,430 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>📱 权限设置指南</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
margin: 0;
padding: 20px;
min-height: 100vh;
}
.container {
max-width: 600px;
margin: 0 auto;
background: rgba(255, 255, 255, 0.1);
border-radius: 15px;
padding: 30px;
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
}
h1 {
text-align: center;
margin-bottom: 30px;
font-size: 28px;
}
.step {
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
border-left: 4px solid #4CAF50;
}
.step h3 {
margin: 0 0 15px 0;
color: #4CAF50;
font-size: 18px;
}
.step p {
margin: 10px 0;
line-height: 1.6;
}
.button-group {
text-align: center;
margin: 30px 0;
}
.btn {
background: #4CAF50;
color: white;
border: none;
padding: 15px 30px;
border-radius: 25px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
margin: 10px;
transition: all 0.3s ease;
text-decoration: none;
display: inline-block;
}
.btn:hover {
background: #45a049;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
.btn-secondary {
background: #2196F3;
}
.btn-secondary:hover {
background: #1976D2;
}
.warning {
background: rgba(255, 193, 7, 0.2);
border: 2px solid #ffc107;
border-radius: 10px;
padding: 15px;
margin: 20px 0;
}
.success {
background: rgba(76, 175, 80, 0.2);
border: 2px solid #4CAF50;
border-radius: 10px;
padding: 15px;
margin: 20px 0;
}
.status {
background: rgba(0, 0, 0, 0.3);
border-radius: 10px;
padding: 15px;
margin: 20px 0;
text-align: center;
}
.status-item {
display: flex;
justify-content: space-between;
align-items: center;
margin: 10px 0;
padding: 10px;
background: rgba(255, 255, 255, 0.1);
border-radius: 5px;
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
background: #f44336;
animation: pulse 2s infinite;
}
.status-indicator.granted {
background: #4CAF50;
animation: none;
}
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}
.browser-guide {
background: rgba(0, 0, 0, 0.2);
border-radius: 10px;
padding: 15px;
margin: 20px 0;
}
.browser-guide h4 {
margin: 0 0 10px 0;
color: #81C784;
}
.browser-guide ul {
margin: 10px 0;
padding-left: 20px;
}
.browser-guide li {
margin: 5px 0;
line-height: 1.5;
}
</style>
</head>
<body>
<div class="container">
<h1>📱 权限设置指南</h1>
<div class="status">
<h3>📊 当前权限状态</h3>
<div class="status-item">
<span>📍 GPS定位权限</span>
<div class="status-indicator" id="gpsIndicator"></div>
</div>
<div class="status-item">
<span>📷 摄像头权限</span>
<div class="status-indicator" id="cameraIndicator"></div>
</div>
<div id="statusText">正在检查权限状态...</div>
</div>
<div class="step">
<h3>🎯 第1步GPS定位权限</h3>
<p>为了在地图上显示您的位置需要获取GPS定位权限</p>
<ul>
<li>当浏览器弹出权限请求时,点击<strong>"允许"</strong></li>
<li>如果已经拒绝,点击地址栏的🔒图标重新设置</li>
<li>确保设备的定位服务已开启</li>
</ul>
<div class="button-group">
<button class="btn" onclick="requestGPSPermission()">📍 请求GPS权限</button>
</div>
</div>
<div class="step">
<h3>📷 第2步摄像头权限</h3>
<p>为了拍摄和传输视频,需要获取摄像头访问权限:</p>
<ul>
<li>当浏览器询问摄像头权限时,点击<strong>"允许"</strong></li>
<li>如果失败,检查其他应用是否占用摄像头</li>
<li>建议使用后置摄像头以获得更好效果</li>
</ul>
<div class="button-group">
<button class="btn" onclick="requestCameraPermission()">📷 请求摄像头权限</button>
</div>
</div>
<div class="browser-guide">
<h4>🔧 不同浏览器的权限设置方法:</h4>
<div style="margin-bottom: 15px;">
<strong>📱 Safari (iOS):</strong>
<ul>
<li>设置 → Safari → 摄像头/麦克风 → 允许</li>
<li>设置 → 隐私与安全性 → 定位服务 → Safari → 使用App期间</li>
</ul>
</div>
<div style="margin-bottom: 15px;">
<strong>🤖 Chrome (Android):</strong>
<ul>
<li>点击地址栏左侧的🔒或ℹ️图标</li>
<li>设置权限为"允许"</li>
<li>或在设置 → 网站设置中调整</li>
</ul>
</div>
<div>
<strong>🖥️ 桌面浏览器:</strong>
<ul>
<li>点击地址栏的🔒图标</li>
<li>将摄像头和位置权限设为"允许"</li>
<li>刷新页面使设置生效</li>
</ul>
</div>
</div>
<div class="warning">
<h4>⚠️ 常见问题解决:</h4>
<p><strong>GPS获取失败</strong></p>
<ul>
<li>移动到窗边或室外获得更好信号</li>
<li>检查设备的定位服务是否开启</li>
<li>在浏览器设置中清除网站数据后重试</li>
</ul>
<p><strong>摄像头无法访问:</strong></p>
<ul>
<li>关闭其他正在使用摄像头的应用</li>
<li>重启浏览器或设备</li>
<li>使用Chrome或Safari等现代浏览器</li>
</ul>
</div>
<div class="button-group">
<a href="gps_test.html" class="btn btn-secondary">🧪 权限测试页面</a>
<a href="mobile_client.html" class="btn" id="continueBtn" style="display: none;">✅ 继续使用系统</a>
</div>
<div class="success" id="successMessage" style="display: none;">
<h4>🎉 权限设置成功!</h4>
<p>所有权限已获取,您现在可以正常使用移动侦察系统了。</p>
</div>
</div>
<script>
let gpsPermission = false;
let cameraPermission = false;
// 页面加载时检查权限状态
window.onload = function () {
checkPermissions();
};
async function checkPermissions() {
// 检查GPS权限
if ('geolocation' in navigator) {
try {
await new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, { timeout: 5000 });
});
updateGPSStatus(true);
} catch (e) {
updateGPSStatus(false);
}
} else {
updateGPSStatus(false);
}
// 检查摄像头权限
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
stream.getTracks().forEach(track => track.stop()); // 停止预览
updateCameraStatus(true);
} catch (e) {
updateCameraStatus(false);
}
} else {
updateCameraStatus(false);
}
updateOverallStatus();
}
function updateGPSStatus(granted) {
gpsPermission = granted;
const indicator = document.getElementById('gpsIndicator');
if (granted) {
indicator.classList.add('granted');
} else {
indicator.classList.remove('granted');
}
}
function updateCameraStatus(granted) {
cameraPermission = granted;
const indicator = document.getElementById('cameraIndicator');
if (granted) {
indicator.classList.add('granted');
} else {
indicator.classList.remove('granted');
}
}
function updateOverallStatus() {
const statusText = document.getElementById('statusText');
const continueBtn = document.getElementById('continueBtn');
const successMessage = document.getElementById('successMessage');
if (gpsPermission && cameraPermission) {
statusText.textContent = '✅ 所有权限已获取!';
statusText.style.color = '#4CAF50';
continueBtn.style.display = 'inline-block';
successMessage.style.display = 'block';
} else {
let missing = [];
if (!gpsPermission) missing.push('GPS定位');
if (!cameraPermission) missing.push('摄像头');
statusText.textContent = `❌ 缺少权限: ${missing.join('、')}`;
statusText.style.color = '#f44336';
continueBtn.style.display = 'none';
successMessage.style.display = 'none';
}
}
async function requestGPSPermission() {
if (!('geolocation' in navigator)) {
alert('❌ 您的设备不支持GPS定位功能');
return;
}
try {
await new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(
(position) => {
alert(`✅ GPS权限获取成功\n位置: ${position.coords.latitude.toFixed(6)}, ${position.coords.longitude.toFixed(6)}`);
resolve(position);
},
(error) => {
let message = '';
switch (error.code) {
case error.PERMISSION_DENIED:
message = '❌ GPS权限被拒绝\n请在浏览器设置中允许位置访问';
break;
case error.POSITION_UNAVAILABLE:
message = '❌ 位置信息不可用\n请移动到室外或窗边';
break;
case error.TIMEOUT:
message = '❌ 位置获取超时\n请检查GPS信号';
break;
default:
message = '❌ GPS获取失败: ' + error.message;
}
alert(message);
reject(error);
},
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 10000 }
);
});
updateGPSStatus(true);
} catch (e) {
updateGPSStatus(false);
}
updateOverallStatus();
}
async function requestCameraPermission() {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
alert('❌ 您的浏览器不支持摄像头功能\n请使用Chrome、Firefox或Safari等现代浏览器');
return;
}
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'environment' },
audio: false
});
// 立即停止流,只是为了测试权限
stream.getTracks().forEach(track => track.stop());
alert('✅ 摄像头权限获取成功!');
updateCameraStatus(true);
} catch (error) {
let message = '';
if (error.name === 'NotAllowedError') {
message = '❌ 摄像头权限被拒绝\n请在浏览器设置中允许摄像头访问';
} else if (error.name === 'NotFoundError') {
message = '❌ 未找到可用的摄像头设备';
} else if (error.name === 'NotReadableError') {
message = '❌ 摄像头被其他应用占用\n请关闭其他使用摄像头的应用';
} else {
message = '❌ 摄像头访问失败: ' + error.message;
}
alert(message);
updateCameraStatus(false);
}
updateOverallStatus();
}
</script>
</body>
</html>

@ -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"

@ -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()

@ -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'
]

@ -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) # 黑色背景
# 人体类别IDCOCO数据集中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 # 地图是否自动刷新

@ -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

@ -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"""<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>实时人员位置追踪系统 🚁</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script type="text/javascript" src="https://webapi.amap.com/maps?v=1.4.15&key={self.api_key}"></script>
<style>
body {{ margin: 0; padding: 0; }}
#mapContainer {{ width: 100%; height: 100vh; }}
.info-panel {{
position: absolute;
top: 10px;
left: 10px;
background: rgba(0,0,0,0.8);
color: white;
padding: 15px;
border-radius: 8px;
font-family: Arial, sans-serif;
min-width: 250px;
z-index: 1000;
}}
.status {{ color: #00ff00; }}
.warning {{ color: #ffaa00; }}
.info {{ color: #00aaff; }}
</style>
</head>
<body>
<div id="mapContainer"></div>
<div class="info-panel">
<h3>🚁 无人机战场态势感知</h3>
<div class="status"> 摄像头在线</div>
<div class="info">📍 坐标: {self.camera_lat:.6f}, {self.camera_lng:.6f}</div>
<div class="info">🧭 朝向: {self.camera_heading}°</div>
<div class="warning" id="personCount">👥 检测到: {len(self.persons_positions)} </div>
<div style="margin-top: 10px; font-size: 12px;">
🔴 红点 = 人员位置<br>
📷 蓝点 = 摄像头位置<br>
实时更新
</div>
</div>
<script>
// 初始化地图
var map = new AMap.Map('mapContainer', {{
zoom: 18,
center: [{self.camera_lng}, {self.camera_lat}],
mapStyle: 'amap://styles/darkblue'
}});
// 添加地图控件
// map.addControl(new AMap.Scale()); // 临时注释掉以避免API兼容性问题
// map.addControl(new AMap.ToolBar());
// 摄像头标记
var cameraMarker = new AMap.Marker({{
position: [{self.camera_lng}, {self.camera_lat}],
icon: new AMap.Icon({{
size: new AMap.Size(32, 32),
image: 'https://webapi.amap.com/theme/v1.3/markers/n/mark_b.png'
}}),
title: '摄像头位置'
}});
map.add(cameraMarker);
// 人员数据
var personsData = {persons_data_json};
var personMarkers = [];
// 添加人员标记
personsData.forEach(function(person, index) {{
var marker = new AMap.Marker({{
position: [person.lng, person.lat],
icon: new AMap.Icon({{
size: new AMap.Size(24, 24),
image: 'https://webapi.amap.com/theme/v1.3/markers/n/mark_r.png'
}}),
title: '人员 ' + person.id + ' - 距离: ' + person.distance.toFixed(1) + 'm'
}});
personMarkers.push(marker);
map.add(marker);
}});
// 定时刷新页面以更新数据
setTimeout(function() {{
location.reload();
}}, 3000);
</script>
</body>
</html>"""
# 保存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}°")

@ -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

@ -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-3600为正北) 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()

@ -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"

@ -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

File diff suppressed because it is too large Load Diff

@ -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-----

@ -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-----

@ -0,0 +1,392 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>设备选择器测试</title>
<style>
body {
background: linear-gradient(135deg, #1e3c72, #2a5298);
color: white;
font-family: 'Microsoft YaHei', sans-serif;
margin: 0;
padding: 20px;
}
.container {
max-width: 500px;
margin: 0 auto;
padding: 20px;
}
.video-container {
background: rgba(0, 0, 0, 0.4);
border-radius: 15px;
margin: 20px 0;
overflow: hidden;
}
.video-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
background: rgba(0, 0, 0, 0.3);
border-radius: 8px 8px 0 0;
}
.device-select-btn {
background: #2196F3;
color: white;
border: none;
padding: 8px 12px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
}
.device-selector {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
}
.device-selector-content {
background: rgba(0, 20, 40, 0.95);
border: 2px solid #00aaff;
border-radius: 15px;
padding: 20px;
max-width: 90%;
max-height: 80%;
overflow-y: auto;
backdrop-filter: blur(10px);
}
.device-selector h3 {
margin: 0 0 15px 0;
color: #00aaff;
text-align: center;
}
.device-list {
margin: 15px 0;
}
.device-item {
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 15px;
margin: 10px 0;
cursor: pointer;
transition: all 0.3s ease;
border: 2px solid transparent;
}
.device-item:hover {
background: rgba(255, 255, 255, 0.2);
border-color: #00aaff;
}
.device-item.selected {
background: rgba(0, 170, 255, 0.3);
border-color: #00aaff;
}
.device-name {
font-weight: bold;
color: #00aaff;
margin-bottom: 5px;
}
.device-id {
font-size: 12px;
color: #ccc;
font-family: monospace;
}
.device-kind {
display: inline-block;
background: #4CAF50;
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
margin-top: 5px;
}
.device-selector-buttons {
display: flex;
justify-content: space-between;
margin-top: 20px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: bold;
margin: 0 5px;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn:disabled {
background: #666;
cursor: not-allowed;
}
.loading {
text-align: center;
color: #ccc;
padding: 20px;
}
.log {
background: rgba(0, 0, 0, 0.3);
border-radius: 8px;
padding: 10px;
margin: 20px 0;
max-height: 200px;
overflow-y: auto;
font-family: monospace;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<h1>🧪 设备选择器测试</h1>
<div class="video-container">
<div class="video-header">
<span>📹 视频设备</span>
<button class="device-select-btn" onclick="showDeviceSelector()">📷 选择设备</button>
</div>
<div id="videoPlaceholder" style="text-align: center; padding: 40px; color: #ccc;">
点击"选择设备"开始使用摄像头
</div>
</div>
<!-- 设备选择弹窗 -->
<div class="device-selector" id="deviceSelector" style="display: none;">
<div class="device-selector-content">
<h3>📷 选择视频设备</h3>
<!-- 本地设备列表 -->
<div>
<h4 style="color: #4CAF50; margin: 15px 0 10px 0;">📱 本地设备</h4>
<div class="device-list" id="localDeviceList">
<div class="loading">正在扫描本地设备...</div>
</div>
</div>
<div class="device-selector-buttons">
<button class="btn btn-secondary" onclick="hideDeviceSelector()">❌ 取消</button>
<button class="btn btn-primary" onclick="refreshDevices()">🔄 刷新设备</button>
<button class="btn" onclick="useSelectedDevice()" id="useDeviceBtn" disabled>✅ 使用选择的设备</button>
</div>
</div>
</div>
<div class="log" id="logPanel">
<div>系统初始化中...</div>
</div>
</div>
<script>
let availableDevices = [];
let selectedDeviceId = null;
let selectedDeviceInfo = null;
// 日志函数
function log(message, type = 'info') {
const logPanel = document.getElementById('logPanel');
const timestamp = new Date().toLocaleTimeString();
const entry = document.createElement('div');
entry.style.color = type === 'error' ? '#ff6b6b' : type === 'success' ? '#51cf66' : '#74c0fc';
entry.textContent = `${timestamp} - ${message}`;
logPanel.appendChild(entry);
logPanel.scrollTop = logPanel.scrollHeight;
}
// 扫描设备
async function scanDevices() {
log('正在扫描可用视频设备...', 'info');
try {
if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
throw new Error('浏览器不支持设备枚举功能');
}
const devices = await navigator.mediaDevices.enumerateDevices();
availableDevices = devices.filter(device => device.kind === 'videoinput');
log(`发现 ${availableDevices.length} 个视频设备`, 'success');
} catch (error) {
log(`设备扫描失败: ${error.message}`, 'error');
availableDevices = [];
}
}
// 显示设备选择器
async function showDeviceSelector() {
log('打开设备选择器', 'info');
const selector = document.getElementById('deviceSelector');
selector.style.display = 'flex';
await scanDevices();
updateDeviceList();
}
// 隐藏设备选择器
function hideDeviceSelector() {
document.getElementById('deviceSelector').style.display = 'none';
clearDeviceSelection();
}
// 刷新设备
async function refreshDevices() {
document.getElementById('localDeviceList').innerHTML = '<div class="loading">正在扫描设备...</div>';
await scanDevices();
updateDeviceList();
}
// 更新设备列表
function updateDeviceList() {
const localList = document.getElementById('localDeviceList');
if (availableDevices.length === 0) {
localList.innerHTML = '<div style="color: #ff6b6b; text-align: center; padding: 20px;">未发现本地摄像头设备<br><small>请确保已连接摄像头并允许浏览器访问</small></div>';
return;
}
localList.innerHTML = '';
availableDevices.forEach((device, index) => {
const deviceItem = document.createElement('div');
deviceItem.className = 'device-item';
deviceItem.onclick = () => selectDevice(device.deviceId, {
label: device.label || `摄像头 ${index + 1}`,
kind: device.kind,
isRemote: false
});
const deviceName = device.label || `摄像头 ${index + 1}`;
const isFrontCamera = deviceName.toLowerCase().includes('front') || deviceName.toLowerCase().includes('前');
const isBackCamera = deviceName.toLowerCase().includes('back') || deviceName.toLowerCase().includes('后');
let cameraIcon = '📷';
if (isFrontCamera) cameraIcon = '🤳';
else if (isBackCamera) cameraIcon = '📹';
deviceItem.innerHTML = `
<div class="device-name">${cameraIcon} ${deviceName}</div>
<div class="device-id">${device.deviceId}</div>
<div class="device-kind">本地设备</div>
`;
localList.appendChild(deviceItem);
});
}
// 选择设备
function selectDevice(deviceId, deviceInfo) {
// 清除之前的选择
document.querySelectorAll('.device-item').forEach(item => {
item.classList.remove('selected');
});
// 选择当前设备
event.currentTarget.classList.add('selected');
selectedDeviceId = deviceId;
selectedDeviceInfo = deviceInfo;
// 启用使用按钮
document.getElementById('useDeviceBtn').disabled = false;
log(`已选择设备: ${deviceInfo.label}`, 'info');
}
// 清除设备选择
function clearDeviceSelection() {
selectedDeviceId = null;
selectedDeviceInfo = null;
document.getElementById('useDeviceBtn').disabled = true;
document.querySelectorAll('.device-item').forEach(item => {
item.classList.remove('selected');
});
}
// 使用选择的设备
async function useSelectedDevice() {
if (!selectedDeviceId || !selectedDeviceInfo) {
log('请先选择一个设备', 'error');
return;
}
try {
log(`正在启动设备: ${selectedDeviceInfo.label}`, 'info');
const constraints = {
video: {
deviceId: { exact: selectedDeviceId },
width: { ideal: 640 },
height: { ideal: 480 }
},
audio: false
};
const stream = await navigator.mediaDevices.getUserMedia(constraints);
// 创建视频元素显示
const placeholder = document.getElementById('videoPlaceholder');
placeholder.innerHTML = `
<video autoplay muted playsinline style="width: 100%; height: auto;"></video>
<div style="font-size: 12px; color: #ccc; margin-top: 10px;">
正在使用: ${selectedDeviceInfo.label}
</div>
`;
const videoElement = placeholder.querySelector('video');
videoElement.srcObject = stream;
hideDeviceSelector();
log(`设备启动成功: ${selectedDeviceInfo.label}`, 'success');
} catch (error) {
let errorMsg = error.message;
if (error.name === 'NotAllowedError') {
errorMsg = '设备权限被拒绝,请允许访问摄像头';
} else if (error.name === 'NotFoundError') {
errorMsg = '设备未找到或已被占用';
}
log(`设备启动失败: ${errorMsg}`, 'error');
}
}
// 初始化
window.addEventListener('load', () => {
log('设备选择器测试页面已加载', 'success');
});
</script>
</body>
</html>

@ -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 = """
<html><head><meta charset="utf-8"><title>网络测试</title></head>
<body style="font-family: Arial; padding: 20px; background: #f0f0f0;">
<h1 style="color: green;"> 网络连接测试成功!</h1>
<p>如果您能看到这个页面说明网络连接正常</p>
<p><strong>测试时间:</strong> %s</p>
<p><strong>您的IP:</strong> %s</p>
<p><a href="/">刷新测试</a></p>
</body></html>
""" % (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()

@ -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()

@ -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()

@ -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}")

@ -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", "安装基础依赖包")
# 安装FlaskWeb功能所需
success2 = install_flask()
return success1 and success2
def install_flask():
"""安装Flask及其依赖"""
print("\n🔧 安装FlaskWeb功能支持...")
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()

@ -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()

@ -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-----

@ -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-----

Binary file not shown.
Loading…
Cancel
Save