Ultra-Fast-Lane-Detection复现解析:分类代替分割的车道线检测实战
2026/9/15 19:16:10 网站建设 项目流程

Ultra-Fast-Lane-Detection 这个项目,我第一次看到是在一个自动驾驶讨论群里,当时有人贴了一张它在 CULane 上的精度榜单截图,说“没用分割,纯分类,跑得飞快”。我当时第一反应是不太信,车道线检测这种任务不做分割怎么做?后来把论文和代码翻了一遍,才明白它把车道线检测重新定义成了“按行分类”的问题,整个思路非常取巧,而且效果确实能打。复现这个项目的过程,我踩了不少坑,也把代码里一些关键细节捋清楚了,这篇就专门聊聊怎么从零把它跑起来,以及跑通之后怎么根据自己的场景做调整。

先说这个项目适合谁看。如果你在做自动驾驶感知、ADAS 相关课程设计、毕业设计,或者单纯想研究一个轻量级视觉模型怎么在嵌入式设备上实时推理,那 Ultra-Fast-Lane-Detection 是一个非常好的样本。它结构简单、依赖少、训练和推理速度都很快,而且官方公开了 CULane 和 TuSimple 两个经典数据集的完整训练配置,复现门槛比大多数论文代码低得多。即使你不是专门做车道线的,把它当成一个“分类代替分割”的典型案例来学习,也很有价值。

1. 整体设计思路拆解:为什么车道线检测能做成分类

1.1 核心思路:把车道线检测变成按行选位置

传统车道线检测的主流做法是语义分割,也就是对图像里的每一个像素预测它属于哪条车道线。这样做精度确实高,但计算量非常大,因为要处理的是整张图的逐像素输出,尤其在嵌入式平台上,一个 512x288 的分割头就能把算力吃干榨净。

Ultra-Fast-Lane-Detection 换了一个角度:它不去逐像素分类,而是把图像划分成固定的行(row)和列(anchor),然后对每一行预测车道线在该行上落在哪个列位置。换句话说,模型不是在“画”车道线,而是在“猜”车道线会经过哪些位置。这样做的好处很明显——输出维度从 HW 降到了 Crows,计算量直接少了一个数量级。

这个思路我第一次看的时候觉得有点反直觉,但仔细想想其实很合理。车道线本身是连续的细长结构,在每一行上通常只有一个确定的 x 坐标位置,所以“该行第几个格子有车道线”这种离散化建模方式,足够表达车道线的空间分布。而且因为不需要解码到像素级,模型可以用很小的分辨率跑,速度自然快。

1.2 为什么不做分割:计算量、全局信息、困难场景

把车道线检测做成行分类,除了快之外,还顺带解决了一个分割方法容易翻车的问题:感受野。

车道线检测里有一个经典的困难场景,就是车道线被车遮挡、磨损严重或者完全看不见。分割模型只能从局部像素的纹理去猜,容易把遮挡物边缘误判成车道线。但 Ultra-Fast-Lane-Detection 用的是全图特征加 row 方向的全局预测,每一行分类的时候都能看到整行的上下文信息,哪怕车道线那一小段被挡了,模型仍然能根据前后的连续性猜出它大概的位置。

另外,分割模型通常需要后处理把像素连通成线,而这种方法直接输出的是每个 row 上的位置点,天然就是结构化的坐标信息,省去了不少后处理逻辑。论文里还做了一个辅助的 segmentation branch 来帮助训练收敛,但推理的时候不需要它,所以不会增加额外的计算负担。

1.3 适用场景和前置要求

这个模型比较适合的场景是结构化道路上的车道线检测,比如高速公路、城市主干道这种车道线清晰、分布规律的环境。对于越野、无车道线或者严重逆光的情况,效果会打折扣,因为它本质上是靠学习“车道线在图像中的分布规律”来做预测的。

复现这个项目需要的基础知识包括:PyTorch 基本训练流程、卷积神经网络基础、图像数据集的组织方式。如果你本身跑过几个分类或者检测模型,那这套代码对你来说几乎没有难度。如果完全没接触过深度学习,建议先补一下 DataLoader 和训练循环的基础知识,再来复现会更顺畅。

2. 环境准备与依赖安装:版本匹配是第一道坎

2.1 基础环境版本选择

