MNIST学習済みモデルとOpenCVを使ってリアルタイムに手書き数字を認識させる


 前回、Python+keras+OpenCVを使って、カメラの映像をキャプチャして手書き数字を認識させるコードを解説しましたが、今回は応用編としてリアルタイムで手書き数字を認識させるコードをご紹介します。

構成

 今回は既に学習済みのモデルを使うことを前提にします。まだMNIST学習済みモデルを作ってない方は、ぼくの以前のエントリーを参考にして作ってみてください(露骨な宣伝)。

 今回のは構成といっても、難しいことは特にないです。前回はカメラでの画像取得と判定を別々の処理にしていましたが、判定の処理をカメラループの中に放り込むだけです。
 前もって解説することも特にないので、どぺーっとコード貼ります。
 ※model = load_model("kerastest.h5")"kerastest.h5"の部分は、自分で用意した学習済みモデルのファイル名に変えてください。


from keras.models import load_model
import numpy as np
import cv2

# カメラから画像を取得して,リアルタイムに手書き数字を判別させる。
# 動画表示
cap = cv2.VideoCapture(1)

model = load_model("kerastest.h5") # 学習済みモデルをロード

# 無限ループ
while(True):

    # 判定用データの初期化
    Xt = []
    Yt = []

    ret, frame = cap.read()

    # 画像のサイズを取得,表示。グレースケールの場合,shape[:2]
    h, w, _ = frame.shape[:3]

    # 画像の中心点を計算
    w_center = w//2
    h_center = h//2

    # 画像の真ん中に142×142サイズの四角を描く
    cv2.rectangle(frame, (w_center-71, h_center-71), (w_center+71, h_center+71),(255, 0, 0))

    # カメラ画像の整形
    im = frame[h_center-70:h_center+70, w_center-70:w_center+70] # トリミング
    im = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY) # グレースケールに変換
    _, th = cv2.threshold(im, 0, 255, cv2.THRESH_OTSU) # 2値化
    th = cv2.bitwise_not(th) # 白黒反転
    th = cv2.GaussianBlur(th,(9,9), 0) # ガウスブラーをかけて補間
    th = cv2.resize(th,(28, 28), cv2.INTER_CUBIC) # 訓練データと同じサイズに整形

    Xt.append(th)
    Xt = np.array(Xt)/255

    result = model.predict(Xt, batch_size=1) # 判定,ソート
    for i in range(10):
        r = round(result[0,i], 2)
        Yt.append([i, r])
        Yt = sorted(Yt, key=lambda x:(x[1]))

    # 判定結果を上位3番目まで表示させる
    cv2.putText(frame, "1:"+str(Yt[9]), (10,80), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255,255,255), 1, cv2.LINE_AA)
    cv2.putText(frame, "2:"+str(Yt[8]), (10,110), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255,255,255), 1, cv2.LINE_AA)
    cv2.putText(frame, "3:"+str(Yt[7]), (10,140), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255,255,255), 1, cv2.LINE_AA)
    cv2.imshow("frame",frame) # カメラ画像を表示

    k =  cv2.waitKey(1) & 0xFF # キーが押下されるのを待つ。1秒置き。64ビットマシンの場合,& 0xFFが必要
    prop_val = cv2.getWindowProperty("frame", cv2.WND_PROP_ASPECT_RATIO) # アスペクト比を取得

    if k == ord("q") or (prop_val < 0): # 終了処理
        break

cap.release() # カメラを解放
cv2.destroyAllWindows() # ウィンドウを消す

 ちょっとひっかかったのは、imshow()のタイミング。OpenCVの仕様上、先に文字列を追加してから呼び出さないと文字が描画されないみたいなので、画像の処理~判定を全部行ってから呼び出しています。つまり、リアルタイムと言いつつ本当は1フレーム前の映像に対する結果を表示していることになります。
 ともあれウメハラみたいに1フレームの世界で生きてるレベルじゃないとまず気にならないはずなので、今回の内容程度なら問題ないと思います。

 あと気を付ける点は、判定結果を格納しているXt[]とYt[]はループが終わるたびに初期化しないとどんどん配列が伸びていろいろと酷いことになるので、ループ冒頭で初期化宣言してリセットするようにしています。
 加えて、学習済みモデルのロードは1回で良いので、while()ループの外に置いています。

実行結果

 上位3つの判定結果候補を左上に表示するようにしました。predict()のデータをそのまま使うとeで表示してくるため、非常に読みにくいので小数点以下3位以降は四捨五入するようにしています。
 カッコ内の左側が予測結果のラベル、右側が割合です。このSSの場合「10割で5だ」と答えています。他の数字でもそこそこいい感じの結果を返してくれましたが、4と9は苦手のようでした。

結論

 処理落ち等心配していましたが、特に問題もなくわりとあっさりめのコードでリアルタイム認識はいけました。OpenCV+Kerasでの画像認識もここまで来ると結構実用的になってくる気がします。真ん中にハムスターが映ったら写真撮るとかもこのコードの応用でたぶん行けます。

 あとは自前でデータセット用意するときにいい感じに自動トリミングしてくれる仕組みがあればいろんな敷居がぐんと下がりそう。なにかいい方法ないだろうか。