☰
MediaPipe人体姿态识别实战:关键点检测与动作计数
2026/10/19 10:42:28 网站建设 项目流程

简介:面向计算机相关专业学生与实战学习者,这份基于MediaPipe实现的人体姿态识别Python源码及模型包,适用于毕业设计、课程设计、期末大作业或答辩演示等场景,适合有一定Python基础、希望接触姿态识别的中级学习者。资源共138个文件,压缩后约11MB,包含Python源码、H5格式训练模型、npy格式关键点/特征数据文件、mp4格式运行效果演示视频以及README说明文档,目录结构清晰,便于按模块查阅和二次开发。项目代码经测试运行稳定,功能完整,评审得分96.5分;配套的视频演示与说明文档可帮助理解姿态识别模型的加载、推理与结果可视化流程,npy数据文件也便于自行训练与实验对比。该资源已有206人学习下载,适合需要快速搭建人体姿态识别原型或完成课程项目的学生参考借鉴,从代码结构到实验结果均有完整呈现,压缩包内附带的README也对环境配置与运行方式做了说明。

1. 人体姿态识别到底识别什么:拿到“源码+模型”前先想清楚的三件事

人体姿态识别在工程上不是“认出画面里是一个人”,而是把人的骨架关键点逐帧抠出来——鼻子、肩膀、手肘、手腕、髋、膝、踝,一共 33 个点,每个点带归一化坐标和可见度。标题里的 mediapipe 是这个任务里性价比最高的方案,它把 BlazePose 的检测加跟踪管线封装成几行 Python 调用,普通 CPU 就能跑实时视频。拿到这类“Python 源码+模型”的压缩包,我建议你先别急着解压跑 demo,而是先确认三件事:依赖环境隔离没有、模型走的是 Solutions API 还是 Task API、以及你最终要的是“画出骨架”还是“算出动作指标”。这三件想清楚,后面几百行代码才不会白写。

2. 把 mediapipe 跑起来:环境搭建与第一个最小推理脚本

2.1 为什么要选 mediapipe 而不是自训模型

做人体姿态识别,技术路线大致有三条:一条是自己标注数据训练关键点回归网络,常见的有基于 HRNet、OpenPose 的思路,效果上限高但工程成本也高,你得准备算力和标注工具;另一条是用现成的人体检测器加关键点分支,组合起来当管线用;第三条就是 mediapipe 这种开箱即用的方案。

我一般会直接选 mediapipe,理由是它把“检测人体框 + 关键点回归 + 帧间跟踪 + 平滑滤波”这几件事打包好了。它的模型基于 BlazePose 架构,分 Lite、Full、Heavy 三个复杂度档位,同一个 Python 接口切参数就能换精度和速度。对于“实时摄像头监控”“健身动作计数”“体态打分”这类需求,你不需要自己训练模型,只需要处理它输出的关键点坐标。这也解释了为什么压缩包叫“源码+模型”——源码负责调推理和算业务指标,模型本身就是那几个几 MB 的 TFLite 文件。

2.2 安装与第一个实时推理脚本

先搭一个干净的虚拟环境,这一步能省掉后面大量依赖冲突的麻烦。我习惯用 venv 或 conda 单开一个环境,避免 mediapipe 的依赖和主业环境纠缠在一起。

python -m venv venv_pose source venv_pose/bin/activate # Windows 下是 venv_pose\Scripts\activate pip install mediapipe opencv-python numpy

装完后写一个最小的摄像头推理脚本,确认模型能加载、关键点能输出。下面这份代码是手动画点版本,故意不用 drawing_utils,方便你看清坐标是怎么落到像素位置的:

import cv2 import mediapipe as mp mp_pose = mp.solutions.pose cap = cv2.VideoCapture(0) # 0 是默认摄像头;改成视频路径也能读 with mp_pose.Pose( static_image_mode=False, # 视频流用 False:先检测人体,后续帧走跟踪 model_complexity=1, # 0=Lite 最快,1=Full 均衡,2=Heavy 最准 smooth_landmarks=True, # 对关键点做时序平滑,压抖动 min_detection_confidence=0.5, min_tracking_confidence=0.5) as pose: while cap.isOpened(): ok, frame = cap.read() if not ok: break # mediapipe 吃 RGB,OpenCV 读进来是 BGR,必须先转 rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results = pose.process(rgb) if results.pose_landmarks is not None: h, w, _ = frame.shape # 拿原图尺寸把归一化坐标换算成像素 for idx, lm in enumerate(results.pose_landmarks.landmark): x, y = int(lm.x * w), int(lm.y * h) cv2.circle(frame, (x, y), 3, (0, 255, 0), -1) cv2.imshow("pose", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()

