
简介这份疲劳驾驶监测数据集面向深度学习与图像识别方向的初学者及课程实践者用于搭建以打哈欠张嘴、闭眼状态为疲劳判定的入门级训练项目。压缩包共约2000个文件以2915张jpg图像和2914个xml标注文件为主图像用于模型输入xml提供目标框与类别信息整体约256.28MB可直接用于目标检测或分类任务的练习。需要留意的是原数据集中存在图片与标注不匹配的情况资源描述中附有一段Python脚本借助os与shutil遍历JPEGImages与Annotations目录按文件名比对后删除缺失对应xml的图片帮助使用者先完成数据清洗再投入训练。目前已有4742人学习下载适合希望快速上手疲劳状态识别、熟悉数据整理与标注校验流程的读者参考实践。1. 训练集里混进“无标注图片”为什么你的疲劳检测模型 mAP 死活上不去跑疲劳驾驶检测的新手项目最容易翻车的不是模型结构而是数据本身。你兴冲冲把Dataset/dataset/JPEGImages里的图片和Annotations里的 XML 一股脑喂给 YOLO 或 Faster R-CNN训练日志看着 loss 在降结果验证时 mAP 卡在 0.3 上不去可视化一看框全歪了——这种“玄学”十有八九是图片和标注对不上号。这个数据集里 1429.xml、894.xml、1062.xml 这些标注文件对应的图片和 JPEGImages 目录下的实际图片并不是一一对应的有相当一部分图片根本没有同名 XML。对做图像识别和深度学习的人来说这种脏数据不清理后面调参调到天亮都是白费。下面按“现象→根因→解决”的顺序把这类故障拆开讲清楚新手能照着命令跑熟手能看到排查思路。2. 图片与标注数量对不上从报错到定位的完整链路2.1 先看现象训练不报错但结果就是不对最迷惑人的地方在于图片和标注不匹配时大部分框架不会直接抛异常。你os.listdir两个目录打印长度发现 JPEGImages 有 2000 多张图Annotations 只有 1400 多个 XML差了 600 左右。但训练脚本照样能跑因为 DataLoader 只读它找到的标注找不到标注的图片要么被静默跳过要么被当成负样本。于是你看到的现象是训练集 loss 正常下降验证集指标却很低推理时有些明明有人的图检测不出框。更坑的是如果你用的是某些自动划分训练/验证集的脚本它按图片列表切分切完之后验证集里混进一堆无标注图评估结果直接失真。我一般会先做一次“对账”把两个目录的文件名去掉扩展名取交集和差集差集就是问题所在。这一步不需要任何深度学习框架纯 Python 就能跑。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)} only_jpeg jpeg_names - anno_names # 有图无标注 only_anno anno_names - jpeg_names # 有标注无图 print(f图片总数: {len(jpeg_names)}) print(f标注总数: {len(anno_names)}) print(f有图无标注: {len(only_jpeg)}) print(f有标注无图: {len(only_anno)}) print(前10个无标注图片:, list(only_jpeg)[:10])这段代码用集合差集做对账os.path.splitext去掉.jpg和.xml后缀保证比较的是同一维度的文件名。only_jpeg就是需要清理的目标only_anno则说明标注文件多余通常可以忽略但如果框架要求严格一一对应也要一并处理。跑完你会看到类似“有图无标注: 600”的输出这就是 mAP 上不去的直接原因。2.2 根因分析为什么数据集会缺标注这类数据集在采集和标注阶段通常是两条流水线图片从视频抽帧或爬取标注由另一批人用 labelImg 之类的工具画框。中间任何一环出问题都会导致不匹配。常见原因有三种一是抽帧后部分图片被人工筛掉模糊、重复、无人脸但标注文件没同步删除二是标注人员中途换工具或换命名规则导致文件名主体不一致比如图片叫1429.jpg标注却存成1429_1.xml三是数据集打包时只拷贝了部分 XML漏拷了。这个数据集属于第一种情况居多——原数据集中存在图片数据与标注数据不匹配的问题需要把不匹配的这部分数据删除。注意不要试图给无标注图片“补标注”。疲劳驾驶检测的标注成本很高闭眼、打哈欠这些状态需要专业判断补标注的代价远大于直接删除。而且如果图片本身质量差模糊、遮挡严重补了也是噪声。所以正确做法是以标注文件为准删除没有对应 XML 的图片。2.3 解决步骤用脚本批量清理无标注图片摘要里给出的参考代码思路是对的但有几个细节容易踩坑。先看原始代码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))这段代码能跑但直接os.remove是危险操作——一旦路径写错或判断逻辑有 bug图片就没了没有后悔药。我一般会改成“先移动到备份目录确认无误再删”。另外pic.split(.)[0]遇到文件名里带多个点的情况会出错比如1429.backup.jpg更稳妥的是os.path.splitext。改进版如下import os import shutil jpeg_dir Dataset/dataset/JPEGImages anno_dir Dataset/dataset/Annotations backup_dir Dataset/dataset/JPEGImages_orphan # 备份目录 os.makedirs(backup_dir, exist_okTrue) anno_set set(os.listdir(anno_dir)) moved 0 for pic in os.listdir(jpeg_dir): name, ext os.path.splitext(pic) if ext.lower() not in (.jpg, .jpeg, .png): continue anno_name name .xml if anno_name not in anno_set: src os.path.join(jpeg_dir, pic) dst os.path.join(backup_dir, pic) shutil.move(src, dst) # 先移走不直接删 moved 1 print(f共移出 {moved} 张无标注图片到 {backup_dir})参数说明anno_set用集合而不是列表查找复杂度从 O(n) 降到 O(1)几千张图差别不大但几万张时很明显。ext.lower()兼容.JPG大写后缀。shutil.move跨磁盘也能用比os.rename稳。跑完之后JPEGImages 里的图片数应该和 Annotations 的 XML 数一致或接近因为可能还有“有标注无图”的情况那部分不影响训练。确认训练正常后再手动删除备份目录。提示清理前先对整个Dataset目录打个压缩包或者至少确认备份目录可写。数据清理是不可逆操作别问我怎么知道的。3. 清理后训练仍报错标注文件本身格式与内容的坑3.1 XML 解析失败ParseError: not well-formed图片和标注数量对齐了不代表标注内容没问题。跑训练时可能遇到xml.etree.ElementTree.ParseError: not well-formed (invalid token)或者 labelImg 打开某个 XML 直接闪退。这类报错通常是 XML 文件在生成时被截断、编码不对或者手动改过导致标签不闭合。疲劳驾驶数据集的标注里object节点包含name如 closed_eye、open_mouth、bndbox等任何一个标签缺失都会让解析器罢工。定位方法是批量校验所有 XML 的可解析性import os import xml.etree.ElementTree as ET anno_dir Dataset/dataset/Annotations bad_files [] for f in os.listdir(anno_dir): if not f.endswith(.xml): continue path os.path.join(anno_dir, f) try: ET.parse(path) except ET.ParseError as e: bad_files.append((f, str(e))) print(f损坏的 XML 数量: {len(bad_files)}) for name, err in bad_files[:20]: print(name, -, err)ET.parse会完整读取并解析 XML遇到格式错误直接抛异常。把bad_files里的文件挑出来用文本编辑器看是不是标签没闭合或者用xmllint --noout file.xml检查。如果只是个别文件损坏直接删除对应图片和 XML如果大面积损坏说明数据集打包时就有问题需要重新获取。3.2 标注内容为空或框越界还有一种更隐蔽的情况XML 能解析但object数量为 0或者bndbox的坐标超出了图片实际尺寸。前者会让图片变成“背景图”后者会让训练时框到图像外面计算 loss 时产生异常梯度。我一般会加一段校验统计每张图的 object 数和框坐标范围import os import xml.etree.ElementTree as ET from PIL import Image anno_dir Dataset/dataset/Annotations jpeg_dir Dataset/dataset/JPEGImages empty_objs [] out_of_bound [] for f in os.listdir(anno_dir): if not f.endswith(.xml): continue tree ET.parse(os.path.join(anno_dir, f)) root tree.getroot() objs root.findall(object) if len(objs) 0: empty_objs.append(f) continue # 读取对应图片尺寸 img_name os.path.splitext(f)[0] .jpg img_path os.path.join(jpeg_dir, img_name) if not os.path.exists(img_path): continue w, h Image.open(img_path).size for obj in objs: bnd obj.find(bndbox) xmin int(bnd.find(xmin).text) ymin int(bnd.find(ymin).text) xmax int(bnd.find(xmax).text) ymax int(bnd.find(ymax).text) if xmin 0 or ymin 0 or xmax w or ymax h: out_of_bound.append((f, xmin, ymin, xmax, ymax, w, h)) break print(空标注文件:, len(empty_objs)) print(框越界文件:, len(out_of_bound))empty_objs里的文件建议连同图片一起删掉因为无目标的图对检测训练没有正贡献。out_of_bound里的框需要修正或删除该 object如果越界严重比如 xmax 是图片宽度的两倍说明标注时用错了坐标系整份标注都不可信。3.3 类别名不统一closed_eye和close_eye混用疲劳驾驶检测通常把“闭眼”“打哈欠”作为疲劳标准但标注人员可能写成closed_eye、close_eye、eyes_closed等多种形式。训练时框架会按name文本聚类导致同一类被拆成多个类模型学不到东西。用一条命令统计所有类别名import os import xml.etree.ElementTree as ET from collections import Counter anno_dir Dataset/dataset/Annotations counter Counter() for f in os.listdir(anno_dir): if not f.endswith(.xml): continue root ET.parse(os.path.join(anno_dir, f)).getroot() for obj in root.findall(object): name obj.find(name).text.strip() counter[name] 1 for name, cnt in counter.most_common(): print(f{name}: {cnt})如果看到closed_eye和close_eye同时出现就需要统一替换。替换前先备份然后用脚本批量改 XML 文本或者用 labelImg 的“更改类别名”功能。统一后的类别建议控制在 2~3 个closed_eye、open_mouth必要时加normal。4. 避坑与常见问题数据清理阶段最容易翻车的 5 个操作4.1 直接os.remove没有备份现象脚本跑完发现删多了或者路径变量写错把整个 JPEGImages 清空。原因os.remove不可逆且没有二次确认。解决一律用shutil.move移到备份目录训练验证通过后再手动删。备份目录不要放在 JPEGImages 内部避免被后续os.listdir再次扫到。4.2 用split(.)[0]处理文件名现象文件名1429.v2.jpg被截成1429和1429.xml误匹配导致该删的没删。原因split(.)按所有点分割取第一段。解决用os.path.splitext只去掉最后一个扩展名得到1429.v2再拼.xml比较。4.3 忽略大小写和扩展名差异现象图片是.JPG大写标注是.xml小写脚本判断1429.JPG对应1429.xml时因为扩展名没统一而误判。原因os.listdir返回原始文件名大小写敏感。解决比较前统一lower()并且只处理图片扩展名白名单。4.4 清理后没有重新划分训练/验证集现象清理前按图片列表划分了 train/val清理后验证集里还有已删除图片的引用训练时报FileNotFoundError。原因划分文件是静态的数据变了没同步。解决清理完成后重新跑一遍划分脚本或者用符号链接方式让划分动态读取当前文件列表。4.5 把“有标注无图”的 XML 也删了现象发现only_anno有几百个一激动全删了结果其中一部分是图片扩展名不是.jpg而是.png被误判为“无图”。原因只按.jpg找对应图片。解决检查图片目录时把所有图片扩展名都纳入.jpg、.jpeg、.png、.bmp都要试一遍确认真的没有对应图片再删 XML。5. 快速定位数据问题的排查顺序与验证方法遇到疲劳驾驶检测训练效果差不要先调模型按下面这个顺序过一遍数据八成问题在前三步就能暴露。排查步骤命令/操作判断标准数量对账集合差集统计图片数 ≈ XML 数差值超过 5% 就要清理XML 可解析ET.parse批量校验损坏文件数为 0空标注/越界遍历 object 和 bndbox空标注文件删除越界框修正类别统一Counter统计 name类别名唯一无同义混用图片可读Image.open批量打开无UnidentifiedImageError这个顺序的逻辑是先保证“有图有标注”再保证“标注能用”最后保证“图片能读”。每一步都有对应的脚本跑一遍几分钟比盲目调参高效得多。验证清理是否彻底最直接的方法是重新跑一次训练看第一个 epoch 的 loss 是否比清理前更低、更平稳。如果清理前 loss 震荡严重清理后明显收敛说明无标注图片确实在干扰。另一个验证点是推理可视化随便挑 20 张验证集图片画上预测框看有没有“该有框的地方没框”。如果清理前有大量漏检清理后漏检减少说明数据对账起了作用。从那以后我每次拿到新数据集第一件事不是写模型而是先跑一遍数量对账和 XML 校验把数据问题挡在训练之前。这个习惯帮我省下了无数个调参的夜晚。希望这份排查思路能帮到你数据集里的清理脚本和校验代码可以直接拿去用照着跑一遍你的疲劳检测项目就能少走一段弯路。本文还有配套的精品资源点击获取