news 2026/9/15 0:20:48

MediaPipe手势识别实战:从21个关键点到数字与石头剪刀布判定

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MediaPipe手势识别实战:从21个关键点到数字与石头剪刀布判定

简介:一套基于MediaPipe的手势识别源码,聚焦数字手势与石头剪刀布等常用手势分类,面向计算机视觉初学者、Python开发者以及快速构建手势交互原型的爱好者。MediaPipe是谷歌开源的跨平台视觉框架,可实时输出手部关键点,该源码围绕其展开,展示了从图像输入到分类结果的完整代码路径。压缩包体积仅3KB,包含2个Python文件,代码紧凑、无明显冗余依赖,运行需要MediaPipe与OpenCV环境,适合直接阅读、调试和复用。目前已有272人学习下载,常被用作手势识别课程设计与入门练习。资源内两个主脚本分别承担数字识别与石头剪刀布判定,完整覆盖关键点检测、特征构造、分类输出等核心环节;读者可在此基础上扩展自定义手势库,也可接入摄像头实现实时识别,进一步应用于手势控制、无接触交互、智能演示等场景,是轻量级手势识别项目的基础参考。

1. 为什么手势识别都绕不开MediaPipe:从21个关键点到分类

摄像头前伸出手掌,屏幕上即时出现数字“2”或者“剪刀”,这种体验在过去要么依赖昂贵的深度相机,要么需要自己用OpenCV写边缘检测加上一堆形态学处理,稍换背景就失灵。基于MediaPipe的手势识别方案把这条链路压缩到了一个Python脚本里:先由MediaPipe Hands模型输出每只手21个关键点的3D坐标,再根据这些坐标计算指尖距离、关节夹角、弯曲程度等几何特征,最后映射成数字或石头剪刀布。这个源码包包含HandRec_main.py和HandRec2_main.py两个主程序,前者处理0到9数字识别,后者处理石头剪刀布,适合想要快速在本地摄像头环境下复现手势识别的Python开发者,也适合做课程设计与Demo演示。你不需要训练任何神经网络,能跑通OpenCV就能把整个流程拆开看明白。

2. 关键点怎么变成数字:HandRec_main.py的几何特征与判定

2.1 为什么选21个关键点而不是整张手图

MediaPipe Hands的核心贡献是把手部姿态估计从目标检测中解耦出来。整个管线分为两步:第一步用BlazePalm检测手掌区域,第二步在手掌区域内回归出21个手部关键点。这种做法保证了两件事:一是手部旋转、缩放之后依然能稳定输出关键点;二是关键点坐标是归一化的,直接除以图像宽高,因此不同分辨率下的输入可以得到一致的坐标范围。HandRec_main.py正是基于这21个点的相对位置关系完成数字识别,而不是把整张手图喂给分类器。

21个关键点的索引是固定的,0是腕关节,1到4是拇指,5到8是食指,9到12是中指,13到16是无名指,17到20是小指。坐标包含x、y、z三个维度,其中z表示关键点相对于腕关节的深度,数值越大离摄像头越近。HandRec_main.py里常用的是x、y坐标计算平面距离和夹角,z坐标在判断手指是否朝向摄像头时才会用到。

2.1.1 手部关键点索引表
关键点索引对应部位在手势识别中的作用
0腕关节全手基准点,距离计算的起点
4拇指指尖判断拇指是否张开或弯曲
8食指指尖数字1、2、3等手指计数核心
12中指指尖数字3、4以及石头剪刀布中的“剪刀”
16无名指指尖数字4以及“布”的判定
20小指指尖数字5以及“布”的判定
5,9,13,17四指根部手指张开的参考锚点

2.2 数字识别里的核心特征:指尖到腕关节的距离比

HandRec_main.py中最基础的特征是“指尖到腕关节的距离”。单独看这个距离没有意义,因为人手大小不同、手离摄像头远近不同,绝对像素距离变化极大。源码采用的做法是归一化:把每根手指指尖与腕关节的距离,除以中指根部(关键点9)到腕关节(关键点0)的距离。这个比值可以消除手的尺寸和摄像头距离的影响,数值越稳定,判定越可靠。

import math def distance(p1, p2): return math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2) def is_finger_extended(hand_landmarks, tip_id, pip_id): wrist = hand_landmarks.landmark[0] tip = hand_landmarks.landmark[tip_id] pip = hand_landmarks.landmark[pip_id] d_tip = distance(wrist, tip) d_pip = distance(wrist, pip) return d_tip > d_pip * 1.1

