ESP32のカメラで静止画を撮影する

M5Cameraなど、ESP32とカメラがセットになったデバイスで、静止画を撮影します。
最終的には、以下のESP32で動作するJavascript環境で実行できるようにします。

(参考) ESP32で動作するJavascript実行環境

ESP32で動作するJavascript実行環境を公開しています。

「電子書籍：M5StackとJavascriptではじめるIoTデバイス制御」

https://poruruba.booth.pm/items/3735175

サポートサイト

https://github.com/poruruba/QuickJS_ESP32_IoT_Device_M5Unified

以下のデバイスで動作確認しました。他のモデルでは未確認です。
・M5Camera X https://www.switch-science.com/products/6555
・TTGO Camera Plus https://ja.aliexpress.com/item/32971057846.html
※いずれももう販売はしていませんが、後継機種があるようです。


# カメラ制御のための関数

カメラの制御には、標準でついているライブラリ<esp_camera.h>を使います。
以下のサンプルを参考にしました。

https://github.com/espressif/arduino-esp32/tree/master/libraries/ESP32/examples/Camera/CameraWebServer

今回は、スナップショット画像だけ欲しいので、ストリーム生成や顔検出機能は省いてシンプルにしました。


・esp_camera_init
カメラを初期化します。引数に、ピン配置や画面解像度などを指定できます。

・esp_camera_fb_get
現時点のフレームバッファに格納されている画像データを取得します。

・esp_camera_fb_return
取得していたフレームバッファの画像を開放します。

・esp_camera_deinit
カメラをリセットして初期状態にするそうです。

・esp_camera_sensor_get
カメラのキャプチャに関する設定変更するための構造体のインスタンスを取得します。

・esp_camera_sensor_get_info
カメラ本体の情報を取得します。

初期化と終了は以下のようにしました。

```cpp:module_camera.cpp
static long camera_initialize(uint8_t type, uint8_t framesize)
{
  camera_config_t config;

  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;

  config.pin_d0 = camera_pins[type][Y2_GPIO_INDEX];
  config.pin_d1 = camera_pins[type][Y3_GPIO_INDEX];
  config.pin_d2 = camera_pins[type][Y4_GPIO_INDEX];
  config.pin_d3 = camera_pins[type][Y5_GPIO_INDEX];
  config.pin_d4 = camera_pins[type][Y6_GPIO_INDEX];
  config.pin_d5 = camera_pins[type][Y7_GPIO_INDEX];
  config.pin_d6 = camera_pins[type][Y8_GPIO_INDEX];
  config.pin_d7 = camera_pins[type][Y9_GPIO_INDEX];
  config.pin_xclk = camera_pins[type][XCLK_GPIO_INDEX];
  config.pin_pclk = camera_pins[type][PCLK_GPIO_INDEX];
  config.pin_vsync = camera_pins[type][VSYNC_GPIO_INDEX];
  config.pin_href = camera_pins[type][HREF_GPIO_INDEX];
  config.pin_sccb_sda = camera_pins[type][SIOD_GPIO_INDEX];
  config.pin_sccb_scl = camera_pins[type][SIOC_GPIO_INDEX];
  config.pin_pwdn = camera_pins[type][PWDN_GPIO_INDEX];
  config.pin_reset = camera_pins[type][RESET_GPIO_INDEX];

  config.xclk_freq_hz = 20000000;
  config.pixel_format = PIXFORMAT_JPEG;

  // if PSRAM IC present, init with UXGA resolution and higher JPEG quality
  //                      for larger pre-allocated frame buffer.

//  config.frame_size = FRAMESIZE_QVGA;
  config.frame_size = (framesize_t)framesize;
  config.jpeg_quality = 10;
  config.fb_count = 1;

  // camera init
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x\n", err);
    return -1;
  }

  return 0;
}

static long camera_dispose(void)
{
  esp_camera_deinit();

  return 0;
}
```

初期化のための関数「camera_initialize」について補足します。

1つ目の引数は、デバイスのモデルの指定です。モデルごとにピン配置が異なるためです。
2つ目の引数は、画像解像度です。最初に指定した解像度以下であれば、起動後も解像度を変更できるため、初期サイズを指定できるようにしています。

これをESP32上のJavascript実行環境から呼び出せるように以下を用意します。
camera_initializeをリトライしています。
起動直後に呼び出すと失敗することがあるようで、1回リトライするようにしました。

```cpp:module_camera.cpp
static JSValue esp32_camera_start(JSContext *ctx, JSValueConst jsThis, int argc, JSValueConst *argv)
{
  if( isInitialized )
    return JS_EXCEPTION;

  uint32_t type = CAMERA_MODEL_WROVER_KIT;
  if( argc >= 1 )
    JS_ToUint32(ctx, &type, argv[0]);
  if( type >= CAMERA_MODEL_NUM )
    return JS_EXCEPTION;

  uint32_t framesize = FRAMESIZE_QVGA;
  if( argc >= 2 )
    JS_ToUint32(ctx, &framesize, argv[1]);

  long ret = camera_initialize(type, framesize);
  if( ret != 0 ){
    ret = camera_initialize(type, framesize);
    if( ret != 0 )
      return JS_EXCEPTION;
  }
  isInitialized = true;

  return JS_UNDEFINED;
}
```

