
简介本资源面向计算机视觉方向的研究者、算法工程师与高校学生提供一套基于YOLOv9实现的人体姿态估计完整项目源码可用于安全监控、体育分析、人机交互、游戏娱乐及虚拟现实等场景下的关键点检测与动作理解。压缩包共188个文件约58.75MB以141个Python脚本为核心配合33个YAML配置、6张示例图片、3个Shell脚本、2份Markdown说明以及TOML、Dockerfile和预训练权重文件覆盖算法实现、参数配置、数据处理与部署流程。目前已有214人学习关注。项目不仅给出可运行的关键点定位代码还包含测试脚本与数据组织方式便于读者深入理解YOLOv9在姿态估计中的运作机制并在此基础上进行二次开发与改进兼顾理论先进性与实际场景中的健壮性、易用性和扩展性。1. 人体姿态估计遇上 YOLOv9为什么这套组合值得你花一个周末跑通姿态估计这件事很多人第一次接触是在健身房动作纠正、工业质检里的工人操作规范检测或者体育训练里的动作打分。传统做法是先用目标检测把人框出来再在框里跑一遍关键点回归两阶段串行延迟高、部署烦。YOLOv9 出来之后一个很自然的想法是能不能把关键点直接挂到检测头上一次前向就出人框和骨架答案是能而且这套「基于 YOLOv9 实现的人体姿态估计」的思路正是当前工业落地里性价比很高的一条路。它解决的核心问题是在保证实时性的前提下同时输出人体边界框和 17 个 COCO 关键点。适合谁适合已经跑通过 YOLO 系列检测、想往关键点方向延伸的算法工程师也适合需要做跌倒检测、手势交互、运动分析的产品团队。你不需要从零搭 backbone也不用去啃 HRNet 那种高分辨率网络的显存开销改检测头就能拿到可用的骨架输出。下面我把选型理由、数据准备、模型改造、训练调参和踩坑记录按顺序讲清楚你照着能复现。2. 从检测到关键点YOLOv9 姿态头的结构改造与选型理由2.1 为什么选 YOLOv9 而不是 YOLOv8-poseYOLOv8 已经自带 pose 分支为什么还要折腾 YOLOv9我当时的判断基于三点。第一YOLOv9 的 PGIProgrammable Gradient Information和 GELAN 结构在浅层特征保留上更稳关键点回归对浅层空间信息依赖很重尤其是手腕、脚踝这种小目标。第二YOLOv9 的辅助可逆分支在训练时能缓解深层网络的梯度消失关键点热图回归的收敛速度比直接改 YOLOv8 快一截。第三如果你手上已经有 YOLOv9 的检测权重迁移到姿态任务只需要替换检测头backbone 和 neck 可以直接复用省掉大量预训练时间。常见做法是backbone 用 YOLOv9-C 或 YOLOv9-Eneck 保持 PAN 结构把原来的检测头换成「检测头 关键点头」的双分支。检测头负责输出 person 类别的框关键点头在框内回归 17 个点的 (x, y, visibility)。这里有个细节关键点头不要直接回归绝对坐标而是回归相对于框中心的偏移量再乘以框的宽高还原这样对不同尺度的人体更鲁棒。2.2 关键点头的三种接法对比接法一在检测头旁边并联一个关键点分支共享 neck 输出。优点是结构简单训练时两个 loss 一起回传缺点是关键点分支和检测分支会抢特征小目标关键点容易抖。接法二先出框再用 ROIAlign 把框内特征抠出来送进一个小型关键点网络。这是两阶段思路精度高但速度掉得明显实时场景不推荐。接法三在检测头的每个 anchor 上直接预测关键点偏移类似 YOLO-Pose 的做法。优点是端到端、速度快缺点是对 anchor 匹配策略敏感正样本不够时关键点学不动。我一般会选接法三但在 loss 上加一个关键点可见性权重让遮挡点的梯度不要主导训练。下面是一个关键点头的最小实现片段基于 PyTorch 风格写你可以直接嵌到自己的 YOLOv9 检测头里。import torch import torch.nn as nn class PoseHead(nn.Module): def __init__(self, in_channels256, num_kpts17, num_classes1): super().__init__() # 检测分支输出框回归 类别 self.det_conv nn.Conv2d(in_channels, 4 num_classes, 1) # 关键点分支每个点预测 x偏移, y偏移, 可见性 self.kpt_conv nn.Conv2d(in_channels, num_kpts * 3, 1) self.num_kpts num_kpts def forward(self, x): det_out self.det_conv(x) # [B, 5, H, W] kpt_out self.kpt_conv(x) # [B, 51, H, W] B, _, H, W kpt_out.shape # 重排成 [B, num_kpts, 3, H, W] kpt_out kpt_out.view(B, self.num_kpts, 3, H, W) return det_out, kpt_out逻辑说明det_conv用 1x1 卷积把通道压到 54 个框参数 1 个 person 类别kpt_conv压到 5117 点 × 3 参数。关键点参数里的 x、y 是相对于当前 grid cell 的偏移visibility 过 sigmoid 变成 0 到 1 的置信度。参数上in_channels要和你 neck 最后一层输出对齐YOLOv9-C 通常是 256num_kpts按 COCO 标准是 17如果你做手部关键点就改成 21。2.3 数据准备COCO 转 YOLO-Pose 格式的四个边界坑COCO 的 keypoints 标注里每个点有 x、y、v 三个值v0 表示未标注v1 表示标注但遮挡v2 表示可见。转成 YOLO-Pose 训练格式时需要把绝对坐标归一化到 0 到 1并且把 v0 的点也保留在标签里但 loss 里要 mask 掉。坑一图像 resize 后关键点没同步缩放。很多人只改了框忘了点结果训练 loss 一直不降。坑二框的宽高为 0 时除零。COCO 里偶尔有退化框转换时要过滤。坑三关键点超出图像边界。归一化后可能出现负数或大于 1要 clip 到 0 到 1。坑四多人场景下关键点和框的对应关系错乱。COCO 的 annotation 是按人组织的转换时要保证每个框和它自己的 17 个点绑定。def coco_to_yolo_pose(img_w, img_h, bbox, keypoints): # bbox: [x, y, w, h] 绝对坐标 # keypoints: [x1,y1,v1, x2,y2,v2, ...] 共 51 个数 x, y, w, h bbox if w 0 or h 0: return None cx (x w / 2) / img_w cy (y h / 2) / img_h nw w / img_w nh h / img_h kpts_norm [] for i in range(0, len(keypoints), 3): kx, ky, kv keypoints[i], keypoints[i1], keypoints[i2] if kv 0: kpts_norm.extend([0.0, 0.0, 0.0]) else: kx min(max(kx / img_w, 0.0), 1.0) ky min(max(ky / img_h, 0.0), 1.0) kpts_norm.extend([kx, ky, 1.0]) return [cx, cy, nw, nh] kpts_norm这段转换函数里kv 0的点写成 0,0,0训练时通过第三位的 0 来 mask。min(max(...))是防止越界。返回的列表前 4 位是框后 51 位是点直接写进 YOLO 的 txt 标签即可。3. 训练配置与调参让关键点 loss 真正降下来的实操参数3.1 损失函数组合与权重设置姿态估计的 loss 一般由三部分组成框回归 lossCIoU 或 DFL、关键点坐标 lossL1 或 Wing Loss、关键点可见性 lossBCE。我常用的权重是框 loss 权重 1.0关键点坐标 loss 权重 0.05可见性 loss 权重 0.5。为什么坐标 loss 权重这么低因为关键点坐标的数值范围是 0 到 1而框的 CIoU 本身也在 0 到 1 附近如果不压低坐标 loss它会主导梯度导致框回归变差。Wing Loss 在小误差区间比 L1 更平滑适合关键点这种对精度敏感的任务。如果你用 L1手腕和脚踝的抖动会明显一些。可见性 loss 用 BCEWithLogitsLoss正样本是 v2 的点负样本是 v0 的点v1 的点忽略。import torch.nn.functional as F def pose_loss(pred_kpt, target_kpt, target_vis): # pred_kpt: [N, 17, 2] 预测坐标 # target_kpt: [N, 17, 2] 真实坐标 # target_vis: [N, 17] 可见性 0/1 coord_loss F.smooth_l1_loss(pred_kpt, target_kpt, reductionnone) coord_loss (coord_loss * target_vis.unsqueeze(-1)).sum() / (target_vis.sum() 1e-6) vis_loss F.binary_cross_entropy_with_logits(pred_vis, target_vis) return 0.05 * coord_loss 0.5 * vis_loss参数说明target_vis为 0 的点不参与坐标 loss 计算避免未标注点污染梯度。1e-6是防止除零。实际训练时框 loss 单独算最后加权求和。3.2 学习率与数据增强的配合YOLOv9 姿态训练我一般用 SGD初始学习率 0.01warmup 3 个 epochcosine 衰减到 0.0001。batch size 根据显存来单卡 24G 用 16 左右。数据增强方面Mosaic 和 MixUp 对检测有帮助但对关键点要小心Mosaic 拼接后关键点坐标要重新映射如果实现不对点会跑到别的图上。我的做法是前 10 个 epoch 开 Mosaic后面关掉让关键点回归在干净数据上收敛。另外随机缩放和旋转要同步作用于框和点。水平翻转时左右关键点要交换索引比如左肩和右肩对调。这个映射表在 COCO 的 17 点里是固定的鼻子不变左眼和右眼交换左耳和右耳交换左肩和右肩交换左肘和右肘交换左手腕和右手腕交换左髋和右髋交换左膝和右膝交换左脚踝和右脚踝交换。3.3 验证指标OKS 怎么算、多少算能用OKSObject Keypoint Similarity是姿态估计的标准指标类似检测里的 IoU。它根据每个关键点的标注方差和预测距离算相似度最后对所有点取平均。COCO 的 OKS 阈值从 0.5 到 0.95步长 0.05算 AP。工业场景里如果 OKS AP 能到 0.65 以上基本可用到 0.75 以上算不错。计算 OKS 时要注意预测点和真实点都要在同一个坐标系下通常是原图坐标。如果你在训练时用了归一化坐标验证时要还原回去。另外visibility 为 0 的点不参与 OKS 计算。import numpy as np def compute_oks(pred, gt, vis, sigmas): # pred, gt: [17, 2] # vis: [17] # sigmas: COCO 17 点的标准差 if vis.sum() 0: return 0.0 d np.linalg.norm(pred - gt, axis1) e d / (sigmas * 2) oks np.exp(-e ** 2) oks (oks * vis).sum() / vis.sum() return okssigmas是 COCO 官方给的 17 个值鼻子 0.026眼睛 0.025耳朵 0.035肩膀 0.079手肘 0.072手腕 0.062髋 0.107膝 0.087脚踝 0.089。这些值越大表示该点越难预测OKS 对误差的容忍度越高。4. 避坑与排查姿态估计训练里最常见的五类翻车4.1 关键点 loss 不降框 loss 正常现象训练几个 epoch 后框的 loss 稳定下降但关键点坐标 loss 一直在高位震荡。原因通常是关键点头的学习率太大或者正样本匹配时关键点分支没拿到足够的正样本。解决把关键点头的初始学习率设为检测头的 0.1 倍或者在 loss 里给关键点分支加一个 warmup前 5 个 epoch 只训练检测头之后再联合训练。4.2 预测的骨架整体偏移现象可视化时发现所有关键点都往一个方向偏比如统一往右下角偏。原因是坐标回归的基准点搞错了。如果你回归的是相对于 grid cell 左上角的偏移但还原时用了 cell 中心就会整体偏移半个 cell。解决统一基准要么都用左上角要么都用中心并且在 decode 时保持一致。4.3 遮挡点预测置信度异常高现象被遮挡的手腕模型给出的 visibility 接近 1但坐标完全不对。原因是训练时 v1 的点被当成正样本训练了可见性。解决可见性 loss 只把 v2 当正样本v1 和 v0 都当负样本或者 v1 直接忽略。我一般选择忽略 v1让模型只学「确定可见」和「确定不可见」。4.4 多人场景关键点串人现象两个人挨得近时A 的手腕点跑到了 B 的身上。原因是关键点分支没有和检测框绑定decode 时按全局 argmax 取点。解决关键点 decode 必须在每个框内部做先通过框的置信度筛选出人再在框对应的特征区域内取关键点。如果用的是 anchor-free 接法要确保每个 grid cell 只负责一个目标。4.5 导出 ONNX 后关键点输出维度对不上现象PyTorch 里推理正常导出 ONNX 后关键点输出变成一维或者维度错乱。原因是关键点头的 view 操作在 ONNX 里不支持动态 shape。解决把 view 换成 reshape并且在导出时固定 batch size 和输入尺寸。如果还是不行把关键点头的输出在 forward 里就整理成 [B, 17, 3] 的格式不要留到后处理。5. 进阶技巧用热图辅助回归把 OKS 再提两个点如果你已经把上面的流程跑通OKS 卡在 0.7 左右想再往上走可以试试热图辅助回归。具体做法是关键点头除了输出坐标偏移再额外输出 17 个热图每个热图是高斯峰值在真实点位置。训练时热图用 MSE loss推理时取热图峰值作为粗略位置再用偏移量做精细修正。这样做的好处是热图提供了空间上的软监督对遮挡和模糊更鲁棒。实现上热图分支可以用几层转置卷积把特征图上采样到输入尺寸的 1/4然后对每个关键点出一个通道。热图 loss 权重我一般设 1.0坐标 loss 权重降到 0.02。推理时先对热图做 sigmoid然后找每个通道的最大值位置再加上该位置的偏移预测得到最终坐标。class PoseHeadWithHeatmap(nn.Module): def __init__(self, in_channels256, num_kpts17): super().__init__() self.kpt_conv nn.Conv2d(in_channels, num_kpts * 3, 1) self.heatmap nn.Sequential( nn.ConvTranspose2d(in_channels, 128, 4, stride2, padding1), nn.ReLU(), nn.ConvTranspose2d(128, num_kpts, 4, stride2, padding1) ) self.num_kpts num_kpts def forward(self, x): kpt_out self.kpt_conv(x) hm_out self.heatmap(x) return kpt_out, hm_out热图分支的转置卷积把特征图放大 4 倍最终热图尺寸是输入的四分之一。训练时真实热图用高斯核生成sigma 取 2 个像素。推理时热图峰值位置乘以 4 得到原图坐标再加上偏移修正。验证这个方法是否有效可以固定其他参数只切换热图分支开和关跑两次验证集看 OKS AP 的变化。我自己的经验是在 COCO val2017 上加熱图后 AP 能提 1.5 到 2.5 个点但推理速度会掉 10% 左右。如果你的场景对帧率要求极高比如 60 FPS 以上这个技巧要慎重。最后说个习惯我每次改完关键点头都会先用一张固定图片跑一遍可视化把骨架画在原图上肉眼确认没有整体偏移和串人再开始大规模训练。这个动作花不了两分钟但能帮你省掉几个小时的无效训练。希望帮到你。本文还有配套的精品资源点击获取