
简介一套基于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 mediapipe0.10.14 opencv-python4.10.0.84 numpy python HandRec_main.pymediapipe0.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_modeFalse、max_num_hands1、model_complexity1、min_detection_confidence0.5、min_tracking_confidence0.5。其中有两个参数对识别结果影响最大我习惯按以下表格调整参数默认值建议值调整原因min_detection_confidence0.50.6过低容易把背景中的杂物当作手过高会导致手快速移动时丢失检测min_tracking_confidence0.50.7手势识别需要连续稳定的关键点过高会在手局部遮挡时停顿max_num_hands11数字和石头剪刀布都只针对单只手改为2会带来额外的计算开销model_complexity110模型快但关键点抖动明显2模型精度高但帧率跌一半尤其注意static_image_mode它默认是False表示使用视频流的追踪模式。如果你直接拿一张图片来做识别一定要把static_image_modeTrue否则第一帧检测后会直接进入追踪逻辑后续对静止图片的处理会出现坐标漂移。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_modeTrue后每一帧都走完整的检测流程关键点会更稳定但帧率会下降一半。实际项目中我通常保留追踪模式并叠加一个“关键点平滑”操作用指数移动平均滤波处理指尖坐标。原因很简单手动跟踪模式下的关键点在快速移动时会有像素级抖动角度计算对抖动很敏感平滑后结果会稳定很多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要好因为它作用于最终特征而不是模型输出。本文还有配套的精品资源点击获取