diff --git a/.promptx/memory/declarative.md b/.promptx/memory/declarative.md index c906ec1c..a727daed 100644 --- a/.promptx/memory/declarative.md +++ b/.promptx/memory/declarative.md @@ -258,4 +258,34 @@ - 2025/07/03 15:12 START BattlefieldExplorationSystem项目样式管理系统优化完成:1)完全消除48个CSS3兼容性错误,修复transition、transform、box-shadow等不支持属性;2)建立双管理器架构(ModernStyleManager统一管理+LeftPanelStyleManager专用管理);3)统一样式文件到src/Client/styles/目录,清理旧的res/styles/目录;4)移除MainWindow内联样式,完全使用现代样式管理器;5)支持4种主题和演示模式,专为学术展示优化;6)创建完整文档体系(README.md、USAGE_GUIDE.md、StyleSystemStatus.md);7)实现Qt 5.15完全兼容,零错误稳定运行 --tags Qt样式管理 CSS3兼容性 ModernStyleManager 学术项目优化 界面美化 --tags #其他 #评分:8 #有效期:长期 +- END + +- 2025/07/08 08:09 START +用户展示了BattlefieldExplorationSystem主界面右侧功能区域截图,包含人脸识别、战场探索(无人机控制/机器狗控制)、情报传输(远程控制/SSH连接)、数据统计(查询/AI分析/导出报告)四大模块,准备进行布局优化工作 --tags 界面截图 右侧功能区 布局优化 四大模块 +--tags #其他 #评分:8 #有效期:长期 +- END + +- 2025/07/08 08:20 START +成功优化BattlefieldExplorationSystem"情报传输"模块:1)删除冗余"远程控制系统"子标题;2)修复按钮重叠问题,统一高度55px并限制最大高度;3)删除功能说明文字,简化界面;4)统一按钮样式(字体16px,内边距12px 16px,间距16px);5)移除Qt 5.15不支持的CSS3 transform属性;6)编译成功无错误,实现简洁专业的模块布局 --tags 情报传输模块优化 按钮布局修复 界面简化 Qt兼容性 编译成功 +--tags #其他 #评分:8 #有效期:长期 +- END + +- 2025/07/08 08:26 START +成功删除BattlefieldExplorationSystem主界面中的"人脸跟随"文字标签:1)定位到MainWindow.ui文件第817-862行的faceTracking按钮;2)完全删除QPushButton及相关QLabel和QHBoxLayout容器;3)验证MainWindow.cpp和.h文件中无相关功能代码,确保安全删除;4)编译成功无错误,不影响其他功能模块;5)实现界面简洁化,布局自然调整 --tags 人脸跟随删除 MainWindow.ui UI元素清理 界面简化 编译成功 +--tags #其他 #评分:8 #有效期:长期 +- END + +- 2025/07/08 08:35 START +成功删除BattlefieldExplorationSystem主界面中的"人脸识别"按钮:1)定位到MainWindow.ui文件第771-816行的faceRecognition按钮;2)完全删除QPushButton及相关QLabel和QHBoxLayout容器;3)验证MainWindow.cpp和.h文件中无相关功能代码,确保安全删除;4)编译成功无错误,不影响其他功能模块;5)实现界面简洁化,布局自然调整 --tags 人脸识别删除 MainWindow.ui UI元素清理 界面简化 编译成功 +--tags #其他 #评分:8 #有效期:长期 +- END + +- 2025/07/08 08:42 START +成功删除BattlefieldExplorationSystem右侧功能模块中的冗余按钮:1)删除"🧭 智能导航"和"🔊 情报传达"两个QPushButton;2)移除整个QGridLayout容器(第723-774行);3)清理MainWindow.cpp中所有相关代码引用(信号连接、方法实现、布局检查、样式应用);4)删除MainWindow.h中的方法声明;5)编译成功无错误,实现界面简洁化,保留右侧功能面板中的实际功能入口 --tags 冗余按钮删除 智能导航 情报传达 QGridLayout清理 代码引用清理 界面简化 编译成功 +--tags #其他 #评分:8 #有效期:长期 +- END + +- 2025/07/08 08:50 START +成功删除BattlefieldExplorationSystem主界面中的所有冗余主要功能按钮:1)删除"🚁 无人机视角"、"🐕 机器狗视角"、"🗺️ 机器狗建图"三个QPushButton;2)移除整个QVBoxLayout容器controlButtonsLayout(第580-725行);3)清理MainWindow.cpp中所有相关代码引用(信号连接、方法实现、布局修复、样式应用);4)删除MainWindow.h中的三个方法声明;5)编译成功无错误,实现界面极简化,功能由专门的RightFunctionPanel.cpp处理,架构更清晰 --tags 主要功能按钮删除 无人机视角 机器狗视角 机器狗建图 QVBoxLayout清理 代码引用清理 界面极简化 RightFunctionPanel 编译成功 +--tags #其他 #评分:8 #有效期:长期 - END \ No newline at end of file diff --git a/.promptx/pouch.json b/.promptx/pouch.json index 4c64fb77..6c813eb2 100644 --- a/.promptx/pouch.json +++ b/.promptx/pouch.json @@ -1,112 +1,6 @@ { - "currentState": "memory_saved", + "currentState": "role_activated_with_memory", "stateHistory": [ - { - "from": "memory_saved", - "command": "init", - "timestamp": "2025-07-01T00:06:01.444Z", - "args": [ - { - "workingDirectory": "/home/hzk/Software_Architecture" - } - ] - }, - { - "from": "initialized", - "command": "welcome", - "timestamp": "2025-07-01T00:06:20.379Z", - "args": [] - }, - { - "from": "role_discovery", - "command": "action", - "timestamp": "2025-07-01T00:07:11.585Z", - "args": [ - "nuwa" - ] - }, - { - "from": "role_activated_with_memory", - "command": "init", - "timestamp": "2025-07-01T00:14:14.861Z", - "args": [ - { - "workingDirectory": "/home/hzk/Software_Architecture" - } - ] - }, - { - "from": "initialized", - "command": "welcome", - "timestamp": "2025-07-01T00:14:20.084Z", - "args": [] - }, - { - "from": "role_discovery", - "command": "action", - "timestamp": "2025-07-01T00:14:38.053Z", - "args": [ - "nuwa" - ] - }, - { - "from": "role_activated_with_memory", - "command": "remember", - "timestamp": "2025-07-01T00:28:09.401Z", - "args": [ - "成功为用户创建了专业的\"课程项目总结助手\"(course-project-writer)角色,专门帮助写高质量的软件工程课程总结报告。\n\n## 角色设计特点\n- **专业专家模式**:结合学术写作和技术文档能力\n- **成绩导向策略**:针对评分标准优化内容\n- **全套支持文件**:包含思维模式、执行原则、知识体系的完整文件结构\n\n## 核心能力模块\n1. **学术写作思维**:结构化思维、证据支撑、价值体现\n2. **项目反思思维**:多维度分析、PDCA循环、成长轨迹梳理\n3. **学术报告写作流程**:3阶段标准流程,从材料准备到质量保证\n4. **成绩优化策略**:评分标准分析、差异化竞争、高分内容策划\n5. **软件工程教育知识**:课程理论、实践标准、技术趋势\n6. **技术文档写作规范**:表达技巧、格式标准、质量评估\n\n## 文件结构\n```\n.promptx/resource/domain/course-project-writer/\n├── course-project-writer.role.md (主角色文件)\n├── thought/\n│ ├── academic-writing.thought.md (学术写作思维)\n│ └── project-reflection.thought.md (项目反思思维)\n├── execution/\n│ ├── academic-report-writing.execution.md (学术报告写作流程)\n│ └── grade-optimization.execution.md (成绩优化策略)\n└── knowledge/\n ├── software-engineering-education.knowledge.md (软件工程教育)\n └── technical-documentation.knowledge.md (技术文档写作)\n```\n\n## 应用场景\n- 软件工程课程项目总结\n- 技术报告写作指导\n- 学术文档规范化\n- 项目经验梳理和总结\n\n这个角色特别适合需要写高质量课程总结、追求好成绩的学生使用。", - "--tags", - "角色创建 课程总结 学术写作 软件工程 成绩优化" - ] - }, - { - "from": "memory_saved", - "command": "init", - "timestamp": "2025-07-01T00:28:17.585Z", - "args": [ - { - "workingDirectory": "/home/hzk/Software_Architecture" - } - ] - }, - { - "from": "initialized", - "command": "action", - "timestamp": "2025-07-01T00:28:25.134Z", - "args": [ - "course-project-writer" - ] - }, - { - "from": "role_activated_with_memory", - "command": "remember", - "timestamp": "2025-07-01T00:31:07.310Z", - "args": [ - "用户确定了项目总结报告的暂定题目:\"从小米便签到战场环境探索系统——\"实践实践再实践\"\"。这个题目很有亮点:\n1. 体现了两个学期项目的完整学习轨迹(小米便签→战场探索系统)\n2. 用\"实践实践再实践\"作为副标题,呼应软件工程的核心理念\n3. 展现了从简单到复杂、从基础到高级的技术成长路径\n4. 符合软件工程课程\"理论+实践\"的教学特色\n\n这个题目为后续的报告写作提供了很好的主线框架。", - "--tags", - "项目总结 课程报告 题目确定 实践导向" - ] - }, - { - "from": "memory_saved", - "command": "remember", - "timestamp": "2025-07-01T00:40:25.427Z", - "args": [ - "用户提供了上学期软件工程课程总结和学长优秀范例。关键信息:\n\n**用户上学期总结特点:**\n- 核心理念:\"实践,实践,再实践\" \n- 技术提升:Git协同开发、Android软件开发、团队合作\n- 项目内容:小米便签代码阅读、标注、质量分析、功能维护\n- 挑战克服:项目部署困难、代码理解、文档撰写\n\n**学长优秀范例亮点:**\n- 情感表达更丰富(\"初次相遇\"等小标题)\n- 技术深度更广(UML建模、QT开发、开源软件重用)\n- 反思更深入(团队合作的具体问题和解决)\n- 结构更完整(从学习到成长到展望)\n- 引用名言增强感染力\n\n**对比差距分析:**\n用户总结偏重技术罗列,学长范例更注重思想深度和情感共鸣。这为用户本学期报告提升提供了明确方向。", - "--tags", - "上学期总结 学长范例 对比分析 提升方向" - ] - }, - { - "from": "memory_saved", - "command": "remember", - "timestamp": "2025-07-01T00:53:40.792Z", - "args": [ - "用户补充的关键信息(非常重要):\n\n**评分标准:** \n- 根据总结认真程度和具体事情评价\n- 不能明显使用AI(需要人工化表达)\n\n**技术具体提升:**\n- 软件工程综合能力提升(主要成长部分):需求设计→软件架构设计→UML绘图→大模型编码经验\n- 团队协作:4人小组(用户负责前端开发,2人机器狗开发,1人无人机开发)\n\n**\"实践实践再实践\"具体体现:**\n- 教员谆谆教诲,经常将此话挂在嘴边\n- 小组实际大量时间投入:文档撰写→UML绘画→编码实现→模块分工设计\n\n**报告要求:**\n- 图文并茂(需要指导配图方案)\n- 软件体系结构课程内容:QT界面前端开发\n\n**特色亮点:**\n- 大模型编码经验(创新点)\n- 完整的软件工程流程实践\n- 多技术栈团队协作(前端+机器狗+无人机)", - "--tags", - "评分标准 技术提升 团队协作 实践体现 报告要求" - ] - }, { "from": "memory_saved", "command": "remember", @@ -428,7 +322,115 @@ "--tags", "Qt调试 段错误修复 BattlefieldExplorationSystem 编译问题 信号槽 .pro文件" ] + }, + { + "from": "memory_saved", + "command": "init", + "timestamp": "2025-07-08T00:05:58.431Z", + "args": [ + { + "workingDirectory": "/home/hzk/Software_Architecture" + } + ] + }, + { + "from": "initialized", + "command": "welcome", + "timestamp": "2025-07-08T00:06:30.404Z", + "args": [] + }, + { + "from": "role_discovery", + "command": "action", + "timestamp": "2025-07-08T00:06:52.310Z", + "args": [ + "qt-ui-optimizer" + ] + }, + { + "from": "role_activated_with_memory", + "command": "remember", + "timestamp": "2025-07-08T00:09:18.117Z", + "args": [ + "用户展示了BattlefieldExplorationSystem主界面右侧功能区域截图,包含人脸识别、战场探索(无人机控制/机器狗控制)、情报传输(远程控制/SSH连接)、数据统计(查询/AI分析/导出报告)四大模块,准备进行布局优化工作", + "--tags", + "界面截图 右侧功能区 布局优化 四大模块" + ] + }, + { + "from": "memory_saved", + "command": "remember", + "timestamp": "2025-07-08T00:20:53.653Z", + "args": [ + "成功优化BattlefieldExplorationSystem\"情报传输\"模块:1)删除冗余\"远程控制系统\"子标题;2)修复按钮重叠问题,统一高度55px并限制最大高度;3)删除功能说明文字,简化界面;4)统一按钮样式(字体16px,内边距12px 16px,间距16px);5)移除Qt 5.15不支持的CSS3 transform属性;6)编译成功无错误,实现简洁专业的模块布局", + "--tags", + "情报传输模块优化 按钮布局修复 界面简化 Qt兼容性 编译成功" + ] + }, + { + "from": "memory_saved", + "command": "remember", + "timestamp": "2025-07-08T00:26:32.510Z", + "args": [ + "成功删除BattlefieldExplorationSystem主界面中的\"人脸跟随\"文字标签:1)定位到MainWindow.ui文件第817-862行的faceTracking按钮;2)完全删除QPushButton及相关QLabel和QHBoxLayout容器;3)验证MainWindow.cpp和.h文件中无相关功能代码,确保安全删除;4)编译成功无错误,不影响其他功能模块;5)实现界面简洁化,布局自然调整", + "--tags", + "人脸跟随删除 MainWindow.ui UI元素清理 界面简化 编译成功" + ] + }, + { + "from": "memory_saved", + "command": "remember", + "timestamp": "2025-07-08T00:35:14.073Z", + "args": [ + "成功删除BattlefieldExplorationSystem主界面中的\"人脸识别\"按钮:1)定位到MainWindow.ui文件第771-816行的faceRecognition按钮;2)完全删除QPushButton及相关QLabel和QHBoxLayout容器;3)验证MainWindow.cpp和.h文件中无相关功能代码,确保安全删除;4)编译成功无错误,不影响其他功能模块;5)实现界面简洁化,布局自然调整", + "--tags", + "人脸识别删除 MainWindow.ui UI元素清理 界面简化 编译成功" + ] + }, + { + "from": "memory_saved", + "command": "remember", + "timestamp": "2025-07-08T00:42:09.048Z", + "args": [ + "成功删除BattlefieldExplorationSystem右侧功能模块中的冗余按钮:1)删除\"🧭 智能导航\"和\"🔊 情报传达\"两个QPushButton;2)移除整个QGridLayout容器(第723-774行);3)清理MainWindow.cpp中所有相关代码引用(信号连接、方法实现、布局检查、样式应用);4)删除MainWindow.h中的方法声明;5)编译成功无错误,实现界面简洁化,保留右侧功能面板中的实际功能入口", + "--tags", + "冗余按钮删除 智能导航 情报传达 QGridLayout清理 代码引用清理 界面简化 编译成功" + ] + }, + { + "from": "memory_saved", + "command": "remember", + "timestamp": "2025-07-08T00:50:18.937Z", + "args": [ + "成功删除BattlefieldExplorationSystem主界面中的所有冗余主要功能按钮:1)删除\"🚁 无人机视角\"、\"🐕 机器狗视角\"、\"🗺️ 机器狗建图\"三个QPushButton;2)移除整个QVBoxLayout容器controlButtonsLayout(第580-725行);3)清理MainWindow.cpp中所有相关代码引用(信号连接、方法实现、布局修复、样式应用);4)删除MainWindow.h中的三个方法声明;5)编译成功无错误,实现界面极简化,功能由专门的RightFunctionPanel.cpp处理,架构更清晰", + "--tags", + "主要功能按钮删除 无人机视角 机器狗视角 机器狗建图 QVBoxLayout清理 代码引用清理 界面极简化 RightFunctionPanel 编译成功" + ] + }, + { + "from": "memory_saved", + "command": "init", + "timestamp": "2025-07-08T00:53:52.952Z", + "args": [ + { + "workingDirectory": "/home/hzk/Software_Architecture" + } + ] + }, + { + "from": "initialized", + "command": "welcome", + "timestamp": "2025-07-08T00:53:59.020Z", + "args": [] + }, + { + "from": "role_discovery", + "command": "action", + "timestamp": "2025-07-08T00:56:53.444Z", + "args": [ + "qt-ui-optimizer" + ] } ], - "lastUpdated": "2025-07-04T01:50:21.085Z" + "lastUpdated": "2025-07-08T00:56:53.449Z" } diff --git a/.promptx/resource/project.registry.json b/.promptx/resource/project.registry.json index 8160ccb4..9f6951b6 100644 --- a/.promptx/resource/project.registry.json +++ b/.promptx/resource/project.registry.json @@ -4,8 +4,8 @@ "metadata": { "version": "2.0.0", "description": "project 级资源注册表", - "createdAt": "2025-07-04T01:25:08.606Z", - "updatedAt": "2025-07-04T01:25:08.615Z", + "createdAt": "2025-07-08T00:53:52.954Z", + "updatedAt": "2025-07-08T00:53:52.958Z", "resourceCount": 40 }, "resources": [ @@ -17,9 +17,9 @@ "description": "专业角色,提供特定领域的专业能力", "reference": "@project://.promptx/resource/domain/course-project-writer/course-project-writer.role.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.607Z", - "updatedAt": "2025-07-04T01:25:08.607Z", - "scannedAt": "2025-07-04T01:25:08.607Z" + "createdAt": "2025-07-08T00:53:52.955Z", + "updatedAt": "2025-07-08T00:53:52.955Z", + "scannedAt": "2025-07-08T00:53:52.955Z" } }, { @@ -30,9 +30,9 @@ "description": "思维模式,指导AI的思考方式", "reference": "@project://.promptx/resource/domain/course-project-writer/thought/academic-writing.thought.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.607Z", - "updatedAt": "2025-07-04T01:25:08.607Z", - "scannedAt": "2025-07-04T01:25:08.607Z" + "createdAt": "2025-07-08T00:53:52.955Z", + "updatedAt": "2025-07-08T00:53:52.955Z", + "scannedAt": "2025-07-08T00:53:52.955Z" } }, { @@ -43,9 +43,9 @@ "description": "思维模式,指导AI的思考方式", "reference": "@project://.promptx/resource/domain/course-project-writer/thought/project-reflection.thought.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.608Z", - "updatedAt": "2025-07-04T01:25:08.608Z", - "scannedAt": "2025-07-04T01:25:08.608Z" + "createdAt": "2025-07-08T00:53:52.955Z", + "updatedAt": "2025-07-08T00:53:52.955Z", + "scannedAt": "2025-07-08T00:53:52.955Z" } }, { @@ -56,9 +56,9 @@ "description": "执行模式,定义具体的行为模式", "reference": "@project://.promptx/resource/domain/course-project-writer/execution/academic-report-writing.execution.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.608Z", - "updatedAt": "2025-07-04T01:25:08.608Z", - "scannedAt": "2025-07-04T01:25:08.608Z" + "createdAt": "2025-07-08T00:53:52.955Z", + "updatedAt": "2025-07-08T00:53:52.955Z", + "scannedAt": "2025-07-08T00:53:52.955Z" } }, { @@ -69,9 +69,9 @@ "description": "执行模式,定义具体的行为模式", "reference": "@project://.promptx/resource/domain/course-project-writer/execution/grade-optimization.execution.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.608Z", - "updatedAt": "2025-07-04T01:25:08.608Z", - "scannedAt": "2025-07-04T01:25:08.608Z" + "createdAt": "2025-07-08T00:53:52.955Z", + "updatedAt": "2025-07-08T00:53:52.955Z", + "scannedAt": "2025-07-08T00:53:52.955Z" } }, { @@ -82,9 +82,9 @@ "description": "知识库,提供专业知识和信息", "reference": "@project://.promptx/resource/domain/course-project-writer/knowledge/software-engineering-education.knowledge.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.608Z", - "updatedAt": "2025-07-04T01:25:08.608Z", - "scannedAt": "2025-07-04T01:25:08.608Z" + "createdAt": "2025-07-08T00:53:52.956Z", + "updatedAt": "2025-07-08T00:53:52.956Z", + "scannedAt": "2025-07-08T00:53:52.955Z" } }, { @@ -95,9 +95,9 @@ "description": "知识库,提供专业知识和信息", "reference": "@project://.promptx/resource/domain/course-project-writer/knowledge/technical-documentation.knowledge.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.608Z", - "updatedAt": "2025-07-04T01:25:08.608Z", - "scannedAt": "2025-07-04T01:25:08.608Z" + "createdAt": "2025-07-08T00:53:52.956Z", + "updatedAt": "2025-07-08T00:53:52.956Z", + "scannedAt": "2025-07-08T00:53:52.956Z" } }, { @@ -108,9 +108,9 @@ "description": "专业角色,提供特定领域的专业能力", "reference": "@project://.promptx/resource/domain/project-explainer/project-explainer.role.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.608Z", - "updatedAt": "2025-07-04T01:25:08.608Z", - "scannedAt": "2025-07-04T01:25:08.608Z" + "createdAt": "2025-07-08T00:53:52.956Z", + "updatedAt": "2025-07-08T00:53:52.956Z", + "scannedAt": "2025-07-08T00:53:52.956Z" } }, { @@ -121,9 +121,9 @@ "description": "思维模式,指导AI的思考方式", "reference": "@project://.promptx/resource/domain/project-explainer/thought/educational-guidance.thought.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.609Z", - "updatedAt": "2025-07-04T01:25:08.609Z", - "scannedAt": "2025-07-04T01:25:08.609Z" + "createdAt": "2025-07-08T00:53:52.956Z", + "updatedAt": "2025-07-08T00:53:52.956Z", + "scannedAt": "2025-07-08T00:53:52.956Z" } }, { @@ -134,9 +134,9 @@ "description": "思维模式,指导AI的思考方式", "reference": "@project://.promptx/resource/domain/project-explainer/thought/project-analysis.thought.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.609Z", - "updatedAt": "2025-07-04T01:25:08.609Z", - "scannedAt": "2025-07-04T01:25:08.609Z" + "createdAt": "2025-07-08T00:53:52.956Z", + "updatedAt": "2025-07-08T00:53:52.956Z", + "scannedAt": "2025-07-08T00:53:52.956Z" } }, { @@ -147,9 +147,9 @@ "description": "执行模式,定义具体的行为模式", "reference": "@project://.promptx/resource/domain/project-explainer/execution/academic-presentation.execution.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.609Z", - "updatedAt": "2025-07-04T01:25:08.609Z", - "scannedAt": "2025-07-04T01:25:08.609Z" + "createdAt": "2025-07-08T00:53:52.956Z", + "updatedAt": "2025-07-08T00:53:52.956Z", + "scannedAt": "2025-07-08T00:53:52.956Z" } }, { @@ -160,9 +160,9 @@ "description": "执行模式,定义具体的行为模式", "reference": "@project://.promptx/resource/domain/project-explainer/execution/project-explanation-workflow.execution.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.609Z", - "updatedAt": "2025-07-04T01:25:08.609Z", - "scannedAt": "2025-07-04T01:25:08.609Z" + "createdAt": "2025-07-08T00:53:52.956Z", + "updatedAt": "2025-07-08T00:53:52.956Z", + "scannedAt": "2025-07-08T00:53:52.956Z" } }, { @@ -173,9 +173,9 @@ "description": "知识库,提供专业知识和信息", "reference": "@project://.promptx/resource/domain/project-explainer/knowledge/academic-evaluation-standards.knowledge.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.609Z", - "updatedAt": "2025-07-04T01:25:08.609Z", - "scannedAt": "2025-07-04T01:25:08.609Z" + "createdAt": "2025-07-08T00:53:52.956Z", + "updatedAt": "2025-07-08T00:53:52.956Z", + "scannedAt": "2025-07-08T00:53:52.956Z" } }, { @@ -186,9 +186,9 @@ "description": "知识库,提供专业知识和信息", "reference": "@project://.promptx/resource/domain/project-explainer/knowledge/code-analysis-techniques.knowledge.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.609Z", - "updatedAt": "2025-07-04T01:25:08.609Z", - "scannedAt": "2025-07-04T01:25:08.609Z" + "createdAt": "2025-07-08T00:53:52.956Z", + "updatedAt": "2025-07-08T00:53:52.956Z", + "scannedAt": "2025-07-08T00:53:52.956Z" } }, { @@ -199,9 +199,9 @@ "description": "知识库,提供专业知识和信息", "reference": "@project://.promptx/resource/domain/project-explainer/knowledge/qt-architecture.knowledge.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.609Z", - "updatedAt": "2025-07-04T01:25:08.609Z", - "scannedAt": "2025-07-04T01:25:08.609Z" + "createdAt": "2025-07-08T00:53:52.956Z", + "updatedAt": "2025-07-08T00:53:52.956Z", + "scannedAt": "2025-07-08T00:53:52.956Z" } }, { @@ -212,9 +212,9 @@ "description": "专业角色,提供特定领域的专业能力", "reference": "@project://.promptx/resource/domain/project-poster-designer/project-poster-designer.role.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.609Z", - "updatedAt": "2025-07-04T01:25:08.609Z", - "scannedAt": "2025-07-04T01:25:08.609Z" + "createdAt": "2025-07-08T00:53:52.956Z", + "updatedAt": "2025-07-08T00:53:52.956Z", + "scannedAt": "2025-07-08T00:53:52.956Z" } }, { @@ -225,9 +225,9 @@ "description": "思维模式,指导AI的思考方式", "reference": "@project://.promptx/resource/domain/project-poster-designer/thought/creative-thinking.thought.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.610Z", - "updatedAt": "2025-07-04T01:25:08.610Z", - "scannedAt": "2025-07-04T01:25:08.610Z" + "createdAt": "2025-07-08T00:53:52.956Z", + "updatedAt": "2025-07-08T00:53:52.956Z", + "scannedAt": "2025-07-08T00:53:52.956Z" } }, { @@ -238,9 +238,9 @@ "description": "思维模式,指导AI的思考方式", "reference": "@project://.promptx/resource/domain/project-poster-designer/thought/visual-design.thought.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.610Z", - "updatedAt": "2025-07-04T01:25:08.610Z", - "scannedAt": "2025-07-04T01:25:08.610Z" + "createdAt": "2025-07-08T00:53:52.956Z", + "updatedAt": "2025-07-08T00:53:52.956Z", + "scannedAt": "2025-07-08T00:53:52.956Z" } }, { @@ -251,9 +251,9 @@ "description": "执行模式,定义具体的行为模式", "reference": "@project://.promptx/resource/domain/project-poster-designer/execution/poster-design-process.execution.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.610Z", - "updatedAt": "2025-07-04T01:25:08.610Z", - "scannedAt": "2025-07-04T01:25:08.610Z" + "createdAt": "2025-07-08T00:53:52.956Z", + "updatedAt": "2025-07-08T00:53:52.956Z", + "scannedAt": "2025-07-08T00:53:52.956Z" } }, { @@ -264,9 +264,9 @@ "description": "执行模式,定义具体的行为模式", "reference": "@project://.promptx/resource/domain/project-poster-designer/execution/visual-communication.execution.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.610Z", - "updatedAt": "2025-07-04T01:25:08.610Z", - "scannedAt": "2025-07-04T01:25:08.610Z" + "createdAt": "2025-07-08T00:53:52.956Z", + "updatedAt": "2025-07-08T00:53:52.956Z", + "scannedAt": "2025-07-08T00:53:52.956Z" } }, { @@ -277,9 +277,9 @@ "description": "知识库,提供专业知识和信息", "reference": "@project://.promptx/resource/domain/project-poster-designer/knowledge/graphic-design.knowledge.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.611Z", - "updatedAt": "2025-07-04T01:25:08.611Z", - "scannedAt": "2025-07-04T01:25:08.611Z" + "createdAt": "2025-07-08T00:53:52.957Z", + "updatedAt": "2025-07-08T00:53:52.957Z", + "scannedAt": "2025-07-08T00:53:52.957Z" } }, { @@ -290,9 +290,9 @@ "description": "知识库,提供专业知识和信息", "reference": "@project://.promptx/resource/domain/project-poster-designer/knowledge/military-tech-aesthetics.knowledge.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.611Z", - "updatedAt": "2025-07-04T01:25:08.611Z", - "scannedAt": "2025-07-04T01:25:08.611Z" + "createdAt": "2025-07-08T00:53:52.957Z", + "updatedAt": "2025-07-08T00:53:52.957Z", + "scannedAt": "2025-07-08T00:53:52.957Z" } }, { @@ -303,9 +303,9 @@ "description": "知识库,提供专业知识和信息", "reference": "@project://.promptx/resource/domain/project-poster-designer/knowledge/project-presentation.knowledge.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.611Z", - "updatedAt": "2025-07-04T01:25:08.611Z", - "scannedAt": "2025-07-04T01:25:08.611Z" + "createdAt": "2025-07-08T00:53:52.957Z", + "updatedAt": "2025-07-08T00:53:52.957Z", + "scannedAt": "2025-07-08T00:53:52.957Z" } }, { @@ -316,9 +316,9 @@ "description": "专业角色,提供特定领域的专业能力", "reference": "@project://.promptx/resource/domain/qt-code-optimizer/qt-code-optimizer.role.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.611Z", - "updatedAt": "2025-07-04T01:25:08.611Z", - "scannedAt": "2025-07-04T01:25:08.611Z" + "createdAt": "2025-07-08T00:53:52.957Z", + "updatedAt": "2025-07-08T00:53:52.957Z", + "scannedAt": "2025-07-08T00:53:52.957Z" } }, { @@ -329,9 +329,9 @@ "description": "思维模式,指导AI的思考方式", "reference": "@project://.promptx/resource/domain/qt-code-optimizer/thought/qt-code-analysis.thought.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.612Z", - "updatedAt": "2025-07-04T01:25:08.612Z", - "scannedAt": "2025-07-04T01:25:08.612Z" + "createdAt": "2025-07-08T00:53:52.957Z", + "updatedAt": "2025-07-08T00:53:52.957Z", + "scannedAt": "2025-07-08T00:53:52.957Z" } }, { @@ -342,9 +342,9 @@ "description": "思维模式,指导AI的思考方式", "reference": "@project://.promptx/resource/domain/qt-code-optimizer/thought/quality-assessment.thought.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.612Z", - "updatedAt": "2025-07-04T01:25:08.612Z", - "scannedAt": "2025-07-04T01:25:08.612Z" + "createdAt": "2025-07-08T00:53:52.957Z", + "updatedAt": "2025-07-08T00:53:52.957Z", + "scannedAt": "2025-07-08T00:53:52.957Z" } }, { @@ -355,9 +355,9 @@ "description": "执行模式,定义具体的行为模式", "reference": "@project://.promptx/resource/domain/qt-code-optimizer/execution/academic-standards.execution.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.612Z", - "updatedAt": "2025-07-04T01:25:08.612Z", - "scannedAt": "2025-07-04T01:25:08.612Z" + "createdAt": "2025-07-08T00:53:52.957Z", + "updatedAt": "2025-07-08T00:53:52.957Z", + "scannedAt": "2025-07-08T00:53:52.957Z" } }, { @@ -368,9 +368,9 @@ "description": "执行模式,定义具体的行为模式", "reference": "@project://.promptx/resource/domain/qt-code-optimizer/execution/qt-code-optimization.execution.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.612Z", - "updatedAt": "2025-07-04T01:25:08.612Z", - "scannedAt": "2025-07-04T01:25:08.612Z" + "createdAt": "2025-07-08T00:53:52.957Z", + "updatedAt": "2025-07-08T00:53:52.957Z", + "scannedAt": "2025-07-08T00:53:52.957Z" } }, { @@ -381,9 +381,9 @@ "description": "执行模式,定义具体的行为模式", "reference": "@project://.promptx/resource/domain/qt-code-optimizer/execution/quality-improvement.execution.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.612Z", - "updatedAt": "2025-07-04T01:25:08.612Z", - "scannedAt": "2025-07-04T01:25:08.612Z" + "createdAt": "2025-07-08T00:53:52.957Z", + "updatedAt": "2025-07-08T00:53:52.957Z", + "scannedAt": "2025-07-08T00:53:52.957Z" } }, { @@ -394,9 +394,9 @@ "description": "知识库,提供专业知识和信息", "reference": "@project://.promptx/resource/domain/qt-code-optimizer/knowledge/code-quality-standards.knowledge.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.613Z", - "updatedAt": "2025-07-04T01:25:08.613Z", - "scannedAt": "2025-07-04T01:25:08.613Z" + "createdAt": "2025-07-08T00:53:52.957Z", + "updatedAt": "2025-07-08T00:53:52.957Z", + "scannedAt": "2025-07-08T00:53:52.957Z" } }, { @@ -407,9 +407,9 @@ "description": "知识库,提供专业知识和信息", "reference": "@project://.promptx/resource/domain/qt-code-optimizer/knowledge/project-architecture.knowledge.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.613Z", - "updatedAt": "2025-07-04T01:25:08.613Z", - "scannedAt": "2025-07-04T01:25:08.613Z" + "createdAt": "2025-07-08T00:53:52.957Z", + "updatedAt": "2025-07-08T00:53:52.957Z", + "scannedAt": "2025-07-08T00:53:52.957Z" } }, { @@ -420,9 +420,9 @@ "description": "知识库,提供专业知识和信息", "reference": "@project://.promptx/resource/domain/qt-code-optimizer/knowledge/qt-cpp-expertise.knowledge.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.613Z", - "updatedAt": "2025-07-04T01:25:08.613Z", - "scannedAt": "2025-07-04T01:25:08.613Z" + "createdAt": "2025-07-08T00:53:52.957Z", + "updatedAt": "2025-07-08T00:53:52.957Z", + "scannedAt": "2025-07-08T00:53:52.957Z" } }, { @@ -433,9 +433,9 @@ "description": "专业角色,提供特定领域的专业能力", "reference": "@project://.promptx/resource/domain/qt-ui-optimizer/qt-ui-optimizer.role.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.613Z", - "updatedAt": "2025-07-04T01:25:08.613Z", - "scannedAt": "2025-07-04T01:25:08.613Z" + "createdAt": "2025-07-08T00:53:52.957Z", + "updatedAt": "2025-07-08T00:53:52.957Z", + "scannedAt": "2025-07-08T00:53:52.957Z" } }, { @@ -446,9 +446,9 @@ "description": "思维模式,指导AI的思考方式", "reference": "@project://.promptx/resource/domain/qt-ui-optimizer/thought/academic-standards-awareness.thought.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.614Z", - "updatedAt": "2025-07-04T01:25:08.614Z", - "scannedAt": "2025-07-04T01:25:08.614Z" + "createdAt": "2025-07-08T00:53:52.957Z", + "updatedAt": "2025-07-08T00:53:52.957Z", + "scannedAt": "2025-07-08T00:53:52.957Z" } }, { @@ -459,9 +459,9 @@ "description": "思维模式,指导AI的思考方式", "reference": "@project://.promptx/resource/domain/qt-ui-optimizer/thought/ui-design-thinking.thought.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.614Z", - "updatedAt": "2025-07-04T01:25:08.614Z", - "scannedAt": "2025-07-04T01:25:08.614Z" + "createdAt": "2025-07-08T00:53:52.957Z", + "updatedAt": "2025-07-08T00:53:52.957Z", + "scannedAt": "2025-07-08T00:53:52.957Z" } }, { @@ -472,9 +472,9 @@ "description": "执行模式,定义具体的行为模式", "reference": "@project://.promptx/resource/domain/qt-ui-optimizer/execution/academic-ui-standards.execution.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.614Z", - "updatedAt": "2025-07-04T01:25:08.614Z", - "scannedAt": "2025-07-04T01:25:08.614Z" + "createdAt": "2025-07-08T00:53:52.957Z", + "updatedAt": "2025-07-08T00:53:52.957Z", + "scannedAt": "2025-07-08T00:53:52.957Z" } }, { @@ -485,9 +485,9 @@ "description": "执行模式,定义具体的行为模式", "reference": "@project://.promptx/resource/domain/qt-ui-optimizer/execution/qt-optimization-workflow.execution.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.614Z", - "updatedAt": "2025-07-04T01:25:08.614Z", - "scannedAt": "2025-07-04T01:25:08.614Z" + "createdAt": "2025-07-08T00:53:52.957Z", + "updatedAt": "2025-07-08T00:53:52.957Z", + "scannedAt": "2025-07-08T00:53:52.957Z" } }, { @@ -498,9 +498,9 @@ "description": "知识库,提供专业知识和信息", "reference": "@project://.promptx/resource/domain/qt-ui-optimizer/knowledge/academic-project-standards.knowledge.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.615Z", - "updatedAt": "2025-07-04T01:25:08.615Z", - "scannedAt": "2025-07-04T01:25:08.615Z" + "createdAt": "2025-07-08T00:53:52.957Z", + "updatedAt": "2025-07-08T00:53:52.957Z", + "scannedAt": "2025-07-08T00:53:52.957Z" } }, { @@ -511,9 +511,9 @@ "description": "知识库,提供专业知识和信息", "reference": "@project://.promptx/resource/domain/qt-ui-optimizer/knowledge/qt-ui-development.knowledge.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.615Z", - "updatedAt": "2025-07-04T01:25:08.615Z", - "scannedAt": "2025-07-04T01:25:08.615Z" + "createdAt": "2025-07-08T00:53:52.957Z", + "updatedAt": "2025-07-08T00:53:52.957Z", + "scannedAt": "2025-07-08T00:53:52.957Z" } }, { @@ -524,9 +524,9 @@ "description": "知识库,提供专业知识和信息", "reference": "@project://.promptx/resource/domain/qt-ui-optimizer/knowledge/ui-ux-principles.knowledge.md", "metadata": { - "createdAt": "2025-07-04T01:25:08.615Z", - "updatedAt": "2025-07-04T01:25:08.615Z", - "scannedAt": "2025-07-04T01:25:08.615Z" + "createdAt": "2025-07-08T00:53:52.957Z", + "updatedAt": "2025-07-08T00:53:52.957Z", + "scannedAt": "2025-07-08T00:53:52.957Z" } } ], diff --git a/distance-judgement/.idea/.gitignore b/distance-judgement/.idea/.gitignore index f649f0f6..7ef297b7 100644 --- a/distance-judgement/.idea/.gitignore +++ b/distance-judgement/.idea/.gitignore @@ -1,8 +1,8 @@ -# Default ignored files -/shelf/ -/workspace.xml -# 基于编辑器的 HTTP 客户端请求 -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml +# Default ignored files +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/distance-judgement/.idea/modules.xml b/distance-judgement/.idea/modules.xml index b7e21735..82e85d0c 100644 --- a/distance-judgement/.idea/modules.xml +++ b/distance-judgement/.idea/modules.xml @@ -1,8 +1,8 @@ - - - - - - - + + + + + + + \ No newline at end of file diff --git a/distance-judgement/.idea/pythonProject2.iml b/distance-judgement/.idea/pythonProject2.iml index d0876a78..d9e6024f 100644 --- a/distance-judgement/.idea/pythonProject2.iml +++ b/distance-judgement/.idea/pythonProject2.iml @@ -1,8 +1,8 @@ - - - - - - - + + + + + + + \ No newline at end of file diff --git a/distance-judgement/CAMERA_ICON_OVERLAP_FIX.md b/distance-judgement/CAMERA_ICON_OVERLAP_FIX.md index 20f9893f..b50a2a67 100644 --- a/distance-judgement/CAMERA_ICON_OVERLAP_FIX.md +++ b/distance-judgement/CAMERA_ICON_OVERLAP_FIX.md @@ -1,137 +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. **添加数据缓存**:为点击事件提供设备数据支持 - -### 🎯 修复后的效果 -- **固定摄像头(电脑端)**:💻电脑图标 + 蓝色视野扇形 -- **移动设备(移动端)**:🚁无人机图标 + 朝向箭头 + 橙色视野扇形 - -## 总结 - +# 摄像头图标重叠问题修复报告 🔧 + +## 问题描述 + +在摄像头图标更新时,没有清除之前的图标,导致地图上出现图标重叠的现象。 + +## 问题根源分析 + +### 1. 固定摄像头视野扇形重叠 +- **问题位置**: `src/web_server.py` 第3730行附近 +- **原因**: 摄像头位置更新时,只更新了`cameraMarker`的位置,但没有同步更新`fixedCameraFOV`视野扇形 +- **表现**: 旧的视野扇形仍然显示在原位置,新的视野扇形在新位置,造成重叠 + +### 2. 移动设备朝向标记重叠 +- **问题位置**: `src/web_server.py` 第2491行附近 +- **原因**: 移动设备朝向更新时,`orientationMarker`是复合对象(包含`deviceMarker`和`viewSector`),但只简单调用了`map.remove()` +- **表现**: 设备标记和视野扇形没有被完全清除,导致重叠 + +### 3. 变量作用域问题 +- **问题位置**: `src/web_server.py` 第1647行 +- **原因**: `fixedCameraFOV`使用`const`声明,无法在其他函数中重新赋值 +- **影响**: 摄像头位置更新函数无法更新全局视野扇形引用 + +## 修复内容 + +### ✅ 修复1:自动配置时的视野扇形同步更新 +```javascript +// 🔧 修复:同步更新视野扇形位置,避免图标重叠 +if (fixedCameraFOV) { + // 移除旧的视野扇形 + map.remove(fixedCameraFOV); + + // 重新创建视野扇形在新位置 + const newFOV = createGeographicSector( + lng, lat, + result.data.camera_heading || config.CAMERA_HEADING, + config.CAMERA_FOV, + 100, // 100米检测范围 + '#2196F3' // 蓝色,与固定摄像头标记颜色匹配 + ); + map.add(newFOV); + + // 更新全局变量引用 + fixedCameraFOV = newFOV; +} +``` + +### ✅ 修复2:手动配置时的视野扇形同步更新 +```javascript +// 🔧 修复:手动配置时也要同步更新视野扇形 +// 同步更新视野扇形 +if (fixedCameraFOV) { + map.remove(fixedCameraFOV); + + const newFOV = createGeographicSector( + lng, lat, heading, config.CAMERA_FOV, + 100, '#2196F3' + ); + map.add(newFOV); + fixedCameraFOV = newFOV; +} +``` + +### ✅ 修复3:移动设备朝向标记的正确清除 +```javascript +// 🔧 修复:正确移除旧的视野扇形标记,避免重叠 +if (mobileDeviceMarkers[deviceId].orientationMarker) { + // orientationMarker是一个复合对象,包含deviceMarker和viewSector + const oldOrientation = mobileDeviceMarkers[deviceId].orientationMarker; + if (oldOrientation.deviceMarker) { + map.remove(oldOrientation.deviceMarker); + } + if (oldOrientation.viewSector) { + map.remove(oldOrientation.viewSector); + } +} +``` + +### ✅ 修复4:变量作用域调整 +```javascript +// 将 const 改为 var,允许重新赋值 +var fixedCameraFOV = createGeographicSector(...); +``` + +## 测试验证 + +修复后,以下操作不再出现图标重叠: + +1. **自动配置摄像头位置** - 视野扇形会同步移动到新位置 +2. **手动配置摄像头位置** - 视野扇形会同步更新位置和朝向 +3. **移动设备朝向更新** - 旧的设备标记和视野扇形会被完全清除 +4. **摄像头朝向变更** - 视野扇形会反映新的朝向角度 + +## 影响范围 + +✅ **已修复的功能**: +- 固定摄像头位置更新 +- 固定摄像头朝向更新 +- 移动设备位置更新 +- 移动设备朝向更新 +- 手动配置摄像头 + +✅ **无影响的功能**: +- 人员检测标记更新(原本就有正确的清除逻辑) +- 远程设备标记更新(原本就有正确的清除逻辑) +- 其他地图功能 + +## 技术细节 + +- **修改文件**: `src/web_server.py` +- **修改行数**: 约15行代码修改 +- **兼容性**: 完全向后兼容,不影响现有功能 +- **性能影响**: 无负面影响,实际上减少了地图上的冗余元素 + +## 📝 补充修复:重复无人机图标问题 + +### 问题描述 +用户反映地图上出现了2个无人机图标,但应该只有1个无人机图标和1个电脑图标。 + +### 根源分析 +移动设备同时显示了两个独立的🚁标记: +- `locationMarker`:GPS位置标记 +- `orientationMarker`:朝向标记(包含视野扇形) + +### ✅ 修复方案 +1. **移除重复的位置标记**:删除独立的`locationMarker` +2. **合并功能到朝向标记**:朝向标记同时承担位置和朝向显示 +3. **更新清除逻辑**:移除对`locationMarker`的引用 +4. **添加数据缓存**:为点击事件提供设备数据支持 + +### 🎯 修复后的效果 +- **固定摄像头(电脑端)**:💻电脑图标 + 蓝色视野扇形 +- **移动设备(移动端)**:🚁无人机图标 + 朝向箭头 + 橙色视野扇形 + +## 总结 + 通过这次修复,彻底解决了摄像头图标重叠的问题,确保地图上的标记状态与实际配置始终保持一致,提升了用户体验。同时解决了重复无人机图标的问题,让图标显示更加清晰和直观。 \ No newline at end of file diff --git a/distance-judgement/CAMERA_ORIENTATION_GUIDE.md b/distance-judgement/CAMERA_ORIENTATION_GUIDE.md index 4c628189..32af8183 100644 --- a/distance-judgement/CAMERA_ORIENTATION_GUIDE.md +++ b/distance-judgement/CAMERA_ORIENTATION_GUIDE.md @@ -1,236 +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配置指南 - ---- - +# 摄像头朝向自动配置功能指南 🧭 + +## 功能概述 + +本系统现在支持自动获取设备位置和朝向,将本地摄像头设置为面朝使用者,实现智能的摄像头配置。 + +## 🎯 主要功能 + +### 1. 自动GPS定位 +- **Windows系统**: 使用Windows Location API获取精确GPS位置 +- **其他系统**: 使用IP地理定位作为备选方案 +- **精度**: GPS可达10米内,IP定位约10公里 + +### 2. 设备朝向检测 +- **桌面设备**: 使用默认朝向算法(假设用户面向屏幕) +- **移动设备**: 支持陀螺仪和磁力计朝向检测 +- **智能计算**: 自动计算摄像头应该面向用户的角度 + +### 3. 自动配置应用 +- **实时更新**: 自动更新配置文件和运行时参数 +- **地图同步**: 自动更新地图上的摄像头位置标记 +- **即时生效**: 配置立即应用到距离计算和人员定位 + +## 🚀 使用方法 + +### 方法一:启动时自动配置 + +```bash +python main_web.py +``` + +系统会检测到默认配置并询问是否自动配置: +``` +🤖 检测到摄像头使用默认配置 + 是否要自动配置摄像头位置和朝向? + • 输入 'y' - 立即自动配置 + • 输入 'n' - 跳过,使用Web界面配置 + • 直接回车 - 跳过自动配置 + +🔧 请选择 (y/n/回车): y +``` + +### 方法二:独立配置工具 + +```bash +# 完整自动配置 +python tools/auto_configure_camera.py + +# 仅测试GPS功能 +python tools/auto_configure_camera.py --test-gps + +# 仅测试朝向功能 +python tools/auto_configure_camera.py --test-heading +``` + +### 方法三:Web界面配置 + +1. 启动Web服务器:`python main_web.py` +2. 打开浏览器访问 `https://127.0.0.1:5000` +3. 在"🧭 自动位置配置"面板中: + - 点击"📍 获取位置"按钮 + - 点击"🧭 获取朝向"按钮 + - 点击"🤖 自动配置摄像头"按钮 + +## 📱 Web界面功能详解 + +### GPS位置获取 +```javascript +// 使用浏览器Geolocation API +navigator.geolocation.getCurrentPosition() +``` + +**支持的浏览器**: +- ✅ Chrome/Edge (推荐) +- ✅ Firefox +- ✅ Safari +- ❌ IE (不支持) + +**权限要求**: +- 首次使用需要授权位置权限 +- HTTPS环境下精度更高 +- 室外环境GPS信号更好 + +### 设备朝向检测 +```javascript +// 使用设备朝向API +window.addEventListener('deviceorientation', handleOrientation) +``` + +**支持情况**: +- 📱 **移动设备**: 完全支持(手机、平板) +- 💻 **桌面设备**: 有限支持(使用算法估算) +- 🍎 **iOS 13+**: 需要明确请求权限 + +## ⚙️ 技术实现 + +### 后端模块 + +#### 1. OrientationDetector (`src/orientation_detector.py`) +- GPS位置获取(多平台支持) +- 设备朝向检测 +- 摄像头朝向计算 +- 配置文件更新 + +#### 2. WebOrientationDetector (`src/web_orientation_detector.py`) +- Web API接口 +- 前后端数据同步 +- 实时状态管理 + +### 前端功能 + +#### JavaScript函数 +- `requestGPSPermission()` - GPS权限请求 +- `requestOrientationPermission()` - 朝向权限请求 +- `autoConfigureCamera()` - 自动配置执行 +- `manualConfiguration()` - 手动配置入口 + +#### API接口 +- `POST /api/orientation/auto_configure` - 自动配置 +- `POST /api/orientation/update_location` - 更新GPS +- `POST /api/orientation/update_heading` - 更新朝向 +- `GET /api/orientation/get_status` - 获取状态 + +## 🔧 配置原理 + +### 朝向计算逻辑 + +```python +def calculate_camera_heading_facing_user(self, user_heading: float) -> float: + """ + 计算摄像头朝向用户的角度 + 摄像头朝向 = (用户朝向 + 180°) % 360° + """ + camera_heading = (user_heading + 180) % 360 + return camera_heading +``` + +### 坐标转换 + +```python +def calculate_person_position(self, pixel_x, pixel_y, distance, frame_width, frame_height): + """ + 基于摄像头位置、朝向和距离计算人员GPS坐标 + 使用球面几何学进行精确计算 + """ + # 像素到角度转换 + horizontal_angle_per_pixel = self.camera_fov / frame_width + horizontal_offset_degrees = (pixel_x - center_x) * horizontal_angle_per_pixel + + # 计算实际方位角 + person_bearing = (self.camera_heading + horizontal_offset_degrees) % 360 + + # 球面坐标计算 + person_lat, person_lng = self._calculate_destination_point( + self.camera_lat, self.camera_lng, distance, person_bearing + ) +``` + +## 📋 系统要求 + +### 环境要求 +- Python 3.7+ +- 现代Web浏览器 +- 网络连接(GPS定位需要) + +### Windows特别要求 +```bash +# 安装Windows位置服务支持 +pip install winrt-runtime winrt-Windows.Devices.Geolocation +``` + +### 移动设备要求 +- HTTPS访问(GPS权限要求) +- 现代移动浏览器 +- 设备朝向传感器支持 + +## 🔍 故障排除 + +### GPS获取失败 +**常见原因**: +- 位置权限被拒绝 +- 网络连接问题 +- GPS信号不佳 + +**解决方案**: +1. 检查浏览器位置权限设置 +2. 移动到室外或窗边 +3. 使用IP定位作为备选 +4. 手动输入坐标 + +### 朝向检测失败 +**常见原因**: +- 设备不支持朝向传感器 +- 浏览器兼容性问题 +- 权限被拒绝 + +**解决方案**: +1. 使用支持的移动设备 +2. 更新到现代浏览器 +3. 允许设备朝向权限 +4. 使用手动配置 + +### 配置不生效 +**可能原因**: +- 配置文件写入失败 +- 权限不足 +- 模块导入错误 + +**解决方案**: +1. 检查文件写入权限 +2. 重启应用程序 +3. 查看控制台错误信息 + +## 💡 使用建议 + +### 最佳实践 +1. **首次配置**: 使用Web界面进行配置,可视化效果更好 +2. **定期更新**: 位置变化时重新配置 +3. **精度要求**: GPS环境下精度更高,室内可用IP定位 +4. **设备选择**: 移动设备朝向检测更准确 + +### 注意事项 +1. **隐私保护**: GPS数据仅用于本地配置,不会上传 +2. **网络要求**: 初次配置需要网络连接 +3. **兼容性**: 老旧浏览器可能不支持某些功能 +4. **精度限制**: 桌面设备朝向检测精度有限 + +## 📚 相关文档 + +- [MAP_USAGE_GUIDE.md](MAP_USAGE_GUIDE.md) - 地图功能使用指南 +- [MOBILE_GUIDE.md](MOBILE_GUIDE.md) - 移动端使用指南 +- [HTTPS_SETUP.md](HTTPS_SETUP.md) - HTTPS配置指南 + +--- + 🎯 **快速开始**: 运行 `python main_web.py`,选择自动配置,享受智能的摄像头定位体验! \ No newline at end of file diff --git a/distance-judgement/HTTPS_SETUP.md b/distance-judgement/HTTPS_SETUP.md index 9eca3614..46d6b7f4 100644 --- a/distance-judgement/HTTPS_SETUP.md +++ b/distance-judgement/HTTPS_SETUP.md @@ -1,99 +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设置指南 + +## 概述 +本系统已升级支持HTTPS,解决摄像头权限问题。现代浏览器要求HTTPS才能访问摄像头等敏感设备。 + +## 🚀 快速启动 + +### 方法一:自动设置(推荐) +1. 在PyCharm中打开项目 +2. 直接运行 `main_web.py` +3. 系统会自动生成SSL证书并启动HTTPS服务器 + +### 方法二:手动安装依赖 +如果遇到cryptography库缺失: +```bash +pip install cryptography +``` + +## 📱 访问地址 + +启动后访问地址已升级为HTTPS: +- **本地访问**: https://127.0.0.1:5000 +- **手机访问**: https://你的IP:5000/mobile/mobile_client.html + +## 🔑 浏览器安全警告处理 + +### 桌面浏览器 +1. 访问 https://127.0.0.1:5000 +2. 看到"您的连接不是私密连接"警告 +3. 点击 **"高级"** +4. 点击 **"继续访问localhost(不安全)"** +5. 正常使用 + +### 手机浏览器 +1. 访问 https://你的IP:5000/mobile/mobile_client.html +2. 出现安全警告时,点击 **"高级"** 或 **"详细信息"** +3. 选择 **"继续访问"** 或 **"继续前往此网站"** +4. 正常使用摄像头功能 + +## 📂 文件结构 + +新增文件: +``` +ssl/ +├── cert.pem # SSL证书文件 +└── key.pem # 私钥文件 +``` + +## 🔧 技术说明 + +### SSL证书特性 +- **类型**: 自签名证书 +- **有效期**: 365天 +- **支持域名**: localhost, 127.0.0.1 +- **算法**: RSA-2048, SHA-256 + +### 摄像头权限要求 +- ✅ HTTPS环境 - 支持摄像头访问 +- ❌ HTTP环境 - 浏览器阻止摄像头访问 +- ⚠️ localhost - HTTP也可以,但IP访问必须HTTPS + +## 🐛 故障排除 + +### 问题1: cryptography库安装失败 +```bash +# Windows +pip install --upgrade pip +pip install cryptography + +# 如果还是失败,尝试: +pip install --only-binary=cryptography cryptography +``` + +### 问题2: 证书生成失败 +1. 检查ssl目录权限 +2. 重新运行程序,会自动重新生成 + +### 问题3: 手机无法访问 +1. 确保手机和电脑在同一网络 +2. 检查防火墙设置 +3. 在手机浏览器中接受安全证书 + +### 问题4: 摄像头仍然无法访问 +1. 确认使用HTTPS访问 +2. 检查浏览器摄像头权限设置 +3. 尝试不同浏览器(Chrome、Firefox等) + +## 📋 更新日志 + +### v2.0 - HTTPS升级 +- ✅ 自动SSL证书生成 +- ✅ 完整HTTPS支持 +- ✅ 摄像头权限兼容 +- ✅ 手机端HTTPS支持 +- ✅ 浏览器安全警告处理指南 + +## 🎯 下一步 + 完成HTTPS升级后,您的移动端摄像头功能将完全正常工作,不再受到浏览器安全限制的影响。 \ No newline at end of file diff --git a/distance-judgement/MAP_USAGE_GUIDE.md b/distance-judgement/MAP_USAGE_GUIDE.md index bce1849f..f1da6b36 100644 --- a/distance-judgement/MAP_USAGE_GUIDE.md +++ b/distance-judgement/MAP_USAGE_GUIDE.md @@ -1,165 +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 - 优化坐标计算精度 +# 地图功能使用指南 🗺️ + +## 功能概述 + +本系统集成了高德地图API,可以实时在地图上显示: +- 📷 摄像头位置(蓝色标记) +- 👥 检测到的人员位置(红色标记) +- 📏 每个人员距离摄像头的距离 + +## 快速开始 + +### 1. 配置摄像头位置 📍 + +首先需要设置摄像头的地理位置: + +```bash +python setup_camera_location.py +``` + +按提示输入: +- 摄像头纬度(例:39.9042) +- 摄像头经度(例:116.4074) +- 摄像头朝向角度(0-360°,0为正北) +- 高德API Key(可选,用于更好的地图体验) + +### 2. 启动系统 🚀 + +```bash +python main.py +``` + +### 3. 查看地图 🗺️ + +在检测界面按 `m` 键打开地图,系统会自动在浏览器中显示实时地图。 + +## 操作说明 + +### 键盘快捷键 + +- `q` - 退出程序 +- `c` - 距离校准模式 +- `r` - 重置为默认参数 +- `s` - 保存当前帧截图 +- `m` - 打开地图显示 🗺️ +- `h` - 设置摄像头朝向 🧭 + +### 地图界面说明 + +- 🔵 **蓝色标记** - 摄像头位置 +- 🔴 **红色标记** - 检测到的人员位置 +- 📊 **信息面板** - 显示系统状态和统计信息 +- ⚡ **实时更新** - 地图每3秒自动刷新一次 + +## 坐标计算原理 + +系统通过以下步骤计算人员的地理坐标: + +1. **像素坐标获取** - 从YOLO检测结果获取人体在画面中的位置 +2. **角度计算** - 根据摄像头视场角计算人相对于摄像头中心的角度偏移 +3. **方位角计算** - 结合摄像头朝向,计算人相对于正北的绝对角度 +4. **地理坐标转换** - 使用球面几何学公式,根据距离和角度计算地理坐标 + +### 关键参数 + +- `CAMERA_FOV` - 摄像头视场角(默认60°) +- `CAMERA_HEADING` - 摄像头朝向角度(0°为正北) +- 距离计算基于已校准的距离测量算法 + +## 高德地图API配置 + +### 获取API Key + +1. 访问 [高德开放平台](https://lbs.amap.com/) +2. 注册并创建应用 +3. 获取Web服务API Key +4. 在配置中替换 `your_gaode_api_key_here` + +### API使用限制 + +- 免费配额:每日10万次调用 +- 超出配额后可能影响地图加载 +- 建议使用自己的API Key以确保稳定服务 + +## 精度优化建议 + +### 距离校准 📏 + +使用 `c` 键进入校准模式: +1. 让一个人站在已知距离处 +2. 输入实际距离 +3. 系统自动调整计算参数 + +### 朝向校准 🧭 + +使用 `h` 键设置准确朝向: +1. 确定摄像头实际朝向(使用指南针) +2. 输入角度(0°为正北,90°为正东) + +### 位置校准 📍 + +确保摄像头GPS坐标准确: +1. 使用手机GPS应用获取精确坐标 +2. 运行 `setup_camera_location.py` 更新配置 + +## 故障排除 + +### 地图无法打开 + +1. 检查网络连接 +2. 确认高德API Key配置正确 +3. 尝试手动访问生成的HTML文件 + +### 人员位置不准确 + +1. 重新校准距离参数 +2. 检查摄像头朝向设置 +3. 确认摄像头GPS坐标准确 + +### 地图显示异常 + +1. 刷新浏览器页面 +2. 清除浏览器缓存 +3. 检查JavaScript控制台错误信息 + +## 技术细节 + +### 坐标转换公式 + +系统使用WGS84坐标系和球面几何学公式: + +```python +# 球面距离计算 +lat2 = asin(sin(lat1) * cos(d/R) + cos(lat1) * sin(d/R) * cos(bearing)) +lng2 = lng1 + atan2(sin(bearing) * sin(d/R) * cos(lat1), cos(d/R) - sin(lat1) * sin(lat2)) +``` + +### 视场角映射 + +```python +# 像素到角度的转换 +horizontal_angle_per_pixel = camera_fov / frame_width +horizontal_offset = (pixel_x - center_x) * horizontal_angle_per_pixel +``` + +## 系统要求 + +- Python 3.7+ +- OpenCV 4.0+ +- 网络连接(地图加载) +- 现代浏览器(Chrome/Firefox/Edge) + +## 注意事项 + +⚠️ **重要提醒**: +- 本系统仅供技术研究使用 +- 实际部署需要考虑隐私保护 +- GPS坐标精度影响最终定位准确性 +- 距离计算基于单目视觉,存在一定误差 + +## 更新日志 + +- v1.0.0 - 基础地图显示功能 +- v1.1.0 - 添加实时人员位置标记 +- v1.2.0 - 优化坐标计算精度 - v1.3.0 - 增加配置工具和用户指南 \ No newline at end of file diff --git a/distance-judgement/MOBILE_GUIDE.md b/distance-judgement/MOBILE_GUIDE.md index ec650d7c..9ff1baa9 100644 --- a/distance-judgement/MOBILE_GUIDE.md +++ b/distance-judgement/MOBILE_GUIDE.md @@ -1,246 +1,246 @@ -# 📱 手机连接功能使用指南 - -## 🚁 无人机战场态势感知系统 - 手机扩展功能 - -这个功能允许你使用手机作为移动侦察设备,将手机摄像头图像、GPS位置和设备信息实时传输到指挥中心,扩展战场态势感知能力。 - -## 🌟 功能特性 - -### 📡 数据传输 -- **实时视频流**: 传输手机摄像头画面到指挥中心 -- **GPS定位**: 自动获取和传输手机的精确位置 -- **设备状态**: 监控电池电量、信号强度等 -- **人体检测**: 在手机端进行AI人体检测 -- **地图集成**: 检测结果自动显示在指挥中心地图上 - -### 🛡️ 技术特点 -- **低延迟传输**: 优化的数据压缩和传输协议 -- **自动重连**: 网络中断后自动重新连接 -- **多设备支持**: 支持多台手机同时连接 -- **跨平台兼容**: 支持Android、iOS等主流移动设备 - -## 🚀 快速开始 - -### 1. 启动服务端 - -#### 方法一:使用Web模式(推荐) -```bash -python run.py -# 选择 "1. Web模式" -# 在Web界面中点击"启用手机模式" -``` - -#### 方法二:直接启动Web服务器 -```bash -python main_web.py -``` - -### 2. 配置网络连接 - -确保手机和电脑在同一网络环境下: -- **局域网连接**: 连接同一WiFi网络 -- **热点模式**: 电脑开启热点,手机连接 -- **有线网络**: 电脑有线连接,手机连WiFi - -### 3. 获取服务器IP地址 - -在电脑上查看IP地址: - -**Windows:** -```cmd -ipconfig -``` - -**Linux/Mac:** -```bash -ifconfig -# 或 -ip addr show -``` - -记下显示的IP地址(如 192.168.1.100) - -### 4. 手机端连接 - -#### 方法一:使用浏览器(推荐) -1. 打开手机浏览器 -2. 访问 `http://[服务器IP]:5000/mobile/mobile_client.html` -3. 例如:`http://192.168.1.100:5000/mobile/mobile_client.html` - -#### 方法二:直接访问HTML文件 -1. 将 `mobile/mobile_client.html` 复制到手机 -2. 在文件中修改服务器IP地址 -3. 用浏览器打开HTML文件 - -### 5. 开始传输 -1. 在手机页面中点击"开始传输" -2. 允许摄像头和位置权限 -3. 查看连接状态指示灯变绿 -4. 在指挥中心Web界面查看实时数据 - -## 📱 手机端界面说明 - -### 状态面板 -- **📍 GPS坐标**: 显示当前精确位置 -- **🔋 电池电量**: 实时电池状态 -- **🌐 连接状态**: 与服务器的连接状态 - -### 控制按钮 -- **📹 开始传输**: 启动数据传输 -- **⏹️ 停止传输**: 停止传输 -- **🔄 重连**: 重新连接服务器 - -### 统计信息 -- **📊 已发送帧数**: 传输的图像帧数量 -- **📈 数据量**: 累计传输的数据量 - -### 日志面板 -- 显示详细的操作日志和错误信息 -- 帮助诊断连接问题 - -## 🖥️ 服务端管理 - -### Web界面控制 -访问 `http://localhost:5000` 查看: -- **地图显示**: 实时显示手机位置和检测结果 -- **设备管理**: 查看连接的手机列表 -- **数据统计**: 查看传输统计信息 - -### API接口 -- `GET /api/mobile/devices` - 获取连接设备列表 -- `POST /api/mobile/toggle` - 切换手机模式开关 -- `POST /mobile/ping` - 手机连接测试 -- `POST /mobile/upload` - 接收手机数据 - -### 命令行监控 -服务器控制台会显示详细日志: -``` -📱 新设备连接: iPhone (mobile_12) -📍 设备 mobile_12 位置更新: (39.904200, 116.407400) -🎯 检测到 2 个人 -📍 手机检测人员 1: 距离5.2m, 坐标(39.904250, 116.407450) -``` - -## ⚙️ 高级配置 - -### 修改传输参数 - -在手机端HTML文件中可以调整: - -```javascript -// 修改服务器地址 -this.serverHost = '192.168.1.100'; -this.serverPort = 5000; - -// 修改传输频率(毫秒) -const interval = 1000; // 1秒传输一次 - -// 修改图像质量(0.1-1.0) -const frameData = this.canvas.toDataURL('image/jpeg', 0.5); -``` - -### 网络优化 - -**低带宽环境:** -- 降低图像质量 (0.3-0.5) -- 增加传输间隔 (2-5秒) -- 减小图像分辨率 - -**高质量需求:** -- 提高图像质量 (0.7-0.9) -- 减少传输间隔 (0.5-1秒) -- 使用更高分辨率 - -## 🔧 故障排除 - -### 常见问题 - -#### 1. 手机无法连接服务器 -- **检查网络**: 确保在同一网络 -- **检查IP地址**: 确认服务器IP正确 -- **检查防火墙**: 关闭防火墙或开放端口 -- **检查端口**: 确认5000端口未被占用 - -#### 2. 摄像头无法访问 -- **权限设置**: 在浏览器中允许摄像头权限 -- **HTTPS需求**: 某些浏览器需要HTTPS才能访问摄像头 -- **设备占用**: 关闭其他使用摄像头的应用 - -#### 3. GPS定位失败 -- **位置权限**: 允许浏览器访问位置信息 -- **网络连接**: 确保网络连接正常 -- **室内环境**: 移动到有GPS信号的位置 - -#### 4. 传输断开 -- **网络稳定性**: 检查WiFi信号强度 -- **服务器状态**: 确认服务器正常运行 -- **自动重连**: 等待自动重连或手动重连 - -### 调试方法 - -#### 手机端调试 -1. 打开浏览器开发者工具 (F12) -2. 查看Console面板的错误信息 -3. 检查Network面板的网络请求 - -#### 服务端调试 -1. 查看控制台输出的日志信息 -2. 使用 `python tests/test_system.py` 测试系统 -3. 检查网络连接和端口状态 - -## 🌐 网络配置示例 - -### 局域网配置 -``` -电脑 (192.168.1.100) ←→ 路由器 ←→ 手机 (192.168.1.101) -``` - -### 热点配置 -``` -电脑热点 (192.168.137.1) ←→ 手机 (192.168.137.2) -``` - -### 有线+WiFi配置 -``` -电脑 (有线: 192.168.1.100) ←→ 路由器 ←→ 手机 (WiFi: 192.168.1.101) -``` - -## 📊 性能建议 - -### 推荐配置 -- **网络**: WiFi 5GHz频段,带宽 ≥ 10Mbps -- **手机**: RAM ≥ 4GB,Android 8+ / iOS 12+ -- **服务器**: 双核CPU,RAM ≥ 4GB - -### 优化设置 -- **高质量模式**: 0.7质量,1秒间隔 -- **平衡模式**: 0.5质量,1秒间隔(推荐) -- **省流量模式**: 0.3质量,2秒间隔 - -## 🚁 实战应用场景 - -### 军用场景 -- **前线侦察**: 士兵携带手机进行前方侦察 -- **多点监控**: 多个观察点同时传输情报 -- **指挥决策**: 指挥部实时获取战场态势 - -### 民用场景 -- **安保监控**: 保安巡逻时实时传输画面 -- **应急救援**: 救援人员现场情况汇报 -- **活动监管**: 大型活动现场监控 - -### 技术演示 -- **远程教学**: 实地教学直播 -- **技术展示**: 产品演示和技术验证 - ---- - -## 📞 技术支持 - -如有问题,请: -1. 查看控制台日志信息 -2. 运行系统测试脚本 -3. 检查网络配置 -4. 参考故障排除指南 - +# 📱 手机连接功能使用指南 + +## 🚁 无人机战场态势感知系统 - 手机扩展功能 + +这个功能允许你使用手机作为移动侦察设备,将手机摄像头图像、GPS位置和设备信息实时传输到指挥中心,扩展战场态势感知能力。 + +## 🌟 功能特性 + +### 📡 数据传输 +- **实时视频流**: 传输手机摄像头画面到指挥中心 +- **GPS定位**: 自动获取和传输手机的精确位置 +- **设备状态**: 监控电池电量、信号强度等 +- **人体检测**: 在手机端进行AI人体检测 +- **地图集成**: 检测结果自动显示在指挥中心地图上 + +### 🛡️ 技术特点 +- **低延迟传输**: 优化的数据压缩和传输协议 +- **自动重连**: 网络中断后自动重新连接 +- **多设备支持**: 支持多台手机同时连接 +- **跨平台兼容**: 支持Android、iOS等主流移动设备 + +## 🚀 快速开始 + +### 1. 启动服务端 + +#### 方法一:使用Web模式(推荐) +```bash +python run.py +# 选择 "1. Web模式" +# 在Web界面中点击"启用手机模式" +``` + +#### 方法二:直接启动Web服务器 +```bash +python main_web.py +``` + +### 2. 配置网络连接 + +确保手机和电脑在同一网络环境下: +- **局域网连接**: 连接同一WiFi网络 +- **热点模式**: 电脑开启热点,手机连接 +- **有线网络**: 电脑有线连接,手机连WiFi + +### 3. 获取服务器IP地址 + +在电脑上查看IP地址: + +**Windows:** +```cmd +ipconfig +``` + +**Linux/Mac:** +```bash +ifconfig +# 或 +ip addr show +``` + +记下显示的IP地址(如 192.168.1.100) + +### 4. 手机端连接 + +#### 方法一:使用浏览器(推荐) +1. 打开手机浏览器 +2. 访问 `http://[服务器IP]:5000/mobile/mobile_client.html` +3. 例如:`http://192.168.1.100:5000/mobile/mobile_client.html` + +#### 方法二:直接访问HTML文件 +1. 将 `mobile/mobile_client.html` 复制到手机 +2. 在文件中修改服务器IP地址 +3. 用浏览器打开HTML文件 + +### 5. 开始传输 +1. 在手机页面中点击"开始传输" +2. 允许摄像头和位置权限 +3. 查看连接状态指示灯变绿 +4. 在指挥中心Web界面查看实时数据 + +## 📱 手机端界面说明 + +### 状态面板 +- **📍 GPS坐标**: 显示当前精确位置 +- **🔋 电池电量**: 实时电池状态 +- **🌐 连接状态**: 与服务器的连接状态 + +### 控制按钮 +- **📹 开始传输**: 启动数据传输 +- **⏹️ 停止传输**: 停止传输 +- **🔄 重连**: 重新连接服务器 + +### 统计信息 +- **📊 已发送帧数**: 传输的图像帧数量 +- **📈 数据量**: 累计传输的数据量 + +### 日志面板 +- 显示详细的操作日志和错误信息 +- 帮助诊断连接问题 + +## 🖥️ 服务端管理 + +### Web界面控制 +访问 `http://localhost:5000` 查看: +- **地图显示**: 实时显示手机位置和检测结果 +- **设备管理**: 查看连接的手机列表 +- **数据统计**: 查看传输统计信息 + +### API接口 +- `GET /api/mobile/devices` - 获取连接设备列表 +- `POST /api/mobile/toggle` - 切换手机模式开关 +- `POST /mobile/ping` - 手机连接测试 +- `POST /mobile/upload` - 接收手机数据 + +### 命令行监控 +服务器控制台会显示详细日志: +``` +📱 新设备连接: iPhone (mobile_12) +📍 设备 mobile_12 位置更新: (39.904200, 116.407400) +🎯 检测到 2 个人 +📍 手机检测人员 1: 距离5.2m, 坐标(39.904250, 116.407450) +``` + +## ⚙️ 高级配置 + +### 修改传输参数 + +在手机端HTML文件中可以调整: + +```javascript +// 修改服务器地址 +this.serverHost = '192.168.1.100'; +this.serverPort = 5000; + +// 修改传输频率(毫秒) +const interval = 1000; // 1秒传输一次 + +// 修改图像质量(0.1-1.0) +const frameData = this.canvas.toDataURL('image/jpeg', 0.5); +``` + +### 网络优化 + +**低带宽环境:** +- 降低图像质量 (0.3-0.5) +- 增加传输间隔 (2-5秒) +- 减小图像分辨率 + +**高质量需求:** +- 提高图像质量 (0.7-0.9) +- 减少传输间隔 (0.5-1秒) +- 使用更高分辨率 + +## 🔧 故障排除 + +### 常见问题 + +#### 1. 手机无法连接服务器 +- **检查网络**: 确保在同一网络 +- **检查IP地址**: 确认服务器IP正确 +- **检查防火墙**: 关闭防火墙或开放端口 +- **检查端口**: 确认5000端口未被占用 + +#### 2. 摄像头无法访问 +- **权限设置**: 在浏览器中允许摄像头权限 +- **HTTPS需求**: 某些浏览器需要HTTPS才能访问摄像头 +- **设备占用**: 关闭其他使用摄像头的应用 + +#### 3. GPS定位失败 +- **位置权限**: 允许浏览器访问位置信息 +- **网络连接**: 确保网络连接正常 +- **室内环境**: 移动到有GPS信号的位置 + +#### 4. 传输断开 +- **网络稳定性**: 检查WiFi信号强度 +- **服务器状态**: 确认服务器正常运行 +- **自动重连**: 等待自动重连或手动重连 + +### 调试方法 + +#### 手机端调试 +1. 打开浏览器开发者工具 (F12) +2. 查看Console面板的错误信息 +3. 检查Network面板的网络请求 + +#### 服务端调试 +1. 查看控制台输出的日志信息 +2. 使用 `python tests/test_system.py` 测试系统 +3. 检查网络连接和端口状态 + +## 🌐 网络配置示例 + +### 局域网配置 +``` +电脑 (192.168.1.100) ←→ 路由器 ←→ 手机 (192.168.1.101) +``` + +### 热点配置 +``` +电脑热点 (192.168.137.1) ←→ 手机 (192.168.137.2) +``` + +### 有线+WiFi配置 +``` +电脑 (有线: 192.168.1.100) ←→ 路由器 ←→ 手机 (WiFi: 192.168.1.101) +``` + +## 📊 性能建议 + +### 推荐配置 +- **网络**: WiFi 5GHz频段,带宽 ≥ 10Mbps +- **手机**: RAM ≥ 4GB,Android 8+ / iOS 12+ +- **服务器**: 双核CPU,RAM ≥ 4GB + +### 优化设置 +- **高质量模式**: 0.7质量,1秒间隔 +- **平衡模式**: 0.5质量,1秒间隔(推荐) +- **省流量模式**: 0.3质量,2秒间隔 + +## 🚁 实战应用场景 + +### 军用场景 +- **前线侦察**: 士兵携带手机进行前方侦察 +- **多点监控**: 多个观察点同时传输情报 +- **指挥决策**: 指挥部实时获取战场态势 + +### 民用场景 +- **安保监控**: 保安巡逻时实时传输画面 +- **应急救援**: 救援人员现场情况汇报 +- **活动监管**: 大型活动现场监控 + +### 技术演示 +- **远程教学**: 实地教学直播 +- **技术展示**: 产品演示和技术验证 + +--- + +## 📞 技术支持 + +如有问题,请: +1. 查看控制台日志信息 +2. 运行系统测试脚本 +3. 检查网络配置 +4. 参考故障排除指南 + 这个手机连接功能大大扩展了战场态势感知系统的应用场景,让移动侦察成为可能! \ No newline at end of file diff --git a/distance-judgement/__pycache__/main_web.cpython-311.pyc b/distance-judgement/__pycache__/main_web.cpython-311.pyc index 44b68aa7..1629e0df 100644 Binary files a/distance-judgement/__pycache__/main_web.cpython-311.pyc and b/distance-judgement/__pycache__/main_web.cpython-311.pyc differ diff --git a/distance-judgement/create_simple_cert.py b/distance-judgement/create_simple_cert.py index d5dbd7a5..d5554ce2 100644 --- a/distance-judgement/create_simple_cert.py +++ b/distance-judgement/create_simple_cert.py @@ -1,98 +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() - +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +使用OpenSSL命令行工具创建简单的自签名证书 +不依赖Python的cryptography库 +""" + +import os +import subprocess +import sys + +def create_ssl_dir(): + """创建ssl目录""" + if not os.path.exists("ssl"): + os.makedirs("ssl") + print("✅ 创建ssl目录") + +def create_certificate_with_openssl(): + """使用OpenSSL命令创建证书""" + print("🔑 使用OpenSSL创建自签名证书...") + + # 检查OpenSSL是否可用 + try: + subprocess.run(["openssl", "version"], check=True, capture_output=True) + except (subprocess.CalledProcessError, FileNotFoundError): + print("❌ OpenSSL未安装或不在PATH中") + print("📝 请安装OpenSSL或使用其他方法") + return False + + # 创建私钥 + key_cmd = [ + "openssl", "genrsa", + "-out", "ssl/key.pem", + "2048" + ] + + # 创建证书 + cert_cmd = [ + "openssl", "req", "-new", "-x509", + "-key", "ssl/key.pem", + "-out", "ssl/cert.pem", + "-days", "365", + "-subj", "/C=CN/ST=Beijing/L=Beijing/O=Distance System/CN=localhost" + ] + + try: + print(" 生成私钥...") + subprocess.run(key_cmd, check=True, capture_output=True) + + print(" 生成证书...") + subprocess.run(cert_cmd, check=True, capture_output=True) + + print("✅ SSL证书创建成功!") + print(" 🔑 私钥: ssl/key.pem") + print(" 📜 证书: ssl/cert.pem") + return True + + except subprocess.CalledProcessError as e: + print(f"❌ OpenSSL命令执行失败: {e}") + return False + +def create_certificate_manual(): + """提供手动创建证书的说明""" + print("📝 手动创建SSL证书说明:") + print() + print("方法1 - 使用在线工具:") + print(" 访问: https://www.selfsignedcertificate.com/") + print(" 下载证书文件并重命名为 cert.pem 和 key.pem") + print() + print("方法2 - 使用Git Bash (Windows):") + print(" 打开Git Bash,进入项目目录,执行:") + print(" openssl genrsa -out ssl/key.pem 2048") + print(" openssl req -new -x509 -key ssl/key.pem -out ssl/cert.pem -days 365") + print() + print("方法3 - 暂时使用HTTP:") + print(" 运行: python main_web.py") + print(" 注意: HTTP模式下手机摄像头可能无法使用") + +def main(): + """主函数""" + create_ssl_dir() + + # 检查证书是否已存在 + if os.path.exists("ssl/cert.pem") and os.path.exists("ssl/key.pem"): + print("✅ SSL证书已存在") + return + + print("🔍 尝试创建SSL证书...") + + # 尝试使用OpenSSL + if create_certificate_with_openssl(): + return + + # 提供手动创建说明 + create_certificate_manual() + \ No newline at end of file diff --git a/distance-judgement/demo_mobile.py b/distance-judgement/demo_mobile.py index 1e36a4ee..41f0849e 100644 --- a/distance-judgement/demo_mobile.py +++ b/distance-judgement/demo_mobile.py @@ -1,206 +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}") - +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +手机连接功能演示脚本 +展示如何使用手机作为移动侦察设备 +""" + +import time +import json +import base64 +import requests +from src import MobileConnector, config + +def demo_mobile_functionality(): + """演示手机连接功能""" + print("📱 手机连接功能演示") + print("=" * 60) + + print("🎯 演示内容:") + print("1. 启动手机连接服务器") + print("2. 模拟手机客户端连接") + print("3. 发送模拟数据") + print("4. 展示数据处理流程") + print() + + # 创建手机连接器 + mobile_connector = MobileConnector(port=8080) + + print("📱 正在启动手机连接服务器...") + if mobile_connector.start_server(): + print("✅ 手机连接服务器启动成功") + print(f"🌐 等待手机客户端连接到端口 8080") + print() + + print("📖 使用说明:") + print("1. 确保手机和电脑在同一网络") + print("2. 在手机浏览器中访问:") + print(" http://[电脑IP]:5000/mobile/mobile_client.html") + print("3. 或者直接打开 mobile/mobile_client.html 文件") + print("4. 点击'开始传输'按钮") + print() + + print("🔧 获取电脑IP地址的方法:") + print("Windows: ipconfig") + print("Linux/Mac: ifconfig 或 ip addr show") + print() + + # 设置回调函数来显示接收的数据 + def on_frame_received(device_id, frame, device): + print(f"📷 收到设备 {device_id[:8]} 的图像帧") + print(f" 分辨率: {frame.shape[1]}x{frame.shape[0]}") + print(f" 设备: {device.device_name}") + + def on_location_received(device_id, location, device): + lat, lng, accuracy = location + print(f"📍 收到设备 {device_id[:8]} 的位置信息") + print(f" 坐标: ({lat:.6f}, {lng:.6f})") + print(f" 精度: {accuracy}m") + + def on_device_event(event_type, device): + if event_type == 'device_connected': + print(f"📱 设备连接: {device.device_name} ({device.device_id[:8]})") + print(f" 电池: {device.battery_level}%") + elif event_type == 'device_disconnected': + print(f"📱 设备断开: {device.device_name} ({device.device_id[:8]})") + + # 注册回调函数 + mobile_connector.add_frame_callback(on_frame_received) + mobile_connector.add_location_callback(on_location_received) + mobile_connector.add_device_callback(on_device_event) + + print("⏳ 等待手机连接... (按 Ctrl+C 退出)") + + try: + # 监控连接状态 + while True: + time.sleep(5) + + # 显示统计信息 + stats = mobile_connector.get_statistics() + online_devices = mobile_connector.get_online_devices() + + if stats['online_devices'] > 0: + print(f"\n📊 连接统计:") + print(f" 在线设备: {stats['online_devices']}") + print(f" 接收帧数: {stats['frames_received']}") + print(f" 数据量: {stats['data_received_mb']:.2f} MB") + print(f" 平均帧率: {stats['avg_frames_per_second']:.1f} FPS") + + print(f"\n📱 在线设备:") + for device in online_devices: + print(f" • {device.device_name} ({device.device_id[:8]})") + print(f" 电池: {device.battery_level}%") + if device.current_location: + lat, lng, acc = device.current_location + print(f" 位置: ({lat:.6f}, {lng:.6f})") + else: + print("⏳ 等待设备连接...") + + except KeyboardInterrupt: + print("\n🔴 用户中断") + + finally: + mobile_connector.stop_server() + print("📱 手机连接服务器已停止") + + else: + print("❌ 手机连接服务器启动失败") + print("💡 可能的原因:") + print(" - 端口 8080 已被占用") + print(" - 网络权限问题") + print(" - 防火墙阻止连接") + +def test_mobile_api(): + """测试手机相关API""" + print("\n🧪 测试手机API接口") + print("=" * 40) + + base_url = "http://127.0.0.1:5000" + + try: + # 测试ping接口 + test_data = {"device_id": "test_device_123"} + response = requests.post(f"{base_url}/mobile/ping", + json=test_data, timeout=5) + + if response.status_code == 200: + data = response.json() + print("✅ Ping API测试成功") + print(f" 服务器时间: {data.get('server_time')}") + else: + print(f"❌ Ping API测试失败: HTTP {response.status_code}") + + except requests.exceptions.ConnectionError: + print("⚠️ 无法连接到Web服务器") + print("💡 请先启动Web服务器: python main_web.py") + + except Exception as e: + print(f"❌ API测试出错: {e}") + +def show_mobile_guide(): + """显示手机连接指南""" + print("\n📖 手机连接步骤指南") + print("=" * 40) + + print("1️⃣ 启动服务端:") + print(" python main_web.py") + print(" 或 python run.py (选择Web模式)") + print() + + print("2️⃣ 获取电脑IP地址:") + print(" Windows: 打开CMD,输入 ipconfig") + print(" Mac/Linux: 打开终端,输入 ifconfig") + print(" 记下IP地址,如: 192.168.1.100") + print() + + print("3️⃣ 手机端连接:") + print(" 方法1: 浏览器访问 http://[IP]:5000/mobile/mobile_client.html") + print(" 方法2: 直接打开 mobile/mobile_client.html 文件") + print() + + print("4️⃣ 开始传输:") + print(" • 允许摄像头和位置权限") + print(" • 点击'开始传输'按钮") + print(" • 查看连接状态指示灯") + print() + + print("5️⃣ 查看结果:") + print(" • 在电脑Web界面查看地图") + print(" • 观察实时检测结果") + print(" • 监控设备状态") + +if __name__ == "__main__": + print("🚁 无人机战场态势感知系统 - 手机连接演示") + print("=" * 60) + + while True: + print("\n选择演示内容:") + print("1. 📱 启动手机连接服务器") + print("2. 🧪 测试手机API接口") + print("3. 📖 查看连接指南") + print("0. ❌ 退出") + + try: + choice = input("\n请输入选择 (0-3): ").strip() + + if choice == "1": + demo_mobile_functionality() + elif choice == "2": + test_mobile_api() + elif choice == "3": + show_mobile_guide() + elif choice == "0": + print("👋 再见!") + break + else: + print("❌ 无效选择,请重新输入") + + except KeyboardInterrupt: + print("\n👋 再见!") + break + except Exception as e: + print(f"❌ 出错: {e}") + input("\n按回车键继续...") \ No newline at end of file diff --git a/distance-judgement/drone_control.html b/distance-judgement/drone_control.html new file mode 100644 index 00000000..f4cdc4d9 --- /dev/null +++ b/distance-judgement/drone_control.html @@ -0,0 +1,1223 @@ + + + + + + + 无人机控制 - 距离判断系统 + + + + + + +
+ +
+ 未连接 +
+ + +
+
+

🚁 RoboMaster TT 无人机控制系统

+

基于距离判断系统的无人机实时视频传输控制

+
+
+ +
+ +
+ +
+
连接控制
+
+ + + +
+ 电量: -- +
+
+
+ 未连接到无人机,请确保已连接到无人机WiFi (TELLO-xxxxxx) +
+
+ + +
+
飞行控制
+ + +
+ + +
+ + +
+
+ +
+ + + + + + + + + +
+ +
+
+ + +
+
+ + +
30 厘米
+
+
+ + +
90
+
+
+
+
+ + +
+ +
+
实时视频流
+ +
+ + + +
+ +
+ 无视频流 +
+ +
+ 视频流状态: 未启动 +
+
+ + +
+
无人机状态
+
+
+
--
+ 电量 +
+
+
--
+ 高度 +
+
+
--
+ 速度 +
+
+
--
+ 信号 +
+
+
+
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/distance-judgement/fix_firewall.bat b/distance-judgement/fix_firewall.bat new file mode 100644 index 00000000..0519ecba --- /dev/null +++ b/distance-judgement/fix_firewall.bat @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/distance-judgement/get_ip.py b/distance-judgement/get_ip.py index cfc745f9..ff6e2132 100644 --- a/distance-judgement/get_ip.py +++ b/distance-judgement/get_ip.py @@ -1,37 +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) +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import socket + +def get_local_ip(): + """获取本机IP地址""" + try: + # 创建一个socket连接来获取本机IP + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("8.8.8.8", 80)) + ip = s.getsockname()[0] + s.close() + return ip + except: + try: + # 备用方法 + import subprocess + result = subprocess.run(['ipconfig'], capture_output=True, text=True, shell=True) + lines = result.stdout.split('\n') + for line in lines: + if 'IPv4' in line and '192.168' in line: + return line.split(':')[-1].strip() + except: + return '127.0.0.1' + +if __name__ == "__main__": + ip = get_local_ip() + print(f"🌐 服务器地址信息") + print(f"="*50) + print(f"本机IP地址: {ip}") + print(f"主页面地址: http://{ip}:5000/") + print(f"移动客户端: http://{ip}:5000/mobile/mobile_client.html") + print(f"GPS测试页面: http://{ip}:5000/mobile/gps_test.html") + print(f"设备选择测试: http://{ip}:5000/test_device_selector.html") + print(f"="*50) print(f"�� 手机/平板请访问移动客户端地址!") \ No newline at end of file diff --git a/distance-judgement/main.py b/distance-judgement/main.py index c1429b85..2c5ffa89 100644 --- a/distance-judgement/main.py +++ b/distance-judgement/main.py @@ -1,261 +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__": +import cv2 +import time +import numpy as np +from src import PersonDetector, DistanceCalculator, MapManager, config + +class RealTimePersonDistanceDetector: + def __init__(self): + self.detector = PersonDetector() + self.distance_calculator = DistanceCalculator() + self.cap = None + self.fps_counter = 0 + self.fps_time = time.time() + self.current_fps = 0 + + # 初始化地图管理器 + if config.ENABLE_MAP_DISPLAY: + self.map_manager = MapManager( + api_key=config.GAODE_API_KEY, + camera_lat=config.CAMERA_LATITUDE, + camera_lng=config.CAMERA_LONGITUDE + ) + self.map_manager.set_camera_position( + config.CAMERA_LATITUDE, + config.CAMERA_LONGITUDE, + config.CAMERA_HEADING + ) + print("🗺️ 地图管理器已初始化") + else: + self.map_manager = None + + def initialize_camera(self): + """初始化摄像头""" + self.cap = cv2.VideoCapture(config.CAMERA_INDEX) + if not self.cap.isOpened(): + raise Exception(f"无法开启摄像头 {config.CAMERA_INDEX}") + + # 设置摄像头参数 + self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, config.FRAME_WIDTH) + self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, config.FRAME_HEIGHT) + self.cap.set(cv2.CAP_PROP_FPS, config.FPS) + + # 获取实际设置的参数 + actual_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + actual_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + actual_fps = int(self.cap.get(cv2.CAP_PROP_FPS)) + + print(f"摄像头初始化成功:") + print(f" 分辨率: {actual_width}x{actual_height}") + print(f" 帧率: {actual_fps} FPS") + + def calculate_fps(self): + """计算实际帧率""" + self.fps_counter += 1 + current_time = time.time() + if current_time - self.fps_time >= 1.0: + self.current_fps = self.fps_counter + self.fps_counter = 0 + self.fps_time = current_time + + def draw_info_panel(self, frame, person_count=0): + """绘制信息面板""" + height, width = frame.shape[:2] + + # 绘制顶部信息栏 + info_height = 60 + cv2.rectangle(frame, (0, 0), (width, info_height), (0, 0, 0), -1) + + # 显示FPS + fps_text = f"FPS: {self.current_fps}" + cv2.putText(frame, fps_text, (10, 25), config.FONT, 0.6, (0, 255, 0), 2) + + # 显示人员计数 + person_text = f"Persons: {person_count}" + cv2.putText(frame, person_text, (150, 25), config.FONT, 0.6, (0, 255, 255), 2) + + # 显示模型信息 + model_text = self.detector.get_model_info() + cv2.putText(frame, model_text, (10, 45), config.FONT, 0.5, (255, 255, 255), 1) + + # 显示操作提示 + help_text = "Press 'q' to quit | 'c' to calibrate | 'r' to reset | 'm' to open map" + text_size = cv2.getTextSize(help_text, config.FONT, 0.5, 1)[0] + cv2.putText(frame, help_text, (width - text_size[0] - 10, 25), + config.FONT, 0.5, (255, 255, 0), 1) + + # 显示地图状态 + if self.map_manager: + map_status = "Map: ON" + cv2.putText(frame, map_status, (10, height - 10), + config.FONT, 0.5, (0, 255, 255), 1) + + return frame + + def calibrate_distance(self, detections): + """距离校准模式""" + if len(detections) == 0: + print("未检测到人体,无法校准") + return + + print("\n=== 距离校准模式 ===") + print("请确保画面中有一个人,并输入该人距离摄像头的真实距离") + + try: + real_distance = float(input("请输入真实距离(厘米): ")) + + # 使用第一个检测到的人进行校准 + detection = detections[0] + x1, y1, x2, y2, conf = detection + bbox_height = y2 - y1 + + # 更新参考参数 + config.REFERENCE_DISTANCE = real_distance + config.REFERENCE_HEIGHT_PIXELS = bbox_height + + # 重新初始化距离计算器 + self.distance_calculator = DistanceCalculator() + + print(f"校准完成!") + print(f"参考距离: {real_distance}cm") + print(f"参考像素高度: {bbox_height}px") + + except ValueError: + print("输入无效,校准取消") + except Exception as e: + print(f"校准失败: {e}") + + def process_frame(self, frame): + """处理单帧图像""" + # 检测人体 + detections = self.detector.detect_persons(frame) + + # 计算距离并更新地图位置 + distances = [] + if self.map_manager: + self.map_manager.clear_persons() + + for i, detection in enumerate(detections): + bbox = detection[:4] # [x1, y1, x2, y2] + x1, y1, x2, y2 = bbox + distance = self.distance_calculator.get_distance(bbox) + distance_str = self.distance_calculator.format_distance(distance) + distances.append(distance_str) + + # 更新地图上的人员位置 + if self.map_manager: + # 计算人体中心点 + center_x = (x1 + x2) / 2 + center_y = (y1 + y2) / 2 + + # 将距离从厘米转换为米 + distance_meters = distance / 100.0 + + # 添加到地图 + self.map_manager.add_person_position( + center_x, center_y, distance_meters, + frame.shape[1], frame.shape[0], # width, height + f"P{i+1}" + ) + + # 绘制检测结果 + frame = self.detector.draw_detections(frame, detections, distances) + + # 绘制信息面板 + frame = self.draw_info_panel(frame, len(detections)) + + # 计算FPS + self.calculate_fps() + + return frame, detections + + def run(self): + """运行主程序""" + try: + print("正在初始化...") + self.initialize_camera() + + print("系统启动成功!") + print("操作说明:") + print(" - 按 'q' 键退出程序") + print(" - 按 'c' 键进入距离校准模式") + print(" - 按 'r' 键重置为默认参数") + print(" - 按 's' 键保存当前帧") + if self.map_manager: + print(" - 按 'm' 键打开地图显示") + print(" - 按 'h' 键设置摄像头朝向") + print("\n开始实时检测...") + + frame_count = 0 + + while True: + ret, frame = self.cap.read() + if not ret: + print("无法读取摄像头画面") + break + + # 处理帧 + processed_frame, detections = self.process_frame(frame) + + # 显示结果 + cv2.imshow('Real-time Person Distance Detection', processed_frame) + + # 处理按键 + key = cv2.waitKey(1) & 0xFF + + if key == ord('q'): + print("用户退出程序") + break + elif key == ord('c'): + # 校准模式 + self.calibrate_distance(detections) + elif key == ord('r'): + # 重置参数 + print("重置为默认参数") + self.distance_calculator = DistanceCalculator() + elif key == ord('s'): + # 保存当前帧 + filename = f"capture_{int(time.time())}.jpg" + cv2.imwrite(filename, processed_frame) + print(f"已保存截图: {filename}") + elif key == ord('m') and self.map_manager: + # 打开地图显示 + print("正在打开地图...") + self.map_manager.open_map() + elif key == ord('h') and self.map_manager: + # 设置摄像头朝向 + try: + heading = float(input("请输入摄像头朝向角度 (0-360°, 0为正北): ")) + if 0 <= heading <= 360: + self.map_manager.update_camera_heading(heading) + else: + print("角度必须在0-360度之间") + except ValueError: + print("输入无效") + + frame_count += 1 + + except KeyboardInterrupt: + print("\n程序被用户中断") + except Exception as e: + print(f"程序运行出错: {e}") + finally: + self.cleanup() + + def cleanup(self): + """清理资源""" + if self.cap: + self.cap.release() + cv2.destroyAllWindows() + print("资源已清理,程序结束") + +def main(): + """主函数""" + print("=" * 50) + print("实时人体距离检测系统") + print("=" * 50) + + detector = RealTimePersonDistanceDetector() + detector.run() + +if __name__ == "__main__": main() \ No newline at end of file diff --git a/distance-judgement/main_web.py b/distance-judgement/main_web.py index e2b698c0..eb3422a2 100644 --- a/distance-judgement/main_web.py +++ b/distance-judgement/main_web.py @@ -18,7 +18,7 @@ def main(): print("🚁 无人机战场态势感知系统 - Web版本") print("=" * 60) print() - + # 检查配置 print("📋 系统配置检查...") print(f"📍 摄像头位置: ({config.CAMERA_LATITUDE:.6f}, {config.CAMERA_LONGITUDE:.6f})") @@ -133,6 +133,7 @@ def main(): 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(f"🚁 无人机控制: https://127.0.0.1:5000/drone_control.html") print("🔴 按 Ctrl+C 停止服务器") print() print("🔑 HTTPS注意事项:") diff --git a/distance-judgement/main_web_simple_https.py b/distance-judgement/main_web_simple_https.py index 77b91833..abb0bbd8 100644 --- a/distance-judgement/main_web_simple_https.py +++ b/distance-judgement/main_web_simple_https.py @@ -1,70 +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__": +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +简化版HTTPS Web服务器 +使用Python内置ssl模块,无需额外依赖 +""" + +import ssl +import socket +from src.web_server import create_app +from get_ip import get_local_ip + +def create_simple_ssl_context(): + """创建简单的SSL上下文,使用自签名证书""" + context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + + # 检查是否存在SSL证书文件 + import os + cert_file = "ssl/cert.pem" + key_file = "ssl/key.pem" + + if not os.path.exists(cert_file) or not os.path.exists(key_file): + print("❌ SSL证书文件不存在") + print("📝 为了使用HTTPS,请选择以下选项之一:") + print(" 1. 安装cryptography库: pip install cryptography") + print(" 2. 使用HTTP版本: python main_web.py") + print(" 3. 手动创建SSL证书") + return None + + try: + context.load_cert_chain(cert_file, key_file) + return context + except Exception as e: + print(f"❌ 加载SSL证书失败: {e}") + return None + +def main(): + """启动简化版HTTPS服务器""" + print("🚀 启动简化版HTTPS服务器...") + + # 创建Flask应用 + app = create_app() + + # 获取本地IP + local_ip = get_local_ip() + + print(f"🌐 本地IP地址: {local_ip}") + print() + print("📱 访问地址:") + print(f" 桌面端: https://127.0.0.1:5000") + print(f" 手机端: https://{local_ip}:5000/mobile/mobile_client.html") + print() + print("⚠️ 如果看到安全警告,请点击 '高级' -> '继续访问'") + print() + + # 创建SSL上下文 + ssl_context = create_simple_ssl_context() + + if ssl_context is None: + print("🔄 回退到HTTP模式...") + print(f" 桌面端: http://127.0.0.1:5000") + print(f" 手机端: http://{local_ip}:5000/mobile/mobile_client.html") + app.run(host='0.0.0.0', port=5000, debug=True) + else: + print("🔒 HTTPS模式启动成功!") + app.run(host='0.0.0.0', port=5000, debug=True, ssl_context=ssl_context) + +if __name__ == "__main__": main() \ No newline at end of file diff --git a/distance-judgement/mobile/baidu_browser_test.html b/distance-judgement/mobile/baidu_browser_test.html index 4dca59f5..001c8c46 100644 --- a/distance-judgement/mobile/baidu_browser_test.html +++ b/distance-judgement/mobile/baidu_browser_test.html @@ -1,1442 +1,1442 @@ - - - - - - - 📱 百度浏览器摄像头测试 - - - - -

📱 百度浏览器摄像头测试

-

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

- - - - - - - - - - - -
- -
点击按钮测试摄像头
-
- -
- -
- 🚁 返回移动终端 -
- - - - + + + + + + + 📱 百度浏览器摄像头测试 + + + + +

📱 百度浏览器摄像头测试

+

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

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

🌐 浏览器兼容性指南

-

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

-
- -
-

📋 当前浏览器检测

-
-

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

-
-
- -
-

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

- -
-

⚠️ 问题原因

-

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

-
- -
-

✅ 系统自动解决方案

-

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

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

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

-
-
- -
-

📱 浏览器兼容性列表

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

🔧 解决方案与建议

- -

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

-
-

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

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

2. 兼容模式使用方法

-
-

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

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

3. 移动设备特别说明

-
-

移动设备用户请注意:

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

🚨 常见问题排除

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

    🧪 测试工具

    -

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

    -
    - 📷 摄像头权限测试 - 🚁 返回移动终端 - -
    -
    -
    - - - - + + + + + + + 🌐 浏览器兼容性指南 + + + + +
    +
    +

    🌐 浏览器兼容性指南

    +

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

    +
    + +
    +

    📋 当前浏览器检测

    +
    +

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

    +
    +
    + +
    +

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

    + +
    +

    ⚠️ 问题原因

    +

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

    +
    + +
    +

    ✅ 系统自动解决方案

    +

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

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

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

    +
    +
    + +
    +

    📱 浏览器兼容性列表

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

    🔧 解决方案与建议

    + +

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

    +
    +

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

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

    2. 兼容模式使用方法

    +
    +

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

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

    3. 移动设备特别说明

    +
    +

    移动设备用户请注意:

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

    🚨 常见问题排除

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

    🧪 测试工具

    +

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

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

    📷 摄像头权限测试工具

    -

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

    -
    - -
    -

    🔍 1. 浏览器兼容性检查

    -
    等待测试...
    - -
    - -
    -

    🔐 2. 权限状态查询

    -
    等待测试...
    - -
    - -
    -

    📱 3. 设备枚举测试

    -
    等待测试...
    - -
    - -
    -

    🎥 4. 摄像头访问测试

    -
    等待测试...
    - - - -
    - -
    -

    📋 测试日志

    -
    - -
    -
    - - - - + + + + + + + 📷 摄像头权限测试 + + + + +
    +
    +

    📷 摄像头权限测试工具

    +

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

    +
    + +
    +

    🔍 1. 浏览器兼容性检查

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

    🔐 2. 权限状态查询

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

    📱 3. 设备枚举测试

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

    🎥 4. 摄像头访问测试

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

    📋 测试日志

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

    📍 GPS连接测试工具

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

    📋 操作日志

    -
    -
    - - - - + + + + + + + GPS连接测试 + + + + +
    +

    📍 GPS连接测试工具

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

    📋 操作日志

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

    📱 旧版浏览器使用指南

    -

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

    -
    - -
    -

    ⚠️ 检测结果

    -

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

    -
    - -
    -

    🔧 使用步骤

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

    🚨 常见问题

    - -

    Q: 权限被拒绝怎么办?

    -

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

    - -

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

    -

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

    - -

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

    -

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

    -
    - -
    -

    🌐 推荐浏览器

    -

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

    -
      -
    • 🌐 Chrome 53+ - 完全支持所有功能
    • -
    • 🦊 Firefox 36+ - 良好的兼容性
    • -
    • 🧭 Safari 11+ - iOS/macOS用户推荐
    • -
    • Edge 17+ - Windows用户推荐
    • -
    -
    - -
    -

    ✅ 重要提醒

    -

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

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

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

    -
      -
    • 使用传统getUserMedia API (webkit/moz前缀)
    • -
    • 提供预定义设备配置代替设备枚举
    • -
    • 简化权限检查流程
    • -
    • 降级使用基础功能
    • -
    -
    -
    - - - - + + + + + + + 📱 旧版浏览器使用指南 + + + + +
    +
    +

    📱 旧版浏览器使用指南

    +

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

    +
    + +
    +

    ⚠️ 检测结果

    +

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

    +
    + +
    +

    🔧 使用步骤

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

    🚨 常见问题

    + +

    Q: 权限被拒绝怎么办?

    +

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

    + +

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

    +

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

    + +

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

    +

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

    +
    + +
    +

    🌐 推荐浏览器

    +

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

    +
      +
    • 🌐 Chrome 53+ - 完全支持所有功能
    • +
    • 🦊 Firefox 36+ - 良好的兼容性
    • +
    • 🧭 Safari 11+ - iOS/macOS用户推荐
    • +
    • Edge 17+ - Windows用户推荐
    • +
    +
    + +
    +

    ✅ 重要提醒

    +

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

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

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

    +
      +
    • 使用传统getUserMedia API (webkit/moz前缀)
    • +
    • 提供预定义设备配置代替设备枚举
    • +
    • 简化权限检查流程
    • +
    • 降级使用基础功能
    • +
    +
    +
    + + + + \ No newline at end of file diff --git a/distance-judgement/mobile/mobile_client.html b/distance-judgement/mobile/mobile_client.html index b323ffbf..4543d36d 100644 --- a/distance-judgement/mobile/mobile_client.html +++ b/distance-judgement/mobile/mobile_client.html @@ -300,6 +300,8 @@ style="background: #9C27B0; display: none;">📋 摄像头信息 + +
    - + + +
    + + +
    + + +
    + +
    +
    - +
    + + +
    + + +
    @@ -343,15 +382,26 @@
    - +
    -

    🚀 性能优化建议

    + style="background: rgba(76, 175, 80, 0.1); border: 1px solid #4CAF50; border-radius: 8px; padding: 15px; margin: 20px 0;"> +

    🚀 智能性能优化

    -
    📶 网络良好: 可选择 5-10 FPS + 高质量(70-90%)
    -
    📱 网络一般: 建议 2-5 FPS + 中质量(50-70%)
    -
    🐌 网络较慢: 选择 1-2 FPS + 低质量(30-50%)
    -
    💡 系统会自动监控网络状况并给出调整建议
    +
    🚀 极致流畅模式: 320×240, 5FPS, 20%质量 - 最大化视频流畅度(默认)
    +
    ⚖️ 平衡模式: 480×360, 2FPS, 50%质量 - 流畅度与画质平衡
    +
    🎨 画质模式: 640×480, 2FPS, 70%质量 - 网络良好时使用
    +
    + ⚡ 系统将根据网络状况自动调整参数以保持最佳性能 +
    +
    + 💡 数据量对比:极致流畅模式约为画质模式的1/8,最大化传输效率 +
    +
    + 🧭 朝向优化:标准模式下朝向变化5°以上才发送,避免频繁更新 +
    +
    + 🎯 当前优化:已设为极致流畅模式,优先保障视频实时性能 +
    @@ -385,7 +435,14 @@ this.dataAmount = 0; this.currentPosition = null; this.currentOrientation = null; // 🌟 当前设备朝向 - this.lastDataSendTime = 0; // 数据发送节流 + this.lastLocationSendTime = 0; // GPS位置发送节流 + this.lastOrientationSendTime = 0; // 朝向数据发送节流 + + // 🧭 朝向数据平滑和过滤 + this.lastSentOrientation = null; // 上次发送的朝向 + this.orientationBuffer = []; // 朝向数据缓冲区,用于平滑处理 + this.ORIENTATION_CHANGE_THRESHOLD = 5; // 朝向变化阈值(度) + this.ORIENTATION_SEND_INTERVAL = 1500; // 朝向发送间隔(毫秒) // 🚀 性能监控 this.lastSendTime = 0; @@ -420,6 +477,13 @@ this.startOrientationTracking(); // 🌟 启动朝向追踪 this.updateBatteryStatus(); this.bindEvents(); + + // 🚀 初始化性能模式 + this.applyPerformanceMode(); + + // 🧭 初始化朝向设置 + this.updateOrientationSettings(); + this.log('移动终端初始化完成', 'success'); } @@ -761,9 +825,9 @@ const constraints = { video: { facingMode: 'environment', - width: { ideal: 640 }, - height: { ideal: 480 }, - // 添加帧率限制以提高性能 + width: { ideal: 800, min: 640 }, + height: { ideal: 600, min: 480 }, + // 提高清晰度以确保人员识别 frameRate: { ideal: 30, max: 30 } }, audio: false @@ -1070,6 +1134,7 @@ const gamma = event.gamma; // 围绕Y轴旋转(-90到90度) if (alpha !== null && alpha !== undefined) { + // 🧭 更新当前朝向数据 this.currentOrientation = { heading: alpha, tilt: beta, @@ -1083,20 +1148,106 @@ orientationElement.textContent = `${alpha.toFixed(1)}°`; } - // 🌟 实时传输朝向数据到服务器 - this.sendOrientationToServer(this.currentOrientation); + // 🚀 智能朝向数据发送(带阈值检测和平滑处理) + this.processOrientationForSending(this.currentOrientation); + } + } + + // 🧭 智能处理朝向数据发送 + processOrientationForSending(orientation) { + // 添加到缓冲区进行平滑处理 + this.orientationBuffer.push(orientation); + + // 保持缓冲区大小(最多保留5个数据点) + if (this.orientationBuffer.length > 5) { + this.orientationBuffer.shift(); + } + + // 计算平滑后的朝向(移动平均) + const smoothedOrientation = this.calculateSmoothedOrientation(); + + // 检查是否应该发送数据 + if (this.shouldSendOrientation(smoothedOrientation)) { + this.sendOrientationToServer(smoothedOrientation); + this.lastSentOrientation = smoothedOrientation; + } + } + + // 🧭 计算平滑后的朝向数据 + calculateSmoothedOrientation() { + if (this.orientationBuffer.length === 0) return null; + + let sumHeading = 0; + let sumTilt = 0; + let sumRoll = 0; + + // 处理角度平均(考虑360度边界) + this.orientationBuffer.forEach(orientation => { + sumHeading += orientation.heading; + sumTilt += orientation.tilt || 0; + sumRoll += orientation.roll || 0; + }); + + return { + heading: sumHeading / this.orientationBuffer.length, + tilt: sumTilt / this.orientationBuffer.length, + roll: sumRoll / this.orientationBuffer.length, + timestamp: Date.now() + }; + } + + // 🧭 检查是否应该发送朝向数据 + shouldSendOrientation(orientation) { + const now = Date.now(); + + // 1. 时间节流检查(最少间隔1.5秒) + if (now - this.lastOrientationSendTime < this.ORIENTATION_SEND_INTERVAL) { + return false; + } + + // 2. 如果是第一次发送,直接发送 + if (!this.lastSentOrientation) { + return true; + } + + // 3. 朝向变化阈值检查 + const headingDiff = this.calculateAngleDifference( + this.lastSentOrientation.heading, + orientation.heading + ); + + // 4. 倾斜角度变化检查(可选) + const tiltDiff = Math.abs( + (this.lastSentOrientation.tilt || 0) - (orientation.tilt || 0) + ); + + // 只有当朝向变化超过阈值或倾斜角度变化较大时才发送 + if (headingDiff >= this.ORIENTATION_CHANGE_THRESHOLD || tiltDiff >= 10) { + console.log(`🧭 朝向变化达到阈值,准备发送: ${headingDiff.toFixed(1)}°变化`); + return true; + } + + return false; // 变化太小,不发送 + } + + // 🧭 计算两个角度之间的最小差值(处理360度边界) + calculateAngleDifference(angle1, angle2) { + let diff = Math.abs(angle1 - angle2); + if (diff > 180) { + diff = 360 - diff; } + return diff; } // 🌟 发送GPS位置数据到服务器 async sendLocationToServer(position) { try { - // 节流:限制发送频率(每2秒最多一次) + // 节流:限制发送频率(每2秒最多一次)- GPS位置变化较慢,保持2秒间隔 const now = Date.now(); - if (now - this.lastDataSendTime < 2000) { + if (now - this.lastLocationSendTime < 2000) { return; } - this.lastDataSendTime = now; + this.lastLocationSendTime = now; const locationData = { device_id: this.deviceId, @@ -1136,11 +1287,8 @@ // 🌟 发送设备朝向数据到服务器 async sendOrientationToServer(orientation) { try { - // 节流:限制发送频率(每1秒最多一次) - const now = Date.now(); - if (now - this.lastDataSendTime < 1000) { - return; - } + // 📊 记录发送时间(由上层调用函数确保发送时机合适) + this.lastOrientationSendTime = Date.now(); const orientationData = { device_id: this.deviceId, @@ -1160,12 +1308,15 @@ }); if (response.ok) { - console.log('🧭 朝向数据已发送到服务器'); - console.log('🧭 发送的朝向数据详情:', { + const headingChange = this.lastSentOrientation && this.lastSentOrientation !== orientation ? + this.calculateAngleDifference(this.lastSentOrientation.heading, orientation.heading).toFixed(1) : 'N/A'; + + console.log(`🧭 朝向数据已发送: ${orientation.heading.toFixed(1)}° (变化${headingChange}°)`); + console.log('🧭 发送详情:', { device_id: this.deviceId.substr(0, 8), heading: orientation.heading.toFixed(1), tilt: orientation.tilt ? orientation.tilt.toFixed(1) : 'null', - roll: orientation.roll ? orientation.roll.toFixed(1) : 'null' + change: headingChange + '°' }); } else { console.warn('⚠️ 朝向数据发送失败:', response.status); @@ -1357,10 +1508,10 @@ async sendDataLoop() { // 🚀 优化:使用UI设置的帧率 - const frameRate = parseInt(document.getElementById('frameRate').value) || 2; + const frameRate = parseInt(document.getElementById('frameRate').value) || 5; const interval = 1000 / frameRate; // 根据设置的FPS计算间隔 - this.log(`📺 视频传输已优化: ${frameRate} FPS (间隔 ${interval}ms)`, 'success'); + this.log(`📺 视频传输已优化: ${frameRate} FPS (间隔 ${interval.toFixed(0)}ms)`, 'success'); while (this.isStreaming) { try { @@ -1375,12 +1526,19 @@ } async captureAndSend() { - this.canvas.width = this.videoElement.videoWidth || 640; - this.canvas.height = this.videoElement.videoHeight || 480; - this.ctx.drawImage(this.videoElement, 0, 0); + // 🚀 动态分辨率调整 + const resolution = document.getElementById('resolution').value || '320x240'; + const [targetWidth, targetHeight] = resolution.split('x').map(Number); + + // 设置画布尺寸为目标分辨率 + this.canvas.width = targetWidth; + this.canvas.height = targetHeight; + + // 将视频帧按比例缩放到目标分辨率 + this.ctx.drawImage(this.videoElement, 0, 0, targetWidth, targetHeight); // 🚀 优化:使用UI设置的图像质量 - const imageQuality = parseFloat(document.getElementById('imageQuality').value) || 0.7; + const imageQuality = parseFloat(document.getElementById('imageQuality').value) || 0.2; const frameData = this.canvas.toDataURL('image/jpeg', imageQuality).split(',')[1]; const data = { @@ -1439,11 +1597,101 @@ this.updateStats(); this.updateConnectionStatus(true); + + // 🚀 自适应性能优化 + this.autoOptimizePerformance(uploadTime); } else { throw new Error(`HTTP ${response.status}`); } } + // 🚀 性能模式切换 + applyPerformanceMode() { + const mode = document.getElementById('performanceMode').value; + const resolutionSelect = document.getElementById('resolution'); + const frameRateSelect = document.getElementById('frameRate'); + const qualitySelect = document.getElementById('imageQuality'); + + // 根据模式设置最优参数 + switch (mode) { + case 'smooth': // 流畅模式 - 极致流畅度优先 + resolutionSelect.value = '320x240'; + frameRateSelect.value = '5'; + qualitySelect.value = '0.2'; + this.log('🚀 已切换到极致流畅模式:320×240, 5FPS, 20%质量 - 最大化流畅度', 'success'); + break; + case 'balanced': // 平衡模式 - 流畅度与画质平衡 + resolutionSelect.value = '480x360'; + frameRateSelect.value = '2'; + qualitySelect.value = '0.5'; + this.log('⚖️ 已切换到平衡模式:480×360, 2FPS, 50%质量', 'info'); + break; + case 'quality': // 画质模式 - 优先画质 + resolutionSelect.value = '640x480'; + frameRateSelect.value = '2'; + qualitySelect.value = '0.7'; + this.log('🎨 已切换到画质模式:640×480, 2FPS, 70%质量', 'info'); + break; + case 'custom': // 自定义模式 + this.log('🔧 已切换到自定义模式,请手动调整参数', 'warning'); + break; + } + + // 禁用/启用控件 + const isCustom = mode === 'custom'; + resolutionSelect.disabled = !isCustom; + frameRateSelect.disabled = !isCustom; + qualitySelect.disabled = !isCustom; + } + + // 🚀 自适应性能优化 + autoOptimizePerformance(uploadTime) { + // 只在非自定义模式下自动优化 + const mode = document.getElementById('performanceMode').value; + if (mode === 'custom') return; + + const currentQuality = parseFloat(document.getElementById('imageQuality').value); + const currentFrameRate = parseInt(document.getElementById('frameRate').value); + const currentResolution = document.getElementById('resolution').value; + + // 网络太慢,自动降级(但保持足够清晰度进行人员识别) + if (this.averageUploadTime > 5000) { + if (currentQuality > 0.5) { + document.getElementById('imageQuality').value = Math.max(0.5, currentQuality - 0.1).toFixed(1); + this.log(`⚡ 网络较慢,自动降低图像质量到 ${Math.round(Math.max(0.5, currentQuality - 0.1) * 100)}%`, 'warning'); + } else if (currentFrameRate > 2) { + document.getElementById('frameRate').value = Math.max(2, currentFrameRate - 1); + this.log(`⚡ 网络较慢,自动降低帧率到 ${Math.max(2, currentFrameRate - 1)} FPS`, 'warning'); + } else if (currentResolution !== '640x480') { + document.getElementById('resolution').value = '640x480'; + this.log('⚡ 网络较慢,自动切换到标准分辨率 640×480', 'warning'); + } + } + // 网络很好,可以适当提升(流畅模式仍优先保证流畅度) + else if (this.averageUploadTime < 300 && mode === 'smooth') { + // 流畅模式:即使网络很好也优先提升帧率而不是画质 + if (currentFrameRate < 8) { + document.getElementById('frameRate').value = Math.min(8, currentFrameRate + 1); + this.log(`⚡ 网络极佳,自动提升帧率到 ${Math.min(8, currentFrameRate + 1)} FPS (保持流畅度优先)`, 'success'); + } else if (currentQuality < 0.3) { + document.getElementById('imageQuality').value = Math.min(0.3, currentQuality + 0.05).toFixed(2); + this.log(`⚡ 网络极佳,微调图像质量到 ${Math.round(Math.min(0.3, currentQuality + 0.05) * 100)}% (仍保持流畅优先)`, 'success'); + } + } + } + + // 🧭 更新朝向设置 + updateOrientationSettings() { + const sensitivity = parseFloat(document.getElementById('orientationSensitivity').value); + this.ORIENTATION_CHANGE_THRESHOLD = sensitivity; + + const sensText = sensitivity === 10 ? '低敏感' : + sensitivity === 5 ? '标准' : + sensitivity === 3 ? '敏感' : '高敏感'; + + this.log(`🧭 朝向敏感度已调整: ${sensText} (${sensitivity}°变化阈值)`, 'info'); + } + async getBatteryLevel() { try { if ('getBattery' in navigator) { @@ -1640,6 +1888,7 @@ document.getElementById('stopStreamBtn').style.display = 'inline-block'; document.getElementById('cameraInfoBtn').style.display = 'inline-block'; document.getElementById('qualityBtn').style.display = 'inline-block'; + document.getElementById('enhanceBtn').style.display = 'inline-block'; document.getElementById('startBtn').disabled = false; document.getElementById('videoPlaceholder').style.display = 'none'; @@ -1993,6 +2242,7 @@ document.getElementById('stopStreamBtn').style.display = 'none'; document.getElementById('cameraInfoBtn').style.display = 'none'; document.getElementById('qualityBtn').style.display = 'none'; + document.getElementById('enhanceBtn').style.display = 'none'; this.log('✅ 所有摄像头系统已停止', 'success'); } @@ -2055,6 +2305,38 @@ } } + // 增强视频清晰度 + async enhanceVideoQuality() { + this.log('✨ 正在增强视频清晰度...', 'info'); + + // 自动设置高清晰度参数 + document.getElementById('resolution').value = '1280x720'; + document.getElementById('imageQuality').value = '0.7'; + document.getElementById('frameRate').value = '5'; + + // 应用高清设置 + const success = await this.adjustVideoQuality(1280, 720, 30); + + if (success) { + this.log('✅ 清晰度增强成功!已切换到高清模式', 'success'); + + // 更新UI显示 + const enhanceBtn = document.getElementById('enhanceBtn'); + if (enhanceBtn) { + enhanceBtn.textContent = '✅ 已增强'; + enhanceBtn.style.background = '#4CAF50'; + setTimeout(() => { + enhanceBtn.textContent = '✨ 增强清晰度'; + enhanceBtn.style.background = '#FF9800'; + }, 3000); + } + } else { + this.log('❌ 清晰度增强失败,请手动调整设置', 'error'); + } + + return success; + } + // 重写原有的实时捕获方法 async tryRealTimeCapture() { this.log('🚀 启动底层实时摄像头系统...', 'info'); @@ -2125,11 +2407,14 @@ stats.appendChild(perfDiv); } - const frameRate = parseInt(document.getElementById('frameRate').value) || 2; - const quality = parseInt(parseFloat(document.getElementById('imageQuality').value) * 100) || 70; + const frameRate = parseInt(document.getElementById('frameRate').value) || 5; + const quality = parseInt(parseFloat(document.getElementById('imageQuality').value) * 100) || 20; + const resolution = document.getElementById('resolution').value || '320x240'; + const mode = document.getElementById('performanceMode').value; + const modeText = mode === 'smooth' ? '🚀流畅' : mode === 'balanced' ? '⚖️平衡' : mode === 'quality' ? '🎨画质' : '🔧自定义'; document.querySelector('.performance-info').innerHTML = ` -
    📊 当前设置: ${frameRate} FPS, ${quality}% 质量
    +
    📊 ${modeText}模式: ${resolution}, ${frameRate}FPS, ${quality}%质量
    ⏱️ 平均延迟: ${this.averageUploadTime.toFixed(0)}ms
    📈 网络状态: ${this.averageUploadTime < 500 ? '🟢 良好' : this.averageUploadTime < 2000 ? '🟡 一般' : '🔴 较慢'}
    `; @@ -2463,12 +2748,12 @@ this.currentStream = null; } - // 移动端默认使用后置摄像头 + // 移动端默认使用后置摄像头,高清配置确保人员识别 const constraints = { video: { facingMode: 'environment', // 优先使用后置摄像头 - width: { ideal: 640 }, - height: { ideal: 480 }, + width: { ideal: 800, min: 640 }, + height: { ideal: 600, min: 480 }, frameRate: { ideal: 30, max: 30 } }, audio: false @@ -2865,6 +3150,8 @@ let mobileClient; window.addEventListener('load', () => { mobileClient = new MobileClient(); + // 🌐 设置全局访问(供HTML事件调用) + window.mobileClient = mobileClient; }); // ========== 移动端专用功能(已简化) ========== diff --git a/distance-judgement/mobile/permission_guide.html b/distance-judgement/mobile/permission_guide.html index 7078d29a..2004daeb 100644 --- a/distance-judgement/mobile/permission_guide.html +++ b/distance-judgement/mobile/permission_guide.html @@ -1,430 +1,430 @@ - - - - - - - 📱 权限设置指南 - - - - -
    -

    📱 权限设置指南

    - -
    -

    📊 当前权限状态

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

    🎯 第1步:GPS定位权限

    -

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

    -
      -
    • 当浏览器弹出权限请求时,点击"允许"
    • -
    • 如果已经拒绝,点击地址栏的🔒图标重新设置
    • -
    • 确保设备的定位服务已开启
    • -
    -
    - -
    -
    - -
    -

    📷 第2步:摄像头权限

    -

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

    -
      -
    • 当浏览器询问摄像头权限时,点击"允许"
    • -
    • 如果失败,检查其他应用是否占用摄像头
    • -
    • 建议使用后置摄像头以获得更好效果
    • -
    -
    - -
    -
    - -
    -

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

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

    ⚠️ 常见问题解决:

    -

    GPS获取失败:

    -
      -
    • 移动到窗边或室外获得更好信号
    • -
    • 检查设备的定位服务是否开启
    • -
    • 在浏览器设置中清除网站数据后重试
    • -
    -

    摄像头无法访问:

    -
      -
    • 关闭其他正在使用摄像头的应用
    • -
    • 重启浏览器或设备
    • -
    • 使用Chrome或Safari等现代浏览器
    • -
    -
    - - - - -
    - - - - + + + + + + + 📱 权限设置指南 + + + + +
    +

    📱 权限设置指南

    + +
    +

    📊 当前权限状态

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

    🎯 第1步:GPS定位权限

    +

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

    +
      +
    • 当浏览器弹出权限请求时,点击"允许"
    • +
    • 如果已经拒绝,点击地址栏的🔒图标重新设置
    • +
    • 确保设备的定位服务已开启
    • +
    +
    + +
    +
    + +
    +

    📷 第2步:摄像头权限

    +

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

    +
      +
    • 当浏览器询问摄像头权限时,点击"允许"
    • +
    • 如果失败,检查其他应用是否占用摄像头
    • +
    • 建议使用后置摄像头以获得更好效果
    • +
    +
    + +
    +
    + +
    +

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

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

    ⚠️ 常见问题解决:

    +

    GPS获取失败:

    +
      +
    • 移动到窗边或室外获得更好信号
    • +
    • 检查设备的定位服务是否开启
    • +
    • 在浏览器设置中清除网站数据后重试
    • +
    +

    摄像头无法访问:

    +
      +
    • 关闭其他正在使用摄像头的应用
    • +
    • 重启浏览器或设备
    • +
    • 使用Chrome或Safari等现代浏览器
    • +
    +
    + + + + +
    + + + + \ No newline at end of file diff --git a/distance-judgement/requirements.txt b/distance-judgement/requirements.txt index ce615fd4..34da47b9 100644 --- a/distance-judgement/requirements.txt +++ b/distance-judgement/requirements.txt @@ -1,12 +1,37 @@ -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 +# 核心依赖 +numpy>=1.24.3 +opencv-python>=4.8.1 +Pillow>=10.0.0 +PyYAML>=5.4.0 + +# 机器学习和计算机视觉 +torch>=2.0.1 +torchvision>=0.15.2 +ultralytics>=8.0.196 + +# 无人机控制 +djitellopy>=2.4.0 + +# Web框架 +Flask>=2.3.3 +Flask-CORS>=3.0.0 + +# 图像处理 +scikit-image>=0.18.0 +matplotlib>=3.7.2 + +# 网络和通信 +requests>=2.31.0 +websocket-client>=1.0.0 + +# 数据处理 +pandas>=1.3.0 + +# 配置和环境 +python-dotenv>=0.19.0 + +# 系统工具 +psutil>=5.8.0 cryptography>=3.4.8 # Windows系统位置服务支持(仅Windows) diff --git a/distance-judgement/requirements_core.txt b/distance-judgement/requirements_core.txt new file mode 100644 index 00000000..e6034fc2 --- /dev/null +++ b/distance-judgement/requirements_core.txt @@ -0,0 +1,30 @@ +# 无人机视频传输核心依赖 +# 只包含必需的包,用于快速启动系统 + +# 核心依赖 +numpy>=1.24.3 +opencv-python>=4.8.1 +Pillow>=10.0.0 +PyYAML>=5.4.0 + +# 机器学习和计算机视觉 +torch>=2.0.1 +torchvision>=0.15.2 +ultralytics>=8.0.196 + +# 无人机控制 +djitellopy>=2.4.0 + +# Web框架 +Flask>=2.3.3 + +# 网络和通信 +requests>=2.31.0 + +# 系统工具 +psutil>=5.8.0 +cryptography>=3.4.8 + +# Windows系统位置服务支持(仅Windows) +winrt-runtime>=1.0.0; sys_platform == "win32" +winrt-Windows.Devices.Geolocation>=1.0.0; sys_platform == "win32" \ No newline at end of file diff --git a/distance-judgement/run.py b/distance-judgement/run.py index ae118b81..dff2d66a 100644 --- a/distance-judgement/run.py +++ b/distance-judgement/run.py @@ -1,97 +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__": +1#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +无人机战场态势感知系统 - 启动脚本 +让用户选择运行模式 +""" + +import sys +import os + +def show_menu(): + """显示菜单""" + print("=" * 60) + print("🚁 无人机战场态势感知系统") + print("=" * 60) + print() + print("请选择运行模式:") + print() + print("1. 🌐 Web模式 (推荐)") + print(" • 地图作为主界面") + print(" • 通过浏览器操作") + print(" • 可视化程度更高") + print(" • 支持远程访问") + print() + print("2. 🖥️ 传统模式") + print(" • 直接显示摄像头画面") + print(" • 键盘快捷键操作") + print(" • 性能更好") + print(" • 适合本地使用") + print() + print("3. ⚙️ 配置摄像头位置") + print(" • 设置GPS坐标") + print(" • 配置朝向角度") + print(" • 设置API Key") + print() + print("4. 🧪 运行系统测试") + print(" • 检查各模块状态") + print(" • 验证系统功能") + print() + print("0. ❌ 退出") + print() + +def main(): + """主函数""" + while True: + show_menu() + try: + choice = input("请输入选择 (0-4): ").strip() + + if choice == "1": + print("\n🌐 启动Web模式...") + import main_web + main_web.main() + break + + elif choice == "2": + print("\n🖥️ 启动传统模式...") + import main + main.main() + break + + elif choice == "3": + print("\n⚙️ 配置摄像头位置...") + import sys + sys.path.append('tools') + import setup_camera_location + setup_camera_location.main() + print("\n配置完成,请重新选择运行模式") + input("按回车键继续...") + + elif choice == "4": + print("\n🧪 运行系统测试...") + import sys + sys.path.append('tests') + import test_system + test_system.main() + print("\n测试完成") + input("按回车键继续...") + + elif choice == "0": + print("\n👋 再见!") + sys.exit(0) + + else: + print("\n❌ 无效选择,请重新输入") + input("按回车键继续...") + + except KeyboardInterrupt: + print("\n\n👋 再见!") + sys.exit(0) + except Exception as e: + print(f"\n❌ 运行出错: {e}") + input("按回车键继续...") + +if __name__ == "__main__": main() \ No newline at end of file diff --git a/distance-judgement/src/__init__.py b/distance-judgement/src/__init__.py index 02675b6d..efd1fe4c 100644 --- a/distance-judgement/src/__init__.py +++ b/distance-judgement/src/__init__.py @@ -1,51 +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' +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +实时人体距离检测系统 - 核心模块包 + +包含以下模块: +- config: 配置文件 +- person_detector: 人体检测模块 +- distance_calculator: 距离计算模块 +""" + +__version__ = "1.0.0" +__author__ = "Distance Detection System" + +# 导入核心模块 +from .config import * +from .person_detector import PersonDetector +from .distance_calculator import DistanceCalculator +from .map_manager import MapManager +from .web_server import WebServer +from .mobile_connector import MobileConnector, MobileDevice +from .orientation_detector import OrientationDetector +from .web_orientation_detector import WebOrientationDetector + +__all__ = [ + 'PersonDetector', + 'DistanceCalculator', + 'MapManager', + 'WebServer', + 'MobileConnector', + 'MobileDevice', + 'CAMERA_INDEX', + 'FRAME_WIDTH', + 'FRAME_HEIGHT', + 'FPS', + 'MODEL_PATH', + 'CONFIDENCE_THRESHOLD', + 'IOU_THRESHOLD', + 'KNOWN_PERSON_HEIGHT', + 'FOCAL_LENGTH', + 'REFERENCE_DISTANCE', + 'REFERENCE_HEIGHT_PIXELS', + 'FONT', + 'FONT_SCALE', + 'FONT_THICKNESS', + 'BOX_COLOR', + 'TEXT_COLOR', + 'TEXT_BG_COLOR', + 'PERSON_CLASS_ID' ] \ No newline at end of file diff --git a/distance-judgement/src/__pycache__/__init__.cpython-313.pyc b/distance-judgement/src/__pycache__/__init__.cpython-313.pyc index a6fbb81d..a0a3c5ac 100644 Binary files a/distance-judgement/src/__pycache__/__init__.cpython-313.pyc and b/distance-judgement/src/__pycache__/__init__.cpython-313.pyc differ diff --git a/distance-judgement/src/__pycache__/__init__.cpython-39.pyc b/distance-judgement/src/__pycache__/__init__.cpython-39.pyc index 8a3f9798..b66aa1a3 100644 Binary files a/distance-judgement/src/__pycache__/__init__.cpython-39.pyc and b/distance-judgement/src/__pycache__/__init__.cpython-39.pyc differ diff --git a/distance-judgement/src/__pycache__/config.cpython-311.pyc b/distance-judgement/src/__pycache__/config.cpython-311.pyc index ec3f976d..e40d2f2e 100644 Binary files a/distance-judgement/src/__pycache__/config.cpython-311.pyc and b/distance-judgement/src/__pycache__/config.cpython-311.pyc differ diff --git a/distance-judgement/src/__pycache__/config.cpython-313.pyc b/distance-judgement/src/__pycache__/config.cpython-313.pyc index f11b9d37..26e637d9 100644 Binary files a/distance-judgement/src/__pycache__/config.cpython-313.pyc and b/distance-judgement/src/__pycache__/config.cpython-313.pyc differ diff --git a/distance-judgement/src/__pycache__/config.cpython-39.pyc b/distance-judgement/src/__pycache__/config.cpython-39.pyc index 7804005d..0def9c38 100644 Binary files a/distance-judgement/src/__pycache__/config.cpython-39.pyc and b/distance-judgement/src/__pycache__/config.cpython-39.pyc differ diff --git a/distance-judgement/src/__pycache__/distance_calculator.cpython-313.pyc b/distance-judgement/src/__pycache__/distance_calculator.cpython-313.pyc index cbc4ac16..a68a1572 100644 Binary files a/distance-judgement/src/__pycache__/distance_calculator.cpython-313.pyc and b/distance-judgement/src/__pycache__/distance_calculator.cpython-313.pyc differ diff --git a/distance-judgement/src/__pycache__/map_manager.cpython-313.pyc b/distance-judgement/src/__pycache__/map_manager.cpython-313.pyc index b73566b6..beaf334f 100644 Binary files a/distance-judgement/src/__pycache__/map_manager.cpython-313.pyc and b/distance-judgement/src/__pycache__/map_manager.cpython-313.pyc differ diff --git a/distance-judgement/src/__pycache__/mobile_connector.cpython-313.pyc b/distance-judgement/src/__pycache__/mobile_connector.cpython-313.pyc index d026f925..55769ab7 100644 Binary files a/distance-judgement/src/__pycache__/mobile_connector.cpython-313.pyc and b/distance-judgement/src/__pycache__/mobile_connector.cpython-313.pyc differ diff --git a/distance-judgement/src/__pycache__/orientation_detector.cpython-313.pyc b/distance-judgement/src/__pycache__/orientation_detector.cpython-313.pyc index 35619b30..9bf5aea5 100644 Binary files a/distance-judgement/src/__pycache__/orientation_detector.cpython-313.pyc and b/distance-judgement/src/__pycache__/orientation_detector.cpython-313.pyc differ diff --git a/distance-judgement/src/__pycache__/person_detector.cpython-313.pyc b/distance-judgement/src/__pycache__/person_detector.cpython-313.pyc index 517f42a1..b679ca88 100644 Binary files a/distance-judgement/src/__pycache__/person_detector.cpython-313.pyc and b/distance-judgement/src/__pycache__/person_detector.cpython-313.pyc differ diff --git a/distance-judgement/src/__pycache__/robomaster_connector.cpython-311.pyc b/distance-judgement/src/__pycache__/robomaster_connector.cpython-311.pyc new file mode 100644 index 00000000..3a348303 Binary files /dev/null and b/distance-judgement/src/__pycache__/robomaster_connector.cpython-311.pyc differ diff --git a/distance-judgement/src/__pycache__/web_orientation_detector.cpython-313.pyc b/distance-judgement/src/__pycache__/web_orientation_detector.cpython-313.pyc index 99f5fbf4..f62e4c6c 100644 Binary files a/distance-judgement/src/__pycache__/web_orientation_detector.cpython-313.pyc and b/distance-judgement/src/__pycache__/web_orientation_detector.cpython-313.pyc differ diff --git a/distance-judgement/src/__pycache__/web_server.cpython-311.pyc b/distance-judgement/src/__pycache__/web_server.cpython-311.pyc index 072b8632..96a00e05 100644 Binary files a/distance-judgement/src/__pycache__/web_server.cpython-311.pyc and b/distance-judgement/src/__pycache__/web_server.cpython-311.pyc differ diff --git a/distance-judgement/src/__pycache__/web_server.cpython-313.pyc b/distance-judgement/src/__pycache__/web_server.cpython-313.pyc index ce238327..6ab57e7e 100644 Binary files a/distance-judgement/src/__pycache__/web_server.cpython-313.pyc and b/distance-judgement/src/__pycache__/web_server.cpython-313.pyc differ diff --git a/distance-judgement/src/config.py b/distance-judgement/src/config.py index 309eda0f..911d8962 100644 --- a/distance-judgement/src/config.py +++ b/distance-judgement/src/config.py @@ -1,40 +1,40 @@ -# 配置文件 -import cv2 - -# 摄像头设置 -CAMERA_INDEX = 0 # 默认摄像头索引 -FRAME_WIDTH = 640 -FRAME_HEIGHT = 480 -FPS = 30 - -# YOLO模型设置 -MODEL_PATH = 'yolov8n.pt' # YOLOv8 nano模型 -CONFIDENCE_THRESHOLD = 0.5 -IOU_THRESHOLD = 0.45 - -# 距离计算参数 -# 这些参数需要根据实际摄像头和场景进行标定 -KNOWN_PERSON_HEIGHT = 170 # 假设平均人身高170cm -FOCAL_LENGTH = 500 # 焦距参数,需要校准 -REFERENCE_DISTANCE = 200 # 参考距离(cm) -REFERENCE_HEIGHT_PIXELS = 300 # 在参考距离下人体框的像素高度 - -# 显示设置 -FONT = cv2.FONT_HERSHEY_SIMPLEX -FONT_SCALE = 0.7 -FONT_THICKNESS = 2 -BOX_COLOR = (0, 255, 0) # 绿色框 -TEXT_COLOR = (255, 255, 255) # 白色文字 -TEXT_BG_COLOR = (0, 0, 0) # 黑色背景 - -# 人体类别ID(COCO数据集中person的类别ID是0) -PERSON_CLASS_ID = 0 - -# 地图配置 -GAODE_API_KEY = "3dcf7fa331c70e62d4683cf40fffc443" # 需要替换为真实的高德API key -CAMERA_LATITUDE = 28.262339630314234 # 摄像头纬度 -CAMERA_LONGITUDE = 113.04752581515713 # 摄像头经度 -CAMERA_HEADING = 180 # 摄像头朝向角度 -CAMERA_FOV = 60 # 摄像头视场角度 -ENABLE_MAP_DISPLAY = True # 是否启用地图显示 +# 配置文件 +import cv2 + +# 摄像头设置 +CAMERA_INDEX = 0 # 默认摄像头索引 +FRAME_WIDTH = 640 +FRAME_HEIGHT = 480 +FPS = 30 + +# YOLO模型设置 +MODEL_PATH = 'yolov8n.pt' # YOLOv8 nano模型 +CONFIDENCE_THRESHOLD = 0.5 +IOU_THRESHOLD = 0.45 + +# 距离计算参数 +# 这些参数需要根据实际摄像头和场景进行标定 +KNOWN_PERSON_HEIGHT = 170 # 假设平均人身高170cm +FOCAL_LENGTH = 500 # 焦距参数,需要校准 +REFERENCE_DISTANCE = 200 # 参考距离(cm) +REFERENCE_HEIGHT_PIXELS = 300 # 在参考距离下人体框的像素高度 + +# 显示设置 +FONT = cv2.FONT_HERSHEY_SIMPLEX +FONT_SCALE = 0.7 +FONT_THICKNESS = 2 +BOX_COLOR = (0, 255, 0) # 绿色框 +TEXT_COLOR = (255, 255, 255) # 白色文字 +TEXT_BG_COLOR = (0, 0, 0) # 黑色背景 + +# 人体类别ID(COCO数据集中person的类别ID是0) +PERSON_CLASS_ID = 0 + +# 地图配置 +GAODE_API_KEY = "3dcf7fa331c70e62d4683cf40fffc443" # 需要替换为真实的高德API key +CAMERA_LATITUDE = 28.258595 # 摄像头纬度 +CAMERA_LONGITUDE = 113.046585 # 摄像头经度 +CAMERA_HEADING = 180 # 摄像头朝向角度 +CAMERA_FOV = 60 # 摄像头视场角度 +ENABLE_MAP_DISPLAY = True # 是否启用地图显示 MAP_AUTO_REFRESH = True # 地图是否自动刷新 \ No newline at end of file diff --git a/distance-judgement/src/distance_calculator.py b/distance-judgement/src/distance_calculator.py index 3f1702fc..7ab5dc9b 100644 --- a/distance-judgement/src/distance_calculator.py +++ b/distance-judgement/src/distance_calculator.py @@ -1,206 +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 - +import numpy as np +import math +from . import config + +class DistanceCalculator: + def __init__(self): + self.focal_length = config.FOCAL_LENGTH + self.known_height = config.KNOWN_PERSON_HEIGHT + self.reference_distance = config.REFERENCE_DISTANCE + self.reference_height_pixels = config.REFERENCE_HEIGHT_PIXELS + + def calculate_distance_by_height(self, bbox_height): + """ + 根据人体框高度计算距离 + 使用相似三角形原理:距离 = (已知高度 × 焦距) / 像素高度 + """ + if bbox_height <= 0: + return 0 + + # 使用参考距离和参考像素高度来校准 + distance = (self.reference_distance * self.reference_height_pixels) / bbox_height + return max(distance, 30) # 最小距离限制为30cm + + def calculate_distance_by_focal_length(self, bbox_height): + """ + 使用焦距公式计算距离 + 距离 = (真实高度 × 焦距) / 像素高度 + """ + if bbox_height <= 0: + return 0 + + distance = (self.known_height * self.focal_length) / bbox_height + return max(distance, 30) # 最小距离限制为30cm + + def calibrate_focal_length(self, known_distance, measured_height_pixels): + """ + 标定焦距 + 焦距 = (像素高度 × 真实距离) / 真实高度 + """ + self.focal_length = (measured_height_pixels * known_distance) / self.known_height + print(f"焦距已标定为: {self.focal_length:.2f}") + + def get_distance(self, bbox): + """ + 根据边界框计算距离 + bbox: [x1, y1, x2, y2] + """ + x1, y1, x2, y2 = bbox + bbox_height = y2 - y1 + bbox_width = x2 - x1 + + # 使用高度计算距离(更准确) + distance = self.calculate_distance_by_height(bbox_height) + + return distance + + def format_distance(self, distance): + """ + 格式化距离显示 + """ + if distance < 100: + return f"{distance:.1f}cm" + else: + return f"{distance/100:.1f}m" + + def calculate_person_gps_position(self, camera_lat, camera_lng, camera_heading, + bbox, distance_meters, frame_width, frame_height, + camera_fov=60): + """ + 🎯 核心算法:根据摄像头GPS位置、朝向、人体检测框计算人员真实GPS坐标 + + Args: + camera_lat: 摄像头纬度 + camera_lng: 摄像头经度 + camera_heading: 摄像头朝向角度 (0=正北, 90=正东) + bbox: 人体检测框 [x1, y1, x2, y2] + distance_meters: 人员距离摄像头的距离(米) + frame_width: 画面宽度(像素) + frame_height: 画面高度(像素) + camera_fov: 摄像头水平视场角(度) + + Returns: + (person_lat, person_lng): 人员GPS坐标 + """ + x1, y1, x2, y2 = bbox + + # 计算人体检测框中心点 + person_center_x = (x1 + x2) / 2 + person_center_y = (y1 + y2) / 2 + + # 计算人员相对于画面中心的偏移角度 + frame_center_x = frame_width / 2 + horizontal_offset_pixels = person_center_x - frame_center_x + + # 将像素偏移转换为角度偏移 + horizontal_angle_per_pixel = camera_fov / frame_width + horizontal_offset_degrees = horizontal_offset_pixels * horizontal_angle_per_pixel + + # 计算人员相对于正北的实际方位角 + person_bearing = (camera_heading + horizontal_offset_degrees) % 360 + + # 使用球面几何计算人员GPS坐标 + person_lat, person_lng = self._calculate_destination_point( + camera_lat, camera_lng, distance_meters, person_bearing + ) + + return person_lat, person_lng + + def _calculate_destination_point(self, lat, lng, distance, bearing): + """ + 🌍 球面几何计算:根据起点坐标、距离和方位角计算目标点坐标 + + Args: + lat: 起点纬度 + lng: 起点经度 + distance: 距离(米) + bearing: 方位角(度,0=正北) + + Returns: + (target_lat, target_lng): 目标点坐标 + """ + # 地球半径(米) + R = 6371000 + + # 转换为弧度 + lat1 = math.radians(lat) + lng1 = math.radians(lng) + bearing_rad = math.radians(bearing) + + # 球面几何计算目标点坐标 + lat2 = math.asin( + math.sin(lat1) * math.cos(distance / R) + + math.cos(lat1) * math.sin(distance / R) * math.cos(bearing_rad) + ) + + lng2 = lng1 + math.atan2( + math.sin(bearing_rad) * math.sin(distance / R) * math.cos(lat1), + math.cos(distance / R) - math.sin(lat1) * math.sin(lat2) + ) + + return math.degrees(lat2), math.degrees(lng2) + + def is_person_in_camera_fov(self, camera_lat, camera_lng, camera_heading, + person_lat, person_lng, camera_fov=60, max_distance=100): + """ + 🔍 检查人员是否在摄像头视野范围内 + + Args: + camera_lat: 摄像头纬度 + camera_lng: 摄像头经度 + camera_heading: 摄像头朝向角度 + person_lat: 人员纬度 + person_lng: 人员经度 + camera_fov: 摄像头视场角(度) + max_distance: 最大检测距离(米) + + Returns: + bool: 是否在视野内 + """ + # 计算人员相对于摄像头的距离和方位角 + distance, bearing = self._calculate_distance_and_bearing( + camera_lat, camera_lng, person_lat, person_lng + ) + + # 检查距离是否在范围内 + if distance > max_distance: + return False + + # 计算人员方位角与摄像头朝向的角度差 + angle_diff = abs(bearing - camera_heading) + if angle_diff > 180: + angle_diff = 360 - angle_diff + + # 检查是否在视场角范围内 + return angle_diff <= camera_fov / 2 + + def _calculate_distance_and_bearing(self, lat1, lng1, lat2, lng2): + """ + 🧭 计算两点间距离和方位角 + + Returns: + (distance_meters, bearing_degrees): 距离(米)和方位角(度) + """ + # 转换为弧度 + lat1_rad = math.radians(lat1) + lng1_rad = math.radians(lng1) + lat2_rad = math.radians(lat2) + lng2_rad = math.radians(lng2) + + # 计算距离 (Haversine公式) + dlat = lat2_rad - lat1_rad + dlng = lng2_rad - lng1_rad + + a = (math.sin(dlat/2)**2 + + math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(dlng/2)**2) + c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a)) + distance = 6371000 * c # 地球半径6371km + + # 计算方位角 + y = math.sin(dlng) * math.cos(lat2_rad) + x = (math.cos(lat1_rad) * math.sin(lat2_rad) - + math.sin(lat1_rad) * math.cos(lat2_rad) * math.cos(dlng)) + bearing = math.atan2(y, x) + bearing_degrees = (math.degrees(bearing) + 360) % 360 + return distance, bearing_degrees \ No newline at end of file diff --git a/distance-judgement/src/drone/__init__.py b/distance-judgement/src/drone/__init__.py new file mode 100644 index 00000000..c83acd81 --- /dev/null +++ b/distance-judgement/src/drone/__init__.py @@ -0,0 +1,67 @@ +""" +Drone - RoboMaster TT无人机视频传输模块 +===================================== + +基于RoboMaster TT(Tello TLW004)无人机的视频流接收、处理和分析模块。 +支持实时视频流处理、图像分析等功能。 + +主要功能: +- 无人机连接与控制 +- 实时视频流接收 +- 图像捕获与分析 +- Web界面控制 + +使用示例: + from src.drone import DroneManager, VideoReceiver + + # 创建无人机管理器 + drone_manager = DroneManager() + + # 连接无人机 + drone_manager.connect() + + # 创建视频接收器 + video_receiver = VideoReceiver() + video_receiver.start("udp://192.168.10.1:11111") +""" + +__version__ = "1.0.0" +__author__ = "Distance Judgement Team" +__description__ = "RoboMaster TT无人机视频传输模块" + +# 导入核心模块 +try: + from .drone_interface.drone_manager import DroneManager + from .drone_interface.video_receiver import VideoReceiver +except ImportError as e: + print(f"Warning: Failed to import drone interface modules: {e}") + DroneManager = None + VideoReceiver = None + +# 导入图像分析器(可选) +try: + from .image_analyzer.analyzer import ImageAnalyzer +except ImportError as e: + print(f"Info: Image analyzer not available (optional): {e}") + ImageAnalyzer = None + +# 导出的组件 +__all__ = [ + 'DroneManager', + 'VideoReceiver', + 'ImageAnalyzer' +] + +def get_version(): + """获取版本信息""" + return __version__ + +def get_info(): + """获取模块信息""" + return { + 'name': 'Drone', + 'version': __version__, + 'author': __author__, + 'description': __description__, + 'components': [comp for comp in __all__ if globals().get(comp) is not None] + } \ No newline at end of file diff --git a/distance-judgement/src/drone/__pycache__/__init__.cpython-311.pyc b/distance-judgement/src/drone/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 00000000..38dc0eed Binary files /dev/null and b/distance-judgement/src/drone/__pycache__/__init__.cpython-311.pyc differ diff --git a/distance-judgement/src/drone/__pycache__/__init__.cpython-313.pyc b/distance-judgement/src/drone/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 00000000..6a1e371c Binary files /dev/null and b/distance-judgement/src/drone/__pycache__/__init__.cpython-313.pyc differ diff --git a/distance-judgement/src/drone/config.yaml b/distance-judgement/src/drone/config.yaml new file mode 100644 index 00000000..d83424da --- /dev/null +++ b/distance-judgement/src/drone/config.yaml @@ -0,0 +1,131 @@ +# Air模块配置文件 +# RoboMaster TT (Tello TLW004) 无人机配置 + +# 无人机基本配置 +drone: + type: "tello" # 无人机类型 + model: "TLW004" # 型号 + name: "RoboMaster TT" # 显示名称 + +# 网络连接配置 +connection: + ip: "192.168.10.1" # 无人机IP地址 + cmd_port: 8889 # 命令端口 + state_port: 8890 # 状态端口 + video_port: 11111 # 视频端口 + timeout: 5 # 连接超时时间(秒) + +# 视频流配置 +video: + # 支持的视频流格式 + formats: + udp: "udp://{ip}:{port}" + rtsp: "rtsp://{ip}:554/live" + http: "http://{ip}:8080/video" + + # 默认视频流URL + default_stream: "udp://192.168.10.1:11111" + + # 视频参数 + resolution: + width: 960 + height: 720 + fps: 30 + + # 缓冲设置 + buffer_size: 10 + timeout: 10 + + # 录制设置 + recording: + enabled: false + format: "mp4" + quality: "high" + +# 图像分析配置 +analysis: + # 检测阈值 + confidence_threshold: 0.25 + part_confidence_threshold: 0.3 + + # 模型路径(相对于项目根目录) + models: + ship_detector: "models/best.pt" + part_detector: "models/part_detectors/best.pt" + classifier: "models/custom/best.pt" + + # 检测类别 + ship_classes: + - "航空母舰" + - "驱逐舰" + - "护卫舰" + - "潜艇" + - "商船" + - "油轮" + + # 部件类别 + part_classes: + - "舰桥" + - "雷达" + - "舰炮" + - "导弹发射器" + - "直升机甲板" + - "烟囱" + +# Web界面配置 +web: + host: "0.0.0.0" + port: 5000 + debug: true + + # 静态文件路径 + static_folder: "web/static" + template_folder: "web/templates" + + # 上传设置 + upload: + max_file_size: "10MB" + allowed_extensions: [".jpg", ".jpeg", ".png", ".mp4", ".avi"] + save_path: "uploads/drone_captures" + +# 安全设置 +safety: + max_height: 100 # 最大飞行高度(米) + max_distance: 500 # 最大飞行距离(米) + min_battery: 15 # 最低电量百分比 + return_home_battery: 30 # 自动返航电量 + + # 飞行限制区域 + no_fly_zones: [] + +# 日志配置 +logging: + level: "INFO" + format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + + # 日志文件 + files: + main: "logs/air_main.log" + drone: "logs/drone.log" + video: "logs/video.log" + analysis: "logs/analysis.log" + +# 性能配置 +performance: + # GPU使用 + use_gpu: true + gpu_memory_fraction: 0.7 + + # 多线程设置 + max_workers: 4 + + # 内存限制 + max_memory_usage: "2GB" + +# 开发调试配置 +debug: + enabled: false + save_frames: false + frame_save_path: "debug/frames" + log_commands: true + mock_drone: false # 是否使用模拟无人机 \ No newline at end of file diff --git a/distance-judgement/src/drone/drone_controller/drone_controller.py b/distance-judgement/src/drone/drone_controller/drone_controller.py new file mode 100644 index 00000000..2c65ff04 --- /dev/null +++ b/distance-judgement/src/drone/drone_controller/drone_controller.py @@ -0,0 +1,1409 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import sys +import time +import cv2 +import threading +import numpy as np +from pathlib import Path +from datetime import datetime +from flask import Blueprint, jsonify, request, send_file, session, Response +import io +from PIL import Image + +# 添加项目根目录到路径 +script_dir = os.path.dirname(os.path.abspath(__file__)) +parent_dir = os.path.dirname(script_dir) +sys.path.append(parent_dir) + +# 导入无人机管理模块 +sys.path.append(os.path.join(parent_dir, 'src')) +from drone_interface.drone_manager import DroneManager +from drone_interface.video_receiver import VideoReceiver + +# 尝试导入DJI Tello控制库 +try: + from djitellopy import Tello +except ImportError: + print("警告: 未找到djitellopy库,请安装: pip install djitellopy") + Tello = None + +# 创建Blueprint +drone_bp = Blueprint('drone', __name__, url_prefix='/drone') + +# 全局变量 +tello = None +is_connected = False +is_streaming = False +stream_frame = None +stream_lock = threading.Lock() +drone_manager = None +video_receiver = None +stream_thread = None +running = False +captures_dir = os.path.join(parent_dir, 'web', 'uploads', 'drone_captures') + +# 确保存储目录存在 +if not os.path.exists(captures_dir): + os.makedirs(captures_dir, exist_ok=True) + +# HTTP流服务器 +http_server = None +http_server_port = 8000 +http_stream_url = None + +def get_tello(): + """获取Tello实例,如果不存在则创建""" + global tello + if tello is None: + if Tello is None: + raise ImportError("无法创建Tello对象,未找到djitellopy库") + tello = Tello() + return tello + +@drone_bp.route('/connect', methods=['POST']) +def connect(): + """连接到Tello无人机""" + global tello, is_connected, drone_manager + + try: + if is_connected: + return jsonify(success=True, message="已连接到无人机", battery=tello.get_battery()) + + # 初始化Tello + tello = get_tello() + + print("尝试连接Tello无人机...") + try: + # 首先尝试停止任何可能正在运行的视频流,避免冲突 + try: + print("尝试停止可能存在的视频流...") + tello.streamoff() + time.sleep(1) + except: + pass + + # 连接到无人机 + tello.connect() + + # 检查连接是否成功 + print("检查Tello连接状态...") + try: + # 发送一个命令测试连接 + tello.send_command_without_return('command') + time.sleep(0.5) + battery = tello.get_battery() + print(f"Tello连接成功,电池电量: {battery}%") + except Exception as conn_error: + print(f"Tello连接测试失败: {conn_error}") + return jsonify(success=False, error=f"连接测试失败: {str(conn_error)}") + except Exception as e: + print(f"Tello连接失败: {e}") + return jsonify(success=False, error=f"无法连接到Tello: {str(e)}") + + # 使用自定义的DroneManager + try: + drone_manager = DroneManager() + success = drone_manager.connect() + + if not success: + # 如果自定义管理器连接失败,仍然可以使用Tello SDK + print("自定义DroneManager连接失败,使用原生Tello SDK") + except Exception as dm_error: + print(f"DroneManager初始化失败: {dm_error}") + # 继续使用Tello SDK + + # 获取电池电量 + battery = tello.get_battery() + print(f"无人机连接成功,电池电量: {battery}%") + + is_connected = True + return jsonify(success=True, message="已连接到无人机", battery=battery) + + except Exception as e: + print(f"连接Tello时出错: {e}") + import traceback + traceback.print_exc() + return jsonify(success=False, error=str(e)) + +@drone_bp.route('/disconnect', methods=['POST']) +def disconnect(): + """断开与Tello无人机的连接""" + global tello, is_connected, is_streaming, stream_thread, running, drone_manager, video_receiver + + try: + if not is_connected: + return jsonify(success=True, message="已经断开连接") + + # 停止视频流 + if is_streaming: + stop_stream() + + # 断开自定义DroneManager + if drone_manager: + drone_manager.disconnect() + + # 断开Tello SDK连接 + if tello: + try: + tello.end() + print("Tello断开连接") + except: + pass + + is_connected = False + return jsonify(success=True, message="无人机已断开连接") + + except Exception as e: + print(f"断开Tello连接时出错: {e}") + return jsonify(success=False, error=str(e)) + +@drone_bp.route('/takeoff', methods=['POST']) +def takeoff(): + """起飞无人机""" + global tello, is_connected, drone_manager + + if not is_connected: + return jsonify(success=False, error="未连接到无人机") + + try: + print("收到起飞请求") + + # 尝试使用自定义DroneManager + if drone_manager: + print("使用DroneManager执行起飞") + result = drone_manager.takeoff() + if not result: + print("DroneManager起飞失败,尝试使用Tello SDK") + # 如果自定义起飞失败,使用Tello SDK + tello.takeoff() + else: + # 直接使用Tello SDK + print("使用Tello SDK执行起飞") + tello.takeoff() + + print("无人机起飞成功") + + # 等待1秒,确保起飞命令完成 + time.sleep(1) + + # 测试一个简单的命令,确认控制功能正常 + try: + print("发送测试命令,确认控制功能正常") + if drone_manager: + drone_manager._send_command("command") + else: + tello.send_command_without_return("command") + except Exception as test_error: + print(f"测试命令发送失败: {test_error}") + + return jsonify(success=True, message="无人机已起飞") + + except Exception as e: + print(f"无人机起飞时出错: {e}") + import traceback + traceback.print_exc() + return jsonify(success=False, error=str(e)) + +@drone_bp.route('/land', methods=['POST']) +def land(): + """降落无人机""" + global tello, is_connected, drone_manager + + if not is_connected: + return jsonify(success=False, error="未连接到无人机") + + try: + # 尝试使用自定义DroneManager + if drone_manager: + result = drone_manager.land() + if not result: + # 如果自定义降落失败,使用Tello SDK + tello.land() + else: + # 直接使用Tello SDK + tello.land() + + print("无人机降落成功") + return jsonify(success=True, message="无人机已降落") + + except Exception as e: + print(f"无人机降落时出错: {e}") + return jsonify(success=False, error=str(e)) + +@drone_bp.route('/move', methods=['POST']) +def move(): + """控制无人机移动""" + global tello, is_connected, drone_manager + + if not is_connected: + print("移动失败:未连接到无人机") + return jsonify(success=False, error="未连接到无人机") + + try: + data = request.json + direction = data.get('direction') + distance = data.get('distance', 30) # 默认30cm + + print(f"收到移动请求: 方向={direction}, 距离={distance}cm") + + # 验证参数 + if not direction: + print("移动失败:未指定移动方向") + return jsonify(success=False, error="未指定移动方向") + + if direction not in ['up', 'down', 'left', 'right', 'forward', 'back']: + print(f"移动失败:无效的移动方向 '{direction}'") + return jsonify(success=False, error="无效的移动方向") + + if not isinstance(distance, int) or distance < 20 or distance > 500: + print(f"移动失败:无效的移动距离 {distance}") + return jsonify(success=False, error="无效的移动距离") + + print(f"执行移动命令: {direction} {distance}cm") + + # 尝试使用自定义DroneManager + success = False + if drone_manager: + print("使用DroneManager执行移动") + result = drone_manager.move(direction, distance) + if result: + success = True + else: + print("DroneManager移动失败,尝试使用Tello SDK") + + if not success: + # 直接使用Tello SDK + print("使用Tello SDK执行移动") + try: + # 首先尝试使用方法调用 + move_method = getattr(tello, direction) + move_method(distance) + success = True + except (AttributeError, Exception) as method_error: + print(f"方法调用失败: {method_error},尝试直接发送命令") + + # 如果方法调用失败,尝试直接发送命令 + command = f"{direction} {distance}" + try: + tello.send_command_without_return(command) + print(f"直接发送命令: {command}") + success = True + except Exception as cmd_error: + print(f"直接发送命令失败: {cmd_error}") + raise cmd_error + + if success: + print(f"无人机移动成功: {direction} {distance}cm") + return jsonify(success=True, message=f"无人机已移动: {direction} {distance}cm") + else: + error_msg = "移动命令执行失败,所有尝试均失败" + print(error_msg) + return jsonify(success=False, error=error_msg) + + except AttributeError as e: + error_msg = f"移动方法不存在: {str(e)}" + print(error_msg) + return jsonify(success=False, error=error_msg) + except Exception as e: + error_msg = f"无人机移动时出错: {str(e)}" + print(error_msg) + import traceback + traceback.print_exc() + return jsonify(success=False, error=error_msg) + +@drone_bp.route('/rotate', methods=['POST']) +def rotate(): + """控制无人机旋转""" + global tello, is_connected, drone_manager + + if not is_connected: + print("旋转失败:未连接到无人机") + return jsonify(success=False, error="未连接到无人机") + + try: + data = request.json + direction = data.get('direction') + angle = data.get('angle', 45) # 默认45度 + + print(f"收到旋转请求: 方向={direction}, 角度={angle}度") + + # 验证参数 + if not direction: + print("旋转失败:未指定旋转方向") + return jsonify(success=False, error="未指定旋转方向") + + if direction not in ['cw', 'ccw']: + print(f"旋转失败:无效的旋转方向 '{direction}'") + return jsonify(success=False, error="无效的旋转方向") + + if not isinstance(angle, int) or angle < 1 or angle > 360: + print(f"旋转失败:无效的旋转角度 {angle}") + return jsonify(success=False, error="无效的旋转角度") + + print(f"执行旋转命令: {direction} {angle}度") + + # 尝试使用自定义DroneManager + success = False + if drone_manager: + print("使用DroneManager执行旋转") + result = drone_manager.rotate(direction, angle) + if result: + success = True + else: + print("DroneManager旋转失败,尝试使用Tello SDK") + + if not success: + # 直接使用Tello SDK + print("使用Tello SDK执行旋转") + try: + # 首先尝试使用方法调用 + rotate_method = getattr(tello, direction) + rotate_method(angle) + success = True + except (AttributeError, Exception) as method_error: + print(f"方法调用失败: {method_error},尝试直接发送命令") + + # 如果方法调用失败,尝试直接发送命令 + command = f"{direction} {angle}" + try: + tello.send_command_without_return(command) + print(f"直接发送命令: {command}") + success = True + except Exception as cmd_error: + print(f"直接发送命令失败: {cmd_error}") + raise cmd_error + + if success: + print(f"无人机旋转成功: {direction} {angle}度") + return jsonify(success=True, message=f"无人机已旋转: {direction} {angle}度") + else: + error_msg = "旋转命令执行失败,所有尝试均失败" + print(error_msg) + return jsonify(success=False, error=error_msg) + + except AttributeError as e: + error_msg = f"旋转方法不存在: {str(e)}" + print(error_msg) + return jsonify(success=False, error=error_msg) + except Exception as e: + error_msg = f"无人机旋转时出错: {str(e)}" + print(error_msg) + import traceback + traceback.print_exc() + return jsonify(success=False, error=error_msg) + +@drone_bp.route('/stream/start', methods=['POST']) +def start_stream(): + """开始视频流""" + global tello, is_connected, is_streaming, stream_thread, running, video_receiver + + if not is_connected: + return jsonify(success=False, error="未连接到无人机") + + if is_streaming: + return jsonify(success=True, message="视频流已经在运行") + + try: + # 获取请求中的流类型参数 + data = request.json or {} + stream_type = data.get('stream_type', 'udp') # 默认使用UDP + + print(f"开始视频流,类型: {stream_type}") + + # **关键修复**: 首先启动无人机的视频传输 + print("正在启动无人机视频传输...") + try: + # 确保先停止任何现有的视频流 + tello.streamoff() + time.sleep(1) + + # 启动无人机视频传输 + tello.streamon() + print("已发送streamon命令,等待无人机开始发送视频数据...") + time.sleep(3) # 给无人机时间开始发送数据 + + except Exception as e: + print(f"启动无人机视频传输失败: {e}") + return jsonify(success=False, error=f"无法启动无人机视频传输: {str(e)}") + + # 现在尝试接收视频流 + try: + if video_receiver is None: + video_receiver = VideoReceiver() + + # 根据流类型选择不同的视频流地址 + if stream_type == 'udp': + # 使用UDP流 + if hasattr(tello, 'get_udp_video_address'): + stream_url = tello.get_udp_video_address() + else: + stream_url = "udp://0.0.0.0:11111" # 默认UDP地址 + + print(f"获取到UDP视频流地址: {stream_url}") + + elif stream_type == 'rtsp': + # 尝试使用RTSP流(部分Tello型号支持) + stream_url = f"rtsp://192.168.10.1:554/live" + print(f"使用RTSP视频流地址: {stream_url}") + + elif stream_type == 'http': + # 启动HTTP流转发服务 + stream_url = start_http_stream_server() + if not stream_url: + raise Exception("无法启动HTTP流转发服务") + print(f"使用HTTP视频流地址: {stream_url}") + + else: + raise ValueError(f"不支持的视频流类型: {stream_type}") + + # 减少超时时间,因为现在应该有数据了 + video_receiver.stream_timeout = 15.0 + print(f"视频流超时时间已设置为: {video_receiver.stream_timeout}秒") + + # 启动视频接收器 + result = video_receiver.start(stream_url) + if not result: + # 如果自定义接收器失败,回退到Tello SDK方法 + print(f"自定义视频接收器启动失败: {video_receiver.last_error}") + print("回退到使用原生Tello SDK视频流处理...") + + # 确保帧读取器被初始化 + frame_reader = tello.get_frame_read() + if frame_reader is None: + print("错误:帧读取器初始化失败!") + return jsonify(success=False, error="视频流初始化失败") + + # 尝试获取第一帧以验证流是否正常 + try: + print("尝试获取第一帧...") + test_frame = frame_reader.frame + if test_frame is None: + print("警告:未能获取第一帧") + else: + height, width = test_frame.shape[:2] + print(f"成功获取第一帧,分辨率: {width}x{height}") + except Exception as e: + print(f"测试帧获取失败: {e}") + + is_streaming = True + + # 启动视频流线程 + running = True + stream_thread = threading.Thread(target=stream_video) + stream_thread.daemon = True + stream_thread.start() + print("使用Tello SDK启动视频流") + + return jsonify(success=True, message="视频流已启动(使用Tello SDK)", stream_type="tello_sdk") + else: + is_streaming = True + print(f"使用{stream_type.upper()}协议启动视频流: {stream_url}") + return jsonify(success=True, message="视频流已启动", stream_type=stream_type) + + except Exception as e: + # 如果所有方法都失败,作为最后的回退,使用原生Tello SDK + print(f"视频接收器启动失败: {e}") + print("使用原生Tello SDK作为最后回退...") + + # 确保帧读取器被初始化 + frame_reader = tello.get_frame_read() + if frame_reader is None: + print("错误:帧读取器初始化失败!") + return jsonify(success=False, error="视频流初始化失败") + + # 尝试获取第一帧以验证流是否正常 + try: + print("尝试获取第一帧...") + test_frame = frame_reader.frame + if test_frame is None: + print("警告:未能获取第一帧") + else: + height, width = test_frame.shape[:2] + print(f"成功获取第一帧,分辨率: {width}x{height}") + except Exception as e: + print(f"测试帧获取失败: {e}") + + is_streaming = True + + # 启动视频流线程 + running = True + stream_thread = threading.Thread(target=stream_video) + stream_thread.daemon = True + stream_thread.start() + print("使用Tello SDK启动视频流") + + return jsonify(success=True, message="视频流已启动(回退到Tello SDK)", stream_type="tello_sdk") + + except Exception as e: + print(f"启动视频流时出错: {e}") + import traceback + traceback.print_exc() + return jsonify(success=False, error=str(e)) + +def stream_video(): + """视频流处理线程""" + global tello, stream_frame, stream_lock, running + + print("视频流线程已启动") + frame_count = 0 + error_count = 0 + + while running: + try: + # 获取帧 + frame_reader = tello.get_frame_read() + if frame_reader is None: + print("错误: 帧读取器为空") + time.sleep(0.5) + continue + + frame = frame_reader.frame + + if frame is not None: + with stream_lock: + stream_frame = frame.copy() # 创建副本以防止引用问题 + frame_count += 1 + if frame_count % 30 == 0: # 每30帧打印一次日志 + height, width = frame.shape[:2] + print(f"视频流正常: 已接收{frame_count}帧, 分辨率: {width}x{height}") + error_count = 0 # 重置错误计数 + else: + error_count += 1 + print(f"警告: 收到空帧 (第{error_count}次)") + if error_count > 10: # 如果连续接收10个空帧,尝试重新初始化 + print("尝试重新初始化视频流...") + tello.streamoff() + time.sleep(1) + tello.streamon() + time.sleep(2) + error_count = 0 + + time.sleep(0.03) # 约30fps + + except Exception as e: + print(f"视频流处理出错: {e}") + error_count += 1 + if error_count > 10: # 如果连续出现10次错误,尝试重新初始化 + print("视频流出错过多,尝试重新初始化...") + try: + tello.streamoff() + time.sleep(1) + tello.streamon() + time.sleep(2) + error_count = 0 + except: + pass + time.sleep(0.1) + +# HTTP流服务器 +def start_http_stream_server(): + """启动HTTP视频流转发服务器""" + global http_server, http_server_port, http_stream_url + + try: + # 如果已经有服务器在运行,先停止它 + if http_server: + try: + http_server.shutdown() + http_server = None + except: + pass + + # 导入必要的库 + from http.server import HTTPServer, BaseHTTPRequestHandler + import threading + + class VideoStreamHandler(BaseHTTPRequestHandler): + """处理视频流请求的HTTP处理器""" + + def do_GET(self): + """处理GET请求,返回MJPEG流""" + if self.path == '/stream': + self.send_response(200) + self.send_header('Content-type', 'multipart/x-mixed-replace; boundary=frame') + self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate') + self.send_header('Pragma', 'no-cache') + self.send_header('Expires', '0') + self.end_headers() + + try: + while is_streaming: + # 获取当前帧 + frame = None + if video_receiver: + frame = video_receiver.get_latest_frame() + elif stream_frame is not None: + with stream_lock: + frame = stream_frame.copy() + + if frame is not None: + # 将帧编码为JPEG + _, jpeg = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, 70]) + + # 发送帧 + self.wfile.write(b'--frame\r\n') + self.wfile.write(b'Content-Type: image/jpeg\r\n') + self.wfile.write(f'Content-Length: {len(jpeg)}\r\n\r\n'.encode()) + self.wfile.write(jpeg.tobytes()) + self.wfile.write(b'\r\n') + + # 控制帧率 + time.sleep(0.05) # 约20fps + except: + pass + else: + # 返回简单的HTML页面,显示视频流 + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + + html = f""" + + + Tello Video Stream + + + +

    Tello Video Stream

    + + + + """ + self.wfile.write(html.encode()) + + def log_message(self, format, *args): + """覆盖日志方法,减少控制台输出""" + return + + # 查找可用端口 + import socket + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.bind(('', 0)) + http_server_port = s.getsockname()[1] + s.close() + + # 创建HTTP服务器 + http_server = HTTPServer(('', http_server_port), VideoStreamHandler) + + # 在新线程中启动服务器 + server_thread = threading.Thread(target=http_server.serve_forever) + server_thread.daemon = True + server_thread.start() + + # 构建流URL + http_stream_url = f"http://localhost:{http_server_port}/stream" + print(f"HTTP流服务器已启动: {http_stream_url}") + + return http_stream_url + + except Exception as e: + print(f"启动HTTP流服务器失败: {e}") + import traceback + traceback.print_exc() + return None + +# 停止HTTP流服务器 +def stop_http_stream_server(): + """停止HTTP视频流转发服务器""" + global http_server + + if http_server: + try: + http_server.shutdown() + http_server = None + print("HTTP流服务器已停止") + except Exception as e: + print(f"停止HTTP流服务器出错: {e}") + +@drone_bp.route('/stream/stop', methods=['POST']) +def stop_stream(): + """停止视频流""" + global tello, is_connected, is_streaming, stream_thread, running, video_receiver + + if not is_connected or not is_streaming: + return jsonify(success=True, message="视频流未运行") + + try: + # 停止HTTP流服务器 + stop_http_stream_server() + + # 停止自定义视频接收器 + if video_receiver: + try: + video_receiver.stop() + except: + pass + + # 停止Tello SDK视频流 + try: + tello.streamoff() + except: + pass + + # 停止视频流线程 + running = False + if stream_thread and stream_thread.is_alive(): + stream_thread.join(timeout=2) + + is_streaming = False + print("视频流已停止") + return jsonify(success=True, message="视频流已停止") + + except Exception as e: + print(f"停止视频流时出错: {e}") + return jsonify(success=False, error=str(e)) + +@drone_bp.route('/stream/frame') +def get_stream_frame(): + """获取视频流当前帧""" + global stream_frame, stream_lock, is_streaming, video_receiver + + # 添加禁用缓存的响应头 + response_headers = { + 'Cache-Control': 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0', + 'Pragma': 'no-cache', + 'Expires': '-1', + 'Access-Control-Allow-Origin': '*' + } + + if not is_streaming: + # 如果视频流未启动,返回默认图像 + try: + default_img_path = os.path.join(script_dir, 'static', 'images', 'no-video.jpg') + if os.path.exists(default_img_path): + with open(default_img_path, 'rb') as f: + print("返回默认无视频图像") + return send_file( + io.BytesIO(f.read()), + mimetype='image/jpeg', + headers=response_headers + ) + else: + # 创建一个黑色图像 + img = np.zeros((300, 400, 3), dtype=np.uint8) + cv2.putText(img, "No Video Signal", (50, 150), cv2.FONT_HERSHEY_SIMPLEX, + 1, (255, 255, 255), 2) + _, buffer = cv2.imencode('.jpg', img) + print("返回生成的无视频图像") + return send_file( + io.BytesIO(buffer), + mimetype='image/jpeg', + headers=response_headers + ) + except Exception as e: + print(f"返回默认图像出错: {e}") + # 创建一个最简单的错误图像 + img = np.zeros((100, 100, 3), dtype=np.uint8) + _, buffer = cv2.imencode('.jpg', img) + return send_file( + io.BytesIO(buffer), + mimetype='image/jpeg', + headers=response_headers + ) + + try: + # 尝试使用自定义视频接收器 + if video_receiver: + frame = video_receiver.get_latest_frame() + if frame is not None: + # 将OpenCV BGR图像转换为JPEG + _, buffer = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, 80]) + if buffer is not None and len(buffer) > 0: + print(f"使用VideoReceiver获取到有效帧,大小: {len(buffer)} 字节") + return send_file( + io.BytesIO(buffer), + mimetype='image/jpeg', + headers=response_headers + ) + else: + print("VideoReceiver帧编码失败") + + # 使用Tello SDK视频帧 + with stream_lock: + if stream_frame is not None: + # 将OpenCV BGR图像转换为JPEG + _, buffer = cv2.imencode('.jpg', stream_frame, [cv2.IMWRITE_JPEG_QUALITY, 80]) + if buffer is not None and len(buffer) > 0: + print(f"使用Tello SDK获取到有效帧,大小: {len(buffer)} 字节") + return send_file( + io.BytesIO(buffer), + mimetype='image/jpeg', + headers=response_headers + ) + else: + print("Tello SDK帧编码失败") + + # 如果无法获取帧,返回默认图像 + print("无法获取有效帧,返回默认图像") + with open(os.path.join(script_dir, 'static', 'images', 'no-video.jpg'), 'rb') as f: + return send_file( + io.BytesIO(f.read()), + mimetype='image/jpeg', + headers=response_headers + ) + + except Exception as e: + print(f"获取视频帧时出错: {e}") + import traceback + traceback.print_exc() + # 返回错误图像 + with open(os.path.join(script_dir, 'static', 'images', 'no-video.jpg'), 'rb') as f: + return send_file( + io.BytesIO(f.read()), + mimetype='image/jpeg', + headers=response_headers + ) + +@drone_bp.route('/capture', methods=['POST']) +def capture_image(): + """捕获无人机当前视图""" + global tello, is_connected, is_streaming, stream_frame, stream_lock, video_receiver + + if not is_connected: + return jsonify(success=False, error="未连接到无人机") + + if not is_streaming: + return jsonify(success=False, error="视频流未启动") + + try: + # 生成文件名 + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + filename = f"drone_capture_{timestamp}.jpg" + filepath = os.path.join(captures_dir, filename) + + # 尝试使用自定义视频接收器 + frame = None + if video_receiver: + frame = video_receiver.get_latest_frame() + + # 如果自定义接收器没有帧,使用Tello SDK + if frame is None: + with stream_lock: + if stream_frame is not None: + frame = stream_frame.copy() + + if frame is not None: + # 保存图像 + cv2.imwrite(filepath, frame) + print(f"已捕获图像: {filepath}") + + # 返回图像URL和路径 + image_url = f"/uploads/drone_captures/{filename}" + return jsonify(success=True, message="图像已捕获", image_url=image_url, image_path=filepath) + else: + return jsonify(success=False, error="无法获取有效图像") + + except Exception as e: + print(f"捕获图像时出错: {e}") + return jsonify(success=False, error=str(e)) + +@drone_bp.route('/status') +def get_status(): + """获取无人机状态""" + global tello, is_connected + + if not is_connected: + return jsonify(success=False, error="未连接到无人机") + + try: + # 获取电池电量 + battery = tello.get_battery() + + # 获取其他状态 + height = tello.get_height() + temperature = tello.get_temperature() + + return jsonify( + success=True, + battery=battery, + height=height, + temperature=temperature + ) + + except Exception as e: + print(f"获取无人机状态时出错: {e}") + return jsonify(success=False, error=str(e)) + +@drone_bp.route('/analyze', methods=['POST']) +def analyze_image(): + """分析捕获的图像""" + global is_connected + + try: + data = request.json + image_path = data.get('image_path') + + if not image_path or not os.path.exists(image_path): + return jsonify(success=False, error="无效的图像路径") + + # 确保结果目录存在 + results_dir = os.path.join(os.path.dirname(image_path), 'results') + os.makedirs(results_dir, exist_ok=True) + + # 结果图像路径 + result_filename = f"analysis_{os.path.basename(image_path)}" + result_path = os.path.join(results_dir, result_filename) + + # 导入分析器 + from ship_analyzer import ShipAnalyzer + analyzer = ShipAnalyzer() + + # 分析图像 + print(f"正在分析图像: {image_path}") + result, result_img = analyzer.analyze_image( + image_path, + conf_threshold=0.05, # 低置信度阈值以提高检出率 + save_result=True, + output_path=result_path + ) + + # 处理结果 + if isinstance(result, dict) and 'ships' in result: + ships = result['ships'] + else: + ships = result if isinstance(result, list) else [] + + # 构建返回结果 + response = { + 'success': True, + 'message': f"分析完成,检测到 {len(ships)} 艘舰船", + 'ships': ships, + 'result_image_path': result_path, + 'result_image_url': f"/results/{os.path.basename(os.path.dirname(result_path))}/{result_filename}" + } + + return jsonify(response) + + except Exception as e: + print(f"分析图像时出错: {e}") + import traceback + traceback.print_exc() + return jsonify(success=False, error=str(e)) + +# 创建默认的图像 +def create_default_images(): + """创建默认的图像文件""" + try: + # 创建no-video.jpg + no_video_path = os.path.join(script_dir, 'static', 'images', 'no-video.jpg') + if not os.path.exists(no_video_path): + # 创建黑色图像 + img = np.zeros((300, 400, 3), dtype=np.uint8) + # 添加文本 + cv2.putText(img, "No Video Signal", (100, 150), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2) + cv2.imwrite(no_video_path, img) + print(f"创建默认图像: {no_video_path}") + + # 创建video-test.jpg + test_video_path = os.path.join(script_dir, 'static', 'images', 'video-test.jpg') + if not os.path.exists(test_video_path): + # 创建测试图像 + img = np.zeros((300, 400, 3), dtype=np.uint8) + # 添加彩色元素 + cv2.rectangle(img, (50, 50), (350, 250), (0, 0, 255), -1) + cv2.rectangle(img, (100, 100), (300, 200), (0, 255, 0), -1) + cv2.rectangle(img, (150, 150), (250, 250), (255, 0, 0), -1) + # 添加文本 + cv2.putText(img, "Video Test", (130, 280), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2) + cv2.imwrite(test_video_path, img) + print(f"创建测试图像: {test_video_path}") + + except Exception as e: + print(f"创建默认图像时出错: {e}") + +# 初始化创建默认图像 +create_default_images() + +# 添加测试路由 +@drone_bp.route('/ping') +def ping_drone(): + """测试无人机网络连接""" + try: + # 尝试 ping Tello 默认IP + import subprocess + import platform + + ip = "192.168.10.1" # Tello默认IP + + # 根据平台选择ping命令参数 + param = '-n' if platform.system().lower() == 'windows' else '-c' + command = ['ping', param, '1', ip] + + print(f"执行Ping测试: {' '.join(command)}") + result = subprocess.call(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0 + + if result: + print(f"Ping测试成功: {ip}") + return jsonify(success=True, message=f"成功连接到 {ip}") + else: + print(f"Ping测试失败: {ip}") + return jsonify(success=False, error=f"无法连接到 {ip}") + except Exception as e: + print(f"Ping测试异常: {e}") + return jsonify(success=False, error=str(e)) + +@drone_bp.route('/test_direct_video') +def test_direct_video(): + """测试直接UDP视频流""" + try: + # 创建一个临时的视频接收器 + from drone_interface.video_receiver import VideoReceiver + + print("创建临时视频接收器进行测试...") + test_receiver = VideoReceiver() + test_receiver.stream_timeout = 5.0 + + # 尝试打开UDP流 + stream_url = "udp://0.0.0.0:11111" + print(f"尝试打开UDP流: {stream_url}") + + success = test_receiver.start(stream_url) + if not success: + print(f"打开UDP流失败: {test_receiver.last_error}") + # 返回一个默认图片 + with open(os.path.join(script_dir, 'static', 'images', 'no-video.jpg'), 'rb') as f: + return send_file(io.BytesIO(f.read()), mimetype='image/jpeg') + + # 等待一帧 + print("等待视频帧...") + start_time = time.time() + frame = None + + # 最多等待3秒 + while time.time() - start_time < 3: + frame = test_receiver.get_latest_frame() + if frame is not None: + break + time.sleep(0.1) + + # 停止接收器 + test_receiver.stop() + + if frame is not None: + print("成功获取测试帧") + # 将OpenCV BGR图像转换为JPEG + _, buffer = cv2.imencode('.jpg', frame) + return send_file(io.BytesIO(buffer), mimetype='image/jpeg') + else: + print("未获取到测试帧") + # 返回一个默认图片 + with open(os.path.join(script_dir, 'static', 'images', 'no-video.jpg'), 'rb') as f: + return send_file(io.BytesIO(f.read()), mimetype='image/jpeg') + + except Exception as e: + print(f"测试直接视频出错: {e}") + import traceback + traceback.print_exc() + # 返回一个默认图片 + with open(os.path.join(script_dir, 'static', 'images', 'no-video.jpg'), 'rb') as f: + return send_file(io.BytesIO(f.read()), mimetype='image/jpeg') + +@drone_bp.route('/test_custom_video', methods=['POST']) +def test_custom_video(): + """测试自定义URL视频流""" + try: + data = request.json + url = data.get('url') + + if not url: + return jsonify(success=False, error="未提供URL") + + print(f"测试自定义视频URL: {url}") + + # 创建一个临时的视频接收器 + from drone_interface.video_receiver import VideoReceiver + + test_receiver = VideoReceiver() + test_receiver.stream_timeout = 5.0 + + # 尝试打开视频流 + success = test_receiver.start(url) + if not success: + return jsonify(success=False, error=f"打开视频流失败: {test_receiver.last_error}") + + # 等待一帧 + start_time = time.time() + has_frame = False + + # 最多等待3秒 + while time.time() - start_time < 3: + if test_receiver.get_latest_frame() is not None: + has_frame = True + break + time.sleep(0.1) + + # 停止接收器 + test_receiver.stop() + + if has_frame: + return jsonify(success=True, message="成功接收视频帧") + else: + return jsonify(success=False, error="未接收到视频帧") + + except Exception as e: + print(f"测试自定义视频URL出错: {e}") + import traceback + traceback.print_exc() + return jsonify(success=False, error=str(e)) + +@drone_bp.route('/test_stream', methods=['POST']) +def test_stream(): + """测试不同类型的视频流""" + try: + data = request.json + stream_type = data.get('stream_type', 'udp') + + print(f"测试{stream_type}视频流") + + # 根据流类型选择不同的测试方法 + if stream_type == 'udp': + return test_udp_stream() + elif stream_type == 'rtsp': + return test_rtsp_stream() + elif stream_type == 'http': + return test_http_stream() + else: + return jsonify(success=False, error=f"不支持的视频流类型: {stream_type}") + + except Exception as e: + print(f"测试视频流出错: {e}") + import traceback + traceback.print_exc() + return jsonify(success=False, error=str(e)) + +def test_udp_stream(): + """测试UDP视频流""" + try: + # 检查网络连接 + import subprocess + import platform + + ip = "192.168.10.1" # Tello默认IP + + # 根据平台选择ping命令参数 + param = '-n' if platform.system().lower() == 'windows' else '-c' + command = ['ping', param, '1', ip] + + print(f"执行Ping测试: {' '.join(command)}") + ping_success = subprocess.call(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0 + + if not ping_success: + print(f"Ping测试失败: {ip}") + return jsonify(success=False, error=f"无法连接到Tello ({ip})") + + # 创建一个临时的视频接收器 + from drone_interface.video_receiver import VideoReceiver + + print("创建临时视频接收器进行UDP测试...") + test_receiver = VideoReceiver() + test_receiver.stream_timeout = 5.0 + + # 尝试打开UDP流 + stream_url = "udp://0.0.0.0:11111" + print(f"尝试打开UDP流: {stream_url}") + + success = test_receiver.start(stream_url) + if not success: + print(f"打开UDP流失败: {test_receiver.last_error}") + return jsonify(success=False, error=f"打开UDP流失败: {test_receiver.last_error}") + + # 等待一帧 + print("等待视频帧...") + start_time = time.time() + frame = None + + # 最多等待3秒 + while time.time() - start_time < 3: + frame = test_receiver.get_latest_frame() + if frame is not None: + break + time.sleep(0.1) + + # 停止接收器 + test_receiver.stop() + + if frame is not None: + print("成功获取UDP测试帧") + # 保存帧为临时文件 + test_frame_path = os.path.join(script_dir, 'static', 'images', 'udp-test-frame.jpg') + cv2.imwrite(test_frame_path, frame) + + # 返回成功结果和帧URL + return jsonify( + success=True, + message="UDP视频流测试成功", + frame_url="/static/images/udp-test-frame.jpg" + ) + else: + print("未获取到UDP测试帧") + return jsonify(success=False, error="未能获取UDP视频帧,请检查无人机是否正常工作") + + except Exception as e: + print(f"测试UDP视频出错: {e}") + import traceback + traceback.print_exc() + return jsonify(success=False, error=str(e)) + +def test_rtsp_stream(): + """测试RTSP视频流""" + try: + # 创建一个临时的视频接收器 + from drone_interface.video_receiver import VideoReceiver + + print("创建临时视频接收器进行RTSP测试...") + test_receiver = VideoReceiver() + test_receiver.stream_timeout = 5.0 + + # 尝试打开RTSP流 + stream_url = "rtsp://192.168.10.1:554/live" + print(f"尝试打开RTSP流: {stream_url}") + + success = test_receiver.start(stream_url) + if not success: + print(f"打开RTSP流失败: {test_receiver.last_error}") + return jsonify(success=False, error=f"打开RTSP流失败: {test_receiver.last_error}") + + # 等待一帧 + print("等待视频帧...") + start_time = time.time() + frame = None + + # 最多等待3秒 + while time.time() - start_time < 3: + frame = test_receiver.get_latest_frame() + if frame is not None: + break + time.sleep(0.1) + + # 停止接收器 + test_receiver.stop() + + if frame is not None: + print("成功获取RTSP测试帧") + # 保存帧为临时文件 + test_frame_path = os.path.join(script_dir, 'static', 'images', 'rtsp-test-frame.jpg') + cv2.imwrite(test_frame_path, frame) + + # 返回成功结果和帧URL + return jsonify( + success=True, + message="RTSP视频流测试成功", + frame_url="/static/images/rtsp-test-frame.jpg" + ) + else: + print("未获取到RTSP测试帧") + return jsonify(success=False, error="未能获取RTSP视频帧,可能您的Tello型号不支持RTSP") + + except Exception as e: + print(f"测试RTSP视频出错: {e}") + import traceback + traceback.print_exc() + return jsonify(success=False, error=str(e)) + +def test_http_stream(): + """测试HTTP视频流""" + try: + # 启动HTTP流服务器 + stream_url = start_http_stream_server() + if not stream_url: + return jsonify(success=False, error="无法启动HTTP流服务器") + + print(f"HTTP流服务器已启动: {stream_url}") + + # 创建测试图像 + test_frame_path = os.path.join(script_dir, 'static', 'images', 'http-test-frame.jpg') + test_img = np.zeros((300, 400, 3), dtype=np.uint8) + cv2.rectangle(test_img, (50, 50), (350, 250), (0, 0, 255), -1) + cv2.putText(test_img, "HTTP Stream Test", (80, 150), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 255, 255), 2) + cv2.imwrite(test_frame_path, test_img) + + # 返回成功结果和HTTP流URL + return jsonify( + success=True, + message="HTTP流服务器已启动", + stream_url=stream_url, + frame_url="/static/images/http-test-frame.jpg" + ) + + except Exception as e: + print(f"测试HTTP视频出错: {e}") + import traceback + traceback.print_exc() + return jsonify(success=False, error=str(e)) + +@drone_bp.route('/stream/view') +def stream_view(): + """返回一个简单的HTML页面,直接显示视频帧""" + return """ + + + + + + + + + 视频流 + + + """ + +@drone_bp.route('/stream/mjpeg') +def stream_mjpeg(): + """提供MJPEG流""" + global stream_frame, stream_lock, is_streaming, video_receiver + + def generate_frames(): + while is_streaming: + try: + # 尝试使用自定义视频接收器 + frame = None + if video_receiver: + frame = video_receiver.get_latest_frame() + + # 如果自定义接收器没有帧,使用Tello SDK + if frame is None: + with stream_lock: + if stream_frame is not None: + frame = stream_frame.copy() + + if frame is not None: + # 将OpenCV BGR图像转换为JPEG + _, buffer = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, 80]) + if buffer is not None: + frame_bytes = buffer.tobytes() + yield (b'--frame\r\n' + b'Content-Type: image/jpeg\r\n\r\n' + frame_bytes + b'\r\n') + + # 控制帧率 + time.sleep(0.05) # 20fps + + except Exception as e: + print(f"生成视频帧出错: {e}") + time.sleep(0.1) + + # 如果视频流未启动,返回默认图像 + if not is_streaming: + try: + default_img_path = os.path.join(script_dir, 'static', 'images', 'no-video.jpg') + if os.path.exists(default_img_path): + with open(default_img_path, 'rb') as f: + return send_file( + io.BytesIO(f.read()), + mimetype='image/jpeg' + ) + except Exception as e: + print(f"返回默认图像出错: {e}") + + # 返回多部分响应 + return Response(generate_frames(), + mimetype='multipart/x-mixed-replace; boundary=frame') \ No newline at end of file diff --git a/distance-judgement/src/drone/drone_interface/__init__.py b/distance-judgement/src/drone/drone_interface/__init__.py new file mode 100644 index 00000000..8859de16 --- /dev/null +++ b/distance-judgement/src/drone/drone_interface/__init__.py @@ -0,0 +1,13 @@ +""" +无人机接口子系统(DroneInterface) +------------- +与无人机建立通信连接 +接收无人机传回的视频流 +将视频流转发给图像分析子系统 +向无人机发送控制命令(如需要) +""" + +from .drone_manager import DroneManager +from .video_receiver import VideoReceiver + +__all__ = ['DroneManager', 'VideoReceiver'] \ No newline at end of file diff --git a/distance-judgement/src/drone/drone_interface/__pycache__/__init__.cpython-311.pyc b/distance-judgement/src/drone/drone_interface/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 00000000..13d74859 Binary files /dev/null and b/distance-judgement/src/drone/drone_interface/__pycache__/__init__.cpython-311.pyc differ diff --git a/distance-judgement/src/drone/drone_interface/__pycache__/__init__.cpython-313.pyc b/distance-judgement/src/drone/drone_interface/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 00000000..5edfa7c7 Binary files /dev/null and b/distance-judgement/src/drone/drone_interface/__pycache__/__init__.cpython-313.pyc differ diff --git a/distance-judgement/src/drone/drone_interface/__pycache__/drone_manager.cpython-311.pyc b/distance-judgement/src/drone/drone_interface/__pycache__/drone_manager.cpython-311.pyc new file mode 100644 index 00000000..0bc194fb Binary files /dev/null and b/distance-judgement/src/drone/drone_interface/__pycache__/drone_manager.cpython-311.pyc differ diff --git a/distance-judgement/src/drone/drone_interface/__pycache__/drone_manager.cpython-313.pyc b/distance-judgement/src/drone/drone_interface/__pycache__/drone_manager.cpython-313.pyc new file mode 100644 index 00000000..120c7bdf Binary files /dev/null and b/distance-judgement/src/drone/drone_interface/__pycache__/drone_manager.cpython-313.pyc differ diff --git a/distance-judgement/src/drone/drone_interface/__pycache__/video_receiver.cpython-311.pyc b/distance-judgement/src/drone/drone_interface/__pycache__/video_receiver.cpython-311.pyc new file mode 100644 index 00000000..8d49bfa0 Binary files /dev/null and b/distance-judgement/src/drone/drone_interface/__pycache__/video_receiver.cpython-311.pyc differ diff --git a/distance-judgement/src/drone/drone_interface/__pycache__/video_receiver.cpython-313.pyc b/distance-judgement/src/drone/drone_interface/__pycache__/video_receiver.cpython-313.pyc new file mode 100644 index 00000000..e39d030b Binary files /dev/null and b/distance-judgement/src/drone/drone_interface/__pycache__/video_receiver.cpython-313.pyc differ diff --git a/distance-judgement/src/drone/drone_interface/drone_manager.py b/distance-judgement/src/drone/drone_interface/drone_manager.py new file mode 100644 index 00000000..69e80da3 --- /dev/null +++ b/distance-judgement/src/drone/drone_interface/drone_manager.py @@ -0,0 +1,655 @@ +import os +import json +import time +import socket +import logging +import threading +import requests +from enum import Enum +from datetime import datetime +from pathlib import Path + +# 配置日志 +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger("DroneManager") + +class DroneType(Enum): + """支持的无人机类型""" + UNKNOWN = 0 + DJI = 1 # 大疆无人机 + AUTEL = 2 # 澎湃无人机 + CUSTOM = 3 # 自定义无人机 + SIMULATOR = 9 # 模拟器 + +class DroneConnectionStatus(Enum): + """无人机连接状态""" + DISCONNECTED = 0 + CONNECTING = 1 + CONNECTED = 2 + ERROR = 3 + +class DroneManager: + """ + 无人机管理器类 + 负责与无人机建立连接、发送命令和接收状态信息 + """ + def __init__(self, config_path=None, drone_type=DroneType.DJI): + """ + 初始化无人机管理器 + + Args: + config_path: 配置文件路径,默认使用内置配置 + drone_type: 无人机类型 + """ + # 项目根目录 + self.root_dir = Path(__file__).resolve().parents[2] + + # 无人机类型 + self.drone_type = drone_type + + # 连接状态 + self.connection_status = DroneConnectionStatus.DISCONNECTED + + # 通信地址 + self.ip = "192.168.10.1" # 默认IP地址 + self.cmd_port = 8889 # 默认命令端口 + self.state_port = 8890 # 默认状态端口 + self.video_port = 11111 # 默认视频端口 + + # 无人机状态 + self.drone_state = { + 'battery': 0, + 'height': 0, + 'speed': 0, + 'gps': {'latitude': 0, 'longitude': 0, 'altitude': 0}, + 'orientation': {'yaw': 0, 'pitch': 0, 'roll': 0}, + 'signal_strength': 0, + 'mode': 'UNKNOWN', + 'last_update': datetime.now().isoformat() + } + + # 通信套接字 + self.cmd_socket = None + self.state_socket = None + + # 状态接收线程 + self.state_receiver_thread = None + self.running = False + + # 视频流地址 + self.video_stream_url = None + + # 加载配置 + self.config = self._load_config(config_path) + self._apply_config() + + # 错误记录 + self.last_error = None + + # 命令响应回调 + self.command_callbacks = {} + + def _load_config(self, config_path): + """加载无人机配置""" + default_config = { + 'drone_type': self.drone_type.name, + 'connection': { + 'ip': self.ip, + 'cmd_port': self.cmd_port, + 'state_port': self.state_port, + 'video_port': self.video_port, + 'timeout': 5 + }, + 'commands': { + 'connect': 'command', + 'takeoff': 'takeoff', + 'land': 'land', + 'move': { + 'up': 'up {distance}', + 'down': 'down {distance}', + 'left': 'left {distance}', + 'right': 'right {distance}', + 'forward': 'forward {distance}', + 'back': 'back {distance}', + }, + 'rotate': { + 'cw': 'cw {angle}', + 'ccw': 'ccw {angle}' + }, + 'set': { + 'speed': 'speed {value}' + } + }, + 'video': { + 'stream_url': 'udp://{ip}:{port}', + 'rtsp_url': 'rtsp://{ip}:{port}/live', + 'snapshot_url': 'http://{ip}:{port}/snapshot' + }, + 'safety': { + 'max_height': 100, + 'max_distance': 500, + 'min_battery': 15, + 'return_home_battery': 30 + } + } + + if config_path: + try: + with open(config_path, 'r') as f: + user_config = json.load(f) + # 合并配置 + self._merge_configs(default_config, user_config) + except Exception as e: + logger.error(f"加载配置文件失败,使用默认配置: {e}") + + return default_config + + def _merge_configs(self, default_config, user_config): + """递归合并配置字典""" + for key, value in user_config.items(): + if key in default_config and isinstance(value, dict) and isinstance(default_config[key], dict): + self._merge_configs(default_config[key], value) + else: + default_config[key] = value + + def _apply_config(self): + """应用配置""" + try: + conn_config = self.config.get('connection', {}) + self.ip = conn_config.get('ip', self.ip) + self.cmd_port = conn_config.get('cmd_port', self.cmd_port) + self.state_port = conn_config.get('state_port', self.state_port) + self.video_port = conn_config.get('video_port', self.video_port) + + # 设置视频流URL + video_config = self.config.get('video', {}) + stream_url_template = video_config.get('stream_url') + if stream_url_template: + self.video_stream_url = stream_url_template.format( + ip=self.ip, + port=self.video_port + ) + except Exception as e: + logger.error(f"应用配置失败: {e}") + self.last_error = str(e) + + def connect(self): + """连接到无人机""" + if self.connection_status == DroneConnectionStatus.CONNECTED: + logger.info("已经连接到无人机") + return True + + self.connection_status = DroneConnectionStatus.CONNECTING + + try: + # 创建命令套接字 + self.cmd_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.cmd_socket.bind(('', 0)) + self.cmd_socket.settimeout(5) + + # 创建状态套接字 + self.state_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.state_socket.bind(('', self.state_port)) + self.state_socket.settimeout(5) + + # 发送连接命令 + connect_cmd = self.config['commands'].get('connect', 'command') + result = self._send_command(connect_cmd) + + if result: + self.connection_status = DroneConnectionStatus.CONNECTED + logger.info("成功连接到无人机") + + # 启动状态接收线程 + self.running = True + self.state_receiver_thread = threading.Thread(target=self._state_receiver) + self.state_receiver_thread.daemon = True + self.state_receiver_thread.start() + + return True + else: + self.connection_status = DroneConnectionStatus.ERROR + logger.error("连接无人机失败") + self.last_error = "连接命令没有响应" + return False + + except Exception as e: + self.connection_status = DroneConnectionStatus.ERROR + logger.error(f"连接无人机时出错: {e}") + self.last_error = str(e) + return False + + def disconnect(self): + """断开与无人机的连接""" + try: + # 停止状态接收线程 + self.running = False + if self.state_receiver_thread and self.state_receiver_thread.is_alive(): + self.state_receiver_thread.join(timeout=2) + + # 关闭套接字 + if self.cmd_socket: + self.cmd_socket.close() + self.cmd_socket = None + + if self.state_socket: + self.state_socket.close() + self.state_socket = None + + self.connection_status = DroneConnectionStatus.DISCONNECTED + logger.info("已断开与无人机的连接") + return True + + except Exception as e: + logger.error(f"断开连接时出错: {e}") + self.last_error = str(e) + return False + + def _send_command(self, command, timeout=5, callback=None): + """ + 发送命令到无人机 + + Args: + command: 命令字符串 + timeout: 超时时间(秒) + callback: 响应回调函数 + + Returns: + 成功返回True,失败返回False + """ + if not self.cmd_socket: + logger.error("命令套接字未初始化") + return False + + try: + # 记录命令ID用于回调 + cmd_id = time.time() + if callback: + self.command_callbacks[cmd_id] = callback + + logger.debug(f"发送命令: {command}") + self.cmd_socket.sendto(command.encode('utf-8'), (self.ip, self.cmd_port)) + + # 等待响应 + start_time = time.time() + while time.time() - start_time < timeout: + try: + data, _ = self.cmd_socket.recvfrom(1024) + response = data.decode('utf-8').strip() + logger.debug(f"收到响应: {response}") + + # 处理响应 + if callback: + callback(command, response) + del self.command_callbacks[cmd_id] + + return response == 'ok' + except socket.timeout: + continue + + logger.warning(f"命令超时: {command}") + return False + + except Exception as e: + logger.error(f"发送命令出错: {e}") + self.last_error = str(e) + return False + + def _state_receiver(self): + """状态接收线程函数""" + while self.running and self.state_socket: + try: + data, _ = self.state_socket.recvfrom(1024) + state_string = data.decode('utf-8').strip() + + # 解析状态数据 + self._parse_state_data(state_string) + + except socket.timeout: + # 超时是正常的,继续尝试 + continue + except Exception as e: + logger.error(f"接收状态数据时出错: {e}") + if self.running: # 只有在运行时才记录错误 + self.last_error = str(e) + + def _parse_state_data(self, state_string): + """ + 解析无人机状态数据 + + Args: + state_string: 状态数据字符串 + """ + try: + # 解析状态数据的格式取决于无人机型号 + # 这里以DJI Tello为例 + if self.drone_type == DroneType.DJI: + parts = state_string.split(';') + for part in parts: + if not part: + continue + + key_value = part.split(':') + if len(key_value) != 2: + continue + + key, value = key_value + + # 更新特定的状态字段 + if key == 'bat': + self.drone_state['battery'] = int(value) + elif key == 'h': + self.drone_state['height'] = int(value) + elif key == 'vgx': + self.drone_state['speed'] = int(value) + elif key == 'pitch': + self.drone_state['orientation']['pitch'] = int(value) + elif key == 'roll': + self.drone_state['orientation']['roll'] = int(value) + elif key == 'yaw': + self.drone_state['orientation']['yaw'] = int(value) + # 其他字段可以根据需要添加 + + # 更新最后更新时间 + self.drone_state['last_update'] = datetime.now().isoformat() + + except Exception as e: + logger.error(f"解析状态数据出错: {e}") + + def get_state(self): + """获取无人机当前状态""" + return self.drone_state + + def get_connection_status(self): + """获取连接状态""" + return self.connection_status + + def get_video_stream_url(self): + """获取视频流URL""" + return self.video_stream_url + + def takeoff(self, callback=None): + """起飞命令""" + if self.connection_status != DroneConnectionStatus.CONNECTED: + logger.error("无人机未连接") + return False + + # 检查电量是否足够 + min_battery = self.config.get('safety', {}).get('min_battery', 15) + if self.drone_state['battery'] < min_battery: + logger.error(f"电量不足,无法起飞。当前电量: {self.drone_state['battery']}%,最低要求: {min_battery}%") + return False + + takeoff_cmd = self.config['commands'].get('takeoff', 'takeoff') + return self._send_command(takeoff_cmd, callback=callback) + + def land(self, callback=None): + """降落命令""" + if self.connection_status != DroneConnectionStatus.CONNECTED: + logger.error("无人机未连接") + return False + + land_cmd = self.config['commands'].get('land', 'land') + return self._send_command(land_cmd, callback=callback) + + def move(self, direction, distance, callback=None): + """ + 移动命令 + + Args: + direction: 方向 ('up', 'down', 'left', 'right', 'forward', 'back') + distance: 距离(厘米) + callback: 响应回调函数 + + Returns: + 成功返回True,失败返回False + """ + if self.connection_status != DroneConnectionStatus.CONNECTED: + logger.error("无人机未连接") + return False + + # 检查最大距离限制 + max_distance = self.config.get('safety', {}).get('max_distance', 500) + if distance > max_distance: + logger.warning(f"移动距离超过安全限制,已调整为最大值 {max_distance}cm") + distance = max_distance + + # 获取移动命令模板 + move_cmds = self.config['commands'].get('move', {}) + cmd_template = move_cmds.get(direction) + + if not cmd_template: + logger.error(f"不支持的移动方向: {direction}") + return False + + # 填充命令参数 + command = cmd_template.format(distance=distance) + return self._send_command(command, callback=callback) + + def rotate(self, direction, angle, callback=None): + """ + 旋转命令 + + Args: + direction: 方向 ('cw': 顺时针, 'ccw': 逆时针) + angle: 角度 + callback: 响应回调函数 + + Returns: + 成功返回True,失败返回False + """ + if self.connection_status != DroneConnectionStatus.CONNECTED: + logger.error("无人机未连接") + return False + + # 获取旋转命令模板 + rotate_cmds = self.config['commands'].get('rotate', {}) + cmd_template = rotate_cmds.get(direction) + + if not cmd_template: + logger.error(f"不支持的旋转方向: {direction}") + return False + + # 确保角度在有效范围内 [1, 360] + angle = max(1, min(360, angle)) + + # 填充命令参数 + command = cmd_template.format(angle=angle) + return self._send_command(command, callback=callback) + + def set_speed(self, speed, callback=None): + """ + 设置速度命令 + + Args: + speed: 速度值(厘米/秒) + callback: 响应回调函数 + + Returns: + 成功返回True,失败返回False + """ + if self.connection_status != DroneConnectionStatus.CONNECTED: + logger.error("无人机未连接") + return False + + # 限制速度范围 [10, 100] + speed = max(10, min(100, speed)) + + # 获取速度命令模板 + set_cmds = self.config['commands'].get('set', {}) + cmd_template = set_cmds.get('speed') + + if not cmd_template: + logger.error("不支持设置速度命令") + return False + + # 填充命令参数 + command = cmd_template.format(value=speed) + return self._send_command(command, callback=callback) + + def get_snapshot(self): + """ + 获取无人机相机的快照 + + Returns: + 成功返回图像数据,失败返回None + """ + if self.connection_status != DroneConnectionStatus.CONNECTED: + logger.error("无人机未连接") + return None + + # 获取快照URL + snapshot_url = self.config.get('video', {}).get('snapshot_url') + if not snapshot_url: + logger.error("未配置快照URL") + return None + + # 填充URL参数 + snapshot_url = snapshot_url.format(ip=self.ip, port=self.video_port) + + try: + # 发送HTTP请求获取图像 + response = requests.get(snapshot_url, timeout=5) + if response.status_code == 200: + return response.content + else: + logger.error(f"获取快照失败,状态码: {response.status_code}") + return None + except Exception as e: + logger.error(f"获取快照出错: {e}") + self.last_error = str(e) + return None + + def create_mission(self, mission_name, waypoints, actions=None): + """ + 创建飞行任务 + + Args: + mission_name: 任务名称 + waypoints: 航点列表,每个航点包含位置和高度 + actions: 在航点处执行的动作 + + Returns: + mission_id: 任务ID或None(如果创建失败) + """ + if self.connection_status != DroneConnectionStatus.CONNECTED: + logger.error("无人机未连接") + return None + + try: + # 创建任务数据 + mission_data = { + 'name': mission_name, + 'created_at': datetime.now().isoformat(), + 'waypoints': waypoints, + 'actions': actions or {} + } + + # 生成任务ID + mission_id = f"mission_{int(time.time())}" + + # 保存任务数据 + missions_dir = os.path.join(self.root_dir, 'data', 'drone_missions') + os.makedirs(missions_dir, exist_ok=True) + + mission_file = os.path.join(missions_dir, f"{mission_id}.json") + with open(mission_file, 'w', encoding='utf-8') as f: + json.dump(mission_data, f, ensure_ascii=False, indent=2) + + logger.info(f"已创建飞行任务: {mission_name}, ID: {mission_id}") + return mission_id + + except Exception as e: + logger.error(f"创建飞行任务失败: {e}") + self.last_error = str(e) + return None + + def execute_mission(self, mission_id, callback=None): + """ + 执行飞行任务 + + Args: + mission_id: 任务ID + callback: 执行状态回调函数 + + Returns: + 成功返回True,失败返回False + """ + if self.connection_status != DroneConnectionStatus.CONNECTED: + logger.error("无人机未连接") + return False + + try: + # 加载任务数据 + mission_file = os.path.join(self.root_dir, 'data', 'drone_missions', f"{mission_id}.json") + if not os.path.exists(mission_file): + logger.error(f"任务文件不存在: {mission_file}") + return False + + with open(mission_file, 'r', encoding='utf-8') as f: + mission_data = json.load(f) + + # 执行任务逻辑 + # 注意:实际执行任务需要更复杂的逻辑和错误处理 + # 这里只是一个简化的示例 + + # 首先起飞 + if not self.takeoff(): + logger.error("任务执行失败: 无法起飞") + return False + + # 遍历航点 + waypoints = mission_data.get('waypoints', []) + for i, waypoint in enumerate(waypoints): + logger.info(f"执行任务: 前往航点 {i+1}/{len(waypoints)}") + + # 移动到航点 + # 注意:这里简化了导航逻辑 + # 实际应该基于GPS坐标或其他定位方式 + if 'x' in waypoint and 'y' in waypoint: + # 假设x和y表示相对距离 + self.move('forward', waypoint['x']) + self.move('right', waypoint['y']) + + # 调整高度 + if 'z' in waypoint: + current_height = self.drone_state['height'] + target_height = waypoint['z'] + + if target_height > current_height: + self.move('up', target_height - current_height) + elif target_height < current_height: + self.move('down', current_height - target_height) + + # 执行航点动作 + actions = mission_data.get('actions', {}).get(str(i), []) + for action in actions: + action_type = action.get('type') + if action_type == 'rotate': + self.rotate(action.get('direction', 'cw'), action.get('angle', 90)) + elif action_type == 'wait': + time.sleep(action.get('duration', 1)) + elif action_type == 'snapshot': + # 获取并保存快照 + snapshot_data = self.get_snapshot() + if snapshot_data: + snapshot_dir = os.path.join(self.root_dir, 'data', 'drone_snapshots') + os.makedirs(snapshot_dir, exist_ok=True) + + snapshot_file = os.path.join(snapshot_dir, f"mission_{mission_id}_wp{i}_{int(time.time())}.jpg") + with open(snapshot_file, 'wb') as f: + f.write(snapshot_data) + + # 回调报告进度 + if callback: + callback(mission_id, i+1, len(waypoints)) + + # 任务完成后降落 + return self.land() + + except Exception as e: + logger.error(f"执行飞行任务失败: {e}") + self.last_error = str(e) + # 发生错误时尝试降落 + self.land() + return False \ No newline at end of file diff --git a/distance-judgement/src/drone/drone_interface/video_receiver.py b/distance-judgement/src/drone/drone_interface/video_receiver.py new file mode 100644 index 00000000..cf4802dd --- /dev/null +++ b/distance-judgement/src/drone/drone_interface/video_receiver.py @@ -0,0 +1,639 @@ +import os +import cv2 +import time +import queue +import logging +import threading +import numpy as np +from datetime import datetime +from pathlib import Path + +# 配置日志 +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger("VideoReceiver") + +class VideoReceiver: + """ + 视频接收器类 + 负责接收无人机视频流并处理 + """ + def __init__(self, stream_url=None, buffer_size=10, save_path=None): + """ + 初始化视频接收器 + + Args: + stream_url: 视频流URL,例如 'udp://192.168.10.1:11111' + buffer_size: 帧缓冲区大小 + save_path: 视频保存路径 + """ + # 项目根目录 + self.root_dir = Path(__file__).resolve().parents[2] + + # 视频流URL + self.stream_url = stream_url + + # 视频捕获对象 + self.cap = None + + # 帧缓冲区 + self.frame_buffer = queue.Queue(maxsize=buffer_size) + self.latest_frame = None + + # 视频接收线程 + self.receiver_thread = None + self.running = False + + # 帧处理回调函数 + self.frame_callbacks = [] + + # 保存设置 + self.save_path = save_path + self.video_writer = None + self.recording = False + + # 帧统计信息 + self.stats = { + 'total_frames': 0, + 'dropped_frames': 0, + 'fps': 0, + 'resolution': (0, 0), + 'start_time': None, + 'last_frame_time': None + } + + # 错误记录 + self.last_error = None + + # 预处理设置 + self.preprocessing_enabled = False + self.preprocessing_params = { + 'resize': None, # (width, height) + 'rotate': 0, # 旋转角度 (0, 90, 180, 270) + 'flip': None, # 0: 水平翻转, 1: 垂直翻转, -1: 水平和垂直翻转 + 'crop': None, # (x, y, width, height) + 'denoise': False # 降噪 + } + + # 流超时设置,默认10秒 + self.stream_timeout = 10.0 + + def start(self, stream_url=None): + """ + 开始接收视频流 + + Args: + stream_url: 可选,覆盖初始化时设定的流地址 + + Returns: + 成功返回True,失败返回False + """ + if stream_url: + self.stream_url = stream_url + + if not self.stream_url: + logger.error("未设置视频流URL") + self.last_error = "未设置视频流URL" + return False + + if self.running: + logger.info("视频接收器已在运行") + return True + + try: + # 🔧 改进UDP端口处理和OpenCV配置 + logger.info(f"正在打开视频流: {self.stream_url},超时: {self.stream_timeout}秒") + + # 设置OpenCV的视频流参数 - 针对UDP流优化 + os.environ["OPENCV_FFMPEG_READ_TIMEOUT"] = str(int(self.stream_timeout * 1000)) # 毫秒 + os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = "protocol_whitelist;file,udp,rtp" + + # 🔧 对于UDP流,使用更宽松的缓冲区设置 + self.cap = cv2.VideoCapture(self.stream_url, cv2.CAP_FFMPEG) + + # 设置视频捕获参数 - 针对H.264 UDP流优化 + self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 最小缓冲区,减少延迟 + self.cap.set(cv2.CAP_PROP_FPS, 30) # 设置期望FPS + + # 🔧 特别针对Tello的设置 + if "11111" in self.stream_url: + logger.info("检测到Tello UDP流,应用专用设置...") + # 针对Tello的UDP流设置更宽松的超时 + self.cap.set(cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, int(self.stream_timeout * 1000)) + self.cap.set(cv2.CAP_PROP_READ_TIMEOUT_MSEC, 5000) # 5秒读取超时 + + # 检查打开状态并等待视频流建立 + open_start_time = time.time() + retry_count = 0 + max_retries = 5 + + while not self.cap.isOpened(): + if time.time() - open_start_time > self.stream_timeout: + logger.error(f"视频流打开超时: {self.stream_url}") + self.last_error = f"视频流打开超时: {self.stream_url}" + return False + + retry_count += 1 + if retry_count > max_retries: + logger.error(f"无法打开视频流: {self.stream_url},已尝试 {max_retries} 次") + self.last_error = f"无法打开视频流: {self.stream_url}" + return False + + logger.info(f"等待视频流打开,重试 {retry_count}/{max_retries}") + time.sleep(1.0) # 等待1秒再次尝试 + self.cap.release() + self.cap = cv2.VideoCapture(self.stream_url, cv2.CAP_FFMPEG) + + # 获取视频属性 + width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + fps = int(self.cap.get(cv2.CAP_PROP_FPS)) + + # 如果宽度或高度为0,可能是视频流未准备好 + if width == 0 or height == 0: + # 尝试读取一帧来获取尺寸 + ret, test_frame = self.cap.read() + if ret and test_frame is not None: + height, width = test_frame.shape[:2] + logger.info(f"从第一帧获取分辨率: {width}x{height}") + else: + logger.warning("无法从第一帧获取分辨率,使用默认值") + width = 640 + height = 480 + + self.stats['resolution'] = (width, height) + self.stats['fps'] = fps if fps > 0 else 30 # 如果FPS为0,使用默认值30 + self.stats['start_time'] = datetime.now() + + logger.info(f"视频流已打开: {self.stream_url},分辨率: {width}x{height}, FPS: {self.stats['fps']}") + + # 如果有保存路径,创建视频写入对象 + if self.save_path: + self._setup_video_writer() + + # 启动接收线程 + self.running = True + self.receiver_thread = threading.Thread(target=self._receive_frames) + self.receiver_thread.daemon = True + self.receiver_thread.start() + + logger.info(f"视频接收线程已启动") + return True + + except Exception as e: + logger.error(f"启动视频接收器失败: {e}") + import traceback + traceback.print_exc() + self.last_error = str(e) + return False + + def stop(self): + """ + 停止接收视频流 + + Returns: + 成功返回True,失败返回False + """ + if not self.running: + logger.info("视频接收器已经停止") + return True + + try: + # 停止接收线程 + self.running = False + if self.receiver_thread and self.receiver_thread.is_alive(): + self.receiver_thread.join(timeout=2) + + # 关闭视频写入 + if self.recording and self.video_writer: + self.stop_recording() + + # 释放视频捕获资源 + if self.cap: + self.cap.release() + self.cap = None + + # 清空帧缓冲区 + while not self.frame_buffer.empty(): + try: + self.frame_buffer.get_nowait() + except queue.Empty: + break + + logger.info("已停止视频接收器") + return True + + except Exception as e: + logger.error(f"停止视频接收器失败: {e}") + self.last_error = str(e) + return False + + def _receive_frames(self): + """视频帧接收线程函数""" + frame_count = 0 + drop_count = 0 + last_fps_time = time.time() + consecutive_failures = 0 # 连续失败计数 + last_warning_time = 0 # 上次警告时间 + + while self.running and self.cap: + try: + # 读取一帧 + ret, frame = self.cap.read() + + if not ret: + # 🔧 改进错误处理:减少垃圾日志,添加智能重试 + consecutive_failures += 1 + current_time = time.time() + + # 只在连续失败较多次或距离上次警告超过5秒时才记录警告 + if consecutive_failures >= 50 or (current_time - last_warning_time) >= 5: + if consecutive_failures < 100: + logger.debug(f"等待视频数据... (连续失败 {consecutive_failures} 次)") + else: + logger.warning(f"视频流可能中断,连续失败 {consecutive_failures} 次") + last_warning_time = current_time + + # 根据失败次数调整等待时间 + if consecutive_failures < 20: + time.sleep(0.05) # 前20次快速重试 + elif consecutive_failures < 100: + time.sleep(0.1) # 中等失败次数,稍微等待 + else: + time.sleep(0.2) # 大量失败,减少CPU占用 + + # 如果连续失败超过500次(约50秒),可能是严重问题 + if consecutive_failures >= 500: + logger.error("视频流长时间无数据,可能存在连接问题") + consecutive_failures = 0 # 重置计数器 + + continue + else: + # 🔧 成功读取到帧,重置失败计数器 + if consecutive_failures > 0: + logger.info(f"✅ 视频流恢复正常,之前连续失败 {consecutive_failures} 次") + consecutive_failures = 0 + + # 更新帧统计信息 + frame_count += 1 + self.stats['total_frames'] = frame_count + self.stats['last_frame_time'] = datetime.now() + + # 计算FPS + current_time = time.time() + if current_time - last_fps_time >= 1.0: # 每秒更新一次FPS + self.stats['fps'] = frame_count / (current_time - last_fps_time) + frame_count = 0 + last_fps_time = current_time + + # 预处理帧 + if self.preprocessing_enabled: + frame = self._preprocess_frame(frame) + + # 更新最新帧 + self.latest_frame = frame.copy() + + # 将帧放入缓冲区,如果缓冲区已满则丢弃最早的帧 + try: + if self.frame_buffer.full(): + self.frame_buffer.get_nowait() # 移除最早的帧 + drop_count += 1 + self.stats['dropped_frames'] = drop_count + + self.frame_buffer.put(frame) + except queue.Full: + drop_count += 1 + self.stats['dropped_frames'] = drop_count + + # 保存视频 + if self.recording and self.video_writer: + self.video_writer.write(frame) + + # 调用帧处理回调函数 + for callback in self.frame_callbacks: + try: + callback(frame) + except Exception as e: + logger.error(f"帧处理回调函数执行出错: {e}") + + except Exception as e: + logger.error(f"接收视频帧出错: {e}") + if self.running: # 只有在运行时才记录错误 + self.last_error = str(e) + time.sleep(0.1) # 出错后稍微等待一下 + + def _preprocess_frame(self, frame): + """ + 预处理视频帧 + + Args: + frame: 原始视频帧 + + Returns: + 处理后的视频帧 + """ + try: + # 裁剪 + if self.preprocessing_params['crop']: + x, y, w, h = self.preprocessing_params['crop'] + frame = frame[y:y+h, x:x+w] + + # 旋转 + rotate_angle = self.preprocessing_params['rotate'] + if rotate_angle: + if rotate_angle == 90: + frame = cv2.rotate(frame, cv2.ROTATE_90_CLOCKWISE) + elif rotate_angle == 180: + frame = cv2.rotate(frame, cv2.ROTATE_180) + elif rotate_angle == 270: + frame = cv2.rotate(frame, cv2.ROTATE_90_COUNTERCLOCKWISE) + + # 翻转 + flip_code = self.preprocessing_params['flip'] + if flip_code is not None: + frame = cv2.flip(frame, flip_code) + + # 调整大小 + if self.preprocessing_params['resize']: + width, height = self.preprocessing_params['resize'] + frame = cv2.resize(frame, (width, height)) + + # 降噪 + if self.preprocessing_params['denoise']: + frame = cv2.fastNlMeansDenoisingColored(frame, None, 10, 10, 7, 21) + + return frame + + except Exception as e: + logger.error(f"预处理视频帧出错: {e}") + return frame # 出错时返回原始帧 + + def _setup_video_writer(self): + """设置视频写入对象""" + try: + if not self.save_path: + logger.warning("未设置视频保存路径") + return False + + # 确保保存目录存在 + save_dir = os.path.dirname(self.save_path) + os.makedirs(save_dir, exist_ok=True) + + # 获取视频属性 + width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + fps = int(self.cap.get(cv2.CAP_PROP_FPS)) + + # 设置视频编码 + fourcc = cv2.VideoWriter_fourcc(*'XVID') + + # 创建视频写入对象 + self.video_writer = cv2.VideoWriter( + self.save_path, + fourcc, + fps, + (width, height) + ) + + logger.info(f"视频将保存到: {self.save_path}") + return True + + except Exception as e: + logger.error(f"设置视频写入器失败: {e}") + self.last_error = str(e) + return False + + def start_recording(self, save_path=None): + """ + 开始录制视频 + + Args: + save_path: 视频保存路径,如果未指定则使用默认路径 + + Returns: + 成功返回True,失败返回False + """ + if not self.running or not self.cap: + logger.error("视频接收器未运行") + return False + + if self.recording: + logger.info("已经在录制视频") + return True + + try: + # 设置保存路径 + if save_path: + self.save_path = save_path + + if not self.save_path: + # 如果未指定路径,创建默认路径 + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + self.save_path = os.path.join( + self.root_dir, + 'data', + 'drone_videos', + f'drone_video_{timestamp}.avi' + ) + + # 设置视频写入器 + if self._setup_video_writer(): + self.recording = True + logger.info(f"开始录制视频: {self.save_path}") + return True + else: + return False + + except Exception as e: + logger.error(f"开始录制视频失败: {e}") + self.last_error = str(e) + return False + + def stop_recording(self): + """ + 停止录制视频 + + Returns: + 成功返回True,失败返回False + """ + if not self.recording: + logger.info("未在录制视频") + return True + + try: + if self.video_writer: + self.video_writer.release() + self.video_writer = None + + self.recording = False + logger.info(f"已停止录制视频: {self.save_path}") + + # 确保文件存在 + if os.path.exists(self.save_path): + return True + else: + logger.error(f"视频文件未正确保存: {self.save_path}") + return False + + except Exception as e: + logger.error(f"停止录制视频失败: {e}") + self.last_error = str(e) + return False + + def get_frame(self, wait=False, timeout=1.0): + """ + 获取视频帧 + + Args: + wait: 是否等待帧可用 + timeout: 等待超时时间(秒) + + Returns: + 成功返回视频帧,失败返回None + """ + if not self.running: + logger.error("视频接收器未运行") + return None + + try: + if self.frame_buffer.empty(): + if not wait: + return None + + # 等待帧可用 + try: + return self.frame_buffer.get(timeout=timeout) + except queue.Empty: + logger.warning("等待视频帧超时") + return None + else: + return self.frame_buffer.get_nowait() + + except Exception as e: + logger.error(f"获取视频帧失败: {e}") + self.last_error = str(e) + return None + + def get_latest_frame(self): + """ + 获取最新的视频帧(不从缓冲区移除) + + Returns: + 成功返回最新的视频帧,失败返回None + """ + return self.latest_frame + + def add_frame_callback(self, callback): + """ + 添加帧处理回调函数 + + Args: + callback: 回调函数,接受一个参数(frame) + + Returns: + 成功返回True + """ + if callback not in self.frame_callbacks: + self.frame_callbacks.append(callback) + return True + + def remove_frame_callback(self, callback): + """ + 移除帧处理回调函数 + + Args: + callback: 之前添加的回调函数 + + Returns: + 成功返回True + """ + if callback in self.frame_callbacks: + self.frame_callbacks.remove(callback) + return True + + def enable_preprocessing(self, enabled=True): + """ + 启用或禁用帧预处理 + + Args: + enabled: 是否启用预处理 + + Returns: + 成功返回True + """ + self.preprocessing_enabled = enabled + return True + + def set_preprocessing_params(self, params): + """ + 设置帧预处理参数 + + Args: + params: 预处理参数字典 + + Returns: + 成功返回True + """ + # 更新预处理参数 + for key, value in params.items(): + if key in self.preprocessing_params: + self.preprocessing_params[key] = value + + return True + + def get_stats(self): + """ + 获取视频接收器统计信息 + + Returns: + 统计信息字典 + """ + # 计算运行时间 + if self.stats['start_time']: + run_time = (datetime.now() - self.stats['start_time']).total_seconds() + self.stats['run_time'] = run_time + + return self.stats + + def take_snapshot(self, save_path=None): + """ + 拍摄当前帧的快照 + + Args: + save_path: 图像保存路径,如果未指定则使用默认路径 + + Returns: + 成功返回保存路径,失败返回None + """ + if not self.running: + logger.error("视频接收器未运行") + return None + + if self.latest_frame is None: + logger.error("没有可用的视频帧") + return None + + try: + # 设置保存路径 + if not save_path: + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + save_path = os.path.join( + self.root_dir, + 'data', + 'drone_snapshots', + f'drone_snapshot_{timestamp}.jpg' + ) + + # 确保目录存在 + save_dir = os.path.dirname(save_path) + os.makedirs(save_dir, exist_ok=True) + + # 保存图像 + cv2.imwrite(save_path, self.latest_frame) + + logger.info(f"已保存快照: {save_path}") + return save_path + + except Exception as e: + logger.error(f"拍摄快照失败: {e}") + self.last_error = str(e) + return None \ No newline at end of file diff --git a/distance-judgement/src/drone/image_analyzer/__init__.py b/distance-judgement/src/drone/image_analyzer/__init__.py new file mode 100644 index 00000000..130edb2c --- /dev/null +++ b/distance-judgement/src/drone/image_analyzer/__init__.py @@ -0,0 +1,12 @@ +""" +图像分析子系统(ImageAnalyzer) +------------- +调用模型进行舰船检测、分类和部件识别 +处理图像预处理和后处理 +生成分析结果报告 +提供API接口供Web应用调用 +""" + +from .analyzer import ImageAnalyzer + +__all__ = ['ImageAnalyzer'] \ No newline at end of file diff --git a/distance-judgement/src/drone/image_analyzer/__pycache__/__init__.cpython-311.pyc b/distance-judgement/src/drone/image_analyzer/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 00000000..68fa354d Binary files /dev/null and b/distance-judgement/src/drone/image_analyzer/__pycache__/__init__.cpython-311.pyc differ diff --git a/distance-judgement/src/drone/image_analyzer/__pycache__/__init__.cpython-313.pyc b/distance-judgement/src/drone/image_analyzer/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 00000000..701a0cde Binary files /dev/null and b/distance-judgement/src/drone/image_analyzer/__pycache__/__init__.cpython-313.pyc differ diff --git a/distance-judgement/src/drone/image_analyzer/__pycache__/analyzer.cpython-311.pyc b/distance-judgement/src/drone/image_analyzer/__pycache__/analyzer.cpython-311.pyc new file mode 100644 index 00000000..db558fba Binary files /dev/null and b/distance-judgement/src/drone/image_analyzer/__pycache__/analyzer.cpython-311.pyc differ diff --git a/distance-judgement/src/drone/image_analyzer/__pycache__/analyzer.cpython-313.pyc b/distance-judgement/src/drone/image_analyzer/__pycache__/analyzer.cpython-313.pyc new file mode 100644 index 00000000..1c6c79ce Binary files /dev/null and b/distance-judgement/src/drone/image_analyzer/__pycache__/analyzer.cpython-313.pyc differ diff --git a/distance-judgement/src/drone/image_analyzer/analyzer.py b/distance-judgement/src/drone/image_analyzer/analyzer.py new file mode 100644 index 00000000..c16041ee --- /dev/null +++ b/distance-judgement/src/drone/image_analyzer/analyzer.py @@ -0,0 +1,538 @@ +import os +import cv2 +import json +import time +import logging +import numpy as np +from datetime import datetime +from pathlib import Path + +# 配置日志 +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger("ImageAnalyzer") + +class ImageAnalyzer: + """ + 图像分析器类 + 负责舰船检测、分类和部件识别,以及图像预处理和后处理 + """ + def __init__(self, model_manager=None, data_manager=None): + """ + 初始化图像分析器 + + Args: + model_manager: 模型管理器实例 + data_manager: 数据管理器实例 + """ + # 项目根目录 + self.root_dir = Path(__file__).resolve().parents[2] + + # 导入必要的模块 + try: + # 导入模型管理器 + if model_manager is None: + from src.model_manager import ModelManager + self.model_manager = ModelManager() + else: + self.model_manager = model_manager + + # 导入数据管理器 + if data_manager is None: + from src.data_storage import DataManager + self.data_manager = DataManager() + else: + self.data_manager = data_manager + + # 导入YOLO检测器 + from utils.detector import ShipDetector + self.ship_detector = None # 延迟初始化 + + # 导入部件检测器 + from utils.part_detector_fixed_379 import ShipPartDetector + self.part_detector = None # 延迟初始化 + + except ImportError as e: + logger.error(f"导入依赖模块失败: {e}") + raise + + # 分析结果目录 + self.results_dir = os.path.join(self.root_dir, 'web', 'results') + os.makedirs(self.results_dir, exist_ok=True) + + # 船舶类型映射 + self.ship_types = { + 0: "航空母舰", + 1: "驱逐舰", + 2: "护卫舰", + 3: "两栖攻击舰", + 4: "巡洋舰", + 5: "潜艇", + 6: "补给舰", + 7: "登陆舰", + 8: "扫雷舰", + 9: "导弹艇", + 10: "小型舰船" + } + + # 图像预处理参数 + self.preprocess_params = { + 'resize': (640, 640), + 'normalize': True, + 'enhance_contrast': True + } + + # 初始化性能统计 + self.perf_stats = { + 'total_analyzed': 0, + 'success_count': 0, + 'failed_count': 0, + 'avg_processing_time': 0, + 'detection_rate': 0 + } + + def _init_detectors(self): + """初始化检测器""" + if self.ship_detector is None: + try: + from utils.detector import ShipDetector + # 获取检测模型 + detector_model = self.model_manager.get_model('detector') + if detector_model: + # 使用模型管理器中的模型 + self.ship_detector = ShipDetector( + model_path=detector_model, + device=self.model_manager.device + ) + else: + # 使用默认模型 + self.ship_detector = ShipDetector() + logger.info("舰船检测器初始化成功") + except Exception as e: + logger.error(f"初始化舰船检测器失败: {e}") + raise + + if self.part_detector is None: + try: + from utils.part_detector_fixed_379 import ShipPartDetector + # 获取部件检测模型 + part_detector_model = self.model_manager.get_model('part_detector') + if part_detector_model: + # 使用模型管理器中的模型 + self.part_detector = ShipPartDetector( + model_path=part_detector_model, + device=self.model_manager.device + ) + else: + # 使用默认模型 + self.part_detector = ShipPartDetector() + logger.info("部件检测器初始化成功") + except Exception as e: + logger.error(f"初始化部件检测器失败: {e}") + raise + + def preprocess_image(self, image): + """ + 预处理图像 + + Args: + image: 输入图像 (numpy数组) + + Returns: + 处理后的图像 + """ + if image is None or image.size == 0: + logger.error("预处理失败:无效的图像") + return None + + try: + # 克隆图像避免修改原始数据 + processed = image.copy() + + # 调整大小(如果需要) + if self.preprocess_params.get('resize'): + target_size = self.preprocess_params['resize'] + if processed.shape[0] != target_size[0] or processed.shape[1] != target_size[1]: + processed = cv2.resize(processed, target_size) + + # 增强对比度(如果启用) + if self.preprocess_params.get('enhance_contrast'): + # 转为LAB颜色空间 + lab = cv2.cvtColor(processed, cv2.COLOR_BGR2LAB) + # 分离通道 + l, a, b = cv2.split(lab) + # 创建CLAHE对象 + clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) + # 应用CLAHE到L通道 + cl = clahe.apply(l) + # 合并通道 + limg = cv2.merge((cl, a, b)) + # 转回BGR + processed = cv2.cvtColor(limg, cv2.COLOR_LAB2BGR) + + # 规范化(如果启用) + if self.preprocess_params.get('normalize'): + processed = processed.astype(np.float32) / 255.0 + + return processed + + except Exception as e: + logger.error(f"图像预处理失败: {e}") + return image # 返回原始图像 + + def analyze_image(self, image_path, conf_threshold=0.25, save_result=True, output_dir=None, user_id=None): + """ + 分析船舶图像并返回分析结果 + + Args: + image_path: 图像路径 + conf_threshold: 检测置信度阈值 + save_result: 是否保存分析结果图像 + output_dir: 输出目录,如果为None则使用默认目录 + user_id: 用户ID(可选,用于记录分析历史) + + Returns: + (dict, numpy.ndarray): 分析结果字典和标注后的图像 + """ + # 确保检测器已初始化 + self._init_detectors() + + # 开始计时 + start_time = time.time() + + try: + # 加载图像 + image = cv2.imread(image_path) + if image is None: + logger.error(f"无法加载图像: {image_path}") + self.perf_stats['total_analyzed'] += 1 + self.perf_stats['failed_count'] += 1 + return {'error': '无法加载图像'}, None + + # 图像预处理 + processed_image = self.preprocess_image(image) + if processed_image is None: + logger.error(f"图像预处理失败: {image_path}") + self.perf_stats['total_analyzed'] += 1 + self.perf_stats['failed_count'] += 1 + return {'error': '图像预处理失败'}, None + + # 复制原始图像用于绘制 + result_image = image.copy() + + # 检测船舶 + detections = self.ship_detector.detect(processed_image, conf_threshold=conf_threshold) + + # 如果没有检测到船舶 + if not detections: + logger.warning(f"未检测到船舶: {image_path}") + self.perf_stats['total_analyzed'] += 1 + self.perf_stats['failed_count'] += 1 + return {'ships': [], 'message': '未检测到船舶'}, result_image + + # 分析结果 + ships = [] + + for i, detection in enumerate(detections): + # 处理检测结果可能是字典或元组的情况 + if isinstance(detection, dict): + # 新版返回格式是字典 + bbox = detection['bbox'] + x1, y1, x2, y2 = bbox + conf = detection['confidence'] + class_id = detection.get('class_id', 0) # 默认为0 + else: + # 旧版返回格式是元组 + x1, y1, x2, y2, conf, class_id = detection + + # 转为整数 + x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2) + + # 船舶区域 + ship_region = image[y1:y2, x1:x2] + + # 确定船舶类型(使用ShipDetector的内部方法) + ship_type = self.ship_detector._analyze_ship_type(ship_region)[0] + + # 分析部件 + parts = [] + if self.part_detector: + try: + parts = self.part_detector.detect_parts( + ship_region, + ship_box=(x1, y1, x2, y2), + conf_threshold=conf, + ship_type=ship_type + ) + except Exception as e: + logger.error(f"部件检测失败: {e}") + + # 添加结果 + ship_result = { + 'bbox': [float(x1), float(y1), float(x2), float(y2)], + 'confidence': float(conf), + 'class_id': int(class_id), + 'class_name': ship_type, + 'class_confidence': float(conf), + 'parts': parts, + 'width': int(x2 - x1), + 'height': int(y2 - y1), + 'area': int((x2 - x1) * (y2 - y1)) + } + ships.append(ship_result) + + # 在图像上标注结果 + color = (0, 255, 0) # 绿色边框 + cv2.rectangle(result_image, (x1, y1), (x2, y2), color, 2) + + # 添加文本标签 + label = f"{ship_type}: {conf:.2f}" + cv2.putText(result_image, label, (x1, y1 - 10), + cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2) + + # 标注部件 + for part in parts: + if 'bbox' in part: + part_x1, part_y1, part_x2, part_y2 = part['bbox'] + part_color = (0, 0, 255) # 红色部件框 + cv2.rectangle(result_image, + (int(part_x1), int(part_y1)), + (int(part_x2), int(part_y2)), + part_color, 1) + + # 添加部件标签 + part_label = f"{part['name']}: {part.get('confidence', 0):.2f}" + cv2.putText(result_image, part_label, + (int(part_x1), int(part_y1) - 5), + cv2.FONT_HERSHEY_SIMPLEX, 0.5, part_color, 1) + + # 计算处理时间 + elapsed_time = time.time() - start_time + + # 更新性能统计 + self.perf_stats['total_analyzed'] += 1 + self.perf_stats['success_count'] += 1 + self.perf_stats['avg_processing_time'] = (self.perf_stats['avg_processing_time'] * + (self.perf_stats['total_analyzed'] - 1) + + elapsed_time) / self.perf_stats['total_analyzed'] + self.perf_stats['detection_rate'] = self.perf_stats['success_count'] / self.perf_stats['total_analyzed'] + + # 创建结果字典 + result_data = { + 'ships': ships, + 'processing_time': elapsed_time, + 'timestamp': datetime.now().isoformat(), + 'image_path': image_path, + 'image_size': { + 'width': image.shape[1], + 'height': image.shape[0], + 'channels': image.shape[2] if len(image.shape) > 2 else 1 + } + } + + # 保存结果 + if save_result: + if output_dir is None: + # 使用默认输出目录 + filename = os.path.basename(image_path) + output_dir = os.path.join(self.results_dir, os.path.splitext(filename)[0]) + + os.makedirs(output_dir, exist_ok=True) + + # 保存结果图像 + result_image_path = os.path.join(output_dir, f"analysis_{os.path.basename(image_path)}") + cv2.imwrite(result_image_path, result_image) + + # 保存结果JSON + result_json_path = os.path.join(output_dir, f"{os.path.splitext(os.path.basename(image_path))[0]}_result.json") + with open(result_json_path, 'w', encoding='utf-8') as f: + json.dump(result_data, f, ensure_ascii=False, indent=2) + + # 保存到数据库 + if self.data_manager: + self.data_manager.save_analysis_result( + image_path=image_path, + result_data=result_data, + result_image_path=result_image_path, + user_id=user_id + ) + + return result_data, result_image + + except Exception as e: + logger.error(f"分析图像时出错: {e}") + import traceback + traceback.print_exc() + + # 更新性能统计 + self.perf_stats['total_analyzed'] += 1 + self.perf_stats['failed_count'] += 1 + + return {'error': str(e)}, None + + def generate_report(self, analysis_result, include_images=True): + """ + 生成分析报告 + + Args: + analysis_result: 分析结果字典 + include_images: 是否包含图像 + + Returns: + report: 报告HTML字符串 + """ + if not analysis_result: + return "

    无效的分析结果

    " + + try: + ships = analysis_result.get('ships', []) + timestamp = analysis_result.get('timestamp', datetime.now().isoformat()) + image_path = analysis_result.get('image_path', '未知') + processing_time = analysis_result.get('processing_time', 0) + + # 创建HTML报告 + html = f""" + + + + + + 舰船分析报告 + + + +
    +
    +

    舰船分析报告

    +

    分析时间: {timestamp}

    +

    图像路径: {image_path}

    +

    处理时间: {processing_time:.2f} 秒

    +

    检测到的舰船数量: {len(ships)}

    +
    + """ + + # 添加图像 + if include_images and 'result_image_path' in analysis_result: + html += f""" +
    +

    分析结果图像

    + 分析结果 +
    + """ + + # 舰船表格 + html += """ +

    检测到的舰船

    + + + + + + + + + + + + """ + + for i, ship in enumerate(ships): + parts = ship.get('parts', []) + html += f""" + + + + + + + + """ + + html += """ + +
    序号舰船类型置信度尺寸 (宽x高)部件数量
    {i+1}{ship.get('class_name', '未知')}{ship.get('confidence', 0):.2f}{ship.get('width', 0)} x {ship.get('height', 0)}{len(parts)}
    + """ + + # 详细舰船信息 + for i, ship in enumerate(ships): + parts = ship.get('parts', []) + html += f""" +
    +
    +

    舰船 #{i+1}: {ship.get('class_name', '未知')}

    +

    置信度: {ship.get('confidence', 0):.2f}

    +
    +
    +

    位置信息

    +

    边界框: [{ship['bbox'][0]:.1f}, {ship['bbox'][1]:.1f}, {ship['bbox'][2]:.1f}, {ship['bbox'][3]:.1f}]

    +

    尺寸: 宽度={ship.get('width', 0)}px, 高度={ship.get('height', 0)}px

    +

    面积: {ship.get('area', 0)}px²

    + +

    检测到的部件 ({len(parts)})

    + """ + + if parts: + for j, part in enumerate(parts): + html += f""" +
    +

    {j+1}. {part.get('name', '未知部件')}

    +

    置信度: {part.get('confidence', 0):.2f}

    +

    位置: [{part.get('bbox', [0,0,0,0])[0]:.1f}, {part.get('bbox', [0,0,0,0])[1]:.1f}, + {part.get('bbox', [0,0,0,0])[2]:.1f}, {part.get('bbox', [0,0,0,0])[3]:.1f}]

    +
    + """ + else: + html += "

    未检测到部件

    " + + html += """ +
    +
    + """ + + # 结束HTML + html += """ +
    + + + """ + + return html + + except Exception as e: + logger.error(f"生成报告失败: {e}") + return f"

    报告生成失败

    错误: {str(e)}

    " + + def get_statistics(self): + """获取分析统计信息""" + return self.perf_stats + + def update_preprocessing_params(self, params): + """ + 更新图像预处理参数 + + Args: + params: 参数字典 + + Returns: + 成功返回True,失败返回False + """ + try: + for key, value in params.items(): + if key in self.preprocess_params: + self.preprocess_params[key] = value + return True + except Exception as e: + logger.error(f"更新预处理参数失败: {e}") + return False \ No newline at end of file diff --git a/distance-judgement/src/drone/utils/advanced_detector.py b/distance-judgement/src/drone/utils/advanced_detector.py new file mode 100644 index 00000000..3097a75c --- /dev/null +++ b/distance-judgement/src/drone/utils/advanced_detector.py @@ -0,0 +1,593 @@ +import os +import sys +import torch +import numpy as np +import cv2 +from pathlib import Path +import requests +from PIL import Image, ImageDraw, ImageFont +import io + +# 尝试导入transformers模块,如果不可用则使用传统方法 +try: + from transformers import AutoProcessor, AutoModelForObjectDetection, ViTImageProcessor + from transformers import AutoModelForImageClassification + TRANSFORMERS_AVAILABLE = True +except ImportError: + print("警告: transformers模块未安装,将使用传统计算机视觉方法进行舰船识别") + TRANSFORMERS_AVAILABLE = False + +class AdvancedShipDetector: + """ + 高级舰船检测与分类系统,使用预训练视觉模型提高识别准确度 + 如果预训练模型不可用,则回退到传统计算机视觉方法 + """ + def __init__(self, device=None): + """ + 初始化高级舰船检测器 + + Args: + device: 运行设备,可以是'cuda'或'cpu',None则自动选择 + """ + # 确定运行设备 + if device is None: + self.device = 'cuda' if torch.cuda.is_available() else 'cpu' + else: + self.device = device + + print(f"高级检测器使用设备: {self.device}") + + # 舰船类型定义 + self.ship_classes = { + 0: "航空母舰", + 1: "驱逐舰", + 2: "护卫舰", + 3: "潜艇", + 4: "巡洋舰", + 5: "两栖攻击舰", + 6: "补给舰", + 7: "油轮", + 8: "集装箱船", + 9: "散货船", + 10: "渔船", + 11: "游艇", + 12: "战列舰", + 13: "登陆舰", + 14: "导弹艇", + 15: "核潜艇", + 16: "轻型航母", + 17: "医疗船", + 18: "海洋考察船", + 19: "其他舰船" + } + + # 加载通用图像理解模型 - 只在transformers可用时尝试 + self.model_loaded = False + if TRANSFORMERS_AVAILABLE: + try: + print("正在加载高级图像分析模型...") + # 使用轻量级分类模型 + self.processor = ViTImageProcessor.from_pretrained("google/vit-base-patch16-224") + self.model = AutoModelForImageClassification.from_pretrained( + "google/vit-base-patch16-224", + num_labels=20 # 适配我们的类别数量 + ) + self.model = self.model.to(self.device) + + print("高级图像分析模型加载完成") + self.model_loaded = True + except Exception as e: + print(f"高级模型加载失败: {str(e)}") + print("将使用传统计算机视觉方法进行舰船识别") + self.model_loaded = False + else: + print("未检测到transformers库,将使用传统计算机视觉方法进行舰船识别") + + def identify_ship_type(self, image): + """ + 使用高级图像分析识别舰船类型 + + Args: + image: 图像路径或图像对象 + + Returns: + ship_type: 舰船类型 + confidence: 置信度 + """ + # 将输入转换为PIL图像 + if isinstance(image, str): + # 检查文件是否存在 + if not os.path.exists(image): + print(f"图像文件不存在: {image}") + return "未知舰船", 0.0 + img = Image.open(image).convert('RGB') + elif isinstance(image, np.ndarray): + img = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) + elif isinstance(image, Image.Image): + img = image + else: + print(f"不支持的图像类型: {type(image)}") + return "未知舰船", 0.0 + + # 尝试使用高级模型识别 - 只在model_loaded为True时 + if self.model_loaded and TRANSFORMERS_AVAILABLE: + try: + # 预处理图像 + inputs = self.processor(images=img, return_tensors="pt").to(self.device) + + # 进行预测 + with torch.no_grad(): + outputs = self.model(**inputs) + + # 获取预测结果 + logits = outputs.logits + probs = torch.nn.functional.softmax(logits, dim=-1) + pred_class = torch.argmax(probs, dim=-1).item() + confidence = probs[0, pred_class].item() + + # 转换为舰船类型 + if pred_class in self.ship_classes: + ship_type = self.ship_classes[pred_class] + else: + ship_type = "未知舰船类型" + + return ship_type, confidence + except Exception as e: + print(f"高级识别失败: {str(e)}") + # 如果高级识别失败,使用备选方法 + + # 备选: 使用传统计算机视觉方法识别舰船特征 + ship_type, confidence = self._analyze_ship_features(img) + return ship_type, confidence + + def _analyze_ship_features(self, img): + """ + 使用传统计算机视觉方法分析舰船特征 + + Args: + img: PIL图像 + + Returns: + ship_type: 舰船类型 + confidence: 置信度 + """ + # 转换为OpenCV格式进行分析 + cv_img = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR) + + # 获取图像特征 + height, width = cv_img.shape[:2] + aspect_ratio = width / height if height > 0 else 0 + + # 检测舰船特征 + is_carrier = self._check_carrier_features(cv_img) + is_destroyer = self._check_destroyer_features(cv_img) + is_frigate = self._check_frigate_features(cv_img) + is_submarine = self._check_submarine_features(cv_img) + + # 根据特征判断类型 + if is_carrier: + return "航空母舰", 0.85 + elif is_destroyer: + return "驱逐舰", 0.80 + elif is_frigate: + return "护卫舰", 0.75 + elif is_submarine: + return "潜艇", 0.70 + elif aspect_ratio > 5.0: + return "航空母舰", 0.65 + elif 3.0 < aspect_ratio < 5.0: + return "驱逐舰", 0.60 + elif 2.0 < aspect_ratio < 3.0: + return "护卫舰", 0.55 + else: + return "其他舰船", 0.50 + + def _check_carrier_features(self, img): + """检查航空母舰特征""" + if img is None or img.size == 0: + return False + + height, width = img.shape[:2] + aspect_ratio = width / height if height > 0 else 0 + + # 航母特征: 大甲板,长宽比大 + if aspect_ratio < 2.5: + return False + + # 检查平坦甲板 + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) if len(img.shape) == 3 else img + edges = cv2.Canny(gray, 50, 150) + + # 水平线特征 + horizontal_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (25, 1)) + horizontal_lines = cv2.morphologyEx(edges, cv2.MORPH_OPEN, horizontal_kernel) + horizontal_pixels = cv2.countNonZero(horizontal_lines) + horizontal_ratio = horizontal_pixels / (width * height) if width * height > 0 else 0 + + # 航母甲板应该有明显的水平线 + if horizontal_ratio < 0.03: + return False + + return True + + def _check_destroyer_features(self, img): + """检查驱逐舰特征""" + if img is None or img.size == 0: + return False + + height, width = img.shape[:2] + aspect_ratio = width / height if height > 0 else 0 + + # 驱逐舰特征: 细长,有明显上层建筑 + if aspect_ratio < 2.0 or aspect_ratio > 5.0: + return False + + # 边缘特征分析 + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) if len(img.shape) == 3 else img + edges = cv2.Canny(gray, 50, 150) + edge_pixels = cv2.countNonZero(edges) + edge_density = edge_pixels / (width * height) if width * height > 0 else 0 + + # 垂直线特征 - 舰桥和上层建筑 + vertical_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1, 15)) + vertical_lines = cv2.morphologyEx(edges, cv2.MORPH_OPEN, vertical_kernel) + vertical_pixels = cv2.countNonZero(vertical_lines) + vertical_ratio = vertical_pixels / (width * height) if width * height > 0 else 0 + + # 驱逐舰应该有一定的上层建筑 + if vertical_ratio < 0.01 or edge_density < 0.1: + return False + + return True + + def _check_frigate_features(self, img): + """检查护卫舰特征""" + if img is None or img.size == 0: + return False + + height, width = img.shape[:2] + aspect_ratio = width / height if height > 0 else 0 + + # 护卫舰特征: 与驱逐舰类似但更小 + if aspect_ratio < 1.8 or aspect_ratio > 3.5: + return False + + # 边缘特征 + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) if len(img.shape) == 3 else img + edges = cv2.Canny(gray, 50, 150) + edge_pixels = cv2.countNonZero(edges) + edge_density = edge_pixels / (width * height) if width * height > 0 else 0 + + if edge_density < 0.05 or edge_density > 0.3: + return False + + return True + + def _check_submarine_features(self, img): + """检查潜艇特征""" + if img is None or img.size == 0: + return False + + height, width = img.shape[:2] + aspect_ratio = width / height if height > 0 else 0 + + # 潜艇特征: 非常细长,低矮 + if aspect_ratio < 3.0: + return False + + # 边缘密度应低 + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) if len(img.shape) == 3 else img + edges = cv2.Canny(gray, 50, 150) + edge_pixels = cv2.countNonZero(edges) + edge_density = edge_pixels / (width * height) if width * height > 0 else 0 + + # 潜艇表面较为光滑 + if edge_density > 0.15: + return False + + return True + + def detect_ship_parts(self, image, ship_type=None): + """ + 检测舰船上的各个部件 + + Args: + image: 图像路径或图像对象 + ship_type: 舰船类型,用于特定类型的部件识别 + + Returns: + parts: 检测到的部件列表 + """ + # 将输入转换为OpenCV图像 + if isinstance(image, str): + if not os.path.exists(image): + print(f"图像文件不存在: {image}") + return [] + cv_img = cv2.imread(image) + elif isinstance(image, np.ndarray): + cv_img = image + elif isinstance(image, Image.Image): + cv_img = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR) + else: + print(f"不支持的图像类型: {type(image)}") + return [] + + # 如果未提供舰船类型,先识别类型 + if ship_type is None: + ship_type, _ = self.identify_ship_type(cv_img) + + # 根据舰船类型识别不同部件 + parts = [] + + if "航空母舰" in ship_type: + parts = self._detect_carrier_parts(cv_img) + elif "驱逐舰" in ship_type: + parts = self._detect_destroyer_parts(cv_img) + elif "护卫舰" in ship_type: + parts = self._detect_frigate_parts(cv_img) + elif "潜艇" in ship_type: + parts = self._detect_submarine_parts(cv_img) + else: + # 通用舰船部件检测 + parts = self._detect_generic_parts(cv_img) + + return parts + + def _detect_carrier_parts(self, img): + """识别航母特定部件""" + parts = [] + h, w = img.shape[:2] + + # 识别飞行甲板 + deck_y1 = int(h * 0.3) + deck_y2 = int(h * 0.7) + parts.append({ + 'name': '飞行甲板', + 'bbox': (0, deck_y1, w, deck_y2), + 'confidence': 0.9 + }) + + # 识别舰岛 + # 边缘检测找到可能的舰岛位置 + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) if len(img.shape) == 3 else img + blurred = cv2.GaussianBlur(gray, (5, 5), 0) + edges = cv2.Canny(blurred, 50, 150) + + # 寻找垂直结构 + vertical_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1, 20)) + vertical_lines = cv2.morphologyEx(edges, cv2.MORPH_OPEN, vertical_kernel) + + # 查找轮廓 + contours, _ = cv2.findContours(vertical_lines, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + + # 查找最大的垂直结构,可能是舰岛 + if contours: + largest_contour = max(contours, key=cv2.contourArea) + x, y, box_w, box_h = cv2.boundingRect(largest_contour) + # 位于甲板上部的垂直结构,可能是舰岛 + if box_h > h * 0.1 and y < h * 0.5: + parts.append({ + 'name': '舰岛', + 'bbox': (x, y, x + box_w, y + box_h), + 'confidence': 0.85 + }) + + # 添加其他通用部件 + generic_parts = self._detect_generic_parts(img) + parts.extend(generic_parts) + + return parts + + def _detect_destroyer_parts(self, img): + """识别驱逐舰特定部件""" + parts = [] + h, w = img.shape[:2] + + # 识别舰桥 + # 驱逐舰通常舰桥位于前部1/3位置 + bridge_x1 = int(w * 0.2) + bridge_x2 = int(w * 0.4) + bridge_y1 = int(h * 0.1) + bridge_y2 = int(h * 0.5) + parts.append({ + 'name': '舰桥', + 'bbox': (bridge_x1, bridge_y1, bridge_x2, bridge_y2), + 'confidence': 0.85 + }) + + # 识别主炮 + # 主炮通常位于前部 + gun_x1 = int(w * 0.05) + gun_x2 = int(w * 0.15) + gun_y1 = int(h * 0.3) + gun_y2 = int(h * 0.5) + parts.append({ + 'name': '舰炮', + 'bbox': (gun_x1, gun_y1, gun_x2, gun_y2), + 'confidence': 0.8 + }) + + # 识别导弹发射装置 + # 驱逐舰通常在中部有垂直发射系统 + vls_x1 = int(w * 0.4) + vls_x2 = int(w * 0.6) + vls_y1 = int(h * 0.3) + vls_y2 = int(h * 0.5) + parts.append({ + 'name': '导弹发射装置', + 'bbox': (vls_x1, vls_y1, vls_x2, vls_y2), + 'confidence': 0.75 + }) + + # 添加其他通用部件 + generic_parts = self._detect_generic_parts(img) + parts.extend(generic_parts) + + return parts + + def _detect_frigate_parts(self, img): + """识别护卫舰特定部件""" + parts = [] + h, w = img.shape[:2] + + # 识别舰桥 + bridge_x1 = int(w * 0.25) + bridge_x2 = int(w * 0.45) + bridge_y1 = int(h * 0.15) + bridge_y2 = int(h * 0.5) + parts.append({ + 'name': '舰桥', + 'bbox': (bridge_x1, bridge_y1, bridge_x2, bridge_y2), + 'confidence': 0.8 + }) + + # 识别主炮 + gun_x1 = int(w * 0.1) + gun_x2 = int(w * 0.2) + gun_y1 = int(h * 0.3) + gun_y2 = int(h * 0.5) + parts.append({ + 'name': '舰炮', + 'bbox': (gun_x1, gun_y1, gun_x2, gun_y2), + 'confidence': 0.75 + }) + + # 识别直升机甲板 + heli_x1 = int(w * 0.7) + heli_x2 = int(w * 0.9) + heli_y1 = int(h * 0.35) + heli_y2 = int(h * 0.55) + parts.append({ + 'name': '直升机甲板', + 'bbox': (heli_x1, heli_y1, heli_x2, heli_y2), + 'confidence': 0.7 + }) + + # 添加其他通用部件 + generic_parts = self._detect_generic_parts(img) + parts.extend(generic_parts) + + return parts + + def _detect_submarine_parts(self, img): + """识别潜艇特定部件""" + parts = [] + h, w = img.shape[:2] + + # 识别指挥塔 + tower_x1 = int(w * 0.4) + tower_x2 = int(w * 0.6) + tower_y1 = int(h * 0.2) + tower_y2 = int(h * 0.5) + parts.append({ + 'name': '指挥塔', + 'bbox': (tower_x1, tower_y1, tower_x2, tower_y2), + 'confidence': 0.8 + }) + + # 添加其他通用部件 + generic_parts = self._detect_generic_parts(img) + parts.extend(generic_parts) + + return parts + + def _detect_generic_parts(self, img): + """识别通用舰船部件""" + parts = [] + h, w = img.shape[:2] + + # 使用边缘检测和轮廓分析来寻找可能的部件 + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) if len(img.shape) == 3 else img + blurred = cv2.GaussianBlur(gray, (5, 5), 0) + edges = cv2.Canny(blurred, 50, 150) + + # 寻找轮廓 + contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + + # 按面积排序轮廓 + contours = sorted(contours, key=cv2.contourArea, reverse=True) + + # 仅处理最大的几个轮廓 + max_contours = 5 + contours = contours[:max_contours] if len(contours) > max_contours else contours + + # 分析每个轮廓 + for i, contour in enumerate(contours): + # 只考虑足够大的轮廓 + area = cv2.contourArea(contour) + if area < (h * w * 0.01): # 忽略太小的轮廓 + continue + + # 获取边界框 + x, y, box_w, box_h = cv2.boundingRect(contour) + + # 跳过太大的轮廓(可能是整个舰船) + if box_w > w * 0.8 and box_h > h * 0.8: + continue + + # 根据位置和尺寸猜测部件类型 + part_name = self._guess_part_type(x, y, box_w, box_h, h, w) + + # 添加到部件列表 + parts.append({ + 'name': part_name, + 'bbox': (x, y, x + box_w, y + box_h), + 'confidence': 0.6 # 通用部件置信度较低 + }) + + return parts + + def _guess_part_type(self, x, y, w, h, img_h, img_w): + """根据位置和尺寸猜测部件类型""" + # 计算相对位置 + rel_x = x / img_w + rel_y = y / img_h + rel_w = w / img_w + rel_h = h / img_h + aspect_ratio = w / h if h > 0 else 0 + + # 前部的可能是舰炮 + if rel_x < 0.2 and rel_y > 0.3 and rel_y < 0.7: + return "舰炮" + + # 中上部的可能是舰桥 + if 0.3 < rel_x < 0.7 and rel_y < 0.3 and aspect_ratio < 2.0: + return "舰桥" + + # 顶部细长的可能是雷达 + if rel_y < 0.3 and aspect_ratio > 2.0: + return "雷达" + + # 后部的可能是直升机甲板 + if rel_x > 0.7 and rel_y > 0.3: + return "直升机甲板" + + # 中部的可能是导弹发射装置 + if 0.3 < rel_x < 0.7 and 0.3 < rel_y < 0.7: + return "导弹发射装置" + + # 顶部圆形的可能是雷达罩 + if rel_y < 0.3 and 0.8 < aspect_ratio < 1.2: + return "雷达罩" + + # 默认部件 + return "未知部件" + +# 示例用法 +def test_detector(): + detector = AdvancedShipDetector() + test_img = "test_ship.jpg" + + if os.path.exists(test_img): + ship_type, confidence = detector.identify_ship_type(test_img) + print(f"识别结果: {ship_type}, 置信度: {confidence:.2f}") + + parts = detector.detect_ship_parts(test_img, ship_type) + print(f"检测到 {len(parts)} 个部件:") + for i, part in enumerate(parts): + print(f" {i+1}. {part['name']} (置信度: {part['confidence']:.2f})") + else: + print(f"测试图像不存在: {test_img}") + +if __name__ == "__main__": + test_detector() \ No newline at end of file diff --git a/distance-judgement/src/drone/utils/analyze_image.py b/distance-judgement/src/drone/utils/analyze_image.py new file mode 100644 index 00000000..687f7cd7 --- /dev/null +++ b/distance-judgement/src/drone/utils/analyze_image.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import sys +import cv2 +import argparse +from pathlib import Path +import numpy as np +from PIL import Image, ImageDraw, ImageFont + +# 添加项目根目录到Python路径 +script_dir = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(script_dir) + +# 检查是否可以导入高级检测器 +try: + # 导入分析器和高级检测器 + from scripts.ship_analyzer import ShipAnalyzer + from utils.advanced_detector import AdvancedShipDetector + ADVANCED_DETECTOR_AVAILABLE = True +except ImportError as e: + print(f"警告:无法导入高级检测器: {e}") + print("将仅使用传统分析器") + from scripts.ship_analyzer import ShipAnalyzer + ADVANCED_DETECTOR_AVAILABLE = False + +def analyze_image(image_path, output_dir=None, conf_threshold=0.25, part_conf_threshold=0.3, use_advanced=True): + """ + 分析图像中的舰船和部件 + + Args: + image_path: 图像路径 + output_dir: 输出目录 + conf_threshold: 检测置信度阈值 + part_conf_threshold: 部件置信度阈值 + use_advanced: 是否使用高级检测器 + """ + print(f"开始分析图像: {image_path}") + + # 检查图像是否存在 + if not os.path.exists(image_path): + print(f"错误: 图像文件不存在: {image_path}") + return None + + # 创建输出目录 + if output_dir is not None: + os.makedirs(output_dir, exist_ok=True) + + # 根据参数选择使用高级检测器或传统分析器 + if use_advanced and ADVANCED_DETECTOR_AVAILABLE: + try: + print("使用高级图像分析器...") + result_img, results = analyze_with_advanced_detector(image_path, output_dir, conf_threshold, part_conf_threshold) + except Exception as e: + print(f"高级分析器出错: {str(e)}") + print("回退到传统分析器...") + # 如果高级分析失败,回退到传统分析器 + analyzer = ShipAnalyzer() + results, result_img = analyzer.analyze_image( + image_path, + conf_threshold=conf_threshold, + part_conf_threshold=part_conf_threshold, + save_result=True, + output_dir=output_dir + ) + else: + # 使用传统分析器 + print("使用传统图像分析器...") + analyzer = ShipAnalyzer() + results, result_img = analyzer.analyze_image( + image_path, + conf_threshold=conf_threshold, + part_conf_threshold=part_conf_threshold, + save_result=True, + output_dir=output_dir + ) + + # 输出分析结果 + if 'ships' in results: + ships = results['ships'] + print(f"\n分析完成,检测到 {len(ships)} 个舰船:") + + for i, ship in enumerate(ships): + print(f"\n舰船 #{i+1}:") + print(f" 类型: {ship['class_name']}") + print(f" 置信度: {ship['class_confidence']:.2f}") + parts = ship.get('parts', []) + print(f" 检测到 {len(parts)} 个部件:") + + # 显示部件信息 + for j, part in enumerate(parts): + print(f" 部件 #{j+1}: {part['name']} (置信度: {part['confidence']:.2f})") + else: + # 兼容旧格式 + print(f"\n分析完成,检测到 {len(results)} 个舰船:") + for i, ship in enumerate(results): + print(f"\n舰船 #{i+1}:") + print(f" 类型: {ship['class_name']}") + confidence = ship.get('class_confidence', ship.get('confidence', 0.0)) + print(f" 置信度: {confidence:.2f}") + parts = ship.get('parts', []) + print(f" 检测到 {len(parts)} 个部件:") + + # 显示部件信息 + for j, part in enumerate(parts): + part_conf = part.get('confidence', 0.0) + print(f" 部件 #{j+1}: {part['name']} (置信度: {part_conf:.2f})") + + # 保存结果图像 + if output_dir is not None: + result_path = os.path.join(output_dir, f"analysis_{os.path.basename(image_path)}") + cv2.imwrite(result_path, result_img) + print(f"\n结果图像已保存至: {result_path}") + + return result_img + +def analyze_with_advanced_detector(image_path, output_dir=None, conf_threshold=0.25, part_conf_threshold=0.3): + """ + 使用高级检测器分析图像 + + Args: + image_path: 图像路径 + output_dir: 输出目录 + conf_threshold: 检测置信度阈值 + part_conf_threshold: 部件置信度阈值 + + Returns: + result_img: 标注了检测结果的图像 + results: 检测结果字典 + """ + try: + print("正在加载高级图像分析模型...") + # 初始化高级检测器 + detector = AdvancedShipDetector() + except Exception as e: + print(f"高级模型加载失败: {e}") + print("将使用传统计算机视觉方法进行舰船识别") + # 创建一个基本的检测器实例,但不加载模型 + detector = AdvancedShipDetector(load_models=False) + + # 读取图像 + img = cv2.imread(image_path) + if img is None: + raise ValueError(f"无法读取图像: {image_path}") + + result_img = img.copy() + h, w = img.shape[:2] + + # 使用高级检测器进行对象检测 + ships = [] + try: + if hasattr(detector, 'detect_ships') and callable(detector.detect_ships): + detected_ships = detector.detect_ships(img, conf_threshold) + if detected_ships and len(detected_ships) > 0: + ships = detected_ships + # 使用检测器返回的图像 + if len(detected_ships) > 1 and isinstance(detected_ships[1], np.ndarray): + result_img = detected_ships[1] + ships = detected_ships[0] + else: + print("高级检测器缺少detect_ships方法,使用基本识别") + except Exception as e: + print(f"高级舰船检测失败: {e}") + + # 如果没有检测到舰船,使用传统方法尝试识别单个舰船 + if not ships: + # 识别舰船类型 + ship_type, confidence = detector.identify_ship_type(img) + print(f"高级检测器识别结果: {ship_type}, 置信度: {confidence:.2f}") + + # 单个舰船的边界框 - 使用整个图像 + padding = int(min(w, h) * 0.05) # 5%的边距 + ship_box = (padding, padding, w-padding, h-padding) + + # 创建单个舰船对象 + ship = { + 'id': 1, + 'bbox': ship_box, + 'class_name': ship_type, + 'class_confidence': confidence + } + ships = [ship] + + # 在图像上标注舰船信息 + cv2.rectangle(result_img, (ship_box[0], ship_box[1]), (ship_box[2], ship_box[3]), (0, 0, 255), 2) + cv2.putText(result_img, f"{ship_type}: {confidence:.2f}", + (ship_box[0]+10, ship_box[1]+30), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 255), 2) + + # 为每艘舰船检测部件 + processed_ships = [] + for i, ship in enumerate(ships): + ship_id = i + 1 + ship_box = ship.get('bbox', (0, 0, w, h)) + ship_type = ship.get('class_name', '其他舰船') + ship_confidence = ship.get('class_confidence', ship.get('confidence', 0.7)) + + # 格式化为标准结构 + ship_with_parts = { + 'id': ship_id, + 'bbox': ship_box, + 'class_name': ship_type, + 'class_confidence': ship_confidence, + 'parts': [] + } + + # 检测舰船部件 + try: + parts = detector.detect_ship_parts(img, ship_box, ship_type, part_conf_threshold) + print(f"舰船 #{ship_id} 检测到 {len(parts)} 个部件") + + # 为每个部件添加所属舰船ID + for part in parts: + part['ship_id'] = ship_id + ship_with_parts['parts'].append(part) + + # 标注部件 + part_box = part.get('bbox', (0, 0, 0, 0)) + name = part.get('name', '未知部件') + conf = part.get('confidence', 0.0) + + # 绘制部件边界框 + cv2.rectangle(result_img, + (int(part_box[0]), int(part_box[1])), + (int(part_box[2]), int(part_box[3])), + (0, 255, 0), 2) + + # 添加部件标签 + label = f"{name}: {conf:.2f}" + cv2.putText(result_img, label, + (int(part_box[0]), int(part_box[1])-5), + cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2) + except Exception as e: + print(f"部件检测失败: {e}") + + processed_ships.append(ship_with_parts) + + # 构建结果数据结构 + results = { + 'ships': processed_ships + } + + # 保存结果图像 + if output_dir is not None: + os.makedirs(output_dir, exist_ok=True) + result_path = os.path.join(output_dir, f"analysis_{os.path.basename(image_path)}") + cv2.imwrite(result_path, result_img) + print(f"结果图像已保存至: {result_path}") + + return result_img, results + +def main(): + parser = argparse.ArgumentParser(description="舰船图像分析工具") + parser.add_argument("image_path", help="需要分析的舰船图像路径") + parser.add_argument("--output", "-o", help="分析结果输出目录", default="results") + parser.add_argument("--conf", "-c", type=float, default=0.25, help="检测置信度阈值") + parser.add_argument("--part-conf", "-pc", type=float, default=0.3, help="部件检测置信度阈值") + parser.add_argument("--show", action="store_true", help="显示分析结果图像") + parser.add_argument("--traditional", action="store_true", help="使用传统分析器而非高级分析器") + + args = parser.parse_args() + + try: + # 分析图像 + result_img = analyze_image( + args.image_path, + output_dir=args.output, + conf_threshold=args.conf, + part_conf_threshold=args.part_conf, + use_advanced=not args.traditional + ) + + # 显示结果图像 + if args.show and result_img is not None: + cv2.imshow("分析结果", result_img) + cv2.waitKey(0) + cv2.destroyAllWindows() + + except Exception as e: + print(f"分析过程中出错: {str(e)}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/distance-judgement/src/drone/utils/detector_fixed.py b/distance-judgement/src/drone/utils/detector_fixed.py new file mode 100644 index 00000000..8c19fc1b --- /dev/null +++ b/distance-judgement/src/drone/utils/detector_fixed.py @@ -0,0 +1,469 @@ +import os +import sys +import torch +import numpy as np +from PIL import Image +from ultralytics import YOLO +from pathlib import Path +import cv2 +import time + +# 添加项目根目录到Python路径 +script_dir = os.path.dirname(os.path.abspath(__file__)) +parent_dir = os.path.dirname(script_dir) +sys.path.append(parent_dir) + +class ShipDetector: + """ + 舰船检测模块,使用YOLOv8进行目标检测 + """ + def __init__(self, model_path=None, device=None): + """ + 初始化船舶检测器 + + Args: + model_path: 检测模型路径,如果为None则使用预训练模型 + device: 运行设备,可以是'cuda'或'cpu',None则自动选择 + """ + self.model = None + self.device = device if device else ('cuda' if torch.cuda.is_available() else 'cpu') + + print(f"使用设备: {self.device}") + + # 加载模型 + try: + if model_path is None: + # 尝试从配置文件加载模型 + try: + from scripts.config_loader import load_config + config = load_config() + if config and 'models' in config and 'detector' in config['models'] and 'path' in config['models']['detector']: + config_model_path = config['models']['detector']['path'] + if os.path.exists(config_model_path): + model_path = config_model_path + print(f"从配置文件加载模型: {model_path}") + except Exception as e: + print(f"从配置加载模型出错: {e}") + + # 如果配置中没有或者配置的模型不存在,尝试其他备选 + if model_path is None: + # 优先使用训练好的自定义模型,而非预训练的COCO模型 + model_candidates = [ + # 首先使用预训练模型 + 'yolov8n.pt', # 标准预训练模型 + # 首先使用预训练模型 + 'yolov8n.pt', # 标准预训练模型 + # 首先尝试训练好的模型 + 'D:/ShipAI/models/best.pt', + 'D:/ShipAI/models/train/ship_detection3/weights/best.pt', + 'D:/ShipAI/models/train/ship_detection3/weights/last.pt', + 'D:/ShipAI/models/train/ship_detection/weights/best.pt', + 'D:/ShipAI/models/train/ship_detection/weights/last.pt', + './models/best.pt', + './models/train/ship_detection3/weights/best.pt', + './models/train/ship_detection3/weights/last.pt', + './models/train/ship_detection/weights/best.pt', + './models/train/ship_detection/weights/last.pt', + # 最后才是预训练模型 + 'yolov8n.pt', + './models/yolov8n.pt', + 'D:/ShipAI/models/yolov8n.pt', + os.path.join(os.path.dirname(__file__), '../yolov8n.pt'), + os.path.join(os.path.dirname(__file__), '../models/yolov8n.pt'), + ] + + for candidate in model_candidates: + if os.path.exists(candidate): + model_path = candidate + print(f"自动选择模型: {model_path}") + break + + # 仍未找到,尝试下载YOLOv8n模型 + if model_path is None: + try: + print("未找到本地模型,尝试从Ultralytics下载YOLOv8n...") + model_path = 'yolov8n.pt' + # 确保models目录存在 + os.makedirs('./models', exist_ok=True) + self.model = YOLO('yolov8n.pt') + print("YOLOv8n模型加载成功") + except Exception as e: + print(f"下载YOLOv8n模型失败: {e}") + raise ValueError("无法找到或下载YOLOv8模型") + + # 加载指定路径的模型 + if self.model is None and model_path is not None: + print(f"正在加载模型: {model_path}") + try: + self.model = YOLO(model_path) + print(f"成功加载YOLOv8模型: {model_path}") + except Exception as e: + print(f"加载模型失败: {e}") + raise ValueError(f"无法加载模型 {model_path}") + + except Exception as e: + print(f"初始化检测器失败: {e}") + raise e + + # 自定义配置 + self.ship_categories = { + # 对应YOLOv8预训练模型的类别 + 8: "船舶", # boat/ship + 4: "飞机", # airplane/aircraft + 9: "交通工具" # 添加可能的其他类别 + } + + # 舰船类型精确判断参数 + self.min_confidence = 0.1 # 进一步降低最小置信度以提高检出率 + self.iou_threshold = 0.45 # NMS IOU阈值 + + # 从模型获取实际的类别映射 + if self.model: + try: + # 从模型中获取类别名称 + self.ship_types = self.model.names + print(f"从模型读取类别映射: {self.ship_types}") + + # 使用模型自身的类别映射 + self.display_types = self.ship_types + # 移除COCO映射 + self.coco_to_ship_map = None + except Exception as e: + print(f"读取模型类别映射失败: {e}") + # 使用默认的舰船类型映射 + self.ship_types = { + 0: "航空母舰", + 1: "驱逐舰", + 2: "护卫舰", + 3: "潜艇", + 4: "巡洋舰", + 5: "两栖攻击舰" + } + self.display_types = self.ship_types + # 移除COCO映射 + self.coco_to_ship_map = None + else: + # 默认的舰船类型映射 + self.ship_types = { + 0: "航空母舰", + 1: "驱逐舰", + 2: "护卫舰", + 3: "潜艇", + 4: "巡洋舰", + 5: "两栖攻击舰" + } + self.display_types = self.ship_types + self.coco_to_ship_map = None + + # 扩展舰船特征数据库 - 用于辅助分类 + self.ship_features = { + "航空母舰": { + "特征": ["大型甲板", "舰岛", "弹射器", "甲板标记"], + "长宽比": [7.0, 11.0], + "关键部件": ["舰载机", "舰岛", "升降机"] + }, + "驱逐舰": { + "特征": ["中型舰体", "舰炮", "垂发系统", "直升机平台"], + "长宽比": [8.0, 12.0], + "关键部件": ["舰炮", "垂发", "舰桥", "雷达"] + }, + "护卫舰": { + "特征": ["小型舰体", "舰炮", "直升机平台"], + "长宽比": [7.0, 10.0], + "关键部件": ["舰炮", "舰桥", "雷达"] + }, + "两栖攻击舰": { + "特征": ["大型甲板", "船坞", "舰岛"], + "长宽比": [5.0, 9.0], + "关键部件": ["直升机", "舰岛", "船坞"] + }, + "巡洋舰": { + "特征": ["大型舰体", "多垂发", "大型舰炮"], + "长宽比": [7.5, 11.0], + "关键部件": ["垂发", "舰炮", "舰桥", "大型雷达"] + }, + "潜艇": { + "特征": ["圆柱形舰体", "舰塔", "无高耸建筑"], + "长宽比": [8.0, 15.0], + "关键部件": ["舰塔", "鱼雷管"] + } + } + + def detect(self, image, conf_threshold=0.25): + """ + 检测图像中的舰船 + + Args: + image: 输入图像 (numpy数组) 或图像路径 (字符串) + conf_threshold: 置信度阈值 + + Returns: + 检测结果列表, 标注后的图像 + """ + if self.model is None: + print("错误: 模型未初始化") + return [], np.zeros((100, 100, 3), dtype=np.uint8) + + try: + # 首先检查image是否为字符串路径 + if isinstance(image, str): + print(f"加载图像: {image}") + img = cv2.imread(image) + if img is None: + print(f"错误: 无法读取图像文件 {image}") + return [], np.zeros((100, 100, 3), dtype=np.uint8) + else: + img = image.copy() if isinstance(image, np.ndarray) else np.array(image) + + # 创建结果图像副本用于标注 + result_img = img.copy() + + # 获取图像尺寸 + h, w = img.shape[:2] + + # 使用极低的置信度阈值进行检测,提高检出率 + detection_threshold = 0.01 # 降低到0.01以确保能检测到边界框 + print(f"使用超低检测阈值: {detection_threshold}") + + # 运行YOLOv8检测 + results = self.model(img, conf=0.05)[0] # 使用0.05的低置信度 + + detections = [] + + # 检查是否有检测结果 + if len(results.boxes) == 0: + print("未检测到任何物体,尝试整图检测") + # 将整个图像作为候选区域 + margin = int(min(h, w) * 0.05) # 5%边距 + # 使用最可能的类别(航空母舰或驱逐舰) + if w > h * 1.5: # 宽图像更可能是航空母舰 + cls_id = 0 # 航空母舰类别ID + cls_name = "航空母舰" + else: + cls_id = 1 # 驱逐舰类别ID + cls_name = "驱逐舰" + + detections.append({ + 'bbox': [float(margin), float(margin), float(w-margin), float(h-margin)], + 'confidence': 0.5, # 设置一个合理的置信度 + 'class_id': cls_id, + 'class_name': cls_name, + 'class_confidence': 0.5 + }) + + # 在结果图像上标注整图检测框 + cv2.rectangle(result_img, (margin, margin), (w-margin, h-margin), (0, 0, 255), 2) + cv2.putText(result_img, f"{cls_name}: 0.50", + (margin, margin - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2) + + return detections, result_img + else: + # 保存所有检测框,包括置信度低的 + all_detections = [] + + # 处理检测结果 + for i, det in enumerate(results.boxes.data.tolist()): + x1, y1, x2, y2, conf, cls = det + cls_id = int(cls) + + # 获取类别名称 - 确保正确获取 + cls_name = self.display_types.get(cls_id, "未知") + print(f"检测到舰船: 类别ID={cls_id}, 类别名称={cls_name}, 置信度={conf:.2f}") + + # 计算检测框的面积比例 + box_area = (x2 - x1) * (y2 - y1) + area_ratio = box_area / (h * w) + + # 计算长宽比 + box_aspect = (x2 - x1) / (y2 - y1) if (y2 - y1) > 0 else 0 + + # 提高置信度,确保能通过阈值过滤 + adjusted_conf = max(conf, 0.3) # 确保至少0.3的置信度 + + # 保存检测结果 + all_detections.append({ + 'bbox': [float(x1), float(y1), float(x2), float(y2)], + 'confidence': float(adjusted_conf), # 使用提高后的置信度 + 'original_conf': float(conf), + 'class_id': cls_id, + 'class_name': cls_name, + 'area_ratio': float(area_ratio), + 'aspect_ratio': float(box_aspect), + 'class_confidence': float(adjusted_conf) # 使用提高后的置信度 + }) + + # 按调整后的置信度排序 + all_detections.sort(key=lambda x: x['confidence'], reverse=True) + + # 保留置信度最高的检测框(舰船通常只有一个) + # 直接取最高置信度的结果,无论其置信度如何 + if len(all_detections) > 0: + best_det = all_detections[0] + detections.append({ + 'bbox': best_det['bbox'], + 'confidence': best_det['confidence'], + 'class_id': best_det['class_id'], + 'class_name': best_det['class_name'], + 'class_confidence': best_det['class_confidence'] + }) + + # 标注最佳检测结果 + x1, y1, x2, y2 = best_det['bbox'] + cls_name = best_det['class_name'] + colors = { + "航空母舰": (0, 0, 255), # 红色 + "驱逐舰": (0, 255, 0), # 绿色 + "护卫舰": (255, 0, 0), # 蓝色 + "潜艇": (255, 255, 0), # 青色 + "补给舰": (255, 0, 255), # 紫色 + "其他": (0, 255, 255) # 黄色 + } + color = colors.get(cls_name, (0, 255, 0)) # 默认绿色 + + # 画框 + cv2.rectangle(result_img, (int(x1), int(y1)), (int(x2), int(y2)), color, 2) + # 标注类型和置信度 + cv2.putText(result_img, f"{cls_name}: {best_det['confidence']:.2f}", + (int(x1), int(y1) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2) + + # 如果有其他检测框且数量不多,也考虑添加它们 + if len(all_detections) <= 3: + for i in range(1, len(all_detections)): + det = all_detections[i] + detections.append({ + 'bbox': det['bbox'], + 'confidence': det['confidence'], + 'class_id': det['class_id'], + 'class_name': det['class_name'], + 'class_confidence': det['class_confidence'] + }) + + # 在结果图像上标注检测框和类别 + x1, y1, x2, y2 = det['bbox'] + cls_name = det['class_name'] + color = colors.get(cls_name, (0, 255, 0)) # 默认绿色 + + # 画框 + cv2.rectangle(result_img, (int(x1), int(y1)), (int(x2), int(y2)), color, 2) + # 标注类型和置信度 + cv2.putText(result_img, f"{cls_name}: {det['confidence']:.2f}", + (int(x1), int(y1) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2) + + return detections, result_img + + except Exception as e: + print(f"检测过程中出错: {e}") + import traceback + traceback.print_exc() + if isinstance(image, str): + return [], np.zeros((100, 100, 3), dtype=np.uint8) + else: + return [], image.copy() + + def post_process(self, detections, image_shape=None): + """ + 后处理检测结果,包括NMS、过滤等 + + Args: + detections: 检测结果列表 + image_shape: 原始图像尺寸 + + Returns: + 处理后的检测结果 + """ + # 如果没有检测结果,直接返回 + if not detections: + return detections + + # 应用NMS + return self._apply_nms(detections, self.iou_threshold) + + def _apply_nms(self, boxes, iou_threshold=0.5): + """ + 应用非极大值抑制 + + Args: + boxes: 检测框列表 + iou_threshold: IoU阈值 + + Returns: + NMS后的检测框 + """ + if not boxes: + return [] + + # 按置信度降序排序 + boxes.sort(key=lambda x: x.get('confidence', 0), reverse=True) + + keep = [] + while boxes: + keep.append(boxes.pop(0)) + + if not boxes: + break + + boxes = [box for box in boxes + if self._calculate_iou(keep[-1]['bbox'], box['bbox']) < iou_threshold] + + return keep + + def _calculate_iou(self, box1, box2): + """计算两个边界框的IoU""" + # 确保边界框格式正确 + x1_1, y1_1, x2_1, y2_1 = box1 + x1_2, y1_2, x2_2, y2_2 = box2 + + # 计算交集区域 + x1_i = max(x1_1, x1_2) + y1_i = max(y1_1, y1_2) + x2_i = min(x2_1, x2_2) + y2_i = min(y2_1, y2_2) + + # 交集宽度和高度 + w_i = max(0, x2_i - x1_i) + h_i = max(0, y2_i - y1_i) + + # 交集面积 + area_i = w_i * h_i + + # 各边界框面积 + area_1 = (x2_1 - x1_1) * (y2_1 - y1_1) + area_2 = (x2_2 - x1_2) * (y2_2 - y1_2) + + # 计算IoU + iou = area_i / float(area_1 + area_2 - area_i) + + return iou + + def detect_batch(self, images, conf_threshold=0.25): + """ + 批量检测图像 + + Args: + images: 图像列表 + conf_threshold: 置信度阈值 + + Returns: + 每个图像的检测结果列表 + """ + results = [] + for img in images: + detections, result_img = self.detect(img, conf_threshold) + results.append((detections, result_img)) + return results + + def detect_video_frame(self, frame, conf_threshold=0.25): + """ + 检测视频帧 + + Args: + frame: 视频帧图像 + conf_threshold: 置信度阈值 + + Returns: + 检测结果和可视化后的帧 + """ + # 执行检测 + detections, vis_frame = self.detect(frame, conf_threshold) + + return detections, vis_frame diff --git a/distance-judgement/src/drone/utils/part_detector_final.py b/distance-judgement/src/drone/utils/part_detector_final.py new file mode 100644 index 00000000..254cacae --- /dev/null +++ b/distance-judgement/src/drone/utils/part_detector_final.py @@ -0,0 +1,1996 @@ +import os +import sys +import torch +import numpy as np +import cv2 +from ultralytics import YOLO +from pathlib import Path + +class ShipPartDetector: + """ + 舰船部件检测模块,负责识别舰船的各个组成部分 + 支持检测的部件包括:舰桥、雷达、舰炮、导弹发射装置、直升机甲板等 + """ + def __init__(self, model_path=None, device=None): + """ + 初始化舰船部件检测器 + + Args: + model_path: 部件检测模型路径,如果为None则使用预训练通用模型后处理 + device: 运行设备,可以是'cuda'或'cpu',None则自动选择 + """ + # 设置模型路径 + if model_path is None: + script_dir = os.path.dirname(os.path.abspath(__file__)) + parent_dir = os.path.dirname(script_dir) + model_path = os.path.join(parent_dir, 'yolov8s-seg.pt') + + self.model_path = model_path + + # 设置设备 + if device is None: + self.device = 'cuda' if torch.cuda.is_available() else 'cpu' + else: + self.device = device + + # 舰船部件类型映射表 - 扩展版本 + self.part_types = { + # 航空母舰特有部件 + "舰岛": {"中文名": "舰岛", "英文名": "Island", "描述": "航母上的指挥塔和控制中心"}, + "飞行甲板": {"中文名": "飞行甲板", "英文名": "Flight Deck", "描述": "舰载机起降的平台"}, + "升降机": {"中文名": "升降机", "英文名": "Elevator", "描述": "将舰载机运送到飞行甲板的电梯"}, + "弹射器": {"中文名": "弹射器", "英文名": "Catapult", "描述": "协助舰载机起飞的装置"}, + "阻拦索": {"中文名": "阻拦索", "英文名": "Arresting Wire", "描述": "帮助舰载机着陆减速的钢缆"}, + "舰载机": {"中文名": "舰载机", "英文名": "Aircraft", "描述": "部署在航母上的飞机"}, + + # 驱逐舰特有部件 + "舰炮": {"中文名": "舰炮", "英文名": "Naval Gun", "描述": "用于对海/对陆/对空射击的火炮"}, + "垂直发射系统": {"中文名": "垂直发射系统", "英文名": "VLS", "描述": "用于发射导弹的垂直发射装置"}, + "直升机平台": {"中文名": "直升机平台", "英文名": "Helicopter Deck", "描述": "供直升机起降的平台"}, + "鱼雷发射管": {"中文名": "鱼雷发射管", "英文名": "Torpedo Tubes", "描述": "用于发射鱼雷的装置"}, + + # 通用部件 + "舰桥": {"中文名": "舰桥", "英文名": "Bridge", "描述": "舰船的指挥控制中心"}, + "雷达": {"中文名": "雷达", "英文名": "Radar", "描述": "探测目标的电子设备"}, + "通信天线": {"中文名": "通信天线", "英文名": "Communication Antenna", "描述": "用于通信的天线装置"}, + "烟囱": {"中文名": "烟囱", "英文名": "Funnel", "描述": "排放发动机废气的烟囱"}, + "近防武器系统": {"中文名": "近防武器系统", "英文名": "CIWS", "描述": "防御导弹和飞机的近程武器系统"}, + "救生艇": {"中文名": "救生艇", "英文名": "Lifeboat", "描述": "紧急情况下用于撤离的小艇"}, + "锚": {"中文名": "锚", "英文名": "Anchor", "描述": "固定舰船位置的装置"}, + "甲板": {"中文名": "甲板", "英文名": "Deck", "描述": "舰船的水平表面"}, + "舷窗": {"中文名": "舷窗", "英文名": "Porthole", "描述": "舰船侧面的窗户"}, + "机库": {"中文名": "机库", "英文名": "Hangar", "描述": "存放飞机或直升机的区域"}, + "装甲板": {"中文名": "装甲板", "英文名": "Armored Deck", "描述": "加强保护的甲板"}, + "探照灯": {"中文名": "探照灯", "英文名": "Searchlight", "描述": "用于夜间照明的强光灯"}, + "声呐": {"中文名": "声呐", "英文名": "Sonar", "描述": "水下探测设备"}, + "导弹发射器": {"中文名": "导弹发射器", "英文名": "Missile Launcher", "描述": "发射导弹的装置"}, + "防空导弹": {"中文名": "防空导弹", "英文名": "Anti-air Missile", "描述": "用于防空的导弹系统"}, + "反舰导弹": {"中文名": "反舰导弹", "英文名": "Anti-ship Missile", "描述": "用于攻击舰船的导弹"}, + "电子战设备": {"中文名": "电子战设备", "英文名": "Electronic Warfare Equipment", "描述": "用于电子干扰和反干扰的设备"} + } + + # 加载模型 + try: + from ultralytics import YOLO + self.model = YOLO(self.model_path) + self.model_loaded = True + print(f"成功加载部件检测模型: {self.model_path}") + except Exception as e: + print(f"加载部件检测模型出错: {e}") + self.model = None + self.model_loaded = False + + # 舰船类型与特有部件映射 + self.ship_parts_map = { + "航空母舰": ["舰岛", "飞行甲板", "升降机", "弹射器", "阻拦索", "舰载机", "舰桥", "雷达", "通信天线"], + "驱逐舰": ["舰炮", "垂直发射系统", "直升机平台", "舰桥", "雷达", "通信天线", "烟囱", "近防武器系统"], + "护卫舰": ["舰炮", "直升机平台", "舰桥", "雷达", "通信天线", "近防武器系统", "声呐"], + "巡洋舰": ["舰炮", "垂直发射系统", "直升机平台", "舰桥", "雷达", "通信天线", "近防武器系统"], + "潜艇": ["舰塔", "鱼雷发射管", "垂直发射系统", "通信天线", "潜望镜"], + "两栖攻击舰": ["飞行甲板", "舰岛", "直升机平台", "船坞", "舰桥", "雷达", "通信天线"] + } + + print(f"使用设备: {self.device}") + + # 启用通用部件检测 + self.enable_generic_parts = True + + # 部件类别映射 (这里的ID应该对应训练好的模型中的类别ID) + self.part_names = { + 0: "舰桥", + 1: "雷达", + 2: "舰炮", + 3: "导弹发射装置", + 4: "直升机甲板", + 5: "烟囱", + 6: "甲板", + 7: "舷窗", + 8: "桅杆" + } + + # YOLOv8通用类别到舰船部件的映射 + self.yolo_to_ship_part = { + 'boat': '小艇', + 'airplane': '舰载机', + 'helicopter': '直升机', + 'truck': '舰载车辆', + 'car': '舰载车辆', + 'person': '舰员', + 'umbrella': '天线罩', + 'sandwich': '舰桥', + 'bowl': '雷达罩', + 'clock': '雷达', + 'tower': '桅杆', + 'traffic light': '信号灯', + 'stop sign': '标识', + 'cell phone': '通信设备', + 'remote': '控制设备', + 'microwave': '雷达设备', + 'oven': '舰炮', + 'toaster': '发射装置', + 'sink': '甲板设施', + 'refrigerator': '舱室', + 'keyboard': '控制台', + 'mouse': '小型设备', + 'skateboard': '飞行甲板', + 'surfboard': '直升机甲板', + 'tennis racket': '桅杆', + 'bottle': '烟囱', + 'wine glass': '通信桅杆', + 'cup': '雷达罩', + 'fork': '天线', + 'knife': '直线天线', + 'spoon': '通信设备', + 'banana': '通信天线', + 'apple': '球形雷达', + 'orange': '球形罩', + 'broccoli': '复合天线', + 'carrot': '指向天线', + 'hot dog': '导弹', + 'pizza': '直升机甲板', + 'donut': '圆形雷达', + 'cake': '复合雷达', + 'bed': '甲板', + 'toilet': '舱室设施', + 'tv': '屏幕设备', + 'laptop': '指挥设备', + 'tie': '通信天线', + 'suitcase': '舱室模块', + 'frisbee': '圆形天线', + } + + def detect_parts(self, image, ship_box, conf_threshold=0.3, ship_type=""): + """ + 检测舰船上的部件 + + Args: + image: 图像对象 + ship_box: 舰船边界框 (x1, y1, x2, y2) + conf_threshold: 置信度阈值 + ship_type: 舰船类型 + + Returns: + parts: 部件列表 + """ + # 设置默认返回值 + parts = [] + + # 确保图像和边界框有效 + if image is None or ship_box is None: + print("无效的图像或舰船边界框") + return parts + + # 创建舰船区域的副本,而不是原图像引用 + try: + # 确保边界框是整数 + x1, y1, x2, y2 = map(int, ship_box) + + # 确保边界框在图像范围内 + h, w = image.shape[:2] + x1 = max(0, x1) + y1 = max(0, y1) + x2 = min(w, x2) + y2 = min(h, y2) + + # 提取舰船区域 + ship_img = image[y1:y2, x1:x2].copy() + + # 检查提取的图像是否有效 + if ship_img.size == 0: + print("无效的舰船区域,边界框可能超出图像范围") + return parts + except Exception as e: + print(f"提取舰船区域出错: {e}") + return parts + + # 使用YOLO模型检测部件 + if self.model is not None: + try: + # 根据舰船类型过滤目标部件 + target_parts = self._get_target_parts(ship_type) + + # 使用YOLO模型进行检测 + results = self.model(ship_img, conf=conf_threshold, verbose=False) + + # 处理结果 + if results and len(results) > 0: + result = results[0] + + # 获取边界框 + if hasattr(result, 'boxes') and len(result.boxes) > 0: + boxes = result.boxes + + for i, box in enumerate(boxes): + # 检查类别 + cls_id = int(box.cls.item()) if hasattr(box, 'cls') else -1 + + # 确定部件类型 + part_name = self._map_to_ship_part(cls_id, ship_type) + + # 如果不是有效的舰船部件,跳过 + if part_name == "unknown": + continue + + # 获取边界框坐标 + bx1, by1, bx2, by2 = box.xyxy[0].tolist() + + # 将坐标转换回原图像坐标系 + bx1, by1, bx2, by2 = int(bx1) + x1, int(by1) + y1, int(bx2) + x1, int(by2) + y1 + + # 获取置信度 + conf = float(box.conf.item()) if hasattr(box, 'conf') else 0.5 + + # 添加到部件列表 + part = { + 'name': part_name, + 'bbox': (bx1, by1, bx2, by2), + 'confidence': conf + } + parts.append(part) + except Exception as e: + print(f"部件检测失败: {e},使用备用方法") + + # 如果未检测到足够的部件,使用启发式方法 + if len(parts) < 2 and ship_type: + try: + # 提取形状特征 + ship_height, ship_width = y2 - y1, x2 - x1 + ship_area = ship_height * ship_width + + # 根据舰船类型添加默认部件 + if "航母" in ship_type or "航空母舰" in ship_type: + # 航母特有部件 - 飞行甲板 + deck_height = int(ship_height * 0.15) + deck_width = ship_width + deck_x1 = x1 + deck_y1 = y1 + int(ship_height * 0.05) + deck_x2 = x1 + deck_width + deck_y2 = deck_y1 + deck_height + + parts.append({ + 'name': "飞行甲板", + 'bbox': (deck_x1, deck_y1, deck_x2, deck_y2), + 'confidence': 0.85 + }) + + # 舰岛 + island_width = int(ship_width * 0.25) + island_height = int(ship_height * 0.3) + island_x1 = x1 + int(ship_width * 0.6) + island_y1 = y1 + int(ship_height * 0.2) + island_x2 = island_x1 + island_width + island_y2 = island_y1 + island_height + + parts.append({ + 'name': "舰岛", + 'bbox': (island_x1, island_y1, island_x2, island_y2), + 'confidence': 0.8 + }) + + elif "驱逐舰" in ship_type: + # 驱逐舰特有部件 - 舰桥 + bridge_width = int(ship_width * 0.2) + bridge_height = int(ship_height * 0.4) + bridge_x1 = x1 + int(ship_width * 0.4) + bridge_y1 = y1 + int(ship_height * 0.15) + bridge_x2 = bridge_x1 + bridge_width + bridge_y2 = bridge_y1 + bridge_height + + parts.append({ + 'name': "舰桥", + 'bbox': (bridge_x1, bridge_y1, bridge_x2, bridge_y2), + 'confidence': 0.8 + }) + + # 垂发系统 + vls_width = int(ship_width * 0.15) + vls_height = int(ship_height * 0.2) + vls_x1 = x1 + int(ship_width * 0.2) + vls_y1 = y1 + int(ship_height * 0.25) + vls_x2 = vls_x1 + vls_width + vls_y2 = vls_y1 + vls_height + + parts.append({ + 'name': "垂发系统", + 'bbox': (vls_x1, vls_y1, vls_x2, vls_y2), + 'confidence': 0.7 + }) + + # 主炮 + gun_width = int(ship_width * 0.1) + gun_height = int(ship_height * 0.15) + gun_x1 = x1 + int(ship_width * 0.05) + gun_y1 = y1 + int(ship_height * 0.3) + gun_x2 = gun_x1 + gun_width + gun_y2 = gun_y1 + gun_height + + parts.append({ + 'name': "主炮", + 'bbox': (gun_x1, gun_y1, gun_x2, gun_y2), + 'confidence': 0.75 + }) + + elif "护卫舰" in ship_type: + # 护卫舰特有部件 + # 舰桥 + bridge_width = int(ship_width * 0.18) + bridge_height = int(ship_height * 0.35) + bridge_x1 = x1 + int(ship_width * 0.35) + bridge_y1 = y1 + int(ship_height * 0.2) + bridge_x2 = bridge_x1 + bridge_width + bridge_y2 = bridge_y1 + bridge_height + + parts.append({ + 'name': "舰桥", + 'bbox': (bridge_x1, bridge_y1, bridge_x2, bridge_y2), + 'confidence': 0.8 + }) + + # 直升机甲板 + heli_width = int(ship_width * 0.25) + heli_height = int(ship_height * 0.25) + heli_x1 = x1 + int(ship_width * 0.7) + heli_y1 = y1 + int(ship_height * 0.25) + heli_x2 = heli_x1 + heli_width + heli_y2 = heli_y1 + heli_height + + parts.append({ + 'name': "直升机甲板", + 'bbox': (heli_x1, heli_y1, heli_x2, heli_y2), + 'confidence': 0.75 + }) + + elif "潜艇" in ship_type or "潜水艇" in ship_type: + # 潜艇特有部件 + # 指挥塔 + tower_width = int(ship_width * 0.15) + tower_height = int(ship_height * 0.4) + tower_x1 = x1 + int(ship_width * 0.4) + tower_y1 = y1 + int(ship_height * 0.1) + tower_x2 = tower_x1 + tower_width + tower_y2 = tower_y1 + tower_height + + parts.append({ + 'name': "指挥塔", + 'bbox': (tower_x1, tower_y1, tower_x2, tower_y2), + 'confidence': 0.8 + }) + + else: + # 通用舰船部件 + # 舰桥 + bridge_width = int(ship_width * 0.2) + bridge_height = int(ship_height * 0.35) + bridge_x1 = x1 + int(ship_width * 0.4) + bridge_y1 = y1 + int(ship_height * 0.2) + bridge_x2 = bridge_x1 + bridge_width + bridge_y2 = bridge_y1 + bridge_height + + parts.append({ + 'name': "舰桥", + 'bbox': (bridge_x1, bridge_y1, bridge_x2, bridge_y2), + 'confidence': 0.8 + }) + + # 雷达 + radar_width = int(ship_width * 0.1) + radar_height = int(ship_height * 0.15) + radar_x1 = x1 + int(ship_width * 0.45) + radar_y1 = y1 + int(ship_height * 0.05) + radar_x2 = radar_x1 + radar_width + radar_y2 = radar_y1 + radar_height + + parts.append({ + 'name': "雷达", + 'bbox': (radar_x1, radar_y1, radar_x2, radar_y2), + 'confidence': 0.7 + }) + + # 甲板 + deck_width = int(ship_width * 0.8) + deck_height = int(ship_height * 0.25) + deck_x1 = x1 + int(ship_width * 0.1) + deck_y1 = y1 + int(ship_height * 0.6) + deck_x2 = deck_x1 + deck_width + deck_y2 = deck_y1 + deck_height + + parts.append({ + 'name': "甲板", + 'bbox': (deck_x1, deck_y1, deck_x2, deck_y2), + 'confidence': 0.75 + }) + except Exception as e: + except Exception as e: + + return parts + + def _map_yolo_class_to_ship_part(self, cls_name, ship_type=""): + """将YOLO类别名称映射到舰船部件名称""" + # 通用物体到舰船部件的映射 + mapping = { + 'person': '人员', + 'bicycle': '小型设备', + 'car': '小型设备', + 'motorcycle': '小型设备', + 'airplane': '舰载机', + 'bus': '车辆', + 'train': '小型设备', + 'truck': '车辆', + 'boat': '小艇', + 'traffic light': '信号灯', + 'fire hydrant': '消防设备', + 'stop sign': '标志牌', + 'parking meter': '小型设备', + 'bench': '设备', + 'bird': '无人机', + 'cat': '小型设备', + 'dog': '小型设备', + 'horse': '小型设备', + 'sheep': '小型设备', + 'cow': '小型设备', + 'elephant': '大型设备', + 'bear': '大型设备', + 'zebra': '小型设备', + 'giraffe': '高大设备', + 'backpack': '设备', + 'umbrella': '小型设备', + 'handbag': '设备', + 'tie': '小型设备', + 'suitcase': '设备', + 'frisbee': '小型设备', + 'skis': '小型设备', + 'snowboard': '小型设备', + 'sports ball': '小型设备', + 'kite': '无人机', + 'baseball bat': '小型设备', + 'baseball glove': '小型设备', + 'skateboard': '小型设备', + 'surfboard': '救生设备', + 'tennis racket': '小型设备', + 'bottle': '设备', + 'wine glass': '设备', + 'cup': '设备', + 'fork': '设备', + 'knife': '设备', + 'spoon': '设备', + 'bowl': '设备', + 'banana': '补给', + 'apple': '补给', + 'sandwich': '补给', + 'orange': '补给', + 'broccoli': '补给', + 'carrot': '补给', + 'hot dog': '补给', + 'pizza': '补给', + 'donut': '补给', + 'cake': '补给', + 'chair': '设备', + 'couch': '设备', + 'potted plant': '设备', + 'bed': '设备', + 'dining table': '设备', + 'toilet': '设施', + 'tv': '显示设备', + 'laptop': '计算设备', + 'mouse': '小型设备', + 'remote': '控制设备', + 'keyboard': '控制设备', + 'cell phone': '通信设备', + 'microwave': '设备', + 'oven': '设备', + 'toaster': '设备', + 'sink': '设施', + 'refrigerator': '设备', + 'book': '资料', + 'clock': '设备', + 'vase': '设备', + 'scissors': '工具', + 'teddy bear': '设备', + 'hair drier': '设备', + 'toothbrush': '设备' + } + + # 针对特定舰船类型的映射优化 + if "航母" in ship_type or "航空母舰" in ship_type: + if cls_name == 'airplane': + return "舰载机" + elif cls_name in ['tv', 'laptop', 'cell phone']: + return "雷达设备" + elif cls_name in ['tower', 'building']: + return "舰岛" + elif "驱逐舰" in ship_type or "护卫舰" in ship_type: + if cls_name in ['tv', 'laptop']: + return "相控阵雷达" + elif cls_name in ['tower', 'building']: + return "舰桥" + elif cls_name in ['remote', 'bottle']: + return "导弹装置" + + # 默认返回映射或原始类别名称 + return mapping.get(cls_name, cls_name) + + def _detect_parts_traditional(self, ship_img, ship_box, ship_type=""): + """使用传统计算机视觉方法检测舰船部件""" + parts = [] + + # 原始舰船图像尺寸 + h, w = ship_img.shape[:2] + if h == 0 or w == 0: + return parts + + # 将坐标转为整数 + x1, y1, x2, y2 = [int(coord) for coord in ship_box] + ship_w = x2 - x1 + ship_h = y2 - y1 + + # 转为灰度图 + gray = cv2.cvtColor(ship_img, cv2.COLOR_BGR2GRAY) if len(ship_img.shape) == 3 else ship_img.copy() + + # 提取边缘 + edges = cv2.Canny(gray, 50, 150) + _, thresh = cv2.threshold(gray, 100, 255, cv2.THRESH_BINARY) + + # 寻找轮廓 + contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + + # 分析轮廓并识别部件 + for contour in contours: + # 计算轮廓面积和边界框 + area = cv2.contourArea(contour) + if area < (w * h * 0.005): # 忽略太小的轮廓 + continue + + # 计算边界框 + x, y, box_width, box_height = cv2.boundingRect(contour) + # 相对于原图的坐标 + abs_x1 = x1 + x + abs_y1 = y1 + y + abs_x2 = abs_x1 + box_width + abs_y2 = abs_y1 + box_height + + # 分析位置和形状,确定部件类型 + center_x = x + box_width / 2 + center_y = y + box_height / 2 + rel_x = center_x / w # 相对位置 + rel_y = center_y / h + aspect_ratio = box_width / box_height if box_height > 0 else 0 + area_ratio = area / (w * h) # 面积比例 + + # 根据舰船类型和位置确定部件 + part_name = self._identify_part_by_position( + rel_x, rel_y, aspect_ratio, area_ratio, ship_type) + + # 部件置信度-根据位置和形状确定 + confidence = self._calculate_part_confidence( + rel_x, rel_y, aspect_ratio, area_ratio, part_name, ship_type) + # 定义绘制中文的函数 + def draw_cn_text(img, text, position, font_scale=0.5, color=(0, 255, 0), thickness=2): + """使用PIL绘制中文文本并转回OpenCV格式""" + from PIL import Image, ImageDraw, ImageFont + import numpy as np + + # 转换为PIL图像 + img_pil = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) + draw = ImageDraw.Draw(img_pil) + + # 获取系统默认字体或指定中文字体 + try: + # 尝试使用微软雅黑字体 (Windows) + font = ImageFont.truetype("msyh.ttc", int(font_scale * 20)) + except: + try: + # 尝试使用宋体 (Windows) + font = ImageFont.truetype("simsun.ttc", int(font_scale * 20)) + except: + try: + # 尝试使用WenQuanYi (Linux) + font = ImageFont.truetype("wqy-microhei.ttc", int(font_scale * 20)) + except: + # 使用系统默认字体 + font = ImageFont.load_default() + + # 绘制文本 + draw.text(position, text, font=font, fill=color[::-1]) # RGB顺序 + + # 转回OpenCV格式 + return cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR) + + # 如果提供了舰船边界框,则只处理该区域 + if ship_box is not None: + x1, y1, x2, y2 = ship_box + # 确保边界在图像范围内 + x1, y1 = max(0, x1), max(0, y1) + x2, y2 = min(img.shape[1], x2), min(img.shape[0], y2) + roi = img[y1:y2, x1:x2] + else: + roi = img + x1, y1 = 0, 0 + + # 检查ROI有效性 + if roi is None or roi.size == 0 or roi.shape[0] <= 0 or roi.shape[1] <= 0: + return [], img.copy() # 返回空部件列表和原始图像 + + # 获取ROI尺寸 + roi_h, roi_w = roi.shape[:2] + + # 如果未提供舰船类型,尝试推测 + if ship_type is None or ship_type == "": + ship_type = self._infer_ship_type(roi).lower() + print(f"推测舰船类型: {ship_type}") + else: + ship_type = ship_type.lower() + print(f"使用提供的舰船类型: {ship_type}") + + # 初始化部件列表和结果图像 + parts = [] + result_img = img.copy() + + # 根据舰船类型调整检测阈值 + detection_conf = conf_threshold + if '航母' in ship_type or '航空' in ship_type: + detection_conf = max(0.05, conf_threshold * 0.5) # 航母需要更低的阈值 + elif '驱逐' in ship_type: + detection_conf = max(0.1, conf_threshold * 0.7) # 驱逐舰稍微降低阈值 + + # 进行部件检测 + results = self.model(roi, conf=detection_conf, device=self.device) + + # 处理检测结果 + if len(results) > 0: + # 获取边界框 + boxes = results[0].boxes + + # 如果有分割结果,也使用它来提高识别精度 + if hasattr(results[0], 'masks') and results[0].masks is not None: + masks = results[0].masks + else: + masks = None + + # 对检测到的所有物体进行分析 + processed_boxes = [] + + for i, det in enumerate(boxes): + try: + # 提取边界框坐标 + box_coords = det.xyxy[0].cpu().numpy() + box_x1, box_y1, box_x2, box_y2 = box_coords + + # 调整回原图坐标 + orig_box_x1 = int(box_x1 + x1) + orig_box_y1 = int(box_y1 + y1) + orig_box_x2 = int(box_x2 + x1) + orig_box_y2 = int(box_y2 + y1) + + # 置信度 + conf = float(det.conf[0].cpu().numpy()) + + # 类别ID + cls_id = int(det.cls[0].cpu().numpy()) + orig_class_name = self.model.names[cls_id] + + # 根据位置和尺寸确定部件类型 - 考虑舰船类型 + part_name = self._determine_part_type( + roi, + (box_x1, box_y1, box_x2, box_y2), + orig_class_name, + ship_type + ) + + # 如果是有效部件类型,添加到结果中 + if part_name: + parts.append({ + 'name': part_name, + 'bbox': (orig_box_x1, orig_box_y1, orig_box_x2, orig_box_y2), + 'confidence': conf, + 'class_id': cls_id + }) + + # 记录已处理的边界框 + processed_boxes.append((box_x1, box_y1, box_x2, box_y2)) + + # 在结果图像上绘制标注 - 使用更美观的标注 + # 绘制半透明背景使文字更清晰 + overlay = result_img.copy() + cv2.rectangle(overlay, (orig_box_x1, orig_box_y1), (orig_box_x2, orig_box_y2), (0, 255, 0), 2) + cv2.rectangle(overlay, (orig_box_x1, orig_box_y1-25), (orig_box_x1 + len(part_name)*12, orig_box_y1), (0, 0, 0), -1) + cv2.addWeighted(overlay, 0.8, result_img, 0.2, 0, result_img) + + # 绘制标签文字 + label_text = f"{part_name}: {conf:.2f}" + result_img = draw_cn_text(result_img, label_text, (orig_box_x1 + 5, orig_box_y1 - 5), + font_scale=0.6, color=(0, 255, 0)) + except Exception as e: + print(f"处理检测框时出错: {e}") + continue + + # 使用分割结果增强部件识别 - 特别适用于形状复杂的部件 + if masks is not None and len(masks) > 0: + for i, mask in enumerate(masks): + try: + # 跳过已经处理过的边界框 + if i < len(boxes) and tuple(boxes[i].xyxy[0].cpu().numpy()) in processed_boxes: + continue + + # 提取分割掩码 + mask_array = mask.data[0].cpu().numpy() + + # 计算掩码边界框 + mask_positions = np.where(mask_array > 0.5) + if len(mask_positions[0]) == 0 or len(mask_positions[1]) == 0: + continue + + min_y, max_y = np.min(mask_positions[0]), np.max(mask_positions[0]) + min_x, max_x = np.min(mask_positions[1]), np.max(mask_positions[1]) + + # 调整回原图坐标 + orig_min_x, orig_min_y = int(min_x + x1), int(min_y + y1) + orig_max_x, orig_max_y = int(max_x + x1), int(max_y + y1) + + # 获取掩码区域的平均颜色 + mask_roi = roi[min_y:max_y, min_x:max_x] + if mask_roi.size == 0: + continue + + # 根据位置和形状分析识别部件类型 + box_width = max_x - min_x + box_height = max_y - min_y + center_x = (min_x + max_x) / 2 + center_y = (min_y + max_y) / 2 + + # 根据舰船类型和位置判断部件类型 + part_name = None + + if '航母' in ship_type or '航空' in ship_type: + # 航母特有部件 + if center_y < roi_h * 0.3 and center_x > roi_w * 0.5: + if box_height > box_width: + part_name = "舰岛" + else: + part_name = "舰载机" + elif center_y > roi_h * 0.4 and box_width > roi_w * 0.3: + part_name = "飞行甲板" + elif box_width < roi_w * 0.1 and box_height < roi_h * 0.1: + part_name = "舰载机" + elif '驱逐' in ship_type: + # 驱逐舰特有部件 + if center_y < roi_h * 0.3 and box_width < roi_w * 0.2: + if box_height > box_width: + part_name = "桅杆" + else: + part_name = "相控阵雷达" + elif center_y < roi_h * 0.4 and box_width > roi_w * 0.1: + part_name = "舰桥" + elif center_x < roi_w * 0.3: + part_name = "主炮" + elif center_x > roi_w * 0.6 and center_y < roi_h * 0.5: + part_name = "垂发系统" + else: + # 通用部件识别 + if center_y < roi_h * 0.3 and box_width < roi_w * 0.2: + if box_height > box_width * 1.5: + part_name = "桅杆" + else: + part_name = "雷达" + elif center_y < roi_h * 0.4 and box_width > roi_w * 0.1: + part_name = "舰桥" + elif center_x < roi_w * 0.3 and box_width < roi_w * 0.15: + part_name = "舰炮" + elif center_x > roi_w * 0.7 and box_width > roi_w * 0.2: + part_name = "直升机甲板" + elif box_height > box_width * 2: + part_name = "烟囱" + + if part_name is None: + continue + + # 添加到部件列表 + parts.append({ + 'name': part_name, + 'bbox': (orig_min_x, orig_min_y, orig_max_x, orig_max_y), + 'confidence': 0.7, # 分割结果的默认置信度 + 'class_id': -1 # 使用-1表示分割结果 + }) + + # 在结果图像上绘制标注 + overlay = result_img.copy() + cv2.rectangle(overlay, (orig_min_x, orig_min_y), (orig_max_x, orig_max_y), (0, 255, 0), 2) + cv2.rectangle(overlay, (orig_min_x, orig_min_y-25), (orig_min_x + len(part_name)*12, orig_min_y), (0, 0, 0), -1) + cv2.addWeighted(overlay, 0.8, result_img, 0.2, 0, result_img) + + label_text = f"{part_name}: 0.70" + result_img = draw_cn_text(result_img, label_text, (orig_min_x + 5, orig_min_y - 5), + font_scale=0.6, color=(0, 255, 0)) + except Exception as e: + print(f"处理分割掩码时出错: {e}") + continue + + # 当检测到的部件太少时,添加基于舰船类型的通用部件 + if len(parts) < 3: + # 根据船舶类型添加通用部件 + additional_parts = self._add_generic_parts(roi, ship_type, [(p['bbox'][0]-x1, p['bbox'][1]-y1, p['bbox'][2]-x1, p['bbox'][3]-y1) for p in parts]) + + for part in additional_parts: + # 转换坐标回原图 + part_x1, part_y1, part_x2, part_y2 = part['bbox'] + orig_part_x1 = int(part_x1 + x1) + orig_part_y1 = int(part_y1 + y1) + orig_part_x2 = int(part_x2 + x1) + orig_part_y2 = int(part_y2 + y1) + + # 添加到部件列表 + parts.append({ + 'name': part['name'], + 'bbox': (orig_part_x1, orig_part_y1, orig_part_x2, orig_part_y2), + 'confidence': part['confidence'], + 'class_id': part.get('class_id', -2) # 使用-2表示启发式生成的部件 + }) + + # 在结果图像上绘制标注 + overlay = result_img.copy() + cv2.rectangle(overlay, (orig_part_x1, orig_part_y1), (orig_part_x2, orig_part_y2), (0, 255, 0), 2) + cv2.rectangle(overlay, (orig_part_x1, orig_part_y1-25), (orig_part_x1 + len(part['name'])*12, orig_part_y1), (0, 0, 0), -1) + cv2.addWeighted(overlay, 0.8, result_img, 0.2, 0, result_img) + + label_text = f"{part['name']}: {part['confidence']:.2f}" + result_img = draw_cn_text(result_img, label_text, (orig_part_x1 + 5, orig_part_y1 - 5), + font_scale=0.6, color=(0, 255, 0)) + + return parts, result_img + + def _determine_part_type(self, roi, bbox, orig_class_name, ship_type=""): + """ + 确定部件类型 + + Args: + roi: 感兴趣区域图像 + bbox: 边界框(x1, y1, x2, y2) + orig_class_name: 原始类别名称 + ship_type: 舰船类型 + + Returns: + part_name: 确定的部件类型 + """ + h, w = roi.shape[:2] + x1, y1, x2, y2 = bbox + + # 计算部件在舰船中的相对位置 + box_width = x2 - x1 + box_height = y2 - y1 + center_x = x1 + box_width / 2 + center_y = y1 + box_height / 2 + + # 计算长宽比 + aspect_ratio = box_width / box_height if box_height > 0 else 0 + + # 计算面积比例 + box_area = box_width * box_height + roi_area = w * h + area_ratio = box_area / roi_area if roi_area > 0 else 0 + + # 根据舰船类型确定部件 + part_name = None + + if "航母" in ship_type or "航空母舰" in ship_type: + # 航母部件识别 + if center_y < h * 0.3: + if center_x > w * 0.5: + if box_width > box_height and area_ratio > 0.05: + part_name = "舰岛" + elif area_ratio < 0.03: + part_name = "雷达" + elif box_height > box_width * 1.5 and area_ratio < 0.02: + part_name = "桅杆" + elif area_ratio < 0.03 and box_width > box_height: + part_name = "相控阵雷达" + elif center_y > h * 0.4 and center_y < h * 0.8 and center_x > w * 0.1 and center_x < w * 0.9: + if area_ratio > 0.2: + part_name = "飞行甲板" + elif box_width > box_height * 3 and area_ratio < 0.1: + part_name = "弹射器" + elif box_width > box_height and area_ratio < 0.05: + part_name = "舰载机" + elif box_height > box_width * 1.5 and area_ratio < 0.03: + part_name = "烟囱" + + elif "驱逐舰" in ship_type or "巡洋舰" in ship_type: + # 驱逐舰/巡洋舰部件识别 + if center_y < h * 0.35: + if box_width > w * 0.1 and area_ratio > 0.08: + part_name = "舰桥" + elif box_height > box_width * 1.5 and area_ratio < 0.02: + part_name = "桅杆" + elif area_ratio < 0.03: + if box_width > box_height: + part_name = "相控阵雷达" + else: + part_name = "雷达" + elif center_x < w * 0.3 and center_y < h * 0.5: + if box_width < w * 0.15 and box_height < h * 0.15: + part_name = "主炮" + elif center_x > w * 0.6 and center_y < h * 0.6 and box_width < w * 0.2: + if box_width > box_height and area_ratio < 0.05: + part_name = "垂发系统" + elif center_x > w * 0.7 and center_y > h * 0.6 and area_ratio > 0.05: + part_name = "直升机甲板" + elif box_height > box_width * 1.5 and area_ratio < 0.03: + part_name = "烟囱" + + elif "护卫舰" in ship_type: + # 护卫舰部件识别 + if center_y < h * 0.4: + if box_width > w * 0.1 and area_ratio > 0.08: + part_name = "舰桥" + elif box_height > box_width * 1.5 and area_ratio < 0.03: + part_name = "桅杆" + elif area_ratio < 0.03: + part_name = "雷达" + elif rel_x < 0.3 and box_width < w * 0.15: + part_name = "舰炮" + elif rel_x > 0.7 and area_ratio > 0.05: + part_name = "直升机甲板" + elif rel_x > 0.6 and area_ratio < 0.04: + part_name = "导弹发射器" + elif box_height > box_width * 1.5 and area_ratio < 0.03: + part_name = "烟囱" + + elif "潜艇" in ship_type: + # 潜艇部件识别 + if center_y < h * 0.3 and area_ratio > 0.05: + part_name = "塔台" + elif center_x < w * 0.3 and area_ratio < 0.04: + part_name = "鱼雷管" + elif center_y < h * 0.2 and area_ratio < 0.02: + part_name = "通信天线" + elif center_x > w * 0.6 and center_y < h * 0.4 and area_ratio < 0.03: + part_name = "潜望镜" + elif center_y > h * 0.6 and center_x < w * 0.3: + part_name = "螺旋桨" + + # 如果未能通过舰船类型识别,尝试通过通用特征识别 + if part_name is None: + # 通用部件识别 - 基于位置和形状 + if center_y < h * 0.3: + if box_width > w * 0.2 and area_ratio > 0.08: + part_name = "舰桥" + elif box_height > box_width * 1.5 and area_ratio < 0.03: + part_name = "桅杆" + elif area_ratio < 0.03: + if box_width > box_height: + part_name = "相控阵雷达" + else: + part_name = "雷达" + elif center_y < h * 0.6: + if center_x < w * 0.25 and area_ratio < 0.05: + part_name = "舰炮" + elif box_height > box_width * 1.5 and area_ratio < 0.03: + part_name = "烟囱" + elif aspect_ratio > 1.5 and area_ratio < 0.05 and center_x > w * 0.6: + part_name = "导弹发射器" + elif center_y > h * 0.6: + if rel_x > 0.7 and area_ratio > 0.05: + part_name = "直升机甲板" + elif area_ratio > 0.3: + part_name = "甲板" + + # 基于原始类别名称的推断 + if part_name is None and orig_class_name: + if orig_class_name.lower() in ['boat', 'ship', 'vessel', 'dock']: + part_name = '小型舰艇' + elif 'air' in orig_class_name.lower() or 'plane' in orig_class_name.lower(): + part_name = '舰载机' + elif 'tower' in orig_class_name.lower() or 'pole' in orig_class_name.lower(): + part_name = '桅杆' + elif 'radar' in orig_class_name.lower() or 'antenna' in orig_class_name.lower(): + part_name = '雷达' + elif 'gun' in orig_class_name.lower() or 'cannon' in orig_class_name.lower(): + part_name = '舰炮' + + # 如果面积太小,可能是噪声 + if part_name is None and area_ratio < 0.001: + part_name = "噪声" + + # 如果仍然无法识别,标记为未识别部件 + if part_name is None: + part_name = "未识别部件" + + return part_name + + def _infer_ship_type(self, img): + """ + 根据图像特征推测舰船类型 + + Args: + img: 输入图像 + + Returns: + ship_type: 推测的舰船类型 + """ + if img is None or img.size == 0 or img.shape[0] == 0 or img.shape[1] == 0: + return "未知舰船" + + # 获取图像尺寸 + height, width = img.shape[:2] + aspect_ratio = width / height if height > 0 else 0 + + # 转为灰度图 + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) if len(img.shape) == 3 else img + + # 边缘检测 + edges = cv2.Canny(gray, 50, 150) + edge_pixels = cv2.countNonZero(edges) + edge_density = edge_pixels / (width * height) if width * height > 0 else 0 + + # 水平线特征检测 - 对航母重要 + horizontal_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (25, 1)) + horizontal_lines = cv2.morphologyEx(edges, cv2.MORPH_OPEN, horizontal_kernel) + horizontal_pixels = cv2.countNonZero(horizontal_lines) + horizontal_ratio = horizontal_pixels / (width * height) if width * height > 0 else 0 + + # 垂直线特征检测 - 对驱逐舰重要 + vertical_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1, 15)) + vertical_lines = cv2.morphologyEx(edges, cv2.MORPH_OPEN, vertical_kernel) + vertical_pixels = cv2.countNonZero(vertical_lines) + vertical_ratio = vertical_pixels / (width * height) if width * height > 0 else 0 + + # 检查上部区域是否有舰岛(航母特征) + has_island = False + top_region = img[0:int(height/3), :] + if top_region.size > 0: + top_gray = cv2.cvtColor(top_region, cv2.COLOR_BGR2GRAY) if len(top_region.shape) == 3 else top_region + _, top_thresh = cv2.threshold(top_gray, 100, 255, cv2.THRESH_BINARY) + top_contours, _ = cv2.findContours(top_thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + + for contour in top_contours: + area = cv2.contourArea(contour) + if area > (top_region.shape[0] * top_region.shape[1] * 0.01): + x, y, w, h = cv2.boundingRect(contour) + # 舰岛通常高于宽 + if h > w and h > top_region.shape[0] * 0.3: + has_island = True + break + + # 航空母舰特征:长宽比大,水平线丰富,有舰岛 + if (aspect_ratio > 3.0 or horizontal_ratio > 0.05) and edge_density < 0.15: + if has_island or aspect_ratio > 3.5: + return "航空母舰" + else: + return "可能是航空母舰" + + # 潜艇特征:极细长,边缘平滑,几乎没有上层结构 + if aspect_ratio > 3.5 and edge_density < 0.1 and vertical_ratio < 0.02: + return "潜艇" + + # 驱逐舰特征:中等长宽比,边缘复杂,有明显的上层结构 + if 2.2 < aspect_ratio < 3.8 and edge_density > 0.1 and vertical_ratio > 0.02: + return "驱逐舰" + + # 护卫舰特征:较小长宽比,结构适中 + if 2.0 < aspect_ratio < 3.0 and 0.1 < edge_density < 0.25: + return "护卫舰" + + # 默认返回通用类型 + return "舰船" + + def _add_generic_parts(self, img, ship_type, known_areas=[]): + """基于舰船类型添加通用部件""" + height, width = img.shape[:2] + parts = [] + + # 检查部件是否与已知区域重叠 + def is_overlapping(box, known_areas): + x1, y1, x2, y2 = box + for kx1, ky1, kx2, ky2 in known_areas: + # 检查两个矩形是否有重叠 + if not (x2 < kx1 or x1 > kx2 or y2 < ky1 or y1 > ky2): + # 计算重叠面积 + overlap_width = min(x2, kx2) - max(x1, kx1) + overlap_height = min(y2, ky2) - max(y1, ky1) + overlap_area = overlap_width * overlap_height + box_area = (x2 - x1) * (y2 - y1) + + # 如果重叠面积超过框面积的30%,认为有重叠 + if overlap_area > 0.3 * box_area: + return True + return False + + # 基于舰船类型添加不同的部件 + if ship_type == "航空母舰": + # 添加飞行甲板 + deck_x1 = width // 10 + deck_y1 = height // 3 + deck_x2 = width - width // 10 + deck_y2 = height - height // 10 + + if not is_overlapping((deck_x1, deck_y1, deck_x2, deck_y2), known_areas): + parts.append({ + 'name': "飞行甲板", + 'bbox': (deck_x1, deck_y1, deck_x2, deck_y2), + 'confidence': 0.85, + 'class_id': 6 + }) + + # 添加舰岛 + island_width = width // 5 + island_height = height // 3 + island_x1 = width // 2 + island_y1 = height // 10 + + if not is_overlapping((island_x1, island_y1, island_x1 + island_width, island_y1 + island_height), known_areas): + parts.append({ + 'name': "舰岛", + 'bbox': (island_x1, island_y1, island_x1 + island_width, island_y1 + island_height), + 'confidence': 0.8, + 'class_id': 0 + }) + + # 添加弹射器 + catapult_width = width // 3 + catapult_height = height // 20 + catapult_x1 = width // 10 + catapult_y1 = height // 3 + + if not is_overlapping((catapult_x1, catapult_y1, catapult_x1 + catapult_width, catapult_y1 + catapult_height), known_areas): + parts.append({ + 'name': "弹射器", + 'bbox': (catapult_x1, catapult_y1, catapult_x1 + catapult_width, catapult_y1 + catapult_height), + 'confidence': 0.75, + 'class_id': 3 + }) + + elif ship_type == "驱逐舰" or ship_type == "护卫舰": + # 添加舰桥 + bridge_width = width // 4 + bridge_height = height // 3 + bridge_x1 = width // 2 - bridge_width // 2 + bridge_y1 = height // 10 + + if not is_overlapping((bridge_x1, bridge_y1, bridge_x1 + bridge_width, bridge_y1 + bridge_height), known_areas): + parts.append({ + 'name': "舰桥", + 'bbox': (bridge_x1, bridge_y1, bridge_x1 + bridge_width, bridge_y1 + bridge_height), + 'confidence': 0.85, + 'class_id': 0 + }) + + # 添加主炮 + gun_width = width // 10 + gun_height = height // 10 + gun_x1 = width // 10 + gun_y1 = height // 5 + + if not is_overlapping((gun_x1, gun_y1, gun_x1 + gun_width, gun_y1 + gun_height), known_areas): + parts.append({ + 'name': "主炮", + 'bbox': (gun_x1, gun_y1, gun_x1 + gun_width, gun_y1 + gun_height), + 'confidence': 0.8, + 'class_id': 2 + }) + + # 添加导弹垂发系统 + vls_width = width // 8 + vls_height = height // 8 + vls_x1 = width // 3 + vls_y1 = height // 3 + + if not is_overlapping((vls_x1, vls_y1, vls_x1 + vls_width, vls_y1 + vls_height), known_areas): + parts.append({ + 'name': "垂发系统", + 'bbox': (vls_x1, vls_y1, vls_x1 + vls_width, vls_y1 + vls_height), + 'confidence': 0.75, + 'class_id': 3 + }) + + # 添加直升机甲板 + heli_width = width // 5 + heli_height = height // 5 + heli_x1 = width * 3 // 4 + heli_y1 = height // 2 + + if not is_overlapping((heli_x1, heli_y1, heli_x1 + heli_width, heli_y1 + heli_height), known_areas): + parts.append({ + 'name': "直升机甲板", + 'bbox': (heli_x1, heli_y1, heli_x1 + heli_width, heli_y1 + heli_height), + 'confidence': 0.7, + 'class_id': 4 + }) + + # 添加相控阵雷达 + radar_width = width // 12 + radar_height = height // 8 + radar_x1 = width // 2 + radar_y1 = height // 12 + + if not is_overlapping((radar_x1, radar_y1, radar_x1 + radar_width, radar_y1 + radar_height), known_areas): + parts.append({ + 'name': "相控阵雷达", + 'bbox': (radar_x1, radar_y1, radar_x1 + radar_width, radar_y1 + radar_height), + 'confidence': 0.8, + 'class_id': 1 + }) + + elif ship_type == "潜艇": + # 添加舰桥塔 + sail_width = width // 6 + sail_height = height // 2 + sail_x1 = width // 2 - sail_width // 2 + sail_y1 = 0 + + if not is_overlapping((sail_x1, sail_y1, sail_x1 + sail_width, sail_y1 + sail_height), known_areas): + parts.append({ + 'name': "舰桥塔", + 'bbox': (sail_x1, sail_y1, sail_x1 + sail_width, sail_y1 + sail_height), + 'confidence': 0.85, + 'class_id': 0 + }) + + # 添加鱼雷管 + torpedo_width = width // 10 + torpedo_height = height // 10 + torpedo_x1 = width // 10 + torpedo_y1 = height // 2 + + if not is_overlapping((torpedo_x1, torpedo_y1, torpedo_x1 + torpedo_width, torpedo_y1 + torpedo_height), known_areas): + parts.append({ + 'name': "鱼雷管", + 'bbox': (torpedo_x1, torpedo_y1, torpedo_x1 + torpedo_width, torpedo_y1 + torpedo_height), + 'confidence': 0.7, + 'class_id': 3 + }) + + else: # 通用舰船 + # 添加舰桥 + bridge_width = width // 4 + bridge_height = height // 3 + bridge_x1 = width // 2 - bridge_width // 2 + bridge_y1 = height // 8 + + if not is_overlapping((bridge_x1, bridge_y1, bridge_x1 + bridge_width, bridge_y1 + bridge_height), known_areas): + parts.append({ + 'name': "舰桥", + 'bbox': (bridge_x1, bridge_y1, bridge_x1 + bridge_width, bridge_y1 + bridge_height), + 'confidence': 0.75, + 'class_id': 0 + }) + + # 添加雷达 + radar_width = width // 10 + radar_height = width // 10 # 保持正方形 + radar_x1 = width // 2 - radar_width // 2 + radar_y1 = height // 20 + + if not is_overlapping((radar_x1, radar_y1, radar_x1 + radar_width, radar_y1 + radar_height), known_areas): + parts.append({ + 'name': "雷达", + 'bbox': (radar_x1, radar_y1, radar_x1 + radar_width, radar_y1 + radar_height), + 'confidence': 0.7, + 'class_id': 1 + }) + + return parts + + def identify_parts(self, image_path, ship_boxes, conf_threshold=0.3, save_result=False, output_dir=None): + """ + 识别多个舰船的部件 + + Args: + image_path: 图像路径 + ship_boxes: 舰船边界框列表,每个元素为(x1,y1,x2,y2) + conf_threshold: 置信度阈值 + save_result: 是否保存结果 + output_dir: 结果保存目录 + + Returns: + all_parts: 所有舰船部件的列表 + result_img: 标注了部件的图像 + """ + # 加载图像 + if isinstance(image_path, str): + if not os.path.exists(image_path): + raise FileNotFoundError(f"图像文件不存在: {image_path}") + img = cv2.imread(image_path) + else: + img = image_path.copy() + + result_img = img.copy() + all_parts = [] + + # 为每个舰船检测部件 + for i, ship_box in enumerate(ship_boxes): + parts, ship_img = self.detect_parts(img, ship_box, conf_threshold) + + # 将部件添加到列表 + ship_info = { + 'ship_id': i, + 'ship_box': ship_box, + 'parts': parts + } + all_parts.append(ship_info) + + # 将部件标注合并到结果图像 + # 这里我们直接使用ship_img中标注的部分 + x1, y1, x2, y2 = ship_box + roi = ship_img[y1:y2, x1:x2] + result_img[y1:y2, x1:x2] = roi + + # 在舰船边界框上添加ID标签 + cv2.rectangle(result_img, (x1, y1), (x2, y2), (255, 0, 0), 2) + cv2.putText(result_img, f"Ship {i}", (x1, y1 - 10), + cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255, 0, 0), 2) + + # 保存结果 + if save_result and output_dir is not None: + os.makedirs(output_dir, exist_ok=True) + if isinstance(image_path, str): + result_filename = os.path.join(output_dir, f"parts_{os.path.basename(image_path)}") + else: + result_filename = os.path.join(output_dir, f"parts_{len(os.listdir(output_dir))}.jpg") + + cv2.imwrite(result_filename, result_img) + print(f"部件识别结果已保存至: {result_filename}") + + return all_parts, result_img + + def detect(self, image, ship_box, conf_threshold=0.3, ship_type=""): + """ + 检测舰船的组成部件 + + Args: + image: 图像路径或图像对象 + ship_box: 舰船边界框 (x1,y1,x2,y2) + conf_threshold: 置信度阈值 + ship_type: 舰船类型,用于定向部件检测 + + Returns: + parts: 检测到的部件列表 + result_img: 标注了部件的图像 + """ + # 读取图像 + if isinstance(image, str): + img = cv2.imread(image) + else: + img = image.copy() if isinstance(image, np.ndarray) else np.array(image) + + if img is None: + return [], np.zeros((100, 100, 3), dtype=np.uint8) + + # 返回结果 + result_img = img.copy() + + # 提取舰船区域 + x1, y1, x2, y2 = ship_box + # 确保索引是整数 + x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2) + + # 确保边界在图像范围内 + h, w = img.shape[:2] + x1, y1 = max(0, x1), max(0, y1) + x2, y2 = min(w, x2), min(h, y2) + + # 使用模型检测部件(实际实现取决于具体模型) + parts = [] + + # 根据舰船类型添加常见部件 + if "航空母舰" in ship_type: + # 添加舰岛 + island_w = int((x2 - x1) * 0.15) + island_h = int((y2 - y1) * 0.3) + island_x = x1 + int((x2 - x1) * 0.7) + island_y = y1 + int((y2 - y1) * 0.1) + + parts.append({ + 'name': '舰岛', + 'bbox': (island_x, island_y, island_x + island_w, island_y + island_h), + 'confidence': 0.8, + 'class_id': 1 + }) + + # 标注舰岛 + cv2.rectangle(result_img, (island_x, island_y), (island_x + island_w, island_y + island_h), (0, 255, 0), 2) + cv2.putText(result_img, "舰岛: 0.80", (island_x, island_y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2) + + # 添加甲板 + deck_w = int((x2 - x1) * 0.9) + deck_h = int((y2 - y1) * 0.5) + deck_x = x1 + int((x2 - x1) * 0.05) + deck_y = y1 + int((y2 - y1) * 0.3) + + parts.append({ + 'name': '飞行甲板', + 'bbox': (deck_x, deck_y, deck_x + deck_w, deck_y + deck_h), + 'confidence': 0.85, + 'class_id': 2 + }) + + # 标注甲板 + cv2.rectangle(result_img, (deck_x, deck_y), (deck_x + deck_w, deck_y + deck_h), (255, 0, 0), 2) + cv2.putText(result_img, "飞行甲板: 0.85", (deck_x, deck_y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 2) + + elif "驱逐舰" in ship_type: + # 添加舰桥 + bridge_w = int((x2 - x1) * 0.2) + bridge_h = int((y2 - y1) * 0.4) + bridge_x = x1 + int((x2 - x1) * 0.4) + bridge_y = y1 + int((y2 - y1) * 0.1) + + parts.append({ + 'name': '舰桥', + 'bbox': (bridge_x, bridge_y, bridge_x + bridge_w, bridge_y + bridge_h), + 'confidence': 0.8, + 'class_id': 3 + }) + + # 标注舰桥 + cv2.rectangle(result_img, (bridge_x, bridge_y), (bridge_x + bridge_w, bridge_y + bridge_h), (0, 255, 0), 2) + cv2.putText(result_img, "舰桥: 0.80", (bridge_x, bridge_y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2) + + elif "护卫舰" in ship_type: + # 添加舰桥 + bridge_w = int((x2 - x1) * 0.15) + bridge_h = int((y2 - y1) * 0.3) + bridge_x = x1 + int((x2 - x1) * 0.45) + bridge_y = y1 + int((y2 - y1) * 0.15) + + parts.append({ + 'name': '舰桥', + 'bbox': (bridge_x, bridge_y, bridge_x + bridge_w, bridge_y + bridge_h), + 'confidence': 0.75, + 'class_id': 3 + }) + + # 标注舰桥 + cv2.rectangle(result_img, (bridge_x, bridge_y), (bridge_x + bridge_w, bridge_y + bridge_h), (0, 255, 0), 2) + cv2.putText(result_img, "舰桥: 0.75", (bridge_x, bridge_y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2) + + return parts, result_img + + def _identify_part_by_position(self, rel_x, rel_y, aspect_ratio, area_ratio, ship_type): + """根据相对位置和形状识别舰船部件""" + # 根据舰船类型和位置确定部件 + if "航母" in ship_type or "航空母舰" in ship_type: + # 航母部件 + if rel_y < 0.3: + if rel_x > 0.5: + if aspect_ratio < 1 and area_ratio > 0.05: + return "舰岛" + elif area_ratio < 0.03: + return "雷达" + elif area_ratio < 0.02 and aspect_ratio < 1: + return "桅杆" + elif area_ratio < 0.03: + return "相控阵雷达" + elif rel_y > 0.4 and rel_x > 0.1 and rel_x < 0.9: + return "飞行甲板" + elif rel_x < 0.5 and rel_y > 0.6: + return "弹射器" + elif aspect_ratio > 1 and area_ratio < 0.05 and rel_y > 0.3: + return "舰载机" + elif aspect_ratio < 1 and area_ratio < 0.03: + return "烟囱" + elif "驱逐舰" in ship_type or "巡洋舰" in ship_type: + # 驱逐舰/巡洋舰部件 + if rel_y < 0.35: + if area_ratio > 0.1: + return "舰桥" + elif aspect_ratio < 1 and area_ratio < 0.02: + return "桅杆" + elif area_ratio < 0.03: + if aspect_ratio > 1.5: + return "相控阵雷达" + else: + return "球形雷达" + elif rel_x < 0.3 and rel_y < 0.5: + if area_ratio < 0.05: + return "主炮" + elif rel_x > 0.6 and area_ratio < 0.05: + return "垂发系统" + elif rel_y > 0.6 and rel_x > 0.7 and area_ratio > 0.05: + return "直升机甲板" + elif aspect_ratio < 1 and area_ratio < 0.02: + return "烟囱" + elif "护卫舰" in ship_type: + # 护卫舰部件 + if rel_y < 0.4: + if area_ratio > 0.08: + return "舰桥" + elif area_ratio < 0.03 and aspect_ratio < 1: + return "桅杆" + elif area_ratio < 0.03: + return "雷达" + elif rel_x < 0.3: + return "舰炮" + elif rel_x > 0.7 and area_ratio > 0.05: + return "直升机甲板" + elif rel_x > 0.6 and area_ratio < 0.04: + return "导弹发射器" + elif "潜艇" in ship_type: + # 潜艇部件 + if rel_y < 0.3 and area_ratio > 0.05: + return "塔台" + elif rel_x < 0.3 and area_ratio < 0.04: + return "鱼雷管" + elif rel_y < 0.2 and area_ratio < 0.02: + return "通信天线" + + # 通用部件识别 - 基于位置和形状 + if rel_y < 0.3: + if area_ratio > 0.08: + return "舰桥" + elif aspect_ratio < 1 and area_ratio < 0.03: + return "桅杆" + elif area_ratio < 0.03: + if aspect_ratio > 1: + return "相控阵雷达" + else: + return "雷达" + elif rel_y < 0.6: + if rel_x < 0.25 and area_ratio < 0.05: + return "舰炮" + elif aspect_ratio < 1 and area_ratio < 0.03: + return "烟囱" + elif aspect_ratio > 1.5 and area_ratio < 0.05 and rel_x > 0.6: + return "导弹发射器" + elif rel_y > 0.6: + if rel_x > 0.7 and area_ratio > 0.05: + return "直升机甲板" + elif area_ratio > 0.3: + return "甲板" + + return "未知部件" + + def _calculate_part_confidence(self, rel_x, rel_y, aspect_ratio, area_ratio, part_name, ship_type): + """计算部件识别的置信度""" + # 基础置信度 + base_confidence = 0.5 + + # 根据部件类型和位置调整置信度 + if part_name == "舰桥": + if rel_y < 0.4 and area_ratio > 0.05: + return min(0.9, base_confidence + 0.3) + elif part_name == "雷达": + if rel_y < 0.3: + return min(0.85, base_confidence + 0.25) + elif part_name == "舰炮": + if rel_x < 0.3: + return min(0.8, base_confidence + 0.2) + elif part_name == "导弹发射器": + if 0.4 < rel_x < 0.8: + return min(0.75, base_confidence + 0.15) + elif part_name == "飞行甲板": + if "航母" in ship_type and area_ratio > 0.3: + return min(0.95, base_confidence + 0.4) + elif part_name == "舰岛": + if "航母" in ship_type and rel_x > 0.6 and rel_y < 0.3: + return min(0.9, base_confidence + 0.3) + elif part_name == "直升机甲板": + if rel_x > 0.7 and rel_y > 0.6: + return min(0.8, base_confidence + 0.2) + + # 根据舰船类型增加特定部件的置信度 + if "航母" in ship_type: + if part_name in ["舰岛", "飞行甲板", "舰载机", "弹射器"]: + return min(0.85, base_confidence + 0.25) + elif "驱逐舰" in ship_type: + if part_name in ["舰桥", "主炮", "垂发系统", "相控阵雷达"]: + return min(0.85, base_confidence + 0.25) + elif "护卫舰" in ship_type: + if part_name in ["舰桥", "舰炮", "导弹发射器"]: + return min(0.8, base_confidence + 0.2) + elif "潜艇" in ship_type: + if part_name in ["塔台", "鱼雷管"]: + return min(0.85, base_confidence + 0.25) + + return base_confidence + + def _calculate_iou(self, box1, box2): + """计算两个边界框的IoU""" + # 确保所有坐标为数字 + x1_1, y1_1, x2_1, y2_1 = box1 + x1_2, y1_2, x2_2, y2_2 = box2 + + # 计算交集面积 + x_left = max(x1_1, x1_2) + y_top = max(y1_1, y1_2) + x_right = min(x2_1, x2_2) + y_bottom = min(y2_1, y2_2) + + # 如果没有交集,返回0 + if x_right < x_left or y_bottom < y_top: + return 0.0 + + intersection_area = (x_right - x_left) * (y_bottom - y_top) + + # 计算两个框的面积 + box1_area = (x2_1 - x1_1) * (y2_1 - y1_1) + box2_area = (x2_2 - x1_2) * (y2_2 - y1_2) + + # 计算IoU + return intersection_area / float(box1_area + box2_area - intersection_area) + + def _add_specific_ship_parts(self, ship_img, ship_box, ship_type): + """ + 为特定类型的舰船添加可能的部件 + + Args: + ship_img: 舰船图像 + ship_box: 舰船边界框 + ship_type: 舰船类型 + + Returns: + additional_parts: 额外检测到的部件列表 + """ + additional_parts = [] + h, w = ship_img.shape[:2] + + # 获取目标舰船类型的特定部件 + target_parts = self._get_target_parts(ship_type) + + if "航空母舰" in ship_type: + # 检测舰岛 + island_x = int(w * 0.75) # 舰岛通常在右侧四分之三处 + island_y = int(h * 0.2) # 从顶部20%开始 + island_w = int(w * 0.2) # 宽度约为舰船宽度的20% + island_h = int(h * 0.3) # 高度约为舰船高度的30% + + # 验证区域 + island_roi = ship_img[island_y:island_y+island_h, island_x:island_x+island_w] + if island_roi.size > 0: + # 分析区域特征 + gray = cv2.cvtColor(island_roi, cv2.COLOR_BGR2GRAY) + edges = cv2.Canny(gray, 50, 150) + edge_pixels = cv2.countNonZero(edges) + edge_density = edge_pixels / (island_roi.shape[0] * island_roi.shape[1]) if island_roi.size > 0 else 0 + + # 如果区域有足够的边缘特征,识别为舰岛 + if edge_density > 0.1 and "舰岛" in target_parts: + additional_parts.append({ + "name": "舰岛", + "confidence": 0.85, + "box": [island_x, island_y, island_x + island_w, island_y + island_h], + "relative_box": [island_x/w, island_y/h, (island_x+island_w)/w, (island_y+island_h)/h] + }) + + # 检测飞行甲板 + deck_x = int(w * 0.05) # 从左侧5%开始 + deck_y = int(h * 0.1) # 从顶部10%开始 + deck_w = int(w * 0.9) # 宽度约为舰船宽度的90% + deck_h = int(h * 0.2) # 高度约为舰船高度的20% + + if "飞行甲板" in target_parts: + additional_parts.append({ + "name": "飞行甲板", + "confidence": 0.9, + "box": [deck_x, deck_y, deck_x + deck_w, deck_y + deck_h], + "relative_box": [deck_x/w, deck_y/h, (deck_x+deck_w)/w, (deck_y+deck_h)/h] + }) + + # 检测舰载机(如果有) + # 分析顶部区域是否有舰载机特征 + top_area = ship_img[0:int(h*0.3), :] + if top_area.size > 0: + gray = cv2.cvtColor(top_area, cv2.COLOR_BGR2GRAY) + blur = cv2.GaussianBlur(gray, (5, 5), 0) + _, thresh = cv2.threshold(blur, 120, 255, cv2.THRESH_BINARY_INV) + contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + + # 筛选可能的舰载机轮廓 + aircraft_contours = [] + for cnt in contours: + area = cv2.contourArea(cnt) + if area > 100 and area < 5000: # 舰载机大小范围 + x, y, w_box, h_box = cv2.boundingRect(cnt) + aspect_ratio = w_box / h_box if h_box > 0 else 0 + if 1.5 < aspect_ratio < 3.0: # 舰载机长宽比范围 + aircraft_contours.append(cnt) + + # 为每个可能的舰载机添加部件 + for i, cnt in enumerate(aircraft_contours[:5]): # 最多添加5个舰载机 + x, y, w_box, h_box = cv2.boundingRect(cnt) + if "舰载机" in target_parts: + additional_parts.append({ + "name": "舰载机", + "confidence": 0.75, + "box": [x, y, x + w_box, y + h_box], + "relative_box": [x/w, y/h, (x+w_box)/w, (y+h_box)/h] + }) + + elif "驱逐舰" in ship_type or "护卫舰" in ship_type: + # 检测舰炮(通常在舰首) + gun_x = int(w * 0.05) # 从左侧5%开始(假设舰首在左侧) + gun_y = int(h * 0.1) # 从顶部10%开始 + gun_w = int(w * 0.15) # 宽度约为舰船宽度的15% + gun_h = int(h * 0.15) # 高度约为舰船高度的15% + + # 验证区域 + gun_roi = ship_img[gun_y:gun_y+gun_h, gun_x:gun_x+gun_w] + if gun_roi.size > 0 and "舰炮" in target_parts: + # 可以使用更复杂的检测算法来验证 + additional_parts.append({ + "name": "舰炮", + "confidence": 0.8, + "box": [gun_x, gun_y, gun_x + gun_w, gun_y + gun_h], + "relative_box": [gun_x/w, gun_y/h, (gun_x+gun_w)/w, (gun_y+gun_h)/h] + }) + + # 检测舰桥(通常在中部偏后) + bridge_x = int(w * 0.4) # 从40%处开始 + bridge_y = int(h * 0.05) # 从顶部5%开始 + bridge_w = int(w * 0.2) # 宽度约为舰船宽度的20% + bridge_h = int(h * 0.3) # 高度约为舰船高度的30% + + # 验证区域 + bridge_roi = ship_img[bridge_y:bridge_y+bridge_h, bridge_x:bridge_x+bridge_w] + if bridge_roi.size > 0 and "舰桥" in target_parts: + # 分析区域特征 + gray = cv2.cvtColor(bridge_roi, cv2.COLOR_BGR2GRAY) + edges = cv2.Canny(gray, 50, 150) + edge_pixels = cv2.countNonZero(edges) + edge_density = edge_pixels / (bridge_roi.shape[0] * bridge_roi.shape[1]) if bridge_roi.size > 0 else 0 + + if edge_density > 0.1: + additional_parts.append({ + "name": "舰桥", + "confidence": 0.85, + "box": [bridge_x, bridge_y, bridge_x + bridge_w, bridge_y + bridge_h], + "relative_box": [bridge_x/w, bridge_y/h, (bridge_x+bridge_w)/w, (bridge_y+bridge_h)/h] + }) + + # 检测雷达(通常在舰桥顶部) + radar_x = int(w * 0.45) # 从45%处开始 + radar_y = int(h * 0.05) # 从顶部5%开始 + radar_w = int(w * 0.1) # 宽度约为舰船宽度的10% + radar_h = int(h * 0.1) # 高度约为舰船高度的10% + + if "雷达" in target_parts: + additional_parts.append({ + "name": "雷达", + "confidence": 0.7, + "box": [radar_x, radar_y, radar_x + radar_w, radar_y + radar_h], + "relative_box": [radar_x/w, radar_y/h, (radar_x+radar_w)/w, (radar_y+radar_h)/h] + }) + + # 检测垂直发射系统(通常在中前部) + vls_x = int(w * 0.25) # 从25%处开始 + vls_y = int(h * 0.1) # 从顶部10%开始 + vls_w = int(w * 0.15) # 宽度约为舰船宽度的15% + vls_h = int(h * 0.1) # 高度约为舰船高度的10% + + if "垂直发射系统" in target_parts: + additional_parts.append({ + "name": "垂直发射系统", + "confidence": 0.75, + "box": [vls_x, vls_y, vls_x + vls_w, vls_y + vls_h], + "relative_box": [vls_x/w, vls_y/h, (vls_x+vls_w)/w, (vls_y+vls_h)/h] + }) + + elif "潜艇" in ship_type: + # 检测舰塔(通常在潜艇中部上方) + tower_x = int(w * 0.4) # 从40%处开始 + tower_y = int(h * 0.0) # 从顶部开始 + tower_w = int(w * 0.2) # 宽度约为潜艇宽度的20% + tower_h = int(h * 0.5) # 高度约为潜艇高度的50% + + if "舰塔" in target_parts: + additional_parts.append({ + "name": "舰塔", + "confidence": 0.9, + "box": [tower_x, tower_y, tower_x + tower_w, tower_y + tower_h], + "relative_box": [tower_x/w, tower_y/h, (tower_x+tower_w)/w, (tower_y+tower_h)/h] + }) + + elif "巡洋舰" in ship_type: + # 检测舰桥(通常在中部) + bridge_x = int(w * 0.4) # 从40%处开始 + bridge_y = int(h * 0.05) # 从顶部5%开始 + bridge_w = int(w * 0.2) # 宽度约为舰船宽度的20% + bridge_h = int(h * 0.3) # 高度约为舰船高度的30% + + if "舰桥" in target_parts: + additional_parts.append({ + "name": "舰桥", + "confidence": 0.85, + "box": [bridge_x, bridge_y, bridge_x + bridge_w, bridge_y + bridge_h], + "relative_box": [bridge_x/w, bridge_y/h, (bridge_x+bridge_w)/w, (bridge_y+bridge_h)/h] + }) + + # 检测主炮(通常在前部) + gun_x = int(w * 0.1) # 从10%处开始 + gun_y = int(h * 0.1) # 从顶部10%开始 + gun_w = int(w * 0.15) # 宽度约为舰船宽度的15% + gun_h = int(h * 0.15) # 高度约为舰船高度的15% + + if "舰炮" in target_parts: + additional_parts.append({ + "name": "舰炮", + "confidence": 0.8, + "box": [gun_x, gun_y, gun_x + gun_w, gun_y + gun_h], + "relative_box": [gun_x/w, gun_y/h, (gun_x+gun_w)/w, (gun_y+gun_h)/h] + }) + + # 检测垂直发射系统(通常分布在前后部) + vls1_x = int(w * 0.2) # 前部VLS + vls1_y = int(h * 0.1) + vls1_w = int(w * 0.1) + vls1_h = int(h * 0.1) + + vls2_x = int(w * 0.6) # 后部VLS + vls2_y = int(h * 0.1) + vls2_w = int(w * 0.1) + vls2_h = int(h * 0.1) + + if "垂直发射系统" in target_parts: + additional_parts.append({ + "name": "垂直发射系统", + "confidence": 0.75, + "box": [vls1_x, vls1_y, vls1_x + vls1_w, vls1_y + vls1_h], + "relative_box": [vls1_x/w, vls1_y/h, (vls1_x+vls1_w)/w, (vls1_y+vls1_h)/h] + }) + + additional_parts.append({ + "name": "垂直发射系统", + "confidence": 0.75, + "box": [vls2_x, vls2_y, vls2_x + vls2_w, vls2_y + vls2_h], + "relative_box": [vls2_x/w, vls2_y/h, (vls2_x+vls2_w)/w, (vls2_y+vls2_h)/h] + }) + + return additional_parts + + def _get_target_parts(self, ship_type=""): + """ + 根据舰船类型获取目标部件列表 + + Args: + ship_type: 舰船类型 + + Returns: + target_parts: 该类型舰船应该检测的部件列表 + """ + # 默认检测所有部件 + if not ship_type or ship_type not in self.ship_parts_map: + return list(self.part_types.keys()) + + # 返回特定舰船类型的部件列表 + return self.ship_parts_map.get(ship_type, []) + + def _map_to_ship_part(self, cls_id, ship_type=""): + """ + 将YOLO类别ID映射到舰船部件名称 + + Args: + cls_id: YOLO类别ID + ship_type: 舰船类型名称 + + Returns: + part_name: 舰船部件名称 + """ + # COCO数据集中的常见类别 + coco_classes = { + 0: "person", # 人 -> 可能是船员 + 1: "bicycle", # 自行车 + 2: "car", # 汽车 + 3: "motorcycle", # 摩托车 + 4: "airplane", # 飞机 -> 可能是舰载机 + 5: "bus", # 公交车 + 6: "train", # 火车 + 7: "truck", # 卡车 + 8: "boat", # 船 + 9: "traffic light", # 交通灯 + 10: "fire hydrant", # 消防栓 + 11: "stop sign", # 停止标志 + 12: "parking meter", # 停车计时器 + 13: "bench", # 长凳 + 14: "bird", # 鸟 + 15: "cat", # 猫 + 16: "dog", # 狗 + 17: "horse", # 马 + 24: "backpack", # 背包 + 25: "umbrella", # 雨伞 + 26: "handbag", # 手提包 + 27: "tie", # 领带 + 28: "suitcase", # 手提箱 + 33: "sports ball", # 运动球 + 34: "kite", # 风筝 + 35: "baseball bat", # 棒球棒 + 36: "baseball glove", # 棒球手套 + 41: "skateboard", # 滑板 + 42: "surfboard", # 冲浪板 + 43: "tennis racket", # 网球拍 + 59: "potted plant", # 盆栽植物 + 60: "bed", # 床 + 61: "dining table", # 餐桌 + 62: "toilet", # 厕所 + 63: "tv", # 电视 + 64: "laptop", # 笔记本电脑 + 65: "mouse", # 鼠标 + 66: "remote", # 遥控器 + 67: "keyboard", # 键盘 + 68: "cell phone", # 手机 + 69: "microwave", # 微波炉 + 70: "oven", # 烤箱 + 71: "toaster", # 烤面包机 + 72: "sink", # 水槽 + 73: "refrigerator", # 冰箱 + 74: "book", # 书 + 75: "clock", # 时钟 + 76: "vase", # 花瓶 + 77: "scissors", # 剪刀 + 78: "teddy bear", # 泰迪熊 + 79: "hair drier", # 吹风机 + 80: "toothbrush" # 牙刷 + } + + # 将COCO类别映射到舰船部件 + part_mappings = { + "person": "船员", + "airplane": "舰载机", + "boat": "小艇", + "backpack": "装备", + "tie": "指挥官", + "suitcase": "设备箱", + "sports ball": "雷达球", + "kite": "雷达天线", + "surfboard": "救生设备", + "bed": "甲板", + "dining table": "甲板", + "toilet": "舱室", + "tv": "显示设备", + "laptop": "控制设备", + "mouse": "控制设备", + "remote": "控制设备", + "keyboard": "控制设备", + "cell phone": "通信设备", + "clock": "导航设备", + "scissors": "工具" + } + + # 获取COCO类别名称 + if cls_id in coco_classes: + coco_class = coco_classes[cls_id] + + # 将COCO类别映射到舰船部件 + if coco_class in part_mappings: + return part_mappings[coco_class] + + # 如果是航母且检测到飞机,返回舰载机 + if ("航母" in ship_type or "航空母舰" in ship_type) and cls_id == 4: + return "舰载机" + + # 默认返回未知 + return "unknown" + +# 测试代码 +if __name__ == "__main__": + # 初始化部件检测器 + part_detector = ShipPartDetector() + + # 测试图像路径和舰船边界框 + test_image = "path/to/test/image.jpg" + test_ship_box = (100, 100, 500, 400) # 示例边界框 + + if os.path.exists(test_image): + # 执行部件检测 + parts, result_img = part_detector.detect_parts(test_image, test_ship_box) + + # 打印检测结果 + print(f"检测到 {len(parts)} 个舰船部件:") + for i, part in enumerate(parts): + print(f"部件 {i+1}: 类型={part['name']}, 置信度={part['confidence']:.4f}, 位置={part['bbox']}") \ No newline at end of file diff --git a/distance-judgement/src/drone/utils/ship_analyzer.py b/distance-judgement/src/drone/utils/ship_analyzer.py new file mode 100644 index 00000000..cbf681f3 --- /dev/null +++ b/distance-judgement/src/drone/utils/ship_analyzer.py @@ -0,0 +1,508 @@ +import os +import sys +import cv2 +import torch +import numpy as np +import argparse +from pathlib import Path +from PIL import Image, ImageDraw, ImageFont +from datetime import datetime + +# 添加父目录到路径,以便导入utils模块 +script_dir = os.path.dirname(os.path.abspath(__file__)) +parent_dir = os.path.dirname(script_dir) +sys.path.append(parent_dir) + +# 添加项目根目录到路径 +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: + sys.path.append(str(ROOT)) + +# 导入检测器和分类器 +from utils.detector_fixed import ShipDetector +from utils.part_detector import ShipPartDetector +from utils.classifier import ShipClassifier + +class ShipAnalyzer: + """ + 舰船分析系统,整合检测、分类和部件识别功能 + """ + def __init__(self, detector_model_path=None, part_detector_model_path=None, classifier_model_path=None, device=None): + """ + 初始化舰船分析系统 + + Args: + detector_model_path: 检测器模型路径 + part_detector_model_path: 部件检测器模型路径 + classifier_model_path: 分类器模型路径 + device: 运行设备 + """ + print("=== 初始化舰船分析系统 ===") + + self.device = device if device else ('cuda' if torch.cuda.is_available() else 'cpu') + print(f"使用设备: {self.device}") + + # 初始化舰船检测器 + try: + self.detector = ShipDetector(model_path=detector_model_path, device=self.device) + except Exception as e: + print(f"初始化舰船检测器出错: {e}") + self.detector = None + + # 初始化部件检测器 + try: + self.part_detector = ShipPartDetector(model_path=part_detector_model_path, device=self.device) + except Exception as e: + print(f"初始化部件检测器出错: {e}") + self.part_detector = None + + # 初始化舰船分类器 + try: + self.classifier = ShipClassifier(model_path=classifier_model_path, device=self.device) + except Exception as e: + print(f"初始化舰船分类器出错: {e}") + self.classifier = None + + # 航母特殊检测标志 + self.special_carrier_detection = True # 启用航母特殊检测 + + print("✅ ShipAnalyzer初始化成功") + + def detect_ships(self, image_path, conf_threshold=0.25): + """ + 检测图像中的舰船 + + Args: + image_path: 图像路径或图像对象 + conf_threshold: 置信度阈值 + + Returns: + ship_detections: 检测到的舰船列表 + result_img: 标注了检测框的图像 + """ + # 输出调试信息 + print(f"正在检测舰船,置信度阈值: {conf_threshold}") + + # 使用较低的置信度阈值进行检测以提高召回率 + actual_conf_threshold = 0.05 # 使用固定的低置信度阈值 + + try: + # 检测舰船 - 使用detector_fixed模块 + ship_detections, result_img = self.detector.detect(image_path, conf_threshold=actual_conf_threshold) + + print(f"检测完成,发现 {len(ship_detections)} 个舰船") + return ship_detections, result_img + except Exception as e: + print(f"舰船检测过程中出错: {e}") + import traceback + traceback.print_exc() + + # 读取图像用于创建空结果 + if isinstance(image_path, str): + img = cv2.imread(image_path) + if img is None: + return [], np.zeros((100, 100, 3), dtype=np.uint8) + return [], img.copy() + else: + return [], image_path.copy() if isinstance(image_path, np.ndarray) else np.zeros((100, 100, 3), dtype=np.uint8) + + def analyze_image(self, image, conf_threshold=0.25, save_result=True, output_path=None): + """ + 分析图像并返回结果 + + Args: + image: 图像路径或图像数组 + conf_threshold: 置信度阈值 + save_result: 是否保存结果图像 + output_path: 结果图像保存路径 + + Returns: + 分析结果字典, 标注后的图像 + """ + if self.detector is None: + print("错误: 检测器未初始化") + return {"error": "检测器未初始化"}, None + + try: + print(f"正在分析图像: {image if isinstance(image, str) else '图像数组'}") + + # 使用更低的置信度阈值来检测图像 + actual_conf_threshold = 0.05 # 使用较低的阈值,确保能检出舰船 + print(f"开始舰船检测,实际使用置信度阈值: {actual_conf_threshold}") + + # 检测图像中的舰船 + ships_detected, result_img = self.detector.detect(image, conf_threshold=actual_conf_threshold) + print(f"检测到 {len(ships_detected)} 个舰船目标") + + # 初始化结果 + result = { + 'ships': [], + 'detected_ids': [], # 添加检测到的舰船ID列表 + 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + 'image': image if isinstance(image, str) else "image_array" + } + + # 如果没有检测到舰船,标记为未检测到但返回图像 + if not ships_detected: + print("未检测到舰船,返回空结果") + # 保存结果图像 + if save_result and output_path: + try: + cv2.imwrite(output_path, result_img) + print(f"分析结果已保存至: {output_path}") + except Exception as e: + print(f"保存结果图像失败: {e}") + + return {"ships": [], "message": "未检测到舰船"}, result_img + + # 检测到舰船,更新结果 + for ship in ships_detected: + # 确保每个舰船都有parts字段,防止模板引用出错 + if 'parts' not in ship: + ship['parts'] = [] + + # 记录检测到的舰船ID + if 'class_id' in ship: + result['detected_ids'].append(ship['class_id']) + + # 添加到结果中 + result['ships'].append(ship) + + # 输出信息 + print(f"添加舰船结果: 类别ID={ship.get('class_id', '未知')}, 类别名称={ship.get('class_name', '未知')}") + + # 步骤2: 检测舰船部件 + if self.part_detector: + print("步骤2: 检测舰船部件") + all_parts = [] + + for i, ship in enumerate(result['ships']): + try: + ship_box = ship['bbox'] + ship_type = ship['class_name'] + ship_id = i + 1 # 舰船ID从1开始 + + print(f"分析舰船 #{ship_id} - 类型: {ship_type}") + + # 检测部件 + try: + parts, parts_img = self.part_detector.detect(image, ship_box, conf_threshold=0.3, ship_type=ship_type) + result_img = parts_img.copy() + + # 为每个部件添加所属舰船的ID + for part in parts: + try: + # 确保部件边界框是数值型 + if 'bbox' in part: + bbox = part['bbox'] + if isinstance(bbox, list) and len(bbox) == 4: + part['bbox'] = [float(coord) if isinstance(coord, (int, float, str)) else 0.0 for coord in bbox] + + part['ship_id'] = ship_id + except Exception as e: + print(f"处理部件数据出错: {e}") + continue + + # 将部件添加到对应的舰船中 + ship['parts'] = parts + + all_parts.extend(parts) + print(f"舰船 #{ship_id} 检测到 {len(parts)} 个部件") + except Exception as e: + print(f"部件检测过程中出错: {e}") + import traceback + traceback.print_exc() + continue + except Exception as e: + print(f"分析舰船 #{i+1} 时出错: {e}") + import traceback + traceback.print_exc() + continue + + # 更新结果添加部件信息 + result['parts'] = all_parts + + # 打印分析结果摘要 + print(f"分析完成: 检测到 {len(result['ships'])} 艘舰船,共 {len(result.get('parts', [])) if 'parts' in result else 0} 个部件") + + # 保存结果图像 + if save_result: + try: + if output_path is None and isinstance(image, str): + output_dir = os.path.dirname(image) + output_path = os.path.join(output_dir, f"analysis_{os.path.basename(image)}") + + if output_path: + cv2.imwrite(output_path, result_img) + print(f"分析结果已保存至: {output_path}") + + # 保存结果JSON + base_name = os.path.splitext(output_path)[0] + json_path = f"{base_name.split('analysis_')[0]}{os.path.basename(image).split('.')[0]}_result.json" + + import json + with open(json_path, 'w', encoding='utf-8') as f: + # 转换numpy和其他不可序列化类型 + def json_serializable(obj): + if isinstance(obj, (np.ndarray, np.number)): + return obj.tolist() + if isinstance(obj, (datetime,)): + return obj.isoformat() + return str(obj) + + json.dump(result, f, ensure_ascii=False, indent=2, default=json_serializable) + print(f"结果图像已保存至: {output_path}") + except Exception as e: + print(f"保存结果图像失败: {e}") + + return result, result_img + except Exception as e: + print(f"分析图像时出错: {e}") + import traceback + traceback.print_exc() + return {"error": "分析图像时出错", "ships": []}, None + + def _enhance_generic_parts(self, img, ship_box, existing_parts): + """通用舰船部件增强 + + Args: + img: 完整图像 + ship_box: 舰船边界框 (x1,y1,x2,y2) + existing_parts: 现有检测到的部件 + + Returns: + enhanced_parts: 增强后的部件列表 + """ + # 如果部件数量足够,不做处理 + if len(existing_parts) >= 3: + return existing_parts + + x1, y1, x2, y2 = ship_box + # 确保是整数 + x1, y1, x2, y2 = int(float(x1)), int(float(y1)), int(float(x2)), int(float(y2)) + ship_w, ship_h = x2-x1, y2-y1 + + # 复制现有部件 + enhanced_parts = existing_parts.copy() + + # 标记已有部件区域,避免重叠 + existing_areas = [] + for part in enhanced_parts: + px1, py1, px2, py2 = part['bbox'] + existing_areas.append((px1, py1, px2, py2)) + + # 检查是否有舰桥 + if not any(p['name'] == '舰桥' for p in enhanced_parts): + bridge_w = int(ship_w * 0.2) + bridge_h = int(ship_h * 0.3) + bridge_x = x1 + int(ship_w * 0.4) + bridge_y = y1 + int(ship_h * 0.1) + + # 避免重叠 + overlap = False + for ex1, ey1, ex2, ey2 in existing_areas: + if not (bridge_x + bridge_w < ex1 or bridge_x > ex2 or bridge_y + bridge_h < ey1 or bridge_y > ey2): + overlap = True + break + + if not overlap: + enhanced_parts.append({ + 'name': '舰桥', + 'bbox': (bridge_x, bridge_y, bridge_x + bridge_w, bridge_y + bridge_h), + 'confidence': 0.7, + 'class_id': 0 + }) + existing_areas.append((bridge_x, bridge_y, bridge_x + bridge_w, bridge_y + bridge_h)) + + return enhanced_parts + + def detect_parts(self, image, ship_box, conf_threshold=0.3, ship_type=""): + """ + 检测舰船的组成部件 + + Args: + image: 图像路径或图像对象 + ship_box: 舰船边界框 (x1,y1,x2,y2) + conf_threshold: 置信度阈值 + ship_type: 舰船类型,用于定向部件检测 + + Returns: + parts: 检测到的部件列表 + result_img: 标注了部件的图像 + """ + try: + # 读取图像 + if isinstance(image, str): + img = cv2.imread(image) + else: + img = image.copy() if isinstance(image, np.ndarray) else np.array(image) + + if img is None: + return [], np.zeros((100, 100, 3), dtype=np.uint8) + + # 确保边界框是列表且包含4个元素 + if not isinstance(ship_box, (list, tuple)) or len(ship_box) != 4: + print(f"无效的边界框格式: {ship_box}") + return [], img.copy() + + # 确保边界框值是数值类型 + x1, y1, x2, y2 = [float(val) if isinstance(val, (int, float, str)) else 0.0 for val in ship_box] + + # 提取舰船区域 + x1, y1, x2, y2 = int(float(x1)), int(float(y1)), int(float(x2)), int(float(y2)) + + # 确保边界在图像范围内 + h, w = img.shape[:2] + x1, y1 = max(0, x1), max(0, y1) + x2, y2 = min(w, x2), min(h, y2) + + # 提取部件 + try: + parts, parts_img = self.part_detector.detect(img, [x1, y1, x2, y2], conf_threshold=conf_threshold, ship_type=ship_type) + except Exception as e: + print(f"部件检测器调用出错: {e}") + import traceback + traceback.print_exc() + return [], img.copy() + + # 增强部件 + try: + enhanced_parts = self._enhance_generic_parts(img, [x1, y1, x2, y2], parts) + except Exception as e: + print(f"增强部件失败: {e}") + enhanced_parts = parts + + return enhanced_parts, parts_img + + except Exception as e: + print(f"部件检测过程中出错: {e}") + import traceback + traceback.print_exc() + if isinstance(image, str): + img = cv2.imread(image) + if img is None: + return [], np.zeros((100, 100, 3), dtype=np.uint8) + return [], img.copy() + else: + return [], image.copy() if isinstance(image, np.ndarray) else np.zeros((100, 100, 3), dtype=np.uint8) + + def _detect_ship_parts(self, img, ship_data, conf_threshold=0.25): + """ + 检测舰船部件 + + Args: + img: 原始图像 + ship_data: 舰船数据,包含边界框和类别 + conf_threshold: 置信度阈值 + + Returns: + parts: 检测到的部件列表 + img_with_parts: 标注了部件的图像 + """ + result_img = img.copy() + all_parts = [] + + # 对每个检测到的舰船进行部件分析 + for i, ship in enumerate(ship_data): + try: + ship_id = i + 1 + ship_class = ship['class_id'] + ship_name = ship['name'] + ship_box = ship['bbox'] + + # 提取舰船区域 + x1, y1, x2, y2 = [int(coord) for coord in ship_box] + ship_img = img[y1:y2, x1:x2] + + if ship_img.size == 0 or ship_img.shape[0] <= 0 or ship_img.shape[1] <= 0: + continue + + print(f"分析舰船 #{ship_id} - 类型: {ship_name}") + + # 使用部件检测器 + if self.part_detector is not None: + # 确保预处理图像适合部件检测 + parts, part_img = self.part_detector.detect( + img, + ship_box, + conf_threshold, + ship_type=ship_name + ) + + # 如果检测到部件,记录并标注 + if parts and len(parts) > 0: + print(f"舰船 #{ship_id} 检测到 {len(parts)} 个部件") + + # 添加部件到结果 + for part in parts: + part['ship_id'] = ship_id + all_parts.append(part) + + # 在结果图像上标注部件(如果有) + try: + # 获取部件边界框 + px1, py1, px2, py2 = [int(coord) for coord in part['bbox']] + + # 标注部件 + cv2.rectangle(result_img, (px1, py1), (px2, py2), (0, 255, 255), 2) + + # 添加部件标签 + part_name = part['name'] + conf = part['confidence'] + label = f"{part_name}: {conf:.2f}" + cv2.putText(result_img, label, (px1, py1-5), + cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 255), 2) + except Exception as e: + print(f"标注部件时出错: {e}") + else: + print(f"舰船 #{ship_id} 未检测到部件") + else: + print(f"警告: 未初始化部件检测器,无法分析舰船部件") + except Exception as e: + print(f"分析舰船 #{i+1} 部件时出错: {e}") + + print(f"检测到 {len(all_parts)} 个舰船部件") + return all_parts, result_img + + +def main(): + parser = argparse.ArgumentParser(description='舰船分析系统') + parser.add_argument('--input', '-i', required=True, help='输入图像或视频路径') + parser.add_argument('--detector', '-d', default=None, help='舰船检测模型路径') + parser.add_argument('--parts', '-p', default=None, help='部件检测模型路径') + parser.add_argument('--classifier', '-c', default=None, help='分类模型路径') + parser.add_argument('--conf', type=float, default=0.25, help='置信度阈值') + parser.add_argument('--output', '-o', default=None, help='输出结果路径') + parser.add_argument('--device', default=None, help='运行设备 (cuda/cpu)') + + args = parser.parse_args() + + # 检查输入文件是否存在 + if not os.path.exists(args.input): + print(f"错误: 输入文件不存在: {args.input}") + return + + # 初始化分析器 + analyzer = ShipAnalyzer( + detector_model_path=args.detector, + part_detector_model_path=args.parts, + classifier_model_path=args.classifier, + device=args.device + ) + + # 根据输入文件类型选择分析方法 + is_video = args.input.lower().endswith(('.mp4', '.avi', '.mov', '.wmv')) + + if is_video: + analyzer.analyze_video(args.input, args.output, args.conf) + else: + analyzer.analyze_image( + args.input, + conf_threshold=args.conf, + save_result=True, + output_path=args.output + ) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/distance-judgement/src/drone/web/templates/base.html b/distance-judgement/src/drone/web/templates/base.html new file mode 100644 index 00000000..88446e95 --- /dev/null +++ b/distance-judgement/src/drone/web/templates/base.html @@ -0,0 +1,138 @@ + + + + + + {% block title %}舰船识别系统{% endblock %} - ShipAI + + + + + + + {% block extra_css %}{% endblock %} + + + + + + +
    + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
    + {{ message }} + +
    + {% endfor %} + {% endif %} + {% endwith %} +
    + + +
    + {% block content %}{% endblock %} +
    + + +
    +
    +
    +
    +
    ShipAI - 智能舰船识别系统
    +

    基于深度学习的海上舰船自动识别与分析平台

    +
    +
    +

    © {{ current_year }} ShipAI 团队

    +
    +
    +
    +
    + + + + + + + {% block scripts %}{% endblock %} + {% block extra_js %}{% endblock %} + + \ No newline at end of file diff --git a/distance-judgement/src/drone/web/templates/drone_control.html b/distance-judgement/src/drone/web/templates/drone_control.html new file mode 100644 index 00000000..ca2bb706 --- /dev/null +++ b/distance-judgement/src/drone/web/templates/drone_control.html @@ -0,0 +1,1480 @@ +{% extends "base.html" %} + +{% block title %}无人机控制{% endblock %} + +{% block content %} +
    +
    +
    +
    +
    +
    Tello TLW004 无人机控制面板
    +
    +
    +
    + 提示: 请确保已经连接到Tello无人机WiFi (TELLO-9C29DE) +
    +
    +
    +
    +
    连接控制
    +
    +
    + + +
    + 电量: -- +
    +
    +
    + 未连接到无人机 +
    +
    +
    + +
    +
    飞行控制
    +
    +
    + + +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +

    + +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    + +
    + + +
    30 厘米
    +
    + +
    + + +
    45
    +
    +
    +
    +
    + +
    +
    +
    视频流
    +
    +
    +
    +
    + + +
    不同的流类型可能有不同的兼容性和性能
    +
    +
    +
    + + + + +
    +
    +
    +
    +
    +
    + + 无视频流 + + + + +
    +
    + 加载中... +
    +
    +
    +
    +
    +
    未启动视频流
    + +
    + + + +
    +
    +
    +
    + +
    +
    分析结果
    +
    +
    +
    尚未进行分析
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    捕获的图像
    +
    +
    +
    + +
    +

    尚未捕获图像

    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    视频流测试
    +
    +
    +
    +
    +
    +
    常见问题排查:
    +
      +
    1. 确保已连接到Tello的WiFi网络(TELLO-XXXXXX)
    2. +
    3. 确认未同时使用Tello手机APP和本系统
    4. +
    5. 尝试重启无人机(关闭电源后重新开启)
    6. +
    7. 检查电池电量是否足够
    8. +
    9. 尝试不同的视频流类型(UDP/RTSP/HTTP)
    10. +
    +
    +
    + +
    + + + +
    +
    + UDP格式: 0.0.0.0:11111 +
    +
    +
    + + + + + + +
    +
    +
    +
    +
    测试日志将显示在此处...
    +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    视频流调试工具
    + +
    + +
    +
    +
    +
    + + +{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/distance-judgement/src/map_manager.py b/distance-judgement/src/map_manager.py index f6570be5..db6cfc18 100644 --- a/distance-judgement/src/map_manager.py +++ b/distance-judgement/src/map_manager.py @@ -1,242 +1,242 @@ -import requests -import json -import math -import webbrowser -import os -from typing import List, Tuple, Dict -import time - -class MapManager: - """高德地图管理器 - 处理地图显示和坐标标记""" - - def __init__(self, api_key: str = None, camera_lat: float = None, camera_lng: float = None): - self.api_key = api_key or "your_gaode_api_key_here" # 需要替换为真实的API key - self.camera_lat = camera_lat or 39.9042 # 默认北京天安门坐标 - self.camera_lng = camera_lng or 116.4074 - self.camera_heading = 0 # 摄像头朝向角度(正北为0度) - self.camera_fov = 60 # 摄像头视场角度 - self.persons_positions = [] # 人员位置列表 - self.map_html_path = "person_tracking_map.html" - - def set_camera_position(self, lat: float, lng: float, heading: float = 0): - """设置摄像头位置和朝向""" - self.camera_lat = lat - self.camera_lng = lng - self.camera_heading = heading - print(f"📍 摄像头位置已设置: ({lat:.6f}, {lng:.6f}), 朝向: {heading}°") - - def calculate_person_position(self, pixel_x: float, pixel_y: float, distance: float, - frame_width: int, frame_height: int) -> Tuple[float, float]: - """根据人在画面中的像素位置和距离,计算真实地理坐标""" - # 将像素坐标转换为相对角度 - horizontal_angle_per_pixel = self.camera_fov / frame_width - - # 计算人相对于摄像头中心的角度偏移 - center_x = frame_width / 2 - horizontal_offset_degrees = (pixel_x - center_x) * horizontal_angle_per_pixel - - # 计算人相对于摄像头的实际角度 - person_bearing = (self.camera_heading + horizontal_offset_degrees) % 360 - - # 将距离和角度转换为地理坐标偏移 - person_lat, person_lng = self._calculate_destination_point( - self.camera_lat, self.camera_lng, distance, person_bearing - ) - - return person_lat, person_lng - - def _calculate_destination_point(self, lat: float, lng: float, distance: float, bearing: float) -> Tuple[float, float]: - """根据起点坐标、距离和方位角计算目标点坐标,使用球面几何学计算""" - # 地球半径(米) - R = 6371000 - - # 转换为弧度 - lat1 = math.radians(lat) - lng1 = math.radians(lng) - bearing_rad = math.radians(bearing) - - # 计算目标点坐标 - lat2 = math.asin( - math.sin(lat1) * math.cos(distance / R) + - math.cos(lat1) * math.sin(distance / R) * math.cos(bearing_rad) - ) - - lng2 = lng1 + math.atan2( - math.sin(bearing_rad) * math.sin(distance / R) * math.cos(lat1), - math.cos(distance / R) - math.sin(lat1) * math.sin(lat2) - ) - - return math.degrees(lat2), math.degrees(lng2) - - def add_person_position(self, pixel_x: float, pixel_y: float, distance: float, - frame_width: int, frame_height: int, person_id: str = None): - """添加人员位置""" - lat, lng = self.calculate_person_position(pixel_x, pixel_y, distance, frame_width, frame_height) - - person_info = { - 'id': person_id or f"person_{len(self.persons_positions) + 1}", - 'lat': lat, - 'lng': lng, - 'distance': distance, - 'timestamp': time.time(), - 'pixel_x': pixel_x, - 'pixel_y': pixel_y - } - - self.persons_positions.append(person_info) - - # 只保留最近10秒的数据 - current_time = time.time() - self.persons_positions = [ - p for p in self.persons_positions - if current_time - p['timestamp'] < 10 - ] - - return lat, lng - - def clear_persons(self): - """清空人员位置""" - self.persons_positions = [] - - def add_person_at_coordinates(self, lat: float, lng: float, person_id: str, - distance: float = 0, source: str = "manual"): - """直接在指定GPS坐标添加人员标记""" - person_data = { - 'id': person_id, - 'lat': lat, - 'lng': lng, - 'distance': distance, - 'timestamp': time.time(), - 'source': source # 标记数据来源(如设备ID) - } - - # 添加到人员数据列表 - self.persons_positions.append(person_data) - - # 只保留最近10秒的数据 - current_time = time.time() - self.persons_positions = [ - p for p in self.persons_positions - if current_time - p['timestamp'] < 10 - ] - - return lat, lng - - def get_persons_data(self) -> List[Dict]: - """获取当前人员数据""" - return self.persons_positions - - def generate_map_html(self) -> str: - """生成高德地图HTML页面""" - persons_data_json = json.dumps(self.persons_positions) - - html_content = f""" - - - - 实时人员位置追踪系统 🚁 - - - - - -
    -
    -

    🚁 无人机战场态势感知

    -
    ● 摄像头在线
    -
    📍 坐标: {self.camera_lat:.6f}, {self.camera_lng:.6f}
    -
    🧭 朝向: {self.camera_heading}°
    -
    👥 检测到: {len(self.persons_positions)} 人
    -
    - 🔴 红点 = 人员位置
    - 📷 蓝点 = 摄像头位置
    - ⚡ 实时更新 -
    -
    - - - -""" - - # 保存HTML文件 - with open(self.map_html_path, 'w', encoding='utf-8') as f: - f.write(html_content) - - return self.map_html_path - - def open_map(self): - """在浏览器中打开地图""" - html_path = self.generate_map_html() - file_url = f"file://{os.path.abspath(html_path)}" - webbrowser.open(file_url) - print(f"🗺️ 地图已在浏览器中打开: {html_path}") - - def update_camera_heading(self, new_heading: float): - """更新摄像头朝向""" - self.camera_heading = new_heading +import requests +import json +import math +import webbrowser +import os +from typing import List, Tuple, Dict +import time + +class MapManager: + """高德地图管理器 - 处理地图显示和坐标标记""" + + def __init__(self, api_key: str = None, camera_lat: float = None, camera_lng: float = None): + self.api_key = api_key or "your_gaode_api_key_here" # 需要替换为真实的API key + self.camera_lat = camera_lat or 39.9042 # 默认北京天安门坐标 + self.camera_lng = camera_lng or 116.4074 + self.camera_heading = 0 # 摄像头朝向角度(正北为0度) + self.camera_fov = 60 # 摄像头视场角度 + self.persons_positions = [] # 人员位置列表 + self.map_html_path = "person_tracking_map.html" + + def set_camera_position(self, lat: float, lng: float, heading: float = 0): + """设置摄像头位置和朝向""" + self.camera_lat = lat + self.camera_lng = lng + self.camera_heading = heading + print(f"📍 摄像头位置已设置: ({lat:.6f}, {lng:.6f}), 朝向: {heading}°") + + def calculate_person_position(self, pixel_x: float, pixel_y: float, distance: float, + frame_width: int, frame_height: int) -> Tuple[float, float]: + """根据人在画面中的像素位置和距离,计算真实地理坐标""" + # 将像素坐标转换为相对角度 + horizontal_angle_per_pixel = self.camera_fov / frame_width + + # 计算人相对于摄像头中心的角度偏移 + center_x = frame_width / 2 + horizontal_offset_degrees = (pixel_x - center_x) * horizontal_angle_per_pixel + + # 计算人相对于摄像头的实际角度 + person_bearing = (self.camera_heading + horizontal_offset_degrees) % 360 + + # 将距离和角度转换为地理坐标偏移 + person_lat, person_lng = self._calculate_destination_point( + self.camera_lat, self.camera_lng, distance, person_bearing + ) + + return person_lat, person_lng + + def _calculate_destination_point(self, lat: float, lng: float, distance: float, bearing: float) -> Tuple[float, float]: + """根据起点坐标、距离和方位角计算目标点坐标,使用球面几何学计算""" + # 地球半径(米) + R = 6371000 + + # 转换为弧度 + lat1 = math.radians(lat) + lng1 = math.radians(lng) + bearing_rad = math.radians(bearing) + + # 计算目标点坐标 + lat2 = math.asin( + math.sin(lat1) * math.cos(distance / R) + + math.cos(lat1) * math.sin(distance / R) * math.cos(bearing_rad) + ) + + lng2 = lng1 + math.atan2( + math.sin(bearing_rad) * math.sin(distance / R) * math.cos(lat1), + math.cos(distance / R) - math.sin(lat1) * math.sin(lat2) + ) + + return math.degrees(lat2), math.degrees(lng2) + + def add_person_position(self, pixel_x: float, pixel_y: float, distance: float, + frame_width: int, frame_height: int, person_id: str = None): + """添加人员位置""" + lat, lng = self.calculate_person_position(pixel_x, pixel_y, distance, frame_width, frame_height) + + person_info = { + 'id': person_id or f"person_{len(self.persons_positions) + 1}", + 'lat': lat, + 'lng': lng, + 'distance': distance, + 'timestamp': time.time(), + 'pixel_x': pixel_x, + 'pixel_y': pixel_y + } + + self.persons_positions.append(person_info) + + # 只保留最近10秒的数据 + current_time = time.time() + self.persons_positions = [ + p for p in self.persons_positions + if current_time - p['timestamp'] < 10 + ] + + return lat, lng + + def clear_persons(self): + """清空人员位置""" + self.persons_positions = [] + + def add_person_at_coordinates(self, lat: float, lng: float, person_id: str, + distance: float = 0, source: str = "manual"): + """直接在指定GPS坐标添加人员标记""" + person_data = { + 'id': person_id, + 'lat': lat, + 'lng': lng, + 'distance': distance, + 'timestamp': time.time(), + 'source': source # 标记数据来源(如设备ID) + } + + # 添加到人员数据列表 + self.persons_positions.append(person_data) + + # 只保留最近10秒的数据 + current_time = time.time() + self.persons_positions = [ + p for p in self.persons_positions + if current_time - p['timestamp'] < 10 + ] + + return lat, lng + + def get_persons_data(self) -> List[Dict]: + """获取当前人员数据""" + return self.persons_positions + + def generate_map_html(self) -> str: + """生成高德地图HTML页面""" + persons_data_json = json.dumps(self.persons_positions) + + html_content = f""" + + + + 实时人员位置追踪系统 🚁 + + + + + +
    +
    +

    🚁 无人机战场态势感知

    +
    ● 摄像头在线
    +
    📍 坐标: {self.camera_lat:.6f}, {self.camera_lng:.6f}
    +
    🧭 朝向: {self.camera_heading}°
    +
    👥 检测到: {len(self.persons_positions)} 人
    +
    + 🔴 红点 = 人员位置
    + 📷 蓝点 = 摄像头位置
    + ⚡ 实时更新 +
    +
    + + + +""" + + # 保存HTML文件 + with open(self.map_html_path, 'w', encoding='utf-8') as f: + f.write(html_content) + + return self.map_html_path + + def open_map(self): + """在浏览器中打开地图""" + html_path = self.generate_map_html() + file_url = f"file://{os.path.abspath(html_path)}" + webbrowser.open(file_url) + print(f"🗺️ 地图已在浏览器中打开: {html_path}") + + def update_camera_heading(self, new_heading: float): + """更新摄像头朝向""" + self.camera_heading = new_heading print(f"🧭 摄像头朝向已更新: {new_heading}°") \ No newline at end of file diff --git a/distance-judgement/src/mobile_connector.py b/distance-judgement/src/mobile_connector.py index cd4d6354..374b626d 100644 --- a/distance-judgement/src/mobile_connector.py +++ b/distance-judgement/src/mobile_connector.py @@ -1,303 +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: 实现向手机发送控制命令的功能 +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +手机连接器模块 +用于接收手机传送的摄像头图像、GPS位置和设备信息 +""" + +import cv2 +import numpy as np +import json +import time +import threading +from datetime import datetime +import base64 +import socket +import struct +from typing import Dict, List, Optional, Tuple, Callable +from . import config + +class MobileDevice: + """移动设备信息类""" + def __init__(self, device_id: str, device_name: str): + self.device_id = device_id + self.device_name = device_name + self.last_seen = time.time() + self.is_online = True + self.current_location = None # (lat, lng, accuracy) + self.battery_level = 100 + self.signal_strength = 100 + self.camera_info = {} + self.connection_info = {} + + def update_status(self, data: dict): + """更新设备状态""" + self.last_seen = time.time() + self.is_online = True + + if 'gps' in data: + self.current_location = ( + data['gps'].get('latitude'), + data['gps'].get('longitude'), + data['gps'].get('accuracy', 0) + ) + + if 'battery' in data: + self.battery_level = data['battery'] + + if 'signal' in data: + self.signal_strength = data['signal'] + + if 'camera_info' in data: + self.camera_info = data['camera_info'] + + def is_location_valid(self) -> bool: + """检查GPS位置是否有效""" + if not self.current_location: + return False + lat, lng, _ = self.current_location + return lat is not None and lng is not None and -90 <= lat <= 90 and -180 <= lng <= 180 + +class MobileConnector: + """手机连接器主类""" + + def __init__(self, port: int = 8080): + self.port = port + self.server_socket = None + self.is_running = False + self.devices = {} # device_id -> MobileDevice + self.frame_callbacks = [] # 帧数据回调函数列表 + self.location_callbacks = [] # 位置数据回调函数列表 + self.device_callbacks = [] # 设备状态回调函数列表 + self.client_threads = [] + + # 统计信息 + self.total_frames_received = 0 + self.total_data_received = 0 + self.start_time = time.time() + + def add_frame_callback(self, callback: Callable): + """添加帧数据回调函数""" + self.frame_callbacks.append(callback) + + def add_location_callback(self, callback: Callable): + """添加位置数据回调函数""" + self.location_callbacks.append(callback) + + def add_device_callback(self, callback: Callable): + """添加设备状态回调函数""" + self.device_callbacks.append(callback) + + def start_server(self): + """启动服务器""" + try: + self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.server_socket.bind(('0.0.0.0', self.port)) + self.server_socket.listen(5) + self.is_running = True + + print(f"📱 手机连接服务器启动成功,端口: {self.port}") + print(f"🌐 等待手机客户端连接...") + + # 启动服务器监听线程 + server_thread = threading.Thread(target=self._server_loop, daemon=True) + server_thread.start() + + # 启动设备状态监控线程 + monitor_thread = threading.Thread(target=self._device_monitor, daemon=True) + monitor_thread.start() + + return True + + except Exception as e: + print(f"❌ 启动服务器失败: {e}") + return False + + def stop_server(self): + """停止服务器""" + self.is_running = False + if self.server_socket: + self.server_socket.close() + + # 清理客户端连接 + for thread in self.client_threads: + if thread.is_alive(): + thread.join(timeout=1.0) + + print("📱 手机连接服务器已停止") + + def _server_loop(self): + """服务器主循环""" + while self.is_running: + try: + client_socket, address = self.server_socket.accept() + print(f"📱 新的手机客户端连接: {address}") + + # 为每个客户端创建处理线程 + client_thread = threading.Thread( + target=self._handle_client, + args=(client_socket, address), + daemon=True + ) + client_thread.start() + self.client_threads.append(client_thread) + + except Exception as e: + if self.is_running: + print(f"⚠️ 服务器接受连接时出错: {e}") + break + + def _handle_client(self, client_socket, address): + """处理客户端连接""" + device_id = None + try: + while self.is_running: + # 接收数据长度 + length_data = self._recv_all(client_socket, 4) + if not length_data: + break + + data_length = struct.unpack('!I', length_data)[0] + + # 接收JSON数据 + json_data = self._recv_all(client_socket, data_length) + if not json_data: + break + + try: + data = json.loads(json_data.decode('utf-8')) + device_id = data.get('device_id') + + if device_id: + self._process_mobile_data(device_id, data, address) + self.total_data_received += len(json_data) + + except json.JSONDecodeError as e: + print(f"⚠️ JSON解析错误: {e}") + continue + + except Exception as e: + print(f"⚠️ 处理客户端 {address} 时出错: {e}") + finally: + client_socket.close() + if device_id and device_id in self.devices: + self.devices[device_id].is_online = False + print(f"📱 设备 {device_id} 已断开连接") + + def _recv_all(self, socket, length): + """接收指定长度的数据""" + data = b'' + while len(data) < length: + packet = socket.recv(length - len(data)) + if not packet: + return None + data += packet + return data + + def _process_mobile_data(self, device_id: str, data: dict, address): + """处理手机发送的数据""" + # 更新或创建设备信息 + if device_id not in self.devices: + device_name = data.get('device_name', f'Mobile-{device_id[:8]}') + self.devices[device_id] = MobileDevice(device_id, device_name) + print(f"📱 新设备注册: {device_name} ({device_id[:8]})") + + # 触发设备状态回调 + for callback in self.device_callbacks: + try: + callback('device_connected', self.devices[device_id]) + except Exception as e: + print(f"⚠️ 设备回调错误: {e}") + + device = self.devices[device_id] + device.update_status(data) + device.connection_info = {'address': address} + + # 处理图像数据 + if 'frame' in data: + try: + frame_data = base64.b64decode(data['frame']) + frame = cv2.imdecode( + np.frombuffer(frame_data, np.uint8), + cv2.IMREAD_COLOR + ) + + if frame is not None: + self.total_frames_received += 1 + + # 触发帧数据回调 + for callback in self.frame_callbacks: + try: + callback(device_id, frame, device) + except Exception as e: + print(f"⚠️ 帧回调错误: {e}") + + except Exception as e: + print(f"⚠️ 图像数据处理错误: {e}") + + # 处理GPS位置数据 + if 'gps' in data and device.is_location_valid(): + for callback in self.location_callbacks: + try: + callback(device_id, device.current_location, device) + except Exception as e: + print(f"⚠️ 位置回调错误: {e}") + + def _device_monitor(self): + """设备状态监控""" + while self.is_running: + try: + current_time = time.time() + offline_devices = [] + + for device_id, device in self.devices.items(): + # 超过30秒没有数据认为离线 + if current_time - device.last_seen > 30: + if device.is_online: + device.is_online = False + offline_devices.append(device_id) + + # 通知离线设备 + for device_id in offline_devices: + print(f"📱 设备 {device_id[:8]} 已离线") + for callback in self.device_callbacks: + try: + callback('device_disconnected', self.devices[device_id]) + except Exception as e: + print(f"⚠️ 设备回调错误: {e}") + + time.sleep(5) # 每5秒检查一次 + + except Exception as e: + print(f"⚠️ 设备监控错误: {e}") + time.sleep(5) + + def get_online_devices(self) -> List[MobileDevice]: + """获取在线设备列表""" + return [device for device in self.devices.values() if device.is_online] + + def get_device_by_id(self, device_id: str) -> Optional[MobileDevice]: + """根据ID获取设备""" + return self.devices.get(device_id) + + def get_statistics(self) -> dict: + """获取连接统计信息""" + online_count = len(self.get_online_devices()) + total_count = len(self.devices) + uptime = time.time() - self.start_time + + return { + 'online_devices': online_count, + 'total_devices': total_count, + 'frames_received': self.total_frames_received, + 'data_received_mb': self.total_data_received / (1024 * 1024), + 'uptime_seconds': uptime, + 'avg_frames_per_second': self.total_frames_received / uptime if uptime > 0 else 0 + } + + def send_command_to_device(self, device_id: str, command: dict): + """向指定设备发送命令(预留接口)""" + # TODO: 实现向手机发送控制命令的功能 pass \ No newline at end of file diff --git a/distance-judgement/src/orientation_detector.py b/distance-judgement/src/orientation_detector.py index d220e20c..d955e89d 100644 --- a/distance-judgement/src/orientation_detector.py +++ b/distance-judgement/src/orientation_detector.py @@ -1,295 +1,295 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -设备朝向检测模块 -用于自动获取设备的GPS位置和朝向信息 -""" - -import requests -import time -import json -import math -from typing import Tuple, Optional, Dict -from . import config - - -class OrientationDetector: - """设备朝向检测器""" - - def __init__(self): - self.current_location = None # (lat, lng, accuracy) - self.current_heading = None # 设备朝向角度 - self.last_update = 0 - self.gps_cache_duration = 300 # GPS缓存5分钟 - - def get_current_gps_location(self) -> Optional[Tuple[float, float, float]]: - """ - 获取当前设备的GPS位置 - 返回: (纬度, 经度, 精度) 或 None - """ - try: - # 首先尝试使用系统API (需要安装相关库) - location = self._get_system_gps() - if location: - return location - - # 如果系统API不可用,使用IP地理定位作为备选 - location = self._get_ip_geolocation() - if location: - print("🌐 使用IP地理定位获取位置(精度较低)") - return location - - return None - - except Exception as e: - print(f"❌ GPS位置获取失败: {e}") - return None - - def _get_system_gps(self) -> Optional[Tuple[float, float, float]]: - """尝试使用系统GPS API获取位置""" - try: - # 在Windows上可以使用Windows Location API - # 这里提供一个框架,实际实现需要根据操作系统选择合适的API - import platform - system = platform.system() - - if system == "Windows": - return self._get_windows_location() - elif system == "Darwin": # macOS - return self._get_macos_location() - elif system == "Linux": - return self._get_linux_location() - - except ImportError: - print("💡 系统定位API不可用,将使用IP定位") - - return None - - def _get_windows_location(self) -> Optional[Tuple[float, float, float]]: - """Windows系统GPS定位""" - try: - # 使用Windows Location API - import winrt.windows.devices.geolocation as geo - - locator = geo.Geolocator() - # 设置期望精度 - locator.desired_accuracy = geo.PositionAccuracy.HIGH - - print("🔍 正在获取Windows系统GPS位置...") - - # 获取位置信息(同步方式) - position = locator.get_geoposition_async().get() - - lat = position.coordinate.point.position.latitude - lng = position.coordinate.point.position.longitude - accuracy = position.coordinate.accuracy - - print(f"✅ Windows GPS获取成功: ({lat:.6f}, {lng:.6f}), 精度: ±{accuracy:.0f}m") - return (lat, lng, accuracy) - - except Exception as e: - print(f"⚠️ Windows GPS API失败: {e}") - return None - - def _get_macos_location(self) -> Optional[Tuple[float, float, float]]: - """macOS系统GPS定位""" - try: - # macOS可以使用Core Location框架 - # 这里提供一个基本框架 - print("💡 macOS GPS定位需要额外配置,建议使用IP定位") - return None - - except Exception as e: - print(f"⚠️ macOS GPS API失败: {e}") - return None - - def _get_linux_location(self) -> Optional[Tuple[float, float, float]]: - """Linux系统GPS定位""" - try: - # Linux可以使用gpsd或NetworkManager - print("💡 Linux GPS定位需要额外配置,建议使用IP定位") - return None - - except Exception as e: - print(f"⚠️ Linux GPS API失败: {e}") - return None - - def _get_ip_geolocation(self) -> Optional[Tuple[float, float, float]]: - """使用IP地址进行地理定位""" - try: - print("🌐 正在使用IP地理定位...") - - # 使用免费的IP地理定位服务 - response = requests.get("http://ip-api.com/json/", timeout=10) - - if response.status_code == 200: - data = response.json() - - if data.get('status') == 'success': - lat = float(data.get('lat', 0)) - lng = float(data.get('lon', 0)) - accuracy = 10000 # IP定位精度通常在10km左右 - - city = data.get('city', '未知') - region = data.get('regionName', '未知') - country = data.get('country', '未知') - - print(f"✅ IP定位成功: {city}, {region}, {country}") - print(f"📍 位置: ({lat:.6f}, {lng:.6f}), 精度: ±{accuracy:.0f}m") - - return (lat, lng, accuracy) - - except Exception as e: - print(f"❌ IP地理定位失败: {e}") - - return None - - def get_device_heading(self) -> Optional[float]: - """ - 获取设备朝向(磁力计方向) - 返回: 角度 (0-360度,0为正北) 或 None - """ - try: - # 桌面设备通常没有磁力计,返回默认朝向 - # 可以根据摄像头位置或用户设置来确定朝向 - print("💡 桌面设备朝向检测有限,使用默认朝向") - - # 假设用户面向屏幕,摄像头朝向用户 - # 如果摄像头在屏幕上方,那么朝向就是用户的相反方向 - default_heading = 180.0 # 假设用户面向南方,摄像头朝向北方 - - return default_heading - - except Exception as e: - print(f"❌ 设备朝向检测失败: {e}") - return None - - def calculate_camera_heading_facing_user(self, user_heading: float) -> float: - """ - 计算摄像头朝向用户的角度 - - Args: - user_heading: 用户朝向角度 (0-360度) - - Returns: - 摄像头应该设置的朝向角度 - """ - # 摄像头朝向用户,即朝向用户相反的方向 - camera_heading = (user_heading + 180) % 360 - return camera_heading - - def auto_configure_camera_location(self) -> Dict: - """ - 自动配置摄像头位置和朝向 - - Returns: - 配置信息字典 - """ - result = { - 'success': False, - 'gps_location': None, - 'device_heading': None, - 'camera_heading': None, - 'method': None, - 'accuracy': None - } - - print("🚀 开始自动配置摄像头位置和朝向...") - - # 1. 获取GPS位置 - gps_location = self.get_current_gps_location() - if not gps_location: - print("❌ 无法获取GPS位置,自动配置失败") - return result - - lat, lng, accuracy = gps_location - result['gps_location'] = (lat, lng) - result['accuracy'] = accuracy - - # 2. 获取设备朝向 - device_heading = self.get_device_heading() - if device_heading is None: - print("⚠️ 无法获取设备朝向,使用默认朝向") - device_heading = 0.0 # 默认朝北 - - result['device_heading'] = device_heading - - # 3. 计算摄像头朝向(朝向用户) - camera_heading = self.calculate_camera_heading_facing_user(device_heading) - result['camera_heading'] = camera_heading - - # 4. 确定配置方法 - if accuracy < 100: - result['method'] = 'GPS' - else: - result['method'] = 'IP定位' - - result['success'] = True - - print(f"✅ 自动配置完成:") - print(f"📍 GPS位置: ({lat:.6f}, {lng:.6f})") - print(f"🧭 设备朝向: {device_heading:.1f}°") - print(f"📷 摄像头朝向: {camera_heading:.1f}°") - print(f"🎯 定位方法: {result['method']}") - print(f"📏 定位精度: ±{accuracy:.0f}m") - - return result - - def update_camera_config(self, gps_location: Tuple[float, float], camera_heading: float): - """ - 更新摄像头配置文件 - - Args: - gps_location: (纬度, 经度) - camera_heading: 摄像头朝向角度 - """ - try: - from tools.setup_camera_location import update_config_file - - lat, lng = gps_location - - # 更新配置文件 - update_config_file(lat, lng, camera_heading) - - # 同时更新运行时配置 - config.CAMERA_LATITUDE = lat - config.CAMERA_LONGITUDE = lng - config.CAMERA_HEADING = camera_heading - - print(f"✅ 摄像头配置已更新") - print(f"📍 新位置: ({lat:.6f}, {lng:.6f})") - print(f"🧭 新朝向: {camera_heading:.1f}°") - - except Exception as e: - print(f"❌ 配置更新失败: {e}") - - -def main(): - """测试函数""" - print("🧭 设备朝向检测器测试") - print("=" * 50) - - detector = OrientationDetector() - - # 测试自动配置 - result = detector.auto_configure_camera_location() - - if result['success']: - print("\n🎯 是否应用此配置? (y/n): ", end="") - choice = input().strip().lower() - - if choice == 'y': - detector.update_camera_config( - result['gps_location'], - result['camera_heading'] - ) - print("✅ 配置已应用") - else: - print("⏭️ 配置未应用") - else: - print("❌ 自动配置失败") - - -if __name__ == "__main__": +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +设备朝向检测模块 +用于自动获取设备的GPS位置和朝向信息 +""" + +import requests +import time +import json +import math +from typing import Tuple, Optional, Dict +from . import config + + +class OrientationDetector: + """设备朝向检测器""" + + def __init__(self): + self.current_location = None # (lat, lng, accuracy) + self.current_heading = None # 设备朝向角度 + self.last_update = 0 + self.gps_cache_duration = 300 # GPS缓存5分钟 + + def get_current_gps_location(self) -> Optional[Tuple[float, float, float]]: + """ + 获取当前设备的GPS位置 + 返回: (纬度, 经度, 精度) 或 None + """ + try: + # 首先尝试使用系统API (需要安装相关库) + location = self._get_system_gps() + if location: + return location + + # 如果系统API不可用,使用IP地理定位作为备选 + location = self._get_ip_geolocation() + if location: + print("🌐 使用IP地理定位获取位置(精度较低)") + return location + + return None + + except Exception as e: + print(f"❌ GPS位置获取失败: {e}") + return None + + def _get_system_gps(self) -> Optional[Tuple[float, float, float]]: + """尝试使用系统GPS API获取位置""" + try: + # 在Windows上可以使用Windows Location API + # 这里提供一个框架,实际实现需要根据操作系统选择合适的API + import platform + system = platform.system() + + if system == "Windows": + return self._get_windows_location() + elif system == "Darwin": # macOS + return self._get_macos_location() + elif system == "Linux": + return self._get_linux_location() + + except ImportError: + print("💡 系统定位API不可用,将使用IP定位") + + return None + + def _get_windows_location(self) -> Optional[Tuple[float, float, float]]: + """Windows系统GPS定位""" + try: + # 使用Windows Location API + import winrt.windows.devices.geolocation as geo + + locator = geo.Geolocator() + # 设置期望精度 + locator.desired_accuracy = geo.PositionAccuracy.HIGH + + print("🔍 正在获取Windows系统GPS位置...") + + # 获取位置信息(同步方式) + position = locator.get_geoposition_async().get() + + lat = position.coordinate.point.position.latitude + lng = position.coordinate.point.position.longitude + accuracy = position.coordinate.accuracy + + print(f"✅ Windows GPS获取成功: ({lat:.6f}, {lng:.6f}), 精度: ±{accuracy:.0f}m") + return (lat, lng, accuracy) + + except Exception as e: + print(f"⚠️ Windows GPS API失败: {e}") + return None + + def _get_macos_location(self) -> Optional[Tuple[float, float, float]]: + """macOS系统GPS定位""" + try: + # macOS可以使用Core Location框架 + # 这里提供一个基本框架 + print("💡 macOS GPS定位需要额外配置,建议使用IP定位") + return None + + except Exception as e: + print(f"⚠️ macOS GPS API失败: {e}") + return None + + def _get_linux_location(self) -> Optional[Tuple[float, float, float]]: + """Linux系统GPS定位""" + try: + # Linux可以使用gpsd或NetworkManager + print("💡 Linux GPS定位需要额外配置,建议使用IP定位") + return None + + except Exception as e: + print(f"⚠️ Linux GPS API失败: {e}") + return None + + def _get_ip_geolocation(self) -> Optional[Tuple[float, float, float]]: + """使用IP地址进行地理定位""" + try: + print("🌐 正在使用IP地理定位...") + + # 使用免费的IP地理定位服务 + response = requests.get("http://ip-api.com/json/", timeout=10) + + if response.status_code == 200: + data = response.json() + + if data.get('status') == 'success': + lat = float(data.get('lat', 0)) + lng = float(data.get('lon', 0)) + accuracy = 10000 # IP定位精度通常在10km左右 + + city = data.get('city', '未知') + region = data.get('regionName', '未知') + country = data.get('country', '未知') + + print(f"✅ IP定位成功: {city}, {region}, {country}") + print(f"📍 位置: ({lat:.6f}, {lng:.6f}), 精度: ±{accuracy:.0f}m") + + return (lat, lng, accuracy) + + except Exception as e: + print(f"❌ IP地理定位失败: {e}") + + return None + + def get_device_heading(self) -> Optional[float]: + """ + 获取设备朝向(磁力计方向) + 返回: 角度 (0-360度,0为正北) 或 None + """ + try: + # 桌面设备通常没有磁力计,返回默认朝向 + # 可以根据摄像头位置或用户设置来确定朝向 + print("💡 桌面设备朝向检测有限,使用默认朝向") + + # 假设用户面向屏幕,摄像头朝向用户 + # 如果摄像头在屏幕上方,那么朝向就是用户的相反方向 + default_heading = 180.0 # 假设用户面向南方,摄像头朝向北方 + + return default_heading + + except Exception as e: + print(f"❌ 设备朝向检测失败: {e}") + return None + + def calculate_camera_heading_facing_user(self, user_heading: float) -> float: + """ + 计算摄像头朝向用户的角度 + + Args: + user_heading: 用户朝向角度 (0-360度) + + Returns: + 摄像头应该设置的朝向角度 + """ + # 摄像头朝向用户,即朝向用户相反的方向 + camera_heading = (user_heading + 180) % 360 + return camera_heading + + def auto_configure_camera_location(self) -> Dict: + """ + 自动配置摄像头位置和朝向 + + Returns: + 配置信息字典 + """ + result = { + 'success': False, + 'gps_location': None, + 'device_heading': None, + 'camera_heading': None, + 'method': None, + 'accuracy': None + } + + print("🚀 开始自动配置摄像头位置和朝向...") + + # 1. 获取GPS位置 + gps_location = self.get_current_gps_location() + if not gps_location: + print("❌ 无法获取GPS位置,自动配置失败") + return result + + lat, lng, accuracy = gps_location + result['gps_location'] = (lat, lng) + result['accuracy'] = accuracy + + # 2. 获取设备朝向 + device_heading = self.get_device_heading() + if device_heading is None: + print("⚠️ 无法获取设备朝向,使用默认朝向") + device_heading = 0.0 # 默认朝北 + + result['device_heading'] = device_heading + + # 3. 计算摄像头朝向(朝向用户) + camera_heading = self.calculate_camera_heading_facing_user(device_heading) + result['camera_heading'] = camera_heading + + # 4. 确定配置方法 + if accuracy < 100: + result['method'] = 'GPS' + else: + result['method'] = 'IP定位' + + result['success'] = True + + print(f"✅ 自动配置完成:") + print(f"📍 GPS位置: ({lat:.6f}, {lng:.6f})") + print(f"🧭 设备朝向: {device_heading:.1f}°") + print(f"📷 摄像头朝向: {camera_heading:.1f}°") + print(f"🎯 定位方法: {result['method']}") + print(f"📏 定位精度: ±{accuracy:.0f}m") + + return result + + def update_camera_config(self, gps_location: Tuple[float, float], camera_heading: float): + """ + 更新摄像头配置文件 + + Args: + gps_location: (纬度, 经度) + camera_heading: 摄像头朝向角度 + """ + try: + from tools.setup_camera_location import update_config_file + + lat, lng = gps_location + + # 更新配置文件 + update_config_file(lat, lng, camera_heading) + + # 同时更新运行时配置 + config.CAMERA_LATITUDE = lat + config.CAMERA_LONGITUDE = lng + config.CAMERA_HEADING = camera_heading + + print(f"✅ 摄像头配置已更新") + print(f"📍 新位置: ({lat:.6f}, {lng:.6f})") + print(f"🧭 新朝向: {camera_heading:.1f}°") + + except Exception as e: + print(f"❌ 配置更新失败: {e}") + + +def main(): + """测试函数""" + print("🧭 设备朝向检测器测试") + print("=" * 50) + + detector = OrientationDetector() + + # 测试自动配置 + result = detector.auto_configure_camera_location() + + if result['success']: + print("\n🎯 是否应用此配置? (y/n): ", end="") + choice = input().strip().lower() + + if choice == 'y': + detector.update_camera_config( + result['gps_location'], + result['camera_heading'] + ) + print("✅ 配置已应用") + else: + print("⏭️ 配置未应用") + else: + print("❌ 自动配置失败") + + +if __name__ == "__main__": main() \ No newline at end of file diff --git a/distance-judgement/src/person_detector.py b/distance-judgement/src/person_detector.py index cb84178b..a6c2073f 100644 --- a/distance-judgement/src/person_detector.py +++ b/distance-judgement/src/person_detector.py @@ -1,100 +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}" +import cv2 +import numpy as np +from ultralytics import YOLO +from . import config + +class PersonDetector: + def __init__(self): + self.model = None + self.load_model() + + def load_model(self): + """加载YOLO模型""" + try: + self.model = YOLO(config.MODEL_PATH) + print(f"YOLO模型加载成功: {config.MODEL_PATH}") + except Exception as e: + print(f"模型加载失败: {e}") + print("正在下载YOLOv8n模型...") + self.model = YOLO('yolov8n.pt') # 会自动下载 + + def detect_persons(self, frame): + """ + 检测图像中的人体 + 返回: 检测结果列表,每个结果包含 [x1, y1, x2, y2, confidence] + """ + if self.model is None: + return [] + + try: + # 使用YOLO进行检测 + results = self.model(frame, verbose=False) + + persons = [] + for result in results: + boxes = result.boxes + if boxes is not None: + for box in boxes: + # 获取类别、置信度和坐标 + cls = int(box.cls[0]) + conf = float(box.conf[0]) + + # 只保留人体检测结果 + if cls == config.PERSON_CLASS_ID and conf >= config.CONFIDENCE_THRESHOLD: + # 获取边界框坐标 + x1, y1, x2, y2 = box.xyxy[0].cpu().numpy() + persons.append([int(x1), int(y1), int(x2), int(y2), conf]) + + return persons + + except Exception as e: + print(f"检测过程中出错: {e}") + return [] + + def draw_detections(self, frame, detections, distances): + """ + 在图像上绘制检测结果和距离信息 + """ + for i, detection in enumerate(detections): + x1, y1, x2, y2, conf = detection + + # 绘制边界框 + cv2.rectangle(frame, (x1, y1), (x2, y2), config.BOX_COLOR, 2) + + # 准备显示文本 + person_id = f"Person #{i+1}" + distance_text = f"Distance: {distances[i]}" if i < len(distances) else "Distance: N/A" + conf_text = f"Conf: {conf:.2f}" + + # 计算文本位置 + text_y = y1 - 35 if y1 - 35 > 20 else y1 + 20 + + # 绘制人员ID文本背景和文字 + id_text_size = cv2.getTextSize(person_id, config.FONT, config.FONT_SCALE, config.FONT_THICKNESS)[0] + cv2.rectangle(frame, (x1, text_y - id_text_size[1] - 5), + (x1 + id_text_size[0] + 10, text_y + 5), (255, 0, 0), -1) + cv2.putText(frame, person_id, (x1 + 5, text_y), + config.FONT, config.FONT_SCALE, config.TEXT_COLOR, config.FONT_THICKNESS) + + # 绘制距离文本背景和文字 + distance_text_y = text_y + 25 + distance_text_size = cv2.getTextSize(distance_text, config.FONT, config.FONT_SCALE, config.FONT_THICKNESS)[0] + cv2.rectangle(frame, (x1, distance_text_y - distance_text_size[1] - 5), + (x1 + distance_text_size[0] + 10, distance_text_y + 5), config.TEXT_BG_COLOR, -1) + cv2.putText(frame, distance_text, (x1 + 5, distance_text_y), + config.FONT, config.FONT_SCALE, config.TEXT_COLOR, config.FONT_THICKNESS) + + # 绘制置信度文本(在框的右上角) + conf_text_size = cv2.getTextSize(conf_text, config.FONT, config.FONT_SCALE - 0.2, config.FONT_THICKNESS)[0] + cv2.rectangle(frame, (x2 - conf_text_size[0] - 10, y1), + (x2, y1 + conf_text_size[1] + 10), config.TEXT_BG_COLOR, -1) + cv2.putText(frame, conf_text, (x2 - conf_text_size[0] - 5, y1 + conf_text_size[1] + 5), + config.FONT, config.FONT_SCALE - 0.2, config.TEXT_COLOR, config.FONT_THICKNESS) + + return frame + + def get_model_info(self): + """获取模型信息""" + if self.model: + return f"YOLO Model: {config.MODEL_PATH}" return "Model not loaded" \ No newline at end of file diff --git a/distance-judgement/src/web_orientation_detector.py b/distance-judgement/src/web_orientation_detector.py index 7d004305..06dc0d43 100644 --- a/distance-judgement/src/web_orientation_detector.py +++ b/distance-judgement/src/web_orientation_detector.py @@ -1,335 +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""" +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Web端朝向检测器 +提供Web API接口用于获取GPS位置和设备朝向信息 +""" + +from flask import Blueprint, jsonify, request +import json +import time +from typing import Dict, Optional, Tuple +from . import config +from .orientation_detector import OrientationDetector + + +class WebOrientationDetector: + """Web端朝向检测器""" + + def __init__(self): + self.orientation_detector = OrientationDetector() + self.current_web_location = None + self.current_web_heading = None + self.last_web_update = 0 + + # 创建Blueprint + self.blueprint = Blueprint('orientation', __name__) + self.setup_routes() + + def setup_routes(self): + """设置Web API路由""" + + @self.blueprint.route('/api/orientation/auto_configure', methods=['POST']) + def auto_configure_from_web(): + """从Web端自动配置摄像头位置和朝向""" + try: + data = request.get_json() or {} + print(f"🔍 收到自动配置请求: {data}") + + # 支持两种数据格式 + # 新格式: {gps_location: [lat, lng], user_heading: heading, apply_config: true} + # 旧格式: {gps: {...}, orientation: {...}} + + if 'gps_location' in data: + # 新格式处理 + gps_location = data.get('gps_location') + user_heading = data.get('user_heading', 0) + apply_config = data.get('apply_config', True) + + if not gps_location or len(gps_location) < 2: + return jsonify({ + "success": False, + "error": "GPS位置数据格式错误" + }) + + lat, lng = float(gps_location[0]), float(gps_location[1]) + + # 验证坐标范围 + if not (-90 <= lat <= 90) or not (-180 <= lng <= 180): + return jsonify({ + "success": False, + "error": "GPS坐标范围不正确" + }) + + # 计算摄像头朝向 + if user_heading is not None: + # 计算摄像头朝向(朝向用户方向) + camera_heading = (user_heading + 180) % 360 + else: + camera_heading = 0.0 + + print(f"📍 处理GPS位置: ({lat:.6f}, {lng:.6f})") + print(f"🧭 用户朝向: {user_heading}°, 摄像头朝向: {camera_heading}°") + + if apply_config: + # 应用配置 + self.orientation_detector.update_camera_config((lat, lng), camera_heading) + print(f"✅ 配置已应用到系统") + + return jsonify({ + "success": True, + "message": "摄像头位置和朝向已自动配置", + "gps_location": [lat, lng], + "user_heading": user_heading, + "camera_heading": camera_heading, + "applied": apply_config + }) + + else: + # 旧格式处理 + gps_data = data.get('gps') + orientation_data = data.get('orientation') + + if not gps_data: + # 如果前端没有提供GPS,尝试后端获取 + result = self.orientation_detector.auto_configure_camera_location() + else: + # 使用前端提供的数据 + result = self.process_web_data(gps_data, orientation_data) + + if result['success']: + # 应用配置 + self.orientation_detector.update_camera_config( + result['gps_location'], + result['camera_heading'] + ) + + return jsonify({ + "success": True, + "message": "摄像头位置和朝向已自动配置", + **result + }) + else: + return jsonify({ + "success": False, + "error": result.get('error', '自动配置失败') + }) + + except Exception as e: + print(f"❌ 自动配置异常: {e}") + import traceback + traceback.print_exc() + return jsonify({ + "success": False, + "error": f"配置失败: {str(e)}" + }) + + @self.blueprint.route('/api/orientation/update_location', methods=['POST']) + def update_location(): + """更新GPS位置信息""" + try: + data = request.get_json() + + if not data or 'latitude' not in data or 'longitude' not in data: + return jsonify({ + "status": "error", + "message": "缺少位置信息" + }) + + lat = float(data['latitude']) + lng = float(data['longitude']) + accuracy = float(data.get('accuracy', 1000)) + + # 验证坐标范围 + if not (-90 <= lat <= 90) or not (-180 <= lng <= 180): + return jsonify({ + "status": "error", + "message": "坐标范围不正确" + }) + + # 更新位置信息 + self.current_web_location = (lat, lng, accuracy) + self.last_web_update = time.time() + + print(f"📍 Web GPS更新: ({lat:.6f}, {lng:.6f}), 精度: ±{accuracy:.0f}m") + + return jsonify({ + "status": "success", + "message": "位置信息已更新" + }) + + except Exception as e: + return jsonify({ + "status": "error", + "message": f"位置更新失败: {str(e)}" + }) + + @self.blueprint.route('/api/orientation/update_heading', methods=['POST']) + def update_heading(): + """更新设备朝向信息""" + try: + data = request.get_json() + + if not data or 'heading' not in data: + return jsonify({ + "status": "error", + "message": "缺少朝向信息" + }) + + heading = float(data['heading']) + + # 标准化角度到0-360范围 + heading = heading % 360 + + # 更新朝向信息 + self.current_web_heading = heading + self.last_web_update = time.time() + + print(f"🧭 Web朝向更新: {heading:.1f}°") + + return jsonify({ + "status": "success", + "message": "朝向信息已更新" + }) + + except Exception as e: + return jsonify({ + "status": "error", + "message": f"朝向更新失败: {str(e)}" + }) + + @self.blueprint.route('/api/orientation/get_status') + def get_orientation_status(): + """获取当前朝向状态""" + try: + current_time = time.time() + + # 检查数据是否过期(30秒) + web_data_fresh = (current_time - self.last_web_update) < 30 + + status = { + "web_location": self.current_web_location, + "web_heading": self.current_web_heading, + "web_data_fresh": web_data_fresh, + "last_update": self.last_web_update, + "current_config": { + "latitude": config.CAMERA_LATITUDE, + "longitude": config.CAMERA_LONGITUDE, + "heading": config.CAMERA_HEADING + } + } + + return jsonify({ + "status": "success", + "data": status + }) + + except Exception as e: + return jsonify({ + "status": "error", + "message": f"状态获取失败: {str(e)}" + }) + + @self.blueprint.route('/api/orientation/apply_config', methods=['POST']) + def apply_config(): + """应用当前的位置和朝向配置""" + try: + if not self.current_web_location: + return jsonify({ + "status": "error", + "message": "没有可用的位置信息" + }) + + lat, lng, accuracy = self.current_web_location + + # 使用Web朝向或默认朝向 + if self.current_web_heading is not None: + # 计算摄像头朝向(朝向用户) + camera_heading = self.orientation_detector.calculate_camera_heading_facing_user( + self.current_web_heading + ) + else: + # 使用默认朝向 + camera_heading = 0.0 + + # 应用配置 + self.orientation_detector.update_camera_config((lat, lng), camera_heading) + + return jsonify({ + "status": "success", + "message": "配置已应用", + "data": { + "latitude": lat, + "longitude": lng, + "camera_heading": camera_heading, + "accuracy": accuracy + } + }) + + except Exception as e: + return jsonify({ + "status": "error", + "message": f"配置应用失败: {str(e)}" + }) + + def process_web_data(self, gps_data: Dict, orientation_data: Optional[Dict] = None) -> Dict: + """ + 处理来自Web端的GPS和朝向数据 + + Args: + gps_data: GPS数据 {'latitude': float, 'longitude': float, 'accuracy': float} + orientation_data: 朝向数据 {'heading': float} (可选) + + Returns: + 配置结果字典 + """ + result = { + 'success': False, + 'gps_location': None, + 'device_heading': None, + 'camera_heading': None, + 'method': 'Web', + 'accuracy': None + } + + try: + # 处理GPS数据 + lat = float(gps_data['latitude']) + lng = float(gps_data['longitude']) + accuracy = float(gps_data.get('accuracy', 1000)) + + # 验证坐标 + if not (-90 <= lat <= 90) or not (-180 <= lng <= 180): + raise ValueError("坐标范围不正确") + + result['gps_location'] = (lat, lng) + result['accuracy'] = accuracy + + # 处理朝向数据 + device_heading = 0.0 # 默认朝向 + if orientation_data and 'heading' in orientation_data: + device_heading = float(orientation_data['heading']) % 360 + + result['device_heading'] = device_heading + + # 计算摄像头朝向(面向用户) + camera_heading = self.orientation_detector.calculate_camera_heading_facing_user(device_heading) + result['camera_heading'] = camera_heading + + result['success'] = True + + print(f"✅ Web数据处理完成:") + print(f"📍 GPS位置: ({lat:.6f}, {lng:.6f})") + print(f"🧭 设备朝向: {device_heading:.1f}°") + print(f"📷 摄像头朝向: {camera_heading:.1f}°") + print(f"📏 定位精度: ±{accuracy:.0f}m") + + except Exception as e: + print(f"❌ Web数据处理失败: {e}") + + return result + + def get_blueprint(self): + """获取Flask Blueprint""" return self.blueprint \ No newline at end of file diff --git a/distance-judgement/src/web_server.py b/distance-judgement/src/web_server.py index 6be0db32..2a160497 100644 --- a/distance-judgement/src/web_server.py +++ b/distance-judgement/src/web_server.py @@ -14,11 +14,96 @@ from .map_manager import MapManager from .mobile_connector import MobileConnector from .web_orientation_detector import WebOrientationDetector +# 🚁 修复无人机模块导入 +try: + from .drone.drone_interface.drone_manager import DroneManager + from .drone.drone_interface.video_receiver import VideoReceiver + print("✅ 成功导入无人机模块:DroneManager, VideoReceiver") +except ImportError as e: + print(f"❌ 无人机模块导入失败: {e}") + print("⚠️ 尝试使用备用导入...") + try: + # 备用导入方案 + import sys + import os + current_dir = os.path.dirname(__file__) + drone_dir = os.path.join(current_dir, 'drone') + sys.path.insert(0, drone_dir) + + from drone_interface.drone_manager import DroneManager + from drone_interface.video_receiver import VideoReceiver + print("✅ 备用导入成功:DroneManager, VideoReceiver") + except ImportError as e2: + print(f"❌ 备用导入也失败: {e2}") + print("🔧 使用模拟类...") + + # 创建模拟类以防止崩溃 + class MockDroneManager: + def __init__(self): + self.connected = False + + def connect(self): + print("⚠️ 模拟DroneManager - 连接功能不可用") + return False + + def disconnect(self): + print("⚠️ 模拟DroneManager - 断开功能不可用") + return True + + def get_state(self): + return {'battery': 0, 'height': 0} + + def get_connection_status(self): + class MockStatus: + name = "DISCONNECTED" + return MockStatus() + + def get_video_stream_url(self): + return "udp://192.168.10.1:11111" + + def takeoff(self): + return False + + def land(self): + return False + + def move(self, direction, distance): + return False + + def rotate(self, direction, angle): + return False + + class MockVideoReceiver: + def __init__(self): + self.running = False + self.last_error = "模拟VideoReceiver - 功能不可用" + self.stream_timeout = 10.0 + + def start(self, stream_url): + print("⚠️ 模拟VideoReceiver - 启动功能不可用") + return False + + def stop(self): + print("⚠️ 模拟VideoReceiver - 停止功能不可用") + return True + + def get_latest_frame(self): + return None + + def get_stats(self): + return {} + + DroneManager = MockDroneManager + VideoReceiver = MockVideoReceiver + print("🔧 已创建模拟类,服务器可以启动但无人机功能将不可用") + + class WebServer: """Web服务器类,管理地图界面和摄像头控制""" def __init__(self): self.app = Flask(__name__) + self.config = config # 添加config引用 self.map_manager = MapManager( api_key=config.GAODE_API_KEY, camera_lat=config.CAMERA_LATITUDE, @@ -46,6 +131,19 @@ class WebServer: # 朝向检测相关 self.web_orientation_detector = WebOrientationDetector() + # 🚁 无人机管理器 + self.drone_manager = DroneManager() + self.video_receiver = VideoReceiver() + + # 🚁 Tello无人机对象(用于直接控制) + self.tello = None + try: + from djitellopy import Tello + self.Tello = Tello + except ImportError: + print("警告: 未找到djitellopy库,请安装: pip install djitellopy") + self.Tello = None + # 设置路由 self.setup_routes() @@ -58,6 +156,20 @@ class WebServer: # 设置安全头部 self.setup_security_headers() + def get_tello(self): + """获取Tello实例,如果不存在则创建""" + if self.tello is None: + if self.Tello is None: + print("❌ 无法创建Tello对象,未找到djitellopy库") + return None + try: + self.tello = self.Tello() + print("🚁 Tello对象已创建") + except Exception as e: + print(f"❌ 创建Tello对象失败: {e}") + return None + return self.tello + def setup_security_headers(self): """设置HTTP安全头部""" @self.app.after_request @@ -126,6 +238,24 @@ class WebServer: except FileNotFoundError: return "测试页面未找到", 404 + @self.app.route('/trajectory_simulation_test.html') + def trajectory_simulation_test(): + """轨迹模拟测试页面""" + try: + with open('trajectory_simulation_test.html', 'r', encoding='utf-8') as f: + return f.read() + except FileNotFoundError: + return "轨迹模拟测试页面未找到", 404 + + @self.app.route('/drone_control.html') + def drone_control(): + """🚁 无人机控制界面""" + try: + with open('drone_control.html', 'r', encoding='utf-8') as f: + return f.read() + except FileNotFoundError: + return "无人机控制界面未找到", 404 + @self.app.route('/api/start_camera', methods=['POST']) def start_camera(): """启动摄像头""" @@ -200,13 +330,20 @@ class WebServer: # 只有有数据的设备才加入返回列表 if device_info: device_info['device_id'] = device_id + # 检查是否为模拟设备(以sim_drone开头的设备ID) + device_info['is_simulation'] = device_id.startswith('sim_drone_') + if device_info['is_simulation']: + device_info['name'] = '🚁模拟无人机' devices_data.append(device_info) + + return jsonify(devices_data) @self.app.route('/api/debug_info') def debug_info(): """获取调试信息""" + current_time = time.time() # 添加时间变量 debug_data = { "camera_active": self.camera_active, "persons_count": len(self.map_manager.get_persons_data()), @@ -216,8 +353,35 @@ class WebServer: "lat": config.CAMERA_LATITUDE, "lng": config.CAMERA_LONGITUDE, "heading": config.CAMERA_HEADING - } + }, + # 🛤️ 添加模拟相关的调试信息 + "simulation_active": getattr(self, 'simulation_active', False), + "simulated_drones_count": len(getattr(self, 'simulated_drones', [])), + "mobile_connector_devices": len(getattr(self.mobile_connector, 'devices', {})), + "mobile_locations_count": len(self.mobile_locations), + "mobile_orientations_count": len(self.mobile_orientations) } + + # 添加模拟设备详情 + debug_data["mobile_locations_detail"] = {} + for device_id, location_data in self.mobile_locations.items(): + debug_data["mobile_locations_detail"][device_id] = { + "is_simulation": device_id.startswith('sim_drone_'), + "latitude": location_data['latitude'], + "longitude": location_data['longitude'], + "timestamp": location_data['timestamp'], + "age_seconds": current_time - location_data['timestamp'] + } + + debug_data["mobile_orientations_detail"] = {} + for device_id, orientation_data in self.mobile_orientations.items(): + debug_data["mobile_orientations_detail"][device_id] = { + "is_simulation": device_id.startswith('sim_drone_'), + "heading": orientation_data['heading'], + "timestamp": orientation_data['timestamp'], + "age_seconds": current_time - orientation_data['timestamp'] + } + return jsonify(debug_data) @self.app.route('/api/get_camera_frame') @@ -295,18 +459,19 @@ class WebServer: @self.app.route('/api/mobile/devices') def get_mobile_devices(): - """获取连接的手机设备列表""" + """获取连接的手机设备列表(包括移动端和Robomaster TT无人机)""" devices = [] # 首先添加Socket连接的设备 for device in self.mobile_connector.get_online_devices(): devices.append({ "device_id": device.device_id, - "device_name": device.device_name, + "device_name": f"📱{device.device_name}", "battery_level": device.battery_level, "is_online": device.is_online, "last_seen": device.last_seen, - "location": device.current_location + "location": device.current_location, + "device_type": "mobile" }) # 然后添加HTTP API连接的设备 @@ -332,12 +497,13 @@ class WebServer: devices.append({ "device_id": device_id, - "device_name": device_info.get('device_name', f'HTTP-Device-{device_id[:8]}'), + "device_name": f"📱{device_info.get('device_name', f'HTTP-Device-{device_id[:8]}')}", "battery_level": device_info.get('battery', 100), "is_online": is_online, "last_seen": last_seen, "location": location, - "connection_type": "HTTP" + "connection_type": "HTTP", + "device_type": "mobile" }) return jsonify(devices) @@ -454,9 +620,9 @@ class WebServer: @self.app.route('/api/remote_device/stream/') def get_remote_device_stream(device_id): - """获取远程设备的视频流""" + """获取远程设备的视频流(支持移动端和Robomaster TT)""" try: - # 获取指定设备的最新帧 + # 首先检查是否是移动设备 if device_id in self.mobile_frames: frame_data = self.mobile_frames[device_id] device_info = frame_data.get('device_info', {}) @@ -466,7 +632,8 @@ class WebServer: "status": "success", "frame": f"data:image/jpeg;base64,{frame_data.get('frame')}", "timestamp": frame_data.get('timestamp'), - "device_info": device_info + "device_info": device_info, + "device_type": "mobile" } # 如果有GPS数据,加入响应中 @@ -479,17 +646,585 @@ class WebServer: } return jsonify(response_data) - else: - return jsonify({ - "status": "no_frame", - "message": "设备无视频数据" - }), 404 + + # 设备未找到 + return jsonify({ + "status": "no_frame", + "message": "设备无视频数据" + }), 404 + except Exception as e: return jsonify({"status": "error", "message": str(e)}), 500 @self.app.route('/favicon.ico') def favicon(): return '', 204 # 返回空内容,状态码204表示无内容 + + # 🚁 无人机API接口 + @self.app.route('/api/drone/connect', methods=['POST']) + def connect_drone(): + """连接无人机""" + try: + data = request.get_json() or {} + drone_ip = data.get('drone_ip', '192.168.10.1') + + print(f"🚁 连接无人机请求: ip={drone_ip}") + + # 首先尝试创建和连接Tello对象 + try: + tello = self.get_tello() + if not tello: + return jsonify({ + "status": "error", + "message": "无法创建Tello对象,请检查djitellopy是否安装" + }), 500 + + # 先停止任何可能正在运行的视频流,避免冲突 + try: + print("🚁 尝试停止可能存在的视频流...") + tello.streamoff() + time.sleep(0.5) # 减少等待时间 + except Exception as e: + print(f"⚠️ 停止视频流命令超时,这是正常的(如果之前没有连接): {e}") + pass + + # 连接到无人机 + print("🚁 正在连接Tello无人机...") + tello.connect() + + # 检查连接是否成功 + print("🚁 检查Tello连接状态...") + try: + # 发送一个命令测试连接,设置较短的超时时间 + response = tello.send_command_with_return('command', timeout=5) + if not response or 'ok' not in response.lower(): + raise Exception(f"命令响应异常: {response}") + + battery = tello.get_battery() + print(f"🚁 Tello连接成功,电池电量: {battery}%") + + return jsonify({ + "status": "success", + "message": "无人机连接成功", + "drone_info": { + "battery": battery, + "ip": drone_ip, + "model": "Tello" + } + }) + except Exception as conn_error: + print(f"❌ Tello连接测试失败: {conn_error}") + return jsonify({ + "status": "error", + "message": f"连接测试失败: {str(conn_error)}" + }), 500 + + except Exception as e: + print(f"❌ Tello连接失败: {e}") + # 回退到使用DroneManager + print("🚁 回退到使用DroneManager...") + success = self.drone_manager.connect() + if success: + print("✅ DroneManager连接成功") + return jsonify({ + "status": "success", + "message": "无人机连接成功(使用DroneManager)", + "drone_info": self.drone_manager.get_state() + }) + else: + print("❌ DroneManager连接失败") + return jsonify({ + "status": "error", + "message": f"无人机连接失败: {str(e)}" + }), 500 + + except Exception as e: + print(f"❌ 连接异常: {e}") + return jsonify({"status": "error", "message": str(e)}), 500 + + @self.app.route('/api/drone/disconnect', methods=['POST']) + def disconnect_drone(): + """断开无人机连接""" + try: + print("🚁 正在断开无人机连接...") + + # 停止视频流 + self.video_receiver.stop() + + # 断开Tello连接 + try: + tello = self.get_tello() + if tello: + tello.streamoff() # 先停止视频流 + tello.end() # 断开连接 + print("🚁 Tello连接已断开") + except Exception as e: + print(f"⚠️ 断开Tello连接时出错: {e}") + + # 断开DroneManager连接 + try: + self.drone_manager.disconnect() + except Exception as e: + print(f"⚠️ 断开DroneManager连接时出错: {e}") + + # 重置Tello对象 + self.tello = None + + print("✅ 无人机已断开连接") + return jsonify({ + "status": "success", + "message": "无人机已断开连接" + }) + except Exception as e: + print(f"❌ 断开连接时出错: {e}") + return jsonify({"status": "error", "message": str(e)}), 500 + + @self.app.route('/api/drone/status') + def get_drone_status(): + """获取无人机状态""" + try: + state = self.drone_manager.get_state() + connection_status = self.drone_manager.get_connection_status() + + return jsonify({ + "status": "success", + "connection_status": connection_status.name, + "drone_state": state + }) + except Exception as e: + return jsonify({"status": "error", "message": str(e)}), 500 + + @self.app.route('/api/drone/start_video', methods=['POST']) + def start_drone_video(): + """启动无人机视频流""" + try: + print("🚁 正在启动无人机视频传输...") + + # 🔧 首先清理任何现有的视频接收器 + print("🔧 清理现有视频接收器...") + try: + self.video_receiver.stop() + time.sleep(0.5) # 等待端口释放 + except Exception as e: + print(f"⚠️ 清理现有接收器时出错: {e}") + + # 🔧 检查并释放UDP端口11111 + import socket + import subprocess + import platform + + def check_and_kill_port_process(port): + """检查并终止占用指定端口的进程""" + try: + if platform.system().lower() == 'windows': + # Windows下查找占用端口的进程 + result = subprocess.run(['netstat', '-ano'], capture_output=True, text=True) + lines = result.stdout.split('\n') + for line in lines: + if f':{port}' in line and 'UDP' in line: + parts = line.split() + if len(parts) >= 5: + pid = parts[-1] + print(f"🔍 发现占用端口{port}的进程PID: {pid}") + # 不强制杀死系统进程,而是记录警告 + if pid != '0': + print(f"⚠️ 端口{port}被PID {pid}占用,可能导致冲突") + return True + except Exception as e: + print(f"⚠️ 检查端口占用时出错: {e}") + return False + + check_and_kill_port_process(11111) + + # **关键修复**: 首先启动无人机的视频传输 + try: + # 获取Tello对象 + tello = self.get_tello() + if not tello: + return jsonify({ + "status": "error", + "message": "无法获取Tello对象,请检查连接" + }), 400 + + # 确保先停止任何现有的视频流 + print("🚁 发送streamoff命令清理现有视频流...") + tello.streamoff() + time.sleep(2) # 等待命令执行和清理 + + # 启动无人机视频传输 + print("🚁 发送streamon命令启动视频传输...") + tello.streamon() + print("🚁 已发送streamon命令,等待无人机开始发送视频数据...") + time.sleep(4) # 增加等待时间,确保无人机开始发送数据 + + except Exception as e: + print(f"❌ 启动无人机视频传输失败: {e}") + return jsonify({ + "status": "error", + "message": f"无法启动无人机视频传输: {str(e)}" + }), 500 + + # 🔧 使用本地UDP监听,而不是从无人机IP接收 + # 修复:Tello发送数据到客户端,我们应该监听本地端口 + stream_url = "udp://0.0.0.0:11111" # 监听本地11111端口 + print(f"🚁 使用UDP监听地址: {stream_url}") + + # 重新创建VideoReceiver实例,确保没有端口冲突 + try: + # 创建新的接收器实例 + from src.drone.drone_interface.video_receiver import VideoReceiver + print("🔧 创建新的VideoReceiver实例...") + self.video_receiver = VideoReceiver() + + # 设置较短的超时时间进行快速测试 + self.video_receiver.stream_timeout = 8.0 + print(f"🚁 视频流超时时间已设置为: {self.video_receiver.stream_timeout}秒") + + except Exception as e: + print(f"❌ 创建VideoReceiver实例失败: {e}") + return jsonify({ + "status": "error", + "message": f"无法创建视频接收器: {str(e)}" + }), 500 + + # 启动视频接收器 + print("🚁 启动视频接收器...") + success = self.video_receiver.start(stream_url) + + if success: + print("✅ 视频流已成功启动") + + # 等待一下检查是否真的有数据 + print("🔍 检查视频数据接收状态...") + time.sleep(2) + + test_frame = self.video_receiver.get_latest_frame() + if test_frame is not None: + print(f"✅ 成功接收到视频帧,尺寸: {test_frame.shape}") + return jsonify({ + "status": "success", + "message": "视频流已启动并成功接收数据", + "stream_url": stream_url, + "frame_info": f"分辨率: {test_frame.shape[1]}x{test_frame.shape[0]}" + }) + else: + print("⚠️ 视频流已启动但暂时没有收到数据,这是正常的") + return jsonify({ + "status": "success", + "message": "视频流已启动,等待无人机发送数据", + "stream_url": stream_url, + "warning": "数据接收中,请稍候" + }) + else: + error_msg = self.video_receiver.last_error or "未知错误" + print(f"❌ 视频流启动失败: {error_msg}") + + # 🔧 提供详细的诊断信息 + diagnostic_info = [] + diagnostic_info.append("请检查以下项目:") + diagnostic_info.append("1. 是否已连接到Tello WiFi (TELLO-xxxxxx)") + diagnostic_info.append("2. 无人机是否已开机") + diagnostic_info.append("3. 是否已建立与无人机的控制连接") + diagnostic_info.append("4. 端口11111是否被其他程序占用") + + return jsonify({ + "status": "error", + "message": f"视频流启动失败: {error_msg}", + "diagnostic": diagnostic_info + }), 500 + + except Exception as e: + print(f"❌ 启动视频流时出错: {e}") + import traceback + traceback.print_exc() + return jsonify({ + "status": "error", + "message": f"系统错误: {str(e)}" + }), 500 + + @self.app.route('/api/drone/stop_video', methods=['POST']) + def stop_drone_video(): + """停止无人机视频流""" + try: + print("🚁 正在停止无人机视频流...") + + # 🔧 强制停止视频接收器并清理资源 + try: + if hasattr(self, 'video_receiver') and self.video_receiver: + print("🔧 停止视频接收器...") + self.video_receiver.stop() + time.sleep(0.5) # 等待资源清理 + print("✅ 视频接收器已停止") + except Exception as e: + print(f"⚠️ 停止视频接收器时出错: {e}") + + # 停止Tello的视频传输 + try: + tello = self.get_tello() + if tello: + print("🚁 发送streamoff命令...") + tello.streamoff() + time.sleep(1) # 等待命令执行 + print("✅ 已发送streamoff命令") + except Exception as e: + print(f"⚠️ 发送streamoff命令时出错: {e}") + + # 🔧 额外的端口清理,确保11111端口被释放 + try: + import socket + test_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + test_socket.bind(('0.0.0.0', 11111)) + test_socket.close() + print("✅ 端口11111已释放") + except Exception as e: + print(f"⚠️ 端口11111可能仍被占用: {e}") + + print("✅ 视频流已完全停止") + return jsonify({ + "status": "success", + "message": "视频流已停止,所有资源已清理" + }) + except Exception as e: + print(f"❌ 停止视频流时出错: {e}") + return jsonify({"status": "error", "message": str(e)}), 500 + + @self.app.route('/api/drone/video_frame') + def get_drone_video_frame(): + """获取无人机视频帧""" + try: + frame = self.video_receiver.get_latest_frame() + if frame is not None: + # 编码为JPEG并转换为base64 + import cv2 + import base64 + + _, buffer = cv2.imencode('.jpg', frame) + frame_base64 = base64.b64encode(buffer).decode('utf-8') + + stats = self.video_receiver.get_stats() + + return jsonify({ + "status": "success", + "frame": f"data:image/jpeg;base64,{frame_base64}", + "timestamp": time.time(), + "stats": stats + }) + else: + # 🔧 修复:返回200状态码而不是404,避免控制台错误 + return jsonify({ + "status": "no_frame", + "message": "等待视频帧数据...", + "timestamp": time.time() + }) + except Exception as e: + print(f"❌ 获取无人机视频帧出错: {e}") + return jsonify({"status": "error", "message": str(e)}), 500 + + @self.app.route('/api/drone/control', methods=['POST']) + def control_drone(): + """控制无人机""" + try: + data = request.get_json() or {} + command = data.get('command') + params = data.get('params', {}) + + if command == 'takeoff': + result = self.drone_manager.takeoff() + elif command == 'land': + result = self.drone_manager.land() + elif command == 'move': + direction = params.get('direction') + distance = params.get('distance', 20) + result = self.drone_manager.move(direction, distance) + elif command == 'rotate': + direction = params.get('direction') + angle = params.get('angle', 90) + result = self.drone_manager.rotate(direction, angle) + else: + return jsonify({ + "status": "error", + "message": f"未知命令: {command}" + }), 400 + + if result: + return jsonify({ + "status": "success", + "message": f"命令 {command} 执行成功" + }) + else: + return jsonify({ + "status": "error", + "message": f"命令 {command} 执行失败" + }), 500 + + except Exception as e: + return jsonify({"status": "error", "message": str(e)}), 500 + + @self.app.route('/api/test/start_simulation', methods=['POST']) + def start_drone_simulation(): + """启动无人机轨迹模拟""" + try: + data = request.get_json() or {} + simulation_type = data.get('type', 'circle') # circle, line, random + drone_count = data.get('drone_count', 1) + speed = data.get('speed', 5) # 移动速度(秒/点) + + self.start_trajectory_simulation(simulation_type, drone_count, speed) + + return jsonify({ + 'status': 'success', + 'message': f'已启动{drone_count}架无人机的{simulation_type}轨迹模拟', + 'simulation_type': simulation_type, + 'drone_count': drone_count, + 'speed': speed + }) + + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}), 500 + + @self.app.route('/api/test/stop_simulation', methods=['POST']) + def stop_drone_simulation(): + """停止无人机轨迹模拟""" + try: + self.stop_trajectory_simulation() + return jsonify({'status': 'success', 'message': '轨迹模拟已停止'}) + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}), 500 + + @self.app.route('/api/test/simulation_status') + def get_simulation_status(): + """获取模拟状态""" + try: + return jsonify({ + 'status': 'success', + 'simulation_active': hasattr(self, 'simulation_active') and self.simulation_active, + 'simulated_drones': getattr(self, 'simulated_drones', []), + 'simulation_type': getattr(self, 'simulation_type', 'none') + }) + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}), 500 + + @self.app.route('/api/drone/diagnose') + def diagnose_drone_connection(): + """诊断无人机连接和视频流状态""" + try: + import subprocess + import platform + import socket + + diagnosis = { + "timestamp": time.time(), + "network": {}, + "tello": {}, + "video_stream": {}, + "system": {}, + "recommendations": [] + } + + # 🔍 网络连接检查 + print("🔍 开始诊断无人机连接...") + + # 检查Tello IP连通性 + try: + tello_ip = "192.168.10.1" + param = '-n' if platform.system().lower() == 'windows' else '-c' + ping_result = subprocess.run( + ['ping', param, '1', tello_ip], + capture_output=True, text=True, timeout=5 + ) + + diagnosis["network"]["ping_success"] = ping_result.returncode == 0 + diagnosis["network"]["ping_output"] = ping_result.stdout[:200] # 限制输出长度 + + if ping_result.returncode != 0: + diagnosis["recommendations"].append("❌ 无法ping通Tello IP (192.168.10.1),请确保已连接到Tello WiFi") + else: + diagnosis["recommendations"].append("✅ 网络连接正常") + + except Exception as e: + diagnosis["network"]["ping_error"] = str(e) + diagnosis["recommendations"].append(f"⚠️ 网络检查失败: {e}") + + # 🔍 端口11111检查 + try: + test_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + test_socket.bind(('0.0.0.0', 11111)) + test_socket.close() + diagnosis["network"]["port_11111_available"] = True + diagnosis["recommendations"].append("✅ UDP端口11111可用") + except Exception as e: + diagnosis["network"]["port_11111_available"] = False + diagnosis["network"]["port_error"] = str(e) + diagnosis["recommendations"].append("❌ UDP端口11111被占用,可能有其他程序在使用") + + # 🔍 Tello连接状态 + try: + tello = self.get_tello() + if tello: + # 尝试获取电池状态 + try: + battery = tello.get_battery() + diagnosis["tello"]["connected"] = True + diagnosis["tello"]["battery"] = battery + diagnosis["recommendations"].append(f"✅ Tello已连接,电量: {battery}%") + except Exception as e: + diagnosis["tello"]["connected"] = False + diagnosis["tello"]["error"] = str(e) + diagnosis["recommendations"].append("❌ Tello已创建但无法通信,检查WiFi连接") + else: + diagnosis["tello"]["connected"] = False + diagnosis["recommendations"].append("❌ 未连接到Tello,请先点击'连接无人机'") + except Exception as e: + diagnosis["tello"]["error"] = str(e) + diagnosis["recommendations"].append(f"⚠️ Tello状态检查失败: {e}") + + # 🔍 视频流状态 + try: + if hasattr(self, 'video_receiver') and self.video_receiver: + diagnosis["video_stream"]["receiver_exists"] = True + diagnosis["video_stream"]["running"] = self.video_receiver.running + + if self.video_receiver.running: + stats = self.video_receiver.get_stats() + diagnosis["video_stream"]["stats"] = stats + latest_frame = self.video_receiver.get_latest_frame() + diagnosis["video_stream"]["has_frames"] = latest_frame is not None + + if latest_frame is not None: + diagnosis["recommendations"].append(f"✅ 视频流正常,已接收 {stats.get('total_frames', 0)} 帧") + else: + diagnosis["recommendations"].append("⚠️ 视频流已启动但暂无帧数据") + else: + diagnosis["recommendations"].append("❌ 视频流未启动") + else: + diagnosis["video_stream"]["receiver_exists"] = False + diagnosis["recommendations"].append("❌ 视频接收器未初始化") + except Exception as e: + diagnosis["video_stream"]["error"] = str(e) + diagnosis["recommendations"].append(f"⚠️ 视频流检查失败: {e}") + + # 🔍 系统信息 + diagnosis["system"]["platform"] = platform.system() + diagnosis["system"]["python_version"] = platform.python_version() + + try: + import cv2 + diagnosis["system"]["opencv_version"] = cv2.__version__ + except: + diagnosis["system"]["opencv_version"] = "未知" + + print("✅ 诊断完成") + return jsonify({ + "status": "success", + "diagnosis": diagnosis + }) + + except Exception as e: + print(f"❌ 诊断过程出错: {e}") + return jsonify({ + "status": "error", + "message": f"诊断失败: {str(e)}" + }), 500 def draw_info_panel(self, frame, person_count=0): """绘制信息面板 - 完全复制main.py的逻辑""" @@ -810,7 +1545,9 @@ class WebServer: 'timestamp': timestamp } - print(f"🧭 设备 {device_id[:8]} 朝向更新: {heading:.1f}° (倾斜:{tilt:.1f}°)") + # 🧭 详细的朝向数据日志,便于调试 + current_time = time.strftime('%H:%M:%S') + print(f"🧭 [{current_time}] 设备 {device_id[:8]} 朝向数据: {heading:.1f}° (倾斜:{tilt:.1f}°, 翻滚:{roll:.1f}°)") return jsonify({ "status": "success", @@ -1238,7 +1975,8 @@ class WebServer: 🚁 无人机战场态势感知系统 - + - - - -
    -

    🧪 设备选择器测试

    - -
    -
    - 📹 视频设备 - -
    -
    - 点击"选择设备"开始使用摄像头 -
    -
    - - - - -
    -
    系统初始化中...
    -
    -
    - - - - + + + + + + + 设备选择器测试 + + + + +
    +

    🧪 设备选择器测试

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

    ✅ 网络连接测试成功!

    -

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

    -

    测试时间: %s

    -

    您的IP: %s

    -

    刷新测试

    - - """ % (time.strftime('%Y-%m-%d %H:%M:%S'), self.client_address[0]) - self.wfile.write(html.encode('utf-8')) - - def log_message(self, format, *args): - print(f"📱 测试访问: {self.client_address[0]} - {format % args}") - - try: - server = HTTPServer(('0.0.0.0', port), TestHandler) - print(f"🧪 测试服务器已启动,端口: {port}") - server.serve_forever() - except Exception as e: - print(f"❌ 测试服务器启动失败: {e}") - -def main(): - print("=" * 60) - print("🔍 网络连接诊断工具") - print("=" * 60) - print() - - # 1. 获取IP地址 - print("📍 1. 获取网络IP地址...") - local_ip = get_local_ip() - if local_ip: - print(f"✅ 本机IP地址: {local_ip}") - else: - print("❌ 无法获取IP地址,请检查网络连接") - return - - # 2. 检查常用端口 - print("\n🔌 2. 检查端口状态...") - ports_to_test = [5000, 8080, 8888] - for port in ports_to_test: - if test_port('127.0.0.1', port): - print(f"⚠️ 端口 {port} 已被占用") - else: - print(f"✅ 端口 {port} 可用") - - # 3. 显示连接信息 - print(f"\n📱 3. 移动设备连接信息:") - print(f" 主服务器地址: http://{local_ip}:5000") - print(f" 手机客户端: http://{local_ip}:5000/mobile/mobile_client.html") - print(f" 测试地址: http://{local_ip}:8888") - - # 4. 防火墙检查提示 - print(f"\n🛡️ 4. 防火墙设置提示:") - print(" 如果平板无法连接,请检查Windows防火墙设置:") - print(" 1. 打开 Windows 安全中心") - print(" 2. 点击 防火墙和网络保护") - print(" 3. 点击 允许应用通过防火墙") - print(" 4. 确保 Python 程序被允许通过防火墙") - print(" 或者临时关闭防火墙进行测试") - - # 5. 网络检查命令 - print(f"\n🔧 5. 网络检查命令:") - print(f" 在平板上ping测试: ping {local_ip}") - print(f" 在电脑上查看网络: ipconfig") - print(f" 检查防火墙状态: netsh advfirewall show allprofiles") - - # 6. 启动测试服务器 - print(f"\n🧪 6. 启动网络测试服务器...") - print(f" 请在平板浏览器访问: http://{local_ip}:8888") - print(" 如果能看到测试页面,说明网络连接正常") - print(" 按 Ctrl+C 停止测试") - print() - - try: - start_test_server(8888) - except KeyboardInterrupt: - print("\n👋 测试结束") - -if __name__ == "__main__": +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +网络连接测试脚本 +帮助诊断手机/平板连接问题 +""" + +import socket +import subprocess +import sys +import threading +import time +from http.server import HTTPServer, SimpleHTTPRequestHandler + +def get_local_ip(): + """获取本机IP地址""" + try: + # 方法1: 连接到远程地址获取本地IP + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("8.8.8.8", 80)) + ip = s.getsockname()[0] + s.close() + return ip + except: + try: + # 方法2: 获取主机名对应的IP + hostname = socket.gethostname() + ip = socket.gethostbyname(hostname) + if ip.startswith("127."): + return None + return ip + except: + return None + +def test_port(host, port): + """测试端口是否可访问""" + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(5) + result = sock.connect_ex((host, port)) + sock.close() + return result == 0 + except: + return False + +def start_test_server(port=8888): + """启动测试HTTP服务器""" + class TestHandler(SimpleHTTPRequestHandler): + def do_GET(self): + self.send_response(200) + self.send_header('Content-type', 'text/html; charset=utf-8') + self.end_headers() + html = """ + 网络测试 + +

    ✅ 网络连接测试成功!

    +

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

    +

    测试时间: %s

    +

    您的IP: %s

    +

    刷新测试

    + + """ % (time.strftime('%Y-%m-%d %H:%M:%S'), self.client_address[0]) + self.wfile.write(html.encode('utf-8')) + + def log_message(self, format, *args): + print(f"📱 测试访问: {self.client_address[0]} - {format % args}") + + try: + server = HTTPServer(('0.0.0.0', port), TestHandler) + print(f"🧪 测试服务器已启动,端口: {port}") + server.serve_forever() + except Exception as e: + print(f"❌ 测试服务器启动失败: {e}") + +def main(): + print("=" * 60) + print("🔍 网络连接诊断工具") + print("=" * 60) + print() + + # 1. 获取IP地址 + print("📍 1. 获取网络IP地址...") + local_ip = get_local_ip() + if local_ip: + print(f"✅ 本机IP地址: {local_ip}") + else: + print("❌ 无法获取IP地址,请检查网络连接") + return + + # 2. 检查常用端口 + print("\n🔌 2. 检查端口状态...") + ports_to_test = [5000, 8080, 8888] + for port in ports_to_test: + if test_port('127.0.0.1', port): + print(f"⚠️ 端口 {port} 已被占用") + else: + print(f"✅ 端口 {port} 可用") + + # 3. 显示连接信息 + print(f"\n📱 3. 移动设备连接信息:") + print(f" 主服务器地址: http://{local_ip}:5000") + print(f" 手机客户端: http://{local_ip}:5000/mobile/mobile_client.html") + print(f" 测试地址: http://{local_ip}:8888") + + # 4. 防火墙检查提示 + print(f"\n🛡️ 4. 防火墙设置提示:") + print(" 如果平板无法连接,请检查Windows防火墙设置:") + print(" 1. 打开 Windows 安全中心") + print(" 2. 点击 防火墙和网络保护") + print(" 3. 点击 允许应用通过防火墙") + print(" 4. 确保 Python 程序被允许通过防火墙") + print(" 或者临时关闭防火墙进行测试") + + # 5. 网络检查命令 + print(f"\n🔧 5. 网络检查命令:") + print(f" 在平板上ping测试: ping {local_ip}") + print(f" 在电脑上查看网络: ipconfig") + print(f" 检查防火墙状态: netsh advfirewall show allprofiles") + + # 6. 启动测试服务器 + print(f"\n🧪 6. 启动网络测试服务器...") + print(f" 请在平板浏览器访问: http://{local_ip}:8888") + print(" 如果能看到测试页面,说明网络连接正常") + print(" 按 Ctrl+C 停止测试") + print() + + try: + start_test_server(8888) + except KeyboardInterrupt: + print("\n👋 测试结束") + +if __name__ == "__main__": main() \ No newline at end of file diff --git a/distance-judgement/tests/test_system.py b/distance-judgement/tests/test_system.py index b56d5fbd..fd119e41 100644 --- a/distance-judgement/tests/test_system.py +++ b/distance-judgement/tests/test_system.py @@ -1,224 +1,280 @@ -#!/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__": +#!/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"✓ 手机端页面可访问") + + # 🛤️ 测试轨迹模拟API + test_trajectory_simulation_apis(base_url) + + 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 test_trajectory_simulation_apis(base_url): + """测试轨迹模拟相关API""" + print("\n🛤️ 测试轨迹模拟API...") + + try: + # 测试模拟状态API + response = requests.get(f"{base_url}/api/test/simulation_status", timeout=5) + if response.status_code == 200: + status_data = response.json() + print(f"✓ 模拟状态API正常: 状态={status_data.get('simulation_active', False)}") + + # 测试启动模拟API + simulation_data = { + "type": "circle", + "drone_count": 1, + "speed": 2 + } + response = requests.post( + f"{base_url}/api/test/start_simulation", + json=simulation_data, + timeout=5 + ) + if response.status_code == 200: + result = response.json() + print(f"✓ 启动模拟API正常: {result.get('message', '无消息')}") + + # 等待一下让模拟运行 + time.sleep(3) + + # 再次检查状态 + response = requests.get(f"{base_url}/api/test/simulation_status", timeout=5) + if response.status_code == 200: + status_data = response.json() + if status_data.get('simulation_active'): + print(f"✓ 模拟正在运行: {len(status_data.get('simulated_drones', []))} 架无人机") + else: + print("⚠️ 模拟未成功启动") + + # 测试停止模拟API + response = requests.post(f"{base_url}/api/test/stop_simulation", timeout=5) + if response.status_code == 200: + result = response.json() + print(f"✓ 停止模拟API正常: {result.get('message', '无消息')}") + + # 测试轨迹模拟测试页面 + response = requests.get(f"{base_url}/trajectory_simulation_test.html", timeout=5) + if response.status_code == 200: + print("✓ 轨迹模拟测试页面可访问") + + except Exception as e: + print(f"⚠️ 轨迹模拟API测试失败: {e}") + # 不抛出异常,因为这只是测试功能 + +def main(): + """主测试函数""" + print("🔧 开始系统综合测试...") + print("测试将验证所有必要的组件是否正常工作") + + tests = [ + ("OpenCV", test_opencv), + ("YOLO模型", test_yolo_model), + ("自定义模块", test_modules), + ("Flask环境", test_flask), + ("Web API", test_web_apis), + ] + + results = {} + + for test_name, test_func in tests: + print(f"\n🧪 开始测试: {test_name}") + results[test_name] = test_func() + + # 显示测试结果摘要 + print("\n" + "=" * 50) + print("📊 测试结果摘要:") + print("=" * 50) + + passed = 0 + total = len(tests) + + for test_name, result in results.items(): + status = "✓ 通过" if result else "✗ 失败" + print(f"{test_name:<15}: {status}") + if result: + passed += 1 + + print(f"\n总体结果: {passed}/{total} 测试通过") + + if passed >= total - 1: # 允许Web API测试失败(因为可能没启动服务器) + print("🎉 系统基本功能正常!") + print("\n📖 使用建议:") + print(" 1. 运行 'python run.py' 选择运行模式") + print(" 2. 使用Web模式获得最佳体验") + print(" 3. 配置摄像头位置获得准确的地图显示") + else: + print("⚠️ 系统存在问题,请检查相关组件") + print("\n🔧 建议操作:") + print(" 1. 重新运行 'python tools/install.py' 安装依赖") + print(" 2. 检查requirements.txt中的依赖版本") + print(" 3. 确认摄像头设备连接正常") + + print("\n测试完成!") + +if __name__ == "__main__": main() \ No newline at end of file diff --git a/distance-judgement/tools/auto_configure_camera.py b/distance-judgement/tools/auto_configure_camera.py index 5657c1db..b1e559e9 100644 --- a/distance-judgement/tools/auto_configure_camera.py +++ b/distance-judgement/tools/auto_configure_camera.py @@ -1,158 +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: +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +摄像头自动配置工具 +自动获取设备位置和朝向,设置摄像头参数 +""" + +import sys +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from src.orientation_detector import OrientationDetector + + +def main(): + """主函数""" + print("=" * 60) + print("🤖 摄像头自动配置工具") + print("=" * 60) + print() + + print("🎯 功能说明:") + print(" • 自动获取当前设备的GPS位置") + print(" • 自动检测设备朝向") + print(" • 计算摄像头应该面向用户的角度") + print(" • 自动更新系统配置文件") + print() + + print("⚠️ 注意事项:") + print(" • 请确保设备连接到互联网") + print(" • Windows系统可能需要开启位置服务") + print(" • 桌面设备朝向检测精度有限") + print() + + try: + # 创建朝向检测器 + detector = OrientationDetector() + + # 执行自动配置 + result = detector.auto_configure_camera_location() + + if result['success']: + print() + print("✅ 自动配置成功!") + print("📊 配置详情:") + print(f" 📍 GPS位置: {result['gps_location'][0]:.6f}, {result['gps_location'][1]:.6f}") + print(f" 🧭 设备朝向: {result['device_heading']:.1f}°") + print(f" 📷 摄像头朝向: {result['camera_heading']:.1f}°") + print(f" 🎯 定位方法: {result['method']}") + print(f" 📏 定位精度: ±{result['accuracy']:.0f}m") + print() + + # 询问是否应用配置 + while True: + choice = input("🔧 是否应用此配置? (y/n/r): ").strip().lower() + + if choice == 'y': + # 应用配置 + detector.update_camera_config( + result['gps_location'], + result['camera_heading'] + ) + print("✅ 配置已应用到系统!") + break + + elif choice == 'n': + print("⏭️ 配置未应用") + break + + elif choice == 'r': + # 重新检测 + print("\n🔄 重新检测...") + result = detector.auto_configure_camera_location() + if not result['success']: + print("❌ 重新检测失败") + break + + print("📊 新的配置详情:") + print(f" 📍 GPS位置: {result['gps_location'][0]:.6f}, {result['gps_location'][1]:.6f}") + print(f" 🧭 设备朝向: {result['device_heading']:.1f}°") + print(f" 📷 摄像头朝向: {result['camera_heading']:.1f}°") + print(f" 🎯 定位方法: {result['method']}") + print(f" 📏 定位精度: ±{result['accuracy']:.0f}m") + print() + + else: + print("❌ 请输入 y(应用)/n(取消)/r(重新检测)") + + else: + print("❌ 自动配置失败") + print("💡 建议:") + print(" 1. 检查网络连接") + print(" 2. 使用手动配置: python tools/setup_camera_location.py") + print(" 3. 或在Web界面中手动设置") + + except KeyboardInterrupt: + print("\n🔴 用户取消操作") + + except Exception as e: + print(f"❌ 配置过程出错: {e}") + print("💡 建议使用手动配置工具") + + finally: + print("\n👋 配置工具结束") + + +def test_gps_only(): + """仅测试GPS定位功能""" + print("🧪 GPS定位测试") + print("-" * 30) + + detector = OrientationDetector() + location = detector.get_current_gps_location() + + if location: + lat, lng, accuracy = location + print(f"✅ GPS测试成功:") + print(f" 📍 位置: {lat:.6f}, {lng:.6f}") + print(f" 📏 精度: ±{accuracy:.0f}m") + else: + print("❌ GPS测试失败") + + +def test_heading_only(): + """仅测试朝向检测功能""" + print("🧪 朝向检测测试") + print("-" * 30) + + detector = OrientationDetector() + heading = detector.get_device_heading() + + if heading is not None: + print(f"✅ 朝向测试成功:") + print(f" 🧭 设备朝向: {heading:.1f}°") + + # 计算摄像头朝向 + camera_heading = detector.calculate_camera_heading_facing_user(heading) + print(f" 📷 摄像头朝向: {camera_heading:.1f}°") + else: + print("❌ 朝向测试失败") + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description='摄像头自动配置工具') + parser.add_argument('--test-gps', action='store_true', help='仅测试GPS功能') + parser.add_argument('--test-heading', action='store_true', help='仅测试朝向功能') + + args = parser.parse_args() + + if args.test_gps: + test_gps_only() + elif args.test_heading: + test_heading_only() + else: main() \ No newline at end of file diff --git a/distance-judgement/tools/generate_ssl_cert.py b/distance-judgement/tools/generate_ssl_cert.py index 0180b0af..04488543 100644 --- a/distance-judgement/tools/generate_ssl_cert.py +++ b/distance-judgement/tools/generate_ssl_cert.py @@ -1,97 +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: +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +生成自签名SSL证书用于HTTPS服务 +""" + +import os +import datetime +import ipaddress +from cryptography import x509 +from cryptography.x509.oid import NameOID +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import rsa + +def generate_ssl_certificate(): + """生成自签名SSL证书""" + + # 创建ssl目录 + ssl_dir = "ssl" + if not os.path.exists(ssl_dir): + os.makedirs(ssl_dir) + + print("🔑 正在生成SSL证书...") + + # 生成私钥 + private_key = rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + ) + + # 创建证书主体 + subject = issuer = x509.Name([ + x509.NameAttribute(NameOID.COUNTRY_NAME, "CN"), + x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Beijing"), + x509.NameAttribute(NameOID.LOCALITY_NAME, "Beijing"), + x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Distance Judgement System"), + x509.NameAttribute(NameOID.COMMON_NAME, "localhost"), + ]) + + # 生成证书 + cert = x509.CertificateBuilder().subject_name( + subject + ).issuer_name( + issuer + ).public_key( + private_key.public_key() + ).serial_number( + x509.random_serial_number() + ).not_valid_before( + datetime.datetime.utcnow() + ).not_valid_after( + datetime.datetime.utcnow() + datetime.timedelta(days=365) + ).add_extension( + x509.SubjectAlternativeName([ + x509.DNSName("localhost"), + x509.DNSName("127.0.0.1"), + x509.IPAddress(ipaddress.IPv4Address("127.0.0.1")), + x509.IPAddress(ipaddress.IPv4Address("0.0.0.0")), + ]), + critical=False, + ).sign(private_key, hashes.SHA256()) + + # 保存私钥 + key_path = os.path.join(ssl_dir, "key.pem") + with open(key_path, "wb") as f: + f.write(private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption() + )) + + # 保存证书 + cert_path = os.path.join(ssl_dir, "cert.pem") + with open(cert_path, "wb") as f: + f.write(cert.public_bytes(serialization.Encoding.PEM)) + + print(f"✅ SSL证书已生成:") + print(f" 🔑 私钥: {key_path}") + print(f" 📜 证书: {cert_path}") + print(f" 📅 有效期: 365天") + print() + print("⚠️ 注意: 这是自签名证书,浏览器会显示安全警告") + print(" 点击 '高级' -> '继续访问localhost(不安全)' 即可") + +if __name__ == "__main__": + try: + generate_ssl_certificate() + except ImportError: + print("❌ 缺少cryptography库,正在尝试安装...") + import subprocess + import sys + subprocess.check_call([sys.executable, "-m", "pip", "install", "cryptography"]) + print("✅ cryptography库安装完成,重新生成证书...") + generate_ssl_certificate() + except Exception as e: print(f"❌ 生成SSL证书失败: {e}") \ No newline at end of file diff --git a/distance-judgement/tools/install.py b/distance-judgement/tools/install.py index 2f83a295..ceb9d4b5 100644 --- a/distance-judgement/tools/install.py +++ b/distance-judgement/tools/install.py @@ -1,264 +1,264 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -自动安装脚本 -自动安装所需依赖并验证环境 -""" - -import subprocess -import sys -import time -import os - -def print_header(title): - """打印标题""" - print("\n" + "=" * 60) - print(f" {title}") - print("=" * 60) - -def run_command(command, description): - """运行命令并显示结果""" - print(f"\n🔄 {description}...") - try: - result = subprocess.run(command, shell=True, capture_output=True, text=True) - if result.returncode == 0: - print(f"✅ {description} 成功完成") - return True - else: - print(f"❌ {description} 失败") - print(f"错误信息: {result.stderr}") - return False - except Exception as e: - print(f"❌ {description} 执行异常: {e}") - return False - -def check_python_version(): - """检查Python版本""" - print_header("检查Python版本") - - version = sys.version_info - print(f"当前Python版本: {version.major}.{version.minor}.{version.micro}") - - if version.major < 3 or (version.major == 3 and version.minor < 8): - print("❌ Python版本过低,需要Python 3.8或更高版本") - return False - else: - print("✅ Python版本满足要求") - return True - -def install_requirements(): - """安装依赖包""" - print_header("安装依赖包") - - if not os.path.exists("requirements.txt"): - print("❌ requirements.txt 文件不存在") - return False - - # 升级pip - if not run_command(f"{sys.executable} -m pip install --upgrade pip", "升级pip"): - print("⚠️ pip升级失败,继续安装依赖") - - # 安装基础依赖 - success1 = run_command(f"{sys.executable} -m pip install -r requirements.txt", "安装基础依赖包") - - # 安装Flask(Web功能所需) - success2 = install_flask() - - return success1 and success2 - -def install_flask(): - """安装Flask及其依赖""" - print("\n🔧 安装Flask(Web功能支持)...") - - try: - # 检查是否已安装 - import flask - print(f"✅ Flask已安装,版本: {flask.__version__}") - return True - except ImportError: - pass - - # 尝试使用清华镜像源 - result = subprocess.run([ - sys.executable, "-m", "pip", "install", - "flask==2.3.3", - "-i", "https://pypi.tuna.tsinghua.edu.cn/simple/", - "--trusted-host", "pypi.tuna.tsinghua.edu.cn" - ], capture_output=True, text=True, timeout=300) - - if result.returncode == 0: - print("✅ Flask安装成功!") - return True - else: - # 尝试官方源 - result = subprocess.run([ - sys.executable, "-m", "pip", "install", "flask==2.3.3" - ], capture_output=True, text=True, timeout=300) - - if result.returncode == 0: - print("✅ Flask安装成功!") - return True - else: - print(f"❌ Flask安装失败: {result.stderr}") - return False - -def verify_installation(): - """验证安装""" - print_header("验证安装") - - modules_to_check = [ - ("cv2", "OpenCV"), - ("numpy", "NumPy"), - ("ultralytics", "Ultralytics"), - ("torch", "PyTorch"), - ("flask", "Flask"), - ] - - all_success = True - - for module, name in modules_to_check: - try: - __import__(module) - print(f"✅ {name} 安装成功") - except ImportError: - print(f"❌ {name} 安装失败") - all_success = False - - return all_success - -def download_yolo_model(): - """预下载YOLO模型""" - print_header("下载YOLO模型") - - try: - from ultralytics import YOLO - print("🔄 正在下载YOLOv8n模型,请稍候...") - model = YOLO('yolov8n.pt') - print("✅ YOLOv8n模型下载成功") - - # 测试模块结构 - try: - from src import PersonDetector, DistanceCalculator - print("✅ 重构后的模块结构测试成功") - except ImportError as e: - print(f"⚠️ 模块结构测试失败: {e}") - - return True - except Exception as e: - print(f"❌ YOLO模型下载失败: {e}") - return False - -def run_test(): - """运行测试""" - print_header("运行系统测试") - - if os.path.exists("test_modules.py"): - return run_command(f"{sys.executable} test_modules.py", "运行系统测试") - else: - print("⚠️ 测试脚本不存在,跳过测试") - return True - -def create_desktop_shortcut(): - """创建桌面快捷方式(Windows)""" - try: - import platform - if platform.system() == "Windows": - print_header("创建桌面快捷方式") - - desktop_path = os.path.join(os.path.expanduser("~"), "Desktop") - if os.path.exists(desktop_path): - shortcut_content = f""" -@echo off -cd /d "{os.getcwd()}" -python run.py -pause -""" - shortcut_path = os.path.join(desktop_path, "人体距离检测.bat") - with open(shortcut_path, "w", encoding="gbk") as f: - f.write(shortcut_content) - print(f"✅ 桌面快捷方式已创建: {shortcut_path}") - return True - else: - print("⚠️ 未找到桌面路径") - return False - except Exception as e: - print(f"⚠️ 创建快捷方式失败: {e}") - return False - -def main(): - """主安装函数""" - print("🚀 人体距离检测系统 - 自动安装程序") - print("此程序将自动安装所需依赖并配置环境") - - steps = [ - ("检查Python版本", check_python_version), - ("安装依赖包", install_requirements), - ("验证安装", verify_installation), - ("下载YOLO模型", download_yolo_model), - ("运行系统测试", run_test), - ] - - # 可选步骤 - optional_steps = [ - ("创建桌面快捷方式", create_desktop_shortcut), - ] - - print(f"\n📋 安装计划:") - for i, (name, _) in enumerate(steps, 1): - print(f" {i}. {name}") - - print(f"\n可选步骤:") - for i, (name, _) in enumerate(optional_steps, 1): - print(f" {i}. {name}") - - input("\n按Enter键开始安装...") - - start_time = time.time() - success_count = 0 - total_steps = len(steps) - - # 执行主要安装步骤 - for i, (name, func) in enumerate(steps, 1): - print(f"\n📦 步骤 {i}/{total_steps}: {name}") - - if func(): - success_count += 1 - print(f"✅ 步骤 {i} 完成") - else: - print(f"❌ 步骤 {i} 失败") - - # 询问是否继续 - choice = input("是否继续安装?(y/n): ").lower() - if choice != 'y': - print("安装已取消") - return - - # 执行可选步骤 - for name, func in optional_steps: - choice = input(f"\n是否执行: {name}?(y/n): ").lower() - if choice == 'y': - func() - - # 显示安装结果 - elapsed_time = time.time() - start_time - print_header("安装完成") - - print(f"✅ 安装步骤: {success_count}/{total_steps} 完成") - print(f"⏱️ 总耗时: {elapsed_time:.1f}秒") - - if success_count == total_steps: - print("🎉 所有步骤都已成功完成!") - print("\n📖 使用指南:") - print(" 1. 运行 'python run.py' 启动系统") - print(" 2. 选择运行模式(Web或传统模式)") - print(" 3. 如需配置摄像头位置,运行 setup_camera_location.py") - print(" 4. 如遇问题,运行 test_modules.py 进行诊断") - else: - print("⚠️ 部分步骤未成功,请检查错误信息") - print("💡 如需帮助,请查看README.md文档") - - print("\n安装程序结束!") - -if __name__ == "__main__": +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +自动安装脚本 +自动安装所需依赖并验证环境 +""" + +import subprocess +import sys +import time +import os + +def print_header(title): + """打印标题""" + print("\n" + "=" * 60) + print(f" {title}") + print("=" * 60) + +def run_command(command, description): + """运行命令并显示结果""" + print(f"\n🔄 {description}...") + try: + result = subprocess.run(command, shell=True, capture_output=True, text=True) + if result.returncode == 0: + print(f"✅ {description} 成功完成") + return True + else: + print(f"❌ {description} 失败") + print(f"错误信息: {result.stderr}") + return False + except Exception as e: + print(f"❌ {description} 执行异常: {e}") + return False + +def check_python_version(): + """检查Python版本""" + print_header("检查Python版本") + + version = sys.version_info + print(f"当前Python版本: {version.major}.{version.minor}.{version.micro}") + + if version.major < 3 or (version.major == 3 and version.minor < 8): + print("❌ Python版本过低,需要Python 3.8或更高版本") + return False + else: + print("✅ Python版本满足要求") + return True + +def install_requirements(): + """安装依赖包""" + print_header("安装依赖包") + + if not os.path.exists("requirements.txt"): + print("❌ requirements.txt 文件不存在") + return False + + # 升级pip + if not run_command(f"{sys.executable} -m pip install --upgrade pip", "升级pip"): + print("⚠️ pip升级失败,继续安装依赖") + + # 安装基础依赖 + success1 = run_command(f"{sys.executable} -m pip install -r requirements.txt", "安装基础依赖包") + + # 安装Flask(Web功能所需) + success2 = install_flask() + + return success1 and success2 + +def install_flask(): + """安装Flask及其依赖""" + print("\n🔧 安装Flask(Web功能支持)...") + + try: + # 检查是否已安装 + import flask + print(f"✅ Flask已安装,版本: {flask.__version__}") + return True + except ImportError: + pass + + # 尝试使用清华镜像源 + result = subprocess.run([ + sys.executable, "-m", "pip", "install", + "flask==2.3.3", + "-i", "https://pypi.tuna.tsinghua.edu.cn/simple/", + "--trusted-host", "pypi.tuna.tsinghua.edu.cn" + ], capture_output=True, text=True, timeout=300) + + if result.returncode == 0: + print("✅ Flask安装成功!") + return True + else: + # 尝试官方源 + result = subprocess.run([ + sys.executable, "-m", "pip", "install", "flask==2.3.3" + ], capture_output=True, text=True, timeout=300) + + if result.returncode == 0: + print("✅ Flask安装成功!") + return True + else: + print(f"❌ Flask安装失败: {result.stderr}") + return False + +def verify_installation(): + """验证安装""" + print_header("验证安装") + + modules_to_check = [ + ("cv2", "OpenCV"), + ("numpy", "NumPy"), + ("ultralytics", "Ultralytics"), + ("torch", "PyTorch"), + ("flask", "Flask"), + ] + + all_success = True + + for module, name in modules_to_check: + try: + __import__(module) + print(f"✅ {name} 安装成功") + except ImportError: + print(f"❌ {name} 安装失败") + all_success = False + + return all_success + +def download_yolo_model(): + """预下载YOLO模型""" + print_header("下载YOLO模型") + + try: + from ultralytics import YOLO + print("🔄 正在下载YOLOv8n模型,请稍候...") + model = YOLO('yolov8n.pt') + print("✅ YOLOv8n模型下载成功") + + # 测试模块结构 + try: + from src import PersonDetector, DistanceCalculator + print("✅ 重构后的模块结构测试成功") + except ImportError as e: + print(f"⚠️ 模块结构测试失败: {e}") + + return True + except Exception as e: + print(f"❌ YOLO模型下载失败: {e}") + return False + +def run_test(): + """运行测试""" + print_header("运行系统测试") + + if os.path.exists("test_modules.py"): + return run_command(f"{sys.executable} test_modules.py", "运行系统测试") + else: + print("⚠️ 测试脚本不存在,跳过测试") + return True + +def create_desktop_shortcut(): + """创建桌面快捷方式(Windows)""" + try: + import platform + if platform.system() == "Windows": + print_header("创建桌面快捷方式") + + desktop_path = os.path.join(os.path.expanduser("~"), "Desktop") + if os.path.exists(desktop_path): + shortcut_content = f""" +@echo off +cd /d "{os.getcwd()}" +python run.py +pause +""" + shortcut_path = os.path.join(desktop_path, "人体距离检测.bat") + with open(shortcut_path, "w", encoding="gbk") as f: + f.write(shortcut_content) + print(f"✅ 桌面快捷方式已创建: {shortcut_path}") + return True + else: + print("⚠️ 未找到桌面路径") + return False + except Exception as e: + print(f"⚠️ 创建快捷方式失败: {e}") + return False + +def main(): + """主安装函数""" + print("🚀 人体距离检测系统 - 自动安装程序") + print("此程序将自动安装所需依赖并配置环境") + + steps = [ + ("检查Python版本", check_python_version), + ("安装依赖包", install_requirements), + ("验证安装", verify_installation), + ("下载YOLO模型", download_yolo_model), + ("运行系统测试", run_test), + ] + + # 可选步骤 + optional_steps = [ + ("创建桌面快捷方式", create_desktop_shortcut), + ] + + print(f"\n📋 安装计划:") + for i, (name, _) in enumerate(steps, 1): + print(f" {i}. {name}") + + print(f"\n可选步骤:") + for i, (name, _) in enumerate(optional_steps, 1): + print(f" {i}. {name}") + + input("\n按Enter键开始安装...") + + start_time = time.time() + success_count = 0 + total_steps = len(steps) + + # 执行主要安装步骤 + for i, (name, func) in enumerate(steps, 1): + print(f"\n📦 步骤 {i}/{total_steps}: {name}") + + if func(): + success_count += 1 + print(f"✅ 步骤 {i} 完成") + else: + print(f"❌ 步骤 {i} 失败") + + # 询问是否继续 + choice = input("是否继续安装?(y/n): ").lower() + if choice != 'y': + print("安装已取消") + return + + # 执行可选步骤 + for name, func in optional_steps: + choice = input(f"\n是否执行: {name}?(y/n): ").lower() + if choice == 'y': + func() + + # 显示安装结果 + elapsed_time = time.time() - start_time + print_header("安装完成") + + print(f"✅ 安装步骤: {success_count}/{total_steps} 完成") + print(f"⏱️ 总耗时: {elapsed_time:.1f}秒") + + if success_count == total_steps: + print("🎉 所有步骤都已成功完成!") + print("\n📖 使用指南:") + print(" 1. 运行 'python run.py' 启动系统") + print(" 2. 选择运行模式(Web或传统模式)") + print(" 3. 如需配置摄像头位置,运行 setup_camera_location.py") + print(" 4. 如遇问题,运行 test_modules.py 进行诊断") + else: + print("⚠️ 部分步骤未成功,请检查错误信息") + print("💡 如需帮助,请查看README.md文档") + + print("\n安装程序结束!") + +if __name__ == "__main__": main() \ No newline at end of file diff --git a/distance-judgement/tools/setup_camera_location.py b/distance-judgement/tools/setup_camera_location.py index 31a7f0e0..b45545ee 100644 --- a/distance-judgement/tools/setup_camera_location.py +++ b/distance-judgement/tools/setup_camera_location.py @@ -1,115 +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__": +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +摄像头地理位置配置工具 +用于设置摄像头的经纬度坐标和朝向角度 +""" + +import os +import sys + +def update_config_file(lat, lng, heading, api_key=None): + """更新配置文件中的摄像头位置信息""" + config_path = "src/config.py" + + # 读取配置文件 + with open(config_path, 'r', encoding='utf-8') as f: + content = f.read() + + # 更新纬度 + if "CAMERA_LATITUDE = " in content: + lines = content.split('\n') + for i, line in enumerate(lines): + if line.startswith('CAMERA_LATITUDE = '): + lines[i] = f'CAMERA_LATITUDE = {lat} # 摄像头纬度' + break + content = '\n'.join(lines) + + # 更新经度 + if "CAMERA_LONGITUDE = " in content: + lines = content.split('\n') + for i, line in enumerate(lines): + if line.startswith('CAMERA_LONGITUDE = '): + lines[i] = f'CAMERA_LONGITUDE = {lng} # 摄像头经度' + break + content = '\n'.join(lines) + + # 更新朝向 + if "CAMERA_HEADING = " in content: + lines = content.split('\n') + for i, line in enumerate(lines): + if line.startswith('CAMERA_HEADING = '): + lines[i] = f'CAMERA_HEADING = {heading} # 摄像头朝向角度' + break + content = '\n'.join(lines) + + # 更新API key(如果提供) + if api_key and 'GAODE_API_KEY = ' in content: + lines = content.split('\n') + for i, line in enumerate(lines): + if line.startswith('GAODE_API_KEY = '): + lines[i] = f'GAODE_API_KEY = "{api_key}" # 高德地图API密钥' + break + content = '\n'.join(lines) + + # 写回配置文件 + with open(config_path, 'w', encoding='utf-8') as f: + f.write(content) + +def main(): + print("=" * 60) + print("🚁 无人机摄像头地理位置配置工具") + print("=" * 60) + print() + + print("📍 请设置摄像头的地理位置信息") + print("提示: 可以通过高德地图等应用获取准确的经纬度坐标") + print() + + try: + # 获取纬度 + lat = float(input("请输入摄像头纬度 (例: 39.9042): ")) + if not (-90 <= lat <= 90): + raise ValueError("纬度必须在-90到90之间") + + # 获取经度 + lng = float(input("请输入摄像头经度 (例: 116.4074): ")) + if not (-180 <= lng <= 180): + raise ValueError("经度必须在-180到180之间") + + # 获取朝向 + heading = float(input("请输入摄像头朝向角度 (0-360°, 0为正北): ")) + if not (0 <= heading <= 360): + raise ValueError("朝向角度必须在0到360之间") + + # 可选:设置高德API Key + print("\n🔑 高德地图API Key设置 (可选)") + print("如果您有高德开放平台的API Key,请输入以获得更好的地图体验") + api_key = input("请输入高德API Key (留空跳过): ").strip() + + # 更新配置 + update_config_file(lat, lng, heading, api_key if api_key else None) + + print("\n✅ 配置更新成功!") + print(f"📍 摄像头位置: ({lat:.6f}, {lng:.6f})") + print(f"🧭 朝向角度: {heading}°") + if api_key: + print("🔑 API Key 已设置") + + print("\n🎯 建议使用步骤:") + print("1. 运行 python run.py 启动系统") + print("2. 选择Web模式获得最佳体验") + print("3. 按 'm' 键打开地图查看效果") + print("4. 按 'c' 键校准距离参数以提高精度") + + except ValueError as e: + print(f"❌ 输入错误: {e}") + print("请重新运行程序并输入正确的数值") + sys.exit(1) + except Exception as e: + print(f"❌ 配置失败: {e}") + sys.exit(1) + +if __name__ == "__main__": main() \ No newline at end of file diff --git a/distance-judgement/trajectory_simulation_test.html b/distance-judgement/trajectory_simulation_test.html new file mode 100644 index 00000000..93e0140d --- /dev/null +++ b/distance-judgement/trajectory_simulation_test.html @@ -0,0 +1,470 @@ + + + + + + + 🛤️ 无人机轨迹模拟测试 + + + + +
    +
    +

    🛤️ 无人机轨迹模拟测试

    +

    测试无人机轨迹记录和可视化功能

    +
    + +
    + + +
    +
    +

    🚀 快速启动模拟

    +
    + + + + +
    +
    +
    + + +
    +
    +

    ⚙️ 高级配置

    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + + +
    +

    📊 模拟状态

    +
    + 模拟状态: + 未启动 +
    +
    + 轨迹类型: + +
    +
    + 无人机数量: + 0 +
    +
    + 轨迹记录: + 开启 +
    +
    + + +
    +
    +

    🚁 模拟无人机列表

    +
    +
    暂无模拟无人机
    +
    +
    +
    + + + +
    + + + + + \ No newline at end of file