OpenCV人脸识别考勤系统实战:从LBPH训练到GUI签到与报表导出
简介这是一套基于OpenCV人脸识别的考勤签到系统Python项目源码面向计算机相关专业正在做毕业设计、课程设计或大作业的学生以及需要项目实战练习的学习者。项目经导师指导并认可通过评审分98分代码功能经验证可稳定运行适合作为入门进阶或初期项目立项演示。压缩包共43个文件约685KB包含12个py源码文件、16个pyc编译文件、10个xml分类器文件、2个ui界面文件及txt依赖说明等涵盖人脸检测、摄像头调用、签到上传、功能函数与主窗口等模块并配有详细注释和GUI界面。资源已有254人学习下载读者可据此掌握OpenCV人脸识别与PyQt界面开发的完整实现思路理解Haar级联分类器的调用方式、签到数据上传流程与项目目录组织也可在此基础上进行二次开发拓展为更完整的考勤管理方案。1. 从一份 zip 说起OpenCV 人脸识别考勤系统到底能跑出什么效果很多人第一次接触「基于 OpenCV 人脸识别的考勤签到系统」是在找毕设题目的阶段。压缩包里通常包含 Python 源码、逐行注释和一个能点开的 GUI 界面听起来像是把摄像头打开、框出人脸、比对成功就记一笔考勤这么简单。真动手跑起来才会发现难点根本不在「识别」两个字而在人脸数据怎么采、特征怎么存、界面和识别线程怎么不打架、识别错了怎么补救。这套系统真正解决的是用一台普通摄像头加一台电脑把「谁来了、几点来的」这件事自动化记录下来并且能导出成表格。它适合两类人——一类是刚学完 Python 基础、想找一个能写进简历或毕设的完整项目练手的人另一类是想在办公室、实验室、小型场地做一套低成本签到工具的人。下面我按自己实际搭过一遍的顺序把选型、代码、参数和踩过的坑讲清楚你照着能复现也能判断这套方案值不值得投入。2. 技术选型为什么是 OpenCV LBPH而不是深度学习那一套2.1 三种主流人脸识别路线的取舍做考勤签到核心诉求是「本地能跑、不依赖网络、识别速度够快、样本量小也能用」。围绕这个诉求常见有三条路线。第一条是 OpenCV 自带的 Haar 级联检测 LBPH 识别器。Haar 负责在画面里框出人脸LBPHLocal Binary Patterns Histograms局部二值模式直方图负责把这张脸转成一个特征向量并比对。它的优点是纯 CPU 就能跑装完opencv-contrib-python就能用每个人只要几张到几十张照片就能训练训练一次几秒钟。缺点是光照变化大、侧脸角度大时准确率会掉。第二条是深度学习方案比如用 face_recognition 库底层是 dlib或者 InsightFace。准确率明显更高尤其是侧脸和暗光场景但 dlib 在 Windows 上编译经常翻车InsightFace 要下模型权重、要 GPU 才跑得舒服对毕设这种「老师要在自己电脑上验收」的场景不友好。第三条是调用云端人脸识别 API。识别准但要联网、要计费、有隐私顾虑考勤这种天天用的场景不适合。我一般会选第一条路线做毕设和轻量落地原因是可复现性最高。下面这张表是我实际对比后的结论参数是常见配置下的量级不是精确基准。方案依赖单人样本需求CPU 单帧耗时典型准确率适合场景Haar LBPHopencv-contrib-python10~30 张30~80ms85%~93%毕设、小场地考勤dlib / face_recognitiondlib、编译工具链1~5 张100~300ms95%对准确率敏感InsightFaceonnxruntime、模型权重1~5 张GPU 下 10~30ms98%有 GPU 的生产环境提示如果只是做毕设答辩LBPH 的准确率完全够用把精力放在 GUI 交互和数据存储上比死磕识别率更容易拿分。2.2 环境搭建避开 ModuleNotFoundError 的第一个坑热词里modulenotfounderror: no module named opencv出现频率极高本质是装错了包。人脸识别要用的是opencv-contrib-python不是opencv-python因为 LBPH 识别器在 contrib 扩展模块里。两个包同时装还会互相覆盖这是新手最容易踩的坑。# 先卸载可能冲突的包避免两个 opencv 互相覆盖 pip uninstall opencv-python opencv-contrib-python -y # 只装 contrib 版本它已经包含基础功能 pip install opencv-contrib-python4.8.1.78 # 验证能打印版本且能创建 LBPH 识别器才算装对 python -c import cv2; print(cv2.__version__); print(hasattr(cv2, face))这段命令的逻辑是先清场再安装最后用hasattr(cv2, face)验证 contrib 模块是否真的可用。参数上版本号建议锁在 4.5 以上4.8.1.78 是我在 Windows 和 Linux 上都验证过比较稳的版本。如果打印出True说明cv2.face可用如果报AttributeError说明装成了普通版回去重装。GUI 部分用tkinter它是 Python 自带的不用额外装配合Pillow把 OpenCV 的帧显示到界面上pip install Pillowtkinter负责窗口、按钮、输入框Pillow负责把 OpenCV 的 BGR 数组转成 tkinter 能显示的图像。这个组合的好处是零额外依赖老师电脑上只要有 Python 就能跑。2.3 项目目录结构怎么组织一份能维护的源码目录要分清「采集、训练、识别、界面、数据」五块。我一般会这样组织你拿到别人的 zip 也可以按这个标准去判断它写得规不规整attendance_system/ ├── dataset/ # 每个人一个子文件夹存采集的人脸图 │ ├── zhangsan/ │ └── lisi/ ├── trainer/ │ └── trainer.yml # LBPH 训练出来的模型文件 ├── attendance/ # 考勤记录按日期存 CSV ├── capture_faces.py # 采集脚本 ├── train_model.py # 训练脚本 ├── recognize_gui.py # 主界面 识别 └── requirements.txtdataset下每个人一个文件夹文件夹名就是姓名这样训练时直接读文件夹名当标签不用额外维护一张姓名到 ID 的映射表省掉一类常见 bug。trainer.yml是 LBPH 训练后的模型体积很小几百 KB 级别。考勤记录按日期分文件方便按天导出。3. 人脸采集与训练把「认识一个人」这件事做扎实3.1 采集脚本为什么建议采 30 张而不是 3 张采集质量直接决定识别上限。很多人图快对着摄像头按三下就完事结果识别时稍微换个角度就认不出。LBPH 对样本数量和角度多样性比较敏感我的经验是每人 30 张左右覆盖正脸、轻微左右偏、戴不戴眼镜、明暗两种光照。import cv2 import os # 采集参数姓名从命令行传入避免写死 name input(请输入姓名拼音用于文件夹名: ).strip() save_dir os.path.join(dataset, name) os.makedirs(save_dir, exist_okTrue) # 加载 Haar 级联检测器路径用 cv2.data 自动定位避免硬编码 face_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) cap cv2.VideoCapture(0) # 0 表示默认摄像头 count 0 TARGET 30 # 每人采集目标张数 while count TARGET: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 检测在灰度图上做更快 faces face_cascade.detectMultiScale( gray, scaleFactor1.2, # 每次缩放比例越大越快但越容易漏 minNeighbors5, # 邻域阈值越大越严格误检越少 minSize(100, 100) # 最小人脸尺寸过滤远处小脸 ) for (x, y, w, h) in faces: count 1 face_img gray[y:yh, x:xw] face_img cv2.resize(face_img, (200, 200)) # 统一尺寸训练必需 cv2.imwrite(os.path.join(save_dir, f{count}.jpg), face_img) cv2.rectangle(frame, (x, y), (xw, yh), (0, 255, 0), 2) cv2.imshow(capture, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows() print(f采集完成共 {count} 张保存在 {save_dir})逻辑说明先建目录再逐帧检测人脸检测到就裁剪、缩放、保存。scaleFactor和minNeighbors是 Haar 检测最关键的两个参数前者控制多尺度搜索的步长后者控制误检。参数上scaleFactor1.2是速度和召回的平衡点调到 1.1 更全但更慢调到 1.4 快但容易漏脸minNeighbors5是常用默认误检多就往上调到 6~8。minSize设成 100×100 能过滤掉背景里的小脸减少误采。注意采集时人脸要占画面足够大别站太远。裁剪出来的图统一缩放到 200×200这个尺寸是 LBPH 的常见输入太大没必要太小丢细节。3.2 训练脚本LBPH 训练到底在算什么LBPH 的原理是把人脸图分成若干小格每格计算局部二值模式直方图再把这些直方图拼起来作为这张脸的特征。训练阶段其实就是把每个人的多张图都转成直方图特征存起来识别时算待测脸和每个已存特征的直方图距离取最近的作为结果。import cv2 import numpy as np import os recognizer cv2.face.LBPHFaceRecognizer_create( radius1, # LBP 圆形邻域半径 neighbors8, # 邻域采样点数 grid_x8, # 水平方向分格数 grid_y8 # 垂直方向分格数 ) faces [] labels [] label_map {} # 姓名 - 数字标签 current_id 0 dataset_path dataset for person_name in os.listdir(dataset_path): person_dir os.path.join(dataset_path, person_name) if not os.path.isdir(person_dir): continue label_map[current_id] person_name 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 None: continue faces.append(img) labels.append(current_id) current_id 1 recognizer.train(faces, np.array(labels)) os.makedirs(trainer, exist_okTrue) recognizer.save(trainer/trainer.yml) # 保存标签映射识别时要用 import json with open(trainer/labels.json, w, encodingutf-8) as f: json.dump(label_map, f, ensure_asciiFalse) print(f训练完成共 {len(label_map)} 人{len(faces)} 张图)逻辑说明遍历dataset下每个子文件夹文件夹名当姓名分配一个数字标签把所有灰度图读进来训练最后保存模型和标签映射。参数上radius和neighbors控制 LBP 算子的粒度默认 1 和 8 就够用grid_x、grid_y是分格数8×8 是经典配置分得越细对局部纹理越敏感但也越容易受噪声影响。labels.json这一步很多人会漏导致识别时只能输出数字标签没法显示姓名这是血泪经验。3.3 识别阈值confidence 到底设多少才不误判LBPH 的predict返回两个值标签和 confidence。注意这里的 confidence 是「距离」越小越像和直觉相反。常见做法是设一个阈值超过就判为陌生人。label, confidence recognizer.predict(gray_face) if confidence 70: name label_map.get(str(label), 未知) else: name 陌生人阈值怎么定没有万能值。我的经验是同一个人正脸通常 confidence 在 30~60光照差或角度偏会到 60~80不同人之间一般大于 80。所以 70 左右是个起点实际用的时候拿几个真人测一遍把「本人最高分」和「他人最低分」之间取中间值。如果两者重叠说明样本质量不够回去补采。提示不要迷信单一阈值。可以在界面上加一个「置信度」显示方便调试时观察正式用再隐藏。4. GUI 界面与识别线程让摄像头和按钮不打架4.1 tkinter 布局一个能用的考勤界面长什么样界面不用花哨够用就行。核心区域三块左边实时画面右上考勤信息右下操作按钮。用tkinter的Frame分区Label显示画面Button触发采集、训练、开始签到。import tkinter as tk from tkinter import messagebox from PIL import Image, ImageTk import cv2 import json import csv import os from datetime import datetime class AttendanceApp: def __init__(self, root): self.root root self.root.title(人脸识别考勤签到系统) self.root.geometry(900x600) # 左侧视频区 self.video_label tk.Label(root, width640, height480, bgblack) self.video_label.place(x10, y10) # 右侧信息区 self.info_text tk.Text(root, width28, height20) self.info_text.place(x660, y10) # 按钮区 tk.Button(root, text开始签到, commandself.start_recognize).place(x660, y400) tk.Button(root, text停止, commandself.stop_recognize).place(x760, y400) tk.Button(root, text导出考勤, commandself.export_csv).place(x660, y450) self.cap None self.running False self.recognizer cv2.face.LBPHFaceRecognizer_create() self.recognizer.read(trainer/trainer.yml) with open(trainer/labels.json, encodingutf-8) as f: self.label_map json.load(f) self.marked_today set() # 当天已签到的人避免重复记录 def start_recognize(self): self.cap cv2.VideoCapture(0) self.running True self.update_frame() def stop_recognize(self): self.running False if self.cap: self.cap.release() def update_frame(self): if not self.running: return ret, frame self.cap.read() if ret: # 识别逻辑见下一节 frame self.process_frame(frame) img cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) img Image.fromarray(img) imgtk ImageTk.PhotoImage(imageimg) self.video_label.imgtk imgtk self.video_label.configure(imageimgtk) self.root.after(30, self.update_frame) # 约 33fps 刷新 def export_csv(self): # 导出逻辑略按日期读 attendance 下的 csv 汇总 messagebox.showinfo(提示, 考勤已导出)逻辑说明update_frame用root.after做定时刷新而不是while死循环这是 tkinter 的关键——死循环会卡住主线程界面直接假死。after(30, ...)表示 30 毫秒后再调一次自己约 33fps够流畅也不吃满 CPU。marked_today用集合去重保证一个人当天只记一次。4.2 识别与签到写盘CSV 追加和去重识别到人之后要写考勤记录。用 CSV 追加按日期分文件字段就四个姓名、日期、时间、置信度。def process_frame(self, frame): gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces self.face_cascade.detectMultiScale(gray, 1.2, 5, minSize(100, 100)) for (x, y, w, h) in faces: face cv2.resize(gray[y:yh, x:xw], (200, 200)) label, confidence self.recognizer.predict(face) if confidence 70: name self.label_map.get(str(label), 未知) color (0, 255, 0) if name not in self.marked_today: self.marked_today.add(name) self.write_attendance(name, confidence) self.info_text.insert(tk.END, f{name} 签到成功\n) else: name 陌生人 color (0, 0, 255) cv2.rectangle(frame, (x, y), (xw, yh), color, 2) cv2.putText(frame, name, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, color, 2) return frame def write_attendance(self, name, confidence): today datetime.now().strftime(%Y-%m-%d) os.makedirs(attendance, exist_okTrue) path fattendance/{today}.csv is_new not os.path.exists(path) with open(path, a, newline, encodingutf-8-sig) as f: writer csv.writer(f) if is_new: writer.writerow([姓名, 日期, 时间, 置信度]) writer.writerow([name, today, datetime.now().strftime(%H:%M:%S), round(confidence, 2)])逻辑说明process_frame负责检测、识别、画框、写盘。write_attendance用utf-8-sig编码这样 Excel 打开中文不乱码这是很多人导出后看到乱码的原因。is_new判断文件是否存在不存在才写表头避免表头重复。注意写文件放在识别循环里如果一秒识别多帧会频繁开文件。用marked_today去重后同一个人当天只写一次压力很小。如果要做多人同时签到把写盘改成队列异步处理更稳。5. 避坑与排查那些让系统「看起来能跑但实际不能用」的问题5.1 摄像头打不开或画面卡死现象点「开始签到」后界面黑屏或者画面卡住不动控制台没有报错。原因cv2.VideoCapture(0)的索引不对或者摄像头被其他程序占用比如微信、钉钉在后台开着摄像头。另一个常见原因是把cap.read()放在了 tkinter 主线程的死循环里界面直接假死。解决先换索引试VideoCapture(1)再关掉其他占用摄像头的程序。刷新逻辑一定用root.after不要用while True。如果还是黑屏单独写个最小脚本cv2.imshow测一下摄像头本身是否正常把问题范围缩小。5.2 识别总是认成同一个人现象不管谁坐到摄像头前都识别成数据集里第一个人。原因训练时标签分配错了或者labels.json和模型对不上。常见于先训练了一次又加了新人重新训练但labels.json没更新导致标签错位。解决每次训练都重新生成labels.json保证模型和映射同源。训练脚本里current_id从 0 开始按os.listdir顺序分配os.listdir的顺序在不同系统上可能不一致稳妥做法是先sorted(os.listdir(...))固定顺序。5.3 光照一变就认不出现象白天采集的样本晚上或者背光时识别率骤降。原因LBPH 虽然比直接像素比对抗光照但极端光照下 Haar 检测本身就框不准裁剪出来的人脸区域偏移特征自然对不上。解决采集时就有意识地覆盖明暗两种光照每种至少 10 张。识别端可以在检测前做一次直方图均衡化cv2.equalizeHist(gray)能明显改善暗光下的检测稳定性。如果场地光照实在差加一个补光灯比调算法管用。5.4 中文路径或中文姓名导致读写失败现象cv2.imread返回 None或者 CSV 打开乱码。原因OpenCV 的imread在部分版本对中文路径支持不好直接返回 None 而不报错。CSV 乱码则是编码问题。解决图片路径尽量用英文和拼音姓名用拼音做文件夹名显示时再映射成中文。CSV 统一用utf-8-sig。如果非要读中文路径用np.fromfile加cv2.imdecode绕过。5.5 打包成 exe 后找不到模型文件现象源码里跑得好好的用 PyInstaller 打包后一运行就报找不到trainer.yml。原因打包后工作目录变了相对路径失效。解决用sys._MEIPASS判断是否在打包环境把模型路径拼成绝对路径。或者干脆把trainer和dataset目录放在 exe 同级用os.path.dirname(sys.executable)定位。这个坑在毕设答辩现场翻车率很高提前测一遍打包版本。6. 进阶技巧把签到记录做成能直接交差的报表系统能识别只是及格线真正让这套东西「能用」的是考勤数据的后处理。我一般会在导出环节加两个动作按人汇总和迟到判断。import pandas as pd from datetime import datetime def build_report(date_str, work_start09:00): df pd.read_csv(fattendance/{date_str}.csv, encodingutf-8-sig) # 每人当天可能有多条取最早一条作为签到时间 df[时间] pd.to_datetime(df[时间], format%H:%M:%S).dt.time first df.sort_values(时间).groupby(姓名).first().reset_index() start datetime.strptime(work_start, %H:%M).time() first[状态] first[时间].apply( lambda t: 正常 if t start else 迟到 ) first.to_csv(fattendance/{date_str}_report.csv, indexFalse, encodingutf-8-sig) return first逻辑说明先把当天所有签到记录读进来按姓名分组取最早时间再和上班时间比早于等于算正常晚于算迟到。参数work_start可以做成界面上的输入框不同场地上下班时间不一样。utf-8-sig保证 Excel 直接打开不乱码。再进一步可以把多天的 report 合并成一张月度表用pandas的pivot_table按人按天透视缺勤的格子标红。这一步做完这套系统就不只是「能识别」而是「能交差」——老师或主管要的就是一张能看的表而不是一个会框脸的窗口。我自己踩过最深的一个坑是早期版本把识别和写盘耦合在一个循环里人多的时候文件句柄开开关关跑一上午偶尔会丢记录。后来改成识别只往内存队列里塞单独一个线程负责落盘再没丢过。做这类小系统稳定性往往比识别率更影响体验。希望帮到你。本文还有配套的精品资源点击获取