【立创·实战派ESP32-S3】文档教程
第 16 章 人脸检测
乐鑫 ESP32-S3 最有特色的功能,要属它的 AI 功能:语音识别和图像识别。上一章学习了语音识别应用,本章学习图像识别应用。
图像识别应用主要参考 ESP-WHO,它是乐鑫推出的智能语音助手,它的开源地址为:
github 链接:https://github.com/espressif/esp-who gitee 链接:https://gitee.com/EspressifSystems/esp-who
乐鑫提供了深度学习开发库,位于 esp-dl 组件中。
esp-dl 参考文档:https://docs.espressif.com/projects/esp-dl/zh_CN/latest/esp32/introduction.html
本章例程,我们实现人脸检测。
16.1 使用例程
使用本例程,和使用之前的例程,稍有不同。需要先手动下载 esp-dl 组件到工程的 components 目录中,然后才可以选择目标芯片和编译下载。
在乐鑫组件官方搜索 esp-dl 下载。
进入官网后,搜索 esp-dl,然后进入页面,在右边点击 download crchive 下载。
下载后解压到 components 目录里面,并且改名称为 esp-dl。(/components/esp-dl/)
通过上面方法,下载好 esp-dl 组件后,就可以选择目标芯片、编译下载了。
开发板液晶屏进入摄像头画面后,横向放置,USB 在右,当你的脸进入画面后,就会看到一个绿色的框,识别到了人脸,并使用红、绿、蓝标出了眼睛、鼻子、嘴的位置。同时,如果识别到人脸,会在终端看到人脸、眼睛、鼻子、嘴在屏幕中的坐标位置。
受模型的局限性,只能是这个方向才能识别到人脸。
16.2 例程讲解
本章的例程是在之前第 10 章摄像头例程的基础上修改来的。第 10 章摄像头例程的原理是摄像头捕获到的画面直接传递给液晶屏显示,本章多了一个环节,摄像头捕获的图像,先传给人脸识别模型,识别动作完成后,把合成的画面再传递给液晶屏显示。
本章例程与摄像头例程相比,在 main 函数中多了几个文件,且 main.c 修改成了 main.cpp,如下图所示:
另外,多了一个 esp-dl 组件。
who_human_face_detection.cpp 文件里面有函数调用 esp-dl 库实现人脸识别。
who_ai_utils.cpp 文件里面有绘制人脸框图以及输出人脸坐标的函数。
打开 main.cpp 文件,看 app_main 主函数。
extern "C" void app_main(void)
{
bsp_i2c_init(); // I2C初始化
pca9557_init(); // IO扩展芯片初始化
bsp_lcd_init(); // 液晶屏初始化
lcd_draw_pictrue(0, 0, 320, 240, gImage_yingwu); // 显示3只鹦鹉图片
vTaskDelay(500 / portTICK_PERIOD_MS); // 延时500毫秒
bsp_camera_init(); // 摄像头初始化
app_camera_ai_lcd(); // 运行AI人脸检测任务
}
2
3
4
5
6
7
8
9
10
最后一行语句 app_camera_ai_lcd()运行人脸检测程序,其它语句都是在之前摄像头例程中实现的。
// 人脸检测
void app_camera_ai_lcd(void)
{
xQueueLCDFrame = xQueueCreate(2, sizeof(camera_fb_t *));
xQueueAIFrame = xQueueCreate(2, sizeof(camera_fb_t *));
xTaskCreatePinnedToCore(task_process_camera, "task_process_camera", 3 * 1024, NULL, 5, NULL, 1);
xTaskCreatePinnedToCore(task_process_lcd, "task_process_lcd", 4 * 1024, NULL, 5, NULL, 0);
xTaskCreatePinnedToCore(task_process_ai, "task_process_ai", 4 * 1024, NULL, 5, NULL, 0);
}
2
3
4
5
6
7
8
9
10
在上面的代码中,创建了两个队列,并创建了三个任务。
xQueueAIFrame 队列用于摄像头捕获到一帧图像后传递消息给 AI,xQueueLCDFrame 队列用于 AI 识别完一帧后传递消息给液晶屏。
task_process_camera 任务中,获取摄像头图像,获取到一帧图像后,发送队列消息 xQueueAIFrame 。
// 摄像头处理任务
static void task_process_camera(void *arg)
{
while (true)
{
camera_fb_t *frame = esp_camera_fb_get();
if (frame)
xQueueSend(xQueueAIFrame, &frame, portMAX_DELAY);
}
}
2
3
4
5
6
7
8
9
10
task_process_ai 任务中,接收到 xQueueAIFrame 队列消息后,开始识别图像,识别完图像后,发送队列消息 xQueueLCDFrame。
// AI处理任务
static void task_process_ai(void *arg)
{
camera_fb_t *frame = NULL;
HumanFaceDetectMSR01 detector(0.3F, 0.3F, 10, 0.3F);
HumanFaceDetectMNP01 detector2(0.4F, 0.3F, 10);
while (true)
{
if (gEvent)
{
if (xQueueReceive(xQueueAIFrame, &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() > 0)
{
draw_detection_result((uint16_t *)frame->buf, frame->height, frame->width, detect_results);
print_detection_result(detect_results);
}
}
if (xQueueLCDFrame)
{
xQueueSend(xQueueLCDFrame, &frame, portMAX_DELAY);
}
else if (gReturnFB)
{
esp_camera_fb_return(frame);
}
else
{
free(frame);
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
task_process_lcd 任务中,接收到 xQueueLCDFrame 队列消息后,把图片显示到液晶屏上。
// lcd处理任务
static void task_process_lcd(void *arg)
{
camera_fb_t *frame = NULL;
while (true)
{
if (xQueueReceive(xQueueLCDFrame, &frame, portMAX_DELAY))
{
lcd_draw_bitmap(0, 0, frame->width, frame->height, (uint16_t *)frame->buf);
esp_camera_fb_return(frame);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
以上就是例程的实现过程。
最后再解释一下这里.c 和.cpp 文件混用的情况。
因为 task_process_ai()
任务函数使用 C++ 实现,所以装此函数的文件,必须是.cpp 文件,它对应的头文件,就是.hpp 文件。
因为 app_main()
函数中,需要调用这个 C++ 文件中的 app_camera_ai_lcd()
函数。所以,需要把 main.c 文件改成 main.cpp 文件。
因为 C++ 文件中的 app_camera_ai_lcd()
函数中需要调用 esp32_s3_szp.c 文件中的 lcd_draw_bitmap()
函数,而且 esp32_s3_szp.c 文件中的其它 c 函数还需要被其它 c 文件正常调用,所以需要在 esp32_s3_szp.h 文件中使用下面语句把内容包含以来。
#ifdef __cplusplus
extern "C"
{
#endif
// 这里是esp32_s3_szp.h文件中的各种内容
#ifdef __cplusplus
}
#endif
2
3
4
5
6
7
8
9
10
因为在 main.cpp 文件中的 app_main()
函数中,还需要调用其它的 c 函数,所以需要在 app_main 名称前面加 extern "C"
。
16.3 例程制作
把第 10 章例程【07-lcd_camera】复制粘贴为副本,把副本重命名为【13-human_face_detection】作为本章例程。
使用 VSCode 打开工程,先在一级目录的 CMakeLists.txt 文件中修改工程名称为 human_face_detection。
project(human_face_detection)
本例程需要使用 esp-dl 组件,按照本章第 1 小节方法下载好组件。
使用 VSCode 打开 esp-who 工程,打开 examples/human_face_detection/lcd 中的 app_main.cpp 文件。
从它的 app_main 函数中可以看到,它分别执行了摄像头、AI、液晶屏 3 个任务程序。它的原理是先把摄像头获取到的图片数据给图像识别模型,然后图像识别模型再把识别后的图像给液晶屏显示。
我们需要复制 esp-who 中的 4 个 C++ 文件到我们的例程 main 文件夹下,这 4 个文件分别是:who_ai_utils.cpp、who_ai_utils.hpp、who_human_face_detection.cpp、who_human_face_detection.hpp。
路径是:esp-who\components\modules\ai
app_main 主函数中,需要调用 who_human_face_detection.cpp 中的文件,所以我们需要把 main.c 修改成 main.cpp 文件。
然后把 app_main 函数中,最后一条语句,由原来的 app_camera_lcd,改成 app_camera_ai_lcd,表示多了一个 ai 环节。
点击打开 esp32_s3_szp.c 文件,把摄像头的 3 个函数定义,剪切到 who_human_face_detection.cpp 文件中,放到最后面。这 3 个函数分别是:task_process_lcd()
、task_process_camera()
、app_camera_lcd()
。把这几个函数用到的 xQueueLCDFrame 队列句柄定义在 esp32_s3_szp.c 文件中删除。
剪切过来后,把 app_camera_lcd()
名称修改为 app_camera_ai_lcd()
,这个函数就是我们需要在主函数中调用的函数。
在 who_human_face_detection.cpp 文件中找到 register_human_face_detection()
函数,把其中的这条语句复制粘贴到 app_camera_ai_lcd()
函数中。
xTaskCreatePinnedToCore(task_process_handler, TAG, 4 * 1024, NULL, 5, NULL, 0);
然后把 register_human_face_detection()
函数定义删除,这个函数中还创建了一个 task_event_handler 任务,把这个任务函数也删除。
在 app_camera_ai_lcd()
函数中,把刚才复制粘贴进去的语句修改一下,把第 1 个参数 task_process_handler 修改成 task_process_ai。把第 2 个参数 TAG,修改成 "task_process_ai"
。
在 app_camera_ai_lcd()
函数中,再创建一个 xQueueAIFrame 队列。
修改好的 app_camera_ai_lcd()
函数如下所示:
void app_camera_ai_lcd(void)
{
xQueueLCDFrame = xQueueCreate(2, sizeof(camera_fb_t *));
xQueueAIFrame = xQueueCreate(2, sizeof(camera_fb_t *));
xTaskCreatePinnedToCore(task_process_camera, "task_process_camera", 3 * 1024, NULL, 5, NULL, 1);
xTaskCreatePinnedToCore(task_process_lcd, "task_process_lcd", 4 * 1024, NULL, 5, NULL, 0);
xTaskCreatePinnedToCore(task_process_ai, TAG, 4 * 1024, NULL, 5, NULL, 0);
}
2
3
4
5
6
7
8
9
因为刚才把 task_process_handler 改成了 task_process_ai,所以我们在前面找到这个函数,把任务函数的定义名称也修改成 task_process_ai。
此 cpp 文件前面有 4 个队列句柄定义,如下:
static QueueHandle_t xQueueFrameI = NULL;
static QueueHandle_t xQueueEvent = NULL;
static QueueHandle_t xQueueFrameO = NULL;
static QueueHandle_t xQueueResult = NULL;
2
3
4
把这 4 个定义删除,重新定义 2 个在 app_camera_ai_lcd()
函数中用到的队列句柄,如下:
static QueueHandle_t xQueueLCDFrame = NULL;
static QueueHandle_t xQueueAIFrame = NULL;
2
在 app_camera_ai_lcd()
函数中创建的 3 个任务处理函数定义,都在这个文件里面。现在我们依次修改这 3 个任务函数。
在摄像头任务函数中,把 xQueueLCDFrame 修改为 xQueueAIFrame,因为摄像头的图像现在要发给 AI 模型,而不是液晶屏了。修改后如下所示:
static void task_process_camera(void *arg)
{
while (true)
{
camera_fb_t *frame = esp_camera_fb_get();
if (frame)
xQueueSend(xQueueAIFrame, &frame, portMAX_DELAY);
}
}
2
3
4
5
6
7
8
9
液晶屏任务函数中,使用到的 esp_lcd_panel_draw_bitmap()
液晶屏绘制函数中的第 1 个参数,此 cpp 文件中没有定义,它在 esp32_s3_szp.c 文件中定义,所以我们需要在 esp32_s3_szp.c 文件中定义一个绘制函数,然后在这里调用就可以。
在 esp32_s3_szp.c 文件中定义定义如下绘制函数。
void lcd_draw_bitmap(int x_start, int y_start, int x_end, int y_end, const void *color_data)
{
esp_lcd_panel_draw_bitmap(panel_handle, x_start, y_start, x_end, y_end, color_data);
}
2
3
4
在 esp32_s3_szp.h 文件中声明这个函数。
void lcd_draw_bitmap(int x_start, int y_start, int x_end, int y_end, const void *color_data);
在 who_human_face_detection.cpp 文件中,加入头文件 esp32_s3_szp.h。
#include "esp32_s3_szp.h"
在 task_process_lcd 任务函数中就可以调用了,如下代码所示:
static void task_process_lcd(void *arg)
{
camera_fb_t *frame = NULL;
while (true)
{
if (xQueueReceive(xQueueLCDFrame, &frame, portMAX_DELAY))
{
lcd_draw_bitmap(0, 0, frame->width, frame->height, (uint16_t *)frame->buf);
esp_camera_fb_return(frame);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
此文件是 cpp 文件,调用的 lcd_draw_bitmap 函数在 c 文件中,所以需要在 esp32_s3_szp.h 文件中,添加下面的语句在文件的开始和末尾处。
#ifdef __cplusplus
extern "C"
{
#endif
// 这里是esp32_s3_szp.h文件中的各种内容
#ifdef __cplusplus
}
#endif
2
3
4
5
6
7
8
9
10
图像 AI 任务中,我们先把条件编译语句去掉,让它固定工作在 TWO_STAGE_ON 状态下,同时也把前面的宏定义删除。
第 1 个 if 条件语句中,把队列接收中的 xQueueFrameI 参数修改为 xQueueAIFrame,摄像头捕获完数据会发送这个队列过来。
第 2 个 if 条件语句中,把 if 条件和发送队列的参数中的 xQueueFrameO 修改为 xQueueLCDFrame,在处理完图像后,会发送这个队列消息给液晶屏。
再往下,只保留 gReturnFB 条件语句,其它两个删除。
函数中的 is_detected 定义也用不着了,删除相关两处地方。
最后如下所示:
static void task_process_ai(void *arg)
{
camera_fb_t *frame = NULL;
HumanFaceDetectMSR01 detector(0.3F, 0.3F, 10, 0.3F);
HumanFaceDetectMNP01 detector2(0.4F, 0.3F, 10);
while (true)
{
if (gEvent)
{
if (xQueueReceive(xQueueAIFrame, &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() > 0)
{
draw_detection_result((uint16_t *)frame->buf, frame->height, frame->width, detect_results);
print_detection_result(detect_results);
}
}
if (xQueueLCDFrame)
{
xQueueSend(xQueueLCDFrame, &frame, portMAX_DELAY);
}
else if (gReturnFB)
{
esp_camera_fb_return(frame);
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
任务中用到了 esp-camera 组件函数,需要在前面包含头文件。
#include "esp_camera.h"
who_human_face_detection.cpp 文件中的 TAG 定义没有用到,删除。
在 who_human_face_detection.hpp 文件中,把 register_human_face_detection 函数的声明,修改成 app_camera_ai_lcd 函数的声明。
void app_camera_ai_lcd(void);
在 main.cpp 文件中,添加 who_human_face_detection.hpp 头文件,这样,在 app_main 函数中就可以正常调用它了。
#include "who_human_face_detection.hpp"
因为 app_main 函数中,不仅调用了 cpp 中的文件,还需要调用 c 中的文件,所以需要在 app_main 函数前面添加 extern "C"
语句,如下所示:
extern "C" void app_main(void)
{
bsp_i2c_init(); // I2C初始化
pca9557_init(); // IO扩展芯片初始化
bsp_lcd_init(); // 液晶屏初始化
lcd_draw_pictrue(0, 0, 320, 240, gImage_yingwu); // 显示3只鹦鹉图片
vTaskDelay(500 / portTICK_PERIOD_MS); // 延时500毫秒
bsp_camera_init(); // 摄像头初始化
app_camera_ai_lcd(); // 让摄像头画面显示到LCD上
}
2
3
4
5
6
7
8
9
10
在 main 下的 CMakeLists.txt 文件中,添加 main 下面的所有 C 文件,然后把 main.c 改成 main.cpp。
这个工程编译后,默认的分区表已经不能满足 bin 文件的大小,所以这里需要使用自定义分区表,我们把【09-wifi_scan_connect】例程中的分区表文件复制到本例程中。然后点击打开分区表文件,把 factory 大小修改为 3M。
在 sdkconfig.default 中,添加使用自定义分区表的语句。
CONFIG_PARTITION_TABLE_CUSTOM=y
至此,文件就全部修改好了。
先选择目标芯片,再选择串口号和串口方式,点击“一键三联”按钮就可以测试了。