知的好奇心 for IoT

IoT関連の知的好奇心を探求するブログです

ESP32で使えるOV3660とOV2640の画質の比較と初期化パラメーター

ストアカで「LINEに通知できる防犯カメラを作ろう!」という講座をやっているのですが、時々撮った写真が緑被りすることがあったのです。

緑被り対策としてイメージセンサーの初期化の後にディレイを入れていたのですが、ディレイを長くすると写真を撮るタイミングが遅くなるので都合が悪いんです。

 

プログラムはM5Cameraを使っていた頃から今のTimer Camera Xまで色々と変わっていますが、カメラモジュールの初期化パラメーターはそのまま使用していました。

 

その他に、Timer Camera XのOV3660になって写る写真がボヤーとした感じになっていて、M5CameraのOV2640より画質が落ちていたので、初期化パラメーターを調べて色々とテストしてみることにしました。

 

以前から使用している初期化パラメーター(抜粋)

config.xclk_freq_hz = 20000000;
config.pixel_format = PIXFORMAT_JPEG;
config.frame_size = FRAMESIZE_UXGA;
config.jpeg_quality = 10
config.fb_count = 1

 

OV2640の写真(M5Stack Unit CAMS3を使用)ディレイあり

 

OV3660の写真(M5Stack Timer Camer Xを使用)ディレイあり

 

初期化パラメーター

GitHubにESP32 Camera Driverというリポジトリがあり、ここにESP32が対応しているイメージセンサーなどの情報が載っていて、初期化パラメーターについても簡単にコメントが書いてありました。

載っていた初期化パラメーター(抜粋)

.xclk_freq_hz = 20000000,//EXPERIMENTAL: Set to 16MHz on ESP32-S2 or ESP32-S3 to enable EDMA mode
.pixel_format = PIXFORMAT_JPEG,//YUV422,GRAYSCALE,RGB565,JPEG
.frame_size = FRAMESIZE_UXGA,//QQVGA-UXGA, For ESP32, do not use sizes above QVGA when not JPEG.
.jpeg_quality = 12, //0-63, for OV series camera sensors, lower number means higher quality
.fb_count = 1, //When jpeg mode is used, if fb_count more than one, the driver will work in continuous mode.
.grab_mode = CAMERA_GRAB_WHEN_EMPTY//CAMERA_GRAB_LATEST. Sets when buffers should be filled

.grab_modeはこれまで使用していなかったのですが、記述しないとデフォルトがCAMERA_GRAB_WHEN_EMPTYになるようです。

 

この初期化パラメーターで、初期化後に全くディレイを入れない状態で写真を撮影したら、OV2640とOV3660で次のような写真が撮れてしまいました。

 

OV2640の写真(M5Stack Unit CAMS3を使用)ディレイなし

 

OV3660の写真(M5Stack Timer Camer Xを使用)ディレイなし

 

しかし、今回のテストのために用意したプログラム(ブラウザでアクセス(リロード)した時に写真を撮って表示する)により、色被りした写真が撮れてしまうことよりも深刻な問題が潜んでいることが発覚しました。

なんと色被りしていた写真は、写真を撮る命令を実行した時のものではなく、どうもイメージセンサーを初期化したときに撮られたものらしかったのです。

 

そこで、写真を撮る動作に影響を与えていると思われる.fb_count.grab_modeの値の組み合わせで、どのような写真が取れるのか確かめてみました。

なんと、正しく動作する組み合わせは.fb_count2.grab_modeCAMERA_GRAB_LATESTの1通りしかありませんでした。

 

画質調整用パラメーター

画質を調整できそうなパラメーターは、ESP32 Camera Driverリポジトリのソースコードを漁っていたら見つけることができました。

載っていた画質調整パラメーター(抜粋)

int8_t brightness;//-2 - 2
int8_t contrast;//-2 - 2
int8_t saturation;//-2 - 2
int8_t sharpness;//-2 - 2
uint8_t denoise;