这段逻辑是常见的“合格实现”方式:对于一个自然张开的手指,指尖到腕关节的距离一定显著大于该手指第二指节(PIP)到腕关节的距离。代码里1.1这个系数是经验阈值,太大会导致手指弯曲时依然被判为伸展,太小则会把半弯的手指误判为伸直。实际运行时,我会根据摄像头距离微调这个系数,距离越近阈值越高。

HandRec_main.py对0到9的判定不是真的在做数字分类,而是先判断哪几根手指是伸直的,再按组合规则译码。数字1对应食指伸直;数字2对应食指和中指伸直;数字3是食指、中指、无名指伸直;数字4是四指伸直但拇指弯曲;数字5是五指全部张开。数字6到9的处理方式则不同,它需要借助指尖与手掌中心的相对位置:数字6是拇指和小指伸出,且小指位于手掌下方;数字7是拇指、食指、中指伸出并向一侧倾斜;数字8是拇指和食指伸出形成类似手枪的姿势;数字9是拇指和食指指尖靠近,形成圆形。

2.3 手掌方向与指尖编码的配合

只有“手指是否伸直”不够用,比如数字6到9里,好几组都包含拇指和小指的组合。HandRec_main.py在判定时引入了手掌方向向量:取腕关节0到中指根部9的方向作为手掌指向,再计算指尖到这个方向向量的投影位置。小指指尖投影在手掌方向线的下方或右侧,就可以区分数字6和数字5。下面这段代码示意了如何把关键点坐标转换为方向特征:

import math def hand_direction(landmarks): wrist = landmarks.landmark[0] middle_mcp = landmarks.landmark[9] dx = middle_mcp.x - wrist.x dy = middle_mcp.y - wrist.y norm = math.sqrt(dx * dx + dy * dy) return dx / norm, dy / norm def finger_side(landmarks, tip_id): dx, dy = hand_direction(landmarks) wrist = landmarks.landmark[0] tip = landmarks.landmark[tip_id] vec_x = tip.x - wrist.x vec_y = tip.y - wrist.y cross = dx * vec_y - dy * vec_x return cross

这里的cross本质上计算的是“指尖相对于手掌方向的叉积”。交叉值大于0表示指尖在手掌方向的左侧,小于0在右侧。数字6中伸出的小指通常位于手掌方向的右侧(负交叉值),而数字5中所有手指均匀分布在两侧,因此这个特征能稳定区分6和5。整个HandRec_main.py就是这样一个特征组合的集合:每个数字至少使用两个特征,一个描述“哪些手指伸直”,一个描述“指尖相对手掌的位置或夹角”。

2.4 中间结果可视化

调试时最怕的是11个判定条件堆在一起,不知道哪个分支误判。我一般会在脚本里临时输出一份调试信息,打印每根手指的d_tip / d_pip比值,以及指尖在手掌方向的交叉值。MediaPipe本身也提供了绘制函数:

mp.solutions.drawing_utils.draw_landmarks( image, hand_landmarks, mp.solutions.hands.HAND_CONNECTIONS)

把这一行加在cv2.imshow之前,可以实时看到关键点连线。如果输出的数字与手型不符,对比连线位置和打印的比值就能快速定位是哪个阈值出了问题,而不是盲目调参。

3. 石头剪刀布与数字共用:HandRec2_main.py的弯曲检测逻辑

3.1 三个手势的判定并不需要“分类器”

HandRec2_main.py处理石头剪刀布时用的是另一种思路:不判断手指是否与腕关节拉开距离,而是判断每个手指的“弯曲程度”。石头对应所有手指弯曲,剪刀对应食指和中指伸直、其余弯曲,布对应五指全部张开。这三个手势在几何特征上差异非常大,所以源码选择直接定义规则,而不是训练一个三分类模型。这种做法的好处是零训练成本,坏处是对手的姿态变化敏感,比如侧着摄像头时,手指弯曲与否在二维平面上容易被战败。

弯曲程度由三个关键点之间的夹角决定。以食指为例,关键点5(掌指关节)、6(近端指尖关节)、8(指尖)构成一个三角形,在指尖自然弯曲时,关键点5和8之间的距离会明显缩短。HandRec2_main.py计算的是“夹角余弦值”,因为反余弦函数acos开销较大,实时视频流中每一帧都要计算四根手指的夹角,累积起来掉帧明显。用余弦值可以直接与阈值比较,省去一次反三角函数计算。

