こんにちは。
フクロウ大好きエンジニアの”はやぶさ”@Cpp_Learningです。
最近、M5Stack入門したので、色々なアプリを作って楽しんでいます。
また、『深層学習 × 画像処理』も大好きなので、本ブログ(はやぶさの技術ノート)で情報発信しています。
など
今回は、自分の大好きなものを組み合わて…
『深層学習 × 画像処理 × M5Stack』でアバターアプリを作りました。
深層学習 ✖︎ 画像処理 ✖︎ M5Stack pic.twitter.com/NqAUP63LgG
— はやぶさ (@Cpp_Learning) September 13, 2019
Contents
アバターアプリを作るモチベーション
我が家には、フクロウの”くるる”ちゃん@kururu_owlが住んでいます。
好奇心旺盛な3歳児の行動は、休日ずーーっと見ていても飽きない(*・ω・)ノ♪
あっちへ行ったり、こっちへ飛んだり…普段は大人しいけどスイッチが入るとアクティブに!
予測不可能な”くるる”ちゃんの行動が可愛らしい!
カメラで撮影した動物の動きに合わせてアバター(イラスト)が動いたら可愛らしいし、ちょっとした行動監視にも使えそうです。
…面白そうなので作ることにしました!以降から開発手順を解説します。
アバターアプリの要素技術
最初に「アバターアプリ」の要素技術を検討します。
- カメラ画像からフクロウを検出する技術
- フクロウの座標データを送信する技術
- アバター(イラスト)を動かす画像処理技術
深層学習による物体検出
❶フクロウ検出については、深層学習による物体検出が使えそうです。
自分の好きな深層学習フレームワークを使えば良いです。
今回は、実験などで扱いやすい易いコードに仕上げた『MXNetの物体検出ソフト』を採用します。
M5StackとPCのUDP通信
❷フクロウの座標データ送信については、以前トライしたUDP通信が使えそうです。
通信環境などを考慮して、通信方式を決めても良いと思います。
今回は「ノートPC + Webカメラで物体検出」⇒「座標データをM5Stackに送信」を実践します。
画像処理によるアニメーション
➌アバター(イラスト)を動かす画像処理については、私の専門分野です。本ブログで「画像処理チュートリアル」をいくつか公開しています。
『M5Stackでアニメーションを作成するチュートリアル』も公開しています。
つまり、『深層学習 × 画像処理 × M5Stack』によるアバターアプリを開発するための要素技術は全て、本ブログ(はやぶさの技術ノート)で公開しています(*・ω・)ノ♪
開発したいアプリが決まったら、まず最初に”そのアプリ”を実現するための要素技術を検討します。
- 習得済みの技術で実現できるか?
- どの技術を組み合わせれば良いか?
- どんな技術を新規に習得する必要があるか?
など、自分のやりたいことを実現するために、多くの技術を習得しましょう!
実践!深層学習×画像処理×M5Stackでアバターアプリ開発
開発するアプリのイメージを再確認します。
『深層学習でフクロウ検出して座標データを送信する”AIアプリ”』
『座標データを受信してアバター(イラスト)を動かす”画像処理アプリ”』
以上の二つのアプリを開発する必要があります。
また、座標データ通信用のネットワークも構築する必要があります。
本記事では、以下のネットワークを構築してアプリの動作確認を行いました。
ホームルータやスマホ(テザリング)などをアクセスポイントとしたネットワークを構築し、UDP通信で座標データをやり取りします。
深層学習で物体検出して座標データを送信する”AIアプリ”
最初に『深層学習でフクロウ検出して座標データ送信する”AIアプリ”』のソースコードを紹介します。
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 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 |
import argparse import matplotlib.pyplot as plt from timeit import default_timer as timer import cv2 import gluoncv as gcv import mxnet as mx import json from socket import socket, AF_INET, SOCK_DGRAM # M5Stack address ADDRESS = "192.168.2.3" PORT = 5555 # M5Stack window size WINDOW_W = 320 WINDOW_H = 240 def main(): parser = argparse.ArgumentParser() parser.add_argument('-model', default='ssd_512_mobilenet1.0_voc') parser.add_argument('-threshold', type=float, default=0.5) parser.add_argument('video') args = parser.parse_args() # Set threshold th = args.threshold # Load the webcam handler if args.video == "0": cap = cv2.VideoCapture(0) else: cap = cv2.VideoCapture(args.video) if not cap.isOpened(): raise ImportError("Couldn't open video file or webcam.") # For UDP s = socket(AF_INET, SOCK_DGRAM) # Compute aspect ratio of video vidw = cap.get(cv2.CAP_PROP_FRAME_WIDTH) vidh = cap.get(cv2.CAP_PROP_FRAME_HEIGHT) vidw = int(vidw) vidh = int(vidh) print(vidw) print(vidh) # Load the model net = gcv.model_zoo.get_model(args.model, pretrained=True) # net = gcv.model_zoo.get_model('yolo3_mobilenet1.0_coco', pretrained=True) # net = gcv.model_zoo.get_model('ssd_512_mobilenet1.0_voc', pretrained=True) # Time parameter accum_time = 0 curr_fps = 0 fps = "FPS: ??" prev_time = timer() frame_count = 1 while True: # Load frame from the camera ret, frame = cap.read() if ret == False: print("Done!") return # Result image result_img = frame.copy() # Image pre-processing frame = mx.nd.array(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)).astype('uint8') rgb_nd, scaled_frame = gcv.data.transforms.presets.ssd.transform_test(frame, short=512, max_size=700) # Run frame through network class_IDs, scores, bounding_boxes = net(rgb_nd) # Convert NDArray to numpy.ndarray bounding_boxes = bounding_boxes.asnumpy() scores = scores.asnumpy() class_IDs = class_IDs.asnumpy() # Class name list class_names = net.classes # Display the result for i, bbox in enumerate(bounding_boxes[0]): if th < scores[0][i]: score = scores[0][i] class_id = class_IDs[0][i] class_name = class_names[int(class_id)] xmin = int(bbox[0]) ymin = int(bbox[1]) xmax = int(bbox[2]) ymax = int(bbox[3]) # Draw box cv2.rectangle(result_img, (xmin, ymin), (xmax, ymax), (0,255,0), 2) text = class_name + " " + ('%.2f' % score) print(text) text_top = (xmin, ymin - 10) text_bot = (xmin + 80, ymin + 5) text_pos = (xmin + 5, ymin) # Draw class and score cv2.rectangle(result_img, text_top, text_bot, (255,255,255), -1) cv2.putText(result_img, text, text_pos, cv2.FONT_HERSHEY_SIMPLEX, 0.35, (0, 0, 0), 1) # Detected target(bird) if class_name == "bird": # M5Stack scale x,y x = str(xmin*WINDOW_W/vidw) y = str(ymin*WINDOW_H/vidh) # JSON data = {'class_name': class_name, 'x': x, 'y': y} dic = json.dumps(data) print(dic) # Send JSON (class_name, x, y) s.sendto(dic.encode(), (ADDRESS, PORT)) # s.sendto(x.encode('utf-8'), (ADDRESS, PORT)) # s.sendto(y.encode('utf-8'), (ADDRESS, PORT)) # time.sleep(1) else: # Cut low score break # Calculate FPS curr_time = timer() exec_time = curr_time - prev_time prev_time = curr_time accum_time = accum_time + exec_time curr_fps = curr_fps + 1 if accum_time > 1: accum_time = accum_time - 1 fps = "FPS: " + str(curr_fps) curr_fps = 0 # Draw FPS in top right corner cv2.rectangle(result_img, (vidw-50, 0), (vidw, 17), (0, 0, 0), -1) cv2.putText(result_img, fps, (vidw-45, 10), cv2.FONT_HERSHEY_SIMPLEX, 0.35, (255, 255, 255), 1) # Draw Frame Number in top left corner cv2.rectangle(result_img, (0, 0), (50, 17), (0, 0, 0), -1) cv2.putText(result_img, str(frame_count), (0, 10), cv2.FONT_HERSHEY_SIMPLEX, 0.35, (255, 255, 255), 1) # Output Result title = args.model + " Result" cv2.imshow(title, result_img) # Stop Processing if cv2.waitKey(1) & 0xFF == ord('q'): break frame_count += 1 if __name__ == '__main__': main() |
既に説明した通りですが、以下の2つの記事で紹介したソースコードを組み合わせて、AIアプリを作成しました。
「MXNetの物体検出ソフト」をベースに「UDP通信(送信用)コード」を追記しています。
ポイントは以下の通りです。
113~127行目の追記コード
- ”bird”を検出したときのみM5Stackにx,y座標を送信
- M5Stackの画面サイズに合わせてx,y座標をリサイズ
- 送信データ(クラス名とx,y座標)をJSONフォーマットにひとまとめ
- M5Stackのアドレスとポートを指定して送信
座標データを受信してアバター(イラスト)を動かす”画像処理アプリ”
次に『座標データを受信してアバター(イラスト)を動かす”画像処理アプリ”』のソースコードを紹介します。
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 |
#include <M5Stack.h> #include <WiFi.h> #include <WiFiUdp.h> #include <ArduinoJson.h> #define N 1024 const char* ssid = "yourssid"; const char* password = "yourpassword"; const int port = 5555; // The udp library class WiFiUDP udp; // M5Stack window size #define WINDOW_W 320 #define WINDOW_H 240 void print_wifi_state(){ M5.Lcd.clear(BLACK); M5.Lcd.setTextColor(YELLOW); M5.Lcd.setCursor(3, 3); M5.Lcd.println(""); M5.Lcd.println("WiFi connected."); M5.Lcd.print("IP address: "); M5.Lcd.println(WiFi.localIP()); M5.Lcd.print("Port: "); M5.Lcd.println(port); } void setup_wifi(){ M5.Lcd.setTextColor(RED); M5.Lcd.setTextSize(2); M5.Lcd.setCursor(3, 10); M5.Lcd.print("Connecting to "); M5.Lcd.println(ssid); // setup wifi WiFi.mode(WIFI_STA); // WIFI_AP, WIFI_STA, WIFI_AP_STA or WIFI_OFF WiFi.begin(ssid, password); // WiFi.begin(); // Connecting .. while (WiFi.status() != WL_CONNECTED) { delay(100); M5.Lcd.print("."); } // print state print_wifi_state(); udp.begin(port); } void setup() { M5.begin(); M5.Lcd.clear(BLACK); M5.Lcd.setTextColor(YELLOW); M5.Lcd.setTextSize(2); M5.Lcd.setCursor(3, 10); setup_wifi(); M5.Lcd.println(""); M5.Lcd.setTextColor(GREENYELLOW); M5.Lcd.setTextSize(3); M5.Lcd.println("Bird Detection."); } void loop() { char packetBuffer[N]; int packetSize = udp.parsePacket(); DynamicJsonDocument doc(N); // get packet if (packetSize){ int len = udp.read(packetBuffer, packetSize); if (len > 0){ packetBuffer[len] = '\0'; // end } // for JSON deserializeJson(doc, packetBuffer); JsonObject obj = doc.as<JsonObject>(); // get param String class_name = obj["class_name"]; uint16_t x = obj["x"]; uint16_t y = obj["y"]; if (x < 0 || WINDOW_W < x){ x = WINDOW_W/2; } if (y < 0 || WINDOW_H < y){ y = WINDOW_H/2; } // Draw image M5.Lcd.clear(BLACK); M5.Lcd.drawJpgFile(SD, "/img/kururu.jpg", x, y); // Draw info M5.Lcd.setTextColor(GREENYELLOW); M5.Lcd.setTextSize(2); M5.Lcd.setCursor(3, 220); M5.Lcd.print("class:"); M5.Lcd.print(class_name); M5.Lcd.print(" x:"); M5.Lcd.print(x); M5.Lcd.print(" y:"); M5.Lcd.print(y); } } |
既に説明した通りですが、以下の2つの記事で紹介したソースコードを組み合わせて、画像処理アプリを作成しました。
「M5Stackで画像処理(動画編)のコード」をベースに「UDP通信(JSONフォーマット受信用)コード」を追記しています。
ポイントは以下の通りです。
UDP/JSON/画像処理(アバター描画)について
- UDP通信で座標データ(JSONフォーマット)を受信
- ArduinoJSONでJSONフォーマットを扱う
- 受信したx,y座標がM5StackのWindowサイズ(320×240)に収まらない場合は画面の中心座標をx,yに代入
- drawJpgFile関数でSDカードに保存したアバター(JPGファイル)をx,y座標を指定して描画
- 受信データ(クラス名とx,y座標)を画面左下に表示
深層学習×画像処理×M5Stackによるアバターアプリ動作手順
以下の手順で『深層学習×画像処理×M5Stackによるアバターアプリ』を楽しめます。
【アバターアプリ動作手順】
- 「画像処理アプリ」をインストールしたM5Stackの電源ON
- M5Stackが自動でネットワークに接続され、座標データ受信待ちになる
- 「AIアプリ」をPCから起動する
- ”くるる”ちゃん(bird)をWebカメラで撮影する
- M5Stackに表示されたアバター(イラスト)が”くるる”ちゃんの座標に合わせて移動
実際に”くるる”ちゃんの映像を使い『アバターアプリ』で遊んでみました。
現実世界の”くるる”ちゃんに合わせてアバター(イラスト)が動くの面白し、可愛い!
深層学習×画像処理×M5Stackによるアバターアプリの改良ポイント
本アプリをベースに自分好みのアプリを作ってみましょう!改良ポイント紹介しますね。
ハードウェア選定
今回は「AIアプリ」をノートPC、「画像処理アプリ」をM5Stackで動かしましたが…
「AIアプリ」をJetson Nano、「画像処理アプリ」をRaspberry Pi(ラズパイ)で動かすなど、自分のやりたいことに合わせて、好きなディバイスを使ってください。
AIアプリ(深層学習による物体検出アプリ)改良
今回は『MXNetの物体検出ソフト』をベースにしました。
本記事で公開している動画では「MobileNetV1-SSDモデル」を使って、物体検出(フクロウ検出)を実現しています。
Model Zoo(モデルの動物園)のObject Detectionカテゴリーで公開しているモデル(Faster-RCNN, YOLO-v3など)も使えるので、処理速度や検出精度を考慮して、目的に合うモデルを使ってください。
また、’bird’以外に‘person’, ‘car’, ‘cat’, ‘dog’なども検出できます。ソースコードの以下の部分を書き換えて、好きな対象の座標データを抽出してください。
1 2 3 4 |
# Detected target(bird) if class_name == "bird": |
もし、Model Zooのモデルで満足できない場合は、独自モデルを作成する必要があります。
検出したい対象の画像を大量に用意して、MXNet・Tensorflow・Pytorch・Chainerなどの深層学習フレームワークを使い、学習(モデル作成)に挑戦してみて下さい。
画像処理アプリ(M5Stackアプリ)改良
今回はフクロウの”くるる”ちゃん@kururu_owlを対象としたアバターアプリなので、以下のイラストをSDカードに保存して、LCDに描画させました。
イラストを変更して、自分好みのアバターアプリにして下さい。
また、以下の記事で紹介した通り、M5Stack用の拡張モジュールやGroveセンサがあります。
モーターを駆動させたり、LEDを光らせたり…アバターの動きに合わせて何かを制御するのも楽しいと思いますよ(*・ω・)ノ♪
本記事で公開しているソースコード(アプリ)は改良OKです。自由な発想でAIアプリ開発を楽しんで頂けたら、とても嬉しいです。
AIアプリ開発チュートリアルまとめ
本記事で「AIアプリ開発チュートリアル」を書きました。本記事を読むことで以下のことを学べたと思います。
【本記事で学べる内容】
- 【AI】深層学習による物体検出を利用したアプリの例
- 【IoT】UDP通信でデータをやり取りする方法
- 【画像処理】好きなイラストを自由に移動させる画像処理
- 【VR】リアルな動物とバーチャルなアバターをリンクさせる方法
- 【マイコン】M5Stack(Arduino)アプリ開発
- 【プログラミング】自分のやりたいことを実現するためのヒント
- インターネット未使用なので、正確には【IoT】一歩手前
- 動物(人含む)の動きに合わせてアバターが動くので、【VR】ではなく【インターフェース】という表現の方が適切かもしれません
Webアプリ以外の「AIアプリ開発チュートリアル」ってあまり無い気がするので、参考になれば嬉しいです。
”くるる”ちゃんのように本記事で楽しく勉強してくれる人が一人でもいたら嬉しいです!
おまけ -AIアプリ開発のヒントとメッセージ-
時間がある人は、以下の”おまけ”も読んでみてね。
”くるる”ちゃんが考えたAIアプリのアイデア
こんにちは。
本記事は面白い内容が”ギュッ”と凝縮されてて、しかもアプリを改良するときのポイントまで書いてある!
ノートPCとM5Stackで手軽に試せるので、初期投資も少ない!
本記事を参考にすれば”くるる”でもAIアプリ作れそう!!
”くるる”は暑いの苦手だから室温によって行動が違うと思うけど…あまり意識したことないからアバターで自分の行動を客観視してみたい!
M5Stackに温度センサ取り付けて、室温とアバターの座標から相関関係を~♪
実用的なアプリ・面白いアプリ…作りたいアプリは人それぞれだと思います。
”くるる”ちゃんのように自由な発想で『AIアプリ開発』を楽しんでくれたら嬉しいです!
メッセージ -本ブログのサポートについて-
本記事は、もし自分が技術書典(技術同人誌即売会)などで本を出すなら『深層学習×画像処理×M5Stack -AIアプリ開発チュートリアル-』というタイトルで書きたい!と想った内容を具現化したものです。
ただ、お金のない学生さんも含め、多くの人に『AIアプリ開発』を楽しんでもらいたい!という想いから、無料公開にしました。
(本は無理でも有料noteで公開したら、Jetson Nanoの購入資金くらい稼げたかな?とか考えたりもするけど…)
もし、本チュートリアルが参考になり、ブログ『はやぶさの技術ノート』をサポートしたいという人がいれば、以下の方法でサポートして頂けると嬉しいです!
【本ブログのサポート方法】
- 本ブログの記事をSNS(Twitterやfacebookなど)でシェア
- 本記事を参考に『アプリ開発』を実践したら、参考資料で本記事を紹介
記事に対して反応があると、次も良い記事書きたいな!というモチベーションに繋がります。気軽にシェアして頂けると嬉しいです。
また、本記事が拡散することで、以下のような『技術で繋がる輪』ができたら、最高に嬉しいと考えています。
- 本記事をシェア
- 多くの人が『AIアプリ開発』に興味をもつ
- 各自が『AIアプリ』に関するアイデアやソースコードを公開(シェア)
- 誰かの『AIアプリ』が新たなアイデアやソースコードの創出に繋がる
多くの人が『AIアプリ』の情報を公開することで、ひとりの人間では思いつかなかった素晴らしい『AIアプリ』が誕生するかもしれません。
名も知らない人同士が『技術で繋がり』イノベーションが起こるかもしれません。
想像するだけでワクワクしますね!もし『AIアプリ』を作ったら、是非”はやぶさ”@Cpp_Learningに自慢してくださいね。
皆様が楽しく『AIアプリ開発』を実践してくれたら、最高に嬉しいです。
(完)