
简介这是一份基于YOLOv8-Pose模型实现运动计数的姿势估计演示资源面向希望在NVIDIA Jetson等边缘设备上快速落地人体关键点检测与动作计数场景的开发者。演示借助人体17个关键点从中选出有判别力的关键点计算连线夹角并对比阈值从而判断深蹲、俯卧撑、仰卧起坐是否完成便于迁移到自定义动作识别。压缩包共15个文件大小为237KB主要包括5个脚本覆盖推理、训练、视频数据提取和程序入口、预训练模型权重、类别映射文件、数据集记录表及说明文档内容上还包含系统环境准备与依赖安装提示。这套文件结构简洁能帮助理解从数据准备到模型部署的完整流程目前已有80人浏览学习。适合有一定编程与深度学习基础、希望在边缘设备上尝试轻量级视觉应用的开发者可将其作为快速原型拓展动作类型或改进判定策略。1. 用YOLOv8做运动计数的姿势估计为什么演示包值得先跑一遍拿到一个命名很直白的演示包使用 YOLOv8 进行运动计数的姿势估计演示.zip第一反应别急着解压。这个包想解决的问题很具体对着摄像头做深蹲、俯卧撑或引体向上让程序自动判断动作有没有做完并把次数累加起来。它不靠光流、不靠手写人形匹配而是先把 YOLOv8-pose 输出的 17 个人体关键点变成角度再用状态机只认一次有效动作。这个思路能通是因为计数的本质不是“看见人在动”而是“看见关键序列从状态 A 到状态 B 再回来”。我建议先用默认权重把 demo 跑起来哪怕只读一段本地 mp4。因为姿势估计模型经常被讲得很玄乎实际落地时瓶颈几乎都在后处理——阈值怎么设、关键点置信度怎么过滤、多人画面选谁计数。跑通一次之后你才会明白为什么网上那些适合小白的 YOLOv8 教程到姿态这里突然就含糊了计数不是模型的事是策略的事。这个演示包的价值也在这里把 YOLOv8 和运动计数从“看到人”到“数出次数”的完整链条串起来。2. 姿势估计的运动计数原理从关键点到角度状态机2.1 YOLOv8-pose 到底输出了什么YOLOv8-pose 不是单独做一个姿态估计网络而是在同一个 YOLOv8 检测头外加了关键点分支。每个检测框内会预测 17 个关键点最终输出一个形状为(1, 56, 8400)的张量。以 640 输入分辨率为例56 表示 4 个框坐标加 1 个类别置信度再加 17×3每个关键点的 x、y、可见度各自占一个通道8400 是三个尺度特征图上的候选点总数。如果你对网络结构图感兴趣直接打印model.model看最后几层“Detect”模块里会多一个 keypoints 分支但我不建议把时间花在画结构图上后处理才是让运动计数跑稳的关键。用 Ultralytics 加载模型非常直接from ultralytics import YOLO model YOLO(yolov8n-pose.pt) result model.predict(squat.mp4)model.predict返回的是一个列表每个元素是一帧的结果。实际视频处理时一定要传streamTrue否则会把整段视频一次性读进内存长视频很容易把内存吃满。result model.predict(squat.mp4, streamTrue) for r in result: if r.keypoints is None: continue print(r.keypoints.data.shape) # (人数, 17, 3)这里r.keypoints.data的最后一维是 x、y、可见度置信度。17 个关键点顺序是 COCO 定义0 鼻子、1 左眼、2 右眼、3 左耳、4 右耳、5 左肩、6 右肩、7 左肘、8 右肘、9 左腕、10 右腕、11 左髋、12 右髋、13 左膝、14 右膝、15 左踝、16 右踝。做深蹲时不需要全部 17 个点只用髋、膝、踝的索引就足够。为什么计数要用角度而不是关键点的绝对坐标因为相机离人的距离不固定同一个深蹲动作在不同距离、不同分辨率下的绝对位移差很多但膝盖角度基本不变。角度对尺度和视角变化相对鲁棒这是它适合做阈值判断的原因。2.2 计数阈值怎么定角度阈值与状态迁移有了三个关键点深蹲动作就转化成一个角度髋、膝、踝三点在膝盖处形成的夹角。站立时这个角度接近 180 度下蹲到底时接近 90 度甚至更小。常见的计数阈值设计是上下阈值不同形成滞回区间动作使用关键点进入“下”状态阈值回到“上”状态阈值深蹲左髋-左膝-左踝膝角 100 度膝角 160 度俯卧撑左肩-左肘-左腕肘角 90 度肘角 150 度引体向上下巴或肩部相对横杆低于横杆高于横杆为什么不用单一阈值比如 120 度因为单一阈值会把一次深蹲拆成两次。人下蹲过程中膝盖角度从 170 度缓慢降到 90 度再缓慢升回 170 度如果用 120 度做分界线角度下降时越过一次 120上升时又越过一次 120程序会误认为做了两次。滞回阈值的做法是角度小于 100 度记为“下”只有从“下”状态再升到 160 度以上才计数一次并且回到“上”状态。这样角度在 100 到 160 之间来回抖动不会触发多余计数。状态迁移可以描述成初始状态是up计数为 0。当前帧膝角小于down_th状态变为down。如果状态已经是down并且膝角大于up_th计数加一状态变回up。如果关键点平均置信度太低直接跳过这一帧不改变状态。这个状态机的核心价值是只对“完成一次往返”计数。不管人蹲得多低、起来多快只要确实从站姿进入蹲姿再回到站姿就会进入一次计数逻辑。2.3 最小可复现运动计数脚本把上面的逻辑落到一个最小脚本里可以拿它跑通演示视频。我用的是本地 mp4方便调试改成摄像头只需要把VideoCapture参数从文件路径换成 0。import math import cv2 import numpy as np from ultralytics import YOLO def knee_angle(kps, hip_idx, knee_idx, ankle_idx): hip kps[hip_idx][:2].astype(np.float32) knee kps[knee_idx][:2].astype(np.float32) ankle kps[ankle_idx][:2].astype(np.float32) v1 hip - knee v2 ankle - knee cos_angle np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2) 1e-7) return math.degrees(math.acos(np.clip(cos_angle, -1.0, 1.0))) model YOLO(yolov8n-pose.pt) cap cv2.VideoCapture(squat.mp4) DOWN_TH 100.0 UP_TH 160.0 state up count 0 while cap.isOpened(): ret, frame cap.read() if not ret: break result model.predict(frame, verboseFalse)[0] if result.keypoints is None or len(result.keypoints.data) 0: continue kps result.keypoints.data[0].cpu().numpy() # 第一个人 conf result.keypoints.conf[0].cpu().numpy() # 每个关键点的置信度 # 参与角度计算的三个点平均置信度太低时跳过这一帧 if conf[[11, 13, 15]].mean() 0.5: continue ang knee_angle(kps, 11, 13, 15) if state up and ang DOWN_TH: state down elif state down and ang UP_TH: count 1 state up print(fcount{count}, knee_angle{ang:.1f}) cap.release()这段脚本有几个参数需要说明。DOWN_TH和UP_TH是滞回阈值100 度和 160 度是我对侧向摄像头下深蹲的常用值。如果你拍的视频是从正面或斜前方拍的膝盖角变化幅度会小一些需要把DOWN_TH调到 120、UP_TH调到 150 重新试。阈值不是越接近 180 越好留出 10 到 20 度的缓冲比硬卡边界稳定。conf[[11, 13, 15]].mean() 0.5是置信度过滤。YOLOv8 在关键点被遮挡时会外推出一个坐标但置信度会掉到 0.4 以下。如果不做过滤人半蹲出去半身时膝盖角度可能瞬间跳到 180状态机立刻被触发计数就会乱掉。跳过这一帧只会让计数慢半拍不会让计数变多。脚本只取result.keypoints.data[0]也就是检测列表里的第一个人。单人的演示场景够用但多人画面不一定是你想要的人。这一块我在第四章展开说。3. 用自己的动作视频训练 YOLOv8 姿势模型标签格式、训练命令与参数调整3.1 把 COCO 关键点数据转成 YOLO pose 标签格式演示包里的通用权重在常见动作上足够用但如果你要数的是“深蹲时膝盖到底有没有过髋”这种更严格的标准通用模型不一定卡得准。最好用自己的动作视频标注二三十个片段微调一个姿势估计模型。网上很多超详细的 YOLOv8 教程都在讲检测和分类到 pose 数据集这里最坑的就是标签格式。YOLO pose 的数据目录结构是这样的squat-dataset/ images/train/ images/val/ labels/train/ labels/val/ data.yaml每张图片对应一个 txt 标签文件。标签每一行表示一个目标class cx cy w h x1 y1 v1 x2 y2 v2 ... x17 y17 v17前五项是类别和检测框中心点、宽高后面是 17 组关键点坐标和可见性。所有坐标都除以图片宽高做归一化。v0表示不可见v1表示可见YOLO 训练时会把v0的关键点排除在损失之外。如果从 COCO 格式的 JSON 转过来核心转换逻辑如下import json coco json.load(open(coco_keypoints.json)) os.makedirs(labels/train, exist_okTrue) for ann in coco[annotations]: img_info next(i for i in coco[images] if i[id] ann[image_id]) w, h img_info[width], img_info[height] x, y, bw, bh ann[bbox] cx, cy x bw / 2, y bh / 2 # 用可见关键点的外接矩形替换原 bbox避免裁掉手脚 vis_kpts ann[keypoints] xs [vis_kpts[i] for i in range(0, len(vis_kpts), 3) if vis_kpts[i 2] 0] ys [vis_kpts[i 1] for i in range(0, len(vis_kpts), 3) if vis_kpts[i 2] 0] if xs: bx_min, bx_max min(xs), max(xs) by_min, by_max min(ys), max(ys) pad 0.05 bw (bx_max - bx_min) * (1 pad * 2) bh (by_max - by_min) * (1 pad * 2) cx (bx_min bx_max) / 2 cy (by_min by_max) / 2 line [0, f{cx / w:.6f}, f{cy / h:.6f}, f{bw / w:.6f}, f{bh / h:.6f}] for i in range(0, len(vis_kpts), 3): kx, ky vis_kpts[i] / w, vis_kpts[i 1] / h kv 1 if vis_kpts[i 2] 0 else 0 line [f{kx:.6f}, f{ky:.6f}, str(kv)] with open(flabels/train/{ann[id]}.txt, w) as f: f.write( .join(line) \n)这段代码里最容易被忽略的是 bbox 替换逻辑。COCO 原版 bbox 通常来自行人检测框它保证框住人但手、脚、头可能有一部分伸出框外。YOLO pose 训练时会根据 bbox 裁剪图片并缩放如果关键点落在 bbox 外面这些点会被裁掉导致可见关键点突然变成不可见。所以我一般会先用所有可见关键点的最小外接矩形再向外扩 5%用它替代原 bbox。类别 ID 在单类姿态数据集里写 0 就行。如果有多类动作比如同时区分深蹲和俯卧撑你需要在data.yaml里写names每个动作一个 ID对应的关键点 ID 要保持同一套 COCO 语义。3.2 训练命令和三个影响精度的参数环境配置方面常见做法是创建一个干净的 conda 环境只装ultralytics和 CUDA 版 PyTorchconda create -n yolo-pose python3.10 -y conda activate yolo-pose pip install ultralytics pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118GTX1660Ti 跑 YOLOv8 的 pose 模型没有问题但显存只有 6GB需要控制 batch 和分辨率。直接跑训练命令yolo pose train datasquat-dataset/data.yaml \ modelyolov8n-pose.pt \ epochs120 imgsz640 \ batch16 device0 patience30第一个关键参数是model。用yolov8n-pose.pt会在 COCO 预训练权重基础上迁移学习收敛快得多如果从头训练用对应的 yaml 文件。不要随手填yolov8n.yaml那是检测模型没有 pose head加载会报错。第二个关键参数是batch。GTX1660Ti 在 640 分辨率、n 模型下 batch 16 接近极限如果训练开始三十分钟内出现 CUDA out of memory直接把 batch 降到 8imgsz 降到 544优先保住能跑完。第三个参数是patience。自定义姿态数据集通常只有几百张图很容易过拟合patience30 让验证 loss 连续 30 个 epoch 不下降就自动停能省下不少时间。一些所谓“head 改进”能提升关键点精度但换 head 之前先确认默认模型在你的数据上已经过拟合。如果连训练集 loss 都降不下去问题多半在数据标注不在网络结构。如果显存不够又不想降分辨率可以从yolov8n-pose换到更小的yolov8n-pose已经是 n 系列了。再往下就是减少类别或裁剪输入 ROI。不要指望yolov8s-pose在 6GB 显卡上跑出好的 batch它比 n 系列参数多不少训练速度慢一倍效果未必有显著提升。3.3 看损失曲线判断是否过拟合YOLOv8 每次训练都会在runs/pose/train/下生成results.csv画损失曲线是判断训练是否正常的最快方式。尤其是train/pose_loss和val/pose_loss这两列能直接看出模型有没有记住训练集。import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(runs/pose/train/results.csv) fig, axes plt.subplots(1, 2, figsize(10, 4)) axes[0].plot(df[epoch], df[train/pose_loss], labeltrain pose loss) axes[0].plot(df[epoch], df[val/pose_loss], labelval pose loss) axes[1].plot(df[epoch], df[train/box_loss], labeltrain box loss) axes[1].plot(df[epoch], df[val/box_loss], labelval box loss) axes[0].legend() axes[1].legend() plt.savefig(loss_curve.png)如果train/pose_loss一路下降val/pose_loss降到某个点后开始回升就是典型过拟合。解决顺序是先加数据增强水平翻转、随机裁剪、旋转 10 度再降低训练轮次或加强早停最后才考虑换更大的模型。还有一个容易忽略的指标是metrics/mAP50-95(P)这里 P 表示 pose 的 mAP不是精度。它衡量关键点预测和真值的重合度。如果 mAP 很高但运动计数还是不准问题几乎不在模型而在后处理的阈值、状态机和多人选择逻辑。不要只盯着 mAP 调模型要先确认计数策略是对的。4. 运动计数落地避坑5 个常见问题与排查顺序4.1 计数重复跳变阈值边界抖动现象一个人慢慢做了十个深蹲程序数出十一个或者人站着不动时偶尔跳出一个计数。原因状态机的阈值区间太窄身体轻微晃动就让膝盖角度越过了DOWN_TH和UP_TH。有些人直接把两个阈值都设成 130 度一次下蹲过程中角度从 170 降到 100再从 100 升回 170中间会多次越过 130计数值自然爆炸。解决把下蹲阈值和起身阈值拉开。我一般设DOWN_TH 100UP_TH 160中间留 60 度的缓冲。此外再加入最小动作间隔两次计数之间至少间隔 0.3 秒用上一帧计数的时间戳判断避免程序把一次低头看手机的动作误判成深蹲。4.2 多人画面分不清谁来计数现象画面里出现两个人时计数有时算到左边人有时算到右边人而且每次切换都可能把计数叠加一次。原因YOLOv8 输出的人群排序不稳定每帧检测框顺序可能变化。直接取keypoints.data[0]的代码在单人场景没问题多人场景里它可能是随机的。解决使用model.track(frame, persistTrue)替代model.predict。Ultralytics 的跟踪模式会给每个人分配一个稳定的 track_id然后指定“只统计画面中心区域的人”或“只统计面积最大的目标”。我更倾向划定一个 ROI比如画面中间 60% 的矩形区域谁的检测框中心落在 ROI 内就计谁。这样有人从旁边走过不会干扰计数运动员自己也更清楚该站在哪里做动作。4.3 关键点置信度低时仍在计数现象人半蹲出去半身出画或转身时计数器突然加一。原因模型对缺半边的关键点会做外推返回一个好像合理的关键点坐标但keypoints.conf里那个点的置信度很低。如果不检查置信度角度计算会把外推坐标当成真实坐标算出一个虚假的“起身”角度。解决参与角度计算的三个关键点平均置信度低于 0.5 时跳过这一帧不更新状态。注意不要在这种情况重置状态否则人重新回到画面中间时会丢掉“刚才已经蹲下去了”这个信息。可以加调试信息把低于阈值的关键点框出来肉眼确认是遮挡还是模型本身不稳定。4.4 GTX1660Ti 训练时显存不足、损失不降现象训练命令刚启动就报 CUDA out of memory或者损失曲线停在 0.4 附近完全不下降。原因6GB 显存扛不住过大的 batch损失不降则常见于标签文件配对错误或者flip_idx没配置对。YOLOv8 训练时默认开启水平翻转如果左右关键点索引没有成对交换翻转后的标签会变成右手在左边、左手在右边模型训练时损失会一直震荡。解决显存不足时把batch16改成batch8同时设workers2减少数据加载内存。损失不降时先检查三件事图片和标签文件名是否一一对应标签里有没有全零行data.yaml的kpt_shape是否写成[17, 3]。如果数据集是自定义关键点顺序还要改yolov8n-pose.yaml里的flip_idx让它跟上你的左右索引定义。这个小配置很容易漏网上教程里很少提到。4.5 RK3588 部署姿态模型的转换注意现象模型在 PC 上跑得好好的转成 RKNN 后关键点坐标偶尔飘到画面外面计数结果变了。原因RK3588 的 NPU 对某些算子的支持不完全尤其 YOLOv8 后处理里的dfl、transpose和 NMS 部分转换时容易被折叠或替换导致 PC 端和板端输出语义不一致。解决先导出 ONNX再进行 RKNN 转换。导出命令是yolo export modelbest.pt formatonnx opset12RKNN Toolkit 转换时设target_platformrk3588do_quantization需要提供一百张左右有代表性的视频帧做量化集。板端后处理不要复用桌面端的 NMS而是按[1, 56, 8400]的原始输出自己解析先按框置信度过滤再做 NMS最后取关键点。如果量化后角度抖动明显可以把最后一层关键点分支保留浮点或做混合量化RK3588 跑 FP16 也能到几十毫秒每帧不一定非要全链路 INT8。5. 让计数更稳的进阶做法关键点平滑、计数验证与模型简化5.1 对关键点做指数平滑不做重滤波深度模型给的关键点不是每帧都刚好落在同一点即使静止不动也会有几像素抖动。我一般会对参与计数的那几个关键点做指数移动平均alpha 0.6 smoothed alpha * current (1 - alpha) * smoothed平滑之后再计算膝盖角度。alpha0.6表示当前帧占六成历史占四成能滤掉高频抖动又不会让动作显得太“粘”。卡尔曼滤波理论上更漂亮但会引入状态更新和延迟姿势估计已经自带时序信息计数场景用 EMA 足够。过度平滑反而会让动作切换变慢导致计数落后真实动作一两帧。5.2 用“录制-回放-核对”验证计数准度不要对着实时摄像头调阈值那是拿测试集当训练集调出来的参数说不清楚。常见做法是先录制一段 5 分钟、包含正面和侧面角度的动作视频人工记下每个动作发生的帧号。然后让程序跑一遍输出每次计数的帧号。对比两组帧号完全一致最好差距超过 0.5 秒就去检查那一段的置信度和角度曲线看是跳帧、多人干扰还是阈值卡得太紧。我习惯用两个指标衡量recall 表示程序数出来的次数占人工总数的比例precision 表示程序数出来的次数里真正有效动作的比例。演示场景至少要求 recall 不低于 95%。如果 recall 低多半是遮挡跳过太多如果 precision 低多半是状态机误判了半蹲或身体晃动。5.3 我是怎么看待演示代码与正式方案差距的如果还想观察模型到底关注哪些特征可以用 GradCAM 之类的方法把特征热力图叠到原图上。但做运动计数落地时我更愿意把调试视图写成“原始帧叠加上关键点、角度、状态和计数”这比特征热力图直白得多。真正的坑往往不是模型注意力偏了而是置信度过滤、阈值区间和多人选择在后处理里打架。如果只是计数深蹲、俯卧撑这一类固定动作也没必要保留全部 17 个关键点。裁剪模型输出到肩、肘、腕、髋、膝、踝这 8 个点可以减少输出通道部署到 RK3588 时模型文件更小、延迟更低。动作定义越简单计数越少依赖“玄学”。这个演示包我最初也踩过不少坑把时间花在换更花哨的 head 上结果发现真正让计数稳定下来的还是那几行状态机代码。先保住置信度过滤和滞回阈值再把模型裁剪成只出有用的点方向就对了。希望帮到你。本文还有配套的精品资源点击获取