这段脚本的逻辑很直白:读一帧 -> 转 RGB -> 丢给 pose.process -> 取 landmarks -> 把归一化坐标乘上宽高还原成像素坐标 -> 画点。这里每个关键点的 x、y 都落在 0 到 1 之间,z 表示相对臀部中心的深度,单位不是像素,后面算角度时要注意,别直接拿 z 当实际距离。

参数里最值得调整的是 model_complexity。做离线视频分析、画面里人比较小,我会拉到 2;跑实时直播或者树莓派这类弱设备,就降到 0。smooth_landmarks 建议保持 True,它本质是帧间平滑,能明显减少关键点抖跳。min_detection_confidence 和 min_tracking_confidence 都是 0 到 1 的阈值,前者管“多久重新检测一次人”,后者管“跟踪失效就放弃”,默认 0.5 够用,不需要动。

2.3 跑通后先做一个“黑匣子测试”

很多新手看到画面上有绿点就开始接业务逻辑,我建议你先花五分钟验证模型输出的稳定性。怎么做?找一段固定机位的视频,人站原地做几个简单的抬手动作,把 landmarks 的原始坐标打出来存成 CSV。然后检查两点:离镜头远的帧里手腕点的 visibility 是不是掉得厉害;动作过程里肩点和髋点有没有突然跳到另一侧。

这一步叫黑匣子测试,因为你不模型内部发生什么,只看输入输出的可靠性。如果抖动频繁,先怀疑 smooth_landmarks 没开或者阈值太低;如果人一动骨骼就“左右换手”,多半是检测置信度不够,模型重新触发检测把左右肩搞反了。这个测试做完,你对“源码+模型”里的模型部分就有底了,后面写任何计数逻辑都建立在稳定的输入上。

3. 拆解姿态识别流程:关键点索引、角度计算与可视化实现

3.1 33 个关键点的索引表与坐标语义

mediapipe Pose 输出的 33 个关键点是有固定顺序的,索引从 0 到 32。做动作分析时最常打交道的几个点,我列在下面:

索引关键点说明
0nose鼻子,判断人体朝向的辅助点
11 / 12left / right shoulder左右肩,躯干和手臂的分界
13 / 14left / right elbow左右肘,和肩、腕配合算肘角
15 / 16left / right wrist左右腕,臂动作的末端
23 / 24left / right hip左右髋,躯干和腿的分界
25 / 26left / right knee左右膝,算腿弯曲角度
27 / 28left / right ankle左右踝,腿动作的末端

每个 landmark 有四个属性:x、y(归一化坐标,0 到 1)、z(深度,相对臀部中心)、visibility(可见度,0 到 1)。

这里要特别提醒:x、y 是归一化的,画点或者算像素距离时要乘图像宽高;z 不是绝对深度,拿它很难反推真实距离;visibility 是很多工程问题的根源,人在画面边缘、肢体遮挡时这个值会掉到 0.3 以下,这种关键点必须过滤,否则后续角度计算会被带偏。

3.2 角度计算:用向量夹角而不是肉眼量

姿态识别的核心价值是“动作指标”,而动作指标的基石是关节角度。比如深蹲计数,本质是髋-膝-踝三点形成的膝角变化;屈肘动作,本质是肩-肘-腕三点形成的肘角变化。

计算三点角度最稳的方式是向量点积加反余弦。下面这个函数我用在无数个项目里,输入三个关键点的坐标,返回以中间点为顶点的角度值:

import numpy as np def calc_angle(a, b, c): """以 b 为顶点,计算 a-b-c 的角度(度)""" a = np.array(a[:2], dtype=np.float32) # 只取 x, y,不要 z b = np.array(b[:2], dtype=np.float32) c = np.array(c[:2], dtype=np.float32) v1 = a - b v2 = c - b cos_a = np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2) + 1e-9) cos_a = np.clip(cos_a, -1.0, 1.0) # 浮点误差可能让值超出 [-1,1] return np.degrees(np.arccos(cos_a))

逻辑说明:先把三个点的 x、y 取出来,构成两个向量 v1(a 指向 b)和 v2(b 指向 c)。用点积公式计算夹角余弦,再通过 np.arccos 转成弧度,乘 57.2958 变成角度。加 1e-9 是防止两个向量长度乘积为 0 时除零,np.clip 防止浮点误差导致 arccos 收到超出定义域的值。

