尧图精选

Python+AI仰卧起坐计数:MediaPipe姿态估计与状态机实现

🕒 发布时间:2026/10/2 8:35:30 📁 来源:尧图网络
简介这套AI健身体能测试方案专注于仰卧起坐动作的自动计数与统计分析面向运动检测、姿态识别方向的Python开发者与学生。资源以MediaPipe姿态估计为核心通过关键点时序判断动作有效性可应用于体育测试自动化、居家健身督导等场景。压缩包共5个文件包含两段Python脚本与两段效果演示视频py文件分别负责姿态解析与核心计数逻辑mp4直观展示运行结果与实测画面另附带完整工程压缩包便于移植修改。已有756人学习使用适合具备一定Python基础、希望快速上手或参考动作识别方案的中级开发者。文件内提供pushup与poseutil两个脚本模块演示视频涵盖算法处理前后对比能帮助理解如何定义动作阈值、过滤无效摆动并输出计数结果动手实践时可直接替换视频输入或调整关键点参数。配套部署教程可在关联资源中查看便于快速复现环境与排查问题。1. Python AI做仰卧起坐计数一条脚本能不能真当体测裁判体测现场最容易起争议的不是秒表而是“仰卧起坐到底做够了没有”。人工数数既要盯动作到不到位又要记个数一分钟下来数漏两三个是常事拿手机录下来再一帧帧回放又太费时间。用Python做AI健身体能测试里的仰卧起坐计数核心思路不是“识别出某个动作然后加一”而是让姿态估计模型先把肩膀、髋部、膝盖的坐标给出来再用几何角度判断动作是否到位最后用状态机统计完成次数。这条链路在普通笔记本CPU上就能实时跑不依赖GPU适合体能测试自动化记录、健身房自主训练以及想从零接触姿态估计落地的人。下面按选型、核心算法、参数校准、避坑和记录导出的顺序把一套可复现的实现讲透。2. 姿态估计选型为什么MediaPipe Pose是本地计数的地基仰卧起坐计数落到代码里第一步不是“写个计数器”而是选一个能稳定输出人体关键点坐标的模型。姿态估计模型输出的只是关键点坐标不是动作标签计数逻辑要自己写。选型错了后面要么跑不动要么环境配置浪费一整天。这里直接对比三个常见方向。2.1 三个候选方案对比OpenPose、YOLO-Pose与MediaPipe PoseOpenPose是姿态估计里的老牌方案输出COCO格式的17个关键点精度确实高但模型体积大、依赖重在普通笔记本CPU上推理速度只有个位数fps。体能测试要实时反馈它更适合服务器离线批处理不适合现场跑。YOLO-Pose把目标检测和关键点估计做成一条链路多人场景优势明显但部署复杂度高通常需要GPU或者做量化转换。如果画面里同时只有一个人在做测试它的多目标能力是用不上的“过剩性能”。MediaPipe Pose用的是BlazePose模型采用检测器加跟踪器两阶段设计第一帧在画面里找人体后续帧靠跟踪延续CPU上单人实时可以跑到20到30fps。安装就是一条pip命令不需要自己准备训练数据也不需要转模型格式。对“本地单场景实时计数”这个需求来说我一般直接选它。2.2 33个关键点里只取肩、髋、膝编号与坐标系MediaPipe Pose一共输出33个关键点但仰卧起坐计数真正用到的只有6个。编号要记牢后面所有代码都依赖这些编号关键点编号在计数中的作用左肩 / 右肩11 / 12起坐端检测躯干是否抬起左髋 / 右髋23 / 24角度顶点旋转轴所在左膝 / 右膝25 / 26固定参照判定躺平与坐起左踝 / 右踝27 / 28辅助判断膝盖是否在垫上滑动每个关键点带着四维数据x、y、z、visibility。x和y是归一化坐标已经除以了图像宽高取值0到1所以算角度时不用关心是720p还是1080p分辨率不影响结果。z是模型估计的深度值以髋部中心为原点问题是单目估计的深度在动作过程中本身会抖动不能拿来算角度实际只用x和y。visibility是可见度0到1关键点被遮挡时这个值会掉下去后面处理左半身右半身切换时要靠它。计数特征定义为“肩-髋-膝”三点夹角以髋为顶点躺平时角度接近180度坐起时角度迅速变小到90度以下。膝盖在标准动作里基本不动髋才是旋转轴这个几何关系比单纯看躯干倾斜更贴近仰卧起坐的定义。2.3 侧面机位是第一前提正面和俯拍为什么都不行这个方案里Camera视角比模型选择更影响结果。从正面拍膝盖和髋在画面里几乎重合角度退化成一个点没法算。从头顶俯拍肩和膝盖都投影到地面人一抬身肩点投影位移很小角度变化非常迟钝阈值没法统一标定。侧面机位是唯一可靠的选择摄像头放在测试者体侧高度大致与髋部水平让肩-髋-膝在画面里形成一条清晰的折线。测试者全程保持同一侧朝向摄像头如果中途翻身转向左右关键点可见性会互换代码里要做双侧切换这节后面会讲。垫子和衣服的颜色也要注意。深色运动服配深色垫子躺平瞬间躯干和背景几乎融为一体检测器很容易丢帧这是后面避坑章节的第一个隐患先在这里埋下。3. 由关键点到次数肩-髋-膝夹角与状态机计数拿到关键点坐标之后要做两件事把坐标转成角度再把角度变化转成次数。前者是几何计算后者是时序状态判断两者分开写后面调试才不会互相污染。3.1 余弦定理算三点夹角cal_angle函数与参数已知肩A、髋B、膝C三点坐标以B为顶点两条边BA和BC的夹角可以用向量点积算import math def cal_angle(a, b, c): 计算三点夹角b为顶点返回角度值0~180度。 ba_x, ba_y a[0] - b[0], a[1] - b[1] bc_x, bc_y c[0] - b[0], c[1] - b[1] dot ba_x * bc_x ba_y * bc_y mod_ba math.hypot(ba_x, ba_y) mod_bc math.hypot(bc_x, bc_y) if mod_ba 0 or mod_bc 0: return 180.0 cos_val max(-1.0, min(1.0, dot / (mod_ba * mod_bc))) return math.degrees(math.acos(cos_val))a、b、c是三个坐标元组b是髋。两个向量的模长是0说明点重合直接返回180度避免除零。cos值钳制到[-1, 1]是必须的浮点误差可能让点积除以模长后的结果变成1.0000001math.acos遇到这个会返回nan角度一变成nan计数就废了。输入用的是归一化后的x和y所以这个角度不受摄像头分辨率影响。用向量点积而不是三边余弦定理省掉一次距离计算少开一次根号实时循环里能省一点是一点。3.2 最小可运行的实时计数循环从摄像头到画面叠加次数有了角度还要解决一个重要问题用左半身还是右半身。侧躺时靠近垫子那一侧的肩、髋、膝会被躯干遮挡visibility普遍低坐标还会漂移。常见做法是左右各算一个角度取可见性较高的一侧如果两侧关键点都低于0.5这一帧不可靠返回-1def pick_visible_angle(lm): 从左右两侧各算一个角度取可见性更高的一侧都过低返回-1。 def angle_of(a_idx, b_idx, c_idx): a (lm[a_idx].x, lm[a_idx].y) b (lm[b_idx].x, lm[b_idx].y) c (lm[c_idx].x, lm[c_idx].y) return cal_angle(a, b, c) vis_left min(lm[11].visibility, lm[23].visibility, lm[25].visibility) vis_right min(lm[12].visibility, lm[24].visibility, lm[26].visibility) if vis_left 0.5 and vis_right 0.5: return -1.0 if vis_left vis_right: return angle_of(11, 23, 25) return angle_of(12, 24, 26)这里用三点的visibility取最小值而不是只信任肩点是保守做法任何一个关键点不可靠这个角度就当无效。左11/23/25和右12/24/26是MediaPipe定义的人体左右侧编号直接按编号取就行。接着是状态机计数器。它把一次完整动作定义为“从躺平状态经过抬起再回到躺平”只有回到躺平才加一class SitUpCounter: def __init__(self, up_th85.0, down_th155.0, confirm_frames3): self.up_th up_th self.down_th down_th self.confirm_frames confirm_frames self.cross_cnt 0 self.state down # down / rising / up self.count 0 def update(self, angle: float) - int: if angle 0: self.cross_cnt 0 return self.count if self.state down: if angle self.up_th: self.cross_cnt 1 if self.cross_cnt self.confirm_frames: self.state rising else: self.cross_cnt 0 elif self.state rising: if angle self.down_th: self.state down # 还没到位就回落视为抖动 elif self.state up: if angle self.down_th: self.count 1 self.state down return self.countdown状态下连续多帧角度都小于up_th才切到rising这是防抖的核心。rising状态下如果角度又回到down_th以上说明是没抬到位的小动作直接打回down不计数。真正进入up之后再回到down_th才认为完成了一次完整的仰卧起坐。主循环这样组织import cv2 import mediapipe as mp mp_pose mp.solutions.pose pose mp_pose.Pose(min_detection_confidence0.7, min_tracking_confidence0.6) counter SitUpCounter(up_th85.0, down_th155.0) 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 angle pick_visible_angle(lm) n counter.update(angle) cv2.putText(frame, fcount: {n}, (10, 50), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 255, 0), 2) cv2.imshow(situp counter, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()运行环境只需两条命令pip install opencv-python和pip install mediapipe。把脚本保存为situp_counter.py运行python situp_counter.py人侧身躺好再启动对着摄像头做完整动作。q键退出。Pose的static_image_mode保持默认False这是视频跟踪模式如果是处理单张图片批量测试再改成True。3.3 为什么不直接按单帧阈值判断抖动、借力与动作速度最直觉的写法是“角度小于阈值就加一大于阈值就复位”看起来没问题实测基本都会翻车。借力甩动时角度在阈值附近来回穿越一次起坐会被算成两三次起身不到位但腰部离地弹一下角度也可能短暂突破阈值。根源是单帧阈值只看了“瞬间角度”而仰卧起坐是一个过程。状态机把计数变成对动作序列的识别必须从躺平出发先进入抬起过程到达最高点再回落到躺平才算一次。中间任何一步被打断都不计数。MediaPipe输出的关键点坐标本身有随机误差在髋部位置换算成角度会有2到5度的波动连续N帧确认正是用来对抗这种噪声的手段。这不是算法炫技是工程上必须要做的稳定化。确认帧数量和帧率要配套。30fps下3帧约100毫秒正常起坐跨越阈值的过程至少持续200到400毫秒不会被误杀而快速抖动通常只持续一到两帧正好被过滤掉。4. 把测试标准写进参数置信度、角度阈值与摄像头摆放同一套代码在不同场地上结果完全不一样差在参数。仰卧起坐计数有三组参数决定成败模型置信度、角度阈值、机位尺寸。调好这三组AI计数才和人工裁判对得上。4.1 min_detection_confidence与min_tracking_confidence两个阈值不要混为一谈MediaPipe Pose有两个容易混淆的参数。min_detection_confidence管的是“在画面里重新找一个人”的置信度门槛min_tracking_confidence管的是“继续跟当前这个人”的门槛。很多人两个都设置成0.9结果做动作时一个转身遮挡跟踪丢失下一帧又当作新的人重新检测关键点轨迹发生跳变计数完全紊乱。单人固定机位的场景我常用0.7配0.6检测阈值稍高避免误判跟踪阈值稍低让跟踪更“粘”原来的目标。如果画面里经常有路人经过可以把tracking进一步降到0.4到0.5增强粘性代价是目标真的离开画面后重新检测会慢上一两秒。这两个值对计数的影响不是线性的而是影响关键点的连续性稳定性比精度更重要。4.2 躺平角与起坐角的设置up_threshold与down_threshold怎么微调角度阈值直接对应体能测试标准。要理解它们得先回到角度定义肩-髋-膝三点夹角躺平时接近180度坐起时变小。down_threshold是躺平判定角默认155度。标准动作要求肩胛骨触垫但真实场地里腰部多少会离地肩髋膝很难到180度常见稳定值在150到170度之间。如果把down_th设到170人会很难完全满足计数偏少设到150则允许没有完全躺平就算一次回落计数偏多。up_threshold是起坐判定角默认85度。体能测试的常见标准是“双肘触膝”或“超过膝盖”这时髋角大约60到80度如果标准只是“躯干离开垫子”髋角90度左右也算有效。85度是个折中值严格测试可以调到70度家庭自测可以放宽到95度。两个阈值之间形成一个约70度的缓冲带这是防抖的物理基础。缓冲带太窄一个5度的坐标噪声就能造成误穿越缓冲带太宽半程动作也会被计入。4.3 摄像头高度、距离与人物占比用标定动作验证参数摄像头高度要以测试者的髋部为基准不是膝盖也不是肩膀。镜头光轴尽量与身体侧面垂直侧躺时躯干中段就是髋部的高度。从高处俯拍会让肩点投影路径变形从低处仰拍又会让膝盖变成前景遮挡。距离上让人体占画面高度的50%到70%比较合适。人物占比越小归一化坐标的小数位分辨率越差一个像素对应的角度变化越大关键点噪声被放大。占20%画面高度和占70%画面高度角度抖动幅度可以差出好几倍。换场地后不要直接拿默认参数跑。我一般让测试者先做两个标准动作打印出实时角度曲线看躺平段的稳定值和坐起段的稳定值。曲线低点的稳定值减去5度作为down_th高点的稳定值加上5度作为up_th。这套标定流程五分钟能完成比现场猜阈值可靠得多。4.4 一套可直接启用的参数速查表参数建议值说明min_detection_confidence0.7单人固定机位避免误检min_tracking_confidence0.6跟踪粘性有路人时降到0.4up_threshold85度折中标肘触膝标准用70度down_threshold155度要求完全回落弱者放宽到150度confirm_frames330fps下约100ms防抖窗口人物占画面高度50%~70%过小则角度抖动放大摄像头高度与髋部大致平齐镜头正对躯干中段提示参数表只是起点不是终点。角度阈值对摄像头左右偏移非常敏感换场地后用标准动作标定五分钟比硬套参数可靠得多。5. 避坑仰卧起坐计数实测中五次翻车与修复这套方案在干净视频上跑通只要一个晚上但拿到真实测试场地调试问题接踵而至。下面五条是出现频率最高的坑按现象、原因、解决的顺序写每一条定位过程都很绕解决方法本身都不复杂。5.1 做到位却没计数手抱头导致肩点被遮挡现象测试者明明做到肘触膝了画面上的计数纹丝不动。打印出角度曲线发现最大折叠角度卡在95度到100度之间死活下不到85度。原因标准体测要求双手交叉抱头肘部大幅外展肩部关键点被手肘或头部遮住visibility掉到0.5以下。pick_visible_angle切到另一侧发现另一侧也被压住连续返回-1状态机一直停在down。解决先从拍摄侧入手让测试者头部略微朝向镜头偏一点把肩部露出来垫子不要顶到腋下。算法侧也有一个备选方案当肩点visibility不足时用耳点编号7或8代替肩点计算“耳-髋-膝”角度。注意头部前移幅度比肩部小这个角度的坐起稳定值通常更大沿用85度阈值会漏计必须重新标定。5.2 旁观者路过触发误计单人ROI限制检测范围现象测试者做到一半有人从画面边缘走过计数突然加一甚至直接清零现场没法解释。原因跟踪器在原目标被遮挡后会触发重新检测路过的旁观者可能被当作新的跟踪目标。如果旁观者的弯腰或起身动作恰好符合“从down到rising再到down”的状态序列就会被计入。解决主循环里加一个单人ROI判断以髋点为锚点只允许它出现在画面中央区域h, w frame.shape[:2] hip_x, hip_y lm[24].x * w, lm[24].y * h if not (0.2 * w hip_x 0.8 * w and 0.1 * h hip_y 0.9 * h): continue同时把min_tracking_confidence降到0.4让跟踪更死盯原目标。ROI加跟踪双重限制后大部分误检都能被拦截。如果路人直接从画面正中穿过ROI也没办法这时只能把机位再贴近测试者让人体占画面更大比例。5.3 髋关节坐标跳变带来的角度抖动关键点低通滤波现象角度曲线在坐起瞬间出现一个尖刺角度从90度突然跳到105度再跳回来计数逻辑被干扰偶尔漏计一次。原因折叠动作让腹部和髋部轮廓急剧变化MediaPipe对髋点的预测在这个阶段不稳定运动服宽松时关键点会在两帧间位移好几个像素换算成角度就是5到10度的跳动。解决对关键点坐标做一阶低通滤波alpha取0.3到0.5之间。alpha太大没平滑效果太小动作变得迟钝起坐判定变慢。一个重要前提只在visibility大于0.5时更新平滑值遮挡帧用上一帧的平滑值顶替。滤波顺序要放在pick_visible_angle之前否则左右侧切换时会把两侧坐标混在一起滤波反而制造新的跳变。5.4 借力甩动造成重复计数连续N帧确认状态切换现象测试者用腰腹爆发力快速甩动一次起坐被算成两次甚至三次计数器比人工快出一倍。原因快速往返时角度在up_threshold附近高速穿越。单帧判断完全无法区分“正常起坐”和“惯性抖动”即使前面加了down到rising的确认帧如果只在入口加防抖up到down方向仍可能被抖出来重复计数。解决所有状态切换都走连续N帧确认不只入口一处。正常的起坐跨越阈值持续200到400毫秒N等于3在30fps下只认100毫秒不会误杀正常动作只会滤掉一到两帧的抖动脉冲。把confirm_frames暴露成构造参数现场遇到快速型测试者时从3调到4或5。5.5 躺平瞬间检测丢失垫子颜色、光照与离线抽帧现象整体漏检率不高但总是在肩部落垫的瞬间丢一到两帧最终计数比人工数少一两个。这个差异单独看不大但在体测现场会被放大成“AI计数不准”的结论。原因躺平瞬间躯干与垫子大面积贴合且相对静止画面对比度最低。垫子和衣服颜色相近或者背光环境下检测器置信度掉到最低人体直接被漏检。解决垫子选和衣服色差明显的颜色灯光从侧面打过来让躯干侧面形成明暗交界线。离线处理视频时不要为了省时间“每N帧抽一帧”再跑分析漏帧会让角度曲线跳过躺平或坐起的平台期计算量太大时优先降低分辨率而不是抽帧。主循环里遇到检测丢失不要立刻清空状态机让状态保持三到五帧等重新检测到人直接接上我的SitUpCounter里angle小于0时不清状态正是这个原因。6. 最后一公里把计数结果导出CSV让一次测试记录可回溯计数数字本身不可复核这是AI统计被质疑时最大的短板。我一般会在状态机每次加一时把帧号、峰值角度和持续时间记下来测试结束后导出CSVimport csv events [] # 每个元素: (frame_id, peak_angle, duration_s) def export_csv(pathsitup_report.csv): with open(path, w, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([index, frame_id, peak_angle_deg, duration_s]) for i, e in enumerate(events, 1): writer.writerow([i, e[0], round(e[1], 1), round(e[2], 2)])frame_id是视频帧计数事后可以用OpenCV跳到对应帧人工复核peak_angle是本次起坐中达到的最小角度也就是最大折叠幅度用来判断这次动作是否达标duration_s是一次起坐从抬起到位到回落到位的时间间隔正常在0.5到1.5秒之间低于0.4秒大概率是借力甩动。把这些字段和人工裁判的记录放一起对比漏计误计一眼就能定位。这个方案我迭代过好几个版本最初的版本只有数字后来加了CSV和角度复核才真正敢在测试场景里用。现在换到新场地我的第一件事不是调代码而是让人先做两个标准动作看角度曲线把阈值定下来——参数永远服从现场不要上来就拿测试者试错。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →