简介:这套基于Python的员工人脸识别考勤系统,面向需要实现摄像头实时检测与身份验证的中高级Python开发者,结合OpenCV与Dlib完成人脸检测、特征点定位及模型训练,可应用于企业门禁、课堂签到等场景。压缩包共657个文件,大小196.19MB,包含501个py源码、44个pyd动态库、22个exe可执行文件,以及pth模型、xml配置、bat脚本、png图像等,涵盖图像处理、模型训练、实时检测、记录报告等完整模块。已有2514人学习下载。项目以WorkAttendanceSystem-master组织目录,除核心代码外还附带数据集、配置文件和README,便于直接搭建运行。通过学习可掌握Haar级联、LBPH、Dlib 68点关键点及CNN人脸识别在真实考勤流程中的落地方式,也能参考项目拆分与文件组织思路,适合作为课程设计或企业二次开发的基础。
1. 从“dilb”这个拼写说起:OpenCV 与 dlib 在人脸识别考勤系统里的分工
“dilb”是 dlib 的常见笔误,很多分享源码的帖子都会打错这个库名。这类 Python + OpenCV + dlib 的员工考勤系统,解决的是中小型公司在离线环境下做刷卡替代方案:员工站到摄像头前,系统判断这个人是谁、工号多少、今天是否已经打卡,然后把记录写进考勤表。和刷指纹、刷工牌相比,它不需要额外硬件,一台带摄像头的 PC 或工控机就能跑。整套链路由三部分构成:OpenCV 负责读取摄像头帧和图像预处理,dlib 负责检测人脸、定位关键点、提取 128 维特征向量,SQLite 或 MySQL 负责员工档案与打卡记录的持久化。很多人以为难点在算法,实际上把“采集—检测—对齐—比对—写库”这条链串稳,才是项目能否落地的关键。
2. 环境搭建与版本匹配:opencv 和 dlib 安装跑通的正确顺序
2.1 先看编译工具链再装 dlib,能少踩一半坑
在“python安装教程”“opencv下载安装教程”这类检索词背后,真正让新手卡住的是 dlib 的安装。dlib 在 PyPI 上主要以源码包方式发布,pip install dlib 时会现场编译 C++ 代码,Windows 上动不动报ModuleNotFoundError: No module named 'dlib',Linux 上则常见CMake must be installed。
以 Ubuntu 22.04 为例,推荐按下面顺序执行:
sudo apt update sudo apt install -y build-essential cmake python3-dev python3-pip python3 -m venv ~/face_attendance source ~/face_attendance/bin/activate pip install --upgrade pip pip install numpy==1.24.4 opencv-python==4.8.1.78 dlib==19.24.2这里把系统级编译工具装好再创建虚拟环境,是为了让 dlib 源码编译时能找到 CMake 和 C++ 编译器。Windows 上同样的思路是提前装 Visual Studio Build Tools,勾选“使用 C++ 的桌面开发”工作负载。numpy、opencv-python、dlib 三个版本号之间没有强耦合,但 numpy 不要直接装最新版,部分 dlib 轮子对 numpy 2.x 的接口还没完全适配。
装完后不要急着跑业务代码,先做一个最小验证:
import cv2 import dlib detector = dlib.get_frontal_face_detector() print("opencv:", cv2.__version__) print("dlib:", dlib.__version__) print("detector ready:", detector is not None)这段代码验证两件事:import 阶段能否同时加载 opencv 和 dlib,dlib 自带的人脸检测器能否实例化。如果出现No module named 'opencv',先检查当前激活的解释器是不是虚拟环境里的 python,很多时候是系统自带 python 和 venv 混用导致。dlib 的编译期报错则要回退到 apt 安装列表,确认 cmake、boost、libx11-dev 是否齐全。
提示:PyPI 上没有名为 opencv 的包,只有 opencv-python,别在源里写错包名。
2.2 两个模型文件决定系统的“眼睛”和“大脑”
dlib 完成人脸识别需要两个不同作用的模型文件。源码包里通常会包含二者,解压后建议固定放到项目根目录的 models/ 子目录中,避免摄像头循环里频繁拼路径出错。
| 模型文件名 | 作用 | 加载函数 |
|---|---|---|
| shape_predictor_68_face_landmarks.dat | 定位 68 个人脸关键点,用于对齐 | dlib.shape_predictor() |
| dlib_face_recognition_resnet_model_v1.dat | 把对齐后的脸转成 128 维特征向量 | dlib.face_recognition_model_v1() |
第一个模型约 90MB,负责找眉毛、眼睛、鼻子、嘴巴的位置;第二个模型是 ResNet 网络权重,负责编码人脸身份。两个文件缺一不可,特征提取器单独加载时不会报错,但计算描述符时会一直失败或返回空结果。下载时留意文件名需要完全一致,版本不一致的模型文件会在加载阶段抛deserialize相关异常。
2.3 摄像头输出格式与检测器输入的匹配
OpenCV 的VideoCapture(0)打开摄像头后,每一帧默认是 BGR 三通道图像。dlib 的 HOG 检测器对灰度图处理效果最稳定,速度也快,因此每帧先转灰度是固定写法:
cap = cv2.VideoCapture(0) if not cap.isOpened(): raise RuntimeError("camera open failed") while True: ret, frame = cap.read() if not ret: continue gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = detector(gray, 1) for face in faces: left, top, right, bottom = face.left(), face.top(), face.right(), face.bottom() cv2.rectangle(frame, (left, top), (right, bottom), (0, 255, 0), 2) cv2.imshow("attendance", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()detector(gray, 1)的第二个参数 1 表示对图像做一次金字塔上采样,让画面里较小的脸放大以后再检测,能明显提升远距离召回率,代价是单帧耗时增加。考勤场景里员工通常距离摄像头 0.5 到 1.5 米,上采样 1 次足够;如果人员经常站在三米外,可以改成 2,同时把预览窗口缩小,否则普通笔记本 CPU 会掉帧。
3. 人脸检测与关键点对齐:把“有一张脸”升级为“一张可识别的正脸”
3.1 HOG + SVM 为什么是考勤系统的默认选择
dlib 的get_frontal_face_detector()内部是 HOG(方向梯度直方图)加线性 SVM 分类器的组合。它把图像分成小块,统计每个小块里梯度方向的分布,再用线性分类器判断当前滑动窗口是否包含人脸。这个方案对算力要求极低,在 i5 级别的 CPU 上处理 640×480 灰度图,单帧通常只要几十毫秒。
另一类检测器是 dlib 的 CNN 人脸检测器,需要额外下载mmod_human_face_detector.dat,对遮挡和侧脸更鲁棒,但 CPU 环境下单帧可能跑到几百毫秒甚至一秒。考勤系统的体验预期是“站过去一秒内给结果”,而且员工会主动正对摄像头,HOG 检测器在实际部署里往往更合适。
class FaceAnalyzer: def __init__(self): self.detector = dlib.get_frontal_face_detector() self.predictor = dlib.shape_predictor("models/shape_predictor_68_face_landmarks.dat") self.embedder = dlib.face_recognition_model_v1( "models/dlib_face_recognition_resnet_model_v1.dat") def locate(self, bgr): gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY) return self.detector(gray, 1) def landmarks(self, bgr, rect): gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY) return self.predictor(gray, rect)把检测器、关键点预测器、特征提取器封装成一个类,是为了让摄像头循环里只初始化一次模型。否则每帧重新加载三个模型文件,不仅启动慢,还容易造成内存持续上涨。locate返回的是dlib.rectangles对象列表,每个矩形有left/right/top/bottom四个属性,坐标系与 OpenCV 一致,左上角是原点,x 轴向右。
3.2 68 个关键点和仿射对齐的具体实现
关键点预测器输出的 68 个点中,人脸轮廓占 17 个,其余分布在眉毛、眼睛、鼻子和嘴巴。考勤识别要稳定,不能直接把检测框里的图像送进特征提取器,而是先用双眼位置把人脸旋转到水平。角度偏 5 度可能影响不大,偏 15 度时同一个人的特征距离会被拉大,跨过阈值造成漏识别。
dlib 没有提供现成的对齐函数,常见做法是自己算仿射变换。核心逻辑是取左右眼外眼角坐标,计算连线与水平方向的夹角,然后构造旋转矩阵:
import numpy as np def align_face(image, shape): left_eye = (shape.part(36).x, shape.part(36).y) right_eye = (shape.part(45).x, shape.part(45).y) dx = right_eye[0] - left_eye[0] dy = right_eye[1] - left_eye[1] angle = np.degrees(np.arctan2(dy, dx)) eyes_center = ((left_eye[0] + right_eye[0]) // 2, (left_eye[1] + right_eye[1]) // 2) matrix = cv2.getRotationMatrix2D(eyes_center, angle, 1.0) aligned = cv2.warpAffine(image, matrix, (image.shape[1], image.shape[0])) return aligned旋转时以两只眼睛的中点为中心,angle 的单位是度,正值代表逆时针旋转。变换后双眼连线被拉成水平线,再送入后续模型时,特征描述符的稳定性会明显提升。注意warpAffine会让图像边缘出现黑边,后续裁剪人脸区域时,建议在原检测框宽高基础上放大 1.2 倍再裁剪,把五官完整包含进来。
关键点的索引在不同版本模型上是固定的,考勤系统里主要用以下几组:
| 关键点索引 | 部位 | 常用场景 |
|---|---|---|
| 36 和 45 | 左右眼外眼角 | 人脸对齐、角度纠正 |
| 27 到 30 | 鼻梁 | 口罩遮挡判断 |
| 48 到 68 | 嘴唇轮廓 | 活体检测、张嘴动作 |
如果后续想判断员工是否戴口罩,不需要额外模型,直接对比 8 号点到 30 号点的距离就可以做一个粗糙判断。鼻子区域的梯度变化被口罩抹平后,这两个点的位置关系会明显异常。
4. 128 维特征与相似度阈值:考勤系统“认人”的参数决定识别率
4.1 员工注册的本质是向量入库
注册员工时,把摄像头拍到的脸转换成 128 维浮点向量,再存入数据库。这里最常见的错误是直接拿检测框内的原图做特征提取。dlib 的 ResNet 模型预期输入是一张正脸图,如果现场拍的是略带俯视角度,注册时又是正面照,那么同一个人两次算出的特征距离可能超过 0.6,直接被判成两个人。
建议注册时也走一遍对齐流程:
def enroll_employee(self, bgr, rect): gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY) shape = self.predictor(gray, rect) aligned = align_face(bgr, shape) a_gray = cv2.cvtColor(aligned, cv2.COLOR_BGR2GRAY) a_rect = dlib.rectangle(0, 0, a_gray.shape[1], a_gray.shape[0]) a_shape = self.predictor(a_gray, a_rect) descriptor = self.embedder.compute_face_descriptor(aligned, a_shape) return np.array(descriptor)对齐后的图像需要重新跑一次关键点定位,因为原图里的人脸位置在旋转后已经偏移,不能再沿用旧 rect。compute_face_descriptor返回一个 dlib description 对象,转成 numpy 数组后方便做欧氏距离计算和持久化。
注册员工时,每人建议拍三张照片:正面、向左偏 15 度、向右偏 15 度。三张向量都存下来,考勤比对时取距离最小值,比只存一张或取三张向量的平均值更稳。原因很简单,不同光照和姿态下,128 维空间里的特征点分布不是均匀球面,平均值会抹掉个体差异。
4.2 欧氏距离与阈值档位
人脸比对一般用欧氏距离。每个员工对应的多个向量一起参与计算,取最小的那个作为最终距离:
def match_employee(query_embedding, employee_vectors, threshold): min_dist = float("inf") matched_id = None for emp_id, vectors in employee_vectors.items(): for vec in vectors: dist = np.linalg.norm(query_embedding - vec) if dist < min_dist: min_dist = dist matched_id = emp_id if min_dist < threshold: return matched_id, min_dist return None, min_dist距离越小代表越像同一个人。代码里的 threshold 是识别准确率最敏感的旋钮,经验值如下:
| 阈值 | 效果 | 适用场景 |
|---|---|---|
| 0.45 | 严格,漏识别率偏高 | 光线稳定、员工人数少 |
| 0.55 | 均衡,误报和漏报相对平衡 | 办公室室内正常照明 |
| 0.65 | 宽松,容易误识别 | 户外、逆光、人员长期戴帽子 |
阈值低于 0.4 时,员工稍微换个发型就可能打不上卡;高于 0.7 时,相似脸型的同事之间会出现互相误打卡。上线前可以先用 20 个人、每人 5 张照片做一轮距离统计,把“同一个人最小距离”和“不同人最大距离”画出来,在两个分布的中间取阈值。
4.3 特征向量的持久化方案
128 维浮点向量不能直接按文本存,常见做法是序列化成 bytes 写入 SQLite。保存时转成np.float32,可以把体积压缩一半,读取时再恢复成向量:
embedding_bytes = embedding.astype(np.float32).tobytes() employee_vectors[emp_id] = [ np.frombuffer(row["embedding"], dtype=np.float32) for row in fetch_rows(emp_id) ]tobytes()和frombuffer的 dtype 必须一致,否则读出来的 128 维会变成 256 个 float16,匹配距离全部异常。建议在模型封装类里单独写一个load_embeddings()方法,统一负责数据库读取和解码,不要在业务代码里散落frombuffer调用。
5. 摄像头、打卡逻辑和 SQLite 串起来的最小可用系统
5.1 考勤打卡系统的表结构设计
考勤系统功能设计里最核心的是两张表:员工表和打卡记录表。员工表存工号、姓名、部门和特征向量,打卡记录表存员工 ID 与打卡时间。SQLite 单文件部署简单,几十个员工的场景完全够用,不需要单独跑 MySQL 服务。
CREATE TABLE employees ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, department TEXT, embedding BLOB NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE attendance_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, employee_id INTEGER NOT NULL, punch_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (employee_id) REFERENCES employees(id) );给attendance_logs的employee_id和punch_time建联合索引,因为每天查询时要反复执行“某个员工当天是否已打卡”的判断。不建索引的话,数据量到一万条以后查询耗时可能从毫秒级涨到百毫秒级。
5.2 打卡循环中的去重与冷却窗口
摄像头循环里每帧都可能检测到同一个人脸,如果每帧都写一次数据库,一分钟会产生几十条重复记录。常见的处理方式是在内存里维护一个打卡时间缓存,同一工号在冷却时间内只打一次卡,同时再去数据库确认当天是否已经打过卡:
last_punch_time = {} COOLDOWN_SECONDS = 5 def already_punched_today(emp_id): row = conn.execute( "SELECT COUNT(*) FROM attendance_logs " "WHERE employee_id=? AND date(punch_time)=date('now')", (emp_id,) ).fetchone() return row[0] > 0 def insert_attendance(emp_id): conn.execute( "INSERT INTO attendance_logs (employee_id, punch_time) " "VALUES (?, datetime('now', 'localtime'))", (emp_id,) ) conn.commit()每次识别到一张脸,先算特征向量,再与员工库比对。匹配成功且不在冷却窗口内,就执行already_punched_today查询。当天没记录才允许写入,否则界面提示“今日已打卡”。
主循环里要多考虑一个场景:画面里同时出现多个员工。此时要对每个检测框独立执行识别,但数据库写入和内存缓存都要按 emp_id 分别处理,避免 A 员工的脸被识别成 B 员工并写入 B 的考勤记录。实践中可以用置信度过滤,距离超过阈值的框直接忽略,不参与打卡。
5.3 视频流不要无限处理,要做抽帧
如果每一帧都跑完整检测加比对,普通笔记本 CPU 的占用率会长期处于 80% 以上并且发热严重。更合理的做法是设置一个处理时间间隔,比如每 0.4 秒处理一帧,其余帧只做预览回显:
frame_interval = 0.4 last_process_time = 0.0 while True: ret, frame = cap.read() now = time.time() if now - last_process_time >= frame_interval: process_frame(frame) last_process_time = now if cv2.waitKey(1) & 0xFF == ord("q"): break0.4 秒的间隔意味着每秒最多比对 2.5 次,对考勤场景已经足够。人员站定后,第一次识别成功就会写库,后面的帧即使没来得及处理也不影响结果。这样 CPU 占用可以压到 20% 以下。
6. 现场调优:阈值档位、照片攻击与离线部署的三个细节
考勤系统上线后,最先暴露的问题通常是误识别和漏识别。先按上一章的阈值表设成 0.55,再根据一星期的实际记录调整。逆光环境建议把摄像头改成固定曝光,让 dlib 检测器在亮度变化大的时段也能稳定找到人脸。如果员工背景经常有人走动,可以把摄像头广角收窄,或者把检测框限制在画面中央区域,减少远处路人脸的干扰。
照片代打卡是这类离线系统被质疑最多的地方。dlib 本身不带活体检测,最简单的缓解手段是用关键点算眼睛纵横比,要求考勤时连续几帧检测到眨眼动作:
def eye_aspect_ratio(shape, side="right"): pts = [shape.part(i) for i in range(36, 42)] if side == "right" \ else [shape.part(i) for i in range(42, 48)] vertical_1 = np.linalg.norm(np.array((pts[1].x, pts[1].y)) - np.array((pts[5].x, pts[5].y))) vertical_2 = np.linalg.norm(np.array((pts[2].x, pts[2].y)) - np.array((pts[4].x, pts[4].y))) horizontal = np.linalg.norm(np.array((pts[0].x, pts[0].y)) - np.array((pts[3].x, pts[3].y))) return (vertical_1 + vertical_2) / (2.0 * horizontal)正常睁眼时 EAR 大约在 0.2 到 0.35,闭眼时降到 0.1 以下。考勤时连续三帧 EAR 小于 0.15,就提示“请注视摄像头”,防止员工低头录入或直接用照片遮挡。这个方法不依赖任何额外模型,是 dlib 68 关键点在考勤系统里最实用的扩展。
部署时还有一些小坑值得提前处理。第一,dlib模型路径不要写死绝对路径,把检测器模型、特征模型和数据库连接字符串都抽到一个 config 文件中,不同工控机上的目录差异就不会影响启动。第二,摄像头索引不一定是 0,USB 摄像头可能被识别成 1 或 2,启动时循环尝试VideoCapture并检查isOpened()。第三,SQLite 写入频率不高,但记得到期清理历史数据,半年后打卡表轻松超过十万行时,再用开工号和日期的联合索引,查询依然能保持在几十毫秒内。
本文还有配套的精品资源,点击获取