简介:这套基于人脸识别的实验室智能门禁系统,以Python为主要开发语言,采用Django框架与MySQL数据库构建,适合作为计算机相关专业毕业设计或课程设计参考。系统利用普通笔记本电脑摄像头即可完成人脸信息登记、识别与比对,并配有完整的后台管理机制,涵盖管理员和用户两类角色:管理员可进行用户管理、实验室管理、预约管理、考勤管理以及门禁识别记录查询;用户则可修改个人资料、查看实验室、提交预约申请、查看预约结果、查询个人考勤,并通过人脸识别进入对应实验室。整体业务链路完整,从页面交互到人脸识别逻辑均有清晰分层,前后台功能划分明确,便于二次开发与功能扩展。资源为一个独立压缩包,体积约103.82MB,方便在线下载与部署。目前已有97人学习使用,适合需要快速搭建智能门禁项目、或希望参考其数据库表设计与业务流转逻辑的开发者。
1. 智能门禁系统为什么值得用Python做人脸识别
实验室门禁不同于普通办公门禁,它通常要求精确到人、可回溯,并且经常要区分在校生、导师和校外访客。传统磁卡容易丢失或外借,指纹识别又需要被验证者主动配合,而基于Python的人脸识别智能门禁系统可以做到“看一眼即开门”,既保留权限差别,又能把出入记录直接关联到人。选择Python并不是因为它性能极致,而是因为从图像采集、人脸检测到模型推理的生态最完整,OpenCV、dlib、face_recognition、ONNX Runtime这些库把算法链路压缩到几百行代码内。接下来的实现会围绕真实门禁的需求展开:先搭算法链路,再完成身份注册与实时识别,最后处理活体防伪和部署调优。整个方案可以跑在树莓派或边缘盒子上,也可以用普通PC配合串口控制门锁。
2. 人脸识别门禁系统的算法链路与选型
2.1 人脸识别与传统门禁卡的差异
人脸识别门禁系统验证的不是“你手里的东西”,而是“你是谁”。它把用户从物理介质中解放出来,不需要额外携带,也避免门禁卡被复制后带来的安防漏洞。但在实验室场景里,人脸识别链路的每一个环节都可能改变最终判断:首先要从画面中找到人脸(detection),接着将人脸按关键点校正到标准姿态(alignment),再用特征提取器得到一组数字向量(embedding),最后与已注册的向量计算距离(matching)。很多人误以为“识别”只是最后一步,实际上光照、姿势、遮挡在检测阶段就已经给误判埋下了隐患。因此选型时不能只盯着最后的准确率,还要看整套链路在门禁机上是否能控制延迟。
2.2 主流Python人脸识别库对比:OpenCV、dlib、face_recognition、DeepFace
在Python环境里,最常被拿来落地的方案有下面几个:
| Python库/模块 | 检测方式 | 特征提取 | 模型体积 | 适用场景 |
|---|---|---|---|---|
| OpenCV Haar | 传统特征 | 无 | <1MB | 快速原型、人脸区域框选 |
| OpenCV DNN | ResNet-10 SSD | 可选择DNN模型 | ~5MB | 检测速度快,常被用于前端 |
| dlib HOG | 经典HOG | 可配合dnn模型 | 约60-100MB | CPU实时,适合树莓派 |
| face_recognition | 封装dlib | 128维ResNet | 约120MB | 低门槛,适合入门与小型门禁 |
| DeepFace | VGG-Face等 | 集成多个预训练模型 | 数百MB | 效果直观但推理耗时较长 |
OpenCV的Haar检测速度快、漏检率高,不建议作为门禁主检测器;face_recognition把dlib包装得足够友好,安装方便,开发周期最短;如果追求更高精度,可以在后期替换成InsightFace/ArcFace配合ONNX Runtime。对门禁系统来说,检测频率很高,但特征提取只需要在检测到人脸之后执行一次,所以不能只比较单帧速度,更要看整条流水线在边缘设备上的并发放行能力。
2.3 为什么优先考虑开源免费商用的人脸识别模型
实验室自用或学校内部项目,通常没有预算去购买商业授权,但实验室数据又属于敏感数据,不适合把特征值传向云端。开源模型可以完全离线运行,特征不出实验室,这是门禁部署的硬性要求。face_recognition使用的ResNet模型基于LFW数据集训练,输出128维向量,授权链清晰,对于内部系统足够稳定。如果要把误识率再降一个数量级,可以换成InsightFace发布的ArcFace系列推理库,它同样开源,模型较大但可以在Jetson上运行。选择时要有两条底线:第一,模型必须能离线独立推理;第二,授权链清晰,不把实验人员的人脸特征交给第三方服务。
2.4 决定实时性的关键:检测与特征提取
门禁系统对延迟的容忍度通常在一秒左右,这意味检测与特征提取的总耗时不能超过300ms。以OpenCV DNN检测器为例,它的输入是300x300的blob,在CPU上单帧大约20-40ms,检测到人脸后,再对裁剪区域做特征提取,这里才是瓶颈。face_recognition在普通i5 CPU上单张128维特征大约90-150ms,整体实时性仍可接受。下面的示例展示如何用OpenCV DNN做前端检测,并为后续特征提取传入裁剪区域:
import cv2 det_net = cv2.dnn.readNetFromCaffe( 'deploy.prototxt', 'res10_300x300_ssd_iter_140000.caffemodel' ) frame = cv2.imread("lab_member.jpg") h, w = frame.shape[:2] # blobFromImage 会做缩放和通道均值归一化,值域保持为0-255 blob = cv2.dnn.blobFromImage( cv2.resize(frame, (300, 300)), 1.0, (300, 300), (104.0, 177.0, 123.0) ) det_net.setInput(blob) detections = det_net.forward() for i in range(detections.shape[2]): conf = detections[0, 0, i, 2] if conf > 0.7: # 阈值越高,漏检增加,误检减少 box = detections[0, 0, i, 3:7] * [w, h, w, h] x1, y1, x2, y2 = box.astype("int") face_crop = frame[y1:y2, x1:x2] # 后续可以送给人脸编码器 cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite("detected.jpg", frame)这里的关键参数是blobFromImage的尺度和均值。均值(104.0, 177.0, 123.0)是模型在训练时的BGR通道均值,不能随意改为RGB,否则检测率会明显下降。计算detections时,每行数据的前两位是batch和class index,第三位是置信度,后面四位是按图像尺寸归一化的边界框,因此要乘回原图的[w, h, w, h]。如果实验室门口人员走动频繁,可以把输入尺寸从300提高到416,同时也要接受耗时翻倍的代价。
3. 用Python实现人脸数据库采集与本地特征注册
3.1 先配置一个干净的Python运行环境
在开始注册人脸之前,先把环境做干净,避免后面被dll依赖问题打扰。常见做法是用Anaconda或venv建一个python3.8/3.10虚拟环境,然后在里面安装face_recognition和opencv-python。如果你在Windows上装dlib经常失败,可以指定python 3.8并使用预编译的dlib包;在Linux或树莓派上则更简单,直接pip install face_recognition即可。安装完成后用一个小脚本确认摄像头可以打开,同时记录图像尺寸,后续所有处理都沿用这个分辨率。
注意:不要在一个同时装了TensorFlow的全局环境里继续装face_recognition,通篇依赖可能导致摄像头帧缓冲或OpenCV二进制冲突。
3.2 摄像头采集人脸图片的完整流程
门禁识别要稳定,注册样本不能只拍一张。我一般会让人站在摄像头前缓慢转头,采集20-30帧并保留检测置信度较高的人脸裁剪图。这样后续生成的特征向量就包含了多个角度和表情变化。下面的代码完成采集:
import cv2 import os cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) out_dir = "faces/zhangsan" os.makedirs(out_dir, exist_ok=True) count = 0 while count < 30: ok, frame = cap.read() if not ok: break small = cv2.resize(frame, (640, 360)) # 在实际注册时替换为OpenCV人脸检测结果,这里给出固定区域示例 x1, y1, x2, y2 = 200, 100, 500, 400 face = frame[y1:y2, x1:x2] if face.size == 0: continue cv2.imwrite(os.path.join(out_dir, f"{count:03d}.jpg"), face) count += 1 cv2.imshow("register", frame) if cv2.waitKey(10) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()这段脚本用VideoCapture读取摄像头,帧率由摄像头默认,但保存图片时只保存检测到的人脸区域。cap.set后的宽高参数建议固定,因为分辨率越高,单帧特征提取越慢。如果你要采集多个人,需要为每个人单独建目录,目录名可以用学号或工号。另外,保存的人脸区域不要带过大背景,会让特征提取器的特征偏向环境。
3.3 生成128维特征向量并存入SQLite
人脸注册的本质是把清洗好的人脸图片交给face_recognition,让它返回128维向量。这个向量在SQLite里可以按BLOB类型存储,便于后续载入时直接比较。下面是一个建表和插入的完整示例:
import sqlite3 import face_recognition import glob import pickle conn = sqlite3.connect("face_auth.db") conn.execute("""CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY, name TEXT NOT NULL UNIQUE, encodings BLOB NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )""") name = "zhangsan" files = glob.glob(f"faces/{name}/*.jpg") all_embeddings = [] for f in files: image = face_recognition.load_image_file(f) emb = face_recognition.face_encodings(image) if emb: all_embeddings.append(emb[0]) # 对同一个人的多次特征取均值,并序列化为bytes mean_emb = sum(all_embeddings) / len(all_embeddings) blob = pickle.dumps(mean_emb.astype("float32")) conn.execute("INSERT INTO users (name, encodings) VALUES (?,?)", (name, blob)) conn.commit()代码的关键在于face_encodings返回的是一个数组,每个元素对应图片中的一张人脸,所以取emb[0]前要确保图片里只有一个人。取均值可以减少单帧抖动,但如果在角度差异过大的样本上取平均,反而会让结果“四不像”。更保险的做法是保留多个特征样本,而不是只存均值。用pickle.dumps序列化特征,是因为SQLite只接受字节串,加载时再用pickle.loads恢复为numpy数组。
3.4 注册多个身份时的批量脚本
当实验室人数多起来之后,逐个执行上面的脚本会浪费时间。可以把采集和注册合并成一个命令行工具,先读取人员名单,再对每个人采集图片并立刻入库。表结构需要增加一个sample_count字段,方便后续查询每个人员的有效特征数。一个实用的策略是一人保存8-10张图片,并且每张图片的特征都要能通过一定置信度验证后再入库,达不到就重新拍。这样可以防止照片模糊或闭眼导致注册质量差。数据库建立后,建议定期把图片文件归档到冷存储,因为SQLite里只留特征向量,原始图片能帮助排查错误识别。
4. 实时门禁识别:从摄像头帧到门锁控制
4.1 载入注册特征并构建内存匹配列表
为提高实时匹配速度,门禁程序启动时就应把SQLite中的人员特征全部加载到内存,而不是每次识别都查询数据库。这里采用numpy数组存储特征,用列表保存人名。加载时要注意特征向量的dtype需要统一为float32,避免比较时隐式转换。
4.2 实时视频流人脸识别循环
主循环代码:
import cv2 import numpy as np import face_recognition known_names = ["zhangsan"] known_encodings = np.load("known_encodings.npy") cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) cooldown = 0 while True: ok, frame = cap.read() if not ok: break rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) boxes = face_recognition.face_locations(rgb, model="hog") if boxes: encodings = face_recognition.face_encodings(rgb, boxes) for encoding in encodings: distances = np.linalg.norm(known_encodings - encoding, axis=1) min_idx = np.argmin(distances) if distances[min_idx] < 0.45: # 开启门锁逻辑 unlock(cooldown) cv2.putText(frame, known_names[min_idx], (20, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) cv2.imshow("door", frame) if cv2.waitKey(10) & 0xFF == ord('q'): break cap.release()这里使用np.linalg.norm计算欧氏距离,而不用face_recognition自带的compare_faces,原因是批量比较时numpy向量化更快。阈值0.45是一个经验值,如果实验室对安全更敏感,可以调低到0.40,但拒识率也会上升。unlock(cooldown)内部会检查距离上次开锁是否超过设定的3秒,防止同一人连续触发。
4.3 门锁控制GPIO与继电器接线
在树莓派上控制电插锁并不复杂,关键是使用继电器隔离GPIO与大电流负载。常见的接法是树莓派的GPIO 17接到光耦继电器的IN端,继电器常开触点串联到电插锁的电源回路。Python端可以用RPi.GPIO库或gpiozero实现:
import RPi.GPIO as GPIO GPIO.setmode(GPIO.BCM) GPIO.setup(17, GPIO.OUT) def set_lock(open_lock): GPIO.output(17, GPIO.HIGH if open_lock else GPIO.LOW)在门禁机初始化时,应该默认GPIO.LOW保持门锁关闭。开锁信号一般保持1至2秒,然后自动恢复低电平,防止电插锁长时间通电发热。接线前要仔细阅读电插锁规格,如果工作电流超过继电器额定值,需要换成交流接触器。以下是常用端子说明:
| 连接对象 | 树莓派/控制板 | 继电器 | 电插锁 |
|---|---|---|---|
| 控制信号 | GPIO17 | IN | - |
| 公共电源 | GND(与继电器共地) | VCC | +12V |
| 输出回路 | - | NC/COM | 串联 |
注意续流二极管要紧靠继电器线圈,否则反向电动势可能打坏GPIO引脚。
4.4 识别到合法人员后的唯一开锁队列
当门禁前方同时出现多人时,程序可能一次性生成多个编码。门禁系统只需要给其中一个人开锁即可,所以加入一个granted布尔标志,当本次循环已经开门后,就不再处理剩余人脸,直接跳过。同时把“识别事件”写入日志文件,格式为时间戳、姓名、距离值。这个日志是后续安全审计的重要依据。
5. 识别率调优与边缘部署的实战要点
5.1 光照、角度和遮挡对识别的影响与对策
实验室门口的灯光往往忽明忽暗,正午的侧光会让五官阴影变得强烈。解决方式是在门禁机上方安装补光灯,保持人脸区域亮度在250-350 lx。摄像头安装高度建议在1.5米处,向下倾斜15度左右,这样既拍到脸部又不至于拍到头顶。注册照片时也要覆盖日常角度,包括略低头、略侧脸和佩戴眼镜的情况。遮挡方面,在后疫情时代不少人戴口罩,普通face_recognition无法识别,可以在门口提供“口罩佩戴+工号”二种备用验证方式,而不是直接降低阈值。
5.2 欧氏距离阈值与误识率/拒识率平衡
face_recognition的128维特征在不同人之间距离通常大于0.5,同一人不同照片的距离通常在0.3-0.5之间,所以映射并不完全线性。可以用提前录制的一段包含多人真实出行的视频来标定阈值:统计合法用户的平均距离和非法用户的平均距离。一个简单参数表如下:
| 阈值设置 | 误识率表现 | 拒识率表现 | 适用场景 |
|---|---|---|---|
| 0.35 | 很低 | 较高,需多次尝试 | 高安全等级 |
| 0.45 | 低 | 低 | 一般实验室门禁 |
| 0.55 | 略微上升 | 极低 | 人流量大的走廊门禁 |
在生产系统里,我倾向于先用统计方法计算阈值:对每个已注册用户随机抽取40%样本作为测试,剩余60%训练,画出ROC曲线,再取误识率低于万分之一对应的距离作为阈值。下面的脚本可以输出每个匹配样本的距离分布:
import numpy as np import face_recognition def compute_distances(folder, known_embedding): distances = [] for img in sorted(folder.glob("*.jpg")): image = face_recognition.load_image_file(str(img)) emb = face_recognition.face_encodings(image) if emb: distances.append(np.linalg.norm(emb[0] - known_embedding)) return np.array(distances) # 输出合法样本的均值、标准差,用于后续选择阈值运行后会看到在理想光照下同一个人样本距离的标准差通常小于0.06,如果标准差偏大则意味着注册样本质量不稳定,需要重拍。
5.3 在Jetson Nano或树莓派上部署时的性能参数
边缘设备的内存和算力有限,不能直接照搬PC上的1080p帧。建议把摄像头分辨率固定为640x480,检测模型使用OpenCV DNN的SSD;如果树莓派4B上检测时间仍超过40ms,可以改用Haar + dlib HOG作为轻量检测,不过漏检率会提高。在Jetson Nano上可以把特征提取模型换成TensorRT优化后的ONNX,但前期开发仍先使用同样的face_recognition跑通业务。以下是一个对单帧推理做简单计时的参考代码:
import time start = time.perf_counter() boxes = face_recognition.face_locations(rgb, model="hog") encoding = face_recognition.face_encodings(rgb, boxes) elapsed = (time.perf_counter() - start) * 1000 print(f"det+enc: {elapsed:.1f} ms")如果单帧总耗时高于500ms,就要考虑减少检测频率,比如每隔一帧做一次检测。另一个技巧是只在画面面积大于30x30像素的人脸区域才做特征提取,避免远处小目标拖慢整条链路。
5.4 批量注册大量人员时的向量化匹配
实验室人员数量达到数百人后,如果使用for循环逐个计算欧氏距离,效果并不差,但视觉上不够优雅。更好的做法是把所有特征堆成一个二维numpy矩阵,用向量化一次算出所有距离。代码可以这样:
# known_features shape: (N, 128),query_feature shape: (128,) diff = known_features - query_feature distances = np.sqrt(np.sum(diff ** 2, axis=1)) best_match = np.argmin(distances)这段代码可以替代第四章中的np.linalg.norm(known_encodings - encoding, axis=1),两者本质一致。当N达到数千时,numpy矩阵运算会显著减少Python层循环开销。
6. 防伪造攻击与系统健康自检
6.1 用眨眼检测对抗照片与视频回放
静态照片打印后放在摄像头前,普通人脸识别也能通过,因为模型的输出与真人没有差异。最实际的防御是活体检测,常见做法是帧间计算眼睛纵横比(EAR)。EAR阈值小于0.2时认为闭眼,在1.5秒内检测到一次从小于0.2到大于0.25的变化,就判定为眨眼。这样能过滤照片,但对预先录制的真人视频仍有风险。如果门禁等级要求更高,需要依靠红外/结构光摄像头,这属于硬件方案,本系统的软件判断只能作为第一道防盾。
6.2 每日自检日志与阈值漂移告警
可以在设备启动时执行健康检查:加载数据库,选取三名随机用户的最后一张注册特征,与当前摄像头拍到的实时特征比对,如果距离长时间高于历史基准的1.5倍,说明摄像头位置移动或环境光照发生了变化,需要在日志中打上警告。维护人员可以定时执行一条命令检查:
grep "WARN" /var/log/access_control.log | tail -20这条命令只查看最近20条警告,如果警告频繁出现,就要重新校准阈值或清洁镜头。
6.3 用录制视频回放验证识别精度
在调整参数前后,使用同一段包含真人出入的视频回放,可以量化系统精度的变化。下面是一个精简的验证脚本:
import face_recognition import cv2 video = cv2.VideoCapture("test_sample.mp4") tp = fp = 0 while True: ok, frame = video.read() if not ok: break rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) boxes = face_recognition.face_locations(rgb, model="hog") encodings = face_recognition.face_encodings(rgb, boxes) # 与实际标签做比对,统计tp/fp # 这里省略逐帧标签逻辑 tp += matched_right fp += wrong_grant print(f"TP={tp} FP={fp}")回放时可以故意加入光照变化或侧脸片段,用来测试系统在不同条件下的稳定性。如果真人和照片都被判为合法,说明活体检测还有缺口,需要在这个环节继续增加新的特征。
本文还有配套的精品资源,点击获取