基于ESP-EYE与行空板的本地化智能门禁系统开发实践
1. 项目缘起从“刷脸”到“刷板”的智能门禁新思路最近在捣鼓一些智能家居的边角料项目发现一个挺有意思的现象很多朋友对智能门禁的认知还停留在“买成品”或者“用树莓派摄像头”这种比较重的方案上。前者成本高、可玩性低后者虽然灵活但树莓派加上摄像头、电源、外壳一套下来体积和功耗都不算小对于只是想给工作室、小仓库或者家里的某个独立房间加个简单门禁的需求来说有点“杀鸡用牛刀”了。恰好手头有几块闲置的ESP-EYE开发板和一块行空板就琢磨着能不能用它们组合出一个轻量、低成本、又有足够“智能”的本地化门禁方案。ESP-EYE自带摄像头和麦克风集成度高行空板则自带屏幕和丰富的GPIO还有Python环境非常适合做本地处理和交互。这个组合的核心思路就是把图像识别、本地决策和人机交互拆分开让ESP-EYE专心“看”行空板专心“想”和“说”各司其职。最终实现的效果是人走到门前ESP-EYE拍照通过Wi-Fi把图像传给行空板行空板运行人脸识别模型判断是否授权然后控制继电器模拟开门并在屏幕上给出友好提示。整个过程无需连接公网数据完全本地处理安全又快速。这个方案特别适合创客、极客或者是对数据隐私比较在意的场景。它不依赖于任何云服务所有逻辑都在本地闭环从硬件成本到软件自由度都给了开发者很大的发挥空间。下面我就把这个从零搭建的过程包括硬件连接、软件配置、代码编写以及我踩过的几个坑详细拆解一遍。2. 硬件选型与连接为什么是ESP-EYE和行空板在开始焊接和写代码之前我们先得搞清楚为什么选这两块板子以及它们之间如何“对话”。这决定了整个系统的稳定性和扩展性。2.1 核心组件功能解析ESP-EYE这款开发板的核心是ESP32芯片集成了OV2640摄像头模组、数字麦克风、8MB PSRAM和4MB Flash。对于门禁项目我们主要看中三点第一强大的Wi-Fi和蓝牙连接能力这是它和行空板通信的桥梁第二内置摄像头免去了我们额外选购和调试摄像头模组的麻烦第三足够的计算能力虽然我们主要用它来采集图像并传输但其双核处理器也能分担一些简单的预处理任务。行空板这是一款面向Python教育和物联网开发的高集成度主板。它自带一块触摸屏有多个可编程按键和RGB灯更重要的是它提供了丰富的接口包括多个GPIO、I2C、UART等并且预装了完整的Python环境及许多常用库。在本项目中它扮演“大脑”的角色接收图像、运行识别算法、控制门锁继电器、在屏幕上显示结果。其他必要配件5V继电器模块用于控制门锁的电磁锁或电机锁。注意继电器只是提供通断信号实际驱动大电流锁具需要另外的电源务必做好电气隔离。杜邦线公对公、母对母用于连接。Micro USB数据线用于给ESP-EYE和行空板供电及编程。电源行空板可通过Type-C供电ESP-EYE可通过Micro USB或VIN引脚供电。为确保稳定建议分别供电。2.2 电路连接详解与安全注意事项连接的核心是建立ESP-EYE与行空板之间的通信以及行空板对继电器的控制。这里我选择使用串口UART作为两者间的通信协议。为什么不用Wi-Fi直连或者HTTP因为串口通信更稳定、延迟更低、无需复杂的网络配置特别适合这种固定设备间短距离、高频率的数据交换。接线步骤ESP-EYE 与 行空板 串口连接ESP-EYE 的TX(GPIO1) 接 行空板的RX(Pin10)。ESP-EYE 的RX(GPIO3) 接 行空板的TX(Pin8)。重要ESP-EYE 的GND必须与行空板的GND(例如 Pin6) 连接共地是通信的基础。行空板 与 继电器模块 连接行空板的某个GPIO口例如D13接 继电器模块的IN(信号输入)引脚。行空板的3.3V(Pin1) 接 继电器模块的VCC。行空板的GND(Pin6) 接 继电器模块的GND。继电器与门锁连接高压部分务必谨慎继电器的常开NO和公共端COM接口串联在门锁电源回路中。具体接法需根据你的锁具类型常闭型/常开型决定。操作前务必断开所有电源如果不熟悉强电建议请教专业人士。注意继电器模块的驱动电压需要确认。有些继电器模块是5V驱动有些是3.3V。行空板的GPIO输出是3.3V电平如果继电器是5V驱动可能需要额外的电平转换电路或选择3.3V驱动的继电器模块否则可能无法可靠吸合。硬件连接好后整体数据流就清晰了ESP-EYE拍照 - 通过串口发送图像数据 - 行空板接收并处理 - 识别成功则触发GPIO控制继电器 - 继电器动作开门。3. ESP-EYE端固件开发图像采集与串口传输ESP-EYE端的任务很纯粹初始化摄像头捕获一帧图像然后将图像数据通过串口发送出去。这里我们使用Arduino框架进行开发因为它对ESP32及摄像头的支持非常友好。3.1 开发环境搭建与核心库首先确保你的Arduino IDE已安装ESP32开发板支持。然后在“工具”菜单中选择开发板为“ESP32 Wrover Module”端口选择对应的串口。需要包含的关键库是ESP32-Camera。你可以通过Arduino的库管理器搜索安装。这个库封装了OV2640等摄像头模组的驱动让我们可以很方便地获取图像。3.2 关键代码实现与参数调优以下是ESP-EYE端代码的核心部分我加了详细注释#include “esp_camera.h” #include “HardwareSerial.h” // 摄像头引脚定义ESP-EYE有特定配置 #define PWDN_GPIO_NUM -1 #define RESET_GPIO_NUM -1 #define XCLK_GPIO_NUM 4 #define SIOD_GPIO_NUM 18 #define SIOC_GPIO_NUM 23 #define Y9_GPIO_NUM 36 #define Y8_GPIO_NUM 37 #define Y7_GPIO_NUM 38 #define Y6_GPIO_NUM 39 #define Y5_GPIO_NUM 35 #define Y4_GPIO_NUM 14 #define Y3_GPIO_NUM 13 #define Y2_GPIO_NUM 34 #define VSYNC_GPIO_NUM 5 #define HREF_GPIO_NUM 27 #define PCLK_GPIO_NUM 25 HardwareSerial SerialPort(2); // 使用UART2 void setup() { Serial.begin(115200); // 用于调试输出 SerialPort.begin(115200, SERIAL_8N1, 16, 17); // RX16, TX17 但ESP-EYE的UART2 TX/RX是固定的GPIO1/3这里需要核对 // 实际上对于ESP-EYE我们常直接用Serial1UART1其TX是GPIO1 RX是GPIO3。 // 更准确的初始化Serial1.begin(115200, SERIAL_8N1, 3, 1); // RX, TX // 但为了代码清晰我们使用预定义的Serial1 Serial1.begin(115200); // 默认使用GPIO1(TX), GPIO3(RX) camera_config_t config; config.ledc_channel LEDC_CHANNEL_0; config.ledc_timer LEDC_TIMER_0; config.pin_d0 Y2_GPIO_NUM; config.pin_d1 Y3_GPIO_NUM; config.pin_d2 Y4_GPIO_NUM; config.pin_d3 Y5_GPIO_NUM; config.pin_d4 Y6_GPIO_NUM; config.pin_d5 Y7_GPIO_NUM; config.pin_d6 Y8_GPIO_NUM; config.pin_d7 Y9_GPIO_NUM; config.pin_xclk XCLK_GPIO_NUM; config.pin_pclk PCLK_GPIO_NUM; config.pin_vsync VSYNC_GPIO_NUM; config.pin_href HREF_GPIO_NUM; config.pin_sscb_sda SIOD_GPIO_NUM; config.pin_sscb_scl SIOC_GPIO_NUM; config.pin_pwdn PWDN_GPIO_NUM; config.pin_reset RESET_GPIO_NUM; config.xclk_freq_hz 20000000; config.pixel_format PIXFORMAT_JPEG; // 输出JPEG格式节省带宽 // 图像质量选择根据串口速度和行空板处理能力调整 config.frame_size FRAMESIZE_SVGA; // 800x600 比VGA大比UXGA小 config.jpeg_quality 12; // 质量1-63值越小质量越高体积越大。12是个不错的平衡点。 config.fb_count 1; // 初始化摄像头 esp_err_t err esp_camera_init(config); if (err ! ESP_OK) { Serial.printf(“摄像头初始化失败错误代码: 0x%x”, err); return; } } void loop() { // 1. 获取一帧图像 camera_fb_t * fb esp_camera_fb_get(); if (!fb) { Serial.println(“图像捕获失败”); delay(1000); return; } // 2. 通过串口发送图像数据 // 先发送一个简单的帧头便于接收方解析例如“IMG_START”和图像长度 Serial1.print(“IMG_START,”); Serial1.print(fb-len); // 发送图像数据长度 Serial1.print(“,”); // 发送图像数据本身。注意直接发送二进制数据Serial1.write是更合适的选择。 // 但为了简单演示我们可以分段发送避免缓冲区问题。 Serial1.write(fb-buf, fb-len); // 这才是正确发送二进制数据的方式 Serial1.print(“IMG_END”); // 发送帧尾 // 3. 释放图像缓冲区 esp_camera_fb_return(fb); // 4. 控制采集频率比如每2秒拍一次避免数据洪流 delay(2000); }关键点与避坑指南串口选择与引脚代码中我用了Serial1.begin(115200)这是ESP32预定义的UART1其默认TX引脚是GPIO1RX是GPIO3这与我们之前的接线一致。务必确认你的接线对应的是UART1的引脚。图像格式与尺寸PIXFORMAT_JPEG和FRAMESIZE_SVGA是关键参数。JPEG格式能极大压缩数据量一幅800x600的图片可能只有10-30KB非常适合串口传输。如果选择RGB格式数据量会大十倍不止串口根本来不及传。jpeg_quality参数可以微调值越小图片质量越高但体积越大需要根据实际识别效果和传输速度权衡。我测试发现12-15之间在速度和识别率上比较均衡。数据帧设计直接发送二进制JPEG数据接收方可能无法判断一帧图像的起止。因此我设计了一个简单的文本协议IMG_START,length,jpeg_dataIMG_END。发送长度信息可以让接收方行空板预先分配好缓冲区提高可靠性。传输稳定性115200的波特率传输30KB数据大约需要2秒。在loop中加了delay(2000)是为了防止发送过快导致接收方缓冲区溢出。在实际应用中更好的做法是等待接收方返回一个“已收到”的应答信号后再拍摄下一张实现简单的流量控制。4. 行空板端Python程序人脸识别与门禁控制行空板运行着我们的核心逻辑。这里我们使用Python主要用到serial库进行串口通信OpenCV和face_recognition库进行人脸识别以及pinpong库行空板自带的库类似GPIO Zero来控制GPIO。4.1 环境准备与依赖安装行空板默认的Python环境可能没有我们需要的库。需要通过SSH或者直接在行空板的终端里安装。# 更新pip pip3 install –upgrade pip # 安装串口库 pip3 install pyserial # 安装OpenCV。行空板是ARM架构需要找兼容的版本有时直接安装opencv-python可能失败。 # 可以尝试安装精简版或使用系统包管理器 # pip3 install opencv-python-headless # 尝试这个无GUI的版本 # 或者通过apt如果行空板基于Debian/Ubuntu sudo apt update sudo apt install python3-opencv # 安装face_recognition库这是关键 pip3 install face_recognition # 注意face_recognition依赖dlib而dlib的编译安装可能很耗时且容易出错。 # 对于行空板ARM最好寻找预编译的wheel文件或者使用更轻量级的人脸识别方案如OpenCV自带的LBPH或深度学习模型如MobileFaceNet。 # 这里为了简化假设face_recognition可以安装成功。如果失败备选方案见下文。关于人脸识别库的选型坑face_recognition库在桌面电脑上很好用但在行空板这类ARM开发板上安装其底层依赖dlib极其困难经常编译失败。这是本项目第一个大坑。我的解决方案是转向更轻量的方案使用OpenCV的DNN模块加载一个轻量级的人脸识别模型例如OpenCV自带的FaceDetectorYN人脸检测和FaceRecognizerSF人脸识别。或者使用insightface项目的Python版本它提供了更现代且高效的模型。这里为了流程完整我先按face_recognition的思路写然后会给出备选方案。4.2 主程序逻辑拆解假设我们已经有了一个授权人脸数据库已知人脸的编码列表主程序的流程如下import serial import cv2 import numpy as np import face_recognition import time from pinpong.board import Board, Pin # 初始化行空板GPIO Board(“UNIHIKER”).begin() # 初始化行空板 relay_pin Pin(Pin.D13, Pin.OUT) # 继电器连接在D13 relay_pin.write_digital(0) # 初始化为低电平继电器断开 # 初始化串口端口号根据行空板实际识别到的串口设备来定通常是 /dev/ttyAMA0 或 /dev/ttyS0 # 需要先通过 ls /dev/tty* 命令确认ESP-EYE连接后出现的设备名 ser serial.Serial(‘/dev/ttyAMA0’, 115200, timeout1) # 清空缓冲区 ser.flushInput() # 加载已知人脸编码这里需要预先准备 # known_face_encodings [] # 这是一个列表存放已知人脸的128维编码 # known_face_names [] # 对应的名字列表 # 示例通过加载图片文件生成 # known_image face_recognition.load_image_file(“authorized_person.jpg”) # known_encoding face_recognition.face_encodings(known_image)[0] # known_face_encodings.append(known_encoding) # known_face_names.append(“Authorized User”) def receive_image(): “”“从串口接收一帧完整的JPEG图像数据”“” # 等待帧头 while True: if ser.in_waiting 0: line ser.readline().decode(‘utf-8’, errors‘ignore’).strip() if line.startswith(‘IMG_START’): try: # 解析长度格式如IMG_START,12345, parts line.split(‘,’) if len(parts) 2: img_length int(parts[1]) break except ValueError: continue # 根据长度读取图像数据 img_data bytearray() bytes_to_read img_length while bytes_to_read 0: chunk ser.read(min(bytes_to_read, 1024)) # 每次最多读1KB if not chunk: break # 超时或中断 img_data.extend(chunk) bytes_to_read - len(chunk) # 读取并丢弃帧尾”IMG_END” # 这里可以简单读一行或者根据协议处理 # ser.readline() return img_data return None def process_image(img_data): “”“处理接收到的图像进行人脸识别”“” if not img_data: return False # 将字节数据转换为numpy数组并解码为OpenCV图像 nparr np.frombuffer(img_data, np.uint8) img cv2.imdecode(nparr, cv2.IMREAD_COLOR) if img is None: print(“图像解码失败”) return False # 将BGR转换为RGBface_recognition库需要RGB格式 rgb_img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 人脸检测 face_locations face_recognition.face_locations(rgb_img) if not face_locations: print(“未检测到人脸”) return False # 人脸编码 face_encodings face_recognition.face_encodings(rgb_img, face_locations) for face_encoding in face_encodings: # 与已知人脸比对 matches face_recognition.compare_faces(known_face_encodings, face_encoding) name “Unknown” # 计算距离找到最匹配的 face_distances face_recognition.face_distance(known_face_encodings, face_encoding) if len(face_distances) 0: best_match_index np.argmin(face_distances) if matches[best_match_index]: name known_face_names[best_match_index] print(f”识别到: {name}“) if name ! “Unknown”: return True # 识别成功 return False # 识别失败或未授权 # 主循环 print(“门禁系统启动…”) while True: try: img_data receive_image() if img_data: print(“收到一帧图像开始识别…”) authorized process_image(img_data) if authorized: print(“授权通过开门”) relay_pin.write_digital(1) # 继电器吸合模拟开门 time.sleep(3) # 保持开门状态3秒 relay_pin.write_digital(0) # 断开继电器 else: print(“未授权拒绝访问。”) # 可以在行空板屏幕上显示提示信息 except KeyboardInterrupt: print(“程序退出”) break except Exception as e: print(f”处理过程中发生错误: {e}“) ser.close()4.3 备选轻量级人脸识别方案由于face_recognition安装困难我们可以采用OpenCV DNN 深度学习模型的方案。这里以OpenCV的FaceRecognizerSF为例需要下载模型文件# 备选方案使用OpenCV FaceRecognizerSF model_path “face_recognition_sface_2021dec.onnx” # 需要下载的模型文件 detector cv2.FaceDetectorYN.create( “face_detection_yunet_2022mar.onnx”, # 人脸检测模型 “”, (320, 320) # 输入尺寸根据摄像头分辨率调整 ) recognizer cv2.FaceRecognizerSF.create(model_path, “”) def process_image_opencv_dnn(img_data): nparr np.frombuffer(img_data, np.uint8) img cv2.imdecode(nparr, cv2.IMREAD_COLOR) if img is None: return False # 设置检测器输入尺寸 height, width, _ img.shape detector.setInputSize((width, height)) # 检测人脸 _, faces detector.detect(img) if faces is None: return False for face in faces: # 获取人脸对齐后的特征 aligned_face recognizer.alignCrop(img, face) # 提取特征向量 face_feature recognizer.feature(aligned_face) # 这里需要与预先提取的已知人脸特征进行比对计算余弦距离或欧氏距离 # known_features [] # 预加载的已知特征列表 # for known_feat in known_features: # similarity recognizer.match(face_feature, known_feat, cv2.FaceRecognizerSF_FR_COSINE) # if similarity 0.6: # 相似度阈值需调整 # return True # 简化假设有一个已知特征 # if known_feature is not None: # similarity recognizer.match(face_feature, known_feature, cv2.FaceRecognizerSF_FR_COSINE) # print(f”相似度: {similarity}“) # if similarity 0.5: # return True return False这个方案需要预先下载.onnx模型文件但一旦配置好运行效率和对ARM架构的兼容性通常比face_recognition更好。5. 系统集成、调试与性能优化当两端代码分别写好并烧录/运行后真正的挑战才开始让它们稳定地协同工作。这个阶段会暴露很多设计时没考虑到的问题。5.1 联合调试与常见问题排查串口通信失败症状行空板收不到任何数据或者收到乱码。排查确认端口在行空板上执行ls /dev/tty*插拔ESP-EYE的USB线观察新增的设备是哪个如/dev/ttyUSB0。在Python代码中修改serial.Serial的参数。确认波特率两端代码的波特率必须严格一致如115200。检查接线确认TX-RX交叉连接GND共地。用万用表测一下通断。电平兼容ESP-EYE是3.3V电平行空板的UART也是3.3V电平匹配没问题。图像数据不完整或无法解码症状行空板收到数据但cv2.imdecode返回None。排查帧同步问题检查ESP-EYE发送的帧头帧尾协议。确保行空板在读取指定长度img_length后缓冲区里正好是完整的JPEG数据。可以在接收代码中加入调试信息打印接收到的数据长度和头尾几个字节与原始JPEG文件对比。串口缓冲区溢出ESP-EYE发送太快。确保ESP-EYE在发送一帧后有足够的延迟delay或等待行空板的应答。可以在协议中加入应答机制行空板收到完整图像并处理完后通过串口发送一个”ACK”给ESP-EYEESP-EYE收到后再拍摄下一张。JPEG质量参数尝试调整jpeg_quality过低的品质值过大可能导致JPEG解码器无法识别。人脸识别速度慢或准确率低症状从拍照到出结果耗时超过3秒或者经常误识别。优化降低图像分辨率将FRAMESIZE_SVGA(800x600) 降为FRAMESIZE_VGA(640x480) 甚至FRAMESIZE_QVGA(320x240)。人脸识别算法在较低分辨率下也能工作但速度会快很多。优化识别区域如果摄像头固定可以只对图像中门所在区域进行人脸检测减少处理面积。调整识别阈值在face_recognition.compare_faces或recognizer.match中有一个容忍度tolerance参数或相似度阈值。默认值可能不适合你的环境光照和角度。需要收集一些正样本授权人和负样本非授权人图片反复调整这个阈值在误拒率FRR和误识率FAR之间取得平衡。多帧验证不要单帧定生死。可以连续识别3帧如果其中2帧识别为同一授权人才触发开门。这能有效防止偶然误识别。5.2 功能扩展与可靠性增强基础功能跑通后可以考虑以下增强点让系统更实用本地人脸数据库管理在行空板上开发一个简单的界面利用其触摸屏用于注册新人脸。流程可以是输入姓名 - 引导用户面对摄像头拍几张照 - 提取特征并保存到文件。这样就不需要预先在代码里写死特征值了。状态显示与交互利用行空板的屏幕和RGB灯提供丰富的反馈。例如等待识别时显示“请正对摄像头”和蓝色呼吸灯识别成功显示绿色对勾和欢迎语识别失败显示红色叉号和“识别失败”系统错误显示黄色感叹号。访客临时密码/二维码除了人脸可以增加备用开门方式。例如在行空板屏幕上生成一个每分钟变化一次的临时密码基于TOTP算法用户通过手机APP查看密码并输入。或者生成一个一次性有效的二维码用户用手机扫码开门。日志记录将所有开门事件时间、识别结果、是/否开门记录到行空板的CSV文件或SQLite数据库中便于后续查看。网络备份与远程管理可选如果环境有局域网可以让行空板作为一个简单的HTTP服务器在同一个网络下的电脑或手机浏览器上可以查看日志、管理人脸库。注意此功能涉及网络务必做好权限验证且不建议将服务暴露到公网。6. 供电、部署与长期运行考量一个原型在桌面上能跑和它能在门上稳定运行365天是两回事。最后这部分聊聊从“项目”到“产品”的最后一公里。供电方案 ESP-EYE和行空板都需要5V供电。最稳妥的方案是使用两个独立的5V/2A以上的电源适配器。如果追求简洁可以找一个输出功率足够大建议5V/4A以上的电源然后通过DC-DC降压模块或HUB分出两路。切忌使用劣质充电宝或功率不足的电源电压不稳会导致设备重启、摄像头初始化失败等各种诡异问题。设备固定与走线 将ESP-EYE和行空板固定在门内侧合适位置。ESP-EYE的摄像头需要透过门上的一个小孔或使用门镜改造看到门外。所有线缆要用扎带或线槽规整好防止被门开关夹到或拉扯。程序自启动与看门狗 我们需要行空板上的Python程序在开机后自动运行并且在程序崩溃后能自动重启。自启动可以将主程序脚本添加到行空板的/etc/rc.local文件在exit 0之前或者创建一个systemd服务单元。看门狗可以在Python程序内部增加一个“心跳”机制另一个守护进程监控这个心跳如果超时就重启主程序。更简单粗暴的方法是使用Linux系统的cron定时任务每隔一段时间检查一次进程是否存在不存在则启动。环境适应性光照OV2640摄像头在光线不足时效果很差。可以考虑增加一个红外补光灯与ESP-EYE同步供电并启用摄像头的夜视模式如果支持。温度设备在密闭空间可能发热。确保安装位置有一定通风。这个项目最大的乐趣在于它不是一个黑盒产品每一个环节你都可以控制、修改和优化。从最基础的“刷脸开门”到增加多重验证、访客管理、远程日志甚至集成语音提示利用ESP-EYE的麦克风有无限的扩展可能。我最初版本只是简单识别后来增加了屏幕上的动态提示和识别成功后的“欢迎回家”语音播放通过行空板的音频口体验感立刻提升了一个档次。动手的过程中你会对嵌入式系统、计算机视觉、网络通信有更 concrete 的理解这比单纯调用一个云API有意思多了。

相关新闻