前回の続き
今回の内容は以前私が投稿したEdge Impulse検証ーM5Stack製ESP32CAM PSRAMの使用の続きです。内容の把握のため以前の投稿を読んでいただくことをお勧めします。
簡単に前回を説明すると、
- Edge Impulseで物体検出モデルを作成しM5stack ESP32CAM-PSRAMで画像検査カメラを作ろう。
- そのためにまず初期設定をしよう。
- 初期設定でつまづいて結局できませんでした、、、、、、。
という感じでゴールどころかスタートすらしなかった状態でした。
しかし今回無事Edge Impulseで作成した物体検出モデルをM5stack ESP32CAM-PSRAMで動かすことができたので、それまでの軌跡を記録します。
Edge Impulseで物体検出モデルを作る
まず最初に、前回は初期設定の部分で躓いたことでブログが止まってしまいましたが、今回はこの初期設定を飛ばし、Edge ImpulseでArduino ideで書き込む物体検出モデルを作成し、それをM5stack ESP32CAM-PSRAMに書き込むこととしました。
1.Edge Impulseにサインイン
まずEdge ImpulseのHPに入ります。アカウントを持っていない人は作成してください。
サインインできたら自身のProfile画面に入り、そこでプロジェクト一覧が確認できます。この時すでに1つのプロジェクトがありますが、今回はこれを使用しません。なぜならこの初期プロジェクトでは画像検出が使用できないからです。
よって”Creat new project”でプロジェクトを新規作成します。
2.スマホを使用して写真撮影
プロジェクト画面に入りましたら、左側のツリーから”Devices”をクリックします。”Connect a new device”をクリックすると、通信する端末をスマホ、PC、開発ボードから選べます。ここではスマホを使用するのでQRコードを自分のスマホで読み取ります。
読み取ったQRコードのURLにアクセスするとEdge Impulseの画面に入ることができ、”Collecting images?”、”Collecting audio?”、”Collecting motion?”の3つを選べます。ここでは”Collecting images?”を選んでタップします。
“Give access to the camera”でスマホのカメラにアクセスします。すると写真を撮る画面に映りますので、対象物を画面に映した状態で”Capture”をタップすると写真が撮影されます。撮影された画像は後述するDatasetに自動でアップロードされます。
3.画像データラベル付け
さて、ここでパソコン作業に戻ります。
さきほど開いていたEdge impulseの画面から左側のツリーの”Data acquisition”をクリックすると下図のような画面に映ります。
“Dataset”の中に先ほどアップロードされた画像が追加されています。
アップロードされた画像に対し、これが『何なのか』を指定します。これが『ラベル付け』というものです。
アップロード初期の画像には何のラベルもつけられていません。この画像を左クリックで選択したのち、”・・・”を左クリックしてして”Edit labels”をクリックします。
カーソルに点線の十字が追従しますので、これをマウスだけが入るようにドラックして選択します。ラベル名の入力画面が出てくるのでここでは”mouse”と入力します。
上画像のように選択とラベル付けができたら”Save labels”をクリックしてラベル編集を終えます。
画像検出にはモデルとなる画像が多ければ多いほど精度がますので、なるべくあらゆる角度、照度を変えた画像を撮影しラベル付けを行いましょう。
1つのラベルに対し複数画像を取りましたら、一番映りのいい画像を選び”・・・”を左クリックしてして”Move to test set”をクリックします。
どれくらいの画像が必要かの目安は”TRAIN/TEST SPLIT”を参照しましょう。画像が足りない場合は!マークが出て、それをクリックするとどのラベルがどれくらい画像が足りていないかが表示されます。
ここでは”FLYPAN”と”KETTLE”の画像が十分ですが、”MOUSE”の画像が足りていないのでこちらを増やす必要があります。
4.Create impulseからマイコンへの書き込み準備まで
左側のツリーから”Create impulse”をクリックします。ここで詳細に設定変更ができるようですが今は初期設定で行きます。そのまま”Save Impulse”をクリックします。
次に左側のツリーから”Image”をクリックします。ここで”Parameters”の”Color depth”から”Grayscale”を選択し、”Save parameters”をクリックします。
自動的に”Generate features”画面に移ります。もし移らなかったら上部のタブから”Generate features”を選択できます。この画面で”Generate features”をクリックします。自動的にジョブが走りるので完了するまでお待ちください。完了したら”Feature generation output”の最終行に”Job completed (success)”の文字列が表示されます。
次に左側ツリーから”Object detection”をクリックします。ここでは物体検出モデルを生成するのに必要なパラメーターを設定します。ここで”Training settings”の”Number of training cycles”の数値を初期値が60となっているのを30とします。これはESP32で推奨されるパラメーターです。Learning rateは0.005としました。
“Choose a different model”をクリックして適用するモデルを選択します。ここでは『FOMO (Faster Objects, More Objects) MobileNet』が推奨と聞いたのでこれを選択しました。
ここまで完了したら”Save & train”をクリックします。自動的にジョブが走りるので完了するまでお待ちください。完了したら”Training output”の最終行に”Job completed (success)”の文字列が表示され、トレーニングパフォーマンスが算出されます。
上記の結果はあまりよくないです。mouseの画像が足りない可能性があります。とはいえここまででESP32CAMに書き込む直前までの準備は完了です。
左側ツリーの”Deployment”をクリックし、”Search deployment options”から”Arduino library”を選択、”Build”ボタンをクリックします。
Arduino libraryとプログラムが自動作成されたらzipファイルがダウンロードされます。
ここまででEdge impulse側での操作は完了です。
5.ESP32CAMへの書き込み
ここでの説明はArduino ideへesp32のボードマネージャーがインストールされている前提でお話しします。
Arduino ideを立ち上げます。ボードマネージャー画面でesp32と検索し、”esp32 by Espressif Systems”の項目でバージョンを『2.0.4』に変更しインストールします。すでにこれ以上のバージョンがインストールされている人もダウングレードしてください。なぜかというと公式HPではESP32のテストは2.0.4で確認されており、私も当初は最新バージョンで行ったらエラーが発生してできなかったからです。
次に”スケッチ”→”ライブラリをインクルード”→”.ZIP形式のライブラリをインストール”をクリックし、先ほどダウンロードされたzipファイルを開きます。
zipライブラリのインクルードが自動で行われ、完了すると”ファイル”→”スケッチ例”→”〇〇〇_inferencing”というスケッチ例が自動作成追加されています。ここで〇〇〇というのは先ほどEdge impulseでプロジェクトを作成したときにつけたプロジェクト名となっています。この中の”esp32″→”esp32_camera”を選んで開きます。
このプログラムがEdge impulseで作成したesp32camで画像検出を行うプログラムです。
しかしこのままではまだプログラムを書き込めません。なぜならM5stack ESP32CAM PSRAM用の設定がこのプログラムに無いからです。このプログラムを手修正してやる必要があります。
switch scienceのM5stack ESP32CAM PSRAMの販売ページにはcamera web serverというテストプログラムがおいています。ここから必要な箇所を拝借しました。
私が修正したプログラムをここに載せます。ただしこれはedge impulseで自動作成したプログラムに手修正を加えたものなので、このプログラムが万人で使用できるものではありません。あくまで手修正の考えを説明するために載せていることをご理解ください。
/* Edge Impulse Arduino examples
* Copyright (c) 2022 EdgeImpulse Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// These sketches are tested with 2.0.4 ESP32 Arduino Core
// https://github.com/espressif/arduino-esp32/releases/tag/2.0.4
/* Includes ---------------------------------------------------------------- */
#include <Object_detection_inferencing.h>
#include "edge-impulse-sdk/dsp/image/image.hpp"
#include "esp_camera.h"
// Select camera model - find more camera models in camera_pins.h file here
// https://github.com/espressif/arduino-esp32/blob/master/libraries/ESP32/examples/Camera/CameraWebServer/camera_pins.h
#define CAMERA_MODEL_M5STACK_V2_PSRAM
//#define CAMERA_MODEL_ESP_EYE // Has PSRAM
//#define CAMERA_MODEL_AI_THINKER // Has PSRAM
#if defined(CAMERA_MODEL_ESP_EYE)
#define PWDN_GPIO_NUM -1
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 4
#define SIOD_GPIO_NUM 18
#define SIOC_GPIO_NUM 23
#define Y9_GPIO_NUM 36
#define Y8_GPIO_NUM 37
#define Y7_GPIO_NUM 38
#define Y6_GPIO_NUM 39
#define Y5_GPIO_NUM 35
#define Y4_GPIO_NUM 14
#define Y3_GPIO_NUM 13
#define Y2_GPIO_NUM 34
#define VSYNC_GPIO_NUM 5
#define HREF_GPIO_NUM 27
#define PCLK_GPIO_NUM 25
#elif defined(CAMERA_MODEL_AI_THINKER)
#define PWDN_GPIO_NUM 32
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 0
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27
#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 21
#define Y4_GPIO_NUM 19
#define Y3_GPIO_NUM 18
#define Y2_GPIO_NUM 5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22
#elif defined(CAMERA_MODEL_M5STACK_V2_PSRAM)
// ESP32 M5STACK V2
#define PWDN_GPIO_NUM -1
#define RESET_GPIO_NUM 15
#define XCLK_GPIO_NUM 27
#define SIOD_GPIO_NUM 22
#define SIOC_GPIO_NUM 23
#define Y9_GPIO_NUM 19
#define Y8_GPIO_NUM 36
#define Y7_GPIO_NUM 18
#define Y6_GPIO_NUM 39
#define Y5_GPIO_NUM 5
#define Y4_GPIO_NUM 34
#define Y3_GPIO_NUM 35
#define Y2_GPIO_NUM 32
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 26
#define PCLK_GPIO_NUM 21
// M5 Stack status/illumination LED details unknown/unclear
// #define LED_PIN x // Status led
// #define LED_ON HIGH //
// #define LED_OFF LOW //
// #define LAMP_PIN x // LED FloodLamp.
#else
#error "Camera model not selected"
#endif
/* Constant defines -------------------------------------------------------- */
#define EI_CAMERA_RAW_FRAME_BUFFER_COLS 320
#define EI_CAMERA_RAW_FRAME_BUFFER_ROWS 240
#define EI_CAMERA_FRAME_BYTE_SIZE 3
/* Private variables ------------------------------------------------------- */
static bool debug_nn = false; // Set this to true to see e.g. features generated from the raw signal
static bool is_initialised = false;
uint8_t *snapshot_buf; //points to the output of the capture
static camera_config_t camera_config = {
.pin_pwdn = PWDN_GPIO_NUM,
.pin_reset = RESET_GPIO_NUM,
.pin_xclk = XCLK_GPIO_NUM,
.pin_sscb_sda = SIOD_GPIO_NUM,
.pin_sscb_scl = SIOC_GPIO_NUM,
.pin_d7 = Y9_GPIO_NUM,
.pin_d6 = Y8_GPIO_NUM,
.pin_d5 = Y7_GPIO_NUM,
.pin_d4 = Y6_GPIO_NUM,
.pin_d3 = Y5_GPIO_NUM,
.pin_d2 = Y4_GPIO_NUM,
.pin_d1 = Y3_GPIO_NUM,
.pin_d0 = Y2_GPIO_NUM,
.pin_vsync = VSYNC_GPIO_NUM,
.pin_href = HREF_GPIO_NUM,
.pin_pclk = PCLK_GPIO_NUM,
//XCLK 20MHz or 10MHz for OV2640 double FPS (Experimental)
.xclk_freq_hz = 20000000,
.ledc_timer = LEDC_TIMER_0,
.ledc_channel = LEDC_CHANNEL_0,
.pixel_format = PIXFORMAT_JPEG, //YUV422,GRAYSCALE,RGB565,JPEG
.frame_size = FRAMESIZE_QVGA, //QQVGA-UXGA Do not use sizes above QVGA when not JPEG
.jpeg_quality = 12, //0-63 lower number means higher quality
.fb_count = 1, //if more than one, i2s runs in continuous mode. Use only with JPEG
.fb_location = CAMERA_FB_IN_PSRAM,
.grab_mode = CAMERA_GRAB_WHEN_EMPTY,
};
/* Function definitions ------------------------------------------------------- */
bool ei_camera_init(void);
void ei_camera_deinit(void);
bool ei_camera_capture(uint32_t img_width, uint32_t img_height, uint8_t *out_buf) ;
/**
* @brief Arduino setup function
*/
void setup()
{
// put your setup code here, to run once:
Serial.begin(115200);
//comment out the below line to start inference immediately after upload
while (!Serial);
Serial.println("Edge Impulse Inferencing Demo");
if (ei_camera_init() == false) {
ei_printf("Failed to initialize Camera!\r\n");
}
else {
ei_printf("Camera initialized\r\n");
}
ei_printf("\nStarting continious inference in 2 seconds...\n");
ei_sleep(2000);
}
/**
* @brief Get data and run inferencing
*
* @param[in] debug Get debug info if true
*/
void loop()
{
// instead of wait_ms, we'll wait on the signal, this allows threads to cancel us...
if (ei_sleep(5) != EI_IMPULSE_OK) {
return;
}
snapshot_buf = (uint8_t*)malloc(EI_CAMERA_RAW_FRAME_BUFFER_COLS * EI_CAMERA_RAW_FRAME_BUFFER_ROWS * EI_CAMERA_FRAME_BYTE_SIZE);
// check if allocation was successful
if(snapshot_buf == nullptr) {
ei_printf("ERR: Failed to allocate snapshot buffer!\n");
return;
}
ei::signal_t signal;
signal.total_length = EI_CLASSIFIER_INPUT_WIDTH * EI_CLASSIFIER_INPUT_HEIGHT;
signal.get_data = &ei_camera_get_data;
if (ei_camera_capture((size_t)EI_CLASSIFIER_INPUT_WIDTH, (size_t)EI_CLASSIFIER_INPUT_HEIGHT, snapshot_buf) == false) {
ei_printf("Failed to capture image\r\n");
free(snapshot_buf);
return;
}
// Run the classifier
ei_impulse_result_t result = { 0 };
EI_IMPULSE_ERROR err = run_classifier(&signal, &result, debug_nn);
if (err != EI_IMPULSE_OK) {
ei_printf("ERR: Failed to run classifier (%d)\n", err);
return;
}
// print the predictions
ei_printf("Predictions (DSP: %d ms., Classification: %d ms., Anomaly: %d ms.): \n",
result.timing.dsp, result.timing.classification, result.timing.anomaly);
#if EI_CLASSIFIER_OBJECT_DETECTION == 1
ei_printf("Object detection bounding boxes:\r\n");
for (uint32_t i = 0; i < result.bounding_boxes_count; i++) {
ei_impulse_result_bounding_box_t bb = result.bounding_boxes[i];
if (bb.value == 0) {
continue;
}
ei_printf(" %s (%f) [ x: %u, y: %u, width: %u, height: %u ]\r\n",
bb.label,
bb.value,
bb.x,
bb.y,
bb.width,
bb.height);
}
// Print the prediction results (classification)
#else
ei_printf("Predictions:\r\n");
for (uint16_t i = 0; i < EI_CLASSIFIER_LABEL_COUNT; i++) {
ei_printf(" %s: ", ei_classifier_inferencing_categories[i]);
ei_printf("%.5f\r\n", result.classification[i].value);
}
#endif
// Print anomaly result (if it exists)
#if EI_CLASSIFIER_HAS_ANOMALY
ei_printf("Anomaly prediction: %.3f\r\n", result.anomaly);
#endif
#if EI_CLASSIFIER_HAS_VISUAL_ANOMALY
ei_printf("Visual anomalies:\r\n");
for (uint32_t i = 0; i < result.visual_ad_count; i++) {
ei_impulse_result_bounding_box_t bb = result.visual_ad_grid_cells[i];
if (bb.value == 0) {
continue;
}
ei_printf(" %s (%f) [ x: %u, y: %u, width: %u, height: %u ]\r\n",
bb.label,
bb.value,
bb.x,
bb.y,
bb.width,
bb.height);
}
#endif
free(snapshot_buf);
}
/**
* @brief Setup image sensor & start streaming
*
* @retval false if initialisation failed
*/
bool ei_camera_init(void) {
if (is_initialised) return true;
#if defined(CAMERA_MODEL_ESP_EYE)
pinMode(13, INPUT_PULLUP);
pinMode(14, INPUT_PULLUP);
#endif
//initialize the camera
esp_err_t err = esp_camera_init(&camera_config);
if (err != ESP_OK) {
Serial.printf("Camera init failed with error 0x%x\n", err);
return false;
}
sensor_t * s = esp_camera_sensor_get();
// initial sensors are flipped vertically and colors are a bit saturated
if (s->id.PID == OV3660_PID) {
s->set_vflip(s, 1); // flip it back
s->set_brightness(s, 1); // up the brightness just a bit
s->set_saturation(s, 0); // lower the saturation
}
#if defined(CAMERA_MODEL_M5STACK_WIDE)
s->set_vflip(s, 1);
s->set_hmirror(s, 1);
#elif defined(CAMERA_MODEL_ESP_EYE)
s->set_vflip(s, 1);
s->set_hmirror(s, 1);
s->set_awb_gain(s, 1);
#endif
is_initialised = true;
return true;
}
/**
* @brief Stop streaming of sensor data
*/
void ei_camera_deinit(void) {
//deinitialize the camera
esp_err_t err = esp_camera_deinit();
if (err != ESP_OK)
{
ei_printf("Camera deinit failed\n");
return;
}
is_initialised = false;
return;
}
/**
* @brief Capture, rescale and crop image
*
* @param[in] img_width width of output image
* @param[in] img_height height of output image
* @param[in] out_buf pointer to store output image, NULL may be used
* if ei_camera_frame_buffer is to be used for capture and resize/cropping.
*
* @retval false if not initialised, image captured, rescaled or cropped failed
*
*/
bool ei_camera_capture(uint32_t img_width, uint32_t img_height, uint8_t *out_buf) {
bool do_resize = false;
if (!is_initialised) {
ei_printf("ERR: Camera is not initialized\r\n");
return false;
}
camera_fb_t *fb = esp_camera_fb_get();
if (!fb) {
ei_printf("Camera capture failed\n");
return false;
}
bool converted = fmt2rgb888(fb->buf, fb->len, PIXFORMAT_JPEG, snapshot_buf);
esp_camera_fb_return(fb);
if(!converted){
ei_printf("Conversion failed\n");
return false;
}
if ((img_width != EI_CAMERA_RAW_FRAME_BUFFER_COLS)
|| (img_height != EI_CAMERA_RAW_FRAME_BUFFER_ROWS)) {
do_resize = true;
}
if (do_resize) {
ei::image::processing::crop_and_interpolate_rgb888(
out_buf,
EI_CAMERA_RAW_FRAME_BUFFER_COLS,
EI_CAMERA_RAW_FRAME_BUFFER_ROWS,
out_buf,
img_width,
img_height);
}
return true;
}
static int ei_camera_get_data(size_t offset, size_t length, float *out_ptr)
{
// we already have a RGB888 buffer, so recalculate offset into pixel index
size_t pixel_ix = offset * 3;
size_t pixels_left = length;
size_t out_ptr_ix = 0;
while (pixels_left != 0) {
// Swap BGR to RGB here
// due to https://github.com/espressif/esp32-camera/issues/379
out_ptr[out_ptr_ix] = (snapshot_buf[pixel_ix + 2] << 16) + (snapshot_buf[pixel_ix + 1] << 8) + snapshot_buf[pixel_ix];
// go to the next pixel
out_ptr_ix++;
pixel_ix+=3;
pixels_left--;
}
// and done!
return 0;
}
#if !defined(EI_CLASSIFIER_SENSOR) || EI_CLASSIFIER_SENSOR != EI_CLASSIFIER_SENSOR_CAMERA
#error "Invalid model for current sensor"
#endif
まず35行目に”#define CAMERA_MODEL_M5STACK_V2_PSRAM”を入れ、もともとあった
#define CAMERA_MODEL_M5STACK_V2_PSRAM
//#define CAMERA_MODEL_ESP_EYE // Has PSRAM
//#define CAMERA_MODEL_AI_THINKER // Has PSRAM
次に”#elif defined(CAMERA_MODEL_AI_THINKER)”のブロックの後である76行目と”#else”の間に”#elif defined(CAMERA_MODEL_M5STACK_V2_PSRAM)”のブロックを入れました。
#elif defined(CAMERA_MODEL_M5STACK_V2_PSRAM)
// ESP32 M5STACK V2
#define PWDN_GPIO_NUM -1
#define RESET_GPIO_NUM 15
#define XCLK_GPIO_NUM 27
#define SIOD_GPIO_NUM 22
#define SIOC_GPIO_NUM 23
#define Y9_GPIO_NUM 19
#define Y8_GPIO_NUM 36
#define Y7_GPIO_NUM 18
#define Y6_GPIO_NUM 39
#define Y5_GPIO_NUM 5
#define Y4_GPIO_NUM 34
#define Y3_GPIO_NUM 35
#define Y2_GPIO_NUM 32
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 26
#define PCLK_GPIO_NUM 21
// M5 Stack status/illumination LED details unknown/unclear
// #define LED_PIN x // Status led
// #define LED_ON HIGH //
// #define LED_OFF LOW //
// #define LAMP_PIN x // LED FloodLamp.
これら手修正のプログラムはcamera web serverのプログラムから拝借したものです。
これらの修正を行いボードを”ESP32 Wrover Module”を選択してプログラムの書き込みを行いました。
プログラム書き込み後、一度接続を解除し再度ESP32CAM PSRAMをパソコンに接続してシリアルモニタを表示されると、カメラが対象物をとらえたときの先ほどラベル付けしたときの名称がシリアルモニタに表示されます。
残念ながらこのページではそのシリアルモニタを載せることができません。
現在の状況
嘘じゃないです。ちゃんとやかんにカメラを向けたときは”kettle”の文字列が表示されましたし、フライパンの時は”flypan”が表示されました。しかしそれで安心してしまって一度他のプログラムを入れたら、何度も入れ直しても通信エラーが発生するようになってしまいました。
経緯を説明すると以下の通りです。
edge impulseのプログラムを入れて画像認識ができた。
↓
ブログのネタのためcamera web serverのプログラムを書き込んでみる。実行するとなぜか通信エラー。
↓
camera web serverを諦めてedge impulseのプログラムを再度入れるとそれも通信エラー
↓
オワタ/(^o^)\
というわけで何のプログラムも動かないマイコンとなりましたとさ。
正直原因は不明です。壊れたのかも不明です。心あたりがあるとするとedge impulseのプログラムを動かし始めたときからESP32CAM PSRAMが発熱しているように感じます。熱暴走して壊れてしまったのかもしれません。
正直悔しいのでいつか買いなおして再チャレンジしたいと思ってます。