Compare commits

...

No commits in common. 'master' and 'gesture-game' have entirely different histories.

27 changed files with 901 additions and 809 deletions
Split View
  1. +0
    -3
      .gitignore
  2. +0
    -149
      README.md
  3. +9
    -0
      environment.yml
  4. BIN
      img/主平台自动化测试结果.png
  5. +4
    -0
      requirements.txt
  6. +0
    -75
      sprint1/sprint1-planning.md
  7. +0
    -10
      sprint1/迭代计划.md
  8. BIN
      sprint1/隔空手势识别系统Sprint1.pptx
  9. BIN
      sprint2/WaveControl-sprint2.pptx
  10. +0
    -120
      sprint2/sprint2.md
  11. BIN
      sprint3/Sprint 3 技术文档:手势识别与游戏控制.pptx
  12. +0
    -146
      sprint3/sprint3.md
  13. BIN
      sprint4/WaveControl-sprint4.pptx
  14. +3
    -0
      src/__init__.py
  15. +27
    -0
      src/app.py
  16. BIN
      src/controllers/.DS_Store
  17. +169
    -0
      src/controllers/gesture_detector.py
  18. BIN
      src/core/.DS_Store
  19. +3
    -0
      src/core/__init__.py
  20. +193
    -0
      src/core/game.py
  21. +9
    -0
      src/main.py
  22. BIN
      src/static/.DS_Store
  23. +227
    -0
      src/static/js/game.js
  24. +172
    -0
      src/static/js/gesture_controller.js
  25. +85
    -0
      src/templates/index.html
  26. +0
    -306
      项目测试文档.md
  27. BIN
      项目测试结果.xlsx

+ 0
- 3
.gitignore View File

@ -1,3 +0,0 @@
node_modules/
src-tauri/
~$项目测试结果.xlsx

+ 0
- 149
README.md View File

