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

📅 发布时间:2026/10/5 18:24:53
VOC疲劳驾驶数据集转YOLO格式训练避坑指南
简介数据集采用Pascal VOC格式包含4362张疲劳驾驶场景图片及对应XML标注文件面向自动驾驶、安全驾驶预警等领域的研究者与算法开发人员。四类标注分别为闭眼、闭嘴、睁眼、张嘴可用于训练疲劳状态检测模型通过眼部与嘴部状态判断驾驶员是否疲劳其中睁眼框4903个、闭嘴框3343个、闭眼框2485个、张嘴框936个类别分布完整且不均衡情况清晰。标注由labelImg工具完成矩形框准确合理但不保证训练模型精度。压缩包内主要包含4362个jpg图像与4362个xml标注文件另附使用说明txt整体大小368.57MB。目前已有2803人学习下载适合需要标准化VOC格式数据集的计算机视觉学习者可直接开展目标检测训练与验证免去自行采集和标注的繁琐过程也可作为模型评估的基准数据便于对比不同算法的疲劳检测效果。1. 拿到4362张VOC疲劳驾驶数据后别急着开train.py很多工程师第一次接触VOC格式的疲劳驾驶数据集解压后看到JPEGImages、Annotations、ImageSets几个文件夹第一反应是把路径填进yolov5或yolov8的配置文件直接训练。这个动作大概率会报no labels found或者训练流程能跑但结果玄学。原因很直接VOC格式和YOLO系列训练框架要求的格式不一样必须在中间做一次格式转换还要处理类别映射、数据划分、坐标归一化这些细节。4362张图、4个类别这个规模适合做疲劳驾驶检测的算法预研和模型选型验证但如果跳过格式转换和必要的训练策略直接硬train你会同时踩中小样本过拟合和标注格式错位两个坑。这篇按格式拆解→转换落地→训练调参→避坑→验证技巧的顺序把整个过程讲透。2. VOC格式结构拆解先弄清楚XML、图片和划分文件怎么配合2.1 目录结构图片、XML、ImageSets三件套谁缺了都不行标准的VOC数据集解压后通常长这样datasets/ ├── JPEGImages/ # 存放jpg原图 ├── Annotations/ # 与图片同名的xml标注文件 └── ImageSets/ └── Main/ # train.txt、val.txt、trainval.txtJPEGImages是图片本体Annotations里是每张图对应的标注文件ImageSets/Main下的txt文件不是标注而是划分文件——每一行是一个不带后缀的文件名告诉训练脚本哪些图片当训练集、哪些当验证集。第一次用VOC的人最容易在这里犯迷糊打开ImageSets看到的全是文件名没有框也没有类别还以为是数据集损坏了。这套结构的源头是PASCAL VOC数据集的组织方式后续很多检测框架的VOC格式数据都沿用了这一约定。和coco2017数据集结构做对比会更清楚COCO用单个json文件承载所有标注信息和划分信息而VOC把标注拆散成一图一个XML把训练/验证划分单独放进txt。每个文件夹之间的对应关系靠文件名维持所以文件名的准确性是整个数据集的命脉。拿到手的第一步应该是先列目录确认三件套是否齐全。有的分发版本会把图片和XML分开放有的干脆没有ImageSets。我见过一个版本把图片、标注、划分文件压缩成了三个独立的zip解压后目录层级混乱。在写转换脚本之前先花两分钟把结构理顺后面能省下一整天的排查时间。2.2 读一条XML标注bndbox是归一化之前的原始坐标用任意文本编辑器打开一个XML内容结构是固定的size节点下面有width和height两个子节点object节点一个目标一个里面放着name标签名和bndbox四个坐标值。坐标值是像素单位的绝对坐标xmin和ymin是框左上角的像素位置xmax和ymax是右下角的像素位置。这套坐标系是后续所有转换工作的基础。YOLO系列训练框架要求标注是归一化的中心点坐标加宽高而VOC给的是绝对像素坐标两者差一个换算步骤。换算公式不复杂但很多人会把方向和单位搞反后面转换章节会展开讲。XML里还有两个值得注意的字段truncated表示目标在图像边缘被截断difficult表示目标本身很难辨认。转换脚本里difficult这个字段要特别留意很多检测训练框架默认把difficult1的目标过滤掉如果不希望过滤需要在转换时做特殊处理或保留标记。实际项目里疲劳驾驶数据集中difficult样本通常是光线极暗或目标被遮挡的图硬让模型学这些样本容易把loss拉爆我一般倾向于转换时直接跳过并在日志里记录。读取XML不要用字符串截取匹配推荐用Python自带的xml.etree.ElementTree标准库不需要安装额外依赖。解析的关键是两步先取size下的宽高再遍历object数组取name和bndbox坐标。注意有的XML里object节点可能缺少name子节点这种就是脏数据解析时要做防御。2.3 先统计再动手核对图片数、XML数和类别分布的三个命令动手转换之前先做体检。重点看三件事图片数量和XML数量是否对得上、类别名称是不是正好4类、每类的样本数分布是否严重失衡。这三项直接用Shell命令就能看个大概。# 统计图片数量 ls JPEGImages | wc -l # 统计XML数量 ls Annotations | wc -l # 找出没有对应XML的图片 comm -23 (ls JPEGImages | sed s/\.jpg$// | sort) \ (ls Annotations | sed s/\.xml$// | sort)前两条命令就是简单的计数第三条用comm做差集比较左列是有图没标注的右列是有标注没图的。comm命令要求两个输入都排好序所以两边都加了sortsed负责把文件名的后缀去掉保证两边拿出去比较的是纯文件名。如果第三条命令有输出说明存在孤儿图片。常见原因有两个分发时漏打包了一部分标注或者文件名大小写不一致导致对应关系断裂。处理办法很简单把孤儿图片挪出JPEGImages目录不要让训练脚本因为找不到标签直接报错。这类问题在第三方数据集里并不少见先处理干净再进入下一步。统计类别分布需要用Python读一遍所有XML核心逻辑就是遍历Annotations目录对每个XML里的object取name字段计数。跑完你会发现疲劳驾驶数据集普遍存在类别不平衡正常驾驶的样本量通常远大于打哈欠、闭眼这类状态样本有的类别可能只有几百张。这个数字直接决定后面的数据增强和损失函数策略先量化再决定怎么做。3. 把VOC转成YOLO训练格式转换脚本与参数细节3.1 为什么要转VOC不能直接喂给YOLO系列训练yolov5训练自己的数据集、yolov8训练自己的数据集时官方要求统一格式每张图对应一个同名txt文件每一行是一个目标格式为class_id x_center y_center width height四个坐标值全部是相对图片宽高的比例取值在0到1之间。打开一个转换好的标签文件看到的就是这样0 0.512 0.435 0.046 0.032 1 0.628 0.712 0.033 0.021第一列是类别id从0开始编号后面四列分别是中心点x、中心点y、目标宽度、目标高度全部是归一化小数。YOLO系列之所以用归一化坐标是因为训练时图片会被resize到统一输入尺寸比如640×640如果标注存放的是绝对像素值resize之后框就全错位了。归一化坐标对resize天然免疫这也是这套格式能通用于不同分辨率图片的原因。VOC的XML标注是绝对像素坐标两者中间差一个换算步骤。把VOC转成YOLO格式本质上就是读XML、算归一化坐标、写出txt。这个转换是绕不过去的除非你打算换用原生支持VOC的检测框架或者自己写一个DataSet类在训练时实时解析XML。有一点要提醒不同来源的VOC数据集类别名不一定一致。同样是打哈欠有的数据集标yawn有的标yawning还有的标mouth_open。二章里让你做类别统计目的就是先把实际类别名列出来再决定映射关系不要想当然地套模板。3.2 转换脚本XML解析、归一化与类别映射以下这段脚本是转换的核心只负责解析单个XML并输出一条归一化的标注行。import xml.etree.ElementTree as ET from pathlib import Path from PIL import Image # 类别映射顺序必须与后续data.yaml中的names顺序一致 CLASSES [normal, yawn, closed_eye, phone] def convert_one_xml(xml_path, out_path, img_path): tree ET.parse(xml_path) root tree.getroot() # 用PIL读取图片实际宽高不要完全信任XML里的size值 with Image.open(img_path) as img: width, height img.size if width 0 or height 0: print(f[错误] 图片尺寸异常: {img_path}) return False lines [] for obj in root.iter(object): name obj.find(name).text.strip() # 未出现在映射表里的类别直接跳过并打印日志供排查 if name not in CLASSES: print(f[跳过] 未知类别 {name} in {xml_path}) continue # difficult字段在此记录但不过滤需要过滤时加continue diff obj.find(difficult) difficult int(diff.text) if diff is not None else 0 xmin float(obj.find(bndbox/xmin).text) ymin float(obj.find(bndbox/ymin).text) xmax float(obj.find(bndbox/xmax).text) ymax float(obj.find(bndbox/ymax).text) # 归一化换算中心点取最小和最大边的中点再除以图片宽或高 cx (xmin xmax) / 2.0 / width cy (ymin ymax) / 2.0 / height w (xmax - xmin) / width h (ymax - ymin) / height # 越界保护防止浮点误差导致的越界值干扰训练 cx min(max(cx, 0.0), 1.0) cy min(max(cy, 0.0), 1.0) w min(max(w, 0.0), 1.0) h min(max(h, 0.0), 1.0) lines.append(f{CLASSES.index(name)} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) Path(out_path).parent.mkdir(parentsTrue, exist_okTrue) with open(out_path, w) as f: f.write(\n.join(lines)) return True这个函数里最核心的是四行归一化计算。x_center等于(xminxmax)/2再除以图片宽度y_center同理目标宽度用xmax减xmin除以图片宽度高度同理。换算公式本身不难翻车往往出在除数上有人直接用XML里的size节点但部分数据集的size字段和图片实际尺寸不一致比如标注完图片又被压缩过一次XML却没有同步更新。所以脚本里用PIL现读图片宽高做除数这是多次踩坑后养成的习惯。min和max的越界保护也是必要的一步。浮点运算在极端情况下会产生1.0000001这种越界值YOLO训练时对标签范围有校验越界值会导致训练中断。裁剪到[0,1]能保证训练不因标签报错但这个保护会掩盖标注本身的问题所以脚本里只做保护不静默吞掉问题配合日志输出方便回查。下面这段是批量转换脚本负责读取ImageSets划分文件并生成训练集和验证集的目录结构。def build_yolo_dataset(voc_root, output_root): voc_root Path(voc_root) output_root Path(output_root) for phase in [train, val]: list_file voc_root / ImageSets / Main / f{phase}.txt if not list_file.exists(): print(f[警告] 缺少划分文件 {list_file}) continue with open(list_file) as f: names [line.strip() for line in f if line.strip()] img_out output_root / images / phase lbl_out output_root / labels / phase img_out.mkdir(parentsTrue, exist_okTrue) lbl_out.mkdir(parentsTrue, exist_okTrue) for name in names: src_img voc_root / JPEGImages / f{name}.jpg src_xml voc_root / Annotations / f{name}.xml dst_img img_out / f{name}.jpg dst_lbl lbl_out / f{name}.txt if not src_img.exists(): print(f[跳过] 图片不存在: {src_img}) continue if not src_xml.exists(): print(f[跳过] XML不存在: {src_xml}) continue try: convert_one_xml(src_xml, dst_lbl, src_img) dst_img.symlink_to(src_img.resolve()) except Exception as e: print(f[失败] {name}: {e})注意循环是从ImageSets划分文件读取文件名列表只转换实际参与训练和验证的图片而不是把JPEGImages里所有图都转一遍。很多转换脚本这一步做反了把所有图片全量转换结果生成了一大堆没有划分文件引用的标签白白浪费磁盘还容易引入脏数据。图片用的是symlink_to软链接方式只新建labels目录下的txt文件图片通过软链接指向原目录。4362张图如果全部复制一份磁盘空间直接翻倍软链接能省掉这部分开销。如果你的环境不支持软链接比如Windows某些目录权限受限把symlink_to改成copy2即可。转换完成后在输出根目录写一个data.yaml这是YOLO系列训练框架的统一入口配置train: ./data/voc2yolo/images/train val: ./data/voc2yolo/images/val nc: 4 names: [normal, yawn, closed_eye, phone]data.yaml的train和val路径要指向images目录而不是labels目录框架会自动在同级目录下找labels。nc必须和names列表长度一致。names的顺序必须与CLASSES列表保持一致这个顺序一旦错位训练出来的模型类别全部错乱模型自己不会报错只有推理时你才会发现类别对不上属于隐蔽性最强的坑。3.3 转换后验证可视化box和检查越界的两个习惯转换脚本跑完先别急着进训练。随机抽几十张图把yolo格式的txt读回来换算成像素坐标在图上把框画出来另存为可视化文件。这一步能一次性暴露三类问题归一化方向搞反了、类别id对不上、图片和标注错位。可视化脚本的核心逻辑很简单读图拿宽高读txt拿归一化坐标乘回像素值后用cv2.rectangle画矩形框再用cv2.putText把类别名写在框上方。跑完浏览一遍生成的可视化图重点看框的位置是否贴合目标。疲劳驾驶数据集里如果发现闭眼的标注把整个头部都框住了说明数据集本身的标注粒度偏粗这个信息很关键它决定你要训练的是眼睛状态检测器还是头部姿态检测器。第二个验证动作是统计所有txt文件里有没有零宽高或负值。零宽高框来自XML中xmax等于xmin的情况训练时会导致loss计算异常。转换脚本虽然做了裁剪处理但裁剪本身掩盖了脏数据的存在。建议在批量转换脚本里加一个计数器把越界样本的路径单独输出成一个log文件最后统一排查。这个动作属于完善习惯不做也不一定出问题但做了能让你对数据集的信任度有一个具体的判断依据。4. 4类别疲劳驾驶检测的训练细节从增强策略到模型选型4.1 四类标签先确认打印类别名确认边界标题写了4个类别但没有给出具体类别名。常见的疲劳驾驶数据集一般把正常驾驶、打哈欠、闭眼、打电话作为四类有的版本把打电话换成低头看手机或吸烟。不同数据集的命名方案确实存在差异所以拿到手第一件事还是执行一遍类别统计把所有object name打印出来对照确认。这一步不能跳。疲劳状态检测和通用目标检测最大的区别在于标签边界模糊打哈欠和说话时嘴型接近图像特征高度重合闭眼和正常眨眼之间没有严格的帧级阈值标注员只能靠主观判断划线。这类标注争议在疲劳驾驶数据集里远比通用目标数据集严重。打印类别名之后建议顺手抽查每个类别5到10张图确认标注框的粒度眼睛区域的目标通常标注的是眼睛框还是整个头部框这决定了模型学到的模式完全不同。如果统计发现某个类别的样本数极少比如只有几十张这类别的分类器基本学不出发判别力。常见做法是两类合并或者放弃该类别的独立识别改做异常检测。4362张图平均到4类每类约1000张在检测任务里属于偏少的量级类别不平衡一定要在训练前量化清楚。4.2 增强策略小目标、小样本用哪些手段疲劳驾驶检测的目标有一个显著特点小。眼睛、嘴巴在常见的驾驶舱图像分辨率下只占几十个像素典型的小目标场景。这带来两个后果模型必须擅长小目标检测增强策略不能只靠全图resize。YOLO系列默认开启的mosaic增强适合这个场景它把四张图拼接在一起训练相当于在小batch下增加了小目标在整体样本中的占比建议保留。在此基础上我常用的三个增强项是随机裁剪放大、亮度对比度扰动、随机旋转。随机裁剪放大对小目标特别有效按一定概率把图像局部区域裁剪出来再resize回输入尺寸强制模型学习细微纹理而不是只依赖全局轮廓。亮度扰动对应驾驶舱内光照变化对眼睛这种局部特征影响很大。旋转角度控制在正负15度以内超过这个范围会产生大量无意义的背景区域。一个容易犯的错误是过度增强。疲劳驾驶检测中打哈欠和头部姿态的语义强相关如果把图旋转90度闭眼变成侧脸闭眼数据分布就被扭曲了。增强策略不是越多越好关键是和应用场景的光照、姿态分布对齐。做增强前先想清楚模型的部署环境如果采集图像是固定的行车记录仪视角训练时就不要做大幅度的仿射变换。4.3 训练参数迁移学习、anchor重算和学习率4362张图的小规模数据集不建议从零初始化训练。加载COCO预训练权重做迁移学习是更稳妥的路径检测模型在COCO上学过的纹理、边缘、形状模式对眼睛、嘴巴、手部动作依然有效真正需要新学的是闭眼打哈欠这些语义组合而不是底层特征。从零训练在这个数据量级上收敛困难且极易过拟合。anchor需要重新计算。YOLO系列默认的anchor尺度是从COCO数据集聚类出来的以行人、车辆、日常物体为主和疲劳驾驶目标的小尺寸分布不匹配。YOLOv5训练时开启autoanchor参数让框架重新聚类YOLOv8通过调整模型配置里的anchor相关参数实现。本质都是对训练集标签做一次k-means聚类重新生成匹配目标尺度分布的初始anchor。训练参数可以参考下面这组组合参数推荐值说明img_size640输入分辨率显存足够可试960batch_size16显存不够降到8学习率同步减半epochs150-200小数据集训练太久反而过拟合lr00.01配合cosine退火策略label_smoothing0.1对噪声标注有一定容忍度model_sizes或m4362张图撑不起x或l版本输入分辨率640是精度和显存的平衡点追求更高精度可以试960但训练时间会显著增加先验证640的效果再决定。batch_size降到8时学习率也要同步从0.01降到0.005否则梯度更新噪声变大loss曲线会震荡。epochs不建议超过200小数据集在这个区间后验证集指标通常会停滞或开始回落。label_smoothing对标注边界模糊的疲劳驾驶数据集很友好它能把one-hot标签软化给模型一定的容错空间。5. 避坑手册4362张小数据集最常见的5个翻车现场5.1 现象一loss正常下降但验证mAP只有零点几loss曲线收敛得很漂亮验证mAP却惨不忍睹这是小数据集训练最典型的挫败感来源。原因一般是两个第一是训练集和验证集分布不一致比如划分文件把白天和夜晚的图全放在了训练集验证集只剩下夜晚数据第二是样本总量太少模型在训练集上记住了图像细节而非泛化特征。解决方法是先回去检查划分文件确认train和val在类别分布、时间段分布上大致一致然后加强训练集的增强力度同时开启早停。验证集每类至少保留50张图少于这个数算出来的mAP波动极大不具备参考意义。5.2 现象二验证集指标比训练集还好验证mAP高于训练mAP看起来是好事实际几乎都是数据泄漏。原因在于ImageSets/Main下面的train.txt和val.txt可能存在重复行同一张图既被划进训练集又被划进验证集模型直接背出了验证集的答案。另一个可能是数据被人为清洗过训练集里都是困难样本验证集全是简单样本。解决方法是先对两个划分文件做一次交集检查第二章里的comm命令就能用上。发现重复行直接把这些图片从验证集移除因为它们对模型来说已经不是未知数据了。5.3 现象三训练时报no labels found但XML明明存在YOLO框架对图片和标签的命名要求严格。图片叫00001.jpg标签必须叫00001.txt标签必须放在labels对应子目录下。很多人转换时目录结构完全正确但训练时依然报没有找到标签。原因往往是转换脚本里输出文件名的后缀处理出了问题生成的标签文件实际叫00001.xml.txt而不是00001.txt。图片和标签的同名对应关系断裂框架扫描标签目录时匹配不上。解决方式是在转换脚本里显式把输出文件名拼接为name .txt不要直接拿XML路径做后缀替换。训练前随机找5张图检查同名txt是否存在于labels目录这个动作十几秒能省掉一上午的排查时间。5.4 现象四小目标漏检闭眼区域经常测不到训练完成后测试单张图片头部能被检出来但细小的闭眼区域频繁漏检。原因有三个递进关系数据集标注粒度本身就是头部级别的框而不是眼睛级别的框anchor尺度与训练数据不匹配输入分辨率太低把小目标细节磨没了。解决的优先级依次是先确认标注粒度如果框是头级别的要么重新标注眼睛区域要么调整任务目标为头部姿态检测而不是眼睛状态识别然后重新聚类anchor最后才考虑把输入分辨率从640提到960。多数情况下前两步就能解决直接调分辨率是最费算力的方案。5.5 现象五闭眼和打哈欠互相误报模型把打哈欠的图判成闭眼或者反过来这类混淆在疲劳驾驶检测里很常见。原因有两层。标注层面打哈欠时人眼会自然眯起标注员把这类图同时打了两个标签而模型在一张图上只能预测一个主类别标注本身就给模型埋下了矛盾的监督信号。模型层面如果输入图只包含人脸局部模型缺乏上下文信息来判断这个动作究竟是打哈欠还是闭眼。解决方式是数据清洗时把打哈欠但眼睛眯起的样本单独归类或者把打哈欠的标注框缩小到嘴部区域让模型分别学习嘴部和眼部的特征两个类别的特征空间自然就分开了。如果数据集没有这个标注粒度就接受模型的边界误差在后处理里做时序滤波连续多帧判定同一个状态才触发报警用时间维度弥补单帧分类的不确定性。6. 从过拟合实验开始小样本训练链路验证的进阶技巧6.1 过拟合实验先行先让模型记住再谈泛化4362张的规模下我习惯先用每类50张图组成一个mini训练集不保留验证集直接原地训练。如果模型在100到200步内loss降不下来说明训练链路有bug——标签路径错误、增强配置异常、anchor不匹配都属于这一类问题。模型在这个mini数据集上应该能轻松背下来如果连背都背不动先回头查数据和预处理流程。过拟合实验通过后再上全量数据对比两次的mAP。全量训练的mAP如果反而下降说明数据集本身噪声占主导这时候投入时间做标注清洗远比继续堆epochs或换更大的模型更划算。6.2 序列验证疲劳驾驶检测和通用检测的分水岭单帧mAP只能说明模型在静态图上的表现部署到车内场景要面对的是连续视频流。闭眼在时间维上是持续事件眨眼是短暂瞬间模型偶发单帧误判可以接受但如果闭眼检测在连续帧里反复闪烁模型就不可用。常见的做法是在输出端加滑动窗口统计取最近5到10帧的检测结果连续N帧判定同一状态才触发报警。这段逻辑写起来不到50行却能把模型从单帧不稳定变成时序稳定是项目从算法演示走向可部署的关键一步。我的习惯是单帧指标用来筛模型序列指标用来定方案。先过拟合验证链路再按类别拆解mAP最后用视频序列做最终验收这套流程在4362张的小数据集上很稳妥。希望这些经验能帮到你。本文还有配套的精品资源点击获取