第2回 : キャプチャー画像を機械学習で判別、声でお知らせ

※ 更新 ’24/9月
ライブラリ ”Tensorflow” の新バージョン「2.16」対応のため更新しました。
’24/9月より前に本ブログを実行した方は第1回(※'24/9月に更新)に戻って、「4. 仮想環境の構築」から再実行して下さい。

  • 旧記事はこちら
    Tensorflow 2.8用。 Tensorflow2.8でもOpencvとtensorflowを1つの仮想環境下でコード実行可能です。 これができなくて困っている方は前記事「旧1回目」のインストール記事をご覧ください。

 
 

しょくぶつ(^^)です。 さあっ ! いよいよ「スプラトゥーン3のプレイ映像を機械学習で分析。対戦相手のスペシャルを声でお知らせ。」を実行します。
ここまで来れば実行だけは簡単です。

Windows PCは前回と同じく、10年前の古いものでも大丈夫ですよ。

(0)前回からいったん終了して、再開したいときは

前回の作業の後で作業をいったん終えた方も多いはずです。そんなときは次の方法で再開です。

  1. miniconda専用のPowershellを開く
    スタート → Miniconda3 (64bit) → Anaconda Powershell Prompt (miniconda3)
  2. 仮想環境の起動
    conda activate py312-cnn
  3. Spyderの起動
    “spyder” と入力
    ※ スタートメニューからの起動だと失敗することがあります。

(1) 次のURLに私が作成したファイルをアップロードしました。ここからファイルを3つ、ダウンロードしてください。

github.com

  • buki-classification.keras
  • csvファイル
  • CNN-predict-publicTF216.py

(2) ファイルを同じフォルダに置いてください。

(3) 前の記事と同じ構成で、機器を接続してください。

(4) spyderで”CNN-predict-public.py”を開いてください。

(5) 前回の記事と同じようにコード中の cap_dev_no = ? を変えてください。

(6) コードを変更します。

キャプチャー機器の説明書を見て、読み取り解像度を設定してください。私が使ったエレコムのAD-HDMICAPBKだと、少なくとも次の2パターンが可能でした。

  • パターン1
      cap_W = 1920
      cap_H = 1080
  • パターン2
      cap_W = 1280
      cap_H = 720

(7) Run file(F5)してください。

Run file(F5)したあと、10秒くらいしてから、"Enterキーを押したら実行"と表示されます。
そしてEnterを押すと、2~3秒もすれば読み取ったブキ名とスペシャル名を発音します。

「スペシャルを声でお知らせ」が成功した様子

注意) "Enterキーを押したら実行"は試合中の画面(例えば上の図)が出てからにしてください。

うまく動かないのは、たいてい、上記(6)の設定ミスです。

(8)コードについて

コード内で記載の通り、予測モデルは機械学習、具体的には 畳み込みニューラルネットワーク(CNN)を駆使して訓練して作成しています。
今回のコードでは予測モデルを使うだけなので、

  • tensorflowの keras.models.load_model でモデルを読み込み
  • predict_on_batch で XPdata に格納した画像からブキ名を予測

という、たった2行で予測ができています。

もし機械学習が無ければ、大まじめに、

  • 背景の除去 (古いPCだと1画像あたり10秒 ? )
  • 各ブキ画像との比較
  • 圧縮画像のノイズ対策
  • 試合進行に伴うブキ画像の拡大・縮小対策
  • 正確な切り取り位置合わせ

などをコード化する必要があるわけです。

   Python初心者には難しかったと思います。
もしかするとPythonというよりはプログラム初心者の方もご覧になっているのでしょうか?
そんな方は、まずは最終行の

  speak_special(predicted_no)

と、その4行上の

  show_predict_buki(predicted_no, Img_cut_org)

を入れ替えて、先にスペシャルの名前発音を試すところから始めるといいかもしれませんね。
元のままだと、試合開始直後に"Enterキーを押したら実行"しても、スペシャル発音のタイミングで遅くて、相手が既にスペシャルを発動していたりするんですよね。

もうすこし詳しい方は、発音後に、スペシャルのイラストも表示できように改造してはどうでしょうか?
サブの発音・表示もできるようにするといいですね。

更に詳しい方は、あまり負荷が増えない程度に、camera.read()をループし続けるように改造してみてください。
カメラのバッファーを溜め続けないためで、これをやるとcamera.read()はだいぶ早くなります。

