资讯动态

人脸识别考勤系统:CNN与PyQt5从特征提取到界面实现

发布时间:2026/9/13 16:35:21 来源:尧图企业网站定制
简介基于CNN神经网络的人脸识别考勤系统是一套完整的PythonPyQt5实现源码包适合需要快速搭建人脸考勤演示项目或学习深度学习落地流程的开发者。系统利用卷积神经网络实现人脸检测与识别界面由PyQt5构建包含人脸录入、实时检测、身份识别等完整考勤闭环可直接运行体验或二次开发。资源共30个文件包括11个Python源码、6个编译后的pyc、3个PyQt5界面ui、多个预训练模型与配置文件如caffemodel、prototxt、xml整体压缩包约39.3MB模型与代码分离便于按模块理解。值得一提的是其中除核心网络结构描述与训练权重外还附带了haarcascade人脸检测备用方案、字体与图片资源、readme说明结构完整可作为课程设计或毕设参考。目前已有365人学习下载适合有一定Python基础、希望掌握CNN人脸识别系统完整工程架构的读者。1. 人脸识别考勤系统里CNN 和 PyQt5 各管哪一段人脸识别考勤系统里最容易被低估的不是识别准确率本身而是从“识别出一张脸”到“完成一次考勤”之间那条漫长的工程链。标题里的 CNN 卷积神经网络管的是特征抽取摄像头捕获的人脸照片经过卷积层、池化层和全连接层被压缩成一个低维向量再用距离比较判断是不是同一个人。而 PyQt5 负责的是桌面端交互实时画面、结果列表、人工复核缺一不可。这篇文章不是把某份源码逐行拆解而是按实际搭这类系统时会走的路径把 CNN 原理、人脸比对、考勤记录、PyQt5 界面和调优手段串起来。适合正在做课设、毕业设计或者想在单位局域网里跑一套离线考勤工具的工程师。你看完可以直接复现也能知道参数改哪里、模型换哪一个。2. CNN 卷积神经网络在人脸识别里的角色从卷积核到 128 维特征向量2.1 一张 CNN 结构图对应的人脸识别流程一张典型的 CNN 结构图里输入是一张 224×224 的三通道照片中间是若干组“卷积 ReLU 池化”最后接全连接层。卷积层用局部感受野把图像边缘、眼角、鼻翼这些局部纹理先抓出来池化层做下采样让特征在位置偏移时仍然稳定。全连接层把前面张开的特征图压成一个平铺向量这个向量的每一维都编码了人脸在某个抽象维度上的属性。对人脸识别来说最后一层不是输出“这个人是谁”的分类概率而是输出一个低维特征向量比如 FaceNet 的 128 维或 ArcFace 的 512 维。这个流程和图像分类的区别在于分类模型在最后一层输出类别 logits做的是闭集分类人脸识别要处理的是开集问题库里可能随时加人不能每来一个新员工就重训一次模型。所以主流方案都是“预训练 CNN 抽取特征 向量检索”把库里所有人预先过一遍模型存下特征库每次识别只算当前人脸和库里特征的相似度再做阈值判断。2.2 预训练模型与特征向量之间的距离计算人脸识别考勤系统里选模型先看输出维度再看运行环境。下面是常见的几类做法适合不同阶段的工程模型输出维度常见做法dlib ResNetface_recognition 内置128CPU 上就能跑起步最快FaceNetInception 主干128 或 512经典三元组损失方案需自行转 ONNXArcFaceInsightFace512精度高适合角度大、光照杂的考勤场景我一般会用 face_recognition 库起步因为它把检测和特征抽取封装得足够简单适合先跑通链路。正式上线再换 ArcFace 这类模型提升精度。无论选哪个核心计算都一样注册时把每张员工照片过一遍 CNN得到特征向量识别时把抓拍帧再过同一模型得到待比对向量最后算两个向量之间的欧氏距离或余弦相似度。2.3 用 face_recognition 生成一张人脸的 128 维特征向量import face_recognition # 注意face_recognition 底层调用 dlib首次运行会自动加载预训练模型 image face_recognition.load_image_file(employee_001.jpg) # modelhog 在 CPU 上更快想更准可以换 cnn但需要 dlib 的 GPU 支持 face_locations face_recognition.face_locations(image, modelhog) face_encodings face_recognition.face_encodings( image, face_locations, num_jitters10 ) if face_encodings: embedding face_encodings[0] print(特征维度:, embedding.shape, 数据类型:, embedding.dtype) else: print(未检测到人脸请检查照片是否清晰)这段代码的逻辑是先从照片里定位人脸框再把人脸区域送入 dlib 的 ResNet 模型输出一个 128 维浮点向量。face_locations 返回的是包含上下左右坐标的元组列表face_encodings 会按人脸框顺序返回对应的特征向量。参数说明有两个重点。modelhog 用的是方向梯度直方图检测器CPU 上单张图几十到上百毫秒适合开发调试换成 cnn 精度更好但要求 dlib 编译了 CUDA 支持。num_jitters 表示特征抽取时对图像做轻微扰动并重复采样的次数注册员工时用 10 能显著降低不同角度下的特征抖动考勤抓拍时用 1 就够了否则 CPU 占用会明显上升。2.4 先归一化再持久化特征库的存取习惯抽取出的 128 维向量不能直接扔进 MySQL 存字符串常见做法是先做 L2 归一化再以二进制形式存进 SQLite 或本地文件。归一化的意义在于把所有向量统一到单位球面上后续用点积就能算余弦相似度用欧氏距离也不会因为图片亮度差异产生整体漂移。import numpy as np def l2_normalize(vec): norm np.linalg.norm(vec) if norm 0: return vec return vec / norm逻辑说明np.linalg.norm 计算向量的 L2 范数也就是平方和开根号除以范数后向量长度变成 1。如果照片是全黑或全白导致范数为 0直接返回原向量比报异常更稳妥后续距离计算会自然判定为不匹配。规范化这一步要同时用在注册和识别两端只归一化一边会导致距离没有可比性。3. 人脸识别考勤的实现路径比对阈值、数据库结构与状态机3.1 考勤状态机未签到、已签到、已签退考勤系统和门禁不同门禁只需要判断“是不是登记过的人”考勤还需要区分上班和下班防止重复打卡。常见做法是给每个员工在当天维护一个状态上班时间第一次匹配成功记为 check_in下班时间第一次匹配记为 check_out同一状态不重复写库。否则摄像头一帧里识别出三次就会插入三条重复记录。这个状态判断可以放在写入函数之前而不是依赖前端按钮。更稳妥的做法是查询当天已有记录再决定动作当天已有记录当前识别时机写入结果无记录上班时间写入 check_in已有 check_in下班时间写入 check_out已有 check_in上班时间重复识别忽略提示已签到已有 check_in 和 check_out任意忽略提示已完成把状态机做到数据层而不是靠界面按钮控制可以避免用户误触或程序异常导致重复打卡。3.2 比对函数最小实现欧氏距离与阈值import numpy as np def match_face(query_embedding, db_embeddings, db_names, threshold0.45): # 计算 query 与特征库中所有人的欧氏距离 dists np.linalg.norm(db_embeddings - query_embedding, axis1) # 找到距离最小的员工 best_idx int(np.argmin(dists)) if dists[best_idx] threshold: return db_names[best_idx], round(float(dists[best_idx]), 4) return None, round(float(dists[best_idx]), 4)这段代码的逻辑是db_embeddings 是一个形状为 (N, 128) 的二维数组N 是员工数query_embedding 是形状为 (128,) 的一维数组。两者相减会触发广播得到 (N, 128) 的差值矩阵np.linalg.norm 加上 axis1 表示按行计算每个员工的 L2 距离结果是一维数组。np.argmin 返回最小距离对应的索引。threshold 参数是判同阈值越小越严格。考勤场景常见起步值是 0.45但这不是万能值。如果员工里有长相接近的兄弟或者同一个人不同日期的发型差异大就需要根据实际距离分布调整。还有一点容易踩坑欧氏距离没有上界不能把 0.6 说成 60% 相似度它只是距离值不是置信度。3.3 考勤记录落库SQLite 表结构与写入代码单机考勤软件用 SQLite 就够不需要引入数据库服务。表结构至少要有员工表和考勤记录表员工表里存特征向量考勤记录表里存打卡动作。CREATE TABLE employees ( employee_id TEXT PRIMARY KEY, name TEXT NOT NULL, embedding BLOB, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE attendance ( id INTEGER PRIMARY KEY AUTOINCREMENT, employee_id TEXT NOT NULL, action TEXT CHECK(action IN (check_in, check_out)), recognized_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );embedding 字段用 BLOB 类型存 numpy 数组的二进制序列化结果取出来后用 np.frombuffer 还原。这样比存 JSON 字符串省空间读取速度也快。attendance 表里的 action 用 CHECK 约束从数据库层面保证只能写入两种状态。import sqlite3 def record_attendance(db_path, employee_id, action): # 参数化查询避免 SQL 拼接注入 conn sqlite3.connect(db_path) try: conn.execute( INSERT INTO attendance (employee_id, action) VALUES (?, ?), (employee_id, action), ) conn.commit() finally: conn.close()逻辑说明sqlite3.connect 每次调用都会打开连接写入完成后立即关闭单机写入频率不高时完全够用。参数用占位符传给 execute而不是直接拼进 SQL 字符串这是最基本的安全习惯。3.4 常见误用特征库维度不一致、阈值照搬人脸识别考勤项目在 CSDN 和各类开源仓库里不少但很多代码只演示了比对没处理工程化细节。最容易出问题的是把不同模型的 embedding 混用。dlib 输出 128 维ArcFace 输出 512 维两者直接比对会报维度错误就算维度恰好一致向量空间也没有可比性。特征库一旦建立识别阶段必须使用同一模型重新抽取换模型意味着重新入库。误用做法现象修正方式把距离值当相似度百分比展示界面显示 0.65 但没认对人明确标注为距离不是置信度直接抄别人的 threshold0.5识别率波动大用距离分布统计确定阈值混合两个模型的特征向量np.linalg.norm 报错或结果无效注册与识别统一模型用图像分类模型的 logits 做比对新员工必须重训整个模型换成特征抽取模型4. PyQt5 桌面端封装摄像头采集、实时预览与线程不卡顿4.1 PyQt5 界面结构画面区、结果区、控制按钮PyQt5 里搭考勤界面常见布局是左中右三块左侧用 QLabel 显示摄像头实时画面中间用 QTableWidget 展示最近识别的打卡记录底部放“开始识别”“停止识别”“导出考勤”三个按钮。界面用 QMainWindow 做容器再用 QVBoxLayout 和 QHBoxLayout 组织控件不需要 Qt Designer 也能写清楚。安装环境时常用 pip install pyqt5 opencv-python face_recognition 一步到位。face_recognition 的依赖 dlib 在 Windows 上偶尔编译失败这时先确认 Python 版本和预编译 wheel 是否匹配或改用 conda 安装 dlib。PyQt5 和 PySide6 的 API 基本一致如果你用的是 PySide6信号声明略有差别但 QThread 的思路完全相同。4.2 用 QThread 把采集和业务逻辑从主线程剥离新手最常见的错误是把 while True 里的 cap.read() 直接写进主窗口初始化函数结果界面一打开就卡死。原因是主线程被视频读取循环占住Qt 的事件循环没法处理鼠标和重绘事件。正确做法是把摄像头读取放进 QThread通过信号把帧数据发给主线程。from PyQt5.QtCore import QThread, pyqtSignal import cv2 import numpy as np class CameraWorker(QThread): frame_ready pyqtSignal(np.ndarray) def __init__(self, camera_id0, parentNone): super().__init__(parent) self.camera_id camera_id def run(self): cap cv2.VideoCapture(self.camera_id) if not cap.isOpened(): return while not self.isInterruptionRequested(): ok, frame cap.read() if ok: # 界面显示和 OpenCV 处理都依赖 RGB 顺序先在源头转好 rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) self.frame_ready.emit(rgb) self.msleep(30) cap.release()逻辑说明cap.read() 读取到的帧默认是 BGR 顺序直接给 Qt 显示会偏蓝所以用 cv2.cvtColor 转成 RGB。isInterruptionRequested 是 Qt 提供的优雅退出标记配合外部调用 requestInterruption() 使用避免线程销毁时还卡在 read 阻塞里。参数说明camera_id 默认 0 代表内置摄像头外接 USB 摄像头可以尝试 1 或 2。msleep(30) 让线程每 30 毫秒取一帧约 33 帧/秒既保证画面流畅又给识别线程留出处理时间。这里不用 time.sleep 是因为 QThread 的 msleep 不会干扰 Qt 内部的事件机制。4.3 识别结果如何流回界面信号槽传递数据采集线程把帧发出来后主窗口槽函数负责更新画面但不能在槽函数里直接跑人脸识别。一个 128 维特征的提取在 CPU 上需要几十到几百毫秒直接跑会拖垮界面帧率。常见做法是单独开一个识别线程采集线程只维护“最新帧”识别线程每次只拿最新的一帧处理丢弃积压帧。def on_frame_arrived(self, rgb_frame): # 只保留最新帧识别线程消费时不会堆积 self.latest_frame rgb_frame if self.frame_counter % 5 0: self.detector.put(self.latest_frame) self.frame_counter 1逻辑说明frame_counter 做计数抽样每 5 帧才丢一次识别任务大幅降低 CPU 压力。识别线程完成后通过另一个 pyqtSignal 把 result 字典传回主线程主线程再更新 QTableWidget。更新表格的代码也很固定def update_table(self, employee_id, action, timestamp): row self.table.rowCount() self.table.insertRow(row) self.table.setItem(row, 0, QTableWidgetItem(employee_id)) self.table.setItem(row, 1, QTableWidgetItem(action)) self.table.setItem(row, 2, QTableWidgetItem(timestamp)) self.table.scrollToBottom()rowCount 返回当前行数insertRow 在末尾追加一行setItem 必须传 QTableWidgetItem 对象字符串不会自动转换。scrollToBottom 让表格始终显示最新记录避免操作员盯着看时还要手动下拉。4.4 PyQt5 开发中的典型坑与规避摄像头画面颜色发蓝先检查 BGR 和 RGB 是否转换关闭主窗口时崩溃十有八九是线程还在运行就被回收。正确的退出顺序是先调用 worker.requestInterruption()再 worker.wait()最后才让事件循环结束。中文显示乱码时可以在 QApplication 初始化后设置全局字体比如 QFont(Microsoft YaHei, 10)。如果想把考勤日报以格式化内容展示出来QTextEdit.setHtml 是轻量做法不必为这个需求引入整套 WebEngine 组件。安装依赖时遇到 pyqt5-qt5 包来源解析异常多数是镜像源拼接问题换官方 PyPI 源或改 uv pip 安装就能绕开。5. 让这套人脸识别考勤系统可用的最后几步5.1 阈值校准与多帧投票0.45 这个阈值只是起点。上线前采集同一人不同光线下的 20 张注册照再采集不同人各 10 张分别计算类内距离和类间距离取两者的中间值作为初始阈值。常见做法是画出距离分布的直方图把阈值定在两类分布的交界处。如果现场有相似脸员工单纯调阈值解决不了要配合多帧投票连续 3 帧识别为同一人才记录打卡单帧命中不写入。same_pairs [...] # 同一人两张照片算出的距离 diff_pairs [...] # 不同人两张照片算出的距离 threshold (max(same_pairs) min(diff_pairs)) / 2这个公式粗糙但能给你一个可讨论的起点实际使用时再根据误识率微调。考勤场景对误识比对漏识更敏感阈值宁可偏严。5.2 模型替换、特征库校验与故障速查dlib 的 128 维特征在白天室内场景够用如果现场有强逆光或大角度可以把模型换成 ArcFace。做法是把模型转成 ONNX用 onnxruntime 加载输入为 112×112 的正脸图。换模型后必须重新生成整个特征库旧特征向量全部作废。启动时先校验特征库维度比运行到比对的报错提示友好得多def check_feature_db(db_embeddings): if db_embeddings.ndim ! 2 or db_embeddings.shape[1] ! 128: raise ValueError(f特征库维度不合法: {db_embeddings.shape})这里 128 对应 dlib ResNet 的输出宽度换成 ArcFace 时改成 512。放在主程序入口处执行确保模型和特征库匹配。现象原因处理界面无画面camera_id 被占用或设备号不对释放摄像头占用换设备号测试换人后仍识别成旧员工特征库没有重新加载热更新后重新读取库文件识别结果只出现一次状态机屏蔽了重复打卡确认这是预期行为不必修复这套链路跑通后后续扩展点基本都在数据侧给考勤记录加导表接口按周汇总异常打卡或者把识别结果接入企业微信通知。核心还是那句CNN 负责把脸变成数字PyQt5 负责把数字变成一个能用的人事流程。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价