基于OpenCV与LBPH的人脸考勤系统设计与实现
2026/9/13 2:55:54 网站建设 项目流程

简介:这是一套基于 OpenCV 人脸识别的考勤系统毕业设计源码,面向计算机及相关专业学生,可直接作为毕业设计、课程设计或期末大作业交付。项目采用 Python 开发,集成了用户界面、人脸图像处理和后台配置,按说明搭建环境后即可运行,覆盖人脸采集、识别、签到信息管理等常见考勤环节。压缩包大小约 239.37MB,共 26 个文件,核心是 3 个 Python 源码文件,另有 15 张 PNG 界面与背景图、1 个 api 文件、1 个配置文件、1 个字体文件和说明文档等辅助材料,目录划分较为清晰,项目结构完整,方便按模块阅读和二次开发。资源中还附带成品软件压缩包与介绍文本,便于先查看运行效果再深入源码;目前已有 369 人学习下载,适合需要高完成度项目参考、快速上手或在此基础上扩展功能的学生。

1. 用 OpenCV 做人脸考勤,为什么毕业设计和工作落地都绕不开它

人脸识别考勤系统几乎占了 Python + OpenCV 毕业设计的半壁江山,但很多流传的“源码包”只跑通了单机 demo:一个人对着摄像头,识别成功打个勾,换个人或者换个光线就荒腔走板。真正值得写进简历的考勤系统,至少要回答三个问题——人脸从哪里来、识别结果怎么变成可信的考勤记录、数据和模型怎么持续维护。这里从 OpenCV 的人脸检测与 LBPH 识别原理讲起,把模块拆分、数据库设计、实时打卡逻辑和阈值调优整条链路串起来,包含可以直接复制的代码和参数对照。新手可以照步骤把系统跑通,有经验的开发者可以重点看第 4、5 章:判重窗口怎么设、置信度阈值在多大规模的人脸库上会失效、数据库为什么不应该只存打卡时间。整套方案不依赖商业 SDK,OpenCV 官方接口加标准库就能成型,既能当毕业设计交付,也能当二次开发的底子。

2. 人脸考勤的技术选型:OpenCV 的 DNN 检测与 LBPH 识别为什么是稳定组合

2.1 人脸检测优先选 DNN 而不是 Haar 级联

网上很多老教程喜欢从 Haar Cascade 教起,但实际做考勤系统,我建议直接用 OpenCV 的 DNN 人脸检测器。Haar 基于 AdaBoost 训练出来的矩形特征分类器,对正面、光照均匀、低分辨率的场景尚可,但考勤现场经常出现背光工位、窗帘拉上一半的侧光环境,漏检率马上抬头。DNN 检测器内部是一个经过预训练的 SSD 网络,cv2.dnn.readNetFromCaffe可以加载配套的 prototxt 和 caffemodel 两个文件,在 CPU 上跑单帧检测大约 20 到 50 毫秒,考勤这种低并发场景完全够用。

import cv2 net = cv2.dnn.readNetFromCaffe( "deploy.prototxt", "res10_300x300_ssd_iter_140000_fp16.caffemodel" ) def detect_faces(frame, conf_threshold=0.7): h, w = frame.shape[:2] blob = cv2.dnn.blobFromImage( cv2.resize(frame, (300, 300)), 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] * [w, h, w, h] x1, y1, x2, y2 = box.astype("int") faces.append((x1, y1, x2, y2, confidence)) return faces

blobFromImage里的三个均值参数是模型预训练时统计出来的,不要随手改成 0,否则检测率会明显波动。conf_threshold建议考勤场景设为 0.7 以上,低于这个值容易把门框、海报里的人脸误检出来,触发后续多余的识别流程。Haar 也并非一无是处,如果你的机器是树莓派 Zero 级别的 CPU,Haar 的检测速度比 DNN 快一个量级,用cv2.CascadeClassifier加载haarcascade_frontalface_default.xml即可,但误检率更高,对侧脸和戴帽子的情况更敏感。考勤摄像头通常固定不动,画面变化不大,DNN 的稳定性优势会被放大,这是我在方案里选它的核心理由。

2.2 LBPH、EigenFace、FisherFace 三选一,哪个适合打卡

检测到人脸之后,下一步是“认出这是谁”。OpenCV 的face模块提供三种经典识别器:LBPH、EigenFace 和 FisherFace。考勤场景只推荐 LBPH,原因有两个:一是它对光照变化更鲁棒,二是它不需要强制所有训练样本统一尺寸,虽然工程上你通常仍会 resize 到固定大小。

识别器数学原理光照鲁棒性样本要求考勤场景适配度
LBPH局部二值模式 + 直方图距离每人 10~50 张推荐
EigenFacePCA 降维每人至少 10 张实验室演示尚可
FisherFaceLDA 判别分析类内样本要充足不推荐

EigenFace 本质是 PCA 降维,把每张人脸拉成一维向量后找主成分方向;FisherFace 是 LDA,追求类间离散度最大、类内离散度最小。这两者在受控的实验室光照下有不错精度,但一旦换成笔记本自带摄像头那种偏色明显、噪点突出的画面,识别率下降非常快。LBPH 的思想完全不同,它不关心全局灰度幅值,而是对每个像素的邻域做二进制编码,描述的是纹理梯度关系,所以光线变化带来的整体亮度偏移对它影响小得多。

2.2.1 LBPH 的邻域编码与直方图距离

LBPH 把图片切成若干小块,对每个小块计算局部二值模式直方图,再把所有小块直方图拼接成整张人脸的特征描述。OpenCV 里用cv2.face.LBPHFaceRecognizer_create()创建时,默认参数是radius=1, neighbors=8, grid_x=8, grid_y=8。半径越大,描述的是更宏观的纹理结构;邻居数一般取 8 或 16,取 16 计算量几乎翻倍,但对细节更敏感。切块数量grid_xgrid_y决定了空间信息的保留程度,8×8 意味着把人脸横向纵向各分 8 份,如果数值调大,特征维度变高,小范围遮挡的容忍度反而下降。

识别时,LBPH 计算待识别特征与库中每个注册特征的直方图距离,距离越小越相似,最终返回两个值:标签label和置信度confidence。这里要特别提醒,LBPH 的置信度是“距离”而不是“概率”,值越小代表越匹配,和 DNN 检测里的conf_threshold方向刚好相反,初学最容易在语义上踩坑,后面所有阈值参数都建立在“距离越小越像”这个大前提下。

2.3 识别阈值与置信度,考勤里比算法更关键的参数

注册样本是 20 张还是 100 张、训练集里有没有戴眼镜的样本,最终都会折算成一个问题:predict返回的距离低于多少才允许打卡。代码层面很容易实现:

recognizer = cv2.face.LBPHFaceRecognizer_create() recognizer.read("attendance_model.yml") label_id, distance = recognizer.predict(gray_face) # distance 是直方图距离,越小越像本人 print(f"label={label_id}, distance={distance:.2f}")

我一般把初版阈值定在 80。小于 80 直接判定为本人,80 到 100 之间标记为低置信度打卡,需要人工复核,大于 100 则拒绝。三档策略比硬阈值实用得多,因为考勤场景里误判的代价是双向的——把张三记成李四是考勤事故,把本人挡在门外则是体验事故。同一个人的样本里如果同时包含眼镜、刘海变化、轻度低头三种形态,LBPH 的类内距离分布会被拉大,阈值就要相应放松到 90 到 100 区间。具体怎么调,第 5 章给出回归验证方案。

3. 考勤系统的模块划分与数据库模型设计

3.1 “注册—识别—记录”的功能边界

一个能真正部署的考勤系统,代码结构上至少要有四个模块:人脸采集模块、模型训练模块、实时识别模块、考勤记录模块。很多毕设源码把前三个写在一个文件里,跑起来没问题,但改阈值、换摄像头、加签到规则时到处是耦合,答辩时老师打开目录就能看到代码管理能力。

