ディープラーニングで物体検出を試してみた - 第1回: 画像の読み込み
こんにちは、けんにぃです。
ナビタイムジャパンで公共交通の時刻表を使ったサービス開発やリリースフローの改善を担当しています。
最近、機械学習の開発を行っていて物体検出について学んだので、ノウハウを前処理・モデルの利用・後処理の 3 回に分けて解説しようと思います。
全記事のリンクはこちらになります。
・第 1 回: 画像の読み込み(前処理)
・第 2 回: TensorFlow Hub を使う(モデルの利用)
・第 3 回: 出力結果をプロットする(後処理)
物体検出とは
物体検出とは画像の中から特定の物体の位置とその物体のカテゴリを推測する問題です。
似たような問題に画像分類がありますが、画像分類は画像に写る物体を分類してくれるだけで、物体の位置までは特定しないという点で物体検出とは異なります。
なぜ物体検出を学んだのか
現在は便利なクラウドサービスが豊富にあるため、機械学習のフローを一から十まで自前で実装することはあまりないと思います。実際、弊社もクラウドサービスを使って機械学習を行っています。しかし機械学習に関する基礎知識がないとクラウドサービスで思ったほどの精度が出なかったときに次の一手を打てなくなるという問題が出てきました。
物体検出の知見を身につけたことで下記のようなアプローチが取れるようになりました。
・クラウドサービスと他のモデルの精度を比較して導入するモデルを検討できる
・自前だと既存のモデルを転移学習して精度向上が図れる
・学習データを作成する際に学習済みモデルを利用できる
3 つ目の話をもう少し詳しく言うと、物体検出は学習データを作成する際に画像のどこに物体がいるのかを示すために物体の座標値を人の手で作っていく必要があります。この作業が人力でとても大変なので、物体の座標値の定義自体を物体検出モデルで行ってみるというアプローチです。
一旦物体検出モデルで物体の座標を特定してもらい、正しく検出されなかった物体だけ人が微修正を施すことで学習データを半自動で作ることができるようになりました。
物体検出の流れ
物体検出を行うための手順とハマったポイントについてまとめます。
1. 画像データを読み込む
2. 画像データを正規化する
3. モデルに画像データを渡して物体検出をする
4. Non-Maximum Suppression を行う
5. 物体を検出した位置を示す領域とカテゴリ名を画像に書き込む
6. 結果を画像データとして出力する
物体検出をとりあえず試すだけなら学習済みのモデルを用意すればよいのですが、その場合でも 6. まで至るのにはそれなりに手間がかかりました。その原因は下記のような問題に直面したためです。
画像処理
画像データの読み書きの仕方にバリエーションが多く
参考サイトによって手順がまちまち
モデルの入手方法
学習済みのモデルはどこから入手するのか(またはどう作るのか)
モデルの使い方
モデルの入出力の構造が分からない
本稿ではこれらの問題を明らかにしていきながら物体検出の流れを見ていこうと思います。TensorFlow で物体検出をするのを前提に解説していきます。
なお本稿で使用するソースコードを下記の GitHub にまとめています。
下記の画像を使って画像処理をやってみようと思います。
dog.jpg
画像処理
画像データの読み込み方には次の 4 通りのやり方があります。
・OpenCV を使う
・Pillow を使う
・TensorFlow を使う
・Keras を使う
画像を機械学習で扱う場合は Pillow を使うケースが一番多いです。しかし本稿ではすべてのライブラリの使い方について解説します。その方が物体検出に関するどのドキュメントを読んでも混乱しないためです。
どのライブラリを使う場合でも TensorFlow のモデルに渡す際はテンソル型に変換する必要があります。テンソルというのは多次元配列という理解で大丈夫です。配列の形状は [batch, height, width, channel] の 4 次元配列にします。
・batch: モデルに渡す画像データ数(サンプル数)
・height: 画像の高さ
・width: 画像の幅
・channel: 画像の色(通常は RGB のデータが入る)
画像を読み込む前に気をつけること
画像データに Exif(位置情報などを保存したデータ)が含まれていると、画像をビューアで見たときとライブラリで読み込んだときで画像の高さ・幅が入れ替わっていることがあります。ビューアは Exif に保存されたカメラの方向を見て画像をよしなに回転表示するからです。
これに気づかずに画像処理を行うと物体検出が正しく行えないので Exif は削除しておいたほうがよいです。Exif の削除は Pillow を使って下記のように出来ます。
from PIL import Image
def remove_exif(image_path):
with Image.open(image_path) as src:
mode = src.mode
size = src.size
data = src.getdata()
with Image.new(mode, size) as dest:
dest.putdata(data)
dest.save(image_path)
Exif を削除した後、画像をビューアで開いたときに画像が回転しているものは Exif によって回転表示されていた画像になります。意図しない回転をしている場合はここで正しい角度に修正しておく必要があります。
それでは各ライブラリで画像を読み込んでみましょう。
OpenCV を使う場合
import cv2 as cv
image = cv.imread("dog.jpg")
image = cv.cvtColor(image, cv.COLOR_BGR2RGB)
読み込んだ結果は NumPy の配列型になります。配列の形状は [height, width, channel] です。OpenCV は色データが BGR の順で格納されるので
cv.cvtColor() で RGB に変換する必要があることに注意してください。
Pillow を使う場合
from PIL import Image
import numpy as np
image = Image.open("dog.jpg")
image = np.array(image)
読み込んだ結果は Image 型になります。Image 型を NumPy の配列型に変換しておきます。これは後でテンソル型に変換する必要があるためです。配列の形状は [height, width, channel] です。
np.array() の代わりに np.asarray() を使っても大丈夫です。
TensorFlow を使う場合
TensorFlow のドキュメントでは下記のやり方が記載されています。
import tensorflow as tf
image = tf.io.read_file("dog.jpg")
image = tf.io.decode_jpeg(image)
tf.read_file() でデータをバイト列として読み込みます(正確にはバイト列が入ったテンソル型になる)。その後 tf.io.decode_jpeg() でバイト列を画像データとしてデシリアライズします。画像のフォーマットを意識せずにデシリアライズしたい場合は tf.io.decode_image() が使用できます。読み込んだ結果はテンソル型になります。
Keras を使う場合
TensorFlow の高水準 API である Keras を使っても画像データを読み込むことが出来ます。
import tensorflow as tf
image = tf.keras.preprocessing.image.load_img("dog.jpg")
image = tf.keras.preprocessing.image.img_to_array(image)
load_img() で読み込んだ結果は Pillow の Image 型になっています。
それを img_to_array() で NumPy の配列型に変換しています。やっていることは Pillow を使う場合とほぼ同じですが下記のような違いがあります。
・Image を np.array() で変換 - 配列の要素型は uint8
・Image を img_to_array() で変換 - 配列の要素型は float32
TensorFlow は暗黙の型変換を許容していないため、思わぬところで型変換されてしまうと入力値を正しくモデルに渡せないことがしばしばあります。そのため各変換の結果の型がどうなるのかは十分に注意しておいたほうが良いです。
下記のように要素型を指定してあげると uint8 の要素に変換できます。
image = tf.keras.preprocessing.image.img_to_array(image, dtype="uint8")
画像データの正規化
モデルが入力値として受け取るテンソルの型が float32 だった場合は、通常はテンソルの値が [0, 1] の範囲に収まるように正規化をする必要があります。
正規化の方法もドキュメントによって複数のやり方が紹介されるので代表的な方法を解説します。いずれのやり方も基本的には同じことをやっているので、物体検出の結果に違いは出ないはずです。
一番シンプルな正規化
一番簡単なやり方は配列の全要素を 255 で割ることです。画像データはピクセルごとに RGB の整数値が [0, 255] で入っているので 255 で割れば [0, 1] に変換できるということです。
image = image / 255
OpenCV で正規化
OpenCV には画像の正規化を行う関数 cv.dnn.blobFromImage() が用意されているため、この関数を使っても正規化が出来ます。
image = cv.dnn.blobFromImage(image, scalefactor=1 / 255, swapRB=True)
swapRB は色データの順序を BGR から RGB に変換するフラグです。
画像の読み込みの説明では cv.cvtColor() で BGR → RGB 変換を行いましたが cv.dnn.blobFromImage() を使えば正規化と同時に RGB に変換することが出来ます。
TensorFlow で正規化
TensorFlow で正規化をする場合は tf.image.convert_image_dtype() が使えます。
image = tf.image.convert_image_dtype(image, tf.float32)
tf.image.convert_image_dtype() は引数で渡された画像データが整数値になっている場合に限り flota32 に変換する際に自動で正規化を行います。
次元拡張とテンソル化
画像データは [height, width, channel] という 3 次元配列で読み込まれますがモデルに画像データを渡すときは [batch, height, width, channel] の 4 次元配列で渡す必要があります。NumPy の関数を使えば配列の次元を拡張することが出来ます。
# その 1
# 画像データを配列の要素にする
image = np.array([image])
# その 2
# 配列要素を新たな軸に沿って結合する
image = np.stack([image], axis=0)
# その 3
# 新たな軸を指定する
image = image[np.newaxis, ...]
# その 4
# 新たな軸を指定する2
image = np.expand_dims(image, axis=0)
どのやり方でも最終的には [1, height, width, channel] という形状の配列になります(バッチ数が 1 なので batch == 1)。
最後にこの 4 次元配列をテンソルに変換します。
# その 1
image = tf.constant(image)
# その 2
image = tf.convert_to_tensor(image)
両者には微妙な違いがあるのですが、知らなくても物体検出は可能なので割愛します。気になる方は下記のページに説明があるので参考にしてください。
https://www.tensorflow.org/api_docs/python/tf/constant
ちなみに TensorFlow には次元拡張をしながらテンソルに変換する関数も用意されています。
# その 1
image = tf.stack([image], axis=0)
# その 2
image = tf.expand_dims(image, axis=0)
まとめ
以上画像データの読み込みと正規化のやり方について解説しました。抑えておくべきポイントをまとめます。
・画像の読み込みと正規化の方法はバリエーションが多く混乱しやすい
・Exif は削除しておく
・OpenCV の色データは BGR で入っているため RGB に変換が必要
・各変換結果における配列の要素型が uint8 or float32 なのかを確認する
・画像データは正規化する
どのライブラリを使えばいいのかは、正直好みのものを使ってもらって大丈夫です。まだ画像処理をしたことがなければ一番事例の多い Pillow を使うことをおすすめします。
次回はテンソル化した画像データをモデルに渡して推論結果を得るところまで解説しようと思います。