简介:一份基于深度学习的人脸识别考勤系统毕业设计项目,面向计算机相关专业学生,解决传统考勤效率低、接触式打卡等问题,适合用于课程设计、毕业设计或人脸识别入门实践。压缩包共包含四十九个文件,大小约十三点一五兆,内有十七个Python脚本、九个编译文件、七张示例人脸图片、四个说明文档及模型权重文件等,覆盖数据预处理、模型训练、人脸检测、特征提取与匹配、数据库存储等完整环节。已有二百三十一人学习下载。项目基于Python与常见深度学习库实现,核心代码包含模型网络定义、训练脚本、评估脚本等,并附有环境依赖、使用说明和常见问题汇总,可直接运行或二次开发。通过该项目可系统掌握从数据集准备到模型部署的完整流程,理解卷积神经网络结构设计、损失函数构造与人脸特征向量比对等关键技术,是综合运用计算机视觉与深度学习知识的良好范例。
1. 深度学习人脸识别考勤系统:毕业设计里最容易被"能跑"骗过去的项目
毕业设计里,"基于深度学习的人脸识别考勤系统"是热门选题:看起来有深度学习、有应用场景、能演示,很多同学手里都有一个 xxx.zip。但真正把 zip 解压后能跑通的人不多,因为这类系统的难点不在"人脸识别"这个黑匣子,而在"考勤"这两个字——现场光线、角度、打卡逻辑、重复签到、数据存储全是坑。从模型选型、数据处理、训练调参,到考勤逻辑与现场排错,每条路都有扎扎实实的坑要填。这套方案适合正在选毕设题的学生,也适合想用低成本方案给公司做考勤的开发者。照着下面每一章操作,你能复现一个可以演示、也能经受现场提问的完整系统。
2. 技术路线怎么定:模型选型与考勤系统整体架构
先说清楚一件事:这类系统是典型的"视觉识别 + 业务记录"工程,不是单纯把一个人脸识别模型跑起来就结束。考勤要的是"谁、在什么时间、有没有合法地打上卡",识别只是前半段。先定技术路线,再谈代码,能省掉后面大量返工。
2.1 为什么考勤系统要选深度学习人脸识别:与传统方案和刷卡方案的差距
考勤场景里,传统方案一直存在:RFID 刷卡、指纹机、OpenCV 传统人脸识别。刷卡和指纹的问题是"必须主动配合",排队、忘带卡、指纹磨损都会让考勤记录失真。传统人脸识别(LBPH、EigenFace)在固定光照、正脸朝向的受控环境里能用,但考勤现场的光线和角度不可控,稍微侧脸或者背光,识别结果就会翻车。
深度学习人脸识别走的路线完全不同:先用卷积神经网络检测出人脸框和关键点,再把对齐后的人脸图像编码成一个固定维度的特征向量,最后用向量相似度判断"是不是同一个人"。CNN 在这个过程里学到的是对光照、角度相对鲁棒的人脸表征,这也是"深度学习"这三个字在标题里的落点。三套方案放在一起看更直观:
| 方案 | 光照变化 | 角度变化 | 维护成本 | 适合场景 |
|---|---|---|---|---|
| OpenCV 传统人脸(LBPH/EigenFace) | 差 | 差 | 低 | 受控环境演示 |
| 深度学习检测 + 特征提取 | 较好 | 较好 | 中,需标注与训练 | 考勤、门禁 |
| 商用现成人脸识别 API | 好 | 好 | 按量付费 | 快速原型,毕设不建议 |
这里要补一个取舍:考勤系统对实时性的要求远低于对正确性的要求,摄像头前每秒抽 2~3 帧做人脸识别就足够,但"认错人"比"识别慢"严重得多。所以后面所有参数设计,都以"别认错人、别漏记人"为优先目标。
2.2 模型选型:检测模型与特征提取模型怎么搭配
一个完整的人脸识别考勤系统,至少要两个模型协同工作。检测与对齐模型负责从画面里找到人脸并摆正,特征提取模型负责把人脸图像变成特征向量。常见搭配是:
- 检测与关键点:MTCNN 容易上手,自带五个人脸关键点;RetinaFace 的框和关键点更稳,但依赖更多。想快速验证流程,OpenCV DNN 自带的 SSD 人脸检测器也能占位。
- 特征提取:FaceNet 的 Inception-ResNet v1、InsightFace 里的 ResNet50(用 ArcFace 损失训练)都是常见选择。我一般直接加载在大型人脸数据集上训练好的权重,再在自己的考勤数据上微调,而不是从零训练。
特征维度一般取 512,人脸图像统一缩放到 112×112。答辩时最容易被问的一个问题就是"这两个模型为什么这么选",回答思路是:检测模型要的是"框得准、关键点稳",特征提取模型要的是"类内距离小、类间距离大",ArcFace 损失正好在训练时拉大了这个间隔。
2.3 考勤系统整体架构:从摄像头采集到记录落库的链路
从摄像头到数据库,完整链路是:
摄像头采集视频帧 → 人脸检测(间隔几帧做一次) → 关键点对齐 → 人脸裁剪与归一化 → 特征提取 → 与底库特征计算余弦相似度 → 最高分超过阈值才判定身份 → 写入考勤记录。
技术栈的常见做法是:训练阶段用 Python + PyTorch,推理阶段把训练好的模型导出成 ONNX,用 ONNX Runtime 加载,这样不依赖训练框架也能跑。界面可以用 PyQt 做桌面端,也可以用 Flask 做个简单 Web 端。环境配置上,Ubuntu 20.04 配 CUDA 加 GPU 版 PyTorch 是主流组合;手里只有 CPU 也能完成小规模训练和推理,只是训练速度要耐心。
整套链路跑通之后回头看,它就是一份完整的深度学习实战项目案例:图像采集、数据标注、卷积神经网络、模型导出部署、业务逻辑和数据库全都覆盖了,这也是这个选题在毕设里经久不衰的原因。
3. 数据准备与模型训练:把"认得脸"变成"认得人"
模型能跑通,不代表它认得你的考勤对象。毕业设计里最常见的翻车点,是给每个人只准备一张证件照,底库也只有一张,现场一测,相似度分数低到根本没法用。先解决数据,再谈训练。
3.1 考勤数据怎么收集:自采照片、公开数据集与数据增强
给每个考勤对象拍 10~20 张现场照片:正面、左右偏头、抬头低头、戴眼镜和不戴眼镜的都要有。如果摄像头是固定在门口或教室前部的,就按实际机位多录几段视频,把帧抽出来做底库。公开数据集(如 LFW、WebFace)的作用是让预训练模型见过足够多的人脸,但考勤识别的是一个固定小群体,最终效果取决于自采数据,公开数据集替代不了。
数据增强方面,随机亮度、对比度、水平翻转、小角度旋转都有用。注意两点:不要做随机裁剪,人脸区域一旦被裁掉就废了;增强强度要克制,把细节磨没了,现场识别会更差。数据目录的常见组织方式就是每人一个文件夹,文件名按"姓名_编号.jpg"排序,标签直接从文件夹和文件名读,简单直接,后面做特征底库和训练集划分都不需要额外维护一张映射表。
3.2 人脸检测与对齐:影响识别率的第一个环节
很多人把原图直接喂给识别模型,效果差就换模型,换完还是差,问题往往不在模型,而在对齐。对齐是先用关键点做人脸仿射变换,将眼睛、鼻子摆到标准位置,再裁剪成固定尺寸。人脸不摆正,同一个人的特征向量在不同角度下会漂移很远。
先用 OpenCV DNN 的人脸检测器做一个快速检测:
import cv2 import numpy as np # 加载人脸检测模型(Caffe 格式:prototxt + weights) net = cv2.dnn.readNetFromCaffe("deploy.prototxt", "res10_300x300_ssd_iter_140000.caffemodel") def detect_face(frame): h, w = frame.shape[:2] # 构造网络输入:缩放到 300x300,并做均值归一化 blob = cv2.dnn.blobFromImage(frame, 1.0, (300, 300), (104.0, 177.0, 123.0)) net.setInput(blob) detections = net.forward() boxes = [] for i in range(detections.shape[2]): conf = detections[0, 0, i, 2] if conf < 0.7: # 置信度阈值,低于此值直接丢弃 continue # 网络输出是归一化坐标,换算回原图像素 x1, y1, x2, y2 = (detections[0, 0, i, 3:7] * [w, h, w, h]).astype(int) boxes.append((x1, y1, x2, y2)) return boxes这段代码里有两个参数需要现场调:置信度阈值 0.7 是通用值,固定机位可以提高到 0.8,减少误检;如果摄像头离人远、人脸小,需要把输入分辨率从 300×300 调大,比如 640×640。检测只是第一步,检测框出来后还要用关键点做对齐,MTCNN 可以直接输出五个关键点,用两个眼睛坐标做简化对齐:
def align_face(img, left_eye, right_eye, output_size=(112, 112)): dx = right_eye[0] - left_eye[0] dy = right_eye[1] - left_eye[1] angle = np.degrees(np.arctan2(dy, dx)) # 绕左眼旋转,让两只眼睛保持水平 M = cv2.getRotationMatrix2D(tuple(left_eye), angle, 1.0) rotated = cv2.warpAffine(img, M, (img.shape[1], img.shape[0])) # 在旋转后的图上以左眼为起点裁剪 112x112 区域 crop = rotated[left_eye[1]:left_eye[1] + output_size[0], left_eye[0]:left_eye[0] + output_size[1]] return cv2.resize(crop, output_size)这是简化版对齐,真实工程会用五点关键点做相似变换,效果更稳,但理解原理靠这个足够。output_size 必须和训练时模型输入尺寸一致,这里取 112×112 对应 ResNet 类模型的输入约定。对齐这一步省不得,它是把"认得脸"变成"认得人"的第一个关卡。
3.3 训练策略与关键参数:用 ArcFace 损失微调特征提取模型
训练策略有两条路。路线 A:完全不训练,直接用预训练模型提取特征存底库。优点是省事,缺点是你很难在答辩时讲清楚"深度学习"体现在哪里,而且预训练模型对现场成像的适配度未必够。路线 B:在自采小数据上用 ArcFace 损失微调特征提取模型,这是推荐做法。
ArcFace 的核心是在余弦相似度上加一个角度间隔 m,让同类人脸的特征在角度空间里更紧凑。PyTorch 里一个可用的简化实现:
import torch import torch.nn as nn import torch.nn.functional as F class ArcFace(nn.Module): def __init__(self, feat_dim, num_classes, s=32.0, m=0.5): super().__init__() self.s = s # 特征向量模长的缩放因子 self.m = m # 角度间隔 # 权重矩阵的每一列代表一个类别的中心向量 self.W = nn.Parameter(torch.randn(feat_dim, num_classes)) def forward(self, x, labels): # x: [batch, feat_dim],训练时是模型输出的特征 # 对 x 和 W 都做 L2 归一化,只保留方向信息 w = F.normalize(self.W, dim=0) x_norm = F.normalize(x, dim=1) cos_theta = torch.matmul(x_norm, w) theta = torch.acos(cos_theta.clamp(-1 + 1e-7, 1 - 1e-7)) one_hot = torch.zeros_like(cos_theta) one_hot.scatter_(1, labels.view(-1, 1), 1.0) # 只在真实类别上加角度间隔 m,其他类别保持原余弦值 output = cos_theta.clone() output = torch.where(one_hot.bool(), torch.cos(theta + m), output) return output * self.s # 缩放后交给交叉熵损失这段代码注意几个要点。s 通常取 32,作用是放大分数让交叉熵训练更稳定;m 取 0.5,间隔过大训练难收敛,过小类内不够紧凑。feat_dim 要与特征提取模型输出的维度一致,ResNet50 最后接一个 512 维全连接层,这里就取 512。num_classes 是考勤总人数。
微调阶段的超参我一般这样设:优化器用 Adam 时学习率 0.001,用 SGD 时 0.01 加 momentum 0.9;weight decay(也就是 L2 正则化)设 0.0001;batch size 从 32 开始,GPU 显存不够就降到 16 或 8。训练轮数 5~10 轮就够,小数据集多训只会过拟合。这条路线本质是迁移学习:从公开数据集预训练权重起步,在自采考勤数据上做增量学习,新手跟着《动手深度学习》这类课程里讲迁移学习的章节走一遍,就能把原理在答辩时讲圆。
3.4 阈值与验证:怎么定"是不是这个人"
识别阶段和分类不同,模型输出的是特征向量,不是类别概率。推理时要把输入人脸的特征向量和底库中每个人的特征向量逐一算余弦相似度,取最高分;只有最高分超过阈值,才判定为这个人。阈值没有通用值,常见范围在 0.4~0.55 之间,必须用自己的数据实测。
具体做法是:把每个考勤对象现场拍的十几张照片里留出几张做验证,再找十来个不在考勤名单里的人当负样本。统计正样本的相似度分数和负样本的相似度分数,画两条分布曲线,阈值取在"负样本一个都进不来"的位置。这一步不做,现场调阈值就是玄学,遇到光线变化成绩忽高忽低也不知道是哪一环出了问题。
4. 考勤系统实现:把模型封装成能正常打卡的应用
模型训练完,剩下的是工程问题。考勤系统不只是一个识别脚本,它要把摄像头、模型、数据库、界面串成一条能稳定运行的流水线。
4.1 推理封装:从摄像头帧到识别结果
训练好的 PyTorch 模型导出成 ONNX 后,用 ONNX Runtime 加载推理,可以摆脱对训练框架的依赖,部署更轻。一个最小推理链路:
import cv2 import numpy as np import onnxruntime as ort # CUDA 不可用时自动回退到 CPU session = ort.InferenceSession( "face_encoder.onnx", providers=["CUDAExecutionProvider", "CPUExecutionProvider"] ) def get_embedding(align_face): # 对齐后人脸缩放 112x112,像素值归一化到 [-1,1] img = cv2.resize(align_face, (112, 112)).astype(np.float32) / 255.0 img = (img - 0.5) / 0.5 # HWC 转 CHW 并增加 batch 维 img = img.transpose(2, 0, 1)[None] feat = session.run(None, {session.get_inputs()[0].name: img})[0] # 特征归一化,方便后续用点积算余弦相似度 feat = feat / np.linalg.norm(feat) return feat.ravel() def match(feat, db_feats, threshold=0.45): # db_feats: [N, 512],每行是一个人的人脸特征 scores = np.dot(db_feats, feat) idx = int(np.argmax(scores)) if scores[idx] >= threshold: return idx, float(scores[idx]) return -1, 0.0这段代码有两点容易踩坑。一是预处理必须和训练时一致:训练时用 ImageNet 的 mean/std 归一化,推理时也用同一套;训练时是 112×112,推理时就不能送 160×160。二是在 4.1 里"特征归一化"很关键,ArcFace 系列的模型在训练时本身就做了归一化,推理时不归一化,余弦相似度会整体偏到不可用的区间。
提示:训练时用什么预处理,推理时就用什么预处理。这一条比任何模型调参都重要,改归一化参数等于重训一次。
4.2 考勤逻辑设计:签到、签退与防重复打卡
识别成功只是拿到了"这张脸是谁",考勤系统还要决定"能不能打这次卡"。最常见的逻辑坑是:一个人在摄像头前站了十秒,系统每秒识别两次,结果十秒内写了二十条签到记录。
解决方案是加时间窗口,同一人距离最后一次打卡小于一定秒数就忽略本次识别:
import sqlite3 import datetime def mark_attendance(conn, name, score, min_interval=60): now = datetime.datetime.now() # 查这个人最后一次打卡时间 cur = conn.execute( "SELECT time FROM attendance WHERE name=? ORDER BY time DESC LIMIT 1", (name,) ) row = cur.fetchone() if row: last = datetime.datetime.fromisoformat(row[0]) # 距上次打卡不足 min_interval 秒,视为重复打卡 if (now - last).total_seconds() < min_interval * 60: return False, "重复打卡" conn.execute( "INSERT INTO attendance(name, time, score) VALUES(?,?,?)", (name, now.isoformat(), score) ) conn.commit() return True, "打卡成功"min_interval 默认 60 秒,表示同一人在 60 秒内只记一次。这个值要按场景调:教室门口人多排队,可以降到 10 秒;办公室单人进出,60 秒更稳。要区分签到和签退,可以加一个 direction 字段,按时间窗口判定:上午到岗前两小时打卡算签到,下午离岗后打卡算签退。SQLite 写入要串行,多线程读没问题,写操作放进单线程队列,否则并发插入时可能丢记录。
4.3 界面与数据存储:考勤记录怎么组织、查看
数据存储用 SQLite 就够,两张表搞定。员工表保存姓名和特征,考勤记录表保存每一次打卡:
| 表 | 字段 | 说明 |
|---|---|---|
| employee | id, name, embedding | embedding 为 512 维特征向量,存 npy 文件路径或 BLOB |
| attendance | id, name, time, score, direction | score 记录识别分数,direction 区分签到/签退 |
特征底库的构建方式是:对每个员工的 10~20 张照片分别提取 embedding,取平均作为该员工的人脸特征向量,存进 employee 表。不要只存一张图的特征,均值特征对光线变化的鲁棒性好得多。识别时 match 函数读到的 db_feats,就是由这张表里的均值特征组成的数组。
界面用 PyQt5 做最小可行版本:左边是摄像头实时画面,画面里用框标出人脸并显示识别结果和分数;右边是今日考勤名单,识别成功且通过时间窗口后,名单里实时更新打卡时间。答辩时这个界面足够演示完整流程。追求 Web 化就用 Flask 加一个摄像头路由和考勤查询页面,但会引入视频流和前端状态管理的时间成本,非必要不增加。
5. 避坑/常见问题排查:训练到考勤现场的 5 个坑
这个项目最容易出问题的地方不在训练,而在训练和现场的连接处。下面五条都是高频踩坑记录,按现象、原因、解决的顺序写。
5.1 GPU 显存不够导致训练 OOM
现象:PyTorch 训练刚跑几个 batch,报错 CUDA out of memory,进程直接退出来。 原因:输入图像尺寸太大、batch size 过大、特征提取模型本身占显存,三者叠加很容易把 6G 的卡撑爆。 解决:先把 batch size 从 64 降到 16 或 8,这是性价比最高的第一步;然后把输入尺寸固定在 112×112,不要拿原始大图进网络。显存还是不够,再考虑用混合精度训练,PyTorch 的 autocast 可以直接套。不要一 OOM 就转 CPU 训练,人脸特征提取模型在 CPU 上训练极慢,小数据也要等很久。
5.2 直接拿预训练模型上考勤,现场"谁都不认识"
现象:用开源预训练权重提取底库特征,答辩现场识别教室里的人,相似度普遍低于阈值,没有一个人能打卡成功。 原因:预训练模型是在大型通用人脸数据集上训练的,对"这所学校教室里的摄像头实拍图"这种特定成像没有适配;更常见的问题是对齐没做,或者推理时的归一化方式和训练时不一致。 解决:先用考勤现场的摄像头录一段视频,离线跑一遍识别,看分数分布到底在什么区间。如果全体分数偏低,优先检查对齐和预处理;确认无误后,用每个人现场拍的 10~20 张照片重建底库特征;还是不行,再走 3.3 的微调路线。不要等到答辩当天才第一次在现场测试,这是血泪教训。
5.3 检测框乱跳,同一人识别结果时好时坏
现象:同一个人站在摄像头前,连续几十帧的结果在正确和错误之间横跳,分数忽高忽低。 原因:每一帧都做完整的人脸检测,检测框位置和大小每帧略有不同,导致对齐后的人脸有细微偏差,特征向量跟着漂移。 解决:不要每一帧都做检测和识别。常见做法是检测到人脸后,用简单的 IoU 匹配把同一个人的框盯住,每隔几帧做一次特征提取和比对;更简单的是只对连续 5 帧中稳定出现 3 帧以上的识别结果打一次签到。这样既减少运动模糊带来的误判,也降低 CPU/GPU 负担。
5.4 考勤时间记录与打卡逻辑时序错乱
现象:一个人 8:59 打卡成功,9:01 又签退成功,或者同一秒写入两条重复记录。 原因:打卡逻辑只看识别分数,没加时间窗口;多线程并发生成多条记录,写库时没有串行控制。 解决:按员工姓名取最后一条考勤记录,用时间间隔过滤重复打卡,4.2 的 mark_attendance 就是这个思路。多线程下写入操作放到单线程队列里,数据库连接本身也要加锁。界面显示"打卡成功"前,先确认记录真的写进数据库了,别让界面和数据库状态不一致。
5.5 模型导出 ONNX 后精度下降
现象:PyTorch 训练时分数分布正常,转 ONNX Runtime 推理后整体分数低了 0.05~0.1,有的正样本直接掉到阈值以下。 原因:导出时输入尺寸和 preprocess 方式不一致,或者动态维度设置导致插值方式变了。最常见的是训练时用的预处理是 RandomResizedCrop,推理时却只做了缩放到 112×112。 解决:导出前把输入尺寸固定成 112×112,用 torch.onnx.export 时设 dynamic_axes=None 或只配 batch 维动态。导出后用同一张图分别跑 PyTorch 和 ONNX,比较输出特征向量的每个维度,误差超过 1e-4 就回去检查图像预处理。这个逐元素比对习惯能省掉大量排查时间。
6. 进阶:考勤系统离线仿真、阈值调优与模型持续更新
模型和系统跑通之后,还有三件事决定它能不能真正投入使用。
6.1 用考勤视频离线仿真代替现场调参
现场调试的问题在于每次都要几个人站在摄像头前配合,效率低而且难以复现。常见做法是录一段 5 分钟的视频,模拟真实考勤:几个人进进出出、停留、聊天、背对镜头。然后用离线脚本按时间戳回放这段视频,跑完整识别和考勤逻辑,输出一张考勤表,和人工观察记录对比。关键是把 4.2 里的"当前时间"替换成视频帧的时间戳,这样就能在一天内反复测试各种阈值和逻辑。
6.2 阈值调优:画出相似度分布再定阈值
阈值不要拍脑袋,要在 6.1 的离线仿真上统计:考勤名单里的人每秒得到若干个正样本分数,非考勤的陌生人得到若干负样本分数。两条分布曲线放在一起,阈值取在"负样本一个都进不去、正样本漏掉最少"的位置。我做过几个考勤项目,这个交叉点大多落在 0.4~0.55 之间,但每次都不同,因为现场光照和摄像头型号直接改变分数基线。
6.3 模型持续更新:让系统越用越准
识别失败的照片不要删,那是数据资产。每周把"识别成功但分数偏低"和"被误识别成某人"的现场样本导出,人工复核后加入微调集。小样本微调时学习率降到 0.0001,在原来的 ArcFace 基础上多训 2~3 轮,只更新特征提取层的参数,避免灾难性遗忘。这套闭环跑起来后,系统会随使用时间越来越贴合现场。
我做完第一个考勤系统后养成的习惯是:任何模型或逻辑改动,都在离线考勤视频上先跑一遍,再去动现场的摄像头。人脸识别这个方向,最不值钱的是"模型能跑通",最值钱的是"在真实场景里的稳定性"。分数阈值、打卡时间窗、漏检率,都只能靠自己的数据去测,别人的参数只能当起点。希望帮到你。
本文还有配套的精品资源,点击获取