Riconoscimento facciale per la domotica con l’ESP32-CAM – Parte 3

Come abbiamo visto nel precedente articolo "Riconoscimento facciale per la domotica con l’ESP32-CAM - Parte 2", il codice dello sketch "CameraWebServerPermanent" registra i volti permanentemente in memoria, ma l’interfaccia utente grafica è limitata. Inoltre, per cancellare i volti occorre un altro apposito codice che oltretutto non consente di cancellare un singolo volto ma cancella tutti i volti registrati. In questo articolo vedremo una più evoluta applicazione di riconoscimento facciale con più ricche funzionalità interattive ed una più efficiente interfaccia grafica.

RICONOSCIMENTO FACCIALE CON WEBSOCKET

Questa applicazione utilizza la libreria ArduinoWebsockets, ovvero il protocollo WebSocket per la comunicazione bidirezionale tra l’ESP32-CAM e il browser del dispositivo connesso alla rete locale Wi-Fi. L’elaborazione dei dati relativi ai frame del rilevamento, acquisizione e riconoscimento dei volti viene eseguita nell'ESP32-CAM. Il dispositivo (client) connesso tramite il browser, invia le istruzioni inerenti l’attività di registrazione dei volti e riceve notifiche tramite il protocollo WebSocket per l'aggiornamento dell'interfaccia grafica. La stessa libreria WebSocket viene utilizzata per inviare i dati della videocamera al browser. Nelle precedenti applicazioni abbiamo installato nell'IDE di Arduino l'ultima versione del pacchetto ESP32, ovvero la 1.0.4. In quest’altra applicazione si necessita di installare la libreria WebSocket nell'IDE di Arduino. Da Strumenti -> Gestione librerie, nella barra di ricerca digitate "arduinowebsockets" e installate la versione 0.5.0 che appare nel risultato della ricerca, come mostrato nella Figura 1.

Figura 1. Installazione libreria ArduinoWebSockets

Installata la libreria "ArduinoWebSockets", copiate il codice seguente in un nuovo sketch che salveremo come "CameraWebSockets", o come volete:

/*******************CameraWebSockets*****************/

#include "ArduinoWebsockets.h"
#include "esp_http_server.h"
#include "esp_timer.h"
#include "esp_camera.h"
#include "camera_index.h"
#include "Arduino.h"
#include "fd_forward.h"
#include "fr_forward.h"
#include "fr_flash.h"

const char* ssid = "Tuo SSID";
const char* password = "Tua PASSWORD";

#define ENROLL_CONFIRM_TIMES 5
#define FACE_ID_SAVE_NUMBER 7

// Select camera model
//#define CAMERA_MODEL_WROVER_KIT
//#define CAMERA_MODEL_ESP_EYE
//#define CAMERA_MODEL_M5STACK_PSRAM
//#define CAMERA_MODEL_M5STACK_WIDE
#define CAMERA_MODEL_AI_THINKER
#include "camera_pins.h"

using namespace websockets;
WebsocketsServer socket_server;

camera_fb_t * fb = NULL;

long current_millis;
long last_detected_millis = 0;

void app_facenet_main();
void app_httpserver_init();

typedef struct
{
uint8_t *image;
box_array_t *net_boxes;
dl_matrix3d_t *face_id;
} http_img_process_result;

static inline mtmn_config_t app_mtmn_config()
{
mtmn_config_t mtmn_config = {0};
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;
return mtmn_config;
}
mtmn_config_t mtmn_config = app_mtmn_config();

face_id_name_list st_face_list;
static dl_matrix3du_t *aligned_face = NULL;

httpd_handle_t camera_httpd = NULL;

typedef enum
{
START_STREAM,
START_DETECT,
SHOW_FACES,
START_RECOGNITION,
START_ENROLL,
ENROLL_COMPLETE,
DELETE_ALL,
} en_fsm_state;
en_fsm_state g_state;

