Detectron2で小銭を数える

# はじめに
この記事では[Detectron2](https://github.com/facebookresearch/detectron2)というFacebook AIが開発している物体検出ライブラリを利用して，自作のデータセットに対して物体検出をしてみます．実際にやってみるとDetectron2に関する日本語・英語の情報があまり見つからず少し苦労したので，私と同じようにDetectron2で物体検出をしようとしている人の助けになれば幸いです．

データセットとして日本硬貨４種類（1円，5円，10円，100円）が含まれた画像を用意し，PretrainedのFaster R-CNNを訓練しています．結果として次のように硬貨を検知できました．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320225/d0e7d9f1-83fa-058a-404d-27d62ff848c9.png)
※データセットに50円，500円硬貨が含まれていないのは，たまたま財布に入っていなかったためです．
※今気づきましたが`hundred`をスペルミスしてますね．恥ずかしいですが直すのが面倒なのでこのままで行きます．

僕は深層学習に関して全く詳しくないため，訓練時のパラメータや設定などは公式チュートリアルそのままか，あるいは適当に決めています．なのでその辺りに関しては説明しません（できません）．データセットの準備や訓練の手順を説明し，上の画像のようなそこそこ使えそうな結果を得ることを目指しています．

作成した硬貨のデータセットはGitHubに上げています．

https://github.com/Be4rR/JapaneseCoins

訓練時のコードはGoogle Colabにあります．

https://colab.research.google.com/drive/1893Uog-I1EFsZ_ADPn1lP5yBgRu4792L?usp=sharing

公式チュートリアルでは，独自のデータセット（風船の画像）のInstance Segmentationを通して基本的な使い方を学ぶことができます．

https://colab.research.google.com/drive/16jcaJoc6bCFAQ96jDe2HwtXj7BMD_-m5#scrollTo=7unkuuiqLdqd

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320225/9c838a59-4c37-8fe9-ec5f-0cd25bffa3eb.png)

# データセット作成
## 画像を用意する
iPhoneで硬貨の画像を30枚程度撮りました．30枚という枚数は適当です．
各画像には硬貨が1~5枚写っています．iPhoneで撮った画像はサイズがとても大きく，訓練に時間がかかってしまいそうだったので大幅に縮小しました．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320225/f956c699-698d-f201-a4ed-0b5b1870e00d.png)

