ARTICLE DETAIL

资讯详情

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

车道线语义分割数据集解析:从mask校验到UNet训练避坑指南

车道线语义分割数据集解析:从mask校验到UNet训练避坑指南 简介这是一份面向自动驾驶场景的语义分割数据集包含约1300张真实道路图像及对应像素级标签共划分背景、虚线、实线三个类别适用于车道线检测、辅助驾驶感知等分割任务。压缩包共2000个文件其中641张jpg原始图片、1357张png标签掩码以及1个txt类别说明和1个py可视化脚本整体大小约146.87MB。数据已按训练集约1200张和验证集约120张拆分图片与masks目录对应清晰可直接接入UNet、SwinUNet等常见分割模型训练。配套可视化脚本支持随机抽取图像将原图、GT掩码及叠加效果保存为图片便于快速核对标注质量。作者还附上了图像分割网络及改进专栏入口可进一步查阅相关模型实现。目前已有302人学习下载适合自动驾驶、计算机视觉方向的学习者与算法工程师使用。1. 车道线语义分割数据集约1300张图、三类掩码先看清楚它适合谁自动驾驶场景里的车道线检测做过实车验证的人都知道最大瓶颈往往不是模型结构而是数据标注的粒度。这份车道线语义分割数据集把目标收敛为三件事背景、虚线、实线。约1300张图片和配套mask训练集1200张左右、验证集120张左右已经按目录划分完毕拿来就能直接训练分割网络。它不是那种动辄几万张的大规模榜单数据而是适合做模型验证、课程设计、毕设实验的中等规模数据集。它的定位很明确解决“从零标注车道线”的重复劳动。对做自动驾驶视觉、语义分割算法、车道线检测相关项目的人来说这里省掉的是最耗时的清洗和标注环节。配套还有一个可视化脚本能随机抽取图片并展示原始图、GT图、GT蒙板叠加图这也正好是你要做结果汇报或论文插图时最常用到的东西。接下来我按实际使用顺序拆一下目录、校验方法、训练参数和踩坑记录。2. 数据集结构与标注约定目录、文件命名和三值mask的像素约定2.1 目录结构与文件命名images/masks 各司其职这份数据在下载解压后第一眼看到的应该是 train 和 valid或 val两个大目录。常见组织方式是每个大目录下再拆 images 和 masks少数版本会把验证集写成 validation打开后确认一下即可。images 里放的是拍摄帧或截图原图masks 里放的是同名同尺寸的标签图。命名上能看到类似Screenshot-from-2024-03-07-22-01-13_png.rf.3640d793c43099d02963013df7ec2eca.jpg这种原始文件名.rf.是 Roboflow 导出数据时留下的标记不影响使用。dataset/ ├── train/ │ ├── images/ │ │ ├── Screenshot-from-2024-03-07-22-01-13_png.rf.3640d793....jpg │ │ └── ... │ └── masks/ │ ├── Screenshot-from-2024-03-07-22-01-13_png.rf.3640d793....png │ └── ... ├── valid/ │ ├── images/ │ └── masks/ └── classes.txt这里有个细节值得先记住images 和 masks 的文件名完全对应只是扩展名不同。原图通常保留 jpg标注图往往是 png因为 png 是无损格式能保证 mask 的像素值不被压缩破坏。classes.txt 里记录的是类别名比如 background、dashed、solid 之类。在写训练代码时这个文件比目录名更值得信任一切以它为准。检查文件是否一一对应时我一般会先把两个目录的文件名列出来做差集。如果出现某个图有原图但没有 mask这种脏数据在训练时会让 DataLoader 报错或直接读到错误标签所以这一步不要省。ls train/images | sed s/\.[^.]*$// | sort /tmp/img_names.txt ls train/masks | sed s/\.[^.]*$// | sort /tmp/mask_names.txt diff /tmp/img_names.txt /tmp/mask_names.txt这段命令把 images 和 masks 目录下的文件名去掉扩展名后排序对比diff 无输出说明完全对应。sed 替换规则是去掉最后一个点后面的扩展名如果文件名单里有点号也要保留只去掉后缀。用这个命令先跑一遍比在 Python 里遍历更直观也更快定位缺失的是哪个文件。2.2 三类 mask 的像素约定与可视化脚本的用法语义分割的 mask 本质是一张与原始图像同尺寸的单通道标签图像素值通常按整数编码。这类数据集的常见约定是 background 为 0、dashed 为 1、solid 为 2但每个数据集不一样动手写损失函数前最好先取一张 mask 打印它的唯一值列表确认一次。from PIL import Image import numpy as np mask_path train/masks/Screenshot-from-2024-03-07-22-01-13_png.rf.3640d793....png mask np.array(Image.open(mask_path)) unique_vals np.unique(mask) print(mask shape:, mask.shape) print(unique pixel values:, unique_vals)这段代码用 PIL 读取 mask 并转成 numpy 数组np.unique 返回所有出现的像素值。如果类别数应该是 3但运行后发现出现 255 或者 127 这样的值说明这条 mask 不是索引编码格式而是有调色板或者被保存成了 RGB 模式后面第 4 章会专门讲这个翻车点。如果 unique 值恰好是 0、1、2那训练时直接用它做交叉熵标签即可。配套可视化脚本的用途是随机抽一张图把原图、GT、GT 蒙板在原图上叠加的结果展示并保存到当前目录。类似这样import matplotlib.pyplot as plt import random from PIL import Image import numpy as np img_dir train/images mask_dir train/masks names [p for p in os.listdir(img_dir)] pick random.choice(names) img np.array(Image.open(os.path.join(img_dir, pick))) mask np.array(Image.open(os.path.join(mask_dir, pick))) fig, axes plt.subplots(1, 3, figsize(15, 5)) axes[0].imshow(img); axes[0].set_title(original) axes[1].imshow(mask, cmapgray); axes[1].set_title(GT) overlay img.copy() overlay[mask 1] [255, 0, 0] # 虚线标红 overlay[mask 2] [0, 255, 0] # 实线标绿 axes[2].imshow(overlay); axes[2].set_title(overlay) plt.savefig(visual_check.png, dpi150, bbox_inchestight)叠加逻辑是把 mask 中像素值为 1 的位置在原图上涂红、像素值为 2 的位置涂绿背景保持原样。这个脚本的价值在于快速判断标注是否贴合车道线边缘如果虚线被标成了实线、或者车道线边缘明显缩进能一眼看出。可视化的保存参数 dpi 和 bbox_inches 建议固定尤其做论文插图时同一批图保持一致的分辨率最后拼图会省很多事。3. 拿数据前先做三件事校验一致性、统计类别频率、核对划分可信度3.1 统计三类像素占比判断类别均衡度分割训练里最常见的隐性问题不是模型不收敛而是类别不均衡导致模型学成“背景预测器”。车道线本身细长像素占比天然偏低虚线实线加起来可能只占 3% 到 5%如果不管它直接拿交叉熵训练模型大概率把所有像素都判成背景也能拿到 95% 以上的准确率。所以拿到数据的第一件事是统计训练集每个类别的像素占比。统计方式很简单遍历所有 mask逐类累计像素数。import os import numpy as np from PIL import Image from collections import Counter mask_dir train/masks cnt Counter() for name in os.listdir(mask_dir): mask np.array(Image.open(os.path.join(mask_dir, name))) vals, counts np.unique(mask, return_countsTrue) for v, c in zip(vals, counts): cnt[int(v)] c total sum(cnt.values()) for cls_id in sorted(cnt): print(fclass {cls_id}: {cnt[cls_id]} pixels, {cnt[cls_id] / total:.4%})Counter 在这里方便地把像素值映射到数量最后输出每个类别的占比。如果虚线或实线的占比低于 1%后面训练时就要考虑加权交叉熵或者 Dice loss如果三类占比差距不大用普通交叉熵也没问题。这个结果对你选择损失函数起着决定性作用一定要在训练之前跑不要等到 loss 出现异常才回头检查。3.2 校验图片与 mask 尺寸和通道数另一个容易被忽略的点是原图与 mask 是否真的同尺寸。Roboflow 导出的数据通常会统一尺寸但偶尔也有漏网之鱼。用一段简单脚本把每张图的尺寸收集起来看分布是否一致。import os from PIL import Image img_dir train/images mask_dir train/masks size_set set() for name in sorted(os.listdir(img_dir))[:50]: with Image.open(os.path.join(img_dir, name)) as im: size_set.add(im.size) mask_name name.replace(.jpg, .png) with Image.open(os.path.join(mask_dir, mask_name)) as im: size_set.add(im.size) print(size_set)只取前 50 张作为抽查即可如果 size_set 里出现了多种尺寸说明这批数据不是统一尺寸。训练时如果直接把整图喂进去网络会因输入尺寸不一致报错或者隐式缩放对应的 mask 若用不同方式缩放标签就会错位。处理办法是把它统一 resize 到固定尺寸比如 512×512并且原图和 mask 必须用相同的插值方式mask 只能用最近邻插值。3.3 核对训练/验证划分可信度约1300张数据里训练集 1200 张、验证集 120 张比例大约是 10:1这个划分对语义分割来说是合理的。但划分可信不代表场景分布可信自动驾驶数据经常出现同一路段连续帧高度相似的情况。如果训练集和验证集都是从同一段视频抽帧而来那么验证集的效果会虚高因为模型见过了几乎一样的路面纹理。我一般会做一次简单的重复度检查随机抽 5 张训练图和 5 张验证图计算它们之间的结构相似度或者直接肉眼观察路面背景是否明显来自同一场景。如果验证集的图片和训练集看起来是同一段路建议自己再按场景重新划分而不是直接用原目录。4. 避坑清单从 mask 读取到训练标签四条高频翻车记录4.1 踩坑mask 读出来是三通道像素值全是 0 和 255现象训练时报错说标签类别数不符打印 mask 的 shape 发现是(H, W, 3)而不是(H, W)而且像素值不是 0、1、2而是 0 和 255。原因部分 mask 在导出时被保存成了 RGB 模式的 PNG虽然肉眼看起来是黑色背景加白色车道线但它不是索引图像。直接用np.array(Image.open(mask_path))会得到一个三通道数组。255 表示白色车道线区域与类别 ID 对不上。解决读入后转灰度并重映射把 255 映射为 1 或 2。如果只有一类前景直接mask np.array(Image.open(p).convert(L))再把 255 改成 1如果有虚线实线两种前景需要参考原始标注工具的颜色约定做映射。mask np.array(Image.open(mask_path).convert(L)) mask np.where(mask 127, 1, 0).astype(np.int64)convert(L) 把 RGB 图转成单通道灰度再按阈值 127 把白色区域置为前景。这个操作只适用二类分割场景三类分割时要先定位每种颜色对应的像素值再做逐通道映射。4.2 踩坑resize 之后 mask 出现 0 和 1 之间的浮点脏值现象训练过程中计算 loss 时出现 NaN或者在输出 mask 可视化时看到边缘有灰色渐变带。打印预处理后的标签数组发现里面有0.392、0.871这种非整数值。原因最常见的翻车是把 mask 也当成普通图像用了双线性插值或双三次插值去缩放。这种插值会在类别边界处产生中间值语义分割的标签必须是离散整数中间值没有意义还会把交叉熵计算搞出 NaN。解决原图用双线性插值没问题mask 必须强制用最近邻插值也就是 PIL 的Image.Resampling.NEAREST或者 OpenCV 的cv2.INTER_NEAREST。from PIL import Image img Image.open(img.jpg).resize((512, 512), Image.Resampling.BILINEAR) mask Image.open(mask.png).resize((512, 512), Image.Resampling.NEAREST)这段代码展示了同一尺寸下原图和 mask 各用各的插值方式。从那以后我每当写数据增强流水线都会在 resize、随机裁剪、旋转这些操作里单独盯住 mask 的 interpolation 参数这个坑属于那种不报错、只让效果悄悄变差的类型。4.3 踩坑训练集 1200 张不等于场景多样性够用现象训练 80 个 epoch 后验证集 mIoU 停在 0.7 左右上不去但训练集 mIoU 已经接近 1.0明显的过拟合。原因车道线数据集虽然是 1200 张但如果连续帧是同一路段拍出来的真正独立场景可能只有几百个。模型在重复看到的场景上表现好换个新路段就不行这是自动驾驶感知里的普遍问题不怪模型。解决用第 3 章的抽查法判断场景相似度如果确实验证集和测试集场景单一考虑引入随机亮度扰动、随机翻转、随机裁剪来增加训练样本的表观多样性。另外可以把这个数据集当预训练数据在自己的场景数据上做微调而不是指望这 1200 张直接覆盖所有路况。4.4 踩坑把验证集当成测试集反复调参最后报告数字虚高现象在验证集上调好了所有超参数最后报告里写这个模型 mIoU 0.82换到真实路段实测只有 0.6。原因验证集参与了超参数选择相当于模型间接见过验证集的标签分布数字自然会偏乐观。120 张验证图不算多反复调参很快就会过拟合到验证集上。解决把这份数据里的 valid 目录继续分成两份一份用于调参一份做最终测试。或者训练时用训练集里再切一小部分做内部验证只把原 valid 目录当成最终评估。120 张图做最终评估其实已经够用关键是保证它在整个流程中只能碰一次。5. 从数据集到训练加载器、损失函数与模型选型UNet / SwinUNet5.1 写一个 PyTorch 数据加载器把 mask 转成 long tensor训练分割模型的第一步是把目录数据包装成 DataLoader。这里的核心逻辑是原图做归一化和增强mask 保持整数标签不变最后转成torch.long因为 PyTorch 的交叉熵损失要求标签是 long 类型。import os import torch from torch.utils.data import Dataset from PIL import Image import numpy as np class LaneSegDataset(Dataset): def __init__(self, img_dir, mask_dir, size(512, 512)): self.img_dir img_dir self.mask_dir mask_dir self.size size self.names [n for n in os.listdir(img_dir) if n.endswith(.jpg)] def __len__(self): return len(self.names) def __getitem__(self, idx): name self.names[idx] img Image.open(os.path.join(self.img_dir, name)).resize(self.size, Image.Resampling.BILINEAR) mask_name name.replace(.jpg, .png) mask Image.open(os.path.join(self.mask_dir, mask_name)).resize(self.size, Image.Resampling.NEAREST) img np.array(img, dtypenp.float32) / 127.5 - 1.0 mask np.array(mask, dtypenp.int64) return torch.from_numpy(img).permute(2, 0, 1), torch.from_numpy(mask)这个 Dataset 的构造函数里固定了输出分辨率为 512×512原图缩放用双线性mask 缩放用最近邻这是上一章踩坑后养成的习惯。归一化写成除以 127.5 再减 1把像素范围映射到 -1 到 1 之间对应许多分割网络的输入分布。返回的 mask 是(512, 512)的二维数组不会被套上通道维度正好匹配交叉熵输入的(B, H, W)格式。5.2 损失函数和指标怎么匹配这份数据三类分割的默认选择是交叉熵但当第 3 章统计出虚线或实线像素占比不足 1% 时需要换用带权重的交叉熵或者加上 Dice loss。加权重的方法是把每个类别的权重设成频率倒数的变形让少数类得到更大的梯度。weights torch.tensor([1.0, 3.0, 5.0]) # 背景、虚线、实线 criterion torch.nn.CrossEntropyLoss(weightweights.cuda())权重数值需要根据第 3 章的统计结果调整虚线占比最低就给它最大权重实线次之背景保持 1。如果发现光调权重还不够稳可以换成 Dice loss 和交叉熵的线性组合Dice loss 对像素占比小的目标非常敏感。def dice_loss(pred, target, eps1e-6): pred torch.softmax(pred, dim1) target_onehot torch.nn.functional.one_hot(target, num_classes3).permute(0, 3, 1, 2) intersection (pred * target_onehot).sum(dim(2, 3)) union pred.sum(dim(2, 3)) target_onehot.sum(dim(2, 3)) return 1 - (2 * intersection eps) / (union eps)softmax 把原始 logits 转成概率分布one_hot 把标签变成 (B, 3, H, W) 的格式才能与预测概率逐元素相乘。eps 参数防止分母为 0。在训练循环中把交叉熵和 dice_loss 按 1:1 相加这类组合在车道线这类细长目标上收敛速度通常比纯交叉熵快不少。5.3 模型选型UNet 起步、SwinUNet 和 TransUNet 改进方向这份数据规模在 1200 张左右直接上超大模型容易过拟合最稳妥的方案是从 UNet 开始。UNet 的编码器-解码器结构天然适合车道线这类像素级任务参数少、收敛快跑通一套流程只需要一到两张卡。作者的 CSDN 专栏里整理了医学图像分割网络 UNet、SwinUNet、TransUNet 的改进笔记思路是通用的应用到车道线场景只需要把输入通道从 1 改成 3、输出类别数改成 3其他结构不变。SwinUNet 把 Transformer 引入分割优势是能建模长距离依赖拟合实线长车道时比普通 UNet 有更好的全局上下文。TransUNet 则在编码器部分用 Transformer 提取特征再与 CNN 特征融合。三个模型的参数量差异很大训练参数建议参考这张表起步。模型输入分辨率batch size初始学习率预期收敛 epoch显存占用UNet512×51281e-460-100约 6-8 GBSwinUNet512×51241e-480-120约 12-16 GBTransUNet512×51245e-5100-150约 10-14 GB在 1200 张训练集规模下学习率不建议直接用 1e-3那通常是为大数据集准备的。1e-4 配合余弦退火和 warmup一般能稳定收敛。如果显存不够把输入分辨率降到 384×384mIoU 的下降通常在 1% 以内但显存压力会小很多。6. 把 GT、预测和不确定区叠在同一张图上验证可视化脚本的三条实用操作训练结束后最直接的验证方式就是抽几张图把真实标签和模型预测叠在一起看差异。纯看 mIoU 指标看不出问题在哪条车道线叠图能直观看到误检是发生在虚线端点、实线破损处还是远处小目标上。第一条操作是保存一张“原图、GT、预测”三栏对比图。预测结果通过torch.argmax把网络输出的三通道概率变成单通道整数标签然后按第 2 章的叠加方法涂色。实线画绿色、虚线画红色、背景保持原样两张叠加图放一起边缘差异一目了然。第二条操作是把预测错误的位置单独抽出来做“错误热区”。统计每个像素上 GT 与预测是否一致把不一致区域用高亮色标记叠到原图上。这比直接看 mIoU 更能定位系统性的错误。比如发现错误集中在虚线端点说明模型对短线段不敏感可以考虑增大 loss 权重或者加长训练周期如果错误集中在远处路面大概率是下采样倍率太大导致小目标丢失需要减小下采样倍数或者提高输入分辨率。第三条操作是记录训练过程中每轮验证集的类别 mIoU而不是只记录平均 mIoU。三类分割中实线和虚线的 mIoU 通常会明显低于背景只看平均值会掩盖这种差距。把每个类别的曲线画出来能看出是不是某一类始终不涨。如果虚线 mIoU 停滞在 0.4 左右说明该类别样本特征或者数据量不足不要急着换模型先回第 3 章看类别像素占比再决定加权重还是补数据。用这份数据集跑完整个过程后我保留了三个固定习惯任何 mask 进入训练管线前必须打印np.unique(mask)任何 resize 操作单独检查 mask 的插值方式任何验证结果在汇报前必须做一次叠图人工确认。从那以后换数据集、换模型、改 loss 时踩的坑明显少了很多。这份数据对初学语义分割和做自动驾驶课程设计的场景都很合适跑通一套标准流程后再决定是否换更大规模的数据希望帮到你。本文还有配套的精品资源点击获取
返回列表