OpenCV+dlib实现离线人脸识别考勤系统:从环境搭建到代码解析
2026/9/12 22:20:00 网站建设 项目流程

简介:基于Python+OpenCV+dlib库开发的人脸识别考勤打卡系统项目源码,属于导师指导并获98分高分通过的课程设计作品。项目面向计算机相关专业正在准备毕业设计、课程设计或期末大作业的学生,也适合需要完整项目实战练习的Python学习者。系统核心功能涵盖录入人脸信息、人脸识别打卡、设置上下班时间与导出打卡日志,压缩包内附带项目说明文档、运行截图和环境依赖清单,可在PyCharm中快速配置并运行。整包共657个文件,压缩包大小约196MB,以Python源码为主,同时包含依赖库、可执行组件、配置文件、模型权重、界面示意图与项目说明文档,便于直接复现与二次开发。目前已有393人学习下载,适合作为理解人脸识别考勤流程、完善课程设计或扩展毕业设计功能的参考起点。

1. 为什么用 OpenCV + dlib 自建人脸考勤而不是买门禁机

很多团队的考勤打卡还在用指纹机,秋冬干燥季节识别率直线下降,一张门禁卡更是可以随手代刷。用一台带摄像头的普通电脑,基于 OpenCV 做图像输入、dlib 做特征提取,就能完全离线地搭起一套人脸识别考勤系统,不依赖任何云 API。这个课程大作业包含录入人脸、实时打卡、上下班时间设置、打卡日志导出四块完整功能,评审 98 分,导师验收通过。它把特征库存在 feature_all.csv 里,打卡记录写进 logcat.csv,源码里还带虚拟环境与项目说明。适合正在做毕业设计或期末课程设计的计算机专业学生,也适合想快速看一遍传统人脸识别链路如何在真实业务里落地的开发者。

2. OpenCV 与 dlib 的依赖选型及 Python 3.8 环境搭建

2.1 人脸识别管线的选型逻辑

这套系统的核心链路是:OpenCV 从摄像头拿帧并做灰度化,dlib 的 HOG 人脸检测器负责把人脸框出来,shape_predictor_68_face_landmarks.dat 定位 68 个关键点,人脸对齐后由 face_recognition_model_v1 模型把关键点区域映射成 128 维特征向量。之所以不用 MTCNN 或 ArcFace,是因为考勤打卡场景要求的是低成本、易部署:dlib 的模型文件只有几十 MB,CPU 单帧检测 20ms 左右,不需要 GPU。后面如果要换成深度模型,只要替换检测和特征提取这两个接口,前端业务逻辑不用动。

OpenCV 在这里承担的是硬件输入和图像预处理。要注意 OpenCV 读进来的 BGR 通道顺序,dlib 的 compute_face_descriptor 接收彩色图像即可,但灰度化后做人脸检测速度更快。我一般会沿用灰度图检测、原彩色图提特征的顺序,既能保证检测速度,又不会因为 RGB/BGR 混用导致特征值漂移。项目里既然指定 Python 3.8,环境上就按这个版本锁死,免得后续踩编译兼容的坑。

2.2 虚拟环境与依赖安装

压缩包里有 activate.bat、deactivate.bat、pyvenv.cfg,说明原作者是在 venv 虚拟环境里跑的。这类打包的虚拟环境路径通常写死在创建机器上,换到别的电脑再次执行 activate.bat 会报 pyvenv.cfg 找不到或 Scripts 目录不存在,此时按原始目录重建即可,没必要直接去改配置文件。项目说明里也写了不需要全部安装,实际只要把摄像头调用、图像处理和特征提取三层依赖装齐。

下面是我在复现时最小化的一组命令:新建虚拟环境后激活,再安装依赖。Windows 下激活脚本是venv\Scripts\activate,如果激活后命令提示符前面没有出现(venv),说明当前终端没有正确切换环境,后面的 pip 会装到全局解释器里。

python -m venv venv # Windows 下进入虚拟环境 venv\Scripts\activate pip install -r requirements.txt

requirements.txt 里核心包固定成下面这个组合,可以避开 dlib 在部分 Python 版本上的编译问题:

