Mask R-CNN
论文链接:https://arxiv.org/pdf/1703.06870v3.pdf ICCV2017的一篇文章
主要贡献: 在faster rcnn的基础上扩展出mask 分支用于实例分割,提出roi align弥补roi pool的misalignment,在计算mask分支的loss的时候predict a binary mask for each class independently, without competition among classes,而不是采用FCN那种的多分类的交叉熵损失。
整体的结构图如下图,简单来说就是对rpn选出的proposal经过ROIAlign,接着经过mask分支进行类别的预测。
mask rcnn根据是否采用FPN采用了两种结构
左边是不加rpn的时候的ResNet C4,即在resnet的第四个block进行roialign的操作,接着经过resnet的第五个block,具体实现的时候结构有点不一样(https://github.com/open-mmlab/mmdetection/blob/master/configs/mask_rcnn_r50_caffe_c4_1x.py) 对于resnet C5输出的feature一个是用于faster rcnn的分类与回归,mask分支首先是上采样到14*14接着在conv变成[nums_pos,80,14,14],这里的nums_pos是rcnn对rpn选出的proposal进行target_proposal之后的所有正样本的数目,右边是加了FPN之后的,其实也是类似的,在将roi根据area分配到FPN的不同level进行roialign之后得到的feature分为两个分支,需要注意的是上面的分支用的roialign是RoIAlign(out_size=(7, 7), sample_num=2, use_torchvision=False),所以得到的feature的size是7*7的,而下面的分支用到的roialign是RoIAlign(out_size=(14, 14), sample_num=2, use_torchvision=False),所以得到的feature的size是14*14的,至于为什么会不同还不知道。。。上面的分支同样也是用于faster rcnn的分类与回归,下面的为mask 分支,首先是经过四个14*14*256的conv,至于这里为什么是四个也还不知道。。。,之后也是进行上采样然后在conv成最后的[nums_pos,80,28,28]
对于faster rcnn中原来的roi pool, 其存在两次整数化,此时的候选框已经和最开始回归出来的位置有一定的偏差,这个偏差会影响检测或者分割的准确度。在论文里,作者把它总结为“不匹配问题”(misalignment)。(来自https://zhuanlan.zhihu.com/p/37998710,讲的比较清楚,可以看看)
于是有了roi align
假设roi被分成2*2的bin,每个bin中有四个采样点,每个采样点的值通过线性插值获得,从文中的实验来时四个采样点效果最好,而且就算是一个采样点的效果也和四个的差不多。
以下是来自https://zhuanlan.zhihu.com/p/65321082的一张图可以更好地理解roi align的必要性,红色框和黄色框都框住了目标,但是经过roi pool之后得到的feature map是同一个,所以这个时候网络要对同一个feature去拟合两个框,这就给训练造成了麻烦。
mask rcnn的损失由三部分组成
前两部分就是faster rcnn里面的回归月分类损失,重点在于最后的Lmask.首先是对就是rcnn对rpn选出的proposal进行target_proposal之后的所有正样本进行mask_target(代码来自:https://github.com/open-mmlab/mmdetection)
def mask_target_single(pos_proposals, pos_assigned_gt_inds, gt_masks, cfg):
"""
gt_masks[nums_pos, h,w] #这里的nums_pos是rcnn对rpn选出的proposal进行target_proposal之后的所有正样本的数目,h,w对应着原图整幅图的大小标注
pos_proposals[nums_pos,4] #,pos_proposals就是rcnn对rpn选出的proposal进行target_proposal之后的所有正样本的box坐标
pos_assigned_gt_inds[nums_pos] #表示pos_proposals中的每个box对应gt_masks中的那个gt
"""
mask_size = _pair(cfg.mask_size) #对应到油FPN的mask rcnn中值就是[28,28]
num_pos = pos_proposals.size(0)
mask_targets = []
if num_pos > 0:
proposals_np = pos_proposals.cpu().numpy() #在cpu上面计算???
_, maxh, maxw = gt_masks.shape
#pos_proposas不能超过图像边界
proposals_np[:, [0, 2]] = np.clip(proposals_np[:, [0, 2]], 0, maxw - 1)
proposals_np[:, [1, 3]] = np.clip(proposals_np[:, [1, 3]], 0, maxh - 1)
pos_assigned_gt_inds = pos_assigned_gt_inds.cpu().numpy()
for i in range(num_pos):
gt_mask = gt_masks[pos_assigned_gt_inds[i]]
bbox = proposals_np[i, :].astype(np.int32)
x1, y1, x2, y2 = bbox
w = np.maximum(x2 - x1 + 1, 1)
h = np.maximum(y2 - y1 + 1, 1)
# mask is uint8 both before and after resizing
# mask_size (h, w) to (w, h)
target = mmcv.imresize(gt_mask[y1:y1 + h, x1:x1 + w], #用proposals_np在原图中截取相应区域,最后直接进行resize其实就是调用cv2.resize,这样简单粗暴的方式不会影响精度吗
mask_size[::-1])
mask_targets.append(target)
mask_targets = torch.from_numpy(np.stack(mask_targets)).float().to(
pos_proposals.device)
else:
mask_targets = pos_proposals.new_zeros((0, ) + mask_size)
return mask_targets #[nums_pos,28,28]
接着与gt计算Lmask
def mask_cross_entropy(pred, target, label, reduction='mean', avg_factor=None):
"""
pred[nums_pos,81,28,28] #对应着mask分支的输出
target[nums_pos,28,28] #对应着mask_target之后的结果
label[nums_pos] #表示target中每个pos_proposal对应的真实label
"""
# TODO: handle these two reserved arguments
assert reduction == 'mean' and avg_factor is None
num_rois = pred.size()[0]
inds = torch.arange(0, num_rois, dtype=torch.long, device=pred.device)
pred_slice = pred[inds, label].squeeze(1) #在pred中取得label对应的那个mask得到[nums_pos,28,28],本来对于pred每个pos_proposal都有[1,81,28,28]的预测,但是在计算损失的时候只取该pos_proposal真实对应的那个类的mask[1,1,28,28],这样就避免了类间竞争
return F.binary_cross_entropy_with_logits( #其实就是sigmoid+二分类的交叉熵
pred_slice, target, reduction='mean')[None]
最后作者的实验证明FPN,roialign,sigmoid+二分类的交叉熵对于精度都是有很大的提升的。