ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

基于树莓派的人脸识别门禁系统设计与实现

基于树莓派的人脸识别门禁系统设计与实现 做这个项目之前我一直在纠结一个问题为什么智能化喊了这么多年公司门口、小区楼道用的还是那张一刷就过的IC卡卡丢了要补办卡被复制了毫无感知访客来了还得保安亲自下来接。后来我把目光放到人脸识别门禁上干脆自己从零撸了一套完整的智能门禁系统。项目做到一半我才发现这东西比想象中有意思也比想象中坑多。这个项目表面上看是一个“摄像头单片机开锁”的小玩意真正落地时你会碰到人脸检测、特征提取、相似度比对、数据库存储、GPIO控制、硬件选型、光线干扰、活体检测等一系列问题。我尽量把整个设计和实操过程都摊开来讲适合刚入门嵌入式AI的开发者、想做毕业设计或课程项目的学生也适合想在公司内部搞一套低成本门禁方案的运维同学参考。1. 项目设计与整体思路1.1 为什么选择离线本地识别方案人脸识别门禁市面上成品很多基本分两类一类是云端识别摄像头把照片传到云端识别完返回结果另一类是本地离线识别所有算法都在门口那台设备上跑。我选择离线方案原因很直接。公司或小区场景下人员进出都是隐私数据人脸照片属于生物特征信息往第三方云服务器传本身就存在合规风险。再一个是延迟问题云端识别无论如何都要经过网络往返高峰期排队反而比刷卡还慢。离线识别的延迟基本可以控制在几百毫秒以内断网也不影响使用这对门禁这种可靠性要求高的场景太关键了。另外成本上也有优势离线方案不需要买云服务不需要按调用次数付费一台终端设备就能搞定所有事情。对于几十人到几百人的中小型团队这笔账算下来非常划算。1.2 整体识别流程与核心模块划分整套系统的数据流可以拆成下面这几个环节摄像头采集画面 → 人脸检测 → 人脸对齐与预处理 → 特征提取 → 特征比对 → 判定结果 → 控制门锁动作每个环节都有专门的算法或硬件负责。摄像头负责“看”人脸检测负责判断画面里有没有人脸以及人脸在哪里预处理把人脸摆正、统一尺寸、调整光照影响特征提取把人脸变成一串数字向量比对环节计算当前特征和数据库里注册特征的相似度最后根据相似度阈值决定是否开门。一开始我打算用现成的云API快速验证后来发现云端API的接口文档翻来覆去就那几个参数对学习帮助不大。于是换成了本地部署模型的方式用OpenCV做人脸检测用FaceNet系列的轻量模型做特征提取数据库用SQLite存特征向量。整个流程吃透了之后再去看那些商业门禁机的宣传语基本能猜出它们内部的大致结构。1.3 技术栈选型的背后逻辑选技术栈的时候我给自己定了三个约束只能在边缘设备上跑、必须实时、要能离线运行。基于这三点我排除了很多看着很炫但跑不动的方案。大尺寸的ResNet系列识别模型在树莓派上推理一次要好几秒根本没法用。最终确定的技术栈是OpenCV做图像处理和人脸检测特征提取用的是基于FaceNet思想训练的轻量化模型跑在CPU上单次推理大约100毫秒左右。数据库用SQLite因为它不需要单独起服务文件型数据库在嵌入式设备上非常友好。这套组合的优点是依赖少、部署简单、性能足够。缺点是如果门禁点位数特别多比如一个园区几十个门每台设备单独存一份特征库管理起来会麻烦。不过对于单机门禁场景这套方案是最务实的选择。2. 硬件选型与电路设计2.1 主控方案的取舍主控是整个系统的“大脑”决定了你的数据处理能力和开发效率。我前后对比了几种方案主控方案优点缺点适用场景树莓派4B生态成熟、资料多、性能够用价格浮动大、功耗略高中小型项目、学习验证Jetson NanoGPU算力强、可以跑大模型开发门槛高、散热麻烦需要极强算力的复杂场景RK3588等国产板性能功耗比好、接口丰富资料相对少、上手成本高产品化落地香橙派/香蕉派价格低、可玩性高部分型号兼容性一般低成本方案验证最终我选了树莓派4B的4GB版本。原因很简单遇到问题能搜到答案。树莓派的社区太庞大了从系统烧录到GPIO控制几乎每个坑都有人踩过。用OpenCV跑轻量化识别模型CPU占用大约在40%左右还有余量跑Web服务或者日志记录。2.2 摄像头与识别模块如何选择摄像头这块很多人会踩坑。树莓派官方CSI摄像头在树莓派上的兼容性确实好但如果你是业余玩家或者想快速验证方案我更推荐免驱USB摄像头插上就能用。CSI接口一旦系统配置有变动可能就要重新折腾驱动。项目里我用的是一颗普通1080P免驱USB摄像头30帧输出价格几十块钱。识别的时候实际只需要取VGA分辨率640x480数据量小而且检测速度更快清晰度完全够用。网上流传的TX510人脸识别模块我也试过。它属于串口级别的成品模块你通过串口协议给它发指令它自己完成检测、特征提取和比对然后把结果返回给主控。这种模块的优势是开发工作量极小不需要懂任何算法串口一接就能用。缺点是灵活度太低模块内置的算法你改不了光线适应性、识别距离、阈值调节都只能通过有限的参数控制出了问题排查起来也很被动。如果目的是快速做出一个demoTX510这类模块是合适的如果你想真正理解人脸识别门禁的原理或者需要二次开发一定选USB摄像头配合自研算法路线。做项目最重要的不是“看起来能用”而是“问题来了能下手修”。2.3 门锁控制与电气安全门禁系统最终动作是开门这就离不开继电器和电磁锁。我选用的是12V供电的电磁锁也叫电插锁通过一个5V触发的继电器模块控制。树莓派的GPIO引脚输出3.3V电压驱动能力很弱不能直接带动继电器线圈所以中间需要加一个三极管或光耦做隔离驱动。实际接线逻辑是树莓派GPIO输出高电平 → 三极管导通 → 继电器线圈通电 → 常开触点闭合 → 电磁锁通电开锁。这里必须强调一个安全问题。电磁锁是12V供电树莓派和摄像头是5V供电如果共用电源不规范很容易造成电压不稳严重的会烧主板。我的做法是使用一个12V 5A的开关电源给电磁锁供电再用一个DC-DC降压模块把12V转成5V给树莓派供电。另外在继电器输出端并联一个反向续流二极管防止电磁锁断开瞬间产生的反向电动势把继电器和GPIO击穿。注意涉及市电和锁具供电的操作务必断开电源后再接线接线前用万用表确认电压极性。如果完全没有电气经验建议先用电工实验板做小电压验证。3. 开发环境与基础配置3.1 系统与基础依赖系统我刷的是树莓派官方Raspberry Pi OS 64位版本。之前用兼容镜像遇到过不少莫名奇妙的坑后来发现官方系统稳定性最好软件源里的软件包也最全。系统装好后第一步是更新软件源和系统包sudo apt update sudo apt upgrade -y sudo apt install python3-pip python3-dev python3-venv git cmake build-essential我习惯把项目依赖装在独立的Python虚拟环境里避免和系统Python环境互相污染python3 -m venv ~/face-door/env source ~/face-door/env/bin/activate3.2 安装OpenCV的经典痛点OpenCV的安装是个老生常谈的问题每次做视觉项目都能碰到。最简单的安装方式是直接用pippip install opencv-python opencv-contrib-python在树莓派上直接用pip安装的OpenCV是预编译版本能正常跑但有几个细节要注意。第一opencv-python和opencv-contrib-python不要混装否则会出现符号冲突报一堆莫名其妙的“undefined symbol”错误。第二预编译版本不一定包含GStreamer支持如果你需要读取RTSP视频流可能会发现cv2.VideoCapture无法打开网络摄像头。我的处理方式是先用pip装预编译版把流程跑通后期确认项目稳定后再考虑源码编译。源码编译OpenCV在树莓派上非常耗时我编译过一次用了将近两个小时性能提升并不明显除非你需要特定的扩展模块否则不建议新手一上来就编译源码。3.3 核心依赖清单项目主要依赖如下Python 3.9OpenCV 4.xNumPy图像矩阵运算dlib可选用于人脸关键点检测配合活体检测sqlite3Python内置无需额外安装RPi.GPIO树莓派GPIO控制Flask可选用于搭建本地管理后台dlib的安装稍微麻烦一点它需要cmake编译安装时务必先确保系统里有build-essential和cmake。遇到编译进度卡住的情况通常是因为内存不够建议增加swap空间或者用pip的预编译wheel包。4. 人脸识别核心算法实现4.1 人脸检测Haar还是DNN人脸检测是整个识别流程的入口。OpenCV提供了多种检测方案最经典的是Haar Cascade级联分类器当时因为它轻量、速度快被广泛应用。你只需要加载一个xml文件几行代码就能实现人脸检测。但我在实际测试中发现Haar对侧脸、暗光、遮挡的漏检率偏高稍微歪一下头就检测不到。而OpenCV的DNN人脸检测器在鲁棒性上明显更好它是基于SSD框架训练的残差网络模型对角度和光线变化的容忍度更高在树莓派CPU上检测一帧大概耗时80毫秒完全在可接受范围内。两种方案的对比如下检测方式模型体积检测速度鲁棒性适用场景Haar Cascade约1MB极快一般正脸、光线均匀场景OpenCV DNN (SSD)约10MB较快较强角度变化、复杂光线4.2 加载DNN检测器的核心代码用OpenCV加载DNN人脸检测器需要两个文件模型结构文件和训练好的权重文件可以从OpenCV官方GitHub仓库下载。import cv2 # 加载DNN人脸检测模型 net cv2.dnn.readNetFromCaffe( deploy.prototxt, res10_300x300_ssd_iter_140000.caffemodel ) def detect_faces(frame): h, w frame.shape[:2] # 缩放至300x300输入网络 blob cv2.dnn.blobFromImage( cv2.resize(frame, (300, 300)), 1.0, (300, 300), (104.0, 177.0, 123.0) ) net.setInput(blob) detections net.forward() faces [] for i in range(detections.shape[2]): confidence detections[0, 0, i, 2] if confidence 0.7: continue box detections[0, 0, i, 3:7] * np.array([w, h, w, h]) x1, y1, x2, y2 box.astype(int) # 保证边界不越界 x1, y1 max(0, x1), max(0, y1) x2, y2 min(w, x2), min(h, y2) faces.append((x1, y1, x2, y2, confidence)) return facesconfidence阈值设为0.7是我反复测试后的经验值。设太高会漏检比如0.85以上在光线不好时经常检测不到人脸设太低会出现大量误检框比如把墙上的装饰画当成人脸后面特征提取就会浪费算力。4.3 特征提取把人脸变成一组数字人脸识别的本质不是“认脸”而是“比对特征”。我们需要把一张人脸图像变成一组有区分度的数字向量这个过程就是特征提取。项目里我采用的是基于FaceNet思想训练的模型。FaceNet的核心思路是用卷积神经网络把一张人脸图像映射到一个128维的欧氏空间中同一个人的不同照片在空间中的距离很近不同人的照片距离很远。这样识别问题就转化成了向量距离计算问题。模型加载和特征提取的代码大致如下from facenet_pytorch import InceptionResnetV1 import torch from PIL import Image import numpy as np # 加载预训练模型 model InceptionResnetV1(pretrainedvggface2).eval() def get_embedding(face_img): # face_img是一张已经裁剪好并对齐的人脸BGR图像 rgb_img cv2.cvtColor(face_img, cv2.COLOR_BGR2RGB) pil_img Image.fromarray(rgb_img).resize((160, 160)) # 转换为tensor并归一化 face_tensor torch.tensor(np.array(pil_img)).permute(2, 0, 1).float() face_tensor (face_tensor - 127.5) / 128.0 face_tensor face_tensor.unsqueeze(0) with torch.no_grad(): embedding model(face_tensor).numpy().flatten() # 归一化便于后续计算余弦相似度 embedding embedding / np.linalg.norm(embedding) return embedding如果你不太想引入PyTorch这么大的依赖也可以使用face_recognition库它底层封装了dlib的模型调用一行代码就能拿到128维特征向量开发效率非常高。但face_recognition在树莓派上的推理速度稍慢而且它内部封装的模型相对固定后续想换更好的模型就不太灵活。4.4 相似度计算与阈值设定拿到特征向量后比对环节用余弦相似度衡量两段特征向量的距离。余弦相似度的计算公式是cosine_similarity (A · B) / (||A|| × ||B||)其中A和B是两个人脸的特征向量点积除以模长乘积。结果范围在-1到1之间数值越大表示越相似。我在上一节已经把特征向量归一化了所以模长等于1余弦相似度实际上就等于点积def cosine_similarity(embedding1, embedding2): return float(np.dot(embedding1, embedding2))关于阈值我测试了多组数据阈值误识率拒识率结论0.75极低很高熟人经常打不开门不实用0.65低中等光线不好时识别率下降明显0.55中等较低整体体验好安全性尚可0.45较高极低容易误开门不适合门禁最终我采用的阈值是0.55。在办公室环境下这个阈值能让大多数人正常通行同时对不是同一个人但长得比较像的情况有一定的拦截率。当然门禁的安全级别和使用场景直接相关如果用在机房等高安全区域阈值应调高到0.65以上。4.5 完整识别主流程代码把检测、特征提取、比对串起来就形成了门禁系统的核心识别逻辑import cv2 import sqlite3 import numpy as np def recognize(frame, known_embeddings, threshold0.55): faces detect_faces(frame) if len(faces) 0: return None, no_face # 取最大人脸进行识别避免多人场景下误选 faces.sort(keylambda f: (f[2]-f[0])*(f[3]-f[1]), reverseTrue) x1, y1, x2, y2, conf faces[0] face_img frame[y1:y2, x1:x2] embedding get_embedding(face_img) best_id None best_score -1 for user_id, known_emb in known_embeddings.items(): score cosine_similarity(embedding, known_emb) if score best_score: best_score score best_id user_id if best_score threshold: return best_id, best_score return None, best_score实际测试中单人识别全流程大约耗时300毫秒也就是一秒钟能处理3帧左右对于门禁场景已经绰绰有余。需要优化时可以考虑跳帧检测比如每3帧只做一次完整识别能在不影响体验的前提下显著降低CPU占用。5. 数据库设计与门禁控制逻辑5.1 SQLite表结构与特征存储方式人脸特征向量是一个128维的浮点数组直接存成Python对象会占用大量空间而且每次读取都要重新解析。SQLite里可以用BLOB类型保存二进制数据存取效率都不错。建表语句如下CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, department TEXT, face_embedding BLOB NOT NULL, photo_path TEXT, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS recognition_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, name TEXT, score REAL, result TEXT, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP );特征向量写入数据库时用NumPy的tobytes()把数组转成二进制读取时再通过np.frombuffer()恢复import numpy as np import sqlite3 def save_embedding(user_id, embedding): binary embedding.astype(np.float32).tobytes() cursor.execute( UPDATE users SET face_embedding? WHERE id?, (binary, user_id) ) conn.commit() def load_embeddings(): cursor.execute(SELECT id, name, face_embedding FROM users) rows cursor.fetchall() known {} for user_id, name, binary in rows: emb np.frombuffer(binary, dtypenp.float32) known[user_id] emb return known这里有一个容易踩的细节数组默认是float64转成二进制后体积翻倍读取时类型对不上还会报错。我统一用float32存储这样128维向量只占512字节几百人的特征库才几百KBSQLite文件依然很小。5.2 注册新用户的人脸录入流程正常使用门禁前需要先把人员的照片录入系统生成特征向量。我设计了两种录入方式一种是直接在采集页面用摄像头拍一张照片另一种是上传照片文件。拍照录入的核心过程是实时显示摄像头画面 → 检测到人脸时自动抓拍 → 裁剪人脸区域 → 提取特征 → 保存到数据库 → 提示录入成功。实操中发现录入照片的质量直接决定识别效果。照片必须是正脸、光线均匀、没有遮挡。我录完第一批用户后发现识别率偏低排查了很久才发现原来有些人录入时是笑着的有些是逆光的还有人戴了帽子特征向量差异很大导致后面识别时频繁失败。后来我在录入界面加了一道审查逻辑拍照时如果检测不到人脸或者人脸置信度低于0.75直接拒绝保存并提示重新拍摄。这看似简单的限制把识别准确率提升了将近一倍。5.3 活体检测防止一张照片就开门如果你只做到上面那一步那这套门禁系统在安全上是不过关的。拿一张打印的A4纸照片放在摄像头前很可能就被直接放行了。这是所有基于2D人脸识别的门禁系统都要面对的问题也就是防照片攻击和防视频攻击。基础方案是活体检测。常见手段包括眨眼检测、张嘴检测、头部姿态变化检测、红外深度图检测、屏幕反光检测等。我在项目上首先实现了基于dlib的人脸关键点检测通过计算眼睛纵横比EAREye Aspect Ratio来判断是否有眨眼动作def eye_aspect_ratio(eye_points): # 计算上下眼睑距离 A np.linalg.norm(eye_points[1] - eye_points[5]) B np.linalg.norm(eye_points[2] - eye_points[4]) # 计算水平眼距 C np.linalg.norm(eye_points[0] - eye_points[3]) return (A B) / (2.0 * C)正常情况下人眼EAR值在0.25左右闭眼时降到0.1以下。识别时连续检测几帧如果EAR值有明显从高到低的波动说明画面里有眨眼动作。照片是静止的EAR值恒定不变直接判为攻击。当然这种2D活体检测对视频攻击比如用手机录制一段眨眼视频是拦不住的。更进一步可以结合红外模块或者结构光深度传感器但那种方案成本和复杂度都会大幅上升。对普通办公环境眨眼检测已经能把安全等级提升一个档次。5.4 GPIO控制电子锁的执行代码识别成功后的动作是开锁。树莓派的GPIO控制继电器继电器驱动电磁锁。开锁逻辑需要加一个延时自动落锁防止门打开了忘记关。import RPi.GPIO as GPIO import time LOCK_PIN 18 UNLOCK_SECONDS 5 GPIO.setmode(GPIO.BCM) GPIO.setup(LOCK_PIN, GPIO.OUT) GPIO.output(LOCK_PIN, GPIO.LOW) def unlock_door(): # 给继电器一个高电平触发开锁 GPIO.output(LOCK_PIN, GPIO.HIGH) time.sleep(UNLOCK_SECONDS) GPIO.output(LOCK_PIN, GPIO.LOW) def close_door(): GPIO.output(LOCK_PIN, GPIO.LOW)有一个细节需要注意程序退出时一定要调用GPIO.cleanup()否则GPIO引脚会保持最后的状态下次程序启动时可能因为引脚状态不对导致继电器误动作。我在部署时还加了一个“看门狗”脚本每隔几秒检查识别主进程是否还在一旦发现进程死掉就自动重启保证门禁系统不至于默默罢工。6. 常见问题与排查技巧实录6.1 摄像头打开失败或画面卡死树莓派上使用USB摄像头最常碰到的问题是设备节点被占用。多个程序同时打开/dev/video0会导致后打开的程序报错。排查步骤用lsusb确认摄像头被系统识别用ls /dev/video*查看设备节点用v4l2-ctl --list-devices查看摄像头详细信息确认代码里打开的是正确的设备编号另外OpenCV的VideoCapture在某些摄像头断开重连后不会自动恢复需要在代码里增加重连机制。我的做法是循环检测cap.isOpened()如果返回False释放资源并休眠3秒后重新打开。6.2 识别率白天高晚上低这是所有2D视觉方案都逃不过的问题。光线对人脸识别的影响极其显著同一张脸在自然光和昏暗灯光下提取的特征向量差异可能超过阈值范围。我的解决方案分三步第一摄像头位置尽量避开逆光门口加一盏补光灯第二OpenCV里用直方图均衡化和伽马校正做图像增强改善暗部细节第三在特征提取前对裁剪好的人脸区域做标准化处理。def preprocess_face(face_img): # 转为灰度后做直方图均衡化 gray cv2.cvtColor(face_img, cv2.COLOR_BGR2GRAY) equalized cv2.equalizeHist(gray) return cv2.cvtColor(equalized, cv2.COLOR_GRAY2BGR)虽然不是特别高级的做法但对改善暗光识别率帮助很大。如果你的预算充足直接选带红外补光灯的摄像头效果最好。6.3 阈值怎么调都有人打不开门遇到这种情况先别怀疑算法大概率是注册照片的质量问题。我排查过很多次之后发现绝大多数识别失败都是因为注册时采集的照片不标准。解决思路是重新采集注册照片并且按照下面标准执行正脸双眼睁开面部无遮挡光照均匀不能逆光不能低头仰头表情自然。录制时取连续3帧照片各提取一组特征向量取平均值存入数据库识别稳定性会有明显提升。6.4 OpenCV源码编译卡死树莓派内存只有4GB源码编译OpenCV时多个编译任务并行很容易OOM。解决方式是减少并行编译线程数同时扩大swap空间。sudo nano /etc/dphys-swapfile # 设置 CONF_SWAPSIZE2048 sudo systemctl restart dphys-swapfile然后编译时指定make -j2虽然编译时间变长了但至少不会中途死掉。不过还是那句话除非有特殊需求不建议新手用源码编译OpenCV社区里针对树莓派的换源方案和预编译wheel包已经很成熟了。6.5 树莓派重启后服务不自动启动门禁设备大概率不会有人守在旁边手动启动程序所以必须配置开机自启。我用了systemd服务来做[Unit] DescriptionFace Door Recognition Service Afternetwork.target [Service] ExecStart/home/pi/face-door/env/bin/python /home/pi/face-door/main.py WorkingDirectory/home/pi/face-door Restartalways Userpi [Install] WantedBymulti-user.target这个配置比较关键的是Restartalways它保证主程序崩溃退出后会被systemd自动拉起。我把这个服务文件放在/etc/systemd/system/face-door.service然后sudo systemctl daemon-reload sudo systemctl enable face-door.service sudo systemctl start face-door.service部署完之后TV断电重启也不会影响门禁使用省去了每天手动启动的麻烦。6.6 多特征匹配的优化方向当录入人员超过几百人每次识别都全表遍历一遍特征库速度会逐渐变慢。普通门禁场景几百人问题不大但如果考虑扩展可以做两件事一是按类聚中心把特征分成多个簇先匹配最近的簇再精确匹配二是用FAISS这类向量检索库建立索引。对于树莓派这种算力有限的设备更推荐第一个方案。编码量不大但能把匹配时间从线性降到对数级别。这个优化我目前还没完全落地只做了个原型验证等数据量上来之后会补上。项目扩展方向与个人体会这个项目做到上线运行后我在实际使用中感受最深的不是那些算法效果而是“稳定压倒一切”。刚开始我做了一堆炫酷的功能比如识别到人之后播放语音欢迎词、根据部门推送不同的问候语后来发现这些功能在门禁场景里都是次要的。真正重要的是设备不能死机、不能漏识别、不能延迟开锁、不能在断电后丢失数据。稳定性远比功能多重要。后续我可以在这个框架上继续扩展的点包括接入钉钉或企业微信的人员同步接口实现人员离职后自动删除权限增加多门联动的考勤统计通过MQTT把开门事件上报到服务器做统一审计。这套设计的价值不在于代码本身而在于它把图像处理、算法模型、嵌入式控制、数据存储这些知识真正串起来让我能在一套系统里看到全貌。最后再分享一个小技巧如果你是第一次做类似项目先别急着买一堆硬件先用电脑的摄像头加虚拟机把识别流程跑通确认算法没问题之后再买树莓派和电磁锁。软件不成熟的时候贸然接线只会增加排查问题的难度。人识别开门这个事情的体验真的只有自己亲手做完才会发现它比想象中容易也比想象中有趣。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表