opencv-python==4.5.3.56 dlib==19.22.1 numpy==1.21.6

参数说明:dlib 19.22.1 对 Python 3.8 支持最稳,OpenCV 4.5.3 的 Python 接口是 cv2,numpy 1.21.6 与 Python 3.8 配套;如果机器上原本装了更高版本 numpy,第一次运行报module 'numpy' has no attribute 'float'时,优先把 numpy 降级。下面是依赖与用途的对照。

依赖库用途关键注意项
opencv-python摄像头读取、灰度化、窗口显示BGR 通道顺序,别和 RGB 混用
dlib人脸检测、68 点关键点、128 维特征描述子Windows 下容易编译失败
numpy特征向量均值、距离计算版本过高会触发兼容问题

2.3 安装 dlib 的常见坑与验证

dlib 在 Windows 上最常见的问题是安装时被拉去现场编译 C++ 源码,报 CMake 或 Visual Studio 工具链缺失。自己机器没有编译环境的话,优先使用预编译的 dlib wheel,或者用 conda 从 conda-forge 渠道安装;再不行就把 Python 降到 3.8 后重试。装好之后建议先跑一段验证脚本,确认摄像头和人脸检测器都处于可用状态:

import cv2 import dlib import numpy as np detector = dlib.get_frontal_face_detector() predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat") facerec = dlib.face_recognition_model_v1("dlib_face_recognition_resnet_model_v1.dat") print("dlib version:", dlib.__version__) cap = cv2.VideoCapture(0) ok, frame = cap.read() print("camera ok:", ok, frame.shape if ok else "no frame") cap.release()

这里把三个核心对象都初始化出来:detector 负责检测,predictor 负责 68 点形状预测,facerec 负责把对齐后的人脸转成 128 维向量。如果第二行报模型文件找不到,确认两个 .dat 文件与代码在同一级目录;如果cap.read()返回 False,先把摄像头驱动装好,或者把设备索引从 0 改成 1。

提示:项目指定 Python 3.8 不是偶然,dlib 19.22 在 Python 3.9 以上存在编译兼容问题;建议锁死版本,不要在 3.11 上硬跑。

3. 人脸录入模块:dlib 特征提取与 feature_all.csv 入库

3.1 录入时为什么要做多帧平均

直接截一帧提取 128 维特征不是不行,但单帧受光照、头部偏转和摄像头自动白平衡影响很大,同一个人的特征向量分布会跳动。录入环节多采样 10 到 20 帧,取特征向量的均值作为该员工的人脸模板,能明显降低后续打卡时的误拒绝率。核心就是两步:先逐帧提取特征,再把特征均值写入 CSV。下面这段代码是录入模块的特征提取部分,也是整个系统里最关键的封装。

import cv2 import dlib import numpy as np detector = dlib.get_frontal_face_detector() predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat") facerec = dlib.face_recognition_model_v1("dlib_face_recognition_resnet_model_v1.dat") def face_feature(frame): gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = detector(gray, 1) # 1 表示做一次金字塔放大,小脸更容易检出 if len(faces) == 0: return None face = max(faces, key=lambda f: f.width() * f.height()) shape = predictor(gray, face) return np.array(facerec.compute_face_descriptor(frame, shape))

参数说明:detector 的第二个参数是 detect_upsample,取 1 表示把输入图像放大一倍再检测,小尺寸人脸更容易被找到,代价是耗时翻倍;按面积取最大人脸能避开背后路人的干扰。compute_face_descriptor 传入原始彩色图和 shape 点集,不要提前把 frame 裁掉,保留上下文信息对 128 维特征的稳定性有帮助。

接着是采集主循环,我一般会把有效帧阈值设为 3,防止对着空墙误保存:

cap = cv2.VideoCapture(0) employee_id = input("输入工号/姓名:") features = [] for i in range(15): ok, frame = cap.read() if not ok: continue f = face_feature(frame) if f is not None: features.append(f) cv2.imshow("register", frame) if cv2.waitKey(100) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows() if len(features) >= 3: mean_vec = np.mean(features, axis=0).astype(np.float32) with open("feature_all.csv", "a", newline="", encoding="utf-8") as fp: writer = csv.writer(fp) writer.writerow([employee_id] + list(mean_vec)) print("saved", employee_id, len(features), "frames")

