简介:一套基于Python的人脸识别智能化小区门禁管理系统源码,面向物联网、人工智能及安防系统方向的开发者与学生,适用于小区、园区等场景的出入身份验证与门禁自动化管理。资源共101个文件、压缩包约12.17MB,主要包含py源码、pyc编译文件、qt编写的ui界面、sql数据库脚本、onnx人脸模型、jpg/png示例图片、txt及md说明文档等,目录划分清晰,兼顾前端采集、后端识别与数据存储。系统涵盖数据采集、人脸识别、MySQL数据管理、用户界面和访问控制五大模块,代码结构与业务逻辑完整,便于二次开发或作为毕业设计参考。目前已有36人学习,资料开放仅用于学习交流,使用时应遵守开源协议与隐私保护要求,不得用于商业用途。整体方案将OpenCV/TensorFlow人脸特征比对与MySQL记录管理结合,能够直观呈现智能化门禁的实现路径。
1. 门禁系统翻车点不在算法:Python人脸识别项目的价值怎么判断
小区门口装了人脸识别门禁,业主群反而更闹腾了:张三刷脸,屏幕却弹出李四的名字;住户换个发型就被拒之门外。拉开差距的往往不是“认不认得出人”这一步,而是从人像采集、特征比对到MySQL写入这一整条数据流水线。这套基于Python和MySQL的人脸识别智能化小区门禁管理系统源码,正好把这条流水线完整摆在你面前,覆盖人像采集、人脸检测与特征比对、用户信息与出入记录管理、门禁放行控制。适合三类人:做毕业设计或课程设计、需要快速跑通一套可演示门禁系统的学生;想搞清楚人脸识别和数据库怎么协作的Python初学者;以及准备做方案选型验证、想先复现一套原型判断可行性的工程师。
2. 先看懂数据流再动手:OpenCV的活和MySQL的活怎么分
门禁系统的架构从逻辑上说是前后端分离的:前端摄像头抓人脸,后端负责解码、比对、落库。无论界面是PyQt还是控制台简化版,这条链路都不会变。我一般把识别链路拆成四步:人脸检测、人脸对齐、特征编码、距离比对。每一步都有对应的Python库,选型时别贪多,够用就行。
2.1 人脸识别主干:检测、对齐、编码、比对的职责拆解
人脸检测解决的是“脸在哪里”的问题,通常用OpenCV的Haar级联或者dlib的HOG检测器;检测到人脸之后,下一步是对齐,也就是把歪头、侧脸、高低角度统一到同一坐标系,这一步直接影响后面的特征编码质量;编码做的事情是把人脸压缩成一个定长向量;最后拿这个向量和库里的所有注册向量算距离,距离小于阈值就放行。
这套源码里最核心的比对逻辑,跑不出下面这段代码的逻辑:
import face_recognition # 读取注册照片,要求是单人正面照 image = face_recognition.load_image_file("register/zhangsan.jpg") faces = face_recognition.face_encodings(image) if len(faces) == 1: face_encoding = faces[0] print("特征维度:", len(face_encoding)) # 128 维 else: print("检测到人脸数量:", len(faces))逻辑说明:load_image_file返回的是RGB格式的图像数组,face_encodings内部已经串联了检测、对齐和编码三步,不需要自己再调模型。faces[0]取出的是第一张人脸的128维特征向量,这一步就是后面MySQL里要存的关键数据。
参数说明:len(faces)一定要校验。注册照片如果是一张多人合照,faces里会有多个特征向量,直接取[0]会把别人的脸注册到你名下,这个坑在后面的避坑章里会专门展开。
比对阶段的核心逻辑类似这样:
import numpy as np import face_recognition # known_encodings 是从数据库读出的所有注册用户特征向量 # frame_encodings 是当前摄像头抓拍到的人脸特征 distances = face_recognition.face_distance(known_encodings, frame_encodings[0]) print("与最近注册脸的欧氏距离:", round(float(np.min(distances)), 3))逻辑说明:face_distance计算的是欧氏距离,不是相似度。数值越小表示越像,默认阈值0.6,小于0.6判定为同一人。这个反直觉点特别容易踩坑——很多人把阈值调低以为“更安全”,实际上是把通过标准卡死了。
2.2 MySQL数据模型:用户表与出入记录表怎么设计
人脸特征往哪里存,是这类系统第一个要做的设计决策。常见的做法是两种:一种是不存特征,每次启动把所有用户照片重新跑一遍编码,注册人数少的时候无所谓,人一多启动时间成倍上涨;另一种是把128维向量序列化后存进MySQL的BLOB字段,启动时一次性读出。门禁系统追求快速启动,我见过的大多数可运行实现都走第二种方案。
核心表结构通常围绕两类数据:注册用户信息和出入记录。下面是一个可以直接落库的建表方案:
CREATE DATABASE door_system DEFAULT CHARACTER SET utf8mb4; USE door_system; CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(64) NOT NULL, photo_path VARCHAR(255) NOT NULL, face_feature BLOB NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE access_log ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NULL, result TINYINT NOT NULL COMMENT '1=放行 0=拒绝', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_time (created_at), CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;逻辑说明:face_feature字段存储的是float32的128维向量序列化后的字节串,128个float32约512字节,一千个注册用户也只有半MB左右,内存占用完全可接受。access_log里的user_id用外键关联到users表,保证日志记录不产生“孤儿数据”。
参数说明:两个关键点。第一,DEFAULT CHARACTER SET utf8mb4必须写在建库语句里,否则遇到中文姓名就是乱码;第二,access_log的idx_time索引是给“查询某个时间段出入记录”准备的,门禁管理后台最常见的操作就是看“今天谁几点进的”,这个索引能省不少查询时间。
2.3 目录结构与样本文件对照:哪些是代码,哪些是“坑源”
拿到压缩包之后,先别急着运行,对着文件清单过一遍,能省掉后面一大半排查时间。这套源码包里的文件可以分成几类:
| 文件 | 名称特征 | 在系统中的可能角色 |
|---|---|---|
| README.md | 项目说明 | 环境依赖、运行步骤、作者说明 |
| 需求(5).docx | 需求文档 | 系统功能与模块设计说明 |
| 002.jpg / 005.jpg / 007.jpg / 008.jpg | 数字编号 | 历史测试图片或采集样本 |
| zs123.jpg / lisi1234.jpg | 账号拼音+编号 | 用户注册照片 |
| 寮犱笁.jpg | 乱码文件名 | GBK编码的“张三.jpg”被错误解码后的产物 |
这里最值得注意的就是“寮犱笁.jpg”这个文件。它的本质是“张三.jpg”在GBK编码下写入,又被UTF-8编码的环境读取,产生了乱码文件名。这说明原作者的工作环境是GBK编码的Windows系统,而大多数现代Python环境和MySQL默认是UTF-8。拿到源码后如果不先处理这个文件名的编码问题,后续导入数据库时中文姓名会一路乱到底。
文件功能看清之后,复现顺序就清晰了:先建库建表,再整理注册照片目录并规范文件名,然后跑特征编码脚本把BLOB字段填上,最后启动主循环接摄像头。下面一章按这个顺序逐段过。
3. 从空MySQL到门禁跑通:环境、建库与主循环逐段复现
复现这套源码最怕的不是逻辑看不懂,而是环境装到一半卡住、数据库连接串写错、摄像头调不起来。这一章按我实际复现的顺序来,每一步都给出能直接跑的命令和代码,同时说清楚每条参数背后的理由。
3.1 环境安装:Python版本、依赖包与两个容易失败的包
拿到源码先别急着执行pip install -r requirements.txt,因为这个包不一定带了完整的依赖清单。更稳妥的做法是先建虚拟环境,再按顺序装依赖。人脸识别相关的库对版本非常敏感,特别是face_recognition会连带安装dlib,dlib需要本地有完整的C++编译工具链,没有工具链时源码编译几乎必失败。
python -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate pip install --upgrade pip pip install opencv-python numpy pymysql pip install face-recognition逻辑说明:先装OpenCV、numpy和pymysql,最后再装face-recognition,这样如果dlib编译失败,前面的基础依赖已经就位,排查范围更小。pymysql是纯Python实现的MySQL客户端,比mysql-connector-python轻量,对这种单机演示系统足够。
参数说明:Python版本建议选3.8到3.10之间,不要一上来就追最新的3.12或3.13。dlib这类带C++扩展的库对Python版本很敏感,新版本刚发布时通常还没有对应的预编译wheel。如果编译dlib实在折腾不动,常见的替代做法是conda install -c conda-forge dlib,conda会直接提供编译好的二进制包,省掉本地工具链的问题。
3.2 初始化数据库:建库建表、插入用户与特征更新
数据库建好后,第一步是插入用户基本记录。这里有一个习惯要养成:照片文件名和用户姓名的对应关系,从一开始就要定好。最简单的约定是“文件名即姓名”,注册照片放进register/目录,文件名就叫张三.jpg、lisi1234.jpg。
USE door_system; INSERT INTO users (name, photo_path) VALUES ('张三', 'register/张三.jpg'), ('李四', 'register/lisi1234.jpg');逻辑说明:这条SQL只是建立了用户基本信息和照片路径的映射,此时face_feature字段还是空的。下一步要做的是读取每张照片,计算特征向量并写回BLOB字段。
import os import pymysql import face_recognition conn = pymysql.connect( host="127.0.0.1", user="root", password="你的密码", database="door_system", charset="utf8mb4" ) cursor = conn.cursor() cursor.execute("SELECT id, photo_path FROM users") for user_id, photo_path in cursor.fetchall(): if not os.path.exists(photo_path): print(f"文件不存在: {photo_path}") continue image = face_recognition.load_image_file(photo_path) faces = face_recognition.face_encodings(image) if len(faces) != 1: print(f"用户 {user_id} 的照片检测到 {len(faces)} 张脸,跳过") continue cursor.execute( "UPDATE users SET face_feature=%s WHERE id=%s", (faces[0].tobytes(), user_id) ) conn.commit()逻辑说明:这段脚本是“注册阶段”的核心。faces[0].tobytes()把128维的float32向量序列化成字节串,方便存进BLOB字段。len(faces) != 1的检查是必要的防线——如果照片里有路人甲的脸,程序会直接跳过并提示,而不是把错误的特征写入。
参数说明:pymysql连接串里的charset="utf8mb4"不是可选项,少了它中文姓名在写入时可能直接乱码。执行完脚本后,可以用下面这条SQL验证特征是否写入成功:
SELECT id, name, LENGTH(face_feature) FROM users;如果LENGTH(face_feature)不是512或显示为NULL,说明那行记录没有成功写入特征,回头检查照片路径和人脸数量。
3.3 主循环:视频流读取、识别比对与放行记录
门禁系统的主体是一个while True循环:读摄像头一帧,转成RGB,找人脸,算特征,和已载入的注册特征算距离,距离小于阈值就写一条放行记录。下面是去掉界面封装后的核心骨架:
import cv2 import numpy as np import face_recognition THRESHOLD = 0.6 # known_encodings 和 known_names # 启动时从 MySQL 读出所有 face_feature 并还原 # known_encodings.append(np.frombuffer(row[0], 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) while True: ok, frame = cap.read() if not ok: continue rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) face_locations = face_recognition.face_locations(rgb, model="hog") face_encodings = face_recognition.face_encodings(rgb, face_locations) for face_encoding in face_encodings: distances = face_recognition.face_distance(known_encodings, face_encoding) idx = np.argmin(distances) if distances[idx] < THRESHOLD: # 放行:写入 access_log,result=1 print("放行:", known_names[idx]) else: # 拒绝:写入 access_log,result=0 print("拒绝: 未注册人员") cv2.imshow("door", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break逻辑说明:cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)这行不能省。OpenCV默认读出来的是BGR顺序,而face_recognition内部按RGB顺序处理,颜色通道顺序搞反了,检测率会明显下降。np.argmin(distances)找出与当前人脸距离最近的注册用户,再用THRESHOLD判断是否放行。
参数说明:face_locations的model="hog"是CPU友好型检测模式,在640x480分辨率下帧率足够;model="cnn"检测更准但CPU下会明显变慢,入门演示用hog合适。cv2.VideoCapture(0, cv2.CAP_DSHOW)中的CAP_DSHOW是Windows下使用DirectShow后端,可以避免部分USB摄像头打不开或延迟的问题。
3.4 验证:识别结果有没有真正写进MySQL
门禁系统最容易忽略的是“数据落地”环节。识别成功并不代表系统完整,你还要去access_log表里确认记录真的写进去了。打开一个新终端连上MySQL,查最近十条出入记录:
SELECT u.name, l.result, l.created_at FROM access_log l LEFT JOIN users u ON l.user_id = u.id ORDER BY l.created_at DESC LIMIT 10;逻辑说明:LEFT JOIN保证即使user_id为空,日志记录也会显示出来,方便排查“拒识”事件。如果这个查询查不到任何记录,优先检查代码里有没有conn.commit()。
pymysql默认autocommit=False,执行完INSERT或UPDATE后必须手动commit(),否则数据只停留在事务里,程序一退出就丢了。这是Python操作MySQL时频率最高的翻车点,没有之一。
4. 门禁源码踩坑实录:四条最容易翻车的细节
这套源码我在本地完整跑过,也在类似的门禁项目里踩过同样的坑。下面四条不是理论推断,是实际会发生的现象、原因和解法,按“现象→原因→解决”的顺序写。
4.1 注册阶段人脸张冠李戴:照片和姓名错位配对
现象:张三站在摄像头前刷脸,屏幕显示的却是“李四,欢迎回家”,后台日志里的名字和实际刷脸人对不上。
原因:常见写法是用os.listdir()扫描注册目录,然后把扫描结果和姓名列表按位置索引直接配对。但文件系统的目录排序在不同操作系统、不同磁盘格式下并不一致,Windows下还会区分字母大小写,结果就是“第0张照片配第0个名字”的顺序假设不成立,一旦有一张照片不在预期位置,后面全部错位。
解决:不要依赖目录扫描顺序,直接用文件名作为姓名标识:
import os for item in os.listdir("register"): if not item.lower().endswith((".jpg", ".jpeg", ".png")): continue name = os.path.splitext(item)[0] # 文件名即姓名 cursor.execute( "INSERT INTO users (name, photo_path) VALUES (%s, %s)", (name, os.path.join("register", item)) )逻辑说明:os.path.splitext(item)[0]取出去扩展名后的文件名,张三.jpg就直接得到张三。如果文件命名是lisi1234.jpg这种账号格式,就单独建一张姓名映射表,但配对的基准永远是文件名或ID,不能是列表位置。
4.2 中文名字进MySQL变成乱码:一乱到底的三级链条
现象:控制台打印姓名正常,但MySQL表里“张三”变成一串“???”或者“寮犱笁”这样的乱码。顺带提一句,源码包里那个“寮犱笁.jpg”文件,就是GBK编码的“张三.jpg”被按UTF-8解码后的产物,属于同一个坑的不同表现。
原因:字符链路涉及四个环节:文件系统文件名编码、Python源码文件编码、pymysql连接字符集、MySQL库表默认字符集。文件系统是GBK,Python源码是UTF-8,数据库表是latin1,任何一级不一致,最终落库的字符串就是乱的。
解决:把连接、库、表三级全部统一成utf8mb4:
conn = pymysql.connect( host="127.0.0.1", user="root", password="你的密码", database="door_system", charset="utf8mb4" )ALTER DATABASE door_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4;注意:如果数据已经乱成“寮犱笁”这种程度,说明写入时字节就已经被破坏,靠ALTER TABLE是救不回来的。唯一的解法是把文件名重新规范为UTF-8,清空users表重新导入。这也是我在前面章节反复强调“先规范文件名再入库”的原因。
4.3 把阈值从0.6调到0.4,门禁直接“全拒”
现象:有人觉得阈值0.6太宽松,改成0.4更安全。结果测试时同一人大晴天正面刷脸也被拒绝,换上0.6又恢复正常。
原因:face_recognition.face_distance()返回的是128维向量之间的欧氏距离,数值越小代表越像。0.4意味着要求两张人脸几乎一模一样,只要换个发型、戴个眼镜、角度偏一点,距离就会冲到0.5以上。门禁场景下阈值越低不等于越安全,而是误拒率急剧上升,安全是靠特征本身的区分度和合理阈值一起配合出来的,不是靠把阈值调到极端。
解决:把阈值恢复到0.6起步,然后打印真实距离分布再微调。调试时加一行:
import numpy as np import face_recognition distances = face_recognition.face_distance(known_encodings, face_encoding) min_idx = np.argmin(distances) print("最近匹配:", known_names[min_idx], "距离:", round(float(distances[min_idx]), 3))逻辑说明:不管是本人还是陌生人,先看距离到底落在什么区间。本人正面正常光照下通常在0.35到0.55之间,不同人通常在0.7以上。如果本人的距离最大值和不同人的距离最小值之间还有空隙,阈值就取在这个空隙里。这个值是用数据跑出来的,不是拍脑袋定的。
4.4 摄像头画面卡顿、识别延迟:缓冲与抽帧没处理
现象:摄像头画面一卡一卡,人站在门口两三秒才出结果,有时候识别到的还是上一帧的人脸。
原因:cv2.VideoCapture在Windows下默认的帧缓冲较大,循环里read()读到的经常是队列里积压的旧帧;再加上每一帧都跑全图人脸检测,CPU被占满,取流和解码互相拖累。
解决:限制缓冲区长度,同时做抽帧识别:
import cv2 cap = cv2.VideoCapture(0, cv2.CAP_DSHOW) cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) frame_id = 0 while True: ok, frame = cap.read() if not ok: continue frame_id += 1 if frame_id % 3 != 0: continue # 每3帧取1帧做识别 # 识别逻辑放在这里逻辑说明:CAP_PROP_BUFFERSIZE设为1是告诉底层尽量只保留最新帧,减少读到旧帧的概率。抽帧不是偷懒,门禁是长时间值守任务,640x480分辨率下每3帧处理1帧,延迟和CPU占用都能控制在合理范围。如果一台机器要带多路摄像头,识别逻辑还应该丢到独立线程里,主线程只负责取流和显示。
5. 进阶验证:用视频回放把阈值校准到能用的数
人脸识别门禁调试最麻烦的地方在于:现场工况不可复现。今天改个阈值,明天换个人戴帽子测试,同一个参数在不同时段的表现都不一样。我的做法是用视频回放替代摄像头,把测试变成可重复的离线过程。
5.1 把摄像头源换成视频文件,让测试可重复
常见做法是先录三段视频:正常光照通行、逆光或夜间通行、戴帽或侧脸通行。然后把主循环里摄像头编号0换成视频文件路径,让同一段视频反复跑,对比每次改动前后效果:
import cv2 # 原来是 VideoCapture(0),调试时换成视频文件 cap = cv2.VideoCapture("door_test.mp4") cap.set(cv2.CAP_PROP_POS_FRAMES, 0) frame_id = 0 while True: ok, frame = cap.read() if not ok: break # 视频读到结尾自然结束 frame_id += 1 if frame_id % 3 != 0: continue # 这里复用主循环的识别逻辑逻辑说明:VideoCapture对视频文件和摄像头的接口完全一致,主循环代码几乎不用改。区别在于视频读到结尾时read()返回False,循环会正常退出,不会像摄像头那样无限阻塞。CAP_PROP_POS_FRAMES设为0是让回放从头开始,方便重复跑同一段素材。
参数说明:测试视频建议保证至少包含三个人的脸部画面,并且覆盖正脸、斜脸、光照变化三种情况。素材太单一,校准出来的阈值没有参考价值。
5.2 记录每个身份的比对距离区间,再定阈值
跑完回放之后,把每次匹配的距离打出来,统计同一人多次进出的距离区间,以及不同人被错误匹配时最近的距离区间。下面是我在自己测试里得到的一组示意数据:
| 测试场景 | 与本人最近注册脸的距离 | 与最近不同人的距离 |
|---|---|---|
| 正面正常光照 | 0.42 ~ 0.51 | 0.78 ~ 0.91 |
| 戴帽或侧脸 | 0.55 ~ 0.68 | 0.88 ~ 0.94 |
| 逆光或夜间 | 0.61 ~ 0.76 | 0.80 ~ 0.99 |
真实数据要跑你自己的视频才能拿到,但这张表能说明一个规律:本人距离分布和不同人距离分布之间的空隙,就是阈值该待的地方。我的测试里,正面和侧脸场景本人最大距离约0.68,不同人最小距离约0.78,阈值取0.6到0.7之间都可以,0.6是face_recognition的默认值,正好落在安全区间里。如果两个分布之间没有空隙、甚至发生了交叠,说明单靠一个阈值解决不了,正确做法是给用户多注册几个角度和光照条件下的照片,而不是继续调阈值。
从那以后,我每次拿到新的门禁或考勤源码,第一件事不是打开IDE改功能,而是先录一段30秒的视频,把识别链路在离线状态完整跑一遍,统计距离分布,把阈值、跳帧、检测模型这些参数一次定好,再让程序接摄像头。识别阈值这种参数,跑出来的数才敢用,临时改的到现场就是玄学。希望帮到你。
本文还有配套的精品资源,点击获取