资讯详情

YOLO疲劳驾驶数据集:三种标签格式转换与训练避坑指南

📅 2026/10/25 22:32:30 | 华诺云谱 👁 阅读
YOLO疲劳驾驶数据集:三种标签格式转换与训练避坑指南
简介YOLO疲劳驾驶目标检测数据集面向目标检测初学者以及需要完成课程设计或毕业设计的学生包含5000张真实驾驶场景图片场景丰富使用LabelImg标注标注框质量高。标签提供VOC、COCO、YOLO三种格式分别存放于不同文件夹可直接用于YOLO系列模型训练免去格式转换步骤。压缩包共2000个文件以xml标签为主另含txt标签、html环境搭建与训练教程、py数据划分脚本整体约389.57MB。已有657人学习下载。随包附赠Windows与Linux环境搭建教程、不同版本训练案例教程以及训练集/验证集/测试集划分脚本读者可按需划分数据并复现完整流程还能根据案例修改训练自己的数据集。对希望快速上手YOLO目标检测的新手或需要数据集完成项目/毕设的同学这套资料能明显降低入门成本。1. YOLO疲劳驾驶数据集5000张图片、三种标签格式先别急着训练一个包含5000张图片的YOLO疲劳驾驶目标检测数据集压缩包里同时躺着VOC、COCO、YOLO三种格式的标签外加划分脚本和训练教程。这类资源对入门者就像一块已经切好的牛排火候对不对取决于你是否先看清纹理。我见过太多人解压后直接把txt扔进yolo训练结果要么loss爆炸、要么mAP虚高、要么推理时框全错位——问题几乎都出在标签格式转换与数据集划分上而不是模型本身。下面按“标签体检→格式互转→集合划分→训练验证”的顺序讲新手能跑通闭环熟手可以直接拿走转换脚本和避坑清单。2. 解压后先做标签体检VOC、COCO、YOLO三种格式的目录与类别分布2.1 三种格式的目录约定先知道文件在哪脚本才不会瞎找这类数据集包的常见组织方式是图片共用一份标注分别按三种格式存放。VOC格式通常是一个完整文件夹里面按JPEGImages放原始图像、Annotations放同名xml、ImageSets/Main放划分好的txt清单COCO格式则把标注塞进一个json文件图片按train2017/val2017这类子目录组织也可能是自定义的images目录YOLO格式最朴素通常就是images和labels两个平级目录labels里的txt和图片同名。解压之后先用两条命令摸底不要急着打开GUI。find . -maxdepth 2 -type d find . \( -name *.xml -o -name *.json -o -name *.txt \) | head -20第一条命令里的maxdepth 2只让find向下看两层目录防止被嵌套结构搞花眼第二条命令把xml、json、txt三种标注文件各列出前20个用来判断格式是否齐全。如果jpg有5000张、xml只有几百个说明图片和标签对不上后面转换之前必须先处理这部分脏数据。格式标注文件坐标表达类别记录方式常见训练框架VOCxml像素坐标xmin/ymin/xmax/ymaxobject标签下的name字符串Faster R-CNN、SSD、mmdetectionCOCOjson像素坐标x/y/w/hx/y为左上角category_id数字Detectron2、mmdetectionYOLOtxt归一化cx/cy/w/h中心点每行第一个数字类别编号YOLOv5、YOLOv8、Ultralytics这个表格值得打印出来贴在屏幕边。VOC和COCO都是像素坐标但一个用“左上右下”、一个用“左上宽高”YOLO是归一化坐标训练时模型读入后会按图宽高还原。三种格式互相转换的坑九成出在坐标系统切换上。还有些打包者会把VOC的xml和YOLO的txt直接平铺在annotations目录里没有按子目录分碰到这种情况就以文件名后缀为准脚本里单独处理。2.2 标签体检用一段Python统计图片数、框数与类别分布打开数据集后别急着配环境先跑一次统计。我见过某个疲劳驾驶包里把“打哈欠”分别标成yawn、Yawn和打哈欠三种写法如果不体检直接训练类别数会从预期的3变成5或6模型永远学不好。下面是解析VOC xml的体检脚本对另外两种格式稍改读取方式即可复用。import os import xml.etree.ElementTree as ET from collections import Counter annotations_dir Annotations class_counter Counter() box_counter 0 image_counter 0 for xml_name in sorted(os.listdir(annotations_dir)): if not xml_name.endswith(.xml): continue image_counter 1 tree ET.parse(os.path.join(annotations_dir, xml_name)) root tree.getroot() for obj in root.findall(object): name obj.findtext(name) if name is None: continue class_counter[name] 1 box_counter 1 print(f图片数: {image_counter}) print(f目标框总数: {box_counter}) print(f类别分布: {dict(class_counter)})这段逻辑本身不难关键在两点。第一用findtext(name)而不是find(name).text缺标签时返回None再跳过不会整个脚本中断第二统计结果出来后要人工看一眼类别分布如果某个类只有几十个框训练时这个类的AP必然不稳定。疲劳驾驶场景里闭眼通常比打哈欠样本少这是先天的类别不均衡后面训练参数要针对它调。如果包里没有VOC格式只有YOLO的txt体检就更直接遍历labels目录读每行第一个数字统计类别编号出现次数再把classes.txt读出来对照编号。注意txt体检时还要检查每行是不是5个值不规范的txt会在训练时让YOLO的loss直接变成nan那是后话。2.3 classes.txt类别顺序一旦定下标签编号必须同步改YOLO训练时txt里每行的第一个数字就是类别编号编号按classes.txt的行序定义。比如下面这个文件第0行对应类别0第1行对应类别1yawn closed_eye bow_head对应关系一旦定死标签里类别0就必须是yawn。如果你觉得bow_head应该放第一位然后把classes.txt改了但txt标签里的编号没同步模型会把“低头”当“打哈欠”训练推理结果全错而且这种错很隐蔽。常见做法是训练前先定一份类别清单并让VOC、COCO、YOLO三种格式共用同一个映射表。COCO的json里category_id如果是1、3、5这种跳号顺序转换脚本要先做一层映射coco_category_id→类别名称→自定义编号绝不能直接把category_id当YOLO编号。这一步既是格式转换的核心也是后面最容易翻车的地方下一章展开讲。3. VOC、COCO、YOLO互转坐标换算公式与四个必踩的边界坑3.1 三种坐标系统像素框、左上角框和归一化中心框VOC的xml里bndbox给的是xmin/ymin/xmax/ymax四个像素值COCO的json里bbox给的是x/y/w/h四个值x和y是左上角坐标YOLO的txt里每行是class cx cy w h全部归一化到0到1之间其中cx/cy是中心点坐标。三种格式切换本质是坐标变换公式如下。# VOC像素坐标 - YOLO归一化坐标 cx (xmin xmax) / 2.0 / img_w cy (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h # COCO像素坐标 - YOLO归一化坐标 cx (x w / 2.0) / img_w cy (y h / 2.0) / img_h w w / img_w h h / img_h注意两个细节。COCO的x/y是左上角转中心点必须加w/2和h/2YOLO的宽高必须为正数COCO里偶尔出现w0或h0的病态标注转换时直接跳过。最关键的还是img_w和img_h必须来自原始图片不能拿缩略图或显示画布的尺寸去除否则归一化坐标整体偏移。3.2 VOC转YOLO完整脚本与没有size字段时的降级方案最常见的工作流是从VOC转YOLO因为网上大量历史数据集是VOC标注格式而YOLOv5/v8是现在的主流。下面这段脚本我基本每次都用改一下class_map就能复用。import os import xml.etree.ElementTree as ET from PIL import Image voc_root VOC yolo_label_root yolo_labels os.makedirs(yolo_label_root, exist_okTrue) class_map {yawn: 0, closed_eye: 1, bow_head: 2} # 与classes.txt一致 for xml_name in os.listdir(os.path.join(voc_root, Annotations)): xml_path os.path.join(voc_root, Annotations, xml_name) tree ET.parse(xml_path) root tree.getroot() size root.find(size) if size is not None: img_w int(size.findtext(width)) img_h int(size.findtext(height)) else: img_path os.path.join(voc_root, JPEGImages, xml_name[:-4] .jpg) img_w, img_h Image.open(img_path).size # 降级方案 lines [] for obj in root.findall(object): name obj.findtext(name) if name not in class_map: continue bndbox obj.find(bndbox) xmin float(bndbox.findtext(xmin)) ymin float(bndbox.findtext(ymin)) xmax float(bndbox.findtext(xmax)) ymax float(bndbox.findtext(ymax)) cx (xmin xmax) / 2.0 / img_w cy (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{class_map[name]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) with open(os.path.join(yolo_label_root, xml_name[:-4] .txt), w) as f: f.write(\n.join(lines))脚本的逻辑是优先读xml里的size字段这是因为size里的宽高才是标注时用的图像分辨率只有xml缺失size时才用PIL读原图兜底。顺序不能反过来否则如果原图被预处理缩放过像素坐标对不上转换结果就是错框。class_map是VOC类别名到YOLO编号的唯一映射如果你改过类别顺序这里必须同步。这个脚本有两个边界要补。第一float(bndbox.findtext(xmin))有可能因为xml里存在不完整标注而报错稳妥做法是用try/except包住转换段跳过错行而不是中断整个脚本第二图片扩展名不一定是.jpg可能是.png或.jpeg如果xml的filename字段可读优先用它拼图片路径。3.3 COCO转YOLO先读categories建立名称映射不直接用category_idCOCO格式转YOLO容易出问题的不是坐标而是类别编号。COCO的category_id是数据集内部的顺序常见的是从1开始且不连续比如1、3、5。YOLO要求0到N-1连续编号所以必须先用category_id找到类别名称再用名称匹配自己的class_map。import json import os json_path annotations/instances_train.json out_dir coco_yolo_labels os.makedirs(out_dir, exist_okTrue) with open(json_path) as f: coco json.load(f) cat_id2name {cat[id]: cat[name] for cat in coco[categories]} img_info {img[id]: img for img in coco[images]} class_map {yawn: 0, closed_eye: 1, bow_head: 2} anns_by_img {} for ann in coco[annotations]: if bbox not in ann or len(ann[bbox]) ! 4: continue anns_by_img.setdefault(ann[image_id], []).append(ann) for img_id, anns in anns_by_img.items(): img img_info[img_id] lines [] for ann in anns: name cat_id2name.get(ann[category_id]) if name not in class_map: continue x, y, w, h ann[bbox] cx (x w / 2.0) / img[width] cy (y h / 2.0) / img[height] w w / img[width] h h / img[height] lines.append(f{class_map[name]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) out_path os.path.join(out_dir, img[file_name].replace(.jpg, .txt)) with open(out_path, w) as f: f.write(\n.join(lines))这段代码有两点值得说明。anns_by_img先把同一张图的框聚合到一起避免后面逐图全量扫描annotations列表5000张图的数据集性能差距明显跳过没有bbox的标注是因为COCO里有些annotation只有segmentation没有外接框这种对YOLO训练没有意义。file_name如果带子目录比如night/001.jpg输出txt时要先os.makedirs对应的子目录脚本里为简洁略掉了用时记得补。3.4 转换后的验证空标签、越界坐标与行数不对都是大敌转换完必须跑一遍全量验证不能抽查。空标签文件会让YOLO训练时mosaic增强拉到空标注轻则报警告重则训练崩溃越界坐标会让loss反复震荡。import os label_root yolo_labels class_num 3 # 与classes.txt行数一致 bad_files [] for txt_name in os.listdir(label_root): path os.path.join(label_root, txt_name) with open(path) as f: lines f.readlines() if len(lines) 0: bad_files.append((txt_name, empty)) continue for line in lines: parts line.strip().split() if len(parts) ! 5: bad_files.append((txt_name, field_num)) break cls_id int(parts[0]) coords [float(v) for v in parts[1:]] if cls_id class_num: bad_files.append((txt_name, class_out_of_range)) break if any([v 0 or v 1 for v in coords]): bad_files.append((txt_name, coord_out_of_range)) break if bad_files: print(异常文件数:, len(bad_files)) for name, reason in bad_files[:20]: print(name, reason) else: print(全部标签检查通过)验证逻辑很简单每行必须有5个字段类别编号不能超过类别总数归一化坐标必须在0到1之间。coord_out_of_range一旦出现先回去查xml里size字段或者COCO的images宽高而不是在超参上浪费时间。空标签文件建议直接删掉对应图片或单独记录文件名在划分脚本里排除。4. 划分脚本的正确姿势分层抽样切出train/val/test并验证集合不重叠4.1 先决定划分产物train.txt清单还是目录复制YOLOv5和YOLOv8对数据集划分的处理不太一样。旧版YOLOv5挺认train.txt、val.txt这种路径清单文件Ultralytics YOLOv8则更多用data.yaml里的train和val字段可以指向目录也可以指向txt。5000张图如果全部复制到train、val、test三个目录磁盘占用直接翻三倍所以常见做法是只生成图片路径清单txt训练时YOLO会按images和labels的同名对应关系自动找标签。划分脚本的输出是三份文件train.txt、val.txt、test.txt每行是图片路径。data.yaml里这么配置path: /your/dataset/root train: train.txt val: val.txt names: 0: yawn 1: closed_eye 2: bow_headpath是数据集根目录train和val写txt文件名YOLO会把images/train/001.jpg自动映射到labels/train/001.txt。注意names的顺序必须和classes.txt一致两边同时错位的情况我也踩过推理时输出的类别名称和实际完全对不上。4.2 分层抽样脚本按类别占比打散数据别让某一个类全留在训练集全局随机划分有个隐藏问题类别不均衡时某个类别的样本可能被随机全部塞进训练集验证集里一个都捞不到。疲劳驾驶里闭眼样本本来就少于打哈欠如果验证集里缺了闭眼mAP指标会严重失真。所以我一般按第一个目标的类别做分层抽样先按类别分组再组内切分。import os import random from collections import defaultdict random.seed(42) # 固定随机种子换机器结果一致 image_dir images label_dir labels train_ratio, val_ratio, test_ratio 0.8, 0.1, 0.1 images [f for f in os.listdir(image_dir) if f.endswith(.jpg)] class2imgs defaultdict(list) for img_name in images: label_path os.path.join(label_dir, img_name.replace(.jpg, .txt)) if not os.path.exists(label_path): print(missing label:, img_name) # 有图无标先暴露出来 continue with open(label_path) as f: first_line f.readline().strip() if not first_line: continue cls_id int(first_line.split()[0]) class2imgs[cls_id].append(img_name) train_set, val_set, test_set [], [], [] for cls_id, imgs in class2imgs.items(): random.shuffle(imgs) n len(imgs) n_train int(n * train_ratio) n_val int(n * val_ratio) train_set imgs[:n_train] val_set imgs[n_train:n_train n_val] test_set imgs[n_train n_val:] random.shuffle(train_set) random.shuffle(val_set) random.shuffle(test_set) for split, imgs in [(train, train_set), (val, val_set), (test, test_set)]: with open(f{split}.txt, w) as f: for img_name in imgs: f.write(os.path.join(image_dir, img_name) \n)脚本的核心是random.seed(42)固定种子保证任何机器上跑出来的划分结果一致。class2imgs按第一个目标的类别把图片分组然后组内random.shuffle再按比例切最后三份列表各自再洗一次避免同一个类别的图集中在一起。如果一张图里有多个类别按第一个目标统计会有一点偏差但作为划分依据足够用。8:1:1不是固定标准数据量小到5000张时这个比例比较稳如果某类样本极缺可以适当把val比例降到0.05。4.3 划分完校验集合交叉、标签存在性和类别均衡划分完别急着训练先验证三件事。第一三个集合不能有重叠第二每个集合里的图片都要有对应标签第三验证集的类别分布要和训练集接近。第一件事用集合交集一行代码就能查。def load_list(path): with open(path) as f: return [line.strip().split(/)[-1] for line in f] train set(load_list(train.txt)) val set(load_list(val.txt)) test set(load_list(test.txt)) print(train/val overlap:, len(train val)) print(train/test overlap:, len(train test)) print(val/test overlap:, len(val test))如果overlap不是0原因通常是图片命名冲突或脚本里用了旧版文件列表。训练集和验证集一旦重叠mAP虚高0.05到0.1很正常尤其疲劳驾驶这种背景单一的数据集模型见过原图后评估分数完全不可信。类别均衡的检查可以复用2.2的统计脚本把train.txt和val.txt里的图片路径读出来分别统计标签分布比例差距超过一倍就说明划分随机性有问题需要排查是否漏了分层。5. 避坑疲劳驾驶目标检测训练最常见的5个翻车现场5.1 训练loss一路下降验证mAP却贴着0很打击人的一种情况是loss曲线漂亮验证集mAP几乎为0。先别怀疑模型第一步查验证集图片路径是否真的对应到了标签。常见原因是划分脚本把train和val的label目录写反了或data.yaml里val指向了无标签目录。YOLO训练时对验证集不计算损失只做评估val的标签缺失不会直接报错只会输出一串极低的mAP。解决把val.txt里的第一行路径拿出来手动把.jpg改成.txt看文件是否存在再复用2.2的统计脚本分集合统计标签数。5.2 class index out of range类别编号比classes.txt还大训练到一半抛ValueError提示class index超范围原因很直接某个txt第一列的类别编号大于等于data.yaml里names的长度。最常见于COCO格式的json里category_id不是从0连续编号转换时直接写入category_id而没有映射成自己的编号。解决转换脚本走coco_category_id→name→自定义index两层映射并在3.4的验证脚本里加cls_id class_num检查。如果数据集自带的txt本身就不规范先写个脚本把所有txt里的类别编号摸一遍取最大值加一得到真实类别数再决定类别清单怎么定。5.3 推理画出的框全错位甚至缩成一条线转换格式时坐标没问题一到项目里截图或视频推理框明显偏上偏下有的框宽高变成0.1以下的细线。原因就是归一化坐标时分母用错了分辨率——拿缩略图的宽高去除原始标注的像素坐标或者COCO转YOLO时高度宽度都除以图像宽度而不是分别除以宽和高。解决转换和推理统一用同一份原图排查时反算某个txt坐标回像素值再按xml的bndbox画在图上对比。如果数值量级差得离谱查img_w和img_h的来源十有八九是图片尺寸取错。5.4 验证集和训练集图片重叠mAP虚高换随机种子重跑划分脚本跑着跑着train和val就重叠了。还有一次更隐蔽压缩包里自带VOC的ImageSets/Main/train.txt和val.txt图省事直接沿用结果VOC那份清单本身就把两类图片混在一起。原因是没有保证图片名全局唯一或者划分前没有先做去重。解决划分脚本只跑一次固定seed42之后每次训练都复用同一份train.txt和val.txt校验时用set做交集判断划分产物单独归档。5.5 白天效果好、夜间场景全废疲劳驾驶检测最吃紧的不是模型结构而是数据分布。白天样本占大头夜间闭眼、低头的正样本少模型学到的是“白天纹理”和“疲劳特征”的组合夜间一上真实场景就漏检。原因在于目标检测的特征对亮度敏感mosaic和hsv增强虽然能整体提亮但无法创造“夜间车窗反光仪表盘弱光”的真实纹理。解决补标夜间帧是最实在的比调任何超参都有效。如果暂时没有夜间样本把训练参数里的hsv_v从默认0.4提到0.6hsv_s提一点能缓解但治不了本。训练完必须单独用夜间视频测一版指标别把白天mAP直接当最终交付数据。6. 训练教程里的关键参数选对权重、跑通推理才敢交付6.1 训练命令四个参数决定训练成败yolo detect train datadata.yaml modelyolov8n.pt \ epochs100 imgsz640 batch16 device0data.yaml里写清楚path、train、val和namesimgsz一旦定下训练和推理必须保持一致batch受显存限制8G卡跑yolov8n用16稳一点先跑通再往上加patience30做早停验证指标连续30轮不涨就停不会浪费时间。疲劳驾驶场景里闭眼样本少epochs100通常够用多了反而容易过拟合。6.2 用验证集挑best.pt用置信度兜底训练结束输出两个权重best.pt按验证集mAP挑选last.pt是最后一轮。类别不平衡的疲劳驾驶数据集上last.pt经常被尾部类别拖累部署必须取best.pt。推理时conf设到0.3到0.5低于0.25误报明显增加——闭眼检测一旦误报比漏报还让人头疼。from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) results model.predict(night_test.jpg, conf0.4, saveTrue)这段代码是我的收尾步骤训练完先拿一张白天一张夜间图各跑一次看框的位置和置信度再决定要不要回炉补数据。以前我改过类别顺序没同步标签训练一整晚第二天用best.pt推理才发现全错。从那以后转换完、划分完、训练完各跑一遍验证脚本这个习惯帮我省下了大量返工时间。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