attendance/ ├─ capture.py # 采集人脸样本 ├─ train.py # 训练 LBPH 模型 ├─ recognize.py # 实时识别并打卡 ├─ db.py # SQLite 封装 ├─ config.py # 阈值、窗口等参数集中管理 ├─ models/ │ ├─ deploy.prototxt │ └─ res10_300x300_ssd_iter_140000_fp16.caffemodel └─ datasets/ └─ employee_1001/ ├─ 0.jpg └─ ...

采集模块负责把人脸框截取出来,统一缩放到 200×200 并转灰度图存储;训练模块读取datasets目录下所有子目录,以子目录名映射标签;识别模块循环读摄像头帧、检测、预测、查库;记录模块把判定成功的事件写入考勤表。四个模块共享的唯一上下文就是“员工 ID”这个标签。config.py里集中放CONF_THRESHOLDMIN_DISTANCEPUNCH_WINDOW这类变量,调参时不用翻三个文件,这也是给代码加分的小细节。

3.2 员工表、流水表、日统计表,三张表缺一不可

数据库是考勤系统最容易偷懒的部分,但恰恰是答辩时最容易被追问的环节。只建一张打卡时间表是最典型的扣分项。合理的表结构至少拆成三张:员工表记录人的属性,流水表记录每次原始打卡事件,日统计表记录每天的首末次打卡和次数。

