「Raspberry PI」タグアーカイブ

ラズパイ4 SCIKIT-LEARN で機械学習(MLP)を学ぶ

概要

 scikit-learn 多層パーセプトロン(MLP:Mulitilayer Perceptron)を使用します。前回投稿の SVM は、基本2クラス分類対応でしたが、MLP は3クラス以上の分類が可能とのことです。
 NeoPixel (フルカラーLED)点灯状態を、USBカメラで撮影し、RGB成分を特徴値、色種類を正解として、MLPモデルに学習させます。次に学習したMLPモデルを使用し、RGB成分値からNeoPixel の8色の点灯状態判定を行います。学習時と判定時で明るさの変化等が無ければ、安定した予測が行える様です。
 反対に周囲の明るさの変化が大きい場所では、部屋の照度を特徴値に加えたり、想定される環境で取得したデータによる学習などが必要なのだと思います。

特徴値取得・判定状況動画

 NeoPixel の特徴値(RGB)取得と判定状況の動画です。動画を見ると色が切り換わる前に判定結果が表示されている様にも感じられます。今回学習したMLPモデルを使用して判定していますが、プログラムの画像加工と表示順が良くないのだと思います。
 
 NeoPixel のRGBデータは、動画にも四角枠を表示している LEDとLEDの境界部(全7カ所)平均値としています。LED中央部はかなり明るく、色の違いによるRGB値の差が少ないためです。7カ所の境界部合成画像を動画左上に拡大表示していますが、どの色の場合でも白い部分が残っています。

  

プログラム構成

 今回作成プログラムと関連ファイルを下記表に記載します。既存プログラムを極力変更せず流用した結果、プログラム数が多く、ファイル名もばらばらになってしまいました。
 また、NeoPixelライブラリ使用に root権限が必要で、反対にUSBカメラをroot権限で使用するとエラーが発生した為、対策を検討せず、プログラムを分け、点灯色指示にテキスト(No5:input_sig.txt)を介在させたことなども理由です。ただ、このおかで、ラズパイでは複数プログラムが同時に実行できることを知りました。

Noファイル名内容
1neo_tes3.pyNeoPixelを点灯。(No5:input_sig.txt) 記載色番号 [0~7] に対応する色点灯。
2usbcamera4.pyNeoPixel撮影、LED部RGB成分取得。RGB・HSV成分, 色番号[0~7]をCSVファイル(No6:RGB_RESULT.csv)に記録。
3sk_learn_MLP
_s00_study.py
CSVファイル(No.6:RGB_RESULT.csv)データにより MLP機械学習する。学習モデルを (No7:led_pattern.pickle) 保存。
4usbcamera4
_MLP_EXEC.py
MLP学習モデル (No7:led_pattern.pickle) 読取後、NeoPixel を無作為に色変更。RGB値から、MLP学習モデルを使用し色判定する。
5input_sig.txtNo2 , 4 プログラム実行時、色番号 [0~7] を書き込むと、No1 プログラムが読取、色番号 [0~7] に対応する色を点灯。
6RGB_RESULT.csv特徴値 RGB・HSV成分、色番号[0~7]を書き込んだCSV形式ファイル。
7led_pattern.pickleMLP機械学習モデル保存ファイル。

プログラム

(1)NeoPixel点灯プログラム

 NeoPixel接続・設定は、下記の過去投稿に記載しています。
 https://kats-eye.net/info/2020/05/02/neopixel-2/

 テキストファイル(input_sig.txt) に記載された色番号 [0~7] に対応する色をNeoPixelで点灯します。

import time
import board
import neopixel
import os

pixel_pin = board.D18   # GPIO-PIN-NO
num_pixels = 8          # The number of NeoPixels

# The order of the pixel colors - RGB or GRB. Some NeoPixels have red and green reversed!
# For RGBW NeoPixels, simply change the ORDER to RGBW or GRBW.
ORDER = neopixel.GRB

pixels = neopixel.NeoPixel(
    pixel_pin, num_pixels, brightness=0.2, auto_write=False, pixel_order=ORDER
)

d_pth = '/home/pi/MySndTest/csv_led/input_sig.txt'   # データ保存ディレクトリパス
inp_cur = 0
inp_bef = 0

