拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Python的人脸识别门禁系统毕业设计:从算法到工程完整实现指南

基于Python的人脸识别门禁系统毕业设计:从算法到工程完整实现指南

简介:基于Python的人脸识别智能门禁系统毕业设计项目,面向计算机相关专业毕业生、课程设计及期末大作业学生,提供可直接运行的高分毕设源码。包内共262个文件,涵盖前端Vue组件(37个vue、8个js)、后端Java代码(20个java)及TypeScript模块(71个ts),同时包含31个png、11个jpg等界面素材和9个mp4操作演示视频,压缩包大小约70.36MB,目录结构清晰,便于部署与二次开发。项目经过严格调试,代码注释详尽,新手也能理解核心逻辑,已有146人学习下载。该系统功能完整、界面美观,覆盖人脸识别门禁的完整流程,涉及Spring Boot、Vue等主流技术栈,可用于算法实现、数据库设计及前后端联调等环节,能有效提升项目完成度与答辩评分,适合直接作为毕业设计或期末大作业使用。

1. 人脸识别门禁毕设,真正拉开差距的不是算法而是完整度

同组同学做“基于STM32的人脸识别门禁”,买一个人脸识别模块接上,代码两三百行,答辩时只能讲“调用模块”。而你用 Python 把摄像头采集、人脸检测、特征提取、数据库比对、继电器开锁、日志记录整条链路都自己写下来,评委随便问一个环节你都能展开讲半小时——这才叫高分毕设。所谓基于Python的毕业设计人脸识别的智能门禁系统,说白了就是一条数据闭环:人脸图片进来,特征提出来,比对数据库,结果控制门锁。它适合本科毕设、也适合想从 Python 入门往综合项目走的开发者,难点不在“人脸识别”本身,而在系统完整度和现场演示不翻车。

2. 识别选型与识别管线:先分清检测和识别,再谈用什么库

2.1 检测和识别是两个环节,很多人栽在第一步

人脸检测是“找到脸在哪”,输出一个矩形框;人脸识别是“判断这是谁”,输出身份或相似度。OpenCV 自带的 Haar Cascade 只能做检测,不能判断身份。把 Haar Cascade 当识别用,是毕设里最常见的翻车现场。

如果坚持纯 OpenCV 路线,能识别身份的是 LBPH(局部二值模式直方图)。它把脸部区域划分成小块,统计纹理直方图,再用近邻分类器比对。优点是零依赖、安装简单、任意 Python 版本都能跑;缺点是特征表达能力弱,对光照、角度、人脸表情极其敏感。门禁场景里人不会正襟危坐等你采集,稍微侧脸或逆光,LBPH 的识别率就掉得没法演示。

我的建议是:检测和特征提取都用face_recognition库,它是 dlib 深度模型的 Python 封装,把一张脸编码成 128 维浮点向量。比对时算两个向量的欧氏距离,距离小于阈值就判定为同一人。这个库 API 极简,中文资料多,毕设选它做主干,既省时间又能在答辩时讲清楚深度特征的含义。

2.2 识别管线的四个环节与参数基线

一套完整的人脸识别管线分成四步:检测、对齐、编码、比对。

  • 检测:face_recognition.face_locations()默认用 HOG 特征检测,CPU 上也能跑;传入model="cnn"精度更高但慢。毕设演示用 HOG 足够。
  • 对齐:dlib 会先定位 68 个面部关键点,按眼睛、鼻子的几何位置把人脸摆正。这一步对侧脸容忍度很关键。
  • 编码:face_encodings()输出 128 维向量,num_jitters参数控制对同一张图多次采样取平均,值越大特征越稳,速度越慢。注册照片建议设 10 到 50,实时识别设 1。
  • 比对:face_distance()或手写np.linalg.norm()算欧氏距离。阈值别用默认的 0.6,门禁场景建议 0.5 左右;0.4 更严格,但人脸角度稍微一变就会拒识。

装好库以后,先用一张自拍验证环境是否通:

