2016-2020人体2D关键点检测:从CPM到HRNet的实战复盘
1. 为什么2020到2016年的人体2D关键点检测值得反复研究如果你正在做人机交互、运动分析、虚拟形象驱动或者视频监控相关的项目大概率绕不开一个基础模块从一张普通RGB图片里把人的骨架关节点找出来。这件事在学术圈叫人体2D关键点检测也有人叫它人体姿态估计、骨架提取、关键点定位。名字听着挺学术但说白了就是让计算机回答一个问题画面里这个人的肩膀、手肘、手腕、膝盖、脚踝分别在哪个像素位置上。我最早接触这个方向是在2018年前后当时想给一个健身动作纠正的小工具加姿态识别功能。试过用传统方法做背景分割加轮廓分析效果惨不忍睹稍微换个衣服颜色或者光线暗一点就全乱套。后来转向基于深度学习的关键点检测才真正把准确率做上来。而2016到2020这五年恰好是这个领域从“能跑通”到“跑得准”的关键窗口期很多经典思路和开源实现至今仍在工业界大量使用。这篇文章不是论文导读也不是简单的代码罗列。我想从一个实际使用者的角度把这五年里几个代表性方法的核心思路、代码落地的关键细节、以及我在复现和改造过程中踩过的坑系统地梳理一遍。无论你是刚入门想找个能跑通的baseline还是已经用过一些模型但想搞清楚背后的设计逻辑应该都能从中找到有用的东西。文章会涉及多人姿态估计和单人姿态估计两条主线覆盖自顶向下和自底向上两种范式并给出可直接参考的代码结构和训练配置建议。2. 五年技术脉络的整体拆解与方案选型逻辑2.1 两条技术路线的分野自顶向下 vs 自底向上人体2D关键点检测在2016年之后基本分成了两大阵营。自顶向下的思路是先用一个人体检测器把画面里的每个人框出来然后对每个框单独做单人关键点检测。自底向上则是先把画面里所有人的关节点都检测出来再通过分组算法把属于同一个人的关节点连起来。这两种思路的取舍非常实际。自顶向下的优势在于每个框里的人体尺度归一化了关键点检测网络只需要专注做好“给定一个人找他的关节”这件事精度通常更高。但它的速度受人数影响很大画面里人越多需要跑的检测次数越多。自底向上则相反网络前向一次就能得到所有人的关节点速度基本恒定但分组逻辑在拥挤场景下容易出错而且高分辨率输入下计算量也不小。我在实际项目里选型时如果场景人数可控比如单人健身跟练、人机交互优先选自顶向下精度有保障。如果是监控场景人数多且对实时性要求高会考虑自底向上或者轻量化的自顶向下方案。2016到2020年间这两条路线都有里程碑式的工作下面分别拆解。2.2 从CPM到OpenPose自底向上的工程化突破2016年的CPMConvolutional Pose Machines是早期很有影响力的工作它用多阶段卷积网络逐步 refine 关键点热图每个阶段都利用上一阶段的输出和图像特征。这个多阶段思路后来被很多方法借鉴。但CPM本身更偏向单人或者简单多人场景真正把自底向上做到工程可用的是2017年的OpenPose。OpenPose的核心贡献有两个一是提出了Part Affinity Fields也就是用向量场来表示肢体连接的方向和归属关系二是设计了一套贪婪匹配算法把检测到的关节点按PAF的指向拼成完整骨架。我第一次跑OpenPose的时候最直观的感受是它在多人重叠场景下的鲁棒性确实比之前的办法好很多而且官方提供了C和Python接口部署相对方便。不过OpenPose的缺点也很明显模型大、推理慢原始版本在普通GPU上跑高分辨率图片只能到个位数帧率。后来出现的OpenPose轻量版和MobilePose等工作通过减少网络宽度和阶段数来提速但精度会有一定下降。如果你的场景对实时性要求高可以考虑这些轻量变体或者用TensorRT做推理加速。2.3 从Hourglass到HRNet自顶向下的精度攀登自顶向下路线里2016年的Stacked Hourglass是一个绕不开的经典。它用堆叠的沙漏模块在每个模块里先下采样再上采样同时保留多尺度信息。这种结构对关键点这种需要精细定位的任务非常友好因为下采样能扩大感受野、捕捉全局姿态上采样又能恢复空间分辨率、精确定位。但Hourglass的精度在2019年被HRNet超越。HRNet的核心思想是全程保持高分辨率不像Hourglass那样先降后升而是并行维护多个分辨率的特征流并且在不同分辨率之间反复交换信息。这样做的好处是高分辨率特征不会被反复下采样破坏关键点定位更准。我在复现HRNet时发现它的参数量比Hourglass大不少但精度提升是实打实的尤其在手腕、脚踝这些小关节上误差明显更小。2019到2020年间还有SimpleBaseline和DarkPose等工作前者用ResNet加反卷积做上采样结构简单但效果不差后者通过编码热图的分布信息来提升定位精度。这些方法各有侧重选哪个取决于你的精度要求和计算预算。2.4 选型时真正该看的几个指标很多刚入门的朋友选模型时只看论文里的AP分数但实际落地时下面这几个指标往往更关键指标说明为什么重要推理速度单帧耗时或FPS实时应用必须考虑论文里的速度往往是最优条件下的模型大小参数量和权重大小移动端部署时直接影响安装包体积和内存占用输入分辨率网络期望的输入尺寸高分辨率精度好但速度慢需要权衡多人鲁棒性拥挤、遮挡场景下的表现监控和体育场景常见单人模型直接套用会崩后处理复杂度分组、NMS等逻辑自底向上的后处理往往比自顶向下复杂得多我个人的经验是如果做移动端或者嵌入式优先考虑轻量化的自顶向下方案比如用MobileNet做backbone的SimpleBaseline变体如果是服务器端且精度优先HRNet加自顶向下检测器是稳妥选择如果是多人密集场景且不想跑多次检测OpenPose系列仍然值得考虑。3. 核心细节解析与代码实操要点3.1 数据准备标注格式与增强策略无论用哪种方法数据都是第一位的。人体2D关键点检测常用的公开数据集有COCO Keypoints、MPII Human Pose、AI Challenger等。COCO的标注格式是每个实例一个bbox加17个关键点每个关键点有x、y、visibility三个值。MPII则是16个关键点标注格式略有不同。实际使用时我建议统一转成COCO格式因为大多数开源代码都默认支持COCO。转换时要注意几个坑一是关键点顺序不同数据集的关节定义不一样比如COCO是鼻子、左右眼、左右耳、左右肩、左右肘、左右腕、左右髋、左右膝、左右踝MPII多了头顶和骨盆二是可见性标注有些数据集用0/1表示可见不可见有些用0/1/2表示未标注、遮挡、可见转换时要统一。数据增强方面常用的有随机旋转、缩放、翻转、颜色抖动。这里有个细节水平翻转时关键点索引也要对应交换比如左肩和右肩的索引要互换否则网络会学到错误的左右关系。我见过有人忘了这一步训练出来的模型左右不分排查了半天才发现是增强代码的问题。# 水平翻转时交换左右关键点索引的示例COCO 17点 flip_pairs [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10], [11, 12], [13, 14], [15, 16]] # 索引0是鼻子不参与交换另外热图生成是关键点检测的核心步骤。通常做法是以每个关键点为中心生成一个高斯分布的热图峰值处为1向外衰减。高斯核的大小需要根据输入分辨率调整太小会导致正样本太少太大则定位模糊。我一般用核大小等于6到8个像素在输出热图分辨率下实测比较稳。3.2 网络结构从沙漏到高分辨率并行以HRNet为例它的结构可以简单理解为先通过几个卷积层把分辨率降到1/4然后分成多个分支分别保持1/4、1/8、1/16、1/32的分辨率每个阶段在不同分支之间做信息交换最后把所有分支的特征上采样到1/4分辨率拼接再通过1x1卷积输出热图。这种设计的精妙之处在于信息交换模块。它不是简单地把低分辨率特征上采样后相加而是让每个分支都能从其他分支获取信息。具体来说低分辨率分支通过上采样把语义信息传给高分辨率分支高分辨率分支通过下采样把空间细节传给低分辨率分支。这种双向流动让网络同时具备强语义和精定位能力。代码实现时有几个参数需要特别注意分支数通常4个分支对应1/4到1/32分支越多精度可能越好但计算量越大。每个分支的通道数常见配置是[32, 64, 128, 256]或[48, 96, 192, 384]后者精度更高但更重。信息交换的频率每个stage都交换一次stage数越多交换越充分但收益会递减。我在复现HRNet时一开始为了省显存把通道数减半结果AP掉了将近5个点后来恢复原配置才达到论文水平。所以如果条件允许尽量不要随意缩减通道数。3.3 损失函数热图回归的细节关键点检测的损失函数通常用均方误差计算预测热图和真实热图之间的逐像素差异。但直接MSE有个问题背景区域占绝大多数正样本关键点附近很少网络容易偏向预测背景。常见的改进有加权MSE给正样本区域更高的权重比如用真实热图的值作为权重。Focal Loss变体借鉴目标检测里的Focal Loss降低易分类样本的权重。KL散度把热图看作概率分布用KL散度衡量预测分布和真实分布的差异。我实测下来加权MSE简单有效权重直接用真实热图的值就行不需要额外调参。另外有些工作会在损失里加入坐标回归分支直接回归关键点的x、y坐标和热图损失联合训练也能提升精度。3.4 后处理从热图到坐标网络输出的是热图需要转成关键点坐标。最简单的方法是取热图峰值位置即argmax。但argmax得到的是整数坐标精度受输出分辨率限制。常用的改进是亚像素定位在峰值附近取一个窗口用高斯拟合或者加权平均来估计更精确的坐标。具体做法是找到峰值点后以它为中心取一个5x5或7x7的窗口计算窗口内所有像素的加权平均坐标权重用热图值。这样能把定位精度提升到亚像素级别。我在手腕、脚踝这些小关节上试过亚像素定位能降低约0.5到1个像素的误差对大关节影响不大。对于自底向上的方法后处理还包括关节点分组。OpenPose用PAF做贪婪匹配具体是先计算所有关节点对之间的PAF线积分然后按积分值从高到低排序依次连接同时保证每个关节点只属于一个人。这个逻辑实现起来不复杂但要注意阈值设置积分值太低的连接要丢弃否则会连出错误的骨架。4. 完整实操流程与关键环节实现4.1 环境搭建与依赖安装我一般用Python 3.8加PyTorch 1.7以上的版本CUDA版本根据显卡驱动选。下面是一套我常用的环境配置# 创建虚拟环境 conda create -n pose python3.8 conda activate pose # 安装PyTorch根据你的CUDA版本调整 pip install torch1.9.0 torchvision0.10.0 # 安装其他依赖 pip install numpy opencv-python matplotlib tqdm pyyaml pip install scipy # 用于高斯拟合等后处理如果你要用HRNet官方代码还需要安装额外的依赖比如mmcv。我建议直接看官方仓库的requirements按版本装不要随意升级否则容易出现API不兼容。4.2 数据加载与预处理流水线数据加载这块我习惯自己写Dataset类因为官方代码有时封装太深改起来不方便。核心逻辑是读图片、读标注、做增强、生成热图、返回张量。import cv2 import numpy as np import torch from torch.utils.data import Dataset class PoseDataset(Dataset): def __init__(self, ann_file, img_prefix, input_size(256, 192), heatmap_size(64, 48)): self.anns load_annotations(ann_file) # 自定义加载函数 self.img_prefix img_prefix self.input_size input_size # 宽, 高 self.heatmap_size heatmap_size def __getitem__(self, idx): ann self.anns[idx] img cv2.imread(f{self.img_prefix}/{ann[image]}) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 根据bbox裁剪并缩放到input_size bbox ann[bbox] crop, scale, offset crop_and_resize(img, bbox, self.input_size) # 关键点坐标变换到裁剪后的坐标系 kps ann[keypoints].copy() kps[:, :2] kps[:, :2] * scale offset # 生成热图 heatmaps generate_heatmaps(kps, self.heatmap_size, sigma2) # 转成张量 crop torch.from_numpy(crop).permute(2, 0, 1).float() / 255.0 heatmaps torch.from_numpy(heatmaps).float() return crop, heatmaps这里有几个细节值得展开。裁剪比例通常会把bbox向外扩一点比如扩1.25倍这样能保留更多上下文信息对关键点检测有帮助。缩放策略保持长宽比缩放短边补齐避免人体变形。热图sigma在输出热图分辨率下sigma取2左右比较合适对应输入分辨率下的sigma大约是2乘以缩放倍数。4.3 模型训练超参设置与训练技巧训练关键点检测模型下面这套配置我用了很多次比较稳超参取值说明优化器Adam比SGD收敛快适合关键点任务初始学习率1e-3如果loss震荡就降到5e-4学习率策略多步衰减在epoch 30、60、90各降10倍Batch size32根据显存调整太小会影响BN效果训练轮数100-150看验证集AP不再提升就停权重衰减1e-4防止过拟合训练时我习惯每5个epoch在验证集上算一次AP保存最好的模型。另外预训练backbone很重要用ImageNet预训练的ResNet或HRNet初始化能明显加快收敛。如果从头训练可能需要多花一倍的时间才能达到相同精度。还有一个技巧是多尺度训练每个batch随机选一个尺度比如0.75、1.0、1.25倍做数据增强测试时用多个尺度预测再平均。这样能提升模型对不同尺度人体的适应能力AP通常能涨1到2个点。4.4 推理与部署从PyTorch到ONNX训练完模型后部署时通常要转成ONNX或者TensorRT。转ONNX时要注意动态轴的设置因为输入图片的尺寸可能不固定。下面是一个转换示例import torch import torch.onnx model YourPoseModel() model.load_state_dict(torch.load(best_model.pth)) model.eval() dummy_input torch.randn(1, 3, 256, 192) torch.onnx.export( model, dummy_input, pose.onnx, input_names[input], output_names[heatmap], dynamic_axes{input: {0: batch, 2: height, 3: width}, heatmap: {0: batch, 2: height, 3: width}}, opset_version11 )转完后建议用onnxruntime跑一遍对比PyTorch和ONNX的输出差异确保数值一致。我遇到过因为opset版本不对导致输出偏差的情况换到opset 11就正常了。如果要用TensorRT加速可以用trtexec工具把ONNX转成engine注意选择FP16或INT8精度。FP16通常能提速30%到50%精度损失很小INT8需要校准数据精度损失稍大但速度提升更明显。5. 常见问题与排查技巧实录5.1 训练不收敛或loss震荡这是最常见的问题。我排查时一般按下面顺序来检查学习率太大导致震荡太小导致收敛慢。可以先跑几个epoch看loss曲线如果上下跳动明显降学习率。检查数据标注有没有关键点坐标超出图片范围有没有可见性标注错误我遇到过一次标注文件里有些关键点的x、y是负数导致热图生成异常loss一直很高。检查热图生成高斯核大小是否合适如果sigma太小正样本区域几乎为零网络学不到东西。检查BN层batch size太小会导致BN统计不准可以改用GroupNorm或者SyncBN。5.2 验证集AP远低于训练集这通常是过拟合或者数据分布不一致。过拟合的话加数据增强、加权重衰减、减模型复杂度。数据分布不一致的话检查训练集和验证集的标注格式、图片分辨率、人体尺度是否差异太大。我有一次用COCO训练、MPII验证AP低得离谱后来发现两个数据集的关节定义不一样统一成COCO格式后才正常。5.3 多人场景下骨架连错自底向上方法常见的问题。排查方向PAF阈值太低会连出错误连接太高会漏连。可以画一下PAF的积分值分布选一个合适的阈值。关节点检测漏检如果某个关节没检测到分组时就会断。可以降低热图峰值阈值让更多候选点进入分组。分组算法贪婪匹配在拥挤场景下容易出错可以改用匈牙利算法做全局最优匹配但速度会慢一些。5.4 推理速度不达标如果FPS不够可以按下面顺序优化优化手段预期提速精度影响减小输入分辨率30%-50%小关节精度下降换轻量backbone50%-100%AP下降2-5点FP16推理30%-50%几乎无影响TensorRT优化50%-100%几乎无影响减少网络阶段数20%-40%AP下降1-3点我一般先试FP16和TensorRT这两个对精度影响最小。如果还不够再考虑换轻量模型。5.5 常见问题速查表问题现象可能原因排查方法loss不下降学习率太小、数据标注错误检查标注、调大学习率loss震荡学习率太大、batch size太小降学习率、增大batch验证AP低过拟合、数据分布不一致加增强、检查数据集左右不分翻转增强未交换索引检查flip_pairs小关节不准输出分辨率太低提高输出热图分辨率多人连错PAF阈值不当调整阈值、换分组算法推理慢模型大、未优化FP16、TensorRT、轻量模型6. 几个我踩过的坑和独家经验第一个坑是热图分辨率的选择。一开始我为了省计算把输出热图设成输入的四分之一结果手腕、脚踝的定位误差很大。后来改成二分之一精度明显提升但速度也降了不少。折中方案是用四分之一热图加亚像素定位精度接近二分之一热图速度却快很多。第二个坑是数据增强的强度。旋转角度太大比如超过45度会导致人体姿态不自然网络学到的先验被破坏。我一般把旋转限制在正负30度以内缩放限制在0.75到1.25倍这样既增加了多样性又不会引入太多噪声。第三个坑是多尺度测试的融合方式。一开始我把不同尺度的热图直接相加结果精度反而下降。后来改成先分别做亚像素定位得到坐标再对坐标做平均效果好很多。原因是不同尺度的热图峰值位置可能有偏移直接相加会导致峰值模糊。第四个坑是模型集成。把HRNet和SimpleBaseline的预测结果融合确实能提升AP但提升幅度有限大概1个点左右而推理时间翻倍。如果对实时性有要求不建议做集成。最后分享一个小技巧用关键点检测的结果反过来过滤人体检测框。如果某个框里检测到的关键点太少或者置信度太低可以认为这个框是误检直接丢弃。这样能减少自顶向下方法里的无效计算也能提升最终输出的质量。我在一个视频分析项目里用了这个技巧误检率降了将近一半。这个方向后续还可以往3D关键点、视频时序姿态估计、轻量化移动端部署等方向扩展。如果这篇文章反馈不错后面可以再聊聊怎么把2D检测结果提升到3D以及怎么在视频里利用时序信息做平滑和补帧。