简介:这份资源是面向计算机相关专业学生与项目实战学习者的毕业设计源码包,聚焦智慧教室场景下的课堂专注度分析与考试作弊检测,基于深度学习技术实现,适合用作毕设、课程设计或期末大作业的参考方案。压缩包共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_thresh=0.5): # RetinaFace 返回 dict,key 是人脸编号 faces = RetinaFace.detect_faces(frame, threshold=conf_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]], dtype=np.float32) src = np.array([landmarks["left_eye"], landmarks["right_eye"], landmarks["nose"], landmarks["mouth_left"], landmarks["mouth_right"]], dtype=np.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] # 右嘴角 ], dtype=np.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]], dtype=np.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"] ], dtype=np.float64) success, rvec, tvec = cv2.solvePnP(MODEL_POINTS, image_points, camera_matrix, dist_coeffs, flags=cv2.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 的计算细节
眼睛闭合时长和打哈欠频率是专注度的负向指标。EAR(Eye Aspect Ratio)和 MAR(Mouth 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_mode=False, max_num_hands=2, min_detection_confidence=0.5, min_tracking_confidence=0.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_hands=2够用,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_dim=12, hidden_dim=64, num_layers=2, num_classes=3): super().__init__() self.lstm = nn.LSTM(input_dim, hidden_dim, num_layers, batch_first=True, dropout=0.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_thresh=0.5, match_thresh=0.8): self.tracker = BYTETracker(track_thresh=track_thresh, match_thresh=match_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, dim=1) 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 度 | 按学生身高微调 | 前排摄像头俯角大 |
| 扭头判定 yaw | 25 到 35 度 | 教室宽度大取小值 | 侧脸 EAR 不可信 |
| 闭眼 EAR | 0.15-0.2 | 按个人基线自适应 | 眼镜反光会干扰 |
| 作弊 LSTM 概率 | 0.6-0.75 | 召回优先取低值 | 配合时序投票 |
5.3 验证方法:离线指标和在线抽查怎么配合
离线看三个指标:专注度分数和人工标注的相关系数,作弊检测的召回率和精确率,跟踪 id 的切换频率。相关系数能到 0.7 以上就算可用,作弊召回率争取 0.85 以上。在线验证靠抽查,每天随机抽 10 段 5 分钟视频,人工看告警是否准确,把误报和漏报的片段截出来做 bad case 分析。
我自己的习惯是每调一次阈值就存一版配置,用日期加版本号命名,跑完验证集记录指标。这样后面翻车了能回滚,不会出现"改了哪里忘了"的情况。这套系统从跑通到能在教室稳定用,我前后调了三个月,最大的教训是别追求端到端,把每个模块的输入输出卡死,出问题才能定位。希望帮到你。
本文还有配套的精品资源,点击获取