简介:这是一份面向自动驾驶场景的语义分割数据集,包含约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, cmap="gray"); 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", dpi=150, bbox_inches="tight")叠加逻辑是把 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_counts=True) for v, c in zip(vals, counts): cnt[int(v)] += c total = sum(cnt.values()) for cls_id in sorted(cnt): print(f"class {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 / SwinUNet)
5.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, dtype=np.float32) / 127.5 - 1.0 mask = np.array(mask, dtype=np.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(weight=weights.cuda())权重数值需要根据第 3 章的统计结果调整,虚线占比最低就给它最大权重,实线次之,背景保持 1。如果发现光调权重还不够稳,可以换成 Dice loss 和交叉熵的线性组合,Dice loss 对像素占比小的目标非常敏感。
def dice_loss(pred, target, eps=1e-6): pred = torch.softmax(pred, dim=1) target_onehot = torch.nn.functional.one_hot(target, num_classes=3).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 | 显存占用 |
|---|---|---|---|---|---|
| UNet | 512×512 | 8 | 1e-4 | 60-100 | 约 6-8 GB |
| SwinUNet | 512×512 | 4 | 1e-4 | 80-120 | 约 12-16 GB |
| TransUNet | 512×512 | 4 | 5e-5 | 100-150 | 约 10-14 GB |
在 1200 张训练集规模下,学习率不建议直接用 1e-3,那通常是为大数据集准备的。1e-4 配合余弦退火和 warmup,一般能稳定收敛。如果显存不够,把输入分辨率降到 384×384,mIoU 的下降通常在 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 时踩的坑明显少了很多。这份数据对初学语义分割和做自动驾驶课程设计的场景都很合适,跑通一套标准流程后再决定是否换更大规模的数据,希望帮到你。
本文还有配套的精品资源,点击获取