何をやったの?
- 既存のアノテーションツールを使って物体検知の自作データセットを作成した。
- アノテーションツールの出力をCOCOデータセットのフォーマットに変換するコードを書いた。
- (数多の)EfficientDetの実装から使いやすそうなものを見つけて学習が回ることを確認した。
- 実際にイラスト中のキャラクター顔検知データセットを作成し、学習済みモデルのデモを作った。
デモ
この記事でメモした方法を使って作成したデータセットによる学習済みのEfficientDetモデルはGithubレポジトリからダウンロードできます。
Google colaboratoryで学習済みモデルを試せるデモも用意したので遊んでみてください。
pipでインストールできるモジュールにしました。事前学習済みのモデルを試すことができます。 AnimeCV
背景(読み飛ばしOK)
最近ふと開発しているキャラクターイラスト自動分類ツールの精度向上のためイラスト中の顔検知がやりたくなりました。
アニメの顔検知では、これまでOpenCVを使っている取り組みやYOLOを使っている取り組みが記事として公開されています。
ただOpenCVのモデルではやはり精度に限度があるらしく、またYOLOの記事の方は学習済みのモデルが公開されていないようです(公開されていたらごめんなさい)。
そこで今回は自分で物体検知モデルを学習してやることにしました。ただデータセットなども無いので、今回は自分でアノテーションからやっていきます。
データ準備
アノテーション
物体検知は比較的メジャーな研究分野なので、アノテーションのためのツールも多く公開されているようです。
今回はこれらのツールの比較記事を参考に、LabelImgを使ってアノテーションを行いました。
追記(2020/11/09):
dockerが利用できないと使いづらいアプリケーションですが、coco-annotatorというツールを用いると直接COCOフォーマットのデータセットが出力できて便利です。GUIも直感的でかなり使いやすいですね。
目的に応じてはこちらを利用した方が良いかもしれません。
LabelImgの使い方
アノテーションのための準備はとても簡単で、必要な環境を整えた後はアノテーション対象のクラス定義のファイル classes.txt
を用意し、アノテーションする画像を1つのディレクトリ(例えば image_dir
) にまとめておくだけです。
クラス定義ファイルは各行にクラス名を書いただけの簡単なもので、例えば今回の場合は「顔」を検知したいだけなので以下のようになります。
face
アノテーションツールを起動するには以下のようにします。
python labelImg.py image_dir classes.txt
ツールのGUIは極めて直感的で、w
キーでBouding Boxの作成を開始した後は対角線の両端に相当する点をクリックするだけです。
ショートカット
このツールには様々な便利なショートカットが用意されていますが、よく使ったのは以下のキーでした。
キー | 機能 |
---|---|
w | 新しいBounding Boxの作成を開始する。 |
d | 次の画像に移る。 |
a | 一個前の画像に移る。 |
Ctrl+S | アノテーション結果を保存。 |
補足
-
アノテーション結果はPascalVOCとYOLOの2通りのフォーマットで保存できます。(デフォルトはPascalVOCです。)
-
管理のしやすさから、私はアノテーション結果のxmlファイルは画像と同じディレクトリに保存しました。そのおかげか、一度ツールを閉じてアノテーションを再開すると以前のアノテーション結果が自動的に読み込まれていました。
データ前処理(COCOフォーマットへの変換)
最近の物体検知の研究ではCOCO2017データセットなどがベンチマークとしてよく用いられており、論文の公開実装でもこのフォーマットを入力するものが多くなっているようです。
そこで、今回はLabelImgでアノテーションした結果をCOCOフォーマットに変換する前処理コードを書きました。前処理コードはGithubで公開しています。
なおこの前処理コードの作成にあたり、具体的なフォーマットについてはこちらの記事を参考にしました。より詳細なフォーマットなどが気になる方はご参照ください。
データセットの階層構造
最終的なデータセットのファイル構成は以下のようになります。project_name
は自由な名前に読み替えてください。
project_name
+- train
| +- <訓練画像>
+- val
| +- <評価用画像>
+- annotations
+- instances_train.json
+- instances_val.json
前処理コード実行例
前処理コードを実行するには以下のコマンドを実行してください。以下では<DIR1>
などのディレクトリ以下にLabelImgによるアノテーション結果のXMLファイルが保存されているとしています。(こちらのディレクトリは空白区切りで複数選択が可能です。)
# 訓練データ
python convert_to_coco.py \
--image-root-dir project_name/train \
--annotation-fn project_name/annotations/instances_train.json \
--copy-images --same-dir \
--annotation-dir <DIR1> <DIR2> ...
# 評価データ(trainをvalに変えるだけです)
python convert_to_coco.py \
--image-root-dir project_name/val \
--annotation-fn project_name/annotations/instances_val.json \
--copy-images --same-dir \
--annotation-dir <DIR1'> <DIR2'> ...
なお、前提として以下を仮定しています:
- 画像と同じディレクトリにPascalVOCフォーマットでアノテーション情報のXMLファイルを保存している。
EfficientDetの学習
EfficientDetの実装を公開しているレポジトリはいくつかあります。いろいろと調べてみた結果 zylo117 /
Yet-Another-EfficientDet-Pytorch が使いやすそうだったので今回はこちらを利用してモデルを学習していきます。
準備
自作データセットでの学習を行うには、環境の準備の他に、①事前学習済みのパラメータのダウンロードと、②自作データセットの定義ファイル作成、を行う必要があります。
事前学習済みパラメータ
EfficientDetの事前学習済みパラメータはGithubレポジトリから'efficientdet-d*.pth'をダウンロードします。
d0
やd7
は(おそらく)論文に記載されているモデルのサイズなどに関する設定に対応しており、数字が小さいほど軽量な代わりに精度が低いモデルとなっているようです。
今回はひとまずefficientdet-d0.pth
をダウンロードしました。
データセット定義ファイル
自作データセットで学習するにはprojects
ディレクトリ以下に設定のymlファイルを作成する必要があるようです。
COCOデータセットの設定から変更する必要がある項目はデータセットの名前であるproject_name
と、検知するクラス名のリストであるobj_list
、あとは利用するGPUの個数であるnum_gpus
あたりでしょう。
anchors_scales
やanchors_ratios
については、私自身まだ物体検知やEfficientDetの細かいアルゴリズムを理解していないため今回はCOCOデータセットのままとしました。
私の場合、定義ファイルは以下のようになりました。
project_name: coco_pixiv # also the folder name of the dataset that under data_path folder
train_set: train
val_set: val
num_gpus: 1
# mean and std in RGB order, actually this part should remain unchanged as long as your dataset is similar to coco.
mean: [0.485, 0.456, 0.406]
std: [0.229, 0.224, 0.225]
# this is coco anchors, change it if necessary
anchors_scales: '[2 ** 0, 2 ** (1.0 / 3.0), 2 ** (2.0 / 3.0)]'
anchors_ratios: '[(1.0, 1.0), (1.4, 0.7), (0.7, 1.4)]'
# must match your dataset's category_id.
# category_id is one_indexed
obj_list: ['face']
anchor_ratios
の設定 (2020/11/09追記)
EfficientDetはアンカーを用いて物体検知で行うため、自作データセットに細長い物体が含まれる場合にはデフォルト設定ではうまく検知ができない場合があります。
そのような場合にはKMeansを使ったツールを用いて自作データセットに対してより適切なアンカー形状を決めることができるようです。
ツールの使い方は簡単で、gitでレポジトリをクローンした際にルートディレクトリにあるkmeans_anchors_ratios.py
を以下のように用いるだけです。
- ただし依存関係で
tqdm
モジュールのインストールが必要です。
python kmeans_anchors_ratios.py \
--instances <COCOフォーマットのアノテーションJSONファイル> \
--num-runs 3 \ # K-Meansを回す回数
--num-anchors-ratios 5 \ # アンカー形状の個数。3とかでも良いと思います
--input-size 512 \ # モデル入力前に画像がリサイズされる大きさ
--anchor-sizes 32 64 128 256 512 # アンカーサイズのリスト
実行すると、出力においてK-Means anchors ratios:
と示されている行に学習されたアンカー形状が表示されます。
学習
学習を実行するには以下を実行します。ハイパーパラメータなどは特に根拠があるわけではないので参考程度に。
python train.py -c 0 -p project_name \
--batch_size 8 --lr 1e-5 --num_epochs 100 \
--load_weights efficientdet-d0.pth \
--data_path <前処理コードで作成したデータセットディレクトリproject_nameがあるディレクトリ>
-c
オプションで指定しているのは前述したd0
とかd7
に対応する数値です。なので、この数値と--load_weights
で指定する事前学習済みパラメータの方のd*
の数値は一致している必要があります。
GTX1070で、学習データ数900個程度、epoch数100の学習は1時間程度かかりました。
評価
学習したモデルを実際に画像に適用して性能がどの程度になるかを見るにはefficientdet_test.py
を実行します。
ただし、このコードは入力画像名などがベタ書きになっているので以下のように変更すると
python efficientdet_test.py <d*に対応する数字> <モデル.pthファイル名> <画像名>
で実際の検知結果を可視化することができます。表示された画像のウィンドウを消すには何かキーを押してください。xボタンで消すとフリーズすることがあります。
評価コード修正例
# Author: Zylo117
"""
Simple Inference Script of EfficientDet-Pytorch
"""
import sys
import time
import torch
from torch.backends import cudnn
from matplotlib import colors
from backbone import EfficientDetBackbone
import cv2
import numpy as np
from efficientdet.utils import BBoxTransform, ClipBoxes
from utils.utils import preprocess, invert_affine, postprocess, STANDARD_COLORS, standard_to_bgr, get_index_label, plot_one_box
compound_coef = int(sys.argv[1])
force_input_size = None # set None to use default size
img_path = sys.argv[3]
# replace this part with your project's anchor config
anchor_ratios = [(1.0, 1.0), (1.4, 0.7), (0.7, 1.4)]
anchor_scales = [2 ** 0, 2 ** (1.0 / 3.0), 2 ** (2.0 / 3.0)]
threshold = 0.2
iou_threshold = 0.2
use_cuda = True
use_float16 = False
cudnn.fastest = True
cudnn.benchmark = True
obj_list = ['face']
color_list = standard_to_bgr(STANDARD_COLORS)
# tf bilinear interpolation is different from any other's, just make do
input_sizes = [512, 640, 768, 896, 1024, 1280, 1280, 1536]
input_size = input_sizes[compound_coef] if force_input_size is None else force_input_size
ori_imgs, framed_imgs, framed_metas = preprocess(img_path, max_size=input_size)
if use_cuda:
x = torch.stack([torch.from_numpy(fi).cuda() for fi in framed_imgs], 0)
else:
x = torch.stack([torch.from_numpy(fi) for fi in framed_imgs], 0)
x = x.to(torch.float32 if not use_float16 else torch.float16).permute(0, 3, 1, 2)
model = EfficientDetBackbone(compound_coef=compound_coef, num_classes=len(obj_list),
ratios=anchor_ratios, scales=anchor_scales)
model.load_state_dict(torch.load(sys.argv[2]))
model.requires_grad_(False)
model.eval()
if use_cuda:
model = model.cuda()
if use_float16:
model = model.half()
with torch.no_grad():
features, regression, classification, anchors = model(x)
regressBoxes = BBoxTransform()
clipBoxes = ClipBoxes()
out = postprocess(x,
anchors, regression, classification,
regressBoxes, clipBoxes,
threshold, iou_threshold)
def display(preds, imgs, imshow=True, imwrite=False):
for i in range(len(imgs)):
if len(preds[i]['rois']) == 0:
continue
for j in range(len(preds[i]['rois'])):
x1, y1, x2, y2 = preds[i]['rois'][j].astype(np.int)
obj = obj_list[preds[i]['class_ids'][j]]
score = float(preds[i]['scores'][j])
plot_one_box(imgs[i], [x1, y1, x2, y2], label=obj,score=score,color=color_list[get_index_label(obj, obj_list)])
if imshow:
cv2.imshow('img', imgs[i])
cv2.waitKey(0)
if imwrite:
cv2.imwrite(f'test/img_inferred_d{compound_coef}_this_repo_{i}.jpg', imgs[i])
out = invert_affine(framed_metas, out)
display(out, ori_imgs, imshow=True, imwrite=True)
print('running speed test...')
with torch.no_grad():
print('test1: model inferring and postprocessing')
print('inferring image for 10 times...')
t1 = time.time()
for _ in range(10):
_, regression, classification, anchors = model(x)
out = postprocess(x,
anchors, regression, classification,
regressBoxes, clipBoxes,
threshold, iou_threshold)
out = invert_affine(framed_metas, out)
t2 = time.time()
tact_time = (t2 - t1) / 10
print(f'{tact_time} seconds, {1 / tact_time} FPS, @batch_size 1')
最後に
今回は物体検知データセットのアノテーションからモデルの学習まで一通りやってみました。
まだ1000枚程度しかアノテーションしていないのですが、学習結果のモデルの性能を見ると思ったよりも実用的なレベルまで学習ができているようです。学習自体も(きちんと収束まではできていませんが)1時間程度と案外お手軽にできるレベルだったので助かりました。
今後はアノテーションの枚数を増やして本来の目的であるイラスト分類のツールの方の精度改善につなげていければと思っています。