资讯详情

YOLOv8姿态估计实现深蹲计数:从关键点检测到状态机实战

📅 2026/9/24 23:39:20 | 华诺云谱 👁 阅读
YOLOv8姿态估计实现深蹲计数:从关键点检测到状态机实战
简介面向 NVIDIA Jetson 平台的 YOLOv8 姿势估计与运动计数演示项目聚焦健身场景中的动作自动识别与计数适合边缘计算、视觉 AI 开发者学习和二次开发。项目基于 YOLOv8-Pose 模型检测人体 17 个关键点通过关键点连线夹角的阈值判定来识别动作当前支持深蹲、俯卧撑和仰卧起坐三种常见训练动作并已在 reComputer Jetson J4011 上完成实测部署。压缩包共 15 个文件体积仅 237KB包含 5 个 Python 脚本覆盖演示、推理、训练与数据提取另含 3 个 CSV 数据文件、1 个预训练模型权重以及 JSON 类别映射、README、LICENSE、标签和说明文档其中 for_detect 目录按 squat、situp、pushup 分类存放数据。已有 80 人学习下载可直接运行演示脚本完成计数也能借助训练脚本扩展动作类型适合希望快速上手 YOLOv8-Pose 边缘推理的开发者。1. 这个演示到底做什么用 YOLOv8 关键点给深蹲计数的完整链路拿到这份“使用 YOLOv8 进行运动计数的姿势估计演示.zip”第一件事应该是解压看结构而不是急着找代码。这类演示包通常就是一份 YOLOv8-pose 权重加一段推理脚本核心链路只有两步先用姿势估计模型从画面里检测出人的骨骼关键点再基于这些关键点算关节角度、判断动作阶段、累加次数。换句话说YOLOv8 负责“看见”计数逻辑负责“看懂”。我见过不少人把代码跑出画面后发现次数要么乱跳、要么一次都不动最后跑去怀疑模型实际问题出在计数状态机上。这篇文章就从模型输出讲到角度计算再落到计数逻辑和排障适合正在做健身计数、康复训练动作评估或者拿它当毕业设计基础的同学。2. 先看懂 YOLOv8-pose 的输出17 个关键点、56 通道与最小推理代码2.1 为什么选 YOLOv8-pose而不是 OpenPose 或 MediaPipe做姿势估计的现成方案不少但这几年工程落地用得最多的还是 YOLOv8-pose。OpenPose 的优势是多人姿态精度高代价是模型重、后处理链路长在普通 CPU 上跑视频几乎谈不上实时MediaPipe 的轻量化和移动端优化做得好但它和检测模型是分开的两套系统拿不到和人脸、人手统一管理的检测框而且对自采动作数据的扩展路径不够直接。YOLOv8-pose 属于单阶段模型网络主干和 YOLOv8 目标检测基本一致只是在检测头里多回归了一组关键点坐标所以一次前向既能输出人的检测框、也能输出骨架点部署链路非常短。这份演示选了 YOLOv8-pose还有一个实际原因它的 API 足够简单。Ultralytics 封装好了权重下载、推理、可视化和 ONNX 导出拿到手两三行就能跑通这对演示项目来说是决定性优势。如果后续你想换动作类别比如从深蹲换成引体向上就需要按 yolov8 训练自己的数据集的流程走用 labelme 给关键点打标签、转成训练格式、调整关键点数量重新训练那是一条独立的工程链路但至少 YOLOv8 生态里这些工具都是现成的。2.2 关键点张量怎么读8400 个候选框每个框 56 个通道YOLOv8-pose 的模型命名后缀是“-pose”比如 yolov8n-pose.pt、yolov8s-pose.pt。以输入图像缩放到 640×640 为例模型会经过三个检测头分别对应 80×80、40×40、20×20 三种尺度的特征图加起来一共 8400 个候选框。每个候选框用 56 个通道描述前 4 个是边界框的 xywh第 5 个是目标置信度剩下 51 个是 17 个关键点的数据每个点占 3 个数依次为 x、y、置信度。也就是说一行 56 维的向量就能完整描述“这个人站在哪、骨架长什么样”。这里要留意一个“黑匣子”问题官方 API 返回的 keypoints 数据已经是还原到原始图像尺寸的像素坐标不再需要你拿模型输出的比例坐标去乘 imgsz。很多人自己写 ONNX 推理时在这里翻了车手动乘一遍缩放系数结果关键点全部飘到画面外。用 Ultralytics 封装时记住直接拿 results[0].keypoints.data 就是可用的坐标。COCO 格式的 17 个关键点有一套固定索引0 鼻子1/2 左右眼3/4 左右耳5/6 左右肩7/8 左右肘9/10 左右腕11/12 左右髋13/14 左右膝15/16 左右踝。运动计数里最常用的是下半身这条链右髋是 12右膝是 14右踝是 16左腿对应 11、13、15。之后的深蹲计数我习惯固定用一侧的腿避免左右腿来回切换导致角度曲线混乱。2.3 最小推理脚本先在一张图上把点打出来拿到演示包后我一般会先不跑完整视频而是抽出一帧画面做单张推理确认模型加载正常、关键点位置符合直觉。这样可以先把模型问题隔离掉再去看计数逻辑。from ultralytics import YOLO import cv2 # 加载姿势估计模型演示包里一般自带权重文件 model YOLO(yolov8n-pose.pt) frame cv2.imread(demo_frame.jpg) results model(frame, imgsz640, conf0.35, verboseFalse)[0] # keypoints.data 形状: [检测到的人数, 17, 3] # 最后一个维度分别是 x, y, 当前关键点的置信度 points results.keypoints.data for person in points: # 右腿链: 右髋12, 右膝14, 右踝16 hip person[12].tolist() knee person[14].tolist() ankle person[16].tolist() print(hip:, hip, knee:, knee, ankle:, ankle)这段代码里的 conf 参数控制的是检测框置信度默认 0.25我习惯调到 0.35在演示场景里可以过滤掉一部分误检。imgsz 是输入网络的尺寸640 是精度和速度的平衡点如果你的机器是纯 CPU 环境可以降到 480 或 416关键点精度损失不会太明显。先把这三个点的坐标打出来下一步才能算角度。如果打印结果里出现了 0 值或负数基本可以判断是这帧画面里人的下半身被遮挡了后面计数时要做置信度过滤。3. 从坐标到角度把骨架数据换算成运动特征3.1 为什么选角度而不是坐标尺度不变与视角容错拿到关键点坐标以后最常见的错误想法是直接用“膝盖点的 y 坐标变化”来判断深蹲深度。这个方案在同一个机位、同一个人身上勉强能用但镜头稍微拉远或人换了个位置y 坐标的变化幅度就完全变了阈值根本没法固定。角度则没有这个问题髋-膝-踝三点夹角在站直时接近 180 度下蹲到低位会小于 90 度这个数值和人的胖瘦、离镜头远近没有直接关系只和动作本身有关。所以计数特征应该优先用角度。以深蹲为例核心看两个角度膝盖角度髋-膝-踝和躯干前倾角度髋-肩-膝或肩-髋-膝连线与竖直方向的夹角。膝盖角度判断下蹲深度躯干角度帮助排除“只弯腰不屈膝”的伪动作。如果只做演示效果膝盖角度一个特征已经能让计数跑起来但要做到“人不骗机器机器也不骗人”建议把两者结合起来。3.2 向量夹角实现atan2 比余弦定理更稳计算三点夹角的方法有两种一种是用余弦定理先算三条边再反解角度另一种是构造向量求夹角。我在实战里只用第二种原因是反余弦函数在 0 度和 180 度附近的导数很小也就是说角度接近极限位置时哪怕关键点只有一两个像素的抖动算出来的角度也会剧烈变化计数曲线会变得很毛糙。用 atan2 向量法角度在 0 到 180 度范围内是单调且平滑的抗噪表现更好。import math def calc_angle(a, b, c): # a: 髋, b: 膝, c: 踝均为 (x, y) 像素坐标 # 以膝盖 b 为顶点构造两条向量 v1 (a[0] - b[0], a[1] - b[1]) v2 (c[0] - b[0], c[1] - b[1]) # atan2 返回向量与 x 轴正方向的夹角范围 [-pi, pi] ang1 math.atan2(v1[1], v1[0]) ang2 math.atan2(v2[1], v2[0]) deg math.degrees(abs(ang1 - ang2)) # 超过 180 度时取补角保证角度落在 [0, 180] if deg 180: deg 360 - deg return deg这里的关键在于向量由同一个顶点指向另外两个点v1 是髋相对膝的方向v2 是踝相对膝的方向它们的夹角就是膝盖弯曲角。之所以强调用 atan2 而不是 arccos是因为 atan2 直接利用坐标判断象限避免了向量模长计算带来的浮点误差累积。实测下来同一段视频用余弦定理算出的角度序列毛刺明显更多用 atan2 后曲线就干净了许多。至于角度范围深蹲站直一般在 160 到 180 度之间全蹲低位在 70 到 90 度左右。3.3 每帧都算一遍但别把低置信度的点当真关键点置信度是 YOLOv8-pose 输出的第三个数它表示模型对这个关键点位置的信心程度。遮挡、模糊、快速运动都会让置信度掉下去。如果一个人侧身站立远端那条腿的踝关节经常会被身体挡住模型只能靠预测补出一个位置这个位置的误差通常很大。直接把这样的点拿去算角度会给计数逻辑递上一颗“定时炸弹”。我一般会在进入角度计算前加一道卡点某条腿的髋、膝、踝三个关键点置信度都大于某个阈值比如 0.3才计算角度否则这一帧直接放弃沿用上一帧的角度。这样既避免了突变实现成本也极低。如果你想看置信度下降到底长什么样可以把每帧的角度和平均置信度一起打印出来翻车的那几帧通常都对应着低置信度区间。4. 把角度变成次数状态机、双阈值与目标选择4.1 从一根曲线到一次计数为什么不能只用一个阈值有了角度序列之后最直觉的计数方式是当膝盖角度从 170 度下降到 100 度以下记一次下蹲回到 170 度以上记一次站起。但这个方式有一个致命问题——角度在阈值附近来回穿越时一次动作会被记成好几次。比如人蹲到 95 度时稍微晃了一下角度回到 105 度又再次下探到 95 度单阈值逻辑就会误以为做了两个深蹲。这不是代码 bug而是阈值判断缺乏“状态记忆”导致的。解决这个问题的标准做法叫滞回比较也叫双阈值。核心思路是给“进入下蹲”和“离开下蹲”分别设不同的阈值两个阈值之间留出缓冲区。等于说机器要看到角度明显低于某个值才认定开始蹲要看到明显高于另一个值才认定站起。中间那段缓冲区内的抖动全部被当作噪声忽略。这个思路在工业控制里叫施密特触发器在运动计数里一样好用。4.2 状态机实现STAND 与 DOWN 两态流转STATE_STAND 0 STATE_DOWN 1 DOWN_TH 100.0 # 角度低于该值判定进入下蹲 UP_TH 155.0 # 角度高于该值判定回到站姿 count 0 state STATE_STAND def update_count(angle): global state, count if state STATE_STAND and angle DOWN_TH: state STATE_DOWN elif state STATE_DOWN and angle UP_TH: state STATE_STAND count 1 return count这个状态机只有两个状态但已经足够处理深蹲计数站姿状态下等待角度下探进入蹲姿后等待角度回升回升达标才计一次数。DOWN_TH 和 UP_TH 的差值就是滞回窗口窗口越大抗抖效果越好缺点是计数会稍微滞后窗口太小基本等于退回单阈值。参数初值我给 DOWN_TH100、UP_TH155但实际一定要按你的机位和被测人实测调整后面第 6 章会讲怎么用曲线图来定这两个值。注意这里的前提是 LEFT/UP 两个状态都必须被满足动作才会被记录。也就是说一个人只蹲到 110 度就站起来永远不会触发计数因为他没进入过 DOWN 状态。这实际上是件好事——它能自动过滤掉半蹲等不标准动作如果你希望计数更宽容把 DOWN_TH 调到 120 即可。4.3 多目标画面里不只一个人怎么办演示视频里如果同时出现两个人YOLOv8-pose 会把两个人的关键点全部输出直接逐个人跑计数逻辑次数会翻倍错乱。常见做法是选一个“主角”取画面中检测框面积最大的人通常是离镜头最近、动作最清晰的那个。也可以取关键点平均置信度最高的人在多人交错场景里更稳但代码会多几行。max_area 0 target_kpts None for box, kpts in zip(results.boxes, results.keypoints.data): x1, y1, x2, y2 box.xyxy[0].tolist() area (x2 - x1) * (y2 - y1) if area max_area: max_area area target_kpts kpts if target_kpts is not None: hip target_kpts[12].tolist() knee target_kpts[14].tolist() ankle target_kpts[16].tolist()这段代码先遍历当前帧检测到的所有人用检测框的宽高算出面积保留面积最大的目标。后面的角度计算和状态机都只针对这一个目标。它的局限在于没有跨帧的目标绑定如果主角走出画面再回来计数会无缝衔接但不会保留之前的状态相当于从头开始。如果你需要跟踪特定一个人就得引入 ByteTrack 之类的多目标跟踪器给每个人分配 track_id再按 track_id 分开维护状态机。演示项目一般不做到这层但你要心里有数明白边界在哪。5. 计数不准的避坑排查4 个高频翻车点与修法5.1 现象角度曲线在阈值附近反复横跳计数频繁重复动作本身是连贯的但关键点检测是逐帧独立进行的像素级抖动会直接传导到角度上。最常见的结果是深蹲到低位时角度在 95 到 105 度之间反复穿越大几分钟计数跟着哗哗涨一次深蹲被记成三四次。原因很简单没有滤波也没有滞回。解决分两层第一层是状态机里保证 DOWN_TH 与 UP_TH 之间有足够余量第二层是给角度序列做平滑。指数平滑是我最常用的方式计算便宜实时性好。angle_smooth 175.0 # 初始值设为站直角度 alpha 0.35 # 平滑系数越大越跟手越小越平滑 def smooth_angle(angle): global angle_smooth angle_smooth alpha * angle (1 - alpha) * angle_smooth return angle_smoothalpha 取 0.3 到 0.5 之间比较合适。取值偏大响应快但滤波效果弱取值偏小曲线很平但动作快时会漏掉低位峰值。我自己的习惯是先跑一遍原始角度曲线看噪声幅度再定 alpha这比上来就拍脑袋设 0.5 靠谱。5.2 现象明明做了深蹲计数始终不增加现象是人在画面上明显蹲下去了角度值也已经低于 DOWN_TH但状态机一直停在站姿。先检查关键点可视化看髋、膝、踝三个点的位置是否稳定。很多时候深蹲时身体前倾髋部会被躯干挡住模型把髋关键点预测到了偏离真实位置的地方导致计算出的膝盖角度虚高根本没降到阈值以下。还有一种可能是你看漏了代码里计算的是膝盖角度而不是大腿与垂直方向的夹角如果被测人习惯宽站距、小腿明显前移膝盖角度的确可能变化很小。解决思路是引入第二条判断特征。把躯干角度肩-髋-膝夹角或髋部相对脚踝的水平位移纳入状态机二者满足其一就认为动作有效。演示项目里最省事的做法是把进入下蹲的判定条件从“膝盖角度小于 100”改成“膝盖角度小于 110 或躯干前倾角度大于 40”两个条件用或连接先确保计数走通再逐步收紧。5.3 现象换成正面视角后计数基本失效正面拍摄时人体的下蹲动作主要体现在深度方向也就是膝盖朝屏幕外弯髋关节向下移动。但二维关键点只有 x 和 y 坐标当人正对镜头时髋、膝、踝三点在画面里的夹角变化非常小角度几乎变成一条平线。这不是算法不行而是 2D 平面投影的信息缺失。任何基于二维关键点算角度的方案都有这个天花板。解决方法是机位选择计数用的拍摄视角必须侧放让膝盖的屈伸方向落在画面平面内。如果机位受场地限制改不了退而求其次改用“髋部相对脚踝的垂直距离”作为特征它比膝盖角度对正面视角更敏感但精度上限有限。这个坑我在早期“翻车”过好几次现在拿到演示项目第一件事就是确认摄像机视角而不是急着调参。5.4 现象CPU 上跑视频像幻灯片动作都结束了他才计完演示包默认参数通常偏保守用 640 输入、加载的还是 s 或 m 型号的权重这在只有 CPU 的环境下非常吃力。如果你是在 Ubuntu 20.04 上用 CPU 搭的 yolov8 环境这个问题最典型画面能出但每帧处理耗时超过 0.5 秒计数结果比真实动作慢出一大截。需要明确的是计数逻辑本身不慢瓶颈全在模型推理上。我建议按下面的优先级逐项放宽直到速度达标调整项默认值推荐值效果模型尺寸yolov8s-poseyolov8n-pose推理时间约减半推理 imgsz640480 或 416计算量按平方下降输入帧率全量逐帧每秒只推理 10 帧计数不受影响CPU 负载大降视频尺寸原始分辨率先缩到宽 640减少预处理耗时即使你的机器是 GTX1660Ti 这类入门独显也可以先用 n 模型加 480 输入跑通确认计数逻辑稳定后再升到 640。别一上来就追求精度演示项目第一优先级是“能实时看到反馈”这直接决定你做计数调参时的体感效率。6. 一个实用验证技巧把角度曲线画出来再调阈值调试计数逻辑时只看最终计数值是不够的因为你不知道阈值设得合不合理。把每帧角度和计数事件画在一张图里是效率最高的验证手段。做法很简单在推理循环里把角度追加到一个列表计数触发时记录当时的帧号最后用 matplotlib 画出来。import matplotlib.pyplot as plt plt.plot(angle_history, labelknee angle) plt.axhline(100, colorr, linestyle--, labelDOWN_TH) plt.axhline(155, colorg, linestyle--, labelUP_TH) for f in count_frames: plt.axvline(f, colororange, alpha0.5) plt.legend() plt.title(knee angle with count events) plt.show()图上能直接看到三条信息角度曲线的波峰波谷是否清晰、两条阈值线是否落在所有波谷之下和波峰之上、计数点是否均匀分布在完整的动作周期上。我之前的“血泪经验”是深蹲计数一次都不出画完曲线才发现那个人蹲得浅膝盖角度最低只到 115 度而我把 DOWN_TH 设成了 90阈值线悬在曲线下方从未被穿越。把 DOWN_TH 调到 120 后计数立刻正常。所以我现在拿到任何计数项目第一件事永远是画曲线其次才是写状态机。再补一个让逻辑更稳的小技巧计数成功后加一个冷却时间。深蹲动作再标准起跳瞬间也可能出现角度快速来回穿越导致一次动作触发两次计数。在 update_count 里记录上次计数的时间戳如果在 0.5 秒内再次满足起立条件直接忽略。这个冷却窗口和滞回阈值不冲突它们在两个维度上各自防抖。此外如果后续要换动作肯定逃不掉训练自己的模型环节微调时把 ultralytics 训练参数 freeze 设为 10冻结骨干网络能明显加速收敛但这属于另一个话题了。希望这篇文章能帮你把这些坑提前绕过去至少让计数跑起来的时候你知道该怀疑哪一行代码。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。