def calc_angle(a, b, c): v1 = (a.x - b.x, a.y - b.y) v2 = (c.x - b.x, c.y - b.y) dot = v1[0] * v2[0] + v1[1] * v2[1] len1 = math.sqrt(v1[0] ** 2 + v1[1] ** 2) len2 = math.sqrt(v2[0] ** 2 + v2[1] ** 2) if len1 == 0 or len2 == 0: return 0.0 return dot / (len1 * len2)

calc_angle返回余弦值,范围在-1到1之间。手指完全伸直时,三个点近似共线,夹角接近180度,其余弦值接近-1。手指完全弯曲时,夹角接近90度,余弦值接近0。HandRec2_main.py中把“弯曲”定义为余弦值大于-0.5,把“伸直”定义为余弦值小于-0.85。这里存在一个模糊区间:-0.85到-0.5之间的手势,源代码会认为“不确定”,保持上一帧的结果保持不变。这个“滞回区间”是一个非常实用的技巧,能有效避免手势在临界状态时反复横跳。

3.2 石头剪刀布的组合规则

得到四根手指(拇指除外)的弯曲状态后,HandRec2_main.py用一条简单的规则组合:

bent_status = { "index": calc_angle(hand_landmarks.landmark[5], hand_landmarks.landmark[6], hand_landmarks.landmark[8]), "middle": calc_angle(hand_landmarks.landmark[9], hand_landmarks.landmark[10], hand_landmarks.landmark[12]), "ring": calc_angle(hand_landmarks.landmark[13], hand_landmarks.landmark[14], hand_landmarks.landmark[16]), "pinky": calc_angle(hand_landmarks.landmark[17], hand_landmarks.landmark[18], hand_landmarks.landmark[20]), } if (bent_status["index"] < -0.85 and bent_status["middle"] < -0.85 and bent_status["ring"] > -0.5 and bent_status["pinky"] > -0.5): result = "scissors" elif bent_status["index"] > -0.5 and bent_status["middle"] > -0.5 and \ bent_status["ring"] > -0.5 and bent_status["pinky"] > -0.5: result = "stone" elif bent_status["index"] < -0.85 and bent_status["middle"] < -0.85 and \ bent_status["ring"] < -0.85 and bent_status["pinky"] < -0.85: result = "paper"

这段代码的判定顺序是有讲究的。它先检查“剪刀”而不是先检查“石头”,因为剪刀比石头更严格:剪刀要求食指和中指伸直、无名指和小指弯曲,而石头只要求四根手指全部弯曲。如果把石头放在前面,一只张开的手在手指状态不明时容易被误判为布。源码把最严苛的、最容易产生误判的手势放在最前面,实际运行中这种顺序能减少至少一倍的错误切换。

3.3 拇指的单独处理

石头剪刀布中拇指其实不参与判定,但HandRec2_main.py仍然计算了拇指的夹角,目的不是为了分类,而是为了过滤无效帧。当整只手握拳时,拇指与食指侧面的距离很近,MediaPipe在低分辨率下会丢失拇指关键点,输出坐标突然跳到图像的另一个角落。如果在某一帧中拇指关键点4的坐标超出了0到1的可信范围,源码会丢弃这一帧,不更新任何手势状态。这种简单的“信心校验”比等待MediaPipe输出handedness分数更可靠,因为handedness只反映左右手,不反映点追踪质量。

4. 把项目跑起来:环境配置、参数调整与排错

4.1 依赖安装与最小运行环境

这个源码包依赖的核心库只有两个:MediaPipe和OpenCV。MediaPipe的Python包在3.7到3.11的Python版本上都有预编译轮子,不建议用3.12以上的版本,因为部分依赖的Protobuf版本还不兼容。安装命令如下:

python -m venv venv source venv/bin/activate # Windows下为 venv\Scripts\activate pip install mediapipe==0.10.14 opencv-python==4.10.0.84 numpy python HandRec_main.py

mediapipe==0.10.14是当前比较稳定的版本,0.10.14之后手部关键点索引没有变化,但内部模型文件体积增大了,低配机器上首次加载更慢。安装完成后如果import mediapipe报缺少libGL.so.1,在Ubuntu上执行apt install libgl1-mesa-glx,在CentOS上执行yum install mesa-libGL即可。

4.2 MediaPipe初始化参数怎么设

