
简介本资源是一套基于Python、Flask与dlib实现的人脸识别企业考勤管理系统面向计算机相关专业的毕业设计学生与课程设计学习者帮助解决考勤场景中人脸识别签到、员工信息管理与后台统计等实际问题。压缩包共416个文件约103.71MB包含32个Python后端脚本、26个HTML页面、118个JavaScript与61个CSS前端资源以及大量jpg、png图片素材另有readme、md说明文档与依赖配置文件前后端结构完整。项目已在Windows 10/11环境严格调试部署教程齐全下载即可运行并附使用文档可直接作为毕业设计或课程设计参考。目前已有170人学习关注。读者可获得完整源码、数据库与前端页面、人脸识别核心逻辑、部署说明及排错思路便于快速理解Flask项目组织方式与dlib人脸识别集成方法也能在此基础上进行功能扩展与二次开发。1. 从一张考勤表说起PythonFlaskdlib 到底能做出什么公司行政每个月末最头疼的事不是算工资而是对考勤。纸质签到表字迹潦草、代签成风指纹机一到冬天就识别不了脱皮的手指IC 卡忘带就得找前台手动补录。这套「基于 PythonFlaskdlib 的人脸识别企业考勤管理系统」解决的正是这个场景员工走到摄像头前系统自动认出是谁、记录打卡时间、写入数据库管理员在网页后台看报表。它适合三类人——正在找毕业设计题目的计算机专业学生、想给中小团队搭一套轻量考勤工具的后端开发者、以及想通过一个完整项目把 Python Web 开发和计算机视觉串起来的学习者。整套技术栈不复杂Flask 负责网页和接口dlib 负责从人脸图像里提取特征向量前端用 HTMLJavaScript 调摄像头。下面按「先跑通识别、再搭起系统、最后避开坑」的顺序讲清楚。2. 人脸识别这条链路dlib 凭什么能认出同一个人2.1 从像素到 128 维向量dlib 的三步走dlib 的人脸识别不是拿两张照片直接做像素比对那样光照一变就废了。它的流程分三步。第一步是人脸检测用 HOG方向梯度直方图特征配合线性分类器在图像里框出人脸区域。HOG 的思路是把图像切成小格子统计每个格子里像素梯度的方向分布人脸的五官边缘会产生稳定的梯度模式据此就能定位。第二步是关键点定位dlib 的 68 点模型会在人脸框内标出眉毛、眼睛、鼻子、嘴唇、下巴的轮廓坐标这一步的作用是把人脸「摆正」——即使你歪着头也能通过关键点做仿射变换对齐。第三步是特征提取把对齐后的人脸送进一个深度残差网络输出一个 128 维的浮点向量。这个向量就是人脸的「指纹」同一个人不同角度拍出来的向量欧氏距离很小不同人之间距离很大。理解这三步很重要因为后面调参和排错都围绕它们展开。检测阶段出问题表现为「框不到脸」关键点阶段出问题表现为「对齐歪了」特征提取阶段出问题表现为「同一个人距离忽大忽小」。每个阶段的输入输出都可以单独验证不要一上来就端到端调试。2.2 环境搭建dlib 安装是第一个拦路虎dlib 依赖 C 编译工具链直接pip install dlib在 Windows 上大概率报错。我一般推荐先装 CMake 和 Visual Studio Build Tools再用 conda 装预编译版本省去编译时间。# 方案一conda 安装推荐新手省去编译 conda install -c conda-forge dlib # 方案二pip 安装需要本机有 CMake 和 C 编译器 pip install cmake pip install dlib # 验证安装是否成功 python -c import dlib; print(dlib.__version__)如果 pip 安装卡在编译阶段超过五分钟八成是缺 Visual Studio 的 C 桌面开发组件。去 Visual Studio Installer 里勾上「使用 C 的桌面开发」再重试。Linux 下相对简单sudo apt install cmake build-essential之后 pip 基本能过。装完之后还要下载两个模型文件shape_predictor_68_face_landmarks.dat关键点模型约 100MB和dlib_face_recognition_resnet_model_v1.dat特征提取模型约 22MB。这两个文件不放对位置代码跑到一半会直接抛异常。2.3 最小可跑的人脸比对脚本在搭 Flask 之前先用一个独立脚本验证 dlib 能不能正确区分人脸。这一步跑通了后面接 Web 只是换了个调用入口。import dlib import numpy as np import face_recognition # 对 dlib 的封装接口更友好 # 加载两张待比对的人脸图片 img1 face_recognition.load_image_file(person_a_1.jpg) img2 face_recognition.load_image_file(person_a_2.jpg) img3 face_recognition.load_image_file(person_b.jpg) # 提取 128 维特征向量一张图可能检测到多张脸取第一张 enc1 face_recognition.face_encodings(img1)[0] enc2 face_recognition.face_encodings(img2)[0] enc3 face_recognition.face_encodings(img3)[0] # 计算欧氏距离 dist_same np.linalg.norm(enc1 - enc2) dist_diff np.linalg.norm(enc1 - enc3) print(f同一个人不同照片的距离: {dist_same:.4f}) print(f不同人之间的距离: {dist_diff:.4f}) # 经验阈值小于 0.6 判为同一人大于 0.6 判为不同人这段代码的核心逻辑是face_encodings内部完成了检测、对齐、特征提取三步返回 128 维 numpy 数组。np.linalg.norm算的是欧氏距离。参数方面0.6 是 face_recognition 库的默认阈值实际项目中我一般调到 0.45 到 0.5 之间因为考勤场景宁可让员工多刷一次也不能把 A 认成 B。如果同一个人两张照片的距离超过 0.5先检查是不是一张正脸一张侧脸侧脸的关键点对齐会偏特征向量自然偏。如果不同人的距离小于 0.5检查是不是双胞胎或者长相极其相似的同事这种情况需要额外加活体检测或提高阈值。3. Flask 后端把识别能力包成考勤接口3.1 项目骨架与数据库设计Flask 的优势是轻不需要像 Django 那样生成一堆目录。我一般按功能拆成四个模块app.py负责路由和启动face_utils.py封装 dlib 的检测和比对models.py定义数据库表config.py放路径和阈值配置。数据库用 SQLite 就够了考勤系统并发不高SQLite 零配置、单文件、方便打包。# models.py from flask_sqlalchemy import SQLAlchemy db SQLAlchemy() class Employee(db.Model): id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(50), nullableFalse) department db.Column(db.String(50)) face_encoding db.Column(db.LargeBinary) # 存 128 维向量的二进制 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, defaultdb.func.now()) status db.Column(db.String(10)) # normal / late / early_leaveface_encoding字段用LargeBinary存 numpy 数组的tobytes()结果读取时用np.frombuffer还原。为什么不存图片路径因为图片文件多了之后管理麻烦而且每次比对都要重新提取特征速度慢。存向量之后比对时直接算距离一次查询就能拿到所有人的特征做批量匹配。注意LargeBinary在不同数据库下的长度限制不同SQLite 没有限制MySQL 需要指定LONGBLOB。3.2 打卡接口从摄像头帧到考勤记录前端通过getUserMedia拿到摄像头画面每隔一秒截一帧转成 base64 发给后端。后端收到后解码成图片提取特征和数据库里所有员工的特征逐一比对取距离最小的那个如果小于阈值就写入考勤记录。# app.py 核心打卡接口 import base64 import numpy as np import face_recognition from flask import Flask, request, jsonify from models import db, Employee, Attendance app Flask(__name__) app.config[SQLALCHEMY_DATABASE_URI] sqlite:///attendance.db db.init_app(app) THRESHOLD 0.48 # 比对阈值考勤场景偏严格 app.route(/api/checkin, methods[POST]) def checkin(): data request.get_json() img_b64 data[image].split(,)[1] # 去掉 data:image/jpeg;base64, 前缀 img_bytes base64.b64decode(img_b64) # 将字节流转为 numpy 数组供 face_recognition 使用 nparr np.frombuffer(img_bytes, np.uint8) img face_recognition.load_image_file(io.BytesIO(img_bytes)) encodings face_recognition.face_encodings(img) if not encodings: return jsonify({code: 1, msg: 未检测到人脸}) current_enc encodings[0] # 从数据库取出所有员工特征做批量比对 employees Employee.query.all() known_encodings [np.frombuffer(e.face_encoding) for e in employees] distances face_recognition.face_distance(known_encodings, current_enc) min_idx np.argmin(distances) if distances[min_idx] THRESHOLD: emp employees[min_idx] record Attendance(employee_idemp.id, statusnormal) db.session.add(record) db.session.commit() return jsonify({code: 0, name: emp.name, distance: float(distances[min_idx])}) else: return jsonify({code: 2, msg: 未匹配到员工})face_distance内部就是算欧氏距离比手写循环快。THRESHOLD设 0.48 是血泪经验0.6 太松戴口罩或者光线暗的时候容易把两个人搞混0.4 太紧同一个人稍微侧脸就认不出。0.48 在多数办公场景下误识率和拒识率比较平衡。如果你们公司有双胞胎阈值要降到 0.4 以下同时加一个「二次确认」按钮让员工在识别失败时手动输入工号补录。io.BytesIO那行是把字节流转成文件对象因为load_image_file既接受路径也接受文件对象。3.3 前端调摄像头与 base64 编码前端不需要复杂框架原生 JavaScript 就够。核心是navigator.mediaDevices.getUserMedia拿到视频流画到 canvas 上再toDataURL转 base64。// 每 1.5 秒截一帧发送避免请求过于频繁 const video document.getElementById(video); const canvas document.getElementById(canvas); const ctx canvas.getContext(2d); navigator.mediaDevices.getUserMedia({ video: { width: 640, height: 480 } }) .then(stream { video.srcObject stream; }); setInterval(() { ctx.drawImage(video, 0, 0, 640, 480); const base64 canvas.toDataURL(image/jpeg, 0.8); // 0.8 质量压缩 fetch(/api/checkin, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ image: base64 }) }) .then(res res.json()) .then(data { if (data.code 0) { document.getElementById(result).innerText 打卡成功${data.name}; } }); }, 1500);toDataURL的第二个参数是 JPEG 压缩质量0.8 在清晰度和传输大小之间比较合适。640x480 的分辨率足够 dlib 检测再大只会增加传输时间。1.5 秒的间隔是防止同一个人连续打卡刷屏实际部署时可以改成「识别成功后暂停 5 秒再继续」。注意getUserMedia在非 HTTPS 环境下浏览器会拒绝本地开发用localhost没问题部署到服务器必须配 SSL 证书否则摄像头根本打不开。4. 避坑与排查那些让我加班到凌晨的瞬间4.1 坑一dlib 检测不到人脸但图片里明明有人现象是接口返回「未检测到人脸」但把同一张图用画图工具打开人脸清清楚楚。原因通常是图片的 EXIF 方向信息没处理。手机拍的照片会带一个旋转标记OpenCV 和 dlib 读进来是按原始像素读的不会自动旋转结果人脸是倒着的HOG 检测器自然框不到。解决办法是在读取后加一步方向校正或者在前端 canvas 绘制时先根据 EXIF 旋转。另一个常见原因是图片太大dlib 的 HOG 检测器对超过 2000 像素的图会漏检我一般先把图缩到 800 像素宽再送检测。4.2 坑二同一个人早上能识别下午就识别不了这是光照变化导致的。dlib 的 128 维向量对光照有一定鲁棒性但前提是训练数据里包含各种光照条件。如果员工注册时只拍了一张正面顺光照片下午逆光时特征向量偏移就会超过阈值。解决思路有两个注册时让员工拍三张不同角度的照片取平均向量或者在前端加一个简单的直方图均衡化把过暗或过亮的画面先拉回来。我一般两个都做注册三张的成本很低均衡化也就几行代码。4.3 坑三Flask 开发服务器一上生产就崩app.run()是开发服务器单线程同时来两个打卡请求就排队。考勤高峰期几十个人同时刷直接超时。必须用 Gunicorn 或 uWSGI 部署。Gunicorn 的命令是gunicorn -w 4 -b 0.0.0.0:5000 app:app-w 4表示 4 个 worker 进程。但注意dlib 的模型加载在每個 worker 里都会占一份内存4 个 worker 就是 4 份内存不够的机器会 OOM。折中方案是-w 2或者把识别逻辑拆成独立的微服务Flask 只负责转发请求。4.4 坑四SQLite 并发写入报 database is lockedSQLite 默认的锁机制是写操作独占整个数据库文件。打卡接口在写入 Attendance 记录时如果同时有另一个请求在写就会报锁错误。解决办法是开启 WAL 模式db.session.execute(PRAGMA journal_modeWAL)。WAL 模式下读和写可以并发写和写仍然串行但考勤场景写操作很短串行完全够用。如果并发量真的很大换 PostgreSQL但那就超出这个项目的轻量定位了。4.5 坑五打包成 zip 后别人跑不起来毕业设计源码包最常见的翻车点就是路径写死。shape_predictor_68_face_landmarks.dat的路径如果写成D:\project\models\...别人解压到 C 盘就找不到。正确做法是用os.path.join(os.path.dirname(__file__), models, xxx.dat)拼相对路径。另外 requirements.txt 里要写清楚 dlib 的版本不同版本 API 有差异face_recognition对 dlib 版本也有要求。我一般锁dlib19.24.0和face_recognition1.3.0这两个组合在 Windows 和 Linux 上都验证过。5. 进阶技巧让考勤系统从「能用」到「好用」5.1 用 Redis 缓存特征向量把比对速度压到 50ms 以内数据库里存几百个员工的 128 维向量每次打卡都全量查询再算距离员工多了之后接口会变慢。我一般把特征向量加载到 Redis 里用 Hash 结构存key 是员工 IDvalue 是向量的二进制。打卡时直接从 Redis 取全部向量省去数据库查询和反序列化的时间。实测 500 个员工的情况下比对耗时从 200ms 降到 40ms 左右。Redis 的hgetall一次拿回所有向量再用 numpy 做批量距离计算比逐个循环快一个数量级。5.2 活体检测防止拿照片打卡这是考勤系统最容易被钻的空子。员工拿手机里同事的照片对着摄像头dlib 照样能提取特征并匹配成功。轻量级的活体检测方案是「眨眼检测」用 dlib 的 68 点模型拿到眼睛的六个关键点计算眼睛纵横比EAR连续几帧 EAR 从大到小再变大说明眨了眼。EAR 的计算公式是(上眼睑到下眼睑的距离) / (眼角到眼角的距离)正常睁眼时 EAR 在 0.25 到 0.3 之间闭眼时降到 0.1 以下。在打卡接口里加一个状态机要求 3 秒内检测到一次眨眼才判定为活体。这个方案不需要额外硬件纯软件实现代价是打卡时间从 1 秒变成 3 秒左右。5.3 考勤报表的生成与导出管理员后台需要看月度报表我一般用 pandas 做聚合再用 openpyxl 导出 Excel。核心逻辑是按员工 ID 和日期分组统计每天的首次打卡时间和末次打卡时间和规定的上班时间比对标记迟到和早退。import pandas as pd from datetime import time def generate_report(year, month): records Attendance.query.filter( db.extract(year, Attendance.check_time) year, db.extract(month, Attendance.check_time) month ).all() df pd.DataFrame([{ employee_id: r.employee_id, date: r.check_time.date(), time: r.check_time.time() } for r in records]) # 按员工和日期聚合取最早和最晚打卡时间 grouped df.groupby([employee_id, date])[time].agg([min, max]).reset_index() # 标记迟到上班时间 9:00 grouped[late] grouped[min].apply(lambda t: t time(9, 0)) grouped.to_excel(freport_{year}_{month}.xlsx, indexFalse) return groupeddb.extract是 SQLAlchemy 的日期提取函数在 SQLite 和 PostgreSQL 下都能用。groupby之后agg([min, max])一次拿到每天的首末打卡时间。late列用apply逐行判断数据量大的时候可以改成向量化操作但月度报表通常几千行apply完全够用。导出的 Excel 直接发给行政省去手动整理的麻烦。5.4 我踩过的最大的坑别在注册环节偷懒最后说一个教训。我第一版做注册功能时只让员工拍一张正面照就存特征。结果上线第一周有三个员工因为戴眼镜和不戴眼镜的差异被拒识还有一个因为换了发型导致距离超标。后来改成注册时拍五张——正面、左转 30 度、右转 30 度、戴眼镜、不戴眼镜——取五张特征向量的平均值存入数据库。拒识率从 8% 降到 1% 以下。注册环节多花三十秒后面省下的是每天被员工堵在工位旁边问「为什么又识别不了」的时间。希望帮到你。本文还有配套的精品资源点击获取