Study Blog

自分の興味関心が向いたものを、好きな時好きなだけ気分で勉強したことを記すブログ

Raspberry Pi4 画像認識 ~⑧独自画像学習 RPi上で推論~

①~⑦までで行った、セットアップから学習まで行ってきました。
学習モデルが完成したので、そのモデルをエッジデバイスとするラズパイに移行してリアルタイム推論を行ってシリーズは終了です。

1. google driveからラズパイへ.tfliteモデルを移行する

windows <-> ラズパイ間のファイルのやり取りを行うために、「samba」というファイルサーバーを入れます。
ここは、ラズパイのターミナル上で行います。

1-1. sambaをインストール

sudo apt install samba -y

1-2. 設定ファイルをconfファイルに追記

バックアップを取っておきます。

sudo cp /etc/samba/smb.conf /etc/samba/smb.conf_backup

nanoというエディタで編集します。

sudo nano /etc/samba/smb.conf 
[pi]
comment = pi
path = /home/pi
force user = pi
read only = no
browsable = yes
public = yes

sambaサービスの再起動

sudo systemctl restart smbd

windowsエクスプローラーの「ネットワーク」を開いて
¥¥(ラズパイのIPアドレス)を入力すると接続できます。

そしたら、以前作成した.tfliteファイルをラズパイ上の適当なフォルダに移行してあげます。

2. 推論プログラム作成

基本的に以前作成したプログラムを使用しますが、一部映像をリアルタイムで推論できるように変更します。

2-1. ライブラリのインポート

まず、必要なライブラリをインポートします。

from picamera2.picamera2 import *
import time

import cv2
import numpy as np
import matplotlib.pyplot as plt

import tensorflow as tf
from tflite_support import metadata

2-2. 準備

MODEL_PATH = "model.tfliteの絶対パス"
SAVE_PATH = "撮影した画像の保存ディレクトリの絶対パス"

# パイカメラの準備(bullseye用)
camera = Picamera2()
camera.start_preview()
camera.configure(camera.preview_configuration())
camera.start()

# tfliteのメタデータ(グー/チョキ/パー)の取得
displayer = metadata.MetadataDisplayer.with_model_file(MODEL_PATH)
for i, file_name in enumerate(displayer.get_packed_associated_file_list()):
    displayer.get_associated_file_buffer(file_name)

label = str(displayer.get_associated_file_buffer(file_name))[2:]
label_list = label.split('\\n')

classes = label_list[:-1]

2-3. 映像をテンソルに変換する関数

コメントのように、映像をnumpy配列として読み込むためそれをそのままtensor型へ変更します。

def preprocess_image(image_path, input_size):
    """Preprocess the input image to feed to the TFLite model"""
    
    """
    # 画像を1枚ずつ読み込む場合
    img = tf.io.read_file(image_path)
    img = tf.io.decode_image(img, channels=3)
    img = tf.image.convert_image_dtype(img, tf.uint8)
    """

 # 映像をnumpy配列として読み込むので、そのままtensorへ変更する
    img = tf.convert_to_tensor(image_path) 

 # 変更なし
    original_image = img
    resized_img = tf.image.resize(img, input_size)
    resized_img = resized_img[tf.newaxis, :]
    resized_img = tf.cast(resized_img, dtype=tf.uint8)
    return resized_img, original_image

2-4. オブジェクト(手)を探す関数

変更ありません

def detect_objects(interpreter, image, threshold):
    """Returns a list of detection results, each a dictionary of object info."""

    signature_fn = interpreter.get_signature_runner()

    # Feed the input image to the model
    output = signature_fn(images=image)

    # Get all outputs from the model
    count = int(np.squeeze(output['output_0']))
    scores = np.squeeze(output['output_1'])
    classes = np.squeeze(output['output_2'])
    boxes = np.squeeze(output['output_3'])

    results = []
    for i in range(count):
        if scores[i] >= threshold:
            result = {
            'bounding_box': boxes[i],
            'class_id': classes[i],
            'score': scores[i]
            }
            results.append(result)
    return results

2-5.推論実行関数

