久しぶりに M5StickV と V-Training で画像分類用の機械学習モデルを準備して利用する（その2）【2021年4月】

この記事は、以下の続きの内容です。

●久しぶりに M5StickV と V-Training で画像分類用の機械学習モデルを準備する（途中まで）【2021年4月】 - Qiita
　https://qiita.com/youtoy/items/a8d954e2d85bb42f512f

上記の記事では、公式ドキュメントに書かれた手順について、以下の内容を進めていました。
　・本体のファームウェアのアップデート
　・学習用画像を撮影するためのプログラム等の準備（マイクロSDカードを使った手順）
　・画像の撮影
　・V-Training への画像のアップロード
そして、上記の手順の最後の項目になっている [V-Training](http://v-training.m5stack.com/) のページでの画像アップロード後に、処理に失敗したというメールが返ってきていました。

この記事では、画像撮影を行う部分に戻って手順をやり直して、機械学習モデルが得られるように試みてみます。

## 画像の枚数を増やして画像をアップロード
撮影対象は前回の記事と同じで、以下の 3つになります。
![クラスの振り分け.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/14ae97fd-e8c1-847b-ad27-7527504b97ad.jpeg)

3つのクラスのそれぞれについて、撮影した画像を追加して 1クラスあたり 50枚ずつになるようにしたうえで、再度アップロードをしました。
![再度V-Trainingでアップロード.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/b2e6f457-6750-c5a0-8fcd-7fbb2bc82d68.jpeg)
アップロード後、メールボックスを確認したところ、失敗のメールは来ていないようだったので、完了のお知らせを待つことに。

## 機械学習モデルを使った処理を動かしてみる
そうすると、10分もしないうちに（6〜7分くらい？）完了のお知らせが来ていました。
![トレーニング完了のお知らせ.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/6c663055-a1f1-b74b-4127-85ed58647c88.jpeg)
モデルに関する情報が書かれていて「Model: Classification MobileNetV1 Alpha: 0.7 Depth: 1」という内容になるようです。

ちなみに、そのメール本文の下には以下のようなグラフが 2つ表示されていました。
![グラフ1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/f8827fb4-8b5c-4af0-49e7-24a9c7db7e66.png)
![グラフ2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/60eb46d8-0d06-236f-65fc-f8837d3278e4.png)
ぱっと見で「精度は大丈夫だろうか？」と思うようなグラフに見えますが、とりあえず先に進んでいきます。

メール本文に書かれたリンクから ZIPファイルがダウンロードでき、解凍してみると以下のファイル 4つが得られました。公式のドキュメントの記載と違っている部分がありますが（.h5 のファイルがある部分で、これは .kmodel のファイルが生成される前段階の変換前ファイルが [HDF5](https://qiita.com/simonritchie/items/23db8b4cb5c590924d95) という理解で良いのかな）、必要なファイル 3つはそろっているようなので、次に進んでいきます。
![モデルなどZIPファイルの中身.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/a1773218-c911-3bce-e822-adb74280d22e.jpeg)
ここで、この機械学習モデルを使う処理が書かれた boot.py の内容を少し見てみます。

```python
import image
import lcd
import sensor
import sys
import time
import KPU as kpu
from fpioa_manager import *

import KPU as kpu

lcd.init()
lcd.rotation(2)

try:
    from pmu import axp192
    pmu = axp192()
    pmu.enablePMICSleepMode(True)
except:
    pass

try:
    img = image.Image("/sd/startup.jpg")
    lcd.display(img)
except:
    lcd.draw_string(lcd.width()//2-100,lcd.height()//2-4, "Error: Cannot find start.jpg", lcd.WHITE, lcd.RED)

task = kpu.load("/sd/【文字列】_mbnet10_quant.kmodel")

labels=["1","2","3"] #You can check the numbers here to real names.

sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.set_windowing((224, 224))
sensor.run(1)

lcd.clear()

while(True):
    img = sensor.snapshot()
    fmap = kpu.forward(task, img)
    plist=fmap[:]
    pmax=max(plist)
    max_index=plist.index(pmax)
    a = lcd.display(img)
    if pmax > 0.95:
        lcd.draw_string(40, 60, "Accu:%.2f Type:%s"%(pmax, labels[max_index].strip()))
a = kpu.deinit(task)
```

.kmodel のファイルを読み込んで利用する処理や、Confidence の値が 0.95 より大きい場合に、結果を画面上に表示する、といったことをやっていそうです。上記のメールで得られていた精度に不安があるので、 `if pmax > 0.95:` の部分の数値を下げたほうが良いかな、とも思ったのですが、まずはこのままで動かしてみました。

その結果がこちら。
![画像分類を実行.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/20c97ba1-2425-ecef-0333-73a6af7fd7e9.jpeg)
こちらは、クラス 1つ目のはずなのですが、意図した結果にはならない状況に...
3つのクラスがある中で、こちらは一番区別しやすそうなもののはず、と思ったのですが。

## 画像の背景を変えて再び
これまでの部分では、フローリングの上に認識対象の物を置いて撮影をしていました。
一方で、公式ドキュメントだと撮影をしている際に白い紙のようなものを敷いて、その上に認識対象の物を置いているようでした。そこで、この部分を公式の内容に合わせる形でやってみました。

ちなみに、学習用画像の見た目の違いは、例えば以下のようになりました。
![背景はフローリング.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/3f24dac5-3cd0-1040-cb82-dd721bbbe421.jpeg)
![背景は白い紙.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/df13f01f-8734-ff1f-6ca2-6312cbbc0fdd.jpeg)

そして、再度アップロードを行ったところ、3分ほどでモデルができたというお知らせが来ていました。
メール本文に出ているグラフ 2種類は、今度は以下のようになりました。前回分よりずいぶん良くなっている感じがします。

![グラフ1（2回目）.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/6bc8ce93-dcc3-accf-03e1-c8ca030f1352.png)

![グラフ2（2回目）.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/a6b9a243-f9c4-3726-1c55-c56e08eedf4c.png)

ただし、得られたデータで画像分類を実行してみると、やはり意図通りのクラスに分類されない感じが...

## まとめ・次のステップについて
今回、前回の記事の続きで完結編となる予定でしたが、完結せずに終わりました。
次に試してみようと思っていることは、以下の 2つです。
　1） 学習用に撮影する対象物を全く違った、見た目でも区別しやすそうなものにして試してみる
　2） .h5 の機械学習モデルのファイルを別の仕組み・プログラムで動かしてみたり、別の形式に変換して使ってみる
これらを試してみたら、また記事に書いてみようと思っています。次こそは、完結編になってくれれば良いな。

上記の 2）について、やるかどうか検討しようと思っているものを最後にメモ。
　●Kerasモデル(.h5)をTensorFlowモデル(.pb)に変換して使用する方法 - Qiita
　　https://qiita.com/karaage0703/items/5946e41b6043795c1b30
　●Keras モデルを TensorFlow.js にインポートする
　　https://www.tensorflow.org/js/tutorials/conversion/import_keras?hl=ja
　●teachablemachine-community/keras.md at master · googlecreativelab/teachablemachine-community
　　https://github.com/googlecreativelab/teachablemachine-community/blob/master/snippets/markdown/image/tensorflow/keras.md
　●Train, Convert, Run MobileNet on Sipeed MaixPy and MaixDuino!-Sipeed-Blog
　　https://blog.sipeed.com/p/680.html

## 余談
今回、画像分類を行うプログラムの boot.py の中を見てみましたが、「前回の記事で機械学習モデル用の画像を取得するために使ったプログラムは、どんな内容だったんだろう？」と気になって、それも見てみました（補足などは書いてないので、単に掲載してみただけになりますが）。

```python
import audio
import gc
import image
import lcd
import sensor
import sys
import time
import uos
import os
from fpioa_manager import *
from machine import I2C
from Maix import I2S, GPIO

#
# initialize
#
lcd.init()
lcd.rotation(2)

try:
    from pmu import axp192
    pmu = axp192()
    pmu.enablePMICSleepMode(True)
except:
    pass

fm.register(board_info.SPK_SD, fm.fpioa.GPIO0)
spk_sd=GPIO(GPIO.GPIO0, GPIO.OUT)
spk_sd.value(1) #Enable the SPK output

fm.register(board_info.SPK_DIN,fm.fpioa.I2S0_OUT_D1)
fm.register(board_info.SPK_BCLK,fm.fpioa.I2S0_SCLK)
fm.register(board_info.SPK_LRCLK,fm.fpioa.I2S0_WS)

wav_dev = I2S(I2S.DEVICE_0)

fm.register(board_info.BUTTON_A, fm.fpioa.GPIO1)
but_a=GPIO(GPIO.GPIO1, GPIO.IN, GPIO.PULL_UP) #PULL_UP is required here!

fm.register(board_info.BUTTON_B, fm.fpioa.GPIO2)
but_b = GPIO(GPIO.GPIO2, GPIO.IN, GPIO.PULL_UP) #PULL_UP is required here!

def findMaxIDinDir(dirname):
    larNum = -1
    try:
        dirList = uos.listdir(dirname)
        for fileName in dirList:
            currNum = int(fileName.split(".jpg")[0])
            if currNum > larNum:
                larNum = currNum
        return larNum
    except:
        return 0


def play_sound(filename):
    try:
        player = audio.Audio(path = filename)
        player.volume(20)
        wav_info = player.play_process(wav_dev)
        wav_dev.channel_config(wav_dev.CHANNEL_1, I2S.TRANSMITTER,resolution = I2S.RESOLUTION_16_BIT, align_mode = I2S.STANDARD_MODE)
        wav_dev.set_sample_rate(wav_info[1])
        spk_sd.value(1)
        while True:
            ret = player.play()
            if ret == None:
                break
            elif ret==0:
                break
        player.finish()
        spk_sd.value(0)
    except:
        pass

def initialize_camera():
    err_counter = 0
    while 1:
        try:
            sensor.reset() #Reset sensor may failed, let's try some times
            break
        except:
            err_counter = err_counter + 1
            if err_counter == 20:
                lcd.draw_string(lcd.width()//2-100,lcd.height()//2-4, "Error: Sensor Init Failed", lcd.WHITE, lcd.RED)
            time.sleep(0.1)
            continue

    sensor.set_pixformat(sensor.RGB565)
    sensor.set_framesize(sensor.QVGA) #QVGA=320x240
    sensor.run(1)

try:
    img = image.Image("/sd/startup.jpg") 
    lcd.display(img) 
except:
    lcd.draw_string(lcd.width()//2-100,lcd.height()//2-4, "Error: Cannot find start.jpg", lcd.WHITE, lcd.RED)   

time.sleep(2)

initialize_camera()

currentDirectory = 1

if "sd" not in os.listdir("/"):
    lcd.draw_string(lcd.width()//2-96,lcd.height()//2-4, "Error: Cannot read SD Card", lcd.WHITE, lcd.RED)   

try:
    os.mkdir("/sd/train")
except Exception as e:
    pass

try:
    os.mkdir("/sd/vaild")
except Exception as e:
    pass

try:
    currentImage = max(findMaxIDinDir("/sd/train/" + str(currentDirectory)), findMaxIDinDir("/sd/vaild/" + str(currentDirectory))) + 1
except:
    currentImage = 0
    pass

isButtonPressedA = 0
isButtonPressedB = 0

try:
    while(True):
        img = sensor.snapshot()

        if but_a.value() == 0 and isButtonPressedA == 0:
            if currentImage <= 30 or currentImage > 35:
                try:
                    if str(currentDirectory) not in os.listdir("/sd/train"):
                        try:
                            os.mkdir("/sd/train/" + str(currentDirectory))
                        except:
                            pass
                    img.save("/sd/train/" + str(currentDirectory) + "/" + str(currentImage) + ".jpg", quality=95)
                    play_sound("/sd/kacha.wav")
                except:
                    lcd.draw_string(lcd.width()//2-124,lcd.height()//2-4, "Error: Cannot Write to SD Card", lcd.WHITE, lcd.RED)
                    time.sleep(1)
            else:
                try:
                    if str(currentDirectory) not in os.listdir("/sd/vaild"):
                        try:
                            os.mkdir("/sd/vaild/" + str(currentDirectory))
                        except:
                            pass
                    img.save("/sd/vaild/" + str(currentDirectory) + "/" + str(currentImage) + ".jpg", quality=95)
                    play_sound("/sd/kacha.wav")
                except:
                    lcd.draw_string(lcd.width()//2-124,lcd.height()//2-4, "Error: Cannot Write to SD Card", lcd.WHITE, lcd.RED)
                    time.sleep(1)
            currentImage = currentImage + 1
            isButtonPressedA = 1

        if but_a.value() == 1:
            isButtonPressedA = 0

        if but_b.value() == 0 and isButtonPressedB == 0:
            currentDirectory = currentDirectory + 1
            if currentDirectory == 11:
                currentDirectory = 1

            currentImage = max(findMaxIDinDir("/sd/train/" + str(currentDirectory)), findMaxIDinDir("/sd/vaild/" + str(currentDirectory))) + 1
                
            isButtonPressedB = 1

        if but_b.value() == 1:
            isButtonPressedB = 0
            
        img.draw_rectangle(0,60,320,1,color=(0,144,255),thickness=10)
        img.draw_string(50,55,"Train:%03d/35   Class:%02d/10"%(currentImage,currentDirectory),color=(255,255,255),scale=1)
        lcd.display(img)

except KeyboardInterrupt:
    pass
```

## 【追記】 
また、再度チャレンジしてみようかという時のために、V-Training関連の記事をちょこちょこ調べて、比較的新しめのものをメモしておこうかと。それと、調べた過程で出てきた UnitV や V-Training関連以外のものについても。

●M5StickVとV-Trainingでお手軽画像認識　その１ – スイッチサイエンス マガジン
　https://mag.switch-science.com/2020/06/30/m5stickv-v-training-1/
●M5StickVとV-Trainingでお手軽画像認識　その2 – スイッチサイエンス マガジン
　https://mag.switch-science.com/2020/08/07/m5stickv-v-training-2/

●M5UnitVの開発環境をセットアップする - Qiita
　https://qiita.com/Nabeshin/items/7d49ca51cfdb10b70014

●UnitV AI Cameraの使い方 - Fusic Tech Blog
　https://tech.fusic.co.jp/posts/2020-05-11-unitv-ai-camera/

●PLEN:bitとUnitV AI cameraで自撮りロボ製作｜PLEN｜note
　https://note.com/plenproject/n/n05f974d4cf05

●M5StickV、UnitVで困ったときの対処や注意すること | ラズパイ好きの日記
　https://raspberrypi.mongonta.com/m5stickv-unitv-tips/