拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

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

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.txt

JPEGImages是图片本体,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这个字段要特别留意,很多检测训练框架默认把difficult=1的目标过滤掉,如果不希望过滤,需要在转换时做特殊处理或保留标记。实际项目里,疲劳驾驶数据集中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命令要求两个输入都排好序,所以两边都加了sort,sed负责把文件名的后缀去掉,保证两边拿出去比较的是纯文件名。

如果第三条命令有输出,说明存在孤儿图片。常见原因有两个:分发时漏打包了一部分标注,或者文件名大小写不一致导致对应关系断裂。处理办法很简单,把孤儿图片挪出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(parents=True, exist_ok=True) with open(out_path, 'w') as f: f.write('\n'.join(lines)) return True

这个函数里最核心的是四行归一化计算。x_center等于(xmin+xmax)/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(parents=True, exist_ok=True) lbl_out.mkdir(parents=True, exist_ok=True) 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输入分辨率,显存足够可试960
batch_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张的小数据集上很稳妥。希望这些经验能帮到你。

本文还有配套的精品资源,点击获取

返回列表