typedef struct
{
char enroll_name[ENROLL_NAME_LEN];
} httpd_resp_value;

httpd_resp_value st_name;

void setup() {
Serial.begin(115200);
Serial.setDebugOutput(true);
Serial.println();

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;
//init with high specs to pre-allocate larger buffers
if (psramFound()) {
config.frame_size = FRAMESIZE_UXGA;
config.jpeg_quality = 10;
config.fb_count = 2;
} else {
config.frame_size = FRAMESIZE_SVGA;
config.jpeg_quality = 12;
config.fb_count = 1;
}

#if defined(CAMERA_MODEL_ESP_EYE)
pinMode(13, INPUT_PULLUP);
pinMode(14, INPUT_PULLUP);
#endif

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

sensor_t * s = esp_camera_sensor_get();
s->set_framesize(s, FRAMESIZE_QVGA);

#if defined(CAMERA_MODEL_M5STACK_WIDE)
s->set_vflip(s, 1);
s->set_hmirror(s, 1);
#endif

WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) 
{
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected");

app_httpserver_init();
app_facenet_main();
socket_server.listen(82);

Serial.print("Camera Ready! Use 'http://");
Serial.print(WiFi.localIP());
Serial.println("' to connect");
}

static esp_err_t index_handler(httpd_req_t *req) 
{
httpd_resp_set_type(req, "text/html");
httpd_resp_set_hdr(req, "Content-Encoding", "gzip");
return httpd_resp_send(req, (const char *)
index_ov2640_html_gz, index_ov2640_html_gz_len);
}

httpd_uri_t index_uri = 
{
.uri = "/",
.method = HTTP_GET,
.handler = index_handler,
.user_ctx = NULL
};

void app_httpserver_init ()
{
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
if (httpd_start(&camera_httpd, &config) == ESP_OK)
Serial.println("httpd_start");
{
httpd_register_uri_handler(camera_httpd, &index_uri);
}
}

void app_facenet_main()
{
face_id_name_init(&st_face_list, 
FACE_ID_SAVE_NUMBER, ENROLL_CONFIRM_TIMES);
aligned_face = dl_matrix3du_alloc
(1, FACE_WIDTH, FACE_HEIGHT, 3);
read_face_id_from_flash_with_name(&st_face_list);
}

static inline int do_enrollment
(face_id_name_list *face_list, dl_matrix3d_t *new_id)
{
ESP_LOGD(TAG, "START ENROLLING");
int left_sample_face = 
enroll_face_id_to_flash_with_name
(face_list, new_id, st_name.enroll_name);
ESP_LOGD(TAG, "Face ID %s Enrollment: Sample %d",
st_name.enroll_name,
ENROLL_CONFIRM_TIMES - left_sample_face);
return left_sample_face;
}

static esp_err_t send_face_list
(WebsocketsClient &client)
{
client.send("delete_faces"); 
// tell browser to delete all faces
face_id_node *head = st_face_list.head;
char add_face[64];
for (int i = 0; i < st_face_list.count; i++) 
// loop current faces
{
sprintf(add_face, "listface:%s", head->id_name);
client.send(add_face); //send face to browser
head = head->next;
}
}

static esp_err_t delete_all_faces(WebsocketsClient &client)
{
delete_face_all_in_flash_with_name(&st_face_list);
client.send("delete_faces");
}

void handle_message(WebsocketsClient &client, 
WebsocketsMessage msg)
{
if (msg.data() == "stream") 
{
g_state = START_STREAM;
client.send("STREAMING");
}
if (msg.data() == "detect") 
{
g_state = START_DETECT;
client.send("DETECTING");
}
if (msg.data().substring(0, 8) == "capture:") 
{
g_state = START_ENROLL;
char person[FACE_ID_SAVE_NUMBER * ENROLL_NAME_LEN] = {0,};
msg.data().substring(8).toCharArray(person, sizeof(person));
memcpy(st_name.enroll_name, person, strlen(person) + 1);
client.send("CAPTURING");
}
if (msg.data() == "recognise") 
{
g_state = START_RECOGNITION;
client.send("RECOGNISING");
}
if (msg.data().substring(0, 7) == "remove:") 
{
char person[ENROLL_NAME_LEN * FACE_ID_SAVE_NUMBER];
msg.data().substring(7).toCharArray(person, sizeof(person));
delete_face_id_in_flash_with_name(&st_face_list, person);
send_face_list(client); // reset faces in the browser
}
if (msg.data() == "delete_all") 
{
delete_all_faces(client);
}
}

void loop() {
auto client = socket_server.accept();
client.onMessage(handle_message);
dl_matrix3du_t *image_matrix = dl_matrix3du_alloc(1, 320, 240, 3);
http_img_process_result out_res = {0};
out_res.image = image_matrix->item;

send_face_list(client);
client.send("STREAMING");

while (client.available()) 
{
client.poll();

fb = esp_camera_fb_get();

if (g_state == START_DETECT 
|| g_state == START_ENROLL 
|| g_state == START_RECOGNITION)
{
out_res.net_boxes = NULL;
out_res.face_id = NULL;

fmt2rgb888(fb->buf, fb->len, fb->format, out_res.image);

out_res.net_boxes = face_detect
(image_matrix, &mtmn_config);

if (out_res.net_boxes)
{
if (align_face(out_res.net_boxes, image_matrix, 
aligned_face) == ESP_OK)
{

out_res.face_id = get_face_id(aligned_face);
last_detected_millis = millis();
if (g_state == START_DETECT) {
client.send("FACE DETECTED");
}

if (g_state == START_ENROLL)
{
int left_sample_face = do_enrollment
(&st_face_list, out_res.face_id);
char enrolling_message[64];
sprintf(enrolling_message, "SAMPLE NUMBER %d FOR %s", 
ENROLL_CONFIRM_TIMES - left_sample_face, 
st_name.enroll_name);
client.send(enrolling_message);
if (left_sample_face == 0)
{
ESP_LOGI(TAG, "Enrolled Face ID: %s", 
st_face_list.tail->id_name);
g_state = START_STREAM;
char captured_message[64];
sprintf(captured_message, "FACE CAPTURED FOR %s", 
st_face_list.tail->id_name);
client.send(captured_message);
send_face_list(client);

}
}

if (g_state == START_RECOGNITION && 
(st_face_list.count > 0))
{
face_id_node *f = recognize_face_with_name
(&st_face_list, out_res.face_id);
if (f)
{
char recognised_message[64];
sprintf(recognised_message, "RECOGNISED %s", f->id_name);
client.send(recognised_message);
}
else
{
client.send("FACE NOT RECOGNISED");
}
}
dl_matrix3d_free(out_res.face_id);
}

}
else
{
if (g_state != START_DETECT) {
client.send("NO FACE DETECTED");
}
}

if (g_state == START_DETECT && millis() - 
last_detected_millis > 500) 
{ 
// Detecting but no face detected
client.send("DETECTING");
}

}

client.sendBinary((const char *)fb->buf, fb->len);

esp_camera_fb_return(fb);
fb = NULL;
}
}

/*********************End CameraWebSockets******************/

Salvate lo sketch in una cartella e nella stessa cartella aggiungete i file camera_index.h e camera_pins.h dopo averli scaricati dai rispettivi seguenti link:

https://robotzero.one/wp-content/uploads/2019/05/camera_index.h

https://robotzero.one/wp-content/uploads/2019/05/camera_pins.h

