
简介这份资源面向嵌入式开发者与物联网学习者聚焦在ESP32-S3芯片上实现人脸检测与识别的完整工程适合已具备ESP-IDF基础、希望把机器学习算法落到低功耗MCU上的中高级开发者。包内共418个文件以h/hpp头文件、c/cpp源码、md说明文档为主辅以npy模型数据、py脚本、a静态库与esp32s3等芯片配置压缩包约25.82MB目录涵盖components组件库、main主程序、分区表与构建配置结构清晰便于按模块研读。已有2098人学习下载。资源完整呈现了图像采集、预处理、特征提取、人脸检测与比对识别的技术链路并附带人脸检测、颜色检测、扫码等预编译库读者可据此理解模型量化、内存管理与硬件加速的落地思路快速搭建可运行的人脸识别原型并对照源码排查问题。1. 一块 30 块钱的板子把“刷脸”这件事从云端拽回了桌面去年帮一个做门禁改造的朋友看现场对方张口就要“云端人脸比对、后台下发权限、断网也能开门”。预算报上去光服务器和带宽就够买几十块开发板了。后来我们把方案换成了 ESP32-S3 本地推理摄像头采集、人脸检测、特征比对全在板子上跑完只把“开门/不开门”这一个结果发出去。整机成本压到百元级断网照常工作隐私数据压根不出设备。这就是 esp32 人脸识别这类项目真正打动人的地方——它不是玩具 Demo而是能把一套刷脸门禁、考勤机、智能柜锁的骨架直接搬进产品里的工程起点。这篇笔记就按“这项目能干什么 → 怎么跑起来 → 核心代码长什么样 → 怎么改成你自己的东西”这条线把一份 ESP32-S3 人脸识别工程拆开讲透新手能照着点亮屏幕熟手能直接看架构和改造点。2. 项目定位与技术选型为什么是 ESP32-S3 而不是树莓派2.1 这个项目到底在解决什么问题先把功能边界说清楚。一份典型的 ESP32-S3 人脸识别工程核心链路是四段摄像头出图 → 人脸检测框出位置 → 人脸特征提取成向量 → 与本地已注册的人脸库做比对命中则触发业务动作开门、打卡、点亮指示灯。整个流程跑在 MCU 上不依赖网络注册和识别都在设备端完成。它适合谁第一类是做智能门禁、考勤、储物柜、实验室门锁这类小场景的硬件开发者需要低成本、低功耗、离线可用的方案第二类是想学边缘 AI 落地的嵌入式工程师ESP32-S3 带向量指令和 AI 加速指令是练手 TinyML 的好平台第三类是学生做课设或竞赛需要一套能跑通、能改、资料相对完整的参考工程。不适合谁需要高精度大库比对几千人以上、需要活体防伪到金融级、需要复杂姿态识别的场景MCU 算力和内存都撑不住这类还是得上带 NPU 的专用芯片或边缘盒子。这里有个反直觉的点很多人第一反应是“树莓派不香吗”。香但树莓派跑 Linux启动几十秒、功耗几瓦、成本几百做电池供电的门锁根本不现实。ESP32-S3 双核 240MHz、带 8MB PSRAM 的型号跑一个轻量人脸模型识别一次几百毫秒到一秒待机功耗毫安级这才是门锁场景要的形态。选型的本质不是“谁算力强”而是“谁的成本和功耗配得上这个场景”。2.2 技术栈拆解从摄像头到模型推理的每一层把技术栈按数据流拆开看一层一层都是可替换的模块理解了这个分层改造时才知道动哪里。层次典型选型作用可替换性图像采集OV2640 / OV3660 / GC0308输出 RGB565 或 JPEG换摄像头需改驱动和分辨率配置显示/交互ST7789 / ILI9341 SPI 屏预览画面、显示识别结果可换成串口日志或 OLED人脸检测ESP-DL 自带轻量检测模型 / MTMN框出人脸区域可换成人脸检测专用轻量网络特征提取MobileFaceNet 类轻量网络量化版输出定长特征向量换模型需同步改比对逻辑比对余弦相似度 / 欧氏距离与本地库比对打分阈值需按模型重新标定存储NVS / SPIFFS / SD 卡存人脸特征库和配置库大时建议上 SD 卡框架ESP-IDF ESP-DL底层驱动与推理引擎也可用 Arduino 框架简化上手这张表是整篇的骨架。你会发现真正“重”的是检测和特征提取两个模型其余都是工程胶水。ESP-DL 是乐鑫官方的深度学习推理库针对自家芯片做了算子优化和量化支持模型一般是 int8 量化后转成 C 数组编进固件或者放 SPIFFS 里加载。选它而不是 TFLite Micro主要原因是官方示例和算子适配更贴合 S3 的指令集踩坑资料也相对集中。2.3 硬件清单与接线要点一套能跑起来的最小系统我一般按这个清单备料ESP32-S3 开发板务必选带 PSRAM 的型号人脸模型和图像缓冲吃内存、OV2640 摄像头模块、SPI 接口的 TFT 屏可选但调试期强烈建议有、若干杜邦线、一个 USB 数据线注意是数据线不是纯充电线这个坑后面会讲。接线这块摄像头走的是 ESP32-S3 的 LCD_CAM 外设引脚不是随便定的得按工程里camera_pin_config的宏来。常见做法是把 D0-D7、PCLK、VSYNC、HREF、XCLK、SDA、SCL 这些信号分配到一组连续的 GPIO 上屏幕则单独占一组 SPI 引脚。接线前先翻工程里的引脚定义头文件别凭经验接S3 的 GPIO 有复用限制接错了轻则不亮重则和 PSRAM 抢引脚直接起不来。提示带 Octal PSRAM 的 S3 模组会占用 GPIO33-37摄像头数据线千万别往这几个脚上排这是新手最容易翻车的地方。3. 从零跑通环境搭建、编译烧录与首次识别3.1 开发环境搭建ESP-IDF 版本别乱选这类工程基本都基于 ESP-IDF版本选择是有讲究的。ESP-DL 对 IDF 版本有对应关系太新的 IDF 可能编译不过旧版 ESP-DL太旧的又缺 S3 的某些驱动。我一般按工程README或CMakeLists.txt里声明的版本走常见是 IDF 4.4 或 5.x 的某个具体小版本。安装流程按官方来Linux/macOS 下大致是# 克隆 IDF以 5.1 为例实际以工程声明为准 git clone -b v5.1 --recursive https://github.com/espressif/esp-idf.git cd esp-idf ./install.sh esp32s3 . ./export.shinstall.sh esp32s3只装 S3 需要的工具链能省不少下载时间export.sh是把idf.py等工具加进当前终端环境注意它只对当前 shell 生效新开终端要重新执行。Windows 下用官方安装器更省事装完在开始菜单里打开 IDF 终端即可。装完验证一下idf.py --version能打印出版本号就说明环境通了。这一步失败的话八成是 Python 环境冲突或者工具链没下全先别急着往下走。3.2 工程配置与编译烧录拿到工程后先看目录里有没有sdkconfig.defaults这个文件决定了 PSRAM 是否开启、Flash 大小、分区表等关键配置。人脸识别工程几乎必然要求开 PSRAM否则模型加载直接内存不足。# 进入工程目录设置目标芯片 idf.py set-target esp32s3 # 打开菜单配置重点确认以下几项 idf.py menuconfig在menuconfig里要确认的项Component config → ESP PSRAM打开并选对模式Quad/Octal 要和板子一致Serial flasher config里 Flash size 选对常见 8MB 或 16MB分区表如果工程自带partitions.csv就别动模型和字库可能占了自定义分区。配置完编译idf.py build第一次编译会拉一堆组件慢是正常的。编译过了就烧录先确认串口# Linux 下一般是 /dev/ttyUSB0 或 /dev/ttyACM0 ls /dev/ttyUSB* # 烧录并打开监视器 idf.py -p /dev/ttyUSB0 flash monitorflash负责烧固件monitor负责看串口日志Ctrl]退出监视器。Windows 下端口类似COM3。烧录失败最常见的原因是没进下载模式——按住 BOOT 键再按一下 RST或者板子本身带自动下载电路就不用管。3.3 首次运行注册人脸与识别验证固件跑起来后串口会打印初始化日志摄像头探测、模型加载、屏幕初始化。如果屏幕接了一般能看到实时预览画面加一个检测框。首次使用要先“注册”——把一张人脸录进本地库。不同工程交互方式不一样常见两种按键触发注册或者串口发指令。以按键为例按下注册键后屏幕提示“请正对摄像头”设备连续采集若干帧提取特征取平均后存入 NVS并分配一个 ID。注册成功后串口会打印类似enroll success, id0的日志。识别阶段人站到镜头前设备检测到人脸、提取特征、与库里逐个比对相似度超过阈值就判定命中串口打印match id0 score0.87之类同时触发业务动作。第一次跑建议先注册 1 到 2 个人确认链路通了再扩库。如果一直识别不上先看日志里检测框有没有出来——检测都没有说明是摄像头或光照问题不是比对逻辑问题这个排查顺序能省很多时间。注意注册时的光照条件和识别时的光照条件尽量一致模型对光照变化敏感这是离线小模型绕不开的物理限制。4. 核心模块拆解一次识别从进摄像头到出结果的完整链路4.1 摄像头初始化与图像采集摄像头是整个链路的入口初始化没做好后面全是白搭。ESP-IDF 下用esp_camera组件核心是填一个camera_config_t结构体。#include esp_camera.h static camera_config_t camera_config { .pin_pwdn -1, // 电源脚模块没有就填 -1 .pin_reset -1, .pin_xclk CAM_XCLK, // 主时钟一般 20MHz .pin_sccb_sda CAM_SDA, // SCCB 配置总线 .pin_sccb_scl CAM_SCL, .pin_d7 CAM_D7, .pin_d6 CAM_D6, .pin_d5 CAM_D5, .pin_d4 CAM_D4, .pin_d3 CAM_D3, .pin_d2 CAM_D2, .pin_d1 CAM_D1, .pin_d0 CAM_D0, .pin_vsync CAM_VSYNC, .pin_href CAM_HREF, .pin_pclk CAM_PCLK, .xclk_freq_hz 20000000, .ledc_timer LEDC_TIMER_0, .ledc_channel LEDC_CHANNEL_0, .pixel_format PIXFORMAT_RGB565, // 人脸检测一般要 RGB .frame_size FRAMESIZE_QVGA, // 320x240够用且省内存 .jpeg_quality 12, .fb_count 2, // 双缓冲采集和推理可并行 .fb_location CAMERA_FB_IN_PSRAM, .grab_mode CAMERA_GRAB_WHEN_EMPTY, }; esp_err_t err esp_camera_init(camera_config); if (err ! ESP_OK) { ESP_LOGE(TAG, camera init failed: 0x%x, err); return; }几个参数值得单独说。pixel_format选PIXFORMAT_RGB565是因为检测模型吃 RGB 输入如果选 JPEG 还得软解反而更慢更吃 CPU。frame_size用 QVGA320×240是精度和内存的平衡点再大人脸检测模型输入也得缩白费带宽。fb_count 2开双缓冲一帧在推理时另一帧可以继续采集避免卡顿但每帧 RGB565 的 QVGA 大约 150KB两帧就是 300KB所以fb_location必须放 PSRAM。grab_mode选WHEN_EMPTY表示缓冲空时才抓新帧防止覆盖正在处理的帧。采集一帧就是esp_camera_fb_get()用完必须esp_camera_fb_return(fb)归还否则缓冲很快耗尽表现为跑几帧后卡死。这个“借了要还”的规矩是摄像头模块最常见的血泪经验。4.2 人脸检测把画面里的人框出来检测环节的目标很单纯输入一帧图输出若干个人脸框坐标和置信度。工程里一般用 ESP-DL 提供的轻量检测模型接口大致是“初始化模型 → 喂入图像 → 拿结果”。#include human_face_detect_msr01.hpp // 模型对象一般全局或静态持有避免反复申请内存 static HumanFaceDetectMSR01 *s_detector nullptr; void detector_init() { // 第二个参数是置信度阈值第三个是 NMS 重叠阈值 s_detector new HumanFaceDetectMSR01(0.3f, 0.5f, 10, 0.5f); } int detect_faces(camera_fb_t *fb, std::listdl::detect::result_t results) { dl::image::img_t img { .data fb-buf, .width fb-width, .height fb-height, .pix_type dl::image::DL_IMAGE_PIX_TYPE_RGB565, }; // 推理结果写进 results每个元素含框坐标和关键点 s_detector-run(img, results); return results.size(); }HumanFaceDetectMSR01的构造参数里第一个是置信度阈值调高会漏检、调低会误检0.3 到 0.5 是常见区间第二个是 NMS非极大值抑制阈值用来合并重叠框人脸密集时调小一点。run的输入是img_t把摄像头缓冲的裸数据、宽高、像素类型包一下就行注意pix_type必须和摄像头输出格式对上对不上会得到一堆乱框或者直接崩。检测结果results里除了框通常还带 5 个关键点双眼、鼻尖、两嘴角这些点后面做对齐会用到。检测这一步的耗时和分辨率强相关QVGA 下一般几十毫秒量级是整个链路里相对轻的一环。4.3 特征提取与比对真正决定“是不是同一个人”检测框出来后把框内人脸裁出来、缩放到模型输入尺寸常见 112×112送进特征提取网络得到一个定长向量常见 128 或 512 维。这一步是识别精度的核心。#include human_face_recognition.hpp static HumanFaceRecognition *s_recognizer nullptr; void recognizer_init() { s_recognizer new HumanFaceRecognition(); } bool extract_feature(camera_fb_t *fb, const dl::detect::result_t face, float *feature_out, int feat_len) { dl::image::img_t img { .data fb-buf, .width fb-width, .height fb-height, .pix_type dl::image::DL_IMAGE_PIX_TYPE_RGB565, }; // 内部会按框裁剪、对齐、归一化再推理出特征向量 return s_recognizer-run(img, face, feature_out, feat_len); }run内部做了裁剪、关键点对齐、归一化这些脏活调用方只要给原图、框和输出缓冲。特征向量拿到后和本地库比对。比对用余弦相似度最直观float cosine_similarity(const float *a, const float *b, int len) { float dot 0.f, na 0.f, nb 0.f; for (int i 0; i len; i) { dot a[i] * b[i]; na a[i] * a[i]; nb b[i] * b[i]; } return dot / (sqrtf(na) * sqrtf(nb) 1e-6f); } // 遍历本地库取最高分 int match_face(const float *feat, int len, float threshold, float *best_score) { int best_id -1; *best_score 0.f; for (int i 0; i g_face_count; i) { float s cosine_similarity(feat, g_face_db[i], len); if (s *best_score) { *best_score s; best_id i; } } return (*best_score threshold) ? best_id : -1; }阈值threshold是这套系统最需要现场标定的参数。同一个模型不同光照、不同摄像头最佳阈值能差 0.1 以上。我一般先用 0.6 起步拿真人反复测看“本人最低分”和“他人最高分”之间的间隔把阈值卡在中间偏本人一侧。间隔太窄说明模型或图像质量不行别硬调阈值先解决光照和分辨率。4.4 特征库存储与业务触发特征库放哪取决于库的大小。几个人到几十个人存 NVS 完全够键值对形式每个 ID 存一段二进制特征。上百人以上NVS 读写和内存都吃紧建议上 SPIFFS 或 SD 卡按文件存。// 以 NVS 为例保存一条特征 nvs_handle_t handle; nvs_open(face_db, NVS_READWRITE, handle); char key[16]; snprintf(key, sizeof(key), face_%d, id); nvs_set_blob(handle, key, feature, feat_len * sizeof(float)); nvs_commit(handle); nvs_close(handle);业务触发就是识别命中后的动作工程里一般抽象成一个回调或事件命中 ID 有效就拉高某个 GPIO 驱动继电器或者通过串口/MQTT 上报一条记录。改造时这块是最容易替换的把“开门”换成“打卡上报”“点亮绿灯”都只是改回调内容不影响识别主链路。5. 避坑与排查那些让我熬夜的现场问题5.1 现象烧录成功但串口一直重启原因通常是 PSRAM 配置和板子不匹配或者摄像头引脚和 PSRAM 抢了 GPIO。S3 带 Octal PSRAM 的模组会占用 33-37 脚摄像头数据线如果排到这几个脚初始化阶段就崩。解决先看menuconfig里 PSRAM 模式选对没有再对照板子原理图核对摄像头引脚把冲突的线挪开。判断是不是 PSRAM 问题可以临时关掉 PSRAM 编译一版能起来就说明是内存配置的锅。5.2 现象画面能出但检测框乱飞或一个人检出好几个框前者多半是像素格式对不上img_t.pix_type写成了 RGB888 而摄像头实际输出 RGB565数据被错误解释。后者是 NMS 阈值设太大重叠框没合并掉。解决先确认摄像头pixel_format和喂给模型的pix_type严格一致再把 NMS 阈值从 0.5 往 0.3 调观察框是否收敛。还有一种情况是图像太暗导致模型输出噪声补光比调参管用。5.3 现象跑十几帧后卡死日志停在fb_get附近典型的帧缓冲没归还。esp_camera_fb_get()拿到的fb在处理完必须esp_camera_fb_return(fb)中间任何提前 return 的分支漏了归还缓冲池很快耗尽。解决把所有出错分支都补上归还或者用 goto cleanup 统一收口。这个坑我在三个不同项目里都见过属于肌肉记忆级别的检查项。5.4 现象本人识别不上但换个人反而能过这不是玄学是阈值和特征质量的问题。常见原因是注册时和识别时光照差异大或者注册只采了一帧、姿态不好。解决注册时多采几帧取平均注册和识别尽量在同一光照环境再拿本人和他人各测 20 次统计分数分布重新定阈值。如果本人最低分和他人最高分几乎重叠说明模型或图像分辨率不够考虑换更高分辨率的检测输入或更好的摄像头别在阈值上死磕。5.5 现象编译报找不到 ESP-DL 头文件多半是组件依赖没声明。ESP-IDF 的组件模型要求在自己的CMakeLists.txt里REQUIRES或PRIV_REQUIRES里写上esp-dl相关组件名光把源码拷进目录不够。解决检查工程main/CMakeLists.txt的依赖列表确认 ESP-DL 组件路径被正确引用如果是把 ESP-DL 作为子模块放的确认EXTRA_COMPONENT_DIRS指对了位置。6. 二次开发改造把 Demo 变成你自己的产品改造这件事我的建议是从“最不影响主链路”的地方下手逐步替换每改一处都留一个可回退的验证点。下面按改造收益从高到低排。第一刀砍在业务触发层。识别命中后的回调是最独立的模块把它抽成一个函数指针或事件队列你就能在不碰识别代码的前提下接继电器、接蜂鸣器、接串口协议。我一般会定义一个on_face_matched(id, score)回调主流程只负责调用具体动作在业务层实现。这样换场景只改一个文件。第二刀是特征库的存储介质。人数少继续用 NVS人数多就换成 SPIFFS 按文件存接口抽象成db_add / db_match / db_delete三个函数底层换实现不影响上层。这里有个细节删除人脸时 NVS 的键不会自动回收空间长期反复注册删除要做一次nvs_erase_key加整理否则分区会慢慢被占满。第三刀才是模型替换。想提精度就换更强的特征提取模型但要注意三件事模型输入尺寸变了裁剪缩放代码要跟着改特征维度变了本地库要清空重录老特征和新模型不兼容量化方式变了比对阈值要重新标定。换模型不是换个文件那么简单是一次小版本升级务必留出标定时间。验证方法上我习惯做一个“离线回放”工具把现场采集的几十张图存到 SD 卡写个循环批量跑检测加比对输出每张的分数。这样调阈值不用真人反复站镜头前效率高很多。下面是个简化示意// 批量回放遍历 SD 卡图片输出比对分数 for (int i 0; i img_count; i) { camera_fb_t *fb load_image_from_sd(i); // 自定义加载 std::listdl::detect::result_t faces; detect_faces(fb, faces); if (faces.empty()) { printf(img %d: no face\n, i); continue; } float feat[FEAT_LEN]; extract_feature(fb, faces.front(), feat, FEAT_LEN); float score; int id match_face(feat, FEAT_LEN, 0.0f, score); printf(img %d: best_id%d score%.3f\n, i, id, score); free_image(fb); }把阈值传 0 是为了强制输出最高分方便你统计分布而不是直接判定。跑完一批把本人和他人分数画个简单的直方图阈值该定在哪一目了然。最后说个我踩过的坑早期我图省事把注册和识别放在同一个任务里串行跑结果注册时屏幕卡住、识别响应也慢。后来拆成采集任务、推理任务、业务任务三个用队列传帧指针流畅度立刻不一样。从那以后我每次做这类边缘视觉项目都强制先把任务划分和缓冲归还这两件事在纸上画一遍再动手省下的调试时间远超画图那十分钟。希望这套拆解能帮到你把板子跑起来只是开始真正有意思的是把它改成你场景里那个“刚好够用”的版本。本文还有配套的精品资源点击获取