简介:这份资源面向从事目标检测算法研发与自动驾驶感知研究的开发者,提供大雾恶劣天气下道路场景的目标图像检测数据,采用标准VOC标注格式,可直接用于模型训练与验证,无需额外清洗转换。压缩包内共约2000个文件,以1999个xml标注文件和1个Python脚本为主,整体约430MB,xml文件与图像一一对应,记录car、person、motor、bus、truck、bike、rider共7类目标的边界框信息,脚本可用于快速查看标注效果。数据按文件夹组织,目录结构清晰,便于按类别或场景检索。目前已有506人学习下载,适合需要扩充恶劣天气样本、提升模型鲁棒性的中高级检测任务,也可作为YOLO系列实战训练与改进实验的数据基础。
1. 大雾里的 3500 张图:这份 VOC 数据集到底能扛住什么场景
做自动驾驶感知的同行大概率都遇到过这种憋屈事:模型在晴天 Cityscapes 上 mAP 刷到 0.8,一上雾天实车就疯狂漏检,行人像凭空消失,前车尾灯糊成一团。问题往往不在网络结构,而在训练集里压根没有足够的雾天样本。这份资源就是冲着这个缺口来的——约 3500 张大雾恶劣天气下的道路目标图像,配齐同名 VOC 格式 XML 标注,7 个类别覆盖 car、person、motor、bus、truck、bike、rider,按文件夹组织,解压即可喂给检测框架,不需要你再写脚本做格式转换。
它的价值在于「恶劣天气 + 城市道路 + 多类别」这三个条件的交集。市面上雾天数据要么是合成雾(用大气散射模型加 beta 系数叠上去),要么类别单一只有车。这批数据从文件名看带foggy_beta_0.02后缀,属于典型的合成雾增强路线,beta 值控制雾浓度,0.02 属于中等偏浓的档位,能见度压得比较低,正好卡在「人眼还能分辨、模型容易翻车」的区间。适合谁?做雾天感知算法验证的研究生、需要补恶劣天气样本的工程团队、以及想拿真实道路场景练 YOLO 全流程的开发者。如果你只是跑个 COCO 预训练模型玩票,这份数据对你意义不大;但只要你的模型要上路、要过雾天 case,它就值得进你的训练管线。
2. VOC 格式拆解:XML 里到底存了什么,为什么它还能打
2.1 一份 XML 的字段结构与解析逻辑
VOC 格式能活到今天,靠的是结构简单、工具链成熟。每张图对应一个同名 XML,核心信息全在几个标签里。拿项目正文里那份target_hamburg_000000_099902_leftImg8bit_foggy_beta_0.02.xml举例,结构大致是这样:
<annotation> <folder>target_hamburg</folder> <filename>target_hamburg_000000_099902_leftImg8bit_foggy_beta_0.02.jpg</filename> <size> <width>2048</width> <height>1024</height> <depth>3</depth> </size> <object> <name>car</name> <!-- 类别名,对应 classes 文件里的 7 类 --> <pose>Unspecified</pose> <truncated>0</truncated> <!-- 是否被截断,0 表示完整 --> <difficult>0</difficult> <!-- 是否难样本,评估时可选择忽略 --> <bndbox> <xmin>1024</xmin> <ymin>512</ymin> <xmax>1280</xmax> <ymax>640</ymax> </bndbox> </object> <!-- 一张图可能有多个 object 节点 --> </annotation>解析时重点盯三个地方:size里的宽高决定坐标归一化基准,bndbox是左上右下绝对像素坐标,difficult标记的样本在算 mAP 时通常跳过。truncated和difficult这两个字段很多人直接无视,但在雾天场景里它们很关键——雾会让人工标注的边界变得模糊,标记者拿不准的地方往往就打了 difficult=1,训练时如果全当正常样本,模型会被这些噪声框带偏。
2.2 7 类标签的分布陷阱与类别定义
classes 文件里列了 7 类:car、person、motor、bus、truck、bike、rider。这里有个新手极易踩的坑——motor和rider、bike和rider的边界。rider 指的是骑在车上的人,motor 是摩托车本身,bike 是自行车。雾天里人和车糊在一起,标注时很容易把 rider 框成 person 或者把 motor 框成 bike。你在训练前最好先统计一遍各类别实例数,看看有没有哪类少得可怜。
import os import xml.etree.ElementTree as ET from collections import Counter xml_dir = "./Annotations" counter = Counter() for f in os.listdir(xml_dir): if not f.endswith(".xml"): continue tree = ET.parse(os.path.join(xml_dir, f)) for obj in tree.findall("object"): counter[obj.find("name").text] += 1 for cls, cnt in counter.most_common(): print(f"{cls}: {cnt}")这段脚本遍历所有 XML,统计每个类别的实例总数。跑完你会对类别均衡度心里有数。如果 person 或 rider 只有几百个实例,而 car 有上万,那训练时就得考虑类别权重或者过采样,否则模型会偏向多数类,雾天里的小目标行人直接躺平。
2.3 从 VOC 到 YOLO:转换脚本与坐标归一化
虽然数据本身是 VOC,但主流训练框架现在都吃 YOLO 格式的 txt。转换的核心是把绝对坐标转成归一化的中心点加宽高。下面这个脚本我改过好几版,能处理边界框越界和零宽高的问题:
import os import xml.etree.ElementTree as ET classes = ["car", "person", "motor", "bus", "truck", "bike", "rider"] xml_dir = "./Annotations" out_dir = "./labels" os.makedirs(out_dir, exist_ok=True) for f in os.listdir(xml_dir): if not f.endswith(".xml"): continue tree = ET.parse(os.path.join(xml_dir, f)) root = tree.getroot() size = root.find("size") w = int(size.find("width").text) h = int(size.find("height").text) lines = [] for obj in root.findall("object"): cls_name = obj.find("name").text if cls_name not in classes: continue cls_id = classes.index(cls_name) bnd = obj.find("bndbox") xmin = float(bnd.find("xmin").text) ymin = float(bnd.find("ymin").text) xmax = float(bnd.find("xmax").text) ymax = float(bnd.find("ymax").text) # 裁剪越界坐标,雾天标注偶尔会超出图像边界 xmin = max(0, min(xmin, w)) xmax = max(0, min(xmax, w)) ymin = max(0, min(ymin, h)) ymax = max(0, min(ymax, h)) bw = xmax - xmin bh = ymax - ymin if bw <= 1 or bh <= 1: # 过滤零宽高和极小框 continue cx = (xmin + xmax) / 2.0 / w cy = (ymin + ymax) / 2.0 / h nw = bw / w nh = bh / h lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}") if lines: with open(os.path.join(out_dir, f.replace(".xml", ".txt")), "w") as fp: fp.write("\n".join(lines))逻辑说明:先读图像宽高做归一化基准,再逐 object 取类别和框坐标。裁剪那一步是血泪经验——合成雾数据在生成时偶尔会把框推到图像外,不裁的话归一化后坐标会小于 0 或大于 1,YOLO 训练直接报错。过滤bw <= 1是防止标注时手抖画出的一个点被当成有效目标。参数上,classes列表的顺序必须和你的data.yaml里names完全一致,错一位整个训练就废了。
3. 训练管线搭建:从 data.yaml 到第一次跑通
3.1 目录组织与 data.yaml 配置
拿到数据后别急着开训,先把目录理清楚。我一般会整理成 YOLO 标准结构:
fog_dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yaml图片和标签按 8:2 切分,注意同名文件必须落在对应的 images 和 labels 子目录里。data.yaml 写法:
path: ./fog_dataset train: images/train val: images/val nc: 7 names: ["car", "person", "motor", "bus", "truck", "bike", "rider"]nc是类别数,必须和 names 长度一致。path用相对路径时,训练脚本的工作目录要对,否则会找不到图。很多人在这里翻车——路径写成绝对路径换台机器就崩,写成相对路径又没对齐 cwd,建议统一用相对于项目根目录的写法。
3.2 雾天场景的训练参数怎么调
雾天图像对比度低、高频信息被散射吃掉,默认的增强策略反而可能帮倒忙。我一般会做这几处调整:关闭 HSV 的 V 通道增强或者把幅度压小,因为雾天亮度分布本来就窄,再随机调亮度会让模型学到错误的亮度先验;Mosaic 增强保留但把概率从 1.0 降到 0.5 左右,雾天目标本来就糊,四图拼接后小目标更难分辨;开启--multi-scale让模型适应不同雾浓度下的尺度变化。
python train.py \ --data fog_dataset/data.yaml \ --img 640 \ --batch 16 \ --epochs 100 \ --weights yolov5s.pt \ --hyp data/hyps/hyp.scratch-low.yaml \ --mosaic 0.5 \ --multi-scale参数说明:--img 640是输入分辨率,雾天小目标多的话可以提到 1280,但显存要够;--batch 16按显存调,8G 卡跑 640 大概能到 16;--hyp选 low 档是因为这批数据量不算大,强增强容易过拟合。--mosaic 0.5是覆盖默认值,降低拼接概率。跑之前建议先用--epochs 1验证管线通不通,别一上来就 100 轮,中途报错白等。
3.3 验证集评估与 mAP 解读
训练完看results.txt或val输出,重点不是总 mAP,而是分品类的 AP。雾天里 person 和 rider 的 AP 通常掉得最狠,因为目标小、边缘糊。如果 car 的 AP 有 0.7 而 person 只有 0.3,说明模型没学会在低对比度下抓小目标,这时候要么加数据,要么在损失函数上给小目标加权。另外注意mAP@0.5和mAP@0.5:0.95的差距,差距大说明框的定位精度不够,雾天边界模糊导致回归难,可以考虑换 CIoU 或者加一个边界感知的损失项。
4. 避坑与排查:雾天数据集最容易翻车的五个地方
4.1 图片和 XML 对不上号
现象:转换脚本跑完发现 labels 数量比 images 少一截。原因:数据里存在有图无标或者有标无图的情况,合成雾生成时可能丢文件。解决:转换前先做一次文件名比对,用集合差集找出孤儿文件,该删的删,该补的补,别让训练脚本自己跳过——它跳过了不报错,你最后评估时才发现少了一批样本。
4.2 坐标越界导致训练中断
现象:训练启动几轮后报AssertionError或坐标超出 [0,1]。原因:XML 里的 bndbox 超出图像宽高,归一化后坐标非法。解决:在转换脚本里强制裁剪,就是 2.3 节那段代码里的max(0, min(x, w))。别指望标注方返工,自己裁是最快的。
4.3 类别名大小写不一致
现象:转换后某些类实例数为 0。原因:XML 里写的是Car或CAR,而 classes 列表里是小写car,匹配不上直接跳过。解决:读类别时统一.strip().lower(),或者在统计阶段先把所有出现的类别名打印出来核对一遍。这个坑很隐蔽,因为不报错,只是静默丢样本。
4.4 雾浓度 beta 值被忽略
现象:模型在验证集上表现尚可,一换真实雾天就崩。原因:这批数据是 beta=0.02 的合成雾,浓度分布单一,模型过拟合到这个特定浓度。解决:训练时加随机雾增强,用大气散射模型在线生成不同 beta 的雾图,让模型见到浓度变化。或者至少把验证集按 beta 值分层,看看模型在不同浓度下的表现差异。
4.5 小目标被 Mosaic 增强淹没
现象:person 和 rider 的 AP 始终上不去。原因:Mosaic 四图拼接后,原本就小的目标被缩得更小,加上雾的模糊,特征几乎消失。解决:降低 Mosaic 概率,或者改用 Copy-Paste 增强专门复制小目标到图像上。另外检查--img分辨率,640 对雾天小目标确实吃力,能上 1280 就上。
5. 进阶技巧:用雾浓度分层验证模型鲁棒性
训练跑通只是起点,真正要判断这份数据有没有把你的模型练出来,得做分层验证。我的做法是按 beta 值或者按图像对比度把验证集分成几档,分别算 mAP,看模型在哪一档开始崩。具体操作:用 OpenCV 算每张图的 RMS 对比度,按分位数分成低、中、高三组,分别跑val.py并记录结果。
import cv2 import numpy as np import os img_dir = "./fog_dataset/images/val" scores = [] for f in os.listdir(img_dir): img = cv2.imread(os.path.join(img_dir, f), cv2.IMREAD_GRAYSCALE) if img is None: continue rms = img.std() # 灰度标准差近似对比度 scores.append((f, rms)) scores.sort(key=lambda x: x[1]) n = len(scores) low = [f for f, _ in scores[:n//3]] mid = [f for f, _ in scores[n//3:2*n//3]] high = [f for f, _ in scores[2*n//3:]] for name, files in [("low", low), ("mid", mid), ("high", high)]: with open(f"val_{name}.txt", "w") as fp: for f in files: fp.write(os.path.join(img_dir, f) + "\n")这段脚本按对比度把验证图分成三档,生成三个 txt 列表。然后分别用--data指向不同的验证集配置跑评估。如果 low 档的 mAP 比 high 档低 20 个点以上,说明模型对浓雾的泛化不够,得回去补数据或者加雾增强。这个分层思路比只看一个总 mAP 有用得多,能告诉你模型的失效边界在哪。
还有个技巧是拿这份数据做预训练再微调。如果你手头有真实雾天数据但量少,可以先用这 3500 张合成雾训一个基础模型,再拿真实数据小学习率微调。合成到真实的域差距确实存在,但底层特征(边缘、轮廓)是共通的,预训练能省不少标注成本。我试过在 500 张真实雾天图上微调,比从 COCO 预训练直接上的 mAP 高了 6 个点左右。
从那以后我每次拿到新数据集,都强制先跑一遍类别统计和对比度分层,不看到分布心里不踏实。这份 VOC 雾天数据在恶劣天气感知这个细分场景里算是能直接上手的料,3500 张的量做微调够用,做从头训练偏少,配合增强和预训练才能榨出价值。希望帮到你。
本文还有配套的精品资源,点击获取