简介:这份资源面向人工智能与计算机视觉方向的初学者及健身应用开发者,聚焦人体姿态估计在体能测试中的落地实践,解决仰卧起坐动作自动计数与个数统计问题。包内共5个文件,以2个Python脚本为核心,分别承担姿态识别与计数逻辑,另含2段mp4演示视频及1个zip依赖包,整体约284.42MB,便于对照运行与效果验证。已有756人学习关注,说明该案例在AI健身与MediaPipe实战中具有一定参考价值。读者可获取完整的仰卧起坐计数实现思路,理解如何借助姿态关键点判断动作幅度与完成次数,并参考演示视频核对识别效果,同时结合依赖包快速搭建本地环境,适合作为课程设计、毕业项目或AI体能测试模块的起步方案。
1. 从一段 12.mp4 说起:这套 Python 仰卧起坐计数到底能跑出什么结果
手里拿到的是一个叫「仰卧起坐.zip」的压缩包,解压后能看到pushup.py、poseutil.py、mediapipe-master.zip、pushupoutput.mp4、12.mp4这几个文件。第一次看到pushup.py这个名字我愣了一下——明明是仰卧起坐,脚本却叫 pushup,这种命名混乱在个人项目里太常见了,不影响运行,但读代码时得先在心里过一遍映射关系。这套东西干的事很直接:拿一段手机或摄像头拍的仰卧起坐视频,用 MediaPipe 把人体姿态关键点抽出来,再靠角度变化判断「躺下—坐起」这个动作周期,最后把个数统计出来,输出一段带计数叠加的pushupoutput.mp4。
它解决的是体能测试里最烦人的那件事:人工数数。体育考试、健身房体测、康复训练记录,一个人做一组仰卧起坐,旁边得有人盯着数,数错了还容易起争执。这套 Python 脚本把这件事自动化了,输入视频、输出计数,中间不需要可穿戴设备,也不需要专门的深度相机,一个普通 RGB 摄像头拍的片子就能跑。适合谁?做 AI 健身方向的产品验证、体育类课程设计、想入门 MediaPipe 姿态估计的开发者,以及需要批量处理体测视频的从业者。12.mp4就是给你试跑的样例,pushupoutput.mp4是跑完的效果参考,先看输出再决定要不要拆代码,这个顺序比较省时间。
2. 拆开 poseutil.py:MediaPipe 姿态关键点是怎么变成计数信号的
2.1 为什么选 MediaPipe 而不是自己训模型
仰卧起坐计数这件事,本质是「动作周期检测」,不是「动作分类」。你不需要知道这是仰卧起坐还是卷腹,只需要知道躯干角度什么时候从接近水平变到接近垂直,再变回去。MediaPipe Pose 提供的 33 个关键点里,跟这个判断直接相关的是肩膀、髋部、膝盖这几个。选它而不是自己训一个姿态模型,理由很实际:开箱即用、CPU 上能跑到实时、关键点定义稳定。自己训模型要标数据、要调 backbone、要处理遮挡,投入产出比在这个场景下不划算。mediapipe-master.zip放在包里,说明作者可能是从源码或某个特定版本拿的依赖,这点后面避坑章节会展开。
MediaPipe Pose 输出的关键点里,每个点有x、y、z和visibility四个值。x、y是归一化到 0~1 的图像坐标,z是相对深度(不是真实距离,别拿它算物理尺寸),visibility表示这个点被遮挡或出画的可能性。计数逻辑里真正用的是x、y和visibility,z基本可以忽略。常见做法是先把关键点画到帧上做可视化,确认抽点没问题,再往上叠计数逻辑,这个顺序能帮你把「抽点错」和「计数逻辑错」两类问题分开。
2.2 角度计算:从三个关键点到躯干夹角
仰卧起坐的核心判据是躯干与大腿的夹角。取同侧的肩膀、髋、膝三个点,算髋关节处的夹角。用向量点积公式:
import math def calculate_angle(a, b, c): # a、b、c 为 (x, y) 元组,b 是顶点(这里取髋关节) # 返回 b 处的夹角,单位度 ang1 = math.degrees(math.atan2(a[1] - b[1], a[0] - b[0])) ang2 = math.degrees(math.atan2(c[1] - b[1], c[0] - b[0])) angle = abs(ang1 - ang2) if angle > 180: angle = 360 - angle return angle这段用atan2分别求两条边的方向角再作差,比直接点积更直观,也避免了向量归一化的额外步骤。参数说明:a传肩膀坐标,b传髋坐标,c传膝坐标。返回的角度在 0~180 之间。躺下时这个角接近 180 度(躯干和大腿几乎成一条线),坐起时接近 90 度甚至更小。计数状态机就建在这个角度上:角度从大于某个阈值降到小于某个阈值,记一次「坐起」,再回到大于阈值,记一次完整周期。
阈值不能拍脑袋定。不同人柔韧性不一样,有人坐起时躯干能压到 60 度,有人只能到 100 度。常见做法是先跑一遍样例视频,把每帧角度打印出来看波形,再定上下两个阈值。我一般会设一个「躺下阈值」比如 150 度,一个「坐起阈值」比如 100 度,中间留出滞回区间,防止在阈值附近抖动导致重复计数。
2.3 状态机与防抖:为什么直接比大小会多计数
最朴素的写法是「角度小于 100 就加一」,跑一遍你会发现计数虚高。原因是人在动作过程中角度会在阈值附近来回抖,一帧小于 100、下一帧又大于 100,状态机没有记忆就会反复触发。正确做法是维护一个状态变量:
# state: 'down' 表示躺下,'up' 表示坐起 # down_thresh、up_thresh 为两个阈值,down_thresh > up_thresh if state == 'down' and angle < up_thresh: state = 'up' # 可选:这里不立即计数,等回到 down 再计 elif state == 'up' and angle > down_thresh: state = 'down' count += 1 # 完成一次完整周期才计数逻辑说明:只有在「躺下」状态下角度跌破坐起阈值,才切到「坐起」;只有在「坐起」状态下角度回升超过躺下阈值,才切回「躺下」并计数加一。这样一次完整的「躺下—坐起—躺下」只计一次。参数说明:down_thresh和up_thresh的差值就是滞回带宽,带宽越大越不容易被抖动干扰,但太大又会漏掉幅度小的动作。经验值是差 30~50 度。visibility低于 0.5 的关键点建议直接跳过该帧,不要用不可靠的点去算角度,否则会出现莫名其妙的计数跳变。
3. 跑通 pushup.py:环境、依赖和一次完整的视频推理
3.1 环境准备与依赖安装
这套脚本依赖 Python 和 MediaPipe。Python 版本建议 3.8 到 3.10,太新的版本 MediaPipe 的 wheel 不一定跟得上,这是血泪经验。装依赖就两条命令:
# 建议先建虚拟环境,避免污染全局 python -m venv venv # Windows 激活 venv\Scripts\activate # Linux / macOS 激活 source venv/bin/activate # 安装核心依赖 pip install mediapipe opencv-python numpy参数说明:mediapipe提供姿态估计,opencv-python负责视频读写和画框,numpy处理数组运算。如果pip install mediapipe卡住或报找不到匹配版本,先确认 Python 版本,再考虑换国内镜像源。mediapipe-master.zip如果是源码包,一般不需要手动装,除非你要改 MediaPipe 内部逻辑;普通使用直接 pip 装官方 wheel 更省事。装完用python -c "import mediapipe; print(mediapipe.__version__)"验证一下,能打印版本号就说明环境通了。
3.2 视频读取与逐帧推理的主循环
pushup.py的主循环结构大致是这样,我按可复现的方式重写了一遍关键部分:
import cv2 import mediapipe as mp mp_pose = mp.solutions.pose pose = mp_pose.Pose( static_image_mode=False, # 视频流用 False,逐帧跟踪更稳 model_complexity=1, # 0 最快、1 均衡、2 最准 smooth_landmarks=True, # 关键点平滑,减少抖动 min_detection_confidence=0.5, min_tracking_confidence=0.5 ) cap = cv2.VideoCapture('12.mp4') fps = cap.get(cv2.CAP_PROP_FPS) w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) writer = cv2.VideoWriter('pushupoutput.mp4', cv2.VideoWriter_fourcc(*'mp4v'), fps, (w, h)) 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: # 取关键点、算角度、更新状态机、画计数 pass writer.write(frame) cap.release() writer.release() pose.close()逻辑说明:static_image_mode=False是关键,视频流下 MediaPipe 会用上一帧的结果做跟踪,比每帧独立检测更稳也更快。model_complexity控制模型大小,体测视频一般用 1 就够,追求速度用 0,追求精度用 2 但帧率会掉。smooth_landmarks=True对计数帮助很大,能压掉一部分关键点抖动。参数说明:min_detection_confidence和min_tracking_confidence调低会让检测更敏感但误检增多,调高则可能漏检,0.5 是常用起点。输出用mp4v编码,兼容性好,但有些播放器对它的支持一般,遇到打不开可以换XVID配.avi。
3.3 计数叠加与输出视频
计数要画到帧上,用cv2.putText:
cv2.putText(frame, f'Count: {count}', (30, 60), cv2.FONT_HERSHEY_SIMPLEX, 1.5, (0, 255, 0), 3)参数说明:(30, 60)是文字左下角坐标,1.5是字号,(0, 255, 0)是 BGR 绿色,3是线宽。坐标别贴着边缘,有些编码器会裁掉最外圈几像素。pushupoutput.mp4就是这套流程跑出来的成品,你可以拿它跟自己的输出逐帧对比,看计数触发点是否一致。如果自己的输出计数偏多,先看角度波形是不是抖得厉害;偏少,先看阈值是不是设得太苛刻。
4. 避坑与排查:这套计数脚本最容易翻车的五个地方
4.1 现象:计数只加不减,或者一直停在 0
原因:状态机初始状态设错,或者阈值方向搞反。如果初始state设成'up',而视频第一帧人是躺着的,状态机要等一个完整周期才会开始计数,前面几次动作全丢。如果down_thresh和up_thresh写反了,条件永远不成立,计数就一直是 0。
解决:把初始状态设成'down',并在第一帧先算一次角度确认落在躺下区间。跑之前打印前 30 帧的角度值,肉眼确认波形方向对不对。这个排查动作花不了一分钟,能省掉半小时的瞎猜。
4.2 现象:同一个人做 10 个,数出来 15 个
原因:关键点抖动导致角度在阈值附近反复穿越。MediaPipe 在遮挡、快速运动、光照差的情况下,关键点会有几像素的跳变,角度跟着抖,状态机被反复触发。
解决:三招一起上。第一,开smooth_landmarks=True。第二,加大滞回带宽,把down_thresh和up_thresh的差值拉到 40 度以上。第三,加一个最小帧间隔,两次计数之间至少隔fps * 0.3帧,低于这个间隔的触发直接忽略。第三招对快速抖动特别有效。
4.3 现象:换一个视频就完全不准
原因:阈值是针对12.mp4调的,换人、换拍摄角度、换机位高度,角度分布就变了。仰卧起坐的拍摄角度对角度计算影响极大,侧面拍和斜上方拍,同一个动作算出来的髋角能差二三十度。
解决:不要指望一套固定阈值通吃。常见做法是加一个自适应初始化:取视频前 2 秒(人还没开始做动作)的角度均值作为「躺下基准」,坐起阈值按基准减 60~70 度动态算。这样换视频时不用手改参数。如果做不到自适应,至少把机位固定下来,侧面 90 度拍摄是最稳的。
4.4 现象:pip 装 mediapipe 报错,或者 import 就崩
原因:Python 版本和 MediaPipe wheel 不匹配,或者系统缺底层依赖。Windows 上常见的是缺 Visual C++ 运行库,Linux 上常见的是缺libGL之类的图形库。
解决:先把 Python 降到 3.8~3.10 区间。Windows 装一下 VC++ Redistributable。Linux 上apt install libgl1 libglib2.0-0。如果用的是mediapipe-master.zip源码编译,还要确认 protobuf 版本,源码编译的坑比 pip 装多得多,非必要不走这条路。
4.5 现象:输出视频能播但没声音,或者播放器打不开
原因:cv2.VideoWriter只写视频轨,不处理音频,原视频的声音不会带过来。编码器选mp4v时,部分播放器兼容性一般。
解决:声音要单独用 ffmpeg 合回去:ffmpeg -i pushupoutput.mp4 -i 12.mp4 -c copy -map 0:v:0 -map 1:a:0 final.mp4。播放器打不开就换XVID编码输出.avi,或者用 ffmpeg 转一遍 H.264。体测场景下声音通常不重要,但如果你要拿输出视频做演示,这一步别省。
5. 进阶:把单视频计数改成批量体测流水线
单视频跑通只是起点。真实体测场景是一批人、一批视频,你要的是「丢一个文件夹进去,出来一张计数表」。这一步的核心改动有三个:把pushup.py里的硬编码路径改成命令行参数或遍历目录;把计数结果写进 CSV 而不是只画在视频上;把每个人的视频单独处理、单独输出,避免状态机跨人串扰。
import os import csv import glob video_dir = './videos' out_dir = './outputs' os.makedirs(out_dir, exist_ok=True) rows = [] for path in glob.glob(os.path.join(video_dir, '*.mp4')): name = os.path.splitext(os.path.basename(path))[0] count = process_video(path, os.path.join(out_dir, name + '_out.mp4')) rows.append({'name': name, 'count': count}) with open('result.csv', 'w', newline='', encoding='utf-8') as f: writer = csv.DictWriter(f, fieldnames=['name', 'count']) writer.writeheader() writer.writerows(rows)逻辑说明:process_video就是前面主循环的封装,返回该视频的计数。每个视频独立初始化pose和状态机,处理完pose.close(),避免上一个视频的关键点缓存影响下一个。参数说明:glob的匹配模式按实际扩展名改,手机拍的可能是.mov。CSV 用utf-8编码,防止姓名里有中文时乱码。
验证方法上,我习惯抽 3 个视频人工数一遍,跟 CSV 里的数字对。误差在 ±1 以内算可用,超过 ±2 就得回去看角度波形。批量跑的时候还要注意内存,pose对象不释放,跑几十个视频后内存会涨上去,pose.close()和gc.collect()该加就加。
有个细节值得单独说:不同人的动作幅度差异很大,统一阈值在批量场景下会暴露得很明显。我的做法是给每个视频单独算自适应阈值,用前 2 秒的躺下角度做基准,坐起阈值动态生成。这样即使一批人里有高有矮、有胖有瘦,计数稳定性也能接受。从那以后我每次做批量体测,都强制先跑一遍角度波形可视化,确认每个人的基准值合理,再开正式计数。希望帮到你。
本文还有配套的精品资源,点击获取