老规矩,先把环境搭好。我用的是这套组合,实测下来比较稳定:

组件版本备注
Python3.83.9 也可以,但别用 3.10+,有些依赖编译会有问题
PyTorch1.10.11.7 到 1.13 应该都行,但别太新,代码里有些 API 写法比较老
CUDA11.3配合 PyTorch 1.10.1,正好匹配
torchvision0.11.1跟 PyTorch 版本严格对应
GCC7.5编译 cupy 相关依赖时用

注意一点,代码仓库里没有提供 environment.yml 这种一键安装文件,需要自己手动装依赖。官方 README 写的很简单,但实际跑起来你会发现有些坑它没提到。

2.2 依赖安装细节

克隆仓库之后,先把 requirements 里的核心依赖装上。官方建议用 pip 直接装,我实际经验是这么装:

pip install torch==1.10.1+cu113 torchvision==0.11.1+cu113 -f https://download.pytorch.org/whl/cu113/torch_stable.html pip install opencv-python pillow numpy tqdm scikit-learn pip install tensorboard # 可选,训练日志可视化

这里有个比较隐蔽的问题:torchvision的版本必须和torch严格对齐,否则后面跑验证代码时可能会在加载预训练权重时报错,原因就是模型结构里的 BN 层参数数量对不上。这个报错特别迷惑人,因为报的是size mismatch,会让人误以为是数据集的问题。

另外,代码里用了tensorboardX或者tensorboard来记录训练曲线,如果你的环境里没有装,训练启动后会报 ImportError。说实话,这个依赖不加也不影响训练,但为了方便观察 loss 变化,建议还是装上。

2.3 数据集准备:CULane 与 TuSimple 的格式差异

Ultra-Fast-Lane-Detection 官方支持两个数据集:CULane 和 TuSimple。两个数据集的标注格式完全不同,如果搞混了,训练出来的模型基本就是废的,而且评估指标也会莫名奇妙变成 0。

先看 TuSimple,它的标注是 JSON 格式,每条数据包含lanes(车道线点的 x 坐标列表)、h_samples(固定的 y 坐标列表)、raw_file(图像路径)。需要注意h_samples从图像顶部往下递增,每个lanes列表长度和h_samples一样,缺的点用 -2 或者 -1 填充。

再看 CULane,它没有统一标注文件,而是每个图像对应一个 .txt 文件,里面每一行代表一条车道线,坐标格式是x1 y1 x2 y2 x3 y3 ...,按单个点的 x 和 y 交替排列。而且 CULane 的 txt 文件里夹在list/train_split/list/test_split/这些目录下,数据加载时会先读这些 list 文件来组织路径。

我在第一次复现时用的 TuSimple,结果训练到一半发现 loss 死活降不下去,查了半天才发现是数据加载时h_samples和实际图像的 y 坐标没对齐。后面换成官方的 CULane 预处理脚本才正常。

3. 代码结构拆解:模型、数据、损失函数三个核心

3.1 仓库目录结构一览

克隆下来的代码虽然不算多,但目录比较乱,我先帮大家梳理一下关键部分:

Ultra-Fast-Lane-Detection/ ├── configs/ # 训练配置文件 │ ├── culane.py │ └── tusimple.py ├── model/ │ ├── model.py # 主模型定义 │ ├── resnet.py # ResNet backbone │ └── seg_model.py # 辅助分割分支 ├── data/ │ ├── dataset.py # 数据加载 │ ├── transform.py # 图像预处理 │ └── mytransforms.py # 自定义变换 ├── train.py # 训练入口 ├── test.py # 测试入口 └── evaluate/ ├── lane.py # 车道线评估 └── culane.py # CULane 评估工具

train.py是训练入口,test.py是测试入口,这两个文件都不大,建议精读。configs目录里的配置文件定义了所有超参数,包括输入图像的尺寸、backbone 类型、训练轮数、学习率策略等。

3.2 模型定义:从 backbone 到分类头

Ultra-Fast-Lane-Detection 的主干网络默认用的是 ResNet34,也支持 ResNet18、ResNet50,甚至可以用更轻量的 MobileNetV3 结构。模型大致分成三部分:

  1. backbone:输出特征图,分辨率是输入图像的 1/8 或 1/16。
  2. 分类头:把特征图池化到固定的 rows 个水平条带,对每个条带做列方向的分类,预测车道线落在哪个 anchor 位置。
  3. 分割辅助头:只在训练时使用,输出低分辨率的语义分割图,帮助模型更好地收敛。

