基于Python的课堂行为图像识别:YOLO与CNN两阶段实战
发布时间:2026/9/28 5:04:59来源:尧图网络
简介这份资源是基于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_okTrue) 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 modelyolov8n.pt dataclassroom.yaml epochs100 imgsz960 batch8 device0imgsz960是因为课堂画面里人偏小输入太小会丢细节batch8按显存调8G 显存跑 960 大概就是这个量级。epochs100配合早停验证 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(weightsmodels.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(), lr1e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30)weightsDEFAULT加载预训练权重课堂数据量不大时这是精度保障。lr1e-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, conf0.4, iou0.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: breakconf0.4是检测置信度阈值课堂遮挡多时可以降到 0.3 换召回。iou0.5控制重叠框合并。分类输入必须用和训练一致的train_tf归一化参数不一致会让精度断崖式下跌。裁剪框要判空边界框贴边时可能裁出空数组。4. 训练调参与评估课堂行为分类的指标怎么看4.1 关键参数与调整方向参数常用值调整方向检测 imgsz960人小就加大显存不够降 batch检测 conf0.3~0.5漏检多降误检多升分类 lr1e-4不收敛降到 5e-5分类 batch32~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, window7): if track_id not in history: history[track_id] deque(maxlenwindow) history[track_id].append(probs) avg sum(history[track_id]) / len(history[track_id]) return avg.argmax(), avg.max()window7是我在 25fps 视频上试出来比较平衡的值太小压不住抖动太大行为切换会滞后。avg.max()低于 0.5 时建议保持上一帧标签。这套平滑逻辑不改变模型本身纯后处理加在任何两阶段方案上都能用。最后说个我自己的习惯每次改完参数先在一个固定的小验证集上跑一遍把检测 mAP、分类准确率、推理帧率三个数记下来再决定这次改动是留还是回滚。课堂行为识别没有一劳永逸的配置只有不断对着真实教室画面调出来的稳定。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网