☰
STM32单片机离线人脸识别:LBP特征提取与卡方距离比对实战
2026/10/21 13:39:30 网站建设 项目流程

简介:面向嵌入式开发者的STM32人脸识别实验工程,基于Cortex-M4内核与OV5640摄像头模块,完整演示在内存与算力受限的单片机环境中,从图像采集、预处理到人脸检测与识别的实现流程。资源包含174个文件,约1.03MB,以72个C源码与72个头文件构成核心代码框架,并配套Keil5工程文件、烧录脚本、PNG示意图与hex固件,目录结构清晰,便于直接编译烧录和二次开发。该资源已吸引8607人学习下载,适合具备基础单片机知识、希望深入理解视觉算法落地的开发者,也适合作为课程设计或竞赛项目的参考案例。通过该工程可研读图像预处理、Haar特征级联、LBP特征提取等典型算法在STM32平台上的实际应用,同时学习针对有限资源进行算法剪枝、功耗优化的工程思路,为智能家居、安防终端等场景提供可参考的软硬件实施方案。

1. 单片机人脸识别:不跑系统也能做的离线识别

第一次把 LBP 直方图比对跑进不带操作系统的板子时,我盯着识别结果愣了几秒:一块主频 168MHz 的单片机、一颗几十块的摄像头、一块 LCD,居然能在 200ms 左右完成一次本地人脸识别。这不是靠云端、不是靠 TensorFlow Lite 这种重型推理框架,而是用肤色分割 + LBP 特征提取 + 卡方距离比对这一整套轻量方案实现的。它适合两类人:一类是电子竞赛、毕设、课程设计需要“人脸识别”但预算有限的学生,另一类是产线上想用低成本门禁替代昂贵模块的嵌入式开发。这套方案核心就三个字:够用、可控、改得动。

要说它解决了什么,最直接的是:不需要 WiFi、不需要云端 API、不需要树莓派,一片 STM32F407 加上 OV2640 摄像头和 2.4 寸 TFT 屏,就能离线完成“录入人脸—保存模板—实时识别”的完整闭环。识别准确率在光照稳定环境下能做到八成以上误识率很低,这对很多场景来说已经足够。接下来我会按“整体架构 → 核心模块 → 踩坑实录 → 落地复现 → 二次开发”的顺序,把整个项目拆给你看。

2. 整体架构与选型:一条从摄像头到 LCD 的数据链路

2.1 硬件选型逻辑

主控选择 STM32F407ZGT6 不是拍脑袋。它带 DCMI 数字摄像头接口,可以直接接 OV2640 的 8 位并行数据线,不需要软件模拟时序;192KB RAM 能放下两帧 QVGA 分辨率的 RGB565 缓冲;168MHz 主频可以在几毫秒内完成整帧肤色分割。如果你用的是 F103,也不是不能做,但 DCMI 缺失后只能用 GPIO 模拟时序,帧率和稳定性都会明显打折,我不建议折腾。

摄像头选择 OV2640 是因为它输出 RGB565 格式方便直接上 LCD,而且支持通过 SCCB 协议配置成 QVGA(320x240),这个分辨率刚好在“细节够用”和“处理量可控”之间。存储端加一张 SPI 接口的 TF 卡模块,存特征模板文件;人机交互用 2.4 寸 SPI 屏加四个按键矩阵,完成录入、删除、识别三个功能的切换。整套物料成本可以压在百元以内,这是这个方案最吸引人的地方。

2.2 软件分层与流水线

我把整个工程分成四层:驱动层(摄像头、LCD、TF 卡、按键)、中间层(帧缓冲管理、人脸检测、LBP 特征提取)、应用层(模板库管理、识别决策)、表现层(菜单逻辑、串口日志)。代码上不跑 RTOS,用裸机前后台架构:主循环跑菜单状态机,DCMI 数据到来时通过 DMA 双缓冲机制在中断里搬运帧,保证采集不丢数据。

关键的识别流水线是这样设计的:DCMI 采集一帧 RGB565 图像 → 肤色分割提取人脸候选区域 → 几何校验(宽高比、面积占比)定位人脸框 → 缩放归一化到 64x64 灰度图 → 提取 LBP 直方图 → 与 SD 卡中的模板做卡方距离比对 → 将结果画到 LCD 上并打印串口日志。下面这段代码是 DCMI + DMA 双缓冲的初始化核心,它解决了“一边采集一边处理”的资源竞争问题:

void dcmi_init(void) { // 双缓冲帧缓冲,大小 = QVGA分辨率 * 2字节(RGB565) frame_buffer[0] = (uint16_t*)&frame_mem[0][0]; frame_buffer[1] = (uint16_t*)&frame_mem[1][0]; // DCMI 配置为 8bit RGB565 模式 DCMI->CR = 0; DCMI->CR |= DCMI_CR_EDM_8BITS; DCMI->CR |= DCMI_CR_ESS; DCMI->CR |= DCMI_CR_PCKPOL; // 启动 DMA2 通道1,关联DCMI接收,开启循环模式 DMA2_Stream1->CR &= ~DMA_SxCR_EN; DMA2_Stream1->PAR = (uint32_t)&DCMI->DR; DMA2_Stream1->M0AR = (uint32_t)frame_buffer[0]; DMA2_Stream1->NDTR = 320 * 240; DMA2_Stream1->CR |= DMA_SxCR_TCIE | DMA_SxCR_CIRC; DMA2_Stream1->CR |= DMA_SxCR_EN; }

逻辑说明:DMA 的 CIRC(循环模式)配合双缓冲会在传输完一帧后自动回到起始地址继续接收,同时触发传输完成中断,在中断服务函数中切换当前活动缓冲。NDTR存入的是像素总数,RGB565 是 16 位,DMA 配置为半字传输时正好匹配。第一次测试时我直接把NDTR写成字节数 153600,结果图像错位得完全不能看,这是很多人会踩的坑。

2.3 模板存储格式设计

模板库不需要数据库,SD 卡上按固定格式组织即可。我在 TF 卡根目录创建了face_db文件夹,每个用户一个目录,目录名形如user_0001,里面存放两张关键角度的特征文件f0.lbp和f1.lbp。每个.lbp文件头部是 16 字节的文件头(魔数 + 版本 + 特征维度 + 数据长度),后面紧跟着 LBP 直方图的 uint8 数组。

这样设计的好处是:新增用户只需新建目录;删除用户只需删目录;遍历库时用f_opendir + f_readdir即可。特征文件大小约 2KB,一张普通 TF 卡存几千个模板毫无压力。模板是按“最小可替换单元”设计的,后续想改成 TensorFlow Lite 的量化模型,只需要替换文件读写层和特征比较层。

3. 核心模块拆解:从肤色分割到 LBP 比对的完整链路

3.1 人脸检测:用 YCrCb 肤色分割替代 Haar 级联

在单片机上跑 OpenCV 的 Haar 级联不现实——一个检测窗口就要遍历几千个位置,每处都要做几十次特征计算。我的做法是把图像从 RGB 转 YCrCb,再做肤色分割。这个方案在室内光照相对稳定时效果足够好,而且整帧处理一遍也就 10ms 上下,实时性完全在可接受范围内。

void skin_detect(uint16_t *rgb_img, uint8_t *skin_mask, int len) { for (int i = 0; i < len; i++) { uint16_t pixel = rgb_img[i]; uint8_t r = (pixel >> 11) & 0x1F; uint8_t g = (pixel >> 5) & 0x3F; uint8_t b = pixel & 0x1F; // 5bit/6bit/5bit 扩展到 8bit 方便计算 r = (r << 3) | (r >> 2); g = (g << 2) | (g >> 4); b = (b << 3) | (b >> 2); int y = (77 * r + 150 * g + 29 * b) >> 8; int cb = ((-43 * r - 85 * g + 128 * b) >> 8) + 128; int cr = ((128 * r - 107 * g - 21 * b) >> 8) + 128; // 常见肤色范围,偏色光源下需要微调 if (cr > 130 && cr < 165 && cb > 90 && cb < 120) { skin_mask[i] = 1; } else { skin_mask[i] = 0; } } }

参数说明:skin_mask是临时申请的字节数组,逐像素标记是否肤色点。这里的 YCrCb 转换用的是简化的整数运算,没有按标准公式精确计算,因为对于肤色分割来说,近似值造成的偏差完全在可接受的阈值范围内。如果现场光源偏黄,需要把 Cr 下限调低;偏蓝则要把 Cb 范围整体下调。实际调试时我把 Cr 的下限从 135 调到 128,因为教室白炽灯下肤色 Cr 值会比标准环境低一点。

得到肤色掩码后,我做连通域分析找候选区域。方法是自底向上扫描skin_mask,用并查集把相邻的肤色点合并成块,记录每个块的外接矩形。然后按三个条件过滤:宽高比在 0.5:1 到 1.5:1 之间(人脸大概在这个范围)、块面积占比在整幅图的 2% 到 20% 之间、矩形内部的肤色密度超过 50%。这三个条件能从背景中把“像脸的块”筛出来,实测在纯色背景下找到的人脸框命中率接近九成。

3.2 LBP 特征提取:256 维直方图的计算

定位到人脸区域后,先把彩色图转成灰度,再缩放到 64x64。这里用最简单的最近邻插值,处理速度很快且对最终特征影响不大——LBP 本质是纹理特征,对分辨率不敏感。然后提取基本 LBP 算子:对每个像素点,以它为中心取半径为 1 的 8 个邻居灰度值,逐个与中心比较,大于等于记 1、小于记 0,按固定顺序拼接成 8 位二进制数,即该像素的 LBP 值。

void lbp_basic(uint8_t *gray, int width, int height, uint32_t *hist) { memset(hist, 0, 256 * sizeof(uint32_t)); // 边界像素不做 LBP,因为取不到完整邻域 for (int y = 1; y < height - 1; y++) { for (int x = 1; x < width - 1; x++) { uint8_t center = gray[y * width + x]; uint8_t code = 0; code |= (gray[(y-1)*width + x] >= center) << 7; code |= (gray[(y-1)*width + x+1] >= center) << 6; code |= (gray[y*width + x+1] >= center) << 5; code |= (gray[(y+1)*width + x+1] >= center) << 4; code |= (gray[(y+1)*width + x] >= center) << 3; code |= (gray[(y+1)*width + x-1] >= center) << 2; code |= (gray[y*width + x-1] >= center) << 1; code |= (gray[(y-1)*width + x-1] >= center) << 0; hist[code]++; } } }

逻辑说明:hist是 256 维的直方图数组,每个维度代表一种 LBP 编码模式出现的次数。代码中的移位顺序是“从左上角顺时针转一圈”,这个顺序必须和录入模板时保持一致,否则同一个人在不同姿态下的直方图会出现维度错位。我第一次实现时在录入和识别两个模块里用了不同的旋转顺序,结果识别率直接掉到三成。

把整个 64x64 的人脸图像做一次 LBP 得到的是全局直方图,缺点是丢失了空间信息——不同位置出现同一个纹理模式会被混在一起。所以在实际方案里,我把人脸图切成 4x4 的 block,每个 block 大小为 16x16,分别提取直方图后拼接成一个 16 * 256 = 4096 维的直方图。4096 个 uint8 存成文件确实不大,但对比计算会稍慢。为了平衡速度与准确率,我最终用了 2x2 分块,也就是 4 个 32x32 的 block,特征维度降到 1024,识别率没有明显下降。

3.3 距离度量与识别决策

特征比对用的距离函数是卡方距离(Chi-square distance),它对直方图数据的量纲差异不敏感,比欧氏距离更适合 LBP 直方图对比。代码如下:

float chi_square(uint8_t *h1, uint8_t *h2, int len) { float score = 0.0f; for (int i = 0; i < len; i++) { int a = h1[i]; int b = h2[i]; if (a + b == 0) continue; int diff = a - b; score += (float)(diff * diff) / (float)(a + b); } return score; }

参数说明:分数越小表示越相似。判断一个人是否为已注册用户,需要和库里所有模板对比,找到最小卡方距离。如果最小距离低于阈值(比如我在室内环境取 180.0),就判定识别成功并返回对应用户名;否则判定为陌生人。这个阈值是经验值,不同光照、不同分块策略下需要重新调。调参的方法我会在第五章讲:分别记录 20 张本人和 20 张陌生人的匹配分数,取两者分界线附近的值再留一点裕量。

决策层的逻辑还包含一个关键机制:连续三帧中至少两帧识别为同一人,才最终确认结果。这个简单的时间滤波把单帧误识别率从百分之六七降到了百分之一以下。代价是响应时间多了两帧的等待,对于门禁类应用来说,300ms 的延迟用户几乎感知不到。

4. 常见问题排查:四个翻车现场的复盘

4.1 花屏和图像错位

现象是 LCD 上显示的图像有明显的彩色条纹,或者图像左右两半是错开的。第一次遇到时我以为摄像头坏了,折腾了好半天才发现问题出在 DMA 配置上。

原因分析:DMA 的NDTR参数错了。RGB565 每个像素是 2 字节,DMA 配置成半字(16bit)传输时,NDTR应填像素数 76800,而配置成字节传输时才需要填 153600。我当时在字节模式下填了 76800,等于只传了半帧,后半段是上一帧的残留数据。解决方式:固定使用半字传输模式,NDTR恒填320 * 240。我把这两行写成了宏,注释里标明了“像素数不是字节数”,从那以后再没出过问题。

4.2 肤色检测把人脸框得乱七八糟

现象是肤色分割后,人脸区域内部出现大量空洞,或者把肤色相近的背景物件(比如木色桌面、脖子阴影)一起框进来。

原因分析:YCbCr 肤色范围是硬编码的,对光线色温变化太敏感。冷光 LED 下 Cr 值整体偏低,热光下 Cb 值会漂移。解决方式分两步:第一步,在初始化时做一次自动白平衡校准——拍一帧纯色画面,统计 YCbCr 分布,用均值动态修正阈值;第二步,在连通域过滤时加入“高光排除”和“低光排除”,把 Y 值大于 240 和小于 40 的像素强制标记为非肤色,因为高光溢出和噪点区特别容易产生假阳性。

4.3 SD 卡初始化失败或读写超时

现象是系统启动时卡在f_mount返回错误码,或者人脸录入时写文件十几秒才成功。

原因分析:大部分买到的是小容量的老卡,SPI 模式兼容性差,而且很多板载 TF 卡槽的电路没有做电平匹配。解决方式:换用 C10 以上等级的大厂卡,在sd_init里把 SPI 通信速率先降到 250kHz 完成初始化,再切换到 18MHz 做读写——这是很多驱动库默认不做的细节。另外我在卡槽电源引脚并了一个 10uF 电容,SD 卡上电瞬间的电流尖峰就不会引发握手失败。

4.4 识别率白天晚上差异极大

现象是白天识别正常,晚上开了暖光台灯后,同一个人频繁被判定为陌生人。

原因分析:LBP 特征对光照变化敏感,暖光环境下人脸明暗分布和白天完全不同,直方图偏移导致卡方距离一下子越过阈值。解决方式:在录入阶段采集两张图片——一张在正常白光下采集,一张在暖光下采集,分别存成f0.lbp和f1.lbp;识别时两个模板都参与对比,取最小值。这个土办法效果立竿见影,相当于做了最简单的人脸姿态和光照增强。想要更彻底的方案,可以在预处理阶段先做直方图均衡化再提取 LBP,代价是每帧增加约 15ms 的处理时间。

5. 完整落地:工程结构、烧录和调参

5.1 工程目录结构

整个工程基于 STM32CubeMX 生成 HAL 库框架,我把业务代码独立成三个目录,方便移植到其它芯片:

Core/ Inc/ Src/ main.c system_stm32f4xx.c APP/ det_skin.c // 肤色检测与连通域分析 feat_lbp.c // LBP 特征提取 rec_decision.c // 卡方距离比对与决策逻辑 ui_menu.c // LCD 菜单状态机 BSP/ bsp_dcmi.c // DCMI + DMA 驱动 bsp_ov2640.c // SCCB 配置和寄存器初始化 bsp_sd_fatfs.c // FatFS 移植层 bsp_ili9341.c // SPI 屏驱动 bsp_keypad.c // 按键扫描

在 CubeMX 里需要开启的时钟和外设包括:主频 168MHz、DCMI(PCLK 上升沿捕获)、DMA2 Stream1、GPIO(SAI 引脚复用到 DCMI 数据线)、SPI1(LCD)、SPI2(TF 卡)、USART2(日志输出)。OV2640 的 SCCB 可以用 I2C1 的软件模拟模式,但更稳的做法是 CubeMX 里直接配置 I2C1,因为硬件 I2C 自带时序控制,不需要自己写延时加减。我把 OV2640 的寄存器初始化序列做成了一个 272 行的静态表,按地址和值成对存放,ov2640_init里逐条写入。

5.2 烧录与运行

烧录用的是板载 ST-Link 连 SWD 接口,用标准库函数直接下载。上电后首先初始化外设,再挂载 FatFS,然后读face_db目录统计用户数。LCD 上会显示主菜单:录入、识别、删除。操作流程是:

按 KEY1 进入录入模式,将人脸对准镜头保持姿势,按 KEY2 触发采集。系统自动完成检测→提取→存卡,LCD 显示“Enroll OK”。录入第二张时保持头部稍微左转或偏右 15 度,这样模板对姿态变化更鲁棒。按 KEY3 进入识别模式,画面中央出现一个绿色的跟踪框(肤色区域外接矩形),矩形上方实时显示匹配分数。按 KEY4 进入删除模式,按上下键选择用户序号,再按确定删除。

串口终端用 115200 波特率可以实时看到调试信息,诸如[DET] face rect=(120,80,80,96) conf=0.87、[REC] match user_0001 score=142.3。我强烈建议保留串口日志,因为 LCD 上只显示了最终结果,调阈值时你需要的中间数据都靠串口打印,方便复盘。

5.3 阈值调节方法

识别阈值没有“万能值”,我的调试方法比较直接。先写一个小测试模式:在当前画面下人手动按键标记“我是本人”和“我不是本人”,各采集 20 次,把所有匹配分数打印到串口。正常情况下,本人分数分布在 100~180,陌生人分数分布在 220~400。取两个分布之间的中点再向内收缩一点,就是初始阈值。如果两类分布有重叠,说明特征提取环节有问题,大概率是人脸框定位不准导致背景被算进直方图,要先调肤色检测的几何校验参数。

另外有一个细节容易被忽略:录入时人脸框的位置和识别时不一致,比如录入时偏左、识别时偏右,会造成同类分数整体抬高。我加了人脸框居中校验模块,只有当外接矩形中心在图像中心区域(中心点偏移不超过正负 32 像素)时才允许录入。这样保证模板特征的人脸尺度一致,识别成功率会有一个可感知的提升。

6. 二次开发:把 LBP 方案升级成轻量神经网络

验证过整套链路后,我很建议你按自己的场景做三处改造。第一处是把 2x2 分块扩展成 4x4 甚至 6x6 分块,配合直方图均衡化,能提升姿态变化下的识别率,特征维度从 1024 升到 4096,卡方距离计算仍然能在 30ms 内完成。第二处是活体检测:在录入时额外采集两张间隔 0.3 秒的帧,比较人脸框的位移偏差和肤色面积变化,如果偏差为正且超过阈值就判定为“正在点头”,这是最简单的活体动作检测方案,能防住静态照片攻击。

第三处是换推理引擎。如果你手头有更高的预算,可以考虑把主控换成带 NPU 的芯片,代码框架完全不用推翻:只需要把feat_lbp.c替换成模型推理接口,在rec_decision.c里把卡方距离替换成 softmax 输出对比。模板存储格式已经在第二章设计成“文件头 + 特征数据”的结构,换成嵌入式的量化模型后,改的只有字节序和长度字段,整体工程结构不需要大动。这个过程会把识别准确率从“够用”拉到“商用”水平,但功耗和成本会同步上升,属于典型的取舍题。

最后分享一个我自己的习惯:凡是改动了图像预处理参数的提交,我一定会把旧参数和对应识别率记录在一个本地 txt 里,防止调参后识别率下降却不知道回退到哪一版。这套项目从画板到跑通前后花了两周,中间踩的那些坑全是实打实调试出来的。如果你也准备动手复现,建议先不用管美观度,把完整链路跑通再谈优化——先让数据流起来,后面每一步改动都有据可依。希望这篇记录能帮你少走几个弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询