Instance Segmentation「Mask-RCNN」をKerasで学習させる

# 0. Introduction
頃日、Semantic Segmentationも束の間、さらにその上位種であるInstance Segmentationが隆盛を極めている。
これはYOLOのようなBBOXによる検出に加え、BBOX内のSegmentationも行うマルチタスクラーニングの一種である。

マルチタスクラーニング化することで、よりIOUの精度を高められたことが特徴の1つである。
今回は、このInstance Segmentationで最も有名だと思われるMask-RCNNを使えるようになりたいと考える。
なお、Mask-RCNNはSOTAではない。

![street.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95636/d96be49c-68bd-2f4e-bb34-d254bdc8eb48.png)

# 1. Installation
まずは以下のような結果を得られよう環境構築をして行く。
![balloon_color_splash.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95636/23a35815-7045-5199-8f5a-638c689b4fdb.gif)

MSCOCOデータセットは重いため今回は使用しない。

## Pull
Kerasで実装されたMask-RCNNを（以下リポジトリ）を用いる。
https://github.com/matterport/Mask_RCNN

まずはクローンする。

```bash
$ git clone https://github.com/matterport/Mask_RCNN
$ cd Mask_RCNN
```

## Environment
以下の環境を整備する。多少、異なっていても動くとは思う。

```
CUDA=10.0
CUDNN=7.6.2
python=3.7
numpy
scipy
Pillow
cython
matplotlib
scikit-image
tensorflow>=1.3.0
keras>=2.0.8
opencv-python
h5py
imgaug
```

一先ずリポジトリに沿って環境を整えて行く

``` bash
$ pip install -r requirements.txt
$ python setup.py install
```

特にエラー等が生じなければ、これで環境は整うはずである。

## Download
次にデータセット等である。ルートフォルダを`Mask_RCNN`内とする。

データセットフォルダを作成し、データセット及び学習済みの重みをダウンロードする。

``` bash
$ mkdir datasets
$ cd datasets/
$ wget https://github.com/matterport/Mask_RCNN/releases/download/v2.1/balloon_dataset.zip
$ wget https://github.com/matterport/Mask_RCNN/releases/download/v2.1/mask_rcnn_balloon.h5
```

次に解凍する。

``` bash
$ unzip balloon_dataset.zip
```

解凍すると、以下のようなフォルダ構成となる。

```
baloon
|- train
  |- xxx.jpg
  |- via_region_data.json
  |- ...
|- val
  |- xxx.jpg
  |- via_region_data.json
  |- ...
```

これにて環境構築はおしまい。

# 2. Prediction
環境構築で整備したデータセット及び重みを用いて識別を行っていく。

まず、ルートフォルダから以下に移動

```bash
$ cd samples/balloon/
```

次に以下を実行

``` bash
$ python balloon.py splash --weights=../../datasets/mask_rcnn_balloon.h5 --image=../../datasets/balloon/val/5603212091_2dfe16ea72_b.jpg
```

そうすると、以下のようなログが表示される。

```
Configurations:
BACKBONE                       resnet101
BACKBONE_STRIDES               [4, 8, 16, 32, 64]
BATCH_SIZE                     1
BBOX_STD_DEV                   [0.1 0.1 0.2 0.2]
COMPUTE_BACKBONE_SHAPE         None
DETECTION_MAX_INSTANCES        100
DETECTION_MIN_CONFIDENCE       0.9
DETECTION_NMS_THRESHOLD        0.3
FPN_CLASSIF_FC_LAYERS_SIZE     1024
GPU_COUNT                      1
GRADIENT_CLIP_NORM             5.0
IMAGES_PER_GPU                 1
IMAGE_CHANNEL_COUNT            3
IMAGE_MAX_DIM                  1024
IMAGE_META_SIZE                14
IMAGE_MIN_DIM                  800
IMAGE_MIN_SCALE                0
IMAGE_RESIZE_MODE              square
IMAGE_SHAPE                    [1024 1024    3]
LEARNING_MOMENTUM              0.9
LEARNING_RATE                  0.001
...
Saved to  splash_20200304T125953.png
...
```

処理が全て終わると同フォルダに`splash_20200304T125953.png`が保存されている。
これを表示すると、上手くバルーンのみが抽出されていることが確認できる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95636/16aa1d9c-9269-0647-87bb-51382d489cce.png)

# 3. Train
既存のバルーンのデータセットで学習を行う。

## Fix depending on `Keras >= 2.3.0`
`Keras==2.3.0`を用いているとデフォルトだと動かないのでコードを修正する。
まずインポート先を環境変数からローカルフォルダに変更する。

`./sample/balloon/`を以下のように変更する。

``` python:balloon.py
...
# Root directory of the project
ROOT_DIR = os.path.abspath("../../")

# Import Mask RCNN
#sys.path.append(ROOT_DIR)  # To find local version of the library
#from mrcnn.config import Config
#from mrcnn import model as modellib, utils

# Root directory of the project
sys.path.append(os.path.abspath("../../mrcnn"))
# Import Mask RCNN
import model as modellib, utils
from config import Config

# Path to trained weights file
COCO_WEIGHTS_PATH = os.path.join(ROOT_DIR, "mask_rcnn_coco.h5")

# Directory to save logs and model checkpoints, if not provided
# through the command line argument --logs
DEFAULT_LOGS_DIR = os.path.join(ROOT_DIR, "logs")
...
```

冒頭の`import`部分をこのように変更して、ローカル内のソースコードの変更を有効にする。
初期設定だと`site-packege`の中から`import`してくるため変更し辛い。

次に同フォルダから`../../mrcnn/model.py`を開いて以下のように変更する

