ARTICLE DETAIL

资讯详情

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

Python多人人脸识别课堂考勤系统:源码架构与部署实战解析

Python多人人脸识别课堂考勤系统:源码架构与部署实战解析 简介这是一套基于Python的多人人脸识别课堂考勤系统源码面向学校教师、教务管理人员及初中级Python开发者。系统通过摄像头采集学生影像利用人脸检测、特征提取与比对自动记录出勤情况覆盖人脸数据采集、存储、识别、考勤统计与后台管理等完整流程可直接作为课堂考勤方案参考也适合用于课程设计或毕业设计。资源包共43个文件压缩包仅31KB包含10个Python核心源码文件、21个HTML页面模板、数据库SQL脚本、CSS样式表、说明文档以及多个zbak配置备份文件目录结构清晰便于按模块研读和二次开发。目前已有104人浏览学习。开发者可从源码中学习OpenCV/Dlib等库的人脸识别调用方法理解Web考勤系统的模块划分、数据库设计与异常处理思路备份文件还能帮助对比配置改动快速掌握多人识别场景的工程实现要点。1. 多人人脸识别课堂考勤系统这份源码到底能做什么课堂考勤一直是教学里最磨人的环节四五十人的班老师点名要花两三分钟学生低头玩手机代签的也不少。用 Python 做的人脸识别课堂考勤系统源码正好是在这个场景里落地的——通过摄像头同时捕捉多名学生自动完成人脸检测、特征比对把出勤时间写进数据库教师和教务后台随时能查。这套源码不是那种单文件算法 demo而是把 Flask Web 服务、后台管理、模板页面和 SQLite 数据库串成了一个完整应用。它适合正在做课程设计、需要快速搭一套可用考勤系统的开发者也适合学校或培训机构的信息化老师拿来二次改造。我按自己的复现流程把它过了一遍下面把系统结构、运行步骤和踩过的坑都说清楚。2. 系统架构与核心模块从 app.py 到 core.py 的代码脉络2.1 项目文件清单与模块职责入手任何源码包我第一件事都是先摸清文件结构不然路由找不到、模板渲染不出来的时候连去哪排查都不知道。这份源码的文件组织比较典型解压后主要文件如下. ├── app.py # Flask 应用入口路由注册 ├── core.py # 人脸检测、特征提取与比对核心 ├── auth.py # 登录认证与会话管理 ├── dashboard.py # 考勤统计与报表接口 ├── admin.py # 后台管理学生/教师/课程 ├── attendance.py # 考勤记录写入与查询 ├── db.py # SQLite 连接与基本封装 ├── data_model.py # 数据模型定义 ├── config.py # 默认配置 ├── config_local.py # 本地环境覆盖配置 ├── requirements.txt # Python 依赖清单 ├── schema.sql # 数据库表结构初始化脚本 ├── templates/ # Jinja2 模板 │ ├── base.html │ ├── error.html │ ├── auth/ # 登录注册页面 │ ├── attendance/ # 考勤页面 │ ├── dashboard/ # 统计报表页面 │ └── admin/ # 后台管理页面 ├── static/ │ └── style.css # 前端样式 └── README.md # 项目说明注意目录里还有几个.zbak后缀的文件比如config_local.py.zbak、requirements.txt.zbak这是开发者保留的备份文件。我之前也习惯在改动大的时候留一份备份部署时直接用没有.zbak的同名文件就行不用理会备份。模块分工上app.py负责创建 Flask 实例、注册蓝图把/、/auth、/dashboard、/admin这些路由挂上去core.py是算法核心摄像头帧进来后先做人脸检测再提取特征向量最后和数据库里的学生特征比对attendance.py负责把识别结果写入考勤表同时处理“同一学生短时间内重复打卡”这类业务逻辑db.py和data_model.py封装了数据库连接和 ORM 映射让core.py不用直接拼 SQL。这种分层方式的好处是换数据库、换人脸识别算法时只需要动对应模块不用把所有文件都改一遍。2.2 数据库设计与 schema.sql考勤记录怎么落库考勤系统的数据模型核心是三张表学生表、考勤记录表、用户表。schema.sql里通常会有类似下面的结构CREATE TABLE IF NOT EXISTS students ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_no TEXT UNIQUE NOT NULL, name TEXT NOT NULL, face_feature TEXT NOT NULL, -- 存储人脸特征向量JSON/二进制编码 photo_path TEXT, -- 学生照片路径便于人工核对 created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS attendance ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_id INTEGER NOT NULL, course_id INTEGER NOT NULL, check_time DATETIME DEFAULT CURRENT_TIMESTAMP, status TEXT DEFAULT present, FOREIGN KEY (student_id) REFERENCES students(id) ); CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, role TEXT DEFAULT teacher ); CREATE UNIQUE INDEX idx_attendance_day ON attendance(student_id, course_id, date(check_time));这段 SQL 里最关键的设计是attendance表的唯一索引idx_attendance_day。它约束了同一个学生在同一门课同一天只能有一条考勤记录从数据库层面杜绝了重复刷脸打卡的问题。face_feature字段是核心人脸识别算法提取出的特征向量通常会序列化成 JSON 字符串存进去下次识别时直接取出比对。如果你用的是face_recognition这类库特征向量是 128 维 float 数组在 Python 端用json.dumps(list_of_floats)存放比对时再用json.loads还原即可。为什么不直接存照片因为特征向量比照片小得多而且特征比对是计算距离不是像素匹配存特征能大幅减少 I/O 和时间。不过要注意不同人脸库的特征维度不一样如果你后续换成 FaceNet512 维需要做兼容处理否则老数据全废。2.3 人脸识别核心流程core.py 里的检测与比对逻辑core.py的逻辑是整份源码的灵魂。它做的事情可以拆成四步读帧、检测人脸、提取特征、与库比对。常见实现是结合 OpenCV 和face_recognition库核心代码大致如下import face_recognition import numpy as np import cv2 def process_frame(frame, known_encodings, known_ids, threshold0.4): # 将 BGR 帧转成 RGBface_recognition 基于 RGB 处理 rgb_frame cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # 1. 检测当前画面中的所有人脸位置 face_locations face_recognition.face_locations(rgb_frame) # 2. 提取每张人脸的 128 维特征编码 face_encodings face_recognition.face_encodings(rgb_frame, face_locations) results [] for encoding in face_encodings: # 3. 与已知特征列表计算距离取最近的一个 distances np.linalg.norm(known_encodings - encoding, axis1) min_idx np.argmin(distances) min_distance distances[min_idx] # 4. 距离小于阈值才算匹配否则视为陌生人 if min_distance threshold: student_id known_ids[min_idx] results.append((student_id, float(min_distance))) else: results.append((None, float(min_distance))) return face_locations, results参数threshold0.4是识别置信度阈值这个值需要按实际场景调。face_recognition官方推荐 0.6 作为一般情况下的临界值但在课堂这种多人密集场景下我建议把阈值压到 0.40.45否则容易把长得相近的学生搞混。min_distance是欧氏距离也可以换成余弦相似度但face_recognition固化的编码方式已经算好了 L2 距离用np.linalg.norm是最直接的。源码里的实际实现可能有更多细节比如把识别结果封装成字典、叠加画框等。但核心套路就是这一步检测 → 编码 → 距离 → 阈值判断。理解了这个流程后面调参、换模型就都有了方向。3. 环境搭建与首次运行把源码跑起来的完整步骤3.1 依赖安装requirements.txt 与 Python 版本选择拿到源码第一件事不是急着跑而是先看依赖。requirements.txt里通常会有这些包Flask2.2.3 opencv-python4.5.5.64 face-recognition1.3.0 numpy1.21.6 Pillow9.2.0 gunicorn20.1.0注意 Python 版本face_recognition依赖的dlib对 Python 版本很挑Python 3.10 以上编译dlib特别容易失败。我的经验是 Python 3.73.9 最稳。安装命令如下python -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate pip install -r requirements.txt如果pip install时dlib报错常见的解决方法是先装 Visual C Build ToolsWindows或cmakeLinux/macOS再单独装dlib。我最早在 Python 3.11 上装face-recognition折腾了一下午没成功后来切回 Python 3.8 一次过。所以这里可以先看README.md里有没有注明 Python 版本没有就直接用 3.8 或 3.9避免踩坑。3.2 配置 config.py 与 config_local.py数据库和摄像头参数源码里把默认配置和本地配置分开了这是好习惯。config.py放团队通用配置config_local.py放你自己的本机变量而且config_local.py加在.gitignore里不进版本库。典型的配置内容如下# config.py import os class BaseConfig: SECRET_KEY os.environ.get(SECRET_KEY, dev-key-change-me) DATABASE os.path.join(os.path.dirname(__file__), attendance.db) CAMERA_INDEX 0 RECOGNITION_THRESHOLD 0.4 CHECK_INTERVAL_SECONDS 60 # config_local.py from config import BaseConfig class LocalConfig(BaseConfig): DATABASE /home/me/attendance/local.db CAMERA_INDEX 1 # 笔记本外接摄像头可能是 1 RECOGNITION_THRESHOLD 0.38DATABASE指定 SQLite 文件路径CAMERA_INDEX是 OpenCVVideoCapture用的摄像头编号笔记本自带摄像头通常是 0外接 USB 摄像头可能是 1。RECOGNITION_THRESHOLD和CHECK_INTERVAL_SECONDS分别控制识别灵敏度与最短打卡间隔。这类参数在app.py或core.py加载时会用import config_local的方式覆盖默认值方便不同环境切换。3.3 启动与验证从 schema 初始化到 admin 登录环境配置好后按下面的顺序操作# 1. 初始化数据库 python -c import db; db.init_db() # 2. 启动 Flask 服务 python app.py如果db.py里没有现成的init_db()函数也可以直接用sqlite3 attendance.db schema.sql导入。启动后打开浏览器访问http://127.0.0.1:5000应该能看到登录页。首次登录需要管理员账号schema.sql或者README.md里通常会预置一个比如admin / admin123。登录进去后先到后台录入学生照片这时系统会对照片提取特征存入数据库。然后在考勤页面打开摄像头对准屏幕前的几个人测试识别成功后页面会实时显示姓名和进教室时间。这一步常见的翻车点是摄像头权限。浏览器访问localhost:5000时如果用的是 HTTP 协议部分浏览器会禁用摄像头导致getUserMedia失败但源码里如果直接走 OpenCV 读取本地摄像头就不受浏览器限制要看实现方式。我复现时直接把 Python 服务跑在宿主机上没开虚拟机摄像头索引 0 才正常。4. 多人识别的关键参数与调优让识别更准更快4.1 人脸检测阈值与识别置信度的设置人脸识别里有两种“阈值”经常混在一起检测阈值人脸区域的可信度和识别阈值特征距离的门限。face_recognition库的大多数函数没有显式暴露检测阈值但你可以通过face_locations的model参数改检测器hog比cnn快但小尺寸人脸容易漏检。识别阈值就是上一节代码里的threshold设置规则建议参考下表场景阈值范围说明一般单人体测0.50.6宽松一点减少拒识多人课堂考勤0.380.45严格一点降低误报高安全性验证0.30以下宁可不识别也不认错课堂场景里误识别比漏识别更麻烦。你把阈值调成 0.6可能张三刷脸被记成李四调成 0.4最多是某个学生低着头没刷上老师在后台补一下就行。源码里的默认值如果是 0.5我会直接改成 0.4然后依据学生的面部特征差异再微调。4.2 多人同时考勤摄像头帧率与队列处理的平衡多人场景最容易出现的问题是画面里同时有 8 个人逐帧跑face_recognition会非常慢。这个库的特征提取和比对是 CPU 密集运算一帧 1080p 图像在普通四核 CPU 上做一次全流程可能要 200ms500ms。源码里如果直接用while True: process_frame(frame)这种串行方式画面会明显卡顿识别结果也会滞后。常见做法是引入跳帧和多线程队列import threading import queue import cv2 import time frame_queue queue.Queue(maxsize10) result_queue queue.Queue() def capture_worker(): cap cv2.VideoCapture(0) while True: ret, frame cap.read() if ret and not frame_queue.full(): frame_queue.put(frame) time.sleep(0.03) def recognition_worker(known_encodings, known_ids): skip 0 while True: frame frame_queue.get() skip 1 if skip % 2 ! 0: # 每两帧处理一次 continue results process_frame(frame, known_encodings, known_ids) result_queue.put(results)这里的skip 2就是跳帧数实际意思是每隔一帧做一次识别。跳帧后识别频率降低但任务队列不会积压frame_queue限制为 10 帧处理不过来时旧帧直接丢弃保证始终显示最新画面。如果服务器配置好把跳帧数设为 1检测密度更高但 CPU 占用会明显上去。我一般会在教室空闲时测一下 CPU 占用率超过 80% 就加大跳帧别把服务拖死。4.3 模型替换从 Haar 到 CNN 的升级路径源码如果用的是 OpenCV 的 Haar 级联检测器或 HOG 检测器你会遇到两类问题Haar 误检多经常把教室后面的海报人脸当真人HOG 对侧脸和低头动作不友好。升级路径有两步先换检测器再换编码器。检测器可以换 OpenCV DNN 人脸检测模块用 pre-trained Caffe 模型res10_300x300_ssd_iter_140000.caffemodelnet cv2.dnn.readNetFromCaffe(deploy.prototxt, res10_300x300_ssd_iter_140000.caffemodel) def detect_faces_dnn(frame): h, w frame.shape[:2] blob cv2.dnn.blobFromImage(frame, 1.0, (300, 300), (104.0, 117.0, 123.0)) net.setInput(blob) detections net.forward() boxes [] for i in range(detections.shape[2]): confidence detections[0, 0, i, 2] if confidence 0.7: box detections[0, 0, i, 3:7] * np.array([w, h, w, h]) x1, y1, x2, y2 box.astype(int) boxes.append((x1, y1, x2, y2)) return boxes替换后process_frame里的face_locations来源就改成这个检测器。特征提取方面face_recognition底层用的模型是 OpenFace 的 NN4换编码器需要重构core.py的接口改动较大。新手可以先只换检测器识别准确率就有明显提升。注意 DNN 模型文件要放在static/models/或单独目录源码包里不一定带模型文件需要自己下载落地的最后一步别漏了。5. 避坑指南人脸考勤系统常见的五个坑5.1 摄像头打开失败或画面卡死现象启动考勤页面后视频区域一片黑或者下拉选择摄像头后画面冻结不动。原因最常见的是摄像头索引不对。笔记本内置摄像头可能是 0但某些品牌型号会把索引让给虚拟摄像头或扩展坞导致自动选择到空设备。其次是 OpenCV 在读取帧时没有处理cap.read()返回的False摄像头被占用比如 Zoom 正在用时程序不报错只是卡在循环里。解决在core.py或入口处加一个设备探测逻辑for idx in range(3): cap cv2.VideoCapture(idx) if cap.isOpened(): ret, _ cap.read() if ret: print(f使用摄像头索引 {idx}) break cap.release()同时用try/finally确保每次识别会话结束都cap.release()避免进程挂掉后摄像头资源不释放。5.2 同一个学生被识别成两个人现象张三第一次刷脸成功第二次换了个角度再刷系统又提示新学生入库或者后台学生列表里出现“张三”“张三2”。原因数据库特征库存在多份特征程序比对时只看距离最近的那一条不会合并属于同一个人的多条特征。如果录入照片时给同一个学生建了两个档案识别结果就会漂移。解决录入逻辑上保证每个学生只有一个student_no录入新照片时先检查是否已有特征。如果已经发生在后端做特征合并把同一个人的多个 128 维向量取平均得到新向量更新students.face_feature字段。我复现时直接在admin.py里加了一个“特征合并”按钮专门处理这类脏数据。5.3 SQLite 数据库锁导致写入失败现象考勤记录写入偶尔报错database is locked尤其在多人同时刷卡、后台管理员同时操作时。原因SQLite 默认在同一时刻只允许一个写入事务Flask 开发服务器的多线程模式下多个请求同时写同一个.db文件就会触发锁等待超时。解决在db.py中设置sqlite3.connect(database, timeout10)并开启 WAL 模式conn.execute(PRAGMA journal_modeWAL;) conn.execute(PRAGMA busy_timeout5000;)WAL 模式下读和写可以并行极大减少锁冲突。如果真的并发量高到 SQLite 扛不住再迁移到 PostgreSQLdata_model.py里把数据库连接部分替换掉就行。5.4 模板渲染404或静态资源加载失败现象跳转页面时出现TemplateNotFound或者登录页完全不带样式CSS 加载不出来。原因Flask 要求模板文件必须放在templates/目录下并且render_template的路径要和实际目录层级一致。源码里templates下有auth/attendance等子目录如果你在app.py里写成render_template(auth_login.html)而实际文件是templates/auth/login.html就会 404。静态资源路径也需要用url_for(static, filenamestyle.css)生成。解决先核对app.py的蓝图前缀和生产环境目录结构。常见做法是给每个模块建蓝图蓝图设置template_folder比如auth_blueprint Blueprint(auth, __name__, template_foldertemplates/auth)这样render_template(login.html)就会自动从templates/auth找文件。静态文件统一放static/并用url_for引用。5.5 考勤重复打卡、防作弊失效现象同一个学生在同一次课堂里反复刷脸系统重复写入多条考勤记录或者学生提前到教室刷了脸就走也算全勤。原因一是attendance表没有加唯一约束我前面在 schema 里写的idx_attendance_day就是防这个的源码如果没加就要补上二是业务层缺少时间窗口校验比如规定“两次考勤间隔必须大于 60 分钟”或“首次打卡后 30 分钟内离开视为早退”。解决在attendance.py的写入函数里增加校验def check_attendance(student_id, course_id): today date.today() last get_last_record(student_id, course_id, today) if last and (datetime.now() - last.check_time).seconds CHECK_INTERVAL_SECONDS: return False, 重复打卡 insert_attendance(student_id, course_id) return True, 成功CHECK_INTERVAL_SECONDS可以从config_local.py读取默认 60 秒课堂场景可以放大到 10 分钟避免学生故意进出刷次数。6. 进阶把考勤系统接到真实课堂环境前的验证清单6.1 用已有照片模拟多人场景批量测试脚本上线前别急着拿真人试先用一批课堂座位照片离线验证识别率。我通常会把学生证件照和课堂照分开用下面的脚本模拟摄像头逐帧识别import os import face_recognition known_encodings [] known_ids [] for img_file in os.listdir(roster_photos): img face_recognition.load_image_file(froster_photos/{img_file}) enc face_recognition.face_encodings(img) if enc: known_encodings.append(enc[0]) known_ids.append(os.path.splitext(img_file)[0]) test_imgs os.listdir(classroom_photos) success 0 for t in test_imgs: frame face_recognition.load_image_file(fclassroom_photos/{t}) locations face_recognition.face_locations(frame) encodings face_recognition.face_encodings(frame, locations) for e in encodings: distances np.linalg.norm(np.array(known_encodings) - e, axis1) if min(distances) 0.45: success 1 print(f识别成功 {success}/{len(test_imgs)})这个脚本帮你提前发现问题是阈值太严还是学生照片角度差异太大。如果成功率低于 80%别急着上线先补录学生照片或调整阈值。6.2 迟到早退判定与考勤报表的二次开发源码的考勤统计一般在dashboard.py里默认只统计出勤/缺勤。真实课堂还需要迟到早退。我建议在后端增加status枚举present、late、early_leave。写入时根据上课时间判断from datetime import datetime, time def get_status(check_time, class_startdatetime.now().replace(hour8, minute0)): if check_time class_start timedelta(minutes15): return late return present报表查询 SQL 可以扩成这样SELECT course_id, COUNT(CASE WHEN statuspresent THEN 1 END) AS full_attend, COUNT(CASE WHEN statuslate THEN 1 END) AS late_count FROM attendance WHERE date(check_time) ? GROUP BY course_id;这样教师就能在 dashboard 页看到每门课的迟到比例不需要额外导 Excel。6.3 我的一次真实部署教训光照和角度对识别率的影响有一回在教室后排墙面装了摄像头首次试运行识别率只有 70%。排查发现学生低头看课本时摄像头拍到的基本是头顶和发际线侧脸角度超过 45 度face_recognition的特征编码表现得极不稳定。后来我做两件事一是调整摄像头位置到黑板斜上方让镜头平视学生面部二是收集每位学生正面、左右侧三个角度的照片存入特征库比对时用“最近距离 角度优先”策略识别率才拉回 95% 以上。从那以后我每次部署考勤系统都要先到真实教室拍 3 分钟课堂视频离线跑一遍识别脚本确认准确率稳定在 90% 以上才正式启用。光照突变、逆光窗户、后排小脸这些问题都必须拿到真实环境验证过才能放心。希望帮到你。本文还有配套的精品资源点击获取
返回列表