车道线检测这个方向,可以说是计算机视觉里最贴近自动驾驶量产场景的任务之一。去年一个学弟找我帮他把关毕业设计,他选的项目就是“深度学习车道线检测(源码+论文)”,我陪他从选数据集、搭模型、调参一路走到写论文,过程中踩了不少坑。借这个题目,我把整条链路完整梳理一遍:从方案选型、数据集处理、网络设计、训练调参、后处理到论文写作,尽量说人话、给能直接落地的细节。
这个项目适合两类人:一类是正在做毕业设计、需要快速出一个能演示能答辩成果的同学;另一类是刚入门计算机视觉的开发者,想拿车道线检测这个具体场景把深度学习的完整流程跑通。做完你能收获的不只是检测效果,而是一整套从数据处理到模型部署的项目思维。
这次分享,我给学弟定的路线很明确:用语义分割做车道线像素级分类,再做曲线拟合输出可视化结果,也就是“分割 + 拟合”两步走。下面按做项目时的真实顺序来讲,这套顺序本身也是论文章节的顺序。
1. 为什么选深度学习方案:车道线检测的项目定位
1.1 车道线检测到底在解决什么问题
先搞清楚任务本身。车道线检测,输入是一张车辆前方摄像头拍到的图像,输出是图像中车道线的位置。这个位置可以用两种形式表示:一是每个车道线像素点的坐标,二是车道线的曲线参数。
不少人会问:车道线不就是几条线吗,用传统图像处理手段做不行吗?确实能,经典的做法是Canny边缘检测加霍夫变换,在理想的高速公路场景下效果尚可。但现实一复杂就顶不住了:路面有裂缝、光照阴影变化、雨天反光、车道线磨损、弯道曲率变大,传统方法全靠人工设计的特征,泛化能力很差。
深度学习方案的核心优势在于:车道线的视觉特征不是人手工设计的,而是从大量标注数据里自动学出来的。模型能学到“车道线是连续的长条结构”“车道线颜色相对固定”“车道线在路面上的位置有先验关系”这些高层语义信息。所以我跟学弟说,毕业设计选深度学习方案,技术上站得住脚,演示效果也更稳定。
1.2 深度学习做车道线检测的几种主流技术路线
当前主流方法大致分三类,各有取舍:
| 技术路线 | 核心思想 | 优点 | 缺点 | 适合程度 |
|---|---|---|---|---|
| 语义分割 | 逐像素分类,预测每个像素是否为车道线 | 直观、易于理解和训练、后处理灵活 | 需要处理正负样本不平衡 | 最适合毕设 |
| 回归/参数化方法 | 直接回归车道线曲线参数或逐行位置 | 轻量、速度快 | 车道线数量不固定时处理复杂 | 进阶可选 |
| 实例分割方法 | 区分不同车道线实例,每个车道线单独掩码 | 输出最完整、能应对车道变化 | 结构复杂、训练成本高 | 挑战型 |
我给学弟选的是语义分割路线。原因很实际:第一,语义分割的标签制作和理解成本最低,他把一条车道线的mask图打开就知道模型要学什么;第二,语义分割网络成熟,U-Net、ERFNet这类结构都有大量公开实现可以参考;第三,论文里好写:输入、输出、网络结构、损失函数都能讲清楚,答辩时不会被问得措手不及。
1.3 这个项目能锻炼哪些核心能力
很多同学做毕设容易陷入一个误区:以为跑通一个开源的检测脚本就算完工。实际上,毕业设计要锻炼的是“解决一个完整问题的能力”。车道线检测这个项目虽然方向不大,但五脏俱全。
做完它,你会经历这些环节:理解任务和评价指标、获取和处理真实数据集、设计数据增强策略、搭建网络模型、进行训练调试、设计后处理算法、评估效果、组织论文结构、整理可复现源码。这些能力,放到任何一个AI岗位的面试里都能拿出来聊,放到任何一个小型AI项目里都是通用技能。这也是我一开始跟学弟强调的:这个题目看上去是“检测”,实际上是一条完整的深度学习项目流水线。
2. 数据集与预处理:项目里最耗时间的部分
2.1 开源数据集怎么选
车道线检测有公开数据集可以直接用,最常见的是TuSimple和CULane。TuSimple来自高速公路场景,样本质量高,标注格式清晰,规模适中,是毕设的首选。CULane则包含更多城市道路、夜晚和拥堵场景,难度大很多,适合做扩展实验。
TuSimple的训练集大概是三千多张图像,验证集几百张,测试集约两千张。每张图像的分辨率是1280×720,帧率下图像之间是连续视频帧关系。对应地,标注文件是JSON格式,记录了每条车道线在特定行上的x坐标。这个数据规模对毕设来说不大不小:太小容易过拟合,太大训练成本高。
我给学弟的建议是:主线用TuSimple,如果能跑通,再考虑CULane的一个子集做泛化性实验。这样论文里既能说“在公开数据集上达到XX准确率”,又能写一段“在更复杂场景下的表现分析”,内容饱满度立刻不一样。
2.2 标注格式解读与mask生成
拿到TuSimple数据,第一步是解析JSON标注。每个JSON文件里有一个样本图像的标注,核心字段包括:
- lanes:一个二维数组,每条车道线存储了它在各采样行上的x坐标,没有的点用-999标记
- hSample:所有采样行坐标数组
- raw_file:原始图像的文件名
举个例子,如果采样行有60行,一条车道线在lanes中就是长度60的数组,第i个元素表示在该车道线在第i个采样行的x坐标,取值为-999说明这一行没有该车道线。
要训练语义分割模型,第一步就是把这种点坐标标注转成像素级的mask。转换逻辑很简单:创建一个全零的二维矩阵,尺寸与图像一致,然后对每条车道线,逐个有效的行-列坐标对,把对应像素置为1。由于标注点之间是稀疏的,需要用线性插值把同一条车道线在相邻采样行之间的点补齐,否则mask会出现断点。
我当初给学弟写了一个数据读取脚本,核心思路如下:
import json import numpy as np from PIL import Image def parse_tusimple_label(json_path, img_shape=(720, 1280)): with open(json_path, 'r') as f: data = json.load(f) mask = np.zeros(img_shape, dtype=np.uint8) h_samples = data['hSample'] lanes = data['lanes'] for lane in lanes: pts_x = [] pts_y = [] for y, x in zip(h_samples, lane): if x != -999: pts_x.append(x) pts_y.append(y) if len(pts_x) < 2: continue # 按距离插值,保证车道线连续 interp_x = np.interp( np.arange(pts_y[0], pts_y[-1]), pts_y, pts_x ).astype(int) interp_y = np.arange(pts_y[0], pts_y[-1]) valid = (interp_x >= 0) & (interp_x < img_shape[1]) & (interp_y >= 0) & (interp_y < img_shape[0]) mask[interp_y[valid], interp_x[valid]] = 1 return mask这段代码有个细节值得注意:我对车道线做了y方向的插值。因为原始标注只在采样行上有坐标,行与行之间没有点,如果直接用稀疏点训练,模型会学得很差。插值后车道线变成连续的细线,训练目标清晰很多。
2.3 数据增强组合:不能少也不能过度
车道线检测里的数据增强,核心目的是提升泛化能力,同时解决一个严重问题——正负样本比例失衡。一张720×1280的图像里,车道线像素通常只有几千个,背景像素接近九十万个,比例差不多1比几百。如果模型把所有像素都预测为背景,准确率依然高达99%,但毫无意义。
常用的有效增强方案包括:
- 水平翻转:因为左右车道线在形态上具有对称性,翻转可以有效增加数据多样性。需要注意的是mask也要同步翻转
- 仿射变换:随机小角度旋转、轻微缩放、平移,模拟车辆行驶中的微小姿态变化
- 亮度调整:模拟白天不同时间段的光线变化
- 颜色抖动:对HSV空间做微调,增强对不同路面颜色的鲁棒性
- 遮挡模拟:随机遮挡图像中某些区域,模拟前车遮挡、雨滴附着等场景
对学弟的训练,我让他用了一套组合:水平翻转、亮度抖动、小角度旋转、随机遮挡,增强概率各设为0.5。没有用特别复杂的CutMix这类方法,因为车道线是结构化的长条目标,强增强可能破坏语义结构,效果反而不稳定。这一条也是建议各位在做类似结构化目标检测时记住的原则:增强不是越强越好,要和任务特性匹配。
3. 模型搭建与训练调参:语义分割网络实战
3.1 网络结构:选轻量骨干还是选大模型
语义分割网络的选项不少:U-Net、DeepLab、PSPNet、ERFNet等。考虑到毕设场景下的硬件条件和训练周期,我给学弟推荐了两条路线:
第一选择是U-Net + ResNet18编码器。U-Net结构简单经典,encoder-decoder加跳层连接的结构非常适合车道线这种小目标分割,因为跳层连接能把浅层细节特征传递到解码端。ResNet18作为主干也不算重。
第二选择是ERFNet。这是一个专门为实时语义分割设计的轻量网络,大量使用非瓶颈残差块,参数量小、推理速度快。在TuSimple上,使用ERFNet类结构配合蒸馏训练,能取得非常好的效果,且CPU都能做推理。
我当时让学弟直接复现了ERFNet的核心模块——非瓶颈残差块,结构上包括先降维卷积、3×3卷积、再升维卷积,中间加上下采样或者膨胀卷积的变体。写出来并不复杂,却对最终效果提升很大。相比直接用U-Net,ERFNet在车道线这个场景下往往训练更快、内存占用更低。
输入分辨率上,不用直接使用原始1280×720,可以把图像resize到288×800或者360×640,这样的好处是显存占用大幅下降,训练速度更快,而车道线检测对空间细节的要求并不像目标检测对边框那么敏感。最终训练时我们用了360×640,在相对充裕的分辨率下尽可能保留细节。
3.2 损失函数:光有交叉熵不够
语义分割最常用的损失是交叉熵,但车道线场景必须考虑类别不平衡。背景像素占绝对主导,普通的交叉熵会让模型严重偏向背景。解决思路有两种:一是给车道线类别更高的权重,二是使用难样本挖掘类损失。
我用的是加权交叉熵加Lovasz损失组合。加权交叉熵让模型注意到少数类,Lovasz损失直接优化IOU这一评价指标,能与评估标准对齐。
加权交叉熵实现很简单,pytorch里CrossEntropyLoss自带weight参数,设置背景权重为1,车道线权重为5-10即可。Lovasz损失实现稍复杂,但网上公开代码很多,自己写也就几十行。这里给一个简化的思路:
import torch.nn.functional as F def weighted_ce(pred, target, pos_weight=8): return F.cross_entropy( pred, target, weight=torch.tensor([1.0, pos_weight]).cuda() )loss设计上还有一个选择,Focal Loss也适合这种极度不平衡的分割任务,它的数学形式天然抑制了易分类背景的梯度贡献。学弟后来在论文里做了一组消融实验:普通交叉熵、加权交叉熵、加权交叉熵+Lovasz,表格一列出来,效果差异非常直观。
3.3 超参数配置示例
训练超参数这块,我直接给学弟套了一组稳定可靠的配置。这也是我实测下来比较省心的组合:优化器用Adam,初始学习率1e-3,采用余弦退火调度;batch size设为8;训练轮数约60个epoch;输入分辨率360×640。此外,训练时开启混合精度训练,显存占用进一步降低。
这组参数下,单卡训练一个ERFNet大概半天能出不错的效果。如果显存只有6GB,可以把batch size降到4,分辨率降到288×800。注意对应的学习率也适当降低一点,比如5e-4,否则loss会震荡。
3.4 训练日志和可视化:判断模型好坏不能只看loss
训练过程中我让学弟每5个epoch保存一次模型和预测结果可视化图。具体做法是:从验证集里抽出几张图,把预测mask叠加到原图上保存成图片,直接肉眼看效果。这一步极其重要。Loss曲线下降只能说明模型在拟合训练集,预测mask能直接反映模型是否学到了车道线的结构模式,比如线是否连续、是否受到路面纹理干扰、弯道处是否跟得住。
他第一次训练时,训练loss一直在跌,验证loss也没过高,但可视化图里预测mask布满噪点。原因后来查出来是数据增强里的仿射变换参数太激进,标签被扭曲得厉害。如果只看loss曲线,这个问题根本发现不了。所以我对所有做视觉方向项目的朋友有个建议:可视化验证是一个基本工作习惯,宁可多花十分钟写保存逻辑,也不要全程黑盒调参。
3.5 部署框架和推理路径
训练完模型,需要把推理跑到视频或单张图片上。推理流程包括:读取图像、缩放、归一化、模型前向、softmax取最大值、生成mask、后处理拟合车道线、叠加绘制、输出结果。
为了视频测试方便,可以写一个简单的推理脚本,用opencv读视频流,处理完写入输出视频。注意推理时要做和训练时一致的数据预处理流程,包括尺寸、颜色空间、归一化均值方差,如果这里不一致,模型效果会明显下降。这个坑非常常见,学弟在验证时曾因为忘记归一化,整体mask预测结果大量缺失,排查了半天。
4. 从mask到车道线:后处理与可视化
4.1 先清理分割噪声
模型输出的原始mask会有很多细碎的误检区域,直接拿去画线会很乱。基本的后处理流程是:先对概率图做阈值二值化,通常0.5左右;然后对二值图做连通域分析,过滤掉面积过小的连通域。
我常用的是OpenCV的connectedComponentsWithStats函数,统计每个连通域的面积和位置,把面积小于某个阈值,比如200像素的连通域全部置零。这个操作能有效去掉远处的细碎误检,以及路面文字、裂缝产生的假阳性。
4.2 拟合曲线:别在图像空间里乱拟合
经过清理的mask仍然是像素集合,如果要输出一条平滑车道线,需要做曲线拟合。车道线形状上是一个长条的曲线结构。在这里有一个经验性的选择:不要用x作为自变量做多项式拟合,也不要在整个图像平面直接做普通的一次拟合。
更稳妥的做法是,按像素行从上到下遍历,在每一行找出mask中车道线像素的x均值。这样把一条车道线表示成一组(y, x)点对,然后对(y, x)做二次多项式拟合。也就是说,以y作为自变量,x作为因变量进行拟合。原因是车道线在图像中通常是纵向延伸的,用y作自变量更自然地对应了车道线“从近到远”的排布。
实现起来用numpy的polyfit即可:
import numpy as np def fit_lane(ys, xs, order=2): # ys: 行坐标, xs: 对应行的车道线平均x坐标 coeffs = np.polyfit(ys, xs, order) return coeffs def predict_x(coeffs, y): return np.polyval(coeffs, y)拟合时还有一个细节:远处车道线像素非常稀疏,拟合误差容易大,可以考虑只使用近处的有效点,或者过滤掉太短的段。一般来说,用最近300个有效点拟合效果就足够,多项式阶数不建议超过3,阶数太高会带来震荡,尤其是远处噪声被放大的时候。
4.3 透视变换:要不要上鸟瞰视角
在车道线检测实际系统中,一个常见操作是把图像通过逆透视变换映射到鸟瞰图,再在鸟瞰图上做拟合和追踪,这样得到的车道线是平行的,拟合建模更准确。很多论文都会带这样的模块。
但对于毕设来说,我会建议后续有时间再扩展。原因很简单:透视变换需要标定相机参数或手工选点计算变换矩阵,处理不好反而引入畸变。而直接在图平面拟合,对TuSimple数据集上的演示效果已经足够。如果论文里想在方法上多一个亮点,可以把透视变换放在“扩展讨论”里,比较有无透视变换的拟合效果差异,这样有理有据,还不影响主线进度。
4.4 视频测试:要让演示效果满足答辩要求
最后生成一个演示视频,让车道线实时叠加上去。这里有个朴素但实用的经验:视频平滑很关键。单帧模型的预测会存在抖动,直接叠加到视频上看起来会闪烁。解决办法是简单的滑动平均:维护最近N帧的拟合系数,取平均后作为当前输出,或者用指数移动平均。我让学弟维护一个长度5的队列,对二次拟合系数做平均,视频稳定性立刻上升一个档次,答辩演示的观感比单独跑一张测试图好太多。
5. 实验评估与论文落地:把效果写清楚
5.1 评价指标:别用一套自己都说不清的数字
TuSimple官方指标是准确率,定义是预测车道线上的点,在给定像素误差范围内和真实车道线点的匹配比例。具体来说,对每条车道线,在采样行上计算预测点与真实点的距离,如果距离小于某个阈值,比如20像素,就认为是正确匹配点。最终准确率等于总正确匹配点除以总匹配点。
实际操作中,很多毕设会直接算分割的OIoU或者像素准确率。我认为这两个指标都可以用,但要在论文里讲清楚定义。最合理的做法是:
- 主指标采用TuSimple官方Accuracy,便于和其他已发表工作对比
- 辅助指标采用车道线类别的IoU,直观反映分割质量
如果你不知道怎么算官方指标,可以从数据集评测代码里理解逻辑,逻辑概括起来就是“预测的车道线像素落在真实车道线附近即算匹配”。这个理解到位后,自己实现一个也不难。
5.2 对比实验和消融实验:论文里怎么设计实验
论文写入关键是要有对比。很多毕设只放模型在自己数据集上的效果图,答辩老师一问“你和什么方法对比过”就卡壳。合理的实验设计至少包含三组:
- 不同骨干网络对比:U-Net、ERFNet、简单CNN三者比较
- 不同损失函数对比:交叉熵、加权CE、加权CE+Lovasz
- 不同输入分辨率对比:288×800、360×640、以及原始分辨率
- 最好再加一组传统方法的基线,Canny+霍夫变换,作为深度学习的对照
这些对比实验都做完后,论文实验章的内容量就够了,而且每个结论都有数据支撑。需要提醒的是,实验设置必须保持一致:用同样的训练轮数、同样的数据增强、同样的评估脚本,只有控制变量得到的对比表格才有说服力。
5.3 论文结构建议
做这个方向的论文,我建议章节结构如下:
第一章绪论:写背景和意义,讲清楚车道线检测在辅助驾驶和自动驾驶里的作用,然后概述你的工作和论文组织。
第二章相关工作:分传统方法和深度学习方法,各写一小节。不需要把网上所有模型都列一遍,挑两个主流结构做代表即可。
第三章方法:这是核心章。写清楚数据预处理、网络结构、损失函数、后处理拟合。网络结构要给出具体模块图和参数表,不要只贴一段代码。
第四章实验:数据集介绍、实验设置、指标定义、对比实验、消融实验、可视化结果展示。
第五章总结与展望:总结工作,说局限,简单提一下后续改进方向。
论文里最容易出问题的地方是图。车道线检测非常适合用图说话:模型结构图、数据增强效果图、预测mask可视化图、拟合结果图、对比效果图。多放图,答辩时讲起来更容易。
5.4 源码交付应该怎么整理
毕设源码不是把训练脚本堆上去就行。我在给学弟规划目录时,定了这样一套结构:
lane_detection/ ├── data/ # 数据读取、标注解析、数据增强 ├── models/ # 网络结构定义 ├── utils/ # 损失函数、可视化、评价指标 ├── configs/ # 配置文件或超参设置 ├── train.py # 训练入口 ├── test.py # 测试与推理入口 └── README.md # 使用说明README是很多人忽略的部分,但答辩和查重时会看。里面至少要写清楚:环境依赖(包括pytorch版本、opencv版本)、数据集下载地址和放置方式、训练命令、测试命令、模型输出说明。一个好的README会大幅提升整个项目的完成度观感。
6. 常见问题与避坑指南
6.1 训练阶段的四个典型问题
训练期间最常遇到的是loss异常。有一种典型情况是新模型loss不下降或下降极慢,先检查学习率。学弟曾把学习率设成1e-2,结果loss直接变成NaN。Adam的默认学习率1e-3在多数情况下都够用,不要一上来就激进调大。
另一个高频问题是标签错位。数据增强对mask和图像分别处理后,我让他检查翻转后的图像和mask能否对齐。有一次他忘了在水平翻转时翻转mask,训练出来的模型车道线预测全部偏到另一边,效果奇差。这种问题和模型无关,纯属数据处理代码bug,但排查起来很费时间。训练前写几个样本的可视化输出,图-标签一对一对上,能很大概率提前发现问题。
显存不足也很常见。我们的方案是降低分辨率、减小batch_size、开启混合精度。不建议为了省显存随意裁剪图像,那样车道线的结构信息会被切断。
6.2 推理阶段的三个典型问题
预测mask噪点多,先看是否是输入数据预处理不一致导致的,比如没有归一化、没有缩放。如果不是,再调后处理里的面积阈值。
车道线断裂严重,特别是远端的车道线几乎消失。这时考虑几个方向:一是训练时增加遮挡增强,让模型更鲁棒;二是后处理里加形态学膨胀,把邻近像素连通;三是训练数据上的车道线标注本身存在断开,需要检查插值逻辑。
弯道拟合效果发散,这往往是多项式阶数过高,或者拟合时包含了过远的噪声点。把阶数降到2或3,限制拟合点的纵向范围,一般能改善。
6.3 最容易被忽视的三个全局性细节
- 随机种子:代码里设置全局随机种子,保证实验可复现。训练前我让学弟固定seed=42,后来做消融实验时,不同模型间才能公平对比。
- 归一化参数:训练和推理必须使用相同的mean和std。如果训练时用的是ImageNet的均值方差,推理时也要一致。
- 指标计算的一致性:评估脚本只跑一次,同一个指标不要一会儿用官方实现一会儿用自己实现的版本。建议把评估代码固定下来,所有对比实验都用它。
这三点都是“细节决定成败”类型的坑,不踩过一次很难意识到,但踩过之后就觉得该写进经验贴里。
7. 个人经验与后续扩展
做完这个项目,我最大的体会是:车道线检测的公开资料和开源代码足够多,但把一条完整链路跑通并写成令人信服的论文,依然要花不少时间。最有效的做法是先“窄后宽”,先把最基本的模型、最基本的后处理链路跑通,再逐步增加对比实验和扩展实验,而不是一上来就想做很多花哨改进。
如果学弟后续有时间,我建议他在三个方向上做扩展:一是把数据集从TuSimple换成CULane,体会真实复杂场景的挑战;二是尝试实例分割方法,区分不同车道线并输出多种类型;三是把模型导出为ONNX,做一次部署层面的优化。这三个方向每一个都能独立成为一篇进阶文章,但它们都建立在当前这个基础项目之上。
另外确实想说一句:训练完成的模型在公开数据集上表现不错,和真正在实车上路测试的感受完全不同。实车测试会面临更多不可控因素,比如强烈的反光、前方车辆遮挡、路面补丁和旧线重叠。这些场景虽然从一开始就被各种数据增强模拟了很多,但真到了实际环境里还是会有不少新问题。
如果让我给后来者一个最后的建议,那就是:动手永远比看教程重要。这个项目所有环节都在推着你做决策——标签怎么处理、网络怎么选、损失怎么设计、曲线怎么拟合,每一个决策都没有标准答案。把决策过程记录下来,本身就是论文素材。等最后看到自己训练的模型在一段真实道路视频上稳稳画出车道线时,你会觉得前面所有调参的夜晚都值了。