☰
Python+OpenCV+PyQt5人脸识别考勤系统源码实战与避坑指南
2026/9/28 1:26:26 网站建设 项目流程

简介:这是一套面向课程设计与毕业设计场景的人脸识别考勤签到系统源码,基于Python、OpenCV与PyQt5开发,适合具备一定Python基础、希望深入实践计算机视觉与桌面应用开发的学习者。系统围绕人脸检测、特征提取与签到记录管理展开,涉及Haar级联分类器、LBPH等识别方法,并配套PyQt5构建的签到与查询界面,同时涵盖数据库操作、时间处理、异常捕获等工程细节。资源包共44个文件,以py源码、pyc编译文件、xml分类器模型、ui界面文件及说明文档为主,压缩包约681KB,目录结构清晰,便于按模块阅读与二次开发。代码中附有详细注释,可帮助读者理解人脸识别流程、GUI信号槽交互与考勤数据存储逻辑。目前已有266人学习,适合作为课程设计、毕业设计参考或计算机视觉入门练手项目。

1. 从一份能跑起来的考勤系统源码说起:Python+OpenCV+PyQt5 到底怎么落地

很多人第一次接触人脸识别考勤,是从下载一份「基于 Python+OpenCV 人脸识别的考勤签到系统源码」开始的。压缩包里通常有主程序、训练脚本、人脸数据目录、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 numpy==1.24.3 # 装 OpenCV 主包即可,人脸检测器在主包里就有 pip install opencv-python==4.8.1.78 # 装 PyQt5,注意 Python 3.11 以上建议用 5.15.9 之后的版本 pip install PyQt5==5.15.10 # 可选:如果源码用了人脸识别库 pip install face_recognition==1.3.0

逻辑说明:opencv-python已经包含 Haar 级联和 LBPH 人脸识别器,不需要额外装 contrib,除非源码里用了 SIFT、SURF 这类专利算法。numpy版本不要太高,否则和旧版 OpenCV 的 ABI 可能不兼容。PyQt5 安装失败时,先确认 Python 位数和 pip 是否最新,再考虑换 3.9 或 3.10 环境。

参数说明:opencv-python==4.8.1.78是一个稳定版本,摄像头读取和cv2.waitKey行为比较一致;PyQt5==5.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, scaleFactor=1.1, # 每次图像缩放比例 minNeighbors=5, # 候选框保留阈值 minSize=(80, 80), # 最小人脸尺寸 flags=cv2.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()

逻辑说明:scaleFactor=1.1表示每次把图像缩小 10% 再检测,值越小检测越慢但越不容易漏。minNeighbors=5控制误检,值越大越严格,但太小会把侧脸漏掉。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( radius=1, # 邻域半径 neighbors=8, # 邻域点数 grid_x=8, # 网格列数 grid_y=8, # 网格行数 threshold=80 # 识别阈值,越小越严格 ) # 训练 recognizer.train(faces, labels) # 保存模型 os.makedirs('trainer', exist_ok=True) recognizer.save('trainer/trainer.yml') # 保存标签映射,方便识别时显示人名 np.save('trainer/label_map.npy', label_map)

逻辑说明:radius和neighbors控制 LBP 算子感受野,grid_x、grid_y把图像分成 8x8 网格,每个网格单独统计直方图,能保留一定空间信息。threshold是识别置信度阈值,LBPH 的predict返回的第二个值越小表示越像,超过阈值就判为未知。

参数说明:threshold=80是一个经验值,实际项目里要根据样本质量调。如果经常把陌生人认成已录入的人,把阈值降到 60 左右;如果自己人经常认不出,先检查训练样本是否太少或光照差异太大,再考虑放宽到 100。radius=1、neighbors=8适合 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_pickle=True).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='', encoding='utf-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_pickle=True。

参数说明:roi必须和训练时做同样的 resize 和equalizeHist,否则识别率会明显下降。CSV 写入用encoding='utf-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 是 BGR,Qt 需要 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 时用encoding='utf-8-sig',Excel 能正确识别;Qt 界面里设置支持中文的字体,比如Microsoft YaHei;如果源码里用cv2.putText在画面上写中文,OpenCV 默认不支持,需要转成 PIL 绘制或直接用英文标签。

6. 把原型推到能用:阈值调优、活体防伪和工程化习惯

最后一章说几个让这套考勤系统从「能跑」到「敢用」的技巧。第一是阈值调优不要凭感觉,做一个简单的评估脚本:把已知人脸和陌生人脸各跑 50 次,统计不同threshold下的通过率和误识率,选一个平衡点。LBPH 的confidence不是概率,不能直接当准确率看,但相对大小有参考价值。

第二是活体防伪。照片对着摄像头也能识别通过,这是 Haar+LBPH 方案的硬伤。低成本做法是加一个眨眼检测或随机动作提示,用cv2.CascadeClassifier加载眼睛检测器,连续几帧检测不到眼睛就判为照片。更稳的做法是换用支持活体检测的 SDK,但那已经超出这份源码的范围。

第三是数据管理。dataset目录会随着人数增加变大,训练时间线性上升。常见做法是定期清理无效样本,把trainer.yml和label_map.npy一起版本化,换机器时直接拷贝。签到 CSV 建议按月份分文件,避免单文件过大。

第四是界面细节。PyQt5 的QTableWidget在记录多时会变慢,可以改用QTableView加模型;摄像头画面比例不对时检查scaled参数;程序退出时一定要释放摄像头和定时器,否则下次启动可能报设备占用。

我自己踩过最深的坑是训练和识别前处理不一致:训练时用了equalizeHist,识别时忘了加,结果同一个人置信度从 40 跳到 90,排查了一下午。后来养成习惯,把前处理写成一个函数,训练和识别都调它,再也没出过这个问题。另外,不要迷信源码里的默认阈值,换一个摄像头、换一个房间,参数就要重新调。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询