Python+MediaPipe实现AI健身评分系统:关节角度与动作质量量化

发布时间:2026/9/28 21:36:56
Python+MediaPipe实现AI健身评分系统:关节角度与动作质量量化 简介这是一套基于Python搭建的AI健身评分系统实现资源面向姿态估计、动作识别及运动分析方向的开发者与健身科技爱好者可应用于体育训练辅助、动作规范检测等场景。项目以举哑铃动作为例先提取人体关键点再计算骨骼向量角并与标准动作实时比对最终输出各肢体评分和总评分。压缩包共114个文件内容覆盖源代码、训练好的h5/pb模型、依赖库与脚本、启动工具、演示视频和说明文档其中包含41个Python源码以及C/Cython扩展模块整体约225.1MB目录结构便于按模块查阅。目前已有42人学习下载。资源提供了从关键点检测、特征计算到评分展示的完整工程文件配合配套文章可快速掌握姿态估计在健身评分中的落地思路适合作为动作评估类项目的开发基础或毕业设计参考。1. 健身评分系统到底在评什么不是数次数是把动作质量变成分数健身App里的自动计数先火过一轮可真正落到私教场景时你会发现次数是最没价值的信息。一个只下蹲到120度的半程深蹲和一个蹲到90度以下的标准深蹲计数都是1但前者对股四头肌的刺激可能只有后者的一半。健身评分系统要解决的正是把“动作完成质量”量化成用户能感知的分数。标题里的 Python 搭建 AI 健身评分系统本质上是一条完整的视觉计算流水线用摄像头采集画面用姿态估计模型从单帧图像中提取人体骨架关键点再按动作标准计算关节角度、动作速度和稳定性最后映射成 0 到 100 的分数。这个方案适合三类人做健身App想要自动纠错反馈的产品团队做体育测试系统需要客观评分标准的工程师以及想在本地跑通一个 PythonAI 视觉项目的人。整套链路不依赖云端一台普通笔记本就能跑出可用效果。业务逻辑的设计顺序是先选姿态估计的骨架方案再把角度算准接着把它转成规则引擎下的分数最后用防抖和置信度过滤把分数稳定下来。这个顺序决定了整套实现的主线。需要先说明的是后面的内容默认你具备基础的 Python 语法水平能读懂函数和字典取值如果你还处在学入门 Python 教程的阶段建议先把代码复制跑起来再回头把每一行拆着看。2. 技术选型与原理为什么用 MediaPipe Pose 算关节角度而不是直接训练分类模型2.1 MediaPipe Pose 的 33 个关键点轻量级骨架方案为什么比 OpenPose 更合适当我第一次搭建健身评分系统时候选方案只有三个OpenPose、MediaPipe Pose 和 YOLO-Pose。OpenPose 精度确实高但 Python 环境配置要装 Caffe 和一堆 CMake 依赖模型权重动辄两三百MB在 8GB 内存的笔记本上做实时推理每帧要 100ms 以上。YOLO-Pose 更偏向目标检测场景对单人的健身动作反而杀鸡用牛刀。MediaPipe Pose 是当时平衡得最好的选择模型权重只有几十MBpip 安装一次到位CPU 上单帧推理大约 20 到 50ms更关键的是它直接输出 33 个归一化关键点坐标省掉了自己写坐标解码的步骤。这 33 个关键点覆盖了健身评分需要的全部部位左右眼、鼻子、耳朵属于头面部左右肩、肘、腕是手臂左右髋、膝、踝、脚趾是下肢。对应 MediaPipe Python SDK 里的mp_pose.PoseLandmark枚举每个关键点都有固定索引。深蹲和俯卧撑主要用肩、髋、膝、踝四个关节开合跳会多用到肘和腕。索引从 0 到 32建议在代码里始终用枚举名而不是裸数字比如mp_pose.PoseLandmark.LEFT_KNEE.value。这样代码可读性高后续扩展动作时也不会把左右手弄混。在mp_pose.Pose构造函数里有三个关键参数需要理解model_complexity、min_detection_confidence、min_tracking_confidence。model_complexity取 0、1、2 三档0 是速度最快精度最低2 是精度最高速度最慢。我的经验是 CPU 上默认取 1GPU 上可以取 2如果目标是移动端取 0 也可接受。min_detection_confidence是首次检测到人体时需要的最低置信度min_tracking_confidence是后续帧里持续跟踪关键点所需置信度。默认 0.5 对大多数场景够用如果画面里明显有人但检测不到把前者调到 0.7如果关键点乱跳把后者调到 0.8。调参时先固定摄像头分辨率和 Python 解释器版本再动这两个值否则最后很难归因是哪一步改动导致结果变化。2.2 坐标读取与归一化直接拿原始像素算角度是第一个大坑MediaPipe 返回的landmark.x和landmark.y不是像素坐标而是归一化坐标。x表示关键点在图像宽度上的比例y表示在图像高度上的比例范围都在 0 到 1 之间z是相对髋部中心的深度值数值的相对意义大于绝对意义。如果直接把这些值当成像素坐标去画图、去算距离做出来的效果会完全错误。要还原成像素坐标必须拿到当前帧的宽高再乘回去# 还原像素坐标的示例frame_w 和 frame_h 是当前帧的宽高 pixel_x int(landmark.x * frame_w) pixel_y int(landmark.y * frame_h)在健身评分系统的代码里这个乘法只在需要把关键点叠加绘制到画面上的时候才用得上。关节角度计算需要的本质是“方向”而两个关键点之间的方向由坐标差决定。归一化坐标本身就是各向同性的缩放只要宽高比例一致算出来的角度不会受影响。所以我在实现里把归一化坐标直接喂给角度计算函数省掉一次乘法和一次坐标转换还避免了浮点精度损失。还有一个容易被忽略的坐标系细节是镜像问题。用普通摄像头采集画面时人会本能地希望画面像照镜子一样左右翻转但翻转后左肩和右肩的坐标也要跟着交换。MediaPipe 处理的是翻转后的图像LEFT_SHOULDER对应的是画面中左侧的肩也就是用户自己的右手侧。如果只用单侧关键点做评分左右混淆影响不大但如果你把左右腿的角度都算进去做对称性评估就必须理清这个映射。建议在做了cv2.flip(frame, 1)镜像预览之后先打印一遍所有关键点坐标验证左右关系与预期一致。2.3 关节角度计算为什么用 atan2 而不是余弦定理深蹲评分最重要的量是膝关节处“髋-膝-踝”的夹角。计算三个关键点的夹角中学数学里有两条路向量点积加反余弦或者两个方向角的差。工程上我强烈建议用 atan2 方向角差值法原因在于数值稳定性。向量点积法先算两条向量的点积再除以模长得到余弦值最后取反余弦。这个流程在角度接近 0 度和 180 度时浮点计算会把余弦值截断导致最终角度误差变大。而 atan2 直接接收两个分量的差返回范围是 -π 到 π天然处理了象限问题。下面是完整的角度计算函数import math def calc_angle(a, b, c): 计算由三个关键点构成的夹角顶点是 b。 a, b, c 各自需要有 .x 和 .y 属性推荐直接传 landmark 对象。 # 两条边的方向向量这里直接用归一化坐标的差值 v1_x a.x - b.x v1_y a.y - b.y v2_x c.x - b.x v2_y c.y - b.y # 两条边各自的方位角 rad1 math.atan2(v1_y, v1_x) rad2 math.atan2(v2_y, v2_x) # 做差并转换到 0~180 度区间 diff abs(rad1 - rad2) * 180.0 / math.pi if diff 180.0: diff 360.0 - diff return diff逻辑说明函数第一步用顶点 b 到 a 和 c 的向量差明确两条边的方向。调用atan2的好处是当向量指向第三象限时能自动返回正确的负角度无需额外判断象限。最后把超过 180 度的差折算回锐角范围对正常关节角度输出已经够用。调用calc_angle时传入的是 MediaPipe 的landmark对象而不是 Python 内置列表。取关键点时保持原样不要先转成 List 再传。如果转成 List属性名丢失代码可读性下降。另一个常见误用是直接把角度存成int导致后续平滑滤波精度不足。建议中间计算始终用float只在最后显示时取整。3. 用 Python 从零搭建健身评分系统摄像头采集、骨架可视化与动作阶段判断3.1 环境准备安装 OpenCV、MediaPipe 与 NumPy搭建这套系统只需要四个 Python 依赖opencv-python、mediapipe、numpy外加标准库math。MediaPipe 目前对 Python 3.8 到 3.11 的兼容性最好如果你用的是 3.12 以上先查一下版本对应关系否则可能在导入阶段就报AttributeError。在 VSCode 配置 Python 环境时记得先给当前项目创建虚拟环境再用终端执行安装避免污染全局环境的包管理。# 创建并激活虚拟环境 python -m venv venv # Windows 下激活 venv\Scripts\activate # macOS/Linux 下激活 source venv/bin/activate # 安装依赖国内网络慢时可以加 -i 镜像源 pip install opencv-python mediapipe numpy参数说明venv不是必须的但对这种带媒体库依赖的项目强烈建议加一层隔离。pip install的版本建议锁定到当前稳定版不要追 rc 版。如果遇到mediapipe安装失败先检查 Python 版本再检查是否缺wheel包通常pip install wheel就能解决。第一次装完可以跑一句import mediapipe验证导入能过说明依赖链路没问题。3.2 实时摄像头采集与骨架可视化先让用户看见自己第一步先做最小可视化闭环让摄像头画面里的骨架线条实时叠加上来。这段代码既是后续所有功能的底座也是调试时最直观的反馈工具。import cv2 import mediapipe as mp mp_pose mp.solutions.pose mp_draw mp.solutions.drawing_utils pose mp_pose.Pose( static_image_modeFalse, # 视频流模式 model_complexity1, # CPU 上的精度/速度折中 min_detection_confidence0.5, min_tracking_confidence0.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 # 镜像翻转让预览画面符合镜子直觉 frame cv2.flip(frame, 1) rgb_frame cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results pose.process(rgb_frame) if results.pose_landmarks: mp_draw.draw_landmarks( frame, results.pose_landmarks, mp_pose.POSE_CONNECTIONS, ) # 顺便取一个膝盖角度打印出来观察 hip results.pose_landmarks.landmark[mp_pose.PoseLandmark.LEFT_HIP.value] knee results.pose_landmarks.landmark[mp_pose.PoseLandmark.LEFT_KNEE.value] ankle results.pose_landmarks.landmark[mp_pose.PoseLandmark.LEFT_ANKLE.value] angle calc_angle(hip, knee, ankle) cv2.putText(frame, fKnee: {int(angle)}, (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) cv2.imshow(AI Fitness, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明cap.set是在打开摄像头后调整采集分辨率640x480 是实测过实时性最稳的设置继续升到 1280x720 会明显感到推理跟不上。pose.process接受 RGB 帧所以要先做cvtColor转换。draw_landmarks会把骨架线和关键点画到原图上POSE_CONNECTIONS是 MediaPipe 预定义的连线关系。打印膝盖角度的目的是让你提前确认坐标系方向人站立时角度应在 160 度以上下蹲时会降到 110 度以下。3.3 动作阶段状态机以深蹲为例的计数逻辑有了角度下一步是把连续的角度序列翻译成“站立”和“蹲下”两个离散状态。这里我用状态机来做避免每一帧独立判断带来的临界抖动。深蹲计数不能只在某一帧角度小于阈值时直接加 1因为一个完整动作包含下降和上升两个过程必须等待状态完成一次“站立→蹲下→站立”的闭环。# 阶段状态机参数 STAND_ANGLE_MIN 160 # 膝角大于该值视为直立 SQUAT_ANGLE_MAX 110 # 膝角小于该值视为蹲下 state stand count 0 smoothed_angle 170.0 # 初始值适配直立状态 while cap.isOpened(): ret, frame cap.read() if not ret: break frame cv2.flip(frame, 1) rgb_frame cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results pose.process(rgb_frame) if results.pose_landmarks: hip results.pose_landmarks.landmark[mp_pose.PoseLandmark.LEFT_HIP.value] knee results.pose_landmarks.landmark[mp_pose.PoseLandmark.LEFT_KNEE.value] ankle results.pose_landmarks.landmark[mp_pose.PoseLandmark.LEFT_ANKLE.value] raw_angle calc_angle(hip, knee, ankle) # 一阶低通滤波抑制单帧抖动 alpha 0.25 smoothed_angle alpha * raw_angle (1 - alpha) * smoothed_angle # 状态转换 if state stand and smoothed_angle SQUAT_ANGLE_MAX: state squat elif state squat and smoothed_angle STAND_ANGLE_MIN: state stand count 1 print(fcount: {count}) cv2.destroyAllWindows()参数说明SQUAT_ANGLE_MAX 110而不是 90STAND_ANGLE_MIN 160而不是 170是在给状态判断留滞回区间。如果直接用 90 和 170 作为切换点膝盖角度在边界附近轻微抖动会导致一帧内反复切换状态把一次动作算成两次这是这类系统最常见的翻车原因。alpha 0.25表示新帧权重较低平滑效果明显如果你发现动作太慢导致漏计数可以把alpha提到 0.4实时性更强但抖动也会增加。4. 健身评分规则的参数设计与代码实现幅度、速度、稳定性怎么量化4.1 动作幅度评分角度阈值与连续偏差打分计数解决的是“做了没有”评分解决的是“做得怎么样”。动作幅度的定义是在动作最低点目标关节角度距离标准角度的偏差。以深蹲为例标准是膝关节角度达到 90 度左右蹲得越低角度越小幅度越好。这里我用分段线性函数来给幅度打分满分 60def score_amplitude(min_angle, target_angle90): 根据深蹲最低点膝角打分目标角度默认 90 度。 min_angle 越小代表蹲得越深幅度越好。 if min_angle target_angle: return 60 elif min_angle target_angle 15: # 每超 1 度扣 1 分线性递减 return 60 - (min_angle - target_angle) elif min_angle target_angle 45: # 超过 15 度后进入惩罚更重的区间 return 45 - (min_angle - target_angle - 15) * 0.5 else: return 15参数说明target_angle是动作标准不同动作不一样。深蹲用 90俯卧撑的肘关节标准角度是 90 到 100 度开合跳的肩关节外展角度则要结合上肢和下肢四条链一起评。分段线性有两个目的一是对轻微不达标保留梯度差异避免所有人挤在 40 分里二是对严重不达标直接压到低分拉开差距。分数设计里没有出现负分和超过 60 分的情况后续合成总分时也方便归一。4.2 速度与稳定性评估用角度标准差和时间窗口过滤噪声幅度只反映最低点但一个动作的质量还包含速度和稳定性。速度方面深蹲的推荐节奏一般是每分钟 8 到 12 次单次动作从下蹲到起身大约 4 到 7 秒。速度太快说明在借力弹跳太慢则可能是控制力不足或负重过大。稳定性用动作过程中膝盖角度的标准差来衡量角度波动越小说明轨迹越平滑。import numpy as np def score_stability(angle_history): 传入最近若干帧的膝角序列返回 0~20 分 if len(angle_history) 5: return 10 std np.std(angle_history) # 标准差越小动作越稳定 if std 3: return 20 elif std 6: return 15 elif std 10: return 10 else: return 5 def score_speed(squat_time): squat_time: 从站到蹲再到站的总秒数 if 4.0 squat_time 7.0: return 20 elif squat_time 4.0: # 太快按偏离程度扣分 return max(5, 20 - (4.0 - squat_time) * 5) else: # 太慢按偏离程度扣分 return max(5, 20 - (squat_time - 7.0) * 3)逻辑说明score_stability里angle_history是动作过程中记录的膝盖角度序列我一般取每次动作从开始到结束的 30 帧左右。标准差小于 3 度说明轨迹相当稳定10 度以上就需要提醒用户控制身体重心。score_speed用动作总时长做判断时长必须和状态机配合记录从状态变为 “squat” 开始计时恢复到 “stand” 结束。4.3 综合评分权重设计把三项指标合成 100 分把幅度、速度、稳定性三个维度合成 100 分需要设计权重。我常用的默认权重是幅度占 60%稳定性占 20%速度占 20%。这个比例的核心逻辑是健身动作首先看幅度幅度不够其他都免谈稳定性和速度更多影响训练体验和受伤风险权重相对低。评分维度满分权重说明动作幅度6060%最低点关节角度与标准角度偏差动作稳定性2020%动作过程中角度标准差动作速度2020%单次动作总耗时是否符合推荐节奏完整评分器如下class FitnessScorer: def __init__(self, target_angle90): self.target_angle target_angle self.amp_score 0 self.stab_score 0 self.speed_score 0 self.total_score 0 def evaluate(self, min_angle, duration_sec, angle_history): self.amp_score score_amplitude(min_angle, self.target_angle) self.stab_score score_stability(angle_history) self.speed_score score_speed(duration_sec) # 按权重合成总分 self.total_score round( 0.6 * self.amp_score 0.2 * self.stab_score 0.2 * self.speed_score, 1 ) return self.total_score这套评分器的输出是 0 到 100 的分数60 以上算合格75 以上算良好90 以上算优秀。实际使用时我通常还会把min_angle和angle_history记进日志方便后来做回归测试时看出分数是怎么算出来的。评分这件事最怕黑匣子用户看到分数时如果完全不知道扣在哪就谈不上纠正动作。5. 健身评分系统的避坑指南抖动、遮挡、视角与延迟的 4 个实战问题5.1 姿态抖动导致角度跳变和分数来回震荡现象用户保持直立时实测膝盖角度在 150 到 170 度之间来回跳有时一帧直接掉到 135 度导致状态机在 “stand” 和 “squat” 之间反复切换计数多算一次稳定性分数也明显偏低。原因MediaPipe 的姿态估计是基于单帧图像的相邻两帧的关键点坐标本身有随机噪声幅度在小范围内波动属于正常。如果摄像头帧率低或者光线偏暗波动会更明显。直接把原始角度喂给状态机等于把噪声也喂了进去。解决在角度进入状态机之前做一阶低通滤波用平滑后的角度做判断。核心代码就一行alpha 0.3 smoothed_angle alpha * raw_angle (1 - alpha) * smoothed_anglealpha是平滑系数值越大越跟踪原始信号值越小越平滑。我一般取 0.2 到 0.35 之间。注意滤波会引入相位延迟如果用户动作很慢延迟几乎无感如果做快速开合跳alpha小于 0.2 会导致状态转换滞后漏掉动作。5.2 身体遮挡时评分反而虚高置信度阈值必须加现象用户下蹲时手正好放在膝盖前方或者身体半转朝向侧面画面里的膝盖关键点被遮挡评分器反而给出了接近满分的幅度分。实际动作是变形的系统却识别成了标准深蹲。原因姿态估计模型在关键点被遮挡时会脑补位置也就是根据先验姿势推断被遮挡的关节点坐标。推理出来的坐标置信度很低但坐标数值仍然存在评分器不检查置信度就直接算角度等于把模型的臆测当成真实结果。解决评分前必须检查关键点的visibility字段低于阈值时跳过评分或给出提示。阈值一般取 0.6光线不足时可以放宽到 0.5但不能更低。MIN_VISIBILITY 0.6 def check_visibility(landmark): 检查单个关键点是否可信 return landmark.visibility MIN_VISIBILITY在评分流程里我的习惯是每次动作完成时先检查髋、膝、踝三个关键点的visibility任何一个低于阈值就丢弃这次动作不参与计数和评分。宁可少算一个也不能把错误数据计入总分。5.3 摄像头角度偏移让深蹲角度系统性偏差现象摄像头从侧面 45 度角拍摄用户明明蹲到了标准深度膝关节角度算出来却比实际大 10 到 15 度分数被系统性压低。摄像头装在高处向下俯拍时偏差方向相反蹲得不够深也会被算成达标。原因关节角度计算基于二维投影只有当摄像头光轴与矢状面接近垂直时三维关节角度才能被准确投影到二维平面。摄像头斜了投影就失真了这不是模型精度的问题而是成像几何的固有限制。解决在交互界面强制要求用户侧身对着摄像头让身体的矢状面尽量与镜头平面平行。具体到实现可以在画面上绘制一个半透明的姿势引导框用户站进框里再开始评分。还要在评分日志里保存摄像头角度设置避免不同位置的摄像头测试结果直接对比。这个误差无法在算法层面完全消除只能从采集侧规范化。5.4 实时评分卡顿分辨率、模型复杂度与帧率取舍现象画面流畅但分数和骨架线条有半秒以上的滞后用户已经站起身了界面上还显示着蹲下的角度。做开合跳时动作快系统几乎无法实时追踪。原因默认摄像头分辨率是 1280x720 甚至更高MediaPipe 在 CPU 上处理 720p 帧时每帧推理时间会超过 80ms加上画面采集和绘制总延迟就上去了。另外model_complexity2在某些机器上也会拖慢推理。解决优先降低采集分辨率到 640x480然后按机器性能调整模型复杂度。这两步对实时体验的提升最明显。cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)如果还想再快可以跳帧处理每两帧跑一次姿态估计中间一帧直接复用上一帧的关键点。注意跳帧会降低动作追踪的平滑度对速度评分结果有影响不建议在需要精确计时评分的场景里使用。6. 进阶让评分系统真正可投入——回归测试、分数校准与多动作扩展6.1 用录制视频做回归测试每次改参数不再怕翻车评分系统的参数一旦改动就存在影响其他动作的风险。我习惯准备三段录制好的视频分别是标准深蹲、半程深蹲、快速借力深蹲作为回归测试集。每次改完阈值或权重用同一个脚本跑一遍视频检查计数和分数是否符合预期。实现上只需把VideoCapture(0)换成VideoCapture(squat_standard.mp4)跑完打印最终的计数和总分。有了这个测试集调参就不是玄学而是可回放、可对比的过程。6.2 评分校准把分数拉到合理分布而不只是盯阈值刚跑通时你会发现分数经常集中在 85 到 95 分或者全部压在 40 分以下区分度很差。原因是你定的阈值和真实用户的分布不匹配。校准方法很简单收集 20 次真实动作的评分记录最小值和最大值再做线性映射到 60 到 95 区间calibrated 60 (raw_score - min_score) / (max_score - min_score) * 35这样能让普通用户看到 70 到 80 分有继续提升的动力专业用户看到 90 分以上获得正反馈。校准后记得把min_score和max_score持久化保存不要每次启动重新计算否则不同时段的分数不可比。6.3 从深蹲扩展到俯卧撑与开合跳状态机重用的经验深蹲的这套状态机思路可以直接迁移到俯卧撑和开合跳。俯卧撑主要看肘关节角度状态切换阈值是“撑起时肘角大于 160 度、下落时小于 100 度”评分维度换成躯干平直度。开合跳则要同时看肩关节外展角度和双脚间距状态机逻辑要从单角度判断改成多关键点联合判断。我现在的做法是定义一套动作配置文件每种动作包含关键点组合、标准角度、状态切换阈值和权重评分器只读配置不写业务代码。这样加一个新动作只需要录一段标准视频标注目标角度基本不用改 Python 代码。这套系统我前后迭代了三个版本最大的教训是别急着加功能先把角度计算和防抖做扎实。很多看起来花哨的问题比如分数飘忽、状态闪烁根源都在最基础的角度噪声没处理好。你如果照着文章把代码跑通我建议先拿自己的深蹲动作测一测看看站立时角度稳不稳、下蹲时能不能稳定触发状态切换。等这两个点都稳了再去调评分权重和做校准进度会快很多。希望帮到你。本文还有配套的精品资源点击获取