多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Python+Flask+dlib人脸识别考勤系统:从环境搭建到阈值调优全流程

Python+Flask+dlib人脸识别考勤系统:从环境搭建到阈值调优全流程 简介本资源是一套基于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 dlib19.24.2 # 安装 OpenCV 和 Flask pip install opencv-python4.8.1.78 pip install Flask2.3.3 pip install flask-sqlalchemy3.0.5 pip install numpy1.24.3逻辑说明cmake是 dlib 源码编译的前置工具dlib19.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 还是 CNNdlib 提供两种人脸检测器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, keylambda 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, threshold0.48): 比对两个人脸特征返回是否匹配及距离 distance np.linalg.norm(feature1 - feature2) return distance threshold, distance # 加载已注册的人脸特征库 import pickle, os def load_known_faces(face_dirdata/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 特征的标准比对方式。threshold0.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_okTrue) 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_keyTrue) name db.Column(db.String(32), uniqueTrue, nullableFalse) department db.Column(db.String(64)) face_file db.Column(db.String(128)) # 对应 pkl 文件名 class Attendance(db.Model): id db.Column(db.Integer, primary_keyTrue) employee_id db.Column(db.Integer, db.ForeignKey(employee.id)) check_time db.Column(db.DateTime, defaultdatetime.now) status db.Column(db.String(16)) # normal / late / early employee db.relationship(Employee, backrefrecords)逻辑说明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(namename).first() now datetime.now() # 计算迟到分钟数 work_dt now.replace(hourWORK_START.hour, minuteWORK_START.minute, second0) late_minutes (now - work_dt).total_seconds() / 60 status late if late_minutes LATE_BUFFER else normal record Attendance(employee_idemp.id, check_timenow, statusstatus) 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(filenamedata/distance.log, levellogging.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(fbest_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 在固定摄像头场景下足够用把时间花在阈值调优和日志分析上收益更大。另外注册照片一定要现场拍不要用员工微信头像头像的美颜和角度会让特征严重偏移后期怎么调阈值都救不回来。希望帮到你。本文还有配套的精品资源点击获取
返回列表