import face_recognition as fr img = fr.load_image_file("me.jpg") locs = fr.face_locations(img) print("检测到的人脸数:", len(locs)) enc = fr.face_encodings(img, known_face_locations=locs)[0] print("特征维度:", len(enc), "数据类型:", enc.dtype)

face_encodings()的第二个参数传检测位置,是为了避免函数内部再检测一次,省一半时间。enc是 128 维的 numpy 数组,后面入库和比对都以它为最小单元。这里能跑通,整个项目的地基就打好了。

2.3 选型对比:为什么首选深度特征路线

方案依赖复杂度准确率适合场景毕设风险
Haar Cascade + LBPH只需 OpenCV光照敏感、相似脸难分人脸检测教学 demo演示易翻车,答辩难展开
face_recognition + dlib需装 dlib,编译有坑稳定,128 维深度特征门禁、考勤、课堂点名环境安装是唯一难点
自训 CNN需 PyTorch / TensorFlow 和 GPU取决于数据量和调参研究型毕设数据标注、训练时间不可控

自己训练 CNN 不是不行,而是本科毕设周期撑不住。你要准备上万张人脸数据、清洗标注、调学习率、防过拟合,最后效果还不一定比预训练模型好。更聪明的做法是主干用face_recognition,在答辩时主动说一句:“如果要做轻量化部署,可以把编码网络换成 MobileFaceNet 或 ArcFace,我的代码只在特征提取这一层做替换。”这句话比“我从零训了一个模型”可信得多。

3. 系统拆分与数据设计:先画模块图,再写代码

3.1 四层模块:采集、识别、控制、记录

门禁系统听起来复杂,拆开就四块:采集层负责从摄像头拿画面;识别层负责检测、编码、比对;控制层负责开门动作,通过串口或 GPIO 驱动继电器、电磁锁;记录层把每次开门和拒绝写入数据库。

四层之间不要互相耦合。识别层只返回“允许/拒绝 + 距离值”,控制层收到允许就发开锁指令,记录层无论结果都写日志。答辩被问“系统架构”时,你能画出这张模块图并说清楚数据流向,比堆功能有用得多。

3.2 数据库表结构:用户表、日志表与特征存储

SQLite 是毕设的最佳选择,一个.db文件搞定,不需要单独装数据库服务。建表语句如下:

CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, feature BLOB NOT NULL, created_at TEXT DEFAULT (datetime('now','localtime')) ); CREATE TABLE IF NOT EXISTS access_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, result TEXT CHECK(result IN ('allow','deny')) NOT NULL, distance REAL, created_at TEXT DEFAULT (datetime('now','localtime')), FOREIGN KEY (user_id) REFERENCES users(id) ); CREATE INDEX IF NOT EXISTS idx_log_user ON access_log(user_id); CREATE INDEX IF NOT EXISTS idx_log_time ON access_log(created_at);

feature字段必须用 BLOB,别用 TEXT。人脸特征向量是 128 个 float32,直接tobytes()存进 BLOB,读出时再np.frombuffer()还原。如果存成文本字符串,精度会在小数点后几位丢失,重启程序后同一个人可能就认不出来了。

result字段用 CHECK 约束限定成allow和deny两个值,从数据库层面防止脏数据。日志表一定要建索引,否则用到几千条记录后,按时间查日志会越来越慢。时间字段用本地时间,datetime('now','localtime')在 SQLite 里直接生成可读字符串,方便答辩时展示。

3.3 注册人脸:为什么至少拍 3 张而不是一张照片走天下

录入人脸是整个系统的数据入口,设计不好后面全是坑。我见过很多人只让用户拍一张正面照就入库,结果识别时换个角度就拒识,然后开始乱调阈值。

正确做法是注册时收集 3 到 5 张不同角度照片(正面、左右微侧),每张都提取特征向量,入库时算它们的平均向量作为这个人的人脸特征。这样能显著减小姿态变化带来的距离波动。注册函数的核心逻辑:

