恶劣天气路面识别实战:COCO转YOLOv8训练与调参
2026/9/11 8:23:20 网站建设 项目流程

简介:面向自动驾驶、智能交通与路面状态监测场景的雨雪天气路面状况数据集,涵盖结冰路面、雪地、下雨湿滑、干燥路面四类典型路况,所有图片均为原始采集图像,并采用COCO格式标注,可直接用于目标检测、图像分类与语义分割等模型的训练与验证。资源包共651个文件,其中646张jpg原始图片构成核心样本,3个json文件提供COCO格式的标注信息,2个txt文件为类别说明或数据划分列表,整体压缩包约26.92MB,体量适中,便于快速下载与离线使用。目前已有1010人学习下载,适用于计算机视觉初学者进行路面状态识别实验,也可作为真实雨雪天气场景下算法测试的补充数据。借助清晰的目录与标注结构,使用者能够直接加载标注文件开展模型训练,省去手动标注成本,快速验证结冰、湿滑等极端天气下的识别效果。

1. 恶劣天气路面识别:一个被低估的长尾工程问题

冻雨夜里的高架桥、刚飘雪的城市快速路、雨停后反光的地面——这四个状态在自动驾驶和道路养护里不是“同一类障碍物”,而是四种完全不同的物理表面。干燥路面的制动距离和结冰路面可能差出 5 倍,但视觉上,结冰和湿滑在夜间几乎无法区分。这个数据集的价值在于它没有做过任何图像增强,全部采用原始图片,并用 COCO 格式做了标记,类别锁定在结冰路面、雪地、下雨湿滑、干燥路面四类。对做智慧交通、自动驾驶感知、道路除雪养护系统的工程师来说,它是少见的“按物理状态分类”而非“按物体分类”的数据集,适合用来训练路面状态分类器或目标检测模型。读完这篇,你能把它转成 YOLO 格式直接训练,也知道结冰和湿滑这两个最容易混的类该怎么调。

2. 路面状态识别为什么难:先搞清楚数据要解决什么问题

2.1 结冰与湿滑的视觉边界:分类任务的核心矛盾

干燥路面纹理清晰,沥青颗粒可见,车道线边缘锐利;湿滑路面有镜面反射,能看到路灯倒影,但车道线依然存在;雪地是低纹理的均匀白色,标线被覆盖;结冰路面最特殊——透明冰让路面看起来更暗更光滑,黑冰(black ice)状态下甚至比干燥路面更暗,只是多了一层不均匀的反光。

这四个类别不是简单的亮度递进。用颜色直方图或灰度统计只能区分干燥和雪地,区分不了结冰和湿滑。这也是为什么这个数据集用原始图片:如果训练前做了去雾、白平衡或 HDR 合成,反而会把结冰和湿滑的反射特性拉近,模型学到的特征在真实摄像头下会失效。原始图片保留了传感器最真实的响应,部署时不吃亏。

2.2 COCO 标记在路面数据里的读写

COCO 格式的核心是三个字段:images记录图片 ID 和尺寸,annotations记录每个目标的类别、边界框和多边形,categories定义类别 ID 到名称的映射。路面状态数据集里,常见做法是对整幅图打一个“路面区域”多边形,或对画面中每一块可行驶区域单独标注。前者适合分类任务,后者适合检测任务。

读 COCO 标注最常见的坑是segmentation字段是嵌套列表,一个多边形可能有多段轮廓。用下面这段代码可以快速检查标注是否正确:

import json from PIL import Image, ImageDraw with open('annotations/instances.json', 'r') as f: coco = json.load(f) # 建立类别 ID 到名称的映射 cat_map = {c['id']: c['name'] for c in coco['categories']} print("类别映射:", cat_map) # 取第一张图和它的标注 img_info = coco['images'][0] anns = [a for a in coco['annotations'] if a['image_id'] == img_info['id']] im = Image.open(f"images/{img_info['file_name']}").convert('RGB') draw = ImageDraw.Draw(im) for a in anns: # COCO 多边形是 [x1,y1,x2,y2,...] 的扁平列表 poly = list(zip(a['segmentation'][0][0::2], a['segmentation'][0][1::2])) draw.polygon(poly, outline=(255, 0, 0), width=3) draw.text(poly[0], cat_map[a['category_id']], fill=(255, 255, 0)) im.save('check_visualize.jpg')

