原文:Ultimate Guide: Building a Mask R-CNN Model for Detecting Car Damage (with Python codes)

[mask_rcnn_damage_detection]

计算机视觉的应用越来越令人惊奇. 从视频目标检测,到计算人流量. 似乎对于计算机视觉来说没有很大的挑战性.
计算机视觉一个很具吸引力的应用是,场景中每个像素的识别,以用于不同的,非常有用的目标应用.

这里针对一个应用场景:构建一个 Mask R-CNN 模型,以检测汽车表面的损坏区域.
其可用于,保险公司针对用户上传的汽车损坏图片进行快速理赔;或者,二手车场景中汽车表面摩擦情况;又或者是,二手车抵押贷款等.

<h2>1. Mask R-CNN 简介</h2>

Mask R-CNN 是一种实例分割模型,识别每个目标实例的像素级位置.
"实例分割" 意味着,对场景内的每个独立目标实例进行分割,不管实例是否属于相同的目标类别. 如识别每个 cars,persons 等.
例如下图,COCO 数据集 训练的 Mask R-CNN GIF 图.

Mask R-CNN 不同于传统的 Faster R-CNN 等目标检测模型,除了识别目标实例的类别和其对应的边界框位置,Mask R-CNN 还会对目标实例边界框内目标实例进行像素级识别. 其可用于,如:

  • 自动驾驶车辆需要准确识别道路的精确像素位置;其它车辆也需要精确像素位置,以避免碰撞.</p>
  • <p>机器人需要识别待抓起的目标的精确像素位置(Amazon 的无人配送机也会用到).</p>

<p>COCO 数据集上 Mask R-CNN 模型训练的最简单方式是采用 Tensorflow Object Detection API.

Tensorflow - 目标检测 API 使用 Using Tensorflow Object Detection to do Pixel Wise Classification

<h2>2. Mask R-CNN 工作原理</h2>

Mask R-CNN 可以看作是 Faster R-CNN 目标检测(类别 + 边界框) 和 FCN(像素级分割) 的结合. 如图:

Mask R-CNN 理解上很简单:Faster R-CNN 对于每个候选目标有两个输出,类别标签和边界框偏移;新增的 FCN 分支则输出目标的 Mask. 二值 Mask 表示在边界框内的每个像素是否属于目标实例.
新增的 Mask 输出不同于类别和边界框输出,其需要提取更精细的目标实例的空间层次. 因此,Mask R-CNN 采用了 FCN 网络:

FCN 是语义分割的流行算法. 其采用多个 Conv 和 Max-pool 层来首先将输入图片尺寸压缩到原始尺寸的 1/32.;然后在压缩后的分辨率粒度进行像素类别预测;最后采用上采样和 deconv 层将压缩尺寸恢复到原始的图片尺寸.

简单来说,Mask R-CNN 可以说是 Faster R-CNN 和 FCN 两个网络的组合. 模型的 loss 函数是分类,边界框和 Mask 预测的总和.

Mask R-CNN 还进行了额外处理,以提升其比 FCN 更精确的像素识别. 具体细节参考论文 Mask R-CNN.

<h2>3. 构建车辆表面损坏检测的 Mask R-CNN 模型</h2>

虽然 Tensorflow - 目标检测 API 也提供了 Mask R-CNN 模型构建方法,但这里还是推荐基于 matterport/Mask_RCNN 构建 Mask R-CNN 模型.

<h3>3.1 数据收集</h3>

这里从 Google 收集了 66 张表面损坏的汽车图片(50 张训练,16 张验证). 如:

<h3>3.2 数据标注</h3>

Mask R-CNN 模型的训练,需要手工标注图片中汽车标签损坏区域. 这里采用 VGG Image Annotator — v 1.0.6 标注工具html 版本.
多边形 Mask 标注,如:

数据标注后,可以下载标注结果,保存为 json 格式.

这里的图片和标注数据集 - damage_detection_images.

<h3>3.3 模型训练</h3>

克隆 Matterport Mask R-CNN’ repository— https://github.com/matterport/Mask_RCNN 后,编写图片和标注数据加载函数.

主要基于 Matterport 提供的 balloon.py 进行修改,添加加载图片和标注数据函数,并添加到 CustomDataset 类.

