M5Cameraを極めて融通の利かない写真専門家にする

# 概要
M5Cameraをいとも簡単に顔認識機付きWebカメラにできる**CameraWebServer**。この恐るべき多機能なサンプルをシェイプアップしまくって、極めて融通の利かない写真専門家にする。顔認識はもちろん、動画ストリーミング、外部からの撮影パラメータ変更などの機能を無効にしまくる。

超初心者ならではの躊躇のなさを発揮するので、やり過ぎ、間違いなどにご注意を。

# 試した環境
* M5Camera Version B
* Arduino IDE 1.8.15
* Arduino core for the ESP stable 1.0.6
* CameraWebServer(Arduino core for the ESP stable 1.0.6に同梱)

# 手順
## 事前学習
CameraWebServerをシンプルにしたい欲求はいろんな方がお持ちのようで、まずは先人たちの知恵を学んだ方が良い。つまるところ、この記事不要説。

https://note.com/khe00716/n/n704ec39c4a26

https://www.mgo-tec.com/blog-entry-esp32-ov2640-ssd1331-test1.html/3

## CameraWebServerを開いて別名で保存する
Arduino core for the ESPが入っていれば、スケッチ例から開ける。
[ファイル]-[スケッチ例]-[ESP32]-[Camera]-[CameraWebServer]
これを別名で保存しておく。今回は**M5Camera-Image**にした。

## 顔認識関係と動画ストリーミング関係を削除する
ガツガツと削除。本来は顔認識関係と動画ストリーミング関係を、それぞれ順を追って削除していった方が分かりやすいけど、ここでは一緒くたにやる。顔を認識したとき、ストリーミング用の画像に四角形を埋め込んだりしてて、二人は切っても切れない関係のご様子なので。

対象は**app_httpd.cpp**。いきなり削除後のソースだけにすると後で振り返るのが大変なので、関係してるパーツごとに削除した履歴を書いておく。以下は残した部分ではなく、削除した部分を書いているので気を付けて。

* 顔認識と動画ストリーミング関係の関数を削除する。

```cpp:app_httpd.cpp
static ra_filter_t * ra_filter_init(){...}
static int ra_filter_run(){...}
static void rgb_print(){...}
static int rgb_printf(){...}
static void draw_face_boxes(){...}
static int run_face_recognition(){...}
static size_t jpg_encode_stream(){...}
```

* 静止画キャプチャー以外のハンドラを削除する。

```cpp:app_httpd.cpp
static esp_err_t stream_handler(){...}
static esp_err_t cmd_handler(){...}
static esp_err_t status_handler(){...}
static esp_err_t index_handler(){...}
```

* 削除したハンドラに対応するURIの定義と登録を削除する。

```cpp:app_httpd.cpp
void startCameraServer(){
...
    httpd_uri_t index_uri = {...};
    httpd_uri_t status_uri = {...};
    httpd_uri_t cmd_uri = {...};
    httpd_uri_t stream_uri = {...};
...
        httpd_register_uri_handler(camera_httpd, &index_uri);
        httpd_register_uri_handler(camera_httpd, &cmd_uri);
        httpd_register_uri_handler(camera_httpd, &status_uri);
...
}
```

* 静止画キャプチャーの関数から顔認識関係を削除する。

