ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

雨雪路面目标检测实战:基于YOLOv9的路况识别与训练调优

雨雪路面目标检测实战:基于YOLOv9的路况识别与训练调优 简介雨雪天气路面状况数据集面向自动驾驶、智能交通与路面养护场景下的路面状态识别任务覆盖结冰路面、雪地、下雨湿滑和干燥路面四类典型路况这类任务在冬季道路安全监测中尤为重要。资源包含646张原始路面图片与一一对应的txt标注文件另有1个yaml配置文件可直接配合YOLOv9进行模型训练与验证txt文件记录每张图片的目标框与类别信息yaml定义类别名称结构清晰便于二次标注或迁移学习。压缩包整体约26.89MB共1293个文件轻量易下载。目前已有860人学习使用适合需要快速构建路面分类检测模型的算法工程师、研究人员及高年级学生。数据图片保留真实拍摄场景能帮助模型在雨雪天气下保持稳定识别能力兼顾科研与落地需求。1. 雨雪天气路面状况数据集把“路况”从主观判断变成可训练的目标检测问题真正跑过路况识别项目的人会先被一件事噎住天气预报不等于路面状态摄像头看到的和气象数据常常对不上。这个名为“雨雪天气路面状况数据集”的包用原始图片和 yolov9 标注把路面状态拆成结冰、雪地、下雨湿滑、干燥四类解决的正是“眼前这段路到底危不危险”这个细粒度感知问题。适合做智能交通巡检、辅助驾驶、机场跑道除冰、道路养护系统的人。它不替代气象传感器而是让视觉模型学会“自己看路面”最少只要一台带摄像头的边缘设备。2. 拆解数据集原始图片配 yolov9 标签到底长什么样2.1 目录结构与标签文件先把 zip 里的家底盘清楚解压这种数据集包后最常见的是下面这种 YOLOv9 可直接识别的目录结构而不是 COCO 或 VOC 的 JSON/XML。如果你拿到手只有一个 images 文件夹没有 labels那就要先补标注转换这一步后面会讲。标准结构通常是weather_road/ ├── images/ │ ├── train/ │ │ ├── 0001_ice.jpg │ │ ├── 0002_snow.jpg │ │ └── ... │ └── val/ │ ├── 1001_wet.jpg │ └── ... ├── labels/ │ ├── train/ │ │ ├── 0001_ice.txt │ │ └── ... │ └── val/ │ └── ... └── classes.txtvalid 的 labels 文件名必须和 images 文件名一一对应只是后缀换成 .txt。先不要急着训练用几条命令检查这个数据集的“底子”包括图片数量、每个标签文件的格式、类别是否存在缺失# 统计训练集图片数量 ls images/train | wc -l # 看一个标签文件的完整内容 cat labels/train/0001_ice.txt # 统计训练集里每类目标的数量第 0 列是类别编号 awk {print $1} labels/train/*.txt | sort | uniq -c上面的awk命令会输出类似3 0、5 1这样的结果第一列是目标数量第二列是类别索引。如果你的 classes.txt 写的顺序是“ice, snow, wet, dry”那么类别 0 是结冰、1 是雪地、2 是湿滑、3 是干燥。这里最容易踩的第一个坑是不同数据集的类别排列不一样比如有的把干燥放在 0把结冰放在 3。后面训练配置的 names 列表必须和 classes.txt 严格一致否则模型学到的全部是错标签。原始图片这一点值得多说两句。很多天气数据集会把图片裁剪、压缩、加白色边距来“统一风格”这个包既然写明用原始图片通常意味着保留了拍摄时的 EXIF 信息、原始分辨率和自然光照。看起来是好事但要注意 YOLOv9 读取图片用的是 OpenCV而 OpenCV 的imread默认不处理 EXIF 旋转方向如果你的原始图片里有手机拍摄的竖屏照片模型训练时会读到旋转 90 度后的图像标签却还是按照原方向标的位置这会导致目标框整体偏移错位第 5 章会给出处理办法。2.2 把任意格式标注转成 yolov9 的归一化 txt一个可复用的脚本如果你的 zip 里不是 txt而是 LabelMe JSON、VOC XML 或 COCO JSON就需要转换。YOLOv9 的标注格式只有一行五列class_index x_center y_center width heightx_center、y_center、width、height 都是相对图片宽高的归一化数值范围是 0~1。下面这个脚本是把 COCO JSON 转成 YOLO txt 的常见做法关键点是坐标换算和跨图片数据合并import json import os def coco_to_yolov9(json_path, output_dir): with open(json_path, r) as f: data json.load(f) # 建立 COCO 图片 id 到文件名的映射 img_map {img[id]: img[file_name] for img in data[images]} # 把宽度高度也存下来后面归一化要用 size_map {img[id]: (img[width], img[height]) for img in data[images]} for ann in data[annotations]: img_id ann[image_id] if img_id not in img_map: continue w, h size_map[img_id] bbox ann[bbox] # COCO 格式是 [x, y, width, height] x, y, bw, bh bbox # 归一化中心坐标和宽高 x_center (x bw / 2) / w y_center (y bh / 2) / h box_w bw / w box_h bh / h # 注COCO 类别 id 是从 1 开始的YOLO 要求从 0 开始 cls_id ann[category_id] - 1 txt_path os.path.join(output_dir, img_map[img_id].replace(.jpg, .txt)) with open(txt_path, a) as f: f.write(f{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}\n) # 用法示例 # coco_to_yolov9(road_annotations.json, converted_labels)这个脚本要注意两点第一如果 COCO 类别 id 不是连续的从 1 到 4你需要额外维护一个类别重映射字典比如把category_id为 5、7、9 的类重新映射成 0、1、2第二标注框超出图片边界的情况不能简单 clamp如果框的 x_center 或 w 小于 0这一条直接丢弃否则训练时 loss 会跳变。YOLO 官方训练逻辑会在输入尺寸变化时自动做 letterbox如果你的标注已经超出边界letterbox 后越界部分被裁掉模型反而学了一个不完整的框。转换完成后再用第 2.1 节的awk命令统计一遍每类数量。如果某个类比如结冰只有几十个框后面训练时就要重点处理类别不平衡这一点在第 4 章会展开。3. 用 YOLOv9 跑通路面识别从 data.yaml 到训练命令3.1 写 data.yamlnames 顺序错一个数字整个模型白练YOLOv9 训练前要准备一个 data.yaml它告诉训练器数据放在哪、有几个类、类别名字是什么。下面是一个针对这个路面数据集的配置# weather_road.yaml train: /path/to/weather_road/images/train val: /path/to/weather_road/images/val nc: 4 names: 0: ice 1: snow 2: wet 3: drytrain 和 val 的路径建议写绝对路径。这里特别强调 names 的顺序它必须和你数据集的 classes.txt 顺序完全一致。如果数据集的 labels 里的类别索引 0 代表“雪地”而你 yaml 里 names[0] 写了“ice”训练时模型输出的 0 类头就会去拟合雪地的特征后面做推断时你拿到的“ice”其实指向的是雪地。验证方法很简单随机拿一个 label 文件对照它的文件名去原图里看一眼框的是不是冰面区域再用上面 yaml 的 names 顺序人工核对一遍。3.2 选模型尺寸yolov9-c 还是 yolov9-eYOLOv9 官方提供了多个尺寸的预训练权重路面状况检测这个任务里我一般会先用 yolov9-c 起训练。原因很直接这类数据集的样本量通常只有几千张个别类别的样本更少e 系列参数量大样本不够时容易过拟合在验证集上 mAP 虚高但换一批图片就失效。如果你的 GPU 显存充足24GB 以上且数据量超过 1 万张再考虑用 e 系列。另一个选择是用带 COCO 预训练权重的 checkpoint而不是从随机权重开始。雨雪路面和 COCO 里的汽车、街道区域不完全相同但低层纹理特征边缘、颜色、反光可以迁移。常见做法是下载官方的yolov9-c.pt放目录后训练脚本会自动加载它的骨干参数。3.3 训练启动命令与参数这组配置明显更稳准备工作就绪后训练命令大致如下cd yolov9 python train.py \ --data /path/to/weather_road.yaml \ --weights yolov9-c.pt \ --img 640 \ --batch 16 \ --epochs 120 \ --device 0 \ --project runs/road_train \ --name v1这段命令里几个参数值得逐个说清楚--img 640不是所有原始图片都是 640 分辨率YOLOv9 会自动做 letterbox 缩放。原始图片如果有大量畸变比如广角镜头建议先用 320 训练 10 个 epoch 看 loss 是否下降再切到 640 微调能节省不少时间。--batch 16路面图片尺寸大显存不够时优先保证 batch 在 8~16 之间。batch 太小会让 BN 层不稳定表现是 loss 震荡、验证集 mAP 每轮都上下跳。--epochs 120不要一看验证集 mAP 到 0.9 就立刻停下。雨雪路面的类别间纹理差异极小多训 20~30 个 epoch 往往能把结冰的召回率拉高 3~5 个百分点。--device 0单卡训练。没有 GPU 就改成--device cpu但速度会慢十倍以上建议至少用一张消费级显卡。还有一组隐藏参数是 YOLOv9 读取的超参数文件data/hyps/hyp.scratch-high.yaml。你可以覆盖其中两个和天气场景强相关的参数mosaic默认是 1.0如果你的数据集里结冰样本很少建议前 20 个 epoch 把 mosaic 设成 0.5避免合成图里四种路面混杂在一起导致模型更抓不到冰面的细纹理hsv_h、hsv_s这些颜色增强参数不用动但需要注意原始图片的颜色偏差——如果图片来自不同摄像头白平衡不一致模型会学成“偏蓝的就是结冰”。这种场景我会在训练前用 Python 做一次全局颜色校正而不是依赖训练时增强。训练过程中查看损耗变化可以直接看终端输出里的obj_loss和cls_loss。如果cls_loss在 20 个 epoch 后仍然高于 0.05先检查标签是否错位上一章提到的方法再检查是否某些类完全没出现在验证集里。正常情况下训练到后半程cls_loss应该平稳降到 0.01 以下。4. 参数与效果验证mAP 达标之后还要看哪三个指标4.1 mAP 不是天花板混淆矩阵才是路面识别的照妖镜训练完成后YOLOv9 会在runs/road_train/v1/下生成一堆结果文件其中results.csv记录着每一轮的metrics/mAP_0.5、metrics/mAP_0.5:0.95、precision、recall。但做路面识别的人不应该只盯 mAP因为四类目标里干燥路面样本通常最多只要把干燥类识别好mAP 很难难看真正要命的是结冰和湿滑这两类样本少、外观相近mAP 高不高往往被干燥类拉平了。我常用的验证方法是单独看混淆矩阵。YOLOv9 训练结束会在runs/road_train/v1/confusion_matrix.png输出你可以核对三个指标结冰类被误判成干燥类的比例如果超过 20%说明模型没学到冰面反光的纹理只学了颜色灰白。干燥类被误判成湿滑的比例常见原因是有阴影的干燥路面色调偏深和湿滑接近。背景被误判成结冰的比例这个如果很高说明是边界框太大导致背景混入过多。如果你想量化这些比例而不是肉眼看图可以用一个小脚本读labels和预测结果做矩阵统计import numpy as np from collections import defaultdict # preds: list of [img_id, class_id, conf, x, y, w, h] # labels: dict {img_id: [(cls, x, y, w, h), ...]} confusion np.zeros((4, 5), dtypeint) # 4类 vs 4类背景 for img_id, preds_in_img in preds.items(): labels_in_img labels[img_id] for p_cls, p_conf, *bbox in preds_in_img: if p_conf 0.25: continue best_iou 0 best_cls -1 for l_cls, *lbox in labels_in_img: iou compute_iou(bbox, lbox) if iou best_iou: best_iou iou best_cls l_cls if best_iou 0.5: confusion[p_cls, best_cls] 1 else: confusion[p_cls, 4] 1 # 背景误检这里compute_iou需要你自己写核心逻辑是取预测框和真值框的交并比。阈值设 0.5 是 COCO 的默认值路面检测场景我一般再算一遍 0.3 的版本因为冰面的边界本身就不清晰人工标注的框也可能偏大或偏小0.5 的严格 IOU 会低估模型对结冰类的召回。4.2 类别不平衡的二层处理样本加权 分阶段增强统计完标签你会发现常见场景分布非常不均匀干燥路面可能几千个框结冰路面两三百个框。直接用原始数据训练模型会把结冰类当成“小概率事件”表现为参数更新时梯度被干燥类主导。有两种常见做法我建议一起用而不是二选一。第一层是给 loss 里的类别项做加权。YOLOv9 的 loss 函数里类别损失默认使用 BCE每个类权重相等。你可以在训练前临时改一下损失计算给结冰类更高的权重。简单做法是修改数据集配置文件不用改代码——在data.yaml里加一个cls_weights字段自己写加载逻辑或者更粗暴地在训练脚本里给cls_loss乘以权重系数。实际工程里我更喜欢用第二个方法。第二种是分阶段增强前 40 个 epoch 把 mosaic 和 mixup 都关了让模型先学到真实的冰面、雪地纹理40 个 epoch 后再打开。因为 mosaic 会把四张不同路面的图拼在一起对于结冰这种依赖局部反光纹理的目标合成图会破坏真实的反射边界模型反而学不到关键特征。而到了训练后期模型已经有一定基础再把增强打开可以提升泛化能力让模型适应不同光照。关键在“验证时也一定要按类别分层评估”。YOLOv9 的 val.py 会输出整体 mAP但你要做的是把验证集的标签按类别拆成四份分别跑出一份 per-class precision 和 recall。这一步能直接判断“训练到底救了谁”。如果经过加权和分阶段增强结冰类的 recall 从 60% 升到 80% 以上而干燥类掉到 90% 以下还需要微调权重或者补充样本。在样本极其匮乏的极端情况我还会补充合成数据——用 Blender 做冰面纹理渲染但这属于进阶话题后面会简单带一下。5. 雨雪天气路面数据集训练的四个典型踩坑现场与排查5.1 验证集 mAP 很高但实际视频里一帧跳、一帧稳现象训练时验证集 mAP0.5 到了 0.92模型看起来很好。部署到卡口后晚上或者雨夜连续视频里模型前一帧识别出“湿滑”后一帧却变成“干燥”再后面一帧又变成“结冰”完全没有稳定输出。原因单个帧之间光照变化和镜头残影会导致目标特征剧烈抖动而 mAP 是逐帧独立计算的平均值它不包含时间连续性。更关键的可能是验证集本身是从训练数据里随机切出来的没按“时间段/天气”分层导致模型忽略了时序上下文。解决把验证集按照“白天干燥、白天雨、夜晚雨、雪天、冰面”五类分开统计而不是简单五五开切分。查看动态坏帧时不要用 mAP 说话直接用一段视频做“连续 30 帧预测结果稳定性测试”。如果连续预测结果来回跳方案是给输出加一个长度为 5 帧的majority vote平滑等价于常见做法用滑动窗口统计最近 5 帧出现最多的类别作为最终结果至少能让部署环境里的输出看起来“符合常识”。5.2 结冰目标框比人工标注大一圈甚至把整条路框进去现象模型把结冰区域的预测框画得很大边界延伸到路边草地、护栏导致和真实目标 IOU 只有 0.3mAP 和 recall 都被拉低。原因结冰路面的视觉特征是一片连续的反光区域没有明确的边缘。人工标注的时候不同标注员的标准也不统一——有的框只包含明显的冰层有的箭头把整条车道都框了进去。解决打开标注文件人工检查极值。用 Python 脚本筛出宽高比大于 5 或者面积超过图片 80% 的框这类框大概率有问题。处理方式有两种一是重标这些过大框把边界缩到反光区域的边缘二是训练时把 IOU 判别阈值从 0.5 降到 0.3让模型对“边界模糊的目标”更宽容。这两种我一般会同时做但重标数据优先级更高因为模型输出的框大小直接受真值框大小影响。5.3 训练 loss 一直不降验证集召回率恒为 0现象训练时box_loss下降但cls_loss不降而且验证集中结冰类 recall 一直为零。查看预测的类别分布发现模型把所有目标都输出为干燥类。原因除了类别不平衡最常见的是data.yaml 的 names 顺序和 classes.txt 不一致或者标签文件里的类别索引超出了nc范围。如果一个标签里出现了4YOLOv9 在读取时会直接丢弃这一行导致某些类的样本数为 0。解决先把所有标签文件过一遍找出超出 0~3 范围的无效行# 输出所有包含了非 0-3 类别的标签文件 awk $1 0 || $1 3 {print FILENAME: $0} labels/**/*.txt再把命中的文件全部打开检查修复类别重映射。另一个常见诱发点是用别人转换好的标签时坐标x_center或width出现负数或大于 1这些同样会导致训练时 loss 计算异常。在标签转换脚本里加一个“范围检查”和“丢弃”逻辑是值得养成的好习惯。5.4 EXIF 方向导致标注错位图片横着、竖着标签对不上现象训练时 loss 急速掉到 0.01 以下但验证集 mAP 也是 0.01画图看标注时发现有的图片里框在目标旁边 90 度方向完全错位。原因这个数据集的图片是“原始图片”手机或部分相机拍摄的照片会把“拍摄时是竖屏”的信息存在 EXIF 里图片本身像素数据还是横向存储。OpenCV 读取时不处理 EXIF 方向导致 YOLOv9 看到的是横图而标注工具看到的或标注时的原始方向是竖图像素坐标系变换之后标签就错了。解决训练前先做一次统一的图片方向矫正用 Python 把 EXIF 的方向信息写入到像素数据里再覆盖保存from PIL import Image, ImageOps, ExifTags def fix_exif_orientation(image_path): img Image.open(image_path) exif img.getexif() if exif is None: return orientation exif.get(274) # 0x0112 Orientation if orientation 3: img img.rotate(180, expandTrue) elif orientation 6: img img.rotate(270, expandTrue) elif orientation 8: img img.rotate(90, expandTrue) img.save(image_path, exifexif) # 批量处理 images 目录 # for p in image_paths: # fix_exif_orientation(p)处理完之后再肉眼抽查 10 张原图与标签叠加确认坐标一致再进训练。这个坑只出现在“原始图片”数据集中反而提醒我们预处理步骤不能省原始图片虽好但元数据会咬人。6. 让模型真正能在路上稳定干活帧间平滑与按季节迭代训练出的单帧模型即使在验证集上表现不错离“靠谱地部署”还差一步。尤其是结冰和湿滑这类视觉差异极小的类别单看某一帧确实容易误判。一个低成本但有效的做法是做预测结果的时序后处理我一般用长度为 5 的滑窗取众数而不是平均值因为类别是离散的取平均会得到无意义的“category 2.3”。核心逻辑如下from collections import Counter class FrameVoteSmoother: def __init__(self, window_size5): self.window [] self.window_size window_size def predict(self, detections): # detections: [{ class_id: int, conf: float, ... }, ...] if detections: dominant_class Counter(d[class_id] for d in detections).most_common(1)[0][0] self.window.append(dominant_class) if len(self.window) self.window_size: self.window.pop(0) if not self.window: return None return Counter(self.window).most_common(1)[0][0]这个类可以在视频推理循环里逐帧调用。注意它只解决“类别名称抖动”不解决“目标框抖动”。框的抖动我见过很多次常见做法是加一个简单的 EMA指数移动平均平滑 bbox 中心点坐标权重取 0.6 历史、0.4 当前帧。不要把平滑做得太过否则目标快速移动时框会拖尾。另外路面数据集的季节性很强。夏天训练好的模型到了冬天见雪就懵这不是模型问题而是数据分布变了。因此我最后的一个习惯是每个季度或每次极端天气出现后重新收集 50~200 张当前环境的图片用半自动脚本把新图片加入训练集做 20 个 epoch 的增量训练。增量训练时学习率要降到原训练的三分之一并固定骨干网络的前几层防止模型把新学到的特征盖掉原来的能力。这是用最少的采集成本守住模型冬天性能的后悔药踩过的坑换来的经验。希望帮到你。本文还有配套的精品资源点击获取
返回列表