ARTICLE DETAIL

资讯详情

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

Python+OpenCV+PyQt5人脸识别考勤系统源码实战与避坑指南

Python+OpenCV+PyQt5人脸识别考勤系统源码实战与避坑指南 简介这是一套面向课程设计与毕业设计场景的人脸识别考勤签到系统源码基于Python、OpenCV与PyQt5开发适合具备一定Python基础、希望深入实践计算机视觉与桌面应用开发的学习者。系统围绕人脸检测、特征提取与签到记录管理展开涉及Haar级联分类器、LBPH等识别方法并配套PyQt5构建的签到与查询界面同时涵盖数据库操作、时间处理、异常捕获等工程细节。资源包共44个文件以py源码、pyc编译文件、xml分类器模型、ui界面文件及说明文档为主压缩包约681KB目录结构清晰便于按模块阅读与二次开发。代码中附有详细注释可帮助读者理解人脸识别流程、GUI信号槽交互与考勤数据存储逻辑。目前已有266人学习适合作为课程设计、毕业设计参考或计算机视觉入门练手项目。1. 从一份能跑起来的考勤系统源码说起PythonOpenCVPyQt5 到底怎么落地很多人第一次接触人脸识别考勤是从下载一份「基于 PythonOpenCV 人脸识别的考勤签到系统源码」开始的。压缩包里通常有主程序、训练脚本、人脸数据目录、PyQt5 界面文件和一份详细注释看起来什么都有但真正跑起来却经常卡在环境、路径、摄像头索引和中文显示上。这篇笔记就围绕这个标题把「源码怎么读、环境怎么配、界面怎么接、识别怎么调、坑在哪」讲清楚让你拿到一份类似源码后能自己复现、改参数、接数据库而不是只会双击运行。这套方案的核心链路其实不复杂OpenCV 负责摄像头取流和人脸检测识别模型负责把人脸映射成身份PyQt5 负责把签到按钮、实时画面、签到记录表拼成一个桌面程序Python 做胶水。它适合课程设计、小型办公室签到、实验室门禁原型也适合想练手 OpenCV 图像处理项目的人。但要注意它和商用门禁机、人脸识别门禁系统设计里的工业方案差距很大光照、角度、遮挡都会让识别率断崖式下跌。下面按「先跑通、再拆解、后调优」的顺序展开中间会给出可直接抄的代码和参数说明。2. 环境与源码结构把第一行代码跑起来之前要确认的事2.1 Python、OpenCV、PyQt5 的版本匹配与安装顺序拿到源码后不要急着pip install -r requirements.txt先看import了哪些包。典型的人脸识别考勤项目会用到opencv-python、opencv-contrib-python、PyQt5、numpy、Pillow有的还会用face_recognition或pandas存签到记录。这里最容易翻车的是 OpenCV 主包和 contrib 包同时安装导致冲突以及 PyQt5 在部分 Python 版本上没有预编译轮子。我一般会先建虚拟环境再按下面顺序装# 创建虚拟环境避免污染系统 Python python -m venv venv # Windows 激活 venv\Scripts\activate # Linux / macOS 激活 source venv/bin/activate # 先装 numpy很多包依赖它 pip install numpy1.24.3 # 装 OpenCV 主包即可人脸检测器在主包里就有 pip install opencv-python4.8.1.78 # 装 PyQt5注意 Python 3.11 以上建议用 5.15.9 之后的版本 pip install PyQt55.15.10 # 可选如果源码用了人脸识别库 pip install face_recognition1.3.0逻辑说明opencv-python已经包含 Haar 级联和 LBPH 人脸识别器不需要额外装 contrib除非源码里用了 SIFT、SURF 这类专利算法。numpy版本不要太高否则和旧版 OpenCV 的 ABI 可能不兼容。PyQt5 安装失败时先确认 Python 位数和 pip 是否最新再考虑换 3.9 或 3.10 环境。参数说明opencv-python4.8.1.78是一个稳定版本摄像头读取和cv2.waitKey行为比较一致PyQt55.15.10对高分辨率屏幕支持较好。如果你用的是 Anaconda建议新建 conda 环境而不是在 base 里折腾否则ModuleNotFoundError: No module named opencv会反复出现。提示安装完成后用python -c import cv2; print(cv2.__version__)和python -c from PyQt5.QtWidgets import QApplication; print(ok)分别验证两个都通过再往下走。2.2 源码目录怎么读从入口文件到人脸数据目录一份典型的考勤签到系统源码目录大致如下attendance_system/ ├── main.py # 程序入口启动 PyQt5 主窗口 ├── ui_main.py # 界面布局代码可能是 pyuic5 生成 ├── face_recognizer.py # 人脸检测与识别封装 ├── train_model.py # 训练人脸模型脚本 ├── dataset/ # 存放采集的人脸图片按人名分文件夹 │ ├── zhangsan/ │ └── lisi/ ├── trainer/ │ └── trainer.yml # LBPH 训练后生成的模型文件 ├── attendance.csv # 签到记录 └── requirements.txt读源码的顺序建议是先看main.py里if __name__ __main__下面做了什么再看face_recognizer.py里检测和识别函数最后看train_model.py怎么把dataset转成trainer.yml。很多人一上来就改界面结果识别不通界面再漂亮也没用。关键点在于dataset的命名规则。LBPH 训练时通常用文件夹名作为标签所以文件夹名就是签到显示的人名。如果文件夹里有非图片文件或者图片尺寸差异太大训练阶段会报错或识别率极低。常见做法是采集时统一 resize 到 200x200 灰度图并做直方图均衡化。import cv2 import os import numpy as np # 读取 dataset 下所有图片统一尺寸和灰度 def load_dataset(dataset_path): faces [] labels [] label_map {} current_id 0 for dir_name in os.listdir(dataset_path): dir_path os.path.join(dataset_path, dir_name) if not os.path.isdir(dir_path): continue label_map[current_id] dir_name for file_name in os.listdir(dir_path): if not file_name.lower().endswith((.jpg, .png, .jpeg)): continue img_path os.path.join(dir_path, file_name) img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) if img is None: continue img cv2.resize(img, (200, 200)) # 直方图均衡化缓解光照不均 img cv2.equalizeHist(img) faces.append(img) labels.append(current_id) current_id 1 return faces, np.array(labels), label_map逻辑说明cv2.imread第二个参数用IMREAD_GRAYSCALE直接读灰度省去后面转换。equalizeHist是 LBPH 前处理里很实用的一步能明显改善暗光下的识别效果。label_map把数字标签映射回人名签到记录里显示的是人名而不是数字。参数说明(200, 200)是常见的人脸归一化尺寸太小会丢细节太大训练慢且对 LBPH 收益有限。如果采集时人脸框选得不准这里 resize 会把背景也带进去所以采集阶段就要用检测器把脸裁出来再保存。3. 人脸检测与识别Haar、LBPH 和实际签到逻辑怎么接3.1 Haar 级联检测器的参数怎么调才不飘OpenCV 自带的人脸检测器最常用的是 Haar 级联haarcascade_frontalface_default.xml文件在cv2.data.haarcascades目录下。检测函数detectMultiScale有几个关键参数直接决定你能不能稳定框到脸。import cv2 # 加载 Haar 级联人脸检测器 face_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) # 打开摄像头0 通常是内置摄像头 cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: break # 转灰度Haar 检测在灰度图上做 gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 检测人脸 faces face_cascade.detectMultiScale( gray, scaleFactor1.1, # 每次图像缩放比例 minNeighbors5, # 候选框保留阈值 minSize(80, 80), # 最小人脸尺寸 flagscv2.CASCADE_SCALE_IMAGE ) for (x, y, w, h) in faces: cv2.rectangle(frame, (x, y), (x w, y h), (0, 255, 0), 2) cv2.imshow(Face Detection, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明scaleFactor1.1表示每次把图像缩小 10% 再检测值越小检测越慢但越不容易漏。minNeighbors5控制误检值越大越严格但太小会把侧脸漏掉。minSize(80, 80)能过滤掉远处的小脸和背景噪声。参数说明如果画面里人脸忽大忽小优先调minSize和scaleFactor。光照偏暗时 Haar 容易失效可以先把gray做一次cv2.equalizeHist。如果检测框抖动严重不要一味加minNeighbors而是检查摄像头分辨率和帧率低帧率下画面模糊会让检测器反复丢框。注意Haar 对正脸效果尚可侧脸、低头、戴口罩基本检测不到。如果项目要求更高常见做法是换成 DNN 人脸检测器但源码里如果只写了 Haar先把 Haar 调稳再考虑替换。3.2 LBPH 训练与识别从 trainer.yml 到签到判断LBPH 是 OpenCV 里自带的人脸识别算法优点是训练快、样本要求低缺点是跨光照和跨角度能力弱。对于课程设计和小型考勤原型它足够用。训练脚本通常长这样import cv2 import numpy as np import os # 假设 load_dataset 返回 faces, labels, label_map faces, labels, label_map load_dataset(dataset) # 创建 LBPH 识别器 recognizer cv2.face.LBPHFaceRecognizer_create( radius1, # 邻域半径 neighbors8, # 邻域点数 grid_x8, # 网格列数 grid_y8, # 网格行数 threshold80 # 识别阈值越小越严格 ) # 训练 recognizer.train(faces, labels) # 保存模型 os.makedirs(trainer, exist_okTrue) recognizer.save(trainer/trainer.yml) # 保存标签映射方便识别时显示人名 np.save(trainer/label_map.npy, label_map)逻辑说明radius和neighbors控制 LBP 算子感受野grid_x、grid_y把图像分成 8x8 网格每个网格单独统计直方图能保留一定空间信息。threshold是识别置信度阈值LBPH 的predict返回的第二个值越小表示越像超过阈值就判为未知。参数说明threshold80是一个经验值实际项目里要根据样本质量调。如果经常把陌生人认成已录入的人把阈值降到 60 左右如果自己人经常认不出先检查训练样本是否太少或光照差异太大再考虑放宽到 100。radius1、neighbors8适合 200x200 的人脸图不要随意改大否则特征维度过高、训练变慢。识别阶段接签到逻辑import cv2 import numpy as np import csv from datetime import datetime recognizer cv2.face.LBPHFaceRecognizer_create() recognizer.read(trainer/trainer.yml) label_map np.load(trainer/label_map.npy, allow_pickleTrue).item() face_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) cap cv2.VideoCapture(0) signed_today set() while True: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale(gray, 1.1, 5, minSize(80, 80)) for (x, y, w, h) in faces: roi gray[y:y h, x:x w] roi cv2.resize(roi, (200, 200)) roi cv2.equalizeHist(roi) label, confidence recognizer.predict(roi) if confidence 80: name label_map.get(label, Unknown) # 同一天同一人只签一次 today datetime.now().strftime(%Y-%m-%d) key f{name}_{today} if key not in signed_today: signed_today.add(key) with open(attendance.csv, a, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([name, datetime.now().strftime(%Y-%m-%d %H:%M:%S)]) cv2.putText(frame, f{name} {confidence:.1f}, (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) else: cv2.putText(frame, Unknown, (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2) cv2.rectangle(frame, (x, y), (x w, y h), (0, 255, 0), 2) cv2.imshow(Attendance, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明signed_today用内存集合做当天去重避免同一个人站在摄像头前反复写入 CSV。confidence 80是识别通过条件和训练时的threshold对应。label_map用np.load读回注意allow_pickleTrue。参数说明roi必须和训练时做同样的 resize 和equalizeHist否则识别率会明显下降。CSV 写入用encodingutf-8否则中文人名在 Windows 上可能乱码。如果多人同时入镜Haar 可能只框到一个人实际考勤场景建议一次只让一个人站到镜头前。4. PyQt5 界面把摄像头画面、签到按钮和记录表拼起来4.1 用 QTimer 把 OpenCV 帧刷到 QLabel 上PyQt5 显示摄像头画面最常见的做法是QTimer定时读帧把QImage转成QPixmap贴到QLabel。不要在主线程里写while True否则界面直接卡死。from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QPushButton, QVBoxLayout, QWidget from PyQt5.QtGui import QImage, QPixmap from PyQt5.QtCore import QTimer, Qt import cv2 import sys class AttendanceWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(人脸识别考勤签到系统) self.resize(900, 700) self.label QLabel(摄像头画面) self.label.setAlignment(Qt.AlignCenter) self.btn QPushButton(开始签到) layout QVBoxLayout() layout.addWidget(self.label) layout.addWidget(self.btn) container QWidget() container.setLayout(layout) self.setCentralWidget(container) self.cap cv2.VideoCapture(0) self.timer QTimer() self.timer.timeout.connect(self.update_frame) self.btn.clicked.connect(self.start_sign) def start_sign(self): if not self.timer.isActive(): self.timer.start(30) # 约 33 帧每秒 def update_frame(self): ret, frame self.cap.read() if not ret: return # OpenCV 是 BGRQt 需要 RGB rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch rgb.shape bytes_per_line ch * w qimg QImage(rgb.data, w, h, bytes_per_line, QImage.Format_RGB888) self.label.setPixmap(QPixmap.fromImage(qimg).scaled( self.label.width(), self.label.height(), Qt.KeepAspectRatio )) def closeEvent(self, event): self.cap.release() event.accept() if __name__ __main__: app QApplication(sys.argv) win AttendanceWindow() win.show() sys.exit(app.exec_())逻辑说明QTimer每 30 毫秒触发一次update_frame读一帧、转 RGB、转QImage、再转QPixmap显示。closeEvent里释放摄像头否则下次运行可能提示设备被占用。参数说明timer.start(30)里的 30 是毫秒太小会增加 CPU 占用太大画面卡顿。scaled里用Qt.KeepAspectRatio保持比例避免画面拉伸。如果界面显示花屏检查bytes_per_line是否用ch * w以及rgb.data是否在QImage构造后被释放。4.2 签到记录表与界面线程安全实际项目里通常还要一个QTableWidget显示签到记录并且识别逻辑不能阻塞界面。常见做法是把识别放在update_frame里同步做因为 LBPH 单帧预测很快如果换成深度学习模型就要考虑QThread。from PyQt5.QtWidgets import QTableWidget, QTableWidgetItem # 在 __init__ 里加表格 self.table QTableWidget(0, 2) self.table.setHorizontalHeaderLabels([姓名, 签到时间]) layout.addWidget(self.table) # 签到成功后更新表格 def add_record(self, name, time_str): row self.table.rowCount() self.table.insertRow(row) self.table.setItem(row, 0, QTableWidgetItem(name)) self.table.setItem(row, 1, QTableWidgetItem(time_str))逻辑说明insertRow在末尾追加一行setItem填入姓名和时间。表格数据源可以和 CSV 保持一致程序启动时读一次 CSV 回填避免重启后记录丢失。参数说明如果记录很多不要每次全量刷新只追加新行即可。表格列宽可以用self.table.setColumnWidth(0, 120)固定避免人名长短导致界面跳动。中文显示乱码时确认源文件编码和 Qt 字体设置Windows 下通常需要设置QApplication.setFont。提示PyQt5 界面设计可以用 Qt Designer 拖控件再用pyuic5转成.py。但转换后的文件不要手改下次重新生成会覆盖业务逻辑单独写在另一个类里。5. 避坑与排查人脸考勤系统最常见的 5 个翻车现场5.1 摄像头打开失败或画面全黑现象cv2.VideoCapture(0)返回False或者read()一直失败界面显示空白。原因摄像头索引不对、被其他程序占用、Linux 下权限不足或者虚拟机没有直通 USB 摄像头。解决先换索引试cv2.VideoCapture(1)、cv2.VideoCapture(2)关闭其他占用摄像头的软件Linux 下把用户加入video组虚拟机里确认 USB 设备已连接。Windows 上还可以在设备管理器里确认摄像头驱动正常。5.2 训练时报「样本为空」或标签错乱现象recognizer.train(faces, labels)抛出异常提示Empty training data或者识别出来的人名和实际不对应。原因dataset目录结构不对图片没读到或者label_map在训练和识别时顺序不一致。解决打印len(faces)和len(labels)确认样本数量检查每个子文件夹是否至少有一张能正常imread的图片训练完把label_map一起保存识别时读同一个文件不要重新遍历目录生成映射。5.3 识别率低自己人经常被判成 Unknown现象明明录入过人脸但confidence总是大于阈值显示 Unknown。原因训练样本太少、光照差异大、人脸没有对齐、训练和识别前处理不一致。解决每个人至少采集 20 到 30 张覆盖不同表情和轻微角度采集时就裁出人脸并统一尺寸训练和识别都做equalizeHist适当放宽threshold但不要为了通过把阈值调到 150 以上否则陌生人也会被认出来。5.4 PyQt5 界面卡死或无响应现象点击开始签到后窗口变灰拖动不了摄像头画面不刷新。原因在主线程里写了阻塞循环或者识别模型太重导致每帧处理时间超过定时器间隔。解决摄像头读取和显示用QTimer不要用while True如果单帧识别超过 100 毫秒把识别放到QThread里通过信号把结果传回主线程更新界面降低摄像头分辨率也能明显减轻负担。5.5 中文人名乱码或 CSV 打不开现象签到表里人名显示成问号或者 Excel 打开 CSV 全是乱码。原因CSV 写入编码和 Excel 默认编码不一致或者 Qt 控件字体不支持中文。解决写 CSV 时用encodingutf-8-sigExcel 能正确识别Qt 界面里设置支持中文的字体比如Microsoft YaHei如果源码里用cv2.putText在画面上写中文OpenCV 默认不支持需要转成 PIL 绘制或直接用英文标签。6. 把原型推到能用阈值调优、活体防伪和工程化习惯最后一章说几个让这套考勤系统从「能跑」到「敢用」的技巧。第一是阈值调优不要凭感觉做一个简单的评估脚本把已知人脸和陌生人脸各跑 50 次统计不同threshold下的通过率和误识率选一个平衡点。LBPH 的confidence不是概率不能直接当准确率看但相对大小有参考价值。第二是活体防伪。照片对着摄像头也能识别通过这是 HaarLBPH 方案的硬伤。低成本做法是加一个眨眼检测或随机动作提示用cv2.CascadeClassifier加载眼睛检测器连续几帧检测不到眼睛就判为照片。更稳的做法是换用支持活体检测的 SDK但那已经超出这份源码的范围。第三是数据管理。dataset目录会随着人数增加变大训练时间线性上升。常见做法是定期清理无效样本把trainer.yml和label_map.npy一起版本化换机器时直接拷贝。签到 CSV 建议按月份分文件避免单文件过大。第四是界面细节。PyQt5 的QTableWidget在记录多时会变慢可以改用QTableView加模型摄像头画面比例不对时检查scaled参数程序退出时一定要释放摄像头和定时器否则下次启动可能报设备占用。我自己踩过最深的坑是训练和识别前处理不一致训练时用了equalizeHist识别时忘了加结果同一个人置信度从 40 跳到 90排查了一下午。后来养成习惯把前处理写成一个函数训练和识别都调它再也没出过这个问题。另外不要迷信源码里的默认阈值换一个摄像头、换一个房间参数就要重新调。希望帮到你。本文还有配套的精品资源点击获取
返回列表