while(True):
    if (os.path.exists(d_pth)):
        try:
            # 点灯色指示内容読込
            f_obj = open(d_pth)
            inp_str = f_obj.read().replace('\n','')
            f_obj.close()
        
            inp_cur = int(inp_str)
            
        except Exception as e :
            inp_cur = 0
            print(e)
        
    else :
        inp_cur = 0
        
    if (inp_cur < 0 and inp_cur > 7) :
        inp_cur = 0

    # 点灯色指示内容をRGB変換し。NEOPIXEL点灯
    if (inp_cur != inp_bef) :
        b_1 = 1 & inp_cur
        b_2 = 1 & (inp_cur>>1)
        b_3 = 1 & (inp_cur>>2)

        pixels.fill((b_3*255,b_2*255,b_1*255))
        pixels.show()
        
        print(inp_cur)
        inp_bef = inp_cur

   

(2)NeoPixel LED部RGB成分取得プログラム

 NeoPixelを撮影し、RGB成分を CSV形式で保管します。

import cv2
import numpy as np
import time

import csv
import os

capture = cv2.VideoCapture(0)
if capture.isOpened() is False:
  raise IOError

x_l = 277                     # 撮像領域開始点(x)
y_u = 251                     # 撮像領域開始点(y)
x_w = 10                      # 撮像領域幅
y_h = 25                      # 撮像領域高さ

x_stp = 26.0                  # 分割領域ステップ(x)
y_stp = -1.6                  # 分割領域ステップ(y)
p_cnt = 7                     # 分割領域数

c_wait = 30                   # 色変化後の待機ループ回数(遅延処理用)
c_wrt = 100                   # 各色毎のCSV書込回数
ttl_cnt = 0                   # 総合ループ回数

s_pth = '/home/pi/MySndTest/csv_led/input_sig.txt'      # 点灯色指示
d_pth = '/home/pi/MySndTest/csv_led/'                   # CSV保存ディレクトリパス
f_nam = 'RGB_RESULT'                                    # CSV保存ファイル名

# 8色対象にする
for sig in range(8) :
  sig_no = 7-sig
  cnt = 0
  csv_cnt = 0

  # 色変化後待機ループ + 各色毎のCSV書込回数
  for lp_cnt in range(c_wrt + c_wait) :
    try:
      if (cnt==0):
        # 点灯色指示用ファイル書込 (0-7)
        f_txt = open( s_pth ,'w')
        f_txt.write(str(sig_no))
        f_txt.close()
        print(str(sig_no))
        time.sleep(0.2)
        cnt = cnt + 1

      # 画像取込
      ret, frame = capture.read()
      if ret is False:
        raise IOError

      frm_tgt=frame[ y_u : y_u + y_h , x_l : x_l + x_w ]

      # 対象領域切取・合成(7箇所)
      for num in range(p_cnt) :
        x_l_tmp = round(x_l + x_stp * num)
        y_u_tmp = round(y_u + y_stp * num)

        frm_tmp = frame[ y_u_tmp : y_u_tmp + y_h , x_l_tmp : x_l_tmp + x_w ]
      
        if num==0:
          frm_tgt = frm_tmp
        else :
          frm_tgt = cv2.hconcat([frm_tgt , frm_tmp])                                                # 合成
          cv2.rectangle(frame,( x_l_tmp , y_u_tmp ),( x_l_tmp + x_w , y_u_tmp + y_h ),(255,0,0),1)  # 描画
      
      cv2.rectangle(frame,( x_l , y_u ),( x_l + x_w , y_u + y_h ),(0,0,255),1)                      # 描画
    
      b = round(frm_tgt.T[0].flatten().mean())          # 領域のRGB平均値(R)
      g = round(frm_tgt.T[1].flatten().mean())          # 領域のRGB平均値(G)
      r = round(frm_tgt.T[2].flatten().mean())          # 領域のRGB平均値(B)

      f_tgt_hsv= cv2.cvtColor(frm_tgt,cv2.COLOR_BGR2HSV)
    
      h = round(f_tgt_hsv.T[0].flatten().mean())        # 領域のHSV平均値(H)
      s = round(f_tgt_hsv.T[1].flatten().mean())        # 領域のHSV平均値(S)
      v = round(f_tgt_hsv.T[2].flatten().mean())        # 領域のHSV平均値(V)

      # CSV登録データ準備
      csv_list=[]
      csv_list.append(b)
      csv_list.append(g)
      csv_list.append(r)
      csv_list.append(h)
      csv_list.append(s)
      csv_list.append(v)
      csv_list.append(sig_no)

      f_pth = d_pth + f_nam + '.csv'                  # 登録ファイル名

      # CSV新規・追加書込
      if(lp_cnt >= c_wait):
        csv_cnt = csv_cnt + 1
        if os.path.exists(f_pth) :
          with open(f_pth, 'a') as f :
            writer = csv.writer(f)
            writer.writerow(csv_list)
                    
        else:
          with open(f_pth, 'w') as f :
            writer = csv.writer(f)
            writer.writerow(csv_list)
    

      #cv2.imshow('frm_tgt',frm_tgt)

      # 色判定領域を拡大し、取込画像に追加
      img_bai = 5 
      h_tgt , w_tgt = frm_tgt.shape[:2]
      frm_tgt_z = cv2.resize(frm_tgt , (int(img_bai * w_tgt) , int(img_bai * h_tgt)))
      h_tgt_z , w_tgt_z = frm_tgt_z.shape[:2]
      frame[0:h_tgt_z,0:w_tgt_z] = frm_tgt_z
      
      # CSV書込値(RGB・HSV)・色番号・書込回数等を表示
      color_txt1 = "B:" + str(b) + " , G:" + str(g) + " , R:" + str(r) 
      color_txt2 = "H:" + str(h)+" , S:"+str(s)+" , V:"+str(v)
      cv2.rectangle(frame,( 1 , 1 ),( w_tgt_z , h_tgt_z),(0,0,255),3)
      cv2.putText(frame,color_txt1,(20,410),cv2.FONT_HERSHEY_PLAIN,1.8,(0,0,255),1,cv2.LINE_AA)
      cv2.putText(frame,color_txt2,(20,440),cv2.FONT_HERSHEY_PLAIN,1.8,(0,0,255),1,cv2.LINE_AA)
      cv2.putText(frame,"ColorNo: " + str(sig_no)+" , CsvWrtCnt: " + str(csv_cnt),(20,470),cv2.FONT_HERSHEY_PLAIN,1.8,(0,0,255),1,cv2.LINE_AA)

      cv2.imshow('frame',frame)
    
   
    except KeyboardInterrupt:
      cv2.putText(frame,"Error",(25,25),cv2.FONT_HERSHEY_PLAIN,2,(0,0,255),1,cv2.LINE_AA)

    if (ttl_cnt == 2) :
      chk=input("Start?")
    
    ttl_cnt =ttl_cnt + 1
    time.sleep(0.1)
  
    k = cv2.waitKey(1)
    if k==27:
      break
    
