简介:车道线检测是自动驾驶与智能交通中的核心视觉任务,对光照、天气和道路变化都有较高要求。项目源码包基于Python卷积神经网络,完整覆盖数据加载、模型搭建、训练评估与推理演示,并附带已在公开数据集上训练好的模型权重,可直接运行演示脚本观察检测效果,适合作为毕业设计、课程项目或科研入门参考。压缩包共包含91个文件,整体约548MB,主要类型有21个Python脚本、33张测试/结果图片、3个预训练模型权重,以及txt说明、配置文件和C++辅助代码等,目录按数据、模型、工具、评估等模块划分,便于按需查阅。当前已有74人学习下载。配套文档对环境安装、参数调优、数据预处理和实时视频流处理等关键步骤给出指引,同时提供Culane/Tusimple训练模型与示例图片,用户可以先跑通推理,再深入研读训练逻辑,学习路径清晰、上手门槛较低。
1. 车道线检测源码包拆解:Python 卷积神经网络、训练好的权重,直接跑通需要几步?
做毕业设计最怕的不是没想法,而是想法有了、环境装了、代码也下载了,结果demo.py一跑就报错。这套车道线检测源码包就是给这种场景准备的:Python 写的卷积神经网络训练与推理流程、CULane 和 Tusimple 两套主流数据集配置、culane_18.pth和tusimple_18.pth两个训练好的权重可以直接加载,另附作业报告和说明文档。无论想快速验证算法效果,还是拿来做毕设基线和改进起点,都不用从零起步。但直接跑之前,有三件事必须先确认:PyTorch 版本和显卡驱动对不对得上、数据集路径有没有配进configs/、加载权重时 backbone 和训练时是否一致。这篇就按这个顺序拆,从文件结构到训练细节,最后把最容易翻车的地方一个个点出来。
2. 工程结构拆解:从 Lane-Detection 文件树看懂训练、推理、评估的分工
拿到压缩包解压之后,第一反应通常是文件怎么这么多。实际上这套代码的目录组织是清晰的,只是把 Python 工程里该有的东西都备齐了。我拿到视觉项目有个习惯:先分三路看——训练入口、推理入口、评估入口。这套源码里对应的是train.py、demo.py和evaluation/。把这三条线捋顺了,剩下就是配置和数据的问题。
2.1 三条主线:train.py、demo.py 与 evaluation/ 各管什么
先看根目录。train.py是训练脚本,承担数据加载、模型初始化、损失计算、反向传播、checkpoint 保存这些完整环节。demo.py是单张图片推理脚本,读入一张图、加载权重、前向推理、把车道线画回原图并保存,适合第一时间验证模型有没有跑通。requirements.txt列的是第三方依赖库,包括 torch、torchvision、numpy、opencv-python、tensorboard 这些。
model/目录下是模型定义,data/目录下是数据集加载与预处理,configs/下是不同数据集的配置,utils/下是 loss、metrics、factory、dist_utils 这类杂项。这种划分在 PyTorch 项目里很常见,好处是想改模型结构不用动数据代码,想换数据集不用改模型代码,训练和推理是两条独立链路。
data/constant.py存的是数据集路径和类别常量,data/dataset.py定义了怎么从磁盘读图、读标注、做 transform,data/dataloader.py封装了 DataLoader 逻辑,包括 shuffle、多进程读取、pin_memory 这些工程细节。data/mytransforms.py是自定义的图像变换,比如随机裁剪、颜色抖动、随机翻转,训练时做数据增强用,推理时不需要。
还有一个容易被忽略但值得说的cpp/目录,里面有 CMakeLists.txt 和 build.sh。这是把模型往 C++ 部署方向引的尝试,核心推理逻辑用 LibTorch 或 ONNX Runtime 重写,训练和 PyTorch 推理仍是 Python。对做毕业设计来说,C++ 部分可以放着不碰,不影响主流程,但如果你答辩时需要展示工程化能力,这倒是个加分项。
作业报告.docx和说明.txt是文档部分,里面有环境安装步骤和运行说明。out/目录存的是推理输出图,tmp/和__pycache__/是运行产生的临时文件,可以忽略。
两个.pth权重文件分别对应 CULane 和 Tusimple 数据集。这两个数据集的车道线定义、图片分辨率、标注方式不同,权重不能混用。比如你用configs/tusimple.py加载culane_18.pth,大概率运行时报 shape 不匹配,或者不报错但输出乱七八杂——最后一个卷积层的输出通道数对不上,或者即使对上了也因训练分布不同而失效。
2.2 model 与 backbone:CNN 是怎么把车道线特征抽出来的
车道线检测的核心是卷积神经网络。backbone.py负责特征提取,把 288×800 或 288×512 的输入图像通过一系列卷积、池化、残差连接,变成分辨率更低但通道数更多的特征图。model.py在此基础上接检测头,输出每个像素属于车道线的概率。这套项目用的是典型的 encoder-decoder 结构,配置文件configs/culane.py和configs/tusimple.py里能看到 backbone 类型、输入输出尺寸这些参数。
用 CNN 做车道线检测,相对目标检测要简单直接:不需要 anchor、不需要 NMS,模型输出就是和输入分辨率相关的分割图或置信度图。训练时的监督信号是标注好的车道线 mask,一张图上每条线做成一个类别或二值图。CULane 把车道线分成了 4 类,Tusimple 是固定 4 条线,这决定了模型输出通道数的设置。
实际读model.py的前向过程,你会看到 backbone 提取特征后,经过几个上采样模块逐步恢复分辨率,最后用 1×1 卷积输出每个像素的类别概率。中间可能会插入辅助分类头做深度监督,加速收敛。训练时主损失用交叉熵,配合utils/loss.py里的辅助损失一起反传。
调参时你需要关心的其实就几件事:输入尺寸、batch size、学习率、类别数。真正改网络结构的场景在毕设里很少,更多是调后处理和训练策略。我实际跑过之后的体会是,CNN 在这个任务上的核心优势在于对光照变化和噪声的鲁棒性。车道线本身是细长条结构,普通分割网络容易漏检,所以很多方案会额外引入 row anchor 或线段约束,但这套代码走的还是像素级分割路线,好处是代码好懂、好改,坏处是极端场景下容易出现断线。后续优化可以从后处理角度对输出 mask 做形态学闭运算,把断线连起来,最后一章我会单独讲。
2.3 配置文件对比:CULane 和 Tusimple 的路径与超参数差异
configs/下两个文件内容不长,核心是数据集路径、输入分辨率、类别数、训练轮数、学习率策略这些。CULane 和 Tusimple 是两种差异很大的数据分布,前者是车载摄像头采集的城市道路,场景复杂、车道线类型多;后者是高速公路场景,车道线相对规整。所以两个配置文件里的关键参数几乎都不一样,训练入口也要区分开。
| 对比维度 | CULane 配置 | Tusimple 配置 |
|---|---|---|
| 标注类别 | 多类别车道线 | 固定 4 条线 |
| 图片场景 | 城市道路、拥堵、光照变化大 | 高速、结构规整 |
| 训练难度 | 更高,需要更多 epoch | 相对容易收敛 |
| 适用的权重 | culane_18.pth | tusimple_18.pth |
读配置的时候注意看data_root和dataset字段,这两个直接决定跑训练的时候数据从哪读。很多人拿到源码喜欢直接把train.py跑起来,结果报 FileNotFoundError,八成就是路径没改。这套代码不会帮你自动下载数据集,数据要自己准备,下一章细说。
3. 上手实操:环境配置、数据集转换、训练与推理命令
这一章的价值在于让你在 30 分钟内把 demo 跑通,然后再决定要不要碰训练。整个过程分三步:装环境、备数据、跑脚本。每一步都有容易翻车的地方,我按自己的经验把检查顺序写出来。
3.1 环境安装:requirements.txt 与 PyTorch 版本匹配
先看 requirements.txt 里锁了哪些版本,再决定用 conda 还是 pip 装。常见做法是先建一个独立的虚拟环境,避免把系统 Python 搞乱:
conda create -n lanedet python=3.8 -y conda activate lanedet pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install -r requirements.txt这里有个细节:--index-url指定的是 CUDA 11.8 的 PyTorch 预编译包,如果你的显卡驱动版本较老,可以换成 cu113 或 cu117。判断依据是nvidia-smi显示的 CUDA 版本,只要驱动版本 >= PyTorch 要求的 CUDA 版本就行,并不是驱动版本越高越好。如果机器没有 NVIDIA 显卡,把--index-url去掉装 CPU 版 torch 也能跑 demo,只是训练会慢到让你怀疑人生。
装完验证一下环境:
python -c "import torch; print(torch.__version__, torch.cuda.is_available())"输出的torch.cuda.is_available()是True,说明 GPU 可用。如果是False,检查是不是装了 CPU 版,或者驱动版本不对。这一步不确认清楚,后面训练和推理可能出现各种玄学问题,比如模型加载慢、显存报错、甚至直接段错误。
3.2 数据准备:CULane/Tusimple 目录结构与 convert_tusimple.py
数据集是这次实操里最耗时间的一环。CULane 和 Tusimple 都需要自己下载,代码不负责拉数据。下载完解压后,目录结构要跟configs/culane.py里的data_root对得上。我通常的做法是先在配置里把data_root指到数据集所在绝对路径,避免相对路径在不同机器上失效。
Tusimple 原始标注是 JSON 格式,一行一个标注,包含 lane 的 x/y 坐标和类别。这套源码在evaluation/scripts/convert_tusimple.py里提供了转换脚本,作用是把 JSON 标注转成训练需要的 mask 格式。运行方式一般是:
python evaluation/scripts/convert_tusimple.py \ --src /path/to/tusimple \ --dst /path/to/tusimple_converted转换脚本做的事情,是把每条车道线的散点坐标连成线,然后在 mask 图上把线宽画出来。这里有一个容易忽略的参数:线宽。Tusimple 标注的原始点是单像素,如果直接画成 1 像素宽,训练时正负样本极度不平衡,模型容易学成"全预测为背景"。常见做法是把线画成 8 像素宽,或者用高斯分布生成一个软 label,这样损失函数才有足够的正样本梯度。
CULane 数据集本身提供的是分割 mask,不需要转换,直接配置路径就能用。但 CULane 的文件结构比较散,要确认list/目录下的 train.txt、val.txt 里写的相对路径和实际文件位置一致,否则 DataLoader 读不到图。
3.3 训练与推理:train.py 和 demo.py 的参数与输出
环境装好、数据就位后,先跑推理验证模型:
python demo.py --config configs/culane.py --weights culane_18.pth --img data/00000.jpg--config指定配置文件,--weights指定权重路径,--img指定输入图片。跑完之后会在out/目录下生成带车道线标注的结果图。
注意权重和配置的对应关系:CULane 权重配 CULane 配置,Tusimple 权重配 Tusimple 配置。我见过有人把tusimple_18.pth塞给culane.py,结果输出图像分辨率不对,检测线位置全偏,还以为是模型坏了。并不是模型坏了,是配置和权重不匹配。
推理没问题了,再碰训练:
python train.py --config configs/culane.py训练时的关键参数在配置里改:batch_size显存不够就调小,optimizer一般用 SGD 或 Adam,lr初学者建议从 1e-3 起步,epochCULane 上一般要几十轮才能看到像样的效果。训练过程中 tensorboard 会记录 loss 和 mIoU,建议每训练几分钟看一眼 loss 曲线,如果 loss 完全不动,赶紧停下来查,不要干等。
训练完成后在out/或tmp/下会保存 checkpoint,里面是模型权重和优化器状态。加载时用torch.load读进来,注意 map_location 参数:
checkpoint = torch.load("culane_18.pth", map_location="cuda:0") model.load_state_dict(checkpoint["model"])如果模型是单卡训的,加载到多卡环境会碰到module.前缀问题。常见解决办法是加载时把 key 里的module.去掉,或者strict=False加载后再打印 missing_keys 逐项排查。这个属于经典坑,我在下一章的排查清单里具体写。
4. 避坑排查:车道线检测从训练到部署的五个高频问题
这一章整理的是我自己跑这套源码时真实踩过的坑,按"现象 → 原因 → 解决"的格式写。每条都对应一个具体的排查路径,新手照着走就行。
4.1 训练时 Loss 不降或乱跳
现象:训练跑了十几个 epoch,loss 一直在 0.7 上下波动,不下降,或者从一个很大的值突然跳到 NaN。
原因排查:先分清是"不降"还是"炸了"。不降,多半是学习率太低或者数据没对齐;NaN,多半是学习率太高、loss 里有除零、或者数据里带了异常值。最容易被忽略的是数据对齐问题——输入图是 BGR 还是 RGB、归一化参数是不是和预训练权重一致,这些不对,CNN 学到的特征全乱套。
解决:学习率先降到 1e-4 试试;检查data/mytransforms.py里的归一化均值和标准差;确认加载预训练权重时strict=True没有被意外禁用,导致 backbone 从随机初始化开始训。如果数据是 RGB,而预训练模型是 BGR 顺序,把通道换回来,很多时候 loss 就正常了。
4.2 推理结果全黑或车道线错位
现象:demo.py跑完,输出的 mask 图全黑,或者画出来的车道线整体偏移、交错。
原因排查:全黑说明模型输出的概率全部被阈值卡掉了,大概率是配置文件里的类别数或输入尺寸和权重不匹配,导致输出 shape 异常。车道线偏移,常见原因是推理时输入图做了 resize,但画结果时没做坐标变换,直接在原图尺寸上叠加了 resize 后的 mask。
解决:先在demo.py里打印模型的输出 shape,确认和配置里的num_classes一致。画线之前,把 mask 用cv2.resize还原到原图尺寸,或者干脆全程保持同一尺寸。我在调试时会加一行断言:
assert mask.shape[:2] == img.shape[:2], "mask 和原图尺寸不一致,先 resize 再叠加"这样问题能立刻暴露,不用对着输出图猜。
4.3 训练时显存溢出(OOM)
现象:CUDA out of memory,训练跑不了几步就崩。
原因排查:显存溢出本质是 batch_size × 输入分辨率 × 模型参数量超出显存上限。CULane 输入分辨率高,backbone 又占了大量显存,默认 batch_size 太大很容易爆。
解决:先把 batch_size 调小一半试试。还不行,就把输入分辨率从 800×288 降到 640×256 之类,代价是检测精度下降。如果还爆,启用梯度累积,每 4 个 batch 反传一次,等效增大 batch_size 但显存占用不变。我一般这样改:
optimizer.zero_grad() loss.backward() if step % accumulation_steps == 0: optimizer.step()accumulation_steps设成 4,相当于 batch_size 不变的情况下模拟了 4 倍 batch 的训练效果,收敛会更稳定。
4.4 加载 .pth 权重报错:missing keys 或 unexpected keys
现象:load_state_dict报 missing keys 或 unexpected keys,模型加载失败。
原因排查:权重是在某个具体模型结构下训练出来的,如果代码里的模型类被改过——比如换了 backbone、改了输出类别数——权重就对应不上了。还有一种情况是训练时开了DataParallel,保存的权重 key 都带module.前缀,加载到单卡模型就会报 unexpected。
解决:先区分两类错误。missing keys 通常是模型比权重多了一些层,常见于修改了 head 输出;unexpected keys 通常是权重比模型多,常见于 DataParallel 前缀。前者可以用strict=False加载,然后冻结 backbone 只训练新 head;后者用字符串替换去掉module.:
state_dict = torch.load("tusimple_18.pth", map_location="cpu")["model"] new_state_dict = {k.replace("module.", ""): v for k, v in state_dict.items()} model.load_state_dict(new_state_dict)map_location="cpu"也是关键,它避免在没 GPU 的机器上加载报错,同时方便排查 key 的差异。
4.5 评估脚本路径报错或指标对不上
现象:跑evaluation/下的脚本时 FileNotFoundError,或者算出来的 F1/mIoU 数值明显不合理。
原因排查:评估脚本通常需要三类路径配置:模型预测结果目录、标注 ground truth 目录、以及 list 文件。三个路径只要有一个不对,脚本就挂。指标对不上,多半是评估时用的预测分辨率或类别映射和 ground truth 不一致。
解决:先读eval_wrapper.py开头的路径配置,把pred_dir、gt_dir、list_path都改成绝对路径。类别映射要在data/constant.py里核对,CULane 的 4 类对应关系不能错。我在跑评估前会单独写一个脚本,随机挑三张图的预测 mask 和 GT mask 叠加可视化,肉眼确认对齐了再跑全量评估,能省下不少排错时间。
5. 评估与调参:把车道线检测从"能跑"压到"稳定"
模型跑通只是第一步,毕设答辩和实际项目更关心的是精度指标和稳定性。这一章讲怎么用这套源码自带的评估体系,以及哪些参数值得动。
5.1 eval_wrapper.py 与官方评估逻辑
evaluation/目录下分culane和tusimple两个子目录,对应官方评估工具。eval_wrapper.py是统一入口,它做两件事:把模型输出的原始 mask 整理成评估工具能读的格式,然后调用官方脚本计算指标。Tusimple 的官方指标是准确率和 FP/FN;CULane 是逐帧 F1 分数。CULane 还按场景细分了正常、拥堵、夜间、雨夜等类别,评估时会分别输出每个场景的 F1,这个对论文里的消融实验很有用。
跑评估前要确认模型输出被正确阈值化成二值 mask,然后用骨架线提取算法把 mask 细化为单像素线,再和 GT 做匹配。CULane 的匹配规则是:预测线和 GT 线的 IoU 超过 0.5 才算 True Positive。这个阈值在官方代码里是写死的,不用改,但要理解它对结果的影响。
5.2 关键参数对精度的真实影响:输入分辨率、类别数、loss 权重
调参之前先得知道哪些参数动了有效果。我在这套源码上做过的实验,按影响力排序大概是:输入分辨率 > 类别数设置 > loss 权重 > 学习率策略。
输入分辨率影响最直接。分辨率低,小目标的细线很容易在降采样过程中丢失;分辨率高,显存占用和训练时间线性上涨。CULane 默认配置的输入尺寸是个平衡点,但如果你想提升雨夜和夜间场景下的 F1,把长边加到 800 以上会有肉眼可见的提升,代价是单卡训练可能要等更久。
类别数设置影响输出通道和损失计算。Tusimple 固定 4 条线是最简单的情况,num_classes=4输出 4 个通道,取 argmax 得到线索引。CULane 的 4 类是按车道线属性划分的,输出同样 4 通道。不要为了省事把num_classes改成 1 去做二分类,会丢掉类别信息,评估时的 F1 也会因为类别对应错乱而失真。
loss 权重主要看utils/loss.py里的实现。如果代码里有主损失 + 辅助损失的结构,辅助损失的权重一般取 0.3~0.5。这两个损失对梯度贡献的量级不同,如果权重配比不合适,训练早期会出现 loss 震荡。我的经验是先把辅助损失权重设为 0,主损失训到稳定再逐步加上去,否则问题互相干扰,很难定位。
5.3 一个可复用的调参实验流程
我每次拿到未知数据集,习惯用同一套调参流程,这里分享出来:
第一步,固定 backbone 和损失函数,先用小数据集跑 5 个 epoch,确认代码链路没有 bug。第二步,用默认配置跑满训练,记录 baseline 指标。第三步,做单变量实验,每次只改一个参数,比如只改输入分辨率、只改学习率,记录 F1 变化。第四步,把效果最好的几个单变量结果合到一起,再跑一轮确认。
这套流程的关键是"每次只改一个变量"。很多人调参喜欢一次改五个地方,最后哪个起作用了都不知道,等于瞎调。训练日志里建议用tensorboard记录每个 epoch 的 train loss、val F1、学习率曲线,调参时翻日志比翻代码快得多。
还有一个容易被忽略的点:backbone 的预训练权重。这套源码虽然带了训好的权重,但如果你要重新训练,backbone 部分最好加载 ImageNet 预训练模型,而不是随机初始化。随机初始化的 CNN 在小数据集上很难收敛,收敛了精度也差一截。看model/backbone.py的代码,如果它支持传入 pretrained 参数,pretrained=True会从 torchvision 拉权重;如果没这个参数,手动下载对应模型文件再加载也行。
6. 进阶:把模型接到视频流里做连续帧车道线检测
模型在单张图片上效果不错,但实际开着视频跑一遍就会发现,逐帧推理的结果在时间轴上抖得厉害。原因是相邻帧车道线位置变化不大,模型却会因为光照、阴影、车辆遮挡等噪声,输出和上一帧相差甚远。解决思路就一个字:平滑。
我处理视频流时,会维护一个车道线置信度缓冲队列,取最近 5 帧的预测结果做加权平均,权重给最近的帧更高。这样即使某一帧检测失败,也不会导致画面突然消失。具体实现上,OpenCV 读视频循环,每帧预处理后走一遍模型,拿到 mask 后先做形态学闭运算补断线,再求加权平均:
import cv2 import numpy as np mask_queue = [] alpha = 0.7 # 当前帧权重 while cap.isOpened(): ret, frame = cap.read() if not ret: break # 预处理:resize + 归一化,和训练时保持一致 input_tensor = transform(frame.copy()).unsqueeze(0) with torch.no_grad(): prob = model(input_tensor).squeeze(0).cpu().numpy() mask = (prob.argmax(0) * 255).astype(np.uint8) kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) if len(mask_queue) == 0: smoothed = mask else: smoothed = cv2.addWeighted(mask, alpha, mask_queue[-1], 1 - alpha, 0) mask_queue.append(smoothed) if len(mask_queue) > 5: mask_queue.pop(0) # 把平滑后的 mask 叠回原图 overlay = frame.copy() overlay[mask > 0] = (0, 255, 0) cv2.imshow("lane", overlay)这段代码里的alpha是关键参数。alpha太大平滑效果弱,画面还是会闪;太小画面拖影严重,车道线位置明显滞后。我一般从 0.7 起步,帧率 30 以上可以调到 0.5,帧率低就调高,保证实时性和稳定性的平衡。
MORPH_CLOSE这个操作可能有人不熟。它是先膨胀再腐蚀,作用是把预测 mask 上细小的断裂处连接起来。车道线是细长结构,经过分割网络输出后经常出现中间断点,闭运算能有效补上。核大小 5×5 是个起步值,分辨率越高可以适当调大。
从那以后,我做车道线检测的演示和视频实验,都会把这段平滑后处理强制加上,哪怕只是临时跑个 demo 也不跳过。它不改变模型本身,但让输出结果从"能识别"变成"看起来真的稳"。希望这套源码的拆解和排错经验能帮到你,尤其是准备毕设的同学,跑通之后花点时间做做消融实验,答辩时就有讲不完的细节了。
本文还有配套的精品资源,点击获取