前言:Mask R-CNN是一个非常灵活的框架,它来源于faster-RCNN和全卷积网络FCN,但是又提出了很多的改进措施,Mask-RCNN非常灵活,我们可以可以增加不同的分支完成不同的任务,可以完成目标分类、目标检测、语义分割、实例分割、人体姿势识别等多种任务,与其说Mask-RCNN是一个实例分割算法,倒不如说Mask-RCNN是一个灵活的框架。
一、Mask-RCNN概览
1.1 诞生背景
1.2 Mask-RCNN简介
1.3 Mask-RCNN的基本架构二、Mask-RCNN的改进点
2.1 ROIAlign改进——最核心的改进点
2.2 Mask-RCNN损失函数的改进
2.3 网络结构上的改进(Network Architecture)
三、Mask-RCNN总结
1.1 诞生背景
论文:Mask RCNN
论文链接:https://arxiv.org/abs/1703.06870
官方代码链接:https://github.com/facebookresearch/Detectron
MXNet版本代码:https://github.com/TuSimple/mx-maskrcnn
Mask R-CNN是ICCV2017的best paper,在一个网络中同时做目标检测(object detection)和实例分割(instance segmentation)。该算法在单GPU上的运行速度差不多是5 fps,并且在COCO数据集的三个挑战赛:instance segmentation、bounding-box object detecton、person keypoint detection中的效果都要优于现有的单模型算法(包括COCO2016比赛的冠军算法)
1.2 Mask-RCNN简介
其实从名字中就可以看出,它跟RCNN系列应该有着密不可分的关系,实际上它是在faster-RCNN的基础之上来进行改进的。前面已经详细介绍过faster-RCNN的原理,它是专门负责做目标检测任务的,以及介绍了FCN系列文章,是专门做语义分割的。这里集中先“定性”的回答几个问题。
问题一:为什么基于faster-RCNN的网络可以用来做实例分割?
目标检测=类别+位置(faster-RCNN)
语义分割=物体轮廓(FCN)
实例分割=类别+位置+物体轮廓(Mask-RCNN)
先定性分析一下,从这里的关系就可以看出来,因为faster-rcnn得到了一张图片上的物体的类别、位置、而实例分割需要得到位置、类别、像素分类这三个信息,所以很自然的想到使用faster-rcnn来完成,我只需要再faster-RCNN的基础之上再引入一个语义分割网络不就可以得到物体的轮廓,这不就可以了嘛,实际上这就是使用Mask-RCNN的基本思想起源,Mask-RCNN也就是用一个专门的分割网络分支,给目标检测的结果进行分割不就可以了吗,这其实是最直观的思路来源。
问题二:Mask-RCNN可以做一些什么类型的任务
既然Mask-RCNN是来源于faster-RCNN,所以faster-RCNN的优点兼而有之。总之,Mask R-CNN是一个非常灵活的框架,可以增加不同的分支完成不同的任务,可以完成目标分类、目标检测、语义分割、实例分割、人体姿势识别等多种任务,Mask-RCNN是一个灵活的框架。
问题三:Mask-RCNN是一个固定的框架吗?它要达到什么目的?
在一定程度上可以这么说,他其实不是一个固定的算法,我们可以灵活地拓展它达到想要的目的。
Mask R-CNN预期达到的目标有:高速、高准确率(高的分类准确率、高的检测准确率、高的实例分割准确率等)、简单直观、易于使用。那怎么去实现这些目标呢?
(1)高速和高准确率:为了实现这个目的,作者选用了经典的目标检测算法Faster-rcnn和经典的语义分割算法FCN,二者结合。Faster-rcnn可以既快又准的完成目标检测的功能;FCN可以精准的完成语义分割的功能,这两个算法都是对应领域中的经典之作。Mask R-CNN比Faster-rcnn复杂,但是最终仍然可以达到5fps的速度,这和原始的Faster-rcnn的速度相当。由于发现了ROI Pooling中所存在的像素偏差问题,提出了对应的ROIAlign策略,加上FCN精准的像素MASK,使得其可以获得高准确率(这个后面会专门说到ROIAlign的问题)。
(2)简单直观:整个Mask R-CNN算法的思路很简单,就是在原始Faster-rcnn算法的基础上面增加了FCN来产生对应的MASK分支。即Mask-RCNN=Faster-rcnn + FCN,更细致的是 RPN + ROIAlign + Fast-rcnn + FCN。
(3)易于使用:整个Mask R-CNN算法非常的灵活,可以用来完成多种任务,包括目标分类、目标检测、语义分割、实例分割、人体姿态识别等多个任务,这将其易于使用的特点展现的淋漓尽致。我很少见到有哪个算法有这么好的扩展性和易用性,值得我们学习和借鉴。除此之外,我们可以更换不同的backbone architecture和Head Architecture来获得不同性能的结果。
1.3 Mask-RCNN的基本架构
前面已经说了一个重要的思想,即:Mask-RCNN=Faster-rcnn + FCN,所以整个Mask-RCNN的结构也就很清晰明了了,如下所示:
这里放两张图片是便于更加方便的理解它的结构。上面的几张结构图中有一个关键的点,ROIPooling被替换成了ROIAlign。
2.1 ROIAlign改进——最核心的改进点
在faster-RCNN中,使用的是ROIPooling,其实本质上就是一个特殊的SPP(空间金字塔池化层),前面已经有文章专门介绍,这里不再赘述,但是它有着致命的问题,Mask-RCNN对它进行了改进。
ROI Pooling和ROIAlign最大的区别是:前者使用了两次量化操作,而后者并没有采用量化操作,使用了线性插值算法,具体的解释讲解如下:,先放一个参考图:
(1)ROI的两次量化操作
如上图所示,为了得到固定大小(7X7)的feature map,我们需要做两次量化操作:
第一次:图像坐标 — feature map坐标。我们来说一下具体的细节,如图我们输入的是一张800x800的图像,在图像中有两个目标(猫和狗),狗的BB大小为665x665,经过VGG16网络后,我们可以获得对应的feature map,如果我们对卷积层进行Padding操作,我们的图片经过卷积层后保持原来的大小,但是由于池化层的存在,我们最终获得feature map 会比原图缩小一定的比例,这和Pooling层的个数和大小有关。在该VGG16中,我们使用了5个池化操作,每个池化操作都是2Pooling,因此我们最终获得feature map的大小为800/32 x 800/32 = 25x25(是整数),但是将狗的BB对应到feature map上面,我们得到的结果是665/32 x 665/32 = 20.78 x 20.78,结果是浮点数,含有小数,但是我们的像素值可没有小数,那么作者就对其进行了量化操作(即取整操作),即其结果变为20 x 20,在这里引入了第一次的量化误差;
第二次:feature map坐标 — ROI feature坐标。然而我们的feature map中有不同大小的ROI,但是我们后面的网络却要求我们有固定的输入,因此,我们需要将不同大小的ROI转化为固定的ROI feature,在这里使用的是7x7的ROI feature,那么我们需要将20 x 20的ROI映射成7 x 7的ROI feature,其结果是 20 /7 x 20/7 = 2.86 x 2.86,同样是浮点数,含有小数点,我们采取同样的操作对其进行取整吧,在这里引入了第二次量化误差。
其实所谓的两次“量化”就是取整,其实就是保证每一个格子都是1,因为像素是没有小数的,那这个取整会导致一个什么样的后果呢?
其实,这里引入的误差会导致图像中的像素和特征中的像素的偏差,即将feature空间的ROI对应到原图上面会出现很大的偏差。原因如下:比如用我们第二次引入的误差来分析,本来是2,86,我们将其量化为2,这期间引入了0.86的误差,看起来是一个很小的误差呀,但是你要记得这是在feature空间,我们的feature空间和图像空间是有比例关系的,在这里是1:32,那么对应到原图上面的差距就是0.86 x 32 = 27.52。这个差距不小吧,这还是仅仅考虑了第二次的量化误差。这会大大影响整个检测算法的性能,因此是一个严重的问题。这就是两次量化所造成的误差。
(2)ROIAlign技术(改进版)
如上图所示,为了得到为了得到固定大小(7X7)的feature map,ROIAlign技术并没有使用量化操作(不进行取整操作),即我们不想引入量化误差,比如665 / 32 = 20.78,我们就用20.78,不用什么20来替代它,比如20.78 / 7 = 2.97,我们就用2.97,而不用2来代替它。这就是ROIAlign的初衷。
那么我们如何处理这些浮点数呢,我们的解决思路是使用“双线性插值”算法。双线性插值是一种比较好的图像缩放算法,它充分的利用了原图中虚拟点(比如20.56这个浮点数,像素位置都是整数值,没有浮点值)四周的四个真实存在的像素值来共同决定目标图中的一个像素值,即可以将20.56这个虚拟的位置点对应的像素值估计出来。
有了这样的一个“双线性插值算法”之后,我们现在就没有必要保证每一个格子都一定是整数了,因为即使它是小数,我们然后通过该算法来进一步确定这个格子里面的像素值即可,具体怎么做呢?参考下图:
如上图所示,蓝色的虚线框表示卷积后获得的feature map(注意:这里的5x5的虚线框并不一定是整数哦,因为没有经过第一次量化,可能是小数),黑色实线框表示ROI feature(也不是整数,每一个格子的大小是小数),最后需要输出的大小是2x2(这个2x2的格子是整数),那具体怎么做呢?大致上分为三个步骤:
这里对上述步骤的第三点作一些说明:这个固定位置是指在每一个矩形单元(每一个bin)中按照固定规则确定的位置。比如,如果采样点数是1,那么就是这个单元的中心点。如果采样点数是4,那么就是把这个单元平均分割成四个小方块(每个小方块长宽都是小数哦)以后它们分别的中心点。显然这些采样点的坐标通常是浮点数,所以需要使用插值的方法得到它的像素值。在相关实验中,作者发现将采样点设为4会获得最佳性能,甚至直接设为1在性能上也相差无几。下面的一组图可以很好地说明整个过程:
这是经过卷积和池化下采样之后得到的缩小的特征图feature map。
这个黑色框框是在特征图上的ROI区域,可以很明显的看出,这个ROI区域是没有经过量化的,每一个格子的长宽都是小数。
在每一个框框里按照一定的规则采样四个点,这四个点的位置可以理解为四个不同的像素值,得到下图这样的结果:
最后通过最大化池化,得到下面的结果
最终得到量化的2x2的输出结果。我们的整个过程中没有用到量化操作,没有引入误差,即原图中的像素和feature map中的像素是完全对齐的,没有偏差,这不仅会提高检测的精度,同时也会有利于实例分割。有时候真的不得不感叹一些大佬的脑回路,对于细节的把握,对于知识的创新真的是让人敬佩。
事实上,ROI Align 在遍历取样点的数量上没有ROIPooling那么多,但却可以获得更好的性能,这主要归功于解决了misalignment的问题。值得一提的是,我在实验时发现,ROI Align在VOC2007数据集上的提升效果并不如在COCO上明显。经过分析,造成这种区别的原因是COCO上小目标的数量更多,而小目标受misalignment问题的影响更大(比如,同样是0.5个像素点的偏差,对于较大的目标而言显得微不足道,但是对于小目标,误差的影响就要高很多)。
2.2 Mask-RCNN损失函数的改进
Mask-RCNN由于在faster-RCNN的架构之上重新添加了分割的分支,网络结构有所变化,最后的损失函数也有相应的变化,由于增加了mask分支,每个ROI的Loss函数如下所示:
其中Lcls和Lbox和Faster r-cnn中定义的相同。这里主要介绍下Lmask。
在Mask R-CNN中,对于新增加的mask支路,其对于每个ROI的输出维度是K*m*m,其中m*m表示mask的大小,K表示K个类别,因此这里每一个ROI一共生成K个binary mask,这就是文章中提到的class-specific mask概念。在得到预测mask后,对mask的每个像素点值求sigmoid函数值(即所谓的per-pixel sigmoid),得到的结果作为Lmask(交叉熵损失函数)的输入之一。需要注意的是这里也只有正样本ROI才会用于计算Lmask,正样本的定义和目标检测一样,都是IOU大于0.5定义为正样本。
其实Lmask和Lcls非常类似,只不过前者Lmask是基于像素点来计算,后者Lcls是基于图像来计算,因此和Lcls类似,虽然这里得到K个mask,但是在计算cross-entropy损失函数时只有ground truth对应的那个mask才有效。举个例子:假设某个ROI的ground-truth类别是K3,那么该ROI的Lmask只和K3类别对应的mask相关,其他K-1个mask都不会对Lmask产生影响,其他的K-1个mask对于loss是没有贡献的。那到底是怎么做的呢?其实这是“分类网络”和“分割网络”互相结合来实现的。
我们定义的Lmask允许网络为每一类生成一个mask,而不用和其它类进行竞争;我只用特定类的那一个mask进行误差计算,那我怎么知道到底是哪一类呢?我们依赖于分类分支所预测的类别标签来选择输出的mask。这样将分类和mask生成分解开来。另外因为一个mask包含多个像素点,所以这里Lmask是每个像素点的交叉熵损失的均值,这也是文章中将Lmask称为average binary cross-entropy loss的原因。
看到这里依然很懵,我决定用自己的方式来说明一下这个损失函数到底怎么求的。
(1)先来看一下经典的FCN的损失函数怎么求
比如有21个类别,那么最终全卷积的的输出通道应该是21个通道的,大小为mxm,如下所示:
最终的损失怎么算呢?其实就是算每一个像素位置所有channel上的的21个像素的21分类交叉熵损失(如上面的绿色部分),最后我的某一个channel上的特征图得到的可能是下面这样的结果,假设概率分类的阈值为>0.5.
这是得到的分割图形,中间的分割图形是我随便画的,它表示在这个第K个特征图上,那些被涂成蓝色的像素都是经过交叉熵损失之后概率为K的概率大于0.5.我们也发现,FCN进行语义分割使用的是 multinomial cross-entropy loss ,在这种情况下各个mask(实际上就是各个channel)之间存在竞争关系
(2)再来看一下Mask-RCNN中分割部分的损失函数怎么求
Mask-RCNN不是一次性对所有的通道channel上的像素求多分类损失,而是只在每一个ROI所对应的类别上对每一个像素求一个sigmoid二分类,如下图所示:
在上图中,第一个ROI我们只在对应的K=3的类别中,即在channel为3的通道上的那个mask上面对每一个像素求二分类,第二个ROI我们只在对应的K=8的类别中,即在channel为8的通道上的那个mask上面对每一个像素求二分类,关键问题是那我怎么知道每一个ROI到底是那个类别呢?这其实就是分类网络的功能了,分类网络告诉分割网络这个对应的ROI是属于哪一个类别K的。这样做的好处是定义的Lmask允许网络为每一类生成一个mask,只用在自己的类别(即channel)上计算损失,而不用和其它类进行竞争。对每个固定的类别的那张特征图的每一个像素计算损失之后,然后再求所有像素的平均,这也是文章中将Lmask称为average binary cross-entropy loss的原因。
(3)关键点总结:
最后:上面的图形完全是个人所化,个人理解,水平有限,如果有大神发现错误,望告知更改,谢谢!
2.3 网络结构上的改进(Network Architecture)
backbone这里就不再说了,下面看一下不同的head architecture,如下图所示:
如上图所示,为了产生对应的Mask,文中提出了两种架构,即
(1)左边的Faster R-CNN/ResNet,左边是基于Faster RCNN引入mask支路后的检测部分。图中的ROI指的就是ROIAlign操作,采用7*7大小的划分得到7*7*1024维度的feature map,然后再接ResNet中的res5结构(Faster RCNN中这部分其实存在对ROI的重复计算,这也是后续类似R-FCN算法的改进点,当然也有一些做法是将ROIPool移到后面来做,也就是基于res5的输出做ROIPool,而不是基于res4的输出来做),另外这里对res5做了修改,使得这个结构不改变输入feature map的宽高,res5结构输出7*7*2048维度的feature map。基于该feature map有两条支路,上面那条支路经过池化层得到1*1*2048维度的输出并作为分类支路和回归支路的输入;下面那条支路接反卷积层和卷积层来得到mask。
(2)右边使用到的backbone是FPN网络,右边是基于FPN算法引入mask支路后的检测部分。FPN中就将原来Faster RCNN中的ROI Pool移到res5后面,也就是ROI Pool层之后不再涉及一些特征提取操作,这样就减少了很多重复计算。图中的ROI指的就是ROIAlign操作,上面一条支路得到维度为7*7*256的feature map(因为FPN算法是基于5个融合特征层分别做检测,这里仅以一个融合特征层为例介绍,每个融合特征层的输出channel都是256,因此经过ROIAlign后得到的输出channel还是256),最后接两个1024维度的全连接层就可以做为分类和回归支路的输入。下面一条支路用14*14大小的划分得到14*14*256的输出,然后接数个卷积和反卷积层得到mask。
Mask R-CNN论文的主要贡献包括以下几点:
(1)ROIAlign。分析了ROI Pool的不足,提升了ROIAlign,提升了检测和实例分割的效果;
(2)组合模型。将实例分割分解为分类和mask生成两个分支,依赖于分类分支所预测的类别标签来选择输出对应的mask。同时利用Binary Loss代替Multinomial Loss,消除了不同类别的mask之间的竞争,生成了准确的二值mask;
(3)并行处理。并行进行分类和mask生成任务,对模型进行了加速。