尧图精选

车道线语义分割实战:1300张图的小样本训练与避坑指南

🕒 发布时间:2026/10/2 14:28:03 📁 来源:尧图网络
简介面向自动驾驶感知与语义分割任务该数据集包含约1300张真实道路场景图像及对应掩码标签覆盖背景、虚线、实线三个类别适用于车道线检测与分割模型的训练和验证。数据已按训练集和验证集划分训练集约1200张验证集约120张并提供classes文件说明类别。压缩包共2000个文件以PNG掩码和JPG原图为主另含1个类别说明txt和1个可视化脚本py整体约146.87MB可直接用于主流分割网络的数据加载。可视化脚本可随机抽取样本将原图、GT掩码及叠加效果一并展示便于快速检查标注质量掩码与原始图像一一对应方便构建数据管道。这一资源能帮研究者省去数据采集与标注时间适合自动驾驶、智能交通领域的学生或工程师用于模型实验、算法比较与论文复现。目前已有302人学习下载。1. 车道线语义分割数据集1300张图能支撑什么不能支撑什么图像分割里的语义分割任务落到自动驾驶上最典型的场景就是车道线检测。这个数据集不是用检测框把车道线框出来而是把虚线、实线、背景三类逐像素分类一张原图配一张同尺寸的标签图总共约1300组。这个规模离量产自动驾驶数据集差着两个数量级但作为入门、原型验证和比赛预研它足够把整套语义分割pipeline跑通数据检查、标签规范、模型训练、指标评估、缺陷分析。适合三类人做毕业设计的学生、刚转岗分割方向的新手工程师以及想评估“只有像素标注、没有实例ID”到底能不能满足车道线场景的算法负责人。我下面按自己拿到这类数据后的处理顺序来讲从验证标签到训练配置再到那些单看指标根本发现不了的坑。2. 三类分割标签体系虚线、实线与背景的边界究竟怎么定义拿到一套数据集先别急着开训练第一步永远是搞清楚标签语义。很多人对“3类分割”的理解不准确上来就把它当成普通二分类的“车道线/非车道线”结果训练完才发现下游根本没法用。这一章把标签定义讲透。2.1 为什么是“3类”而不是“线/非线”两类或者“实例级”一类语义分割算法回答的问题是“每个像素属于哪个类别”和实例分割的区别在于不区分“第几条线”。车道线被前车遮挡、被阴影切断时同一条线会断成好几段实例分割还得操心每段的ID是不是同一条语义分割根本不管这些它只负责把像素归到背景、虚线、实线里的某一类。这个任务定义简单所以自动驾驶里车道线检测大量采用语义分割思路先分像素再按连通域做后处理。虚线和实线为什么必须拆开因为下游决策完全不同。虚线区域允许变道实线禁止压线如果标签只写“车道线/非车道线”下游规划模块还得再猜线型猜错就是变道违规。把线型编码进标签是语义分割数据集制作阶段就能定下来的事成本最低收益最大。所以这份数据集的“3类”不是拍脑袋定的它对应的是自动驾驶规划模块的真实输入需求。2.2 标注规范与文件命名一张图配一张同名的单通道mask这类数据集常见的组织方式是两个目录images里放原图masks或labels里放同名PNG标签图。标签图一般有两种存法拿到手必须先确认是哪一种。第一种是灰度单通道像素值直接用0、1、2代表背景、虚线、实线这是最干净的形式代码里读进来就是索引直接扔给交叉熵损失。第二种是RGB调色板用红色画虚线、蓝色画实线、黑色当背景这主要是标注工具导出的默认格式训练前必须转成单通道索引否则模型会把它当成三通道颜色去拟合等于白白浪费了通道容量。另外还要确认mask里允不允许出现255这类“空洞值”。有些标注工具在边缘会产生抗锯齿过渡像素导出的PNG里会出现不是0/1/2的数值。严格说这是脏标签轻则让loss出现莫名波动重则让模型在边缘学到错误边界。正规做法是在数据加载阶段加一行保护把大于2的像素全部归为背景同时统计这类像素占比超过一定比例就说明标注导出有问题需要重新生成。还有一类参数要留意图像分辨率是否统一。1300张图如果来源不同可能有1920×1080也有1280×720这会影响后面的数据增强策略。我见过不少人卡在尺寸不统一上DataLoader里一报错就手动resize结果把细线resize没了还不知道这里先埋个伏笔第4章具体说。2.3 用20行脚本验收一套车道线分割数据集拿到数据先写个小脚本做体检把每张图的类别占比和异常像素全打出来。我一般会先跑这个再决定要不要动数据。脚本逻辑不复杂重点是检查三件事类别占比是否合理、有没有超出0/1/2的像素、虚线和实线的比例是不是严重失衡。import cv2 import numpy as np from pathlib import Path mask_dir Path(datasets/masks) class_names {0: 背景, 1: 虚线, 2: 实线} for mask_path in sorted(mask_dir.glob(*.png)): mask cv2.imread(str(mask_path), cv2.IMREAD_GRAYSCALE) unique, counts np.unique(mask, return_countsTrue) total mask.size ratio {int(k): float(v) / total for k, v in zip(unique, counts)} # 检查类别是否干净 bad_pixels [k for k in ratio.keys() if k not in (0, 1, 2)] # 检查线体是否少得反常 lane_ratio ratio.get(1, 0) ratio.get(2, 0) flag if bad_pixels: flag f[脏像素:{bad_pixels}] if lane_ratio 0.01: flag [车道线占比过低] print(f{mask_path.name}: 虚线{ratio.get(1,0):.4f} f实线{ratio.get(2,0):.4f} 背景{ratio.get(0,0):.4f} {flag})这段脚本的用途不是跑一遍就完而是把输出重定向到文件里逐个看。重点看两类样本车道线占比低于1%的图说明要么是场景里本来就没线要么是标注漏了带脏像素标记的图说明标注规范执行得有问题。如果一套1300张的数据里超过50张都有脏像素我会要求先清洗数据再训练省得后面排查时怀疑人生。类别占比的正常范围大概是背景92%-98%虚线和实线各占1%-4%。虚线占比明显低于实线也是常事因为虚线一条条有间隔画出来天然比实线稀疏这种类别不均衡到后面训练时要靠损失函数拉回来这里先留个印象。3. 用1300张图训练车道线分割模型目录、增强、参数与评估标签验完了接下来进入正题怎么把这1300张图变成能用的分割模型。这一章按我的落地顺序写从目录整理、数据增强、模型选型到训练参数和评估指标每个环节都有可以直接抄的代码和参数。3.1 目录整理与训练验证划分小样本更要把验证集当回事处理数据集用于语义分割训练第一步是把目录整理成模型和DataLoader都能直接吃的结构。常见做法是这样datasets/ ├── train/ │ ├── images/ │ └── masks/ ├── val/ │ ├── images/ │ └── masks/ └── test/ ├── images/ └── masks/划分比例我一般用8:1:1但小样本场景下更推荐8:2的“训练验证”两分法把测试集并进验证集。因为1300张里真正覆盖到的场景不多验证集留20%已经够评估稳定性了再从训练里抠出10%做测试反而让训练集更紧张。划分代码很简单但有两个点容易被忽略一是随机划分前一定要固定seed否则每次跑出来的结果都不一样后面调参对比就乱了二是划分时要确保同一采集场景的连续帧不要同时落在训练集和验证集里否则验证指标会虚高。import random from pathlib import Path random.seed(2024) img_root Path(datasets/images) mask_root Path(datasets/masks) all_names sorted(p.stem for p in img_root.glob(*.png)) random.shuffle(all_names) n_val int(len(all_names) * 0.2) val_names set(all_names[:n_val]) train_names set(all_names[n_val:]) for name in train_names: img_root.rename(Path(fdatasets/train/images/{name}.png)) mask_root.rename(Path(fdatasets/train/masks/{name}.png)) # val 同理这里按需复制或移动注意这里用的是rename生产环境里更推荐copy而不是move因为后面重新洗数据时原文件保留着还能再划分一次。移动完原目录没了想换个划分比例就得重新解压源文件这是自己给自己找麻烦。3.2 数据增强小样本的关键但第一条纪律是别改语义1300张图对深度学习来说属于小样本不做好增强模型很快就把训练集里的路面纹理背下来了。但车道线分割的增强有一条纪律增强不能改变类别语义。具体来说像RandomErasing这种随机抹掉一块的操作坚决不用——它可能正好把车道线抹掉强制模型去学“没有线的地方也能有线的预测”像错位、扭曲很强的增强也要谨慎评估车道线是刚性结构扭曲后虚线实线的几何特征全变了。我常用的增强组合是albumentations库写的直接贴配置import albumentations as A train_transform A.Compose([ A.RandomCrop(512, 512, p1.0), A.HorizontalFlip(p0.5), A.RandomBrightnessContrast( brightness_limit0.2, contrast_limit0.2, p0.5 ), A.ColorJitter( brightness0.2, contrast0.2, saturation0.2, hue0.05, p0.3 ), ])RandomCrop而不是直接整图resize是被很多人都忽略的关键决定。1920×1080的图直接压到512×512远处车道线只有1到3个像素一压就没了。随机裁512×512局部区域相当于让模型既见过近处粗线也见过远处细线训练样本的信息量比整图resize大得多。HorizontalFlip对车道线是安全的虚线翻过去还是虚线实线同理左右车道线都能学。亮度对比度抖动用来模拟阴影和逆光对这类场景最实用。验证集上不要做任何随机增强这是规矩。最多做一个中心裁剪保证和训练图像尺寸一致。3.3 模型选择与训练配置从轻量backbone到损失函数模型选择上有个反直觉的建议小样本别用太强的backbone。ResNet18起步就够了或者MobileNetV3加分割头。backbone太强比如ResNet101或Swin Transformer训练集只有一千来张时模型很快学会“背图”验证集mIoU看起来还行换一段新场景立刻现原形。轻量模型加合理增强泛化反而更好。我用PyTorch把数据集读取和损失函数这一段写完整训练循环就是常规写法不重复import torch import torch.nn as nn from torch.utils.data import Dataset import cv2 import numpy as np from pathlib import Path class LaneSegDataset(Dataset): def __init__(self, img_dir, mask_dir, transformNone, size(512, 512)): self.imgs sorted(Path(img_dir).glob(*.png)) self.masks sorted(Path(mask_dir).glob(*.png)) self.transform transform self.size size assert len(self.imgs) len(self.masks), 图和标签数量不一致 def __getitem__(self, idx): img cv2.imread(str(self.imgs[idx])) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img cv2.resize(img, self.size, interpolationcv2.INTER_LINEAR) mask cv2.imread(str(self.masks[idx]), cv2.IMREAD_GRAYSCALE) # 这里必须用最近邻插值线性插值会产生小数类别 mask cv2.resize(mask, self.size, interpolationcv2.INTER_NEAREST) mask[mask 2] 0 # 脏像素保护 if self.transform is not None: aug self.transform(imageimg, maskmask) img, mask aug[image], aug[mask] img torch.from_numpy(img.transpose(2, 0, 1)).float() / 255.0 return img, torch.from_numpy(mask).long() criterion nn.CrossEntropyLoss( weighttorch.tensor([1.0, 10.0, 10.0], devicecuda) )两个点值得展开。第一mask的resize必须用INTER_NEAREST。用线性插值会把0/1/2之间的边界插出0.5之类的数值虽然读取时代码把小数转成了long型整数但类别边界已经错了训练出来的线边缘会重影。第二交叉熵的类别权重。背景权重设为1虚线和实线设为10是因为线体像素占比太低不给权重的话模型输出全背景也能拿到很低的loss。权重具体数值不用一步到位可以先从10起步训练完看虚线IoU如果发现模型把路面裂缝误判成虚线说明权重给大了降到5再试。训练参数照着下面跑基本不会翻车输入分辨率512×512batch size 8优化器AdamW初始学习率1e-3带5个epoch的warmup之后cosine退火总共训练90个epoch。上面这个配置在单张消费级显卡上跑完大约一小时内这份数据集规模正合适。如果训练时显存不够先把batch size降到4学习率也要跟着降到5e-4batch size和learning rate是联动的只降batch不降lrloss会震荡。3.4 评估指标mIoU之外务必看每一类的IoU不少人训练完只看一个mIoU0.9就觉得模型很好了。这在车道线分割任务里是个大坑。一张图里背景占95%以上模型把背景全预测对把虚线和实线全漏掉mIoU仍然可能很高。真正要盯的是Per-class IoU尤其是虚线IoU和实线IoU。每类IoU的算法就是常规语义分割评估IoU_class (pred class AND mask class)的像素数 / (pred class OR mask class)的像素数在代码里把每个类别的IoU打出来合格的验收线一般是背景IoU大于0.9虚线IoU大于0.6实线IoU大于0.7。如果虚线IoU只有0.3说明模型几乎没学到虚线这时候去看预测图多半是虚线断成碎点或者全被预测成背景。只报mIoU不报每类IoU的要么是外行要么是想好看。还有一个我每次都会做的验证把预测结果叠在原始图像上导出成图。可视化不是锦上添花是排查问题的主力手段。我习惯把背景设为原图虚线画绿色实线画红色然后挑验证集里最容易翻车的那些样本逐张看。很多问题只有看到图像才想得通比如“虚线IoU低是因为虚线区域被树影盖住了”这种结论只看数字永远得不到。4. 车道线分割数据集避坑记录5个让模型翻车的常见问题这一章是把数据训练和评估里最常见的坑集中踩一遍。每条按“现象、原因、解决”来写都是我实际见过的翻车场景无一例外。4.1 类别严重不平衡模型学成了“永远输出背景”现象训练loss正常下降验证loss也在降mIoU报出来0.9以上但保存的预测图里车道线全没了路面干干净净一片背景。再看细一点虚线实线的类别IoU只有0.1上下。原因背景像素占比超过95%普通交叉熵被背景主导模型发现“全部预测背景”的损失已经很低没有动机去学那1%的线体。mIoU高是背景撑起来的掩盖了真实问题。解决给类别加权我通常在CrossEntropyLoss里设背景权重1.0、虚线权重10.0、实线权重10.0。加完权重后再看loss如果训练损失比之前明显高说明模型开始被迫关注线体像素了这是正常现象。如果模型疯狂把车道线预测成背景但代价变成了把路面纹理误判成虚线权重回调到5。另外可以尝试将Dice Loss和CrossEntropy组合使用Dice Loss对前景小目标更敏感但调起来比单纯加权麻烦新手先用权重方案。4.2 resize导致细线在标签里消失现象原图是1920×1080训练时直接resize到512×512出来的预测图近处车道线还算完整远处消失点附近全是断点甚至一整段虚线凭空没了。检查数据集的时候线还在一看预测图线就短了一截。原因透视关系下远处车道线在图像里只有1到3个像素宽。整图resize是把图像等比压缩远端的细线在缩放过程中被平均掉标签也跟着从“虚线”变成了“背景”。这种情况下无论怎么调模型都补不回来因为监督信号本身已经丢了。解决训练和推理都不用整图resize。训练时用随机裁剪从原始分辨率的大图里裁512×512的块推理时用滑窗拼接把大图切块预测再合并结果。如果随机裁剪导致样本里近处粗线占比过高可以按“远、中、近”三个区域分别设置裁剪比例比如近处40%、中间40%、远处20%让模型三种尺度都见过。这个坑是最不好排查的因为loss不长异常mIoU也正常只有把预测图放大盯远处才能发现。4.3 标注风格不统一有人在画“线体”有人在画“骨架”现象训练出来的车道线分割结果比原图里的线明显粗一圈或者线的边缘模模糊糊像套了一层虚影。验证时发现同一张图里两条相邻车道线一条预测得准一条偏粗。原因语义分割数据集制作时没有统一标注规范。一部分标注员画的是车道线的完整宽度也就是“线体”另一部分只描了一根2像素的“骨架”。两种风格混在一起模型学到的是“大概这么粗”而不是确定的标准。解决数据验收阶段做线宽统计。遍历所有mask把每个连通域的宽度分布打出来正常车道线的标注宽度应该在一个合理区间里波动比如8到15像素。如果有的连通域只有2像素有的却有30像素这就是标注风格漂移了。解决方式不是重新标1300张而是把风格偏掉的样本筛出来找工具统一重画一遍大约会占20%到30%的量。这类问题只能靠人眼看图发现我一般把mask二值化后叠在灰度原图上用半透明方式快速滚动排查。4.4 光照和阴影验证集在阴天正常一到逆光就翻车现象白天正常光照的验证集上模型mIoU稳定在0.85以上把一段逆光或树影严重的视频喂进去虚线几乎消失实线只剩靠近车头的一段远处全被预测成背景。原因训练集里光照分布太偏。白天顺光的样本占了绝大多数模型学到的是“高对比度下的车道线”而不是“车道线这个结构本身”。阴影里车道线对比度低模型的边缘响应弱自然就漏了。数据增强里的亮度对比度抖动有作用但增强只能加“扰动”改变不了分布缺口。解决按采集时段和场景去补数据。理想的比例是白天顺光50%、阴影15%、逆光20%、黄昏10%、夜间5%1300张里如果阴影和逆光加起来不到50张模型必翻车。补数据优先补逆光和阴影这两类样本对泛化影响最大。如果暂时补不了数据就把增强里的亮度扰动范围调大从0.2放到0.3同时加一个CLAHE预处理把局部对比度拉起来算是缓解手段但不是根治。4.5 预测断线后盲目做闭运算虚线被粘成了实线现象预测出来的实线在阴影处断成几截于是做了个3×3或5×5的形态学闭运算想接上。结果断线确实接上了但虚线也被闭运算连成整根实线下游规划模块看到一整排实线判定全程禁变道路径规划变得保守。原因闭运算是先膨胀后腐蚀对相邻碎片按结构元尺寸连接它不区分“虚线间隔”和“断线裂缝”。竖着的虚线像素间距本来就小闭运算一膨胀间隔就被填上了虚线语义被破坏。解决后处理不要用通用的大核闭运算。要么不做闭运算改用连通域分析只把距离小于阈值、方向一致的碎片连接起来要么用1×3或1×5的定向核只沿车道线的走向方向连接避免横向把相邻虚线块粘一起。还有一个更简单的办法分割模型输出后不直接二值化而是保留每个像素的概率值用动态阈值按连通域过滤这样断线的召回率会好很多代价是多写一点后处理代码。5. 从单帧指标到连续帧验证车道线分割真正该盯的指标5.1 一帧准不代表连续稳单帧mIoU高连续帧之间照样可能闪。车道线是连续结构相邻两帧的预测结果不该剧烈变化但模型可能对这一帧的树影敏感下一帧树影没了预测就来回跳。这种抖动在单帧指标上完全看不出来对自动驾驶却是致命问题。我常用的验证方法是从验证集里按时间顺序抽连续5到10帧逐帧推理算相邻帧预测结果的差异比例change_ratio (pred_t ! pred_{t1}).float().mean().item()差异比例稳定在10%以下才说明模型学的是结构超过15%就要警惕过拟合纹理了。5.2 把预测结果滚动播一遍比任何指标都直观的做法是把验证集预测结果按帧合成一段视频和原图并排逐帧播放。我用ffmpeg把推理结果输出成mp4滚动看三遍比看十张静态图有用得多。跳变、断线、误检全部是无处可藏的。我现在拿到任何分割数据集的第一反应就是把预测结果滚动播放一遍再回头看指标。5.3 我的收尾建议1300张图能不能投入我的结论是能但前提是你把它当pipeline验证集而不是量产训练集。用这套数据把数据检查、训练、评估、连续帧验证的流程全部跑顺调参经验和踩坑记录都积累下来后面再到5万张标注数据时你就不会慌因为该踩的坑你已经踩过一遍了。如果真要往自动驾驶决策上用我还会再补夜间和雨天样本等连续帧验证稳定了才松口。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →