雨雪路况COCO数据集转YOLOv8训练全流程实战
简介一份面向自动驾驶、智能交通与计算机视觉研究者的雨雪天气路面状况图像数据集专注于结冰、雪地、下雨湿滑及干燥四类路面的识别与分类任务。包内共651个文件其中646张原始jpg图片覆盖不同光线与天气下的道路实景3个json文件为COCO格式标注信息可直接用于目标检测、实例分割等模型训练2个txt文件提供类别或说明性参考整体压缩包仅26.92MB轻量易用。目前已有1010人学习下载适合作为路面状态感知、车辆安全预警等课题的算法验证与训练数据补充。原始图像与标准COCO标注配套省去自行采集标注的繁琐环节便于快速开展模型迭代与效果评估尤其适合高校实验室和初创团队在资源有限条件下进行路面状况识别相关研究。1. 雨雪天气路面状况数据集结冰、雪地、湿滑、干燥四类为什么用 COCO 标记而不是分类标签我最早看到这个数据集标题时的第一反应是这不就是图像分类吗给整张图打个“结冰路面”或“雪地”的标签就够了为什么要用 COCO 标记这种检测标注格式后来在自己项目里试了一轮才发现真实道路场景根本不像比赛数据集那么干净——同一条路面上可能左侧路面干燥、右侧有积水放大看还有积雪和冰面混杂。整图分类在这种场景下几乎必翻车因为它没办法告诉你“哪一块区域是湿滑路面”。这就是这套雨雪天气路面状况数据集存在的价值用矩形框把每张原始图片里的结冰路面、雪地、下雨湿滑、干燥路面分别标出来模型训练完可以直接输出目标的类别和位置喂给自动驾驶感知、道路养护巡检或者安防监控系统做进一步决策。适合谁用做过检测任务、想拿真实野外数据做鲁棒性评估的算法工程师以及做交通场景视觉感知的实习生和研究生。如果你是第一次碰 COCO 标记格式这套数据也是个不错的练手样本。2. 先拆数据再看模型路面状况数据集的图片组织与 COCO 标注结构2.1 路面识别为什么走检测路线冰面不是全局属性是局部区域属性结冰路面、雪地、下雨湿滑、干燥路面这四类状态在真实图片里往往同时存在。我以前做过一个道路养护项目甲方给的需求是“识别这段路是否结冰”直接套分类模型现场效果很差。问题出在哪一辆车在干燥路面行驶前方几百米有一小段桥面结冰整张图里冰面只占几个像素块分类标签打“结冰”会导致模型学习不到位置信息漏报严重打“干燥”又等于吞掉了小目标。检测思路把问题拆成“哪里是什么状态”框住冰面区域之后还能联动车辆控制策略比如只对框内区域限速。这套数据集用 COCO 标记来标说明作者从一开始就在引导你往检测方向走而不是做分类。另一个角度看四类路面状态在图像特征上高度相似——雪地是白的结冰也反光是白的阴天湿滑路面和干燥路面在灰度上几乎没区别——这类细粒度外观差异用全局特征特别容易混局部框内特征反而能逼模型去学纹理、反光和阴影这些小尺度线索。2.2 zip 包里的原始图片与 COCO 标记文件先解压、先列清单再谈训练数据以 zip 包形式分发常见做法是直接解压到项目目录。拿到压缩包之后我一般不急着解压全部文件而是先看压缩包里有哪几层目录结构、图片是什么格式、标注文件是单个 JSON 还是按图片分多个 JSON。用命令行的 unzip -l 扫一眼能提前发现目录嵌套、文件名乱码、伪加密这类问题。unzip -l 雨雪天气路面状况数据集.zip | head -50这段命令只列压缩包内容不真正展开。看输出结果时重点关注三件事一是图片目录和标注 JSON 是不是在同一个根目录下训练脚本后面要拼绝对路径二是图片后缀是不是统一常见是 .jpg偶尔夹杂 .png不统一会在数据加载时报找不到文件三是标注 JSON 的文件名是不是和图片目录名一一对应。这步排查能省下后面大量无头绪的报错时间。确认结构没问题后再整体解压我习惯解压到一个不带中文和空格的路径下比如 /data/road_condition避免后面训练框架对路径解析出幺蛾子。提示解压阶段如果工具提示“输入密码”但又没有密码大概率不是发布方加密了内容而是遇到了 zip 伪加密——文件头标志位被改动实际数据并没有加密。换 7-Zip 打开多数伪加密包能直接解如果确实需要密码按数据使用规范找发布方要不要绕过授权。2.3 COCO 标记的核心表结构images、categories、annotations 怎么读这套数据集的标注文件如果遵循 COCO 标准内部就是一个 JSON 对象包含三个强相关的数组。我拆解一下{ images: [ { id: 1, file_name: 20240115_103223.jpg, width: 1920, height: 1080 } ], categories: [ {id: 1, name: 结冰路面}, {id: 2, name: 雪地}, {id: 3, name: 下雨湿滑}, {id: 4, name: 干燥路面} ], annotations: [ { id: 10001, image_id: 1, category_id: 1, bbox: [1280, 422, 360, 210], area: 75600, iscrowd: 0 } ] }images 表记录每张原始图片的文件名和宽高这是后续做坐标归一化时唯一可靠依据千万不能自己去读图片尺寸替代有些图片的 EXIF 旋转信息会导致宽高和像素矩阵不一致。categories 表里类别 id 从 1 开始还是从 0 开始不同标注工具习惯不同后面转换成 YOLO 标签时必须以这份文件里的 id 为准不能想当然减 1。annotations 表里 bbox 是左上角 x、左上角 y、框宽 w、框高 h单位是像素area 字段是标注工具按多边形或矩形容器算出来的面积模型训练不用它做监督但可以用它筛极端小框。这里要特别提醒COCO 的 bbox 是 [x, y, w, h]不是 [x1, y1, x2, y2]转 YOLO 或其他格式时极易踩坑后文会专门写这条。3. 把 COCO 标记转成 YOLO 格式转换脚本、坐标归一化与数据集切分3.1 为什么要转格式YOLOv8 的默认输入不是 COCO json有人会问YOLOv8 内置支持 COCO 格式直接训练不就行了支持 COCO 是指框架内置了 COCO 数据集的类别配置不代表你放一个自定义的 json 标注文件进去就能跑。如果你用的是 ultralytics 库自定义数据集最稳定的输入形态是每张图对应一个同名 txt 标签文件每行写“类别id x_center y_center w h”坐标是相对图片宽高的归一化浮点数。这套路面数据集给的是 COCO 标记所以第一步是把 json 转成 YOLO 需要的 txt。还有一个隐含好处转换过程可以顺手做数据清洗。原始标注里可能有空框、越界框、类别 id 漂移全在转换脚本里过滤掉而不是等模型训练到一半才报错。3.2 转换脚本从 json 读取、归一化、写出逐图 txt 标签下面这个脚本是我在类似数据集上反复改过几轮的版本直接可复用。你只需要改三个变量json 路径、图片根目录、输出目录。import json import os from pathlib import Path def coco_to_yolo(coco_path, img_root, label_root): with open(coco_path, r, encodingutf-8) as f: coco json.load(f) # 建立 image_id - 图片信息的映射 img_info {img[id]: img for img in coco[images]} # 建立原 category_id - 新 id 的映射 # COCO categories 的 id 可能从1开始YOLO 要求从0开始连续编号 cat_ids [cat[id] for cat in coco[categories]] cat_map {old_id: new_id for new_id, old_id in enumerate(cat_ids)} label_root Path(label_root) label_root.mkdir(parentsTrue, exist_okTrue) # 按 image_id 聚合同一张图的所有标注框 ann_by_img {} for ann in coco[annotations]: ann_by_img.setdefault(ann[image_id], []).append(ann) for img_id, anns in ann_by_img.items(): info img_info[img_id] img_w, img_h info[width], info[height] if img_w 0 or img_h 0: # 部分标注文件里宽高为0这图没法归一化直接跳过 continue # 文件名去后缀保证 txt 和图片同名 stem Path(info[file_name]).stem txt_path label_root / f{stem}.txt lines [] for ann in anns: # 跳过 crowd 类标注这类通常是人群或遮挡区域不适合当检测目标 if ann.get(iscrowd, 0): continue cat_new_id cat_map[ann[category_id]] x, y, w, h ann[bbox] # 过滤掉宽高为负或面积太小的无效框 if w 1 or h 1: continue # COCO bbox 是左上角坐标转成中心点坐标并归一化 cx (x w / 2) / img_w cy (y h / 2) / img_h nw w / img_w nh h / img_h # 归一化之后越界通常是标注框边缘出图了做截断 cx min(max(cx, 0.0), 1.0) cy min(max(cy, 0.0), 1.0) nw min(nw, 1.0) nh min(nh, 1.0) lines.append(f{cat_new_id} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}) # 有些图确实没标框输出空 txt训练时会自动忽略 txt_path.write_text(\n.join(lines), encodingutf-8) print(f转换完成共处理 {len(ann_by_img)} 张图片)逻辑说明分三段。第一类别 id 必须重新映射。COCO 的 categories 里 id 通常从 1 开始但 YOLO 的标签第一列必须从 0 开始递增直接用原 id 会导致类别错位到不存在的编号上训练时你会看到 loss 正常但 mAP 全是 0。第二归一化的基准必须用 json 里 images 字段记录的宽高而不是自己 cv2.imread 出来的尺寸。很多手机或行车记录仪照片带 EXIF 旋转信息cv2 默认不修正旋向读出来的宽高可能和标注工具基于的宽高差 90 度坐标全错。第三min/max 截断操作是为了处理标注框边缘超出图片边界的样本但只适合框超出一两个像素的情况如果大量框偏移几十个像素多半是标注基准出错截断只会掩盖问题要回去查 json。参数层面需要说明coco_path 指 annotator 导出的 json 绝对路径img_root 是图片所在目录脚本本身不读图只用于后续组织路径label_root 是输出 txt 的目录。如果你确定这张数据集的 categories id 本身就是 0 开头把 enumerate 改成直接取原值即可但保留重映射更安全。3.3 数据集切分随机种子固定验证集按场景抽而不是按文件抽转换完标签后下一步是把图片和 txt 按比例切成分训练集和验证集。做法是把图片文件列表打乱按 8:1:1 或者 9:1 切。这里有个关键点很多路面数据是从连续视频抽帧得到的同一段路的相邻帧极度相似随机打乱后相似帧会同时出现在训练集和验证集导致验证指标虚高部署到新路段马上露馅。更稳妥的做法是按文件名前缀或拍摄时间分组比如文件名带日期时间的把同一天或同一路段的数据整体分到一边。切分脚本如下import os import random import shutil from pathlib import Path random.seed(42) img_root Path(/data/road_condition/images) label_root Path(/data/road_condition/labels) train_img_dir Path(/data/road_condition/train/images) val_img_dir Path(/data/road_condition/val/images) train_lbl_dir Path(/data/road_condition/train/labels) val_lbl_dir Path(/data/road_condition/val/labels) for d in [train_img_dir, val_img_dir, train_lbl_dir, val_lbl_dir]: d.mkdir(parentsTrue, exist_okTrue) imgs sorted(img_root.glob(*.jpg)) sorted(img_root.glob(*.png)) random.shuffle(imgs) val_count max(1, int(len(imgs) * 0.2)) val_imgs imgs[:val_count] train_imgs imgs[val_count:] def move_pair(img_path, img_dst, lbl_dst): shutil.copy(img_path, img_dst / img_path.name) lbl_path label_root / f{img_path.stem}.txt if lbl_path.exists(): shutil.copy(lbl_path, lbl_dst / lbl_path.name) for img in train_imgs: move_pair(img, train_img_dir, train_lbl_dir) for img in val_imgs: move_pair(img, val_img_dir, val_lbl_dir)这段脚本把原始图片和标签一起复制到 train/val 目录下不改动原始数据集。随机种子固定成 42保证每次运行切分结果一致复现实验不玄学。复制而不是移动的好处是切分错了还能重新来算是一个后悔药。如果你发现验证集里某个类别完全没出现不要靠加大验证集比例硬撑得返回去看数据采集分布这类野外数据集的类别分布天然不均后文避坑章节会细说。3.4 转换完后的自检空标签、类别数和坐标范围一次查清转换和切分完成之后先别急着开训练用一段自检脚本把整个标签目录扫一遍。这个步骤我每次都会做至少能拦下三成训练事故。import os from pathlib import Path label_root Path(/data/road_condition/train/labels) class_ids set() empty_files [] bad_lines [] for txt_path in label_root.glob(*.txt): lines txt_path.read_text(encodingutf-8).strip().splitlines() if not lines: empty_files.append(txt_path.name) continue for line in lines: parts line.strip().split() if len(parts) ! 5: bad_lines.append((txt_path.name, line)) continue cls int(parts[0]) coords [float(v) for v in parts[1:]] if cls 0: bad_lines.append((txt_path.name, line)) if not all(0.0 v 1.0 for v in coords): bad_lines.append((txt_path.name, line)) class_ids.add(cls) print(类别总数:, len(class_ids)) print(空标签文件数:, len(empty_files)) print(异常行数:, len(bad_lines)) if empty_files: print(空标签示例:, empty_files[:3]) if bad_lines: print(异常行示例:, bad_lines[:3])主要查三类问题类别 id 是否只有预期的那几个比如预期四类但输出发现类别 id 跳到 108说明 json 里混入了无关标注空标签文件数量是否正常雪天远场景大量小目标被人工漏标空文件比例超过 10% 要提醒自己模型漏检基线会偏高归一化坐标是否落在 [0,1] 区间外出现 1.3 这种值就是转写时除错了尺寸。这三种问题如果发生在训练中途报错信息往往晦涩难懂提前用脚本扫一遍是最划算的投入。4. 用 YOLOv8 训练自己的路面状况数据集从 data.yaml 到 mAP4.1 按 COCO2017 数据集结构把数据摆好不然后面全是路径报错YOLOv8 的 ultralytics 库默认期望数据目录按照 images/train、images/val、labels/train、labels/val 这种结构组织和 COCO2017 数据集的组织逻辑一脉相承。你不需要完全照搬 COCO 的 json 文件组织方式但目录树要保持这个骨架。常见做法是建一个项目根目录里面只放 images 和 labels 两个兄弟目录名字必须叫 labels不能叫 annotationsultralytics 加载时写死了目录名。结构如下/data/road_condition/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yaml如果你的原始 zip 解压后目录层级是 annotations/xxx.json 和 source_images/xxx.jpg那就先按第 3 章的脚本把 txt 标签生成好再手动摆成上面这个骨架。需要强调的是YOLO 训练时通过图片路径推导标签路径——它拿到 images/train/a.jpg就去 labels/train/a.txt 找标签两个目录的名字必须严格对应多一个字符都不行。4.2 写 data.yaml类别清单、路径配置与三个冷启动检查data.yaml 是训练入口内容如下path: /data/road_condition train: images/train val: images/val names: 0: 结冰路面 1: 雪地 2: 下雨湿滑 3: 干燥路面这里的关键点有三个。第一path 字段建议写绝对路径。相对路径在本地跑没问题换机器或者换用户之后容易找不到数据集报 FileNotFoundError 时你会浪费半小时排查。第二names 的编号顺序必须和标签 txt 里第一列的类别 id 完全一致。比如第 3 章转换脚本里如果用了 sort categories 的方式顺序可能和原始 json 里 categories 表的排列一致但换另一种排序顺序就会错位。第三路径字段 train 和 val 写的是相对 path 的相对路径不要在中间加斜杠或加点号ultralytics 对路径拼接比较敏感。提示类别名可以用中文。YOLOv8 训练和推理都能处理中文类别名最后画框显示中文标签没问题。如果在意导出到 TensorRT 等部署框架时的兼容性可以改成拼音或英文比如 icy、snow、wet、dry。4.3 训练命令别一上来就用最大模型先把小模型当冒烟测试环境安装不多说pip install ultralytics 即可。训练命令推荐分两步走先小模型跑十几个 epoch 验证数据链路再上正式模型全量训练。yolo detect train \ modelyolov8s.pt \ datadata.yaml \ epochs15 \ imgsz640 \ batch16 \ projectroad_runs \ namesmoke_test \ workers4冒烟测试阶段只关注一件事训练能不能正常开始loss 有没有下降趋势。epochs 设 15 就够imgsz 用 640和正式训练一致避免验证时分辨率切换带来的指标误差。batch 大小看显存8GB 显存跑 16 会爆显存就降到 8。workers 是数据加载线程数Windows 上设 4 以上偶尔会报 DataLoader worker 相关错误降到 2 能规避。冒烟测试通过后再跑正式训练yolo detect train \ modelyolov8m.pt \ datadata.yaml \ epochs100 \ imgsz640 \ batch16 \ optimizerAdamW \ lr00.001 \ patience15 \ projectroad_runs \ nameroad_v1这里我通常不选 yolov8n 或 yolov8s 当正式模型路面四分类的框内特征差异偏细微小模型参数量不够对结冰路面和湿滑路面的区分能力明显偏弱。yolov8m 是性价比比较高的档位精度比 s 高一截推理速度在边缘设备上也能接受。optimizer 换成 AdamWlr0 降到 0.001是针对中小规模自定义数据集的稳妥配置SGD 在大数据集上表现好但几千张图片的小数据集用 SGD 收敛慢还容易在局部震荡。patience 设 15训练 15 个 epoch 没有验证集提升就自动停省时间。4.4 验证指标要拆到每个类别看结冰路面的漏检和干燥路面的误检不是一回事训练结束看一眼总 mAP好看不代表能上线。YOLOv8 训练结束后会在 project 目录下生成 results.csv 和混淆矩阵图必须打开逐类看。这套数据集的四类里干燥路面样本最多模型对它的 mAP 通常会很高掩盖结冰路面的低召回。结冰路面的误报来源通常是雪地——两个类别颜色相近纹理不同模型容易在雪地边缘切出一块冰面框。下雨湿滑路面的特征是反光但夜里路灯反光和湿滑反光在视觉上几乎一致所以这类样本的框经常把路灯附近的干燥路面一起圈进去。看混淆矩阵时重点盯 diagonal 之外的数值如果结冰路面被误分到雪地的比例超过 15%说明模型学到的是“亮白色雪地”不是真正的冰面纹理。5. 路面数据集实战的常见问题和排查五个翻车现场5.1 zip 伪加密导致解压卡死换 7-Zip 解决现象双击 zip 后提示输入密码输入空密码或任何密码都报错用 Python zipfile 模块解压时抛 RuntimeError说文件加密。原因发布方打包时用了某些压缩工具或者文件在传输过程中被第三方工具改动加密标志位形成 zip 伪加密。解决换用 7-Zip 直接解压多数伪加密包可以无视标志位直接展开部分伪加密发生在单个文件上解压报错的文件单独用 7-Zip 提取。排查过程用 unzip -l 看输出里有没有 perms 字段带l标记有则疑似伪加密。正规做法仍然是找发布方确认而不是动歪心思破解。5.2 json 里的宽高和实际图片不一致坐标整体错位现象标签框画出来了但框的位置集体偏右或偏下有些框完全落在地面以下。原因标注时用的预览图被裁剪过或缩放过于千分比但 json 里没有同步更新宽高还有一种是 EXIF 旋转导致图片实际像素矩阵是竖向标注软件自动旋正了但 json 记录的 width/height 没有被刷新。解决转换脚本不用 json 的宽高改用 cv2.imread 实际读取图片 shape 作为归一化分母。这个方案能规避多数宽高不一致问题代价是速度慢一点但正确性优先。5.3 雪地类别样本太少模型直接忽略这个类现象训练 loss 正常下降但验证集里雪地类别的 recall 是 0输出结果里完全没有雪地的框。原因数据采集集中于晴天和雨天雪天样本占比可能只有百分之几。检测框架对极不均衡类别默认不做什么特殊处理少样本类学不动。解决第一个手段是给少样本类加 loss 权重YOLOv8 里通过 class weights 实现给雪地设 2.0 或更高第二个手段是做复制粘贴增强把雪地框内的区域裁出来随机贴到干燥路面的背景图上生成新训练样本第三个手段是干脆接受该类别当前数据量的精度上限在部署端用规则兜底比如气温低于 0 度且检测到反光区域就告警。5.4 COCO json 里有 iscrowd1 的标注转换时没过滤导致框形状异常现象训练中断报错说 polygon 点集无法解析或 bbox 宽高为负。原因COCO 标注中 iscrowd1 的标注通常是分割mask而非矩形框或者标注软件导出的 bbox 字段为空字符串。解决转换脚本里 already 写了if ann.get(iscrowd, 0): continue如果你是从网上下载的转换工具很多老脚本不过滤 crowd 项要在读取 annotation 对象后立刻判断而不是等写入标签时报错再回头查。另一个相关情况是 bbox 字段存在但内容是字符串 nullfloat() 转换时抛 ValueError可以在解析前加一个 isinstance 判断。5.5 转换脚本重跑后类别 id 顺序变了旧训练白跑现象第一次转换后训练到一半发现 json 里 categories 表和预测结果顺序对不上重新生成标签再训练mAP 反而不如之前。原因COCO json 里的 categories 数组顺序可能不稳定有些标注工具导出时按名称排序有些按 id 排序两次转换脚本如果用了不同的排序规则类别 id 映射全变之前训练的权重作废。解决转换脚本里先把 categories 按 id 排好序再建映射并且把映射表打印出来人工确认一次。我习惯把映射表存成类目清单文件每次训练前对比一遍防止改过 json 之后悄悄换顺序。6. 进阶验证与部署可视化脚本、混淆矩阵和四类状态的落地判断6.1 把标注框和预测框画回原图肉眼比数值更早发现问题训练完的 mAP 再高也要抽一批 val 图把框画出来看。这里用一段轻量脚本同时画原图标注框和模型预测框对比观察。import cv2 from ultralytics import YOLO model YOLO(road_runs/road_v1/weights/best.pt) img cv2.imread(/data/road_condition/images/val/snow_road_023.jpg) results model(img, conf0.25, iou0.5) for box in results[0].boxes: x1, y1, x2, y2 box.xyxy[0].tolist() cls int(box.cls[0]) label f{model.names[cls]} {box.conf[0]:.2f} cv2.rectangle(img, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) cv2.putText(img, label, (int(x1), int(y1) - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) cv2.imwrite(check_snow_road.jpg, img)conf 设 0.25 比训练时默认的 0.5 低一点方便观察漏检目标是否真的没检出来还是检出来了但被置信度阈值滤掉了。这个脚本的价值在于区分两类问题完全没框说明模型没学到该类特征有框但置信度低说明该调阈值而不是回炉重训。6.2 用混淆矩阵挑出类间混淆再决定要不要做两阶段模型打开训练输出目录里的 confusion_matrix.png如果结冰路面和雪地之间的混淆明显先不要急着堆数据。常见做法是先看看是不是标注本身有问题——标注员对“积雪覆盖路面”和“冰雪混合路面”的边界判断也会不一致模型学到的混淆可能正是标注者的混淆。对这类问题我一般加一个二阶段判断检测网络只负责输出“有路面异常”的框再框内区域用一个轻量级分类网络细化到具体类别。这个做法在样本少的时候比单检测模型更稳。6.3 把四类预测结果接进下游逻辑不是所有湿滑框都需要紧急制动最后聊部署层。直接拿检测框的类别名去驱动车辆控制是危险的因为雨天湿滑和积水深浅是连续量不是离散标签。我在路面预警项目里的做法是检测模型输出候选框和类别后再用框内图像统计平均灰度差和纹理能量分数。干燥路面框内纹理差异大结冰和湿滑路面纹理能量明显偏低结合这个分数给输出加一个“置信度修正”再送进下游告警阈值判断。这么做的好处是不用改检测模型只动后处理逻辑现场调参也很快。这套数据的实践让我养成了一个习惯标注格式是 COCO 还是 YOLO 都不是重点重点是你对每张原始图片的缺陷和标注噪声有没有体感最好的训练集永远是那些能让你在写转换脚本时就发现标注异常的样本。希望这些记录帮到你少走一段我走过的弯路。本文还有配套的精品资源点击获取