【MMDetection】 データセットのカスタマイズ編

## 概要
まだまだ日本語情報が少ない物体検出フレームワーク"MMDetection"について、学んだことを記録していこうと思います。間違い等ありましたらぜひコメントで教えてください。よろしくお願いします。

前回：[config実践編](https://qiita.com/bellpond/items/3a692914b4559fbe51b3)

今回は、自分で用意したデータセットを利用する方法をまとめます。

## データセットの処理について
データセットの形式（アノテーションの形式）は、COCO形式, PascalVOC形式, YOLO形式, ...と多岐に渡り、基本的にはデータセット毎に異なるためこれらを統一的に処理するのは困難です。

MMDetectionでは、内部で独自のアノテーション形式を用いることで統一的な処理を実現しています。従って、データセットはこの独自形式と互換性を保つように変換処理される必要があります。

独自形式のサンプル↓

```json
[
    {
        'filename': 'a.jpg',
        'width': 1280,
        'height': 720,
        'ann': {
            'bboxes': <np.ndarray, float32> (n, 4),
            'labels': <np.ndarray, int64> (n, ),
            'bboxes_ignore': <np.ndarray, float32> (k, 4),
            'labels_ignore': <np.ndarray, int64> (k, ) (optional field)
        }
    },
    ...
]
```

現在（2021.01.29）MMDetectionでは、COCO形式とPascalVOC形式のデータセットに対応しており、独自形式への変換処理の面倒をみてくれます。

一方で、他の形式のデータセットを利用する場合は、後述するように何らかの処理を行う必要があります。

## 対応しているデータセットについて
以下のデータセットに関しては変換コードが準備されているので、特別な処理を必要とせず、データを正しく配置するだけで使えるようになります。データの配置方法はconfigファイルの`data`キーの中を見ればわかります（[例](https://github.com/open-mmlab/mmdetection/blob/master/configs/_base_/datasets/coco_instance.py#L30)）。

- [COCO](https://cocodataset.org/#home)
- [PASCAL VOC](http://host.robots.ox.ac.uk/pascal/VOC/)
- [Cityscapes](https://www.cityscapes-dataset.com/)
- [DeepFashion](http://mmlab.ie.cuhk.edu.hk/projects/DeepFashion.html)
- [LVIS](https://www.lvisdataset.org/)
- [WIDER FACE](http://shuoyang1213.me/WIDERFACE/)

## 対応していないデータセットについて
ここからが今回の本題です。

上述したデータセット以外のデータセットを使う場合には何かしらの処理が必要です。
データセットの形式により必要な処理が異なるので、場合分けをします。

### 1. COCO形式 or PascalVOC形式の場合
この場合ベースconfigが用意されているので、それを元に部分的な修正を加えることで新しいデータセットのベースconfigファイルが完成します。これを`configs/_base_/datasets`に加えることで、このファイルを継承すれば簡単にデータセットが利用可能になります。

※ モデルを構築する際は、分類クラス数等をデータセットに応じて変更するのを忘れないようにしましょう。

#### 1.1. configファイルの作成
対応する形式のベースconfigを継承して、クラス数やラベル、データへのパス等の設定を修正します。

```my_dataset.py
_base_ = 'coco_detection.py'  # COCO形式の場合  # インスタンスセグメンテーション用なら'coco_instance.py'
# _base_ = 'voc0712.py'  # PascalVOC形式の場合

classes = ('a', 'b', 'c', 'd', 'e')  # クラスラベル
data = dict(
    samples_par_gpu=2,
    workers_par_gpu=2,
    train=dict(
        type=dataset_type,
        classes=classes,  # COCOデータセットのクラスをオーバーライド
        ann_file='path/to/your/train/annotation_data',
        img_prefix='path/to/your/train/image_data'),
    val=dict(
        type=dataset_type,
        classes=classes,  # COCOデータセットのクラスをオーバーライド
        ann_file='path/to/your/val/annotation_data',
        img_prefix='path/to/your/val/image_data'),
    test=dict(
        type=dataset_type,
        classes=classes,  # COCOデータセットのクラスをオーバーライド
        ann_file='path/to/your/test/annotation_data',
        img_prefix='path/to/your/test/image_data')
)
```



### 2. その他の形式の場合
#### 2.0. 事前にCOCO形式へ変換
公式ドキュメントでは、データセットを事前にCOCO形式に変換することを推奨しています。事前の変換が可能な場合は、変換してから「[COCO形式 or PascalVOC形式の場合](https://qiita.com/bellpond/items/84c718608adeeca2a2f0#1-coco%E5%BD%A2%E5%BC%8F-or-pascalvoc%E5%BD%A2%E5%BC%8F%E3%81%AE%E5%A0%B4%E5%90%88)」の手順を踏むと良いです。

事前に変換をしない場合は学習時にオンラインでの変換を行うことになります。
#### 2.1. configファイルの作成
クラス名やデータ処理のパイプライン、データロード等、データに関する設定を記述します。
COCOのベースconfig（[coco_detection.py](https://github.com/open-mmlab/mmdetection/blob/f07de13b82b746dde558202f720ec2225f276d73/configs/_base_/datasets/coco_detection.py)）を参考にすると良いです。

```my_dataset.py
dataset_type = 'MyDataset'
data_root = 'path/to/your/data_root/'
img_norm_cfg = dict(  # 画像の正規化に関する情報
    mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True)
train_pipeline = [  # 学習時のデータ処理パイプライン
    dict(type='LoadImageFromFile'),
    dict(type='LoadAnnotations', with_bbox=True),
    dict(type='Resize', img_scale=(1333, 800), keep_ratio=True),
    dict(type='RandomFlip', flip_ratio=0.5),
    dict(type='Normalize', **img_norm_cfg),
    dict(type='Pad', size_divisor=32),
    dict(type='DefaultFormatBundle'),
    dict(type='Collect', keys=['img', 'gt_bboxes', 'gt_labels']),
]
test_pipeline = [  # テスト時のデータ処理パイプライン
    dict(type='LoadImageFromFile'),
    dict(
        type='MultiScaleFlipAug',
        img_scale=(1333, 800),
        flip=False,
        transforms=[
            dict(type='Resize', keep_ratio=True),
            dict(type='RandomFlip'),
            dict(type='Normalize', **img_norm_cfg),
            dict(type='Pad', size_divisor=32),
            dict(type='ImageToTensor', keys=['img']),
            dict(type='Collect', keys=['img']),
        ])
]
data = dict(  # データロードに関する設定
    samples_per_gpu=2,
    workers_per_gpu=2,
    train=dict(
        type=dataset_type,
        ann_file=data_root + 'path/to/your/train/annotation_data',
        img_prefix=data_root + 'path/to/your/train/image_data',
        pipeline=train_pipeline),
    val=dict(
        type=dataset_type,
        ann_file=data_root + 'path/to/your/val/annotation_data',
        img_prefix=data_root + 'path/to/your/train/image_data',
        pipeline=test_pipeline),
    test=dict(
        type=dataset_type,
        ann_file=data_root + 'path/to/your/test/annotation_data',
        img_prefix=data_root + 'path/to/your/train/image_data',
        pipeline=test_pipeline))
evaluation = dict(interval=1, metric='bbox')  # 評価に関する設定
```
データロードに関するパイプラインの設定は次回以降まとめたいと思います。

#### 2.2. データセットクラスの作成
アノテーション形式をオンラインでMMDetectionの独自形式に変換するために、`CustomDataset`クラスを継承して独自のデータセットクラスを定義します。

このクラスの中で、特別な処理が必要なメソッドをオーバーライドします。

今回は、`annotations.txt`ファイルに以下のようにアノテーションが記述されていると仮定し、このアノテーションを変換するために、`mmdet/datasets/my_dataset.py`を作成します。

```text
#
000001.jpg
1280 720
2
10 20 40 60 1
20 40 50 60 2
#
000002.jpg
1280 720
3
50 20 40 60 2
20 40 30 45 2
30 40 50 60 3
```

以下のメソッドをオーバーライドします。

- `load_annotations（self, ann_file）`：アノテーションをロード・変換するメソッド
- `get_ann_info(self, idx)`：インデックスを用いてアノテーションを返すメソッド

```my_dataset.py
import mmcv
import numpy as np

from .builder import DATASETS
from .custom import CustomDataset

@DATASETS.register_module()  # データセットモジュールを登録するためのおまじない
class MyDataset(CustomDataset):  # configで設定したdataset_typeと同名にする

    CLASSES = ('person', 'bicycle', 'car', 'motorcycle')

    def load_annotations(self, ann_file):
        ann_list = mmcv.list_from_file(ann_file)

        data_infos = []
        for i, ann_line in enumerate(ann_list):
            if ann_line != '#':
                continue

            img_shape = ann_list[i + 2].split(' ')
            width = int(img_shape[0])
            height = int(img_shape[1])
            bbox_number = int(ann_list[i + 3])

            anns = ann_line.split(' ')
            bboxes = []
            labels = []
            for anns in ann_list[i + 4:i + 4 + bbox_number]:
                bboxes.append([float(ann) for ann in anns[:4]])
                labels.append(int(anns[4]))

            data_infos.append(
                dict(
                    filename=ann_list[i + 1],
                    width=width,
                    height=height,
                    ann=dict(
                        bboxes=np.array(bboxes).astype(np.float32),
                        labels=np.array(labels).astype(np.int64))
                ))

        return data_infos

    def get_ann_info(self, idx):
        return self.data_infos[idx]['ann']
```
#### 2.3 \_\_init\_\_.py の修正
自分で新しく定義したクラスがimportされるように、`mmdet/datasets/__init__.py`を修正します。

```__init__.py
# 追加
from .my_dataset import MyDataset

# 修正
__all__ = [
    'CustomDataset', ... , 'MyDataset'  # 'MyDataset'を追加
]
```

## おまけ
既存のデータセットの一部のクラスのみを使って学習を行いたい場合、configファイルで`data.classes`を以下の様に修正することで実現可能です。指定しなかったクラスのアノテーションは自動的に無視されます。

```python
classes = ('person', 'bicycle', 'car')  # この3つだけ学習したい場合
data = dict(
    train=dict(classes=classes),
    val=dict(classes=classes),
    test=dict(classes=classes))
```
また、テキストファイルにクラスを列挙して読み込むことも可能です。

```classes.txt
person
bicycle
car
```

```python
classes = 'path/to/classes.txt'
data = dict(
    train=dict(classes=classes),
    val=dict(classes=classes),
    test=dict(classes=classes))
```

## 参考文献
- [公式ドキュメント](https://mmdetection.readthedocs.io/en/latest/index.html)