ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于YOLO的真实课堂行为检测:从数据集到YOLOv8训练实践

基于YOLO的真实课堂行为检测:从数据集到YOLOv8训练实践 简介真实课堂场景下的教室行为检测目标检测数据集主要面向计算机视觉学习者、算法工程师以及智慧课堂项目开发团队可用于学生行为识别、课堂专注度分析等模型训练与效果验证。数据约11,800张已标注图像采用YOLO标注格式共4个行为类别覆盖回答问题、板书等常见课堂动作数据已按训练集、验证集划分拿到后即可直接喂给YOLO系列模型进行训练、评估或改进实验。资源包共2000个文件以1999个txt标签文件和1个Python可视化脚本show.py为主整体约787.64MB。txt文件与图像标注一一对应运行show.py可快速预览标签框位置与类别是否准确便于排查标注问题、检查数据质量。目前已有76人学习适合用来补充课堂行为检测数据集、复现并改进YOLOv5检测流程或开展真实场景目标检测实验。1. 真实课堂行为检测这个数据集解决的是“谁在干什么”不是“谁开心”教室摄像头装了好几年真正能自动回答“这节课有多少人在举手、多少人在趴桌”的系统却不多见。原因不在模型而在数据通用的行人检测能框出人但框不出“举手”“低头”“趴桌”这类行为状态。这份已标注的真实课堂数据集约 11,800 张标签是 YOLO 标注格式意味着你省掉最耗时的收集和标注环节直接进入目标检测训练流程。它回答的是可观察的行为状态比如“左排第三个学生在低头写字”“后排靠窗学生在趴桌”适合做课堂活跃度评估、智慧督课、教学质量分析这类方向。但它答不了“这学生开不开心”那种主观判断那不是目标检测该干的事别指望一个框解决所有问题。2. 为什么课堂行为检测用目标检测而不是姿态估计“框类别”够用且最稳2.1 三条技术路线对比目标检测为什么是首选做课堂行为识别常见的技术路线有三条目标检测、姿态估计加行为分类、视频动作识别。这三条路线我都在不同项目里试过实际落地体验差别很大路线标注成本遮挡鲁棒性推理开销落地难度目标检测框行为类别低一个框一个标签好框不依赖关键点是否可见低单阶段检测器实时性高低姿态估计行为分类高需要标十几个人体关键点差关键点被课桌前排遮挡就丢中两个模型串联中高视频动作识别高要标连续片段差依赖完整动作片段截断就失效高3D 卷积计算量大高我一般直接用目标检测。理由很直接教室是高密度遮挡场景后排学生经常被前排挡住半边身体姿态估计依赖的关键点在这种遮挡下经常不可见而边界框在遮挡情况下反而稳定。那“行为”怎么用框表达关键在类别设计上。把行为定义成视觉上可区分的状态而不是细微的肢体动作目标检测完全扛得住。2.2 从类别设计反推这个数据集能覆盖什么在 YOLO 标注里行为就是每个框类别 ID 对应的标签。一份真实课堂数据集常见的类别包括站立、举手、低头写字、趴桌、走动有的还会加“阅读”和“玩手机”。这些类别能用一个框区分是因为它们在图像上都有明显可辨的特征站立有完整人形轮廓举手有手臂朝上的方向特征趴桌只有头肩且倾斜低头时头和肩的轮廓关系会变。换句话说这个数据集能覆盖的业务问题是课堂参与度不是体态诊断。这里有一个很容易踩的坑类别定义不一致。比如“举手”这个动作有的标注者把“坐着举手”和“站着举手”分开标有的统一标成“举手”。如果类别拆得过细每类样本量骤减模型收敛难度变大如果合并得太粗又容易把不同姿态混在一个框里。我拿到数据后会先统计每个类别的框数占比如果某两个类别的框数和图像特征高度接近就会考虑在训练阶段把它们合并而不是硬让模型学那些极难区分的边界。2.3 YOLO 标注格式的正确解读txt、归一化坐标和类别编号拿到“已标注、YOLO 标注格式”的数据第一件事就是确认格式是否真的是 YOLO 系而不是 Pascal VOC 或 COCO 的转换。YOLO 格式的规则是每张图像对应一个同名 .txt 文件txt 每一行代表一个目标每行五个字段类别 ID、归一化中心 x、归一化中心 y、归一化宽 w、归一化高 h。坐标值都除以了图像宽高所以落在 0 到 1 之间。下面把一次框的字段拆开看字段位置内容示例含义第1列类别 ID整数2对应类别列表的第 3 个第2列框中心点 x / 图像宽0.5120归一化中心横坐标第3列框中心点 y / 图像高0.3840归一化中心纵坐标第4列框宽 / 图像宽0.2133归一化宽第5列框高 / 图像高0.2850归一化高注意一个常见的翻车点YOLO 格式要求的是中心点坐标加宽高而很多转换脚本给的是左上角坐标加宽高直接训练会导致框的位置整体偏移。拿到数据后先随手看一眼标签内容再动手训练# 查看一张图对应的标签文件前五行 head -5 dataset/labels/classroom_001_000123.txt # 统计所有标签里每个类别的框数确认类别分布没有异常 awk {print $1} dataset/labels/*.txt | sort | uniq -c逻辑说明head 命令只看单文件内容确认字段数是不是 5 列、坐标是不是都在 0~1 之间。awk 命令把 dataset/labels 下所有 txt 的第一列取出来按类别 ID 计数能立刻发现“某个类别框数只有几十个”之类的极端分布。参数说明classroom_001_000123.txt 是示例文件名按你实际目录结构替换awk 的单引号脚本里$1 是行首第一个字段。2.4 真实课堂数据为什么不能拿通用行人数据集替代有的人觉得课堂场景就是“人行为”直接拿 COCO 或 CrowdHuman 训练一个行人检测器再自己写规则判断行为。这个思路表面可行但实际效果很差。通用数据集标注的是人、头、身体没有“举手”“趴桌”这类行为语义你得再套一层规则去猜而规则在透视变形和遮挡面前极其脆弱。更关键的是域匹配问题。课堂场景的摄像头架在教室后方或侧上方视角固定光线偏暖黄桌椅密度高。通用数据集来自街景、室内监控、网络图片成像质量、目标尺度分布和课堂数据差异巨大。我做过对比实验用 COCO 预训练权重直接微调和用课堂数据从零训练前者在验证集上的 mAP 反而低因为预训练分布和微调目标分布打架。相对地标题里这种专门采集的真实课堂数据训练出来的模型在部署时几乎不用做太多的域适应这正是它值钱的地方。3. 用 YOLOv8 跑通课堂行为检测标签校验、数据划分与训练命令3.1 环境准备ultralytics 的安装与版本选择这类数据集最方便的训练路径是用 ultralytics 库里 YOLOv8 或更新版本的模型。常见的热搜问题里“yolov11(ultralytics)环境配置”其实并不是复杂的操作多数翻车在新旧环境里 torch、CUDA 和 opencv 版本互相打架。我建议用 conda 单独建一个环境不要往现有 pytorch 环境里硬塞# 用 conda 隔离环境避免污染已有工程 conda create -n classroom python3.10 -y conda activate classroom # 按机器实际 CUDA 版本选择 torch 索引这里以 CUDA 11.8 为例 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics逻辑说明ultralytics 是当前 YOLO 系列模型训练和推理的统一入口,一条 pip 命令会拉齐依赖。先装 torch 再装 ultralytics能避免 ultralytics 自动拉取一套和你机器不匹配的 torch。参数说明python3.10 是兼容性较好的版本cu118 对应 CUDA 11.8如果你的机器 nvidia-smi 显示支持更高版本可以换成 cu121 或 cu124。CPU 机器不要加 index-url直接装训练速度会明显慢但不影响代码运行。3.2 先写脚本校验标签防止训练半天才发现标注错位拿到数据最容易出现的问题是训练跑了几十轮loss 曲线一直不降结果发现 txt 里坐标没有归一化或者类别 ID 超出了类别总数。这种问题属于数据质量的黑匣子不提前看后面所有时间都白费。我会先跑一遍标签校验脚本import os # 假设数据集目录为 dataset/images 和 dataset/labels一一对应 img_dir dataset/images label_dir dataset/labels num_classes 6 # 先按你的类别清单改这个数字 broken [] for img_name in os.listdir(img_dir): stem, ext os.path.splitext(img_name) if ext.lower() not in (.jpg, .jpeg, .png): continue txt_path os.path.join(label_dir, stem .txt) if not os.path.exists(txt_path): broken.append((img_name, missing label)) continue with open(txt_path) as f: for line in f: parts line.split() if len(parts) ! 5: broken.append((img_name, field count)) continue cls, x, y, w, h int(parts[0]), *map(float, parts[1:]) if cls num_classes: broken.append((img_name, class id overflow)) if not all(0 v 1 for v in (x, y, w, h)): broken.append((img_name, range error)) print(total images:, len(os.listdir(img_dir))) print(broken count:, len(broken)) for item in broken[:20]: print(item)逻辑说明脚本先按文件名遍历图片找同名 txt再逐行拆字段检查字段数、类别区间、坐标是否在 0 到 1 之间。对 11,800 张的规模这个脚本几十秒就能跑完。如果出现 missing label哪怕只有十几张也要在划分数据集时单独排除否则这几张图在验证阶段会被算作漏检拖低整体召回率。参数说明num_classes 要和后面的 data.yaml 里 nc 保持一致ext 白名单可以按你的图像格式扩展。3.3 划分训练集和验证集按时间段分组而不是纯随机行为检测数据如果来自视频抽帧相邻帧之间的画面高度相似。纯随机划分会让同一节课同一秒钟的帧同时出现在训练集和验证集里验证分数虚高部署到新视频上马上露馅。我习惯先按文件名中的时间段前缀分组再在组内划分import os, random, shutil from collections import defaultdict src_img dataset/images src_label dataset/labels out_root dataset_split train_ratio 0.85 random.seed(42) # 假设文件名形如 room01_20250310_090001_000123.jpg取前三段作为视频段标识 groups defaultdict(list) for name in os.listdir(src_img): stem os.path.splitext(name)[0] key _.join(stem.split(_)[:3]) groups[key].append(name) train_names, val_names [], [] for key, names in groups.items(): random.shuffle(names) cut max(1, int(len(names) * (1 - train_ratio))) val_names.extend(names[:cut]) train_names.extend(names[cut:]) # 按划分结果复制图片和标签到新目录 def copy_split(split, names): os.makedirs(f{out_root}/images/{split}, exist_okTrue) os.makedirs(f{out_root}/labels/{split}, exist_okTrue) for name in names: stem os.path.splitext(name)[0] shutil.copy(f{src_img}/{name}, f{out_root}/images/{split}/{name}) shutil.copy(f{src_label}/{stem}.txt, f{out_root}/labels/{split}/{stem}.txt) copy_split(train, train_names) copy_split(val, val_names) print(train:, len(train_names), val:, len(val_names))逻辑说明这里的 key 是“房间号_日期_时间起点”同一段连续画面分到同一组再抽 15% 做验证。这样验证集和训练集在内容上真正独立评估结果更接近部署后的真实表现。用 copy 而不是 move保留原始数据集后面想重新划分不用再下载一遍算是留了一手后悔药。参数说明train_ratio0.85 是个稳妥值如果你的类别很多或者样本量小可以调到 0.9stem.split(_) 里取前几段要按你实际文件名规则改。3.4 训练 YOLOv8data.yaml、训练命令和第一轮观察点数据划分好之后写 data.yaml 文件。这是 YOLO 训练的配置入口路径错了什么都跑不起来# dataset_split 目录下的 data.yaml path: /absolute/path/to/dataset_split train: images/train val: images/val nc: 6 names: [standing, raising_hand, writing, sleeping, walking, reading]逻辑说明path 必须是绝对路径相对路径在 ultralytics 的某些子进程调用下会解析到错误位置。nc 表示类别数量要和校验脚本的 num_classes 一致。names 列表的顺序就是 txt 里类别 ID 的映射顺序ID 为 0 对应列表第一个元素这个顺序错一个位整个训练结果就全错了。训练命令如下cd /path/to/dataset_split yolo detect train \ datadata.yaml \ modelyolov8n.pt \ epochs120 \ imgsz640 \ batch16 \ projectclassroom_runs \ nameexp1 \ patience20逻辑说明modelyolov8n.pt 会下载 COCO 预训练权重做迁移学习。课堂行为在 COCO 里没有直接对应的类别但底层的纹理、边缘、人体特征仍然可以迁移比随机初始化收敛快很多。epochs 设置 120patience20 表示验证指标连续 20 轮不涨就提前停止能省不少时间。参数说明batch16 是 8GB 左右显存的常见选择配 fp16 混合精度可以跑 640 分辨率12GB 以上显存可以试 batch324GB 显存建议先降到 imgsz512 把流程跑通再回到 640 调精度。训练开始后盯两件事前 10 轮 loss 是不是明显下降验证集 mAP 是不是稳步上涨。如果 loss 在某个值附近抖动 20 轮不上不下优先怀疑标签质量问题不要盲目加轮次。3.5 验证指标mAP0.5 和 mAP0.5:0.95 分别告诉你什么训练结束看指标时重点区分这四个数字mAP0.5 是交并比阈值 0.5 时的平均精度反映粗定位能力课堂行为业务里框只需要用来统计数量不用像素级贴合这个值够看就行mAP0.5:0.95 是多个阈值下的平均对框位要求更严论文里常写业务里不用太较真。precision 和 recall 的比值更重要课堂行为检测漏检比误检更难受漏掉一次举手等于漏掉一次互动误检一帧还可以通过后处理滤掉。我会把验证集预测结果按类别单独看 AP。如果“趴桌”AP 比“举手”低 20 个点以上大概率不是模型能力问题而是数据里两类样本数差太多。这部分处理搬到下一章。4. 课堂行为检测的避坑记录遮挡、小目标、类别不平衡与标注漂移4.1 前后排遮挡截断框的宽高比分布泄露了标注质量问题现象模型对后排学生检测效果尚可但前排学生的框经常偏大或左右漂移尤其是坐在第一排靠墙的学生框经常把他身后投影仪屏幕的一角也包进去。原因真实课堂是透视布局后排学生头肩完整前排学生经常被后排同学的头和桌面遮挡。标注者在处理遮挡目标时如果习惯性把框放大到“能包住可见区域”同一个行为类别的框宽高比就会变得极不稳定。模型拟合这种参差不齐的框回归目标定位自然飘。解决训练前先把所有标签按类别统计宽高比分布。如果某个类别的宽高比标准差大于 0.4大概率是标注框风格不一致。已标注数据不好重标我的做法是训练时把 mosaic 数据增强概率调到 1.0让模型每次迭代都见过大量被截断拼接的框形态相当于隐式告诉模型框的可见部分可以是不完整的。同时把 imgsz 提到 768前排目标的像素量增大后回归会更稳。4.2 后排学生只有 60 像素高小目标漏检的三种缓解方案现象验证集里“站立”召回率不错但最后一排的“举手”几乎检不出来预测框全部集中在前三排。原因教室摄像头固定在高位视角覆盖整个教室后排学生在 1080p 原图里经常只有 40×60 像素。640×640 输入分辨率下这类小目标在深层特征图里只剩几个像素语义信息严重衰减。这是小目标检测的经典难题。解决三条路可以叠加。第一把 imgsz 从 640 提到 896对后排小目标的 AP 提升立竿见影代价是训练时间增加三成左右第二推理阶段做 Tiling把 1920×1080 原图切成 3×3 重叠块每块送检测器后在原图上还原框坐标适合离线分析不适合实时第三检查数据增强里的 mosaic 概率调高到 0.8 以上让模型见更多不同尺度下的同一行为。我实际项目里会先用 imgsz896 跑一轮看后排指标不够再上 Tiling。4.3 类别不平衡举手框是趴桌框的 20 倍怎么调现象总框数里“举手”“站立”“低头”占 85%而“趴桌”“走动”占比很低训练后的模型对稀有类别几乎失明。原因真实课堂行为本身就是长尾分布学生一天里多数时间在写字听讲趴桌和走动是少数。标注时如果没有做类别配额控制模型整体的损失会被高频类别主导低频类别学到的东西很少预测时置信度普遍偏低。解决先用 2.3 的 awk 命令统计各类别框数再对症处理。第一个办法是在训练时设置 class_weights按框数反比给稀有类别更高权重第二个办法是对稀有类别做单类别增强水平翻转、小角度旋转、亮度抖动把有效样本量放大第三个办法是推理时对稀有类别降低置信度阈值比如全局 conf0.25“走动”单独用 0.15再用时序滤波消掉误检。需要提醒的是如果稀有类别只有几百个框这些办法都是治标不治本优先补标注或找同类公开数据别指望损失函数权重变出信息量。4.4 标注漂移一个校验脚本比人工抽检更早发现问题现象训练到第 90 轮mAP 突然掉了 8 个点重跑一遍还在同一个位置掉点排除随机种子问题后人工抽帧才发现在某一小时的图像里“玩手机”被大量标成了“低头”。原因人工标注在不同批次、不同班次之间的标准会缓慢漂移。行为边界本身就有模糊地带一个标“玩手机”的标注员和一个标“低头”的标注员面对同样一张图很可能给出不同标签。数据集越大这种漂移越隐蔽人工抽检 5% 往往看出什么问题。解决不要全量人工重检按时间分桶统计类别占比。如果一个时间桶里某类别的比例和全局比例偏差超过 20%就标记出来让人翻帧确认。还有一个更高效的办法先用当前模型预测整个训练集把模型置信度很高但原始标签是另一类别的样本导出来绝大多数是标签错误。我第一次用这个办法定位漂移只花两个小时就锁定了问题段比之前人工两天还没结果快得多。这是把模型当数据质检员用强烈建议养成习惯。5. 从框到行为统计跟踪、课堂参与度曲线与投入产出评估5.1 用 ByteTrack 把帧级检测串成行为段检测器输出的是单帧上的框但课堂上“趴桌 3 秒”和“趴桌 30 秒”的意义完全不同。要得到行为持续时间需要把检测框跨帧关联成轨迹。常见做法是用 ByteTrack 这类无需额外训练的跟踪器我用一个简化伪代码来表示核心思路from collections import defaultdict # tracks 以 track_id 为键保存连续帧的检测结果 tracks defaultdict(list) for frame_id, dets in enumerate(video_dets): for det in dets: tid match_track(det, tracks, iou_thres0.3, max_age30) tracks[tid].append((frame_id, det.cls, det.conf, det.bbox))逻辑说明匹配的核心是相邻两帧之间同一个人框的 IoU在固定摄像头场景下IoU 匹配足够稳定。max_age30 表示目标连续 30 帧没匹配上就终止轨迹。参数说明iou_thres0.3 是低阈值允许一定程度的抖动max_age 越大轨迹越能扛住瞬时遮挡但也会把不同人的轨迹错误拼接30 帧约等于 1 秒比较平衡。5.2 课堂参与度曲线怎么算框轨迹有了就能把行为按业务意义分档站立、举手算积极写字、阅读算中性趴桌、玩手机算消极。按一分钟窗口统计participation_score (积极人数 × 1.0 中性人数 × 0.5 消极人数 × 0) / 当前已检出人数每分钟算一个值滑动窗口画成曲线一整节课的课堂参与度变化一目了然。权重可以根据学校要求调但公式要保持简单方便向业务方解释。5.3 投入产出这条线值不值得做从投入看11,800 张已标注数据足够撑起一个可用的行为检测模型但真正落地时工程量不在训练而在推理链路和业务对接。如果做离线课评晚上跑批任务一台带 RTX 3060 的机器就能处理十几间教室的录像成本低推荐先从这里切入。如果做实时督课要接教室的 RTSP 监控流1080p 分辨率下一边软解一边检测单路实时问题不大但多路并发就得做抽帧策略。业务侧的价值很明确一份可量化的课堂参与度曲线比主观评课更能说服人。我见过不少项目死在不重视标签质量上而这个数据集的标注格式和规模已经把最贵的一步做完了剩下的工程问题都是可控的。回头想想我的习惯是任何行为检测数据到手先花半小时校验标签再花一小时做跟踪和统计的闭环验证能省后面一星期的调试时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表