简介:基于Python、OpenCV与PyQt5构建的人脸识别考勤签到系统源码,面向课程设计、毕业设计及计算机视觉入门学习者。项目采用PyQt5完成签到界面,通过OpenCV内置Haar级联分类器实现人脸检测,并结合特征提取与匹配完成身份验证,覆盖从图像采集、人脸比对到考勤记录存储的完整链路。压缩包共44个文件,以12个py源码文件为核心,辅以16个pyc编译文件、10个xml级联分类器配置、2个ui界面文件及requirements.txt依赖清单,整体容量681KB,结构紧凑便于直接运行与二次开发。项目包含详细注释,涉及Python语法、OpenCV图像处理、PyQt5控件布局、数据库操作、datetime时间处理及try-except异常处理等知识点,可快速理解人脸识别考勤系统的实现思路。目前已有266人学习下载,适合需要参考完整项目、快速上手GUI程序或完成课设/毕设的读者。
1. 基于 OpenCV 的人脸识别考勤签到系统:下载之后怎么把它真正跑起来
下载过这类基于 Python + OpenCV + PyQt5 的人脸识别考勤签到系统的人,第一反应基本都是找到 MainWindow.py 双击运行,然后被一堆缺包报错劝退。这个资源不一样的地方,在于它把代码拆成了 face_model.py、CamShow.py、Functional_function.py 等几个独立模块,训练、摄像头采集、签到记录分开写,报错时能顺着模块定位问题,而不是在一坨代码里猜。系统走的是 Haar 级联检测人脸 + LBPH 特征识别的路线,配上 PyQt5 做的图形界面,不依赖 GPU,普通笔记本就能跑,非常适合课程设计、毕业设计拿来二次改造,也适合想弄懂 OpenCV 人脸识别完整流程的初学者。我的建议是:先别急着跑,花十分钟把文件结构看懂,后面能少踩一半坑。
2. 考勤签到系统的整体结构:从文件清单反推模块划分与识别选型
2.1 从文件清单反推系统架构
这个项目的根目录下,code 文件夹里躺着 MainWindow.py、QtUI.py、CamShow.py、upload.py、face_model.py、Functional_function.py、Camo_open.py,另外还有一个 ui 子目录,里面放着 upload.ui、CamShow.ui 以及对应的 py 文件。数据部分在 data 目录下,三个 haarcascade 开头的 xml 文件分别是正面人脸检测器、另一版本的人脸检测器、带眼镜的人眼检测器。这几个文件决定了整个系统的检测能力边界。
按我拆项目的习惯,会把文件映射成四层来看:
| 层次 | 对应文件 | 作用 |
|---|---|---|
| 界面入口层 | MainWindow.py、QtUI.py、ui/*.ui | 窗口布局、按钮事件、信号槽绑定 |
| 业务逻辑层 | Functional_function.py | 签到时间计算、迟到判断、记录写入 |
| 视觉处理层 | CamShow.py、upload.py、Camo_open.py | 摄像头采集、照片上传、人脸裁剪 |
| 模型与数据层 | face_model.py、data/*.xml | 训练模型、加载分类器、保存识别结果 |
Camo_open.py 这个命名应该是 camera_open 的笔误,功能大概率是封装摄像头打开逻辑。ui 目录下又放了一份 CamShow.py 和 upload.py,常见做法是把带界面的窗口类和纯逻辑类分开维护,所以你改代码时要注意以哪一份为准,否则会出现“明明改了却不起作用”的玄学问题。
另外,根目录的一堆__pycache__里能看到 cpython-37 和 cpython-39 两种后缀,说明作者至少在 Python 3.7 和 3.9 上跑过这个项目,这对后面你选解释器版本是个很实用的参考信息。不要一上来就用最新的 Python 3.12,后面第三章我会说为什么。
2.2 为什么选 Haar 检测 + LBPH 识别,而不是深度学习方案
很多人拿到项目后第一反应是:人脸识别怎么不用深度学习?这个疑问可以理解,但放在课设和毕设场景下,Haar + LBPH 反而是更合理的选型。Haar 级联分类器是 OpenCV 里经典的物体检测方案,通过大量正负样本训练出一串级联分类器,能在灰度图上快速扫描出人脸区域。它的优点是 CPU 上跑得飞快,树莓派都能带动,缺点是对大角度侧脸、暗光环境比较敏感。
识别部分用 LBPH,全称 Local Binary Patterns Histograms,中文叫局部二值模式直方图。它把脸部分成小块,提取每个块的纹理直方图,拼成一个特征向量来做比对。和 Eigenfaces、Fisherfaces 相比,LBPH 对光照变化更鲁棒一点,而且不需要像深度学习那样动辄几千张训练图。在课设这种“每个学生录几张照片就要上线”的场景里,这个取舍非常现实。
这里有几个关键参数你改代码时一定会碰到:
scaleFactor:检测窗口缩放比例,常见 1.1 到 1.3。越小检测越慢但越准,我一般取 1.1。minNeighbors:每个候选区域至少被检测到几次才算真脸,常见 3 到 6。过大容易漏检,过小容易误检。minSize:最小人脸尺寸,太小人脸会被忽略,比如(50, 50)。
这三个参数直接决定摄像头画面里的人能不能被框住,是后续识别环节的前置条件。很多识别不准的问题,根源其实是第一步人脸都没好好框住。
2.3 业务数据流:注册、训练、签到三步走
这套系统的业务逻辑比代码结构更好理解,就三步:
- 注册:打开 upload.py 或界面对应按钮,选一张正面照片,系统用 Haar 检测人脸,把人脸区域裁剪保存到预定的目录。这一步相当于给每个员工建人脸档案。
- 训练:跑 face_model.py,读取所有已注册的人脸图片,提取 LBPH 特征,训练出识别模型,保存成 xml 或 yml 文件。
- 签到:MainWindow.py 启动后,CamShow.py 打开摄像头,每一帧先做人脸检测,把检测到的人脸交给模型做 predict,返回一个标签和置信度。标签能对到人,置信度在阈值内,就认为签到成功,Functional_function.py 负责把时间写入记录文件。
整个流程是典型的“先检测后识别”两段式结构,跟人脸识别门禁机的工作原理本质上是一样的,只是精度和速度有差距。理解了这个数据流,你再看代码时会发现每个文件各司其职,不会一头扎进某个函数里出不来。
3. 环境搭建与跑通最小流程:版本匹配、依赖安装、摄像头自检
3.1 requirements.txt 与解释器版本:为什么建议用 Python 3.7 到 3.9
拿到项目第一步不是点运行,而是看依赖。项目里有 requirements.txt,这比很多只丢一堆 py 文件的资源良心得多。结合__pycache__里的 pyc 版本,我建议你直接装 Python 3.8 或 3.9,不要装 3.10 以上的新版本。原因是 PyQt5 的老版本在 Python 3.10 之后的某些环境里会出现信号槽相关的兼容问题,而 OpenCV 的 contrib 包在新 Python 版本下也可能因为 wheel 编译问题让你原地折腾半天。
安装依赖用 pip 一把梭:
pip install opencv-contrib-python PyQt5 numpy注意,这里我特意写的是opencv-contrib-python,不是opencv-python。原因是这个项目的 face_model.py 里要用到 LBPH 识别器,而 LBPH 属于 OpenCV 的 face 模块,只有 contrib 版本才带。如果你只装了 opencv-python,运行时会报类似 AttributeError 的错误,说找不到 face 相关属性。这是这个项目最常见的第一个翻车点。
如果你网速慢,可以加国内镜像源:
pip install opencv-contrib-python PyQt5 numpy -i https://pypi.tuna.tsinghua.edu.cn/simple装完后验证一下导入:
python -c "import cv2; print(cv2.__version__); import PyQt5; print('pyqt5 ok')"能正常输出版本号,说明环境这一关过了。如果 import cv2 就报 ModuleNotFoundError,先别怀疑代码,去确认你当前用的 python 命令和 pip 是不是同一个解释器。这是 Python 环境里出现频率最高的低级坑,很多人装了半天发现装到了另一个 Python 上。
3.2 摄像头调用的前置检查:VideoCapture 的索引和状态判断
OpenCV 调用摄像头走的是 VideoCapture 接口,默认参数是 0,代表第一个摄像头。这个 0 在笔记本上通常是内置摄像头,在台式机上可能是第一个 USB 摄像头。代码里常见写法是:
import cv2 cap = cv2.VideoCapture(0) # 0 表示默认摄像头索引 if not cap.isOpened(): print("摄像头打开失败,请检查索引和权限") exit() ret, frame = cap.read() if not ret: print("读取帧失败,可能是摄像头被占用") exit() print("摄像头正常工作,画面尺寸:", frame.shape)这段代码解决的是“摄像头黑屏”的一半问题。isOpened() 返回 False 说明设备本身没打开成功,ret 为 False 说明设备打开了但读不到画面,这两个状态要分开排查。前者通常是索引不对或者权限没给,后者通常是别的程序正在占用摄像头,比如钉钉或腾讯会议还开着。
我一般会写一个几行的自检脚本去遍历 0 到 2 三个索引,逐个尝试打开,哪个能读到画面就用哪个。这样能快速排除索引写死导致的黑屏问题。
3.3 最小可运行路径:先注册、再训练、后签到
整个项目拿到手,不要一上来就点签到按钮。你需要先建立人脸库,否则模型是空的,识别什么都是“未知”。最小可运行路径我建议这样走:
# 第一步:跑上传注册脚本,准备 2 到 3 张人脸照片 python upload.py # 第二步:跑训练脚本,生成模型文件 python face_model.py # 第三步:启动主界面,此时再去点签到按钮 python MainWindow.py三步对应的结果分别是:程序能把你选的照片里的人脸裁出来、训练过程能打印出样本数量和处理结果、主窗口能弹出并且摄像头画面不黑。走到第三步,说明系统全链路已经通了。
需要特别说明的是,如果你在 ui 目录下也看到了同名文件,入口优先使用根目录下的 MainWindow.py,别两个路径混着跑,否则你注册的照片和训练脚本读的目录可能对不上。这类“目录不一致”的问题在原始资源里很常见,代码本身没问题,是路径太分散导致的。
跑通之后,再去动代码改逻辑。先把最小闭环跑起来,再谈优化,这是所有带硬件的项目通用的调试顺序。
4. 核心代码模块拆解:照片注册、LBPH 模型训练、实时识别与考勤记录
4.1 upload.py:照片里的人脸是怎么被裁剪保存的
upload.py 的核心是从一张用户选定的照片里把脸抠出来,存成后续训练要用的样本。这个过程依赖 Haar 级联检测器,加载方式如下:
import cv2 import os # 加载级联检测器,路径用绝对路径更稳 face_cascade = cv2.CascadeClassifier( "data/haarcascade_frontalface_default.xml" ) img = cv2.imread("input.jpg") if img is None: print("图片读取失败,检查路径") exit() gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # scaleFactor=1.1 检测更细,minNeighbors=5 减少误检 faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(50, 50)) for i, (x, y, w, h) in enumerate(faces): face = img[y:y+h, x:x+w] # 裁剪人脸区域 save_path = f"data/faces/user1_{i}.jpg" cv2.imwrite(save_path, face) print("已保存:", save_path) print(f"检测到 {len(faces)} 张人脸")逻辑不复杂,但有一个非常重要的细节:检测用的是灰度图,保存用的是原图裁剪。检测阶段转灰度是为了加速,裁剪阶段用原图是为了保留颜色信息。这里的x, y, w, h四元组就是检测框的坐标和尺寸,img[y:y+h, x:x+w]是 numpy 数组的切片语法,注意顺序是高在前宽在后,写反了会截出一张转置过的脸。
参数上,如果发现照片里人脸没被检测到,优先调大minSize或调小scaleFactor;如果发现背景被当成脸,调大minNeighbors。我一般会要求每个用户至少存 10 到 20 张,而且覆盖正面、左右轻微转头的角度,这比追求单张识别精准带来的收益大得多。
4.2 face_model.py:LBPH 模型的训练与保存
face_model.py 负责把所有注册的人脸照片读进来,提取 LBPH 特征,训练出识别模型并保存成文件。核心代码结构如下:
import cv2 import os import numpy as np # 注意:LBPH 识别器在 contrib 包里,依赖 opencv-contrib-python recognizer = cv2.face.LBPHFaceRecognizer_create() faces = [] labels = [] label_map = {} # 标签到人名的映射 # 假设每个用户照片放在 data/faces/ 下,文件名形如 user1_0.jpg for file_name in os.listdir("data/faces"): path = os.path.join("data/faces", file_name) img = cv2.imread(path, cv2.IMREAD_GRAYSCALE) # 训练一律用灰度图 if img is None: continue label = int(file_name.split("_")[0].replace("user", "")) label_map.setdefault(label, file_name) faces.append(img) labels.append(label) # train() 要求输入是列表,且照片尺寸最好统一 recognizer.train(faces, np.array(labels)) recognizer.write("data/face_model.yml") print("模型已保存,共训练样本:", len(faces)) print("标签映射:", label_map)这里有两个容易被忽略的地方。第一,LBPH 识别器要求传入灰度图,如果你把彩色图传进去,OpenCV 会直接报错或者训练出一个异常模型。第二,train()的第二个参数必须是 numpy 数组形式展开的标签,而且标签要从 0 开始连续编号,否则后续 predict 返回的 label 可能对不上人。实际项目中,label_map 还会被单独存成 json,因为 yml 文件里只有标签数字,没有姓名,识别出 label=1 你要自己去对应到“张三”。
训练完成后,根目录会多出一个 face_model.yml。以后每新增一个人,重新跑一遍 face_model.py 即可。这个过程是纯 CPU 计算,几十张照片一两秒就能结束,所以课程设计答辩现场临时加人也能应付。
4.3 CamShow.py:实时帧处理与识别结果回写
CamShow.py 是系统的视觉核心,它从摄像头读帧,对每一帧做人脸检测,再把检测到的人脸交给 LBPH 模型识别。简化后的核心循环:
import cv2 face_cascade = cv2.CascadeClassifier("data/haarcascade_frontalface_default.xml") recognizer = cv2.face.LBPHFaceRecognizer_create() recognizer.read("data/face_model.yml") cap = cv2.VideoCapture(0) while True: ret, frame = cap.read() if not ret: break gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(100, 100)) for (x, y, w, h) in faces: roi = gray[y:y+h, x:x+w] # 识别也用灰度图 label, confidence = recognizer.predict(roi) name = label_map.get(label, "Unknown") # confidence 小于阈值才认为是本人,否则标记为陌生 if confidence < 80: cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2) cv2.putText(frame, name, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) else: cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 0, 255), 2) cv2.putText(frame, "Unknown", (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2) cv2.imshow("Attendance", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()这段代码里最关键的是confidence的阈值判断。LBPH 识别器返回的 confidence 表示距离,数值越小代表越相似。但很多人踩坑在高估了“小于多少才算同一人”——这个值和训练数据、光照都相关,没有万能阈值。常见做法是在 50 到 90 之间试,把 80 当作初值。如果频繁把陌生人识别成员工,就往小调,比如 60;如果员工本人经常被拒识,就往大调,比如 90。
另外注意,这里的最小人脸尺寸设成了(100, 100),因为摄像头画面里人脸如果太小,识别特征根本不够,不如直接忽略。这也呼应了前面说的:识别率低首先要检查是不是框都没框住。
4.4 Functional_function.py:签到时间记录与迟到判断
Functional_function.py 处理的是业务逻辑,它把“识别成功”这个事件落成一条考勤记录。核心代码大致如下:
from datetime import datetime import csv import os WORK_START_TIME = "09:00:00" # 上班时间,可自行修改 def record_attendance(name): now = datetime.now() time_str = now.strftime("%H:%M:%S") date_str = now.strftime("%Y-%m-%d") # 判断是否迟到:用当前时间字符串直接和上班时间比较 status = "正常" if time_str <= WORK_START_TIME else "迟到" file_path = f"attendance_{date_str}.csv" is_new_file = not os.path.exists(file_path) with open(file_path, mode="a", newline="", encoding="utf-8") as f: writer = csv.writer(f) if is_new_file: writer.writerow(["姓名", "日期", "时间", "状态"]) writer.writerow([name, date_str, time_str, status]) print(f"{name} 签到成功 {time_str} 状态: {status}") return status这里的strftime是 datetime 模块的标准格式化方法,%H:%M:%S表示时:分:秒。判断迟到用时间字符串直接比较,在固定格式下是可行的,因为“09:00:00”到“23:59:59”的字典序和实际时间顺序一致,对课设来讲已经够用。真正落到生产环境,我会建议转成datetime.time对象再比较,避免跨天、24 小时制等边界问题。
新增员工时,如果你的界面是 PyQt5 的,按钮的 clicked 信号会触发一个槽函数,槽函数里先调 CamShow 的识别逻辑,拿到 name 后再调record_attendance(name)。这个调用链就是整个项目的主干:界面 → 摄像头识别 → 业务记录。
5. 常见问题与避坑手册:从依赖报错到界面卡死的排查实录
5.1 ModuleNotFoundError: No module named 'cv2'
现象:运行 MainWindow.py 或任何 import cv2 的文件,直接报错 ModuleNotFoundError: No module named 'cv2'。
原因:最常见的是装错了环境。很多人电脑里有 Anaconda、Python 3.7、Python 3.9 多个解释器,终端里敲 pip install 装的包,和 IDE 里用的解释器不是同一个。其次是只装了 opencv-python,但这个资源依赖 contrib 包的 face 模块,装错包也会在后续报属性缺失。
解决:先确认解释器再安装。如果你用 PyCharm,看右下角解释器路径;用命令行的话,执行python -m pip install opencv-contrib-python,用-m指定当前 python 环境安装,而不是直接敲 pip。装完后跑python -c "import cv2; print(cv2.__version__)"验证。
5.2 AttributeError: module 'cv2' has no attribute 'face'
现象:代码走到cv2.face.LBPHFaceRecognizer_create()这一行时报错,说 face 不是 cv2 的属性。
原因:这是 opencv-python 和 opencv-contrib-python 的经典区别。face 模块属于 contrib 扩展包,标准版 opencv-python 里没有。你虽然能看到 cv2 正常导入,但里面缺这个子模块。
解决:pip uninstall opencv-python先卸掉,再pip install opencv-contrib-python重装。两个包不能共存,安装 opencv-contrib-python 之后,cv2.face就能导入了。这是这个资源最值得提前知道的坑,README 里如果没有特别写明,几乎每个人都会踩一次。
5.3 摄像头画面黑屏或 isOpened() 返回 False
现象:PyQt5 界面能弹出来,但摄像头区域一片黑,或者终端打印摄像头打开失败。
原因:三个高频原因。第一,VideoCapture(0) 里的索引 0 不对,你机器上摄像头是索引 1;第二,摄像头正被其他软件占用,比如浏览器会议、直播软件;第三,虚拟机或远程桌面环境没有把物理摄像头映射进去。
解决:写一个循环依次尝试 0、1、2 三个索引,哪个能打开就用哪个。排查占用时把浏览器、会议软件全部关掉,再重新运行。如果是虚拟机,需要在虚拟机设置里把 USB 摄像头连接过去。这里最容易忽略的细节是程序退出后摄像头没释放,导致下一次运行打不开,所以代码里记得写cap.release(),或者在 PyQt5 里设置窗口关闭事件时释放。
5.4 cv2.error 报错出现在 detectMultiScale 附近
现象:运行时出现一大段 cv2.error,里面能看到类似 OpenCV(4.4.0) 的版本字样,报错位置在 detectMultiScale 调用处。
原因:最常见是传入的灰度图是空的,即gray是 None。这往往不是算法问题,而是imread读取图片时路径错了,OpenCV 读不到文件时不会抛异常,而是静默返回 None,直到后续逻辑报错你才看到崩溃。Windows 下还会出现路径分隔符反斜杠转义的问题。
解决:所有cv2.imread后面立即判断if img is None并打印当前尝试的绝对路径。路径拼接尽量用os.path.join(),别手写带反斜杠的字符串。这招在处理这个资源里分散在各处的人脸图片路径时极其有用。
5.5 PyQt5 界面卡死:一进识别界面窗口就无响应
现象:点签到按钮后,窗口标题变成“未响应”,转圈,过几秒才恢复,甚至直接崩掉。
原因:人脸检测和识别是耗时操作,如果直接写在 PyQt5 的信号槽函数里,主线程被阻塞,界面就卡死了。摄像头循环如果放在主线程里while True地读帧,GUI 事件循环根本没有机会处理界面刷新。
解决:把摄像头和识别逻辑放进单独的 QThread,或者用 Python 的 threading 模块开一个工作线程,主线程只负责接收识别结果并刷新界面标签。常见的做法是自定义一个继承 QThread 的类,在 run() 里做 VideoCapture 循环,通过信号把识别结果传回主线程。这个改造不算复杂,但对这种带摄像头实时流的 PyQt5 应用来说是必须的一步。如果原始代码没做线程处理,我建议你拿到后优先补上。
5.6 LBPH 识别置信度波动大,同一人时好时坏
现象:同一个人站在摄像头前,一会识别成自己,一会识别成 Unknown,甚至识别成别人。
原因:光照变化是最大变量。摄像头和注册照片的光照不一致时,LBPH 的特征直方图差异会变大。其次是注册照片太少、角度单一,模型没学够特征。第三个原因是阈值设得过于激进,80 到 90 之间对环境影响非常敏感。
解决:提高注册样本的多样性,至少 10 张以上,包含左右微侧脸和不同光线;识别场景的光照和注册场景保持一致;把阈值从 80 调到 60 到 100 之间做对比测试。我做课设验收时一般会录 15 张照片,然后让测试人现场站三个位置、开灯关灯各测一次,取一个在多数场景下都能通过又不误认的阈值。
6. 进阶验证与改造:把置信度调成可见参数,做增量训练和自测集
6.1 用三类测试对象验证系统可靠性
判断系统能不能交付,不要只靠“能识别我自己”。我习惯组织三组测试:第一组是已注册人员,正常光线正面站立,要求识别成功率在九成以上;第二组是已注册人员但戴眼镜、换发型,观察识别率掉到多少;第三组是未注册人员,看他是否被误认为某个已注册姓名。第三组尤其重要,因为误识别在考勤场景里比拒识更致命,意味着系统把路人当成员工并签到了。每组测 20 次,统计通过率、拒识率和误识率,拿数据做验收。
6.2 把置信度阈值暴露到 PyQt5 界面
原代码里阈值写在逻辑中,每次调优都要改代码重新跑。你可以花十分钟把它变成一个界面参数:在 QtUI.py 里加一个 QSlider 或 QSpinBox,默认值 80,范围 50 到 120,信号连接到一个全局变量或类的属性。识别循环每帧读取这个值作为判断条件。这样做的好处是现场调试不用重启程序,也能在答辩时展示“参数对识别效果的影响”,是个加分项。
6.3 利用 LBPH 的 update() 做增量训练,而不是全部重训
前面说每加一个人要重新跑 face_model.py,对于几十人规模的课设没问题。但如果想做得更好看一点,可以改用增量训练:新员工录好人脸照片后,用 recognizer 的update()方法,把新样本融合进现有模型,而不是重新训练所有人。
new_faces = [cv2.imread(path, cv2.IMREAD_GRAYSCALE) for path in new_files] new_labels = np.array([next_label] * len(new_faces)) recognizer.update(new_faces, new_labels) recognizer.write("data/face_model.yml")这个做法的实际收益是训练耗时不会随人数线性增长,但在答辩演示时,更重要的是展示你理解了模型保存与读档的机制,而不是每次启动都从头训练。模型文件用 yml 保存,部署到另一台机器时记得把 face_model.yml 和 data 目录一起拷走,否则新机器上会提示找不到模型文件。
从那以后,我每次拿到这种开源的考勤项目,都强制自己先看一遍pycache里能看出哪个 Python 版本跑过、requirements 里写的是标准包还是 contrib 包、摄像头索引是不是写死成 0,再决定怎么装环境、怎么调第一轮参数。这套顺序帮我省掉了大量重复踩坑的时间,希望帮到你。
本文还有配套的精品资源,点击获取