@ -1,149 +0,0 @@
# 📡 WaveControl 隔空手势控制系统
> 一套融合**手势识别**与**语音控制**的非接触式人机交互系统,包含主控制平台、手语学习平台 WaveSign、赛车游戏控制模块三大子系统,致力于打造自然、高效、多场景适配的“举手即控”体验。
## 项目简介
在厨房、医疗、演讲等“无法触控”或“不便触控”的环境中,传统鼠标/键盘交互模式效率低、操作受限。**WaveControl**以此为切入点,构建了一个基于摄像头识别的隔空控制系统,融合手势识别与语音识别,实现对系统级输入(键盘/鼠标)、手语教学以及游戏控制等功能。
项目采用模块化架构设计,包含三大子系统:
1. **主控制平台**:支持窗口操作、媒体控制、鼠标替代、游戏映射等功能
2. **赛车游戏交互系统**:实现与《Rush Rally Origins》等赛车游戏的隔空手柄交互
3. **WaveSign 手语通**:面向听障人群的手语学习与社区互动平台
## 测试信息
本项目配套了完整的[测试文档](./项目测试文档.md),涵盖以下子模块:
- ✅ **主控制系统测试程序**:手势识别 → 键盘鼠标映射 → 交互反馈
- ✅ **游戏控制测试程序**:手势实时识别 → 虚拟手柄信号发送 → 赛车游戏响应验证
- ✅ **手语识别与打分测试程序**:摄像头实时捕捉动作 → MediaPipe评分 → UI动画与文本反馈
- ✅ **用户交互测试**:社区发帖、点赞评论、任务管理、日程提醒等核心功能均有覆盖
测试方式支持浏览器调试 + 控制台运行日志追踪,若遇白屏可通过 Chrome DevTools 检查模块加载或路径引用情况。
## Git 分支说明
| 分支名 | 描述 | 跳转链接 |
| -------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| `master` | 主分支,已完成整合,适用于演示与部署 | [🔗 master 分支](https://gitee.com/wydhhh/software-engineering/tree/master/) |
| `finalv1` | 前后端初步融合尝试,手势 → 页面响应逻辑测试阶段 | [🔗 finalv1](https://gitee.com/wydhhh/software-engineering/tree/finalv1/) |
| `finalv2` | 完成手势识别与前端事件联动,页面按钮联动测试 | [🔗 finalv2](https://gitee.com/wydhhh/software-engineering/tree/finalv2/) |
| `finalv3` | 增加语音识别、手势控制切换、音乐控制、手语平台接入、各子系统联调优化 | [🔗 finalv3](https://gitee.com/wydhhh/software-engineering/tree/finalv3/) |
| `gesture` | 手势识别逻辑独立开发模块 | [🔗 gesture](https://gitee.com/wydhhh/software-engineering/tree/gesture/) |
| `gesture_for_chrome` | 针对 Chrome 插件开发的手势控制方案(PPT翻页) | [🔗 gesture_for_chrome](https://gitee.com/wydhhh/software-engineering/tree/gesture_for_chrome/) |
| `gesture-game` | 初步游戏控制实验,控制小球移动 | [🔗 gesture-game](https://gitee.com/wydhhh/software-engineering/tree/gesture-game/) |
| `game_control` | 控制《Rush Rally Origins》赛车游戏,已支持加速转向等 | [🔗 game_control](https://gitee.com/wydhhh/software-engineering/tree/game_control/) |
| `wavesign` | 手语通子系统开发主线,包括教学评分、社区、日程等 | [🔗 wavesign](https://gitee.com/wydhhh/software-engineering/tree/wavesign/) |
| `web` | 最初的网页原型设计,UI 静态草稿 | [🔗 web](https://gitee.com/wydhhh/software-engineering/tree/web/) |
| `screenshot` | 手势控制截屏模块,用于快速抓取操作界面 | [🔗 screenshot](https://gitee.com/wydhhh/software-engineering/tree/screenshot/) |
| `vosk_inc` | 接入 VOSK 实现语音识别与实时字幕展示 | [🔗 vosk_inc](https://gitee.com/wydhhh/software-engineering/tree/vosk_inc/) |
## 技术架构
| 层级 | 技术方案 |
| ------------ | ------------------------------------------------------------ |
| 前端 | vue3 + TypeScript +HTML + CSS + Tailwind CSS + JavaScript + PySide2(Qt GUI) |
| 后端 | Django 4.x(主平台 + 手语通) + Python 脚本逻辑(游戏控制) |
| 手势识别 | MediaPipe Hand Landmarker |
| 虚拟设备控制 | 键盘鼠标模拟、vgamepad 虚拟手柄(XInput) |
| 数据处理 | Kalman Filter(手势抖动滤波)、SQLite3 数据库 |
## 模块介绍
### 1️⃣ 主控制平台(WaveControl)
##### ✌️ **功能特色**
- 多种预设手势操作(点击、滚动、后退等)
- 手势范围调节与自定义手势库
- 支持语音识别辅助控制
- 界面直观,状态反馈实时
##### 🎯 应用场景
- 📺 沙发上追剧时,不再找遥控器,用手势暂停/快进
- 👨‍🏫 教学/演讲中,用手势控制 PPT 流畅翻页
- 🧑‍🍳 厨房做饭时,隔空查食谱不怕弄脏设备
- 🏥 医疗/无菌操作室中,非接触式操作电脑界面
- 🕹️ 游戏中挥手即控,沉浸感倍增
🖼️ **UI 示例**
- 控制主面板(准确率/响应时间/识别窗口)
- 手势管理面板(快捷操作映射设置)
### 2️⃣ 游戏控制模块
🔗 项目演示:[游戏控制项目演示视频](https://www.bilibili.com/video/BV1H5gLzsEn8?vd_source=46a0e2ec60dfb8a247c96905ee47d378)
🎮 目标:**无需实体手柄,通过摄像头即可玩赛车游戏!**
适配游戏:Steam平台《Rush Rally Origins》及支持 Xbox手柄的其他游戏
**实现要点:**
| 功能 | 技术说明 |
| ------------ | ----------------------------------------------------------- |
| 摄像头识别 | OpenCV + MediaPipe |
| 手势控制映射 | 👍右手拇指上扬 = 加速 👍左手 = 刹车 ✋左倾 = 左转,右倾 = 右转 |
| 虚拟手柄接口 | vgamepad + XInput |
| 抖动滤除 | Kalman 滤波器平滑动作 |
| UI反馈 | PySide2 构建调试窗口 |
### 3️⃣ 手语通子项目:**WaveSign**
🔗 项目演示:[手语通项目演示视频](https://www.bilibili.com/video/BV1Fig5zHEhq?vd_source=46a0e2ec60dfb8a247c96905ee47d378)
项目定位:帮助听障人群及其家人朋友学习、练习、交流手语的综合平台
**功能模块:**
- ✅ **手语教学与评分系统**:上传视频或用摄像头练习手语动作,系统打分反馈
- 🗺️ **课程地图与互动练习**:任务式学习,配合卡片式巩固练习
- 👥 **社区交流**:发帖、评论、点赞、关注等功能
- 📅 **日程管理**:内置待办事项与日历,辅助学习安排
- 🧭 **生活服务**
- 出行导航(结合地图与实时提醒)
- 辅助器具推荐
- 就业信息推送
- 无障碍亲子/技能活动预告
**技术实现:**
- Django + SQLite 构建用户系统与服务逻辑
- 使用 MediaPipe 实时评分用户手语表现
- 前端页面响应式 + 卡片式交互体验
## 项目成员
王云岱 朱子玥 杨嘉莉
## 📌 项目进度
- ✅ 第一轮:系统原型 + UI设计 + 手势识别框架
- ✅ 第二轮:主平台功能实现 + 手势控制实现
- ✅ 第三轮:打通语音识别 + 游戏交互控制 + 手语通平台初步实现
- ✅ 第四轮:赛车游戏交互实现 + 手语通平台完整搭建
- 🧪 第五轮:综合测试 + 用户体验调优 + 结项演示

+ 9
- 0
environment.yml View File

@ -0,0 +1,9 @@
name: snakeai
channels:
- defaults
- conda-forge
dependencies:
- python=3.9
- pip
- pip:
- flask==3.0.0

BIN
img/主平台自动化测试结果.png View File

Before After
Width: 2036  |  Height: 681  |  Size: 93 KiB

+ 4
- 0
requirements.txt View File

@ -0,0 +1,4 @@
flask==3.0.0
mediapipe==0.10.8
opencv-python==4.8.1.78
numpy==1.24.3

+ 0
- 75
sprint1/sprint1-planning.md View File

@ -1,75 +0,0 @@
### PPT1 标题 时间 组员
- 标题:隔空手势识别系统Sprint1
- 时间:[具体时间]
- 组员:[组员姓名]
### PPT2 motivation
在当今数字化时代,传统的鼠标键盘操作方式存在一定的局限性,如在某些场景下操作不够便捷、卫生等问题。隔空手势识别系统的出现,能够为用户提供更加自然、便捷的交互方式,填补了市场在非接触式交互领域的空白。它可以广泛应用于智能家居、智能办公、教育演示等多个场景,为用户带来全新的体验。
### PPT3 影响地图
- why?
- 解决传统交互方式在特殊场景下的不便,如疫情期间减少接触、医疗场景避免交叉感染等。
- 满足用户对更加自然、便捷交互方式的需求,提升用户体验。
- 助力企业提升产品的科技感和竞争力,开拓新的市场领域。
- who?
- 普通消费者:用于家庭娱乐、智能家居控制等。
- 办公人群:在会议演示、办公操作中提高效率。
- 教育工作者:在教学过程中进行更加生动的演示。
- 医疗人员:在医疗操作中避免交叉感染。
- how?
- 核心部分分工:算法团队负责手势识别和语音识别算法的开发;软件团队负责系统的整体架构和交互界面的设计;硬件团队负责传感器等硬件设备的选型和开发。
- 采用的技术:计算机视觉技术用于手势识别,语音识别技术用于语音交互,深度学习算法提高识别的准确性和稳定性。
- what?
- 核心功能:远程操控鼠标指针、点击操作、复杂手势识别与响应、语音识别与指令执行。
### PPT4 头脑风暴
- 点子1:在游戏中使用手势进行角色控制,增强游戏的沉浸感。
- 点子2:在商场的自助购物终端上使用手势操作,提高购物效率。
- 点子3:在汽车驾驶中,通过手势控制车内娱乐系统和导航系统,提高驾驶安全性。
- 点子4:在智能健身房中,使用手势控制健身设备和课程选择。
- 点子5:在虚拟现实和增强现实场景中,使用手势进行更加自然的交互。
### PPT5 basic design
系统采用分层架构设计,包括硬件层、驱动层、算法层和应用层。硬件层负责数据的采集,驱动层负责硬件设备的驱动和数据传输,算法层负责手势和语音的识别,应用层负责与用户的交互和业务逻辑的处理。
### PPT6 用户故事
- card1
- conversation1:用户在客厅看电视,想要切换频道,但不想起身找遥控器。
- confirmation1:用户通过隔空手势轻松切换频道,无需使用遥控器。
- card2
- conversation2:办公人员在会议中需要展示文档,想要翻页但不想触碰鼠标。
- confirmation2:办公人员通过手势实现文档的翻页操作,提高会议效率。
- card3
- conversation3:教育工作者在教学过程中,想要展示图片但不想走到电脑前操作。
- confirmation3:教育工作者通过手势控制图片的切换和缩放,使教学更加生动。
### PPT7 初步开发构想
- v1(本周四):完成硬件设备的选型和采购,搭建开发环境,实现基本的手势识别算法。
- v2(下周一):完成系统的整体架构设计,实现手势识别与鼠标指针的初步关联。
- v3(下周四):完善手势识别的准确性和稳定性,实现语音识别功能,并进行初步的测试。
- v4(最终):完成系统的所有功能开发,进行全面的测试和优化,准备上线发布。
### PPT8 初步模块设想
- 子模块:硬件模块、手势识别模块、语音识别模块、交互界面模块、业务逻辑模块。
- 工作量对比:硬件模块和算法模块的工作量相对较大,交互界面模块和业务逻辑模块的工作量相对较小。可以用简单的数字比例表示,如硬件模块:30%,手势识别模块:25%,语音识别模块:20%,交互界面模块:15%,业务逻辑模块:10%。
- 优先级:手势识别模块和硬件模块的优先级较高,需要优先开发;语音识别模块和交互界面模块的优先级次之;业务逻辑模块的优先级相对较低。
### PPT9 future view
未来,我们将不断优化系统的性能和功能,拓展更多的应用场景。例如,与更多的智能家居设备进行集成,实现更加智能化的家居控制;在工业领域应用,提高生产效率和安全性等。
### PPT10 用户旅程
- 优化点1:手势识别的准确性和稳定性需要进一步提高,减少误识别的情况。
- 优化点2:语音识别的灵敏度和准确性需要优化,确保在不同环境下都能准确识别用户的语音指令。
- 优化点3:交互界面的设计需要更加简洁直观,提高用户的操作体验。
- 优化点4:系统的响应速度需要加快,减少用户操作的等待时间。
- 优化点5:增加更多的手势和语音指令,满足用户多样化的需求。
- 优化点6:提高系统的兼容性,支持更多的操作系统和硬件设备。
### PPT11 在迭代中开发
- sprint planning:在每个冲刺阶段开始前,制定详细的计划,明确目标和任务。
- daily scrum:每天进行短会,沟通工作进展和遇到的问题。
- sprint review:在每个冲刺阶段结束后,进行评审,展示成果并收集反馈。
- sprint retrospective:对每个冲刺阶段进行回顾和总结,分析问题并提出改进措施。
### PPT12 thanks
感谢大家的聆听!

+ 0
- 10
sprint1/迭代计划.md View File

@ -1,10 +0,0 @@
# 📈 迭代计划(Sprint Plan)
## 🗓️ 项目时间线
| 时间 | 内容说明 |
| -------- | ------------------------------------------------- |
| 本周四 | 🧩 第一次迭代演示(系统架构设计 + 原型 + UI 草稿) |
| 下周一 | 🔁 第二次迭代演示(打通手势识别 → 系统控制的闭环) |
| 下周四 | 🎙 第三次迭代演示(加入语音控制,初步功能联动) |
| 下下周三 | 🚀 第四次迭代汇报(最终系统完整交付 + 场景演示) |

BIN
sprint1/隔空手势识别系统Sprint1.pptx View File


BIN
sprint2/WaveControl-sprint2.pptx View File


+ 0
- 120
sprint2/sprint2.md View File

@ -1,120 +0,0 @@
## 一、本轮迭代目标
本 Sprint 主要目标是完成 **基于浏览器端的手势控制系统雏形**,实现如下基础功能:
- 搭建前端 Web 应用结构与 UI 页面
- 集成 MediaPipe 手势识别模型,完成实时手势检测
- 映射部分手势为系统控制行为(鼠标移动、点击等)
- 搭建基础手势处理逻辑与组件封装结构
- 支持语音识别指令发送流程(准备后端对接)
------
## 二、本轮主要完成内容
| 类别 | 工作内容 |
| -------- | ------------------------------------------------------------ |
| 前端搭建 | 基于 Vite + Vue 3 + TypeScript 完成项目结构、模块划分、页面初始化 |
| 手势识别 | 集成 Google MediaPipe WASM 模型,完成实时检测与渲染 |
| 控制逻辑 | 封装 `GestureHandler`、`TriggerAction`,实现手势到系统操作的映射 |
| UI设计 | 完成仪表盘式主界面设计,支持响应式、自定义手势预览等 |
| 模型结构 | 初步梳理 `Detector` 识别流程,统一封装初始化与推理逻辑 |
| 后端准备 | 初始化 Python 接口 WebSocket 构建,支持语音识别交互 |
------
## 三、系统结构设计
```
[用户摄像头]
[VideoDetector.vue] → 捕捉视频帧,传入检测器
[Detector.ts] → 调用 MediaPipe 识别手势
[GestureHandler.ts] → 映射行为(鼠标/键盘/语音)
[TriggerAction.ts] → 向后端/系统发出控制指令
[Python 后端](语音识别/扩展指令) ← WebSocket 接入
```
------
## 四、核心技术栈与理由
| 技术 | 用途 | 原因 |
| ---------------- | -------------------- | ------------------------------------------ |
| Vue 3 + Vite | 前端框架 + 构建工具 | 快速开发,热更新快,Composition API 更灵活 |
| TypeScript | 增强类型约束 | 减少运行时错误,提升可维护性 |
| MediaPipe Tasks | 手势识别模型 | 体积小,支持浏览器部署,准确率高 |
| WebSocket | 前后端实时通信 | 保持实时交互流畅 |
| Python + FastAPI | 后端扩展接口(语音) | 快速搭建接口,后续支持 Py 模型运行 |
------
## 五、主要功能点与说明
### 1. Detector 类封装
- 初始化 WASM 模型与识别器
- 封装 `detect()` 每帧调用逻辑
- 支持获取 `landmarks`, `gestures`, `handedness`
### 2. 手势识别与映射
- 仅识别食指 → 鼠标移动
- 食指 + 中指 → 鼠标点击
- 食指 + 拇指捏合 → 滚动
- 四指竖起 → 发送快捷键
- 小指+拇指 → 启动语音识别(前端 WebSocket)
### 3. UI 界面模块
- 实时视频预览窗口
- 子窗口:手势反馈 / 执行动作提示
- 配置面板:开关检测 / 配置行为映射
------
## 六、识别代码示例(关键手势)
```ts
if (gesture === HandGesture.ONLY_INDEX_UP) {
this.triggerAction.moveMouse(x, y)
}
if (gesture === HandGesture.INDEX_AND_THUMB_UP) {
this.triggerAction.scrollUp()
}
```
- 每种手势匹配后,调用封装的 WebSocket + 系统接口发送动作
- `TriggerAction` 支持复用
------
## 七、问题与解决方案
| 问题描述 | 解决方法 |
| --------------------------------- | ---------------------------------------------------- |
| WebSocket 连接后反复断开 | 增加连接状态判断 `readyState !== OPEN`,避免提前发送 |
| Tauri 插件调用 `invoke undefined` | 使用 `npm run tauri dev` 启动,而非普通浏览器启动 |
| 页面刷新后路由丢失(404) | Vue Router 改为 `createWebHashHistory()` 模式 |
| 模型未加载或报错提示 | 增加 loading 控制,捕捉初始化异常 |
------
## 八、未来规划(Sprint 2 预告)
| 方向 | 说明 |
| ----------------- | --------------------------------------------------- |
| 多手势拓展 | 三指滚动、多指触发多功能、多手联合判断 |
| 自定义 .task 模型 | 支持用户采集数据,自定义模型替换现有 MediaPipe 模型 |
| 后端语音指令集成 | Vosk 模型识别语音,触发系统控制 |
| 设置项管理 | 实现用户自定义快捷键、手势配置界面 |

BIN
sprint3/Sprint 3 技术文档:手势识别与游戏控制.pptx View File


+ 0
- 146
sprint3/sprint3.md View File

@ -1,146 +0,0 @@
# Sprint 3 技术文档
**识别到控制闭环打通 + 支持游戏操作**
------
## 一、概述
本阶段实现了一个运行于浏览器端的手势识别系统,结合 MediaPipe 和 WebSocket,打通从摄像头手势识别 → 系统控制(鼠标、滚动、键盘) → 控制小游戏角色的全流程。
新增支持多种系统控制动作和游戏动作,包括:左右移动、跳跃、组合跳跃等。
------
## 二、模块结构概览
模块分工如下:
- **Detector.ts**
加载并初始化 MediaPipe 手势模型,检测手部关键点与手势状态,并调用 `GestureHandler`
- **GestureHandler.ts**
将识别出的手势映射为系统操作(鼠标、滚动、键盘指令等),支持连续确认、平滑处理、动作节流。
- **TriggerAction.ts**
封装 WebSocket 通信,向后端发送 JSON 格式的控制命令。
```
[摄像头视频流]
[VideoDetector.vue]
- 捕捉视频帧
- 显示实时画面
[Detector.ts]
- 使用 MediaPipe Tasks WASM 模型进行手势识别
- 返回关键点、手势类型
[GestureHandler.ts]
- 将识别结果转为行为
- 识别特定组合,如食指 + 拇指 → 滚动
[TriggerAction.ts]
- 向 WebSocket 发送控制指令
- 控制鼠标/键盘/后端动作
[后端 Python]
- WebSocket 接口接收指令
- 准备语音接口/控制中枢
```
------
## 三、识别手势说明(扩展后)
每帧识别手指竖起状态(拇指到小指,0 或 1),组合为状态串,查表识别对应手势:
| 状态串 | 手势名 | 动作描述 |
| ----------- | ------------------- | ------------------------- |
| `0,1,0,0,0` | only_index_up | 鼠标移动 |
| `1,1,0,0,0` | index_and_thumb_up | 鼠标点击 |
| `0,0,1,1,1` | scroll_gesture_2 | 页面滚动 |
| `1,0,1,1,1` | scroll_gesture_2 | 页面滚动(兼容变体) |
| `0,1,1,1,1` | four_fingers_up | 发送快捷键(如播放/全屏) |
| `1,1,1,1,1` | stop_gesture | 暂停/开始识别 |
| `1,0,0,0,1` | voice_gesture_start | 启动语音识别 |
| `0,0,0,0,0` | voice_gesture_stop | 停止语音识别 |
| `0,1,1,0,0` | jump | 小人跳跃(上键) |
| `1,1,1,0,0` | rightjump | 小人右跳(右+上组合) |
| 自定义判断 | direction_right | 小人右移(长按右键) |
| 自定义判断 | delete_gesture | 小人左移(长按左键) |
------
## 四、控制行为逻辑说明
### 鼠标控制
- **移动**:基于食指指尖位置映射至屏幕坐标,支持平滑处理,减少抖动。
- **点击**:食指与拇指同时上举,节流处理防止重复点击。
- **滚动**:捏合(拇指+食指)后上下移动控制滚动方向,带阈值判断。
### 快捷键/系统操作
- 四指上举 → 发送自定义按键(如全屏、暂停、下一集等)。
- 五指上举 → 暂停/开始识别。支持进度提示。
- 删除手势(右手拇指伸出,其余收起)→ 连续发送 Backspace/方向键。
- 自定义 holdKey 方法支持模拟长按。
### 游戏控制扩展(键盘模拟)
| 手势名 | 动作 |
| --------------- | -------------------- |
| delete_gesture | 向左移动(左键长按) |
| direction_right | 向右移动(右键长按) |
| jump | 向上跳跃(上键) |
| rightjump | 右跳(右后延迟上) |
------
## 五、性能与稳定性处理
| 问题 | 解决方案 |
| ------------------ | ------------------------------------------------------ |
| 手势误判 | 连续帧确认(minGestureCount ≥ 5) |
| 鼠标抖动 | 屏幕坐标引入平滑系数 `smoothening` |
| 动作重复触发 | 节流控制点击/滚动/快捷键/方向键(如 `CLICK_INTERVAL`) |
| WebSocket 中断重连 | 异常断线后自动重连,带重试时间间隔 |
| 滚动误触 | 拇指与食指距离阈值控制是否处于“捏合”状态 |
| UI 状态提示不同步 | 使用全局 store(如 `app_store.sub_window_info`)更新 |
------
## 六、技术特性总结
- 支持 MediaPipe WASM 模型在前端本地运行,低延迟识别
- 所有动作模块化封装,便于未来扩展新手势、新控制指令
- 手势控制与后端通过 WebSocket 实时交互,接口稳定
- 支持游戏场景(虚拟角色)控制,具备实际互动展示能力
- 控制逻辑可配置:如识别区域、快捷键内容、手势灵敏度
------
## 七、后续优化建议(下一阶段)
| 方向 | 目标 |
| -------------------- | ----------------------------------------------- |
| 增加左手手势组合识别 | 允许双手协同控制,比如捏合+四指等 |
| 提升模型识别精度 | 支持定制 MediaPipe .task 文件、自采样训练 |
| UI 设置面板 | 提供手势→动作自定义、阈值调节、快捷键修改 |
| 引入语音控制 | Whisper/Vosk 实现语音命令解析,结合手势联动使用 |
| 多模式切换 | 视频控制、PPT 控制、游戏控制等可切换交互模式 |
| 场景演示/视频录制 | 准备真实交互展示视频,用于演示或上线宣传 |
------
## 八、结语
本轮迭代成功实现了从手势识别到控制动作的完整闭环,扩展支持了游戏角色控制(多方向移动与跳跃),并在操作流畅性、准确性与系统解耦方面都取得实质进展。后续可以围绕用户可配置性、语音识别联动与多场景适配进一步扩展系统能力。

BIN
sprint4/WaveControl-sprint4.pptx View File


+ 3
- 0
src/__init__.py View File

@ -0,0 +1,3 @@
"""
Snake Game Package
"""

+ 27
- 0
src/app.py View File

@ -0,0 +1,27 @@
from flask import Flask, render_template, request, jsonify
import os
from controllers.gesture_detector import GestureDetector
app = Flask(__name__)
gesture_detector = GestureDetector()
@app.route('/')
def index():
return render_template('index.html')
@app.route('/detect_gesture', methods=['POST'])
def detect_gesture():
try:
data = request.json
image_data = data.get('image')
if not image_data:
return jsonify({'error': '没有收到图像数据'}), 400
result = gesture_detector.process_image(image_data)
return jsonify(result)
except Exception as e:
return jsonify({'error': str(e)}), 500
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=8080)

BIN
src/controllers/.DS_Store View File


+ 169
- 0
src/controllers/gesture_detector.py View File

@ -0,0 +1,169 @@
import cv2
import numpy as np
import mediapipe as mp
import base64
import time
class GestureDetector:
def __init__(self):
# 初始化MediaPipe Hands
self.mp_hands = mp.solutions.hands
self.hands = self.mp_hands.Hands(
static_image_mode=False,
max_num_hands=1,
min_detection_confidence=0.7,
min_tracking_confidence=0.5
)
self.mp_draw = mp.solutions.drawing_utils
self.last_gesture = None
# 手掌状态追踪
self.palm_start_time = None
self.palm_hold_duration = 0.5 # 需要手掌打开持续0.5秒才触发
self.is_palm_triggered = False
def process_image(self, image_data):
"""处理图像数据并识别手势"""
try:
# 解码Base64图像数据
image_data = image_data.split(',')[1]
image_bytes = base64.b64decode(image_data)
nparr = np.frombuffer(image_bytes, np.uint8)
frame = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
# 转换颜色空间
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# 处理图像
results = self.hands.process(frame_rgb)
if results.multi_hand_landmarks:
landmarks = results.multi_hand_landmarks[0]
# 绘制手部关键点和连接线
self.mp_draw.draw_landmarks(
frame,
landmarks,
self.mp_hands.HAND_CONNECTIONS
)
# 识别手势
gesture = self._recognize_gesture(landmarks)
# 将处理后的图像编码为Base64
_, buffer = cv2.imencode('.jpg', frame)
processed_image = base64.b64encode(buffer).decode('utf-8')
return {
'gesture': gesture,
'processed_image': f'data:image/jpeg;base64,{processed_image}'
}
else:
# 如果没有检测到手,重置状态
self.palm_start_time = None
self.is_palm_triggered = False
return {'gesture': None, 'processed_image': None}
except Exception as e:
print(f"手势识别错误: {str(e)}")
return {'gesture': None, 'processed_image': None}
def _recognize_gesture(self, landmarks):
"""识别手势类型"""
# 检查是否是手掌打开手势
is_palm_open = self._is_palm_open(landmarks)
current_time = time.time()
if is_palm_open:
if self.palm_start_time is None:
# 开始手掌打开计时
self.palm_start_time = current_time
elif not self.is_palm_triggered and (current_time - self.palm_start_time) >= self.palm_hold_duration:
# 手掌打开持续时间达到阈值且未触发过
self.is_palm_triggered = True
return 'TOGGLE_PAUSE'
else:
# 不是手掌打开,重置状态
self.palm_start_time = None
self.is_palm_triggered = False
# 检查方向控制
direction = self._check_direction(landmarks)
if direction:
return direction
return None
def _is_palm_open(self, landmarks):
"""检查是否是手掌打开手势(所有手指伸直)"""
# 获取所有手指的关键点
finger_tips = [
self.mp_hands.HandLandmark.INDEX_FINGER_TIP,
self.mp_hands.HandLandmark.MIDDLE_FINGER_TIP,
self.mp_hands.HandLandmark.RING_FINGER_TIP,
self.mp_hands.HandLandmark.PINKY_TIP
]
finger_pips = [
self.mp_hands.HandLandmark.INDEX_FINGER_PIP,
self.mp_hands.HandLandmark.MIDDLE_FINGER_PIP,
self.mp_hands.HandLandmark.RING_FINGER_PIP,
self.mp_hands.HandLandmark.PINKY_PIP
]
# 检查所有手指是否伸直
for tip_id, pip_id in zip(finger_tips, finger_pips):
tip = landmarks.landmark[tip_id]
pip = landmarks.landmark[pip_id]
# 如果指尖不是明显高于PIP关节,说明手指没有伸直
if tip.y >= pip.y - 0.02: # 添加一个小的容差值
return False
# 特别检查拇指是否伸直(与其他手指分开)
thumb_tip = landmarks.landmark[self.mp_hands.HandLandmark.THUMB_TIP]
thumb_ip = landmarks.landmark[self.mp_hands.HandLandmark.THUMB_IP]
thumb_mcp = landmarks.landmark[self.mp_hands.HandLandmark.THUMB_MCP]
# 检查拇指是否张开(与其他手指成一定角度)
thumb_angle = self._calculate_angle(thumb_mcp, thumb_ip, thumb_tip)
if thumb_angle < 150: # 拇指需要足够伸直
return False
return True
def _calculate_angle(self, point1, point2, point3):
"""计算三个点形成的角度"""
vector1 = np.array([point1.x - point2.x, point1.y - point2.y])
vector2 = np.array([point3.x - point2.x, point3.y - point2.y])
cos_angle = np.dot(vector1, vector2) / (np.linalg.norm(vector1) * np.linalg.norm(vector2))
angle = np.degrees(np.arccos(np.clip(cos_angle, -1.0, 1.0)))
return angle
def _check_direction(self, landmarks):
"""检查方向控制手势"""
# 获取食指关键点
index_tip = landmarks.landmark[self.mp_hands.HandLandmark.INDEX_FINGER_TIP]
index_pip = landmarks.landmark[self.mp_hands.HandLandmark.INDEX_FINGER_PIP]
wrist = landmarks.landmark[self.mp_hands.HandLandmark.WRIST]
# 检查食指是否伸直
if index_tip.y < index_pip.y:
# 计算方向
dx = index_tip.x - wrist.x
dy = index_tip.y - wrist.y
threshold = 0.05
if abs(dx) > abs(dy): # 水平方向
if abs(dx) > threshold:
return 'LEFT' if dx > 0 else 'RIGHT'
else: # 垂直方向
if abs(dy) > threshold:
return 'DOWN' if dy > 0 else 'UP'
return None
def __del__(self):
"""清理资源"""
self.hands.close()

BIN
src/core/.DS_Store View File


+ 3
- 0
src/core/__init__.py View File

@ -0,0 +1,3 @@
"""
Core game components
"""

+ 193
- 0
src/core/game.py View File

@ -0,0 +1,193 @@
import pygame
import random
import numpy as np
from controllers.voice_controller import VoiceController
class SnakeGame:
"""贪吃蛇游戏核心类"""
def __init__(self, width=800, height=600, grid_size=20):
"""
Args:
width (int):
height (int):
grid_size (int):
"""
self.width = width
self.height = height
self.grid_size = grid_size
# 初始化语音控制器
self.voice_controller = VoiceController()
self.voice_enabled = False
self.reset()
# 初始化Pygame
pygame.init()
self.screen = pygame.display.set_mode((width, height))
pygame.display.set_caption('语音手势贪吃蛇')
self.clock = pygame.time.Clock()
# 颜色定义
self.colors = {
'background': (0, 0, 0),
'snake': (0, 255, 0),
'food': (255, 0, 0),
'text': (255, 255, 255)
}
def reset(self):
"""重置游戏状态"""
self.snake = [(self.width // 2, self.height // 2)]
self.direction = (self.grid_size, 0) # 初始向右移动
self.food = self._generate_food()
self.score = 0
self.game_over = False
self.paused = False
def _generate_food(self):
"""生成食物的位置"""
while True:
x = random.randrange(0, self.width, self.grid_size)
y = random.randrange(0, self.height, self.grid_size)
if (x, y) not in self.snake:
return (x, y)
def update(self):
"""更新游戏状态"""
if self.game_over or self.paused:
return
# 移动蛇
new_head = (
(self.snake[0][0] + self.direction[0]) % self.width,
(self.snake[0][1] + self.direction[1]) % self.height
)
# 检查碰撞
if new_head in self.snake:
self.game_over = True
if self.voice_enabled:
self.voice_controller.speak("游戏结束")
return
self.snake.insert(0, new_head)
# 检查是否吃到食物
if new_head == self.food:
self.score += 1
if self.voice_enabled:
self.voice_controller.speak(f"得分:{self.score}")
self.food = self._generate_food()
else:
self.snake.pop()
def change_direction(self, new_direction):
"""
Args:
new_direction (tuple): (grid_size, 0)
"""
# 防止直接反向移动
if (new_direction[0] * -1, new_direction[1] * -1) != self.direction:
self.direction = new_direction
def draw(self):
"""绘制游戏画面"""
self.screen.fill(self.colors['background'])
# 绘制蛇
for segment in self.snake:
pygame.draw.rect(self.screen, self.colors['snake'],
(segment[0], segment[1], self.grid_size-2, self.grid_size-2))
# 绘制食物
pygame.draw.rect(self.screen, self.colors['food'],
(self.food[0], self.food[1], self.grid_size-2, self.grid_size-2))
# 绘制分数
font = pygame.font.Font(None, 36)
score_text = font.render(f'分数: {self.score}', True, self.colors['text'])
self.screen.blit(score_text, (10, 10))
# 绘制语音控制状态
voice_status = "语音控制: 开启" if self.voice_enabled else "语音控制: 关闭"
voice_text = font.render(voice_status, True, self.colors['text'])
self.screen.blit(voice_text, (10, 50))
if self.game_over:
game_over_text = font.render('游戏结束!按R重新开始', True, self.colors['text'])
text_rect = game_over_text.get_rect(center=(self.width/2, self.height/2))
self.screen.blit(game_over_text, text_rect)
pygame.display.flip()
def handle_voice_command(self):
"""处理语音命令"""
if not self.voice_enabled:
return
command = self.voice_controller.get_command()
if command:
if command == 'UP':
self.change_direction((0, -self.grid_size))
elif command == 'DOWN':
self.change_direction((0, self.grid_size))
elif command == 'LEFT':
self.change_direction((-self.grid_size, 0))
elif command == 'RIGHT':
self.change_direction((self.grid_size, 0))
elif command == 'PAUSE':
self.paused = True
elif command == 'RESUME':
self.paused = False
elif command == 'START' and self.game_over:
self.reset()
def handle_input(self):
"""处理键盘输入"""
for event in pygame.event.get():
if event.type == pygame.QUIT:
return False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_r and self.game_over:
self.reset()
elif event.key == pygame.K_SPACE:
self.paused = not self.paused
elif event.key == pygame.K_v: # 切换语音控制
self.voice_enabled = not self.voice_enabled
if self.voice_enabled:
self.voice_controller.start_listening()
else:
self.voice_controller.stop_listening()
elif not self.paused and not self.game_over:
if event.key == pygame.K_UP:
self.change_direction((0, -self.grid_size))
elif event.key == pygame.K_DOWN:
self.change_direction((0, self.grid_size))
elif event.key == pygame.K_LEFT:
self.change_direction((-self.grid_size, 0))
elif event.key == pygame.K_RIGHT:
self.change_direction((self.grid_size, 0))
return True
def run(self):
"""运行游戏主循环"""
running = True
while running:
running = self.handle_input()
self.handle_voice_command()
self.update()
self.draw()
self.clock.tick(10) # 控制游戏速度
# 清理资源
if self.voice_enabled:
self.voice_controller.stop_listening()
pygame.quit()

+ 9
- 0
src/main.py View File

@ -0,0 +1,9 @@
from core.game import SnakeGame
def main():
"""游戏主入口函数"""
game = SnakeGame()
game.run()
if __name__ == "__main__":
main()

BIN
src/static/.DS_Store View File


+ 227
- 0
src/static/js/game.js View File

@ -0,0 +1,227 @@
class SnakeGame {
constructor() {
this.canvas = document.getElementById('gameCanvas');
this.ctx = this.canvas.getContext('2d');
this.gridSize = 20;
this.snake = [{x: 15, y: 15}];
this.direction = {x: 0, y: 0};
this.food = this.generateFood();
this.score = 0;
this.gameLoop = null;
this.isPaused = false;
this.isGameOver = false;
// 初始化控制按钮
this.startBtn = document.getElementById('startBtn');
this.pauseBtn = document.getElementById('pauseBtn');
this.restartBtn = document.getElementById('restartBtn');
this.gestureBtn = document.getElementById('gestureBtn');
this.scoreElement = document.getElementById('score');
// 初始化手势控制器
this.gestureController = new GestureController(this);
// 绑定事件处理
this.bindEvents();
}
bindEvents() {
// 键盘控制
document.addEventListener('keydown', (e) => this.handleKeyPress(e));
// 按钮控制
this.startBtn.addEventListener('click', () => this.startGame());
this.pauseBtn.addEventListener('click', () => this.togglePause());
this.restartBtn.addEventListener('click', () => this.restartGame());
this.gestureBtn.addEventListener('click', () => this.toggleGestureControl());
// 触摸控制
let touchStartX = 0;
let touchStartY = 0;
this.canvas.addEventListener('touchstart', (e) => {
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
});
this.canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
if (this.isPaused || !this.gameLoop) return;
const touchEndX = e.touches[0].clientX;
const touchEndY = e.touches[0].clientY;
const dx = touchEndX - touchStartX;
const dy = touchEndY - touchStartY;
if (Math.abs(dx) > Math.abs(dy)) {
if (dx > 0 && this.direction.x === 0) this.direction = {x: 1, y: 0};
else if (dx < 0 && this.direction.x === 0) this.direction = {x: -1, y: 0};
} else {
if (dy > 0 && this.direction.y === 0) this.direction = {x: 0, y: 1};
else if (dy < 0 && this.direction.y === 0) this.direction = {x: 0, y: -1};
}
});
}
handleKeyPress(e) {
if (this.isPaused || !this.gameLoop) return;
switch (e.key) {
case 'ArrowUp':
if (this.direction.y === 0) this.direction = {x: 0, y: -1};
break;
case 'ArrowDown':
if (this.direction.y === 0) this.direction = {x: 0, y: 1};
break;
case 'ArrowLeft':
if (this.direction.x === 0) this.direction = {x: -1, y: 0};
break;
case 'ArrowRight':
if (this.direction.x === 0) this.direction = {x: 1, y: 0};
break;
}
}
generateFood() {
const maxX = this.canvas.width / this.gridSize - 1;
const maxY = this.canvas.height / this.gridSize - 1;
let food;
do {
food = {
x: Math.floor(Math.random() * maxX),
y: Math.floor(Math.random() * maxY)
};
} while (this.snake.some(segment => segment.x === food.x && segment.y === food.y));
return food;
}
update() {
if (this.isPaused || this.isGameOver) return;
// 计算新的蛇头位置
const head = {
x: (this.snake[0].x + this.direction.x + this.canvas.width / this.gridSize) % (this.canvas.width / this.gridSize),
y: (this.snake[0].y + this.direction.y + this.canvas.height / this.gridSize) % (this.canvas.height / this.gridSize)
};
// 检查碰撞
if (this.snake.some(segment => segment.x === head.x && segment.y === head.y)) {
this.gameOver();
return;
}
// 移动蛇
this.snake.unshift(head);
// 检查是否吃到食物
if (head.x === this.food.x && head.y === this.food.y) {
this.score += 10;
this.scoreElement.textContent = this.score;
this.food = this.generateFood();
} else {
this.snake.pop();
}
}
draw() {
// 清空画布
this.ctx.fillStyle = '#000';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// 绘制蛇
this.ctx.fillStyle = '#0f0';
this.snake.forEach(segment => {
this.ctx.fillRect(
segment.x * this.gridSize,
segment.y * this.gridSize,
this.gridSize - 2,
this.gridSize - 2
);
});
// 绘制食物
this.ctx.fillStyle = '#f00';
this.ctx.fillRect(
this.food.x * this.gridSize,
this.food.y * this.gridSize,
this.gridSize - 2,
this.gridSize - 2
);
// 游戏结束显示
if (this.isGameOver) {
this.ctx.fillStyle = '#fff';
this.ctx.font = '48px Arial';
this.ctx.textAlign = 'center';
this.ctx.fillText('游戏结束!', this.canvas.width/2, this.canvas.height/2);
}
}
startGame() {
if (this.gameLoop) return;
this.direction = {x: 1, y: 0};
this.gameLoop = setInterval(() => {
this.update();
this.draw();
}, 100);
this.startBtn.disabled = true;
this.pauseBtn.disabled = false;
this.restartBtn.disabled = false;
}
togglePause() {
this.isPaused = !this.isPaused;
this.pauseBtn.textContent = this.isPaused ? '继续' : '暂停';
}
gameOver() {
this.isGameOver = true;
clearInterval(this.gameLoop);
this.gameLoop = null;
this.startBtn.disabled = true;
this.pauseBtn.disabled = true;
this.restartBtn.disabled = false;
}
restartGame() {
clearInterval(this.gameLoop);
this.snake = [{x: 15, y: 15}];
this.direction = {x: 0, y: 0};
this.food = this.generateFood();
this.score = 0;
this.scoreElement.textContent = '0';
this.gameLoop = null;
this.isPaused = false;
this.isGameOver = false;
this.pauseBtn.textContent = '暂停';
this.startBtn.disabled = false;
this.pauseBtn.disabled = true;
this.restartBtn.disabled = true;
this.draw();
}
toggleGestureControl() {
if (this.gestureBtn.classList.contains('active')) {
this.gestureController.stop();
this.gestureBtn.classList.remove('active');
this.gestureBtn.textContent = '启用手势控制';
} else {
this.gestureController.start();
this.gestureBtn.classList.add('active');
this.gestureBtn.textContent = '关闭手势控制';
}
}
changeDirection(newDirection) {
// 防止直接反向移动
if ((newDirection.x * -1 !== this.direction.x) || (newDirection.y * -1 !== this.direction.y)) {
this.direction = newDirection;
}
}
}
// 初始化游戏
window.onload = () => {
new SnakeGame();
};

+ 172
- 0
src/static/js/gesture_controller.js View File

@ -0,0 +1,172 @@
class GestureController {
constructor(game) {
this.game = game;
this.video = null;
this.canvas = null;
this.ctx = null;
this.isEnabled = false;
this.lastGesture = null;
this.lastCommandTime = 0;
this.commandCooldown = 100; // 命令冷却时间(毫秒)
this.setupVideoElement();
this.setupGestureCanvas();
}
setupVideoElement() {
// 创建视频元素
this.video = document.createElement('video');
this.video.setAttribute('playsinline', '');
this.video.style.transform = 'scaleX(-1)'; // 镜像显示
this.video.style.display = 'none';
document.body.appendChild(this.video);
}
setupGestureCanvas() {
// 创建手势显示画布
this.canvas = document.createElement('canvas');
this.canvas.width = 320;
this.canvas.height = 240;
this.canvas.style.position = 'fixed';
this.canvas.style.right = '20px';
this.canvas.style.bottom = '20px';
this.canvas.style.border = '2px solid #333';
this.canvas.style.borderRadius = '8px';
this.canvas.style.display = 'none';
this.ctx = this.canvas.getContext('2d');
// 添加手势提示文本
this.canvas.style.position = 'relative';
const gestureHint = document.createElement('div');
gestureHint.style.position = 'absolute';
gestureHint.style.bottom = '260px';
gestureHint.style.right = '20px';
gestureHint.style.backgroundColor = 'rgba(0,0,0,0.7)';
gestureHint.style.color = 'white';
gestureHint.style.padding = '10px';
gestureHint.style.borderRadius = '5px';
gestureHint.style.fontSize = '14px';
gestureHint.innerHTML = '手势提示:<br>• 食指指向控制方向<br>• 手掌完全张开切换暂停/继续';
document.body.appendChild(gestureHint);
this.gestureHint = gestureHint;
document.body.appendChild(this.canvas);
}
async start() {
if (this.isEnabled) return;
try {
// 获取摄像头权限
const stream = await navigator.mediaDevices.getUserMedia({
video: {
width: 320,
height: 240,
facingMode: 'user'
}
});
this.video.srcObject = stream;
await this.video.play();
this.isEnabled = true;
this.canvas.style.display = 'block';
this.gestureHint.style.display = 'block';
// 开始手势检测循环
this.detectGestures();
} catch (error) {
console.error('无法访问摄像头:', error);
alert('无法访问摄像头,请确保已授予摄像头权限。');
}
}
stop() {
if (!this.isEnabled) return;
// 停止视频流
const stream = this.video.srcObject;
if (stream) {
stream.getTracks().forEach(track => track.stop());
}
this.video.srcObject = null;
this.canvas.style.display = 'none';
this.gestureHint.style.display = 'none';
this.isEnabled = false;
this.lastGesture = null;
}
async detectGestures() {
if (!this.isEnabled) return;
// 在画布上绘制视频帧
this.ctx.drawImage(this.video, 0, 0, this.canvas.width, this.canvas.height);
// 发送帧数据到服务器进行手势识别
try {
const imageData = this.canvas.toDataURL('image/jpeg', 0.5);
const response = await fetch('/detect_gesture', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ image: imageData })
});
const result = await response.json();
if (result.processed_image) {
// 显示处理后的图像(包含手部关键点标记)
const img = new Image();
img.onload = () => {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.drawImage(img, 0, 0, this.canvas.width, this.canvas.height);
};
img.src = result.processed_image;
}
this.handleGestureResult(result);
} catch (error) {
console.error('手势识别错误:', error);
}
// 继续下一帧检测
requestAnimationFrame(() => this.detectGestures());
}
handleGestureResult(result) {
if (!result.gesture) return;
const currentTime = Date.now();
if (currentTime - this.lastCommandTime < this.commandCooldown) {
return; // 如果在冷却时间内,忽略新的命令
}
const gesture = result.gesture;
if (gesture !== this.lastGesture) {
this.lastGesture = gesture;
switch (gesture) {
case 'UP':
this.game.changeDirection({x: 0, y: -1});
break;
case 'DOWN':
this.game.changeDirection({x: 0, y: 1});
break;
case 'LEFT':
this.game.changeDirection({x: -1, y: 0});
break;
case 'RIGHT':
this.game.changeDirection({x: 1, y: 0});
break;
case 'TOGGLE_PAUSE':
this.game.togglePause();
break;
}
this.lastCommandTime = currentTime;
}
}
}

+ 85
- 0
src/templates/index.html View File

@ -0,0 +1,85 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>贪吃蛇游戏</title>
<style>
body {
display: flex;
flex-direction: column;
align-items: center;
background-color: #f0f0f0;
font-family: Arial, sans-serif;
}
#gameContainer {
margin-top: 20px;
position: relative;
}
#gameCanvas {
border: 2px solid #333;
background-color: #000;
}
#scoreBoard {
font-size: 24px;
margin: 10px 0;
color: #333;
}
#controls {
margin: 20px 0;
padding: 10px;
background-color: #fff;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.button {
padding: 10px 20px;
margin: 0 5px;
border: none;
border-radius: 5px;
background-color: #4CAF50;
color: white;
cursor: pointer;
transition: background-color 0.3s;
}
.button:hover {
background-color: #45a049;
}
.button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
#gestureBtn {
background-color: #2196F3;
}
#gestureBtn:hover {
background-color: #1976D2;
}
#gestureBtn.active {
background-color: #f44336;
}
.control-group {
margin: 10px 0;
}
</style>
</head>
<body>
<h1>贪吃蛇游戏</h1>
<div id="scoreBoard">分数: <span id="score">0</span></div>
<div id="gameContainer">
<canvas id="gameCanvas" width="800" height="600"></canvas>
</div>
<div id="controls">
<div class="control-group">
<button id="startBtn" class="button">开始游戏</button>
<button id="pauseBtn" class="button" disabled>暂停</button>
<button id="restartBtn" class="button" disabled>重新开始</button>
</div>
<div class="control-group">
<button id="gestureBtn" class="button">启用手势控制</button>
</div>
</div>
<script src="/static/js/gesture_controller.js"></script>
<script src="/static/js/game.js"></script>
</body>
</html>

+ 0
- 306
项目测试文档.md View File

@ -1,306 +0,0 @@
# 📋 WaveControl 隔空手势控制系统 - 测试文档
## 一、测试概述
### 1.1 测试目标
确保系统三个核心子模块功能完整、交互稳定、性能可靠,满足多场景下的非接触式人机交互需求,包括主控制平台、手语通平台、游戏控制模块。
### 1.2 测试对象
- 主控制系统(控制面板 + 手势库管理 + 语音识别)
- WaveSign 手语通系统(教学、评分、社区)
- 虚拟赛车手柄系统(游戏中控制响应)
### 1.3 测试类型
| 测试类型 | 说明 |
| ------------ | ------------------------------------------ |
| 功能测试 | 各模块是否能完成核心功能 |
| 集成测试 | 各子模块之间的数据流与交互是否正确 |
| 性能测试 | 系统是否在高帧率下稳定运行、响应及时 |
| 边界测试 | 识别模糊手势、断网、摄像头断连等异常情况 |
| 用户体验测试 | 普通用户是否能流畅使用、易上手、有清晰反馈 |
## 二、测试环境
| 项目 | 配置 |
| ----------- | --------------------------------- |
| 操作系统 | Windows 10 / 11、MacOS |
| Python 版本 | Python 3.8+ |
| 浏览器 | Chrome / Edge |
| 识别设备 | USB 外接摄像头 / 笔记本自带摄像头 |
| 游戏平台 | Steam 平台《Rush Rally Origins》 |
## 三、测试用例设计
### ✅ 主控制系统
| 序号 | 手势名称 | 手势动作说明 | 所属类型 |
| ---- | ------------ | --------------------------------------------- | -------- |
| 01 | 光标控制 | 竖起食指滑动控制光标位置 | 通用控制 |
| 02 | 鼠标左键点击 | 食指 + 大拇指上举执行点击 | 通用控制 |
| 03 | 滚动控制 | okay 手势(食指+拇指捏合),上下移动滚动页面 | 通用控制 |
| 04 | 全屏控制 | 四指并拢向上 → 触发设定键(默认 f 键) | 通用控制 |
| 05 | 退格 | 特定手势触发退格键 | 通用控制 |
| 06 | 开始语音识别 | 六指手势触发语音识别启动 | 通用控制 |
| 07 | 结束语音识别 | 拳头手势触发语音识别停止 | 通用控制 |
| 08 | 暂停/继续 | 单手张开保持 1.5 秒触发暂停/继续识别 | 通用控制 |
| 09 | 向右移动 | 拇指上抬,其余手指收回 → 控制游戏角色向右移动 | 游戏控制 |
| 10 | 跳跃 | 食指、中指上举 → 控制跳跃动作 | 游戏控制 |
| 11 | 右跳跃 | 拇指 + 食指 + 中指上举 → 控制右跳跃 | 游戏控制 |
| 12 | 上一首 | 大拇指向左摆动 → 上一首音乐 | 音乐控制 |
| 13 | 下一首 | 大拇指向右摆动 → 下一首音乐 | 音乐控制 |
| 14 | 暂停/播放 | 比耶手势(✌️ ) → 暂停或播放音乐 | 音乐控制 |
| 15 | 切换音乐模式 | rock 手势(🤘)→ 切换音乐/普通控制模式 | 模式切换 |
### 🤟 手语通 WaveSign
#### ✅ 1. **SLClassroom(手语教室)模块**
| 用例编号 | 用例名称 | 测试点 | 预期结果 |
| -------- | ------------------ | --------------------------- | --------------------------- |
| TC-SL-01 | 摄像头实时识别 | 摄像头接通后手势是否被识别 | 返回手语内容 + 实时评分动画 |
| TC-SL-02 | 视频上传评分 | 上传手语视频后是否正常评分 | 返回分数、标准建议 |
| TC-SL-03 | 视频课程学习流程 | 是否能顺序播放、标记已学 | 视频播放正常,课程解锁 |
| TC-SL-04 | 翻转卡片练习 | 卡片是否翻转 + 显示正确答案 | 点击翻面后显示预设解释 |
| TC-SL-05 | 任务式课程地图跳转 | 点击课程节点是否正确跳转 | 跳转至对应课程页 |
#### ✅ 2. **Community(社区系统)模块**
| 用例编号 | 用例名称 | 测试点 | 预期结果 |
| --------- | ------------- | -------------------------- | -------------------------- |
| TC-COM-01 | 发帖功能 | 输入文字/图片/视频发帖 | 帖子成功展示 + ID 唯一标识 |
| TC-COM-02 | 评论功能 | 帖子下评论 + 删除 | 评论正常显示/删除 |
| TC-COM-03 | 点赞机制 | 点赞后数值变化 | 点赞数+1,重复点则取消 |
| TC-COM-04 | 热门话题显示 | 帖子互动数高时是否上热门区 | 热门区出现帖子 |
| TC-COM-05 | 内容审核机制 | 敏感词是否被拦截/提示 | 给出“内容不合规”提示 |
| TC-COM-06 | 标签推荐 | 选择话题是否推荐相关内容 | 推荐结果合理、及时加载 |
| TC-COM-07 | 关注/取关系统 | 关注后是否成功建立关注关系 | 动态展示更新 |
#### ✅ 3. **Schedule(日程与任务模块)**
| 用例编号 | 用例名称 | 测试点 | 预期结果 |
| --------- | -------------------- | -------------------------- | ------------------------- |
| TC-SCH-01 | 添加任务清单 | 是否可设置日期/优先级 | 列表显示任务 + 状态可勾选 |
| TC-SCH-02 | 事件提醒触发 | 设置提醒是否能按时通知 | 到时弹出提醒/响铃提示 |
| TC-SCH-03 | 日/周/月视图切换 | 是否能无误切换不同日历视图 | 各视图正常显示 |
| TC-SCH-04 | 删除任务是否更新视图 | 删除后是否同步更新 | 日历图/列表同步清除 |
#### ✅ 4. **LifeServing(生活服务)模块**
| 用例编号 | 用例名称 | 测试点 | 预期结果 |
| -------- | ---------------- | ---------------------------------- | ---------------------------- |
| TC-LS-01 | 发布内容管理 | 发布好物推荐/活动等信息 | 内容展示无误 |
| TC-LS-02 | 辅助器具推荐 | 推荐列表是否分类清晰/加载正确 | 分类显示 + 图片正常 |
| TC-LS-03 | 学习设备推荐 | 展示硬件学习工具列表 | 内容图文加载完整 |
| TC-LS-04 | 就业信息推送 | 职位内容、公司信息展示是否完整 | 包含岗位名称、描述、联系方式 |
| TC-LS-05 | 残障友好企业标识 | 是否加V / 标签区分 | 有“友好企业”提示 |
| TC-LS-06 | 无障碍路线规划 | 是否能绘制无台阶/电梯路线 | 地图路径合理 |
| TC-LS-07 | 实时公交提醒 | 路线是否加载正确/更新是否及时 | 实时展示公交进站时间 |
| TC-LS-08 | 活动预告展示 | 亲子/文娱/演出分类信息加载是否完整 | 可报名 + 有时间地点说明 |
#### ✅ 5. **MyPage(个人中心)模块**
| 用例编号 | 用例名称 | 测试点 | 预期结果 |
| -------- | ------------------ | ------------------------------- | ---------------------------- |
| TC-MY-01 | 修改头像 | 上传头像 + 预览功能是否生效 | 显示新头像 |
| TC-MY-02 | 修改资料 | 昵称/简介/联系方式可修改 | 保存后页面同步更新 |
| TC-MY-03 | 收藏管理 | 收藏帖子/课程后是否能查看 | 我的收藏页显示内容 |
| TC-MY-04 | 账号注册/登录/登出 | 是否可注册新用户 + 正确跳转状态 | 新用户进入首页,旧账号可退出 |
| TC-MY-05 | 权限角色识别 | 是否区分普通用户/审核员角色 | 页面显示不同选项 |
#### ✅ 6. **技术实现相关(稳定性/架构)测试**
| 用例编号 | 用例名称 | 测试点 | 预期结果 |
| ---------- | --------------------- | ----------------------------- | ------------------------ |
| TC-TECH-01 | SQLite 数据读写测试 | 批量操作课程/帖子是否存取正常 | 数据不丢失,响应时间正常 |
| TC-TECH-02 | MediaPipe模型崩溃恢复 | 强行关闭摄像头后是否重连 | 显示重连提示/自动恢复 |
| TC-TECH-03 | Tailwind 前端样式响应 | 各分辨率下页面是否响应式变化 | 不溢出,元素自适应 |
### 🏎️ 游戏控制模块
| 用例编号 | 模块 | 用例名称 | 前置条件 | 测试步骤 | 预期结果 | 优先级 |
| ---------- | -------- | ---------------- | ------------------------ | -------------------------------- | ---------------------------------- | ------ |
| TC-GAME-01 | 游戏控制 | 加速动作识别 | 摄像头运行正常,程序启动 | 举起右手拇指上扬 | 车辆开始加速 | 高 |
| TC-GAME-02 | 游戏控制 | 刹车动作识别 | 同上 | 举起左手拇指上扬 | 车辆开始减速 | 高 |
| TC-GAME-03 | 游戏控制 | 左右转向动作识别 | 同上 | 向左倾手掌 | 车辆向左转弯 | 高 |
| TC-GAME-04 | 游戏控制 | 手势连续识别切换 | 程序运行中 | 快速从加速 → 左转 → 刹车切换手势 | 每步操作都有反馈,游戏响应流畅 | 高 |
| TC-GAME-05 | 游戏控制 | 识别抖动干扰测试 | 程序运行中,抖动手指 | 快速小幅度摆动手指 | 系统不误触操作,保持稳定 | 中 |
| TC-GAME-06 | 游戏控制 | 虚拟手柄断连恢复 | 手柄模拟开启中 | 断开 vgamepad 端口 → 重连 | 程序检测中断并尝试自动恢复 | 中 |
| TC-GAME-07 | 游戏控制 | 低帧率下性能表现 | 模拟20fps摄像头 | 尝试完成左右转、加速等动作 | 出现识别滞后,界面给出低帧警告 | 中 |
| TC-GAME-08 | 游戏控制 | UI反馈准确性 | 摄像头运行中 | 做出加速动作,观察 UI 面板反馈 | 显示“当前动作:加速”,图像区域标亮 | 中 |
## 四、测试程序实现与技术支撑
本项目测试除使用主系统 UI 操作外,亦开发了两套独立测试程序用于识别稳定性、输入准确性与边界场景的验证,覆盖核心逻辑路径,支撑高频回归测试与离线分析。
### 4.1 手势识别测试程序(wavecontrol-test 模块)
项目测试脚本集中存放于 `wavecontrol-test/src/` (gesture分支)路径下,采用 TypeScript + Vue3 框架实现,通过 MediaPipe 实时检测与手势逻辑模块协同,实现系统功能验证。
#### 4.1.1 hand_landmark 模块
- **detector.ts**
核心手部关键点检测模块,封装对 MediaPipe 的调用逻辑,统一输出手部21个关键点的坐标、置信度等数据。
- 功能点:初始化摄像头流、绑定回调、封装模型参数。
- 用于:为 `VideoDetector.vue``gesture_handler.ts` 提供关键点数据源。
- **gesture_handler.ts**
手势解析与事件派发模块,将 landmark 数据解析为具体手势动作(如光标控制、点击、跳跃等)。
- 支持自定义手势库扩展。
- 与游戏控制模块或系统控制指令绑定。
- **VideoDetector.vue**
Vue 组件封装,展示摄像头实时画面 + 可视化 landmark 点位(调试模式用)。
- 提供测试 UI 面板,便于调试每个手势识别过程。
- 集成 FPS 状态、实时识别手势结果反馈。
#### 4.1.2 独立运行说明与调试提示
- **模块定位:**
`wavecontrol-test` 为独立测试工程,当前未集成至主项目的 UI 页面路由体系,主要用于**手势识别逻辑的单元测试与调试验证**。
- **运行状态说明:**
启动后页面加载为空白(白屏),属**正常表现**,原因如下:
- 项目尚未渲染任何主界面组件;
- 测试逻辑运行主要依赖控制台输出来验证关键逻辑(如 landmark 检测、手势判断等)。
- **调试建议:**
启动后打开浏览器开发者工具(快捷键 `F12``Ctrl+Shift+I`),查看:
- 控制台(Console):打印识别结果、错误日志、手势状态;
- 网络(Network):确认模型文件是否成功加载;
- 元素(Elements):手动挂载 `VideoDetector.vue`注入测试组件进行临时渲染调试。
### 4.2 游戏控制模块测试(`test.py` 等)
### 4.2.1 模块定位与结构
该模块为游戏控制核心动作识别的测试环境,主要用于模拟真实场景下用户的手势输入,评估系统能否准确识别特定动作(如加速、转弯、刹车等),并通过 OpenCV 实时可视化手势状态与角度变化。
- **所在目录:** `wavecontrol/test_py/`(`game_control` 分支)
- **相关脚本:**
- `test.py`:主测试入口,执行实时手势识别与状态展示。
- `gesture_detector.py`:封装具体的手势识别逻辑。
- `utils.py`:通用工具类,如角度计算、数据格式处理等。
- `test_xbox.py`:拟用于连接虚拟 Xbox 控制器,模拟游戏输入。
- `test_gui.py`:图形界面测试入口。
### 4.2.2 核心测试逻辑(基于 `test.py`
**功能点解析:**
- **手势状态追踪:**
通过 `gesture_status` 字典记录五类状态:
- `Left Fist`、`Right Fist`:用于映射刹车/加速等动作
- `Left Thumb`、`Right Thumb`:映射转向或音量等功能
- `Angle`:表示当前手势的偏转角度,用于模拟方向盘行为或特殊动作
- **视觉反馈:**
使用 `OpenCV` (`cv2`) 实时将识别结果叠加在摄像头图像上,显示五种状态的当前值,方便快速人工验证是否识别准确。
- **模块化调用:**
`GestureDetector` 实例通过 `gesture_detector.py` 导入,使得识别逻辑可独立测试、方便集成到主游戏模块。
------
#### 4.2.3 使用说明
- **启动方法:**
```
python test.py
```
**运行效果:**
- 打开摄像头窗口;
- 实时显示识别到的五项手势状态;
- 开发者可通过手部动作验证识别准确性。
- **测试目标:**
- 验证关键手势在自然使用状态下的识别准确率;
- 评估系统在不同光照/距离/角度条件下对“加速”“刹车”“左右转”的响应稳定性;
- 检查 OpenCV 图像反馈是否与真实操作一致,便于回归对比。
## 五、测试结果分析
#### 项目整体手动测试结果:
[⌛️项目测试结果](./项目测试结果.xlsx)
#### 主平台自动化测试结果:
全部测试通过,覆盖率达79%
![主平台自动化测试结果](./img/主平台自动化测试结果.png)
#### WaveSign平台自动化测试结果:
全部测试通过,覆盖率达93%
具体测试设计和结果见以下链接:
[WaveSign测试文档](https://gitee.com/wydhhh/software-engineering/blob/wavesign/WaveSign%E6%B5%8B%E8%AF%95%E6%96%87%E6%A1%A3.md)
[WaveSign测试结果html报告](https://gitee.com/wydhhh/software-engineering/tree/wavesign/htmlcov)
### **5.1 测试通过率**
- **总体通过率:**
在所有模块(主控制系统 + WaveSign + 游戏控制模块)共计 **50+ 条用例 × 10次测试 = 500+ 次执行记录**中,约 **88%** 的测试结果为“通过”,**12%** 的测试记录显示“未通过”。
- **通过率较高的模块:**
- **主控制系统:**通过率 **>90%**,大部分基础手势(如光标控制、鼠标点击、滚动控制)表现稳定。
- **WaveSign 社区与个人中心模块:**通过率 **约95%**,发帖、评论、关注、资料修改等常规操作无明显问题。
- **通过率较低的模块:**
- **WaveSign SLClassroom(手语教室):**实时识别和视频上传评分测试中偶尔出现 **识别延迟****评分不稳定**,通过率约 **80%**
- **游戏控制模块:**“低帧率下性能表现”与“识别抖动干扰测试”未通过率稍高(**20% 左右**),主要原因是低帧率摄像头模拟下识别滞后明显。
------
### **5.2 典型问题分析**
- **(1)识别类问题:**
- 当背景复杂或光照不足时,MediaPipe 模型识别准确率下降,个别测试记录显示 **响应时间超过0.7s**,甚至未正确识别手势。
- “六指手势启动语音识别”在部分测试中未触发,需要优化手势阈值。
- **(2)性能问题:**
- 游戏模块在模拟 **20fps 低帧率**摄像头时,出现识别滞后,UI延迟反馈。
- 部分 UI 动画在高分辨率(4K)设备上存在轻微卡顿,需要进一步优化前端渲染。
- **(3)社区系统:**
- 敏感词拦截逻辑偶尔误报,如普通词语被识别为敏感词。
- **(4)任务提醒功能:**
- **日/周/月视图切换**测试中发现,在多任务快速切换时,少数场景下界面刷新不完全。
------
### **5.3 综合结论**
- WaveControl 系统整体功能 **满足预期目标**,大部分核心功能已稳定实现。

BIN
项目测试结果.xlsx View File


Loading…
Cancel
Save