疲劳驾驶数据集图片与XML标注不匹配?清洗与YOLO转换指南
简介这是一份面向深度学习和图像识别入门练习的疲劳驾驶监测数据集包含人体疲劳状态下的真实照片可重点将打哈欠时的张嘴状态与闭眼状态作为疲劳判定标准适合有一定Python基础、正在学习目标检测或图像分类的初学者也适用于课程设计与毕业设计中的小型项目。资源包大小约256.28MB解压后主要为2915张jpg图片与2914个xml标注文件图片存放于JPEGImages目录标注文件存放于Annotations目录可直接接入常见目标检测框架的数据组织方式。已有4742人学习。值得注意的是原始数据存在图片与标注不匹配的问题作者提供了Python清洗脚本思路通过比对JPEGImages和Annotations目录自动删除无对应xml的图片可帮助使用者快速获得干净的数据集。拿到资源后可直接进行数据预处理、划分训练集并开展模型训练减少在数据整理上耗费的精力专注算法效果调优。1. 疲劳驾驶数据集图片和标注对不上先别急着把模型跑起来下载好“疲劳驾驶监测数据集”之后多数人的第一反应是解压、看两眼图片然后直接扔进深度学习训练脚本里跑。结果通常会卡在一个很不起眼的地方JPEGImages 目录下的某张照片在 Annotations 目录里找不到同名的 xml。轻则训练时数据加载报错重则模型练完发现验证精度很低完全不知道问题出在哪里。这个现象在公开的疲劳驾驶数据集里不算罕见图片和标注不匹配是新手最容易踩的坑。下面这条排查路径从“为什么会不匹配”讲起覆盖清理数据、解析 xml 到验证数据的完整流程适合刚入门人工智能图像识别、准备拿疲劳驾驶数据练手的新手——也适合被数据集折腾得怀疑人生的熟手。看清根因之后再动手比直接开训省下几倍时间。2. 图片与 XML 对不上目录结构、命名规则与差集定位2.1 数据集目录结构与命名规律这份疲劳驾驶监测数据集是典型的 VOC 风格目录布局。图片统一放在 JPEGImages 文件夹里标注文件统一放在 Annotations 文件夹里两者一一对应图片叫什么名标注文件就叫什么名只是后缀不同。标注文件是 xml里面记录的是这张图片的宽度、高度、通道数、目标类别和检测框坐标。因为定位是新手练习项目整体目录层级不复杂解压后核心就这两个文件夹。实际解压后的布局大致是下面这样Dataset/dataset/ ├── JPEGImages │ ├── 894.jpg │ ├── 1429.jpg │ ├── 1062.jpg │ └── ... └── Annotations ├── 894.xml ├── 1429.xml ├── 1062.xml └── ...命名规律看起来很简单但恰恰是命名在坑人。我拿这份数据做清洗时观察到图片编号从八百多号一路排到两千六百多号也就是八百多号到两千五百多号之间中间有不少断号位置——这些断号附近往往就是图片与标注失配的重灾区。如果你的训练代码只是把 JPEGImages 整个文件夹喂进去模型并不知道哪张图没有标签。遇到缺标注时数据加载器要么直接抛异常中断要么更隐蔽地把下一张 xml 错配给当前图片。这种错位学习比直接报错更危险Loss 看起来也会下降训练过程完全不报错但模型学到的是“人脸位置和标签错开”的假特征验证集上一测 mAP 就到不了合理值。所以第一步永远是先看结构而不是先跑模型。把两个目录的文件数打出来对比是成本最低、但最能暴露问题的健康检查。2.2 为什么会产生“有图无标注”和“有标无图”同一个数据集里出现图片和标注对不上通常来自三个来源。第一种是标注软件漏标。采集到的疲劳驾驶照片会先做人工筛选和标注标注过程中难免有漏掉的图片照片还在目录里对应的 xml 没有被生成。第二种是数据合并时没有对齐。比如有人分两批收集图片一批带了标注另一批只有裸图合并时只把图片拷贝进 JPEGImagesAnnotations 目录没有同步更新两边数量就对不上。第三种是文件名规则不统一。某些 xml 里记录的 filename 字段和实际文件名不一致或者图片从 png 格式转成 jpg 后后缀变了而 xml 里还写着旧文件名。这三类问题有一个共同特征它们解压时不会给出任何错误提示只会在训练中途或者最终评估时以“精度低、Loss 奇怪、验证集指标为 0”的形式暴露。也就是说等报错再处理就晚了必须主动做一次配对检查。配对检查的思路就是把 JPEGImages 当作“事实图片集合”把 Annotations 当作“有效标签集合”两边不重合的部分就是需要处理的对象。2.3 用脚本找出不匹配项先统计再动手配对检查的常见做法是把 JPEGImages 下所有文件名抽出来把 Annotations 下所有文件名抽出来分别去掉后缀然后做集合差。我一般先跑一段统计脚本把两边数量和不匹配数量一次性打出来import os jpeg_dir Dataset/dataset/JPEGImages anno_dir Dataset/dataset/Annotations jpeg_names {os.path.splitext(f)[0] for f in os.listdir(jpeg_dir)} anno_names {os.path.splitext(f)[0] for f in os.listdir(anno_dir)} print(JPEG 数量:, len(jpeg_names)) print(XML 数量:, len(anno_names)) print(有图无标注:, len(jpeg_names - anno_names)) print(有标注无图:, len(anno_names - jpeg_names))os.path.splitext会把文件名和后缀拆开比如894.jpg变成894和.jpg这样图片和 xml 就能用同一套主名比较。差集用集合减法直接算出来jpeg_names - anno_names是“有图片但没标注”的文件属于清理对象反方向的差集是“有标注但缺图片”的文件这种多半是图片漏拷贝了要优先去补图片而不是顺手删 xml。这段脚本的统计结果不要只看数字还要把差集的具体文件名打印出来。如果“有图无标注”占比到了百分之十几说明这批数据的标注工作没有完成需要重新评估能不能用只有零星几个文件对不上才适合直接走清理流程。我还会注意一个细节os.listdir返回的是目录下所有文件名如果系统生成了隐藏文件比如 mac 下的.DS_Store会混进集合干扰统计必要时用endswith(.jpg)之类的条件过滤。3. 清理不匹配图片删除脚本、路径修改与误删后悔药3.1 直接删除不匹配图片的脚本明确差集之后下一步就是把“有图无标注”的图片从 JPEGImages 里清掉。原数据集自带的清理思路是遍历图片目录逐个检查同名 xml 是否存在不存在就删除图片。脚本本身不长我把它完整贴在这里import os, shutil jpeg Dataset/dataset/JPEGImages # 图片目录按实际路径修改 jpeg_list os.listdir(jpeg) anno Dataset/dataset/Annotations # 标注目录按实际路径修改 anno_list os.listdir(anno) for pic in jpeg_list: name pic.split(.)[0] # 去掉图片扩展名得到主名 anno_name name .xml # 拼出期望的标注文件名 print(anno_name) if anno_name not in anno_list: # 如果标注不存在 os.remove(os.path.join(jpeg, pic)) # 删除这张图片这个脚本按顺序做了四件事第一列出 JPEGImages 下所有文件名第二列出 Annotations 下所有文件名第三对每张图片提取主名拼出同名 xml 去anno_list里查第四查不到就调用os.remove删除图片。os.path.join(jpeg, pic)的作用是把目录和文件名拼成完整路径避免手写路径时漏掉分隔符。原脚本里name pic.split(.)[0]有两个隐患需要注意。第一个隐患是多后缀文件名如果图片叫driver.01.jpgsplit(.)[0]得到的是driver而不是driver.01拼出来的driver.xml自然匹配不上最后会把有标注的图片误删。更稳妥的写法是os.path.splitext(pic)[0]它只剥离最后一个扩展名。第二个隐患是anno_list里可能混入非 xml 文件比如隐藏的Thumbs.db判断时最好加上anno_name.endswith(.xml)条件。3.2 路径改法和“安全删除”模式脚本里的jpeg和anno两个变量必须改成你解压后的实际路径。用相对路径时要注意当前工作目录如果脚本在 notebook 里跑而 notebook 所在的目录和Dataset不在同一层Dataset/dataset/JPEGImages这个相对路径就会指向不存在的位置os.listdir直接报 FileNotFoundError。这种“路径少了一个层级”的问题是新手最容易翻车的地方。建议先跑一小段print(os.path.exists(jpeg))确认路径存在再执行删除。我更推荐的做法是不要一上来就物理删除而是先把删除动作改成移动给误删留后悔药os.makedirs(unmatched, exist_okTrue) shutil.move(os.path.join(jpeg, pic), unmatched/ pic)把文件移动到unmatched/目录而不是删除跑完检查无误后再手动清空这个目录。另一种更轻量的做法是把os.remove(...)临时换成print(will delete:, pic)先做一次 dry run确认要删的文件名单没有误伤再把 print 改回删除。这一步花不了两分钟但能避免因为路径写错、命名规则判断错把几百张有效图片一次性删掉。3.3 清理方向为什么删图而不是删 XML清理时方向很有讲究。图片和标注对不上标准做法是删除“有图无标注”的图片保留所有 xml。原因是标注是人工成本最高的部分一张图从截取、筛选到框选目标可能花了几分钟甚至更久的人工投入丢了 xml 等于把标签浪费了而没有任何标注的图片在目标检测训练里既不能提供正样本也不能提供正确的负样本留着只会让数据加载器报错。反过来如果出现“有标注无图”要先去检查图片是不是漏拷贝了而不是删掉 xml。漏拷贝补上即可删了 xml 就把有效标注废了。这个原则同样适用于后续的类别筛选。如果某些 xml 里只有你不关心的类别比如误标了方向盘、后视镜这类目标正确做法是保留 xml在转换标签时按类别名过滤而不是把 xml 直接删掉。这样以后想扩展类别原始标注还在。3.4 清理后的目录核对清理结束后不要急着训练。回到 2.3 的统计脚本再跑一遍确认“有图无标注”和“有标注无图”都变成 0两边文件数量相等。我习惯把清理前后数量记录成一行文字存在项目笔记里方便后续回溯“为什么数据集是这个规模”。数量对上了接下来才进入标注解析和训练流程。如果数量仍然对不上说明清理脚本的匹配逻辑还有问题回到 3.1 检查文件名后缀处理方式。4. 解析 XML 与构造训练标签把打哈欠张嘴和闭眼状态喂进模型4.1 XML 标注文件里有什么清理干净后的 xml 文件才是真正要用的标注。打开任意一个文件内容结构大致如下annotation folderJPEGImages/folder filename894.jpg/filename size width1280/width height720/height depth3/depth /size object nameyawn/name bndbox xmin512/xmin ymin301/ymin xmax701/xmax ymax402/ymax /bndbox /object /annotation训练时真正要读的就三个部分size里的宽高object里的name以及bndbox里的四个坐标。name表示这个目标属于哪个类别bndbox表示目标在图片中的边界框位置。一张图上如果出现多个对象xml 里就会有多个object块解析时必须循环读取不能只取第一个。这里特别容易出错的是filename字段。有些 xml 里的filename写了旧名字和 JPEGImages 里实际文件名不同这类不一致不会影响清理脚本的配对判断但会在后面对应图片时产生错位。所以解析 xml 时我习惯以实际图片文件名为准而不依赖 xml 里的filename字段去拼路径。4.2 VOC 转 YOLO 完整脚本与归一化细节现在很多目标检测训练框架用的是 YOLO 格式标注是 txt 文件每行表示一个目标包含类别索引、中心点 x、中心点 y、目标宽度、目标高度并且所有数值都归一化到 0 到 1 之间。把 VOC 格式转成 YOLO 格式的完整脚本如下import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, out_dir, class_names): tree ET.parse(xml_path) root tree.getroot() w int(root.find(size/width).text) h int(root.find(size/height).text) filename root.find(filename).text name_no_ext filename.rsplit(., 1)[0] lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_names: continue cls_id class_names.index(name) box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) x_center (xmin xmax) / 2.0 / w y_center (ymin ymax) / 2.0 / h box_w (xmax - xmin) / w box_h (ymax - ymin) / h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) with open(os.path.join(out_dir, name_no_ext .txt), w) as f: f.write(\n.join(lines)) class_names [yawn, closed_eye] voc_to_yolo(Dataset/dataset/Annotations/894.xml, labels, class_names)这段代码的关键点在于归一化时的分母。w和h必须来自 xml 里的size字段而不是用cv2.imread现读图片得到的尺寸。正常情况两者相等但一旦原图被压缩过或经过批量缩放xml 里的 size 还是原始分辨率用图片实际尺寸去归一化边界框就会整体偏移训练也看不出来推理时框全打在错误位置。第二个容易错的地方是除法要用浮点数(xmin xmax) / 2.0 / w才能保留小数如果写成/ 2在 Python 2 环境里整数除法会直接截断。脚本把每个 xml 转换并写出一个同名 txt 文件。class_names是类别列表脚本按类别名找到索引不在列表里的类别直接跳过。这一步相当于把“多标签的可能情况”在进入训练前就过滤干净。4.3 类别策略把打哈欠张嘴和闭眼作为疲劳标准这份数据集的标注设计思路很直接训练时把“打哈欠张嘴”和“闭眼”两种状态作为疲劳判别标准。从目标检测的角度看就是两个类别yawn表示张嘴打哈欠的状态closed_eye表示闭眼状态。嘴和眼睛区域都可以用矩形框标出来模型学的是“这块区域当前处于什么状态”。新手练习时我不建议一开始就同时训两个类别。先设置class_names [yawn]只跑张嘴检测让数据加载、Loss 计算、验证评估整条链路先跑通再扩展成[yawn, closed_eye]。两个类别一起训也不是不行但一旦 Loss 不收敛你要同时排查两个类别的标注质量定位成本翻倍。先单类别后双类别是一个性价比很高的推进方式。等模型能稳定输出一个状态再叠第二个状态之后调置信度阈值才有意义。5. 避坑与排查用这份数据集训练前最容易翻车的四个点5.1 清理脚本误删了有标注的图片现象跑完清理脚本后JPEGImages 数量少了很多对照 Annotations 查看发现某些图片明明有对应 xml却也一起消失了。原因脚本的匹配方向或者匹配逻辑写错了。常见的有两种一种是把 XML 列表当遍历主体在图片列表里找不到同名文件就删除 xml方向反了另一种是图片文件名里有多后缀比如driver.01.jpg用split(.)[0]取主名导致匹配失败把好图误删。解决执行删除前先用统计脚本打印差集确认清理对象就是“有图无标注”的图片删除动作改成printdry run 模式核对文件名单更稳的做法是改用shutil.move把图片移到unmatched/目录确认无误后再清空。从那以后我每次清理数据集都会强制走一遍“先统计、再 dry run、最后删除”的顺序。5.2 XML 解析报错ParseError、空文件与编码问题现象批量转换标注时ET.parse(xml_path)抛出xml.etree.ElementTree.ParseError程序中断。排查看是某个 xml 文件是 0 字节或者标签少了一个闭合。原因原数据集的标注文件有一部分没有正常收尾就导出了也可能在拷贝过程中损坏。这类文件数量往往不多但会卡住整个批量转换流程。编码问题也可能触发解析异常比如 xml 头声明的是 UTF-8实际内容里混入了 GBK 编码的中文字符。解决转换脚本里加异常捕获把坏文件单独隔离出来try: tree ET.parse(xml_path) except ET.ParseError as e: print(bad xml:, xml_path, e) continue遇到ParseError的文件移到broken/目录逐个决定修复还是放弃。0 字节说明这张图没有可用标注按第 3 章的清理逻辑删掉对应图片少闭合标签可以手工补上编码问题则用errorsignore读取或统一转换成 UTF-8 后重新解析。批量转换前先全量扫一遍 xml 能不能被正常解析能提前暴露绝大多数坏文件。5.3 类别标签不统一yawn、Yawn、打哈欠同时出现现象训练时发现类别数不对预期两个类别跑出来却有三四个。打印标签统计结果后发现name字段有的写成yawn有的写成Yawn还有的写了中文“打哈欠”。原因标注过程是人工完成的不同标注者对同一类目标的命名习惯不一样有的用小写英文有的用首字母大写有的直接写中文。VOC 转 YOLO 时class_names.index(name)找不到这些变体要么跳过要么当成额外类别处理。解决转换前先全量扫描所有 xml对name字段做去重统计。我一般这样做import os import xml.etree.ElementTree as ET anno_dir Dataset/dataset/Annotations names set() for f in os.listdir(anno_dir): if not f.endswith(.xml): continue tree ET.parse(os.path.join(anno_dir, f)) for obj in tree.getroot().findall(object): names.add(obj.find(name).text) print(names)如果names集合里出现多个写法就维护一个映射字典把所有变体统一成标准类别名后再转换。统一标签的另一个好处是类别索引不会乱YOLO 训练时类别 ID 一旦分配错误所有标注都会错位训练后评估指标完全不可信。5.4 训练 Loss 不收敛归一化坐标和尺寸对不上现象模型训练几十轮Loss 不降反升或者验证集 mAP 一直是 0。可视化检查发现预测框位置整体偏到图的左上角或右下角。原因VOC 转 YOLO 时归一化坐标的分母用错了尺寸。有人用cv2.imread读图片后得到的 shape 作为w和h但 xml 里记录的是原始分辨率更常见的是x_center (xmin xmax) / 2 / w里漏了/ 2.0整数除法把中心点坐标算成了整数框就整体偏移。解决回到 4.2 的转换脚本确认w和h从size字段读取所有除法用浮点数。转换完一个 xml 后挑一张图片手工核对打开 txt 里的一行数值代回x_center * w验证还原出的中心点是不是和 xml 里的 bndbox 中心一致。训练前再做一次可视化验证把标注框画在图上框的位置不对就不要开训。这个检查只要做一次就能把 90% 以上的坐标转换问题拦在训练前面。6. 快速定位三步验证这份疲劳驾驶数据集是否干净6.1 统计文件配对率任何数据集到手第一件事是跑数量对比。JPEGImages 和 Annotations 数量必须一致不一致就找差集。可以一步到位find Dataset/dataset/JPEGImages -name *.jpg | wc -l find Dataset/dataset/Annotations -name *.xml | wc -l这两条命令只回答“总数对不对”真正要核对的是同名配对率需要按第 2.3 节的方式把 jpg 和 xml 文件名分别提取出来做差集。只要差集不为空就先跑清理清理完再统计直到两边数量相等、差集为 0。6.2 可视化检查标注框是否贴合目标数量对上不等于标注正确。我每次都会随机抽十张图把真实边界框画到图片上看一眼。方法是读 xml 里的坐标用 OpenCV 画矩形import cv2 import xml.etree.ElementTree as ET img cv2.imread(Dataset/dataset/JPEGImages/894.jpg) tree ET.parse(Dataset/dataset/Annotations/894.xml) root tree.getroot() for obj in root.findall(object): box obj.find(bndbox) xmin int(box.find(xmin).text) ymin int(box.find(ymin).text) xmax int(box.find(xmax).text) ymax int(box.find(ymax).text) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 255, 0), 2) cv2.putText(img, obj.find(name).text, (xmin, ymin - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite(check.jpg, img)框应该正好圈住眼睛或嘴部区域name文字和状态一致。如果框整体偏离目标说明坐标转换有问题而不是数据本身坏了如果图里明明有目标但一个框都没有说明这张图的标注没有被正确配对返回第 5.2 节检查解析逻辑。6.3 小样本冒烟训练数据验证完最后一步是冒烟测试。每次我都会从清理后的数据里随机抽 50 张图把训练迭代数设置得极小比如 10 个 epoch先跑通数据加载、标签读取、Loss 计算整条链路。这几步正常后再上全量数据。这样做的成本很低但能把 80% 的数据集问题拦在正式训练之前——包括类别索引错乱、标注文件路径读不到、图片解码异常这些隐藏问题全都会在小样本训练时暴露。从那以后我每次拿到新数据集都会强制走一遍这三个检查先对文件名再画框看标注最后小样本跑链路。磨合多了你会发现很多训练翻车都不是模型结构的问题而是数据集在最初就不干净。这份疲劳驾驶监测数据集本身质量不错把不匹配的图片清掉打哈欠张嘴和闭眼两个状态作为训练标签完全够新手练手源资源里也直接提供了可复现的清理脚本照着跑就能得到一份干净的训练集。希望帮到你。本文还有配套的精品资源点击获取