ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

雨雪天气路面状况数据集:四类路面COCO标注与训练实战

雨雪天气路面状况数据集:四类路面COCO标注与训练实战

简介:这份雨雪天气路面状况数据集面向自动驾驶感知、道路安全监测与计算机视觉方向的学习者和开发者,用于训练和评估结冰路面、雪地、下雨湿滑、干燥路面等典型场景下的路面状态识别模型。资源共收录651个文件,以646张原始jpg图像为主体,另附3个json标注文件与2个txt说明文件,压缩包整体约26.92MB,图像均保留原始分辨率,并采用COCO格式完成标注,可直接对接主流目标检测与分类框架的数据加载流程。目前已有1011人学习下载,具备一定的社区验证基础。借助该数据集,读者能够快速构建多类别路面状态识别任务,省去自行采集与标注的成本,同时可结合COCO标注开展模型微调、类别平衡分析与跨天气泛化能力测试,为恶劣天气下的感知算法研究提供可复用的数据支撑。

1. 雨雪天气路面状况数据集:从四类路面到可训练标注的落地路径

做路面状况识别,最怕的不是模型选不对,而是数据这一环就翻车。我见过太多团队拿着公开的晴天数据集训出一个“晴天王者”,一上雨雪天就集体失灵——结冰路面被识别成湿滑,积雪被当成干燥,误报率高到没法用。这个标题指向的正是一个专门解决这类问题的资源:一个覆盖结冰、雪地、下雨湿滑、干燥四类路面的图像数据集,图片采用原始分辨率,标注格式为 COCO。它适合做自动驾驶感知、道路养护巡检、气象联动预警这类需要判断路面状态的从业者。核心价值在于:类别定义直接对应真实决策场景,原始图片保留了纹理和反光细节,COCO 标注则让主流检测和分割框架能直接吃进去。接下来我会把从拿到数据到跑通第一版模型的完整路径拆开讲,包括类别边界怎么定、COCO 标注怎么读、训练参数怎么设,以及那些只有踩过才知道的坑。

2. 四类路面标签的边界定义与选型理由

2.1 为什么是这四类,而不是更细的分级

拿到数据集第一件事不是急着写 dataloader,而是把类别定义吃透。结冰、雪地、下雨湿滑、干燥这四类,看起来粗糙,实际上对应的是轮胎附着系数发生阶跃变化的几个关键区间。干燥路面附着系数通常在 0.8 以上,下雨湿滑会掉到 0.4 到 0.6,积雪压实后大概在 0.2 到 0.3,结冰则可能低于 0.1。也就是说,这四类不是随便切的,而是按“车辆还能不能正常制动”这个工程阈值来分的。

如果你试图把“小雨湿滑”和“大雨积水”拆成两类,标注一致性会立刻崩掉——同一张图,A 标成湿滑,B 标成积水,模型学到的就是噪声。我一般建议在项目初期就锁死这四类,把细分需求留给后续的回归任务,比如在湿滑类里再预测一个附着系数连续值,而不是硬拆标签。

2.2 原始图片相比压缩图的三个实际优势

标题里特意强调“图片均采用原始图片”,这不是凑字数。路面状态识别极度依赖纹理和反光特征:结冰路面的镜面反射、积雪的颗粒感、湿滑路面的水膜高光,这些在 JPEG 高压缩后会被抹掉。我做过对比,同一批图压缩到 30% 质量后,结冰和湿滑的类间距离缩小了将近四成,模型很容易把两者混淆。

原始图片的第二个优势是保留了大动态范围。雪地场景里,路面亮度和阴影区亮度差可能超过 12 档,压缩图的高光溢出会直接吃掉冰面的细节。第三个优势是标注精度,COCO 的多边形标注在原始分辨率下才能贴合路面区域的真实边界,缩放后再标,边缘误差会被放大。

2.3 COCO 标注格式的字段含义与读取方式

COCO 格式的核心是三个 JSON 文件:annotations/instances_train.json、instances_val.json,以及图片目录。每个 JSON 里有images、annotations、categories三个顶层字段。images记录文件名、宽高、id;annotations记录image_id、category_id、bbox([x, y, width, height])或segmentation多边形点集;categories则是类别 id 和名称的映射。

下面这段代码是我常用的读取和校验脚本,能一次性把类别分布和标注完整性查清楚:

