☰
Python人脸识别考勤系统实战:OpenCV+face_recognition+Flask全栈开发
2026/9/28 16:40:20 网站建设 项目流程

简介:这是一套面向高校计算机相关专业学生的Python毕业设计/课程设计完整项目,主题为基于人脸识别的上课考勤系统,适合需要完成毕设或课设、希望拿到可部署可运行源码的同学。项目包含前端界面与后端录入界面,覆盖学生信息输入、人脸数据录入与训练、多条件搜索修改、多选删除、人脸识别追踪签到等完整流程,并支持生成签到表格与导出Excel签到表。系统按功能划分为录入端、管理端、监控端三部分,在原始项目框架上做了大量补充与优化,修复了部分模块实现不完整的问题。压缩包共42个文件,以xml分类器文件、png运行截图、ui界面文件、py脚本及whl依赖包为主,整体约46.97MB,另附详细部署教程与项目运行图。目前已有1656人学习下载,可作为毕业设计、课程设计的参考方案,帮助读者快速理解人脸识别考勤系统的模块划分与实现思路。

1. 从零搭一套能跑通的课堂人脸考勤:为什么我不建议你直接抄开源

每年毕业季,GitHub 上都会冒出一批“Python 人脸识别考勤系统”,但真正能跑通、能演示、能写进论文的不到三成。问题不在算法本身,而在工程链路:摄像头采集、人脸检测、特征比对、后端入库、前端展示,任何一环断了,答辩现场就是大型翻车现场。我见过太多同学本地跑通了face_recognition的 demo,一接 Flask 就报跨域,一换摄像头就花屏,一上多线程就死锁。这套系统的核心难点从来不是“识别人脸”,而是“把识别结果稳定地变成一条考勤记录,并且让老师能在浏览器里看到”。适合谁?适合有 Python 基础、做过至少一个 Web 小项目、想在 2 到 3 周内交付一个可演示系统的本科生。如果你连pip install都没用过,建议先补 Python 安装教程和环境配置,再回来读这篇。下面我按真实落地顺序拆:先讲技术选型为什么这么定,再给可复制的代码和参数,最后把踩过的坑一条条列出来。

2. 技术选型:为什么是 OpenCV + face_recognition + Flask,而不是深度学习全家桶

2.1 人脸检测与识别方案的取舍逻辑

毕业设计的时间预算通常只有 3 到 4 周,其中还要写论文、做 PPT、准备答辩。如果你选 MTCNN + FaceNet + 自训练分类器,光调参和准备数据集就能吃掉两周,而且没有 GPU 的笔记本跑一次推理要等好几秒,演示时摄像头画面卡成 PPT,老师第一印象就差了。我一般推荐face_recognition库,它底层封装了 dlib 的 HOG 检测器和 ResNet 特征提取,CPU 上单张人脸识别耗时约 0.3 到 0.8 秒,精度在课堂场景(正面、光照均匀、无遮挡)足够用。更重要的是,它的 API 极其简单:face_locations做人脸框定位,face_encodings把一张脸转成 128 维向量,compare_faces做欧氏距离比对。你不需要理解 ResNet 的残差结构,也能把系统跑起来。常见做法是:注册阶段每人采集 5 到 10 张不同角度的照片,取编码均值作为该学生的特征模板;考勤阶段每 2 秒抓一帧,检测到人脸后与模板库比对,距离小于 0.45 判定为同一人。这个阈值不是拍脑袋定的,dlib 官方建议 0.6 是宽松阈值,0.45 到 0.5 之间能有效降低误识率,课堂场景我一般设 0.45。

2.2 前后端分离的最小可行架构

标题里明确要求“包括前端,后端录入界面”,这意味着你不能只写一个命令行脚本。最小可行架构是:后端用 Flask 提供 RESTful 接口,负责接收前端上传的注册照片、执行人脸编码、写入 SQLite 数据库、返回考勤记录;前端用 HTML + JavaScript 做两个页面——录入页(上传照片、填写学号姓名)和考勤看板(实时显示今日签到列表)。前后端分离项目实战里最常见的坑是跨域,Flask 端加flask-cors一行解决。数据库选 SQLite 而不是 MySQL,是因为毕业设计演示环境通常就是一台笔记本,SQLite 零配置、单文件、方便打包,老师要看数据直接拷走.db文件就行。如果你确实想用 MySQL,把连接字符串换掉即可,但记得在论文里写清楚部署步骤。前端不需要上 Vue 或 React,原生fetchAPI 足够,减少构建工具链的复杂度,避免答辩时 npm 报错这种低级问题。

2.3 环境搭建与依赖锁定

先把环境搭干净。我习惯用venv而不是conda,因为face_recognition依赖 dlib,dlib 的编译对 Python 版本和 CMake 很敏感,venv隔离性更好。下面是完整的环境初始化命令,逐条执行即可。

# 创建虚拟环境,Python 版本建议 3.8 到 3.10,3.11 以上 dlib 编译容易报错 python -m venv venv # 激活虚拟环境,Windows 用 venv\Scripts\activate source venv/bin/activate # 先装 CMake 和 dlib 的编译依赖,Ubuntu 下需要 build-essential pip install cmake==3.26.4 pip install dlib==19.24.2 # 再装 face_recognition,它会自动拉取 numpy 和 Pillow pip install face_recognition==1.3.0 # Web 框架和跨域支持 pip install Flask==2.3.3 flask-cors==4.0.0 # 数据库操作,SQLite 不需要额外驱动,但用 SQLAlchemy 更方便 pip install Flask-SQLAlchemy==3.0.5

逻辑说明:先装cmake是因为 dlib 的setup.py需要调用 CMake 生成构建文件;锁定dlib==19.24.2是因为更高版本在部分 Windows 环境下缺少预编译 wheel,会触发源码编译,耗时且容易失败。face_recognition==1.3.0是当前稳定版,它的face_encodings返回 128 维 numpy 数组,后续比对直接用np.linalg.norm算欧氏距离。参数方面,face_locations的model参数默认是"hog",CPU 友好;如果换了 GPU 机器可以改"cnn",但毕业设计没必要。number_of_times_to_upsample控制上采样次数,默认 1,如果人脸在画面里很小,调到 2 能提高检出率,但耗时翻倍。

3. 后端核心:人脸注册、特征比对与考勤入库的完整实现

3.1 数据库模型与注册接口

先定义两张表:Student存学号、姓名、人脸编码(用 BLOB 存 numpy 数组的二进制);Attendance存学号、签到时间、状态。用 Flask-SQLAlchemy 声明模型,注意人脸编码要序列化,不能直接存 numpy 数组。

from flask import Flask, request, jsonify from flask_sqlalchemy import SQLAlchemy from flask_cors import CORS import face_recognition import numpy as np import pickle from datetime import datetime app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///attendance.db' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False CORS(app) # 允许前端跨域访问,生产环境应指定 origins db = SQLAlchemy(app) class Student(db.Model): id = db.Column(db.Integer, primary_key=True) student_id = db.Column(db.String(20), unique=True, nullable=False) name = db.Column(db.String(50), nullable=False) face_encoding = db.Column(db.LargeBinary, nullable=False) # pickle 后的 128 维向量 class Attendance(db.Model): id = db.Column(db.Integer, primary_key=True) student_id = db.Column(db.String(20), nullable=False) timestamp = db.Column(db.DateTime, default=datetime.utcnow) status = db.Column(db.String(10), default='present') @app.route('/api/register', methods=['POST']) def register(): student_id = request.form['student_id'] name = request.form['name'] file = request.files['photo'] # 将上传文件转为 numpy 数组,face_recognition 只认 RGB 格式 img = face_recognition.load_image_file(file) encodings = face_recognition.face_encodings(img) if len(encodings) == 0: return jsonify({'code': 400, 'msg': '未检测到人脸,请重新上传'}), 400 if len(encodings) > 1: return jsonify({'code': 400, 'msg': '检测到多张人脸,请确保照片中只有一人'}), 400 # 取第一张人脸的编码,pickle 序列化后存入数据库 encoding_blob = pickle.dumps(encodings[0]) if Student.query.filter_by(student_id=student_id).first(): return jsonify({'code': 409, 'msg': '该学号已注册'}), 409 student = Student(student_id=student_id, name=name, face_encoding=encoding_blob) db.session.add(student) db.session.commit() return jsonify({'code': 200, 'msg': '注册成功'})

逻辑说明:face_recognition.load_image_file接受文件对象,内部用 PIL 打开并转成 RGB numpy 数组。face_encodings返回一个列表,每个元素是 128 维向量。如果列表为空说明没检测到人脸,直接返回 400 让前端提示用户重拍;如果多于一张,说明照片里有其他人,也拒绝。pickle.dumps把 numpy 数组转成字节流,SQLite 的LargeBinary字段可以存。注意student_id加了唯一约束,重复注册返回 409。参数方面,face_encodings的num_jitters参数控制对每张脸做多少次采样,默认 1,调到 10 能提高编码稳定性但耗时增加 10 倍,注册阶段可以设 5,考勤阶段保持 1。

3.2 考勤比对接口与阈值调优

考勤接口接收前端传来的实时截图,检测人脸后与库中所有学生编码逐一比对,取距离最小且小于阈值的作为匹配结果。这里的关键是阈值和比对策略。

@app.route('/api/checkin', methods=['POST']) def checkin(): file = request.files['frame'] img = face_recognition.load_image_file(file) # 考勤阶段用 hog 模型,速度快;upsample 设为 1 平衡精度和耗时 locations = face_recognition.face_locations(img, model='hog') if not locations: return jsonify({'code': 404, 'msg': '画面中未检测到人脸'}), 404 encodings = face_recognition.face_encodings(img, locations) students = Student.query.all() if not students: return jsonify({'code': 400, 'msg': '学生库为空,请先注册'}), 400 known_encodings = [pickle.loads(s.face_encoding) for s in students] results = [] for encoding in encodings: # 计算与所有已知编码的欧氏距离 distances = face_recognition.face_distance(known_encodings, encoding) best_idx = int(np.argmin(distances)) best_distance = distances[best_idx] if best_distance < 0.45: # 阈值,课堂场景建议 0.45 student = students[best_idx] # 同一天同一学生只记一次,避免重复签到 today = datetime.utcnow().date() existing = Attendance.query.filter( Attendance.student_id == student.student_id, db.func.date(Attendance.timestamp) == today ).first() if not existing: record = Attendance(student_id=student.student_id) db.session.add(record) db.session.commit() results.append({ 'student_id': student.student_id, 'name': student.name, 'distance': round(float(best_distance), 4), 'status': '已签到' if not existing else '今日已签到' }) else: results.append({'status': '陌生人', 'distance': round(float(best_distance), 4)}) return jsonify({'code': 200, 'data': results})

逻辑说明:face_distance返回一个数组,每个元素是待识别编码与库中对应编码的欧氏距离。np.argmin取最小距离的索引。阈值 0.45 是经验值,低于它才认为是同一人。如果距离在 0.45 到 0.55 之间,系统会判为陌生人,但实际可能是同一个人因为光照或角度导致编码偏移,这种情况在演示时会被老师质疑“为什么本人站在摄像头前还识别不出来”。解决办法有两个:一是注册时多采集几张不同角度的照片,取编码均值;二是考勤时连续抓 3 帧,取距离最小的那次结果。参数方面,face_distance的model参数默认"large",对应 128 维编码,不要改。db.func.date是 SQLAlchemy 调用 SQLite 的date()函数,用于按天去重。

3.3 前端录入页与考勤看板的最小实现

前端不需要复杂框架,两个 HTML 文件加原生 JS 即可。录入页用FormData上传照片和学号姓名,考勤看板用setInterval每 3 秒抓一帧摄像头画面发给后端。

<!-- register.html 核心片段 --> <form id="registerForm"> <input type="text" id="studentId" placeholder="学号" required> <input type="text" id="name" placeholder="姓名" required> <input type="file" id="photo" accept="image/*" required> <button type="submit">注册</button> </form> <script> document.getElementById('registerForm').onsubmit = async (e) => { e.preventDefault(); const formData = new FormData(); formData.append('student_id', document.getElementById('studentId').value); formData.append('name', document.getElementById('name').value); formData.append('photo', document.getElementById('photo').files[0]); const res = await fetch('http://127.0.0.1:5000/api/register', { method: 'POST', body: formData }); const data = await res.json(); alert(data.msg); }; </script>

逻辑说明:FormData自动设置multipart/form-data编码,Flask 端用request.form和request.files分别取字段和文件。注意fetch的 URL 要写后端实际地址,如果前端用file://协议打开,浏览器会拦截跨域请求,必须用http-server或 Flask 的static目录托管前端页面。参数方面,accept="image/*"限制文件选择器只显示图片,但后端仍要校验文件类型,防止上传脚本文件。考勤看板的摄像头采集用navigator.mediaDevices.getUserMedia({ video: true }),把<video>元素作为画面源,再用canvas的toBlob抓帧。

4. 避坑与排查:课堂演示现场最容易翻车的 5 个点

4.1 摄像头画面卡顿或黑屏