import sqlite3 import numpy as np import face_recognition as fr def add_face(name, img_path, conn): img = fr.load_image_file(img_path) locs = fr.face_locations(img) if len(locs) != 1: raise ValueError(f"注册照片必须且只能有一张脸,当前检测到 {len(locs)} 张") enc = fr.face_encodings(img, known_face_locations=locs)[0] feature_blob = enc.astype(np.float32).tobytes() conn.execute( "INSERT OR REPLACE INTO users(name, feature) VALUES(?, ?)", (name, feature_blob), ) conn.commit()

INSERT OR REPLACE保证同名用户重复注册时更新特征而不是新增记录,演示时如果录错了人,直接重录就行,不用去删数据库。len(locs) != 1的校验很重要,注册照片里出现多张脸,face_encodings会默认取第一张,你可能把陌生人特征录到别人名下。

注册完随手验证一下:用同一张照片重新提取特征,和库里这条记录算一次距离,如果距离大于 0.3,说明图片本身清晰度有问题,先换照片再入库。

4. 核心实现:从摄像头画面到门锁动作的完整链路

4.1 实时识别最小循环

实时识别和单张图片识别最大的区别是速度。摄像头每秒来 25 到 30 帧,如果每一帧都全尺寸跑一次深度特征提取,笔记本 CPU 直接拉满,画面卡成幻灯片。常见做法是跳帧 + 缩帧:每 2 帧处理一次,识别用缩小一半的画面,显示用原画面。

import cv2 import numpy as np import face_recognition as fr import sqlite3 # 启动时一次性加载数据库里的全部特征 conn = sqlite3.connect("door.db") names, feats = [], [] for row in conn.execute("SELECT name, feature FROM users"): names.append(row[0]) feats.append(np.frombuffer(row[2], dtype=np.float32)) cap = cv2.VideoCapture(0, cv2.CAP_DSHOW) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) frame_idx = 0 while True: ok, frame = cap.read() if not ok: break frame_idx += 1 # 跳帧:每隔一帧才做识别,画面流畅优先 if frame_idx % 2 != 0: cv2.imshow("door", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break continue small = cv2.resize(frame, (0, 0), fx=0.5, fy=0.5) locs = fr.face_locations(small, model="hog") encs = fr.face_encodings(small, locs) for enc in encs: dists = [np.linalg.norm(enc - f) for f in feats] best = int(np.argmin(dists)) if dists[best] < 0.5: print(f"允许开门: {names[best]}, 距离 {dists[best]:.3f}") # 这里调 unlock() else: print(f"拒绝: 陌生人, 最近距离 {dists[best]:.3f}") cv2.imshow("door", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()

几个关键点:np.argmin找的是最近的特征,dists[best] < 0.5是判定条件,阈值卡得太紧会出现本人频繁拒识,调太松陌生人也能混进来。0.5是经验值,演示前拿本人和陌生人各测十次再微调。缩帧到 640x480 而不是 1280x720,识别速度能快一倍以上,代价是远距离小脸检测不到——门禁场景人脸本来就靠近摄像头,这个取舍是划算的。

4.2 眨眼活体检测:让一张照片没法开门

face_recognition 对平面照片一样能提取特征,拿一张手机照片放在摄像头前,距离完全可能低于阈值直接开门。门禁系统必须加活体检测,最轻量、可解释性最强的方案是眨眼检测:连续若干帧里眼睛从睁开变闭合再睁开,才算活人。

方案基于 dlib 的 68 点关键点模型,计算眼睛纵横比(EAR)。眼睛睁开时 EAR 在 0.25 到 0.35 之间,闭合时明显小于 0.2。

import numpy as np def eye_aspect_ratio(eye_points): # eye_points 是单只眼睛的 6 个关键点坐标 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) def is_blink(shape): # dlib 68 点模型里:左眼 42-47,右眼 36-41 left_ear = eye_aspect_ratio(shape[42:48]) right_ear = eye_aspect_ratio(shape[36:42]) return (left_ear + right_ear) / 2.0 < 0.2

