ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

YOLOv5+HRnet人体姿态估计实战:从数据到部署全流程解析

YOLOv5+HRnet人体姿态估计实战:从数据到部署全流程解析 简介这是一份基于YOLOv5的HRnet人体姿态估计完整工程面向需要快速实现图片、视频及摄像头实时关键点检测的开发者与学生下载后只需修改本地路径即可直接运行省去繁琐的环境搭建和调参过程。包内共2000个文件以1867个Python脚本为主配合C/C源码、头文件、txt配置说明与Markdown文档覆盖目标检测、姿态估计、骨骼绘制及结果演示等模块压缩包大小约841.53MB。具体内容包括添加SPPF模块、修改yaml文件、获取边界框与关键点、绘制骨骼等完整流程并整理了functional.py警告、matplotlib后端设置、Upsample属性错误、gbk解码异常等常见报错分析同时支持照片、视频和实时摄像头三种演示方式。目前已有1748人学习下载适合希望快速上手YOLOv5姿态估计并深入理解代码细节的实践者。1. 先开枪再瞄准为什么人体姿态估计要“YOLOv5HRnet”绑在一起一张多人照片摆在你面前要实时标出每个人的手、脚、肩膀单一的 YOLOv5 只能画框单一的 HRnet 在多人场景下会被整张图的背景干扰到崩溃。把两段式检测接起来才算真正把“人在哪”和“姿势啥样”这两件事解耦——YOLOv5 负责从整图里把人头、人体框挑出来HRnet 再对每个裁好的框回归 17 个关键点。这个组合在工程上有个明显红利检测和姿态模型可以分开训练、分开换检测部分用现成的 COCO 预训练权重姿态部分单独调参部署时如果算力紧张还能先量化姿态网络而不动检测网络。这篇文章就照这条线把数据准备、训练、调参与部署的完整路径拆开讲适合手里有摄像头、想跑通实时多人姿态估计的工程师。2. 选型对照YOLOv5HRnet的“工作流”在姿态估计里的位置2.1 自顶向下与自底向上一条分界线决定你的延迟人体姿态估计领域有个绕不过去的分叉口自顶向下Top-Down和自底向上Bottom-Up。自顶向下的做法就是“先检测后关键点”先把人从画面里框出来再对每个框单独估计关键点。自底向上的做法是“先点后分组”整个画面同时回归所有关节的 heatmap再用肢体连接算法把人分组。前者精度高后者在遮挡严重和密集人群里更稳——但如果画面里人少、背景可控我会直接选自顶向下因为每个人单独过网络的流程可以用并发和批处理压掉延迟。YOLOv5HRnet 就是典型的两段式自顶向下方案。YOLOv5 把检测做到每张图 20ms 以内HRnet 的裁剪分支只处理一个固定尺寸的小图通常 256×192 或 384×288单个人体的推理时间在 GPU 上可以压到 10ms 以下。整体端到端延迟主要瓶颈不在网络推理而在“检测框到裁剪坐标”的后处理粘合层。这个衔接层的设计精度直接决定了姿态输出的抖动程度后面避坑章节会重点展开。代码和原理的对应关系其实很简单。YOLOv5 的输出是[x1, y1, x2, y2, conf, cls]姿态网络拿到的输入是经过仿射变换的[3, H, W]张量。这两个张量之间的转换就隔着一个 resize 和一个坐标映射不涉及任何复杂的特征对齐。把这个流程想清楚你就知道调参的着力点分别落在哪段检测框是否稳定影响关键点的输入分布是否平稳姿态网络的学习率与损失权重影响关键点回归本身的精度两者之间的 padding 方式影响边缘姿态的完整性。2.2 HRnet高分辨率分支比Swin-T和Hourglass好在哪HRnet 的核心贡献是全程保持高分辨率特征图而不是像 Hourglass 那样先降采样再上采样恢复。它在整个网络里并行维护四个分辨率分支1/4、1/8、1/16、1/32每个阶段做一次多分辨率融合。这个设计的直接收益是小目标比如手指尖、脚踝的特征不会被下采样信号稀释掉。人体姿态里脚跟和手腕的关节尺寸其实很小如果在 1/8 分辨率以下做回归关节坐标偏移会被放大很多倍。我用过一个直观对比同样在 256×192 的输入下用 Hourglass 训练一个人体关键点模型收敛速度快但踩到 65 的 PCK0.2 之后怎么都上不去切到 HRnet-W32同样的数据、同样的学习率策略PCK 缓慢爬到 72 附近。差距来自“跳层连接 vs 并行融合”的差异——HRnet 在每层都做高分辨率与低分辨率之间的信息交换不是只在最后拼接。这个机制对关节这种强结构、小面积的目标天然友好。实际选型时要看自己的显存和芯片。HRnet-W32 是默认选择W48 精度再抬 1~2 个点但参数量接近翻倍。如果部署对象是树莓派或 Jetson Nano 这类边缘设备我更建议 W32 深度可分离卷积替换或者干脆在导出时做通道剪枝。HRnet 的结构对剪枝的容忍度比 ResNet 高因为多分支融合会兜住部分被剪掉的信息。2.3 数据流设计检测框裁剪、缩放、归一化喂给姿态网络的真实样子把整个流水线的张量形状写清楚代码就不会乱阶段输入张量形状输出张量形状关键参数YOLOv5 检测[B, 3, 640, 640][B, N, 6]6 代表 x1,y1,x2,y2,conf,clsimgsz640conf_thres0.25坐标映射[N, 4][N, 4]裁剪框padding1.2clip 到原图边界裁剪与缩放原图 裁剪框[N, 3, 256, 192]保持长宽比灰度填充归一化[N, 3, 256, 192][N, 3, 256, 192]mean[0.485,0.456,0.406]std[0.229,0.224,0.225]HRnet 推理[N, 3, 256, 192][N, 17, 64, 48]输出是分辨率 1/4 的 heatmap坐标解码[N, 17, 64, 48][N, 17, 2]argmax 仿射逆变换注意裁剪阶段不是简单的方框剪下来然后 resize。常见做法是先按检测框的宽高各扩 20%padding1.2再把扩完的框映射到目标分辨率最后用仿射变换把原图区域拉成 256×192。这步如果做成了直接拉伸会让长宽比失衡导致身体比例在模型输入端横竖各缩一半——姿态网络对这种情况相当敏感误差能到 3 到 5 个像素。最后一个要点输出的 heatmap 是 64×48 的 1/4 分辨率特征图。argmax 找到峰值点之后如果要亚像素精度可以在峰值邻域做高斯插值或取 3×3 窗口的加权平均。官方 HRnet 的后处理里常用 “soft-argmax” 思路把峰值坐标往高响应方向偏移一小段。这一步在你做部署时对面部关键点或者手部关节的稳定性提升很明显代价只是每点多算 9 个像素的权重求和。3. 从COCO到自己的数据集标注格式与预处理全套脚本3.1 COCO关键点标注的17个点与可见性标志YOLOv5HRnet 训练主路径上跑的是 COCO 格式的标注17 个关键点的索引顺序是固定的0 鼻子、1 左眼、2 右眼、3 左耳、4 右耳、5 左肩、6 右肩、7 左肘、8 右肘、9 左腕、10 右腕、11 左髋、12 右髋、13 左膝、14 右膝、15 左踝、16 右踝。每个关键点除了坐标还要带一个可见性标志v0 表示该点在图像里没有标注出来1 表示遮挡但位置可推测2 表示完全可见。训练时只有v0的点参与损失计算。很多人把v0的点也丢进去训练结果就是网络把看不见的关节“平均”到背景中间——训练收敛是收敛了但推理时只要有遮挡就乱飘。标注工具推荐用 Labelme 或 COCO Annotator前者导出 JSON 后转格式后者直接在线协作。如果你用自己的数据做单人场景可以直接复用 COCO 的 17 点模板如果是做判别式的特殊场景如手部 21 点、面部 68 点需要在这套流程外面套一层自定义关键点解析。我一般建议新手先从 17 点跑通全流程再改 keypoint 数量和类别字典避免一次改动太多变量导致问题定位困难。3.2 坐标转换与裁剪脚本从 YOLO 检测标注到关键点训练集YOLOv5 的标签是归一化的中心坐标[cx, cy, w, h]姿态标注则是绝对像素坐标。这两个坐标系之间的换算关系是数据准备阶段最容易出错的点。下面这段脚本把 YOLOv5 格式的检测框和 COCO 格式的关键点标注合并成一个可以直接训练的姿态训练集import json import numpy as np import cv2 import os def convert_coco_to_hrnet(data_root, json_path, out_root): with open(json_path, r) as f: coco json.load(f) img_info {img[id]: img for img in coco[images]} # 建立图片ID到标注的映射 anns {} for ann in coco[annotations]: img_id ann[image_id] anns.setdefault(img_id, []).append(ann) # 关键点索引COCO 的 17 点顺序 num_keypoints 17 for img_id, ann_list in anns.items(): img_meta img_info[img_id] img_path os.path.join(data_root, img_meta[file_name]) img cv2.imread(img_path) h, w img.shape[:2] for ann in ann_list: # ann[keypoints] 是 [x1,y1,v1, x2,y2,v2, ...] 的扁平列表 kps np.array(ann[keypoints]).reshape(-1, 3) kps[:, 0] np.clip(kps[:, 0], 0, w - 1) kps[:, 1] np.clip(kps[:, 1], 0, h - 1) # 过滤无用的标注所有点都是 v0 if (kps[:, 2] 0).all(): continue # 归一化到 [0,1] 区间方便后续统一缩放 kps[:, 0] / w kps[:, 1] / h # 保存成训练可直接读取的 npz 格式 save_id f{img_id}_{ann[id]} np.savez_compressed( os.path.join(out_root, f{save_id}.npz), imageimg_path, keypointskps.astype(np.float32), bboxnp.array(ann[bbox], dtypenp.float32) )这段脚本的要点在最后几行——直接保存原图路径和归一化的关键点坐标而不是把图片抠出来重新存储。这样训练时每次 epoch 动态裁剪数据增强的随机性不会被提前固化。注意bbox的保存格式是[x, y, w, h]后续做裁剪时要用它计算左上角和右下角别用 YOLO 的中心坐标格式来算否则各差半个框的偏移。还有一个细节保存的关键点坐标是归一化的加载时需要乘以当前图片的实际宽高。如果训练脚本里做的是随机缩放就要把归一化坐标和缩放系数一起传进仿射矩阵否则坐标会错位。我的习惯是统一在数据加载函数里做“从归一化到像素”的还原而不是在保存时就锁定像素值这样数据增强的灵活性大很多。3.3 训练数据分布检查裁剪框比例与关键点可见性统计训练前必须跑一次数据分布检查不然训练到一半才发现“所有样本的右手全是v0”之类的问题返工成本极高。检查两个指标每个关键点的可见性占比以及裁剪后的人体框长宽比分布。import numpy as np import glob npz_files glob.glob(your_dataset/*.npz) num_kps 17 vis_count np.zeros(num_kps) total_count 0 aspect_ratios [] for f in npz_files: data np.load(f) kps data[keypoints] # 形状 [17, 3]第三列是可见性 vis kps[:, 2] 0 vis_count vis.astype(np.int32) total_count 1 bbox data[bbox] # [x, y, w, h] if bbox[2] 0 and bbox[3] 0: aspect_ratios.append(bbox[2] / bbox[3]) vis_ratio vis_count / total_count print(每个关键点的可见性占比, vis_ratio) print(裁剪框宽高比均值, np.mean(aspect_ratios))如果某个关键点的可见性占比低于 60%就要人工检查标注是否漏标了。长宽比均值如果偏离 0.75COCO 的标准人体框比例太远说明你的场景里多为横躺或极宽姿势这会影响裁剪时 padding 的默认参数。数值本身没有绝对标准但异常分布要在训练前发现。3.4 数据增强旋转、缩放、翻转时的对称点映射关键点数据增强和大物体检测完全不一样核心区别在于“对称轴”。水平翻转之后左肩的 x 坐标镜像到了右肩如果你直接翻转不交换索引模型会学到左右互搏输入左肩位置网络输出“右肩左肩混合”的特征。def augment_keypoints(image, kps, flip_indices): # kps 形状 [17, 3]flip_indices 是翻转后的索引映射 if np.random.rand() 0.5: image cv2.flip(image, 1) # 水平翻转 h, w image.shape[:2] # 翻转x坐标 kps[:, 0] w - 1 - kps[:, 0] # 重新排列关键点索引 kps kps[flip_indices] # 随机旋转 [-30, 30] 度 angle np.random.uniform(-30, 30) M cv2.getRotationMatrix2D((w // 2, h // 2), angle, 1.0) image cv2.warpAffine(image, M, (w, h), flagscv2.INTER_LINEAR) # 旋转坐标 ones np.ones((kps.shape[0], 1)) kps_homo np.concatenate([kps[:, :2], ones], axis1) kps_rot kps_homo M.T kps[:, :2] kps_rot[:, :2] return image, kpsflip_indices的映射必须和 COCO 索引对齐左眼和右眼互换、左肩和右肩互换、左肘和右肘互换鼻子和脖子这类中间点保持不变。右边是严格的对称交换flip_indices [0, 2, 1, 4, 3, 6, 5, 8, 7, 10, 9, 12, 11, 14, 13, 16, 15]增强里还有一个常见误区是旋转角度过大。超过 45 度的旋转会让大多数关键点漂出图片边界生成大量v0的伪标注。我的经验是取 [-30, 30] 度即可如果业务本身有倒地、侧躺等极端姿态应该在标注层面额外补充样本而不是靠旋转硬转出来。数据增强的最后一环是遮挡模拟。用随机矩形把部分区域遮挡掉再把遮挡区域内的关键点的可见性强制置 0。这步对提升遮挡鲁棒性效果明显但注意不要和真实场景的遮挡语义冲突——比如你是做健身动作识别器材遮挡是常有的AI 模拟遮挡的分布尽量与真实相似。4. 训练配置与超参调节让损失曲线按预期掉下来4.1 官方配置文件里三个必须改的参数HRnet 的官方仓库里提供的是通用配置直接拿来训练自己的数据集十个里有八个精度不合格。必须改的是这三处第一处是DATASET.NUM_JOINTS默认是 17如果你改了关键点数量这里要同步变更。第二处是LOSS.USE_TARGET_WEIGHT官方默认 True含义是只对v0的像素位置计算损失。如果你希望让网络学会“主动忽略遮挡区域”保持 True如果想强行让网络去预测被遮挡的点就改成 False——但一般不建议后者这会导致遮挡处的输出不可控。第三处是TRAIN.END_EPOCHCOCO 官方跑 210 个 epoch但自建数据集通常 80 到 120 个 epoch 就收敛。不要全套照搬否则最后一阶段的学习率衰减会把模型拉向过拟合。还有一个容易踩的配置项是TRAIN.LR_SCHEDULER。HRnet 默认用MultiStepLR在 170 和 200 epoch 各降一次学习率。如果你只训 80 个 epoch衰减点设在 60 和 75 附近否则还没降到低学习率的精调阶段训练就结束了关键点精度会停在次优解。4.2 训练命令行与冻结权重技巧python tools/train.py \ --cfg experiments/coco/hrnet/w32_256x192_adam_lr1e-3.yaml \ --modelDir output/models \ --logDir output/logs \ --dataDir /path/to/your/dataset训练命令里的--cfg指向 YAML 配置文件核心参数都在里面。训练日志会输出每个 epoch 的acc和loss这里的acc实际计算的是关键点命中率距离阈值内的关键点比例不是最终评测的 PCK 或 OKS。如果训练初期acc不涨先检查数据加载是否正常再看学习率是否过小。如果是从头训练自己的姿态数据集建议先冻结 HRnet 的 stem 和前两个 stage只训练最后两个 stage 的输出头。做法是把MODEL.PRETRAINED设为官方 ImageNet 权重或 COCO 预训练权重然后在tools/train.py里加一层requires_grad_(False)冻结逻辑。我的经验是冻结前两阶段时精度几乎没有损失但训练速度提升 30% 到 50%显存占用也明显下降。我在实际项目里还会调整 batch size 与输入分辨率。256×192 分辨率下 batch size 32 是显存和精度的平衡点384×288 分辨率的精度高约 2 个 OKS但推理耗时翻倍。如果显存只有 11GB比如 2080Tibatch size 降到 16同时把WORKERS调到 8 来保证 GPU 利用率不掉。4.3 训练监控PCK 与 OKS别只看一个指标训练过程中每个 epoch 结束都会打印验证集的表现但打印的acc用的是简单阈值命中率即预测点与标注点之间像素距离小于某个固定阈值就算命中。这个指标有一个致命弱点它没有把目标大小归一化。同一个 10 像素的偏移在手掌这种小目标上是严重错误在躯干这种大目标上可能是可接受的误差。所以训练日志里的acc只能看趋势不能跨数据集比较。真正部署前要看的指标是 OKSObject Keypoint Similarity。它会按每个关键点的尺度sigma做归一化。计算方式为def compute_oks(gt_kps, pred_kps, sigma, bbox_area): # gt_kps 和 pred_kps 形状 [17, 2] dist np.sqrt(np.sum((gt_kps - pred_kps) ** 2, axis1)) exp_term dist ** 2 / (2 * (sigma ** 2) * (bbox_area 1e-6)) oks np.exp(-exp_term) return np.mean(oks[gt_kps[:, 2] 0]) # 只看可见点其中sigma是 COCO 官方给定的每个关键点的归一化标准差比如手腕的 sigma 是 0.072肩膀是 0.026。实际评测时会把所有样本的 OKS 汇总按不同阈值0.5、0.75计算 AP。APOKS0.75 比 APOKS0.5 对关键点位置的精细度要求更高如果 0.75 上不去说明模型大体位置对但细节飘需要提高分辨率或增加训练 epoch。训练时还要盯一个“非官方”但很实用的信号预测热力图峰值分布。每 20 个 epoch 抽样 20 张验证图片把预测的 heatmap 可视化叠加在原图上如果看到热力图有“双峰”或“弥散到整个关节区域”说明该关节类别之间存在语义模糊或数据量不足。这个可视化习惯能让你提前发现训练数据的问题而不是等到部署后被业务方拍回来返工。5. 避坑指南姿态估计训练与部署里的5个翻车现场5.1 现象一验证损失下降肢体点全糊在一起现象训练损失前 30 个 epoch 正常下降但可视化输出时肘部和腕部的热力图糊成一片所有肢体点挤在躯干中央。原因裁剪时 padding 太小。人体框边缘的肢体被切掉了网络只能在剩余可见区域做平均猜测。尤其当检测框由 YOLOv5 输出时如果置信度阈值设得太低框偏紧上手部位直接被截断。解决把裁剪时的 padding 从 1.0 提升到 1.2 到 1.5确保手腕和脚踝有完整上下文另一个做法是在检测阶段就把置信度阈值从 0.25 提到 0.4剔除边界错框让姿态网络吃到的都是完整的人体。5.2 现象二换了自建数据集精度比 COCO 预训练权重低了 10 个点现象用 COCO 上训练的权重直接在自己的数据上测试效果大跌微调之后虽然回升但训练集只有 5000 张精度始终上不去。原因领域漂移。COCO 的图片里人体占比大、背景多样你的场景如果是鱼眼镜头、摄像头装在角角落落或拍摄距离极近空间的分布完全不一样。解决不需要重新标注 10 万张。常见做法是收集 2000 张未标注的真实场景图片做伪标注用现有模型跑出关键点只保留 OKS 高置信度的结果。这个过程等于让模型自己“挑”出最舒服的样本再用它们做一次微调。5000 张真值标注加上 2000 张伪标注在我的项目里把 PCK 从 58 抬到了 69。5.3 现象三单张图没问题视频里姿态“跳”现象单帧推理完全正常但视频模式下同一关节点的坐标在相邻帧之间抖动剧烈帧率波动越大抖动越明显。原因不是网络的错是检测框的后处理没做平滑。YOLOv5 在少量帧里输出的框会上下浮动一个到两个像素裁剪框的漂移被 HRnet 放大成关键点的大幅抖动。解决用指数移动平均EMA对检测框坐标做平滑。设定alpha0.6每帧的平滑框坐标为smooth alpha * current_box (1 - alpha) * previous_smooth。注意 alpha 太小会导致动作跟不上太大则滤波不干净。另外在姿态解码阶段对关键点也做一次轻量平滑def smooth_pose(curr_kps, prev_kps, alpha0.6): return alpha * curr_kps (1 - alpha) * prev_kps这个技巧在实时交互项目比如动作捕捉驱动数字人里能直接省掉一整套复杂的滤波管线先跑通再考虑卡尔曼滤波或 One Euro Filter。5.4 现象四树莓派5上部署之后输出坐标全部错位现象PC 端验证一切正常代码原封不动移到树莓派 5 上关键点位置偏了 20 到 30 像素网络结果完全对不上画面内容。原因输入分辨率或展缩参数不一致。树莓派端如果通过摄像头采集图片尺寸和 PC 端测试图片的长宽比不一样resize 时的 scale 参数没同步更新。另一个常见原因是 padding 的颜色值不一致——OpenCV 的BORDER_CONSTANT用的填充值是标量元组你写成了 0但在训练时填充的是 ImageNet 均值的 RGB 元组模型对背景颜色的统计特征变了。解决把预处理单独封装成函数输入输出都固定形状在 PC 端和树莓派端共用同一份代码填充值统一用(128, 128, 128)或训练时的均值而不是 0。部署前用同一张测试图分别跑 PC 端和树莓派端对比中间张量每个通道的均值不一致的地方就是错误源头。5.5 现象五关键点数量少但在实际场景里频繁误检现象把模型部署到实际监控场景每帧都输出 17 个点但很多点明显不合理——手肘的位置跑到头部侧面去了。原因没有做肢体结构校验。模型只针对每个点独立回归它没有“人体关节连接关系”的先验。当遮挡严重时单点回归会落到视觉上最像的位置但整体骨头结构就是不对的。解决后处理加一个肢体连通性滤波。用预定义的骨骼连接对如肩到肘、肘到腕计算每条骨长的历史均值和方差当某条骨长偏离超过 2 倍标准差时把对应末端点按历史骨长的方向拉回合理范围。这个滤波不是纯数学约束而是把人体结构学常识塞进后处理开销极小每帧几百条浮点运算。我在手部姿态项目里用这个手段把关键点的奇异值跳变减少了约 40%。6. 部署落地把模型压进树莓派和边缘盒子的具体手法6.1 ONNX导出与TensorRT加速模型训练完成后部署的第一步是把 PyTorch 权重转成 ONNX再根据硬件选择推理引擎。常见做法是先把 HRnet 和 YOLOv5 分别导出# YOLOv5 导出 ONNX python export.py --weights yolov5s.pt --include onnx --img 640 # HRnet 导出 ONNX python tools/pytorch2onnx.py \ --cfg experiments/coco/hrnet/w32_256x192_adam_lr1e-3.yaml \ --checkpoint output/models/pose_best.pth \ --output hrnet_w32.onnxONNX 导出时有两个参数要特别注意--dynamic-axes如果开启会让 batch 维度和宽高维度可变这种动态 shape 在 TensorRT 里会走优化不佳的 fallback。我的做法是固化成固定[1, 3, 256, 192]输入换用批处理的方式解决并发需求。另外 HRnet 的最后一层是取 heatmap 最大值坐标的argmax这个算子在不同推理引擎里的实现有细微差别。Torch 的argmax在并列最大值时取第一个ONNX Runtime 的 TopK 可能取最后一个导致输出坐标偏一个像素。解决方法是把argmaxsoft-argmax的偏移计算放在 ONNX 图外在 Python 或 C 后处理里显式做坐标反算。如果目标平台是树莓派 5TensorRT 不适用那是 NVIDIA GPU 专属推荐用 ONNX Runtime 的 CPU 推理后端或把模型转成 NCNN 格式。NCNN 对 ARM 平台的 NEON 指令优化明显实测 256×192 的 HRnet-W32 在树莓派 5 上单推理约 150msYOLOv5s 约 200ms整体帧率能到 3 到 5 FPS。这个速度做实时姿态演示有点吃力但做离线采集分析完全够用。6.2 后处理归一化一个容易被忽略但决定毫秒级差异的步骤部署时最容易翻车的不是网络推理而是后处理的坐标反算。YOLOv5 输出的检测框坐标是相对于 640×640 输入图的而关键点坐标是相对于裁剪图 256×192 的。两层坐标叠加必须保证“反算”顺序完全一致。def decode_pose(det_boxes, kps_heatmaps, orig_shape): # det_boxes: [N, 4] 原图中的检测框 # kps_heatmaps: [N, 17, 64, 48] HRnet 输出的原始 heatmap all_kps [] for i, box in enumerate(det_boxes): x1, y1, x2, y2 box w, h x2 - x1, y2 - y1 # 裁剪框带 padding 恢复到原图坐标 pad_x w * 0.2 pad_y h * 0.2 crop_x1, crop_y1 x1 - pad_x, y1 - pad_y crop_w, crop_h w 2 * pad_x, h 2 * pad_y hm kps_heatmaps[i] # [17, 64, 48] kps [] for j in range(17): h_idx, w_idx np.unravel_index(np.argmax(hm[j]), hm[j].shape) # 关键点从 heatmap 坐标系映射到裁剪图 256x192 scale_x 256 / 48 scale_y 192 / 64 kp_x (w_idx 0.5) * scale_x kp_y (h_idx 0.5) * scale_y # 再反向映射到原图 orig_x crop_x1 kp_x / 256 * crop_w orig_y crop_y1 kp_y / 192 * crop_h kps.append([orig_x, orig_y]) all_kps.append(np.array(kps)) return all_kps这段代码里最容易被忽略的是0.5的偏移修正。heatmap 的每个像素代表一个响应区域的中心而不是响应区域左上角。如果直接用整数值索引乘上缩放比所有关键点会系统性偏移半个像素。批量场景下伤害不大但在精细动作捕捉里这个误差累计起来就非常明显。在编码时也提醒你一点如果用了 soft-argmax 做亚像素细化把偏移量加到这个0.5之后别加在索引之前。6.3 端到端验证用固定图片和标签跑一次回归测试部署以后不能靠肉眼看了算完事要有一组固定的回归测试集。我的习惯是挑 20 张覆盖不同场景的测试图每张图人工标注关键点写一个自动化脚本把整条链路跑一遍输出平均 PCK 和 OKS再和历史版本对比任何一次代码修改若导致指标跌破阈值就告警。def regression_test(model_path, test_dir, threshold0.65): # 加载模型、预处理、推理、解码、评测 avg_oks evaluate(model_path, test_dir) if avg_oks threshold: raise RuntimeError(fOKS {avg_oks:.3f} 低于阈值 {threshold}) return avg_oks这组回归测试不只是检验功能正确性更是在每次更新模型权重、改预处理参数、切换推理后端时快速判断改动是改善还是回退。我见过太多项目在部署阶段反复调“深度学习模型参数”结果问题出在图像缩放的插值算法上——PyTorch 默认用的是双线性ONNX Runtime 在 CPU 上可能跑成最近邻如果回归测试能在切换后端的第一时间抓手这个问题能省下大量排查时间。最后说一个习惯部署版本确定后把整条预处理和后处理的参数全部固化到一个 JSON 文件输入尺寸、padding 比例、填充像素值、缩放系数、阈值与模型文件一起归档。每次部署新设备先跑一次回归测试再交付。这个习惯帮我避开了至少三次“换了 Python 版本之后图像数组的 dtype 从 float32 变成 float64 导致坐标验算错位”的坑。希望帮到你。本文还有配套的精品资源点击获取
返回列表