ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

基于Python+OpenCV+dlib的人脸识别考勤打卡系统实现与部署

基于Python+OpenCV+dlib的人脸识别考勤打卡系统实现与部署 简介基于PythonOpenCVdlib库开发的人脸识别考勤打卡系统完整项目面向计算机相关专业正在准备毕业设计、课程设计或期末大作业的学生也适合需要人脸识别实战练习的开发者。项目实现人脸信息录入、人脸识别打卡、上下班时间设置、打卡日志导出等核心功能并附项目说明文档与环境依赖清单便于快速运行和二次开发。资源共657个文件压缩包约196.2MB主要包含Python源码、动态链接库、可执行程序、配置文件和说明文档类型覆盖完整目录结构清晰可直接对照学习。目前已有393人学习下载作为课程大作业或毕设参考具备较高实用性。通过该项目可深入理解人脸识别考勤系统的模块划分、OpenCV与dlib的配合用法以及打卡数据的收集与导出思路。1. 课程大作业里的“人脸识别考勤打卡系统”本质上是一条图像识别流水线很多人看到“课程大作业基于pythonopencvdlib库实现的人脸识别考勤打卡系统”这个标题第一反应是找现成的代码然后直接跑起来交差。但实际上这个项目真正值钱的部分不是那几十行“打卡框”而是把 OpenCV 的图像处理、dlib 的人脸检测与特征提取、128 维描述子的比对再加上打卡记录的去重机制全部串起来。这几个环节只要有一个没想明白就会看到“检测到人脸但认不出人”“同一张脸每次打卡都算一次”“换个光线就翻车”的现象。这篇文章从选择 dlib 而不是 OpenCV 自带检测器开始讲到环境安装里的 dlib 编译坑再到特征提取、入库、实时比对和 CSV 打卡记录最后落在实际部署需要校准的阈值和防误判技巧上。适合正在做课程设计、毕业设计的人也适合在考勤门禁机上想自己实现离线识别而不调用云 API 的工程师。2. 在人脸识别考勤系统里python、opencv、dlib 各自分担什么角色2.1 OpenCV 管画面前处理dlib 管人脸检测与特征提取这个项目里 OpenCV 和 dlib 的职责有重叠但不冲突。OpenCV 负责读取摄像头、颜色格式转换、图像缩放、绘制矩形框和文字dlib 负责“哪里有人脸”“五官在哪”“这个人是谁”这三个核心问题。入门者最容易犯的错误是拿 OpenCV 的 Haar 级联检测器替代 dlib 的人脸检测然后发现侧脸检测不出来或者把橱窗里的人脸也框出来。dlib 提供的人脸检测器默认是基于 HOG方向梯度直方图和线性 SVM 的模型。它在中等分辨率、正脸朝向的场景下非常稳定而且不需要额外下载模型文件。配合 dlib 的 68 点人脸关键点模型可以做到人脸对齐也就是把眼睛、鼻子、嘴的位置矫正到统一坐标再交给后面的识别模型。这个对齐动作才是提高识别精度的关键比换一个更大的网络更有效。在考勤打卡场景里摄像头通常固定在门框或闸机旁边人一般是正脸或者略微偏头HOG 检测器足够用。dlib 还有一个 CNN 人脸检测器精度更高但速度慢很多部署在普通 CPU 上很难做到实时。所以课程项目里用get_frontal_face_detector()是合理的选型。2.1.1 为什么选 dlib 而不是 OpenCV 自带的人脸检测器我先说结论OpenCV 自带的 Haar 级联不适合做考勤打卡。Haar 级联是 2001 年的方法对光照和姿态非常敏感检测框经常抖动而且没有直接给出可以用于对齐的关键点。如果你要自己写人脸识别考勤系统通常需要三个模型的协同人脸检测、关键点定位、特征提取。dlib 在同一个生态里解决了前两个第三个也有配套模型。OpenCV 后来提供了基于深度学习的 DNN 检测器精度不错但你需要额外下载 Caffe 或 TensorFlow 权重且它只给出人脸框没有关键点。如果你已经有 OpenCV DNN 的人脸检测器那你还需要另外找关键点模型不如直接用 dlib 全家桶方便。检测方式速度侧脸鲁棒性需要额外模型适用场景OpenCV Haar快差无正脸、硬件弱、不要求关键点dlib HOG中中等无考勤摄像头、中等距离、正脸dlib CNN慢好需下载 cnn 模型精度优先、GPU 或强 CPUOpenCV DNN中好需下载模型不想引入 dlib 依赖时可以看到dlib HOG 是课程作业里代价最低的方案而且它和 dlib 的关键点、特征提取模型天然衔接不用自己做人脸框到关键点的配对逻辑。2.1.2 shape_predictor 和 face_recognition_model 在考勤里的分工shape_predictor_68_face_landmarks.dat是 dlib 自带的 68 点人脸关键点模型负责定位眉毛、眼角、鼻尖、嘴角等位置。dlib_face_recognition_resnet_model_v1.dat是一个基于 ResNet 的 128 维特征提取模型它把一张对齐后的 112x112 或 150x150 人脸图像压缩成一个 128 维的浮点向量。这个向量在人脸识别领域被称为“人脸嵌入face embedding”。在考勤系统里这两个模型配合的方式是先用人脸检测器找到人脸框再用关键点模型算出 68 个点接着通过dlib.get_face_chip()做仿射变换得到对齐后的裁剪图最后把裁剪图送入特征提取模型得到 128 维向量。特征提取时用的图像必须是同一个对齐方式否则今天测的向量和明天测的向量根本不在同一个空间。这也是为什么很多人“换了张照片就识别不了”的直接原因。import dlib 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 get_face_feature(img_rgb): faces detector(img_rgb, 1) if len(faces) 0: return None shape predictor(img_rgb, faces[0]) aligned_face dlib.get_face_chip(img_rgb, shape, size150) return facerec.compute_face_descriptor(aligned_face)这段代码里detector(img_rgb, 1)的第二个参数是上采样次数1表示把图像放大一倍再检测能提高小人脸检出率但计算量会增加。size150是get_face_chip输出的人脸图像边长150 像素对 dlib 的识别模型来说比较平衡如果摄像头离人脸远可以调大到 200但如果原始图像分辨率低调大只会插值不会增加细节。2.2 特征比对的底层逻辑128 维描述子是怎么来的dlib 的face_recognition_model_v1内部是一个残差网络作者使用近似三元组损失去训练它让同一个人的不同照片距离更近不同人的照片距离更远。最终输出的 128 维向量并不是传统意义上的“特征矩”或“哈希值”更像是在一个高维向量空间里的坐标。两个人越相似坐标距离越近。所以识别时最常用的做法就是计算两个向量之间的欧氏距离。dlib 官方给出的参考是距离小于 0.6 基本可以认为是同一个人0.6 到 0.7 之间属于模糊区超过 0.7 基本不是同一个人。这个 0.6 是经验值不是理论阈值。实际考勤环境里摄像头画面噪声、人脸角度、光线色温都会让距离变大所以第四节里我会讲怎么根据你自己录的数据重新校准阈值。还有一个容易忽略的点必须使用同一个shape_predictor和face_recognition_model文件。网上有各种重新训练过的 dlib 模型它们输出的向量空间不一致不能混用。你下载的代码里如果只给了源码而没有那两个 .dat 文件跑起来第一件事就是去 dlib 官方文件列表里下载配套版本。3. 搭建可复现的人脸考勤环境python、opencv、dlib 的最小安装路径3.1 版本组合怎么选为什么 dlib 安装容易踩坑很多人在安装依赖时直接pip install dlib然后看到一个红色的 CMake 编译错误就以为是人脸识别算法本身有问题。实际上 dlib 是一个需要 C 编译的库Python 只是一个包装。Windows 下如果你没有 Visual Studio 的 C 编译套件pip install dlib就会尝试从源码编译成功率很低。经验上比较稳的组合是Python 3.8 或 3.10搭配 OpenCV 4.x 和 dlib 19.22 或 19.24。这个组合在 Windows 和 Linux 上都有预编译轮子。如果你用的是 Python 3.11 以上dlib 官方没有提供预编译包conda-forge 也不一定能找到合适的版本除非你愿意花时间踩编译坑否则不建议用于课程作业。opencv-python和opencv-contrib-python的区别也要说一句前者只包含 OpenCV 主模块足够这个项目用后者额外包含 contrib 模块比如一些特征匹配算法。考勤系统只需要基本图像读写、缩放、绘制装opencv-python就行不要装两个版本的 OpenCV否则会出现MODULE_NOT_FOUND或者 dll 加载顺序混乱。3.2 安装步骤和常见报错3.2.1 使用 pip 安装 dlib 碰到编译错误怎么办python -m pip install --upgrade pip python -m pip install opencv-python python -m pip install dlib在干净的环境里这三条命令的前两条一般不会出问题。如果第三条报error: Microsoft Visual C 14.0 is required说明你的电脑缺少 C 编译器。最简单的解决方法是先确认你的 Python 版本如果低于 3.11可以用conda安装替代方案也可以下载别人编译好的 wheel 文件。对于 Windows 用户我更推荐直接使用 Miniconda然后走 conda-forge 渠道因为 conda-forge 里的 dlib 是预编译的不需要本地编译器。具体做法见下一小节。注意即使你用了pip安装成功也要留意 dlib 的依赖比如CMake和Boost不过这些是编译时才需要的安装预编译包后并不需要手动配置。3.2.2 使用 conda 安装 dlib 的替代方案conda create -n attend python3.8 -y conda activate attend conda install -c conda-forge dlib opencv -yconda create创建了一个名为attend的干净环境指定 Python 3.8conda activate attend激活环境conda install从 conda-forge 频道安装 dlib 和 opencv。注意这里安装的是 conda 版的 opencv不要再通过 pip 安装opencv-python否则两个版本会共存import cv2时到底加载哪一个由 Python 的搜索路径决定经常出现“实际运行的 OpenCV 版本和你安装的不一致”这种诡异问题。安装完成后在同一个终端里启动 Python做一次导入检查import cv2 import dlib import sys print(python:, sys.version) print(opencv:, cv2.__version__) print(dlib:, dlib.__version__)cv2.__version__和dlib.__version__会分别输出版本号。如果import cv2报ModuleNotFoundError: No module named cv2先检查你有没有激活正确的环境如果在 Jupyter Notebook 里运行需要重启 Kernel 再导入。3.3 验证安装读取摄像头并检测人脸的最小代码装好之后先不要急着写复杂代码用一个小脚本确认摄像头和 dlib 都正常。注意如果你用的是 Jupyter Notebook摄像头调用的 GUI 窗口有时会无响应建议把代码存成test_webcam.py再运行。import cv2 import dlib det dlib.get_frontal_face_detector() cap cv2.VideoCapture(0) if not cap.isOpened(): raise SystemExit(摄像头打开失败检查权限和索引号) ret, frame cap.read() if ret: gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces det(gray, 0) print(检测到人脸数:, len(faces)) for f in faces: cv2.rectangle(frame, (f.left(), f.top()), (f.right(), f.bottom()), (0, 255, 0), 2) cv2.imshow(test, frame) cv2.waitKey(0) cap.release() cv2.destroyAllWindows()VideoCapture(0)里0是摄像头索引如果电脑插了多个摄像头可能需要换1或2。det(gray, 0)的0表示不做图像上采样能获得最快的检测速度。画矩形时(f.left(), f.top())和(f.right(), f.bottom())分别表示人脸框的左上角和右下角cv2.rectangle的最后一个参数2是线条粗细。这里有一个常见选择det(gray, 1)能提升小脸检测率但检测时间会变长。在实时打卡场景我倾向于先用det(gray, 0)跑通流程再根据实际摄像头分辨率决定是否增大上采样次数。4. 实现人脸识别考勤打卡系统的关键代码从录入到打卡4.1 录入人脸把新人脸转成 128 维特征存进本地人脸库4.1.1 用摄像头或图片目录采集人脸录入阶段要解决的是“把某个人的脸变成可存储的向量”。如果是单人照直接从一张图片提取特征如果一张图里有多个人的脸需要先检测出人脸框再分别提取。最简单可靠的方式是注册用户时让他正对摄像头按一次空格拍摄一张并且只提取画面中唯一一张人脸。import cv2 import dlib import numpy as np import os det dlib.get_frontal_face_detector() pre dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) rec dlib.face_recognition_model_v1(dlib_face_recognition_resnet_model_v1.dat) def extract_feature_from_file(img_path): img cv2.imread(img_path) if img is None: return None rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) faces det(rgb, 1) if len(faces) ! 1: print(f{img_path}: 检测到 {len(faces)} 张人脸跳过) return None shape pre(rgb, faces[0]) chip dlib.get_face_chip(rgb, shape, size150) vec np.array(rec.compute_face_descriptor(chip)) return vecimg_path指向一张本地人脸照片cv2.imread读的是 BGR 颜色空间而 dlib 的检测器预期输入是 RGB所以必须先cvtColor。det(rgb, 1)使用上采样 1 倍可以检出小尺寸人脸代价是时间延长。compute_face_descriptor返回一个 dlib vector 对象这里用np.array(...)转成 NumPy 数组方便后面对 128 个维度做向量运算。4.1.2 序列化和存储把特征存进 pickle 文件特征向量本身是 128 个浮点数存储很简单。课程项目一般用一个字典来维护身份映射键是姓名值是特征向量和录入时间。整个字典通过 pickle 序列化到本地文件下次启动时再加载。import pickle import os def register_face(name, vec, db_pathfaces.pkl): db {} if os.path.exists(db_path): with open(db_path, rb) as f: db pickle.load(f) db[name] vec with open(db_path, wb) as f: pickle.dump(db, f)db_path可以改成你自己项目里的路径比如data/faces.pkl。db[name] vec这一步会把同名的旧特征覆盖掉这是合理的因为人脸会随发型、胖瘦变化定期重新录入可以保证识别效果。如果想让一个用户存多张不同角度照片可以把 dict 改造成{name: [vec1, vec2, ...]}比对时对这个列表取最小距离即可。注意pickle 不是安全格式如果你的程序要接收外部上传的.pkl文件不要直接加载。考勤数据属于个人敏感信息生产环境应存到数据库并限制文件访问权限。课程设计里用 pickle 只是为了快速迭代。4.2 实时打卡摄像头逐帧识别并写考勤记录4.2.1 比对函数欧氏距离与阈值设定打卡时程序从摄像头读取每一帧提取当前人脸特征然后与库里所有特征逐一计算欧氏距离。距离最小的人脸如果小于阈值就认为是匹配成功。def find_match(vec, db, threshold0.6): min_dist float(inf) min_name None for name, saved_vec in db.items(): dist np.linalg.norm(vec - saved_vec) if dist min_dist: min_dist dist min_name name if min_dist threshold: return min_name, min_dist return None, min_distnp.linalg.norm(vec - saved_vec)计算的就是欧氏距离两个 128 维向量逐项比较后开平方。threshold是核心参数0.6 只适合“清晰室内、正脸、距离固定”的理想场景。实际课程项目往往没有固定摄像头下面这个阈值表可以给你一个起点摄像头距离推荐阈值备注0.5 米内正脸0.55 - 0.58高精度误识别少0.5 - 1 米0.58 - 0.62推荐 0.61 米以上0.62 - 0.68人脸尺寸小特征波动大阈值设得越小系统越严格偶尔会把同一个人判断成陌生人设得越大越容易把两个人判断成同一个也就是误打卡。后面第五节会介绍如何用距离分布来校准这个值。4.2.2 打卡记录写入 CSV 和去重机制考勤系统最大的坑是重复打卡。一个人站在摄像头前 3 秒程序处理了 90 帧其中 80 帧都识别成功如果不加去重CSV 里就会多出 80 条记录。常见做法是维护一个已打卡集合只有第一次识别成功时才写入。import csv from datetime import datetime CHECKED_IN set() def mark_attendance(name, csv_pathattendance.csv): today datetime.now().strftime(%Y-%m-%d) now datetime.now().strftime(%H:%M:%S) key (today, name) if key in CHECKED_IN: return False with open(csv_path, a, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([today, now, name, 正常]) CHECKED_IN.add(key) return TrueCHECKED_IN是一个全局集合元素是(日期, 姓名)元组。之所以要把日期拼进去是因为跨天之后同一姓名不应该被昨天的打卡记录拦住。encodingutf-8保证中文姓名不乱码newline避免在 Windows 上写 CSV 时出现空行。如果你希望一天结束自动清空集合可以在程序里根据时间判断或者到点重启进程。4.3 可视化与交互在画面上绘制识别结果和打卡状态主循环把前面所有函数串起来同时兼顾显示。这里为了实时性先把原始帧缩小一半再做检测因为 dlib 的 HOG 检测器分辨率越高耗时越长。绘制姓名和距离时如果识别成功会显示绿色框如果没匹配上可以显示红色框。cap cv2.VideoCapture(0) scale 0.5 while True: ret, frame cap.read() if not ret: break small cv2.resize(frame, (0, 0), fxscale, fyscale) rgb cv2.cvtColor(small, cv2.COLOR_BGR2RGB) faces det(rgb, 0) for face in faces: shape pre(rgb, face) chip dlib.get_face_chip(rgb, shape, size150) vec np.array(rec.compute_face_descriptor(chip)) name, dist find_match(vec, db, threshold0.6) x1, y1 face.left(), face.top() x2, y2 face.right(), face.bottom() if name: cv2.rectangle(small, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(small, f{name} {dist:.2f}, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) mark_attendance(name) else: cv2.rectangle(small, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.imshow(attendance, small) key cv2.waitKey(1) 0xFF if key ord(q): break cap.release() cv2.destroyAllWindows()这里有一个细节检测和特征提取都是在缩小的small上运行的所以face.left()等坐标对应当前的缩小图。如果你要标回原始frame需要把坐标除以scale。实际开发里一般直接用缩小的图做显示减少cv2.putText时的字体缩放比例处理。waitKey(1)等待 1 毫秒让 GUI 窗口能刷新画面这个值不等于摄像头帧率但它直接影响循环循环速度。5. 考勤系统的三个进阶点人脸质量过滤、阈值校准和离线部署5.1 人脸质量过滤模糊检测、光线补偿、角度矫正摄像头抓拍的画面不一定都适合做识别。当人脸快速移动时画面会产生运动模糊这种模糊人脸提取出的特征向量噪声很大容易产生远超阈值的距离也可能碰巧匹配到另一个人。所以在特征提取前加一道模糊检测非常简单有效。def is_blurry(gray_img, threshold50): lap cv2.Laplacian(gray_img, cv2.CV_64F).var() return lap thresholdcv2.Laplacian计算图像的二阶导数然后取方差。方差越大图像边缘越锐利说明画面越清晰小于阈值就认为是模糊帧跳过识别。阈值 50 是在 640x480 分辨率下的经验开始值如果摄像头分辨率更高可以适当调大到 80 以上。这能明显减少“人在移动时被误打卡”的情况。5.2 阈值校准用距离分布确定欧氏距离阈值第四节提到的 0.6 只是起点。真正能稳定工作的系统一定要用自己的摄像头录一组数据来校准。方法很简单对库里每个人拍 20 张照片分别提取特征计算“同一个人两两之间的距离”再拿 20 个不同人的照片互相计算距离得到“不同人两两之间的距离”。统计这两组距离的最小值和最大最小值阈值大致取在两组距离的中间值附近。我有一个比较直观的判断方法如果同人距离最大在 0.53不同人距离最小在 0.71那么 0.62 附近的阈值都没问题。但如果两条距离分布有重叠就需要增加注册照片数或者调整摄像头位置。可以用一个简单的脚本统计距离分布把结果打个表格比对类型常见距离范围建议处理同一个人0.35 - 0.60阈值高于最大值不同人0.60 - 1.20阈值低于最小值模糊/角度大0.65 - 0.90拦截或要求重新录入这个表格不是标准答案以你自己算出来的数字为准。5.3 离线部署注意事项内存占用、帧率优化、摄像头选型课程作业如果要求“部署到树莓派或一台普通的教室电脑”一定要控制检测频率。比如每 3 帧只做一次人脸检测和特征提取否则 CPU 会被 dlib 占满其他程序基本没法用。一个折中方案是先拿运动检测或人脸检测的在上一帧的坐标位置作为 ROI只对该区域提取特征。另外考勤打卡不影响系统性能的关键是摄像头不要太大。接 USB 摄像头时尽量选择支持 1280x720 输出的型号然后在代码里用cv2.resize降到 640x480 或更低。OpenCV 的cv2.CAP_PROP_FRAME_WIDTH和cv2.CAP_PROP_FRAME_HEIGHT可以直接设置摄像头采集分辨率。cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)这两行放在VideoCapture之后、read之前。设置参数后读取回来的frame本身就是 640x480省掉了resize。如果设置的输出分辨率摄像头不支持set会静默失败但通常 640x480 所有摄像头都支持。最后别忘了离线环境下一定要把.dat模型文件和faces.pkl一并打包进部署目录不然换一台电脑跑起来还是会提示找不到模型。本文还有配套的精品资源点击获取
返回列表