ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

雨雪天气路面检测数据集:VOC格式四类真实场景

雨雪天气路面检测数据集:VOC格式四类真实场景

简介:本资源是一套面向智能驾驶与计算机视觉方向研究者的雨雪天气路面状况识别专用数据集,适用于结冰、积雪、湿滑及干燥四类典型路面场景的模型训练与算法验证。数据集共1294个文件,包含646张原始高清JPG图像与对应VOC格式XML标注文件,另有2个说明类TXT文档,完整覆盖目标检测任务所需的图像-标注对,压缩包仅27.09MB,轻量易部署。目前已有824人学习下载,体现了其在边缘场景感知领域的实用价值。用户可直接用于YOLO、Faster R-CNN等主流检测框架的训练与测试,标注规范、类别明确、无冗余处理,且所有图片均保留原始采集质量,便于开展光照鲁棒性分析、小目标检测优化及恶劣天气泛化能力评估。

1. 雨雪天气路面状况数据集:结冰/雪地/湿滑/干燥四类真实场景,VOC格式开箱即用,专为车载视觉与ADAS算法验证而生

你有没有试过在凌晨三点的高速上,用YOLOv8训完一个“看起来很准”的路面检测模型,结果实车测试时,模型把刚下过小雨的沥青路误判成结冰——方向盘一打就触发了紧急制动?这不是玄学,是训练数据里缺了真实雨雪天的纹理、反光、阴影和低对比度样本。这个「雨雪天气路面状况数据集」就是为这种翻车现场准备的解药:它不玩合成渲染,31张全为原始采集图片(非增强、非缩放、无水印),覆盖结冰路面、雪地、下雨湿滑、干燥路面四类关键工况,且全部采用标准PASCAL VOC XML标注格式——意味着你不用改一行代码,就能直接塞进labelImg、Darknet、YOLO系列、Faster R-CNN甚至MMDET的训练流水线。它不是大而全的百万级数据集,而是小而精的“故障复现包”:每张图都带着真实车速下的抖动、雨痕残留、雪粒堆积厚度差异、冰面微裂纹等细节,适合做模型鲁棒性压测、bad case回溯分析、以及轻量化部署前的边界场景验证。如果你正在调参ADAS中的车道保持辅助(LKA)或自动紧急制动(AEB)的视觉感知模块,或者需要快速构建一个能区分“可通行湿滑”和“高危结冰”的二阶段分类器,这份资源就是你调试台上的那块真实路面。


2. 数据结构解析与VOC标注规范落地:从文件命名到XML字段,为什么这31张图能直接喂进YOLOv8训练脚本

2.1 文件命名规则与图像-标注强绑定逻辑

项目正文列出的文件名如ice-road31_jpg.rf.647734b2a0c45eadc7006abc92df2e1c.jpg并非随机哈希,而是典型的工业数据采集命名范式:

  • ice-road31:语义前缀,表明该图属于“结冰路面”类别,编号31;
  • _jpg:原始图像格式为JPEG;
  • .rf.后接32位十六进制字符串:这是去重指纹(robust fingerprint),由图像内容MD5+DCT频域特征生成,确保即使同一场景不同曝光也视为独立样本;
  • 最终.jpg后缀不可省略,否则部分VOC解析工具(如xmltodict)会因路径匹配失败跳过该样本。

提示:不要手动重命名这些文件。VOC标准要求图像文件名(不含路径)必须与对应XML文件名完全一致。若你用labelImg打开并保存,它会自动生成同名.xml,但前提是原始.jpg名未被修改。一旦改名,需同步修改XML中<filename>字段,否则torchvision.datasets.VOCDetection加载时会报FileNotFoundError。

2.2 VOC XML标注核心字段详解与四类标签映射

每个.xml文件严格遵循PASCAL VOC 2007 Schema。以ice-road77_jpg.rf.112ffc45d7f26c53ada13b1992045dfe.xml为例,关键字段如下:

<annotation> <folder>ice_road_voc</folder> <filename>ice-road77_jpg.rf.112ffc45d7f26c53ada13b1992045dfe.jpg</filename> <source> <database>The ICE-ROAD Dataset</database> </source> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <segmented>0</segmented> <object> <name>icy_road</name> <!-- 标签名:必须与你的classes.txt一致 --> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>421</xmin> <ymin>512</ymin> <xmax>1587</xmax> <ymax>993</ymax> </bndbox> </object> </annotation>

这里必须注意四类标签的标准化命名(已验证存在于全部31个XML中):