现象:前端<video>标签显示黑屏,或者画面每隔几秒卡住。原因通常是getUserMedia没有在 HTTPS 或 localhost 下调用,浏览器安全策略要求摄像头权限只能在安全上下文获取。解决:本地开发用127.0.0.1或localhost访问前端页面,不要用file://直接打开 HTML。如果必须局域网访问,用 Flask 托管前端静态文件,或者用ngrok做临时 HTTPS 映射(注意不要用于生产)。另一个原因是摄像头被其他程序占用,关掉腾讯会议、钉钉等软件再试。

4.2 人脸检测报“未检测到人脸”但画面里明明有人

现象:注册时上传的照片肉眼可见人脸,但接口返回 400。原因有三:一是照片格式是 CMYK 或 RGBA,face_recognition只认 RGB,需要先转;二是人脸在画面中占比太小,HOG 检测器漏检;三是侧脸角度超过 30 度。解决:前端上传前用 canvas 压缩并转成 JPEG,后端加一步img = img[:, :, ::-1]把 BGR 转 RGB(如果用的是 OpenCV 读图)。对于小脸,把face_locations的number_of_times_to_upsample设为 2。侧脸问题只能靠多角度注册缓解。

4.3 识别距离阈值设错导致“认错人”

现象:A 同学站在摄像头前,系统显示 B 同学已签到。原因:阈值设得太大,比如 0.6,不同人之间的编码距离可能小于这个值。解决:把阈值降到 0.45,并在注册阶段确保每人照片清晰、正面、光照均匀。如果仍然误识,检查是否有两个学生的编码距离小于 0.5,这种情况说明注册照片质量太差,重新采集。血泪经验:答辩前一定要用全班同学的照片做一轮交叉验证,把误识率压到 0。

4.4 SQLite 并发写入报“database is locked”

现象:多个前端页面同时提交考勤,后端报sqlite3.OperationalError: database is locked。原因:SQLite 默认只允许一个写连接,Flask 开发服务器多线程模式下会冲突。解决:在app.config里加SQLALCHEMY_ENGINE_OPTIONS = {'connect_args': {'check_same_thread': False}},或者换 MySQL。更简单的办法是考勤接口加一个threading.Lock(),串行化写操作。毕业设计并发量低,加锁足够。

4.5 前端上传大图导致请求超时

现象:手机拍的照片 5MB 以上,上传后 Flask 报 413 或超时。原因:Flask 默认MAX_CONTENT_LENGTH是 16MB,但大图传输慢,且face_recognition处理高分辨率图耗时剧增。解决:前端用 canvas 把图片压缩到宽度 640px 再上传,后端设app.config['MAX_CONTENT_LENGTH'] = 5 * 1024 * 1024。压缩代码三行:创建 Image 对象、画到 canvas、canvas.toBlob回调里拿压缩后的 Blob。

5. 进阶技巧:用多帧投票把识别准确率再提一档

最后一章不讲虚的,给一个我实际用过的技巧:多帧投票。单帧识别受光照和角度影响大,偶尔会漏检或误判。做法是考勤接口连续抓 5 帧,每帧独立比对,取出现次数最多的学生 ID 作为最终结果。如果 5 帧里没有任一学生出现超过 2 次,判为“未识别”。这个逻辑用 Python 的collections.Counter十行代码搞定。

from collections import Counter def multi_frame_vote(frames, known_encodings, students, threshold=0.45): votes = [] for img in frames: locations = face_recognition.face_locations(img, model='hog') if not locations: continue encodings = face_recognition.face_encodings(img, locations) for encoding in encodings: distances = face_recognition.face_distance(known_encodings, encoding) best_idx = int(np.argmin(distances)) if distances[best_idx] < threshold: votes.append(students[best_idx].student_id) if not votes: return None counter = Counter(votes) top_id, top_count = counter.most_common(1)[0] # 至少 3 帧一致才认定,避免偶然误识 return top_id if top_count >= 3 else None

逻辑说明:frames是连续抓取的 numpy 数组列表,每个元素是一帧 RGB 图像。对每帧做人脸检测和编码,把匹配到的学号追加到votes。最后用Counter统计出现次数,取最多的那个,且要求至少 3 票。参数方面,帧数建议 5 到 7,太少投票没意义,太多前端等待时间超过 2 秒体验差。阈值保持 0.45 不变。这个技巧在答辩演示时特别有用:你站在摄像头前稍微偏头,单帧可能失败,但多帧投票能稳住结果。我一般还会在注册阶段对每个学生采集 8 张照片,取编码的中位数而不是均值,中位数对异常值更鲁棒。最后说一个习惯:每次改完阈值或换摄像头,先用 10 个已知身份的照片跑一轮混淆矩阵,看误识率和漏识率,别凭感觉调参。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询