Python+Flask+dlib人脸识别考勤系统:从原理到实战
2026/9/23 13:00:48 网站建设 项目流程

简介:这份资源是面向高校计算机相关专业学生与课程设计学习者的企业考勤管理系统完整项目,采用Python、Flask与dlib构建人脸识别核心,前端结合Vue实现管理界面,适合作为毕业设计、课程设计或技术练手参考。压缩包共416个文件,约103.71MB,其中32个py文件承载后端业务与识别逻辑,26个html与118个js、61个css构成前端页面与交互,另有79张jpg、51张png等图片素材及xml、字体等配置资源,目录结构完整,便于按模块查阅。项目已在Windows 10/11环境严格调试,下载即用,并附使用文档与部署教程,可帮助读者快速理解人脸识别考勤的完整实现链路,包括人脸采集、特征比对、考勤记录管理等环节,同时提供排错思路与运行说明。目前已有170人学习关注,适合需要完整项目方案、文档支撑与可运行源码的读者参考借鉴。

1. 从一张打卡照片说起:这套 Flask + dlib 考勤系统到底解决什么问题

很多公司还在用指纹机打卡,冬天手指干裂识别不了,夏天出汗也识别不了,前台每个月月底对着 Excel 手动补卡补到怀疑人生。这套基于 Python + Flask + dlib 的人脸识别企业考勤管理系统,要解决的就是这件事:员工走到摄像头前,系统自动认出是谁、记录时间、写入数据库,后台能按部门按月份导出考勤报表。它适合两类人:一类是正在找毕业设计题目的同学,需要一套能跑通、能讲清楚原理、代码量适中的完整项目;另一类是中小企业里负责行政或 IT 的工程师,想用低成本方案替换掉老旧的指纹打卡机。整套技术栈是 Python 做后端逻辑、Flask 提供 Web 服务和页面路由、dlib 负责人脸检测和 128 维特征提取、SQLite 或 MySQL 存考勤记录。和 OpenCV 自带的 Haar 级联相比,dlib 的 HOG 检测器在正脸场景下误检率更低,配合 ResNet 预训练模型输出的特征向量,同一个人不同角度拍的照片余弦距离能稳定控制在 0.4 以内,这是它能用于考勤而不是玩具 demo 的关键。下面从环境搭建一路讲到活体检测和考勤报表,把每个环节的参数和踩坑点都摊开说。

2. 环境搭建与 dlib 安装:为什么你的 pip install 总是翻车

2.1 Python 版本与依赖选型

dlib 对 Python 版本和编译工具链非常敏感。我一般锁 Python 3.8 或 3.9,这两个版本在 Windows 和 Linux 上都有预编译好的 dlib wheel,能省掉大量编译时间。Python 3.11 以上虽然也能装,但 dlib 19.24 之前的版本没有对应 wheel,pip 会直接去源码编译,需要本机有 CMake 和 Visual Studio Build Tools,新手在这一步翻车的概率超过七成。

核心依赖清单如下:

包名推荐版本作用
Flask2.3.xWeb 框架,路由和模板渲染
dlib19.24.x人脸检测 + 128 维特征提取
opencv-python4.8.x摄像头取流、图像预处理
numpy1.24.x特征向量距离计算
face-recognition1.3.0对 dlib 的封装,简化比对逻辑
Flask-SQLAlchemy3.0.xORM,管理员工和考勤表

face-recognition 这个库底层就是调 dlib,但它把检测、编码、比对封装成了三行代码,毕业设计里用它能少写两百行胶水代码。如果你要讲原理,可以不用它,直接调 dlib 的get_frontal_face_detectorface_recognition_model_v1

2.2 在 Windows 上装 dlib 的可行路径

先确认 Python 版本:

python --version # 输出应为 Python 3.8.x 或 3.9.x

如果版本不对,去 python.org 下载对应安装包,安装时勾选 "Add Python to PATH"。装完后升级 pip:

python -m pip install --upgrade pip

然后安装 CMake,这是 dlib 编译的必备工具:

pip install cmake

