☰
OpenCV人脸识别考勤系统实战:从LBPH训练到活体检测避坑指南
2026/10/1 14:05:27 网站建设 项目流程

简介:这是一份面向计算机相关专业学生与项目实战学习者的OpenCV人脸识别考勤系统完整资料,可直接用于毕业设计、课程设计或期末大作业。项目围绕图像采集、人脸检测、特征提取与人脸匹配四个核心环节展开,涉及灰度化、直方图均衡化等预处理手段,以及Eigenfaces、Fisherfaces、LBPH等经典特征提取算法,并配有详细文档说明各功能模块的设计原理与实现思路。压缩包共123个文件,以100张jpg人脸样本图像为主,另含6个xml分类器配置、5个py源码脚本、2个exe可执行程序及bat批处理、yml、cfg等环境与运行配置文件,整体约1.88MB,结构清晰便于按模块查阅。目前已有48人学习关注。项目经过严格调试,可直接运行,读者既能获得完整源码与文档,也能借此理解人脸识别算法的落地流程,并针对光照变化、姿态表情差异及性能优化等问题积累排错与调优经验。

1. 从一台打卡机说起:OpenCV 人脸识别考勤系统到底难在哪

很多公司前台那台打卡机,员工走过去刷脸,屏幕上跳出名字和工号,后台自动记一条时间戳。看起来简单,但真让你用 OpenCV 从零搭一套人脸识别考勤系统,你会发现难点根本不在「识别」两个字上。识别本身有现成的人脸检测器和特征提取器,几行代码就能跑通。真正让人翻车的是:同一个人早上戴眼镜、中午摘了、下午换了发型,系统还认不认得出;光线从窗户斜射进来,半边脸过曝,检测框直接飘走;两个人长得像,阈值设松了互相冒名,设紧了本人打不上卡。

这套系统的本质,是一条从摄像头取帧、人脸检测、特征提取、比对识别、到考勤记录落库的完整流水线。OpenCV 在其中承担图像采集、预处理、检测和部分特征计算的工作,识别环节通常搭配 face_recognition、dlib 或 OpenCV 自带的 LBPH 算法。适合谁做?适合有 Python 基础、想拿一个完整项目练手图像处理和后端逻辑的开发者,也适合需要给小型团队搭一套低成本考勤方案的技术负责人。源码和详细文档的价值在于,它把这条流水线上每个环节的参数和边界都固定下来了,你不需要从论文开始啃,照着调就能跑。

2. 人脸识别考勤系统的技术选型:为什么是 OpenCV 加 LBPH 而不是深度学习

2.1 三条技术路线的真实成本对比

做考勤系统的人脸识别,市面上常见三条路:OpenCV 自带的 LBPH 算法、dlib 的 ResNet 特征提取、以及基于深度学习的人脸识别模型。三条路我都跑过,说下真实感受。

LBPH 是 OpenCV contrib 模块里的局部二值模式直方图算法。它的原理是把人脸图像分成若干小区域,在每个区域内计算像素的局部二值模式,然后统计直方图作为特征。优点是训练极快,新增人员只需要把新的人脸图片加进去重新训练,几秒钟的事。缺点是对光照和姿态变化比较敏感,同一个人不同角度拍的照片,特征差异可能比不同人还大。

dlib 的 ResNet 模型输出 128 维特征向量,通过计算欧氏距离判断是否同一个人。这个方案精度明显高于 LBPH,对光照和角度有一定鲁棒性。代价是需要编译 dlib,在 Windows 上编译经常卡在 CMake 和 Visual Studio 版本匹配上,很多人在这里就放弃了。

深度学习方案精度最高,但需要 GPU 推理,部署成本直接上一个台阶。对于几十人到几百人规模的公司考勤,说实话用不上。

我一般会推荐 LBPH 起步,原因是考勤场景有个天然优势:同一个人每天在同一个摄像头前打卡,角度和光照相对固定。LBPH 在这个约束下表现完全够用,而且部署零依赖,pip install opencv-contrib-python 就能跑。

方案精度训练速度部署难度适合规模
OpenCV LBPH中等秒级极低50 人以内
dlib ResNet较高分钟级中等500 人以内
深度学习模型高小时级高不限

2.2 环境搭建与依赖安装的完整命令

先把环境跑通。Python 版本建议 3.8 到 3.10,太新的版本有些包还没跟上。

