概要
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をシンプルにしたい欲求はいろんな方がお持ちのようで、まずは先人たちの知恵を学んだ方が良い。つまるところ、この記事不要説。
CameraWebServerを開いて別名で保存する
Arduino core for the ESPが入っていれば、スケッチ例から開ける。
[ファイル]-[スケッチ例]-[ESP32]-[Camera]-[CameraWebServer]
これを別名で保存しておく。今回はM5Camera-Imageにした。
顔認識関係と動画ストリーミング関係を削除する
ガツガツと削除。本来は顔認識関係と動画ストリーミング関係を、それぞれ順を追って削除していった方が分かりやすいけど、ここでは一緒くたにやる。顔を認識したとき、ストリーミング用の画像に四角形を埋め込んだりしてて、二人は切っても切れない関係のご様子なので。
対象は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(){...}
- 静止画キャプチャー以外のハンドラを削除する。
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の定義と登録を削除する。
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);
...
}
- 静止画キャプチャーの関数から顔認識関係を削除する。
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サーバの起動関係を削除する。
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);
}
...
- 顔認識の初期設定関係を削除する。
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);
...
この辺りのいらない判断は、以下の記事を参考にした。ただし、以下の記事は「顔認識をしよう!」という前向きな記事。こんな使われ方は不本意かも知れない…。
- 用なしになったヘッダーファイルの読み込みを削除する。
#include "img_converters.h"
#include "camera_index.h"
...
#include "fb_gfx.h"
#include "fd_forward.h"
#include "fr_forward.h"
- もはや無意味になった定数や変数、型の定義を削除する。
#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を変更する。変えないと何となく気持ち悪い。
void startCameraServer(){
...
httpd_uri_t capture_uri = {
// .uri = "/capture",
.uri = "/",
.method = HTTP_GET,
.handler = capture_handler,
.user_ctx = NULL
};
...
ここまでやると、事前学習で紹介した以下のサイトのapp_httpd.cppとほとんど同じになる。
お好みの撮影パラメータを定義する
対象はM5Camera-Image.ino。これも考えようによってはガツガツ削除できるけど、うっかりカメラの機種が変わることも考えて、今回はガッツかないことにする。なぜか?それはいきなり販売中止になると、頭の中が真っ白になるから。
- 解像度やJPEG品質とかの設定を追加する。
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は上下左右が逆に映るので、これを直す。
#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の場合はそうならない想定なんだろうな…。うちのがおかしいのか?
どんな撮影パラメータが使えるかは、事前学習で紹介した以下の記事にたくさん挙げて下さってる。両手を合わせながらありがたく参考にする。
camera_index.hを削除する
別に削除しなくてもいいけど、使わないので削除する。薄情な世の中。
動かす
あとは、CameraWebServerでお約束のカメラの機種とかWi-Fiの設定をして、M5Cameraに流し込めばOK。起動するとシリアルモニターにアクセス先のIPアドレスが表示されるので、ブラウザでそこにアクセス。無言で注文通りに今の瞬間を捉えた画像が表示される。
最後に
ド級の初心者にはちょっとハードル高かった。でも、先人様たちのおかげ、見様見真似で何とかなった。後は同じM5Cameraに、先の記事に書いた環境センサーもつないで、写真も撮れて温度も教えてくれる偉人にするだけ。