Mask R-CNN论文地址:https://arxiv.org/abs/1703.06870,论文于2017年发表在ICCV
上,获得了2017年ICCV
的最佳论文奖。
我们可以看到论文的一作是ResNet的何凯明
,还有提出Faster RCNN系列的Ross Girshick
.
Mask R-CNN
是在Faster R-CNN
的基础上加了一个用于预测目标分割Mask
的分支(即可预测目标的Bounding Boxes信息、类别信息以及分割Mask信息)。
Mask R-CNN不仅能够同时进行目标检测与分割,还能很容易地扩展到其他任务,比如再同时预测人体关键点信息。
Faster RCNN
,Mask R-CNN的结构也很简单,就是在通过RoIAlign
(在原Faster R-CNN论文中是RoIPool
)得到的RoI
基础上并行添加一个Mask分支(小型的FCN)。通过Mask分支我们就能对我们检测的每一个目标生成一个Mask分割蒙版。如果想检测每个人的关键点信息,也可以并联一个keypoints detection分支。Faster -RCNN
源码中使用的也是RoiAlign
,而不是RoIPooling对于Mask分支论文中作者也讲到其实它跟FCN非常相似,下图是作者给出Mask分支更加详细的结构,我们可以看到其实它有两个不同的形式。
左边的结构是不带有FPN(特征金字塔结构),右边是带有FPN结构(FPN使用在Faster-RCNN的backbone中)的,并且在我们日常使用过程中,更加常用的也是右边的分支,接下来我们也会以右边为例进行网络讲解。
在之前的Faster RCNN
中,会使用RoIPool
将RPN
得到的Proposal
池化到相同大小。这个过程会涉及quantization
或者说取整操作,这会导致定位不是那么的准确(文中称为misalignment
问题)。
RoI Pooling
的话,对应的AP
值为28.2,如果采用RoIAlign
的话就能达到34
,很明显提升了5.8
个点,提升的效果非常明显。图中左半部分是针对分割的场景,同样采用RoI Pooling
的话对应的AP为23.6
,如果采用RoIAlign
的话AP
就能达到30.9
,一下子就提升了7.3
个点。通过这几组数据,很明显使用RoIAlign
它的定位会更加准确下面的示意图就是RoIPool
的执行过程,其中会经历两次quantization
。假设通过RPN
得到了一个Proposal
,它在原图上的左上角坐标是 ( 10 , 10 ) (10,10) (10,10),右下角的坐标是 ( 124 , 124 ) (124 , 124 ) (124,124) ,对应要映射的特征层相对原图的步距为32
,通过RoIPool
期望的输出为2x2
大小
Proposa
l映射到特征层上,对于左上角坐标 ( 10 , 10 ) (10,10) (10,10),进行32
倍下采样。 很明 10 32 \frac{10}{32} 3210 是不能被整除的,在RoIPooling
中会对它进行四舍五入,就得到了对应特征图上左上角点后(0,0);同理对于右下角坐标 124 32 \frac{124}{32} 32124进行四舍五入得到右下角点的坐标为 ( 4 , 4 ) (4,4) (4,4)。将proposal映射到对应上图特征层上从第0行到第4行,从第0列到第4列的区域(黑色矩形框)。这是第一次quantization
。2x2
大小,所以需要将映射在特征层上的Proposal
划分成2x2
大小区域。但现在映射在特征层上的Proposal
是5x5
大小,无法整除均分,所以强行划分后有的区域大有的区域小,如上图所示。这是第二次quantization
。maxpool
即可得到RoIPool
的输出, ( 1.6874 0.4676 2.0242 2.3571 ) \bigl( \begin{matrix} 1.6874 & 0.4676 \\ 2.0242 & 2.3571 \end{matrix} \bigr) (1.68742.02420.46762.3571),对应图中每个区域用蓝色标出来的4
个数值。这边用torchvison
中的RoIPool
做了相关实验,代码如下:import torch
from torchvision.ops import RoIPool
def main():
torch.manual_seed(1)
x=torch.randn(1,1,6,6)
print(f"feature map:\n{x}")
proposal=[torch.as_tensor([[10,10,124,124]],dtype=torch.float32)] #定义proposal 左上角点(10,10) 右下角度(124,124)
roi_pool=RoIPool(output_size=2,spatial_scale=1/32) #输出大小2x2 下采样为32倍
roi_pool(x,proposal)
print(f"roi pool":\n{roi})
if __name__=='__main__':
main()
终端输出:
feature map:
tensor([[[[-1.5256, -0.7502, -0.6540, -1.6095, -0.1002, -0.6092],
[-0.9798, -1.6091, -0.7121, 0.3037, -0.7773, -0.2515],
[-0.2223, 1.6871, 0.2284, 0.4676, -0.6970, -1.1608],
[ 0.6995, 0.1991, 0.1991, 0.0457, 0.1530, -0.4757],
[-1.8821, -0.7765, 2.0242, -0.0865, 2.3571, -1.0373],
[ 1.5748, -0.6298, 2.4070, 0.2786, 0.2468, 1.1843]]]])
roi pool:
tensor([[[[1.6871, 0.4676],
[2.0242, 2.3571]]]])
得到的输出和上面图片展示的是一样的。
下面的示意图就是RoIAlign
的执行过程。同样假设通过RPN
得到了一个Proposal
,它在原图上的左上角坐标是(10,10)
,右下角的坐标是 (124,124) (124,124)
,对应要映射的特征层相对原图的步距为32
,通过RoIAlign
期望的输出为2x2
大小:
Proposal
映射到特征层上,通过RoIAlign
方式不会进行取整。根据 10 32 = 0.3125 \frac{10}{32}=0.3125 3210=0.3125,映射到特征图上左上角坐标为 ( 0.3125 , 0.3125 )
(不进行四舍五入),同理右下角坐标根据$\frac{124}{32}=3.875,映射到特征图上右下角点坐标为( 3.875 , 3.875 )
(不进行四舍五入)。为了方便理解,将特征层上的每个元素用一个点表示,就能得到图中下方的grid网格。图中蓝色的矩形框就是Proposal(没有quantization
)2x2
大小,故将Proposal
均分为2x2
四个子区域(没有quantization
)。接着根据sampling_ratio
在每个子区域中设置采样点,原论文中默认设置的sampling_ratio
为2
,区域内长宽位置均匀取4个采样点(当采用多个采样点时,每个区域的输出取所有采样点的均值),这里为了方便讲解将sampling_ratio
设置成1,只采样一个点。
这里以第一个子区域为例,因为这里将sampling_ratio设置成为1,所以每个子区域只需要设置一个采样点。第一个子区域的采样点为图中黄色
的点(即为该子区域的中心点),坐标为 ( 1.203125 , 1.203125 )
,然后找出离该采样点最近的四个点(即图中用红色箭头标出的四个黑点),然后利用双线性插值
即可计算得到采样点对应的输出− 0.8546
(如果不了解双线性插值可参考博文),又由于该子区域只有一个采样点,故该子区域的输出就为 − 0.8546
。同理其他子区域也是一样,分别找到各自区域的中心点,以及离中心点最近的4个点,利用双线性差值
计算得到采样点的输出。
图中x,y
为采样点的坐标位置, f 1 , f 2 , f 3 , f 4 f_1,f_2,f_3,f_4 f1,f2,f3,f4分别对应离采样点最近的四个点的数值, u u u表示采用点到所在网格Top
距离, v v v表示采用点离所在网格的left
距离。
下面是使用Torchvision
库中实现的RoIAlign
方法,通过对比计算结果和我们刚刚讲的是一样的。
import torch
from torchvision.ops import RoIAlign
def bilinear(u, v, f1, f2, f3, f4):
return (1-u)*(1-v)*f1 + u*(1-v)*f2 + (1-u)*v*f3 + u*v*f4
def main():
torch.manual_seed(1)
x = torch.randn((1, 1, 6, 6))
print(f"feature map: \n{x}")
proposal = [torch.as_tensor([[10, 10, 124, 124]], dtype=torch.float32)]
roi_align = RoIAlign(output_size=2, spatial_scale=1/32, sampling_ratio=1)
roi = roi_align(x, proposal)
print(f"roi align: \n{roi}")
u = 0.203125
v = 0.203125
f1 = x[0, 0, 1, 1] # -1.6091
f2 = x[0, 0, 1, 2] # -0.7121
f3 = x[0, 0, 2, 1] # 1.6871
f4 = x[0, 0, 2, 2] # 0.2284
print(f"bilinear: {bilinear(u, v, f1, f2, f3, f4):.4f}")
if __name__ == '__main__':
main()
终端输出:
feature map:
tensor([[[[-1.5256, -0.7502, -0.6540, -1.6095, -0.1002, -0.6092],
[-0.9798, -1.6091, -0.7121, 0.3037, -0.7773, -0.2515],
[-0.2223, 1.6871, 0.2284, 0.4676, -0.6970, -1.1608],
[ 0.6995, 0.1991, 0.1991, 0.0457, 0.1530, -0.4757],
[-1.8821, -0.7765, 2.0242, -0.0865, 2.3571, -1.0373],
[ 1.5748, -0.6298, 2.4070, 0.2786, 0.2468, 1.1843]]]])
roi align:
tensor([[[[-0.8546, 0.3236],
[ 0.2177, 0.0546]]]])
bilinear: -0.8546
可以看到torchvision
实现的RoIAlign和我们计算的输出是一样的。通过我们讲的例子我们可以知道的RoIAlign
在计算过程是没有涉及到任何取整操作的,所以它的定位会更加准确。作者在论文中也提到采样点的个数和位置对最终的结果并没什么影响
,所以我们一般都把sample_rate
设置为2
,默认每个区域设置4个采样点。
前面有提到,对于带有FPN和不带有FPN的Mask R-CNN,他们的Mask分支不太一样。下图左边是不带FPN结构的Mask分支,右侧是带有FPN结构的Mask分支(灰色部分为原Faster R-CNN预测box, class信息的分支,白色部分为Mask分支)
由于在我们日常使用中,一般都是·使用的带有FPN的网络
,对于带FPN结构的Mask RCNN,它上面一个分支是Faster-RCNN预测器,注意它所使用的RoIAlign
和下面的Mask
分支采用的RoIAlign
其实是不一样的,也就是这两个分支并不共用RoIAlign
,上面一个分支通过RoIAlign
得到的RoI
大小是7x7
,但是在Mask分支中,我们通过RoIAlign
得到的大小是14x14
,因为对于分割任务而言,我们要求的分割结果,精度要高一些,所以需要保留更多的细节信息,所以Mask分支没有池化到7x7
大小,而是池化到14x14
。
HxWx256
,经过RoIAlign
之后被池化为14x14x256
,接下来依次通过4个卷积层,这个四个卷积层后面都跟了ReLU
激活函数,并且都是kernel为3x3
,步距为1
的卷积层。经过这4个卷积层我们得到的输出依旧是14x14x256
,接下来再通过一个转置卷积,通过转置卷积会将输入特征的高宽
进行翻倍,由14x14
变为28x28
,然后在通过一个1x1
卷积来调整输出channel,使得channel等于分类的个数 n u m c l s num_{cls} numcls,最终输出的特征层大小为 28 ∗ 28 ∗ n u m c l a s s 28*28*num_{class} 28∗28∗numclass,也就是说针对每个类别我们都预测了一个蒙版,并且这个蒙版大小都是28x28
。在Mask R-CNN中,对预测的Mask以及Class进行解耦
之前在讲FCN
的时候有提到过,FCN
是对每个像素针对每个类别都会预测一个分数,然后对每个像素沿channel方向做softmax处理,得到每个像素归属每个类别的概率,不同类别之间存在竞争关系
,哪个概率高就将该像素分配给哪个类别。(因为softmax处理后所有类别概率之和为1,某些概率值大了的话,其他类型额概的率就会变小,所以不同类别间存在竞争关系,也就是mask和class之间存在耦合关)但在Mask R-CNN中,作者将预测Mask
和class
进行了解耦,即对输入的RoI
针对每个类别都单独预测一个Mask
,但是我么不会针对每个像素沿channel方向做softmax处理,而是最终根据box, cls分支预测的classes信息来选择对应类别的Mask(不同类别之间不存在竞争关系
)。作者说解耦后带来了很大的提升。下表是原论文中给出的消融实验结果,其中softmax代表原FCN方式(Mask和class未解耦),sigmoid代表Mask R-CNN中采取的方式(Mask和class进行了解耦)。
AP
为24.8
,采用sigmoid
解耦的方式,获得的AP
达到了30.3
,提升了5.5个点。也就是说在Mask R-CNN中,对预测的Mask以及Class进行解耦是非常必要的。预测
的时候输入Mask分支
的目标是由Fast R-CNN提供的
(即预测的最终目标)。 并且训练时采用的Proposals全部是Fast R-CNN阶段匹配到的正样本
。这里说下我个人的看法(不一定正确),在训练时Mask分支利用RPN提供的目标信息能够扩充训练样本的多样性(因为RPN提供的目标边界框并不是很准确,一个目标可以呈现出不同的情景,类似于围着目标做随机裁剪。从另一个方面来看,通过Fast R-CNN得到的输出一般都比较准确了
,再通过NMS
后剩下的目标就更少了)。在预测时为了获得更加准确的目标分割信息以及减少计算量(通过Fast R-CNN后的目标数会更少),此时利用的是Fast R-CNN提供的目标信息。Mask R-CNN
的损失就是在Faster R-CNN
的基础上加上了Mask分支上的损失,即:
L o s s = L r p n + L f a s t r c n n + L m a s k Loss =L_{rpn} + L_{fast_rcnn} + L_{mask} Loss=Lrpn+Lfastrcnn+Lmask
关于Faster R-CNN的损失计算,这里就不在赘述,参考博文:RCNN、Fast-RCNN、Faster-RCNN理论合集,关于Mask分支上的损失就是二值交叉熵损失(Binary Cross Entropy
)
logits
(网络预测的输出)是什么,targets
(对应的GT)是什么。前面有提到训练时输入Mask
分支的目标是RPN
提供的Proposals
,所以网络预测的logits是针对每个Proposal对应每个类别的Mask信息(注意预测的Mask大小都是28x28)。并且这里输入的Proposals都是正样本(在Fast R-CNN阶段采样得到的),对应的GT信息(box、cls)也是知道的。Proposal
(图中黑色的矩形框),通过RoIAlign
后得到对应的特征信息(shape为14x14xC
),接着通过Mask Branch
预测每个类别的Mask信息得到图中的logits
(logits通过sigmoid
激活函数后,所有值都被映射到0至1之间)。通过Fast R-CNN分支正负样本匹配过程我们能够知道该Proposal的GT类别为猫(cat),所以将logits中对应类别猫的预测mask(shape为28x28)提取出来。然后根据Proposal在原图对应的GT上裁剪并缩放到28x28
大小,得到图中的GT mask
(对应目标区域为1,背景区域为0)。最后计算logits中预测类别为猫的mask与GT mask的BCELoss(BinaryCrossEntropyLoss
)即可。这里再次强调一遍,在真正预测推理的时候,输入Mask分支的目标是由Fast R-CNN分支提供的
。
如上图所示,通过Fast R-CNN
分支,我们能够得到最终预测的目标边界框信息以及类别信息。接着将目标边界框
信息(注意此处不是经过RPN得到的Proposals)提供给Mask分支
,经过RoIAlign
就能预测得到该目标的logits
信息,再根据Fast R-CNN
分支提供的类别信息
将logits中对应该类别的Mask信息提取出来,即针对该目标预测的Mask信息(shape为28x28
,由于通过sigmoid激活函数,数值都在0到1之间)。然后利用双线性插值将Mask缩放到预测目标边界框大小,并放到原图对应区域。接着通过设置的阈值(默认为0.5)将Mask转换成一张二值图
,比如预测值大于0.5
的区域设置为前景剩下区域都为背景。现在对于预测的每个目标我们就可以在原图中绘制出边界框信息,类别信息以及目标Mask信息
本博客参考:太阳花小绿豆的 Mask R-CNN网络详解