运行前需要在HandRec_main.py里检查mp.solutions.hands.Hands()的构造参数。默认参数是static_image_mode=Falsemax_num_hands=1model_complexity=1min_detection_confidence=0.5min_tracking_confidence=0.5。其中有两个参数对识别结果影响最大,我习惯按以下表格调整:

参数默认值建议值调整原因
min_detection_confidence0.50.6过低容易把背景中的杂物当作手,过高会导致手快速移动时丢失检测
min_tracking_confidence0.50.7手势识别需要连续稳定的关键点,过高会在手局部遮挡时停顿
max_num_hands11数字和石头剪刀布都只针对单只手,改为2会带来额外的计算开销
model_complexity110模型快但关键点抖动明显,2模型精度高但帧率跌一半

尤其注意static_image_mode,它默认是False,表示使用视频流的追踪模式。如果你直接拿一张图片来做识别,一定要把static_image_mode=True,否则第一帧检测后会直接进入追踪逻辑,后续对静止图片的处理会出现坐标漂移。

4.3 摄像头画面翻转与帧率瓶颈

大多数笔记本摄像头的画面是镜像的,用户伸手向左,画面里手向右。HandRec_main.py里通常有一个cv2.flip(frame, 1)操作,这是最容易被新手删掉的一行。删掉后手势判定本身不会出错,但用户体验会很奇怪,因为关键点坐标和屏幕画面不一致,你会发现自己必须先反向移动手才能得到预期结果。

帧率是另一个坑。MediaPipe Hands在CPU上单帧推理约20到40毫秒,加上OpenCV读取和绘制,计整条循环很容易跌到20帧以下。源码如果直接用cv2.VideoCapture(0)默认分辨率是640x480,这足够用。如果你把它改成1280x720,识别延迟会明显增加。我一般会额外加一个跳帧逻辑:

ret, frame = cap.read() if not ret: break frame = cv2.resize(frame, (320, 240)) rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) result = hands.process(rgb_frame)

把输入降为320x240后,MediaPipe模型内部会先把图像缩放到192x192,输入分辨率再高也只会增加OpenCV读取和预处理时间。我实测从640x480降到320x240,帧率从22提升到30,识别精度几乎没有变化,因为关键点模型在192x192分辨率下工作,输入图太大反而会引入更多背景噪声。

4.4 常见Error定位顺序

如果运行后脚本直接退出,先看摄像头是否被占用。Windows下多个Python进程同时打开同一个摄像头会报VIDEOIO ERROR: V4L2CV_IMWRITE,关闭其他占用摄像头的软件即可。如果程序能运行但没有任何显示,检查cv2.imshow后面是否漏了cv2.waitKey(1),没有这一行窗口会直接无响应。如果提示No module named 'numpy',说明你用的虚拟环境之前装了多个Python版本,用which python确认当前解释器路径后再安装依赖。

还有一个非常隐蔽的问题:MediaPipe会输出NaN坐标。当手的一部分超出画面边界,模型补全的关键点坐标可能是NaN,而不是0。在计算距离和夹角之前加一个math.isnan检查是必要的。源码包里的两个主程序都缺少这个保护,我建议在hand_landmarks.landmark[tip_id]取值后立刻断言:

if any(math.isnan(lm.x) or math.isnan(lm.y) for lm in hand_landmarks.landmark): continue

这一行能防止数字6到9的识别结果里偶尔出现一个0.000的异常值,特别是当手靠近镜头边缘时。

5. 从固定手势到自定义:扩展角度特征与性能优化

两个主程序覆盖了0到9和石头剪刀布,但手势识别的真实需求往往不止这些。最常见的扩展是识别更多手势,比如数字10、点赞、OK、竖中指。这些手势大部分可以用已有的角度特征组合出来,不需要重新引入模型。以“点赞”为例,它的特征是拇指伸直,其余四指弯曲。在HandRec2_main.py已有的弯曲检测基础上,额外增加拇指夹角判定即可:

thumb_angle = calc_angle(hand_landmarks.landmark[1], hand_landmarks.landmark[2], hand_landmarks.landmark[4]) if thumb_angle < -0.8 and bent_status["index"] > -0.3 and bent_status["middle"] > -0.3: result = "thumb_up"

这里calc_angle的第一个参数换了:标准角度计算中的三个点,对于拇指应该取掌骨根部1、近节指骨2、指尖4。拇指的角度特性和其他四指不同,因为它有更大的横向活动范围,所以阈值也要单独调,我用的是-0.8而不是-0.85

