尧图精选

MediaPipe姿态估计实现仰卧起坐计数:关键点与状态机实战

🕒 发布时间:2026/10/2 18:08:46 📁 来源:尧图网络
简介面向AI健身与体能测试场景本资源以Python为工具结合MediaPipe姿态估计技术实现仰卧起坐动作的自动识别与次数统计。适合希望将计算机视觉用于运动检测的学生、开发者及健身项目爱好者从零搭建一套可演示的计数原型。资源压缩包共5个文件主要包括2个Python脚本负责姿态关键点提取与计数逻辑、2个mp4视频演示输出效果与配套操作说明以及1个zip依赖包内含MediaPipe运行所需组件整体约284.42MB。目前已有756人学习下载。通过这份资料使用者可快速了解人体姿态估计在体能测试中的落地方式拿到直接可用的脚本与依赖素材结合演示视频理解处理流程与界面效果减少环境配置和算法调试的时间成本。适合用作课程设计、项目验证或前期技术预研的参照。1. 用MediaPipe给仰卧起坐计数这套Python代码包能解决什么问题你如果需要一套能自动数仰卧起坐个数的工具别急着从零训练模型先把手上这套基于MediaPipe姿态估计的源码跑通一个小时就能出结果。压缩包里放着pushup.py、poseutil.py、测试视频12.mp4和输出示例pushupoutput.mp4作者把俯卧撑计数的逻辑改成了仰卧起坐版本用人体关键点算出躯干折叠角度再用状态机把一次完整的“躺下-起身”翻译成一个计数。它适合体育测试自动记录、健身房自测以及想学姿态估计落地的同学B站演示视频和CSDN配套部署教程都能帮你快速对上号。2. 姿态估计选型与角度定义33个关键点里只用6个2.1 为什么选MediaPipe Pose而不是先检框再分类仰卧起坐计数这件事最朴素的做法是拿一个动作分类模型识别“躺下”和“起身”两个状态。但分类模型需要标注大量样本换一个人、换一个机位很可能就失效。姿态估计走的是另一条路先找出人体关键关节坐标再用几何关系去判断动作进行到哪个阶段。MediaPipe Pose在这种单人、固定机位、动作重复的场景里是性价比最高的开源方案它在普通CPU上跑一帧只需要几毫秒到十几毫秒不需要额外准备GPU也不需要像YOLOv8-pose那样先过一遍目标检测再回归关键点。代价当然也有MediaPipe Pose对遮挡和多人场景的鲁棒性不如检测类模型。但这恰好不构成问题——仰卧起坐测试本来就是一个人一块垫子画面里最多多一个计时员。把复杂度拉满的模型用在单一动作场景里纯属浪费算力。我一般先按0.5的检测置信度跑一遍原始视频如果角度曲线稳定就不用再折腾模型参数。2.2 关键点编号与肩-髋-膝角度MediaPipe Pose输出33个关键点每个点包含归一化的x、y、z坐标和一个visibility置信度。仰卧起坐只看躯干折叠程度真正要用的核心点只有6个左肩11、右肩12、左髋23、右髋24、左膝25、右膝26。关键点编号位置在本项目中的用途11 / 12左肩 / 右肩取中点得到肩中心23 / 24左髋 / 右髋取中点得到髋中心作为角度顶点25 / 26左膝 / 右膝取中点得到膝中心为什么要取左右中点而不是随便用单侧因为固定机位下人体侧躺或轻微扭转时某一侧肩膀或髋部可能被自己身体挡住单侧坐标会飘。取中点相当于把两侧误差平均掉只要一侧还稳定角度就不会瞬间跳变。角度定义是以髋中心为顶点计算肩中心-髋中心-膝中心三点形成的夹角。平躺时肩、髋、膝近似一条直线角度接近180度起身后躯干和折叠的大腿夹角变小标准动作一般会落在60到100度之间。这个从180到90再回到180的反复变化就是计数器的输入信号。2.3 角度计算函数归一化坐标别忘还原成像素MediaPipe返回的x、y都是0到1之间的相对坐标直接用会把长宽比信息丢掉算出来的角度是错的。常见做法是先乘上图像宽高把点还原成像素坐标再计算。下面这个函数是所有计数逻辑的基础import math def angle_between(a, b, c): 三点夹角b 是顶点返回角度度 ba (a[x] - b[x], a[y] - b[y]) bc (c[x] - b[x], c[y] - b[y]) # 点积除以模长乘积得到余弦值 cos_angle (ba[0] * bc[0] ba[1] * bc[1]) / ( math.hypot(ba[0], ba[1]) * math.hypot(bc[0], bc[1]) ) # clip 到 [-1, 1]防止浮点误差导致 acos 返回 nan cos_angle max(-1.0, min(1.0, cos_angle)) return math.degrees(math.acos(cos_angle))逻辑很直观把顶点b到a、顶点b到c的两个向量做点积除以两个向量模长乘积得到夹角的余弦值反余弦后转成度数。最容易翻车的是浮点误差让余弦值略大于1或略小于-1直接丢给acos会返回nan所以必须加clip。这个函数不挑坐标系传入的字典里只要有x和y字段就能算。注意用中点而不是单侧点时字典结构保持一致midpoint返回的字典同样带上x和yangle_between不用改一行代码。3. 计数逻辑落地从关键点到状态机再到输出mp43.1 poseutil.py把MediaPipe推理封装成工具类资源包里的poseutil.py就是推理封装。我不建议把MediaPipe的调用散落在主循环里一是参数没地方统一管理二是进程结束时容易忘记释放资源。封装成类之后主代码只需要关心get_landmarks这一件事。import cv2 import mediapipe as mp class PoseUtil: def __init__(self, static_image_modeFalse, model_complexity1, min_detection_confidence0.5, min_tracking_confidence0.5): self.mp_pose mp.solutions.pose self.pose self.mp_pose.Pose( static_image_modestatic_image_mode, model_complexitymodel_complexity, min_detection_confidencemin_detection_confidence, min_tracking_confidencemin_tracking_confidence ) def get_landmarks(self, frame): 输入 BGR 帧返回以关键点编号为 key 的坐标字典 rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results self.pose.process(rgb) if not results.pose_landmarks: return None h, w frame.shape[:2] pts {} for idx, lm in enumerate(results.pose_landmarks.landmark): pts[idx] { x: lm.x * w, y: lm.y * h, visibility: lm.visibility } return pts def close(self): self.pose.close()参数说明static_image_modeTrue时每一帧都做完整检测适合单张图片但对视频流很慢视频流保持False让它用上一帧关键点做跟踪速度快一个量级。model_complexity1是性能折中0更快但角度抖动更明显2更稳但对CPU不友好。min_detection_confidence和min_tracking_confidence我通常都用0.5低于这个值说明画面里没有可靠人体返回None让主逻辑跳过这一帧。资源包里那个mediapipe-master.zip就是带版本配套的MediaPipe源码包如果你网络环境装不上对应版本可以直接离线解压后用里面的模块避免了版本不匹配导致的API差异。3.2 滞回状态机阈值翻转与防抖窗口拿到角度序列后计数环节我推荐一个非常朴素的两状态有限状态机DOWN躺下和UP起身。每次从DOWN翻转到UP记录为“正在起身”只有再次回到DOWN也就是真正躺平一次才计一个数。直觉上的依据是一次完整动作必须包含起和躺两个过程只起不躺不算完整次数。def count_situp(angles, up_th140.0, down_th160.0, window5): 用滞回窗口把角度序列翻译成仰卧起坐次数 # 滑动平均去噪 smoothed [] for i in range(len(angles)): seg angles[max(0, i - window 1): i 1] smoothed.append(sum(seg) / len(seg)) count 0 state DOWN for v in smoothed: if state DOWN and v up_th: state UP elif state UP and v down_th: state DOWN count 1 return count, smoothed阈值为什么成对出现如果只用一个阈值比如150度角度在150度附近来回抖时状态会连续翻转一次动作可能被计成四五次。up_th和down_th之间留出滞回区间角度必须明显小于up_th才判定起身明显大于down_th才判定躺下中间这段区间无论怎么抖都不改变状态。window参数是滑动窗口帧数。30fps视频里window5大约覆盖0.17秒足以压掉单帧识别噪声又不会让动作开始时间和实际差太多。如果视频是60fpswindow可以取7到9原理一样都是为了覆盖相近的真实时间。3.3 主循环与可视化OpenCV画线画点写计数主循环的任务是逐帧读取测试视频12.mp4调用PoseUtil拿关键点计算肩-髋-膝角度推进状态机再把关键点和当前计数画到帧上最后写入pushupoutput.mp4。这里最容易翻车的是VideoWriter参数——fps不匹配会让输出视频加速或减速播放宽高不对会直接写不了文件。import cv2 cap cv2.VideoCapture(12.mp4) fps cap.get(cv2.CAP_PROP_FPS) # 用原视频帧率不要写死 30 width int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) writer cv2.VideoWriter(pushupoutput.mp4, cv2.VideoWriter_fourcc(*mp4v), fps, (width, height)) util PoseUtil(model_complexity1) state, count DOWN, 0 def midpoint(p1, p2): return {x: (p1[x] p2[x]) / 2, y: (p1[y] p2[y]) / 2} while True: ret, frame cap.read() if not ret: break pts util.get_landmarks(frame) if pts is None: writer.write(frame) # 没检测到人也照常写帧保持时间轴 continue shoulder midpoint(pts[11], pts[12]) hip midpoint(pts[23], pts[24]) knee midpoint(pts[25], pts[26]) angle angle_between(shoulder, hip, knee) if state DOWN and angle 140: state UP elif state UP and angle 160: state DOWN count 1 # 画出 6 个关键点方便回看识别结果 for idx in (11, 12, 23, 24, 25, 26): p pts[idx] cv2.circle(frame, (int(p[x]), int(p[y])), 5, (0, 255, 0), -1) cv2.putText(frame, fcount: {count}, (30, 60), cv2.FONT_HERSHEY_SIMPLEX, 2, (0, 0, 255), 3) writer.write(frame) util.close() cap.release() writer.release()这里把6个关键点用绿色画在帧上回看时能一眼看出到底是识别错了还是动作本身不标准计数写在左上角字号放大因为输出视频经常要投屏演示。没检测到人的帧也不能丢直接原样写进输出视频否则最终mp4会缺帧、画面卡顿。一个细节midpoint函数返回的结构和关键点字典保持一致同样带x、y字段所以angle_between的入参格式统一不用为中点单独写一套计算。4. 避坑排查五个让计数直接翻车的真实场景4.1 个数忽多忽少先查阈值而不是查代码现象同样的视频程序跑出的计数和人工数出的差一半甚至一次动作被计成三次。原因多数情况不是识别失败而是阈值滞回区间不合适。up_th设得过高起身过程中的抖动帧提前满足起身条件down_th设得太低躺下过程中还没躺平又触发一次计数两个阈值挨得太近等于把滞回机制废掉。解决我一般先把角度序列导出成CSV看一眼真实曲线的峰谷值平躺时大概多少度标准坐起时最低到多少度。用最低起身角减15度左右作为up_th用平躺角减5度左右作为down_th。别凭感觉拍脑袋曲线会告诉你答案。4.2 只起半个也算一次up_th设太低现象人根本没有完全坐直只把上半身抬离地面一点计数就开始跳。原因up_th取值比半程角度还低导致半程动作就闯入起身状态。另外如果动作靠惯性甩起来肩部最高点短暂超过阈值也会被误判成一次完整起身。解决把up_th往上提至少在标准动作最低角度的基础上加10度以上。体能测试场景里宁可漏计也不要多计多计一次比少计一次更伤害公信力。还有一招观察相邻帧的角度变化量突然的甩动会产生非常大的瞬时变化变化量超过30度直接沿用上一帧角度把惯性动作过滤掉。4.3 关键点瞬间跳变visibility过滤要做在源头现象角度曲线出现90度甚至更离谱的尖峰计数跟着乱跳。原因手在髋部附近摆动、身体出画、或者深色衣服配深色背景都可能让个别关键点检测突然失效。MediaPipe虽然给每个点标了visibility但直接用坐标算角度时完全不看这个值异常坐标就被送进状态机了。解决在get_landmarks里做两层防护。第一层visibility低于0.5的关键点直接置为None外层判断任一核心点为None就跳过这一帧第二层对角度做一阶差分瞬时变化超过30度就沿用上一帧角度。数据干净了状态机才不会被异常帧带偏。4.4 臀部离地导致计数提前只算角度不够现象人一拱髋点就跟着往上移角度先达到阈值但肩其实没起来。原因仰卧起坐发力不标准或偷懒时经常靠抬屁股借力。髋点一上移肩-髋-膝夹角的变化是真实的但动作本身不算数。几何角度里没有包含“位置”这个概念所以借力动作也会被当成标准动作。解决加一条辅助约束。记录起始帧髋中心的y坐标当髋中心上移超过阈值比如50像素判定为借力并冻结状态机。更通用的做法是把肩中心相对初始位置的高度也放进判定条件肩没真正起来就不翻转状态。项目想要严格计分这个条件必须加。4.5 画面里出现第二个人锁定目标而不是全盘收下现象有人从镜头前走过或者场地里多个垫子同时测试计数突然被别人的动作污染。原因MediaPipe Pose本身支持多人pose_landmarks字段给的是按检测顺序排列的列表。很多实现只取第一个人顺序一换目标就飘。解决在PoseUtil里解析多个结果按关键点的边界框面积排序取面积最大的一个人体作为目标。更稳一点的做法是记录上一帧髋中心坐标选择与上一帧距离最近的人体实例作为同一对象这实质就是最简单的跨帧跟踪。测试场景场地固定这套逻辑足够用。提示以上五条建议按优先级处理。先解决阈值问题再看遮挡和多人最后才考虑借力检测避免一次改太多参数反而定位不了问题。5. 验证计数一致性用角度曲线反推阈值再批量跑测试代码改好之后第一件事不是直接看计数对不对而是把所有角度导出来画一次曲线。仰卧起坐的次数应该等于角度曲线的波谷数——每次躺平是谷每次起身是峰波谷到波峰算半次波峰回到波谷算一次完整动作。我习惯先把角度序列存成CSV用几行脚本数波谷数量和状态机输出的count对一下对不上就说明状态翻转逻辑有问题先查逻辑再谈调参。import csv with open(angles.csv, w, newline) as f: writer csv.writer(f) writer.writerow([frame, angle]) for i, a in enumerate(angles): writer.writerow([i, a])CSV导出来后直接画折线图波峰波谷的个数和数值一眼看清。标准动作下平躺谷底一般在160度以上起身峰顶一般在90度到110度如果峰顶只有120度说明动作本身不到位先别急着调阈值先调整测试者动作。参数含义建议范围调法up_th判定起身的角度上限130~150比峰顶角度高10~15度down_th判定躺下的角度下限150~170比谷底角度低5~10度window滑动平均帧数3~7帧率越高取越大model_complexity模型精度档位0/1/2CPU吃力用0结果抖动用2批量测试的套路我固定下来把12.mp4先跑一遍人工数一次看程序输出再剪几个片段把仰卧起坐速度放慢、加快分别测确认阈值对速度不敏感。测试视频没问题之后再换真人实时摄像头。注意实时摄像头和视频文件在帧率上差别很大建议先用录制好的视频统一验证不要一上来就开摄像头否则问题混在一起根本没法定位。最后说一个我自己的教训我第一次拿到这套代码直接用默认参数跑输出计数比人工数多了7次。后来导出曲线才发现视频里测试者起身时习惯性先把头甩起来肩部还没上升头部惯性让髋部关键点被误识别了半秒钟。我把up_th从默认值往上调了15度又加了臀部上移检测计数才对上。从那以后我每次拿到别人写的姿态计数demo第一件事都是先跑一段标准动作视频把角度曲线画出来再调阈值和状态机最后才谈正式使用。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →