
简介这份资源面向对人工智能与体能测试感兴趣的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 numpy1.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_modeFalse, # 视频流模式 model_complexity1, # 0/1/21 是速度和精度的平衡点 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.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_landmarksTrue会让关键点在时间维度上做平滑代价是动作特别快的时候会有轻微延迟但对计数来说利大于弊。跑通之后你应该能看到摄像头画面里自己的骨架被画出来了。如果骨架闪烁严重或者经常丢失先检查光照——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_threshold140, up_threshold80): 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_threshold140和up_threshold80之间留了 60° 的缓冲区这叫迟滞区间目的是防止角度在阈值附近抖动时反复触发。这两个参数需要根据实际拍摄角度微调后面会讲怎么调。3.3 把关键点接入状态机的完整流程现在把姿态估计和状态机串起来import cv2 import mediapipe as mp import math mp_pose mp.solutions.pose pose mp_pose.Pose( static_image_modeFalse, model_complexity1, smooth_landmarksTrue, min_detection_confidence0.5, min_tracking_confidence0.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_threshold140, up_threshold80): 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, fAngle: {int(angle)}, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) cv2.putText(frame, fCount: {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_threshold140和up_threshold80是初始值。如果你躺下时角度只有 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_threshold140, up_threshold80, min_frame_gap15): 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.count4.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: # 坐起状态 pass4.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_modeTrue配合多目标检测但那样性能会下降。最简单的做法是确保拍摄画面里只有一个人这是最可靠的方案。5. 让计数更稳的几个进阶技巧5.1 用滑动窗口平滑角度曲线原始角度数据即使开了smooth_landmarks仍然会有毛刺。加一个长度为 5 的滑动窗口做均值滤波能显著减少误触发from collections import deque angle_buffer deque(maxlen5) 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 个标准仰卧起坐观察角度曲线躺平时角度范围是多少坐起时最低角度是多少中间过渡阶段角度变化是否平滑根据实际数据来设阈值而不是拍脑袋。我一般会先设一个宽松的范围比如 down150, up70跑一遍看计数是否偏多或偏少然后逐步收紧。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 个慢速动作测试是否多计。这三组测试跑完基本就能判断参数是否可用。希望帮到你。本文还有配套的精品资源点击获取