| 第五十九章 人脸识别实验   人脸识别是一种基于人的脸部特征信息进行身份识别的一种生物识别技术。它使用摄像机或摄像头采集含有人脸的图像或视频流,并自动在图像中检测和跟踪人脸,进而对检测到的人脸进行脸部识别的一系列相关技术。本章,我们使用乐鑫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函数的工作原理。 /**  * @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 人脸识别效果图 
 |