ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于深度学习的人脸识别考勤系统设计与实现

基于深度学习的人脸识别考勤系统设计与实现 简介基于深度学习的人脸识别考勤系统毕设项目面向计算机相关专业正在准备毕业设计的学生以及需要项目实战练习的开发者适用于毕业设计、课程设计、期末大作业等场景。项目以人脸录入、人脸识别、考勤管理、课堂管理、班级管理、日志管理为核心功能采用基于深度学习的FaceNet算法提取人脸特征完成身份判断与考勤记录。压缩包共2000个文件其中包含1957个Python脚本覆盖后端逻辑与算法实现11个PDF文档可作为论文撰写参考HTML/CSS/JS文件支撑简单的Web展示界面另附文本说明、Shell脚本、XML/JSON配置等辅助文件整体大小84.31MB。所有源码经过严格调试下载即可运行既可直接作为毕设提交也适合二次开发与深入学习。已有696人学习下载是一份完整的高分项目参考方案。1. 人脸识别考勤系统为什么值得当作深度学习毕设来做很多本科生选毕业设计时会把“人脸识别考勤系统”和“图书管理系统”“电商网站”归为一类觉得无非是调一个接口、连一个数据库、做一个界面然后包装成“基于深度学习”。但真正动手之后才发现难点不在“考勤”而在“人脸识别”那一层光线变化、角度偏移、口罩遮挡、发型改变、手机照片翻拍攻击每一个真实场景中的问题都会让识别准确率从演示时的 98% 掉到实际可用的 60%。这门课设或毕设的核心价值在于它把“深度学习模型训练”和“软件系统工程”两件事同时逼出来了。你既要会用 PyTorch 或 TensorFlow 训练人脸特征提取模型也要能设计一套可靠的考勤业务闭环——从摄像头采集帧、人脸检测对齐、特征提取、相似度比对再到数据库写入、缺勤统计、可视化展示。这也是为什么企业招聘时看到这类题目会默认你具备了“算法落地”的基本认知而不只是在 CSDN 上跑通过一个开源项目。本文就按“最小可用版本 → 识别与考勤打通 → 工程化优化 → 冲刺高分细节”这条路径把一个可复现的基于深度学习的人脸识别考勤系统讲透源码对应 Python 3.8、PyTorch 生态适合直接作为毕业论文核心实现框架。2. 用 Python 搭建人脸识别考勤系统的最小实现选型与代码骨架2.1 为什么首选 MTCNN FaceNet而不是直接调云端 API做本科毕设首先要考虑的是“在论文里能把原理写清楚”其次才是效果。市面上像百度 AI、阿里云人脸识别 API 的效果确实很好但它们的核心模型是黑盒你的论文里只能写“调用第三方接口”这会让“基于深度学习”这个题目失去立足点。所以我一般建议用开源模型人脸检测用 MTCNN人脸特征提取用 FaceNetInception ResNet v1两者都有成熟的 PyTorch 实现模型文件总共不到 100MB普通 CPU 也能跑但 GPU 能明显提升训练和推理速度。MTCNN 分三个阶段P-Net、R-Net、O-Net先通过候选框快速筛选人脸区域再逐步精细化最后输出人脸的 5 个关键点左眼、右眼、鼻尖、左嘴角、右嘴角。这 5 个关键点在考勤场景中特别重要因为我们需要对检测到的人脸做仿射变换affine transformation把人脸矫正到标准位置。FaceNet 的核心理念是把人脸图像映射到一个 128 维或 512 维的嵌入空间在这个空间里同一个人的不同照片距离近不同人的照片距离远。考勤比对时只需要计算特征向量之间的欧氏距离或余弦相似度即可。# requirements.txt 核心依赖 # python3.8 # pytorch1.10 # torchvision0.11 # facenet-pytorch2.5.3 # opencv-python4.8.0 # numpy1.21 # pandas1.5 # Pillow9.0 # Flask2.2.0 (用于后端服务)这里把依赖锁得比较细是为了避免在写论文的“环境配置”章节时出现版本不兼容的尴尬。facenet-pytorch 这个库封装了 MTCNN 和 FaceNet 的推理逻辑不需要自己实现复杂的网络结构。如果你的学校对“是否允许使用第三方库”有要求可以说明 MTCNN 和 FaceNet 是公开的深度学习模型结构你使用的是其预训练权重并结合自己的数据集进行微调这属于深度学习模型应用而非接口调用。2.2 注册阶段为每个学生生成 128 维人脸特征向量考勤系统的第一步不是打卡而是录入人脸。通常的做法是让每个学生正对摄像头拍摄 10 到 20 张照片然后批量生成特征向量取平均作为该学生在系统中的“标准人脸”。这样做的目的是降低单张照片的光线、表情和角度噪声。如果只拍一张之后打卡时稍微侧脸就可能导致误判。注册流程的核心代码如下import torch from facenet_pytorch import MTCNN, InceptionResnetV1 from PIL import Image import numpy as np import os device torch.device(cuda:0 if torch.cuda.is_available() else cpu) mtcnn MTCNN( image_size160, margin32, keep_allFalse, # 每张图只取置信度最高的一张脸 devicedevice ) resnet InceptionResnetV1(pretrainedvggface2).eval().to(device) def get_face_embedding(img_path): 输入一张图片路径返回 512 维人脸特征向量numpy 格式 如果图片中未检测到人脸返回 None img Image.open(img_path).convert(RGB) # mtcnn 内部会完成检测 对齐 缩放为 160x160 face_tensor mtcnn(img) if face_tensor is None: return None # 增加 batch 维度: (1, 3, 160, 160) face_tensor face_tensor.unsqueeze(0).to(device) with torch.no_grad(): embedding resnet(face_tensor).cpu().numpy()[0] # 归一化方便后续用余弦相似度比对 embedding embedding / np.linalg.norm(embedding) return embedding参数说明image_size160是 FaceNet 要求的输入尺寸不能随意修改margin32表示在检测到的人脸框外再扩展 32 像素把额头和下巴边缘包含进来防止裁剪时把关键器官切掉keep_allFalse在考勤场景下是有意为之因为打卡时人脸应该是画面中最大、最正的一个如果画面里同时出现多个同学的脸系统只对置信度最高的人脸做识别避免误打卡。如果你想在论文里做“多人同时考勤”的进阶功能可以把keep_allTrue但后续的比对逻辑会复杂不少。批量注册时推荐把每个人的多张照片特征取平均然后存入数据库或一个本地的.npy文件。如果某张照片没检测到人脸直接跳过不要用空白向量污染特征库。我在实际测试中遇到比较多的坑是同学们用手机拍的照片可能是 HEIC 格式PIL 直接打不开所以注册端我一般会加一个格式转换函数统一转成 JPEG。2.3 打卡阶段实时帧采集与相似度计算注册阶段是离线批量处理打卡阶段则是实时推理。常见做法是用 OpenCV 打开摄像头循环读取帧每帧先做一次快速检测。这里有一个性能权衡MTCNN 在第一阶段 P-Net 的运算量相对较小所以可以先每 3 帧做一次全图检测检测到人脸后再做特征提取这样可以减轻 CPU 的负担。import cv2 def recognize_face_from_frame(frame, threshold0.75): 从单帧图像中识别出最匹配的学生信息 threshold: 余弦相似度阈值低于该值视为未注册 # BGR 转 RGB因为 PIL/PyTorch 使用 RGB 通道顺序 rgb_frame cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # 转为 PIL Image pil_img Image.fromarray(rgb_frame) face_tensor mtcnn(pil_img) if face_tensor is None: return None, 0.0 face_tensor face_tensor.unsqueeze(0).to(device) with torch.no_grad(): embed resnet(face_tensor).cpu().numpy()[0] embed embed / np.linalg.norm(embed) best_name None best_score -1.0 # 与注册库中的所有特征做余弦相似度比对 for student_id, ref_embed in face_db.items(): score np.dot(embed, ref_embed) if score best_score: best_score score best_name student_id if best_score threshold: return None, best_score return best_name, best_score这里使用余弦相似度而不是欧氏距离是因为打卡场景中同一个学生的注册特征是多张照片的平均向量而实时帧受光线影响的变化主要体现在“长度”上方向相对稳定。余弦相似度对向量长度不敏感所以更鲁棒。阈值threshold0.75是经验值需要根据实际采集设备调整。建议注册阶段就额外留一部分照片做验证集画出相似度分布的直方图取两个分布交叉点作为阈值。3. 从识别到考勤数据库设计、打卡逻辑与防重复提交3.1 考勤系统的数据库表结构设计人脸识别只是前端感知层考勤系统的核心是与课程表、学生信息、打卡时间关联起来的关系型数据模型。很多本科生的代码里只有一张“打卡记录表”这是远远不够的因为论文里要体现功能完整性和规范性。我通常设计四张核心表student学生信息表、course课程表、attendance_record考勤记录表、course_student课程与学生关联表。CREATE DATABASE IF NOT EXISTS attendance_system DEFAULT CHARSET utf8mb4; USE attendance_system; CREATE TABLE student ( student_id VARCHAR(20) PRIMARY KEY COMMENT 学号, name VARCHAR(50) NOT NULL COMMENT 姓名, class_name VARCHAR(50) DEFAULT NULL COMMENT 班级, face_embedding LONGTEXT NOT NULL COMMENT 512维特征向量的Base64编码, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT学生信息表; CREATE TABLE course ( course_id INT AUTO_INCREMENT PRIMARY KEY, course_name VARCHAR(100) NOT NULL, teacher_name VARCHAR(50) NOT NULL, start_time TIME NOT NULL COMMENT 上课时间, end_time TIME NOT NULL COMMENT 下课时间, location VARCHAR(100) DEFAULT NULL COMMENT 教室 ) ENGINEInnoDB COMMENT课程表; CREATE TABLE course_student ( id INT AUTO_INCREMENT PRIMARY KEY, course_id INT NOT NULL, student_id VARCHAR(20) NOT NULL, UNIQUE KEY uk_course_student (course_id, student_id), FOREIGN KEY (course_id) REFERENCES course(course_id), FOREIGN KEY (student_id) REFERENCES student(student_id) ) ENGINEInnoDB COMMENT选课关联表; CREATE TABLE attendance_record ( record_id BIGINT AUTO_INCREMENT PRIMARY KEY, student_id VARCHAR(20) NOT NULL, course_id INT NOT NULL, check_in_time DATETIME NOT NULL COMMENT 打卡时间, face_score DECIMAL(5, 4) NOT NULL COMMENT 人脸识别最高相似度, is_late TINYINT DEFAULT 0 COMMENT 1迟到 0正常, is_valid TINYINT DEFAULT 1 COMMENT 1有效记录 0重复打卡被过滤, UNIQUE KEY uk_student_course_date (student_id, course_id, check_in_time), INDEX idx_time (check_in_time) ) ENGINEInnoDB COMMENT考勤记录表;参数说明与设计决策face_embedding使用LONGTEXT而不是BLOB是因为在实际教学中直接通过 SQL 查看 Base64 字符串更方便调试Python 端读取后base64.b64decode()再np.frombuffer()转回 float32 数组即可。uk_student_course_date这个唯一键是一个关键设计它保证了同一个学生在同一门课的同一天不会重复写入有效打卡记录但这还不够因为业务上还要考虑“打完卡立刻退出再进系统打第二次卡”的情况所以需要在应用层继续做拦截。3.2 业务层打卡接口事务、防重与迟到判断打卡接口的逻辑不是简单地把“识别到的人”和“当前时间”插入数据库而是需要按课程时间去判断类型。比如第一节课是 08:00 开始那么 07:50 到 08:15 之间打卡记为“正常”08:15 之后记为“迟到”。这个“宽限期”是考勤管理中常见的业务规则需要在论文功能设计里单独说明。from flask import Flask, request, jsonify from datetime import datetime import pymysql app Flask(__name__) db_config { host: localhost, user: root, password: yourpassword, database: attendance_system, charset: utf8mb4 } LATE_GRACE_MINUTES 15 app.route(/api/attendance/check_in, methods[POST]) def check_in(): 请求参数: student_id, course_id, face_score data request.get_json() student_id data.get(student_id) course_id data.get(course_id) face_score data.get(face_score, 0.0) if not student_id or not course_id: return jsonify({code: 400, msg: 参数不完整}), 400 conn pymysql.connect(**db_config) try: with conn.cursor() as cursor: # 查询课程时间信息用于判断迟到 cursor.execute( SELECT start_time FROM course WHERE course_id %s, (course_id,) ) course cursor.fetchone() if not course: return jsonify({code: 404, msg: 课程不存在}), 404 now datetime.now() # 检查是否已经打过卡 cursor.execute( SELECT record_id FROM attendance_record WHERE student_id %s AND course_id %s AND DATE(check_in_time) CURDATE() AND is_valid 1 , (student_id, course_id) ) if cursor.fetchone(): return jsonify({code: 409, msg: 今日已打卡请勿重复操作}), 409 # 判断迟到 start_dt datetime.combine(now.date(), course[0]) late_seconds (now - start_dt).total_seconds() is_late 1 if late_seconds LATE_GRACE_MINUTES * 60 else 0 cursor.execute( INSERT INTO attendance_record (student_id, course_id, check_in_time, face_score, is_late) VALUES (%s, %s, %s, %s, %s) , (student_id, course_id, now, face_score, is_late) ) conn.commit() return jsonify({code: 200, msg: 打卡成功, is_late: is_late}) except Exception as e: conn.rollback() return jsonify({code: 500, msg: str(e)}), 500 finally: conn.close()从代码测试的角度建议至少准备两个学生账号和两个课程做单元测试第一个学生在课程开始前 5 分钟打卡预期返回抖音is_late0第二个学生在开始后 20 分钟打卡预期返回is_late1然后再对同一个学生重复提交打卡请求验证 409 拦截。中间的DATE(check_in_time) CURDATE()条件会触发全表扫描考勤数据量大了之后建议改成check_in_time CURDATE() AND check_in_time DATE_ADD(CURDATE(), INTERVAL 1 DAY)这样能走idx_time索引。3.3 识别与打卡的桥接如何避免误识别导致别人代打卡人脸识别考勤系统最容易受到的质疑是“替人打卡”。这里要做两层防御第一层是活体检测比如要求用户眨眼或摇头但这在普通摄像头下会让体验变差所以很多毕设想用静态照片攻击测试来证明系统有风险意识到位第二层是在识别模块中返回相似度分数并在前端展示同时后端把分数写入字段face_score。如果某个同学的打卡记录连续多次人脸相似度都很低管理员可以人工复查。我见过不少失败的做法是把识别到的学生名直接写入打卡记录不做任何校验。更稳妥的做法是先识别返回一个“候选人列表”由前端二次确认后提交考勤接口。这样即使用户不是 100% 确定识别对也有纠错机会。例如返回 Top-3 候选人让操作员在触屏上点一下确认所选学生这个交互在论文中可作为“降低误识率”的功能亮点。4. 让系统稳定运转并发处理、性能优化与异常情况测试4.1 人脸识别服务的并发瓶颈与处理方案当考勤场景从“单机测试”变成“全班 40 人同时刷脸进教室”时你立刻会遇到两个瓶颈一是 MTCNN FaceNet 推理在单线程下每帧约需 0.3 到 0.8 秒GPU 上会降到 0.05 秒左右如果 10 个人同时请求每个请求排队时间会线性拉长二是 Flask 开发服务器默认不支持高并发。我的建议是用Gunicorn 启动多 worker并把 Flask 的同步处理改为“异步任务提交 轮询结果”的模型。# gunicorn.conf.py import multiprocessing bind 0.0.0.0:5000 workers multiprocessing.cpu_count() * 2 1 threads 4 timeout 30 worker_class gthread这里的参数设计思路是workers是进程数每个进程拥有独立的模型实例和内存空间threads是每个进程内线程数适合处理数据库 I/O 和文件 I/O。GPU 作为共享资源时多个 worker 都会往同一块 GPU 显存上加载模型如果显存不够 6GB建议把workers设置为 1 到 2。考勤请求通常每秒钟最多 2 到 3 次所以这个并发配置已经足够应付一个中型实验室的规模。4.2 人脸注册库的加载与更新策略在 2.3 节的识别代码中face_db是一个 Python 字典每次启动时从数据库加载。如果学生有 500 人每人 512 维 float32 向量内存占用大约 500 × 512 × 4 / 1024 / 1024 1MB加载到内存完全不是问题。但要注意更新策略当管理员在学生管理页面新增一名学生时face_db不会自动刷新需要实现一个简单的版本号机制。import pickle import threading face_db {} db_lock threading.Lock() def load_face_db_from_mysql(): 从数据库全量加载特征库返回字典 {student_id: embedding} global face_db conn pymysql.connect(**db_config) new_db {} try: with conn.cursor() as cursor: cursor.execute(SELECT student_id, face_embedding FROM student) for sid, embed_b64 in cursor.fetchall(): embed_bytes base64.b64decode(embed_b64) embed np.frombuffer(embed_bytes, dtypenp.float32) new_db[sid] embed finally: conn.close() with db_lock: face_db new_db print(f[INFO] 人脸库刷新完成共 {len(face_db)} 人)这里的db_lock是关键的并发保护机制。识别请求在读取face_db时管理员同时在写入新数据如果不同步会造成字典遍历错误。整体替换先构建 new_db 再整体赋给 face_db而不是一个个插入能保证读取方要么看到完整旧数据要么看到完整新数据不会出现“旧库里混入一个新特征”的中间状态。这个实践在分布式系统中叫“原子更新”写在论文里很有含金量。4.3 性能压测与硬件配置参考为了在毕业论文中呈现“系统稳定性”的数据你可以写一个简单的压测脚本模拟摄像头打卡请求。这里无需使用 JMeter直接用 Python 的requests库发送并发请求即可import requests import threading import time students [20210001, 20210002] # 假设有 2 个测试学生 courses [1, 2] url http://localhost:5000/api/attendance/check_in def send_request(student_id): payload { student_id: student_id, course_id: courses[0], face_score: 0.92, } try: resp requests.post(url, jsonpayload, timeout5) print(resp.json()) except Exception as e: print(fError: {e}) # 模拟 20 个并发请求 start time.time() threads [threading.Thread(targetsend_request, args(s,)) for s in students * 10] for t in threads: t.start() for t in threads: t.join() print(fTotal time: {time.time() - start:.2f}s)根据在学校实验室两台机器上的实测数据性能表现大致如下环境单次识别耗时20并发响应成功率CPU 占用CPU: i5-10400无独显0.65 s80% 在 3s 内返回35%GPU: NVIDIA GTX 1650 (4GB)0.12 s100% 在 1s 内返回15%批量注册 100 张照片42 s-50%这里补充说明CPU 环境的低成功率来自 Flask 默认的threadedFalse单线程模式加上 Gunicorn 后成功率会明显改善GPU 版本的显存需求主要来自 MTCNN 的 P-Net 等三层网络同时载入用torch.cuda.empty_cache()可以降低显存碎片。这些数据在论文中可以作为“系统性能测试”章节的支撑但建议你在自己机器上重新跑一份数据因为硬件不同结论会有差异。4.4 异常场景无法检测到人脸、多人同框、光线过暗考勤机最常见的报错是“无人脸”。这可能由两个原因造成摄像头角度太低只拍到额头或者逆光导致脸部过暗。MTCNN 对这类图像比较敏感需要在代码里增加图像增强的预处理分支。另一个容易踩的坑是当画面中有多个人脸时keep_allFalse会选择面积最大的人脸但如果老师站在学生旁边面积最大的可能是老师这会导致“学生被识别成老师”或“老师被识别成学生”的错误。解决方式是分析 MTCNN 的置信度输出并优先选择图像中心的框def detect_largest_center_face(mtcnn, pil_img): 返回图片中心区域中面积最大的人脸框 避免课堂场景中路人甲入镜干扰 boxes, probs mtcnn.detect(pil_img) if boxes is None: return None # 过滤置信度低于 0.9 的检测框 high_conf_idx [i for i, p in enumerate(probs) if p 0.9] if not high_conf_idx: return None # 按面积排序取最大的 best_box None best_area 0 for i in high_conf_idx: x1, y1, x2, y2 boxes[i] area (x2 - x1) * (y2 - y1) if area best_area: best_area area best_box boxes[i] return best_box这里使用mtcnn.detect()而不是mtcnn()是因为我们无需对齐后的人脸图像只需要框坐标。真实场景中摄像头一般放在门口 1.5 米高度学生从远处走近检测框会从小到大变化选择面积最大的框能保证是正面靠近的那个人。写论文时可以把“中心优先 最大面积优先”的组合策略作为识别模块的贡献之一。5. 从高分到卓越提升识别率的 8 个模型参数与训练细节高分毕业设计项目之间的差距往往不在“实现功能”而在“模型深度”。为了让论文评审老师看到你真正理解深度学习而不是只会跑通开源代码下面给出 8 个可以在论文中展开的关键参数与策略建议选其中 3 到 4 个做对比实验形成数据图。1. Embedding 维度对识别率的影响。FaceNet 原始设计支持 128 维和 512 维两种。粗略感觉 512 维更好但在人脸数量不超过 200 人的班级考勤场景中512 维反而可能引入噪声。你可以用 100 名学生自采数据集做对比实验统计不同维度下的 Top-1 准确率和误识率。这组实验工作量不大但非常有说服力。2. 图像对齐对识别率的提升。我们使用的是 MTCNN 自带的对齐功能但margin参数会影响对齐效果。margin 太小会截断下颌太大则引入背景干扰。建议测试margin0, 16, 32, 64四组效果画出相似度分布图。通常 margin32 是最佳区间。3. 阈值选择方法。不要拍脑袋定 0.75。做法是收集 200 张“本人打卡”照片和 200 张“他人替刷”照片分别计算与注册库的相似度画出两个直方图。最优阈值取两条曲线的交点或者使用最大化 F1-score 的阈值。这个实验在论文中建议给出图表。4. 数据增强对自建模型的微调效果。如果不用预训练模型而是自己从零训练一个小型 CNN数据增强是必须的随机亮度调整、随机水平翻转、随机平移与缩放以及高斯模糊。这里特别提醒随机遮挡Random Erasing对口罩场景的鲁棒性提升非常明显用 50% 概率把人脸下半部分区域随机用灰色块遮盖来模拟口罩。实际测试中加了该增强后戴口罩照片的识别率能提升约 17 个百分点。5. 特征融合方法。在实际中仅用实时帧单帧特征识别受眨眼、瞬间表情影响很大。工程上可以取连续 5 帧的识别结果对这 5 帧的特征向量做加权平均或者采取“5 帧中至少 3 帧识别为同一个学生才返回结果”的投票机制。前者的缺点是加重计算后者的缺点是延迟增加约 0.3 秒。6. 难样本挖掘Hard Negative Mining。FaceNet 的基础损失是三元组损失Triplet Loss由锚点、正样本、负样本组成。训练时如果把“张三的照片”和“张三相貌相似的李四照片”作为负样本对会让模型学到更细致的差异。考勤场景中最难分的是双胞胎或同寝室男生这部分在论文中建议作为讨论点。7. 错误样本回注机制。系统上线后每次打卡失败的照片自动保存到./failed_samples/目录每周管理员挑选其中确实属于某个学生的照片重新加入该学生的注册特征集。这个机制不需要重新训练模型却能在项目答辩时体现“持续优化”的理念。8. 模型蒸馏。FaceNet 参数量约 2.3 千万InceptionResnetV1在无 GPU 的实验室电脑上运行压力不小。如果导师要求“轻量化”可以尝试用 MobileNet 作为学生网络把 FaceNet 的输出512 维向量作为软标签在自建数据集上做蒸馏训练将参数量降到 1/4 以下推理速度可以提升到 CPU 30fps 级别。除了参数调优还有一个容易被忽略但在论文中非常加分的小功能导出缺勤名单。写一个定时任务脚本每天晚上 9 点扫描当天所有课程的考勤记录与course_student表做左连接找出没有任何打卡记录的学生生成 Excel 表格并发送到导员邮箱。这个脚本逻辑不复杂但真正做到“考勤系统不是只打卡而是闭环管理”。在最后的冲刺阶段把上述 8 点中的第 2、3、4 项做对比实验用 Matplotlib 导出相似度分布图和准确率柱状图你的毕设论文从功能展示到创新点就都齐了。本文还有配套的精品资源点击获取
返回列表