# 创建虚拟环境,避免污染系统 Python python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # macOS/Linux: source venv/bin/activate # 安装核心依赖 pip install opencv-contrib-python==4.8.1.78 pip install opencv-python==4.8.1.78 pip install numpy==1.24.3 pip install Pillow==10.0.0 pip install pandas==2.0.3 # 如果要用 dlib 方案,额外安装 # pip install dlib==19.24.2 # pip install face-recognition==1.3.0

这里有个坑要注意:opencv-contrib-python 和 opencv-python 不能同时装不同版本,否则会互相覆盖。我一般只装 opencv-contrib-python,它包含了主模块和 contrib 模块,LBPH 就在 contrib 里。如果你之前装过 opencv-python,先 pip uninstall 掉再装 contrib 版本。

安装完之后验证一下:

import cv2 import numpy as np # 检查 OpenCV 版本 print(f"OpenCV version: {cv2.__version__}") # 检查 LBPH 识别器是否可用 try: recognizer = cv2.face.LBPHFaceRecognizer_create() print("LBPH recognizer: OK") except AttributeError: print("LBPH recognizer: NOT FOUND - 请安装 opencv-contrib-python") # 检查人脸检测器 cascade_path = cv2.data.haarcascades + 'haarcascade_frontalface_default.xml' detector = cv2.CascadeClassifier(cascade_path) print(f"Haar cascade loaded: {not detector.empty()}")

这段代码做了三件事:打印 OpenCV 版本确认安装成功,尝试创建 LBPH 识别器确认 contrib 模块可用,加载 Haar 级联检测器确认人脸检测功能正常。如果 LBPH 那行报 AttributeError,说明你装的是 opencv-python 而不是 opencv-contrib-python,卸载重装即可。

2.3 人脸检测器的选择与参数调优

OpenCV 自带的人脸检测器有几种:Haar 级联、LBP 级联、DNN 模块。考勤场景我一般用 Haar 级联,因为它在正面人脸检测上召回率不错,速度也快。DNN 检测器精度更高但需要额外下载模型文件,部署时多一个依赖。

Haar 检测器的关键参数有三个:

import cv2 detector = cv2.CascadeClassifier( cv2.data.haarcascades + 'haarcascade_frontalface_default.xml' ) cap = cv2.VideoCapture(0) # 0 表示默认摄像头 while True: ret, frame = cap.read() if not ret: break gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # detectMultiScale 的三个核心参数 faces = detector.detectMultiScale( gray, scaleFactor=1.1, # 每次图像缩小的比例 minNeighbors=5, # 每个候选框需要被检测多少次才保留 minSize=(80, 80) # 最小人脸尺寸 ) for (x, y, w, h) in faces: cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2) cv2.imshow('Face Detection', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()

scaleFactor 控制图像金字塔的缩放步长,1.1 表示每次缩小 10%。设小了检测更细致但速度慢,设大了速度快但可能漏掉小脸。minNeighbors 是投票机制,值越高误检越少但漏检越多,考勤场景建议 5 到 6。minSize 根据摄像头到人的距离调,一般 80x80 起步,如果人站得远就调小到 50x50。

3. 从拍照到训练:人脸数据采集与 LBPH 模型训练全流程

3.1 人脸样本采集脚本与质量把控

数据采集是整套系统里最容易被忽视但最影响效果的环节。我见过太多人随便拍几张就训练,结果识别率惨不忍睹。采集时要注意:每人至少 30 张,不同角度(左转、右转、抬头、低头各来几张),不同表情(正常、微笑、严肃),光照尽量均匀。

import cv2 import os def collect_faces(person_id, person_name, save_dir='dataset', num_samples=50): """ 采集指定人员的人脸样本 person_id: 人员编号,用于文件命名 person_name: 人员姓名 save_dir: 样本保存目录 num_samples: 采集数量 """ # 创建保存目录 person_dir = os.path.join(save_dir, f"{person_id}_{person_name}") os.makedirs(person_dir, exist_ok=True) detector = cv2.CascadeClassifier( cv2.data.haarcascades + 'haarcascade_frontalface_default.xml' ) cap = cv2.VideoCapture(0) count = 0 print(f"开始采集 {person_name} 的人脸样本,按空格键拍照,按 q 退出") while count < num_samples: ret, frame = cap.read() if not ret: break gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = detector.detectMultiScale(gray, 1.1, 5, minSize=(80, 80)) for (x, y, w, h) in faces: cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2) # 在框上方显示已采集数量 cv2.putText(frame, f"Count: {count}/{num_samples}", (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow('Collect Faces', frame) key = cv2.waitKey(1) & 0xFF if key == ord(' ') and len(faces) == 1: # 只采集一张人脸的情况,避免多人入镜 (x, y, w, h) = faces[0] # 保存灰度人脸区域,统一缩放到 200x200 face_img = cv2.resize(gray[y:y+h, x:x+w], (200, 200)) file_path = os.path.join(person_dir, f"{count}.jpg") cv2.imwrite(file_path, face_img) count += 1 print(f"已采集 {count}/{num_samples}") elif key == ord('q'): break cap.release() cv2.destroyAllWindows() print(f"{person_name} 采集完成,共 {count} 张") # 使用示例 collect_faces(person_id=1, person_name="zhangsan", num_samples=50)