接着装 dlib。如果你的 Python 是 3.8/3.9 的 64 位版本,直接:

pip install dlib==19.24.2

pip 会优先找 wheel,找到就直接装,找不到才编译。如果卡在编译阶段报错,最省事的办法是去 GitHub 上找对应版本的 whl 文件手动安装,但注意 whl 文件名里的 cp38、cp39 要和你的 Python 版本匹配,win_amd64 要和系统架构匹配。

装完验证:

import dlib print(dlib.__version__) # 应输出 19.24.2 detector = dlib.get_frontal_face_detector() print("dlib 检测器加载成功")

这段代码先导入 dlib 并打印版本号,然后加载默认的 HOG 人脸检测器。如果这一步不报错,说明 dlib 安装成功。参数上没有什么可调的,get_frontal_face_detector()返回的是一个基于 HOG 特征的检测器对象,后续所有检测都靠它。

2.3 Linux 服务器上的安装差异

Linux 上装 dlib 通常更顺,因为 apt 能直接装编译依赖:

sudo apt update sudo apt install -y build-essential cmake libopenblas-dev liblapack-dev libx11-dev pip install dlib==19.24.2

libopenblas-devliblapack-dev是数值计算库,dlib 的矩阵运算依赖它们。不装的话编译能过,但运行时会报 BLAS 相关的链接错误。libx11-dev是图形界面相关,服务器上如果只跑 Web 不显示窗口,可以不装,但 OpenCV 的imshow会不可用,调试时要注意。

提示:如果你在树莓派上部署,dlib 编译会非常慢,建议直接用 apt 装python3-dlib,虽然版本旧一点,但省时间。

3. 人脸检测与特征提取:dlib 的 128 维向量是怎么算出来的

3.1 HOG 检测器与 CNN 检测器的选择

dlib 提供两种人脸检测器:HOG 和 CNN。HOG 检测器基于方向梯度直方图,速度快,CPU 上就能跑,适合考勤这种正脸场景。CNN 检测器基于 Max-Margin 目标检测,精度更高,能检测侧脸和小脸,但需要 GPU 才跑得动,CPU 上单帧要好几秒。

考勤场景我一般用 HOG,因为员工走到摄像头前基本都是正脸,HOG 足够。代码:

import dlib import cv2 detector = dlib.get_frontal_face_detector() img = cv2.imread("employee.jpg") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 第二个参数 1 表示对图像上采样一次,能检测到更小的脸 faces = detector(gray, 1) for face in faces: x, y, w, h = face.left(), face.top(), face.width(), face.height() cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2) cv2.imwrite("detected.jpg", img)

detector(gray, 1)里的 1 是上采样次数。上采样会把图像放大,让原本像素少的小脸变得可检测,代价是计算量翻倍。考勤场景人脸离摄像头近,用 0 就行;如果摄像头装在远处,用 1 或 2。face.left()等方法返回检测框的左上角坐标和宽高,用于画框或裁剪。

3.2 用 ResNet 模型提取 128 维特征

检测到人脸后,下一步是提取特征。dlib 用的是 ResNet-34 的变体,在 300 万张人脸数据上训练过,输出 128 维向量。同一个人的不同照片,向量之间的欧氏距离小于 0.6;不同人之间大于 0.6。这个 0.6 就是默认阈值。

import dlib import numpy as np detector = dlib.get_frontal_face_detector() # 加载预训练的人脸特征提取模型 sp = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat") facerec = dlib.face_recognition_model_v1("dlib_face_recognition_resnet_model_v1.dat") img = dlib.load_rgb_image("employee.jpg") faces = detector(img, 1) for face in faces: shape = sp(img, face) # 第三个参数 1 表示用 1 次采样计算特征,越大越精确但越慢 descriptor = facerec.compute_face_descriptor(img, shape, 1) vec = np.array(descriptor) print(vec.shape) # (128,) print(vec[:5]) # 打印前 5 维看看

