简介:一套基于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.pymediapipe==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=False、max_num_hands=1、model_complexity=1、min_detection_confidence=0.5、min_tracking_confidence=0.5。其中有两个参数对识别结果影响最大,我习惯按以下表格调整:
| 参数 | 默认值 | 建议值 | 调整原因 |
|---|---|---|---|
min_detection_confidence | 0.5 | 0.6 | 过低容易把背景中的杂物当作手,过高会导致手快速移动时丢失检测 |
min_tracking_confidence | 0.5 | 0.7 | 手势识别需要连续稳定的关键点,过高会在手局部遮挡时停顿 |
max_num_hands | 1 | 1 | 数字和石头剪刀布都只针对单只手,改为2会带来额外的计算开销 |
model_complexity | 1 | 1 | 0模型快但关键点抖动明显,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: V4L2或CV_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-learn的RandomForestClassifier,训练数据就从现有的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_yalpha取0.3到0.5之间,太小轨迹滞后明显,太大会让角度计算产生惯性误差。经过平滑后,手指在伸直和弯曲之间的切换会更“干脆”,不会出现前后两帧在“石头”和“布”之间反复跳跃的情况。这个技巧可以直接应用到HandRec_main.py的数字6到9判定中,效果比简单调min_tracking_confidence要好,因为它作用于最终特征而不是模型输出。
本文还有配套的精品资源,点击获取