ディープラーニングで物体検出を試してみた - 第2回: TensorFlow Hub を使う
こんにちは、けんにぃです。
ナビタイムジャパンで公共交通の時刻表を使ったサービス開発やリリースフローの改善を担当しています。
物体検出の解説 2 回目になります。1 回目では画像データの読み込みと正規化について解説しました。今回はモデルを入手して推論するところまでを解説しようと思います。
全記事のリンクはこちらになります。
・第 1 回: 画像の読み込み(前処理)
・第 2 回: TensorFlow Hub を使う(モデルの利用)
・第 3 回: 出力結果をプロットする(後処理)
モデルを入手する
精度を追求しなければフルスクラッチでモデルを作らなくても学習済みのモデルを直接使うか、学習済みのモデルを転移学習させるだけでそこそこの精度のモデルを用意できます。
そのような目的のために TensorFlow Hub という、学習済みのモデルを管理するリポジトリが活用できます。
解きたい問題ごとにモデルを検索することが出来ます。下記は物体検出用のモデルを検索した結果です。
https://tfhub.dev/s?module-type=image-object-detection
今回はこれらのモデルの中から Faster R-CNN というモデルを使って物体検出をしてみようと思います。
モデルの入手
Faster R-CNN のモデルのページがこちらになります。
https://tfhub.dev/tensorflow/faster_rcnn/inception_resnet_v2_640x640/1
このモデルは COCO 2017 という有名なデータセットを学習したモデルになっています。
まずは TensorFlow Hub のモデルをコード上で扱えるようにするため下記の Python パッケージをインストールします。
$ pip install tensorflow_hub
モデルのロードは下記のようにモデルの URL を指定するだけです。
import tensorflow_hub as hub
model = hub.load("https://tfhub.dev/tensorflow/faster_rcnn/inception_resnet_v2_640x640/1")
ただしこのやり方だとダウンロードしたモデルがテンポラリディレクトリに保存されてしまうためモデルのデータがディスクにキャッシュされません。
これを防ぐためには TFHUB_CACHE_DIR という環境変数に好きなディレクトリを定義します。そうするとそのディレクトリにモデルが保存されるようになります。
export TFHUB_CACHE_DIR=~/.cache/tfhub_modules
環境変数を設定して先程の Python コードを実行すると下記のような構成でファイルが保存されます。
~/.cache/tfhub_modules
├── d23662e9580da74332b143db23a6a5e3e9887a13
│ ├── saved_model.pb
│ └── variables
│ ├── variables.data-00000-of-00001
│ └── variables.index
└── d23662e9580da74332b143db23a6a5e3e9887a13.descriptor.txt
モデルの構造を確認
モデルを使用するには下記の 2 つを確認する必要があります。
・入力値となるテンソルの形状と型
・出力値の構造
モデルのページにこれらの情報が記載されています。
入力値
テンソルの形状
[1, height, width, 3]
3 はチャネル (RGB) データで [0, 255] の整数
テンソルの型
tf.uint8
1 回目で解説しましたが画像データは正規化をすると [0, 1] の tf.float32 型になります。しかし形状と型を見る限り、このモデルの入力は正規化をする前の画像データを渡せば良さそうです。入力値の型はモデルによるので、正規化が必要なモデルも存在します。
またバッチには未対応とも書かれています。つまり一度に一つの画像データしか渡せないということです。テンソルの形状の最初の軸が 1 となっているのはそのためです。
出力値
出力値は下記のテンソルを含む辞書になっています。
・num_detections: 検出した物体数
・detection_boxes: 検出した物体を囲むバウンディングボックスの座標
・detection_classes: 検出した物体のカテゴリ ID
・detection_scores: 検出した物体のスコア(正確さの割合)
・raw_detection_boxes: NMS 実行前のバウンディングボックスの座標
・raw_detection_scores: NMS 実行前のスコア
・detection_anchor_indices: アンカーボックスのインデックス
・detection_multiclass_scores: 物体が各カテゴリに属する確率
※ Non-Max Suppression (NMS) については後ほど解説します。
モデルの入出力に関する説明がドキュメントに記載されていない場合は
ある程度推測しないといけなくなりますが saved_model_cli というコマンドを使うとモデルの構造を調べることが出来ます。
saved_model_cli は TensorFlow をインストールすると使うことが出来るようになります。ちなみに SavedModel というのは TensorFlow が提供している、モデルを保存するためのデータフォーマットです。
$ saved_model_cli show --all --dir ~/.cache/tfhub_modules/d23662e9580da74332b143db23a6a5e3e9887a13
実行すると標準出力にモデルの入出力の形式が出力されます。
MetaGraphDef with tag-set: 'serve' contains the following SignatureDefs:
signature_def['__saved_model_init_op']:
The given SavedModel SignatureDef contains the following input(s):
The given SavedModel SignatureDef contains the following output(s):
outputs['__saved_model_init_op'] tensor_info:
dtype: DT_INVALID
shape: unknown_rank
name: NoOp
Method name is:
signature_def['serving_default']:
The given SavedModel SignatureDef contains the following input(s):
inputs['input_tensor'] tensor_info:
dtype: DT_UINT8
shape: (1, -1, -1, 3)
name: serving_default_input_tensor:0
The given SavedModel SignatureDef contains the following output(s):
outputs['detection_anchor_indices'] tensor_info:
dtype: DT_FLOAT
shape: (1, 100)
name: StatefulPartitionedCall:0
outputs['detection_boxes'] tensor_info:
dtype: DT_FLOAT
shape: (1, 100, 4)
name: StatefulPartitionedCall:1
outputs['detection_classes'] tensor_info:
dtype: DT_FLOAT
shape: (1, 100)
name: StatefulPartitionedCall:2
outputs['detection_multiclass_scores'] tensor_info:
dtype: DT_FLOAT
shape: (1, 100, 91)
name: StatefulPartitionedCall:3
outputs['detection_scores'] tensor_info:
dtype: DT_FLOAT
shape: (1, 100)
name: StatefulPartitionedCall:4
outputs['num_detections'] tensor_info:
dtype: DT_FLOAT
shape: (1)
name: StatefulPartitionedCall:5
outputs['raw_detection_boxes'] tensor_info:
dtype: DT_FLOAT
shape: (1, 300, 4)
name: StatefulPartitionedCall:6
outputs['raw_detection_scores'] tensor_info:
dtype: DT_FLOAT
shape: (1, 300, 91)
name: StatefulPartitionedCall:7
Method name is: tensorflow/serving/predict
Defined Functions:
Function Name: '__call__'
Option #1
Callable with:
Argument #1
input_tensor: TensorSpec(shape=(1, None, None, 3), dtype=tf.uint8, name='input_tensor')
推論を実行する
それではモデルで推論を行ってみましょう。画像データの読み込みからモデルに投入するまでの一連の流れをコードでまとめると下記のようになります。
import tensorflow as tf
import tensorflow_hub as hub
# 画像データの読み込み
image = tf.io.read_file("dog.jpg")
image = tf.io.decode_jpeg(image)
# image = tf.image.convert_image_dtype(image, tf.float32) # 今回は正規化は不要
image = tf.expand_dims(image, axis=0)
# モデルをロード
model = hub.load("https://tfhub.dev/tensorflow/faster_rcnn/inception_resnet_v2_640x640/1")
# 推論を実行
outputs = model(image)
なお、画像はこちらを使用しました。
dog.jpg
推論が実行できたら次は outputs のテンソルを紐解いていきましょう。
物体の検出数
物体の検出数は outputs["num_detections"] にバッチごと(画像データごと)に入っています。
・outputs["num_detections"][0]:1 枚目の画像に対する物体検出数
・outputs["num_detections"][1]:2 枚目の画像に対する物体検出数
・...
このモデルはバッチ処理が出来ないという記載がありますので、実際には画像データ 1 枚分の結果しか入っていません。
(つまり outputs["num_detections"][1] は存在しない)
型がテンソル型になっているので int にキャストすることで検出数が取り出せます。
>>> int(outputs["num_detections"][0])
100
100 個の物体が検出されています。
物体のカテゴリ
物体のカテゴリ(人か犬かといった情報)は outputs["detection_classes"] にバッチごとに入っています。
>>> outputs["detection_classes"][0]
<tf.Tensor: shape=(100,), dtype=float32, numpy=
array([18., 3., 3., 38., 34., 1., 3., 18., 3., 3., 64., 3., 3.,
3., 62., 1., 43., 34., 90., 3., 3., 18., 1., 38., 3., 38.,
27., 2., 1., 34., 3., 3., 64., 31., 18., 3., 1., 90., 28.,
64., 27., 2., 1., 2., 8., 1., 64., 64., 8., 34., 84., 3.,
64., 1., 64., 90., 15., 84., 17., 1., 1., 8., 64., 6., 1.,
3., 3., 64., 62., 3., 1., 18., 43., 38., 31., 18., 1., 1.,
17., 62., 28., 84., 34., 1., 28., 43., 34., 38., 27., 1., 63.,
18., 64., 1., 31., 1., 64., 3., 67., 56.], dtype=float32)>
検出数と等しく 100 個の要素が入っています。これらの数値は各カテゴリ名に対して割り振られた ID のため float32 を int にキャストして使用します。
# テンソル型から NumPy の配列型に変換し、要素を int にキャストする
>>> outputs["detection_classes"][0].numpy().astype(int)
なお配列の要素は確率の高い順にソートされているため、先頭要素が一番高確率で検出できた物体になります。つまり、この画像は ID が 18 の物体が高確率で検出できていることになります。
では ID に対するカテゴリ名はどこで確認できるかというと残念ながら
モデルにはそのようなデータは同梱されていません。モデルを作成する時に使用された COCO 2017 の中に ID とカテゴリ名の対応マップがあるのですが
それと等価なデータがこちらのリポジトリにも保存されています。
このリポジトリは Faster R-CNN のモデルを作成したプロジェクト TensorFlow Model Garden のリポジトリです。
この対応マップで ID 18 のカテゴリ名を確認すると...
item {
name: "/m/0bt9lr"
id: 18
display_name: "dog"
}
どうやら犬が検出されたようです。
物体のスコア
検出した物体の正確さを示すスコアは outputs["detection_scores"] にバッチごとに入っています。
>>> outputs["detection_scores"][0]
<tf.Tensor: shape=(100,), dtype=float32, numpy=
array([0.99925274, 0.0648334 , 0.01799043, 0.01530765, 0.01412826,
0.01380715, 0.0116262 , 0.01066197, 0.00900557, 0.0081861 ,
0.00731483, 0.00696975, 0.0065852 , 0.00630195, 0.00616357,
0.00611935, 0.00587029, 0.00566598, 0.00556432, 0.00544886,
0.0050333 , 0.00494089, 0.00466382, 0.00457683, 0.00424531,
0.00419263, 0.00397073, 0.003891 , 0.00386721, 0.00383355,
0.00372294, 0.00354954, 0.00351555, 0.00347336, 0.00346773,
0.00331007, 0.00307669, 0.00290215, 0.00288037, 0.00281268,
0.00278104, 0.00269164, 0.00263826, 0.00261052, 0.00258567,
0.00255021, 0.00249831, 0.00248101, 0.00247912, 0.0024005 ,
0.00229001, 0.00225819, 0.00221491, 0.00214771, 0.00214332,
0.00205837, 0.00205029, 0.0020309 , 0.00193076, 0.00181139,
0.00180791, 0.00179861, 0.00174428, 0.0017014 , 0.00166955,
0.00165858, 0.0016013 , 0.0015927 , 0.00159112, 0.00158385,
0.00157703, 0.00153112, 0.00149884, 0.00146106, 0.00146085,
0.00142878, 0.00140073, 0.00139768, 0.00138087, 0.00136648,
0.00136217, 0.00134561, 0.0013374 , 0.00129804, 0.00128889,
0.00127693, 0.0012628 , 0.00124146, 0.00123321, 0.00123049,
0.00120346, 0.00119727, 0.00119625, 0.00117976, 0.00117661,
0.00114907, 0.00111835, 0.00110908, 0.00108942, 0.00106593],
dtype=float32)>
outputs["detection_classes"] の要素順に対応する形で要素が入っているため、先程の「犬」と検出された物体の正確さは配列の先頭要素 0.99925274 つまり 99.9% です。2 要素目以降はかなり小さい値になっているため、物体は犬以外に何も検出されなかったということになります。
物体の位置
物体の位置を示すバウンディングボックスは outputs["detection_boxes"] にバッチごとに入っています。バウンディングボックスというのは物体を囲む矩形のことです。先頭要素は犬と検出された物体に対するバウンディングボックスの座標値になります。
>>> outputs["detection_boxes"][0, 0] # 0 バッチ目の 0 番目の物体を囲むバウンディングボックス
<tf.Tensor: shape=(4,), dtype=float32, numpy=array([0.17213099, 0.16459921, 0.83958703, 0.7772619 ], dtype=float32)>
座標値 [0.17213099, 0.16459921, 0.83958703, 0.7772619] は [y_min, x_min, y_max, x_max] の順で入っています。
・y_min, x_min: バウンディングボックスの左上の座標
・y_max, x_max: バウンディングボックスの右下の座標
座標値がずいぶん小さい値になっていますが、これは正規化座標と言って
x 座標は画像の幅で割り、y 座標は画像の高さで割った値になっています。
そのため座標値は [0, 1] に収まる値になります。
画像に対する実際の座標値が欲しければ画像の幅・高さを掛ければよいです。
>>> height, width = image.shape[1:3]
>>> box = outputs["detection_boxes"][0, 0] * [height, width, height, width]
座標値は整数で扱うので、整数にキャストしつつ Python のリスト型に変換したければ次のようにします。
>>> box.numpy().astype(int).tolist()
[110, 79, 537, 373]
正しいかどうかは画像にバウンディングボックスを当てはめないと分かりませんが、それっぽい値になってますね!
Non-Max Suppression
最後に Non-Max Suppression (NMS) について解説しておきます。
物体検出のモデルは複数のバウンディングボックスを使って物体の位置を探しに行くため、それら複数のバウンディングボックスが同じ物体を検出してしまうと重なり合ったバウンディングボックスが結果に含まれてしまいます。
17 個のバウンディングボックスが検出された例
このように互いに重なり合ったバウンディングボックスを一つにまとめてしまうことを Non-Max Suppression と言います。outputs["raw_detection_boxes"], outputs["raw_detection_scores"] には NMS 実行前のデータが入っています。今回使用したモデルは勝手に NMS をやってくれていますが、モデルによっては自分で行う必要があります。
TensorFlow には NMS を行う関数が用意されています。
https://www.tensorflow.org/api_docs/python/tf/image#working_with_bounding_boxes
・tf.image.combined_non_max_suppression
・tf.image.non_max_suppression
・tf.image.non_max_suppression_overlaps
・tf.image.non_max_suppression_padded
・tf.image.non_max_suppression_with_scores
NMS のやり方は別のモデルの使い方を解説する時に合わせて行います。NMS については下記のサイトに詳しく書かれていますので、参考にして下さい。
まとめ
物体検出のモデルを利用するときのまとめはこちらです。
・TensorFlow Hub で学習済みモデルが利用できる
・モデルの利用時は入出力の構造を知っておく必要がある
・NMS で重なり合ったバウンディングボックスを一つにまとめる
次回は推論結果を画像に埋め込む処理を解説しようと思います。