这个脚本的逻辑是:打开摄像头,实时检测人脸并画框,按空格键保存当前人脸区域。保存时统一缩放到 200x200 灰度图,这是 LBPH 训练的标准输入尺寸。注意只采集单张人脸的情况,如果画面里有多人就不保存,避免样本污染。

采集完成后,dataset 目录下会按人员分文件夹存放,每个文件夹里是编号的 jpg 文件。这个结构方便后续训练脚本遍历。

3.2 LBPH 模型训练与参数含义

样本采集完就可以训练了。LBPH 的训练过程很快,核心是把每张人脸图片转换成 LBP 直方图特征,然后存成一个模型文件。

import cv2 import numpy as np import os def train_lbph_model(dataset_dir='dataset', model_path='trainer.yml'): """ 训练 LBPH 人脸识别模型 dataset_dir: 样本目录,结构为 dataset/人员ID_姓名/编号.jpg model_path: 模型保存路径 """ recognizer = cv2.face.LBPHFaceRecognizer_create( radius=1, # LBP 圆形邻域半径 neighbors=8, # 邻域像素点数量 grid_x=8, # 水平方向划分的网格数 grid_y=8, # 垂直方向划分的网格数 threshold=100.0 # 识别阈值,距离大于此值判为未知 ) faces = [] labels = [] # 遍历数据集目录 for person_dir in os.listdir(dataset_dir): person_path = os.path.join(dataset_dir, person_dir) if not os.path.isdir(person_path): continue # 从文件夹名解析人员 ID,格式为 "1_zhangsan" person_id = int(person_dir.split('_')[0]) for img_name in os.listdir(person_path): img_path = os.path.join(person_path, img_name) img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) if img is None: continue # 统一尺寸 img = cv2.resize(img, (200, 200)) faces.append(img) labels.append(person_id) print(f"共加载 {len(faces)} 张人脸样本,{len(set(labels))} 个人员") # 训练 recognizer.train(faces, np.array(labels)) recognizer.save(model_path) print(f"模型已保存到 {model_path}") return recognizer # 执行训练 train_lbph_model()

LBPH 的五个参数需要理解一下。radius 和 neighbors 控制 LBP 算子的感受野,radius=1、neighbors=8 是经典配置,对应 3x3 邻域。grid_x 和 grid_y 把人脸图分成 8x8 的网格,每个网格单独计算直方图,这样保留了空间信息,比整图一个直方图效果好很多。threshold 是识别阈值,预测时返回的距离大于这个值就判定为未知人员,设太小会导致本人被拒,设太大会导致冒名通过。

训练完成后会生成 trainer.yml 文件,里面存的是每个人脸的特征直方图和对应标签。这个文件通常几百 KB 到几 MB,取决于人员数量和样本数量。

3.3 识别与考勤记录写入的联动逻辑

训练好模型后,识别脚本要做三件事:检测人脸、预测身份、写考勤记录。

import cv2 import numpy as np import csv from datetime import datetime import os class AttendanceSystem: def __init__(self, model_path='trainer.yml', names_file='names.csv'): self.recognizer = cv2.face.LBPHFaceRecognizer_create() self.recognizer.read(model_path) self.detector = cv2.CascadeClassifier( cv2.data.haarcascades + 'haarcascade_frontalface_default.xml' ) # 加载人员 ID 到姓名的映射 self.id_to_name = {} if os.path.exists(names_file): with open(names_file, 'r', encoding='utf-8') as f: reader = csv.reader(f) for row in reader: if len(row) >= 2: self.id_to_name[int(row[0])] = row[1] # 考勤记录文件 self.attendance_file = 'attendance.csv' if not os.path.exists(self.attendance_file): with open(self.attendance_file, 'w', newline='', encoding='utf-8') as f: writer = csv.writer(f) writer.writerow(['姓名', '日期', '时间', '状态']) # 防止同一人短时间内重复打卡 self.last_seen = {} def mark_attendance(self, name): """写入考勤记录,5 分钟内不重复""" now = datetime.now() today = now.strftime('%Y-%m-%d') time_str = now.strftime('%H:%M:%S') # 检查是否重复打卡 key = f"{name}_{today}" if key in self.last_seen: last_time = self.last_seen[key] if (now - last_time).total_seconds() < 300: return False self.last_seen[key] = now # 判断迟到:9 点之后算迟到 status = '正常' if now.hour < 9 else '迟到' with open(self.attendance_file, 'a', newline='', encoding='utf-8') as f: writer = csv.writer(f) writer.writerow([name, today, time_str, status]) print(f"考勤记录:{name} {today} {time_str} {status}") return True def run(self): cap = cv2.VideoCapture(0) while True: ret, frame = cap.read() if not ret: break gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = self.detector.detectMultiScale(gray, 1.1, 5, minSize=(80, 80)) for (x, y, w, h) in faces: face_roi = cv2.resize(gray[y:y+h, x:x+w], (200, 200)) # 预测 label, confidence = self.recognizer.predict(face_roi) if confidence < 100: name = self.id_to_name.get(label, f"未知_{label}") color = (0, 255, 0) self.mark_attendance(name) else: name = "未知" color = (0, 0, 255) # 画框和标签 cv2.rectangle(frame, (x, y), (x+w, y+h), color, 2) cv2.putText(frame, f"{name} ({confidence:.1f})", (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) cv2.imshow('Attendance System', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows() # 运行 system = AttendanceSystem() system.run()

这段代码的核心逻辑:predict 方法返回两个值,label 是预测的人员 ID,confidence 是距离值。注意 LBPH 的 confidence 是距离,越小越可信,和一般分类器的置信度含义相反。我设的阈值是 100,低于 100 认为是已知人员,高于 100 判为未知。这个阈值需要根据实际数据调,后面会讲怎么调。

mark_attendance 方法做了防重复打卡,同一人 5 分钟内只记一次。考勤状态简单按 9 点判断迟到,实际项目里可以配置多个时间段。

4. 识别率上不去?人脸识别考勤系统避坑与排查清单

4.1 识别率忽高忽低,先查光照和摄像头

现象:同一个人有时候秒识别,有时候反复打不上卡,confidence 值波动很大。

原因:LBPH 对光照变化敏感。如果摄像头正对窗户,上午和下午的光线方向不同,人脸阴影分布变化,LBP 特征跟着变。另外摄像头自动曝光如果频繁调整,也会导致图像亮度不稳定。

解决:固定摄像头位置和角度,避免逆光。如果无法避免,在检测前做直方图均衡化:

# 在灰度转换后加一步 gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) gray = cv2.equalizeHist(gray) # 直方图均衡化,缓解光照不均

另外把摄像头的自动曝光关掉,手动设一个固定值。OpenCV 里可以用 cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0.25) 尝试关闭自动曝光,具体值取决于摄像头驱动。

