简介面向学生课堂行为分析与智慧教育场景这份已标注的YOLO格式目标检测数据集覆盖低头、转头两个类别可用于学生上课状态识别、课堂参与度评估及教学辅助系统开发。数据共约2400张课堂图像已按训练集、验证集划分节省了数据整理与拆分流程标签自动区分低头、转头两类行为为模型提供清晰的监督信号。1999个txt标签文件采用标准YOLO格式记录目标类别与归一化框坐标可直接用于YOLOv5、YOLOv8等主流检测网络的训练与验证。配套的show.py可视化脚本可快速生成带标注框的预览图方便检查标签是否准确尤其适合数据清洗与效果抽查。资源包整体约298.38MB压缩后文件结构简单适合目标检测入门学习与算法对比实验。作者还提供了YOLOv5改进实战专栏及更多视觉项目可在此基础上进一步做模型轻量化或精度优化。目前已有129人学习下载适合从事课堂行为识别、智慧教学研发的算法工程师和学生研究者使用。1. 学生低头、转头行为检测2,400 张 YOLO 格式数据够不够用教室摄像头对着几十个学生老师不可能每分钟都盯住每个人的状态。学生上课低头、转头行为检测要解决的就是把“谁在低头、谁在转头”这件事从人眼盯变成程序盯最直接的实现方式是图像目标检测对每一帧画面里的学生头部画框并给出框对应的行为标签。这套 2,400 张 YOLO 格式标注数据按每张图片里平均 38 个学生来算头部样本量大致在万级对两个类别的行为检测来说是一个完全能起步的量级前提是你把数据校验、标注规范和训练参数都处理到位。本文按“解读格式 → 校验数据 → 训练模型 → 调阈值 → 避坑 → 做统计”的顺序把这条路走一遍。2. 读懂 YOLO 格式标注两个类别的行为检测比想象中多一点门道2.1 YOLO 标注格式的三要素类别编号、归一化中心点、宽高YOLO 标注格式的核心是一条文本记录每张图片对应一个同名的 .txt 文件文件名和图片名一致后缀不同。文件里每一行代表一个目标格式是固定的四段式类别编号、归一化后的中心点 x、中心点 y、归一化后的宽 w、高 h。归一化的意思是 x、y、w、h 都除以图片的宽高取值范围落在 0 到 1 之间。这样做的好处是不同分辨率的图片可以共用同一套标签训练时无论输入 640 还是 960坐标都能直接映射回去。这里最容易踩的第一个坑是坐标系的中心点还是左上角。很多人从 COCO 的 JSON 格式转过来习惯性地把左上角坐标和宽高填进 YOLO 的 txt模型训练出来框全是偏的。YOLO 格式里的坐标永远是目标的中心点不是框的左上角。用 OpenCV 读取图片之后把目标框从 (x1, y1, x2, y2) 转成 YOLO 格式的代码写法是固定的import cv2 import os # 原标注来自其他格式例如 (left, top, right, bottom) def convert_to_yolo(img_path, box, class_id): img cv2.imread(img_path) h, w img.shape[:2] x_center (box[0] box[2]) / 2 / w y_center (box[1] box[3]) / 2 / h box_w (box[2] - box[0]) / w box_h (box[3] - box[1]) / h return f{class_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f} # 写入 txt # with open(label_path, w) as f: # f.write(line \n)这段代码做的事情就是把任意来源的框坐标换算成归一化中心点加归一化宽高。注意box[2] - box[0]算的是框宽不要习惯性地写box[2] - box[0] 1在归一化大坐标下多一个像素误差不明显但在小目标上会影响 IoU 计算。转换完建议立刻抽查三张图把标注还原回图上画框确认没有错位再进训练流程否则数据量越大错得越稳。2.2 低头与转头的标注边界这两类不是互斥关系目标检测里的每个框只有一个类别但学生的低头和转头在物理上是可以同时发生的——低头看手机时头稍微偏左这个框到底是head_down还是head_turn标注规范必须提前定死。否则 2,400 张图可能标出两种风格模型训练时同一形态的样本被分到两个类别里类别内特征不一致mAP 上不去。我的做法是给这两个类别定义一个优先级低头优先。只要头部姿态明显向下倾斜能观察到下巴收起或额头朝向下方就标成head_down转头是指头部水平方向明显转向左右且同时能看到脖颈侧面或后脑勺的形态。两者都沾边时按低头算。这个规则的好处是类别之间的边界清晰模型不用在同一张脸上反复纠结是横转还是下俯收敛更快。另一个常见边界问题是头部框要不要包含脖子。建议只框头部主体从发顶到下巴不包含肩膀和躯干。包含的像素越多类别间相似度越高误检率越大。2.3 训练之前先写脚本统计这批数据的分布和坐标异常拿到 2,400 张 YOLO 格式标注不要急着开训。先做一次数据体检重点看三类问题类别是否平衡、坐标是否越界、是否存在空标签文件。教室场景里低头和转头的频次天然不均衡有些图里可能全是低头转头样本整体偏少这时训练出来的模型会对转头类别偏保守漏检概率高。写一个脚本把整个数据集的标注扫一遍import os from collections import Counter label_dir labels class_names {0: head_down, 1: head_turn} counts Counter() empty_files [] out_of_range [] for fname in os.listdir(label_dir): if not fname.endswith(.txt): continue path os.path.join(label_dir, fname) lines [l.strip() for l in open(path) if l.strip()] if not lines: empty_files.append(fname) continue for line in lines: parts line.split() if len(parts) ! 5: print(f格式异常: {fname} - {line}) continue cid int(parts[0]) x, y, w, h map(float, parts[1:]) counts[cid] 1 if x 0 or y 0 or w 0 or h 0 or x 1 or y 1 or x w/2 1.02 or y h/2 1.02: out_of_range.append((fname, line)) print(类别统计:, {class_names[k]: v for k, v in counts.items()}) print(空标签文件:, len(empty_files)) print(坐标越界记录:, len(out_of_range))这个脚本的价值在于处理小数据集的“慢性病”。越界记录我留了 1.02 的容差因为有些标注工具会画出超出图片边界几个像素的框严格按超过 1 就报错误报太多。但容差不能超过 1.05超过这个值的框说明归一化计算时宽高用错了分母必须往回查。空标签文件也要重视如果一张教室全景图里没有任何标注它有可能是标签确实为空也有可能是标注保存不完整。我的习惯是统计完之后跑一遍可视化程序把 20 张随机图片的标注画出来人工过目比任何脚本都管用。3. 用 YOLOv8 在本地训练低头/转头检测环境配置、数据集组织与最小命令3.1 环境配置与目录结构先跑通再调参训练行为检测模型常见的选择是 YOLOv8。虽然 YOLOv11 和 YOLOv6 都有社区版本但 YOLOv8 的生态最成熟遇到问题搜索解决方案最容易部署文档也最齐。环境配置上用 Anaconda 新建虚拟环境是大多数人验证过的路径不会污染系统 Python。显卡按需选择没有 NVIDIA GPU 也可以用 CPU 跑只是 2,400 张图、100 个 epochCPU 可能要跑十几个小时GPU 十几分钟到半小时。conda create -n yolo python3.10 -y conda activate yolo pip install ultralyticsultralytics这个包把训练、验证、导出都封装进了一个命令行入口装好之后就算环境齐全了。紧接着把数据集整理成 YOLO 要求的目录结构dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── dataset.yamltrain 和 val 的比例我一般按 8:2 切2400 张数据留 480 张做验证已经足够看出模型真实水平。划分时要注意按教室或按时间片切避免同一批学生在相邻帧同时出现在训练集和验证集那会让验证指标虚高后面部署到新教室时直接掉点。我的常用做法是先把所有文件名打乱再按比例切分同时保证同一个学生的连续帧尽量分到同一边简单有效。3.2 dataset.yaml 与训练命令参数从默认值开始dataset.yaml是告诉模型去哪里读数据、分几类、类名是什么的配置文件内容如下path: ./dataset train: images/train val: images/val nc: 2 names: 0: head_down 1: head_turnnc必须是类别数names的索引从 0 开始这两处一定要和标签文件里的类别编号严格对应否则训练不报错但预测出来的类别含义全错。配置写好后最小训练命令是yolo detect train \ datadataset.yaml \ modelyolov8s.pt \ epochs100 \ imgsz640 \ batch16 \ patience20字段含义分别是modelyolov8s.pt使用 COCO 预训练权重作为起点epochs100是最大训练轮数实际上大概率在 50 到 80 轮就触发早停imgsz640是输入分辨率batch16是每批样本数patience20表示 20 轮验证指标没有提升就停止训练。先解释参数选择再讨论细节。2,400 张数据用 COCO 预训练模型做迁移学习是必须的随机初始化权重在这个数据量下很难收敛预训练权重相当于让模型先认识“头部”这个通用概念再微调去分辨低头和转头。分辨率 640 是性价比折中教室场景下后排学生头部可能只有 30 到 40 像素后续如果发现小目标漏检可以把imgsz提到 960代价是训练和推理速度都变慢。batch按显存调节显存不足时降到 8但不要低于 4否则 BN 层的统计量不稳定损失曲线会像锯齿。优化器默认用auto它会根据模型和数据自动选择多数情况下是 AdamW新手不要乱改先跑通再说。3.3 训练过程中盯什么损失曲线、mAP、早停训练开始后终端会持续输出每轮的损失值和指标重点观察两组内容。第一组是train/box_loss、train/cls_loss和对应的val/box_loss、val/cls_loss如果训练损失在下降而验证损失在第 40 轮左右开始回升说明模型开始过拟合靠早停会自动挑出验证指标最好的权重。第二组是metrics/mAP50(B)和metrics/mAP50-95(B)前者是 IoU 阈值 0.5 下的平均精度后者是 0.5 到 0.95 的均值。对行为检测来说mAP50 是最直接的参考能做到 0.85 以上这个模型已经具备实用价值。训练完成后模型会保存两份权重best.pt和last.ptbest 是验证集上表现最好的那一轮。之后所有验证和部署都使用best.ptlast.pt只在你想恢复中断训练时才有意义。这是我被坑过一次后养成的习惯——有次图省事用了 last.pt 做推理结果那恰好是过拟合最严重的一轮误检多到没法看。4. 验证与阈值调优把检测结果接到行为统计之前先解决两个核心问题4.1 混淆矩阵怎么看低头和转头为什么会互相错跑完训练先做一次正式验证把混淆矩阵和类别指标导出来yolo detect val \ modelruns/detect/train/weights/best.pt \ datadataset.yaml验证完成后会在结果目录里生成confusion_matrix.png和results.csv。对二分类目标检测来说混淆矩阵里除了两个类别还有 background 类总共三行三列。最需要警惕的现象是head_down的样本被预测成head_turn的比例偏高这通常不是模型问题而是标注时边界定得不够死。低头和转头在视觉上有交叉区域头下低的同时略带侧转标注规范里“低头优先”的策略在训练集里贯彻得越彻底混淆就越少。如果混淆矩阵显示误分类集中出现在某个类别可以回到标注阶段把有争议的样本重新归档到优先级更高的类别然后重训。results.csv里逐行记录了每个类别的 precision、recall、mAP50。低头类别 recall 低表示很多低头行为没被框出来对策是检查是不是小目标漏检或者标注框是否过紧。转头类别 precision 低表示把大量不是转头的框标成了转头对策是提高置信度阈值或者增加负样本。这些判断逻辑适用于大多数目标检测调优场景不只是行为检测。4.2 置信度阈值和 IoU 阈值行为统计场景里宁可漏检不要误检目标检测模型输出的每个框都带一个置信度分数默认 0.25 的阈值对通用检测场景比较友好但课堂行为统计的场景不同。误检的代价比漏检大漏检一个低头最多是这一帧少算了几秒误检一个低头会把整节课的低头时长统计抬高而且误检往往是持续性的同一个人可能连续几十帧被误检对统计结果的影响会被放大。所以我通常把 conf 阈值调到 0.35 到 0.4如果验证时 precision 和 recall 差距很大还可以继续往 0.5 调。yolo detect predict \ modelruns/detect/train/weights/best.pt \ sourcetest_video.mp4 \ conf0.4 \ iou0.5 \ imgsz640 \ saveTrueconf0.4是只保留置信度大于 0.4 的框iou0.5是 NMS 判断两个框是否重叠的阈值。iou一般保持 0.5 不动调高会让密集人群里的重叠框更难被合并调低又可能导致一个目标输出两个框教室场景里学生肩并肩坐着iou高于 0.6 时相邻学生容易被合并成一个框低于 0.4 时同一个头可能出现重复框。除非发现明显的重复检测否则这个参数不需要动。真正值得反复试的是conf上课场景的统计任务建议在最开始用 0.4然后在验证集上遍历 0.3、0.4、0.5 三个值比较它们的 precision 和 recall挑一个对统计误差影响最小的平衡点。5. 课堂行为检测避坑标注格式、特征混淆与部署时最容易翻车的五个点标签文件里有负数坐标或者远超边界的坐标现象训练 loss 在初期就出现 NaN或者训练过程异常卡顿检查标签时发现某些行的 x、y、w、h 是负数或大于 2。原因标注工具导出的坐标没有除以图片尺寸直接把像素坐标当作归一化坐标写进了 txt个别框的左上角在图片外所以出现负数x 和 w 都写成了原始像素值时x 超过 1 很常见。解决用 2.3 节的统计脚本全局扫描一遍把所有越界记录挑出来用代码修复而不是手动一万条去改。修复公式就是把像素值除以对应图片的宽高重新写回 txt。模型把低头和转头同时预测出来一个目标输出两个框现象单张图片里同一个学生头部区域模型同时给出head_down和head_turn两个框置信度还都挺高。原因两个类别的训练样本边界划得不够清楚导致模型在模糊区域同时激活了两个类别这是标注规范问题不是模型问题。解决回到 2.2 节把“低头优先”的规则落到每一张有争议的样本上合并掉双重标签然后重新训练。不要在推理端想办法比如写规则让同时出现时只保留低头那样会掩盖标注层的问题换个教室效果更差。后排小目标漏检严重坐在最后一排的学生几乎检测不到现象验证集上近距离学生的检测准确率很高但图片中远距离学生几乎全部漏检尤其在后排角落。原因640 输入分辨率下后排学生的头部只有 25×25 像素上下属于典型的小目标YOLO 骨干网络下采样多次后小目标特征已经被压得很少。解决第一步把imgsz提高到 960 或 1280看看漏检改善幅度第二步是检查标注框是否已经把头部完整框住很多标注人员在远处小头上会随手画一个小框框偏小导致特征提取不够。这两步做完还不够就考虑在推理时把画面分成多个区域分别检测但那是拆东墙补西墙的做法有性能代价。微调的时候模型崩了loss 突然爆炸变成 NaN现象训练到第 10 轮左右loss 从正常值陡增到几百甚至 Inf随后所有指标变成 NaN权重文件也废了。原因学习率过高batch 太小或者标签文件里存在格式异常导致反向传播时梯度计算炸掉。解决先检查标签格式把我第 2.3 节的脚本跑一遍排除数据问题再把优化器固定为 adam初始学习率设为 0.001 或 0.0005。很多人喜欢把学习率改成 0.01 提速在 COCO 预训练模型上微调时这个值偏激进中等规模的教室数据集根本扛不住。加一句训练中定期把 best.pt 备份到另一个目录是必要的崩了还能恢复不然白跑几个小时。摄像头视角差异大桌下低头和正常坐姿低头分不清现象训练集来自某几个固定机位的教室效果不错部署到视角更高或更低的新教室时误检率明显上升。原因目标检测模型对视角和光照是敏感的训练集里的“低头”在俯视视角下就是“能看到头顶”在新机位的平视视角下变成“脸部正对镜头”特征完全变了。解决数据增强方向做两件事第一在训练配置里开启 HSV 亮度增强模拟不同光线第二标注时刻意收集多种摄像头高度下的样本。如果没有条件补数据部署时就把算法定位成“只有当头部姿态明显低于正常坐姿水平时才判定为低头”并新场景先用一周记录误检案例攒够了再微调一轮。6. 落地技巧用“事件计时法”把逐帧检测结果换算成课堂行为时长行为检测最终输出不能是逐帧的框列表那是过程数据不是结果数据。实际需要的是这堂课里某个学生低头了多少秒转头发生了多少次。逐帧累加的问题在于检测结果在帧与帧之间是抖动的同一个低头动作会被拆成“低头—正常—低头”多段累加时长会失真。我的做法是在检测结果之上套一层轻量状态机用连续帧确认来滤掉抖动。# 状态机连续确认 10 帧才进入低头状态连续 5 帧正常才退出 MIN_CONFIRM 10 MIN_RELEASE 5 class BehaviorStateMachine: def __init__(self): self.state 0 # 0 正常, 1 低头 self.confirm 0 self.down_duration 0 # 累计低头帧数 self.last_state_time 0 def update(self, is_down, frame_id): if is_down: self.confirm 1 if self.state 0 and self.confirm MIN_CONFIRM: self.state 1 self.last_state_time frame_id else: if self.state 1: self.confirm - 1 if self.confirm -MIN_RELEASE: self.state 0 self.down_duration frame_id - self.last_state_time self.confirm 0 else: self.confirm 0 return self.state逻辑是检测到低头不立刻确认连续 10 帧都检测到才切换到低头状态从低头状态退出时要连续 5 帧检测不到防止因为一两帧的漏检导致状态反复横跳。时长统计从状态确认进入开始计到确认退出为止这样一整个低头动作被累计成一段连续时长而不是被帧级噪声拆成十几段碎片。转头行为类似但统计维度是次数而不是时长——记录状态进入的次数即可。这个状态机替换掉逐帧累加后统计数值立刻变得稳定之前一节 45 分钟的课能统计出 20 分钟低头时长现在出来的数据更接近老师主观观察的结果。我自己做行为检测项目以来最大的感受是模型做出来只是第一步真正让方案被认可的是最后这层时序处理它决定了输出数据可信不可信。希望帮到你把这套流程跑通后课堂行为检测的基本盘就算立住了。本文还有配套的精品资源点击获取