OpenCV人脸识别考勤系统实战:从环境搭建到防代打卡

发布时间:2026/9/23 21:57:28
OpenCV人脸识别考勤系统实战:从环境搭建到防代打卡 简介这份资源是面向高校学生与Python初学者的OpenCV人脸识别考勤系统完整项目源码适合作为课程设计、期末大作业或自学计算机视觉的实战参考。项目围绕考勤管理场景实现了用户注册登录、人脸检测与识别、打卡记录及数据查询等核心模块并借助dlib模型完成特征提取与比对。压缩包共28个文件约116.61MB其中15个Python源文件承载主要业务逻辑7个PNG图片用于界面与流程展示另有2个dat模型数据文件、2个txt说明文档及gitignore等配置项目录按attendanceSystem、utils、db、config等模块划分结构清晰。目前已有276人学习下载。通过研读源码读者可掌握人脸检测、特征点定位、数据库操作与日志记录等实现思路并理解从注册到打卡的完整业务流程便于二次开发与功能扩展。1. 从一张摄像头截图到考勤记录OpenCV 人脸识别考勤系统到底怎么落地很多团队做考勤第一反应是上指纹机或者刷卡但实际用起来问题不少指纹磨损打不上卡、代打卡屡禁不止、设备成本高。用 OpenCV 加 Python 做一套人脸识别考勤系统本质上是把普通 USB 摄像头变成一台考勤终端——员工走到摄像头前系统自动识别身份并写入考勤记录。这套方案适合中小团队、实验室、教室这类人数在几十到几百人的场景硬件成本可以压到一台普通电脑加一个摄像头。核心链路其实就四步人脸检测、人脸编码、比对识别、记录落库。OpenCV 负责前两步的底层图像处理Python 负责调度和业务逻辑。源码层面整套系统通常包含注册模块、识别模块、考勤记录模块和后台管理模块。下面按实际搭建顺序把每个环节的参数、代码和坑讲清楚。2. 环境搭建与 OpenCV 安装别在第一步就翻车2.1 Python 环境选择与 OpenCV 安装的三种方式环境搭建是这套系统最容易劝退人的地方。我见过太多人卡在ModuleNotFoundError: No module named opencv上折腾半天最后放弃。先说结论推荐用 Anaconda 创建独立虚拟环境再用 pip 装 opencv-python不要直接往系统 Python 里塞。为什么不用conda install opencv因为 conda 源里的 OpenCV 版本往往滞后而且和opencv-contrib-python的兼容性经常出问题。人脸识别需要的face模块在 contrib 包里conda 装 contrib 版本更容易踩版本冲突的坑。# 创建虚拟环境Python 版本建议 3.8 到 3.10 conda create -n face_attendance python3.9 -y conda activate face_attendance # 安装核心依赖opencv-contrib-python 包含 face 模块 pip install opencv-contrib-python4.8.1.78 pip install opencv-python4.8.1.78 pip install numpy1.24.3 pip install Pillow10.0.0 pip install openpyxl3.1.2这里解释一下参数和版本选择。opencv-contrib-python和opencv-python版本号必须一致否则会出现cv2.face找不到的玄学问题。numpy 锁在 1.24.x 是因为 1.25 以上和部分 OpenCV 版本存在 ABI 不兼容表现为导入 cv2 直接段错误。Pillow 用于处理中文标签绘制openpyxl 用于导出考勤表。安装完成后验证import cv2 import numpy as np print(cv2.__version__) # 确认 face 模块可用 print(hasattr(cv2, face)) # 确认数据模块可用 print(hasattr(cv2, data))如果hasattr(cv2, face)返回 False说明装的是opencv-python而不是 contrib 版本卸载重装即可。如果返回 True 但调用时报错检查 numpy 版本。提示Linux 环境下如果要用 CUDA 加速需要从源码编译 OpenCV步骤较多普通考勤场景用 CPU 推理完全够用不必折腾。2.2 摄像头选型与采集参数配置摄像头这块我一般推荐罗技 C920 或同级别的 1080P USB 摄像头帧率 30fps 足够。不要用笔记本自带摄像头做正式部署角度和画质都不稳定。采集参数直接影响识别率核心是分辨率和曝光。分辨率建议 640x480 用于检测1280x720 用于注册照片采集。太高的分辨率会拖慢检测速度太低则人脸区域像素不足。import cv2 def open_camera(index0, width640, height480): cap cv2.VideoCapture(index) # 设置采集分辨率 cap.set(cv2.CAP_PROP_FRAME_WIDTH, width) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height) # 关闭自动曝光避免光线变化导致识别不稳定 cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0.25) cap.set(cv2.CAP_PROP_EXPOSURE, -4) if not cap.isOpened(): raise RuntimeError(f摄像头 {index} 打开失败) return capCAP_PROP_AUTO_EXPOSURE设为 0.25 是关闭自动曝光的常见值不同摄像头驱动可能不同需要实测。曝光值 -4 是经验值光线暗就调大光线强就调小。这一步不做后面识别率会随环境光变化剧烈波动。2.3 人脸检测器选型Haar 还是 DNNOpenCV 自带的人脸检测方案主要有两种Haar 级联和 DNN 模块。很多人一上来就用 Haar因为代码简单但 Haar 的误检率和漏检率在实际考勤场景下很难接受。检测方案速度准确率侧脸支持推荐场景Haar 级联快中差快速原型验证DNN SSD中高较好正式考勤部署DNN YuNet快高好推荐方案YuNet 是 OpenCV 5.x 之后内置的轻量人脸检测模型速度和准确率平衡得很好。使用前需要下载模型文件但不需要额外安装深度学习框架。import cv2 # 加载 YuNet 人脸检测模型 detector cv2.FaceDetectorYN.create( modelface_detection_yunet_2023mar.onnx, config, input_size(320, 320), score_threshold0.9, nms_threshold0.3, top_k5000 ) def detect_faces(frame): h, w frame.shape[:2] detector.setInputSize((w, h)) _, faces detector.detect(frame) return faces if faces is not None else []score_threshold设为 0.9 是过滤低置信度检测考勤场景宁可漏检也不要误检。nms_threshold控制重叠框合并0.3 是通用值。input_size设小可以提速但太小会漏掉远处的人脸。3. 人脸注册与特征编码把脸变成能比对的数字3.1 人脸注册流程与照片采集规范注册环节决定了整个系统的上限。我踩过的最大坑是注册时只拍一张正面照结果员工戴眼镜、换发型、光线变化后就识别不了。正确做法是每人采集 15 到 30 张多角度照片。采集脚本要引导用户做轻微转头、眨眼、表情变化import cv2 import os def register_face(person_name, save_dirdataset, max_count30): save_path os.path.join(save_dir, person_name) os.makedirs(save_path, exist_okTrue) cap cv2.VideoCapture(0) count 0 while count max_count: ret, frame cap.read() if not ret: break faces detect_faces(frame) if len(faces) 1: # 只采集单张人脸的帧 x, y, w, h faces[0][:4].astype(int) # 外扩 20% 留出边缘 pad int(0.2 * w) x1, y1 max(0, x - pad), max(0, y - pad) x2, y2 min(frame.shape[1], x w pad), min(frame.shape[0], y h pad) face_img frame[y1:y2, x1:x2] face_img cv2.resize(face_img, (200, 200)) cv2.imwrite(os.path.join(save_path, f{count:03d}.jpg), face_img) count 1 cv2.imshow(Register, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()外扩 20% 是为了让特征提取时包含更多面部轮廓信息。保存尺寸统一 200x200减少后续处理差异。采集过程中要求用户缓慢左右转头覆盖不同角度。3.2 LBPH 特征训练与参数调优OpenCV 提供三种人脸识别器LBPH、Eigenfaces、Fisherfaces。考勤场景我推荐 LBPH因为它对光照变化鲁棒而且支持增量更新——新员工入职不需要重新训练全部数据。import cv2 import numpy as np import os def train_lbph(dataset_dirdataset, model_pathtrainer.yml): recognizer cv2.face.LBPHFaceRecognizer_create( radius1, neighbors8, grid_x8, grid_y8, threshold80.0 ) faces [] labels [] label_map {} current_label 0 for person_name in sorted(os.listdir(dataset_dir)): person_dir os.path.join(dataset_dir, person_name) if not os.path.isdir(person_dir): continue label_map[current_label] 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_label) current_label 1 recognizer.train(faces, np.array(labels)) recognizer.save(model_path) # 保存标签映射 import json with open(label_map.json, w, encodingutf-8) as f: json.dump(label_map, f, ensure_asciiFalse) return recognizer, label_map参数说明radius是 LBP 算子半径1 适合 200x200 的人脸图。neighbors是采样点数量8 是标准值。grid_x和grid_y把图像分成 8x8 网格每个网格单独统计直方图增强局部特征表达能力。threshold是识别阈值低于这个值才认为是同一个人80 是经验值需要根据实际数据调整——设太低会拒识设太高会误识。3.3 识别阈值怎么定用 ROC 曲线找平衡点threshold 这个参数不能拍脑袋。正确做法是拿一批已知身份的照片做测试画出不同阈值下的误识率和拒识率曲线。import cv2 import numpy as np import json def evaluate_threshold(recognizer, label_map, test_dir, thresholds): results {} for th in thresholds: recognizer.setThreshold(th) correct 0 total 0 false_accept 0 for person_name, label in [(v, k) for k, v in label_map.items()]: person_dir os.path.join(test_dir, person_name) if not os.path.isdir(person_dir): continue for img_name in os.listdir(person_dir): img cv2.imread(os.path.join(person_dir, img_name), cv2.IMREAD_GRAYSCALE) if img is None: continue pred_label, confidence recognizer.predict(img) total 1 if pred_label label and confidence th: correct 1 elif pred_label ! label and confidence th: false_accept 1 results[th] { accuracy: correct / total if total else 0, false_accept_rate: false_accept / total if total else 0 } return results跑完这段代码选误识率为 0 且准确率最高的阈值。实际项目中我一般把阈值定在 70 到 85 之间具体看数据分布。4. 考勤逻辑与数据落库识别之后才是业务4.1 实时识别循环与防重复打卡识别到人只是第一步考勤业务要处理的是什么时候算上班、什么时候算下班、同一个人连续出现在画面里怎么只记一次。import cv2 import time import json import csv from datetime import datetime, date class AttendanceSystem: def __init__(self, model_pathtrainer.yml, label_map_pathlabel_map.json): self.recognizer cv2.face.LBPHFaceRecognizer_create() self.recognizer.read(model_path) with open(label_map_path, r, encodingutf-8) as f: self.label_map {int(k): v for k, v in json.load(f).items()} self.detector cv2.FaceDetectorYN.create( face_detection_yunet_2023mar.onnx, , (320, 320) ) # 记录当天已打卡人员防止重复 self.today_records {} self.cooldown 60 # 同一人 60 秒内不重复记录 def mark_attendance(self, name): now time.time() today date.today().isoformat() key f{today}_{name} if key in self.today_records: if now - self.today_records[key] self.cooldown: return False self.today_records[key] now # 写入 CSV with open(attendance.csv, a, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([today, name, datetime.now().strftime(%H:%M:%S)]) return True def run(self): cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: break h, w frame.shape[:2] self.detector.setInputSize((w, h)) _, faces self.detector.detect(frame) if faces is not None: for face in faces: x, y, fw, fh face[:4].astype(int) gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) roi gray[y:yfh, x:xfw] roi cv2.resize(roi, (200, 200)) label, confidence self.recognizer.predict(roi) if confidence 80: name self.label_map.get(label, Unknown) if self.mark_attendance(name): cv2.putText(frame, f{name} OK, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) else: cv2.putText(frame, Unknown, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2) cv2.rectangle(frame, (x, y), (xfw, yfh), (0, 255, 0), 2) cv2.imshow(Attendance, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()cooldown 设为 60 秒是防止同一个人站在摄像头前被反复记录。实际部署时可以按考勤规则调整比如上班打卡后 8 小时内不再记录。4.2 考勤数据存储方案对比小规模用 CSV 就够了人数超过 50 或者需要多终端同步建议上 SQLite。再大就要考虑 MySQL 或 PostgreSQL。方案适用人数并发查询能力部署复杂度CSV 30无差极低SQLite 200单机中低MySQL 200高强中SQLite 版本的核心改动import sqlite3 def init_db(db_pathattendance.db): conn sqlite3.connect(db_path) conn.execute( CREATE TABLE IF NOT EXISTS records ( id INTEGER PRIMARY KEY AUTOINCREMENT, person_name TEXT NOT NULL, check_date TEXT NOT NULL, check_time TEXT NOT NULL, UNIQUE(person_name, check_date, check_time) ) ) conn.commit() return connUNIQUE 约束防止重复插入比在应用层判断更可靠。4.3 考勤报表导出与异常处理导出 Excel 是刚需用 openpyxl 按人按天汇总from openpyxl import Workbook import sqlite3 def export_report(db_pathattendance.db, outputreport.xlsx): conn sqlite3.connect(db_path) cursor conn.execute( SELECT person_name, check_date, MIN(check_time), MAX(check_time) FROM records GROUP BY person_name, check_date ) wb Workbook() ws wb.active ws.append([姓名, 日期, 最早打卡, 最晚打卡]) for row in cursor: ws.append(list(row)) wb.save(output)异常处理要注意识别失败时不要直接丢弃记录到 unknown 日志里方便后续排查是光线问题还是新人未注册。5. 避坑与排查那些让我加班到凌晨的问题5.1 识别率突然下降现象系统跑了一周都正常某天开始频繁认错人。原因最常见的是摄像头自动曝光被系统更新重置或者镜头脏了。其次是新注册的人脸照片质量差拉低了整体模型区分度。解决先擦镜头再检查CAP_PROP_AUTO_EXPOSURE是否被重置。然后重新跑一遍阈值评估看是不是新数据导致最优阈值偏移。如果是重新训练模型并调整 threshold。5.2 多人同时出现在画面导致漏检现象两个人一起走到摄像头前只识别出一个人。原因YuNet 的top_k参数默认值可能不够或者 NMS 阈值过高把相邻人脸合并了。解决把top_k调到 5000 以上nms_threshold从 0.3 降到 0.2。同时检查score_threshold是否过高0.9 在多人场景可以降到 0.85。5.3 中文姓名显示乱码现象OpenCV 的putText画中文变成问号。原因OpenCV 内置字体不支持中文。解决用 Pillow 绘制中文再转回 OpenCV 格式from PIL import Image, ImageDraw, ImageFont import cv2 import numpy as np def draw_chinese(frame, text, position, font_pathsimhei.ttf, font_size30): img_pil Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) draw ImageDraw.Draw(img_pil) font ImageFont.truetype(font_path, font_size) draw.text(position, text, fontfont, fill(0, 255, 0)) return cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)字体文件需要自己准备Linux 下可以用fc-list :langzh找系统里的中文字体。5.4 摄像头被其他程序占用现象cap.isOpened()返回 False或者读帧一直失败。原因Linux 下常见于多个进程同时访问/dev/video0Windows 下可能是杀毒软件或会议软件占用了摄像头。解决Linux 用fuser /dev/video0查占用进程杀掉后重试。代码里加重试逻辑不要一次失败就退出。5.5 模型文件路径找不到现象cv2.FaceDetectorYN.create报错找不到 onnx 文件。原因工作目录和模型文件目录不一致或者模型文件没下载。解决用绝对路径或者在代码开头os.chdir到脚本所在目录。模型文件从 OpenCV 官方仓库下载不要用来源不明的文件。6. 从能跑到好用三个让考勤系统真正上线的技巧第一个技巧是活体检测防照片攻击。LBPH 本身不区分真人和照片员工拿手机翻拍一张脸就能代打卡。最简单的活体检测是眨眼检测用 OpenCV 的级联分类器检测眼睛要求 3 秒内眨眼一次才记录考勤。代码不复杂但能挡住 90% 以上的照片攻击。eye_cascade cv2.CascadeClassifier(cv2.data.haarcascades haarcascade_eye.xml) def check_blink(eye_roi_list, blink_counter): for eye_roi in eye_roi_list: eyes eye_cascade.detectMultiScale(eye_roi, 1.1, 5) if len(eyes) 0: blink_counter 1 return blink_counter第二个技巧是多帧投票。单帧识别可能因为表情或角度出错连续 5 帧里至少 3 帧识别为同一个人再确认。这个逻辑加在mark_attendance之前能显著降低误识率。第三个技巧是增量更新模型。新员工入职后不需要重新训练全部数据。LBPH 支持update方法把新人的照片追加进去就行recognizer.update(new_faces, np.array(new_labels)) recognizer.save(trainer.yml)但要注意增量更新多次后模型会膨胀建议每季度做一次全量重训。验证系统是否达标我一般用三个指标误识率低于 1%、拒识率低于 5%、单帧识别耗时低于 200ms。达不到就调阈值或换检测模型。这套系统我前后部署过三个版本最大的教训是注册照片的质量比算法选择重要十倍。与其纠结用 LBPH 还是深度学习模型不如先把采集环节的灯光和角度标准化。希望帮到你。本文还有配套的精品资源点击获取