简介:一套基于MediaPipe的人体姿态识别Python源码与预训练模型,面向人工智能、计算机视觉及自动化等专业的毕设、课设与实际项目练习者。资源实现了从视频输入、关键点检测到行为分类的完整流程,内置7个Python脚本和120个npy特征数据,便于查看数据集组织与模型训练/推理逻辑;另有8个mp4演示视频,可直接查看不同动作的识别效果。包内还包含h5模型、v2配置文件及README说明,整体138个文件约11.04MB,结构紧凑,适合在导师指导下快速跑通。该项目源自大四毕业设计,经导师指导并评审获得96.5分,代码均测试成功,可放心下载。目前已有206人学习,也适合作为课程设计、期末大作业或初期立项演示基础;如运行中有环境配置问题,可通过私信获得远程教学支持。
1. 基于 MediaPipe 的人体姿态识别:为什么这套方案能直接在业务里落地
做动作识别时,很多开发者第一反应是找模型、标数据、跑训练,结果卡在第一步。基于 MediaPipe 设计实现人体姿态识别,python 源码加上模型包,恰恰把这条路径压缩成“装依赖、读帧、拿坐标”三步。它不要求你懂检测头或热力图回归,输出直接是 33 个关键点的归一化坐标和骨骼连线,普通的笔记本 CPU 就能跑实时。
但把 demo 变成能用的过程里,真正的麻烦不在调用代码,而在画面镜像、关键点抖动、角度计算方向、视频与图片模式差异这些细节点。这篇文章按“原理、上手、业务化、避坑、优化”的顺序展开,让新手能照着复现,也让有经验的人直接带走参数设置和踩坑记录。标题里提到的源码加模型,实际落地时最关键的恰恰是那三个置信度参数和一个状态机。
2. MediaPipe 姿态模型原理:33 个关键点与两段式推理到底在做什么
2.1 选型:为什么是 MediaPipe 而不是重模型或 17 点方案
人体姿态估计在开源社区有几条主流路线。一类是早期基于热力图回归的方案,关键点精度确实高,但模型体积大、推理依赖 GPU,普通笔记本 CPU 上很难跑满实时帧率。另一类是轻量化的 17 点方案,速度快,但点位集中在躯干和四肢大关节,缺少脚踝、脚跟、脚趾这些细节。对深蹲、硬拉、跳绳这类需要看下肢角度和脚部位置的动作,17 点方案在计算关节角度时会有明显误差。
MediaPipe 的 Pose 模型走的是另一条路线:先用一个轻量检测器在整帧中定位人体框,再用一个回归网络在框内输出关键点。检测器不是每帧都跑,而是当跟踪丢失时才重新触发;其余帧靠跟踪模块复用上一帧的关键点,做小范围修正。这种两段式设计把每帧平均计算量压得很低,CPU 上也能保持 30 FPS 左右,这是它适合做实时交互和动作计数的根本原因。
选型时还要看输出点位密度。MediaPipe Pose 输出 33 个关键点,覆盖鼻尖、双肩、双肘、双腕、双髋、双膝、双踝、双脚跟和脚趾,点位编号稳定且顺序固定。这意味着你可以把“取第 26 号点当右膝”写死在代码里,不需要做目标匹配。对业务开发来说,这个稳定性和可预期性比“精度更高但输出顺序可能变化”的方案更省事。
这里补一个容易误判的点:两段式架构里,跟踪模式依赖上一帧的关键点位置做预测。一旦画面里人快速移动、转身或被遮挡,跟踪会短暂丢失,触发重新检测。所以视频里出现偶尔的断帧不一定是模型坏了,很可能是跟踪失效后的正常重检。理解了这个机制,你才懂得为什么 min_tracking_confidence 这个参数值得单独调,而不是只动检测阈值。
2.2 33 个关键点索引表与 model_complexity 档位选择
常用关键点索引如下,完整 33 点的编号在 MediaPipe 官方文档和代码注释里都能查到:
| 索引 | 部位 | 索引 | 部位 |
|---|---|---|---|
| 0 | 鼻尖 | 23 / 24 | 左髋 / 右髋 |
| 11 / 12 | 左肩 / 右肩 | 25 / 26 | 左膝 / 右膝 |
| 13 / 14 | 左肘 / 右肘 | 27 / 28 | 左踝 / 右踝 |
| 15 / 16 | 左腕 / 右腕 | 29 / 30 | 左脚跟 / 右脚跟 |
| 17 / 18 | 左小指 / 右小指 | 31 / 32 | 左脚趾 / 右脚趾 |
注意这里的“左/右”指的是画面中的人体左侧和右侧。如果你用前置摄像头做自拍镜像,画面左右和你的真实左右是反的,这个问题在第 5 章单独讲,但写索引之前就要有这个意识。
model_complexity 参数控制模型档位,取值 0、1、2。0 对应轻量模型,体积小、推理快,适合低端设备或同时跑多个识别实例;2 对应重量级模型,关键点回归更准,对 z 坐标和遮挡情况改善明显,但 CPU 上帧率下降显著。我一般默认用 1,实时交互和计数场景都够用;只有拍摄距离固定、人体占画面比例较大、且对深度坐标有需求时才切到 2。
pose = mp_pose.Pose( static_image_mode=False, model_complexity=1, min_detection_confidence=0.5, min_tracking_confidence=0.5, )这四个参数里,前两个决定速度和精度的平衡,后两个决定检测与跟踪的灵敏度。min_detection_confidence 控制“多久认为画面里有人”,逆光或远距离时检测框忽隐忽现,把它降到 0.3 往往立竿见影,代价是背景里的假人体更容易触发检测。min_tracking_confidence 控制“跟踪质量差到什么程度就放弃跟踪、重新检测”,调高会让模型更频繁地重检,关键点更稳但更耗 CPU。
最后说归一化坐标。landmarks 里的 x、y 范围是 0 到 1,z 以髋部中心为原点,单位是相对值。换算成像素时用 x 乘画面宽度、y 乘画面高度,这个操作绝大多数新手都会漏,直接拿 0.4 这种小数去算距离,结果自然不对。visibility 字段表示关键点可见度,低于 0.5 的点通常是遮挡或出画造成的,业务逻辑里应当跳过。
注意:mediapipe 的 solutions.Pose 接口内置模型,不需要手动加载 zip 里的模型文件;如果项目里是独立的 .tflite 或 .task 模型,通常对应的是新版 PoseLandmarker 任务 API 或其他推理链路,加载方式完全不同,先确认代码里用的是哪套 API。
3. 本地跑通第一版识别程序:摄像头实时画面中的骨骼叠加
3.1 环境安装与最小验证
先把依赖装好。常见做法是用 pip 安装 mediapipe 和 opencv-python,前者提供姿态模型和推理封装,后者负责摄像头读取与画面绘制。
pip install mediapipe opencv-python装完先做一个最小验证,确认包能加载、模型能初始化,再往下写业务代码。
python -c "import mediapipe as mp; print('mediapipe ok')"如果 import 报错,优先检查 Python 版本。mediapipe 的预编译包对 Python 版本有要求,常见可用范围是 3.8 到 3.11,过新的版本可能还没有对应轮子。此时用 conda 或 pyenv 建一个 3.10 的虚拟环境再装,比手动编译省事得多。这一步卡住的人不少,但基本都是版本环境问题,换个虚拟环境就好,跟代码本身没有关系。
3.2 摄像头实时识别的核心脚本与参数说明
下面这段是完整的实时识别脚本,直接可用。逻辑不复杂:循环读摄像头帧、转 RGB、送入 Pose 推理、拿到关键点后绘制骨骼,再显示出来。
import cv2 import mediapipe as mp mp_pose = mp.solutions.pose mp_drawing = mp.solutions.drawing_utils pose = mp_pose.Pose( static_image_mode=False, model_complexity=1, min_detection_confidence=0.5, min_tracking_confidence=0.5, ) cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) while cap.isOpened(): ret, frame = cap.read() if not ret: break # OpenCV 读出来是 BGR,MediaPipe 要求 RGB 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, ) cv2.imshow("mediapipe pose", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()static_image_mode=False 表示进入视频跟踪模式,算法会复用上一帧的关键点做跟踪,帧率明显快于逐帧检测。model_complexity=1 是精度和速度的折中,前面已经说过。min_detection_confidence 控制人体检测阈值,调低更容易检出,也更容易误检;min_tracking_confidence 控制跟踪丢失后的重检测时机。
results.pose_landmarks 里的 x、y 是归一化坐标,范围 0 到 1,直接使用时必须乘上画面宽高换算成像素。绘制函数内部自动处理了转换,但你算角度、距离时如果不换算,数值会完全不对。z 坐标的原点在髋部中心附近,单位是相对值,不能当真实距离直接用,这一点后面还会再提。
3.3 图片和视频文件的输入差异与处理方式
处理单张图片时,把 static_image_mode 改成 True。这个模式下没有跨帧跟踪,每一张图都独立走完整检测,结果更稳定但更慢。对图片来说这个代价无所谓,对视频流来说就需要特别注意不要误开。
pose_img = mp_pose.Pose(static_image_mode=True, model_complexity=1) img = cv2.imread("test.jpg") rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) results = pose_img.process(rgb) if results.pose_landmarks: lm = results.pose_landmarks.landmark print("关键点数量:", len(lm)) for idx, pt in enumerate(lm): print(idx, round(pt.x, 3), round(pt.y, 3), round(pt.z, 3), round(pt.visibility, 2))打印时重点看 visibility 字段,它表示关键点的可见置信度,范围 0 到 1。低于 0.5 的点通常是遮挡或超出画面造成的,业务逻辑里应该跳过而不是直接参与角度计算,否则会出现手指到肘部这种完全错误的角度值。
视频文件处理和摄像头几乎一样,只需把 VideoCapture 的参数从 0 换成文件路径。常见坑是输入视频分辨率过高,比如 4K 视频直接送进模型,每帧处理时间成倍增长。我的习惯是先做一次 resize,把短边压到 720 以内,推理速度会快很多,精度损失在多数动作分析场景中可接受。输出视频时还要注意 cv2.VideoWriter 的编码参数要跟文件名后缀对应,否则生成的 mp4 可能打不开。
4. 把关键点变成业务逻辑:关节角度计算与深蹲计数
4.1 关节角度函数与计算方向
拿到 33 个关键点后,最常用的操作是计算三点夹角,也就是关节弯曲角度。比如深蹲看膝盖弯曲角度,需要取髋、膝、踝三个点,膝是中点。用向量夹角比用余弦定理更稳,因为 atan2 对接近 0 度和 180 度的小角度变化更敏感,不会像 arccos 那样在边界出现数值抖动。
import math def calc_angle(a, b, c): """计算三点夹角,b 是角的顶点,返回 0~180 度""" ax, ay = a[0], a[1] bx, by = b[0], b[1] cx, cy = c[0], c[1] rad1 = math.atan2(cy - by, cx - bx) rad2 = math.atan2(ay - by, ax - bx) deg = abs((rad1 - rad2) * 180.0 / math.pi) if deg > 180.0: deg = 360.0 - deg return deg调用时从 landmark 里取坐标,注意只取前两维 x、y,不要混入 z。z 坐标单位是相对值且抖动更大,混入平面角度计算会把本来稳定的角度曲线变成毛刺。如果确实想在三维空间里算角度,应该先对坐标做平滑,再计算空间向量夹角,而不是直接套二维公式。
def lm_point(lm, idx): return (lm[idx].x, lm[idx].y) # 深蹲右腿:右髋(24)、右膝(26)、右踝(28) hip = lm_point(lm, 24) knee = lm_point(lm, 26) ankle = lm_point(lm, 28) angle = calc_angle(hip, knee, ankle)计算方向要前后一致。同一个膝盖弯曲,取髋-膝-踝算出的角度和踝-膝-髋算出的补角不同,应该统一为“由近端到远端”的顺序,这样 0 度表示完全折叠、180 度表示伸直,语义清晰,后续写阈值判断时不容易混。
4.2 深蹲计数的状态机与阈值调参
有了角度就能做最典型的动作计数。直接的做法是“小于某个阈值算蹲下,大于某个阈值算站起”,但这样会在临界点反复触发,卡在边界时一次深蹲能被数成三次。更稳的做法是加一个布尔状态,形成小状态机:只有从“站起”到“蹲下”的边沿转换才计数一次。
squat_count = 0 is_down = False # 在每帧循环里 if results.pose_landmarks: lm = results.pose_landmarks.landmark hip = (lm[24].x, lm[24].y) knee = (lm[26].x, lm[26].y) ankle = (lm[28].x, lm[28].y) angle = calc_angle(hip, knee, ankle) if angle < 100 and not is_down: is_down = True if angle > 160 and is_down: is_down = False squat_count += 1100 和 160 是经验阈值,代表“蹲到大腿接近水平”和“站直”。不同动作标准要调整,比如半蹲可以把 100 改成 120,宽距深蹲因为踝关节活动度差异,可能需要把复位阈值降到 150。调参的方法是打印角度曲线,观察一个完整动作的峰值和谷值,把阈值设在两者之间,留出 20 度左右的裕量。
还有一个容易忽略的问题:单帧角度突变会直接改变状态。比如画面遮挡导致关键点跳变,角度从 90 度瞬变到 150 度,状态机就会误判一次。更稳的写法是连续 3 帧满足条件才确认状态翻转,或者在计算角度前对坐标做 EMA 平滑,付出的代价是动作判定会滞后 2 到 3 帧,对计数场景完全可接受。
# 单指数平滑:alpha 越大越跟手,越小越平滑 alpha = 0.4 smooth_x = alpha * new_x + (1 - alpha) * prev_x smooth_y = alpha * new_y + (1 - alpha) * prev_y5. MediaPipe 姿态识别避坑指南:5 个让识别翻车的实际问题
下面这 5 个问题是我见过最多的踩坑点,每个都按“现象、原因、解决”写清楚。提前看完,能省掉不少调试时间。
5.1 前置摄像头镜像导致左右关键点对调
现象是:做左手动作,代码取的是右腕坐标,计数逻辑全部失灵,角度数值也明显不符合生理结构。原因是前置摄像头画面是镜像的,MediaPipe 输出的是“画面中人体”的左右,而不是你真实的左右,代码里写死的左/右索引和实际操作的一侧对不上。解决办法是统一在送入 process 之前用 cv2.flip(frame, 1) 把画面翻转为正向,让画面左对应真实左,索引和业务逻辑一一对应。翻转必须在取关键点坐标之前做,否则坐标还是翻转前的。如果不想翻转画面,就手动交换左右索引:左腕 15 换右腕 16,左髋 23 换右髋 24,依此类推。
5.2 光照不足或逆光时检测框反复消失
现象是:帧率正常,但骨骼连线经常整段消失,动作被切成一段一段,计数完全没法做。原因是画面亮度过低或逆光导致人体检测置信度低于 min_detection_confidence,检测器认为画面里没有人,自然没有关键点输出。解决办法分两步:先改善物理环境,正面补光比任何参数都有效;再在代码层面把 min_detection_confidence 从 0.5 降到 0.3,让检测器更愿意出框。降阈值后背景里的假人可能偶尔触发检测,需要在业务层过滤,单人场景可以取画面中心附近的人体,或者用关键点的 visibility 均值做一次合理性校验。
5.3 关键点抖动让角度曲线毛刺严重
现象是:人静止站立时,手腕坐标仍在几个像素范围内跳动,画出来的骨骼边缘有小幅颤抖,角度曲线呈现明显噪声。原因是回归模型的输出本身带有随机性,跟踪模式下这种波动会被放大,另外画面分辨率低时归一化坐标的像素量化误差也很明显。解决办法是对参与角度计算的关键点做单指数平滑,alpha 取 0.3 到 0.5。不要用滑动窗口平均,因为它会引入明显延迟且对突变反应迟钝。更进阶的做法是只对计算用的坐标做平滑,绘制仍用原始点,这样画面保持跟手,角度曲线干净。
5.4 把 z 坐标当真实距离使用导致深度误判
现象是:用某个关键点的 z 值判断手离身体远近,结果忽远忽近,无法稳定触发交互逻辑。原因是 z 坐标不是真实深度,它是模型基于单目图像估计出的相对深度,原点在髋部中心附近,数值范围小,且在不同体型、不同拍摄距离下尺度不一致。解决办法是判断“手伸出去多远”时改用关键点之间的像素距离,比如手腕到肩膀的平面距离,再除以髋到肩的距离做归一化。这个比值和真实距离相关性更好,且不受人离摄像头远近影响。如果确实需要稳定的深度信息,把 model_complexity 切到 2 会让 z 坐标平滑一些,但仍然不能替代深度相机。
5.5 RGB 与 BGR 通道混用导致检测率骤降
现象是:画面显示颜色偏蓝偏绿,甚至完全检测不到人体,但代码看起来没有明显问题。原因是 OpenCV 的 imread 和 VideoCapture 返回的是 BGR 通道顺序,而 MediaPipe 的 process 接口要求 RGB,直接送 BGR 不会报错,但模型接收到的通道分布不对,特征响应明显变差。解决办法是统一在 process 之前调用 cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)。如果用 PIL 读图,PIL 默认就是 RGB,不需要再转一次;如果用 skimage 读图,它返回的数值范围是 0 到 1 的浮点型,需要先乘 255 再转 uint8。混合使用多个图像库时最容易出这类问题,建议全项目只走一条读取路径。
6. 进阶:从 Demo 到可用方案的性能优化与验证习惯
识别 demo 跑通后,接下来通常是卡顿问题。按见效程度排序,第一是降输入分辨率,把摄像头输出设为 640x480,或者对读取的帧先 resize 再推理,推理耗时几乎减半,关键点精度在动作分析场景里几乎不受影响。第二是隔帧推理,姿态跟踪模式本身有帧间连续性,每 2 帧推理一次、中间帧沿用上一帧关键点,帧率可以再提一档。第三是把摄像头读取和推理分开,用两个线程加一个队列,避免 USB 读取阻塞推理循环。三件事做完,绝大多数设备都能回到实时帧率。
| 优化手段 | 预期收益 | 代价 |
|---|---|---|
| 降分辨率到 640x480 | 推理耗时减半 | 小目标精度略降 |
| 隔帧推理(每 2 帧) | 帧率接近翻倍 | 判定延迟 1 帧 |
| EMA 平滑 | 角度曲线明显变稳 | 滞后 2~3 帧 |
| 读取与推理线程分离 | 消除 USB 读取卡顿 | 代码复杂度上升 |
验证环节我习惯在改完参数后先录一段 1 到 2 分钟的视频,人工标注动作次数,再跑算法对比。计数准确率低于 95% 时,优先怀疑阈值设置而不是模型本身,把角度曲线导出来看一个完整动作的峰谷形态,是最直接的排查手段。之前做模拟项目X 时,我把所有帧都送进 2 档模型,CPU 直接打满,帧率掉到个位数,后来才发现降分辨率加隔帧推理就能在画质几乎不变的情况下恢复实时。从那以后我习惯先看瓶颈在哪再决定调参方向,而不是一味堆精度。希望帮到你。
本文还有配套的精品资源,点击获取