[代码解析]Mask R-CNN介绍与实现(转)
文章来源
简介
论文地址:Mask R-CNN
源代码:matterport - github
代码源于matterport的工作组,可以在github上fork它们组的工作。
软件必备
复现的Mask R-CNN是基于Python3,Keras,TensorFlow。
- Python 3.4+
- TensorFlow 1.3+
- Keras 2.0.8+
- Jupyter Notebook
- Numpy, skimage, scipy
建议配置一个高版本的Anaconda3+TensorFlow-GPU版本。
Mask R-CNN论文回顾
Mask R-CNN(简称MRCNN)是基于R-CNN系列、FPN、FCIS等工作之上的,MRCNN的思路很简洁:Faster R-CNN针对每个候选区域有两个输出:种类标签和bbox的偏移量。那么MRCNN就在Faster R-CNN的基础上通过增加一个分支进而再增加一个输出,即物体掩膜(object mask
)。
先回顾一下Faster R-CNN, Faster R-CNN主要由两个阶段组成:区域候选网络(Region Proposal Network,RPN)和基础的Fast R-CNN模型。
RPN用于产生候选区域
Fast R-CNN通过RoIPool层对每个候选区域提取特征,从而实现目标分类和bbox回归
MRCNN采用和Faster R-CNN相同的两个阶段,具有相同的第一层(即RPN),第二阶段,除了预测种类和bbox回归,并且并行的对每个RoI预测了对应的二值掩膜(binary mask
)。示意图如下:
这样做可以将整个任务简化为mulit-stage pipeline,解耦了多个子任务的关系,现阶段来看,这样做好处颇多。
主要工作
损失函数的定义
依旧采用的是多任务损失函数,针对每个每个RoI定义为
掩膜分支针对每个RoI产生一个的输出,即K个分辨率为的二值的掩膜,为分类物体的种类数目。依据预测类别分支预测的类型,只将第的二值掩膜输出记为。
掩膜分支的损失计算如下示意图:
- mask branch 预测个种类的二值掩膜输出
- 依据种类预测分支(Faster R-CNN部分)预测结果:当前RoI的物体种类为
- 第个二值掩膜输出就是该RoI的损失
对于预测的二值掩膜输出,我们对每个像素点应用sigmoid函数,整体损失定义为平均二值交叉损失熵。
引入预测个输出的机制,允许每个类都生成独立的掩膜,避免类间竞争。这样做解耦了掩膜和种类预测。不像是FCN的方法,在每个像素点上应用softmax函数,整体采用的多任务交叉熵,这样会导致类间竞争,最终导致分割效果差。
掩膜表示到RoIAlign层
在Faster R-CNN上预测物体标签或bbox偏移量是将feature map压缩到FC层最终输出vector,压缩的过程丢失了空间上(平面结构)的信息,而掩膜是对输入目标做空间上的编码,直接用卷积形式表示像素点之间的对应关系那是最好的了。
输出掩膜的操作是不需要压缩输出vector,所以可以使用FCN(Full Convolutional Network),不仅效率高,而且参数量还少。为了更好的表示出RoI输入和FCN输出的feature之间的像素对应关系,提出了RoIAlign层。
先回顾一下RoIPool层:
其核心思想是将不同大小的RoI输入到RoIPool层,RoIPool层将RoI量化成不同粒度的特征图(量化成一个一个bin),在此基础上使用池化操作提取特征。
下图是SPPNet内对RoI的操作,在Faster R-CNN中只使用了一种粒度的特征图:
平面示意图如下:
这里面存在一些问题,在上面量操作上,实际计算中是使用的是,的量化的步长,是舍入操作(rounding
)。这套量化舍入操作在提取特征时有着较好的鲁棒性(检测物体具有平移不变性等),但是这很不利于掩膜定位,有较大负面效果。
针对这个问题,提出了RoIAlign层:避免了对RoI边界或bin的量化操作,在扩展feature map时使用双线性插值算法。这里实现的架构要看FPN论文:
一开始的Faster R-CNN是基于最上层的特征映射做分割和预测的,这会丢失高分辨下的信息,直观的影响就是丢失小目标检测,对细节部分丢失不敏感。受到SSD的启发,FPN也使用了多层特征做预测。这里使用的top-down的架构,是将高层的特征反卷积带到低层的特征(即有了语义,也有精度),而在MRCNN论文里面说的双线性差值算法就是这里的top-down反卷积是用的插值算法。
总结
MRCNN有着优异的效果,除去了掩膜分支的作用,很大程度上是因为基础特征网络的增强,论文使用的是ResNeXt101+FPN的top-down组合,有着极强的特征学习能力,并且在实验中夹杂这多种工程调优技巧。
但是吧,MRCNN的缺点也很明显,需要大的计算能力并且速度慢,这离实际应用还是有很长的路,坐等大神们发力!
如何使用代码
项目的源代码地址为:github/Mask R-CNN
满足运行环境
- Python 3.4+
- TensorFlow 1.3+
- Keras 2.0.8+
- Jupyter Notebook
- Numpy, skimage, scipy, Pillow(安装Anaconda3直接完事)
- cv2
下载代码
linux环境下直接clone到本地
git clone https://github.com/matterport/Mask_RCNN.git
Windows下下载代码即可,地址在上面
下载模型在COCO数据集上预训练权重(
mask_rcnn_coco.h5
),下载地址releasses Page.如果需要在COCO数据集上训练或测试,需要安装
pycocotools
,clone
下来,make
生成对应的文件,拷贝下工程目录下即可(方法可参考下面repos内的README.md
文件)。- Linux: https://github.com/waleedka/coco
- Windows: https://github.com/philferriere/cocoapi. You must have the Visual C++ 2015 build tools on your path (see the repo for additional details)
如果使用COCO数据集,需要:
- pycocotools (即第4条描述的)
- MS COCO Dataset。2014的训练集数据
- COCO子数据集,5K的minival和35K的validation-minus-minival。(这两个数据集下载比较慢,没有贴原地址,而是我的优快云地址,分不够下载的可以私信我~)
下面的代码分析运行环境都是jupyter。
代码分析-数据预处理
项目源代码:matterport - github
inspect_data.ipynb展示了准备训练数据的预处理步骤.
导包
导入的coco包需要从coco/PythonAPI上下载操作数据代码,并在本地使用make
指令编译.将生成的pycocotools
拷贝至工程的主目录下,即和该inspect_data.ipynb
文件同一目录。
import os
import sys
import itertools
import math
import logging
import json
import re
import random
from collections import OrderedDict
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import matplotlib.lines as lines
from matplotlib.patches import Polygon
import utils
import visualize
from visualize import display_images
import model as modellib
from model import log
%matplotlib inline
ROOT_DIR = os.getcwd()
# 选择任意一个代码块
# import shapes
# config = shapes.ShapesConfig() # 使用代码创建数据集,后面会有介绍
# MS COCO 数据集
import coco
config = coco.CocoConfig()
COCO_DIR = "/root/模型复现/Mask_RCNN-master/coco" # COCO数据存放位置
加载数据集
COCO数据集的训练集内有82081张图片,共81类。
# 这里使用的是COCO
if config.NAME == 'shapes':
dataset = shapes.ShapesDataset()
dataset.load_shapes(500, config.IMAGE_SHAPE[0], config.IMAGE_SHAPE[1])
elif config.NAME == "coco":
dataset = coco.CocoDataset()
dataset.load_coco(COCO_DIR, "train")
# Must call before using the dataset
dataset.prepare()
print("Image Count: {}".format(len(dataset.image_ids)))
print("Class Count: {}".format(dataset.num_classes))
for i, info in enumerate(dataset.class_info):
print("{:3}. {:50}".format(i, info['name']))
>>>
>>>
loading annotations into memory...
Done (t=7.68s)
creating index...
index created!
Image Count: 82081
Class Count: 81
0. BG
1. person
2. bicycle
...
77. scissors
78. teddy bear
79. hair drier
80. toothbrush
随机找几张照片看看:
# 加载和展示随机几张照片和对应的mask
image_ids = np.random.choice(dataset.image_ids, 4)
for image_id in image_ids:
image = dataset.load_image(image_id)
mask, class_ids = dataset.load_mask(image_id)
visualize.display_top_masks(image, mask, class_ids, dataset.class_names)
Bounding Boxes(bbox)
这里我们不使用数据集本身提供的bbox坐标数据,取而代之的是通过mask计算出bbox,这样可以在不同的数据集下对bbox使用相同的处理方法。因为我们是从mask上计算bbox,相比与从图片计算bbox转换来说,更便于放缩,旋转,裁剪图像。
# Load random image and mask.
image_id = random.choice(dataset.image_ids)
image = dataset.load_image(image_id)
mask, class_ids = dataset.load_mask(image_id)
# Compute Bounding box
bbox = utils.extract_bboxes(mask)
# Display image and additional stats
print("image_id ", image_id, dataset.image_reference(image_id))
log("image", image)
log("mask", mask)
log("class_ids", class_ids)
log("bbox", bbox)
# Display image and instances
visualize.display_instances(image, bbox, mask, class_ids, dataset.class_names)
>>>
>>>
image_id 41194 http://cocodataset.org/#explore?id=190360
image shape: (428, 640, 3) min: 0.00000 max: 255.00000
mask shape: (428, 640, 5) min: 0.00000 max: 1.00000
class_ids shape: (5,) min: 1.00000 max: 59.00000
bbox shape: (5, 4) min: 1.00000 max: 640.00000
调整图片大小
因为训练时是批量处理的,每次batch要处理多张图片,模型需要一个固定的输入大小。故将训练集的图片放缩到一个固定的大小(1024×1024)
,放缩的过程要保持不变的宽高比,如果照片本身不是正方形,那边就在边缘填充0.(这在R-CNN论文里面论证过)。
需要注意的是:原图片做了放缩,对应的mask也需要放缩,因为我们的bbox是依据mask计算出来的,这样省了修改程序了~
# Load random image and mask.
image_id = np.random.choice(dataset.image_ids, 1)[0]
image = dataset.load_image(image_id)
mask, class_ids = dataset.load_mask(image_id)
original_shape = image.shape
# 调整到固定大小
image, window, scale, padding = utils.resize_image(
image,
min_dim=config.IMAGE_MIN_DIM,
max_dim=config.IMAGE_MAX_DIM,
padding=config.IMAGE_PADDING)
mask = utils.resize_mask(mask, scale, padding) # mask也要放缩
# Compute Bounding box
bbox = utils.extract_bboxes(mask)
# Display image and additional stats
print("image_id: ", image_id, dataset.image_reference(image_id))
print("Original shape: ", original_shape)
log("image", image)
log("mask", mask)
log("class_ids", class_ids)
log("bbox", bbox)
# Display image and instances
visualize.display_instances(image, bbox, mask, class_ids, dataset.class_names)
>>>
>>>
image_id: 6104 http://cocodataset.org/#explore?id=139889
Original shape: (426, 640, 3)
image shape: (1024, 1024, 3) min: 0.00000 max: 255.00000
mask shape: (1024, 1024, 2) min: 0.00000 max: 1.00000
class_ids shape: (2,) min: 24.00000 max: 24.00000
bbox shape: (2, 4) min: 169.00000 max: 917.00000
原图片从(426, 640, 3)
放大到(1024, 1024, 3)
,图片的上下两端都填充了0(黑色的部分):
Mini Mask
训练高分辨率的图片时,表示每个目标的二值mask也会非常大。例如,训练一张1024×1024
的图片,其目标物体对应的mask需要1MB的内存(用boolean变量表示单点),如果1张图片有100个目标物体就需要100MB。讲道理,如果是五颜六色就算了,但实际上表示mask的图像矩阵上大部分都是0,很浪费空间。
为了节省空间同时提升训练速度,我们优化mask的表示方式,不直接存储那么多0,而是通过存储有值坐标的相对位置来压缩表示数据的内存,原理和压缩算法差类似。
- 我们存储在对象边界框内(bbox内)的mask像素,而不是存储整张图片的mask像素,大多数物体相对比于整张图片是较小的,节省存储空间是通过少存储目标周围的0实现的。
- 将mask调整到小尺寸
56×56
,对于大尺寸的物体会丢失一些精度,但是大多数对象的注解并不是很准确,所以大多数情况下这些损失是可以忽略的。(可以在config类中设置mini mask的size。)
说白了就是在处理数据的时候,我们先利用标注的mask信息计算出对应的bbox框,而后利用计算的bbox框反过来改变mask的表示方法,目的就是操作规范化,同时降低存储空间和计算复杂度。
image_id = np.random.choice(dataset.image_ids, 1)[0]
# 使用load_image_gt方法获取bbox和mask
image, image_meta, bbox, mask = modellib.load_image_gt(
dataset, config, image_id, use_mini_mask=False)
log("image"<