```cpp:app_httpd.cpp
static esp_err_t capture_handler(httpd_req_t *req){
...
    size_t out_len, out_width, out_height;
    uint8_t * out_buf;
    bool s;
    bool detected = false;
    int face_id = 0;
    if(!detection_enabled || fb->width > 400){
...
        if(fb->format == PIXFORMAT_JPEG){
...
        } else {
            jpg_chunking_t jchunk = {req, 0};
            res = frame2jpg_cb(fb, 80, jpg_encode_stream, &jchunk)?ESP_OK:ESP_FAIL;
            httpd_resp_send_chunk(req, NULL, 0);
            fb_len = jchunk.len;
        }
...
    }

    dl_matrix3du_t *image_matrix = dl_matrix3du_alloc(1, fb->width, fb->height, 3);
    if (!image_matrix) {
        esp_camera_fb_return(fb);
        Serial.println("dl_matrix3du_alloc failed");
        httpd_resp_send_500(req);
        return ESP_FAIL;
    }

    out_buf = image_matrix->item;
    out_len = fb->width * fb->height * 3;
    out_width = fb->width;
    out_height = fb->height;

    s = fmt2rgb888(fb->buf, fb->len, fb->format, out_buf);
    esp_camera_fb_return(fb);
    if(!s){
        dl_matrix3du_free(image_matrix);
        Serial.println("to rgb888 failed");
        httpd_resp_send_500(req);
        return ESP_FAIL;
    }

    box_array_t *net_boxes = face_detect(image_matrix, &mtmn_config);

    if (net_boxes){
        detected = true;
        if(recognition_enabled){
            face_id = run_face_recognition(image_matrix, net_boxes);
        }
        draw_face_boxes(image_matrix, net_boxes, face_id);
        free(net_boxes->score);
        free(net_boxes->box);
        free(net_boxes->landmark);
        free(net_boxes);
    }

    jpg_chunking_t jchunk = {req, 0};
    s = fmt2jpg_cb(out_buf, out_len, out_width, out_height, PIXFORMAT_RGB888, 90, jpg_encode_stream, &jchunk);
    dl_matrix3du_free(image_matrix);
    if(!s){
        Serial.println("JPEG compression failed");
        return ESP_FAIL;
    }

    int64_t fr_end = esp_timer_get_time();
    Serial.printf("FACE: %uB %ums %s%d\n", (uint32_t)(jchunk.len), (uint32_t)((fr_end - fr_start)/1000), detected?"DETECTED ":"", face_id);
    return res;
...
```

* 動画ストリーミング用HTTPサーバの起動関係を削除する。

```cpp:app_httpd.cpp
httpd_handle_t stream_httpd = NULL;
....
void startCameraServer(){
...
    config.server_port += 1;
    config.ctrl_port += 1;
    Serial.printf("Starting stream server on port: '%d'\n", config.server_port);
    if (httpd_start(&stream_httpd, &config) == ESP_OK) {
        httpd_register_uri_handler(stream_httpd, &stream_uri);
    }
...
```

* 顔認識の初期設定関係を削除する。

```cpp:app_httpd.cpp
void startCameraServer(){
...
    ra_filter_init(&ra_filter, 20);
    
    mtmn_config.type = FAST;
    mtmn_config.min_face = 80;
    mtmn_config.pyramid = 0.707;
    mtmn_config.pyramid_times = 4;
    mtmn_config.p_threshold.score = 0.6;
    mtmn_config.p_threshold.nms = 0.7;
    mtmn_config.p_threshold.candidate_number = 20;
    mtmn_config.r_threshold.score = 0.7;
    mtmn_config.r_threshold.nms = 0.7;
    mtmn_config.r_threshold.candidate_number = 10;
    mtmn_config.o_threshold.score = 0.7;
    mtmn_config.o_threshold.nms = 0.7;
    mtmn_config.o_threshold.candidate_number = 1;
    
    face_id_init(&id_list, FACE_ID_SAVE_NUMBER, ENROLL_CONFIRM_TIMES);
...
```

この辺りのいらない判断は、以下の記事を参考にした。ただし、以下の記事は「顔認識をしよう！」という前向きな記事。こんな使われ方は不本意かも知れない…。

https://qiita.com/dzonesasaki/items/8954e3286f98ab9dc1f0

* 用なしになったヘッダーファイルの読み込みを削除する。

```cpp:app_httpd.cpp
#include "img_converters.h"
#include "camera_index.h"
...
#include "fb_gfx.h"
#include "fd_forward.h"
#include "fr_forward.h"
```

* もはや無意味になった定数や変数、型の定義を削除する。