画像取得は以下の感じです。
先ほど指定された解像度に合わせて画像ファイルがmallocして返されますので、
使い終わったらfreeします。
途中で気づいたのですが、esp_camera_fb_getで取得しても、なぜか最新のスナップショット画像が取得できないことがありました。
そこで、2回取得するようにし、2回目の画像を使うようにしています。
camera_fb_tは、使い終わったら、esp_camera_fb_returnを呼び出して開放するようにします。

```cpp:module_camera.cpp
static long camera_get_capture(uint8_t **pp_image, size_t *p_image_size)
{
  camera_fb_t *fb = NULL;
  fb = esp_camera_fb_get();
  if( fb )
    esp_camera_fb_return(fb);
  fb = esp_camera_fb_get();
  if (!fb){
      Serial.println("Camera capture failed");
      return -1;
  }

  if (fb->format != PIXFORMAT_JPEG){
    Serial.println("Unknown format");
    esp_camera_fb_return(fb);
    return -1;
  }
  
  *pp_image = (uint8_t*)malloc(fb->len);
  if( *pp_image == NULL ){
    Serial.println("Out of memory");
    esp_camera_fb_return(fb);
    return -1;
  }
  memmove(*pp_image, fb->buf, fb->len);
  *p_image_size = fb->len;
  esp_camera_fb_return(fb);

  return 0;
}
```

(参考)
[ESP32で使えるOV3660とOV2640の画質の比較と初期化パラメーター](https://intellectualcuriosity.hatenablog.com/entry/2024/03/21/215308)


# カメラのキャプチャに関する設定変更

esp_camera_sensor_getの呼び出しで設定できる構造体「sensor_t」とesp_camera_sensor_get_infoで取得可能な構造体「camera_sensor_info_t」のパラメータについて補足します。
たくさんありすぎて、よくわからないパラメータがたくさんありますが、このライブラリで設定・取得できるものを転記しておきました。

|機能|意味|値|デフォルト|
|---|---|---|---|
|framesize|フレームサイズ|FRAMESIZE_96X96(96x96)～FRAMESIZE_QSXGA(2560x1920)|FRAMESIZE_QVGA|RW
|quality|JPEG Quality|0～63|10|RW
|brightness|明るさ|-2～2|0|RW
|contrast|コントラスト|-2～2|0|RW
|saturation|彩度|-2～2|0|RW
|special_effect|特殊効果|0:No Effect<br>1:Negative<br>2:Grayscale<br>3:Red Tint<br>4:Green Tint<br>5:Blue Tint<br>6:Sepia|0|RW
|awb(whitebal)|自動ホワイトバランス(Automatic White Balance)|bool|true|WR
|awb_gain||bool|true|RW
|wb_mode|ホワイトバランスモード|0:Auto<br>1:Sunny<br>2:Cloudy<br>3:Office<br>4:Home|0|RW
|aec(exposure_ctrl)|自動露出制御(Automatic Exposure Control)|bool|true|WR
|aec2||bool|false|RW
|aec_value||0～1200|335|RW
|ae_level||-2～2|0|RW
|agc(gain_ctrl)|自動ゲイン制御(Automatic Gain Control)|bool|true|WR
|agc_gain||0～30|0|RW
|gainceiling||0～6|0|RW
|bpc||bool|false|RW
|wpc||bool|true|RW
|raw_gma||bool|true|RW
|lenc|レンズ補正(Lens Correction)|bool|true|RW
|hmirror|水平方向の反転表示|bool|false|RW
|vflip|垂直方向の反転表示|bool|false|RW
|dcw||bool|true|RW
|colorbar|カラーバーフレーム出力|bool|false|RW
|sharpness|シャープネス|-2～2|0|RW
|denoise|||0|RW

こちらのサイトを参考にさせていただきました。

(参考)

[esp32-cameraライブラリを読み解く　～モジュール接続、動作チェック編～](https://www.mgo-tec.com/blog-entry-esp32-ov2640-ssd1331-test1.html/3#title07)  
[Change ESP32-CAM OV2640 Camera Settings: Brightness, Resolution, Quality, Contrast, and More](https://randomnerdtutorials.com/esp32-cam-ov2640-camera-settings/)  
[esp32-camera/sensor.h](https://github.com/espressif/esp32-camera/blob/master/driver/include/sensor.h)  

# 呼び出し例

デバイスのGrove端子に接続した物理ボタンを押すと、画像をキャプチャする例です。
JPEGデータをBASE64エンコードして表示しています。

```js:main.js
import * as camera from "Camera";
import * as input from "Input";

input.openCustomButton(input.BUTTON_X, 4, true);
camera.start(camera.MODEL_M5STACK_V2_PSRAM);
console.log(JSON.stringify(camera.getParameter()));

function loop(){
	esp32.update();

	if( input.wasPressed(input.BUTTON_X)){
		console.log("wasPressed");
		var buffer = camera.getPicture();
		var base64 = utils.base64Encode(new Uint8Array(buffer));
		console.log(base64);
	}
}
```

以上