简介:一套基于语义分割的车道线检测Python项目,面向计算机视觉与深度学习方向的毕设、课程设计及工程实践者,解决自动驾驶与辅助驾驶中的车道线精准识别分割问题。项目包含完整源码与详细说明,源码结构清晰、注释详尽,便于直接学习或二次开发。压缩包共32个文件,以Python脚本、模型权重文件(meta、data、index、checkpoint)、测试图片及README说明文档为主,大小仅1.35MB,便于下载部署。其中GCN、ERFNet、LCNet等网络实现展示了不同语义分割模型在车道线任务上的应用,threshold_0.5、0.7、0.9等对比图直观呈现阈值对检测结果的影响,README也给出了技术原理与优化方向。目前已有65人学习下载,通过实操可掌握数据预处理、模型训练、测试与阈值调优的完整流程,理解语义分割在车道线检测中的核心思想,为后续研究和工程实践打下坚实基础。
1. 车道线检测里的语义分割:它到底在解决什么问题
在辅助驾驶和自动驾驶的感知栈里,车道线检测看着不起眼,做起来却非常折磨人。白天强光、夜晚路灯、雨天反光、路面接缝,传统边缘检测加霍夫变换的方案在这些场景里几乎全军覆没。基于语义分割方法的车道线检测把任务转成逐像素分类,网络直接输出“当前像素是不是车道线”的 mask,鲁棒性比手工特征高出一个量级。这份语义分割车道线检测的 python 源码加项目说明,走的就是这条技术路线,从数据准备、模型训练到推理可视化全链路都有覆盖。适合正在入门自动驾驶感知方向的学生,也适合想快速验证分割方案效果的工程师。
2. 为什么语义分割能扛住车道线检测:从传统方案到分割模型
2.1 传统方案的天花板:边缘检测与霍夫变换的局限
很多人在接触深度学习之前,最早接触的车道线检测方案都是 Canny 边缘检测叠加霍夫变换。思路很直白:先提取图像里的边缘,再用霍夫变换找直线。这个方案在干净的高速公路上确实能跑,但一遇到真实路况就露怯。
首先是噪声问题。路面裂缝、轮胎印、护栏阴影、桥面伸缩缝,这些东西在边缘检测里全是“边缘”,霍夫变换会把这些噪声当成候选线段。其次是弯道问题,霍夫变换本质在找直线,遇到高速公路出口那种大曲率弯道,要么拟合出一堆短线,要么直接丢线,后处理再怎么写也救不回来。最要命的是光照变化,逆光时边缘被强光吞掉,晚上路灯下阴影和车道线灰度接近,雨天反光更是把边缘响应彻底打乱。我见过不少团队在传统方案上加了各种滤波、ROI 限制、跟踪平滑,效果说不上完全不能用,但始终处在“勉强能用但随时会崩”的状态。
语义分割解决的是问题本质上的变化。传统方案先想方设法找几何特征,再拿几何特征去匹配车道线;语义分割直接让模型从大量标注数据里学习“车道线像素长什么样”。它不依赖固定的边缘响应,也不假设车道线一定是直线,像素级别的分类输出天然就能处理弯曲、断裂和部分遮挡。这就是为什么近几年的车道线检测方案几乎都转向了深度学习。
2.2 语义分割、实例分割与目标检测:车道线任务该选谁
目标检测输出的是 bounding box,这对车道线来说是灾难。一条车道线可能贯穿整幅图像的上下区域,宽高比极端,而且没有清晰的边界框语义,一个框根本描述不了它;目标检测也天然处理不了“一条线中间断了一段但语义上还是同一条线”的情况。
实例分割相比目标检测进了一步,它输出每个独立物体的像素级 mask,能区分“这是第几条车道线”。但车道线检测真的需要区分个体吗?自动驾驶决策层更关心车道线的位置、曲率和左右边界关系,一般情况下需要的是“哪些像素属于车道线”,而不是“这条车道线的唯一 ID”。语义分割输出前景背景二分类 mask,计算量比实例分割小很多,网络结构也更简单,换来的精度损失在大多数场景里是可以接受的。
YOLO 新版本里经常把语义分割和实例分割放在一起对比,思路刚好能用来理解车道线任务:如果目标是给每个物体单独编号,选实例分割;如果目标是给整张图做像素级分类,选语义分割。车道线就是典型的后者。当然,像 LaneNet 这类方案在语义分割之外又加了一个 embedding 分支,用来把属于不同车道线的像素聚类区分,属于语义分割的增强版,后处理阶段我会在末尾单独说。
2.3 车道线分割的模型选型思路
确定了语义分割这个框架,接下来是选网络。这个选择基本由算力、实时性要求和训练资源三个因素决定。
| 模型 | 参数量级 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|---|
| UNet | 小 | 结构简单、容易训练、显存占用低 | 感受野有限,长距离上下文弱 | 入门跑通、CPU 推理验证 |
| DeepLabV3+ | 中 | ASPP 模块扩大感受野,边界细节好 | 训练收敛稍慢 | 精度优先的离线感知方案 |
| ENet | 很小 | 推理极快,适合嵌入式 | 精度上限较低 | 行车记录仪级硬件部署 |
| SCNN | 中 | 空间信息传播,擅长细长结构 | 实现复杂,推理慢 | 车道线专用、弯道场景 |
如果是第一次跑通这份源码,我一般建议从 UNet 入手。它结构对称,编码器下采样提取特征,解码器上采样恢复分辨率,skip connection 把浅层细节和深层语义拼在一起,对车道线这种细长目标很有帮助。DeepLabV3+ 的 ASPP 模块能同时用多个膨胀率的空洞卷积捕获多尺度信息,适合想直接上高精度方案的团队,但训练时间明显更长。SCNN 是专门为车道线设计的,它把特征图按行和列做空间传递,对断裂的车道线恢复能力极强,代价是速度。
实际项目里还有个容易被忽略的点:模型输入分辨率。语义分割网络的输出空间分辨率越低,显存占用越少,但车道线在低分辨率下会丢失大量细节。常见做法是把输入图像缩放到 512x288 这类接近相机纵横比的分辨率,保证车道线在纵向分辨率足够细,同时让计算量可控。
3. 把车道线检测项目跑通:Python 环境、数据加载与训练命令
3.1 Python 与深度学习环境配置:版本选择和 VSCode 调试
这份源码不管内部用的是 PyTorch 还是 TensorFlow,Python 环境配置的思路是一致的。我建议用 Python 3.9 到 3.10,这个区间里主流的深度学习库兼容性最稳,别一上来就装最新版本,很多库的预编译轮子还没跟上。
conda create -n lanedet python=3.9 -y conda activate lanedet conda install pytorch torchvision torchaudio cudatoolkit=11.3 -c pytorch pip install opencv-python pillow numpy tqdm albumentations这里选择 conda 而不是直接 pip 装 PyTorch,原因是 conda 会把 CUDA 运行时一起装好,省去单独折腾 CUDA Toolkit 的步骤。cudatoolkit=11.3 这个版本要看你机器的显卡驱动,如果驱动比较新,也可以装 11.8 或 12.x,对应关系在 PyTorch 官网上能查到。装完之后在终端里跑一句python -c "import torch; print(torch.cuda.is_available())",输出 True 就说明 CUDA 环境对了。
VSCode 配置方面,装好 Python 插件后最关键的一步是把解释器指向 conda 环境,命令面板里搜 Python: Select Interpreter,选 lanedet 环境,不然在 VSCode 的终端里明明激活了环境,调试器却用了系统 Python,经常出现包找不到的翻车现场。
3.2 读懂源码的目录结构与数据加载
拿到源码包先别急着跑训练,先把目录结构过一遍。车道线分割项目的代码组织通常比较固定,常见做法是 data 目录放数据集,models 目录放网络定义,utils 目录放指标计算和可视化工具,根目录下是 train.py 和 infer.py。项目说明文件里一般会写明每个脚本的作用,把这张地图理清,后面改参心理就有底。
数据加载是第一个会出问题的地方,最核心的细节是 mask 的读取和预处理。看一段典型的自定义 Dataset 实现:
# data/dataset.py import os import cv2 import torch from torch.utils.data import Dataset class LaneDataset(Dataset): def __init__(self, img_dir, mask_dir, input_size=(512, 288)): self.img_dir = img_dir self.mask_dir = mask_dir self.input_size = input_size self.img_names = sorted(os.listdir(img_dir)) def __len__(self): return len(self.img_names) def __getitem__(self, idx): img_path = os.path.join(self.img_dir, self.img_names[idx]) mask_name = self.img_names[idx].replace('.jpg', '.png') mask_path = os.path.join(self.mask_dir, mask_name) img = cv2.imread(img_path) mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) img = cv2.resize(img, self.input_size) mask = cv2.resize(mask, self.input_size, interpolation=cv2.INTER_NEAREST) img = torch.from_numpy(img).permute(2, 0, 1).float() / 255.0 mask = torch.from_numpy(mask).long() return img, mask这段代码有几个参数值得细说。input_size=(512, 288)表示宽 512、高 288,OpenCV 的 resize 参数顺序是 (宽, 高),和很多人的直觉相反。mask 的 resize 必须用cv2.INTER_NEAREST,如果用双线性插值,mask 里原本只有 0 和 1 的像素会被插出小数,变成灰蒙蒙的过渡带,训练时 loss 会被这些错误标签带偏。图像归一化放到__getitem__里统一除以 255,转成 float,而 mask 保持 long 类型,因为 PyTorch 的交叉熵损失要求目标张量是整型类别索引。
3.3 训练入口与关键参数解读
训练脚本的启动命令一般长这样:
python train.py \ --dataset ./data \ --model unet \ --epochs 120 \ --lr 1e-3 \ --batch-size 8 \ --gpu 0这些参数里,--lr 1e-3是初始学习率,用 Adam 优化器时这个值一般不用动;如果换成 SGD,建议降到 0.01 再按 0.9 的动量配。--batch-size 8在 512x288 输入下大概占 6 到 8 GB 显存,如果你的显卡只有 4 GB,要么把输入尺寸缩到 384x216,要么把 batch-size 降到 4。
训练过程中最值得盯的不是 loss 的绝对值,而是 mask 的可视化结果。项目说明里通常会给一个--vis之类的开关,每隔几十个 epoch 把验证集上的预测 mask 叠加在原图上存出来。我习惯每 5 个 epoch 手动看一眼,如果 mask 开始出现断裂或者模糊边界,说明网络容量不够或者下采样过度,这时候调参数才有方向。
损失函数是训练的灵魂。基础分类用交叉熵,但车道线分割里正负像素严重不均衡,背景占了绝大多数。常见做法要么给类别加权重,要么用 Dice loss 和交叉熵的组合。一条值得牢记的血泪经验:单用交叉熵在这个任务上很容易让模型陷入背景类的局部最优,输出全黑的预测图,训练前期看到这种情况先别急着乱调学习率,把类别权重加进去再试。
3.4 推理与可视化:从 mask 到车道线叠加图
训练完的模型做推理,逻辑比训练简单得多,核心就是前向传播加 argmax:
model.eval() with torch.no_grad(): logits = model(img_tensor.unsqueeze(0)) pred = torch.argmax(logits, dim=1).squeeze(0).cpu().numpy() overlay = img.copy() overlay[pred > 0] = (0, 255, 0)注意model.eval()不能省,它会把 Dropout 和 BatchNorm 切到推理模式。torch.no_grad()关掉梯度计算,既省显存又提高推理速度。argmax沿着通道维度取最大值索引,得到的就是每个像素的类别 ID,如果类别 1 代表车道线,pred > 0就是车道线区域,直接在原图上把对应像素涂成绿色保存,就能直观评估效果。
4. 自制车道线数据集:标注规范、格式转换与数据增强
4.1 标注工具与标注规范:线还是面决定模型上限
跑通预训练模型只是第一步,真正要落地必须有自己的数据集。语义分割数据集制作看起来简单,但标注规范定不好,后面所有工作都在还债。
工具层面用 labelme 就够,它支持多边形标注,导出的 JSON 格式也方便解析。真正需要花心思定的是规范:车道线是标成点线还是标成面。我的建议是标成有一定宽度的面,因为语义分割网络输出的是像素区域,如果标注只有单像素宽,下采样到 288 高度后这条线基本就消失了,模型学到的是一条断续的虚线。标成面并不需要在每个像素上抠边界抠得非常精细,保持在原图上 4 到 6 像素宽的均匀带子就可以,关键是左右边界要贴住车道线的实体边界。
遮挡和断开怎么标也容易出分歧。我一般的原则是:车道线被车辆遮挡时,能通过上下文推断的部分全部补齐标注;被严重径向模糊看不清的部分保持背景。这个原则的逻辑是,模型需要学会“从上下文恢复被遮挡的车道线”,而不是学会“看不见就放弃”。
4.2 把 Polygon 标注转成灰度 Mask:转换脚本
labelme 默认保存的是 polygon 坐标,训练需要的是和原图对齐的灰度 mask。转换脚本是每个项目都绕不开的一步,核心逻辑是解析 JSON 里的 shapes,用cv2.fillPoly填充多边形:
import json import cv2 import numpy as np def labelme_json_to_mask(json_path, img_shape=(288, 512)): with open(json_path, 'r', encoding='utf-8') as f: data = json.load(f) mask = np.zeros(img_shape[:2], dtype=np.uint8) for shape in data['shapes']: label = shape['label'] points = np.array(shape['points'], dtype=np.int32) if label == 'lane': cv2.fillPoly(mask, [points], 1) return mask这段代码里,img_shape要和你之后训练用的输入分辨率一致,避免 mask 和图像尺寸对不上。cv2.fillPoly的第二个参数要求传一个列表,每个元素是一个多边形,这个细节经常有人写错导致只画出最后一个多边形。类别 ID 的约定要全局统一,背景给 0、车道线给 1,如果以后要区分实线和虚线,就按 label 分别赋 1 和 2。
这个脚本跑完之后,必须抽几张图做叠加验证,把 mask 转成半透明红色叠在原图上。这一步能发现两类问题:一是标注坐标和图像尺寸的对齐关系错了,二是 fillPoly 在多边形自交时产生的异常区域。别迷信脚本输出,可视化才是最直接的质检手段。
4.3 训练用数据增强:雨雾、明暗与水平翻转
标注数据量不够的时候,数据增强是成本最低的补数据方案。车道线场景里最有效的增强不是随机裁剪,而是天气和光照模拟。albumentations 库里直接有现成的增强算子:
import albumentations as A from albumentations.pytorch import ToTensorV2 transform = A.Compose([ A.RandomBrightnessContrast(p=0.5), A.RandomShadow(p=0.3), A.RandomRain(slant_range=(-10, 10), drop_length=20, drop_width=1, p=0.2), A.HorizontalFlip(p=0.5), A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)), ToTensorV2(), ])这里有几个参数值得注意。RandomRain的drop_length控制雨丝长度,drop_width控制雨丝宽度,默认参数会让整个画面糊成一团,建议把drop_length设在 15 到 25 之间,drop_width保持 1 到 2。HorizontalFlip对二分类的语义分割是安全的,左右翻转之后车道线还是车道线,类别语义不变,但如果你的 mask 里要区分实线和虚线,翻转会改变语义,就要谨慎使用。
用数据增强时要同步增强图像和 mask,albumentations 的Compose会在内部保证image和mask用同一个随机种子做变换,这一点比手动写random逻辑可靠得多。增强强度不是越大越好,我见过有人把亮度对比度拉到极端,生成一堆完全失真的训练样本,模型反而学废了。
4.4 公开数据集与自采数据的组合策略
自己从零采集标注数据的成本很高,最划算的组合是先拿公开数据集跑通基线,再补充自采数据做领域适配。车道线检测领域最常用的公开数据集是 TuSimple 和 CULane,TuSimple 偏高速公路场景,CULane 包含市区、夜间、雨天等多种子场景,难度高一个量级。
这两个数据集的目录结构和标注格式类似,图像和标注一一对应,标注通常是 polyline 而不是 polygon。导入时自己写一个解析脚本,把 polyline 转成带宽度的 polygon,再走前面那套转换流程。遇到白天效果好、夜间崩的情况,一般不是模型问题,而是训练集里夜间样本占比太低,解决方案是把夜间数据在增强策略里单独加权重,或者直接混入更多夜间公开数据。
5. 车道线语义分割的避坑现场:5 个最常翻车的点
5.1 现象:loss 降不动,预测输出全是背景
新手拿到源码跑训练,第一次看验证结果大概率是失望的:模型输出一片全黑,一张 mask 上找不出几个车道线像素。这个现象出现的频率高到几乎可以称作这个方向的第一课。
原因分析起来也不复杂,语义分割在车道线任务上正负样本极度不均衡,背景类像素占比通常在 95% 以上。如果直接用交叉熵损失,模型只要学会把所有像素都预测成背景,损失就已经很低了。梯度更新时前景类贡献的信号被淹没,网络根本没有动力去学习车道线的特征。
解决思路有两条路并行。第一,给损失函数加类别权重,前景类权重设成背景类的几倍到十几倍,具体倍数根据训练集里两类像素的统计比例来定。第二,换用 Dice loss 或 Focal Loss,Dice loss 直接优化区域重叠度,对前景占比小的情况更友好。
5.2 现象:训练正常,但预测的 mask 断成碎条
loss 降得不错,前景也能被找出来了,但预测出来的车道线是一截一截的,断点在弯道和遮挡处尤其密集。这个坑几乎每个做过这个方向的人都会踩一遍。
原因主要是两个层面。第一,网络下采样倍数太高,UNet 这类模型下采样到输入分辨率的 1/16 甚至 1/32 时,lane 这种细长目标在低分辨率特征图上只剩几个像素,上采样恢复后中间的信息已经丢了。第二,标注质量不够,或多或少的标注断裂让模型学到的是残缺形状。
解决办法也很直接。先查标注,把所有 mask 和原图叠加逐张过一遍,重点看弯道附近的标注是否连贯。然后把网络输出的 mask 做一次形态学膨胀和闭运算去连接断点,OpenCV 的cv2.dilate和cv2.morphologyEx就能处理。如果还不行,说明标注和模型都有问题,考虑把网络输出的特征图分辨率从 1/16 提到 1/8。
5.3 现象:显存溢出,训练直接中断
显存溢出基本发生在刚把 batch-size 调大或者换了高分辨率输入的时候。报错信息一般是 CUDA out of memory,紧接着就是训练进程崩溃。
这类问题本质是显存管理没做好,跟真正的代码 bug 关系不大。解决路径按成本从低到高排列:先把batch-size降下来,直到训练能稳定跑完一个 epoch;然后把输入分辨率从 512x288 降到 384x216,这两条能解决大部分问题。还不够就开 PyTorch 的混合精度训练,让torch.cuda.amp自动给部分算子用 FP16 计算,显存占用能再降三分之一左右。最后可以用梯度累积,累积多个 batch 再更新一次参数,效果等价于增大 batch-size,但显存占用不变。
5.4 现象:白天效果好,夜晚和雨天预测崩掉
这是所有做视觉感知的工程师都绕不开的痛点。白天 mIoU 看着还不错,一换到夜间场景,预测 mask 彻底面目全非。
本质上这是训练数据分布和测试数据分布不一致的问题,模型在黑匣子里死记了白天的亮度纹理特征,没有学到车道线真正的结构特征。解决思路是给训练集补夜间数据,同时加光照扰动增强。另一个建议是检查 mask 里标注的完整性,夜间标注往往比白天更难做,如果夜间数据里本身有大量漏标,模型学到的就是噪声。
5.5 现象:两条相邻车道线被预测成一条
场景稍微复杂一点就出问题:两条实线挨得比较近时,模型输出把它们连成了一片,中间没有间隙。这在变道辅助里是致命错误。
原因通常来自两个方向。一是网络感受野的问题,大感受野让网络更倾向于输出连续区域,两条线的细间隙被“抹平”了;二是标注时两条线离得太近,polygon 标在一起了。查标注如果没问题,就在后处理里加一层形态学开运算,先腐蚀断开再膨胀恢复,能把粘连的细缝重新分出来。
6. 从 mask 到可用的车道线:后处理与模型验证技巧
训练完的分割模型输出是逐像素分类结果,但这个结果还不能直接交给下游控制模块,需要一轮后处理把它变成车道线的几何参数。我一般会在 mask 上做两件事:提取车道线中心点,然后做多项式拟合。
import numpy as np def extract_lane_points(mask, num_slices=18): h, w = mask.shape ys = np.linspace(h - 1, 0, num_slices, dtype=int) points = [] for y in ys: row = mask[y] xs = np.where(row > 0)[0] if len(xs) > 10: points.append((int(xs.mean()), y)) return np.array(points) def fit_poly(points, deg=3): coeffs = np.polyfit(points[:, 1], points[:, 0], deg) return coeffsextract_lane_points把图像纵向切成若干层,每一层取车道线像素的水平均值作为该 y 坐标下的车道线 x 坐标。之所以取均值而不是取某个边界点,是为了抵抗分割 mask 里的小噪声。fit_poly对 y 坐标拟合 x 坐标,用三次多项式而不是直线拟合,是为了还原弯道形态。拟合完之后,把多项式输出的 x 序列存成结构化输出,自动驾驶下游模块可以直接拿到每一条车道线的曲率和横向偏移。
验证指标方面,除了常规的 mIoU 和 Pixel Accuracy,我更建议单独拉一组指标:在纵向分层的采样点上计算预测中心线和真值中心线的横向误差,单位是像素。这个指标比 mIoU 更接近控制模块真正关心的东西,能直观反映车道线位置偏差。
最后说一个我自己的习惯:每次训练完多模态数据集,我都会挑出 CULane 里夜间、雨天、逆光三个子序列,专门跑一遍推理并录成视频,用肉眼反复看几遍。这个习惯已经帮我发现过很多 mIoU 掩盖掉的细节问题。自动驾驶方向没有捷径,模型考试分数再高,上路才是真正的考题。希望这些实战经验能帮你在车道线检测这条路上少走几步弯路,也祝你把这份源码跑成一套真正能上路的方案。
本文还有配套的精品资源,点击获取