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 UnoArduino 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 硬件连接实战图解

连接是关键,接错了轻则不工作,重则烧设备。大家跟着我的步骤来:

  1. OpenMV与Arduino通信:我们需要让OpenMV把识别结果告诉Arduino。这里使用串口通信(UART)。将OpenMV的TX引脚连接到Arduino的RX引脚(通常是D0),将OpenMV的RX引脚连接到Arduino的TX引脚(通常是D1)。注意:在连接时,务必确保OpenMV和Arduino的GND(地线)连接在一起,这是保证通信稳定的基础,很多奇怪的通信故障都是因为地线没共接。
  2. Arduino控制舵机:舵机有三根线:信号线(通常是橙色或白色)、电源正极(红色)、地线(棕色或黑色)。将舵机信号线连接到Arduino的一个PWM引脚(例如D9),正极和地线连接到为舵机准备的独立5V电源上。同样,这个独立电源的地线也必须和Arduino的GND连接在一起,确保所有设备共地。
  3. 供电:用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))是一个关键操作,它把摄像头视野固定在这个分辨率上。这个尺寸是很多人脸识别算法的标准输入尺寸,能保证我们抓取到的是正脸区域。
  • 参数numnnum是人员ID,第一个人就是1,照片会保存在s1文件夹。录入下一个人时,记得把num改成2,以此类推。n是每人采集的样本数。我强烈建议n至少设为20,最好30以上。样本越多,涵盖的表情、轻微角度变化就越多,后续识别的容错率就越高。只拍三五张的话,稍微侧个脸可能就认不出来了。
  • 拍摄流程:用红蓝LED指示状态是个好习惯。红灯亮是“准备”,给人时间摆好姿势;蓝灯亮瞬间“拍照”,确保抓拍的是静态清晰图像。拍完一张后,脚本会循环继续拍下一张,直到拍满n张。
  • 存储:确保你的OpenMV插上了SD卡,并且根目录下有一个singtown文件夹(可以手动创建)。脚本会自动在singtown下创建s1s2等子文件夹来存放不同人的照片。

踩坑提醒:拍摄环境的光线要均匀,避免一侧过亮或过暗。让人脸尽量充满窗口,但也不要太近导致超出范围。最好能采集一些带有轻微表情(微笑)和微小头部转动的照片,这样建立的人脸库更健壮。

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. 系统集成、调试与优化思路

硬件连好了,代码也写完了,接下来就是最激动人心的环节——把各部分组装起来,上电测试!

集成与上电步骤:

  1. 分别测试:先不要全部连在一起。用OpenMV的IDE运行人脸录入脚本,确保能正常拍照并保存到SD卡。然后运行识别脚本,在IDE的串口终端里观察打印的差异度数据,对着摄像头看看能否正确识别出已录入的人脸。接着,单独测试Arduino,上传控制代码,打开串口监视器,手动输入123然后回车,看舵机是否能正确转动到指定角度。
  2. 通信联调:将OpenMV的TX/RX与Arduino的RX/TX交叉连接,共地。打开Arduino的串口监视器,再运行OpenMV的识别脚本。当识别成功时,你应该能在Arduino的串口监视器里看到“收到指令: X”的提示,同时舵机动作。这是最关键的一步,确保指令流畅通无阻。
  3. 机械安装:将舵机通过支架固定在门或锁具上,连接好连杆。这里需要反复调试doorAngleOpendoorAngleClosed这两个角度值,确保它们对应门锁的实际开、闭位置。可能需要多次修改代码、上传测试。

常见问题与调试“药方”:

  • 问题一: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:增加样本数量和质量。回到3.1节,给每个人多拍些照片,在不同光线(白天、晚上开灯)下都拍一些。
    • 药方2:调整阈值THRESHOLD。这是最有效的微调手段。在稳定光照下,多测几次已录入人员和陌生人的差异度,找到一个合适的平衡点。
    • 药方3:优化拍摄环境。避免背景过于杂乱,避免强光直射摄像头或人脸形成阴影。可以考虑在摄像头周围加一圈柔光LED补光灯,效果会提升非常明显。
  • 问题三:舵机动作无力或发热严重。
    • 药方:检查电源! 这几乎是我强调最多的一点。务必使用独立的、功率足够的5V电源给舵机供电。Arduino的USB口或板载5V引脚无法提供持续的大电流。

项目优化与扩展思路: 这个基础版本跑通后,你可以把它玩出更多花样:

  1. 增加反馈机制:给Arduino接上蜂鸣器和RGB LED。识别成功时绿灯亮、播放欢快音调;失败时红灯亮、播放警报音。
  2. 引入OLED显示屏:在门口加一个小屏幕,识别成功时显示“欢迎回家,XXX”,体验感瞬间提升。
  3. 网络功能:给Arduino加上ESP8266 Wi-Fi模块,当识别到陌生人时,可以给你的手机发送一条报警通知。
  4. 活体检测:这是进阶方向。基础LBP算法无法区分照片和真人。可以尝试让被识别者轻微转头或眨眼,通过多帧判断是否为活体,提升安全性。

整个项目从硬件采购、焊接连线,到代码调试、机械安装,我带着团队的小伙伴完整走了一遍,大概花了一个周末的时间。最难的部分其实不是代码,而是硬件连接稳定性和光线环境调试。当最后对着摄像头刷脸,听到“咔哒”一声门锁打开的时候,那种亲手创造智能设备的满足感,是无可替代的。希望这份详细的指南能帮你少走弯路,顺利打造出属于你自己的智能门禁。如果在制作过程中遇到任何问题,随时可以拿着串口打印的日志信息来分析,那里面藏着所有问题的答案。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