资讯详情

资讯详情

Python+OpenCV+PyQt5人脸识别考勤系统实战:从环境搭建到避坑指南

简介这是一套面向课程设计与毕业设计场景的人脸识别考勤签到系统源码基于Python、OpenCV与PyQt5构建适合希望将计算机视觉与桌面GUI结合实践的学习者。项目以Haar级联分类器完成人脸检测配合LBPH等特征提取方法实现识别并通过PyQt5搭建签到与查询界面同时涉及SQLite数据库存储、datetime时间处理、异常捕获及文件读写等工程细节代码中配有详细注释便于理解与二次开发。资源包共44个文件以py源码、pyc缓存、xml分类器模型、ui界面文件及md说明文档为主压缩后约681KB目录按功能模块划分清晰。目前已有266人学习下载可作为课程设计或毕业设计的完整参考方案帮助读者快速掌握从人脸检测、特征匹配到界面交互与考勤记录管理的整体实现思路。1. 从一份带 PyQt5 界面的考勤源码说起这套东西到底能跑出什么效果很多人第一次接触人脸识别考勤脑子里想的是门禁机那种刷脸开门的硬件方案但真正适合个人开发者练手、也最容易二次开发的其实是一套纯软件方案Python 负责逻辑OpenCV 负责图像处理和人脸检测PyQt5 负责桌面界面最后把识别结果写进签到记录。这个组合的好处是全部跑在普通电脑和 USB 摄像头上不需要专用硬件源码结构清晰注释到位的话新手也能顺着读下来。这套方案能解决的核心问题很具体员工或学生走到摄像头前系统自动判断你是谁然后记录签到时间避免手工点名或刷卡代签。适合谁适合想入门计算机视觉的 Python 学习者、需要给课程设计或毕设找落地方向的学生、以及想给小型团队做一套轻量考勤工具的人。它不适合追求工业级精度和防伪能力的场景这一点后面会反复提到。整篇文章我会按环境怎么搭 → 人脸检测和识别怎么串起来 → PyQt5 界面怎么接 → 坑在哪 → 怎么验证和进阶的顺序讲每一步都落到能直接抄的命令和代码上。2. 环境搭建Python、OpenCV、PyQt5 三件套怎么装才不翻车2.1 版本选择与虚拟环境隔离这套方案对版本不算特别挑剔但有几个组合是经过大量人验证、报错最少的。Python 建议 3.8 到 3.10太新的版本比如 3.12 之后在装某些 OpenCV 轮子时偶尔会遇到没有预编译包、被迫本地编译的情况对新手就是灾难。OpenCV 用opencv-python和opencv-contrib-python两个包后者包含人脸识别模块cv2.face只装前者会报module cv2 has no attribute face这是血泪经验里出现频率最高的一个。先建虚拟环境别把依赖装到全局否则以后做别的项目版本冲突会让你想重装系统# 创建虚拟环境名字叫 att_env python -m venv att_env # Windows 激活 att_env\Scripts\activate # Linux / macOS 激活 source att_env/bin/activate # 升级 pip避免装包时各种奇怪的解析错误 python -m pip install --upgrade pip激活成功后命令行前面会出现(att_env)前缀。这一步的意义在于把项目依赖和系统 Python 隔离开后面就算装崩了删掉整个文件夹重来就行不用动系统环境。2.2 安装 OpenCV 与 PyQt5 的具体命令依赖装的时候顺序有讲究先装 numpy因为 OpenCV 依赖它某些情况下 pip 自动解析会装错版本# 先装 numpy指定一个稳定版本 pip install numpy1.24.3 # 装 OpenCV 主包和 contrib 包版本保持一致 pip install opencv-python4.8.1.78 pip install opencv-contrib-python4.8.1.78 # 装 PyQt5 pip install PyQt55.15.9装完立刻验证别等到写代码时才发现问题import cv2 import numpy as np from PyQt5.QtWidgets import QApplication # 打印版本确认 contrib 模块在 print(OpenCV:, cv2.__version__) print(numpy:, np.__version__) # 关键确认人脸模块存在 print(face module:, hasattr(cv2, face))如果hasattr(cv2, face)输出False说明你装的是普通opencv-python卸载后重装 contrib 版本即可。参数上opencv-python和opencv-contrib-python版本号必须一致混装不同版本会出现符号冲突表现为 import 直接崩溃。提示如果你在 Ubuntu 上装可能还需要sudo apt install libgl1之类的系统库否则import cv2会报libGL.so.1: cannot open shared object file。这是 Linux 下最常见的翻车点和 Python 包本身无关。2.3 摄像头与项目目录结构摄像头用cv2.VideoCapture(0)打开0 是默认设备号。如果笔记本有多个摄像头可能要试 1 或 2。项目目录建议这样组织后面代码里的路径都按这个来attendance_system/ ├── main.py # 程序入口启动 PyQt5 界面 ├── face_engine.py # 人脸检测与识别逻辑 ├── ui_main.py # 界面定义 ├── trainer/ │ └── trainer.yml # 训练好的人脸模型 ├── dataset/ │ └── user_001/ # 每个人一个文件夹存人脸样本 └── records/ └── attendance.csv # 签到记录目录结构清晰的好处是训练模型、采集样本、写记录各管各的出问题时能快速定位是哪一层。很多人把所有逻辑塞进一个文件几百行之后自己都读不下去这是新手最容易犯的结构性错误。3. 人脸检测与识别从 Haar 级联到 LBPH 的完整链路3.1 人脸检测为什么先用 Haar 级联人脸识别分两步先找到脸在哪再判断这是谁。第一步用检测器OpenCV 自带 Haar 级联分类器文件是haarcascade_frontalface_default.xml随包安装路径可以用cv2.data.haarcascades拿到。它的优点是快、无需训练、CPU 就能跑缺点是侧脸、遮挡、光线差时漏检明显。import cv2 # 加载 Haar 级联人脸检测器 cascade_path cv2.data.haarcascades haarcascade_frontalface_default.xml detector cv2.CascadeClassifier(cascade_path) def detect_faces(gray_frame): # scaleFactor 控制金字塔缩放步长1.1~1.3 常用 # minNeighbors 越大越严格误检少但可能漏检 # minSize 限制最小人脸尺寸过滤远处噪点 faces detector.detectMultiScale( gray_frame, scaleFactor1.2, minNeighbors5, minSize(80, 80) ) return facesscaleFactor1.2表示每次图像缩小 20% 再检测值越小检测越细但越慢minNeighbors5是每个候选框周围需要多少个邻居才确认调大能压误检minSize(80,80)把太小的框直接丢掉避免把背景纹理当脸。这三个参数是调优的核心光线差的环境把minNeighbors降到 3 到 4能救回一些漏检。3.2 用 LBPH 做识别训练与预测识别器选 LBPHLocal Binary Patterns Histograms它是 OpenCV contrib 里的人脸识别算法优点是样本需求少、对光照变化相对鲁棒、训练快适合小规模考勤。它不需要 GPU几百张图几分钟就能训完。采集样本时每个人拍 30 到 50 张覆盖不同角度和表情import cv2 import os def collect_samples(user_id, save_dirdataset, count40): cam cv2.VideoCapture(0) detector cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) user_dir os.path.join(save_dir, fuser_{user_id}) os.makedirs(user_dir, exist_okTrue) saved 0 while saved count: ok, frame cam.read() if not ok: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces detector.detectMultiScale(gray, 1.2, 5, minSize(80, 80)) for (x, y, w, h) in faces: saved 1 # 统一裁剪成灰度人脸并保存 face gray[y:yh, x:xw] cv2.imwrite(os.path.join(user_dir, f{saved}.jpg), face) cv2.rectangle(frame, (x, y), (xw, yh), (0, 255, 0), 2) cv2.imshow(collect, frame) if cv2.waitKey(1) 0xFF 27: break cam.release() cv2.destroyAllWindows()保存的是灰度图因为 LBPH 只吃灰度提前转好省得训练时再处理。文件名用递增数字训练时按文件夹名映射到用户 ID。训练阶段把所有样本读进来标签用整数 IDimport cv2 import os import numpy as np def train_model(dataset_dirdataset, model_pathtrainer/trainer.yml): recognizer cv2.face.LBPHFaceRecognizer_create() faces, labels [], [] label_map {} current_id 0 for user_name in sorted(os.listdir(dataset_dir)): user_path os.path.join(dataset_dir, user_name) if not os.path.isdir(user_path): continue label_map[current_id] user_name for img_name in os.listdir(user_path): img_path os.path.join(user_path, img_name) gray cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) if gray is None: continue faces.append(gray) labels.append(current_id) current_id 1 recognizer.train(faces, np.array(labels)) os.makedirs(os.path.dirname(model_path), exist_okTrue) recognizer.save(model_path) return label_mapLBPHFaceRecognizer_create()有几个可选参数radius默认 1控制局部二值模式的邻域半径neighbors默认 8邻域点数grid_x、grid_y默认 8把图像分成 8×8 网格分别统计直方图。默认值对考勤场景够用样本少的时候可以把grid_x、grid_y调到 6 减少特征维度降低过拟合。预测时返回标签和置信度置信度是距离值越小越像def predict(recognizer, gray_face, label_map, threshold70): label, confidence recognizer.predict(gray_face) if confidence threshold: return label_map.get(label, unknown), confidence return unknown, confidencethreshold70是经验值低于它认为是同一个人高于它判为陌生人。这个阈值必须根据你的实际数据调样本质量差就调高到 80 到 90误识多就调低。没有万能值这是最需要动手试的参数。3.3 把检测和识别串成实时循环单帧流程是读帧 → 转灰度 → 检测人脸 → 对每张脸裁剪 → 送识别器 → 得到名字 → 画框和文字。实时循环里要注意性能每帧都做全图检测会卡常见做法是隔帧检测或者缩小检测区域。def run_recognition(recognizer, label_map): cam cv2.VideoCapture(0) detector cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) while True: ok, frame cam.read() if not ok: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces detector.detectMultiScale(gray, 1.2, 5, minSize(80, 80)) for (x, y, w, h) in faces: face gray[y:yh, x:xw] name, conf predict(recognizer, face, label_map) color (0, 255, 0) if name ! unknown else (0, 0, 255) cv2.rectangle(frame, (x, y), (xw, yh), color, 2) cv2.putText(frame, f{name} {conf:.0f}, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, color, 2) cv2.imshow(attendance, frame) if cv2.waitKey(1) 0xFF 27: break cam.release() cv2.destroyAllWindows()这段代码是识别引擎的核心PyQt5 界面只是把它包起来显示。理解了这个循环界面部分就是换皮的事。4. PyQt5 界面接入把识别循环塞进窗口而不卡死4.1 界面线程与识别线程必须分开新手最容易踩的坑是把while True的识别循环直接写在 PyQt5 主线程里结果窗口直接卡死点哪都没反应。原因是 Qt 的事件循环被你的死循环占住了界面无法刷新。正确做法是把识别逻辑放到QThread里通过信号把结果传回主线程更新界面。from PyQt5.QtCore import QThread, pyqtSignal import cv2 class RecognizeThread(QThread): # 信号传回当前帧和识别到的名字 frame_ready pyqtSignal(object) name_detected pyqtSignal(str) def __init__(self, recognizer, label_map): super().__init__() self.recognizer recognizer self.label_map label_map self.running True def run(self): cam cv2.VideoCapture(0) detector cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) while self.running: ok, frame cam.read() if not ok: continue gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces detector.detectMultiScale(gray, 1.2, 5, minSize(80, 80)) for (x, y, w, h) in faces: face gray[y:yh, x:xw] name, conf predict(self.recognizer, face, self.label_map) if name ! unknown: self.name_detected.emit(name) cv2.rectangle(frame, (x, y), (xw, yh), (0, 255, 0), 2) cv2.putText(frame, name, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) # 转成 RGB 再发Qt 显示需要 rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) self.frame_ready.emit(rgb) def stop(self): self.running False self.wait()pyqtSignal(object)用来传 numpy 数组object类型能兼容。name_detected信号在识别到已知人脸时发出主线程收到后写签到记录。stop()里先置标志再wait()保证线程干净退出不然关窗口时可能报QThread: Destroyed while thread is still running。4.2 主窗口布局与信号连接主窗口放一个 QLabel 显示视频一个 QListWidget 显示签到名单一个按钮启动和停止from PyQt5.QtWidgets import (QWidget, QLabel, QPushButton, QListWidget, QVBoxLayout, QHBoxLayout) from PyQt5.QtGui import QImage, QPixmap import cv2 import datetime class MainWindow(QWidget): def __init__(self, recognizer, label_map): super().__init__() self.recognizer recognizer self.label_map label_map self.thread None self.signed set() self.video_label QLabel(摄像头未启动) self.video_label.setFixedSize(640, 480) self.list_widget QListWidget() self.start_btn QPushButton(启动签到) self.stop_btn QPushButton(停止) btn_layout QHBoxLayout() btn_layout.addWidget(self.start_btn) btn_layout.addWidget(self.stop_btn) right_layout QVBoxLayout() right_layout.addWidget(self.list_widget) right_layout.addLayout(btn_layout) main_layout QHBoxLayout() main_layout.addWidget(self.video_label) main_layout.addLayout(right_layout) self.setLayout(main_layout) self.start_btn.clicked.connect(self.start_recognition) self.stop_btn.clicked.connect(self.stop_recognition) def start_recognition(self): self.thread RecognizeThread(self.recognizer, self.label_map) self.thread.frame_ready.connect(self.update_frame) self.thread.name_detected.connect(self.on_name) self.thread.start() def update_frame(self, rgb): h, w, ch rgb.shape img QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888) self.video_label.setPixmap(QPixmap.fromImage(img)) def on_name(self, name): # 同一个人一次会话只记一次 if name in self.signed: return self.signed.add(name) ts datetime.datetime.now().strftime(%Y-%m-%d %H:%M:%S) self.list_widget.addItem(f{name} {ts}) with open(records/attendance.csv, a, encodingutf-8) as f: f.write(f{name},{ts}\n) def stop_recognition(self): if self.thread: self.thread.stop() self.thread Noneupdate_frame里构造QImage时第四个参数是每行字节数必须传ch * w传错会导致图像花屏或错位这是 PyQt5 显示 OpenCV 图像最经典的坑。on_name用set去重保证一次会话同一个人只签一次避免摄像头对着人时疯狂写记录。4.3 启动入口与资源释放入口文件把模型加载、界面启动串起来import sys from PyQt5.QtWidgets import QApplication import cv2 if __name__ __main__: app QApplication(sys.argv) recognizer cv2.face.LBPHFaceRecognizer_create() recognizer.read(trainer/trainer.yml) # label_map 需要和训练时一致实际项目里存成 json 读回来 label_map {0: user_001, 1: user_002} win MainWindow(recognizer, label_map) win.show() sys.exit(app.exec_())label_map在真实项目里应该训练时存成 JSON启动时读回来硬编码只适合演示。关窗口时 Qt 会触发退出但识别线程如果还在跑需要在closeEvent里调stop_recognition否则进程可能残留。5. 避坑与排查这套方案最容易翻车的 5 个地方5.1 报错module cv2 has no attribute face现象是运行到cv2.face.LBPHFaceRecognizer_create()直接崩。原因是装的是opencv-python而不是opencv-contrib-python人脸模块在 contrib 包里。解决是先pip uninstall opencv-python opencv-contrib-python再只装opencv-contrib-python版本和 numpy 对齐。装完用hasattr(cv2, face)验证。5.2 摄像头打开但画面全黑或卡在第一帧现象是cam.read()返回 True 但图像全黑或者界面一直不动。原因通常是摄像头被其他程序占用比如另一个没关的脚本、视频会议软件或者设备号不对。解决是先关掉所有可能占用摄像头的程序把VideoCapture(0)换成 1 或 2 试Linux 下可以用ls /dev/video*看设备。另外有些摄像头前几帧是黑的循环里丢弃前 5 帧再显示。5.3 识别总是判成 unknown 或认错人现象是明明是同一个人置信度却很高距离大或者把 A 认成 B。原因有三个样本太少或角度单一、阈值设得不合理、光照差异太大。解决是每人采 40 张以上覆盖多角度重新训练把threshold从 70 调到 85 试采集和识别时的光照尽量一致别一个在窗边一个在背光。LBPH 对光照敏感这是算法本身的边界不是代码问题。5.4 PyQt5 界面卡死或无响应现象是点了启动按钮后窗口变灰拖不动。原因是识别循环跑在主线程。解决是必须用QThread把while循环放进run()通过信号更新界面。检查点frame_ready.emit传的是 numpy 数组主线程里转QImage时行字节数参数别写错。如果还是卡降低识别频率比如每 3 帧处理一次。5.5 关窗口后进程不退出或报线程错误现象是点了关闭命令行还挂着或者报QThread: Destroyed while thread is still running。原因是识别线程没停。解决是在主窗口重写closeEvent先调self.thread.stop()再event.accept()。stop()里置runningFalse后wait()等线程结束别用terminate()强杀会留下资源泄漏。6. 进阶与验证怎么判断这套考勤系统值不值得继续投入6.1 用混淆矩阵验证识别效果而不是凭感觉很多人做完就凭看着还行下结论这不靠谱。正确做法是留出一部分没参与训练的样本做测试统计每个用户的识别情况画一个混淆矩阵。下面这段代码读测试集输出每个样本的预测结果import cv2 import os import numpy as np def evaluate(recognizer, label_map, test_dirtest_dataset, threshold70): # 收集真实标签和预测标签 y_true, y_pred [], [] for user_name in sorted(os.listdir(test_dir)): user_path os.path.join(test_dir, user_name) if not os.path.isdir(user_path): continue for img_name in os.listdir(user_path): gray cv2.imread(os.path.join(user_path, img_name), cv2.IMREAD_GRAYSCALE) if gray is None: continue label, conf recognizer.predict(gray) pred label_map.get(label, unknown) if conf threshold else unknown y_true.append(user_name) y_pred.append(pred) # 统计准确率 correct sum(1 for t, p in zip(y_true, y_pred) if t p) print(f准确率: {correct / len(y_true):.2%}) # 打印每个用户的错分情况 for t, p in zip(y_true, y_pred): if t ! p: print(f真实 {t} - 预测 {p})跑完看准确率和错分明细。如果某个用户总是被认成另一个说明这两个人样本特征太像或者采集时光照接近需要补采或换角度。准确率低于 80% 就别急着上生产先优化样本和阈值。这个验证步骤是判断值不值得做的关键数据说话比感觉靠谱。6.2 从 LBPH 升级到深度学习识别的边界LBPH 的天花板很明显人一多、光照一变、角度一大准确率就掉。想往上走常见路线是换成人脸检测用 DNN 模型OpenCV 的FaceDetectorYN或 MTCNN识别用 embedding 模型比如 FaceNet 类思路把每张脸映射成高维向量再算余弦相似度。这条路精度高很多但代价是需要更多样本、需要 GPU 加速推理、代码复杂度上一个台阶。判断要不要升级看三个指标用户数超过 20 人、识别准确率长期低于 90%、有防代签需求。如果只是课程设计或小团队十几个人LBPH 完全够用别为了炫技把项目搞复杂。我自己的习惯是先用 LBPH 跑通全流程把界面、记录、线程这些工程问题解决掉再考虑换识别后端因为工程问题比算法问题更耗时间。6.3 签到记录的去重与时间窗口实际用起来会发现摄像头对着人时同一个人会被反复识别记录里一堆重复。除了前面用set去重更稳的做法是加时间窗口同一个人 5 分钟内只记一次。把on_name改成记录上次签到时间import time class MainWindow(QWidget): def __init__(self, recognizer, label_map): # ... 前面的初始化 self.last_sign {} # name - timestamp def on_name(self, name): now time.time() last self.last_sign.get(name, 0) # 300 秒内重复识别直接忽略 if now - last 300: return self.last_sign[name] now ts datetime.datetime.now().strftime(%Y-%m-%d %H:%M:%S) self.list_widget.addItem(f{name} {ts}) with open(records/attendance.csv, a, encodingutf-8) as f: f.write(f{name},{ts}\n)300这个窗口按你的考勤粒度调一天一签就设 86400一节课一签设 3000 左右。这个改动很小但能避免记录表被刷爆是上线前必须补的一环。6.4 我踩过的最大一个坑最后说个我自己的教训。早期做这套东西时我把训练和识别放在同一个脚本里每次启动都重新训练一遍样本一多启动要等好几分钟还以为是代码效率问题折腾了半天多线程。后来才反应过来训练是离线的一次性动作识别是加载模型后的推理两者必须分开。现在我的习惯是训练脚本单独跑产出trainer.yml主程序只读模型。这个分离看起来是常识但新手阶段特别容易混在一起导致启动慢、调试烦。把训练和推理解耦是这套方案从能跑到好用的分水岭。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →