简介:一份基于人脸识别技术的小区门禁管理系统完整源代码,使用Python 3.6.8开发,搭配MySQL 5.7数据库,覆盖管理员端与用户端全部核心流程,适合高校毕业设计、课程设计,以及希望借助完整项目入门Python人脸识别开发的读者。系统实现管理员注册登录、管理员账号增删、用户数据管理、人脸图片批量增删与单条操作,并可查询已录入但未训练模型的用户;用户端支持实时人脸识别、姓名缩写显示、语音提示开门、陌生人与拉黑用户响铃告警,且限定单人识别,业务闭环完整。资源包共84个文件,以.py/.pyc可执行源码为主,辅以.ui界面文件、数据库.sql脚本、.onnx人脸模型、.mp3提示音、jpg/png图像素材与需求说明文档,整体压缩包约12.33MB,代码分层包含utils、dao、service等模块,部署文档完整,可快速还原环境并运行调试。已有44人学习,非常适合用于毕业设计参考、课程实训或在此基础之上进行功能二次开发。
1. 人脸识别门禁系统:从毕设源码落地到生产逻辑
拿到“基于人脸识别智能化小区门禁管理系统源代码(完整前后端+mysql+说明文档+LW).zip”这类压缩包,第一件事不该是解开去点运行,而是先想清楚它是四层结构的合成体:摄像头端的抓拍与识别、后端业务接口、MySQL 里落库的业主和门禁日志、以及 Vue/管理系统这类人机交互界面。标题里的 LW 是论文的缩写,说明这套交付物还包含毕业设计文档,系统的架构图、ER 图、测试截图都会在文档里对应出现。人脸识别门禁和普通考勤机的差异在容错责任:考勤迟到可以补卡,门禁放行一个陌生人则直接变成安全事件。因此它不能只有“能认脸”这个 demo,还需要把阈值、有效期、黑名单、异常记录全部纳入业务闭环。这篇文章按大多数人做这类系统时的实际路径来拆:先建模数据库,再把人脸识别链路串起来,最后落到本地部署和答辩调优。适合正在改毕设源码的同学,也适合想把这个题目改造成小规模商用方案的一线工程师。
2. 前后端分离架构与 MySQL 表设计:业主、访客和门禁日志怎么建模
人脸识别门禁管理系统的代码形态,绝大多数是前后端分离:Spring Boot 提供 REST 接口,Vue 配合 Element UI 做管理页面,MySQL 存持久化数据,识别引擎要么内嵌在后端进程里,要么单独拆成一个识别服务。为什么把识别引擎单独拆开而不是揉进 Controller?因为人脸比对是 CPU/内存密集操作,业务接口却需要快速响应管理页面请求,两者混在一起时,一次批量注册人脸会把整个接口堵住。拆开之后,识别请求走独立线程池或者独立进程,就算识别服务卡住,物业后台的查询、登记接口仍然能正常返回。
2.1 模块边界:设备端、识别服务、管理后台各管什么
整套系统可以按物理位置切三个模块。设备端是单元门或小区大门口的人脸识别门禁机,它的职责是持续抓拍、检测画面里是否有人脸、把抓拍图上传给识别服务;识别服务接收抓拍图后,做人脸特征提取,再去 MySQL 里做 1:N 比对,返回是否放行;管理后台则负责录入业主信息、上传底库照片、查看开门记录、管理访客预约。这三个模块通过 HTTP 接口交互,实际项目里常见的是门禁机主动向上抛数据,后台管理的是配置下发,设备端本地也会缓存一份黑名单和白名单,断网时保证基本通行能力。
2.2 MySQL 核心表:业主、管理员、门禁记录、访客登记
先把业务对象列清楚,建表就不会乱。一般的门禁管理系统会涉及四类实体:业主用户是人脸底库的归属者,物业管理员是操作后台的人,门禁记录是每一次识别行为的流水,访客登记是临时授权记录。这四张表基本能覆盖 80% 的毕设需求,如果再加上设备表,就能适配多个门口的场景。
下面是一组常用的建表语句,字段名尽量用工整的命名规范,方便写 MyBatis 映射:
CREATE TABLE `face_user` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_name` varchar(32) NOT NULL COMMENT '业主姓名', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `unit_no` varchar(32) DEFAULT NULL COMMENT '楼栋单元,例如 3-2-501', `face_feature` blob COMMENT '人脸特征向量,12288字节', `face_image_url` varchar(255) DEFAULT NULL COMMENT '人脸底库照片访问路径', `status` tinyint NOT NULL DEFAULT '1' COMMENT '1正常 0停用', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='业主表'; CREATE TABLE `access_log` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint DEFAULT NULL COMMENT '识别命中的业主ID,未命中为NULL', `capture_image_url` varchar(255) NOT NULL COMMENT '现场抓拍图', `compare_score` decimal(6,4) DEFAULT NULL COMMENT '人脸比对相似度或距离值', `access_time` datetime NOT NULL COMMENT '识别时间', `access_status` tinyint NOT NULL COMMENT '1放行 0拒绝', `device_no` varchar(64) DEFAULT NULL COMMENT '门禁机编号', PRIMARY KEY (`id`), KEY `idx_access_time` (`access_time`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='门禁通行记录'; CREATE TABLE `visitor_record` ( `id` bigint NOT NULL AUTO_INCREMENT, `visitor_name` varchar(32) NOT NULL, `visitor_phone` varchar(20) NOT NULL, `visited_user_id` bigint NOT NULL COMMENT '被访业主', `visit_reason` varchar(128) DEFAULT NULL, `start_time` datetime NOT NULL, `end_time` datetime NOT NULL, `status` tinyint NOT NULL DEFAULT '0' COMMENT '0待审核 1已通过 2已到期', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='访客登记表';两组字段要解释一下:face_feature用 blob 存的是人脸识别模型提取出来的特征向量,而不是底库照片本身。以 Dlib 的 128 维人脸模型为例,每个 float 是 4 字节,总长 512 字节;如果是 ArcSoft 虹软之类的引擎,特征可能是 1024 字节或更长,用 blob 最省事。识别时把 blob 读出转成 float 数组,送去和现场提取的特征做距离计算。compare_score字段要提前想清楚存的是什么语义:Dlib 用的是欧氏距离,值越小越相似;虹软和百度的人脸比对返回的是相似度,值越大越相似。字段设计成 decimal 两种都兼容,代码里再根据引擎做阈值方向判断。
2.3 人脸特征存储的另一种方案:特征文本化
用 blob 存特征,MySQL 单表压力不大,但如果人脸数量超过十万,全表扫描比对就不现实了。实际项目里有两种折中方案:一是把特征向量转 Base64 或 Hex 字符串存到 text/varchar 字段,代码里先读一列再反序列化,虽然多了一道转换,但排查问题时能看到特征数据,便于复现比对错误;二是用独立的特征索引中间件,比如 Redis 里面维护一个user_id -> feature的哈希表,重启时再全量加载。对毕设和小规模小区来说,直接用 blob 存特征最简单,识别服务启动时一次性加载全部业主特征到内存,后续比对全在内存里做,MySQL 只承担持久化职责。
3. 人脸识别核心链路:摄像头抓拍、特征比对与阈值调校
人脸识别门禁的实时链路可以抽象成三个阶段:抓拍、比对、放行回写。很多毕设项目在实际演示时没有真实门禁机,会用笔记本摄像头 + 继电器模块模拟开锁,或者干脆在管理端上传一张照片作为“模拟抓拍图”。不管用哪种方式,核心代码路径是一致的:拿到一帧图片,检测人脸位置,裁剪出人脸区域,送入特征提取模型,再和数据库里的底库特征做距离/相似度计算,最后根据阈值判定是否放行。
3.1 抓拍与人脸检测:先确保检测框稳定再谈识别
摄像头取流最常用的方案是 OpenCV 的 VideoCapture。门禁机通常是 RTSP 流,本地测试则直接开相机索引。下面这段 Python 代码展示了抓拍帧、检测人脸并截取人脸的典型处理方式:
import cv2 cap = cv2.VideoCapture(0) # 分辨率设低一点,减少人脸检测耗时 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) detector = cv2.CascadeClassifier( cv2.data.haarcascades + 'haarcascade_frontalface_default.xml' ) while True: ret, frame = cap.read() if not ret: break gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = detector.detectMultiScale( gray, scaleFactor=1.1, minNeighbors=5, minSize=(80, 80) ) for (x, y, w, h) in faces: face_img = frame[y:y+h, x:x+w] recognize_face(face_img) # 送入识别模块 cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2)scaleFactor=1.1表示每次缩放检测窗口的倍率,数值越接近 1 检测越精细但是越慢;minNeighbors=5是最小邻域框数,数值调大可以减少误检但可能漏掉侧脸;minSize设成 80 以上可以过滤掉远处小人脸,在小区单元门口这种固定机位的场景,可以把 minSize 设得更大,比如 120,让系统只关注靠近摄像头的人脸。
3.2 特征比对:阈值方向决定了误放行和拒真率
特征比对是整个系统里最需要调参的地方。这里以 Dlib 的face_recognition库为例,因为它离线可用、社区资料多,毕设项目最常用:
import face_recognition import numpy as np # 加载现场抓拍图并编码 capture_img = face_recognition.load_image_file("capture.jpg") encodings = face_recognition.face_encodings(capture_img) if not encodings: raise RuntimeError("NO_FACE") capture_encoding = encodings[0] # 从 MySQL 读取已注册业主的特征向量 registered_vectors, registered_ids = load_all_features_from_db() # 计算当前人脸与所有底库之间的欧氏距离 distances = face_recognition.face_distance(registered_vectors, capture_encoding) # 取距离最小的作为候选 min_idx = int(np.argmin(distances)) min_distance = float(distances[min_idx]) # 距离越小越相似,0.45 是常见阈值 matched_user_id = registered_ids[min_idx] if min_distance < 0.45 else None这里的核心参数是 0.45。face_recognition 库的官方建议里,0.6 是“较宽松”的分界,适合允许家庭内部成员共享;0.4 以下适合门禁这类要求高安全的场景。但实际阈值不能照抄,因为底库照片质量、摄像头分辨率、光照角度都会直接影响距离分布。正确做法是先用 20 张已注册业主的日常照片跑一遍,统计“本人比对距离”的分布,再拿 50 张陌生面孔跑一次,统计“陌生人距离”的分布,两组分布重叠区域的中值就是阈值。常见情况下,本人距离在 0.25 到 0.40 之间,陌生人距离在 0.50 以上,取 0.42 左右可以得到较低的误放行率。
3.3 门禁控制与记录回写:放行不能只靠一个布尔值
识别通过之后,系统要做的不是单纯返回 true,而是联动 GPIO 控制继电器开锁,同时把流水写库。用树莓派或者 SCM 板卡控制继电器时,通常通过串口或 HTTP 调用设备端接口:
public AccessResult verifyAndOpenDoor(BufferedImage captureImage, String deviceNo) { // 1. 人脸检测 + 特征提取 FaceInfo faceInfo = faceEngine.detectAndExtract(captureImage); if (faceInfo == null) { return AccessResult.reject("NO_FACE_DETECTED"); } // 2. 与内存中的业主特征库做 1:N 比对 MatchResult match = faceEngine.search(faceInfo.getFeature(), threshold); // 3. 判断业主状态是否正常 if (match.getUserId() == null) { accessLogService.save(deviceNo, null, null, match.getDistance(), 0); return AccessResult.reject("NO_MATCH"); } FaceUser user = userMapper.selectById(match.getUserId()); if (user.getStatus() != 1) { accessLogService.save(deviceNo, user.getId(), null, match.getDistance(), 0); return AccessResult.reject("USER_DISABLED"); } // 4. 调用设备端开门接口,超时 2 秒 boolean opened = doorDeviceClient.openDoor(deviceNo, 3000); accessLogService.save(deviceNo, user.getId(), captureImagePath, match.getDistance(), 1); return AccessResult.allow(user.getId()); }写日志时要注意,access_log表的 insert 不能放在开门动作之前。如果先写库再开门,门开了但日志丢失的情况会出现;如果先开门再写库,日志没落盘就以为识别失败,又会造成重复放行。更稳健的做法是:把日志保存和开门动作放在一个事务边界内,开门调用设置为短超时,一旦超时立即回滚日志并返回“识别成功但设备无响应”,让维护人员去检查门禁机网络。
4. 本地部署与联调:JDK/Node/MySQL 环境、初始化 SQL 与常见报错
毕设工程拿到手之后,部署环节卡住的人比写代码卡住的还多。这一类前后端分离项目,本地跑通的标准动作是:先确认 Java、Maven、Node、MySQL 版本兼容,再导入 SQL 初始化数据库,然后按顺序启动后端和前端。很多人习惯先npm run serve再启动后端,前端页面倒是出来了,一登录就报网络错误,因为后端还没起来或者数据库连接池报错。
4.1 环境版本搭配和安装注意点
给出一套稳妥的组合:JDK 1.8 配 Spring Boot 2.x,Node 14 以上配 Vue 2 或 Vue 3,MySQL 5.7 或 8.0。JDK 17 也能跑大多数 Spring Boot 2.7 项目,但需要额外确认 pom.xml 里有没有引入 javax 与 jakarta 的兼容问题。MySQL 8.0 默认认证插件是 caching_sha2_password,老版本的数据库驱动可能连不上,解决办法是把 pom 里的 mysql-connector-java 升级到 8.0 以上,或者在建用户时指定IDENTIFIED WITH mysql_native_password BY '123456'。
4.2 初始化数据库与修改后端配置
打开项目里自带的 SQL 文件导入后,还要检查它的字符集。如果 SQL 文件是用 navicat 导出的,很可能在开头带着SET NAMES utf8mb4,这种情况下直接用命令行导入更稳妥:
mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS access_system DEFAULT CHARSET utf8mb4;" mysql -u root -p access_system < src/main/resources/access_system.sql导入之后,打开application.yml修改数据源和识别参数。这里是一份典型配置文件,关键项都用注释标出来了:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/access_system?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true app: face: # Dlib 欧氏距离阈值,越小越严格 threshold: 0.42 # 底库照片本地存放路径 face-image-path: D:/data/access/faces/ capture-image-path: D:/data/access/captures/serverTimezone=Asia/Shanghai必须显式设置,否则高版本的 MySQL 驱动在从 8 位时区取时间时会直接抛异常。map-underscore-to-camel-case开启后,unit_no字段能自动映射到unitNo属性,这个配置在 MyBatis-Plus 里默认是开的,但在老项目里经常被关掉,导致查出来的字段全是 null。
前端项目一般在web或vue-front目录下,找到.env文件或者vue.config.js里的 devServer 代理配置:
module.exports = { devServer: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, pathRewrite: { '^/api': '' } } } } };这套代理的含义是:当前端页面请求/api/login时,devServer 把它转发到后端的/login,同时把原始请求头里的 Host 改成localhost:8080。如果不配changeOrigin: true,后端拿到的 Origin 仍然是 8081 端口,一旦后端开启了 CORS 限制,就会报跨域错误。
4.3 启动命令与三个高发报错排查
后端如果是 Maven 工程,启动命令如下,前后端不要共用终端窗口:
cd backend mvn spring-boot:run前端执行:
cd web npm config set registry https://registry.npmmirror.com npm install npm run serve运行期排错,三个报错出现频率最高。第一个是Access denied for user 'root'@'localhost',需要检查 MySQL 密码是否真的和 yml 里一致;第二个是Cannot connect to MySQL server on localhost:3306,一般是 MySQL 服务没启动,Windows 下执行net start mysql或者去服务管理器找 MySQL 服务;第三个是npm ERR! code ELIFECYCLE,通常不是代码问题而是端口被占用,把 vue.config.js 里的 port 改成 8082 再跑一次即可。
提示:如果后台返回了 404 而前端接口地址看起来没问题,先看 devServer 代理路径和后端
@RequestMapping前缀是否对应。很多项目把后端接口统一加上了/api前缀,代理又做了一次路径重写,两边处理不好就会出现“浏览器能看到请求,后端接口就是不匹配”的局面。
5. 答辩与性能调优:识别准确率、并发读写和安全边界怎么讲清楚
如果你的目标是毕业设计答辩,这部分会是评委最关心的落点:系统到底怎么验证“人脸识别有效”,阈值为什么是这个数字,十万人以内性能会不会崩。把这三个问题准备清楚,比堆功能介绍有用得多。
5.1 用一份实测数据给出准确率结论
不要只截图说“能识别成功”,而是准备三组对照数据:已注册业主本人识别 50 次,统计通过率;陌生人冒用识别 50 次,统计拒绝率;弱光环境(晚上门口灯光不足)识别 20 次,统计失败率。把距离分布做成表格,例如阈值 0.42 时,本人距离均值 0.31、最大 0.40,陌生人距离均值 0.62、最小 0.49,这样阈值设置的依据一目了然。调整阈值时观察两个指标:误放行率(FAR)和拒真率(FRR),阈值调大 0.45 时 FAR 可能从 0 涨到 4%,阈值调小到 0.38 时 FRR 可能从 2% 涨到 10%。答辩时讲这份数据,比任何技术名词都更能说明你对系统的理解。
5.2 并发读写下的人脸库加载策略
业主规模增大后,识别速度的瓶颈不在比对本身,而在特征加载。如果每次识别都查 MySQL、反序列化 blob、再去做 1:N 遍历,单次响应会超过 500ms。常见做法是在内存里维护一个全局特征库,每次业主注册或修改后,增量更新这个特征库;门禁识别时直接查内存,不触碰 MySQL。Spring Boot 里可以用ConcurrentHashMap<Long, byte[]>模拟这个特征缓存,注意更新时要做原子替换,避免识别线程读到半写状态的特征数据。识别服务要设置独立的线程池,避免占用 Tomcat 的请求线程;否则门禁机上报的抓拍请求一多,管理后台登录接口就会跟着变慢。
5.3 活体检测与照片攻击的边界
毕设项目里最容易被打脸的场景是“用一张照片刷脸”。OpenCV 级联检测 + Dlib 特征比对在这里是防不住的,因为照片本身包含完整的人脸特征。要补这个漏洞,有三挡做法:最轻量的方案是要求用户眨眼或左右转头做动作配合,帧率在 15fps 时连续采集 5 帧画面做动态比对;第二挡是加红外摄像头或深度传感器,检测活体深度信息,但这需要硬件支持,门禁机选型时就要预留;第三挡是检查图像熵值和反光异常,用 DCT 变换检测屏幕摩尔纹,属于纯算法方案,适合纯软件改造。答辩时至少要把第一挡实现出来,并用一次“拿照片尝试开门”的测试证明系统会拒绝,这是人脸识别门禁比人脸打卡更被关注的功能差异点。
系统交付时建议保留一批演示数据:20 个已注册业主底库照片、对应的特征向量缓存、3 天的通行日志,以及一条访客预约记录。这样无论演示还是验收,都能在五分钟内完整展示“登记录入、现场识别、放行记录、访客到期停用”的闭环路径,而不是临时去注册一个人脸再等摄像头抓拍。
本文还有配套的精品资源,点击获取