☰
基于CNN的人脸识别考勤系统:从数据到答辩全流程指南
2026/10/1 5:55:39 网站建设 项目流程

简介:基于卷积神经网络的学生人脸识别考勤系统,是一份完整的Python毕设源码与配套文档,适用于计算机、人工智能、自动化等专业的学生、教师或从业者,可用于期末课程设计、课程大作业或毕业设计参考。压缩包共55个文件,体积2.22MB,包含23个Python脚本、12个Qt界面ui文件、7个pyc编译文件、3个numpy数据文件、2个SQLite数据库以及1份docx说明文档,涵盖人脸检测、人脸特征提取、考勤管理、界面交互等完整模块,目录结构清晰。代码已经过调试测试,确保可运行,且作者称答辩评审分达98分,具备较高的学习借鉴价值。目前已有213人学习下载。适合基础薄弱者按步骤学习,也可由能力较强的读者在此基础上修改扩展,以实现不同考勤场景功能,整体是深度学习入门与毕设参考的实用资料。

1. 人脸识别考勤系统:这个毕设题到底要你做出什么东西

一个班的课,一学期下来点名就要耗掉好几十分钟,代签、漏签更是常事。把人脸识别考勤系统当毕业设计的人,多半是被这个场景触动的:摄像头往讲台上一架,学生进门看一眼屏幕,考勤就自动完成。Python 加卷积神经网络(CNN)加人脸识别,三个词凑起来,既有“人工智能”的门面,又有“能跑通、能演示”的落地空间。这套方案解决的核心问题是把人工点名变成无感考勤,适合需要毕设代码的专业学生,也适合不想买昂贵门禁机的实验室。拿到源码和文档说明,你要做的不是跑通就交差,而是把它拆成数据采集、模型训练、实时识别、考勤落库四条链路,每条都要能在答辩时讲清楚。

2. 技术选型:CNN 模型、人脸检测库与数据集的三方取舍

2.1 为什么毕设选 CNN 而不是传统人脸识别算法

在动代码之前,很多同学会先问一句:人脸识别一定要用 CNN 吗?答案是看场景。传统人脸识别算法里的 LBPH(局部二值模式直方图)和 Eigenfaces(特征脸),在光照均匀、姿势端正的实验室条件下做得不错,但教室里的真实情况是窗边逆光、后排人脸小、有人低头玩手机,还有人戴口罩。这些情况下,手工设计的纹理特征和线性子空间方法变得非常脆弱,识别率经常掉到七成以下。

CNN 的立足点是端到端特征学习。卷积层从原始像素出发,逐层提取边缘、纹理、五官结构等特征,不再依赖人工设计特征算子,面对光照和姿态变化有更强的鲁棒性。具体到这份毕设里,CNN 走的是“人脸检测 → 区域缩放 → 卷积分类 → 输出学号”这条路,属于轻量级方案,但足够应付几十人班级的考勤。相比之下,工业级门禁机里的方案通常是 FaceNet、ArcFace 这类用海量数据预训练的大模型,识别精度高但参数量动辄几十兆,还得配套专用硬件。毕设用不上那么大的模型,也不建议直接用,因为老师会追问“模型太黑盒”以及“训练复杂度太高”——自己搭一个小型 CNN 反而每一步都能解释清楚。

选 CNN 还有一个实际考量:答辩好讲。卷积层、池化层、全连接层、softmax 分类,都是《深度学习》课程里的基础概念。老师不要求你刷到 SOTA,只要能说清楚“为什么卷积结构能做人脸识别”以及“过拟合是怎么压下去的”,就比套一个大黑匣子划算得多。

2.2 人脸检测库选型:Haar、OpenCV DNN、MTCNN 三选一

人脸识别考勤的第一环不是认人,而是先“找到人在哪”。这一步叫人脸检测,常见方案有三个,优缺点差别很大:

