简介:这是一份基于深度学习的人脸识别考勤系统毕业设计项目,面向计算机相关专业正在准备毕设的学生,以及需要项目实战练习的学习者。系统可支撑260人的考勤数据,包含完整可运行的Python源码、演示视频、使用手册,并经过导师指导与严格调试,适合直接作为毕业设计、课程设计或期末大作业使用。压缩包共26个文件,以Python脚本、图像资源、字体文件、说明文档及配置文件为主,覆盖界面展示、用户信息管理、考勤状态记录等模块,整体大小约239.41MB。核心代码基于face_recognition库实现人脸识别,配有UI界面与多张界面截图,便于理解前后端交互逻辑。目前已有158人学习下载,对于希望快速搭建人脸识别考勤系统、参考高质量毕设写法或进行二次开发的读者而言,这份资料具备较强的参考价值。
1. 基于深度学习的人脸识别考勤系统:为什么它稳、它值不值得做
基于深度学习的人脸识别考勤系统,大概是本科毕业设计里投入产出比最高的一类选题:技术栈只有 Python,依赖库成熟,演示效果又足够直观。它解决的是教室或办公室最具体的考勤登记问题——刷脸即打卡,后台自动生成记录,不需要排队按指纹或人工点名。适合有 Python 基础、想在毕业设计里用上深度学习的本科生,也适合想在团队里快速搭一套考勤原型的工程师。不过做完一遍你会有个明显感受:这个项目的难点不在人脸识别算法本身,而在怎么把识别能力稳定嵌进考勤流程里。光线一变就漏打卡、相似脸误打卡、阈值怎么设,才是真正决定项目质量的地方。拿到源码包后,先跑通演示视频对应的主线流程,再谈优化,这是最省时间的路径。
2. 从检测到比对:人脸识别考勤的技术链路与模型选型
2.1 传统方法为什么在考勤场景不稳:LBPH 与特征脸的边界
很多教材会把 OpenCV 自带的 LBPH 和 Eigenfaces 当作人脸识别的入门案例。这两种方法的思路是把人脸图像投影到低维空间,用像素统计特征做分类。在受控环境下——正对镜头、光线均匀、背景单一——它们确实能跑通,人脸识别门禁系统的早期原型也大量依赖这类方法。但考勤现场恰恰是最不受控的:上午十点的窗边阳光、傍晚的顶灯逆光、侧脸说话时拍到的半张脸、偶尔换上的眼镜,任何一个变化都可能让 LBPH 的识别率明显下滑。
传统方法的核心缺陷在于特征表达能力不足。LBPH 描述的是纹理局部二值模式,Eigenfaces 描述的是全局灰度分布,它们都没有真正学到“这张脸是谁”的语义信息。换句话讲,如果一个人稍微转个头,像素分布就变了,分类器跟着失效。考勤系统对误识和漏识都很敏感,漏一次打卡就要手动补记录,用传统方法做出来的演示效果经不起现场检验。
2.2 深度学习链路:检测、对齐、特征提取与比对
深度学习方法解决的是同一件事:让模型自动从人脸图像里学到多尺度的语义特征。常见的技术链路拆成四步。第一步是人脸检测,从整帧画面里找到人脸框;第二步是特征提取,把检测到的人脸区域喂给 CNN 骨干网络,输出一个固定长度的特征向量;第三步是归一化,对向量做 L2 归一化消除亮度影响;第四步是比对,计算当前向量和注册库中向量的欧氏距离或余弦相似度,距离小于阈值就判定为同一人。
特征提取是整个链路的核心。以 dlib 的 ResNet 模型为例,输入一张 150x150 的对齐后人脸,CNN 经过若干卷积层和残差块后压缩成 128 维浮点向量。同一个人的不同照片,在这套特征空间里会聚成一团;不同人的照片则天然分散。距离越近,相似度越高。这个“度量学习”的思路让系统不需要存储原始照片,只存特征向量,既省空间又便于后续扩展人员库。
2.3 模型选型:face_recognition 与 ArcFace 各自的适用边界
毕设和中小型考勤项目里,最稳妥的起点是 face_recognition 库。它封装了 dlib 的预训练 ResNet 模型,输出 128 维特征,CPU 上即可运行,不需要你从头训练任何网络。而如果追求更高的精度,可以换成 InsightFace 里的 ArcFace 系列,输出 512 维特征,在困难样本上的表现更强,但对硬件有要求。下面这张表列出常见选型:
| 方案 | 特征维度 | 是否需要自训练 | CPU 运行 | 适用场景 |
|---|---|---|---|---|
| OpenCV LBPH | 取决于参数 | 需要训练 | 流畅 | 受控环境演示,精度低 |
| face_recognition | 128 | 否,用预训练权重 | 流畅 | 毕设、中小型考勤系统 |
| InsightFace ArcFace | 512 | 否,用预训练权重 | 建议 GPU | 高精度考勤、门禁机 |
选型原则是:能跑通、能讲清、能演示。face_recognition 之所以适合毕设,是因为它有预设的检测器、对齐逻辑和特征提取器,代码量少,答辩时你能解释清楚每一层在做什么,而不是打开一个黑匣子。ArcFace 适合作为进阶对比实验出现,如果你有 GPU 或处理服务器上的离线识别任务,用它做一轮对照能明显提升论文说服力。无论选哪个,核心参数都是“距离阈值”,这个点上很容易翻车,后面专门讲。
3. 人脸注册与数据准备:用 OpenCV 建出可用的训练集
3.1 用摄像头批量抓帧:Haar Cascade 采集脚本
考勤系统的第一步是给每个参与者建立人脸档案。这里说的“训练集”并不是让你从零训练 CNN,而是采集注册样本,供预训练模型提取特征后存入人脸库。采集工具直接用 OpenCV 的 Haar Cascade 检测器就够了,它不需要 GPU,实时性也好。先安装环境:
pip install opencv-python -i https://pypi.tuna.tsinghua.edu.cn/simple用清华镜像源能省去很多网络等待时间,这是 Python 项目环境里很常见的做法。采集脚本如下:
import cv2 import os save_dir = "faces/alice" os.makedirs(save_dir, exist_ok=True) cap = cv2.VideoCapture(0) cascade = cv2.CascadeClassifier( cv2.data.haarcascades + "haarcascade_frontalface_default.xml" ) count = 0 while count < 80: ok, frame = cap.read() if not ok: break gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = cascade.detectMultiScale( gray, scaleFactor=1.1, minNeighbors=5, minSize=(100, 100) ) for (x, y, w, h) in faces: face = frame[y:y+h, x:x+w] face = cv2.resize(face, (160, 160)) cv2.imwrite(f"{save_dir}/{count:03d}.jpg", face) cv2.imwrite(f"{save_dir}/{count:03d}_flip.jpg", cv2.flip(face, 1)) count += 2 cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2) cv2.imshow("collect", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()逻辑说明:脚本循环读取摄像头帧,先用灰度图做检测,因为 Haar 特征在灰度空间上计算,能减少颜色干扰;检测到人脸后裁剪、缩放到 160x160 并落盘。代码里的count += 2是因为我同时保存了原图和水平翻转图,等于在注册阶段就做了一次基础增强。参数说明:scaleFactor=1.1控制每层金字塔的缩放比例,数值越小检测越精细但耗时越长;minNeighbors=5要求至少 5 个邻近矩形框才确认为人脸,用来压低误检率。采集时请主动变换角度和远近,左右转头、稍微低头抬头都要拍,别坐在一个姿势下连拍 80 张,否则后面识别很容易翻车。
3.2 图像预处理与数据增强:解决小样本过拟合
用预训练模型提取特征时,理论上不需要大量训练数据,但注册样本太单一时,特征向量的覆盖范围会偏窄。举个例子:你只采集了正脸照,识别时用户稍微偏头 20 度,提取出的 128 维特征可能就跑到阈值之外了。数据增强在这里不是为了让网络多学,而是为了让特征提取结果更稳定。常见做法是对注册样本做亮度扰动和平移扰动:
import cv2 import numpy as np def augment(img): # 随机亮度调整,模拟上午和下午的光照差异 alpha = 0.8 + 0.4 * np.random.rand() img = cv2.convertScaleAbs(img, alpha=alpha, beta=0) # 随机水平平移 5% 像素,模拟人没有完全正对摄像头 rows, cols = img.shape[:2] dx = int(0.05 * cols * np.random.uniform(-1, 1)) M = np.float32([[1, 0, dx], [0, 1, 0]]) img = cv2.warpAffine(img, M, (cols, rows)) return img这段代码放在注册脚本里,对裁剪出的人脸图先增强再保存。convertScaleAbs的alpha是增益系数,0.8 到 1.2 之间随机变化,相当于把照片调暗或调亮 20%。平移矩阵M让图像左右偏移最多 5% 的像素宽度,模拟人脸没有完全居中的情况。处理后,同一个人的注册特征会覆盖更广的光照和位置范围,实时识别时就不容易因为一点环境变化就掉线。
3.3 训练集与评估集划分:留出调阈值的数据
很多毕设源码包里的目录结构是faces/已知人员/每人一个文件夹,这种做法本身没问题,但要注意别把所有采集到的图片一股脑全拿去注册。我一般会按 7:3 划分:七成进注册库,三成放进独立的faces/test/目录,用来做后续的阈值验证。这样做的价值在于,threshold不是拍脑袋定的,而是通过评估集算出来的。
faces/ ├── known/ │ ├── alice/ # 注册库,约60张 │ ├── bob/ │ └── carol/ └── test/ ├── alice/ # 评估集,约20张 ├── bob/ └── carol/目录组织好后,注册脚本逐个读取known下每个人的照片,提取特征后保存成encodings.pkl,方便识别阶段直接加载。测试集里的照片严格独立,注册时不可见,这样才能反映系统在真实场景下的表现。
4. 识别与考勤主流程:特征比对、阈值判断和打卡记录
4.1 实时识别循环:人脸定位、128 维编码和阈值比对
识别阶段的核心代码是把人脸检测、特征提取和比对串成一个实时循环。在 Python 里,face_recognition 库把这三步封装得很好,先加载注册库,再逐帧处理摄像头画面:
import face_recognition import cv2 import pickle import datetime import sqlite3 # 加载注册阶段保存的特征向量 with open("encodings.pkl", "rb") as f: known_encodings, known_names = pickle.load(f) # 初始化数据库 conn = sqlite3.connect("attendance.db") conn.execute(""" CREATE TABLE IF NOT EXISTS records ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, time TEXT NOT NULL, date TEXT NOT NULL, status TEXT NOT NULL ) """) # 跳帧计数器 frame_skip = 0 cap = cv2.VideoCapture(0) while True: ok, frame = cap.read() if not ok: break frame_skip += 1 if frame_skip % 3 != 0: continue # face_recognition 内部使用 RGB 顺序 rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # 检测人脸位置;model="hog" 适合 CPU,cnn 需要 CUDA boxes = face_recognition.face_locations(rgb, model="hog") # 提取当前画面中所有人脸的 128 维特征 encodings = face_recognition.face_encodings(rgb, boxes) for box, encoding in zip(boxes, encodings): # 计算与注册库中每个人的距离 distances = face_recognition.face_distance(known_encodings, encoding) min_idx = int(distances.argmin()) # 考勤场景建议用 0.45 而不是默认 0.6,宁缺毋滥 if distances[min_idx] < 0.45: name = known_names[min_idx] else: name = "unknown" top, right, bottom, left = box cv2.rectangle(frame, (left, top), (right, bottom), (0, 255, 0), 2) cv2.putText(frame, name, (left, top - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imshow("attendance", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()逻辑说明:face_recognition.face_locations返回的是人脸的边界框坐标,face_encodings对这些框逐一提取特征。face_distance返回一个数组,每个元素对应当前人脸和注册库中某个人的欧氏距离,我用argmin()取出距离最小的那个人的索引。如果最小距离小于 0.45,就判定为这个人;否则标记为 unknown。参数说明:model="hog"在 CPU 上每帧耗时约 50 到 100 毫秒,cnn精度更高但需要 CUDA 环境;0.45这个阈值不是固定的,应该根据评估集调整,后面第 5 章会专门展开。
4.2 打卡逻辑:SQLite 去重与迟到状态判定
识别出是谁之后,下一步才是考勤业务逻辑:同一人同一天只允许打一次卡,超过固定时间记为迟到。这段逻辑与识别解耦,放进独立的函数里更好维护:
def punch(name, work_start_minute=9 * 60 + 30): now = datetime.datetime.now() date_str = now.date().isoformat() time_str = now.strftime("%H:%M:%S") # 去重:同一天同一人只记录一次 exists = conn.execute( "SELECT 1 FROM records WHERE name = ? AND date = ?", (name, date_str) ).fetchone() if exists is not None: return "ALREADY" current_minute = now.hour * 60 + now.minute status = "NORMAL" if current_minute <= work_start_minute else "LATE" conn.execute( "INSERT INTO records (name, time, date, status) VALUES (?, ?, ?, ?)", (name, time_str, date_str, status) ) conn.commit() return status逻辑说明:先查records表里有没有同一天同名记录,SELECT 1的作用是只要存在即返回一行,比SELECT *省内存;查到就返回ALREADY,主循环里不再重复插入。迟到判定用“当前时间距零点分钟数”和 9:30 对应的 570 比较,避免字符串比较的格式坑。work_start_minute参数可以外部传入,比如上午第一节课开始时间,方便后续改成不同的考勤规则。
4.3 结果可视化:在画面上叠加姓名和打卡状态
实时识别出姓名后,画框和文字能让演示效果提升一个档次。上面的代码里已经用了cv2.rectangle和cv2.putText,但有几个细节要注意。putText默认字体不支持中文,直接写中文会显示成问号,我一般用姓名的拼音或英文缩写代替;如果一定要显示中文,可以用 PIL 先渲染成图片再贴到帧上,代码会多出十几行,对毕设来说收益一般。另外,文字位置放在人脸框的上方 10 像素处,避免遮挡面部,画框颜色在识别成功时用绿色,unknown 时用红色,视觉上更直观。
5. 避坑:人脸识别考勤的高频翻车点与排查方法
5.1 同一张脸换个光线就认不出:光照敏感不是玄学
现象是上午录入的样本,下午靠窗位置就识别失败,人脸框能框住,但系统返回 unknown。原因是预训练模型对低照度、逆光和色温变化很敏感,单一样本的 128 维特征覆盖范围不够。解决方法是注册阶段多角度、多时段采集,同时在预处理里加直方图均衡化:
gray = cv2.cvtColor(face, cv2.COLOR_BGR2GRAY) gray = cv2.equalizeHist(gray) face = cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)equalizeHist能拉伸灰度分布,让暗部细节更明显。但注意它会把彩色信息丢干净,如果后面还要用彩色特征,更好的做法是只在检测阶段用灰度增强,特征提取阶段保留原图。这个坑我踩过两次,别在预处理阶段用力过猛。
5.2 相似脸误识别:双胞胎与阈值调优
现象是 A 打卡成功,后台却显示 B 的名字,双胞胎或者长得比较像的同学之间尤其明显。原因是 128 维特征空间里,两人距离本身就接近,默认阈值一宽松就跨过去了。考勤场景里误识比拒识更麻烦:拒识还能手动补卡,误识会直接记错人。解决办法是调低阈值,同时加一道低成本二次校验。常见做法是识别成功后弹窗要求输入学号后四位,或者考勤机配合工卡做双重确认。阈值怎么调?我一般对评估集里所有照片算一遍距离,画出同类距离和异类距离的分布,找两者交叉点附近的数值,0.4 到 0.5 是一个比较现实的区间。
5.3 Windows 下安装 face_recognition 失败:dlib 编译坑
现象是pip install face_recognition报错,控制台出现CMake must be installed或Failed to build dlib。原因是 dlib 需要本地编译 C++ 扩展,Windows 环境下缺 Visual Studio 的 C++ 构建工具就会失败。解决顺序是:先安装 CMake,再安装 Visual Studio Build Tools(勾选“使用 C++ 的桌面开发”工作负载),最后重新执行 pip 安装:
pip install cmake pip install dlib pip install face_recognition如果还是失败,换 Python 3.8 到 3.10 的 64 位版本试试,新版 Python 对 dlib 的 wheel 支持不一定跟得上。这个坑几乎每个在 Windows 上跑人脸识别的同学都会遇到,提前装好能省出一下午。
5.4 实时视频卡顿:每帧全图编码的代价
现象是画面预览正常,一进入识别循环就掉到 5 帧以下,CPU 占用接近 100%。原因是每帧都做一次全图人脸检测加 128 维编码,dlib 的 HOG 检测器虽然快,但 640x480 的分辨率下仍要消耗大量计算。解决方法是跳帧处理,每隔 3 帧识别一次,识别结果保持到下一轮刷新;另外可以先把帧缩小到一半再做检测,检测到人脸后再在原图上裁剪编码:
small = cv2.resize(frame, (0, 0), fx=0.5, fy=0.5) boxes = face_recognition.face_locations(small, model="hog") # boxes 的坐标需要放大 2 倍映射回原图这样做的代价是框位置会有一点点滞后,但对考勤打卡来说完全够用。如果还需要更高帧率,就换model="hog"之外更快的检测器,比如 OpenCV 的 DNN 人脸检测。
5.5 画面颜色发蓝发绿:OpenCV 的 BGR 与 RGB 混用
现象是 OpenCV 窗口里画面颜色正常,但 face_recognition 始终检测不到人脸,或者检测框位置偏移。原因是 OpenCV 的cv2.imread和VideoCapture返回的都是 BGR 顺序,而 face_recognition 内部按 RGB 处理,通道顺序反了会让特征编码严重偏离。解决方法是显式转换,不要在一个文件里混用两种顺序:
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)判断是不是踩了这个坑,最简单的方法是打印一张人脸图的 RGB 均值,如果红色通道明显偏低,基本就是顺序反了。这个坑只影响调用了 face_recognition 的开发方式,如果你全程用 OpenCV 自己写检测和特征提取,反而不会遇到。
6. 效果验证与答辩:用准确率和 ROC 曲线证明系统可用
6.1 留出集评估脚本:准确率、召回率与混淆矩阵
毕设答辩时,“能跑”不算亮点,“能证明自己调过参”才算。用第 3 章留出的faces/test/评估集,写一个批量评估脚本,遍历测试照片,统计正确识别、误识和漏识的数量:
import os import face_recognition with open("encodings.pkl", "rb") as f: known_encodings, known_names = pickle.load(f) tp = fp = fn = 0 threshold = 0.45 for true_name in os.listdir("faces/test"): path = os.path.join("faces/test", true_name) for file in os.listdir(path): img = face_recognition.load_image_file(os.path.join(path, file)) enc = face_recognition.face_encodings(img) if not enc: continue dist = face_recognition.face_distance(known_encodings, enc[0]) pred = known_names[int(dist.argmin())] if dist.min() < threshold else "unknown" if pred == true_name: tp += 1 elif pred != "unknown": fp += 1 else: fn += 1 precision = tp / (tp + fp) if (tp + fp) else 0 recall = tp / (tp + fn) if (tp + fn) else 0 f1 = 2 * precision * recall / (precision + recall) if (precision + recall) else 0 print(f"precision={precision:.3f} recall={recall:.3f} F1={f1:.3f}")这个脚本的本质是把阈值当成唯一超参数,通过评估集观察不同值下的表现。把threshold从 0.3 扫到 0.6,记录每一档的精度和召回率,你就能画出一条 ROC 曲线,答辩时贴上去,比任何文字描述都有说服力。注意face_encodings在检测不到人脸时返回空列表,脚本里跳过了这些样本,实际部署时它们应该算作漏识,统计时要单独计一笔。
6.2 进阶方向:活体检测与多模型对照
如果你还有余力,可以加一个最简单的活体检测:注册时记录用户完成“眨眼”或“张嘴”动作的 3 帧序列,识别时要求摄像头连续捕捉到动作变化才打卡成功。这种方案不依赖红外设备,纯 OpenCV 就能实现,能挡住照片和手机屏幕攻击,是考勤系统场景里很实用的加分项。另一个加分点是模型对照:把评估脚本换成 ArcFace 再跑一遍,对比两者在相同阈值下的准确率和 F1,能证明你做过选型而只是调用了一个库。
这两件事都不难,但工作量都不小,建议在主线流程稳定运行一周之后再动手。我自己的血泪经验是:别等答辩前一天才去调阈值,评估集越早建好,后期返工越少,参数调优的时间永远是越提前越划算。希望帮到你。
本文还有配套的精品资源,点击获取