
简介面向计算机相关专业学生与毕业设计、课程设计人群这份基于Python与OpenCV的人脸识别考勤签到系统是一套完整可运行的高分课程项目。系统以OpenCV人脸检测级联模型为底层视觉方案搭配PyQt5搭建图形操作界面覆盖摄像头实时采集、人脸比对识别、签到信息记录等核心流程代码分段附带注释经导师指导评审获得99分期末大作业与入门级实战练习者均可直接参考。压缩包共44个文件包含12个Python源码、10个XML分类器配置、2个Qt界面ui文件及说明文档整体仅690KB目录划分清晰便于快速部署运行。目前已有405人学习下载。从算法配置、界面布局到签到功能实现均有完整呈现既可学习OpenCV人脸识别的工程化落地方式也可直接作为课程设计成果提交或二次开发起点。1. 期末大作业选人脸识别考勤这套 OpenCV 方案为什么够用做考勤类课程设计很多人第一反应就是上百度人脸识别 SDK 或者直接搬深度模型结果环境配三天、训练跑一夜、答辩讲不清楚。这个基于 Python OpenCV PyQt5 的人脸识别考勤签到系统走的是完全相反的路子用 OpenCV 自带的 haar 级联检测人脸再结合 LBPH 特征训练识别模型界面用 PyQt5 拼出来整套代码从录入人脸、训练模型到点击签到全链路在本地就能闭环跑通。它不追新也不炫技但好在每一步都看得见、改得动、讲得明白特别适合期末大作业、课程设计这种既要交东西、又要接受老师提问的场景。评审能拿 99 分靠的不是模型多高级而是功能完整度——摄像头实时画面、人脸框选、签到记录写入、界面可操作这些都齐了。2. 源码结构与识别原理三个 xml 文件和一个 LBPH 模型如何分工拿到这份源码先别急着跑把目录结构看明白后面的排错效率能提高一倍。整个项目里有三类文件需要区分一类是 OpenCV 自带的级联分类器三个 xml 文件一类是 PyQt5 界面相关的 .py 和 .ui还有一类是核心业务逻辑负责录入、训练、签到。搞清楚这些文件各自干什么比你一行行读代码有用得多。2.1 三个 haar 级联分类器什么时候用哪个项目里有三个 xmlhaarcascade_frontalface_default.xml、haarcascade_frontalface_alt2.xml、haarcascade_eye_tree_eyeglasses.xml。这是 OpenCV 传统人脸检测方案里最经典的一组模型全部由 OpenCV 官方训练好直接 cv2.CascadeClassifier 加载就能用不需要任何额外依赖。haarcascade_frontalface_default.xml 是正面人脸检测的默认模型检测速度最快误检率中等适合实时摄像头场景。haarcascade_frontalface_alt2.xml 是它的改进版本特征集更完整对稍微偏转的人脸容忍度更高但检测速度会慢一点。代码里的常见做法是先加载 default如果检测不到再尝试 alt2两个模型做互补。eye_tree_eyeglasses.xml 是用来检测眼睛的专门优化过戴眼镜的情况。在考勤场景里它有一个很实用的玩法检测到人脸框之后再在框内检测眼睛如果眼睛存在说明这是一个真正的人脸而不是背景误检。这个验证步骤能明显减少摄像头前放一张照片就能签到的漏洞属于这个项目里比较聪明的细节。实际使用中还有个选型细节如果你们教室光线特别暗default 模型的漏检率会上升这时候把 minNeighbors 从 5 降到 3检出率能回来不少代价是误检框变多。如果你追求屏幕显示流畅优先用 default如果追求识别率优先用 alt2。两个模型都加载、先框先得也是我比较推荐的方式。2.2 为什么选 LBPH 而不是 EigenFace 或 FisherFaceOpenCV 的 face 模块里其实有三种经典识别器EigenFace、FisherFace、LBPH。这份代码用的是 LBPHLocal Binary Patterns Histograms局部二值模式直方图。为什么大作业要选它原因很实际。EigenFace 基于 PCA 降维把每张人脸图拉成向量找主成分方向。它的毛病是对光照极敏感同一张脸换个光源数值变化比换个人还大。FisherFace 基于 LDA需要每个类别每个人的样本分布比较均匀考勤场景里通常每个人就十张左右照片类别一多FisherFace 的投影矩阵就很容易不稳定。LBPH 的思路完全不同它先把图像转成 LBP 编码图每个像素和周围 8 个邻居比较大于记为 1、小于记为 0凑成二进制数。然后把人脸图像切成 grid_x × grid_y 个小块每块统计 256 维直方图拼成一个大的特征向量。识别时计算特征向量之间的距离距离越小越是同一个人。LBPH 对光照变化的鲁棒性明显好于 EigenFace对样本数量要求也低每个人三五张图就能训起来这正好是课程设计的资源条件。而且 LBPH 不需要像深度学习那样动辄上千张图做训练十几秒就能完成训练答辩现场演示的容错率极高。2.3 模块文件职责这套代码里哪些文件是核心把项目里二十几个文件理一遍按职责可以分成四组每组对应一条链路。文件职责是否核心MainWindow.py主窗口逻辑绑定按钮事件核心CamShow.py摄像头画面显示与帧刷新核心Camo_open.py摄像头资源打开与释放管理核心upload.py人脸录入采集人脸样本存图片核心face_model.py训练 LBPH 模型保存 yml 文件核心Functional_function.py签到判断、时间记录、防重复核心ui/CamShow.ui、ui/upload.uiQt Designer 原始界面稿辅助三个 haarcascade xml人脸/眼睛检测模型数据核心依赖requirements.txt依赖版本清单辅助README.md运行说明先读这个这里要提醒一下ui/ 目录里的 .ui 文件是 Qt Designer 的工程文件真正运行的是通过 pyuic 转换出来的 .py。如果你打算用 Qt Designer 改界面布局改完 .ui 必须重新生成 .py否则不生效。这个细节很多同学栽过跟头后面环境章节我会专门讲转换命令。MainWindow.py 和 Functional_function.py 的边界值得说一句MainWindow 只管界面事件比如按钮点击、显示摄像头画面真正的业务判断——今天是否已签到、当前置信度是否低于阈值、记录写进哪个文件——都在 Functional_function.py 里。这种「界面与业务分离」的结构是老师答辩时比较看重的点也是这份代码拿高分的一个原因。3. 环境搭建与首次运行让这份代码在你的电脑上先跑起来课程设计项目的通病是代码在作者的电脑上能跑换台机器就各种报错。这份代码我实际跑下来环境坑主要集中在一个点上cv2.face 模块找不到。下面把完整的搭建顺序和关键参数写清楚照着走基本二十分钟内能启动。3.1 环境准备与依赖安装先建一个干净的虚拟环境避免和你机器上其他 Python 项目打架。Python 版本建议用 3.8 或 3.9这个项目里有 3.7 和 3.9 两个版本的 pyc 缓存文件说明作者至少在两个版本上都跑通过但 3.9 的兼容性最稳。python -m venv face_env face_env\Scripts\activate pip install opencv-python opencv-contrib-python pyqt5 numpy安装成功后用一条命令验证核心依赖是否都能正常导入python -c import cv2, numpy, PyQt5; print(cv2.__version__)这里最关键的一点是LBPH 识别器在 OpenCV 里位于 cv2.face 子模块而这个子模块只有 opencv-contrib-python 才有。如果你只装了 opencv-python训练那一步会直接报module cv2 has no attribute face。如果你之前装过 opencv-python再装 contrib 时两个版本可能冲突建议先把旧的卸载干净再装。桌面端千万别装 opencv-python-headless那个版本不带 GUI 窗口能力摄像头画面弹不出来。requirements.txt 里应该锁了一些版本但实际验证下来只要保证 opencv-contrib-python 和 PyQt5 都是较新版本代码逻辑不受影响。PyQt5 相关的经典坑——labelme 无法安装 pyqt5——和这个项目无关这里直接用 pip 装官方包即可。3.2 先把摄像头链路打通CamShow.py 的关键参数整个系统的第一步不是训练模型而是确保摄像头能稳定出图。打开项目里的 CamShow.py核心代码逻辑是打开摄像头、循环读帧、显示到界面。import cv2 cap_id 0 # 0 表示笔记本内置摄像头外接 USB 摄像头通常改成 1 cap cv2.VideoCapture(cap_id) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) cap.set(cv2.CAP_PROP_FPS, 30) ret, frame cap.read() if not ret: print(摄像头读取失败检查 cap_id 或驱动) else: gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) print(摄像头正常画面尺寸:, frame.shape)这里有三点需要注意。cap_id 的取值macOS 上内置摄像头是 0Windows 笔记本内置摄像头是 0但如果你插了 USB 摄像头系统可能把内置的排到 1外接的占 0所以「摄像头打不开」时第一件事就是换 cap_id。分辨率参数640×480 是检测速度和人脸清晰度的平衡点用 1080p 增益很小但 CPU 占用翻倍用 320×240 则小尺寸人脸很难检测不建议再低。FPS 设置只是请求值最终实际帧率取决于硬件和检测耗时不要强求。读帧循环里还有一个容易被忽略的点每次执行 detectMultiScale 前都要把 BGR 转成灰度图因为 haar 级联分类器只接受单通道灰度输入。如果直接传彩色图不会报错但检测结果全空。3.3 用 Qt Designer 改了界面后如何同步项目保留了 .ui 原始文件这是为了让你能在 Qt Designer 里可视化地改界面。ui 文件有两个CamShow.ui 对应摄像头画面窗口upload.ui 对应录入窗口。如果你想改按钮位置、加一个显示签到的 label正确流程是在 Qt Designer 里改完保存然后用命令行把 .ui 转成 .py。pyuic5 -x ui/CamShow.ui -o CamShow.py pyuic5 -x ui/upload.ui -o upload.pypyuic5 命令在虚拟环境的 Scripts 目录下如果你激活了虚拟环境系统 PATH 里会自动带上。执行完后覆盖掉原 .py 文件重新运行主程序即可。这里最基础的坑是改完 .ui 但不重新生成 .py运行界面纹丝不动以及直接手改 .py 而不回改 .ui下次一旦重新生成手工修改就全丢了。我的习惯是界面布局只改 .ui业务逻辑只改 .py两层不混着动。4. 核心链路拆解录入、训练、签到三步之间到底怎么配合整个系统的运行链路可以浓缩成三个动作录入人脸样本、训练模型、签到识别。这个项目的代码也是按这个顺序组织的upload.py 负责录入face_model.py 负责训练Functional_function.py 负责签到判断。下面把每条链路的逻辑和关键参数讲透这也是你答辩时被问得最多的地方。4.1 录入人脸upload.py 在采集什么录入模块的目的是采集某个人的人脸样本保存到 data 目录下对应的子文件夹里。这一步骤的质量直接决定后面识别效果比训练参数本身影响更大。import cv2 import os def collect_face(name, save_rootdata, target_count30): cap cv2.VideoCapture(0) face_cascade cv2.CascadeClassifier( haarcascade_frontalface_default.xml) save_dir os.path.join(save_root, name) os.makedirs(save_dir, exist_okTrue) count 0 while count target_count: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale( gray, scaleFactor1.1, minNeighbors5, minSize(80, 80)) for (x, y, w, h) in faces: face gray[y:y h, x:x w] face cv2.resize(face, (200, 200)) cv2.imwrite(f{save_dir}/{name}_{count}.jpg, face) count 1 cap.release()录入时检测人脸用的是 detectMultiScale它的三个参数直接影响采集质量。scaleFactor1.1 表示每次搜索窗口缩放 10%这个值越小检测越精细但速度越慢小于 1.05 时一帧可能要跑上百毫秒。minNeighbors5 表示候选矩形周围至少要凑齐 5 个相邻矩形才确认是人脸这个值调大可以减少误检但也会漏掉侧脸。minSize(80,80) 表示小于 80×80 像素的候选框直接丢弃既过滤掉远处的小脸误检也保证裁剪出来的人脸有足够像素做训练。一个非常现实的采样建议不要站在原地拍 30 张一模一样的照片那样训练出来的模型对角度和光线没有泛化能力。采集时左右转转头、靠近一点再远离一点、换个光源方向让 30 张样本覆盖一些变化识别阶段会省很多事。另外保存的图片要统一成灰度图LBPH 用的就是灰度特征这里提前转灰度能避免后面训练代码出错。4.2 训练模型face_model.py 里 LBPH 的参数含义采集完成后face_model.py 负责把所有样本读出来训练 LBPH 识别器并把模型保存为 yml 文件。这个 yml 文件就是后续签到的「底库」里面存的是每张人脸的特征直方图。import cv2 import numpy as np import os def load_data(data_dirdata): images, labels [], [] label_id 0 for name in os.listdir(data_dir): person_dir os.path.join(data_dir, name) if not os.path.isdir(person_dir): continue for img_name in os.listdir(person_dir): img_path os.path.join(person_dir, img_name) img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) if img is not None: images.append(img) labels.append(label_id) label_id 1 return images, np.array(labels) recognizer cv2.face.LBPHFaceRecognizer_create() recognizer.setRadius(1) recognizer.setNeighbors(8) recognizer.setGridX(8) recognizer.setGridY(8) images, labels load_data(data) recognizer.train(images, labels) recognizer.save(face_model.yml)LBPH 的四个核心参数答辩时老师大概率会追问。setRadius(1)LBP 算子的采样半径半径为 1 就是看周围 3×3 邻域里的 8 个像素点半径太大特征太粗太小对噪声敏感。setNeighbors(8)采样点数8 是常用默认值对应 8 位二进制编码直方图维度是 2 的 8 次方等于 256。setGridX(8) 和 setGridY(8)把人脸切成 8×864 个小块每块分别提取直方图再拼接这样能保留一定的空间信息不至于把整张脸混成一个直方图。你可以试一下把 grid 改成 4×4识别率会下降因为空间细节丢了改成 16×16 也不一定更好因为小块面积太小直方图统计不稳定。4.3 签到判断Functional_function.py 的置信度阈值逻辑模型训练好之后签到就变成三件事摄像头实时取帧、检测人脸、调用 predict 判断是哪一个 label。def check_in(frame, recognizer, id2name, threshold90): gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale(gray, 1.1, 5) if len(faces) 0: return None, 未检测到人脸 (x, y, w, h) faces[0] face cv2.resize(gray[y:yh, x:xw], (200, 200)) label, confidence recognizer.predict(face) if confidence threshold: name id2name[label] return name, f签到成功置信度 {confidence:.1f} else: return None, f未匹配置信度过高 {confidence:.1f}这里最关键的是理解 OpenCV 对 confidence 的定义。predict 返回的 confidence 是一个距离值数值越接近 0 表示越相似数值越大表示越不像。threshold 取多少没有固定答案它受训练样本数量、光照条件、摄像头分辨率共同影响。我一般会拿 5 个不同的人各测 3 次看同一人的 confidence 通常落在哪个区间、不同人的落在哪个区间取两者的分界点再加 10 到 20 的余量作为 threshold。实训室环境下 90 左右是比较常见的起调值但一定要实测。这个模块还承担了防重复签到的功能。学生刷一次脸后程序把学号或姓名记录到当天打卡列表里第二次识别到同一人时先查列表命中就直接提示「今日已签到」。这个防重复逻辑如果漏了考勤记录会被同一张脸刷出几十条整套系统就失去意义了。5. 常见问题与排查最容易翻车的五个坑及修复记录这套系统我前后跑了三遍第一遍在环境上卡住第二遍在摄像头链路上卡住第三遍才把识别调稳。下面五条是踩坑频率最高的每一条都按现象、原因、解决三段写清楚遇到问题可以直接对照。5.1 报错 cv2.face 不存在现象运行 face_model.py 到cv2.face.LBPHFaceRecognizer_create()这行报AttributeError: module cv2 has no attribute face。原因你只安装了 opencv-python这个发行版是 OpenCV 的主模块不包含 face 子模块。LBPH、EigenFace、FisherFace 全部放在了 opencv-contrib-python 里。两个包都装的时候后装的会把先装的部分模块覆盖掉导致 face 属性丢失。解决先卸载再重装确保 final 状态只有 opencv-contrib-python。pip uninstall opencv-python opencv-contrib-python -y pip install opencv-contrib-python python -c import cv2; print(cv2.face.LBPHFaceRecognizer_create())5.2 摄像头黑屏或整个程序崩溃现象点击「开始签到」后界面弹出来但画面全黑或者程序直接无响应退出。原因最常见的是摄像头索引不对——笔记本配备的多个摄像头内置、USB 外接在系统里各有编号代码写死 0 但你的外接摄像头占的是 1。另一个原因是上次运行没释放摄像头资源摄像头被上一个进程占着新进程拿不到数据流。解决先把摄像头索引改成可配置参数逐个试。同时保证程序退出时调用 cap.release()。更稳妥的做法是注入 try/finally 结构无论识别过程是否报错都释放摄像头。如果改完索引依然黑屏检查 Windows 隐私设置里是否允许应用访问摄像头这个和代码无关但极易误判。5.3 中文姓名路径导致训练失败现象录入时用中文姓名建文件夹训练时发现 images 数组为空或者报图片读取错误。原因cv2.imread 对中文路径支持不好路径里有中文会返回 None。录入时保存的路径如果包含「张伟」这类中文目录名读出来全是 None训练循环直接跳过。解决不能用 cv2.imread 读这种路径改用 np.fromfile 配合 cv2.imdecode。这两步是固定的搭配先读二进制再解码成图像。import numpy as np import cv2 def read_cn_img(path): data np.fromfile(path, dtypenp.uint8) return cv2.imdecode(data, cv2.IMREAD_GRAYSCALE)除了图片模型保存路径也尽量用英文。训练好的 yml 文件如果保存到中文路径下load 时也可能出问题这个比读图片更隐蔽。5.4 界面卡死、窗口无响应现象开启摄像头画面后界面上的按钮点了没反应拖动窗口像拖果冻过一会直接提示「未响应」。原因视频读取和界面刷新抢了同一个线程。PyQt5 的界面事件循环和 while 循环如果在一个线程里读帧操作会把界面线程堵死。解决标准做法是把视频采集放到 QThread 子线程里子线程每读一帧通过信号槽把画面传给主线程 UI。信号槽机制本质是事件投递主线程空闲时处理不会因为忙于读帧而卡死。这是 PyQt5 界面开发必须掌握的架构方式也是这份代码里最值得学的工程经验。5.5 模型把所有人都识别成同一个人现象不同学生轮流刷脸识别结果永远返回同一个名字但 confidence 数值还挺低。原因基本可以断定是训练样本出了问题。最常见的情况是录入阶段人脸框没框稳sample 里混了大量背景图这些背景图被统一收进第一个 label模型就把所有人脸都往第一个 label 上靠。另一个可能原因是训练时 label 编号错乱多个人的照片被赋了同一个 label。解决打开 data 目录人工抽查每个子文件夹里的图片如果三分之一的图都不含清晰人脸重新录入。录入时可以在屏幕上实时显示当前帧的检测框确认框压住脸再存图。训练代码里检查 label 顺序确保每进入一个人的目录 label_id 才加 1而不是每张图都加 1。6. 从「能跑」到「稳签」连续帧投票让识别结果变得可信系统跑通只是第一步真正阻碍它落地的是偶发误判。摄像头前晃动一下、光照闪了一下、两个人侧脸角度相似都可能让 predict 返回一个错误的低置信度结果。这个问题单靠调阈值很难根治因为误判的 confidence 有时和正确结果处于同一个区间。我一般会加一个多帧投票机制不拿单帧结果直接判定而是连续取 5 帧识别结果做统计超过半数指向同一个 label 才算签到成功。vote_dict {} vote_target 5 def judge_with_vote(label, confidence, threshold90): if confidence threshold: vote_dict.clear() return None vote_dict[label] vote_dict.get(label, 0) 1 if vote_dict[label] vote_target: vote_dict.clear() return label return None逻辑解释每一帧识别出一个候选 label 后只记录、不决定。同一个 label 被连续命中 3 帧时就失败退出命中 5 帧才确认签到成功并写入记录。因为相邻帧间隔只有几十毫秒5 帧总耗时不到 0.2 秒几乎无感但晃头、遮挡、光照闪动造成的偶发误判基本都会被过滤掉。这个思路后来我在做真实门禁设备时也沿用下来了手动调阈值永远不如从时间维度上做确认稳。签到这个场景还有一个特别值得加的细节签到成功瞬间把界面上的按钮置灰、状态栏显示「已签到」防止学生反复刷脸刷出多条记录。防重复逻辑往前端挪一层比只在后端查历史记录响应更快体验也更好。这个项目我带过的几个学生照着跑从环境搭建到能演示签到普遍两小时内搞定。真正拉开差距的是后续调优的耐心——录入时多变换角度、多测几轮阈值、加上投票机制这三步做完这套系统完全可以拿到公司门口做真实考勤。我后来在项目里处理摄像头抓拍误报问题回头再去读这份大作业代码才意识到当年那些「设计感」其实是导师一句一句逼出来的工程习惯。从那以后我每次给识别类功能搭骨架都会强制走完一遍「输入源 → 预处理 → 特征提取 → 判定 → 落库」的链路再回头看界面代码顺序一旦颠倒排错范围就收不回来了。希望帮到你。本文还有配套的精品资源点击获取