``` python:model.py
...
        for name in loss_names:
            if name in self.keras_model.metrics_names:
                continue
            layer = self.keras_model.get_layer(name)
            #self.keras_model.metrics_names.append(name)
            loss = (
                tf.reduce_mean(layer.output, keepdims=True)
                * self.config.LOSS_WEIGHTS.get(name, 1.))
            #self.keras_model.metrics_tensors.append(loss)
            self.keras_model.add_metric(loss, name)
...
```

`self.keras_model.metrics_names.append(name)`及び`self.keras_model.metrics_tensors.append(loss)`が
`self.keras_model.add_metric(loss, name)`にAPIが変更されているため、これらを書き換える。

## Training
これで以下のコマンドで学習ができる。

```bash
$ python balloon.py train --dataset=../../datasets/balloon/ --weights=imagenet
```

`--weights`に重みを指定することで転移学習することができる。
例えば以下であれば最新の重みを用いて学習することになる。

```bash 
 --weights=last
```

## Training without weight
最初から学習したい場合は以下のように重みのロードをコメントアウトすればよい。

``` python:balloon.py
    # Load weights
#    print("Loading weights ", weights_path)
#    if args.weights.lower() == "coco":
#        # Exclude the last layers because they require a matching
#        # number of classes
#        model.load_weights(weights_path, by_name=True, exclude=[
#            "mrcnn_class_logits", "mrcnn_bbox_fc",
#            "mrcnn_bbox", "mrcnn_mask"])
#    else:
#        model.load_weights(weights_path, by_name=True)
#
    # Train or evaluate
    if args.command == "train":
        train(model)
    elif args.command == "splash":
        detect_and_color_splash(model, image_path=args.image,
                                video_path=args.video)
```

コマンドはこんな感じ。

```bash
$ python balloon.py train --dataset=../../datasets/balloon/ --weights=
```

学習が始まると以下のように表示される。

``` bash
100/100 [==============================] - 6s 6ms/step - loss: 11.5805 - rpn_class_loss: 0.6961 - rpn_bbox_loss: 6.0425 - mrcnn_class_loss: 0.3481 - mrcnn_bbox_loss: 4.0630 - mrcnn_mask_loss: 0.4308 - val_loss: 22.7146 - val_rpn_class_loss: 3.1622 - val_rpn_bbox_loss: 10.6883 - val_mrcnn_class_loss: 0.0000e+00 - val_mrcnn_bbox_loss: 0.0000e+00 - val_mrcnn_mask_loss: 0.0000e+00
```

# 4. Train with custom datasets
次に自身のデータセットを用いて識別を行う方法を説明する。

## 準備
`sample/balloon`がサンプルとしても扱いやすいので、こちらをベースに修正していく。
まずルートフォルダからコピーを行う。

```bash
$ cd sample
$ cp -R balloon customDataset
$ cd customDataset
```

## データセットの構成
以下の該当部分を書き換えれば自身のデータセットを読み込ませることが出来る。

``` python:balloon.py
class BalloonDataset(utils.Dataset):

    def load_balloon(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("balloon", 1, "balloon")

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

        # Load annotations
        # VGG Image Annotator (up to version 1.6) 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
        # Note: In VIA 2.0, regions was changed from a dict to a list.
        annotations = json.load(open(os.path.join(dataset_dir, "via_region_data.json")))
        annotations = list(annotations.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:
            # Get the x, y coordinaets of points of the polygons that make up
            # the outline of each object instance. These are stores in the
            # shape_attributes (see json format above)
            # The if condition is needed to support VIA versions 1.x and 2.x.
            if type(a['regions']) is dict:
                polygons = [r['shape_attributes'] for r in a['regions'].values()]
            else:
                polygons = [r['shape_attributes'] for r in a['regions']] 

            # 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(
                "balloon",
                image_id=a['filename'],  # use file name as a unique image id
                path=image_path,
                width=width, height=height,
                polygons=polygons)
```

方法としては2つ。

1つは以下のアノテーションファイルを作成する

```
        # { 'filename': '28503151_5b5b7ec140_b.jpg',
        #   'regions': {
        #       '0': {
        #           'region_attributes': {},
        #           'shape_attributes': {
        #               'all_points_x': [...],
        #               'all_points_y': [...],
        #               'name': 'polygon'}},
        #       ... more regions ...
        #   },
        #   'size': 100202
        # }
```

2つ目は以下の構造に自身のデータセットを入れる。

``` python
            self.add_image(
                "balloon",
                image_id=a['filename'],  # use file name as a unique image id
                path=image_path,
                width=width, height=height,
                polygons=polygons)
```

今回は2つ目の方法を取る。

`image_path`、`width`、`height`、`polygons`が主なデータである。
これ等のデータは以下のように表現されている。

```
../../datasets/balloon/train/34020010494_e5cb88e1c4_k.jpg
2048
1536
[{'name': 'polygon', 'all_points_x': [1020, 1000, 994, 1003, 1023, 1050, 1089, 1134, 1190, 1265, 1321, 1361, 1403, 1428, 1442, 1445, 1441, 1427, 1400, 1361, 1316, 1269, 1228, 1198, 1207, 1210, 1190, 1177, 1172, 1174, 1170, 1153, 1127, 1104, 1061, 1032, 1020], 'all_points_y': [963, 899, 841, 787, 738, 700, 663, 638, 621, 619, 643, 672, 720, 765, 800, 860, 896, 942, 990, 1035, 1079, 1112, 1129, 1134, 1144, 1153, 1166, 1166, 1150, 1136, 1129, 1122, 1112, 1084, 1037, 989, 963]}]
```

よって、画像1つに対して、このペアを入力すればよい。
なお、この方法は1クラスの方法である。