基于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) 连接,共地是通信的基础。
- ESP-EYE 的
行空板 与 继电器模块 连接:
- 行空板的某个GPIO口(例如
D13)接 继电器模块的IN(信号输入)引脚。 - 行空板的
3.3V(Pin1) 接 继电器模块的VCC。 - 行空板的
GND(Pin6) 接 继电器模块的GND。
- 行空板的某个GPIO口(例如
继电器与门锁连接(高压部分,务必谨慎):
- 继电器的
常开(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); // RX=16, TX=17, 但ESP-EYE的UART2 TX/RX是固定的GPIO1/3?这里需要核对 // 实际上,对于ESP-EYE,我们常直接用Serial1(UART1),其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引脚是GPIO1,RX是GPIO3,这与我们之前的接线一致。务必确认你的接线对应的是UART1的引脚。 - 图像格式与尺寸:
PIXFORMAT_JPEG和FRAMESIZE_SVGA是关键参数。JPEG格式能极大压缩数据量,一幅800x600的图片可能只有10-30KB,非常适合串口传输。如果选择RGB格式,数据量会大十倍不止,串口根本来不及传。jpeg_quality参数可以微调,值越小图片质量越高但体积越大,需要根据实际识别效果和传输速度权衡。我测试发现12-15之间在速度和识别率上比较均衡。 - 数据帧设计:直接发送二进制JPEG数据,接收方可能无法判断一帧图像的起止。因此我设计了一个简单的文本协议:
IMG_START,<length>,<jpeg_data>IMG_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, timeout=1) # 清空缓冲区 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转换为RGB(face_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-EYE,ESP-EYE收到后再拍摄下一张。 - JPEG质量参数:尝试调整
jpeg_quality,过低的品质(值过大)可能导致JPEG解码器无法识别。
- 帧同步问题:检查ESP-EYE发送的帧头帧尾协议。确保行空板在读取指定长度
- 症状:行空板收到数据但
人脸识别速度慢或准确率低:
- 症状:从拍照到出结果耗时超过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有意思多了。