ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

OpenCV人脸识别考勤系统实战:从摄像头到考勤表

OpenCV人脸识别考勤系统实战:从摄像头到考勤表 简介这份源码面向高校学生与Python初学者提供一套基于OpenCV的人脸识别考勤系统完整实现可用于课程设计、期末项目或自学计算机视觉与图像处理的练手场景。压缩包共28个文件约116.61MB以15个py源码文件为核心辅以7个png界面截图、2个txt说明、2个dat模型数据及gitignore、gitkeep等配置项涵盖登录验证、人脸检测与识别、考勤记录查询等模块并附带dlib人脸关键点与特征模型文件。目前已有276人学习下载。读者可从中获得可直接运行的考勤系统源码、清晰的模块划分与数据库操作示例以及人脸检测识别流程的完整实现思路便于在此基础上二次开发或撰写项目文档。1. 从一台摄像头到一张考勤表OpenCV 人脸识别考勤系统到底怎么落地很多人第一次听到「基于 OpenCV 的 Python 人脸识别考勤系统」脑子里浮现的是公司门口那台刷脸门禁机。但真到自己动手问题立刻变成摄像头怎么读、人脸怎么框、框完怎么认、认完怎么存、存完怎么防止同一个人刷三次。这套系统本质上是把「视频流采集 → 人脸检测 → 特征比对 → 考勤记录落库」串成一条稳定流水线OpenCV 负责前两步和图像预处理识别环节可以接 LBPH、FaceNet 或 ArcFacePython 负责调度和业务逻辑。它适合两类人想用 Python 入门计算机视觉的开发者以及需要给中小团队做一套低成本考勤方案的实施者。源码结构不复杂难的是光照、角度、重复打卡这些工程细节。下面按「先跑通最小闭环再补识别精度最后处理考勤业务」的顺序拆开讲。2. 环境搭建与最小可跑闭环先让 cv2 能读到你的脸2.1 安装 OpenCV 与常见报错处理Windows 和 Linux 上装 OpenCV 的路径不一样踩的坑也不一样。最常见的是ModuleNotFoundError: No module named opencv和cv2.error: OpenCV(4.4.0) ... pip-req-build前者是包名装错后者多半是 numpy 版本和 OpenCV 编译版本不匹配。# 推荐用虚拟环境避免污染系统 Python python -m venv venv # Windows venv\Scripts\activate # Linux / macOS source venv/bin/activate # 安装核心依赖opencv-python 是主包contrib 版才有 LBPH 等扩展 pip install opencv-python4.8.1.78 pip install opencv-contrib-python4.8.1.78 pip install numpy1.24.3 pip install pillow这里固定版本不是玄学。OpenCV 4.8 和 numpy 1.24 的组合在 Windows 和 Ubuntu 上都验证过能避开cv2.error里那串pip-req-build的编译报错。opencv-contrib-python必须装因为后面 LBPH 识别器在 contrib 包里只装opencv-python会报AttributeError: module cv2 has no attribute face。验证安装import cv2 import numpy as np print(cv2.__version__) # 应输出 4.8.1 print(hasattr(cv2, face)) # 应输出 True如果hasattr(cv2, face)是 False说明 contrib 包没装成功先卸载两个包再重装 contrib 版。Linux 上如果报libGL.so.1找不到执行sudo apt install libgl1-mesa-glx即可这是 Ubuntu 配置 OpenCV 时的高频问题。2.2 用 Haar 级联做第一版人脸检测最小闭环不需要识别先让程序能框出人脸。OpenCV 自带 Haar 级联分类器模型文件在cv2.data.haarcascades目录下不用额外下载。import cv2 # 加载 OpenCV 自带的正脸检测器 face_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) cap cv2.VideoCapture(0) # 0 表示默认摄像头 if not cap.isOpened(): raise RuntimeError(摄像头打开失败检查设备索引或权限) while True: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # Haar 需要灰度图 faces face_cascade.detectMultiScale( gray, scaleFactor1.1, # 每次图像缩放比例越小越慢但越全 minNeighbors5, # 邻域阈值越大误检越少但可能漏检 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 Detect, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()scaleFactor和minNeighbors是 Haar 检测的两个命门。scaleFactor1.1是精度和速度的平衡点调到 1.05 检测更细但帧率掉得明显minNeighbors5能压掉大部分误检如果发现人脸框闪烁先加这个值。minSize设成 80×80 能过滤掉背景里的小噪点摄像头分辨率 640×480 时这个值比较合适。Haar 的局限要提前知道侧脸、戴口罩、强逆光下漏检率很高。如果考勤场景是固定工位、正面采集Haar 够用如果是门口走动刷脸建议直接上 DNN 人脸检测器OpenCV 的cv2.dnn模块可以加载 Caffe 或 ONNX 模型精度提升明显代价是首次加载慢几百毫秒。2.3 采集人脸样本并训练 LBPH 识别器检测跑通后下一步是让系统「认识」每个人。LBPHLocal Binary Patterns Histograms是 OpenCV contrib 里自带的人脸识别算法优点是训练快、样本需求少、不需要 GPU缺点是跨姿态和跨光照的泛化能力一般。对考勤这种每人固定角度采集几十张的场景LBPH 是性价比最高的起点。采集脚本的核心逻辑输入姓名和工号用检测器框脸把灰度人脸区域裁剪成统一尺寸存盘。import cv2 import os face_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) name input(输入姓名: ).strip() uid input(输入工号: ).strip() save_dir os.path.join(dataset, f{uid}_{name}) os.makedirs(save_dir, exist_okTrue) cap cv2.VideoCapture(0) count 0 while count 60: # 每人采集 60 张覆盖轻微角度变化 ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale(gray, 1.1, 5, minSize(80, 80)) for (x, y, w, h) in faces: count 1 face cv2.resize(gray[y:yh, x:xw], (200, 200)) # 统一尺寸 cv2.imwrite(os.path.join(save_dir, f{count}.jpg), face) cv2.rectangle(frame, (x, y), (xw, yh), (0, 255, 0), 2) cv2.putText(frame, fCount: {count}/60, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imshow(Collect, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()采集时让人轻微左右转头、改变表情但不要大幅度移动。60 张是经验值低于 30 张 LBPH 容易把两个人认混高于 100 张训练时间线性增长但精度提升有限。统一 resize 到 200×200 是为了让 LBPH 的直方图特征维度一致尺寸不统一会在训练时报维度错误。训练脚本import cv2 import numpy as np import os dataset_path dataset faces, labels, label_map [], [], {} current_id 0 for person in sorted(os.listdir(dataset_path)): person_dir os.path.join(dataset_path, person) if not os.path.isdir(person_dir): continue label_map[current_id] person 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_id) current_id 1 recognizer cv2.face.LBPHFaceRecognizer_create( radius1, neighbors8, grid_x8, grid_y8 ) recognizer.train(faces, np.array(labels)) recognizer.save(trainer.yml) # 保存 id 到姓名的映射识别时要用 import json with open(labels.json, w, encodingutf-8) as f: json.dump(label_map, f, ensure_asciiFalse, indent2)radius、neighbors、grid_x、grid_y是 LBPH 的四个核心参数。radius1表示以 1 像素为半径计算 LBP 特征值越大越能捕捉粗纹理但细节丢失grid_x8, grid_y8把 200×200 的人脸切成 8×8 网格分别统计直方图网格越多空间信息保留越好但特征维度从 256 涨到 256×64训练和识别都会变慢。默认值在考勤场景够用如果发现双胞胎或长相相近的人容易混先把grid_x、grid_y提到 10 再试。3. 识别流水线与考勤业务逻辑从置信度到打卡记录3.1 实时识别与置信度阈值调参训练完trainer.yml后识别脚本把检测、预测、判定串起来。LBPH 的predict返回两个值预测标签和置信度距离。距离越小表示越像通常 0 到 100 之间超过 80 基本可以认为是不认识的人。import cv2 import json import numpy as np recognizer cv2.face.LBPHFaceRecognizer_create() recognizer.read(trainer.yml) with open(labels.json, r, encodingutf-8) as f: label_map {int(k): v for k, v in json.load(f).items()} face_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) CONFIDENCE_THRESHOLD 70 # 低于此值才认为是已知人员 cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale(gray, 1.1, 5, minSize(80, 80)) for (x, y, w, h) in faces: face cv2.resize(gray[y:yh, x:xw], (200, 200)) label, confidence recognizer.predict(face) if confidence CONFIDENCE_THRESHOLD: name label_map.get(label, Unknown) color (0, 255, 0) text f{name} ({confidence:.1f}) else: name Unknown color (0, 0, 255) text fUnknown ({confidence:.1f}) cv2.rectangle(frame, (x, y), (xw, yh), color, 2) cv2.putText(frame, text, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2) cv2.imshow(Attendance, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()CONFIDENCE_THRESHOLD是整套系统最需要现场调参的地方。设成 70 偏严格陌生人基本不会误认但同一个人换个发型可能被拒设成 85 偏宽松通过率高但容易把相似脸认错。我的做法是先用 75 跑一周把误识和拒识的记录拉出来看再往回调 5 到 10。这个值没有通用答案和采集样本质量、光照稳定性直接相关。3.2 考勤记录落库与防重复打卡识别出姓名后考勤逻辑要解决三件事记录时间、判断迟到早退、防止同一人短时间内重复写入。用 SQLite 足够单文件、零配置中小团队几十人规模完全扛得住。import sqlite3 from datetime import datetime, timedelta def init_db(): conn sqlite3.connect(attendance.db) conn.execute( CREATE TABLE IF NOT EXISTS records ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL, name TEXT NOT NULL, check_time TEXT NOT NULL, status TEXT DEFAULT normal ) ) conn.commit() return conn def mark_attendance(conn, uid, name, work_start09:00): now datetime.now() today now.strftime(%Y-%m-%d) # 查最近一条记录5 分钟内不重复写入 cur conn.execute( SELECT check_time FROM records WHERE uid? ORDER BY id DESC LIMIT 1, (uid,) ) row cur.fetchone() if row: last datetime.strptime(row[0], %Y-%m-%d %H:%M:%S) if now - last timedelta(minutes5): return False # 重复打卡忽略 start_dt datetime.strptime(f{today} {work_start}, %Y-%m-%d %H:%M) status late if now start_dt else normal conn.execute( INSERT INTO records (uid, name, check_time, status) VALUES (?, ?, ?, ?), (uid, name, now.strftime(%Y-%m-%d %H:%M:%S), status) ) conn.commit() return True5 分钟去重窗口是经验值。太短同一个人站在摄像头前会被连续写入十几条太长午休回来可能被误判为重复。如果考勤分上午下午两次把去重逻辑改成「按 uid 日期 上下午」判断更准确。status字段用late和normal区分早退判断可以在下班时间点再跑一次比对或者加一个check_out字段单独记录。3.3 识别精度不够时的三条升级路径LBPH 在样本质量好时准确率能到 90% 以上但一旦光照变化大或人员戴眼镜掉得很快。三条常见升级路径第一条是换 DNN 人脸检测器。OpenCV 的cv2.dnn.readNetFromCaffe可以加载 ResNet SSD 人脸检测模型对侧脸和遮挡的鲁棒性比 Haar 高一个档次检测阶段漏检少了识别阶段自然更稳。第二条是换特征提取方案。把 LBPH 换成 FaceNet 或 ArcFace 的 ONNX 模型用cv2.dnn推理出 128 维或 512 维特征向量再算余弦相似度。ArcFace 在戴口罩场景下的表现明显优于 LBPH代价是需要额外下载模型文件推理也要 GPU 才流畅。第三条是加预处理。直方图均衡化cv2.equalizeHist能缓解光照不均人脸对齐用眼睛检测器定位后旋转校正能解决头部倾斜问题。这两步加在检测和识别之间代码量不大对 LBPH 的提升却很直接。方案准确率固定光照准确率变化光照硬件要求实现成本Haar LBPH90% 左右70% 左右CPU 即可低DNN LBPH93% 左右80% 左右CPU 即可中DNN ArcFace97% 左右92% 左右建议 GPU高选哪条取决于场景。固定工位、光线稳定第一套就够门口走动刷脸直接上第三套中间方案容易陷入调参泥潭。4. 避坑与排查那些让考勤系统翻车的细节4.1 摄像头读不到画面或帧率骤降现象cap.read()一直返回 False或者画面卡成幻灯片。原因通常是设备索引不对、被其他程序占用或者分辨率设太高导致 USB 带宽不够。解决先用cv2.VideoCapture(0)到(3)逐个试索引关掉占用摄像头的会议软件把cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)和CAP_PROP_FRAME_HEIGHT显式设成 640×480不要用默认的 1080p。Linux 上还要确认当前用户在video组里否则会报权限错误。4.2 同一个人被识别成不同标签现象同一个人今天识别成工号 001明天变成 003。原因多半是采集样本时人脸区域偏移太大LBPH 的网格直方图对不齐。解决采集时确保人脸框居中裁剪后统一 resize 到 200×200训练前把明显模糊或侧脸超过 30 度的样本删掉如果还混把grid_x、grid_y从 8 提到 10增加空间分辨率。4.3 陌生人被误认为已录入人员现象没录过的人站在摄像头前系统给出一个已有姓名且置信度低于阈值。原因是 LBPH 的置信度是相对距离样本少的时候分布很宽。解决把CONFIDENCE_THRESHOLD从 70 降到 60每人采集样本从 60 张提到 100 张在识别前加一个「活体检测」的简化版——连续 5 帧都识别为同一人且置信度稳定才写入考勤单帧结果直接丢弃。4.4 考勤记录时间对不上现象数据库里存的时间和实际打卡时间差了几个小时。原因是datetime.now()取的是系统本地时间如果服务器时区设成 UTC就会差 8 小时。解决在脚本开头显式设置时区或者用datetime.now(timezone(timedelta(hours8)))固定东八区。另外 SQLite 存字符串时格式要统一%Y-%m-%d %H:%M:%S不要混用其他格式否则按时间排序会出错。4.5 训练时报内存不足或维度错误现象recognizer.train抛异常提示样本尺寸不一致或内存溢出。原因是数据集中混入了非灰度图或尺寸不统一的图片。解决训练前加一段校验遍历所有样本确认img.shape (200, 200)且len(img.shape) 2样本量超过 5000 张时分批训练或改用增量更新recognizer.update不要一次性全量重训。5. 把识别结果变成可用的考勤报表导出与验证技巧系统跑通之后真正决定它能不能被团队接受的是考勤数据能不能方便地导出和核对。我一般会在 SQLite 之上加一个导出脚本按日期范围生成 CSV同时把异常记录标出来。import sqlite3 import csv from datetime import datetime def export_report(start_date, end_date, out_filereport.csv): conn sqlite3.connect(attendance.db) cur conn.execute( SELECT uid, name, check_time, status FROM records WHERE date(check_time) BETWEEN ? AND ? ORDER BY check_time , (start_date, end_date)) rows cur.fetchall() # 按人按天聚合取每天最早一条作为上班打卡 daily {} for uid, name, check_time, status in rows: day check_time[:10] key (uid, name, day) if key not in daily or check_time daily[key][0]: daily[key] (check_time, status) with open(out_file, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([工号, 姓名, 日期, 上班打卡, 状态]) for (uid, name, day), (check_time, status) in sorted(daily.items()): writer.writerow([uid, name, day, check_time[11:], status]) conn.close() print(f导出 {len(daily)} 条记录到 {out_file}) export_report(2025-01-01, 2025-01-31)encodingutf-8-sig是为了让 Excel 打开 CSV 不乱码这个细节坑过很多人。按天取最早一条作为上班打卡是常见做法如果一天要打多次卡把聚合逻辑改成按uid day 上下午分组即可。验证识别效果不能只看训练准确率要拿没参与训练的新样本测。我的习惯是每人留 10 张不加入训练集跑一遍识别脚本统计通过率和误识率。通过率低于 85% 就回去补样本或调阈值误识率高于 2% 就收紧CONFIDENCE_THRESHOLD。这个验证步骤花不了半小时但能避免上线后被同事吐槽「刷十次过三次」。最后说一个我踩过的坑早期版本我把识别和考勤写在一个死循环里结果摄像头帧率一高SQLite 写入频繁触发锁竞争程序跑几小时就卡死。后来改成识别线程和写入线程分离用队列传递打卡事件写入端批量提交稳定性才上来。如果部署在工位机上长期运行这个架构调整值得一开始就做。希望帮到你。本文还有配套的精品资源点击获取
返回列表