EAR 阈值 0.2 是个经验基线,摄像头距离人脸 0.5 米以内都适用。如果发现真人也经常被判定为眨眼,先打印正常睁眼时的 EAR 值,用实测值的一半做阈值,别照搬别人的参数。活体检测放在比对成功之后,只有距离达标且检测到一次眨眼才发开门信号。这段逻辑能挡住静态照片,但挡不住提前录好的视频,答辩时主动提一句这个局限,比被评委问出来体面得多。

4.3 串口控制继电器与状态反馈

识别结果最终要落到“门开没开”这个动作上。毕设里最常见的执行器是继电器,用单片机的串口接收 Python 指令,再通过 GPIO 控制继电器吸合,带动电磁锁或蜂鸣器。

import serial import time try: ser = serial.Serial("COM3", 9600, timeout=0.5) except serial.SerialException: print("警告: 串口未连接,切换到模拟开门模式") ser = None def unlock(duration=2.0): if ser: ser.write(b"1") # 继电器吸合,开门 time.sleep(duration) # 保持开门时间 if ser: ser.write(b"0") # 继电器断开,落锁

Windows 下 COM 口号在设备管理器里查,Linux 下一般是/dev/ttyUSB0,如果提示无权限,执行sudo usermod -aG dialout $USER再重新登录。unlock()里的duration是个值得细调的参数:电磁锁一般 1 到 2 秒足够,蜂鸣器模拟可以设 3 秒方便评委看清。

如果不想外接硬件,树莓派或行空板这类带 GPIO 的开发板也能做同样的控制,Python 代码用gpiozero或板子自带库把ser.write(b"1")换成拉高一个引脚就行。控制层保持函数签名不变,整个识别层完全不用动。

5. 常见拦路坑与排查清单:五个现场翻车案例

5.1 Windows 下 dlib/face_recognition 装不上

现象:pip install face_recognition跑了一会儿,最后输出一串红色报错,关键字段是Failed to build dlib。很多人卡在这一步直接放弃整个项目。

原因:dlib 没有现成的 Windows wheel(部分 Python 版本除外),pip 会尝试从源码编译,而编译需要 Visual Studio Build Tools 和 CMake。没装这两个工具,编译必失败。

解决:先装 Visual Studio Build Tools,安装时勾选“使用 C++ 的桌面开发”工作负载,再装 CMake,最后重试pip install dlib。Python 3.8 / 3.9 可以直接找第三方预编译 wheel 文件安装,省掉整个编译过程。如果你用的是 conda,conda install -c conda-forge dlib能直接装二进制包,是最省事的路径。这条经验是很多人的血泪教训:环境问题先解决,别在装库上耗一天。

5.2 第一帧识别奇慢,之后才流畅

现象:程序启动后,第一次检测到人脸要卡 2 到 3 秒才出结果,之后每帧倒是挺快。答辩演示时正好卡在最开始,场面很尴尬。

原因:首次调用时,模型权重文件要加载进内存,HOG 检测器也要初始化。另外如果摄像头默认输出 1280x720,首帧解码和缩放本身就慢。

解决:程序启动时先跑一次空检测预热模型,比如主动对一张纯色图片调用face_locations();把face_locations()的number_of_times_to_upsample从默认 1 改成 0,检测小脸能力略降,但速度翻倍。门禁场景人脸占画面比例大,完全够用。再配合缩帧和跳帧,首帧卡顿问题基本消失。

5.3 逆光和暗光下识别率暴跌

现象:白天在窗边录入人脸,识别时背对窗户,人脸上半部处于阴影里,系统完全认不出;晚上开灯又稍微好一点,但整体不如白天稳定。

原因:人脸特征对亮度分布敏感。注册照片和识别照片的光照差异太大,128 维特征向量会被拉远,距离超过阈值就被拒识。

解决:注册和识别尽量在同一光照环境完成,这是成本最低的办法。如果两个光照环境没法统一,就在识别前加 CLAHE 对比度增强:

