ARTICLE DETAIL

资讯详情

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

基于YOLOv8的学生课堂低头转头行为检测实战:数据标注与训练全流程

基于YOLOv8的学生课堂低头转头行为检测实战:数据标注与训练全流程 简介面向学生课堂行为分析的目标检测数据集由约2,400张已标注图像构成包含低头、转头两个类别采用YOLO标注格式便于直接接入YOLOv5等主流检测框架适用于课堂纪律监测、学生注意力评估、智慧教室系统开发等应用场景。数据已完成训练集与验证集划分并附带show.py可视化脚本可一键查看标注框与原始图便于核对标签质量、定位错标漏标降低数据检查成本。压缩包共2000个文件其中1999个为txt格式的标签文件另含1个Python绘图脚本整体体积298.38MB文件名与图像一一对应目录简洁易于按序加载和批量处理。已有129人学习/下载。作者还不断更新YOLOv5改进实战博客覆盖结构优化、注意力机制嵌入与训练策略调参读者可依托这套数据完成从标准检测训练到精度提升的完整链路。1. 学生上课低头、转头行为检测从约2,400张YOLO标注数据切进来“学生上课低头、转头行为检测”这个标题本质是一个面向课堂场景的图像目标检测加行为状态判断任务输入教室摄像头画面输出每个学生的头部框和标签比如 head_down低头、head_turn转头。约2,400张、YOLO格式的已标注数据是把这类目标检测项目拉下神坛的关键——你不需要自己从零标几万张也能把行为检测闭环跑通。它适合正在做教育数字化、课堂专注度分析的一线工程师也适合想用真实业务数据入门目标检测与数据标注流程的新手。难点不在于YOLO训练本身而在于“低头”和“转头”的边界定义、标注一致性以及小数据集的过拟合控制。2. 读懂YOLO标注并整理数据目录、label文件、class配置拿到约2,400张已标注图片时最容易犯的错误是直接扔进YOLO训练命令完事。但YOLO格式的坑恰恰在“txt文件和图片要配套”这六个字上目录错位、类别ID和names不匹配、坐标没归一化任何一个都会让训练结果变成废铁。我拿到数据的第一件事永远是先验证标注而不是开训练。以下是我每次接手行为检测数据的固定整理流程。2.1 五列数字的含义与格式检查脚本YOLO格式的label是跟图片同名的txt文件每一行五个数值类别ID、归一化后的中心点x、中心点y、框宽、框高。第一列决定这个框属于“低头”还是“转头”后四列全部是相对图像宽高的比例区间理论上在0到1之间。最容易被忽略的一点这里存的是中心点坐标不是左上角坐标也不是像素坐标很多人第一次画框就错在这里。from pathlib import Path import numpy as np def check_yolo_labels(label_dir: Path, img_w: int 1920, img_h: int 1080): bad_lines [] for txt in label_dir.glob(*.txt): for line in txt.read_text().splitlines(): line line.strip() if not line: continue # 空行直接跳过不报错 parts line.split() if len(parts) ! 5: bad_lines.append((txt.name, line, 字段数量不是5)) continue cls int(parts[0]) x_c, y_c, w, h map(float, parts[1:]) if x_c 0 or y_c 0 or w 0 or h 0: bad_lines.append((txt.name, line, 包含非正数)) if x_c 1 or y_c 1 or w 1 or h 1: bad_lines.append((txt.name, line, 坐标超出归一化范围)) if bad_lines: print(f[FAIL] {len(bad_lines)} 条异常标注) for item in bad_lines[:10]: print(item) else: print(f[OK] {len(list(label_dir.glob(*.txt)))} 个txt均通过基础检查)这段检查脚本的逻辑很简单遍历目录下所有txt逐行判断“是不是五个字段”“坐标是不是在0到1之间”。img_w和img_h参数在这里没有直接参与运算但我建议保留因为下一步把归一化坐标还原成像素画框时会用到。这个脚本只能查出格式错误查不出“框没贴住头”这种标注质量问题所以还要做可视化抽查。import cv2 from pathlib import Path def draw_boxes(img_path: Path, txt_path: Path, out_path: Path): img cv2.imread(str(img_path)) h, w img.shape[:2] with open(txt_path) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue cls, x_c, y_c, bw, bh parts x_c, y_c, bw, bh float(x_c) * w, float(y_c) * h, float(bw) * w, float(bh) * h x1, y1 int(x_c - bw / 2), int(y_c - bh / 2) x2, y2 int(x_c bw / 2), int(y_c bh / 2) color (0, 255, 0) if cls 0 else (0, 0, 255) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.imwrite(str(out_path), img)注意这里从归一化坐标还原回像素时用的是“中心点减半宽、中心点减半高”的方式直接把YOLO坐标当左上角是最常见的低级错误。我一般会从train和val里各抽10到20张图画完框之后拼成一张网格图快速扫一眼重点看两类框的颜色是否和预期一致、头部是否被完整包住。这一步能省掉后面大量无效训练。2.2 用固定随机种子划分train/val并生成classroom.yaml约2,400张是小数据val比例我习惯取15%到20%也就是360到480张。划分策略上有个比比例更重要的点如果数据是按视频帧连续截取的随机按单张图片划分会造成同一学生的相邻帧同时出现在训练和验证里val指标虚高部署时原形毕露。更稳的做法是先按视频片段分组再按组划分同时固定随机种子保证可复现。import random from pathlib import Path import shutil random.seed(42) img_root Path(images) label_root Path(labels) # 收集图片按拍摄片段分组例如 g001_0001.jpg 归到 g001 组 groups {} for img in img_root.glob(*.jpg): prefix img.name.split(_)[0] groups.setdefault(prefix, []).append(img.stem) group_names list(groups.keys()) random.shuffle(group_names) val_count max(1, round(len(group_names) * 0.2)) val_groups set(group_names[:val_count]) for img in img_root.glob(*.jpg): stem img.stem prefix stem.split(_)[0] sub val if prefix in val_groups else train img_dst img_root / sub / img.name txt_dst label_root / sub / (stem .txt) shutil.copy(img, img_dst) shutil.copy(label_root / (stem .txt), txt_dst)这个脚本的关键在于“先按片段分组再切分”不是简单调一个random_split完事。随机种子固定为42后续换模型、调参数时验证集保持不变指标才有可比性。如果数据文件名里没有片段前缀至少也要按拍摄时间分成几大块再切否则val意义不大。划分完的目录结构必须长成下面这样这是YOLO训练器的硬性预期images/train/*.jpg images/val/*.jpg labels/train/*.txt labels/val/*.txt对应写入classroom.yamlpath: . train: images/train val: images/val names: 0: head_down 1: head_turn类别顺序在这里不是小事。names里的0/1必须和标注txt第一列完全一致如果数据里head_down是0head_turn是1yaml也必须是这个顺序写反了就是经典的自杀式训练。另外我要提醒一点很多行为检测原始数据只标了两个行为类漏掉了“正常听讲”这个背景类。如果训练时所有头部都必须被分到低头或转头其中一个类模型会把不确定样本硬塞进去后期行为判定会很难受。正式项目里我通常会在标注阶段补一个head_normal类哪怕只补一部分效果也比裸两类好。2.3 与标注工具对接时的三类坑数据标注工具这块开源路线里LabelStudio、LabelImg、CVAT都很常见。我习惯用LabelStudio做复核它能直接导出YOLO格式省去转换。但用到实际数据上有三个坑基本每次都会碰到。第一LabelStudio导出类别顺序可能按字母排序比如head_down和head_turn导出后ID顺序和你建标签时的顺序不一致。解决办法是导出后立刻跑2.1的检查脚本并统计txt第一列出现过的类别ID最大值和yaml里的names数量对一遍。第二用预标注模型辅助标注时很多人会把置信度低于0.4的预测框直接丢进label集合作废。在约2,400张数据里混进三四十个脏框就足够把“转头”类带偏。所以辅助标注后必须做人工抽检而不是信任模型的黑盒子输出。第三不要拿其他领域的标注习惯硬套。三维目标检测、遥感图像目标检测用的是三维框或旋转框跟这种课堂水平框行为数据完全不是一套东西。这个标题对应的就是最简单的水平框加互斥状态老老实实按这个格式来别自己发明新标注。3. 用YOLOv8跑通训练与验证命令、参数、产物YOLO系列迭代到现在v5、v8、v11的生态都很成熟训练体验差别不大。我习惯用Ultralytics的YOLOv8一条命令把训练、验证、导出全串起来对约2,400张数据来说少折腾环境比纠结选哪个版本更重要。这一章按“先冒烟、再训练、后验证导出”的顺序走。3.1 环境准备与标注冒烟测试环境配置不是这个项目最大的成本最大的成本是数据一致性。所以装完ultralytics之后我不会直接开训练而是先跑一次推理冒烟测试确认整条链路能通。python -m venv .venv source .venv/bin/activate pip install ultralytics yolo predict modelyolov8n.pt sourceimages/val/img0001.jpg conf0.5这条命令会联网下载YOLOv8n预训练模型权重如果服务器禁止外网下载就把权重文件手动放到项目根目录yolo命令会自动识别。跑出来的检测框是COCO的80类不是我们的head_down和head_turn这没关系只要输出有框说明读图、推理、后处理都正常。真正要在这个阶段验证的是图片和label的对应关系我用一段独立小脚本处理。from pathlib import Path img_root Path(images/train) label_root Path(labels/train) missing_txt [p.name for p in img_root.glob(*.jpg) if not (label_root / (p.stem .txt)).exists()] missing_img [p.name for p in label_root.glob(*.txt) if not (img_root / (p.with_suffix(.jpg)).exists()] print(缺txt:, missing_txt[:10]) print(缺jpg:, missing_img[:10]) if not missing_txt and not missing_img: print([OK] 图片与标注一一对应)这段脚本逻辑很直白检查每个jpg有没有同名txt每个txt有没有同名jpg。很多数据集翻车就翻在某个子目录少复制了一批文件训练器不会报错只是默默训练最后val指标差你都不知道该查谁。冒烟测试通过后再进训练后面每一步出了问题都能快速定位。3.2 训练命令与2400张小数据集的参数选择训练命令我通常写成一行带注释的风格方便日志里追溯当时用了什么参数。以下是我在单卡V100或RTX 3080上训练这类数据的最小命令yolo detect train \ dataclassroom.yaml \ modelyolov8n.pt \ epochs120 \ imgsz640 \ batch16 \ lr00.01 \ patience30 \ projectruns \ nameclassroom_baseline \ device0参数说明如下model对约2,400张数据yolov8n够用且跑得快如果显卡显存16G以上可以换yolov8s试试但对小数据量提升不会太大。我更推荐先拿n跑通再考虑s。imgsz这是最值得花时间调的参数。教室摄像头原图如果是1920×1080后排学生的头往往只有三四十像素缩到640后更小。如果验证阶段发现后排漏检直接升到800或1024不要动网络结构。batchBN层在batch太小时统计不稳定我最低接受16。显存不够就降imgsz别降batch。epochs2400张、单卡跑100到150轮足够patience30让训练在验证指标不再提升时自动停省算力。lr0迁移学习场景0.01是常规值。如果日志显示损失在头几十轮就剧烈震荡先把lr0降到0.001这是最便宜的一步。训练日志里的box_loss、cls_loss、dfl_loss多看看。对行为检测这个任务cls_loss最关键如果你发现cls_loss降得很慢大概率是“低头”和“转头”两类在低置信度区间严重打架不是学习率的问题。loss曲线对新手像个黑匣子但它其实是最早暴露问题的地方下降正常说明数据没问题异常震荡说明训练配置或标注有问题别急着换网络。3.3 验证、混淆矩阵到ONNX导出训练完成后产物在runs/classroom_baseline/weights/best.pt接下来跑验证命令yolo detect val \ modelruns/classroom_baseline/weights/best.pt \ dataclassroom.yaml \ batch16验证结束后runs/classroom_baseline/下会生成confusion_matrix.png、PR_curve.png、F1_curve.png这些文件。它们不是给汇报用的装饰图混淆矩阵看的是两个行为类之间有没有系统性误检PR曲线决定你部署时置信度阈值卡0.4还是0.6。很多团队只盯着mAP不打开这些图真到上线才发现阈值选错了。导出ONNX的命令也很短yolo export modelruns/classroom_baseline/weights/best.pt \ formatonnx imgsz640 opset12导出后我建议立刻在本地跑一次onnxruntime推理对比PyTorch模型的输出而不是等到部署机器上才发现算子和精度对不上。另外要特别注意ONNX输出的类别顺序如果导出后head_down和head_turn的顺序反了业务侧画框颜色就跟着反这种问题在课堂上演示时极其尴尬。4. 学生行为检测训练避坑5个现象、原因与解决这一章全部是我实际跑行为检测数据时反复踩过的坑每条按“现象、原因、解决”写。前三条靠数据整理阶段解决后两条靠训练和后处理解决。4.1 损失下降但mAP0类别ID和names顺序错位现象训练时train/box_loss下降得很正常但验证结束后mAP50是0PR曲线上一个有效点都没有。原因最常见的是两个类的标注txt第一列是0/1但classroom.yaml里names把两个类顺序写反了也有可能是val标签目录在划分时没同步导致验证集为空训练器不报错直接输出空指标。解决先跑2.1的格式检查脚本再看yaml的names与txt第一列的顺序是否一致。然后数一下images/val与labels/val的图片数量是否对得上。还有一个隐藏问题yaml里的path: .在换机器后可能会失效手写的相对路径不如改成绝对路径稳。遇到这种玄学mAP问题时把yaml路径改成绝对路径能排除一半嫌疑。4.2 低头和转头互相误检标注边界没卡死现象把置信度阈值降到0.3之后两类框大量叠在同一个人的头上模型明显分不清低头和转头。原因这是行为检测数据集的头号问题。“低头”和“转头”是视觉上的连续状态没有硬边界。如果标注规范里没写清楚判定规则同一个头在不同帧会一会儿标低头、一会儿标转头模型学到的是模糊边界而不是清晰状态。解决我一般会定两条硬规则。低头头部下倾超过约15度且下巴明显低于肩线转头头部绕垂直轴偏转超过约30度且能看到侧脸或耳廓。第二条规则是互斥的同一帧同一个人头只能有一个状态两可的样本宁可标head_normal也不硬塞。然后把标注结果随机抽20人复核一致性达不到9成就回头重标。这个功夫花在训练前永远比训练后调参划算。4.3 后排小目标漏检imgsz和增强没跟上现象验证集mAP50看起来还行但细看结果全是前排大头部贡献的后排小头几乎没被召回。原因约2,400张图里后排头部像素少缩到640后很多头只有10×10像素模型根本学不到有效特征。这不是网络结构的问题是输入分辨率和小目标样本量的问题。解决先把imgsz从640提到800或1024用同一份数据跑一次对比观察小目标召回是否上来。显存不够就减少batch或换yolov8n别一开始就上大模型。第二招是增强mosaic和copy-paste能通过拼接生成更多小目标样本代码里默认开启但要确认训练配置里的hsv_h、hsv_s、hsv_v没有被人为关掉。第三招是部署时把conf_thres降到0.25提升低置信度召回代价是误检变多用下一节的时序后处理兜住。4.4 训练中lossnan或BN崩溃学习率与batch失配现象训练到第20到40轮终端打印loss: nan或者loss从某一步开始指数级涨上去GPU显存显示还够但已经停不下来。原因对小batch跑YOLOBN层统计不稳定学习率过大时梯度先发散一发散就拉不回来。batch4或8、配合lr00.1的情况下中早期崩溃概率很高。解决先确认batch不是“自动选择”的默认值显存允许时至少batch16再把lr0降到0.001。如果还崩直接把预训练权重去掉用modelyolov8n.yaml从头训练。约2,400张数据从头训练一样能收敛只是epoch要多给一些。第三个办法是冻结backbone前几层给训练命令加freeze10让特征提取层先别乱动这招在目标检测模型微调崩了时很管用我自己用过的成功率不低于一半。4.5 单帧检测在视频里抖动缺时序后处理现象单张图片测试mAP很好但拿到连续视频里一个人头的标签在低头和转头之间来回跳肉眼看着就是残影闪烁。原因行为检测本质是序列问题而当前模型只是逐帧目标检测。两帧之间头部状态恰好处于过渡带分类置信度相持不下单帧阈值根本判不出稳定状态。解决在检测结果后面加一个跟踪器用ByteTrack或DeepSort给每个头一个稳定ID然后对同一个ID在最近5帧内做状态投票至少3帧是head_down才输出低头。这不需要上大模型一个滑动窗口就能消掉大半抖动。我在实际交付时一般把这种时序规则直接做进服务端而不是指望单帧网络一步到位。提示以上五个避坑点都建立在一个前提上——数据标注本身没有大问题。如果前两步的格式检查和可视化抽查没做再好的训练参数也是徒劳。5. 进阶验收用低置信度难例和时序窗口把行为检测做到可用5.1 把“低置信度样本”捞出来回流标注混淆矩阵能告诉你两个类有没有互相误检但定位到具体哪张图、哪个头还需要逐图翻。我常用的做法是写一个难例抽取脚本把所有置信度落在0.3到0.55之间的框捞出来集中人工复核。这个区间最需要看低于0.3大概率是误检高于0.6说明模型已经稳了中间地带才是标注质量升级的抓手。from ultralytics import YOLO from pathlib import Path model YOLO(runs/classroom_baseline/weights/best.pt) hard_cases [] for img_p in Path(images/val).glob(*.jpg): res model(img_p, conf0.3, verboseFalse) for box in res[0].boxes: conf float(box.conf) cls int(box.cls) if 0.3 conf 0.55: hard_cases.append((str(img_p), cls, round(conf, 3))) hard_cases.sort(keylambda x: x[2]) for case in hard_cases[:20]: print(case)把脚本输出结果攒20到30个样本回到标注工具里统一复核能纠正一批边界不清的低质量标注。这一轮回流比堆epochs有效得多。如果数据来源分散最好按摄像头编号或拍摄时段分组抽取避免偏斜。5.2 用跟踪与滑动窗口输出稳定行为状态给每个检测框配一个跟踪ID之后状态判定可以从单帧分类改成滑动窗口投票。核心代码很短用一个deque就能实现from collections import deque class HeadState: def __init__(self, window5): self.history deque(maxlenwindow) def update(self, cls_id): self.history.append(cls_id) if not self.history: return -1 avg sum(self.history) / len(self.history) return round(avg)这里cls_id为0或1窗口长度5意味着连续5帧里至少3帧是同一状态输出才切换。窗口越长越稳但延迟也越高我用5刚好覆盖课堂场景的眨眼级抖动。升级方向是把头部姿态角或肩膀位置等几何特征加进来参与状态判断也可以在后端融合音频特征那就是多模态目标检测的范畴了在数据量只有约2,400张的前提下先把时序规则用起来比试任何花哨模型都划算。我第一次给学生行为检测项目调模型时mAP到了0.83就兴冲冲去做演示结果整段视频里转头状态几乎全部漏报。后来把验证集逐帧翻完才发现问题出在标注样例太少、边界混乱而不是网络结构。所以现在每次接手行为检测数据我第一件事是抽20张图叠框第二件事是统计每个类别的样本数第三件事才是训练。希望帮到你。本文还有配套的精品资源点击获取
返回列表