逻辑说明:循环最多取 15 帧,但真正进入 features 列表的只有检测到人脸的帧;末尾len(features) >= 3的判定能避免"对着空墙按下保存"的误操作。csv.writer 以追加模式写入,encoding 必须显式指定 utf-8,否则中文姓名在 Windows 默认 GBK 环境下读回会变成乱码。np.mean 对多个 128 维向量逐元素取平均,生成一个同时覆盖正面和轻微侧脸的中间模板。

3.2 feature_all.csv 的字段与存储方式

项目里的人工特征库就是 feature_all.csv,每一行对应一个员工模板,结构如下:

字段类型说明
namestr工号或姓名,作为打卡记录的主键
feature_0 ~ feature_127floatdlib 输出的 128 维人脸描述子

一行共 129 列,没有表头。读取时用 csv.reader 逐行解析,把第二列开始的字符串转成 np.float32。值得注意的一个细节:dlib 输出的描述子本身不是相似度分数,而是欧氏空间中的坐标,所以同一个人的特征向量会在某个方向聚集,但绝对值没有直接业务含义。文件里的特征值大致在 -1 到 1 之间,如果发现两行完全相同,通常是同一份样本被重复写库,录入时应该先按名字做覆盖或去重。

3.3 注册界面与业务校验的衔接

源码的录入窗口用 tkinter 实现,点击新建录入后弹出姓名输入框,内部调用的就是上面的采集逻辑。课程验收时老师一般会关心两个点:一是是否做了重复姓名覆盖,二是录入过程中用户转头离开会不会写进库。常见做法是在录入前先查一遍 feature_all.csv,若 name 已存在则提示覆盖;采集期间统计有效人脸帧数并实时显示在界面上,少于 3 帧直接拒绝保存。这套代码里判断可以在调用采集函数的外层完成,不必侵入 dlib 相关逻辑。

4. 打卡识别与考勤日志:欧氏距离阈值和 logcat.csv 写入策略

4.1 128 维特征如何判定是同一个人

考勤打卡的核心不是检测到人脸,而是判定这张脸属于哪个人。项目采用最近邻思路:把当前摄像头画面中的人脸转成 128 维向量,与 feature_all.csv 中所有模板计算欧氏距离,取最小距离对应的姓名作为识别结果,距离小于阈值才放行打卡。欧氏距离越小,两张脸越可能是同一个人。

import csv import numpy as np import dlib def load_feature_db(path="feature_all.csv"): records = [] with open(path, newline="", encoding="utf-8") as fp: for row in csv.reader(fp): if len(row) == 129: records.append((row[0], np.array(row[1:], dtype=np.float32))) return records def find_match(candidate, records, threshold=0.55): dists = [np.linalg.norm(candidate - vec) for _, vec in records] best_idx = int(np.argmin(dists)) name, min_dist = records[best_idx][0], dists[best_idx] return name, min_dist, min_dist < threshold

逻辑说明:load_feature_db 要求每行 129 列,能过滤掉写入过程中断了半段的数据;dists 用列表推导式计算与所有模板的 L2 范数,np.argmin 取最小距离下标。threshold 直接决定系统的松紧程度,0.55 是教室和办公室场景比较好用的起点。如果多名员工长相接近,把阈值压到 0.50 左右;如果员工经常侧脸或戴眼镜,阈值可以放到 0.60,但误识别风险会上升。

阈值适用场景实际表现
0.45单人值守、要求严格漏打卡明显增多
0.55默认教室/办公室误报与漏报均衡
0.65复杂光照或部分遮挡容易出现他人误匹配

4.2 打卡时间规则和上下班设置

只识别出人还不够,考勤系统要决定这次打卡算正常、迟到还是早退。项目在设置界面读取上下班时间,默认上班 09:00、下班 18:00。状态判定的常见策略是:上班前 30 分钟内打卡记为正常;上班后 60 分钟内记为迟到;更晚则视为无效打卡。一天内同一人只允许产生一条上班记录,重复刷卡直接忽略。

