ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于Python的在线课堂考勤系统:Flask+OpenCV+dlib人脸识别实战

基于Python的在线课堂考勤系统:Flask+OpenCV+dlib人脸识别实战 简介这是一套基于Python与卷积神经网络实现的在线课堂考勤系统完整工程包面向需要掌握人脸识别与深度学习落地流程的开发者与教育技术研究者可用于课堂签到、学生状态监测与注意力检测等教学管理场景。资源共98个文件包含22个Python脚本、15个XML配置、6个PNG图片以及多个pt模型权重、可执行exe、csv标注等文件压缩包约409MB覆盖数据预处理、模型训练、界面部署与数据库对接等完整环节。系统以CNN为核心识别人脸特征并实现了人脸识别与注意力检测两个功能模块。目前已有1461人学习下载。通过直接对照源码与自带数据集读者可完整理解从人脸数据收集、模型构建到在线考勤部署的实现流程也可在此基础上二次开发快速适配自有课堂或实验项目代码目录按功能模块划分清楚人脸识别与注意力检测两套流程均可独立运行参考。1. 基于python的在线课堂考勤系统先把三个核心诉求拆清楚基于python的在线课堂考勤系统名字听着像个课程设计作业真拆开做就会发现它是一条完整的技术链路摄像头前的人脸检测与特征比对、后台的签到规则与防重复机制、网页端给老师和学生看的实时反馈。它解决的是教室里最常见的三个麻烦——老师逐人点名占用课堂时间、学生私下换人代签、课后统计出勤全靠手工抄表。做毕设的人需要它来同时覆盖Python后端和算法调用培训机构或小型教学团队则想用它省掉一台昂贵的考勤机。这套系统的价值不在“拍一张脸”这个动作而在从图像到记录再到报表的闭环是否可靠。2. 技术栈如何选Flask、OpenCV 与 dlib 的搭配逻辑选型这一步决定了后面百分之六十的坑。常见做法是后端用 Flask识别层用 OpenCV 加 dlib数据先落 SQLite等并发上来了再换 MySQL。这是从业者最常用的组合不是因为它们各自最好而是因为合在一起踩坑最少。方案依赖复杂度识别精度部署成本适合场景Flask OpenCV LBPH低pip 装 opencv-python 即可中受角度和光照影响大低最小可复现的毕设演示Flask dlib/face_recognition高Windows 上装 dlib 很折腾高128 维特征比对中小班课堂、真实考勤Django 商用云 API低但要联网按次计费很高高有预算、不限网络环境我一般建议优先走第二行dlib 的安装坑虽然多但识别精度足够支撑真实课堂而且 face_recognition 封装了特征提取和距离比对业务代码量很小。如果机器死活装不上 dlib再退到第一行用 OpenCV 自带的 LBPH 兜底逻辑照样走得通只是换脸和侧脸场景会翻车。2.1 后端框架对比Flask 与 Django 的取舍签到系统对外只暴露几个接口注册人脸、提交签到、查询记录、导出报表。用 Flask 写这些路由非常直接一个 app.py 就能把最小闭环跑起来调试时不需要额外配置。Django 自带 admin 后台和 ORM管理学生数据和课程记录确实省事但它的中间件和模板体系对这个规模的项目属于前期负担。我的判断是如果后续要做完整的教务系统多教室、多角色权限、课程排期那就值得上 Django如果目标就是把一个班的考勤跑通并给出可复现的流程Flask 是更稳的起点。开发环境上建议先把 Python 装好并配好环境变量再用 VS Code 打开项目目录选择对应的解释器。这一步看起来基础实际操作时不少人卡在 pip 装包装到了别的 Python 版本上。检查方式很简单在 VS Code 终端里运行python --version确认和你预期的一致再开始pip install。2.2 人脸识别方案为什么是 OpenCV 检测加 dlib 特征纯 OpenCV 的 LBPH 方案需要先用一批人脸照片训练模型训练集的质量直接决定识别率而且它对人脸角度很敏感学生低头看一眼手机就识别失败。dlib 的方案则不同它直接提取人脸的 128 维特征向量不需要训练自己的模型只用把每个学生的注册照片编码后存下来。识别时同样提取现场帧的特征算一下欧氏距离小于阈值就认为匹配。常见做法是让 OpenCV 和 dlib 配合OpenCV 负责读取摄像头帧、缩放图像、转换色彩空间dlib 负责定位人脸和提取特征。face_recognition 库把后两步封装得更友好内部还是 dlib。至于热门的 YOLO 人脸检测对这个项目来说是杀鸡用牛刀模型文件和推理框架反而拖累部署。2.3 先跑通最小链路从摄像头帧到考勤落库不要一上来就写完整系统先把这条链路跑通摄像头取帧、缩放并转 RGB、定位人脸、提取特征、与库中特征比对、写入考勤表、返回结果。用 Flask 写一个最小的接口骨架确认链路没断再往里面填细节。from flask import Flask, request, jsonify import threading app Flask(__name__) lock threading.Lock() app.route(/api/attendance, methods[POST]) def attendance(): # 前端把摄像头帧压缩为 JPEG 后 POST 过来后端统一入口 data request.get_json() # framework 层只做三件事解析帧 - 识别 - 写入记录 return jsonify({status: ok, matched: True}) if __name__ __main__: app.run(host0.0.0.0, port8000)这段代码定义了后端的统一入口/api/attendance接收来自任意客户端的签到请求。threading.Lock()先放在这里是为了防止后面多线程同时写 SQLite 时出现锁冲突。这个骨架本身不识别任何人脸但它划清了边界Flask 只负责接收请求和返回 JSON识别逻辑放在独立的模块里不要和路由混在一起。参数上需要注意host设为0.0.0.0才能让局域网里其他设备访问调试时port可以先用8000后面部署再用 waitress 接管。3. 考勤数据模型设计三张表怎么承载签到规则很多失败的考勤项目不是识别不行而是数据模型没设计好。签到记录变成一堆流水账回答不了“这节课谁没来”“谁经常迟到”这两个最基本的问题。问题出在把考勤当成打卡记录存而不是按照课程维度的状态流转来存。数据应该设计成结构化数据先明确要回答哪些问题再建表。3.1 三张核心表学生、课程与签到记录我常用的最小设计是三张表students存学生身份信息和人脸特征courses存课程与上课时间段attendance_records存每次签到的结果。人脸特征以 BLOB 形式直接存在学生表里减少一次关联查询。-- 学生表身份信息 人脸特征向量 CREATE TABLE students ( student_no TEXT PRIMARY KEY, -- 学号 name TEXT NOT NULL, -- 姓名 face_feature BLOB NOT NULL, -- 128 维特征 pickle 后的字节 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 课程表一门课对应一个教学班 CREATE TABLE courses ( course_id INTEGER PRIMARY KEY AUTOINCREMENT, course_name TEXT NOT NULL, teacher TEXT NOT NULL, start_time TEXT NOT NULL, -- 上课时间形如 08:00 end_time TEXT NOT NULL, -- 下课时间形如 09:40 opened_date TEXT NOT NULL -- 上课日期避免跨周混淆 ); -- 签到记录表谁在哪节课签了到 CREATE TABLE attendance_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_no TEXT NOT NULL, course_id INTEGER NOT NULL, check_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, status TEXT CHECK (status IN (normal, late, absent)), confidence REAL, -- 识别时的距离值越小越可信 UNIQUE (student_no, course_id) -- 防重复签到的核心约束 );三张表的关联逻辑很直观签到记录通过student_no和course_id分别指向学生和课程。face_feature用 BLOB 存是因为 OpenCV 和 dlib 的特征向量本质是浮点数组pickle 后存数据库比存成文本更省空间。UNIQUE (student_no, course_id)是防重复签到的唯一屏障靠代码判断永远有并发窗口。需要注意opened_date不能省否则同一门课周一和周三的记录会互相覆盖。3.2 签到状态机正常、迟到与缺勤的判定顺序签到不是“拍脸成功就写一条记录”这么简单。课堂考勤需要考虑时间维度开课后 15 分钟内签到算正常15 到 45 分钟算迟到超过 45 分钟直接记为缺勤。缺勤不写签到表而是查不到记录后由报表逻辑推导出来这样不会产生冗余数据。from datetime import datetime, timedelta def build_check_status(course_start: str, check_time: datetime) - str: start datetime.strptime(course_start, %H:%M).replace( yearcheck_time.year, monthcheck_time.month, daycheck_time.day ) delta (check_time - start).total_seconds() / 60 if delta 15: return normal if delta 45: return late return absent def record_attendance(student_no: str, course_id: int): # UNIQUE 约束兜底重复签到会抛 IntegrityError with db_conn() as conn: try: conn.execute( INSERT INTO attendance_records (student_no, course_id, status) VALUES (?, ?, ?), (student_no, course_id, build_check_status(...)), ) conn.commit() return True except sqlite3.IntegrityError: return False # 已签到忽略本次请求build_check_status先算出签到时间与课程开始时间的差值再按 15 分钟和 45 分钟两个边界切分状态。record_attendance里的IntegrityError捕获是最后一道防线两个学生同时签到、两个线程同时写同一条记录时数据库唯一约束比任何业务判断都可靠。你会注意到缺席状态并没有被 insert 进表这是故意为之报表层用“该上课但签到表里没有记录”来推导缺席语义更清晰。4. 人脸识别模块怎么落face_recognition 的调用姿势与参数调优识别模块是整个系统里最像黑匣子的部分。face_recognition 封装了 dlib 的人脸检测和特征提取拿过来用很简单但它的三个关键参数直接决定课堂能不能用距离阈值、检测模型、帧采样间隔。下面按注册和识别两条路线给代码骨架参数说明放在每段代码后面。4.1 注册人脸把学生照片编码成 128 维特征注册阶段的质量决定了识别阶段的天花板。每个学生至少准备三张照片正面、左右各偏 15 度最好在教室真实光线下拍。常见做法是让系统在签到时自动抓拍并允许老师手动标记姓名考勤结束前统一存入特征库。import face_recognition import pickle def register_face(student_no: str, img_paths: list) - bytes: # img_paths 传 3 张照片路径列表返回合并后的特征 all_encodings [] for path in img_paths: image face_recognition.load_image_file(path) encodings face_recognition.face_encodings(image) if len(encodings) ! 1: # 一张照片里必须恰好只有一张人脸多脸或无人脸都拒绝注册 raise ValueError(f{path} 中检测到 {len(encodings)} 张人脸) all_encodings.append(encodings[0]) # 多张照片的特征取平均降低单张照片的角度偏差 avg [sum(col) / len(col) for col in zip(*all_encodings)] return pickle.dumps(avg)face_encodings返回的是列表中每个人脸的 128 维向量这里强制要求每张照片只检测到一张人脸避免注册时框错人。取平均的目的是把多张照片的视角差异折中成一个稳定特征这和训练机器学习模型时做数据增强的思路一致。实际落地时照片可以用手机拍完传到后台也可以用摄像头现场抓拍后者的光线和识别现场更接近推荐优先用现场抓拍。4.2 实时识别与签到降采样、提取特征与距离比对识别端的核心逻辑是每一帧都可能有多张人脸每张人脸都要和库里所有人算距离取最小值判断是否匹配。这个过程不能每帧都做否则 CPU 直接被打满。import cv2 import numpy as np import face_recognition def recognize_frame(frame, known_encodings, known_nos, threshold0.45): # frame 是 cv2 读到的 BGR 帧先缩小到原来一半再识别 small cv2.resize(frame, (0, 0), fx0.5, fy0.5) rgb cv2.cvtColor(small, cv2.COLOR_BGR2RGB) boxes face_recognition.face_locations(rgb, modelhog) encodings face_recognition.face_encodings(rgb, boxes) results [] for encoding in encodings: distances face_recognition.face_distance(known_encodings, encoding) best_idx int(np.argmin(distances)) if distances[best_idx] threshold: results.append((known_nos[best_idx], float(distances[best_idx]))) return resultsface_locations的modelhog是速度优先模式CPU 上跑得动modelcnn精度更高但需要 CUDA课堂场景不划算。face_distance返回欧氏距离数值越小代表越像np.argmin拿到最相似的那条注册记录。阈值的含义要记牢距离小于阈值才认为识别成功不是大于。0.45 是一个比较严格的起点小班课误识率低但偶尔漏检调成 0.6 会更宽容代价是长得像的同学可能互相认错。识别出来之后不要立即写库先在前端展示候选人和距离值让真人点击确认签到这个二次确认能挡掉大部分误识翻车。4.3 必调的三个参数距离阈值、检测模型与帧间隔参数推荐值调大效果调小效果face_distance阈值0.45漏检减少误识增加误识减少漏检增加face_locations模型hog速度快小脸漏检cnn 精度高但慢帧间隔每 3 帧抽 1 帧更流畅但可能漏掉低头瞬间更稳但 CPU 明显升高帧间隔的调法要结合教室人数20 人以内每秒处理 3 帧足够50 人以上的大课建议降采样比例从 0.5 调到 0.25让单帧检测耗时控制在 100 毫秒以内。识别线程和视频预览线程要分离预览走原始帧识别线程用队列接收抽帧后的副本避免阻塞摄像头画面。这套参数的常见失败模式是把阈值调到 0.7 追求“都能识别”结果误识率高得没法用课堂考勤宁可漏一次让学生补签也不能认错人记到别人头上。5. 踩坑自查识别率、依赖安装与并发写入的五个典型问题这一章是我做同类项目时真实见过的坑按“现象 → 原因 → 解决”写方便你对照排查。每条都值得在集成测试前先看一遍。5.1 光线一变就认不出人现象早上第一节课识别正常下午靠窗的同学大面积签到失败误差在 0.5 左右徘徊。原因face_recognition 提取的特征对光照敏感注册照片是室内暖光识别现场是下午窗边冷光同一个人的 128 维特征漂移了。这不是模型坏了是训练时的数据分布和现场不一致。解决注册照片和识别现场的光线尽量一致最可靠的办法是直接在教室里用同一台摄像头抓拍注册照。如果做不到在识别前用cv2.equalizeHist对灰度图做直方图均衡化能缓解一部分光照差异。千万不要指望阈值调大能解决那只会让误识率跟着涨。5.2 距离阈值 0.45 还是认错人现象A 同学签到成功记录却写到了 B 同学头上返回距离值 0.44刚好卡在阈值内。原因注册样本太单薄每个人只有一张正脸照五官接近的同学在特征空间里本来就很近。0.45 对单人照片阈值来说已经偏宽。解决每个学生至少注册 3 张照片并取平均特征这是成本最低的改进。同时把阈值收紧到 0.4并加上页面二次确认。如果双胞胎同班二次确认是唯一的兜底方案。5.3 SQLite 并发签到抛 database is locked现象五个学生同时刷脸日志里出现sqlite3.OperationalError: database is locked部分签到丢失。原因Flask 默认多线程处理请求每个请求独立连接数据库SQLite 对并发写支持有限多个写事务同时提交时直接报锁。解决写操作串行化。我采用的办法是全局锁加 WAL 模式建连时执行PRAGMA journal_modeWAL再在写入口用threading.Lock()包住 commit。更稳的做法是维护一个内存队列签到结果先放进队列由单线程批量落库但这对小系统来说有点过度设计锁就够了。5.4 face_recognition 装不上Windows 下 dlib 的编译门槛现象pip install face_recognition在 Windows 上卡在building wheel for dlib超过十分钟最后报编译错误。原因face_recognition 依赖 dlib而 dlib 在 Windows 上没有官方预编译 wheelpip 会现场编译需要 CMake 和 Visual Studio Build Tools。很多人连 python 环境变量都没配好终端用的 Python 版本和 VS Code 侧边栏选的不是同一个装包失败的排查更加混乱。解决先确认python --version和pip --version一致然后用 conda 安装 dlibconda install -c conda-forge dlib能绕开源码编译。装好 dlib 后再pip install face_recognition就很快了。Python 3.10 以上的版本对旧 dlib 支持较差有条件直接建一个 Python 3.9 的 conda 环境省掉大半兼容问题。如果这一步实在过不去就退回 OpenCV 的 LBPH 方案识别精度低一些但至少系统能跑。5.5 视频预览流畅但识别一卡一卡现象摄像头预览画面没问题一开启自动识别就变成幻灯片CPU 占用到 100%。原因识别线程和预览线程抢 CPU且每一帧都做全图人脸检测和特征提取。face_recognition 虽然封装得很优雅但单帧完整处理在 CPU 上要消耗 300 毫秒以上几路请求同时进来就卡死。解决抽帧是唯一的正解。每 3 帧只取 1 帧送进识别降采样比例调整为 0.5识别线程和预览之间用queue.Queue解耦识别线程没处理完就丢弃新帧保证队列长度永远为 1。这样 CPU 占用能压到 30% 以下漏检的代价由前面提到的二次签到机制兜住。6. 从演示到部署用 waitress 跑服务并做一次准确率验收系统能在 IDE 里跑通只是第一步课堂真实使用会有多人并发访问、长时间连续运行这些压力。演示环境用的 Flask 自带服务单线程处理能力弱把它换成生产级的 WSGI 服务器是一劳永逸的做法。from waitress import serve from app import app if __name__ __main__: serve(app, host0.0.0.0, port8000, threads8)waitress 是纯 Python 的 WSGI 服务器Windows 和 Linux 都能装省去配置 gunicorn 和 Nginx 的麻烦。threads8表示允许 8 个并发工作线程签到这种短请求完全够用。注意启动后要在防火墙放行 8000 端口同一局域网内用http://电脑IP:8000访问不要用 localhost。验证准确率时别靠真人反复进出镜头既累又不可复现。我的做法是录一段 3 分钟的教室视频包含每个学生依次走近摄像头的画面然后把这段视频循环播放作为识别输入统计结果。跑三次取平均值重点看三个指标识别准确率、误识率、平均响应时间。准确率低于 90% 就先加注册照片数量误识率高于 2% 就收紧阈值到 0.4响应时间超过 500 毫秒就调低帧采样频率。这套指标跑完系统才算真的能交付。我头一次做这个系统时把阈值设成 0.6觉得“宽容一点更友好”结果一个班识别完有三对同学互相记错了考勤。后来才发现注册照片是在办公室拍的识别现场在教室光线色温完全不一样。从那以后我养成了一个习惯注册照片一定在识别现场拍采集完先跑一轮 10 人小测试再正式用。这个习惯帮我避掉了很多后续的玄学问题希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进