薬剤師のプログラミング学習日記

プログラミングやコンピュータに関する記事を書いていきます

物体検出モデルYOLOv3で画像から錠剤を検出する

前回は錠剤画像の刻印を強調してみましたが、今度は物体検出モデルを使って薬が写った画像から錠剤(またはカプセル)を検出してみます。
Windows10でkeras版のYOLOv3を使用しました。
環境を構築し、自分で写真を撮ってオリジナルのデータセットを作成して、モデルを学習後に実際に検出させてみる、という手順で書いていきたいと思います。
f:id:enokisaute:20210728010846j:plain


物体検出モデルYOLOとは

物体検出モデルは入力画像を元に複数の物体の種類・位置・サイズを出力することができます。
YOLOは「You Only Look Once」の略で、名前が示す通り一度画像を見ただけで物体検出を行うことができます。他にも以下のような特徴があります。

  • 単一の畳み込みニューラルネットワーク(CNN)を使用している
  • 画像全体をスキャンして候補を選ぶ方式のモデルと比べて高速である。そのため実用的な速度でリアルタイムに物体検出を行うことができる

今回はこのYOLOのバージョン3のモデルを使ってみました。

環境を構築してYOLOv3の動作を確認する

GPU環境がない場合は以前紹介したGoogle Colaboratoryを使うという方法もありますが、今回は自分のPCのGPUを使って学習を行いました。

・Windows10
・Anaconda / Python 3.7
・GPU GTX 1070

まずは適当な名前で仮想環境を作成します。

conda create -n yolo python=3.7

上記の環境を起動した後、yolov3を動かすのに必要なライブラリをインストールしていきます。
バージョンの組み合わせによってはうまく動かないことがあるらしいので、tensorflowとkerasは自分の環境でも安定して動作する(と思われた)以下のバージョンを指定してインストールしました。他のライブラリについては特にバージョンは指定せずに最新のものを入れました。

・keras 2.2.4
・tensorflow 1.13.1
・matplotlib
・pillow
・OpenCV

conda install tensorflow-gpu==1.13.1 keras==2.2.4 pillow matplotlib

pip install opencv-python

このままでやるとモデルのロード時にエラーが出たので、h5pyのバージョンは落として入れなおしました。

conda install h5py==2.10.0


次にソースコードをダウンロードします。
GitHub - qqwweee/keras-yolo3: A Keras implementation of YOLOv3 (Tensorflow backend)
の『↓Code』から『Download ZIP』をクリックしてダウンロード後、適当な場所に展開しておきます。
次に、YOLOv3の事前学習済みモデルの重みを以下のURLからダウンロードして展開したkeras-yolo3-masterフォルダ内に保存します。
https://pjreddie.com/media/files/yolov3.weights


ここまでで一旦YOLOv3が自分の環境で動作するか確認しておきます。
keras-yolo3-masterフォルダに移動後、以下のコマンドでダウンロードしたweightsがkerasで使えるように変換します。

python convert.py yolov3.cfg yolov3.weights model_data/yolo.h5

物体検出したい適当な画像を同じフォルダ内に入れてYOLOv3を実行します。

python yolo_video.py --image

”Input image filename:”と出力されるので用意した画像のファイル名を入力します。
f:id:enokisaute:20210717181913p:plain
人や信号機や車、バスなどが検出されています。事前学習済みのモデルでは80の物体検出ができます。

訓練データとなる画像を集めてアノテーションを行う

画像に写った錠剤とカプセルの2クラスを検出できるようにしたいので、まずはこれらの画像を集めます。今回は自分のスマホのカメラで撮った画像を使うことにしました。
ざっとですが、画像データの概要は以下の通りです。

  • 画像枚数は332枚
  • 錠剤100種類、カプセル15種類くらいの2クラス
  • 一枚の画像に検出対象は1~10個程度。何枚もの画像に同じ物が重複している
  • ほとんどの画像の背景は同じで、基本的には錠剤用のトレイに載せた状態。一包化された状態のものも20枚くらい。
  • 真上からだけでなくやや角度をつけて斜めから撮ったり、照明の色温度を変えて撮影。(後から気づいたことですが、YOLOv3は学習時に画像の明度や色相もランダムに変化させて学習するので、照明に変化をつける必要はなかったと思います。)
  • 画像サイズは1:1(3000x3000)、4:3(4000x3000)、16:9(4000x2250)のものなどが混在している

