Python+MediaPipe实现仰卧起坐自动计数:姿态估计与状态机详解
简介这份资源面向对人工智能与计算机视觉感兴趣的开发者、健身应用方向的学习者以及需要完成体能测试类课程设计的学生核心解决的是如何用 Python 实现仰卧起坐动作的自动计数与个数统计问题。包内共 5 个文件以 py 脚本与 mp4 演示视频为主另含一个 zip 压缩包整体约 284.42MB脚本负责姿态识别与计数逻辑视频用于展示实际运行效果与动作判定过程。资源围绕 MediaPipe 姿态估计展开涉及关键点提取、动作幅度判断与计数状态机等环节可帮助读者理解从视频流到计数结果的完整链路。目前已有 756 人学习适合希望快速上手人体姿态分析、并将算法落地到健身计数场景的读者参考也可作为相关项目二次开发的起点。1. 从摄像头到计数AI健身体能测试里仰卧起坐统计到底难在哪健身房里做体能测试仰卧起坐是最常见的项目之一但人工计数一直是件麻烦事教练盯着看容易走神学员自己数又容易偷懒一组 60 秒下来到底做了多少个、动作是否标准往往各执一词。用 Python 加摄像头做仰卧起坐自动计数就是把这件凭肉眼的事变成凭数据的事——摄像头采集画面姿态估计模型提取人体关键点再用几何逻辑判断躺下—坐起的完整周期最后累加计数。这套方案适合有 Python 基础、想快速搭一个可跑原型的人也适合做 AI 健身方向产品验证的开发者。它不需要昂贵的压力传感器或穿戴设备一台普通笔记本加一个 USB 摄像头就能起步核心难点不在模型本身而在怎么把关键点的坐标变化翻译成一次有效计数。2. 仰卧起坐计数的技术选型为什么用姿态估计而不是目标检测2.1 目标检测和姿态估计的本质区别做仰卧起坐计数第一反应可能是训练一个模型识别躺下和坐起两个状态。这是目标检测的思路给画面框出人体再分类当前姿态。但目标检测有个致命问题——它输出的是边界框而仰卧起坐的核心动作是躯干角度的连续变化边界框在躺下和坐起时可能都是一个横着的人形框区分度极低。你很难靠一个矩形框判断躯干和地面的夹角是 30 度还是 80 度。姿态估计Pose Estimation输出的是人体关键点坐标比如肩膀、髋部、膝盖、脚踝。有了这些点的坐标躯干角度就是一个简单的三角函数计算。仰卧起坐的判定逻辑天然适合用关键点几何关系来表达肩髋连线与水平面的夹角从接近 0 度躺下变到接近 90 度坐起再回落就是一个完整周期。所以选型结论很明确用姿态估计提取关键点用几何逻辑做状态机计数而不是用目标检测做分类。常见做法是用 MediaPipe Pose 或 YOLOv8-Pose。MediaPipe 的优势是轻量、CPU 上就能跑到实时适合快速验证YOLOv8-Pose 精度更高但需要 GPU 才流畅。如果只是做体能测试原型MediaPipe 足够用而且安装简单不需要配 CUDA 环境。2.2 环境搭建与依赖安装先把 Python 环境准备好。如果你还没装 Python去官网下载 3.8 以上版本即可安装时记得勾选Add Python to PATH。装完在命令行验证python --version pip --version接下来装核心依赖。MediaPipe 和 OpenCV 是必装的numpy 用于坐标计算pip install mediapipe opencv-python numpy如果你用的是较新的 Python 版本比如 3.12MediaPipe 可能暂时没有对应的 wheel 包这时候建议用 Python 3.10 或 3.11 建一个虚拟环境python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate pip install mediapipe opencv-python numpy参数说明opencv-python是完整版包含 GUI 功能能直接cv2.imshow显示画面如果你在服务器上跑无头模式可以换成opencv-python-headless体积更小。mediapipe自带预训练的姿态模型不需要额外下载权重文件安装完就能用。提示MediaPipe 对 numpy 版本有要求如果安装后 import 报错先检查 numpy 是否被降级到了不兼容的版本用pip install numpy2通常能解决。2.3 用 MediaPipe 提取关键点的最小代码先跑通能拿到关键点这一步再谈计数逻辑。下面这段代码打开摄像头实时检测人体姿态并画出骨架import cv2 import mediapipe as mp mp_pose mp.solutions.pose mp_draw mp.solutions.drawing_utils # 初始化姿态估计模型model_complexity1 是速度和精度的折中 pose mp_pose.Pose( static_image_modeFalse, # 视频流模式逐帧跟踪 model_complexity1, # 0最快/1均衡/2最准 smooth_landmarksTrue, # 平滑关键点减少抖动 min_detection_confidence0.5, # 检测置信度阈值 min_tracking_confidence0.5 # 跟踪置信度阈值 ) cap cv2.VideoCapture(0) # 0 表示默认摄像头 while cap.isOpened(): ret, frame cap.read() if not ret: break # MediaPipe 需要 RGB 输入OpenCV 默认是 BGR rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results pose.process(rgb) if results.pose_landmarks: # 画出骨架连线方便肉眼确认关键点是否稳定 mp_draw.draw_landmarks( frame, results.pose_landmarks, mp_pose.POSE_CONNECTIONS ) # 取出左肩(11)和左髋(23)的坐标后面计数要用 lm results.pose_landmarks.landmark shoulder lm[mp_pose.PoseLandmark.LEFT_SHOULDER] hip lm[mp_pose.PoseLandmark.LEFT_HIP] print(f肩: ({shoulder.x:.3f}, {shoulder.y:.3f}) f髋: ({hip.x:.3f}, {hip.y:.3f})) cv2.imshow(Pose, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明pose.process()接收 RGB 图像返回 33 个关键点的归一化坐标x、y 范围 0~1。smooth_landmarksTrue会让相邻帧的关键点做平滑代价是动作切换时略有延迟但对计数这种低频事件影响不大。model_complexity设成 1 是实测下来 CPU 上最舒服的档位设 2 会明显掉帧设 0 则关键点抖动偏大。参数说明min_detection_confidence和min_tracking_confidence都设 0.5 是默认值。如果光线差导致关键点频繁丢失可以降到 0.3如果背景里有多个人导致误检可以升到 0.7。摄像头分辨率建议设成 640x480太高会拖慢推理速度对计数精度没有实质帮助。3. 把关键点变成计数角度计算与状态机设计3.1 躯干角度的计算方法仰卧起坐的核心判据是躯干与水平面的夹角。取左肩和左髋两个点计算它们连线与水平方向的夹角import math def calc_torso_angle(shoulder, hip): 计算躯干与水平面的夹角度 shoulder/hip 是归一化坐标 (x, y)y 轴向下为正 dx shoulder.x - hip.x dy shoulder.y - hip.y # atan2 返回弧度转成角度 angle math.degrees(math.atan2(abs(dy), abs(dx))) return angle逻辑说明dy是肩髋在垂直方向的差值dx是水平方向差值。当人完全躺平时肩和髋几乎在同一水平线上dy接近 0角度接近 0 度当人坐起来时肩明显高于髋dy变大角度接近 90 度。用abs()是为了兼容人面朝左或朝右两种情况不需要区分方向。参数说明这里用的是归一化坐标所以dx、dy都是 0~1 之间的小数但atan2只关心比值归一化不影响角度结果。如果你用的是像素坐标结果一样。3.2 状态机计数逻辑有了角度接下来要设计一个状态机把连续的角度变化翻译成一次有效动作。最简单的做法是设两个阈值角度小于 45 度算躺下状态大于 90 度算坐起状态从躺下到坐起再回到躺下计一次数。class SitupCounter: def __init__(self, down_thresh45, up_thresh90): self.down_thresh down_thresh # 躺下阈值 self.up_thresh up_thresh # 坐起阈值 self.state down # 初始状态 self.count 0 def update(self, angle): if self.state down and angle self.up_thresh: self.state up elif self.state up and angle self.down_thresh: self.state down self.count 1 # 完整周期计数加一 return self.count逻辑说明状态机只有两个状态down表示当前处于躺下姿态up表示当前处于坐起姿态。只有当角度从低于down_thresh上升到超过up_thresh再从高于up_thresh回落到低于down_thresh才完成一次计数。这样设计的好处是能过滤掉半程动作——如果只坐起一半就躺回去角度没超过 90 度状态不会切换不计数。参数说明down_thresh45和up_thresh90是经验值。如果测试者柔韧性好、坐起幅度大可以把up_thresh降到 80如果测试者体型偏胖、坐起困难可以降到 70。两个阈值之间要有足够间隔至少 20 度否则角度在阈值附近抖动会导致状态反复切换出现一个动作计好几次的翻车情况。3.3 完整可运行代码把关键点提取、角度计算、状态机串起来import cv2 import mediapipe as mp import math mp_pose mp.solutions.pose mp_draw mp.solutions.drawing_utils pose mp_pose.Pose( static_image_modeFalse, model_complexity1, smooth_landmarksTrue, min_detection_confidence0.5, min_tracking_confidence0.5 ) class SitupCounter: def __init__(self, down_thresh45, up_thresh90): self.down_thresh down_thresh self.up_thresh up_thresh self.state down self.count 0 def update(self, angle): if self.state down and angle self.up_thresh: self.state up elif self.state up and angle self.down_thresh: self.state down self.count 1 return self.count def calc_torso_angle(shoulder, hip): dx shoulder.x - hip.x dy shoulder.y - hip.y return math.degrees(math.atan2(abs(dy), abs(dx))) counter SitupCounter() cap cv2.VideoCapture(0) while cap.isOpened(): ret, frame cap.read() if not ret: break rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results pose.process(rgb) if results.pose_landmarks: lm results.pose_landmarks.landmark shoulder lm[mp_pose.PoseLandmark.LEFT_SHOULDER] hip lm[mp_pose.PoseLandmark.LEFT_HIP] angle calc_torso_angle(shoulder, hip) count counter.update(angle) # 在画面上显示角度和计数 cv2.putText(frame, fAngle: {angle:.1f}, (30, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) cv2.putText(frame, fCount: {count}, (30, 100), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) mp_draw.draw_landmarks(frame, results.pose_landmarks, mp_pose.POSE_CONNECTIONS) cv2.imshow(Situp Counter, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明每帧计算躯干角度喂给状态机状态机返回当前累计计数。画面上同时显示角度和计数方便调试时观察角度变化是否符合预期。如果角度显示正常但计数不动说明阈值设得不对如果计数乱跳说明关键点抖动严重需要调平滑参数或换更稳定的拍摄角度。参数说明摄像头建议放在测试者正侧面距离 2~3 米确保肩、髋、膝、踝都能入镜。正面拍摄会导致肩髋在图像上重叠角度计算失效。光线要均匀避免逆光否则 MediaPipe 容易丢失关键点。4. 避坑与排查仰卧起坐计数最常见的 5 个翻车现场4.1 计数只加不减但一个动作加了三四次现象做一次仰卧起坐计数从 0 跳到 3 甚至 4。原因关键点在阈值附近抖动角度在 45 度和 90 度之间反复穿越状态机被反复触发。MediaPipe 的smooth_landmarks只能缓解不能根除。解决在状态机里加一个最小帧间隔或冷却时间。比如每次计数后锁定 0.5 秒期间不响应新的状态切换。或者把阈值间隔拉大down_thresh30、up_thresh100让抖动不容易穿越两个阈值。4.2 躺着不动也在计数现象测试者躺在地上休息计数自己往上涨。原因摄像头角度偏了肩髋连线在图像上恰好处于某个角度区间加上关键点抖动状态机误判。解决加一个运动检测前置条件——只有关键点坐标在最近几帧内有明显变化时才允许状态机更新。或者要求角度必须连续 N 帧超过阈值才切换状态单帧穿越不算。4.3 关键点频繁丢失计数断断续续现象画面里人明明在动但角度显示时有时无计数漏掉好几次。原因光线不足、背景杂乱、衣服颜色和背景接近都会导致 MediaPipe 检测置信度下降。解决先改善拍摄条件——正面补光、纯色背景、穿深色衣服。如果条件受限把min_detection_confidence降到 0.3同时把model_complexity升到 2 提高精度代价是帧率下降。还可以在关键点丢失时保持上一帧的角度值而不是直接跳过避免状态机被重置。4.4 不同体型的人用同一套阈值计数偏差大现象同一个人计数准换个人就不准了。原因down_thresh45、up_thresh90是针对标准体型调的。个子高的人躯干长同样坐起角度变化更明显柔韧性差的人坐起幅度小角度到不了 90 度。解决在正式计数前加一个 3 秒校准阶段让测试者躺平记录此时的基准角度再让测试者坐起一次记录峰值角度用这两个值动态设置阈值。常见做法是取基准角度加 20 度作为down_thresh取峰值角度的 80% 作为up_thresh。4.5 摄像头帧率不够快速动作被漏计现象慢速做计数准快速做会漏。原因普通 USB 摄像头在 640x480 下通常 30fps但 MediaPipe 推理本身要耗时实际处理帧率可能只有 15~20fps。如果一次仰卧起坐只用 1 秒中间可能只采到十几帧状态切换的瞬间可能被跳过。解决降低摄像头分辨率到 320x240 提高帧率或者换用model_complexity0的轻量模型。如果还是不够可以在状态机里用角度变化趋势代替绝对阈值——只要角度从下降趋势转为上升趋势且幅度超过 30 度就计一次不要求必须穿越固定阈值。5. 从能跑到好用动态阈值校准与计数结果验证上面那套固定阈值的代码能跑通但离好用还有距离。我在实际测试里发现最影响计数准确率的不是模型精度而是阈值是否匹配当前测试者。所以最后一章讲一个具体技巧用前两次动作自动校准阈值再用角度曲线验证计数是否合理。校准逻辑很简单让测试者先做两次标准动作程序记录这两次动作中的最低角度和最高角度取最低角度加 15 度作为down_thresh取最高角度减 15 度作为up_thresh。这样阈值就跟着人的实际动作幅度走而不是硬编码。class AutoCalibrateCounter: def __init__(self): self.angles [] self.calibrated False self.down_thresh 45 self.up_thresh 90 self.state down self.count 0 def calibrate(self, angle): 前 60 帧收集角度自动设定阈值 self.angles.append(angle) if len(self.angles) 60: lo min(self.angles) hi max(self.angles) # 留 15 度余量避免阈值贴太近 self.down_thresh lo 15 self.up_thresh hi - 15 self.calibrated True print(f校准完成: down{self.down_thresh:.1f}, fup{self.up_thresh:.1f}) def update(self, angle): if not self.calibrated: self.calibrate(angle) return 0 if self.state down and angle self.up_thresh: self.state up elif self.state up and angle self.down_thresh: self.state down self.count 1 return self.count逻辑说明前 60 帧约 2~3 秒只收集角度不计数用这段时间让测试者完成至少一次完整动作。min和max分别对应躺下和坐起的极值各留 15 度余量是为了让阈值落在动作区间的中间位置避免边界抖动。校准完成后自动切换到计数模式。参数说明60 帧是 30fps 下约 2 秒如果测试者动作慢可以加到 90 帧。15 度余量是经验值动作幅度大的人可以加到 20 度幅度小的人减到 10 度。校准阶段要在画面上给出提示让测试者知道现在在做校准动作还不计数。验证计数是否合理最直接的方法是看角度曲线。把每帧的角度值存下来计数结束后用 matplotlib 画出来import matplotlib.pyplot as plt # angles 是计数过程中记录的角度列表 plt.plot(angles) plt.axhline(ycounter.up_thresh, colorr, linestyle--, labelup) plt.axhline(ycounter.down_thresh, colorg, linestyle--, labeldown) plt.xlabel(Frame) plt.ylabel(Torso Angle (deg)) plt.legend() plt.show()如果曲线是干净的波峰波谷形状每个波峰对应一次坐起计数就应该等于波峰数量。如果曲线毛刺很多说明关键点抖动严重需要回头调平滑参数或拍摄条件。如果波峰高度不一致说明测试者动作幅度不稳定动态阈值比固定阈值更合适。我自己的习惯是每次改完阈值或状态机逻辑先录一段 30 秒的视频跑一遍把角度曲线和计数结果对照着看。曲线骗不了人计数对不对看一眼波峰数量就清楚了。这套方法从原型到能用大概花一个下午就能调稳剩下的就是根据实际测试场景微调拍摄角度和光线。希望帮到你。本文还有配套的精品资源点击获取