ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Python+OpenCV实现在线课堂人脸识别考勤系统

Python+OpenCV实现在线课堂人脸识别考勤系统 简介基于Python与卷积神经网络的在线课堂考勤系统完整项目包面向教育技术开发者、Python学习者和人工智能入门者可用于实现人脸识别考勤、在线课堂签到与课堂管理自动化尤其适合需要将深度学习模型落地到Web应用场景的读者参考。资源包共98个文件包含22个Python源码脚本、15个XML配置、28个JPG人脸样本、5个DAT数据文件以及3个PT训练权重等整体约409MB既有模型训练与测试代码也有人脸检测的工程配置和示例图像便于对照理解CNN在考勤场景中的完整流程。已有1461人学习下载说明项目具备不错的参考价值。通过该资源可以获得可运行的考勤系统源码、PyCharm工程文件、训练好的模型权重及配套数据有助于快速复现在线课堂考勤流程并在此基础上扩展人脸识别、注意力检测等功能。1. 在线课堂考勤为什么比线下打卡难一个量级在线课堂的考勤问题比线下点名麻烦得多。线下你可以看座位、看眼神、听声音作弊成本高线上呢学生挂机刷时长、切屏刷网课、请人代挂老师只能靠一张截图和口头点名考勤数据基本是摆设。基于 Python 的在线课堂考勤系统就是一套用摄像头 人脸识别 自动点名替代人工截图的服务器方案——它解决的核心问题是怎么确认屏幕对面这个人“真的在线”而不是电脑自己在线。这套方案的技术栈不复杂OpenCV 做画面采集、人脸检测和比对Flask 做本地服务端SQLite 做名单存储最后把考勤结果导出成表格。适合三类人想给自家网课平台加考勤功能的后端开发、学校或培训机构的信息化老师、以及做 Python 项目练手并打算把简历写实的求职者。整个系统的生产级难点不在算法而在工程细节本文按落地顺序拆给你看。2. 系统整体设计为什么选 OpenCV dlib Flask 这套组合2.1 在线考勤的四个必备环节注册、点名、判定、导出一个能用的考勤系统不是“装个摄像头识别人脸”这么简单。我拆过这类项目核心链路至少四条缺一条都会在真实课堂里翻车。人脸库注册环节。开学第一周要把每个学生的正面照录入系统照片来源可以是学生交来的证件照、教务系统里的学籍照片也可以是开课前的视频采集帧。这个环节的产物是“学号 人脸特征向量”的对照表后续点名只跟这张表比对不跟原图比对。点名环节。老师发起点名后客户端打开摄像头每隔一段时间抓一帧画面检测画面里有没有人脸、是谁的脸把结果写进当次课堂的记录表。点名有两种模式一次性点名只在上课开始前打一次卡和持续抽检全程每隔 35 分钟静默识别一次。后者能有效防挂机因为挂机的学生不会一直盯着屏幕。判定环节。系统要回答三个问题来了没迟到没中途走了没判定逻辑不复杂但必须约定好规则比如上课前 5 分钟内识别成功算“准时”之后 15 分钟内算“迟到”中途累计 10 分钟识别不到且无手动签退记录算“早退”。规则要写在配置里不要写死在代码里因为每个学校的考勤制度不一样。导出环节。考勤记录存进数据库只是第一步老师真正需要的是 Excel 或 PDF 报表。这一环用 pandas 把查询结果转成 DataFrame再 to_excel 导出列宽、表头、迟到/缺勤标记都要处理好不然老师拿到一张乱码表等于白做。2.2 选型对比人脸识别库的四个候选与我的取舍人脸识别这个环节Python 生态里能用的东西不少但每个都有明显边界选错了后面全是坑。我把实际对比过的方案列出来方案人脸检测特征提取安装难度适合场景OpenCV Haar Cascade快但老式侧脸/暗光容易漏不支持低pip 直接装仅做人脸存在性检测OpenCV DNN 人脸检测器快且准支持小脸不支持低检测环节加速dlib face_recognition中上正面脸稳128 维特征向量质量高高需编译史上最折腾小规模千人以内人脸比对PaddleClas / InsightFace强强工业级中大规模、复杂场景我大多数项目用 dlib face_recognition 组合不是因为它最好而是因为它在“教学考勤”这个量级几百人课堂正面坐姿室内光线下准确率够用且生态成熟。face_recognition 库封装了 dlib 的检测和特征提取接口就三个函数face_locations、face_encodings、compare_faces新手也能一天上手。但要注意face_recognition 对 Python 版本挑剔3.8 / 3.9 是安全区3.10 以上装 dlib 经常要去源码编译十个人有八个在这里栽跟头。Flask 选型没有悬念。在线考勤的并发量不高——一个教室最多几十个学生同时提交识别结果Flask 的开发服务器都能扛住没必要上 FastAPI 或 Django。它够轻路由简单配合 HTML 模板就能做管理页面。生产部署就换 gunicorn四行命令的事。2.3 目录结构与数据库设计开工前先想清的事开始写代码之前先把工程结构摆好。我习惯下面这种布局后续加功能不会乱online_attendance/ ├── app.py # Flask 主入口 ├── config.py # 全局配置阈值、点名间隔、课程表 ├── face_service.py # 人脸注册 识别核心逻辑 ├── attendance_service.py # 考勤判定与统计 ├── models.py # SQLite 表结构定义 ├── camera_client.py # 客户端侧本地摄像头抽帧 上报 ├── templates/ │ ├── index.html # 管理后台首页 │ └── live_check.html # 点名页面 ├── static/ │ ├── js/capture.js # 浏览器端 getUserMedia 采集 │ └── css/style.css ├── data/ │ ├── faces/ # 学生照片原图注册时保存 │ ├── faces_db.npy # 人脸特征向量库 │ └── attendance.db # SQLite 数据库 └── exports/ └── attendance_2024xx.xlsx # 导出报表目录数据库表设计就三张别做复杂了。students 表id、student_no学号、name、face_encoding特征向量存文本用 numpy 转 bytes 后写 BLOB、created_at。courses 表id、course_name、teacher、schedule上课时间描述。attendance_records 表id、course_id、student_id、check_in_time、statuspresent / late / absent / leave、face_distance识别相似度留作审计追溯。提示face_distance 这个字段多数考勤系统不存但我强烈建议存。万一老师质疑“这个学生明明没来你怎么记到了”你可以拉出当时的比对距离证明这个人脸确实匹配上了。这是唯一能说清“机器为什么这么判”的黑匣子记录。3. 从零实现考勤核心人脸注册、课堂点名与迟到早退判定3.1 用摄像头完成学生人脸注册照片采集与特征入库注册阶段的核心目标把一张人脸照片变成一串 128 维的特征向量存起来。我用 OpenCV 调摄像头抓拍再用 face_recognition 提取特征写回 SQLite。import cv2 import face_recognition import numpy as np import sqlite3 import datetime def register_student(student_no: str, name: str, save_photo: bool True): cap cv2.VideoCapture(0) if not cap.isOpened(): raise RuntimeError(无法打开摄像头请检查设备权限) captured_frame None # 让画面预热 1 秒避免首帧过暗 for _ in range(10): ret, frame cap.read() # 显示实时预览等待用户按空格键拍照 while True: ret, frame cap.read() if not ret: continue cv2.imshow(Registration, frame) key cv2.waitKey(1) 0xFF if key ord( ): captured_frame frame.copy() break elif key 27: # ESC 取消 cap.release() cv2.destroyAllWindows() return None cap.release() cv2.destroyAllWindows() # 检测并提取特征 rgb_frame cv2.cvtColor(captured_frame, cv2.COLOR_BGR2RGB) face_locations face_recognition.face_locations(rgb_frame) if len(face_locations) ! 1: raise ValueError(f检测到 {len(face_locations)} 张人脸注册必须单人入镜) face_encoding face_recognition.face_encodings(rgb_frame, face_locations)[0] # 存档原图 特征向量 if save_photo: photo_path fdata/faces/{student_no}_{datetime.date.today()}.jpg cv2.imwrite(photo_path, captured_frame) conn sqlite3.connect(data/attendance.db) conn.execute( INSERT INTO students (student_no, name, face_encoding, created_at) VALUES (?, ?, ?, ?), (student_no, name, face_encoding.tobytes(), datetime.datetime.now()) ) conn.commit() conn.close() return face_encoding逻辑说明先打开摄像头并做 10 帧预热这一步很多人都忽略导致注册照片整体偏黑。随后进入预览循环空格键拍照ESC 取消。拍到原图后转 RGB 色彩空间这是 face_recognition 的硬性要求——它内部基于 dlib 的 RGB 模型你传 BGR 进去识别率直接打对折。特征提取完成后把 numpy 数组用 tobytes() 转成二进制存入数据库等点名时再 np.frombuffer 还原。参数说明face_recognition.face_locations 默认用 HOG 检测器速度中等、正面脸准。如果你想在注册时对照片质量要求更高一点把参数 model 改成 cnn 并传入检测区域它更准但慢注册环节人少用 CNN 也无妨。注意我要求单张人脸入镜注册阶段从严否则特征向量混了后面点名永远不准。另外OpenCV 的 VideoCapture(0) 的 0 代表默认摄像头如果你接了两个摄像头要显式指定设备号。3.2 点名主循环摄像头连续抽帧与人脸对比到了上课点名环节客户端每 3 秒抽一帧检测到人脸就提取特征和库里所有人比对。这个循环的难点不是识别而是不要让每一帧都做全量比对——几百个学生逐一算欧氏距离CPU 直接拉满。我在代码里做了两个优化先用 HOG 检测人脸区域再对检测到的人脸做全库比对比对结果命中后进入冷却期同一张脸 30 秒内不重复上报。import cv2 import face_recognition import numpy as np import sqlite3, time # 全局特征库启动时加载一次 def load_face_db(): conn sqlite3.connect(data/attendance.db) rows conn.execute(SELECT id, student_no, name, face_encoding FROM students).fetchall() conn.close() known_encodings [] known_meta [] for sid, sno, name, enc_blob in rows: known_encodings.append(np.frombuffer(enc_blob, dtypenp.float64)) known_meta.append({id: sid, no: sno, name: name}) return known_encodings, known_meta def check_in_loop(course_id: int, interval: float 3.0, threshold: float 0.45): known_encodings, known_meta load_face_db() cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) # 降低分辨率HOG 检测在 480p 上足够速度提升 3 倍 last_report_time {} reported set() # 已确认出席的学生不再重复上报 while True: ret, frame cap.read() if not ret: time.sleep(0.5) continue # 缩小画面提升速度 small_frame cv2.resize(frame, (0, 0), fx0.5, fy0.5) rgb_frame cv2.cvtColor(small_frame, cv2.COLOR_BGR2RGB) face_locations face_recognition.face_locations(rgb_frame) if not face_locations: time.sleep(interval) continue face_encodings face_recognition.face_encodings(rgb_frame, face_locations) for encoding in face_encodings: distances np.linalg.norm(known_encodings - encoding, axis1) min_idx np.argmin(distances) if distances[min_idx] threshold: student known_meta[min_idx] now time.time() # 冷却期控制同一学生 30 秒内不重复上报避免刷屏 if student[no] not in last_report_time or now - last_report_time[student[no]] 30: last_report_time[student[no]] now if student[no] not in reported: reported.add(student[no]) record_attendance(course_id, student[id], distances[min_idx]) print(f[{time.strftime(%H:%M:%S)}] {student[name]} 签到成功距离 {distances[min_idx]:.3f}) time.sleep(interval)逻辑说明我在循环里做了三件关键事。先把画面缩到 50%人脸检测的耗时从约 300ms 降到约 90ms代价是检测精度小幅下降但在摄像头距离人脸 0.52 米的课堂场景完全够用。然后用向量范数批量算距离而不是调 face_recognition.compare_faces 这个高层接口——因为后者一次只能比一张批量算距离一次比对全库性能差距是几十倍的。最后用 last_report_time 做冷却控制防止同一个学生在画面里停留时每 3 秒就触发一次写库。参数说明threshold 是欧氏距离阈值0.45 是我测下来比较稳的值。如果你在光线好的教室、学生坐得正可以放宽到 0.5如果教室光线复杂或有逆光收紧到 0.4 更稳但误拒率会上升。这个值的调优玄学很多第 5 章单独讲。冷缺期的 30 秒不是拍脑袋定的它要至少覆盖一次完整的人脸检测循环同时保证学生中途离开再回来后能在 30 秒后重新签到。3.3 迟到、早退、缺勤的判定用时间窗口规则替代人工登记考勤判定是整个系统里逻辑最简单但规则最容易扯皮的部分。我采用三次点名窗口规则上课前 5 分钟开启第一次点名上课后 15 分钟结束第二次点名在上课后 30 分钟第三次在下课前 10 分钟。每次点名窗口内只要能识别到一次就算“在线”。from datetime import datetime, timedelta def calculate_attendance_status(records: list, course_start: datetime, course_end: datetime): records: 该学生本次课的所有识别记录 [datetime, ...] 返回状态: present / late / absent / left_early if not records: return absent first_seen min(records) last_seen max(records) late_cutoff course_start timedelta(minutes15) early_cutoff course_end - timedelta(minutes10) if first_seen late_cutoff: status late # 第一次出现时课程已经开始超过 15 分钟 elif last_seen early_cutoff: status left_early # 最后一次出现距离下课超过 10 分钟 else: status present return status逻辑说明这套规则的优势在于简单透明任何人拿到这段代码都能解释判定依据。present / late / absent / left_early 四种状态覆盖了绝大多数课堂情况不会出现“既迟到又早退”这种让老师不知道怎么扣分的叠加态——实际业务里只需要取更严重的那个状态迟到 早退 旷课半天这部分逻辑我放在报表导出时处理。参数说明15 分钟和 10 分钟这两个窗口值是可配置的。大学课堂常见 15 分钟企业培训会短些5 分钟。我把它们放在 config.py 里方便教务老师自己改。注意一个边界情况如果课程只有 45 分钟15 分钟的迟到窗口就显得太长等于说上课 30 分钟内到都算准时这不合理。所以我在启动时加了校验迟到窗口不能超过课程时长的三分之一。4. 部署与集成把考勤服务嵌入钉钉、腾讯会议和本地教务系统4.1 远程课堂的摄像头画面从哪里来本地客户端 vs 浏览器采集在线课堂的考勤实现卡住所有人的第一个问题不是人脸识别而是画面从哪来。如果是自研网课平台学生端网页直接调摄像头画面实时传给考勤服务端这是最顺的方案。但很多机构用的是钉钉、腾讯会议、Zoom 上课考勤服务拿不到视频流必须另想办法。我常用的方案分成两类。浏览器采集在自研系统里用 getUserMedia API 从摄像头取视频流每 3 秒截一帧 base64 图片 POST 给 Flask 接口接口端用京东的 face_service 识别后返回结果。这个方式的坑在于浏览器安全策略——非 HTTPS 域名下 getUserMedia 直接拒绝本地调试要开 localhost部署到内网服务器也要配证书。虚拟摄像头方案如果课在腾讯会议上就另写一个本地程序用 OpenCV 读摄像头画面识别完人脸后把画面通过 pyvirtualcam 推给虚拟摄像头设备腾讯会议选择这个虚拟摄像头作为视频源。这样考勤和上课画面互不干扰缺点是每台学生机都要装齐 Python 环境 依赖维护成本高更适合企业内训这种机器可控的场景。// 浏览器端采集帧并上传核心片段 const video document.getElementById(camera-preview); const canvas document.createElement(canvas); const stream await navigator.mediaDevices.getUserMedia({ video: { width: 640, height: 480 }, audio: false }); video.srcObject stream; setInterval(async () { canvas.width video.videoWidth; canvas.height video.videoHeight; canvas.getContext(2d).drawImage(video, 0, 0); const frame canvas.toDataURL(image/jpeg, 0.7); // 压缩到 70% 质量 const resp await fetch(/api/check_in, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ course_id: 101, image: frame }) }); const result await resp.json(); if (result.status ok) { showToast(已签到${result.name}); } }, 3000);逻辑说明这个前端片段每隔 3 秒抓一帧转 JPEG base64 上传。压缩质量选 0.7 是考量过的——0.95 的图人脸识别精度并不会比 0.7 有明显提升但单帧体积从 200KB 涨到 900KB对移动端流量不友好。后端 Flask 拿到 base64 后解码再走同一条 face_recognition 比对链路。注意getUserMedia 一定要加 { audio: false }否则浏览器会弹出麦克风权限请求学生一旦拒绝整个页面脚本就断了。考勤只需要画面不需要声音。4.2 多课堂并发的调度线程池、队列与容错一个考勤服务通常要同时支持多个直播间/课程不能把识别逻辑放在 Flask 的请求处理线程里阻塞执行——每个学生的签到请求要跑一次全库比对耗时 200ms 以上并发一上来接口就雪崩。我把识别任务丢进独立的线程池接口立即返回“排队中”识别完成后再通过回调或轮询更新状态。from concurrent.futures import ThreadPoolExecutor import queue, threading # 全局线程池4 个工作线程任务队列容量 1000 executor ThreadPoolExecutor(max_workers4) task_queue queue.Queue(maxsize1000) def async_recognize(course_id: int, student_no: str, image_bytes: bytes): 把识别任务提交到线程池返回 task_id future executor.submit(recognize_task, course_id, student_no, image_bytes) return future def recognize_task(course_id: int, student_no: str, image_bytes: bytes): # 这里执行真正的 face_recognition 比对逻辑 try: result do_face_compare(student_no, image_bytes) save_attendance(course_id, student_no, result) return {status: ok, result: result} except Exception as e: # 识别失败不能影响其他学生签到 save_error_log(course_id, student_no, str(e)) return {status: error, message: str(e)}逻辑说明ThreadPoolExecutor 的 max_workers 设 4 是我在 8 核 CPU 上的实测值。识别任务主要是 CPU 密集型的特征提取线程数设成核数的一半比较合理留一半核心给 Flask 的请求处理和数据库读写。queue 容量 1000 是为了缓冲突发流量——比如上课前 30 秒所有学生同时提交签到队列能兜住不会因为请求过多直接 502。等队列满了后续请求走快速失败让客户端 3 秒后重试。参数说明这个方案有一个隐藏坑就是 face_recognition 库在加载模型时不是线程安全的多线程并发调用偶尔会报 model not loaded。解决方式是在模块加载时就初始化全局模型对象并在 recognize_task 外层加一个全局锁或使用 threading.local 存储模型实例。4.3 考勤报表导出用 pandas 生成老师能直接看的 Excel考勤的最后一公里是报表。老师不关心你数据库里存了什么他只看一个结果谁来了、谁没来、谁迟到。import pandas as pd from datetime import datetime def export_report(course_id: int, date: str): conn sqlite3.connect(data/attendance.db) query SELECT s.student_no, s.name, MAX(a.check_in_time) as last_check_in, a.status FROM students s LEFT JOIN attendance_records a ON s.id a.student_id AND a.course_id ? AND date(a.check_in_time) ? GROUP BY s.id df pd.read_sql_query(query, conn, params(course_id, date)) conn.close() # 生成统计列 df[是否出勤] df[status].map({present: 是, late: 迟到, absent: 否, left_early: 早退}) df df.rename(columns{ student_no: 学号, name: 姓名, last_check_in: 最后识别时间, status: 状态 }) filename fexports/考勤_{date}_{course_id}.xlsx with pd.ExcelWriter(filename, engineopenpyxl) as writer: df.to_excel(writer, indexFalse, sheet_namef考勤记录) # 设置列宽 worksheet writer.sheets[考勤记录] worksheet.column_dimensions[A].width 12 worksheet.column_dimensions[B].width 10 worksheet.column_dimensions[C].width 22 return filename逻辑说明查询用 LEFT JOIN 保证没签到的学生也会出现在结果里status 字段为 NULL映射时填“否”。这个点很关键——如果只查 attendance_records 表缺勤的人根本不会在表里出现报表会漏人。参数说明date 参数要用 YYYY-MM-DD 格式因为 SQLite 的 datetime 默认以文本方式存储字符串比较天然兼容这种格式。导出用 openpyxl 引擎而非 xlsxwriter是因为 openpyxl 支持对单元格宽度的后续修改且批量安装时 openpyxl 通常已被其他依赖带进来了。5. 避坑指南摄像头延迟、口罩识别、光线翻车5.1 摄像头画面卡顿与延迟堆积不是识别慢是缓存没清现象考勤客户端运行 5 分钟后画面越来越卡识别准确率急剧下降最后直接像幻灯片一样一帧一帧跳。原因opencv 的 VideoCapture.read() 从缓冲区取最新帧但摄像头驱动默认缓冲 30 帧。你处理一帧要 100ms缓冲区里就堆积了 34 帧read() 返回的永远是队尾的旧帧旧帧里的人脸姿势已经过时识别自然乱套。解决改用 grab() 只做解码丢弃然后 read() 取最新帧。或者干脆把缓冲区设小。完整修复方式是cap cv2.VideoCapture(0) # 清空缓冲区连续读取并丢弃 5 帧 for _ in range(5): cap.grab() # 后续循环中先 grab() 再 read() while True: cap.grab() # 丢弃上一帧 ret, frame cap.read() # 取当前最新帧这条经验在局域网摄像头和 USB 摄像头上都适用。USB 摄像头尤其明显因为 UVC 协议默认缓冲较大。5.2 dlib 安装不通过Python 3.10 以上的编译地狱现象pip install dlib 时报错 error: Microsoft Visual C 14.0 is required或者编译到一半 gcc 报内存不足。原因dlib 需要从源码编译需要 C 编译器。Windows 上要 Visual Studio Build Tools 的 C 桌面开发组件Linux 上要 gcc 和 cmake。Python 3.10 因为 ABI 变化基本没有预编译 wheel必须走源码编译耗时十几分钟且极易失败。解决三个方案任选。方案一直接用 Python 3.8 或 3.9 创建虚拟环境pip install dlib 前先装好 cmake 和编译器。方案二如果实在不想装编译器换用 OpenCV DNN 的 YuNet 人脸检测器 一个轻量识别模型纯 pip 安装无编译步骤。方案三用 Anacondaconda install -c conda-forge dlibconda 渠道有预编译包Windows 和 Linux 都能直接装。提示装完 dlib 后一定要先跑一段python -c import face_recognition确认没有 DLL load failed 再继续。很多人在这一步卡住后面代码全跑不了。5.3 学生戴着口罩识别率直接断崖下跌现象系统上线后第二天大量学生戴着口罩上课识别失败率从 5% 飙升到 60%老师投诉不断。原因face_recognition 的 128 维特征模型是从整张人脸提取的口罩遮住下半张脸等于丢掉了 40% 的特征信息特征向量偏移严重。这是模型设计的边界不是阈值调优能解决的。解决教学场景不能强制摘口罩那就改考勤策略分成两档。第一档如果画面检测到的人脸有口罩记录“到课但未验明身份”让老师人工确认。第二档无口罩学生走常规人脸比对距离阈值从 0.45 放宽到 0.5因为口罩下特征向量会偏一点阈值太严容易误拒。实现方式是在检测前先判断有没有口罩用 OpenCV DNN 的口罩检测模型跑一下或者简单点看下半脸区域的特征是否平坦。我个人建议考勤场景不要让口罩检测和人脸识别串行成本太高直接对戴口罩学生走人工确认通道最快。5.4 教室内光线过暗或逆光检测器找到脸但特征提取失败现象下午三四点靠窗位置的教室斜阳照进来学生背光坐人脸区域一片黑。face_locations 能框出人脸但 face_encodings 返回空列表。原因face_recognition 的编码模型对低照度和强逆光很敏感输入图像质量差时模型返回置信度不足自动丢弃。这不是 bug是模型的自我保护机制。解决在送入模型前先做图像增强。最有效的操作是直方图均衡化OpenCV 一句cv2.equalizeHist(gray_frame)就能提升对比度但对彩色图要做 YCrCb 转换后只对 Y 通道均衡不然颜色会偏掉。更狠一点的做法是 Gamma 校正把暗部提亮。另外把检测到的 ROI 区域做局部自适应直方图均衡CLAHE比整图均衡效果好一个级别。我在代码里加了配置开关默认开启 CLAHEimport cv2 def enhance_face_region(face_image): 对检测到的人脸区域做 CLAHE 增强解决逆光和暗光识别失败 lab cv2.cvtColor(face_image, cv2.COLOR_BGR2LAB) l_channel, a_channel, b_channel cv2.split(lab) clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) cl clahe.apply(l_channel) enhanced cv2.merge((cl, a_channel, b_channel)) return cv2.cvtColor(enhanced, cv2.COLOR_LAB2BGR)clipLimit 设 2.0 是保守值设太大画面会出现过曝的噪点人脸纹理被破坏识别率反而下降。tileGridSize 8x8 对 640x480 画面刚好不用调。5.5 Flask 开发服务器并发假死现象多个学生同时提交签到请求时页面转圈很久才响应偶尔直接 502。用 Flask 的 app.run() 启动时一个请求卡住全部卡住。原因Flask 自带的开发服务器是单进程单线程模型同一时间只能处理一个请求。人脸识别是 CPU 密集操作一个请求占住 CPU其他请求全部排队。解决本地测试用开发服务器没问题但部署到真实课堂环境必须换 gunicorn。启动命令加 4 个 workergunicorn -w 4 -b 0.0.0.0:5000 app:app如果不想上 gunicornWindows 环境装不了就在 app.run() 里加 threadedTrue。这个参数让 Flask 使用多线程处理请求开发调试足够生产别用。6. 进阶技巧用“三次点名 眨眼活体检测”把挂机率压到最低前文提到的持续抽检方案能防住“挂着不动”的学生但防不住“播放录好的视频”这种更高级的作弊。录一段学生本人的视频循环播放摄像头看到的是真人但没在听课。解决这个问题的标准做法是活体检测在考勤场景用最简单的眨眼检测就足够了——一个正常在线听课的人每分钟眨眼 1520 次而视频播放很难精确模拟眨眼节奏。这个方案成本低不需要深度学习模型dlib 的 68 点关键点检测就能算出眼睛的开合程度。眨眼检测的核心指标叫 EAREye Aspect Ratio眼睛纵横比。检测到人眼的关键点后计算上下眼睑之间的距离与眼宽的比例正常情况下这个值稳定在 0.25 左右眨眼瞬间会掉到 0.15 以下。在同一窗口内检测到一次“从高到低再到高”的变化就算一次眨眼。from scipy.spatial import distance as dist # 左右眼的 68 点关键点索引dlib 模型 LEFT_EYE list(range(42, 48)) RIGHT_EYE list(range(36, 42)) EAR_THRESHOLD 0.2 # 低于该值判定为闭眼 CONSEC_FRAMES 2 # 连续 2 帧闭眼才计一次眨眼 def eye_aspect_ratio(eye_points): 计算眼睛纵横比 EAR vertical_1 dist.euclidean(eye_points[1], eye_points[5]) vertical_2 dist.euclidean(eye_points[2], eye_points[4]) horizontal dist.euclidean(eye_points[0], eye_points[3]) return (vertical_1 vertical_2) / (2.0 * horizontal) def detect_blink(landmarks, frame_count): left_ear eye_aspect_ratio(landmarks[LEFT_EYE]) right_ear eye_aspect_ratio(landmarks[RIGHT_EYE]) ear (left_ear right_ear) / 2.0 if ear EAR_THRESHOLD: frame_count 1 else: if frame_count CONSEC_FRAMES: blink_detected True frame_count 0 return blink_detected, ear, frame_count逻辑说明右眼索引 3641左眼索引 4247这是 dlib 官方 shape_predictor_68_face_landmarks 模型的标准定义。EAR 计算原理不复杂上下眼睑距离越小、眼睛越细长EAR 越小。0.2 这个阈值来自论文和实测综合——正常睁眼时 EAR 普遍在 0.250.35 之间闭眼时低于 0.15取 0.2 做分界能避免误判。CONSEC_FRAMES 设为 2 是因为单帧 EAR 偏低可能是噪声或部分遮挡连续两帧偏低才算真正的眨眼。把眨眼检测和三次点名策略结合考勤的可信度能上一个台阶每次点名窗口内要求学生在 10 秒内完成至少一次自然眨眼并用双眼 EAR 曲线验证是活人。同时保留前文说的冷却期——学生完成一次有效签到后30 秒内不再重复要求眨眼减少打扰。真实部署时我还会配合随机点名弹窗老师可以随时发起一次额外点名所有学生端立即弹出验证框3 分钟内未完成者标记“存疑”课后人工复核。这套组合拳打下来挂机刷课的成本远远高于正常上课的成本考勤数据才真正有参考价值。这个系统我做了很多个版本最大的体会是在线考勤的技术难点从来不在人脸识别准确率而在如何理解真实的课堂使用场景。老师要的是省心学生要的是别被打扰教务处要的是数据可追究。搞清楚这三者的平衡点技术选型和参数调整自然就有方向了。你动手做的时候也别想着一次到位先从单机跑通点名流程再慢慢加自动判定、加报表、加活体检测每加一层都实测一轮踩过的坑就是下一个版本的需求清单。希望帮到你。本文还有配套的精品资源点击获取
返回列表