Il file "camera_index.h" contiene le istruzioni per l'interfaccia HTML, mentre "camera_pins.h" definisce i pin della videocamera dell’ESP32-CAM. Chiudete e riaprite l’IDE di Arduino per confermare i due file nell’IDE. Selezionate la scheda con Strumenti -> Scheda -> ESP32 Wrover Module, poi con Strumenti -> Porta selezionate la porta COM (su Windows) o ttyUSB (su Mac) corrispondente al driver del convertitore seriale CH340, infine selezionate il tipo di partizione di memoria con Strumenti -> Partition Scheme -> Face Recognition…Ora controllate che sia selezionato il modello giusto di videocamera, ovvero che sia non commentata la videocamera CAMERA_MODEL_AI_THINKER e che invece siano commentati gli altri modelli di videocamera.

Inserite nel codice il vostro SSID e la PASSWORD della vostra rete Wi-Fi locale. Alimentate il circuito che avete realizzato se avete seguito la Parte 1 di questo progetto, altrimenti realizzate il circuito di Figura 4 su una breadboard sperimentale. Collegate la scheda convertitore seriale CH340 ad una porta USB del computer. Predisponete la scheda ESP32-CAM in modalità di programmazione collegando a massa il pin GPIO0, poi premete il pulsante RESET della scheda ESP32-CAM e caricate lo sketch. Terminata la compilazione e il caricamento dello sketch, togliete il collegamento a massa del pin GPIO0 e aprite il monitor seriale di Arduino e impostatelo alla velocità di trasmissione di 115200 baud. Premendo il pulsante RESET della scheda vedrete nel monitor seriale l’indirizzo IP dell’ESP32-CAM, come già mostrato nella Figura 2.

Figura 2. Indirizzo IP dell’ESP32-CAM

Copiate l’indirizzo IP nel browser di un dispositivo connesso alla rete Wi-Fi. Vedrete apparire un’immagine come quella riportata in Figura 3.

Figura 3. Interfaccia grafica per l’inserimento dei volti

Il riquadro grigio a sinistra dell’immagine contiene l’immagine del volto da registrare; la barra verde indica lo stato del rilevamento della videocamera, ovvero se un volto è stato identificato o no; nella barra sottostante si inserisce il nome della persona di cui si desidera registrare il volto; il pulsante "STREAM" avvia le riprese video della videocamera; i pulsanti "DETECT" e "RECOGNISE" devono essere premuti per predisporre la cattura del volto; il pulsante "CAPTURE" avvia la registrazione del volto; sotto la scritta "Captured Faces" viene riportato l’elenco dei nominativi delle persone il cui volto è stato registrato e cliccando sulla crocetta rossa a fianco lo si cancella. Premendo il pulsante "DELETE ALL" si cancellano tutti i volti registrati.

Nel prossimo paragrafo vedremo come realizzare la stessa applicazione utilizzando un solo codice che integra le funzioni degli sketch "CameraWebSockets" e "AccessControl".

CONTROLLO ACCESSI CON RICONOSCIMENTO FACCIALE

Abbiamo già precisato che il sistema di riconoscimento facciale presentato in questo progetto, pur svolgendo bene il suo lavoro, non è un sistema di controllo degli accessi professionale da utilizzare come un sistema di sicurezza, in quanto può essere facilmente ingannato non potendo distinguere se un volto è "vivo" o ripreso da una fotografia. Potrebbe comunque essere utilizzato come sistema di controllo secondario per aprire porte o attivare sistemi da persone che già sono state identificate da un altro sistema di controllo accessi primario. Di seguito viene riportato il codice del controllo accessi con riconoscimento facciale che chiameremo "FaceAccessControl", o come preferite. [...]

ATTENZIONE: quello che hai appena letto è solo un estratto, l'Articolo Tecnico completo è composto da ben 4632 parole ed è riservato agli ABBONATI. Con l'Abbonamento avrai anche accesso a tutti gli altri Articoli Tecnici e potrai fare il download in formato PDF eBook e Mobi per un anno. ABBONATI ORA, è semplice e sicuro.

Scarica subito una copia gratis

Scrivi un commento

Seguici anche sul tuo Social Network preferito!

Send this to a friend