こんにちは。技術研究所の910です。
今回は少々今更感がありますが、TensorFlowに実装されたTensorFlow Object Detection APIを試してみようと思います。

 

…とは言ったものの、How to train your own Object Detector with TensorFlow’s Object Detector APIに丁寧に手順がまとめられていますので、詳細なやり方についてはこちらの記事をご覧になるのが良いかと思います。
なのでこの記事では、このAPIを使うことで、どれだけ簡単に物体検出を試せるのかをご覧いただければと思います。

作業環境

(tf) ysk@LAKuEN$ sw_vers && python -V && pip freeze | grep -e '\(opencv\|tensorflow\)'
ProductName: Mac OS X
ProductVersion: 10.13.2
BuildVersion: 17C88
Python 3.6.3
opencv-python==3.3.0.10
tensorflow==1.4.0
tensorflow-tensorboard==0.4.0rc2

前準備

Quick Start: Jupyter notebook for off-the-shelf inferenceにも書いてある通り、先にinstallationを済ませておく必要があります。

(tf) ysk@LAKuEN$ git clone https://github.com/tensorflow/models.git
(tf) ysk@LAKuEN$ cd models/research/
# Protobufで書かれたライブラリをビルド
(tf) ysk@LAKuEN$ protoc object_detection/protos/*.proto --python_out=.
# 必要なライブラリをPYTHONPATHに追加
(tf) ysk@LAKuEN$ export PYTHONPATH=$PYTHONPATH:`pwd`:`pwd`/slim
# unittestしとく
(tf) ysk@LAKuEN$ python object_detection/builders/model_builder_test.py

また、本記事の題名にもある通り、私はvenvで仮想環境を作って利用しております。
しかしvenvで作った仮想環境を使う場合、activateの度にPYTHONPATHを追加せねばならず、非常に面倒です。
そこで、こちらを参考にして、bin/activate内にPYTHONPATHの切り替え処理を追加しました。

"""bin/activate"""
# deactivate()内の以下の箇所を書き換え
unset VIRTUAL_ENV
if [ ! "$1" = "nondestructive" ] ; then
unset -f deactivate
# 追記部分
export PYTHONPATH="$OLD_PYTHONPATH"
unset OLD_PYTHONPATH
fi
# deactivate nondestructive の行の直後に以下を追記
# installationにてPYTHONPATHに追加していたパスと同じもの
export OLD_PYTHONPATH=$PYTHONPATH
export PYTHONPATH="$PYTHONPATH:models/research:models/research/slim"

これでactivate、deactivateの度にPYTHONPATHが切り替えられるようになりました。

データセットの作成

参考記事で紹介されているRaccoon Datasetを使い、TFRecords形式のデータセットを作成しました。
データの数こそ少ないですが、アライグマの可愛い画像がたくさん入っていますのでオススメです。笑

ちなみに、こちらのリポジトリでは画像データやアノテーションデータ(物体の位置やクラスラベルなどのメタデータ)の他、変換済みのTFRecords形式のデータセット、ckptファイルも公開されていますので、すぐに検出を行うこともできます。
しかし、今回は学習から検出まで全て試してみたかったので、敢えてTFRecordsファイルの作成から行いました。

また、データセットの作成にはデータセットの公開元が公開しているスクリプトを利用しました。

ファイル名 処理概要
xml_to_csv.py 指定ディレクトリ内のxmlを全件読み取り→単一のcsvとして吐き出し
generate_tfrecord.py csvの各行をExampleオブジェクトに詰めてTFRecordsファイルに書き込み

中身を見ると分かりますが、これらのスクリプトで行っていることは非常にシンプルなので、任意のデータを使いたい場合にも特に困ることはなさそうです。

(tf) ysk@LAKuEN$ git clone https://github.com/datitran/raccoon_dataset.git
(tf) ysk@LAKuEN$ cd ./raccoon_dataset-master/
# annotations内にあるxmlファイルの内容をまとめた、を単一のcsvファイルにまとめる
# ⇛ annotationsと同階層に、"raccoon_labels.csv"というファイルが作成される
(tf) ysk@LAKuEN$ python xml_to_csv.py ./raccoon_dataset-master/annotations/
# アノテーションデータ(csv)の修正
# annotations/*.xml内の画像ファイル名の拡張子は全てpngになっているが、実際の画像ファイルの拡張子が全て.jpgだったので揃える
(tf) ysk@LAKuEN$ sed s/.png/.jpg/g raccoon_labels.csv > raccoon_labels_replaced.csv
# アノテーションデータ(csv)を2分割
# 上記にて作成したcsvを、適当にtrain、test用に適当に分割
# それぞれraccoon_labels_test.csv、raccoon_labels_train.csvとする
# TFRecordsに変換
(tf) ysk@LAKuEN$ python generate_tfrecord.py \
--csv_input raccoon_labels_test.csv \
--output_path raccoon_test.tfrecords
(tf) ysk@LAKuEN$ python generate_tfrecord.py \
--csv_input raccoon_labels_train.csv \
--output_path raccoon_train.tfrecords

設定ファイルと学習済みモデルの準備

学習を行う際に最低限必要なファイルは、モデルの構造や学習方法などを指定する為のconfigファイルと、クラスとIDを対応付けるファイルの2つとなります。
また上記のファイルに加え、転移学習させる為に、記事でオススメされているssd_mobilenet_v1_cocoも用意しました。

クラスとIDを対応付けるファイルの内容は参考記事に記載されている通り、以下のように設定しました。

"""labels.pbtxt"""
item {
id: 1
name: 'raccoon'
}

configファイルも記事に従いssd_mobilenet_v1_pets.configを微修正して利用しました。
修正するに当たってはconfigファイルの概要protoファイルを見て、どんな設定ができるのか確認しながら進めていました。

configファイルを書換えた箇所は以下の通りです。元のconfigファイルと照らし合わせてご覧いただければと思います。

"""ssd_mobilenet_v1_pets.config"""
model {
ssd {
# アライグマだけを検出するので、クラス数を1に
num_classes: 1
...
}
...
}
train_config: {
...
# ローカルで転移学習させるので、ckptを指定
fine_tune_checkpoint: "ssd_mobilenet_v1_coco_11_06_2017/model.ckpt"
...
# 訓練回数の指定
num_steps: 100
...
}
train_input_reader: {
tf_record_input_reader {
# 訓練用のデータセットのパスを指定
input_path: "raccoon_train.tfrecords"
}
# クラスのマップファイルを指定
label_map_path: "raccoon.pbtxt"
}
eval_input_reader: {
tf_record_input_reader {
# 評価用のデータセットのパスを指定
input_path: "raccoon_test.tfrecords"
}
# クラスのマップファイルを指定
# train_input_reader/label_map_pathと同じパスを指定
label_map_path: "raccoon.pbtxt"
...
}

ちなみにprotoファイルを見る限り、評価の間隔やckptの吐き出しの間隔は時間単位でのみ指定できるようです。
本当はstep数で指定したかったので、ここだけはちょっと残念なところでした。。

学習

ファイルが用意できたら、以下のコマンドを叩けば学習が開始されます。
私はGPUを使わないで学習させたので、途中経過をTensorBoardで見ながら待ちました。
時間の都合上、ミニバッチサイズ24で100step回すという設定にしましたが、それでも35分ほど掛かりました。

# 学習の開始
(tf) ysk@LAKuEN$ python object_detection/train.py \
--train_dir ./raccoon_log \
--pipeline_config_path ./ssd_mobilenet_v1_pets.config
Instructions for updating:
Please switch to tf.train.create_global_step
INFO:tensorflow:depth of additional conv before box predictor: 0
INFO:tensorflow:depth of additional conv before box predictor: 0
INFO:tensorflow:depth of additional conv before box predictor: 0
INFO:tensorflow:depth of additional conv before box predictor: 0
INFO:tensorflow:depth of additional conv before box predictor: 0
INFO:tensorflow:depth of additional conv before box predictor: 0
INFO:tensorflow:Summary name /clone_loss is illegal; using clone_loss instead.
INFO:tensorflow:Restoring parameters from /Users/ysk/Downloads/ssd_mobilenet_v1_coco_11_06_2017/model.ckpt
INFO:tensorflow:Starting Session.
INFO:tensorflow:Saving checkpoint to path /Users/ysk/Desktop/raccoon_log_2/model.ckpt
INFO:tensorflow:Starting Queues.
INFO:tensorflow:global_step/sec: 0
INFO:tensorflow:Recording summary at step 0.
INFO:tensorflow:global step 1: loss = 14.5257 (41.192 sec/step)
INFO:tensorflow:global step 2: loss = 12.7733 (21.154 sec/step)
...
INFO:tensorflow:global step 99: loss = 3.4134 (14.855 sec/step)
INFO:tensorflow:global step 100: loss = 3.2981 (14.630 sec/step)
INFO:tensorflow:Stopping Training.
INFO:tensorflow:Finished training! Saving model to disk.
real 35m9.895s
user 84m48.347s
sys 6m11.684s

学習が終わればそのままckptファイルを使って検出することも可能ですが、単一ファイルにまとまっていた方が何かと取り回しがし易いので、こちらに従ってpbファイルに固めました。

(tf) ysk@LAKuEN$ python object_detection/export_inference_graph.py \
--input_type image_tensor \
--pipeline_config_path raccoon_log/pipeline.config \
--trained_checkpoint_prefix raccoon_log/model.ckpt-100 \
--output_directory raccoon_pretrained.pb

作成した学習済みモデルでの検出

pb形式に固めた学習済みモデルで検出を試してみました。
学習のstep数やデータの数の少なさから精度は期待できないと思いましたので、今回はCPU環境でどれだけの検出速度が出せるのかをチェックすることにしました。

検出に使用した動画データはNHKクリエイティブライブラリーで公開されているアライグマの動画となります。
当該動画の詳細はこんな感じです。

# predict.pyは動画を処理する為に作成したスクリプト
(tf) ysk@LAKuEN$ time python predict.py --videopath ./D0002031034_00000_V_000.mp4
100% (1885 of 1885) |***************************************| Elapsed Time: 0:05:03
real 5m9.032s
user 13m40.783s
sys 0m25.475s

上記のログにも出ておりますが、この動画は1885フレームで構成されています。
FPSが29.97であるのに対し、1885フレーム / 309秒 = 6.100324フレーム/秒で検出を行えているので、検出する間隔を1/5に間引けば疑似的にリアルタイム処理が行えるということになります。
検出の間隔をある程度間引いてしまっていい場合ならば、充分実用に耐えるレベルの速度が出ているのではないでしょうか。

また、検出結果の動画を確認してみましたが、やはり全体的に検出の精度は悪くなってしまっていました。
以下の画像は最も良く検出できたフレームになりますが、他のフレームでは誤検出が多発していました。

しかし、結果を見た感じでは、データやstep数を増やせばまだまだ精度の向上が見込めるのではないかと考えています。

まとめ

ご覧いただいた通り、お試しレベルであれば非常に簡単に物体検出用の学習済みモデルを作ることができました。
これだけ簡単に試せてしまうとなると、やはり学習させる為の大量のデータをどう確保するのかが重要だな、と身に染みて感じました。

画像データならばスクレイピング等で集められますが、アノテーションデータはまだ人間の手で作る必要がありますので、今後アノテーションデータの作成の段階から試してみようと思います。