
简介一套基于深度学习的人脸识别考勤系统毕业设计项目面向计算机专业正在准备毕设或需要项目实战练习的学生。系统支持260人考勤数据管理已通过导师指导认可适合作为毕业设计、课程设计或期末大作业直接使用。资源包共26个文件包含Python源代码、界面图片资源、配置文件、使用手册docx及演示视频压缩包大小239.41MB结构清晰便于查阅。其中3个Python文件覆盖核心识别与考勤逻辑15张PNG素材用于系统界面展示。项目经过严格调试可以直接运行已有158人学习。借助演示视频和手册可快速上手并理解深度学习人脸识别考勤系统的设计思路与实现细节对完成毕设论文和系统演示均有帮助。1. 人脸识别考勤系统毕设选题前先想清楚这三件事如果你正在为 Python 毕业设计选题发愁人脸识别考勤系统几乎是「性价比最高」的方向之一技术栈成熟、演示效果好、还能写进简历。但很多同学拿到这类项目后第一反应是「跑通就行」结果答辩时被问一句「FaceNet 和 ArcFace 的区别是什么」就卡住了。这套基于深度学习的人脸识别考勤系统源码本质上是一条完整的工程链路——从摄像头采集人脸、MTCNN 检测对齐、FaceNet 提取 128 维特征向量到 SVM 分类器判定身份、SQLite 落库记录考勤最后用 Tkinter 界面展示签到状态。它解决的不只是「人脸能对上号」的问题而是「在教室/办公室光线杂乱、人角度偏转的情况下怎么把识别结果可信地写进考勤表」。适合本科毕设、课程设计也适合想补全『检测→识别→业务』全链路经验的初学者。下文按我拆项目时的真实顺序展开每一步都能照着复现。2. 系统架构与模型选型为什么是 MTCNN FaceNet而不是直接跑一个 YOLO2.1 三层架构检测、特征提取、分类判定各管一摊拿到源码后先把工程目录捋一遍。这套系统的逻辑分三层和多数人脸识别项目一致检测层MTCNNMulti-task Cascaded Convolutional Networks负责从摄像头帧里把人脸框出来。它输出人脸边框坐标、关键点左眼/右眼/鼻尖/左嘴角/右嘴角和置信度。因为考勤场景下摄像头角度固定、人员会走动检测层必须容忍一定程度的模糊和侧脸。特征层FaceNet 把检测到的人脸区域 Embedding 成 128 维浮点向量。这个向量是人脸的「数字指纹」同一个人的不同照片向量距离近不同人的向量距离远。源码里用的是预训练权重不需要你自己训练大规模 CNN——这对毕设来说几乎是必须的因为从头训练 FaceNet 需要数百万张人脸数据和数周 GPU 时间。判定层拿到 128 维向量后用训练好的 SVM 分类器或直接算余弦相似度判断「这个人是谁」。源码默认用 SVM因为考勤场景人员是固定的一个班/一个办公室属于封闭集识别SVM 在小样本分类上比纯阈值判定更稳。选这套组合而不是「YOLO 检测 ResNet 分类」原因是 YOLO 擅长目标检测但不直接给身份语义ResNet 分类器面对「同一个人在不同光线下的脸」容易过拟合到背景。MTCNN FaceNet 的组合在 LFW 数据集上准确率能到 99% 以上对毕设来说是成熟且安全的路线。2.2 源码目录与关键文件说明项目解压后核心文件集中在以下位置文件/目录作用说明mtcnn/人脸检测模块包含 P-Net、R-Net、O-Net 三个子网络的推理代码facenet/特征提取模块加载预训练的 Inception-ResNet-v1 模型输出 128 维向量src/train_svm.py训练分类器读取已知人脸的特征向量训练 SVM 并保存模型src/attendance.py考勤主逻辑摄像头循环检测、识别、写库、UI 刷新data/faces/原始人脸样本每个人一个子目录存采集到的正面照data/embeddings/特征向量缓存train_svm.py 生成的 .npy 文件避免重复提取attendance.dbSQLite 数据库存员工信息、签到记录、签到状态demo.mp4演示视频作者跑通后的完整录制建议先看这个再动代码我第一次打开这套代码时建议你别急着跑python main.py而是先看src/attendance.py的主循环。它大致是读摄像头帧 → MTCNN 检测人脸 → 存在人脸则对齐并缩放为 160x160 → FaceNet 提特征 → SVM 预测 → 写数据库 → GUI 显示。如果你能看到这一串逻辑说明环境大概率没问题如果 import 阶段就报错九成是 TensorFlow 或 PyTorch 版本不对下文会专门讲。2.3 为什么用预训练模型而不是自己训练很多同学纠结「深度学习模型是不是必须自己训练」。以 FaceNet 为例它的训练需要三元组损失Triplet Loss——每次迭代选锚点、正样本、负样本让锚点与正样本距离小于锚点与负样本距离。这个训练过程极其依赖数据挖掘策略硬train的话光数据集就要几十 GB。这套源码用的是作者在公开数据集上预训练好的 Inception-ResNet-v1 权重直接加载即可提取高质量特征。你只需要用自己的数据训练一个「小分类器」——SVM 或 Softmax 层——把 128 维向量映射到人员 ID。这是工程上的明智取舍底层通用特征复用开源成果上层业务分类自己训练既符合毕设工作量要求又不会让训练时间失控。3. 数据准备与模型训练从人脸采集到特征向量落库的完整流程3.1 人脸采集环节数量和质量哪个优先这套系统没有内置自动采集脚本我建议用data/faces/目录结构手动整理每个人一个文件夹命名带学号或工号例如data/faces/20210001/。每个文件夹放 1020 张该人的正面照。注意以下几点光线变化采集时尽量覆盖不同光线条件比如窗户边、灯下、暗一点的位置各几张。考勤现场的光线和你采集时光线差异过大的话SVM 很容易翻车。角度正脸为主左右旋转 15 度内的照片各来一两张。纯正脸训练出来的模型对低头看手机的人不友好。数量 vs 质量10 张高质量的 50 张模糊的。模糊样本会污染特征向量均值属于典型的「垃圾进垃圾出」。3.2 预处理与特征向量提取MTCNN 检测出人脸后FaceNet 要求输入为 160x160 的 RGB 图像。MTCNN 的 O-Net 会返回 5 个关键点坐标利用这些关键点把眼睛对齐到固定位置再缩放到 160x160这样能消除头部姿态带来的大部分偏差。这个过程在src/align.py里实现核心代码如下import cv2 import numpy as np from mtcnn import MTCNN from facenet import FaceNet detector MTCNN() embedder FaceNet() # 内部加载预训练权重输出维度 128 def extract_embedding(image_path): img cv2.imread(image_path) img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 检测人脸取置信度最高的一张 faces detector.detect_faces(img_rgb) if not faces: return None face max(faces, keylambda x: x[confidence]) x, y, w, h face[box] face_img img_rgb[y:yh, x:xw] # 缩放到 FaceNet 要求的输入尺寸 face_resized cv2.resize(face_img, (160, 160)) face_resized (face_resized - 127.5) / 128.0 # 标准化 # 提取 128 维特征向量 embedding embedder.embeddings(np.expand_dims(face_resized, 0))[0] return embedding这段代码的逻辑是读图 → MTCNN 检测人脸框 → 裁剪 → 缩放到 160x160 → 标准化 → FaceNet 提特征。参数上注意三个点第一detect_faces返回的box是(x, y, w, h)但在某些版本里可能是(x, y, w, h)或(x1, y1, x2, y2)建议打印一下确认这个坑我踩过第二confidence过滤阈值一般设 0.9太低会把背景杂物当人脸第三标准化用的(img - 127.5) / 128.0是 FaceNet 官方推荐的改成别的值会掉点。3.3 训练 SVM 分类器特征向量提取完接下来把他们喂给 SVM。src/train_svm.py的核心逻辑如下import os import pickle import numpy as np from sklearn import svm from sklearn.preprocessing import LabelEncoder def load_embeddings(data_dir): 遍历 data/faces/ 下的每个子目录提取特征并打标签 X, y [], [] for person_id in os.listdir(data_dir): person_dir os.path.join(data_dir, person_id) if not os.path.isdir(person_dir): continue for img_file in os.listdir(person_dir): emb extract_embedding(os.path.join(person_dir, img_file)) if emb is not None: X.append(emb) y.append(person_id) return np.array(X), np.array(y) X, y load_embeddings(data/faces/) le LabelEncoder() y_encoded le.fit_transform(y) # RBF 核 SVMC 控制误分类惩罚gamma 控制 RBF 的宽度 clf svm.SVC(C10, gamma0.01, probabilityTrue) clf.fit(X, y_encoded) with open(models/svm_classifier.pkl, wb) as f: pickle.dump({model: clf, label_encoder: le}, f)SVM 的参数选择有讲究C太大容易过拟合到训练集考勤现场来一张新角度照片就会误判C太小欠拟合不同人的特征向量区分不开。我一般先用默认的C1.0, gammascale跑一遍看训练集准确率——如果训练集都不到 95%说明特征提取环节有问题或数据质量太差先别调参。如果训练集 100%这类小数据集很容易再拿标定集现场拍几张没参加训练的照片测低于 90% 就调高C到 10 或调整gamma。调参不是玄学但多数时候问题不在 SVM 而在数据。4. 考勤逻辑与数据库落库签到判重与迟到早退的工程细节4.1 考勤判定的完整流程与状态机识别到人脸并预测出人员 ID 后不能立刻写数据库——直接写会造成「同一张脸在视频流里被识别 30 次数据库里出现 30 条签到记录」。这是人脸考勤系统最常见的翻车现场。工程上必须引入「签到状态机」源码的做法是给每个识别到的人维护一个最近签到时间戳last_sign_in {} # person_id - 上次签到时间戳 def process_attendance(frame): embedding extract_embedding_from_frame(frame) if embedding is None: return None probs classifier.predict_proba([embedding])[0] max_prob np.max(probs) person_id label_encoder.inverse_transform([np.argmax(probs)])[0] if max_prob 0.6: return None # 置信度不足视为陌生人不签到 now time.time() last last_sign_in.get(person_id, 0) if now - last 10: # 10 秒内不重复签到 return None last_sign_in[person_id] now write_attendance_to_db(person_id, now) return person_id这段代码有几个关键参数可调置信度阈值 0.6决定「多像才算这个人」调高了会漏签调低了会代签去重间隔 10 秒决定同一个人多久内只记一次。实际场景里10 秒间隔太短——学生打完卡在旁边踱步 5 秒又入镜会生成第二条记录建议改成 3060 秒。还有一种特殊情况两个不同的人长得像SVM 预测概率都不超过 0.6系统会两者都不认。这时需要手动录入源码的 GUI 里应该留了手动打卡入口可以看下代码里的manual_sign_in方法。4.2 数据库表结构设计与写入策略项目用的是 SQLite单文件免安装适合毕设。核心两张表employees员工信息和attendance_records签到记录。后者至少包含这些字段字段名类型说明idINTEGER PRIMARY KEY自增主键person_idTEXT员工编号对应employees.person_idcheck_in_timeTEXT签到时间ISO 格式statusTEXTnormal/late/early_leaveconfidenceREALSVM 输出概率留存备查写入策略上建议用单条 INSERT 加事务控制不要每帧 INSERT——二维码考勤机也不会一帧写一次库。源码的做法是内存里攒一批记录每 5 秒批量写入一次避免频繁磁盘 IO 导致摄像头画面卡顿。对于毕设答辩SQLite 足够如果老师说「并发怎么办」你就答「生产环境换 PostgreSQL Redis 缓存」但不要往毕设里加复杂度会翻倍。4.3 GUI 展示逻辑Tkinter 刷新不卡顿的要点源码的界面部分用 Tkinter 实现核心坑是「在摄像头循环里直接更新 UI 组件会卡死」。正确做法是用after()定时调度 UI 刷新而不是在 while 循环里同步更新import tkinter as tk class AttendanceApp: def __init__(self): self.root tk.Tk() self.video_label tk.Label(self.root) self.video_label.pack() self.status_label tk.Label(self.root, text等待签到...) self.status_label.pack() self.update_frame() # 启动视频刷新循环 def update_frame(self): # 从摄像头读一帧识别人脸更新画面 frame capture_frame() if frame is not None: processed process_frame_with_box(frame) # 画框显示名字 display_image(processed) # 转 ImageTk.PhotoImage 并 set self.root.after(30, self.update_frame) # 约 33 FPS这里after(30)是 30 毫秒刷新一次对应约 33 FPS。如果你机器性能差改成 50 毫秒20 FPS也不会觉得卡。注意ImageTk.PhotoImage不能声明为局部变量必须挂在对象属性上否则图片会被垃圾回收画面变成黑屏——这个坑至少能浪费新手半小时。5. 避坑专题人脸识别考勤系统最常见的五个翻车现场5.1 TensorFlow 与 Python 版本不匹配导致模型加载失败现象运行train_svm.py时加载 FaceNet 权重直接报UnknownError: Op type not registered BlockLSTM或各种No module named tensorflow.contrib。原因这套项目的 FaceNet 实现是老版本 TensorFlow 1.x 的写法Python 3.8 以上环境装 TensorFlow 2.x 后contrib模块被移除底层算子注册不兼容。解决最省事的方式是创建一个 Python 3.7 TensorFlow 1.15 的虚拟环境。如果你已经在 Python 3.10 上建议改用 TensorFlow 2.x 兼容模式或者换成keras-facenet之类的库来加载权重。我拆这套代码时直接锁了 Python 3.7 TF 1.15一步到位。5.2 摄像头画面模糊导致检出率骤降现象程序能跑但离摄像头 2 米以外的人脸几乎检测不到走近了才能签到。原因MTCNN 是轻量网络对模糊人脸非常敏感考勤现场光线暗时摄像头自动增益会拉高 ISO产生噪声MTCNN 把噪声误判为「非人脸」。解决先看cv2.VideoCapture的帧率与分辨率设置源码里有没有cap.set(3, 640)之类的参数。分辨率越高MTCNN 在 CPU 上推理越慢。常见做法是把分辨率设为 640x480并用下面的代码增强光线# 人脸检测前先做 CLAHE 自适应直方图均衡化 import cv2 clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) enhanced clahe.apply(gray)再配合cap.set(cv2.CAP_PROP_BRIGHTNESS, ...)手动调亮度比纯靠算法稳得多。5.3 同一个人多次签到数据库里刷出一条龙现象一个人站在摄像头前 1 分钟数据库里出了 20 条签到记录。原因代码没有签判重逻辑每一帧识别成功都执行了一次写入。解决按第 4 章讲的引入last_sign_in字典做时间窗口去重窗口设 3060 秒。如果演示时需要「连续识别不同的人」可以展示识别框上的人名变化但别让数据库记录刷屏。5.4 置信度阈值设太低陌生人被认成班里的人现象放一张别班的照片在摄像头前系统把他识别成某个学生并签了到。原因SVM 的predict_proba在样本类别少时容易给出虚高概率当面别人数只有 5 个人时就算是陌生人Softmax/SVM 输出概率也不会太低。解决最暴力的手段是拉高阈值到 0.85 甚至 0.9宁可不识别也不瞎识别。更稳的做法是额外引入「人脸特征向量与已知库最小距离」判断——距离大于某阈值直接视为陌生人。源码里如果有face_distance函数就优先用这个比 SVM 概率可靠。5.5 程序退出时摄像头占用不释放第二次启动报错现象第一次运行正常关掉程序后马上再运行报Camera cant be opened。原因没有调用cap.release()摄像头资源未释放驱动层面设备还被占用。解决程序退出事件里强制释放def on_closing(): cap.release() cv2.destroyAllWindows() root.destroy()另外注意如果在 Jupyter Notebook 里多次运行cv2.VideoCapture(0)也可能因为内核态资源未回收导致打不开摄像头遇到这情况就把 notebook kernel 重启别跟驱动死磕——这也是血泪经验。6. 阈值调参与验证把识别准确率从 90% 拉到 98% 的实操技巧6.1 用网格搜索替你试阈值很多同学调 SVM 置信度阈值是全靠手感0.7 不行改 0.80.8 不行改 0.75跑来跑去还是看不出差别。正确做法是把阈值这个参数交给网格搜索让数据说话from sklearn.metrics import accuracy_score def evaluate_thresholds(embeddings, labels, classifier, label_encoder, threshold_list): 在独立验证集上测试不同阈值的准确率 best_threshold 0.7 best_acc 0.0 for thr in threshold_list: preds [] for emb, true_label in zip(embeddings, labels): prob classifier.predict_proba([emb])[0] max_prob np.max(prob) pred label_encoder.inverse_transform([np.argmax(prob)])[0] if max_prob thr: pred unknown # 阈值过滤视为陌生人 preds.append(pred) # 注意unknown 算预测错否则拉高阈值只会提高精确率但降低召回率 acc accuracy_score(labels, preds) if acc best_acc: best_acc acc best_threshold thr return best_threshold, best_acc这个脚本特别有用。把threshold_list设为[0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9]在验证集上跑一遍出来的那组阈值就是你们教室场景下的最优值。注意要保证验证集里有「陌生人」样本——去隔壁班拍几张脸放进去否则这个测试没有意义。6.2 特征向量空间里的「距离」才是最终防线SVM 概率是「相对信念」FaceNet 向量之间的欧氏距离才是「绝对度量」。生产级系统通常会双保险SVM 预测出 ID 且置信度超阈值同时算该向量与该 ID 所有已知向量均值的欧氏距离距离超上限就强制判为陌生人。def verify_with_distance(embedding, person_id, class_centers, distance_threshold1.0): center class_centers[person_id] dist np.linalg.norm(embedding - center) return dist distance_threshold # 距离超过阈值拒绝识别FaceNet 的 128 维向量经过 L2 归一化后同一个人不同照片的欧氏距离通常在 0.81.1 之间不同人的距离通常在 1.2 以上。这个阈值需要自己在现场标定手持设备绕教室走一圈记录你和同学各自的距离分布找出能分开两个分布的切点。6.3 活体检测的边界与后续实验方向这套源码默认不带活体检测意味着用一张打印的照片贴在摄像头前很大概率会被识别成真人。如果你答辩被问到诚实回答「当前实现聚焦于识别精度活体检测是防线上的下一步」比硬着头皮说支持要好。如果想往「安全增强」方向加工作量可以补一个眨眼检测用 OpenCV 的EAREye Aspect Ratio计算眼睛开合度连续 N 帧 EAR 低于阈值即认定为眨眼只有检测到眨眼才放行。这样既不会大幅改动原有架构又能让系统在答辩时多一个「考虑到了活体攻击」的亮点。从那以后我每次拿到这类人脸识别项目第一件事就是检查三处代码有没有置信度过滤、有没有时间窗口去重、有没有摄像头资源释放。这三行代码决定你演示时出丑概率。然后强制自己跑一遍阈值网格搜索不会花超过 20 分钟但能让答辩数据好看起来。这套源码里值得研究的细节不少尤其是 MTCNN 关键点对齐那块改一下对齐方式可能直接提升你考场上的识别率。希望帮到你。本文还有配套的精品资源点击获取