class CustomDataset(utils.Dataset):

    def load_custom(self, dataset_dir, subset):
        """Load a subset of the Balloon dataset.
        dataset_dir: Root directory of the dataset.
        subset: Subset to load: train or val
        """
        # Add classes. We have only one class to add.
        self.add_class("damage", 1, "damage")

        # Train or validation dataset?
        assert subset in ["train", "val"]
        dataset_dir = os.path.join(dataset_dir, subset)

        # Load annotations
        # VGG Image Annotator saves each image in the form:
        # { 'filename': '28503151_5b5b7ec140_b.jpg',
        #   'regions': {
        #       '0': {
        #           'region_attributes': {},
        #           'shape_attributes': {
        #               'all_points_x': [...],
        #               'all_points_y': [...],
        #               'name': 'polygon'}},
        #       ... more regions ...
        #   },
        #   'size': 100202
        # }
        # We mostly care about the x and y coordinates of each region
        annotations1 = json.load(open(os.path.join(dataset_dir, "via_region_data.json")))
        # print(annotations1)
        annotations = list(annotations1.values())  # don't need the dict keys

        # The VIA tool saves images in the JSON even if they don't have any
        # annotations. Skip unannotated images.
        annotations = [a for a in annotations if a['regions']]

        # Add images
        for a in annotations:
            # print(a)
            # Get the x, y coordinaets of points of the polygons that make up
            # the outline of each object instance. There are stores in the
            # shape_attributes (see json format above)
            polygons = [r['shape_attributes'] for r in a['regions'].values()]

            # load_mask() needs the image size to convert polygons to masks.
            # Unfortunately, VIA doesn't include it in JSON, so we must read
            # the image. This is only managable since the dataset is tiny.
            image_path = os.path.join(dataset_dir, a['filename'])
            image = skimage.io.imread(image_path)
            height, width = image.shape[:2]

            self.add_image(
                "damage",  ## for a single class just add the name here
                image_id=a['filename'],  # use file name as a unique image id
                path=image_path,
                width=width, height=height,
                polygons=polygons)

    def load_mask(self, image_id):
        """Generate instance masks for an image.
       Returns:
        masks: A bool array of shape [height, width, instance count] with
            one mask per instance.
        class_ids: a 1D array of class IDs of the instance masks.
        """
        # If not a balloon dataset image, delegate to parent class.
        image_info = self.image_info[image_id]
        if image_info["source"] != "damage":
            return super(self.__class__, self).load_mask(image_id)

        # Convert polygons to a bitmap mask of shape
        # [height, width, instance_count]
        info = self.image_info[image_id]
        mask = np.zeros([info["height"], info["width"], len(info["polygons"])],
                        dtype=np.uint8)
        for i, p in enumerate(info["polygons"]):
            # Get indexes of pixels inside the polygon and set them to 1
            rr, cc = skimage.draw.polygon(p['all_points_y'], p['all_points_x'])
            mask[rr, cc, i] = 1

        # Return mask, and array of class IDs of each instance. Since we have
        # one class ID only, we return an array of 1s
        return mask.astype(np.bool), np.ones([mask.shape[-1]], dtype=np.int32)
完整 python 脚本 - custom.py

inspect_custom_data.ipynb 在图片上可视化 mask 标注,如图:

模型训练时,采用 COCO 数据集上预训练模型作为断点,进行迁移学习.

## Train a new model starting from pre-trained COCO weights
python3 custom.py train --dataset=/path/to/datasetfolder --weights=coco

## Resume training a model that you had trained earlier
python3 custom.py train --dataset=/path/to/datasetfolder --weights=last

单张 GPU,模型训练 10 个 epochs,耗时 20-30 分钟.

<h3>3.4 模型验证</h3>

inspect_custom_weights.ipynb 可以对模型权重进行可视化检查.
在该 notebook 中设置模型训练的最后一个断点模型文件. 有助于对权重和偏置的分布进行安全性检查. 例如:

<h3>3.5 图片测试与预测</h3>

inspect_custom_model.ipynb 提供了对 train/val 数据集中的图片进行模型测试,并可视化预测结果. 例如:

结果可以看出,构建的 Mask R-CNN 模型很好的检测除了汽车表面的损坏区域. 深度学习真强大!

Mask R-CNN 是目标检测的进化版本,检测精度更高.

<h2>References</h2>

Last modification:October 9th, 2018 at 09:31 am