基于STM32与MogFace的嵌入式人脸识别门禁系统设计
基于STM32与MogFace的嵌入式人脸识别门禁系统设计
最近在捣鼓一个智能门禁的小项目,想把AI人脸识别塞进一个巴掌大的嵌入式板子里。市面上的人脸识别方案要么太贵,要么功耗太高,不太适合家用或者小成本部署。琢磨了一下,决定用经典的STM32F103C8T6最小系统板做主控,再搭配一个摄像头和一个网络模块,把复杂的AI识别任务“外包”给一台小服务器。这样既能保证识别的准确度,又能把嵌入式端的成本和复杂度降下来。
这个方案的核心思路很简单:STM32负责“看”和“传”,它用摄像头拍下人脸照片,然后通过网络发给远端的服务器;服务器上跑着MogFace模型,专门负责“认”,识别出是谁后,再把结果告诉STM32;STM32收到指令,是熟人就开门,是陌生人就报警或者记录。整个过程听起来复杂,但拆解开来,每一步都有成熟的模块和代码可以参考,实现起来并没有想象中那么难。下面我就把这个从零搭建的过程,以及其中遇到的一些坑和解决办法,跟大家详细聊聊。
1. 系统整体设计与硬件选型
做任何嵌入式项目,第一步都是把蓝图画清楚,硬件选对。我们这个门禁系统,本质上是一个典型的物联网边缘计算节点。
1.1 系统架构与工作流程
整个系统可以分成三大部分:采集端、识别端和执行端。
- 采集端(STM32 + 摄像头):这是系统的“眼睛”。STM32F103C8T6单片机驱动OV7670摄像头模块(带FIFO,简化了时序控制),定时或者由外部触发(比如按门铃)拍摄一张人脸图像。
- 识别端(服务器 + MogFace):这是系统的“大脑”。我们在一台有公网IP的云服务器或者本地局域网内的电脑上,部署一个基于MogFace人脸检测与识别模型的WebAPI服务。它接收来自STM32的图片,进行人脸检测、特征提取和比对,最后返回识别结果(如用户ID或“陌生人”)。
- 执行端(STM32 + 门锁):这是系统的“手”。STM32根据服务器返回的识别结果,通过一个继电器模块来控制电磁锁的开关,实现开门或拒绝操作。同时,也可以通过串口或者一个OLED小屏幕来显示识别状态。
它们之间的数据流是这样的:摄像头拍照 -> STM32压缩/编码图片 -> 通过网络模块发送HTTP POST请求到服务器 -> 服务器调用MogFace模型识别 -> 服务器返回JSON格式结果 -> STM32解析结果并控制继电器。
1.2 核心硬件清单与选型考量
硬件不在多,而在够用和好搭配。下面是我用到的核心部件:
-
主控芯片:STM32F103C8T6最小系统板
- 为什么选它:大名鼎鼎的“蓝色药丸”,性价比之王。72MHz主频,20KB RAM,64KB Flash,对于运行图像采集、网络通信和控制逻辑的代码绰绰有余。社区资源极其丰富,遇到问题基本都能找到答案。
- 关键作用:协调所有外设,执行核心控制逻辑,是整个系统的指挥官。
-
图像采集:OV7670摄像头模块(带AL422B FIFO)
- 为什么选它:便宜,30万像素对于近距离人脸识别足够用。最关键的是自带FIFO芯片,可以缓存一帧图像,STM32可以像读存储器一样慢慢把数据读出来,完美避开了直接驱动OV7670时严苛的时序要求,大大降低了开发难度。
- 注意点:需要额外连接一个24MHz的有源晶振给OV7670提供时钟。
-
网络通信:ESP8266-01S WiFi模块
- 为什么选它:几块钱的成本,就能让STM32接入网络。通过AT指令与STM32通信,我们可以让它连接家里的WiFi,然后代表STM32向服务器发送HTTP请求。虽然也可以直接用STM32的串口转以太网模块,但ESP8266在成本和便捷性上优势明显。
- 工作模式:我们将其设置为STA模式,连接到无线路由器,然后使用TCP连接与服务器通信。
-
执行机构:5V继电器模块
- 作用:STM32的IO口驱动能力很弱,无法直接控制12V的电磁锁。继电器模块就是一个用弱电(STM32的3.3V)控制强电(锁的12V)的电子开关。
- 连接:STM32的IO口控制继电器模块的输入脚,继电器的常开触点串联在电磁锁的供电回路中。
-
其他:USB转TTL串口模块(用于调试和烧录程序)、12V电源(给电磁锁供电)、杜邦线若干。
硬件连接好后,实物就像个“蜘蛛网”,但逻辑是清晰的:摄像头和ESP8266接在STM32的IO口和串口上,继电器接在另一个IO口,电源各自供好。
2. 嵌入式端开发:图像采集与网络传输
硬件搭好了,接下来就是让STM32“动”起来。这部分代码主要在Keil MDK或者STM32CubeIDE里开发。
2.1 摄像头图像采集与JPEG编码
OV7670带FIFO的方案,驱动起来比较省心。
// 示例:读取一帧图像数据到缓冲区
void CAMERA_CaptureFrame(uint8_t *frame_buffer) {
// 1. 发送复位和开始捕获命令给FIFO控制器
FIFO_WRST_LOW();
FIFO_WRST_HIGH();
FIFO_RRST_LOW();
FIFO_RRST_HIGH();
FIFO_RCK_LOW();
delay_us(100);
FIFO_OE_ENABLE(); // 使能FIFO输出
// 2. 启动OV7670曝光一帧
CAMERA_StartCapture();
while(CAMERA_IsCaptureDone() == 0); // 等待捕获完成
// 3. 从FIFO中读取一帧数据 (假设为QVGA: 320*240)
uint32_t i;
for(i = 0; i < (320*240*2); i++) { // 2 bytes per pixel (RGB565)
FIFO_RCK_HIGH();
delay_ns(50);
*frame_buffer++ = READ_DATA_PORT(); // 读取高字节
FIFO_RCK_LOW();
delay_ns(50);
*frame_buffer++ = READ_DATA_PORT(); // 读取低字节
FIFO_RCK_LOW();
}
FIFO_OE_DISABLE(); // 关闭FIFO输出
}
读出来的数据是RGB565格式的,体积比较大(3202402 ≈ 150KB)。直接通过网络发送非常慢且占用带宽。必须压缩。我们可以在STM32端集成一个轻量级的JPEG编码库(如 tiny_jpeg),将150KB的RGB数据压缩到5-10KB,传输效率提升十几倍。
// 伪代码:JPEG编码
uint32_t jpeg_encode_rgb565_to_buffer(uint8_t *rgb565_buffer, uint8_t *jpeg_buffer, int width, int height, int quality) {
// 调用tiny_jpeg库函数进行压缩
// 返回编码后JPEG数据的实际大小
return encoded_size;
}
2.2 通过ESP8266发送HTTP请求
STM32通过串口(USART)以AT指令的方式指挥ESP8266。
// 示例:配置ESP8266并发送HTTP POST请求 (伪代码,需根据实际AT指令调整)
void ESP8266_SendImage(const uint8_t *jpeg_data, uint32_t jpeg_size) {
char cmd[256];
uint8_t resp[512];
// 1. 连接到WiFi (假设已配置好SSID和密码)
// sendATCommand("AT+CWJAP=\"SSID\",\"PASSWORD\"", resp, 2000);
// 2. 建立TCP连接到服务器(假设服务器IP为192.168.1.100,端口5000)
sprintf(cmd, "AT+CIPSTART=\"TCP\",\"%s\",%d", SERVER_IP, SERVER_PORT);
sendATCommand(cmd, resp, 3000);
// 3. 准备HTTP POST请求数据
// 首先计算整个POST数据的长度
uint32_t content_len = jpeg_size;
uint32_t total_len = snprintf(NULL, 0, POST_HEADER_TEMPLATE, content_len) + content_len;
// 发送数据长度指令
sprintf(cmd, "AT+CIPSEND=%d", total_len);
sendATCommand(cmd, resp, 1000); // 等待 >
// 4. 发送HTTP头部和JPEG图像体
sprintf(cmd, POST_HEADER_TEMPLATE, content_len); // 生成头部
uart_send_string(USART2, cmd); // 发送头部
uart_send_binary(USART2, jpeg_data, jpeg_size); // 发送JPEG二进制数据
// 5. 等待并解析服务器响应
// 响应通常是JSON,如 {"status":"success", "user_id":"1001"} 或 {"status":"unknown"}
// 解析后,根据user_id控制继电器
}
这里的关键是构造正确的HTTP POST请求。头部需要指定 Content-Type: image/jpeg 和正确的 Content-Length。服务器地址和端口需要替换成你自己部署服务的地址。
3. 服务器端搭建:MogFace WebAPI服务
嵌入式端把“脏活累活”都干完了,服务器端就专心做它擅长的AI推理。我们用Python的Flask框架来快速搭建一个WebAPI。
3.1 环境准备与模型加载
首先在服务器上安装必要的库:Flask, opencv-python, numpy,以及MogFace相关的依赖。MogFace是一个优秀的人脸检测模型,我们可以用它先检测出人脸,再用人脸识别模型(如ArcFace)提取特征,并与数据库中的特征进行比对。
# app.py
from flask import Flask, request, jsonify
import cv2
import numpy as np
import json
# 假设有MogFace和特征提取的封装库
from face_detector import MogFaceDetector
from face_recognizer import ArcFaceRecognizer
app = Flask(__name__)
# 初始化模型(启动时加载,避免每次请求都加载)
detector = MogFaceDetector(model_path='./models/mogface.pth')
recognizer = ArcFaceRecognizer(model_path='./models/arcface.pth')
# 加载已知人脸特征数据库
face_db = {}
try:
with open('face_database.json', 'r') as f:
face_db = json.load(f) # 格式: {"user_id_1": [feat1, feat2...], ...}
except FileNotFoundError:
pass
print("服务器模型加载完成,等待请求...")
3.2 设计识别API接口
我们设计一个 /recognize 的POST接口,接收图片,返回识别结果。
@app.route('/recognize', methods=['POST'])
def recognize_face():
if 'image' not in request.files:
return jsonify({'status': 'error', 'msg': 'No image file provided'}), 400
file = request.files['image']
# 将上传的文件数据转换为OpenCV图像格式
img_data = np.frombuffer(file.read(), np.uint8)
img = cv2.imdecode(img_data, cv2.IMREAD_COLOR)
if img is None:
return jsonify({'status': 'error', 'msg': 'Invalid image data'}), 400
# 步骤1: 使用MogFace进行人脸检测
bboxes, landmarks = detector.detect(img)
if len(bboxes) == 0:
return jsonify({'status': 'success', 'user_id': 'unknown', 'msg': 'No face detected'})
# 假设只处理第一张检测到的人脸
x1, y1, x2, y2 = map(int, bboxes[0])
face_img = img[y1:y2, x1:x2]
# 步骤2: 对齐人脸(根据landmarks,可选但推荐)
aligned_face = align_face(img, landmarks[0])
# 步骤3: 使用ArcFace提取人脸特征
face_feature = recognizer.extract_feature(aligned_face)
# 步骤4: 与数据库中的特征进行比对
user_id = "unknown"
min_dist = 0.6 # 距离阈值,可调整
for db_id, db_features in face_db.items():
for db_feat in db_features:
# 计算余弦相似度或欧氏距离
dist = np.linalg.norm(face_feature - db_feat)
if dist < min_dist:
min_dist = dist
user_id = db_id
break
# 步骤5: 返回识别结果
return jsonify({
'status': 'success',
'user_id': user_id,
'confidence': float(1 - min_dist), # 转换为置信度
'face_count': len(bboxes)
})
def align_face(img, landmark):
# 简单的人脸对齐函数,可根据5个或68个关键点实现
# 这里省略具体实现,对齐能显著提升识别精度
return aligned_img
if __name__ == '__main__':
# 监听所有网络接口,端口5000
app.run(host='0.0.0.0', port=5000, debug=False) # 生产环境需关闭debug
这个API做了几件事:接收图片、检测人脸、提取特征、比对数据库、返回JSON结果。face_database.json 需要你预先用已知人脸的图片提取特征并存储进去,这个过程通常称为“注册”。
4. 系统联调与实战效果
代码都写好了,接下来就是最激动人心也最容易出错的环节——把各部分连起来跑通。
4.1 联调步骤与问题排查
-
分模块测试:
- STM32 + 摄像头:先用串口把拍到的图片数据打印出来,或者用SD卡保存一张,在电脑上看看能不能正常显示,确保采集环节没问题。
- STM32 + ESP8266:单独测试AT指令,确保能连上WiFi,能Ping通服务器,能发送一个简单的HTTP GET请求并收到回复。
- 服务器API:用Postman或者curl工具,模拟STM32发送一个JPEG图片的POST请求,看API是否能正确返回人脸识别结果。
-
集成测试:
- 将JPEG编码和HTTP发送整合到STM32的主循环里。可以先设定一个按键,每按一次,就拍一张照、编码、发送一次。
- 在服务器端,详细打印日志,记录接收到的图片大小、检测到的人脸数、识别结果等,方便定位问题。
-
常见坑点:
- 内存不足:STM32的RAM很小,大的图像缓冲区容易导致崩溃。务必合理分配内存,使用全局数组或动态内存时要小心。
- 网络超时:ESP8266连接服务器或发送数据可能失败。代码里一定要有重试机制和超时判断。
- 数据格式错误:HTTP请求头格式不对,或者JPEG数据不完整,会导致服务器无法解析。用网络调试工具仔细对比请求包。
- 识别精度:光线、角度、遮挡都会影响识别率。需要在不同条件下测试,并考虑在服务器端加入活体检测(如眨眼、张嘴判断)以增强安全性。
4.2 实际运行展示与体验
当所有绿灯亮起,系统跑通的那一刻,成就感十足。实际运行起来是这样的:
- 有人走到门前,STM32通过红外传感器或定时触发摄像头拍照。
- 大约1-2秒内(时间主要花在JPEG编码和网络传输上),STM32的OLED屏幕上会显示“识别中...”。
- 服务器识别完成后,结果通过WiFi返回。如果识别为注册用户(比如
user_id: "1001"),STM32会控制继电器吸合1秒钟,“咔哒”一声,电磁锁打开,OLED显示“欢迎回家,张三”。如果是陌生人,则继电器不动作,OLED显示“识别失败”,并可能触发蜂鸣器报警。 - 整个识别过程的延迟,在局域网内可以控制在2-3秒,基本满足门禁的实时性要求。识别准确率在光线良好的室内,对于注册过的人脸可以达到95%以上。
这个方案的优势很明显:成本低(主控板+模块不到百元)、灵活(识别算法在服务器端,可以随时升级替换模型)、功耗相对可控(STM32和ESP8266都可以进入低功耗模式)。当然,它依赖网络,如果服务器宕机或者网络中断,门禁会失效,所以适合对网络环境比较有把握的场景,或者作为备用方案。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐


所有评论(0)