尧图精选

高分辨率车道线语义分割实战:从数据集处理到模型训练避坑指南

🕒 发布时间:2026/10/1 5:35:19 📁 来源:尧图网络
简介这是一份面向计算机视觉与自动驾驶研究者的高分辨率高速车道线图像语义分割数据集包含6类分割标签背景、白实线等已按约2000张训练集与800张测试集划分完毕可直接用于分割模型训练与评估适合具备一定深度学习基础的学习者进行车道线检测实践。资源共约2000个文件压缩包大小约430MB其中png/jpg图像对应原始图片与mask标签另有classes类别说明txt以及一个可视化Python脚本便于快速查看原图、GT及叠加蒙版效果。目前已有152人学习该资源。配套的可视化脚本可随机抽取图片展示原始图、真实分割标签及原图蒙版叠加结果帮助读者直观核对标注质量若需进一步改进分割网络还可结合作者专栏中的UNet、SwinUNet、TransUNet等模型思路形成从数据到模型训练的完整闭环。1. 高分辨率车道线语义分割数据集2,800张能做什么、不能做什么做高速车道线感知时碰到一个高分辨率图像语义分割数据集6个分割类别约2,800对图像和标签。这个规模放到语义分割领域不算大但胜在场景聚焦整批数据都来自高速车道线比一次性丢给你十二万张杂乱图片更适合快速验证算法。它能解决的是车道线像素级归属问题哪些像素是实线、虚线、双黄线、路缘还是背景而不是目标检测那种画框粒度。适合刚入门图像分割数据集制作、正想跑通语义分割pipeline的工程师也适合需要用真实数据测试语义分割模型差异的选型阶段。它不能做的是不增强、不调loss、不处理类别不平衡就指望它直接部署到夜间匝道或雨天反光场景。这个容量决定了它更适合当预训练底座或迁移学习起点。2. 把原始图像与标签整理成可训练的数据集目录结构与标签校验2.1 先看清目录结构别急着写训练代码很多人拿到分割数据集的第一步就是写DataLoader结果训练时才炸出各种文件错位。我一般先花五分钟做目录体检。假设你解压后的目录长这样lanedata/ images/ 00001.jpg 00002.jpg labels/ 00001.png 00002.png第一件事是统计文件数和后缀ls images | wc -l # 期望约2800 ls labels | wc -l # 期望约2800 file images/00001.jpg labels/00001.png这一步跑完能发现常见的文件缺失、后缀带大写、标签是jpg压缩图等问题。jpg做语义标签是灾难压缩会让边缘产生混叠等训练完你会发现车道线边缘像毛玻璃。紧接着写一个Python脚本读一批图像确认尺寸、通道、标签的像素值分布from PIL import Image import numpy as np import os img_root lanedata/images lab_root lanedata/labels imgs sorted(os.listdir(img_root))[:20] for name in imgs: img Image.open(os.path.join(img_root, name)) lab Image.open(os.path.join(lab_root, name.replace(.jpg, .png))) print(name, img.size, img.mode, lab.size, lab.mode) arr np.array(lab) print( label unique:, np.unique(arr))这段代码的作用是摸清图像尺寸是否全图统一、标签是单通道索引图还是三通道伪彩色图。数据集一般会在说明文档里写清每类像素值的对应关系但还是要以实际读出来的unique值为准。如果unique值是[0,1,2,3,4,5]那恭喜标签已经是索引图可以直接当成模型输出通道数来用。如果unique值得到[255,128,64]这种灰度值或者三通道RGB值你需要做一次映射把像素值归一到0~5的类别编号。我见过有人把标签原样读成RGB三通道喂给模型loss直接原地爆炸因为模型输出6个通道而标签是3通道。这不是算法问题是数据结构没对齐。图像分割数据集制作规范里标签必须是单通道索引图每个像素值代表一个类别不能是调色板展开后的彩色图。2.2 统一尺寸与切patch高分辨率不是直接塞GPU的理由这批数据叫“高分辨率”但具体多高要实测。如果是2000x1000乃至4000x3000直接把整图放进网络是不现实的显存受不了。常见做法是切patch训练、整图推理。对车道线这种细长物体切patch时要注意不能让一条完整的线被切得支离破碎。我先给一个统一resize脚本把图像和标签同步缩放到合适范围比如高度1024、宽度2048。关键点标签缩放必须用最近邻插值否则类别边界会被插出过渡灰度值。from PIL import Image def resize_pair(img, lab, target(2048, 1024)): img img.resize(target, Image.BILINEAR) lab lab.resize(target, Image.NEAREST) # 标签绝不能用线性插值 return img, lab # target传(w,h)对应PIL的size这里的target传的是(width, height)PIL里是图像尺寸不是numpy的shape。为什么标签用NEAREST因为双线性会在类别边界产生中间值比如0和1之间插出0.5而语义分割的交叉熵loss期望标签是整型类别。一旦出现中间值CrossEntropyLoss会把它当成另一个类别或者在你做one-hot转换时直接报错。这是高分辨率数据里最常踩的坑。如果你不想整体缩放也可以做滑窗切patch。切patch时要保证图像和标签裁剪坐标完全一致起点坐标取整除32的倍数这样后面网络下采样时不会产生对齐偏差def crop_pair(image, label, origin, size): x, y origin w, h size return image[y:yh, x:xw], label[y:yh, x:xw]传入的origin坐标最好都是32的整数倍。比如800x600这种非对齐尺寸会导致下采样后的特征图分辨率对不上标签。很多人不以为然最后评估mIOU时发现整体偏低找半天想不到是这个原因。高分辨率图像尺寸大一味的resize会丢失细线信息所以patch大小建议在512x512到896x1024之间保证每条车道线在patch里至少延续200个像素。2.3 划分训练集和验证集别用随机划分把场景打散2800张不算多划分方式直接影响验证集的可靠性。最简单是随机按比例划分但对车道线场景我建议按路段或拍摄序列分组。如果数据是按连续视频帧抽的前后帧高度相似随机划分会把同一段路的图像同时塞进训练集和验证集让验证分数虚高。具体操作是看一眼文件名前缀。比如highway_001_01.jpg下划线前的highway_001是序列号按序列号划分而不是按单帧划分。import os import random from collections import defaultdict img_root lanedata/images seq_map defaultdict(list) for name in os.listdir(img_root): seq name.split(_)[0] seq_map[seq].append(name) seqs list(seq_map.keys()) random.seed(42) random.shuffle(seqs) split int(len(seqs) * 0.8) train_seqs, val_seqs seqs[:split], seqs[split:]这段代码把每个序列作为一个整体分配确保验证集与训练集的场景重合度低。如果你确认这批数据是互不关联的单帧图那用随机划分也没问题。划分完后生成train.txt和val.txt每行写相对路径后续DataLoader直接读这两个文件避免每次启动都重新遍历目录。此时才算把数据交给模型。不要跳过标签校验这一步我后面会专门列避坑章节但这里先记住珍惜一次人工可视化检查的20分钟能帮你省下两个调试的晚上。3. 选择语义分割模型从U-Net到DeepLabV3的选型与训练配置3.1 车道线为什么必须用语义分割而不是检测框有人会问既然只关心车道线能不能用目标检测的YOLOv8-seg之类YOLOv8-seg做的是实例分割或者带mask的检测车道线在图像里往往横贯整幅而且同一条虚线被车头遮挡后会分成多段实例ID不稳定。语义分割算法处理的是像素级分类不管同一条线分几段只要是同一类别就算对更适合车道线感知。所以这个数据集从一开始就是围绕语义分割设计的后面所有训练配置都按6类分割来写。如果你想让推理速度更快可以用轻量级分割模型但前提是你已经在baseline上验证过类别边界质量。3.2 模型选型编码器选ResNet50还是轻量级Backbone高速场景对精度和实时性都有要求。我常用三个模型做对比模型编码器参数量适合场景显存占用量U-NetResNet50约32M小数据集、细线保留中DeepLabV3ResNet50约40M多尺度物体、边界较好较高SegFormer-B2MiT-B2约24M高效推理、感受野大较低2800张数据量不大直接上Swin-Transformer这类大模型很容易过拟合。我一般会先用DeepLabV3配ResNet50做baseline因为ASPP空洞卷积天然适合高分辨率小目标车道线细长普通下采样会把线缩到只剩几个像素。U-Net优势在于跳跃连接能还原边缘细节但原版U-Net对高分辨率输入的编码效率不如ASPP。SegFormer有分层Transformer特征如果推理设备支持TensorRT也可以做到实时。如果你的部署平台是Jetson Orin这类算力有限的设备就把候选模型定为SegFormer-B2和MobileNetV3-Large做decoder的轻量版本。3.3 最小可训练脚本从DataLoader到fit循环假设你已经把图像和标签整理成了两个文件夹并切成了训练用patch。下面是一个PyTorch训练脚本骨架覆盖数据读取、模型加载、loss和优化器import torch import torch.nn as nn from torch.utils.data import Dataset, DataLoader from PIL import Image import torchvision.transforms as T from transformers import SegformerForSemanticSegmentation class LaneDataset(Dataset): def __init__(self, img_list, img_root, lab_root, size(512, 512)): self.img_list img_list self.img_root img_root self.lab_root lab_root self.size size self.transform T.Compose([ T.Resize(size, T.InterpolationMode.BILINEAR), T.ToTensor(), T.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) def __len__(self): return len(self.img_list) def __getitem__(self, idx): name self.img_list[idx] img Image.open(f{self.img_root}/{name}).convert(RGB) lab Image.open(f{self.lab_root}/{name.replace(.jpg, .png)}) img self.transform(img) lab T.Resize(self.size, T.InterpolationMode.NEAREST)(lab) lab torch.as_tensor(np.array(lab), dtypetorch.long) return img, lab # 以SegFormer为例输出6类 model SegformerForSemanticSegmentation.from_pretrained( nvidia/mit-b2, num_labels6 ) optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay0.01) criterion nn.CrossEntropyLoss(ignore_index255)这个脚本里有几个参数容易让人迷惑T.Resize(size, NEAREST)里的size是(W, H)和PIL的resize行为一致。ignore_index255用来忽略标签里的无效像素如果数据集没有255可以去掉。训练循环就是标准的前向与反向注意要把model.train()和model.eval()切清楚for epoch in range(60): model.train() for img, lab in train_loader: img, lab img.cuda(), lab.cuda() out model(pixel_valuesimg).logits loss criterion(out, lab) optimizer.zero_grad() loss.backward() optimizer.step()SegFormer的model(pixel_valuesimg)返回对象里logits是形状[B, 6, H, W]的分数图已经上采样到输入尺寸。如果你用DeepLabV3或其他库输出接口不同但loss计算逻辑一致。这个脚本跑通后就拥有了一个最小可训练系统后续调参数就往这个框架里填。但注意基线交叉熵在2800张这种小数据集上往往跑不出好效果。如果验证集mIOU一直不动问题很可能出在loss和数据均衡上下一章详细处理。4. 训练过程中的6个关键参数与loss设计让车道线细节不再糊掉4.1 loss组合CE Dice是车道线最优解之一车道线分割最大的痛点是类别不平衡。图像里95%以上像素是背景一条车道线可能只占几十个像素。如果直接用交叉熵模型学到“全部预测背景”就能拿到很低loss验证时mIOU也能有0.8多但可视化全黑。我建议用CrossEntropy和DiceLoss组合公式如下loss 0.5 * CE_loss dice_lossDice计算的是类别预测区域与标签区域的交集比并集对小目标敏感。在PyTorch里可以简单实现def dice_loss(pred, target, num_classes6, eps1e-7): pred pred.softmax(dim1) target_onehot torch.nn.functional.one_hot(target, num_classes).permute(0, 3, 1, 2).float() intersection (pred * target_onehot).sum(dim(2, 3)) cardinality pred.sum(dim(2, 3)) target_onehot.sum(dim(2, 3)) dice (2 * intersection eps) / (cardinality eps) return 1 - dice.mean()这个实现里每个类别都参与dice而不是只算前景类。注意one_hot之后通道在最后一维需要permute到[B, C, H, W]。我把CE和dice取0.5:0.5的权重如果你发现模型预测很激进度比如车道线过粗可以把dice权重调到0.3如果预测太保守断断续续就把dice权重调到0.7。这个比例没有绝对玄学按你视觉评估的结果调。除了dice也有人用focal loss但车道线不是难例问题而是稀缺问题focal的贡献不如dice直接。4.2 学习率策略poly衰减比阶梯式更适合分割语义分割任务的常用学习率策略是poly即学习率随迭代次数指数衰减def poly_lr(optimizer, base_lr, curr_iter, max_iter, power0.9): lr base_lr * (1 - curr_iter / max_iter) ** power for g in optimizer.param_groups: g[lr] lr如果你用阶梯式下降比如每30轮除以10经常会遇到在第31轮loss跳一个尖峰因为后期lr太大会反复跳出收敛区域。poly则平滑过渡配合warmup更好。在训练前10%的迭代里把lr从0升到base_lr可以避免开局过大更新导致标签噪声放大。base_lr我一般设1e-4到3e-4如果用了混合精度可以适当放大到5e-4。这里的max_iter是一次训练的迭代总数每个epoch循环里调用一次函数即可优化器会自动更新参数组里的lr。4.3 输入尺寸与batch size高分辨率要求显存预算这里列一组我实际跑过的组合大家可以参照自己的GPU显存来定GPU显存输入尺寸batch size梯度累积备注8GB512x51242训练慢但能跑12GB896x51241兼顾细线与显存24GB1024x102461推荐配置车道线是细长结构输入高度不要低于384否则一条10像素宽的线缩成2像素网络根本无从学习。我一般会把输入宽度保留在2048甚至4096的patch上用增强来把线宽控制在5到15像素这个区间。如果显存不够梯度累积是一个有效的后悔药它并不改变loss计算方式只是把多个batch的梯度累加后再更新效果近似等价于更大batch。4.4 数据增强翻转、光照以及别做几何扭曲高速车道线有强方向性水平翻转会让左侧虚实线与右侧混淆如果数据集标注时没有区分左右车道线可以开翻转如果区分了必须关闭。这是数据增强里最容易被忽略的细节。我常用的增强有T.RandomHorizontalFlip(p0.5), # 如果左右类别是不同标签则p0 T.ColorJitter(brightness0.3, contrast0.3),不要用随机旋转和透视变换。车道线是刚性直线旋转15度后虚线变斜线与真实分布不符。除非你确认部署场景真有坡度起伏否则这类“过度增强”会让模型学出奇怪幻觉。高分辨率场景里随机裁剪本身就能提供大量位置变化配合光照抖动已经足够。还可以用RandomAffine里的scale-only模式只做轻微缩放保持纵横比。5. 车道线分割典型坑标签错位、类别不平衡与高分辨率显存不足5.1 标签与图像错位导致预测整体偏移现象训练loss正常下降可视化时预测的车道线位置总是比原图偏右或偏下几像素。原因最常见是数据预处理时图像用了CenterCrop标签却在前一步做了RandomCrop或者resize时用opencv的cv2.resize读取BGR标签用PIL读取RGB转来转去宽度对齐错了。另一个隐蔽原因是裁剪坐标没有取偶数或32的倍数在下采样时四舍五入导致特征图与标签分辨率不对齐。高分辨率图特别容易触发这个问题因为像素偏移在放大到1080p后视觉差异非常明显。解决统一图像和标签的预处理管线标签只用NEAREST插值。裁剪坐标取32的倍数。在训练代码里加一个断言assert img.shape[2:] lab.shape[1:], image and label size mismatch用这个断言把同步问题扼杀在数据读取阶段。如果你在验证时发现掩码边缘错位先检查是不是resize参数传反了width和height对调也是常见翻车点。5.2 验证mIOU看着还行可视化却翻车现象mIOU到0.85但画出来的掩码边界全是锯齿车道线断成一条虚线甚至中间有空洞。原因类别不平衡让模型学会了预测连通域中概率较高的区域对细长结构的轮廓不敏感。DiceLoss只能缓解数量级不平衡它自身也偏向形状较大的区域。高分辨率下一条车道线在像素上比背景少了两个数量级模型倾向于把低置信度的区域置为背景造成断线。解决在后处理阶段做连通域分析和形态学骨架修复。我常用的手段是先用3x3的闭运算把断裂处连接再用5x5的中值滤波去孤立点最后按类别保留最大连通域import cv2 mask prediction.astype(np.uint8) kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations1)注意闭运算会把两条平行的线粘连如果类别之间有明确的间隔比如双黄线先用类别ID分开再处理。这一步属于推理阶段的补救措施不是训练问题。如果你发现训练时dice loss降不下去优先怀疑标签本身有缺口而不是模型。5.3 高分辨率图像把所有GPU显存吃光训练直接OOM现象CUDA out of memory即使batch size降到2也崩。原因输入分辨率太高且没有做任何显存控制。也有人是backbone的BN统计量太大导致的峰值显存。高分辨率数据集最常见的错误就是把整图喂进去忘了训练patch和推理整图的区别。解决先确认训练输入分辨率是不是被resize到了2000x4000。把输入降到1024x1024开启混合精度和梯度累积。用torch.cuda.ampscaler torch.cuda.amp.GradScaler() with torch.cuda.amp.autocast(): logits model(img) loss criterion(logits, lab) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()如果单卡还不行切成patch训练每个patch保持完整线段的上下文patch大小至少256x512重叠16像素。推理时再用滑窗拼接这样训练和推理的分辨率不会差太多。不要试着用梯度累积去解决所有OOM它只影响更新频率不能降低单次前向的峰值显存。5.4 数据只有2,800张训练集还按背景占大头过拟合严重现象训练loss降到0.2验证loss反而涨到0.8验证mIOU不升反降。原因样本量小模型把背景纹理背下来了没有学到车道线语义。背景里的路肩、护栏、树影这些纹理高度重复模型容易记住这些纹理和类别的组合一旦验证集出现新场景就懵。解决除了前面说的增强还可以用预训练backbone。从ImageNet预训练的ResNet50出发比随机初始化收敛快得多。另一个有效做法是加载一个在Cityscapes上预训练的DeepLabV3权重把最后分类层换成6类。迁移学习对小数据集是后悔药别从零起步。另外训练轮数不要超过80配合早停策略监控验证loss连续5轮不降就保存最优权重。2800张数据的模型参数量控制在40M以内否则很容易记住训练集。5.5 推理时直接把全图塞进去导致残差犄角现象训练时用512x512 patch跑通验证很顺利一到整图推理就爆显存或者输出错位。原因推理尺寸与训练尺寸差异太大插值放大了累积误差。高分辨率图的尺寸可能是训练patch的16倍模型在训练时从没见过这么大跨度的尺度。解决推理也切成与训练尺寸接近的patch做滑窗拼接。拼接重叠区域用线性权值融合消除拼缝。这是高分辨率分割的常识但很多新手会忽略。拼缝的产生是因为每个patch独立预测周围概率被截断。重叠区域权值从边缘到中心线性过渡能有效减少视觉拼缝。这种后处理代码不复杂但能让可视化效果提升一档。这几条是我做过分割项目后留下的血泪经验。每一类问题在第2章和第4章都有对应的解决基础碰到具体现象时先按这里定位能少走弯路。如果你发现自己的错误不在上述列表里那大概率是读取路径写错了检查os.path.join的拼接Windows和Linux的分隔符也能坑人。6. 让模型在高速场景更稳的四个技巧后处理、多尺度与边缘细化最后一个环节写四个我实际用下来收益最明显的技巧它们不需要重新训练只要在推理阶段加几行代码。第一连通域过滤。把预测掩码按类别提取连通域面积小于阈值的直接丢弃。高速场景里远处的小点是噪声不是车道线。对6类分割阈值设为50像素能去掉大部分孤立噪点。这个阈值不是固定的可以按输入分辨率的千分之一估算。第二多尺度推理。用0.6、1.0、1.4三个尺度对图像缩放后分别预测把概率图插值回原尺寸再求平均。多尺度对大目标更稳车道线这种细长结构受益有限但能防止线被下采样吞掉。只在推理阶段使用别加入训练否则每次训练都要做三倍前向划不来。第三帧间时序平滑。如果是视频流输入用最近N帧的概率做指数加权平均比帧内后处理更有效。这一点要记录在部署代码里不在训练阶段处理。指数权重的衰减系数设0.7对突然出现的误检有很好的压制作用。第四边缘细化。把分割概率图与图像梯度图结合在预测结果中保留车道线边缘处的尖锐度。简单做法是用拉普拉斯算子的梯度幅度乘到概率上让边缘更锐利。如果你发现闭运算把两条线粘连边缘细化也能帮忙分开。我自己的习惯是跑通baseline之后先看坏案例把断线、漏检、错检分别统计再决定增强还是后处理。模型不一定是调得越花越好2,800张的训练集能稳定输出完整车道线已经达到预期。希望这篇整理能帮你在类似的高分辨率车道线分割项目上少熬几个夜祝训练顺利。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →