简介:这是一套面向高校计算机相关专业学生与课程设计学习者的企业考勤管理系统完整项目源码,采用Python结合Flask框架与dlib人脸识别库实现,可作为毕业设计或课程设计参考。项目已通过导师指导与答辩评审,在Windows 10/11环境下完成调试,下载后可直接运行,并配有部署教程与使用文档。压缩包共416个文件,约103.71MB,其中32个py文件承载后端业务逻辑与人脸识别核心,26个html与118个js、61个css构成前端页面与交互,另有79张jpg、51张png等图片资源用于界面展示与识别素材,dat、xml等文件支撑模型与配置。目前已有170人学习关注。读者可获得一套结构完整、模块清晰的人脸识别考勤系统实现方案,涵盖人脸采集、比对识别、考勤记录管理等环节,便于理解Flask项目组织方式与dlib集成思路,适合作为二次开发或答辩演示的基础工程。
1. 从一张打卡照片说起:这套 Flask + dlib 考勤系统到底解决什么问题
很多公司还在用指纹机打卡,冬天手指干裂识别不了,夏天出汗也识别不了,前台每个月月底对着 Excel 手动核对考勤,一整天就搭进去了。这套基于 Python + Flask + dlib 的人脸识别企业考勤管理系统,解决的就是这个场景:员工走到摄像头前,系统自动识别身份、记录打卡时间、写入数据库,后台能按人按天导出考勤报表。它适合谁?适合正在做毕业设计、需要一套能跑通、能演示、能写论文的完整项目的同学,也适合小团队想搭一个内部考勤原型、验证人脸识别方案可行性的工程师。
技术栈上,Flask 负责 Web 服务和接口,dlib 负责人脸检测和 128 维特征提取,前端用 HTML + JavaScript 调摄像头,数据存 SQLite 或 MySQL。整套东西不依赖云服务,本地就能跑,这对毕业设计答辩来说很关键——断网也能演示。下面我从环境搭建一路讲到识别调参和避坑,把这条链路拆开。
2. 环境搭建:dlib 装不上是第一个拦路虎
2.1 Python 版本与依赖选择
dlib 对 Python 版本和编译环境很挑。我一般用 Python 3.8,原因是这个版本和 dlib 19.22 的兼容性最稳,网上踩坑记录也最多,遇到问题好搜。Python 3.11 以上装 dlib 经常卡在 CMake 编译阶段,新手很容易在这里放弃。
核心依赖清单如下:
| 依赖包 | 版本建议 | 作用 |
|---|---|---|
| Flask | 2.2.x | Web 框架,提供路由和接口 |
| dlib | 19.22.x | 人脸检测 + 特征提取 |
| opencv-python | 4.5.x | 摄像头读取和图像预处理 |
| numpy | 1.23.x | 数组运算,dlib 的底层依赖 |
| face-recognition | 1.3.0 | 对 dlib 的封装,简化调用 |
| Flask-SQLAlchemy | 3.0.x | ORM,操作考勤数据库 |
安装顺序有讲究,先装 numpy,再装 opencv,最后装 dlib。因为 dlib 编译时会链接 numpy 的头文件,顺序反了会报numpy/arrayobject.h not found。
# 先升级 pip 和 setuptools,避免旧版解析依赖出错 python -m pip install --upgrade pip setuptools wheel # 按顺序安装,numpy 必须最先 pip install numpy==1.23.5 pip install opencv-python==4.5.5.64 pip install cmake==3.25.0 pip install dlib==19.22.1 pip install face-recognition==1.3.0 pip install Flask==2.2.5 pip install Flask-SQLAlchemy==3.0.5这里cmake单独装是因为 dlib 编译需要它,很多人漏掉这一步直接报CMake is not installed。dlib==19.22.1这个版本在 Windows 和 Linux 上都有预编译轮子,能省掉大量编译时间。如果你在 Windows 上还是编译失败,去搜dlib wheel找对应 Python 版本的.whl文件手动安装,这是最省事的后悔药。
2.2 验证 dlib 是否真正可用
装完不代表能用,必须跑一段最小验证代码。很多人装完 import 成功就以为没事,结果一调用检测函数就崩。
import dlib import cv2 import numpy as np # 加载 dlib 的正脸检测器 detector = dlib.get_frontal_face_detector() # 加载 68 点关键点模型(需要单独下载 shape_predictor_68_face_landmarks.dat) predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat") # 加载人脸识别模型,输出 128 维特征向量 face_rec_model = dlib.face_recognition_model_v1("dlib_face_recognition_resnet_model_v1.dat") # 用一张测试图验证整条链路 img = cv2.imread("test_face.jpg") rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) faces = detector(rgb, 1) # 第二个参数 1 表示上采样一次,提高小脸检出率 for face in faces: shape = predictor(rgb, face) descriptor = face_rec_model.compute_face_descriptor(rgb, shape) vec = np.array(descriptor) print("检测到人脸,特征维度:", vec.shape) # 应该是 (128,)这段代码验证了三件事:检测器能加载、关键点模型能加载、特征提取能跑通。detector(rgb, 1)里的1是上采样次数,图片里人脸小的时候调成 2,但速度会明显变慢。compute_face_descriptor返回的 128 维向量就是后续比对的核心数据,两张脸的向量欧氏距离小于 0.6 一般认为是同一人,这个阈值后面还会细说。
两个.dat模型文件需要提前下载好放在项目根目录,shape_predictor_68_face_landmarks.dat大约 100MB,dlib_face_recognition_resnet_model_v1.dat大约 22MB。文件路径写错会直接报Unable to open,这是新手最常见的翻车点。
3. Flask 后端:把识别能力包成考勤接口
3.1 项目目录结构与数据库设计
目录结构决定了后面好不好维护。我见过太多毕业设计把所有代码堆在一个app.py里,改一处崩三处。推荐这样分:
attendance_system/ ├── app.py # Flask 入口 ├── models.py # 数据库模型 ├── face_utils.py # 人脸识别封装 ├── static/ │ └── known_faces/ # 已注册员工人脸特征 ├── templates/ │ ├── index.html # 打卡页面 │ └── admin.html # 管理后台 └── attendance.db # SQLite 数据库数据库两张表就够:员工表存基本信息和 128 维特征,考勤表存打卡记录。
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(50), nullable=False) department = db.Column(db.String(50)) # 128 维特征存成 JSON 字符串,简单直接 face_encoding = db.Column(db.Text, nullable=False) created_at = db.Column(db.DateTime, default=datetime.now) 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) # 打卡类型:上班 / 下班 check_type = db.Column(db.String(10), default='in')face_encoding用Text存 JSON 而不是BLOB,好处是调试时能直接看内容,坏处是占空间略大。对几百人的公司来说完全够用。check_type区分上下班,后面统计工时才不会乱。
3.2 人脸注册与打卡两个核心接口
注册接口负责把员工照片转成特征存库,打卡接口负责实时比对。
from flask import Flask, request, jsonify import face_recognition import numpy as np import json from models import db, Employee, Attendance app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///attendance.db' db.init_app(app) @app.route('/register', methods=['POST']) def register(): name = request.form['name'] department = request.form['department'] file = request.files['photo'] # 从上传的图片中提取人脸特征 image = face_recognition.load_image_file(file) encodings = face_recognition.face_encodings(image) if len(encodings) == 0: return jsonify({'code': 400, 'msg': '未检测到人脸,请重新上传'}) if len(encodings) > 1: return jsonify({'code': 400, 'msg': '图片中有多张人脸,请上传单人照'}) emp = Employee( name=name, department=department, face_encoding=json.dumps(encodings[0].tolist()) ) db.session.add(emp) db.session.commit() return jsonify({'code': 200, 'msg': '注册成功', 'id': emp.id})注册时强制单人照是血泪经验。多人合照会导致特征库混乱,后面识别谁都不准。face_encodings返回列表,长度就是检测到的人脸数,这个判断必须做。
打卡接口是核心,逻辑是:接收前端传来的图片 → 提取特征 → 和库里所有人比对 → 找距离最小的 → 判断是否小于阈值。
@app.route('/checkin', methods=['POST']) def checkin(): file = request.files['photo'] check_type = request.form.get('check_type', 'in') image = face_recognition.load_image_file(file) encodings = face_recognition.face_encodings(image) if len(encodings) == 0: return jsonify({'code': 400, 'msg': '未检测到人脸'}) unknown = encodings[0] employees = Employee.query.all() best_match = None min_distance = 1.0 # 初始设一个大于阈值的大数 for emp in employees: known = np.array(json.loads(emp.face_encoding)) # 计算欧氏距离,越小越像 distance = np.linalg.norm(known - unknown) if distance < min_distance: min_distance = distance best_match = emp # 0.45 是实测比较稳的阈值,比默认 0.6 更严格 if best_match and min_distance < 0.45: record = Attendance( employee_id=best_match.id, check_type=check_type ) db.session.add(record) db.session.commit() return jsonify({ 'code': 200, 'name': best_match.name, 'distance': round(min_distance, 4) }) return jsonify({'code': 404, 'msg': '未匹配到员工,请先注册'})阈值0.45是我调出来的。官方默认 0.6 太松,容易把相似的人认错,尤其是戴眼镜和不戴眼镜的同一人、或者长相接近的同事。收紧到 0.45 后误识率明显下降,代价是偶尔本人站的角度不好会被拒,需要重拍。这个取舍在考勤场景里,宁可让人多刷一次,也不能把张三认成李四。
4. 前端摄像头采集:浏览器里怎么拿到能识别的画面
4.1 用 getUserMedia 抓帧并上传
前端不需要复杂框架,原生 JavaScript 调摄像头就行。关键是抓帧的时机和图片质量。
// 打开摄像头 const video = document.getElementById('video'); navigator.mediaDevices.getUserMedia({ video: { width: 640, height: 480 } }) .then(stream => { video.srcObject = stream; }) .catch(err => { alert('摄像头打开失败:' + err.message); }); // 抓取当前帧并上传 function captureAndCheckin() { const canvas = document.createElement('canvas'); canvas.width = 640; canvas.height = 480; const ctx = canvas.getContext('2d'); ctx.drawImage(video, 0, 0, 640, 480); // 转成 blob 上传,质量 0.9 兼顾清晰度和体积 canvas.toBlob(blob => { const formData = new FormData(); formData.append('photo', blob, 'capture.jpg'); formData.append('check_type', 'in'); fetch('/checkin', { method: 'POST', body: formData }) .then(res => res.json()) .then(data => { if (data.code === 200) { document.getElementById('result').innerText = '打卡成功:' + data.name; } else { document.getElementById('result').innerText = data.msg; } }); }, 'image/jpeg', 0.9); }分辨率设 640×480 是权衡结果。太高上传慢、后端处理也慢,太低人脸像素不够,dlib 检测不到。toBlob的第三个参数0.9是 JPEG 压缩质量,低于 0.7 会出现明显块状伪影,影响特征提取。抓帧前最好让用户正对镜头、光线均匀,背光或侧脸是识别失败的两大主因。
4.2 前端体验上的几个细节
打卡页面要给出明确的视觉反馈。摄像头预览框加一个人脸框提示,让用户知道脸有没有在画面里。识别中显示 loading,避免用户狂点按钮重复提交。识别成功后播放一声提示音,这个在嘈杂的办公环境里很有用。
另外,getUserMedia在非 HTTPS 环境下,除了 localhost 之外都会被浏览器拦截。本地开发用127.0.0.1没问题,部署到服务器必须配 HTTPS,否则摄像头根本打不开。这是很多人部署后「功能失效」的真正原因,不是代码问题。
5. 避坑与排查:识别不准、装不上、部署崩的实战记录
5.1 现象:同一个人有时能识别有时不能
原因:dlib 对光照和角度敏感,侧脸超过 30 度、或者顶光造成眼窝阴影,特征向量会偏移,距离超过阈值。解决:注册时采集 3 张不同角度的照片,取特征平均值存库,比单张鲁棒得多。代码上把face_encodings返回的多个向量求均值即可。
5.2 现象:pip install dlib 卡在 Building wheel 十几分钟然后失败
原因:没有预编译轮子,pip 尝试从源码编译,缺 CMake 或 Visual Studio 编译工具链。解决:先pip install cmake,Windows 上装 Visual Studio Build Tools 并勾选 C++ 桌面开发。实在不行直接找对应版本的.whl文件,pip install xxx.whl秒装。
5.3 现象:Flask 部署到服务器后,上传大图报 413
原因:Flask 默认请求体上限是 16MB,手机拍的原图经常超过。解决:app.config['MAX_CONTENT_LENGTH'] = 32 * 1024 * 1024,同时前端压缩后再传,双保险。
5.4 现象:识别速度慢,一个人要等三四秒
原因:每次打卡都遍历全库计算距离,人多了就慢;另外图片没缩放,dlib 在全尺寸图上检测很耗时。解决:先把图片缩放到宽度 320 再检测,速度提升明显;特征比对用 numpy 向量化,别用 for 循环逐个算。
5.5 现象:数据库里特征存进去,读出来比对全都不匹配
原因:json.dumps存的是列表,读出来json.loads后是 Python list,直接和 numpy 数组做减法会广播出错或结果不对。解决:读出来后np.array(json.loads(...))转成 numpy 数组再算,这个类型转换漏掉会导致玄学般的「明明是同一个人却匹配不上」。
6. 让识别更稳的进阶技巧:多帧投票与阈值自适应
单帧识别在真实考勤场景里不够稳,员工走过来的过程中总会有几张糊的。我一般会做多帧投票:连续抓 5 帧,每帧都算一次匹配结果,取出现次数最多的那个人作为最终结果。这样偶尔一帧识别错不会影响整体判断,代价是打卡慢了一点点,但准确率提升很明显。
from collections import Counter def multi_frame_checkin(frames, employees, threshold=0.45): """frames 是连续抓取的图片列表""" results = [] for img in frames: encodings = face_recognition.face_encodings(img) if not encodings: continue unknown = encodings[0] best_id, min_dist = None, 1.0 for emp in employees: known = np.array(json.loads(emp.face_encoding)) dist = np.linalg.norm(known - unknown) if dist < min_dist: min_dist, best_id = dist, emp.id # 只有低于阈值才计入投票 if best_id and min_dist < threshold: results.append(best_id) if not results: return None # 取票数最多的员工 return Counter(results).most_common(1)[0][0]frames建议取 5 帧,间隔 200 毫秒,覆盖员工站定的过程。Counter统计出现次数,most_common(1)取第一名。如果 5 帧里没有一帧低于阈值,说明这个人没注册或者光线太差,返回 None 让前端提示重试。
阈值也可以做成自适应的:统计每个员工历史打卡的成功距离,取平均值的 1.2 倍作为个人阈值。经常戴眼镜的员工,特征距离普遍偏大,用统一阈值对他不公平。这个优化在几十人规模时效果有限,上百人时值得做。
最后说个习惯:每次改完识别相关代码,我都会拿同一组测试图跑一遍回归,记录每个人的匹配距离,画个简单分布。距离突然整体变大,说明模型加载或图片预处理出了问题,能第一时间发现。这套系统不难,难的是把每个环节的边界摸清楚,希望帮到你。
本文还有配套的精品资源,点击获取