☰
Python+MediaPipe实现仰卧起坐自动计数:姿态估计与状态机实战
2026/10/12 18:00:50 网站建设 项目流程

简介:这份资源面向对人工智能与体能测试感兴趣的Python开发者、计算机视觉初学者及健身应用方向的学生,聚焦仰卧起坐动作的自动计数与个数统计问题。包内共5个文件,以2个py脚本、2个mp4演示视频和1个zip压缩包为主,脚本承担姿态识别与计数逻辑,视频用于展示实际运行效果,整体约284.42MB。已有756人学习下载,说明该方案在同类小项目中具备一定参考价值。读者可从中获取基于MediaPipe的人体姿态估计实现思路、仰卧起坐动作判定与计数算法、可复用的姿态工具脚本,以及配套的效果演示录屏,便于对照理解关键帧处理、关键点提取与计数触发条件,并在此基础上迁移到俯卧撑等其他体能测试动作,快速搭建属于自己的AI健身计数原型。

1. 从摄像头到计数:仰卧起坐自动统计到底靠什么跑通

健身房里教练拿个计数器手动按,一组 30 个仰卧起坐,数到第 18 个接了个电话,回来就忘了数到哪。这个场景太常见了。用 Python 做仰卧起坐计数统计,核心要解决的不是"识别动作",而是"在连续视频流里稳定判断一次完整的起坐周期,并且不重复计数"。听起来简单,但真正跑起来,光线变化、衣服颜色、拍摄角度、动作快慢都会让计数结果飘得离谱。

这套方案适合谁?有 Python 基础、想做一个能实际跑起来的 AI 健身小工具的人。不需要深度学习训练经验,但需要会装库、会调摄像头、能看懂基本的坐标和角度计算。整条链路是:摄像头采集视频帧 → 人体姿态估计提取关键点 → 根据关键点角度变化判断动作状态 → 状态机完成计数。下面从环境搭建开始,一步步把这条路走通。

2. 姿态估计选型与 Python 环境搭建:为什么不用训练自己的模型

2.1 MediaPipe 和 OpenPose 的取舍

做仰卧起坐计数,第一步是从画面里拿到人体的骨骼关键点。常见做法有三种:MediaPipe Pose、OpenPose、以及自己训练一个关键点检测模型。我一般会直接选 MediaPipe,原因很实际:

  • MediaPipe 在 CPU 上就能跑到 30 FPS 以上,不需要 GPU,笔记本就能跑
  • 它直接输出 33 个 3D 关键点,包含髋、膝、踝、肩、肘、腕,仰卧起坐需要的点全都有
  • 安装简单,pip install mediapipe就完事,不像 OpenPose 要编译 Caffe 或者配一堆依赖

OpenPose 精度确实高一些,但部署成本对个人项目来说不划算。自己训练模型就更没必要了——仰卧起坐的动作模式很固定,通用姿态估计模型完全够用。这里的关键认知是:你不需要一个"仰卧起坐识别模型",你需要的是可靠的关键点坐标,然后用几何逻辑去判断动作。

2.2 环境安装的完整命令

先确认 Python 版本。MediaPipe 对版本有要求,Python 3.8 到 3.11 都能用,3.12 目前部分版本还不支持。用python --version看一下,如果是 3.12 建议降到 3.10 或 3.11。

# 创建虚拟环境,避免和系统包冲突 python -m venv fitness_env # 激活虚拟环境 # Windows: fitness_env\Scripts\activate # macOS/Linux: source fitness_env/bin/activate # 安装核心依赖 pip install mediapipe opencv-python numpy # 验证安装 python -c "import mediapipe; print(mediapipe.__version__)"

如果mediapipe安装报错,大概率是 numpy 版本冲突。先pip install numpy==1.24.3再装 mediapipe。另一个常见问题是 Windows 上缺少 Visual C++ 运行库,装一下 VC++ Redistributable 就行。

提示:不要用pip install mediapipe在全局环境装,MediaPipe 依赖的 protobuf 版本经常和其他库打架,虚拟环境能省很多事。

2.3 验证摄像头和 MediaPipe 能跑通

装完之后先跑一个最小验证,确认摄像头能打开、MediaPipe 能检测到人体:

