Mask R-CNN
- 摘要
- Abstract
- 文章信息
- 研究动机
- Mask RCNN
- RoIPool与RoIAlign
- 双线性插值
- Mask Branch(FCN)
- 其他细节
- Mask RCNN损失
- Mask分支预测
- 网络搭建
- 创新点与不足
- 总结
摘要
本篇博客介绍了Mask R-CNN,这是一种用于实例分割的模型,能够在目标检测的基础上实现像素级分割。其核心思想是在Faster R-CNN框架中引入一个并行的Mask分支,同时完成目标检测(分类和边界框回归)和实例分割(像素级掩码生成)。针对Faster R-CNN中RoIPool导致的特征图与原始图像空间不对齐问题,Mask R-CNN提出了RoIAlign技术,通过双线性插值消除量化误差,显著提升了分割精度。此外,Mask分支采用全卷积网络(FCN)结构,能够高效地生成目标的二值掩码。但Mask R-CNN也存在一些不足,如计算复杂度高、对小目标分割效果有限、数据需求高等。未来的改进方向包括优化模型效率(如轻量化设计)、提升小目标分割能力(如引入多尺度特征融合)以及探索无监督或弱监督学习方法以降低数据标注成本。
Abstract
This blog introduces Mask R-CNN, a model designed for instance segmentation that achieves pixel-level segmentation on top of object detection. Its core idea is to introduce a parallel Mask branch into the Faster R-CNN framework, enabling simultaneous object detection (classification and bounding box regression) and instance segmentation (pixel-level mask generation). To address the misalignment issue between feature maps and the original image caused by RoIPool in Faster R-CNN, Mask R-CNN proposes RoIAlign, which eliminates quantization errors through bilinear interpolation, significantly improving segmentation accuracy. Additionally, the Mask branch adopts a Fully Convolutional Network (FCN) structure, efficiently generating binary masks for targets. However, Mask R-CNN also has some limitations, such as high computational complexity, limited effectiveness in segmenting small objects, and high data requirements. Future improvements may include optimizing model efficiency (e.g., lightweight design), enhancing small object segmentation (e.g., multi-scale feature fusion), and exploring unsupervised or weakly supervised learning methods to reduce data annotation costs.
文章信息
Title:Mask R-CNN
Author:Kaiming He, Georgia Gkioxari, Piotr Dollar, Ross Girshick
Source:https://arxiv.org/abs/1703.06870
研究动机
在 Mask R-CNN提出之前,计算机视觉领域中的目标检测和语义分割任务的效果都取得了明显提升。语义分割是在不区分实例的情况下对每个像素进行分类。而实例分割既是语义分割,又是一种检测的形式,其目标是将每个像素分类为一类固定的类别,而不区分对象。本文为了实现简洁、快速的实例分割,将目标检测方法与语义分割方法相结合,使用目标检测来得到边界框并对边界框中的目标进行分类,再对每个边界框中的物体进行像素级分类(生成二值掩码)。
Mask RCNN
The method, called Mask R-CNN, extends Faster R-CNN by adding a branch for predicting an object mask in parallel with the existing branch for bounding box recognition.
Mask RCNN是在Faster RCNN的基础上添加一个掩码分支(FCN),用于预测每个感兴趣区域(ROI)上的分割掩码,与现有的分类和边界框回归分支并行。掩模分支是应用于每个RoI的小FCN,预测像素级的分割掩码。
上图是Mask RCNN网络的框架图,其中“class box”所在分支是是Faster RCNN,下面的分支扩展的掩码分支。与Faster RCNN的的区别除了添加了掩码分支外,原本的RoIPool被替换成了RoIAlign,这可以减少定位误差,二者具体对比见下文。
另外,文中给出了两种结构的Mask分支,一种是打击FPN的,一种是不带的。结构如下:
不带FPN结构的mask分支与 class、box 分支共用 RoIAlign 部分。
而不带FPN结构的mask分支与class、box分支不共用RoIAlign部分。在训练过程中,对于class, box分支RoIAlign将RPN(Region Proposal Network)得到的Proposals池化到7x7大小,而对于Mask分支RoIAlign将Proposals池化到14x14大小。
RoIPool与RoIAlign
Faster R-CNN was not designed for pixel-to-pixel alignment between network inputs and outputs. This is most evident in how RoIPool, the de facto core operation for attending to instances, performs coarse spatial quantization for feature extraction.
在 Faster RCNN 中,用到的是RoIPool操作,其将RPN得到的Proposal池化到固定大小。这个过程在计算中涉及取整操作,导致定位有偏差(Mask RCNN论文中称为misalignment问题)。
下面以一个例子说明RoIPool的计算流程。
假设通过RPN得到了一个Proposal,它在原图上的左上角坐标是(10,10),右下角的坐标是 ( 124 , 124 ) ,对应要映射的特征层相对原图的步距为32,要求通过RoIPool得到的输出大小为 2 × 2 2\times 2 2×2,具体步骤如下:
- 将proposal映射到特征层上,左上角计算为 ( 10 32 , 10 32 ) (\frac {10}{32},\frac {10}{32}) (3210,3210),四舍五入取整的左上角坐标为 ( 0 , 0 ) (0,0) (0,0),右下角计算为 ( 124 32 , 124 32 ) (\frac {124}{32},\frac {124}{32}) (32124,32124),四舍五入取整的右下角坐标为 ( 4 , 4 ) (4,4) (4,4)。映射到特征图上为上面的黑色框部分(第0到第4行,第0到第4列)
- 因为RoIPool的期望输出大小为 2 × 2 2\times 2 2×2,但第一步映射到特征图上的大小为 5 × 5 5\times 5 5×5,不能均分,所以划分后的区域又大又小。如上图所示的划分方法,每个区域的大小都不一样。
- 对第二步划分后的每个区域中实施maxpool操作,得到固定大小的输出,如上图蓝色区域。
其中,第一步在计算proposal映射到特征图和第二步按输出对特征图上的proposal划分区域时都涉及取整操作,可能导致定位结果有偏差,进而影响后续的检测效果。
为了解决这个问题,Mask RCNN的作者提出了用RoIAlign来代替原本的RoIPool,以获得更精确的定位信息。
RoIAlign 的计算过程其实与RoIPool相似,但其计算位置和划分区域时并不进行取整,在最后计算输出时用双线性插值法。
为方便说明二者的差别,还是以上面的假设为例,说明RoIAlign的计算过程:
采样率是指每个子区域采样点的个数,为方便说明这里设置为1.
- 将proposal映射到特征层上,左上角坐标为 ( 10 32 , 10 32 ) (\frac {10}{32},\frac {10}{32}) (3210,3210),写成小数为 ( 0.3125 , 0.3125 ) (0.3125,0.3125) (0.3125,0.3125),不进行四舍五入,同理,右下角坐标映射为 ( 3.875 , 3.875 ) (3.875,3.875) (3.875,3.875)。得到的映射图为图中大的蓝色框区域。
- 因为RoIPool的期望输出大小为 2 × 2 2\times 2 2×2,所以将映射的Proposal划分为2x2四个子区域,是对映射区域的均分,不使用四舍五入取整,得到的结果入图中蓝色区域中的划分。
- 为每个字区域分配采样点,采样点需均匀分布在子区域内。这里设置每个子区域采样一个点,即子区域的中心点。计算每个子区域中每个采样点的值(具体是用双线性插值方法计算),每个子区域内所有样本点的均值作为此子区域的输出。
RoIAlign整个计算过程中不涉及取整操作,所以其得到的特征图与原始图像得以高度对齐。
作者也在文中提到,将RoIPool替换成RoIAlign后,分割的Mask准确率相对提升了10%到50%(见下图d),并且将预测Mask和class进行了解耦,解耦后也带来了很大的提升(见下图b)。
双线性插值
双线性插值(bilinear interpolation),又称为双线性内插。在数学上,双线性插值是有两个变量的插值函数的线性插值扩展,其核心思想是在两个方向分别进行一次线性插值。在数字图像和音频处理领域都有应用。
在图像处理中,双线性插值法考虑围绕未知像素的计算位置的 2 × 2 2\times 2 2×2 最近邻域的已知像素。然后对这4个像素进行加权平均,以得出其最终的内插值。
现对双线性插值的公式进行推导:
如下图,已知P的位置,求其像素值, 已知相邻 2 × 2 2\times 2 2×2 的像素区域对应位置和像素值,其中坐下角像素区域标号为11,左上角像素区域标号为12,右下角像素区域标号为21,右上角像素区域标号为22。
Q是对应像素的中心点
双线性插值其实就是目标像素值相邻四个像素的像素值加权和值。
当目标元素与某个相邻元素的距离越近,目标元素元素与该相邻像素的对角像素组成的矩形框面积大小就越大,该相邻像素对应的权重值就越大。
Mask Branch(FCN)
Mask分支分为带FPN和不带FPN两种,下图中左边是不带RPN的,右边是带RPN的,图中灰色部分为原Faster R-CNN预测box, class信息的分支,白色部分为Mask分支。
实际应用中,带FPN的Mask分支较常见,下图为其详细结构:
FCN是对每个像素针对每个类别都会预测一个分数,然后通过softmax得到每个类别的概率(不同类别之间存在竞争关系),哪个概率高就将该像素分配给哪个类别。但在Mask R-CNN中,作者将预测Mask和class进行了解耦,即对输入的RoI针对每个类别都单独预测一个Mask,最终根据box, cls分支预测的classes信息来选择对应类别的Mask(不同类别之间不存在竞争关系)。作者说解耦后带来了很大的提升。
另外还需要注意一个细节:训练网络的时候输入Mask分支的目标是由RPN提供的,即Proposals,但在预测的时候输入Mask分支的目标是由Fast R-CNN提供的(即预测的最终目标)。并且训练时采用的Proposals全部是Fast R-CNN阶段匹配到的正样本。
其他细节
Mask RCNN损失
Mask R-CNN的损失就是在Faster R-CNN的基础上加上了Mask分支上的损失,即:
Mask分支上的损失就是二值交叉熵损失(Binary Cross Entropy)。
如下图所示,假设通过RPN得到了一个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分支预测
在真正预测推理的时候,输入Mask分支的目标是由Fast R-CNN分支提供的。
如上图所示,首先,通过Fast R-CNN分支生成目标的边界框信息和类别预测结果。随后,将这些边界框信息输入到Mask分支中,生成目标的logits(未归一化的概率值)。根据Fast R-CNN分支提供的类别信息,从logits中提取出对应类别的Mask(形状为28x28,经过sigmoid激活函数处理后,其值范围在0到1之间)。接下来,使用双线性插值将Mask缩放到与预测边界框相同的尺寸,并将其映射到原始图像中的对应区域。为了将Mask转换为二值图,设定一个阈值(通常为0.5),将大于阈值的区域标记为前景,其余区域标记为背景。最终,对于每个检测到的目标,可以在原始图像中绘制出其边界框、类别标签以及对应的Mask信息。
网络搭建
- 骨干网络搭建,使用 ResNet50 作为骨干网络,并结合 FPN(Feature Pyramid Network)提取多尺度特征。
class Backbone(nn.Module):def __init__(self):super(Backbone, self).__init__()# 加载预训练的 ResNet50resnet = resnet50(pretrained=True)# 提取 ResNet 的前四个阶段(去掉最后的全连接层和平均池化层)self.layer0 = nn.Sequential(resnet.conv1, resnet.bn1, resnet.relu, resnet.maxpool)self.layer1 = resnet.layer1self.layer2 = resnet.layer2self.layer3 = resnet.layer3self.layer4 = resnet.layer4def forward(self, x):# 提取多尺度特征c1 = self.layer0(x) # [batch_size, 256, H/4, W/4]c2 = self.layer1(c1) # [batch_size, 512, H/8, W/8]c3 = self.layer2(c2) # [batch_size, 1024, H/16, W/16]c4 = self.layer3(c3) # [batch_size, 2048, H/32, W/32]return c1, c2, c3, c4
- 搭建FPN(Feature Pyramid Network),用于生成多尺度的特征金字塔。
class FPN(nn.Module):def __init__(self, in_channels_list, out_channels):super(FPN, self).__init__()# 定义 lateral 和 output 卷积层self.lateral_convs = nn.ModuleList()self.output_convs = nn.ModuleList()for in_channels in in_channels_list:self.lateral_convs.append(nn.Conv2d(in_channels, out_channels, kernel_size=1))self.output_convs.append(nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1))def forward(self, c1, c2, c3, c4):# 自顶向下生成特征金字塔p4 = self.lateral_convs[3](c4) # [batch_size, 256, H/32, W/32]p3 = self.lateral_convs[2](c3) + F.interpolate(p4, scale_factor=2, mode="nearest") # [batch_size, 256, H/16, W/16]p2 = self.lateral_convs[1](c2) + F.interpolate(p3, scale_factor=2, mode="nearest") # [batch_size, 256, H/8, W/8]p1 = self.lateral_convs[0](c1) + F.interpolate(p2, scale_factor=2, mode="nearest") # [batch_size, 256, H/4, W/4]# 对每个金字塔层进行输出卷积p4 = self.output_convs[3](p4)p3 = self.output_convs[2](p3)p2 = self.output_convs[1](p2)p1 = self.output_convs[0](p1)return p1, p2, p3, p4
- 搭建RPN(Region Proposal Network),用于生成候选区域。
class RPN(nn.Module):def __init__(self, in_channels, num_anchors):super(RPN, self).__init__()# 定义 RPN 的卷积层self.conv = nn.Conv2d(in_channels, in_channels, kernel_size=3, padding=1)# 定义分类头和回归头self.cls_logits = nn.Conv2d(in_channels, num_anchors, kernel_size=1)self.bbox_pred = nn.Conv2d(in_channels, num_anchors * 4, kernel_size=1)def forward(self, x):# 前向传播x = self.conv(x)logits = self.cls_logits(x) # 分类分数bbox_deltas = self.bbox_pred(x) # 边界框偏移量return logits, bbox_deltas
- 搭建RoIAlign(Region of Interest Align),用于将候选区域映射到固定大小的特征图。
class RoIAlign(nn.Module):def __init__(self, output_size):super(RoIAlign, self).__init__()self.output_size = output_sizedef forward(self, features, rois):# 使用 RoIAlign 操作return ops.roi_align(features, rois, self.output_size)
- 搭建Head Network(头部网络),包括分类头、边界框回归头和掩码头。
class RoIAlign(nn.Module):def __init__(self, output_size):super(RoIAlign, self).__init__()self.output_size = output_sizedef forward(self, features, rois):# 使用 RoIAlign 操作return ops.roi_align(features, rois, self.output_size)
class MaskRCNNHead(nn.Module):def __init__(self, in_channels, num_classes):super(MaskRCNNHead, self).__init__()# 定义卷积层self.conv1 = nn.Conv2d(in_channels, 256, kernel_size=3, padding=1)self.conv2 = nn.Conv2d(256, 256, kernel_size=3, padding=1)self.conv3 = nn.Conv2d(256, 256, kernel_size=3, padding=1)# 定义输出卷积层self.mask_pred = nn.Conv2d(256, num_classes, kernel_size=1)def forward(self, x):# 前向传播x = torch.relu(self.conv1(x))x = torch.relu(self.conv2(x))x = torch.relu(self.conv3(x))masks = self.mask_pred(x) # 掩码预测return masks
- 完整的Mask RCNN模型:
class MaskRCNN(nn.Module):def __init__(self, num_classes):super(MaskRCNN, self).__init__()# Backbone + FPNself.backbone = Backbone()self.fpn = FPN(in_channels_list=[256, 512, 1024, 2048], out_channels=256)# RPNself.rpn = RPN(in_channels=256, num_anchors=9)# RoIAlignself.roi_align = RoIAlign(output_size=7)# Head Networkself.fast_rcnn_head = FastRCNNHead(in_channels=256, num_classes=num_classes)self.mask_rcnn_head = MaskRCNNHead(in_channels=256, num_classes=num_classes)def forward(self, x):# 提取特征c1, c2, c3, c4 = self.backbone(x)p1, p2, p3, p4 = self.fpn(c1, c2, c3, c4)# RPN 生成候选区域rpn_logits, rpn_bbox_deltas = self.rpn(p4)# RoIAlignrois = self.generate_rois(rpn_bbox_deltas) # 生成候选区域roi_features = self.roi_align(p4, rois)# Head Networkcls_logits, bbox_deltas = self.fast_rcnn_head(roi_features)masks = self.mask_rcnn_head(roi_features)return cls_logits, bbox_deltas, masksdef generate_rois(self, bbox_deltas):# 生成候选区域(这里简化实现)return torch.rand(10, 4) # 假设生成 10 个候选区域
创新点与不足
Mask RCNN在Faster R-CNN的基础上增加了一个并行的Mask分支,用于生成每个目标的像素级分割掩码,实现了目标检测与实例分割的统一框架。用RoIAlign替代RoIPool,解决了特征图与原始图像之间的空间不对齐问题,显著提升了分割精度。另外,Mask分支的计算开销小,且模型设计灵活,可扩展至其他任务(如人体姿态估计、关键点检测)。
但由于增加了Mask分支和RoIAlign操作,推理速度较慢,难以满足实时性要求高的场景。而且对于小目标或密集目标,Mask分支的分割精度可能下降,尤其是当目标边界不清晰时。
总结
Mask R-CNN通过结合目标检测和像素级分割,实现了高效的实例分割,其核心创新在于引入Mask分支和RoIAlign层。Mask R-CNN的实现流程分为以下几个步骤:首先,通过主干网络(如ResNet-FPN)提取多尺度特征图;接着,利用区域建议网络(RPN)生成候选区域;然后,通过RoIAlign技术将候选区域映射到特征图上,并提取固定大小的特征;最后,分别通过分类分支、回归分支和Mask分支生成目标的类别、边界框和像素级掩码。尽管存在计算复杂度高、对小目标处理能力有限等不足,但其高精度和灵活性使其在计算机视觉领域得到了广泛应用。后续研究(如PointRend、CondInst等)针对其不足进行了进一步优化,推动了实例分割技术的发展。