編集履歴一覧に戻る
katiのアイコン画像

kati が 2022年09月11日16時17分27秒 に編集

初版

タイトルの変更

+

だれがとったの

タグの変更

+

spresense

+

Edge-Impulse

+

Arduino-IDE

+

カメラボード

メイン画像の変更

メイン画像が設定されました

記事種類の変更

+

製作品

ライセンスの変更

+

(MIT) The MIT License

本文の変更

+

# デモ動画 @[youtube](https://www.youtube.com/watch?v=7p-0MpNwS58) # 部品 使用するハードウェアを次に示します。 | 部品名 | 個数| |:---:|:---| | Spresenseメインボード | 1 | | Spresense拡張ボード | 1 | | Spresenseカメラボード | 1 | | 液晶モジュール | 1 | 使用するソフトウェアを次に示します。 | ソフトウェア名 | |:---:|:---| | Arduino IDE | | Edge Impulse | # 設計図 「だれがとったの」は、誰かがテーブルに置いたお菓子を取ると、テキストにより取られたことを示す「Someone Take it!」とその時のキャプチャ画像を液晶モジュールに表示します。お菓子を元に戻すと、テキストにより戻されたことを示す「Found Snack!」とその時のキャプチャ画像を液晶モジュールに表示します。 「だれがとったの」は、次に示すハードウェアにより構成されます。 ![キャプションを入力できます](https://camo.elchika.com/ad21404197fe22f599433609b29f4b82b4ded3b3/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f32376166663263352d333862312d343363632d386666362d6634376364396530343432352f33396365636266652d313965362d343432302d623361382d643634323639373638393064/) 「だれがとったの」は、次のようにして開発しました。 - Spresenseメインボード上で動作するプログラムはArduino IDEを使って開発します。 - Spresenseカメラボードで取得した画像から、物体検出モデルを使って取られたお菓子を判断し、Spresense拡張ボードに接続された液晶モジュールに、取られたこととその時のキャプチャ画像を表示します。 - お菓子を検出する物体検出モデルはEdge Impulseを使って作成し、作成した物体検出モデルをArduino IDEで読み込める形式で出力し、作成したプログラムに埋め込みます。 ## 接続図 Spresenseメインボード を実装したSpresense拡張ボードに、SPIインタフェースを持つ液晶モジュールを次のように接続します。 ![キャプションを入力できます](https://camo.elchika.com/7c07b384275f92bda479b650759233325afe4b9c/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f32376166663263352d333862312d343363632d386666362d6634376364396530343432352f66323536653330362d323761382d346330342d623837302d666634326137613735316431/) ## ハードウェア 液晶モジュールの背面からSpresense拡張ボードにSPIインタフェースにより接続します。Spresenseメインボードに接続されたSpresenseカメラボードは、お菓子を撮影します。 ![キャプションを入力できます](https://camo.elchika.com/d5333b5a4a5f19cc61675443a27ddd14525988f8/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f32376166663263352d333862312d343363632d386666362d6634376364396530343432352f33616232616335392d383639362d343564332d626130352d383138303230643139613262/) # Edge ImpulseによるエッジAI Edge Impulseのアカウントを作成してSpresense を接続し、学習データを取得します。物体検出モデルを作成して、作成した物体検出モデルをArduino用としてDeployします。 ## 学習データ取得 Spresenseのカメラ画像から学習データを取得します。 ![キャプションを入力できます](https://camo.elchika.com/841c1e6ba3689d8002cc50e641b0a4b8f62481de/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f32376166663263352d333862312d343363632d386666362d6634376364396530343432352f62373764626530622d333762622d343239642d613639622d323334353065356265363263/) ## 特徴抽出 学習データの特徴を抽出して、その結果を「Feature explorer」に表示します。 ![キャプションを入力できます](https://camo.elchika.com/0d643da0413716951dc2ec41e3189c107dfda159/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f32376166663263352d333862312d343363632d386666362d6634376364396530343432352f34326563376535662d636330372d343832622d383262382d616539306439393962333837/) ## 物体検出モデル作成 物体検出モデルを作成します。終了すると次のように「Confusion matrix」により学習結果が表示されます。 ![キャプションを入力できます](https://camo.elchika.com/f389157fb2fe30ae23543c5a46c5da5cce31d068/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f32376166663263352d333862312d343363632d386666362d6634376364396530343432352f38663139613938312d626430302d343964662d383434312d333537613162346139303063/) # ソースコード Spresenseメインボード上で動作するプログラム「だれがとったの」はArduino IDEを使って開発します。プログラム「だれがとったの」は次の3つのコードに分割されます。 - Arduinoソースコード - カメラソースコード - 液晶ソースコード ## Arduinoソースコード Arduinoソースコードは、Spresenseカメラボードからの画像データをEdge Impulseにより作成した物体検出モデルに引き渡し、物体検出モデルからの物体検出結果を液晶モジュールへ引き渡します。 - インクルードファイル「ImpulseSpresense_inferencing.h」は、Edge Impulseによリ作成した物体検出モデルをArduino用としてDeployすることで作成されます。 - 関数「run_inference_to_make_predictions」で取得したSpresenseカメラボードからの画像データを、作成した物体検出モデルに入力します。物体検出モデルの物体検出結果を液晶モジュールに表示します。 - 関数「CAMERA_TakePicture_CamImage」でSpresenseカメラボードから画像データを取得します。 - 関数「run_classifier」で物体検出を行います。 - 関数「LCD_Display」でキャプチャした画像を液晶モジュールに表示します。 - 関数「LCD_Text」で、物体検出結果に従い、お菓子が取られたことを示す「Someone Take it!」と、お菓子が戻されたことを示す「Found Snack!」を表示します。 ```c:ImpulseSpresense.ino /* Includes ---------------------------------------------------------------- */ // tomo #include <tomosoft-project-1_inferencing.h> #include <ImpulseSpresense_inferencing.h> #include "spresense.h" /* Include board defines */ #include "CameraSpresense.h" #include "LCDSpresense.h" /* Prototype ---------------------------------------------------------------- */ void run_inference_to_make_predictions(); /* DEFINITIONS ---------------------------------------------------------------- */ #define BAUDRATE (115200) #define LED0_ON digitalWrite(LED0, HIGH) /* Macros for Switch on/off Board LEDs */ #define LED0_OFF digitalWrite(LED0, LOW) #define LED1_ON digitalWrite(LED1, HIGH) #define LED1_OFF digitalWrite(LED1, LOW) #define PICTURE_SIGNALLING_ON LED0_ON /* Switch on LED when take a photo */ #define PICTURE_SIGNALLING_OFF LED0_OFF /* Switch off LED when taken photo */ /* VARIABLES ---------------------------------------------------------------- */ char msg[128] ; static uint8_t *ei_camera_capture_out = NULL; const char *classes[] = {"Found", "Loss"}; void setup() { // put your setup code here, to run once: pinMode(LED0, OUTPUT); /* LED0 as output. Used to signal taking a photo */ pinMode(LED1, OUTPUT); /* LED0 as output. Used to signal taking a photo */ Serial.begin(BAUDRATE); /* Open serial communications and wait for port to open */ while (!Serial) { ; } /* Wait for serial port to connect. Needed for native USB port only */ LCD_Initialize(); CAMERA_Initialize(); } void loop() { // put your main code here, to run repeatedly: //sleep(10); run_inference_to_make_predictions(); } void run_inference_to_make_predictions() { // Summarize the Edge Impulse FOMO model inference settings (from model_metadata.h): ei_printf("\nInference settings:\n"); ei_printf("\tImage resolution: %dx%d\n", EI_CLASSIFIER_INPUT_WIDTH, EI_CLASSIFIER_INPUT_HEIGHT); ei_printf("\tFrame size: %d\n", EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE); ei_printf("\tNo. of classes: %d\n", sizeof(ei_classifier_inferencing_categories) / sizeof(ei_classifier_inferencing_categories[0])); // Take a picture with the given still picture settings. CamImage img = CAMERA_TakePicture_CamImage(); #ifdef SDCARD_SAVE SDCARD_SaveFile("TakePicture.YUV422", (const char *)img.getImgBuff(), img.getImgSize()); #endif if (CAMERA_IsImageAvailable(img)) { CAMERA_Stop(); // Resize the currently captured image depending on the given FOMO model. CamImage res_img; img.resizeImageByHW(res_img, EI_CLASSIFIER_INPUT_WIDTH, EI_CLASSIFIER_INPUT_HEIGHT); Serial.printf("Captured Image Resolution: %d / %d\nResized Image Resolution: %d / %d", img.getWidth(), img.getHeight(), res_img.getWidth(), res_img.getHeight()); #ifdef SDCARD_SAVE SDCARD_SaveFile("resizeImage.YUV422", (const char *)res_img.getImgBuff(), res_img.getImgSize()); #endif // Convert the resized (sample) image data format to GRAYSCALE so as to run inferences with the model. res_img.convertPixFormat(CAM_IMAGE_PIX_FMT_GRAY); Serial.print("\nResized Image Format: "); Serial.println((res_img.getPixFormat() == CAM_IMAGE_PIX_FMT_GRAY) ? "GRAYSCALE" : "ERROR"); #ifdef SDCARD_SAVE SDCARD_SaveFile("convert.GRAY", (const char *)res_img.getImgBuff(), res_img.getImgSize()); #endif // Run inference: ei::signal_t signal; ei_camera_capture_out = res_img.getImgBuff(); // Create a signal object from the resized and converted sample image. signal.total_length = EI_CLASSIFIER_INPUT_WIDTH * EI_CLASSIFIER_INPUT_HEIGHT; signal.get_data = &ei_camera_cutout_get_data; // Run the classifier: ei_impulse_result_t result = { 0 }; LED0_ON; EI_IMPULSE_ERROR _err = run_classifier(&signal, &result, false); LED0_OFF; if (_err != EI_IMPULSE_OK) { ei_printf("ERR: Failed to run classifier (%d)\n", _err); CAMERA_Start(); return; } // Print the inference timings on the serial monitor. ei_printf("Predictions (DSP: %d ms., Classification: %d ms., Anomaly: %d ms.): \n", result.timing.dsp, result.timing.classification, result.timing.anomaly); // Obtain the object detection results and bounding boxes for the detected labels (classes). bool bb_found = result.bounding_boxes[0].value > 0; for (size_t ix = 0; ix < EI_CLASSIFIER_OBJECT_DETECTION_COUNT; ix++) { auto bb = result.bounding_boxes[ix]; if (bb.value == 0) continue; LED1_ON; // Print the detected bounding box measurements on the serial monitor. ei_printf(" %s (", bb.label); ei_printf_float(bb.value); ei_printf(") [ x: %u, y: %u, width: %u, height: %u ]\n", bb.x, bb.y, bb.width, bb.height); int ei_classifier_w = EI_CLASSIFIER_INPUT_WIDTH; int ei_classifier_h = EI_CLASSIFIER_INPUT_HEIGHT; //LCD_Box(bb.label, bb.x, bb.y, bb.width, bb.height, ei_classifier_w, ei_classifier_h); LCD_Display( img); msg[0] = 0; if (bb.label == "nka") { sprintf(msg, "Someone Take it!"); LCD_Text( msg); break; } if (bb.label == "ka") { sprintf(msg, "Found Snack!"); LCD_Text( msg); break; } } if (!bb_found) { LED1_OFF; ei_printf(" No objects found!\n"); } // If the Edge Impulse FOMO model predicted a label (class) successfully: CAMERA_Start(); } else { Serial.println("Failed to take a picture!"); sleep(2); } } int ei_camera_cutout_get_data(size_t offset, size_t length, float *out_ptr) { // Convert the given image data (buffer) to the out_ptr format required by the Edge Impulse FOMO model. size_t bytes_left = length; size_t out_ptr_ix = 0; // read byte for byte while (bytes_left != 0) { // grab the value and convert to r/g/b uint8_t pixel = ei_camera_capture_out[offset]; uint8_t r, g, b; mono_to_rgb(pixel, &r, &g, &b); // then convert to out_ptr format float pixel_f = (r << 16) + (g << 8) + b; out_ptr[out_ptr_ix] = pixel_f; // and go to the next pixel out_ptr_ix++; offset++; bytes_left--; } return 0; } static inline void mono_to_rgb(uint8_t mono_data, uint8_t *r, uint8_t *g, uint8_t *b) { uint8_t v = mono_data; *r = *g = *b = v; } ``` ```c:spresense.h #ifndef _SPRESENSE_H_ #define _SPRESENSE_H_ // Define the required parameters to run an inference with the Edge Impulse model. #define SERIAL_PRINT_DEBUG /* Used to check through the Serial Terminal what's happening */ #define CAMERA_PRESENT /* Used to activate camera functions to take photos */ //#define SDCARD_SAVE /* Used to save the taken photos to the SD Card */ /******************************************************************************** -----> CAMERA ********************************************************************************/ //Includes #include <Camera.h> /* Include for Camera */ /******************************************************************************** -----> SDCARD ********************************************************************************/ #include <SDHCI.h> /* Include for SD card */ /******************************************************************************** -----> LCD ST7789 ********************************************************************************/ #include <Adafruit_GFX.h> // Core graphics library #include <Adafruit_ST7789.h> // Hardware-specific library for ST7789 #include <Fonts/FreeMonoBold12pt7b.h> #include <SPI.h> #define TFT_CS 10 #define TFT_RST 9 // Or set to -1 and connect to Arduino RESET pin #define TFT_DC 8 #endif /* _SPRESENSE_H_ */ ``` ## カメラソースコード カメラソースコードはSpresenseカメラボードを制御して撮影などを行います。 ```c:CameraSpresense.cpp /******************************************************************************** -----> INCLUDES ********************************************************************************/ #include "spresense.h" /* Include board defines */ #include "CameraSpresense.h" #include "LCDSpresense.h" /******************************************************************************** -----> DEFINITIONS ********************************************************************************/ #define CAMERA__WIDTH 320 #define CAMERA__HEIGHT 320 /******************************************************************************** -----> VARIABLES ********************************************************************************/ /******************************************************************************** -----> FUNCTIONS ********************************************************************************/ static void camera_CallBack(CamImage vF_Image); bool CAMERA_Initialize(void) { bool bRet = true; CamErr err = CAM_ERR_SUCCESS; #ifdef SERIAL_PRINT_DEBUG Serial.println("Prepare camera"); #endif /* SERIAL_PRINT_DEBUG */ err = theCamera.begin(); if (err != CAM_ERR_SUCCESS) { bRet = false; } // Start video stream and print errors, if any. CAMERA_Start(); // Set the Auto white balance parameter and print errors, if any. CAMERA_SetAutoWhiteBalanceMode(); // Set the still picture parameters and print errors, if any. CAMERA_SetStillPictureImageFormat(); return (bRet); } bool CAMERA_Start(void) { bool bRet = true; CamErr err = CAM_ERR_SUCCESS; #ifdef SERIAL_PRINT_DEBUG Serial.println("\nStart streaming"); #endif /* SERIAL_PRINT_DEBUG */ err = theCamera.startStreaming(true, camera_CallBack); // sleep(10); if (err != CAM_ERR_SUCCESS) { bRet = false; } return (bRet); } void CAMERA_Stop(void) { Serial.println("\nStop"); theCamera.startStreaming(false, camera_CallBack); // sleep(10); //theCamera.end(); } bool CAMERA_SetStillPictureImageFormat(void) { bool bRet = true; CamErr err = CAM_ERR_SUCCESS; /* Set parameters about still picture. In the following case, QUADVGA and JPEG. */ #ifdef SERIAL_PRINT_DEBUG Serial.println("Set still picture format"); #endif /* SERIAL_PRINT_DEBUG */ //sleep(10); err = theCamera.setStillPictureImageFormat( CAMERA__WIDTH, CAMERA__HEIGHT, CAM_IMAGE_PIX_FMT_YUV422); // 7); if (err != CAM_ERR_SUCCESS) { bRet = false; Serial.print("setStillPictureImageFormat: "); Serial.println(err); } return (bRet); } CamImage CAMERA_TakePicture_CamImage(void) { CamImage Image; #ifdef SERIAL_PRINT_DEBUG Serial.println("Picture taken"); #endif /* SERIAL_PRINT_DEBUG */ Image = theCamera.takePicture(); return (Image); } bool CAMERA_IsImageAvailable(CamImage vF_Image) { return (vF_Image.isAvailable()); } bool CAMERA_SetAutoWhiteBalanceMode(void) { bool bRet = true; CamErr err = CAM_ERR_SUCCESS; /* Auto white balance configuration */ #ifdef SERIAL_PRINT_DEBUG Serial.println("Set Auto white balance parameter"); #endif /* SERIAL_PRINT_DEBUG */ err = theCamera.setAutoWhiteBalanceMode(CAM_WHITE_BALANCE_AUTO); if (err != CAM_ERR_SUCCESS) { bRet = false; } return (bRet); } /**************************************************************************** Callback from Camera library when video frame is captured. ****************************************************************************/ void camera_CallBack(CamImage vF_Image) { /* Check the img instance is available or not. */ CamErr Error = CAM_ERR_SUCCESS; if (vF_Image.isAvailable()) { #ifdef SDCARD_SAVE SDCARD_SaveFile("CallBack.YUV422", (const char *)vF_Image.getImgBuff(), vF_Image.getImgSize()); #endif LCD_Display(vF_Image); } else { Serial.print("Failed to get video stream image\n"); } } ``` ```c:CameraSpresense.h #ifndef _CAMERA_SPRESENSE_H_ #define _CAMERA_SPRESENSE_H_ /******************************************************************************** -----> FUNCTIONS ********************************************************************************/ bool CAMERA_Initialize(void); CamImage CAMERA_TakePicture_CamImage(void); bool CAMERA_IsImageAvailable(CamImage vF_Image); bool CAMERA_SetAutoWhiteBalanceMode(void); bool CAMERA_SetStillPictureImageFormat(void); void CAMERA_Stop(void); bool CAMERA_Start(void); #endif /* _CAMERA_SPRESENSE_H_ */ ``` ## 液晶ソースコード 液晶ソースコードは液晶モジュールへの表示を行います。 ```c:LCDSpresense.cpp /******************************************************************************** -----> INCLUDES ********************************************************************************/ #include "spresense.h" /* Include board defines */ #include "LCDSpresense.h" /*********************************** -----> DEFINITIONS ********************************************************************************/ #define TFT_WIDTH 320 #define TFT_HEIGHT 240 /******************************************************************************** -----> VARIABLES ********************************************************************************/ extern const char *classes[]; uint32_t color_codes[] = {0xffff - ST77XX_GREEN, 0xffff - ST77XX_MAGENTA}; SDClass theSD; Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST); int picnum = 0; int predicted_class = -1; /******************************************************************************** -----> FUNCTIONS ********************************************************************************/ bool SDCARD_Initialize(void); void SDCARD_SaveFile(char *vF_Filename, const char *vF_Buffer, size_t vF_Size); File SDCARD_Open(char *vF_Filename); void invert(uint16_t *databuf, int count) { for (int i = 0; i < count; i ++) { //Serial.print(*( databuf + i)); Serial.print(" "); *( databuf + i) = 0xffff - *( databuf + i); //Serial.println(*( databuf + i)); } } void LCD_Initialize(void) { tft.init(TFT_HEIGHT, TFT_WIDTH); // Init ST7789 240x320 tft.setRotation(3); // 1 or 3 SDCARD_Initialize(); } void LCD_Display(CamImage img) { img.convertPixFormat(CAM_IMAGE_PIX_FMT_RGB565); invert((uint16_t *)img.getImgBuff(), img.getImgSize() / 2); tft.drawRGBBitmap(0, 0, (uint16_t *)img.getImgBuff(), TFT_WIDTH , TFT_HEIGHT); Serial.print("D"); #ifdef SDCARD_SAVE SDCARD_SaveFile("LCD_Disp.RGB565", (const char *)img.getImgBuff(), img.getImgSize()); #endif } void LCD_Box(const char *bb_label, int b_b_x, int b_b_y, int b_b_w, int b_b_h, int ei_classifier_w, int ei_classifier_h) { // Get the predicted label (class). if (bb_label == "ka") predicted_class = 0; if (bb_label == "nka") predicted_class = 1; Serial.print("Predicted Class: "); Serial.println(predicted_class); int box_scale_x = tft.width() / ei_classifier_w; b_b_x = b_b_x * box_scale_x; b_b_w = b_b_w * box_scale_x * 16; if ((b_b_w + b_b_x) > (tft.width() - 10)) b_b_w = tft.width() - b_b_x - 10; int box_scale_y = tft.height() / ei_classifier_h; b_b_y = b_b_y * box_scale_y; b_b_h = b_b_h * box_scale_y * 16; if ((b_b_h + b_b_y) > (tft.height() - 10)) b_b_h = tft.height() - b_b_y - 10; Serial.print(b_b_x); Serial.print(" "); Serial.print(b_b_y); Serial.print(" "); Serial.print( b_b_w); Serial.print(" "); Serial.println(b_b_h); // Display the predicted label (class) and the detected bounding box on the TFT screen. for (int i = 0; i < 2; i++) { tft.drawRect(b_b_x + i, b_b_y + i, b_b_w - (2 * i), b_b_h - (2 * i), color_codes[predicted_class]); } int c_x = 10, c_y = 10, r_x = 120, r_y = 40, r = 3, offset = 6; //tft.fillRoundRect(c_x, c_y, r_x, r_y, r, 0xffff - ST77XX_WHITE ); tft.fillRoundRect(c_x + offset, c_y + offset, r_x - (2 * offset), r_y - (2 * offset), r, color_codes[predicted_class]); tft.setTextColor(0xffff - ST77XX_WHITE ); tft.setFont( &FreeMonoBold12pt7b ); tft.setCursor(c_x + (2 * offset), c_y + (4 * offset)); tft.printf(classes[predicted_class]); } void LCD_Text( char *text) { tft.setTextColor(0xffff - ST77XX_WHITE ); tft.setFont( &FreeMonoBold12pt7b ); tft.setCursor(40, 210); tft.printf(text); } bool SDCARD_Initialize(void) { bool bRet = false; while (!theSD.begin()) { Serial.println("Insert SD card."); /* wait until SD card is mounted. */ } if (theSD.beginUsbMsc()) { Serial.println("USB MSC Failure!"); } bRet = true; return; } void SDCARD_SaveFile(char *vF_Filename, const char *vF_Buffer, size_t vF_Size) { if (picnum > 50) return; else if (picnum == 50) { picnum++; if (theSD.endUsbMsc()) { Serial.println("USB MSC Failure!"); } if (theSD.beginUsbMsc()) { Serial.println("USB MSC Failure!"); } } else { picnum++; Serial.print("S" ); // Serial.println(vF_Filename); theSD.remove(vF_Filename); File myFile = theSD.open(vF_Filename, FILE_WRITE); myFile.write(vF_Buffer, vF_Size); myFile.close(); } } File SDCARD_Open(char *vF_Filename) { File rootCertsFile = theSD.open(vF_Filename, FILE_READ); return (rootCertsFile); } ``` ```c:LCDSpresense.h #ifndef _LCD_SPRESENSE_H_ #define _LCD_SPRESENSE_H_ /******************************************************************************** -----> FUNCTIONS ********************************************************************************/ void LCD_Initialize(void); void LCD_Display(CamImage img); void LCD_Box(const char *bb_label, int b_b_x, int b_b_y, int b_b_w, int b_b_h, int ei_classifier_w, int ei_classifier_h); void LCD_Text( char *text); void SDCARD_SaveFile(char *vF_Filename, const char *vF_Buffer, size_t vF_Size); #endif /* _LCD_SPRESENSE_H_ */ ```