Deep High-Resolution Representation Learning for Human Pose Estimation
Abstract
-
在本文中,我们感兴趣的人的姿态估计问题,重点是学习可靠的高分辨率表示。大多数现有的方法恢复高分辨率表示从低分辨率表示由高到低分辨率网络。相反,我们提出的网络在整个过程中保持高分辨率表示。
-
我们从一个高分辨率的子网络开始作为第一阶段,逐步增加高分辨率到低分辨率的子网络,形成更多的阶段,并将多分辨率的子网络并行连接起来。我们进行重复的多尺度融合,使得每个高分辨率到低分辨率的表示不断接收其他并行表示的信息,从而产生丰富的高分辨率表示。因此,预测的关键点热图可能更准确,在空间上更精确。我们通过两个基准数据集上的上级姿态估计结果来经验性地证明我们的网络的有效性:COCO关键点检测数据集和MPII人体姿势数据集。此外,我们在PoseTrack数据集上展示了我们的网络在姿态跟踪方面的优势。代码和模型已在 leoxiaobin/deep-high-resolution-net.pytorch: The project is an official implementation of our CVPR2019 paper "Deep High-Resolution Representation Learning for Human Pose Estimation"
-
提出了HRNet(High-Resolution Net,高分辨率网络) ,针对 2D 人体姿态估计任务的核心痛点,打破了传统网络 "先高到低降采样、再低到高恢复分辨率" 的串行架构范式,实现了全程保持高分辨率表征 的核心设计。网络以高分辨率子网络为起点,逐步新增并行的高低分辨率子网络,通过重复多尺度信息融合让不同分辨率表征持续交互,生成兼具高语义准确性与空间精度的关键点热图。
Introduction
-
二维人体姿态估计是计算机视觉中的一个基本而又具有挑战性的问题,其目标是定位人体解剖学上的关键点(如肘、腕等)或部位,在人体动作识别、人机交互、动画等方面有着广泛的应用。本文主要研究的是单人位姿估计问题,它是多人位姿估计,视频姿态估计和跟踪等相关问题的基础
-
最近的发展表明,深度卷积神经网络已经达到了最先进的性能。大多数现有的方法通过网络传递输入,通常由串联连接的高到低分辨率子网络组成,然后提高分辨率。例如,Hourglass 通过对称的从低到高的过程恢复高分辨率。SimpleBaseline 采用一些转置卷积层来生成高分辨率。此外,扩张卷积也用于放大高到低分辨率网络(例如,VGGNet或ResNet)的后面的层。
-
本文提出了一种新的结构--高分辨率网络(HighResolutionNet,HRNet),它能够在整个过程中保持高分辨率的表示,从高分辨率的子网络开始,逐步增加高分辨率到低分辨率的子网络,形成更多的阶段,并将多分辨率子网络并行连接。通过在并行多分辨率子网络之间交换信息,进行重复的多尺度融合。在整个过程中,我们一遍又一遍地使用高分辨率子网络。我们通过网络输出的高分辨率表示来估计关键点。最终的网络如图1所示。
-

-
图 1. 说明了所提出的 HRNet 的架构。它由并行的高到低分辨率子网络组成,并在多分辨率子网络之间重复信息交换(多尺度融合)。水平和垂直方向分别对应于网络的深度和特征图的比例。
-
-
与现有广泛使用的用于姿态估计的网络相比,我们的网络具有两个优点。(i)我们的方法将高分辨率到低分辨率的子网络并行连接,而不是像大多数现有解决方案中那样串联连接。因此,我们的方法能够保持高分辨率,而不是通过低到高的过程恢复分辨率,因此,预测的热图在空间上可能更精确 。(ii)大多数现有的融合方案聚合低级和高级表示。相反,我们执行重复的多尺度融合,以在相同深度和相似级别的低分辨率表示的帮助下提升高分辨率表示,反之亦然,从而导致高分辨率表示对于姿态估计也是丰富的。因此,我们预测的热图可能更准确。
-
我们通过经验证明了两个基准数据集的上级关键点检测性能:COCO关键点检测数据集和MPII Human Pose数据集。此外,我们还展示了我们的网络在PoseTrack数据集上的视频姿势跟踪方面的优越性。
-
HRNet 针对传统网络 "先降采样再恢复分辨率" 串行架构导致的空间信息丢失、高分辨率表征不连续的核心痛点,提出了两大颠覆性架构设计:
- 并行多分辨率子网络设计 :打破了传统串行级联范式,以高分辨率子网络为起点,逐步新增并行的低分辨率分支,所有分辨率分支全程并行运行,全程保持高分辨率表征,无需后续单独的分辨率恢复过程,从根源上避免了分辨率恢复带来的空间信息不可逆丢失;
- 重复多尺度融合(交换单元)设计 :通过交换单元在网络全程持续进行跨分支的双向信息交互,让高分辨率特征获得低分辨率分支的强语义信息,低分辨率特征也能获得高分辨率分支的精细空间信息,实现了高低分辨率特征的双向赋能,解决了传统方法仅能单向融合高低层级特征的局限。
-
其本质是实现了高空间定位精度与高语义判别准确性的兼顾,让最终输出的关键点热图既具备亚像素级的精准位置信息,又有足够强的语义判别能力,大幅提升了关键点检测的鲁棒性与精度。
Related Work
-
单人姿势估计的大多数传统解决方案采用概率图模型或图像结构模型,最近通过利用深度学习更好地建模一元和成对能量或模仿迭代推理过程来改进。如今,深度卷积神经网络提供了主导解决方案,有两种主流方法:回归关键点的位置,以及估计关键点热图,然后选择具有最高热度值的位置作为关键点。
-
大多数用于关键点热图估计的卷积神经网络由一个类似于分类网络的主干子网络组成,它降低了分辨率,一个主体产生与其输入相同分辨率的表示,然后是一个回归器估计热图,其中关键点位置被估计,然后在全分辨率下转换。主体主要采用从高到低和从低到高的框架,可能用多尺度融合和中间(深度)监督来增强。
-
从高到低和从低到高。从高到低的过程旨在生成低分辨率和高级别的表示,从低到高的过程旨在生成高分辨率的表示。这两个过程都可能重复多次以提高性能。
-
典型的网络设计模式包括:(i)对称的高到低和低到高过程。沙漏及其后续设计了低到高过程作为高到低过程的镜像。(ii)重的高到低和轻的低到高。高到低过程基于ImageNet分类网络,例如,而从低到高的过程只是一些双线性上采样或转置卷积层。(iii)与扩张卷积的组合。在ResNet或VGGNet的最后两个阶段中采用扩张卷积来消除空间分辨率损失,然后是轻微的从低到高的过程以进一步提高分辨率,避免了仅使用扩张卷积的昂贵计算成本。图2示出了四个代表性的姿态估计网络。
-