4.2 训练时样本不够,新增人员后旧人员识别变差

现象:新加了一个人重新训练后,原来能识别的人开始频繁报未知。

原因:LBPH 训练时所有样本一起参与,如果新人的样本数量远多于旧人,或者新人样本质量差(模糊、侧脸多),会影响整体模型的直方图分布。另外如果训练时没有固定随机种子,每次训练结果会有微小差异。

解决:保证每个人样本数量均衡,建议每人 30 到 50 张。新增人员时不要只加新人样本,把旧人样本一起重新训练。训练前检查每张样本的质量,模糊的、检测框偏移的删掉。我一般会写个脚本先过一遍样本,把尺寸不对的、全黑的、全白的筛掉。

4.3 confidence 阈值设多少合适,怎么科学地调

现象:阈值设 80 时本人经常被拒,设 120 时偶尔有两个人互相认错。

原因:LBPH 的 confidence 是距离值,不同数据集的距离分布不同,没有一个通用阈值。样本质量好、人员之间差异大,距离整体偏低;样本模糊、人员之间长得像,距离整体偏高。

解决:用验证集来调。把每个人的样本留出 5 张不参与训练,训练完后用这 5 张做预测,记录本人预测的距离分布,再拿其他人的样本做预测,记录非本人的距离分布。找一个能分开这两个分布的值作为阈值。