f_txt = open( s_pth ,'w')
f_txt.write(str(0))
f_txt.close()

capture.release()
cv2.destroyAllWindows()

  

(3)MLP機械学習プログラム

 MLPと SVMのプログラム上で、特に異なるのは 行番1のライブラリ読込と行番35学習モデルの定義の箇所です。行番35のMLP学習モデル定義について、考え方を整理しておきます。

clf = MLPClassifier(hidden_layer_sizes=(100, ), max_iter=10000, tol=0.00001, random_state=1)

 MLP学習モデル定義の設定内容です。

No設定項目内容
1hidden_layer_sizes=(100, )100個のニューロンで構成する中間層を1層定義。例えば (100,50,100) は、3層の中間層を定義する。
2max_iter=10000学習過程で入力に対する出力が一致する様に各ニューロンへの入力値の重みづけを行うシナプス強度調整を行う。調整回数が設定を超えると学習を終了する。
3tol=0.00001学習過程でシナプス強度調整により、入力に対する出力が完全に一致すると損失関数出力値は0になる。1回の調整で損失関数出力値変化が設定値以下の場合が3回続くと学習を終了する。
4random_state=1乱数系列設定。系列が同じだと再実行しても同じ結果になるらしい。‘none’ を設定すると毎回乱数系列を変更する。

 CSVファイル(RGB_RESULT.csv)のデータを読み込んで、MLP機械学習するプログラムです。学習モデルは (led_pattern.pickle) に保存します。

from sklearn.neural_network import MLPClassifier
import numpy as np
import pickle
import csv

csv_fil = ['bk20210531_RGB_RESULT.csv']                             # CSVファイル名
dt_frq_arr = np.empty((0,3) , int)                                  # データ格納 numpy.ndarray
dt_rtn_arr = np.empty((0) , int)