def judge_attendance(now, start="09:00", end="18:00"): now_min = now.hour * 60 + now.minute start_min = int(start[:2]) * 60 + int(start[3:]) end_min = int(end[:2]) * 60 + int(end[3:]) if now_min <= start_min: return "ontime" if now_min <= start_min + 60: return "late" return "unrecognized"

参数说明:now_min 把当前时刻转成分钟数,方便与上下班时间做单值比较;晚于上班时间 60 分钟以上返回 unrecognized,不会写入考勤记录。这个函数只做状态计算,真正的落库需要在外面加一层当天是否已打卡的检查,否则摄像头连拍几帧会把同一次进公司写成多条记录。

4.3 logcat.csv 的追加写入与去重

考勤日志文件是 logcat.csv,每次有效打卡追加一行。写入前先读一遍已有记录,判断同一天同姓名是否已经存在。这里给一个实用的写入函数,包含日期、时间、状态和匹配距离四个字段:

import csv from datetime import date, datetime def already_logged(name, log_path="logcat.csv"): today = date.today().isoformat() with open(log_path, newline="", encoding="utf-8") as fp: for row in csv.reader(fp): if len(row) >= 3 and row[0] == today and row[1] == name: return True return False def write_log(name, status, distance, log_path="logcat.csv"): if already_logged(name): return False row = [date.today().isoformat(), name, datetime.now().strftime("%H:%M:%S"), status, f"{distance:.3f}"] with open(log_path, "a", newline="", encoding="utf-8") as fp: csv.writer(fp).writerow(row) return True

逻辑说明:already_logged 用日期加姓名做联合主键,满足条件则直接跳过,防止一帧识别成功后连续触发写盘。距离字段在排错时很有价值,比如某个员工每天的距离值从 0.2 慢慢涨到 0.6,说明库里保存的模板已经老化,需要重新录入。CSV 方案在单人考勤场景没有问题,但多人同时刷卡写同一个文件时可能丢行,所以最后一章把存储层换成 SQLite。项目里的识别失败帧还会保存成 suspicious_时间戳.jpg,方便事后人工核对。

5. 运行排查与把特征库从 CSV 迁移到 SQLite

5.1 三个高频运行错误

先列实际运行中会遇到的三个问题。第一个是ModuleNotFoundError: No module named 'dlib',原因基本是安装时没有装进当前 Python 环境,用 PyCharm 的话要确认项目解释器选的是 venv 目录下的 python.exe。第二个是cv2.VideoCapture(0)返回 False,常见原因是笔记本摄像头默认索引不是 0,或者驱动被其他软件占用,把索引改成 1 或 2 逐个尝试。第三个是模型文件位置不对,两个 .dat 模型尽量放在项目根目录,不要在代码里写绝对路径,否则换机器就崩。

5.2 把 CSV 换成 SQLite 的迁移方式

feature_all.csv 在几百人规模下还能跑,但考勤日志是持续增长的。CSV 读一行扫一行,统计某个月谁迟到几次需要把整个文件加载进内存。SQLite 的查询效率高一个量级,而且写入带并发安全。最小改动就是只动存取层,识别算法部分完全不用改:

import sqlite3 import numpy as np def init_db(): conn = sqlite3.connect("attendance.db") conn.execute("CREATE TABLE IF NOT EXISTS feature " "(name TEXT PRIMARY KEY, feature BLOB)") return conn def insert_feature(conn, name, vec): blob = np.asarray(vec, dtype=np.float32).tobytes() conn.execute("REPLACE INTO feature(name, feature) VALUES(?,?)", (name, blob)) conn.commit()

逻辑说明:REPLACE 代替 INSERT,遇到同名员工直接覆盖,省掉了原来查重再删除的步骤。BLOB 字段里存的是 float32 数组的字节串,读取时用 np.frombuffer 还原成 128 维向量,识别距离计算逻辑和 4.1 节完全一致。常见的坑是写完数据后忘记 commit,症状是查询 count 一直不变,所以我会把 commit 放在每次写入之后。如果继续压精度,给每个员工存三次不同光照下的模板,识别时取三个最近距离的最小值,比单模板平均更抗环境变化。

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

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

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

立即咨询