f:id:enokisaute:20210718112704p:plain

できるだけ少ない画像枚数で良い結果を得たいと思い、画像データを継ぎ足しては学習→結果の確認、と繰り返すうちに統一のない画像サイズのデータとなってしまいました。
ネットで検索していると、まず画像枚数については1カテゴリ1000枚、画像サイズは統一しておく、という情報がありました。1カテゴリあたりの必要枚数についてはともかく、画像サイズについてはソースを追っていくと統一する必要はないのでは、と考えました。

今回は「とりあえず」でやってみましたが、正直不明な点も多かったです。後の記事では、学習用データにはどのような画像を用いれば良いかなども調べてみましたので、よろしければご参照ください。

次はアノテーション(画像中の物体に対して位置・大きさ、クラスを示すこと)です。
アノテーションツールもいろいろあるみたいですが、私はLablelImgをいうツールを使いました。こちらからダウンロードします。

Windows_v1.8.0をダウンロードして起動すると下のような画面が起動します。
「Open Dir」には集めた画像フォルダ、「Change Save Dir」にはアノテーションを保存するフォルダを指定します。
すぐに操作には慣れると思いますが、次の2点を行うと作業がやりやすいと思います。
・windows_v1.8.0>dataフォルダ内のpredefined_classes.txtファイルのクラスをカスタマイズ(自分のデータで使用するクラスに)しておく
・検出対象左上で「w」キーを押してマウスで右下にドラッグすることでボックスを描く
f:id:enokisaute:20210718122452p:plain

YOLOv3を学習する

学習前の下準備

keras-yolo3-masterフォルダ直下を以下のように構成して画像はJPEGImagesフォルダに、作成したアノテーションファイル(xmlファイル)はAnnotationsフォルダに置きます。

└── keras-yolo3-master
    ├── VOCdevkit
    │   └── VOC2007
    │      ├── Annotations
    │      ├── ImageSets
    │      │   └── Main
    │      └── JPEGImages

Mainフォルダにはtrain.txt、val.txt、test.txtの3つのファイルを作成します。そのうちtrain.txtは次のように各画像ファイル名から拡張子を除いたものを並べます。*1val.txtとtest.txtは空ファイルで構いません。

jpg_file001
jpg_file002
jpg_file003
.
.

次にこれらVOCdevkitにある画像ファイルのパス、アノテーション、クラス情報が載ったtxtファイルを作成します。
まずvoc_annnotation.pyの6行目のclasses =[]を自作データのクラスに合わせて変更します。
今回は"tablet"と"capsule"でアノテーションしたので次のようにします。

classes = ["tablet", "capsule"]

以下を実行するとkeras-yolo3-masterにtxtファイルが3つ作成されます。

python voc_annotation.py

model_dataフォルダのvoc_classes.txtも以下のように修正します。上で修正したvoc_annotation.pyのclassesと同じ並びにしておきます。

tablet
capsule


自作データのアンカーボックスを求める

YOLOv3は3つの異なるスケールで検出を行っており、このスケールごとに3つのアンカーボックス、計9つのアンカーボックスを使用しています。様々なサイズ、縦横比の異なるアンカーボックスを用意してボックスごとに認識を行い、予測されたボックスの幅と高さを計算しているとのことです。
しかし、model_dataフォルダ内のyolo_anchors.txtのアンカーはCOCOデータセットのものなので、自作データのアンカーを計算するためにはkmeans.pyを実行してk-meansクラスタリングで求めます。
ということで、kmeans.pyの8行目と99行目にあるfilenameを"2007_train.txt"に修正してこれを実行します。

python kmeans.py

次のような表示が出て同フォルダ内にyolo_anchors.txtというファイルが作成されます。

K anchors:
 [[156 153]
 [184 187]
 [197 317]
 [215 213]
 [250 245]
 [281 181]
 [286 291]
 [347 333]
 [432 451]]
