ARTICLE DETAIL

资讯详情

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

基于深度学习的在线课堂学生疲劳检测系统

基于深度学习的在线课堂学生疲劳检测系统 简介这套基于深度学习的学生面部表情疲劳检测在线课堂系统源码面向掌握Python与PyTorch基础、希望动手实践计算机视觉项目的高校学生尤其适合作为毕业设计或期末大作业的参考。项目完整覆盖从数据加载到模型训练评估的流程包含大量人脸表情图像数据集、dataloader数据加载器、基于CNN等结构的模型定义、train.py训练脚本以及result结果记录并附有README说明文档可帮助理解疲劳表情的关键特征提取、眼睛开合状态识别等核心任务。压缩包约47.44MB以近3万张jpg图像为主另含6个Python脚本、2个csv标注文件、1个png示意图和1个markdown说明目录分层明确便于按模块检索学习。目前已有179人学习下载代码与文档配合使用能显著降低上手门槛读者可完整跟踪数据预处理、模型训练、验证与结果分析过程为后续在人工智能、教育技术等领域的工作打下扎实基础。1. 在线课堂系统里为什么疲劳检测要用深度学习而非规则判断网课老师最头疼的不是讲课而是不知道屏幕另一端的学生是清醒还是昏昏欲睡。传统点名提问只能抽查想要持续感知课堂状态就得靠摄像头。深度学习基于学生面部表情疲劳检测的在线课堂系统就是把深度学习图像分类能力接到课堂场景里摄像头抓取面部区域模型判断当前表情属于疲劳还是专注再把结果汇成一条课堂状态曲线。这套思路做成的项目正好是毕业设计和期末大作业里少见的“有算法、有界面、有完整流程”的源码包解压zip后你既能看到CNN训练链路也能看到前端预警和课堂状态的联动改起来比纯图像分类项目丰富很多。它适合两类人一类是想把深度学习落到实际场景的本科毕设学生另一类是课程设计需要交“能演示、能答辩”系统的同学。理解了它你其实就理解了一个带实时视频流的端到端AI应用是怎么组装起来的。下面我会按“原理拆解—复现源码—系统集成—避坑—进阶验证”的顺序把它讲透。2. 拆解项目核心面部表情如何映射到疲劳状态以及为什么多是CNN方案2.1 疲劳检测不只看“打哈欠”常用表情标签与判定逻辑疲劳是一个生理状态面部表情只是观测信号。一个成熟项目不会只做“眨眼频率”这种单一指标而是把面部动作单元组合起来。常见做法是将学生表情分成几类专注、困倦、分心、疲劳。摄像头先用人脸检测器框出脸部再裁剪成固定尺寸输入分类网络。网络输出的不是“疲劳”和“不疲劳”的二值而是一个概率分布。系统根据阈值把概率映射为课堂状态比如疲劳概率大于0.7时触发提醒。这个映射关系是项目的灵魂。源码里一般会有一个label_map或者class_names文件你需要检查它定义了几类。如果是二分类训练难度低但误报率偏高如果是四分类以上模型能区分“疲惫眨眼”和“正常眨眼”但对数据量和标注一致性要求高。毕设阶段建议至少用三类清醒、轻度疲劳、重度疲劳。轻度疲劳对应频繁眨眼、眼睛半闭重度疲劳对应闭眼超过0.5秒或打哈欠。这个粒度足够支撑在线课堂系统的告警逻辑又不会让模型训练太难。还有一个容易忽略的点在线课堂系统里“疲劳”不是一个静态结果而是时间窗口里的统计量。源码里通常会维护一个长度为N的滑动窗口每收到一帧的预测结果就推入窗口计算最近30秒内重度疲劳帧的比例。只有比例超过阈值才真正触发告警否则单人单次闭眼会被当成噪声。这个设计比“单帧判定”严谨得多答辩时也更好讲。2.2 为什么选深度学习表情识别而不是传统视觉特征早期疲劳检测依赖眼部纵横比EAR、嘴巴纵横比MAR这些手工特征加上阈值判断。它的问题在于光线、角度一变化阈值就得重调。在家庭网课环境下学生位置不固定侧脸、低头、手托腮都常见传统特征很容易翻车。深度学习CNN直接学“什么长相算疲劳”对姿态变化更鲁棒而且源码里的模型结构一般就是ResNet18、MobileNetV2或者更轻的SqueezeNet。选择轻量级网络是合理决策在线课堂系统要实时跑不能为了精度上ResNet50导致推理延迟高。如果你打开源码发现模型是VGG16不必急着换。VGG16在中等规模表情数据集上精度可靠只是参数量大。项目能跑通是第一优先级后续再替换MobileNet。此外这个项目的深度学习部分通常包含两种范式的选择一是用预训练模型做特征提取只训练全连接层二是整个网络端到端微调。课程设计阶段建议用预训练模型微调最后一层因为表情数据集往往只有几万张从头训练容易过拟合。代码里如果再带上人脸检测模型如OpenCV的Haar或MTCNN你要注意推理链路里它的输出框需要做padding再做resize最后进分类网络这些细节在复现时往往会暴露。2.3 用PyTorch打印模型结构确认源码里的分类头参数拿到源码后不要急着跑训练先确认模型输出层和你的类别数是否一致。常见做法是写一个几行的Python脚本打印模型。import torch from models import FatigueCNN # 以源码实际模型名为准 model FatigueCNN(num_classes3) print(model) total sum(p.numel() for p in model.parameters() if p.requires_grad) print(f可训练参数量: {total / 1e6:.2f}M)这段代码有两个作用第一打印出来的结构能让你看清卷积层怎么堆叠、全局池化后面接了几个全连接第二可训练参数量能帮你判断这个模型是否适合CPU训练。参数量在5M左右的MobileNetV2用笔记本CPU跑一个epoch大约几分钟如果是20M以上的VGG建议直接换预训练权重或者改用GPU。参数说明num_classes必须和标签文件里的类别数一致。源码如果自带的是二分类权重而你想做成三分类最后一行全连接层的out_features需要改成3并且这一层的权重会被随机初始化不能直接加载旧权重。打印完结构后顺手检查一下config.py里的CLASS_NAMES确认顺序和训练标签一致避免后面出现“标签错位”这种隐蔽问题。3. 把源码跑起来环境配置、数据准备与最小复现命令3.1 项目解压后的第一件事核对依赖与目录结构从zip解压后先别急着双击train.py。先在终端里进入项目根目录看看目录组织。常见的毕设结构长这样online_classroom/ ├── config.py ├── requirements.txt ├── dataset/ │ ├── images/ │ └── labels.csv ├── models/ │ └── fatigue_cnn.py ├── scripts/ │ ├── extract_frames.py │ └── split_dataset.py ├── train.py ├── inference.py └── web/ ├── app.py └── templates/ └── index.html这个结构反映了三条主线models是深度学习模型定义scripts是数据准备工具web是课堂系统的服务端。很多毕设源码会把这些混在一个文件里你花十分钟拆成清晰目录后面改起来会舒服很多。接着创建虚拟环境并安装依赖python -m venv venv source venv/bin/activate # Windows 下是 venv\Scripts\activate pip install -r requirements.txt如果requirements.txt缺失或者安装失败就用最稳妥的一套组合torch、torchvision、opencv-python、numpy、pandas、flask、pillow。版本不需要强制最新PyTorch用2.x系列即可因为源码主要是用torchvision里的预训练模型接口API相对稳定。注意人脸检测器如果用的是OpenCV自带Haar级联不需要额外安装如果用的是MTCNN则要额外装mtcnn库且它依赖tensorflow。这时候你需要评估一下项目复杂度——纯PyTorchOpenCV的Haar方案在毕设答辩里完全够用没必要为了“先进”引入一套几十GB的TensorFlow依赖。3.2 数据准备视频切帧、人脸对齐与标签生成训练一个表情分类模型前需要整理数据集。最常见的来源有两个一是公开表情数据集比如FER2013、CK二是自己录几段网课视频把学生面部表情抓下来。公开数据集标注规范但“课堂疲劳”场景不突出自录视频标注麻烦但更贴近你的毕设题目。我建议混合使用公开数据做预训练微调自录数据做最后验证。如果源码里有视频文件你需要把视频切成人脸图片。下面这段脚本是常见的做法import cv2 import os video_path data/classroom.mp4 output_dir dataset/images/fatigue os.makedirs(output_dir, exist_okTrue) cap cv2.VideoCapture(video_path) face_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) frame_id 0 count 0 while cap.isOpened(): ret, frame cap.read() if not ret: break if frame_id % 5 0: # 每5帧取1帧避免连续帧冗余 gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale( gray, scaleFactor1.1, minNeighbors5, minSize(60, 60) ) for (x, y, w, h) in faces: # 向外扩20像素尽量框住额头和下巴 x1 max(0, x - 20) y1 max(0, y - 20) x2 min(frame.shape[1], x w 20) y2 min(frame.shape[0], y h 20) face frame[y1:y2, x1:x2] face_resized cv2.resize(face, (224, 224)) cv2.imwrite(f{output_dir}/frame_{count:05d}.jpg, face_resized) count 1 frame_id 1 cap.release() print(f提取到 {count} 张人脸图)代码解释用OpenCV的Haar级联检测正脸每5帧抽一帧降低冗余。scaleFactor1.1表示每次缩放10%这个值越小检测越慢但召回率越高minNeighbors5控制误检值越大框越少但越可靠。人脸框外扩20像素是为了保留部分额头和下巴——表情分类和纯人脸识别不一样眼睛、嘴巴周围的信息很重要裁太紧会丢失特征。提取完图片后你大概率需要手动检查和删掉一些误检图。这一步是“黑匣子”最容易藏脏数据的地方别偷懒。3.3 训练一个CNN疲劳分类器关键参数与命令行拿到标注好的图片后训练脚本通常长这样python train.py \ --data_dir dataset \ --model mobilenetv2 \ --num_classes 3 \ --epochs 30 \ --batch_size 32 \ --lr 0.001 \ --pretrained参数解释model选择MobileNetV2因为它小且推理快num_classes3对应清醒/轻度疲劳/重度疲劳batch_size32在8G显存下比较稳如果显存不够就降到16lr0.001是Adam优化器的经典初始值配合权重衰减weight_decay1e-4防止过拟合pretrained表示加载ImageNet预训练权重。训练过程中的两个关键点一是保存准确率最高的权重而不是最后一轮的权重常见的做法是torch.save(model.state_dict(), best_model.pt)放在验证集准确率提升时二是设置early stopping连续5个epoch验证集loss不下降就停止避免在课堂数据集上反复过拟合。源码里如果没写这两个逻辑建议自己加上答辩时可以说“我加了正则化和模型选择策略”。训练结束后测试脚本会对一张新图片输出预测概率import torch from torchvision import transforms from PIL import Image model.load_state_dict(torch.load(best_model.pt, map_locationcpu)) model.eval() transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) img Image.open(test_face.jpg) inputs transform(img).unsqueeze(0) with torch.no_grad(): outputs model(inputs) probs torch.softmax(outputs, dim1) print(probs) # [清醒概率, 轻度疲劳概率, 重度疲劳概率]注意数据预处理方式必须和训练时完全一致尤其是Normalize的均值和标准差。很多人测试时翻车就是因为训练时用了ImageNet归一化测试时忘了做。4. 在线课堂系统的集成玩法视频流接入、前后端交互与实时告警4.1 从摄像头到模型推理按帧处理与ROI截取模型训练好之后下一步是接到在线课堂系统里。最简单的集成方式是写一个inference.py用OpenCV读取摄像头每帧做一次人脸检测和分类推理。需要注意实时性连续对每一帧跑完整深度学习推理CPU会扛不住。常见做法是跳帧比如每3帧处理一次或者用队列缓存最近几帧由推理线程慢慢消费。import cv2 import torch import numpy as np cap cv2.VideoCapture(0, cv2.CAP_DSHOW) # Windows下避免摄像头打开慢 face_cascade cv2.CascadeClassifier(cv2.data.haarcascades haarcascade_frontalface_default.xml) frame_skip 3 frame_count 0 while True: ret, frame cap.read() if not ret: break frame_count 1 if frame_count % frame_skip ! 0: continue gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale(gray, 1.1, 5, minSize(80, 80)) for (x, y, w, h) in faces: face frame[max(0, y-20):yh20, max(0, x-20):xw20] face_resized cv2.resize(face, (224, 224)) inputs transform(face_resized).unsqueeze(0) with torch.no_grad(): probs torch.softmax(model(inputs), dim1).numpy()[0] fatigue_prob probs[2] # 重度疲劳概率 label FATIGUE if fatigue_prob 0.7 else AWAKE cv2.rectangle(frame, (x, y), (xw, yh), (0, 0, 255) if label FATIGUE else (0, 255, 0), 2) cv2.putText(frame, f{label}: {fatigue_prob:.2f}, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2) cv2.imshow(Online Classroom, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这段代码演示了最小实时推理链路。CAP_DSHOW是Windows下推荐的后端能避免打开摄像头时的卡顿frame_skip3让推理频率降到约10FPS已经足够课堂告警使用。fatigue_prob 0.7是阈值你可以通过调整它控制灵敏度。注意这个0.7阈值对应的是“重度疲劳类别”的概率如果你的模型输出顺序不同要按CLASS_NAMES索引千万别写死成probs[2]。4.2 把疲劳状态推进课堂系统Flask推送与Web端展示视觉部分跑通后在线课堂系统需要一个服务端把模型输出暴露给前端。用Flask写一个轻量API是毕设最常见的选择。流程是前端页面开启摄像头通过fetch把图片帧传到后端后端跑推理后返回JSON状态前端定时刷新页面上的课堂状态仪表盘。from flask import Flask, request, jsonify, render_template import base64 import cv2 import numpy as np app Flask(__name__) app.route(/api/detect, methods[POST]) def detect(): data request.get_json() image_b64 data[image].split(,)[1] img_bytes base64.b64decode(image_b64) np_arr np.frombuffer(img_bytes, np.uint8) frame cv2.imdecode(np_arr, cv2.IMREAD_COLOR) # 复用上一节的人脸检测与推理代码提取fatigue_prob fatigue_prob predict_fatigue(frame) return jsonify({ fatigue_prob: round(fatigue_prob, 4), status: FATIGUE if fatigue_prob 0.7 else NORMAL }) app.route(/) def index(): return render_template(index.html) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)解释前端把摄像头帧转成base64 JSON传给/api/detect后端解码成OpenCV图像再跑模型返回概率和状态。host0.0.0.0是为了让局域网内的教师端也能访问如果你只需要本机演示改成127.0.0.1更安全。如果想做得更完整可以把疲劳检测结果写进一个课堂状态日志记录学生ID、时间、疲劳频率最后导出Excel。这一步虽然不是深度学习核心但往往是毕设评分里“系统完整性”的加分项。5. 做疲劳检测毕设最容易踩的坑从数据不平衡到课堂复杂场景5.1 现象验证集准确率95%但真实课堂几乎失效训练时验证集上准确率很高一放到真实课堂就频频误报把正常学生也说成疲劳。原因课堂环境和训练数据分布不一致。公开表情数据集多为正面、均匀光照、无遮挡而真实课堂上学生可能侧脸、戴眼镜、手撑下巴甚至背光。模型学到的是“数据集风格”不是真正的“疲劳特征”。另外疲劳样本太少导致模型倾向于预测多数类验证集正好也是同类分布所以指标好看。解决用至少20%的真实课堂视频截图做测试集不要和训练集混在一起。如果效果不佳用自录数据对模型做微调只改最后的全连接层训练10轮左右即可。不要指望一个通用表情模型直接在教室场景零成本迁移。另一个办法是提高疲劳类别在训练数据中的占比通过过采样或对疲劳图片做亮度/噪声增强让模型见过更“脏”的输入。5.2 现象一张脸被检测成多个框导致疲劳预测错乱在两个人同框或人脸有遮挡时Haar检测器会在同一张脸上输出多个重叠框导致同一个表情被判定多次前端告警频率异常。原因detectMultiScale的minNeighbors设置过小Haar本身对遮挡和脸部边缘敏感容易产生重复检测。此外框外扩逻辑可能把背景也包进去让分类器拿到混乱输入。解决先做非极大值抑制NMS把IoU大于0.3的重叠框合并。OpenCV没有内置NMS可以用torchvision.ops.nms或直接写一个十几行的NMS函数。另一个人为可靠的方案是只取面积最大的检测框作为当前目标学生的脸适用于单人网课场景。你在答辩时可以说“我用了最大脸策略来稳定告警”听上去更工程化。5.3 现象训练loss降不下去先检查标签和预处理顺序训练时loss一直在0.8左右徘徊几个epoch后也没有明显下降。原因大概率是标签错位或者数据预处理顺序不对。比如labels.csv里的图片路径和实际文件名对不上或者数据增强里先RandomHorizontalFlip再RandomResizedCrop导致人脸被翻转后语义改变。另一个常见原因是归一化参数用错把图像像素除以255后没有正常化导致输入数值范围异常。解决先打印一个batch的数据和标签人工比对。然后检查transform里是否在ToTensor之前做ResizeNormalize的均值方差是否按ImageNet标准填。养成一个好习惯单独写一个脚本可视化训练样本把若干张输入图片和标签叠在一起输出眼睛看一遍比调试数值高效得多。5.4 现象实时推理掉帧严重GPU利用率上不去摄像头画面卡顿GPU占用率却只有40%左右CPU核也没跑满。原因视频读取、人脸检测、图像预处理的耗时远大于CNN推理本身而且主循环里所有操作都是串行的。摄像头在Windows下默认读取会产生较高延迟人脸检测如果在CPU上跑Haar已经比GPU推理慢一个数量级。解决把视频读取放到一个独立线程用队列传递帧给推理线程。推理时先将人脸检测结果缓存如果上一帧的检测框和当前帧的位置差距不大可以直接沿用上一帧框不必每帧都跑Haar。更进阶的做法是用OpenCV的dnn模块加载一个轻量人脸检测模型替代Haar但会额外增加依赖。最后别忘了给推理部分用torch.set_grad_enabled(False)关闭梯度减少显存占用。5.5 现象答辩时老师问“你的疲劳标准是哪来的”很多项目把“疲劳”当黑匣子source code里就是一堆标签和阈值说不出依据答辩容易垮。原因标注疲劳本身就有主观性。公开数据集根本没有“课堂疲劳”这个标准很多人用CK或FER2013凑数类别映射完全是拍脑袋。解决提前把判定逻辑写成文档疲劳程度参考PERCLOS单位时间闭眼比例和打哈欠频率。比如定义“疲劳状态最近30秒内闭眼时长占比超过40%”或“重度疲劳帧占比超过25%”。模型中输出的疲劳概率是这些指标的视觉代理。哪怕你的阈值是0.7也要给出理由在自建验证集上画ROC曲线选择兼顾召回率和误报率的点。答辩时拿出这张图说服力会强很多。6. 给毕设加分的进阶验证Grad-CAM热力图与课堂实测记录6.1 用Grad-CAM看模型到底在盯眼睛还是嘴巴疲劳判定的关键区域是眼睛和嘴巴。你可以用Grad-CAM可视化模型注意力证明它不是盯着背景或头发在分类。from torchvision.models import mobilenet_v2 from torch.nn.functional import relu # 假设model是已加载权重的模型target_layer是最后一层卷积 target_layer model.features[-1] # MobileNetV2的last conv gradients None activations None def forward_hook(module, input, output): global activations activations output.detach() def backward_hook(module, grad_input, grad_output): global gradients gradients grad_output[0].detach() target_layer.register_forward_hook(forward_hook) target_layer.register_backward_hook(backward_hook) output model(inputs.unsqueeze(0)) output[:, 2].backward() # 重度疲劳类别 weights gradients.mean(dim(2, 3), keepdimTrue) cam relu((weights * activations).sum(dim1, keepdimTrue)) cam torch.nn.functional.interpolate(cam, size(224, 224), modebilinear)代码说明注册前向和反向hook拿到目标层的激活值和梯度用梯度对激活图加权平均得到类激活热力图。output[:, 2].backward()表示我们要看的是“重度疲劳”这个类别的注意力区域。如果热力图中眼睛和嘴巴区域更亮说明模型学到了合理特征。6.2 课堂小样本实测记录告警延迟和误报次数做一个简单的实测表在答辩时展示。选取一段3分钟课堂视频记录学生真实状态和系统状态时间(秒)学生状态系统状态是否一致0-30清醒NORMAL是31-60打哈欠NORMAL否漏报61-90闭眼FATIGUE是91-120低头写字FATIGUE否误报121-180犯困眨眼FATIGUE是写完表后统计漏报率和误报率。漏报率高的方向是降低阈值误报率高就提高阈值。这一套验证做完你不仅完成了毕设功能还补上了“实验设计”和“性能分析”两块内容导师最看重这部分。我自己的习惯是每次改完模型或阈值都会录一段30秒的自己摄像头画面做回归测试看看告警频率是不是符合直觉。经历过一次模型在光线下集体翻车后再也不敢只盯着公开数据集指标。这个项目真正有价值的部分不在训练精度有多高而在于你亲手把“深度学习”和“在线课堂系统”捏在一起并且知道它哪里会坏、怎么修。希望这一篇能帮你在复现源码时少走几步弯路。本文还有配套的精品资源点击获取
返回列表