ARTICLE DETAIL

资讯详情

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

基于CNN与PyQt5的人脸识别考勤系统实战:从模型训练到界面部署

基于CNN与PyQt5的人脸识别考勤系统实战:从模型训练到界面部署 简介本资源是一套基于CNN神经网络的人脸识别考勤系统完整项目采用Python与PyQt5开发面向计算机相关专业学生及需要完成毕业设计、期末大作业或课程设计的学习者。项目将卷积神经网络人脸识别与考勤签到功能结合解决传统考勤效率低、代签等问题适合具备一定Python基础、希望快速上手深度学习实战的读者。压缩包共30个文件约32.54MB包含11个py源码文件、3个ui界面文件、6个pyc编译文件以及caffemodel模型、prototxt网络配置、xml人脸检测器、ttf字体和jpg/png图片素材等覆盖模型、界面与资源三类内容。目前已有142人学习下载。代码注释完整新手也能看懂部署后即可运行项目功能完善、界面美观、操作简单可直接作为毕设或课程设计使用具有较高的实际应用与参考价值。1. 从一张考勤表说起CNN 人脸识别考勤系统到底在做什么先看一个真实场景。某小型公司前台每天早上要手动核对三十多号人的到岗情况用纸质签到表月底统计时发现代签、漏签、字迹潦草认不出的一堆。换成指纹打卡机之后手指脱皮、出汗、按不准的问题又来了。这类场景就是人脸识别考勤系统最典型的落地需求一台普通摄像头一套能跑在办公电脑上的程序员工走到镜头前自动识别身份并记录时间后台能导出考勤表。这个标题里的技术栈其实分三层。底层是 CNN 卷积神经网络负责从人脸图像中提取特征并完成身份分类中间层是 OpenCV 和深度学习推理框架负责摄像头取流、人脸检测、图像预处理上层是 PyQt5 桌面界面负责实时画面显示、员工信息管理、考勤记录查询和导出。三层各司其职缺一层这个系统就跑不起来。适合谁看正在做 Python 毕业设计的学生、想搭一套小型考勤原型的开发者、以及需要理解 CNN 从训练到部署完整链路的从业者。下面按「先跑通再优化」的顺序把每一层的实现细节和踩坑点讲清楚。2. CNN 人脸识别模型的选型、训练与导出2.1 为什么不用传统特征方法而选 CNN早期人脸识别方案用的是 Haar 级联加 LBPH 局部二值模式直方图原理是人脸区域的纹理统计特征做比对。这套方法在光照均匀、正脸、无遮挡的条件下能跑但一旦侧脸超过十五度、或者办公室灯光偏暖偏暗识别率断崖式下跌。CNN 的优势在于卷积核能自动学习从边缘到五官再到整体人脸结构的层次化特征对光照变化、小角度偏转、表情变化的鲁棒性明显更好。具体到考勤场景我一般推荐两种路线。第一种是轻量级分类网络比如一个五到八层的自定义 CNN输入 112×112 的灰度或 RGB 人脸图输出层节点数等于员工人数。这种方案训练快、推理快、代码量少适合毕业设计和小团队内部使用。第二种是迁移学习路线用预训练的 FaceNet 或 ArcFace 提取 128 维或 512 维特征向量再用人脸库做余弦相似度匹配。后者的优势是新增员工不需要重新训练模型只要把人脸特征入库即可扩展性更好。选哪条路取决于你的员工数量是否固定。如果就是给一个班级或一个固定团队做考勤人数不变第一种够用。如果人员会频繁变动第二种更省事。2.2 用 Keras 搭一个可用的 CNN 分类模型下面是一个可以直接跑的 CNN 模型定义输入尺寸 112×112输出类别数由num_classes控制。这个结构在几百到几千张人脸样本上训练准确率能到 95% 以上。import tensorflow as tf from tensorflow.keras import layers, models def build_face_cnn(num_classes, input_shape(112, 112, 3)): model models.Sequential([ # 第一层卷积提取边缘和纹理32个3x3卷积核 layers.Conv2D(32, (3, 3), activationrelu, paddingsame, input_shapeinput_shape), layers.BatchNormalization(), # 加速收敛防止梯度问题 layers.MaxPooling2D((2, 2)), # 特征图从112x112降到56x56 # 第二层卷积提取五官局部特征64个卷积核 layers.Conv2D(64, (3, 3), activationrelu, paddingsame), layers.BatchNormalization(), layers.MaxPooling2D((2, 2)), # 降到28x28 # 第三层卷积提取更全局的人脸结构特征 layers.Conv2D(128, (3, 3), activationrelu, paddingsame), layers.BatchNormalization(), layers.MaxPooling2D((2, 2)), # 降到14x14 layers.Flatten(), # 展平成一维向量 layers.Dropout(0.5), # 随机丢弃50%神经元防过拟合 layers.Dense(256, activationrelu), layers.Dropout(0.3), layers.Dense(num_classes, activationsoftmax) # 输出各类别概率 ]) model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-3), losscategorical_crossentropy, metrics[accuracy] ) return model逻辑说明三层卷积逐步扩大感受野从局部纹理到全局结构。每层卷积后接 BatchNormalization 是为了让每批数据的分布稳定训练时不容易震荡。两个 Dropout 层分别放在全连接前后是防止小数据集上过拟合的关键。最后一层 softmax 输出每个员工类别的概率取最大值对应的类别作为识别结果。参数说明input_shape设为 112×112×3 是权衡了精度和速度的结果再大推理会慢再小人脸细节丢失严重。learning_rate初始设 1e-3如果训练 loss 震荡明显就降到 1e-4。num_classes必须等于你实际员工数量多一个少一个都会导致标签错位。2.3 数据采集与增强的实操要点训练数据从哪来最直接的办法是用 OpenCV 调摄像头让每个员工录 100 到 200 张不同角度和表情的人脸图。采集脚本的核心逻辑是检测到人脸后裁剪出人脸区域缩放到 112×112按员工姓名建文件夹保存。import cv2 import os def collect_faces(person_name, save_dirdataset, max_count150): save_path os.path.join(save_dir, person_name) os.makedirs(save_path, exist_okTrue) cap cv2.VideoCapture(0) # 0是默认摄像头 face_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml) count 0 while count max_count: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale(gray, 1.3, 5) for (x, y, w, h) in faces: # 向外扩展10%边界保留更多面部上下文 margin int(0.1 * w) x1 max(0, x - margin) y1 max(0, y - margin) x2 min(frame.shape[1], x w margin) y2 min(frame.shape[0], y h margin) face_img frame[y1:y2, x1:x2] face_img cv2.resize(face_img, (112, 112)) count 1 cv2.imwrite(os.path.join(save_path, f{count}.jpg), face_img) cv2.rectangle(frame, (x, y), (xw, yh), (0, 255, 0), 2) cv2.imshow(Collecting, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明用 Haar 级联做人脸检测虽然精度不如深度学习检测器但速度快、依赖少采集阶段够用。裁剪时向外扩 10% 边界是为了让模型看到下巴和额头的一部分避免只看到五官中心导致泛化差。参数说明max_count150是经验值太少模型学不够太多采集时间长且冗余。detectMultiScale的1.3是缩放步长5是最小邻居数这两个值调大检测更严格但可能漏检调小则误检增多。采集完之后必须做数据增强否则模型对光照和角度的泛化能力很差。Keras 的ImageDataGenerator可以一行代码搞定旋转、平移、亮度调整。from tensorflow.keras.preprocessing.image import ImageDataGenerator train_datagen ImageDataGenerator( rescale1./255, # 像素值归一化到0-1 rotation_range15, # 随机旋转±15度 width_shift_range0.1, # 水平平移10% height_shift_range0.1, # 垂直平移10% brightness_range[0.8, 1.2], # 亮度随机调整 horizontal_flipTrue, # 水平翻转 validation_split0.2 # 20%做验证集 )这里有个容易翻车的地方水平翻转对人脸是合理的但如果你做的是有方向性的人脸识别比如区分左右侧脸翻转会引入错误标签。考勤场景一般不需要区分左右可以开。2.4 训练完成后的模型导出与推理封装训练完成后模型保存为.h5格式推理时直接加载。但要注意Keras 模型在 PyQt5 界面里直接调用会阻塞主线程导致界面卡死。常见做法是把推理放在单独的QThread里或者用QTimer定时触发单帧推理。# 保存模型 model.save(face_model.h5) # 推理时加载 from tensorflow.keras.models import load_model import numpy as np model load_model(face_model.h5) # 假设已经有人脸图 face_img形状(112,112,3) face_input face_img.astype(float32) / 255.0 face_input np.expand_dims(face_input, axis0) # 增加batch维度 predictions model.predict(face_input, verbose0) class_id np.argmax(predictions) confidence predictions[0][class_id] if confidence 0.85: # 置信度阈值 print(f识别为员工ID: {class_id}置信度: {confidence:.2f}) else: print(置信度不足拒绝识别)置信度阈值设 0.85 是一个保守值。设太低会误识别陌生人设太高会导致员工本人偶尔打不上卡。实际部署时建议先用一批测试图跑一遍看员工本人的最低置信度是多少再往下留 5% 的余量作为阈值。3. PyQt5 界面搭建从摄像头画面到考勤记录3.1 界面布局设计与核心控件选型PyQt5 做桌面考勤界面核心控件就几个QLabel显示摄像头实时画面QPushButton做打卡和注册按钮QTableWidget显示考勤记录QLineEdit做员工姓名输入。布局用QHBoxLayout和QVBoxLayout嵌套左边放视频画面右边放操作面板和记录表。from PyQt5.QtWidgets import (QMainWindow, QWidget, QLabel, QPushButton, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem, QLineEdit) from PyQt5.QtCore import Qt, QTimer from PyQt5.QtGui import QImage, QPixmap import cv2 class AttendanceWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(人脸识别考勤系统) self.resize(1000, 600) # 左侧摄像头画面 self.video_label QLabel(摄像头未启动) self.video_label.setFixedSize(640, 480) self.video_label.setAlignment(Qt.AlignCenter) self.video_label.setStyleSheet(border: 2px solid #333;) # 右侧操作区 self.name_input QLineEdit() self.name_input.setPlaceholderText(输入员工姓名) self.register_btn QPushButton(注册人脸) self.attendance_btn QPushButton(开始考勤) self.record_table QTableWidget(0, 3) self.record_table.setHorizontalHeaderLabels([姓名, 时间, 状态]) right_layout QVBoxLayout() right_layout.addWidget(self.name_input) right_layout.addWidget(self.register_btn) right_layout.addWidget(self.attendance_btn) right_layout.addWidget(self.record_table) main_layout QHBoxLayout() main_layout.addWidget(self.video_label) main_layout.addLayout(right_layout) container QWidget() container.setLayout(main_layout) self.setCentralWidget(container) # 摄像头与定时器 self.cap cv2.VideoCapture(0) self.timer QTimer() self.timer.timeout.connect(self.update_frame) self.timer.start(30) # 约33fps def update_frame(self): ret, frame self.cap.read() if not ret: return # 在这里调用人脸检测和识别逻辑 rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch rgb.shape qt_img QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888) self.video_label.setPixmap(QPixmap.fromImage(qt_img).scaled( self.video_label.size(), Qt.KeepAspectRatio))逻辑说明QTimer每 30 毫秒触发一次update_frame从摄像头读一帧转成 QImage 再显示到 QLabel 上。这是 PyQt5 显示 OpenCV 视频流的标准做法。注意 OpenCV 读出来是 BGR 格式Qt 显示需要 RGB所以必须做颜色空间转换否则画面颜色会偏蓝。参数说明timer.start(30)里的 30 是毫秒对应约 33 帧每秒。如果你的电脑性能一般可以改成 50 或 66降低刷新率减少 CPU 占用。video_label.setFixedSize(640, 480)是显示区域大小和摄像头分辨率匹配最好不匹配时KeepAspectRatio会自动缩放但可能留黑边。3.2 把识别结果写进考勤表并去重考勤的核心逻辑是识别到员工后检查今天是否已经打过卡没打过就插入一条记录打过就更新状态或忽略。这里用 SQLite 做本地存储最省事不需要额外装数据库服务。import sqlite3 from datetime import datetime def init_db(): conn sqlite3.connect(attendance.db) c conn.cursor() c.execute(CREATE TABLE IF NOT EXISTS records ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, date TEXT NOT NULL, time TEXT NOT NULL, status TEXT DEFAULT 正常 )) conn.commit() return conn def mark_attendance(conn, name): today datetime.now().strftime(%Y-%m-%d) now datetime.now().strftime(%H:%M:%S) c conn.cursor() # 检查今天是否已有记录 c.execute(SELECT id FROM records WHERE name? AND date?, (name, today)) if c.fetchone(): return False # 已打卡不重复插入 c.execute(INSERT INTO records (name, date, time) VALUES (?, ?, ?), (name, today, now)) conn.commit() return True逻辑说明先查后插是最简单的去重方式。date字段单独存是为了按天查询方便不用从时间戳里截取。status字段默认「正常」后续可以根据打卡时间判断是否迟到比如超过 9:00 就更新为「迟到」。参数说明数据库文件attendance.db放在程序同级目录即可。如果多人共用一台机器SQLite 的并发写入能力有限但考勤场景下写入频率极低完全够用。3.3 用 QThread 解决界面卡死问题这是 PyQt5 做人脸识别最常翻车的地方。如果你直接在update_frame里调用model.predict()每帧推理耗时 50 到 200 毫秒界面会明显卡顿甚至无响应。正确做法是把推理放到独立线程里。from PyQt5.QtCore import QThread, pyqtSignal import numpy as np class InferenceThread(QThread): result_ready pyqtSignal(str, float) # 信号姓名, 置信度 def __init__(self, model, class_names): super().__init__() self.model model self.class_names class_names self.frame None self.running True def set_frame(self, frame): self.frame frame def run(self): while self.running: if self.frame is None: self.msleep(10) continue face cv2.resize(self.frame, (112, 112)) face face.astype(float32) / 255.0 face np.expand_dims(face, axis0) preds self.model.predict(face, verbose0) idx np.argmax(preds) conf float(preds[0][idx]) if conf 0.85: self.result_ready.emit(self.class_names[idx], conf) self.frame None self.msleep(10)逻辑说明子线程持续等待新帧有帧就推理推理完通过信号把结果发回主线程更新界面。pyqtSignal是跨线程通信的安全方式不要直接在子线程里操作 UI 控件。参数说明msleep(10)是线程休眠 10 毫秒避免空转占满 CPU。conf 0.85的阈值和前面模型推理部分保持一致。4. 避坑与排查人脸考勤系统最常见的五个翻车点4.1 摄像头画面正常但识别率极低现象界面能显示摄像头画面但十次有八次识别失败或识别成别人。原因训练时的人脸裁剪方式和推理时不一致。训练用的是 Haar 检测后扩 10% 边界再缩放推理时如果直接整帧缩放人脸在画面中的比例和位置完全不同模型相当于在看另一种分布的数据。解决推理时必须走和训练完全相同的预处理流程——先检测人脸再扩边界再缩放到 112×112。把预处理封装成一个函数训练和推理共用。4.2 PyQt5 界面启动后闪退无报错现象双击运行程序窗口一闪就没了命令行也没输出错误。原因最常见的是QApplication没有在创建窗口之前实例化或者sys.exit(app.exec_())写成了app.exec()没有接sys.exit。另一个可能是 OpenCV 的cv2.VideoCapture(0)返回了空对象后续cap.read()直接抛异常。解决在main函数里严格按QApplication → 窗口实例 → app.exec_()的顺序写。摄像头打开后加一句if not self.cap.isOpened(): print(摄像头打开失败)做保护。4.3 模型训练 loss 不下降或震荡严重现象训练几十个 epochloss 一直在 2.0 附近波动准确率上不去。原因学习率太大导致梯度爆炸或者数据没有做归一化。Keras 的ImageDataGenerator里rescale1./255如果忘了写输入像素值在 0 到 255 之间卷积核权重更新会非常剧烈。解决先确认rescale写了。然后把学习率从 1e-3 降到 1e-4 试一轮。如果还不行检查标签编码是否正确——categorical_crossentropy要求标签是 one-hot 格式如果你传的是整数标签要用sparse_categorical_crossentropy。4.4 新增员工后必须重新训练整个模型现象公司来了新员工你发现用分类网络方案必须把所有人脸重新训练一遍耗时且麻烦。原因分类网络的输出层节点数固定等于训练时的类别数新增一类就要改输出层维度并重新训练。解决如果人员变动频繁换用特征向量方案。用预训练模型提取人脸特征存入数据库识别时算余弦相似度。新增员工只需提取特征入库不用碰模型。这是选型阶段就要考虑的问题不要等到部署了才发现。4.5 考勤记录时间与实际不符现象员工明明 8:55 打的卡记录里显示 9:05。原因摄像头读取和推理有延迟如果打卡时间取的是「识别完成时刻」而不是「人脸进入画面时刻」会有几秒到十几秒的偏差。另外如果程序启动时没有校准系统时间也可能有累积误差。解决在检测到人脸的那一帧就记录时间戳而不是等推理完成再取时间。如果对时间精度要求高可以在程序启动时从网络时间服务器同步一次系统时间。5. 从能跑到好用三个让考勤系统真正落地的技巧5.1 用置信度平滑过滤掉偶发误识别单帧识别的置信度会有波动同一个人连续几帧可能一会儿 0.92 一会儿 0.78。如果每帧都触发打卡逻辑会出现同一个人被反复识别、或者偶尔被误识别成别人的情况。我一般会做一个滑动窗口连续 5 帧中至少 3 帧识别为同一个人且平均置信度超过 0.85才真正触发打卡。这个逻辑用一个字典缓存最近几帧的结果就能实现代码量不超过 20 行但能显著降低误识别率。5.2 考勤数据导出为 Excel 的实用写法SQLite 里的记录最终要交给行政或 HR导出成 Excel 是最通用的格式。用pandas一行代码就能搞定但要注意中文字段名和日期格式。import pandas as pd import sqlite3 def export_to_excel(db_pathattendance.db, output考勤记录.xlsx): conn sqlite3.connect(db_path) df pd.read_sql_query( SELECT name AS 姓名, date AS 日期, time AS 时间, status AS 状态 FROM records ORDER BY date DESC, time DESC, conn ) df.to_excel(output, indexFalse, engineopenpyxl) conn.close() print(f已导出 {len(df)} 条记录到 {output})逻辑说明直接用 SQL 的AS别名把英文字段名转成中文这样导出的 Excel 表头就是中文不用在 pandas 里再改。ORDER BY date DESC, time DESC保证最新的记录在最上面。参数说明需要提前pip install pandas openpyxl。如果记录量超过几万条导出会慢一些但考勤场景一般不会到这个量级。5.3 一个容易被忽略的细节人脸注册时的质量校验注册环节如果采到的人脸图质量差模糊、过暗、侧脸太大会直接拉低后续识别率。我习惯在注册时加一道校验用 OpenCV 的拉普拉斯算子算图像清晰度低于阈值就提示重新采集。import cv2 def check_face_quality(face_img): gray cv2.cvtColor(face_img, cv2.COLOR_BGR2GRAY) # 拉普拉斯方差值越小越模糊 laplacian_var cv2.Laplacian(gray, cv2.CV_64F).var() if laplacian_var 100: return False, f图像模糊清晰度 {laplacian_var:.0f}请重新采集 # 检查亮度 mean_brightness gray.mean() if mean_brightness 50: return False, 光线过暗请调整环境光 if mean_brightness 200: return False, 光线过曝请避免强光直射 return True, 质量合格逻辑说明拉普拉斯方差是衡量图像高频分量多少的指标模糊图像高频分量少方差值低。亮度均值判断过暗或过曝。这两个指标结合起来能过滤掉大部分低质量样本。参数说明清晰度阈值 100 是经验值不同摄像头和分辨率下可能需要微调。建议先用一批正常样本跑一遍看方差分布再定阈值。这套系统从模型训练到界面交互再到数据存储完整跑通大概需要两到三周业余时间。最大的坑不在 CNN 本身而在预处理一致性和线程安全这两件事上。我自己的习惯是每改一次预处理逻辑就把训练和推理的代码放在同一个文件里对照着看一遍确认两边完全一致再跑。希望帮到你。本文还有配套的精品资源点击获取
返回列表