FPN详细
图a
图a是在传统的图像处理当中是比较常见的一个方法。针对我们要检测不同尺度的目标时,会将图片缩放成不同的尺度,然后将每个尺度的图片依次通过我们的算法进行预测。 优点是它创建的多尺度特征的所有层次都包含很强的语义特征,包括高分辨率的层次。这种方法的优点是精度比较高。缺点是我们生成多少尺度的图片我们就要重新去预测多少次,需要大量的算力和内存空间。
图b
图b是Fast R-CNN和Faster R-CNN中采用的一种方式。将图片通过进行卷积和池化操作得到我们最终的特征图,然后在最终的特征图上进行预测。它只使用了最后卷积层的结果,卷积网络中的不同层次会产生不同空间分辨率的特征图,但是不同的卷积层得到的特征图会有很大的语义鸿沟。高分辨率具有很好的低层次特征,但是不利于识别物体,低分辨率的特征具有很好的高层次特征,但是不利于识别小物体。
图c
SSD网络中使用的是图c所示金字塔,SSD中将卷积网络中不同层计算出的特征图组成了一个特征金字塔。但是为了避免使用低层次的特征,在构建特征金子塔的时候是从后面的层次开始,并且又追加了几个新层。这样就损失了高分辨率的特征图,对检测小目标是不利的。
图d
也就是FPN结构,将不同特征图上的特征去进行融合,在融合得到的特征图上再进行预测。主要分了自下向上的一条路径和自上到下的一条路径。自下向上就是深度卷积网络的前向提取特征的过程,自上而下则是对最后卷积层的特征图进行上采样的过程,横向的连接则是融合深层的卷积层特征和浅层卷积特征的过程。这也就是为什么对小物体也有很好的检测效果,它融合了深层卷积层的高级别特征和浅层卷积层的低级别特征。
第一步: 自下而上的路径。取深度卷积网络,每次卷积图片尺度缩小一倍,我们记为C2 , C3 , C4。C4为最小的特征图。将空间信息少但是语义信息强的最深层卷积层的C4进行1x1卷积调整通道数再进行最大池化操作得到P5,注意P5只用于RPN部分,不在Fast-RCNN中使用。
第二步: 自上而下的路径。首先得到P4;将P4进行2倍的上采样与进行1x1的卷积调整通道后的C3利用横向连接加到一起得到P3;最后将P3进行2倍的上采样与进行1x1的卷积调整通道后的C2利用横向连接加到一起得到P2。
[Image]
第三步: 得到的结果P4,P3,P2后面接一个3x3的卷积来减轻上采样的混叠效应(aliasing effect),进行预测。
class FPN(nn.Module):
'''
FPN需要初始化一个list,代表ResNet每一个阶段的Bottleneck的数量
'''
def __init__(self, layers):
super(FPN, self).__init__()
#构建C1
self.inplanes = 64
self.conv1 = nn.Conv2d(3, 64, 7, 2, 3, bias=False)
self.bn1 = nn.BatchNorm2d(64)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(3, 2, 1)
#自下而上搭建C2、C3、C4、C5
self.layer1 = self._make_layer(64, layers[0])
self.layer2 = self._make_layer(128, layers[1], 2)
self.layer3 = self._make_layer(256, layers[2], 2)
self.layer4 = self._make_layer(512, layers[3], 2)
#对C5减少通道,得到P5
self.toplayer = nn.Conv2d(2048, 256, 1, 1, 0)
#3*3卷积融合
self.smooth1 = nn.Conv2d(256, 256, 3, 1, 1)
self.smooth2 = nn.Conv2d(256, 256, 3, 1, 1)
self.smooth3 = nn.Conv2d(256, 256, 3, 1, 1)
#横向连接,保证每一层通道数一致
self.latlayer1 = nn.Conv2d(1024, 256, 1, 1, 0)
self.latlayer2 = nn.Conv2d( 512, 256, 1, 1, 0)
self.latlayer3 = nn.Conv2d( 256, 256, 1, 1, 0)
#构建C2到C5
def _make_layer(self, planes, blocks, stride=1):
downsample = None
#如果步长不为1,进行下采样
if stride != 1 or self.inplanes != Bottleneck.expansion * planes:
downsample = nn.Sequential(
nn.Conv2d(self.inplanes, Bottleneck.expansion * planes, 1, stride, bias=False),
nn.BatchNorm2d(Bottleneck.expansion * planes)
)
layers = []
layers.append(Bottleneck(self.inplanes, planes, stride, downsample))
#更新输入输出层
self.inplanes = planes * Bottleneck.expansion
#根据block数量添加bottleneck的数量
for i in range(1, blocks):
layers.append(Bottleneck(self.inplanes, planes))
return nn.Sequential(*layers
#自上而下上采样
def _upsample_add(self, x, y):
_,_,H,W = y.shape
#逐个元素相加
return F.upsample(x, size=(H,W), mode='bilinear') + y
def forward(self, x):
#自下而上
c1 = self.maxpool(self.relu(self.bn1(self.conv1(x))))
c2 = self.layer1(c1)
c3 = self.layer2(c2)
c4 = self.layer3(c3)
c5 = self.layer4(c4)
#自上而下,横向连接
p5 = self.toplayer(c5)
p4 = self._upsample_add(p5, self.latlayer1(c4))
p3 = self._upsample_add(p4, self.latlayer2(c3))
p2 = self._upsample_add(p3, self.latlayer3(c2))
#卷积融合,平滑处理
p4 = self.smooth1(p4)
p3 = self.smooth2(p3)
p2 = self.smooth3(p2)
return p2, p3, p4, p5