今、話題になっているAIoT(AI+IoT)デバイスの「M5StickV」を少し触ってみたので、今回はその際に知り得た情報を紹介します。
M5StickVの概要
まず、M5StickVをご存知ですか?
M5StickVは、M5Stack社が販売している小型AIカメラデバイスで、Kendryte社のプロセッサK210を搭載しています。非常に小さなデバイスで、一般的な消しゴムを少し大きくしたぐらいの大きさですが、カメラ以外に、液晶ディスプレイやLEDランプ、スイッチ、ジャイロセンサーも装備しています。そして、高性能なニューラルネットワークプロセッサ(KPU)をもっているだけでなく、購入後にセットアップすると、すぐにYOLOモデルを利用した物体検出機能が使えるようになっています。
日本では、スイッチサイエンスや共立電子産業などから購入できますが、なんと3000円程度という低価格です!(2020年11月時点の情報)これは、オープンなライセンスとなるRISC-Vを活用しているからなのでしょう。
仕様に関する参考資料
|
M5StickVのセットアップ
以下の手順でセットアップを実行できます。(私はWindows10で行いました)
セットアップ手順:
- microSDカードを用意する
- 私が購入したmicroSDは以下の通りです。
- ちなみに「動作したもの」としてのmicroSDカード情報が、有志により公開されているので、この中から利用したいものを選択することも可能です。
- 公式ページ「M5StickV Quick Start」を開き、手順通りに作業を行う
- ファームウェアをダウンロードする
- ファームウェアの書き込みツール「Kflash_GUI」をダウンロードする
- 「Kflash_GUI」を利用して、ファームウェアを書き込む
M5StickVで開発
M5StickVでは、MicroPythonをベースにしたMaixPyという言語でプログラミングができます。
- リファレンス
- ニューラルネットワークだけでなく、OpenMVをベースとしたと思われる画像処理のライブラリも利用できます
また、プログラムを動かす方法として、以下の4つの手段を取ることができます。
プログラム実行手段:
< シリアル通信 >
「PuTTY」などで接続して、ターミナル上でMaixPyのコードを書いて実行します
-
-
- 手順「Serial-Tool」
-
< 専用開発環境 >
「MaixPy IDE」という開発環境があるので、M5StickVに接続させて、ローカルにあるファイルを作成・編集し、実行すれば動きます
-
-
- ターミナルも実行できます
- カメラの映像をIDE内でも表示してくれます
- 手順「MaixPy IDE」
-
< データ転送ツール >
「uPyLoader」いうツールが提供されているので、M5StickVに接続させて、ファイルをアップロードさせて、実行することができます
-
-
- ターミナルも実行できます
- 接続時、M5StickV上でプログラミングが実行中の場合は接続できませんので、処理を終了させておく必要があります
(※この事については、補足説明を後述します) - 資料「uPyLoader – README.md」
-
< その他 >
microSDカードに、プログラムを書いた「boot.py」というファイルを保存し、M5StickVの電源を入れます
-
- M5StickVが起動時に、「boot.py」というファイルを探しに行きます。SDカード上にあればをそれを実行します
- M5StickVが起動時に、「boot.py」ファイルがなければ、自身で「boot.py」ファイルを作成し、実行します
- 実行させる中身は以下の内容です
ちなみに、私は主に「MaixPy IDE」を用いて、コードを書いては動かして、という作業を繰り返しました。また、後で取り上げるモデルのファイルをアップロードする際、「uPyLoader」で作業を行うこともできますが、非常に遅いです。面倒ですが、SDカードを直接抜き差ししてファイルを入れ替えた方が断然に速いです。
その他、実際に開発していて困ったところがあったので、その対処方法を記載しておきます。
その1:
M5StickVは上記の4つ目の手段でも記載したように、「boot.py」ファイルを自動実行します。また、ネットの記事でもよく見かけますが、電源がなかなか切れず、すぐに再起動してしまいます。プログラムを実行していると、上記「uPyLoader」での接続が必ず失敗するので、故意にプログラムを停止できる仕組みを用意しておいた方がいいです。
下記例では、ボタンを利用できるようにして、起動時にボタンが押されていていたら、プログラムを停止できるようにしています。
1 2 3 4 5 6 7 8 9 10 11 |
import sys from board import board_info from fpioa_manager import fm from Maix import GPIO fm.register(board_info.BUTTON_A, fm.fpioa.GPIO1) but_a=GPIO(GPIO.GPIO1, GPIO.IN, GPIO.PULL_UP) if but_a.value() == 0: sys.exit() |
その2:
ディスプレイに映る画像の向きが、何もしないと下図「初期状態」のようにおかしいので、以下のコードで下図「修正後」の状態にします。
初期状態 | 修正後 |
1 2 3 4 5 |
import lcd lcd.init() lcd.rotation(2) |
自身のニューラルネットワークモデルを利用する
M5StickVではデフォルトで、YOLOモデルでの顔認識機能が入っていますが、自身でニューラルネットワークモデルを作って実行することもできます。
モデルの変換
Maixpyで実行できるモデルは「kmodel」というものになります。そのため、例えば「Keras」で学習したモデルを使いたい場合は、その「Keras」モデルを「kmodel」に変換する必要があります。ただし、「kmodel」は「TensorFlow Lite」モデルからしか変換できないので、一旦、「Keras」から「TensorFlow Lite」への変換を行います。
それから、「kmodel」は2MB未満にしないとロード時にメモリ不足となり、処理が落ちてしまうので、「Keras」モデルの時から軽量なものにする必要があります。処理内で他のことも一緒にしようと思うと、ファームウェアを必要最低限の軽量版に変更するなどの対応が必要になります。
その他にも、色々と考慮を行いました。
例えば、今回私は、MobileNetという学習済みのモデルを転移学習したのですが、追加した出力層付近のレイヤーの数、レイヤーあたりのノード数を多くしても、サイズが大きくなるので、その数を調整しました。また、ファインチューニングも試したのですが、ファインチューニングで再学習するレイヤーを増やすと、なぜかサイズが大きくなったので、その点も考慮しました。
ちなみに、私が変換した際のモデルのサイズは以下の通りでした。
3model.kmodel1.9Mさらに変換された「kmodel」
モデル | サイズ | 備考 | |
---|---|---|---|
1 | model.h5 | 7.9M | 元々の「Keras」モデル |
2 | model.tflite | 7.1M | 変換された「TensorFlow Lite」モデル |
この「kmodel」のサイズは、非常に小さくなっています。モデル変換時に量子化の処理が行われているようです。量子化の仕様までは分かりませんが、TensorFlow Liteでの量子化した結果と比べると、「Weight quantization Model」と同じくらい軽量化されています。(ちなみに、TensorFlow Liteでの量子化されたモデルを、「kmodel」に変換することはできませんでした。)
3model_iq.tflite2.0M「TensorFlow Lite」モデル / Integer quantization Model4model_fi.tflite2.0M「TensorFlow Lite」モデル / Full integer quantization Model
モデル | サイズ | 備考 | |
---|---|---|---|
1 | model_wq.tflite | 1.9M | 「TensorFlow Lite」モデル / Weight quantization Model |
2 | model_fp16.tflite | 3.6M | 「TensorFlow Lite」モデル / Float16 quantization Model |
※参考:サンプルコード
- 「TensorFlow Lite」モデル(量子化なし)への変換
123456789101112import tensorflow as tf # version 2.xmodel = tf.keras.models.load_model('model.h5')# モデルを変換 :converter = tf.lite.TFLiteConverter.from_keras_model(model)tflite_model = converter.convert()# 保存with open('model.tflite', 'wb') as f:f.write(tflite_model) - 「kmodel」への変換
12345678910111213141516# 変換ツール「nncase」の準備mkdir -p nccmkdir -p workspacemkdir -p imagesmkdir -p logcd nccwget https://github.com/kendryte/nncase/releases/download/v0.1.0-rc5/ncc-linux-x86_64.tar.xztar -Jxf ncc-linux-x86_64.tar.xzrm ncc-linux-x86_64.tar.xz# 「images」ディレクトリに画像を準備しておく# (※処理は省略)# 変換処理の実行./ncc/ncc -i tflite -o k210model --dataset images /content/model.tflite /content/model.kmodel
※注意事項:
-
-
- 「ncc」コマンドでオプション「–dataset」で指定している「images」は自身で用意したディレクトリです。
- ディレクトリの中には学習で用いた画像を入れています。
- 画像は量子化の処理に利用されるようです。
- 上記サンプルコードは「Maix ToolBox」のコードを参考にしました。
- 「Keras」モデルと「kmodel」でパディングの仕様が異なるため、「Keras」モデルのパディングを一部変更しておく必要があるようです。
- 「ncc」コマンドでオプション「–dataset」で指定している「images」は自身で用意したディレクトリです。
-
モデルのデプロイ
「kmodel」の準備ができたら、SDカードにモデルを保存して、Maixpyでロードして、処理することになります。
1 |
kpu.load("/sd/model.kmodel") # モデルの読み込み |
M5StickVを利用したデモ
M5StickVで元々使える顔検出機能を使って、検出した顔の1つ目と2つ目を入れ替えるカメラを作ってみました。
次のデモ動画を見ると、顔を入れ替える際に、境い目をブレンディングしたりしていないので、入れ替えた箇所がはっきり分かると思います。また、手振れが激しいので、顔を入れ替わった状態の画像をじっと見られるように、ディスプレイの横にあるボタンでディスプレイの描画を一時停止し、確認し易くしました。(一時停止中は白色LEDランプを点灯させています)もう一度、同じボタンを押すと、元の状態に戻ります。
※顔画像は、データセット「IMDB-WIKI」をMatplotlibで画面に描画したものになります。
以下にコードを掲載しますが、画像のリサイズがうまくできなかったり、画像の上書きにスライスが利用できず、1ピクセルずつ上書きしたりと、普段との使い勝手との違いに困りました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
import sensor import image import lcd import KPU as kpu import gc from fpioa_manager import * from Maix import GPIO import sys lcd.init() lcd.rotation(2) # 画面の向きとカメラの向きをそろえておく fm.register(board_info.BUTTON_A, fm.fpioa.GPIO1) but_a = GPIO(GPIO.GPIO1, GPIO.IN, GPIO.PULL_UP) fm.register(board_info.LED_W, fm.fpioa.GPIO3) led_w = GPIO(GPIO.GPIO3, GPIO.OUT) if but_a.value() == 0: print('[info]: Exit by user operation') sys.exit() # 現在の空きヒープの25%以上が占有されるようになると、GCを引き起こします gc.collect() gc.threshold(gc.mem_free() // 4 + gc.mem_alloc()) led_w.value(1) #消灯 sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) sensor.run(1) def image_exchange(img_org, face, img_face): width = face.rect()[2] height = face.rect()[3] x1 = face.rect()[0] x2 = x1 + width y1 = face.rect()[1] y2 = y1 + height for x,x_new in zip(range(x1,x2), range(width)): for y,y_new in zip(range(y1,y2), range(height)): try: a = img_org.set_pixel(x,y, img_face.get_pixel(x_new, y_new)) except Exception as e: #a = img_org.set_pixel(x,y, (0,0,0)) # 黒塗り pass # 何もしない。もとの顔のままとする。 task_face = kpu.load(0x300000) anchor = (1.889, 2.5245, 2.9465, 3.94056, 3.99987, 5.3658, 5.155437, 6.92275, 6.718375, 9.01025) a = kpu.init_yolo2(task_face, 0.5, 0.3, 5, anchor) try: while(True): img = sensor.snapshot() code = kpu.run_yolo2(task_face, img) if code: count = len(code) if count >= 2: face1 = code[0] face1_width = face1.rect()[2] face1_height = face1.rect()[3] face1_x1 = face1.rect()[0] face1_x2 = face1_x1 + face1_width face1_y1 = face1.rect()[1] face1_y2 = face1_y1 + face1_height img_face1 = img.copy(face1.rect()) face2 = code[1] face2_width = face2.rect()[2] face2_height = face2.rect()[3] face2_x1 = face2.rect()[0] face2_x2 = face2_x1 + face2_width face2_y1 = face2.rect()[1] face2_y2 = face2_y1 + face2_height img_face2 = img.copy(face2.rect()) # 実行してもエラーにならないが画像サイズは変更されなかった #a = img_face2.resize(face1_width, face1_height) #a = img_face1.resize(face2_width, face2_height) #a = img.draw_rectangle(face1.rect(), color=(0,0,0), fill=True) # 顔の場所を黒塗り #a = img.draw_rectangle(face2.rect(), color=(0,0,0), fill=True) # 顔の場所を黒塗り image_exchange(img, face1, img_face2) image_exchange(img, face2, img_face1) if but_a.value() == 0: time.sleep(1) while(True): led_w.value(0) #点灯 a = lcd.display(img) if but_a.value() == 0: break # 認識された顔の数を画面左上に描画 a = img.draw_string(50, 50, "count: {}".format(len(code)), color=(255,0,0), scale=2, mono_space=False) else: led_w.value(1) #消灯 a = lcd.display(img) a = kpu.deinit(task_face) except Exception as e: if "task_face" in locals(): a = kpu.deinit(task_face) #raise sys.exit() |
まとめ
普段利用しているPythonと違い、Numpyが使えないなど、使い勝手の違いに悩むところもありましたが、それ以上に、Pythonライクなコードを書いて、SDカードに保存するだけで、簡単に動作する容易性が大変うれしいです。
今後はジャイロセンサーのデータを利用したアプリでも作って、試してみたいなと思っています。
このような安価なデバイスが、これからもどんどん出てくると思います。初期投資費用もほとんどかからず非常に簡単なので、是非皆さんも手始めに試してみて下さい。
執筆者プロフィール
- 入社以来、C/S型の業務システム開発に従事してきました。ここ数年は、SalesforceやOutSystemsなどの製品や、スクラム開発手法に取り組み、現在のテーマは、DeepLearning/機械学習です。
この執筆者の最新記事
- Pick UP!2021.11.11VoTTを複数人で使って、アノテーションを行いたい!(ファイル移行を用いて)
- Pick UP!2020.11.20AIoTデバイス「M5StickV」、はじめの一歩
- RPA2019.08.15「OSSのRPA」+「自作の三目並べマシン」でGoogleに挑む!
- AI2019.04.22暗記学習(Rote Learning)で三目並べを強くする