参数上要注意两点:一是只取 x、y,不要混入 z。z 是深度估计,和 x、y 不在一个度量体系,混进来角度会凭空多出误差。二是这个函数算出的角度范围是 0 到 180 度,它不区分“膝盖往前弯”还是“往后伸”,如果业务需要区分,要额外结合关键点的位置关系判断。

3.3 用 drawing_utils 输出完整骨架

手动画点只能看到点,看不到骨骼连线。mediapipe 自带的 drawing_utils 能一次把 33 个点和连接线画出来,但要注意一个细节:draw_landmarks 需要的是 NormalizedLandmarkList 这个 proto 结构,不是普通列表。从 results 里拿到的正好是它,直接传就行。这里给出带完整可视化的版本:

import cv2 import mediapipe as mp mp_pose = mp.solutions.pose mp_drawing = mp.solutions.drawing_utils def draw_pose(frame, results): """把 pose 结果画到原图上,返回 BGR 图像""" rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) mp_drawing.draw_landmarks( rgb, results.pose_landmarks, mp_pose.POSE_CONNECTIONS, mp_drawing.DrawingSpec(color=(0, 255, 0), thickness=2), mp_drawing.DrawingSpec(color=(0, 0, 255), thickness=2)) return cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR)

这段代码的核心是 POSE_CONNECTIONS,它是一个定义好的关键点连接表,告诉 drawing_utils 哪些点之间要连线。DrawingSpec 的 color 用 RGB 顺序,第一个参数配的是关键点样式,第二个配的是连线样式。因为 mediapipe 处理的是 RGB 图,画完要再转回 BGR 给 OpenCV 显示或写入视频。

3.4 坐标系混乱是最大陷阱

做姿态识别时最容易翻车的地方不是模型调用,而是坐标系的来回倒腾。OpenCV 读图像是 BGR,mediapipe 推理要 RGB,画图又回到 BGR;关键点坐标是归一化的,画图要乘回像素,写 CSV 存分析又要存归一化原始值。一个项目里这几个变换交叉出现,稍不留神就会画出来的骨架错位或者偏移。

我自己的习惯是定一条铁律:推理前统一转 RGB,推理后统一转回 BGR,坐标一律保存归一化原始值,只在“画点”和“算像素距离”这两个环节临时乘宽高。这样所有下游逻辑,比如角度计算、计数状态机,都基于同一套归一化坐标,不会出现“训练时用的归一化值、推理时用的像素值”这种互相打架的局面。

4. 把关键点变成业务指标:深蹲计数与姿态评分的改造方案

4.1 状态机是计数的正确姿势

拿到膝角序列后,最直观的做法是“角度小于 100 就算一次深蹲”,但这种阈值判断在实际视频里会疯狂误计。因为关键点本身有抖动,角度会在阈值边缘反复横跳,一秒钟给你数出三四个深蹲。

正确的做法是引入状态机。把动作拆成“站立态”和“下蹲态”,只有从站立进入下蹲、再回到站立,这一整个闭环才计一次。加上冷却帧机制,防止状态切换瞬间的噪声干扰。下面这个类是深蹲计数的核心实现:

class SquatCounter: def __init__(self): self.state = "stand" self.reps = 0 self.cooldown = 0 # 状态切换后的冷却帧数 self.angle_buffer = [] # 最近几帧的膝角,用于平滑 def update(self, knee_angle, visibility_ok): """每帧调用一次,返回累计次数""" if not visibility_ok: return self.reps # 关键点不可信,这帧直接跳过 # 简单移动平均,窗口 5 帧 self.angle_buffer.append(knee_angle) if len(self.angle_buffer) > 5: self.angle_buffer.pop(0) angle = sum(self.angle_buffer) / len(self.angle_buffer) if self.cooldown > 0: self.cooldown -= 1 if self.state == "stand" and angle < 100: self.state = "squat" elif self.state == "squat" and angle > 150 and self.cooldown == 0: self.state = "stand" self.reps += 1 self.cooldown = 15 # 15 帧内不允许再次计数,防抖 return self.reps

逻辑说明:每次 update 先检查 visibility_ok,关键点不可见时直接返回,不参与状态变更。角度进入前先过一帧 5 窗口的移动平均,把单帧噪声压平。状态机只在“站立 -> 下蹲”瞬间锁状态,在“下蹲 -> 站立”并且冷却结束才计数。

参数上可以按场景调:深蹲是否到底的标准就是那个 100 度,如果你做的是半蹲,可以改到 120;站立判定 150 度对应膝盖接近伸直。cooldown 是防抖的核心参数,普通视频 10 到 20 帧合适,放慢动作视频要调大,否则一个慢速深蹲可能被计两次。

4.2 姿态评分:角度误差映射成百分制