这里的核心代码在model/model.py里的UFLDLaneNet类中。最关键的一个参数是num_rownum_col,分别对应 rows 分类数和 cols 分类数。CULane 配置里一般设置num_row=72num_col=81,这个值决定了模型的输出粒度和计算量。如果你跑的是自定义数据集,这两个参数要重新设置,否则模型输出的点数就跟你的标签对不上。

分类头不直接用全局池化,而是把 feature map 按高度方向切分成多个横向条带,每个条带内部做一次全局平均池化,再输入到一个小的全连接分类器。这样做的好处是保留了垂直方向的位置信息,模型可以区分“车道线在图像上半部分还是下半部分”。

3.3 损失函数:分类损失加辅助分割损失

损失函数这块,官方实现是分类损失和分割损失的加权和。分类损失用的是 CrossEntropyLoss,针对每个 row 上每个 lane 的列位置做分类;分割分支用的是带 OHEM(在线难例挖掘)的交叉熵损失,只对难样本计算梯度。

具体做的时候,Train 模式下的总 loss 是这样算的:

loss = ce_loss + seg_loss_weight * seg_loss

seg_loss_weight在配置文件中通常设为 1.0。但我在实测中发现,如果数据集中车道线特别细、分割标签比较稀疏,这个权重可以适当调低到 0.5,能让主干分类结果更稳定一些。这个根据实际任务调整就行,不是死的。

另外留意一下,官方代码里在计算分类 loss 时,会忽略ignore_index对应的位置。这个 index 在配置里通常是 -2,对应的是标注中缺失的车道线点。所以数据预处理时千万不要把缺失点随便填成 0,否则模型会把“无车道线”也当作一个类别来学。

3.4 数据加载与标签生成

数据加载的逻辑在dataset.py里,核心工作是把车道线的坐标点转换成分类标签。流程大概是:

  1. 读取图像和标注点。
  2. 把图像缩放到模型输入尺寸,比如 800x320 或者 512x256。
  3. 对每条车道线,根据配置的num_row数量,将图像在高度方向均匀分成若干行,找到每个行对应的 x 坐标。
  4. 把 x 坐标映射到0 ~ num_col-1的整数标签。

这个标签映射过程是整个预处理里最容易出错的环节。源码里有一个generate_row_col_labels函数,只要你传入的标注格式正确,它就能自动生成标签。需要注意,如果一条车道线在某一行没有对应的 x 坐标(比如超出了图像边界),标签就填ignore_index,训练时会被忽略。

4. 完整复现实操:从训练到测试的一线记录

4.1 准备工作清单

开始之前,确保以下东西都备齐了:

  • 单张 NVIDIA 显卡,显存至少 8GB,推荐 11GB 以上(我用的是 1080Ti 11GB 跑的 CULane)。
  • 下载好的数据集,TuSimple 大约 4GB,CULane 大约 60GB,注意 CULane 解压后会更大。
  • 在项目根目录新建data文件夹,将数据集软链接进去。
  • 如果需要预训练模型做 fine-tune,可以下载官方的 ResNet34 预训练权重,放到pretrained文件夹里。

这里我要特别强调一个容易栽跟头的点:官方代码里默认从 ImageNet 预训练权重开始训练,但这个权重文件位置是在model/resnet.py里写死的。如果你不提前下载好对应权重并放到指定位置,训练启动时会报RuntimeError: Could not load pretrained model,虽然代码里会提示自动下载,但国内网络环境下很容易卡住。

4.2 训练启动与关键参数解释

训练命令很简单:

python train.py --dataset tusimple --config configs/tusimple.py

--dataset参数有两个可选值:culanetusimple--config指定对应的配置文件。启动后,代码会先打印配置信息,然后是模型参数量统计,接着进入训练循环。

core 几个关键参数我列个表说一下:

参数默认值说明
epochs50训练轮数,CULane 上 50 轮差不多能收敛到论文水平
batch_size32显存不够就降到 16 或 8
lr0.01初始学习率,如果数据集很小建议降到 0.001
optimizerSGD官方用的 SGD + momentum 0.9
scheduler多步衰减一般在 30、45 轮时衰减 0.1
input_width800在配置文件中定义
input_height320在配置文件中定义