如果你想做的不是加一两个手势,而是希望系统能自动区分不同的手势组合,我建议把每根手指的夹角余弦值组成一个4维向量,作为特征输入一个浅层分类器。常见做法是用scikit-learnRandomForestClassifier,训练数据就从现有的calc_angle输出里录制。这样比手工写规则更适应不同人的手型。录制500帧左右,每帧保存一个夹角向量和标签,训练精度可以达到95%以上。这个思路保留了MediaPipe做关键点提取、传统机器学习做分类的优点,避免端到端深度学习模型动辄几小时的训练时间。

性能优化方面,除了前面提到的分辨率调整,还可以让MediaPipe只检测不追踪。把static_image_mode=True后,每一帧都走完整的检测流程,关键点会更稳定,但帧率会下降一半。实际项目中我通常保留追踪模式,并叠加一个“关键点平滑”操作:用指数移动平均滤波处理指尖坐标。原因很简单,手动跟踪模式下的关键点在快速移动时会有像素级抖动,角度计算对抖动很敏感,平滑后结果会稳定很多:

smoothed_x = alpha * tip.x + (1 - alpha) * prev_tip_x smoothed_y = alpha * tip.y + (1 - alpha) * prev_tip_y

alpha取0.3到0.5之间,太小轨迹滞后明显,太大会让角度计算产生惯性误差。经过平滑后,手指在伸直和弯曲之间的切换会更“干脆”,不会出现前后两帧在“石头”和“布”之间反复跳跃的情况。这个技巧可以直接应用到HandRec_main.py的数字6到9判定中,效果比简单调min_tracking_confidence要好,因为它作用于最终特征而不是模型输出。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 0:19:50

命理计算引擎:PySide6+纯函数式架构实现高精度八字推演

1. 这不是算命软件&#xff0c;而是一套可验证、可复现的命理计算基础设施 “命理计算引擎”这个词听起来玄乎&#xff0c;但拆开来看&#xff0c;它本质上就是一套 输入确定性参数&#xff08;出生年月日时、地点&#xff09;、输出结构化结果&#xff08;八字干支、十神关系…

作者头像 李华
网站建设 2026/9/15 0:19:48

SpringBoot非遗管理系统开发实践与架构解析

1. 项目背景与核心价值非物质文化遗产管理系统作为文化保护领域的重要数字化工具&#xff0c;其开发过程涉及文化遗产数据管理的多个专业维度。这个基于SpringBoot的毕业设计项目&#xff0c;本质上是一个具备完整CRUD功能的Web应用系统&#xff0c;主要解决非遗项目的信息录入…

作者头像 李华
网站建设 2026/9/15 0:19:28

Tauri Windows开发:link.exe not found报错排查与解决指南

在 Windows 上折腾 Tauri 开发&#xff0c;“link.exe not found”这条报错可以说是新手劝退率最高的一道坎。我第一次撞上它是在一个周五晚上&#xff0c;代码逻辑全写完了&#xff0c;Rust 侧编译也一路通过&#xff0c;偏偏到链接可执行文件时终端弹出一片红色&#xff0c;提…

作者头像 李华
网站建设 2026/9/15 0:19:21

今天就能上手的3个GitHub项目:freeCodeCamp、public-apis、starship

GitHub上到底有多少个仓库&#xff1f;这个数字今天已经到亿级了&#xff0c;所以“逛GitHub”这件事&#xff0c;对大多数刚接触的人来说&#xff0c;不是没东西看&#xff0c;而是东西太多看不完。Star高的不一定适合你&#xff0c;Star少的又担心没人维护。我平时被问得最多…

作者头像 李华
网站建设 2026/9/15 0:18:21

SAP功能位置标签版本管理:CDS视图与增量抽取实战

1. 先从功能位置的“编号”说起做 SAP PM&#xff08;工厂维护&#xff09;的同事应该都有这种经历&#xff1a;功能位置&#xff08;Functional Location&#xff09;作为设备台账的顶层对象&#xff0c;按工厂、区域、产线一层层搭起来&#xff0c;编号往往直接反映了物理位置…

作者头像 李华
网站建设 2026/9/15 0:17:48

UC网盘直链提取技术解析与免登录下载方案

1. UC网盘资源下载的常见场景分析作为国内主流网盘服务之一&#xff0c;UC网盘在日常工作文件共享和影视资源传播中应用广泛。但许多用户都遇到过这样的困境&#xff1a;当同事发来一个UC网盘链接&#xff0c;或是论坛找到某部影视资源时&#xff0c;点击后却弹出强制登录页面。…

作者头像 李华