简介这份道路结冰数据集面向计算机视觉与深度学习方向的算法开发者、交通智能化研究人员及目标检测学习者聚焦路面结冰状态的自动识别问题。数据集共1527张图像标注分为clear-road与ice-road两类同时提供Pascal VOC格式的XML文件与YOLO格式的TXT标签并附带data.yaml配置文件方便直接接入主流检测框架训练。压缩包内文件总数2000个以1527个xml标注文件和473个txt标签文件为主整体约98.96MB图像样本为jpg格式目录划分清晰。该资源可应用于高速公路结冰预警、城市道路实时监测以及桥梁、隧道出入口等特殊路段的定制化检测帮助读者快速搭建训练与验证流程省去从零标注的成本。目前已有513人学习下载适合需要快速验证结冰检测算法或开展相关课程实践的用户参考使用。1. 道路结冰数据集实战1527 张 YOLO/VOC 标注文件怎么用起来冬天跑高速最怕的不是堵车是路面那一层看着像水、压上去直接失控的暗冰。做自动驾驶感知或者道路巡检算法的团队每年入冬前都要面对同一个问题结冰样本太少模型没见过几场真正的冻雨上车就翻车。这份道路结冰数据集就是冲着这个场景来的——1527 张实拍道路图像全部完成 YOLO 和 VOC 双格式标注直接对应目标检测任务里的“结冰区域”这一类。它适合两类人一类是刚接触计算机视觉、想拿一个真实场景数据集把 YOLO 训练流程跑通的新手另一类是在做道路病害、气象联动、边缘巡检设备需要快速补一批冬季负样本的开发者。数据集本身不复杂但怎么把 1527 张图喂进模型、怎么避免小样本过拟合、怎么在 VOC 和 YOLO 之间来回切换这些才是真正花时间的地方。下面按我实际拆包的顺序从目录结构一路讲到二次开发。2. 数据集结构与标注格式1527 张图怎么组织、VOC 和 YOLO 差在哪拿到一个数据集我第一件事不是写训练脚本而是先把目录树打出来。目录结构决定了你后面写 DataLoader 时要不要做路径映射也决定了你换格式时会不会把标签搞丢。这份道路结冰数据集的组织方式比较标准属于“图像 双格式标签”的典型布局。2.1 目录结构与文件命名规律解压后大致是这样一个结构不同打包方式可能略有差异但核心目录不会变road_ice_dataset/ ├── images/ # 全部 1527 张原始图像 │ ├── ice_0001.jpg │ ├── ice_0002.jpg │ └── ... ├── labels_yolo/ # YOLO 格式标签 │ ├── ice_0001.txt │ ├── ice_0002.txt │ └── ... ├── annotations_voc/ # VOC 格式标签 │ ├── ice_0001.xml │ ├── ice_0002.xml │ └── ... └── classes.txt # 类别定义这里有几个细节值得注意。图像和标签是按文件名主干一一对应的ice_0001.jpg对应ice_0001.txt和ice_0001.xml这意味着你写数据集类时可以直接用stem做索引不需要额外维护映射表。classes.txt里通常只有一行ice也就是单类别检测——这一点很关键它决定了你后面模型输出通道数应该是 54 个坐标 1 个类别置信度而不是 COCO 那种 80 类。我一般会先跑一段脚本核对图像和标签数量是否一致避免训练到一半才发现有图没标签import os from pathlib import Path root Path(road_ice_dataset) imgs {p.stem for p in (root / images).glob(*.jpg)} yolo {p.stem for p in (root / labels_yolo).glob(*.txt)} voc {p.stem for p in (root / annotations_voc).glob(*.xml)} print(图像数量:, len(imgs)) print(YOLO 标签数量:, len(yolo)) print(VOC 标签数量:, len(voc)) print(有图无 YOLO 标签:, imgs - yolo) print(有图无 VOC 标签:, imgs - voc)这段脚本做的是集合差运算。imgs - yolo会列出所有有图像但缺 YOLO 标签的主干名正常情况下应该是空集。如果非空要么是标注漏了要么是文件名大小写不一致——Windows 下不敏感、Linux 下敏感这个坑我踩过不止一次。2.2 VOC 与 YOLO 标注格式的本质区别很多人以为 VOC 和 YOLO 只是“两种文件后缀”其实它们的坐标表达逻辑完全不同转换时最容易出错。VOC 的 XML 用的是绝对像素坐标格式是左上角(xmin, ymin)和右下角(xmax, ymax)annotation filenameice_0001.jpg/filename size width1920/width height1080/height /size object nameice/name bndbox xmin412/xmin ymin688/ymin xmax1503/xmax ymax1042/ymax /bndbox /object /annotationYOLO 的 txt 用的是归一化中心坐标 宽高每行一个目标格式是class_id cx cy w h全部除以图像宽高归一化到 0~10 0.4987 0.8009 0.5682 0.3278这两个格式的换算关系是# VOC - YOLO 转换核心逻辑 def voc_to_yolo(xmin, ymin, xmax, ymax, img_w, img_h): cx (xmin xmax) / 2.0 / img_w # 中心 x 归一化 cy (ymin ymax) / 2.0 / img_h # 中心 y 归一化 w (xmax - xmin) / img_w # 宽归一化 h (ymax - ymin) / img_h # 高归一化 return cx, cy, w, h参数说明img_w、img_h必须取该图实际的宽高不能统一用 1920×1080因为数据集里可能混有不同分辨率的图。我见过有人图省事全按 1920×1080 算结果低分辨率图的框全部偏移mAP 直接掉十几个点。反过来 YOLO 转 VOC 时要把归一化坐标乘回宽高再取整注意xmax和ymax要min(..., img_w-1)做边界裁剪否则会越界。提示转换后建议随机抽 20 张用可视化脚本画框核对别信“转换脚本跑完没报错”就等于对了。2.3 类别定义与标注质量初判单类别ice的好处是省去了类别不平衡的烦恼但也带来一个隐患结冰区域和湿滑路面、积水反光在视觉上高度相似标注边界本身就带主观性。我一般会先统计一下框的尺寸分布看看标注是否合理import numpy as np from pathlib import Path ws, hs [], [] for txt in Path(road_ice_dataset/labels_yolo).glob(*.txt): for line in txt.read_text().strip().splitlines(): _, _, _, w, h map(float, line.split()) ws.append(w); hs.append(h) ws, hs np.array(ws), np.array(hs) print(框数量:, len(ws)) print(宽 均值/中位数:, ws.mean().round(3), np.median(ws).round(3)) print(高 均值/中位数:, hs.mean().round(3), np.median(hs).round(3)) print(小目标占比(w0.05):, (ws 0.05).mean().round(3))如果小目标占比很高说明大量结冰区域在图中只占很小一块这时候用默认的 640 输入尺寸训练会丢细节需要调大imgsz或者用切片推理。这个统计结果直接决定你后面训练配置怎么设比盲目开训靠谱得多。3. 训练环境搭建与 YOLO 跑通从零到第一个权重文件数据集看明白了接下来就是让它真正跑起来。这一章我按“装环境 → 配数据 → 起训练 → 看结果”的顺序走每一步都给可复制的命令和代码。目标很明确让你在半小时内看到第一个best.pt。3.1 环境依赖与版本选择目标检测训练对版本比较敏感尤其是 CUDA、PyTorch 和 ultralytics 三者之间的匹配。我一般用 conda 隔离环境避免污染主环境conda create -n ice_det python3.10 -y conda activate ice_det # 按你的 CUDA 版本装 PyTorch这里以 CUDA 11.8 为例 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 安装 ultralyticsYOLOv8/v11 通用 pip install ultralytics opencv-python numpy装完先验证 GPU 是否可用import torch print(CUDA 可用:, torch.cuda.is_available()) print(设备:, torch.cuda.get_device_name(0) if torch.cuda.is_available() else CPU)如果CUDA 可用是 False别急着往下走先解决驱动和 CUDA 版本问题。用 CPU 训 1527 张图不是不能跑但一轮可能要几十分钟调参阶段会非常痛苦。常见做法是先确认nvidia-smi显示的 CUDA 版本再选对应的 PyTorch 安装命令版本对不上是新手最容易卡住的地方。3.2 数据集配置文件与目录重排ultralytics 要求固定的目录结构所以要把前面的数据整理成images/labels加train/val的形式。我一般写个脚本自动划分按 8:2 切import random, shutil from pathlib import Path src_img Path(road_ice_dataset/images) src_lbl Path(road_ice_dataset/labels_yolo) dst Path(ice_yolo) for split in [train, val]: (dst / images / split).mkdir(parentsTrue, exist_okTrue) (dst / labels / split).mkdir(parentsTrue, exist_okTrue) stems sorted(p.stem for p in src_img.glob(*.jpg)) random.seed(42) # 固定随机种子保证可复现 random.shuffle(stems) split_idx int(len(stems) * 0.8) train_stems, val_stems stems[:split_idx], stems[split_idx:] for split, subset in [(train, train_stems), (val, val_stems)]: for s in subset: shutil.copy(src_img / f{s}.jpg, dst / images / split / f{s}.jpg) shutil.copy(src_lbl / f{s}.txt, dst / labels / split / f{s}.txt) print(训练集:, len(train_stems), 验证集:, len(val_stems))参数说明random.seed(42)是为了让每次划分结果一致方便复现实验0.8是训练集比例1527 张按这个切大约是 1221 训练、306 验证。样本量不算大验证集再小的话指标波动会很明显所以不建议切到 9:1。然后写ice.yamlpath: ./ice_yolo train: images/train val: images/val nc: 1 names: [ice]nc: 1对应单类别names顺序必须和classes.txt一致否则类别索引会错位。3.3 启动训练与关键参数解读配置齐了就可以起训。我一般先用小模型快速验证流程通不通再换大模型yolo detect train \ dataice.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ patience20 \ projectruns/ice \ nameexp1逐个说关键参数modelyolov8n.pt是 nano 版预训练权重速度快适合先跑通imgsz640是输入分辨率如果前面统计发现小目标多这里可以提到 960 或 1280但显存占用会明显上升batch16在 8G 显存上比较稳爆显存就降到 8lr00.01是初始学习率小数据集上我一般会降到 0.005 甚至 0.001避免早期震荡patience20是早停验证指标 20 轮不涨就停省时间。训练过程中重点看两个输出box_loss和mAP50。box_loss持续下降说明回归在收敛mAP50是 IoU 阈值 0.5 下的平均精度单类别任务里这个数最直观。如果mAP50卡在 0.3 以下不动大概率是标注有问题或者学习率太大先回去抽查标签。3.4 推理验证与结果可视化训完拿best.pt跑推理确认模型真的学到了东西from ultralytics import YOLO model YOLO(runs/ice/exp1/weights/best.pt) results model.predict( sourceice_yolo/images/val, conf0.25, # 置信度阈值 iou0.45, # NMS IoU 阈值 saveTrue, # 保存带框结果图 projectruns/ice, namepredict ) for r in results[:5]: print(r.path, 检测到, len(r.boxes), 个目标)conf0.25是过滤低置信框的阈值结冰区域边界模糊这个值可以适当调低到 0.15 看召回但太低会引入大量误检iou0.45控制 NMS 合并重叠框的力度。跑完去runs/ice/predict里翻结果图重点看两类错误漏检明明有冰没框出来和误检把水渍当冰。这两类错误的处理方向完全相反漏检要降阈值或加数据误检要提阈值或补负样本。4. 常见问题排查训练不收敛、标签错位、显存爆炸怎么破这一章是我自己踩过的坑合集每条都按“现象 → 原因 → 解决”写。数据集训练出问题八成不是模型不行而是数据和配置的细节没对齐。4.1 训练 loss 不降反升现象前几轮box_loss从 2.0 降到 1.5然后突然反弹到 3.0 以上mAP50归零。原因最常见的是学习率过大导致梯度爆炸其次是标签里有非法值比如归一化坐标超过 1 或者为负。1527 张的小数据集对学习率非常敏感默认的 0.01 往往偏大。解决先把lr0降到 0.001 重跑同时用脚本扫一遍所有标签检查是否有越界值from pathlib import Path bad [] for txt in Path(ice_yolo/labels).rglob(*.txt): for i, line in enumerate(txt.read_text().strip().splitlines()): vals list(map(float, line.split()[1:])) if any(v 0 or v 1 for v in vals): bad.append((txt.name, i, vals)) print(非法标签:, bad[:10])4.2 验证集 mAP 正常但实际推理全是误检现象训练日志里mAP50有 0.7但拿验证集图片跑predict发现满屏都是框。原因验证集和训练集划分时没有打乱或者验证集图片和训练集高度相似导致指标虚高。另一种可能是conf阈值设得太低。解决确认划分脚本里random.shuffle生效推理时把conf从 0.25 提到 0.4 再看。如果还是误检多说明模型没学到结冰的判别特征需要补一批“湿滑但未结冰”的负样本让模型学会区分。4.3 显存爆炸CUDA out of memory现象训练刚启动就报RuntimeError: CUDA out of memory。原因batch或imgsz超过显存容量。1527 张图如果分辨率普遍是 1920×1080imgsz1280加batch16在 8G 卡上必爆。解决按显存阶梯式下调先降batch到 8再不行降imgsz到 640。也可以用梯度累积模拟大 batchyolo detect train dataice.yaml modelyolov8n.pt epochs100 imgsz640 batch4 accumulate4accumulate4表示每 4 个 batch 更新一次权重等效 batch 为 16但显存占用按 4 算。4.4 VOC 转 YOLO 后框整体偏移现象转换后的 YOLO 标签画出来框整体往左上或右下偏。原因转换时用了统一的图像宽高而不是每张图的实际宽高或者把xmax/ymax当成了宽高而不是右下角坐标。解决转换函数必须从对应 XML 的size里读width和height逐图计算。转换完抽 20 张可视化核对别跳过这一步。4.5 训练到一半报“找不到标签文件”现象WARNING: No labels found然后所有标签被当成背景。原因ultralytics 要求images/train和labels/train目录层级严格对应且标签文件名主干必须和图像一致。常见错误是把标签放成了labels/train/images/xxx.txt多套了一层。解决用yolo detect train ... rectFalse前先确认目录结构或者直接跑一遍官方校验yolo detect train dataice.yaml modelyolov8n.pt epochs1看启动日志里打印的train: Scanning ... images, ... backgrounds如果backgrounds数量等于图像总数说明标签一个都没读到回去查路径。5. 二次开发与改造从单类别检测到多场景落地的几个实用技巧数据集跑通只是起点真正要落地到道路巡检或车载预警还得做几件事把单类别扩成多类别、把 VOC 流程接进来、做数据增强补样本、以及把模型导出到边缘设备。这一章讲我实际改造时用得最多的几个手法。5.1 从单类别扩展到“结冰 积水 湿滑”多类别单类别ice只能告诉你“这里有冰”但业务上往往需要区分“冰”和“水”。改造的第一步是改classes.txt和ice.yamlnc: 3 names: [ice, water, wet]然后要把现有 1527 张图的标签重新过一遍把原本笼统标成ice的框按实际情况拆成三类。这里有个省力的做法先用现有单类别模型跑一遍推理把高置信框作为预标注人工只做修正比从零标快得多。改完类别后nc变了预训练权重的检测头维度不匹配训练时 ultralytics 会自动重建检测头但建议把lr0调小到 0.001让新头稳定收敛。5.2 把 VOC 标注流程接进训练管线有些团队标注工具只出 VOC 格式那就需要一条稳定的转换链路。我一般封装成一个可复用的转换脚本而不是每次手写import xml.etree.ElementTree as ET from pathlib import Path def voc_to_yolo_txt(xml_path, out_path, class_map): tree ET.parse(xml_path) root tree.getroot() w int(root.find(size/width).text) h int(root.find(size/height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_map: continue bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) cx (xmin xmax) / 2 / w cy (ymin ymax) / 2 / h bw (xmax - xmin) / w bh (ymax - ymin) / h lines.append(f{class_map[name]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) out_path.write_text(\n.join(lines)) class_map {ice: 0, water: 1, wet: 2} for xml in Path(road_ice_dataset/annotations_voc).glob(*.xml): voc_to_yolo_txt(xml, Path(ice_yolo/labels/train) / f{xml.stem}.txt, class_map)class_map把类别名映射成索引顺序必须和ice.yaml的names一致。转换后同样要抽检可视化尤其是边界框贴边的样本容易出现xmax等于图像宽度导致归一化后等于 1.0 的边界情况。5.3 小样本数据增强策略1527 张对检测任务来说偏少直接训容易过拟合。ultralytics 内置了增强但默认参数偏保守我一般会针对性调yolo detect train \ dataice.yaml \ modelyolov8n.pt \ epochs150 \ imgsz640 \ hsv_h0.015 hsv_s0.7 hsv_v0.4 \ degrees10 translate0.1 scale0.5 \ fliplr0.5 mosaic1.0 mixup0.1参数说明hsv_h/s/v控制色调、饱和度、亮度扰动结冰在不同光照下颜色差异大这三个值可以适当放大degrees10做小角度旋转模拟不同拍摄角度mosaic1.0是四图拼接对小数据集提升明显mixup0.1做图像混合比例别太高否则结冰边界会糊掉。增强不是越多越好我一般会留 10% 的验证集完全不做增强用来观察真实泛化能力。5.4 导出与边缘部署验证训完的best.pt要上设备通常导出成 ONNX 或 TensorRT# 导出 ONNX yolo export modelruns/ice/exp1/weights/best.pt formatonnx imgsz640 # 导出 TensorRT需要 GPU 环境 yolo export modelruns/ice/exp1/weights/best.pt formatengine halfTrue imgsz640halfTrue表示 FP16 半精度速度提升明显但精度可能掉一点点导出后一定要用同一批验证图对比 ONNX 和 PyTorch 的输出确认框的位置和数量基本一致。我见过导出后坐标整体偏移的案例原因是预处理里的 letterbox 参数没对齐这种问题只能靠对比测试发现。从那以后我每次改完类别或转换格式都强制走一遍“抽 20 张可视化 导出后对比推理”的流程再也不敢直接拿转换脚本的结果去训。希望这份拆解能帮你把这份道路结冰数据集真正用起来少走几个我踩过的弯路。本文还有配套的精品资源点击获取