简介:本资源是一套基于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. 从一张考勤表说起:Python+Flask+dlib 到底能做出什么
公司行政每个月末最头疼的事,不是算工资,而是对考勤。纸质签到表字迹潦草、代签成风,指纹机一到冬天就识别不了脱皮的手指,IC 卡忘带就得找前台手动补录。这套「基于 Python+Flask+dlib 的人脸识别企业考勤管理系统」,解决的正是这个场景:员工走到摄像头前,系统自动认出是谁、记录打卡时间、写入数据库,管理员在网页后台看报表。它适合三类人——正在找毕业设计题目的计算机专业学生、想给中小团队搭一套轻量考勤工具的后端开发者、以及想通过一个完整项目把 Python Web 开发和计算机视觉串起来的学习者。整套技术栈不复杂:Flask 负责网页和接口,dlib 负责从人脸图像里提取特征向量,前端用 HTML+JavaScript 调摄像头。下面按「先跑通识别、再搭起系统、最后避开坑」的顺序讲清楚。
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_key=True) name = db.Column(db.String(50), nullable=False) department = db.Column(db.String(50)) face_encoding = db.Column(db.LargeBinary) # 存 128 维向量的二进制 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=db.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_id=emp.id, status='normal') 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 locked
SQLite 默认的锁机制是写操作独占整个数据库文件。打卡接口在写入 Attendance 记录时,如果同时有另一个请求在写,就会报锁错误。解决办法是开启 WAL 模式:db.session.execute('PRAGMA journal_mode=WAL')。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 版本也有要求。我一般锁dlib==19.24.0和face_recognition==1.3.0,这两个组合在 Windows 和 Linux 上都验证过。
5. 进阶技巧:让考勤系统从「能用」到「好用」
5.1 用 Redis 缓存特征向量,把比对速度压到 50ms 以内
数据库里存几百个员工的 128 维向量,每次打卡都全量查询再算距离,员工多了之后接口会变慢。我一般把特征向量加载到 Redis 里,用 Hash 结构存,key 是员工 ID,value 是向量的二进制。打卡时直接从 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(f'report_{year}_{month}.xlsx', index=False) return groupeddb.extract是 SQLAlchemy 的日期提取函数,在 SQLite 和 PostgreSQL 下都能用。groupby之后agg(['min', 'max'])一次拿到每天的首末打卡时间。late列用apply逐行判断,数据量大的时候可以改成向量化操作,但月度报表通常几千行,apply完全够用。导出的 Excel 直接发给行政,省去手动整理的麻烦。
5.4 我踩过的最大的坑:别在注册环节偷懒
最后说一个教训。我第一版做注册功能时,只让员工拍一张正面照就存特征。结果上线第一周,有三个员工因为戴眼镜和不戴眼镜的差异被拒识,还有一个因为换了发型导致距离超标。后来改成注册时拍五张——正面、左转 30 度、右转 30 度、戴眼镜、不戴眼镜——取五张特征向量的平均值存入数据库。拒识率从 8% 降到 1% 以下。注册环节多花三十秒,后面省下的是每天被员工堵在工位旁边问「为什么又识别不了」的时间。希望帮到你。
本文还有配套的精品资源,点击获取