资讯中心

MediaPipe人体姿态识别:CPU实时33关键点检测实战

📅 2026/9/30 15:22:40
MediaPipe人体姿态识别:CPU实时33关键点检测实战
简介人体姿态估计是计算机视觉的基础任务之一旨在从图像或视频中定位人体关键点并推断动作语义。传统方案如OpenPose依赖GPU算力难以在普通设备上实时运行。MediaPipe基于BlazePose模型通过“先检测人框再回归关键点”的两段式架构将姿态识别压低到CPU也能流畅跑动的区间单帧输出33个身体关键点并内置帧间跟踪在640x480分辨率下可达25FPS以上。这一特性让人体关键点检测得以应用于健身动作计数、肢体康复评估、智能交互等场景。本文将围绕MediaPipe的安装、推理代码、角度计算与工程落地展开讲解如何从视频流中提取关键点坐标并实现动作判断帮助开发者在无GPU环境下快速构建人体姿态识别原型。1. 为什么说 mediapipe 把人体姿态识别拉到了 CPU 也能实时跑的区间一个做动作识别的朋友问我笔记本没有独显OpenPose 卡成 PPT有没有什么方案能先把效果跑起来我的答案基本都是同一句话先看 mediapipe。基于 mediapipe 设计实现人体姿态识别核心价值在于把曾经需要 GPU 才能实时的姿态估计压到了普通 CPU 上单帧输出 33 个身体关键点在 640x480 分辨率下跑到 25 FPS 以上并不夸张。这个工程适合三类人要做动作比对和健身计数的应用开发者拿现成源码做毕设又想讲清楚原理的学生以及想快速验证姿态识别能不能落进自己业务里的产品原型负责人。这篇就按我从拿到源码包到跑通、到踩坑、再到把输出变成真实业务数据的顺序把整套东西讲透。2. 选型先想清楚mediapipe 的 33 点方案靠什么撑起实时推理2.1 BlazePose 的两段式结构先找人框再回归关键点mediapipe 的 pose 方案底子是 BlazePose它的速度快不是靠魔法而是靠两段式设计第一段是一个轻量的人体检测器负责在整张图上找到人的位置并给出包围框第二段才是关键点回归器只在检测框内部做精细回归输出 33 个身体关键点。这个“先框人再回归”的取舍非常关键。OpenPose 这类方案是整张图跑一遍关节亲和场算的是全图每个像素到关节的关联精度高但计算量也大。BlazePose 把重计算限制在人物框内同时在视频流场景下会用上一帧的关键点位置做跟踪对齐也就是说同一帧里不需要重新检测人只需要在预测位置附近回归一次关键点。所以它在 CPU 上能跑出接近实时的帧率。我在看源码包时一般会先确认这个两段式逻辑有没有被改动。很多基于 mediapipe 的工程实际只调用了mp.solutions.pose那段“先找框再回归”的逻辑完全封装在官方库里源码包真正写的部分是循环推理、坐标提取和动作判断。理解了这个结构你就知道为什么这类源码包的推理代码通常只有一两个文件而真正有看头的是后续怎么处理和判断关键点。2.2 33 个关键点坐标系每个点都带着 x、y、z 和 visibilitymediapipe 的 pose 输出是 33 个NormalizedLandmark每个点包含四个字段x、y是归一化坐标除以了图像宽高范围 0 到 1z是深度估计值visibility是模型对该点的置信度范围也是 0 到 1。关键点索引不是随便排的。头部和面部占 0 到 10躯干和上肢在 11 到 22下肢在 23 到 32。日常做动作判断最常用的是这么几个索引名称典型用途0鼻尖头部朝向基准11 / 12左肩 / 右肩肩部水平、含胸判断13 / 14左肘 / 右肘手臂弯曲角度15 / 16左手腕 / 右手腕手部位置23 / 24左髋 / 右髋骨盆基准、与肩线对比25 / 26左膝 / 右膝大腿小腿夹角27 / 28左踝 / 右踝支撑脚判断z轴这一段特别容易误导新手它是以髋部中心为原点的相对深度值不是真实的三维空间坐标。这意味着你可以用它判断人的前后倾、躯干旋转趋势但不能拿去做精确的 3D 重建或测距。我在源码包看到有人直接拿z算两臂长度差结果误差很大就是这个原因。2.3 对照 OpenPose 和 HRNet为什么源码包都爱选 mediapipe很多人体姿态识别源码包选 mediapipe 而不是 OpenPose 或 HRNet原因在工程落地的几个硬指标上维度OpenPoseHRNetmediapipe poseCPU 实时很难很难可跑 25 FPS 左右GPU 依赖强依赖强依赖无独显也能跑关键点数25含脸手有更多17 / 21 等33安装成本需编译或下大体积包需配置环境与权重pip 直接安装帧间跟踪无内建无内建内置跟踪机制模型文件独立下载较大独立下载随 pip 包按需加载对做产品原型和毕设的人来说mediapipe 几乎是最短路径不需要提前下载几十 MB 的权重文件放到指定目录不需要为 CUDA 版本头痛pip 装完直接能跑。它也正好满足“低显存运行模型”这批诉求——集显笔记本、树莓派这类设备都能扛住。代价是精度上限比大模型低尤其在肢体交叉、遮挡严重时关键点会漂移这个我在第 5 章会展开讲。2.4 拿到“python 源码 模型.zip”后先看什么解压完源码包第一件事不是双击运行而是先扫一眼目录结构。我经手过的这类源码包无论谁写的结构基本都逃不出下面几个部分目录/文件常见内容作用main.py/pose_demo.py主入口打开摄像头或读取视频跑推理循环pose_utils.py角度计算、坐标提取函数把 33 点原始坐标变成可判断的关节角models/.tflite/.pth等权重文件自训练模型或离线模型存储requirements.txt依赖清单pip 一键安装依赖data/或output/测试视频或保存结果跑完验证用的素材和输出目录注意看入口脚本里有没有写死模型路径。如果代码里只有mp.solutions.pose.Pose(...)而没有加载自定义模型路径说明走的是 mediapipe 内置模型models 目录里那部分可能是给离线部署或二次训练准备的替代方案。先把这个关系理清后面跑代码就不会出现“模型没加载”的幻觉问题。3. 把环境搭到能跑mediapipe 安装与 Python 版本那点事3.1 用虚拟环境装 mediapipe一条命令避开系统版本混乱的后悔药装 mediapipe 之前强烈建议先建一个独立的 Python 虚拟环境。我见过太多人把包直接装进系统 Python跑了个把月后因为另一个项目要装旧版 TensorFlow把 mediapipe 的依赖给顶掉了整个环境直接报废。虚拟环境就是后悔药随时删掉重建。# 建虚拟环境名字叫 pose_env python -m venv pose_env # Windows 激活 pose_env\Scripts\activate # Linux / macOS 激活 source pose_env/bin/activate # 激活后安装依赖 pip install mediapipe opencv-python numpy这段命令里python -m venv是 Python 自带的虚拟环境工具不需要额外安装。激活后终端提示符前面会出现(pose_env)看到这个再继续装包。pip install mediapipe opencv-python numpy一次装齐三件套mediapipe 是推理核心opencv-python 负责读图和显示numpy 用来做坐标和角度运算。这里有一个非常现实的坑mediapipe 对 Python 版本支持是分阶段的并不是最新 Python 一定能装。如果你用 Python 3.12 以上版本pip install mediapipe报“找不到匹配版本”先别急着换源换镜像大概率是当前 mediapipe 版本还没出对应 Python 3.12 的 wheel。换成 Python 3.9 或 3.10 重开一个虚拟环境基本能解决。这也是很多人会搜“mediapipe 安装”看到一堆版本问题帖的原因之一。3.2 验证安装并跑通第一个最小推理先读图片而不是上摄像头环境装好后先用一张静态图片验证整条链路通不通比直接开摄像头省心得多。如果摄像头读取或者实时显示出了问题你会分不清是环境问题还是代码问题。import cv2 import mediapipe as mp mp_pose mp.solutions.pose mp_draw mp.solutions.drawing_utils # 读入一张包含人像的图片路径换成你自己的图 img cv2.imread(test.jpg) # mediapipe 内部按 RGB 处理BGR 转 RGB 是必须的 rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 这里用 static_image_modeTrue单张图片不做帧间跟踪 with mp_pose.Pose(static_image_modeTrue, model_complexity1) as pose: result pose.process(rgb) if result.pose_landmarks: # 在原图上绘制骨架和关键点画完再转回 BGR 显示 mp_draw.draw_landmarks( img, result.pose_landmarks, mp_pose.POSE_CONNECTIONS) # 打印鼻子关键点的归一化坐标 nose result.pose_landmarks.landmark[mp_pose.PoseLandmark.NOSE] print(fnose: ({nose.x:.3f}, {nose.y:.3f}, {nose.z:.3f})) cv2.imshow(pose_test, img) cv2.waitKey(0) cv2.destroyAllWindows()这段代码的逻辑顺序很典型读图、转 RGB、推理、画骨架、显示。static_image_modeTrue告诉模型这张图是独立的不做跨帧跟踪适合单张图片测试model_complexity1是默认档位对应完整版模型速度和精度比较平衡。打印鼻子的归一化坐标是为了确认输出确实到了如果这一步能看到坐标数字说明 mediapipe 本体已经正常工作后面折腾摄像头才有个可靠前提。3.3 requirements 里没写全的依赖缺什么补什么别信“一条命令跑通”很多源码包的requirements.txt写得并不完整常见的是只写了mediapipe和opencv-python但代码里其实还用了scipy做关键点平滑、pandas存 CSV、matplotlib画曲线。遇到这种问题表现就是跑起来后突然在某个import scipy处报ModuleNotFoundError。处理方法分两步。先看报错的是哪个模块直接pip install缺的那个包。注意要装在当前激活的虚拟环境里别装到外面。其次是养成一个习惯拿到源码包第一晚就完整跑一遍入口脚本把所有缺失依赖一次性补齐后面调参时才不会反复被环境问题打断。VSCode 用户记得在左下角把 Python 解释器切到pose_env终端里确认which python指向虚拟环境路径这一步错了会在“已经装了但 import 不到”的迷惑状态里卡很久。4. 把人体姿态识别推理代码跑起来从视频流到动作角度4.1 初始化 Pose 实例model_complexity 和置信度阈值怎么配合进入视频推理前先花一分钟把Pose()初始化参数说清楚。这些参数直接决定你后面是“骨架跟手”还是“关键点乱跳”。# 初始化姿态识别器 pose mp_pose.Pose( static_image_modeFalse, # False 表示视频流模式启用帧间跟踪 model_complexity1, # 0fast 1full 2heavyCPU 建议 1 smooth_landmarksTrue, # 打开关键点平滑减少抖动 min_detection_confidence0.5, # 首次检测到人所需的最低置信度 min_tracking_confidence0.5 # 跟踪丢失判定阈值低于此值重新检测 )static_image_mode如果设成True每一帧都会当成独立图片重新找人不利用上一帧的位置信息速度会明显下降视频场景必须设为False才能吃到跟踪的加速收益。model_complexity三档对应三种模型0 是最轻量的 lite速度快但精度弱1 是 full默认值CPU 上速度和精度最平衡2 是 heavy关键点更稳但 CPU 上帧率可能掉到 15 FPS 以下。smooth_landmarksTrue会做帧间滤波代价是细微动作响应稍慢但换来的是画面上少很多抖动。min_detection_confidence和min_tracking_confidence是两只“看门狗”前者管“这一帧有没有人”后者管“上一帧跟踪的人这一帧还跟不跟得住”。如果你发现画面里的人物骨架经常凭空消失把min_detection_confidence调低到 0.3 到 0.4如果发现背景里有人一晃而过导致骨架跳到别人身上就把两个值都往上调到 0.6 以上。后者的调法会在第 5 章展开。4.2 逐帧推理与骨架绘制mediapipe 官方绘图工具的最小代码视频推理的主循环其实不长核心就是“读帧、转 RGB、推理、画骨架、显示”这五步反复执行。import cv2 import mediapipe as mp mp_pose mp.solutions.pose mp_draw mp.solutions.drawing_utils cap cv2.VideoCapture(0) # 0 是默认摄像头改成路径就是读视频文件 frame_w int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) frame_h int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) # 用 4.1 节的参数初始化 pose mp_pose.Pose( static_image_modeFalse, model_complexity1, min_detection_confidence0.5, min_tracking_confidence0.5) while cap.isOpened(): ok, frame cap.read() if not ok: break # 推理前把 BGR 转 RGBmediapipe 的输入约定要遵守 rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) result pose.process(rgb) if result.pose_landmarks: # 在原始 BGR 帧上绘制骨架颜色可以自行调整 mp_draw.draw_landmarks( frame, result.pose_landmarks, mp_pose.POSE_CONNECTIONS, mp_draw.DrawingSpec(color(0, 255, 0), thickness2, circle_radius2), mp_draw.DrawingSpec(color(0, 0, 255), thickness2)) cv2.imshow(pose, frame) if cv2.waitKey(1) 0xFF 27: # ESC 退出 break cap.release() cv2.destroyAllWindows()这段代码是这类源码包最内核的骨架几乎所有动作判断逻辑都长在if result.pose_landmarks:这个分支里面。draw_landmarks的两个DrawingSpec参数分别控制关键点圆点和骨架连线的样式第一个是所有点的绘制规范第二个是连线。这里我习惯把点画成绿色、线画成红色在绿色背景下比默认配色更清晰。waitKey(1)是 OpenCV 显示画面的必须步骤去掉它窗口会直接无响应。如果你拿到的源码包里有模型目录但入口代码用的还是mp.solutions.pose不要怀疑它就是在跑 mediapipe 内置模型那个模型目录是给后续替换或二次训练准备的。真正要替换模型时入口代码通常会出现torch.load(models/xxx.pth)或interpreter.allocate_tensors()这类调用跟上面这个官方 API 是两套体系。4.3 从关键点到动作判断用夹角函数识别“抬手”与“下蹲”画出骨架只是第一步业务里真正要的是“现在手抬起来了没有”“这个人是不是蹲下去了”。关键点坐标本身不是动作要转成角度和相对距离。import math def calc_angle(a, b, c): 计算三点夹角单位度。b 是角的顶点。 # 将坐标转成向量 ab (a[0] - b[0], a[1] - b[1]) bc (c[0] - b[0], c[1] - b[1]) # 点积 / 模长 夹角的余弦值 dot ab[0] * bc[0] ab[1] * bc[1] norm_ab math.hypot(ab[0], ab[1]) norm_bc math.hypot(bc[0], bc[1]) if norm_ab 0 or norm_bc 0: return 0.0 cos_angle max(-1.0, min(1.0, dot / (norm_ab * norm_bc))) return math.degrees(math.acos(cos_angle))这个函数是动作判断的地基。以左肘为例取左肩坐标shoulder、左肘坐标elbow、左手腕坐标wrist调用calc_angle(shoulder, elbow, wrist)得到的就是手臂在肘部的弯曲角度。当手臂完全伸直时这个角接近 180 度大臂贴小臂折叠时接近 0 度。“抬手”判断就可以写成“肘角小于 45 度持续 5 帧”这样具体的规则。拿到角度后从result.pose_landmarks.landmark[]里取坐标时要记得把归一化坐标乘以图像宽高转回像素坐标再算角度。可以直接用归一化坐标算因为反正最后都是比值但在调试打印坐标时转成像素才直观h, w, _ frame.shape landmarks result.pose_landmarks.landmark # 取左肩、左肘、左手腕的像素坐标 shoulder (landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER].x * w, landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER].y * h) elbow (landmarks[mp_pose.PoseLandmark.LEFT_ELBOW].x * w, landmarks[mp_pose.PoseLandmark.LEFT_ELBOW].y * h) wrist (landmarks[mp_pose.PoseLandmark.LEFT_WRIST].x * w, landmarks[mp_pose.PoseLandmark.LEFT_WRIST].y * h) arm_angle calc_angle(shoulder, elbow, wrist) print(fleft arm angle: {arm_angle:.1f})膝关节、髋关节的角度判断同一个套路替换索引即可。源码包里的pose_utils.py本质上就是这一套函数的堆叠区别只在于有人把角度判断和动作判断混在一个循环里写死了阈值有人做成了配置文件。我在后面第 6 章会演示怎么把这些角度组合成一个真正的计数逻辑。5. 避坑人体姿态识别跑起来之后的五个翻车现场5.1 import mediapipe 直接崩 DLL load failed现象pip install mediapipe成功但import mediapipe报DLL load failed while importing mediapipe或者直接闪退。原因最常见的是 Python 装成了 32 位版本而 mediapipe 的 wheel 只提供 64 位构建。其次是 Windows 系统缺了 Microsoft Visual C Redistributable 运行库mediapipe 底层的 C 扩展加载不起来。这类问题跟代码无关环境不对装几遍都一样。解决先确认 Python 位数——在终端里跑python -c import platform; print(platform.architecture())输出必须包含(64bit, WindowsPE)或带64bit。如果是32bit卸载后重装 64 位 Python再用虚拟环境重装依赖。运行库缺失的话把系统里已安装的“Visual C Redistributable”更新到较新版本再重试。这个顺序能解决九成以上的环境崩溃问题。5.2 首次运行卡在初始化黑色窗口一动不动现象代码能跑摄像头画面还没出来程序就卡死在pose.process()第一次调用终端无任何报错。原因mediapipe 在首次使用 pose 方案时需要准备模型文件这个过程对网络和本机状态比较敏感。模型没准备好时进程会一直挂在初始化阶段表现就是“卡住”而不是报错。解决先确保当前网络环境能正常访问外网然后耐心等一次完整下载后面再跑就是本地加载了。如果你的源码包里models/目录有现成的.tflite文件检查入口代码是否支持离线加载路径很多源码包只是把模型文件放了进去但实际代码并没有调用这种要手动改一下模型加载方式。判断模型文件有没有被真正用上我一般会在代码里加一行打印mp_pose.__file__看包路径再从入口脚本反向追踪模型加载逻辑。5.3 画面里同时出现两个人骨架瞬间乱跳现象一个人在前面做动作后面有人路过骨架突然跳到了路过的人身上或者两个人之间反复横跳。原因mp.solutions.pose的设计目标是单人体态跟踪它的内部跟踪器在多人场景下会把检测框锁定在置信度最高的那个人身上目标一换人输出自然乱。解决如果业务允许把镜头对准单人或者用 OpenCV 做人脸/人头检测框定范围只在框内跑 pose。更激进的做法是换成mp.solutions.pose之外的多人方案比如 MediaPipe 的 pose 在某些版本下通过循环裁切暴力模拟多人但帧率会大幅下降。对多数项目控制拍摄范围比改算法模型更省事。5.4 摄像头预览是镜像的动作判断左右全反现象画面上看到的人是左右镜像的抬起左手画面里像右手导致动作判断逻辑反过来。原因摄像头预览为了符合人的习惯通常会做水平镜像翻转但推理是在原始帧上做的。也就是说你看到的是翻转后的画面而模型看到的是未翻转的原图左右肩索引对不上。解决统一输入策略。要么在读取摄像头后立即用cv2.flip(frame, 1)翻转然后用翻转后的帧去做推理和绘制保持“所见即所判”要么完全不翻转接受左现实、右画面。我一般选前者因为体验更自然。关键点是推理和绘制必须用同一个帧不能预览用翻转帧、推理用原帧这个不一致是很多动作判断奇奇怪怪的直接来源。5.5 暗光、逆光、人与背景颜色相近时关键点漂移现象白天窗户前做动作半边身体的关键点忽亮忽暗手腕的坐标在手臂和背景之间来回跳。原因mediapipe 的 lightweight 模型在遮挡、低对比度、强光下的鲁棒性有限。关键点回归本质是在特征图上做预测背景和前景分不出来时回归结果就会被背景纹理带跑。解决拍摄环境上做调整最有效——镜头略高于头部让人体在画面里占三分之二高度避免大面积玻璃或强光源进入背景光线尽量均匀不要半边脸亮半边脸暗。代码层面把min_detection_confidence和min_tracking_confidence提高到 0.6 到 0.7让模型宁可不输出也不要输出低置信度的预测。还有一个我自己常用的技巧对连线的长度做合理性校验比如前 30 帧统计手臂长度的中位数当某帧手臂长度偏离中位数 30% 以上时直接丢弃该帧的关键点不要进入角度计算。这个防护表面看是玄学实际非常有效。6. 让输出真正可用关键点数据的三种落地技巧6.1 把每帧坐标写进 CSV跑一次实验留一份底调参最怕的是“刚才那个效果没保存”。我习惯在推理循环里把关键点坐标追加到一个列表结束时用pandas写 CSV。每列按“帧号_部位_x/y/z/visibility”命名后期画曲线、做对比都有据可查。import csv rows [] # 在循环内rows.append([frame_id, nose_x, nose_y, nose_z, nose_vis, ...]) with open(pose_output.csv, w, newline) as f: writer csv.writer(f) writer.writerow([frame, nose_x, nose_y, nose_z, nose_vis]) writer.writerows(rows)6.2 深蹲计数的最小状态机别一上来就上 LSTM动作计数最稳的不是神经网络是状态机。深蹲可以抽象成“站立 → 下蹲 → 恢复站立”三个状态每次走完一遍计数加一。state stand count 0 # 每帧计算髋部与膝部的相对高度比 hip_y landmarks[mp_pose.PoseLandmark.LEFT_HIP].y knee_y landmarks[mp_pose.PoseLandmark.LEFT_KNEE].y ratio hip_y / knee_y # 大于 1 表示髋部高于膝部 if state stand and ratio 1.3: state squat # 髋部高于膝部进入下蹲 elif state squat and ratio 1.1: state stand # 髋部接近膝部恢复站立 count 1阈值 1.3 和 1.1 不是通用值要根据镜头高度和人物距离重新标定。标定方法很简单站直测一次 ratio蹲到底测一次 ratio取两者中间值两个阈值。这个 trick 是从动作捕捉项目里带出来的习惯比对着网上的“标准值”硬抄靠谱得多。6.3 把坐标打包成 JSON给前端或游戏引擎用的输出格式如果下一步接 Unity 或网页可视化最省事的是每帧组装一个 JSON dict而不是传 33 个浮点数组。Unity 侧只需要解析landmarks[11]就能拿到左肩坐标前端 Three.js 同理。data { frame: frame_id, left_shoulder: [ls.x, ls.y, ls.z], left_elbow: [le.x, le.y, le.z], left_wrist: [lw.x, lw.y, lw.z], arm_angle: arm_angle }我第一次拿这个工程做动作计数时把阈值硬写在循环里调了一下午后来才意识到阈值标定本身就是整个项目里最需要反复验证的部分于是改成配置文件加 CSV 回放的方式之后调参效率高了很多。希望帮到你。本文还有配套的精品资源点击获取

看完文章,想为自己的企业也做一次专业网站诊断?

尧图顾问免费为您评估现有网站,并给出建站/改版建议与报价方案。

免费获取方案