今回は本題の実行が目的なのでコードの詳細は省略しました。
でもコードの中にコメントを多く入れたので、頑張れば理解できるはずです。
コード詳細が知りたい方はリクエストください。

本題の実行そのものは、これでおしまいです。
次回以降は、どうやって予測モデルを作ったか、その方法を少しずつ、書いていきます。
よくある機械学習の記事とは違い、自分で学習データをゼロから用意するという激闘編も予定しています。

それとも、もっと使いやすいツールに改造しましょうかね ? ブキ分析以外をやってみますかね ?

# -*- coding: utf-8 -*-
"""
Created on Sun Sep 31 10:00:14 2023

@author: plant-smile
"""
# 動作確認時の主なライブラリのバージョン
# keras                     3.4.1
# matplotlib                3.9.1
# numpy                     1.26.4
# opencv-python             4.10.0.84
# pandas                    2.2.2
# pip                       24.0
# python                    3.12.4
# pyttsx3                   2.90
# scikit-learn              1.5.1
# tensorflow                2.16.1
# 更新履歴
# CNN-predict-public.py → CNN-predict-publicTF216.py
# 1. TensorFlow 2.8から2.16に対応するため、 load_modelのimportを変更
# 2. 画像を縮小関数original_resizeを、アフィン変換を駆使するように変更(機能は同じ)
# 3. カットした画像のファイル保存関数 save_cut_img を追加。

import numpy as np
import pandas as pd
import pyttsx3
import cv2
import matplotlib.pyplot as plt
# 旧 from tensorflow.python.keras.models import load_model
from tensorflow import keras
import datetime

# # # # #
cap_dev_no = 1
cap_W = 1920
cap_H = 1080
# # # # #

def original_resize(img_loc, reduce_size_loc): 
    img_H, img_W, img_c = img_loc.shape
    # 画像imgの縮小比。 横幅をreduce_size_locピクセルに収める。
    W_ratio = reduce_size_loc/img_W
    # そのように縮小した際の高さ
    H_img2 =img_H*W_ratio
    # アフィン変換
    # https://imagingsolution.net/program/python/opencv-python/opencv_python_affine_transformation/#toc3
    # ここでは、アスペクト比を変えずに、画像を縮小するのにアフィン変換を使用
    affineMatrix = cv2.getRotationMatrix2D( (0,(reduce_size_loc-H_img2)/2),
                                           0, W_ratio)
    img2 = cv2.warpAffine(img_loc, affineMatrix, 
                          (reduce_size_loc,reduce_size_loc),
                          borderMode=cv2.BORDER_REPLICATE)
    return(img2)

def cap_camera_to_img(): 
    print("プレイ中画像読み込み")
    camera = cv2.VideoCapture(cap_dev_no,cv2.CAP_DSHOW)
    camera.set(cv2.CAP_PROP_BUFFERSIZE, 1) # バッファーを少なくする(デフォルトは3)
    camera.set(cv2.CAP_PROP_FRAME_WIDTH,cap_W)
    camera.set(cv2.CAP_PROP_FRAME_HEIGHT,cap_H)
    ret, img_loc = camera.read()
    camera.release()
    return(img_loc)

def cut_img_to_XPdata(img_loc):
    # 画面右上に表示される相手側のブキ画像4個(幅:高さ=80:64=5:4)を切り出して
    # XPdataに格納
    # すこし大きめに切り出している理由は、ガチマッチ進行によるブキ画像の拡大表示に
    # 対応するため。
    XPdata =[]
    Img_cut_org = []
    Htop, Hbottom = int(16/720*cap_H), int((16+64)/720*cap_H)
    Wtop = int(692/1280*cap_W)
    for i in range(4):
        Wbottom = Wtop + int(80/1280*cap_W)
        img_cut = img_loc[Htop: Hbottom, Wtop: Wbottom]
        # オリジナルのカット画像の保管(ファイル保存したいとき用)
        Img_cut_org.append (img_cut)
        # サイズを64x64に統一
        img_cut = original_resize(img_cut, 64)
        XPdata.append (img_cut)
        Wtop = Wtop + int(59/1280*cap_W)
    # np.arrayが必要な理由。
    # 1. tensorflowで作成したモデルへの入力はnp.arrayである必要があるため。
    # 2. [NParray,NParray,NParray,NParray,]というリストを
    #   (4, 64,64,3)のnp.array of unit8に、さらに/255することでfloat64 に
    #   変換するため。
    XPdata=np.array(XPdata)/255
    return(XPdata, Img_cut_org)