检测方案原理速度(CPU)精度适用场景
OpenCV Haar CascadeHaar-like 特征 + Adaboost极快,>30fps低,误检多光线稳定、距离近的正面照
OpenCV DNN(ResNet-10 SSD)轻量 SSD 目标检测网络快,20fps 左右中,对角度有一定容忍教室光线变化不大的实时场景
MTCNN三个 CNN 级联(P-Net/R-Net/O-Net)较慢,10-15fps高,对遮挡和侧脸更稳追求精度、可接受部分帧丢失

毕设我一般推荐 MTCNN 或 OpenCV DNN。Haar Cascade 虽然加载快、调用简单,但误检率实在太高,拍个墙面、衣服纹理都能框出“脸”,而且对侧脸和低头姿势几乎没有抵抗力,考勤现场会频繁把不相关区域送进分类器。MTCNN 本身就是由三个卷积神经网络组成的级联结构:P-Net 快速生成候选框,R-Net 精筛,O-Net 输出最终人脸框和关键点坐标。用 MTCNN 做检测,整条链路从检测到识别都是卷积神经网络,和标题的“基于卷积神经网络”完全一致,论文里也能多写一节。代价是 CPU 下帧率偏低,后面会讲怎么用跳帧来弥补。

OpenCV DNN 是务实之选。它内置的 ResNet-10 SSD 模型权重很小,加载后 CPU 实时性很好,精度介于 Haar 和 MTCNN 之间。如果你的开发机没有独立显卡,又想演示流畅,就选它。另外要注意 Dlib 的 HOG 人脸检测器虽然也常用,但本质是传统特征,不是 CNN,和这个题目不太搭,不推荐。

2.3 数据集怎么准备:公开数据集、班级自采与数据增强的配比

CNN 是数据喂出来的,但“基于卷积神经网络的学生人脸识别考勤系统”这个题目有个特殊约束:你要识别的是本班学生,不是明星脸。公开数据集(LFW、CASIA-WebFace)里的人脸和你的用户对不上,直接拿公开数据训出来,模型不认识张三李四。所以自采数据是绕不开的一步。

常见做法是让每个学生坐在摄像头前,转动头部、变化表情、摘戴眼镜,采集 30 到 50 张图片。一个 30 人的班级,总原始样本约 1000 到 1500 张,一个下午能采完。之后用 Keras 的 ImageDataGenerator 做数据增强,随机旋转、平移、缩放、水平翻转、亮度扰动,能把训练样本扩到 3 到 5 倍,模拟教室不同座位角度和光线变化。需要注意,增强只作用于训练集,验证集用原始图片,否则验证结果会虚高。

还有一类同学想让省事,直接从网上爬明星脸当训练集。这种做法的最大问题是现场识别效果不可控——你训出来的模型比的是“谁更像某张网络图片”,而不是“谁更像坐在教室里的这个人”,到了答辩演示阶段很容易当场翻车。自采数据虽然累,但它是整个源码里最不可替代的部分,也是答辩时可以拿出来讲的真实工作量。

3. 从 0 到 1 复现:训练一个能认人的 CNN 并把它接进考勤系统

3.1 制作标签数据集:目录结构、划分与归一化

采完原始图,第一件事是整理成 Keras 直接可读的数据集结构。规范是一个文件夹一个学生,文件夹名就是学号或姓名,因为flow_from_directory会自动按目录名生成标签。

../dataset/processed/ ├── train/ │ ├── zhangsan/ # 每类 40~60 张增强图 │ ├── lisi/ │ └── wangwu/ └── val/ ├── zhangsan/ # 每类 10~15 张原始图 ├── lisi/ └── wangwu/

训练验证划分有一个经常踩的坑:不要随机切分。视频里连续采集的帧高度相似,随机划分会导致同一个动作的相近帧同时出现在训练和验证里,验证集被“剧透”,准确率虚高。我习惯按采集顺序切,一个人从头到尾的帧,前 80% 进训练,后 20% 进验证。这样验证集里全是模型没见过的姿势,测出来才是真实水平。

