
简介面向智慧课堂应用场景的YOLOv9学生上课状态识别系统基于YOLOv9-s模型实现在课堂学生状态数据集上平均准确率达99.6%可辅助教师自动分析抬头、低头、玩手机等学习行为适合深度学习课程设计、毕业设计及智慧教室项目二次开发。压缩包共176个文件以Python源码83个py、YAML配置文件、训练好的.pt权重、训练可视化jpg/png图片及评估指标csv为主整体约59.91MB结构清晰便于直接部署或继续调优。已有209人学习下载。资源不仅提供完整的检测与训练代码还附带训练好的YOLOv9-s模型、评估指标曲线和运行教程覆盖数据配置、模型训练、推理测试全流程包含结果预测示例图和标签可视化图方便使用者快速验证效果并理解模型输出。对于需要完成课堂状态检测相关课题的学生或研究者是一份可直接上手的完整方案。1. 线下课堂点名与状态识别为何选YOLOv9课堂里教师关心两件事谁来了以及来了之后在不在状态。传统签到只能解决第一件第二件依赖巡堂时肉眼扫一遍课时一长难免漏看后排。把YOLOv9部署到教室的固定摄像头后面一次性输出每个学生的位置、类别和置信度再按时间轴聚合出“低头了多久、有没有举手、是否在睡觉”这类统计才算是把“状态识别”从技术演示变成能落地的教学辅助工具。选YOLOv9而不是YOLOv8或RT-DETR不是因为它更“新”。YOLOv9在中等算力设备上的推理延迟、显存占用和训练迭代次数的组合对一台带GTX 3060或以上显卡的普通工作站最友好PGI可编程梯度信息机制让深层的梯度回传更完整对“低头看书”和“低头玩手机”这种细粒度姿态差异收敛得比v8更稳。标题里这个方案还打包了训练好的模型和评估指标曲线意味着你拿到的是一套已经跑通闭环的基线而不是需要从零调起的研究原型。它的核心价值是在兼容YOLOv5/v8数据格式的前提下几天内把课堂数据训练成可用的检测系统。下面按“网络结构选型 → 数据标注 → 训练调参 → 指标解读 → 推理落地”五步展开每一步都能直接照做。2. YOLOv9的结构特点与课堂状态检测的耦合点2.1 GELAN骨架与PGI模块在轻量场景下的实际作用YOLOv9对课堂场景的适配关键不在“多准”而在“在低算力下多稳”。GELANGeneralized Efficient Layer Aggregation Network本质上是一种跨层特征聚合策略它把不同深度的特征图通过可学习的权重融合而不像v8的C2f那样靠简单的concat和split。对教室这种目标尺度跨度大的场景——前排学生占画面大块、后排学生只有几十像素——GELAN的好处是浅层细节和深层语义能被同时保留小目标漏检的比例明显下降。PGIProgrammable Gradient Information是另一个决定性设计。它通过辅助可逆分支在训练时给主干补充梯度信号推理时把这个分支丢掉。这意味着你训练时享受的是深层网络的表达能力部署时跑的还是轻量模型。从实际效果看使用PGI后“睡眠”和“低头”这两个类别在特征相近时的区分度会显著提高因为底层卷积核学到了更多姿态边缘信息而不是只依赖肤色和衣物颜色。2.2 从检测头反推课堂状态标签体系目标检测是“框类”课堂状态识别的难点在于类别之间不是互斥的。一个人可以同时表现为“低头”和“看手机”检测头只能给一个最大概率类别所以标签设计需要在标注阶段做取舍。常见的做法是把类别拆成可观测、低歧义的状态集类别名判定标准容易混淆的类别attentive正脸或侧脸朝向前方眼睁开normalsleeping头趴在桌面上或眼闭合bowing_headbowing_head头明显低下但肩膀未落sleepingraising_hand单臂或双臂上举超过肩线noneusing_phone手持设备或目光聚焦于桌面下方bowing_head这个表的核心是“行为可判定”。如果某帧中人既低头又看手机标注为using_phone优先如果无法确定是否手持设备就标bowing_head。这套优先级规则要写进标注规范文档里否则多人协作标注时一致性会崩。从检测头角度反推类别数不建议超过8类。YOLOv9的损失函数包含分类损失和回归损失类别过多而数据不足时分类分支会过拟合到高频类导致“举手”这类低频动作的召回率断崖式下降。宁可把“睡觉”和“趴桌”合并成一类也不要靠堆类别数来追求统计指标好看。2.3 与YOLOv8/RT-DETR的选型对比很多人问既然torch官方都推RT-DETR了还学YOLOv9是不是落后了这个问题需要分级看待。对比项YOLOv9YOLOv8RT-DETR训练收敛速度快PGI辅助中慢小目标能力好中好部署依赖纯PyTorch即可同左需要Transformer算子模型大小中等偏大大源码可读性好好一般对课堂状态检测这类实时性要求高、数据量有限通常几千张的场景RT-DETR的Transformer结构容易在小数据集上欠拟合收敛需要更精细的lr调度。YOLOv8则吃亏在小目标上后排学生的人脸框经常被漏检。YOLOv9是在二者之间的平衡点。3. 课堂状态数据集的标注格式与JSON解析3.1 COCO格式JSON与json.loads解析的落地写法课堂状态检测的数据集格式沿用COCO JSON。目录结构固定为classroom_dataset/ ├── images/ │ ├── train/ # 约80%图片 │ └── val/ # 约20%图片 ├── annotations/ │ ├── instances_train.json │ └── instances_val.json └── classes.txt其中-json文件里最核心的两个字段是categories和annotations。categories定义类别索引annotations里每条记录包含image_id、category_id、bboxx, y, w, h和area。标注工具导出的格式有时不是标准COCO需要写脚本清洗。这里给出一段常用的解析脚本import json with open(annotations/instances_train.json, r, encodingutf-8) as f: data json.loads(f.read()) # 统计每个类别的目标数量 from collections import Counter cat_counter Counter() img_id_to_name {img[id]: img[file_name] for img in data[images]} for ann in data[annotations]: cat_counter[ann[category_id]] 1 print(类别分布, dict(cat_counter)) print(图片总数, len(data[images])) print(标注总数, len(data[annotations])) # 检查是否有空标注图片 annotated_img_ids set(a[image_id] for a in data[annotations]) empty_imgs [img for img in data[images] if img[id] not in annotated_img_ids] print(空标注图片数, len(empty_imgs))逻辑说明用json.loads把整个文件读成Python字典先按category_id统计分布确认没有被某个类别主导到失衡的程度。接着检查空标注图片这些图片要么在训练时被自动忽略浪费了算力要么说明漏标注严重。参数上category_id的编号从1开始如果是标注工具导出时从0开始必须整体加1否则YOLOv9训练时第一个类会被静默跳过。3.2 labelme标注与类别顺序一致性该系统的训练代码采用Darknet风格data.yaml文件里的类别顺序必须与标注工具中的类别顺序完全一致。标注JSON只存数字ID不存文字标签顺序一旦错位模型学到的东西就整体平移了。我的做法是在data/cls.names里固定类别顺序永不更改标注时打开同一个names文件加载标签训练前运行一次上面的统计脚本打印category_id - name映射关系并人工核对。标注框的尺寸也值得注意。课堂图片常见1080p分辨率目标小标注框边缘不要裁剪得过紧建议在目标外扩5%再提交给标注工具复核。太紧的框会让回归分支在训练时反复震荡。3.3 数据增强策略与误检边界课堂场景的特殊性在于静态背景占比高、人物目标重复出现。直接按COCO默认增强跑模型会学到一个坏习惯把固定位置的物体当成背景的一部分。常见的缓解方式是hsv_h: 0.015 hsv_s: 0.5 hsv_v: 0.5 scale: 0.5 fliplr: 0.5 mosaic: 1.0 mixup: 0.2mosaic: 1.0在中间训练阶段能大幅提升小目标检测能力但最后20个epoch应降为0否则目标位置被过度混叠边框回归不稳定。mixup对课堂场景要小人脸与背景混叠严重时姿态特征会被破坏。数据增强的验证有一个技巧增强后的数据要先单独跑一次推理看看“变造”后的图片是否仍能被人工辨认出原本的状态。如果决策性状都被增强抹掉了反而会拖累收敛。4. 训练YOLOv9的完整命令与关键参数调优4.1 源码目录准备与依赖确认拿到训练好的模型和源码包后先确认环境。Python版本推荐3.8到3.11之间PyTorch 1.13或2.x均可CUDA优先用11.x。不要一上来就装最新版torchYOLOv9的某些预训练权重在不同torch版本下加载行为不同。cd yolo9-classroom python -m pip install -r requirements.txt # 可选用清华源加速 # python -m pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txtrequirements.txt里主要包含torch、torchvision、opencv-python、numpy、pandas、seaborn。其中pandas和seaborn是生成评估指标曲线的依赖容易漏装。装完后执行一次版本检查python -c import torch, cv2; print(torch.__version__, cv2.__version__)4.2 训练命令与必须调整的5个参数训练入口是源码中的train.py它从data.yaml读取数据集路径和类别数从yolov9.yaml读取网络结构。最小可运行训练命令如下python train.py \ --data data.yaml \ --cfg models/yolov9.yaml \ --weights yolov9.pt \ --img 640 \ --batch-size 16 \ --epochs 100 \ --device 0 \ --workers 4各参数的选型逻辑参数值说明--img640标准输入尺寸。课堂远端小目标多可尝试768但显存占用会翻倍--batch-size16非A100级别的卡建议8-16否则OOM频繁--epochs100课堂数据量小几千张100轮足够用早停--workers4Windows下别超过4容易踩DataLoader锁的坑--device0单卡训练指定0多卡用0,1,2,3额外要注意的是--patience早停参数。如果验证集mAP连续15个epoch不涨训练会自动停止。课堂数据通常迭代到60-80轮就能收敛后期mAP曲线是平缓的不要因为小波动就手动中断。4.3 Loss曲线怎么看、什么情况该停训练日志会输出四行指标box_loss、cls_loss、dfl_loss、mAP50。前三个是损失值趋势是前10个epoch快速下降再缓慢走平。判断是否过拟合要看验证集和训练集的cls_loss差训练集持续下降、验证集开始反弹说明模型开始背数据而不是学特征课堂场景里特征相对固定这种反弹会在epoch 80后出现此时应立刻停止并用best权重。4.4 训练时的三个高频坑第一个坑是CUDA out of memory。显存不足时不要直接调低batch_size先确认数据加载过程是每张图都是1920x1080原图喂入。数据加载时已经按--img缩放过内存暴涨通常是因为--workers开太高或开了cache选项把整份数据集拷进内存。第二种情况把--workers降到2即可。第二个坑是路径中文。Windows下源码包解压到桌面或新建文件夹这类路径读取权重文件经常报FileNotFoundError。统一放D:\yolo9-classroom这类纯英文路径。第三个坑是预训练权重与配置文件不匹配。yolov9.yaml有n/s/m/c/e几个不同深度的变体加载的yolov9.pt权重如果来自c变体直接套到e变体的配置上第一轮就会崩溃。解法是--cfg和--weights都对应同一个变体名称。5. 评估指标曲线解读与置信度阈值选取5.1 从results.csv提取mAP50与mAP50-95曲线训练完成后有两个产物best.pt和last.pt以及runs/train/exp/results.csv。这个CSV每行是一个epoch的指标是绘制评估指标曲线最直接的数据源。import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(runs/train/exp/results.csv) df.columns [c.strip() for c in df.columns] # 去掉列名首尾空格 # 简化列名原长列名带空格处理一次后续方便 epochs df[epoch] map50 df[metrics/mAP50(B)] map50_95 df[metrics/mAP50-95(B)] plt.figure(figsize(10, 6)) plt.plot(epochs, map50, labelmAP50, linewidth2) plt.plot(epochs, map50_95, labelmAP50-95, linewidth2) plt.xlabel(Epoch); plt.ylabel(mAP); plt.title(YOLOv9 Classroom State Detection: Eval Metrics Curves) plt.legend(); plt.grid(True, alpha0.3) plt.savefig(eval_metric_curves.png, dpi200) print(fBest mAP50: {map50.max():.3f} epoch {map50.idxmax()}) print(fBest mAP50-95: {map50_95.max():.3f} epoch {map50_95.idxmax()})逻辑说明先给列名做strip是因为Ultralytics系代码生成的CSV列名首尾带空格直接pandas读取后列名会多出空格。metrics/mAP50(B)是IoU阈值0.5的mAP对课堂状态检测参考价值最高mAP50-95是0.5到0.95的均值更严苛主要用来横向对比不同模型很难作为业务上的验收线。如果两条曲线都贴近0.5甚至更低先别调网络回去看第3章的类别分布统计。类别严重不平衡时mAP会被高频类拉起来低频类比如举手被完全淹没。目标检测的mAP在类别间差异大于0.3时说明低频类需要加样本或换损失权重。5.2 PR曲线与混淆矩阵的实战价值除了mAP曲线验证脚本还会输出PR_curve.png和confusion_matrix.png。PR曲线是Precision-Recall的权衡轨迹曲线越靠近右上角越好每个类别画一条可以精准定位哪个状态识别最弱。课堂场景的背景类别是隐式的如果混淆矩阵里active类大量被预测为dealing_phone说明标注规范里对“看手机”的优先级定义不一致需回头处理标注而不是继续增加训练轮数。5.3 基于验证集找最优置信度阈值训练好的模型默认置信度阈值是0.25这个值不一定适合课堂场景。教室监控距离远目标小0.25会产生大量误检把背景里某个静态物体当成“举手”。正确的做法是用验证集扫一遍from pathlib import Path import torch model torch.hub.load(, custom, pathruns/train/exp/weights/best.pt, sourcelocal) # 若无法加载hub模型改用源码里的detect.py但扫描阈值逻辑可复用 val_images list(Path(dataset/val/images).glob(*.jpg)) for conf_thr in [0.15, 0.25, 0.35, 0.45, 0.5]: tp fp 0 for img_path in val_images[:50]: results model(str(img_path), confconf_thr) # results.pandas().xyxy[0] 是包含类别、置信度的DataFrame for *_, conf in results.pandas().xyxy[0].itertuples(): if conf conf_thr: tp 1 else: fp 1 print(fconf{conf_thr:.2f} tp{tp} fp{fp} precision{tp/(tpfp):.2f})这个脚本的思路是把符合真实标注的检测视为正样本超出的视为误检。对课堂场景我一般把置信度设在0.35到0.45之间宁可漏检个别“睡觉”的也不要让后排有人举手时被误判成“玩手机”。参数conf直接影响NMS前的筛选同一模型在不同conf下框的数量变化不是线性的实测扫一遍比凭经验猜更快。6. 本地加载训练好的模型做实时推理与状态统计落地环节的核心是把best.pt接到detect.py或自定义推理逻辑中。以下是集成后的推理脚本import cv2 from pathlib import Path import pandas as pd # 加载本地模型 model torch.hub.load(, custom, pathruns/train/exp/weights/best.pt, sourcelocal) model.conf 0.4 model.iou 0.5 # 类别顺序来自数据集配置文件 class_names [active, sleeping, bowing_head, raising_hand, using_phone] cap cv2.VideoCapture(classroom_demo.mp4) fps cap.get(cv2.CAP_PROP_FPS) stat {name: 0 for name in class_names} total_frames 0 while cap.isOpened(): ret, frame cap.read() if not ret: break total_frames 1 results model(frame) dets results.pandas().xyxy[0] for _, row in dets.iterrows(): cls int(row[class]) stat[class_names[cls]] 1 / fps # 按秒累计时长 cap.release() # 输出课堂状态统计 total_time total_frames / fps for k, v in stat.items(): print(f{k}: {v:.1f}秒 (占比 {v/total_time*100:.1f}%))代码说明torch.hub.load的source设为local时只从本地路径加载权重不需要联网。model.conf和model.iou在推理前直接赋值模型内部拿这两个阈值过滤候选框。统计逻辑用1 / fps把帧计数换算成秒数更适合后续汇报“课堂上学生低头时长为多久”。进阶用法还体现在状态聚合上。逐帧帧统计会因检测抖动产生毛刺建议滑窗聚合取每10帧的众数作为该时间片的状态。这个技巧在课堂场景能消除偶尔一两帧跳变让最终统计曲线稳健。导出统计结果后再把每帧的检测结果写入CSV就能用pandas做“按分钟聚合的学生专注度报告”贴合线下课堂的行政统计需求。训练好的模型已经内置类别名配置加载后直接调用模型会输出类别索引而非文本提前检查data.yaml里的names顺序确保索引映射正确。整套链条从源码到评估曲线再到实时推理已经闭环后续要扩展新状态类别只需在classes.txt追加标签并重新训练。本文还有配套的精品资源点击获取