ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

车道线多值分割数据集实战:从标注解析到训练落地与后处理

车道线多值分割数据集实战:从标注解析到训练落地与后处理 简介这份资源面向从事自动驾驶感知、车道线检测与图像分割研究的开发者与学习者提供道路场景下的多值语义分割数据集可用于训练和评估分割网络。数据按训练集与测试集划分训练集约6300张图像及对应mask测试集约2200张共约8000张样本覆盖左转、右转、前进等13类分割目标类别定义可参考classes文件。压缩包共2000个文件以1998个png图像与掩膜为主另含1个py可视化脚本和1个json配置整体约280.81MB。附带的脚本可随机抽取一张图片展示原图、GT图像及GT在原图上的蒙板效果并保存至当前目录便于快速核验标注质量。目前已有93人学习适合需要现成车道线分割数据、想验证多类别分割模型效果或搭建训练流程的读者使用。1. 车道线多值分割数据集从标注格式到训练落地的完整路径拿到一个约 8000 张规模的车道线图像分割数据集标注是多值分割而非二值掩码这件事本身就值得认真对待。很多做车道线检测的团队一开始用二值分割跑 baseline把车道线像素标成前景、其余标成背景模型很快就能收敛到一个看起来不错的 IoU但一上车就发现相邻车道线粘连、虚线断裂、车道数量判断错误。根因在于二值掩码丢掉了车道线的实例归属信息模型学到的是“哪里像车道线”而不是“这是第几条车道线”。多值分割数据集把每条车道线赋予独立像素值让模型在像素级同时完成检测与区分这才是车道线感知真正需要的监督信号。这个数据集适合做自动驾驶感知、ADAS 车道保持、以及图像分割算法在结构化道路场景下的验证尤其适合想从二值分割进阶到实例级分割的从业者。8000 张的体量不算大但足够把数据管线、标注解析、增强策略和训练调参的坑踩一遍下面按落地顺序拆开讲。2. 多值分割标注的解析与数据管线搭建2.1 多值掩码和 one-hot 编码的本质区别多值分割的标注图是一张单通道灰度图或索引图每个像素值代表一个类别 ID。车道线场景下常见约定是 0 为背景1 到 N 分别对应第 1 到第 N 条车道线。这里有一个容易翻车的点如果直接用交叉熵损失模型会把类别 ID 当成有序数值认为类别 3 和类别 4 的距离比类别 1 和类别 4 更近这显然不符合车道线之间无序且对称的关系。正确做法是把多值掩码转成 one-hot 编码用 softmax 配合交叉熵或者用多通道 sigmoid 配合二值交叉熵。前者适合车道线互斥的场景后者适合车道线可能重叠或需要多标签输出的场景。我一般会先确认标注里车道线是否互斥再决定用哪种损失。另一个细节是标注图的位深。8 位图最多支持 256 个类别车道线场景通常够用但如果数据集里出现了 255 这个像素值要确认它是背景还是第 255 条车道线。有些标注工具默认用 255 表示忽略区域有些则用 255 表示前景。拿到数据集第一件事就是统计像素值分布用 numpy 跑一遍 unique看清楚每个值出现的频率和空间分布。2.2 用 Python 读取标注并做可视化校验在训练之前必须把原图和标注图叠在一起看几组确认标注和图像对齐、车道线 ID 连续、没有错标漏标。下面这段代码做三件事读取图像和标注、统计类别分布、生成叠加可视化。import numpy as np import cv2 import os from collections import Counter def inspect_lane_dataset(img_path, mask_path, num_samples5): 检查车道线多值分割数据集的标注质量 img_path: 原图目录 mask_path: 标注目录 num_samples: 随机抽检数量 img_files sorted(os.listdir(img_path)) # 统计所有标注的像素值分布 pixel_counter Counter() for fname in img_files[:200]: # 先抽 200 张看分布 mask cv2.imread(os.path.join(mask_path, fname), cv2.IMREAD_GRAYSCALE) if mask is None: print(f警告{fname} 标注读取失败) continue unique, counts np.unique(mask, return_countsTrue) pixel_counter.update(dict(zip(unique.tolist(), counts.tolist()))) print(像素值分布前 10 个) for val, cnt in pixel_counter.most_common(10): print(f 像素值 {val}: {cnt} 像素) # 抽检可视化 for fname in img_files[:num_samples]: img cv2.imread(os.path.join(img_path, fname)) mask cv2.imread(os.path.join(mask_path, fname), cv2.IMREAD_GRAYSCALE) if img is None or mask is None: continue # 把多值掩码映射成彩色方便肉眼检查 colored np.zeros_like(img) for lane_id in np.unique(mask): if lane_id 0: continue color np.random.randint(50, 255, size3).tolist() colored[mask lane_id] color overlay cv2.addWeighted(img, 0.6, colored, 0.4, 0) cv2.imwrite(fcheck_{fname}, overlay) print(f已生成校验图 check_{fname}车道线 ID: {np.unique(mask).tolist()}) if __name__ __main__: inspect_lane_dataset(./images, ./masks)这段代码的逻辑说明pixel_counter累计前 200 张标注的像素值频次用来判断类别是否均衡、有没有异常值。np.unique返回每个标注图里出现的车道线 ID如果某张图里出现了训练集约定之外的 ID比如约定 1 到 4 却出现了 7就要回去查标注规范。可视化部分用随机颜色区分不同车道线叠加到原图上肉眼就能看出标注是否贴合车道线边缘。参数方面num_samples控制抽检数量实际项目中我至少会看 20 张覆盖直道、弯道、夜间、雨天等场景。cv2.IMREAD_GRAYSCALE确保标注按单通道读取避免三通道读取后像素值被复制到三个通道导致 ID 混乱。2.3 数据集划分与增强策略的取舍8000 张数据按 7:2:1 划分训练、验证、测试训练集 5600 张验证集 1600 张测试集 800 张。划分时要按场景分层不能随机打乱否则可能出现训练集全是直道、验证集全是弯道的情况。我一般会先给每张图打上场景标签比如直道、弯道、路口、夜间、雨天再在每个场景内按比例抽。增强策略上车道线分割对几何变换敏感水平翻转可以用但要注意翻转后车道线 ID 的顺序会变如果标注里 ID 是按从左到右编号的翻转后需要重新映射 ID。随机裁剪和旋转要谨慎裁剪可能把车道线裁断旋转会引入黑边。颜色抖动、亮度对比度调整可以放心用对车道线位置没有影响。Mosaic 增强在 YOLOv8 分割训练里很常见但车道线场景下 Mosaic 会把四张图的道路拼在一起车道线方向混乱我一般会把 Mosaic 概率调低到 0.3 以下或者直接关掉。3. 从多值掩码到训练输入格式转换与 DataLoader 实现3.1 把多值掩码转成训练框架需要的格式不同训练框架对分割标签的格式要求不一样。PyTorch 的交叉熵损失接受LongTensor类型的类别索引图形状是[H, W]值域是[0, num_classes-1]。如果标注里车道线 ID 是 1 到 4背景是 0那正好可以直接用类别数设为 5。但如果标注里用了 255 表示忽略区域就需要把 255 映射成一个额外的忽略类别或者在损失函数里用ignore_index255。YOLOv8 分割任务要求标签是多边形坐标加类别需要把掩码转成轮廓点。这里给一个通用的转换脚本把多值掩码转成 PyTorch 分割训练用的索引图同时生成忽略掩码。import numpy as np import cv2 import os def convert_mask_to_index(mask_path, output_path, ignore_val255, num_classes5): 将多值掩码转换为训练用索引图处理忽略区域 mask_path: 原始标注目录 output_path: 转换后输出目录 ignore_val: 标注中表示忽略区域的像素值 num_classes: 包含背景的类别总数 os.makedirs(output_path, exist_okTrue) for fname in os.listdir(mask_path): mask cv2.imread(os.path.join(mask_path, fname), cv2.IMREAD_GRAYSCALE) if mask is None: continue # 把忽略区域映射到 num_classes训练时用 ignore_index 跳过 converted mask.copy() converted[mask ignore_val] num_classes # 检查是否有超出范围的像素值 valid_vals set(range(num_classes 1)) actual_vals set(np.unique(converted).tolist()) if not actual_vals.issubset(valid_vals): print(f{fname} 存在异常像素值: {actual_vals - valid_vals}) cv2.imwrite(os.path.join(output_path, fname), converted) print(f转换完成输出目录: {output_path}) if __name__ __main__: convert_mask_to_index(./masks, ./masks_indexed, ignore_val255, num_classes5)逻辑说明converted[mask ignore_val] num_classes把忽略区域统一映射到类别数这个值上训练时损失函数设ignore_indexnum_classes就能跳过这些像素。valid_vals检查确保转换后没有意外像素值。参数num_classes5对应背景加 4 条车道线实际类别数要根据数据集标注规范调整。如果标注里没有忽略区域ignore_val可以设成一个不会出现的值比如 -1这样转换逻辑不会误伤。3.2 自定义 Dataset 和 DataLoader 的关键参数PyTorch 的 Dataset 需要返回图像张量和标签张量。图像做归一化标签保持LongTensor。下面是一个针对车道线多值分割的 Dataset 实现重点处理了图像和标注的同步增强。import torch from torch.utils.data import Dataset, DataLoader import cv2 import numpy as np import os import random class LaneSegDataset(Dataset): def __init__(self, img_dir, mask_dir, img_size(512, 1024), augmentTrue): self.img_dir img_dir self.mask_dir mask_dir self.img_size img_size self.augment augment self.files sorted(os.listdir(img_dir)) def __len__(self): return len(self.files) def __getitem__(self, idx): fname self.files[idx] img cv2.imread(os.path.join(self.img_dir, fname)) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) mask cv2.imread(os.path.join(self.mask_dir, fname), cv2.IMREAD_GRAYSCALE) # 同步缩放 img cv2.resize(img, self.img_size, interpolationcv2.INTER_LINEAR) mask cv2.resize(mask, self.img_size, interpolationcv2.INTER_NEAREST) # 同步水平翻转注意车道线 ID 重映射 if self.augment and random.random() 0.5: img np.fliplr(img).copy() mask np.fliplr(mask).copy() # 如果车道线 ID 按从左到右编号翻转后需要重映射 # 这里假设 ID 1 到 4翻转后 1-4, 2-3 remap {1: 4, 2: 3, 3: 2, 4: 1, 0: 0, 5: 5} mask np.vectorize(remap.get)(mask) # 颜色抖动 if self.augment and random.random() 0.3: img img.astype(np.float32) img * random.uniform(0.8, 1.2) img np.clip(img, 0, 255).astype(np.uint8) # 归一化 img img.astype(np.float32) / 255.0 img (img - [0.485, 0.456, 0.406]) / [0.229, 0.224, 0.225] img torch.from_numpy(img).permute(2, 0, 1).float() mask torch.from_numpy(mask).long() return img, mask # DataLoader 配置 dataset LaneSegDataset(./images, ./masks_indexed, img_size(512, 1024)) loader DataLoader( dataset, batch_size8, shuffleTrue, num_workers4, pin_memoryTrue, drop_lastTrue )逻辑说明cv2.resize对图像用双线性插值对标注用最近邻插值避免标注 ID 被插值成小数。水平翻转后remap字典把车道线 ID 按左右顺序对调这一步如果漏掉模型会学到错误的 ID 顺序。颜色抖动只改图像不改标注保证几何对齐。归一化用 ImageNet 均值方差如果数据集风格差异大可以换成数据集自身的统计值。DataLoader 的num_workers根据 CPU 核数调整pin_memoryTrue在 GPU 训练时能加速数据传输。drop_lastTrue避免最后一个 batch 只有一张图导致 BatchNorm 报错。3.3 类别不平衡的处理加权损失和 OHEM车道线像素在整张图里占比通常不到 5%背景占 95% 以上直接用交叉熵会让模型倾向于全预测背景。常见做法是给交叉熵加类别权重背景权重设小车道线权重设大。权重可以按类别频率的倒数来算也可以手动调。我一般先用1 / log(1.02 freq)这种平滑倒数避免权重过于极端。另一个做法是 OHEM在线难例挖掘只对损失最高的前 K 个像素回传梯度。PyTorch 里可以用torch.topk实现但要注意保持梯度可导。实际项目中加权损失加 Dice Loss 的组合比较稳Dice Loss 对类别不平衡不敏感能拉高车道线的召回。4. 训练车道线分割模型的避坑与排查清单4.1 损失不下降但 IoU 在涨现象训练日志里 loss 震荡或缓慢下降但验证集 IoU 稳步上升。原因多值分割的交叉熵对背景像素占主导loss 被背景拉低车道线像素的损失被淹没。解决把 loss 拆开看分别记录背景和车道线的损失值。如果车道线损失不降加 Dice Loss 或 Focal Loss。我一般会在训练脚本里每 100 个 iteration 打印一次各类别的平均损失这样能快速定位是哪个类别拖后腿。4.2 相邻车道线粘连模型分不清 ID现象可视化预测结果时两条相邻车道线被预测成同一条或者 ID 互换。原因多值分割的类别边界在标注里是硬边界但图像上相邻车道线之间可能只有几个像素的间隔卷积感受野下采样后边界信息丢失。解决在解码器里加边界感知模块或者用更高的输出分辨率。另一个实用技巧是在损失函数里加边界加权对标注中类别边界附近的像素给更高权重。具体做法是用cv2.Canny或形态学梯度提取边界生成边界权重图乘到损失上。4.3 虚线车道线被预测成实线或断裂现象虚线车道线的虚线段之间被模型连起来或者虚线段被漏检。原因虚线在单帧图像里本身就不连续模型如果只依赖局部纹理容易把间隔填上。解决在数据增强里加入随机遮挡模拟虚线间隔让模型学会区分“真实间隔”和“遮挡”。另外时序信息对虚线很关键如果只有单帧可以在后处理里用形态学闭运算把虚线段连起来但闭运算的核大小要按车道线宽度调太大就会把相邻车道线也连上。4.4 夜间和雨天场景 IoU 骤降现象白天场景 IoU 0.85夜间场景掉到 0.5 以下。原因夜间车道线对比度低雨天路面反光标注在这些场景下本身也可能不一致。解决先检查夜间标注质量如果标注本身模糊模型再强也学不好。确认标注没问题后在增强里加入亮度扰动、高斯噪声、运动模糊模拟夜间和雨天成像。另外夜间场景可以单独采样一个 batch用更高的学习率微调或者用域适应方法把白天特征迁移到夜间。4.5 训练到后期验证集 IoU 突然掉点现象前 80 个 epoch IoU 稳步上升之后突然下降。原因过拟合或者学习率没有及时衰减。解决加早停策略验证集 IoU 连续 10 个 epoch 不升就停。学习率用余弦退火或 ReduceLROnPlateau在 IoU 平台期自动降学习率。另外检查数据增强是否太弱如果训练集和验证集分布差异大增强可以再激进一些。5. 多值分割的进阶技巧从像素级到实例级的后处理训练完模型输出的是每个像素的类别概率取 argmax 得到多值掩码。但实际车道线感知需要的是每条车道线的实例包括车道线数量、每条线的位置和曲率。从多值掩码到实例中间差一个后处理。常见做法是对每个类别 ID 分别取二值掩码做连通域分析过滤掉面积过小的连通域再对每个连通域拟合曲线。如果标注里车道线 ID 本身就是按实例编号的那 argmax 之后直接按 ID 取连通域就行。但如果模型预测的 ID 有噪声比如同一条车道线被预测成两个 ID就需要用聚类或跟踪来合并。下面这段代码演示从多值掩码提取车道线实例并拟合二次曲线。import numpy as np import cv2 def mask_to_lane_instances(mask, min_area100): 从多值分割掩码提取车道线实例 mask: 模型预测的多值掩码H x W值域 0 到 num_classes-1 min_area: 最小连通域面积过滤噪声 返回: 每条车道线的拟合曲线系数和像素点 instances [] for lane_id in np.unique(mask): if lane_id 0: continue binary (mask lane_id).astype(np.uint8) num_labels, labels, stats, centroids cv2.connectedComponentsWithStats(binary, connectivity8) for i in range(1, num_labels): area stats[i, cv2.CC_STAT_AREA] if area min_area: continue ys, xs np.where(labels i) # 按 y 坐标排序拟合 x f(y) sort_idx np.argsort(ys) ys_sorted ys[sort_idx] xs_sorted xs[sort_idx] # 二次多项式拟合 coeffs np.polyfit(ys_sorted, xs_sorted, 2) instances.append({ lane_id: int(lane_id), area: int(area), coeffs: coeffs.tolist(), points: (xs_sorted.tolist(), ys_sorted.tolist()) }) return instances # 假设 pred_mask 是模型输出的多值掩码 # instances mask_to_lane_instances(pred_mask, min_area150) # for inst in instances: # print(f车道线 ID {inst[lane_id]}面积 {inst[area]}拟合系数 {inst[coeffs]})逻辑说明connectedComponentsWithStats对每个类别 ID 的二值掩码做连通域分析min_area过滤掉面积过小的噪声区域。np.polyfit对每个连通域的像素点拟合二次曲线因为车道线在图像上通常呈抛物线形状。拟合时按 y 坐标排序保证 x 是 y 的函数避免多值对应。参数min_area根据图像分辨率和车道线宽度调512x1024 的图像上车道线面积通常几百到几千像素min_area150能过滤掉大部分噪声。如果车道线是竖直的也可以按 x 排序拟合 y f(x)看哪种更稳定。进阶用法上如果要做车道线跟踪可以把当前帧的实例和上一帧的实例做匈牙利匹配用拟合曲线的系数距离作为匹配代价。匹配上之后用卡尔曼滤波平滑曲线系数减少帧间抖动。这套后处理在嵌入式平台上跑单帧耗时可以控制在 10ms 以内比端到端的实例分割模型轻量得多。我自己的习惯是每次拿到新的车道线数据集先跑一遍标注可视化确认 ID 顺序和忽略区域再跑一遍过拟合测试用 100 张图训练到 loss 接近 0看模型能不能复现标注。这一步能排除数据管线的绝大多数问题。过拟合测试通过后再上全量数据调学习率和增强策略。这套流程帮我省了很多后悔药希望帮到你。本文还有配套的精品资源点击获取
返回列表