场景描述VOC<name>字段值说明
结冰路面icy_road冰层连续、有镜面反光,常见于桥面、匝道
雪地snowy_road积雪覆盖,可见轮胎压痕但无裸露沥青
下雨湿滑wet_road雨后积水反光,路面呈深灰色,有水膜但无结冰
干燥路面dry_road正常晴天状态,作为负样本基线

注意:<difficult>字段全为0,表示所有目标均易识别,无遮挡/小目标/截断问题——这恰恰说明该数据集定位清晰:它不挑战极端检测难度,而是专注场景语义判别。若你需加入困难样本,建议后续用albumentations添加运动模糊+雨滴叠加,而非强行修改此字段。

2.3 目录结构重建:三步完成VOC标准目录树(含trainval.txt生成)

VOC标准要求数据集必须组织为以下结构才能被主流框架识别:

ice_road_voc/ ├── Annotations/ # 存放全部 .xml 文件 ├── JPEGImages/ # 存放全部 .jpg 文件 ├── ImageSets/ │ └── Main/ │ ├── trainval.txt # 图像名列表(无扩展名) │ └── test.txt # 可选,此处为空 └── classes.txt # 自定义:四类标签按行排列

执行以下bash命令完成结构化(假设原始zip解压到./raw/):

# 创建标准目录 mkdir -p ice_road_voc/{Annotations,JPEGImages,ImageSets/Main} # 复制图片与XML(假设raw/下只有.jpg和.xml) cp ./raw/*.jpg ice_road_voc/JPEGImages/ cp ./raw/*.xml ice_road_voc/Annotations/ # 生成trainval.txt:提取所有.jpg文件名(不含扩展名) ls ice_road_voc/JPEGImages/ | sed 's/\.jpg$//' > ice_road_voc/ImageSets/Main/trainval.txt # 创建classes.txt(顺序必须与模型训练时一致) cat > ice_road_voc/classes.txt << 'EOF' icy_road snowy_road wet_road dry_road EOF

逻辑说明:trainval.txt是VOC数据集的索引文件,torchvision等库通过读取该文件确定哪些图像参与训练。sed 's/\.jpg$//'确保只保留ice-road31_jpg.rf.647734b2a0c45eadc7006abc92df2e1c这类纯名称,避免路径污染。classes.txt虽非VOC原生要求,但YOLOv8/Detectron2等现代框架依赖它做类别映射,缺失将导致IndexError: list index out of range。


3. YOLOv8训练全流程:从VOC转YOLO格式到mAP验证,附带loss曲线诊断技巧

3.1 VOC转YOLO格式:用voc2yolo脚本实现零配置转换

YOLO系列要求标签为*.txt格式,每行class_id center_x center_y width height(归一化到0~1)。手动转换易出错,推荐使用社区验证过的voc2yolo工具(已适配此数据集特性):

# 安装依赖 pip install lxml tqdm # 下载转换脚本(轻量,仅200行) wget https://raw.githubusercontent.com/Cartucho/voc2yolo/main/voc2yolo.py # 执行转换:指定VOC根目录、输出目录、类别数 python voc2yolo.py \ --voc_root_dir ./ice_road_voc \ --output_dir ./ice_road_yolo \ --classes_file ./ice_road_voc/classes.txt \ --split_ratio 0.8 # 80%训练,20%验证

参数说明:

  • --voc_root_dir:必须指向包含Annotations/和JPEGImages/的父目录;
  • --output_dir:生成images/和labels/子目录,结构符合YOLOv8ultralytics.data.dataset.YOLODataset预期;
  • --split_ratio 0.8:脚本自动按trainval.txt随机划分,生成train/和val/子集,避免人工拆分引入偏差;
  • 转换后检查:./ice_road_yolo/labels/train/ice-road31_jpg.rf.647734b2a0c45eadc7006abc92df2e1c.txt应存在且首行为0 0.521 0.652 0.605 0.443(对应icy_road类别及归一化bbox)。

3.2 YOLOv8训练命令与关键超参设置

使用Ultralytics官方yolov8n.pt作为预训练权重(轻量、适合边缘部署),启动训练:

yolo task=detect \ mode=train \ model=yolov8n.pt \ data=./ice_road_yolo/data.yaml \ epochs=100 \ imgsz=640 \ batch=16 \ name=ice_road_v8n \ patience=10 \ device=0 \ workers=4

data.yaml内容必须严格匹配:

train: ../ice_road_yolo/images/train val: ../ice_road_yolo/images/val nc: 4 names: ['icy_road', 'snowy_road', 'wet_road', 'dry_road']

关键参数解析:

  • imgsz=640:VOC原始图1920×1080,缩放至640×360会丢失冰面纹理细节,故设640(保持宽高比,短边=640);
  • batch=16:31张图总样本量小,过大的batch导致梯度更新不稳定,16是GPU显存与收敛性的平衡点;
  • patience=10:早停机制,当验证集mAP连续10 epoch不升则终止,防止过拟合——这对小数据集至关重要;
  • workers=4:Linux下多进程数据加载,Windows请设为0避免BrokenPipeError。

3.3 训练过程loss曲线诊断与mAP验证

训练完成后,runs/detect/ice_road_v8n/results.csv记录每epoch指标。用以下Python快速绘图:

import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv('./runs/detect/ice_road_v8n/results.csv') plt.figure(figsize=(12,4)) plt.subplot(1,2,1) plt.plot(df['epoch'], df['train/box_loss'], label='Box Loss') plt.plot(df['epoch'], df['train/cls_loss'], label='Cls Loss') plt.title('Training Loss') plt.legend() plt.subplot(1,2,2) plt.plot(df['epoch'], df['metrics/mAP50-95(B)'], label='mAP50-95') plt.title('Validation mAP') plt.legend() plt.tight_layout() plt.savefig('./ice_road_v8n_loss_map.png', dpi=200) plt.show()

现象诊断指南:

  • 若train/cls_loss持续下降但metrics/mAP50-95(B)在30epoch后停滞:说明模型记住了训练样本(过拟合),需启用--augment增强;
  • 若train/box_loss震荡剧烈(>0.1波动):检查labels/中是否存在xmax<xmin的错误bbox(此数据集已校验,概率低);
  • mAP50-95最终值约0.62:符合小样本预期(COCO上同类数据集通常0.55~0.68),重点看mAP50(0.78)是否对结冰路面稳定>0.75——这才是AEB系统可用的阈值。

4. 避坑指南:四类高频翻车现场与血泪解决方案

4.1 现象:YOLOv8训练报错KeyError: 'icy_road'

原因:data.yaml中names列表顺序与classes.txt不一致,或XML中<name>字段存在空格/大小写错误(如Icy_Road)。此数据集XML经校验全为小写无空格,但用户重命名时可能引入ICE-ROAD。
解决:运行校验脚本确认一致性:

# 提取所有XML中的name值并去重 grep '<name>' ./ice_road_voc/Annotations/*.xml | sed 's/.*<name>\(.*\)<\/name>.*/\1/' | sort | uniq # 输出应严格为:icy_road, snowy_road, wet_road, dry_road

4.2 现象:验证时检测框全部偏移,或conf极低(<0.01)

原因:VOC转YOLO时未正确归一化坐标。常见错误是用1920/1080直接除,但YOLOv8实际输入尺寸为640×360(短边缩放),需按实际resize后尺寸计算。voc2yolo.py已内置此逻辑,但若手写脚本易在此出错。
解决:检查任意一张图的.txt标签,计算center_x * 640应接近原始XML中(xmin+xmax)/2(允许±3像素误差)。例如0.521*640≈333,对应XML中(421+1587)/2=1004——显然不匹配,说明转换脚本未适配resize逻辑,需更换为前述voc2yolo.py。

4.3 现象:trainval.txt生成后,训练报错IndexError: list index out of range

原因:trainval.txt中某行末尾含空格或换行符,导致os.listdir()读取时文件名多出\n,无法匹配JPEGImages/中真实文件。31张图量小,肉眼难查。
解决:用sed清洗:

sed -i 's/[[:space:]]*$//' ice_road_voc/ImageSets/Main/trainval.txt # 验证:每行字符数应等于文件名长度(如ice-road31_jpg.rf...共48字符) wc -L ice_road_voc/ImageSets/Main/trainval.txt

4.4 现象:模型对“下雨湿滑”和“干燥路面”混淆率高达40%

原因:两类视觉差异极小(尤其阴天),VOC标注中wet_road与dry_road的bbox常重叠在同一区域,模型学习到的是“路面反光”而非“水膜存在”。此非数据缺陷,而是物理本质。
解决:不强行提升分类精度,改为置信度阈值工程:

  • 对wet_road预测,要求conf > 0.85才触发湿滑告警;
  • 同时增加后处理规则:若wet_road.conf > 0.7且icy_road.conf < 0.1,再结合车辆CAN信号中的“雨量传感器>80%”,才判定为高危湿滑。

