ARTICLE DETAIL

资讯详情

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

基于YOLOv9的课堂学生状态识别:从标注到部署全流程

基于YOLOv9的课堂学生状态识别:从标注到部署全流程 简介本资源面向深度学习入门者、智慧课堂方向毕业设计学生及计算机视觉开发者提供一套基于YOLOv9-s的线下课堂学生上课状态识别检测系统完整方案可识别学生听课、低头、举手等学习状态帮助快速搭建可运行的目标检测项目。压缩包共176个文件约59.91MB以83个Python源码、30个YAML配置文件、18个编译缓存文件为主另含17张JPG与9张PNG评估指标曲线图、3个pt权重文件、5个XML标注及CSV训练日志覆盖训练、验证、推理全流程。资源内附训练过程截图YOLOv9-s模型平均准确率达99.6%并配有运行教程从环境配置、数据集准备、参数修改到训练与测试均有说明读者可据此复现训练曲线、替换自有数据集并完成检测效果验证。目前已有209人学习适合需要完整源码、预训练模型与评估曲线的课程项目或毕设参考。1. 从一张教室监控截图说起YOLOv9 课堂状态识别到底在做什么一间普通高校教室摄像头装在讲台斜上方画面里四十多个学生有人抬头看黑板有人低头写字有人趴在桌上还有人侧头和同桌说话。如果让你用肉眼盯十分钟你能大致判断谁在听课、谁在走神但让你盯一整个学期、几十间教室人力根本扛不住。基于 YOLOv9 实现的线下课堂学生上课状态识别检测系统要解决的就是这件事把「抬头听讲、低头看书、趴桌睡觉、转头说话」这几类课堂行为变成目标检测模型能稳定输出的类别标签再配上 Python 源码、运行教程、训练好的模型和评估指标曲线让一个没做过检测项目的人也能在本地把整套流程跑起来。它适合三类人一是做课程设计或毕业设计的学生需要一个能演示、能讲清指标、能改类别的完整工程二是想入门 YOLO 系列目标检测的 Python 开发者需要一个真实场景而不是 COCO 玩具数据集三是教育信息化方向的技术人员想评估「课堂行为自动分析」这件事在现有硬件和标注成本下到底能做到什么程度。核心词 YOLOv9、python、模型、训练、评估指标会贯穿全文我不会只讲概念而是把数据怎么标、模型怎么训、指标曲线怎么看、翻车点在哪一条条摊开。2. 课堂状态识别的类别设计与 YOLOv9 选型理由2.1 为什么是四到六类而不是「认真/不认真」两类很多人第一反应是二分类认真和不认真。真做起来就会发现这个划分没法标。一个学生低头可能是在记笔记也可能是在玩手机从俯拍或斜拍画面里根本区分不了。所以课堂状态识别通常按可见姿态来定类别而不是按心理状态。常见做法是四类抬头听讲head_up、低头书写或看书head_down、趴桌lying、转头或侧身turn_head。如果摄像头角度好、分辨率够可以再加「举手」和「站立」两类变成六类。类别设计直接决定后面标注成本和模型难度。四类里「抬头」和「低头」的区分主要靠头部朝向和肩部姿态「趴桌」靠人体框的宽高比和位置「转头」最难因为侧脸和正脸在低分辨率下特征接近。我一般建议第一版就做四类把「转头」和「低头」的边界先跑通再考虑加类。类别名一旦定下后面数据集目录、data.yaml、评估曲线全是围绕它转的中途改类等于重标。2.2 YOLOv9 相比 v8、v5 在课堂场景的实际差异YOLOv9 的核心改动是可编程梯度信息PGI和 GELAN 结构官方论文里的卖点是参数效率和信息保留。落到课堂场景我关心的是三件事小目标召回、遮挡下的稳定性、训练显存占用。教室里人头在 1080P 画面里通常只占 60×80 到 120×150 像素属于中小目标后排学生互相遮挡严重一张 24G 显存的卡要能跑 batch 16 以上才划算。实际对比下来YOLOv9c 在同样数据集上比 YOLOv8m 的 mAP50 高 1 到 3 个点主要体现在「低头」和「转头」这两类容易混的类别上因为 PGI 对浅层特征的保留更好。代价是训练时显存比 v8 同级别高一些推理速度略慢。如果只是做课程演示YOLOv9t 或 YOLOv9s 足够如果要追求指标好看用 YOLOv9c 或 YOLOv9e但要注意显存。选型没有绝对答案关键看你更在意「跑得动」还是「指标高」。2.3 数据集目录结构与 data.yaml 的最小配置YOLO 系列的数据集组织方式是固定的images 和 labels 分开放train/val 再分。课堂数据集我一般这样建dataset/ ├── images/ │ ├── train/ # 训练图片jpg 或 png │ └── val/ # 验证图片 ├── labels/ │ ├── train/ # 与图片同名的 txt │ └── val/ └── data.yamldata.yaml 是训练入口字段不能写错# data.yaml path: ./dataset # 数据集根目录相对或绝对都行 train: images/train # 相对 path 的训练图片目录 val: images/val # 相对 path 的验证图片目录 nc: 4 # 类别数必须和下面 names 长度一致 names: 0: head_up # 抬头听讲 1: head_down # 低头书写/看书 2: lying # 趴桌 3: turn_head # 转头/侧身这里最容易翻车的是nc和names对不上或者 labels 里的类别索引超出范围。YOLO 的标签格式是class_id x_center y_center width height全部归一化到 0 到 1。如果标注工具导出的是 VOC 的 xml 或 COCO 的 json必须先转换不能直接丢进去训。转换脚本后面第 3 章会给。提示data.yaml 里的 path 建议用绝对路径调试一次确认能读到图再改回相对路径能省掉一半「找不到图片」的报错。3. 从标注到训练把课堂数据集喂进 YOLOv9 的完整链路3.1 标注规范什么算一个框边界怎么定课堂场景标注最大的争议是「框到哪」。我的经验是以人体可见部分为准头部和躯干都要包含被桌子挡住下半身时框到桌面边缘即可不要凭想象补全。趴桌的学生如果整个人趴在桌上框住可见的背部和头部如果只露出一个头就只框头但要在标注文档里写清楚否则不同标注员标准不一致模型学出来就是玄学。「转头」这一类尤其要注意侧脸超过 45 度才算轻微偏头不算否则和「抬头」的边界会糊掉。标注时建议用 LabelImg 或 X-AnyLabeling导出 YOLO txt 格式。每张图标注完抽查一遍重点看有没有漏标后排小目标——漏标比错标危害更大模型会把漏标的人当成背景学进去。3.2 VOC/COCO 转 YOLO 格式的转换脚本很多现成课堂数据集是 VOC 格式需要转成 YOLO txt。下面这个脚本处理 VOC xmlimport os import xml.etree.ElementTree as ET # 类别名到索引的映射必须和 data.yaml 的 names 顺序一致 CLASS_MAP {head_up: 0, head_down: 1, lying: 2, turn_head: 3} def convert_voc_to_yolo(xml_dir, out_dir, img_w, img_h): os.makedirs(out_dir, exist_okTrue) for xml_file in os.listdir(xml_dir): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(xml_dir, xml_file)) root tree.getroot() # 有些数据集 size 字段缺失需要从图片实际尺寸读这里假设存在 size root.find(size) w int(size.find(width).text) if size else img_w h int(size.find(height).text) if size else img_h lines [] for obj in root.iter(object): name obj.find(name).text.strip() if name not in CLASS_MAP: continue # 跳过不在类别表里的目标 cls_id CLASS_MAP[name] bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) # 转成归一化的中心点 宽高 x_c (xmin xmax) / 2.0 / w y_c (ymin ymax) / 2.0 / h bw (xmax - xmin) / w bh (ymax - ymin) / h # 裁剪到 0-1防止标注越界导致训练报错 x_c, y_c min(max(x_c, 0), 1), min(max(y_c, 0), 1) bw, bh min(max(bw, 0), 1), min(max(bh, 0), 1) lines.append(f{cls_id} {x_c:.6f} {y_c:.6f} {bw:.6f} {bh:.6f}) out_path os.path.join(out_dir, xml_file.replace(.xml, .txt)) with open(out_path, w) as f: f.write(\n.join(lines)) if __name__ __main__: convert_voc_to_yolo(./voc_xml, ./labels/train, 1920, 1080)逻辑说明脚本遍历 xml读每个 object 的类别和 bbox按图片宽高归一化。CLASS_MAP必须和 data.yaml 的 names 索引完全一致否则模型学到的类别会错位。img_w/img_h是兜底参数当 xml 里没有 size 字段时用。归一化后做了 0 到 1 的裁剪因为标注越界比如 xmax 超过图片宽度会让 YOLO 训练直接报错或产生异常框。参数上xml_dir是 VOC 标注目录out_dir是输出 txt 目录要和 images 目录结构对应。3.3 训练命令与关键超参epochs、imgsz、batch 怎么定YOLOv9 官方仓库的训练入口是 train.py 或 yolo 命令行。假设你已经装好 ultralytics 或克隆了官方仓库最小训练命令# 单卡训练模型用 yolov9c数据配置指向 data.yaml python train.py \ --weights yolov9c.pt \ # 预训练权重没有就从头训但收敛慢 --data data.yaml \ # 数据集配置 --epochs 150 \ # 课堂数据一般 100-200 轮 --imgsz 640 \ # 输入尺寸小目标多可上 960 --batch 16 \ # 按显存调24G 卡 16 比较稳 --device 0 \ # GPU 编号 --project runs/train \ # 输出目录 --name classroom_v1 # 本次实验名参数说明epochs不是越大越好课堂数据集通常几千张150 轮左右验证指标就平了再训容易过拟合。imgsz是输入分辨率640 是默认如果后排小目标召回差可以提到 960但显存和训练时间会明显上升。batch受显存限制跑不动就降到 8 或 4但 batch 太小 BN 层统计不稳指标会抖。weights用预训练权重能显著加快收敛从头训在几千张图上很难到理想指标。训练过程中重点看runs/train/classroom_v1/下的 results.csv 和 weights 目录。results.csv 每轮记录 loss 和 mAP是后面画评估曲线的数据源。如果 loss 一直不降先查标签格式如果 mAP 震荡查 batch 和学习率。3.4 评估指标曲线怎么读mAP50、mAP50-95、PR 曲线训练完会生成混淆矩阵、PR 曲线、F1 曲线等。课堂场景我重点看三个指标含义课堂场景参考值mAP50IoU0.5 时的平均精度四类一般 0.85 以上算可用mAP50-95IoU 从 0.5 到 0.95 的平均通常比 mAP50 低 10-20 个点各类 PR 曲线每类的精度召回权衡看「转头」类是否明显偏低mAP50 高但 mAP50-95 低说明框的位置不够准可能是标注框松紧不一致。PR 曲线里如果「转头」类的曲线明显塌陷说明这类样本少或和「抬头」混了需要补样本或重新定义边界。评估指标曲线不是拿来看好看的是拿来定位问题的哪一类拖后腿就去补哪一类的数据和标注。4. 推理部署与课堂状态统计的落地细节4.1 用训练好的模型跑单张图和视频训练完的权重在runs/train/classroom_v1/weights/best.pt。推理单张图from ultralytics import YOLO # 加载训练好的课堂状态模型 model YOLO(runs/train/classroom_v1/weights/best.pt) # 单张图片推理conf 是置信度阈值 results model.predict( sourcetest_classroom.jpg, conf0.4, # 低于 0.4 的框丢弃课堂场景可适当调低 iou0.5, # NMS 的 IoU 阈值人多时调低减少重叠框 imgsz960, # 和训练时接近太小会丢小目标 saveTrue # 保存带框的结果图 ) # 打印每个框的类别和坐标 for box in results[0].boxes: cls_id int(box.cls) print(model.names[cls_id], box.xyxy.tolist(), float(box.conf))逻辑说明conf控制召回和误检的平衡课堂场景宁可多检一点也别漏可以设 0.3 到 0.4。iou是 NMS 阈值学生密集时框重叠多调低到 0.45 能减少重复框。imgsz推理时最好和训练一致训练用 960 推理用 640 会掉点。视频推理把 source 换成视频路径即可但要注意逐帧推理速度1080P 视频在单卡上大概 20 到 40 FPS取决于模型大小。4.2 从检测框到「课堂状态统计」的转换检测出框只是第一步真正有用的是统计。比如一帧里有多少人抬头、多少人趴桌整节课的抬头率曲线怎么变。做法是按帧统计各类别数量再按时间聚合import cv2 from collections import defaultdict def analyze_video(video_path, model, sample_fps1): cap cv2.VideoCapture(video_path) fps cap.get(cv2.CAP_PROP_FPS) interval int(fps / sample_fps) # 每秒抽 sample_fps 帧降低计算量 frame_id 0 stats defaultdict(list) while True: ret, frame cap.read() if not ret: break if frame_id % interval 0: res model.predict(frame, conf0.4, imgsz960, verboseFalse)[0] count defaultdict(int) for box in res.boxes: count[model.names[int(box.cls)]] 1 total sum(count.values()) or 1 # 记录每一类的占比 for k in [head_up, head_down, lying, turn_head]: stats[k].append(count.get(k, 0) / total) frame_id 1 cap.release() return stats逻辑说明sample_fps控制抽帧频率课堂状态变化慢每秒 1 帧足够能大幅降低计算量。统计的是各类别占当前帧总人数的比例而不是绝对数量这样不同教室人数不同也能横向比较。返回的 stats 可以画成时间序列曲线抬头率下降、趴桌率上升的时段就是注意力低谷。参数上conf和推理时一致interval由视频帧率和采样率算出。4.3 部署时的性能与显存取舍如果只是离线分析录播视频用 YOLOv9c 960 输入没问题。如果要实时处理多路摄像头就得降模型或降分辨率。我的经验是单路 1080P 实时YOLOv9s 640 能到 50 FPS 以上四路以上建议用 YOLOv9t 或做帧间跳采。显存方面推理比训练省很多YOLOv9c 推理 1080P 大概占 2 到 3G训练时 batch 16 要 18G 以上。部署前先用nvidia-smi看显存别等跑起来才 OOM。注意实时多路场景不要每帧都跑检测用抽帧 跟踪如 ByteTrack能省大量算力但跟踪会引入 ID 切换问题课堂统计里要权衡。5. 避坑与排查课堂状态识别最常见的五个翻车点5.1 指标很高但实际画面全是误检现象验证集 mAP50 到 0.9但拿真实教室视频一跑墙上海报、椅子靠背都被框成「低头」。原因验证集和训练集同分布都是标注过的干净图模型没学过负样本。解决在训练集里加入不含学生的空教室图、含干扰物的图作为背景负样本或者提高推理 conf 阈值到 0.5 以上先压误检再补数据。5.2 「转头」和「抬头」永远分不开现象混淆矩阵里这两类互相误判严重PR 曲线都低。原因标注边界模糊侧脸 30 度和 50 度都被标成不同类模型学不到稳定特征。解决重新定义边界比如以鼻子是否超出肩膀轮廓为准重标一批数据或者干脆合并成「非低头」一类减少类别数。5.3 训练 loss 正常但 mAP 一直是 0现象loss 在降但验证 mAP 始终为 0 或极低。原因九成是标签路径或格式问题比如 labels 目录和 images 目录没对应、txt 里类别索引超范围、坐标没归一化。解决写个小脚本抽查几个 txt确认每行 5 个值、第一个是整数且在 nc 范围内、后四个在 0 到 1 之间。5.4 换教室后指标断崖式下跌现象在 A 教室训的模型放到 B 教室召回率掉一半。原因摄像头角度、光照、桌椅布局不同属于域偏移。解决要么在多个教室的数据上混合训练要么做在线微调用 B 教室少量标注数据继续训几十轮。没有万能模型跨域是检测落地的常态。5.5 显存够但训练中途崩现象训练跑几十轮后报 CUDA out of memory。原因不是显存不够是数据加载或缓存泄漏或者验证阶段 batch 没设小。解决把val阶段的 batch 调小检查 dataloader 的 workers 是否过多导致内存涨必要时加--cache ram或关掉缓存。6. 进阶技巧用评估曲线反推标注质量而不是只调参大部分人拿到评估指标曲线第一反应是调学习率、换模型、加 epoch。我踩过的坑告诉我课堂状态识别里指标上不去的头号原因不是超参是标注质量。PR 曲线和混淆矩阵其实是标注问题的黑匣子记录仪只是很多人不会读。具体做法训练完先别急着调参打开混淆矩阵找误判最多的那一对类别。比如「head_down」被大量判成「lying」就去翻这些样本的原图大概率会发现趴桌和低头的标注边界在你们团队里根本没统一。这时候调参是白费重标 200 张边界样本再训一轮指标往往比调参涨得多。我一般会做一个「标注一致性抽查」随机抽 50 张图让两个人独立标算一下框的 IoU 和类别一致率低于 0.85 就先统一标准再训。另一个技巧是用验证集的预测结果反查漏标。把模型在验证集上的高置信度预测框和原标注对比如果模型框出了一个标注里没有的人很可能是漏标。把这些图挑出来补标相当于用模型帮你找标注漏洞。这个循环做两三轮指标和实际画面表现都会明显变好。最后一个习惯每次训练都把 data.yaml、超参、git commit 记在一个小本子上别只靠记忆。课堂数据集一改之前的指标就不可比了没有记录你会陷入「上次那个 0.9 是怎么来的」的后悔药困境。这套流程不复杂难的是每次都老老实实做。希望帮到你。本文还有配套的精品资源点击获取
返回列表