ARTICLE DETAIL

资讯详情

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

VOC转YOLO格式:疲劳驾驶数据集训练实战与避坑指南

VOC转YOLO格式:疲劳驾驶数据集训练实战与避坑指南 简介这是一份基于Pascal VOC格式的疲劳驾驶检测数据集面向计算机视觉、智能驾驶安全及驾驶员监控等方向的学习者、研究人员与算法工程师可直接用于目标检测或疲劳状态识别任务。数据包共含8725个文件即4362张jpg图片与对应的4362个xml标注文件外加1个txt使用说明整体压缩后约368.57MB。所有标注由labelImg完成以矩形框标出目标区域覆盖closed_eye、closed_mouth、open_eye、open_mouth四个类别分别代表闭眼、闭嘴、睁眼、张嘴状态四个类别框数分别为2485、3343、4903、936合计过万分布较均衡可用于训练多状态疲劳检测模型。xml严格遵循VOC格式与图片同名对应便于接入YOLO、SSD、Faster R-CNN等主流框架也易于二次筛选或转换。该数据集不含分割txt需自行划分训练集与验证集作者特别声明不保证模型精度但标注准确合理。目前已有2803人学习下载可大幅节省数据采集与人工标注成本适合需要快速验证模型效果或构建驾驶行为分析系统的开发场景。1. 疲劳驾驶数据集到手后先别急着开训做疲劳驾驶检测的同行应该都有体会模型结构不是最难的部分最难的是搞到一份标注干净、类别统一、能直接喂给训练脚本的数据集。这套VOC格式的疲劳驾驶数据集一共4362张图、4个类别刚好卡在“小数据集能跑通项目”和“大数据集够做预训练”之间的甜点位置。如果你正在做驾驶员状态监测、车队安全预警或者驾考设备这份数据集能让你少花两周时间在数据清洗上。先说结论VOC格式决定了它需要经过一次格式转换才能喂给YOLO系列但转换成本极低4个类别覆盖的是驾驶场景里最典型的几种危险状态不是那种标注得乱七八糟的通用数据集。下面我会把数据集内部结构、格式转换脚本、训练配置和最容易踩的坑一次讲完全程可复现。2. 拆开数据集看VOC标注目录结构、XML字段与4个类别的分布2.1 VOC数据集的固定目录结构VOC格式本身不复杂但它有一套约定俗成的目录规矩拿到数据集第一件事就是核对这些目录是否存在、路径是否对得上。标准的VOC结构长这样dataset_root/ ├── Annotations/ # 存放所有XML标注文件 │ ├── 000001.xml │ └── ... ├── JPEGImages/ # 存放所有原始图片 │ ├── 000001.jpg │ └── ... ├── ImageSets/ │ └── Main/ # 存放train.txt / val.txt / trainval.txt │ ├── train.txt │ ├── val.txt │ └── trainval.txt拿到数据集后第一步不是看图片而是检查这三个目录的文件数量是否匹配。正常情况下Annotations里的XML文件数应该等于JPEGImages里的图片数每张图对应一个同名的XML。如果发现XML比图片少说明有图片没标注训练时会白算一张图反过来则是标注了不存在的图片转换脚本会直接报错。ImageSets/Main下的txt文件决定了哪些图进训练集、哪些进验证集文件里每行是图片的文件名不带扩展名。这套划分逻辑是从Pascal VOC时代传下来的YOLO训练时不直接用这个文件而是需要转换成自己的目录规则后面我会给转换脚本。2.2 XML里到底存了什么一个完整的标注文件解剖随便打开一个XML文件你会看到这样的结构annotation folderJPEGImages/folder filename000001.jpg/filename size width1280/width height720/height depth3/depth /size object nameyawn/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin350/xmin ymin210/ymin xmax590/xmax ymax430/ymax /bndbox /object /annotation这些字段里size是整个文件最容易忽略但最关键的部分。width和height是图片原始尺寸后面做坐标归一化全靠这两个值写死或者写错都会导致标注框偏移。object节点下的name是类别名bndbox是真实框坐标注意是xmin/ymin/xmax/ymax格式代表左上角和右下角两个点的像素坐标不是中心点坐标。这里有三个坑值得提前说。第一truncated和difficult字段在转换时如果不处理YOLO训练时会当成普通样本参与计算导致模型被困难样本带偏第二2. 同一张图可以存在多个object节点意味着一个画面里可能同时出现驾驶员闭眼和打哈欠两种情况转换脚本要写循环而不是只取第一个对象第三depth字段通常恒为3RGB三通道但个别数据集的截图可能是灰度图存成了JPG此时depth会写1转换公式不变但训练时要注意输入通道。2.3 4个类别分别是什么类别定义与典型场景这套数据集是4个类别以驾驶场景疲劳状态检测为基准。按常见标注习惯这4类一般是打哈欠、闭眼、打电话和正常驾驶。前三个是危险状态第四个是正常状态用于做负样本。类别的具体命名在VOC里是字符串不同数据集的命名习惯不一样有的叫yawn、close_eye、phone、normal有的叫mouth_open、eye_closed、distraction、safe。动手训练前要先确认命名因为转换脚本里的类别映射表是硬编码的写错了就全乱套。用下面这段命令快速提取所有XML里的类别名看看实际命名是什么grep -rh name Annotations/ | sort | uniq -c | sort -rn这条命令会统计每个类别出现的次数既能看到命名也能看到类别分布。164张图分4类平均每类1090张但实际分布不可能这么均匀。如果发现某个类别只有300张另一个类别有1800张训练时就要处理类别不平衡问题后面第4章会讲具体方案。2.4 4362张意味着什么数据规模与训练策略的关系在目标检测领域4362张图是一个分水岭。对于YOLOv5s这类轻量模型这个规模勉强够用配合预训练权重和增强策略能跑到可用水平对于YOLOv8m以上或者更大的模型这个规模偏小必须使用COCO预训练权重做迁移学习从零训练大概率过拟合。另一个关键点是单张图里的目标数量。很多驾驶场景数据集的一个特点是单张图里目标少可能只有1到2个标注框这和COCO那种密集检测场景完全不同。如果目标是闭眼检测一个框就够了但如果要做驾驶员全身姿态估计可能需要多个框标注不同部位。这套数据集是4类目标检测不是姿态估计别拿去做关键点任务格式不通用。还有一个容易被忽略的点图片尺寸。VOC格式的XML里有size字段记录了原始尺寸但4362张图的尺寸未必统一。有的数据采集来自行车记录仪输出是1920x1080有的来自摄像头截图可能是1280x720。训练时YOLO会做letterbox缩放尺寸不统一问题不大但要注意标注坐标是相对于原始图还是已经缩放过的图转换脚本里用XML里的width/height做分母就不会错。3. 把VOC转成YOLO格式转换脚本与四个边界坑3.1 为什么要转换VOC和YOLO的标注差异YOLO系列训练时读的不是XML而是每个图片对应一个同名的txt文件。这个txt文件每行是一行标注格式为class_id x_center y_center width height其中四个坐标值全部是归一化到0到1之间的小数。对比一下就能看出差异VOC存的是真实像素坐标的左上角和右下角YOLO存的是归一化后的中心点和宽高。VOC用XML文件存一张图的所有标注YOLO用txt每行存一个目标VOC的类别是字符串YOLO的类别是整数索引。这就是为什么必须写转换脚本而且转换过程中最容易出错的就是坐标系变换。3.2 完整转换脚本从XML到txt的Python实现下面这个脚本可以直接复制使用它处理了目录遍历、XML解析、坐标归一化、边界裁剪和类别映射五个环节import os import xml.etree.ElementTree as ET # 类别映射表按你的XML里实际类别名调整 class_dict { yawn: 0, # 打哈欠 close_eye: 1, # 闭眼 phone: 2, # 打电话 normal: 3 # 正常驾驶 } voc_root dataset_root yolo_root yolo_dataset # 创建YOLO目录结构 images_dir os.path.join(yolo_root, images) labels_dir os.path.join(yolo_root, labels) os.makedirs(images_dir, exist_okTrue) os.makedirs(labels_dir, exist_okTrue) ann_dir os.path.join(voc_root, Annotations) img_dir os.path.join(voc_root, JPEGImages) for xml_file in os.listdir(ann_dir): if not xml_file.endswith(.xml): continue xml_path os.path.join(ann_dir, xml_file) tree ET.parse(xml_path) root tree.getroot() # 读取图片尺寸 size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) # 对应每个图片的txt标注文件 txt_name xml_file.replace(.xml, .txt) txt_path os.path.join(labels_dir, txt_name) lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_dict: print(f警告: {xml_file} 里有未知类别 {name}, 已跳过) continue # 解析bndbox坐标 bndbox obj.find(bndbox) xmin int(float(bndbox.find(xmin).text)) ymin int(float(bndbox.find(ymin).text)) xmax int(float(bndbox.find(xmax).text)) ymax int(float(bndbox.find(ymax).text)) # 边界保护裁剪到图像范围内 xmin max(0, min(xmin, img_w)) xmax max(0, min(xmax, img_w)) ymin max(0, min(ymin, img_h)) ymax max(0, min(ymax, img_h)) # 防止宽高为零标注错误的兜底 if xmax xmin or ymax ymin: print(f警告: {xml_file} 里存在无效框, 已跳过) continue # VOC坐标转YOLO归一化坐标 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 # 归一化后坐标理论上在[0,1]区间浮点保护 x_center max(0.0, min(1.0, x_center)) y_center max(0.0, min(1.0, y_center)) box_w max(0.0, min(1.0, box_w)) box_h max(0.0, min(1.0, box_h)) class_id class_dict[name] lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) with open(txt_path, w) as f: f.write(\n.join(lines)) # 复制对应图片到新目录 img_name xml_file.replace(.xml, .jpg) img_src os.path.join(img_dir, img_name) if os.path.exists(img_src): img_dst os.path.join(images_dir, img_name) os.system(fcp {img_src} {img_dst}) else: print(f错误: {img_name} 图片不存在, 已跳过对应标注) print(转换完成)这段脚本的逻辑分四个层次先解析XML拿图片尺寸和所有标注框然后对每个框做坐标变换核心公式是把左上角和右下角的像素坐标转成中心点加宽高的归一化坐标变换前做边界裁剪防止标注越界最后按class_id x_center y_center width height的顺序写入txt文件同时把图片复制到新目录。3.3 参数与细节说明坐标公式为什么要这样写转换公式的核心是“除以图片宽高得到比例”。(xmin xmax) / 2是中心点的像素坐标再除以img_w得到归一化中心点x坐标xmax - xmin是框的像素宽度除以img_w得到归一化宽度。这个变换本身不难难的是处理好三个边界情况。第一XML里的坐标可能是浮点数。有的标注工具会输出带小数点的坐标比如xmin350.5所以代码里用了float()而不是int()否则会报错。第二个别标注框的坐标超出了图片范围可能是标注工具的手误边界裁剪确保转换结果不会出现负数或大于1的坐标值,这会导致YOLO训练时NaN损失。第三同一张图里可能有多个目标脚本用循环遍历所有object节点而不是只取第一个这是VOC多目标场景最常见的错误写法。3.4 转换后必须做的三个验证步骤转换脚本跑完不是终点验证转换结果才是重点。第一个验证是数文件数find yolo_dataset/labels -name *.txt | wc -l find yolo_dataset/images -name *.jpg | wc -l两个数字必须一致。第二个验证是看txt内容是否合理cat yolo_dataset/labels/000001.txt正常输出应该是几行格式如0 0.452 0.331 0.216 0.278这样的数字。如果出现超出行数或者异常值比如坐标大于1或者宽高为0说明转换有bug。第三个验证是可视化把标注框画回图片上看位置对不对。这一步最直观但经常被跳过后面第5章避坑里会专门讲怎么画。4. 用YOLOv8在本地跑通疲劳驾驶检测划分、配置与训练命令4.1 数据集划分train/val不是随便切通过前面的转换脚本YOLO格式的数据集已经躺在yolo_dataset目录里了。接下来把数据划分成训练集和验证集。4362张图按8:2划分训练集约3490张验证集约872张。这里有个细节如果原始数据的ImageSets/Main目录里已经提供了train.txt和val.txt优先用它们因为数据集作者可能按场景或时间段做了划分避免同一个人的图片同时出现在训练集和验证集里造成虚假的高精度。如果没有现成划分文件按下面这个脚本随机划分就行import os import random random.seed(42) images_dir yolo_dataset/images labels_dir yolo_dataset/labels all_images [f for f in os.listdir(images_dir) if f.endswith(.jpg)] random.shuffle(all_images) train_ratio 0.8 split_idx int(len(all_images) * train_ratio) train_files all_images[:split_idx] val_files all_images[split_idx:] # 生成训练集和验证集的txt清单 with open(train.txt, w) as f: for img in train_files: f.write(os.path.join(images_dir, img) \n) with open(val.txt, w) as f: for img in val_files: f.write(os.path.join(images_dir, img) \n) print(f训练集 {len(train_files)} 张, 验证集 {len(val_files)} 张)划分时有个容易忽略的点YOLO训练时需要保证每个图片都有对应的标注文件但反过来标注文件没有对应图片也不行。用set做一次双向检查是值得的很多翻车事故就是划分脚本把标注文件路径写错了训练时读不到标签导致所有图片被自动跳过训练完才发现mAP全是0。4.2 data.yaml配置路径和类别数不能写错YOLOv8用data.yaml文件配置数据集路径、类别数量和类别名。内容如下path: ./yolo_dataset train: train.txt val: val.txt names: 0: yawn 1: close_eye 2: phone 3: normal三个地方容易出错path后面是数据集根目录train和val是相对路径还是绝对路径取决于path的写法如果不放心就用绝对路径names的索引从0开始顺序必须和转换脚本里的class_dict完全一致否则模型学出来的类别名和真实类别对不上第四行是类别数量由names列表长度决定不用显式写但如果你删掉一个类别一定要同步改names。4.3 训练命令与参数选择小数据集的保命参数对于4362张这种规模的数据集推荐用小模型加预训练权重先跑通流程再考虑精度。命令如下yolo train \ modelyolov8s.pt \ datadata.yaml \ epochs100 \ imgsz640 \ batch16 \ workers4 \ device0 \ patience20 \ pretrainedTrue几个参数的选择逻辑modelyolov8s.pt加载COCO预训练权重s版本参数量适中在单卡上训练速度可以接受。如果显存不够把batch降到8或4同时把imgsz从640降到512——疲劳驾驶检测的目标通常集中在人脸区域512分辨率足够用。patience20是早停参数连续20个epoch验证集指标不涨就自动停止防止小数据集上后期过拟合。要重点检查的是device参数。Windows机器上如果有NVIDIA显卡device0是比较稳的如果只有CPU改成devicecpu但训练时间会拉长几倍。macOS的M系列芯片可以试devicemps但这个后端在YOLO里还不够稳遇到报错就老老实实用CPU跑。4.4 训练过程的监控信号loss降不下去怎么办训练启动后不要只盯着进度条要看三个指标train/box_loss、val/box_loss和mAP0.5。正常的训练曲线是box_loss稳步下降mAP0.5逐步上升。如果发现train/box_loss下降但val/box_loss快速上升这是典型的过拟合信号处理方式是把epochs调低或者加数据增强。另一个常见现象是mAP0.5前期一直是零。这时候先别慌小数据集上目标较少前10个epoch预测置信度普遍很低mAP为零是正常的。如果跑了30个epoch还是零就要回去检查标注文件是否为空、类别索引是否从零开始、图片路径是否有中文——这三个问题任何一个都会让训练静默失效。5. 避坑指南疲劳驾驶数据集训练中最常见的5个翻车点5.1 坐标转换方向搞反训练出来的框全是上下颠倒现象训练完成后用模型推理检测框能框住目标但位置明显偏移比如闭眼检测框画在了额头上。原因VOC的ymin/ymax坐标有的标注工具是左上角为原点有的是左下角。多数工具按左上角原点但如果你用的数据标注平台配置了不同坐标系转换公式里的y_center计算就会出错。解决转换脚本里加一行调试输出随机挑一张图把XML里的原始坐标和转换后的txt坐标都打出来人工核对比例关系。如果你发现y_center算出来大于1或者小于0坐标系铁定反了。5.2 标注框宽高为零训练直接崩现象训练过程中出现NaNloss或者某个epoch结束时loss值直接变成nan。原因个别XML里的标注框xmin xmax这类标注一般是标注工具的手误画框时没拖出有效面积。转换脚本里如果没有判断宽高大于零生成的txt里会出现宽度为0的无效标注训练时算loss除零。解决转换脚本里已经加了if xmax xmin or ymax ymin的判断这会跳过无效框。但如果你没跑转换脚本而是直接手工改过标注记得检查所有txt文件里有没有0.000000宽度的行有就删掉并同步图片。5.3 类别不平衡导致正常驾驶类目mAP虚高现象训练完后normal类别的mAP高达0.95但close_eye只有0.4。原因4362张图里正常驾驶的画面远多于危险状态模型学到的是“凡是没检测到其他三类就算正常”而不是真的学会了区分正常驾驶的特征。解决观察训练日志里的图片类别分布如果normal类超过40%可以用yolo train的class_weights参数给少数类加权。常见做法是统计每类目标数量把少数类的loss权重调大1.5到2倍强制模型关注闭眼和打电话这类危险状态。5.4 图片尺寸不统一letterbox后坐标错位现象验证集mAP正常但部署到真实的摄像头画面后检测框错位。原因训练时YOLO会把图片缩放到imgsz大小如果原始图片有1920x1080的高分辨率截图也有640x480的压缩小图模型见到的图像内容分布跨度大推理时对不同分辨率输入的适应性差。解决转换前统一图片尺寸。常见做法是用OpenCV把所有图片的较长边缩放到1280短边等比缩放不足部分填黑再更新XML里的size字段。这个过程比较费时但能显著提升模型在真实场景下的稳定性。5.5 验证集和训练集数据泄露论文级精度但实战全废现象训练集mAP 0.97验证集mAP 0.96看起来非常好但拿到现场拍的照片一测精度掉一半。原因数据划分时没有按“人”来切。如果数据采集时同一个司机在不同时间段拍了多张照片随机划分会让同一个人的闭眼图片同时出现在训练集和验证集里模型实际上是记住了这个人长什么样而不是学会了判断闭眼状态。解决按视频片段或采集会话做分组划分。4392张图如果来自几十个不同录像片段把同一个片段的图全部放进训练集或验证集不让它分散。这是疲劳驾驶数据集的经典坑论文里好看的数字不一定是真本事。6. 验证模型效果用混淆矩阵和PR曲线检验4分类的边界训练结束后验证工作比训练本身更重要。在疲劳驾驶场景里我们要的不是“平均指标好看”而是“危险状态不能漏”。跑一下验证集评估yolo val modelruns/train/exp/weights/best.pt datadata.yaml结果会生成confusion_matrix.png和PR_curve.png。先看混淆矩阵重点关注“真实闭眼被预测成打哈欠”这类跨类别混淆。在疲劳驾驶检测里闭眼和打哈欠常常同时出现模型把两者搞混一般不影响最终判断但如果是“闭眼被预测成正常”这就是一个必须修复的严重漏检。再看PR曲线尤其关注close_eye类别的曲线。如果曲线在置信度0.7以上才陡然上升说明模型对这个类别的置信度普遍偏低推理时需要把该类别的置信度阈值单独调低。部署时常见做法是给close_eye设0.3的阈值给normal设0.7的阈值而不是统一用默认的0.25。最后一个值得做的验证是用测试视频跑一次推理把检测结果输出成标注视频逐帧看模型在连续画面下的表现。单帧检测精度高不代表视频里能用——目标闪烁、相邻帧漏检都会让实际体验大打折扣。如果视频里闭眼状态出现抽风式检测给推理代码加一个简单的状态机判断连续3帧检测到闭眼才报警比调模型参数见效更快。我自己的习惯是训练完先不看mAP先挑10张典型的夜间驾驶图、10张戴墨镜的图、10张逆光图跑一遍用肉眼看结果再决定要不要调训练参数。数据集的边界往往不在平均指标里而在这些难例里。如果你也踩过“指标漂亮但实战翻车”的坑大概率是漏掉了这一步——希望帮到你。本文还有配套的精品资源点击获取
返回列表