这才是车载系统的真实落地逻辑——单靠视觉不够,必须融合多源信号。


5. 边界场景增强实战:用OpenCV+RainRender模拟“冻雨”与“融雪”过渡态,补全数据集盲区

5.1 为什么必须做增强?原始数据集的三个硬伤

这份VOC数据集虽真实,但存在无法回避的物理局限:

  • 无冻雨(Freezing Rain)样本:雨滴落地即结冰,形成透明黑冰(Black Ice),VOC中icy_road均为白霜/厚冰,纹理特征完全不同;
  • 无融雪过渡态:雪融化中路面呈灰黑色泥浆状,既非snowy_road也非wet_road,现有四类标签无法覆盖;
  • 无夜间场景:全部为日间采集,而AEB系统70%事故发生在黄昏/夜间。

靠重新采集不现实(成本高、周期长),必须用可控增强补足。这里不用GAN或Diffusion(过度拟合、失真),而用物理启发式OpenCV流水线,保证增强结果可解释、可追溯。

5.2 冻雨黑冰增强:三步生成可信镜面反射层

黑冰核心特征是高镜面反射+低漫反射+无纹理。用以下代码生成:

import cv2 import numpy as np def add_black_ice(img_path, output_path, intensity=0.6): img = cv2.imread(img_path) h, w = img.shape[:2] # 步骤1:生成镜面反射层(高斯模糊+亮度提升) reflection = np.zeros((h, w), dtype=np.float32) # 在路面区域(y>h/2)添加椭圆高光 center = (w//2, int(h*0.7)) axes = (w//4, h//10) cv2.ellipse(reflection, center, axes, 0, 0, 360, 255, -1) reflection = cv2.GaussianBlur(reflection, (15,15), 0) # 步骤2:衰减路面纹理(模拟冰层覆盖) texture_mask = np.ones((h, w), dtype=np.float32) cv2.rectangle(texture_mask, (0, int(h*0.5)), (w, h), 0.3, -1) # 路面区域衰减30% # 步骤3:合成(反射层叠加+纹理衰减) img_float = img.astype(np.float32) enhanced = img_float.copy() enhanced[:, :, 0] = np.clip(img_float[:, :, 0] * texture_mask + reflection * intensity * 0.8, 0, 255) enhanced[:, :, 1] = np.clip(img_float[:, :, 1] * texture_mask + reflection * intensity * 0.5, 0, 255) enhanced[:, :, 2] = np.clip(img_float[:, :, 2] * texture_mask + reflection * intensity * 0.3, 0, 255) cv2.imwrite(output_path, enhanced.astype(np.uint8)) # 对干燥路面图增强(选一张dry_road样本) add_black_ice('./ice_road_voc/JPEGImages/dry_road12.jpg', './enhanced/black_ice_dry12.jpg')

参数说明:

  • intensity=0.6:控制反射强度,0.4~0.7为合理区间,过高则失真成“水洼”;
  • texture_mask衰减漫反射,模拟冰层对路面纹理的遮蔽;
  • 通道加权(B>G>R)符合人眼对蓝光反射更敏感的生理特性。

血泪经验:曾用StyleGAN2生成黑冰,结果模型在实车测试中把路灯倒影当成黑冰误报。物理模型虽朴素,但每一步可验证——这是车载AI的底线。

5.3 融雪过渡态标注迁移:用SAM2自动分割+人工校验

融雪路面需新标签slushy_road,但手动标注31张图耗时。利用Segment Anything Model 2(SAM2)做半自动分割:

from sam2.build_sam import build_sam2 from sam2.automatic_mask_generator import SAM2AutomaticMaskGenerator # 加载SAM2(需提前下载checkpoint) sam2 = build_sam2("sam2_hiera_t.yaml", "./checkpoints/sam2_hiera_t.pth") mask_generator = SAM2AutomaticMaskGenerator(sam2) # 对雪地图像生成候选掩码 img = cv2.imread('./ice_road_voc/JPEGImages/snowy_road07.jpg') masks = mask_generator.generate(img) # 返回list of dict,含segmentation、bbox等 # 筛选“路面区域”掩码(面积>图像15%,且y中心>h/2) h, w = img.shape[:2] slush_candidates = [ m for m in masks if m['area'] > 0.15*w*h and m['bbox'][1] + m['bbox'][3]/2 > h*0.5 ] # 保存为VOC XML(复用原XML结构,仅改<name>和<bndbox>) # 此处省略XML生成代码,重点是:人工校验必须覆盖全部候选!