for f_num in range(len(csv_fil)) :                                  # ファイル数分ループ処理
    dat_frg = []                                                    # 一時保存リスト
    with open('/home/pi/MySndTest/csv_led/' + csv_fil[f_num]) as f: # CSVファイル開く
        reader = csv.reader(f)
        for row in reader :                                         # ファイル行ループ
            if len(row)>=2 :                                        # 1行に2データ以上を対象
                tmp=[]
                tmp.append(int(float(row[0])))                      # 文字列をintに変換
                tmp.append(int(float(row[1])))                      # 文字列をintに変換
                tmp.append(int(float(row[2])))                      # 文字列をintに変換  
                dat_frg.append(tmp)                                 # 一時保存リストに追加

                dt_rtn_arr = np.append(dt_rtn_arr,int(float(row[6])))
                
    dt_frq = np.array(dat_frg)                                      # numpy.ndarrayに変換 
    dt_frq_arr = np.append( dt_frq_arr , dt_frq , axis=0 )

print(dt_frq_arr)
print(dt_rtn_arr)
print("")

rtn=input("")

# 分類用に多層ニューラルネットワークを用意
# ランダムな要素を固定した場合(毎回同じ結果)
clf = MLPClassifier(hidden_layer_sizes=(100, ), max_iter=10000, tol=0.00001, random_state=1)
# 毎回ランダムな場合
#clf = MLPClassifier(hidden_layer_sizes=(100, ), max_iter=10000, tol=0.00001, random_state=None)

clf.fit(dt_frq_arr , dt_rtn_arr)                                    # 学習

# テストデータに対して予測
test_data = [[250,227,182],[255,230,177],[83,90,92],[245,230,177]]  # テストデータ
test_label = clf.predict(test_data)                                 # 結果(予測値)

# テスト結果の表示
print("入力データ:{0}".format(test_data))
print("予測結果  :{0}".format(test_label))
print("")

# 学習モデル保存
pickle.dump(clf , open('led_pattern.pickle',mode='wb'))
print("finished")

 

(4)NeoPixel色判定プログラム(MLP利用)

 MLP学習モデル (led_pattern.pickle) 読取後、NeoPixel を無作為に色変更し、RGB値からMLP学習モデルを使用し色判定するプログラムです。

import cv2
import numpy as np
import time

import os
import random
import pickle

clf = pickle.load(open('led_pattern.pickle' , mode='rb')) # 学習モデルロード

capture = cv2.VideoCapture(0)
if capture.isOpened() is False:
  raise IOError

x_l = 279                     # 撮像領域開始点(x)
y_u = 256                     # 撮像領域開始点(y)
x_w = 10                      # 撮像領域幅
y_h = 25                      # 撮像領域高さ

x_stp = 26.0                  # 分割領域ステップ(x)
y_stp = -1.6                  # 分割領域ステップ(y)
p_cnt = 7                     # 分割領域数

c_wait = 10                   # 色変化後の待機ループ回数(遅延処理用)


s_pth = '/home/pi/MySndTest/csv_led/input_sig.txt'      # 点灯色指示
d_pth = '/home/pi/MySndTest/csv_led/'                   # CSV保存ディレクトリパス
f_nam = 'RGB_RESULT'                                    # CSV保存ファイル名