輝度、コントラスト、彩度、シャープネス、ノイズ除去の調整が行えるようです。

これらのパラメーターでOV3660の画質を向上させることができるか試行錯誤した結果、次のパラメータでOV2640に近い画質に改善することができました。

s->set_brightness(s, -1);
s->set_contrast(s, 1);
s->set_saturation(s, 2);
s->set_denoise(s, 1);

 

OV3660の写真(M5Stack Timer Camer Xを使用)画質調整なし

 

OV3660の写真(M5Stack Timer Camer Xを使用)画質調整あり

 

新たな問題

OV3660の画質も向上し一件落着っと思ったのですが、シリアルモニタに見慣れないメッセージcam_hal: EV-EOF-OVFが幾つも出ているのに気づきました。

 

また、この状態だと、まれに次のような変な写真が撮れてしまうことがありました。

 

途中から緑被りしている写真

 

上記の画質調整パラメーターを使用するとこのメッセージが表示されるようで、.fb_count1 .grab_modeCAMERA_GRAB_WHEN_EMPTYに戻すと、ほぼメッセージが表示されなくなることがわかりました。

 

八方塞がりな感じになってしまったため、正攻法で攻めるのをやめて邪道な方法をとることにしました。

写真を撮る命令の実行で前回の写真が得られるなら、1度に2回連続写真を撮ってしまえば今の写真が得られる訳です。

 

コードは次のようになります。

camera_fb_t *fb = esp_camera_fb_get();
esp_camera_fb_return(fb);
fb = esp_camera_fb_get();

 

写真を撮った後にバッファを解放してから2回目の写真を撮るようにしないと、マイコンがダンプを吐いてリブートしてしまうので注意してください。

 

テストプログラム

CameraTest.ino

#include <WiFi.h>                       // WiFiライブラリ
#include <esp_camera.h>                 // ESPカメラライブラリ
#include <esp_http_server.h>            // ESP Webサーバーライブラリ
#include <ESPmDNS.h>                    // マルチキャストDNSライブラリ

//#define CAMERA_MODEL_XIAO_ESP32S3     // 使用するボードのコメントを外す
//#define CAMERA_MODEL_UNIT_CAMS3       // 使用するボードのコメントを外す
#define CAMERA_MODEL_TIMER_CAM          // 使用するボードのコメントを外す
#include "camera_pins.h"

#define WIFI_SSID     "********"        // *にWiFiのSSIDを書く
#define WIFI_PASS     "********"        // *にWiFiのパスワードを書く
#define HOST_NAME     "esp32camera"     // ホスト名
#define VFLIP         1                 // カメラの設置向き 1:正常 0:逆さ

// Webサーバー用オブジェクト作成
httpd_handle_t camera_httpd;

// マイコンの電源投入後に、最初に呼び出される関数(ディープスリープからの復帰後も)
void setup() {
  Serial.begin(115200);                 // シリアルモニタの通信速度設定
  initCamera();                         // カメラの初期化
  connectWiFi();                        // WiFi接続
  MDNS.begin(HOST_NAME);                // mDNS初期化
  startCameraServer();                  // Webサーバーの開始
}

// setup関数の後に、繰り返し呼び出される関数
void loop() {
  delay(1000);
}

// カメラの初期化(CameraWebServerのサンプルコードを改良)
void initCamera() {
  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.frame_size =   FRAMESIZE_UXGA;
  config.jpeg_quality = 10;
  config.fb_count =     1;
  config.fb_location  = CAMERA_FB_IN_PSRAM;
  config.grab_mode    = CAMERA_GRAB_WHEN_EMPTY;
  //config.grab_mode    = CAMERA_GRAB_LATEST;
  esp_camera_init(&config);
  sensor_t *s = esp_camera_sensor_get();
  s->set_brightness(s, -1);             // 輝度 -2 - 2
  s->set_contrast(s, 1);                // コントラスト -2 - 2
  s->set_saturation(s, 2);              // 彩度 -2 - 2
  s->set_denoise(s, 1);                 // ノイズ除去
  s->set_vflip(s, VFLIP);
#if defined(CAMERA_MODEL_TIMER_CAM)
  s->set_hmirror(s, !VFLIP);
#else
  s->set_hmirror(s, VFLIP);
#endif
  Serial.println("カメラ初期化完了");
}

