第5回: androidスマホだけで機械学習の予測モデルを使おう ! (3) 元のコードをTensorflow lite用に修正

この記事は移転しました。約2秒後に新記事へ移動します。移動しない場合はココをクリックしてください。


しょくぶつ(^^)です。
前回までで書いたように、androidスマホでTensorflowを使って機械学習を使う準備はなかなか大変ですね。
私も書いていて、ちょっと飽きてしまいました。
そこで一番楽しいWindowsでTensorflowを使っていたコードを、androidスマホでTensorflow liteを使って動かすための改変方法を先に書いてしまいます。
Tensorflow lite 、つかってみた、だけじゃないです。バッチリ動いていますよ~っと伝えたくて。
なお、コードを動かしたいだけならば、この回は読み飛ばしてかまいません。

コードの改変を説明します

Windowsで動いていたコードをTermux環境で動かすに当たり、工夫したのは次の2点です。

  • TersorFlowのコードをlite版に書き換え

何カ所か書き換えます。
なお、学習データは予めWindowsパソコンの方で変換します。

  • キャプチャー機器からの画像取り込み

python単体ではできないので、「USBカメラ」というアプリと連携します。

   

次のURLに私が作成したファイルをアップロードしました。

github.com

TersorFlowのコードをlite版に書き換え !! (1)

※ 次のサイトを参考にしました。 https://tpsxai.com/raspberrypi_tensorflow_lite/

TersorFlowで作成した学習データを、予めWindowsの方で変換します。
私が作成したものは"buki-classification.h5"というファイル名です。 これを次のコードで"buki-classification.tflite"というファイルに変換しました。

# -*- coding: utf-8 -*-
"""
Created on Tue Nov 31 13:03:26 2023
@author: plant-smile
"""
import tensorflow as tf
from tensorflow.python.keras.models import load_model

# モデル読み込み (Tensorflow使用, 畳み込みニューラルネットワーク(CNN)を駆使して訓練したものです)
if __name__ == '__main__':
    model =load_model('buki-classification.h5')
    #TFliteモデルへの変換
    converter = tf.lite.TFLiteConverter.from_keras_model(model)
    tflite_model = converter.convert()
    open("buki-classification.tflite", "wb").write(tflite_model)

TersorFlowのコードをlite版に書き換え !! (2)

※ 次のサイトを参考にしました。 https://tpsxai.com/raspberrypi_tensorflow_lite/

いよいよコード自体の書き換えです。
その箇所を挙げていきます。

ライブラリのインポートは次のように変更します。

元
from tensorflow.python.keras.models import load_model
修正
try:
    # linux
    import tflite_runtime.interpreter as tflite
except ImportError:
    # win
    from tensorflow import lite as tflite

お気付きの通り、このように書くことで、このコードをスマホだけでなくWindowsでも動作させることができます。
スマホで動かす前にWindowsで試行する際に便利ですね。

モデルの読み込みは次のように変更します。 これはもう、こういうものだと思ってください。

元
# モデル読み込み (Tensorflow使用, 畳み込みニューラルネットワーク(CNN)を駆使して訓練したものです)
#loaded_model =load_model('buki-classification.h5')
修正
# モデルの読み込み
interpreter = tflite.Interpreter(model_path="buki-classification.tflite")
# モデルのテンソルへの割り当て
interpreter.allocate_tensors()
# 入力の詳細の取得
input_details = interpreter.get_input_details()
# 出力の詳細の取得
output_details = interpreter.get_output_details()

かんじん要の、予測部分です。

元
predicted_labels = loaded_model.predict_on_batch(XPdata)

この1行のコードは、(64,64,3)の画像が4個入った XPdata という(4, 64, 64, 3)のndarrayを入力とします。 そして、予測結果がベクトル4つからなる predicted_labels として出力する、というものです。 これを次のように変更します。

変更
predicted_labels=[]
for i in range(4):
    # バッチ次元の追加と型変換
    XPdata_tmp= np.expand_dims(XPdata[i], axis=0).astype("float32")
    # 入力画像のセット
    interpreter.set_tensor(input_details[0]['index'], XPdata_tmp)
    # 処理実行
    interpreter.invoke()
    # 出力の取り出し
    predicted_labels.append( interpreter.get_tensor(output_details[0]['index']) )

まず、for文で4回ループさせています。 これは、Tensorflow liteでは画像4つをまとめて処理することが(私は)できなかったためです。

