简介:面向计算机相关专业毕业生与项目实战学习者,提供一套基于深度学习的人脸识别考勤系统Python完整源码。项目为经导师指导并认可的高分本科毕业设计,评审98分,覆盖人脸检测、特征提取、考勤记录管理、可视化界面等核心模块,既适合作为毕业设计参考,也可直接用于课程设计或期末大作业。资源包共25个文件,包含3个Python主程序文件、15张界面与状态图片、字体图标、配置文件、API对接说明、使用手册等,整包约239MB,目录按核心功能组织,便于对照源码快速定位逻辑并完成二次开发。目前已有166人学习下载。拿到手后可结合手册和源码理清完整业务流程,参考界面素材与配置项搭建运行环境,在此基础上替换数据集、扩展考勤报表或接入现有管理系统,是一条可直接落地的高分解决方案。
1. 基于深度学习的人脸识别考勤系统:这个毕设题目为什么难,又为什么值得做
教室门口排长队、代签成风、老师对着花名册一个个喊名字——本科毕设选人脸识别考勤系统,多半是被这三件事逼出来的。这个题目听起来只是“跑个模型、写个界面”,实际是一条完整链路:人脸检测、人脸对齐、特征提取、相似度比对、考勤落库、展示界面,任何一环翻车,答辩现场都会被一眼看穿。
这个标题里最关键的两个词是“深度学习”和“考勤系统”。前者决定了识别精度比传统 Haar 特征高一个数量级,后者决定了你不只要让模型认得出人,还要让它替我记下“谁、几点、迟到没有”。顺手把找源码、跑 demo、换数据、改参数、写论文整套流程走通,也顺便解决掉你最关心的那个问题:拿到手之后,我能不能在三天里让它跑起来。
2. 人脸识别考勤系统的技术选型:从 OpenCV 到 FaceNet/ArcFace,到底该信谁
2.1 一张人脸从摄像头到你名字的完整链路
一个考勤系统里的人脸识别,不是“一张图喂进模型就出名字”那么简单。常见落地链路是五步:检测、对齐、归一化、特征提取、比对判识。检测负责告诉系统“画面里哪块区域是人脸”;对齐把歪头、侧脸的框校正成正脸;归一化把不同光线、清晰度下的脸缩放到同一标准;特征提取用深度卷积网络把一张 160×160 或 112×112 的人脸编码成一个固定长度的向量;比对则是把这个向量和注册库里所有人的向量算相似度,超过阈值就认为是同一个人。
考勤系统和手机人脸解锁的差别在于,手机是用户主动配合、知道自己在被识别,考勤则经常是“走过去的那一瞬间完成识别”。这意味着你狠多时候要处理侧脸、低头、逆光、戴帽子的画面,并不能靠用户配合来降低难度。
2.2 检测器选谁:Haar、MTCNN 还是 RetinaFace**
老一代 OpenCV 自带的 Haar 级联检测器,优点是纯 CPU 也能跑得飞起,但它的误检率和漏检率在教室门口这种复杂背景里会让人崩溃。我见过有人拿 Haar 做考勤,结果窗户外的树影、黑板上的白字都能被当成脸,打卡记录一片混乱。它适合做“人脸存在性检测”的演示,不适合做考勤判据。
现在的毕业设计我一般建议直接上 MTCNN,它是“检测 + 对齐”一体的,能同时输出人脸框和五个关键点(左眼、右眼、鼻尖、左右嘴角),拿到关键点之后可以用仿射变换把脸转正。如果想要更高的精度,再往上换 RetinaFace。但做本科毕设,MTCNN 的精度已经足够,而且它对显存的要求低,CPU 也能跑。用 OpenCV DNN 模块加载 Caffe 模型做检测也是常见做法,但少了关键点输出,后面要对齐还得再补一环。
2.3 特征提取选谁:FaceNet 是教学首选,ArcFace 是工程首选
特征提取是整个系统的核心判据。经典方案是 FaceNet,它把最后一层改成了 128 维的嵌入输出——换成工程界的说法,就是把人脸压缩成一个 128 维的指纹串。它的好处是模型相对小、推理快、LFW 精度可达 98% 以上,做毕设完全够用。另一条路线是 ArcFace,它通过加角间距损失让类内距离更紧、类间距离更松,论文指标普遍比 FaceNet 再高一截,很多商用考勤机和门禁机用的就是这类的变体。
选型时还会涉及一个容易被低估的问题:模型参数量和显存占用不是一回事。很多同学看到网上的模型文件只有几十 MB,就以为显卡随便跑,结果加载常见的 ResNet-50 做骨干的 ArcFace,光推理一个 batch 就能吃掉 1GB 显存。做考勤系统你没有必要上大 batch,每次只丢一张人脸进去,推理延时反而比批量处理更好控制。省下来的显存预算可以全砸在界面和后端上。
2.4 比对策略:欧氏距离还是余弦相似度
特征提取完以后,怎么判断两张脸是不是同一个人?常见两种:欧氏距离和余弦相似度。欧氏距离受向量长度影响,如果模型输出没有做 L2 归一化,那不同人的特征向量长度差异可能比方向差异还大,容易误判。所以我在考勤系统里只认余弦相似度,或者先把向量 L2 归一化再算距离,这两种等价。
阈值是考勤系统里最玄学的一个参数。阈值定高了,同一个同学稍微换个角度打卡就被拒识,一脸尴尬地反复刷脸;定低了,隔壁同学往摄像头前一站,系统把他也记成你。正确做法不是拍脑袋定 0.6 还是 0.8,而是从注册库里所有人两两算相似度,画出同一人的相似度分布和不同人的相似度分布,把阈值取在两个分布交叠带的中位点。
| 环节 | 常用方案 | 精度水平 | 部署成本 | 毕设推荐度 |
|---|---|---|---|---|
| 检测 | Haar 级联 | 低,误检多 | 极低,纯 CPU | 不推荐 |
| 检测+对齐 | MTCNN | 中高 | 低,CPU 可跑 | 推荐 |
| 检测+对齐 | RetinaFace | 高 | 中,需 GPU 更顺 | 有余力再上 |
| 特征提取 | FaceNet | 高(LFW 98%+) | 低 | 首选 |
| 特征提取 | ArcFace | 很高 | 中高显存 | 追求高分可换 |
3. 跑通一个人脸考勤最小系统:数据集准备、特征注册与识别的完整代码
3.1 绕开“从零训练模型”的坑:用预训练模型 + 迁移特征
很多第一次碰深度学习的同学,看到“基于深度学习”几个字就以为要从头训练一个 CNN,这是毕设里最大的误解。人脸识别模型要在百万级人脸数据上训练很多天,本科毕设没有必要也扛不住这个成本。常见做法是直接加载 ImageNet 或 VGG Face 上预训练好的权重,把网络当特征提取器用,只做推理,不做训练。
下面这套最小系统是我跑通多次的路线:Python 3.8 以上环境,装facenet-pytorch和opencv-python,前者自带 MTCNN 检测器和 InceptionResnetV1 预训练模型,后者负责读摄像头帧。先把环境搭好:
pip install facenet-pytorch opencv-python torch torchvision这段把人脸识别需要的三件套一次装齐。facenet-pytorch里封装了 MTCNN 和 FaceNet 权重,不用自己拼模型结构;torch是深度学习框架;opencv-python负责图像读取和摄像头操作。装完以后,你不需要配 GPU,CPU 也能跑,只是每识别一帧可能要几百毫秒。
3.2 特征注册:把每位同学的 N 张照片压缩成一条特征向量
考勤系统的第一步是“注册”。我给每个人拍三到五张不同角度的正脸照,放进以姓名命名的文件夹,逐个提取特征,最后把特征和姓名存进一个.npy文件。这一步是离线的,不需要摄像头实时参与,所以跑得慢一点也完全没关系。
import torch from facenet_pytorch import MTCNN, InceptionResnetV1 import cv2 import numpy as np from pathlib import Path mtcnn = MTCNN(image_size=160, margin=0, keep_all=False) model = InceptionResnetV1(pretrained='vggface2').eval() def enroll_person(person_name: str, photo_dir: Path): embeddings = [] for img_path in sorted(photo_dir.glob('*.jpg')): img = cv2.imread(str(img_path)) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) face = mtcnn(img) if face is not None: emb = model(face.unsqueeze(0)).detach().numpy().flatten() embeddings.append(emb) if embeddings: avg_emb = np.mean(embeddings, axis=0) avg_emb = avg_emb / np.linalg.norm(avg_emb) return person_name, avg_emb return None with open('embeddings.npy', 'wb') as f: np.save(f, np.array([d for d in all_embs]))这段代码的核心逻辑:用 MTCNN 从照片里裁出对齐后的人脸,缩放成 160×160,丢给 InceptionResnetV1 得到 512 维向量(新版 FaceNet 是 512 维,老论文写的 128 维是旧版结构)。同一人多张照片的特征做平均,再做 L2 归一化,目的是消除照片亮度和对比度带来的向量长度差异。注册时只保留最大的一张人脸,因为考勤机前大多数时候只有一个人,keep_all=False可以避免背景里路过的人干扰注册。
3.3 实时识别:摄像头帧、特征比对与阈值判断的完整循环
注册完就能跑实时识别。这里有个细节:每帧都跑检测和特征提取,CPU 机器会卡成幻灯片。常见做法是控制帧率,比如每 0.5 秒取一次帧做识别,中间跳过其余帧。识别通过时把人名和相似度打在画面左上角,方便演示和录屏。
known_names = np.load('names.npy', allow_pickle=True) known_embs = np.load('embeddings.npy', allow_pickle=True) cap = cv2.VideoCapture(0) if not cap.isOpened(): raise RuntimeError('摄像头初始化失败,检查设备索引号') while True: ret, frame = cap.read() if not ret: continue # 每 3 帧抽 1 帧识别,其余帧直接显示 frame_to_check = cv2.resize(frame, (640, 480)) face = mtcnn(frame_to_check) if face is not None: emb = model(face.unsqueeze(0)).detach().numpy().flatten() emb = emb / np.linalg.norm(emb) # 同库里所有人算余弦相似度,取最高分 scores = np.sum(known_embs * emb, axis=1) best_idx = int(np.argmax(scores)) if scores[best_idx] >= 0.75: name = known_names[best_idx] cv2.imshow('attendance', frame_to_check) if cv2.waitKey(1) & 0xFF == ord('q'): breaknp.sum(known_embs * emb, axis=1)算的是所有注册特征与当前特征的余弦相似度,因为 L2 归一化后,内积就等于余弦值。阈值 0.75 是我在普通教室光线、普通 USB 摄像头下的起点,不同摄像头拍出来的分数线差很多,后面避坑章节会专门说怎么调。识别结果我在画面上只显示人名,不显示相似度分数,真实考勤系统里分数也不该暴露给学生看,免得有人拿张高分照片来试用。
3.4 这个最小系统验证了什么:能在几种光线条件下跑通
这套最小系统能验证的,是你整个毕设的“主链路”通没通。我建议录制三段时间不短的视频:一段正常光线正脸打卡、一段逆光侧脸打卡、一段两个人前后脚路过摄像头。后面两段用来逼出问题——逆光时 MTCNN 可能检不到脸,两个人同时入镜时可能识别错人。出现这类问题不要慌,这是所有考勤系统都要面对的边界,留到论文“局限与改进”里写反而成了加分项。
最小系统跑通后,你才真正拿到了自己代码的“地基”。后续加考勤记录、加界面,都在这套代码上迭代。
4. 把识别结果落成考勤记录:数据库设计、重复打卡判重与迟到判断
4.1 考勤系统不是“识别对”就完事:还需要一张表把账记清楚
人脸识别准确只是前半场,后半场是考勤逻辑。我见过不少拿识别 demo 直接去答辩的同学,老师一问“怎么防止同一节课重复打卡”“迟到时间怎么算”,当场愣住。这段是考勤系统和普通识别 demo 的最大区别,也是高分毕设和及格毕设的分水岭。
考勤系统至少要有两张表:学生表(或员工表)和考勤记录表。学生表存姓名、学号、注册特征向量的路径;考勤记录表存“谁、哪天、第几节课、签到时间、是否迟到”。如果你做的是企业考勤,还要加上下班时段和加班时长;做教室考勤,session 字段用来区分上午第一二节、下午五六节这种不同时段,防止同一天重复签到但不属于同一节课。
CREATE TABLE students ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_no TEXT UNIQUE NOT NULL, name TEXT NOT NULL, face_embedding_id INTEGER NOT NULL ); CREATE TABLE attendance_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_id INTEGER NOT NULL, session TEXT NOT NULL, check_date TEXT NOT NULL, check_in_time TEXT NOT NULL, is_late INTEGER DEFAULT 0, UNIQUE(student_id, session, check_date) );UNIQUE(student_id, session, check_date)是表结构层面的判重保障,即使代码里漏了判重,数据库也不会写入同一节课的两条记录。这是最稳的兜底,比在 Python 里用一个字典去重可靠得多,因为程序崩溃后字典缓存会丢,而数据库唯一约束永远生效。is_late用整数 0/1 表示,避免存字符串、布尔值在不同数据库之间的兼容性问题。
4.2 签到逻辑的代码实现:冷却时间窗口与迟到判断阈值
有了表结构,签到逻辑里最关键的是两件事:识别到人之后多久能再次签到、迟到的判定线怎么定。重复签到常见于学生走到摄像头前停下来多刷了几次脸,避免这种情况不能只靠数据库唯一约束,因为那样会直接报错而不是静默忽略。
import sqlite3 from datetime import datetime conn = sqlite3.connect('attendance.db') COOLDOWN_SECONDS = 120 # 同一人两次签到最短间隔,防重复打卡 LATE_SECONDS = 8 * 3600 + 20 * 60 # 08:20 之后算迟到 last_checkin = {} def record_checkin(student_id: str, session: str, now: datetime = None): now = now or datetime.now() key = (student_id, session) if key in last_checkin: elapsed = (now - last_checkin[key]).total_seconds() if elapsed < COOLDOWN_SECONDS: return 'duplicate' date_str = now.strftime('%Y-%m-%d') time_str = now.strftime('%H:%M:%S') seconds_since_midnight = now.hour * 3600 + now.minute * 60 + now.second is_late = 1 if seconds_since_midnight > LATE_SECONDS else 0 try: conn.execute( 'INSERT INTO attendance_log ' '(student_id, session, check_date, check_in_time, is_late) ' 'VALUES (?, ?, ?, ?, ?)', (student_id, session, date_str, time_str, is_late) ) conn.commit() last_checkin[key] = now return 'ok' except sqlite3.IntegrityError: return 'already_exists'COOLDOWN_SECONDS设成 120 秒,意思是同一个人同一节课两分钟内只记一次。这个窗口不能设太短,否则学生“低头捡笔——抬头看镜头”这种间隔也会触发二次写入;也不能设太长,否则学生走出教室又回来拿书,会错过第二次签到。课程考勤我一般推荐 60 到 180 秒区间,具体调多少要看你们教室到镜头的距离和摄像头前人流密集度。迟到判定写死成 08:20,实际部署时应该从课程表里动态读,这个版本为了演示写死,方便你理解位置在哪。
4.3 考勤记录落库后的可视化管理界面
识别和签到逻辑都跑通以后,还有一个老师一定会问的功能:“我能不能看到今天谁没来?”很多拿高分的人脸考勤项目都会带一个简单的管理界面,用 Tkinter 还是 PyQt5 不重要,重要的是能查、能导出、能看统计。我的习惯是做一个“实时签到 + 历史查询 + 导出 CSV”三页的简单界面。
Tkinter 的优势是 Python 自带,打包体积小,写入论文时不用额外引第三方依赖;PyQt5 界面更好看,但打包后体积大,答辩演示时一旦环境缺 DLL 容易翻车。我推荐 Tkinter,因为它不挑机器,用 PyInstaller 打包时少踩很多坑。界面代码里把 4.2 节的record_checkin直接封装成一个按钮回调,识别线程拿到人名后只负责把student_id传给这个回调,UI 线程负责显示。界面和识别线程之间不要共享可变状态,否则卡顿和死锁会让你调试到怀疑人生。
4.4 考勤数据与特征库的目录组织
代码文件多了以后,目录结构直接决定你后续维护和论文作图的效率。我在做这个项目时用的组织方式,可以让整套代码在“本地 demo”和“论文交付”两种形态之间切换。
attendance_system/ ├── app/ │ ├── main.py # 主程序入口,启动识别线程和 UI │ ├── recognition.py # MTCNN + FaceNet 封装 │ ├── attend.py # 考勤逻辑,含冷却和迟到 │ └── database.py # SQLite 连接与建表 ├── data/ │ ├── raw_photos/ # 注册用原始照片,按人名分文件夹 │ ├── embeddings.npy # 注册特征向量 │ └── names.npy # 对应对姓名 └── attendance.db # 考勤记录数据库这个结构的核心原则:识别代码、考勤业务、界面三块彼此不交叉。recognition.py不 import 数据库,attend.py不 import 摄像头,后面换模型、换数据库、换界面框架时,只需要动对应模块,其他不动。这也是论文里“模块化设计”一章的现成素材。
5. 人脸考勤系统常见坑与排查:阈值玄学、光线翻车、摄像头选择与打卡判重
5.1 识别结果时好时坏:决定成败的往往不是模型,而是摄像头参数
现象:同一个注册库,在实验室摄像头下识别率 98%,换到答辩教室的投影仪自带摄像头立刻掉到 70%,甚至有两人互相误识别。原因:模型没变,变的是摄像头分辨率、白平衡、自动增益。投影仪摄像头色彩偏冷,自动曝光还会在窗口强光下压暗人脸,导致特征偏移。解决:我一般建议固定使用同一个 USB 摄像头做注册和签到,并且关闭摄像头的自动白平衡,锁定曝光时间。
cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_AUTO_WB, 0) # 关闭自动白平衡 cap.set(cv2.CAP_PROP_EXPOSURE, -6) # 锁定曝光,数值依摄像头而定这种“玄学”问题在论文里不好写,但答辩现场如果识别不了人,代码写得再好也白搭。所以我在做毕设时都会在文档里留一小节“实验环境说明”,把摄像头型号和参数写进去,让老师和评委明白识别率不是模型不行,而是采集环境对齐过的。参数取值有摄像头的个体差异,需要现场拿注册人的脸多试几轮再定。
5.2 阈值调优的翻车现场:相似度 0.8 不是标准答案
现象:把阈值从 0.7 调到 0.8 以后,某同学连续刷了六次脸,每次都提示不认识。原因:不同摄像头采集的注册照片和现场照片,相似度分布差异很大。我在普通 720P 摄像头下测过,同一人现场脸和注册照相似度往往只有 0.68 到 0.85 之间,0.7 的阈值会频繁拒识,0.8 更是几乎全军覆没。解决:注册和签到用同一个摄像头,保证光照接近。然后从你自己班级里收集 5 到 10 个人,每人注册 3 张、测试 5 张现场照,统计同一人分数和不同人分数,取两个分布分界处的中值。不要直接抄别人的阈值。
5.3 戴眼镜、换发型后识别失败:注册阶段的“多样性”是后悔药
现象:一位同学注册时戴黑框眼镜,今天换了隐形眼镜,识别失败。另一位同学刘海盖住额头,误识别成别人。原因:注册照片太少,只覆盖了一个姿态,模型没见过的外观变化空间太大。解决:注册时让每个人提交 3 张有变化的照片:一张正脸、一张稍侧脸、一张戴/不戴眼镜各拍一张。注册特征存平均向量,覆盖面比单张照片宽。这里要特别注意,很多网上数据集只有正脸照,注册效果会远差于实地采集。
5.4 识别成功但考勤没记录:冷却时间窗口设成了负反馈
现象:人脸识别画面上显示了姓名,但考勤列表里找不到这条记录。原因:上一条考勤记录刚写入,两分钟冷却窗口没过,这次识别结果被默认成“重复打卡”丢掉了。在课上第一次用的时候,全班前几个人都能正常记录,后面进来的人全部被冷却窗口过滤掉。解决:不是把COOLDOWN_SECONDS改成 0,而是在弹窗上显示“已签到,无需重复打卡”,并把这次识别当成演示数据也要写进一个临时日志表。
5.5 摄像头前两个人同时出现:MTCNN 的 keep_all 选择
现象:两个同学并肩走到摄像头前,系统把其中一人 A 识别成另一人 B。原因:MTCNN有两张脸时,如果设置了keep_all=False,它默认只保留一张“置信度最高”的脸,但这个最高置信度的脸不一定是靠得最近的学生,反而可能是背景里更聚焦的那张。解决:考勤场景里单人打卡是常态,双人同框时我建议直接丢弃这一帧,不做识别,并提示“画面中有多人,请单独打卡”。等到下一帧只剩一人时再识别,逻辑最简单也最稳。多人同时识别考勤在论文里可以写进展望,但不要为了这个功能牺牲单人识别率。
6. 最后一步让系统“能答辩”:做一次带时间轴的演示验证
到了这个阶段,你的系统已经能注册、能签到、能记录、能查错。但答辩和毕设验收看的不是代码跑起来那一瞬间,而是你能否证明“它真的可用”。这里我强烈建议你录一段 3 到 5 分钟的演示视频,按时间轴表记录每个关键动作,这比现场开摄像头稳得多。现场一旦设备出错、光线不对、有人路过画面,你很难在两分钟内恢复到理想状态。
| 时间轴 | 动作 | 预期效果 | 对应模块 |
|---|---|---|---|
| 00:00–00:30 | 启动系统,进入签到界面 | 窗口弹出,摄像头预览流畅 | main.py |
| 00:30–01:00 | 同学 A 正脸签到 | 画面显示姓名,数据库写入记录 | recognition + attend |
| 01:00–01:20 | 同学 A 再次签到 | 提示“已签到”,不重复入库 | 冷却窗口 |
| 01:20–01:50 | 同学 B 佩戴眼镜签到 | 显示 B,不误识别成 A | 注册照片多样性 |
| 01:50–02:20 | 打开考勤统计页,导出 CSV | 出现 A、B 的记录与迟到标记 | 数据库 + UI |
这段视频的时间轴同时展示了你的全链路,也顺带展示了“重复签到不过”这个细节,评委往往对“不是只做人脸识别,还考虑了实际考勤规则”这一点印象很深。
另一个能拉开分数差距的小技巧是把活体检测做进去。很多毕设人脸考勤系统被老师当场质疑“拿照片能不能签到”,如果你在代码里加了一个最简单的眨眼检测——连续几帧里眼睛关键点纵向上距离从大变小的跳变记为一次眨眼,就能直接回应这个问题。这个实现不会增加太多代码量,但在论文里能单独开一小节,性价比很高。
说了这么多,我最大的教训是:人脸识别考勤这个题目,难度不在深度学习本身,而在把所有环节串起来以后还能稳定跑完一节课。我第一版代码跑通识别只花了一天,但为调摄像头和阈值又折腾了两周。如果你现在正在做这个题目,先把第 3 章的最小系统跑起来,那之后每一步都有抓手。希望帮到你。
本文还有配套的精品资源,点击获取