# 对灰度图做 CLAHE,再返回给 face_recognition 使用 gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) enhanced = clahe.apply(gray)

clipLimit=2.0控制对比度增强强度,调太大画面会失真,人脸特征反而变差。加了这段预处理后,暗光下的识别率能提一截,但不要指望它解决极端逆光——那种场景只能换补光灯,算法救不了硬件。

5.4 照片不仅能开门,甚至比本人还快

现象:把手机里的大头照放摄像头前,系统直接放行;本人站在前面,偶尔还要调整角度才能通过。评委看到这一幕,项目基本就告别高分了。

原因:没有活体检测,且照片中的人脸占比通常比真人更“标准”,特征距离比真人的还小。这是门禁系统的原理漏洞,不是偶然故障。

解决:把 4.2 节的眨眼检测接入比对流程,只有“距离达标 + 检测到眨眼”才允许开门。同时把阈值从 0.6 收紧到 0.45 到 0.5,让照片的相似度不足以轻松通过。这里要说明白:眨眼检测只能防照片,防不了提前录好的翻拍视频。想防视频需要配合动作指令(比如要求转头、张嘴)或红外深度相机,毕设做到眨眼级别已经能体现安全意识,把边界讲清楚就行。

5.5 特征向量存数据库后再取出,识别率明显下降

现象:注册时测试识别一切正常,重启程序后从 SQLite 读出特征,同一个人突然认不出来,距离比陌生人还远。

原因:特征入库时用了str()或np.array2string()把向量转成了文本。numpy 默认打印只保留几位小数,转回数组时精度已经损失;更隐蔽的问题是 dtype 从 float32 变成了 float64,距离计算和原特征不一致。

解决:严格用二进制存取,入库feature.astype(np.float32).tobytes(),出库np.frombuffer(feature_blob, dtype=np.float32)。这两个函数是成对出现的,缺一个都会出问题。我在这个坑上栽过一次,后来习惯把存取封装成两个独立函数,并在注册后自动做一次“读库还原 + 比对”自检,距离大于 0.3 就报错,防止坏数据悄悄流进系统。

6. 答辩定稿前,用三件事把完整度再提一档

6.1 用 FAR / FRR 把识别精度变成答辩数据

“我的系统能识别”是陈述,“误识率 0.5%、拒识率 2%”是数据,后者的说服力完全不在一个量级。准备 10 个已注册用户和 10 个陌生人,每人拍 10 次测试,统计两个指标:FAR 是非授权用户被放行的比例,FRR 是授权用户被拒绝的比例。把阈值从 0.4 调到 0.6,记录每个阈值下的 FAR 和 FRR,做成一张表:

阈值FARFRR
0.400.0%5.0%
0.500.5%2.0%
0.603.0%0.5%

答辩时拿这张表讲“阈值为什么选 0.5”,比任何形容词都管用。我一般会把这个测试脚本直接挂在项目里,评委想看随时能再跑一遍。

6.2 给程序加连续失败告警与截图留存

真正的门禁系统不能只记一条 deny 日志就完事,连续多次识别失败要触发告警。实现起来不复杂:维护一个fail_count,失败加一,成功清零,达到 3 次就把当前帧截图存到logs/目录并蜂鸣报警。这个功能演示效果极好,评委能看到系统不仅会拒绝,还会留下“犯罪证据”。

6.3 固定演示顺序:注册、开门、拒绝、防照片

答辩演示最怕临场乱点。我的习惯是先演示注册一个测试用户,再演示该用户正常开门,然后换一个未注册的人脸演示拒绝,最后拿手机照片演示活体检测拦截。这个顺序覆盖了系统的四个核心功能,每一步都有明确预期。如果答辩电脑没有 Python 环境,提前用 PyInstaller 把程序打包成 exe,启动前把数据库和摄像头检查一遍,现场翻车的概率就低到可以忽略。

我现在的习惯是每次演示前,从头到尾跑一遍完整流程,确认数据库干净、串口在线、摄像头光圈正常,再上台。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表