图片尺寸选 128×128 是权衡过的。224×224 的输入在 CPU 上单帧预测要 200 到 400ms,考勤画面会明显卡顿;128×128 对小型 CNN 来说单次预测约 30 到 80ms,摄像头采集能做到 5 到 10fps 的刷新率,体感完全不同。下面这段代码完成划分和统计,顺手检查类别均衡:

import os import shutil raw_root = "../dataset/raw" train_root = "../dataset/processed/train" val_root = "../dataset/processed/val" for student in os.listdir(raw_root): raw_dir = os.path.join(raw_root, student) if not os.path.isdir(raw_dir): continue files = sorted(os.listdir(raw_dir)) split_idx = int(len(files) * 0.8) for f in files[:split_idx]: os.makedirs(os.path.join(train_root, student), exist_ok=True) shutil.copy(os.path.join(raw_dir, f), os.path.join(train_root, student, f)) for f in files[split_idx:]: os.makedirs(os.path.join(val_root, student), exist_ok=True) shutil.copy(os.path.join(raw_dir, f), os.path.join(val_root, student, f)) # 打印每个类别的数量,排查类别不均衡 for root in [train_root, val_root]: for student in sorted(os.listdir(root)): n = len(os.listdir(os.path.join(root, student))) print(f"{root}/{student}: {n}")

这段代码的逻辑是按文件名排序后做 8:2 切分。split_idx决定前多少张进训练,剩下的进验证。打印统计的目的是排查“张三采了 100 张、李四只有 20 张”这类问题——类别不均衡会让李四的召回率崩到惨不忍睹。如果确实有人缺席没拍到,就只对这个类别额外加增强轮次,而不是整体加数据。

3.2 搭建并训练 CNN 分类器:模型结构、关键参数与训练回调

模型不用复杂。这个考勤场景的类别数是固定的(几十个学生),不需要像人脸识别门禁机那样做百万级参数量的大模型。我用的是三组“卷积 + 批归一化 + 池化”堆叠,再接全连接层和 softmax 输出的结构,输入是单通道灰度图。

import tensorflow as tf from tensorflow.keras.models import Sequential from tensorflow.keras.layers import (Conv2D, MaxPooling2D, Flatten, Dense, Dropout, BatchNormalization) from tensorflow.keras.preprocessing.image import ImageDataGenerator from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau IMG_SIZE = (128, 128) BATCH_SIZE = 32 NUM_CLASSES = 3 # 按实际学生人数修改 # 训练集增强,验证集只归一化 train_datagen = ImageDataGenerator( rescale=1.0 / 255, rotation_range=15, # 随机旋转 ±15 度 width_shift_range=0.1, # 水平随机偏移 10% height_shift_range=0.1, # 垂直随机偏移 10% brightness_range=(0.8, 1.2), # 亮度扰动,模拟不同座位光照 horizontal_flip=True, ) val_datagen = ImageDataGenerator(rescale=1.0 / 255) train_gen = train_datagen.flow_from_directory( "../dataset/processed/train", target_size=IMG_SIZE, batch_size=BATCH_SIZE, class_mode="categorical", color_mode="grayscale") val_gen = val_datagen.flow_from_directory( "../dataset/processed/val", target_size=IMG_SIZE, batch_size=BATCH_SIZE, class_mode="categorical", color_mode="grayscale") model = Sequential([ Conv2D(32, (3, 3), activation="relu", input_shape=(128, 128, 1)), BatchNormalization(), MaxPooling2D((2, 2)), Conv2D(64, (3, 3), activation="relu"), BatchNormalization(), MaxPooling2D((2, 2)), Conv2D(128, (3, 3), activation="relu"), BatchNormalization(), MaxPooling2D((2, 2)), Flatten(), Dense(256, activation="relu"), Dropout(0.5), # 随机丢弃一半神经元,防止过拟合 Dense(NUM_CLASSES, activation="softmax"), ]) model.compile(optimizer="adam", loss="categorical_crossentropy", metrics=["accuracy"]) callbacks = [ EarlyStopping(monitor="val_loss", patience=5, restore_best_weights=True), ReduceLROnPlateau(monitor="val_loss", factor=0.5, patience=2), ] history = model.fit(train_gen, validation_data=val_gen, epochs=50, callbacks=callbacks) model.save("../models/cnn_face_model.h5")

