« IIJが20ギガで1880円のプラン | トップページ | 4つのカメラを取り付け可能にするアダプタ「Raspberry Pi用マルチカメラアダプタ」 »

2021年2月27日 (土)

Raspberry Pi × 画像認識 で「後出しじゃんけん機」作ってみた

久しぶりの、Raspberry Piネタです。

今日は、どちらかというと「教育用」なネタです。

Raspberry Piと画像認識を応用した「後出しじゃんけん機」なるものを作ってみました。

なんじゃそら?という名前ですが、簡単に言うと、

(1) グー、チョキ、パー をラズパイカメラで撮影して記録 (各30枚づつくらい)

(2) 上の写真を用いて深層学習(CNN)を実施

(3) できたモデルを使い、カメラで撮影した「手」を推論する

(4) 推論結果に対し、勝つ手の画像を表示する

……という、Raspberry Piを使った一種のAIです。

これ、要するに、身近なもので画像認識AIを体感しよう!って趣旨の工作&プログラムです。

通常なら、Raspberry PiとPCを組み合わせるところですが、この両者の行き来が煩わしいので、学習用のデータ取りから学習、そして推論までを、すべてRaspberry Pi上でできるようにしてます。

まず、準備ですが、以下の3つのコードをコピペして持って行ってください。

「1_camera.py」


import glob
import time
import os
import io

# for Raspberry Pi
import RPi.GPIO as GPIO
import picamera

GPIO.cleanup()

#for Raspberry Pi
GPIO.setmode(GPIO.BCM)
port1 = 17 # gu
port2 = 27 # choki
port3 = 22 # pa

GPIO.setup(port1, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.setup(port2, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.setup(port3, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)

count = 1
flag = 0

# File remove
file_list = glob.glob('./data/0_gu/*')
for file in file_list:
    os.remove(file)
file_list = glob.glob('./1_choki/*')
for file in file_list:
    os.remove(file)
file_list = glob.glob('./data/2_pa/*')
for file in file_list:
    os.remove(file)

print('Ready!')

try:
    while True:
        sc = str(count)
        ssc = sc.zfill(4)
        #GPIOの17,27,22がオンになったら、画像を取り込んで認識を開始
        if GPIO.input(port1):
            label = '0_gu'
            flag = 1
        elif GPIO.input(port2):
            label = '1_choki'
            flag = 1
        elif GPIO.input(port3):
            label = '2_pa'
            flag = 1
            
        if flag ==1 :
            print(ssc + ':' + label)
            with picamera.PiCamera() as camera:
                    camera.resolution = (12896)
                    camera.start_preview()
                    camera.capture('./data/'+label+'/'+label+ssc+'.jpg')
            count +=1
            flag = 0

        time.sleep(0.01)

except KeyboardInterrupt:
    GPIO.cleanup()

「2_train.py」


#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import sys
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt

import keras
from keras.utils import np_utils
from keras.models import Sequential
from keras.layers import Dense,Dropout,Flatten
from keras.layers import Conv2D,MaxPooling2D
from keras.preprocessing.image import array_to_img,img_to_array,load_img
from keras import backend as K
from sklearn.model_selection import train_test_split
from keras.models import load_model
from keras.callbacks import ModelCheckpoint

# ======= hypter param =====
batch_size = 10
epochs = 50
# ==========================

path=os.getcwd()+'/data/'

class_count = 0
folder_list=os.listdir(path)

for folder in folder_list:

  class_count = class_count+1

NUM_CLASSES = class_count
IMAGE_SIZE = 28

# Loss
def plot_history_loss(fit):
    # Plot the loss in the history
    axL.plot(fit.history['loss'],label="loss for training")
    axL.plot(fit.history['val_loss'],label="loss for validation")
    axL.set_title('model loss')
    axL.set_xlabel('epoch')
    axL.set_ylabel('loss')
    axL.legend(bbox_to_anchor=(10), loc='lower right'borderaxespad=1fontsize=10)

# Accurascy
def plot_history_acc(fit):
    # Plot the loss in the history
    axR.plot(fit.history['acc'],label="loss for training")
    axR.plot(fit.history['val_acc'],label="loss for validation")
    axR.set_title('model accuracy')
    axR.set_xlabel('epoch')
    axR.set_ylabel('accuracy')
    axR.legend(bbox_to_anchor=(11), loc='upper right'borderaxespad=1fontsize=10)

if __name__ == '__main__':

    count=0
    folder_list = sorted(os.listdir(path))

    train_image = []
    train_label = []
    test_image = []
    test_label = []
    X = []
    Y = []

    label = 'label.txt'
    
    f = open(label, 'w')
    for folder in folder_list:
        subfolder = os.path.join(path,folder)
        file_list = sorted(os.listdir(subfolder))

        filemax = 0

        i = 0

        for file in file_list:

            i = i + 1

            img = img_to_array(load_img('./data/' + folder + '/' + file,target_size=(28,28)))
            X.append(img)
            Y.append(count)
        
        label_name = folder + ' ' + str(count) + '\n'
        f.write(label_name)

        count +=1

    X = np.asarray(X)
    Y = np.asarray(Y)
    X = X.astype('float32')
    X = X / 255.0

    Y = np_utils.to_categorical(Y, NUM_CLASSES)

    train_image, test_image, train_label, test_label = train_test_split(X,Y,test_size=0.20)
    
    f.close()
    print(u'画像読み込み終了')

    input_shape = (IMAGE_SIZE, IMAGE_SIZE, 3)

    model = Sequential()
    model.add(Conv2D(32,kernel_size=(3,3),
                     activation='relu',
                     padding='same'
                     input_shape=input_shape))
    model.add(MaxPooling2D(pool_size=(2,2)))

    model.add(Conv2D(64, (3,3), activation='relu'padding='same'))
    model.add(MaxPooling2D(pool_size=(2,2)))

    model.add(Flatten())
    model.add(Dense(512activation='relu'))
    model.add(Dropout(0.5))
    model.add(Dense(NUM_CLASSES, activation='softmax'))

    model.compile(loss=keras.losses.categorical_crossentropy,
                  optimizer=keras.optimizers.Adadelta(),
                  metrics=['accuracy']
                  )

    chkpt = './model_28.h5'
    cp_cb = ModelCheckpoint(filepath = chkpt, monitor='val_loss'verbose=1
                            save_best_only=Truemode='auto')

    history = model.fit(train_image, train_label,
              batch_size=batch_size,
              epochs=epochs,
              verbose=1,
              validation_data=(test_image, test_label),
              callbacks=[cp_cb],
              )

    model.summary()

    score = model.evaluate(test_image, test_label, verbose=0)

    fig, (axL, axR) = plt.subplots(ncols=2figsize=(10,4))

    plot_history_loss(history)
    plot_history_acc(history)

    fig.savefig('./loss_acc.png')
    plt.close()

「3_atdashi.py」


#!/usr/bin/env python

import os
import sys
import numpy as np
import tensorflow as tf

import keras
from keras.utils import np_utils
from keras.models import Sequential
from keras.layers import Dense,Dropout,Flatten
from keras.layers import Conv2D,MaxPooling2D
from keras.preprocessing.image import array_to_img,img_to_array,load_img
from keras import backend as K
from sklearn.model_selection import train_test_split
from keras.models import load_model
import time

import RPi.GPIO as GPIO
import picamera

i = 0
label_name = []

label = 'label.txt'

f = open(FLAGS.label,'r')
for line in f:
  line = line.rstrip()
  l = line.rstrip()
  label_name.append(l)
  i = i + 1

NUM_CLASSES = i
IMAGE_SIZE = 28

if __name__ == '__main__':
    test_image = []
    
    # model read
    model = load_model('./model_28.h5')
    model.summary()
    
    # for Raspberry Pi

    GPIO.cleanup()

    #for Raspberry Pi
    GPIO.setmode(GPIO.BCM)
    port1 = 24 # switch

    GPIO.setup(port1, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)

    print('Ready!')
    try:
        while True:
            if GPIO.input(port1):
                with picamera.PiCamera() as camera:
                    camera.resolution = (12896)
                    camera.start_preview()
                    camera.capture('./tmp.jpg')
                
                img = img_to_array(load_img('./tmp.jpg' , target_size=(28,28)))
                test_image.append(img)
                test_image = np.asarray(test_image)
                test_image = test_image.astype('float32')
                test_image = test_image / 255.0
    
                predictions = model.predict_classes(test_image)
    
                print(label_name[predictions[0]], u'です。')
                
                test_image = []
            
            time.sleep(0.01)

    except KeyboardInterrupt:
        GPIO.cleanup()

事前に、TensorFlow、Keras、numpyなどのライブラリをpip3コマンドで取り込んでおいてください。

(なお、TensorFlowは1.14、kerasは2.2.4 を推奨)

で、Raspberry Piの回路図は以下の通り。4つのタクトスイッチを使います。

Atdasi01

ちょっとわかりにくいですが、各タクトスイッチにつながる線で、黒は「GPIO24」、青は「GPIO17」、黄は「GPIO27」、緑は「GPIO22」、そして赤は「3.3V」につながってます。

あ、Raspberry Pi用のカメラも当然、つないでおきます。

で、先の3つのプログラムコードを、一つのフォルダに入れます。

Atdashi02

また、同じフォルダ内に「data」という名前のフォルダを作り

Atdashi03

その下に、上のような「グー」「チョキ」「パー」に当たる3種類のフォルダを作っておきます。

Img_0159

で、我が家のRaspberry Piで組んでみました。

Img_0164

こんな感じに、4つのタクトスイッチとつないでます。

なお、カメラは5インチ液晶の裏に両面テープで張り付けておきました。

これで、準備完了です。

(1) グー、チョキ、パー をラズパイカメラで撮影して記録 (各30枚づつくらい)

まず、自分の手を使って、「グー」「チョキ」「パー」の手を集めます。

Raspberry Pi上でターミナルを開き、

> python3 1_camera.py

と入力し、実行。

ターミナル上に「Ready!」と表示されたら、準備完了です。撮影を開始します。

Raspberry Piのカメラの前で「グー」「チョキ」「パー」の3種類の手を構えて、先の回路図にあるボタン「グー」「チョキ」「パー」で、手にあったものを選んで押します。

Img_0162

すると以下のような感じに、各フォルダ内に画像がたまっていきます。

Atdashi04

各ラベルごとに、20~30枚づつくらい撮影してください。

なお、合計が100枚を超えると、Raspberry Pi 3B+ではメモリーが飛ぶっぽいです(うちは飛びました)。

(2) 上の写真を用いて深層学習(CNN)を実施

(1)で教師データができたので、学習を実行します。同じフォルダ内で、

> python3 2_train.py

と実行します。

デフォルトでは、50エポックほど流れます。大体5分くらいで終わります。

なお、この50エポック内の最良のモデルのみが保存される仕組みになっているため、学習が終わればそのまま(3)へと移行します。

が、その前に、学習の状況を確認しておきましょう。

作業フォルダー中に、「loss_acc.png」という画像ファイルができているはずです。

Loss_acc

その名の通り、Loss(損失)値とAccuracy(精度)値のグラフが出てきます。Loss値は0に近いほど、Accuracyは1に近いほど、高精度なモデルができている目安になります。

が、よく見るとどちらも2色あります。これは、学習用データでの検証値(Train)と、評価用データでの検証値(Val)となります。

簡単に言うと、上の画像のようにLoss、Accuracy共に、この両者がほぼ同じところに収束していれば、学習としてはまずまずです。

これがTrainとValとが離れていると、いわゆる”過学習”と呼ばれる状態になります。その場合は汎用性が落ちているので、パラメータを変更するなり、(1)からやり直すなりして、再学習する必要が出てきます。

(例えば、batchの値を10→20に変えてみる 等)

単純に、何も変えずにもう一度実行するだけでよくなることもあります。何度実行しても改善されないときのみ、パラメータをいじるか、あるいは写真の撮り直しを実行してみてください。

グラフを確かめてから、(3)へ行きます。

(3) できたモデルを使い、カメラで撮影した「手」を推論する

ここでは、「3_atodashi.py」を使います。

> python3 3_atodashi.py

と実行したのち、

Img_0163

こんな感じに、カメラの前で手を構えます。

もし、写真のようにチョキを出しているときに、プロンプト上で

 > 2_choki

と表示されたら、推論成功です。

グーやパーだと勘違いしていたら、学習に失敗している可能性があります。

(1)か、(2)あたりからやり直してください。

あるいは、手が近すぎる or 遠すぎることもあります。何度か実験した経験での話ですが、ちょっと動かしてみると、精度が上がる距離がどこかにあることが多いです。

さて、画像認識としてはここまでで終了ですが、これではまだ「後出しじゃんけん機」ではありませんね。

(4) 推論結果に対し、勝つ手の画像を表示する

Raspberry Piに後出しじゃんけんをさせるために、「3_atodashi.py」を以下のコードに置き換えます。


#!/usr/bin/env python

import os
import sys
import numpy as np
import tensorflow as tf
import cv2

import keras
from keras.utils import np_utils
from keras.models import Sequential
from keras.layers import Dense,Dropout,Flatten
from keras.layers import Conv2D,MaxPooling2D
from keras.preprocessing.image import array_to_img,img_to_array,load_img
from keras import backend as K
from sklearn.model_selection import train_test_split
from keras.models import load_model
import time

import RPi.GPIO as GPIO
import picamera

i = 0
label_name = []

label = 'label.txt'

f = open(label,'r')
for line in f:
  line = line.rstrip()
  l = line.rstrip()
  label_name.append(l)
  i = i + 1

NUM_CLASSES = i
IMAGE_SIZE = 28

if __name__ == '__main__':
    test_image = []
    
    # model read
    model = load_model('model_28.h5')
    model.summary()
    
    # for Raspberry Pi

    GPIO.cleanup()

    #for Raspberry Pi
    GPIO.setmode(GPIO.BCM)
    port1 = 24 # switch

    GPIO.setup(port1, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)

    print('Ready!')
    try:
        while True:
            if GPIO.input(port1):
                with picamera.PiCamera() as camera:
                    camera.resolution = (12896)
                    camera.start_preview()
                    camera.capture('./tmp.jpg')
                
                img = img_to_array(load_img('./tmp.jpg' , target_size=(28,28)))
                test_image.append(img)
                test_image = np.asarray(test_image)
                test_image = test_image.astype('float32')
                test_image = test_image / 255.0
    
                predictions = model.predict_classes(test_image)
    
                print(label_name[predictions[0]], u'です。')

                #後出し処理
                if predictions[0] == 0# 「グー」だった時
                    reac = "pa.png" # 「パー」を返す
                elif predictions[0] == 1# 「チョキ」だった時
                    reac = "gu.png" # 「グー」を返す
                else# 残り、すなわち「パー」だった時
                    reac = "choki.png" # 「チョキ」を返す

                img = cv2.imread(reac)
                cv2.imshow("reaction",img)
                key = cv2.waitKey(1000
                cv2.destroyAllWindows()
                
                test_image = []
            
            time.sleep(0.01)

    except KeyboardInterrupt:
        GPIO.cleanup()

「#後出し処理」というコメント行がある後ろ当たりに、その後出しじゃんけんの処理が追加されてます。

また、画像表示にはOpenCVを用いてますので、pip3コマンドでOpenCVをインストールしておいてください。

> pip3 install python-opencv

で、このほかに画像が3枚、つまり、グー、チョキ、パーの画像をそろえる必要があります。

私はいらすとやで落としましたが、なんでもいいです。

Atodashi05

こんな感じに「グー」「チョキ」「パー」それぞれ「gu.png」「choki.png」「pa.png」として、Raspberry Pi上のプログラムコードの入った同じフォルダに入れておいてください。

で、この状態で(3)と同様に動かすと、推論の直後に、Raspberry Piが勝つと思う手を表示してきます。

Img_0205

こんな具合です。

(3)でそこそこの精度が出るモデルであれば、正しい「勝ち手」を出してくれるはずですね。

はい、以上が「後出しじゃんけん機」を作るまで、です。

(2)で使う「Train.py」という学習用コードを読めばわかる通り、28×28の画像で学習、推論させてます。この解像度じゃないと、Raspberry Piでは学習できません。

かなり低い解像度ですが、案外この解像度でも、じゃんけんくらいは認識できるようです。

もっとも、一筋縄ではいかないところもありますね。背景に依存しちゃったり、あるいは過学習で現実の手をうまく認識しなかったり……その辺りは、トライアンドエラーで実験してみるのがよいかと思います。

今回入れてませんが、Grad‐Camを使ったりして、モデルの確からしさを調べるのもいいですね。実際、会社にある同じコードでは、Grad-Camを組み込んでます。

どちらかというと、画像認識の理屈よりも、それを実際に使ってみてノウハウっぽいものを学ぶというのが狙いのプログラムコード。やってみると、思ったよりも精度は出ることもあるし、思い通りにいかないこともあります。

もちろん、ここに書いただけでは不十分な話が多いです。パラメータまで含めたら、一介のブログ記事では書ききれないほどいろいろあります。

このほか、精度におけるカメラと手の距離の依存性が高いので、HC-SR04などの距離センサーと組み合わせて撮影させてみるなど、電子工作の題材にもぴったりですね。

とまあ、我ながらうまい仕掛けを作ったものだと思っていたんですが。

これを作り上げた後に、「人気ブロガーからあげ先生のとにかく楽しいAI自作教室」(日経BP)という本にも、同じようにじゃんけんの手を学習させるというコードがあることが判明。

で、Kindle版で購入してみてみたんですが……あちらはRaspberry Pi上ではなく、しかも画像セットはあらかじめ準備されたものを使用、かつ、Google Colab上で動かすというものでした。いやはや、幸いにもこちらのやってることとは、かぶってませんでしたね。

まあ、似たようなことは皆さん、考えるものですね。

ちなみにこの本、画像だけでなく、自然言語など、幅広いカテゴリーの話が載っていたりと、なかなか面白い本です。買って損はありません。

著者のからあげ氏のブログは、Raspberry Pi関係のコードではとてもお世話になってますね。私もよく、参考にさせてもらってます。

以上、「画像認識を体で覚える」仕組みを作ってみた、というお話でした。


人気ブロガーからあげ先生のとにかく楽しいAI自作教室

« IIJが20ギガで1880円のプラン | トップページ | 4つのカメラを取り付け可能にするアダプタ「Raspberry Pi用マルチカメラアダプタ」 »

Raspberry Pi・Arduino・電子工作」カテゴリの記事

数値解析系」カテゴリの記事

コメント

コメントを書く

(ウェブ上には掲載しません)

« IIJが20ギガで1880円のプラン | トップページ | 4つのカメラを取り付け可能にするアダプタ「Raspberry Pi用マルチカメラアダプタ」 »

当ブログ内検索

スポンサード リンク

ブログ村

無料ブログはココログ