简介:这份资源面向智慧教室与教育质量评估场景,提供了一套基于深度学习的课堂专注度监测与作弊行为识别系统Python实现,适合计算机视觉方向的毕业设计选题者、算法学习者及教育信息化开发者参考。系统整合面部特征分析、微表情解析与身份认证模块,实现动态考勤管理;在学术规范监督上,通过关键骨骼点轨迹分析建立头部偏转、视线俯角与物品传递三类异常行为识别机制,核心组件包括ResNet面部特征提取模型、68点面部轮廓定位器及随机森林行为分类器。资源包共752个文件,以382个Python源码为主体,辅以zbak备份、md说明、yaml配置、jpg与gif图示、cfg与cpp底层文件等,压缩包约87.35MB,目录结构按检测系统与权重模块划分清晰。目前已有82人学习下载,读者可获取完整算法实现、模型权重文件与推理演示脚本,便于快速复现并理解非接触式感知在教育评估中的落地思路。
1. 课堂专注度监测与作弊行为识别:从摄像头到告警的完整链路
教室后排那个总低着头的学生,到底是在记笔记还是在刷手机?监考时两个学生手指在桌下同步敲击,是巧合还是在对答案?这类问题靠人眼盯屏越来越不现实——一个老师同时看几十路画面,注意力衰减比学生走神还快。基于深度学习的课堂专注度监测与作弊行为识别系统,要解决的就是把「看画面」这件事交给模型:用普通摄像头采集课堂视频,通过人脸检测、头部姿态估计、视线方向判断输出专注度分数,再叠加手部动作与双人姿态的时序分析识别疑似作弊行为。这套方案适合有 Python 基础、想做一个能跑起来的深度学习实战项目的人,也适合需要课堂行为分析原型的教育技术从业者。它不追求论文级精度,追求的是从零搭出一条可复现、可调参、可扩展的工程链路。
2. 系统拆解:专注度与作弊识别各自需要什么模型
2.1 专注度监测的技术路线选型
专注度不是一个可以直接标注的标签,它是一组行为特征的加权结果。常见做法是拆成三个子信号:头部姿态、眼睛开合度、视线方向。头部姿态用 6D 旋转表示,偏航角(yaw)超过阈值通常意味着看向别处;眼睛开合度用 EAR(Eye Aspect Ratio)衡量,持续偏低可能是闭眼或低头;视线方向则通过虹膜中心相对眼眶的位置估算。
模型选型上有两条路。一条是端到端回归:输入人脸区域,直接输出专注度分数。这条路需要大量标注数据,且标注主观性强,不同标注者对同一画面的打分可能差 20 分以上。另一条是特征融合:先用轻量模型提取头部姿态和眼部特征,再用规则或小型分类器融合成专注度。我一般推荐第二条路,原因是可解释、可调参、冷启动快,不需要一开始就攒几万条标注。
特征提取用 MediaPipe Face Mesh 是性价比最高的选择。它输出 468 个面部关键点,在 CPU 上单帧推理约 5 到 8 毫秒,足够支撑 30 帧的课堂视频流。头部姿态通过 solvePnP 从关键点反解旋转向量,眼睛开合度直接由关键点坐标算欧氏距离比值。
import cv2 import mediapipe as mp import numpy as np mp_face_mesh = mp.solutions.face_mesh face_mesh = mp_face_mesh.FaceMesh( max_num_faces=5, # 一帧最多检测5张脸,对应小组场景 refine_landmarks=True, # 开启虹膜关键点,用于视线估计 min_detection_confidence=0.5, min_tracking_confidence=0.5 ) # 左眼上下眼睑关键点索引(MediaPipe Face Mesh 标准索引) LEFT_EYE_TOP = 159 LEFT_EYE_BOTTOM = 145 LEFT_EYE_LEFT = 33 LEFT_EYE_RIGHT = 133 def eye_aspect_ratio(landmarks, w, h): top = np.array([landmarks[LEFT_EYE_TOP].x * w, landmarks[LEFT_EYE_TOP].y * h]) bottom = np.array([landmarks[LEFT_EYE_BOTTOM].x * w, landmarks[LEFT_EYE_BOTTOM].y * h]) left = np.array([landmarks[LEFT_EYE_LEFT].x * w, landmarks[LEFT_EYE_LEFT].y * h]) right = np.array([landmarks[LEFT_EYE_RIGHT].x * w, landmarks[LEFT_EYE_RIGHT].y * h]) vertical = np.linalg.norm(top - bottom) horizontal = np.linalg.norm(left - right) return vertical / (horizontal + 1e-6) # 加极小值防止除零 cap = cv2.VideoCapture(0) while cap.isOpened(): ret, frame = cap.read() if not ret: break h, w = frame.shape[:2] rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results = face_mesh.process(rgb) if results.multi_face_landmarks: for face_landmarks in results.multi_face_landmarks: ear = eye_aspect_ratio(face_landmarks.landmark, w, h) # EAR 低于 0.2 通常判定为闭眼或低头 status = "focused" if ear > 0.2 else "distracted" cv2.putText(frame, f"EAR:{ear:.2f} {status}", (30, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) cv2.imshow("Focus Monitor", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()这段代码的关键参数有三个。max_num_faces决定单帧处理人数,设太大会拖慢推理,课堂全景建议不超过 10。refine_landmarks开启后才有虹膜点,做视线估计必须开,但会多约 2 毫秒开销。EAR 阈值 0.2 是经验值,戴眼镜的学生可能整体偏低,实际部署时建议按班级前 30 秒的均值做自适应校准,而不是硬编码。
2.2 作弊行为识别的时序建模思路
作弊行为和走神不同,它是时序事件,单帧画面很难判断。两个学生同时低头看桌下,单帧看只是低头,连续 3 秒保持同步才可疑。所以作弊识别模块的输入不是单帧,而是一段滑动窗口内的姿态序列。
常见做法是双流结构:空间流用姿态关键点判断手部是否进入桌面以下区域,时间流用 LSTM 或 1D 卷积判断动作的同步性和持续性。手部检测可以用 MediaPipe Hands,它输出 21 个手部关键点,判断手腕关键点的 y 坐标是否低于桌面参考线即可触发「手部异常」信号。
import torch import torch.nn as nn class CheatLSTM(nn.Module): def __init__(self, input_dim=42, hidden_dim=64, num_layers=2, num_classes=3): super().__init__() # input_dim=42: 两只手各21个关键点的(x,y)展平 self.lstm = nn.LSTM(input_dim, hidden_dim, num_layers, batch_first=True, dropout=0.3) self.fc = nn.Sequential( nn.Linear(hidden_dim, 32), nn.ReLU(), nn.Dropout(0.3), nn.Linear(32, num_classes) # 正常 / 疑似低头 / 疑似传递物品 ) def forward(self, x): # x shape: (batch, seq_len, 42) out, _ = self.lstm(x) out = out[:, -1, :] # 取最后一个时间步 return self.fc(out) # 滑动窗口构造:每30帧为一个样本,步长10帧 def build_sequences(hand_keypoints, window=30, stride=10): seqs = [] for i in range(0, len(hand_keypoints) - window, stride): seqs.append(hand_keypoints[i:i+window]) return torch.tensor(seqs, dtype=torch.float32)input_dim=42对应双手 21 个关键点的 x、y 坐标。hidden_dim=64是隐藏层维度,课堂场景数据量不大,64 足够,设到 256 反而容易过拟合。num_classes=3是行为类别数,实际项目里可以按需求扩展成「正常、低头、侧身、传递物品」四类。滑动窗口 30 帧对应约 1 秒视频,步长 10 帧保证相邻样本有重叠,避免事件被切碎。
训练时有个血泪经验:作弊样本天然稀少,正常行为占 90% 以上,直接训练模型会退化成「全部预测正常」。必须做类别加权或过采样,CrossEntropyLoss的weight参数按类别频率倒数设置,通常能把作弊类的召回率从 30% 拉到 70% 以上。
3. 从零跑通:环境搭建与最小可运行系统
3.1 Python 环境与依赖安装的确定性步骤
深度学习环境最怕版本冲突。我一般用 conda 建独立环境,Python 版本锁 3.8 到 3.10 之间,太新的版本某些包轮子还没跟上。安装顺序也有讲究:先装 PyTorch,再装 OpenCV 和 MediaPipe,最后装辅助库,避免 numpy 版本被反复覆盖。
conda create -n classroom_monitor python=3.9 -y conda activate classroom_monitor # 安装 PyTorch,CPU 版本足够跑通原型,有 GPU 换对应 CUDA 版本 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu # 安装视觉与关键点检测库 pip install opencv-python==4.8.1.78 pip install mediapipe==0.10.9 # 辅助库 pip install numpy pandas matplotlib scikit-learn tqdm安装完必须验证三个点:import cv2不报错、import mediapipe不报错、torch.cuda.is_available()返回预期值。如果 cv2 导入报libGL相关错误,在 Linux 上装libgl1-mesa-glx即可,这是新手最容易卡住的地方。
提示:不要用
pip install opencv-python不带版本号,最新版有时和 mediapipe 依赖的 numpy 版本打架,锁版本能省掉大量排查时间。
3.2 摄像头采集与专注度分数实时输出
最小可运行系统只需要一个脚本:读摄像头、检测人脸、算 EAR 和头部姿态、输出专注度分数、画在画面上。专注度分数用加权公式:score = 0.5 * ear_score + 0.3 * head_score + 0.2 * gaze_score,权重可以按实际场景调。
import cv2 import mediapipe as mp import numpy as np mp_face_mesh = mp.solutions.face_mesh face_mesh = mp_face_mesh.FaceMesh(max_num_faces=5, refine_landmarks=True) # 头部姿态参考点(3D模型点,单位任意,比例正确即可) MODEL_POINTS = np.array([ (0.0, 0.0, 0.0), # 鼻尖 (0.0, -63.6, -12.5), # 下巴 (-43.3, 32.7, -26.0), # 左眼左角 (43.3, 32.7, -26.0), # 右眼右角 ], dtype=np.float64) def get_head_pose(landmarks, w, h): image_points = np.array([ (landmarks[1].x * w, landmarks[1].y * h), # 鼻尖 (landmarks[152].x * w, landmarks[152].y * h), # 下巴 (landmarks[33].x * w, landmarks[33].y * h), # 左眼左角 (landmarks[263].x * w, landmarks[263].y * h), # 右眼右角 ], dtype=np.float64) focal_length = w center = (w / 2, h / 2) camera_matrix = np.array([ [focal_length, 0, center[0]], [0, focal_length, center[1]], [0, 0, 1] ], dtype=np.float64) dist_coeffs = np.zeros((4, 1)) success, rotation_vec, _ = cv2.solvePnP( MODEL_POINTS, image_points, camera_matrix, dist_coeffs, flags=cv2.SOLVEPNP_ITERATIVE ) if not success: return None rotation_mat, _ = cv2.Rodrigues(rotation_vec) # 提取偏航角,判断是否转头 sy = np.sqrt(rotation_mat[0, 0]**2 + rotation_mat[1, 0]**2) yaw = np.arctan2(-rotation_mat[2, 0], sy) * 180 / np.pi return yaw cap = cv2.VideoCapture(0) while cap.isOpened(): ret, frame = cap.read() if not ret: break h, w = frame.shape[:2] rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results = face_mesh.process(rgb) if results.multi_face_landmarks: for face in results.multi_face_landmarks: yaw = get_head_pose(face.landmark, w, h) if yaw is not None: # 偏航角绝对值小于25度视为面向黑板 head_score = 1.0 if abs(yaw) < 25 else max(0, 1 - (abs(yaw) - 25) / 45) cv2.putText(frame, f"Yaw:{yaw:.1f} HeadScore:{head_score:.2f}", (30, 80), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 0, 0), 2) cv2.imshow("Classroom Monitor", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()solvePnP的flags用SOLVEPNP_ITERATIVE在只有 4 个点的情况下最稳定。偏航角阈值 25 度是课堂场景的经验值,超过 25 度基本可以判定为转头看别处。head_score的衰减公式让分数平滑下降而不是硬切,避免学生轻微转头就触发告警。
3.3 作弊行为数据标注与模型训练流程
作弊识别模型需要标注数据。没有公开的课堂作弊数据集,只能自己采。采集时注意三点:多角度、多光照、多人数。每个疑似作弊片段截取 30 帧,标注类别。标注工具用 LabelImg 或自己写个简单的帧选择脚本都行。
训练流程分四步:加载标注、构造滑动窗口序列、划分训练验证集、训练 LSTM。验证集必须按视频片段划分而不是按帧随机划分,否则同一片段的相邻帧会同时出现在训练和验证集里,准确率虚高。
from torch.utils.data import Dataset, DataLoader import numpy as np class CheatDataset(Dataset): def __init__(self, sequences, labels): self.sequences = sequences self.labels = labels def __len__(self): return len(self.sequences) def __getitem__(self, idx): return self.sequences[idx], self.labels[idx] # 假设 hand_data 是 (N, 42) 的关键点序列,labels 是帧级标签 # 按视频片段划分,避免数据泄漏 def split_by_clip(clip_ids, test_ratio=0.2): unique_clips = np.unique(clip_ids) np.random.shuffle(unique_clips) split = int(len(unique_clips) * (1 - test_ratio)) train_clips = set(unique_clips[:split]) train_idx = [i for i, c in enumerate(clip_ids) if c in train_clips] val_idx = [i for i, c in enumerate(clip_ids) if c not in train_clips] return train_idx, val_idx # 类别权重:作弊类样本少,给更高权重 class_weights = torch.tensor([1.0, 3.0, 5.0]) criterion = nn.CrossEntropyLoss(weight=class_weights) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4)weight_decay=1e-4是 L2 正则化系数,防止 LSTM 过拟合小数据集。类别权重[1.0, 3.0, 5.0]按正常、低头、传递物品的频率倒数粗略设置,实际项目里用sklearn.utils.class_weight.compute_class_weight算更准。训练 50 个 epoch 左右,观察验证集 F1 而不是准确率,作弊类 F1 能到 0.6 以上就说明模型学到了东西。
4. 避坑与排查:课堂场景里最容易翻车的五个点
4.1 光照突变导致关键点抖动
现象:教室开灯或拉窗帘瞬间,MediaPipe 输出的关键点坐标跳变,专注度分数从 0.8 骤降到 0.2,误报一片。
原因:Face Mesh 基于图像纹理做回归,光照突变改变了纹理分布,模型输出不稳定。
解决:对关键点做滑动平均滤波,窗口取 5 帧。同时对输入帧做直方图均衡化,减少光照影响。如果教室光照条件差,考虑加一个补光灯,硬件解决比算法补救便宜。
4.2 多人场景下人脸 ID 跳变
现象:同一学生在相邻两帧被分配了不同的人脸索引,专注度分数在两个人之间来回跳。
原因:MediaPipe 的multi_face_landmarks不保证跨帧 ID 一致,它只做单帧检测。
解决:自己维护一个简单的 IOU 跟踪器,用上一帧的人脸框和当前帧做匹配,IOU 大于 0.5 视为同一人。或者引入轻量跟踪算法如 ByteTrack,但会增加约 10 毫秒延迟。课堂场景人数固定,IOU 跟踪足够。
4.3 EAR 阈值对戴眼镜学生失效
现象:戴眼镜的学生 EAR 常年低于 0.2,系统一直判定为闭眼,专注度分数极低。
原因:镜片反光和镜框遮挡导致眼睑关键点定位偏移。
解决:部署时先跑 30 秒校准,记录每个学生正常注视时的 EAR 均值,阈值设为均值的 70%。同时开启refine_landmarks提高眼部关键点精度。如果反光严重,建议学生调整座位角度或摘掉反光镜片。
4.4 作弊模型把「捡笔」误判为「传递物品」
现象:学生弯腰捡掉在地上的笔,手部关键点进入桌面以下区域,模型输出「疑似传递物品」告警。
原因:模型只学了手部位置,没学动作持续时间和身体姿态配合。
解决:在 LSTM 输入里加入身体姿态关键点(肩膀、肘部),捡笔时肩膀会明显下沉,传递物品时肩膀相对稳定。同时要求异常信号持续超过 1.5 秒才触发告警,单帧或短时信号忽略。
4.5 视频流延迟累积导致告警滞后
现象:系统运行 10 分钟后,画面和告警延迟越来越大,最后卡死。
原因:cv2.VideoCapture的缓冲区累积,读取速度跟不上处理速度。
解决:设置cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)限制缓冲区为 1 帧。同时把推理放在独立线程,主线程只负责读帧和显示,用队列传递数据。如果还卡,降低输入分辨率到 640x480,课堂监测不需要 1080p。
5. 进阶技巧:用姿态序列做作弊行为的可解释告警
专注度分数是个连续值,作弊告警是个离散事件,两者输出形式不同。进阶做法是把作弊识别从分类问题转成异常检测问题:正常行为建模成高斯分布,作弊行为是分布外的低概率事件。这样不需要大量作弊标注,只需要正常行为数据就能训练。
具体做法是用自编码器(Autoencoder)重建手部姿态序列,正常序列重建误差小,作弊序列重建误差大。阈值设为正常验证集重建误差的 95 分位数。这个方法的优势是可解释:告警时能输出哪几帧重建误差超标,对应视频里哪段动作异常,老师可以直接回看确认。
class PoseAutoencoder(nn.Module): def __init__(self, input_dim=42, latent_dim=16): super().__init__() self.encoder = nn.Sequential( nn.Linear(input_dim, 32), nn.ReLU(), nn.Linear(32, latent_dim), nn.ReLU() ) self.decoder = nn.Sequential( nn.Linear(latent_dim, 32), nn.ReLU(), nn.Linear(32, input_dim) ) def forward(self, x): z = self.encoder(x) return self.decoder(z) # 训练只用正常行为数据 model = PoseAutoencoder() criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) # 推理时计算重建误差 with torch.no_grad(): recon = model(normal_sequence) error = ((recon - normal_sequence) ** 2).mean(dim=1) threshold = torch.quantile(error, 0.95) is_anomaly = error > thresholdlatent_dim=16是压缩维度,太小重建误差整体偏高,太大则失去异常检测能力,16 到 32 之间比较合适。阈值用 95 分位数而不是固定值,能适应不同教室的基线差异。
验证方法上,我习惯用「留出片段」做最终测试:训练时完全没见过某个学生的数据,测试时看模型对这个学生的告警是否合理。如果模型只对训练过的学生有效,说明过拟合了,需要增加数据多样性。
这套系统我前后调了三个月,最大的教训是:不要追求端到端一步到位,先把专注度分数跑稳,再加作弊识别,最后做告警联动。每加一个模块就回归测试一遍,否则出了问题根本不知道是哪层翻的车。希望帮到你。
本文还有配套的精品资源,点击获取