def show_predict_buki(predicted_no, Img_cut_org):
    print("取り込んだ画像と予測categoriesの表示")
    # Pillow(PIL)を使って4個のブキ画像を並べて表示します。
    plt.figure(figsize=(15,15))
    for i in range(4):
        plt.subplot(2,2,i+1)
        plt.xticks([])
        plt.yticks([])
        img_cut = cv2.cvtColor(Img_cut_org[i], cv2.COLOR_BGR2RGB)
        plt.imshow( img_cut )
        plt.xlabel( categories[ predicted_no[i] ] )
    plt.show(block=False)

def speak_buki(predicted_no):
    # ブキ名を発音します。
    for i in range(4):
        engine.say( name_buki[ predicted_no[i] ] )
        engine.runAndWait()
        print( name_buki[ predicted_no[i] ] )

def speak_special(predicted_no):
    # スペシャル名を発音します。
    engine.say('スペシャルは')
    engine.runAndWait()
    print('スペシャルは')
    for i in range(4):
        engine.say( name_special[ predicted_no[i] ] )
        engine.runAndWait()
        print( name_special[ predicted_no[i] ] )

def save_cut_img(predicted_no, Img_cut_org):
    # 画像の保存        
    now = datetime.datetime.now()
    for i in range(4):
        f_name = './cap_screen/cap_' + categories[ predicted_no[i] ] \
             + now.strftime('%Y%m%d_%H%M%S') +str(i)
        cv2.imwrite(f_name + ".png", Img_cut_org[i])

if __name__ == '__main__':
    # モデル読み込み (Tensorflow使用, 畳み込みニューラルネットワーク(CNN)を
    # 駆使して訓練したものです)
    #旧 loaded_model =load_model('buki-classification.h5')
    loaded_model=keras.models.load_model('buki-classification.keras')
    # データベースをdataframeとして読み込み (Pandas使用)
    f_name_in = "buki-spe-list.csv"
    df = pd.read_csv(f_name_in, index_col=0, encoding="Shift-JIS")
    name_buki = df["name_J"].tolist()
    name_special = df["func"].tolist()
    filename_special = df["fname_func"].tolist()
    categories = df.index.values.tolist()
    # 音声合成ライブラリpyttsx3の初期化
    engine = pyttsx3.init()
    # 音声のスピード調整
    rate = engine.getProperty('rate')
    print('デフォルトの音声スピード: {}'.format(rate))
    engine.setProperty('rate', 200)

    while True:
        input("Enterキーを押したら実行")
        plt.close()
        # プレイ中画像読み込み
        img = cap_camera_to_img()
        # 画面右上に表示される相手側のブキ画像4個を切り出してXPdataに格納
        XPdata, Img_cut_org = cut_img_to_XPdata(img)
        #プレイ中画像の予測 (Tensorflow使用)
        predicted_labels = loaded_model.predict_on_batch(XPdata)
        # 予測した結果(ラベル)をone-hotベクトルから、普通の数値に変換。たとえば
        # predicted_labels[?] = [1,0,0,0,0,0,0,0....,0,0]
        # → predicted_no[?]= 0
        # predicted_labels[?] = [0,0,0,0,0,1,0,0....,0,0]
        # → predicted_no[?]= 5
        predicted_no = []
        for i in range(4):
            predicted_no.append(np.argmax( predicted_labels[i] ))
        # 取り込んだ画像と予測categoriesの表示
        show_predict_buki(predicted_no, Img_cut_org)
        # ブキの名前発音
        speak_buki(predicted_no)
        # スペシャルの名前発音
        speak_special(predicted_no)
        # 画像の保存        
        #save_cut_img(predicted_no, Img_cut_org)
プロフィール
この記事を書いた人
しょくぶつ (^ ^)

機械系の技術者です。学会発表13、論文掲載6(和・英)。プログラミング歴40年(つまり8bit世代ですね)。ゲーム歴40年(ファミコン世代ですね)。
お問い合わせは下記のメールアドレスにお寄せください。
plantsmilehatena@gmail.com

しょくぶつ (^ ^)をフォローする
pythonスプラトゥーン3
しょくぶつ (^ ^)をフォローする
タイトルとURLをコピーしました