『计算机视觉』FPN:feature pyramid networks for object detection

对用卷积神经网络进行目标检测方法的一种改进,经过提取多尺度的特征信息进行融合,进而提升目标检测的精度,特别是在小物体检测上的精度。FPN是ResNet或DenseNet等通用特征提取网络的附加组件,能够和经典网络组合提高原网络效果。html

1、问题背景

网络的深度(对应到感觉野)与总stride一般是一对矛盾的东西,经常使用的网络结构对应的总stride通常会比较大(如32),而图像中的小物体甚至会小于stride的大小,形成的结果就是小物体的检测性能急剧降低。python

传统解决这个问题的思路包括:git

(1)多尺度训练和测试,又称图像金字塔,以下图(a)所示。目前几乎全部在ImageNet和COCO检测任务上取得好成绩的方法都使用了图像金字塔方法。然而这样的方法因为很高的时间及计算量消耗,难以在实际中应用。github

(2)特征分层,即每层分别预测对应的scale分辨率的检测结果。以下图(c)所示。SSD检测框架采用了相似的思想。这样的方法问题在于直接强行让不一样层学习一样的语义信息。而对于卷积神经网络而言,不一样深度对应着不一样层次的语义特征,浅层网络分辨率高,学的更可能是细节特征,深层网络分辨率低,学的更可能是语义特征。算法

 

于是,目前多尺度的物体检测主要面临的挑战为:网络

1. 如何学习具备强语义信息的多尺度特征表示?app

2. 如何设计通用的特征表示来解决物体检测中的多个子问题?如object proposal, box localization, instance segmentation.框架

3. 如何高效计算多尺度的特征表示?ide

2、特征金字塔网络(Feature Pyramid Networks)

做者提出了FPN算法。作法很简单,以下图所示。把低分辨率、高语义信息的高层特征和高分辨率、低语义信息的低层特征进行自上而下的侧边链接,使得全部尺度下的特征都有丰富的语义信息。post

图中未注明的是融合以后的feat还须要进行一次3*3卷积

做者的算法结构能够分为三个部分:自下而上的卷积神经网络(上图左),自上而下过程(上图右)和特征与特征之间的侧边链接。

自下而上的部分其实就是卷积神经网络的前向过程。在前向过程当中,特征图的大小在通过某些层后会改变,而在通过其余一些层的时候不会改变,做者将不改变特征图大小的层归为一个阶段,所以每次抽取的特征都是每一个阶段的最后一个层的输出,这样就能构成特征金字塔。具体来讲,对于ResNets,做者使用了每一个阶段的最后一个残差结构的特征激活输出。将这些残差模块输出表示为{C2, C3, C4, C5},对应于conv2,conv3,conv4和conv5的输出。

自上而下的过程采用上采样进行。上采样几乎都是采用内插值方法,即在原有图像像素的基础上在像素点之间采用合适的插值算法插入新的元素,从而扩大原图像的大小。经过对特征图进行上采样,使得上采样后的特征图具备和下一层的特征图相同的大小。

根本上来讲,侧边之间的横向链接是将上采样的结果和自下而上生成的特征图进行融合。咱们将卷积神经网络中生成的对应层的特征图进行1×1的卷积操做,将之与通过上采样的特征图融合,获得一个新的特征图,这个特征图融合了不一样层的特征,具备更丰富的信息。 这里1×1的卷积操做目的是改变channels,要求和后一层的channels相同在融合以后还会再采用3*3的卷积核对每一个融合结果进行卷积,目的是消除上采样的混叠效应,如此就获得了一个新的特征图。这样一层一层地迭代下去,就能够获得多个新的特征图。假设生成的特征图结果是P2,P3,P4,P5,它们和原来自底向上的卷积结果C2,C3,C4,C5一一对应。金字塔结构中全部层级共享分类层(回归层)。

3、fast rcnn中的特征金字塔

Fast rcnn中的ROI Pooling层使用region proposal的结果和特征图做为输入。通过特征金字塔,咱们获得了许多特征图,做者认为,不一样层次的特征图上包含的物体大小也不一样,所以,不一样尺度的ROI,使用不一样特征层做为ROI pooling层的输入。大尺度ROI就用后面一些的金字塔层,好比P5;小尺度ROI就用前面一点的特征层,好比P4。可是如何肯定不一样的roi对应的不一样特征层呢?做者提出了一种方法:640?wx_fmt=png,224是ImageNet的标准输入,k0是基准值,设置为5,表明P5层的输出(原图大小就用P5层),w和h是ROI区域的长和宽,假设ROI是112 * 112的大小,那么k = k0-1 = 5-1 = 4,意味着该ROI应该使用P4的特征层。k值作取整处理。这意味着若是RoI的尺度变小(好比224的1/2),那么它应该被映射到一个精细的分辨率水平。 

