做姿态估计的人,应该都绕不开“堆叠沙漏网络”(Stacked Hourglass Networks,SHN)这一经典结构。它发表于2016年的欧洲计算机视觉会议(ECCV),论文标题是Stacked Hourglass Networks for Human Pose Estimation,到现在依然是人体关键点检测领域绕不开的参考基准之一。这篇笔记不是简单的翻译,而是我结合复现经验,把论文的核心思想、模块设计、训练细节和踩坑点重新拆了一遍,希望给准备入门口态估计或者想回头补经典的同学一份可以直接上手的参考。
人体姿态估计,简单说就是给定一张单人图像,标记出肩膀、手肘、手腕等关键点的位置。2016年之前的深度方法大多数直接用卷积网络回归坐标,但坐标回归难以处理肢体遮挡、自相似、多尺度变化等复杂情况。堆叠沙漏网络的核心思路是:用多个上下对称的沙漏状结构反复处理图像,让网络能在局部证据和全局空间关系之间来回交换信息,最终输出每个关键点的热图。这篇文章适合刚接触姿态估计的学生、想复现经典模型的工程师,以及计划把Hourglass网络当作下游任务骨干的开发者。
1. 核心设计思想:为什么姿态估计需要“堆叠”和“沙漏”
1.1 姿态估计本身到底难在哪里
如果你只看过演示视频,可能会觉得姿态估计很简单:一个人站在画面里,标几个点不就行了?但真正把模型应用到真实场景后会发现,问题其实非常集中。
第一是局部特征不足。单独看一小块图像,人的手肘和膝盖可能长得非常像,光线不好时甚至和背景纹理也没明显区别。网络不能只盯着“这个位置的像素像不像肘部”,而必须参考整条手臂的走向、躯干的方向、邻接关节的位置,才能把正确的关键点指派到正确的位置。比如侧身站立的人,一只手肘被躯干遮挡,视觉上根本没有肘部信息,模型只能靠另一只手的位置、肩膀角度、腰部弯曲程度去推断。
第二是尺度变化。同一颗摄像头里,人走近和走远,关节尺寸相差很大;如果是俯拍、仰拍,各部位之间的相对比例也不一样。如果网络只用固定分辨率的卷积核,难以兼顾大尺度和小尺度上的关键点。
第三是遮挡和自相似。手被身体挡住时,正面看不到手,只能靠上下文猜测;左右手外观几乎一模一样,如果不利用“右手在画面哪一侧”“躯干朝向”这类全局信息,模型就很容易左右互换。
所以,姿态估计不能用一个固定感受野粗暴处理。局部感受野太小,看不到上下文;太大,又丢了细节。2016年前后的很多方法试图直接用全连接层回归每个关节的x、y坐标,相当于把空间信息压成一组数,彻底丢失了二维空间结构。堆叠沙漏网络正是在这个困境下出现的一种新思路:先在空间上压缩特征以建立全局理解,再逐级恢复分辨率以保留精确定位,并且把这个过程重复多次。
1.2 “沙漏”是如何同时兼顾局部细节和全局语义的
单个Hourglass模块的结构,从名字上就能看出来:先逐级下采样到很低的尺寸,再逐级上采样回输入分辨率,中间宽、两端窄,确实像沙漏。这个过程有点像人看一幅画的顺序:先眯起眼睛看大概构图,再睁开眼睛看细节。
下采样阶段,网络通过卷积和池化不断减小特征图的空间尺寸,同时增加通道数,感受野迅速扩大,能够把“人与背景的分离”“躯干朝向”“左右对称关系”这类全局线索编码进深层特征。此时特征的空间信息虽然粗化了,但语义信息非常足。
如果到此为止,那和普通的分类网络没有本质区别。关键是上采样阶段。Hourglass模块要恢复回原分辨率,而仅靠高维语义特征是无法把每个像素准确定位出来的。为此,作者把下采样过程中各层相同分辨率的特征保存下来,通过跳跃连接直接加到上采样的对应层上。这样,上采样层既能获得来自深层的高层语义,又能保留来自浅层的空间细节。这种设计,相当于在每个尺度上都在做“局部+全局”的信息融合,而不是只在最后阶段突然混合。
单看一个Hourglass模块,它已经完成了“由细到粗,再由粗到细”的一次完整推理。但作者发现,仅仅做一次还不够。有时第一遍推理会给出一个粗糙但不精准的姿态,需要结合这个粗糙姿态重新修正。于是就有了堆叠结构。
1.3 堆叠的阶段式监督解决了什么
堆叠多个Hourglass模块,就是把前一个模块预测的关键点热图作为下一阶段的提示信息。网络不是一锤子买卖,而是像医生看CT片一样,看第一眼有了初步判断,然后拿着笔在片子上描一描,再看第二眼、第三眼,越看越确定。
具体做法是:每个Hourglass模块的尾部会输出一份预测热图,将这份热图和原特征一起送给下一个Hourglass模块。为了把热图变成与特征匹配的格式,作者用了一个1x1卷积把热图映射到特征通道维度,然后直接与原特征逐元素相加。这个操作很朴素,却非常有效:下一阶段的网络知道上一阶段在哪里“猜”了什么,就可以针对这些不确定性做修正。
还有一个容易忽略的技术点:中间监督。因为堆叠后网络很深,如果只在最后计算损失,前面模块的梯度会被层层稀释,很难训练。作者的做法是在每个Hourglass模块后都计算一次损失,也就是每个阶段都有监督信号。这样一来,梯度可以通过多条短路径回传,网络在早期阶段也能学到有意义的特征。实际运行时,所有阶段的损失加起来,反向传播更新整个网络。
2. 网络结构细节与关键模块解析
2.1 输入输出与热图表示
先明确输入输出。论文的标准设置是输入一张裁剪好的人体图像,尺寸固定为256x256,通道数为3(RGB)。网络输出不是坐标,而是K张大小为64x64的热图,K等于要预测的关键点数量。每一张热图对应一个关键点,热图上的值表示该关键点落在对应空间位置的概率,实际是一个以真实关键点为中心的高斯响应。
为什么用热图,而不是直接用全连接层回归两个浮点数?我刚开始也不理解,后来自己写了坐标回归的对比实验才发现问题:直接回归坐标的模型容易过拟合,而且对图像中人体位置的平移非常敏感。稍微平移一下人体,输出坐标就要跟着变,网络不得不记忆“坐标和位置”的映射关系,泛化性能很差。而热图是一种空间概率表示,关键点位置以峰值形式体现,模型只需要学会“哪里更像关键点”,天然具有平移不变性。后续几乎所有优秀的姿态估计模型都采用热图输出,这一点是整个论文的基石。
生成标签热图时,通常以真实关键点坐标为中心,填充一个二维高斯核。设关键点坐标为(cx, cy),特征图尺寸为HxW,则热图上像素(i,j)的值可以写成:
exp(-((i - cx)^2 + (j - cy)^2) / (2 * sigma^2))
sigma一般取1到1.5个像素(相对于64x64热图)。如果输入分辨率更大,sigma也要相应调整。这里有一个很容易忽略的细节:关键点坐标是浮点数,在计算高斯时要保留小数参与运算,不能直接取整,否则会产生量化误差,导致热图峰值偏离真实位置。
2.2 残差模块作为基础单元
Hourglass网络内部并没有发明一个新的卷积块,而是直接使用了残差网络里的残差模块(Residual Block)。经典残差模块由两个3x3卷积组成,中间可以加一个1x1的瓶颈结构,每个卷积后接BatchNorm和ReLU。
残差模块的核心价值在于,梯度可以通过恒等路径直接从输出传到输入,有效缓解深层网络的退化问题。作者在每个Hourglass内部大量使用这种模块,而不是用VGG式的连续卷积。我在复现时也试过换成普通的3x3卷积堆叠,发现网络更深时开始出现训练不稳定,而残差模块明显更稳。
实际复现时,残差模块的通道数要统一。论文里基础通道数一般设置为256。如果你显存有限,可以降到128,但精度会下降。通道数变化不只影响参数,还会影响每个尺度上跳跃连接的信息量,太小了会导致上采样阶段无法恢复精细的空间结构。
2.3 单级Hourglass模块的完整流程
一个Hourglass模块的完整流程可以这样描述。假设输入特征图尺寸是64x64,通道数为C。
第一步,用一个残差模块提取浅层特征,同时把这份特征保存为“跳跃连接”的最终层,用于后面的按元素相加。
第二步,进行下采样。使用2x2最大池化或者步长为2的卷积将分辨率减半,比如从64到32。每下采样一次,就经过若干个残差模块,得到新的特征,同时把这一层的特征保存起来,作为后续上采样时对应尺寸的跳跃连接。这个过程重复几次,直到特征图变为4x4左右。
第三步,在最低分辨率4x4的特征上,连续使用多个残差模块,让网络在最抽象的维度充分处理全局语义。在这个小分辨率上,每个像素都代表原图较大区域,特征已经高度语义化。
第四步,开始上采样。这里的上采样方式不是转置卷积,而是先用最近邻插值把特征图分辨率放大两倍,然后接一个残差模块。最近邻插值不会像转置卷积那样产生棋盘伪影,而且计算量小。上采样后,把之前保存的同分辨率跳跃连接特征拿出来,执行逐元素相加,而不是拼接。相加操作把“全局处理后的语义”和“浅层保留的细节”融合起来,然后继续送入残差模块。
第五步,重复上采样和跳跃连接融合,直到恢复成与输入相同的64x64分辨率。
最后,经过一个1x1卷积把特征通道数从C变为K,得到这个Hourglass阶段的热图输出。注意,这个输出是中间产物,不是最终结果。
我把这个流程用伪代码整理一下,帮助理解:
def hourglass_module(x): # x: 输入特征图,假设 H x W x C res_feat = residual(x) # 初始残差模块,保存浅层特征 # ---- 下采样路径 ---- down1 = maxpool(res_feat) # H/2 skip1 = residual(down1) # 保存跳跃连接 down2 = maxpool(skip1) # H/4 skip2 = residual(down2) down3 = maxpool(skip2) # H/8 skip3 = residual(down3) down4 = maxpool(skip3) # H/16,论文中最低分辨率 bottom = residual(down4) bottom = residual(bottom) bottom = residual(bottom) # 最底层全局处理 # ---- 上采样路径 ---- up3 = nearest_upsample(bottom) # 恢复 H/8 up3 = residual(up3) up3 = elementwise_add(up3, skip3) up2 = nearest_upsample(up3) # 恢复 H/4 up2 = residual(up2) up2 = elementwise_add(up2, skip2) up1 = nearest_upsample(up2) # 恢复 H/2 up1 = residual(up1) up1 = elementwise_add(up1, skip1) out = nearest_upsample(up1) # 恢复 H out = residual(out) out = elementwise_add(out, res_feat) # 与最开始的浅层特征融合 return out这个伪代码省略了具体卷积通道数和BatchNorm,但整体结构完全一致。注意,这里的maxpool是2x2步长2,所有残差模块都保持空间尺寸不变(padding适当)。如果你自己实现,最低分辨率不一定是4x4,可以根据输入大小动态调整下采样级数,比如输入224时可能降到7x7,但要保证经过上采样后能恢复到原分辨率。
2.4 跨模块的特征融合与中间监督
一个Hourglass完成后,会得到分辨率与输入相同的特征图,以及一份64x64xK的热图预测。论文并没有让每个Hourglass独立,而是把预测热图反馈到输入特征里,再交给下一个Hourglass。具体有两条数据流:
第一条是原始特征流:当前Hourglass的输入特征(或者经过初始处理后)直接传给下一个Hourglass,保证底层空间信息不丢失。第二条是预测反馈流:当前热图通过一个1x1卷积,将K通道映射到C通道,再和原始特征流逐元素相加,形成下一个Hourglass的输入。这个过程让下一个模块知道上一个模块的预测结果,相当于自校正循环。
为什么把热图映射回特征通道再相加,而不是直接把热图作为额外输入?因为热图是2D单通道,空间信息非常稀疏,直接拼接到高维特征里会让网络负担很大。通过1x1卷积可以学习“从热图到特征空间”的变换,让预测结果以更语义化的方式参与下一阶段。我在复现时也试过直接拼接,效果不如相加,而且参数量暴增,看来简洁的设计确实经过了深思熟虑。
每个Hourglass阶段后都会计算一次损失,训练时把损失的累加值作为最终损失回传。这样做不仅加深了梯度路径,还能让网络在每个阶段都学会输出中间姿态。推理时,可以直接使用最后一个阶段的输出作为最终结果;如果想把所有阶段输出平均一下,通常也能带来微弱的稳定性提升。
3. 复现过程中的实操要点
3.1 数据准备与预处理
复现的第一步不是搭模型,而是准备训练样本。人体姿态估计的数据集通常提供的是原图、人体检测框、关键点坐标和可见性标记。标准流程是:根据检测框把人体区域裁剪出来,再做仿射变换,缩放到固定尺寸。论文将输入统一到256x256,对应的热图分辨率是64x64,相当于每次预测下采样4倍。
训练时要做数据增强,我强烈建议至少包含下面几种:
- 随机旋转:范围在正负30度以内。超过45度容易让头部和身体比例失真,反而干扰关键点位置。
- 随机缩放:0.75到1.25倍,模拟人体在画面中的远近变化。
- 水平翻转:这是最关键的一条增强策略,因为它可以把左、右关键点互换,大幅提升左右混淆的鲁棒性。翻转后,所有关键点坐标也要对应互换索引,比如左手腕索引与右手腕索引互换。
- 随机遮挡:在输入图像上随机挖掉一块方形区域,模拟关节被遮挡的场景。论文原文没有重点提,但实际训练时加上能明显提高遮挡情况下的表现。
还有一个很容易被忽略的细节:图像标准化。输入到网络前,要把像素归一化到[-1,1]或者其他固定范围。如果不做标准化,网络的初始损失会非常大,训练极不稳定。代码里可以预先计算数据集的均值方差,或者直接用固定的归一化方案。
3.2 关键点热图生成与坐标后处理
前面已经写到了热图公式。这里重点说几个实现时容易出错的地方。为了提升效率,不要用双重for循环逐像素计算热图,而要用网格坐标矩阵向量化生成。比如先构造一张大小为HxW的坐标网格,每个像素对应坐标(i,j),再按通道计算高斯值。这样可以批量生成所有关键点的热图。
sigma的选择也很讲究。如果输入是256x256,热图是64x64,那么一个像素对应原图4x4的区域。对于手腕、脚踝这些相对小的部位,sigma取1.5比较合适;如果想追求更高的定位精度,可以用1。sigma太大会让热图过于平滑,峰值不明显,多个关键点的热图重叠;太小又会让正样本区域过小,训练困难。我通常先用sigma=1.5跑通流程,再调优到1,最终精度能提升零点几个点。
训练好后,如何从热图中恢复关键点坐标?简单方案是取热图最大值的坐标,乘以4(因为64x64到256x256的下采样比例),再加上裁剪时的偏移,就得到原图中的关键点坐标。进阶方案是在峰值附近做一个带权重的重定位:取热图最大值附近一个3x3或5x5的小区域,用热图值作为权重计算加权平均,得到亚像素精度。这个操作能稳定提升1到2个像素的定位精度。
3.3 训练配置与优化选择
经典网络的训练方式和现在不太一样,但很多经验仍然管用。优化器选RMSprop比较稳妥,学习率初始化为2.5e-4,配合阶段性衰减;用Adam也可以,但学习率要调低一些,比如1e-4。batch size在单卡上选16到32,如果显存不够就减小输入分辨率,或者使用梯度累积。
权重初始化很重要。残差模块里的卷积层使用Kaiming初始化,BatchNorm的gamma初始为1、beta初始为0,可以避免网络一开始就饱和。损失函数采用每个阶段的MSE(均方误差),等价于对热图每个像素做L2损失。训练时,把所有Hourglass阶段的损失求和,然后一起反传。
损失函数可以这样写:
Loss = sum_{t=1}^{T} sum_{k=1}^{K} || pred_heatmap_{t,k} - gt_heatmap_k ||^2
训练轮数可能需要200个epoch甚至更多。早期loss下降很快,但后期平台期很长,需要耐心。学习率衰减策略建议先用固定学习率训练50个epoch,再每30个epoch衰减0.1。还要持续在验证集上评估指标,不要等训练loss完全收敛后再停,可能已经过拟合。
3.4 测试时增强与精度提升
到了测试阶段,可以使用一些不增加训练成本但稳定提点的技巧。最常用的是多尺度测试:对输入图像按比例缩放成多个尺寸(比如0.5、1、1.5),分别得到热图,再将不同尺寸的热图resize回原始热图尺寸,取平均。由于Hourglass本身已经解决了尺度问题,多尺度测试的收益虽然不如其他方法明显,但通常仍有零点几个点的提升。
翻转测试也是一个经典做法:分别对原图、水平翻转后的图像预测热图,把翻转后的热图水平翻转回来,再与原图热图取平均。这样相当于让模型对同一个姿态做了两次推理,能缓解左右关键点混淆,特别适合对称性强的部位。要注意,翻转后必须把左右关键点索引对应回去,否则最终结果会出现左右互换的错误。
如果最终任务是输出关键点的像素坐标,别忘记把热图坐标乘以下采样比例,再经过仿射变换的逆变换还原到原图。很多刚复现的同学在这一步出错,把坐标算在裁剪后的图像里,导致预测结果整体偏移。
4. 常见问题与排查技巧实录
4.1 热图峰值不尖锐或出现双峰
如果训练出来某类关键点的热图一团糊,先别怀疑网络结构,八成是标签生成出了问题。检查高斯核的sigma是不是太大,多个关键点的热图是否互相重叠;或者个别关键点坐标超出图像范围,仍然生成高斯,导致边界处响应异常。解决方法是:对超出边界的关键点,要么不参与损失计算,要么把中心裁到边界内再生成。
双峰常见于翻转增强后标签没有正确互换左右索引,导致同一个关键点被放在两个不同位置。排查方法很简单:把一张训练图的标签热图可视化出来,看看峰值位置是否和标注一致。如果发现对称的两个位置都有响应,基本可以确定是翻转索引表写错了。
4.2 训练不收敛或loss震荡
常见原因有三个。一是学习率太高,尤其在用RMSprop时,2.5e-4听起来不大,但如果换成了Adam,可能需要降到1e-4。二是数据未标准化,输入像素范围不一致会让损失跳动很大。三是BatchNorm放的位置不对,或训练/测试模式没有正确切换。
另外,不要一上来就使用过大的输入分辨率,先用较小的分辨率把模型训练通,再逐步放大,能减少因显存不足导致的训练中断。如果loss一直降不下去,可以先只训练一个Hourglass模块,验证单个模块能不能过拟合少量样本。这一步能快速排除代码bug。
4.3 输出坐标整体偏移
预测的热图峰值位置看着正确,但还原到原图后整体偏差很大,这几乎可以确定是坐标变换链出了问题。建议把整条变换链单独写成函数,输入一个模拟点,打印每一层的坐标,检查它是否还能映射回去。常见错误是忘记裁剪偏移、忘记除以比例因子、或者翻转后还原时用错了中心。坐标变换必须用裁剪时的中心点作为旋转中心,而不是图像左上角。
4.4 显存不够,训练不起来
Hourglass网络参数量不小,如果显存不足,可以优先减少batch size,或者把输入分辨率从256降到224,牺牲一点精度换来能跑通。更优雅的方案是使用梯度累积,相当于扩大batch size而不增加显存。如果连单张图都跑不了,很可能是基础通道数设置太大,把通道数从256降到128,模型会小很多。
如果使用高分辨率输入,上采样阶段会产生大量中间特征,临时变量占据显存。实在不行就减少Hourglass的堆叠数量,从8个降到4个,先验证正确性,再逐步加回来。
4.5 左右关键点混淆
左右互换是姿态估计里最常见的错误之一。如果模型经常把左手腕识别成右手腕,说明网络没有充分利用左右对称性之外的上下文。除了使用翻转测试,还可以在训练时增加随机旋转,增强网络对视角变化的鲁棒性;如果使用多尺度输入,也能降低因人体旋转导致的左右歧义。另一个有效做法是给关键点定义可见性掩码,遮挡太严重时直接忽略该点的损失,避免网络学到错误样本。
我把这些常见问题和对应的排查思路整理成一张速查表:
| 现象 | 可能原因 | 解决动作 |
|---|---|---|
| 热图一片模糊 | sigma过大,热图生成坐标取整 | 降低sigma,保留浮点坐标计算 |
| 热图出现双峰 | 翻转增强后索引未互换 | 可视化标签热图,核对索引表 |
| 训练不收敛 | 学习率太高、数据未归一化 | 调低学习率,检查图像归一化 |
| 坐标整体偏移 | 仿射逆变换错误 | 单独测试坐标变换链 |
| 左右关键点搞反 | 上下文信息不足 | 增加全局特征、使用翻转测试 |
| 显存不足 | batch过大、通道数过大 | 减小batch,设置梯度累积,降通道数 |
5. 从Hourglass看后续演进与个人体会
5.1 经典结构对后续工作的影响
堆叠沙漏网络在2016年提出了几种后来被广泛复用的范式:多尺度特征融合、热图替代坐标回归、阶段式中间监督、级联细化。之后的很多姿态估计工作都沿用或吸收了这些概念。比如后来的编码器-解码器结构、高分辨率网络,都多多少少受Hourglass启发。即便在Transformer流行后,其内部的多尺度patch融合和分层设计,也能看到和Hourglass类似的“先全局后局部”思路。所以把这篇文章读透,远不止是复现一个模型,而是理解后续许多网络设计的源头。
5.2 个人在实际复现后的一点体会
我在复现过程中最深的感受是:这篇论文写得干净利落,每个设计都有明确的目的。比如跳跃连接用相加而不是拼接,减少参数又不损精度;上采样用最近邻插值而不是转置卷积,避免了很多奇怪的伪影。这些选择都不是炫技,而是对问题建模的真实思考。
对于准备入门人体关键点检测的同学,我的建议是不要急着调参,先手写一个单级Hourglass跑通,再扩展为堆叠版本;接着自己实现一遍热图生成和坐标还原代码,把整个流程的每个环节想清楚。经典论文常读常新,每次回看网络结构和损失设计,都还能发现值得学习的地方。