## Annotationをつける
撮影した画像の硬貨に対して[Bounding Box](https://www.nemotos.net/?p=12#:~:text=Bounding%20Box%E3%81%A8%E3%81%AF%E3%80%81%E7%94%BB%E5%83%8F,%E3%81%A8%E3%81%84%E3%81%86%E6%83%85%E5%A0%B1%E3%81%AE%E3%82%88%E3%81%86%E3%81%A7%E3%81%99%E3%80%82)を付けていきます．

Bounding Boxのデータを保存する形式にはいろいろな種類がありますが，その中でもDetectron2はCOCOフォーマットという形式をサポートしています．もちろんそれ以外の形式でも使えますが，COCOフォーマットで作っておくとデータセットの読み込みがとても楽です．

そのために[COCO Annotator](https://github.com/jsbroks/coco-annotator)というツールを利用しました．
使い方については以下のリンクを参考にしました．初めて使うと少し戸惑う部分があるので公式のGetting Startedに沿って進めていくのがおすすめです．

- [COCO Annotator Getting Started](https://github.com/jsbroks/coco-annotator/wiki/Getting-Started)
- [COCO Annotatorを使って骨格検知用のデータセットを作ってみる](COCO Annotatorを使って骨格検知用のデータセットを作ってみる)

# Detectron2

ここから実際にDetectron2を使っていきます．コードはGoogle Colabにあります．

https://colab.research.google.com/drive/1893Uog-I1EFsZ_ADPn1lP5yBgRu4792L?usp=sharing

## セットアップ
基本的に公式のチュートリアルそのままです．Detectron2をインストールしたあとに「ランタイムの再起動」を行う必要があるので注意してください．

## データセット登録

GitHubから硬貨データセットをダウンロードし展開します．

```bash
!wget https://github.com/Be4rR/JapaneseCoins/blob/main/coins.zip?raw=true -q -O coins.zip
!unzip coins.zip > /dev/null
```

ダウンロードしたデータセットを`register_coco_instances`で登録します．

```python
register_coco_instances("coins", {}, "./coins/coco-1612779490.2197058.json", "./coins")
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320225/bc61edab-88ff-8951-9727-905477143eb5.png)

https://detectron2.readthedocs.io/en/latest/modules/data.html?highlight=register_coco#detectron2.data.datasets.register_coco_instances

`name`は重複しなければなんでも良いみたいです．後でデータセットを指定するときに使います．
`json_file`はAnnotationファイルのパスを指定します．
`image_root`は画像を含むフォルダのパスを指定します．

データセットが正しく登録できているか確認します．
データセットの中からランダムに３枚選び，画像とAnnotationを表示します．

```python
coins_metadata = MetadataCatalog.get("coins")
dataset_dicts = DatasetCatalog.get("coins")

for d in random.sample(dataset_dicts, 3):
  img = cv2.imread(d["file_name"])
  visualizer = Visualizer(img[:, :, ::-1], metadata=coins_metadata, scale=1.0)
  vis = visualizer.draw_dataset_dict(d)
  cv2_imshow(vis.get_image()[:, :, ::-1])
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320225/94379b96-ed64-48dc-09cf-1896d96c42ee.png)

`hundred`をスペルミスしてますがデータセットの登録は問題なさそうです．

ちなみに`img[:, :, ::-1]`は画像のRGB形式⇔BGR形式を変換しています．
画像を読み込むとき`cv2.imread`はBGR形式で画像を読み込むのに対して，`Visualizer`はRGB形式で画像を受け取るため変換が必要になります．
参考：[Python, OpenCVでBGRとRGBを変換するcvtColor](https://note.nkmk.me/python-opencv-bgr-rgb-cvtcolor/)

## 訓練

Faster RCNNをModel Zooからダウンロードして訓練（Fine Tune）します．

いろんなパラメータがありますが，とりあえず変えないといけないのは分類のクラス数`cfg.MODEL.ROI_HEADS.NUM_CLASSES`です．今回は１円，５円，１０円，１００円を分類するので`4`にします．

学習率など他にも大事なパラメータがありますが，決め方をよく知らないのでスルーします．

```python
cfg = get_cfg()
cfg.merge_from_file(model_zoo.get_config_file("COCO-Detection/faster_rcnn_R_50_FPN_3x.yaml"))
cfg.DATASETS.TRAIN = ("coins",)
cfg.DATASETS.TEST = ()
cfg.DATALOADER.NUM_WORKERS = 2
cfg.MODEL.WEIGHTS = model_zoo.get_checkpoint_url("COCO-Detection/faster_rcnn_R_50_FPN_3x.yaml")
cfg.SOLVER.IMS_PER_BATCH = 2
cfg.SOLVER.BASE_LR = 0.0004
cfg.SOLVER.MAX_ITER = (
    500
)
cfg.MODEL.ROI_HEADS.BATCH_SIZE_PER_IMAGE = (
    128
)
cfg.MODEL.ROI_HEADS.NUM_CLASSES = 4

os.makedirs(cfg.OUTPUT_DIR, exist_ok=True)
trainer = DefaultTrainer(cfg)
trainer.resume_or_load(resume=False)
trainer.train()
```

タスクごとに様々なモデルが使えます．どんなモデルがあるのかについては次のリンクを参考にしてください．

https://github.com/facebookresearch/detectron2/blob/master/MODEL_ZOO.md


## 硬貨を検出させてみる

訓練したモデルを読み込みます．
`cfg.MODEL.ROI_HEADS.SCORE_THRESH_TEST`は検出の閾値です．

```python
cfg.MODEL.WEIGHTS = os.path.join(cfg.OUTPUT_DIR, "model_final.pth")
cfg.MODEL.ROI_HEADS.SCORE_THRESH_TEST = 0.6
predictor = DefaultPredictor(cfg)
```

Annotationが付いていない`IMG_4693.jpg`～`IMG_4695.jpg`を使って硬貨を検出させてみます．

```python
for num in [4693, 4694, 4695]:    
    im = cv2.imread(f"./coins/IMG_{num}.jpg")
    outputs = predictor(im)
    v = Visualizer(im[:, :, ::-1],
                   metadata=coins_metadata, 
                   scale=1.0
    )
    v = v.draw_instance_predictions(outputs["instances"].to("cpu"))
    cv2_imshow(v.get_image()[:, :, ::-1])
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320225/67b48fe2-4f30-de41-f539-2772af04efba.png)

たまに重複して検出してしまったりしていますが，概ねうまく検出できています．

## おわりに

荒削りですがデータセット作成から訓練，簡単なテストまでできました．
モデルの評価まではできませんでしたが，気になる方は公式のチュートリアルに説明があるので見てみてください．