while (True) :
  sig_no = random.randint(0,7)  # 乱数で色番号を決定(0-7)
  c_wrt = random.randint(0,20)  # ループ回数
  cnt = 0
  csv_cnt = 0

  for lp_cnt in range(c_wrt + c_wait) :
    try:
      if (cnt==0):
        # 点灯色指示用ファイル書込 (0-7)
        f_txt = open( s_pth ,'w')
        f_txt.write(str(sig_no))
        f_txt.close()
        print(str(sig_no))
        time.sleep(0.2)
        cnt = cnt + 1

      # 画像取込
      ret, frame = capture.read()
      if ret is False:
        raise IOError

      frm_tgt=frame[ y_u : y_u + y_h , x_l : x_l + x_w ]

      # 対象領域切取・合成(7箇所)
      for num in range(p_cnt) :
        x_l_tmp = round(x_l + x_stp * num)
        y_u_tmp = round(y_u + y_stp * num)

        frm_tmp = frame[ y_u_tmp : y_u_tmp + y_h , x_l_tmp : x_l_tmp + x_w ]
      
        if num==0:
          frm_tgt = frm_tmp
        else :
          frm_tgt = cv2.hconcat([frm_tgt , frm_tmp])
          cv2.rectangle(frame,( x_l_tmp , y_u_tmp ),( x_l_tmp + x_w , y_u_tmp + y_h ),(255,0,0),1)
      
      cv2.rectangle(frame,( x_l , y_u ),( x_l + x_w , y_u + y_h ),(0,0,255),1)
    
      b = round(frm_tgt.T[0].flatten().mean())          # 領域のRGB平均値(R)
      g = round(frm_tgt.T[1].flatten().mean())          # 領域のRGB平均値(G)          
      r = round(frm_tgt.T[2].flatten().mean())          # 領域のRGB平均値(B)

      # 学習モデル入力データ準備
      temp_data = []
      temp_data.append(int(b))
      temp_data.append(int(g))
      temp_data.append(int(r))

      inp_data = []
      inp_data.append(temp_data)                         # 入力データ(BGR)
      rtn_label = clf.predict(inp_data)                  # 結果(予測値)

      # 色判定領域を拡大し、取込画像に追加
      img_bai = 5 
      h_tgt , w_tgt = frm_tgt.shape[:2]
      frm_tgt_z = cv2.resize(frm_tgt , (int(img_bai * w_tgt) , int(img_bai * h_tgt)))
      h_tgt_z , w_tgt_z = frm_tgt_z.shape[:2]
      
      frame[0:h_tgt_z,0:w_tgt_z] = frm_tgt_z

      # CSV書込値(RGB・HSV)・色番号・書込回数等を表示
      color_txt1 = "B:" + str(b) + " , G:" + str(g) + " , R:" + str(r) 
      cv2.rectangle(frame,( 1 , 1 ),( w_tgt_z , h_tgt_z),(0,0,255),3)

      # 結果(予測値)を色名称に変換
      if (rtn_label[0]==sig_no):
        if (sig_no==7):
          col_n="White"
        elif (sig_no==6):
          col_n="Yellow"
        elif (sig_no==5):
          col_n="Pink"
        elif (sig_no==4):
          col_n="Red"
        elif (sig_no==3):
          col_n="Light blue"
        elif (sig_no==2):
          col_n="Green"
        elif (sig_no==1):
          col_n="Blue"
        elif (sig_no==0):
          col_n="No Light"
        else :
          col_n="- Unknown Color -"
          
        jdg_str = col_n + " !"
        
      else:
        jdg_str = "Thinking Now."

      # 結果(予測値)・CSV書込値(RGB・HSV)・色番号・書込回数等を表示
      cv2.putText(frame,jdg_str,(20,360),cv2.FONT_HERSHEY_PLAIN,4,(0,0,255),3,cv2.LINE_AA)
      cv2.putText(frame,"Predict Color No : " + str(rtn_label[0]) ,(20,400),cv2.FONT_HERSHEY_PLAIN,1.8,(255,0,0),1,cv2.LINE_AA)
      cv2.putText(frame,"Output Color No : " + str(sig_no),(20,435),cv2.FONT_HERSHEY_PLAIN,1.8,(255,0,0),1,cv2.LINE_AA)
      cv2.putText(frame,color_txt1,(20,470),cv2.FONT_HERSHEY_PLAIN,1.8,(255,0,0),1,cv2.LINE_AA)
      
      cv2.imshow('frame',frame)
    
   
    except KeyboardInterrupt:
      cv2.putText(frame,"Error",(25,25),cv2.FONT_HERSHEY_PLAIN,2,(0,0,255),1,cv2.LINE_AA)

    time.sleep(0.1)
  
    k = cv2.waitKey(1)
    if k==27:
      break
    
f_txt = open( s_pth ,'w')
f_txt.write(str(0))
f_txt.close()

capture.release()
cv2.destroyAllWindows()

 

(5)学習データ例

 次に学習データ例(抜粋)を示します。 
 左から R・G・B・H・S・V 成分となっています。最右は、色番号 [0~7] になっています。

  <省略>
250,228,189,96,63,250,7
250,228,189,96,63,250,7
250,228,189,96,63,250,7
250,228,189,96,63,250,7
250,228,189,96,63,250,7
158,186,190,28,47,190,6
158,185,189,27,47,189,6
158,185,189,28,47,190,6
158,185,189,28,47,190,6
  <省略>

 NeoPixel点灯時に [ input_sig.txt ] に書き込む色番号と色名称、NeoPixelライブラリに指定するRGB値を表に示します。

色番号色名称RGB
0消灯状態000
100255
202550
3みずいろ0255255
425500
5ピンク2550255
62552550
7255255255


まとめ

 実際にやってみることで、使い方のイメージがつかめました。実用環境でも試してみたいと思います。
 また、最初は電話着信音の分類に使いたかったのですが、周波数成分(特徴値)の次元数が同じではないので、NeoPixelの色分類で試しました。この様な場合の処理方法なども、今後いろいろ試してみたいと思います。