shape_predictor_68_face_landmarks.dat是人脸关键点模型,定位 68 个特征点(眉毛、眼睛、鼻子、嘴巴、下巴轮廓)。compute_face_descriptor会先用关键点做人脸对齐,把脸摆正,再送进 ResNet 提取特征。第三个参数是采样次数,1 表示只算一次,精度够用;设成 10 会更稳但慢十倍。考勤场景用 1 就行。

两个模型文件需要单独下载,dlib 的 GitHub 仓库或官网都有,注意下载后放在项目目录下,路径别写错。

3.3 特征比对与阈值调优

拿到 128 维向量后,比对就是算欧氏距离:

import numpy as np def compare_faces(vec1, vec2, threshold=0.6): distance = np.linalg.norm(vec1 - vec2) return distance < threshold, distance # 假设已知员工 A 的特征向量存在数据库里 known_vec = np.load("employee_a.npy") new_vec = np.array(descriptor) is_same, dist = compare_faces(known_vec, new_vec) print(f"距离: {dist:.4f}, 是否同一人: {is_same}")

np.linalg.norm算的是欧氏距离。阈值 0.6 是 dlib 官方推荐值,但实际用的时候要根据你的摄像头和光照调。我一般会先用 10 个员工各拍 5 张照片,两两比对,画出距离分布图,找到类内距离和类间距离的分界点。如果误识率高(把别人认成你),把阈值降到 0.5;如果拒识率高(你自己认不出来),把阈值升到 0.65。这个调参过程没有捷径,必须用你自己的数据跑一遍。

注意:特征向量存数据库时用 BLOB 类型或直接存成 .npy 文件,不要存成字符串,浮点数转字符串再转回来会有精度损失。

4. Flask 后端与考勤逻辑:从摄像头取流到写入数据库

4.1 Flask 项目结构与路由设计

一个能跑的考勤系统,Flask 这边至少需要这几个路由:

路由方法作用
/GET首页,显示打卡入口
/registerPOST员工注册,上传照片提取特征
/attendancePOST打卡,比对特征并记录时间
/recordsGET考勤记录查询,支持按日期筛选
/exportGET导出 Excel 报表

项目目录结构:

attendance_system/ ├── app.py # Flask 主程序 ├── models.py # 数据库模型 ├── face_utils.py # dlib 封装 ├── static/ │ └── known_faces/ # 存员工特征文件 ├── templates/ │ ├── index.html │ └── records.html └── attendance.db # SQLite 数据库

app.py里初始化 Flask 和数据库:

from flask import Flask, render_template, request, jsonify from flask_sqlalchemy import SQLAlchemy import os app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///attendance.db' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['UPLOAD_FOLDER'] = 'static/known_faces' db = SQLAlchemy(app) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=True)

SQLALCHEMY_DATABASE_URI指向 SQLite 文件,毕业设计用 SQLite 足够,部署到企业时换成 MySQL 只需改这一行。host='0.0.0.0'让局域网内其他机器也能访问,方便测试。debug=True开发时用,上线要关掉。

4.2 员工注册:照片上传与特征入库

注册接口接收前端传来的照片,提取特征后存到数据库:

from flask import request, jsonify import face_recognition import numpy as np import os @app.route('/register', methods=['POST']) def register(): name = request.form.get('name') department = request.form.get('department') file = request.files.get('photo') if not file or not name: return jsonify({'code': 400, 'msg': '缺少姓名或照片'}) # 保存照片 filepath = os.path.join(app.config['UPLOAD_FOLDER'], f"{name}.jpg") file.save(filepath) # 提取特征 image = face_recognition.load_image_file(filepath) encodings = face_recognition.face_encodings(image) if len(encodings) == 0: return jsonify({'code': 400, 'msg': '照片中未检测到人脸'}) if len(encodings) > 1: return jsonify({'code': 400, 'msg': '照片中有多张人脸,请上传单人照'}) # 存特征到文件,路径记到数据库 vec_path = os.path.join(app.config['UPLOAD_FOLDER'], f"{name}.npy") np.save(vec_path, encodings[0]) # 写入员工表(省略 ORM 细节) # employee = Employee(name=name, department=department, vec_path=vec_path) # db.session.add(employee) # db.session.commit() return jsonify({'code': 200, 'msg': '注册成功'})

