YOLOv8姿势估计运动计数:关键点角度与Jetson部署实践
2026/9/24 23:38:14 网站建设 项目流程

简介:面向NVIDIA Jetson平台的YOLOv8运动计数姿势估计演示项目,适合嵌入式AI开发者与健身科技爱好者,用于在边缘设备上自动识别深蹲、俯卧撑、仰卧起坐等动作并统计次数。下载页标签虽标注为java,但实际为Python实现,基于YOLOv8-Pose模型检测人体17个关键点,通过计算连线夹角判断动作是否达标。ZIP压缩包共15个文件,包括5个Python脚本、3个CSV数据文件、预训练模型权重、JSON配置及说明文档等,整体约237KB,目录结构清晰。目前已有80人浏览学习,适合需要部署轻量级视觉计数应用或研究姿态估计算法的人员。其中demo.py可直接运行推理,Inference.py与get_data_from_video.py支持数据提取和训练,可在此基础上扩展更多运动类型,是YOLOv8-Pose在Jetson上落地的实用参考。

1. 先别急着搭环境:YOLOv8 运动计数 姿势估计 demo 到底能做什么

很多人看到 YOLOv8 运动计数 姿势估计 这个 demo,第一反应是先在自己电脑上装一个完整的 YOLOv8 环境,结果卡在 PyTorch 和 CUDA 版本组合上大半天。我在 Jetson 上复现这套资源之后的理解是:最好先用项目里现成的 best_model.pt 把一条完整链路跑通,再回头谈训练和调参。这套 demo 做的事情其实很具体——用 YOLOv8-Pose 检测人体 17 个关键点,根据深蹲、俯卧撑、仰卧起坐动作中关节夹角的变化完成计数;reComputer Jetson J4011 上实测可用,其他 NVIDIA Jetson 设备也能迁移。适合做健身动作计数、AI 私教原型、边缘推理盒子演示的从业者,也适合拿 YOLOv8 做毕业设计但不想从零开始写状态机的学生。下面我会从判定原理一路讲到 Jetson 部署命令,再把数据整理、训练参数和踩坑记录都放出来。

2. 运动计数是怎么算出来的:从 17 个关键点、关节夹角到计数状态机

2.1 YOLOv8-Pose 不是简单画骨骼:输出里其实有 17 个关键点

YOLOv8-Pose 是 YOLOv8 检测头之外再加一个关键点回归分支,输入一帧画面后,输出每个检测人的类别、边界框和关键点坐标。COCO 姿态定义里是 17 个关键点,分别是鼻子、眼睛、耳朵、肩膀、手肘、手腕、髋部、膝盖和脚踝,索引从 0 到 16。调用pred = model(frame)之后,pred[0].keypoints.data的形状是 (num_person, 17, 3),最后一维前两个值是归一化的 x、y,第三个值是置信度。

这里有个容易忽略的点:默认输出的是归一化坐标,范围在 0~1 之间,是否要放大到像素尺寸取决于你后面怎么算角度。由于角度只和三条边的相对长度有关,归一化坐标之间算余弦并不会影响角度,但如果你要判断关键点是否在画面外,就必须先乘上图像的宽和高。常见的关键点索引对应关系如下:

索引关键点索引关键点索引关键点
0鼻子6右腕12右髋
1左眼7左髋13左膝
2右眼8右髋14右膝
3左耳9左膝15左踝
4右耳10右膝16右踝
5左肩11左踝--

我用这个索引表做开发的频率很高,几乎每个动作都要回查一遍。比如深蹲看左腿的 11、13、15 三个点,俯卧撑看左臂的 5、7、9 三个点,仰卧起坐看躯干的 5、11、13 三个点。实际项目里应该把这份索引表放进idx_2_category.json旁边,或者直接写成一个常量数组,而不是每次都打开文档数。

