
简介基于Python的OpenCV与MediaPipe框架构建了一套轻量级手势识别与手指计数方案面向计算机视觉方向毕设学生及入门开发者可在普通摄像头环境中实时运行。方案将Google开源的MediaPipe手部关键点检测模型与OpenCV图像处理相结合实现了手部区域定位、指尖坐标提取与指头打开状态判定最终完成0到5的手指数量统计代码注释清晰便于移植到其他视觉应用中。资源包含完整可运行的Python源码以及算法说明文档并提供测试数据帮助快速上手MediaPipe手部关键点检测与OpenCV图像处理流程。压缩包共5个文件以.py脚本和.md文档为主另有.gitignore辅助文件整体仅7KB结构精简便于阅读与二次修改。目前已有1583人浏览学习适用于毕设演示、课堂实验或手势交互小项目读者可直接运行代码并对照文档理解手部检测、指尖判定、计数逻辑等核心环节。1. 从一份可运行的mediapipe手势识别项目说起很多做计算机视觉毕设的同学一开始都会纠结要不要自己训练一个手势分类网络。实际上如果目标只是“手指数数”“手势交互”这类原型演示完全不需要从零训练。mediapipe 这个谷歌开源的框架已经把手部 21 个关键点检测做到了 CPU 也能跑到 30 帧左右OpenCV 只负责读视频流、画框、显示文字两个模块组合起来就是一套能答辩、能演示的项目。这个压缩包里的 HandTrackingModule.py 和 main.py 就是典型的分层结构模型封装和业务逻辑彻底分开读懂以后想改成“点赞”“比心”或者 0 到 9 手势识别都是在同一套关键点坐标上做文章。2. mediapipe手部关键点模型与OpenCV图像处理链路2.1 环境搭建pip安装与Anaconda环境隔离拿到项目的第一件事不是跑代码而是把依赖装干净。两个核心模块的安装命令非常短pip install opencv-python pip install mediapipe这里有一个经常被忽略的坑mediapipe 依赖里会带一份 OpenCV如果你当前环境已经有 opencv-python版本不一致时很容易出现_ARRAY_API not found或者cv2.error之类的告警。我的建议是不要直接在 base 环境里硬装而是用 Anaconda 单独建一个环境把机器学习项目和其他日常项目隔离开。conda create -n handdemo python3.9 conda activate handdemo pip install mediapipe opencv-python装完之后用两行代码验证是否成功import cv2 import mediapipe as mp print(cv2.__version__) print(mp.__version__)如果控制台能正常输出两个版本号说明依赖没问题。从这一步开始后续所有代码都建议在这同一个环境下运行避免出现“代码在自己电脑跑通、在答辩电脑上跑不起来”的尴尬情况。2.2 项目文件结构与入口解压python-gesture-recognition-master.zip之后目录结构通常是这样的具体文件以压缩包为准python-gesture-recognition-master/ ├── main.py # 主程序入口读取视频流并显示结果 ├── HandTrackingModule.py # 手部关键点检测封装类 ├── readme.md # 中文运行说明 ├── README.en.md # 英文说明 └── data/ # 测试图片或测试视频main.py 是启动入口HandTrackingModule.py 是核心封装。第一次跑通的时候我一般先不急着接摄像头而是先在 main.py 里找到cv2.VideoCapture(0)这一段把参数确认好再运行。如果摄像头不能直接用readme 里一般会说明如何切换成本地测试视频。2.3 从摄像头到屏幕的帧处理流程这个项目的整体流程实际上就是一个 OpenCV 调用相机 mediapipe 关键点检测的标准链路。OpenCV 的VideoCapture底层封装了本地的摄像头驱动或者视频文件解码你不需要自己处理 V4L2 或 DirectShow 这类底层细节。整个主循环可以拆成四个动作读帧、色彩转换、关键点检测、叠加绘制。import cv2 from HandTrackingModule import HandTrackingModule cap cv2.VideoCapture(0) detector HandTrackingModule(max_num_hands2) while cap.isOpened(): ok, frame cap.read() if not ok: break # 镜像画面让左右移动和真实手势保持一致 frame cv2.flip(frame, 1) # 在原始 BGR 帧上绘制手部关键点和骨架 frame detector.find_hands(frame) # 返回关键点坐标列表后续手指计数会用到 lm_list detector.get_position(frame) # 显示当前检测到的关键点数量方便判断模型是否生效 cv2.putText(frame, fPoints: {len(lm_list)}, (10, 40), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) cv2.imshow(Hand Tracking, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这段代码的逻辑很直白cap.read()从摄像头读出一帧 BGR 图像cv2.flip(frame, 1)做水平翻转因为摄像头画面默认反向不翻过来会感觉像在照镜子。detector.find_hands()负责把检测结果画在帧上detector.get_position()再把归一化坐标换算成像素坐标返回。cv2.putText只是用来打一个调试信息如果屏幕上 Points 的数量始终是 0优先检查摄像头索引、光线和背景复杂度不要急着改算法。mediapipe 的Hands模型内部是一个“检测 追踪”的组合第一帧先检测手的位置后续帧会直接追踪。这样做的好处是在视频流里能保持较高的推理速度坏处是手离开画面再回来时可能会有 1 到 2 帧的延迟后才能重新锁定。这个延迟在毕设演示中是可以接受的但在做实时交互控制时通常还要加一个置信度过滤。3. HandTrackingModule手部关键点提取与绘制实现3.1 为什么把检测器封装成独立模块把 HandTrackingModule 单独拆成一个类而不是在手势识别主循环里直接写mp.solutions.hands是为了让代码边界更清晰。类里只做两件事初始化 hands 模型、提供关键点坐标。至于坐标是拿去做手指计数、音量控制还是翻页都不属于这个类的职责范围。这样设计的好处是你想把项目从“手指计数”改成“手势识别 0 到 9”时不需要改动检测代码只需要在 main.py 或者上层应用里替换业务逻辑。HandTrackingModule 的构造方法把所有可调参数都集中暴露出来import cv2 import mediapipe as mp class HandTrackingModule: def __init__(self, static_image_modeFalse, max_num_hands1, min_detection_confidence0.5, min_tracking_confidence0.5): self.mp_hands mp.solutions.hands self.mp_draw mp.solutions.drawing_utils self.hands self.mp_hands.Hands( static_image_modestatic_image_mode, max_num_handsmax_num_hands, min_detection_confidencemin_detection_confidence, min_tracking_confidencemin_tracking_confidence) self.results None这里需要注意static_image_mode这个参数。如果是实时视频流置为 False 会用跟踪模型推理速度快如果是对单张图片做批量识别需要置为 True。默认值 False 对毕设演示是合理的因为大部分场景都是摄像头实时画面。3.2 mediapipe Hands 初始化参数表在实际调参过程中我习惯把下面几个参数当成第一优先级参数默认值作用static_image_modeFalseFalse 启用视频流追踪模式True 用于单张图片识别max_num_hands1最多检测手数量做双人交互时设为 2min_detection_confidence0.5手部检测的置信度阈值低于该值不返回结果min_tracking_confidence0.5追踪阶段置信度阈值低于该值会重新检测很多时候检测不到手问题不是代码而是min_detection_confidence太高。光线不足、手离摄像头太远、画面里反光物体太多都会让置信度下降。我一般会先把检测阈值降到 0.3确认模型能稳定检测之后再逐步调高到 0.5。3.3 关键点坐标提取与归一化换算mediapipe 输出的 landmark 是经过归一化的浮点数范围在 0 到 1 之间分别表示 x 和 y 在整个画面上的比例。要换算成像素坐标只需要乘上图像的宽和高。def get_position(self, img, hand_no0): h, w, _ img.shape lm_list [] if self.results.multi_hand_landmarks: hand_lms self.results.multi_hand_landmarks[hand_no] for lm_id, lm in enumerate(hand_lms.landmark): x, y int(lm.x * w), int(lm.y * h) lm_list.append([lm_id, x, y]) return lm_list这里hand_no参数用于选择检测到的第几只手。lm_id对应媒体管道官方定义的 21 个关键点索引0 是腕关节1 到 4 是拇指5 到 8 是食指9 到 12 是中指13 到 16 是无名指17 到 20 是小指。后续手指计数的十根手指头全部依赖这组索引。3.4 绘制手部骨架与关键点绘制逻辑放在find_hands方法里它会把手部骨架直接叠加到原始帧上。def find_hands(self, img, drawTrue): img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) self.results self.hands.process(img_rgb) if self.results.multi_hand_landmarks: for hand_lms in self.results.multi_hand_landmarks: if draw: self.mp_draw.draw_landmarks( img, hand_lms, self.mp_hands.HAND_CONNECTIONS) return img注意第一行代码mediapipe 要求输入 RGB 图像而 OpenCV 读取的默认是 BGR必须先做COLOR_BGR2RGB转换。draw_landmarks是在原来的 BGR 图像上画线所以不会影响后续显示。如果做完单张图片的测试需要把static_image_mode参数设为 True然后走同样的调用流程detector HandTrackingModule(static_image_modeTrue) img cv2.imread(data/test.jpg) img detector.find_hands(img) points detector.get_position(img) print(points)这一段是验证封装是否正常的最快方法不用启动摄像头就能确认模型是否工作。4. 手指计数从关键点到判定规则的推导4.1 手指判定的问题模型有了 21 个关键点之后“手指伸开了没有”并不是一个黑盒分类问题而是一个可以手工设计的几何判断问题。对于食指、中指、无名指、小指最直观的判断方式是比较指尖和它相邻关节的纵坐标手指向上伸直时指尖 y 值比关节更小因为图像坐标系的原点通常在左上角y 轴向下。但这个方法在手掌倒置、手指朝向摄像头、或者手倾斜比较大的情况下会失效。所以更稳的做法是计算手指关键点之间的角度用关节弯曲程度来判断。4.2 坐标法简单直观的快速实现下面这段count_fingers函数使用坐标法先处理拇指再处理其余四指def count_fingers(lm_list): if not lm_list: return 0 fingers [] # 拇指4号点是拇指尖3号点是拇指第二节 # 正常情况下拇指尖 x 坐标会小于第二节 x 坐标 if lm_list[4][1] lm_list[3][1]: fingers.append(1) else: fingers.append(0) # 其余四指指尖坐标比上一关节更靠上判定为伸展 for tip_id in [8, 12, 16, 20]: if lm_list[tip_id][2] lm_list[tip_id - 2][2]: fingers.append(1) else: fingers.append(0) return sum(fingers)这段代码的逻辑非常容易读懂但有一个前提假设手是从摄像头视角正面朝上且没有太大俯仰角度。如果这个假设不成立比如手掌反过来握着拳头那就会数出错误的结果。坐标法适合快速演示要交作业的话也能通过但我不建议直接把它作为唯一的判定逻辑。4.3 角度法用关节夹角提升鲁棒性更稳定的方案是计算三个关键点构成的角度。例如食指的伸直程度取 8 号指尖、6 号近端指节、5 号掌指关节点三个点形成的夹角接近 180 度代表伸直接近 0 到 90 度代表弯曲。import math def get_angle(p1, p2, p3): a math.dist(p1, p2) b math.dist(p2, p3) c math.dist(p1, p3) if a * b 0: return 0 cos_val (a * a b * b - c * c) / (2 * a * b) cos_val max(-1, min(1, cos_val)) return math.degrees(math.acos(cos_val))调用角度函数需要把列表中的点转换成元组坐标。比如判断食指p1 tuple(lm_list[8][1:]) p2 tuple(lm_list[6][1:]) p3 tuple(lm_list[5][1:]) angle get_angle(p1, p2, p3)参数说明p1是指尖p2是中间关节p3是手掌与手指连接处。math.dist计算二维欧氏距离math.acos返回弧度利用余弦定理反推出夹角。伸直时角度一般在 150 度以上握拳时角度会明显缩小。你可以把这个角度阈值设成 130 度然后根据实际测试结果再调整。4.4 把计数结果显示到画面把指技术调用整合进 main.py 非常简单只需要在获得 lm_list 后调用count_fingers再把结果通过 OpenCV 绘制到画面上。fingers count_fingers(lm_list) cv2.putText(frame, fFingers: {fingers}, (10, 100), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 0, 0), 3)这里要注意的一点当手完全离开画面时lm_list是空列表count_fingers会直接返回 0此时画面上会显示Fingers: 0。如果不想让这个 0 干扰演示可以在lm_list为空时显示一个提示文字比如No Hand这样答辩的时候会显得更严谨。对于测试数据如果压缩包里的data目录有视频文件可以直接把cv2.VideoCapture(0)替换成cv2.VideoCapture(data/test.mp4)这样就能在没有摄像头的环境下验证整套流程。5. 把毕设做成可演示的进阶调试技巧5.1 打印关键点坐标定位手指误判每次计数出现偏差先别急着改阈值。在计数函数前加一行print(lm_list[4], lm_list[8], lm_list[12])看拇指和食指的关键点坐标是否和预期一致。如果发现某些手指的角度值一直在临界点附近波动比如 145 度和 155 度之间跳变那就把角度法的阈值从 150 改成 130并加上一个连续三帧的平滑逻辑防止画面抖动导致数字乱跳。5.2 常见报错与解决方式运行项目时最常遇到的问题是ModuleNotFoundError: No module named mediapipe说明当前 Python 环境不是安装模块的那个环境。在命令行用conda activate handdemo切换后再运行。第二个常见问题是摄像头打不开终端会提示error: (-215:Assertion failed) !_img.empty()这种情况先把cv2.VideoCapture(0)改成cv2.VideoCapture(1)试试或者检查笔记本的摄像头物理开关。5.3 从手指计数扩展到 0 到 9 手势识别手指计数只能覆盖 0 到 5想识别 6 到 9 需要把弯曲手指的分布也考虑进去。比如用角度法计算四根手指各自的弯曲程度把它们组成一个长度为 4 的 1/0 向量再加上拇指状态就是 5 维特征。把这组特征和你定义的 0 到 9 手势模板做欧氏距离匹配距离最小的模板就是识别结果。预定义模板时从测试数据里多采集几帧取平均角度比在纸上画理论值更可靠。这样扩展以后整个项目就从手指计数变成了一个简单的静态手势识别系统而底层的关键点检测代码一行都不用改。本文还有配套的精品资源点击获取