简介面向自动驾驶车道线分割任务提供一套图像分辨率500-1000的三类语义分割数据集原图为jpg掩码为png。掩码像素值0代表背景、1代表左车道线、2代表右车道线类别定义清晰。数据划分训练集与测试集其中训练集3075张图片及对应mask测试集129张共3204组图像-标签对。压缩包内共2000个文件以png掩码为主辅以jpg原图、类别说明txt和可视化脚本py整体约156.58MB。目前已有167人学习浏览。资源附带的可视化脚本可直接运行随机抽图展示原始图像、GT掩码以及叠加效果便于快速核查标注质量类别txt则便于接入主流分割框架减少数据预处理负担。适用于自动驾驶感知、车道保持等算法研究和高校相关课程实践。1. 大分辨率车道线语义分割到底解决什么问题做自动驾驶感知的同行看到“图像分割数据集大分辨率下自动驾驶车道线语义分割3类”这种标题时最关心的其实就三件事这套数据能不能喂给现有语义分割模型标签到底准不准以及拿到手之后怎么验证。车道线在真实画面里只有几十像素宽被缩到512×512再进网络细线基本就断了。这也是为什么大分辨率对这个任务不是锦上添花而是刚需。这篇笔记面向正在找车道线训练数据、或者准备自己标一套3类语义分割数据集的工程师从标签体系、构建流程、可视化脚本到避坑验证一次讲透并按可直接复现的方式给出命令、参数和踩坑记录。2. 3类分割的标签体系与大分辨率选型先想清楚再动手2.1 为什么是3类而不是单类或5类“3类”在很多公开项目里并不是严格统一的。做自动驾驶车道线分割最常见也最省事的定义是0背景1车道线2可行驶区域。车道线只管lane marking不管白色实线、白色虚线、黄色单线还是黄色双线统一合并成1类可行驶区域指当前车道上能让车辆碾压的沥青或水泥路面但不包括车道线本身。这样划分之后下游决策模块能拿到的信息其实非常干净路在哪里、哪里有根线需要关注。那为什么不做单类单类只输出车道线看起来训练简单但实际落地时没法区分“线”和“路”车辆不知道该局限在哪一块路面里行驶。而做到5类甚至更多——比如把黄实线、白虚线、白实线、路沿、可行驶区域分开——标注成本和错标概率会成倍上升。语义分割模型在细线上本来就容易泄漏特征类别越多边界上的翻车概率越大。实际项目里如果不需要做变道决策或交规识别3类已经足够支撑车道保持和碰撞预警的输入。类别方案输出内容标注成本典型用途单类只标车道线低车道线检测研究、Baseline3类背景车道线可行驶区域中车道保持、避障、可通行空间估计5类以上区分线型/路沿/路面高高精地图、法规约束判断我一般会把类别定义固化到项目文档第一页因为后面所有映射脚本、可视化脚本、训练代码都依赖这一行定义。一旦中途改类别回归成本比重新标一批数据还高。2.2 大分辨率意味着什么显存、感受野、标注成本和精度大分辨率不是单纯把图片调大它直接影响三件事显存占用、感受野和标注粒度。以1080p的输入为例一个轻量级分割网络在batch size8、512×512输入下能轻松塞进24G显卡但同样结构喂1920×1080显存占用会涨到两到三倍以上。工程上常见的做法是训练时不开全图而是从大图上裁剪patch。这时候分辨率的意义是保留车道线在patch内的原始宽度而不是追求全图一次forward。感受野这块容易被人忽视。车道线是细长结构局部看就是一个很窄的条但如果能同时看到整条路的方向和透视关系分类会更稳。低分辨率下长距离车道线在降采样后会被抹成不连续的小点网络感受野再大也救不回来。大分辨率配合合适的空洞卷积结构才能同时拿到局部边缘和全局走向。换句话说模型精度对这种细节任务是有“分辨率上限”的输入分辨率不足换更大的backbone也只是浪费算力。从标注角度1920×1080的图上一根车道线通常只有4到8个像素宽缩到512后只剩1到2个像素人工根本标不准。这也是我在数据准备阶段坚持“宁可裁块不可盲目resize”的原因。很多开源数据集的标签在原图上是对的一旦在预处理里顺手做了一次resizemask的边界就会产生锯齿和像素漂移而且这种错误不会在训练acc里立刻暴露只会表现为反复出现的预测断裂线。所以大分辨率选型更像是给后面所有下游步骤设定了一个不可回退的约束原图是什么分辨率标签就应该是什么分辨率。2.3 数据集目录结构与标签格式动手做数据之前先把目录定死后面会省很多事。我常用的结构是dataset_root/ ├── train/ │ ├── images/ │ │ ├── 000001.jpg │ │ └── ... │ ├── masks/ │ │ ├── 000001.png │ │ └── ... │ └── vis/ │ ├── 000001_vis.png │ └── ... ├── val/ │ ├── images/ │ ├── masks/ │ └── vis/ └── class_names.jsonimages放原始大图masks放与图像同尺寸的单通道8bit PNG。像素值只允许出现0、1、2绝对不要用RGB彩色图当训练标签。class_names.json记录类别名称和像素值的对应关系让后续训练脚本和可视化脚本读同一个文件避免“我记得2类是路面”这种事发生。vis目录是可视化输出我会把原始图、伪彩色mask、叠加图三者拼成一张横向长图保存方便每天翻一眼看有没有错标。{ 0: background, 1: lane_marking, 2: drivable_area }文件名建议用全局递增数字比如000001.jpg。不要继续沿用源数据自带的时间戳或相机编号因为时间戳容易把拍摄序列的信息带进训练集模型可能学到“哪台相机”而不是“哪条车道线”。真正的场景信息统一放进meta.csv按样本号索引。这样后面对比实验、切分验证集时只需要改一个文件不必动训练代码。这里有一个很容易踩的细节mask存成PNG后如果工具或脚本误用JPEG保存即使质量设为95也会在车道线边缘产生压缩伪影训练时模型会把伪影当边缘特征。所以我要求标签文件入库后做一次校验用np.unique检查类别值再用np.all(mask.shape origin.shape[:2])确认尺寸对齐。这一条血泪经验在后面避坑章节里还会展开。提示类别像素值必须是0/1/2连续整数不要让“0”本身既表示背景又表示无效区域。否则训练时Ignore Index的处理会异常所有背景像素都会参与loss计算。3. 从公开数据到自制数据集三步构建可复现的3类车道线数据集3.1 选型与抽帧哪些源头数据值得转换如果你不想从零采集常见做法是以公开自动驾驶数据集做源头行业内用得比较多的是BDD100K、ApolloScape和Cityscapes。这三套数据各有偏向BDD100K有大量美国道路的白天、黑夜、雨天场景原图以1080p为主分辨率接近我们的目标ApolloScape的车道线标注是逐像素的精细度最高但拍摄场景集中在城区固定路线Cityscapes的街景质量高但标注类别更偏通用物体车道线相对弱一些。一般不推荐直接把整套源数据灌进来。自动驾驶车道线最怕的是样本雷同连续视频帧里除了车头晃动几乎没变化训练集塞进几千张几乎一样的图验证集和测试集也被污染。抽帧策略是每隔N帧取一帧N具体取几要看源数据帧率。30fps的视频我通常每5帧取1帧相当于6fps的有效帧率既能保留动态变化又不会让时间相邻帧大量重复。抽帧用Python脚本比ffmpeg更可控因为可以在抽帧的同时记录场景标签和分辨率import cv2 import csv from pathlib import Path video_path Path(/data/raw/segment_01.mp4) out_dir Path(/data/selected_frames) out_dir.mkdir(parentsTrue, exist_okTrue) cap cv2.VideoCapture(str(video_path)) frame_idx 0 sample_interval 5 save_id 0 meta_rows [] while True: ok, frame cap.read() if not ok: break if frame_idx % sample_interval 0: save_name f{save_id:06d}.jpg cv2.imwrite(str(out_dir / save_name), frame, [cv2.IMWRITE_JPEG_QUALITY, 95]) meta_rows.append({ id: save_id, video: video_path.stem, frame: frame_idx, h: frame.shape[0], w: frame.shape[1] }) save_id 1 frame_idx 1 cap.release() with open(out_dir / meta.csv, w, newline) as f: writer csv.DictWriter(f, fieldnames[id, video, frame, h, w]) writer.writeheader() writer.writerows(meta_rows)这个脚本的关键参数是sample_interval和IMWRITE_JPEG_QUALITY。采样间隔越大数据多样性越低但标注去重越彻底质量参数建议不低于90低于90的话图像压缩块会干扰细线标注。meta.csv是之后做训练集/验证集切分和场景均衡的依据不要省。切分时最好先按video分组再随机抽视频而不是从所有帧里直接随机抽避免同一个视频的相邻帧同时出现在训练集和验证集里。3.2 标签重映射把复杂标注压成3类的具体脚本公开数据集的标签很少直接就是0/1/2通常需要重映射。BDD100K的语义分割标签有几十个类别ApolloScape的车道线更是按ID区分不同车道线和路沿。如果你把源标签直接当训练标签用类别数会爆炸。正确姿势是先可视化确认再写映射表。我一般会准备一个统一的转换函数输入是源标签的RGB图像或单通道索引图输出是我们约定的3类单通道图。这里以色索引图为例import numpy as np from PIL import Image # 源数据集中颜色到我们3类语义的映射 COLOR_MAP { 1: (0, 0, 0), # 背景 2: (255, 0, 0), # 车道线统一成红色 3: (0, 255, 0), # 可行驶区域统一成绿色 } def remap_mask(src: np.ndarray, src_to_target: dict) - np.ndarray: src: HxWx3 的RGB标签图 src_to_target: 把源类别ID映射到0/1/2的字典 h, w src.shape[:2] out np.zeros((h, w), dtypenp.uint8) for source_id, target_id in src_to_target.items(): color COLOR_MAP[source_id] mask (src[:, :, 0] color[0]) \ (src[:, :, 1] color[1]) \ (src[:, :, 2] color[2]) out[mask] target_id return out src_rgb np.array(Image.open(src_label.png).convert(RGB)) # 源类别1是背景2是车道线3是可行驶区域 mapping {1: 0, 2: 1, 3: 2} label remap_mask(src_rgb, mapping) Image.fromarray(label).save(train/masks/000001.png)这段代码的关键是向量化的颜色匹配。逐像素for循环在1080p图上会跑几十秒而向量化版本几乎一瞬完成。另一个坑是彩色标签转灰度后直接用np.unique会得到大量中间值因为抗锯齿边缘产生了过渡色所以必须在RGB空间做确定性颜色匹配。如果源标签本身就是单通道索引图把COLOR_MAP换成ID映射字典即可逻辑不用变。映射之后做一次类别占比统计。如果某张图可行驶区域占比为0多半是源数据没覆盖当前车道如果车道线占比超过10%则说明映射把路面裂缝、接缝或其他装饰线也并进来了需要回去核对颜色区间。这个统计脚本应该在整个数据集上跑一遍输出异常样本ID而不是只看一两张。3.3 大图裁剪与重叠滑窗分辨率不足时的补图方案原始1080p大图对很多训练pipeline来说仍然太大显存一次装不下若干张。常见做法是训练时直接用随机crop从大图上裁patch好处是相当于在线数据增强坏处是patch之间的上下文被切断离线切好patch的好处是方便检查标签对齐坏处是存储翻倍。我习惯离线做一份“滑窗patch版本”给前期调试用同时保留原始大图供正式训练随机crop。滑窗裁剪的代码要保证图像和mask使用完全相同的坐标并且要处理右边缘和下边缘的剩余区域def sliding_window_crop(image, mask, patch_size640, overlap128): stride patch_size - overlap h, w image.shape[:2] patches [] y_candidates list(range(0, h - patch_size 1, stride)) if y_candidates[-1] patch_size h: y_candidates.append(h - patch_size) x_candidates list(range(0, w - patch_size 1, stride)) if x_candidates[-1] patch_size w: x_candidates.append(w - patch_size) for y in y_candidates: for x in x_candidates: img_patch image[y:y patch_size, x:x patch_size] mask_patch mask[y:y patch_size, x:x patch_size] # 过滤掉几乎没有车道线和路面区域的空patch if (mask_patch 0).sum() (patch_size * patch_size * 0.01): continue patches.append((img_patch, mask_patch, (x, y))) return patches这里的patch_size与overlap是两个最关键的参数。patch太小车道线局部上下文不够patch太大显存又容易溢出。我用640×640加128像素重叠基本能覆盖大多数crop需求。重叠区域的意义是让同一根车道线在不同patch里出现多次增加训练时的位置多样性也避免车道线恰好在patch边缘被截断。补尾部的逻辑要尤其小心。如果不补靠近图像右边缘和下边缘的车道线永远进不了训练如果补了又会产生尺寸不齐的patch。正确做法是让最后一行/一列patch往左上偏移而不是把图resize。离线patch保存时命名规则里带上x_y坐标比如000001_001_002.png后续排查错标时能直接回到大图定位。在线随机crop则不需要保存但必须保证图像和mask使用同一个随机种子否则就会出现标签错位这种灾难。4. 标签文件格式与数据可视化代码让每一步都有后悔药4.1 标签文件的存法单通道PNG还是RLE掩码在图像分割数据集里标签存储格式常见的是两派单通道PNG和RLE掩码。车道线这种任务我坚定选PNG原因是线状结构对边界精度要求极高。RLE是游程编码对大块连续区域压缩效率很好但车道线长且细run长度经常很短压缩不了多少反而要额外处理解码。单通道8bit PNG无损能被PIL、cv2直接读取调试时用np.unique扫一眼就知道类数对不对。存储方式空间效率解码速度边界保持适用场景单通道PNG中快无损车道线/细线分割RLE掩码高中依赖解码COCO类实例分割彩色PNG低快有损风险标注中间产物JSON多边形高慢边缘精度高人工标注原始格式这里需要区分“训练时用的标签格式”和“标注工具导出的格式”。标注工具导出彩色PNG或JSON多边形都是正常的但进入训练数据集前必须统一转成单通道PNG。有项目直接拿标注工具导出的RGB图训练CrossEntropyLoss把每个RGB组合当作独立类别训练了十几个epoch都降不下去这就是典型的黑匣子问题模型没挂但你喂错了它也不会主动报错。所以我在数据入库脚本的第一行就会加断言assert len(np.unique(labels)) 3。4.2 数据可视化代码原图mask融合视图的三联输出可视化是数据集的体检报告。没有可视化标签质量只能靠训练指标间接猜测有了可视化错标、漏标、边界偏移十几分钟就能发现。这里给一套常用的可视化脚本把原图、伪彩色mask、融合叠加图横向拼在一起import cv2 import numpy as np from pathlib import Path # 类别颜色0背景纯黑1车道线红色2可行驶区域绿色 CLASS_COLORS { 0: (0, 0, 0), 1: (255, 0, 0), 2: (0, 255, 0), } def visualize_sample(image_path, mask_path, output_path, alpha0.4): image cv2.imread(str(image_path)) mask cv2.imread(str(mask_path), cv2.IMREAD_UNCHANGED) if mask.ndim 3: raise ValueError(mask must be single-channel PNG) h, w mask.shape[:2] color_mask np.zeros((h, w, 3), dtypenp.uint8) for cls, color in CLASS_COLORS.items(): color_mask[mask cls] color overlay cv2.addWeighted(image, 1 - alpha, color_mask, alpha, 0) combo np.hstack([image, color_mask, overlay]) cv2.imwrite(str(output_path), combo) visualize_sample( image_pathtrain/images/000001.jpg, mask_pathtrain/masks/000001.png, output_pathtrain/vis/000001_vis.png )逻辑说明cv2.imread读取mask时用IMREAD_UNCHANGED保证不会把单通道转成三通道彩色mask的构建顺序是先建纯黑底图再按类别像素值填充颜色。alpha控制叠加透明度我习惯取0.4。太低看不清mask边缘太高会遮住路面纹理人眼就分不清mask是否贴合真实路面。在此基础上可以批量跑整个目录生成一张HTML索引或把多张vis图拼成大图。建议每次新增或修改标签后强制跑一遍可视化宁可慢一点也要让人眼先过一遍再进训练。很多人觉得“训练完看结果”就能知道数据好坏但那样找问题的路径太长了一个错标样本在几百张图里对精度的影响会被平均掉很难定位。4.3 用可视化核对标签的常见套路可视化不只是给人看也可以用一点统计逻辑来“挑毛病”。第一是看类别占比曲线画每张mask中车道线像素比例和可行驶区域像素比例正常车道线占比一般在1%到5%占比突然为0的样本要警惕。第二是看边缘像素偏移把mask和原图的灰度梯度叠加如果mask边缘超出原图中车道线亮度梯度两个像素以上大概率是映射或标注偏移。一个有效的小技巧是“差值图”检查把原图转灰度后用Canny提边缘再看mask边界膨胀后的区域是否与Canny边缘有重合。没有重合的地方往往就是标签错位。脚本不复杂gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) edges cv2.Canny(gray, 50, 150) mask_binary (mask 1).astype(np.uint8) * 255 mask_boundary cv2.Canny(mask_binary, 50, 150) mask_boundary_dilate cv2.dilate(mask_boundary, np.ones((3, 3), dtypenp.uint8)) coverage (mask_boundary_dilate edges).sum() / (mask_boundary_dilate.sum() 1e-6)这个coverage指标最好控制在0.5以上。低得离谱说明mask和图像内容错位不要再调网络了先把标签修掉。Canny的阈值50/150按图像对比度调对比度差的雨天夜景可以降到30/100但所有样本要用同一组参数否则指标之间不可比。这套方法不需要高深数学却能省掉好几天“训练不收敛”的排查时间。5. 避坑与排查大分辨率车道线数据集的5个真实翻车现场5.1 彩色PNG被当成训练标签类别数变成几万现象训练loss掉不下去验证mIoU卡在0.2上下打印np.unique(label)发现类别值从0到几千都有。原因标注工具导出的mask是RGB三通道彩色图每条线抗锯齿后颜色组合极多。训练代码直接np.array(Image.open(path))得到H×W×3数组被当作H×W的索引图每个“伪像素值”都成了独立类别。解决入库前统一转单通道。用前面写的remap_mask按颜色区间映射映射后再断言set(np.unique(label)) {0, 1, 2}。转完顺手跑一遍可视化别只在控制台里打印几个数字。5.2 resize时掩码用了线性插值车道线出现半透明中间值现象训练时loss能收敛但预测出来的车道线边缘发虚像是被水彩画晕开。原因图像预处理里用双线性插值resize原图同样的resize函数也作用于mask。双线性插值在mask边界会产生类似0.4、0.6这样的中间值之后做round或argmax细线有可能被直接置成背景。解决原图和mask分别处理mask的resize方法固定传cv2.INTER_NEAREST。如果数据集已经污染把高分辨率原图重新裁patch比在resize后的图上修补更划算。宁可多耗一点磁盘也不要在细线任务上省插值。5.3 滑窗裁剪时坐标没对齐mask比原图偏移一个方向现象可视化叠加图里车道线的红色mask整体偏在路面右侧和原图车道线位置差出几十个像素。原因离线裁剪图像时用的是image[y:yh, x:xw]裁剪mask时却用了另一个变量名或者提前做了一次resize导致两者shape相同但起点不同。这种错误在mask是单通道时特别隐蔽因为shape[:2]完全一致。解决裁剪函数把image和mask放在同一个函数里先计算boxes再循环里统一用同一个y和x。保存前加一行断言assert img_patch.shape[:2] mask_patch.shape[:2]每次生成patch数据集后随机抽样10组用np.hstack([img_patch, mask_patch])肉眼确认一次对齐。5.4 类别极度不平衡模型把所有像素都预测成背景现象训练loss下降但车道线类别mIoU接近0模型输出全是背景。原因3类数据里背景通常占85%以上车道线只占2%左右。默认CrossEntropyLoss里每个类别的权重都是1模型很快发现全预测背景成本最低。这不是分辨率问题也不是模型结构问题而是训练目标被背景淹没。解决在loss里给背景、车道线、可行驶区域设置权重常见做法是按像素占比倒数开根号。比如背景0.3、车道线20、可行驶区域2。更省事的是用OhemCrossEntropy只取loss值最高的部分像素回传。要注意权重别设太极端否则模型会为保车道线产生大量噪点。5.5 源数据类别定义和你的3类定义不完全一致映射后出现逻辑冲突现象可行驶区域把对向车道和人行道也算进去了或者路沿被标成车道线模型在交叉口附近预测很乱。原因不同数据集的类别定义有差异。有的把“路沿/curb”当作独立类有的把“人行横道”也算drivable area。你只看了文档里的类名没看实际像素分布就做了映射。解决转换前先把每个源类别单独染色输出一张全黑底图人工扫一遍再写映射字典。特别要留意“背景”的定义源数据的背景可能包含绿化带、建筑、天空而你的背景只期望它是“非路面、非车道线”。如果混入大量重复纹理模型会学到混乱的边界。遇到定义冲突宁可把这类像素标成背景也不要硬塞进可行驶区域。6. 进阶用mIoU和边界准确率验证数据集质量以及标注迭代技巧数据集做得对不对最终要看训练任务跑出来的指标。建议固定一个轻量级分割网络比如DeepLabV3或SegFormer-B0输入分辨率统一640×640训练100个epoch然后记录三个数字整体mIoU、车道线类别的IoU、可行驶区域的IoU。如果车道线类别IoU低于0.6先别急着调模型结构回头检查标签中有多少断裂线和边界偏移。数据集本身的质量问题靠换大网络是压不住的。另一个更敏感的验证指标是“边界准确率”。在mask边界附近取一个3×3窗口统计预测边界和标签边界重合的比例。车道线任务的真实痛点往往不是整条线漏检而是边界歪了两个像素。这个指标不需要额外库用上面Canny差值图的思路就能算。标注迭代上我现在比较推荐“半自动难例回灌”的流程先拿一个预训练模型跑一批粗标数据把预测结果转成伪标签让人工在伪标签上修错误而不是从零画线。修完的样本重新训练再预测另一批数据如此循环。这个流程对车道线特别有效因为车道线是规则的长条结构模型给出的初始轮廓往往已经很接近了人工只需要拖动节点校正偏差点位。我自己早期吃过亏觉得数据集“差不多”就行结果训练时细线反复断掉后来才发现是可视化脚本没有接入数据管线错标累积了一个星期才暴露。从那以后我把“改标签必出vis图出vis图必抽看”写成了固定习惯。这个习惯也许不会让你的模型一夜变强但绝对能让你每次实验失败之后更快判断是数据的问题还是模型的问题。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?