训练过程中,最值得关注的 loss 走势是分类 loss。正常情况下前几个 batch 的 loss 会从 4 到 5 快速下降到 2 左右,然后慢慢降到 1 以下。如果用 TuSimple 数据集,50 轮大概要跑 3 到 4 个小时;CULane 数据量大,50 轮在 1080Ti 上大概要 12 个小时以上,做好心理准备。

4.3 验证与测试

训练完成后,用test.py在测试集上看效果:

python test.py --dataset tusimple --config configs/tusimple.py --models_dir checkpoint --model_name model_50.pth

test.py会把预测结果保存成 JSON 或者 png 文件,然后交给评估脚本计算指标。注意,官方评估脚本和测试脚本是分开的,test.py只负责生成结果,真正算 F1、AP、Accuracy 这些指标的是evaluate/culane.py或者evaluate/tusimple_evaluate.py

如果你在测试阶段报错说找不到预测结果文件,多半是test.py内部保存路径跟评估脚本没对上。建议先跑通单张图片的可视化,确保模型输出正常,再跑全量测试。

4.4 可视化输出与结果解读

官方没有直接提供可视化脚本,但我自己写了一个简单的逻辑,把模型输出的 row-col 标签还原成坐标点,然后画到原图上。大概思路是:

  1. 把每个 row 上预测的 anchor 索引转换成 x 坐标。
  2. 用配置里的row_anchor数组还原 y 坐标。
  3. 在图上用cv2.polylines画线。

可视化的时候你会发现,模型输出的点列不一定完全平滑,这是正常的,因为它是逐行分类产生的离散结果。如果线条抖动比较厉害,可以做一个均值滤波或者用三次样条拟合一下,视觉上会好很多。我在实际项目中就加了一个 5 点滑动平均,效果立竿见影。

5. 常见问题与排查技巧实录

场景一:训练一开始就报 Shape mismatch

这个报错九成是预训练权重和模型结构不匹配。常见原因有两个:一是 PyTorch 版本不同导致 BN 层的num_batches_tracked字段对不上,二是配置文件里backbone类型跟权重不对应。解决办法很简单:重新下载对应版本的预训练权重,或者把配置文件里的backbone改成和权重一致的模型。

场景二:loss 为 NaN

出现 NaN 最常见的原因是学习率太大,尤其是 batch_size 比较小的时候。我实测发现,批量大小降到 8 以后,0.01 的学习率很容易起飞,建议按 batch_size 比例把学习率调低:学习率除以 4 或者 8。另外一个原因是数据里的标签越界,比如 x 坐标超出了num_col-1的范围,导致 loss 计算时索引越界,进而出 NaN。

场景三:CULane 评估脚本跑不起来

CULane 的官方评估工具是用 C++ 写的,需要编译。代码仓库虽然带了源码和 Makefile,但路径经常对不上。我的做法是直接用 Python 版的evaluate/lane.py,把预测的 lane 点序列转成 txt 格式,然后调用官方工具的外部接口。这块是整个复现过程中最折腾的,如果不想跟 C++ 编译死磕,可以直接用官方测试脚本生成的 txt 结果,再配合evaluate/culane.py里的 Python 版评估逻辑,跑出来的数据也有参考价值。

4. 显存不足怎么办

如果显存只有 6GB,建议把batch_size降到 8,同时把输入分辨率从 800x320 降到 640x256。注意,修改输入分辨率后,num_rownum_col也要跟着调整,否则标签映射会不匹配。具体关系是:num_row等于你切分的横向条带数,num_col等于横向 anchor 数,论文里推荐的行列数取决于输入尺寸,按比例缩放即可。

常见问题速查表

现象可能原因解决方案
训练 loss 不降数据集格式错误、学习率太大检查标注坐标是否对齐,调低学习率
验证精度为 0评估脚本路径问题确认预测结果和全局配置匹配
图片上有乱线后处理没做平滑增加均值滤波或者样条拟合
测试时显存溢出输入尺寸太大调低 batch_size 或者输入分辨率
多 GPU 报错batch_size 分配不均使用单卡,或者修改DistributedDataParallel配置

6. 调优与二次开发:从复现到落地

6.1 超参数调整方向

