资讯详情

Python+OpenCV自建人脸识别考勤签到系统:从LBPH到PyQt5实战

📅 2026/9/28 14:34:11 | 华诺云谱 👁 阅读
Python+OpenCV自建人脸识别考勤签到系统:从LBPH到PyQt5实战
简介基于Python与OpenCV的人脸识别考勤签到系统源码完整包含可运行的PyQt5图形界面与逐模块中文代码注释定位为高校计算机专业的课程设计、期末大作业及毕业设计参考项目适合已有Python基础、希望快速掌握人脸识别应用开发的初学者或实战学习者。压缩包共44个文件12个py源码文件覆盖主窗口、摄像头调用、人脸建模、功能函数等核心模块2个ui界面文件对应可视化交互界面10个xml配置及级联分类器文件支撑人脸与眼部检测整体仅690KB工程结构清晰、便于按模块阅读。已有405人学习浏览。该项目经导师指导并以99分评审通过代码完整可运行内置haarcascade人脸检测模型与签到上传完整流程可帮助理解OpenCV人脸识别从检测、建模到识别签到的全链路思路也适合作为答辩演示和毕业设计功能展示。1. 人脸识别门禁机已经到处是为什么还要自己写一套PythonOpenCV考勤签到系统人脸识别门禁机单价早就被打下来了但真到了小团队内部考勤它反而不好用打卡记录存在设备里导出格式各家不一样想跟工资表、值班表联动还得手工倒腾更别提有些场景只需要“人脸签到”不需要“开门禁”。用PythonOpenCV在普通电脑上自建一套人脸识别考勤签到系统成本几乎为零逻辑完全透明识别阈值、判重规则、报表格式都能自己定。这篇文章就把我常用的工程方案拆开讲算法怎么选、PyQt5界面怎么和摄像头协作、签到记录怎么落地以及那些不做不知道的坑。2. 选对人脸识别算法再动手OpenCV里三种方案怎么取舍2.1 检测和识别是两件事先别混在一起很多人第一次接触OpenCV人脸相关功能都是被cv2.CascadeClassifier的detectMultiScale带进门的。这个Haar级联检测器很经典但它解决的是“人脸在哪”的问题也就是检测不是“这个人是谁”的问题。考勤系统真正要的是后者即识别或认证。我的建议是老项目里还在用Haar级联的可以直接换掉。原因很直接——Haar在侧脸、低头、暗光下的漏检率偏高考勤场景里用户不会每次都正对摄像头。OpenCV官方后来主推的DNN人脸检测器基于ResNet-10 SSD输入一张图直接输出人脸框和置信度速度在CPU上也能跑到每帧几十毫秒准确率比Haar高一个档次。import cv2 import numpy as np def load_face_detector(proto_path, model_path, conf_threshold0.7): # proto_path 是 deploy.prototxtmodel_path 是 res10_300x300_ssd_iter_140000.caffemodel net cv2.dnn.readNetFromCaffe(proto_path, model_path) return net, conf_threshold def detect_faces(net, frame, conf_threshold0.7): h, w frame.shape[:2] # 输入归一化到 300x300均值是 OpenCV 官方模型训练时用的 BGR 均值 blob cv2.dnn.blobFromImage(frame, 1.0, (300, 300), (104.0, 177.0, 123.0)) net.setInput(blob) detections net.forward() faces [] for i in range(detections.shape[2]): confidence detections[0, 0, i, 2] if confidence conf_threshold: box detections[0, 0, i, 3:7] * np.array([w, h, w, h]) x1, y1, x2, y2 box.astype(int) faces.append((x1, y1, x2, y2)) return faces这段代码里要特别说明两个参数conf_threshold默认取 0.7如果现场误检多就往上调到 0.8如果漏检多就降到 0.5别固定一个值用到底。blobFromImage的均值参数用的是(104, 177, 123)这是模型自带的 BGR 均值很多人直接填(0,0,0)效果立刻变差。2.2 用 LBPH 做识别考勤场景够用关键参数要调对检测到人脸之后下一步是识别。OpenCV 自带三种传统识别器EigenFace、FisherFace、LBPH。考勤系统里我一般选 LBPH原因是它对光线变化的容忍度相对好训练不需要大量样本一个人给 20 到 30 张照片就能出可用结果。EigenFace 和 FisherFace 对光照太敏感在同一个办公室上午下午光线不同很容易翻车。import cv2 import os # 训练 LBPH 识别器 recognizer cv2.face.LBPHFaceRecognizer_create() faces_data [] labels_data [] for label, person_dir in enumerate(sorted(os.listdir(train_data))): # train_data 下每个子目录放一个员工的照片目录名建议用“工号_姓名” for img_name in os.listdir(ftrain_data/{person_dir}): img_path ftrain_data/{person_dir}/{img_name} gray cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) faces_data.append(cv2.resize(gray, (200, 200))) labels_data.append(label) recognizer.train(faces_data, np.array(labels_data)) recognizer.save(models/face_recognizer.yml)训练阶段的三个要点第一图片统一 resize 到 200x200LBPH 对分辨率不敏感但尺寸不一致会引入额外噪声。第二标签必须从 0 开始连续编号否则训练会直接报错。第三训练前最好对每张图做一次直方图均衡化cv2.equalizeHist把光照差异压一压这是我试过最有效的预处理手段。识别阶段同样要先做灰度化和 resize再调predictgray_face cv2.cvtColor(face_roi, cv2.COLOR_BGR2GRAY) gray_face cv2.resize(gray_face, (200, 200)) gray_face cv2.equalizeHist(gray_face) label, confidence recognizer.predict(gray_face) # confidence 是距离度量越小说明越接近LBPH 通常 80 以内算可信 if confidence 70: name employee_map[label] else: name unknownLBPH 的confidence和其他识别器不太一样它不是概率是样本到模型的距离。判断阈值建议用 60 到 80 之间的值想严格就设 60想宽松就 80。这个参数直接决定了考勤现场会不会把隔壁同事认成你。2.3 摄像头取流read() 不是随便调一调就完事摄像头取流看起来是cap.read()一个循环但真正做考勤界面时很少有人意识到read()是阻塞的。普通 USB 摄像头默认 30fpsread()会按帧率返回但 OpenCV 的VideoCapture在 Windows 下经常有缓冲队列问题画面延迟能到几百毫秒。cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) cap.set(cv2.CAP_PROP_FPS, 15) # 考勤场景 15fps 足够省 CPU把缓冲区降到 1再降分辨率到 640x480、帧率到 15识别延迟明显下降。这里有个反直觉的点分辨率越低DNN 检测的耗时反而越稳定因为检测前你还会把帧缩放到 300x300高分辨率原图除了占内存没有实际收益。后续画人脸框时只要把检测框坐标按原图宽度 / 300的比例放大回去就行。3. PyQt5 界面和摄像头协作别让 UI 卡死你的识别线程3.1 界面分区预览区、打卡记录区、状态区各放什么用 PyQt5 做考勤界面最忌讳把代码全堆在QMainWindow里。以我的习惯主窗口分成三个区域左侧是摄像头预览QLabel右侧是当日打卡记录QTableWidget底部是当前识别结果和签到状态QLabel。三个区域各干各的事互不干扰。from PyQt5.QtWidgets import QMainWindow, QLabel, QTableWidget, QVBoxLayout, QHBoxLayout, QWidget from PyQt5.QtCore import Qt class AttendanceWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(人脸识别考勤签到系统) self.resize(1000, 600) self.video_label QLabel(摄像头画面) self.video_label.setAlignment(Qt.AlignCenter) self.video_label.setMinimumSize(640, 480) self.record_table QTableWidget() self.record_table.setColumnCount(4) self.record_table.setHorizontalHeaderLabels([工号, 姓名, 签到时间, 状态]) self.status_label QLabel(等待识别...) central_widget QWidget() layout QHBoxLayout(central_widget) left_layout QVBoxLayout() left_layout.addWidget(self.video_label) left_layout.addWidget(self.status_label) layout.addLayout(left_layout, stretch3) layout.addWidget(self.record_table, stretch2) self.setCentralWidget(central_widget)这里有一个界面设计细节记录表格用QTableWidget而不是QListWidget因为考勤记录天然是行列结构后面要按时间排序、按工号过滤表格控件省去很多自己画行的麻烦。底部状态栏单独用一个QLabel不要混进QTableWidget里因为识别状态是实时刷新的混在一起会让表格闪个不停。3.2 QTimer 轮询和子线程推送两条路线的取舍摄像头是持续数据流人脸检测是耗时操作如果直接在 PyQt5 主线程里跑检测循环窗口会直接“假死”因为事件循环被阻塞了。常见做法有两种QTimer 定时刷新或者开一个子线程跑检测再通过信号把结果发回主线程。我倾向子线程方案原因很实际QTimer 轮询本质上还是在主线程里干活一旦某帧检测耗时超过定时器间隔UI 就开始卡顿时间长了窗口拖动都会掉帧。子线程方案看着多写几行代码但后期加人脸库比对、加数据库写入逻辑上都顺。import threading import cv2 import time from PyQt5.QtCore import pyqtSignal, QObject class CameraWorker(QObject): # 自定义信号把 BGR 帧转成 QImage 后发出去 frame_signal pyqtSignal(object) result_signal pyqtSignal(str, str) # 工号, 姓名 def __init__(self, cap): super().__init__() self.cap cap self.running False def run(self): self.running True while self.running: ret, frame self.cap.read() if not ret: continue # 识别结果会在这里发信号主线程负责刷新 UI self.frame_signal.emit(frame) time.sleep(0.05) # 限制循环速度别让 CPU 空转这个 worker 的运行循环里没有直接更新任何控件所有 UI 操作都通过pyqtSignal发回主线程。这是 PyQt5 多线程的铁律子线程不能直接碰控件否则大概率随机崩溃报RuntimeError或者黑屏。3.3 信号槽回传识别结果跨线程更新控件的正确姿势信号槽是 PyQt5 跨线程通信的官方通道。子线程里emit信号后主线程连接的槽函数会在事件循环里被调用这样天然避免了线程安全问题。# 子线程发来一帧画面主线程更新预览 def update_frame(self, frame): rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch rgb.shape bytes_per_line ch * w qimage QImage(rgb.data, w, h, bytes_per_line, QImage.Format_RGB888) self.video_label.setPixmap(QPixmap.fromImage(qimage).scaled( self.video_label.width(), self.video_label.height(), Qt.KeepAspectRatio)) # 子线程发来识别结果主线程更新记录表 def update_result(self, employee_id, name): row_count self.record_table.rowCount() self.record_table.insertRow(row_count) timestamp time.strftime(%Y-%m-%d %H:%M:%S) self.record_table.setItem(row_count, 0, QTableWidgetItem(employee_id)) self.record_table.setItem(row_count, 1, QTableWidgetItem(name)) self.record_table.setItem(row_count, 2, QTableWidgetItem(timestamp)) self.status_label.setText(f已识别: {name})注意QImage(rgb.data, ...)这里有个隐患传给 QImage 的字节数据必须与rgb生命周期一致如果rgb出了update_frame就被回收界面会花屏。最简单的做法是在槽函数里用.copy()复制一份再显示或者让rgb在槽内局部存活到 QImage 用完因为 PyQt5 在转换到 QPixmap 后 QImage 就不再被引用了。4. 考勤逻辑和落地数据签到不是“识别到了”那么简单4.1 员工底库怎么建照片采集、命名规范、特征入库一条线识别效果的上限由训练数据决定这个经常被忽略。给每个员工采集样本时我在train_data/下按工号_姓名建目录每个目录放 20 到 30 张不同角度、不同光照的正面照照片可以在签到时顺便采集但不要只从同一段视频里取帧。import cv2 import os def collect_samples(employee_id, name, save_dirtrain_data, sample_count30, cap_index0): person_dir f{save_dir}/{employee_id}_{name} os.makedirs(person_dir, exist_okTrue) cap cv2.VideoCapture(cap_index) count 0 while count sample_count: ret, frame cap.read() if not ret: continue faces detect_faces(net, frame) if len(faces) 1: x1, y1, x2, y2 faces[0] face_roi frame[y1:y2, x1:x2] if face_roi.size 0: continue face_roi cv2.resize(face_roi, (200, 200)) cv2.imwrite(f{person_dir}/{count:03d}.jpg, face_roi) count 1 # 每采集一张把预览画出来确认人脸框没偏 cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imshow(collecting, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()采集代码里两个要点一是只保存检测置信度最高的那张脸多人同框时跳过避免把别人收进员工库二是文件名用三位序号000.jpg到029.jpg保证排序稳定。训练时如果发现某个人识别率低优先检查他的样本是不是几乎同一角度补拍几张戴眼镜、偏头、稍微低头的照片比调阈值管用得多。4.2 判重规则和签到时间窗别让人脸每帧都打一次卡识别到人脸只是第一步考勤系统还得回答一个问题这个人今天算不算签到签到了算不算迟到。最简单的方案是维护一个“最近识别记录”字典按员工 ID 记录最后一次签到时间只有间隔超过设定时间窗才写入新记录。last_checkin {} CHECKIN_INTERVAL 60 * 60 # 同一个人一小时内不重复打卡 def process_checkin(employee_id, name, record_callback): now time.time() last_time last_checkin.get(employee_id) if last_time and now - last_time CHECKIN_INTERVAL: return False, 重复签到 last_checkin[employee_id] now record_callback(employee_id, name, now) return True, 签到成功这里有个容易被忽视的细节时间窗按“同一人一小时内只算一次”设计但不同班级、不同班次可能要求不一样。我把CHECKIN_INTERVAL做成配置项而不是常量换班次时改配置不用动代码。判重字典也要注意内存员工几百人时无所谓上万人时要用collections.OrderedDict做 LRU只保留最近活跃的 ID。迟到、早退的判断规则我的做法是签到时间按“当日首次有效记录”算系统配置里写late_threshold 09:00:00数据库中查该员工当日是否有更早记录没有则与阈值比较。这个逻辑不要放在信号槽里因为 UI 刷新会拖慢判断正确位置是写入数据库之前由业务层单独调用。4.3 CSV 落地与 Excel 中文乱码一个utf-8-sig解决考勤记录最终要给人事用 Excel 打开CSV 是最通用的格式。直接用open(records.csv, w, encodingutf-8)写入的 CSVExcel 打开大概率中文乱码原因是 Excel 默认用 GBK 解码 CSV。解决方式是用utf-8-sig编码写入时带 BOMExcel 就能正确识别。import csv def save_checkin_record(employee_id, name, timestamp): with open(attendance_records.csv, modea, encodingutf-8-sig, newline) as f: writer csv.writer(f) writer.writerow([employee_id, name, time.strftime(%Y-%m-%d %H:%M:%S, time.localtime(timestamp))])除了编码还有两个 CSV 写入的细节newline必须写否则 Windows 下每行末尾会出现多余空行文件用追加模式打开不要把整张表读进内存再重写考勤记录会越来越大全表重写迟早出性能问题。如果哪天要按月份导出报表建议按月建 CSV 文件文件名带202501这样的月份前缀后续统计时按文件名过滤。5. OpenCV 考勤系统避坑指南四个高频问题逐个拆5.1 No module named cv2装错包和装错环境的组合陷阱现象运行import cv2直接报ModuleNotFoundError: No module named cv2但pip list里明明看到 opencv 相关的包。原因大概率装的是opencv而不是opencv-python。PyPI 上存在一个叫opencv的历史遗留包它不提供cv2模块专门坑新手的。解决统一用pip install opencv-python安装并且在 VSCode 或 PyCharm 里确认用的是同一个虚拟环境。另外opencv-python-headless适合服务器部署本地做界面演示要用带 GUI 的opencv-python。如果遇到opencv与opencv-contrib-python混装先全部卸载再重装一个cv2.face模块只在 contrib 版本里有这个容易漏。5.2 摄像头打不开或画面黑屏索引被占用是玄学吗现象VideoCapture(0)返回的cap.isOpened()是 False或者能打开但画面全黑有时还把 QQ、微信的摄像头调用直接顶掉了。原因摄像头索引 0 被其他程序占用OpenCV 拿不到设备句柄。黑屏则通常是read()前几帧还没就绪直接进入检测循环。解决先给摄像头一个预热时间打开后循环读 5 帧丢弃再开始处理。如果确认没被占用还是打不开把索引换成 1 试试笔记本加装 USB 摄像头时索引不一定是 0。还有一个排查技巧用系统自带的相机 App 先打开一次确认摄像头硬件没问题再关掉跑你的程序。5.3 识别不准、乱框乱跳归一化、阈值和光照三件事现象人脸框在画面里一跳一跳名字一会儿对一会儿错有时把显示屏里的照片人也识别进去了。原因一是检测置信度阈值太低屏幕上的照片人脸被当成真人二是送入 LBPH 的 ROI 没有做灰度化、resize、直方图均衡化特征不稳定三是把识别器对“未知人”的confidence也当成可信结果。解决检测阈值调到 0.75 以上识别器阈值设在 70 左右低于阈值一律显示“unknown”。图像预处理三步不能省cvtColor、resize、equalizeHist顺序也不能换。另外在识别前把人脸 ROI 边缘往内收缩 10 像素再裁能减少背景进入特征区域的影响。5.4 迟到早退统计对不上业务日期和系统时间混用现象员工凌晨 0 点前后打卡迟到统计归到了前一天或者跨月统计时记录被分到错误的月份。原因签到时间存的是完整时间戳但统计时用系统当前日期去匹配记录日期凌晨时段和月末切换时就会错位。解决入库时加一列business_date按time.localtime(timestamp).tm_year和tm_yday拼出业务日期统计按business_date过滤而不是按当前日期。还有一个和 OpenCV 无关但考试勤必遇到的问题电脑系统时间不准人脸识别系统再准也白搭部署时写个启动自检判断系统时间距网络时间差超过 5 分钟就拒绝启动。6. 值得继续投入的改进方向活体检测、特征存储和打包发布这套系统跑通之后真正值得继续投入的改进有三个方向。第一个是活体检测。只用 OpenCV 传统识别器时一张打印照片就能骗过系统所以在识别成功后加一个眨眼检测很必要。OpenCV 有cv2.face之外的眼睛关键点方案或者直接按 EAR 距离比判断眼睛开合连续 3 帧 EAR 值低于阈值再回升才算一次眨眼这个方法不需要额外模型成本最低。第二个是特征存储升级。员工几百人时LBPH 的predict速度会明显下降因为每次都在遍历全部训练样本。建议把特征提取换到深度网络例如用 OpenCV DNN 加载人脸特征模型把每张脸映射成 128 维向量再存进 SQLite 或 FAISS 做近邻检索。这个改动对现有界面无感因为信号槽和 UI 结构不变只替换predict背后的实现。第三个是打包发布。本地跑通了交给人事部门用才是终点。用 PyInstaller 打包时要注意两点OpenCV 的模型文件prototxt和caffemodel要放到资源目录用sys._MEIPASS兼容冻结后的路径cv2.face需要确认opencv-contrib-python在打包环境里已经装好。打包出来的 exe 第一次启动会比较慢这是正常现象不需要额外加启动画面。如果现场机器没有 NVIDIA 显卡别碰 CUDA 版 OpenCV 的预编译包CPU 版在 640x48015fps 下跑 DNN 检测加 LBPH 识别已经够用GPU 提速换来的是打包体积和部署环境复杂度成倍上升。我自己第一次把这套系统部署到办公室时最大的教训不是算法选型而是没有做系统自检摄像头被占用、模型文件路径写死、CSV 打开乱码三个问题在一上午全踩了一遍。后来把所有配置抽到config.ini模型路径和摄像头索引都从配置读取再启动前先跑一轮自检问题少了一大半。人脸识别考勤系统本质上不是一个算法项目而是一个工程项目把基础的环境、路径、数据格式做好算法自然就稳了。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