简介:基于深度学习Python实现的课堂专注度行为识别系统,面向高校人工智能、电子信息、物联网等相关专业学生及科研人员,适用于课堂场景下的学生行为检测与专注度分析,可作为毕业设计、课程设计或项目立项演示。资源共2000个文件,压缩包大小114.36MB,代码以Python、Java、Vue、C++为主——Python负责模型构建与推理,Java与Vue搭建后端服务及前端交互界面,C++用于算子优化,另含模型权重、设计文档及SQL脚本,方便快速部署与二次开发。已有75人学习下载。资源内含完整源码及预训练模型,代码经过严格测试,功能完善,可正常运行;配套项目说明与设计文档,帮助理解系统架构与训练流程。基础薄弱的用户还可借助远程教学快速上手,在现有基础上扩展手势识别、表情分析等功能,适合作为算法学习与工程实践的综合参考。
1. 把“课堂专注度”翻译成可计算的指标
教室后排学生低头、趴桌、玩手机,老师在讲台上往往只能看见前排。传统课堂考勤只能确认“人来没来”,无法回答“学生是否在专注听讲”。课堂专注度行为识别系统要解决的,正是把“专注”这个主观体验,翻译成从摄像头画面里可重复计算的客观指标。它的技术栈并不玄学:Python 做胶水,深度学习模型做检测,后挂一段逻辑把姿态时序映射成分数。适合的人群也明确:想给教室、自习室、实验机房做行为分析的教育信息化团队,或者正打算用姿态估计做垂直场景应用的工程师。
这套系统最常见的落地形态,是用姿态估计模型检测画面中每个人的头部关键点,接着用一个轻量分类头判断“低头、侧身、趴桌、正常”等状态,再按时间窗口聚合出注意力评分。需要说明的是,这类纯视觉方案只能推断姿态,不能真正读懂学生的内心活动,但它能以较低成本给出一个可量化、可回放、可审计的专注度参考值。下文按一条可复现的主线展开:先定方案选型,再讲数据与训练,然后给出推理管线与评分算法,最后收在量化部署和几个容易踩的坑上。
2. 行为识别方案选型:为什么用姿态估计而不是端到端 CNN
2.1 先分清目标检测、姿态估计和行为识别三件事
课堂专注度系统的输入是教室监控画面,输出是每个人在某段时间片段内的专注度等级。这里面有两个层次的任务:空间上要找出“谁在画面里”,时序上要判断“这个人在干什么”。目标检测模型只能回答“哪里有人”,CNN 视频分类模型能回答“这段视频里有什么行为”,但它给不出精确到个体的定位结果。要在多人教室场景里同时完成定位与行为判断,姿态估计是更自然的中间表示。
所谓姿态估计,是预测图片中每个人体关节点的坐标,常用 COCO 格式的 17 个关键点,包括双眼、双耳、双肩、双肘、双腕、双髋、双膝、双踝。专注度相关的行为,比如低头、趴桌、单手托腮、转身交流,都能用头部、肩部、手腕这些关键点的位置关系表示。相比直接输入图像做分类,姿态关键点带来的好处是引入了人体结构的先验,模型不容易被衣服颜色、背景纹理这类与行为无关的因素干扰。
2.2 常用方案的取舍:YOLO-Pose 与两阶段姿态估计
常见做法有两类。第一类是自顶向下方法,先做人检测,再对每个检测框单独做姿态估计,代表如 HRNet、RTMPose;第二类是自底向上方法,先在整张图上回归所有关键点,再聚类组装成个体,代表如 OpenPose。课堂场景人通常在画面中占据几十像素高度的尺寸,人数在十几到几十人之间,两类方法都能用,但工程上我一般倾向于选 YOLOv8-Pose 这类单阶段方案。
单阶段方案把检测和姿态任务在一个网络里完成:特征图经过检测头输出人体框,同时经过姿态头输出每个框内人的关键点热图。它比两阶段少一次 ROI 裁剪与二次推理,延迟低很多,在多路摄像头并发分析时优势明显。准确率在关键点 PCK 指标上比 HRNet 略低,但课堂场景对关键点的要求是“头在哪儿、肩在哪儿、手在不在桌子上”,不需要精确到像素级,单阶段方案完全够用。如果希望关键点更稳一点,可以换成 RTMPose-t 或 RTMPose-s,它在轻量级模型里精度比 YOLOv8-Pose 更高,推理速度也更适合边缘设备。
2.3 状态分类器:用规则还是用分类头
拿到关键点以后,需要把它映射到“低头、趴桌、正常、侧身”这些状态。有一种做法是纯规则:计算头部关键点与颈部连线与竖直方向的夹角,角度大于阈值就判低头。规则方案可解释性强,但泛化能力差,不同摄像头的俯仰角、学生身高差异会显著影响阈值。
更稳妥的做法是保留一个轻量分类头。把姿态估计输出的 17 个关键点归一化之后,喂给一个只有两三层全连接的小网络,输出状态概率。这个分类头参数量只有几万个,训练成本很低,但鲁棒性比手工规则好很多。实际系统中,规则常被用作后备方案,当分类头置信度低于某个阈值时回退到角度规则,保证不会出现“无法判断”的真空期。
# 姿态分类器的核心结构示例 class FocusHead(nn.Module): def __init__(self, num_keypoints=17, num_classes=4): super().__init__() self.fc1 = nn.Linear(num_keypoints * 3, 64) # 输入 x/y/置信度 self.fc2 = nn.Linear(64, 32) self.out = nn.Linear(32, num_classes) def forward(self, kpts): x = torch.relu(self.fc1(kpts)) x = torch.relu(self.fc2(x)) return self.out(x)这里输入取每个关键点的坐标和置信度共 51 维,是为了让分类头知道哪些关键点是遮挡或不可见的。教室场景里侧面坐姿会导致一半关键点被遮挡,直接把缺失点填 0 会误导分类器,把置信度一并输入能让模型学会忽略不可靠的坐标。分类头输出的 4 类通常设定为正常听讲、低头、趴桌、转身交流,也可根据需求加入“玩手机”这个类别。
2.4 开发环境与基础依赖
这套系统的代码规模不大,依赖相对固定。Python 版本建议用 3.9 或 3.10,PyTorch 按本机 CUDA 版本装 1.13 以上即可。推理阶段用 ONNX Runtime 代替 PyTorch,内存占用少,加载模型更快。如果要在 Jetson 等边缘设备上部署,还需要 NanoCV 或 TensorRT 做加速,这部分后面章节单独讲。
conda create -n focus python=3.10 -y conda activate focus pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics onnxruntime opencv-python numpy pandas需要说明的是,ultralytics 库只用于训练和导出模型,推理环节不依赖它。很多系统出问题就在这一步:把 ultralytics 的 YOLO 类直接塞进生产服务的循环里,每次请求都创建新实例,导致显存溢出。正确做法是训练完导出 ONNX,生产环境只加载 ONNX Runtime 的 InferenceSession。
3. 训练数据与模型训练:从合成数据到课堂数据微调
3.1 数据从哪来:不要指望现成数据集
课堂专注度这个细分场景没有公开的标准数据集。通用姿态数据集如 COCO 包含大量站姿、运动姿态,但几乎没有趴桌、侧头看邻座这类课堂动作,直接用它训练的分类器在教室场景会水土不服。开源的课堂行为数据集大多来自国外大学公开课录像,场景单一,标注风格与本项目未必一致。最可靠的方式是自建小数据集再配合预训练模型微调。
在冷启动阶段,常见做法是先在合作教室装一台测试相机,采集一周左右的课堂视频,然后抽取帧做标注。注意抽帧不要均匀抽,要用场景检测按画面变化分布抽帧,保证覆盖不同的光线、座位角度和人数密度。按一个 40 人教室计算,5000 张标注图的规模足够训练一个行为分类头。很多人忽略的是,姿态估计器本身可以直接用 COCO 预训练权重,不需要重新训练,只需精调分类头。这样对标注量的要求大幅降低。
3.2 标注协议:把行为类别定义到可判别
标注类别设计直接决定系统效果上限。类别太少则无法区分“趴桌睡觉”和“低头看书”,类别太多则标注人员无法稳定判断。推荐五类标注方案,标注工具用 LabelMe 或 X-AnyLabeling 均可,先标人体框再关联行为标签。
| 类别 | 定义标准 | 关键姿态特征 |
|---|---|---|
| listening | 正常听讲 | 面朝前方,头颈夹角小于 30 度 |
| looking_down | 低头 | 头部下垂,视线低于讲台水平线 |
| lying | 趴桌 | 头部高度明显低于肩部,手部在头部附近 |
| turned | 转身交流 | 肩部旋转超过 45 度,头部朝向侧方 |
| occluded | 遮挡/离开 | 关键点置信度大面积低于阈值 |
第五个“遮挡/离开”类别很关键。系统运行时大概率会遇到学生起身走到过道、被前排遮挡等情况,强制分到其他四类会产生噪声标签,反而拖累模型。把它单独立为一种状态,评分时可以按中性处理,不扣分也不加分。
3.3 数据增强:针对课堂场景的特殊处理
通用姿态估计的数据增强通常是随机翻转、旋转、缩放、颜色抖动,课堂场景还要额外处理两个问题。第一是透视变形,教室摄像头通常安装在黑板正上方,画面中后排学生的姿态压缩明显,同一个人在不同座位拍摄到的姿态差异很大。应对方式是在训练时做随机透视变换,把关键点坐标矩阵随机做小角度射影变换,而不是简单的仿射变换。
第二是时序抖动。单个静态帧的分类结果会有随机波动,同一段低头动作经常在 3-5 帧内被识别成“低头”和“正常”交替。单纯靠模型无法彻底解决,需要结合时序平滑。训练阶段可以加入帧间关键点坐标的时序一致性约束,但实现麻烦,工程上更常见的是在推理阶段处理。这里先记一个结论:先做轻量的坐标平滑,再把分类结果做时间窗口投票,系统表现会明显上一个台阶。
3.4 训练配置与模型清洗
训练前用 COCO 预训练权重初始化姿态特征提取部分,只用新数据微调头部和 FC 层。如果是从零训练整个网络,至少需要两万张以上标注图才够,而微调因为特征提取器已经学到通用的人体结构,课堂数据 3000 张就能收敛。YOLOv8-Pose 的微调命令如下:
yolo pose train \ model=yolov8n-pose.pt \ data=classroom_focus.yaml \ epochs=60 \ imgsz=640 \ batch=16 \ lr0=0.001 \ cos_lr=Trueclassroom_focus.yaml指向数据集目录及五类标签的映射,lr0设为 0.001 而不是默认的 0.01,是为了避免大学习率破坏预训练特征。训练完成后要检查验证集上每个类别的混淆矩阵,重点看 listening 和 looking_down 之间的混淆比例,这两个类别是最容易分不清的。如果混淆严重,优先检查标注质量而不是加数据,很多标注人员在快速标注时会把“略带低头的听讲”标成 listening,把“抬头但视线下垂”标成 looking_down,这类标签噪声对模型的影响比视觉噪声更大。
“伪标注+人工抽检”的迭代策略也值得采用:先用第一版模型批处理全部课堂视频,得到行为标签,再抽 10% 的片段让标注人员修正,把修正后的数据合并进训练集。这个流程能把标注成本降低 60% 以上,同时模型在第二轮迭代后能明显改善边缘姿态的稳定性。
4. 推理管线与专注度评分算法
4.1 推理管线设计:视频流处理要分清三层
课堂系统的推理不是拿单张图片跑一次模型那么简单,视频流处理需要分层设计。最底层是视频输入层,负责从本地录像或 RTSP 流拉取帧;中间是模型推理层,运行姿态估计模型;最上层是状态聚合层,把连续帧的姿态状态聚合成专注度评分。每层之间用队列解耦,避免视频源的帧率波动影响模型推理。
模型推理层的吞吐量要仔细核算。教室场景每帧可能检测出 20-40 人,YOLOv8n-Pose 在单个 GPU 上大约能做到 40 毫秒一帧。按 25fps 的视频流计算,跳过中间帧,每 6 帧采样 1 帧,推理频率约 4fps,这个频率对行为识别来说足够。行为是慢变量,低头动作至少持续 1 秒以上,太高的推理频率只会增加计算压力,不会提升评分精度。
4.2 关键代码:ONNX 模型加载与推理
模型导出之后,生产环境只需要 ONNX Runtime。下面的代码实现了一个最小但可用的推理类,包含预处理、关键点格式转换和结果输出三个环节:
import numpy as np import cv2 import onnxruntime as ort class PoseFocusEngine: def __init__(self, onnx_path, conf_thres=0.5, iou_thres=0.45): self.session = ort.InferenceSession( onnx_path, providers=["CUDAExecutionProvider", "CPUExecutionProvider"]) self.conf_thres = conf_thres self.iou_thres = iou_thres self.input_name = self.session.get_inputs()[0].name def preprocess(self, frame): # 保持纵横比缩放到 640,剩余区域用灰边填充 h, w = frame.shape[:2] scale = 640 / max(h, w) nh, nw = int(h * scale), int(w * scale) resized = cv2.resize(frame, (nw, nh)) canvas = np.full((640, 640, 3), 114, dtype=np.uint8) x0, y0 = (640 - nw) // 2, (640 - nh) // 2 canvas[y0:y0+nh, x0:x0+nw] = resized blob = canvas[:, :, ::-1].transpose(2, 0, 1) / 255.0 return blob[np.newaxis, ...].astype(np.float32), scale, x0, y0 def inference(self, frame): blob, scale, x0, y0 = self.preprocess(frame) outputs = self.session.run(None, {self.input_name: blob})[0] return self.postprocess(outputs, scale, x0, y0, frame.shape)推理输出的原始张量形状是[1, 56, 8400],其中 56 表示 4 个框坐标值加上 51 个关键点坐标值,8400 是特征图摊平后的候选框数量。后处理要先把框坐标还原到原图,再做 NMS 去掉重叠的检测框,最后对置信度高于阈值的人提取 17 个关键点。这里有几个隐含的细节:关键点坐标同样要除以缩放系数并减去填充偏移;模型输出的关键点数组是 x、y 交替排列,没有可见性标识,需要额外判断数值是否在有效范围内。
4.3 专注度评分:从状态到分数的时域聚合
获得每一帧每个人的行为类别后,专注度评分是最后一道工序。常见的错误是直接对单帧类别计算百分比,这会让评分在相邻帧之间大幅跳变。正确做法分成两个步骤:先按时间窗口做类别平滑,再用平滑后的概率计算评分。
class FocusScorer: def __init__(self, window_size=30, pos_weight=1.0, neg_penalty=0.5): self.history = [] self.window_size = window_size self.pos_weight = pos_weight self.neg_penalty = neg_penalty def update(self, state_prob): # state_prob: dict,包含各类别概率,如 {"listening": 0.8, "looking_down": 0.1} self.history.append(state_prob) if len(self.history) > self.window_size: self.history.pop(0) return self.score() def score(self): if not self.history: return 0.0 focus_sum = 0.0 for prob in self.history: focus = (prob.get("listening", 0) * self.pos_weight - prob.get("looking_down", 0) * self.neg_penalty - prob.get("lying", 0) * 1.0) focus_sum += max(0, min(1, focus)) return round(focus_sum / len(self.history), 3)socre中listening权重为 1,looking_down扣 0.5,lying扣 1.0,turned和occluded不参与计算。窗口大小取 30 帧对应大约 7 秒的时间窗,这个时长能有效滤除短暂低头看表、转头看门等瞬时动作的干扰。窗口太短会让评分灵敏度过高,窗口太长则会掩盖真实的注意力下降趋势,30 帧是经验平衡点。
评分结果还需要按人和按班级两个维度输出。按人维度给出每个学生的时间-专注度曲线,用于生成个人分析报告;按班级维度把同一时刻所有人的评分取平均,得到班级整体专注度曲线。后者的价值在于辅助教师复盘教学环节:提交讲解、学生练习、小组讨论这些教学环节对应的专注度曲线形态有明显差异,可以作为教学设计的参考信息。
4.4 多人场景下的数据关联
多人场景要额外处理数据关联问题。因为推理是间隔采样的,同一个学生需要跨帧关联才能形成时序轨迹。最常用的是 IoU 跟踪:用当前帧检测框与上一帧所有轨迹的预测框计算交并比,最近邻匹配。但当学生起身、走出画面再回来时,新检测框会脱轨,被当成新的跟踪对象。
更稳的方案是结合关键点框和位置嵌入特征。具体来说,在检测框的 IoU 之外,用人体姿态关键点计算相似度,作为同一个人重识别的特征。即使跟踪目标短暂丢失,特征相似度匹配也能把轨迹重新接上。课堂场景因为座位固定,另一种更简单的方法是利用座位先验:记录每个学生首次入座的位置,后续帧优先按“位置在本人座位附近”来匹配检测框,准确率远高于纯 IoU 追踪。
5. 部署实战:INT8 量化时的两个关键决策
系统从验证环境到教室实装,最大的坎不是准确率而是算力。一台教室的摄像头按 4 路计算,每路 25fps 实时拉流,纯 PyTorch 推理需要一张 2080Ti 以上显卡才能跑平稳,而 INT8 量化后单路推理延迟能降到原来的三分之一。但量化做不好,准确率会掉 5% 甚至更多,这里有两个决策点直接影响量化损失。
第一个决策点是动态量化还是静态量化。静态量化需要收集校准数据,推理时按校准统计的缩放因子做定点换算,精度损失小,适合本系统;动态量化按运行时数值范围动态计算缩放因子,精度损失大,适用于对延迟不敏感的场景。静态量化在 ONNX Runtime 里可以用onnxruntime.quantization.quantize_static接口完成,校准集不要用测试集,而是用新采集的教室灰度图样本,数量 200 到 500 张即可。
第二个决策点是归一化层的处理方式。课堂模型的输入已经做了除以 255 的归一化,叠加 BatchNorm 层后,量化图在数值分布上可能非常不均匀,导致量化颗粒度变大、精度下降。常见做法是训练后把 BatchNorm 层折叠进前面卷积层,再导出 ONNX,量化的数值分布会更集中。有一点容易被忽略:分类头由于输出概率值集中在 0 和 1 附近,量化时极易出现偏差,遇到分类效果下降严重的场景,可以把分类头单独保留为 FP32,只对姿态检测部分做 INT8。
部署完成后建议留一套离线验证脚本,用一段固定课堂视频做回归测试,每次更新模型或调整量化参数都会跑一遍,对比位姿检测 mAP 和专注度评分的误差。这套脚本能挡住大多数回归问题。另外,评分结果不要直接存储每一帧的原始值,按每 30 秒聚合输出一条记录即可,一天的录像只需要几千行数据库记录,后续生成周报时再按需重算。这个做法能让数据层和存储层保持简洁,也方便回溯时快速定位到指定时段的行为评分。
本文还有配套的精品资源,点击获取