
简介一份基于深度学习的人脸识别考勤系统毕业设计项目面向计算机相关专业学生解决传统考勤效率低、接触式打卡等问题适合用于课程设计、毕业设计或人脸识别入门实践。压缩包共包含四十九个文件大小约十三点一五兆内有十七个Python脚本、九个编译文件、七张示例人脸图片、四个说明文档及模型权重文件等覆盖数据预处理、模型训练、人脸检测、特征提取与匹配、数据库存储等完整环节。已有二百三十一人学习下载。项目基于Python与常见深度学习库实现核心代码包含模型网络定义、训练脚本、评估脚本等并附有环境依赖、使用说明和常见问题汇总可直接运行或二次开发。通过该项目可系统掌握从数据集准备到模型部署的完整流程理解卷积神经网络结构设计、损失函数构造与人脸特征向量比对等关键技术是综合运用计算机视觉与深度学习知识的良好范例。1. 深度学习人脸识别考勤系统毕业设计里最容易被能跑骗过去的项目毕业设计里基于深度学习的人脸识别考勤系统是热门选题看起来有深度学习、有应用场景、能演示很多同学手里都有一个 xxx.zip。但真正把 zip 解压后能跑通的人不多因为这类系统的难点不在人脸识别这个黑匣子而在考勤这两个字——现场光线、角度、打卡逻辑、重复签到、数据存储全是坑。从模型选型、数据处理、训练调参到考勤逻辑与现场排错每条路都有扎扎实实的坑要填。这套方案适合正在选毕设题的学生也适合想用低成本方案给公司做考勤的开发者。照着下面每一章操作你能复现一个可以演示、也能经受现场提问的完整系统。2. 技术路线怎么定模型选型与考勤系统整体架构先说清楚一件事这类系统是典型的视觉识别 业务记录工程不是单纯把一个人脸识别模型跑起来就结束。考勤要的是谁、在什么时间、有没有合法地打上卡识别只是前半段。先定技术路线再谈代码能省掉后面大量返工。2.1 为什么考勤系统要选深度学习人脸识别与传统方案和刷卡方案的差距考勤场景里传统方案一直存在RFID 刷卡、指纹机、OpenCV 传统人脸识别。刷卡和指纹的问题是必须主动配合排队、忘带卡、指纹磨损都会让考勤记录失真。传统人脸识别LBPH、EigenFace在固定光照、正脸朝向的受控环境里能用但考勤现场的光线和角度不可控稍微侧脸或者背光识别结果就会翻车。深度学习人脸识别走的路线完全不同先用卷积神经网络检测出人脸框和关键点再把对齐后的人脸图像编码成一个固定维度的特征向量最后用向量相似度判断是不是同一个人。CNN 在这个过程里学到的是对光照、角度相对鲁棒的人脸表征这也是深度学习这三个字在标题里的落点。三套方案放在一起看更直观方案光照变化角度变化维护成本适合场景OpenCV 传统人脸LBPH/EigenFace差差低受控环境演示深度学习检测 特征提取较好较好中需标注与训练考勤、门禁商用现成人脸识别 API好好按量付费快速原型毕设不建议这里要补一个取舍考勤系统对实时性的要求远低于对正确性的要求摄像头前每秒抽 23 帧做人脸识别就足够但认错人比识别慢严重得多。所以后面所有参数设计都以别认错人、别漏记人为优先目标。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 考勤数据怎么收集自采照片、公开数据集与数据增强给每个考勤对象拍 1020 张现场照片正面、左右偏头、抬头低头、戴眼镜和不戴眼镜的都要有。如果摄像头是固定在门口或教室前部的就按实际机位多录几段视频把帧抽出来做底库。公开数据集如 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, s32.0, m0.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, dim0) x_norm F.normalize(x, dim1) 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.9weight decay也就是 L2 正则化设 0.0001batch size 从 32 开始GPU 显存不够就降到 16 或 8。训练轮数 510 轮就够小数据集多训只会过拟合。这条路线本质是迁移学习从公开数据集预训练权重起步在自采考勤数据上做增量学习新手跟着《动手深度学习》这类课程里讲迁移学习的章节走一遍就能把原理在答辩时讲圆。3.4 阈值与验证怎么定是不是这个人识别阶段和分类不同模型输出的是特征向量不是类别概率。推理时要把输入人脸的特征向量和底库中每个人的特征向量逐一算余弦相似度取最高分只有最高分超过阈值才判定为这个人。阈值没有通用值常见范围在 0.40.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, threshold0.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_interval60): 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 就够两张表搞定。员工表保存姓名和特征考勤记录表保存每一次打卡表字段说明employeeid, name, embeddingembedding 为 512 维特征向量存 npy 文件路径或 BLOBattendanceid, name, time, score, directionscore 记录识别分数direction 区分签到/签退特征底库的构建方式是对每个员工的 1020 张照片分别提取 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 直接拿预训练模型上考勤现场谁都不认识现象用开源预训练权重提取底库特征答辩现场识别教室里的人相似度普遍低于阈值没有一个人能打卡成功。 原因预训练模型是在大型通用人脸数据集上训练的对这所学校教室里的摄像头实拍图这种特定成像没有适配更常见的问题是对齐没做或者推理时的归一化方式和训练时不一致。 解决先用考勤现场的摄像头录一段视频离线跑一遍识别看分数分布到底在什么区间。如果全体分数偏低优先检查对齐和预处理确认无误后用每个人现场拍的 1020 张照片重建底库特征还是不行再走 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.050.1有的正样本直接掉到阈值以下。 原因导出时输入尺寸和 preprocess 方式不一致或者动态维度设置导致插值方式变了。最常见的是训练时用的预处理是 RandomResizedCrop推理时却只做了缩放到 112×112。 解决导出前把输入尺寸固定成 112×112用 torch.onnx.export 时设 dynamic_axesNone 或只配 batch 维动态。导出后用同一张图分别跑 PyTorch 和 ONNX比较输出特征向量的每个维度误差超过 1e-4 就回去检查图像预处理。这个逐元素比对习惯能省掉大量排查时间。6. 进阶考勤系统离线仿真、阈值调优与模型持续更新模型和系统跑通之后还有三件事决定它能不能真正投入使用。6.1 用考勤视频离线仿真代替现场调参现场调试的问题在于每次都要几个人站在摄像头前配合效率低而且难以复现。常见做法是录一段 5 分钟的视频模拟真实考勤几个人进进出出、停留、聊天、背对镜头。然后用离线脚本按时间戳回放这段视频跑完整识别和考勤逻辑输出一张考勤表和人工观察记录对比。关键是把 4.2 里的当前时间替换成视频帧的时间戳这样就能在一天内反复测试各种阈值和逻辑。6.2 阈值调优画出相似度分布再定阈值阈值不要拍脑袋要在 6.1 的离线仿真上统计考勤名单里的人每秒得到若干个正样本分数非考勤的陌生人得到若干负样本分数。两条分布曲线放在一起阈值取在负样本一个都进不去、正样本漏掉最少的位置。我做过几个考勤项目这个交叉点大多落在 0.40.55 之间但每次都不同因为现场光照和摄像头型号直接改变分数基线。6.3 模型持续更新让系统越用越准识别失败的照片不要删那是数据资产。每周把识别成功但分数偏低和被误识别成某人的现场样本导出人工复核后加入微调集。小样本微调时学习率降到 0.0001在原来的 ArcFace 基础上多训 23 轮只更新特征提取层的参数避免灾难性遗忘。这套闭环跑起来后系统会随使用时间越来越贴合现场。我做完第一个考勤系统后养成的习惯是任何模型或逻辑改动都在离线考勤视频上先跑一遍再去动现场的摄像头。人脸识别这个方向最不值钱的是模型能跑通最值钱的是在真实场景里的稳定性。分数阈值、打卡时间窗、漏检率都只能靠自己的数据去测别人的参数只能当起点。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。