简介:自动驾驶车道线分割数据集,面向计算机视觉与自动驾驶感知方向的学习者与开发者,可用于虚线、实线及背景三类别的语义分割模型训练与验证。数据集已划分训练集与验证集,共约1300张图像及对应标签,另包含用于快速预览的可视化脚本,能随机抽取样本并生成原图、GT及叠加展示图,便于检查标注质量与模型输入效果。压缩包共2000个文件,以png标签图和jpg原图为主,附带类别说明txt与可视化py脚本,整体大小约146.87MB,结构简洁易用。目前已有302人学习下载,适合配合U-Net、Swin-UNet等分割网络进行实操练习或算法改进实验。
1. 1300张车道线图能撑起哪些活:先把数据集的边界说清楚
在自动驾驶感知栈里,车道线分割是离量产最近、也最容易在评测集上翻车的任务之一。没有它,车道保持、变道决策和局部路径规划全部悬空;有了它,哪怕只用三类像素级标签——背景、虚线、实线——就能把感知链路的最低成本版本跑通。本文要拆的车道线语义分割数据集(3类分割,约1300张数据和标签)就是这样一个常见配置:图量不大,但类别边界清楚,适合验证图像分割算法、做车道线检测模型的对比训练,也适合当语义分割数据集制作的参照模板。别小看这1300张:用不好它比噪声还坑,用对了能帮你把网络结构、损失函数、类别权重这些参数一次量清楚。下面从标注规范开始讲到训练、增强和验收。
2. 把虚线、实线、背景三类标对:语义边界与标注规范
拿到任何车道线分割数据集,第一个要问的不是模型,而是标签怎么定义的。这个数据集只有3类,听起来比COCO的80类简单,但真实的坑全在半像素和边界约定里。同一根车道线,不同标注者可能给出不同掩码,差别不在网络,而在规范。
三类分割的语义边界如果没定死,训练出来的模型就会在“虚线断口该不该补上”这种问题上摇摆。这一章先把类别定义、像素值、文件组织讲透,这部分不扎实,后面所有训练都是白做。
2.1 三类标签的语义边界:虚线空白段算背景,不算“第四类”
常见的像素约定是:背景=0,虚线=1,实线=2。看起来直接,但有一个特别容易讨论不清的点——虚线由线段和间隔组成,间隔部分的路面该标什么?答案是背景。间隔在语义上仍然是路面,它不是一条“隐形的线”,更不是第三种类别。很多新手做语义分割数据集制作时,会下意识把虚线的断口也标记成虚线,结果模型学到的是“虚线的轮廓是一个连续长条”,断口处反而被错误地预测成实线。
实线的定义相对简单:连续涂刷的车道分隔线、道路边缘线都算实线。这里要补充一个约定:双黄线、白实线、路缘线在像素级标注里都归为实线,不区分颜色。因为语义分割任务的目标是“哪里有线”,而不是“线是什么颜色”,颜色信息交给图像本身去学习,标签层不需要额外编码。
还有一类像素需要明确排除:路口停止线、斑马线、导流线这类“线状交通标线”。它们看着像线,但语义上不是车道边界。多数自动驾驶车道线数据集的标注规范都会写明这一类不纳入3类范畴。我们自己做规范时也建议顺手把这类线在标注软件里隐藏,否则训练时模型会把停止线当成实线,在路口场景输出一大堆假线。这个边界不写进标注文档,后面返工成本极高。
另外值得提一句,车道线分割用的是语义分割而不是实例分割。早期做车道线检测的同行里,也有人尝试用实例分割给每条车道线单独分配ID,但对下游规划来说,车道线不需要区分“这是第几条线”,只需要知道哪个像素属于线、线是虚线还是实线。语义分割的输出直接是逐像素类别,推理链路短、延迟低。从YOLO系列里关于实例分割与语义分割的区分也能看到,两者的分界从来不是模型结构,而是输出粒度和使用方式。
2.2 标注与掩码文件组织:多段线转灰度PNG的像素约定
拿到数据后第一件事,先检查目录结构和标签文件格式。车道线数据集的常见组织方式是图像与标签分离存储,标签用灰度PNG,一个像素对应一个类别ID。PNG是无损压缩,能保证像素值不因压缩而漂移, JPEG 格式的掩码坚决不能用。
常见目录结构如下:
lane_dataset/ ├── images/ │ ├── 000001.jpg │ └── ... ├── labels/ │ ├── 000001.png │ └── ... ├── train.txt └── val.txtimages放原图,labels放灰度掩码,train.txt和val.txt每行写一个不带后缀的图片ID。这样做的目的是让 Dataset 类只读文本列表,不遍历全目录,训练时数据加载顺序可控,验证集也不会被误混进训练列表。
标签制作阶段,我一般用多段线工具标注,然后把多段线栅格化成掩码。直接拿画笔涂,边缘毛刺多,线宽不稳定,后期的训练会被迫去拟合标注噪声。多段线的好处是车道线本质上就是细长结构,几个关键点就能描述一段线,栅格化后线宽一致,边界干净。
拿到这套数据后,先写一个小脚本检查标签合法性,别急着开训练:
import cv2 import numpy as np from collections import defaultdict mask = cv2.imread('labels/000042.png', cv2.IMREAD_GRAYSCALE) counts = defaultdict(int) for v in mask.flatten(): counts[v] += 1 total = mask.size for cls_id in sorted(counts): print(f'class {cls_id}: {counts[cls_id]} px, {counts[cls_id] / total:.4%}')这段代码统计一张标签中每个类别的像素占比。正常情况下,背景占比应该在 85%–95%,虚线、实线合计占比在 5%–15% 左右。如果你看到类别ID出现了3、255或者其他非约定值,说明标注导出时颜色表没映射干净,这类非法像素会在训练时被当作未知类别处理,轻则损失函数跳变,重则整个预测输出乱掉。
注意:标签文件必须保证与原图尺寸一致。CNN 训练时会强制 resize 到固定尺寸,但标注导出环节如果分辨率不统一,会出现标签和原图局部错位的诡异效果,这种问题在可视化阶段很难一眼看出来。
还要检查掩码的边界质量。放大看几根虚线的断口处,边缘不能有多像素的渐变过渡。灰度PNG的像素值应当是 0、1、2 这种硬标签,如果边界出现 3–10 的中间值,说明标注工具用了抗锯齿导出,必须重新栅格化。这种细节决定了模型学到的边界是锐利的还是糊的。
3. 用1300张图训出能用的车道线分割模型:PyTorch最小训练管线
数据检查和标注规范确认后,接下来就是把图像和标签喂进网络。1300张图规模不大,很多团队会用 DeepLabv3+、PSPNet 这类语义分割算法做主力模型。我一般推荐先把 ResNet50 作为骨架的 DeepLabv3+ 跑通,因为它收敛稳定、显存占用适中,用来做 baseline 比较合适。
这一章给出一套完整的最小训练管线,覆盖数据加载、损失设计、训练循环和保存策略。代码可以直接复制改路径跑起来。
3.1 Dataset 类的三个关键:路径对齐、插值、标签不回退化
先把 Dataset 类写出来:
import os import cv2 import torch from torch.utils.data import Dataset class LaneSegDataset(Dataset): def __init__(self, root, split_file, size=(512, 288), transform=None): self.root = root self.size = size self.transform = transform with open(split_file, 'r', encoding='utf-8') as f: self.ids = [line.strip() for line in f if line.strip()] def __len__(self): return len(self.ids) def __getitem__(self, idx): img_id = self.ids[idx] img = cv2.imread(os.path.join(self.root, 'images', f'{img_id}.jpg')) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) mask = cv2.imread(os.path.join(self.root, 'labels', f'{img_id}.png'), cv2.IMREAD_GRAYSCALE) img = cv2.resize(img, self.size, interpolation=cv2.INTER_LINEAR) mask = cv2.resize(mask, self.size, interpolation=cv2.INTER_NEAREST) img_t = torch.from_numpy(img).permute(2, 0, 1).float() / 255.0 mask_t = torch.from_numpy(mask).long() if self.transform: img_t, mask_t = self.transform(img_t, mask_t) return img_t, mask_t这里有三个关键点。第一,cv2.imread读出来是 BGR 顺序,需要转成 RGB,否则模型学到的颜色分布是错的,典型表现是训练 loss 正常但可视化结果偏色。第二,mask 的 resize 必须用cv2.INTER_NEAREST最近邻插值,不能用线性插值。线性插值会在类别边界制造出 0.5、1.5 这类中间值,转成long()后类别发生偏移,相当于标签被悄悄篡改。第三,图像归一化在这里用除以 255 完成,网络内部再用 ImageNet 统计量做正则化也可以,但要在 transform 里写清楚,避免训练和推理不一致。
3.2 损失、类别权重与优化器参数:让虚线类不被背景吞掉
车道线分割天然是样本不均衡问题。一张 1280×720 的图里,背景占据绝大部分像素,虚线、实线加在一起往往只有几十万像素。直接套用标准 CrossEntropy,模型会快速收敛到“全预测为背景”,mIoU 看似不低,但车道线一条都出不来。
做法是给类别加权重:
import torch.nn as nn class_weights = torch.tensor([0.2, 10.0, 5.0], device='cuda') criterion = nn.CrossEntropyLoss(weight=class_weights, ignore_index=255)权重值怎么定?一个经验起点是背景权重 0.1~0.3,虚线权重 5~10,实线权重 3~6。背景占比高,权重压低;虚线和实线占比低,权重抬高。但权重不是越大越好:虚线的断口处与背景相邻,权重过大会让模型为了压低损失,把断口处强行补成线,结果虚线连成实线,实线反而出现毛刺。这个我在避坑章节里会单独展开。
优化器和调度器用下面的配置:
optimizer = torch.optim.AdamW(model.parameters(), lr=2e-4, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.PolynomialLR(optimizer, total_iters=60, power=0.9)AdamW 在语义分割里的收敛速度通常优于 SGD,配合多项式衰减能稳定收尾。初始学习率 2e-4 对 ResNet50 作为骨架的 DeepLabv3+ 来说是安全区间,batch size 建议 4 到 8,这个配置在单张 3080 级别显卡上可以跑动。
3.3 训练主循环与保存策略:每训练几轮就存一次断点
训练主循环:
def train_one_epoch(model, loader, optimizer, criterion): model.train() total_loss = 0 for imgs, masks in loader: imgs = imgs.cuda() masks = masks.cuda() optimizer.zero_grad() out = model(imgs)['out'] loss = criterion(out, masks) loss.backward() optimizer.step() total_loss += loss.item() * imgs.size(0) return total_loss / len(loader.dataset) for epoch in range(60): loss = train_one_epoch(model, loader_train, optimizer, criterion) scheduler.step() print(f'epoch {epoch:03d} loss {loss:.4f}') if epoch % 5 == 0: torch.save(model.state_dict(), f'checkpoints/lane3cls_epoch{epoch}.pth')model(imgs)['out']是针对 DeepLabv3+ 这类检测头的写法,输出字典里out才是分割 logits。每 5 个 epoch 存一次断点,不是为了省存储,而是万一后期 loss 震碎,还能回退到中间检查点。1300 张图训练一个 epoch 很快,不要每轮都存,浪费磁盘也干扰判断。
训练时一定用 ImageNet 预训练权重初始化骨架,随机初始化在 1300 张的小数据集上非常容易陷入不收敛。加载权重后先冻结骨架跑 5 个 epoch,只训分割头,等 loss 稳定后再解冻全部层,这是我在小数据集上常用的稳妥手法。
4. 自动驾驶路况对数据集的苛刻要求:光照、磨损与动态遮挡
1300 张图看起来数量不小,但自动驾驶路况的分布远比想象中宽。同一段路,白天、夜晚、逆光、雨天、树荫遮挡,成像差异巨大。而很多公开车道线数据集的采集条件和标注规范,并不一定覆盖这些极端情况。
如果整个自动驾驶决策链路还想把决策延迟压进 32.8 毫秒量级,车道线分割的前端感知必须足够鲁棒。这章从光照、磨损、遮挡三个角度讲怎样评估并补强这套数据集。
4.1 白天训练、夜间失效:亮度扰动是第一类增强
一个在晴天中午采集的数据集,模型在白天测试集上 mIoU 可能到 0.7,但一上夜路立刻掉到 0.3。这不是模型结构问题,而是亮度分布完全错位。夜间的车道线亮度低、对比度差,自车大灯照射距离有限,图像前亮后暗,模型根本没在训练时见过这种全局梯度。
应对手法是亮度对比度增强。用 albumentations 实现:
import albumentations as A import cv2 train_transform = A.Compose([ A.RandomBrightnessContrast(brightness_limit=0.35, contrast_limit=0.3, p=0.8), A.GaussNoise(var_limit=(10.0, 50.0), p=0.5), A.RandomShadow(shadow_roi=(0, 0, 1, 1), num_shadows_limit=2, p=0.3), A.ShiftScaleRotate(shift_limit=0.05, scale_limit=0.05, rotate_limit=5, border_mode=cv2.BORDER_CONSTANT, value=0, p=0.5), A.HorizontalFlip(p=0.5), ])RandomBrightnessContrast把亮度范围拉宽,模拟从清晨到黄昏再到夜晚的不同曝光。RandomShadow在图像上叠加阴影块,模拟桥洞、树荫和高架遮挡。这两个增强是车道线分割最实用的两个操作,比盲目加模糊效果好得多。
4.2 车道线磨损与遮挡:数据集里的“隐形类别不平衡”
另一个比亮度更隐蔽的问题是车道线本身的磨损和污渍。旧路面的实线可能断成好几段,虚线可能只剩一小截。标注时如果只按“能看清的线”标,那磨损部分会被标成背景,模型学到的是“只有高对比度的线才是线”,这与真实路况严重不符。
自动化驾驶场景里,磨损线的像素量和完整线完全不是一个量级,这构成了语义层面的“隐形类别不平衡”。处理手段不是单纯调权重,而是要在增强里加入降质操作:对图像局部做高斯模糊、降低对比度,或者给线区域加少量噪声,让模型学会从纹理线索中恢复线形,而不是依赖明亮边界。
如果模型在验证集上对磨损段的召回率明显低于完整段,可以用图像形态学方法做后处理,但治本手段还是在增强阶段补足低对比度样本。别指望只用 1300 张原始图就能覆盖所有路面状况,增强是必须做的。
4.3 用增强把1300张“扩”到能用:代码与取舍
直接用上面的train_transform每轮生成不同样本,一个 epoch 里模型看到的有效变体数能扩大几十倍。这套增强里有几个取舍值得注意。
第一,HorizontalFlip对语义分割是安全的,因为背景、虚线、实线的类别语义不会因左右翻转而改变,这一点和实例分割不同,实例分割翻转后标注框和 mask 也要跟着翻。第二,ShiftScaleRotate的border_mode用BORDER_CONSTANT并把填充值设为 0,这样图像周围补的是黑色像素,对应掩码补的是背景类别,语义一致。第三,不要加入垂直翻转。车道线照片有明确的上下结构,车头方向、地平线位置都是强先验,垂直翻转会让模型看到车道线挂在天空上的荒谬样本。
裁剪也要谨慎。车道线是长条结构,随机裁剪很容易裁掉半根线,破坏连续性。我更推荐全图 resize 到固定尺寸而非随机裁剪,必要的时候只做水平平移,不做垂直平移。
5. 车道线分割避坑记录:5条从训练到验证踩出来的问题
下面这些坑,不是从文档上看来的,是真实训练里踩过后又排查出来的。每一条都按现象、原因、解决三个层次写,对应训练环节里最容易翻车的五个位置。
5.1 标签和原图对不齐:训练曲线好但边界全花
- 现象:训练 loss 稳步下降,验证 mIoU 也不差,但把预测结果和原图叠加后发现,车道线预测区域整体偏移了几个像素,虚线断口位置错位,边界模糊。
- 原因:标签和原图不是严格对齐的。有些数据集的图像经过畸变矫正或裁剪,但标签没有做同等变换,或者标注导出时分辨率设置不一致。CNN 训练时会强制 resize,把像素级误差掩盖了,但推理时面对原始比例图像,错位立刻暴露。
- 解决:先写脚本随机抽取 10 组图,将原图和掩码按 50% 透明度叠加,人工看对齐情况。再检查掩码尺寸是否与原图一致,不一致就用最近邻插值重采样修正。
5.2 虚线权重设太高:断口被补成实体线
- 现象:虚线的断口处被模型预测成实线,整条虚线变成一根连续长条,验证集上虚线 IoU 很高,但实线上出现毛刺,整体车道形态失真。
- 原因:类别权重失衡。虚线像素占比少,为了拉高它的损失权重,把断口处也压向了虚线类。网络学习到“紧邻线段的区域大概率也是线”,于是把断口填满。
- 解决:降低虚线的权重比,从 10 降到 6 左右,并调高背景权重到 0.3,让断口处保持背景。训练后用形态学开运算清洗预测结果,去掉细小的误检连接。
5.3 连续帧混进训练验证集:指标虚高近说谎
- 现象:训练时验证 mIoU 高达 0.85,但把它部署到另一段完全没见过的路上,性能骤降到 0.5。
- 原因:1300 张图如果能按连续帧提取,那么相邻帧之间高度相似。随机划分训练验证集时,同一路段的连续画面同时出现在两端,模型相当于提前见过“正确答案”,验证集失去意义。
- 解决:划分数据必须按视频片段或场景段进行,不能按帧随机。同一段连续画面全部进训练集或全部进验证集,确保验证集都是新鲜场景。这个环节直接影响你对模型实际能力的判断。
5.4 随机初始化在1300张上不收敛
- 现象:模型从头训练,loss 下降缓慢,到第 30 个 epoch 仍在高位震荡,验证集几乎输出全背景。
- 原因:语义分割网络的参数量远大于 1300 张图能提供的信息量,随机初始化让模型无法在有限样本上建立有效的特征提取器。
- 解决:加载 ImageNet 预训练权重的骨干网络,冻结前几层,先训练分割头。等 loss 下降到平稳后,再解冻全部层继续微调。如果显存紧张,换轻量骨架如 MobileNetV3 也能跑出可用结果。
5.5 只看mIoU不看边界质量:指标好看但实车没法用
- 现象:验证集 mIoU 在 0.72 左右,看起来达标,但实车测试时车道线预测结果断断续续,曲线路段尤其严重。
- 原因:mIoU 对整体像素覆盖敏感,但对车道线这种细长结构的边界完整性不敏感。细线区域只要偏移一个像素,IoU 可能掉几个点,但视线连续性对下游规划更重要。
- 解决:增加边界 IoU 作为辅助指标,即只计算车道线边缘像素的预测准确率。同时人工抽查连续多帧预测结果,关注线段的连续性和断口位置是否合理。指标不是越多越好,但边界质量必须在验收清单里。
6. 验证这1300张数据,把主动学习用起来再榨一轮
模型训完,评估指标算完,不代表工作结束。我的一般习惯是先把预测结果可视化出来,用最笨的办法检查,再考虑用主动学习补一轮数据。这两个步骤,往往比调模型结构更能提升最终效果。
6.1 用九宫格可视化验证:比单看指标可靠
把原图、标签、预测结果横向排在一起,自己看 10 组以上:
import matplotlib.pyplot as plt import torch pred = torch.argmax(out, dim=1).cpu().numpy().squeeze() gt = masks.cpu().numpy().squeeze() fig, axes = plt.subplots(1, 3, figsize=(15, 5)) axes[0].imshow(img) axes[1].imshow(gt, cmap='gray') axes[2].imshow(pred, cmap='gray') plt.show()重点看三件事:虚线断口是否保持断开;实线是否连续;阴影和路面边缘有没有被误判为线。这三类错误在 mIoU 上不一定体现得很明显,但人眼一扫就能判断模型学到了什么。
6.2 主动学习:用低置信度样本补标最有价值
1300 张图用完以后,如果需要继续提升,我一般不会盲目采集更多数据。先把手头未标注的图像过一遍模型,用预测熵找出模型最不确定的样本,只补标那些图。
with torch.no_grad(): prob = torch.softmax(out, dim=1) entropy = -(prob * prob.clamp(min=1e-8).log()).sum(dim=1) hard_score = entropy.mean(dim=(1, 2)).cpu().numpy() hard_idx = np.argsort(hard_score)[-200:]hard_score越高,表示模型对这一帧的类别分配越没有把握。挑出分值最高的 200 张,人工补标后加入训练集。这套流程比随机扩充数据效率高很多,成本集中在标注上,花得更值。
在我自己处理车道线项目时,最后养成的习惯是永远留一个验证用的连续路段,不上测试集,只用来做反复可视化检查。模型参数可以反复调,但留出来的这段路不能动,动一次指标就脏一次。路况的多样性永远比模型结构重要,数据集的清理和补强,才是精度提升的真正杠杆。
希望帮到你。
本文还有配套的精品资源,点击获取