与RPN同样,FPN每层feature map加入3*3的卷积及两个相邻的1*1卷积分别作分类和回归的预测。在RPN中,实验对比了FPN不一样层feature map卷积参数共享与否,发现共享仍然能达到很好性能,说明特征金字塔使得不一样层学到了相同层次的语义特征。

 

  • 用于RPN的FPN:用FPN替换单一尺度的FMap。它们对每一个级都有一个单一尺度的anchor(不须要多级做为其FPN)。它们还代表,金字塔的全部层级都有类似的语义层级。

  • Faster RCNN:他们以相似于图像金字塔输出的方式观察金字塔。所以,使用下面这个公式将RoI分配到特定level。

    • 640?wx_fmt=png

    • 其中w,h分别表示宽度和高度。k是分配RoI的level。640?wx_fmt=png是w,h=224,224时映射的level。

4、其余问题

Q1:不一样深度的feature map为何能够通过upsample后直接相加?

答:做者解释说这个缘由在于咱们作了end-to-end的training,由于不一样层的参数不是固定的,不一样层同时给监督作end-to-end training,因此相加训练出来的东西可以更有效地融合浅层和深层的信息。

 

Q2:为何FPN相比去掉深层特征upsample(bottom-up pyramid)对于小物体检测提高明显?(RPN步骤AR从30.5到44.9,Fast RCNN步骤AP从24.9到33.9)

答:做者在poster里给出了这个问题的答案

对于小物体,一方面咱们须要高分辨率的feature map更多关注小区域信息,另外一方面,如图中的挎包同样,须要更全局的信息更准确判断挎包的存在及位置。

 

Q3:若是不考虑时间状况下,image pyramid是否可能会比feature pyramid的性能更高?

答:做者以为通过精细调整训练是可能的,可是image pyramid(金字塔)主要的问题在于时间和空间占用太大,而feature pyramid能够在几乎不增长额外计算量状况下解决多尺度检测问题。

5、代码层面看FPN

本部分截取自知乎文章:从代码细节理解 FPN,做者使用Mask-RCNN的源码辅助理解FPN结构,项目地址见MRCNN,关于MRCNN,文章『计算机视觉』RCNN学习_其三:Mask-RCNN会介绍。

一、 怎么作的上采样?

高层特征怎么上采样和下一层的特征融合的,代码里面能够看到:

P5 = KL.Conv2D(256, (1, 1), name='fpn_c5p5')(C5)

C5是 resnet最顶层的输出,它会先经过一个1*1的卷积层,同时把通道数转为256,获得FPN 的最上面的一层 P5。

KL.UpSampling2D(size=(2, 2),name="fpn_p5upsampled")(P5)

Keras 的 API 说明告诉咱们:

也就是说,这里的实现使用的是最简单的上采样,没有使用线性插值,没有使用反卷积,而是直接复制。

二、 怎么作的横向链接?

P4 = KL.Add(name="fpn_p4add")
    ([KL.UpSampling2D(size=(2, 2), name="fpn_p5upsampled")(P5),
      KL.Conv2D(256,(1, 1), name='fpn_c4p4')(C4)])

这里能够很明显的看到,P4就是上采样以后的 P5加上1*1 卷积以后的 C4,这里的横向链接实际上就是像素加法,先把 P5和C4转换到同样的尺寸,再直接进行相加。

注意这里对从 resnet抽取的特征图作的是 1*1 的卷积:

1x1的卷积我认为有三个做用:使bottom-up对应层降维至256;缓冲做用,防止梯度直接影响bottom-up主干网络,更稳定;组合特征。

三、 FPN自上而下的网络结构代码怎么实现?

# 先从 resnet 抽取四个不一样阶段的特征图 C2-C5。
_, C2, C3, C4, C5 =
resnet_graph(input_image, config.BACKBONE,stage5=True, train_bn=config.TRAIN_BN)

# Top-down Layers 构建自上而下的网络结构
# 从 C5开始处理,先卷积来转换特征图尺寸
P5 = KL.Conv2D(256, (1, 1), name='fpn_c5p5')(C5)
# 上采样以后的P5和卷积以后的 C4像素相加获得 P4,后续的过程就相似了
P4 = KL.Add(name="fpn_p4add")([
            KL.UpSampling2D(size=(2, 2), name="fpn_p5upsampled")(P5),
            KL.Conv2D(256, (1, 1),name='fpn_c4p4')(C4)])