如果你跑通了复现,接下来想在自己数据上提精度,我建议优先调整这几个方向:

  • num_row 和 num_col:这两个值控制输出粒度。num_row 越大,纵向坐标越密集,但计算量也线性增长。在弯道多的场景,适当加大 num_row 能明显减少车道线拐弯处的毛刺。
  • backbone 选择:追求速度就换 ResNet18 或者 MobileNetV3-Small,追求精度上 ResNet50。实测 CULane 上 ResNet34 到 ResNet50 的 F1 提升大约 0.5 个点,但推理速度下降了将近一半。
  • 数据增强:官方默认的增强比较基础,只有随机翻转和颜色扰动。在复杂天气场景建议加上随机亮度和对比度调整,对鲁棒性帮助很大。
  • row_anchor 配置:这个参数非常关键,它定义了在图像哪些 y 坐标处做分类预测。默认配置是针对 CULane 图像尺寸设计的,如果换成车载相机画面,一定要重新标定 row_anchor,否则模型输出的纵坐标位置会整体偏移。

6.2 自定义数据集上的迁移

迁移到自定义数据集的流程其实不复杂,核心就是:

  1. 把标注转成数据集格式(CULane 的 txt 格式相对简单)。
  2. 修改配置文件中的dataset路径、input_widthinput_heightnum_rownum_col
  3. 修改data/dataset.py中的类别数(通常从 4 类车道线变成你自己的车道线类别数)。
  4. 加载官方预训练权重时,分类头部分会维度不匹配,需要手动把最后一层重新初始化。

我在实际项目里遇到过的情况是:摄像头安装位置和 CULane 数据集差异很大,导致直接迁移效果很差。后来我把标注重新做了一遍,并且根据相机的俯视角度重新计算了 row_anchor,模型才真正可用。这一步不要偷懒,标准视角下的模型不能直接套到俯视视角上。

6.3 推理加速与部署思路

跑通之后,如果你想往工程化方向走,有几个简单但有效的发力点:

  • 计算图优化:训练时用的辅助分割 branch 在推理时完全不需要,只要导出模型时不包含就行。
  • 半精度推理:用 PyTorch 的half()或者 ONNX Runtime 开启 FP16,在支持 Tensor Core 的显卡上能获得将近 2 倍加速。
  • TensorRT 部署:把 ONNX 模型转成 TensorRT engine,配合端侧 GPU 可以达到几十毫秒以内推理一帧。整个转换流程不复杂,但要特别注意 UFLD 模型里的gatherargmax操作在 TensorRT 中的兼容性,我遇到过一次算子不支持的情况,最后是通过把 argmax 换成topk解决的。
  • 去掉后处理:模型输出的 row 位置点天生就是排序后的,不需要像分割那种找连通域的操作,直接连接起来画线就完事,这给部署省了很大功夫。

6.4 我踩过的几个坑,提前帮你避开

复盘整个复现过程,最让我头疼的不是模型结构,而是一些看起来很低级的细节。第一个是 CULane 数据集的软链接问题,官方代码默认从data/CULane读数据,但我的数据盘挂在别处,用软链接之后,评估脚本里又有一个路径拼接写死了相对路径,折腾了半天。第二个是num_col调小到 50 以下时,车道线细的弯道会明显“断线”,因为列方向的量化太粗了,这个坑让我一度以为是模型收敛出了问题。第三个是训练和评估时用的图像尺寸必须完全一致,哪怕差一个像素,行分类的标签都会错位,精度直接掉到个位数。

如果你也准备复现这个项目,我的建议是:先用 TuSimple 数据集跑通全流程,因为这个数据集小,标注格式简单,肉眼验证效果很直观。等整条链路跑顺了,再换到 CULane 上做大规模训练。千万不要一上来就啃 CULane 的 60GB 数据和 C++ 评估工具,容易把自己劝退。

最后再分享一个实用小技巧:训练完模型之后,别急着只看 F1 指标,找几张有代表性的图(比如弯道、遮挡、雨天)单独可视化一下。很多时候指标差不多,但实际视觉效果差异很大,尤其是车道线边缘的连续性和弯道处的贴合度,这些才是落地时真正影响驾驶体验的细节。这个项目给了我一个很大的启发:很多看似只能靠分割解决的问题,换一个建模思路,可能效率和精度都能兼顾。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询