Ultra-Fast-Lane-Detection复现解析:分类代替分割的车道线检测实战
发布时间:2026/9/15 19:16:15 作者:尧图编辑部 阅读量:1,286

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 torch1.10.1cu113 torchvision0.11.1cu113 -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 结构。模型大致分成三部分backbone输出特征图分辨率是输入图像的 1/8 或 1/16。分类头把特征图池化到固定的 rows 个水平条带对每个条带做列方向的分类预测车道线落在哪个 anchor 位置。分割辅助头只在训练时使用输出低分辨率的语义分割图帮助模型更好地收敛。这里的核心代码在model/model.py里的UFLDLaneNet类中。最关键的一个参数是num_row和num_col分别对应 rows 分类数和 cols 分类数。CULane 配置里一般设置num_row72、num_col81这个值决定了模型的输出粒度和计算量。如果你跑的是自定义数据集这两个参数要重新设置否则模型输出的点数就跟你的标签对不上。分类头不直接用全局池化而是把 feature map 按高度方向切分成多个横向条带每个条带内部做一次全局平均池化再输入到一个小的全连接分类器。这样做的好处是保留了垂直方向的位置信息模型可以区分“车道线在图像上半部分还是下半部分”。3.3 损失函数分类损失加辅助分割损失损失函数这块官方实现是分类损失和分割损失的加权和。分类损失用的是 CrossEntropyLoss针对每个 row 上每个 lane 的列位置做分类分割分支用的是带 OHEM在线难例挖掘的交叉熵损失只对难样本计算梯度。具体做的时候Train 模式下的总 loss 是这样算的loss ce_loss seg_loss_weight * seg_lossseg_loss_weight在配置文件中通常设为 1.0。但我在实测中发现如果数据集中车道线特别细、分割标签比较稀疏这个权重可以适当调低到 0.5能让主干分类结果更稳定一些。这个根据实际任务调整就行不是死的。另外留意一下官方代码里在计算分类 loss 时会忽略ignore_index对应的位置。这个 index 在配置里通常是 -2对应的是标注中缺失的车道线点。所以数据预处理时千万不要把缺失点随便填成 0否则模型会把“无车道线”也当作一个类别来学。3.4 数据加载与标签生成数据加载的逻辑在dataset.py里核心工作是把车道线的坐标点转换成分类标签。流程大概是读取图像和标注点。把图像缩放到模型输入尺寸比如 800x320 或者 512x256。对每条车道线根据配置的num_row数量将图像在高度方向均匀分成若干行找到每个行对应的 x 坐标。把 x 坐标映射到0 ~ num_col-1的整数标签。这个标签映射过程是整个预处理里最容易出错的环节。源码里有一个generate_row_col_labels函数只要你传入的标注格式正确它就能自动生成标签。需要注意如果一条车道线在某一行没有对应的 x 坐标比如超出了图像边界标签就填ignore_index训练时会被忽略。4. 完整复现实操从训练到测试的一线记录4.1 准备工作清单开始之前确保以下东西都备齐了单张 NVIDIA 显卡显存至少 8GB推荐 11GB 以上我用的是 1080Ti 11GB 跑的 CULane。下载好的数据集TuSimple 大约 4GBCULane 大约 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参数有两个可选值culane和tusimple。--config指定对应的配置文件。启动后代码会先打印配置信息然后是模型参数量统计接着进入训练循环。core 几个关键参数我列个表说一下参数默认值说明epochs50训练轮数CULane 上 50 轮差不多能收敛到论文水平batch_size32显存不够就降到 16 或 8lr0.01初始学习率如果数据集很小建议降到 0.001optimizerSGD官方用的 SGD momentum 0.9scheduler多步衰减一般在 30、45 轮时衰减 0.1input_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.pthtest.py会把预测结果保存成 JSON 或者 png 文件然后交给评估脚本计算指标。注意官方评估脚本和测试脚本是分开的test.py只负责生成结果真正算 F1、AP、Accuracy 这些指标的是evaluate/culane.py或者evaluate/tusimple_evaluate.py。如果你在测试阶段报错说找不到预测结果文件多半是test.py内部保存路径跟评估脚本没对上。建议先跑通单张图片的可视化确保模型输出正常再跑全量测试。4.4 可视化输出与结果解读官方没有直接提供可视化脚本但我自己写了一个简单的逻辑把模型输出的 row-col 标签还原成坐标点然后画到原图上。大概思路是把每个 row 上预测的 anchor 索引转换成 x 坐标。用配置里的row_anchor数组还原 y 坐标。在图上用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_row和num_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 自定义数据集上的迁移迁移到自定义数据集的流程其实不复杂核心就是把标注转成数据集格式CULane 的 txt 格式相对简单。修改配置文件中的dataset路径、input_width、input_height、num_row、num_col。修改data/dataset.py中的类别数通常从 4 类车道线变成你自己的车道线类别数。加载官方预训练权重时分类头部分会维度不匹配需要手动把最后一层重新初始化。我在实际项目里遇到过的情况是摄像头安装位置和 CULane 数据集差异很大导致直接迁移效果很差。后来我把标注重新做了一遍并且根据相机的俯视角度重新计算了 row_anchor模型才真正可用。这一步不要偷懒标准视角下的模型不能直接套到俯视视角上。6.3 推理加速与部署思路跑通之后如果你想往工程化方向走有几个简单但有效的发力点计算图优化训练时用的辅助分割 branch 在推理时完全不需要只要导出模型时不包含就行。半精度推理用 PyTorch 的half()或者 ONNX Runtime 开启 FP16在支持 Tensor Core 的显卡上能获得将近 2 倍加速。TensorRT 部署把 ONNX 模型转成 TensorRT engine配合端侧 GPU 可以达到几十毫秒以内推理一帧。整个转换流程不复杂但要特别注意 UFLD 模型里的gather和argmax操作在 TensorRT 中的兼容性我遇到过一次算子不支持的情况最后是通过把 argmax 换成topk解决的。去掉后处理模型输出的 row 位置点天生就是排序后的不需要像分割那种找连通域的操作直接连接起来画线就完事这给部署省了很大功夫。6.4 我踩过的几个坑提前帮你避开复盘整个复现过程最让我头疼的不是模型结构而是一些看起来很低级的细节。第一个是 CULane 数据集的软链接问题官方代码默认从data/CULane读数据但我的数据盘挂在别处用软链接之后评估脚本里又有一个路径拼接写死了相对路径折腾了半天。第二个是num_col调小到 50 以下时车道线细的弯道会明显“断线”因为列方向的量化太粗了这个坑让我一度以为是模型收敛出了问题。第三个是训练和评估时用的图像尺寸必须完全一致哪怕差一个像素行分类的标签都会错位精度直接掉到个位数。如果你也准备复现这个项目我的建议是先用 TuSimple 数据集跑通全流程因为这个数据集小标注格式简单肉眼验证效果很直观。等整条链路跑顺了再换到 CULane 上做大规模训练。千万不要一上来就啃 CULane 的 60GB 数据和 C 评估工具容易把自己劝退。最后再分享一个实用小技巧训练完模型之后别急着只看 F1 指标找几张有代表性的图比如弯道、遮挡、雨天单独可视化一下。很多时候指标差不多但实际视觉效果差异很大尤其是车道线边缘的连续性和弯道处的贴合度这些才是落地时真正影响驾驶体验的细节。这个项目给了我一个很大的启发很多看似只能靠分割解决的问题换一个建模思路可能效率和精度都能兼顾。