这段代码做两件事:一是确认categories的 ID 是否是 0 起始(COCO 官方是从 1 开始的,很多标注工具导出的则从 0 开始);二是把多边形画回原图,检查标注是否贴合路面边缘。路面标记线的错标很常见——雪地场景下标注员常常把露出的一小段标线漏掉,这种遗漏在后续训练中会持续干扰模型。

2.3 从 COCO 转 YOLO 的坑:归一化与类别 ID

YOLO 系列训练需要 txt 格式的标签,每行是class cx cy w h,全部归一化到 [0,1]。转格式本身不复杂,但两个细节容易翻车。

第一,COCO 的bbox字段是[x, y, width, height]的绝对像素值,YOLO 需要的是中心点坐标和宽高,必须做换算。第二,COCO 的类别 ID 不一定是连续的 0、1、2、3,如果直接用原始 ID 写进 txt,训练时会报类别越界。转换时应该按categories在 JSON 里的顺序重新映射,而不是直接读category_id字段:

python tools/coco2yolo.py \ --coco annotations/instances.json \ --img-dir images \ --out-dir labels \ --reindex

转换脚本里必须有--reindex这一步:它把所有存在的category_id排序后映射到 0 开始的连续整数。比如原始 ID 是 [1, 3, 5, 7],映射后就变成 [0, 1, 2, 3]。不做这一步,训练时 loss 会直接崩。

还有一个容易漏的:数据划分必须按场景而不是按图片随机切。这个数据集里的图片很可能来自同一路段的多帧连拍,如果同一场景的帧分别进了 train 和 val,验证指标会虚高,部署后回退严重。我的做法是按file_name的前缀(通常代表拍摄时间和地点)分组,再对组做 8:2 划分。

3. 结冰路面与湿滑路面:样本不均衡和标注噪声的处理

3.1 类别占比与最小编解:先统计再训练

动手训练前,先统计每个类别的样本量、标注框数量和平均面积。缺了这一步,后面所有调参都是在猜。

from collections import Counter import json with open('annotations/instances.json') as f: coco = json.load(f) cat_count = Counter() for a in coco['annotations']: cat_count[a['category_id']] += 1 total = sum(cat_count.values()) for cid, cnt in cat_count.most_common(): name = coco['categories'][cid]['name'] if cid in cat_count else 'unknown' print(f"{name:8s} {cnt:5d} 张 ({cnt/total*100:.1f}%)")

这个数据集最容易出现的情况是:干燥路面样本最多,结冰路面最少(毕竟 0°C 以下还要路面有水的采集窗口很短)。结冰样本少带来的问题不仅是过拟合,而是模型会把“暗色路面”直接判给湿滑类,因为湿滑类样本多、先验概率大。

常见做法是给结冰类做复制粘贴增强(Copy-Paste):从结冰图片里裁出路面区域,贴到干燥路面的下半部分。路面是有透视关系的,粘贴时要沿着消失线缩放,不能直接等比粘贴。这个操作可以用任意图片标注工具完成,不需要写生成模型。

3.2 原始图片不做色彩增强,Mosaic 按需开关

“原始图片训练”不代表完全不做数据增强,而是只做几何增强,不做光度增强。Mosaic 和 Mixup 属于几何/混合增强,可以用;色彩抖动、随机亮度、高斯模糊这些会破坏反射信息的操作要关掉。

YOLOv8 的默认增强策略里,hsv_hhsv_shsv_v默认都开启了,对路面识别来说建议全部改成 0。原因很简单:这是分类任务,不是检测任务,颜色的微小偏移直接改变类别语义——结冰和湿滑的差异恰恰在反射光的分布上。训练命令:

yolo detect train \ data=road_condition.yaml \ model=yolov8m.pt \ epochs=100 \ imgsz=1280 \ batch=16 \ hsv_h=0.0 \ hsv_s=0.0 \ hsv_v=0.0 \ degrees=0 \ fliplr=0.5 \ mosaic=0.5

关键参数说明:

  • imgsz=1280:结冰纹理是小尺度特征,640 下结冰和湿滑的灰度差异不到 10 个像素,1280 能保留更多反射细节。显存不够就降到 960,不要降到 640。
  • mosaic=0.5:Mosaic 把四张小图拼成一张,对小物体检测帮助大,但路面是整幅大目标,拼接会导致路面色块混杂。前 30 个 epoch 开 0.5,后面降到 0.1。
  • fliplr=0.5:水平翻转保留。路面状态和左右方向无关,可以放心开。
  • degrees=0:旋转必须关。道路是有重力方向的,旋转 90° 会让车顶的积雪被误判成路面雪地。

3.3 用 mmrotate 的思路解决倾斜路面区域

如果标注的是不规则路面块(弯道、坡道、匝道),水平框会框进大量背景。这时候可以借鉴 mmrotate 处理 DOTA 数据集的思路,用旋转框(oriented bounding box)替代水平框。

把 COCO 转成 DOTA 格式,核心是取多边形的最小外接矩形,输出cx cy w h angle

import numpy as np import cv2 def poly_to_obb(poly): pts = np.array(poly, dtype=np.float32).reshape(-1, 2) rect = cv2.minAreaRect(pts) (cx, cy), (w, h), angle = rect # OpenCV 的 angle 范围是 [-90, 0],转成 [0, 180) angle = angle if angle >= 0 else angle + 180 return cx, cy, w, h, angle

旋转框的好处是让模型学到“路面区域是有方向的”,弯道场景下不会把两侧护栏和植被当作可行驶路面。代价是训练时间增加约 20%,因为旋转框的 IoU 计算更复杂。如果这个数据集的标注是整幅图一个大框,那就没必要用 mmrotate,YOLO 水平框就够。

4. 用 YOLOv8 训练自己的数据集:从 COCO 到模型的完整闭环

4.1 数据集目录结构与 YAML 配置

把转换好的数据按标准结构组织,这是 YOLO 系列训练的基本要求:

road_condition/ ├── images/ │ ├── train/ # 干燥 1200 张,雪地 500 张 │ └── val/ ├── labels/ │ ├── train/ # 与 images 同名 .txt │ └── val/ ├── road_condition.yaml └── annotations/ └── instances.json

road_condition.yaml内容:

path: /path/to/road_condition train: images/train val: images/val names: 0: dry 1: icy 2: rainy 3: snowy

注意names的索引必须和转换脚本输出的class字段一致。这里把icy放在dry后面而不是最后,是因为 YOLO 训练时的类别顺序不影响精度,但影响混淆矩阵的阅读——把容易混淆的两类放一起,验证时一眼能看错在哪。

YOLOv8 的path字段支持绝对路径和相对路径。建议写绝对路径,因为训练脚本常常被 IDE 或终端从不同目录启动,相对路径容易踩坑。

4.2 训练命令与超参数:针对四分类的配置

训练命令:

yolo detect train \ data=road_condition.yaml \ model=yolov8m.pt \ epochs=100 \ imgsz=1280 \ batch=8 \ optimizer=AdamW \ lr0=1e-4 \ lrf=0.01 \ warmup_epochs=3 \ cos_lr=True \ patience=15 \ seed=42

和默认配置比较,改动集中在三处:

  • optimizer=AdamW + lr0=1e-4:路面分类的 loss 曲面比通用目标检测更陡峭,SGD 的默认 0.01 初始学习率容易发散。AdamW 配合小学习率更稳,收敛后 mAP 通常比 SGD 高 0.5-1 个点。
  • warmup_epochs=3:前 3 个 epoch 让 BN 统计量稳定下来,特别是结冰类样本少,batch 里可能连续几轮没有结冰样本。
  • patience=15:结冰类的 AP 提升是缓慢的,过早停止(patience=5)会在结冰类刚要起势时打断训练。

对比一下 YOLOv5 训练自己的数据集时的常用参数:v5 的--hyp默认配置里hsv_h=0.015hsv_s=0.7,这些对路面任务同样要置零。

4.3 从指标和混淆矩阵判断模型有没有真正学会

训练完先看 per-class AP,不要只看 mAP。跑验证集并输出混淆矩阵:

yolo detect val \ model=runs/detect/train/weights/best.pt \ data=road_condition.yaml \ conf=0.25 \ iou=0.5

训练日志里会生成confusion_matrix_normalized.png。重点关注:

类别常见误判
icy被判为 rainy(夜间反光相似)
snowy被判为 dry(阳光下的融雪区域)
rainy被混入 dry(小雨且无倒影)

如果 icy 被大量判为 rainy,两个手段:

  1. 给结冰类增加样本,不是网上随便搜图,而是从同路段的历史监控里截帧,让分布一致。
  2. 在 loss 里加大结冰类的权重。YOLO 官方不支持直接配 per-class loss weight,常见做法是把结冰样本复制 1.5 倍进数据集,简单有效。
4.4 推理时的预处理必须和训练一致

由于这个数据集是原始图片,推理时的预处理也应当做到最小化。不要在部署管线里加白平衡、去雾或自动对比度增强,这些操作会改变反射强度分布。YOLO 推理时唯一允许的预处理是 letterbox 缩放,而且rect=True模式要谨慎用——它用批量内最短边对齐,会让同一批次的图片尺寸不一致,对整幅路面图来说影响不大,但会拉长 NMS 时间。

一个常被忽略的问题:摄像头安装高度和角度。数据集里的图片如果是车栽视角(1.5-2 米高度),部署在路侧杆件(6 米高度)时精度会明显下降。这时候不需要重新标注,用测试时增强(TTA)可以缓解:

yolo detect predict \ model=best.pt \ source=road_camera_01.jpg \ imgsz=1280 \ augment=True

augment=True会在推理时做多尺度、翻转和颜色微调再投票,能提升一点泛化,但速度会慢 2 倍,只建议在离线分析场景用。

5. 结冰路面的误判回收:类激活图验证技巧

5.1 提取难例样本构建硬负样本集

训练结束后,把验证集里被误判的图片单独抽出来,按“预测类别-真实类别”分组保存:

yolo detect val \ model=best.pt \ data=road_condition.yaml \ save_txt=True \ save_conf=True

然后写脚本对比预测结果和标注,把wrong_icy_as_rainy这类图片归档为硬负样本集。这些图片会用于:

  • 追加进训练集,重新训练一轮,通常能挽回 2-3 个点的结冰类 AP。
  • 作为摄像头点位调优的参考:如果某一个点位反复把结冰判成湿滑,说明该位置的摄像头白平衡有问题,需要在 ISP 端校准,而不是继续加大模型复杂度。

5.2 用类激活图快速定位“看错了什么”

不像通用目标检测可以画出 bbox 看位置,路面状态分类的激活区域是判断模型是否真正学到纹理特征的关键。用 Grad-CAM 生成激活热图:

import torch from ultralytics import YOLO from ultralytics.utils.torch_utils import model_info model = YOLO('best.pt') # 取 backbone 最后一层作为目标层 target_layer = model.model.model[-2] # 注册 forward hook 前向传播 activation = {} def hook(module, input, output): activation['value'] = output hook_handle = target_layer.register_forward_hook(hook)

单张图推理后,对识别为结冰的图片检查热图的高亮区域。两个结论性特征:

  • 高亮区域集中在路面中央且沿着车辆行驶方向分布,说明模型在看路面纹理,特征合理。
  • 高亮区域散落在路肩护栏、路牌或天空上,说明模型学到了背景线索而不是路面本身。这种模型换一个摄像头点位就会失效。

针对后一种情况,最简单的修补不是重新训练,而是检查训练集里结冰图片的拍摄背景是否单一。如果所有结冰图片都在同一个高架桥上拍的,模型大概率把桥护栏的形态当成了特征。做法是混入不同路段的结冰图片,哪怕只有几十张,也能有效打断背景相关的特征映射。

最后一招:把结冰类的分类阈值单独提高。YOLO 的 per-class 阈值调整在每个类别上独立生效,四分类场景下把 icy 的置信度阈值设为 0.35、其他类保持 0.25,能减少将湿滑误报为结冰的虚警数量,代价只是结冰类召回率下降 1-2 个点——在除雪调度场景里,漏报一次结冰的代价远大于多报一次。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询