P3 = KL.Add(name="fpn_p3add")([
            KL.UpSampling2D(size=(2, 2), name="fpn_p4upsampled")(P4),
            KL.Conv2D(256, (1, 1), name='fpn_c3p3')(C3)])
P2 = KL.Add(name="fpn_p2add")([
            KL.UpSampling2D(size=(2, 2),name="fpn_p3upsampled")(P3),
            KL.Conv2D(256, (1, 1), name='fpn_c2p2')(C2)])


# P2-P5最后又作了一次3*3的卷积,做用是消除上采样带来的混叠效应
# Attach 3x3 conv to all P layers to get the final feature maps.
P2 = KL.Conv2D(256, (3, 3), padding="SAME", name="fpn_p2")(P2)
P3 = KL.Conv2D(256, (3, 3), padding="SAME",name="fpn_p3")(P3)
P4 = KL.Conv2D(256, (3, 3), padding="SAME",name="fpn_p4")(P4)
P5 = KL.Conv2D(256, (3, 3), padding="SAME",name="fpn_p5")(P5)
# P6 is used for the 5th anchor scale in RPN. Generated by
# subsampling from P5 with stride of 2.
P6 = KL.MaxPooling2D(pool_size=(1, 1), strides=2,name="fpn_p6")(P5)

# 注意 P6是用在 RPN 目标区域提取网络里面的,而不是用在 FPN 网络
# Note that P6 is used in RPN, but not in the classifier heads.
rpn_feature_maps = [P2, P3, P4, P5, P6] 
# 最后获得了5个融合了不一样层级特征的特征图列表;

注意 P6是用在 RPN 目标区域提取网络里面的,而不是用在 FPN 网络;

另外这里 P2-P5最后又作了一次3*3的卷积,做用是消除上采样带来的混叠效应。

四、 如何肯定某个 ROI 使用哪一层特征图进行 ROIpooling ?

看代码:

# Assign each ROI to a level in the pyramid based on the ROI area.
# 这里的 boxes 是 ROI 的框,用来计算获得每一个 ROI 框的面积
y1, x1, y2, x2 = tf.split(boxes, 4, axis=2)
h = y2 - y1
w = x2 - x1
# Use shape of first image. Images in a batch must have the same size.
# 这里获得原图的尺寸,计算原图的面积
image_shape = parse_image_meta_graph(image_meta)['image_shape'][0]
# Equation 1 in the Feature Pyramid Networks paper. Account for
# the fact that our coordinates are normalized here.
# e.g. a 224x224 ROI (in pixels) maps to P4
# 原图面积
image_area = tf.cast(image_shape[0] * image_shape[1], tf.float32)

# 分两步计算每一个 ROI 框须要在哪一个层的特征图中进行 pooling
roi_level = log2_graph(tf.sqrt(h * w) / (224.0 / tf.sqrt(image_area)))
roi_level = tf.minimum(5, tf.maximum(
    2, 4 + tf.cast(tf.round(roi_level), tf.int32)))

不一样尺度的ROI,使用不一样特征层做为ROI pooling层的输入,大尺度ROI就用后面一些的金字塔层,好比P5;小尺度ROI就用前面一点的特征层,好比P4。那怎么判断ROI改用那个层的输出呢?论文的 K 使用以下公式,代码作了一点更改,替换为roi_level:

# 代码里面的计算替换为如下计算方式:
roi_level = min(5, max(2, 4 + log2(sqrt(w * h) / ( 224 / sqrt(image_area)) ) ) )
224是ImageNet的标准输入,k0是基准值,设置为5,表明P5层的输出(原图大小就用P5层),w和h是ROI区域的长和宽,image_area是输入图片的长乘以宽,即输入图片的面积,假设ROI是112 * 112的大小,那么k = k0-1 = 5-1 = 4,意味着该ROI应该使用P4的特征层。k值会作取整处理,防止结果不是整数。

五、 上面获得的5个融合了不一样层级的特征图怎么使用?

能够看到,这里只使用2-5四个特征图:

for i, level in enumerate(range(2, 6)):
            # 先找出须要在第 level 层计算ROI
            ix = tf.where(tf.equal(roi_level, level))
            level_boxes = tf.gather_nd(boxes, ix)

            # Box indicies for crop_and_resize.
            box_indices = tf.cast(ix[:, 0], tf.int32)

            # Keep track of which box is mapped to which level
            box_to_level.append(ix)

            # Stop gradient propogation to ROI proposals
            level_boxes = tf.stop_gradient(level_boxes)
            box_indices = tf.stop_gradient(box_indices)

            # Crop and Resize
            # From Mask R-CNN paper: "We sample four regular locations, so
            # that we can evaluate either max or average pooling. In fact,
            # interpolating only a single value at each bin center (without
            # pooling) is nearly as effective."
            #
            # Here we use the simplified approach of a single value per bin,
            # which is how it's done in tf.crop_and_resize()
            # Result: [batch * num_boxes, pool_height, pool_width, channels]
            # 使用 tf.image.crop_and_resize 进行 ROI pooling 
            pooled.append(tf.image.crop_and_resize(
                feature_maps[i], level_boxes, box_indices, self.pool_shape,
                method="bilinear"))

对每一个 box,都提取其中每一层特征图上该box对应的特征,而后组成一个大的特征列表pooled。

六、 金字塔结构中全部层级共享分类层是怎么回事?

先看代码:

# ROI Pooling
# Shape: [batch, num_boxes, pool_height, pool_width, channels]
# 获得通过 ROI pooling 以后的特征列表
x = PyramidROIAlign([pool_size, pool_size],
                    name="roi_align_classifier")([rois, image_meta] + feature_maps)

# 将上面获得的特征列表送入 2 个1024通道数的卷积层以及 2 个 rulu 激活层
# Two 1024 FC layers (implemented with Conv2D for consistency)
x = KL.TimeDistributed(KL.Conv2D(1024, (pool_size, pool_size), padding="valid"),
                       name="mrcnn_class_conv1")(x)
x = KL.TimeDistributed(BatchNorm(), name='mrcnn_class_bn1')(x, training=train_bn)
x = KL.Activation('relu')(x)
x = KL.TimeDistributed(KL.Conv2D(1024, (1, 1)),
                       name="mrcnn_class_conv2")(x)
x = KL.TimeDistributed(BatchNorm(), name='mrcnn_class_bn2')(x, training=train_bn)
x = KL.Activation('relu')(x)

shared = KL.Lambda(lambda x: K.squeeze(K.squeeze(x, 3), 2),
                   name="pool_squeeze")(x)

# 分类层
# Classifier head
mrcnn_class_logits = KL.TimeDistributed(KL.Dense(num_classes),
                                        name='mrcnn_class_logits')(shared)
mrcnn_probs = KL.TimeDistributed(KL.Activation("softmax"),
                                 name="mrcnn_class")(mrcnn_class_logits)

# BBOX 的位置偏移回归层
# BBox head
# [batch, boxes, num_classes * (dy, dx, log(dh), log(dw))]
x = KL.TimeDistributed(KL.Dense(num_classes * 4, activation='linear'),
                       name='mrcnn_bbox_fc')(shared)
# Reshape to [batch, boxes, num_classes, (dy, dx, log(dh), log(dw))]
s = K.int_shape(x)
mrcnn_bbox = KL.Reshape((s[1], num_classes, 4), name="mrcnn_bbox")(x)

这里的PyramidROIAlign获得的 x就是上面一步获得的从每一个层的特征图上提取出来的特征列表,这里对这个特征列表先接两个1024通道数的卷积层,再分别送入分类层和回归层获得最终的结果。

也就是说,每一个 ROI 都在P2-P5中的某一层获得了一个特征,而后送入同一个分类和回归网络获得最终结果。

FPN中每一层的heads 参数都是共享的,做者认为共享参数的效果也不错就说明FPN中全部层的语义都类似。

七、 它的思想是什么?

把高层的特征传下来,补充低层的语义,这样就能够得到高分辨率、强语义的特征,有利于小目标的检测。

八、 横向链接起什么做用?

若是不进行特征的融合(也就是说去掉全部的1x1侧链接),虽然理论上分辨率没变,语义也加强了,可是AR降低了10%左右!做者认为这些特征上下采样太屡次了,致使它们不适于定位。 Bottom-up的特征包含了更精确的位置信息。

6、资源资料

Feature Pyramid Networks for Object DetectionCVPR 2017论文)

知乎:特征金字塔网络FPN

知乎:从代码细节理解 FPN

FPN特征金字塔网络--论文解读

详解何恺明团队4篇大做 | 从特征金字塔网络、Mask R-CNN到学习分割一切

源码资料:

  • 官方:Caffe2

    https://github.com/facebookresearch/Detectron/tree/master/configs/12_2017_baselines

  • Caffe

    https://github.com/unsky/FPN

  • PyTorch

    https://github.com/kuangliu/pytorch-fpn (just the network)

  • MXNet

    https://github.com/unsky/FPN-mxnet

  • Tensorflow

    https://github.com/yangxue0827/FPN_Tensorflow

相关文章
相关标签/搜索