# 阈值调优脚本 import cv2 import numpy as np import os def evaluate_threshold(model_path, test_dir): recognizer = cv2.face.LBPHFaceRecognizer_create() recognizer.read(model_path) genuine_distances = [] # 本人预测距离 impostor_distances = [] # 非本人预测距离 for person_dir in os.listdir(test_dir): person_path = os.path.join(test_dir, person_dir) if not os.path.isdir(person_path): continue true_id = int(person_dir.split('_')[0]) for img_name in os.listdir(person_path): img = cv2.imread(os.path.join(person_path, img_name), 0) if img is None: continue img = cv2.resize(img, (200, 200)) label, confidence = recognizer.predict(img) if label == true_id: genuine_distances.append(confidence) else: impostor_distances.append(confidence) print(f"本人距离:均值 {np.mean(genuine_distances):.1f}," f"最大 {np.max(genuine_distances):.1f}") print(f"非本人距离:均值 {np.mean(impostor_distances):.1f}," f"最小 {np.min(impostor_distances):.1f}") # 建议阈值取本人最大值和非本人最小值的中点 suggested = (np.max(genuine_distances) + np.min(impostor_distances)) / 2 print(f"建议阈值:{suggested:.1f}") evaluate_threshold('trainer.yml', 'test_dataset')

这个脚本分别统计本人和非本人的预测距离分布,取本人最大值和非本人最小值的中点作为建议阈值。如果两个分布有重叠,说明样本区分度不够,需要增加样本或改善采集条件。

4.4 摄像头读帧卡顿导致识别延迟

现象:画面明显卡顿,人站在摄像头前要等两三秒才出识别结果。

原因:detectMultiScale 是计算密集型操作,如果每帧都做全图检测,分辨率高的时候帧率会掉到个位数。另外如果识别和显示在同一个线程里,识别耗时直接拖慢画面刷新。

解决:降低检测频率,比如每 3 帧检测一次,中间帧沿用上一次的检测框。或者先把图像缩小再检测,检测到人脸后再在原图上裁剪。

frame_count = 0 last_faces = [] while True: ret, frame = cap.read() if not ret: break frame_count += 1 # 每 3 帧做一次检测 if frame_count % 3 == 0: gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 缩小到一半再检测,速度提升明显 small_gray = cv2.resize(gray, (0, 0), fx=0.5, fy=0.5) faces_small = detector.detectMultiScale(small_gray, 1.1, 5, minSize=(40, 40)) # 坐标还原到原图尺寸 last_faces = [(x*2, y*2, w*2, h*2) for (x, y, w, h) in faces_small] for (x, y, w, h) in last_faces: cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2) cv2.imshow('Optimized', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break

缩小到一半再检测,计算量降到四分之一,检测框坐标乘以 2 还原即可。配合每 3 帧检测一次,帧率能从个位数拉到 20 以上。

4.5 打包部署后摄像头打不开

现象:开发环境跑得好好的,打包成 exe 或者部署到另一台机器后,VideoCapture(0) 返回空。

原因:常见三种情况。一是目标机器没有摄像头或者摄像头被其他程序占用。二是 OpenCV 的摄像头后端在 Windows 上默认用 MSMF,某些摄像头驱动不兼容,需要切到 DSHOW。三是打包工具没有把 OpenCV 的依赖 dll 打进去。

解决:先换后端试试:

# Windows 上优先尝试 DSHOW 后端 cap = cv2.VideoCapture(0, cv2.CAP_DSHOW) # 如果还不行,列出可用后端逐个试 for backend in [cv2.CAP_DSHOW, cv2.CAP_MSMF, cv2.CAP_ANY]: cap = cv2.VideoCapture(0, backend) if cap.isOpened(): print(f"可用后端:{backend}") break

打包用 PyInstaller 时,加 --add-data 把 cv2.data.haarcascades 目录带上,否则 Haar 模型文件找不到。

5. 让考勤系统真正能用:活体检测与多线程的进阶技巧

5.1 用眨眼检测挡住照片冒名

LBPH 方案有个致命问题:拿一张照片对着摄像头也能识别通过。考勤场景必须加活体检测。最简单的活体检测是眨眼检测,用 OpenCV 的级联分类器检测眼睛,连续帧里眼睛从有到无再到有,判定为眨眼。