Accuracy: 87.66%

作成されたファイルはmodel_dataフォルダに移動させておきます。
私のデータの場合は元々のアンカーボックスで学習するよりも、これを行っておくことでlossが4~5くらい変わってきました。

続いてtrain.pyを一部修正します。

  • 17行目のannotation_path = 'train.txt'はvoc_annotation.pyで作成された'2007_train.txt'に変更
  • 使っているGPUのメモリによってはバッチサイズが大きいとエラーが出て学習が途中で止まるので、適宜変更します。57行目と76行目のbatch_size=32は私はそれぞれ8と4に変更しました。
  • val_split=0.1は検証データの比率ですが、私は0.2に変更して訓練:検証=8:2となるようにしました。
YOLOv3の入力画像サイズについて

25行目のinput_shapeはデフォルトでは(412, 412)ですが、32で割り切れる数字であれば608などの値でも設定できます。
訓練画像データはこのinput_shapeにリサイズされた上でネットワークへ入力されます。デフォルトでは学習時に、リアルタイムでdata augmentation(データ拡張)され、拡大や縮小といった変化も加えられています。
なので、極端に小さい、大きいなどの画像サイズが混ざっているのでなければ、こちらで前もって統一したサイズにしておかなくても大きな問題はなさそうです。
YOLOv3が実際にどのようなデータ拡張を行っているかも調べたので、これについてもまた書きたいと思っています。


今回は110エポック回して、lossはだいたい25くらいまで減少しました。
以下のグラフは訓練データのlossです。「畳み込みニューラルネットワークを自作の手書き数字画像に利用する」でやったようにTensorBorardでログを確認することができます。
train.py実行前に以下のコマンドで起動しておき、ブラウザでhttp://localhost:6006/にアクセスします。

tensorboard --logdir=</logs/000のフルパス> --host 0.0.0.0

f:id:enokisaute:20210728005807p:plain
lossはまだまだ低いとは言えませんが、とりあえず、このまま出来た重みを使って実際に画像から錠剤を検出できるかやってみました。

学習後のモデルで錠剤(カプセル)を検出させてみる

学習が終わったらlogs/000フォルダにできたモデルの重みファイル「trained_weights_final.h5」をmodel_dataフォルダに移します。(またはyolo.pyの"model_path"を書き換えます)
今回は静止画なので、上でやったのと同じようにyolo_video.pyを実行すればOKです。

そのまま検出ボックスを表示させるとオブジェクトが近いところではボックスが重なって見にくくなってしまうので、コードを次のように修正しました。
yolo.pyのdetect_image()内の132行目あたりの以下の部分です。3e-2を2e-2に、//300を//450にして(値の大きさで調節可能)ボックスの文字と枠が小さく細く表示されるようにしました。

font = ImageFont.truetype(font='font/FiraMono-Medium.otf',
                    size=np.floor(2e-2 * image.size[1] + 0.5).astype('int32'))
        thickness = (image.size[0] + image.size[1]) // 450

物体検出モデルの性能評価にはmAP(Mean Average Precision)という評価指標が使われるようですが、今回はいくつかの画像から実際に錠剤やカプセルが検出できるかを見てみます。
f:id:enokisaute:20210728015457j:plain
すべての錠剤とカプセルを正しく検出できています。ですが、これは訓練データにあった画像なのでうまくいっても当然です。
今度は訓練データにはない画像でやってみます。
錠剤画像の刻印(識別コード)を強調させて表示する」で使用した画像です。
f:id:enokisaute:20210728003758j:plain
こちらもきれいにできているようです。
他に、一包化されたものはどうか見てみました。錠剤がわざと印字の部分に隠れるようにしてみましたがうまく検出できています。
f:id:enokisaute:20210728004830j:plain
ここにはたまたまうまくいった例を載せてみましたが、錠剤に比べてカプセルがうまく検出できなかったり(データが少なかったためか)、一包化されたものは光の反射があったり印字との重なり具合などの、ほんのちょっとした違いで検出できない場合もありました。
次はもう少し掘り下げてやってみたいと思います。
www.yakupro.info


*1:最終行には改行を入れます