简介:本资源面向计算机视觉方向的研究者、算法工程师及具备一定深度学习基础的学生,提供一套基于YOLOv9实现的人体姿态估计完整项目源码,可用于安全监控、体育分析、人机交互、游戏娱乐与虚拟现实等场景下的关键点检测与动作理解。压缩包共188个文件,约58.75MB,以141个Python脚本为核心,配合33个YAML配置、6张示例图片、3个Shell脚本、2份Markdown说明、1个TOML配置、1个Dockerfile及1个预训练权重文件,覆盖算法实现、参数配置、数据处理与容器化部署等环节。目前已有214人学习关注。项目不仅给出可运行的关键点定位代码,还包含测试流程与数据组织方式,便于读者理解YOLOv9在姿态估计任务中的训练与推理机制,并在此基础上进行二次开发与性能调优,适合作为课程设计、毕业设计或工程落地的实战参考。
1. 从一张监控截图说起:YOLOv9 做人体姿态估计到底解决什么问题
工地上摔了一跤,事后调监控,画面里有人倒地,但系统只框出了「人」,没告诉你他是弯腰捡东西还是真摔了。这就是纯目标检测的天花板——它知道「有人」,不知道「人在做什么」。人体姿态估计要补的正是这块:把一个人拆成 17 个关键点(鼻子、左右眼、左右耳、左右肩、左右肘、左右腕、左右髋、左右膝、左右踝),输出每个点的坐标和置信度。有了骨架,摔倒、举手、攀爬、越界这些动作才有判断依据。
标题里的 YOLOv9 是 2024 年发布的目标检测框架,它本身不直接输出关键点,但它的骨干网络(Backbone)和特征融合结构(PAN-FPN)非常适合改造成关键点检测器。所谓「基于 YOLOv9 实现的人体姿态估计」,主流做法是把检测头从「类别 + 框」换成「关键点热力图或坐标回归」,复用 YOLOv9 的预训练权重做迁移学习。这套方案适合谁?适合手上有 GPU、想快速跑通一个能落地的人体姿态估计 pipeline 的工程师,尤其是已经熟悉 YOLO 系列、不想从零搭 HRNet 或 OpenPose 的人。下面我把选型、数据、训练、推理、踩坑一条线讲清楚,你照着能复现。
2. 为什么选 YOLOv9 改姿态估计:和 HRNet、OpenPose 的取舍
2.1 三条技术路线的真实差异
人体姿态估计不是只有一条路。常见做法分三类:自顶向下(Top-Down)、自底向上(Bottom-Up)、单阶段直接回归。HRNet 是自顶向下的代表,先检测人框,再对每个人框跑关键点网络,精度高但速度受人数影响大;OpenPose 是自底向上,先出所有关键点再聚类成人,人数多时速度稳但后处理复杂;YOLO-Pose 这类单阶段方案,把关键点直接挂在检测头上,一次前向出结果,速度和精度折中。
YOLOv9 改姿态估计属于第三类。它的优势在于:骨干网络已经在大规模检测数据上预训练过,特征提取能力强;PAN-FPN 结构对多尺度目标友好,小人体也能覆盖;训练和部署链路成熟,ONNX、TensorRT 导出都有现成工具。劣势也明显:关键点精度上限不如 HRNet,尤其是遮挡和极端姿态。所以选型逻辑很简单——如果你的场景是实时视频流、人数中等、对精度要求不是毫米级,YOLOv9 改姿态估计是性价比最高的;如果要做医疗康复级别的关节角度测量,老老实实上 HRNet。
2.2 改造 YOLOv9 检测头的具体做法
YOLOv9 原版输出的是[batch, num_anchors, 4+1+num_classes],姿态估计需要输出[batch, num_anchors, 4+1+17*3],其中 17 个关键点每个有 x、y、置信度三个值。改造点在检测头最后一层卷积:把输出通道从num_classes+5改成17*3+5。下面是一个简化的 PyTorch 检测头改造代码,基于 YOLOv9 的Detect模块思路:
import torch import torch.nn as nn class PoseHead(nn.Module): def __init__(self, in_channels, num_keypoints=17, num_classes=1): super().__init__() self.num_keypoints = num_keypoints # 框回归分支:4 个坐标 + 1 个 objectness + num_classes self.reg_branch = nn.Conv2d(in_channels, 4 + 1 + num_classes, 1) # 关键点分支:每个点 x, y, conf 三个值 self.kpt_branch = nn.Conv2d(in_channels, num_keypoints * 3, 1) def forward(self, x): reg = self.reg_branch(x) # [B, 5+nc, H, W] kpt = self.kpt_branch(x) # [B, 51, H, W] B, _, H, W = kpt.shape kpt = kpt.view(B, self.num_keypoints, 3, H, W) return reg, kpt逻辑说明:reg_branch负责预测人体框和置信度,kpt_branch负责预测 17 个关键点的偏移和可见性。参数上,in_channels要和 YOLOv9 neck 输出对齐,通常是 256 或 512;num_classes在只做人姿态时设为 1(person 类)。关键点分支的 3 个值里,x、y 是相对于网格的偏移量,conf 是 sigmoid 后的可见性概率。训练时框回归用 CIoU Loss,关键点用 Wing Loss 或 MSE,可见性用 BCE。
2.3 预训练权重怎么迁移
不要从零训练。YOLOv9 的官方预训练权重(在 COCO 上训的检测模型)可以直接加载到骨干和 neck 部分,检测头因为输出通道变了,需要重新初始化。常见做法是:加载权重时跳过detect层,其余层严格匹配。如果用的是 Ultralytics 风格的代码,可以在model.load_state_dict时加strict=False,然后打印缺失的 key 确认只有检测头没加载上。这一步能省掉至少 50 个 epoch 的收敛时间,血泪经验:从零训姿态估计,没有 10 万张标注图根本别想收敛。
3. 数据准备:从 COCO Keypoints 到自定义标注的完整链路
3.1 COCO Keypoints 格式拆解
COCO 的关键点标注存在annotations里,每个人体实例有keypoints字段,长度 51(17 个点 × 3),顺序是固定的:鼻子、左眼、右眼、左耳、右耳、左肩、右肩、左肘、右肘、左腕、右腕、左髋、右髋、左膝、右膝、左踝、右踝。每个点的三个值分别是 x、y、visibility(0 未标注、1 标注但不可见、2 可见)。很多人第一次做姿态估计,直接把 visibility=0 的点也当正样本训,结果模型学出一堆乱飘的骨架——这是最常见的翻车点之一。
3.2 把 COCO 转成 YOLO 训练格式
YOLOv9 训练需要每张图一个 txt,每行一个实例:class x_center y_center w h kpt1_x kpt1_y kpt1_v ...。下面是一个转换脚本的核心逻辑:
import json import os from pycocotools.coco import COCO def coco_to_yolo(coco_json, img_dir, out_dir): coco = COCO(coco_json) os.makedirs(out_dir, exist_ok=True) for img_id in coco.getImgIds(): img_info = coco.loadImgs(img_id)[0] W, H = img_info['width'], img_info['height'] ann_ids = coco.getAnnIds(imgIds=img_id, iscrowd=False) lines = [] for ann in coco.loadAnns(ann_ids): if ann.get('num_keypoints', 0) < 5: continue # 关键点太少的实例直接丢弃 x, y, w, h = ann['bbox'] # 归一化框中心 cx, cy = (x + w / 2) / W, (y + h / 2) / H nw, nh = w / W, h / H kpts = ann['keypoints'] kpt_str = [] for i in range(17): kx, ky, kv = kpts[i*3], kpts[i*3+1], kpts[i*3+2] if kv == 0: kpt_str.extend(['0', '0', '0']) else: kpt_str.extend([f'{kx/W:.6f}', f'{ky/H:.6f}', str(kv)]) lines.append(f"0 {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f} " + " ".join(kpt_str)) with open(os.path.join(out_dir, f"{img_info['file_name'].split('.')[0]}.txt"), 'w') as f: f.write("\n".join(lines))逻辑说明:num_keypoints < 5的实例直接跳过,因为关键点太少对训练是噪声。visibility=0 的点写成0 0 0,训练时通过 mask 忽略。参数上,坐标全部归一化到 0~1,这是 YOLO 系列的标准输入格式。转换完记得检查一下:随机抽 10 张图用脚本画出来,确认骨架和原图对齐,这一步能提前发现 90% 的标注错位问题。
3.3 自定义数据标注的坑
如果你要标自己的数据,推荐用 LabelMe 或 CVAT。LabelMe 导出的是 JSON,需要再转 COCO 再转 YOLO,链路长但灵活。CVAT 可以直接导出 COCO Keypoints 格式,省一步。标注时注意:遮挡点标 visibility=1 而不是 0,只有完全不在画面里的点才标 0。另外,标注顺序必须严格按 COCO 的 17 点顺序,顺序错了模型学出来的骨架是拧的,而且这种错误在 loss 曲线上看不出来,只能靠可视化发现。
4. 训练配置:超参、Loss 和显存优化的实操参数
4.1 关键超参怎么设
YOLOv9 姿态估计的训练超参和纯检测有差异。下面是一组在 4 张 3090 上跑通的配置,输入 640×640,batch 32:
| 参数 | 值 | 说明 |
|---|---|---|
| 初始学习率 | 0.01 | 用 SGD,warmup 3 epoch |
| 最终学习率 | 0.0001 | cosine 衰减 |
| 权重衰减 | 0.0005 | 防止过拟合 |
| 关键点 Loss 权重 | 0.05 | 框 Loss 权重 1.0,关键点不能喧宾夺主 |
| 可见性 Loss 权重 | 0.1 | BCE,单独一项 |
| 训练 epoch | 200 | COCO 上 150 左右收敛 |
| 数据增强 | mosaic + flip | flip 时注意左右关键点要交换索引 |
关键点 Loss 权重是最玄学的参数。设太大,框检测精度掉;设太小,关键点学不动。我一般从 0.05 起步,看 loss 曲线:如果框的 mAP 正常涨但关键点 OKS 不涨,就加到 0.1;如果框的 mAP 掉,就降到 0.02。
4.2 左右翻转时关键点索引怎么换
数据增强里的随机水平翻转,对姿态估计是个特殊坑。翻转后,左肩变右肩,左肘变右肘,索引必须交换。COCO 17 点的左右对应关系是:1↔2(眼)、3↔4(耳)、5↔6(肩)、7↔8(肘)、9↔10(腕)、11↔12(髋)、13↔14(膝)、15↔16(踝),0(鼻子)不变。代码里维护一个flip_idx数组,翻转时同步交换关键点顺序,否则模型会学到「左肩在右边」这种错误先验。
4.3 显存不够时的三个降级方案
显存爆了别急着换卡,先试这三招:第一,把输入从 640 降到 512,显存降约 36%,精度掉 1~2 个点;第二,开 AMP 混合精度,显存降 30% 左右,速度还快;第三,把 batch 降到 16 但开梯度累积 2 步,等效 batch 32。这三招组合用,12G 显存的 3060 也能跑 640 输入。注意 AMP 下关键点回归的数值稳定性,如果 loss 出现 NaN,把关键点分支的卷积强制转 float32。
5. 推理与部署:从 PyTorch 到 ONNX 再到 TensorRT 的排查清单
5.1 导出 ONNX 时的三个必查项
PyTorch 训完导出 ONNX,姿态估计比纯检测多两个输出,容易出问题。导出命令:
python export.py --weights best.pt --include onnx --img 640 --opset 12导出后必查三件事:第一,用onnx.checker.check_model验证模型合法性;第二,用onnxruntime跑一张测试图,和 PyTorch 输出对比,关键点坐标误差应小于 1e-3;第三,确认输出节点名字,姿态估计的输出通常是两个 tensor,别只拿了一个。常见翻车:opset 版本太低不支持某些算子,或者动态 batch 维度没设对,导致部署时只能跑 batch=1。
5.2 TensorRT 加速的精度对齐
TensorRT 用 FP16 推理能提速 2~3 倍,但关键点坐标对数值精度敏感。FP16 下关键点偏移量可能出现 1~2 像素的抖动,对大多数应用够用,但如果你要做关节角度计算,建议关键点分支保留 FP32,只对骨干用 FP16。TensorRT 构建 engine 时加--fp16标志,然后用trtexec跑 benchmark 确认延迟。如果发现关键点置信度输出全是 0 或 1,检查 sigmoid 层有没有被错误融合。
5.3 后处理:从输出张量到骨架图
模型输出的是归一化的关键点偏移和置信度,后处理要做三件事:把偏移量还原到原图坐标、按置信度阈值过滤低质量点、把点连成骨架。骨架连接关系是固定的 19 条边(COCO 定义),画图时用不同颜色区分左右。后处理代码里最容易错的是坐标还原:别忘了先乘回输入尺寸 640,再除以缩放比例还原到原图。这一步错了,骨架会整体偏移,但置信度看着正常,排查起来很费时间。
6. 避坑与排查:5 个真实踩过的坑
6.1 关键点全挤在图像中心
现象:训练几个 epoch 后,所有关键点预测都集中在图像中心附近,loss 不降。原因:关键点分支的初始学习率太大,或者关键点 Loss 权重过高,导致模型输出退化为均值。解决:把关键点分支的学习率单独调低(用参数组),或者把关键点 Loss 权重从 0.1 降到 0.02,重新 warmup。
6.2 验证集 OKS 很高但可视化骨架是乱的
现象:验证指标 OKS 0.7+,但画出来骨架左右颠倒或点序错乱。原因:验证时的关键点索引顺序和训练时不一致,常见于自定义数据集转换时顺序写错。解决:写一个固定的keypoint_order常量,训练、验证、推理三处共用,别各写各的。
6.3 小人体关键点全丢
现象:大的人体关键点正常,远处小人体关键点置信度全低于阈值。原因:YOLOv9 的 P3 特征图 stride 是 8,对小于 32 像素的人体,关键点分辨率不够。解决:加一个 P2 检测头(stride 4),或者把输入尺寸从 640 提到 960。代价是显存和延迟上升,按场景取舍。
6.4 训练 loss 震荡不收敛
现象:loss 在 0.5 到 2.0 之间反复跳,不下降。原因:数据增强里的 mosaic 对姿态估计太激进,四张图拼一起后关键点归属混乱。解决:前 100 epoch 关掉 mosaic,只用随机缩放和翻转,后 100 epoch 再开。或者把 mosaic 概率从 1.0 降到 0.3。
6.5 ONNX 推理结果和 PyTorch 对不上
现象:PyTorch 输出正常,ONNX 输出关键点整体偏移。原因:导出时没有把后处理里的 letterbox 参数固化,ONNX 输入直接是原图,但模型期望的是 letterbox 后的图。解决:要么在 ONNX 外面套一层预处理,要么导出时把 letterbox 写进模型。我一般选前者,预处理用 numpy 写,部署时和训练时用同一份代码。
7. 进阶技巧:用 OKS 做模型选择,别只看 mAP
训练姿态估计模型,最后选哪个 checkpoint 是个技术活。纯检测看 mAP 就行,但姿态估计的 mAP 是框的指标,关键点质量要看 OKS(Object Keypoint Similarity)。OKS 的计算逻辑是:对每个关键点,算预测点和真值的距离,除以一个和人体尺度相关的归一化因子,再按关键点类型加权。COCO 官方给的 OKS 权重里,鼻子、眼睛这些刚性点权重大,手腕、脚踝这些灵活点权重小。
实操中我一般这样做模型选择:每 10 个 epoch 在验证集上算一次 OKS,同时算框的 mAP,两个指标都涨才保留。如果 mAP 涨但 OKS 掉,说明模型在牺牲关键点精度换框精度,这时候要调关键点 Loss 权重。如果 OKS 涨但 mAP 掉,说明关键点分支过拟合了,加关键点分支的 dropout 或降学习率。
还有一个技巧:用 OKS 的阈值扫描来确定部署时的关键点置信度阈值。把验证集所有关键点的置信度和 OKS 画成散点图,找 OKS=0.5 对应的置信度作为阈值,比拍脑袋设 0.5 科学得多。这个阈值在不同场景下差异很大,室内近景可能 0.3 就够,室外远景可能要 0.7。
最后说个习惯:每次改完模型结构或 Loss,先跑 10 个 epoch 的小实验,看 loss 曲线和可视化骨架,别一上来就训 200 epoch。我在这上面浪费过至少两周的卡时,后来养成小步快跑的习惯,迭代效率翻倍。希望帮到你。
本文还有配套的精品资源,点击获取