M5cameraでhttp postによる画像送信


1. はじめに

M5cameraはカメラとESP32 wroverが内蔵された製品です.これを使って,定期的にhttp postでサーバにカメラ画像をアップロードしてみます.開発は,Arduino-IDEで行います.ライブラリはHTTPClientとesp_cameraを使います.サーバ側はnode-redを使います.

1.1. 利用したバージョン

  • Arduino-IDE : ver 1.8.13
  • Arduino core for the ESP32 : ver 1.0.4 (2019.10月版) (ESP32ライブラリ(ボードマネージャで設定)が古いと動かないことがあります)
  • Node-red : ver 1.1.2

2. m5cameraのコーディング

処理は次のような流れになります.
1. Wi-Fi設定
2. HTTPClient設定
3. カメラ初期設定
4. カメラ画像取得
5. HTTP post実施

以前作成したHTTPClientでバイナリファイルを送信を利用します.
まずは,カメラ画像の取得について記載します.

2.1. カメラ画像取得

カメラからの画像取得は非常に簡単です.
esp_camera.hを使い,初期設定後,このたった1行で画像取得できます.

 camera_fb_t * fb = esp_camera_fb_get();

fbは次のようになっています.(esp_camera.hより)

typedef struct {
    uint8_t * buf;              /*!< Pointer to the pixel data */
    size_t len;                 /*!< Length of the buffer in bytes */
    size_t width;               /*!< Width of the buffer in pixels */
    size_t height;              /*!< Height of the buffer in pixels */
    pixformat_t format;         /*!< Format of the pixel data */
    struct timeval timestamp;   /*!< Timestamp since boot of the first DMA buffer of the frame */
} camera_fb_t;

JPEGで取得すれば,必要なのはバッファの開始位置と長さとなります.例えば,バッファの最後の内容は下記のように記述できます.
c++
fb->buf[fb->len-1]

あとは,バッファの中身をHTTP postするだけです.ここでは,以前作成したHTTPClientでバイナリファイルを送信の関数httppostを利用します.

httppost( fb->buf , fb->len);

2.2. httppostの修正

但し,関数httppost内で,バッファを複製しhttp.POSTに渡していますので,mallocではなく,ps_mallocで大きいPSDRAM内に複製しています.
その点だけ変更した関数httppostです.

関数httppost
int32_t httppost( uint8_t * ui8BufJpg, uint32_t iNumDat ){

  String stMyURL="";
  stMyURL+=URL1;
  stMyURL+=String(servC);
  stMyURL+=URL2;
  myHttp.begin(stMyURL);

  String stConType ="";
  stConType +="multipart/form-data; boundary=";
  stConType +=STRING_BOUNDARY;
  myHttp.addHeader("Content-Type", stConType);

  String stMHead="";
  stMHead += "--";
  stMHead += STRING_BOUNDARY;
  stMHead += "\r\n";
  stMHead += STRING_MULTIHEAD02;
  stMHead += "\r\n";
  stMHead += STRING_MULTIHEAD03;
  stMHead += "\r\n";
  stMHead += "\r\n";
  uint32_t iNumMHead = stMHead.length();

  String stMTail="";
  stMTail += "\r\n";
  stMTail += "--";
  stMTail += STRING_BOUNDARY;
  stMTail += "--";
  stMTail += "\r\n";
  stMTail += "\r\n";  
  uint32_t iNumMTail = stMTail.length();

  uint32_t iNumTotalLen = iNumMHead + iNumMTail + iNumDat;

  uint8_t *uiB = (uint8_t *)ps_malloc(sizeof(uint8_t)*iNumTotalLen);

  for(int uilp=0;uilp<iNumMHead;uilp++){
    uiB[0+uilp]=stMHead[uilp];
  }
  for(int uilp=0;uilp<iNumDat;uilp++){
    uiB[iNumMHead+uilp]=ui8BufJpg[uilp];
  }
  for(int uilp=0;uilp<iNumMTail;uilp++){
    uiB[iNumMHead+iNumDat+uilp]=stMTail[uilp];
  }

  int32_t httpResponseCode = (int32_t)myHttp.POST(uiB,iNumTotalLen);
  myHttp.end();
  free(uiB);
  return (httpResponseCode);
}