import torch from ultralytics import YOLO model = YOLO("best_model.pt") # 或者换成 yolov8n-pose.pt results = model("pushup_frame.jpg", verbose=False)[0] boxes = results.boxes.xyxy.cpu().numpy() keypoints = results.keypoints.data.cpu().numpy() # (N, 17, 3) for person_id, kpts in enumerate(keypoints): nose_conf = kpts[0, 2] left_hip = kpts[11, :2] left_knee = kpts[13, :2] left_ankle = kpts[15, :2] print(person_id, nose_conf, left_hip, left_knee, left_ankle)

逻辑说明:这段代码先把 keypoints 从显存拷回 CPU,再按 COCO 索引取出左腿三个关键点。results.keypoints.data是 YOLOv8 相对稳定的输出格式,但不同小版本之间字段略有差异,有的版本把置信度放在results.keypoints.conf而不是第三列,所以打印一下维度最保险。

参数说明:verbose=False可以关掉每次推理的日志输出,boxes.xyxy是四列的像素坐标检测框,keypoints.data是归一化坐标。如果要画骨骼,可以使用results.plot(),但要在画布上标注角度,还是得自己取点并用 matplotlib 或 OpenCV 画线。很多人在这步踩坑是因为results.keypoints是 Keypoints 对象不是 Tensor,直接.cpu()会报错,建议先打印类型再决定怎么转。

2.2 深蹲、俯卧撑、仰卧起坐的夹角判定逻辑:先定关节再定阈值

README 的核心思想是选有判别力的关键点,计算关键点连线之间的夹角,当夹角达到一定阈值就认为完成了某个动作。以深蹲为例,判别力最强的是大腿和小腿之间的夹角,也就是髋、膝、踝三点构成的膝角;人体直立时这个角接近 180°,蹲到底时会小于 90°。用统一的向量夹角公式可以覆盖所有动作:

import numpy as np def calc_angle(a, b, c): # a, b, c 是三个关键点的像素坐标,b 是角点 ba = a - b bc = c - b cosine = np.dot(ba, bc) / (np.linalg.norm(ba) * np.linalg.norm(bc) + 1e-6) return np.degrees(np.arccos(np.clip(cosine, -1.0, 1.0))) # 深蹲:左髋=11,左膝=13,左踝=15 hip, knee, ankle = kpts[11, :2], kpts[13, :2], kpts[15, :2] squat_angle = calc_angle(hip, knee, ankle) print(f"squat angle: {squat_angle:.1f}")

参数说明:calc_angle的输入是三个点,b是角点也就是关节所在位置;加1e-6是为了除零保护,坐标相同或关键点重叠时避免 NaN;np.clip把余弦值夹到 [-1, 1],因为浮点误差可能算出 1.0000001 导致arccos返回 NaN。这个函数在 demo_pro.py 里是最核心的底层函数,几乎所有动作计数都建立在它上面。

三种动作的关节组合和阈值可以直接映射成一张表,我建议在做开发时把它做成配置文件而不是写死在代码里:

动作判别夹角关键点组合触发阈值(示例)复位阈值(示例)
深蹲膝角髋-膝-踝膝角 < 90°膝角 > 150°
俯卧撑肘角肩-肘-腕肘角 < 80°肘角 > 140°
仰卧起坐髋角肩-髋-膝髋角 < 60°髋角 > 120°

注意这些阈值不是死的,它会受摄像头安装高度、人物距离和动作幅度影响。更稳的做法是把“下压角 < 阈值”和“回位角 > 阈值”成对保存,即下文会讲到的状态机。只用一个角度超过阈值就计数,通常会导致俯卧撑压到一半就重复计数,这个现象我在实际部署时遇到至少三次。

还有一个常被忽略的点:用单帧算角度会有抖动,尤其是手肘和膝盖这种小关节,COCO 关键点本身存在几个像素的噪声,换算成角度后可能抖动 5° 到 10°。我一般会在角度序列上做一个长度为 5 的滑动平均:

