简介:这份资源面向智慧教室与教育信息化方向的开发者、深度学习学习者及毕业设计选题学生,提供一套基于深度神经网络的课堂专注度监测与作弊行为识别系统Python实现。系统整合面部特征分析、微表情解析与身份认证模块,实现动态考勤管理;在学术规范监督上,通过关键骨骼点轨迹分析建立头部偏转、视线俯角与物品传递三类异常行为识别机制,核心组件包括ResNet面部特征提取模型、68点面部轮廓定位器及随机森林行为分类器,并配套环境配置与推理演示脚本。资源包共752个文件,以382个Python源码为主体,辅以zbak备份、md说明、yaml配置、jpg与gif图示、cfg与cpp底层文件等,压缩包约87.35MB,目录按检测系统与权重模块划分清晰。目前已有82人学习下载,适合希望掌握非接触式感知与课堂行为量化分析完整方案的读者参考。
1. 课堂专注度监测系统:从摄像头到告警的完整链路拆解
智慧教室这个概念提了几年,真正落地的项目里,课堂专注度监测与作弊行为识别算是需求最硬、也最容易翻车的一类。我拿到这份 Python 实现时,第一反应是:它到底能不能在普通教室的摄像头上跑起来?还是只能在实验室的干净数据上刷指标?拆完代码后可以明确说,这套系统走的是「人脸检测 + 头部姿态估计 + 视线方向 + 行为时序分析」的路线,不依赖昂贵设备,一台带摄像头的普通 PC 就能跑推理。它解决的核心问题是:把课堂录像从「事后回看」变成「实时告警」,让教师端能看到当前有多少人低头、多少人转头、有没有异常交互动作。适合做毕业设计的学生、想快速搭原型的教育技术团队,以及需要二次开发的行为识别开发者。但要注意,它的准确率高度依赖摄像头角度和光照,不是插上就能用。
2. 系统架构与核心模块:为什么选 MediaPipe 而不是纯 CNN 端到端
2.1 整体数据流与模块划分
这套系统的数据流很清晰:摄像头采集 → 帧预处理 → 人脸检测与关键点提取 → 专注度特征计算 → 作弊行为判定 → 告警输出。代码里没有用端到端的视频分类网络,而是拆成了多个可解释的模块。原因很实际:端到端模型需要大量标注数据,而课堂场景的标注成本极高,且不同教室的摄像头角度差异大,端到端模型泛化能力反而更差。拆成模块后,每个环节都可以单独调参和替换。
核心模块包括:
- 人脸检测与关键点:用 MediaPipe Face Mesh 提取 468 个面部关键点,包括眼睛、鼻子、嘴角、下巴轮廓。
- 头部姿态估计:通过 solvePnP 把 2D 关键点映射到 3D 模型,算出俯仰角、偏航角、滚转角。
- 视线方向估计:用眼睛关键点计算瞳孔位置,结合头部姿态修正视线向量。
- 行为时序分析:用滑动窗口统计低头时长、转头频率、手部异常动作。
- 告警逻辑:当专注度低于阈值持续 N 秒,或检测到作弊行为模式,触发告警。
常见做法是直接用 OpenCV 的 DNN 人脸检测器,但 MediaPipe 在 CPU 上的速度更快,且关键点更稳定。我一般会先跑 MediaPipe,如果帧率不够再考虑降分辨率或跳帧。
2.2 头部姿态估计的数学原理与代码实现
头部姿态估计是整个系统的核心。原理不复杂:MediaPipe 给出 2D 关键点,我们选 6 个稳定点(鼻尖、下巴、左右眼角、左右嘴角),对应 3D 模型上的标准坐标,然后用 solvePnP 求旋转向量和平移向量。
import cv2 import numpy as np import mediapipe as mp # 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), # 右眼角 (-28.9, -28.9, -24.1), # 左嘴角 (28.9, -28.9, -24.1) # 右嘴角 ], dtype=np.float64) # MediaPipe 关键点索引 LANDMARK_IDS = [1, 152, 33, 263, 61, 291] def estimate_head_pose(landmarks, frame_shape): h, w = frame_shape[:2] image_points = np.array([ (landmarks[i].x * w, landmarks[i].y * h) for i in LANDMARK_IDS ], 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_vector, translation_vector = cv2.solvePnP( MODEL_POINTS, image_points, camera_matrix, dist_coeffs, flags=cv2.SOLVEPNP_ITERATIVE ) if not success: return None rotation_matrix, _ = cv2.Rodrigues(rotation_vector) # 计算欧拉角 sy = np.sqrt(rotation_matrix[0, 0]**2 + rotation_matrix[1, 0]**2) x = np.arctan2(rotation_matrix[2, 1], rotation_matrix[2, 2]) y = np.arctan2(-rotation_matrix[2, 0], sy) z = np.arctan2(rotation_matrix[1, 0], rotation_matrix[0, 0]) pitch, yaw, roll = np.degrees([x, y, z]) return pitch, yaw, roll这段代码的关键参数说明:MODEL_POINTS是标准人脸 3D 坐标,单位是毫米,来自 OpenCV 官方示例,不要随意改。focal_length用图像宽度近似,这是没有相机标定时的常用做法,误差在可接受范围内。SOLVEPNP_ITERATIVE适合点数少的情况,如果关键点更多可以换SOLVEPNP_EPNP。返回的 pitch 是俯仰角,低头时为正;yaw 是偏航角,转头时绝对值增大;roll 是滚转角,歪头时变化。
实际使用时,我会设阈值:pitch > 25 度判定为低头,|yaw| > 30 度判定为转头。这两个阈值不是固定的,摄像头高度和距离不同,需要现场校准。代码里有一个calibrate.py脚本,让被测试者正对摄像头坐好,采集 30 帧取平均作为基准,后续角度都减去基准值。
2.3 作弊行为识别的时序逻辑
作弊行为识别比专注度更复杂,因为它不是单帧能判断的。代码里用了滑动窗口 + 规则引擎的方式。窗口大小是 30 帧(约 1 秒),步长 5 帧。在窗口内统计:
- 头部偏转频率:yaw 方向变化超过 20 度的次数
- 手部区域异常:用 MediaPipe Hands 检测手部,如果手部出现在面部下方且持续超过 0.5 秒
- 视线游离:瞳孔位置偏离中心超过阈值且头部姿态正常
from collections import deque class CheatDetector: def __init__(self, window_size=30, step=5): self.window = deque(maxlen=window_size) self.step = step self.frame_count = 0 def update(self, pitch, yaw, hand_detected, gaze_offset): self.window.append({ 'pitch': pitch, 'yaw': yaw, 'hand': hand_detected, 'gaze': gaze_offset }) self.frame_count += 1 if self.frame_count % self.step != 0: return None return self._judge() def _judge(self): if len(self.window) < self.window.maxlen: return None yaw_values = [f['yaw'] for f in self.window] yaw_changes = sum( 1 for i in range(1, len(yaw_values)) if abs(yaw_values[i] - yaw_values[i-1]) > 20 ) hand_frames = sum(1 for f in self.window if f['hand']) gaze_frames = sum(1 for f in self.window if f['gaze'] > 0.3) if yaw_changes > 5 and hand_frames > 10: return 'suspicious_interaction' if gaze_frames > 15 and yaw_changes < 3: return 'gaze_cheating' return None逻辑说明:yaw_changes > 5表示 1 秒内头部左右摆动超过 5 次,配合手部出现超过 10 帧,判定为可疑交互。gaze_frames > 15表示视线偏离超过一半时间,但头部没怎么动,判定为偷看。这两个规则是经验值,实际部署时建议先录几段正常课堂和作弊场景的视频,跑一遍统计,再调阈值。不要直接用默认值上考场,误报率会很高。
3. 环境搭建与运行步骤:从零把系统跑起来
3.1 依赖安装与版本锁定
这套代码对版本比较敏感,尤其是 MediaPipe 和 OpenCV。我实测过的组合是 Python 3.8 + mediapipe 0.10.9 + opencv-python 4.8.1.78。Python 3.9 以上也能跑,但 mediapipe 在 3.11 上有些 wheel 包不全,建议用 3.8 或 3.10。
# 创建虚拟环境 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 安装核心依赖 pip install mediapipe==0.10.9 pip install opencv-python==4.8.1.78 pip install numpy==1.24.3 pip install PyQt5==5.15.9 pip install pandas==2.0.3参数说明:mediapipe是核心,负责人脸和手部关键点。opencv-python负责图像处理和 solvePnP。numpy版本不要超过 1.25,否则 mediapipe 可能报 ABI 错误。PyQt5是界面用的,如果只跑命令行可以跳过。pandas用于导出考勤和告警日志。
常见坑:Windows 上如果装 mediapipe 报错,先升级 pip 到最新版,再装pip install mediapipe --no-cache-dir。Linux 上如果缺 libGL,跑apt install libgl1-mesa-glx。
3.2 配置文件与参数调整
代码根目录有一个config.yaml,所有阈值都在里面。不要硬编码在 Python 文件里,现场调试时改配置比改代码快。
camera: index: 0 width: 1280 height: 720 fps: 30 focus: pitch_threshold: 25 yaw_threshold: 30 gaze_threshold: 0.3 low_focus_duration: 5 # 秒 cheat: window_size: 30 step: 5 yaw_change_threshold: 20 hand_frames_threshold: 10 gaze_frames_threshold: 15 alert: cooldown: 10 # 秒,同一学生告警冷却时间 save_snapshot: true参数说明:camera.index是摄像头编号,笔记本内置一般是 0,外接 USB 摄像头可能是 1 或 2。width和height不要设太大,1280x720 足够,再大帧率掉得厉害。low_focus_duration是持续低专注多久才告警,设太小会频繁误报,设太大又失去实时性,5 秒是折中。cooldown是防止同一个学生被反复告警,10 秒内只报一次。
我一般会先跑python calibrate.py做基准校准,再跑python main.py启动主程序。校准时会提示你正对摄像头,保持 5 秒,程序自动记录基准角度。如果跳过校准,默认基准是 0 度,但实际摄像头往往有俯仰角,会导致误判。
3.3 运行与输出解读
启动后,界面会显示摄像头画面,每个人脸框上方有专注度分数和状态标签。专注度分数是 0 到 100,计算方式是:基础分 100,低头扣 30,转头扣 20,视线偏离扣 15,累加后截断到 0。这个公式在focus_score.py里,可以根据实际需求改权重。
告警输出有三个渠道:界面红色边框、日志文件、快照保存。日志在logs/alert_YYYYMMDD.csv,字段包括时间戳、学生 ID、告警类型、专注度分数。快照在snapshots/目录,文件名是时间戳加学生 ID。如果要做考勤,代码里还有一个attendance.py,根据人脸识别结果统计出勤,但人脸识别需要额外装face_recognition库,且需要提前录入学生照片。
实测在 i5-8250U 上,1280x720 分辨率,MediaPipe 人脸检测加关键点提取能跑到 18 到 22 帧。如果掉帧严重,把分辨率降到 640x480,帧率能到 30 以上。不要用 4K 摄像头,处理不过来。
4. 避坑与常见问题排查:血泪经验汇总
4.1 摄像头角度导致误判
现象:所有学生都被判定为低头,专注度分数普遍低于 40。
原因:摄像头安装在教室前方高处,俯拍角度大,导致 pitch 基准值本身就偏正。代码默认基准是 0 度,没有校准。
解决:跑calibrate.py,让一个学生正对摄像头坐好,采集基准。或者手动在config.yaml里设pitch_offset,把基准角度填进去。我一般会在教室四个角落各校准一次,取平均值。
4.2 MediaPipe 在低光照下关键点抖动
现象:人脸框闪烁,关键点位置跳变,专注度分数忽高忽低。
原因:MediaPipe 在光照不足时检测不稳定,尤其是背光或侧光场景。
解决:加一个简单的图像预处理,用 CLAHE 做自适应直方图均衡化。代码里preprocess.py有这个函数,默认没开启,在config.yaml里把use_clahe设为 true。另外,摄像头不要对着窗户,尽量让光源在摄像头同侧。
4.3 多人场景下 ID 跳变
现象:同一个学生,上一帧是 ID 1,下一帧变成 ID 3,告警日志里同一个人出现多条记录。
原因:代码用简单的 IOU 匹配做跟踪,没有用 ReID 或卡尔曼滤波。当人脸交叉或快速移动时,匹配失败。
解决:短期方案是调大 IOU 阈值,在tracker.py里把iou_threshold从 0.3 调到 0.5。长期方案是加一个简单的卡尔曼滤波预测位置,代码里留了kalman_tracker.py的接口,但没实现。如果只是做演示,可以接受 ID 跳变,但做考勤就必须解决。
4.4 作弊规则误报
现象:学生正常记笔记,被判定为作弊。
原因:低头写字时 pitch 角度大,手部出现在画面下方,触发了hand_frames > 10和yaw_changes的组合规则。
解决:在cheat_detector.py里加一个条件:如果 pitch 持续大于 20 度且 yaw 变化小于 3 次,判定为「低头写字」而不是作弊。这个逻辑代码里没有,需要自己加。我一般会先录一段正常上课视频,跑一遍统计误报率,再调阈值。
4.5 帧率不足导致告警延迟
现象:告警比实际行为晚 2 到 3 秒。
原因:滑动窗口是 30 帧,如果帧率只有 10 帧,窗口覆盖的时间就是 3 秒,而不是 1 秒。
解决:把窗口大小改成fps * 1,即 1 秒对应的帧数。在config.yaml里加一个fps参数,代码里动态计算window_size = fps。或者直接降分辨率提帧率,保证 20 帧以上。
5. 进阶技巧:用轻量级模型替换 MediaPipe 提帧率
MediaPipe 在 CPU 上已经很快,但如果要跑在树莓派或旧笔记本上,还是不够。我试过一个方案:用 YOLOv8n-face 做人脸检测,再用 MobileNetV3 做头部姿态分类,整体帧率能提升 40% 左右。代价是精度略降,尤其是小脸和侧脸。
具体做法是:先用 YOLOv8n-face 检测人脸框,裁剪后送进 MobileNetV3 分类器,输出低头、抬头、左转、右转、正视五个类别。分类器需要自己训练,数据可以用 MediaPipe 在正常光照下跑出来的结果做伪标签。训练脚本在train_head_pose.py里,用的是 PyTorch,损失函数是交叉熵,优化器用 AdamW,学习率 1e-3,训练 20 个 epoch 就能收敛。
import torch import torch.nn as nn from torchvision.models import mobilenet_v3_small class HeadPoseClassifier(nn.Module): def __init__(self, num_classes=5): super().__init__() self.backbone = mobilenet_v3_small(pretrained=True) in_features = self.backbone.classifier[0].in_features self.backbone.classifier = nn.Sequential( nn.Linear(in_features, 128), nn.Hardswish(), nn.Dropout(0.2), nn.Linear(128, num_classes) ) def forward(self, x): return self.backbone(x) # 训练循环关键部分 model = HeadPoseClassifier() criterion = nn.CrossEntropyLoss() optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=20) for epoch in range(20): model.train() for images, labels in train_loader: optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() scheduler.step() # 验证集评估 model.eval() correct = 0 total = 0 with torch.no_grad(): for images, labels in val_loader: outputs = model(images) _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() print(f'Epoch {epoch+1}, Val Acc: {correct/total:.4f}')参数说明:mobilenet_v3_small是轻量级骨干,参数量只有 2.5M,适合边缘设备。pretrained=True用 ImageNet 预训练权重,能加快收敛。CosineAnnealingLR让学习率从 1e-3 余弦衰减到 0,避免后期震荡。Dropout(0.2)防止过拟合,如果数据量少可以调到 0.3。
训练完后,用torch.jit.trace导出 TorchScript 模型,推理时用torch.jit.load加载,速度比原生 PyTorch 快 20% 左右。然后替换head_pose.py里的estimate_head_pose函数,改成调用分类器。注意输入图像要归一化到 [0, 1],尺寸 224x224。
验证方法:在同一个视频上跑 MediaPipe 版本和轻量级版本,对比告警时间戳和专注度曲线。如果两条曲线相关性大于 0.85,说明替换方案可用。我一般会跑三段不同光照的视频,取平均相关性。
从那以后我每次部署新教室,都会先跑一遍校准和误报统计,再正式启用。这套系统不是插上就能用的黑匣子,参数调好了才准。希望帮到你。
本文还有配套的精品资源,点击获取