
简介基于深度学习的人脸识别考勤系统源码是一套高分Python毕业设计项目评审分为98分主要面向计算机相关专业正在完成大作业、毕业设计的学生以及需要项目实战练习的学习者同样适用于课程设计与期末大作业场景。整套资源以zip压缩包形式提供共25个文件约239.41MB内部包含3个Python源码文件、15张界面背景与提示图片、字体与图标文件、配置文件、API文件以及docx使用手册等从代码实现到界面素材再到文档说明一应俱全。已有191人浏览学习。项目在导师指导下完成并获认可人脸识别模型的调用、考勤记录逻辑、用户界面布局等模块组织清晰可直接作为毕业设计或课程设计的完整参考方案帮助理解深度学习人脸识别考勤系统从数据到界面的整体实现流程适合用于二次开发与实战演练。1. 人脸识别考勤系统毕业设计里最值得复现的深度学习落地题人脸识别考勤系统在毕业设计里属于“看起来难、做起来顺”的题目摄像头抓一张脸和库里的人脸比对一下写一条打卡记录。但真正把它做成能跑通的 Python 源码你会碰到检测模型怎么选、特征向量怎么比、阈值怎么定、数据库怎么去重、界面和线程怎么联动一整套问题哪一环断了都跑不出演示效果。这篇文章按我自己的实操顺序把基于深度学习的人脸识别考勤系统从环境、模型、打卡逻辑到翻车排查完整讲一遍。适合打算拿这个方向做毕业设计或者想在本地搭一套可演示考勤 Demo 的从业者——跟着复现别只照抄要能讲清楚每段代码为什么这么写。2. 选型与环境搭建先立住检测、特征、存储、界面四个组件人脸识别考勤系统不是一个模型是一条流水线。很多人一上来就问“用什么网络训练人脸模型”这个提问方向就偏了。考勤场景下你要做的是“识别已经注册过的人”不是“从零训练一个分类器”。真正要立住的组件有四个人脸检测器、特征提取器、特征存储与比对逻辑、考勤业务界面。这一章先把前三个组件的选型理由讲透再给出能一次装齐的环境命令。2.1 两段式架构为什么检测和识别必须分开常见的人脸识别方案都是两段式先用目标检测模型从画面里找到人脸框再把框内图像送入特征提取模型得到一条向量。检测只回答“脸在哪”识别回答“这是谁”。两者分开的好处是检测模型可以换成更快或更准的版本识别模型不动反过来识别模型升级也不影响检测。很多毕设作品只用 OpenCV 的 Haar Cascade 做检测再用 LBPH 或直方图比对做识别。这个路线不是深度学习方案答辩时很容易被问住Haar 是手工设计的滑动窗口特征LBPH 本质是纹理直方图和“基于深度学习”不沾边。既然标题写的是深度学习检测和识别至少一边必须用深度模型。用 dlib 的 HOG 检测也同理HOG 是人工特征过不了评委那关。我的选择是检测用 MTCNN识别用 FaceNet 的 Inception ResNet v1。检测负责输出人脸框和关键点特征模型负责把人脸对齐后变成一条 512 维向量。下面这张表是当时对比过的检测方案检测方案是否深度学习CPU 单帧耗时约特点与坑Haar Cascade否10ms 级侧脸、遮挡、暗光下漏检严重dlib HOG否20ms 级对偏转角度敏感适合正脸MTCNN是60-120ms带关键点回归能对齐适合毕设RetinaFace是150ms 以上精度高但依赖更多CPU 吃力MTCNN 输出人脸框和五官关键点FaceNet 把关键点用来做仿射对齐。这个组合在 720p 分辨率下能跑到接近实时而且是同一个库提供的依赖省心。2.2 模型选型为什么 MTCNN 加 FaceNet 在这里最稳用 facenet-pytorch 库一个 pip 包同时提供 MTCNN 和 InceptionResnetV1 预训练模型不需要自己去下两个来源的权重再对齐版本。深度学习人脸识别最常见的翻车现场就是版本地狱检测模型是 TensorFlow 的特征模型是 PyTorch 的数据格式还要自己转光调通就耗掉两三天。facenet-pytorch 内部已经把 MTCNN 的输出按 InceptionResnetV1 需要的 160x160 尺寸处理好省掉了最脏的预处理代码。很多人问为什么不用 ArcFace 或 CosFace。ArcFace 精度更高但需要先下载权重再封装模型还要自己处理输入归一化对刚接触深度学习的同学来说封装成本偏高。FaceNet 的 InceptionResnetV1 在 vggface2 上预训练过类内距离紧凑考勤这种小规模封闭集够用。另一个常见问题是“要不要自己训练”不用。预训练模型已经在百万级人脸上学到通用的人脸表示考勤只需要提取特征后做距离比对除非你面对的全是极暗光、大角度俯拍这种极端分布否则不需要微调。还有一点容易忽略特征提取模型输出的是 512 维浮点向量不是分类概率。这意味着底层库的人脸向量不能直接用“类别”必须自己做距离检索。很多新手在这里误以为 FaceNet 模型可以直接输出人名这也是答辩时容易露馅的地方。2.3 环境安装与验证一条命令装齐依赖环境我建议用虚拟环境隔离不要在全局 Python 里直接装否则后面装 torch 和 opencv 时会污染系统环境。用 vscode 配 Python 环境时记得先激活虚拟环境再选解释器不然终端里敲 python 用的还是全局版本。python -m venv venv source venv/bin/activate # Windows 是 venv\Scripts\activate pip install --upgrade pip pip install opencv-python pillow numpy scipy pip install facenet-pytorch pip install torch torchvision这段命令的逻辑是先建虚拟环境隔离项目依赖再装图像处理相关库。opencv-python 提供摄像头读取和图像格式转换Pillow 是 facenet-pytorch 的图像接口依赖scipy 用来做余弦距离计算。facenet-pytorch 装完会自动带徒 torchvision 的依赖声明但 torch 本体比较大单独列出来装是为了让你能按自己机器的 CUDA 版本来选。装完先别急着写业务代码跑一段验证脚本确认模型能加载权重能下载import torch from facenet_pytorch import MTCNN, InceptionResnetV1 print(torch:, torch.__version__) device cuda if torch.cuda.is_available() else cpu print(device:, device) mtcnn MTCNN(image_size160, margin0, devicedevice) resnet InceptionResnetV1(pretrainedvggface2).eval().to(device) dummy torch.randn(1, 3, 160, 160) with torch.no_grad(): emb resnet(dummy) print(embedding shape:, emb.shape) # 期望是 [1, 512]这里的 MTCNN 参数里image_size160 必须和 InceptionResnetV1 的输入尺寸一致margin0 意思是检测到的人脸不要额外外扩device 自动选了 CPU 或 GPU。第一次运行 InceptionResnetV1(pretrainedvggface2) 会联网下载约 100MB 的权重文件卡住的话手动把权重放到 torch 的缓存目录里再跑。emb.shape 打印出 [1, 512] 说明特征提取器工作正常。2.4 数据集准备先拍 20 张照片再谈“训练”考勤系统要准备的是注册照片库不需要准备训练集。每位员工拍 1 到 3 张正脸照要求光线正常、五官完整、没有夸张表情。照片放到固定目录按人名分文件夹后面注册脚本会自动遍历。data/ raw/ zhangsan/ 001.jpg 002.jpg lisi/ 001.jpg这里有个很容易踩的坑注册照片如果是在教室、宿舍这种背光环境拍的人脸会偏暗MTCNN 很可能检测不到或者检测到了但特征质量很差导致后面死活认不出来。我的做法是拍完后先本地预览一遍用下一章的特征提取脚本把每张照片过一遍凡是返回 None 的照片直接删掉重拍。宁可用质量高的 1 张也不要凑 5 张模糊图。3. 核心代码MTCNN 检测与 FaceNet 特征提取把一张脸变成 512 维向量这一章直接给能跑的代码。核心目标是把摄像头的一帧画面处理成一条 512 维的浮点向量然后和一个已注册向量做距离比对。整章围绕三个函数展开get_embedding、register、recognize。3.1 准备一个人脸特征提取函数输入一帧输出一条向量import cv2 import numpy as np import torch from PIL import Image from facenet_pytorch import MTCNN, InceptionResnetV1 from scipy.spatial.distance import cosine device cuda if torch.cuda.is_available() else cpu mtcnn MTCNN(image_size160, margin0, min_face_size30, devicedevice) resnet InceptionResnetV1(pretrainedvggface2).eval().to(device) def get_embedding(frame_bgr): rgb cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB) pil_img Image.fromarray(rgb) face_tensor mtcnn(pil_img) if face_tensor is None: return None with torch.no_grad(): emb resnet(face_tensor.unsqueeze(0)).detach().cpu().numpy()[0] norm np.linalg.norm(emb) if norm 0: return None return emb / norm这段代码把一整帧 BGR 图像转成 RGB因为 OpenCV 读出来的通道顺序和 facenet-pytorch 预期不一致。转成 PIL.Image 是因为 MTCNN 内部按 PIL 处理图像。face_tensor 已经是 160x160 的对齐后人脸unsqueeze(0) 给它加一个 batch 维度再送入 resnet。最后对向量做了 L2 归一化这样后面用欧氏距离和余弦距离的趋势是一致的阈值更好描述。注意 min_face_size30 表示小于 30 像素的人脸直接忽略。摄像头离人 1 米左右时30 像素以下的框基本都是远处路过的行人不可能是来打卡的人。这个参数要跟着摄像头分辨率和安装距离调拉高可以减少误检拉低能识别更远的人脸。3.2 注册流程把照片库变成可比对的底库from pathlib import Path db {} for person_dir in Path(data/raw).iterdir(): if not person_dir.is_dir(): continue name person_dir.name embeddings [] for img_path in person_dir.glob(*.jpg): frame cv2.imread(str(img_path)) emb get_embedding(frame) if emb is not None: embeddings.append(emb) if embeddings: db[name] np.mean(embeddings, axis0) db[name] db[name] / np.linalg.norm(db[name]) print(fregistered {name}: {len(embeddings)} images) np.savez(data/registered.npz, **db)注册逻辑是遍历 data/raw 下每个子目录以目录名作为人名提取该目录里所有照片的向量最后取平均。同一人多张照片取平均的好处是能抵消表情、角度带来的轻微偏移。如果照片里有极端角度平均后反而会拉到中间位置所以前一步照片质量把关很重要。np.savez 把字典直接序列化成 npz 文件key 就是人名value 是 512 维向量。加载时用 np.load(data/registered.npz) 就能拿回字典。注意不要用 pickle 存 numpy 数组跨 Python 小版本时容易读不出来。3.3 实时识别摄像头帧循环与最相似检索def recognize(frame, db, threshold0.25): emb get_embedding(frame) if emb is None: return None, None best_name None best_dist float(inf) for name, db_emb in db.items(): dist cosine(emb, db_emb) if dist best_dist: best_dist dist best_name name return best_name if best_dist threshold else None, best_dist cap cv2.VideoCapture(0) while True: ok, frame cap.read() if not ok: break name, dist recognize(frame, db) if name: cv2.putText(frame, f{name} {dist:.2f}, (30, 60), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 0), 2) cv2.imshow(attendance, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()recognize 函数从数据库字典里遍历所有人用 scipy 的 cosine 函数算余弦距离取距离最小的人作为候选如果最小距离超过了 threshold宁可认为是陌生人。这里 cosine 距离和相似度是反的距离越接近 0 越像同一个人距离接近 1 说明两者方向差异很大。可视化界面里用 OpenCV 的 putText 把名字和距离打在画面上方便答辩截图。一个重要的性能细节get_embedding 里每次都在创建 PIL.ImageMTCNN 内部也在做金字塔缩放720p 一帧大约耗时 60 到 120ms。连续视频流里如果处理不过来可以跳帧处理比如每处理 2 帧再显示 1 帧不影响演示效果。3.4 相似度阈值怎么用一组自拍实测定阈值而不是拍脑袋阈值是人脸识别考勤系统里最“玄学”的参数。设小了真人来了也认不出设大了隔壁工位的同事互相打卡成功。正确做法是拿你自己的数据实测距离分布。same_dists [] diff_dists [] # 同一个人不同照片之间的距离 for person_dir in Path(data/raw).iterdir(): frames [cv2.imread(str(p)) for p in person_dir.glob(*.jpg)] embs [get_embedding(f) for f in frames if get_embedding(f) is not None] for i in range(len(embs)): for j in range(i 1, len(embs)): same_dists.append(cosine(embs[i], embs[j])) # 不同人照片之间的距离 all_people list(Path(data/raw).iterdir()) for i in range(len(all_people)): for j in range(i 1, len(all_people)): e1 db[all_people[i].name] e2 db[all_people[j].name] diff_dists.append(cosine(e1, e2)) print(same person:, np.mean(same_dists), np.max(same_dists)) print(diff person:, np.mean(diff_dists), np.min(diff_dists))打印出来的数据会给你一个直观印象同一个人不同照片的余弦距离通常在 0.1 到 0.25 之间不同人之间的距离通常在 0.4 到 0.7 之间。阈值取“类内距离上界”和“类间距离下界”的中点最稳比如 0.3。如果你发现自己的类内距离都超过 0.35那大概率是照片质量问题不是阈值问题先回头修照片不要硬调阈值。4. 考勤业务逻辑从识别结果到数据库打卡记录注意三个细节识别出“这是张三”只是第一步。考勤系统要解决的是张三今天打了几次卡、有没有迟到、记录存哪、怎么避免重复打卡。这一章讲数据库设计与打卡主流程顺便说一下这套 Python 方案和商用门禁机的边界。4.1 为什么必须有三张表员工表、打卡表、日志表的职责划分考勤系统不要只搞一张表至少拆成员工表和打卡表。日志表建议要调试排错时能少走很多弯路。CREATE TABLE employees ( id INTEGER PRIMARY KEY AUTOINCREMENT, emp_no TEXT UNIQUE NOT NULL, name TEXT NOT NULL, embedding BLOB ); CREATE TABLE attendance ( id INTEGER PRIMARY KEY AUTOINCREMENT, emp_no TEXT NOT NULL, check_date TEXT NOT NULL, check_time TEXT NOT NULL, status TEXT, UNIQUE(emp_no, check_date) ); CREATE TABLE log ( id INTEGER PRIMARY KEY AUTOINCREMENT, ts TEXT DEFAULT (datetime(now, localtime)), level TEXT, msg TEXT );employees 表里 embedding 字段用 BLOB 存 npz 的向量字节串但实际跑起来你会发现把 512 维向量存在 SQLite 里再读出来做比对效率不高也不方便。更快的做法是启动时把 npz 底库全部加载进内存字典SQLite 里只存员工编号和姓名向量文件单独存在磁盘上。attendance 表用 UNIQUE(emp_no, check_date) 约束同一人同一天只能有一条记录这个约束比在业务代码里先查再写更安全。4.2 打卡主流程识别成功、判重、写入一条记录import sqlite3 from datetime import datetime def punch_card(name): conn sqlite3.connect(attendance.db) cur conn.cursor() cur.execute(SELECT emp_no FROM employees WHERE name ?, (name,)) row cur.fetchone() if row is None: conn.close() return unknown user emp_no row[0] today datetime.now().strftime(%Y-%m-%d) now datetime.now().strftime(%H:%M:%S) cur.execute( SELECT id FROM attendance WHERE emp_no ? AND check_date ?, (emp_no, today) ) if cur.fetchone(): conn.close() return already punched status normal if now 09:00:00: status late cur.execute( INSERT INTO attendance (emp_no, check_date, check_time, status) VALUES (?, ?, ?, ?), (emp_no, today, now, status) ) conn.commit() conn.close() return ok: status这段逻辑按“先查员工编号、再查当天是否已有记录、最后插入”的顺序执行。INSERT 之前先 SELECT 判断去重这是业务层的第一道防线表结构里的 UNIQUE 约束是第二道防线。两重保险在单线程下足够多线程并发时只看 SQLite 的报错信息后面的避坑章节会展开。4.3 迟到早退与补卡判定口径要提前定死迟到判定用字符串比较 “09:00:00” 在 HH:MM:SS 格式下是字典序和真实时间序一致的所以能直接比字符串。但早退不能只靠打卡时间算早退要等到下班时再打一次卡才知道。常见做法是考勤系统只负责记录两次打卡时间迟到、早退、缺卡都交给统计脚本事后算。你现在就要定死的口径是一天打几次卡算正常跨天班怎么算补卡由谁审批。毕设答辩时最容易被追问的就是这三个口径提前想清楚比优化模型更省时间。4.4 本地界面与商用门禁机的边界界面用 Tkinter 或 PyQt 都能做Tkinter 零额外依赖PyQt 好看但要多装一个包。界面里至少要有摄像头画面、识别状态、打卡结果、考勤记录列表四个区域。不建议在这一步花太多时间美化先跑通业务闭环再换界面库。商用的人脸识别门禁机和你这套 Python 系统本质差别在硬件和算法服务化门禁机有红外补光、宽动态摄像头、活体检测芯片应对暗光和照片攻击的能力强很多。Python 方案的优势是完全可定制比如你可以随时改打卡规则、接自己的数据库、批量导入导出。边界要心里有数演示时别承诺“能替代门禁机”而是说“这是一套可二次开发的考勤门禁系统原型”。5. 避坑人脸识别考勤系统最常见的 5 个翻车现场这一章全是血泪经验。每一条都是我在调这类系统时真实踩过、或者帮别人排查过的现象按“现象、原因、解决”三段写。5.1 摄像头打开失败或黑屏先看占用再看索引现象VideoCapture(0) 返回 True但 read() 一直返回黑帧或者直接报 PermissionError。原因Windows 下摄像头被其他应用占用比如微信、Teams、OBSmacOS 下终端进程没有摄像头权限笔记本在虚拟机里没有把 USB 摄像头透传给虚拟机。另一个隐藏坑是 VideoCapture 传入的索引写死为 0但系统里摄像头设备被别的应用占用了索引导致你的进程拿不到流。解决先彻底退出占用摄像头的应用然后写一个小的枚举脚本尝试索引 0、1、2打印每个设备是否可读。macOS 上第一次运行要在“系统设置—隐私与安全性—摄像头”里给 Python 解释器打勾。如果接入的是 USB 摄像头建议用 VideoCapture(1) 测试内置摄像头和外接摄像头的设备排序往往不是直觉顺序。最后拿到 cap 后加一句 time.sleep(1) 再 read给摄像头自动曝光一点时间。5.2 认不出人阈值不是拍脑袋定的现象注册照片里能识别一到摄像头前就认不出来或者更糟糕张三刷成了李四。原因注册照片和现场光线差异太大导致特征向量漂移或者阈值设得太严类内距离稍微一波动就拒绝。很多同学阈值直接抄网上代码里的 0.5但网上的 0.5 可能是针对欧氏距离的你用的是余弦距离两者不能混用。解决按 3.4 节的方法实测类内距离和类间距离分布阈值取两者的中间地带。如果实测发现类内最大距离已经超过类间最小距离说明样本质量有问题不是阈值能救的。还有一个补救办法为同一人多注册几张不同光线下的照片注册时取平均向量能显著压缩现场光线变化带来的漂移。5.3 多人同框时把背景墙也当成人脸现象摄像头前面路过两个人识别结果在两个人名之间疯狂跳变有时墙壁纹理或海报人像也被当成人脸。原因MTCNN 默认 keep_allFalse 只会返回置信度最高的一张脸但置信度最高不等于你要打卡的那个人。如果画面里出现海报、屏幕里的人像这些也会产生较高置信度的检测框。keep_allFalse 时MTCNN 并没有告诉你“只有一个人”它只是返回了得分最高的那个。解决现场场景加一个 ROI 区域只在画面中间某个矩形区域内做检测并用代码过滤人脸框中心点是否落在 ROI 内。另外可以改 MTCNN 的 thresholds 参数把三个检测阶段的置信度阈值从默认的 [0.7, 0.8, 0.9] 拉高到 [0.8, 0.85, 0.9]背景误检会明显减少。多人同时出镜的场景要说明这是单目标考勤机合理预期是“一人打卡他人回避”。5.4 背光和暗光检测率骤降先别换模型做预处理现象白天靠窗位置一切都好傍晚开灯后检测率从 90% 掉到 40%逆光时整张脸是黑的MTCNN 直接返回 None。原因MTCNN 在极低照度下人脸边缘和五官细节丢失检测网络置信度骤降。摄像头自动增益把背景调亮人脸欠曝这种情况不是模型问题是成像质量问题。解决先做亮度归一化再送模型。用 OpenCV 的 CLAHE 对 Y 通道做自适应直方图均衡只处理亮度通道不要动色彩不然肤色会失真。具体做法是把 BGR 转 YUV对 Y 通道执行 clahe.apply()再合并回 YUV 转回 BGR。如果环境光线实在差优先调摄像头曝光参数cap.set(cv2.CAP_PROP_EXPOSURE, -6) 这类参数按你的摄像头来试。最稳妥的方案还是给考勤机加一个补光灯20 块的 USB 补光灯比任何代码都有效。5.5 SQLite 提示 database is locked多线程写入要开 WAL现象界面线程和识别线程同时跑打卡功能偶尔报 sqlite3.OperationalError: database is locked多跑几次打卡记录丢失。原因SQLite 默认在并发写时会锁整个数据库文件。Python 的 sqlite3 连接默认 check_same_threadTrue如果识别线程里创建的连接被主线程拿去用还会直接报 “SQLite objects created in a thread can only be used in that same thread”。解决第一每个线程创建自己的连接不要跨线程共享连接对象第二连接时传 check_same_threadFalse 并开启 WAL 模式第三写操作加简单重试。WAL 模式让读写不互斥大幅降低锁冲突conn sqlite3.connect(attendance.db, check_same_threadFalse) conn.execute(PRAGMA journal_modeWAL;)6. 验证与进阶用一段视频做离线回归再给系统加个眨眼活体检测6.1 用录制好的视频代替摄像头做离线回归调试期不要一直盯着摄像头否则每次改动都要真人站在那里试效率太低。我一般先用手机录一段 30 秒的测试视频正脸靠近、左右偏头、走到摄像头前打卡、转身离开。然后把代码里的 VideoCapture(0) 换成视频文件路径跑完整流程把每一帧的识别结果输出到 CSV和人工标注的期望结果对比。python evaluate.py --video test_zhangsan.mp4 --db data/registered.npz --csv result.csv统计两个指标有人脸的帧里识别出正确人名的比例以及错误识别帧数。如果正确率低于 70%先看是哪一段视频拉低的通常是转身瞬间侧脸导致。这能帮你做出“系统在正脸朝向时的准确率是多少”这样有依据的结论答辩时比口头说“效果很好”有说服力得多。6.2 三个低成本提升方向多角度注册、眨眼活体、阈值表第一个方向是给每位员工注册多角度照片不只正脸左右各 15 度再拍两张平均出来的特征向量对角度变化的鲁棒性会明显提升。第二个方向是加活体检测防止用照片或手机屏幕打卡。低成本做法是用 dlib 或 MediaPipe 提取眼睛关键点算 EAR 眼纵横比EAR (p2-p6 距离 p3-p5 距离) / (2 × p1-p4 距离)闭眼时 EAR 显著下降。连续 3 帧 EAR 低于阈值再判定出现一次眨眼在识别结果基础上要求“检测到眨眼才算打卡成功”。这个方案能挡住打印照片攻击但挡不住翻拍视频攻击答辩时如实说明就好。第三个方向是把阈值按时段或光线条件做成查表。白天、傍晚、晚上各测一组类内距离存成配置切到对应时段自动换阈值。这个做法很土但在没有补光灯的场景里非常实用。我给自己定的习惯是每次改完阈值或模型参数都留一份当时的距离分布输出截图下次翻车时对比两个分布的偏移量十次里有八次能直接定位是光线变了还是模型权重换了。这套系统做到能稳定识别、能判重打卡、能讲清阈值和边界就已经超过大多数拿来就跑不通的源码。希望帮到你。本文还有配套的精品资源点击获取