from collections import deque angle_buffer = deque(maxlen=5) def smooth_angle(angle): angle_buffer.append(angle) return sum(angle_buffer) / len(angle_buffer)

逻辑说明:deque(maxlen=5)能自动丢弃最老的帧,队列里始终保留最近五个角度值。每算完一帧就把原始角度进去,取平均后作为当前帧角度。这样既能滤掉关键点抖动,又不会带来太明显的延迟。代价是快速起落动作的响应会慢几帧,如果你对实时性要求极高,可以把窗口缩到 3。

2.3 为什么选 Jetson 而不是普通 PC 或树莓派:JetPack、TensorRT 和显存边界

这个 demo 在 NVIDIA Jetson 上跑有硬件理由,不是恰巧用 Jetson。Jetson 自带 JetPack 系统镜像,JetPack 里已经整合了 CUDA、cuDNN 和 TensorRT,YOLOv8 导出成 TensorRT engine 后在边缘设备上跑到实时的可能性比树莓派高得多。普通 PC 当然能跑,但要扛住摄像头实时推理就得看显卡;树莓派则连 YOLOv8-Pose 的 CPU 推理都很难跟上实时帧率。reComputer Jetson J4011 属于 Orin NX 模块的整机方案,显存带宽和算力都不错,跑 YOLOv8s-pose 级别的模型做单路视频计数压力不大。

Jetson 和普通 Linux 主机的部署差异主要体现在三处:系统镜像、PyTorch 来源和推理引擎。桌面 Ubuntu 上直接pip install torch通常会装上 x86_64 的 CUDA 版,但 Jetson 是 ARM64 架构,必须用 JetPack 配套的 wheel 包。很多关于 ubuntu20.04 搭建 yolov8 环境的教程,到了 Jetson 上就失效,原因就是把桌面版 CUDA 的安装方式照搬了过来。我一般建议用下表快速判断:

对比项Jetson普通 PC树莓派
x86 的 torch wheel不适用适用不适用
JetPack 自带 CUDA支持
TensorRT 加速原生支持需要额外编译不支持
实时 YOLOv8-Pose可跑到 20~30 FPS看显卡很难实时

到这里,原理部分已经能解释“为什么这类 demo 适合做成 Jetson 应用”。接下来的重点是把你手上的设备跑通,所以我直接进入部署步骤。

3. 在 Jetson 上把 demo 完整跑起来:环境、数据整理、训练四步走

3.1 先从 JetPack 装起:系统镜像、pip 和基础依赖

资源里的安装说明建议第一步把 JetPack 刷入 Jetson 设备。Jetson 刷机方式通常有两种:用 NVIDIA SDK Manager 在主机上刷,或者用厂商提供的镜像直接烧录到 eMMC/SD 卡。reComputer 整机一般出厂已烧好 JetPack,但建议你先确认系统版本,至少看到 CUDA 编译器再往下走:

nvcc --version sudo apt update sudo apt install -y python3-pip pip3 install --upgrade pip pip3 install ultralytics

注意:sudo apt update可能因为镜像源问题变慢,Jetson 上不建议一上来就换源,除非厂商镜像里的源已经失效。安装 ultralytics 时会自动拉起 numpy、opencv-python、torch 等依赖;如果你已经装了 JetPack 自带的 torch,就不要让 pip 随便覆盖,先执行pip3 list | grep torch查看当前版本。

安装完成后最快速的验证不是跑摄像头,而是先跑一张图,确认模型能够加载。把项目文件解压到~/pose_counter,然后运行:

cd ~/pose_counter python3 Inference.py --model best_model.pt --source data/squat/test.jpg

如果缺少--source参数,就打开 Inference.py 看 argparse 的默认值。这类工程代码里常见的参数是--model--source--device--conf--device在 Jetson 上写0表示第一张 CUDA 显卡,写cpu则强制 CPU 推理,通常没必要。资源里没有对 GPU 类型做特殊打包,所以只要 torch 能调用 CUDA,这一步就能直接看到一张画了骨骼点和角度的输出图。

