从零构建OpenMV人脸识别门禁系统:硬件选型与代码实战
1. 项目缘起:为什么选择OpenMV做人脸识别门禁?
大家好,我是老张,在嵌入式视觉和智能硬件这块摸爬滚打了十来年。今天想和大家聊聊一个特别有意思、也特别有成就感的DIY项目——用OpenMV做一个人脸识别的智能门禁系统。你可能在办公室、小区单元楼或者一些科技展会上见过类似的东西,感觉挺高大上的,但其实自己动手做一个,成本并不高,技术门槛也比想象中低很多。
为什么我推荐用OpenMV来做这件事呢?首先,它是个“开箱即用”的视觉模块。对于很多刚入门的朋友来说,最头疼的就是摄像头驱动、图像采集、算法移植这些底层工作。OpenMV把这些都打包好了,它内置了MicroPython解释器,你直接用Python脚本就能调用摄像头、处理图像、运行机器学习的模型,省去了大量搭建环境、编译代码的麻烦。其次,它足够小巧和低功耗。一个火柴盒大小的板子,集成了摄像头、处理器和存储,非常适合嵌入到门禁、锁具这类对体积和功耗敏感的设备里。最后,它的生态很友好。社区活跃,有大量的示例代码和库,像人脸检测、特征提取(LBP算法)这些功能都有现成的API,你不需要从零开始写复杂的图像处理代码。
那么,这个门禁系统能做什么呢?简单说,就是“认脸开门”。系统会预先录入授权人员的人脸照片,当有人站在门前时,OpenMV会实时捕捉画面,进行人脸检测和识别。如果识别出是授权人员,它就会通过串口发送一个指令给控制板(比如Arduino),控制板再驱动一个舵机或电磁锁,实现自动开门。整个过程完全自动化,既安全又便捷。这个项目非常适合对物联网、嵌入式AI感兴趣的硬件爱好者、学生创客,或者想给自家工作室、车库增加一点科技感的极客们。接下来,我就手把手带你从硬件选型到代码实战,把这个系统搭建起来。
2. 硬件清单与连接:打造系统的“躯干”
动手之前,咱们得先把“家伙事儿”备齐。这个项目的硬件核心就三样:OpenMV摄像头模块、主控板(这里我们用Arduino)和执行机构(舵机)。别担心,这些都是非常常见且廉价的电子元件。
2.1 核心硬件选型指南
1. OpenMV摄像头模块: 市面上主要有OpenMV Cam H7和OpenMV Cam M7等型号。对于人脸识别门禁这个场景,我强烈推荐OpenMV Cam H7。原因很简单,它的处理器更强(ARM Cortex-M7),运行人脸检测和LBP特征匹配这类计算时更流畅,帧率更高,识别体验更好。虽然M7版本更便宜,但处理速度会慢一些,在光线稍差或者需要快速响应的门禁场景下可能会卡顿。投资一个好点的核心,整个系统的稳定性和用户体验会提升一个档次。
2. 主控板(Arduino): OpenMV本身有GPIO可以控制简单外设,但为了系统架构更清晰(视觉识别和控制逻辑分离),也为了未来扩展更多功能(比如加个键盘、显示屏、网络模块),我们选择用Arduino作为主控。型号上,Arduino Uno或Arduino Nano就完全够用了。它们价格便宜,资料丰富,驱动舵机更是小菜一碟。
3. 执行机构(舵机): 舵机负责转动门锁的机械部分。你需要一个180度或270度的标准舵机(比如SG90或MG996R)。SG90扭矩小(1.8kg/cm),适合轻型的模型锁;如果你要驱动真正的插销式门锁,建议选择扭矩更大的MG996R(10kg/cm以上)。别忘了,舵机工作电压通常是5V,但MG996R在高扭矩运行时电流可能超过1A,所以务必为舵机准备一个独立的5V/2A电源,不要直接从Arduino板子上取电,否则很容易烧坏板载的稳压芯片,这是我踩过的一个坑。
4. 其他必要配件:
- 杜邦线:公对公、公对母都需要,用于连接。
- 电源:两个部分需要供电。OpenMV和Arduino可以通过一个5V/2A的USB电源适配器供电。舵机则需要前述的独立电源。
- 面包板:方便接线和测试。
- 电阻和LED(可选):可以加个状态指示灯,比如识别成功亮绿灯,失败亮红灯,这样更直观。
2.2 硬件连接实战图解
连接是关键,接错了轻则不工作,重则烧设备。大家跟着我的步骤来:
- OpenMV与Arduino通信:我们需要让OpenMV把识别结果告诉Arduino。这里使用串口通信(UART)。将OpenMV的TX引脚连接到Arduino的RX引脚(通常是D0),将OpenMV的RX引脚连接到Arduino的TX引脚(通常是D1)。注意:在连接时,务必确保OpenMV和Arduino的GND(地线)连接在一起,这是保证通信稳定的基础,很多奇怪的通信故障都是因为地线没共接。
- Arduino控制舵机:舵机有三根线:信号线(通常是橙色或白色)、电源正极(红色)、地线(棕色或黑色)。将舵机信号线连接到Arduino的一个PWM引脚(例如D9),正极和地线连接到为舵机准备的独立5V电源上。同样,这个独立电源的地线也必须和Arduino的GND连接在一起,确保所有设备共地。
- 供电:用USB线为OpenMV和Arduino供电。舵机电源独立供电。
为了方便理解,我画一个简单的连接关系图(文字描述):
[5V USB适配器] --> [OpenMV] & [Arduino] (提供逻辑电源)
| (串口TX) ---> (Arduino RX)
| (串口RX) <--- (Arduino TX)
| (GND) --------- (Arduino GND) --- (舵机电源GND)
[舵机独立5V/2A电源] --> [舵机红黑线]
[Arduino PWM Pin D9] --> [舵机信号线]
这样,硬件“躯干”就搭建完毕了。接下来,我们给这个躯干注入“灵魂”——代码。
3. 代码实战(上):OpenMV端的人脸录入与识别
代码部分我们分两块讲,先在OpenMV上完成人脸图像的采集和识别算法,再在Arduino上编写控制逻辑。OpenMV的代码我们用MicroPython来写,非常直观。
3.1 人脸图像采集:建立你的“人脸库”
人脸识别的前提是有一个已知人脸的数据库。我们需要写一个脚本,让OpenMV自动拍摄并保存授权人员的人脸照片。这个步骤就像给系统“登记人脸”。
# 文件:face_enrollment.py
# 功能:人脸录入脚本
import sensor, image, pyb
# 初始化LED,用于拍照提示
RED_LED_PIN = 1
BLUE_LED_PIN = 3
# 初始化摄像头
sensor.reset()
sensor.set_pixformat(sensor.GRAYSCALE) # 使用灰度图,处理速度快
sensor.set_framesize(sensor.B128X128) # 设置图像大小
sensor.set_windowing((92, 112)) # 开窗,裁剪出92x112的人脸区域(标准)
sensor.skip_frames(time=2000) # 等待摄像头稳定
# 关键参数设置
num = 1 # 被拍摄者编号:1号人员
n = 30 # 计划为每个人拍摄30张样本
print("准备录入第 %d 位人员的人脸,请面对摄像头..." % num)
while(n):
# 第一阶段:红灯亮,提示准备
pyb.LED(RED_LED_PIN).on()
sensor.skip_frames(time=3000) # 等待3秒,让人调整姿势
pyb.LED(RED_LED_PIN).off()
# 第二阶段:蓝灯亮,开始拍照
pyb.LED(BLUE_LED_PIN).on()
# 捕获一帧图像
img = sensor.snapshot()
# 保存图像到SD卡。路径格式:singtown/s1/30.pgm
# ‘singtown’是文件夹,’s1‘是1号人员的子文件夹
img.save("singtown/s%s/%s.pgm" % (num, n))
pyb.LED(BLUE_LED_PIN).off()
print("已拍摄第 %d 张样本" % (30 - n + 1))
n -= 1
print("第 %d 位人员人脸录入完成!请更换下一位。" % num)
代码解读与实操要点:
- 图像格式与尺寸:我们使用
GRAYSCALE(灰度图),因为人脸识别中的LBP特征在灰度图上计算效率最高。set_windowing((92,112))是一个关键操作,它把摄像头视野固定在这个分辨率上。这个尺寸是很多人脸识别算法的标准输入尺寸,能保证我们抓取到的是正脸区域。 - 参数
num和n:num是人员ID,第一个人就是1,照片会保存在s1文件夹。录入下一个人时,记得把num改成2,以此类推。n是每人采集的样本数。我强烈建议n至少设为20,最好30以上。样本越多,涵盖的表情、轻微角度变化就越多,后续识别的容错率就越高。只拍三五张的话,稍微侧个脸可能就认不出来了。 - 拍摄流程:用红蓝LED指示状态是个好习惯。红灯亮是“准备”,给人时间摆好姿势;蓝灯亮瞬间“拍照”,确保抓拍的是静态清晰图像。拍完一张后,脚本会循环继续拍下一张,直到拍满n张。
- 存储:确保你的OpenMV插上了SD卡,并且根目录下有一个
singtown文件夹(可以手动创建)。脚本会自动在singtown下创建s1,s2等子文件夹来存放不同人的照片。
踩坑提醒:拍摄环境的光线要均匀,避免一侧过亮或过暗。让人脸尽量充满窗口,但也不要太近导致超出范围。最好能采集一些带有轻微表情(微笑)和微小头部转动的照片,这样建立的人脸库更健壮。
3.2 人脸识别与串口通信:让摄像头“思考”并“说话”
建好人脸库后,我们就可以编写主识别脚本了。这个脚本会实时运行,完成“检测人脸 -> 提取特征 -> 与库中比对 -> 发送结果”这一系列动作。
# 文件:face_recognition_doorlock.py
# 功能:实时人脸识别并控制门禁
import sensor, image, time, pyb
# 初始化摄像头(参数与录入时保持一致!)
sensor.reset()
sensor.set_pixformat(sensor.GRAYSCALE)
sensor.set_framesize(sensor.B128X128)
sensor.set_windowing((92, 112))
sensor.skip_frames(time=3000) # 更长的时间让摄像头适应环境光
# 初始化串口:与Arduino通信的桥梁
uart = pyb.UART(3, 9600) # 使用UART3,波特率9600。请根据你的实际接线选择UART口(1或3)
# 系统参数配置
NUM_SUBJECTS = 3 # 人脸库中总共有3个人(即s1, s2, s3文件夹)
NUM_SUBJECTS_IMGS = 30 # 每个人有30张样本图(与录入时的n一致)
# 初始化舵机对象(如果舵机直接接在OpenMV上,可用此方式。但我们用Arduino控制,故注释掉)
# servo = pyb.Servo(1)
print("人脸识别门禁系统启动...")
while(True):
# 1. 捕获当前图像
img = sensor.snapshot()
# 2. 提取当前人脸的LBP特征
# LBP(局部二值模式)是一种纹理特征描述子,非常适合用来描述人脸。
d0 = img.find_lbp((0, 0, img.width(), img.height()))
# 3. 与人脸库进行比对
pmin = 999999 # 初始化最小差异度为一个很大的数
match_id = 0 # 初始化匹配到的人员ID为0(表示未知)
for s in range(1, NUM_SUBJECTS+1): # s从1遍历到3
dist_sum = 0 # 累计与某人所有样本的差异度
for i in range(1, NUM_SUBJECTS_IMGS+1): # i从1遍历到30
# 加载库中第s个人的第i张图片
img_lib = image.Image("singtown/s%d/%d.pgm" % (s, i))
# 提取库中图片的LBP特征
d1 = img_lib.find_lbp((0, 0, img_lib.width(), img_lib.height()))
# 计算当前人脸特征d0与库中特征d1的差异度,并累加
dist_sum += image.match_descriptor(d0, d1)
# 计算平均差异度
avg_dist = dist_sum / NUM_SUBJECTS_IMGS
print("Subject %d 平均差异度: %d" % (s, avg_dist))
# 找出平均差异度最小的人,即最匹配的人
if avg_dist < pmin:
pmin = avg_dist
match_id = s
# 4. 判断识别结果并执行动作
print("-- 最终结果 --")
print("最匹配ID: %d, 最小差异度: %d" % (match_id, pmin))
# 设置一个差异度阈值。低于阈值才认为是有效匹配。
# 这个阈值需要根据实际测试调整。太小会拒识,太大会误识。
THRESHOLD = 5000
if match_id != 0 and pmin < THRESHOLD:
# 识别成功
print(">>> 识别成功!人员ID: %d" % match_id)
# 通过串口发送开门指令。这里我们约定发送人员ID的字符格式。
uart.write(str(match_id) + '\n') # 发送如 "2\n"
print("已发送开门指令至Arduino。")
# 等待一段时间,避免连续触发
time.sleep_ms(3000)
else:
# 识别失败(可能是陌生人,或差异度过大)
print(">>> 识别失败:陌生人或置信度不足。")
# 可以发送一个特定的失败指令,比如 '0'
# uart.write('0\n')
time.sleep_ms(1000)
代码核心逻辑与调试技巧:
- LBP特征比对:这是代码的核心。
find_lbp()函数会计算图像的LBP特征图,match_descriptor()会计算两个特征图之间的差异度(距离)。距离值越小,说明两张脸越相似。我们遍历库中每个人的所有照片,分别计算与当前人脸的差异度并求平均,最后选出平均差异度最小的那个人作为匹配结果。 - 阈值(THRESHOLD)是关键:这是整个系统的“安全阀”。如果最小差异度
pmin仍然大于你设定的THRESHOLD,即使找到了一个相对最像的,系统也会判定为“识别失败”(陌生人)。这个阈值需要你根据实际测试来校准。方法:让已录入人员站在摄像头前,记录下成功识别时的pmin值(通常会是一个几千的数字);再让一个陌生人试试,记录pmin值。取一个介于两者之间的值作为阈值。我一开始没设阈值,结果光线一变,明明是我自己,差异度却很大,导致识别失败;或者有时会把陌生人误认成库中人,阈值设好后,这些问题就基本解决了。 - 串口通信协议:我们定义了一个简单的文本协议。识别到ID为2的人,就通过
uart.write('2\n')发送字符2和一个换行符\n。换行符是告诉Arduino一条指令结束了,方便它解析。你也可以设计更复杂的协议,比如OPEN:2。 - 循环与延时:识别成功后,我加了
time.sleep_ms(3000)的延时。这是为了防止门刚打开,人在离开过程中,摄像头又立刻捕捉到人脸并触发二次识别,导致舵机反复动作。这个“不响应期”可以根据门的实际开关时间来调整。
4. 代码实战(下):Arduino端的指令解析与舵机控制
OpenMV已经变成了一个“会看脸并报号”的智能传感器,接下来就需要一个“听话的手”——Arduino来接收指令并控制舵机动作。
// 文件:arduino_door_controller.ino
// 功能:接收串口指令,控制舵机开关门
#include <Servo.h>
// 定义舵机对象和引脚
Servo myServo;
const int servoPin = 9;
// 定义门的状态
int doorAngleClosed = 10; // 门关闭时舵机的角度(需根据实际安装调整)
int doorAngleOpen = 100; // 门打开时舵机的角度(需根据实际安装调整)
int doorState = 0; // 0表示关,1表示开
// 串口接收相关变量
String inputString = ""; // 用于存储接收到的字符串
bool stringComplete = false; // 标志是否收到完整字符串(以换行符结尾)
void setup() {
// 初始化串口,波特率必须与OpenMV设置一致
Serial.begin(9600);
// 预留一点时间让串口稳定
delay(100);
// 初始化舵机
myServo.attach(servoPin);
myServo.write(doorAngleClosed); // 上电后确保门是关闭状态
doorState = 0;
// 初始化接收字符串
inputString.reserve(10); // 预留10字节空间
Serial.println("Arduino门禁控制器就绪...");
Serial.println("等待OpenMV指令...");
}
void loop() {
// 检查是否收到完整的串口指令
if (stringComplete) {
// 去除可能的回车换行符
inputString.trim();
Serial.print("收到指令: ");
Serial.println(inputString);
// 解析指令
int personId = inputString.toInt(); // 将字符串转换为整数
// 判断:如果ID在有效范围内(例如1-3),则执行开门动作
if (personId >= 1 && personId <= 3) {
openDoor();
// 开门后,等待5秒,然后自动关门
delay(5000);
closeDoor();
} else if (personId == 0) {
// 这里可以处理识别失败的指令,例如闪灯报警
Serial.println("识别失败,禁止通行!");
// 例如:digitalWrite(ALARM_LED, HIGH); delay(500); digitalWrite(ALARM_LED, LOW);
} else {
Serial.println("未知指令,忽略。");
}
// 清空字符串,准备接收下一条指令
inputString = "";
stringComplete = false;
}
// 主循环其他任务(如果有)可以放在这里
}
// 串口事件中断函数,当有数据到达时自动调用
void serialEvent() {
while (Serial.available()) {
char inChar = (char)Serial.read(); // 读取一个字符
if (inChar == '\n') { // 如果收到换行符,说明一条指令结束
stringComplete = true;
} else {
inputString += inChar; // 否则,将字符添加到字符串中
}
}
}
// 开门函数
void openDoor() {
if (doorState == 0) { // 只有门是关着的时候才执行开门
Serial.println("执行开门动作...");
myServo.write(doorAngleOpen);
delay(500); // 等待舵机转动到位
doorState = 1;
Serial.println("门已打开。");
}
}
// 关门函数
void closeDoor() {
if (doorState == 1) { // 只有门是开着的时候才执行关门
Serial.println("执行关门动作...");
myServo.write(doorAngleClosed);
delay(500); // 等待舵机转动到位
doorState = 0;
Serial.println("门已关闭。");
}
}
Arduino代码精讲与安全考量:
- 串口数据接收:我们没有使用简单的
Serial.read(),而是利用了serialEvent()中断函数和字符串拼接的方式。这样做的好处是能稳定地接收不定长的指令(如"2\n"),避免数据接收不完整。stringComplete标志位是线程安全的常见做法。 - 指令解析与动作执行:收到字符串后,用
toInt()转换成整数,然后判断其范围。这里我们约定1-3是合法ID,触发开门;0是识别失败;其他值忽略。你可以根据录入的人数修改这个范围。 - 舵机控制与状态机:我们定义了
openDoor()和closeDoor()两个函数,并引入了doorState变量来记录门的状态。这是一个简单的状态机,可以防止重复开门或关门。比如,在门已经打开的情况下,即使又收到开门指令,也不会让舵机乱转。 - 自动关门与安全:代码里设定开门后延迟5秒自动关门。在实际门禁应用中,这个逻辑需要谨慎设计。更好的做法是结合人体传感器(如红外热释电传感器)判断门前是否还有人,或者增加一个手动关门按钮。直接定时关门存在夹伤风险。我这里只是为了演示自动控制的完整性。
- 调试信息:通过
Serial.println()输出大量状态信息到Arduino的串口监视器,这对于调试硬件连接和通信协议至关重要。当你发现门没反应时,首先就应该打开串口监视器,看看有没有收到OpenMV发来的指令。
5. 系统集成、调试与优化思路
硬件连好了,代码也写完了,接下来就是最激动人心的环节——把各部分组装起来,上电测试!
集成与上电步骤:
- 分别测试:先不要全部连在一起。用OpenMV的IDE运行人脸录入脚本,确保能正常拍照并保存到SD卡。然后运行识别脚本,在IDE的串口终端里观察打印的差异度数据,对着摄像头看看能否正确识别出已录入的人脸。接着,单独测试Arduino,上传控制代码,打开串口监视器,手动输入
1、2、3然后回车,看舵机是否能正确转动到指定角度。 - 通信联调:将OpenMV的TX/RX与Arduino的RX/TX交叉连接,共地。打开Arduino的串口监视器,再运行OpenMV的识别脚本。当识别成功时,你应该能在Arduino的串口监视器里看到“收到指令: X”的提示,同时舵机动作。这是最关键的一步,确保指令流畅通无阻。
- 机械安装:将舵机通过支架固定在门或锁具上,连接好连杆。这里需要反复调试
doorAngleOpen和doorAngleClosed这两个角度值,确保它们对应门锁的实际开、闭位置。可能需要多次修改代码、上传测试。
常见问题与调试“药方”:
- 问题一:OpenMV串口发数据了,但Arduino没反应。
- 检查1:波特率。双方
Serial.begin(9600)和uart = pyb.UART(3, 9600)的波特率必须一模一样。 - 检查2:接线。确认TX->RX, RX->TX交叉连接,并且GND一定共接。
- 检查3:串口端口。OpenMV的UART口(是1还是3)取决于你插在了哪个引脚上,务必和代码里
pyb.UART(3, 9600)的数字对应上。
- 检查1:波特率。双方
- 问题二:识别率不高,时好时坏。
- 药方1:增加样本数量和质量。回到3.1节,给每个人多拍些照片,在不同光线(白天、晚上开灯)下都拍一些。
- 药方2:调整阈值
THRESHOLD。这是最有效的微调手段。在稳定光照下,多测几次已录入人员和陌生人的差异度,找到一个合适的平衡点。 - 药方3:优化拍摄环境。避免背景过于杂乱,避免强光直射摄像头或人脸形成阴影。可以考虑在摄像头周围加一圈柔光LED补光灯,效果会提升非常明显。
- 问题三:舵机动作无力或发热严重。
- 药方:检查电源! 这几乎是我强调最多的一点。务必使用独立的、功率足够的5V电源给舵机供电。Arduino的USB口或板载5V引脚无法提供持续的大电流。
项目优化与扩展思路: 这个基础版本跑通后,你可以把它玩出更多花样:
- 增加反馈机制:给Arduino接上蜂鸣器和RGB LED。识别成功时绿灯亮、播放欢快音调;失败时红灯亮、播放警报音。
- 引入OLED显示屏:在门口加一个小屏幕,识别成功时显示“欢迎回家,XXX”,体验感瞬间提升。
- 网络功能:给Arduino加上ESP8266 Wi-Fi模块,当识别到陌生人时,可以给你的手机发送一条报警通知。
- 活体检测:这是进阶方向。基础LBP算法无法区分照片和真人。可以尝试让被识别者轻微转头或眨眼,通过多帧判断是否为活体,提升安全性。
整个项目从硬件采购、焊接连线,到代码调试、机械安装,我带着团队的小伙伴完整走了一遍,大概花了一个周末的时间。最难的部分其实不是代码,而是硬件连接稳定性和光线环境调试。当最后对着摄像头刷脸,听到“咔哒”一声门锁打开的时候,那种亲手创造智能设备的满足感,是无可替代的。希望这份详细的指南能帮你少走弯路,顺利打造出属于你自己的智能门禁。如果在制作过程中遇到任何问题,随时可以拿着串口打印的日志信息来分析,那里面藏着所有问题的答案。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)