mac OS で labelImg と事前に作成した擬似ラベルを使って物体検出用アノテーションを作成する

# この記事は？
物体検出に用いられるアノテーションを作成する [labelImg](https://github.com/tzutalin/labelImg) の mac OS での使い方を解説した記事です。特に、アノテーション対象のオブジェクトの位置とクラスが事前にざっくりわかっていてそれを補正したいという場合の使い方を解説しています。

![demo3.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238125/c5e05f35-6267-33af-91bd-0ac52ea2913e.jpeg)

# labelImg とは？
labelImg では以下のようなことができます。

1. **物体検出のためのアノテーション (bbox, class) を無料で作成できる**
2. **商用利用可能 (MIT LICENSE)**
3. **事前に作成した (擬似) アノテーションを読み込んで GUI 上で補正できる**

特に、3 が他のアノテーションツールにはない強みで、適当に事前学習したモデルでそれっぽい仮の bbox を作成できるとアノテーション作業がかなり楽になります。例えば、人物インスタンスにアノテーションをつけたい時には COCO や CrowdHuman などで学習したモデルの confidence を思いっきり下げてとりあえずの bbox を作成するとゼロベースで bbox の矩形を描くよりずっと高速にアノテーションができます。

# 準備
## 環境構築
まずはリポジトリをクローンします。
```bash
git clone git@github.com:tzutalin/labelImg.git
cd labelImg
```

20220616時点での mac での推奨は [pipenv](https://pipenv.pypa.io/en/latest/) を使うやり方です[^docker]。python3 や pipenv が既にインストールされてる場合は対応する箇所は適宜スキップして良いです。
```bash
brew install python3
pip3 install pipenv
pipenv run pip install pyqt5==5.15.2 lxml
pipenv run make qt5py3
```

[^docker]: docker for mac を使って docker 利用の方も試してみたのですがこちらは docker image の中に labelImg で動かすためのライブラリがインポートされていない or パスが通ってない旨のエラーが出てうまくいきませんでした。

## バグの修正
20220616現在の labelImg の master ブランチにはバグがありそのまま使うとエラーを吐かれて GUI が停止します。そこでバグに当たる箇所である `libs/labelDialog.py` の74行目を以下のように修正します。`QPoint` の引数の型を float から　int に変換するだけです。

```
(before fix): offset += QPoint(btn.size().width()/4, btn.size().height()/2)
 (after fix): offset += QPoint(int(btn.size().width()/4), int(btn.size().height()/2))
```

# 使い方
## ラベルの設定
bbox のアノテーションを始める前にクラスラベルを定義する必要があります。GUI で作成した bbox にここで定義したラベルの中から選んでクラスを付与することになるためです。

クラスラベルは　　`data/predefined_classes.txt`　の中で定義します。`data/predefined_classes.txt` は1行に1つのクラスラベルが記された構成になっています。例えば、ヒト、トカゲ、カエルにアノテーションをつけたい場合は以下のようにファイルを構成します。`data/predefined_classes.txt` の中身は日本語で書いても英語で書いても大丈夫です。

```predefined_classes.txt
ヒト
トカゲ
カエル
```

## (optional) 擬似ラベルを作成してアノテーションを補助する場合
擬似ラベルを作成する場合、以下のようなフォルダ構成を作成します。

```
data
  ┣ 00000001.jpg
  ┣ 00000001.txt
  ┣ 00000002.jpg
  ┣ 00000002.txt
  ...
  ┗ classes.txt
```

ここで、`*.jpg` はアノテーションをつける画像[^jpg]、`*.txt` は同じ名称の画像に対応する擬似アノテーション、`classes.txt` は擬似アノテーションに利用されたクラスラベルを定義したファイルです。

[^jpg]: 自分は jpg しか試してないですが、他の suffix を持つ画像でもできると思います。

### 擬似ラベルの作成
擬似ラベルを作成できる場合には作成したほうが確実にアノテーションコストを削減できます。小規模であってもアノテーションをつけるオブジェクトのクラスを含むデータセットがあれば、そこで finetune したモデルを使って擬似ラベルを付与することをお勧めします。

擬似ラベルは以下のようなテキストファイルとして作成する必要があります。いわゆる YOLO 型のアノテーションと言われているやつで、`class center-x center-y width height` でオブジェクトを表現します。ここで `class` は int 型、 `center-x` `center-y` `width` `height` は全て画像のサイズで正規化された float で値のレンジは `[0, 1]` です。例えば、画像中央に bbox の中心があり、幅と高さがそれぞれ画像の幅と高さの1/10、クラスが0であるオブジェクトは以下のように表現されます。

```pseudo_label.txt
0 0.5 0.5 0.1 0.1
```

同一画像の中に複数のオブジェクトがあるときは同様に改行してオブジェクトを定義します。1行当たり1インスタンスになるようにしてください。

```pseudo_label.txt
0 0.5 0.5 0.1 0.1
1 0.25 0.75 0.2 0.3
2 0.75 0.25 0.1 0.2
```

また、擬似ラベルのファイル名は suffix を除いて画像の名称と一致する必要があります。つまり、`00000001.jpg` という画像に擬似ラベルをつけるときは `00000001.txt` という名称で擬似ラベルを作成する必要があります。

### `classes.txt` の作成
擬似ラベルを作成した場合、擬似ラベルで定義したクラス (int) に対応する str 型のクラスを定義したファイルを用意する必要があります。ファイルの構成は `data/predefined_classes.txt` と同様です。擬似ラベルを作成する際に利用したモデルのラベルをそのまま使えば良いと思います。
 
## GUI の起動とアノテーション
`labelImg.py` を動かすと GUI を起動できます。

```bash
pipenv run python3 labelImg.py
```

GUI さえ起動すればアノテーション作業自体は割と直感的にできます。日本語にも対応しているので特に迷うことはないと思います。

## tips
- アノテーションフォーマットは YOLO (たぶんデフォルト) を選択する
- `表示` → `自動で保存する` を選択するとアノテーションを保存する際に出てくる確認のポップアップを排除できるのでアノテーション速度が改善します
    - このオプションをつけない場合 `command + s` で保存できます
- `a` で前の画像、`d` で次の画像に移動します。`自動で保存する` オプションをつけている場合は移動した瞬間にアノテーションが保存されます
- `w` で新しい矩形を作成します
- `command + +` で拡大、`command + -` で縮小
    - 個人的に使い勝手はあんまり良くないです 
- 擬似ラベルを作成するときは class-agnostic な NMS をかけて矩形の数を絞ると楽になる気がします
    - pytorch (torchvision) の場合は `batched_nms` の代わりに `nms` を使う感じです。
- 動画のアノテーションを行う場合は `command + v` で直線のフレームのアノテーションをコピーできるのでインスタンス数が変わらないときはアノテーションを楽できる時があります