CREATE TABLE employees ( emp_id INTEGER PRIMARY KEY, emp_no TEXT NOT NULL UNIQUE, name TEXT NOT NULL, department TEXT, photo_path TEXT, registered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE attendance_logs ( log_id INTEGER PRIMARY KEY, emp_id INTEGER REFERENCES employees(emp_id), punch_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, device_location TEXT, confidence REAL, status TEXT DEFAULT 'normal', CHECK (status IN ('normal', 'low_conf', 'duplicate')) ); CREATE TABLE daily_summary ( summary_id INTEGER PRIMARY KEY, emp_id INTEGER REFERENCES employees(emp_id), work_date TEXT NOT NULL, first_punch TEXT, last_punch TEXT, punch_count INTEGER, UNIQUE(emp_id, work_date) );

attendance_logs里的confidence字段在不少源码里被省略,但这是事后排查误判的唯一凭证;status字段用来标记低置信度打卡和重复打卡,避免直接删记录掩盖问题。daily_summary是物化出来的日统计表,当天结束后由脚本汇总生成,查询月报时不需要再扫全量流水。补充一点,这里的INTEGER PRIMARY KEY在 SQLite 中本身就是 rowid 别名,自带自增行为,不需要写AUTOINCREMENT,后者还会引入额外的系统表开销,对考勤这种写入量完全没必要。

3.3 SQLite 与 MySQL 的取舍

毕设和中小型企业内部考勤,SQLite 完全够用。考勤系统的写入频率极低,每人每天两到四次打卡,一个 200 人的公司一天流水也就 800 行左右,SQLite 单文件数据库处理这种量级毫无压力,免安装、单文件备份,答辩现场把数据库文件拷走就能演示。

但 SQLite 有并发写入限制,多个识别终端同时打卡时容易出现database is locked。如果你的系统要接两台以上识别终端,至少要把数据库层抽成独立服务,或者干脆切 MySQL。切换时只改db.py里的连接逻辑,把sqlite3.connect换成配置了连接池的pymysql调用,三张建表语句在 MySQL 里同样成立,CHECK约束在 MySQL 8.0 之前不会强制生效,需要改成ENUM类型或者交给应用层校验。这里的取舍原则是:单机演示 SQLite,并发部署 MySQL,表结构保持不变,接口层做到平滑切换。

4. 核心代码实现:采集、训练、实时打卡与记录导出

4.1 摄像头人脸采集与数据集生成

采集阶段的代码要点有两个:一是连续帧去重,避免存下十几张几乎一样的照片;二是保存原始彩色照片,训练时再转灰度,给以后换深度学习模型留退路。cv2.VideoCapture(0)里的 0 代表默认摄像头设备号,笔记本外部摄像头通常是 1,接入 USB 摄像头后设备号会变,把设备号也放进config.py更稳妥。

import cv2 import os cap = cv2.VideoCapture(0) save_dir = "datasets/employee_1001" os.makedirs(save_dir, exist_ok=True) count = 0 max_samples = 50 while count < max_samples: ret, frame = cap.read() if not ret: break faces = detect_faces(frame, conf_threshold=0.8) for (x1, y1, x2, y2, conf) in faces: margin = 20 x1 = max(0, x1 - margin) y1 = max(0, y1 - margin) x2 = min(frame.shape[1], x2 + margin) y2 = min(frame.shape[0], y2 + margin) face = frame[y1:y2, x1:x2] face = cv2.resize(face, (200, 200)) if count % 3 == 0: # 每隔 3 帧存一次,减少相似帧 cv2.imwrite(f"{save_dir}/{count}.jpg", face) count += 1 cv2.imshow("capture", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()

margin参数把裁剪框向外扩 20 像素,多带一点额头和下巴的上下文,特征稳定性比紧贴脸框好不少。每隔三帧存一次,对应 30 FPS 摄像头每秒产生约 10 张候选帧,足够覆盖头部轻微偏转的多样性。采集时让员工前后移动头部、摘戴一次眼镜、做一次表情变化,50 张样本基本覆盖日常打卡姿态。这里有个容易被忽视的点:检测阈值在采集阶段要调到 0.8 以上,宁可漏几帧也不要把背景框进来,脏样本会直接污染训练集。

4.2 训练 LBPH 识别模型并持久化

训练脚本的任务是把datasets下的图片映射成标签数组,然后交给 LBPH 训练并保存模型。注册顺序会影响标签编号,所以同时导出一份label_map.json保存标签和员工目录的对应关系,否则识别结果里只有数字,还得回头猜是谁。

import cv2 import os import json import numpy as np faces, labels = [], [] label_map = {} for label_id, emp_dir in enumerate(sorted(os.listdir("datasets"))): label_map[str(label_id)] = emp_dir for fname in os.listdir(f"datasets/{emp_dir}"): img = cv2.imread(f"datasets/{emp_dir}/{fname}", cv2.IMREAD_GRAYSCALE) img = cv2.equalizeHist(img) # 直方图均衡化,缓解亮度不均 faces.append(img) labels.append(label_id) recognizer = cv2.face.LBPHFaceRecognizer_create( radius=1, neighbors=8, grid_x=8, grid_y=8 ) recognizer.train(faces, np.array(labels)) recognizer.write("attendance_model.yml") with open("label_map.json", "w") as f: json.dump(label_map, f, ensure_ascii=False, indent=2)

equalizeHist可以消除一部分室内灯光色温和亮度不均的影响,但要注意识别阶段必须对灰度图做同样的操作,两边特征空间不一致会导致识别率暴跌。训练数据里如果某个人的样本数量明显多于其他人,LBPH 的直方图统计会偏向样本多的类,出现过拟合倾向,采集时就应该控制每人样本数基本一致,而不是张三 20 张、李四 80 张。

4.3 实时打卡识别链路:捕捉、比对、判重、写库

实时识别是核心链路,业务逻辑层的关键点在“识别到人以后要不要立刻打卡”和“同一人连续出现会不会重复打卡”这两个规则上。以下代码演示了完整的判重和落库逻辑:

import cv2 import time from db import AttendanceDB db = AttendanceDB("attendance.db") recognizer = cv2.face.LBPHFaceRecognizer_create() recognizer.read("attendance_model.yml") last_punch = {} # emp_id -> (timestamp, distance) def on_face_detected(emp_id, distance, ts): prev = last_punch.get(emp_id) if prev and ts - prev[0] < 300: # 5 分钟内不重复打卡 return if distance <= 80: status = "normal" db.insert_log(emp_id, ts, distance, status) elif distance <= 100: status = "low_conf" db.insert_log(emp_id, ts, distance, status) else: return last_punch[emp_id] = (ts, distance)
4.3.1 判重时间窗口怎么定

last_punch字典用员工 ID 作键,记录上次成功打卡的时间戳。时间窗口 300 秒意味着同一员工在五分钟内只记录一次,即使他中途离开工位又回来也不会产生两条流水。窗口设多长需要和考勤规则对齐,如果公司要求上午下午各打一次卡,窗口应设为 4 小时而不是 5 分钟,否则下午那次打卡会被静默忽略。on_face_detected里先判重、再判距离、最后写库的顺序不能颠倒,保证所有入库记录都有明确的状态标记,方便后续人工复核。

4.4 导出月度考勤报表

考勤记录最终要变成可读的报表。最简单的方案是写一个查询脚本,按日期范围导出并用 pandas 聚合:

import sqlite3 import pandas as pd conn = sqlite3.connect("attendance.db") df = pd.read_sql_query(""" SELECT e.emp_no, e.name, l.punch_time, l.status FROM attendance_logs l JOIN employees e ON e.emp_id = l.emp_id WHERE l.punch_time BETWEEN ? AND ? ORDER BY e.emp_no, l.punch_time """, conn, params=["2025-06-01 00:00:00", "2025-06-30 23:59:59"]) df["date"] = pd.to_datetime(df["punch_time"]).dt.date summary = df.groupby(["emp_no", "name", "date"])["punch_time"].agg( ["first", "last", "count"] ) summary.to_excel("attendance_monthly.xlsx")

agg(["first", "last", "count"])一次性提取每天的首次打卡、末次打卡和打卡次数,这就是物化daily_summary表时的同一套聚合逻辑。to_excel需要额外安装openpyxl,如果只做网页展示,把to_excel换成to_json即可无缝切换输出格式。参数列表里用?占位符而不是 f-string 拼 SQL,这是避免 SQL 注入的基本功,考勤系统可能不直接暴露给公网,但代码习惯要从毕业设计就开始养成。

5. 真实环境下的人脸考勤调优与误判排查

5.1 光照、角度、遮挡对置信度的影响

把模型放到真实环境前,先用三组对照实验确定基线:正面均匀光、侧光、背光各采集一次,记录 distance 的均值与标准差。正常情况下“本人距离集中在 50~80,陌生人距离大于 120”,两类之间的距离差超过 40 才算一条健康的特征边界。如果差距不足 30,优先怀疑训练样本里的背景占比太高,因为背景纹理也会进入 LBPH 的直方图。解决办法是把采集时的margin从 20 收小到 10,让人脸像素占比更高。

5.2 照片袭击与替打卡的防线

LBPH 无法做活体检测,拿一张手机照片对着摄像头就能完成注册和打卡,这是人脸考勤系统的固有短板。OpenCV 自带的cv2.Facemark可以标定眼鼻嘴关键点,进而验证眨眼动作;更轻量的方案是计算人脸框内连续帧的变化量,真实人头会有细微的平移和眨眼,而静止照片几乎没有任何高频变化。毕业设计答辩时明确写出这一条作为系统边界——照片袭击是已知漏洞,活体检测属于后续迭代项,比回避问题要诚实得多。这里还要提一个容易被忽略的部署细节:考勤机不要放在正对窗户的位置,逆光下人脸区域过曝,equalizeHist也很难救回来。

5.3 用回归验证集校准阈值

建一个 20 人的验证集,每人挑 5 张不在训练集里的样本,再混入 50 张陌生人脸,逐张跑predict得到距离分布,用这个分布反推阈值。误收率(把陌生人放进来)和误拒率(把本人挡门外)的交叉点就是通常意义上的最优阈值。把这段测试脚本固化成evaluate.py,每次扩充训练数据后重跑一遍,确认新增样本没有破坏已有人员的特征边界。实际调阈值时我习惯把低置信度档位先放宽到 100,运行两周后查status='low_conf'的记录,如果占比超过 5%,说明采集样本质量在下降,优先补样本而不是继续放宽阈值,补完后回归验证集是最快的校准手段。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询