简介:一套基于Python、OpenCV与PyQt5构建的人脸识别考勤签到系统源码,面向课程设计与毕业设计场景,覆盖人脸检测、特征提取、签到记录与查询管理的完整流程。资源共44个文件,包含12个Python脚本、16个pyc缓存、10个OpenCV级联分类器XML和2个PyQt5界面UI文件,压缩包整体仅681KB,目录结构清晰,便于按界面层、功能层和数据层分段研读;其中py文件承载核心逻辑,xml为预训练的人脸检测模型,ui为可视化窗口布局定义。源码注释详尽,从Haar级联人脸检测、LBPH特征提取到PyQt5信号与槽交互均有落地实现,并配有数据库/文件存储、日期时间处理与异常捕获等模块,能帮助开发者掌握考勤系统的工程化写法。目前已有266人学习下载,非常适合正在准备课程设计或毕业设计、希望系统学习OpenCV人脸识别与PyQt5桌面应用开发的初学者作为实战参考。
1. 基于 Python+OpenCV 的人脸考勤:这源码解决的是什么场景
把摄像头往门口一放,员工靠近屏幕,系统喊出名字并自动写一条打卡记录——这就是基于 python+opencv 人脸识别考勤签到系统最朴素的使用场景。这类项目在 opencv 图像处理里属于非常典型的落地练习:既要处理实时视频流,又要跑人脸检测与识别,还得用 pyqt5 把结果以桌面界面的方式展示给操作员。它解决的是不想买昂贵门禁机、又希望签到这个动作尽量无人值守的需求,特别适合中小团队、实验室和毕业设计,成本就是一部 USB 摄像头加一台普通电脑。适合有一定 Python 基础,想改识别逻辑、接自有数据库、把界面换成自己公司风格的开发者。我下面直接按“环境跑通 → 核心代码 → 考勤逻辑 → 踩坑 → 如何验证”的顺序来讲,新手能跟完,熟手也能找到可调的参数。
2. 把项目跑起来之前:环境、依赖与目录结构
2.1 安装 Python 与 OpenCV、PyQt5:版本组合和常见安装失败
这类项目对 Python 版本其实很敏感。我见过太多人按最新的 python 安装教程装完 3.12,转头pip install opencv-python发现装上了却 import 不了,或者 PyQt5 装完打开窗口直接闪退。最稳的组合是 Python 3.8 或 3.9,搭配 opencv-contrib-python 4.5.5 和 PyQt5 5.15.7。3.10 也能用,但 3.12 容易踩到轮子还没跟上来的坑,不建议新手上来就挑战。
# 建议先建一个虚拟环境,避免污染系统 Python python -m venv venv # Windows 激活命令;Linux/macOS 用 source venv/bin/activate venv\Scripts\activate # 核心依赖 pip install numpy pip install opencv-contrib-python==4.5.5.64 pip install pyqt5==5.15.7 pip install pillow这里最容易翻车的点是 OpenCV 的包名。很多人只装了opencv-python,结果代码里写cv2.face.LBPHFaceRecognizer_create()时直接报No module named 'cv2.face'。因为opencv-python是精简版,不带face模块;人脸识别要用opencv-contrib-python。另外不要同时装这两个包,它们会互相覆盖,最后 import cv2 时版本都对不上。
如果pip install慢到怀疑人生,加一个国内镜像源就行:pip install -i https://pypi.tuna.tsinghua.edu.cn/simple opencv-contrib-python。装完之后用一条命令验证环境:python -c "import cv2; print(cv2.getBuildInformation())"。能打印出来就说明 cv2 没问题;如果明明 pip show 里有 opencv,python 却说找不到,那大概率是虚拟环境没激活,pip 装到了另一个 Python 解释器里。这种“opencv 安装成功却找不到 cv2”的情况,本质不是安装失败,而是解释器路径错位。
2.2 源码目录应该怎么摆:样本、模型和三层结构
拿到一个考勤签到系统源码,不要急着直接双击 main.py。这类项目一般都有三块职责:界面(UI)、识别核心(core)、数据存储(db)。混在一个文件里后患无穷,因为摄像头线程要和界面线程通信,识别逻辑又要和数据库耦合,一旦写成一坨,调一个打卡时间逻辑都可能牵动整个界面。
常见的目录结构是这样的:
attendance/ ├─ main.py # 程序入口,只负责启动 QApplication ├─ requirements.txt # 依赖清单 ├─ core/ │ ├─ detector.py # 人脸检测,封装 Haar Cascade │ ├─ recognizer.py # 训练和预测,封装 LBPH │ └─ database.py # SQLite 读写,负责签到记录 ├─ ui/ │ ├─ main_window.py # PyQt5 主窗口,按钮和表格 │ └─ camera_thread.py # 摄像头 QThread,不停发画面和识别结果 ├─ data/ │ ├─ samples/ │ │ ├─ 10001/ # 每个工号一个文件夹 │ │ │ ├─ 0.jpg │ │ │ └─ 1.jpg │ └─ models/ │ └─ face_model.yml # 训练出来的人脸模型 └─ attendance.db每个目录下面的代码都应该带上详细注释。我一般会把函数说明写在 docstring 里,尤其是detector.py里的detect_all_faces()和recognizer.py里的predict(),因为参数含义不写在注释里,换一个人维护时基本只能靠猜。数据库文件不要放在项目根目录,建议放data/下,备份时长整目录拷走就行。
2.3 第一次启动:从注册人脸到完成一次签到
建议按“注册人脸 → 训练模型 → 启动签到”三步走,不要跳步。先把人脸样本准备好,再训练模型,最后才开界面。很多人拿到代码后直接启动界面,结果识别框里全是 0,或者所有人都被识别成同一个人,都是因为模型只有一个空架子。
第一步是采集人脸样本。下面的代码会打开摄像头,检测到人脸后裁切、缩放、保存为灰度图,每个工号存 30 到 50 张:
# collect_faces.py import cv2 emp_id = input("输入工号(用于创建文件夹): ").strip() detector = cv2.CascadeClassifier("haarcascade_frontalface_default.xml") cap = cv2.VideoCapture(0) count = 0 while count < 30: ok, frame = cap.read() if not ok: continue gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = detector.detectMultiScale(gray, 1.1, 5, minSize=(100, 100)) for (x, y, w, h) in faces: face = gray[y:y + h, x:x + w] face = cv2.resize(face, (160, 160)) cv2.imwrite(f"data/samples/{emp_id}/{count}.jpg", face) count += 1 print(f"已采集 {count}/30")这里有两个设计点:保存灰度图是因为 LBPH 识别器只吃单通道,彩色图进去反而会多出无效信息;缩放成 160×160 是为了让训练和预测都固定输入尺寸,避免模型对分辨率过敏感。注意要预先创建data/samples/{emp_id}这个文件夹,否则imwrite会因为路径不存在而静默失败,这一步很容易被忽略。
第二步是训练模型。把data/samples下所有照片读进来,打上标签,交给 LBPH 训练,最后写成一个 yml 文件:
# train.py import cv2 import glob import os import numpy as np recognizer = cv2.face.LBPHFaceRecognizer_create(radius=1, neighbors=8, grid_x=8, grid_y=8) faces, labels = [], [] for path in glob.glob("data/samples/*/*.jpg"): label = int(path.split(os.sep)[-2]) # 文件夹名就是员工 ID face = cv2.imread(path, cv2.IMREAD_GRAYSCALE) face = cv2.resize(face, (160, 160)) faces.append(face) labels.append(label) labels = np.array(labels) recognizer.train(faces, labels) recognizer.write("data/models/face_model.yml") print(f"训练完成,共 {len(faces)} 张样本")这样训练出来的模型就可以被主界面加载。跑通这两个脚本之后,再启动 main.py,选择 USB 摄像头,就能在 PyQt5 窗口里看到实时画面和人名框了。第一次启动如果画面里有人名但考勤记录没有写入,多半是还没有走到“写入数据库”的判定逻辑,这一块我在第 4 章展开。
3. 核心代码逐个拆:人脸检测、特征训练与 PyQt5 显示
3.1 用 Haar 级联检测人脸:detectMultiScale 的参数与返回结果
OpenCV 里的人脸检测最常见做法是 Haar Cascade,它在 OpenCV 4.x 里依然是默认选择。不是因为它最强,而是胜在不需要额外下载模型、CPU 也能跑得动。识别考勤这种场景,Haar 的误检率是能接受的。
# core/detector.py import cv2 class FaceDetector: def __init__(self, cascade_path="haarcascade_frontalface_default.xml"): self.cascade = cv2.CascadeClassifier(cascade_path) def detect(self, gray, scale_factor=1.1, min_neighbors=5, min_size=(80, 80)): faces = self.cascade.detectMultiScale( gray, scaleFactor=scale_factor, minNeighbors=min_neighbors, minSize=min_size, flags=cv2.CASCADE_SCALE_IMAGE ) return faces # 返回 [(x, y, w, h), ...]detectMultiScale里有三个参数需要重点理解。scaleFactor控制图像缩放步长,1.1 表示每轮把图像缩小 10%,值越小越精确但速度越慢,值太大容易把远处的小人脸漏掉。minNeighbors表示每个候选框需要被附近多少个检测框“投票”才能保留,5 到 6 是比较折中的值;设成 1 或 2 误报会暴增,背景里的插线板都可能被当成人脸。minSize是检测框最小尺寸,如果你的摄像头离人脸比较远,可以设成 80×80 或更小,否则识别模块拿到的人脸区域太小,LBPH 几乎不可能给出稳定结果。
另外,Haar 对侧脸和大角度俯仰确实不行。如果你发现考勤时员工需要正对摄像头才能签到,这不是代码 bug,是 Haar 的固有局限。想减少这种“玄学”失败,可以在检测之前对灰度图做一次直方图均衡化,把逆光造成的暗部细节拉回来:
gray = cv2.equalizeHist(gray)别小看这一行,很多“十次识别八次失败”的翻车现场,加完这一行之后成功率明显上升,因为 LBPH 对光照变化没那么鲁棒,先做直方图均衡化相当于帮它把对比度统一了。
3.2 LBPH 训练与预测:radius、neighbors、grid 与置信度阈值
考勤系统里,人脸识别部分最常用的是 OpenCV 自带的 LBPH(Local Binary Patterns Histograms)。它不依赖 GPU,训练速度以秒计,几百张样本就能见到效果。对比动不动就要跑深度模型的方案,LBPH 是“入门首选、可解释性强”的那一类。
# core/recognizer.py import cv2 class FaceRecognizer: def __init__(self, model_path=None, threshold=80): self.recognizer = cv2.face.LBPHFaceRecognizer_create( radius=1, neighbors=8, grid_x=8, grid_y=8 ) self.recognizer.setThreshold(threshold) if model_path: self.recognizer.read(model_path) def train(self, faces, labels): self.recognizer.train(faces, labels) def predict(self, face): label, confidence = self.recognizer.predict(face) if confidence >= self.recognizer.getThreshold(): return -1, confidence return label, confidencepredict()返回两个值,label 是整数标签,confidence 是置信度距离。对 LBPH 来说,距离越小表示越匹配。所以threshold的语义是“超过这个距离就认为不认识”,默认值通常取 80 到 100。如果想让系统更严格,防止陌生人刷脸成功,就应该降低阈值到 60 左右;如果房间里光线差、识别总失败,可以提高阈值到 110,但代价是有可能把两个人判成同一个人。
识别器参数里,radius=1表示计算 LBP 特征时采样半径;neighbors=8表示使用 8 个采样点,这也是 OpenCV 官方示例值;grid_x=8, grid_y=8把图像切分成 8×8 的块计算直方图。切块越多,空间位置信息保留得越好,但特征维度也会变大。实测下来 8×8 是速度和精度的平衡点,grid_x=11, grid_y=11在小数据集上并不会明显更好,反而容易过拟合。
训练之后,模型里的 label 是文件夹名解析出来的整数。如果你把工号“001”当成字符串保存,读回来时int("001")会变成 1,这个细节在训练脚本里要注意,否则工号对不上人。我一般会在模型旁边配一个employees.json,把整数 label 和真实工号、姓名对应起来。
3.3 PyQt5 界面怎么接摄像头:BGR 转 RGB、QImage 与 QThread
PyQt5 界面设计里最容易出问题的,是把摄像头读取放到主线程。QTimer一秒钟刷新 30 次,每次cap.read()加检测再加预测,整个 UI 会卡到没法看,窗口拖都拖不动。正确做法是把摄像头采集放到 QThread 里,识别结果通过信号发回主窗口。
# ui/camera_thread.py import cv2 import time from PyQt5.QtCore import QThread, pyqtSignal from PyQt5.QtGui import QImage class CameraThread(QThread): frame_ready = pyqtSignal(QImage) face_detected = pyqtSignal(int, float) # label, confidence def __init__(self): super().__init__() self._run_flag = True def run(self): cap = cv2.VideoCapture(0) while self._run_flag: ok, frame = cap.read() if ok: gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = detector.detect(gray) # 对每个人脸框画矩形并展示 for (x, y, w, h) in faces: cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2) rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch = rgb.shape qimg = QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888) self.frame_ready.emit(qimg.copy()) time.sleep(0.03) # 30 帧左右 def stop(self): self._run_flag = False self.wait()这段代码里必须解释三个细节。第一,cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)不能省,OpenCV 读进来是 BGR,PyQt5 的 QImage 却按 RGB 解析,不转换的话人脸会整体偏蓝偏暗。第二,qimg.copy()一定要加,因为QImage构造时引用的是rgb.data的内存,一旦frame被下一次cap.read()覆盖,那块内存就被改掉了,界面显示的就是残影和花屏。第三,time.sleep(0.03)是为了给 UI 线程留呼吸时间,不让线程空转占满 CPU;如果识别很慢,把间隔调成 0.05 或 0.06,画面上每秒帧数低一点但不会卡死。
信号face_detected在实际项目中还要带上识别结果,比如self.face_detected.emit(label, confidence)。主窗口收到后,把 label 翻译成姓名,再显示到状态栏,同时把签到记录写进 SQLite。这个流程放到第 4 章继续讲。
4. 考勤逻辑:签到记录、时间窗与重复签到去重
4.1 数据库表设计:员工表、签到表和时间字段
人脸识别只是把“是谁”这个问题回答完了,考勤系统还要回答“什么时候来的、算不算迟到、今天打了几次卡”。这部分最好用 SQLite,标准库自带,不需要额外启数据库服务。
CREATE TABLE employees ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, department TEXT ); CREATE TABLE attendance ( id INTEGER PRIMARY KEY AUTOINCREMENT, employee_id INTEGER NOT NULL, check_time TEXT NOT NULL, status TEXT DEFAULT '正常', UNIQUE(employee_id, check_time), FOREIGN KEY (employee_id) REFERENCES employees(id) );attendance表里我特意加了UNIQUE(employee_id, check_time),防止同一秒插入两条重复记录。实际代码里,签到时间用datetime.now().strftime("%Y-%m-%d %H:%M:%S")存储,字符串格式化之后,后面判断迟到只需要比较串的前后顺序即可。如果你担心请假、加班等场景,可以再加一个attendance_type字段,日常考勤用不上就先不加,避免把表结构搞复杂。
“迟到”的判断不能写在 SQL 里,SQL 只负责存和查,业务规则放在 Python 层更清晰。常见做法是把考勤时间上限(比如 9:30)作为常量写在配置里,界面上的“统计考勤”按钮再统一跑一遍计算。
4.2 签到去重与迟到判断:同一个工号一天只打一次卡
考勤签到系统的第一原则是“一天只能有一个有效签到记录”。如果每识别到一次人脸就 insert 一条,某人坐工位上被摄像头扫到十次,就要删十条脏数据。我一般会在写入之前先查当天是否已有记录,没有才写入。
# core/database.py import sqlite3 from datetime import datetime def check_in(emp_id, name): today = datetime.now().strftime("%Y-%m-%d") now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") conn = sqlite3.connect("data/attendance.db") cur = conn.cursor() cur.execute( "SELECT id FROM attendance WHERE employee_id=? AND check_time LIKE ?", (emp_id, today + "%") ) if cur.fetchone() is not None: conn.close() return "already_checked_in" cur.execute( "INSERT INTO attendance (employee_id, check_time, status) VALUES (?, ?, ?)", (emp_id, now, "正常") ) conn.commit() conn.close() return "ok"这段代码的巧妙处在于用LIKE做日期前缀匹配,比DATE(check_time) = ?可读性好一些,也避免了格式化日期时遇到2025-05-01和2025-5-1不一致的麻烦。注意check_time的格式必须是四位年份、两位月份、两位日期、两位小时,字符串比较才可靠。如果项目用的是 24 小时制之外的显示格式,最好在写入时统一成 ISO 格式,后面所有统计都不用再做字符串转换。
迟到的判断,如果公司是 9:00 上班,我的建议是 9:00 之前正常,9:00 到 9:30 显示“迟到”,9:30 之后还没来那就别写签到,直接按缺勤处理。这一块和“只写一次”是配合的:第一次写入时就把状态算好,之后不再变更。
4.3 多帧确认减少误签:同一人脸连续多帧命中才写入
单帧识别结果直接写库,会被偶然的侧脸、遮挡、表情变化坑到。人脸识别器偶尔会把 A 识别成 B,置信度距离在阈值边缘抖动是常态。更稳的姿势是做一个滑动窗口,在最近 20 帧里同一 label 出现足够多的次数,才触发签到。
# core/vote_recognizer.py from collections import deque class VoteRecognizer: def __init__(self, vote_threshold=15, window_size=20): self.window = deque(maxlen=window_size) self.vote_threshold = vote_threshold def feed(self, label, confidence, conf_threshold=80): if confidence < conf_threshold: self.window.append(label) else: self.window.append(-1) if len(self.window) < self.window.maxlen: return -1 hits = self.window.count(label) if hits >= self.vote_threshold: self.window.clear() # 触发后清空,防止连续重复写库 return label return -1这个滑动窗口解决了一个很隐蔽的问题:如果员工在人脸识别器前站了 3 秒,每秒 10 帧,每一帧都返回同一个 label,那 30 次识别结果就会触发 30 次check_in()。虽然数据库有去重条件,但每次都查一次库也会拖慢界面响应。更可怕的是当员工离开后,另一个人出现在镜头里,如果滑动窗口没清空,旧 label 的票数可能还够,导致二次误签。所以触发调用check_in()之前,必须先清空窗口。
我建议把“写入考勤”这个动作单独放到vote_recognizer.feed()返回 label 之后,不要放在识别线程里。界面主线程收到 signal,再发起check_in(),这样线程和数据库之间是单向传递,排查问题也容易。如果你想让签到更严格,可以把窗口扩大到 30 帧、阈值设为 25;如果考勤点人流量大、排队打卡,窗口太大反而会让后面的人迟迟签不上,8 帧里命中 6 帧也是可用的配置。
5. 避坑指南:六个真实翻车现场与排查方法
5.1 ModuleNotFoundError: No module named 'cv2',或安装后仍报错
现象是执行import cv2时报ModuleNotFoundError: No module named 'cv2',但用 pip list 却能看到 opencv。原因无非两种:当前 Python 环境不是安装 opencv 的环境;或者装的是opencv-python,包名对应模块确实是 cv2,不会报这个错。解决时先执行python -c "import sys; print(sys.executable)"看解释器路径,再执行pip -V看 pip 指向哪里。如果路径不一致,激活正确的虚拟环境,或者用python -m pip install opencv-contrib-python强制装到当前解释器。另外,有些 IDE 自己虚拟了一个环境,终端里能 import,IDE 里不能,这种情况直接在 IDE 的终端里重装一次即可。
5.2 cv2.error: OpenCV(4.4.0) 窗口卡死或摄像头打不开
现象是程序崩溃,报一段cv2.error: OpenCV(4.4.0) C:\Users\...,常见于摄像头被其他程序占用的场景,也有可能是cap.read()返回False但代码没有处理。原因是 OpenCV 的 VideoCapture 在摄像头被占用时不会给友好提示,而是把错误堆栈抛出来。解决方法是启动摄像头前先cap.isOpened()判断,并且给摄像头释放加finally:
cap = cv2.VideoCapture(0) if not cap.isOpened(): raise RuntimeError("摄像头打开失败,请检查是否有其他程序占用")如果镜头画面是黑的,检查电脑摄像头权限,Windows 的“隐私设置”里经常会默认关闭摄像头访问。
5.3 界面卡死成白屏:摄像头循环放到了主线程
现象是 PyQt5 窗口能打开,但一启动识别就白屏,拖不动,任务管理器里 CPU 飙到 100%。原因是把cap.read()、人脸检测、预测全写在了 UI 线程的循环里。解决方法是把所有耗时操作搬进QThread,主线程只负责接收信号画界面。经验是,UI 线程里不能有任何超过 50ms 的操作,否则用户感觉到的就是卡顿。改成线程后还要注意一个细节:在线程run()里不要直接操作 QLabel 或 QTableWidget,这些控件不是线程安全的,必须通过pyqtSignal把结果传回主线程再更新。
5.4 全员识别成同一个人:训练样本太少或误把背景当人脸
现象是无论谁站到摄像头前,识别结果全部显示 A 的姓名,或者几乎每次都返回 -1。原因大概率有两个:样本量不够,每个员工只存了 5 张照片,LBPH 把所有人往同一类上拟合;还有一种可能是采集样本时把非人脸区域也保存了,训练集里大量“空脸”,模型被污染。解决方法是每个工号至少 30 张,且保证图像里有完整的眉毛、眼睛、鼻子区域;训练前在采集脚本里打印检测框的宽高,如果框明显小于真实人脸,说明minSize设小了,把背景框进来是常见误操作。训练完成后,立刻对着摄像头做一次自测,把 confidence 打出来,低于 80 才算基本合格。
5.5 中文路径读不到样本:cv2.imread 的编码问题
现象是程序在 Linux 上跑得好好的,换到 Windows 后,cv2.imread("data/样本/10001/1.jpg")直接返回 None。原因是 OpenCV 的imread在 Windows 下对中文路径支持很差,底层用的是系统编码。解决办法是用np.fromfile读取字节再交给cv2.imdecode,写文件时用cv2.imencode:
def imread_unicode(path): data = np.fromfile(path, dtype=np.uint8) return cv2.imdecode(data, cv2.IMREAD_COLOR) def imwrite_unicode(path, img): ext = os.path.splitext(path)[1] ok, buffer = cv2.imencode(ext, img) if ok: buffer.tofile(path)考勤系统里员工姓名经常就是中文,如果数据库和文件夹都用工号,问题不大;但一旦涉及“按姓名导出考勤表”,文件路径里的中文名就会触发这个坑。我建议整个项目绝对路径下不要有任何中文目录,所有员工文件夹只用数字工号,姓名只存在数据库和界面显示层。
5.6 模型文件读取后 predict 报错:OpenCV 版本或标签类型不匹配
现象是训练完成后写出的face_model.yml,换台电脑读取时报TypeError或cv2.error。原因可能是训练时标签数组用了 Python 列表而不是 numpy 数组;LBPH 在 OpenCV 4.x 要求标签必须是np.int32。训练脚本里必须写labels = np.array(labels, dtype=np.int32)。另外,recognizer.read(model_path)之后最好调用一次recognizer.setThreshold(80),因为 yml 文件里不一定保存了 threshold,不设置的话会用默认阈值,预测结果可能完全不是你想要的效果。模型跟随源码一起分发时,要注意 OpenCV 的大版本必须一致,4.5 训练出来的 yml 在 4.4 上有时可以读,有时会报 “The NodeType” 之类的错误,最好在 requirements.txt 里锁定版本号。
6. 从能签到到考勤可用:准确率验证与三个实用改进
6.1 先给识别器出一份成绩单:在测试集上算准确率
不要凭“感觉挺准”就上线。我会先从每个员工手里额外采集 10 张不参与训练的照片,单独放到data/test/工号/目录,然后跑一段验证脚本:
import glob, cv2, numpy as np recognizer = cv2.face.LBPHFaceRecognizer_create() recognizer.read("data/models/face_model.yml") recognizer.setThreshold(80) correct = 0 total = 0 for path in glob.glob("data/test/*/*.jpg"): true_label = int(path.split("/")[-2]) img = cv2.imread(path, cv2.IMREAD_GRAYSCALE) img = cv2.resize(img, (160, 160)) pred_label, conf = recognizer.predict(img) total += 1 if pred_label == true_label and conf < 80: correct += 1 print(f"准确率: {correct / total:.2f}")准确率低于 0.90 时,别急着改界面,先把样本数和光照条件补起来。我还会打印所有预测失败的 confidence 分布,如果失败样本的 confidence 普遍在 100 到 120 之间,就说明阈值 80 太严,可以放松到 95;如果失败的样本 confidence 只有 60 却依然预测错,说明模型本身就分不清两个员工,加样本比调阈值更管用。
6.2 三个低成本改进:换识别器、导出 Excel、增量训练
想让识别效果向人脸识别门禁机靠近,最直接的办法是把 LBPH 换成face_recognition库或 ArcFace 的 Python SDK。LBPH 在正脸、均匀光照下够用,但侧脸、遮挡、暗光环境确实不稳定。face_recognition底层是深度学习模型,使用门槛不高,只是人脸向量维度更高、比对耗时稍长。对考勤这种低频场景,完全可以承担。如果追求更高精度,再考虑 ArcFace,不过那就需要 GPU 或者至少 CPU 推理库了。
导出 Excel 是让行政人员愿意用这个系统的关键。考勤数据躺在 SQLite 里,很多人不会写 SQL。加一个按钮,用 pandas 读取attendance表,按月透视成表格:
import pandas as pd df = pd.read_sql("SELECT * FROM attendance", sqlite3.connect("data/attendance.db")) pivot = df.pivot_table(index="employee_id", columns="check_time", values="status", aggfunc="first") pivot.to_excel("考勤统计.xlsx")增量训练解决的是“老员工模型里没有新员工”的问题。新员工入职,只采集他的样本,然后加载已有模型继续train(),最后write()回同一个文件,而不是把所有老照片重新训练。注意 OpenCV 的 LBPH 在同一实例上连续train()会累积新数据,但 OpenCV 4.5 有个坑:新训练的数据和旧模型合并时会降低旧员工的特征权重,所以新员工样本最好一次采集 50 张以上。
6.3 我最后的习惯:先看日志,再调阈值,最后改界面
最后一次给同事做考勤系统时,我把阈值默认设成了 150,结果所有人互相串脸,场面非常尴尬。后来我养成了一个习惯:所有识别结果先写入日志,包括 label、confidence、时间、摄像头帧号,跑一天再统计日志里的 distribution。这样调参数时有数据依据,而不是靠肉眼“好像准了”。阈值、滑动窗口大小、检测的minNeighbors,每改一个参数就重新跑一遍测试集。界面永远放最后改,因为数据不稳定的系统,界面再好看也没用。
做这类基于 python+opencv 人脸识别的考勤签到系统,最大的成就感不是它识别得多准,而是它稳定地跑了三个月,没人抱怨打卡记录对不上。希望你也能先跑通最小闭环,再盯准参数和日志,把它做成真正敢交给行政用的工具。希望帮到你。
本文还有配套的精品资源,点击获取