ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

YOLOv8姿势估计运动计数实战:从关键点到状态机

YOLOv8姿势估计运动计数实战:从关键点到状态机 简介这份资源是一套基于 YOLOv8-Pose 模型的运动计数姿势估计演示项目面向具备一定 Python 与深度学习基础、希望在 NVIDIA Jetson 边缘设备上落地视觉 AI 应用的开发者。项目通过检测人体 17 个关键点选取有判别力的关键点计算连线夹角当夹角达到阈值即判定动作完成目前支持深蹲、俯卧撑、仰卧起坐三类运动计数并已在 reComputer Jetson J4011 上完成测试部署。压缩包共 15 个文件、约 237KB包含 5 个 py 脚本推理、训练、视频取数、演示等、3 个 csv 数据文件、1 个 pt 模型权重、1 个 json 类别配置以及 md 说明、txt 资源说明、license 等结构紧凑、开箱即用。已有 80 人学习下载。读者可据此掌握关键点夹角判定的计数逻辑、Jetson 端部署流程与模型推理脚本组织方式并可在现有代码基础上扩展更多运动类型与动作识别功能。1. 用 YOLOv8 姿势估计做运动计数从关键点到计数的完整链路健身房里对着镜子做深蹲手机架在旁边自动数次数——这个场景听起来简单但真动手做就会发现目标检测只能告诉你“画面里有人”没法告诉你“这个人蹲下去了没有”。运动计数的核心不在于检测而在于理解人体姿态的变化。YOLOv8 的姿势估计Pose Estimation模型输出的 17 个 COCO 关键点恰好提供了从“有人”到“人在做什么动作”的桥梁。这个方案要解决的问题很具体给定一段运动视频或实时摄像头流自动识别动作类型并计数比如深蹲、俯卧撑、开合跳。适合谁有 Python 基础、了解 YOLO 基本推理流程、想做一个能跑起来的运动计数 Demo 的开发者。整条链路是YOLOv8-Pose 推理出关键点 → 计算关节角度 → 用角度阈值判定动作阶段 → 状态机计数。听起来不复杂但每个环节都有参数要调、有坑要踩。2. YOLOv8-Pose 推理与关键点提取模型选型、环境配置与最小可跑代码2.1 模型选型n/s/m/l/x 怎么选CPU 和 GPU 的取舍YOLOv8-Pose 提供了五个尺度的预训练权重yolov8n-pose、yolov8s-pose、yolov8m-pose、yolov8l-pose、yolov8x-pose。参数量从 3.3M 到 88.4M 不等推理速度差异巨大。我一般会这样选CPU 推理场景比如 Ubuntu 20.04 上没独显的开发机直接用 yolov8n-pose。在 i7-12700 上单帧推理大约 80-120ms勉强能跑 8-10 FPS做运动计数够用了。yolov8s-pose 在 CPU 上会掉到 5 FPS 以下体验很差。中端 GPUGTX 1660 Ti 6GByolov8s-pose 或 yolov8m-pose。1660 Ti 跑 yolov8s-pose 在 640×640 输入下大约 15-20ms 一帧实时性没问题。yolov8m-pose 大约 30-40ms也能接受。边缘设备RK3588、Orin 等优先考虑 yolov8n-pose 导出 ONNX 或 RKNN 格式。RK3588 的 NPU 对 yolov8n-pose 支持较好INT8 量化后单帧可以做到 20ms 以内。关键点是运动计数不需要极高精度但需要稳定帧率。关键点抖动一两像素不影响角度计算但帧率掉到 5 FPS 以下状态机就会漏掉动作的中间状态导致计数不准。环境配置方面Ubuntu 20.04 CPU 版本的搭建步骤# 创建虚拟环境 python3 -m venv yolov8_env source yolov8_env/bin/activate # 安装 ultralytics会自动拉取 torch CPU 版本 pip install ultralytics # 验证安装 yolo checks如果你有 NVIDIA GPU先装好对应 CUDA 版本的 PyTorch再装 ultralytics# 以 CUDA 11.8 为例 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics预训练权重不需要手动下载ultralytics 在首次推理时会自动拉取。如果你需要手动管理权重文件可以从 ultralytics 的 GitHub Release 页面获取放到项目根目录即可。2.2 最小推理代码从视频帧到 17 个关键点下面这段代码是整条链路的起点输入一帧图像输出每个人的 17 个关键点坐标和置信度from ultralytics import YOLO import cv2 import numpy as np # 加载 YOLOv8-Pose 模型n 尺度适合 CPU 或边缘设备 model YOLO(yolov8n-pose.pt) # 打开视频源0 表示默认摄像头也可以传入视频文件路径 cap cv2.VideoCapture(0) while cap.isOpened(): ret, frame cap.read() if not ret: break # 推理conf 控制检测置信度阈值 results model(frame, conf0.5, verboseFalse) # results[0].keypoints 是 Keypoints 对象 # .xy 形状为 (num_persons, 17, 2).conf 形状为 (num_persons, 17) if results[0].keypoints is not None: keypoints_xy results[0].keypoints.xy.cpu().numpy() keypoints_conf results[0].keypoints.conf.cpu().numpy() for person_idx in range(keypoints_xy.shape[0]): kps keypoints_xy[person_idx] # (17, 2) confs keypoints_conf[person_idx] # (17,) # 只保留置信度大于 0.5 的关键点 valid_mask confs 0.5 # 后续角度计算只用 valid_mask 为 True 的点 # 这里先打印左右肩和左右髋的坐标作为示例 # COCO 索引5左肩, 6右肩, 11左髋, 12右髋 print(fPerson {person_idx}:) print(f L-Shoulder: {kps[5]}, conf{confs[5]:.2f}) print(f R-Shoulder: {kps[6]}, conf{confs[6]:.2f}) print(f L-Hip: {kps[11]}, conf{confs[11]:.2f}) print(f R-Hip: {kps[12]}, conf{confs[12]:.2f}) # 可视化ultralytics 自带绘制 annotated results[0].plot() cv2.imshow(YOLOv8-Pose, annotated) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明model(frame)返回的Results对象中keypoints属性包含了所有检测到的人的关键点。.xy给出像素坐标.conf给出每个点的置信度。COCO 的 17 个关键点顺序是固定的0鼻子1左眼2右眼3左耳4右耳5左肩6右肩7左肘8右肘9左手腕10右手腕11左髋12右髋13左膝14右膝15左脚踝16右脚踝。参数说明conf0.5是检测框的置信度阈值不是关键点置信度。关键点置信度需要单独用keypoints_conf过滤。如果画面里人多可以调高conf减少误检如果动作幅度大导致关键点被遮挡可以适当降低关键点置信度阈值到 0.3但会引入更多噪声。提示model(frame)默认输入尺寸是 640×640如果原始帧分辨率很高YOLO 会先缩放再推理关键点坐标是相对于原始帧的不需要手动还原。3. 从关键点到角度关节角度计算与动作阶段判定3.1 用三个关键点算夹角向量点积的工程实现运动计数的核心数学工具是关节角度。以深蹲为例我们关心的是膝关节角度髋-膝-踝三点构成的夹角。当人站立时膝盖角度接近 180°下蹲到大腿与地面平行时膝盖角度大约 90°蹲到最低点可能到 70° 甚至更小。计算三点夹角的公式是向量点积import numpy as np def calculate_angle(a, b, c): 计算三点构成的夹角b 为顶点。 a, b, c 均为 (x, y) 格式的 numpy 数组或列表。 返回角度值单位为度范围 [0, 180]。 a np.array(a) b np.array(b) c np.array(c) # 向量 ba 和 bc ba a - b bc c - b # 点积和模长 dot_product np.dot(ba, bc) norm_ba np.linalg.norm(ba) norm_bc np.linalg.norm(bc) # 防止除零 if norm_ba 0 or norm_bc 0: return 0.0 # 余弦值裁剪到 [-1, 1]防止浮点误差导致 arccos 报错 cos_angle np.clip(dot_product / (norm_ba * norm_bc), -1.0, 1.0) angle np.degrees(np.arccos(cos_angle)) return angle逻辑说明ba是从顶点 b 指向 a 的向量bc是从 b 指向 c 的向量。点积除以模长乘积得到余弦值再用arccos反推角度。np.clip是必须的因为浮点运算可能让余弦值略微超出 [-1, 1]导致arccos返回 NaN。参数说明这个函数对关键点的顺序不敏感但顶点必须在中间。比如算左膝角度传入的顺序应该是(左髋, 左膝, 左踝)即(kps[11], kps[13], kps[15])。3.2 深蹲、俯卧撑、开合跳的角度阈值与状态机设计不同动作关心的关节不同动作主要关节关键点索引站立/起始角度最低点角度深蹲膝关节11-13-15左或 12-14-16右160°100°俯卧撑肘关节5-7-9左或 6-8-10右150°90°开合跳膝关节 手腕位置11-13-15 手腕 y 坐标160°不适用状态机的设计思路是定义一个下降阈值和一个上升阈值中间留出滞回区间防止角度在阈值附近抖动导致重复计数。class SquatCounter: def __init__(self, down_thresh100, up_thresh160): self.down_thresh down_thresh # 低于此角度认为已蹲下 self.up_thresh up_thresh # 高于此角度认为已站起 self.state up # 初始状态为站立 self.count 0 def update(self, knee_angle): if self.state up and knee_angle self.down_thresh: # 从站立进入下蹲 self.state down elif self.state down and knee_angle self.up_thresh: # 从下蹲回到站立计数一次 self.state up self.count 1 # 其他情况保持当前状态 return self.count逻辑说明状态机只有两个状态——up和down。只有当角度从高于up_thresh跌到低于down_thresh时才切换到down只有从down状态回到高于up_thresh时才计数并切回up。中间的滞回区间100° 到 160°不触发任何状态切换有效防止了关键点抖动导致的误计数。参数说明down_thresh和up_thresh需要根据实际动作幅度调整。如果用户蹲得不够深down_thresh可以放宽到 110°如果用户站不直up_thresh可以降到 150°。这两个值没有万能解建议先用一段录制视频跑一遍打印角度曲线观察实际的最小值和最大值再设定阈值。注意左右两侧的关键点可能因为遮挡或视角问题置信度不同。我一般会取左右两侧角度的平均值或者选择置信度更高的那一侧。如果两侧都低于置信度阈值这一帧就跳过不更新状态机。4. 避坑与排查关键点抖动、遮挡、多人干扰的 5 个血泪教训4.1 现象计数忽多忽少角度曲线像心电图原因YOLOv8-Pose 在单帧推理时关键点坐标会有 1-3 像素的抖动。如果直接拿原始角度喂给状态机角度在阈值附近来回跳状态机就会反复触发。解决对角度做滑动平均滤波。维护一个长度为 5 的队列每次取平均值再送入状态机。代码很简单from collections import deque angle_buffer deque(maxlen5) def smooth_angle(new_angle): angle_buffer.append(new_angle) return sum(angle_buffer) / len(angle_buffer)窗口大小 5 是我试出来的经验值太小滤波效果不够太大引入延迟深蹲快的人会漏计。4.2 现象人一转身计数直接停摆原因YOLOv8-Pose 对侧面和背面视角的关键点置信度会明显下降尤其是肩部和髋部。当置信度低于阈值时角度计算用的关键点被过滤掉状态机收不到有效输入。解决不要只依赖单侧关键点。左右两侧都算角度取置信度更高的那一侧。如果两侧都低于 0.3这一帧直接跳过保持状态机当前状态不变。另外摄像头尽量放在人的正前方或侧前方 45°避免正侧面。4.3 现象画面里两个人计数互相干扰原因results[0].keypoints返回的是所有人的关键点如果不做区分两个人的角度会混在一起送入同一个状态机。解决用检测框的 IoU 或中心点距离做简单的目标跟踪。最简单的方式是只处理画面中检测框面积最大的那个人其他人忽略。如果必须支持多人需要引入 ByteTrack 或 BoT-SORT 做 ID 分配每个 ID 维护独立的状态机。ultralytics 内置了model.track()接口可以直接用results model.track(frame, persistTrue, conf0.5) # results[0].boxes.id 就是每个检测框的 track ID4.4 现象GPU 上跑得好好的换到 CPU 后计数全乱原因CPU 推理速度慢帧率从 30 FPS 掉到 8 FPS状态机在两次推理之间“看不到”中间状态。比如深蹲从站立到最低点只用了 0.5 秒但 CPU 每 0.12 秒才出一帧可能直接从站立跳到最低点中间的下蹲过程被跳过。解决降低输入分辨率。YOLOv8 默认 640×640改成 320×320 可以让 CPU 推理速度翻倍。在model()调用时传入imgsz320即可。代价是关键点精度会下降但对于深蹲这种大动作320 分辨率足够。4.5 现象深蹲计数对了但俯卧撑完全数不出来原因俯卧撑的肘关节角度变化范围比深蹲小而且手腕位置固定YOLOv8-Pose 对手腕关键点的置信度在俯卧撑姿势下经常低于 0.5。解决俯卧撑场景下把关键点置信度阈值降到 0.3同时改用肩-肘-腕三点计算角度。如果手腕置信度仍然不够可以用肩-肘-髋的角度作为替代虽然不如肘关节直接但稳定性更好。另外俯卧撑的down_thresh和up_thresh需要重新标定不能直接套用深蹲的参数。5. 进阶技巧用视频回放验证计数逻辑以及一个我常用的调试习惯5.1 离线验证先跑视频文件再上实时摄像头实时摄像头调试计数逻辑非常痛苦因为动作过去了就过去了没法回看。我习惯先用一段录制的视频文件跑离线验证import cv2 from ultralytics import YOLO model YOLO(yolov8n-pose.pt) cap cv2.VideoCapture(squat_demo.mp4) counter SquatCounter(down_thresh100, up_thresh160) # 用于记录角度曲线方便事后分析 angle_log [] frame_idx 0 while cap.isOpened(): ret, frame cap.read() if not ret: break results model(frame, conf0.5, verboseFalse) if results[0].keypoints is not None: kps results[0].keypoints.xy.cpu().numpy() confs results[0].keypoints.conf.cpu().numpy() if kps.shape[0] 0: # 取第一个人 person_kps kps[0] person_confs confs[0] # 左膝角度左髋(11) - 左膝(13) - 左踝(15) if person_confs[11] 0.3 and person_confs[13] 0.3 and person_confs[15] 0.3: angle calculate_angle( person_kps[11], person_kps[13], person_kps[15] ) smoothed smooth_angle(angle) count counter.update(smoothed) angle_log.append((frame_idx, angle, smoothed, counter.state, count)) frame_idx 1 cap.release() # 打印角度曲线观察状态切换点 for entry in angle_log: print(fFrame {entry[0]}: raw{entry[1]:.1f}, smooth{entry[2]:.1f}, state{entry[3]}, count{entry[4]})跑完这段代码你会得到一张角度随时间变化的日志表。重点看两个地方一是平滑后的角度曲线是否单调下降再单调上升如果中间有反复说明滤波窗口不够大或者关键点抖动太严重二是状态切换的帧号是否和视频里实际动作对得上。如果计数比实际多了检查down_thresh是不是设得太高导致轻微下蹲也被判定为一次如果少了检查up_thresh是不是设得太高导致站起时角度没回到阈值以上。5.2 一个我常用的调试习惯把角度画在视频上比起看日志更直观的方式是把当前角度和计数直接画在视频帧上# 在帧上绘制角度和计数 cv2.putText(frame, fAngle: {smoothed:.1f}, (30, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) cv2.putText(frame, fCount: {counter.count}, (30, 100), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) cv2.putText(frame, fState: {counter.state}, (30, 150), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)这样在回放视频时你能同步看到角度值、状态和计数的变化一眼就能看出状态机在哪个动作阶段误判了。这个习惯帮我省了大量来回改参数的时间。5.3 参数速查表参数作用深蹲推荐值俯卧撑推荐值开合跳推荐值conf检测框置信度0.50.50.5关键点置信度阈值过滤低质量关键点0.50.30.5down_thresh判定进入下蹲/下降状态100°90°不适用up_thresh判定回到站立/起始状态160°150°不适用滤波窗口角度平滑555imgsz推理输入尺寸640GPU/ 320CPU640640最后说一个我踩过的坑不要试图用一个状态机覆盖所有动作。深蹲和俯卧撑的角度变化模式完全不同硬塞进一个状态机只会让参数互相打架。正确的做法是每个动作一个独立的状态机类通过一个动作分类器可以用简单的规则比如看手腕是否高于肩部来切换。这个方案值不值得做如果你只是想做一个能跑的运动计数 DemoYOLOv8-Pose 加状态机是最短路径一个周末就能出原型。但如果要上生产环境关键点抖动、多人跟踪、光照变化这些问题需要持续调优不是一劳永逸的事。希望帮到你。本文还有配套的精品资源点击获取
返回列表