☰
2016-2020人体2D关键点检测:从CPM到HRNet的实战复盘
2026/10/15 10:44:11 网站建设 项目流程

1. 为什么2020到2016年的人体2D关键点检测值得反复研究

如果你正在做人机交互、运动分析、虚拟形象驱动或者视频监控相关的项目,大概率绕不开一个基础模块:从一张普通RGB图片里把人的骨架关节点找出来。这件事在学术圈叫人体2D关键点检测,也有人叫它人体姿态估计、骨架提取、关键点定位。名字听着挺学术,但说白了就是让计算机回答一个问题:画面里这个人的肩膀、手肘、手腕、膝盖、脚踝分别在哪个像素位置上。

我最早接触这个方向是在2018年前后,当时想给一个健身动作纠正的小工具加姿态识别功能。试过用传统方法做背景分割加轮廓分析,效果惨不忍睹,稍微换个衣服颜色或者光线暗一点就全乱套。后来转向基于深度学习的关键点检测,才真正把准确率做上来。而2016到2020这五年,恰好是这个领域从“能跑通”到“跑得准”的关键窗口期,很多经典思路和开源实现至今仍在工业界大量使用。

这篇文章不是论文导读,也不是简单的代码罗列。我想从一个实际使用者的角度,把这五年里几个代表性方法的核心思路、代码落地的关键细节、以及我在复现和改造过程中踩过的坑,系统地梳理一遍。无论你是刚入门想找个能跑通的baseline,还是已经用过一些模型但想搞清楚背后的设计逻辑,应该都能从中找到有用的东西。文章会涉及多人姿态估计和单人姿态估计两条主线,覆盖自顶向下和自底向上两种范式,并给出可直接参考的代码结构和训练配置建议。

2. 五年技术脉络的整体拆解与方案选型逻辑

2.1 两条技术路线的分野:自顶向下 vs 自底向上

人体2D关键点检测在2016年之后基本分成了两大阵营。自顶向下的思路是先用一个人体检测器把画面里的每个人框出来,然后对每个框单独做单人关键点检测。自底向上则是先把画面里所有人的关节点都检测出来,再通过分组算法把属于同一个人的关节点连起来。

这两种思路的取舍非常实际。自顶向下的优势在于每个框里的人体尺度归一化了,关键点检测网络只需要专注做好“给定一个人,找他的关节”这件事,精度通常更高。但它的速度受人数影响很大,画面里人越多,需要跑的检测次数越多。自底向上则相反,网络前向一次就能得到所有人的关节点,速度基本恒定,但分组逻辑在拥挤场景下容易出错,而且高分辨率输入下计算量也不小。

我在实际项目里选型时,如果场景人数可控(比如单人健身跟练、人机交互),优先选自顶向下,精度有保障。如果是监控场景人数多且对实时性要求高,会考虑自底向上或者轻量化的自顶向下方案。2016到2020年间,这两条路线都有里程碑式的工作,下面分别拆解。

2.2 从CPM到OpenPose:自底向上的工程化突破

2016年的CPM(Convolutional 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 python=3.8 conda activate pose # 安装PyTorch(根据你的CUDA版本调整) pip install torch==1.9.0 torchvision==0.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, sigma=2) # 转成张量 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_version=11 )

转完后建议用onnxruntime跑一遍,对比PyTorch和ONNX的输出差异,确保数值一致。我遇到过因为opset版本不对导致输出偏差的情况,换到opset 11就正常了。

如果要用TensorRT加速,可以用trtexec工具把ONNX转成engine,注意选择FP16或INT8精度。FP16通常能提速30%到50%,精度损失很小;INT8需要校准数据,精度损失稍大,但速度提升更明显。

5. 常见问题与排查技巧实录

5.1 训练不收敛或loss震荡

这是最常见的问题。我排查时一般按下面顺序来:

  1. 检查学习率:太大导致震荡,太小导致收敛慢。可以先跑几个epoch,看loss曲线,如果上下跳动明显,降学习率。
  2. 检查数据标注:有没有关键点坐标超出图片范围?有没有可见性标注错误?我遇到过一次,标注文件里有些关键点的x、y是负数,导致热图生成异常,loss一直很高。
  3. 检查热图生成:高斯核大小是否合适?如果sigma太小,正样本区域几乎为零,网络学不到东西。
  4. 检查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,以及怎么在视频里利用时序信息做平滑和补帧。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询