1.前言
这篇文章是ECCV2016的论文,Jia Deng组的工作,是top-down算法,非常经典,当时也是在各种公开数据集上霸榜。在FLIC和MPII上都是第一名(在当时),是sota算法。现在很多关于姿态估计的论文都有参考SHN或者会拿他作对比,可以说是比较典型的姿态估计算法。
2. 网络结构
SHN网络名字起的很不错,级联的沙漏网络,顾名思义,沙漏网络就表示该网络具有高度对称性,多个沙漏网络进行级联,其实不级联也是可以检测的,只是检测效果会差一些,作者认为人体关节点之间有较强的相关性,前面沙漏检测出的关键点对后面的检测有帮助,所以前面的输出可以作为后面的输入的一部分,见下图的虚线部分,这个后面再讨论。
级联的沙漏网络
2.1单个沙漏网络
单个沙漏网络如上图所示,这是一个4阶版本的沙漏网络,表示有四次下采样和四次下采样。方块大小表示feature maps大小,方块变小方式下采样,方块变大是上采样,加号表示按元素相加。其他全部都是残差模块,上方的连线方式也是一些残差模块,但是没有改变feature maps的大小,只是改变通道数,变成与下面相同,然后才可以按元素相加。
看一下更加具体的版本
该图来自https://blog.csdn.net/shenxiaolu1984/article/details/51428392。浅绿色部分是一些残差模块。看上去很明朗,就是一些残差模块,先下采样然后上采样,这样的网络结构提取特征很充分,在不同的分辨率有进行卷积,然后还有特征融合。但是也有一些弊端,不能使用pretrained model,因为它不像cpn那样,GlobalNet是resnet50或者resnet101,可以直接使用在ImageNet上预训练的模型进行初始化。没有预训练模型用来初始化,一般需要训练更久然后效果会更差一些,但是没有预训练的情况下,当数据很充分,训练也很充分,合理使用BN或者GN,炼丹能力较好的情况下,是可以达到预训练的效果(Kaiming He的最新论文的结论,Rethinking)。
2.2看一下Pytorch版本实现
-
class HourglassNet(nn.Module):
-
'''Hourglass model from Newell et al ECCV 2016'''
-
def __init__(self, block, num_stacks=2, num_blocks=4, num_classes=16):
-
"""
-
参数解释
-
:param block: hg块元素
-
:param num_stacks: 有几个hg
-
:param num_blocks: 在两个hg之间有几个block块
-
:param num_classes: keypoint个数,也就是最后的heatmap个数
-
"""
-
super(HourglassNet, self).__init__()
-
-
self.inplanes =
64
-
self.num_feats =
128
-
self.num_stacks = num_stacks
-
self.conv1 = nn.Conv2d(
3, self.inplanes, kernel_size=
7, stride=
2, padding=
3,
-
bias=
True)
# 第一次下采样
-
self.bn1 = nn.BatchNorm2d(self.inplanes)
-
self.relu = nn.ReLU(inplace=
True)
-
self.layer1 = self._make_residual(block, self.inplanes,
1)
#self.planes = 64,有downsample(只是改变channel数)
-
self.layer2 = self._make_residual(block, self.inplanes,
1)
#有downsample(只是改变channel数)
-
# 这一次的bottleneck没有downsample,因为self.planes == planes(self.num_feats=128)*2 = 256
-
self.layer3 = self._make_residual(block, self.num_feats,
1)
-
self.maxpool = nn.MaxPool2d(
2, stride=
2)
#第二次下采样
-
# build hourglass modules
-
ch = self.num_feats*block.expansion
#128*2=256
-
hg, res, fc, score, fc_, score_ = [], [], [], [], [], []
-
for i
in range(num_stacks):
-
hg.append(Hourglass(block, num_blocks, self.num_feats,
4))
#block, num_blocks, planes, depth=4
-
res.append(self._make_residual(block, self.num_feats, num_blocks))
-
fc.append(self._make_fc(ch, ch))
-
score.append(nn.Conv2d(ch, num_classes, kernel_size=
1, bias=
True))
-
if i < num_stacks
-1:
-
fc_.append(nn.Conv2d(ch, ch, kernel_size=
1, bias=
True))
-
score_.append(nn.Conv2d(num_classes, ch, kernel_size=
1, bias=
True))
-
self.hg = nn.ModuleList(hg)
-
self.res = nn.ModuleList(res)
-
self.fc = nn.ModuleList(fc)
-
self.score = nn.ModuleList(score)
-
self.fc_ = nn.ModuleList(fc_)
-
self.score_ = nn.ModuleList(score_)
-
-
def _make_residual(self, block, planes, blocks, stride=1):
#planes = 64,blocks=4
-
downsample =
None
-
if stride !=
1
or self.inplanes != planes * block.expansion:
-
# 这里的downsample只有改变通道数的功能,并没有下采样的功能,因为调用时stride固定为1
-
downsample = nn.Sequential(
-
nn.Conv2d(self.inplanes, planes * block.expansion,
-
kernel_size=
1, stride=stride, bias=
True),
-
)
-
-
layers = []
-
# 只在每个block的第一个bottleneck做downsample,因为channel数不相同
-
layers.append(block(self.inplanes, planes, stride, downsample))
-
self.inplanes = planes * block.expansion
#self.planes是改变的,从最开始的64,128,256
-
for i
in range(
1, blocks):
#因为blocks=1 ,后面都不会执行
-
layers.append(block(self.inplanes, planes))
-
-
return nn.Sequential(*layers)
-
-
def _make_fc(self, inplanes, outplanes):
-
bn = nn.BatchNorm2d(inplanes)
-
conv = nn.Conv2d(inplanes, outplanes, kernel_size=
1, bias=
True)
-
return nn.Sequential(
-
conv,
-
bn,
-
self.relu,
-
)
-
-
def forward(self, x):
-
out = []
-
x = self.conv1(x)
#下采样
-
x = self.bn1(x)
-
x = self.relu(x)
-
-
x = self.layer1(x)
-
x = self.maxpool(x)
#下采样
-
x = self.layer2(x)
-
x = self.layer3(x)
-
-
for i
in range(self.num_stacks):
-
y = self.hg[i](x)
-
y = self.res[i](y)
-
y = self.fc[i](y)
-
score = self.score[i](y)
-
out.append(score)
-
if i < self.num_stacks
-1:
-
fc_ = self.fc_[i](y)
-
score_ = self.score_[i](score)
-
x = x + fc_ + score_
-
-
return out
在上面,Bottleneck是使用expansion=2的版本的残差Bottleneck,通常是是使用4阶版本的沙漏网络,结构就跟上图一样,很多残差模块加下采样和上采样。这个实现使用了递归实现,这么短的代码就实现了那么长的网络结构,PyTorch真香,呵呵~
2.3完整网络结构
看了上面的单个Hourglass结构,下面看下完整的网络结构。很简单,前面加了几层卷积,后面就是Hourglass的级联模式,Hourglass之间的级联稍微有一些特殊处理。
网络的从一个7*7的卷积开始,然后接着3个残差模块,这一共会经过两次下采样,如果输入是256*256的,那么经过这个前端网络处理feature maps变为64*64的尺寸。后面就开始级联多个Hourglass部分接口,只与多少个可能要根据实际情况确定。作者实验试过2,4,8,好像是越多越好,然后越到后面输出预测越准,符合直觉预期,说明经过级联是有效的,前面的输出对后面的训练是有帮助的。
下面是Hourglass之间的连接结构图,有一些特征融合在里面。
图来自https://blog.csdn.net/wangzi371312/article/details/81174452
如上图,N1代表第一个沙漏网络,提取出的混合特征经过1个1x1全卷积网络后,分成上下两个分支,上部分支继续经过1x1卷积后,进入下一个沙漏网络。下部分支先经过1x1卷积后,生成heat map,就是图中蓝色部分.
上图中蓝色方块比其他三个方块要窄一些,这是因为heat map矩阵的depth与训练数据里的节点数一致,比如 [1x64x64x16],其他几个则具有较高的depth,如 [1x64x64x256]
heat_map继续经过1x1卷积,将depth调整到与上部分支一致,如256,最后与上部分支合并,一起作为下一个沙漏网络的输入。
前面提到过,由于人体关节点之间的较强相关性,作者认为前面检测出的heat maps对后面的预测是有帮助的,最初的输入,heatmaps经过1*1卷积调整channels数,以及上一级Hourglass的输出三个做按元素相加,作为下一级Hourglass的输入。
2.4 完整网络结构PyTorch实现
-
class HourglassNet(nn.Module):
-
'''Hourglass model from Newell et al ECCV 2016'''
-
def __init__(self, block, num_stacks=2, num_blocks=4, num_classes=16):
-
"""
-
参数解释
-
:param block: hg块元素
-
:param num_stacks: 有几个hg
-
:param num_blocks: 在两个hg之间有几个block块
-
:param num_classes: keypoint个数,也就是最后的heatmap个数
-
"""
-
super(HourglassNet, self).__init__()
-
-
self.inplanes =
64
-
self.num_feats =
128
-
self.num_stacks = num_stacks
-
self.conv1 = nn.Conv2d(
3, self.inplanes, kernel_size=
7, stride=
2, padding=
3,
-
bias=
True)
-
self.bn1 = nn.BatchNorm2d(self.inplanes)
-
self.relu = nn.ReLU(inplace=
True)
-
self.layer1 = self._make_residual(block, self.inplanes,
1)
#self.planes = 64
-
self.layer2 = self._make_residual(block, self.inplanes,
1)
-
self.layer3 = self._make_residual(block, self.num_feats,
1)
#这一次的bottleneck没有downsample,因为self.planes == planes(self.num_feats=128)*2 = 256
-
self.maxpool = nn.MaxPool2d(
2, stride=
2)
#TODO 这个maxpool需不需要。论文里是有2次下采样,从256降到64,
-
-
# build hourglass modules
-
ch = self.num_feats*block.expansion
#128*2=256
-
hg, res, fc, score, fc_, score_ = [], [], [], [], [], []
-
for i
in range(num_stacks):
-
hg.append(Hourglass(block, num_blocks, self.num_feats,
4))
#block, num_blocks, planes, depth=4
-
res.append(self._make_residual(block, self.num_feats, num_blocks))
-
fc.append(self._make_fc(ch, ch))
-
score.append(nn.Conv2d(ch, num_classes, kernel_size=
1, bias=
True))
-
if i < num_stacks
-1:
-
fc_.append(nn.Conv2d(ch, ch, kernel_size=
1, bias=
True))
-
score_.append(nn.Conv2d(num_classes, ch, kernel_size=
1, bias=
True))
-
self.hg = nn.ModuleList(hg)
-
self.res = nn.ModuleList(res)
-
self.fc = nn.ModuleList(fc)
-
self.score = nn.ModuleList(score)
-
self.fc_ = nn.ModuleList(fc_)
-
self.score_ = nn.ModuleList(score_)
-
-
def _make_residual(self, block, planes, blocks, stride=1):
#planes = 64,blocks=4
-
downsample =
None
-
if stride !=
1
or self.inplanes != planes * block.expansion:
-
downsample = nn.Sequential(
-
nn.Conv2d(self.inplanes, planes * block.expansion,
-
kernel_size=
1, stride=stride, bias=
True),
-
)
-
-
layers = []
-
layers.append(block(self.inplanes, planes, stride, downsample))
#只在每个block的第一个bottleneck做下采样,因为channel数不相同
-
self.inplanes = planes * block.expansion
#self.planes是改变的,从最开始的64,128,256
-
for i
in range(
1, blocks):
-
layers.append(block(self.inplanes, planes))
-
-
return nn.Sequential(*layers)
-
-
def _make_fc(self, inplanes, outplanes):
-
bn = nn.BatchNorm2d(inplanes)
-
conv = nn.Conv2d(inplanes, outplanes, kernel_size=
1, bias=
True)
-
return nn.Sequential(
-
conv,
-
bn,
-
self.relu,
-
)
-
-
def forward(self, x):
-
out = []
-
x = self.conv1(x)
-
x = self.bn1(x)
-
x = self.relu(x)
-
-
x = self.layer1(x)
-
x = self.maxpool(x)
-
x = self.layer2(x)
-
x = self.layer3(x)
-
-
for i
in range(self.num_stacks):
-
y = self.hg[i](x)
-
y = self.res[i](y)
-
y = self.fc[i](y)
-
score = self.score[i](y)
-
out.append(score)
-
if i < self.num_stacks
-1:
-
fc_ = self.fc_[i](y)
-
score_ = self.score_[i](score)
-
x = x + fc_ + score_
-
-
return out
fc和score分别表示hourglass的输出两个支路,score是得到heatmaps,经过的卷积的channel 数好keypoints个数相同。fc_和score_分别表示当后面还需要级联Hourglass时,需要做一些1*1的卷积改变featuremaps的通道数,这样后面才能做按元素相加,然后作为后面的输入。
3. 中继监督
通过端到端地堆叠多个沙漏,我们将网络架构进一步细化,将一个沙漏的输出作为输入提供给下一个沙漏,但是每个Hourglass都会输出heatmaps,然后也会计算loss。这提供了具有用于重复自底向上、自顶向下的推理的机制的网络,允许在整个图像上重新评估初始估计和特征。这种方法的关键是预测我们可以应用损失的中间群体。预测是在通过每个沙漏之后生成的,其中网络有机会在本地和全局上下文中处理特性。随后的沙漏模块允许再次处理这些高级特征,以进一步评估和重新评估高阶空间关系。这与其他姿态估计方法类似,这些姿态估计方法在多个迭代阶段和中间监督下表现出很强的性能。
下面这个图挺有意思的,这个Ablation实验部分。作者实验了不同的级联方式对准确率的影响,和中间Hourglass输出heatmaps的准确率规律,在参数量几乎相同的情况下,每个残差模块有不同的个数,这样网络的总层数几乎相同。可以看到,小的Hourglass多级联几次有利于准确率提升,后面层的输出比前面的输出效果好非常多,在小的Hourglass上看的尤其明显,级联了8次,前面2级的效果很差。
作者还做了一些有趣的实验,loss计算位置,在网络结构相似的情况下,loss影响不是特别大,在每个Hourglass的单独输出上计算loss效果是最好的。
4. 训练设置
5. 结论与结果
- 设计了一个新的单人姿态估计网络Hourglass,效果也是棒棒的,如果用于多人需要单独的行人检测作为前端预处理。
- 中继监督的作用很大,
- 级联的Hourglass效果非常好,当时sota方法
- 但对一些遮挡问题难以处理,这是绝大部分算法的难题
参考文献:
[1] Newell A , Yang K , Deng J . Stacked Hourglass Networks for Human Pose Estimation[J]. 2016.
[2] https://github.com/bearpaw/pytorch-pose
[3]https://blog.csdn.net/wangzi371312/article/details/81174452
[4] https://blog.csdn.net/shenxiaolu1984/article/details/51428392