这段代码有四个关键参数值得说透。第一,BatchNormalization放在每个Conv2D之后,能让每层输出分布稳定,在光照变化明显的验证集上更稳,这是很多初版源码缺失的细节。第二,Dropout(0.5)放在全连接前,对几十人的小样本场景是刚需,否则模型会把训练图“背”下来,验证集却一塌糊涂。第三,EarlyStopping(patience=5)连续 5 轮验证损失不降就停,restore_best_weights=True保证回滚到最优权重,不用手动保存。第四,ReduceLROnPlateau(factor=0.5, patience=2)是两轮不降时学习率减半,让 loss 后期能钻过小坑。epochs 设置 50 并不代表跑满 50,实际在 15 到 25 轮左右就会触发早停收敛。

3.3 实时识别与考勤落库:摄像头读帧、置信度过滤与 SQLite 去重

模型训练完,接下来是把识别链路接进考勤系统。最简实现是 OpenCV 读帧,MTCNN 检测人脸,裁剪送入 CNN,softmax 概率过滤后写入 SQLite。

import cv2 import numpy as np import sqlite3 import datetime import tensorflow as tf from mtcnn.mtcnn import MTCNN model = tf.keras.models.load_model("../models/cnn_face_model.h5") class_names = list(train_gen.class_indices.keys()) detector = MTCNN() conn = sqlite3.connect("../attendance.db") conn.execute("""CREATE TABLE IF NOT EXISTS attendance ( student_name TEXT NOT NULL, date TEXT NOT NULL, checkin_time TEXT NOT NULL, UNIQUE(student_name, date) )""") CONF_THRESHOLD = 0.85 # 置信度低于此值判为未知 cap = cv2.VideoCapture(0) def predict_and_checkin(frame): faces = detector.detect_faces(frame) for face in faces: x, y, w, h = face["box"] # 越界保护,防止检测框超出画面导致裁剪崩溃 x, y = max(0, x), max(0, y) w, h = min(w, frame.shape[1] - x), min(h, frame.shape[0] - y) crop = cv2.cvtColor(frame[y:y+h, x:x+w], cv2.COLOR_BGR2GRAY) crop = cv2.resize(crop, (128, 128)).astype("float32") / 255.0 crop = np.expand_dims(crop, axis=(0, -1)) probs = model.predict(crop, verbose=0)[0] idx = int(np.argmax(probs)) if probs[idx] >= CONF_THRESHOLD: name = class_names[idx] today = datetime.date.today().isoformat() now = datetime.datetime.now().strftime("%H:%M:%S") conn.execute( "INSERT OR IGNORE INTO attendance (student_name, date, checkin_time) VALUES (?, ?, ?)", (name, today, now), ) conn.commit() cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2) cv2.putText(frame, name, (x, y-8), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 255, 0), 2) while True: ret, frame = cap.read() if not ret: break predict_and_checkin(frame) cv2.imshow("attendance system", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows() conn.close()

这段代码里有三个容易翻车的地方。第一,MTCNN 返回的box坐标可能越界或为负,裁剪前必须 clamp,否则frame[y:y+h, x:x+w]会直接抛异常,程序在演示现场崩溃很难看。第二,model.predict逐帧调用在 CPU 上是性能瓶颈,检测加识别一帧约 130ms,画面会卡。想顺滑,就在while循环里加一个帧计数器,每 3 帧才执行检测识别,中间帧只显示画面,考勤记录的实时性并没有实质损失。第三,考勤去重依赖UNIQUE(student_name, date)约束配合INSERT OR IGNORE,同一个学生同一天只记一条,这条逻辑在答辩时讲成“数据库层防重复”,是很清晰的设计点。