```cpp:app_httpd.cpp
#define ENROLL_CONFIRM_TIMES 5
#define FACE_ID_SAVE_NUMBER 7

#define FACE_COLOR_WHITE  0x00FFFFFF
#define FACE_COLOR_BLACK  0x00000000
#define FACE_COLOR_RED    0x000000FF
#define FACE_COLOR_GREEN  0x0000FF00
#define FACE_COLOR_BLUE   0x00FF0000
#define FACE_COLOR_YELLOW (FACE_COLOR_RED | FACE_COLOR_GREEN)
#define FACE_COLOR_CYAN   (FACE_COLOR_BLUE | FACE_COLOR_GREEN)
#define FACE_COLOR_PURPLE (FACE_COLOR_BLUE | FACE_COLOR_RED)

typedef struct {
        size_t size; //number of values used for filtering
        size_t index; //current value index
        size_t count; //value count
        int sum;
        int * values; //array to be filled with values
} ra_filter_t;

typedef struct {
        httpd_req_t *req;
        size_t len;
} jpg_chunking_t;

#define PART_BOUNDARY "123456789000000000000987654321"
static const char* _STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY;
static const char* _STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n";
static const char* _STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n";

static ra_filter_t ra_filter;
...
static mtmn_config_t mtmn_config = {0};
static int8_t detection_enabled = 0;
static int8_t recognition_enabled = 0;
static int8_t is_enrolling = 0;
static face_id_list id_list = {0};
```

* M5Cameraに写真を撮ってもらうURIを変更する。変えないと何となく気持ち悪い。

```cpp:app_httpd.cpp
void startCameraServer(){
...
    httpd_uri_t capture_uri = {
//        .uri       = "/capture",
        .uri       = "/",
        .method    = HTTP_GET,
        .handler   = capture_handler,
        .user_ctx  = NULL
    };
...
```

ここまでやると、事前学習で紹介した以下のサイトの**app_httpd.cpp**とほとんど同じになる。

https://note.com/khe00716/n/n704ec39c4a26

## お好みの撮影パラメータを定義する

対象は**M5Camera-Image.ino**。これも考えようによってはガツガツ削除できるけど、うっかりカメラの機種が変わることも考えて、今回はガッツかないことにする。なぜか？それはいきなり販売中止になると、頭の中が真っ白になるから。

* 解像度やJPEG品質とかの設定を追加する。

```cpp:app_httpd.cpp
  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;

  // My setting
  config.frame_size = FRAMESIZE_QVGA;
  config.jpeg_quality = 10;
  config.fb_count = 1;
```
追加したのは、`// My setting`の部分。

* 画像の向きを調整する。うちのM5Cameraは上下左右が逆に映るので、これを直す。

```cpp:
#if defined(CAMERA_MODEL_M5STACK_WIDE) || defined(CAMERA_MODEL_M5STACK_ESP32CAM)
  s->set_vflip(s, 1);
  s->set_hmirror(s, 1);
#endif

  // For M5Camera
  s->set_vflip(s, 1);
  s->set_hmirror(s, 1);
```
追加したのは、`// For M5Camera`の部分。上の方にある`CAMERA_MODEL_M5STACK_WIDE`ってきっと、M5Cameraの魚眼版のことだと思うんだけど、これの場合は元々、上下左右を逆にするようになってる。通常版のM5Cameraの場合はそうならない想定なんだろうな…。うちのがおかしいのか？

どんな撮影パラメータが使えるかは、事前学習で紹介した以下の記事にたくさん挙げて下さってる。両手を合わせながらありがたく参考にする。

https://www.mgo-tec.com/blog-entry-esp32-ov2640-ssd1331-test1.html/3

## camera_index.hを削除する

別に削除しなくてもいいけど、使わないので削除する。薄情な世の中。

## 動かす

あとは、CameraWebServerでお約束のカメラの機種とかWi-Fiの設定をして、M5Cameraに流し込めばOK。起動するとシリアルモニターにアクセス先のIPアドレスが表示されるので、ブラウザでそこにアクセス。無言で注文通りに今の瞬間を捉えた画像が表示される。

# 最後に
ド級の初心者にはちょっとハードル高かった。でも、先人様たちのおかげ、見様見真似で何とかなった。後は同じM5Cameraに、先の記事に書いた環境センサーもつないで、写真も撮れて温度も教えてくれる偉人にするだけ。