Featured image of post MediaPipe Hands 与 “火影结印” 实战解析

MediaPipe Hands 与 “火影结印” 实战解析

深入剖析使用 MediaPipe Hands 实现 Web 端高保真手势识别。本文以“火影忍者结印”项目为例,详细讲解手势数据的预处理、空间归一化算法以及前端状态机逻辑的实现,助你掌握 Web AI 交互开发的核心技巧。

MediaPipe Hands 与 “火影结印” 实战解析

组件库介绍

MediaPipe Hands 是 Google 开源的一个高保真手部追踪解决方案。它利用机器学习(ML)从单帧图像中推断出每只手的 21 个 3D 关键点(Landmarks)。不同于传统的物体检测,它不需要依赖昂贵的深度摄像头,仅凭普通的 RGB 摄像头就能在移动设备和 Web 浏览器上实现实时的手势捕捉。其底层的轻量级模型架构经过高度优化,能够以毫秒级的延迟输出手掌、手指关节的精确坐标,是目前 Web 端最主流的手势识别方案之一。

使用场景

MediaPipe Hands 的应用场景非常广泛,主要包括:

  • AR/VR 交互:在增强现实或虚拟现实中,用手直接抓取、控制虚拟物体,提供沉浸式体验。
  • 手语翻译:实时捕捉手语动作并翻译成文本或语音,辅助听障人士沟通。
  • 非接触式控制:智能家居或车载系统的手势控制(如挥手切歌、捏合调节音量)。
  • 创意互动游戏:如我们开发的“火影结印”或“体感钓鱼”,利用手势作为游戏的核心输入控制器。
  • 教学与康复:用于钢琴教学的手指指法纠正,或手部康复训练的动作监测。

简单交互演示

https://demo.liurb.org/media_hand_01/index.html

模拟火影忍者结印教学

先看看效果

https://heresmy.app/playground/mediaPipe-hands/naruto-handsign/index.html

本项目利用 MediaPipe Hands 实现了一个趣味演示:识别用户是否完成了特定的“忍术结印”动作。

项目背景:结印与十二地支

在《火影忍者》设定中,施放忍术需要通过特定的手势组合来引导查克拉。这些手势原型源自中国的“十二地支”。本质上,这是一个静态手势序列匹配的问题。系统需要按顺序识别出用户是否做出了正确的手势。

常见的手势对照如下:

地支英文名对应动作描述
Rat食指中指伸直,其余手指弯曲
Ox手指交叉,类似握拳
Tiger双手食指中指竖起并在胸前交叉
Hare小指伸出,拇指并拢
Dragon复杂的指关节扣合
Snake双手合十,指尖向上
Horse双手食指相抵,呈三角形
Ram食指中指指尖向上
Monkey手掌平放,手指弯曲
Bird指尖相对,形似鸟嘴
Dog右手握拳放在左手掌心
Boar双手并在下方,指尖向下

对照图

为什么需要数据预处理?

大家可能会尝试通过编写硬编码规则来识别手势,例如:“如果食指指尖的 Y 坐标小于关节 Y 坐标,且拇指打开角度大于 30 度,这就是【未】"

这种基于规则(Rule-based)的方法有极大的局限性:

  1. 代码极其臃肿:每个人手大小、拍摄角度、距离镜头远近不同,需要写无数个 if-else 来覆盖边界情况。
  2. 难以维护:一旦需要添加新动作,或者调整判定标准,整个逻辑很容易崩塌。
  3. 鲁棒性差:手稍微旋转一下,简单的坐标判断就会失效。

基于是一个入门级的程度,所以先不采用训练复杂的神经网络模型,而是采用一种叫做 “单样本学习 (One-Shot Learning)” 的思路。简单来说,就是把静态的照片变成数据,然后拿用户的实时数据跟这张照片的数据去“比距离”。

预处理的核心步骤如下:

  1. 数据清洗:筛选出清晰的 12 种手势图片。
  2. 特征提取:使用 Python 版的 MediaPipe 库处理图片,获取 21 个关键点坐标。
  3. 空间归一化 (关键步骤)
    • 以腕部为中心:将所有点的坐标减去腕部 (Wrist) 的坐标,消除手在画面位置的影响。
    • 尺度归一化:计算手掌最大距离,将所有坐标除以该距离,消除手掌大小(离镜头远近)的影响。
  4. 序列化:将归一化后的 21 个点 (x, y) 保存为 JSON 数组。

第一步:数据清洗

我这边使用的是在 kaggle 上面一位印度小哥的数据集,里面有十二个手部姿势,文件夹里面有很多视频帧图片,我们只需要每个姿势里面挑一张最清晰准确的。

印度小哥的数据集
https://www.kaggle.com/datasets/vikranthkanumuru/naruto-hand-sign-dataset/data

经过我的测试,使用动漫里面的手部动作是不能识别的,必须要用真人的。如果最后感觉不理想的话,可以通过拍摄自己的手部方式来调教效果。

第二步:安装环境

在我安装 python 环境的时候遇到很多坑,就是关于 mediapipe 依赖库的版本问题,所以必须严格按照我这边的版本来。

首先,python 使用 3.10.x 版本,以下我是通过 uv 工具进行环境的安装,如果你是通过 pip 也是类似的。