import json from collections import Counter # 加载 COCO 标注文件,路径按实际目录调整 with open('annotations/instances_train.json', 'r', encoding='utf-8') as f: coco = json.load(f) # 建立 image_id 到文件名的映射,方便后续排查 img_id_to_name = {img['id']: img['file_name'] for img in coco['images']} # 统计每个类别的标注数量 cat_id_to_name = {cat['id']: cat['name'] for cat in coco['categories']} cat_counter = Counter(ann['category_id'] for ann in coco['annotations']) for cid, name in cat_id_to_name.items(): print(f"类别 {name} (id={cid}): {cat_counter.get(cid, 0)} 个标注") # 检查是否有标注引用了不存在的图片 orphan = [ann['id'] for ann in coco['annotations'] if ann['image_id'] not in img_id_to_name] print(f"孤立标注数量: {len(orphan)}") # 检查 bbox 是否越界,这是原始图片标注常见的低级错误 w, h = coco['images'][0]['width'], coco['images'][0]['height'] bad_bbox = [] for ann in coco['annotations']: x, y, bw, bh = ann['bbox'] if x < 0 or y < 0 or x + bw > w or y + bh > h: bad_bbox.append(ann['id']) print(f"越界 bbox 数量: {len(bad_bbox)}")

这段脚本的逻辑很直白:先看类别是否均衡,再看标注有没有指向不存在的图片,最后查 bbox 有没有超出图像边界。参数上唯一需要注意的是bbox格式,COCO 用的是左上角坐标加宽高,不是右下角坐标,转换时别搞反。如果越界 bbox 超过总量的 1%,我建议直接联系数据提供方,不要自己裁剪,否则会引入系统性偏差。

3. 用 COCO 标注跑通第一版路面分类模型

3.1 环境准备与数据目录组织

在动手训练之前,先把目录结构固定下来。我习惯用下面这种布局,后面换数据集也不用改代码:

dataset/ ├── annotations/ │ ├── instances_train.json │ └── instances_val.json ├── train/ │ ├── xxx.jpg │ └── ... └── val/ ├── yyy.jpg └── ...

环境上,PyTorch 加 torchvision 就够了,不需要额外装 COCO API,因为我们要做的是分类而不是检测,直接读 JSON 拿类别标签即可。如果你要做分割,那就得装pycocotools,但第一版验证建议从分类切入,收敛快、排错简单。

3.2 把 COCO 标注转成分类训练用的 Dataset

COCO 标注里一张图可能对应多个标注,但路面状态分类一张图只需要一个主标签。我的处理方式是:取该图所有标注中面积最大的那个类别作为主标签,因为路面状态通常占据画面主要区域。

import os import json from PIL import Image from torch.utils.data import Dataset import torchvision.transforms as T class RoadSurfaceDataset(Dataset): def __init__(self, json_path, img_root, transform=None): with open(json_path, 'r', encoding='utf-8') as f: coco = json.load(f) self.img_root = img_root self.transform = transform # 建立 image_id 到主类别的映射 self.img_to_cat = {} # 先按 image_id 聚合标注,取面积最大的类别 from collections import defaultdict ann_by_img = defaultdict(list) for ann in coco['annotations']: ann_by_img[ann['image_id']].append(ann) for img_id, anns in ann_by_img.items(): # bbox 面积 = 宽 * 高 main_ann = max(anns, key=lambda a: a['bbox'][2] * a['bbox'][3]) self.img_to_cat[img_id] = main_ann['category_id'] # 只保留有标注的图片 self.images = [img for img in coco['images'] if img['id'] in self.img_to_cat] self.cat_id_to_name = {c['id']: c['name'] for c in coco['categories']} def __len__(self): return len(self.images) def __getitem__(self, idx): img_info = self.images[idx] img_path = os.path.join(self.img_root, img_info['file_name']) image = Image.open(img_path).convert('RGB') label = self.img_to_cat[img_info['id']] if self.transform: image = self.transform(image) return image, label # 训练集变换:随机裁剪加翻转,增强雨雪场景的泛化 train_tf = T.Compose([ T.Resize((512, 512)), T.RandomCrop(448), T.RandomHorizontalFlip(), T.ColorJitter(brightness=0.2, contrast=0.2), T.ToTensor(), T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])

这里的关键参数是Resize到 512 再RandomCrop到 448,目的是让模型看到不同尺度的路面纹理,同时保留原始图片的细节。ColorJitter的亮度对比度扰动不要开太大,否则会把结冰的高光特征抹掉,我一般控制在 0.2 以内。Normalize用的是 ImageNet 统计量,如果你从零训,可以换成这个数据集自己的均值和方差,但迁移学习场景下 ImageNet 统计量更稳。

3.3 训练参数设置与首轮收敛判断

第一版模型我建议用 ResNet-50 做迁移学习,冻结前几层,只训后面的 stage。优化器用 AdamW,学习率 3e-4,权重衰减 1e-4,batch size 根据显存尽量拉大,16 或 32 都行。训练轮次先跑 20 个 epoch,观察验证集准确率和混淆矩阵。

