
简介这份资源面向具备 Python 基础、希望入门计算机视觉与实时姿态检测的开发者围绕 MediaPipe 预训练模型展开讲解如何借助网络摄像头完成面部标志、手部跟踪与全身姿态的实时识别。内容覆盖 MediaPipe 应用场景、三类预训练模型的功能差异、环境依赖安装以及摄像头视频流捕获、面部检测、Holistic 手部与身体姿态绘制的完整示例代码可帮助读者快速搭建可运行的原型。资源包共 1 个 docx 文档约 16KB以图文与代码片段形式组织便于按知识点顺序阅读和复制调试。目前已有 357 人学习适合作为人机交互、健身指导、手势控制等方向的入门参考也能为后续计算机视觉研究提供基础代码骨架。1. 从一张摄像头画面里同时拿到脸、手和骨架MediaPipe 到底替你做了什么你打开笔记本摄像头想让程序认出你抬了哪只手、比了个什么手势、肩膀有没有歪甚至想用它驱动一个虚拟形象或者做健身动作计数。这件事听起来像三个独立任务人脸关键点、手部关键点、身体姿态估计。传统做法是分别训三个模型、分别调预处理、分别做后处理光是把三路结果对齐到同一帧坐标系就够折腾一整天。MediaPipe 把这三件事收进同一套图计算框架里用 Python 调起来往往几十行就能跑通这也是它在姿势检测这个方向被反复搜索的原因。它解决的核心问题是把「检测 关键点回归」的流水线封装成开箱即用的 Solutions你只负责喂帧、读结果。适合谁做体感交互、健身计数、手势控制、动作纠正、直播特效的开发者尤其是想先跑通再优化的那批人。但要注意它给你的是 2D 归一化坐标加一个相对深度不是毫米级三维重建边界先认清后面少走弯路。2. 环境装不对后面全是玄学Python 与 MediaPipe 的安装路径2.1 版本匹配比装最新版更重要MediaPipe 对 Python 版本和系统架构有明确偏好。截至我写这篇时的经验Python 3.8 到 3.11 是相对稳的区间3.12 早期版本上部分 wheel 还没跟上。很多人搜「python安装教程」「python安装」装了个最新版结果pip install mediapipe直接报找不到匹配版本然后开始怀疑网络。其实先看 wheel 有没有你的平台标签更实在。Windows 上建议用 64 位 Python32 位基本可以放弃。macOS 分 Intel 和 Apple SiliconApple Silicon 要确认 pip 拉的是 arm64 的包。Linux 上如果用的是系统自带 Python权限和依赖容易打架我一般用 venv 隔离。下面这套流程在三个平台上都通用只是激活命令不同。# 创建独立环境避免污染系统 Python python -m venv mp_env # Windows 激活 mp_env\Scripts\activate # macOS / Linux 激活 source mp_env/bin/activate # 升级 pip老 pip 经常解析不到新 wheel python -m pip install --upgrade pip # 安装核心包opencv 用于读摄像头和画图 pip install mediapipe opencv-python逻辑说明venv 把依赖锁在项目目录里后面就算装崩了删掉重来成本极低。--upgrade pip不是可选项旧 pip 的依赖解析器在遇到 mediapipe 这种带原生扩展的包时容易选错版本。opencv-python 负责 VideoCapture 和 imshowmediapipe 本身不提供摄像头读取。参数说明如果你需要指定版本用pip install mediapipe0.10.x这种形式但具体小版本号以你执行pip index versions mediapipe看到的为准不要照抄别人博客里的数字。国内网络拉包慢是常态配一个可用的镜像源属于常规操作这里不展开。2.2 验证安装是否真的可用装完别急着写业务代码先跑一个最小验证。很多人卡在「装是装上了import 就报错」多半是 numpy 版本冲突或者 protobuf 版本不匹配。# verify_install.py import mediapipe as mp import cv2 import numpy as np print(mediapipe:, mp.__version__) print(opencv:, cv2.__version__) print(numpy:, np.__version__) # 尝试实例化一个最轻的解决方案确认原生库能加载 mp_hands mp.solutions.hands with mp_hands.Hands(static_image_modeTrue, max_num_hands1) as hands: print(hands solution loaded ok)逻辑说明mp.solutions下面挂着各个开箱方案实例化时会加载对应的计算图。如果这一步报ImportError或RuntimeError说明原生扩展没装好而不是你业务代码的问题。参数说明static_image_modeTrue表示按单张图处理不做帧间跟踪验证阶段用它最省事。提示如果 import mediapipe 报 protobuf 相关错误先pip install --upgrade protobuf再不行就重建 venv。这类冲突用升级单个包解决的概率大概七成剩下三成靠重装。3. 三路检测的最小可跑代码脸、手、姿态各来一遍3.1 人脸关键点Face Mesh 的 468 个点怎么读Face Mesh 输出 468 个三维点x, y, zx 和 y 是归一化到 [0,1] 的图像坐标z 是相对深度原点大致在头部中心值越小越靠近镜头。这个 z 不是真实距离别拿它当测距用。import cv2 import mediapipe as mp mp_face_mesh mp.solutions.face_mesh mp_drawing mp.solutions.drawing_utils cap cv2.VideoCapture(0) # 0 是默认摄像头 with mp_face_mesh.FaceMesh( max_num_faces1, # 只检测一张脸省算力 refine_landmarksTrue, # 额外输出虹膜和嘴唇精细点 min_detection_confidence0.5, min_tracking_confidence0.5 ) as face_mesh: while cap.isOpened(): ok, frame cap.read() if not ok: break # MediaPipe 要 RGBOpenCV 默认 BGR rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) rgb.flags.writeable False # 只读减少拷贝 results face_mesh.process(rgb) rgb.flags.writeable True if results.multi_face_landmarks: for face_landmarks in results.multi_face_landmarks: mp_drawing.draw_landmarks( imageframe, landmark_listface_landmarks, connectionsmp_face_mesh.FACEMESH_TESSELATION, landmark_drawing_specNone, connection_drawing_specmp_drawing.DrawingSpec( color(0, 255, 0), thickness1) ) cv2.imshow(Face Mesh, frame) if cv2.waitKey(1) 0xFF 27: # ESC 退出 break cap.release() cv2.destroyAllWindows()逻辑说明process接收 RGB 帧返回结果对象multi_face_landmarks是列表每个元素含landmark数组。FACEMESH_TESSELATION是三角网格连接关系画出来是密集网调试时看得清。参数说明refine_landmarksTrue会多出虹膜点做眼神追踪才需要纯表情分析可以关掉省一点算力。min_detection_confidence和min_tracking_confidence默认 0.5光线差的环境可以降到 0.3 试试但误检会变多。3.2 手部检测21 个点与左右手判定手部方案输出每只手的 21 个关键点外加handedness告诉你这是左手还是右手。注意这个左右手判定是基于图像内容的不是基于用户视角前置摄像头有镜像时容易反做手势控制要留意。import cv2 import mediapipe as mp mp_hands mp.solutions.hands mp_drawing mp.solutions.drawing_utils cap cv2.VideoCapture(0) with mp_hands.Hands( static_image_modeFalse, # 视频流用 False启用跟踪 max_num_hands2, model_complexity1, # 0 快 1 准默认 1 min_detection_confidence0.5, min_tracking_confidence0.5 ) as hands: while cap.isOpened(): 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: for idx, hand_landmarks in enumerate(results.multi_hand_landmarks): mp_drawing.draw_landmarks( frame, hand_landmarks, mp_hands.HAND_CONNECTIONS) # 取手腕点(0)的像素坐标方便后续逻辑 h, w, _ frame.shape wrist hand_landmarks.landmark[0] cx, cy int(wrist.x * w), int(wrist.y * h) label results.multi_handedness[idx].classification[0].label cv2.putText(frame, f{label}, (cx, cy), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) cv2.imshow(Hands, frame) if cv2.waitKey(1) 0xFF 27: break cap.release() cv2.destroyAllWindows()逻辑说明static_image_modeFalse时内部会做帧间跟踪速度明显快于逐帧检测视频场景务必用 False。model_complexity控制网络规模0 适合低端设备1 是精度和速度的平衡点。参数说明max_num_hands2是常见上限设更大值收益很低且拖慢速度。手腕点索引是 0指尖是 4、8、12、16、20做手势判定时常用这几个点算角度。3.3 身体姿态33 个关键点与可见性分数Pose 方案输出 33 个身体关键点每个点带visibility表示该点被遮挡的概率估计。做动作计数时visibility 低于阈值的关键点要丢弃否则会算出离谱角度。import cv2 import mediapipe as mp mp_pose mp.solutions.pose mp_drawing mp.solutions.drawing_utils cap cv2.VideoCapture(0) with mp_pose.Pose( static_image_modeFalse, model_complexity1, smooth_landmarksTrue, # 平滑抖动动作分析建议开 min_detection_confidence0.5, min_tracking_confidence0.5 ) as pose: while cap.isOpened(): ok, frame cap.read() if not ok: break rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results pose.process(rgb) if results.pose_landmarks: mp_drawing.draw_landmarks( frame, results.pose_landmarks, mp_pose.POSE_CONNECTIONS) # 取左肩(11)和左肘(13)算夹角示例 lm results.pose_landmarks.landmark if lm[11].visibility 0.5 and lm[13].visibility 0.5: cv2.putText(frame, left arm visible, (30, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 0, 0), 2) cv2.imshow(Pose, frame) if cv2.waitKey(1) 0xFF 27: break cap.release() cv2.destroyAllWindows()逻辑说明smooth_landmarksTrue会在时间维度上做滤波代价是引入轻微延迟实时性要求极高的场景可以关。参数说明model_complexity同样分 0/1/22 最准但明显吃 CPU。关键点索引要记牢11/12 是左右肩13/14 是左右肘15/16 是左右腕23/24 是左右髋25/26 是左右膝27/28 是左右踝。注意三路方案不要在同一帧上串行调用三次process那样 CPU 占用会翻倍。常见做法是分线程或者按需只开一路具体取舍看你的业务重心。4. 把三路结果合到一帧坐标对齐与性能取舍4.1 归一化坐标怎么映射回像素三路输出的都是归一化坐标画图时 MediaPipe 的draw_landmarks会自动处理但你要做业务逻辑比如判断手是否碰到脸就必须自己转像素坐标。转换公式统一px int(x * frame_width)py int(y * frame_height)。z 值不参与像素映射只用于判断前后关系。def to_pixel(landmark, w, h): 把归一化关键点转成像素坐标并做边界裁剪 x min(max(int(landmark.x * w), 0), w - 1) y min(max(int(landmark.y * h), 0), h - 1) return x, y逻辑说明裁剪是必要的因为归一化坐标偶尔会略微超出 [0,1]直接乘出来可能是负数或越界画图或索引会出错。参数说明w 和 h 来自frame.shape[1]和frame.shape[0]注意顺序别搞反。4.2 三路同开的性能账我实测过一组数据同一台普通笔记本无独显640x480 分辨率下只开 Hands 大约 25 到 30 FPS只开 Pose 大约 20 到 25 FPS只开 Face Mesh 大约 15 到 20 FPS三路串行同开直接掉到 8 到 12 FPS。这个数字因机器而异但量级关系是稳定的。方案关键点数相对开销典型用途Face Mesh468高表情、眼神、面部特效Hands21 x 2中手势控制、手语Pose33中高健身计数、动作纠正取舍思路如果业务只需要手和脸就别开 Pose如果只需要骨架Hands 和 Face Mesh 都关掉。真要三路同开把分辨率降到 320x240或者用model_complexity0再或者把三路拆到不同帧轮流处理比如奇数帧跑手、偶数帧跑脸用时间换帧率。4.3 多线程读取摄像头避免阻塞OpenCV 的cap.read()是阻塞的主线程里读帧再推理帧率会被推理时间拖死。常见做法是开一个独立线程专门读帧主线程只取最新帧做推理。import threading import cv2 class CameraReader: def __init__(self, src0): self.cap cv2.VideoCapture(src) self.frame None self.running True self.lock threading.Lock() self.thread threading.Thread(targetself._update, daemonTrue) self.thread.start() def _update(self): while self.running: ok, frame self.cap.read() if ok: with self.lock: self.frame frame def read(self): with self.lock: return self.frame.copy() if self.frame is not None else None def release(self): self.running False self.thread.join() self.cap.release()逻辑说明读帧线程持续覆盖self.frame主线程拿到的永远是最新帧不会积压旧帧。加锁保证读写不冲突。参数说明daemonTrue让线程随主进程退出避免程序卡住不关。这个模式在需要实时性的场景里几乎是标配。5. 避坑与排查那些让我重装三次环境的问题5.1 摄像头读出来全黑或直接报错现象cap.read()返回 False或者画面全黑。原因摄像头被其他程序占用比如刚关掉的 Zoom 没释放或者索引 0 不是你的目标摄像头。解决换索引试cv2.VideoCapture(1)关掉占用程序Linux 上检查/dev/video*权限。我遇到过笔记本自带摄像头和 USB 摄像头索引互换的情况别死磕 0。5.2 关键点抖动严重动作计数忽多忽少现象手静止时关键点也在小幅跳导致角度阈值反复触发。原因逐帧独立检测没有时间平滑或者光照不足导致置信度在阈值附近波动。解决Pose 开smooth_landmarksTrue自己做计数时加一个状态机比如角度连续 3 帧超过阈值才计一次而不是单帧触发。这个「连续 N 帧」的后悔药能省掉大量误计数。5.3 左右手判定反了现象举右手显示 Left。原因前置摄像头默认有镜像而 handedness 基于图像内容判定。解决如果你做了cv2.flip(frame, 1)判定结果要相应取反或者干脆不依赖 handedness用关键点几何关系自己判断。这个坑在直播特效里特别常见翻车一次就记住了。5.4 内存缓慢增长直到卡死现象跑十几分钟后程序越来越慢内存占用持续上升。原因每帧都创建了新的绘图对象或结果没释放或者摄像头线程没做帧丢弃导致队列积压。解决绘图用draw_landmarks直接画在原帧上别每帧新建大数组读帧线程用覆盖模式而不是队列模式。用tracemalloc定位增长点比瞎猜快。5.5 打包成 exe 后模型文件找不到现象源码跑得好好的PyInstaller 打包后报模型路径不存在。原因MediaPipe 的.tflite模型文件在包里是数据文件打包时没被收集。解决用--add-data把 mediapipe 的 modules 目录带上或者改用--collect-all mediapipe。这个坑在交付阶段才暴露建议早点做打包测试。6. 从能跑到好用把关键点变成动作判定的几个技巧跑通三路检测只是起点真正决定项目能不能落地的是「怎么把关键点变成稳定的事件」。我一般会先定义一个角度计算函数用三个点算夹角这是动作判定的地基。import math def angle_between(a, b, c): 计算 b 点处的夹角a/b/c 均为 (x, y) 像素坐标 ang math.degrees( math.atan2(c[1] - b[1], c[0] - b[0]) - math.atan2(a[1] - b[1], a[0] - b[0]) ) ang abs(ang) return 360 - ang if ang 180 else ang逻辑说明用atan2算两条边的方向角再相减比向量点积更直观也避免除零。参数说明返回 0 到 180 度肘部伸直接近 180完全弯曲接近 30 到 40。做俯卧撑计数时肘角从大于 160 降到小于 90 再回到大于 160 算一次配合连续帧确认。第二个技巧是给每个关键点加一个「可信度门控」。Pose 的 visibility 低于 0.5 时这个点参与的角度计算直接丢弃宁可这一帧不判定也不要拿一个飘到画面外的点算出错误角度。我见过太多计数不准的案例根因都是没做这个门控。第三个技巧是归一化尺度。不同人离镜头远近不同像素距离没有可比性。做动作幅度判定时用「肩宽」或「躯干长度」作为基准做归一化比如「手腕到肩的距离除以肩宽」这样阈值才能跨人复用。这个细节决定了你的参数是只能自己用还是能给别人用。最后一个习惯把每一路的开关做成配置项而不是写死在代码里。调试时经常需要单独看某一路硬编码会让你反复改代码重启。我现在的模板里Face、Hands、Pose 各有一个布尔开关外加一个分辨率配置换场景时改配置就行。这套东西我踩了三四次坑才稳定下来希望帮到你。本文还有配套的精品资源点击获取