资讯详情

智慧教室行为识别:专注度分析与作弊检测的深度学习实战

📅 2026/10/11 17:13:34 | 华诺云谱 👁 阅读
智慧教室行为识别:专注度分析与作弊检测的深度学习实战
简介这份资源是面向计算机相关专业学生与项目实战学习者的毕业设计源码包聚焦智慧教室场景下的课堂专注度分析与考试作弊检测基于深度学习技术实现适合用作毕设、课程设计或期末大作业的参考方案。压缩包共626个文件约87.73MB以383个Python源码为主体配合yaml配置、md说明文档、cfg与prototxt网络定义、cu与cpp底层算子、jpg与png示意图及sh运行脚本等覆盖模型训练、推理部署与数据处理的完整链路。目前已有346人学习下载说明该方案在同类毕设选题中具备一定参考价值。读者可从中获取经导师指导并认可的高分项目结构理解专注度识别与作弊行为检测的模型组织方式借鉴配置文件与脚本的排错思路并借助说明文档快速复现运行环境为自身课题的选题、实现与答辩提供可落地的实践参照。1. 智慧教室里的两套模型专注度分析和作弊检测到底在做什么智慧教室这个场景说白了就是把普通教室装上摄像头让机器替老师看课堂。但真正做过的人都知道摄像头拍到的画面本身没有价值有价值的是从画面里提取出的两类判断一是学生此刻专注不专注二是有没有人在作弊。这两个任务看起来都是看人但技术路线差别很大——专注度分析本质是回归或分类问题输入是连续视频帧输出是一个专注度分数或状态标签作弊检测则更接近行为识别需要捕捉手部动作、头部朝向、视线方向这些细粒度特征还要处理时序上的异常。我带过几届毕设发现大部分同学卡在第一步不知道这套系统到底由哪些模块组成更不知道每个模块的输入输出长什么样。常见的做法是拆成四条流水线——人脸检测与对齐、头部姿态估计、眼部/嘴部状态分析、手部与桌面区域行为识别。前三条服务于专注度第四条主要服务于作弊检测。两条线共用同一个视频解码和帧采样模块但后面的特征提取和决策逻辑是分开的。这套方案适合谁如果你正在做基于深度学习的大学生课堂状态检测类毕设或者想用 Python 把深度学习模型部署到真实教室场景里那接下来的内容就是按这个思路展开的。我一般会建议先用公开数据集把单帧分类跑通再考虑时序建模最后才碰多目标跟踪。顺序反了调试成本会翻倍。2. 从视频帧到专注度分数人脸、姿态、眼部特征的提取链路2.1 为什么先做人脸检测而不是直接上分类网络很多同学一上来就想用 CNN 端到端出专注度分数结果训练 loss 降不下去。原因很简单教室画面里人脸只占很小一块区域背景桌椅、黑板、窗户占了大半直接送整帧进网络模型大部分算力都浪费在无关像素上。常见做法是先用人脸检测器把每个人脸抠出来再送进后续网络。我一般用 RetinaFace 或 YOLOv8-face 做检测前者对小脸更友好后者速度快适合实时。检测完做 5 点对齐把眼睛和嘴角位置摆正后续眼部状态判断会稳很多。这里有个参数要注意检测置信度阈值设 0.5 起步教室后排人脸小可以降到 0.35但误检会增多需要配合跟踪做后处理。import cv2 import numpy as np from retinaface import RetinaFace def detect_and_align(frame, conf_thresh0.5): # RetinaFace 返回 dictkey 是人脸编号 faces RetinaFace.detect_faces(frame, thresholdconf_thresh) results [] if isinstance(faces, dict): for key, face in faces.items(): x1, y1, x2, y2 face[facial_area] landmarks face[landmarks] # 5 点左右眼、鼻、左右嘴角 # 按关键点做仿射对齐到 112x112 aligned align_face(frame, landmarks) results.append({ bbox: [x1, y1, x2, y2], aligned: aligned, landmarks: landmarks }) return results def align_face(frame, landmarks): # 标准 5 点模板ArcFace 常用 dst np.array([[38.29, 51.69], [73.53, 51.50], [56.02, 71.73], [41.55, 92.36], [70.73, 92.20]], dtypenp.float32) src np.array([landmarks[left_eye], landmarks[right_eye], landmarks[nose], landmarks[mouth_left], landmarks[mouth_right]], dtypenp.float32) tform cv2.estimateAffinePartial2D(src, dst)[0] return cv2.warpAffine(frame, tform, (112, 112))这段代码的逻辑是检测到人脸后用 5 个关键点算仿射变换矩阵把脸扭正到统一尺寸。参数conf_thresh控制检测灵敏度dst是固定模板不要改。对齐后的 112x112 图像才是后续网络的输入。2.2 头部姿态估计用 solvePnP 还是直接回归头部姿态是专注度里权重很高的特征——低头看手机、扭头说话、趴桌子这些动作靠头部俯仰角和偏航角就能区分。两条路线一是用 solvePnP 配合 3D 人脸模型算欧拉角二是用轻量网络直接回归角度。前者不需要训练但对关键点精度敏感后者需要标注数据但鲁棒性更好。我一般先用 solvePnP 快速搭出基线因为不用训练就能跑。OpenCV 的solvePnP需要 2D 关键点和对应的 3D 模型点。3D 点可以用标准人脸模型2D 点就是前面检测到的 5 点或 68 点。算出来的旋转向量转成欧拉角俯仰角大于 25 度基本可以判定为低头。import cv2 import numpy as np # 标准 3D 人脸模型点对应 5 个关键点 MODEL_POINTS np.array([ [0.0, 0.0, 0.0], # 鼻尖 [-30.0, 30.0, -30.0], # 左眼 [30.0, 30.0, -30.0], # 右眼 [-25.0, -30.0, -30.0], # 左嘴角 [25.0, -30.0, -30.0] # 右嘴角 ], dtypenp.float64) def estimate_head_pose(landmarks, frame_size): h, w frame_size[:2] focal w # 近似焦距 center (w / 2, h / 2) camera_matrix np.array([[focal, 0, center[0]], [0, focal, center[1]], [0, 0, 1]], dtypenp.float64) dist_coeffs np.zeros((4, 1)) image_points np.array([ landmarks[nose], landmarks[left_eye], landmarks[right_eye], landmarks[mouth_left], landmarks[mouth_right] ], dtypenp.float64) success, rvec, tvec cv2.solvePnP(MODEL_POINTS, image_points, camera_matrix, dist_coeffs, flagscv2.SOLVEPNP_ITERATIVE) if not success: return None rmat, _ cv2.Rodrigues(rvec) # 转欧拉角 sy np.sqrt(rmat[0, 0]**2 rmat[1, 0]**2) pitch np.arctan2(-rmat[2, 0], sy) yaw np.arctan2(rmat[1, 0], rmat[0, 0]) roll np.arctan2(rmat[2, 1], rmat[2, 2]) return np.degrees([pitch, yaw, roll])参数说明focal用图像宽度近似教室摄像头焦距未知时这是常用做法MODEL_POINTS是通用人脸比例换数据集不用改。返回的 pitch 为负表示低头yaw 绝对值大表示扭头。阈值我一般设 pitch -20 度或 |yaw| 30 度算分心。2.3 眼部状态和嘴部状态EAR 与 MAR 的计算细节眼睛闭合时长和打哈欠频率是专注度的负向指标。EAREye Aspect Ratio和 MARMouth Aspect Ratio是两个不用训练就能算的几何特征。EAR 用眼睛 6 个关键点的垂直距离和水平距离比值正常睁眼在 0.25 到 0.35 之间闭眼降到 0.15 以下。MAR 类似嘴巴张开高度比宽度打哈欠时超过 0.6。def eye_aspect_ratio(eye_points): # eye_points: 6 个点顺序为左右角、上两点、下两点 A np.linalg.norm(eye_points[1] - eye_points[5]) B np.linalg.norm(eye_points[2] - eye_points[4]) C np.linalg.norm(eye_points[0] - eye_points[3]) return (A B) / (2.0 * C) def mouth_aspect_ratio(mouth_points): # mouth_points: 8 个点左右角、上下各 3 点 A np.linalg.norm(mouth_points[1] - mouth_points[7]) B np.linalg.norm(mouth_points[2] - mouth_points[6]) C np.linalg.norm(mouth_points[3] - mouth_points[5]) D np.linalg.norm(mouth_points[0] - mouth_points[4]) return (A B C) / (2.0 * D)这两个函数依赖 68 点关键点检测器dlib 或 mediapipe 都行。注意 EAR 对头部姿态敏感侧脸时数值会失真所以要先做姿态判断yaw 超过 30 度时 EAR 不可信直接跳过眼部判断。这是血泪经验很多同学在这一步翻车模型把侧脸全判成闭眼。3. 作弊检测的行为识别分支手部、桌面区域与时序建模3.1 手部检测和桌面区域划分怎么做作弊检测的核心是手在干什么。常见作弊动作包括手伸到桌下、传递纸条、偷看邻座、拿手机。这些动作的共同点是手部离开正常答题区域或出现在异常位置。所以第一步是把手检测出来第二步是定义桌面区域和异常区域。手部检测可以用 MediaPipe Hands轻量且 CPU 就能跑。桌面区域划分有两种做法一是固定区域假设摄像头位置不变手动标定桌面矩形二是用答题纸或试卷检测来自动定位。毕设里我一般建议先用固定区域简单可靠。import mediapipe as mp mp_hands mp.solutions.hands hands mp_hands.Hands(static_image_modeFalse, max_num_hands2, min_detection_confidence0.5, min_tracking_confidence0.5) def detect_hands(frame): rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results hands.process(rgb) hand_landmarks [] if results.multi_hand_landmarks: for hand in results.multi_hand_landmarks: points [(lm.x, lm.y) for lm in hand.landmark] hand_landmarks.append(points) return hand_landmarks def is_hand_below_desk(hand_points, desk_region): # desk_region: (x1, y1, x2, y2) 桌面区域 # 取手腕点landmark 0判断 wrist_y hand_points[0][1] desk_bottom desk_region[3] return wrist_y desk_bottom参数说明max_num_hands2够用min_detection_confidence设 0.5太低会误检。is_hand_below_desk用归一化坐标手腕点 y 超过桌面下边界就判定手在桌下。这个逻辑简单但有效实测能抓住大部分桌下小动作。3.2 时序建模为什么单帧不够LSTM 和 3D CNN 怎么选单帧只能看到手在桌下这个状态但作弊是一个动作过程——手伸下去、停留、拿东西上来。只看单帧会漏掉伸下去又马上上来这种试探行为也会把捡笔误判成作弊。所以需要时序建模。两条主流路线一是用 LSTM/GRU 对每帧特征序列做分类输入是手部关键点坐标、头部姿态角、帧间位移这些低维特征二是用 3D CNN 或 SlowFast 直接吃视频片段。毕设里我推荐 LSTM 路线因为特征工程可控训练数据需求小调试直观。import torch import torch.nn as nn class CheatLSTM(nn.Module): def __init__(self, input_dim12, hidden_dim64, num_layers2, num_classes3): super().__init__() self.lstm nn.LSTM(input_dim, hidden_dim, num_layers, batch_firstTrue, dropout0.3) self.fc nn.Linear(hidden_dim, num_classes) def forward(self, x): # x: (batch, seq_len, input_dim) out, _ self.lstm(x) # 取最后时间步 return self.fc(out[:, -1, :])输入维度 12 是我常用的配置左右手腕 x/y 各 4 维头部 pitch/yaw/roll 3 维帧间位移 2 维加上一个手是否在桌面区域的布尔值。序列长度取 30 帧对应 1 秒左右30fps。类别设 3 类正常、疑似、作弊。训练时注意正负样本极度不均衡作弊样本少要用加权交叉熵或 focal loss。3.3 多目标跟踪怎么把帧级判断绑到具体学生身上教室里有几十个人检测出来的手和脸必须对应到具体学生否则专注度分数和作弊告警会张冠李戴。常见做法是用 ByteTrack 或 DeepSORT 做多目标跟踪给每个学生分配一个 track_id然后把帧级的人脸、手部、姿态特征按 track_id 聚合。from yolox.tracker.byte_tracker import BYTETracker class StudentTracker: def __init__(self, track_thresh0.5, match_thresh0.8): self.tracker BYTETracker(track_threshtrack_thresh, match_threshmatch_thresh) self.student_features {} # track_id - 特征列表 def update(self, detections, frame): # detections: 人脸框列表 online_targets self.tracker.update(detections, frame.shape[:2], frame.shape[:2]) for target in online_targets: tid target.track_id if tid not in self.student_features: self.student_features[tid] [] # 把当前帧的姿态、EAR 等特征追加进去 self.student_features[tid].append(extract_features(target, frame)) return online_targets参数说明track_thresh是检测置信度阈值match_thresh是匹配阈值教室场景人多遮挡多match_thresh可以设 0.8 到 0.9 之间。student_features按 track_id 存特征序列后续做时序判断时按 id 取。注意 track_id 会因遮挡丢失而切换需要做 id 重连简单做法是用人脸特征做相似度匹配。4. 避坑与排查训练和部署阶段最容易翻车的五个点4.1 现象模型在验证集上准确率 95%一到教室视频就乱判原因训练数据是公开数据集人脸角度、光照、分辨率跟真实教室差太多。公开数据集大多是正面、均匀光照、单人脸教室是侧脸、逆光、小脸、密集人群。解决用教室实拍视频抽帧做微调至少标 500 张。如果没法实拍做数据增强时重点加运动模糊、亮度变化、小尺度缩放。我一般会把输入分辨率从 112 提到 224小脸特征保留更多。4.2 现象EAR 计算出来全是 0.1 左右所有人被判闭眼原因68 点关键点检测器在侧脸或戴眼镜时点位漂移眼睛上下点距离算错。解决先判断头部姿态yaw 超过 30 度直接跳过眼部判断戴眼镜场景换用 mediapipe 的 face mesh它对眼镜鲁棒性更好。另外 EAR 阈值不要写死按每个学生的睁眼基线做自适应取前 100 帧的中位数作为该学生的正常值。4.3 现象作弊检测误报率极高捡笔、翻书全报警原因单帧判断加固定阈值没有时序平滑。解决用滑动窗口做投票连续 15 帧里超过 10 帧判定异常才报警。另外把手在桌下和手在桌下超过 3 秒区分开短时间离开桌面不算作弊。LSTM 输出加一个置信度阈值低于 0.7 不报警。4.4 现象多目标跟踪 id 频繁切换同一个学生一会儿是 1 号一会儿是 5 号原因遮挡导致检测丢失跟踪器重新分配 id。解决ByteTrack 的match_thresh调高到 0.9同时开启低分检测框二次匹配。更稳的做法是维护一个人脸特征库新 id 出现时跟历史特征做余弦相似度匹配超过 0.6 就认为是同一人合并特征序列。4.5 现象GPU 显存不够batch size 降到 1 还是 OOM原因同时加载了人脸检测、姿态估计、手部检测、LSTM 四个模型显存叠加。解决按流水线分时加载检测阶段只留检测模型特征提取完释放显存再加载时序模型。或者用 ONNX Runtime 做推理显存占用比 PyTorch 低不少。教室场景帧率不用太高5fps 采样就够计算量降下来显存压力也小。5. 把两套模型串成完整系统推理流程、阈值调优和验证方法5.1 推理流水线的编排顺序四个模块不能并行跑要按依赖关系串。我一般这样排视频解码 → 每 6 帧取一帧5fps→ 人脸检测与跟踪 → 头部姿态估计 → 眼部/嘴部状态 → 手部检测 → 特征聚合 → LSTM 时序判断 → 输出专注度分数和作弊告警。专注度分数用加权公式基础分 100低头扣 15闭眼超 2 秒扣 10扭头扣 20手在桌下扣 25最低 0 分。作弊告警单独走 LSTM 分支不混入专注度。def process_frame(frame, tracker, pose_estimator, hand_detector, lstm_model): detections detect_faces(frame) targets tracker.update(detections, frame) results [] for target in targets: tid target.track_id aligned align_face(frame, target.landmarks) pose pose_estimator.estimate(target.landmarks, frame.shape) ear compute_ear(target.landmarks) mar compute_mar(target.landmarks) hands hand_detector.detect(frame) hand_below any(is_hand_below_desk(h, DESK_REGION) for h in hands) feature_vec build_feature(pose, ear, mar, hand_below) tracker.student_features[tid].append(feature_vec) # 攒够 30 帧做时序判断 if len(tracker.student_features[tid]) 30: seq torch.tensor(tracker.student_features[tid][-30:]).unsqueeze(0) with torch.no_grad(): logits lstm_model(seq) prob torch.softmax(logits, dim1) cheat_score prob[0][2].item() focus_score compute_focus_score(pose, ear, mar, hand_below) results.append({track_id: tid, focus: focus_score, cheat_prob: cheat_score}) return results这段是核心编排逻辑。build_feature把姿态角、EAR、MAR、手部状态拼成 12 维向量。compute_focus_score按前面说的扣分规则算。注意 LSTM 每帧都跑一次浪费算力可以每 5 帧跑一次中间帧用上一次结果。5.2 阈值调优用 ROC 曲线找最佳工作点专注度和作弊检测的阈值不能拍脑袋定。我一般会标一个 200 段的小验证集包含正常听课、低头、扭头、桌下动作、传递物品几类然后画 ROC 曲线找最佳工作点。作弊检测更看重召回率阈值可以设低一点宁可误报不漏报因为误报可以人工复核漏报就是事故。指标阈值范围调优方向注意事项人脸检测置信度0.35-0.5后排小脸降阈值低于 0.3 误检激增低头判定 pitch-20 到 -30 度按学生身高微调前排摄像头俯角大扭头判定 yaw25 到 35 度教室宽度大取小值侧脸 EAR 不可信闭眼 EAR0.15-0.2按个人基线自适应眼镜反光会干扰作弊 LSTM 概率0.6-0.75召回优先取低值配合时序投票5.3 验证方法离线指标和在线抽查怎么配合离线看三个指标专注度分数和人工标注的相关系数作弊检测的召回率和精确率跟踪 id 的切换频率。相关系数能到 0.7 以上就算可用作弊召回率争取 0.85 以上。在线验证靠抽查每天随机抽 10 段 5 分钟视频人工看告警是否准确把误报和漏报的片段截出来做 bad case 分析。我自己的习惯是每调一次阈值就存一版配置用日期加版本号命名跑完验证集记录指标。这样后面翻车了能回滚不会出现改了哪里忘了的情况。这套系统从跑通到能在教室稳定用我前后调了三个月最大的教训是别追求端到端把每个模块的输入输出卡死出问题才能定位。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