【导读】本篇博文我们一起来讨论总结一下目标检测任务中用来处理目标多尺度的一些算法。视觉任务中处理目标多尺度主要分为两大类:
目标检测中存在不同目标实例之间的尺度跨度非常大,在多尺度的物体中,大尺度的物体由于面积大、特征丰富,通常来讲较为容易检测。难度较大的主要是小尺度的物体,而这部分小物体在实际工程中却占据了较大的比例。通常认为绝对尺寸小于32×32的物体,可以视为小物体或者物体宽高是原图宽高的1/10以下,可以视为小物体。
小物体由于其尺寸较小,可利用的特征有限,这使得其检测较为困难。当前的检测算法对于小物体并不友好,体现在以下4个方面:
多尺度的检测能力实际上体现了尺度的不变性,当前的卷积网络能够检测多种尺度的物体,很大程度上是由于其本身具有超强的拟合能力。
较为通用的提升多尺度检测的经典方法有:
接下来,我们主要将主要介绍U-shape/V-shape型多尺度处理、SNIP、TridentNet、FPN这四大多尺度解决方法。
U-shape/V-shape型多尺度处理
这种方式有点类似U-Net的结构,通过采用对称的encoder-decoder结构,将高层特征逐渐与低层特征融合,这样的操作类似于将多个感受野进行混合,使得输出的多尺度信息更为丰富;Face++团队在2018年COCO比赛上,在backbone最后加入gpooling操作,获得理论上最大的感受野,类似于V-shape结构,结果证明确实有效。该方法虽然比SSD的单层输出多尺度信息相比更好,但其也存在问题:
SNIP
用ImageNet预训练的模型在迁移到COCO数据集中时很可能产生一定的domain-shift偏差,什么是domain-shift呢?简单来说,ImageNet 是用来图像分类的,目标一般 scale 比较大,而 object detection 数据集中的目标的 scale 跨度很大。在 ImageNet 这种大目标数据集上 预训练的特征,如果直接用在检测那些小目标,这些特征是不能很好地匹配和对齐的,效果并不会很好,这就是 domain-shift 造成的。这篇论文首先分析了小尺度与预训练模型尺度之间的关系,作者认为要解决domian-shift问题,就要让输入分布接近模型预训练的分布,基于图像金字塔,提出了一种尺度归一化的训练机制,称为SNIP(Scale-Normalization for Image Pyramids)。
该论文的主要贡献在于:
(1)通过实验验证了upsampling对提高小目标检测的效果
(2)提出了一种Scale Normalization for Image Pyramids的方法
(3)提出一个思想:即要让输入的分布接近预训练模型的分布
SNIP的网络结构如下图所示:
具体的设计和实现细节:
(1)3个尺度分别拥有各自的RPN模块,并且各自预测指定范围内的物体。最后不同分支的proposal进行汇总。
(2)由于图像金字塔的应用,对于大尺度(高分辨率)的feature map,对应的RPN只负责预测被放大的小目标;对于小尺度(低分辨率)的feature map,对应的RPN只负责预测被缩小的大目标。这样真实的物体尺度分布在较小的区间内,避免了极大或者极小的物体。
(3)在RPN阶段,如果真实物体不在该RPN预测范围内,会被判定为无效,并且与该无效物体的IoU大于0.3的Anchor也被判定为无效的Anchor。
(4)在训练时,只对有效的Proposal进行反向传播。在测试阶段,对有效的预测Boxes先缩放到原图尺度,利用Soft NMS将不同分辨率的预测结果合并。
(5)实现时SNIP采用了可变形卷积的卷积方式,并且为了降低对于GPU的占用,将原图随机裁剪为1000×1000大小的图像。
总体来说,SNIP是多尺度训练(Multi-Scale Training)的改进版本。SNIP让模型更专注于物体本身的检测,剥离了多尺度的学习难题。在网络搭建时,SNIP也使用了类似于MST的多尺度训练方法,构建了3个尺度的图像金字塔。MST的思想是使用随机采样的多分辨率图像使检测器具有尺度不变特性。然而作者通过实验发现,在MST中,对于极大目标和过小目标的检测效果并不好,但是MST也有一些优点,比如对一张图片会有几种不同分辨率,每个目标在训练时都会有几个不同的尺寸,那么总有一个尺寸在指定的尺寸范围内。SNIP的做法是只对size在指定范围内的目标回传损失,即训练过程实际上只是针对某些特定目标进行,这样就能减少domain-shift带来的影响。
TridentNet
对于一个detector本身而言,影响backbone的主要有以下三个因素:network depth(structure),downsample rate和receptive field。对于网络深度而言,ResNet证明了一般网络越深(网络的表达能力就越强),相应的结果会越好。下采用率方面,由于下采样次数过多对于小物体有负面影响。那么有没有可能单独分离出receptive field,保持其他变量不变,来验证它对detector性能的影响。所以,该论文做了一个验证性实验,分别使用ResNet50和ResNet101作为backbone,改变最后一个stage中每个3*3 conv的dilation rate。通过这样的方法,我们便可以固定同样的网络结构,同样的参数量以及同样的downsample rate,只改变网络的receptive field。我们很惊奇地发现,不同尺度物体的检测性能和dilation rate正相关!也就是说,更大的receptive field对于大物体性能会更好,更小的receptive field对于小物体更加友好。
总结一下,TridentNet主要在原始的backbone上做了以下三点变化:
TridentNet网络结构
论文中还做了非常详尽的ablation analyses,包括有几个branch性能最好;trident block应该加在网络的哪个stage;trident block加多少个性能会饱和。这些就不展开在这里介绍了,有兴趣的读者可以参照原文。
FPN
FPN将深层信息上采样,与浅层信息逐元素地相加,从而构建了尺寸不同的特征金字塔结构,性能优越,FPN如今已成为Detecton算法的标准组件,不管是one-stage(RetinaNet、DSSD)、two-stage(Faster R-CNN、Mask R-CNN)还是four-stage(Cascade R-CNN)都可用;
如下图所示,FPN把低分辨率、高语义信息的高层特征和高分辨率、低语义信息的低层特征进行自上而下的侧边连接,使得所有尺度下的特征都有丰富的语义信息。
算法结构可以分为三个部分:自下而上的卷积神经网络(上图左),自上而下过程(上图右)和特征与特征之间的侧边连接。
FPN对于不同大小的RoI,使用不同的特征图,大尺度的RoI在深层的特征图上进行提取,小尺度的RoI在浅层的特征图上进行提取。FPN的代码实现如下:
# Build the shared convolutional layers.
# Bottom-up Layers
# Returns a list of the last layers of each stage, 5 in total.
# 扔掉了C1
_, C2, C3, C4, C5 = resnet_graph(input_image, "resnet101", stage5=True)
# Top-down Layers
# TODO: add assert to varify feature map sizes match what's in config
P5 = KL.Conv2D(256, (1, 1), name='fpn_c5p5')(C5) # C5卷积一下就当做P5
P4 = KL.Add(name="fpn_p4add")([ # P4 开始有了对应元素add操作
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)])
# 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是P5的极大值池化
P6 = KL.MaxPooling2D(pool_size=(1, 1), strides=2, name="fpn_p6")(P5)
# Note that P6 is used in RPN, but not in the classifier heads.
rpn_feature_maps = [P2, P3, P4, P5, P6]
mrcnn_feature_maps = [P2, P3, P4, P5]
优缺点分析:
参考链接: