ARTICLE DETAIL

资讯详情

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

高速车道线语义分割实战:2800张高分辨率图像与UNet训练避坑

高速车道线语义分割实战:2800张高分辨率图像与UNet训练避坑 简介面向计算机视觉与自动驾驶感知方向的开发者提供一套高分辨率高速车道线图像语义分割数据集覆盖背景、白实线等6类分割目标共约2800张图片与对应的像素级标签。数据已按训练集约2000张和测试集约800张划分完毕images与masks目录一一对应省去自行整理的麻烦可直接用于Unet、SwinUnet、TransUnet等分割模型的训练与评估。压缩包内共2000个文件以png图片与jpg原图格式为主另包含类别说明txt与可视化脚本py便于了解类别定义并快速检验标签效果。资源包大小约429.98MB内容紧凑实用。目前已有152人学习下载。配套提供随机抽图展示原始图、GT及蒙板叠加结果的可视化脚本能帮助新手直观核对标注质量对于研究车道线检测、语义分割网络改进的读者是一份可直接落地的数据集基准与工具链同时可搭配作者公开的分割网络专栏进一步深入学习。1. 高速车道线语义分割为什么2800张高分辨率图是道坎拿到一个“6类分割、约2,800张高分辨率高速车道线图像语义分割数据集”时我第一反应不是“有数据了”而是“这个量级刚刚好卡在一个尴尬位置”。语义分割模型普遍吃数据UNet、DeepLabV3这类网络在Cityscapes上动辄上万张图2800张听上去不够看。但高分辨率是个变数如果原图是1080p甚至更高直接整图resize到512×512训练车道线这种细长结构会断成一截一截模型根本学不到“线”的语义。反过来把高分辨率图切成patch喂进去2800张能膨胀成几万张有效样本效果反而比堆数量更稳。这个数据集解决的就是高速场景下车道线的像素级分类适合做自动驾驶感知预研、语义分割算法基线验证以及想自己折腾数据标注和训练流程的工程师。2. 理解6类分割车道线数据集的类别设计与标签真相2.1 语义分割与车道线检测像素级分类的边界车道线检测有两个技术路线传统视觉用颜色和边缘提取深度学习里又分目标检测框选和语义分割两种。语义分割解决的是“每一个像素属于哪一类”的问题输出和原图同尺寸的标签图每个像素的值是类别ID。对高速车道线来说车道线本身是细长条检测框很难紧密贴合而分割可以把虚线、实线、路沿和背景分开到像素级。这个数据集标注的是像素掩码所以训练目标是让模型对每个像素做出6选1的判断。用图像分割算法处理这类数据时最常见的是FCN和UNet。FCN把全连接层换成卷积能接受任意尺寸输入但上采样比较粗糙UNet用跳跃连接把下采样的高层语义和上采样的空间细节拼起来对细线结构更友好。车道线就是典型的“空间细节”主导场景所以后面我会优先用UNet做基线而不是一上来就上大模型。2.2 6类分割到底分哪6类标签约定与灰度索引标题只说6类没说具体是哪6类这种数据集发布时一般会附带一个类别对照表。常见做法是背景加上车道线、道路表面、路沿、护栏、车辆这样五类也有把车道线拆成实线、虚线、双黄线三类的。具体映射关系要看数据集的README但标签文件的存储格式通常是灰度索引图——每个像素值直接对应类别ID比如0是背景1到5是各个目标类别。也有的是RGB可视化图那种是给人看的训练前必须先转成索引图。我一般会先写一段脚本统计标签分布确认边界和类别数再开始训练。下面这段用PIL读灰度图并统计每个类别的像素占比from PIL import Image import numpy as np import glob label_paths glob.glob(./labels/*.png) for p in label_paths[:3]: label np.array(Image.open(p)) # 灰度图自动读成2D数组 values, counts np.unique(label, return_countsTrue) total label.size print(f{p}:) for v, c in zip(values, counts): print(f 类别 {v}: {c} 像素 ({c/total:.2%}))这里有两个关键点。第一用PIL的Image.open读标签别用OpenCV的cv2.imread因为后者默认把灰度图读成3通道BGR后面处理索引会错乱。第二np.unique返回的是像素值列表正常来说应该是0到5连续整数如果看到类似255这种值说明标签图末尾有填充或标注工具加了边界线需要先处理掉。统计结果大概率会告诉你一个残酷现实背景像素可能占90%以上车道线只占2%到3%。这个数字决定了损失函数必须调整否则模型学出来就是把整张图预测成背景。2.3 高分辨率下的标签质量检查边界锯齿与缺口高分辨率图像的分割标签比低分辨率更容易出现边界锯齿因为标注工具在放大边缘时鼠标轨迹很难贴合车道线中心尤其虚线处弯道部分经常出现缺口。还有一个典型问题是车道线宽度在不同路段不一致有的标注按内侧边缘描有的按外侧边缘描模型会被这种不一致搞晕。我的习惯是在训练前对标签做一次快速形态学检查。比如统计每个类别连通域数量或者用骨架化看车道线是否连续。下面这段代码可以找出标签中车道线类的断裂位置from scipy import ndimage label np.array(Image.open(./labels/000001.png)) lane_mask (label 1) # 假设类别1是车道线 structure np.ones((3, 3), dtypenp.int32) components, num ndimage.label(lane_mask, structurestructure) # 连通域数量远大于图像中车道线数量说明标注断裂严重 print(f车道线连通域数量: {num} 车道线像素总数: {lane_mask.sum()})如果连通域数量是实际车道线数量的几十倍说明标注缺漏很多。这种标签直接训练会在断裂处产生错误预测解决方法是训练前做形态学闭运算补缺口但注意闭运算会改变车道线宽度最好只对小缺口操作。另一个更省事的做法是训练时把这类标注质量差的样本丢弃或降低它们在采样中的权重。3. 把2800张高分辨率图变成可训练的数据集格式转换与划分3.1 标签格式检查从RGB掩码到索引图灰度图背后的类别编码很多公开数据集发布的是彩色掩码图每个类别一种颜色视觉上很直观但训练时没法直接用。PyTorch的CrossEntropyLoss要求target是(H, W)的整数索引图通道数得是1而不是3。所以第一步是把RGB掩码转成灰度索引图。假如数据集的类别颜色是固定的比如背景(0,0,0)、车道线(255,0,0)、道路(0,255,0)那么转换脚本可以写成COLOR_MAP { (0, 0, 0): 0, # 背景 (255, 0, 0): 1, # 车道线 (0, 255, 0): 2, # 道路 (0, 255, 255): 3, # 路沿 (255, 255, 0): 4, # 护栏 (0, 0, 255): 5, # 车辆 } def rgb_to_index(rgb_path, out_path): rgb np.array(Image.open(rgb_path).convert(RGB)) index np.zeros((rgb.shape[0], rgb.shape[1]), dtypenp.uint8) for k, v in COLOR_MAP.items(): mask np.all(rgb k, axis-1) index[mask] v Image.fromarray(index.astype(np.uint8)).save(out_path) # 批量转换 import os from pathlib import Path rgb_dir Path(./labels_rgb) out_dir Path(./labels) out_dir.mkdir(exist_okTrue) for p in rgb_dir.glob(*.png): rgb_to_index(str(p), str(out_dir / p.name))转换时有两个坑。第一颜色映射表必须覆盖所有像素如果标签图有抗锯齿边缘颜色会混出中间值np.all(rgb k)匹配不上那些像素就会残留成0背景。检查方法是转换后统计类别数如果只剩5类或更少说明有颜色没映射上。第二类别ID的排列顺序影响损失函数权重建议把背景固定为0车流相关的类别排后面。3.2 训练/验证/测试集划分随机划分的坑与固定随机种子保存图像和标签的目录结构一般是images/和labels/文件名一一对应。怎么划分训练集和验证集很多人直接random.shuffle就完事但这个数据集的场景是高速车道线图像序列大概率存在帧间连续关系。如果随机划分同一路段的相似画面可能同时出现在训练和验证集里验证分数虚高换一段路就崩。我一般会先按文件名前缀分组假设文件名有路段ID比如highway_001_0001.png就按highway_001这个前缀分场景确保同一场景的帧只进一个集合。如果文件名没有场景信息就用聚类或人工标记。这里给个严苛做法的示例固定随机种子并考虑场景分组import random import os from collections import defaultdict random.seed(42) # 固定随机种子复现划分结果 images sorted(os.listdir(./images)) groups defaultdict(list) for img in images: scene_id img.split(_)[0] _ img.split(_)[1] # 假设文件名含场景前缀 groups[scene_id].append(img) group_names list(groups.keys()) random.shuffle(group_names) train_groups group_names[:int(len(group_names)*0.8)] val_groups group_names[int(len(group_names)*0.8):int(len(group_names)*0.9)] test_groups group_names[int(len(group_names)*0.9):] train_imgs [img for g in train_groups for img in groups[g]] # 同理生成 val_imgs, test_imgs固定随机种子非常重要。网络模型有随机性数据划分必须可复现否则你调整了模型结构结果变好了还是变坏了都说不清。另外2800张图如果按8:1:1划分验证集只有280张波动会很大。更稳妥的做法是用5折交叉验证但训练周期会拉长自己权衡。3.3 高分辨率图像如何喂给语义分割模型裁剪、缩放与重叠Patch高分辨率图直接送到网络里不现实显存撑不住。常规操作有两种要么全图缩放到固定尺寸损失细节要么随机裁剪成patch。对车道线这种细目标裁剪是绝对首选。比如原图是1920×1080随机裁成768×768的patch一张图能裁出几十个不同区域等于天然做了数据增强。推理时用滑窗加重叠再把预测拼回原图。下面是一个PyTorch数据集的随机裁剪实现注意图像和标签要同步裁剪import random import torch from torch.utils.data import Dataset from PIL import Image import numpy as np class LaneCropDataset(Dataset): def __init__(self, img_dir, label_dir, crop_size(768, 768)): self.img_paths sorted([os.path.join(img_dir, f) for f in os.listdir(img_dir)]) self.label_paths sorted([os.path.join(label_dir, f) for f in os.listdir(label_dir)]) self.crop_size crop_size def __len__(self): return len(self.img_paths) def __getitem__(self, idx): img np.array(Image.open(self.img_paths[idx]).convert(RGB)) label np.array(Image.open(self.label_paths[idx])) # 已经是索引图 h, w, _ img.shape ch, cw self.crop_size # 保证裁剪尺寸不超过原图 top random.randint(0, h - ch) left random.randint(0, w - cw) img_crop img[top:topch, left:leftcw, :] label_crop label[top:topch, left:leftcw] # 转Tensor并归一化 img_t torch.from_numpy(img_crop.transpose(2, 0, 1)).float() / 255.0 label_t torch.from_numpy(label_crop).long() return img_t, label_t裁剪尺寸的设定有讲究。2,800张图如果只裁一个区域等于没有扩充如果你在__getitem__里随机裁每次epoch采到的patch都不同相当于自动做了无限数据增强。我一般每个epoch让每张图出现在训练中至少两次也就是设置len(dataset)2800但实际循环轮数按需调整。另外裁剪区域如果太偏上天空或太偏下车头要么全背景要么全是车漆对分割没帮助。一个改进是限制裁剪区域在路面范围但这样需要标注里带“路面”类别作为先验简单做法是随机裁剪时不裁剪最上方20%的区域。4. 基于UNet或DeepLabV3跑通基线模型选择与关键参数4.1 选型UNet、FCN与DeepLabV3在高速车道线场景的取舍语义分割模型很多但车道线场景有几个约束类别只有6个目标细长高速场景光照单一但阴影和路面反光多。2800张数据规模不大模型太大容易过拟合太小又学不到长距离上下文。我的选型逻辑是这样UNet是首选基线它的对称编码解码结构加上跳跃连接能在保持空间分辨率的同时融合多尺度特征对车道线这种边缘清晰、宽度窄的目标效果好而且参数量在30M左右一块消费级显卡就能跑。FCN虽然经典但上采样太粗暴输出边界糊车道线边缘会宽一圈。DeepLabV3用空洞卷积扩大感受野能捕捉长距离的车道线走向对弯道和坡道更有利但训练收敛慢需要更长轮次。如果你时间紧先用UNet跑通流程再换DeepLabV3对比。实现上直接用现成库最快。以PyTorch的segmentation_models_pytorch为例import segmentation_models_pytorch as smp model smp.Unet( encoder_nameresnet34, # 编码器用预训练ResNet34 encoder_weightsimagenet, # ImageNet预训练权重 in_channels3, classes6, # 类别数 activationNone, # 训练时不接softmax损失函数里处理 )encoder_name的选择影响很大。ResNet34提取特征适中训练稳定MobileNetV3更轻适合后续落地部署。预训练权重必须加载因为2800张图不够从头训练一个深层编码器Imagenet上学到的边缘和纹理特征迁移到车道线场景能省很多时间。4.2 损失函数与类别不平衡车道线像素占比太低交叉熵直接翻车直接拿标准交叉熵训练模型会陷入“全图预测为背景”的局部最优因为背景占了绝对多数。即使预测出一点车道线损失占比也小梯度被背景主导。解决办法是加权交叉熵给车道线类更高的权重。权重的设置一般用中位数频率平衡或者简单按各类别像素占比倒数。比如背景占比93%车道线占比3%其他四类各占1%那么权重可以设为[1, 10, 5, 5, 5, 5]。注意权重不是越大越好过大的权重会让模型把所有细纹理都当成车道线产生大量假阳性。PyTorch里实现很简单import torch.nn as nn class_weights torch.tensor([1.0, 10.0, 5.0, 5.0, 5.0, 5.0]) criterion nn.CrossEntropyLoss(weightclass_weights.cuda())除了加权交叉熵另一个常用方案是Focal Loss。它通过调节难易样本的权重让模型更关注那些预测不准的像素。对车道线这种少数类Focal Loss的效果往往更好但多了一个需要调的gamma参数我一般先用加权交叉熵跑基线再看要不要换。4.3 训练参数batch size、学习率、数据增强与Early Stopping训练参数我按经验给一组起点值后面根据loss曲线再调参数推荐值说明batch size8~16取决于显存768×768输入下8比较稳优化器Adam初始lr1e-3比SGD收敛快学习率策略余弦退火或ReduceLROnPlateau数据增强随机翻转、亮度扰动、随机裁剪高速场景不需要重度增强早停patience10验证集mIoU不涨就停最大epoch1002800张图通常50轮左右收敛数据增强里有一个坑水平翻转会把车道线从左侧翻到右侧如果数据集中双向车道都有翻转没问题。但如果所有图像都是同向高速路翻转后车道线语义不变但道路护栏和路沿会出现在错误侧反而增加训练难度。我一般先不开翻转看验证loss是否偏高再决定。训练代码片段如下重点看早停逻辑best_iou 0 patience 10 wait 0 for epoch in range(100): train_one_epoch(model, train_loader, criterion, optimizer) val_iou evaluate(model, val_loader) if val_iou best_iou: best_iou val_iou torch.save(model.state_dict(), best_unet.pth) wait 0 else: wait 1 if wait patience: print(f早停于 epoch {epoch}) break4.4 评估指标mIoU之外的“车道线IoU”语义分割的标准指标是mIoU即所有类别IoU的平均值。但在这个数据集上mIoU会被背景类拉高。比如背景IoU有0.98车道线IoU只有0.4mIoU可能算出来是0.75看起来还行实际上车道线断得一塌糊涂。所以评估时要单独看每个类别的IoU尤其车道线这一类的IoU。类别IoU的计算要避开混淆矩阵的坑def compute_iou_per_class(pred, target, num_classes6): ious [] for cls in range(num_classes): pred_mask (pred cls) target_mask (target cls) intersection (pred_mask target_mask).sum().item() union (pred_mask | target_mask).sum().item() iou intersection / union if union 0 else 0 ious.append(iou) return ious另一个对车道线更敏感的指标是Recall因为漏检比误检更危险。预测里如果漏掉一段车道线车辆决策就会出错。所以训练时除了看IoU还要记录每个epoch的车道线类别IoU和Recall模型选择时以两者加权作为标准而不是只看mIoU。5. 高分辨率车道线语义分割的5个避坑记录现象、原因、解决5.1 标签图读出来全黑或全是背景现象训练时调用np.unique(label)发现整个训练集标签只有一个值0或者全是0和255。 原因标签图实际是RGB彩图用OpenCV的cv2.imread读取时即使以灰度模式读如果标注工具把颜色索引存储在RGB通道读出来就不是0到5的整数而是被压缩的灰度值。也有可能是转换脚本里Image.open没有转灰度直接读成了3通道数组后面交叉熵接受不了3通道target。 解决统一用Image.open(p)读标签如果发现arr.ndim 3先判断是不是RGB可视化掩码如果是就执行第3.1节的颜色映射转换。检查最终标签的shape是不是2维unique值是不是0到5。5.2 高分辨率图直接resize到512×512车道线断成虚线现象训练集图片用cv2.resize改成512×512训练完成后预测的单张图车道线中间有规律性空白断裂间距均匀。 原因cv2.resize默认用线性插值车道线在缩小过程中像素被平均灰度值被背景稀释低于模型判为车道线的阈值。尤其虚线类的车道线本身有缺口缩放后缺口变大线宽从3像素缩到1像素甚至消失。 解决训练和推理都不要直接resize小图。用随机裁剪和滑窗推理保持输入分辨率在原始尺寸附近。如果必须缩放到固定尺寸用最近邻插值同时处理图像和标签损失会小一些但边缘还是会变粗。5.3 训练后预测全是背景车道线完全不出现在输出里现象验证集mIoU有0.9以上但把预测结果可视化发现所有像素都是背景色车道线类别一张都没有。 原因这是类别不平衡的极端表现。权重设置不合理或者学习率太大导致模型初始阶段就快速收敛到“全背景”这个局部最优后面怎么训练都拉不回来。 解决先把损失函数权重调大车道线类至少放到5以上并且训练前先过100个样本验证损失能正常下降。另外初始学习率从1e-4开始比1e-3稳特别是使用预训练编码器时用1e-3容易冲坏预训练权重。5.4 显存不足batch size只能设2训练极慢现象用768×768的输入batch size设8直接CUDA out of memory降到2才能跑但一个epoch要跑几百步训练极慢。 原因高分辨率加UNet的显存开销确实高UNet的跳跃连接保存了所有中间特征图显存占用几乎是纯卷积网络的两倍。 解决先把输入尺寸调到512×512试跑显存释放后再逐步加大。不要小看512车道线仍然能保持3像素左右够用。另一个办法是用混合精度训练PyTorch的amp能把显存砍一半损失却不大。如果显存还是不够就用梯度累积batch size 2累积8步等效batch size 16代价是训练时间变长。5.5 验证集mIoU高但实拍视频里车道线闪烁、抖动现象离线测试图像效果不错转到视频连续帧上车道线预测位置在帧间跳变或者在一段平直路上突然消失一帧。 原因语义分割逐帧独立推理没有利用时间连续性。高分辨率图像中的高速场景车辆抖动和路面反光都会造成单帧误检模型对这类噪声敏感。 解决落地时加入时序平滑最简单的做法是对每帧预测的概率图做指数滑动平均然后再取argmax。代码上就是维护一个running_prob每帧用0.8的系数叠加新预测。这个技巧能显著减少闪烁但会增加推理延迟适合车载设备算力足够的情况。6. 从训练到落地的进阶验证车道线IoU、可视化与模型轻量化模型训练好不等于任务结束离能上车还差两步可靠的验证和裁剪部署。先从验证说起语义分割模型的输出是概率图直接argmax得到类别ID可视化时要和原图叠加。我习惯把车道线类单独拎出来用绿色通道叠加到原图上这样一眼就能看出哪里有断裂、哪里有误检pred model(img_tensor).squeeze(0) # (6, H, W) prob_lane torch.softmax(pred, dim0)[1] # 类别1的概率 lane_mask (prob_lane 0.5).cpu().numpy().astype(np.uint8) overlay img.copy() overlay[lane_mask 1] [0, 255, 0] # 绿色标记车道线 Image.fromarray(overlay).save(result.png)这个可视化脚本是排查模型毛病的利器。我见过一个模型在路灯阴影下把黑色沥青裂缝当成车道线mIoU数值看不出问题但叠加图画出来全是绿色杂点这才意识到训练数据里缺阴影下的负样本。所以可视化不只是给别人看效果更是给自己找数据盲区。验证完就该考虑部署了。2800张训练出的UNet如果直接上嵌入式设备ResNet34编码器加解码器大概60MB内存占用每秒推理速度在Jetson这类设备上可能只有不到10帧。轻量化有两个方向模型替换成MobileNetV3编码器的UNet参数量能降到原来的三分之一再接一步PTQ量化int8权重的模型体积再砍75%速度能到20帧以上。量化注意一个问题车道线的类间差异很小虚线和实线的纹理差别在量化后可能被抹掉所以量化后要在验证集上重新计算车道线IoU如果掉了超过两个点就只量化权重不量化激活。工程上还有一个教训这2,800张数据训练出的模型泛化到雨天和夜间会明显退化这是数据规模决定的不要指望调参能弥补。我会在部署文档里明确标注模型的适用条件让下游感知模块在恶劣天气时切到摄像头除雾或雷达融合通道。真正想把高速车道线分割做好这个数据集适合做基线但最终要按自己的场景补几万张标注图。希望帮到你。本文还有配套的精品资源点击获取
返回列表