资讯动态

STM32单片机离线人脸识别:LBP特征提取与卡方距离比对实战

发布时间:2026/10/21 13:39:33 来源:尧图企业网站定制
简介面向嵌入式开发者的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 协议配置成 QVGA320x240这个分辨率刚好在“细节够用”和“处理量可控”之间。存储端加一张 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 x1] center) 6; code | (gray[y*width x1] center) 5; code | (gray[(y1)*width x1] center) 4; code | (gray[(y1)*width x] center) 3; code | (gray[(y1)*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、DCMIPCLK 上升沿捕获、DMA2 Stream1、GPIOSAI 引脚复用到 DCMI 数据线、SPI1LCD、SPI2TF 卡、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) conf0.87、[REC] match user_0001 score142.3。我强烈建议保留串口日志因为 LCD 上只显示了最终结果调阈值时你需要的中间数据都靠串口打印方便复盘。5.3 阈值调节方法识别阈值没有“万能值”我的调试方法比较直接。先写一个小测试模式在当前画面下人手动按键标记“我是本人”和“我不是本人”各采集 20 次把所有匹配分数打印到串口。正常情况下本人分数分布在 100180陌生人分数分布在 220400。取两个分布之间的中点再向内收缩一点就是初始阈值。如果两类分布有重叠说明特征提取环节有问题大概率是人脸框定位不准导致背景被算进直方图要先调肤色检测的几何校验参数。另外有一个细节容易被忽略录入时人脸框的位置和识别时不一致比如录入时偏左、识别时偏右会造成同类分数整体抬高。我加了人脸框居中校验模块只有当外接矩形中心在图像中心区域中心点偏移不超过正负 32 像素时才允许录入。这样保证模板特征的人脸尺度一致识别成功率会有一个可感知的提升。6. 二次开发把 LBP 方案升级成轻量神经网络验证过整套链路后我很建议你按自己的场景做三处改造。第一处是把 2x2 分块扩展成 4x4 甚至 6x6 分块配合直方图均衡化能提升姿态变化下的识别率特征维度从 1024 升到 4096卡方距离计算仍然能在 30ms 内完成。第二处是活体检测在录入时额外采集两张间隔 0.3 秒的帧比较人脸框的位移偏差和肤色面积变化如果偏差为正且超过阈值就判定为“正在点头”这是最简单的活体动作检测方案能防住静态照片攻击。第三处是换推理引擎。如果你手头有更高的预算可以考虑把主控换成带 NPU 的芯片代码框架完全不用推翻只需要把feat_lbp.c替换成模型推理接口在rec_decision.c里把卡方距离替换成 softmax 输出对比。模板存储格式已经在第二章设计成“文件头 特征数据”的结构换成嵌入式的量化模型后改的只有字节序和长度字段整体工程结构不需要大动。这个过程会把识别准确率从“够用”拉到“商用”水平但功耗和成本会同步上升属于典型的取舍题。最后分享一个我自己的习惯凡是改动了图像预处理参数的提交我一定会把旧参数和对应识别率记录在一个本地 txt 里防止调参后识别率下降却不知道回退到哪一版。这套项目从画板到跑通前后花了两周中间踩的那些坑全是实打实调试出来的。如果你也准备动手复现建议先不用管美观度把完整链路跑通再谈优化——先让数据流起来后面每一步改动都有据可依。希望这篇记录能帮你少走几个弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