简介:面向目标检测与姿态估计入门及进阶开发者,该资源以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 降到很低,实际可视化时左边肩膀的点跑到鼻子上,这是典型的静默错误。
| 索引 | 关键点 | 索引 | 关键点 |
|---|---|---|---|
| 0 | nose | 9 | left wrist |
| 1 | left eye | 10 | right wrist |
| 2 | right eye | 11 | left hip |
| 3 | left ear | 12 | right hip |
| 4 | right ear | 13 | left knee |
| 5 | left shoulder | 14 | right knee |
| 6 | right shoulder | 15 | left ankle |
| 7 | left elbow | 16 | right ankle |
| 8 | right elbow |
COCO 的标注文件里还有一个细节容易被忽略:keypoints序列是[x1, y1, v1, x2, y2, v2, ...],每三个数字一组,最后的v表示可见性标志。v=0表示该点不在图内且未标注,v=1表示存在但被遮挡,v=2表示可见。训练时 loss 计算只针对v>0的关键点,如果把不可见点也塞进监督,模型会被迫去预测一个毫无意义的位置,关键点输出会整体偏移。
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 一条YOLOv5+HRnet串联的最小推理链路(代码)
下面这段代码展示了从一帧原始图像到“检测框 + 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), dtype=np.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 类别计算 loss:
python 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), sigma=2, num_kpts=17): heatmap = np.zeros((num_kpts, *img_size), dtype=np.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 计算预测点落在以真实点为中心、以躯干直径为尺度的一个阈值内的百分比,直观理解是“预测点和真实点的距离是否小于人体尺寸的某个比例”。PCK@0.2 是边缘部署验收时我习惯看的指标,它比 OKS 更贴近“肉眼看不出来错位”的体验。
OKS 是 COCO 关键点评估使用的指标,它给每个关键点分配了不同的标准差,鼻子和大关节的标准差不同,更精细地反映标注质量差异。如果你要和其他公开模型对比,用 OKS;如果你只关心自己的视频跑出来效果如何,PCK 更直接。建议训练时每隔 5 个 epoch 在验证集上计算一次 PCK@0.5 和 PCK@0.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, alpha=0.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 关键点后处理查错顺序与验证脚本
部署之前,我习惯给自己留一个“后悔药”式的工具:一段离线回放脚本,读入录好的视频,叠加检测框、关键点和置信度输出,逐帧保存。这样现场效果不对时,可以回放分析,而不是对着板子上的实时画面猜问题。查错顺序也固定,先看检测框是否稳定,再检查裁剪坐标还原是否出错,最后才怀疑模型精度。检测框不稳定会连带关键点抖动,裁剪还原出错会让关键点在画面上的位置整体偏移,这两种问题如果先去调模型参数,等于白费功夫。
验收时以 PCK@0.2 为标准,在测试视频上人工标注 100 帧,统计各关键点的命中率。如果整体命中率在 85% 以上,观感基本可接受。手腕、脚踝这类末端点命中最容易低于均值,不要只盯着平均分看,单独统计这组点位的命中率,它们的表现决定了用户会不会抱怨。
多年做这类项目下来,最深的教训是别把模型精度看得太重,先把输入到输出的链路按帧对齐。去年一个项目里,关键点模型精度指标很好看,现场视频依然抖动明显,最后排查半天发现是检测框平滑参数在低帧率下取样过少,没起到作用。后来我固定了检测框平滑与关键点滤波双通道,才彻底解决这类观感问题。希望这些经验能帮你少走一段弯路。
本文还有配套的精品资源,点击获取