简介:本资源是一套基于Python、Flask与dlib实现的人脸识别企业考勤管理系统,面向计算机相关专业的毕业设计学生与课程设计学习者,可帮助解决考勤场景下的人脸检测、身份识别与打卡记录管理等核心问题。压缩包共416个文件,约103.71MB,以118个js、61个css、51个png与79个jpg等前端资源为主,配合32个py后端脚本、26个html页面及少量xml、字体与数据文件,构成前后端分离的完整工程结构。该项目为个人高分毕业设计,已通过导师指导与答辩评审,得分97分,并在Windows 10/11环境下严格调试,下载即用。资源内含源码与使用文档,部署教程齐全,读者可据此掌握Flask路由设计、dlib人脸特征提取、数据库建模与前端页面交互等关键环节,也可作为课程设计或二次开发的基础模板。目前已有170人学习关注。
1. 从一份 97 分毕设拆起:Flask + dlib 考勤系统到底能跑出什么
去年帮学弟看毕设,他拿到的需求是"企业考勤管理系统",第一反应是上 SpringBoot + 百度人脸 API,结果导师一句"要本地化、要能离线识别"直接推翻。后来换成本地 dlib + Flask 这套组合,答辩拿了 97 分。这份资源就是那套方案的完整源码包,核心链路是:浏览器调摄像头抓帧 → 后端 Flask 接收 base64 图像 → dlib 做 68 点人脸检测 + ResNet 特征提取 → 与本地人脸库比对 → 命中后写考勤记录进数据库。
它解决的是"人脸识别 + Web 管理后台"这条完整闭环,不是单点 demo。适合三类人:一是毕设/课程设计需要交完整系统的学生,二是想学 Flask 前后端联调但不想从零搭脚手架的开发者,三是需要给小型团队做本地打卡原型的工程师。整套代码在 Windows 10/11 上调试过,下载解压后按文档走就能起服务,省掉的是环境踩坑和模块拼装的时间。
2. 环境搭建与依赖锁定:dlib 编译是第一个分水岭
2.1 为什么 dlib 装不上是常态
dlib 不是纯 Python 包,它底层是 C++ 模板库,pip install dlib 时如果找不到预编译 wheel,就会触发本地编译,而本地编译又依赖 CMake 和 Visual Studio 的 C++ 构建工具。很多人卡在这一步,报错信息通常是CMake is not installed或者error: Microsoft Visual C++ 14.0 or greater is required。这不是代码问题,是工具链缺失。
常见做法有两种:一是装 CMake + VS Build Tools 后源码编译,耗时 10 到 30 分钟;二是直接找对应 Python 版本的 dlib wheel 文件离线安装。我一般推荐第二种,尤其是 Python 3.8/3.9 这两个版本,wheel 资源最全。注意 Python 版本别选太新,3.11 之后部分依赖的兼容性会变差,这个项目在 3.8 上验证最稳。
2.2 依赖安装的完整命令链
先建虚拟环境,别在全局环境里装,dlib 和 opencv 的版本冲突会污染其他项目。
# 创建虚拟环境,指定 Python 3.8 python -m venv venv # Windows 激活 venv\Scripts\activate # 升级 pip,老版本 pip 解析 wheel 会出错 python -m pip install --upgrade pip激活后装核心依赖,顺序有讲究:先装 numpy,因为 dlib 和 opencv 都依赖它,先装能避免重复编译。
# 先装 numpy,锁定 1.23 以下版本,兼容性最好 pip install "numpy<1.24" # 装 opencv,用于图像预处理和摄像头抓帧 pip install opencv-python==4.5.5.64 # 装 dlib,如果本地有 wheel 就指定路径 pip install dlib-19.22.99-cp38-cp38-win_amd64.whl # 装 Flask 及数据库相关 pip install Flask==2.0.3 Flask-SQLAlchemy==2.5.1参数说明:numpy<1.24是因为 1.24 之后移除了部分 dlib 依赖的旧接口;opencv 选 4.5.5 是它在 Windows 上对摄像头设备的兼容性最稳;Flask 2.0.3 是该项目验证过的版本,2.1 之后部分扩展的导入路径有变化。装完用pip list核对一遍,重点看 dlib 和 opencv 是否都显示已安装。
2.3 人脸模型文件的放置位置
dlib 的人脸检测和特征提取需要两个预训练模型文件:shape_predictor_68_face_landmarks.dat和dlib_face_recognition_resnet_model_v1.dat。这两个文件加起来约 100MB,不在 pip 包里,需要单独下载后放到项目指定目录。项目文档里一般会写明路径,常见是放在models/目录下。如果路径不对,运行时会报Unable to open shape_predictor之类的错误,这时候检查代码里dlib.shape_predictor()的入参路径是否和实际文件位置一致。
提示:模型文件路径建议用绝对路径或基于
os.path.dirname(__file__)拼接,别用相对路径,否则换个启动目录就找不到文件。
3. 人脸识别核心链路:从抓帧到比对命中的四步拆解
3.1 前端抓帧与 base64 传输
浏览器端调getUserMedia拿到摄像头流,画到 canvas 上,再toDataURL转成 base64 字符串,POST 给后端。这一步的坑在于图像质量:分辨率太高传输慢,太低识别率掉。项目里一般设成 640x480,JPEG 质量 0.8 左右。
// 前端抓帧并转 base64 const canvas = document.createElement('canvas'); canvas.width = 640; canvas.height = 480; const ctx = canvas.getContext('2d'); ctx.drawImage(videoElement, 0, 0, 640, 480); // 质量 0.8,兼顾清晰度和传输体积 const base64Image = canvas.toDataURL('image/jpeg', 0.8); fetch('/api/attendance/check', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({image: base64Image}) });逻辑说明:drawImage把视频当前帧画到 canvas,toDataURL输出带data:image/jpeg;base64,前缀的字符串。后端接收后要先去掉前缀再解码,否则cv2.imdecode会返回 None。参数上,640x480 是识别精度和传输速度的平衡点,再低到 320x240 时侧脸和戴眼镜的场景容易漏检。
3.2 后端解码与 dlib 特征提取
后端拿到 base64 后,先解码成 numpy 数组,再转灰度图,然后走 dlib 的检测和特征提取流程。
import base64 import cv2 import numpy as np import dlib # 初始化检测器和特征提取器 detector = dlib.get_frontal_face_detector() sp = dlib.shape_predictor('models/shape_predictor_68_face_landmarks.dat') facerec = dlib.face_recognition_model_v1('models/dlib_face_recognition_resnet_model_v1.dat') def extract_face_descriptor(base64_str): # 去掉 data:image/jpeg;base64, 前缀 img_data = base64_str.split(',')[1] img_bytes = base64.b64decode(img_data) nparr = np.frombuffer(img_bytes, np.uint8) img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) # 转灰度,dlib 检测器只吃灰度图 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 检测人脸,第二个参数是上采样次数,1 表示放大一倍检测 faces = detector(gray, 1) if len(faces) == 0: return None # 取第一张脸,计算 68 点 shape = sp(gray, faces[0]) # 提取 128 维特征向量 descriptor = facerec.compute_face_descriptor(img, shape) return np.array(descriptor)逻辑说明:detector(gray, 1)的第二个参数是上采样次数,设 1 会把图像放大一倍再检测,能提升小脸检出率,但耗时翻倍。如果场景里人脸占画面比例大,设 0 就够。compute_face_descriptor返回的是 128 维向量,这是 dlib 的 ResNet 模型输出,后续比对就是算两个向量的欧氏距离。参数上,距离阈值一般设 0.6,小于 0.6 判定为同一人,这个值在 LFW 数据集上的准确率约 99.38%,但实际考勤场景受光照影响,可以放宽到 0.5 到 0.55 之间减少误识。
3.3 人脸库比对与阈值判定
注册时把每个员工的特征向量存进数据库,打卡时逐个比对,取距离最小的那个。
def find_match(descriptor, threshold=0.55): # 从数据库取出所有人脸特征 employees = Employee.query.filter(Employee.face_encoding.isnot(None)).all() min_dist = float('inf') matched = None for emp in employees: # 数据库存的是 JSON 字符串,转回 numpy 数组 known = np.array(json.loads(emp.face_encoding)) # 计算欧氏距离 dist = np.linalg.norm(descriptor - known) if dist < min_dist: min_dist = dist matched = emp # 距离小于阈值才算命中 if min_dist < threshold: return matched, min_dist return None, min_dist逻辑说明:np.linalg.norm算的是两个 128 维向量的欧氏距离,距离越小越像。阈值 0.55 比官方推荐的 0.6 更严,是为了降低误识率——考勤场景里把别人认成你比认不出你更麻烦。如果员工数量上百,逐个比对会慢,常见优化是先把所有人脸特征加载进内存缓存,或者用 faiss 做向量检索,但毕设规模一般几十人,直接循环够用。
3.4 考勤记录写入与去重
命中后写考勤记录,这里有个容易忽略的点:同一个人短时间内多次打卡要防重。
from datetime import datetime, timedelta def mark_attendance(employee_id): now = datetime.now() # 查最近 5 分钟内的记录,防止重复打卡 recent = Attendance.query.filter( Attendance.employee_id == employee_id, Attendance.check_time > now - timedelta(minutes=5) ).first() if recent: return {'status': 'duplicate', 'msg': '5 分钟内已打卡'} record = Attendance( employee_id=employee_id, check_time=now, check_type='上班' if now.hour < 12 else '下班' ) db.session.add(record) db.session.commit() return {'status': 'ok', 'time': now.strftime('%H:%M:%S')}逻辑说明:timedelta(minutes=5)是防重窗口,设太短起不到作用,设太长会漏掉正常的上下班两次打卡。5 分钟是常见折中值。check_type按小时判断上下午,如果企业有更复杂的班次规则,这里要换成读排班表。参数上,防重窗口建议做成配置项,不同企业考勤规则不一样。
4. Flask 后端结构与数据库设计:别把逻辑全塞进路由
4.1 项目目录分层
拿到源码先看目录结构,这个项目典型的分层是:
project/ ├── app.py # 入口,注册蓝图 ├── models.py # SQLAlchemy 模型 ├── views/ │ ├── auth.py # 登录注册 │ ├── attendance.py # 考勤打卡 │ └── admin.py # 后台管理 ├── utils/ │ └── face_utils.py # dlib 封装 ├── static/ # css/js/模型文件 ├── templates/ # jinja2 模板 └── models/ # dlib 模型文件分层的好处是路由只负责收请求和返响应,人脸处理逻辑全在utils/face_utils.py,数据库操作在models.py。改识别阈值只动一个文件,不用满项目找。
4.2 核心数据表设计
考勤系统最少三张表:员工表、人脸特征表、考勤记录表。人脸特征单独一张表而不是塞进员工表,是为了一个员工可以存多张人脸(正脸、侧脸),提升识别率。
| 表名 | 关键字段 | 说明 |
|---|---|---|
| employee | id, name, dept, position | 员工基本信息 |
| face_encoding | id, employee_id, encoding, create_time | 128 维特征,JSON 存储 |
| attendance | id, employee_id, check_time, check_type | 打卡记录 |
encoding字段用 Text 类型存 JSON 字符串,别用 Blob,调试时不好看。check_time加索引,按时间范围查记录时快很多。
4.3 蓝图注册与路由划分
from flask import Flask from views.auth import auth_bp from views.attendance import attendance_bp from views.admin import admin_bp app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///attendance.db' app.config['SECRET_KEY'] = 'your-secret-key' # 注册蓝图,url_prefix 区分模块 app.register_blueprint(auth_bp, url_prefix='/auth') app.register_blueprint(attendance_bp, url_prefix='/api/attendance') app.register_blueprint(admin_bp, url_prefix='/admin') if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=True)逻辑说明:url_prefix把不同模块的接口隔开,前端调/api/attendance/check就知道是考勤相关。host='0.0.0.0'让局域网内其他设备也能访问,方便用手机测试摄像头。debug=True开发时用,上线要关掉,否则有安全风险。数据库用 SQLite 够毕设用,要上生产换 MySQL 只需改连接字符串。
5. 避坑与排查:那些文档里不会写的翻车现场
5.1 摄像头打不开或黑屏
现象:前端页面摄像头区域一片黑,控制台报NotAllowedError或NotFoundError。 原因:浏览器权限没给,或者页面不是 HTTPS/localhost 环境。getUserMedia在非安全上下文里直接被禁。 解决:本地测试用localhost或127.0.0.1访问,别用局域网 IP。如果必须用 IP,得配 HTTPS 证书。另外检查浏览器地址栏左侧的摄像头权限图标,手动允许。
5.2 识别总是返回 None
现象:摄像头正常,但后端一直返回"未检测到人脸"。 原因:三种可能——图像 base64 前缀没去掉导致解码失败、灰度转换漏了、上采样次数设 0 导致小脸检不出。 解决:先在extract_face_descriptor里打印img.shape,如果是 None 说明解码失败,检查split(',')[1]有没有拿到数据。再打印len(faces),如果是 0 就把detector(gray, 1)的上采样改成 2 试试。光照太暗也会导致检测失败,加个直方图均衡化cv2.equalizeHist(gray)能改善。
5.3 同一个人识别成不同人
现象:同一个人换个角度或光线,识别结果就变了。 原因:注册时只存了一张正脸照,特征覆盖不够;或者阈值设太高,把不同人认成同一人。 解决:注册时引导用户多角度采集 3 到 5 张,每张都提取特征存进face_encoding表。比对时算所有特征的最小距离,而不是只比一张。阈值从 0.6 降到 0.5 能减少误识,但会提升拒识率,需要按场景权衡。
5.4 数据库写入报编码错误
现象:打卡时db.session.commit()抛UnicodeEncodeError。 原因:SQLite 默认编码和 Python 字符串编码不一致,或者员工姓名里有特殊字符。 解决:建库时指定charset='utf8',连接字符串写成sqlite:///attendance.db?charset=utf8。员工姓名入库前做一次name.encode('utf-8').decode('utf-8')清洗。如果用的是 MySQL,确保库和表的字符集都是utf8mb4。
5.5 部署到服务器后模型文件找不到
现象:本地跑得好好的,传到服务器就报Unable to open shape_predictor。 原因:模型文件太大没传上去,或者路径用了 Windows 的反斜杠。 解决:模型文件用scp或 FTP 单独传,别混在代码压缩包里容易漏。路径统一用os.path.join拼接,别手写models\xxx.dat。上线前在服务器上ls -lh models/确认两个 .dat 文件都在,大小对得上。
6. 从能跑到好用:识别率调优与答辩演示技巧
把系统跑起来只是及格线,答辩要拿高分得在识别率和演示效果上下功夫。我帮学弟调优时总结了几个实操点,都是现场能验证的。
第一是光照预处理。dlib 对光照敏感,背光或侧光下检测率断崖式下跌。在extract_face_descriptor里加一步 CLAHE(限制对比度自适应直方图均衡化),比全局equalizeHist更稳,不会把噪点也放大。
# CLAHE 预处理,clipLimit 控制对比度增强幅度 clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) gray = clahe.apply(gray)clipLimit=2.0是经验值,设太高会过增强产生伪影,设太低没效果。tileGridSize设 8x8 表示把图分成 8x8 块分别均衡化,适合人脸这种局部光照不均的场景。
第二是多特征注册。注册接口改成接收多张图,每张都提特征存库。比对时算待识别特征与所有注册特征的最小距离。实测同一个人存 3 张(正脸、左转 30 度、右转 30 度),识别率能从 70% 提到 90% 以上。
第三是演示时的取巧。答辩现场光线不可控,提前准备一段录好的视频文件代替实时摄像头,用cv2.VideoCapture('demo.mp4')逐帧读,识别效果稳定得多。代码里加个开关,演示模式读视频,正常模式读摄像头。
| 调优项 | 默认值 | 调优后 | 效果 |
|---|---|---|---|
| 上采样次数 | 0 | 1 | 小脸检出率提升 |
| 距离阈值 | 0.6 | 0.5 | 误识率下降 |
| 注册照片数 | 1 | 3 | 识别率提升约 20% |
| 光照预处理 | 无 | CLAHE | 逆光场景可用 |
第四是日志留痕。每次识别把距离值、命中员工、耗时写进日志文件,答辩时导师问"识别率多少"你能直接翻日志给数据,比空口说"挺准的"有说服力。
import logging logging.basicConfig(filename='recognition.log', level=logging.INFO) # 在比对后记录 logging.info(f'emp={matched.name if matched else "none"}, ' f'dist={min_dist:.4f}, cost={elapsed:.3f}s')从那以后我每次交付带识别的项目,都强制走一遍"多光照 + 多角度 + 日志留痕"这三步,不然现场翻车概率太高。这套源码的骨架已经搭好了,调优就是在这上面加料。希望帮到你。
本文还有配套的精品资源,点击获取