import cv2 import mediapipe as mp mp_pose = mp.solutions.pose pose = mp_pose.Pose( static_image_mode=False, # 视频流模式 model_complexity=1, # 0/1/2,1 是速度和精度的平衡点 smooth_landmarks=True, # 平滑关键点,减少抖动 min_detection_confidence=0.5, # 检测置信度阈值 min_tracking_confidence=0.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.solutions.drawing_utils.draw_landmarks( frame, results.pose_landmarks, mp_pose.POSE_CONNECTIONS) cv2.imshow('Pose Test', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()

这段代码的逻辑很直白:读帧 → 转 RGB → 送入 MediaPipe → 拿到关键点 → 画骨架。model_complexity参数控制模型大小,0 最快但精度低,2 最准但慢,1 是大多数场景的默认选择。smooth_landmarks=True会让关键点在时间维度上做平滑,代价是动作特别快的时候会有轻微延迟,但对计数来说利大于弊。

跑通之后你应该能看到摄像头画面里自己的骨架被画出来了。如果骨架闪烁严重或者经常丢失,先检查光照——MediaPipe 对逆光和过暗环境很敏感,这是后面避坑章节要展开的点。

3. 从关键点到计数:角度计算与状态机设计

3.1 用髋膝肩三个点算躯干角度

仰卧起坐的核心判断依据是躯干和地面的夹角变化。最直接的做法是取肩、髋、膝三个关键点,计算髋关节处的角度。当人躺平时,肩-髋-膝接近一条直线,角度大约 160°~180°;坐起来时,躯干折叠,角度缩小到 60°~90°。

import math def calculate_angle(a, b, c): """ 计算三个点形成的角度,b 是顶点 a, b, c 都是 (x, y) 坐标 返回角度值,单位度 """ # 向量 ba 和 bc ba = (a[0] - b[0], a[1] - b[1]) bc = (c[0] - b[0], c[1] - b[1]) # 点积和模长 dot = ba[0] * bc[0] + ba[1] * bc[1] mag_ba = math.sqrt(ba[0]**2 + ba[1]**2) mag_bc = math.sqrt(bc[0]**2 + bc[1]**2) if mag_ba == 0 or mag_bc == 0: return 0 cos_angle = dot / (mag_ba * mag_bc) # 防止浮点误差导致 acos 越界 cos_angle = max(-1.0, min(1.0, cos_angle)) angle = math.degrees(math.acos(cos_angle)) return angle

这个函数是纯几何计算,没有玄学。传入三个点的坐标,返回顶点处的夹角。在仰卧起坐场景里,a 取肩膀,b 取髋部,c 取膝盖。MediaPipe 输出的坐标是归一化的(0~1 之间),直接拿来算角度没问题,因为角度计算只依赖相对位置。

3.2 状态机:怎么判断"完成了一次"

有了角度数据之后,不能简单地"角度小于 90 就加一",那样会在临界点反复触发。正确做法是用状态机:

class SitupCounter: def __init__(self, down_threshold=140, up_threshold=80): self.down_threshold = down_threshold # 躺下时的角度阈值 self.up_threshold = up_threshold # 坐起时的角度阈值 self.state = "down" # 初始状态:躺下 self.count = 0 self.last_angle = 180 def update(self, angle): """ 根据当前角度更新状态和计数 返回当前计数 """ if self.state == "down" and angle < self.up_threshold: # 从躺下状态进入坐起状态 self.state = "up" elif self.state == "up" and angle > self.down_threshold: # 从坐起状态回到躺下状态,完成一次 self.state = "down" self.count += 1 self.last_angle = angle return self.count

状态机的逻辑是:只有"躺下 → 坐起 → 躺下"这个完整循环才计数一次。down_threshold=140和up_threshold=80之间留了 60° 的缓冲区,这叫迟滞区间,目的是防止角度在阈值附近抖动时反复触发。这两个参数需要根据实际拍摄角度微调,后面会讲怎么调。

3.3 把关键点接入状态机的完整流程

现在把姿态估计和状态机串起来:

import cv2 import mediapipe as mp import math mp_pose = mp.solutions.pose pose = mp_pose.Pose( static_image_mode=False, model_complexity=1, smooth_landmarks=True, min_detection_confidence=0.5, min_tracking_confidence=0.5 ) def calculate_angle(a, b, c): ba = (a[0] - b[0], a[1] - b[1]) bc = (c[0] - b[0], c[1] - b[1]) dot = ba[0] * bc[0] + ba[1] * bc[1] mag_ba = math.sqrt(ba[0]**2 + ba[1]**2) mag_bc = math.sqrt(bc[0]**2 + bc[1]**2) if mag_ba == 0 or mag_bc == 0: return 0 cos_angle = max(-1.0, min(1.0, dot / (mag_ba * mag_bc))) return math.degrees(math.acos(cos_angle)) class SitupCounter: def __init__(self, down_threshold=140, up_threshold=80): self.down_threshold = down_threshold self.up_threshold = up_threshold self.state = "down" self.count = 0 def update(self, angle): if self.state == "down" and angle < self.up_threshold: self.state = "up" elif self.state == "up" and angle > self.down_threshold: self.state = "down" self.count += 1 return self.count 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: landmarks = results.pose_landmarks.landmark # 获取肩、髋、膝的坐标(归一化值) shoulder = (landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].x, landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].y) hip = (landmarks[mp_pose.PoseLandmark.LEFT_HIP.value].x, landmarks[mp_pose.PoseLandmark.LEFT_HIP.value].y) knee = (landmarks[mp_pose.PoseLandmark.LEFT_KNEE.value].x, landmarks[mp_pose.PoseLandmark.LEFT_KNEE.value].y) angle = calculate_angle(shoulder, hip, knee) count = counter.update(angle) # 在画面上显示角度和计数 cv2.putText(frame, f"Angle: {int(angle)}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) cv2.putText(frame, f"Count: {count}", (10, 70), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) mp.solutions.drawing_utils.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()

这段代码就是完整的可运行版本。逻辑链条:每帧提取左肩、左髋、左膝三个点的坐标 → 算髋关节角度 → 送入状态机 → 更新计数 → 在画面上叠加显示。用左侧关键点是因为 MediaPipe 对左右侧的检测逻辑一致,选一侧就行,不需要两侧都算。

参数方面,down_threshold=140和up_threshold=80是初始值。如果你躺下时角度只有 120 而不是 160,说明摄像头角度偏了,或者 MediaPipe 对躺姿的关键点估计有偏差,需要调整。这个后面在避坑章节详细说。

4. 仰卧起坐计数翻车现场:5 个血泪踩坑记录

4.1 现象:计数疯狂跳变,一组做了 20 个显示 47 个

原因:角度在阈值附近抖动,状态机反复切换。MediaPipe 的关键点即使开了smooth_landmarks,在动作缓慢或者画面模糊时仍然会有几度的抖动。如果down_threshold和up_threshold之间只差 10°,角度在 85° 附近来回跳,状态机就会疯狂触发。

解决:拉大迟滞区间。down_threshold和up_threshold之间至少留 40°~60° 的间隔。另外可以在状态机里加一个"最小帧间隔"——两次计数之间至少间隔 15 帧,防止高频误触发:

class SitupCounter: def __init__(self, down_threshold=140, up_threshold=80, min_frame_gap=15): self.down_threshold = down_threshold self.up_threshold = up_threshold self.state = "down" self.count = 0 self.frame_since_last_count = 0 self.min_frame_gap = min_frame_gap def update(self, angle): self.frame_since_last_count += 1 if self.state == "down" and angle < self.up_threshold: self.state = "up" elif self.state == "up" and angle > self.down_threshold: if self.frame_since_last_count >= self.min_frame_gap: self.state = "down" self.count += 1 self.frame_since_last_count = 0 return self.count

4.2 现象:人躺下后 MediaPipe 直接丢失关键点,画面里骨架消失

原因:MediaPipe 的 Pose 模型是基于直立人体数据训练的,当人完全躺平时,模型对"人体"的检测置信度会大幅下降。尤其是侧躺或者摄像头从侧面拍的时候,检测失败率很高。

解决:两个方向。一是调整拍摄角度,摄像头放在侧面偏上方 30°~45°,不要完全水平。二是降低min_detection_confidence到 0.3,牺牲一些误检率换取检出率。如果还是丢,可以在丢失关键点时保持上一帧的角度值,不要立即重置状态机:

last_valid_angle = 180 # 保存上一次有效角度 if results.pose_landmarks: # ... 计算 angle ... last_valid_angle = angle else: angle = last_valid_angle # 用上一次的值兜底

4.3 现象:换了个人来测,计数完全不准

原因:不同人的体型差异导致关键点位置比例不同。个子高的人髋关节到肩膀的距离长,同样角度变化对应的像素位移更大;个子矮的人则相反。另外,穿宽松衣服时 MediaPipe 对髋部的定位会偏。

解决:不要用绝对角度阈值,改用相对角度变化。先让人躺平 3 秒,记录此时的基准角度base_angle,然后坐起时判断角度是否比基准小了 50° 以上:

# 校准阶段:记录躺平时的基准角度 if calibration_frames < 90: # 前 3 秒校准 base_angles.append(angle) calibration_frames += 1 if calibration_frames == 90: base_angle = sum(base_angles) / len(base_angles) else: # 判断是否坐起:角度比基准小 50 度以上 if base_angle - angle > 50: # 坐起状态 pass

4.4 现象:做动作快了就漏计数,慢了就多计数

原因:摄像头帧率不够或者 MediaPipe 处理速度跟不上。如果摄像头只有 15 FPS,一个快速仰卧起坐(1 秒完成)只有 15 帧,状态机可能来不及捕捉到完整的"躺下→坐起→躺下"过程。反过来,动作太慢时,在"坐起"位置停留太久,如果角度有波动,可能被误判为多次。

解决:确保摄像头帧率在 30 FPS 以上。如果 MediaPipe 处理不过来,把model_complexity设为 0,牺牲精度换速度。另外在状态机里加一个"超时重置"——如果处于"up"状态超过 5 秒还没回到"down",强制重置为"down"状态,防止卡死。

4.5 现象:画面里有多个人,计数乱套

原因:MediaPipe 默认只检测置信度最高的一个人,但如果画面里有人走动,检测目标会切换,导致角度数据跳变。

解决:在画面里画一个 ROI(感兴趣区域)框,只处理框内的关键点。或者用mp.solutions.pose.Pose的static_image_mode=True配合多目标检测,但那样性能会下降。最简单的做法是确保拍摄画面里只有一个人,这是最可靠的方案。

5. 让计数更稳的几个进阶技巧

5.1 用滑动窗口平滑角度曲线

原始角度数据即使开了smooth_landmarks仍然会有毛刺。加一个长度为 5 的滑动窗口做均值滤波,能显著减少误触发:

from collections import deque angle_buffer = deque(maxlen=5) def smooth_angle(new_angle): angle_buffer.append(new_angle) return sum(angle_buffer) / len(angle_buffer)

这个改动很小,但效果立竿见影。窗口长度不要超过 7,否则动作快的时候会有明显延迟,导致漏计数。

5.2 用双阈值加时间窗口做二次确认

在状态机之上再加一层确认逻辑:当状态从"down"切换到"up"时,不立即认为动作开始,而是要求角度在"up"阈值以下持续至少 3 帧,才确认进入"up"状态。同理,"up"到"down"也要持续 3 帧。这样能过滤掉绝大部分瞬时抖动。

5.3 参数调试的实操方法

把角度值实时打印到终端,做 5 个标准仰卧起坐,观察角度曲线:

  • 躺平时角度范围是多少?
  • 坐起时最低角度是多少?
  • 中间过渡阶段角度变化是否平滑?

根据实际数据来设阈值,而不是拍脑袋。我一般会先设一个宽松的范围(比如 down=150, up=70),跑一遍看计数是否偏多或偏少,然后逐步收紧。

5.4 记录原始数据做离线调参

在代码里加一个 CSV 记录功能,把每帧的角度、状态、计数都写进去。跑完之后用 Excel 或者 Python 画个图,一眼就能看出阈值设得对不对:

import csv with open('angle_log.csv', 'w', newline='') as f: writer = csv.writer(f) writer.writerow(['frame', 'angle', 'state', 'count']) # 在循环里写入 writer.writerow([frame_idx, angle, counter.state, counter.count])

这个习惯帮我省了很多时间。实时调参靠感觉,离线看曲线靠数据,后者靠谱得多。

5.5 一个容易被忽略的细节:摄像头高度

摄像头放在地面高度和放在胸口高度,拍出来的角度曲线完全不同。地面高度拍,躺下时肩膀和髋部几乎重叠,角度计算会失真。建议摄像头放在侧面、高度约 60~80cm,稍微俯拍。这个位置能让肩、髋、膝三个点在画面里保持较好的分离度,角度计算最稳定。

最后说一个我自己的习惯:每次改完参数,先做 10 个标准动作验证计数是否准确,再做 5 个快速动作测试是否漏计,最后做 5 个慢速动作测试是否多计。这三组测试跑完,基本就能判断参数是否可用。希望帮到你。

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

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

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

立即咨询