第五十九章 人臉識別實驗
人臉識別是一種基於人的臉部特徵信息進行身份識別的一種生物識別技術。它使用攝像機或攝像頭採集含有人臉的圖像或視頻流,並自動在圖像中檢測和跟蹤人臉,進而對檢測到的人臉進行臉部識別的一系列相關技術。本章,我們使用樂鑫AI庫來實現人臉識別功能。
本章分為如下幾個部分:
59.1 硬件設計
59.2 軟件設計
59.3 下載驗證
59.1 硬件設計
1.例程功能
本章實驗功能簡介:使用樂鑫官方的ESP32-WHO AI庫對OV2640和OV5640攝像頭輸出的數據進行人臉識別。當長按BOOT按鍵時,錄入當前對焦的人臉;當單擊BOOT按鍵時,識別當前人臉,識別時需和人臉倉庫中的人臉匹配;當雙擊BOOT按鍵時,刪除當前人臉,但前提是這個張人臉之前已經在人臉倉庫當中。
2.硬件資源
1)LED燈
LED-IO1
2)XL9555
IIC_INT-IO0(需在P5連接IO0)
IIC_SDA-IO41
IIC_SCL-IO42
3)SPILCD
CS-IO21
SCK-IO12
SDA-IO11
DC-IO40(在P5端口,使用跳線帽將IO_SET和LCD_DC相連)
PWR- IO1_3(XL9555)
RST- IO1_2(XL9555)
4)CAMERA
OV_SCL-IO38
OV_SDA- IO39
VSYNC- IO47
HREF- IO48
PCLK- IO45
D0- IO4
D1- IO5
D2- IO6
D3- IO7
D4- IO15
D5- IO16
D6- IO17
D7- IO18
RESET-IO0_5(XL9555)
PWDN-IO0_4(XL9555)
3.原理圖
本章實驗使用的KPU為ESP32-S3的內部資源,因此並沒有相應的連接原理圖。
59.2 軟件設計
59.2.1 程序流程圖
程序流程圖能幫助我們更好的理解一個工程的功能和實現的過程,對學習和設計工程有很好的主導作用。下面看看本實驗的程序流程圖:
圖59.2.1.1 程序流程圖
59.2.2 程序解析
在本章節中,我們將重點關注兩個文件:esp_face_recognition.cpp和esp_face_ recognition.hpp。其中,esp_face_recognition.hpp主要聲明瞭esp_face_recognition函數,其內容相對簡單,因此我們暫時不作詳細解釋。本章節的核心關注點是esp_face_recognition.cpp文件中的函數。
接下來,我們將詳細解析esp_face_ recognition_ai_strat函數的工作原理。
/**
* @brief AI圖像數據開啓
* @param 無
* @retval 1:創建失敗;0:創建成功
*/
uint8_t esp_face_recognition_ai_strat(void)
{
/* 創建隊列及任務 */
xQueueFrameO = xQueueCreate(5, sizeof(camera_fb_t *));
xQueueAIFrameO = xQueueCreate(5, sizeof(camera_fb_t *));
xQueueKeyState = xQueueCreate(1, sizeof(int *));
xQueueEventLogic = xQueueCreate(1, sizeof(int *));
xMutex = xSemaphoreCreateMutex();
/* 初始化按鍵 */
esp_key_init(GPIO_NUM_0);
/* 創建任務 */
xTaskCreatePinnedToCore(esp_key_trigger,"esp_key_scan",1024,NULL,5, NULL,0);
xTaskCreatePinnedToCore(esp_event_generate, "event_logic",1024,NULL,
5, NULL,0);
xTaskCreatePinnedToCore(esp_camera_process_handler, "esp_camera_process ",
4 * 1024, NULL, 5, &camera_task_handle, 1);
xTaskCreatePinnedToCore(esp_ai_process_handler, "esp_ai_process_handler",
6 * 1024, NULL, 5, &ai_task_handle, 1);
xTaskCreatePinnedToCore(esp_task_event_handler, "esp_task_event_handler",
4 * 1024, NULL, 5, NULL, 1);
if (xQueueFrameO != NULL
|| xQueueAIFrameO != NULL
|| xQueueEventLogic != NULL
|| camera_task_handle != NULL
|| ai_task_handle != NULL)
{
return 0;
}
return 1;
}
在上述函數中,我們創建了四個消息隊列以遞交消息,並設置了一個互斥鎖來防止任務優先級翻轉。此外,還定義了五個任務:檢測KEY按鍵狀態(用於判斷按鍵處於哪種狀態,如長按、點擊和雙擊等)、事件任務、攝像頭獲取任務、AI處理任務和按鍵掃描任務任務。
下面作者分為介紹這些任務的實現流程,如下:
1,檢測KEY按鍵狀態任務
/**
* @brief 按鍵掃描(判斷短按、長按及雙擊狀態)
* @param ticks_to_wait:等待時間
* @retval 返回按鍵狀態
*/
int esp_key_scan(TickType_t ticks_to_wait)
{
gpio_num_t io_num;
BaseType_t press_key = pdFALSE;
BaseType_t lift_key = pdFALSE;
int64_t backup_time = 0;
int64_t interval_time = 0;
static int64_t last_time = 0;
while (1)
{
xQueueReceive(gpio_evt_queue, &io_num, ticks_to_wait);
if (gpio_get_level(io_num) == 0)
{
press_key = pdTRUE;
backup_time = esp_timer_get_time();
interval_time = backup_time - last_time;
}
else if (press_key)
{
lift_key = pdTRUE;
last_time = esp_timer_get_time();
backup_time = last_time - backup_time;
}
if (press_key & lift_key)
{
press_key = pdFALSE;
lift_key = pdFALSE;
if (backup_time > LONG_PRESS_THRESH)
{
return KEY_LONG_PRESS;
}
else
{
if ((interval_time < DOUBLE_CLICK_THRESH)&&(interval_time > 0))
return KEY_DOUBLE_CLICK;
else
return KEY_SHORT_PRESS;
}
}
}
}
/**
* @brief 按鍵任務
* @param arg:未使用
* @retval 無
*/
static void esp_key_trigger(void *arg)
{
arg = arg;
int ret = 0;
while (1)
{
ret = esp_key_scan(portMAX_DELAY);
xQueueOverwrite(xQueueKeyState, &ret);
}
vTaskDelete(NULL);
}
在上述任務函數中,首先通過調用esp_key_scan函數獲取按鍵的狀態,包括長按、雙擊或點擊等。然後,使用FreeRTOS API函數xQueueOverwrite將消息發送到事件任務中。
2,事件任務
/**
* @brief 事件生成任務
* @param arg:未使用
* @retval 無
*/
void esp_event_generate(void *arg)
{
arg = arg;
static key_state_t key_state;
while (1)
{
/* 接收狀態 */
xQueueReceive(xQueueKeyState, &key_state, portMAX_DELAY);
/* 判斷狀態 */
switch (key_state)
{
case KEY_SHORT_PRESS: /* 短按狀態 */
recognizer_state = RECOGNIZE;
break;
case KEY_LONG_PRESS: /* 長按狀態 */
recognizer_state = ENROLL;
break;
case KEY_DOUBLE_CLICK: /* 雙擊狀態 */
recognizer_state = DELETE;
break;
default:
recognizer_state = DETECT;
break;
}
/* 發送狀態 */
xQueueSend(xQueueEventLogic, &recognizer_state, portMAX_DELAY);
}
}
上述任務函數調用xQueueReceive函數接收KEY的消息,然後根據消息判斷處於哪個事件狀態,最後調用xQueueSend函數發送事件消息至按鍵掃描任務處理。
3,按鍵掃描任務
/**
* @brief 按鍵掃描任務
* @param arg:未使用
* @retval 無
*/
static void esp_task_event_handler(void *arg)
{
arg = arg;
recognizer_state_t _gEvent;
while (1)
{
xQueueReceive(xQueueEventLogic, &(_gEvent), portMAX_DELAY);
xSemaphoreTake(xMutex, portMAX_DELAY);
gEvent = _gEvent;
xSemaphoreGive(xMutex);
}
}
該任務函數就是為了防止優先級翻轉問題。
4,攝像頭任務
/**
* @brief 攝像頭圖像數據獲取任務
* @param arg:未使用
* @retval 無
*/
static void esp_camera_process_handler(void *arg)
{
arg = arg;
camera_fb_t *camera_frame = NULL;
while (1)
{
/* 獲取攝像頭圖像 */
camera_frame = esp_camera_fb_get();
if (camera_frame)
{
/* 以隊列的形式發送 */
xQueueSend(xQueueFrameO, &camera_frame, portMAX_DELAY);
}
}
}
該任務函數最主要的作用是獲取攝像頭的圖像數據,併發送圖像數據至AI處理任務。
5,AI處理任務函數
/**
* @brief 攝像頭圖像數據傳入AI處理任務
* @param arg:未使用
* @retval 無
*/
static void esp_ai_process_handler(void *arg)
{
arg = arg;
camera_fb_t *frame = NULL;
HumanFaceDetectMSR01 detector(0.3F, 0.3F, 10, 0.3F);
HumanFaceDetectMNP01 detector2(0.4F, 0.3F, 10);
FaceRecognition112V1S16 *recognizer = new FaceRecognition112V1S16();
show_state_t frame_show_state = SHOW_STATE_IDLE;
recognizer_state_t _gEvent;
recognizer->set_partition(ESP_PARTITION_TYPE_DATA,
ESP_PARTITION_SUBTYPE_ANY, "fr");
recognizer->set_ids_from_flash();
while(1)
{
xSemaphoreTake(xMutex, portMAX_DELAY);
_gEvent = gEvent;
gEvent = DETECT;
xSemaphoreGive(xMutex);
if (_gEvent)
{
bool is_detected = false;
if (xQueueReceive(xQueueFrameO, &frame, portMAX_DELAY))
{
std::list<dl::detect::result_t> &detect_candidates =
detector.infer((uint16_t *)frame->buf, {(int)frame->height,
(int)frame->width, 3});
std::list<dl::detect::result_t> &detect_results =
detector2.infer((uint16_t *)frame->buf,
{(int)frame->height,
(int)frame->width, 3},
detect_candidates);
if (detect_results.size() == 1)
is_detected = true;
if (is_detected)
{
switch (_gEvent)
{
/* 註冊 */
case ENROLL:
recognizer->enroll_id((uint16_t *)frame->buf,
{(int)frame->height,
(int)frame->width, 3},
detect_results.front().keypoint,
"", true);
ESP_LOGW("ENROLL", "ID %d is enrolled",
recognizer->get_enrolled_ids().back().id);
frame_show_state = SHOW_STATE_ENROLL;
break;
/* 識別 */
case RECOGNIZE:
recognize_result = recognizer->recognize(
(uint16_t *)frame->buf,
{(int)frame->height,
(int)frame->width, 3},
detect_results.front().keypoint);
print_detection_result(detect_results);
if (recognize_result.id > 0)
ESP_LOGI("RECOGNIZE", "Similarity: %f, Match ID: %d",
recognize_result.similarity, recognize_result.id);
else
ESP_LOGE("RECOGNIZE", "Similarity: %f, Match ID: %d",
recognize_result.similarity, recognize_result.id);
frame_show_state = SHOW_STATE_RECOGNIZE;
break;
/* 刪除 */
case DELETE:
vTaskDelay(10);
recognizer->delete_id(true);
ESP_LOGE("DELETE", "% d IDs left",
recognizer->get_enrolled_id_num());
frame_show_state = SHOW_STATE_DELETE;
break;
default:
break;
}
}
if (frame_show_state != SHOW_STATE_IDLE)
{
static int frame_count = 0;
switch (frame_show_state)
{
/* 刪除圖像 */
case SHOW_STATE_DELETE:
esp_rgb_printf(frame, RGB565_MASK_RED,
"%d IDs left",
recognizer->get_enrolled_id_num());
break;
/* 圖像識別 */
case SHOW_STATE_RECOGNIZE:
if (recognize_result.id > 0)
esp_rgb_printf(frame, RGB565_MASK_GREEN,
"ID %d",
recognize_result.id);
else
esp_rgb_print(frame, RGB565_MASK_RED, "who ?");
break;
/* 圖像註冊 */
case SHOW_STATE_ENROLL:
esp_rgb_printf(frame, RGB565_MASK_BLUE,
"Enroll: ID %d",
recognizer->get_enrolled_ids().back().id);
break;
default:
break;
}
if (++frame_count > FRAME_DELAY_NUM)
{
frame_count = 0;
frame_show_state = SHOW_STATE_IDLE;
}
}
if (detect_results.size())
{
draw_detection_result((uint16_t *)frame->buf,
frame->height, frame->width, detect_results);
}
}
if (xQueueAIFrameO)
{
xQueueSend(xQueueAIFrameO, &frame, portMAX_DELAY);
}
else if (gReturnFB)
{
esp_camera_fb_return(frame);
}
else
{
free(frame);
}
}
}
}
上述任務函數主要負責處理圖像數據,並將其提交給AI庫進行處理。根據按鍵的不同狀態,系統會執行不同的操作:
1,如果長按按鍵,系統會將人臉數據傳入分區表。
2,如果點擊按鍵,系統將當前識別的人臉與人臉庫中的人臉進行對比。如果匹配成功,系統將提示人臉識別成功的信息。
3,如果雙擊按鍵,系統同樣會將當前識別的人臉與人臉庫中的人臉進行對比。如果匹配成功,系統不僅會提示人臉識別成功的信息,還會從人臉庫中刪除匹配的人臉圖像數據。
59.3 下載驗證
如果在檢測過程中發現人臉,需要長按BOOT按鍵,將人臉信息錄入人臉存儲區。當再次檢測到人臉時,只需短按BOOT按鍵,系統就會識別當前的人臉(與存儲區的人臉數據進行匹配)。如果識別成功,該系統會將人臉識別處理的圖像數據顯示在LCD上,否則,提示人臉識別失敗,如下圖所示。
圖59.3.1 人臉識別效果圖