简介这份资源是一套基于 YOLOv8-Pose 模型的运动计数姿势估计演示项目面向具备一定 Python 与深度学习基础、希望在 NVIDIA Jetson 边缘设备上落地视觉 AI 应用的开发者。项目通过检测人体 17 个关键点选取有判别力的关键点计算连线夹角当夹角达到阈值即判定动作完成目前支持深蹲、俯卧撑、仰卧起坐三类动作计数并已在 reComputer Jetson J4011 上完成测试部署。压缩包共 15 个文件约 237KB包含 5 个 py 脚本推理、训练、视频取数等、3 个 csv 数据文件、1 个 pt 模型权重、1 个 json 类别配置以及 md 说明与 license 等结构紧凑、便于直接运行与二次开发。已有 80 人学习下载。读者可据此掌握关键点角度判定逻辑、Jetson 端部署流程与模型推理脚本组织方式并在此基础上扩展更多运动类型与动作识别功能。1. 从「运动计数」说起YOLOv8 姿势估计到底在数什么健身房里做深蹲教练要数你做了多少个家里跟练 HIITApp 要判断你开合跳有没有到位。这类需求落到工程上本质是同一个问题从视频流里识别出人体关键点再根据关键点的运动轨迹判断「一次动作」是否完成最后累加计数。YOLOv8 的姿势估计Pose Estimation模型正好卡在这个位置上——它一次前向就能输出每个人体的 17 个 COCO 关键点坐标和置信度比「先检测再单独跑姿态网络」的两阶段方案省一半推理时间。这个演示包要解决的核心链路是视频帧 → YOLOv8-Pose 推理 → 关键点序列 → 动作状态机 → 计数输出。适合谁做智能健身镜、体育考试计数、康复训练监测的工程师以及想拿 YOLOv8 做毕业设计但卡在「检测出来之后怎么变成业务逻辑」这一步的同学。姿势估计本身只是中间产物真正难的是怎么把一堆坐标变成「一次有效动作」这也是后面几章要拆开讲的重点。2. YOLOv8-Pose 推理链路从权重加载到关键点输出2.1 模型选型与权重获取YOLOv8 官方提供 5 个尺度的 Pose 权重n/s/m/l/x。运动计数场景我一般直接上yolov8n-pose.pt或yolov8s-pose.pt原因很实际——计数只关心关键点相对位置不需要检测小目标n 版本在 GTX1660Ti 上跑 640×640 能到 80 FPS足够覆盖 30FPS 视频的实时处理。如果你要同时做多人计数且画面里人比较小再考虑 s 或 m。权重不需要手动去官网翻ultralytics 包会自动下载。但国内网络环境下自动下载经常卡住稳妥做法是提前把.pt文件放到项目根目录代码里直接指定本地路径。环境配置上CPU 版本用pip install ultralytics即可GPU 版本需要先装好对应 CUDA 的 PyTorch再装 ultralytics否则会默认走 CPU。# 创建虚拟环境避免和系统 Python 冲突 python -m venv venv_yolopose source venv_yolopose/bin/activate # Windows 用 venv_yolopose\Scripts\activate # 先装 PyTorch以 CUDA 11.8 为例再装 ultralytics pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics opencv-python这里有个顺序坑如果先pip install ultralytics它会自动拉一个 CPU 版 torch后面再装 GPU 版会出现版本冲突。所以先 torch 后 ultralytics是铁律。验证是否走 GPUimport torch print(torch.cuda.is_available()) # True 才说明 GPU 可用2.2 单帧推理与关键点解析YOLOv8-Pose 的输出结构比检测模型多一个维度。检测输出是[x1,y1,x2,y2,conf,cls]Pose 输出是[x1,y1,x2,y2,conf,cls, kpt_x1,kpt_y1,kpt_conf1, ... kpt_x17,kpt_y17,kpt_conf17]也就是每个框后面跟 51 个值17 个点 × 3。用 ultralytics 高层 API 时这些已经被封装好但你要做计数就必须拿到原始关键点所以得理解results[0].keypoints这个对象。from ultralytics import YOLO import cv2 import numpy as np model YOLO(yolov8n-pose.pt) def extract_keypoints(frame): 输入一帧 BGR 图像返回每个人体的 17x3 关键点数组 results model(frame, conf0.5, iou0.5, verboseFalse) if results[0].keypoints is None: return [] # keypoints.data 形状: [num_persons, 17, 3]最后一维是 (x, y, conf) kpts results[0].keypoints.data.cpu().numpy() return kpts cap cv2.VideoCapture(squat_demo.mp4) while cap.isOpened(): ret, frame cap.read() if not ret: break persons extract_keypoints(frame) for p in persons: # p[11] 左髋, p[12] 右髋, p[13] 左膝, p[14] 右膝 left_hip, right_hip p[11], p[12] left_knee, right_knee p[13], p[14] # 后续计数逻辑在这里展开 cv2.imshow(pose, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release()参数说明conf0.5是人体检测置信度阈值太低会引入误检导致计数虚高iou0.5控制 NMS 重叠阈值多人场景下如果人挨得近可以调到 0.6 减少漏检。keypoints.data里每个点的第三维是置信度低于 0.3 的点建议直接丢弃否则遮挡时坐标会乱飘后面角度计算全是噪声。2.3 关键点索引与动作相关性COCO 的 17 个关键点索引是固定的做不同动作要盯不同的点。深蹲看髋、膝、踝11-16开合跳看手腕和脚踝9-10, 15-16俯卧撑看肩、肘、腕5-10。这个映射关系必须先查清楚再写逻辑否则会出现「代码跑通了但数不准」的玄学问题。索引部位深蹲开合跳俯卧撑5/6肩--关键7/8肘--关键9/10腕-关键关键11/12髋关键--13/14膝关键--15/16踝关键关键-选点原则选运动幅度最大、遮挡概率最低的关节。深蹲时膝盖角度变化最明显所以用髋-膝-踝三点算夹角开合跳手腕和脚踝的横向距离变化最稳定就用距离阈值判断。3. 把关键点变成计数动作状态机与角度判定3.1 用三点夹角定义动作相位计数的核心不是「检测到人」而是「判断一次完整动作周期」。最通用的做法是用三点夹角定义相位。以深蹲为例计算髋-膝-踝的夹角站立时接近 180°下蹲到底时接近 90°。把连续角度序列画出来是一个周期性波形一次完整的「下蹲-站起」对应波形的一个谷底。import numpy as np def calc_angle(a, b, c): 计算 b 为顶点时a-b-c 的夹角角度制 a, b, c np.array(a[:2]), np.array(b[:2]), np.array(c[:2]) ba a - b bc c - b # 防止零向量导致除零 norm np.linalg.norm(ba) * np.linalg.norm(bc) if norm 1e-6: return None cos_val np.clip(np.dot(ba, bc) / norm, -1.0, 1.0) return np.degrees(np.arccos(cos_val)) def get_knee_angle(person_kpts): 取左腿膝角置信度不够时回退到右腿 lh, lk, la person_kpts[11], person_kpts[13], person_kpts[15] rh, rk, ra person_kpts[12], person_kpts[14], person_kpts[16] if lk[2] 0.3 and lh[2] 0.3 and la[2] 0.3: return calc_angle(lh, lk, la) if rk[2] 0.3 and rh[2] 0.3 and ra[2] 0.3: return calc_angle(rh, rk, ra) return Nonenp.clip那一步是血泪经验浮点误差会让 cos 值偶尔超出 [-1,1]arccos直接返回 nan整个计数就断了。置信度回退逻辑也很关键侧身深蹲时一条腿经常被遮挡只认一条腿会漏计数。3.2 状态机设计与阈值标定拿到角度序列后用双阈值状态机判断动作周期。设下蹲阈值down_thresh100°站立阈值up_thresh160°状态在UP和DOWN之间切换每次从 DOWN 回到 UP 就计一次数。class SquatCounter: def __init__(self, down_thresh100, up_thresh160, min_frames5): self.down_thresh down_thresh self.up_thresh up_thresh self.min_frames min_frames # 防止抖动误触发 self.state UP self.count 0 self.frame_in_state 0 def update(self, angle): if angle is None: return self.count self.frame_in_state 1 if self.state UP and angle self.down_thresh: if self.frame_in_state self.min_frames: self.state DOWN self.frame_in_state 0 elif self.state DOWN and angle self.up_thresh: if self.frame_in_state self.min_frames: self.state UP self.count 1 self.frame_in_state 0 return self.count参数怎么定down_thresh和up_thresh之间要留至少 40° 的迟滞带否则角度在阈值附近抖动会疯狂计数。min_frames是防抖帧数30FPS 下设 5 意味着一个相位至少持续 0.17 秒能过滤掉大部分关键点跳变。这两个参数没有万能值必须拿你自己的视频跑一遍把角度曲线打出来看谷底和峰顶的实际数值才是标定依据。3.3 多人场景的 ID 绑定画面里有多个人时每帧检测到的人体顺序不固定不能简单用列表下标对应。常见做法是用 YOLOv8 内置的跟踪器model.track给每个人分配 track_id然后为每个 id 维护独立的计数器。model YOLO(yolov8n-pose.pt) counters {} # track_id - SquatCounter results model.track(frame, persistTrue, conf0.5, verboseFalse) if results[0].boxes.id is not None: ids results[0].boxes.id.cpu().numpy().astype(int) kpts results[0].keypoints.data.cpu().numpy() for tid, kp in zip(ids, kpts): if tid not in counters: counters[tid] SquatCounter() angle get_knee_angle(kp) counters[tid].update(angle)persistTrue让跟踪器在帧间保持状态这是多人计数的前提。注意 track_id 在目标离开画面再回来时可能变化如果业务要求长期统计同一个人需要额外做人脸或外观特征绑定这已经超出姿势估计的范围了。4. 避坑与排查计数不准的五个真实原因4.1 现象计数只增不减或疯狂跳数原因角度阈值迟滞带太窄或者关键点置信度低导致角度在阈值附近高频抖动。解决先把down_thresh和up_thresh拉开到 50° 以上再把min_frames提到 8-10。如果还抖在角度序列上做滑动平均窗口 5 帧用平滑后的值喂给状态机。4.2 现象侧身或背对镜头时完全不计原因只用了单侧腿的关键点遮挡侧置信度低于 0.3 被丢弃。解决实现左右侧自动回退逻辑见 3.1 代码并且把置信度阈值从 0.3 降到 0.2 试试。如果两侧都遮挡说明机位有问题工程上应该引导用户调整拍摄角度而不是硬扛。4.3 现象GPU 显存溢出跑几分钟就崩原因每帧都创建新的 tensor 且没有释放或者model.track的跟踪器缓存无限增长。解决推理时加torch.no_grad()定期清理counters里长时间未出现的 track_id。另外imgsz不要盲目设 1280640 对计数场景足够显存占用差 4 倍。4.4 现象CPU 版本帧率只有 3-5 FPS原因默认用了yolov8n-pose.pt但输入分辨率没降或者没开half推理。解决CPU 场景把imgsz降到 416 甚至 320计数不需要高分辨率。如果必须 640考虑用 OpenVINO 导出加速ultralytics 支持model.export(formatopenvino)CPU 上能提速 2-3 倍。4.5 现象换一个视频计数就完全不对原因阈值是针对上一个视频标定的机位、人物身高、拍摄距离变了角度分布整体偏移。解决把阈值做成可配置参数每次换场景先跑一段「标定模式」——只输出角度曲线不计数人工看一眼谷底峰值再填参数。这是最土但最可靠的办法别指望一套阈值打天下。5. 进阶技巧用角度速度曲线提升计数鲁棒性纯阈值状态机在标准动作下够用但遇到「半蹲」「快速弹动」这类非标准动作就会翻车。我后来改用的方案是在角度基础上再算角速度用「角度 角速度」双条件触发。下蹲时角速度为负且绝对值超过阈值站起时角速度为正这样能区分「真的蹲下去了」和「只是晃了一下」。class VelocitySquatCounter: def __init__(self, down_angle100, up_angle160, vel_thresh30): self.down_angle down_angle self.up_angle up_angle self.vel_thresh vel_thresh # 度/秒 self.prev_angle None self.prev_time None self.state UP self.count 0 def update(self, angle, timestamp): if angle is None: return self.count if self.prev_angle is not None: dt timestamp - self.prev_time if dt 0: velocity (angle - self.prev_angle) / dt # 下蹲角度减小且速度够快 if self.state UP and angle self.down_angle and velocity -self.vel_thresh: self.state DOWN # 站起角度增大且速度够快 elif self.state DOWN and angle self.up_angle and velocity self.vel_thresh: self.state UP self.count 1 self.prev_angle angle self.prev_time timestamp return self.countvel_thresh30度/秒是经验值慢速控制训练可以降到 15爆发式训练可以提到 50。这个方案的好处是对静止抖动天然免疫——站着不动时角速度接近 0怎么晃都不会触发。代价是需要稳定的时间戳如果用视频文件测试记得用cap.get(cv2.CAP_PROP_POS_MSEC)拿真实时间而不是帧序号。验证方法上我习惯把角度曲线、角速度曲线、状态切换点和计数结果画在同一张图上用 matplotlib 输出成视频对照看。哪一帧误计数了一眼就能定位是角度判错还是速度判错。这个可视化脚本值得花半小时写后面调参效率能翻好几倍。最后说个习惯每次换动作类型深蹲换开合跳我不会直接改代码而是新建一个 counter 类把关键点索引、判定逻辑、阈值全部独立出来。运动计数这个方向动作种类是无限的但「取关键点 → 算特征 → 状态机 → 计数」这个骨架是通用的。骨架稳定了加新动作就是填参数的事。希望帮到你。本文还有配套的精品资源点击获取