简介:这份资源是基于Python与深度学习框架实现的课堂行为图像识别分类项目源码,面向计算机、人工智能相关专业的学生及自学者,可用于毕业设计、期末大作业或课程实践,帮助解决课堂场景下学生行为自动分类的建模与实现问题。压缩包共包含1206个文件,以1183张jpg图像样本为主体,辅以14个py源码文件、3个png图示、2个md说明文档及license等配置项,整体约100.29MB,数据与代码结构完整,便于直接训练与复现。项目已通过严格调试,评审分达到95分以上,可放心运行。目前已有571人学习下载,读者可从中获取完整的图像分类流程、数据集组织方式、模型训练脚本与推理代码,并参考目录结构快速理解课堂行为识别的实现思路,适合作为深度学习入门到实战的参考案例。
1. 课堂行为图像识别:从一段监控视频到可用的分类模型
教室里摄像头拍下的画面,和 ImageNet 里那些摆拍图完全是两回事。学生低头、侧身、被前排挡住、光线忽明忽暗,一个班里几十号人同时出现在一帧里,你要判断的是每个人此刻在听课、写字、举手还是趴桌子。基于 Python 的深度学习课堂行为图像识别分类项目,要解决的就是把这种非受控场景下的学生行为自动分成若干类别,输出每帧里每个人的行为标签。它适合两类人:一类是正在做课程设计或毕业设计、需要一套能跑通的完整流程的学生;另一类是已经会写 Python、想找一个真实场景练手检测加分类的工程师。热搜里 python 安装教程、vscode python 环境配置、深度学习入门这些词反复出现,说明大量人卡在环境而不是算法本身,所以这篇会从数据准备一路讲到推理部署,把每一步的参数和坑都摊开。
课堂行为识别和通用图像分类最大的区别在于:它不是给整张图打一个标签,而是先定位到人,再判断这个人在干什么。常见做法是两阶段——检测器负责框出每个学生,分类器负责判断框内的行为。也有单阶段方案直接输出行为框,但对小目标和遮挡的鲁棒性往往不如两阶段稳。我一般会先跑通两阶段,因为中间结果可查,哪一步出问题一目了然,调起来有后悔药。数据集通常来自公开课堂行为数据集或自己标注的教室监控截图,类别常见的有听讲、书写、举手、趴桌、站立、讨论这几类,具体类别数按你的标注来定,不要照搬别人的标签体系。
2. 数据准备与标注:课堂行为数据集怎么攒才不返工
2.1 课堂场景的数据特点与采集策略
课堂监控画面有几个绕不开的特点。第一是视角固定但俯角大,学生呈前后排堆叠,后排人脸小、遮挡重。第二是光照不均,靠窗一侧过曝、靠门一侧偏暗。第三是行为边界模糊,比如“低头写字”和“低头玩手机”在单帧上几乎一样,只能靠时序或手部区域区分。采集时我建议按教室、时段、课程类型分层抽样,至少覆盖上午、下午、靠窗、靠门、前排、后排这几个维度,否则模型很容易学到“靠窗=听讲”这种伪相关。
采集频率上,如果只做单帧分类,每秒抽 1 到 2 帧就够;如果后面想接时序模型,建议保留原始视频或按 5 到 10 fps 抽帧。分辨率不要一味求高,1080p 缩到 960 宽通常足够,再高只是徒增显存。每类行为至少准备 800 到 1500 个样本框,类别不均衡时优先补少样本类,而不是简单复制。
2.2 用 LabelImg 标注并转成 YOLO 格式
标注工具用 LabelImg 或 Labelme 都行,检测任务用矩形框即可。标注时统一规则:框住可见身体范围,严重遮挡超过一半的样本直接丢弃,不要硬标。标完导出 YOLO 格式,每张图对应一个 txt,每行是类别 中心x 中心y 宽 高,坐标都归一化到 0 到 1。
import os import xml.etree.ElementTree as ET # 把 LabelImg 的 VOC xml 批量转成 YOLO txt classes = ["listen", "write", "raise", "lie", "stand", "discuss"] xml_dir = "annotations" out_dir = "labels" os.makedirs(out_dir, exist_ok=True) def convert(xml_path, out_path): tree = ET.parse(xml_path) root = tree.getroot() size = root.find("size") w = int(size.find("width").text) h = int(size.find("height").text) lines = [] for obj in root.iter("object"): name = obj.find("name").text if name not in classes: continue cls_id = classes.index(name) bnd = obj.find("bndbox") x1 = float(bnd.find("xmin").text) y1 = float(bnd.find("ymin").text) x2 = float(bnd.find("xmax").text) y2 = float(bnd.find("ymax").text) # 归一化并转成中心点加宽高 cx = (x1 + x2) / 2.0 / w cy = (y1 + y2) / 2.0 / h bw = (x2 - x1) / w bh = (y2 - y1) / h lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") with open(out_path, "w") as f: f.write("\n".join(lines)) for f in os.listdir(xml_dir): if f.endswith(".xml"): convert(os.path.join(xml_dir, f), os.path.join(out_dir, f.replace(".xml", ".txt")))这段脚本做三件事:解析 xml、按类别表映射 id、把绝对坐标归一化成 YOLO 需要的中心点格式。classes列表顺序必须和后面训练配置里的names完全一致,错一位整个训练就废了。归一化用图像真实宽高,不要用固定值,否则不同分辨率混在一起会错位。转换完抽查几张,用可视化脚本把框画回原图确认没偏。
2.3 划分训练集与类别不均衡处理
划分比例常用 7:2:1,但课堂数据要按“教室”划分而不是随机划分,否则同一教室的相似帧会同时进训练和验证,指标虚高。类别不均衡时,轻度过采样少样本类,重度不均衡就在损失里加类别权重。别用随机旋转 90 度这种增强,课堂场景里人是站立的,转 90 度会造出物理上不存在的样本。
3. 检测加分类两阶段模型:YOLO 框人,CNN 判行为
3.1 为什么选两阶段而不是端到端
端到端方案听起来省事,但课堂场景里小目标密集,单阶段直接回归行为框容易在遮挡处漏检。两阶段的好处是检测和分类解耦:检测器只关心“这里有没有人”,分类器只关心“这个框里的人在干嘛”。检测器可以用 YOLOv8n 或 YOLOv5s 这类轻量模型,分类器用 ResNet18 或 MobileNetV3,整体在单张消费级显卡上就能训。常见做法是先用检测器裁出人框,再把框缩放到 224×224 送进分类网络,两个阶段分别训练、分别调参,出问题能快速定位是框不准还是分类错。
3.2 训练检测器:YOLOv8 配置文件与关键参数
YOLOv8 的数据配置用一个 yaml 描述路径和类别。下面是最小配置。
# classroom.yaml path: ./dataset train: images/train val: images/val nc: 6 names: ["listen", "write", "raise", "lie", "stand", "discuss"]path是数据集根目录,train和val是相对路径下的图片文件夹,标签文件夹默认与图片同级同名替换为 labels。nc是类别数,必须和 names 长度一致。训练命令:
yolo detect train model=yolov8n.pt data=classroom.yaml epochs=100 imgsz=960 batch=8 device=0imgsz=960是因为课堂画面里人偏小,输入太小会丢细节;batch=8按显存调,8G 显存跑 960 大概就是这个量级。epochs=100配合早停,验证 mAP 连续 20 轮不升就停。训练时重点看验证集的 mAP50 和召回,召回低说明漏检多,优先加数据或调低置信度阈值,而不是盲目加轮数。
3.3 训练行为分类器:ResNet18 微调与数据增强
分类器输入是检测框裁出的人体图。数据增强用随机水平翻转、颜色抖动、随机擦除,不要用大角度旋转。下面是用 PyTorch 微调 ResNet18 的核心代码。
import torch import torch.nn as nn from torchvision import models, transforms # 分类类别与检测类别保持一致 num_classes = 6 model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT) model.fc = nn.Linear(model.fc.in_features, num_classes) # 课堂场景增强:翻转加颜色抖动,不做大角度旋转 train_tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(0.2, 0.2, 0.2), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=30)weights=DEFAULT加载预训练权重,课堂数据量不大时这是精度保障。lr=1e-4是微调常用值,太大容易把预训练特征冲掉。CosineAnnealingLR让学习率余弦下降,后期收敛更稳。训练时如果训练集准确率远高于验证集,先查是不是同一教室的帧泄漏到了两边,再考虑加增强。
3.4 两阶段串联推理:从视频帧到行为标签
推理时先检测再分类,把结果画回原图。核心逻辑如下。
from ultralytics import YOLO import cv2 import torch detector = YOLO("runs/detect/train/weights/best.pt") clf = model.eval().cuda() names = ["listen", "write", "raise", "lie", "stand", "discuss"] cap = cv2.VideoCapture("classroom.mp4") while True: ok, frame = cap.read() if not ok: break results = detector(frame, conf=0.4, iou=0.5)[0] for box in results.boxes: x1, y1, x2, y2 = map(int, box.xyxy[0].tolist()) crop = frame[y1:y2, x1:x2] if crop.size == 0: continue inp = train_tf(crop).unsqueeze(0).cuda() with torch.no_grad(): pred = clf(inp).argmax(1).item() cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, names[pred], (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow("result", frame) if cv2.waitKey(1) == 27: breakconf=0.4是检测置信度阈值,课堂遮挡多时可以降到 0.3 换召回。iou=0.5控制重叠框合并。分类输入必须用和训练一致的train_tf,归一化参数不一致会让精度断崖式下跌。裁剪框要判空,边界框贴边时可能裁出空数组。
4. 训练调参与评估:课堂行为分类的指标怎么看
4.1 关键参数与调整方向
| 参数 | 常用值 | 调整方向 |
|---|---|---|
| 检测 imgsz | 960 | 人小就加大,显存不够降 batch |
| 检测 conf | 0.3~0.5 | 漏检多降,误检多升 |
| 分类 lr | 1e-4 | 不收敛降到 5e-5 |
| 分类 batch | 32~64 | 按显存调 |
| 增强强度 | 中 | 过拟合时加强,欠拟合时减弱 |
4.2 评估指标与混淆矩阵排查
检测看 mAP50 和召回,分类看每类准确率和混淆矩阵。课堂行为里最容易混的是“听讲”和“书写”,因为两者都是低头坐姿,区别在手部位置。混淆矩阵里如果这两类互相错得多,说明单帧信息不够,要么加手部关键点特征,要么引入短时序。另一个常见混淆是“举手”和“站立”,前者手臂举起但身体坐姿,后者整体离座,框的高度比例能区分,可以在分类前加一个宽高比过滤。
评估时按教室分组统计,别只看总体准确率。某个教室准确率明显低,多半是光照或视角差异,针对性补该教室数据比全局调参有效。
5. 避坑与排查:课堂行为识别项目里最容易翻车的五件事
现象:训练 loss 正常下降,但验证 mAP 一直很低。原因:训练集和验证集按随机帧划分,同一教室相邻帧高度相似,造成数据泄漏,验证集其实在“背答案”。 解决:按教室或按视频片段划分,确保验证集来自训练时没见过的教室。
现象:检测框位置对,但分类结果几乎全是一类。原因:分类训练时类别极度不均衡,或者归一化参数和预训练模型不匹配。 解决:先统计各类样本数,加类别权重;核对 Normalize 的均值和方差是否为 ImageNet 标准值。
现象:推理时画面卡顿,帧率只有个位数。原因:检测和分类串行跑在同一张卡上,且每帧都重新加载模型。 解决:模型在循环外加载一次;分类可以攒 batch 再推理;必要时用半精度model.half()。
现象:换一个教室准确率暴跌。原因:模型学到了特定教室的背景、光照、座位布局等伪特征。 解决:训练时加入不同教室数据,增强里加随机亮度对比度,必要时对背景做随机遮挡。
现象:举手行为经常漏检。原因:举手时人体框变高变窄,和训练集中坐姿框的宽高比差异大,检测器没学好这种形态。 解决:补充举手样本,或在检测后加宽高比规则做二次筛选,别只靠网络。
6. 把模型跑得更稳:时序平滑与置信度融合的实用技巧
单帧分类最大的问题是抖动,同一学生相邻帧一会儿“听讲”一会儿“书写”,输出像心电图。我一般会在推理后加一层时序平滑:对每个检测框用跟踪算法分配 id,然后对同一 id 最近 5 到 10 帧的分类概率做滑动平均,取平均后最大的类别作为输出。这样既压住了抖动,又不会引入太大延迟。跟踪可以用简单的 IOU 匹配,也可以用 ByteTrack,课堂场景里 IOU 匹配基本够用。
另一个技巧是置信度融合。检测置信度和分类置信度相乘作为最终分数,低于阈值的框直接丢弃,能过滤掉一部分误检。如果某帧某个框的分类概率分布很平(最大概率不到 0.5),说明模型也不确定,这时可以回退到上一帧的平滑结果,而不是硬输出一个类别。
from collections import deque # 每个跟踪 id 维护一个概率队列 history = {} def smooth(track_id, probs, window=7): if track_id not in history: history[track_id] = deque(maxlen=window) history[track_id].append(probs) avg = sum(history[track_id]) / len(history[track_id]) return avg.argmax(), avg.max()window=7是我在 25fps 视频上试出来比较平衡的值,太小压不住抖动,太大行为切换会滞后。avg.max()低于 0.5 时建议保持上一帧标签。这套平滑逻辑不改变模型本身,纯后处理,加在任何两阶段方案上都能用。
最后说个我自己的习惯:每次改完参数,先在一个固定的小验证集上跑一遍,把检测 mAP、分类准确率、推理帧率三个数记下来,再决定这次改动是留还是回滚。课堂行为识别没有一劳永逸的配置,只有不断对着真实教室画面调出来的稳定。希望帮到你。
本文还有配套的精品资源,点击获取