基于PyQt5和CNN的人脸识别考勤系统实战拆解
发布时间:2026/9/13 16:18:01 锦皓数字建站

简介基于CNN神经网络的人脸识别考勤系统是一份面向Python开发者和深度学习初学者的完整项目源码适用于课堂签到、公司考勤等需要快速刷脸登记的场景。项目以卷积神经网络为核心完成人脸检测与特征识别搭配PyQt5构建图形界面支持人脸信息录入、实时摄像头检测、身份匹配与考勤标记。压缩包共30个文件主体包括11个Python源文件、6个pyc编译文件、3个PyQt5界面ui文件、3张示例图片并附带caffemodel预训练模型、prototxt网络结构文件、XML人脸检测分类器以及说明文档整体大小39.3MB。已有365人学习下载适合想研究CNN工程落地、或需要课设/毕设参考的读者。通过源码可以直观理解图像预处理、模型加载、界面交互与业务逻辑如何串成完整系统目录划分清晰便于二次修改和功能扩展。1. 为什么这套 CNN 考勤系统值得拿来拆一遍多数人搜「Python 人脸识别考勤系统源码」心里想的其实是两件事能不能跑起来以及跑起来之后我能不能改。这套基于 PyQt5 的 CNN 人脸识别考勤项目正好卡在中间位置——它不是调个 API 的玩具也不是几百 MB 的工程而是 27 个文件的精简结构用 Caffe 训练好的 caffemodel 做推理PyQt5 做界面摄像头实时抓帧做比对。你能在一台没有独显的笔记本上把整条链路走通也能看清从检测到识别再到界面回显的每一环。更适合谁一类是想入门深度学习落地、但不想被训练环节卡住的人——模型是现成的你只管推理和工程化另一类是做过 PyQt5 桌面工具、想把界面和 CV 算法拼起来的人这个项目的 UI 和算法是分离的ui/目录下全是.ui文件和对应的.py生成代码改界面不用碰模型代码。下面按「环境与结构 → 人脸检测 → CNN 识别 → 录入与考勤 → 调优排错」推一遍能复现的部分我都给到具体命令和参数。2. PyQt5 与 CNN 推理环境搭建及项目结构拆解在动手之前先把目录结构和依赖关系理清不然装完库运行launcher.py报一堆 ImportError你都不知道是缺包还是路径错了。2.1 从 upload.zip 看模块划分解压后大致分成四块这个划分方式在小型 CV 桌面项目里很典型目录/文件作用是否要改driver/cam.py摄像头采集封装换摄像头源时改models/cnn_deploy.prototxtcnn_iter_3560000.caffemodel网络结构与权重一般不碰haarcascade_frontalface_alt.xmlHaar 人脸检测器可换更快的 LBP 版本ui/*.ui与*.pyPyQt5 界面与自动生成代码改界面必碰launcher.py/launcher_sing_face.py程序入口主入口config.py路径与阈值配置经常改注意.pyc文件和.py文件同时存在如cam.pyc和cam.py、face_search.pyc和face_search.py这说明打包者既留了源码也留了编译产物。Python 导入时优先命中的是.py只要.py在.pyc基本是冗余的直接删掉不影响运行——但在某些 Python 版本下.pyc的魔数不匹配反而会报bad magic number删掉更省心。fzyy.TTF是界面用到的字体文件mark.jpg、mask.jpg、black.jpg、yellow.png是界面素材images/目录通常用于存放录入的人脸样本。2.2 依赖安装与虚拟环境不要直接往系统 Python 里装Caffe 的兼容层对 NumPy 版本很挑装乱了整个环境都得重来。用 venv 隔离python -m venv venv # Windows 下激活 venv\Scripts\activate # Linux / macOS source venv/bin/activate # 先锁 NumPy 到 1.xCaffe 的 python 绑定不认 NumPy 2.x 的 ABI pip install numpy2 opencv-python pyqt5PyQt5 在 Windows 上装完如果启动报缺Qt5Core.dll一般是 wheel 没下全重装pyqt5-qt5这个子包即可pip install --force-reinstall pyqt5-qt5在 PyCharm 里配置 PyQt5 的话把项目解释器指到刚建的 venv再确认PyQt5出现在包列表里就行不用额外配 Qt Designer 路径除非你要改.ui文件。2.3 推理框架的选择为什么是 Caffe 而不是直接换 ONNX看到cnn_iter_3560000.caffemodel这种命名就知道是 Caffe 系的老模型。它的问题是部署麻烦好处是结构透明——cnn_deploy.prototxt是纯文本你能一行行读出每一层怎么连。如果只是想跑通用caffe或opencv的dnn模块加载都行但更推荐用 OpenCV 的 DNN 接口因为它不依赖完整 Caffe 环境import cv2 import numpy as np # 用 OpenCV DNN 加载 Caffe 模型避开安装完整的 Caffe net cv2.dnn.readNetFromCaffe( models/cnn_deploy.prototxt, models/cnn_iter_3560000.caffemodel ) def extract_feature(face_bgr): 把一张人脸图转成特征向量用于后续比对 # Caffe 模型一般要求 227x227 或 224x224 输入按 prototxt 里定义的来 blob cv2.dnn.blobFromImage( face_bgr, 1.0, (227, 227), (104.0, 177.0, 123.0), # Caffe 常用的 BGR 均值 swapRBFalse, cropFalse ) net.setInput(blob) # 取倒数第二层全连接输出当特征具体层名看 prototxt feat net.forward(fc7) return feat.flatten()blobFromImage的三个均值(104.0, 177.0, 123.0)是 Caffe 在 ImageNet 上的标准减均值参数顺序是 BGR所以swapRB保持False。尺寸必须和 prototxt 里第一个卷积层的输入对齐否则net.forward会直接抛尺寸不匹配。fc7是层名实际叫什么得打开 prototxt 搜一下name:字段确认不同训练脚本导出的名字不一样。这套写法的价值在于把模型推理从 Caffe 环境里解耦出来只在requirements里留个opencv-python就够了。3. Haar 级联人脸检测与预处理参数设置检测和识别是两个独立环节很多人卡在检测上——摄像头采集到的帧质量差Haar 检不到脸后面的 CNN 再准也没用。3.1 摄像头采集与帧预处理driver/cam.py一般是对cv2.VideoCapture的一层封装。默认用 0 号设备多摄像头时改索引。实际跑的时候先把采集逻辑单独拎出来验证import cv2 cap cv2.VideoCapture(0) # 设置采集分辨率太高会拖慢检测640x480 是速度和清晰度的平衡点 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) while True: ok, frame cap.read() if not ok: break # 镜像翻转符合照镜子的直觉录入时体验更好 frame cv2.flip(frame, 1) cv2.imshow(preview, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()分辨率这块有取舍1080p 下每帧 Haar 检测要几十毫秒帧率掉到十几帧界面会显得卡320x240 检测快但脸太小CNN 特征区分度下降。所以 640x480 是这类桌面考勤场景的常见落点。如果机器性能实在紧张把采集分辨率保持 640x480检测前再降采样到一半检出框坐标乘以 2 映射回原图即可。3.2 haarcascade_frontalface_alt 的检测参数项目用的是haarcascade_frontalface_alt.xml比默认的haarcascade_frontalface_default.xml略准但稍慢。核心参数就scaleFactor、minNeighbors、minSize三个import cv2 cascade cv2.CascadeClassifier(haarcascade_frontalface_alt.xml) gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 直方图均衡化缓解逆光和偏暗场景漏检 gray cv2.equalizeHist(gray) faces cascade.detectMultiScale( gray, scaleFactor1.1, # 每次缩放 10%越小越慢但越不容易漏 minNeighbors5, # 邻域阈值调大减少误检调小减少漏检 minSize(80, 80) # 小于这个尺寸的框直接丢弃过滤远景干扰 )scaleFactor设 1.1 是通用值调到 1.05 检测更细但耗时几乎翻倍minNeighbors从 5 往下调到 3能在脸部被部分遮挡时多检出一些代价是背景里的花纹、相框容易误报成脸。minSize很关键——不设的话远处的小块纹理会被当成脸导致后续 CNN 拿到一堆垃圾输入。提示如果detectMultiScale一直返回空元组先确认gray是真实灰度图而不是三通道再用cv2.imwrite存一帧到磁盘看一眼画面是否正常曝光九成问题出在采集环节而不是检测参数。3.3 人脸对齐与 ROI 裁剪检出框之后不要直接把框裁出来喂给 CNN。Haar 框偏大且位置有抖动直接裁会导致同一张脸在不同帧里特征差异明显。常见做法是按框中心扩一点边距再统一缩放def crop_face(frame, box, margin0.2): x, y, w, h box # 按比例外扩保留完整下巴和额头减少边界切脸 mx, my int(w * margin), int(h * margin) x1 max(0, x - mx) y1 max(0, y - my) x2 min(frame.shape[1], x w mx) y2 min(frame.shape[0], y h my) face frame[y1:y2, x1:x2] # resize 到模型输入尺寸插值方式影响不大但别用最近邻 return cv2.resize(face, (227, 227), interpolationcv2.INTER_LINEAR)外扩 20% 是个经验值脸在画面边缘时注意用max/min把坐标夹到图像范围内否则切片会越界返回空数组后续 resize 直接崩。这套裁剪逻辑在录入和识别两个流程里要共用同一份不然录入的特征和识别时提的特征分布不一致比对分数会整体偏低。4. CNN 特征提取与考勤比对逻辑实现检测出人脸之后真正决定识别效果的是特征怎么提、怎么比、阈值怎么定。4.1 基于 caffemodel 的特征向量提取模型文件cnn_iter_3560000.caffemodel里的3560000是训练迭代次数属于训练得比较充分的一版。特征提取调用前面extract_feature那个函数即可关键在于取哪一层。全连接层越靠后越抽象fc7通常是 4096 维fc8是分类输出维度。用fc7当特征比fc8更通用因为fc8的每个维度绑定了训练时的具体类别换场景就失效。def cosine_sim(a, b): 余弦相似度取值 [-1,1]越接近 1 越像 a a / (np.linalg.norm(a) 1e-8) b b / (np.linalg.norm(b) 1e-8) return float(np.dot(a, b))加1e-8是防止零向量除出 NaN。相比欧氏距离余弦相似度对整体亮度变化更鲁棒考勤场景下光照条件不固定用余弦更稳。4.2 人脸库的组织与匹配face_search.py负责在人脸库里找最相似的人。人脸库最简单的存法就是一个目录一个人、目录名当身份标签每个身份存若干张特征向量import os, pickle, numpy as np FACE_DB images/faces.pkl def build_face_db(face_dir): 遍历 images/ 下每个身份文件夹提特征存成 pickle db {} for person in os.listdir(face_dir): pdir os.path.join(face_dir, person) if not os.path.isdir(pdir): continue feats [] for img_name in os.listdir(pdir): img cv2.imread(os.path.join(pdir, img_name)) if img is None: continue feats.append(extract_feature(cv2.resize(img, (227, 227)))) if feats: # 同一身份多张样本取平均抵消单张噪声 db[person] np.mean(feats, axis0) with open(FACE_DB, wb) as f: pickle.dump(db, f) return db单一身份存平均特征的好处是比对时只需算一次余弦速度快坏处是如果录入的几张图姿态差异大平均值会落在中间地带反而谁都不像。样本超过 5 张且姿态多样时改成保留全部特征、比对时取最大相似度效果更好。4.3 考勤判定阈值与去重匹配环节的核心是阈值。阈值设 0.75 还是 0.85直接决定误识率和漏识率的平衡阈值表现适用场景0.60 ~ 0.70几乎不漏识误识偏高内部演示、人数少0.75 ~ 0.80平衡点一般考勤场景0.85 以上误识极低容易认不出安全要求高的门禁THRESHOLD 0.78 attendance {} # {身份: 首次打卡时间} def recognize(feat, db): best_name, best_score None, -1.0 for name, ref in db.items(): score cosine_sim(feat, ref) if score best_score: best_name, best_score name, score if best_score THRESHOLD: return None, best_score return best_name, best_score打卡去重按「当天 身份」为键第一次命中后写入字典就跳过后续帧避免摄像头前站三秒刷出几十条记录。如果课程设计里有时间粒度要求比如按半天把键从name换成(name, date, AM/PM)即可。5. 界面联调、性能调优与踩坑排查算法通了之后剩下的坑基本都在 GUI 线程和资源管理上。5.1 PyQt5 主线程与视频帧刷新PyQt5 里最典型的错误是在主线程里跑while True刷摄像头界面直接假死。正确做法是把采集和识别放到QThread里通过信号把结果发回主线程更新控件from PyQt5.QtCore import QThread, pyqtSignal from PyQt5.QtGui import QImage, QPixmap class DetectThread(QThread): frame_ready pyqtSignal(QImage) def run(self): cap cv2.VideoCapture(0) while self.running: ok, frame cap.read() if not ok: continue frame cv2.flip(frame, 1) rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch rgb.shape # QImage 必须保持 stride 一致否则显示会错位 img QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888) self.frame_ready.emit(img.copy()) cap.release()QImage如果直接引用rgb.data这块内存而rgb在下一轮循环被覆盖界面就会显示花屏。所以 emit 之前.copy()一份独立内存这是新手最容易忽略的一行。信号槽跨线程默认就是队列连接不用担心加锁问题。5.2 识别耗时统计与降帧策略实时识别不需要每帧都跑 CNN。用时间戳控制识别频率比如每 300ms 识别一次中间帧只做检测和画面显示import time last_recog 0 RECOG_INTERVAL 0.3 # 秒 def on_frame(frame): global last_recog now time.time() faces detect(frame) if faces and now - last_recog RECOG_INTERVAL: last_recog now feat extract_feature(crop_face(frame, faces[0])) name, score recognize(feat, db) update_ui(name, score)把RECOG_INTERVAL从 0.3 调到 0.5CPU 占用能明显下降代价是识别响应慢半拍。老机器上跑不动的时候先调这个参数比换模型成本低得多。5.3 常见报错对照现象大概率原因处理bad magic number.pyc与当前 Python 版本不匹配删掉对应的.pycUnknown layer type换了新版 Caffe 或 OpenCV用 OpenCV DNN 加载或降 OpenCV 版本摄像头打开但画面全黑被其他程序占用或索引不对换VideoCapture(1)试关掉占用程序界面卡死无响应视频循环跑在主线程移到 QThread所有人识别成同一个特征未归一化或阈值过低检查余弦相似度实现抬高阈值提示images/目录里如果混进了非人脸图比如截图、风景会对平均特征造成污染导致该身份的比对分数整体偏低。录入流程加一道检测检不到脸的图片直接拒绝入库。6. 用单张照片批量录入与识别结果复核的技巧很多人录入人脸时要求同事对着摄像头摆半天姿势其实没必要。既然特征提取走的是静态图路径直接用一组照片批量建库比现场摆拍高效得多把每个人 5 到 8 张不同角度的照片放进images/姓名/目录跑一遍build_face_db就能生成特征库全程不用开摄像头。注意照片里人脸占比要够大正脸至少占画面高度的三分之一侧脸超过 45 度就别放进去了Haar 本身就检不准。复核识别效果的时候别只用「识别成谁」这一个指标把分数打出来看分布。正常情况同一个人不同帧的余弦相似度应该在 0.85 以上不同人之间低于 0.6中间这段是模糊区。批量跑一批测试图把每个身份的分数画成直方图如果同人分布跨过了不同人的分布说明特征区分度不够这时候调阈值是治标真正该做的是重新组织录入样本或者把特征从fc7换成更靠前的卷积层输出再拼接。还有一个容易被忽略的点config.py里通常写死了模型路径、阈值、摄像头索引这些值改完之后如果程序读的还是旧值八成是 Python 缓存了模块导入。改配置后重启进程别指望热更新生效。整套流程跑通之后真正的可维护性来自把阈值、路径、间隔这些全部收进配置文件算法代码里只留import config这样换台机器部署时只改一个文件。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。