拓冰建站拓冰建站
首页 / 资讯中心 / 正文

基于Python+OpenCV+MediaPipe的手势数字识别与猜拳对战实现

简介基于PythonOpenCVMediaPipe实现的手部数字识别项目面向计算机视觉初学者与机器学习爱好者适用于数字手势交互、智能猜拳等场景。项目集成数字手势分类器与猜拳决策模型利用MediaPipe提取21个手部关键点坐标实现1-5数字的实时识别与石头剪刀布动态判定准确率超过95%并支持摄像头、视频、图片三种输入模式兼顾实时演示与离线批量测试。资源包共6个文件包括3个Python源码、1段MP4演示视频、1份说明文档和1张示例图片压缩后仅8.97MB源码涵盖手部跟踪、HSV颜色提取、主程序三个模块说明文档还给出了OpenCV 4.11.0、NumPy 1.26.4、MediaPipe 0.10.14及其与Python 3.12配合运行的环境清单预设分析模式0/1/2分别对应摄像头、视频、图片方便直接复现。目前已有97人学习下载适合希望快速上手手势识别、仔细拆解关键点检测与规则判定思路的开发者参考实践。 手部数字识别这个项目最初起因很简单我想给孩子做一个能“看懂手势”的小玩具结果做着做着就把数字识别、石头剪刀布判胜负、摄像头实时交互全塞进了同一套系统。做完才发现这东西其实挺适合作为 Python OpenCV MediaPipe 的入门练手项目因为它麻雀虽小但把“图像采集—关键点检测—状态解析—业务逻辑”这条完整的视觉处理链路都串起来了。你拿到这套源码可以用摄像头实时比数字 0 到 5也可以丢一张图片或一段视频进去批量识别同时它内置了猜拳对战模式识别出你出的是石头、剪刀还是布再随机出一个电脑手势直接告诉你谁赢谁输。整个项目在正常光照、手部正对相机的场景下准确率可以稳定超过 95%。如果你正想接触 Python 图像处理或者想在摄像头画面里做点交互效果这套代码是个很不错的起点。1. 项目定调这套系统到底做了什么1.1 一句话说清项目形态你可以把整个系统理解成一条流水线OpenCV 负责把摄像头、视频文件或图片读成一帧一帧的画面然后 MediaPipe 从画面中检测出手部位置并返回一只手上的关键点坐标接下来关键点坐标被换算成“哪根手指是伸直的、哪根手指是弯曲的”这个状态数组最后由业务逻辑层把这个状态数组转换成数字标签或者转换成石头剪刀布的手势再做胜负判断。所以整个项目本质上是一个“规则驱动的视觉识别系统”。它没有训练任何自定义模型而是充分借用了 MediaPipe 预训练好的手部关键点模型把自己要解决的数字识别、猜拳识别全部写成了对关键点坐标的规则判断。这个设计有很实际的好处代码量小逻辑透明任何一步识别结果不对你都能肉眼看出是检测环节的问题还是状态判断环节的问题。1.2 技术选型背后的取舍为什么是 Python OpenCV MediaPipe而不是别的方案Python 不用多说做图像处理原型最快OpenCV 的读取、绘制、窗口显示功能是现成的几乎成了 CV 项目的默认底座。真正需要纠结的是手部关键点检测这一步。最原始的做法是自己训练一个关键点检测模型比如用深度学习框架标注一两万张手部图像训练一个回归网络输出 21 个关键点。这个方法可定制性强但对大多数人来说成本太高光是数据标注就劝退了。第二种思路是用目标检测模型先把手掌框出来再进行后续判断但目标是拿到“指尖、指根”这类细粒度信息单纯的目标检测做不到还得再接一个分类网络链路变长了。第三种就是我们最终选的 MediaPipe Hands它直接输出手腕 每根手指的 4 个关键点一共 21 个点每个点还带 x、y、z 三个坐标开箱即用精度和速度都够日常交互需求。OpenCV 在这个项目里的定位也很明确它不是识别的主角而是负责图像数据的搬运和展示。帧读取、颜色空间转换、缩放、画点画线、显示窗口、保存视频这些都是 OpenCV 的活。MediaPipe 本身没有图像读取能力它只接收一张 RGB 图像返回识别结果两者配合非常默契。2. 手部识别的底层逻辑21个关键点就是全部基础2.1 MediaPipe Hands 在工作时到底“看”了什么MediaPipe Hands 的完整检测链路分两步。第一步是手掌检测palm detection它会在整张图中找到手掌的大致区域输出一个手掌边界框。第二步是手部关键点回归hand landmark model在框出的区域里精确回归出 21 个关键点的坐标。这 21 个点的分布很有规律关键点 0手腕关键点 1~4拇指其中 4 是拇指指尖关键点 5~8食指其中 8 是食指指尖关键点 9~12中指其中 12 是中指指尖关键点 13~16无名指其中 16 是无名指指尖关键点 17~20小指其中 20 是小指指尖每个关键点的坐标有 x、y、z 三个值。x、y 是归一化到 0~1 的相对坐标要得到图像上的像素位置需要乘以图像的宽和高。z 坐标代表的是该点相对手腕的深度值手腕本身就是参考零点所以 z 值只表示手的内部相对深度不能直接用来测量手距离摄像头多远。实际做手势判断时我们真正关心的是“手指指尖相对于手指根部的方位变化”。手是一个能绕手腕自由旋转的器官你在侧面比剪刀、正面比剪刀拍到画面里的形状差异非常大。要解决这个问题就不能只看绝对坐标而要看关键点之间的相对几何关系。2.2 手指伸直还是弯曲角度法是关键判断手指是伸直还是弯曲常见做法有三种。最简单粗暴的是比较指尖和指根的 y 坐标。假设手掌朝上、手指竖直向上伸那指尖的 y 值一定比指根小。但手的旋转一旦变化这个规则就失效了。侧着握手时指尖和指根的 y 值差可能趋近于零误判率直线上升。第二种是计算关键点之间的距离比例。比如把指尖到手腕的距离除以中指的长度大于某个比例就算伸直。这个方法比单纯比较坐标稍微鲁棒一点但手的缩放、手指长短比例差异还是会带来明显误差。第三种就是我最终采用的“关节夹角法”。以食指为例取三个点指根 MCP编号 5、中间关节 PIP编号 6、指尖 TIP编号 8计算向量 MCP→PIP 与向量 PIP→TIP 的夹角。手指伸直时这三个点基本在同一条直线上夹角接近 180 度手指弯曲时夹角明显变小。这个方法关注的是手指内部的相对形变不依赖手的整体旋转方向稳定性最好。夹角计算的代码很简单import math def angle_between(p1, p2, p3): # 计算向量 p1p2 和 p3p2 的夹角p2 是公共点 v1 (p1[0] - p2[0], p1[1] - p2[1]) v2 (p3[0] - p2[0], p3[1] - p2[1]) cos_theta (v1[0] * v2[0] v1[1] * v2[1]) / ( math.hypot(v1[0], v1[1]) * math.hypot(v2[0], v2[1]) 1e-6 ) cos_theta max(-1.0, min(1.0, cos_theta)) return math.degrees(math.acos(cos_theta))然后对五根手指分别取点算夹角def finger_states(landmarks): tips [4, 8, 12, 16, 20] # 拇指、食指、中指、无名指、小指的指尖 pips [3, 6, 10, 14, 18] # 各手指的中间关节 mcp [2, 5, 9, 13, 17] # 各手指的根部关节 states [] for i in range(5): ang angle_between(mcp[i], pips[i], tips[i]) states.append(ang 150) # True 表示伸直False 表示弯曲 return states这里的 150 度阈值是我在大量测试中调出来的经验值。不同的人手型、年龄、关节柔韧度不一样阈值太大会把本来就伸不直的手指判成弯曲阈值太小又容易把微微弯曲的手指判成伸直。150 这个值在多数成年人身上表现比较稳定如果你发现自己的手总是误判优先微调这个参数。3. 核心算法把状态变成结果3.1 数字识别一组状态对应一个数字当手指状态数组出来后数字识别就变成了一张映射表。我用的是常见的手势规范也就是从食指开始往外数数字拇指食指中指无名指小指0弯弯弯弯弯1弯伸弯弯弯2弯伸伸弯弯3弯伸伸伸弯4弯伸伸伸伸5伸伸伸伸伸把这套规则写成代码就是逐个比对def recognize_number(states): if not any(states): return 0 if states [False, True, False, False, False]: return 1 if states [False, True, True, False, False]: return 2 if states [False, True, True, True, False]: return 3 if states [False, True, True, True, True]: return 4 if all(states): return 5 return -1 # 无法识别边界情况也要想清楚。有人习惯用拇指比 1或者数字 3 时直接伸出三根手指而不是逐根伸。如果程序返回 -1我建议在画面里显示“无法识别”的提示而不是猜测用户意图。猜测往往会在后面引入一连串问题。3.2 石头剪刀布与自动胜负判断猜拳的映射比数字映射更简单因为它只有三个类别石头全部手指弯曲状态等价于数字 0剪刀食指和中指伸直其余弯曲状态等价于数字 2布全部手指伸直状态等价于数字 5所以猜拳识别可以复用同一个状态数组只是换一套语义标签def recognize_gesture(states): if not any(states): return 石头 if states [False, True, True, False, False]: return 剪刀 if all(states): return 布 return None胜负判断用的是经典的猜拳规则表。石头赢剪刀剪刀赢布布赢石头相同手势为平局。我把它写成了一个映射表win_map { (石头, 剪刀): 1, (剪刀, 布): 1, (布, 石头): 1, (石头, 布): -1, (剪刀, 石头): -1, (布, 剪刀): -1, } def judge(player, computer): if player computer: return 0 return win_map.get((player, computer), 0)返回 1 表示玩家赢返回 -1 表示电脑赢返回 0 表示平局。电脑手势可以随机生成也可以做成简单的循环逻辑增加可玩性。3.3 一次检测两套语义的设计心得数字识别和猜拳共用一个手指状态数组这个复用设计是整个项目里我觉得最值得讲的一个点。很多新手写这类程序遇到数字识别写一套 if 判断遇到猜拳又写一套 if 判断代码重复不说两套判断还可能因为标准不一致而互相打架。这里我们把“物理状态”和“语义结果”彻底拆开MediaPipe 输出关键点坐标是物理信号手指状态数组是中间特征数字标签和猜拳标签都是语义结果。物理层只算一次语义层可以任意扩展。以后想加个手势指南针、手势点赞、手势 OK根本不需要动检测和状态计算部分只需要在语义层加一个新的映射函数就行。这个思路放在真实项目里就是“特征层与业务层解耦”小项目里有这种意识后面写大项目会省很多事。4. 三种识别模式一个核心函数打天下4.1 统一入口一切输入都先变成帧三种识别模式分别是摄像头实时识别、视频文件识别、图片识别。从工程角度讲模式多并不等于代码量大关键是要把“帧处理”抽象成统一入口。图片就是单帧视频是连续的帧序列摄像头是实时的帧流。看起来来源不同但一旦进入处理流程眼前都只有一个对象BGR 格式的 numpy 数组。所以我设计了一个核心处理函数接收一帧返回处理后的画面和识别结果def process_frame(frame): # 输入OpenCV 读到的 BGR 图像 # 输出标注后的图像 识别结果字典 rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results hands.process(rgb) ... return annotated_frame, result_dict图片模式的调用方式就是读一张图处理一次显示结果。视频模式和摄像头模式则是一个循环不断读取新帧调用同一个函数只是循环的终止条件和帧来源不同。4.2 摄像头实时模式的两个优化细节摄像头模式最考验工程细节。第一件要做的事是把画面水平翻转否则你抬手向左画面里的手却向右体验非常别扭。cv2.flip(frame, 1) 一行代码解决。第二件要注意的是输入尺寸。MediaPipe 处理 640x640 的输入和 1920x1080 的输入耗时差距非常明显。摄像头原始分辨率往往很高直接送进去不仅慢而且功耗高。我的做法是先缩放帧让最长边不超过 720识别完再基于缩放后的坐标在原始画面绘制结果这样既保证了显示清晰度又保证了处理速度。再一个细节是结果平滑。摄像头模式下单帧误判很容易造成数字在 1 和 2 之间来回跳。我加了一个简单的防抖逻辑只有连续两帧识别结果一致时才更新画面上的最终标签。这不会增加太多延迟但能明显改善视觉体验。4.3 图片和视频模式的处理差异图片模式没有实时性压力反而要注意绘制结果别把原图内容盖住。画关键点时我习惯用细线和小圆点文字放在画面上方留出空白区域。视频模式最坑的是输出保存。用 cv2.VideoWriter 保存结果视频时写入帧的尺寸必须和初始化时保持一致否则文件要么打不开要么花屏。二是编码器要选对我这边测试比较稳妥的是 mp4v 编码输出 .mp4 文件。三是有时候源视频的帧率很高逐帧识别并保存会极慢可以通过按间隔取帧来控制处理密度比如每两帧处理一帧输出视频的帧率设为原视频帧率的一半。5. 环境安装与快速复现5.1 版本选择与安装命令项目依赖三个库OpenCV、MediaPipe、NumPy。MediaPipe 底层依赖 NumPy装的时候会自动带上但手动装一遍更稳妥。Python 版本建议用 3.8 到 3.10太新的版本有时会遇到部分库兼容性问题。安装命令很简单pip install opencv-python mediapipe numpy如果下载速度慢尤其是 MediaPipe 这个包比较大可以加清华镜像源pip install opencv-python mediapipe numpy -i https://pypi.tuna.tsinghua.edu.cn/simple装完后做一次快速验证import cv2 import mediapipe as mp print(cv2.__version__) print(mp.__version__)能正常打印版本号环境就算通了。5.2 核心代码三段式解析完整工程代码量不大核心模块就三段。第一段是初始化 MediaPipe Hands 模型mp_hands mp.solutions.hands hands mp_hands.Hands( static_image_modeFalse, # 视频/摄像头流模式 max_num_hands1, # 同时最多检测 1 只手 min_detection_confidence0.5, min_tracking_confidence0.5, )static_image_mode 在纯图片识别时设为 True在视频和摄像头模式下设为 False这样会启用跟踪机制关键点更稳定。max_num_hands 设为 1 是为了避免多人同时入镜时判断冲突猜拳场景下每次只需要看一只手。第二段是主循环以摄像头模式为例cap cv2.VideoCapture(0) while True: ok, frame cap.read() if not ok: break frame cv2.flip(frame, 1) rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results hands.process(rgb) if results.multi_hand_landmarks: lm results.multi_hand_landmarks[0].landmark pts [(p.x, p.y) for p in lm] states finger_states(pts) number recognize_number(states) gesture recognize_gesture(states) # 绘制关键点和标签 ... else: # 画面提示没有检测到手 ... cv2.imshow(Hand Recognition, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()第三段是猜拳对战逻辑。识别出玩家的手势后随机生成电脑手势调用 judge 函数判断胜负把结果绘制在画面上。这里有个小细节电脑手势不要每帧都随机不然玩家手势还没摆好电脑已经换了好几轮了。我是在玩家手势发生改变时才重新随机一次电脑手势对战体验自然很多。6. 实测调优与常见问题排查6.1 准确率 95% 不是凭空来的“识别准确率 95%” 这个数字在实际测试中是有前提条件的。我测试时固定了三个约束光线均匀、手部正对摄像头、手距离摄像头 30 到 70 厘米。在这三个条件内数字识别和猜拳识别的准确率都能稳定超过 95%。一旦超出约束范围准确率会下降。比如逆光环境下手部轮廓和背景对比度低关键点检测会不稳定手侧对摄像头时手指之间的空间关系被压缩弯曲状态判断容易出错手离得太远关键点抖动严重误判率上升。这不是模型不好而是所有视觉系统的通病。要提升准确率可以从几个方向入手。一是调节 min_detection_confidence 和 min_tracking_confidence默认 0.5 可以往上调到 0.7减少误检测但会牺牲一点检测距离。二是优化手指判定阈值150 度不是圣旨你可以用一段测试视频把手部状态和实际画面录下来回放统计一下自己的阈值偏离情况。三是增加结果平滑如前面说的连续两帧一致才更新标签这能让最终展示层面的准确率明显提升。6.2 会踩的坑与排查速查表我实际开发过程中踩过不少坑整理成了一张排查表面向摄像头/视频/图片三种模式通用现象可能原因解决方式摄像头画面黑或打不开摄像头索引错误或被其他程序占用cv2.VideoCapture(0) 改成 1 试其它索引先关掉占用摄像头的软件报错 No module named cv2OpenCV 未安装或运行解释器不是当前虚拟环境在当前环境执行 pip install opencv-python报错 No module named mediapipeMediaPipe 未安装或 Python 版本过高确认 Python 版本在 3.8~3.10重新 pip install mediapipe一直检测不到手光照太暗、背景杂乱、手离摄像头太远调整光照简化背景让手靠近摄像头并正对画面识别结果在数字/手势间来回跳单帧误判或手指状态处于阈值临界区增加连续多帧一致才更新的平滑逻辑微调角度阈值画面卡顿严重输入分辨率过大逐帧全量推理开销高缩放帧后再送 MediaPipe最长边控制在 720 以内保存的视频打不开VideoWriter 尺寸与写入帧尺寸不一致保证写入的每一帧 width、height 与初始化完全一致cv2.putText 中文乱码OpenCV 原生不支持中文渲染改用英文标签或用 PIL 先把中文画到透明层再叠加到画面最后再分享一个我的个人习惯这类视觉识别项目我最先跑通的往往不是摄像头模式而是图片模式。先用几张固定图片把识别逻辑调稳再上摄像头问题定位会快非常多。如果你也打算从零复现这套系统建议按图片→视频→摄像头的顺序推进会少走不少弯路。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门