3.2 理清推理脚本:demo.py、demo_pro.py 与 Inference.py 的分工

项目里同时出现 demo.py、demo_pro.py 和 Inference.py,很多人会困惑该跑哪个。按常见工程组织习惯,Inference.py 是把模型加载、前处理、关键点后处理封装成类的推理模块,demo.py 是调用这个模块把单张图或单段视频展示出来的最小示例,demo_pro.py 则带有完整的动作计数和可视化逻辑。你完全可以只看 demo.py 理解加载流程,但要改计次逻辑就改 demo_pro.py。

# demo_pro.py 里常见的一段计数核心逻辑 from Inference import PoseCounter import cv2 counter = PoseCounter("best_model.pt", device=0) cap = cv2.VideoCapture(0) # 0 是 Jetson 的 USB 摄像头 angle_threshold = 90 # 膝角或肘角的下限 rep_state = "up" # 初始状态 while cap.isOpened(): ok, frame = cap.read() if not ok: break rep_state, count = counter.update(frame, rep_state, angle_threshold) cv2.putText(frame, f"rep: {count}", (20, 50), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 0), 2) cv2.imshow("pose counter", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()

逻辑说明:PoseCounter.update内部先跑model(frame)得到关键点,再算动作对应的关节角,最后根据前后两帧角度判断是否完成一次动作。参数rep_state="up"表示当前动作处于回位状态,只有从 up 进入 down 再回到 up 才会计数,这个设计就是避免同一视角里角度抖动被误判成多次动作。angle_threshold是触发角度,不同动作应该动态传入,而不是在类内部写死成 90。

参数说明:cv2.VideoCapture(0)使用默认 USB 摄像头,Jetson 的 CSI 摄像头通常不能直接这么用,需要走 gstreamer 管道或者使用 Jetson 官方库。如果你手上的摄像头是 CSI 口的,请先把 demo_pro.py 里的视频源换成 gstreamer 字符串,否则打开会黑屏。

3.3 data 目录和 get_data_from_video.py:训练数据不是凭空来的

资源里 data 目录下设 squat、situp、pushup 三个子目录,生产级数据组织一般是这样的:每个子目录放该动作的视频片段或者图片帧,另外可能有一个 txt 或 json 记录每一帧的标注框和关键点。如果你只想测试计数,直接用项目里已经剪切好的视频;如果你想重新训练,最省事的方式是把手机拍的视频切成帧:

python get_data_from_video.py --video data/squat/01.mp4 \ --out data/squat/images --fps 5

常见做法是 get_data_from_video.py 里用 OpenCV 按固定间隔抽帧,避免每帧都写入导致数据集全部是重复动作。抽帧后要自己用标注工具标注每个人的框和 17 个关键点,再转成 YOLO-pose 的 txt 格式;如果没有现成标注,YOLOv8 官方也支持从 COCO 格式转换,但原项目给的是从视频抽帧这一部分,后续标注仍需人工完成。抽帧参数--fps 5表示每秒只取 5 帧,比 30fps 全取能减少约六分之五的重复样本,训练时也更快。

YOLO-pose 的标注格式和检测任务类似,只是每个点的坐标紧跟在框后面:

0 0.5 0.5 0.3 0.4 0.52 0.48 1 0.53 0.50 1 ... 0.48 0.60 1

这一行的含义是:第一列是类别,第二到第五列是x_center y_center width height,从第六列开始每三个数是一组关键点的 x、y、可见性。可见性用 0 表示未标注,1 表示可见,2 表示被遮挡但在框内。检查数据时最容易漏的是关键点数量,17 个点意味着框后面必须有 51 个数,如果漏了一个点,训练会直接报 kpt shape 错误。

3.4 用 train.py 训练自己的姿势估计模型:参数 freeze、epochs 和数据划分

训练脚本通常在 train.py 内部调用 ultralytics 的 YOLO 接口,你需要改动的不是算法细节,而是数据配置和训练参数。常见做法是在 data 同级准备一个 pose_dataset.yaml,内容如下:

path: /home/user/pose_counter/data train: images/train val: images/val kpt_shape: [17, 3] names: 0: person

然后运行:

python train.py --model yolov8n-pose.pt --data pose_dataset.yaml \ --epochs 100 --batch 32 --imgsz 640 --device 0 --freeze 10

参数说明:--model yolov8n-pose.pt是 COCO 预训练骨架,用它做迁移学习可以少标样本;--freeze 10表示冻结前 10 层网络权重,适合数据量不大时防止过拟合;--imgsz 640是训练分辨率,Jetson 上如果显存不够可以降到 480。训练完成后会在runs/pose/train/exp/weights/best_model.pt输出模型,把它覆盖到项目根目录的 best_model.pt,再跑一次 demo.py 就能看到新动作的推断结果。

这里特别提醒:train.py 不一定把 yaml 路径写在命令行,有的项目把数据路径写死在脚本里。建议打开 train.py 先看是不是model.train(data="data.yaml", ...),如果是就去改 yaml 的path字段;改错了最常见的报错是Dataset '...' not found或者 keypoint shape 对不上。另一点是 Jetson 上跑训练很慢,如果是几百张图的小数据可以接受,若是几千张图,我一般会在 x86 显卡服务器上训练完再把 best_model.pt 拷到 Jetson,这个 demo 并没有硬性要求必须在 Jetson 上训练。

4. 部署避坑与常见问题:先排查模型加载、NaN 和重复计数

4.1 模型权重加载失败或 torch 版本冲突

现象:运行model = YOLO("best_model.pt")时抛RuntimeError: PytorchStreamReader ... corrupted或直接段错误,换成 yolov8n-pose.pt 又能跑。

原因:best_model.pt 是用某个 PyTorch 版本训练保存的,当前 Jetson 上的 torch 版本不支持旧格式;或者权重文件本身没拷贝完整,SD 卡传输中断导致文件头损坏。

解决:先执行python3 -c "import torch; print(torch.__version__)"确认版本,和训练环境尽量保持一致。如果是拷贝损坏,重新传一次并比对 md5;如果确认版本不一致,就按 JetPack 版本找对应的 torch 和 torchvision wheel 包重装。这个“先查 torch 版本再谈模型”的习惯我吃过两次亏,每次都能少走很多弯路。如果你的设备上还有别的项目,建议用 venv 隔离环境,避免覆盖系统 torch。

4.2 关键点坐标突然变成 NaN 或者角度乱跳

现象:画面里只有一个人时计数正常,两个人交叉走过时角度值偶尔变成 nan,或者同一姿势下深蹲计数在 20 和 21 之间反复横跳。

原因:目标重叠导致某个人的关键点置信度很低,被过滤之后剩下的坐标数量不够;也可能calc_angle里两个向量模长为 0,除零保护失效。

解决:在算角度前用keypoints[i, idx, 2] > conf_thres过滤低置信度点,无法满足三个点都有效时直接跳过这一帧。角度计算里除了1e-6,还应该对坐标做合理性检查,比如 x、y 必须在图像范围内。把 conf_thres 设置到 0.3~0.5 可以解决大部分乱跳,但它也会降低动作捕捉的灵敏度,需要按场景折中。更复杂的情况是多目标时应该选出画面中置信度最高或框最大的人来计数,否则旁边有人路过,关键点直接跳到路人身上。

4.3 一个动作被连续记录两次或者漏计

现象:俯卧撑做一次却显示两次,或者做完了计数不动,等下一次动作快结束才突然加一。

原因:常见原因是状态机没有从 down 回到 up 的复位条件。如果只在“夹角 < 阈值”时计数,那么身体停留在最低点的几帧都被当成新的一次动作;漏计则是因为要求回到 up 的角度太高,测试者没有完全伸直手臂就进行了下一次。