次に、「バッチ次元の追加と型変換」を行っています。本コードでは
np.expand_dims(XPdata[i], axis=0).astype("float32")
とすることで、XPdata[i]のaxisを1つ増やして、型をfloat32に変換しています。
ここが大事なので丁寧に説明しますね。
まず、モデルの入力の詳細情報は、すこし前の修正で記載した、

input_details = interpreter.get_input_details()

にて input_details という変数に入っています。 この中身は次の通りです。

[{'name': 'serving_default_conv2d_input:0',
  'index': 0,
  'shape': array([ 1, 64, 64,  3]),
  'shape_signature': array([-1, 64, 64,  3]),
  'dtype': numpy.float32,
  'quantization': (0.0, 0),
  'quantization_parameters': {'scales': array([], dtype=float32),
   'zero_points': array([], dtype=int32),
   'quantized_dimension': 0},
  'sparsity_parameters': {}}]

確認するのは3点です。

  • index  "0"です。

  • shape  array([ 1, 64, 64, 3]) です。

  • dtype  配列の型は float32 です。

その一方、入力データXPdata[i]は(64, 64, 3)と次元が1つ足りません。また、型を調べたところfloat32ではなくfloat64でした。 だから「バッチ次元の追加と型変換」したわけです。
(逆に、lite版ではない tensorflow は、これを自動でやってくれるんですね)

その次は「入力画像のセット」です。これは、さっき書いたようにindexが"0"と分かったので、その通りに
interpreter.set_tensor(input_details[0]['index'], XPdata_tmp)
と記載しました。

最後の「出力の取り出し」は、
predicted_labels.append( interpreter.get_tensor(output_details[0]['index']) )
とすることで、predicted_labelsというリストに追記をしています。

キャプチャー機器からの画像取り込み

python単体ではできないので、「USBカメラ」というアプリと連携します。

キャプチャー機器をUSB端子に接続して「USBカメラ」を起動して、キャプチャー画像は見られるでしょうか?

次は好みですが、私は 全画面表示 にしました。

そうしたら、電源ボタン + 音量ダウンボタン などでスクリーンショットを撮ってください。

そして、スクリーンショット自体の画像サイズと、その画像内のキャプチャー画面のサイズを、パソコンなどで調べて下さい。

そして

# # # # #
#cap_dev_no = 1
cap_W = 1920    # キャプチャー画像の横幅
cap_H = 1080    # キャプチャー画像の縦幅
cap_Left = 215 # キャプチャー画像の横の原点。
cap_Right = cap_Left + 1920
cap_Top = 0 #キャプチャー画像の縦の原点 
cap_Bottom = 1080
# # # # #

に記載して下さい。

そして、実際に使うときは、「USBカメラ」でキャプチャーしつつ、画面上にブキ一覧が表示されたら、スクリーンショットを撮ってください。 スクリーンショットは /sdcard/Pictures/Screenshots/ に保管されるので、この場所のファイルが増えたことをpython側でglob関数とmax関数を使って検知し、そのスクリーンショットを取り込みます。
フォルダ更新監視には Watchdog を使う方法 (ちょっと難しい) がよく使われていますが、本コードではこれで十分でしょう。

これをpython側で実現するために、次のようにコードを変更しました。
なお、sleep文はシステム側でスクリーンショットが書き込み終わる前にpython側が読み込みを開始してしまうのを防ぐためです。
とりあえず0.9秒としましたが、もう少し短めでもいいかも。

元
img = cap_camera_to_img()
※ 「cap_camera_to_img」はopencvを使ってカメラ画像を読み込む自作関数
変更
list_of_files =glob.glob('/sdcard/Pictures/Screenshots/*')
latest_file = max(list_of_files, key=os.path.getctime)
old_l_file=""+latest_file
while (latest_file == old_l_file):
    old_l_file=""+latest_file
    list_of_files =glob.glob('/sdcard/Pictures/Screenshots/*')
    latest_file = max(list_of_files, key=os.path.getctime)
time.sleep(0.9)

その他の変更

発音部分

例えば次のように変更して、termux-tts-speakを使うようにしています。

元
engine.say('スペシャルは')
変更
tmp=subprocess.getoutput("termux-tts-speak スペシャルは")

次回こそは、上記のように変更したコードで機械学習予測モデルを実行して、「スプラの敵のスペシャルを声でお知らせ」までたどり着きたいと思います。