Keras+TensorflowでRapsberry Piライントレーサー作ってみた
某所のライントレース大会向けに機械学習を使ったライントレーサーを作ってみた。
そろそろいいかなと思うのでblogで公開してみる。
完成した動作は↓を参照。
Keras+TensorflowでCNNを使ったラズパイライントレーサ作ってみた。
— michu (@ginnyu_tei) 2017年9月17日
挙動がかなり怪しい場面もあるがそこそこ動いてる。 pic.twitter.com/jEGCEZ7BjU
(なお、この動画の後は自分の影を黒と認識して机から落ちました・・・)
何分大会がある週の週末に思いついて一日で実装したので適当な実装箇所が
数多くあるが時間がなかったのでご容赦頂きたい。
本体解説
外観
2階建てで下のプレートはタミヤダブルギヤボックスとモーターコントローラ(DRV8830)
上はRaspberry Pi3とRaspberry Piカメラ+スマートフォン用クリップレンズ (広角)
広角のクリップレンズはラズパイカメラの画角が狭くて足元が映らなかったのでその対策。
電源はモバイルバッテリーを使っている
なぜ機械学習で実装しようと思ったか?
もともと機械学習を使うつもりはまったくなかった
機械学習は学習データを大量に用意するのが面倒だと思っていたからだ
なのでOpenCVの画像処理を使って↓の様に輪郭抽出と矩形取得を使って大会に持っていく予定だった。
(上下反対なのは実装の都合上ラズパイカメラを逆さまに取り付けているだけで意図はないです。)
輪郭抽出するためカメラ画像を2値化する必要がある。それが下の画像
発表資料にこの画像を貼り付けた時に
「あれ?2値化してしかも背景がない画像なら大量に自動生成して学習できるんじゃない?」
と大会の最終週にもかからず思いついてしまったので時間が全くないのにやってしまった。
学習データの自動生成
OpenCVの楕円描画関数を使った。
画像サイズ40x30(ラズパイだとこの画像サイズが限界だった) の中にランダムな楕円を描画して
楕円が右か左かの学習データを作成した。
5つ位描画してみたのが↓。若干怪しい画像もあるがまあ良しとした。
カメラの二値化画像と全く似ていないが結果は問題なく左右を認識できた。
これを10000データ自動生成した 。(学習結果を確認するために追加で+300生成)
学習プログラム(Windows)
ラズパイでもWindows PCでも動く機械学習の環境を探した結果
言語はPythonでライブラリはTensorFlow+Kerasを使った。
↓はWindowsPCで実行した学習
import cv2 import numpy as np import matplotlib.pyplot as plt import keras from keras.models import Sequential from keras.layers import Dense, Dropout, Flatten from keras.layers import Conv2D, MaxPooling2D from keras import backend as K from IPython.display import SVG from keras.utils.vis_utils import model_to_dot width = 40 height = 30 line_width = 8 #plt.figure(figsize=(50,40)) train_num = 10000 #train_num = 9 data_num = 300 x_train = np.zeros((train_num,height,width,1), dtype=np.uint8) y_train = np.zeros((train_num), dtype=np.uint8) x_test = np.zeros((data_num,height,width,1), dtype=np.uint8) y_test = np.zeros((data_num), dtype=np.uint8) #########学習データ生成 for i in range(2): if i == 0: loop_num = train_num else: loop_num = data_num j = 0 while(j<loop_num): #背景作成 image = np.zeros((height,width,1), dtype=np.uint8) #image.fill(255) #ランダム座標生成 #rand_num = 1:左0:右 rand_num = np.random.randint(0,2) rand_center = np.random.randint(0,10) if rand_num == 1: center = (rand_center,height) else: center = (width-rand_center,height) angle = (np.random.randint(0,60),np.random.randint(10,120)) #print(center,angle) cv2.ellipse(image,center,angle,360,0,360,255,line_width) #真っ黒は削除 if(np.max(image)==0): continue index = np.where(image == 255) #楕円の中心が偏っても楕円が画像中心を超える場合は左右逆にする if (index[0][0] == 0): #print(j,index) if (index[1][0] < width/2+line_width) & (rand_num==0): rand_num = 1 elif(index[1][0] > width/2-line_width) & (rand_num==1): rand_num = 0 if i == 0: x_train[j] = image.copy() y_train[j] = rand_num #画像チェック #image=cv2.cvtColor(image,cv2.COLOR_GRAY2RGB) #plt.subplot(190+j+1) #plt.title(str(rand_num) +" "+ str(index[0][0]) +" "+ str(index[1][0])) #plt.imshow(image) else: x_test[j] = image.copy() y_test[j] = rand_num j = j + 1 x_train = x_train / 255 x_test = x_test / 255 y_train = keras.utils.to_categorical(y_train, 2) y_test = keras.utils.to_categorical(y_test, 2) #NN構築 model = Sequential() model.add(Conv2D(32, kernel_size=(3, 3),activation='relu',input_shape=(height,width,1))) model.add(Conv2D(64, (3, 3), activation='relu')) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Dropout(0.25)) model.add(Flatten()) model.add(Dense(32, activation='relu')) model.add(Dropout(0.5)) model.add(Dense(2, activation='softmax')) model.summary() model.compile(loss=keras.losses.categorical_crossentropy,optimizer=keras.optimizers.Adadelta(),metrics=['accuracy']) #学習 model.fit(x_train, y_train,batch_size=128,epochs=12,verbose=1,validation_data=(x_test, y_test)) #NNと重みをファイル出力 json_string = model.to_json() open('c:\cnn_model_40_30.json', 'w').write(json_string) model.save_weights("c:\cnn_model_weights_40_30.hdf5") #学習結果確認 score = model.evaluate(x_test, y_test, verbose=1) print('Test loss:', score[0]) print('Test accuracy:', score[1])
↑の実行結果
tensorflow-gpu使ったのですぐに終了した
_________________________________________________________________ Layer (type) Output Shape Param # ================================================================= conv2d_3 (Conv2D) (None, 28, 38, 32) 320 _________________________________________________________________ conv2d_4 (Conv2D) (None, 26, 36, 64) 18496 _________________________________________________________________ max_pooling2d_2 (MaxPooling2 (None, 13, 18, 64) 0 _________________________________________________________________ dropout_3 (Dropout) (None, 13, 18, 64) 0 _________________________________________________________________ flatten_2 (Flatten) (None, 14976) 0 _________________________________________________________________ dense_3 (Dense) (None, 32) 479264 _________________________________________________________________ dropout_4 (Dropout) (None, 32) 0 _________________________________________________________________ dense_4 (Dense) (None, 2) 66 ================================================================= Total params: 498,146 Trainable params: 498,146 Non-trainable params: 0 _________________________________________________________________ Train on 10000 samples, validate on 300 samples Epoch 1/12 10000/10000 [==============================] - 14s - loss: 0.3082 - acc: 0.8600 - val_loss: 0.2049 - val_acc: 0.8867 Epoch 2/12 10000/10000 [==============================] - 2s - loss: 0.1949 - acc: 0.9089 - val_loss: 0.1447 - val_acc: 0.9300 Epoch 3/12 10000/10000 [==============================] - 1s - loss: 0.1539 - acc: 0.9280 - val_loss: 0.1327 - val_acc: 0.9433 Epoch 4/12 10000/10000 [==============================] - 1s - loss: 0.1338 - acc: 0.9372 - val_loss: 0.1316 - val_acc: 0.9400 Epoch 5/12 10000/10000 [==============================] - 1s - loss: 0.1140 - acc: 0.9504 - val_loss: 0.1181 - val_acc: 0.9500 Epoch 6/12 10000/10000 [==============================] - 1s - loss: 0.1043 - acc: 0.9579 - val_loss: 0.1069 - val_acc: 0.9500 Epoch 7/12 10000/10000 [==============================] - 1s - loss: 0.0997 - acc: 0.9631 - val_loss: 0.0847 - val_acc: 0.9733 Epoch 8/12 10000/10000 [==============================] - 1s - loss: 0.0878 - acc: 0.9615 - val_loss: 0.1088 - val_acc: 0.9600 Epoch 9/12 10000/10000 [==============================] - 1s - loss: 0.0815 - acc: 0.9690 - val_loss: 0.0981 - val_acc: 0.9567 Epoch 10/12 10000/10000 [==============================] - 1s - loss: 0.0784 - acc: 0.9691 - val_loss: 0.0958 - val_acc: 0.9700 Epoch 11/12 10000/10000 [==============================] - 1s - loss: 0.0746 - acc: 0.9714 - val_loss: 0.0693 - val_acc: 0.9800 Epoch 12/12 10000/10000 [==============================] - 1s - loss: 0.0715 - acc: 0.9737 - val_loss: 0.0732 - val_acc: 0.9667 32/300 [==>...........................] - ETA: 0sTest loss: 0.073219653219 Test accuracy: 0.966666666667
このCNN自体はKerasのサンプルコードの層をそのまま流用して
画像サイズが小さくなった分と出力層が右・左の2つになった分ネットワークを小さくしている。
半年前はこれにTensorBoard組み込んで学習結果確認をしていましたがそのコードを紛失しましたorz
各層の大きさは適当に決めました。時間がなかったので追い込んでいません。
今の結果を見る限りもっと小さくても良さそうです。
推論&モータ制御プログラム(raspbian)
RaspberryPiで動かしたコードを↓に載せる
学習したモデルと重みを読み込んで推論している。
推論した結果に適当な重みを付けて左右のモーター電圧を調整することで
左右に曲がるようになっている。
# -*- coding: utf-8 -*- import cv2 import numpy as np import keras from keras.models import Sequential from keras.layers import Dense, Dropout, Flatten from keras.layers import Conv2D, MaxPooling2D from keras.models import model_from_json from keras.preprocessing.image import load_img, img_to_array import smbus import time i2c = smbus.SMBus(1) #学習に使ったモデルを読み込み json_string = open('./cnn_model_40_30.json', 'r').read() model = model_from_json(json_string) #学習した重みを読み込み model.load_weights("./cnn_model_weights_40_30.hdf5") model.compile(loss=keras.losses.categorical_crossentropy,optimizer=keras.optimizers.Adadelta(),metrics=['accuracy']) model.summary() print "eval start" cap = cv2.VideoCapture(0) try: while True: ret,frame = cap.read() #cv2.imwrite('orig.bmp', frame); frame = cv2.resize(frame,(40,30)) frame = cv2.flip(frame, -1); #グレースケール→2値化 img_gray = cv2.cvtColor(frame,cv2.COLOR_BGR2GRAY) ret, img_dst = cv2.threshold(img_gray,0,255,cv2.THRESH_BINARY_INV|cv2.THRESH_OTSU) img_dst /= 255 #学習データからの予測 #predicted[0][0]:Right predicted[0][1]:Left predicted = model.predict(np.array([img_to_array(img_dst)]), 1, 0) print(predicted); r_motor = (int(predicted[0][1] * 3) << 2); l_motor = (int(predicted[0][0] * 3) << 2); #予測結果に適当に重みをつけてモーター制御 if (r_motor == 0): r_motor = 0x00 else: r_motor = 0x1A + r_motor if (l_motor == 0): l_motor = 0x00 else: l_motor = 0x1A + l_motor #DRB8830の設定 i2c.write_byte_data(0x63, 0x00, r_motor) i2c.write_byte_data(0x64, 0x00, l_motor) except KeyboardInterrupt: print("get SIGINT") #割り込み(Ctrl+C)でモーターを止めて終了 i2c.write_byte_data(0x63, 0x00, 0x00) #R i2c.write_byte_data(0x64, 0x00, 0x00) #L cap.release() cv2.destroyAllWindows()
結果
上のツイートの通り一日で実装できた。
自分の影を黒認識するのは二値化部分の問題だと思うのでまあ、セーフ
反省点
真面目に(?)CNN構築するの初めてだったので各層のサイズや層の数などは適当にやったのが反省点
もっと小さくても左右2値なんだから認識出来る気がする。
それと今回は割り切って左右にしたが分類数を増やしてちょっと右とかかなり左とか入れたかった。
これ書いてて思ったが入力が2値なんだからCNNも2値化できるんじゃないかな?
やる気がでたらこれも実験してみたい。