简介:基于Python+OpenCV+dlib库开发的人脸识别考勤打卡系统完整项目,面向计算机相关专业正在准备毕业设计、课程设计或期末大作业的学生,也适合需要人脸识别实战练习的开发者。项目实现人脸信息录入、人脸识别打卡、上下班时间设置、打卡日志导出等核心功能,并附项目说明文档与环境依赖清单,便于快速运行和二次开发。资源共657个文件,压缩包约196.2MB,主要包含Python源码、动态链接库、可执行程序、配置文件和说明文档,类型覆盖完整,目录结构清晰,可直接对照学习。目前已有393人学习下载,作为课程大作业或毕设参考具备较高实用性。通过该项目可深入理解人脸识别考勤系统的模块划分、OpenCV与dlib的配合用法,以及打卡数据的收集与导出思路。
1. 课程大作业里的“人脸识别考勤打卡系统”,本质上是一条图像识别流水线
很多人看到“课程大作业基于python+opencv+dlib库实现的人脸识别考勤打卡系统”这个标题,第一反应是找现成的代码,然后直接跑起来交差。但实际上,这个项目真正值钱的部分不是那几十行“打卡框”,而是把 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 或强 CPU |
| OpenCV 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, size=150) return facerec.compute_face_descriptor(aligned_face)这段代码里,detector(img_rgb, 1)的第二个参数是上采样次数,1表示把图像放大一倍再检测,能提高小人脸检出率,但计算量会增加。size=150是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 python=3.8 -y conda activate attend conda install -c conda-forge dlib opencv -yconda create创建了一个名为attend的干净环境,指定 Python 3.8;conda 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, size=150) 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_path="faces.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, threshold=0.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.6 |
| 1 米以上 | 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_path="attendance.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="", encoding="utf-8") as f: writer = csv.writer(f) writer.writerow([today, now, name, "正常"]) CHECKED_IN.add(key) return TrueCHECKED_IN是一个全局集合,元素是(日期, 姓名)元组。之所以要把日期拼进去,是因为跨天之后同一姓名不应该被昨天的打卡记录拦住。encoding="utf-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), fx=scale, fy=scale) 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, size=150) vec = np.array(rec.compute_face_descriptor(chip)) name, dist = find_match(vec, db, threshold=0.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, threshold=50): 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一并打包进部署目录,不然换一台电脑跑起来还是会提示找不到模型。
本文还有配套的精品资源,点击获取