
简介基于人脸识别的学生签到考勤管理系统毕业设计论文面向需要完成考勤/请假类课题的本专科生及初入门的系统开发者。文档围绕学生在线请假与上课出勤管理场景完整覆盖JSP SQL Server 2000环境下的系统分析、架构设计、数据库E-R图与数据字典、UML用例、假期/考勤/后台三大模块及人脸识别技术应用并包含登录、修改密码等具体功能实现细节可直接作为毕业设计说明书修改参考。压缩包内包含1个docx文档大小369KB便于下载与查阅。目前已有138人学习适合正在撰写高校考勤管理类论文、需要借鉴功能模块划分与实现细节的读者。1. 为什么学生考勤要走上人脸识别这条“重技术”路线上课点名最心疼的不是那两分钟而是你刚把名单念完低头补妆的同学已经替三个室友“答到”了。IC卡和指纹机要专用硬件会在几十人的教室里排起队二维码签到码一旦被发到群里等于签给全班看。用教室已有的电脑摄像头做考勤让每个学生走到镜头前自然停留半秒身份由人脸特征比对给出这是成本最低、体验最顺的落地方式。一套能交付的人脸识别签到考勤系统至少包含注册、识别、去重、统计四段。注册阶段把每张脸转成特征向量并入库识别阶段对摄像头帧做人脸检测、对齐和比对返回学号和置信度再去重、判定迟到早退写入考勤记录最后在管理端按课程和日期生成学生考勤报表。这套方案适合正在做课程设计或毕业设计的人也适合培训机构和中小学校部署。做论文和做工程可以共用一条主线把人脸识别当成黑盒接口把精力放到检测质量、阈值调优和考勤业务规则上比堆砌一堆模型参数更有说服力。2. 人脸识别考勤系统的技术选型与最小运行环境2.1 本地识别与云端API怎么取舍考勤场景对延迟和隐私都敏感。学生的正脸照片属于个人敏感信息如果走云API每一次签到都会把照片上传到第三方服务器很多学校的信息化部门在这一步就直接否决。本地识别则把所有计算放在教室电脑或机房服务器上照片不出内网断网时也能签到。40人左右的班级用一台普通i5 CPU跑Dlib的人脸检测加特征提取单帧约150ms按3秒抽一帧的节奏完全够用。云端API的优势是精度高、不用自己训练模型但按次数计费一个2000人的院系每天刷脸产生的费用并不低。建议把云API作为备选方案本地识别作为主体。如果对识别精度要求更高可以把Dlib换成InsightFace系列的ArcFace或国内开源的MobileFaceNet它们的模型文件同样可以在本地推理只是环境配置和显存要求会高一些。2.2 最小技术栈组合OpenCV Dlib Flask MySQL一套能跑的方案可以把技术栈收敛到四种OpenCV负责图像读入和基础处理Dlib负责检测、对齐和特征提取Flask提供签到与查询的HTTP接口MySQL存放学生档案、特征向量和考勤记录。前端用Vue3加Element Plus做后台管理页面教师端可以写一个独立的Python窗口程序也可以直接复用浏览器摄像头。组件职责备选方案OpenCV摄像头采集、图像预处理、画框无Dlib人脸检测、关键点对齐、128维特征InsightFace、FaceNet、OpenCV DNNFlask签到接口、学生管理接口Spring Boot、DjangoMySQL学生信息、特征向量、考勤记录SQLite仅测试、PostgreSQL提示Dlib在Windows上的安装依赖CMake和VS Build Tools建议使用Python 3.10以下的版本直接把opencv-python、dlib、flask、numpy装在同一环境里能省掉大量编译时间。选型时还要考虑将来答辩或交接的问题。Flask全程只有几个路由代码量小适合把精力留在识别和业务上如果团队更熟悉Java把Flask换成Spring Boot并不影响前面的人脸识别模块因为特征提取已经封装成独立服务只需把比对接口暴露为REST API。2.3 从一张照片到一个人名的可运行链路在写注册和签到之前先把最小链路跑通给定一张学生照片程序能够检测出人脸框。下面的代码直接可用import cv2 import dlib detector dlib.get_frontal_face_detector() img cv2.imread(student_001.jpg) rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) faces detector(rgb, 1) for face in faces: l, t, r, b face.left(), face.top(), face.right(), face.bottom() cv2.rectangle(img, (l, t), (r, b), (0, 255, 0), 2) cv2.imwrite(face_boxes.jpg, img)detector(rgb, 1)的第二个参数是图像金字塔的上采样次数。值越大图像会被放大更多次再检测小脸越容易找到但检测耗时成倍上升。教室摄像头离学生两三米远、人脸占比小建议保留为1必要时调成2近景自拍场景保持0即可。COLOR_BGR2RGB是必须的Dlib内部用RGB顺序OpenCV默认读成BGR不转换会让人脸检测率明显下降。这一步跑通后注册环节要做的就只剩下把框内人脸转成特征向量下一章从这个环节展开。3. 人脸注册与学生特征入库的完整实现3.1 用摄像头采集学生面部样本的采集脚本注册的第一件事是拿到干净的人脸照片。可以直接在管理系统里调用学生上传的照片但考勤识别与学籍照经常存在发型、眼镜、表情上的差异。我一般会在开学第一周用摄像头现场采集每人抓5到10帧既保证光线一致也让学生知道采集用途。下面的脚本按下空格键保存当前帧按q退出并以学号前缀命名文件import cv2 import os student_id 2023001 save_dir fdataset/{student_id} os.makedirs(save_dir, exist_okTrue) cap cv2.VideoCapture(0) if not cap.isOpened(): raise RuntimeError(摄像头未打开请检查权限或设备索引) count 0 while True: ok, frame cap.read() if not ok: continue cv2.imshow(register, frame) key cv2.waitKey(1) 0xFF if key ord( ): cv2.imwrite(f{save_dir}/{student_id}_{count:02d}.jpg, frame) count 1 if key ord(q) or count 10: break cap.release() cv2.destroyAllWindows()VideoCapture(0)的0表示默认摄像头笔记本带多个摄像头时换成1或2。每按一次空格保存的是原始帧还没有做人脸检测所以采集时要让学生正对镜头、保持正常表情。10张的下限是为了后续通过多张样本取平均特征降低单帧噪声对入库向量的影响如果注册人数很多5张也可以接受。3.2 人脸对齐为什么裁剪的人脸要做关键点对齐把人脸框直接缩放到150x150并不能作为特征提取输入。不同学生站的位置有高低差点头角度也不一样同样是正脸眼睛和嘴的相对位置会偏移。先做关键点对齐让两眼保持水平可以显著提高同一人不同帧的相似度。Dlib提供5点和68点两种关键点模型识别对齐用5点足够68点主要用来做姿态判断比如检测出头点得太低或侧面太多。对齐逻辑用Dlib自带算子最省事import dlib detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_5_face_landmarks.dat) face detector(rgb, 1)[0] shape predictor(rgb, face) aligned_face dlib.get_face_chip(rgb, shape, size150, padding0.25)get_face_chip会把人脸旋转到水平并按padding0.25把脸周围留出25%的边界再统一缩放到150x150。padding太大背景会混进特征太小会把下巴和刘海裁掉0.25是一个在多人数据集上验证过的安全值。注意shape_predictor_5_face_landmarks.dat属于Dlib的辅助模型需要单独下载放到项目目录中。3.3 提取128维特征向量并写入MySQL人脸对齐完成后特征提取已经是固定的两步用face_recognition_model_v1计算128维描述子再写入数据库。训练好的Dlib ResNet模型输出128维浮点向量这个向量在不同照片之间用欧氏距离或余弦相似度衡量。完整代码分为特征计算和入库两部分import dlib import numpy as np import mysql.connector net dlib.face_recognition_model_v1(dlib_face_recognition_resnet_model_v1.dat) rgb dlib.load_rgb_image(fdataset/{sid}/{sid}_00.jpg) face detector(rgb, 1)[0] shape predictor(rgb, face) aligned dlib.get_face_chip(rgb, shape, size150, padding0.25) vector np.array(net.compute_face_descriptor(aligned)) vector vector / np.linalg.norm(vector) # L2归一化 embedding vector.astype(np.float32).tobytes() conn mysql.connector.connect(host127.0.0.1, userroot, password****, databaseattendance) cursor conn.cursor() cursor.execute( INSERT INTO student (student_id, name, face_embedding) VALUES (%s, %s, %s), (sid, name, embedding), ) conn.commit()compute_face_descriptor接收的已经是150x150的对齐图直接传原始人脸框会降低识别率。归一化这一步容易被忽略不除以模长两个人脸向量即使方向一致距离也会因为亮度不同被拉大归一化后欧氏距离和余弦相似度的排序结果一致后续选择哪个指标都可行。入库时用tobytes()把浮点数组转成BLOB取出时用np.frombuffer(embedding, dtypenp.float32)还原即可。建表结构可以精简到几个必要字段CREATE TABLE student ( id INT AUTO_INCREMENT PRIMARY KEY, student_id VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, face_embedding BLOB NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );student_id做唯一键是签到记录关联学生的主线face_embedding用BLOB而不是VARCHAR可以避免Base64编码带来的33%空间浪费和一次解码开销。3.4 批量注册照片的清洗规则与隐私注意一个班几十人连续注册时最容易出现的问题是采集到了模糊帧、半张脸或背后同学的脸。注册脚本不会阻止这种坏样本进库需要加一道简单清洗只取面积最大的人脸丢弃太小的人脸并用Laplacian算子方差过滤模糊图。检查项建议值说明人脸宽度最小值80像素小于此值说明学生离镜头太远Laplacian方差≥ 80低于此值的帧视为模糊每人样本数510张多张取平均特征更稳光线条件均匀自然光或教室开灯避免一半脸亮一半脸暗特征向量一旦入库建议把原始照片从注册目录中清理掉系统只保留128维向量。这样既减少磁盘占用也降低照片外泄风险。管理系统里可以保留“重新注册”功能但旧向量要做逻辑删除不能直接覆盖否则正在签到的请求可能读到半写状态的数据。4. 考勤打卡系统的识别、去重与签到记录实现4.1 课堂签到的最小识别流程教师端程序的任务可以拆成四步定时抓帧、检测并提取特征、与库比对、提交结果。不需要对每一帧都识别每3秒抽一帧即可学生从走到镜头前到签完离开通常能获得20次以上的判定机会。识别函数在本地执行返回最相似的学号、距离和第二相似学号供后续做阈值判断def verify_face(aligned_face): vector np.array(net.compute_face_descriptor(aligned_face)) vector vector / np.linalg.norm(vector) dists np.linalg.norm(embeddings - vector, axis1) # embeddings为库矩阵 idx np.argsort(dists) return student_ids[idx[0]], float(dists[idx[0]]), float(dists[idx[1]])embeddings是上一章入库后取出的所有特征向量组成的二维矩阵每行对应一个学生。用np.linalg.norm(..., axis1)一次性算完整个库的距离比自己写循环快得多。这里返回第二名距离是为了下一节提到的“摇摆拒绝”判断不是多余的打印值。4.2 置信度阈值怎么调误识别与漏识别的边界Dlib官方建议欧氏距离小于0.6算同一人但这只是一个跨数据集的经验值。教室摄像头和在网上下载的明星数据集在分辨率、压缩噪声和表情分布上差异巨大直接套0.6很危险。更稳妥的做法是注册完成后先对库里的特征做一次自检统计同一人多张样本之间的距离再随机挑不同学生算一遍类别距离。阈值区间行为适合场景 0.45严格误识别少漏识别多光线稳定、注册样本多的班级0.450.6Dlib通用区间大多数教室环境0.60.7宽松漏识别少误识别多摄像头分辨率低、学生经常低头阈值确定后还要加一道“第一名和第二名距离差”的保护。如果第一名距离是0.3但第二名只有0.35说明模型在两个人之间摇摆应该拒绝本次识别并要求重新抬头。判定逻辑是best_dist / second_dist 0.8才接受这个比例可以根据实测放宽到0.85。注意调阈值一定要用没参加过注册的新照片做测试。拿注册时的同帧照片测所有距离都会偏小得到的阈值没有参考价值。4.3 考勤打卡系统的去重与迟到早退规则设计识别通过只说明“这是谁”还需要决定“这次签到是否有效”。最常见的业务规则是同一学生同一课程一天只签一次第二次不再覆盖第一次记录。用Redis的SETNX可以做到但大多数考勤系统并发量很低查数据库加唯一索引更简单。签到状态需要区分正常、迟到和早退。正常和迟到由签到时间对比课程开始时间决定早退只能靠放学时再刷一次脸或者在课中抽查。考虑到学生签到到了以后离开座位去重规则通常以“签退”事件独立保存不做覆盖更新。签到记录表设计如下CREATE TABLE attendance_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY, student_id VARCHAR(20) NOT NULL, course_id VARCHAR(20) NOT NULL, checkin_time DATETIME NOT NULL, status ENUM(normal, late, leave_early) NOT NULL, distance FLOAT NOT NULL, UNIQUE KEY uk_student_course (student_id, course_id, DATE(checkin_time)) );这里的唯一键用DATE(checkin_time)允许MySQL对同一天同一课程同一学生只保留一条记录并发插入时只有一条成功天然避免重复签到。distance字段保存当时的比对距离后续排查“某人频繁被认成别人”时可以直接查这个字段。4.4 签到接口与一次成功签到的返回体教师端把抓到的对齐图编码为Base64图片发送到Flask接口识别、判定、写库都在服务端完成。这样教师端的代码只做采集和显示真正的业务逻辑便于统一升级。app.post(/api/checkin) def checkin(): data request.get_json() img_bytes base64.b64decode(data[image]) arr cv2.imdecode(np.frombuffer(img_bytes, np.uint8), cv2.IMREAD_COLOR) rgb cv2.cvtColor(arr, cv2.COLOR_BGR2RGB) faces detector(rgb, 1) if len(faces) 0: return {ok: False, reason: no_face} aligned dlib.get_face_chip(rgb, predictor(rgb, faces[0]), size150, padding0.25) sid, dist, second_dist verify_face(aligned) if dist THRESHOLD or dist / second_dist 0.8: return {ok: False, reason: low_confidence, dist: dist} insert_attendance(sid, data[course_id], dist) return {ok: True, student_id: sid, status: normal, dist: dist}接口先解码Base64再走和人脸注册完全一致的检测、对齐、提取链路。返回体里带上dist而不是只返回布尔值是为了让教师端能对低置信度的签到给出视觉反馈如果学生连续三次都是low_confidence界面应提示“请靠近摄像头再试”。5. 考勤统计报表与 Vue3 后台管理页面实现5.1 查询某门课某个月的考勤汇总SQL签到表写完后统计报表的核心是一个按课程和日期聚合的查询。报表要求一般有两层每个学生每天一条出勤记录迟到/正常/缺勤以及每个学生在一个周期内的出勤率。后者用条件聚合实现SELECT s.student_id, s.name, DATE_FORMAT(c.checkin_time, %Y-%m) AS month, COUNT(DISTINCT CASE WHEN c.status normal THEN c.checkin_time END) AS normal_count, COUNT(DISTINCT CASE WHEN c.status late THEN c.checkin_time END) AS late_count, COUNT(*) AS total_days FROM student s LEFT JOIN attendance_record c ON s.student_id c.student_id WHERE c.course_id %s AND c.checkin_time BETWEEN %s AND %s GROUP BY s.student_id, s.name, monthLEFT JOIN保证了没有签到记录的学生也会出现在结果里normal_count和late_count都是0如果换成INNER JOIN缺勤学生会被直接过滤掉报表就少了“全部未到”这一行。COUNT(DISTINCT CASE...)是对日期去重而不是对记录去重防止同一天多条记录把次数加高。缺勤人数的计算建议在SQL外面做把应上课天数减去已经签到天数即可。日期范围参数由前端传递注意时区问题如果MySQL和你所在地区时区不一致BETWEEN查询可能少一天数据统一在连接串中设置时区或在使用前约定好作息时间表。5.2 用 Vue3 Element Plus 搭建考勤后台管理页面管理后台不需要复杂的图表库表格加日期选择器就能覆盖90%的使用场景。Vue3 Element Plus是目前最容易找到代码参考的组合核心是三个绑定课程下拉框、日期范围、表格数据。script setup import { ref, watch } from vue; import axios from axios; const courseId ref(); const dateRange ref([]); const rows ref([]); async function loadReport() { const params { course_id: courseId.value, start: dateRange.value?.[0], end: dateRange.value?.[1], }; const resp await axios.get(/api/report, { params }); rows.value resp.data.rows; } watch([courseId, dateRange], loadReport); /script template el-select v-modelcourseId placeholder选择课程 el-option label数据结构 valueCS101 / el-option label操作系统 valueCS202 / /el-select el-date-picker v-modeldateRange typedaterange value-formatYYYY-MM-DD / el-table :datarows el-table-column propstudent_id label学号 width110 / el-table-column propname label姓名 / el-table-column propnormal_count label正常 / el-table-column proplate_count label迟到 / el-table-column propabsent_count label缺勤 / /el-table /templatewatch([courseId, dateRange], loadReport)在用户切换课程或日期范围时自动触发查询不需要手动点“查询”按钮。value-formatYYYY-MM-DD让日期组件直接输出字符串避免和后端收到的时间对象不一致。缺席字段absent_count由后端计算并返回前端不参与业务计算这是为了避免前后端各算一套、相互对不上。5.3 导出考勤报表时处理中文文件名与编码运营老师最常用的导出格式是Excel或CSV。CSV体积小、Excel能直接打开导出接口不要返回纯表格直接把文件流返回。import csv from flask import Response def export_csv(filename, header, rows): buf StringIO() writer csv.writer(buf) writer.writerow(header) writer.writerows(rows) return Response( \ufeff buf.getvalue(), mimetypetext/csv; charsetutf-8, headers{Content-Disposition: fattachment; filename{filename}.csv}, )字符串前面的\ufeff是UTF-8 BOM放在CSV开头后Windows上的Excel打开中文表头才不乱码。如果使用pandas导出Excel记得用df.to_excel(report.xlsx)而不是to_csvxlsx格式没有编码问题但需要额外安装openpyxl。导出数据量上千行时接口可能耗时较长建议限制单次导出范围不能超过一个学期。6. 人脸签到系统的识别率验证与专项调优6.1 用“留人法”而不是“留照片法”评估识别率测评时的典型错误是把已注册照片的回放帧拿来测准确率这样得到的永远是高分。正确做法是每个学生准备一组没有参与特征库计算的照片比如采集10张其中8张入库2张专门用于测试测试时统计这2张照片能否被正确识别到本人。最终画一条阈值与错误率的对应关系看误识别率和漏识别率的交点落在哪个阈值区间再结合实际课堂情况取整。6.2 抽帧与并发压力验证签到接口上线前用录制好的视频做回放测试比真人反复走动更可重复。压力方面一个班40人同时涌到镜头前瞬时请求可能达到几十并发可以用下面的命令做个简单验证ab -n 100 -c 10 -p checkin_payload.json \ -H Content-Type: application/json \ http://127.0.0.1:5000/api/checkin-c 10表示10个并发-p指定一张已经转成Base64的图片JSON。如果平均响应时间超过500ms优先检查人脸检测是否用了过大的上采样次数以及在比对时是否每次都从MySQL读取全部特征。Dlib的检测器不是线程安全的Flask多线程模式下要加锁或用每个线程一个detector实例否则高并发时会出现奇怪的段错误。6.3 把特征库预加载进内存的最后一招当学生人数超过几百人每签一次到都查数据库取全部特征会明显拖慢平均响应。常见做法是在服务启动时把所有学生特征一次性读入之后只维护一个student_id - vector的字典比对时转为numpy矩阵计算vectors, ids [], [] with conn.cursor() as cur: cur.execute(SELECT student_id, face_embedding FROM student) for sid, blob in cur: vec np.frombuffer(blob, dtypenp.float32) vectors.append(vec) ids.append(sid) EMB np.vstack(vectors).astype(np.float32) STU_IDS ids内存占用量是人数乘512字节128个float32×4字节2000人也只有1MB左右完全值得全部预加载。后续再注册新学生时不要直接改全局矩阵而是追加到列表后重新np.vstack一次更新操作加读写锁避免签到请求读到半构建的矩阵。这个优化做完单人签到的接口可以稳定在100ms以内波动。本文还有配套的精品资源点击获取