Python+MediaPipe姿态估计的AI仰卧起坐计数方案与落地实践
简介基于AI视觉的仰卧起坐自动化计数工具面向体育测试、健身训练以及人工智能应用开发场景帮助教练和开发者摆脱手动计数的不便快速实现动作识别与个数统计。项目以Python为核心结合MediaPipe姿态估计技术对仰卧起坐动作进行关键点提取与逻辑判断覆盖从姿态解算到计数输出的完整链路适合具备一定Python基础、希望接触人体姿态识别项目的学习者。资源包共5个文件包括两个Python脚本算法主程序与姿态工具模块、两段MP4效果演示视频和一个ZIP依赖压缩包整体约284.42MB其中视频可直观展示真实运行效果ZIP包便于快速搭建依赖环境。目前已有756人学习或下载。通过这份资源读者可获得项目的全部源代码、核心函数模块、运行演示画面及依赖整理能够据此复现、改造或迁移至其他健身动作计数任务节省从头搭建和调试的时间。1. 体能测试要的是“个数”为什么需要一套 AI 仰卧起坐计数方案一次性做完一分钟的仰卧起坐、人工在旁边压脚数数数到后面往往记不清是 23 个还是 25 个改用手机录像回放慢慢数又要把视频拖来拖去。AI 健身体能测试里的“AI仰卧起坐计数个数统计”本质上就是用 Python 加普通摄像头从视频帧里识别人的姿态关键点再根据身体角度的变化自动判断起、卧动作并输出总数。它解决的是体测现场最烦人的重复劳动不需要穿戴设备不打扰动作测试完立刻给出个数、每分钟速率和动作次数曲线。适合体育老师、体能训练机构、健身 app 开发者以及想给自己做日常训练统计的个人。2. 选择姿态估计而不是传感器仰卧起坐计数怎么做才靠谱2.1 从“人形检测”到“动作判定”技术链路拆开看仰卧起坐计数看起来是个“人躺着起来再躺下”的判断问题但摄像头看到的是颜色块不是动作。要让程序理解“起”和“卧”必须经过两步先找到人的关键点再根据关键点的几何关系推算动作状态。关键点提取这一步常见做法是用姿态估计模型它会把人的鼻子、肩膀、髋部、膝盖、脚踝等部位映射成一组坐标。拿到坐标之后仰卧起坐的判定就不需要再依赖整张图片只需要看肩、髋、膝之间的几何关系。这个思路比直接训练一个“数仰卧起坐个数”的分类模型更稳因为动作判定依赖的是相对位置而不是某个固定样子的画面。这一步选型直接决定后面所有实现方式。如果选传统背景差分测试者稍微动一下、光线变一下就会把背景误判成人影如果选目标检测加小动作分类又需要准备大量仰卧起坐标注数据而且每个新场景都要重新调。姿态估计的好处是人体骨架结构是固定的肩、髋、膝这类关键点在绝大多数角度下都有明确的语义不需要针对“仰卧起坐”这个动作专门标注大量样本。2.2 MediaPipe、OpenPose 与普通摄像头方案的取舍当前开源方案里最常被拿来比较的两个是 OpenPose 和 MediaPipe Pose。OpenPose 精度高、关键点全但它依赖较大的神经网络CPU 上跑起来很吃力体测现场一般只有普通笔记本实时代码会卡到没法用。MediaPipe Pose 是谷歌开源的轻量姿态估计方案模型文件小CPU 上有较稳定的实时推理速度输出 33 个人体关键点覆盖肩、髋、膝、肘这些仰卧起坐判定需要的部位。俯卧撑、深蹲这类动作计数也常见到 MediaPipe Pose说明这套关键点坐标判断动作状态的通用性已经比较成熟。相比之下智能手表、腰带式传感器方案虽然精度高但每套设备都要固定绑在测试者身上多人轮流测试时消毒、佩戴、校准的时间比测试本身还长。摄像头方案的现场接受度明显更高这也是把它作为“AI健身体能测试”落地首选的原因。下表是我在选型时比较关心的几个维度对比项MediaPipe PoseOpenPose穿戴传感器部署成本低pip 安装即可高依赖环境重高硬件采购与维护单人实时性CPU 可用需要较好 GPU实时但连接复杂是否接触身体否否是多人场景旧 API 单人稳定新 API 有多人支持支持多人但开销大每人都要设备调参友好度默认参数能跑通参数链路较深几乎不可调参2.3 最小运行环境与输入视频的约定跑这套方案不需要专门的测试房我一般按这个标准配置环境一台带摄像头的普通笔记本Python 3.8 以上安装 opencv-python、mediapipe、numpy 三个库。视频输入可以是摄像头实时画面也可以是提前录好的体测视频代码上只是读视频源的方式不同。输入视频有一个硬性约定测试者要尽量侧对着摄像头躺下让肩、髋、膝在画面里能同时看到。如果摄像头放在正前方人躺下时膝盖会挡住一部分躯干关键点判定容易乱。更稳妥的做法是摄像头放在垫子侧面约 45 度到 90 度的位置高度与地面接近这样肩到髋的躯干线在画面里最清晰后面算角度也会更稳定。3. 用 Python 和 MediaPipe 提取仰卧起坐姿态关键点3.1 安装依赖并初始化 Pose 模型先把环境准备好。下面这段命令会安装三个库其中 mediapipe 提供姿态关键点模型opencv-python 负责读摄像头和画结果numpy 用来做坐标的向量计算。pip install opencv-python mediapipe numpy装完之后初始化 Pose 模型。这里有几个参数会直接影响到后续判定稳定性我常用的初始化方式如下import cv2 import mediapipe as mp mp_pose mp.solutions.pose pose mp_pose.Pose( static_image_modeFalse, model_complexity1, smooth_landmarksTrue, min_detection_confidence0.5, min_tracking_confidence0.5 )逻辑说明static_image_modeFalse表示处理连续视频帧模型会利用前后帧的跟踪关系来稳定关键点model_complexity1是精度和速度的中间档数值越大越精细但越慢体测场景 CPU 实时跑建议先用 1smooth_landmarksTrue会做时间平滑避免关键点逐帧乱跳。两个confidence参数控制“把人识别出来”的门槛和“持续跟踪”的门槛设太低容易把墙上的影子当成人设太高则测试者稍微转身就会丢人0.5 起步是比较稳妥的经验值。3.2 从视频帧里拿到肩、髋、膝坐标初始化完成之后写一个最基础的读取帧循环把 Pose 模型跑起来cap cv2.VideoCapture(0) # 0 表示默认摄像头也可以是视频文件路径 while cap.isOpened(): ret, frame cap.read() if not ret: break # MediaPipe 的 Pose 模型期望 RGB 输入OpenCV 默认读进来是 BGR rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results pose.process(rgb) if results.pose_landmarks: # 遍历 33 个关键点取得归一化坐标 h, w, _ frame.shape for idx, lm in enumerate(results.pose_landmarks.landmark): x int(lm.x * w) y int(lm.y * h) # 这里可以按 idx 取肩、髋、膝的像素坐标 # 关键点索引左肩 11、右肩 12、左髋 23、右髋 24、左膝 25、右膝 26 if idx in [11, 12, 23, 24, 25, 26]: cv2.circle(frame, (x, y), 5, (0, 255, 0), -1) cv2.imshow(pose count, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明MediaPipe 输出的坐标是归一化的范围 0 到 1 之间所以要乘上图像的宽和高变成像素坐标。注意图像坐标系的 y 轴是向下的人在画面里坐起来时肩膀的 y 值会变小而躺下时肩和髋的 y 值接近相等这个方向关系在后面算角度时要始终记着。参数说明窗口显示名pose count可以随便改waitKey(1)里的 1 表示每帧等待 1 毫秒这样视频播放速度才接近真实帧率按 q 键退出循环。3.3 把坐标换算成角度核心数学与单位说明拿到肩、髋、膝坐标之后下一步是算“躯干离地角度”。我用的是肩部中点和髋部中点连线与水平方向的夹角平躺时这个角度接近 0 度坐起来时角度明显增大。计算代码如下import math def get_torso_angle(landmarks, frame_shape): h, w frame_shape[:2] # 取左右肩和左右髋的平均坐标避免单侧被遮挡导致抖动 left_shoulder landmarks[11] right_shoulder landmarks[12] left_hip landmarks[23] right_hip landmarks[24] shoulder_x (left_shoulder.x right_shoulder.x) / 2.0 * w shoulder_y (left_shoulder.y right_shoulder.y) / 2.0 * h hip_x (left_hip.x right_hip.x) / 2.0 * w hip_y (left_hip.y right_hip.y) / 2.0 * h # atan2 返回弧度angle 是肩髋连线与水平方向的夹角 angle_rad math.atan2(shoulder_y - hip_y, shoulder_x - hip_x) angle_deg abs(angle_rad * 180.0 / math.pi) return angle_deg逻辑说明atan2(shoulder_y - hip_y, shoulder_x - hip_x)计算的是“从髋部指向肩部”的向量与水平方向之间的夹角。因为图像 y 轴向下而肩在髋的上方shoulder_y - hip_y是负值取绝对值之后正好得到一个 0 到 90 度之间的角度。注意我这里用的是左右肩的平均值和左右髋的平均值相当于用躯干中线来判断比只用单侧更抗遮挡。这个角度的物理含义很重要它直接对应测试者“背部离地”的程度。平躺放松时肩和髋基本同高角度接近 0标准仰卧起坐坐起到最高点时肩明显高于髋角度通常会超过 40 度。这个角度就是后面计数阈值设计的基准。3.4 可视化叠加调试时你要盯着“线”不要只盯“数”计数写到最后一定会遇到“代码看起来对但实际动作识别不准”的情况这时候只打印角度数值很难定位问题。我的习惯是把关键点、连线、角度和当前计数直接画在视频上肉眼对着动作逐帧检查。mp_drawing mp.solutions.drawing_utils if results.pose_landmarks: # 画骨架线 mp_drawing.draw_landmarks( frame, results.pose_landmarks, mp_pose.POSE_CONNECTIONS, mp_drawing.DrawingSpec(color(0, 255, 0), thickness2, circle_radius3), mp_drawing.DrawingSpec(color(255, 0, 0), thickness2) ) # 在画面左上角实时显示躯干角度 angle get_torso_angle(results.pose_landmarks.landmark, frame.shape) cv2.putText(frame, fangle: {angle:.1f}, (30, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 2)逻辑说明draw_landmarks把 33 个关键点以及骨架连接线画出来POSE_CONNECTIONS是 MediaPipe 预设的连接关系表不需要自己定义。调试时的重点不是看关键点被画得对不对而是看肩髋连线角度和人体实际姿态是否一致。如果人明明坐得很高角度数字却很小说明坐标提取或角度计算公式有问题这种问题越早暴露越好。参数说明DrawingSpec里的thickness控制线宽circle_radius控制关键点半径color用 BGR 三元组。视频分辨率高时线宽和点半径可以加大一档否则在现场投屏上看不清。4. 设计仰卧起坐计数状态机角度阈值、防抖与防重复4.1 两个状态还是四个状态计数逻辑的边界拿到躯干角度之后最容易想到的逻辑是“角度超过某个阈值算一个”。但如果你真的这么写会出现两个问题第一次坐起来就计数躺下去到一半又计数人在最高点轻微晃动某个瞬间角度抖过阈值造成重复计数。所以计数不能只看单帧要看状态迁移。最简单的可靠设计是两个状态up表示躯干抬起down表示躯干平躺。一次完整的仰卧起坐必须经历down - up - down这样一个往返只有从down进入up再回到down时才累加计数。这样即使人在最高点停了 3 秒计数也不会重复。4.2 关键参数怎么调起角阈值、卧角阈值、稳定帧数参数取值直接决定识别是否贴合现场动作标准我建议起步用这组经验值参数推荐值作用UP_ANGLE45 度躯干角度超过此值判定为“起”DOWN_ANGLE15 度躯干角度低于此值判定为“卧”STABLE_FRAMES5 帧状态变化需要连续满足多少帧才生效TOUCH_PIXELS图像高度的 8%肘部与膝盖距离低于该值时辅助确定“起”到位UP_ANGLE和DOWN_ANGLE中间留了 30 度的缓冲区。意思是角度在 15 到 45 度之间的中间地带不计入任何状态只有过了边界才算状态切换。这能过滤掉那种“半起半卧”的无效动作。STABLE_FRAMES5的作用是防抖动某一帧角度突然冲到 50 度但下一帧又回到 20 度不会立即切状态。需要说明不同人的动作幅度差异很大。有人坐起到最高点时躯干角度只有 40 度有人能到 70 度。固定阈值对标准动作有效但对动作幅度偏小的人会漏计。常见做法是加入自校准测试开始时先让人平躺 2 秒采集平卧角度再坐起 2 秒采集起身角度用这两次实测值动态生成阈值等于给每个测试者单独标定边界。4.3 一人测试多人入镜只锁定目标人物体测现场常常一个垫子周围站着好几个同学或同事MediaPipe Pose 旧版本默认只输出画面里最明显的一个人一旦旁边的人离镜头更近关键点可能突然跳到另一个人身上。这个问题不解决计数就会莫名其妙乱跳。最简单的锁定手段是画面中央画一个识别框只计算框内人物的关键点。因为测试者躺下时身体中心通常就在画面中央而围观人群在两侧。更精确一点的做法是取当前帧检测到的人体肩髋中点的 x 坐标如果偏离画面中心超过一定像素就不参与计数让状态机保持上一帧的状态不变。CENTER_MARGIN 0.2 # 躯干中点偏离画面中心 20% 以上则忽略 def is_target_person(landmarks, frame_shape): h, w frame_shape[:2] left_hip landmarks[23] right_hip landmarks[24] hip_center_x (left_hip.x right_hip.x) / 2.0 * w normalized_offset abs(hip_center_x - w / 2.0) / w return normalized_offset CENTER_MARGIN逻辑说明hip_center_x是髋部中点的像素横坐标如果测试者保持在画面中央这个值应该接近w / 2。normalized_offset是偏离量与画面宽度的比值超过 0.2 就认为当前关键点不是被测者。这段代码在多人场景里非常救命它不增加额外模型负担只是用几何位置做过滤。4.4 整段代码串起来从视频到计数结果把状态机和防抖逻辑组合起来完整的计数核心如下import cv2 import math import mediapipe as mp UP_ANGLE 45.0 DOWN_ANGLE 15.0 STABLE_FRAMES 5 CENTER_MARGIN 0.2 mp_pose mp.solutions.pose def get_torso_angle(landmarks, frame_shape): h, w frame_shape[:2] ls landmarks[11] rs landmarks[12] lh landmarks[23] rh landmarks[24] sx (ls.x rs.x) / 2.0 * w sy (ls.y rs.y) / 2.0 * h hx (lh.x rh.x) / 2.0 * w hy (lh.y rh.y) / 2.0 * h return abs(math.atan2(sy - hy, sx - hx) * 180.0 / math.pi) def is_target_person(landmarks, frame_shape): h, w frame_shape[:2] lh landmarks[23] rh landmarks[24] hip_center_x (lh.x rh.x) / 2.0 * w return abs(hip_center_x - w / 2.0) / w CENTER_MARGIN pose mp_pose.Pose(model_complexity1, smooth_landmarksTrue) cap cv2.VideoCapture(0) phase down # 当前确认阶段 stable_frames 0 count 0 while cap.isOpened(): ret, frame cap.read() if not ret: break rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results pose.process(rgb) target_phase None if results.pose_landmarks: landmarks results.pose_landmarks.landmark if is_target_person(landmarks, frame.shape): angle get_torso_angle(landmarks, frame.shape) if angle UP_ANGLE: target_phase up elif angle DOWN_ANGLE: target_phase down if target_phase is not None: if target_phase ! phase: stable_frames 1 else: stable_frames 0 if stable_frames STABLE_FRAMES: if phase up and target_phase down: count 1 phase target_phase stable_frames 0 cv2.putText(frame, fcount: {count}, (30, 80), cv2.FONT_HERSHEY_SIMPLEX, 1.5, (0, 255, 0), 3) cv2.imshow(situp count, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows() print(f最终计数{count})逻辑说明phase记录当前确认阶段的取值stable_frames记录目标状态连续出现的帧数。当检测到的目标阶段与当前阶段不同时先不急着切换而是连续累积stable_frames。只有连续 5 帧都指向同一个新阶段才真正切换阶段。关键的加一动作发生在“从 up 回到 down”的那一次切换而不是“从 down 变成 up”的第一次起身这样天然避免了最高点晃动带来的重复计数。参数说明UP_ANGLE45和DOWN_ANGLE15的差值要足够大给中间状态留缓冲如果测试者动作幅度偏小可以分别降到 35 和 10但不要小于 10否则躺平时稍有晃动就会被误判为起身。STABLE_FRAMES越大越稳定但计数响应越慢30fps 视频下 5 帧大约是 0.17 秒体感是完全跟手的。5. 仰卧起坐计数落地避坑5 个最容易翻车的点5.1 现象躺下时角度反而变小计数一直卡住我调试第一版时遇到过这种情况人明明躺平了角度却显示接近 90 度坐起来反而变成 0 度整个计数逻辑完全反了。反复看代码才发现问题出在atan2(shoulder_y - hip_y, shoulder_x - hip_x)的正负方向理解上。图像坐标 y 轴向下肩在髋上方时shoulder_y - hip_y是负值得到的角度在数学上是一个负角度直接取绝对值确实能拿到 0 到 90 度之间的正数但前提是肩必须在髋上方。如果画面里人的脚尖朝向另一侧肩髋连线的朝向会反过来角度含义也跟着反。解决不要把绝对值当成万能钥匙调试时先打印未取绝对值前的正负号和角度确认当前测试方向下“坐起”对应的角度变化方向。更稳妥的做法是拿测试开始前 30 帧自动判断平躺时取到的角度如果小于起身时角度说明方向正常如果相反就对角度做180 - angle的映射保证统一语义。5.2 现象摄像头斜着拍标准动作也测不出现场条件不允许把摄像头放在完美侧面结果就是动作标准的人测出来角度只有二三十度怎么都够不到 45 度阈值。原因很好理解斜拍视角下人坐起来的垂直位移在画面里被压缩了肩部相对髋部的纵向高度差变小计算出的角度自然偏小。解决不要硬调阈值迁就角度偏小的问题转而做动态基准。测试开始前先让测试者平躺 2 秒取down_base再坐起到位 2 秒取up_base然后用(up_base down_base) / 2作为切换阈值。这样就算摄像头角度再不标准只要相对变化明显计数逻辑仍然成立。5.3 现象手抱头时肘部遮挡躯干关键点乱跳双手抱头是仰卧起坐的常见姿势但手臂和躯干重叠时MediaPipe 的肩部、肘部关键点会发生漂移躯干角度跟着上下抖动严重时一帧 50 度一帧 10 度。这个问题在动作标准测试者身上也避不开因为肘部确实会遮挡一部分肩胸区域。解决把主判据固定在肩髋中点连线角度上肘部相关数据只做辅助提示。另外不要为了追求高精度把model_complexity调到 2遮挡场景下高复杂度模型反而更容易产生大幅抖动因为它的关键点回归对局部纹理更敏感而遮挡区域的纹理是混乱的。我一般保持model_complexity1并打开smooth_landmarksTrue让时间平滑把单帧漂移吃掉。5.4 现象动作做到一半人从画面里短暂丢失测试者坐起来时身体前倾头部或肩部短暂超出画面上边界模型找不到完整关键点连续几帧results.pose_landmarks为空计数停住。动作完成时人又回到画面里结果这次“起来再躺下”没有被记录。解决当某一帧没有人形结果时不要让状态机强制回到某个默认状态而是保持上一帧的phase不变同时累积一个lost_frames计数器。只有当丢失帧数超过 30 帧时才认为测试者真的离开了位置再把phase重置为down。这样短暂离画不影响计数真正中断测试也不会在回来时产生错误加分。5.5 现象检测框里同时出现两个人计数串了前面用了is_target_person过滤偏离画面中心的人但现场有时会出现两个人都靠近画面中央比如辅助人员蹲在垫子旁边压脚。MediaPipe 单人姿态模型的输出可能在这两个人之间跳变上一帧是测试者下一帧变成压脚的人计数逻辑彻底乱掉。解决在目标过滤条件里再加一个“躯干中心高度”约束。测试者仰卧时髋部位置应该在画面下半部分压脚的人蹲着时躯干中心通常更高。记录测试者初始 5 帧的髋部中心坐标作为锚点之后每帧只认距离锚点最近的人形。这个方案不依赖额外模型只是一个坐标距离判定但很有效。6. 把 AI 计数结果做成体测报告数据导出与验收6.1 输出每次动作耗时与每分钟个数的 CSV一分钟体测除了要总数还要知道动作节奏是不是高开低走这类数据用 CSV 导出最方便。每次完成一次down - up - down切换时把当前累计序号和时间戳追加到文件里。import csv import time start_time time.time() records [] def record_repetition(count): elapsed time.time() - start_time records.append([count, round(elapsed, 2)]) with open(situp_records.csv, w, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([序号, 完成时间秒]) writer.writerows(records)逻辑说明records.append在每次成功加一时调用elapsed是从测试开始到当前动作完成的时间。写完这一版 CSV 后再用 pandas 读出来画一条“累计次数-时间”曲线一眼就能看到后半程动作速率有没有明显下降。encodingutf-8是为了避免 Excel 打开时中文乱码字段用英文写也完全可以。6.2 用抽帧回放验证计数准确性计数代码写完之后不要直接拿给体测现场用我的习惯是先录一段 30 秒的标准动作视频离线跑一遍同时人工再看视频数出真实次数。对比时会发现两种典型问题动作幅度不够导致漏计最高点晃动导致多计。验证时的具体做法是把叠加了角度和计数结果的视频用cv2.VideoWriter保存成 mp4逐帧回放。遇到角度突然越过阈值但实际动作没到位的帧把那一帧的图片截出来看关键点连线是不是画错了位置。一条经验是标注了关键点和骨架线的调试视频里如果骨架线画在身体轮廓上但计数仍然不准问题多半在阈值参数如果骨架线明显画到手臂或背景上问题在姿态关键点提取要先调整摄像头角度和画面对焦。6.3 把“能跑”变“能用”稳定性和性能的底线最后聊一个容易被忽略的细节实时性能和稳定性的取舍。体测现场没人愿意等程序一帧一帧卡着算如果发现检测延迟明显先别急着换电脑把model_complexity降到 0 试试代价是精度略有下降但帧率会明显提升。另外测试时尽量固定摄像头自动曝光而不是开启自动对焦模式尤其不要在窗户旁边测否则光线一变关键点抖动会跟着变严重。我现在的习惯是拿着这套代码去现场前先做一次静默验收测完一遍输出 CSV再人工核一遍个数误差超过 2 个就调参重来不超过就定版。这个方向的坑基本都踩过一遍了真正决定它能不能落地的不是模型选型而是角落里那个摄像头的角度。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →