简介:这份资源面向嵌入式开发与物联网视觉应用的学习者,提供基于ESP32-S3芯片实现人脸检测与识别的完整工程代码。ESP32-S3集成Wi-Fi、蓝牙及神经网络加速单元,适合在低功耗边缘设备上运行机器学习算法,资源涵盖图像采集、预处理、特征提取、人脸检测与识别、性能优化及内存管理等技术环节,并涉及模型量化压缩与面部数据安全等实践考量。压缩包共418个文件,约25.82MB,以h/hpp头文件、c/cpp源码、md说明文档、npy数据文件、txt配置及py脚本为主,另含a静态库、so动态库、onnx与pt模型文件、csv分区表及sdkconfig配置,覆盖人脸检测、猫脸检测、颜色检测与条码扫描等模块。目前已有2090人学习下载,适合希望掌握ESP32-S3端侧视觉方案、对照工程结构快速上手或二次开发的开发者参考。
1. 一块不到百元的板子跑人脸识别,这事到底靠不靠谱
前阵子有个做智能门禁的哥们找我,说他们想在一台带屏的终端上做人脸打卡,预算卡得死,问能不能不上 Linux 方案。我第一反应是摇头,直到把 esp32S3 人脸识别这套东西完整跑了一遍——ESP32-S3 自带向量指令加速,配上一颗 OV2640/OV3660 摄像头和一块 SPI 屏,整机成本压到很低,识别延迟在本地就能做到几百毫秒级。它适合谁?做门禁考勤、智能门锁、访客机这类边缘端小设备的开发者,尤其是那些不想背 Linux 系统包袱、又需要离线人脸能力的场景。这篇就把这套源码从目录结构、核心链路到二次开发,按我实际拆包的顺序讲一遍。
2. 项目定位与技术选型:为什么是 ESP32-S3 而不是树莓派
2.1 这套源码解决的真实问题
先把定位说清楚。这套 esp32S3 人脸识别项目,本质是一个跑在 MCU 上的离线人脸检测 + 识别固件工程,不是云端方案,也不依赖网络。它的典型工作流是:摄像头采集一帧图像 → 人脸检测框出人脸 → 对齐裁剪 → 提取特征向量 → 和本地已注册的人脸库比对 → 输出识别结果(匹配到谁 / 陌生人)。
为什么强调「离线」?因为门禁、考勤这类场景对网络稳定性极其敏感,一旦断网整个系统就瘫了,这是血泪经验。把识别放在本地,网络只负责上报结果,系统鲁棒性完全不一样。
技术选型上,核心是乐鑫的 ESP32-S3 芯片。相比 ESP32 老款,S3 增加了向量指令(SIMD)和更高的主频,这对跑神经网络推理是质变——人脸检测和特征提取本质都是卷积运算,向量指令能把推理速度拉起来。常见做法是搭配 ESP-DL 或 ESP-WHO 这套官方推理框架,模型量化成 int8 后塞进 Flash。
2.2 硬件与软件栈清单
一份能跑起来的工程,硬件和软件得配套。下面是我拆包后整理的典型配置,具体型号以你手上的源码为准:
| 类别 | 典型配置 | 说明 |
|---|---|---|
| 主控 | ESP32-S3 系列模组 | 需带 PSRAM,推理吃内存 |
| 摄像头 | OV2640 / OV3660 | DVP 接口,RGB565 或 JPEG 输出 |
| 显示屏 | SPI LCD(如 ST7789) | 用于预览和结果展示,可选 |
| 存储 | 外部 Flash / SD 卡 | 存人脸库和模型 |
| 开发框架 | ESP-IDF | 官方 SDK,工程基于 CMake |
| 推理库 | ESP-DL / ESP-WHO | 提供检测与识别模型 |
| 模型 | 人脸检测 + 人脸识别两个量化模型 | int8 量化,放 Flash 分区 |
这里有个容易翻车的点:PSRAM 不是可选项。人脸检测模型输入一般是 240x240 或更大,中间特征图占的内存远超片内 SRAM,没有 PSRAM 直接跑不起来或者疯狂重启。我一般会在menuconfig里先把 PSRAM 打开并设为 Octal 模式,再动别的。
软件栈的分层大致是这样:底层是 ESP-IDF 提供的驱动(I2C 控制摄像头、SPI 驱动屏、文件系统),中间是 ESP-DL 的算子库和模型加载,上层是业务逻辑(检测、识别、人脸库管理)。理解这个分层,后面改代码才知道该动哪一层。
2.3 和常见替代方案的差别
有人会问,为啥不用树莓派或者 K210?树莓派算力强、生态好,但功耗和成本高,开机慢,做电池供电的小设备不划算。K210 有 KPU 专跑神经网络,但生态和资料相对少,摄像头适配坑多。ESP32-S3 的优势在于成本、功耗、生态三者平衡,而且 ESP-IDF 的工程化程度高,量产烧录、OTA 都成熟。代价就是算力有限,模型必须精简量化,识别库容量也受限——这是选型时就要接受的约束,不是后期能绕过去的。
3. 目录结构与核心链路:一个请求从采集到识别返回
3.1 工程目录怎么读
拿到源码先别急着编译,把目录结构摸清楚能省一半时间。这类 ESP-IDF 工程的典型结构如下:
esp32s3-face-recognition/ ├── CMakeLists.txt # 顶层构建入口 ├── sdkconfig.defaults # 默认配置(PSRAM、分区、模型路径) ├── main/ │ ├── CMakeLists.txt │ ├── app_main.c # 程序入口,初始化各模块 │ ├── camera/ # 摄像头初始化与取帧 │ ├── detect/ # 人脸检测封装 │ ├── recognize/ # 特征提取与比对 │ └── face_db/ # 人脸库读写(Flash/NVS) ├── components/ │ ├── esp-dl/ # 推理算子库 │ └── esp-who/ # 检测识别高层封装 ├── models/ # 量化模型文件 └── partitions.csv # 分区表,模型和库各占一块读目录有个诀窍:先看app_main.c和partitions.csv。前者告诉你程序启动后按什么顺序初始化,后者告诉你模型和人脸库存在哪、有多大。很多人编译过了但一跑就崩,八成是分区表里模型分区太小,模型加载失败。
3.2 摄像头取帧与检测链路
核心链路从取帧开始。下面这段是摄像头初始化和取帧的典型写法,基于 esp32-camera 组件:
// camera/camera_init.c #include "esp_camera.h" static camera_config_t camera_config = { .pin_pwdn = -1, .pin_reset = -1, .pin_xclk = 15, .pin_sccb_sda = 4, // I2C 数据线,控制摄像头寄存器 .pin_sccb_scl = 5, // I2C 时钟线 .pin_d7 = 16, .pin_d6 = 17, .pin_d5 = 18, .pin_d4 = 12, .pin_d3 = 10, .pin_d2 = 8, .pin_d1 = 9, .pin_d0 = 11, .pin_vsync = 6, .pin_href = 7, .pin_pclk = 13, .xclk_freq_hz = 20000000, // 摄像头主时钟 20MHz .pixel_format = PIXFORMAT_RGB565, // 检测模型吃 RGB,别用 JPEG .frame_size = FRAMESIZE_QVGA, // 320x240,够检测用 .fb_count = 2, // 双缓冲,取帧不阻塞 .fb_location = CAMERA_FB_IN_PSRAM // 帧缓冲放 PSRAM }; esp_err_t camera_init(void) { esp_err_t err = esp_camera_init(&camera_config); if (err != ESP_OK) { ESP_LOGE("CAM", "init failed: 0x%x", err); return err; } return ESP_OK; }逻辑说明:pixel_format选PIXFORMAT_RGB565是因为检测模型输入要求 RGB,如果选 JPEG 还得额外解码,白白浪费算力。fb_count = 2开双缓冲,一帧在推理时另一帧可以继续采集,避免卡顿。fb_location必须指向 PSRAM,QVGA 的 RGB565 一帧就是 150KB,片内 SRAM 放不下两帧。
参数怎么改:frame_size如果识别距离要求远,可以升到FRAMESIZE_VGA,但内存和推理时间都会涨;xclk_freq_hz一般 20MHz 稳定,调高可能花屏,调低帧率下降。引脚定义必须和你实际接线一致,接错了初始化直接失败。
3.3 检测与识别两段式推理
取到帧后进入两段式推理:先检测人脸位置,再对裁剪出的人脸做特征提取。这是行业通用做法,因为识别模型只认对齐后的小图,直接喂整帧效果很差。
// recognize/face_pipeline.c #include "human_face_detect_msr01.hpp" #include "human_face_recognition.hpp" void face_pipeline(camera_fb_t *fb) { // 1. 检测:输入 RGB 帧,输出人脸框列表 HumanFaceDetectMSR01 detector(0.3F, 0.3F, 10, 0.3F); std::list<dl::detect::result_t> &results = detector.infer((uint16_t *)fb->buf, {fb->height, fb->width, 3}); // 2. 对每张脸做特征提取 HumanFaceRecognition *recognizer = get_recognizer(); for (auto &face : results) { // 裁剪并对齐到识别模型输入尺寸 dl::Tensor<uint8_t> aligned = align_face(fb, face); // 提取 512 维特征向量 dl::Tensor<int8_t> feature = recognizer->infer(aligned); // 3. 与本地人脸库比对,返回最相似 ID 和相似度 match_result_t match = face_db_match(feature); ESP_LOGI("FACE", "id=%d score=%.2f", match.id, match.score); } }逻辑说明:HumanFaceDetectMSR01的构造参数依次是置信度阈值、NMS 阈值、最大检测数、分数阈值,这几个值直接决定检测灵敏度和误检率。infer传入的是帧缓冲指针和形状{高, 宽, 通道},注意顺序别写反。识别阶段先align_face做对齐,这一步很关键,人脸歪着提特征相似度会掉一大截。
参数怎么改:检测阈值调低能检出更多脸但误检增加,门禁场景我一般设 0.5 以上求稳。相似度阈值(match.score的判定线)是识别准确率和误识率的核心旋钮,常见做法是设 0.6~0.7,太低会把陌生人认成熟人,太高本人也刷不过。
3.4 人脸库的存储与比对
人脸库是这套系统的「后悔药」——注册进去的人脸特征得持久化,掉电不能丢。常见做法是用 NVS 或独立 Flash 分区存特征向量。
// face_db/face_db.c #define MAX_FACES 20 // 最多注册 20 人 #define FEATURE_DIM 512 // 特征维度 #define FEATURE_BYTES (FEATURE_DIM * sizeof(int8_t)) // 特征库结构:id + 特征向量,连续存放 static int8_t face_features[MAX_FACES][FEATURE_DIM]; esp_err_t face_db_add(int id, const int8_t *feature) { if (id < 0 || id >= MAX_FACES) return ESP_ERR_INVALID_ARG; memcpy(face_features[id], feature, FEATURE_BYTES); // 写入 NVS,key 用 "face_<id>" return nvs_write_blob("face_db", id, feature, FEATURE_BYTES); } match_result_t face_db_match(const dl::Tensor<int8_t> &feature) { match_result_t best = {.id = -1, .score = 0.0f}; for (int i = 0; i < MAX_FACES; i++) { float sim = cosine_similarity(feature.data, face_features[i], FEATURE_DIM); if (sim > best.score) { best.id = i; best.score = sim; } } return best; }逻辑说明:MAX_FACES决定库容量,20 人对应 20×512 字节约 10KB,Flash 完全放得下,要扩到几百人得换存储策略。比对用余弦相似度,遍历全库取最高分,这是最朴素的线性搜索,库小的时候够用。
参数怎么改:FEATURE_DIM必须和识别模型输出维度一致,改错了比对结果全是乱的。库容量大了以后线性搜索会变慢,可以加个粗筛或者用向量索引,但 MCU 上一般没必要,几十人以内线性扫完全够。
4. 编译烧录与运行:从零把固件跑起来
4.1 环境搭建与编译
ESP-IDF 工程对环境版本敏感,我一般锁定一个稳定版本,别用最新的。步骤大致如下:
# 1. 拉取并安装 ESP-IDF(以 v5.x 为例,具体以源码 README 为准) git clone --recursive https://github.com/espressif/esp-idf.git cd esp-idf && ./install.sh esp32s3 . ./export.sh # 2. 进入工程目录,设置目标芯片 cd esp32s3-face-recognition idf.py set-target esp32s3 # 3. 打开配置菜单,确认 PSRAM、Flash 大小、分区表 idf.py menuconfig # 4. 编译 idf.py build # 5. 烧录并监视串口 idf.py -p /dev/ttyUSB0 flash monitor逻辑说明:set-target会重置部分配置,所以要在menuconfig之前执行。menuconfig里重点确认三项:PSRAM 使能并选对模式、Flash 容量和分区表匹配、模型文件路径正确。这三项任一不对,编译能过但运行必崩。
参数怎么改:串口设备名 Windows 下是COMx,Linux 下是/dev/ttyUSBx,插上板子用ls /dev/tty*确认。烧录波特率默认就行,不稳定可以降到 460800。
4.2 首次运行该看什么
烧录完打开 monitor,正常启动日志会依次出现:PSRAM 初始化成功、摄像头探测到 PID、模型加载完成、开始推理。如果卡在某一步,日志会明确告诉你。
提示:第一次跑建议先只开检测、不开识别,确认摄像头出图正常,再逐步打开识别链路。一次性全开,出问题很难定位是哪一环。
我一般会先看摄像头那行日志有没有打印出传感器型号,没有就是 I2C 接线或引脚配置错了。模型加载失败通常是分区表里模型分区太小,或者模型文件没被打进固件。
4.3 人脸注册流程
系统跑起来后,得先注册人脸才能识别。注册流程一般是:进入注册模式 → 对准摄像头 → 采集多张 → 取平均特征 → 存库。源码里通常有个注册函数或者通过串口命令触发。注册时建议采集 3~5 张不同角度,取特征平均,比单张鲁棒得多。注册完记得重启验证一下能不能刷脸通过,别等部署了才发现库是空的。
5. 避坑与常见问题排查
5.1 上电就重启,串口刷屏
现象:板子一上电就反复重启,日志里出现Guru Meditation或内存分配失败。原因:九成是 PSRAM 没开或者模式选错,帧缓冲和模型内存分配不到。解决:进menuconfig确认 PSRAM 使能,模式(Quad/Octal)和你的模组一致,fb_location设为 PSRAM。
5.2 摄像头初始化失败
现象:日志打印cam init failed或探测不到传感器 PID。原因:引脚定义和实际接线不符,或者 I2C(SCCB)线接反。解决:对照原理图逐个核对pin_sccb_sda/scl和 DVP 数据线,用万用表量一下供电是否正常。
5.3 识别总是认错人或认不出
现象:本人刷不过,或者陌生人被认成已注册的人。原因:相似度阈值设得不合适,或者注册时人脸没对齐、光照太差。解决:先把阈值调到 0.6 左右试,注册时保证正面、光照均匀,多采几张取平均。
5.4 帧率低、画面卡顿
现象:预览一卡一卡的,识别延迟明显。原因:帧尺寸开太大,或者单缓冲导致取帧和推理互相阻塞。解决:frame_size降到 QVGA,fb_count设 2 开双缓冲,推理和采集分到不同任务。
5.5 模型加载失败
现象:启动时报模型文件找不到或校验失败。原因:分区表里模型分区太小,或者模型没被 CMake 打进固件。解决:检查partitions.csv给模型留够空间,确认CMakeLists.txt里模型文件被idf_component_register的 EMBED 引用。
6. 二次开发:换模型、加活体、接业务
跑通之后,真正有价值的是改造。我一般从三个方向下手。
换模型。源码自带的检测识别模型是通用版,如果你场景固定(比如只做考勤),可以换成更小的专用模型,推理更快、内存更省。换模型要注意输入尺寸和量化方式必须和代码里的infer调用匹配,输入形状对不上直接崩。改完先跑一遍检测,确认框的位置对,再开识别。
加活体检测。门禁最怕拿照片骗过,纯特征比对是防不住的。常见做法是加一个眨眼或张嘴的动作检测,或者用红外双目。在 MCU 上做动作活体,一般是连续采几帧,检测关键点变化。这块代码要新开一个任务,别塞进识别主循环,否则帧率掉得厉害。
接业务逻辑。识别结果出来后要驱动继电器开门、上报服务器、记录日志。我一般把业务逻辑和识别链路解耦,识别任务只往队列里丢结果,业务任务从队列取,这样识别帧率不受业务阻塞影响。上报用 MQTT 或 HTTP 都行,注意别在识别任务里做网络请求,会拖垮实时性。
下面是一个解耦的骨架,供参考:
// 识别任务只负责推理,结果丢队列 static QueueHandle_t result_queue; void recognize_task(void *arg) { while (1) { camera_fb_t *fb = esp_camera_fb_get(); match_result_t r = face_pipeline(fb); esp_camera_fb_return(fb); xQueueSend(result_queue, &r, 0); // 非阻塞投递 } } // 业务任务消费结果,做开门/上报 void business_task(void *arg) { match_result_t r; while (1) { if (xQueueReceive(result_queue, &r, portMAX_DELAY)) { if (r.score > 0.65f) { gpio_set_level(RELAY_PIN, 1); // 开门 vTaskDelay(pdMS_TO_TICKS(1000)); gpio_set_level(RELAY_PIN, 0); report_to_server(r.id); // 上报 } } } }逻辑说明:识别任务和业务任务通过队列通信,识别任务永远不被网络或继电器操作阻塞。xQueueSend用 0 超时,队列满了就丢,保证识别实时性优先。阈值 0.65 是开门判定线,比识别显示阈值可以略高一点求稳。
参数怎么改:队列长度按业务处理速度设,一般 4~8 够用。继电器保持时间按实际门锁调。上报函数里记得加超时,别让网络卡死业务任务。
验证改造是否成功,我的习惯是先单元验证再联调:换完模型先单独跑检测看框准不准,加完活体先单独测动作识别率,最后再串起来。跳过单测直接联调,出问题根本分不清是哪一环。
从那以后我每次改识别链路,都强制先跑一遍「只检测不识别」的回归,确认摄像头和检测没问题再往上叠。这套 esp32S3 人脸识别的源码,坑基本都在内存和阈值上,把这两块摸透,二次开发就顺了。希望帮到你。
本文还有配套的精品资源,点击获取