简介:本资源是一套基于Python、Flask与dlib实现的人脸识别企业考勤管理系统,属于高分毕业设计项目源码,面向计算机相关专业的毕业生及课程设计学习者,可帮助解决人脸考勤场景下的身份验证与出勤统计问题。项目已通过导师指导与答辩评审,得分97分,在Windows 10/11环境下完成严格调试,下载后即可运行,并配有完整部署教程。压缩包共416个文件,约103.71MB,其中32个py文件承载后端业务逻辑与dlib人脸识别核心,26个html与118个js、61个css构成前端交互界面,另有大量jpg、png、gif等图片资源用于人脸样本与页面素材,以及xml、字体文件等辅助内容。目前已有170人学习关注。读者可获得完整可运行的考勤系统源码、数据库与依赖配置、人脸识别模块实现思路及部署排错参考,适合直接用于毕业设计答辩或课程设计提交。
1. 从一张打卡照片说起:这套 Flask + dlib 考勤系统到底解决什么问题
很多公司还在用指纹机打卡,冬天手指干裂识别不了,夏天出汗也识别不了,前台每个月月底对着 Excel 手动核对迟到早退,这种重复劳动既费时又容易出错。这套基于 Python + Flask + dlib 的人脸识别企业考勤管理系统,核心思路就是用摄像头替代指纹头,员工走到门口刷脸,后台自动记录时间、判断迟到早退、生成考勤报表。它适合两类人:一类是正在做毕业设计、需要一套能跑通、能演示、有完整文档的项目的学生;另一类是中小企业里想低成本搭一套内部考勤工具的后端或全栈工程师。整套系统不依赖云服务,本地一台带摄像头的电脑就能跑起来,技术栈是 Python 做识别、Flask 做 Web 服务、SQLite 或 MySQL 存数据,前端用 Bootstrap 快速搭页面。下面我会按「环境怎么配 → 人脸识别怎么接 → 考勤逻辑怎么写 → 坑在哪 → 怎么验证」的顺序,把每个环节的参数和代码都摊开讲清楚。
2. 环境搭建与依赖选型:dlib 编译为什么总翻车
2.1 Python 版本与虚拟环境的选择
dlib 对 Python 版本和编译工具链非常敏感,这是整套系统里第一个容易翻车的地方。我一般会锁定 Python 3.8 到 3.10 之间,太新的版本(比如 3.12)在装 dlib 时经常找不到预编译包,只能源码编译,而源码编译又依赖 CMake 和 Visual Studio Build Tools,新手很容易卡在这一步。虚拟环境用 venv 就够了,不需要上 conda,因为 conda 装 dlib 有时会拉一个和 OpenCV 冲突的版本。
# 创建虚拟环境,指定 Python 3.9 python -m venv venv # Windows 激活 venv\Scripts\activate # macOS / Linux 激活 source venv/bin/activate # 先升级 pip,避免旧版 pip 解析依赖出错 python -m pip install --upgrade pip这段命令的逻辑是先隔离出一个干净的 Python 环境,避免全局包污染。参数上python -m venv venv里的第二个venv是目录名,可以改成你喜欢的名字。激活之后命令行前面会出现(venv)前缀,说明环境生效了。如果激活时报「无法加载文件,因为在此系统上禁止运行脚本」,那是 Windows 执行策略的问题,用管理员 PowerShell 执行Set-ExecutionPolicy RemoteSigned即可。
2.2 dlib 与 OpenCV 的安装顺序和版本匹配
依赖安装顺序很关键:先装 CMake,再装 dlib,最后装 OpenCV 和 Flask。如果先装了 OpenCV,再装 dlib 时可能因为 numpy 版本被顶掉而报错。我常用的版本组合是 dlib 19.24、opencv-python 4.8、Flask 2.3、numpy 1.24,这套组合在 Windows 和 Ubuntu 上都验证过。
# 先装构建工具 pip install cmake # 安装 dlib,如果本机没有编译环境,用预编译 wheel pip install dlib==19.24.2 # 安装 OpenCV 和 Flask pip install opencv-python==4.8.1.78 pip install Flask==2.3.3 pip install flask-sqlalchemy==3.0.5 pip install numpy==1.24.3逻辑说明:cmake是 dlib 源码编译的前置工具;dlib==19.24.2这个版本在 PyPI 上有对应的 Windows wheel,能省掉大量编译时间;opencv-python负责摄像头读取和图像预处理;flask-sqlalchemy用来把考勤记录映射成数据库表。参数上,如果你在 Linux 服务器部署,dlib 可能需要pip install dlib --no-cache-dir强制重新编译,并且提前apt install build-essential cmake libopenblas-dev liblapack-dev。
提示:如果
pip install dlib卡在「Building wheel for dlib」超过五分钟,大概率是在源码编译。此时可以换用pip install dlib-bin,这是社区维护的预编译版本,接口完全一致。
2.3 Flask 项目骨架与目录结构
环境好了之后,先把 Flask 的骨架搭出来。我习惯按功能拆目录,而不是把所有代码塞进一个 app.py,因为考勤系统后面要加人脸注册、识别、报表导出,混在一起维护成本很高。
face_attendance/ ├── app.py # Flask 入口 ├── models.py # 数据库模型 ├── face_utils.py # dlib 人脸检测与特征提取 ├── attendance.py # 考勤逻辑 ├── static/ │ ├── css/ │ └── js/ ├── templates/ │ ├── index.html # 打卡页面 │ ├── register.html # 人脸注册页面 │ └── report.html # 考勤报表页面 ├── data/ │ └── faces/ # 存放注册人脸特征文件 └── requirements.txt这个结构里,face_utils.py是核心,负责把一张图片转成 128 维特征向量;attendance.py负责比对特征并写入数据库;app.py只做路由和请求分发。data/faces/目录用来存每个人注册后的特征文件,格式可以是 pickle 或 npy,不建议直接存原始照片,既占空间又有隐私风险。
3. 人脸识别核心链路:从摄像头帧到 128 维特征向量
3.1 dlib 人脸检测器的选择:HOG 还是 CNN
dlib 提供两种人脸检测器:HOG 和 CNN。HOG 速度快,在 CPU 上就能跑,适合考勤这种「一个人站在摄像头前」的场景;CNN 精度高,但需要 GPU 或者较长的推理时间。我一般默认用 HOG,因为考勤机通常固定位置、光照稳定,HOG 的召回率足够。如果公司门口光线复杂、员工戴帽子或口罩较多,再考虑换 CNN,但要注意 CNN 模型文件mmod_human_face_detector.dat需要单独下载,体积约 700KB。
import dlib import cv2 import numpy as np # 初始化 HOG 人脸检测器和 68 点关键点预测器 detector = dlib.get_frontal_face_detector() predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat") face_rec_model = dlib.face_recognition_model_v1("dlib_face_recognition_resnet_model_v1.dat") def extract_face_feature(image_path): """从图片中提取第一张人脸的 128 维特征向量""" img = cv2.imread(image_path) # dlib 需要 RGB 格式,OpenCV 默认 BGR rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 第二个参数 1 表示对图像上采样一次,提升小脸检测率 faces = detector(rgb, 1) if len(faces) == 0: return None # 取面积最大的人脸,避免背景误检 face = max(faces, key=lambda r: r.width() * r.height()) shape = predictor(rgb, face) # 第 3 个参数 num_jitters 控制抖动次数,1 表示不抖动,速度快 feature = face_rec_model.compute_face_descriptor(rgb, shape, 1) return np.array(feature)逻辑说明:detector(rgb, 1)里的1是上采样次数,值越大检测越细但越慢,考勤场景用 1 就够。max(faces, key=...)是为了防止画面里出现多张脸时取错目标,实际部署时如果多人同时打卡,应该改成遍历所有人脸并逐一比对。compute_face_descriptor返回的是 128 维向量,这个向量就是后续比对的基础。参数num_jitters设为 1 时速度最快,设为 10 会让人脸特征更稳定但耗时增加约十倍,注册阶段可以用 10,识别阶段用 1。
3.2 人脸比对:欧氏距离阈值怎么定
拿到 128 维特征后,比对就是算两个向量的欧氏距离。距离越小越像,但阈值定多少直接决定误识率和拒识率。dlib 官方建议 0.6 作为默认阈值,但实际考勤场景我一般会调到 0.45 到 0.5 之间,因为考勤宁可让员工多刷一次,也不能让陌生人混进来。
def compare_faces(feature1, feature2, threshold=0.48): """比对两个人脸特征,返回是否匹配及距离""" distance = np.linalg.norm(feature1 - feature2) return distance < threshold, distance # 加载已注册的人脸特征库 import pickle, os def load_known_faces(face_dir="data/faces"): known = {} for file in os.listdir(face_dir): if file.endswith(".pkl"): name = file.replace(".pkl", "") with open(os.path.join(face_dir, file), "rb") as f: known[name] = pickle.load(f) return known def recognize(face_feature, known_faces): best_name, best_dist = None, 1.0 for name, feat in known_faces.items(): matched, dist = compare_faces(face_feature, feat) if matched and dist < best_dist: best_name, best_dist = name, dist return best_name, best_dist逻辑说明:np.linalg.norm计算的是 L2 距离,这是 dlib 特征的标准比对方式。threshold=0.48是我在几十次测试后定的经验值,光线均匀时 0.5 也能用,但逆光或侧脸时距离会偏大,所以留一点余量。recognize函数遍历所有人脸库,取距离最小的那个作为结果,如果没有任何一个低于阈值就返回None,表示未注册人员。参数上,如果公司人数超过 500,遍历比对会变慢,这时候应该上向量索引,比如用 faiss 做近似最近邻搜索,但毕业设计规模用遍历完全够。
3.3 人脸注册接口:把特征写进文件而不是数据库
注册环节我建议把特征存成 pickle 文件,而不是塞进数据库 BLOB 字段。原因是特征文件方便备份和迁移,而且读取速度比数据库快。数据库里只存员工基本信息和考勤记录。
from flask import Flask, request, jsonify import os, pickle app = Flask(__name__) FACE_DIR = "data/faces" os.makedirs(FACE_DIR, exist_ok=True) @app.route("/register", methods=["POST"]) def register(): name = request.form.get("name") file = request.files.get("photo") if not name or not file: return jsonify({"code": 1, "msg": "姓名和照片不能为空"}) # 保存临时图片 tmp_path = os.path.join(FACE_DIR, f"{name}_tmp.jpg") file.save(tmp_path) feature = extract_face_feature(tmp_path) if feature is None: os.remove(tmp_path) return jsonify({"code": 2, "msg": "未检测到人脸,请重新拍摄"}) # 特征存 pickle,删除原图 with open(os.path.join(FACE_DIR, f"{name}.pkl"), "wb") as f: pickle.dump(feature, f) os.remove(tmp_path) return jsonify({"code": 0, "msg": "注册成功"})逻辑说明:接口接收name和photo两个表单字段,先落盘临时图片,再调用extract_face_feature提取特征。如果返回None说明没检测到人脸,直接删掉临时文件并返回错误码。注册成功后删除原图,只保留特征文件,这样既省空间又避免照片泄露。参数上,request.files.get("photo")要求前端用multipart/form-data提交,如果用 base64 传图需要额外解码,毕业设计里用表单更简单。
4. 考勤业务逻辑:打卡时间、迟到判定与报表生成
4.1 数据库模型设计:员工表与考勤记录表
考勤系统的数据模型不复杂,但字段设计要考虑迟到、早退、缺卡三种状态。我一般用两张表:employee存员工信息,attendance存每次打卡记录。打卡记录里存check_time和status,状态在写入时就算好,避免报表阶段反复计算。
from flask_sqlalchemy import SQLAlchemy from datetime import datetime db = SQLAlchemy() class Employee(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(32), unique=True, nullable=False) department = db.Column(db.String(64)) face_file = db.Column(db.String(128)) # 对应 pkl 文件名 class Attendance(db.Model): id = db.Column(db.Integer, primary_key=True) employee_id = db.Column(db.Integer, db.ForeignKey("employee.id")) check_time = db.Column(db.DateTime, default=datetime.now) status = db.Column(db.String(16)) # normal / late / early employee = db.relationship("Employee", backref="records")逻辑说明:Employee.name加唯一约束,防止同名注册覆盖。face_file存 pkl 文件名而不是路径,方便迁移。Attendance.status用字符串而不是枚举,是为了 SQLite 兼容性,MySQL 里可以换成 ENUM。check_time默认取当前时间,但实际打卡时应该由服务端时间决定,不能信客户端传的时间,否则员工改本机时间就能伪造打卡。
4.2 打卡接口与迟到判定规则
打卡接口是整个系统里调用最频繁的,逻辑要尽量轻。识别到人脸后,先查员工,再判断当前时间是否超过上班时间,最后写记录。上班时间我一般设成 9:00,迟到阈值给 5 分钟缓冲,也就是 9:05 之后算迟到。
from datetime import time, datetime WORK_START = time(9, 0) LATE_BUFFER = 5 # 分钟 @app.route("/checkin", methods=["POST"]) def checkin(): file = request.files.get("photo") tmp_path = "data/tmp_checkin.jpg" file.save(tmp_path) feature = extract_face_feature(tmp_path) os.remove(tmp_path) if feature is None: return jsonify({"code": 2, "msg": "未检测到人脸"}) known = load_known_faces() name, dist = recognize(feature, known) if name is None: return jsonify({"code": 3, "msg": "未注册人员"}) emp = Employee.query.filter_by(name=name).first() now = datetime.now() # 计算迟到分钟数 work_dt = now.replace(hour=WORK_START.hour, minute=WORK_START.minute, second=0) late_minutes = (now - work_dt).total_seconds() / 60 status = "late" if late_minutes > LATE_BUFFER else "normal" record = Attendance(employee_id=emp.id, check_time=now, status=status) db.session.add(record) db.session.commit() return jsonify({"code": 0, "name": name, "status": status, "time": now.strftime("%H:%M:%S")})逻辑说明:接口先落盘临时图,提取特征后立刻删除,避免磁盘堆积。recognize返回姓名和距离,距离可以记日志用于后续调阈值。迟到判定用now - work_dt算分钟数,超过LATE_BUFFER就标late。参数上,WORK_START和LATE_BUFFER建议放到配置文件或数据库,方便不同部门设不同班次。如果一天要打两次卡(上班和下班),需要再加一个type字段区分。
4.3 考勤报表:按人按天聚合的 SQL 写法
报表页面要展示每个人每天的打卡状态,用 SQL 的GROUP BY聚合最直接。下面这条查询按员工和日期分组,统计正常、迟到次数。
SELECT e.name AS 姓名, e.department AS 部门, DATE(a.check_time) AS 日期, MIN(a.check_time) AS 首次打卡, SUM(CASE WHEN a.status = 'normal' THEN 1 ELSE 0 END) AS 正常次数, SUM(CASE WHEN a.status = 'late' THEN 1 ELSE 0 END) AS 迟到次数 FROM attendance a JOIN employee e ON a.employee_id = e.id WHERE a.check_time >= :start_date AND a.check_time < :end_date GROUP BY e.name, e.department, DATE(a.check_time) ORDER BY 日期 DESC, 姓名 ASC;逻辑说明:DATE(a.check_time)在 SQLite 和 MySQL 里都能用,把时间戳截断到天。SUM(CASE WHEN...)是条件聚合的标准写法,比多次子查询快。参数:start_date和:end_date由前端日期选择器传入,注意结束日期要加一天,否则当天的记录会被漏掉。如果公司有多个班次,MIN(check_time)只能取首次打卡,需要根据班次表关联判断。
注意:SQLite 的
DATE()函数对时区不敏感,如果服务器用 UTC 时间,报表日期会差一天。解决办法是存时间时统一用本地时间,或者查询时用datetime(check_time, 'localtime')。
5. 避坑与排查:dlib 识别不准、Flask 上传失败、时间对不上
5.1 现象:同一个人识别距离忽大忽小,有时超过阈值
原因:摄像头自动曝光导致画面亮度变化,dlib 的 HOG 检测器对光照敏感,特征向量会漂移。另外,注册时用的照片和打卡时的角度差异太大也会导致距离偏大。
解决:注册时拍三张不同角度的照片,取特征平均值作为该员工的模板。打卡时如果距离在阈值边缘(比如 0.45 到 0.55 之间),可以提示「请正对摄像头再试一次」,而不是直接拒绝。代码上把compare_faces的阈值做成可配置,方便现场调试。
5.2 现象:Flask 接收上传图片时报 413 Request Entity Too Large
原因:Flask 默认限制上传大小为 16MB,手机拍的原图可能超过这个值。另外,如果前端用 base64 传图,字符串体积会比原图大三分之一。
解决:在app.config里设置MAX_CONTENT_LENGTH,比如 8MB,同时前端上传前用 canvas 压缩到 640 宽度。
app.config["MAX_CONTENT_LENGTH"] = 8 * 1024 * 1024 # 8MB如果还是报错,检查 Nginx 的client_max_body_size,默认是 1MB,需要同步调大。
5.3 现象:打卡记录的时间比实际时间早或晚几个小时
原因:服务器时区设置不对,或者 Docker 容器里默认 UTC 时间。datetime.now()取的是系统时间,系统时区错了,记录就错了。
解决:在 Flask 启动时设置时区,或者用datetime.utcnow()存 UTC,展示时再转本地时间。我一般直接在app.py开头加:
import os, time os.environ["TZ"] = "Asia/Shanghai" time.tzset() # Linux/macOS 有效,Windows 需要改注册表或用 pytzWindows 下time.tzset()不可用,建议用pytz或zoneinfo显式转换。
5.4 现象:dlib 在服务器上安装报错「No module named ‘dlib’」但 pip 显示已安装
原因:虚拟环境没激活,或者 pip 装到了全局 Python 里。另一种可能是服务器上有多个 Python 版本,pip和python指向不同版本。
解决:用python -m pip install dlib代替pip install dlib,确保装到当前解释器。然后用python -c "import dlib; print(dlib.__version__)"验证。如果还不行,检查which python和which pip是否在同一目录。
5.5 现象:多人同时打卡时,系统只识别到一个人
原因:extract_face_feature里用了max(faces, key=...)只取最大人脸,多人入镜时其他人被忽略。
解决:把函数改成返回所有人脸特征,打卡接口遍历比对,每个人分别写记录。但要注意,多人同时打卡时摄像头画面里人脸较小,检测率会下降,实际部署建议一人一刷,或者用广角摄像头并提高上采样次数到 2。
6. 进阶技巧:用 Flask 蓝图拆分模块 + 用距离日志反推阈值
6.1 用蓝图把注册、打卡、报表拆成独立模块
当系统功能变多,app.py会膨胀到几百行,改一个接口容易碰到另一个。Flask 的蓝图(Blueprint)就是解决这个问题的。把注册、打卡、报表分别放到views/目录下,每个模块一个蓝图,app.py只负责注册和启动。
# views/checkin.py from flask import Blueprint, request, jsonify from face_utils import extract_face_feature, load_known_faces, recognize from models import db, Employee, Attendance checkin_bp = Blueprint("checkin", __name__) @checkin_bp.route("/checkin", methods=["POST"]) def checkin(): # 具体逻辑同上,略 pass # app.py from flask import Flask from views.checkin import checkin_bp from views.register import register_bp from views.report import report_bp app = Flask(__name__) app.register_blueprint(checkin_bp, url_prefix="/api") app.register_blueprint(register_bp, url_prefix="/api") app.register_blueprint(report_bp, url_prefix="/api")逻辑说明:蓝图让每个模块有自己的路由前缀,/api/checkin、/api/register、/api/report互不干扰。参数上url_prefix可以统一加/api,方便前端用 axios 统一配置 baseURL。拆分后每个文件控制在 100 行以内,调试时定位问题更快。
6.2 用距离日志反推最佳阈值
dlib 的默认阈值 0.6 在考勤场景偏松,但具体调到多少,不能拍脑袋。我的做法是在recognize里把每次比对的best_dist写进日志文件,跑一周后统计:同一个人打卡的距离分布,以及未注册人员被误识的距离分布。两条分布的交叠点就是最佳阈值。
import logging logging.basicConfig(filename="data/distance.log", level=logging.INFO, format="%(asctime)s %(message)s") def recognize_with_log(face_feature, known_faces): best_name, best_dist = None, 1.0 for name, feat in known_faces.items(): matched, dist = compare_faces(face_feature, feat) if matched and dist < best_dist: best_name, best_dist = name, dist logging.info(f"best_name={best_name} best_dist={best_dist:.4f}") return best_name, best_dist逻辑说明:日志记录每次识别的姓名和最小距离,格式化成四位小数方便统计。跑一周后,用 pandas 读日志,分别画best_name非空和空的距离直方图。如果误识率偏高,就把阈值从 0.48 降到 0.45;如果拒识率偏高,就升到 0.52。这个方法是数据驱动的,比凭感觉调参靠谱得多。
6.3 验证方法:用混淆矩阵检查识别效果
最后一步验证,不要只看「能不能识别」,要算准确率和误识率。准备一个测试集:每个员工 5 张不同角度的照片,再加 10 个未注册人员的照片。跑一遍识别,统计四个指标:正确识别、错误识别、正确拒绝、错误拒绝。用 sklearn 的confusion_matrix一行就能出结果。
from sklearn.metrics import confusion_matrix, accuracy_score y_true = [...] # 真实标签,未注册用 "unknown" y_pred = [...] # 系统输出 print(confusion_matrix(y_true, y_pred)) print("准确率:", accuracy_score(y_true, y_pred))逻辑说明:y_true和y_pred都是字符串列表,confusion_matrix会自动对齐标签。准确率高于 95% 且误识率为 0 时,这套阈值就可以上线了。如果误识率不为 0,优先降阈值,因为考勤系统里「放陌生人进来」比「让员工多刷一次」严重得多。
我自己做这套系统时,最大的教训是:不要一开始就追求 CNN 高精度,HOG 在固定摄像头场景下足够用,把时间花在阈值调优和日志分析上,收益更大。另外,注册照片一定要现场拍,不要用员工微信头像,头像的美颜和角度会让特征严重偏移,后期怎么调阈值都救不回来。希望帮到你。
本文还有配套的精品资源,点击获取