简介:YOLO疲劳驾驶目标检测数据集面向计算机视觉目标检测方向的开发者与学生,提供真实驾驶场景下高质量图片共1000张,场景覆盖日间、夜间、不同光照及视角,可用于疲劳驾驶行为识别模型的训练与验证。压缩包内共2000个文件,以xml、txt两种标签文件为主体,另有html格式的环境搭建与训练教程、py格式数据集划分脚本和yaml模型配置文件,分别对应VOC、COCO和YOLO三种常用标注格式,能够直接接入YOLO系列模型进行训练。附赠的Linux/Windows双版本环境搭建与训练教程,配合三个划分脚本,可一键生成训练集、验证集、测试集,省去手动整理数据的流程。资源还包含详细的案例修改思路,适合刚入门目标检测的学生快速上手,也可供研究人员在疲劳驾驶场景下对比不同标注格式的模型效果。目前已有165人学习下载,包体约77.53MB,是一份结构清晰、可直接落地的数据集配套包。
1. 拿到YOLO疲劳驾驶目标检测数据集,先别急着训练
一千张图片的YOLO疲劳驾驶目标检测数据集,看着不大,却带齐了VOC、COCO和YOLO三种格式的标签。多数人拿到压缩包后第一反应是解压、改路径、跑训练,这个顺序在数据充足时问题不大,放到疲劳驾驶这类小目标场景里就有风险:眼睛和嘴巴的标注框只有几十个像素,三种标签一旦坐标口径不一致,损失函数会被少量错样本带偏。三种格式并存不是为了好看,而是不同工具链的入场券,关键工作是确认三份标注由同一份坐标派生,再让划分和训练都基于这个前提。下面从格式映射、数据划分、训练命令到训练后自检逐步展开,适合做智能座舱疲劳检测、或刚接触目标检测流程的同学照着跑通。
2. 三种格式标签先对齐:VOC、COCO与YOLO的映射关系
2.1 目录里真正需要关心的四类文件
解压这类数据包后,目录命名可能各不相同,但有效文件逃不出四种:图片、VOC格式的XML、COCO格式的JSON、YOLO格式的TXT。与其背目录名,不如先找出它们之间的对应规律。绝大多数情况下,图片去掉扩展名之后的主文件名就是整套数据的同步键,001.jpg对应001.xml和001.txt,这比任何字段都可靠。
拿到压缩包我一般会先整理成下面这种结构,后面所有脚本都只认这套结构:
fatigue_dataset/ ├── JPEGImages/ # 图片统一为 jpg,文件名是主键 │ ├── 001.jpg │ └── 002.jpg ├── Annotations/ # VOC:每张图一个 XML │ ├── 001.xml │ └── 002.xml ├── annotations.json # COCO:整个数据集合并成一个 JSON └── labels_yolo/ # YOLO:每张图一个 TXT ├── 001.txt └── 002.txt如果原始压缩包不是这样命名的,建议先写一个几行的小脚本把图片统一重命名为数字序号,再同步修改三个标签目录里的文件名。这里的坑在于重命名时只改图片不改标签,等于直接废掉一组标注。整理目录这事看起来繁琐,但它决定后面划分脚本是写一遍就能用,还是在每个环节都加特判。
2.2 VOC、COCO、YOLO三份标注的对照表
三种格式的差异,用一张表就能看明白:
| 维度 | VOC XML | COCO JSON | YOLO TXT |
|---|---|---|---|
| 文件组成 | 每张图片一个XML | 整个集合一个JSON | 每张图片一个TXT |
| 边界框表示 | (xmin, ymin, xmax, ymax) | 左上角(x, y)加宽高(w, h) | 中心点(xc, yc)加宽高(w, h) |
| 坐标单位 | 像素绝对值 | 像素绝对值 | 归一化相对值,范围0到1 |
| 类别写法 | 字符串标签名 | category_id加categories映射表 | 整数ID,从0开始 |
| 读取特点 | 大量小文件IO,适合人工核对 | 一次载入,内存开销整体可控 | 每行一个目标,训练直接读取 |
拆开看,最核心的差异只有两处。一是坐标参数化,VOC用左上右下两个对角点,YOLO用中心点加宽高,COCO则取左上角加宽高;二是类别表达,VOC给的是字符串,YOLO给的是整数索引。只要把这两组规则理清,三种格式互换就变成了机械操作,不再需要对着报错信息猜原因。
2.3 VOC的XML转换成YOLO的TXT,核心只有四行换算
以疲劳驾驶数据集最常用的场景为例,把VOC坐标转成YOLO训练要吃的TXT格式:
import xml.etree.ElementTree as ET def convert_one(xml_path, out_txt, img_w, img_h, class_map): root = ET.parse(xml_path).getroot() lines = [] for obj in root.findall('object'): name = obj.find('name').text if name not in class_map: # 标签名不在映射表里,跳过 continue b = obj.find('bndbox') xmin = float(b.find('xmin').text) ymin = float(b.find('ymin').text) xmax = float(b.find('xmax').text) ymax = float(b.find('ymax').text) x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h box_w = (xmax - xmin) / img_w box_h = (ymax - ymin) / img_h lines.append(f"{class_map[name]} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}") open(out_txt, 'w').write('\n'.join(lines))调用这个函数前有三个参数必须确认。class_map是标签名字符串到数字ID的字典,例如{"eye_open": 0, "eye_closed": 1, "mouth_open": 2},不能靠猜测,需要先把全部XML里的标签名去重再定义。img_w和img_h必须是图片真实尺寸,直接读图片获取,不可以用标注文件里的推测值;归一化一旦用错尺寸,训练时所有框都会偏移。转换后输出的TXT每行是五个值:类别ID、中心点x、中心点y、框宽、框高,全部落在0到1之间。
这里最容易踩的坑是图片EXIF旋转信息。部分手机或行车记录仪拍摄的图片带旋转角,读图库拿到的宽高和YOLO解码后的实际宽高不一致,转出来的坐标会整体错位。我一般会在数据处理阶段先把图片统一转成基准朝向并重存,再做坐标换算,省掉后面的不可复现问题。
2.4 为什么三种格式要并存
YOLO训练最终只吃TXT,那为什么还要保留VOC和COCO两份标签?疲劳驾驶场景的数据往往要进入不同的下游处理。有人要把检测结果接进目标跟踪模块,有人要用COCO评估协议算mAP,还有人先做分类再切检测分支。保留原生格式,本质上是保留工具选择的空间,不是数据冗余。
关键约束在于,三份标注必须镜像同一份坐标来源。如果在VOC里修正了一个框,却没有同步更新JSON和TXT,后续训练对的样本就会变成脏数据。处理这类资源时,我建议只维护一份基准标注,其余格式用脚本生成,而不是在三个目录里分别手工改。
3. 划分脚本的实现思路与可直接复制的代码
3.1 为什么划分层级必须落在文件名上
训练集、验证集、测试集的划分脚本,听起来只是做一次随机打乱,实际要解决的是图片和三套标签的同步问题。常见做法是先对图片文件名做随机划分,再依据图片主文件名去复制对应的标签文件。原因有两点:图片是数据集的事实来源,按图片划分不会出现“有标注无图片”或“有图片无标注”的孤儿文件;后续想调整比例时,也只需要重新跑一遍脚本,不需要动原始数据。
疲劳驾驶数据里还有一个容易忽略的背景:这类图片大多从视频里抽帧得到,相邻帧内容高度相似。如果直接对全部图片做全局随机,同一段视频的相似帧会同时进入训练集和验证集,造成验证分数虚高,换一段真实场景视频后性能明显下跌。我一般会先按视频片段分组,在组层面做划分;如果数据包里没有提供视频分组信息,至少要知道这个局限,不要在结果里过度解读验证集分数。
3.2 一份可直接跑的train/val/test划分脚本
下面这个脚本以YOLO格式的标签为例,按8:1:1划分数据:
import random import shutil from pathlib import Path IMG_DIR = Path("fatigue_dataset/JPEGImages") LAB_DIR = Path("fatigue_dataset/labels_yolo") OUT_DIR = Path("fatigue_dataset/split") train_ratio, val_ratio, test_ratio = 0.8, 0.1, 0.1 random.seed(20241001) imgs = sorted(IMG_DIR.glob("*.jpg")) # 前置校验:缺标签的图片进问题清单,不参与划分 broken = [p.name for p in imgs if not (LAB_DIR / (p.stem + ".txt")).exists()] if broken: raise SystemExit(f"发现{len(broken)}张图片缺标签,先处理: {broken[:3]}") random.shuffle(imgs) n = len(imgs) train_end = int(n * train_ratio) val_end = train_end + int(n * val_ratio) for split_name in ["train", "val", "test"]: (OUT_DIR / "images" / split_name).mkdir(parents=True, exist_ok=True) (OUT_DIR / "labels" / split_name).mkdir(parents=True, exist_ok=True) for part, split_name in [ (imgs[:train_end], "train"), (imgs[train_end:val_end], "val"), (imgs[val_end:], "test"), ]: for img in part: shutil.copy2(img, OUT_DIR / "images" / split_name / img.name) shutil.copy2(LAB_DIR / (img.stem + ".txt"), OUT_DIR / "labels" / split_name / (img.stem + ".txt")) print(f"划分完成: train={train_end}, val={val_end-train_end}, test={n-val_end}")这个脚本有两个设计值得说明。一是前置校验放在最前面,绝大多数划分脚本的失败不是因为复制出错,而是标签缺失没有被发现,等到训练时才暴露;这里直接抛异常,逼着你先解决数据完整性问题。二是用copy2而不是move,保留原始文件,方便后续重跑或调整比例,多占一点磁盘换来的是低成本试错。
划分完成后,结果目录里images/train与labels/train的文件名一一对应,data.yaml里直接指到这两个目录就能开始训练。
3.3 划分比例与三个边界参数
| 参数 | 作用 | 1000张图建议值 |
|---|---|---|
| random.seed | 固定随机序列,保证每次划分结果一致 | 任一整数,记录到训练文档里 |
| train_ratio | 训练集占比 | 0.8 |
| val_ratio | 每轮训练结束后做验证 | 0.1 |
| test_ratio | 最终模型评估用,只在最后碰 | 0.1 |
这里特别强调 test 集合的纪律。验证集每个 epoch 都在参与模型选择,本质上已经被“用过了”;测试集应该只在最终权重上跑一次,如果反复拿测试集调阈值、调置信度,测试集就慢慢变成了第二个验证集,最终评估数字会失真。这个小数据集本身就不适合频繁试探测试集。
另一个要注意的是类别不平衡。疲劳驾驶检测里睁眼框数量通常远大于闭眼,打哈欠的嘴部开合样本更少。如果随机划分后闭眼样本全部砸进验证集,训练集就学不到闭眼特征。轻量做法是在划分前扫描每个TXT的类别ID,把小类别样本按比例分配到三个集合中,其余样本再随机划分;不需要搞复杂的样本加权,只要保证每个小类别在训练集里出现的次数足够多就行。
3.4 COCO标注如何跟着划分走
YOLO格式的标签是一张图一个TXT,直接复制文件就能完成划分。COCO格式不同,整个集合只有一个JSON文件,不能用文件复制的方式同步。常见做法是把JSON加载进来,按划分结果里的图片ID过滤图片列表和标注列表,再写一个新JSON。用ultralytics训练时其实用不到COCO JSON,但如果你后续要接mmdetection或其他以COCO为输入的框架,这一步就得做。
import json def split_coco(json_path, img_ids, out_path): whole = json.load(open(json_path)) img_set = set(img_ids) imgs = [im for im in whole["images"] if im["id"] in img_set] annos = [a for a in whole["annotations"] if a["image_id"] in img_set] save = dict(whole) save["images"], save["annotations"] = imgs, annos json.dump(save, open(out_path, "w"), ensure_ascii=False, indent=2)img_ids从哪来?划分脚本在切分图片时,同时把图片主文件名映射到COCO JSON里的image_id即可,不要用文件名去比对,COCO的image_id才是唯一标识。过滤完后顺手打印一下每个split里的类别分布,能提前发现划分严重失衡的问题。
4. YOLO训练教程:从data.yaml到训练命令
4.1 选择YOLO版本的实判标准
yolo第几代了这个问题在社区里几乎每周都有人问,但做项目不是追新,而是看生态稳定性。以目前资料完整度和出错后能快速检索到答案的程度来看,我一般直接用YOLOv8及后续版本的ultralytics命令行,因为yolo detect train这个入口和参数命名在多个大版本里保持稳定,换版本时迁移成本很低。对于疲劳驾驶这种单卡就能跑的轻量场景,优先用nano或small规格,不要一上来就上超大模型。1000张图的数据量撑不起大模型的参数量,强行训练的结果往往是验证集分数虚高、真实场景泛化差。
4.2 data.yaml的写法和类别顺序陷阱
给出一份最小可用的配置文件:
# fatigue_dataset/split/data.yaml path: /data/fatigue_dataset/split train: images/train val: images/val test: images/test nc: 3 names: 0: eye_open 1: eye_closed 2: mouth_openpath建议写绝对路径,避免不同终端工作目录不一致导致图片路径解析失败。names的顺序必须和TXT里的整数ID一一对应,顺序一旦写错,训练不会报错,但推理时类别张冠李戴。最快的核对方法是随便打开几个TXT,看每行第一个数字是几,0就代表names[0]里的类别。
如果标签文件里出现了大于等于nc的整数ID,训练会直接出错或静默跳过。建议划分完成后写一个扫描目录里所有TXT、输出最大类别ID的小命令,把这个错误挡在训练前。
4.3 训练命令、参数速查表与显存考量
yolo detect train \ data=/data/fatigue_dataset/split/data.yaml \ model=yolov8n.pt \ epochs=60 \ imgsz=640 \ batch=16 \ patience=15 \ project=/data/fatigue_dataset/runs \ name=fatigue_v8n \ device=0逐参数说明一下。model=yolov8n.pt表示加载COCO预训练权重而不是从头训练,1000张图从零开始训练收敛很慢,预训练权重能提供基础视觉特征;如果想彻底从头训练,把模型参数写成yolov8n.yaml,但要接受明显更长的训练时间和更低的上限。epochs=60配patience=15表示最多训练60轮,若验证集指标连续15轮不提升就提前停止,防止小数据集过拟合。
| 参数 | 默认值 | 本项目建议 | 什么情况要改 |
|---|---|---|---|
| epochs | 100 | 60 | 数据量更小则继续下调 |
| batch | 16 | 8到16 | 显存不足时降到8 |
| imgsz | 640 | 640或960 | 标注框过小时升到960 |
| patience | 100 | 15 | 小数据集依赖早停防过拟合 |
| device | cpu | 0 | 有多张卡时指定卡号 |
关于imgsz要多说一句。疲劳驾驶场景里眼睛和嘴巴的框都很小,如果原图尺寸达到1080p以上,640的输入会把小目标压得更小。可以先跑一版640看mAP,再试960对比小目标召回率。显存不够时优先降batch而不是降imgsz,因为imgsz对检测精度的影响更直接。如果团队里有人用AMD显卡跑YOLO,通常走ROCm版PyTorch,device填0或1即可,安装时注意PyTorch与ROCm的版本匹配,否则驱动层报错会浪费大量时间。
4.4 训练输出里必须检查的四个文件
训练结束后到runs/fatigue_v8n目录下,重点看这四类产物:
runs/fatigue_v8n/ ├── weights/ │ ├── best.pt │ └── last.pt ├── args.yaml ├── results.csv └── confusion_matrix.pngargs.yaml记录了完整训练参数,排查“为什么别人复现不出我的结果”时先看它。results.csv里有每一轮的box_loss、cls_loss和mAP指标,要同时看训练集和验证集的loss曲线;只降训练loss不降验证loss是过拟合的明显信号。目标检测的损失由边框回归损失和分类损失叠加构成,如果验证集box_loss已经稳定但mAP50-95仍在波动,说明框定位够了,问题更多出在类别区分上,优先检查困难样本和类别样本数。weights/best.pt是验证指标最好的一次权重,last.pt是最后一轮权重,正式部署取best,不要在测试集上拿last反复试。
5. 训练前先自检标签,训练后做时序统计验证
5.1 训练前用五段式校验脚本拦截脏标签
疲劳驾驶小目标数据里最常见的脏数据是:框宽高为0、坐标归一化后超出0到1区间、类别ID越界。这类问题会让训练过程忽上忽下,还容易被误判为模型问题。我一般会在划分之后跑一个标签自检脚本:
from pathlib import Path def scan_yolo_labels(img_dir, label_dir, nc): problem = [] for img in sorted(img_dir.glob("*.jpg")): txt = label_dir / (img.stem + ".txt") if not txt.exists(): problem.append(f"{img.name}: 缺标签") continue for idx, line in enumerate(txt.read_text().splitlines(), 1): parts = line.split() if len(parts) != 5: problem.append(f"{img.name} 第{idx}行 非5段") continue cls = int(parts[0]) xc = float(parts[1]); yc = float(parts[2]) w = float(parts[3]); h = float(parts[4]) if cls >= nc: problem.append(f"{img.name} 类别ID越界: {cls}") if w <= 0 or h <= 0: problem.append(f"{img.name} 框宽高异常: {w}x{h}") if any(v < 0 or v > 1 for v in [xc, yc, w, h]): problem.append(f"{img.name} 坐标越界") return problem校验维度覆盖四类问题:标注行数量是否5段、类别ID是否在合法范围、框宽高是否非零、归一化坐标是否越界。运行时把结果输出到文件而不是直接打印在终端,几十条错误刷屏会盖住后面的训练日志。自检全部通过再进训练,能省掉大量因数据问题导致的训练发散排查时间。
5.2 用best.pt做一次测试集推理验证
训练完成后,用测试集做一轮推理,比只看指标更直观:
yolo detect predict \ model=/data/fatigue_dataset/runs/fatigue_v8n/weights/best.pt \ source=/data/fatigue_dataset/split/images/test \ conf=0.25 \ save=True预测图会输出到runs/fatigue_v8n/predict目录。打开图片看三件事:闭眼小框是否贴合眼睛轮廓、睁眼是否被误判成闭眼、大幅侧脸时嘴巴框是否漂移。指标只能告诉你整体水平,图片能告诉你错在哪里。
5.3 疲劳驾驶评估的重点在时间窗口而不是单帧mAP
疲劳驾驶检测与通用目标检测最大的区别在于,单帧指标不能直接对应实际告警质量。一个司机闭眼2秒和眨眼0.2秒,在单帧图上可能都是“闭眼”类别,但对疲劳判断的意义完全不同。落地时常见的做法是引入时间窗口统计,例如闭眼帧占比、连续N帧闭眼、单位时间内打哈欠次数,再结合阈值触发报警。检测模型负责提供可靠的单帧结果,判断逻辑负责在时间序列上做聚合;mAP50-95反映的是检测器的基础能力,而真正决定系统是否可用的,是这些统计指标在真实驾驶视频上的稳定性。先验证检测器,再验证时间窗口参数,这条路比单独盯着测试集数字更接近工程实际。
本文还有配套的精品资源,点击获取