ARTICLE DETAIL

资讯详情

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

多人人脸识别课堂考勤系统:Python源码架构与调优实战

多人人脸识别课堂考勤系统:Python源码架构与调优实战 简介基于Python的多人人脸识别课堂考勤系统源码为一套面向校园考勤场景的完整实现适合教学管理人员、Python学习者及需要快速搭建人脸识别项目的开发者。系统通过摄像头采集影像借助人脸检测与特征比对自动记录出勤覆盖数据采集、身份识别、考勤统计等核心环节源码中可看到OpenCV/Dlib等主流库的应用思路。压缩包共43个文件总大小约31KB主要包含10个Python业务模块如attendance、auth、core等、21个HTML前端页面与1个CSS样式文件另有schema.sql数据库脚本、requirements.txt依赖清单及README说明结构清晰便于按功能模块查看。目前已有104人浏览学习。通过这套源码读者可以掌握多人人脸识别考勤系统的工程组织方式包括人脸数据管理、管理员/教师端页面逻辑、登录认证与配置分离等设计细节还可直接基于其中的数据模型与配置模板进行二次开发作为课程设计或课题参考很有价值。1. 多人人脸识别课堂考勤这套 Python 源码解决的是什么问题传统课堂点名占用上课时间扫码签到又能被代签钻空子。基于 Python 的多人人脸识别课堂考勤系统本质上是用 OpenCV/Dlib 这类成熟人脸识别算法把「摄像头看到谁」自动变成「谁在几点几分到了课堂」再落到 SQLite 里供教师端查询统计。它的核心价值不只是替代点名而是让考勤记录变成一份可追溯、可统计、可导出的数据资产。这套源码适合三类人正在做毕设或课设的 Python 方向学生、学校或培训机构的信息化管理人员以及想学习 Flask OpenCV 数据库如何串成一个完整 Web 应用的一线开发者。拿到源码之后你不用从零搭建骨架重点是搞清楚每个文件负责什么以及识别参数怎么调才不漏检、不误报。2. 源码结构与数据流Flask SQLite 骨架下每个文件的分工2.1 从文件列表看系统架构五个核心文件先定位这套源码从文件命名上能直接看出是典型的 Flask 应用结构同时把识别逻辑和数据访问做了一层隔离。我拿到源码第一件事不是急着跑而是把文件按「入口 → 业务 → 数据」三层归类这样后面改哪里心里有数。文件职责定位关键依赖app.pyFlask 应用入口注册路由和蓝图Flask、db.pyattendance.py考勤业务逻辑识别结果落库face_recognition、db.pycore.py人脸识别核心封装检测与特征比对cv2、face_recognitionadmin.py / dashboard.py管理端与教师端页面路由Flask、db.pydb.py / schema.sql数据库连接与表结构定义sqlite3常见的错误是上来直接改 app.py结果发现页面能开但考勤写不进去原因往往是没理清 attendance.py 和 db.py 之间的调用关系。我一般按「页面请求 → 路由 → 业务函数 → 数据库」这条链去读代码也就是从 dashboard.py 或 attendance.py 的接口函数往里追比顺着文件列表读效率高得多。2.2 数据库表结构考勤记录不是只存一个时间戳schema.sql 里定义了系统最底层的数据模型。人脸识别考勤系统至少需要三张表学生表存姓名和学号以及人脸特征向量考勤记录表存每次识别到的打卡信息用户表存教师或管理员账号。特征向量可以直接以文本或 BLOB 形式存进 SQLite因为识别时一次性加载全部向量到内存比对单节课几十人的数据量完全扛得住。CREATE TABLE students ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_no TEXT UNIQUE NOT NULL, name TEXT NOT NULL, face_encoding TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE attendance_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_id INTEGER NOT NULL, course_id INTEGER, checkin_time DATETIME DEFAULT CURRENT_TIMESTAMP, status TEXT DEFAULT present, FOREIGN KEY (student_id) REFERENCES students(id) ); CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, role TEXT DEFAULT teacher );face_encoding 字段存的是人脸特征向量序列化后的字符串通常是 128 个浮点数转成的文本。之所以不单独建一张特征表是因为学生数量有限单表存储更简单查询时一次读出来反序列化即可。注意 student_no 加了 UNIQUE 约束这能防止同一个学生被重复录入照片时产生多条记录识别后写考勤时也要用 student_id 做去重判断。2.3 Flask 路由与页面组织登录、考勤、仪表盘是怎么串起来的app.py 负责把页面和接口组织起来。典型的路由包含登录页、仪表盘、考勤页和管理端。这套源码里 templates 目录下有 dashboard、admin、auth、attendance 等子目录对应的就是 Flask 的 render_template 视图层。from flask import Flask, render_template, request, session, redirect import db import attendance import dashboard app Flask(__name__) app.secret_key change_this_in_config app.route(/) def index(): if user_id not in session: return redirect(/auth/login) return redirect(/dashboard) app.route(/attendance/live, methods[GET, POST]) def attendance_live(): if request.method POST: result attendance.process_frame(request.json[frame_data]) return {status: ok, result: result} return render_template(attendance/live.html) if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)这段路由的核心逻辑是未登录用户重定向到登录页考勤页面 POST 一帧图像数据过来交给 attendance.process_frame 处理返回识别结果。host 设为 0.0.0.0 是为了让教室里的学生端也可以通过局域网访问大屏页面。debugTrue 在开发时有用但部署到教学楼服务器时一定要关掉否则异常信息会直接暴露到页面上。3. 人脸识别核心链路OpenCV 检测、特征比对与多人去重的实现方式3.1 人脸检测选型Haar、HOG、CNN 在课堂场景的取舍core.py 里封装的人脸检测部分决定系统能不能在教室场景下稳定框出人脸。OpenCV 的 Haar 级联分类器是最轻量的方案检测速度快、CPU 就能跑但对侧脸和小尺寸人脸不敏感。Dlib 的 HOG 线性 SVM 检测器比 Haar 更准对角度变化有一定容忍度缺点是慢一点。再往上就是基于 CNN 的检测模型精度最高但需要更多算力。课堂考勤场景有一个特殊性学生坐在座位上基本是正脸或轻微侧脸人脸在画面里的尺寸通常在几十像素以上而且摄像头通常固定不动。我建议优先用 HOG 检测器配合 minSize 参数过滤掉远处的小人脸避免把教室后面墙上的人形海报也框出来。import dlib detector dlib.get_frontal_face_detector() def detect_faces(frame): # 参数1是图像参数2是上采样次数提升小人脸召回率越大越慢 faces detector(frame, 1) return [(f.left(), f.top(), f.right(), f.bottom()) for f in faces]上采样次数设为 1 是课堂场景的折中点。设成 0 时速度快但漏检明显后排学生的人脸可能直接被跳过设成 2 时检测更全但帧率下降在只跑 CPU 的教室电脑上会出现画面卡顿。我一般会先用 1 跑一轮看漏检情况再决定是否加采。3.2 特征提取与比对tolerance 参数直接决定误报率检测到人脸后要提取特征向量。这套源码用的是 face_recognition 库封装的特征提取能力它基于 dlib 的 resNet 模型把每张人脸压成一个 128 维向量。识别阶段不需要训练分类器拿实时帧的向量和数据库里预存的向量做欧氏距离比对距离小于阈值就认为是同一个人。import face_recognition import numpy as np known_encodings [] known_names [] def load_known_faces(): for student in db.get_all_students(): encoding np.frombuffer(student[face_encoding].encode(), dtypenp.float64) known_encodings.append(encoding) known_names.append(student[name]) def identify_face(face_image): encoding face_recognition.face_encodings(face_image) if len(encoding) 0: return None distances face_recognition.face_distance(known_encodings, encoding[0]) min_index np.argmin(distances) if distances[min_index] 0.45: # 阈值越小越严格 return known_names[min_index] return Noneface_recognition.face_distance 返回的是一个距离数组最小距离小于阈值才认定为匹配。0.45 是我在多人课堂场景常用的起始值配合光线较好的教室环境能兼顾误报和漏报。如果发现经常把 A 认成 B就往下调如果发现本班学生经常识别失败就往上调。这属于典型的需要按现场环境微调的参数源码里给的值只能作为起点。3.3 多人去重与追踪同一节课不能给同一个人记十次考勤多人场景最大的问题不是识别而是去重。一节课 45 分钟摄像头一直在跑张三只要进入画面就会被反复识别到。如果不做去重考勤表会被刷屏。去重逻辑一般有两种一是基于时间窗口同一学生两次打卡之间必须超过 10 分钟才再次写入二是基于会话标记系统运行期间每个学生只记第一次识别到的时间。from datetime import datetime, timedelta last_seen {} def check_and_record(student_id, course_id): now datetime.now() last last_seen.get(student_id) # 距离上次记录不到5分钟直接丢弃这次识别结果 if last and (now - last) timedelta(minutes5): return False last_seen[student_id] now db.insert_attendance(student_id, course_id, now) return True时间窗口设 5 分钟的考虑是学生中途上厕所出去再回来不应该再产生第二条记录但如果是上午第二节课再次识别到窗口已经过期可以正常记为新考勤。这个 last_seen 字典放在内存里就可以系统重启后清空不影响考勤数据的完整性因为真正的考勤记录已经落库了。3.4 视频流主循环线程化避免页面卡死实时识别不能直接在 Flask 请求里跑 while True 循环否则页面请求会一直挂着。常见做法是把摄像头识别放到一个独立线程里主线程只负责读取最新的识别结果。源码里的 attendance.py 基本就是这个思路用一个全局变量保存当前帧的识别输出Web 页面通过轮询或 WebSocket 拿结果。import cv2 import threading frame_result {frame: None, names: []} def recognition_loop(camera_index0, course_id1): cap cv2.VideoCapture(camera_index) while True: ret, frame cap.read() if not ret: continue faces detect_faces(frame) names [] for (top, right, bottom, left) in faces: face_image frame[top:bottom, left:right] name identify_face(face_image) if name and name ! Unknown: names.append(name) frame_result[frame] frame frame_result[names] names threading.Thread(targetrecognition_loop, args(0, 1), daemonTrue).start()识别循环单独跑线程的好处是即使 Flask 页面同时被多个学生访问识别也不会中断。camera_index 参数要留意笔记本自带摄像头通常是 0外接 USB 摄像头可能是 1接错会出现黑屏。识别到的 names 只是展示用真正写数据库还是在 check_and_record 里做避免展示线程和写入线程耦合。4. 考勤记录与统计数据库写入逻辑、课时判定和教师端仪表盘4.1 考勤写入逻辑识别成功不等于记录成功人脸识别成功只是第一步真正落库时还要判断当前是否处于有效上课时间、该学生是否属于本课程、之前是否已经记过。这套源码把判断逻辑放在了 attendance.py 里与识别逻辑分离好处是换一套识别引擎时考勤规则不用动。def process_recognition_result(student_id, course_id): student db.get_student_by_id(student_id) if not student: return {success: False, reason: student_not_found} course db.get_course_by_id(course_id) if not course or not course_is_active(course): return {success: False, reason: course_not_active} if db.has_record(student_id, course_id, present): return {success: False, reason: already_recorded} db.insert_attendance(student_id, course_id, datetime.now()) return {success: True, name: student[name]}course_is_active 是课时判定的核心它要处理的是「这门课现在是否正在上课」。最简单的实现是查课程表把当前时间落在课程开始前 10 分钟到结束后 10 分钟的区间内当成有效识别时间。这个时间窗口太短学生早到就记不上太长下课后的识别也算出勤。我一般把提前量设为 10 分钟延后量设为 15 分钟这样既允许学生提前到教室又不会在课间把下一节课的人误记到上一节。4.2 统计查询与报表教师端要看到的是缺课名单教师端仪表盘不是把原始识别记录直接铺在页面上而是应该按课程、按日期聚合给出应到、实到、缺勤、迟到几类统计。源码里 dashboard.py 的查询逻辑值得直接复用它用一条 SQL 就完成了按课程统计出勤人数的操作。def get_course_attendance_summary(course_id): query SELECT COUNT(DISTINCT s.id) AS total_students, COUNT(DISTINCT a.student_id) AS present_count FROM students s LEFT JOIN attendance_records a ON s.id a.student_id AND a.course_id ? AND DATE(a.checkin_time) DATE(now) row db.query_one(query, (course_id,)) absent_count (row[total_students] - row[present_count]) return { total: row[total_students], present: row[present_count], absent: absent_count }这条 SQL 用 LEFT JOIN 保证即使没有考勤记录的学生也会出现在结果里这是统计缺勤名单的关键手法。如果改用 INNER JOIN没来上课的学生根本不会出现在查询结果里缺勤人数就算不出来。DATE(a.checkin_time) DATE(now) 限定统计当天记录需要查历史记录时把它改成参数即可。4.3 迟到与早退判定不能只看是够出现过更完整的考勤系统还要区分正常、迟到、早退。做法是在考勤记录表里扩展一个 status 字段写入时根据识别时间和课程开始时间的关系赋值。这套源码提供了这个字段但默认写入逻辑只填了 present需要手工扩展判定条件。def decide_status(checkin_time, course): start datetime.strptime(course[start_time], %H:%M) if checkin_time.time() start.time(): return late return present调用时把 checkin_time 替换成识别当刻的时间。这个逻辑可以在 check_and_record 里调用也可以放在写入后的异步任务里做。我建议放在写入时同步判断因为迟到名单需要实时体现在教师屏幕上异步处理会有几分钟延迟教师看到的结果就不够直观。判了 late 之后仍然写入记录只是 status 不同统计缺勤时 late 算实到统计纪律时可以单独筛选。4.4 前端页面数据呈现表格与图表选哪种dashboard 模板里通常有一个考勤汇总页和一个课程管理页。汇总页用表格列出每位学生的姓名、学号、出勤状态、识别时间管理页用折线图展示一周内每天出勤率变化。表格用原生 HTML Jinja2 模板就能解决图表部分建议引入简单的前端库而非自研因为考勤场景的数据量不大不需要上重图表框架。table thead trth学号/thth姓名/thth状态/thth识别时间/th/tr /thead tbody {% for record in records %} tr td{{ record.student_no }}/td td{{ record.name }}/td td{{ record.status }}/td td{{ record.checkin_time }}/td /tr {% endfor %} /tbody /tablestatus 字段建议用中文或约定好的缩写直接渲染不要在前端再做一次映射因为 Jinja2 模板里写大量 if-else 会很难维护。识别时间列保留完整日期时间便于教师核对具体时刻。表格数据量超过一屏时加一个简单的分页或滚动容器这个在源码模板里已经有了直接用就行。5. 运行避坑摄像头打不开、识别率低、多人漏检的排查记录5.1 现象程序启动后画面黑屏或马上闪退原因cv2.VideoCapture(camera_index) 的索引不对。笔记本内置摄像头、外接 USB 摄像头、教室监控采集卡分别占用不同的设备索引固定的 0 不一定代表你要用的那个设备。解决方法不要硬编码索引把 camera_index 放进 config.py 里做成配置项。启动时写一个重试链依次尝试 0、1、2哪个能读到有效帧就用哪个。我常用一个简单的探测函数循环打开设备并读取一帧判断返回值能有效避免黑屏启动。5.2 现象Windows 上 pip install face_recognition 报错原因face_recognition 依赖 dlib而 dlib 需要 CMake 编译Windows 上经常因为缺少 Build Tools 或 CMake 环境变量没配齐导致安装失败。很多新手在这一步就放弃了这个项目其实完全可以绕过。解决方法先安装 CMake 和 Visual Studio Build Tools再 pip install dlib最后装 face_recognition。另一个省事的方案是直接安装包含了预编译 dlib 的 conda 环境。这一步属于环境搭建的踩坑区不属于业务代码问题源码本身没有问题卡住的往往都是编译链。另外 Python 版本建议用 3.8 或 3.9face_recognition 在新版本 Python 上有时会有兼容性问题。5.3 现象识别率低同一个学生有时候认得出有时候认不出原因光照变化和头部姿态。教室窗户透进来的自然光在一天之内变化很大上午顺光下午逆光同一张脸的特征值会漂移。此外学生低头写字或歪头说话时检测器能框出人脸但特征提取质量差。解决方法在识别预处理阶段做一次直方图均衡化用 cv2.equalizeHist 或 CLAHE 提升暗部细节。更有效的是给学生建立多角度特征库录入照片时拍正面、左右偏转 15 度三张取特征向量的平均值。我见过有的项目只存一张入学照结果这个学生换了发型就识别失败加了三张角度照后误报率立竿见影地降低。5.4 现象多人同时进入画面时漏检严重后排学生识别不到原因检测器的 minSize 参数没调。后排学生人脸在画面中可能只有 40×40 像素Haar 或 HOG 检测器对小于 minSize 的目标直接跳过。另一个原因是帧尺寸太大处理器忙不过来导致跳帧。解决方法把待识别的帧按宽边缩放到 640 或 800 像素再做检测同时把 minSize 设为 (60, 60)这样后排小脸也能被召回。缩放帧是最常被忽略的性能优化直接识别 1920×1080 的原始帧和缩放到 640 宽度后的帧检测速度能差三四倍而识别准确率几乎不变。占用资源降低后系统能处理的并发人数自然就上去了。5.5 现象数据库里学生姓名显示乱码原因SQLite 连接时没有设置 text_factory 或写入读取时编码不一致。Python 3 下 SQLite 默认支持 UTF-8但有时源码里连接数据库没有显式声明Windows 控制台输出时用了 GBK 导致看起来像乱码。解决方法连接 SQLite 后执行 PRAGMA encoding UTF-8并在读取显示时用 utf-8 重新编码处理。更彻底的做法是代码里统一用 Unicode 字符串不在业务层做编解码转换。这条踩坑看着小但在交付给学校时非常致命教师打开考勤表看到一堆乱码第一印象就直接否定整份源码。5.6 现象本机能跑部署到教学楼无显示器服务器上就崩原因服务器上没有任何桌面环境OpenCV 的 GUI 功能初始化失败。虽然不是每台机器都会报错但 cv2.imshow 和 cv2.waitKey 在没有显示器的环境中一定会出问题。解决方法把识别循环里的 imshow 调用去掉或者加一个 USE_GUI 开关在无头模式下只做识别和落库不进画面显示。教师端看实时画面改为通过 Web 页面拉取识别结果而不是依赖本机窗口。这类服务器上最常见的坑就是 debugTrue 没关加上 imshow 调用双重踩雷。6. 提升识别准确率的实操从扩充训练照片到阈值调优6.1 更聪明的做法离线批量入库代替逐个拍照很多人在录入学生照片时一张一张地拍浪费时间且质量不一。更高效的方式是准备每个人 3 到 4 张不同角度的清晰照片放在一个目录里写一段批量入库脚本自动提取特征向量写入数据库。这样既保证了特征库的多样性又能在短时间内完成几十人的录入。import os import glob import face_recognition def batch_load_students(photo_dir): for photo_path in glob.glob(photo_dir /*.jpg): filename os.path.basename(photo_path) student_no, name filename.split(_)[0], filename.split(_)[1].split(.)[0] image face_recognition.load_image_file(photo_path) encodings face_recognition.face_encodings(image) if len(encodings) 0: print(f跳过无人脸图片: {photo_path}) continue encoding_str encodings[0].tobytes().decode(latin1) db.insert_student(student_no, name, encoding_str)文件名约定成学号_姓名.jpg脚本就能自动解析并入库。tobytes().decode(latin1) 是特征向量序列化的常用手法latin1 编码可以无损还原任意字节序列。入库后看一眼打印日志凡是出现跳过的图片多半是照片里有多个人脸或人脸太小这种照片即使强行入库也会在后续识别时造成干扰。6.2 阈值调优的操作方法先收集误报样例再调参数tolerance 不是拍脑袋定的要基于实际数据调。第一步是让系统在没有学生时对着空教室跑 10 分钟把误识别到的人脸全部记下来第二步是让每个学生在镜头前正常走一圈记录哪些人被漏识别。这两份数据加起来小于总样本的 2%说明当前阈值合格否则按比例上下调整 tolerance。调阈值有个经验法则误报率每降低 1 个百分点漏报率可能上升 1 到 2 个百分点。所以在校园场景宁可让阈值稍松因为缺勤裁定的成本远高于«个别误报»。一个学生被误认成另一个学生教师看得到名字不对可以手动更正但一个到课学生被漏记成缺勤教师事后核查的成本就高得多。从这个角度看0.45 的初始值偏严时可以放宽到 0.5视现场反馈定。6.3 识别质量验证的土办法用录制视频离线回放上线前最靠谱的验证不是让学生排队实测而是把一节真实课堂的录像录下来离线跑一遍识别循环对比人工标注的结果和系统识别结果。录像回放的好处是你可以固定测试集每次改完代码或调完参数就跑同一个视频看到识别结果的变化。如果每次改动后结果都在往更好的方向走说明调参路径是对的。我会准备两个视频一个是阳光充足的上午实录一个是光线较暗的阴天实录分别统计识别准确率。两个视频都达到 95% 以上再让学生真正用。从那以后我每次做考勤类项目都会先做一套离线验证数据再谈上线人脸识别这东西很吃现场条件光靠实验室里的自导自演远远不够。这个习惯帮我挡掉了大量现场翻车的情况希望帮到你。本文还有配套的精品资源点击获取
返回列表