// WiFi接続
void connectWiFi() {
  WiFi.begin(WIFI_SSID, WIFI_PASS);     // WiFi接続
  Serial.print(WIFI_SSID "に接続中");
  int i = 0;
  while (WiFi.status() != WL_CONNECTED) { // 繋がるまでループ
    if (++i > 5) ESP.restart();         // 5回でリセット
    Serial.print(".");
    delay(1000);
  }
  Serial.println("接続完了");
}

// Webサーバーの開始
void startCameraServer() {
  httpd_config_t config = HTTPD_DEFAULT_CONFIG();

  // ストリーミング用URI
  httpd_uri_t index_uri = {
    .uri       = "/",
    .method    = HTTP_GET,
    .handler   = capture_handler,
    .user_ctx  = NULL
  };

  // Webサーバーの開始とハンドラー関数の登録
  httpd_start(&camera_httpd, &config);
  httpd_register_uri_handler(camera_httpd, &index_uri);

  Serial.print("次のURLで表示できます。'http://" HOST_NAME ".local'");
}

// ストリーミング用ハンドラー関数
esp_err_t capture_handler(httpd_req_t *req) {
  camera_fb_t *fb = esp_camera_fb_get();
  esp_camera_fb_return(fb);
  fb = esp_camera_fb_get();
  httpd_resp_set_type(req, "image/jpeg");
  httpd_resp_set_hdr(req, "Content-Disposition", "inline; filename=capture.jpg");
  httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
  httpd_resp_send(req, (const char *)fb->buf, fb->len);
  esp_camera_fb_return(fb);
  return ESP_OK;
}

camera_pins.h

#if defined(CAMERA_MODEL_XIAO_ESP32S3)
#define PWDN_GPIO_NUM     -1
#define RESET_GPIO_NUM    -1
#define XCLK_GPIO_NUM     10
#define SIOD_GPIO_NUM     40
#define SIOC_GPIO_NUM     39
#define Y9_GPIO_NUM       48
#define Y8_GPIO_NUM       11
#define Y7_GPIO_NUM       12
#define Y6_GPIO_NUM       14
#define Y5_GPIO_NUM       16
#define Y4_GPIO_NUM       18
#define Y3_GPIO_NUM       17
#define Y2_GPIO_NUM       15
#define VSYNC_GPIO_NUM    38
#define HREF_GPIO_NUM     47
#define PCLK_GPIO_NUM     13

#elif defined(CAMERA_MODEL_UNIT_CAMS3)
#define PWDN_GPIO_NUM     -1
#define RESET_GPIO_NUM    -1
#define XCLK_GPIO_NUM     11
#define SIOD_GPIO_NUM     17
#define SIOC_GPIO_NUM     41
#define Y9_GPIO_NUM       13
#define Y8_GPIO_NUM       4
#define Y7_GPIO_NUM       10
#define Y6_GPIO_NUM       5
#define Y5_GPIO_NUM       7
#define Y4_GPIO_NUM       16
#define Y3_GPIO_NUM       15
#define Y2_GPIO_NUM       6
#define VSYNC_GPIO_NUM    42
#define HREF_GPIO_NUM     18
#define PCLK_GPIO_NUM     12

#elif defined(CAMERA_MODEL_TIMER_CAM)
#define PWDN_GPIO_NUM     -1
#define RESET_GPIO_NUM    15
#define XCLK_GPIO_NUM     27
#define SIOD_GPIO_NUM     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    22
#define HREF_GPIO_NUM     26
#define PCLK_GPIO_NUM     21

#else
#error "Camera model not selected"
#endif