1
2
3
4
5
6
7
8
# 安装 python3.10
uv venv --python python3.10 .venv_py310

# 激活环境
.\.venv_py310\Scripts\activate

# 安装依赖库
uv pip install "mediapipe==0.10.9" "numpy<2" "protobuf==3.20.3" opencv-python

第三步:生成 json 文件

我们准备好了素材,就是第一步中整理的 12 张图片,然后通过 python 脚本,将这些图片依次喂给 MediaPipe Hands

我们还需要做数据标准化,这是很关键的步骤。MediaPipe 返回的坐标是绝对位置,直接对比没法用(因为用户手的大小、在屏幕的位置和你的照片不一样)。你需要对照片产生的坐标做归一化处理:

  • 平移:将手腕点(索引 0)作为原点 (0,0,0)。即:所有点的坐标减去手腕点的坐标。
  • 缩放:计算手掌的大小(例如手腕到中指指尖的距离),让所有点的坐标除以这个距离。这样无论手大如篮球还是小如硬币,数值范围都在 0~1 之间。

相关的代码、图片和 json 文件,可以通过关注公众号,回复【hand】获取

运行代码后,会输出如下的 JSON 格式内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
{
  "snake": [
    {
      "label": "Right",
      "landmarks": [
        { "x": 0, "y": 0 },           // 手腕永远是 0,0
        { "x": -0.123, "y": -0.456 }, // 拇指根部
        ...                           // 一共21个点
      ]
    },
    {
      "label": "Left",                // 如果是双手结印,会有第二个数组
      "landmarks": [ ... ]
    }
  ],
  "ram": [ ... ]
}

如果提供的图片无法识别,控制台会有输出相关的提示,到时候就替换那张图即可。

前端使用特征文件

在 Web 前端,我们加载这个 JSON 文件,并在每一帧进行实时的匹配计算。

核心逻辑代码解析 (script.js):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// A. 加载预处理的数据
let signData = null;
async function init() {
    const res = await fetch("./hand_signs.json");
    signData = await res.json();
}

// B. 运行时核心算法:也是做同样的归一化处理
function getNormalizedLandmarks(landmarks) {
    const wrist = landmarks[0];
    let maxDist = 0;

    // 1. 相对坐标:以腕部为原点
    const centered = landmarks.map((lm) => ({
        x: lm.x - wrist.x,
        y: lm.y - wrist.y,
    }));

    // 2. 计算缩放比例 (最大距离)
    centered.forEach((p) => {
        const dist = Math.sqrt(p.x * p.x + p.y * p.y);
        if (dist > maxDist) maxDist = dist;
    });

    // 3. 归一化
    return centered.map((p) => ({
        x: p.x / maxDist,
        y: p.y / maxDist,
    }));
}

// C. 相似度计算 (欧氏距离)
function calculateDistance(userHand, targetHand) {
    let totalDist = 0;
    for (let i = 0; i < userHand.length; i++) {
        const dx = userHand[i].x - targetHand[i].x;
        const dy = userHand[i].y - targetHand[i].y;
        totalDist += Math.sqrt(dx * dx + dy * dy);
    }
    return totalDist / userHand.length; // 返回平均误差
}

// D. 判定逻辑
if (minError < 0.3) {
    // 匹配成功!
    console.log("Jutsu Activated!");
}

通过这种方式,我们只需要维护一份 json 数据文件,代码逻辑非常通用且简洁。无论添加多少种新手势,核心算法都不需要修改。

实例解析:豪火球之术 (Fireball Jutsu)

为了更直观地理解,我们先整理并定义了“豪火球之术”的结印序列:

1
2
3
4
5
{
    "id": "fireball",
    "name": "火遁 · 豪火球之术",
    "sequence": ["snake", "ram", "monkey", "boar", "horse", "tiger"]
}

其处理流程是一个典型的状态机 (State Machine)

  1. 初始状态currentStep = 0,系统期待用户结出第一个印 “巳 (Snake)”
  2. 匹配检测
    • 每一帧,MediaPipe 返回当前手势的关键点。
    • 系统将其与 snake 的标准特征进行比对。
    • 如果误差小于阈值(例如 0.3),且不在冷却期内,判定为匹配成功。
  3. 状态流转
    • 触发 nextStep() 函数。
    • currentStep 加 1,变为 1。
    • 系统提示用户结下一个印 “未 (Ram)”
    • 同时记录 lastSuccessTime 防止瞬间重复触发(防抖)。
  4. 循环与完成
    • 用户依次完成 monkey -> boar -> horse
    • 当最后一个印 “寅 (Tiger)” 匹配成功后,currentStep 等于序列长度。
    • 触发 completeMove(),播放成功特效(如屏幕出现巨大的火球动画)。

通过这种设计,我们将复杂的动态交互拆解为了一系列简单的静态匹配任务,既保证了识别的准确率,又大大降低了开发难度。

钓鱼游戏

作为钓鱼佬的最爱,那肯定是钓鱼了,于是也大概弄了一个 demo

https://heresmy.app/playground/mediaPipe-hands/fishing-game/index.html

本博客所有内容无特殊标注均为大卷学长原创内容,复制请保留原文出处。
Built with Hugo
Theme Stack designed by Jimmy