import cv2 eye_cascade = cv2.CascadeClassifier( cv2.data.haarcascades + 'haarcascade_eye.xml' ) def detect_blink(eye_history, current_eyes): """ eye_history: 最近几帧的眼睛检测结果列表 current_eyes: 当前帧检测到的眼睛数量 """ eye_history.append(current_eyes) if len(eye_history) > 10: eye_history.pop(0) # 检测到眼睛数量从 >=2 变成 0 再变回 >=2 if len(eye_history) >= 6: mid = len(eye_history) // 2 before = eye_history[:mid] after = eye_history[mid:] had_eyes = any(e >= 2 for e in before) lost_eyes = any(e == 0 for e in before[mid-2:mid+2] if mid-2 >= 0) has_eyes_again = any(e >= 2 for e in after) if had_eyes and lost_eyes and has_eyes_again: return True return False

这个逻辑是维护一个最近 10 帧的眼睛数量队列,检测「有眼睛 → 没眼睛 → 有眼睛」的模式。实际用的时候,识别到人脸后先要求用户眨眼,眨眼确认后才写入考勤记录。这样照片就过不了。

5.2 多线程架构让识别和界面互不阻塞

单线程方案里,识别一帧要几十毫秒,界面就卡几十毫秒。人多的时候体验很差。改成生产者-消费者模式:一个线程专门读摄像头帧放进队列,一个线程从队列取帧做识别,主线程负责显示。

import threading import queue import cv2 import time class VideoStream: """独立线程读取摄像头帧""" def __init__(self, src=0): self.cap = cv2.VideoCapture(src, cv2.CAP_DSHOW) self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) self.q = queue.Queue(maxsize=2) # 只保留最新两帧 self.running = True self.thread = threading.Thread(target=self._reader, daemon=True) self.thread.start() def _reader(self): while self.running: ret, frame = self.cap.read() if not ret: break # 队列满了就丢掉旧帧,保证实时性 if self.q.full(): try: self.q.get_nowait() except queue.Empty: pass self.q.put(frame) def read(self): return self.q.get() def release(self): self.running = False self.cap.release() # 使用 stream = VideoStream(0) while True: frame = stream.read() # 识别逻辑放在这里,不阻塞读帧 cv2.imshow('Multi-thread', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break stream.release()

队列 maxsize 设为 2,保证永远处理最新帧。如果识别速度跟不上,旧帧自动丢弃,不会累积延迟。这个架构下界面始终流畅,识别结果晚一两帧出来对考勤没影响。

5.3 考勤数据落库与导出

CSV 适合小规模,人多了还是上 SQLite。建一张考勤表,加唯一索引防止重复。

import sqlite3 from datetime import datetime def init_db(db_path='attendance.db'): conn = sqlite3.connect(db_path) cursor = conn.cursor() cursor.execute(''' CREATE TABLE IF NOT EXISTS attendance ( id INTEGER PRIMARY KEY AUTOINCREMENT, person_id INTEGER NOT NULL, person_name TEXT NOT NULL, check_date TEXT NOT NULL, check_time TEXT NOT NULL, status TEXT DEFAULT '正常', UNIQUE(person_id, check_date) ) ''') conn.commit() return conn def record_attendance(conn, person_id, person_name): now = datetime.now() date_str = now.strftime('%Y-%m-%d') time_str = now.strftime('%H:%M:%S') status = '正常' if now.hour < 9 else '迟到' cursor = conn.cursor() try: cursor.execute(''' INSERT INTO attendance (person_id, person_name, check_date, check_time, status) VALUES (?, ?, ?, ?, ?) ''', (person_id, person_name, date_str, time_str, status)) conn.commit() return True except sqlite3.IntegrityError: # 唯一索引冲突,说明今天已经打过卡 return False

UNIQUE(person_id, check_date) 保证每人每天只有一条记录,重复打卡直接触发 IntegrityError,不用在应用层做判断。导出的时候一个 SQL 查询就能出月度报表。

这套方案我从采集、训练、识别到落库完整跑过,最深的教训是:别在样本质量上偷懒。我最早做的时候每人只拍了 10 张,全是正面正常表情,结果有个同事戴了副新眼镜就认不出来了。后来老老实实每人拍 50 张,覆盖各种角度和表情,识别率从 70% 拉到 95% 以上。活体检测也千万别省,我见过有人拿手机翻拍照片成功打卡的,这在考勤场景是致命的。希望帮到你。

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

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

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

立即咨询