4. 踩坑排查:人脸识别考勤系统最容易翻车的五个真实场景

4.1 逆光与侧脸导致检测框反复跳动、识别率骤降

现象:靠窗的学生被阳光直射时,人脸框在脸上跳来跳去,识别结果在“张三”和“未知”之间反复横跳,考勤记录一直写不进去。

原因:人脸检测对光照已经比识别环节更敏感,但逆光时人脸区域过曝或过暗,灰度图变成一片白或一片黑,CNN 提取不到纹理特征,softmax 输出概率被拉平,跨不过置信度阈值。

解决:预处理环节把灰度图做 CLAHE(对比度受限自适应直方图均衡化),替代普通直方图均衡化,能明显改善过暗区域的纹理表达。若仍不行,先用小样本测试把阈值从 0.85 降到 0.75,确认是不是阈值设太严导致漏判;如果两者都无效,最现实的调整是移动摄像头安装位置,让光源来自人脸前方而不是背后。这个结论可以直接写进论文的“系统测试与分析”一节。

4.2 训练验证划分不当导致“虚高准确率”

现象:训练时准确率 98%,验证集也 95%,一到现场实测只有 60%,换个戴眼镜的就认不出。

原因:划分数据集时用了随机洗牌,同一个学生的连续采样帧被同时分进训练和验证,模型相当于提前看过了验证题的答案。训练损失和验证损失都好看,真实场景却打回原形。

解决:严格按时间序切分,前 70% 到 80% 的帧做训练,后 20% 到 30% 做验证。更严格的做法是按采集批次切分:今天采的做训练,明天采的做验证,模拟真实场景下不同时间、不同光线的分布漂移。这条经验放在任何数据驱动的毕设里都适用,提前避开能少走很多弯路。

4.3 小样本下的严重过拟合:训练准确率到 100%,验证集却忽高忽低

现象:训练 10 轮后训练集准确率到 100%,验证集只有 82%,而且每个 epoch 之间差三四个百分点,模型明显在“背题”。

原因:总样本可能只有几百张,CNN 的参数量却达到几十万级别,模型直接把训练图片的收入记了下来,没有学到泛化的面部特征。Dropout 太小、数据增强太弱、全连接层太宽,三个问题往往同时存在。

解决:按顺序调整三处。第一,Dropout从 0.3 提到 0.5。第二,ImageDataGenerator的rotation_range加到 20,brightness_range放宽到 (0.7, 1.3)。第三,全连接层从 256 降到 128,同时把卷积核数量 64 改成 32,降低模型容量。小样本场景下模型“傻一点”反而更稳,这是一个反直觉但非常有效的原则。

4.4 多人同时入镜时识别互相干扰、考勤记录张冠李戴

现象:两个学生并肩进门,系统有时把两人识别成同一个名字,或者第一帧认出甲,第二帧把甲的名字安到乙头上。

原因:MTCNN 能检测出多个框,但每个框送入 CNN 时可能裁剪到旁边同学的脸部边缘或重合区域。另外当前代码是每帧对每个检测框独立预测,没有跨帧跟踪,身份标签在人脸位置抖动时容易跳变。

解决:做两层加固。第一,对检测框做过滤:去掉面积过小的框,并检查检测框之间的 IoU,重叠度过高的保留置信度更高的那个。第二,加入简单跨帧记忆:记录上一次识别成功的人脸位置,新帧优先对与旧位置 IoU 最大的框做预测,连续 3 到 5 帧都是同一个人才写入考勤。这样既避免单帧误判,也让多人场景下的记录更稳定。

4.5 模型在另一台电脑上加载失败或 CUDA 环境不生效

现象:源码和 h5 模型拷贝到室友电脑上,load_model报兼容性错误,或者明明装了 TensorFlow GPU 版,跑起来还是 CPU 的速度。