健身纠正、体态打分这类需求不关心次数,关心“动作标不标准”。实现思路是把当前关节角度和标准动作的目标角度做误差映射,越接近满分越高。

def posture_score(current_angle, target_angle, tolerance=20): """ 把当前角度映射到 0-100 分 current_angle: 当前帧关节角度 target_angle: 标准动作的目标角度 tolerance: 偏差多少度以内算满分,超过多少度直接 0 分 """ diff = abs(current_angle - target_angle) if diff >= tolerance: return 0 return max(0, int(100 * (1 - diff / tolerance)))

这个函数的业务含义是:肘部标准角度是 90 度,当前测出来 95 度,tolerance 给 20,那得分就是 75 分。tolerance 决定打分松紧,给动作纠正类场景一般给 15 到 25,给教练展示场景可以更严格。实际工程里还要给多关节加权平均,比如深蹲时膝角权重 0.5、髋角权重 0.3、背角权重 0.2,合成一个综合分。

4.3 完整流程:从视频帧到带数据的输出视频

把上面几块拼起来,一个完整的“输入视频 -> 输出骨架叠加视频 + 实时计数”的管线是这样:

import cv2 import mediapipe as mp mp_pose = mp.solutions.pose input_path = "squat_test.mp4" output_path = "squat_result.mp4" cap = cv2.VideoCapture(input_path) fps = int(cap.get(cv2.CAP_PROP_FPS)) width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) writer = cv2.VideoWriter(output_path, cv2.VideoWriter_fourcc(*"mp4v"), fps, (width, height)) counter = SquatCounter() with mp_pose.Pose(model_complexity=1, min_detection_confidence=0.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 is not None: lm = results.pose_landmarks.landmark # 取左髋 23、左膝 25、左踝 27,算左腿膝角 hip = [lm[23].x, lm[23].y] knee = [lm[25].x, lm[25].y] ankle = [lm[27].x, lm[27].y] angle = calc_angle(hip, knee, ankle) visibility_ok = lm[25].visibility > 0.5 and lm[27].visibility > 0.5 reps = counter.update(angle, visibility_ok) cv2.putText(frame, f"REPS: {reps}", (30, 60), cv2.FONT_HERSHEY_SIMPLEX, 1.5, (0, 255, 0), 3) frame = draw_pose(frame, results) writer.write(frame) cap.release() writer.release()

这段代码把前面所有内容串起来了:calc_angle 算角度、SquatCounter 做状态机计数、draw_pose 画骨架、putText 叠加计数文本、VideoWriter 写输出视频。参数上注意 VideoWriter 的尺寸必须和源视频一致,否则打不开输出文件;编码器用 mp4v 兼容性最好。

5. 人体姿态识别常见问题排查:从装不上、跑不动到数错数的 5 个场景

5.1 安装后导入报错,protobuf 版本冲突

现象:执行 import mediapipe 时直接抛错,提示 RuntimeError 和 protobuf 相关,或者装完 mediapipe 后项目里其他模块开始异常。

原因:mediapipe 内部强依赖特定范围的 protobuf 版本,而你的项目之前已经装了一个更高或更低版本,pip 解析依赖时没有彻底降级,两个版本在运行时冲突。这是这类源码包最常见的第一道坎。

解决:不要在主环境里强装,临时开一个干净虚拟环境。如果已经装了,先卸载再重装:pip uninstall protobuf mediapipe 后重新 pip install mediapipe,让 pip 自己解析依赖版本。换用虚拟环境后这个坑基本不会再踩第二次。

5.2 摄像头画面左右颠倒,抬手抬错边

现象:对着摄像头抬手,画面里左肘角度变化,但计算用的右肘数据没反应,左右手完全反了。

原因:摄像头自拍场景通常有镜像显示,OpenCV 读到的原始画面本身没镜像,但人看到的是镜像;另外 mediapipe 的坐标体系以图像左上角为原点,你视觉直觉里的“左手”和坐标系里的左手可能是两回事。

解决:按业务实际需求决定是否 cv2.flip(frame, 1) 镜像画面。动作识别项目里,我建议不要镜像,统一按图像坐标系来处理左右侧;只有做展示类应用时才镜像画面。同时要确认代码里取的 13/14、15/16 索引和你动作侧一致。

5.3 人离得远一点,骨架就开始乱跳

现象:人物在画面中占比小,或者走动到画面边缘时,关键点突然飘移,肩点跳到背景上,visibility 明显下降。

原因:BlazePose 的检测器对目标尺寸敏感,人体框太小或部分出画时,回归出的关键点在语义上不可信。visibility 存在的意义就是告诉你“这个点别当真”。

解决:过滤条件是硬性的,凡是 visibility 低于 0.5 的关键点一律不参与角度计算和计数。另外尽量让人体在画面中占足够大比例;离线视频分析时可以把 model_complexity 提到 2,增加小目标下的关键点回归精度。

5.4 视频处理很卡,CPU 占用拉满

现象:跑实时摄像头时画面掉到十几帧每秒,处理 1080p 视频时动辄几分钟。

原因:最常见的是每帧都做完整检测,而不是进入跟踪模式;或者输入分辨率过高,模型在每一帧的推理时间被拉长。static_image_mode=True 是重灾区,不少人写循环时没注意这个参数。

解决:视频流必须保持 static_image_mode=False,这样只有第一帧(或丢失跟踪时)做完整检测,后续帧走帧间跟踪,推理开销小一个量级。其次先缩放图像到 640x480 左右再喂给模型,推理速度能翻倍。最终手段是 model_complexity 降到 0,精度损失换实时性。

5.5 计数逻辑不收敛,深蹲数出“半蹲”

现象:同一个深蹲视频跑两遍,结果一次计数偏多一次偏少;或者半蹲被算成了全蹲。

原因:计数阈值和冷却参数没有按实际视频调。半蹲的膝角可能到 110 度左右,如果阈值设在 100,半蹲就不会触发状态切换;但画面抖动可能让角度瞬间越过阈值,配合过短的冷却窗口就会误计。

解决:每次拿到新视频,先把 30 帧左右的关键点角度打印出来看一眼实际角度范围,再定阈值。全蹲膝角一般小于 90 度,半蹲在 100 到 130 之间。阈值和冷却时间要成对调:阈值卡得紧就把冷却拉大,阈值卡得松就缩小冷却,这两个是互锁的参数,不要只改一个。

6. 姿态识别落地最后一公里:模型导出、推理提速与离线验证

真正把“源码+模型”用进产品,还差三件事。第一件是接口选型。前面的代码用的是 mp.solutions.pose,这是经典 API,写演示和验证最快;但要做跨平台部署,我建议切到新的 Task API,也就是 PoseLandmarker。两者推理底层是同一套模型,但 Task API 的输入输出更规范,支持直接加载 .task 格式的模型文件,也支持视频流模式和 GPU delegate,在移动端落地时更省事。

第二件事是推理提速。除了前面说的缩放分辨率和 model_complexity=0,还有一个容易被忽略的技巧:减少重复检测。实时场景里人离开画面又回来,模型要重新检测,这瞬间 CPU 会拉高。PoseLandmarker 的 running_mode 选 VIDEO,底层会自动分配检测和跟踪的优先级,比手动每帧喂图更稳。另外如果业务只关心上半身动作,可以裁掉图像下半部分再送进去,输入小一圈,延迟低一截。

第三件事是离线验证,这一步最重要也最容易被跳过。我每次写完一个姿态识别方案,都会把关键点输出成 CSV,再用画图工具拉出角度曲线检查平滑度和异常跳点:

import csv import cv2 import mediapipe as mp mp_pose = mp.solutions.pose cap = cv2.VideoCapture("test.mp4") with open("landmarks.csv", "w", newline="") as f: writer = csv.writer(f) writer.writerow(["frame", "left_knee_angle", "right_knee_angle", "left_hip_visible", "right_knee_visible"]) frame_idx = 0 with mp_pose.Pose(model_complexity=2) 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: lm = results.pose_landmarks.landmark # 左右膝角分别算出来写入 CSV l_knee = calc_angle([lm[23].x, lm[23].y], [lm[25].x, lm[25].y], [lm[27].x, lm[27].y]) r_knee = calc_angle([lm[24].x, lm[24].y], [lm[26].x, lm[26].y], [lm[28].x, lm[28].y]) writer.writerow([frame_idx, l_knee, r_knee, lm[23].visibility, lm[25].visibility]) frame_idx += 1 cap.release()

把 CSV 拉进电子表格或者画图工具,一眼就能看出哪几帧角度突变、哪几帧 visibility 低到不可信。曲线光滑但偶发尖峰,就调 smooth_landmarks 或移动平均窗口;整段乱跳,先怀疑模型复杂度和画面里的人体占比。这套离线验证方法,比我调试实时画面时边看边猜效率高得多。

我自己的习惯是:任何“源码+模型”项目,动手改业务逻辑之前,先把这一套 CSV 验证流程跑通,确认输入数据是稳的。数据都不稳,后面堆再多状态机和评分函数都是白费。这个习惯帮我避开了很多只在演示视频里成立的方案。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询