def run_odt_and_draw_results(image_path, interpreter, threshold=0.6):
    """Run object detection on the input image and draw the detection results"""
    # Load the input shape required by the model
    _, input_height, input_width, _ = interpreter.get_input_details()[0]['shape']

    # Load the input image and preprocess it
    preprocessed_image, original_image = preprocess_image(
        image_path,
        (input_height, input_width)
    )

    # Run object detection on the input image
    results = detect_objects(interpreter, preprocessed_image, threshold=threshold)

    # Plot the detection results on the input image
    original_image_np = original_image.numpy().astype(np.uint8)
    for obj in results:
        # Convert the object bounding box from relative coordinates to absolute
        # coordinates based on the original image resolution
        ymin, xmin, ymax, xmax = obj['bounding_box']
        xmin = int(xmin * original_image_np.shape[1])
        xmax = int(xmax * original_image_np.shape[1])
        ymin = int(ymin * original_image_np.shape[0])
        ymax = int(ymax * original_image_np.shape[0])

        # Find the class index of the current object
        class_id = int(obj['class_id'])

        # Draw the bounding box and label on the image
        color = [int(c) for c in COLORS[class_id]]
        cv2.rectangle(original_image_np, (xmin, ymin), (xmax, ymax), color, 2)
        # Make adjustments to make the label visible for all objects
        y = ymin - 15 if ymin - 15 > 15 else ymin + 15
        label = "{}: {:.0f}%".format(classes[class_id], obj['score'] * 100)
        cv2.putText(original_image_np, label, (xmin, y),
        cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)

    # Return the final image
    original_uint8 = original_image_np.astype(np.uint8)
    return original_uint8

2-6. ラズパイカメラからの映像をrgbへ変更する関数

この部分は要改良の余地ありです。
ラズパイからの映像をpicamera2モジュールで読み込むと、rgbaという形式になっていました。
numpy配列をtensorとして読み込むためには、rgba -> rgbへ次元を変更する必要があります。
そのため、以下の関数を追加しました。

def rgba2rgb( rgba, background=(255,255,255) ):
    # reference(https://stackoverflow.com/questions/50331463/convert-rgba-to-rgb-in-python)
    row, col, ch = rgba.shape

    if ch == 3:
        return rgba

    assert ch == 4, 'RGBA image has 4 channels.'

    rgb = np.zeros( (row, col, 3), dtype='float32' )
    r, g, b, a = rgba[:,:,0], rgba[:,:,1], rgba[:,:,2], rgba[:,:,3]

    a = np.asarray( a, dtype='float32' ) / 255.0

    R, G, B = background

    rgb[:,:,0] = r * a + (1.0 - a) * R
    rgb[:,:,1] = g * a + (1.0 - a) * G
    rgb[:,:,2] = b * a + (1.0 - a) * B

    return np.asarray( rgb, dtype='uint8' )

2-7.メイン実行プログラム

上記で作成した関数を使用して実行関数を作成して終了です。

if __name__ == '__main__':
    # 画像を撮影した際の番号
    count = 1
    # 推論の際の四角の色を指定
    COLORS = np.random.randint(0, 255, size=(len(classes), 3), dtype=np.uint8)

    # 推論と映像表示
    while True:
        image = camera.capture_array()
        image = rgba2rgb(image)    # rgba を rgb へ変更

        # 映像表示を終了する際のキー指定
        key = cv2.waitKey(1)
        if key == 27: # when ESC key is pressed break
            break

        # 推論
        interpreter = tf.lite.Interpreter(model_path=str(MODEL_PATH))
        interpreter.allocate_tensors()
        result = run_odt_and_draw_results(image, interpreter=interpreter)

        # 撮影をする際のキー指定
        if key == ord('c'):    # when "c" key is pressed capture image as "SAVE_PATH/(count).png"
            cv2.imwrite(SAVE_PATH + str(count) + '.png', result)
            count += 1

        cv2.imshow('View', result)
        
    # 終了する際のプログラム
    camera.close()
    cv2.destroyAllWindows()

3. 推論実行

上記で準備したプログラムをラズパイ上で実行すると、このようになりました。
パー

チョキ

グー

まず、全体的に画像が青みがかっています。
これは、「rgba2rgb」関数の部分で生じている問題です。今後調査をして改良しようと思います。

次に、推論精度が全体的に低めです。
これは、モデル作成段階である程度分かっていたことで、画像の枚数を増加させることで改善できると思います。

おおざっぱではありますが、0(ラズパイのセットアップ)~10(オリジナルモデルの作成・推論)までを解説してまいりました。
これで簡単なPoC程度を行うことはできるかと思います。
さらなる改善のためには、機械学習自体を深く学んだり、問題そのものへの深い理解が必要になると思います。

実際に準備から推論することを経験する人はなかなかいないと思います。
また、セットアップ->オリジナルモデルの作成->推論までを一挙に解説している記事などがなかったため作成してみました。

情報は"生もの"で時間経過と共に、各ライブラリのアップデートやサポート終了等によって使えなくなってしまいます。
可能な限り最新の情報を得ながら実践してみてください。

お疲れさまでした。

          • 関連記事-----

melostark.hatenablog.com
melostark.hatenablog.com
melostark.hatenablog.com
melostark.hatenablog.com
melostark.hatenablog.com
melostark.hatenablog.com
melostark.hatenablog.com