Fooping が 2022年09月25日00時00分23秒 に編集
コメント無し
本文の変更
## はじめに センシングプロセッサ「CXD5602」が搭載されたSONY製のボードSpresense。 メインボード+拡張ボードはArduino同等サイズでありながら、GPS内蔵、ハイレゾオーディオ機能搭載、マルチコア、カメラ操作、エッジAI、etc...と、なんでもできる、ハイスペックな開発ボードです。 読みは「スプレッセンス」。(エスプリセンスとか呼んでいたのは内緒) 自身のペインを解決するべくSpresenseを活用したエッジAIにチャレンジしてみます。 ## 解決したい課題 **勝手に我が家の駐車場を使用する人がいて困っている** ある日、外出先から帰宅すると見たことのない車が我が家の駐車場に停まっていました。 停める場所を失った我が家の車は、コインパーキングへの駐車を余儀なくされます。 仕方がないので、勝手に駐車していた車のワイパー部に、連絡くださいとメモを挟んでおきました。 しばらくしてから駐車場を見てみると、なんと、いつの間にかその車は姿を消しているではありませんか!! 連絡が来る事はなく、コインパーキングの料金支払いは当然自腹。 平日の日中は家族が車を使うので駐車場は空車のはずだが、 その時間に誰かが勝手に使っていることもあるようで、気持ち悪いことこの上ありません。 メモを置いても効果がないなら停めた瞬間に直接会話するしかありません。 ## 解決策 自宅窓から見える駐車場をSpresenseのカメラで撮影し、 画像認識・分類によって駐車車両の有無を認識、 車が駐車されたことを認識すると音と映像・動作で通知を行うシステムを作ることにします。 ++以下の条件を満たすようにソリューションを考えました。 ・我が家はマンションで、車が停まっているかどうかは、窓の外を見れば確認できる。 ・家の外にセンサーなどを設置することはできない。 ・車が停められたらすぐに気づきたい ・気づくためには、音か動きで知らせてくれれば良い++ ## 動作 今回製作したAI駐車監視システムのデモはこちらです。 @[twitter](https://twitter.com/FoopingTech/status/1572496664207953920?s=20&t=d7Zi_cHf_xi8chuNMwfM-g) ## 準備物 | 部品 | 個数 | URL | |:---:|:---:|:---| | Spresenseメインボード | 1 | https://amzn.asia/d/1imNUGE | | Spresense拡張ボード | 1 | https://amzn.asia/d/0KGa83W | | Spresenseカメラ | 1 | https://amzn.asia/d/3hKDS1p | | Mic&LCD KIT for SPRESENSE | 1 | https://amzn.asia/d/aZTbhyx | | SDカード 16GB | 1 | https://amzn.asia/d/3lLUnTS | | サーボモータ | 1 | https://amzn.asia/d/7diNJzp | | スピーカー | 1 | https://amzn.asia/d/1dl7tqS | | タブレット・スマートフォン用モニターアーム | 1 | https://amzn.asia/d/8UEhlgq | | ジャンパワイヤset | 1 | https://amzn.asia/d/iLkaoc4 | | タッピンねじ M2x5 | 8 | https://amzn.asia/d/aUIo3vh | | タッピンねじ M2x8 | 10 | ↑ | | なべ子ねじ M3x10 | 6 | https://amzn.asia/d/h9GY47S | | なべ子ねじ M3x14 | 1 | ↑ | | 六角ナットM3 | 1 | ↑ | ## LCDの組み付け LCDはカメラがどこに向いているか確認するために使用します。 今回は、差し込むだけ使えるMic&LCD KIT for SPRESENSEを使用します。 接続方法は[公式のGit](https://github.com/autolab-fujiwaratakayoshi/MIC-LCD_kit_for_SPRESENSE)に記載の通り、LCDキットの正面からみて最も左のピンがD04ピンになるように差し込みます。また、ジャンパーピンを3.3Vにセットします。これで画面が表示されるようになります。 @[twitter](https://twitter.com/FoopingTech/status/1564661621716180992?s=20&t=cLP3lYKnwu2ziho-7BmbDg) ++Spresenseメインボードと拡張ボードの接続に関しては[こちら](https://developer.sony.com/develop/spresense/docs/introduction_ja.html#_spresenseメインボードとspresense拡張ボードの接続方法)を参照ください。嵌合が不完全でSDカードが読み込めないなどの現象が発生するので、しっかりと組み付けたことを確認します。++ ## 撮影用駐車場スタジオの作成 今回は入力画像に基づき駐車場に車が停車しているか否かを分類するCNN(Convolution Neural Network)を構築します。 CNNを構築するためには分類するカテゴリ毎に学習に用いる画像を準備する必要があります。 画像の集め方はWeb上の画像をスクレイピングしても良いですし、外へ出て車や駐車場の写真を撮影して集めても良いです。 今回は自分で撮影した画像を用い、かつ、様々な水準の画像をできるだけ少ない時間で集めるために、 車のおもちゃ(トミカ)を撮影した画像を用いて学習を試みます。 この方法であれば室内で撮影を完結できます。 無地のグレーなプレートに白いテープを張り付けて簡易なトミカ用駐車場を作成します。 今回はレゴの道路用のプレートを裏向きで使用しました。 裏向きにする理由は表には不要な白線や横断歩道がプリントされているためです。 @[twitter](https://twitter.com/FoopingTech/status/1570023566758006786?s=20&t=cLP3lYKnwu2ziho-7BmbDg) ++Spresense上で実行する都合、学習モデルはあまり大きくできないので、できるだけ小さくなるように考慮が必要です。 今回は撮影した画像は加工してモノクロ、28 × 28ピクセルに縮小します。 小さく荒い画像へ変換されるため、自作の撮影用駐車場スタジオで学習用の画像を集めたとしても結果的に実際の車両と駐車場を区別することができます。 ただし、実際の車両と駐車場の画像で学習したほうが認識精度は向上すると考えられます。++ ## 学習用画像の撮影 学習用画像の準備にはスマートフォンなどの手持ちのカメラで撮影した画像を使って準備しても良いのですが、 今回はSpresenseのカメラを使って撮影します。 ++カメラをお持ちではないお子様の自由研究などにも活用いただけるかと思います。++ 以下のカメラ撮影ソフトをArduinoIDEで書き込みます。 ArduinoIDEとはオープンソースハードウェアArduino用の開発環境です。 SpresenseではArduinoIDEがサポートされており、今回はこれを使います。 ArduinoIDEのインストールについては[こちら](https://developer.sony.com/develop/spresense/docs/arduino_set_up_ja.html)をご参照ください。 ```arduino:カメラ撮影ソフト #include <SDHCI.h> #include <stdio.h> /* for sprintf */ #include <Camera.h> #define BAUDRATE (115200) #define TOTAL_PICTURE_COUNT (1) SDClass theSD; int take_picture_count = 0; int value=0; bool CamReady=true; bool CamDone=false; const int DIN_PIN = 7; //TFT設定 #include <SPI.h> #include "Adafruit_ILI9341.h" /* LCD Settings */ #define TFT_RST 8 #define TFT_DC 9 #define TFT_CS 10 Adafruit_ILI9341 tft = Adafruit_ILI9341(&SPI, TFT_DC, TFT_CS, TFT_RST); void putStringOnLcd(String str, int color) { int len = str.length(); tft.fillRect(0,224, 320, 240, ILI9341_BLACK); tft.setTextSize(2); int sx = 160 - len/2*12; if (sx < 0) sx = 0; tft.setCursor(sx, 225); tft.setTextColor(color); tft.println(str); } /** * Print error message */ void printError(enum CamErr err) { Serial.print("Error: "); switch (err) { case CAM_ERR_NO_DEVICE: Serial.println("No Device"); break; case CAM_ERR_ILLEGAL_DEVERR: Serial.println("Illegal device error"); break; case CAM_ERR_ALREADY_INITIALIZED: Serial.println("Already initialized"); break; case CAM_ERR_NOT_INITIALIZED: Serial.println("Not initialized"); break; case CAM_ERR_NOT_STILL_INITIALIZED: Serial.println("Still picture not initialized"); break; case CAM_ERR_CANT_CREATE_THREAD: Serial.println("Failed to create thread"); break; case CAM_ERR_INVALID_PARAM: Serial.println("Invalid parameter"); break; case CAM_ERR_NO_MEMORY: Serial.println("No memory"); break; case CAM_ERR_USR_INUSED: Serial.println("Buffer already in use"); break; case CAM_ERR_NOT_PERMITTED: Serial.println("Operation not permitted"); break; default: break; } } /** * Callback from Camera library when video frame is captured. */ void CamCB(CamImage img) { /* Check the img instance is available or not. */ if (img.isAvailable()) { /* If you want RGB565 data, convert image data format to RGB565 */ img.convertPixFormat(CAM_IMAGE_PIX_FMT_RGB565); Serial.print("Image data size = "); Serial.print(img.getImgSize(), DEC); Serial.print(" , "); Serial.print("buff addr = "); Serial.print((unsigned long)img.getImgBuff(), HEX); Serial.println(""); uint16_t* imgBuf = (uint16_t*)img.getImgBuff(); String gStrResult = "Ready"; if(CamDone){ gStrResult = "PIC=OK"; CamDone=false; } //TFT描画 tft.drawRGBBitmap(0, 0, (uint16_t *)img.getImgBuff(), 320, 224); putStringOnLcd(gStrResult, ILI9341_YELLOW); } else { Serial.println("Failed to get video stream image"); } } /** * @brief Initialize camera */ void setup() { //SW用 pinMode( DIN_PIN, INPUT_PULLUP ); //TFT tft.begin(); tft.setRotation(1); CamErr err; /* Open serial communications and wait for port to open */ Serial.begin(BAUDRATE); while (!Serial) { ; /* wait for serial port to connect. Needed for native USB port only */ } /* Initialize SD */ while (!theSD.begin()) { /* wait until SD card is mounted. */ Serial.println("Insert SD card."); } /* begin() without parameters means that * number of buffers = 1, 30FPS, QVGA, YUV 4:2:2 format */ Serial.println("Prepare camera"); err = theCamera.begin(); if (err != CAM_ERR_SUCCESS) { printError(err); } /* Start video stream. * If received video stream data from camera device, * camera library call CamCB. */ Serial.println("Start streaming"); err = theCamera.startStreaming(true, CamCB); if (err != CAM_ERR_SUCCESS) { printError(err); } /* Auto white balance configuration */ Serial.println("Set Auto white balance parameter"); err = theCamera.setAutoWhiteBalanceMode(CAM_WHITE_BALANCE_DAYLIGHT); if (err != CAM_ERR_SUCCESS) { printError(err); } Serial.println("Set still picture format"); err = theCamera.setStillPictureImageFormat( CAM_IMGSIZE_QUADVGA_H, CAM_IMGSIZE_QUADVGA_V, CAM_IMAGE_PIX_FMT_JPG); if (err != CAM_ERR_SUCCESS) { printError(err); } theCamera.startStreaming(true, CamCB); } void CheckSW(){ value = digitalRead( DIN_PIN ); Serial.println( value ); if(value == 0){ CamReady=true; CamDone=true; } } void loop() { sleep(0.5); CheckSW(); if(CamReady) { Serial.println("call takePicture()"); CamImage img = theCamera.takePicture(); if (img.isAvailable()) { /* Create file name */ char filename[16] = {0}; sprintf(filename, "PICT_%03d.JPG", take_picture_count); Serial.print("Save taken picture as "); Serial.print(filename); Serial.println(""); theSD.remove(filename); File myFile = theSD.open(filename, FILE_WRITE); myFile.write(img.getImgBuff(), img.getImgSize()); myFile.close(); take_picture_count++; } else { Serial.println("Failed to take picture"); } CamReady=false; } } ``` 書き込みが終わったら、D07ピンに接続されるボタン(SpresenseにLCDキットを差し込んだ状態で一番下のボタン)を押すとカメラで撮影し、画像をSDカードに保存します。 学習用の画像は様々な角度から撮影します。 ++私が今回検出したいのは、家から見下ろしたときの車両の有無ですので、 上から見下ろすアングルの画像や正面から撮影したもの、右斜め・左斜め、隣に車両がある・ないものなど、 いくつも水準を振って撮影しました。++ ## アノテーションとデータセットの作成 学習用画像の撮影を終えたら、学習に必要な部位のみの画像となるようにを切り出します。Windows10に付属のフォトアプリに画像を切り抜く(トリミング)機能があるため、これを使用します。 学習する際に、正方形の画像を入力することから、切り抜く画像はあらかじめ正方形とします。 ![画像をトリミング](https://camo.elchika.com/820eb3d2bd35c937a851b1dffb6b0043740a41b2/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f34306535643335612d306636632d343334642d393230302d6562623231383562613066622f65393534663433342d636139642d343662352d393461362d336531303732393661333139/) 駐車中の車両画像100枚、空車の駐車場画像100枚を目安に画像を準備し、roadとvehicleというフォルダ作成し、それぞれの画像を入れます。 ![キャプションを入力できます](https://camo.elchika.com/098a4b358d2df9d2b940ac143b75ea86bcce78a4/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f34306535643335612d306636632d343334642d393230302d6562623231383562613066622f39353236616430612d393131382d343664632d383536372d643565303466333765316565/) ![キャプションを入力できます](https://camo.elchika.com/fa3722cbd4012cc6a3a8a4aa8bc7533222932deb/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f34306535643335612d306636632d343334642d393230302d6562623231383562613066622f36366163383363392d323365382d343661352d393664632d623164356438363866383861/) ![キャプションを入力できます](https://camo.elchika.com/559e25d27bd9057f5035567a4fe73bd2cef785e8/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f34306535643335612d306636632d343334642d393230302d6562623231383562613066622f63353565643231312d626665352d343638662d396338642d396462646539616137323736/) ## Neural Network Consoleのインストール SonyのNeural Network Console Windowsアプリ(以降、NNC)をインストールします。 [こちら](https://dl.sony.com/ja/app/)からダウンロードのうえインストールしてください。 ## NNCで学習の準備 次に、フォルダへ分けた画像をNNCへ読み込みます。 NNCを起動したらデータセット > +データセットを作成> Image Classificationをクリックします。 ![キャプションを入力できます](https://camo.elchika.com/99c8b8f8ec752456af247d6bdf6f27a285278886/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f34306535643335612d306636632d343334642d393230302d6562623231383562613066622f38643265643333662d653132302d346363362d613862332d383331366630366535663133/) すると以下のウィンドウが開きますので、項目一つずつ入力します。 ソースディレクトリに先ほど学習用データをフォルダ分けしたフォルダ(ディレクトリ)を指定します。 出力ディレクトリにはデータセットを保存するフォルダ(ディレクトリ)を指定します。今回はわかりやすいデスクトップを指定しました。 変形方法はトリミング、出力カラーチャンネルは1(モノクロ)を指定します。 出力幅、高さはそれぞれ28とします。 これで、学習用画像をモノクロ、28x28pixelに変換する準備が整いました。 最後に出力ファイルの割合を80と20にして適用をクリックします。 ![キャプションを入力できます](https://camo.elchika.com/f50cd27059ef8b7f3ffce97fdab4b3da9280591b/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f34306535643335612d306636632d343334642d393230302d6562623231383562613066622f34363936336432322d623563352d346464622d626333312d353964663933303837326633/) ++全画像の80%を学習用データセット、残りの20%を評価用データセットとして使用します。++ 次にCNN(Convolution Neural Network)を作成します。 プロジェクト>新しいプロジェクトを開く で新しいプロジェクトを作成し、左側メニューから関数をドラッグ&ドロップして以下のように並べます。 ![今回構築するCNN](https://camo.elchika.com/c6b3f0e50e70ebe24c9235b4b44d6159c4ef579e/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f34306535643335612d306636632d343334642d393230302d6562623231383562613066622f31366430393931632d346130362d346331392d393034372d343362613764653964363038/) 続いて各ブロックのレイヤプロパティを設定します。 ブロックをクリックすると左にレイヤプロパティが表示されますので1つずつ設定します。 ImageAugmentation - MinScale:0.7 - MaxScale1.3 - Angle:0.3 - Distortion:0.3 - Noise:0.03 RandomShift - Shift:2,2 Convolution - OutMaps: 10 - kernelShape:7,7 - BorderMode : valid MaxPooling - Shape:4,4 Affine - OutShape:2 ++各ブロックで実行する処理の概要を下図に示します。 詳しい内容は[こちら](https://www.youtube.com/watch?v=-9ESIYqzVrw)をご覧ください。 ![学習モデル処理概要](https://camo.elchika.com/4ca6d108e63a462c0adb70b561ab41cd7479f7cc/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f34306535643335612d306636632d343334642d393230302d6562623231383562613066622f38663661336365352d626338302d346538652d386537342d656639396338373732646231/)++ ++何層にも及ぶ深いCNNを構築するとモデルが重たくなりSpresenseで実行実行できない場合があるようです。 @[twitter](https://twitter.com/FoopingTech/status/1570978743891660806?s=20&t=cLP3lYKnwu2ziho-7BmbDg)++ 次にCNNに入力するデータを選択します。 上部のデータセット>Training>データセットを開く(四角に右斜め上矢印の記載されたアイコン)をクリック、先ほどNNCに取り込んだTrainデータを選択します。 今度はValidation>データセットを開くをクリック、testデータを選択します。 ![キャプションを入力できます](https://camo.elchika.com/503028d598ff8cb339b363edffc61ae0dfb5f975/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f34306535643335612d306636632d343334642d393230302d6562623231383562613066622f35666634396133392d633738642d343637312d623435372d393934613430316162666537/) 最後に上部のコンフィグをクリックしバッチサイズを16に変更します。 ![キャプションを入力できます](https://camo.elchika.com/8d362797bc1d9ed1974f49d3780b51e957f14fa1/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f34306535643335612d306636632d343334642d393230302d6562623231383562613066622f32366335323232382d303561352d346165312d623665382d353961636366616234346434/) これで学習する準備が整いました。 ## NNCで学習 編集>実行(右上の青いボタン)で学習を実行します。 ![キャプションを入力できます](https://camo.elchika.com/33e80730717ba3ed15b5c4703b8ed517ffac2990/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f34306535643335612d306636632d343334642d393230302d6562623231383562613066622f63353165376437652d616137382d346266382d383964382d653634636539623636633836/) 学習が始まると学習曲線が描かれ学習の様子を確認できます。 ![キャプションを入力できます](https://camo.elchika.com/416631b00ba2b662fc1a17a7114ddd31d30f8a1c/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f34306535643335612d306636632d343334642d393230302d6562623231383562613066622f62356264363461342d633664302d343635342d626138322d366637383031323365653436/) 学習が完了したら評価を実行(右上の青ボタン)します。 すると学習済のモデルでtest用のデータセットを使った評価が実行されます。 評価が完了したら混同行列のラジオボタンをクリックし、Accuracyを確認します。 今回は1(=100%)と表示されており、test用の画像では全問正解したことを確認できました。 ![キャプションを入力できます](https://camo.elchika.com/d45528a6ccc6e9f1cd30dc407fb8b3ea3375603e/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f34306535643335612d306636632d343334642d393230302d6562623231383562613066622f32363931396462322d363339382d346439332d396239622d626532333561666561623363/) ++Accuracyの値が低い場合はCNNの設定や学習用のデータ数不足、カテゴリの分類ミスなどが考えられますので見直ししてみてください。++ 評価結果が問題なければ、学習済モデルをエクスポートします。 アクション>エクスポート>NNB(NNabla C Runtime file format)を選択します。 するとmodel.nnbが出力されます。 ![キャプションを入力できます](https://camo.elchika.com/93b3a37c383cf5b146bcc7e4064fddcf5c0a1c00/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f34306535643335612d306636632d343334642d393230302d6562623231383562613066622f33633963633830382d343632642d346433662d386539372d353035646137383838626231/) 出来上がったmodel.nnbをSDカードのルートフォルダに書き込み、Spresenseへ差し込みます。 @[twitter](https://twitter.com/FoopingTech/status/1571027660972920833?s=20&t=cLP3lYKnwu2ziho-7BmbDg) ++実車両でテストしたところおもちゃで学習した割には高精度に車両の有無を検出することができました。++ @[twitter](https://twitter.com/FoopingTech/status/1571030707237167111?s=20&t=cLP3lYKnwu2ziho-7BmbDg) ## スピーカーから音を出す準備 次にSpresenseから音を出す設定を行います。 ### DSPファイルインストール まず、[チュートリアル](https://developer.sony.com/develop/spresense/docs/arduino_tutorials_ja.html#_mp3_player)に従い、DSPファイルをインストールします。 DSPファイルをダウンロードしてSDカードへ書き込み読み込ませる方法と、DSPインストーラーを使用する方法がありますが、今回はDSPインストーラーを使用してインストールします。 Arduino IDE上から、ファイル>スケッチ例>Spresense用のスケッチ例 Audio > dsp_installer >mp3_dec_installerを選択し、Spresenseへ書き込みを実行します。 シリアルモニタを起動しボーレート115200bpsを選択するとメッセージが表示されインストール先を問われます。 今回はSPI-Flashを選択しますので「2」を入力して送信します。(詳細は[こちら](https://developer.sony.com/develop/spresense/docs/arduino_tutorials_ja.html#_dspインストーラー用いてインストールする方法)) ### MP3ファイルの準備 次にMP3ファイルを2つ準備しsound.mp3、sound2.mp3とファイル名に設定しSDカードのルートフォルダに書き込みます。 ++[フリーの音源サイト](https://dova-s.jp)が便利です。 いつも活用させていただいています。++ ここで、2つのMP3ファイルは同じサンプリングレートに設定する必要があります。 ++今回は[Audacity](https://www.audacityteam.org)というソフトでサンプリングレートの確認と変更をしました。 サンプリングレート(サンプリング周波数)を確認し44100(下図左下参照)に変更し保存します。 ![Audacityの操作画面](https://camo.elchika.com/a7aa7e1fdf922da6d3270b46ec342a6398500885/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f34306535643335612d306636632d343334642d393230302d6562623231383562613066622f36363035383264392d373564392d343032372d393930342d613361333531653264313033/)++ @[twitter](https://twitter.com/FoopingTech/status/1571517224946651138?s=20&t=4_J-ifcB78YohqRBiun36g) Spresenseの拡張ボードに実装されているオーディオジャックにスピーカーを接続します。 ++スピーカーは100円ショップ(セリア)で購入したものを使用しました。 アンプは内蔵していないものですがSpresenseの音量を最大にすれば、十分聞こえます。 ただしサイズが大きく、ケースにマウントしづらいため、プラグを残してネジ穴のある別のスピーカーへ付け替えました。++ @[twitter](https://twitter.com/FoopingTech/status/1571535987901345793?s=20&t=d7Zi_cHf_xi8chuNMwfM-g) ## サーボモータを回す 視認性を向上させるためにサーボモータで動きをつけます。 下図のようにサーボモータを接続します。 ![キャプションを入力できます](https://camo.elchika.com/3e0be4c1cc9ffd0e7ce73cff44ddfcf0969988f1/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f34306535643335612d306636632d343334642d393230302d6562623231383562613066622f64353433356235332d666462332d346264352d396436372d353865396461393430623739/) @[twitter](https://twitter.com/FoopingTech/status/1571156478945533952?s=20&t=4_J-ifcB78YohqRBiun36g) ++サーボモータの動作は[こちら](https://burariweb.info/electronic-work/arduino-learning/aruduino-servomotor-control.html)を参照しました。++ ++今回使用するサーボモータは5V保証のものですが、3.3Vでも実力動作します(n=2で確認) 動作保証が必要なアプリケーションで応用して使う場合は3.3V保証されたモータを使用するか、電圧をレベルシフトして5V系で使用することを検討してください。++ ## ソフトの書き込み 以下のソフトをSpresenseへ書き込みます。 LCDに表示された赤枠内に監視したい駐車場が映る様にセットして監視を開始します。 ```arduino:駐車場監視ソフト #include <Camera.h> #include <SPI.h> #include <EEPROM.h> #include <DNNRT.h> #include "Adafruit_ILI9341.h" #include <Servo.h> Servo myservo; // Servoオブジェクトの宣言 const int SV_PIN = 0; // サーボpin setting int before_value=0; bool ChangeModeFlag=false; #include <SDHCI.h> SDClass theSD; /* Audio Setting*/ #include <Audio.h> AudioClass *theAudio; bool ErrEnd = false; static void audio_attention_cb(const ErrorAttentionParam *atprm) { puts("Attention!"); if (atprm->error_code >= AS_ATTENTION_CODE_WARNING) { ErrEnd = true; } } /* LCD Settings */ #define TFT_RST 8 #define TFT_DC 9 #define TFT_CS 10 #define DNN_IMG_W 28 #define DNN_IMG_H 28 #define CAM_IMG_W 320 #define CAM_IMG_H 240 #define CAM_CLIP_X 104 #define CAM_CLIP_Y 64 #define CAM_CLIP_W 112 //DNN_IMGのn倍であること(clipAndResizeImageByHWの制約) #define CAM_CLIP_H 112 //DNN_IMGのn倍であること(clipAndResizeImageByHWの制約) #define LINE_THICKNESS 5 Adafruit_ILI9341 tft = Adafruit_ILI9341(&SPI, TFT_DC, TFT_CS, TFT_RST); uint8_t buf[DNN_IMG_W*DNN_IMG_H]; DNNRT dnnrt; DNNVariable input(DNN_IMG_W*DNN_IMG_H); //static uint8_t const label[2]= {0,1}; static String const label[2]= {"EMPTY", "VEHICLE"}; void putStringOnLcd(String str, int color) { int len = str.length(); tft.fillRect(0,224, 320, 240, ILI9341_BLACK); tft.setTextSize(2); int sx = 160 - len/2*12; if (sx < 0) sx = 0; tft.setCursor(sx, 225); tft.setTextColor(color); tft.println(str); } void drawBox(uint16_t* imgBuf) { /* Draw target line */ for (int x = CAM_CLIP_X; x < CAM_CLIP_X+CAM_CLIP_W; ++x) { for (int n = 0; n < LINE_THICKNESS; ++n) { *(imgBuf + CAM_IMG_W*(CAM_CLIP_Y+n) + x) = ILI9341_RED; *(imgBuf + CAM_IMG_W*(CAM_CLIP_Y+CAM_CLIP_H-1-n) + x) = ILI9341_RED; } } for (int y = CAM_CLIP_Y; y < CAM_CLIP_Y+CAM_CLIP_H; ++y) { for (int n = 0; n < LINE_THICKNESS; ++n) { *(imgBuf + CAM_IMG_W*y + CAM_CLIP_X+n) = ILI9341_RED; *(imgBuf + CAM_IMG_W*y + CAM_CLIP_X + CAM_CLIP_W-1-n) = ILI9341_RED; } } } void PlaySound(int i){ File myFile; /* Open file placed on SD card */ if(i==0)myFile = theSD.open("sound.mp3"); else if(i==1)myFile = theSD.open("sound2.mp3"); else exit(1); /* Verify file open */ if (!myFile) { printf("File open error\n"); exit(1); } printf("Open! 0x%08lx\n", (uint32_t)myFile); /* Send first frames to be decoded */ err_t err = theAudio->writeFrames(AudioClass::Player0, myFile); if ((err != AUDIOLIB_ECODE_OK) && (err != AUDIOLIB_ECODE_FILEEND)) { printf("File Read Error! =%d\n",err); myFile.close(); exit(1); } puts("Play!"); /* Main volume set -1020 to 120 */ theAudio->setVolume(120); theAudio->startPlayer(AudioClass::Player0); theAudio->stopPlayer(AudioClass::Player0,AS_STOPPLAYER_ESEND); myFile.close(); usleep(40000); } void CamCB(CamImage img) { if (!img.isAvailable()) { Serial.println("Image is not available. Try again"); return; } CamImage small; CamErr err = img.clipAndResizeImageByHW(small , CAM_CLIP_X, CAM_CLIP_Y , CAM_CLIP_X + CAM_CLIP_W -1 , CAM_CLIP_Y + CAM_CLIP_H -1 , DNN_IMG_W, DNN_IMG_H); if (!small.isAvailable()){ putStringOnLcd("Clip and Reize Error:" + String(err), ILI9341_RED); return; } small.convertPixFormat(CAM_IMAGE_PIX_FMT_RGB565); uint16_t* tmp = (uint16_t*)small.getImgBuff(); float *dnnbuf = input.data(); float f_max = 0.0; for (int n = 0; n < DNN_IMG_H*DNN_IMG_W; ++n) { dnnbuf[n] = (float)((tmp[n] & 0x07E0) >> 5); if (dnnbuf[n] > f_max) f_max = dnnbuf[n]; } /* normalization */ for (int n = 0; n < DNN_IMG_W*DNN_IMG_H; ++n) { dnnbuf[n] /= f_max; } String gStrResult = "?"; dnnrt.inputVariable(input, 0); dnnrt.forward(); DNNVariable output = dnnrt.outputVariable(0); img.convertPixFormat(CAM_IMAGE_PIX_FMT_RGB565); uint16_t* imgBuf = (uint16_t*)img.getImgBuff(); //Box描画 drawBox(imgBuf); //TFT描画 tft.drawRGBBitmap(0, 0, (uint16_t *)img.getImgBuff(), 320, 224); //text描画 int index = output.maxIndex(); gStrResult = String(label[index]) + String(":") + String(output[index]); Serial.println(gStrResult); putStringOnLcd(gStrResult, ILI9341_YELLOW); //モード変更有無をチェック if(index!=before_value){ ChangeModeFlag=true; } //モード変更時サーボ回転、音を鳴らす if(ChangeModeFlag){ if(index==0){myservo.write(0); PlaySound(1); } if(index==1){myservo.write(180); PlaySound(0); } } //更新 before_value=index; //flag初期化 ChangeModeFlag=false; } void setup() { Serial.begin(115200); tft.begin(); tft.setRotation(1); myservo.attach(SV_PIN, 500, 2400); while (!theSD.begin()) { putStringOnLcd("Insert SD card", ILI9341_RED); } File nnbfile = theSD.open("model.nnb"); int ret = dnnrt.begin(nnbfile); if (ret < 0) { Serial.println("dnnrt.begin failed" + String(ret)); putStringOnLcd("dnnrt.begin failed" + String(ret), ILI9341_RED); return; } // start audio system theAudio = AudioClass::getInstance(); theAudio->begin(audio_attention_cb); puts("initialization Audio Library"); /* Set clock mode to normal ハイレゾかノーマルを選択*/ theAudio->setRenderingClockMode(AS_CLKMODE_NORMAL); /*select LINE OUT */ theAudio->setPlayerMode(AS_SETPLAYER_OUTPUTDEVICE_SPHP, AS_SP_DRV_MODE_LINEOUT); /*init player DSPファイルはSPI-Flashを指定*/ err_t err = theAudio->initPlayer(AudioClass::Player0, AS_CODECTYPE_MP3, "/mnt/spif/BIN", AS_SAMPLINGRATE_AUTO, AS_CHANNEL_STEREO); /* Verify player initialize */ if (err != AUDIOLIB_ECODE_OK) { printf("Player0 initialize error\n"); exit(1); } //start Camera theCamera.begin(); theCamera.startStreaming(true, CamCB); } void loop() { } ```
## IoT化 次にIoT化を行います。 Spresenseにはインターネットに接続する機能がないため、M5StickCからWi-Fiを経由してIFTTTへリクエストを送信することで実現します。 車両を認識したらSpresenseのD1pinをLo→Hi出力し、NPNトランジスタを経由してM5StickCの入力ピンがHi→Loを認識、 これをトリガにIFTTTへリクエストを送信しスマートフォンへプッシュ通知を送信します。 接続図は以下の通りです。
![キャプションを入力できます](https://camo.elchika.com/3865d6b0b565111e85061e4a5281afd59dafd8bd/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f34306535643335612d306636632d343334642d393230302d6562623231383562613066622f64396534353339312d313730382d343265652d623632342d383966396338346337386362/)
## ケースの準備 窓の外にある車を常時監視するためにカメラを適切な位置にマウントする必要があります。 今回はスマートフォン用のモニタアームを使用して位置を固定することにしました。 Spresense+カメラ、スピーカー、サーボモータを収め、モニタアームにマウントできるサイズのケースを製作します。 ![スマートフォン用モニタアームに取り付けたSpresense用ケース](https://camo.elchika.com/1cf3469b8eb26ce2e5985879f8b1b9a9fbe0464d/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f34306535643335612d306636632d343334642d393230302d6562623231383562613066622f37336539666365632d656161622d343439332d626363662d613235616331656637386232/) ケースは高機能CAD Fusion360で設計しました。 Spresenseの[ハードウェア設計資料](https://developer.sony.com/develop/spresense/docs/hw_design_ja.html)にDXFファイルなど必要な情報がありますので、Fusion360に読み込み、設計します。 @[twitter](https://twitter.com/FoopingTech/status/1571809889923760128?s=20&t=d7Zi_cHf_xi8chuNMwfM-g) Spresenseのカメラはフラットワイヤで接続されており自由度は大きくありません。 ワイヤにテンションをかけず、窓の外の車両を撮影するために、カメラは下部に配置し、角度を可変できるようにしました。 @[twitter](https://twitter.com/FoopingTech/status/1572439954508353536?s=20&t=d7Zi_cHf_xi8chuNMwfM-g) 3Dプリント用のSTLファイルを公開済みです。 必要な方は[こちら](https://www.thingiverse.com/thing:5529032)からダウンロードしてご使用ください。 ++3Dプリンタの精度によっては追加加工が必要な場合があります。(特にねじ穴部など)++ ケースの準備ができたら、組み付けして完成です! ## 所感 1週間ほど本システムを運用していて、**雨の日は誤認識しやすい**など、いくつか気になる点が出てきました。 雨の日は車両の天面の光の反射・照り返し方が変わってしまい、特徴が変わってしまったのだと考えられます。 今後、認識率の低い画像を取得した時には画像を保存して残しておき、再学習するようにしていけば徐々に精度は向上していくものと考えられます。とにかく素早く運用を開始してデータを集めるのがミソだということを改めて実感しました。 正しく認識できる様になっていく過程は、まるで赤ちゃんを育てているみたいですね。 また、今は室内から窓を通して外をモニタしているため暗くなると部屋が映り込んで認識できなくなります。 これはできるだけ窓に近づけ、映り込まない様にカバーを追加することで対策ができそうです。また、HDRカメラを使用することで対応できると考えられます。 現状でも、夜は自家用車が駐車場に帰ってきているのでそもそも監視する必要がなく運用上は問題ありません。 また、今回製作したシステムは学習データとソフトを入れ替えれば他のアプリケーションにも転用できると考えます。 - グーチョキパーの手の画像を学習させ音とモータのモーションで反応するじゃんけんゲーム - 人の顔を検出したら挨拶するシステム - モニタアームを使用せずサーボモータの回転でレールを走行し、カメラ自体を移動させながら監視する自律移動式監視システム 駐車場問題が落ち着いたら次のアプリケーション制作に取り掛かりたいものです。 ## おわりに Spresenseのドキュメントは日本語で記載されていて、しかもかなり充実しているのでサクサク開発することができました。 特にDNNの基礎知識・仕組み・考え方はSONY様が公開されているYoutubeの解説動画が非常にわかりやすく、充実していて、とても勉強になりました。 たくさんあって、どこから見ればいいかわからない!となってほぼ全部目を通したのですが、 その過程で目次の位置づけの動画とリンクがありましたので、ご興味あれば以下の動画から参照してみると良いかと思います。 @[youtube](https://youtu.be/llv3-hSx1MU)