
简介基于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) # scaleFactor1.1 检测更细minNeighbors5 减少误检 faces face_cascade.detectMultiScale(gray, scaleFactor1.1, minNeighbors5, minSize(50, 50)) for i, (x, y, w, h) in enumerate(faces): face img[y:yh, x:xw] # 裁剪人脸区域 save_path fdata/faces/user1_{i}.jpg cv2.imwrite(save_path, face) print(已保存:, save_path) print(f检测到 {len(faces)} 张人脸)逻辑不复杂但有一个非常重要的细节检测用的是灰度图保存用的是原图裁剪。检测阶段转灰度是为了加速裁剪阶段用原图是为了保留颜色信息。这里的x, y, w, h四元组就是检测框的坐标和尺寸img[y:yh, x:xw]是 numpy 数组的切片语法注意顺序是高在前宽在后写反了会截出一张转置过的脸。参数上如果发现照片里人脸没被检测到优先调大minSize或调小scaleFactor如果发现背景被当成脸调大minNeighbors。我一般会要求每个用户至少存 10 到 20 张而且覆盖正面、左右轻微转头的角度这比追求单张识别精准带来的收益大得多。4.2 face_model.pyLBPH 模型的训练与保存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 文件里只有标签数字没有姓名识别出 label1 你要自己去对应到“张三”。训练完成后根目录会多出一个 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, scaleFactor1.1, minNeighbors5, minSize(100, 100)) for (x, y, w, h) in faces: roi gray[y:yh, x:xw] # 识别也用灰度图 label, confidence recognizer.predict(roi) name label_map.get(label, Unknown) # confidence 小于阈值才认为是本人否则标记为陌生 if confidence 80: 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) else: cv2.rectangle(frame, (x, y), (xw, yh), (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 fattendance_{date_str}.csv is_new_file not os.path.exists(file_path) with open(file_path, modea, newline, encodingutf-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再决定怎么装环境、怎么调第一轮参数。这套顺序帮我省掉了大量重复踩坑的时间希望帮到你。本文还有配套的精品资源点击获取