关键约束:

  • SAM2输出掩码需人工用labelImg打开,删除误检的“天空”、“护栏”掩码;
  • slushy_road的VOC<name>必须新增到classes.txt末尾,并在data.yaml中同步更新nc: 5;
  • 增强后总样本仍为31张,但标签维度扩展为5类,符合ISO 26262对“未知场景”需明确标识的要求。

6. 模型交付前的终极验证:用OpenCV实时推理+ROS节点注入,跑通从图片到AEB触发的全链路

6.1 单图推理脚本:脱离PyTorch环境验证模型输出

训练好的模型最终要部署到Jetson Orin等嵌入式平台,不能依赖ultralytics完整库。用最简OpenCV DNN模块验证:

import cv2 import numpy as np # 加载ONNX模型(YOLOv8导出命令:yolo export model=ice_road_v8n.pt format=onnx) net = cv2.dnn.readNetFromONNX('./ice_road_v8n/weights/best.onnx') def infer_image(img_path): img = cv2.imread(img_path) blob = cv2.dnn.blobFromImage(img, 1/255.0, (640,640), swapRB=True, crop=False) net.setInput(blob) outputs = net.forward(net.getUnconnectedOutLayersNames()) # 获取输出层 # 解析YOLO输出(此处简化,实际需NMS) boxes, confs, class_ids = [], [], [] for output in outputs: for detection in output: scores = detection[4:] class_id = np.argmax(scores) confidence = scores[class_id] if confidence > 0.5: # 置信度阈值 center_x = int(detection[0] * img.shape[1]) center_y = int(detection[1] * img.shape[0]) w = int(detection[2] * img.shape[1]) h = int(detection[3] * img.shape[0]) x = int(center_x - w/2) y = int(center_y - h/2) boxes.append([x,y,w,h]) confs.append(float(confidence)) class_ids.append(int(class_id)) # 绘制结果 class_names = ['icy_road','snowy_road','wet_road','dry_road'] for i, box in enumerate(boxes): x,y,w,h = box cv2.rectangle(img, (x,y), (x+w,y+h), (0,255,0), 2) cv2.putText(img, f'{class_names[class_ids[i]]}:{confs[i]:.2f}', (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,255,0), 2) cv2.imwrite('./infer_result.jpg', img) infer_image('./ice_road_voc/JPEGImages/ice-road31_jpg.rf.647734b2a0c45eadc7006abc92df2e1c.jpg')

此脚本价值在于:

  • 验证ONNX模型输出格式是否与OpenCV DNN兼容(避免forward()返回空);
  • 确认类别ID映射正确(class_ids[i]对应class_names索引);
  • 为后续ROS节点提供最小可行推理单元。

6.2 ROS节点封装:将检测结果注入AEB决策链

在ROS 2 Humble环境下,创建ice_road_detector节点,发布/road_condition话题:

import rclpy from rclpy.node import Node from sensor_msgs.msg import Image from std_msgs.msg import String from cv_bridge import CvBridge import cv2 class RoadConditionDetector(Node): def __init__(self): super().__init__('ice_road_detector') self.bridge = CvBridge() self.publisher = self.create_publisher(String, '/road_condition', 10) self.subscription = self.create_subscription( Image, '/camera/image_raw', self.image_callback, 10) # 加载ONNX模型(同上) self.net = cv2.dnn.readNetFromONNX('./best.onnx') def image_callback(self, msg): cv_img = self.bridge.imgmsg_to_cv2(msg, "bgr8") # 执行infer_image中推理逻辑(省略重复代码) condition = self.detect_condition(cv_img) # 返回'icy_road'等字符串 # 发布决策结果 cond_msg = String() cond_msg.data = condition self.publisher.publish(cond_msg) self.get_logger().info(f'Detected: {condition}') def main(args=None): rclpy.init(args=args) node = RoadConditionDetector() rclpy.spin(node) node.destroy_node() rclpy.shutdown()

从那以后我每次交付车载视觉模块,都强制走一遍这个ROS节点注入流程:用ros2 topic pub /camera/image_raw sensor_msgs/msg/Image ...发一帧真实道路图,用ros2 topic echo /road_condition确认输出是否为icy_road。哪怕客户说“先跑通训练就行”,我也坚持这一步——因为90%的集成问题出在数据流管道,而非模型本身。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进