资讯中心

Python+OpenCV+dlib实现人脸识别考勤打卡系统实战解析

📅 2026/9/27 4:44:25
Python+OpenCV+dlib实现人脸识别考勤打卡系统实战解析
简介基于PythonOpenCVdlib库实现的人脸识别考勤打卡系统是一款适用于课程设计、期末大作业及毕业设计的高分项目源码包作者经导师指导并通过评审综合评分98分。项目面向计算机相关专业学生及需要实战练习的开发者涵盖录入人脸信息、人脸识别打卡、设置上下班时间、导出打卡日志等完整模块运行环境基于Python3.8建议使用PyCharm打开依赖清单见requirement.txt。压缩包共657个文件大小196.2MB主体为501个py源码文件另有44个pyd动态库、22个exe辅助工具、19个png界面截图以及txt、xml、cfg等配置与说明文件结构清晰便于直接阅读与二次开发。资源内附项目说明文档、环境配置文件、模型权重以及相关静态资源可帮助读者快速复现项目流程、理解人脸识别考勤的具体实现方式并在此基础上扩展或修改功能。目前已有393人学习适合作为毕业设计参考或实战练习项目。1. 从考勤打卡这个场景说起为什么人脸识别方案值得自己做一遍考勤打卡看起来是个小需求但真要在实验室或小团队里落地你会发现指纹机要录入指纹、刷卡要带卡、手机定位打卡又容易被钻空子。人脸识别考勤系统恰好卡在“够用”和“有技术含量”之间摄像头是现成的算法栈是 OpenCV dlib 这两个老牌库做成一个桌面应用就能覆盖录入人脸、日常打卡、导出日志的完整闭环。这个用 Python 写的人脸识别考勤打卡系统正是按这个思路组织的源码和项目说明都齐适合正在做课程设计、期末大作业、毕设起步阶段的计算机相关专业学生。我自己拆完一遍的感觉是它不追求花哨的深度学习模型而是把 dlib 的人脸检测、特征提取和人脸比对老老实实串起来每一步都能看到中间结果这对学习者和二次开发反而更友好。2. 技术选型与运行环境为什么是 dlib OpenCV 而不是深度学习方案2.1 先搞清楚 OpenCV 和 dlib 在这个项目里各自干什么很多人一上来就把 OpenCV 和 dlib 混着说其实这两个库的分工非常明确。OpenCV 负责图像读取、色彩空间转换、图像缩放、画框、显示窗口这些基础操作它更像是整个系统的“眼睛”和“手”dlib 负责两件核心的事——人脸检测和人脸关键点定位再利用训练好的模型把检测到的人脸区域转换成一个 128 维的特征向量。这个特征向量就是后续人脸比对的依据。选 dlib 而不是直接上 MTCNN、FaceNet 这种深度学习方案有一个很现实的原因课程设计和考勤这种场景数据量不大也不要求海量并发dlib 提供的是传统机器学习路线模型文件更小CPU 上跑得动依赖也简单。retinaface 这类方案精度是高但环境配置复杂装 CUDA、装 PyTorch 就足够劝退一批人了。这个项目的定位本身就不是刷榜而是演示完整流程所以 dlib 是恰到好处的选择。项目中还带了一个挺容易被忽略的文件feature_all.csv。这是录入阶段生成的人脸特征库每一行是一个人名对应一个 128 维特征向量。识别阶段做的事本质上就是把摄像头实时抓到的脸也转成 128 维向量然后和 CSV 里的每一行算欧氏距离或余弦相似度距离小于阈值就判定为同一个人。2.2 环境搭建Python 3.8 下把 dlib 和 OpenCV 一次装对项目说明里强调环境是 Python 3.8建议用 PyCharm 运行。这一点非常实在因为 dlib 的安装对 Python 版本比较挑剔太新的版本可能没有预编译包太旧又和 OpenCV 的新版本冲突。我把 Windows 下的安装步骤和容易翻车的地方一起说清楚。# 建议使用虚拟环境避免污染全局 Python python -m venv venv venv\Scripts\activate # 先安装 numpy后续编译 dlib 时需要 pip install numpy1.19.5 # 安装 opencv-python pip install opencv-python4.5.3.56 # 安装 dlib pip install dlib19.22.0这里有个关键点一定要先装 numpy 再装 dlib。dlib 在安装过程中如果检测不到 numpy会跳过一些编译优化甚至直接编译失败。版本上我给的这几个是在 Python 3.8 下验证过的组合如果装的是裸的pip install dlib大概率会从源码编译耗时十几分钟不说还可能因为缺少 CMake 和 Visual Studio C 构建工具直接报错。装完以后建议用下面这段代码做一次快速验证确认摄像头和 dlib 的人脸检测器都能正常工作import cv2 import dlib detector dlib.get_frontal_face_detector() cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces detector(gray, 1) for face in faces: cv2.rectangle(frame, (face.left(), face.top()), (face.right(), face.bottom()), (0, 255, 0), 2) cv2.imshow(face_test, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()上面这段代码最需要注意的参数是detector(gray, 1)里的第二个参数1它表示对图像做一次上采样后再检测能提高检测小脸的成功率但代价是计算时间变长。如果摄像头里人脸占画面比例比较大改成 0 速度会快不少。我在实际调试过程中发现这个参数对检测稳定性的影响比预期大尤其是坐得离摄像头远的时候1和0的检出率差距肉眼可见。2.3 虚拟环境目录里的隐藏信息项目压缩包里能看到 activity.bat、deactivate.bat、pyvenv.cfg 这些文件这说明作者是直接在虚拟环境目录下做的项目打包。如果你用 PyCharm 打开项目解释器选择那个venv\Scripts\python.exe基本可以做到开箱即用不需要再手动安装依赖。但要注意一点虚拟环境是绑定绝对路径的换一台电脑后pyvenv.cfg里的home路径不对PyCharm 会报解释器无效这时重新配置一下解释器路径就行不用重新创建环境。3. 系统核心流程拆解录入、训练与打卡是怎么串起来的3.1 整体流程从录入到打卡只有三步把整个考勤系统的运行逻辑压缩成一句话录入阶段采集人脸特征写入 CSV识别阶段实时提取人脸特征与 CSV 比对命中后写入打卡日志。我画了一下模块间的关系可以这样理解录入模块打开摄像头捕获人脸 → 用 dlib 检测人脸区域 → 用 shape_predictor 定位 68 个关键点 → 用 face_recognition_model 提取 128 维特征 → 人名和特征向量写入 feature_all.csv。识别打卡模块同样走检测、关键点、特征提取的管线 → 读 CSV 里的特征库 → 遍历计算欧氏距离 → 距离最小的那个小于阈值就判定命中 → 获取当前系统时间 → 写入 logcat.csv。日志管理模块读取 logcat.csv 展示打卡记录按时间筛选导出或清空记录。时间设置模块设置上班时间和下班时间打卡记录里会标识是否迟到、早退。这个流程里有一个模块很多人第一次做会漏掉——人脸对齐face alignment。直接用原始人脸区域提取特征会因为头部倾斜、侧脸角度导致特征偏差很大。dlib 的做法是先用 68 点关键点检测器找到眼睛、鼻子、嘴巴的位置然后做仿射变换把人脸校准到标准位置最后再送到特征提取模型。项目里虽然没有单独把这个步骤拆成可见模块但标准 dlib 流程里这是默认包含的。3.2 录脸模块的代码实现与数据落盘逻辑录脸是决定整个系统识别准确率的第一道关口。我以项目实际采用的思路为例把核心逻辑整理成一个可运行的代码框架import cv2 import dlib import numpy as np import csv import os detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(model/shape_predictor_68_face_landmarks.dat) facerec dlib.face_recognition_model_v1(model/dlib_face_recognition_resnet_model_v1.dat) FEATURE_FILE feature_all.csv def get_face_feature(frame): 从一帧图像中提取最大人脸的128维特征向量 gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces detector(gray, 1) if len(faces) 0: return None, None # 取画面上面积最大的人脸避免误选背景中的小脸 face max(faces, keylambda f: (f.right() - f.left()) * (f.bottom() - f.top())) shape predictor(gray, face) # 第2个参数是采样半径调大一点对模糊图像更鲁棒 return np.array(facerec.compute_face_descriptor(frame, shape, 20)), face def save_feature(name, feature): 把特征向量追加写入CSV一行一个人 exists os.path.exists(FEATURE_FILE) with open(FEATURE_FILE, a, newline, encodingutf-8) as f: writer csv.writer(f) if not exists: writer.writerow([name] [fdim_{i} for i in range(128)]) writer.writerow([name] feature.tolist())注意看compute_face_descriptor的第三个参数20这是采样次数。dlib 会在这个值指定的次数内对图像做抖动jitter处理每次重新采样提取特征后取平均这样得到的特征向量比单次提取稳定得多。默认值是 1也就是不抖动但考勤系统这种对稳定性要求高的场景我一般会调到 10 到 20。代价是每次特征提取耗时从几十毫秒涨到一两秒录入阶段完全能承受识别阶段要看机器性能我当时在 i5 的笔记本上跑 20 次采样一帧大约 1.6 秒实时性还是可以接受的。录入时还有一个容易被忽视的细节CSV 文件里如果已经有了同名人的特征是覆盖还是追加项目没有做去重所以我在实际使用时养成一个习惯重新录一个人之前先把原来的行删掉或者用新特征单独存一个临时文件比对距离确认差异足够大再写入。3.3 识别打卡模块从摄像头取帧到写日志的完整链路识别打卡模块的逻辑比录入多一个比对环节。下面这段代码展示了从取帧到写日志的核心逻辑注意我在比对阶段没有简单取最小值而是加了“最小距离和第二小距离的比值”这个筛选条件这是我从实际使用中总结出来的防误判经验import csv import time import cv2 import numpy as np import dlib def load_feature_db(csv_path): 加载特征库返回字典: {name: np.array([128])} db {} with open(csv_path, r, encodingutf-8) as f: reader csv.reader(f) header next(reader, None) for row in reader: if len(row) ! 129: continue name row[0] vec np.array(row[1:], dtypenp.float64) db[name] vec return db def recognize(face_feature, db, threshold0.5): 在特征库中寻找最匹配的人脸返回人名或None best_name None best_dist float(inf) second_dist float(inf) for name, vec in db.items(): # 欧氏距离越小说明特征越接近 dist np.linalg.norm(face_feature - vec) if dist best_dist: second_dist best_dist best_dist dist best_name name elif dist second_dist: second_dist dist # 两个条件同时满足才判定为识别成功 if best_dist threshold and (second_dist - best_dist) 0.05: return best_name return None识别时的threshold参数是整个系统里最需要调的东西。dlib 官方文档给的参考值是 0.6但实际使用中 0.6 过于宽松不同人之间特征距离可能小到 0.4 左右同一个人在不同光线下的距离也可能到 0.55。我在自己电脑上测试时0.45 到 0.5 之间误识率最低。这个值没有绝对的正确答案需要根据实际摄像头和环境光线慢慢试。后面避坑章节我会专门讲怎么用一组照片去标定这个阈值。第二小距离的约束是我自己加的一道保险。如果最小距离是 0.49 通过了阈值但第二小距离是 0.51说明这个人跟库里的好几个候选人都长得接近这种情况下判定命中的风险比较大。当second_dist - best_dist太小意味着区分度不够宁可判定为不认识也不要乱打卡——考勤系统误打一次卡比漏打一次卡麻烦得多。4. 关键模块与技术细节特征库管理、考勤判定与日志落盘4.1 特征库的管理策略CSV 不只是存储还是识别速度的关键很多人觉得 feature_all.csv 就是个存储文件随便写写就行。实际上随着录入人数增长这个文件的大小直接影响识别速度。我在项目里用过 30 个人的特征库线性扫描一次大约需要 30 次 128 维欧氏距离计算耗时在毫秒级完全无感。但如果是考勤系统要容纳几百人线性遍历就会开始有延迟需要对特征库做结构化处理。另一个和特征库相关的问题是增量更新。项目目前的逻辑是启动时一次性加载全部特征到内存字典里录入新成员后必须重启程序才能生效。我一般会在 UI 上加一个“刷新特征库”按钮原理很简单重新执行一次load_feature_db更新全局字典。特征库的备份也值得养成习惯。我遇到过几次摄像头角度调整后原来录入的特征识别率骤降的情况后来通过保留不同时间点的 feature_all.csv 备份对比哪个版本的特征在当前的摄像头角度下更稳定反而比重新录一遍人更省事。建议每次批量录入完就把 CSV 复制一份标注日期和数据日志分开存放。4.2 上下班时间设置与迟到早退判定逻辑考勤系统如果只是把人脸识别出来然后记录时间那跟一个打卡机没有区别。真正的业务逻辑在时间判定上。项目设置里可以配置上班时间和下班时间我把判定的核心逻辑整理出来from datetime import datetime def judge_attendance(record_time, work_start, work_end, late_buffer5): 根据打卡时间和上下班时间判定考勤状态 work_start/work_end 为 HH:MM 格式字符串 late_buffer 为迟到宽限分钟数默认宽限5分钟 r_t record_time.strftime(%H:%M) r_dt datetime.strptime(r_t, %H:%M) s_dt datetime.strptime(work_start, %H:%M) e_dt datetime.strptime(work_end, %H:%M) # 迟到超过上班时间但还没到迟到硬线 if r_dt s_dt: late_minutes (r_dt - s_dt).seconds // 60 if late_minutes late_buffer: return 正常宽限 else: return 迟到 # 早退下班前打卡 elif r_dt e_dt: return 早退 else: return 正常这个模块里最值得讨论的是“宽限分钟数”这个参数。我在第一次使用这个系统时直接把迟到判定写死为上班时间之后就算迟到结果同事因为电梯排队晚了两分钟就被标迟到体验很差。后来加了一个 5 分钟的 buffer 参数让使用者可以在界面里自行调整。考勤系统本质上是给人用的规则的灵活性比算法的精确性更重要。下班时间还有个细节可能存在跨天的情况比如夜班岗位晚上 10 点上班、次日早上 6 点下班。如果上下班时间跨过了零点简单的字符串比较就会出错。常见的做法是如果work_end work_start说明下班时间在第二天那么早退判定就要把打卡时间加上 24 小时再算。我建议实现时间判定时把这一层考虑进去避免后期改逻辑。4.3 打卡日志 logcat.csv 的字段设计与幂等控制logcat.csv 是整个系统的输出结果也是老师或导师检查项目时最关心的部分。字段设计我建议包含编号、姓名、打卡时间、日期、考勤状态、打卡类型上班/下班。项目里 logcat.csv 的内容比较简洁但如果你要把它升级成一个可交付的考勤系统一个“是否已打卡”的状态字段是必须的。这里最常见的 bug 是重复打卡——程序在识别成功后下一帧又识别出同一张脸会再次写入一条记录导致一分钟内同一个人的打卡记录刷屏。我在项目里看到的做法是写入前先检查当前时间窗口内是否已经有这个人的记录。不过更合理的方案是把“防重复”逻辑放在写入函数里加一个基于内存的缓存last_record_cache {} def write_log(name, status, cache_seconds10): now datetime.now() key f{name}_{now.strftime(%Y%m%d_%H:%M)} # 同一分钟内同一个人只记录一次 if last_record_cache.get(name) now.strftime(%Y%m%d_%H%M): return False last_record_cache[name] now.strftime(%Y%m%d_%H%M) with open(logcat.csv, a, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([name, now.strftime(%Y-%m-%d %H:%M:%S), status]) return True注意cache_seconds这个参数我没有真正用到而是用了分钟作为自然时间窗口。原因很简单打卡本身只需要上班和下班各一次一分钟内连续写入没有意义用分钟粒度做缓存就够了。如果你想支持中午多次进出可以把它改成按小时或者自定义时间段去重这个逻辑完全取决于你的考勤规则。还有一个容易被忽略的问题logcat.csv 的文件锁。如果程序运行中你用 Excel 手动打开了这个文件Python 在往里面追加数据时会因为文件被占用而抛权限错误。我自己遇到过一次后来处理方式是捕获异常后提示用户关闭文件而不是直接崩溃退出。你如果交付给别人使用这一点一定要做异常处理。5. 避坑指南从环境安装到实际打卡的 6 个高频翻车点5.1 dlib 安装失败Windows 下缺少 CMake 和 C 构建工具现象pip install dlib执行到一半报错提示error: command cl.exe failed或者直接提示找不到 CMake。原因Windows 下如果当前 Python 版本没有对应的 dlib 预编译 wheel 包pip 会尝试从源码编译编译需要 Visual Studio 的 C 编译器和 CMake。大部分学生机器上都没有装这两样所以必翻车。解决优先用预编译包。Python 3.8 下pip install dlib19.22.0通常能直接拉到 wheel。如果还不行去 pypi 或第三方镜像站下载对应版本和对应 Python 版本的.whl文件用pip install 文件路径安装。再不行才考虑装 Visual Studio Build Tools 勾选 C 桌面开发负载然后从源码编译整个过程大约半小时需要耐心。5.2 OpenCV 装成了 opencv-contrib-python 导致功能异常现象项目能跑但人脸检测时偶尔报错或者某些模块找不到而代码里 import cv2 明明没有报错。原因opencv-python 和 opencv-contrib-python 是两个不同的包后者包含额外模块。项目里如果只用了基础功能装哪个都行但如果代码里用了cv2.face这样的子模块例如 LBPH 人脸识别器就必须装 opencv-contrib-python。两个包混装会互相覆盖出现非常诡异的问题。解决统一环境卸载重装成同一个包。考虑到这个项目主要用 dlib 做人脸识别OpenCV 只做图像读写和显示装基础的opencv-python就够了不用追新版本4.5 系列即可新版本在某些摄像头驱动下反而有兼容问题。5.3 摄像头打不开或画面黑屏现象cv2.VideoCapture(0)执行后ret一直是 False或者画面正常但识别不到人脸。原因第一笔记本摄像头被其他软件占用比如 Zoom、微信视频都占着摄像头OpenCV 拿不到设备句柄。第二摄像头索引不对内置摄像头是 0外接 USB 摄像头可能是 1。第三dlib 检测不到人脸因为画面太暗或者人离镜头太远。解决先关掉所有占用摄像头的软件然后写一个循环把VideoCapture(0)到VideoCapture(3)都试一遍找到能用的索引。画面太暗的问题可以在录入阶段加一个亮度判断np.mean(gray)小于 40 就提示用户开灯。人脸太小的问题把检测器的上采样参数调整为 1并且提示用户坐到离摄像头 40 到 70 厘米的范围内。5.4 识别准确率时好时坏光线和角度对特征提取的影响现象白天识别率很高傍晚开灯后同一个人的识别率明显下降或者人正对摄像头能识别低头看手机就识别不了。原因dlib 的人脸特征提取模型是在近似正脸、光照均匀的数据集上训练的极端侧脸、阴阳脸都会让特征向量发生明显偏移。我实测过同一张脸在顺光和逆光下提取的特征向量欧氏距离可以达到 0.45 以上已经接近不同人的距离区间。解决录入阶段尽量覆盖不同光线条件。我一般会给同一个人录入两到三次分别在不同时间段录特征都写进 CSV。识别阶段对画面做一次直方图均衡化cv2.equalizeHist能稍微缓解光照不均匀的问题。如果识别率还是不稳定就需要回到上采样参数和特征提取采样次数上做调整把compute_face_descriptor的第三个参数从 1 改成 10 以上效果非常明显。5.5 CSV 文件中文乱码现象打卡日志里中文名字变成乱码用 Excel 打开 logcat.csv 时中文全是乱码但用 PyCharm 打开是正常的。原因Excel 默认用 ANSI 编码打开 CSV 文件如果 Python 写入时用了 UTF-8 编码但没有写 BOM 头Excel 就会按照本地系统编码去解析中文必然乱码。这个跟程序逻辑没关系纯粹是编码踩坑。解决写入 CSV 时打开文件用encodingutf-8-sig这个编码会在文件开头写入 BOM 标记Excel 就能正确识别为 UTF-8。注意读取时不要用utf-8-sig直接用utf-8或utf-8-sig都行BOM 会被自动跳过。我在改完编码后所有历史乱码文件只能重新生成所以这个坑越早避越好。5.6 同一个人识别成了另一个人阈值过宽和特征库污染现象同事 A 打卡系统显示成同事 B。这是最严重的故障也是考勤系统最不能接受的错误。原因有两个可能。第一是阈值设得太宽比如设成 0.7两个人特征距离本来是 0.55结果被误判为同一个人。第二是录入时特征库写坏了比如录 A 的时候背景里出现了 B 的大脸提取的特征可能是 A 和 B 的混合体。解决先用 5.5 中提到的排除法缩小嫌疑。如果阈值过宽参考项目文档里的建议把阈值调到 0.45 到 0.5 之间重新测试。如果特征库污染重新删除该行并重新录入。为了根除这个问题我在录入模块里加了一个预览功能提取特征前先把检测到的人脸区域实时显示出来确认框住的是本人再点保存这个改动彻底解决了“录错人”的问题。6. 进阶验证与实用扩展给系统加一个“自检模式”和阈值标定工具系统能跑通打卡流程只是第一步真正让它稳定可用还需要做一次性的标定和验证。我用的方法是准备一组测试照片包含每个人的正脸、侧脸、戴眼镜如果有这三种情况然后写一个离线脚本批量测试不同阈值下的误识率和拒识率。import csv import numpy as np import dlib import cv2 # 这就是一个最简的阈值标定脚本 # 逻辑读入一批“本人”照片和“他人”照片计算特征距离画出距离分布 detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(model/shape_predictor_68_face_landmarks.dat) facerec dlib.face_recognition_model_v1(model/dlib_face_recognition_resnet_model_v1.dat) def img_to_feature(img_path): img cv2.imread(img_path) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) faces detector(gray, 1) if len(faces) 0: return None shape predictor(gray, faces[0]) return np.array(facerec.compute_face_descriptor(img, shape, 10)) # 假设 same_pairs 里是同一人的两张照片路径diff_pairs 里是不同人的照片路径 same_dists, diff_dists [], [] for p1, p2 in same_pairs: f1, f2 img_to_feature(p1), img_to_feature(p2) if f1 is not None and f2 is not None: same_dists.append(np.linalg.norm(f1 - f2)) for p1, p2 in diff_pairs: f1, f2 img_to_feature(p1), img_to_feature(p2) if f1 is not None and f2 is not None: diff_dists.append(np.linalg.norm(f1 - f2))统计一下same_dists的最大值和diff_dists的最小值如果两者的中点和same_dists的最大值之间还有余量说明这个环境下的识别是稳定的。阈值取两者之间的中值附近比如同一个人的距离最大是 0.42不同人的距离最小是 0.55那阈值就取 0.48 左右留出 0.06 的安全余量。这个脚本考勤系统上线时跑一次换摄像头、换光线环境后再跑一次比凭感觉调阈值靠谱得多。另一个我强烈推荐加的功能是识别结果的“二次确认”。第一次识别命中后程序不直接写日志而是弹出一张抓拍的人脸缩略图和识别的姓名让用户确认。这虽然多了一步人工操作但在考勤这种严肃场景下误打卡的概率降到几乎为零。等系统运行一段时间确认阈值标定稳定了再把二次确认关闭实现全自动打卡。我自己的习惯是每次拿到要部署的人脸识别类项目先不看源码细节而是把特征库、阈值参数、摄像头索引这几个关键配置全部抽出来放到一个配置文件里避免每次换环境都要在代码里大海捞针。这个项目用 CSV 存特征库已经比很多把数据写在内存里的示例强很多了。拿到源码后我建议你先从录入模块开始跑录两个人然后关掉程序重新启动再测试识别确认整个闭环通畅之后再去调阈值和打磨界面。从那以后每次给类似系统做验收我都会用一份固定的人脸照片集跑一遍阈值标定这个习惯帮我避开了不少摄像头换了以后识别率骤降的麻烦希望也能帮到你。本文还有配套的精品资源点击获取

看完文章,想为自己的企业也做一次专业网站诊断?

尧图顾问免费为您评估现有网站,并给出建站/改版建议与报价方案。

免费获取方案