-
图 2.依赖于从高到低和从低到高框架的代表性姿态估计网络的图示。 (一)沙漏。(b) 级联金字塔网络。 © SimpleBaseline :用于从低到高处理的转置卷积。 (d) 与扩张卷积的组合。右下图例:reg = 常规卷积,扩张 = 扩张卷积,trans = 转置卷积、跨步 = 跨步卷积、concat =串联。在(a)中,从高到低和从低到高的过程是对称的。在(b)、©和(d)中,作为分类网络(ResNet或VGGNet)的一部分的从高到低的过程是重的,而从低到高的过程是轻的。在(a)和(b)中,高到低和低到高过程的相同分辨率层之间的跳跃连接(虚线)主要旨在融合低级和高级特征。在(b)中,右侧部分finenet结合了通过卷积处理的低级和高级特征。
-
-
多尺度融合。直接的方法是将多分辨率图像分别馈送到多个网络中并聚合输出响应图。Hourglass 及其扩展通过跳跃连接将高到低过程中的低级别特征逐渐组合到低到高过程中的相同分辨率高级别特征。在级联金字塔网络中,一个globalnet在从高到低的过程中将低到高的级别特征逐渐组合到从低到高的过程中,然后一个refinenet将通过卷积处理的从低到高的级别特征组合起来。我们的方法重复多尺度融合,这部分受到了深度融合及其扩展的启发。
-
中间监督。早期为图像分类而开发的中间监督或深度监督也被用来帮助深度网络训练和提高热图估计质量。沙漏方法和卷积姿态机方法将中间热图处理为剩余子网络的输入或输入的一部分。
-
我们的方法。我们的网络并行连接从高到低的子网络。它在整个过程中保持高分辨率表示,以实现空间精确的热图估计。它通过重复融合从高到低的子网络产生的表示来生成可靠的高分辨率表示。我们的方法与大多数现有作品不同,后者需要单独的从低到高的上采样过程并聚合低级和高级表示。我们的方法在不使用中间热图监督的情况下,在关键点检测精度方面具有优越性,并且在计算复杂性和参数方面具有高效性。
-
有相关的多尺度网络用于分类和分割。我们的工作部分受到其中一些的启发,并且存在明显的差异,使得它们不适用于我们的问题。由于每个子网络(深度、批量归一化)和多尺度融合缺乏适当的设计,卷积神经结构和互连的CNN无法产生高质量的分割结果。
-
网格网络,许多权重共享的U-net 的组合,由两个独立的多分辨率表示的融合过程组成:在第一阶段,信息仅从高分辨率发送到低分辨率;在第二阶段,信息仅从低分辨率发送到高分辨率,因此竞争力较低。多尺度densenets 不针对并且不能生成可靠的高分辨率表示。
| 方法 | 骨干网络 | 参数量 | GFLOPs | AP |
|---|---|---|---|---|
| 8-stage Hourglass | Hourglass | 25.1M | 14.3 | 66.9 |
| CPN+OHKM | ResNet-50 | 27.0M | 6.20 | 69.4 |
| SimpleBaseline | ResNet-152 | 68.6M | 15.7 | 72.0 |
| HRNet-W32(无预训练) | HRNet-W32 | 28.5M | 7.1 | 73.4 |
| HRNet-W32(ImageNet 预训练) | HRNet-W32 | 28.5M | 7.1 | 74.4 |
| HRNet-W48(ImageNet 预训练) | HRNet-W48 | 63.6M | 14.6 | 75.1 |
Approach
-
人体姿态估计,也称为关键点检测,旨在从大小为W × H × 3的图像 I 中检测 K 个关键点或部位(例如,肘部、手腕等)的位置。现有技术方法将该问题转换为估计大小为W ′ ×H ′的K个热图,{H1,H2,.,HK},其中每个热图 Hk 指示第 k 个关键点的位置置信度。
-
我们遵循广泛采用的流水线来使用卷积网络预测人类关键点,该卷积网络由两个步幅卷积组成的主干组成,降低分辨率,主体输出具有与其输入特征图相同分辨率的特征图,和一个回归估计热图的关键点位置的选择和转换到全分辨率。我们重点介绍了主体的设计,并介绍了我们的高分辨率网络(HRNet),如图1所示。
-
顺序多分辨率子网络:现有的姿态估计网络是通过串联连接高分辨率到低分辨率子网络来构建的,其中形成一个阶段的每个子网络由一系列卷积组成,并且存在跨相邻子网络的下采样层以将分辨率减半。
-
设 Nsr 是第 s 阶段的子网络,r 是分辨率指数(其分辨率是第一个子网络的分辨率的 1 2 r − 1 \frac{1}{2^{r−1}} 2r−11)。具有S(例如,4)个阶段的高到低网络可以表示为:
- N 11 → N 22 → N 33 → N 44 . ( 1 ) N_{11} → N_{22} → N_{33} → N_{44}. (1) N11→N22→N33→N44.(1)
-
并行多分辨率子网络。我们从一个高分辨率子网络开始作为第一阶段,逐步增加从高到低的分辨率子网络,形成新的阶段,并将多分辨率子网络并行连接。因此,后一阶段的并行子网络的分辨率由前一阶段的分辨率和额外的更低分辨率组成。包含4个并行子网络的示例网络结构如下所示,
-
N_{11}\\to N_{21}\\to N_{31}\\to N_{41} \\ ```\searrow ~~~~~~~~~~~~~~~~~~~~~~\searrow N_{33}\to N_{43}\\ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\searrow N_{44}
-
-
重复多尺度融合。我们在并行子网络之间引入交换单元,使得每个子网络重复接收来自其他并行子网络的信息。这里是一个例子,展示了交换信息的方案。我们将第三阶段分为几个阶段。(例如,3个)交换块,并且每个块由3个并行卷积单元组成,其中交换单元跨并行单元,其给出如下,
-
C 31 1 ↘ ↗ C 31 2 ↘ ↗ C 31 3 ↘ C 32 1 ⟶ ε 3 1 ⟶ C 32 2 ⟶ ε 3 2 ⟶ C 32 3 ⟶ ε 3 3 C 33 1 ↗ ↘ C 33 2 ↗ ↘ C 33 3 ↗ C^1_{31}\searrow ~~~~~~~~ \nearrow C^2_{31}\searrow ~~~~~~~~\nearrow C^3_{31}\searrow \\ C^1_{32}\longrightarrow \varepsilon^1_3 \longrightarrow C^2_{32}\longrightarrow \varepsilon^2_3 \longrightarrow C^3_{32}\longrightarrow \varepsilon^3_3\\ C^1_{33}\nearrow ~~~~~~~~ \searrow C^2_{33}\nearrow ~~~~~~~~\searrow C^3_{33}\nearrow C311↘ ↗C312↘ ↗C313↘C321⟶ε31⟶C322⟶ε32⟶C323⟶ε33C331↗ ↘C332↗ ↘C333↗
-
其中 C s r b C^b_{sr} Csrb 表示第 s 级中第 b 个块的第 r 分辨率的卷积单位, ε s b \varepsilon^b_s εsb 是对应的交换单位。
-
-
我们在图3中说明了交换单元,并在下面给出了公式。为了讨论方便,我们去掉了下标s和上标b。输入是s响应映射:{X1,X2,.,Xs}。输出是s响应映射:{Y1,Y2,.,Ys},其分辨率和宽度与输入相同。每个输出是输入映射的聚合, Y k = ∑ i = 1 s a ( X i , k ) Y_k = \sum^s_{i=1} a(X_i,k) Yk=∑i=1sa(Xi,k) 跨级交换单元具有额外的输出映射 Y s + 1 : Y s + 1 = a ( Y s , s + 1 ) Y_{s+1}:Y_{s+1} = a(Y_s,s + 1) Ys+1:Ys+1=a(Ys,s+1) 。
-

-
图 3. 说明交换单元如何从左到右分别聚合高分辨率、中度和低分辨率的信息。右图例:strided 3×3 = strided 3×3 卷积,上采样。 1×1 = 1 × 1 卷积后的最近邻上采样。
-
-
函数a(Xi,k)由从分辨率 i 到分辨率 k 的上采样或下采样 Xi 组成。我们采用步幅3 × 3卷积进行下采样。例如,一个步幅为 2 的步幅 3×3 卷积用于 2× 下采样,两个连续步幅为2的步幅3 × 3卷积用于4×下采样。对于上采样,采用1 × 1卷积后的简单最近邻采样来对齐通道数,当 i = k 时,a(·,·)就是一个标识连接:a(Xi,k)= Xi .
-
热图估计。我们简单地从最后一个交换单元输出的高分辨率表示中回归热图,经验上效果很好。损失函数,定义为均方误差,用于比较预测的热图和地面实况热图。GT 热图是通过应用2D高斯产生的,标准偏差为1像素,以每个关键点的 group truth 位置为中心。网络实例化。我们通过遵循ResNet的设计规则来实例化用于关键点热图估计的网络,以将深度分配到每个阶段,并将通道数量分配到每个分辨率。
-
主体,即我们的 HRNet,包含四个阶段和四个并行子网络,其分辨率逐渐降低到一半,相应的宽度(通道数)增加到两倍。第一阶段包含 4 个残差单元,每个单元与 ResNet-50 相同,由宽度为 64 的瓶颈形成,后面是一个 3×3 卷积,将特征图的宽度减小到 C。第二、第三、第四阶段分别包含 1、4、3 个交换块。一个交换块包含 4 个残差单元,其中每个单元在每个分辨率下包含两个 3 × 3 卷积,以及一个跨分辨率的交换单元。综上,总共有8个交换单元,即进行了8次多尺度融合。
-
在我们的实验中,我们研究了一小网和一大网:HRNet-W32和HRNet-W48,其中32和48分别表示最后三个阶段高分辨率子网的宽度(C)。其他三个并行子网络的宽度对于HRNet-W32为64、128、256,对于HRNet-W48为96、192、384。
-
HRNet 的核心设计:
-
从高分辨率子网络(如 1/1 尺度)作为初始分支,逐步添加 1/2、1/4、1/8 尺度的低分辨率分支,所有分支并行存在;
-
执行重复多尺度融合:每个阶段让不同分辨率分支互相交互(高分辨率分支向低分辨率传递空间细节,低分辨率向高分辨率传递语义信息);
-
最终直接从最高分辨率分支输出关键点热力图,无需大量反卷积 "补救" 分辨率损失。
-

-
图 4. MPII(上)和 COCO(下)数据集中一些示例图像的定性结果:包含视点和外观变化、遮挡、多人和常见的成像伪影。
-
Experiments
COCO Keypoint Detection
-
数据集。 COCO 数据集 包含超过 200, 000 个图像和 250, 000 个用 17 个关键点标记的人物实例。我们在 COCO train2017 数据集上训练我们的模型,包括 57K 图像和 150K 人物实例。我们在 val2017 集和 test-dev2017 集上评估我们的方法,分别包含 5000 张图像和 20K 图像。
-
评估指标。标准评估指标基于对象关键点相似度 (OKS): O K S = ∑ i e x p ( − d i 2 / 2 s 2 k i 2 ) δ ( v i > 0 ) ∑ i δ ( v i > 0 ) OKS =\frac{\sum_i exp(−d^ 2_i /2s^2k^2_i )δ(vi>0)}{\sum_i δ(v_i>0)} OKS=∑iδ(vi>0)∑iexp(−di2/2s2ki2)δ(vi>0) 。这里 di 是检测到的关键点和相应的 GT 之间的欧几里德距离,vi 是 GT 的可见性标志,s 是对象比例,ki 是控制衰减的每个关键点常量。我们报告标准平均精度和召回分数:AP50(OKS 处的 AP = 0.50)AP75、AP(10 个位置处 AP 分数的平均值,OKS = 0.50、0.55、...、0.90、0.95;中等对象的 APM、大型对象的 APL、OKS 处的 AR = 0.50、0.55、...、 0.90、0.95。
-
训练。我们将人体检测框的高度或宽度扩展到固定的长宽比:高度:宽度= 4:3,然后从图像中裁剪该框,将其大小调整为固定大小,256×192或384×288。数据增强包括随机旋转(−45°,45°),随机缩放(0.65,1.35)和翻转。还涉及半身数据增强。
-
我们使用 Adam 优化器 。学习计划遵循设置。基础学习率设置为 1e−3,并在第 170 和 200 epoch 分别降至 1e−4 和 1e−5。训练过程在 210 个 epoch 内终止。
-
测试。使用类似于的两阶段自上而下范式:使用人物检测器检测人物实例,然后预测检测关键点。我们对验证集和测试开发集使用 SimpleBaseline 提供的同一个人检测器。我们通过平均原始图像和翻转图像的头部图来计算热图。通过在从最高响应到第二高响应的方向上调整最高热值位置四分之一偏移来预测每个关键点位置。
-
验证集上的结果。我们在表 1 中报告了我们的方法和其他最先进方法的结果。我们的小型网络 - HRNet-W32,使用输入大小 256 × 192 从头开始训练,获得了 73.4 的 AP 分数,优于具有相同输入大小的其他方法。 (i) 与 Hourglass 相比,我们的小型网络将 AP 提高了 6.5 个点,并且我们网络的 GFLOPs 低得多,不到一半,而参数数量相似,我们的网络稍大一些。 (ii) 与没有和有 OHKM 的 CPN 相比,我们的网络模型尺寸稍大,复杂度稍高,分别获得了 4.8 和 4.0 点的增益。(iii) 与之前性能最佳的 SimpleBaseline 相比,我们的 HRNet-W32 获得了显着的改进:具有相似模型大小和 GFLOP 的主干 ResNet-50 获得了 3.0 点增益,模型大小 (#Params) 和 GFLOP 是我们的两倍的主干 ResNet-152 获得了 1.4 点增益。
-

-
表 1. COCO 验证集上的比较。 Pretrain = 在 ImageNet 分类任务上预训练主干网。 OHKM = 在线硬关键点挖掘。
-
-
我们的网络可以受益于 (i) 在 ImageNet 上预训练的模型进行训练:HRNetW32 的增益为 1.0 点; (ii) 通过增加宽度来增加容量:对于输入大小 256 × 192 和 384 × 288,HRNet-W48 分别获得 0.7 和 0.5 点增益。
-
考虑到输入大小为384 × 288,我们的HRNet-W32和HRNet-W48得到了75.8和76.3 AP,与输入大小为256 × 192相比分别有1.4和1.2的改进。与使用ResNet-152作为主干的SimpleBaseline 相比,我们的HRNet-W32和HRNetW48在AP方面分别获得1.5和2.0个点的增益,计算成本分别为45%和92.4%。
-
测试开发集上的结果。表2报告了我们的方法和现有的最先进的方法的姿态估计性能。我们的方法明显优于自底向上的方法。另一方面,我们的小型网络HRNet-W32的AP为74.9。它优于所有其他自顶向下的方法,并且在模型大小(#Params)和计算复杂性(GFLOPs)方面更有效。我们的大型号HRNet-W48,达到了最高的75.5 AP。与具有相同输入大小的SimpleBaseline 相比,我们的小型和大型网络分别获得1.2和1.8的改进。使用来自AI Challenger 的额外数据进行训练,我们的单个大网络可以获得77.0的AP。
-

-
表 2. COCO 测试开发集的比较。 #Params 和 FLOPs 是针对姿态估计网络计算的,不包括用于人体检测和关键点分组的参数。
-
MPII Human Pose Estimation
-
数据集。MPII人体姿态数据集包括从大范围的真实世界活动中拍摄的带有全身姿态注释的图像。大约有25K个图像和40K个对象,其中有12K个对象用于测试,其余的对象用于训练集。数据扩充和训练策略与 MS COCO相同,除了输入大小被裁剪为256 × 256,以便与其他方法进行公平比较。
-
测试。测试过程与COCO中的测试过程几乎相同,除了我们采用标准测试策略使用提供的人员框而不是检测到的人员框。执行六级金字塔测试过程。
-
评估指标。使用标准指标,PCKh(正确关键点的头部归一化概率)得分。如果关节福尔斯在地面实况位置的αl像素内,则关节是正确的,其中α是常数,l是头部大小,对应于地面实况头部边界框对角线长度的60%。报告PCKh@0.5(α = 0.5)得分。
-
测试集上的结果。表3和表4显示了PCKh@0.5的结果,模型大小和性能最好的方法的GFLOPs。我们通过使用ResNet-152作为输入大小为256 × 256的主干来重新实现SimpleBaseline 。我们的HRNet-W32达到了92.3 PCKh@0.5得分,并且优于堆叠沙漏方法及其扩展。我们的结果与2018年11月16日排行榜上先前发表的结果中的最佳结果相同。我们想指出的是,与我们的方法互补的方法利用组合模型来学习人体的配置,并采用多级中间监督,我们的方法也可以从中受益 。我们还测试了我们的大网络-HRNetW 48,并获得了相同的结果92.3。原因可能是该数据集中的性能趋于饱和。
-

-
表 3. MPII 测试集上的比较 (PCKh@0.5)。
-

-
表 4. 表 3 中报告的一些性能最佳方法的#Params 和 GFLOP。GFLOP 是使用输入大小 256 × 256 计算的。
-
Application to Pose Tracking
-
数据集。 PoseTrack 是视频中人体姿势估计和关节跟踪的大规模基准。该数据集基于流行的 MPII Human Pose 数据集提供的原始视频,包含 550 个视频序列,66、374 帧。视频序列分为 292、50、208 个视频,分别用于训练、验证和测试。训练视频的长度范围在 41−151 帧之间,距离视频中心 30 帧有密集注释。验证/测试视频中的帧数范围在 65 - 298 帧之间。 MPII Pose 数据集中关键帧周围的 30 帧进行了密集注释,之后每第四帧进行注释。总共,这包含大约 23, 000 个标记帧和 153, 615 个姿势注释。
-
评估指标。我们从两个方面评估结果:逐帧多人姿态估计和多人姿态跟踪。姿态估计通过平均精度 (mAP) 进行评估,如 中所做的那样。多人姿态跟踪通过多目标跟踪精度(MOTA)进行评估 。
-
训练。我们在 PoseTrack2017 训练集上训练 HRNet-W48 以进行单人姿势估计,其中网络由在 COCO 数据集上预训练的模型初始化。我们通过将所有关键点(针对一个人)的边界框长度扩展 15%,从训练帧中带注释的关键点中提取人物框作为网络的输入。训练设置(包括数据增强)与 COCO 几乎相同,只是学习计划不同(现在是用于微调):学习率从 1e−4 开始,在第 10 个 epoch 下降到 1e−5,在第 15 个 epoch 下降到 1e−6;迭代在 20 个 epoch 内结束。
-
测试。我们按照来跟踪跨帧的姿势。它由三个步骤组成:人物框检测和传播、人体姿势估计以及跨邻近帧的姿势关联。我们使用与 SimpleBaseline 中使用的相同的人物框检测器,并通过根据 FlowNet 2.0 计算的光流传播预测的关键点,将检测到的框传播到附近的帧中,然后进行非极大值抑制以去除框。姿态关联方案基于一帧中的关键点与根据光流从附近帧传播的关键点之间的对象关键点相似性。然后使用贪婪匹配算法来计算附近帧中关键点之间的对应关系。
-
PoseTrack2017 测试集的结果。表 5 报告了结果。与第二佳方法(SimpleBaseline 中使用 ResNet-152 作为主干的 FlowTrack)相比,我们的方法在 mAP 和 MOTA 方面分别获得了 0.3 和 0.1 点的增益。相对于 FlowTrack 的优越性与 COCO 关键点检测和 MPII 人体姿势估计数据集上的优越性一致。这进一步暗示了我们的姿态估计网络的有效性。
-

-
表 5. PoseTrack2017 测试集上的姿态跟踪结果。
-
Ablation Study
-
我们研究了我们的方法中每个组件对 COCO 关键点检测数据集的影响。除有关输入尺寸影响的研究外,所有结果都是在 256 × 192 输入尺寸上获得的。
-
反复进行多尺度融合。我们实证分析了重复多尺度融合的效果。我们研究了我们网络的三种变体。 (a)W/o中间交换单元(1融合):除了最后一个交换单元之外,多分辨率子网络之间没有交换。 (b) W/跨阶段交换单元(3个融合):每个阶段内的并行子网络之间没有交换。 © W/跨级和级内交换单元(总共8个融合):这是我们提出的方法。所有网络都是从头开始训练的。表 6 给出的 COCO 验证集结果表明,多尺度融合是有帮助的,更多的融合会带来更好的性能。
-

-
表 6.重复多尺度融合中使用的交换单元的消融研究。国际。跨阶段交换=跨阶段的中间交换,Int。内部交换=阶段内的中间交换。
-
-
分辨率维护。我们研究了 HRNet 的一个变体的性能:所有四个从高到低分辨率的子网络都是在开始时添加的,并且深度相同;融合方案与我们的相同。我们的 HRNet-W32 和变体(具有类似的 #Params 和 GFLOP)都是从头开始训练并在 COCO 验证集上进行测试的。该变体的 AP 为 72.5,低于 HRNet-W32 的 73.4 AP。我们认为原因是从低分辨率子网络的早期阶段提取的低级特征没有多大帮助。此外,没有低分辨率并行子网络的类似参数和 GFLOP 的简单高分辨率网络表现出低得多的性能。
-
Representation resolution。我们从两个方面研究表示分辨率如何影响姿态估计性能:从高到低检查每个分辨率的特征图估计的热图的质量,并研究输入大小如何影响质量。我们训练由针对 ImageNet 分类预训练的模型初始化的小型和大型网络。我们的网络输出从高到低解决方案的四个响应图。最低分辨率响应图的热图预测质量太低,AP 分数低于 10 分。图 5 报告了其他三个地图的 AP 分数。比较表明分辨率确实影响关键点预测质量
-

-
图 5. 高分辨率和低分辨率表示的消融研究。 1×、2×、4×分别对应高、中、低分辨率的表示。
-
-
图 6 显示了与 SimpleBaseline (ResNet-50) 相比输入大小如何影响性能。由于在整个过程中保持高分辨率,较小输入尺寸的改进比较大输入尺寸更显着,例如,256×192的改进为4.0点,128×96的改进为6.3点。这意味着我们的方法在计算成本也是重要因素的实际应用中更具优势。另一方面,我们的输入尺寸为 256 × 192 的方法优于输入尺寸为 384 × 288 的 SimpleBaseline。
-

-
图 6. 说明了我们的 HRNet 和 SimpleBaseline 的性能如何受到输入大小的影响。
-
Conclusion and Future Works
-
在本文中,我们提出了一种用于人体姿态估计的高分辨率网络,产生准确且空间精确的关键点热图。其成功源于两个方面:(i)全程保持高分辨率,无需恢复高分辨率; (ii) 反复融合多分辨率表示,呈现可靠的高分辨率表示。未来的工作包括应用到其他密集的预测tasks,例如,人脸对齐,对象检测,语义分割,以及聚合多分辨率表示在一个不太轻的方式的调查。
-
2D 人体姿态估计是计算机视觉的基础任务,目标是定位人体解剖学关键点(如手肘、手腕等),是人体动作识别、人机交互、动画制作等应用的核心基础,同时也是多人姿态估计、视频姿态跟踪等复杂任务的基石。
-
主流深度学习方法均采用串行高到低 + 低到高的架构范式:先通过连续降采样提取高语义、低分辨率特征,再通过上采样、转置卷积、空洞卷积等方式恢复高分辨率,代表方案包括 Hourglass 对称编解码架构、SimpleBaseline 转置卷积轻量上采样、结合空洞卷积的 ResNet 变体等。
-
传统架构的核心痛点:分辨率恢复过程中会不可逆地丢失精细的空间位置信息,无法保证高分辨率表征的连续性,导致最终预测的关键点热图无法兼顾语义准确性与空间定位精度。
-
HRNet(高分辨率网络) 彻底打破了传统串行架构范式,核心创新分为两大核心设计,同时完成了标准化的网络实例化与训练流程定义。并行多分辨率子网络设计
- 网络以高分辨率子网络作为第一阶段,逐步新增高到低分辨率子网络形成新的阶段,所有分辨率子网络全程并行连接,而非传统的串行级联。
- 标准架构包含 4 个阶段,对应 4 路并行分支,分辨率依次减半,通道数依次翻倍,全程保持最高分辨率分支的连续运行,无需后续单独的分辨率恢复过程,从根源上避免了空间信息的丢失。
- 传统串行网络结构为 N11→N22→N33→N44,而 HRNet 为多分支并行的递进结构,每阶段新增一路更低分辨率分支,同时完整保留之前的所有分辨率分支。
-
重复多尺度融合(交换单元)设计。在网络全程中,通过交换单元(Exchange Unit) 跨并行多分辨率子网络进行重复的信息交互,让每一路分辨率表征都能持续接收其他并行分支的信息,实现高低分辨率特征的双向赋能。标准 HRNet 架构共执行8 次多尺度融合,其中跨阶段融合 3 次,阶段内融合 5 次。具体实现规则:
- 同分辨率分支:直接恒等映射传递特征,无额外变换;
- 降采样:采用步幅为 2 的 3×3 卷积实现 2 倍降采样,多倍降采样通过连续步幅卷积实现;
- 上采样:采用最近邻插值完成上采样,配合 1×1 卷积对齐通道数,保证特征维度匹配后再进行融合。
-
-
数据增强包括随机旋转(±45°)、随机缩放(0.65-1.35)、水平翻转、半身数据增强;损失函数采用均方误差(MSE),拟合以关键点真值为中心、标准差 1 像素的 2D 高斯热图。
多分辨率并行子网络设计
-
HRNet 彻底打破了传统 "高到低降采样 + 低到高恢复分辨率" 的串行架构,核心创新是全程保持高分辨率分支,并逐步新增并行的低分辨率分支,实现多分辨率表征的同步演进。
-
以高分辨率子网络为第一阶段,提取基础高分辨率特征。每进入新的阶段,新增一路分辨率减半的低分辨率分支,同时完整保留之前的所有分辨率分支,形成并行结构。分辨率每减半一次,通道数翻倍(保证信息容量的平衡)。以标准 4 阶段 HRNet 为例,架构如下:
-
#mermaid-svg-b2FNGMFO8EEZa19v{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-b2FNGMFO8EEZa19v .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-b2FNGMFO8EEZa19v .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-b2FNGMFO8EEZa19v .error-icon{fill:#552222;}#mermaid-svg-b2FNGMFO8EEZa19v .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-b2FNGMFO8EEZa19v .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-b2FNGMFO8EEZa19v .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-b2FNGMFO8EEZa19v .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-b2FNGMFO8EEZa19v .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-b2FNGMFO8EEZa19v .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-b2FNGMFO8EEZa19v .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-b2FNGMFO8EEZa19v .marker{fill:#333333;stroke:#333333;}#mermaid-svg-b2FNGMFO8EEZa19v .marker.cross{stroke:#333333;}#mermaid-svg-b2FNGMFO8EEZa19v svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-b2FNGMFO8EEZa19v p{margin:0;}#mermaid-svg-b2FNGMFO8EEZa19v .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-b2FNGMFO8EEZa19v .cluster-label text{fill:#333;}#mermaid-svg-b2FNGMFO8EEZa19v .cluster-label span{color:#333;}#mermaid-svg-b2FNGMFO8EEZa19v .cluster-label span p{background-color:transparent;}#mermaid-svg-b2FNGMFO8EEZa19v .label text,#mermaid-svg-b2FNGMFO8EEZa19v span{fill:#333;color:#333;}#mermaid-svg-b2FNGMFO8EEZa19v .node rect,#mermaid-svg-b2FNGMFO8EEZa19v .node circle,#mermaid-svg-b2FNGMFO8EEZa19v .node ellipse,#mermaid-svg-b2FNGMFO8EEZa19v .node polygon,#mermaid-svg-b2FNGMFO8EEZa19v .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-b2FNGMFO8EEZa19v .rough-node .label text,#mermaid-svg-b2FNGMFO8EEZa19v .node .label text,#mermaid-svg-b2FNGMFO8EEZa19v .image-shape .label,#mermaid-svg-b2FNGMFO8EEZa19v .icon-shape .label{text-anchor:middle;}#mermaid-svg-b2FNGMFO8EEZa19v .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-b2FNGMFO8EEZa19v .rough-node .label,#mermaid-svg-b2FNGMFO8EEZa19v .node .label,#mermaid-svg-b2FNGMFO8EEZa19v .image-shape .label,#mermaid-svg-b2FNGMFO8EEZa19v .icon-shape .label{text-align:center;}#mermaid-svg-b2FNGMFO8EEZa19v .node.clickable{cursor:pointer;}#mermaid-svg-b2FNGMFO8EEZa19v .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-b2FNGMFO8EEZa19v .arrowheadPath{fill:#333333;}#mermaid-svg-b2FNGMFO8EEZa19v .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-b2FNGMFO8EEZa19v .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-b2FNGMFO8EEZa19v .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-b2FNGMFO8EEZa19v .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-b2FNGMFO8EEZa19v .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-b2FNGMFO8EEZa19v .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-b2FNGMFO8EEZa19v .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-b2FNGMFO8EEZa19v .cluster text{fill:#333;}#mermaid-svg-b2FNGMFO8EEZa19v .cluster span{color:#333;}#mermaid-svg-b2FNGMFO8EEZa19v div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-b2FNGMFO8EEZa19v .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-b2FNGMFO8EEZa19v rect.text{fill:none;stroke-width:0;}#mermaid-svg-b2FNGMFO8EEZa19v .icon-shape,#mermaid-svg-b2FNGMFO8EEZa19v .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-b2FNGMFO8EEZa19v .icon-shape p,#mermaid-svg-b2FNGMFO8EEZa19v .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-b2FNGMFO8EEZa19v .icon-shape .label rect,#mermaid-svg-b2FNGMFO8EEZa19v .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-b2FNGMFO8EEZa19v .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-b2FNGMFO8EEZa19v .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-b2FNGMFO8EEZa19v :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 输入图像
256*192*3
Stem阶段2个步幅卷积
Stage 1高分辨率分支 r=1
64*48*32
Stage 2 分支 r=164*48*32
Stage 2 分支 r=232*24*64
交换单元 1次
Stage 3 分支 r=164*48*32
Stage 3 分支 r=232*24*64
Stage 3 分支 r=416*12*128
交换单元 4次
Stage 4 分支 r=164*48*32
Stage 4 分支 r=232*24*64
Stage 4 分支 r=316*12*128
Stage 4 分支 r=48*6*256
交换单元 3次
输出高分辨率热图64*48*K
-
r为分辨率索引,r=1表示最高分辨率(输入经 Stem 降采样 4 倍后的分辨率),K为关键点数量(如 COCO 数据集为 17)。
-
重复多尺度信息融合策略(交换单元)
-
HRNet 的核心竞争力在于交换单元(Exchange Unit),它通过跨并行分支的持续信息交互,让高低分辨率特征双向赋能:
- 高分辨率分支获得低分辨率分支的强语义信息(提升关键点判别能力);
- 低分辨率分支获得高分辨率分支的精细空间信息(辅助语义特征的定位)。
-
交换单元对不同分辨率分支的特征进行三种变换,再聚合求和:
操作类型 适用场景 具体实现 恒等映射 同分辨率分支 直接传递特征,无变换 下采样 从高分辨率到低分辨率 步幅为 2 的 3×3 卷积(2 倍降采样),多倍降采样通过连续步幅卷积实现 上采样 从低分辨率到高分辨率 最近邻插值上采样 + 1×1 卷积(对齐通道数) -
以 Stage 3 的三个并行分支(r=1、r=2、r=4)为例,交换单元的信息流动如下:
-
#mermaid-svg-yAjAnpsSrS0OTZ0c{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-yAjAnpsSrS0OTZ0c .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yAjAnpsSrS0OTZ0c .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yAjAnpsSrS0OTZ0c .error-icon{fill:#552222;}#mermaid-svg-yAjAnpsSrS0OTZ0c .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-yAjAnpsSrS0OTZ0c .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yAjAnpsSrS0OTZ0c .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yAjAnpsSrS0OTZ0c .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yAjAnpsSrS0OTZ0c .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yAjAnpsSrS0OTZ0c .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yAjAnpsSrS0OTZ0c .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yAjAnpsSrS0OTZ0c .marker{fill:#333333;stroke:#333333;}#mermaid-svg-yAjAnpsSrS0OTZ0c .marker.cross{stroke:#333333;}#mermaid-svg-yAjAnpsSrS0OTZ0c svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yAjAnpsSrS0OTZ0c p{margin:0;}#mermaid-svg-yAjAnpsSrS0OTZ0c .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-yAjAnpsSrS0OTZ0c .cluster-label text{fill:#333;}#mermaid-svg-yAjAnpsSrS0OTZ0c .cluster-label span{color:#333;}#mermaid-svg-yAjAnpsSrS0OTZ0c .cluster-label span p{background-color:transparent;}#mermaid-svg-yAjAnpsSrS0OTZ0c .label text,#mermaid-svg-yAjAnpsSrS0OTZ0c span{fill:#333;color:#333;}#mermaid-svg-yAjAnpsSrS0OTZ0c .node rect,#mermaid-svg-yAjAnpsSrS0OTZ0c .node circle,#mermaid-svg-yAjAnpsSrS0OTZ0c .node ellipse,#mermaid-svg-yAjAnpsSrS0OTZ0c .node polygon,#mermaid-svg-yAjAnpsSrS0OTZ0c .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-yAjAnpsSrS0OTZ0c .rough-node .label text,#mermaid-svg-yAjAnpsSrS0OTZ0c .node .label text,#mermaid-svg-yAjAnpsSrS0OTZ0c .image-shape .label,#mermaid-svg-yAjAnpsSrS0OTZ0c .icon-shape .label{text-anchor:middle;}#mermaid-svg-yAjAnpsSrS0OTZ0c .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-yAjAnpsSrS0OTZ0c .rough-node .label,#mermaid-svg-yAjAnpsSrS0OTZ0c .node .label,#mermaid-svg-yAjAnpsSrS0OTZ0c .image-shape .label,#mermaid-svg-yAjAnpsSrS0OTZ0c .icon-shape .label{text-align:center;}#mermaid-svg-yAjAnpsSrS0OTZ0c .node.clickable{cursor:pointer;}#mermaid-svg-yAjAnpsSrS0OTZ0c .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-yAjAnpsSrS0OTZ0c .arrowheadPath{fill:#333333;}#mermaid-svg-yAjAnpsSrS0OTZ0c .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-yAjAnpsSrS0OTZ0c .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-yAjAnpsSrS0OTZ0c .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yAjAnpsSrS0OTZ0c .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-yAjAnpsSrS0OTZ0c .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yAjAnpsSrS0OTZ0c .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-yAjAnpsSrS0OTZ0c .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-yAjAnpsSrS0OTZ0c .cluster text{fill:#333;}#mermaid-svg-yAjAnpsSrS0OTZ0c .cluster span{color:#333;}#mermaid-svg-yAjAnpsSrS0OTZ0c div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-yAjAnpsSrS0OTZ0c .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-yAjAnpsSrS0OTZ0c rect.text{fill:none;stroke-width:0;}#mermaid-svg-yAjAnpsSrS0OTZ0c .icon-shape,#mermaid-svg-yAjAnpsSrS0OTZ0c .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yAjAnpsSrS0OTZ0c .icon-shape p,#mermaid-svg-yAjAnpsSrS0OTZ0c .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-yAjAnpsSrS0OTZ0c .icon-shape .label rect,#mermaid-svg-yAjAnpsSrS0OTZ0c .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yAjAnpsSrS0OTZ0c .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-yAjAnpsSrS0OTZ0c .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-yAjAnpsSrS0OTZ0c :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 恒等
步幅3*3卷积*1下采样2倍
步幅3*3卷积*2下采样4倍
最近邻上采样+1*1上采样2倍
恒等
步幅3*3卷积*1下采样2倍
最近邻上采样+1*1上采样4倍
最近邻上采样+1*1上采样2倍
恒等
求和
求和
求和
求和
求和
求和
分支 r=164*48*32
聚合 r=1 输出
分支 r=2 输入
分支 r=4 输入
分支 r=232*24*64
分支 r=1 输入
聚合 r=2 输出
分支 r=4 输入
分支 r=416*12*128
分支 r=1 输入
分支 r=2 输入
聚合 r=4 输出 -
设交换单元的输入为 s 个分辨率的特征图 {X1,X2,...,Xs}(Xi 对应第 i 个分辨率),输出为 {Y1,Y2,...,Ys},则每个输出特征图的计算为:
-
Y k = ∑ i = 1 s a ( X i , k ) Y_k=∑_{i=1}^sa(X_i,k) Yk=i=1∑sa(Xi,k)
-
其中 a(Xi,k) 表示将 Xi 从分辨率 i 变换到分辨率 k 的操作(恒等 / 下采样 / 上采样)。
-
-
以 HRNet-W32 (高分辨率分支通道数 C=32)、输入图像 256×192×3(H×W×C)为例,详细拆解从输入到输出的维度变换。
阶段 核心操作 输出分辨率(H×W) 输出通道数 说明 输入 - 256×192 3 原始图像 Stem 2 个步幅 2 的 3×3 卷积 64×48 256 连续 2 次降采样,提取初始特征 Stage 1 4 个 Bottleneck + 1 个 3×3 卷积 64×48 32 高分辨率基础特征提取,通道降为 32 Stage 2 新增 r=2 分支 + 1 个交换块 64×48(r=1)32×24(r=2) 32(r=1)64(r=2) 首次多尺度融合 Stage 3 新增 r=4 分支 + 4 个交换块 64×48(r=1)32×24(r=2)16×12(r=4) 32(r=1)64(r=2)128(r=4) 持续多尺度融合,丰富语义 Stage 4 新增 r=8 分支 + 3 个交换块 64×48(r=1)32×24(r=2)16×12(r=4)8×6(r=8) 32(r=1)64(r=2)128(r=4)256(r=8) 最终多尺度融合,输出高分辨率特征 输出 1×1 卷积(回归热图) 64×48 K(如 17) 预测关键点热图 -
(1)Stem 阶段,快速降采样,提取低层次视觉特征(如边缘、角点)。通过连续两个步幅卷积,将输入分辨率压缩 4 倍,减少后续计算量,同时保留基础的空间结构信息。
- 输入:256×192×3
- 第一个卷积:3×3,步幅 = 2,输出通道 = 64 → 输出:128×96×64(H/2, W/2)
- 第二个卷积:3×3,步幅 = 2,输出通道 = 256 → 输出:64×48×256(H/4, W/4)
-
(2)Stage 1,构建高分辨率基础表征。利用 4 个 Bottleneck 单元(借鉴 ResNet-50 的设计),在高分辨率下提取丰富的局部特征,为后续多尺度融合奠定 "空间精准" 的基础。
- 输入:64×48×256
- 4 个 Bottleneck 单元:每个 Bottleneck 保持通道数 256 不变 → 输出:64×48×256
- 3×3 卷积:输出通道 = 32(HRNet-W32 的 C 值)→ 输出:64×48×32(第一个并行分支 r=1)
-
(3)Stage 2
- 新增分支 r=2:通过步幅 = 2 的 3×3 卷积对 r=1 分支降采样,分辨率减半(32×24),通道翻倍(64)→ 分支 r=2:32×24×64
- 交换块:包含 4 个 Residual Unit(每个分支内的特征提取)和 1 个交换单元 → 输出两个分支维度不变:64×48×32(r=1)、32×24×64(r=2)
-
(4)Stage 4 输出
- 取最高分辨率分支 r=1 的输出:64×48×32
- 1×1 卷积回归热图:输出通道数为关键点数量 K(如 COCO 为 17)→ 最终输出:64×48×17
-
在每个阶段内持续进行多尺度信息交互。单次融合无法充分挖掘高低分辨率特征的互补性,重复融合(标准 HRNet 共 8 次)能让特征持续优化,最终生成高质量的关键点热图。
-
-
HRNet 将人体姿态估计转化为关键点热图回归任务,通过均方误差(MSE)衡量预测热图与 Ground Truth 热图的差异。输入:图像 I(大小 W×H×3),输出:K 个关键点热图 {H1,H2,...,HK}(大小 W′×H′,W′=W/4,H′=H/4),每个热图 Hk 表示第 k 个关键点的位置置信度。
-
Ground Truth 高斯热图生成,对每个关键点的 Ground Truth 坐标 (xk,yk),生成一个以该坐标为中心的 2D 高斯热图: H k g t ( x , y ) = e x p ( − ( x − x k ) 2 + ( y − y k ) 2 2 σ 2 ) H_k^{gt}(x,y)=exp(−\frac{(x−x_k)^2+(y−y_k)^2}{2σ^2}) Hkgt(x,y)=exp(−2σ2(x−xk)2+(y−yk)2)。σ:高斯核标准差,论文中设为 1 像素;热图大小:与预测热图一致(W′×H′),若关键点坐标超出热图范围则截断。损失函数为所有关键点、所有样本的预测热图与 GT 热图的均方误差平均:
-
L = 1 N × K ∑ i = 1 N ∑ k = 1 K ∥ H k p r e d ( i ) − H k g t ( i ) ∥ 2 2 L=\frac1{N×K}∑{i=1}^N∑{k=1}^K∥H_k^{pred(i)}−H_k^{gt(i)}∥_2^2 L=N×K1i=1∑Nk=1∑K∥Hkpred(i)−Hkgt(i)∥22
-
N:批次样本数;K:关键点数量;Hkpred(i):第 i 个样本第 k 个关键点的预测热图;Hkgt(i):第 i 个样本第 k 个关键点的 GT 热图。
-
-
推理时,对预测热图进行两个后处理步骤得到最终关键点坐标:取热图中响应值最高的位置作为初始坐标;向第二高响应值的方向偏移 1/4 像素,提升亚像素级定位精度。
代码核心模块解读
python
deep-high-resolution-net.pytorch/
├── lib/ # 核心算法层(模型、损失、工具)
│ ├── models/ # 模型定义(PoseResNet基线 + HRNet核心)
│ ├── config/ # 配置解析(数据集/模型/训练超参)
│ ├── core/ # 损失函数、训练/推理逻辑
│ ├── dataset/ # COCO/MPII数据集加载与预处理
│ └── utils/ # 仿射变换、日志、模型保存等工具
├── tools/ # 工程脚本(训练/测试)
│ ├── train.py # 训练主程序
│ └── test.py # 测试主程序
├── demo/ # 推理演示(图片/视频)
│ ├── inference.py # 视频姿态估计核心逻辑
│ └── demo.py # 快速推理演示
├── experiments/ # 实验配置文件(COCO/MPII)
└── README.md # 环境、数据、实验结果说明
-
PoseResNet 实现(
lib/models/pose_resnet.py),基于 ResNet 的 Bottleneck/BasicBlock 构建下采样分支,输出 1/32 分辨率的特征;上采样层通过_make_deconv_layer构建反卷积层,将低分辨率特征上采样到输入尺寸的 1/4(如 256x192 输入→64x48 特征→256x192 热力图);输出层final_layer将上采样后的特征映射为关键点热力图(COCO 为 17 个关键点,MPII 为 16 个)。 -
推理流程(
demo/inference.py),"多人体姿态估计" 的两阶段范式:- 阶段 1:人体检测(Faster R-CNN)→ 输出人体框(
get_person_detection_boxes); - 阶段 2:框转换(
box_to_center_scale)→ 仿射变换裁剪人体区域 → HRNet 输出热力图 →get_final_preds将热力图峰值转换为关键点坐标;
- 阶段 1:人体检测(Faster R-CNN)→ 输出人体框(
-
低分辨率分支学习到的全局语义信息(如人体整体姿态、遮挡关系)通过双向融合传递到高分辨率分支,解决了传统高分辨率特征 "语义信息不足" 的问题。从
pose_resnet.py的_make_deconv_layer可知,PoseResNet 的反卷积是单向上采样:- 反卷积本质是 "插值(如转置卷积的零填充)+ 卷积",无法恢复降采样(maxpool / 步长 2 卷积)丢失的空间细节,代码中
num_deconv_layers=3(3 层反卷积)仅能 "补全" 尺寸,无法还原像素级的关节位置; - 反卷积的
kernel=4/padding=1设计导致特征图存在 "棋盘效应",进一步降低定位精度; - 反卷积层仅从低分辨率特征单向传递信息,无高分辨率特征向低分辨率的反馈,语义信息与空间信息割裂,鲁棒性差。
- 反卷积本质是 "插值(如转置卷积的零填充)+ 卷积",无法恢复降采样(maxpool / 步长 2 卷积)丢失的空间细节,代码中
-
密集预测任务(语义分割、目标检测、姿态估计)的核心需求是 "高分辨率的空间细节"+"低分辨率的语义信息",HRNet 的设计完美匹配:
- 多分辨率并行:语义分割需要像素级分割(高分辨率),目标检测需要全局语义(低分辨率),HRNet 的并行分支可同时输出不同分辨率特征,无需额外的特征金字塔(FPN);
- 重复多尺度融合:分割 / 检测任务中,不同尺度的目标(如小目标检测、细粒度分割)需要跨尺度信息,HRNet 的双向融合让每个尺度特征都融合了其他尺度的信息,提升对多尺度目标的鲁棒性;
- 无分辨率损失:传统方法(如 ResNet+FPN)先降分辨率再上采样,HRNet 全程维护高分辨率,对密集预测的 "像素级精准度" 至关重要。论文中 HRNet 已迁移到语义分割 / 目标检测,核心就是这一设计的通用性。
-
JointsMSELoss(热力图 MSE 损失)的优缺点
- 核心假设 "关键点对应热力图峰值" 贴合姿态估计的监督逻辑,代码中
TARGET_TYPE: 'gaussian'(高斯热力图)让损失对关键点周围像素的监督更平滑,训练更稳定;易与卷积网络结合,梯度回传更顺畅,是姿态估计的工业界主流选择。 - 监督粒度是 "热力图像素" 而非 "关键点坐标",存在像素级误差累积;对高斯核的 σ 参数敏感(代码中 COCO 256x192 输入 σ=2,384x288 输入 σ=3),σ 过小易过拟合,过大则监督模糊;计算量高:需生成与特征图尺寸一致的热力图(如 64x48),再逐像素计算 MSE。
- 核心假设 "关键点对应热力图峰值" 贴合姿态估计的监督逻辑,代码中
-
可以在
lib/core/loss.py中新增JointsL1Loss类,在tools/train.py中替换损失函数:将criterion = JointsMSELoss(...)改为criterion = JointsL1Loss(...):pythonclass JointsL1Loss(nn.Module): def __init__(self, use_target_weight=True): super(JointsL1Loss, self).__init__() self.criterion = nn.L1Loss(reduction='mean') self.use_target_weight = use_target_weight def forward(self, output, target, target_weight): batch_size = output.size(0) num_joints = output.size(1) heatmaps_pred = output.reshape((batch_size, num_joints, -1)).split(1, 1) heatmaps_gt = target.reshape((batch_size, num_joints, -1)).split(1, 1) loss = 0.0 for idx in range(num_joints): pred = heatmaps_pred[idx].squeeze() gt = heatmaps_gt[idx].squeeze() if self.use_target_weight: loss += 0.5 * self.criterion(pred * target_weight[:, idx], gt * target_weight[:, idx]) else: loss += 0.5 * self.criterion(pred, gt) return loss / num_joints -
get_affine_transform的核心作用是:将检测框内的人体做尺度归一化 + 对齐,适配模型输入尺寸(如 256x192)。去掉后会出现以下问题:- 不同图片中人体大小差异极大(如远距离小人 vs 近距离大人),直接输入整图会导致模型无法学习到统一的关键点特征。
- 整图中人体仅占小部分区域,模型注意力被背景干扰,关键点热力图的峰值易被背景噪声覆盖;
- 无仿射变换的旋转 / 裁剪,倾斜、弯曲的人体无法对齐到模型的 "标准人体" 特征空间,关节点定位错误率飙升;
inference.py中get_final_preds依赖仿射变换的逆变换将热力图坐标映射回原图,去掉后坐标映射完全错误,输出的关键点位置偏离实际。
-
flip test 的实现逻辑
- 推理时,对输入图片生成 "原图 + 水平翻转图" 两个版本;
- 分别将两张图输入模型,得到两组热力图;
- 将翻转图的热力图做水平翻转 + 关键点对称映射(如左手腕↔右手腕);
- 对两组热力图取平均,作为最终的热力图;
- 从平均后的热力图提取峰值,作为关键点坐标。
- 解决 "左右不对称" 的特征偏差:模型训练时虽有翻转数据增强,但推理时单张图仍可能因视角 / 遮挡导致单侧关键点预测错误,flip test 通过对称融合弥补;降低热力图的噪声:两张图的热力图平均后,背景噪声被抵消,关键点峰值更突出。
-
HRNet 多分辨率分支的维度对齐方式
- 高分辨率→低分辨率:通过步长 2 的 3x3 卷积下采样,将高分辨率特征(如 1/1)的尺寸降到低分辨率分支(如 1/2)的尺寸;
- 低分辨率→高分辨率:通过 "双线性插值 + 1x1 卷积" 上采样,将低分辨率特征的尺寸升到高分辨率分支的尺寸,插值保证尺寸匹配,卷积保证通道数匹配。
- 融合时,通过 1x1 卷积调整通道数:如高分辨率分支(32 通道)下采样后,用 1x1 卷积将通道数升到 64,匹配 1/2 分支的通道数;低分辨率分支(64 通道)上采样后,用 1x1 卷积将通道数降到 32,匹配 1/1 分支的通道数。
- 每个融合阶段前,通过
_align_dimension类函数(推测)检查各分支的尺寸 / 通道数,确保可逐元素相加;卷积 / 插值的步长、padding 严格计算,避免尺寸偏差(如_get_deconv_cfg函数的逻辑复用)。
-
HRNet(自上而下)vs DEKR(自下而上)的优劣势
维度 HRNet(自上而下) DEKR(自下而上) 核心流程 先检测人体框 → 单人体姿态估计 先检测所有关键点 → 分组为人体 优势 精度高、鲁棒性强 速度快(30+FPS)、适合多人场景 局限 速度慢(5~10FPS)、依赖检测框精度 精度低(AP0.72 左右)、小人体关键点易漏检 适用场景 高精度需求(如医疗、姿态分析) 实时需求(如视频监控、直播) -
数据处理:HRNet
lib/dataset/coco.py加载 "人体框 + 单人体关键点",训练时针对单人体裁剪;DEKR 则是加载 "整图 + 所有人关键点",训练时无需裁剪,直接处理整图。 -
推理流程:HRNet(
demo/inference.py),用 Faster R-CNN 检测人体框;对每个框做仿射变换;单框输入模型,输出单人体关键点。 而 DEKR 整图输入模型,输出所有关键点的热力图 + 偏移量;对关键点聚类,分组为不同人体;无检测框依赖,直接输出多人关键点。HRNet 输出 "人体框 ID + 关键点坐标",按框分组;DEKR 输出 "关键点坐标 + 人体 ID",按关键点分组。
-
PoseHighResolutionNet(并行多分辨率 + 特征融合)
-
HRNet 的核心创新是全程保持高分辨率特征,并行多分辨率分支,跨分支特征融合,区别于传统 "高分辨率→低分辨率→上采样恢复" 的范式。
pythonclass PoseHighResolutionNet(nn.Module): def __init__(self, cfg, **kwargs): self.inplanes = 64 # 初始通道数 extra = cfg['MODEL']['EXTRA'] # 配置参数(多分支/融合策略) super(PoseHighResolutionNet, self).__init__() # 1. Stem层:下采样到1/4分辨率(输入256x256 → 64x64) # 输入:[B, 3, H, W] (例:[B,3,256,256]) self.conv1 = nn.Conv2d(3, 64, kernel_size=3, stride=2, padding=1, bias=False) # [B,64,128,128] self.bn1 = nn.BatchNorm2d(64, momentum=BN_MOMENTUM) self.conv2 = nn.Conv2d(64, 64, kernel_size=3, stride=2, padding=1, bias=False) # [B,64,64,64] self.bn2 = nn.BatchNorm2d(64, momentum=BN_MOMENTUM) self.relu = nn.ReLU(inplace=True) self.layer1 = self._make_layer(Bottleneck, 64, 4) # 残差块,输出[B,256,64,64](64*4=256) # 2. Stage2:初始化多分辨率分支(HRNet核心起点) self.stage2_cfg = extra['STAGE2'] num_channels = self.stage2_cfg['NUM_CHANNELS'] # 例:[32, 64] → 2个分支(高/低分辨率) block = blocks_dict[self.stage2_cfg['BLOCK']] # 分支内的基础块(BasicBlock/Bottleneck) num_channels = [num_channels[i] * block.expansion for i in range(len(num_channels))] # 通道数扩展 # 过渡层:将layer1的单分支特征 → Stage2的多分支特征(维度匹配) self.transition1 = self._make_transition_layer([256], num_channels) # Stage2:多分支并行 + 跨分支特征融合 → 输出多分辨率特征列表 self.stage2, pre_stage_channels = self._make_stage(self.stage2_cfg, num_channels) # 3. Stage3/Stage4:逐步增加低分辨率分支,保持高分辨率分支 # Stage3:在Stage2基础上新增更低分辨率分支(例:3个分支) self.stage3_cfg = extra['STAGE3'] num_channels = self.stage3_cfg['NUM_CHANNELS'] block = blocks_dict[self.stage3_cfg['BLOCK']] num_channels = [num_channels[i] * block.expansion for i in range(len(num_channels))] self.transition2 = self._make_transition_layer(pre_stage_channels, num_channels) self.stage3, pre_stage_channels = self._make_stage(self.stage3_cfg, num_channels) # Stage4:最终分支数(例:4个分支),仅保留高分辨率输出用于预测 self.stage4_cfg = extra['STAGE4'] num_channels = self.stage4_cfg['NUM_CHANNELS'] block = blocks_dict[self.stage4_cfg['BLOCK']] num_channels = [num_channels[i] * block.expansion for i in range(len(num_channels))] self.transition3 = self._make_transition_layer(pre_stage_channels, num_channels) self.stage4, pre_stage_channels = self._make_stage(self.stage4_cfg, num_channels, multi_scale_output=False) # 4. 最终预测层:高分辨率特征→热力图(17个关节点) self.final_layer = nn.Conv2d( in_channels=pre_stage_channels[0], # 高分辨率分支通道数 out_channels=cfg['MODEL']['NUM_JOINTS'], # COCO 17个关节点 kernel_size=extra['FINAL_CONV_KERNEL'], # 3x3 stride=1, padding=1 if extra['FINAL_CONV_KERNEL'] == 3 else 0 ) -
多分支过渡层(_make_transition_layer), 将上一阶段的多分辨率特征,映射到当前阶段的多分支通道 / 分辨率,实现分支扩展(逐步增加低分辨率分支):
pythondef _make_transition_layer(self, num_channels_pre_layer, num_channels_cur_layer): num_branches_cur = len(num_channels_cur_layer) # 当前分支数(Stage2=2, Stage3=3, Stage4=4) num_branches_pre = len(num_channels_pre_layer) # 上一阶段分支数 transition_layers = [] for i in range(num_branches_cur): # 情况1:当前分支索引 < 上一阶段分支数 → 通道匹配(分辨率不变) if i < num_branches_pre: if num_channels_cur_layer[i] != num_channels_pre_layer[i]: # 通道数不一致时:1x1卷积调整通道 transition_layers.append( nn.Sequential( nn.Conv2d(num_channels_pre_layer[i], num_channels_cur_layer[i], 3, 1, 1, bias=False), nn.BatchNorm2d(num_channels_cur_layer[i]), nn.ReLU(inplace=True) ) ) else: transition_layers.append(None) # 通道一致,无需变换 # 情况2:当前分支索引 ≥ 上一阶段分支数 → 新增低分辨率分支(下采样) else: conv3x3s = [] # 逐步下采样(每次stride=2),直到匹配目标分辨率 for j in range(i+1-num_branches_pre): inchannels = num_channels_pre_layer[-1] # 基于上一阶段最低分辨率分支 outchannels = num_channels_cur_layer[i] if j == i-num_branches_pre else inchannels conv3x3s.append( nn.Sequential( nn.Conv2d(inchannels, outchannels, 3, 2, 1, bias=False), # stride=2下采样 nn.BatchNorm2d(outchannels), nn.ReLU(inplace=True) ) ) transition_layers.append(nn.Sequential(*conv3x3s)) # 新增低分辨率分支 return nn.ModuleList(transition_layers) -
多分辨率特征融合(HighResolutionModule),
_make_stage中调用HighResolutionModule,实现跨分支特征融合:- 每个分支内:残差块堆叠,保持自身分辨率;
- 跨分支:高分辨率分支→下采样→融合到低分辨率分支;低分辨率分支→上采样→融合到高分辨率分支;
- 效果:各分支共享多尺度信息,既保留高分辨率的细节,又有低分辨率的语义。
-
前向传播(维度变化示例),以输入
[B,3,256,256]为例:pythondef forward(self, x): # Stem层:[B,3,256,256] → [B,256,64,64] x = self.conv1(x) # [B,64,128,128] x = self.bn1(x) x = self.relu(x) x = self.conv2(x) # [B,64,64,64] x = self.bn2(x) x = self.relu(x) x = self.layer1(x) # [B,256,64,64] # Stage2:单分支→2分支(64x64 + 32x32) x_list = [] for i in range(self.stage2_cfg['NUM_BRANCHES']): if self.transition1[i] is not None: x_list.append(self.transition1[i](x)) else: x_list.append(x) y_list = self.stage2(x_list) # [B, C1,64,64], [B,C2,32,32] # Stage3:2分支→3分支(64x64 + 32x32 + 16x16) x_list = [] for i in range(self.stage3_cfg['NUM_BRANCHES']): if self.transition2[i] is not None: x_list.append(self.transition2[i](y_list[-1])) else: x_list.append(y_list[i]) y_list = self.stage3(x_list) # [B,C1,64,64], [B,C2,32,32], [B,C3,16,16] # Stage4:3分支→4分支,仅保留高分辨率输出 x_list = [] for i in range(self.stage4_cfg['NUM_BRANCHES']): if self.transition3[i] is not None: x_list.append(self.transition3[i](y_list[-1])) else: x_list.append(y_list[i]) y_list = self.stage4(x_list) # [B,C1,64,64](仅保留高分辨率分支) # 最终预测:高分辨率特征→热力图 [B,17,64,64] x = self.final_layer(y_list[0]) return x -
HRNet 中所有坐标映射(原图→裁剪图、热力图→原图)都基于仿射变换 ,核心是
get_affine_transform和transform_preds。get_affine_transform:生成仿射变换矩阵,计算 "原图坐标→裁剪后坐标" 或 "裁剪后坐标→原图坐标" 的仿射变换矩阵(3x2),支持旋转、缩放、平移。pythondef get_affine_transform( center, # 裁剪中心 [2,] (x,y) scale, # 裁剪尺度 [2,] (w_scale, h_scale) rot, # 旋转角度(°) output_size, # 裁剪后尺寸 [2,] (w,h) shift=np.array([0, 0], dtype=np.float32), # 平移偏移 inv=0 # 0:原图→裁剪图;1:裁剪图→原图(逆变换) ): # 1. 尺度标准化:scale→实际像素宽度(基于200像素基准) if not isinstance(scale, np.ndarray) and not isinstance(scale, list): scale = np.array([scale, scale]) scale_tmp = scale * 200.0 # 例:scale=[1.0,1.0] → scale_tmp=[200,200] src_w = scale_tmp[0] # 原图中裁剪框的宽度 dst_w = output_size[0] # 裁剪后宽度 dst_h = output_size[1] # 裁剪后高度 # 2. 旋转角度→弧度,计算旋转后的方向向量 rot_rad = np.pi * rot / 180 src_dir = get_dir([0, src_w * -0.5], rot_rad) # 原图中裁剪框的y轴方向 [2,] dst_dir = np.array([0, dst_w * -0.5], np.float32) # 裁剪后y轴方向 [2,] # 3. 定义仿射变换的3个控制点(原图src ↔ 裁剪后dst) src = np.zeros((3, 2), dtype=np.float32) # 原图3个点 [3,2] dst = np.zeros((3, 2), dtype=np.float32) # 裁剪后3个点 [3,2] # 控制点1:裁剪中心 src[0, :] = center + scale_tmp * shift # 原图中心 + 偏移 dst[0, :] = [dst_w * 0.5, dst_h * 0.5] # 裁剪后中心 # 控制点2:中心+y轴方向(旋转后) src[1, :] = center + src_dir + scale_tmp * shift dst[1, :] = np.array([dst_w * 0.5, dst_h * 0.5]) + dst_dir # 控制点3:垂直于前两个点(保证仿射变换的正交性) src[2:, :] = get_3rd_point(src[0, :], src[1, :]) dst[2:, :] = get_3rd_point(dst[0, :], dst[1, :]) # 4. 生成仿射变换矩阵(OpenCV接口) if inv: trans = cv2.getAffineTransform(np.float32(dst), np.float32(src)) # 逆变换:裁剪→原图 else: trans = cv2.getAffineTransform(np.float32(src), np.float32(dst)) # 正变换:原图→裁剪 return trans # 返回3x2变换矩阵 -
affine_transform:单坐标点的仿射变换。将单个坐标点(x,y)通过变换矩阵映射到目标空间:
pythondef affine_transform(pt, t): # pt:输入点 [2,] (x,y);t:3x2变换矩阵 new_pt = np.array([pt[0], pt[1], 1.]).T # 齐次坐标 [3,] new_pt = np.dot(t, new_pt) # 矩阵乘法:3x2 × 3x1 → 2x1 return new_pt[:2] # 输出映射后的点 [2,] -
transform_preds:批量坐标的仿射逆变换。将热力图上的关键点坐标(裁剪后空间)映射回原图空间,是推理阶段的核心映射函数(热力图坐标:
[17,2](17 个关节点)→ 经过transform_preds→ 原图坐标[17,2]):pythondef transform_preds( coords, # 热力图提取的坐标 [num_joints, 2] (x,y) center, # 裁剪中心 [2,] scale, # 裁剪尺度 [2,] output_size # 裁剪后尺寸 [2,] ): target_coords = np.zeros(coords.shape) # 初始化输出 [num_joints,2] # 生成逆变换矩阵(裁剪→原图) trans = get_affine_transform(center, scale, 0, output_size, inv=1) # 逐关节点映射 for p in range(coords.shape[0]): target_coords[p, 0:2] = affine_transform(coords[p, 0:2], trans) return target_coords # 原图空间坐标 [num_joints,2] -
推理核心函数:get_final_preds(inference.py)。从模型输出的热力图中提取关键点坐标,经亚像素优化后映射回原图,是推理的最后一步:
pythondef get_final_preds(config, batch_heatmaps, center, scale): # 输入: # batch_heatmaps:模型输出热力图 [B, num_joints, H, W](例:[1,17,64,64]) # center:裁剪中心 [B,2] # scale:裁剪尺度 [B,2] # 1. 从热力图中提取峰值坐标(整数像素) coords, maxvals = get_max_preds(batch_heatmaps) # coords: [B,17,2], maxvals: [B,17,1] heatmap_height = batch_heatmaps.shape[2] # 热力图高度(64) heatmap_width = batch_heatmaps.shape[3] # 热力图宽度(64) # 2. 后处理:亚像素优化(提升坐标精度) if config.TEST.POST_PROCESS: for n in range(coords.shape[0]): # 遍历批次(B) for p in range(coords.shape[1]): # 遍历关节点(17) hm = batch_heatmaps[n][p] # 单个关节点的热力图 [64,64] # 峰值坐标四舍五入为整数 px = int(math.floor(coords[n][p][0] + 0.5)) py = int(math.floor(coords[n][p][1] + 0.5)) # 确保坐标在热力图内部(避免越界) if 1 < px < heatmap_width-1 and 1 < py < heatmap_height-1: # 计算峰值邻域的梯度(x/y方向) diff = np.array( [ hm[py][px+1] - hm[py][px-1], # x方向梯度(右-左) hm[py+1][px]-hm[py-1][px] # y方向梯度(下-上) ] ) # 亚像素偏移:梯度符号×0.25(将整数坐标→亚像素坐标) coords[n][p] += np.sign(diff) * .25 # 3. 坐标拷贝(避免原数组修改) preds = coords.copy() # [B,17,2] # 4. 映射回原图空间(逐样本) for i in range(coords.shape[0]): preds[i] = transform_preds( coords[i], # 当前样本的关节点坐标 [17,2] center[i], # 当前样本的裁剪中心 [2,] scale[i], # 当前样本的裁剪尺度 [2,] [heatmap_width, heatmap_height] # 热力图尺寸(裁剪后尺寸) ) # 输出: # preds:原图空间的关节点坐标 [B,17,2] # maxvals:各关节点的热力图峰值概率 [B,17,1] return preds, maxvals- 亚像素优化 :通过热力图邻域梯度,将整数像素坐标细化到 0.25 像素精度,提升关键点定位精度;
get_affine_transform(原图→裁剪)→ 模型推理→transform_preds(裁剪→原图),保证坐标空间一致;
- 亚像素优化 :通过热力图邻域梯度,将整数像素坐标细化到 0.25 像素精度,提升关键点定位精度;
数据增强设计(transforms.py)
-
HRNet 的核心数据增强是水平翻转 (训练增强 + 测试翻转融合),对应
flip_back和fliplr_joints。fliplr_joints:关节点水平翻转,训练时翻转图片后,同步翻转关节点坐标,并交换左右对称关节点(如左眼↔右眼):pythondef fliplr_joints(joints, joints_vis, width, matched_parts): # 输入: # joints:关节点坐标 [num_joints,2] # joints_vis:关节点可见性 [num_joints,1] # width:图片宽度(用于水平翻转计算) # matched_parts:对称关节点对(例:[(1,2),(3,4),...] 左眼↔右眼) # 1. 水平翻转坐标:x = width - x - 1 joints[:, 0] = width - joints[:, 0] - 1 # 2. 交换对称关节点(左右互换) for pair in matched_parts: joints[pair[0], :], joints[pair[1], :] = joints[pair[1], :], joints[pair[0], :].copy() joints_vis[pair[0], :], joints_vis[pair[1], :] = joints_vis[pair[1], :], joints_vis[pair[0], :].copy() return joints*joints_vis, joints_vis # 可见性掩码后返回 -
flip_back:热力图翻转融合(测试阶段),测试时对图片翻转推理,将翻转后的热力图还原,与原图推理结果融合,提升精度(Flip Test):
pythondef flip_back(output_flipped, matched_parts): # 输入:output_flipped 翻转后的热力图 [B, num_joints, H, W] assert output_flipped.ndim == 4,\ 'output_flipped should be [batch_size, num_joints, height, width]' # 1. 水平翻转热力图(还原x轴) output_flipped = output_flipped[:, :, :, ::-1] # 2. 交换对称关节点的热力图 for pair in matched_parts: tmp = output_flipped[:, pair[0], :, :].copy() output_flipped[:, pair[0], :, :] = output_flipped[:, pair[1], :, :] output_flipped[:, pair[1], :, :] = tmp return output_flipped # 还原后的热力图 [B,17,H,W]