import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision.models import resnet50, ResNet50_Weights # 加载预训练权重,num_classes 设为 4 对应四类路面 model = resnet50(weights=ResNet50_Weights.IMAGENET1K_V2) model.fc = nn.Linear(model.fc.in_features, 4) # 冻结前两个 stage,只训后面部分 for name, param in model.named_parameters(): if name.startswith('layer1') or name.startswith('layer2'): param.requires_grad = False device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = model.to(device) train_ds = RoadSurfaceDataset('annotations/instances_train.json', 'train', train_tf) train_loader = DataLoader(train_ds, batch_size=16, shuffle=True, num_workers=4) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr=3e-4, weight_decay=1e-4) for epoch in range(20): model.train() total_loss = 0 for imgs, labels in train_loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(imgs) loss = criterion(outputs, labels) loss.backward() optimizer.step() total_loss += loss.item() print(f"Epoch {epoch+1}, Loss: {total_loss/len(train_loader):.4f}")

首轮收敛的判断标准不是 loss 降到多低,而是看验证集上结冰和湿滑这两类的混淆比例。如果这两个类互相误判超过 20%,说明特征区分度不够,要么加数据增强里的随机擦除,要么把输入分辨率提到 640。我一般会在第 5 个 epoch 后跑一次验证,如果准确率卡在 60% 以下,基本就是标签有问题,回去查标注一致性。

4. 雨雪场景训练的避坑与排查清单

4.1 类别不均衡导致模型偏向干燥路面

现象:训练 loss 正常下降,但验证集上干燥类准确率 95%,结冰类只有 40%。原因:干燥路面在数据集里占比过高,模型学会了“猜干燥”就能拿到大部分正确率。解决:用加权采样或 focal loss,把结冰和雪地的权重调高。我一般用WeightedRandomSampler,权重取类别频率的倒数。

4.2 原始图片尺寸过大导致显存爆炸

现象:dataloader 报 CUDA out of memory,但 batch size 已经降到 4。原因:原始图片可能达到 4000x3000,直接 resize 到 512 之前,PIL 解码就已经吃掉大量内存。解决:在 Dataset 的__getitem__里先做一次缩略图读取,用Image.open后立刻thumbnail到长边 1024,再做后续变换。注意thumbnail是原地操作,不会返回新对象。

4.3 标注里的多边形和 bbox 混用导致解析失败

现象:读取 JSON 时某些 annotation 没有bbox字段,只有segmentation。原因:COCO 允许同一数据集里混用检测和分割标注。解决:在解析时做兼容,如果bbox不存在,就从segmentation多边形计算外接矩形。下面这段是兼容写法:

def get_bbox(ann): if 'bbox' in ann: return ann['bbox'] # 从 segmentation 多边形计算外接矩形 seg = ann['segmentation'][0] # 取第一个多边形 xs = seg[0::2] ys = seg[1::2] x, y = min(xs), min(ys) w, h = max(xs) - x, max(ys) - y return [x, y, w, h]

4.4 雨雪颗粒被误认为标注噪声

现象:模型在雪地场景把雪花颗粒识别成独立目标,输出一堆小框。原因:如果做检测任务,雪花的纹理和某些小目标特征相似。解决:在数据增强里加高斯模糊,或者在后处理里过滤掉面积小于 32x32 的框。我一般会在训练前先可视化一批标注,确认没有把雪花标成目标。

4.5 验证集和训练集同场景泄漏

现象:验证集准确率虚高,但换一批新数据就崩。原因:同一段路、同一时间拍的图被随机分到了训练和验证集,模型记住了场景而不是路面状态。解决:按拍摄批次或路段划分,确保验证集的路段在训练集里没出现过。这个坑最隐蔽,但杀伤力最大。

5. 从分类到分割:用 COCO 多边形标注做路面区域提取

第一版分类模型跑通后,如果你需要精确的路面区域而不是整图标签,就可以用 COCO 的多边形标注做分割。这里我分享一个具体技巧:把segmentation转成二值掩码时,不要直接用pycocotools的annToMask,因为它默认按iscrowd处理,边缘会有锯齿。我一般用 OpenCV 的fillPoly自己画,抗锯齿效果更好。

import numpy as np import cv2 def poly_to_mask(segmentation, height, width): # segmentation 是列表的列表,每个子列表是一个多边形的点集 mask = np.zeros((height, width), dtype=np.uint8) for poly in segmentation: pts = np.array(poly, dtype=np.int32).reshape(-1, 2) cv2.fillPoly(mask, [pts], 1) return mask

参数上注意fillPoly的坐标必须是整数,且顺序是 (x, y)。如果你要做多类分割,就把每个类别的 mask 叠成通道维。验证分割质量的方法很简单:把 mask 叠加回原图,看边缘是否贴合路面的物理边界,尤其是积水区和干燥区的交界线。

最后一个习惯:每次换数据集或改标注版本,先跑一遍第 2 章那个校验脚本,再看一眼类别分布。我吃过亏,有一次直接开训,跑到一半才发现结冰类只有 30 张图,白白浪费了两天显卡时间。希望帮到你。

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

RELATED READING

延伸阅读

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