简介:这份资源面向嵌入式开发与物联网视觉应用的学习者,提供基于ESP32-S3芯片实现人脸检测与识别的完整工程代码。ESP32-S3集成Wi-Fi、蓝牙与神经网络加速单元,适合在低功耗边缘设备上运行轻量级机器学习算法,资源涵盖图像采集、预处理、特征提取、人脸检测与识别等环节的工程实现,并涉及模型量化、内存管理与功耗控制等嵌入式优化思路。压缩包共418个文件,约25.82MB,以h/hpp头文件、c/cpp源码、md说明文档、npy与onnx模型数据、py脚本及a/so静态库为主,另含Makefile、CMakeLists.txt、sdkconfig、partitions.csv等构建与分区配置,目录结构清晰,便于按模块查阅。目前已有2090人学习下载,适合希望快速上手ESP32-S3人脸识别项目、研究嵌入式端模型部署与性能调优的开发者参考。
1. 从一块不到百元的开发板说起:esp32S3 人脸识别到底能跑出什么效果
前阵子帮一个做智能门禁的团队看方案,他们原本用树莓派加摄像头做本地人脸比对,成本压不下来,功耗也高。后来换了一块 esp32S3 开发板,带摄像头模组,整机成本直接砍到原来的三分之一,离线识别响应在 1 秒以内。这就是 esp32S3 人脸识别最典型的落地场景:不是替代服务器级的人脸识别,而是在门禁、考勤机、智能开关这类低功耗、低成本、离线优先的设备上,把“检测到人脸并判断是不是同一个人”这件事跑通。
esp32S3 本身带向量指令加速,配合 ESP-DL 和 esp-who 这套官方推理框架,可以在没有网络的情况下完成人脸检测和特征比对。它适合谁?适合做嵌入式 AI 落地的工程师、想从 STM32 转 AI 视觉的开发者,以及需要快速验证离线人脸方案的产品团队。但要注意,它跑不了 ArcFace 那种大模型,特征维度通常压到 512 甚至更低,识别距离和角度都有边界。下面我把这套东西从环境搭建到参数调优拆一遍,能抄的代码直接给,踩过的坑也一并说清。
2. 环境搭建与模型选型:为什么不是随便拿个模型就能跑
2.1 开发环境的三层结构
esp32S3 人脸识别不是装个 Arduino 库就能完事的。它依赖三层:底层是 ESP-IDF,中间是 esp-who 应用框架,上层才是你的人脸识别逻辑。很多人翻车就翻在直接用 Arduino IDE 装了个 esp32 包,结果发现 esp-who 根本编不过去。
我一般会这样搭环境:
# 克隆 ESP-IDF,注意版本要和 esp-who 匹配 git clone -b v5.1.2 --recursive https://github.com/espressif/esp-idf.git cd esp-idf ./install.sh esp32s3 . ./export.sh # 克隆 esp-who,放在和 esp-idf 同级目录 cd .. git clone --recursive https://github.com/espressif/esp-who.git cd esp-who/examples/human_face_detection idf.py set-target esp32s3 idf.py menuconfig这里有几个参数必须确认:Component config → ESP-WHO Configuration → Camera Configuration里选对摄像头型号,常见的是 OV2640 和 OV3660,引脚定义不一样。ESP32S3-Specific里要把 PSRAM 打开,因为人脸检测模型和图像缓冲区都吃内存,不开 PSRAM 跑不起来。
逻辑说明:ESP-IDF 提供编译工具链和底层驱动,esp-who 封装了摄像头初始化、图像预处理和模型推理的调用接口。set-target esp32s3会切换编译目标,menuconfig里配置的摄像头参数会生成对应的 sdkconfig 文件,编译时直接读这个配置。
2.2 模型选型:检测模型和识别模型分开看
esp-who 里人脸相关模型分两类:检测模型(MTMN)和识别模型(MFN)。MTMN 负责从图像里框出人脸,MFN 负责把框出来的人脸转成特征向量。选型时重点看三个参数:输入分辨率、模型大小、推理耗时。
| 模型 | 输入尺寸 | Flash 占用 | PSRAM 占用 | 单帧耗时(esp32s3 @240MHz) |
|---|---|---|---|---|
| MTMN 轻量 | 240x240 | 约 180KB | 约 300KB | 约 120ms |
| MTMN 标准 | 320x240 | 约 350KB | 约 500KB | 约 220ms |
| MFN 轻量 | 56x56 | 约 120KB | 约 200KB | 约 80ms |
如果做门禁这种近距离场景,MTMN 轻量加 MFN 轻量就够了,整体响应能压在 300ms 以内。如果要做多人同时检测,得上标准版,但帧率会掉到 3 帧左右。这里有个反直觉的点:分辨率不是越高越好,320x240 以上对识别准确率提升很有限,但耗时翻倍。
2.3 摄像头引脚配置的坑
不同开发板的摄像头引脚定义不一样,menuconfig 里选错型号直接黑屏。我一般会先跑一个摄像头测试例程,确认图像能出来再上人脸模型。测试代码如下:
// 摄像头初始化关键参数 camera_config_t config = { .pin_pwdn = -1, .pin_reset = -1, .pin_xclk = 15, .pin_sccb_sda = 4, .pin_sccb_scl = 5, .pin_d7 = 16, .pin_d6 = 17, .pin_d5 = 18, .pin_d4 = 19, .pin_d3 = 20, .pin_d2 = 21, .pin_d1 = 22, .pin_d0 = 23, .pin_vsync = 25, .pin_href = 26, .pin_pclk = 27, .xclk_freq_hz = 20000000, .pixel_format = PIXFORMAT_RGB565, .frame_size = FRAMESIZE_QVGA, // 320x240 .jpeg_quality = 12, .fb_count = 2, // 双缓冲,避免丢帧 };参数说明:xclk_freq_hz一般设 20MHz,太高会花屏;pixel_format用人脸检测时选 RGB565,因为模型输入是 RGB;fb_count设 2 可以双缓冲,设 1 在推理时会丢帧。如果图像偏绿或偏紫,检查pin_sccb_sda和pin_sccb_scl有没有接反。
3. 人脸检测与特征提取的代码落地:从图像到特征向量
3.1 检测流程的四个阶段
esp32S3 上跑人脸检测,实际是四步:摄像头取帧、图像缩放、MTMN 推理、后处理画框。很多人以为模型推理最耗时,其实图像缩放和后处理也占不少时间。我实测过,QVGA 图像缩放到 240x240 大概 30ms,MTMN 推理 120ms,后处理 20ms,加起来接近 170ms。
核心调用逻辑如下:
// 人脸检测主循环 while (1) { camera_fb_t *fb = esp_camera_fb_get(); if (!fb) { continue; } // 将 RGB565 转为模型需要的格式 dl_matrix3du_t *image_matrix = dl_matrix3du_alloc(1, fb->width, fb->height, 3); fmt2rgb888(fb->buf, fb->len, fb->format, image_matrix->item); // 运行 MTMN 检测 box_array_t *net_boxes = face_detect(image_matrix, &mtmn_config); if (net_boxes) { // 遍历检测到的人脸 for (int i = 0; i < net_boxes->len; i++) { // 截取人脸区域,送入 MFN 提取特征 dl_matrix3du_t *aligned_face = align_face(image_matrix, &net_boxes->box[i]); dl_matrix3du_t *feature = face_recognize(aligned_face); // feature 就是 512 维特征向量,后续做比对 } free(net_boxes->box); free(net_boxes); } dl_matrix3du_free(image_matrix); esp_camera_fb_return(fb); }逻辑说明:face_detect返回的是检测框数组,每个框包含人脸位置和关键点。align_face根据关键点做人脸对齐,这一步很关键,不对齐的话识别率会掉 20% 以上。face_recognize输出特征向量,后续可以存到 Flash 里做比对。
参数说明:mtmn_config里有三个阈值——min_face控制最小人脸尺寸,设太小会误检,设太大会漏检,一般设 40 像素;score_threshold是置信度阈值,默认 0.5,光线差的时候可以降到 0.3;nms_threshold是非极大值抑制阈值,控制重叠框的合并,一般 0.3 到 0.5 之间。
3.2 特征比对与阈值设定
特征提取出来后,比对就是算欧氏距离或余弦相似度。esp-who 里默认用欧氏距离,距离越小越像。但阈值设多少,直接决定误识率和拒识率。
// 特征比对示例 float compute_distance(dl_matrix3du_t *feat1, dl_matrix3du_t *feat2) { float sum = 0; for (int i = 0; i < 512; i++) { float diff = feat1->item[i] - feat2->item[i]; sum += diff * diff; } return sqrt(sum); } // 判定逻辑 float dist = compute_distance(feature, stored_feature); if (dist < 0.8f) { // 判定为同一人 } else { // 判定为陌生人 }这里 0.8 是我在门禁场景下调出来的经验值。如果做考勤,可以放宽到 1.0,减少拒识;如果做支付级验证,得压到 0.5 以下,但拒识率会明显上升。注意,不同批次模型输出的特征分布不一样,换模型后阈值必须重新标定。
3.3 把特征存到 Flash
识别出来的人脸特征需要持久化,否则重启就丢了。esp32S3 一般用 NVS 或 SPIFFS 存。NVS 适合存少量特征,SPIFFS 适合存多个人脸模板。
// 使用 NVS 存储人脸特征 nvs_handle_t my_handle; nvs_open("face_db", NVS_READWRITE, &my_handle); nvs_set_blob(my_handle, "user_001", feature->item, 512 * sizeof(float)); nvs_commit(my_handle); nvs_close(my_handle);参数说明:nvs_set_blob的 key 用用户 ID,value 是 512 个 float,总共 2KB。NVS 分区默认大小有限,存 10 个人脸模板没问题,再多就要改分区表。注意 float 的字节序,跨设备迁移时要做转换。
4. 避坑与排查:那些让我熬夜的翻车现场
4.1 图像偏色或花屏
现象:摄像头出来的图像整体偏绿,或者上半部分花屏。 原因:XCLK 频率不对,或者 SCCB 引脚接触不良。OV2640 对时钟敏感,20MHz 是安全值,但有些板子走线差,得降到 10MHz。 解决:先把xclk_freq_hz降到 10MHz 试,如果还花屏,用万用表量 SCCB 引脚通断,确认没有虚焊。
4.2 检测不到人脸
现象:图像正常,但 MTMN 一直返回空。 原因:min_face设太大,或者图像格式不对。MTMN 要求输入是 RGB888,如果直接喂 RGB565 数据,检测全废。 解决:检查fmt2rgb888有没有调用,确认min_face调到 40 以下,光线暗的时候把score_threshold降到 0.3。
4.3 识别率忽高忽低
现象:同一个人,有时候能识别,有时候识别成陌生人。 原因:人脸对齐没做好,或者特征存储时精度丢失。align_face依赖关键点检测,关键点不准,对齐就歪。 解决:在align_face后加一个检查,如果关键点置信度低于 0.5,直接丢弃这一帧,不要送识别。另外存特征时确认是 float 不是 int8。
4.4 内存不够导致重启
现象:跑几分钟后设备重启,串口打印Guru Meditation Error。 原因:PSRAM 没开,或者fb_count设太大。双缓冲加模型内存,峰值能到 2MB 以上。 解决:menuconfig 里确认SPIRAM开启,fb_count从 2 降到 1,dl_matrix3du_alloc后必须配对dl_matrix3du_free,漏一个就内存泄漏。
4.5 帧率突然掉到 1 帧
现象:刚开始跑 5 帧,跑一会儿掉到 1 帧。 原因:摄像头过热,或者任务优先级冲突。esp32S3 双核,如果人脸任务和 WiFi 任务抢同一个核,推理会被打断。 解决:把人脸任务绑到核心 1,WiFi 绑到核心 0,用xTaskCreatePinnedToCore指定核心。另外加个散热片,OV2640 连续工作温度能到 60 度以上。
5. 进阶技巧:把识别耗时从 300ms 压到 150ms
5.1 图像预处理的裁剪策略
MTMN 标准版输入 320x240,但实际人脸只占中间一块。我一般会先把图像裁剪到中间 240x240,再送模型。这样推理量减少 25%,耗时从 220ms 降到 160ms 左右。裁剪代码如下:
// 裁剪中间区域,减少推理量 int crop_x = (fb->width - 240) / 2; int crop_y = (fb->height - 240) / 2; dl_matrix3du_t *cropped = dl_matrix3du_alloc(1, 240, 240, 3); for (int y = 0; y < 240; y++) { memcpy(cropped->item + y * 240 * 3, image_matrix->item + (y + crop_y) * fb->width * 3 + crop_x * 3, 240 * 3); }逻辑说明:直接操作内存拷贝,避免逐像素循环。crop_x和crop_y算的是偏移量,确保裁剪区域居中。注意image_matrix的步长是fb->width * 3,不是 240 * 3。
5.2 跳帧检测与动态阈值
不是每一帧都需要跑检测。我一般会隔一帧检测一次,中间帧复用上一次的检测框。同时根据环境光动态调score_threshold:光线好时设 0.5,光线差时降到 0.3。这样整体帧率能稳定在 6 到 8 帧。
| 策略 | 推理耗时 | 帧率 | 识别率 |
|---|---|---|---|
| 每帧检测 | 220ms | 4.5 fps | 92% |
| 隔帧检测 | 110ms(均摊) | 8 fps | 90% |
| 隔帧+裁剪 | 80ms(均摊) | 10 fps | 88% |
识别率掉 4 个百分点,但帧率翻倍,门禁场景完全够用。
5.3 特征比对用 int8 量化
MFN 输出的 float 特征占 2KB,比对时算 512 次浮点乘加。如果量化成 int8,内存降到 512 字节,比对速度提升 3 倍。代价是识别率掉 2% 左右。量化代码如下:
// float 转 int8 量化 int8_t quantized[512]; float scale = 0.0078f; // 根据特征分布调整 for (int i = 0; i < 512; i++) { quantized[i] = (int8_t)(feature->item[i] / scale); }参数说明:scale是量化系数,一般取特征绝对值的最大值除以 127。这个值需要根据实际特征分布标定,设大了溢出,设小了精度损失。我一般会先跑 100 张人脸,统计特征范围再定。
从那以后我每次调阈值前,都会先跑一遍标定集,把距离分布画出来再定阈值,不再拍脑袋。希望帮到你。
本文还有配套的精品资源,点击获取