2.3. カメラ初期設定

説明の流れが逆になりましたが,初期設定は次のように行います.

これはサンプルコードの CameraWebServerを利用します.m5 camera model Bは次のような設定です.

設定部分
#define PWDN_GPIO_NUM     -1
#define RESET_GPIO_NUM    15
#define XCLK_GPIO_NUM     27
#define SIOD_GPIO_NUM     22 //25
#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 //22
#define HREF_GPIO_NUM     26
#define PCLK_GPIO_NUM     21

  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM;
  config.pin_vsync = VSYNC_GPIO_NUM;
  config.pin_href = HREF_GPIO_NUM;
  config.pin_sscb_sda = SIOD_GPIO_NUM;
  config.pin_sscb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 20000000;
  config.pixel_format = PIXFORMAT_JPEG;
  config.jpeg_quality = 10;
  config.frame_size = FRAMESIZE_UXGA; 
  //FRAMESIZE_ + QVGA|CIF|VGA|SVGA|XGA|SXGA|UXGA
  config.fb_count = 1;

ポイントは,JPEGで取得する点です.JPEGのバイナリがフレームバッファに入るため,加工不要です.サイズは,好きなものを選択するのが良いと思います.ここでは最大のUXGAにしています.

2.4. Arduinoのボード側設定

  • ボードは, ESP32 Wrover Module
  • Partition Schemeは,Huge App(3MB No OTA/1MB SPIFFS)を選択します

3. サーバー側

Node-Redを使います.
次のようなノードを配置します.

http inノードは,メソッドをpostにし,ファイルをアップロードにチェックを入れ,URLを決めます.
アップロードされたファイルは,fileノードを使い,サーバ側へ一旦保存します.
fileノードは,payloadに入っているものを保存します.
そこで,次のように関数を書いて,payloadに流し込みました.

let mymsgB={};
var myArray = [];
myArray=msg.req.files[0].buffer;
mymsgB.payload=myArray;
mymsgB.filename="test.jpg";

return [msg,mymsgB];

関数を二股にしている理由は特になく,http inから二つに分岐しても良いです.

画像の確認には,http getを利用しています.
postする側はESP32なのでその画像をESP32に見せる必要はなく,上げられた画像を別で見たいはずですので,getで見ます.
こちらのhttp inノードは,メソッドをgetにします.ファイルは,先ほど一次保存したものと整合をとるようにします.あとは,そのバイナリを応答として返します.その返信の前にchangeノードで次のようにヘッダを追加します.

ESP32のHTTPClientでpostメソッドを使う際にヘッダを追加しましたがそれと同じですね.

4. サンプル

下記に置きました.
https://github.com/dzonesasaki/m5camera_httppost_sample

5. むすび

M5cameraはとても高機能で,たったこれだけの事にだけに使うのは勿体ない感じがします.画像をエッジ側(ここではM5camera)で解析する処理しても良いのでは?と感じます.しかし,一方,エッジ側は負荷を軽減したいというニーズもあるように思います.例えば,エッジ側の電源が十分に確保できず,省エネ化したい場合などです.そういう場合は,今回の手法は価値があるように思います.

なお,省エネ化のためには,ループ処理にあるものを全てsetup関数内に持って行き,deepやlightなスリープを行うという方法があると思います.これは撮影の間隔次第かもしれません.
あとは,電力消費の主役はWi-Fiです.残したい画像が来た時だけ,Wi-Fiを接続し,サーバにアップロードするという方法もあるかもしれません.例えば,人の顔を認識したらその画像をアップするなどです.Haar-Like程度であれば,ESP32で処理できるように思います.