解决:把角度判断改成三段状态:up -> going_down -> down -> going_up -> up,每完成一次闭环才 count + 1。触发阈值和复位阈值分开,比如肘角低于 80° 判定 down,高于 140° 判定 up;两个阈值之间的范围是滞回区间,能吸收轻微抖动。阈值不是对称的,要故意留出 20° 到 30° 的死区。用下面的状态判断逻辑替换简单阈值判断:

state = "up" count = 0 for angle in angle_sequence: if state == "up" and angle < down_threshold: state = "down" elif state == "down" and angle > up_threshold: state = "up" count += 1

逻辑说明:state只做 down 和 up 两个状态,但触发和复位分别用两个阈值;只有当状态从 up 进入 down,再回到 up,完整闭环才算一次。这样做以后,角度在阈值附近抖动时不会反复计数。

4.4 USB 摄像头打不开或者画面延迟严重

现象:cap = cv2.VideoCapture(0)返回 True 但read()一直返回 False,或者打开后画面明显滞后,推理帧率只剩个位数。

原因:Jetson 的 USB 摄像头在插拔后设备号变化,或者摄像头默认请求了 Jetson 不支持的像素格式;延迟严重则是 OpenCV 默认会在缓冲区排队旧帧,读出来的是半秒前的画面。

解决:先用ls /dev/video*确认设备节点,再在代码里设置:

cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('M', 'J', 'P', 'G')) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)

一般推荐 MJPG 格式和 640x480 分辨率。只处理当前帧可以循环读几次再推理,或者用cap.grab()丢弃过期帧,实战中画面延迟问题多数不是推理慢,而是缓冲队列没清理。如果你是在 Jetson 上用 CSI 摄像头,需要走 gstreamer 管道,设置方式和 USB 摄像头不同,建议优先用 USB 摄像头跑通整个流程。

5. 让它更像一个能上场的计数器:状态机维护、新动作扩展和导出小技巧

开发到这一步,很多人会想给它加第四个动作,比如引体向上。加动作的先决条件是确认 YOLOv8-Pose 的 17 个关键点里能提取出稳定夹角,引体向上可以用肘角区分上拉和放下;然后把你采集好的视频加入 data 目录新子文件夹,按相同流程抽帧、标注、训练、导出,再在 demo_pro.py 的动作表里加一行阈值。动作表最好做成 JSON,不要散落在代码里:

{ "squat": {"point": [11, 13, 15], "down_angle": 90, "up_angle": 150}, "pushup": {"point": [5, 7, 9], "down_angle": 80, "up_angle": 140}, "situp": {"point": [5, 11, 13], "down_angle": 60, "up_angle": 120} }

在 Inference.py 中读入这个 JSON 后,不用改主循环就能动态配置动作。新动作上线前,建议拿一段手工数过次数的视频做离线验证,用标注工具或人工计数对照输出结果,而不是直接上摄像头,否则你很难判断是阈值问题还是标注问题。

性能优化方面,Jetson 上比起改模型结构,更有效的是把 PyTorch 模型导出成 TensorRT engine:

yolo export model=best_model.pt format=engine device=0 half=True

导出后的 engine 文件配合YOLO("best_model.pt.engine")可以在 Orin NX 设备上明显降低推理延迟,代价是部署环境不能随意换。另一个性价比更高的技巧是在 model() 调用里传conf=0.4, iou=0.5,YOLOv8-Pose 有时会把背景中的人形误检出来,过滤掉低置信度目标后,计数会稳定很多。

从我在 Jetson 上复现这个资源的结果看,它最大的价值不是把标准库跑通,而是给了一套从关键点到计数状态机的完整闭环。刚拿到资源时我也想着一步到位改出五六个动作,结果卡在阈值调参上浪费了一个晚上。从那以后我每次换设备部署,都强制自己先跑到“能用原 demo 计一次标准动作”的阶段,确认摄像头、权重和动作配置三者对齐,然后再谈加需求和调阈值。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询