ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python人脸识别考勤系统实战:OpenCV+face_recognition+Flask全栈开发

Python人脸识别考勤系统实战:OpenCV+face_recognition+Flask全栈开发 简介这是一套面向高校计算机相关专业学生的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依赖 dlibdlib 的编译对 Python 版本和 CMake 很敏感venv隔离性更好。下面是完整的环境初始化命令逐条执行即可。# 创建虚拟环境Python 版本建议 3.8 到 3.103.11 以上 dlib 编译容易报错 python -m venv venv # 激活虚拟环境Windows 用 venv\Scripts\activate source venv/bin/activate # 先装 CMake 和 dlib 的编译依赖Ubuntu 下需要 build-essential pip install cmake3.26.4 pip install dlib19.24.2 # 再装 face_recognition它会自动拉取 numpy 和 Pillow pip install face_recognition1.3.0 # Web 框架和跨域支持 pip install Flask2.3.3 flask-cors4.0.0 # 数据库操作SQLite 不需要额外驱动但用 SQLAlchemy 更方便 pip install Flask-SQLAlchemy3.0.5逻辑说明先装cmake是因为 dlib 的setup.py需要调用 CMake 生成构建文件锁定dlib19.24.2是因为更高版本在部分 Windows 环境下缺少预编译 wheel会触发源码编译耗时且容易失败。face_recognition1.3.0是当前稳定版它的face_encodings返回 128 维 numpy 数组后续比对直接用np.linalg.norm算欧氏距离。参数方面face_locations的model参数默认是hogCPU 友好如果换了 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_keyTrue) student_id db.Column(db.String(20), uniqueTrue, nullableFalse) name db.Column(db.String(50), nullableFalse) face_encoding db.Column(db.LargeBinary, nullableFalse) # pickle 后的 128 维向量 class Attendance(db.Model): id db.Column(db.Integer, primary_keyTrue) student_id db.Column(db.String(20), nullableFalse) timestamp db.Column(db.DateTime, defaultdatetime.utcnow) status db.Column(db.String(10), defaultpresent) 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_idstudent_id).first(): return jsonify({code: 409, msg: 该学号已注册}), 409 student Student(student_idstudent_id, namename, face_encodingencoding_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, modelhog) 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_idstudent.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 idregisterForm input typetext idstudentId placeholder学号 required input typetext idname placeholder姓名 required input typefile idphoto acceptimage/* required button typesubmit注册/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目录托管前端页面。参数方面acceptimage/*限制文件选择器只显示图片但后端仍要校验文件类型防止上传脚本文件。考勤看板的摄像头采集用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 或 RGBAface_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, threshold0.45): votes [] for img in frames: locations face_recognition.face_locations(img, modelhog) 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 个已知身份的照片跑一轮混淆矩阵看误识率和漏识率别凭感觉调参。希望帮到你。本文还有配套的精品资源点击获取
返回列表