YOLOv5+HRnet姿态估计实战:从数据准备到边缘设备部署
简介面向目标检测与姿态估计入门及进阶开发者该资源以YOLOv5为检测主干结合HRnet与SimDR实现人体关键点实时识别适用于图片、视频及摄像头场景。包内为可直接运行的原工程文件共2000个文件以1867个Python脚本为主辅以C/C源码、txt参数与说明、md文档等整体压缩包841.53MB结构覆盖项目克隆、环境配置、SPPF模块添加、yaml文件修改、边界框与关键点绘制等完整链路。已有1749人学习浏览适合希望跳过配置环节、直接上手实践或参考完整工程结构的同学。资源还包含照片/视频/实时演示结果及五个典型报错分析路径问题、Upsample属性错误、gbk解码等可帮助快速完成环境搭建与排错节省自主摸索时间。1. YOLOv5姿态估计为什么选“检测关键点”双网络在康复训练动作纠正的项目里摄像头装在墙角人站在两三米外只靠目标检测能框出“人”但哪条腿在抬、胳膊肘弯到多少度框完全给不了答案。YOLOv5姿态估计解决的就是这个缺口先让YOLOv5把画面里的每个人框出来再把框内图像交给HRnet回归鼻子、肩膀、肘、腕、髋、膝、踝等17个人体关键点最终输出每个关键点的像素坐标。这条串联链路同时利用了YOLOv5在检测上的稳健召回和HRnet高分辨率特征图在关键点定位上的精度并且在rk3568这类边缘设备上可以把两个模型分别量化、分别替换工程上非常灵活。适合要把多人姿态估计跑通并调到可交付状态的工程师参考。2. 从COCO数据到训练集人体检测框与17个关键点的对齐2.1 多人姿态估计的两种主流结构自顶向下与自底向上做多人姿态估计最先要选的就是框架结构。自顶向下路线是“先检测人再对每个人做关键点回归”也就是本方案的形态。自底向上路线则是“先回归出所有关键点和肢体连接线再聚类成个体”OpenPose 是这条路线的代表。选择自顶向下最直接的理由是精度好控制每个检测框裁剪出来后关键点只负责框里的这一条人训练收敛稳定推理时也能直接得到“哪个框配哪组关键点”的配对结构。自底向上的优势是推理耗时不会随着画面人数线性上涨在人数极多的场景下有吞吐优势但它的聚类过程在两人交叉、遮挡时经常翻车把人手和旁边人的肩膀拼到同一副骨架里去。我实际做过对比工业现场的摄像头画面里通常只有2到6人自顶向下的耗时完全可以接受而精度和调试成本明显更友好。这个选择还带来一个工程红利YOLOv5检测模型和HRnet关键点模型是解耦的检测模型可以单独换更小的s版本压缩耗时关键点模型可以单独提升输入分辨率来改善小尺度人的精度两者互不牵连。另一个需要接受的现实是实时性预算必须分摊到两个模型上。同样处理一帧检测端消耗一部分毫秒关键点端按人数成倍消耗。所以后续章节里所有关于输入尺寸、量化、批次合并的讨论本质上都是在争取这部分预算。2.2 关键点标注格式COCO的17个点顺序不能记错训练数据大多沿用 COCO 数据集的17点定义。这个顺序是行业默认的模型输出头的通道顺序、heatmap 的可视化顺序、评估脚本的关键点顺序全靠它对齐。顺序一旦错了训练过程不会报错但评估结果会非常诡异比如左肩的 loss 降到很低实际可视化时左边肩膀的点跑到鼻子上这是典型的静默错误。索引关键点索引关键点0nose9left wrist1left eye10right wrist2right eye11left hip3left ear12right hip4right ear13left knee5left shoulder14right knee6right shoulder15left ankle7left elbow16right ankle8right elbowCOCO 的标注文件里还有一个细节容易被忽略keypoints序列是[x1, y1, v1, x2, y2, v2, ...]每三个数字一组最后的v表示可见性标志。v0表示该点不在图内且未标注v1表示存在但被遮挡v2表示可见。训练时 loss 计算只针对v0的关键点如果把不可见点也塞进监督模型会被迫去预测一个毫无意义的位置关键点输出会整体偏移。2.3 数据划分与预处理裁框、缩放、归一化的三件事拿到标注后第一件事是划分训练验证集我一般按 8:1:1 处理并且按视频序列划分而不是按单帧随机划分。理由很直白同一段视频的相邻帧高度相似如果它们同时出现在训练集和验证集里验证指标会虚高部署时一跑真实视频就露馅。预处理阶段有三个动作必须做对。第一是裁框以检测框为中心向外扩展1.25倍把肘部、手腕这些经常探出框外的点包进来扩展比例太小会让关键点被迫依赖图像边缘的信息。第二是缩放HRnet 常见输入是256x192操作时保持长宽比缩放到短边192剩余区域用边缘像素填充而不是直接拉伸变形因为拉伸会改变肢体的比例关系。第三是归一化使用 ImageNet 的均值和标准差这一步与预训练权重匹配很多复现失败都是因为自己改了 mean 和 std 却没有同步训练预训练模型。训练集增强也直接影响姿态估计的泛化。随机旋转在正负40度之间缩放系数0.7到1.3水平翻转时注意关键点标签要做左右交换鼻子与双眼这类中轴点维持原索引。还有一个不常提但很有用的增强随机遮挡用黑色矩形块盖住图像中部区域逼模型在部分肢体不可见时依靠上下文推测位置这对后续检测框不准时的推理非常有效。3. 在YOLOv5检测框上接HRnet模型结构与最小推理代码3.1 HRnet为什么比简单U-Net适合关键点高分辨率分支的保持很多人问为什么不直接用 U-Net 当关键点回归网络。U-Net 的结构是先下采样再上采样降维过程把空间位置信息压进语义特征上采样时再尝试补回来这个过程会有信息损耗。部位遮挡时关键点的定位只依赖低分辨率的语义特征精度会明显下降。HRnet 的思路完全不同它从头到尾保持一条高分辨率分支同时并行做低分辨率分支并且在不同分辨率之间反复融合信息高分辨率支路始终握着精确的空间位置。这个差异在实际效果上很直观肩、髋这类大关节两者差距不大但手腕、脚踝这类细小关节HRnet 的定位误差通常更小。另外 HRnet 可以通过控制并行分支的宽度得到多个规格工程上常用 HRnet-W32 作为速度与精度的平衡点W48 精度更好但推理耗时大幅增加边缘设备一般不选。主流的落地方式有两种第一种直接使用公开的 HRnet 预训练权重从头训练关键点分支第二种借助 MMPose 这类工具箱把 HRnet 导出成 ONNX再接入自己的推理工程。两种方式我都用过如果项目只需要做单人姿态估计或人数可控的多人估计直接加载预训练权重更省事因为骨干网络的特征提取能力已经足够真正需要训练的是输出头部分的微调。3.2 从框坐标到HRnet输入仿射变换与heatmap解码YOLOv5 给出的坐标是原图中的矩形框而 HRnet 期望的输入是一张裁剪修正后的单人图。这个转换看起来简单实际有三个坑。第一必须按检测框的中心点做外扩而不是直接在框的四个边界外扩像素否则框的偏移会直接带偏关键点的相对位置。第二缩放时保持长宽比用仿射变换把裁剪区域变成256x192变换矩阵里的 scale 因子需要在解码关键点坐标时逆向使用。第三如果只做简单cv2.resize同一个人的不同帧可能因为检测框轻微抖动造成输入图像内容不断变化最终关键点就会抖动。heatmap 解码是另一个重点。模型输出的不是坐标而是17张热力图每张热力图上的峰值位置对应一个关键点。最简单做法是取argmax定位但 argmax 只能给出整数值像素坐标在视频中会产生明显的逐帧跳动。更稳的做法是在峰值附近做亚像素偏移用热力图峰值点上下左右相邻位置的响应值计算偏移量把坐标推到一个连续值。这一步不需要额外训练是纯后处理但效果非常明显连续帧的腕点平滑度能上一个台阶。3.3 一条YOLOv5HRnet串联的最小推理链路代码下面这段代码展示了从一帧原始图像到“检测框 17个关键点”的最小闭环。加载检测模型和 HRnet 模型的部分不同框架封装差异很大我写成函数占位你按自己手头的权重格式替换即可。import cv2 import numpy as np import torch class PoseEstimator: def __init__(self, det_weights, hrnet_weights): # 常见做法torch.hub.load或YOLOv5的DetectMultiBackend根据代码版本自选 self.detector load_yolov5_detector(det_weights) # 常见做法直接加载HRnet-W32预训练模型或从ONNX导入 self.hrnet load_hrnet(hrnet_weights) self.input_h, self.input_w 256, 192 # HRnet输入尺寸 self.expand_ratio 1.25 # 检测框外扩比例 def estimate(self, frame): results self.detector(frame) # YOLOv5推理 boxes results.pred[0][:, :4].cpu().numpy() # xyxy格式 scores results.pred[0][:, 4].cpu().numpy() classes results.pred[0][:, 5].cpu().numpy() all_keypoints [] for box, score, cls in zip(boxes, scores, classes): if cls ! 0 or score 0.5: # 只处理person类 continue x1, y1, x2, y2 box.astype(int) cx, cy (x1 x2) / 2, (y1 y2) / 2 w (x2 - x1) * self.expand_ratio h (y2 - y1) * self.expand_ratio # 裁剪并限制在图像范围内 sx1, sy1 int(max(0, cx - w / 2)), int(max(0, cy - h / 2)) sx2, sy2 int(min(frame.shape[1], cx w / 2)), int(min(frame.shape[0], cy h / 2)) crop frame[sy1:sy2, sx1:sx2] # 保持长宽比缩放到HRnet输入大小 crop_h, crop_w crop.shape[:2] scale min(self.input_h / crop_h, self.input_w / crop_w) nh, nw int(crop_h * scale), int(crop_w * scale) resized cv2.resize(crop, (nw, nh)) canvas np.zeros((self.input_h, self.input_w, 3), dtypenp.float32) canvas[:nh, :nw] resized # 归一化ImageNet均值标准差 canvas (canvas / 255.0 - np.array([0.485, 0.456, 0.406])) / np.array([0.229, 0.224, 0.225]) tensor torch.from_numpy(canvas.transpose(2, 0, 1)).unsqueeze(0).float() with torch.no_grad(): heatmaps self.hrnet(tensor)[0].cpu().numpy() # (17, H, W) kps self._decode_heatmaps(heatmaps) # 把相对坐标映射回原图 for kp in kps: kp[0] kp[0] / self.input_w * nw / crop_w * (sx2 - sx1) sx1 kp[1] kp[1] / self.input_h * nh / crop_h * (sy2 - sy1) sy1 all_keypoints.append((box, kps)) return all_keypoints这段代码的关键在坐标还原环节。HRnet 输出的是输入尺寸下的相对坐标我们先把它乘回nw/nh再乘回裁剪缩放比例最后加上裁剪区域的偏移量sx1/sy1才得到原图像坐标。这里最容易出错的点是如果直接把相对坐标当作原图坐标远距离目标的关键点会整体偏向裁剪框左上角。3.4 heatmap解码的亚像素偏移实现下面是_decode_heatmaps的常见实现放在这里一并说明。def _decode_heatmaps(self, heatmaps): decoded [] for hm in heatmaps: # hm: (H, W) h, w hm.shape idx np.argmax(hm) y, x np.unravel_index(idx, (h, w)) # 亚像素偏移用一阶差分估算峰值偏移方向 if 0 y h - 1 and hm[y, x] 1e-8: dy (hm[y 1, x] - hm[y - 1, x]) / (2 * hm[y, x]) y np.clip(y dy * 0.25, 0, h - 1) if 0 x w - 1 and hm[y, x] 1e-8: dx (hm[y, x 1] - hm[y, x - 1]) / (2 * hm[y, x]) x np.clip(x dx * 0.25, 0, w - 1) decoded.append([x / w * self.input_w, y / h * self.input_h]) return decodeddy和dx是热力图峰值位置两侧的响应差值0.25 表示最大偏移不超过四分之一的像素间距这个经验值在多数 HRnet 权重上表现稳定。如果你觉得关键点仍然抖动可以把这个系数降到 0.15代价是定位精度略微下降。这个参数属于典型的“玄学”范畴没有理论上的最优值必须针对自己的视频帧率做实验。4. 训练自己的关键点数据集yolov5超参数、损失函数与验证指标4.1 YOLOv5只做人检测改类别数还是用现成person权重YOLOv5 姿态估计里的 YOLOv5 部分只承担“检测出人”这一件事所以不要急着改检测头去直接回归关键点那是另一种方案类似 YOLOv8-pose 的思路和标题中 HRnet 组合的设计并不相同。检测端的常见做法有两种一是直接用 COCO 预训练的yolov5s.pt推理时过滤其他类别只保留 person 类二是用自己场景的数据微调一个单类检测模型。如果场景固定比如摄像头位置不动、拍摄范围基本不变强烈建议微调单类模型。原因很实际通用 person 权重在标准视角下效果不错但在俯拍、远距离、人穿深色衣服等场景会漏检而漏检意味着整个姿态估计管道对这个人的关键点输出完全丢失。微调成本很低数据量几百张就够关键是这些图要覆盖你实际场景的站位、角度和光照范围。# person.yaml train: /data/person/images/train val: /data/person/images/val nc: 1 names: [person]训练命令注意把--classes参数使用起来只对 person 类别计算 losspython train.py --data person.yaml --weights yolov5s.pt --epochs 100 --img 640 --batch 16微调后的检测模型比通用模型更能容忍场景内的背景干扰也更容易在 640 输入下跑出更干净的框这几个边界场景决定了关键点质量的基线。4.2 调yolov5超参数前三必调项在 yaml 超参数文件里lr0、anchor和mosaic是影响 person 检测收敛速度的三个关键项。初始学习率lr0建议从 0.01 起步如果你的数据只有几百张降到 0.001 更稳避免前期 batch 梯度震荡太大把预训练权重洗掉。很多人直接沿用 COCO 训练时的默认 0.01小数据场景下训练到中期 mAP 容易反复跳。anchor 适合用遗传算法重新计算。person 框的宽高比明显比 COCO 全体目标瘦长默认锚框里大量是适合汽车和行人的方框。在train.py加--noautoanchor可以锁定自动锚框策略或者直接让 YOLOv5 在训练前重新聚类一次因为腰包不匹配会直接导致小目标召回率上不去而这个现象从 loss 曲线里根本看不出来。mosaic增强在纯 person 场景建议降低到 0.5 以下。Mosaic 把四张图拼接在一起人物经常被切断检测模型被迫学习残缺目标的特征这对提升通用检测有帮助但会伤害姿态估计场景下“框住完整人”的要求。完整人框被切开意味着关键点裁剪也跟着出错。4.3 HRnet监督头与损失MSE在heatmap上的加减法HRnet 的训练通常使用 heatmap 回归的 MSE 损失。做法是把每个关键点坐标展开成一个以该点为中心、半径为sigma的二维高斯分布模型的任务不是直接预测坐标而是预测这个热图的分布形态。MSE 损失比较温和不会因为个别点的误差产生剧烈梯度这对遮挡部位尤其友好。生成热图时有两个细节影响训练效果。第一sigma的值一般取 2 到 3小目标居多的数据集建议调到 1.5否则热图峰值区域过大多个相邻关键点的热图互相重叠优化目标变模糊。第二生成热图时使用np.maximum而不是赋值这样重叠区域取最大响应值不会因为后一个关键点的写入把前一个点的峰值冲掉。def generate_heatmap(keypoints, img_size(256, 192), sigma2, num_kpts17): heatmap np.zeros((num_kpts, *img_size), dtypenp.float32) y_grid, x_grid np.mgrid[0:img_size[0], 0:img_size[1]] for i, (x, y) in enumerate(keypoints): if x 0 or y 0: continue r int(round(sigma * 3)) y0, y1 max(0, int(y) - r), min(img_size[0], int(y) r 1) x0, x1 max(0, int(x) - r), min(img_size[1], int(x) r 1) gauss np.exp(-((y_grid[y0:y1, x0:x1] - y) ** 2 (x_grid[y0:y1, x0:x1] - x) ** 2) / (2 * sigma ** 2)) heatmap[i, y0:y1, x0:x1] np.maximum(heatmap[i, y0:y1, x0:x1], gauss) return heatmap训练 HRnet 时初始学习率设到 1e-3用 Adam 优化器batch size 在 32 到 64 之间。如果发现 loss 卡在 0.02 附近降不下去优先检查 heatmap 生成代码里坐标和图像尺寸的对应关系我遇到过多次“训练的输入是 256x192但生成的 heatmap 是 192x256”模型根本学不到东西loss 不下来反而正常。4.4 评估用PCK还是OKS别拿mAP一通乱用目标检测那套 mAP 评估在关键点上适用性有限。关键点任务常用的两个指标是 PCK 和 OKS。PCK 计算预测点落在以真实点为中心、以躯干直径为尺度的一个阈值内的百分比直观理解是“预测点和真实点的距离是否小于人体尺寸的某个比例”。PCK0.2 是边缘部署验收时我习惯看的指标它比 OKS 更贴近“肉眼看不出来错位”的体验。OKS 是 COCO 关键点评估使用的指标它给每个关键点分配了不同的标准差鼻子和大关节的标准差不同更精细地反映标注质量差异。如果你要和其他公开模型对比用 OKS如果你只关心自己的视频跑出来效果如何PCK 更直接。建议训练时每隔 5 个 epoch 在验证集上计算一次 PCK0.5 和 PCK0.2而不是只看 loss。有几次 loss 稳定下降但 PCK 不动说明模型在学习热图的整体分布却忽略了峰值位置的精确度这时候需要检查 sigma 和亚像素偏移的解码方式。5. HRnet落地避坑关键点抖动、坐标错位的定位与修复5.1 现象多人场景A人的肘接到B人的肩两人交叉或距离较近时裁剪框里除了目标人往往还带着旁边人的手臂或肩膀。HRnet 只看框内图像不会区分是不是同一个人于是容易出现 A 人的肘部和 B 人的肩膀同时被激活解码出来关键点像是长在另一个人身上。原因有两层一是检测框外扩比例不够框边缘把另一个人的肢体切进来二是训练裁剪阶段没有模拟这种交叉干扰。解决方法是先把外扩比例调到 1.3同时过滤掉面积过小的检测框这些框大多是远距离误检或遮挡碎块。训练集增强里加入“随机把另一张图的肢体区域拼贴进当前裁剪图”的干扰策略让模型见过这种混乱输入推理时抗干扰能力会显著提升。5.2 现象静态图片正常视频里关键点疯狂抖动单帧推理结果完全正确但逐帧跑视频时手腕和脚踝的坐标在相邻帧之间跳变好几个像素看起来像是人在抽搐。这种问题在关键点落地场景中最常见也是最难接受的一类缺陷。主要原因是检测框本身在抖动。YOLOv5 每一帧给出的框并不完全一致框的中心点、宽高在像素级别波动这种波动经过裁剪、缩放、坐标还原三步放大后关键点位置的稳定性自然被破坏。解决路径分两级先对检测框做时间维度的平滑再对关键点输出做后滤波。检测框平滑通常用指数移动平均alpha 设为 0.6 到 0.8帧率越高可以越接近 0.8。关键点后处理可以用一维中值滤波窗口取 3 到 5 帧这个操作会带来几十毫秒的延迟但能极大提升视频观感。def smooth_boxes(prev_box, cur_box, alpha0.7): if prev_box is None: return cur_box return alpha * prev_box (1 - alpha) * cur_box注意平滑时要对四个坐标同时做而不是只平滑中心点否则框的宽高会畸变。还有一点经验关键点后滤波只对连续帧同一 ID 的目标做目标跟踪 ID 切换的瞬间滤波窗口必须重置否则会出现前一个人和后一个人的关键点混叠。5.3 现象近处好远端差小目标人的脖子被画到肩膀上画面中近处的人关键点正常远端小目标的关键点错得离谱最典型的是脖子点偏到肩膀位置。原因是 HRnet 的输入分辨率固定为 256x192小目标在整图里只占很小的像素区域裁出来后对应到 ROI 内部关键点的空间差异可能只有 2 到 3 个像素。热图在这个尺度下峰值重叠严重亚像素解码再准也无济于事。常见处理是提高 HRnet 输入分辨率到 384x288或者对检测置信度低的框做针对性放大后再送 HRnet。但提高分辨率直接带来推理耗时翻倍边缘设备上不一定承受得起。我一般建议先检查检测置信度阈值低于 0.3 的框直接放弃这类框即使做了关键点也大概率是噪声高于 0.5 的框再送 HRnet。如果远距离人的业务优先级高把输入分辨率提高到 384x288 是相对简单的方案同时配合前面提到的检测框平滑避免高分辨率下更多细节的抖动被放大。5.4 现象单帧耗时正常整条视频帧率上不去单人时单帧推理 15 毫秒五人以上直接掉到 70 多毫秒帧率完全不可用。这是自顶向下结构逃不开的特性。问题往往不是模型太慢而是每个人单独送一次网络batch size 为 1 的多次推理在 GPU 上完全没有发挥并行能力同时 CPU 与 GPU 之间的拷贝开销被多次放大。解决方法是批量化推理把一帧里的所有裁剪图拼接成一个大 batch一次性送进 HRnet。由于每个人的裁剪尺寸可能不同需要在 batch 前做尺寸对齐可以用固定分辨率并填充边缘来完成。batch size 还建议补齐到 4 或 8 的倍数减少底层计算单元的浪费。另一个思路是设置最大跟踪人数超过 N 人时只对检测置信度最高的 N 个框做关键点这个做法在视频会议场景很常用。单帧耗时从 15 毫秒到 70 毫秒其实大部分时间消耗在小 batch 的调度上和模型本身的算力关系不大。6. 把模型跑到rk3568与树莓派4b量化手段与实时优化6.1 检测与关键点分开量化rk3568上INT8的取舍rk3568 这类边缘盒子上部署统一转 RKNN 已经是标准路径。我的习惯是两个模型分开做 INT8 量化而不是打包成一个整体因为 YOLOv5 对量化误差不敏感HRnet 却非常容易掉点。关键点模型的 heatmap 输出是连续的置信分布INT8 量化后峰值周围的细节被抹平亚像素偏移计算出来的坐标直接影响 PCK。量化时的校准数据集务必来自实际场景不要用 COCO 验证集。校准图 100 到 200 张足够关键是要覆盖不同人数、不同距离的分布。RKNN Toolkit 转出 INT8 后务必单独验证 HRnet 的 PCK 变化如果掉点超过 5%退回混合精度方案检测端保持 INT8关键点端保持 FP16。这也是标题里“实时检测”任务下最常见的工程折中。树莓派4b部署yolov5时常见做法是先把模型转成 ONNX再用 ONNX Runtime 或者 NCNN 做推理。树莓派 CPU 上跑 640 输入的 YOLOv5s 加 256x192 的 HRnet通常只能到每秒 2 到 3 帧优化空间主要在三处检测端降到 480 输入、HRnet 降到 192x144、关键点只对画面中最接近摄像头的三个人做。上 NPU 之前先做这些软件层面的裁剪比直接换硬件更划算。6.2 关键点后处理查错顺序与验证脚本部署之前我习惯给自己留一个“后悔药”式的工具一段离线回放脚本读入录好的视频叠加检测框、关键点和置信度输出逐帧保存。这样现场效果不对时可以回放分析而不是对着板子上的实时画面猜问题。查错顺序也固定先看检测框是否稳定再检查裁剪坐标还原是否出错最后才怀疑模型精度。检测框不稳定会连带关键点抖动裁剪还原出错会让关键点在画面上的位置整体偏移这两种问题如果先去调模型参数等于白费功夫。验收时以 PCK0.2 为标准在测试视频上人工标注 100 帧统计各关键点的命中率。如果整体命中率在 85% 以上观感基本可接受。手腕、脚踝这类末端点命中最容易低于均值不要只盯着平均分看单独统计这组点位的命中率它们的表现决定了用户会不会抱怨。多年做这类项目下来最深的教训是别把模型精度看得太重先把输入到输出的链路按帧对齐。去年一个项目里关键点模型精度指标很好看现场视频依然抖动明显最后排查半天发现是检测框平滑参数在低帧率下取样过少没起到作用。后来我固定了检测框平滑与关键点滤波双通道才彻底解决这类观感问题。希望这些经验能帮你少走一段弯路。本文还有配套的精品资源点击获取