face_recognition.face_encodings返回一个列表,每张脸对应一个 128 维向量。如果列表为空说明没检测到脸,长度大于 1 说明有多张脸,这两种情况都要拒绝。特征存成.npy文件而不是数据库 BLOB,是因为文件读写更快,而且方便调试时直接np.load查看。数据库里只存文件路径。

4.3 打卡接口:实时比对与防重复打卡

打卡是核心逻辑,要处理三件事:检测人脸、比对特征、防止同一人短时间内重复打卡。

from datetime import datetime, timedelta @app.route('/attendance', methods=['POST']) def attendance(): file = request.files.get('photo') if not file: return jsonify({'code': 400, 'msg': '未收到照片'}) # 临时保存 temp_path = 'static/temp.jpg' file.save(temp_path) # 提取特征 image = face_recognition.load_image_file(temp_path) encodings = face_recognition.face_encodings(image) if len(encodings) == 0: return jsonify({'code': 400, 'msg': '未检测到人脸'}) new_vec = encodings[0] # 遍历所有员工特征比对 known_dir = app.config['UPLOAD_FOLDER'] best_match = None best_distance = 1.0 for fname in os.listdir(known_dir): if not fname.endswith('.npy'): continue known_vec = np.load(os.path.join(known_dir, fname)) distance = np.linalg.norm(known_vec - new_vec) if distance < best_distance: best_distance = distance best_match = fname.replace('.npy', '') if best_distance > 0.6: return jsonify({'code': 404, 'msg': '未匹配到员工,请先注册'}) # 防重复打卡:5 分钟内同一人只记一次 five_min_ago = datetime.now() - timedelta(minutes=5) # recent = Attendance.query.filter_by(name=best_match).filter(Attendance.time > five_min_ago).first() # if recent: # return jsonify({'code': 200, 'msg': f'{best_match} 已打卡,请勿重复'}) # 写入考勤记录 # record = Attendance(name=best_match, time=datetime.now()) # db.session.add(record) # db.session.commit() return jsonify({'code': 200, 'msg': f'{best_match} 打卡成功', 'distance': round(best_distance, 4)})

比对逻辑是遍历所有.npy文件,找距离最小的那个。如果最小距离仍大于 0.6,说明不是已注册员工。防重复打卡用时间窗口,5 分钟内同一人只记一次,避免员工站在摄像头前连续触发。best_distance返回给前端可以用于调试,正式上线可以去掉。

提示:员工数量超过 200 人时,遍历比对会变慢。常见做法是把所有特征向量加载到一个矩阵里,用np.argmin一次性算完,比 Python 循环快几十倍。

5. 避坑与排查:考勤系统上线后最容易翻车的 5 个地方

5.1 现象:白天能识别,晚上全挂

原因:摄像头没有红外补光,晚上光线不足,dlib 的 HOG 检测器直接检测不到人脸。HOG 依赖梯度信息,光线暗的时候梯度不明显,检测器就瞎了。

解决:换带红外补光的摄像头,或者在打卡点加一盏常亮灯。如果硬件改不了,把 OpenCV 读到的帧先做直方图均衡化:

gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) gray = cv2.equalizeHist(gray) faces = detector(gray, 1)

equalizeHist把灰度分布拉均匀,暗部细节会变明显。但这不是万能药,光线太暗还是得靠硬件。

5.2 现象:同一个人,换个角度就认不出来

原因:dlib 的 128 维特征对角度敏感,侧脸超过 30 度,特征距离会明显变大。注册时只拍了一张正脸照,打卡时稍微偏头就匹配不上。

解决:注册时让员工拍 3 到 5 张不同角度的照片,每张提取一个特征向量,打卡时和所有向量比对,取最小距离。存储上可以一个员工对应多个.npy文件,文件名加后缀区分。

5.3 现象:照片里有多个人,系统随机选一个

原因:face_recognition.face_encodings返回列表,代码里直接取encodings[0],如果照片里有两个人,取到的是检测框排序后的第一个,不一定是打卡的人。