原因:Keras 模型跨版本加载时,如果训练和加载的 TensorFlow 版本相差太大(比如 2.6 训的模型拿到 2.15 加载),可能产生SavedModel兼容警告甚至报错。GPU 不生效的原因基本是 CUDA 与 cuDNN 版本和 TensorFlow 自带的不匹配,框架检测失败后会静默回退到 CPU。

解决:在源码 README 里锁定精确版本,写清楚pip install tensorflow==2.10.*这类命令。训练环境与运行环境不一致是毕设演示时最普遍的事故源头。GPU 跑不起来不用死磕,这套小模型用 CPU 推理一帧 80ms 完全能接受;追求更好的跨环境兼容性,就在训练完后额外导出一次 SavedModel 格式,比单独 h5 文件在版本迁移时更稳。

5. 验收与进阶:让考勤系统从“能跑”到“能答辩”

5.1 用混淆矩阵代替准确率评估模型

只看准确率在这个场景里不够可信,因为如果有人总是被认成另一个人,整体准确率可能还有 95%,但被误认的同学天天考勤失败。打印混淆矩阵,能直观看到每个学生被识别成谁、误判集中在哪个方向:

import os import numpy as np import tensorflow as tf from sklearn.metrics import confusion_matrix, classification_report model = tf.keras.models.load_model("../models/cnn_face_model.h5") val_root = "../dataset/processed/val" class_names = sorted(os.listdir(val_root)) class_to_idx = {name: i for i, name in enumerate(class_names)} y_true, y_pred = [], [] for class_name in class_names: class_dir = os.path.join(val_root, class_name) for img_file in os.listdir(class_dir): img = tf.keras.preprocessing.image.load_img( os.path.join(class_dir, img_file), color_mode="grayscale", target_size=(128, 128)) img = tf.keras.preprocessing.image.img_to_array(img) / 255.0 img = np.expand_dims(img, axis=0) prob = model.predict(img, verbose=0)[0] y_true.append(class_to_idx[class_name]) y_pred.append(int(np.argmax(prob))) print(classification_report(y_true, y_pred, target_names=class_names)) print(confusion_matrix(y_true, y_pred))

如果混淆矩阵显示张三和李四主要是在侧脸角度互认,那就针对侧脸补样本重训,这种“由数据驱动决策”的过程,正是答辩时老师最想看到的工程思路。

5.2 界面化与考勤导出的低成本收尾

命令行跑通和毕设演示之间还差一个界面。常见做法是用 Tkinter 包一个简单考勤面板:左边放摄像头画面,实时框出人脸和姓名置信度,右边放当天的考勤表格。Tkinter 是 Python 自带库,不需要额外安装,赶时间时最省事。核心实现就是把 3.3 节的识别函数挂到 UI 的定时器里,每 100 毫秒刷新一次画面,并同步更新表格。导出考勤 Excel 用 pandas 读 SQLite 再to_excel,十行代码就能闭环。

5.3 答辩高频问题与验收自查清单

答辩时最常被追问的三个问题:为什么选 CNN,样本不够怎么办,实时性和准确率是多少。这三个问题的答案分别在前面的 2.1、4.3 和 5.1 里。验收时建议过一遍下面的自查清单:光照变化下同一人能连续识别成功 10 次以上;两个人同时入镜能分别打上正确标签;置信度过滤能把陌生人挡在门外;同一个人重复出现在摄像头前不会重复记录考勤;界面版识别刷新率不低于 5fps。

我自己的习惯是,拿到任何毕设源码先做四件事:锁定 Python 版本到 3.8 到 3.10,锁定 TensorFlow 版本,把项目里所有路径改成相对路径,最后用绝对路径校验一遍数据目录能读通。做到这四条,换电脑演示时基本不会翻车。人脸识别考勤这个方向技术栈成熟、演示效果直观、提分点也清晰,只要把第 2 到第 4 章这套链路完整跑通,答辩的底气就有了。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询