Python+CNN人脸识别考勤系统:从特征提取到数据库落库

发布时间:2026/10/3 17:53:19
Python+CNN人脸识别考勤系统:从特征提取到数据库落库 简介基于卷积神经网络的学生人脸识别考勤系统是一份完整的毕业设计项目面向计算机、通信、人工智能、自动化等专业的学生与从业者可直接用于课程设计、大作业或毕业设计。项目采用Python语言实现涵盖人脸检测、特征提取、数据库存储与考勤管理流程代码经过调试测试具备较好的运行稳定性与二次开发空间。整套资源共55个文件包括23个Python脚本、12个Qt界面UI文件、7个pyc编译文件、3个文本说明、3张示例图片、3个检测模型npy数据文件、2个SQLite数据库文件及1份Word版手册压缩包整体约2.22MB目录划分清晰便于按模块查阅与修改。目前已有213人学习浏览适合初学者逐步理解卷积神经网络在识别考勤场景中的应用也可为高水平开发者提供改造基础例如替换模型或扩展界面功能整体具有较高的参考与借鉴价值。1. 这个课题到底在解决什么CNN 人脸识别与考勤的边界把摄像头架在教室门口学生进门的瞬间自动打卡这就是这个课题最朴素的描述。很多刚开始接触 python 的人以为难点在卷积神经网络本身实际上做过一遍就会发现模型把人认出来只是第一步真正让项目卡壳的是“认出来之后怎么把考勤状态写对、写不重复、还能在答辩时讲清楚”。这份毕业设计源码把 CNN 人脸识别和考勤系统串成一条完整链路覆盖人脸检测、特征提取、数据库落库、迟到判定和配套文档说明适合做毕设、课设也适合想从算法练习跨到完整业务系统的入门者。它解决的不是“人脸识别准确率刷到 99%”这种单点问题而是“一个可演示、可解释、可扩展的考勤系统该怎么做”。2. 拆解系统骨架数据流、数据库设计与 CNN 选型理由2.1 人脸考勤的全流程数据流从摄像头帧到一条考勤记录一个完整的人脸考勤系统数据流是单向的每一环的输出都是下一环的输入。拿最常见的实现顺序来说摄像头逐帧读取画面拿到的是 BGR 格式的图像帧。人脸检测模块在帧里找人脸框返回坐标 (x, y, w, h)。把人脸区域裁剪出来做缩放和归一化统一成模型输入尺寸。CNN 特征提取网络把这张人脸变成一个固定长度的向量常见的是 128 维。这个向量和数据库里预存的每个学生的特征向量算距离距离最小的那个且小于阈值的人判定为匹配成功。匹配成功后把 student_id、当前时间写入考勤表再根据时间判断状态是正常还是迟到。这套流程里第 4 步是卷积神经网络的主场但第 1、2、5、6 步反而是翻车高发区。摄像头的帧率、画面的光线、检测框的抖动、数据库并发写入任何一个环节出问题识别率再高也白搭。所以拿到源码包之后第一步不是去读模型代码而是先把这条数据流在脑子里过一遍搞清楚每一步的数据长什么样、存在哪里、传给谁。2.2 MySQL 表设计与签到状态机考勤系统的核心数据是两张表学生信息表和考勤记录表。学生表存学号、姓名、特征向量考勤表存谁在什么时间打了卡。两张表通过 student_id 关联。字段类型说明idINT 自增主键student_idVARCHAR(20)学号唯一nameVARCHAR(50)姓名face_encodingTEXT128 维特征向量序列化成字符串存储created_atDATETIME建档时间考勤记录表的设计决定了后面迟到判定的代码怎么写。注意一个关键点同一天同一个学生只应该有一条考勤记录重复识别不能插入新记录。这个约束用数据库唯一索引来做比在业务代码里先查再插要可靠得多能避免并发场景下两条重复数据同时写入。CREATE TABLE attendance ( id INT AUTO_INCREMENT PRIMARY KEY, student_id VARCHAR(20) NOT NULL, attendance_date DATE NOT NULL, check_time DATETIME NOT NULL, status ENUM(normal, late, absent) DEFAULT normal, UNIQUE KEY uk_student_date (student_id, attendance_date) );建表时的 UNIQUE KEY 是签到逻辑的基石。有了它后面用INSERT IGNORE就能保证一天一人一条记录重复识别时直接忽略不会把早到的正常记录覆盖成迟到。状态字段用 ENUM 而不是字符串数据库层面就能拦截非法值。字段名建议统一用 check_time避免和 MySQL 关键字冲突。状态机的逻辑也很直接上课时间之前识别到写 normal迟到判定时间之后识别到写 late一整天没有记录的在日终批处理里补写 absent。不要在写入考勤记录的同时去修改历史数据状态一旦落库就不动它缺勤是离线统计出来的不是实时算出来的。这样各模块职责清晰答辩时也容易解释。2.3 为什么选 CNN 而不是传统人脸识别算法选型这个问题决定了项目的技术方向和答辩深度。如果只是做一个能跑的考勤demoOpenCV 自带的 LBPH 人脸识别器也能识别训练还快十几行代码就完事但它的上限很低光照变化大一点、学生换个发型、戴个眼镜识别率就明显下滑。这是因为 LBPH 本质上是提取局部纹理直方图它描述的是“这张脸长什么样”而不是“这张脸是谁的本质特征”泛化能力有限。卷积神经网络CNN的思路不同。它通过多层卷积核堆叠逐层从像素里抽象出边缘、纹理、五官结构最后在全连接层之后输出一个低维向量作为人脸的身份嵌入。这个向量学到的不是具体像素而是不同人脸之间的可区分性。用三元组损失训练出来的模型同一个人的不同照片在向量空间里距离近不同人的照片距离远。项目里常说的“128 维特征”就是这么来的。选 CNN 还有一个现实理由工程上太方便了。训练好的模型可以导出成 ONNX 格式用 OpenCV 的 DNN 模块直接加载推理不需要额外装 PyTorch 或 TensorFlow 的运行时。这对毕业设计的部署环境非常友好。普通笔记本用 CPU 跑一次特征提取大约 50 到 100 毫秒做课堂考勤完全够用。3. 把核心链路跑通检测、128 维特征与签到落库代码3.1 人脸检测OpenCV Haar 还是 MTCNN人脸检测是整条链路的入口检测框不准后面的一切都失真。OpenCV Haar 级联是最省事的方案一个 XML 文件就能跑适合快速验证流程。MTCNN 是深度学习的检测方案能额外输出五个关键点坐标方便做人脸对齐但安装依赖更重CPU 上速度也慢一些。方案速度关键点适用场景OpenCV Haar极快CPU 实时无流程演示、快速验证MTCNN较慢约 100ms/帧双眼鼻尖嘴角需要对齐后做高精度识别先用 Haar 把整条链路跑通是性价比最高的路径。检测到人脸框之后写一个 webcam 采集脚本存人脸图为后面的建档和训练准备数据。import cv2 import os import time camera cv2.VideoCapture(0) face_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) student_id 2024001 save_dir fdataset/{student_id} os.makedirs(save_dir, exist_okTrue) count 0 while count 20: ret, frame camera.read() if not ret: continue gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale( gray, scaleFactor1.1, minNeighbors5, minSize(64, 64) ) for (x, y, w, h) in faces: face_roi frame[y:y h, x:x w] cv2.imwrite(f{save_dir}/{count}_{int(time.time())}.jpg, face_roi) count 1 cv2.rectangle(frame, (x, y), (x w, y h), (0, 255, 0), 2) cv2.imshow(collect, frame) if cv2.waitKey(1) 0xFF ord(q): break camera.release() cv2.destroyAllWindows()scaleFactor1.1表示每次缩放检测窗口的倍率值越小检测越精细但越慢。minNeighbors5是每个候选框需要满足的邻居数量值越大误检越少但太大会漏检远处的人脸。采集时注意每一个学生保持 20 张以上并且变换角度、光线、表情尽量模拟真实考勤场景下的变化。另外采样间隔用时间戳命名可以天然去重避免一帧里重复写文件。3.2 用 CNN 提取 128 维人脸特征ONNX 推理代码训练好的 CNN 模型一般不直接部署而是先导出成 ONNX再用 OpenCV 的 DNN 加载。这样做的好处是推理环境只需要 opencv-python 和 numpy不需要把整个深度学习框架带到考场或者客户机器上。项目里的模型路径建议统一放在 models 目录下和主代码分离。import cv2 import numpy as np MODEL_PATH models/face_embedding.onnx INPUT_SIZE 160 net cv2.dnn.readNetFromONNX(MODEL_PATH) def get_embedding(face_img): # OpenCV 读进来是 BGR模型训练时用的是 RGB先做通道转换 rgb cv2.cvtColor(face_img, cv2.COLOR_BGR2RGB) resized cv2.resize(rgb, (INPUT_SIZE, INPUT_SIZE)) # 归一化到 [0, 1]再按训练时的 mean/std 做标准化 normalized resized.astype(np.float32) / 255.0 mean np.array([0.485, 0.456, 0.406], dtypenp.float32) std np.array([0.229, 0.224, 0.225], dtypenp.float32) normalized (normalized - mean) / std # 构造 NCHW 格式的 blob 并前向推理 blob cv2.dnn.blobFromImage(normalized) net.setInput(blob) embedding net.forward().flatten() # L2 归一化让所有特征向量长度一致距离比较才有意义 embedding embedding / np.linalg.norm(embedding) return embedding特征提取这一步有两个必须注意的参数。第一个是输入尺寸模型训练时用 160x160推理就必须用 160x160随便改大会增加计算量改小会丢信息。第二个是归一化方式训练时怎么减均值除方差推理时就要保持一致否则特征分布偏移距离比较全部失效。flatten()把模型输出从 (1, 128, 1, 1) 压成一维数组np.linalg.norm做的是 L2 归一化归一化之后所有特征向量的模长都是 1算欧氏距离的范围就固定下来了。建档时把每个学生的多张照片分别提取特征取平均作为该学生的基准特征。这个平均操作能抹掉单张照片里的噪声比如眨眼、灯光闪烁造成的特征波动。3.3 签到落库与迟到判定SQL 写入的边界处理识别到学生之后剩下的事情要和数据库打交道。这个环节最常见的错误是一帧识别成功了就往表里插一条记录结果学生站在门口等朋友十秒钟被插了二十条考勤。解决办法有两个数据库层面加唯一索引代码层面用 INSERT IGNORE。前面已经建好了带唯一索引的表这里直接写写入逻辑。import pymysql from datetime import datetime, time DB_CONFIG { host: localhost, user: root, password: 123456, database: attendance, charset: utf8mb4, } def check_in(student_id, cursor): now datetime.now() today now.date() # 迟到判定线9 点前算正常之后算迟到 late_boundary datetime.combine(today, time(9, 0, 0)) status normal if now late_boundary else late sql INSERT IGNORE INTO attendance (student_id, attendance_date, check_time, status) VALUES (%s, %s, %s, %s) cursor.execute(sql, (student_id, today, now, status))用 INSERT IGNORE 之后重复识别同一个学生只会写入第一条记录后面全部被唯一索引挡住不会更新原始状态。这样的语义是“第一次打卡为准”契合考勤直觉早到的人不会被后来的一条迟到记录覆盖。如果项目要做到“早退重签”或者“外出回来再签一次”那就得改成ON DUPLICATE KEY UPDATE并且要额外设计状态变更规则这个属于边界需求通常毕设阶段不需要。写库之后最好加一个返回提示让前端界面知道这次签到是新增还是忽略。常见做法是把cursor.rowcount返回出来1 表示新插入0 表示重复识别。这样摄像头画面里可以显示“张三已签到”和“张三重复识别”两种不同提示演示效果会好很多。4. 避坑记录CNN 人脸考勤最常见的 5 个翻车现场4.1 中文路径下读不出图片cv2.imread 的玄学失败现象从数据库或者 Excel 导入学生名单之后程序报错说找不到图片但路径明明是对的。手动打开图片资源管理器能看到代码就是读不出来。原因OpenCV 的imread底层调用的 C 接口不支持中文路径。Windows 系统下只要路径里出现中文文件夹名或者中文文件名就会返回 None。用学生姓名做目录名的人几乎必踩这个坑。解决读图片不要直接用cv2.imread(path)改用先把文件读成字节数组再交给cv2.imdecode解码。import cv2 import numpy as np def imread_unicode(path): data np.fromfile(path, dtypenp.uint8) img cv2.imdecode(data, cv2.IMREAD_COLOR) return img这个函数在 Windows 和 Linux 下表现一致写完直接替换全项目所有cv2.imread调用。同理cv2.imwrite写中文路径也会失败解决办法是对应使用cv2.imencode加tofile。4.2 阈值拍脑袋乱设识别率看着高现场全是误检现象demo 阶段用一张自己的照片测怎么测怎么准。到了教室实际跑A 同学刷脸成功签了到后台一看签的是 B 同学的名字。原因特征比对阈值设得太宽松了。人脸特征向量经过 L2 归一化后欧氏距离范围大致在 0 到 2 之间不同人之间的距离在 0.8 到 1.5 之间很常见。如果阈值设成 1.2等于默认“只要长得有点像我数据库里的人就算匹配成功”。解决阈值必须用数据标定不能拍脑袋。用 10 个人的数据做一次离线测试计算“同一个人的照片两两之间的距离”和“不同人照片两两之间的距离”取两类距离分布交界处的值作为阈值。常见做法是设成等错误率点也就是误拒率和误识率相等的那个数值。4.3 检测框抖动导致重复打卡刷屏现象画面里一个人识别模块时而框住整张脸时而只框住半张脸特征向量在临界点附近波动一会儿匹配成 A一会儿匹配失败。数据库连着插好几条记录。原因Haar 检测框不是稳定的。人脸轻微转头、光线波动都会让检测框的位置和大小跳变裁剪出来的区域内容跟着变特征向量随之漂移。解决写库时已经在数据库层面做了去重但更有效的办法是在业务逻辑里加时间窗口锁。同一个 student_id 在 5 分钟内只允许签到一次不管识别成功多少次。这个限制可以放在内存缓存里也可以用一张临时表实现属于典型的防御性写法。4.4 光线一变识别率骤降卷积神经网络的黑匣子问题现象上午拉窗帘的教室识别正常下午阳光斜射进教室一大半人脸检测不到检测到的也匹配不上。原因训练数据里没有覆盖这种光线条件。CNN 模型虽然比传统算法鲁棒但它学到的特征分布受训练数据影响极大。如果建档照片全是实验室的荧光灯环境那模型对强逆光、侧光的表征能力就是缺失的。解决建档时多采集几个时间段、几个角度的照片比如上午、下午、室内、走廊各拍几张。更直接的办法是在采集脚本里做数据扩增把亮度、对比度随机变化后的图片也加进特征库。另外推荐在摄像头画面上加一个简单的亮度检测画面整体过暗或过亮时给出提示而不是拿劣质帧硬跑识别。4.5 戴口罩之后特征失效预处理救不回来的硬伤现象Demo 答辩时没人戴口罩效果完美。第二天实际使用一半学生戴着口罩进教室识别结果全部落在阈值之外。原因口罩遮住了鼻子和嘴巴CNN 提取的特征向量里对应区域的信息全部丢失向量整体偏移。这不是调阈值能解决的问题阈值放宽就会开始误识别。解决两个方向。一是改检测环节戴上口罩的人脸框本身也发生变化先保证检测框能稳定框住眼部区域然后用眼周特征做匹配。二是业务上做降级处理检测到口罩时自动转向人工确认流程在界面上弹出该学生的照片让管理员核对。很多商业门禁系统也是这样做的不算功能缺陷而是产品设计的一部分。5. 训练与调优把识别率从 demo 拉到答辩可用5.1 数据准备与扩增每人至少 20 张的多角度样本人脸识别模型的效果三分模型七分数据。对于毕业设计来说如果用开源预训练模型做特征提取建档数据的质量直接决定最终效果。每名学生至少采集 20 张人脸图要求覆盖正面、左右轻微侧头、抬头低头以及上午和下午两个时间段的自然光环境。数据扩增是弥补样本量不足最有效的手段。下面这段代码模拟了真实环境中最常见的变化左右翻转、亮度调整、高斯噪声。import cv2 import numpy as np def augment_face(image): augmented [] # 水平翻转模拟人脸左右侧转 flipped cv2.flip(image, 1) augmented.append(flipped) # 亮度增强模拟光线变强 brighter cv2.convertScaleAbs(image, alpha1.2, beta20) augmented.append(brighter) # 亮度减弱模拟逆光或者阴影 darker cv2.convertScaleAbs(image, alpha0.8, beta-20) augmented.append(darker) # 高斯噪声模拟摄像头传感器噪声 noise np.random.normal(0, 4, image.shape).astype(np.uint8) noisy cv2.add(image, noise) augmented.append(noisy) return augmented扩增的本质是告诉卷积神经网络“同一个人可以长这样”让模型在后续比对时对这些变化不敏感。建档时如果每个学生有 20 张原始照片扩增后变成 80 张特征提取时对 80 个特征向量取平均基准特征的稳定性会明显提升。注意alpha和beta的值不要过大亮度变化太夸张反而让模型学到错误关联。5.2 训练参数与阈值校准让 FAR 和 FRR 都看得过去如果项目需要自己训练 CNN 而不是直接用预训练模型最核心的参数就是下面这几个。参数建议值说明输入尺寸160x160太小丢细节太大增加计算量批量大小32显存不够就降到 16初始学习率1e-3用余弦退火或阶梯下降衰减训练轮数50 到 100配合早停防止过拟合特征维度128主流人脸识别模型的标配三元组间隔0.2 到 0.5同类与异类距离的最小间隔训练时最需要盯的是损失曲线不要只看最终准确率。三元组损失如果下降平稳说明模型正在把同类样本拉近、异类样本推远。如果损失震荡严重优先调低学习率其次是检查数据是否乱掉了比如同一个人的照片混进了另一个人的脸。阈值校准需要同时看两类错误误识 FRR 是“是他但没认出来”误识 FAR 是“不是他却被认成他”。考勤场景里宁可多让老师看一眼也不能认错人所以阈值应该往严格方向调。实操上先算同类距离和异类距离的分布再画一条 ROC 曲线取误识率低于 1% 的阈值点。5.3 离线评估用混淆矩阵而不是“看起来能识别”答辩时最怕的一句话是“你这个准确率是多少”如果回答“看起来挺准的”基本等于送命。所以一定要准备一个可复现的离线评估流程把采集好的数据分成测试集跑一遍识别输出准确率和混淆矩阵。评估脚本的思路是不调用摄像头直接读取每张测试照片提取特征和建档特征库比对得到预测结果再和真实标签对比。这样跑出来的指标才是可信的。评估时特别关注“哪些人被经常认错”混淆矩阵的行列交叉点会直接暴露容易混淆的人脸比如两个戴相似眼镜的男生。评估结果出来后如果某个人的准确率明显低于其他人就去检查建档照片质量大概率是照片模糊、光线过暗或者角度太偏。补拍这个人几个角度的照片重新建档比重新训练模型省事得多。6. 从课设到可用离线评估脚本与几个进阶方向6.1 一个能算准确率的离线评估脚本下面这段脚本是评估流程的最小实现把测试集里每张图依次和特征库比对最后统计识别准确率。import os import numpy as np from collections import defaultdict def evaluate(test_dir, embeddings_db): correct 0 total 0 confusion defaultdict(int) for student_id in os.listdir(test_dir): student_dir os.path.join(test_dir, student_id) if not os.path.isdir(student_dir): continue for img_name in os.listdir(student_dir): img_path os.path.join(student_dir, img_name) target get_embedding(imread_unicode(img_path)) best_id, best_dist None, float(inf) for db_id, db_emb in embeddings_db.items(): dist np.linalg.norm(target - db_emb) if dist best_dist: best_dist dist best_id db_id total 1 if best_dist THRESHOLD and best_id student_id: correct 1 confusion[(student_id, best_id)] 1 print(faccuracy: {correct / total:.2%})这个脚本的输入是两个目录一个是建档特征库一个是测试集。测试集的数据必须是建档时没见过的照片拿同一批照片又建档又测试准确率会虚高到没有参考意义。运行完看混淆矩阵行是真实学生列是预测结果对角线越粗越好。6.2 答辩前值得加的两个方向第一个方向是活体检测。现在大多数 CNN 人脸识别模型都能被一张照片打印件骗过去在摄像头前面放一张打印照片就能完成签到这在答辩时会被老师直接质疑。最简单的活体检测是眨眼检测连续几帧里判断眼睛的开合状态没有眨眼动作就判定为照片攻击。用 OpenCV 的眨眼检测加一个简单的状态计数几十行代码就能实现。第二个方向是轻量化部署。如果项目能跑通完整流程可以顺手把模型从 FaceNet 换到 MobileFaceNet网络更小CPU 推理速度能降到 30 毫秒以内。换成轻量模型之后整个系统就可以脱离 GPU 运服务器在普通笔记本甚至树莓派上跑这刚好对应了“人脸识别门禁机”这类终端设备的轻量化思路。答辩时可以讲清楚为什么选轻量模型、换模型后准确率损失了多少这比堆一堆花哨功能更能体现工程理解。我做这个方向的课设时最深刻的教训就是一开始把阈值随手设成 0.6结果现场 10 个人有 3 个互相认错被老师当场指出来。后来老老实实采集数据、跑距离分布、画混淆矩阵才把误识压下去。参数这种东西必须从自己的数据里长出来不能靠感觉。项目本身不难难的是每一步都不糊弄希望帮到你。本文还有配套的精品资源点击获取