解决:注册时强制单人照,检测到多张脸直接拒绝。打卡时如果检测到多张脸,选面积最大的那个,因为打卡的人通常离摄像头最近:

if len(encodings) > 1: # 按人脸框面积排序,取最大的 face_locations = face_recognition.face_locations(image) areas = [(bottom - top) * (right - left) for top, right, bottom, left in face_locations] max_idx = areas.index(max(areas)) new_vec = encodings[max_idx] else: new_vec = encodings[0]

5.4 现象:Flask 部署到服务器后,上传的照片路径报错

原因:Windows 上路径用反斜杠,Linux 上用正斜杠,代码里硬编码了static\\known_faces,到 Linux 上就找不到目录。

解决:统一用os.path.join拼接路径,或者用pathlib.Path。另外 Flask 的UPLOAD_FOLDER要用绝对路径,相对路径在服务器上会相对于启动目录,容易出错:

from pathlib import Path BASE_DIR = Path(__file__).resolve().parent app.config['UPLOAD_FOLDER'] = str(BASE_DIR / 'static' / 'known_faces')

5.5 现象:打卡记录时间比实际早 8 小时

原因:服务器时区是 UTC,datetime.now()返回 UTC 时间,而你在东八区。

解决:用datetime.now()之前先设时区,或者直接用datetime.utcnow() + timedelta(hours=8)。更规范的做法是装pytz

from datetime import datetime import pytz tz = pytz.timezone('Asia/Shanghai') now = datetime.now(tz)

数据库存时间时统一存 UTC,展示时再转本地时区,这样跨时区部署不会乱。

6. 活体检测与报表导出:让这套系统从能用到好用

活体检测是考勤系统从 demo 到实用的分水岭。没有活体检测,员工拿手机里别人的照片对着摄像头就能代打卡。常见做法有两种:一是动作配合,要求员工眨眼或转头,用 dlib 的 68 个关键点算眼睛纵横比(EAR)判断是否眨眼;二是用 OpenCV 的 LBP 纹理分析,区分真实人脸和屏幕翻拍。

眨眼检测的实现思路:dlib 的 68 个关键点里,左眼是 36 到 41 号点,右眼是 42 到 47 号点。眼睛纵横比公式是:

def eye_aspect_ratio(eye_points): # eye_points 是 6 个点的坐标列表 vertical_1 = np.linalg.norm(eye_points[1] - eye_points[5]) vertical_2 = np.linalg.norm(eye_points[2] - eye_points[4]) horizontal = np.linalg.norm(eye_points[0] - eye_points[3]) return (vertical_1 + vertical_2) / (2.0 * horizontal) # 连续帧中 EAR 小于 0.2 然后回到 0.3 以上,判定为一次眨眼

EAR 在眼睛睁开时约 0.3,闭合时降到 0.2 以下。连续检测到 EAR 先降后升,就算一次眨眼。打卡时要求员工在 3 秒内眨一次眼,通过后才进入特征比对。这个逻辑用 dlib 的关键点就能做,不需要额外模型。

报表导出用openpyxlpandas

import pandas as pd from flask import send_file @app.route('/export') def export(): # 假设从数据库查到了记录列表 records = [ {'姓名': '张三', '部门': '技术部', '打卡时间': '2024-01-15 09:02:33'}, {'姓名': '李四', '部门': '市场部', '打卡时间': '2024-01-15 09:15:07'}, ] df = pd.DataFrame(records) output_path = 'attendance_report.xlsx' df.to_excel(output_path, index=False) return send_file(output_path, as_attachment=True)

pd.DataFrame把记录列表转成表格,to_excel写出 xlsx 文件,send_file以附件形式返回给浏览器。列名直接用中文,导出的报表行政人员能直接看。如果数据量大,可以按月份筛选后再导出,避免一次性加载几万条记录。

我自己的习惯是:每次改完比对阈值或换摄像头,先拿 5 个员工各拍 10 张照片跑一遍全量测试,统计误识率和拒识率,确认没问题再上线。这套系统最怕的不是算法不准,而是环境变了没重新调参。希望帮到你。

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

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

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

立即咨询