车道线检测-初步研究

初步研究车道线检测方法

0.简介

Implementing lane detection is the first, essential task when building a self-driving car.
—— Sebastian Thrun (godfather of self-driving cars)

在自动驾驶和智能交通系统的发展中,车道线检测作为其中至关重要的一环,扮演着无可替代的角色。车道线不仅是道路交通标记的重要组成部分,更是车辆导航、路径规划和环境感知的基础。因此,准确、稳定的车道线检测系统对于实现安全、高效的智能交通至关重要。

随着深度学习技术的飞速发展,车道线检测技术已经取得了显著的进步。传统的基于图像处理和机器学习的方法逐渐被基于深度学习的端到端模型所取代,使得车道线检测系统在准确性和鲁棒性上都得到了显著提升。

车道线检测方法:传统方法、基于深度学习方法、BEV视角下车道线检测方法、大统一方法

1.传统检测方法

几个主流的传统车道线检测的方法,主要还是借助于opencv和一些算法来实现:

  • 1.颜色阈值:如果只是检测简单的黄色和白色车道线,我们可以用这种方法来实现。把一般常见的RGB通道的图片转化到HSV或者HSL的颜色特征,然后人工设置一个黄色阈值和白色阈值,就能检测出来图像中黄色和白色车道线的位置。

  • 2.边缘检测+霍夫变换:灰度图像–>高斯平滑(把图片变模糊,下一步边缘检测就会滤掉一些不重要的线条)–>边缘检测(检测出图像中边缘的点)–>选择ROI(可以选出路面区域)–>霍夫变换(获得车道线的直线参数)–>投影到原图上。

PS:说说笔者对霍夫变换的理解:将一条直线转换到表征直线的参数空间(可以用斜率m和截距b表征一条直线,那么霍夫空间就是m和b的函数,极坐标表示也类似)。所以经过笛卡尔坐标系下的一个点,可以有无数条直线,即可以有无数种m和b的对应关系,在霍夫空间中,则对应着一条直线。图片上通过边缘检测可以离散得到N个点,而对应的霍夫空间可以表示成N条直线,这N条直线的交点,对应的m和b,即为我们所想要检测的车道线。

    1. 基于拟合的检测: 利用RANSAC等算法拟合车道线。

优点:不需要数据积累
缺点:鲁棒性较差;需要人工手动调参;霍夫变换不能做弯道检测;拟合的方法稳定性较差。

虽然我们要肯定前人的成果,但是不得不说,传统的车道线检测方法需要人工手动地去调整算子和阈值,不仅工作量大且而且鲁棒性也较差,在复杂的环境下,检测结果就不够理想。

2.基于模型检测方法

无需手动设计特征提取规则、泛化能力强、适应性强、准确性高。主流的方法分为三种:基于分割的方案(segmentation-based),基于锚的方法(anchor-based),基于参数的方法(parameter-based)。

2.1 基于分割的方法(segmentation-based)

2.1.1 LaneNet

2.1.1.1 LaneNet简介

顾名思义,把车道线检测的任务当成分割任务来做,通过模型得到图片中哪些pixels属于车道线,哪些不是,并且知道哪些pixels是属于同一条车道线,所以,这其实是一个实例分割(instance segmentation)的任务。

提到这类方法,不得不提它的开山之作——LaneNet!
论文地址:https://arxiv.org/pdf/1802.05591.pdf
github地址:https://github.com/MaybeShewill-CV/lanenet-lane-detection
在这里插入图片描述
作者用用共享的Encoder模型,设计了两个Decoder分支:车道线分割分支和车道embedding分支。前者对像素进行二分类,输出哪些pixels是车道线,哪些是背景,使用标准的交叉熵损失函数;后者输出不同的车道实例,每个pixel初始化一个embedding,通过loss的设计,使得属于同一车道的embedding距离尽可能小,属于不同车道的embedding距离尽可能大。
在这里插入图片描述
得到不同车道的embedding之后,可以通过任意聚类算法来完成实例分割,论文中基于mean-shift来实现的。LaneNet的输出是每条车道线的像素集合,但还需要通过回归来得到完整的车道线。传统方法通常将图像投影到鸟瞰图中,然后使用2阶或3阶多项式进行拟合。然而,这种方法存在一个问题,即变换矩阵H只被计算一次,所有图像都使用相同的变换矩阵,这可能导致在地形变化(如山地或丘陵)的情况下产生误差。为了解决这个问题,论文又设计了一个H-Net网络,它能够训练出能够预测变换矩阵H的模型,其实就是6个参数。H-Net的输入是图像数据,输出是变换矩阵H。通过这个设计,模型能够根据不同的场景和视角,灵活地调整变换矩阵H,从而更好地适应不同地形和环境下的车道线检测任务。

2.1.1.2 LaneNet

参考1:无人驾驶环境感知 | 01 车道线检测网络LanNet原理及实现

2.1.1.2.1 资源汇总:

论文下载地址:https://arxiv.org/abs/1802.05591
github项目地址:https://github.com/MaybeShewill-CV/lanenet-lane-detection
LanNet资料合集:https://ai-wx.blog.csdn.net/article/details/108575549
LanNet论文翻译:车道线检测网络之LaneNet

2.1.1.2.2 LaneNet 简介

传统的车道线检测方法依赖于手工提取的特征来识别,如颜色的特征、结构张量、轮廓等,这些特征还可能与霍夫变换、各种算子或卡尔曼滤波器相结合。在识别车道线之后,采用后处理技术来过滤错误检测并将其分组在一起以形成最终车道。然而,由于道路场景的变化,这些传统的方法容易出现鲁棒性问题!
更新的方法利用深度学习模型,这些模型被训练用于像素级车道分割。但这些方法仅限于检测预定义的固定数量的车道,例如当前车道,并且不能应对车道改变。
基于此,2018年Davy Neven等人提出一种新的车道线检测网络LaneNet,LaneNet主要做出了如下两个贡献:
1.将车道检测问题归结为一个实例分割问题,其中每条车道都形成了自己的实例,可以端到端地进行训练。
2.构建了一个新的网络H-Net,用于学习给定输入图像的透视变换参数,该透视变换能够对坡度道路上的车道线进行良好地拟合,克服了鲁棒性不好的问题。

2.1.1.2.3 整体结构分析

作者提出了一个多分支的网络结构,包含一个二值化分割网络(lane segmentation)和一个实例分割网络(lane embedding),从而实现端到端、任意数量的车道线检测。具体来说,二值分割网络输出所有的车道线像素,而实例分割网络将输出的车道线像素分配到不同的车道线实例中。整体的网络结构图如下:
在这里插入图片描述
另一方面,数据集输入到H-Net网络中,学习到透视变换参数H矩阵。用于不同车道线实例的像素,进行车道线拟合,从而得到上图所示连续点状的车道线。

LaneNet的整体网络结构如下:
在这里插入图片描述
二值化分割网络
Lanenet的一个分支为二值化分割网络,该网络将车道线像素与背景区分开。由于目标类别是2类(车道/背景),并且高度不平衡,因此参考了ENet,损失函数使用的是标准的交叉熵损失函数。
实例分割网络
该分支网络参考了《Semantic Instance Segmentation with a Discriminative Loss Function》,使用基于one-shot的方法做距离度量学习,将该方法集成在标准的前馈神经网络中,可用于实时处理。该分支网络训练后输出一个车道线像素点距离,基于归属同一车道的像素点距离近,不同车道线像素点距离远的基本思想,利用聚类损失函数聚类得到各条车道线。
聚类损失函数
在这里插入图片描述
网络结构图
LaneNet的架构基于编码器-解码器网络ENet,该网络是由5个阶段组成。前3个阶段是编码器网络,进行了两次下采样;后两个阶段是解码器网络,进行了两次上采样。
在这里插入图片描述
LaneNet在该网络的基础上修改成了双分支网络。由于ENet的编码器比解码器包含更多的参数,完全在两个任务之间共享完整的编码器将导致不令人满意的结果。因此,LaneNet只在两个分支之间共享前两个阶段(1和2),留下ENet编码器的阶段3和完整的ENet解码器作为每个单独分支的主干。分割分支的最后一层输出单通道图像,用于二值化分割;而实例分割分支的最后一层输出N通道图像,其中N是实例维度。每个分支的损失项都是相等加权的,并通过网络反向传播。

H-Net 网络结构
LaneNet网络输出的是每条车道线的像素集合,常规的处理是将图像转为鸟瞰图,然后用二次或三次多项式拟合出弯曲的车道线。然而,目前所使用的透视变换矩阵的参数通常是预先设定、不会改变的,在面对水平线波动的影响(如上下坡)等情况下的车道线拟合并不准确,鲁棒性不强。因此,作者提出了H-net模型,用来学习透视变换矩阵的参数H。
在这里插入图片描述
H有6个自由度,放置零是为了强制约束,即在变换下水平线保持水平。
H-NET的网络体系结构较小,由3x3卷积、BN层 和 Relu 的连续块构成。使用最大池化层来降低维度,并在最后添加2个全连接层。完整的网络结构如下图所示:
在这里插入图片描述
最后一个全连接层的结点数是6,对应的就是H矩阵中的6个参数。

LaneNet 性能优点
检测速度。在英伟达1080Ti显卡上进行测试,检测一帧大小为512x512的彩色图片,耗时19ms,因此每秒可处理50帧左右。
在这里插入图片描述
检测精度。 通过使用LaneNet结合三阶多项式拟合和H-Net的变换矩阵,在tuSimple挑战中检测精度达到96.4%,获得了第四名,与第一名相比只有0.5%的差异。结果可以在下表中看到。

在这里插入图片描述

2.1.1.2 手把手带你实现 LaneNet

该项目在github上已经开源,获得了1.3k的星标,想试试的同学可克隆下来:https://github.com/MaybeShewill-CV/lanenet-lane-detection,如果打不开,也可以从我的百度云网盘下载:LaneNet资料合集 ,提取码:1024

代码结构和各部分功能如下:

lanenet-lane-detection
├── config //配置文件
├── data //一些样例图片和曲线拟合参数文件
├── data_provider // 用于加载数据以及制作 tfrecords
├── lanenet_model 
│   ├── lanenet.py //网络布局 inference/compute_loss/compute_acc
│   ├── lanenet_front_end.py // backbone 布局
│   ├── lanenet_back_end.py // 网络任务和Loss计算 inference/compute_loss
│   ├── lanenet_discriminative_loss.py //discriminative_loss实现
│   ├── lanenet_postprocess.py // 后处理操作,包括聚类和曲线拟合
├── model //保存模型的目录semantic_segmentation_zoo
├── semantic_segmentation_zoo // backbone 网络定义
│   ├── __init__.py
│   ├── vgg16_based_fcn.py //VGG backbone
│   └─+ mobilenet_v2_based_fcn.py //mobilenet_v2 backbone
│   └── cnn_basenet.py // 基础 block
├── tools //训练、测试主函数
│   ├── train_lanenet.py //训练
│   ├── test_lanenet.py //测试
│   └──+ evaluate_dataset.py // 数据集评测 accuracy
│   └── evaluate_lanenet_on_tusimple.py // 数据集检测结果保存
│   └── evaluate_model_utils.py // 评测相关函数 calculate_model_precision/calculate_model_fp/calculate_model_fn
│   └── generate_tusimple_dataset.py // 原始数据转换格式
├─+ showname.py //模型变量名查看
├─+ change_name.py //模型变量名修改
├─+ freeze_graph.py//生成pb文件
├─+ convert_weights.py//对权重进行转换,为了模型的预训练
└─+ convert_pb.py //生成pb文

如果想要自行训练的同学,可以下载TuSimple数据集,进行训练。同样,我们也可以直接使用官方训练好的模型,来输入图片,看看测试效果。为了方便,下面我们直接加载已经训练好的模型,进行本地测试。
(1) 下载TuSimple数据集,如果不训练可以跳过这一步。
(2) 下载训练好的模型,下载链接:LaneNet资料合集 ,提取码:1024

代码讲解参考连接:
无人驾驶环境感知 | 01 车道线检测网络LanNet原理及实现
无人驾驶环境感知 | 01 车道线检测网络LanNet原理及实现

测试分析
从图中可以看出,对自己的图片进行检测时,最终的检测结果虽然能够完美地与实际车道线重合,但是延伸至了空中。
产生这种情况最主要的原因是:没有自己制作数据集进行训练,从而得到更有针对性的模型造成的。由于这里我使用的测试模型是在TuSimple数据集下训练得到的,所以我们对TuSimple中的图片测试效果会很好,比如前面的1.jpg。
如果我们想要对自己的图片进行测试,得到更好的效果,那么就需要自己的数据集。比较好的办法是:
1.首先在TuSimple数据集下进行训练,得到的训练模型作为预训练模型,这一部分工作其实已经做好了,大家直接下载预训练模型即可
2.然后,在预训练模型的基础上,加载自己制作的数据集,再进行训练,直到达到预期的效果。
采用这种迁移学习的思想,往往能够事半功倍!

2.1.2 SCNN

LaneNet算是基于分割的车道线方法的先驱之作,但是除此之外,仍然有效果更棒的文章,比如:

SCNN(论文地址:https://arxiv.org/pdf/1712.06080.pdf)的作者就发现,只用简单的CNN来检测车道线,无法提取pixels之间的空间关系,所以会出现当车道线被遮挡时无法被连续识别的问题。于是提出使用空间CNN(spatial CNN)的方法,来增强pixels之间横纵方向的信息传递,从而提高车道线检测的连续性。

2.1.3 RESA

RESA(论文地址: https://arxiv.org/pdf/2008.13719.pdf)的作者为了提取车道线丰富的空间特征,设计了一个REcurrent Feature-Shift Aggregator模块,利用切片特征图的垂直和水平移动来直接信息聚合。

2.2 基于锚的方法 (anchor-based)

Anchor这个词翻译成中文叫“锚”,一直让很多CV新人很不解,其实简单的理解就是“预设的参照”。目标检测任务中,模型对“在哪里有什么目标”不太清楚,所以我们会在图像上预先设计好不同大小,不同长宽比的锚框(anchor boxes),任务即变成“在这个锚框中有没有目标,离得有多远”。那么在车道“线”检测的任务中,预设“框”似乎有点不太合适了,而是要设计“锚线”(anchor lines)。

2.2.1 LineCNN

LineCNN的作者就是受到Faster-RCNN的启发,提出了line proposal (LP),其实就是 anchor lines。因为车道线起始点一般都是图像的左、下、右边向外延伸。所以作者的LP都是从特征图上的这三边上的每个点,沿不同的角度,来生成的。每个LP用一个长度为77的向量表示,[负样本的概率, 正样本的概率, 起始坐标y, 起始坐标x, 车道线长度, 72个偏移量]。作者也设计了一款距离,用来计算车道线和LP之间的距离。

2.2.2 LaneATT

论文地址:https://arxiv.org/pdf/2010.12035.pdf
代码链接:https://github.com/lucastabelini/LaneATT
在这里插入图片描述
先用CNN提取特征,然后根据 anchor lines的x和y的坐标,在特征图上挑出固定长度的特征,得到 ailoc,但是这个特征也只是局部特征,如果遇到车道线被遮挡的情况,还需要融合全局的特征来进行预测。所以,作者提出了一个注意力机制,用来获取全局特征aiglob。融合后的特征,用于两个预测分支:分类分支用来预测类别(k个类别车道线和1个背景类别);回归分支基于anchor的起始点s,预测出线的长度L, 以及N个点的坐标与anchor的偏移。

上述两个方法都是从图像的左下右三个边为起始点,去预设anchor lines,但是有的数据,可能因为车前盖的影响,车道线并非从这三边出发。基于此,ADNet(https://arxiv.org/pdf/2308.10481.pdf)的作者就将anchor分解为学习起点和相关方向的heatmap,消除了预设的anchor lines起始点的限制。并提出来大核注意力模块LKA,目的是提高生成anchor的质量,增加感受野。

当然,基于anchor的车道线检测方案,不仅仅局限于anchor lines, 还有一些其它的思路,比如:CondLaneNet (https://arxiv.org/pdf/2105.05003.pdf)就是利用一个 proposal head(作用有点类似Faster-RCNN中的Region Proposal Network),预测车道线起始点的heatmap,然后会对于特征图中的每一行,车道线的点在每一行的位置,和在纵向的pixel会被预测出来,再通过预测一个offset,来得到车道线的点。

2.2.3 基于参数的方法(parameter-based)

车道线说白了,就是线;是线,就可以用多项式拟合!一次多项式、二次多项式、三次多项式……来拟合曲线,都是有一些参数的。常用的三次多项式拟合曲线 就需要4个参数。该类方法的思路就是直接预测这些参数。经典的论文就是——PolyLaneNet!
PolyLaneNet!
论文地址:https://arxiv.org/pdf/2004.10924.pdf
github地址:https://github.com/lucastabelini/PolyLaneNet
在这里插入图片描述
作者设计了多项式曲线回归(deep polynomial regression),输出表示图像中每个车道线的多项式。好处是可以学习整体车道表示,推理速度较快,但实际上这种方法在准确度上并不高。

2.2.3.1 PolyLaneNet:开源,多项式回归实时高效

参考1:PolyLaneNet:最新车道线检测开源算法,多项式回归实时高效
论文地址:https://arxiv.org/pdf/2004.10924.pdf
代码地址:https://github.com/lucastabelini/PolyLaneNet
由于自动驾驶场景的特殊性,完成此任务的方法必须做到实时(+30 FPS),因此车道线检测算法不仅需要有效(即具有较高的准确性),而且还需要高效(即快速)。在这项工作中,提出了一种用于车道线检测的新方法,该方法将来自安装在车辆中的前视摄像头的图像用作输入,并通过深度多项式回归输出代表图像中每个车道标记的多项式。在TuSimple数据集上本文的方法与现有的最新方法相比具有一定的竞争力,同时保持了效率(115 FPS)。此外,本文还介绍了另外两个公共数据集上的大量定性结果,以及最近的车道线检测工作所使用的评估指标的局限性。

自动驾驶汽车应该能够估计行车道,因为除了作为空间限制之外,每个车道还提供了特定的视觉提示来决定行进路线。此外,检测相邻的车道可能会很有用,这样系统的决策可能基于对交通场景的更好理解通道估计(或检测),乍看之下似乎微不足道,但可能非常具有挑战性。尽管车道标记相当标准化,但其形状和颜色却有所不同。当出现虚线或部分遮挡的车道标记时,估计车道需要对场景进行语义理解。此外,环境本身具有多种多样的特征:可能有很多交通,人流过路,或者可能只是一条免费的高速公路。此外,这些环境还受多种天气(例如,雨,雪,晴天等)和照明(例如白天,黑夜,黎明,隧道等)的条件的影响。

车道线估计(或检测)任务的传统方法包括提取手工特征然后进行曲线拟合。尽管这种方法在正常和有限的情况下往往会很好地起作用,但在不利条件下(如上述情况)通常不如所需的那样鲁棒。因此,随着许多计算机视觉问题的发展,最近开始使用深度学习来学习强大的功能并改善车道线标记估计过程。尽管如此,仍有一些限制需要解决。首先,许多基于深度学习的模型将车道标记估计分为两个步骤:特征提取和曲线拟合。大多数工作都是通过基于分割的模型来提取特征的,这些模型通常效率低下,并且难以自动驾驶所需的实时运行。另外,分割步骤不足以提供车道标记估计,因为必须对分割图进行后处理才能输出交通线。此外,这两个步骤的过程可能会忽略全局信息,当缺少视觉提示时(例如在强烈的阴影和遮挡中),这尤其重要。其次,其中一些工作是由私人公司执行的,这些公司通常不提供复制其结果的手段,并且在私人数据集上开发其方法,这阻碍了研究的进展。最后,评估标准还有改进的余地。这些方法通常仅在美国的数据集上进行测试(通常对发展中国家的道路维护得不太好),并且评估指标过于宽松(它们允许出现错误,从而妨碍了适当的比较)。在这种情况下,专注于消除两步过程的方法可进一步降低处理成本,这将有利于通常依赖于低能耗和嵌入式硬件的高级驾驶员辅助系统(ADAS)。

本文工作提出了PolyLaneNet,一种用于端到端车道线检测估计的卷积神经网络。PolyLaneNet从安装在车辆中的前视摄像头获取输入图像,并输出代表图像中每个车道标记的多项式,以及域车道多项式和每个车道的置信度得分。该方法与现有的最新方法相比具有竞争优势,同时速度更快,不需要后处理即可获得车道估算值。并公开发布了源代码(用于训练和推理)和经过训练的模型,从而可以复制本文中介绍的所有结果。
在这里插入图片描述
PolyLaneNet期望从前视车辆摄像头中获取输入图像,并为每个图像输出Mmax车道线候选标记(表示为多项式)以及水平线的垂直位置,这有助于定义车道线标记的上限。PolyLaneNet的体系结构包括一个主干网络(用于特征提取),该主干网络附加有一个全连接层,具有Mmax + 1个输出。PolyLaneNet采用多项式表示法而不是一组标记点。
在这里插入图片描述
其中,K是定义多项式阶数的参数。如图1所示,多项式具有受限域:图像的高度。除系数外,模型还针对每个车道标记j估计垂直偏移量j和预测置信度得分cj∈[0,1]。总之,PolyLaneNet模型可以表示为

在这里插入图片描述
其中,I为输入图像,θ为模型参数。在运行中的系统中,如图1所示,只有置信度得分大于或等于阈值的候选车道线才被视为检测到。

class OutputLayer(nn.Module):
   def __init__(self, fc, num_extra):
       super(OutputLayer, self).__init__()
       self.regular_outputs_layer = fc
       self.num_extra = num_extra
       if num_extra > 0:
           self.extra_outputs_layer = nn.Linear(fc.in_features, num_extra)

   def forward(self, x):
       regular_outputs = self.regular_outputs_layer(x)
       if self.num_extra > 0:
           extra_outputs = self.extra_outputs_layer(x)
       else:
           extra_outputs = None

       return regular_outputs, extra_outputs


class PolyRegression(nn.Module):
   def __init__(self,
                num_outputs,
                backbone,
                pretrained,
                curriculum_steps=None,
                extra_outputs=0,
                share_top_y=True,
                pred_category=False):
       super(PolyRegression, self).__init__()
       if 'efficientnet' in backbone:
           if pretrained:
               self.model = EfficientNet.from_pretrained(backbone, num_classes=num_outputs)
           else:
               self.model = EfficientNet.from_name(backbone, override_params={'num_classes': num_outputs})
           self.model._fc = OutputLayer(self.model._fc, extra_outputs)
       elif backbone == 'resnet34':
           self.model = resnet34(pretrained=pretrained)
           self.model.fc = nn.Linear(self.model.fc.in_features, num_outputs)
           self.model.fc = OutputLayer(self.model.fc, extra_outputs)
       elif backbone == 'resnet50':
           self.model = resnet50(pretrained=pretrained)
           self.model.fc = nn.Linear(self.model.fc.in_features, num_outputs)
           self.model.fc = OutputLayer(self.model.fc, extra_outputs)
       elif backbone == 'resnet101':
           self.model = resnet101(pretrained=pretrained)
           self.model.fc = nn.Linear(self.model.fc.in_features, num_outputs)
           self.model.fc = OutputLayer(self.model.fc, extra_outputs)
       else:
           raise NotImplementedError()

       self.curriculum_steps = [0, 0, 0, 0] if curriculum_steps is None else curriculum_steps
       self.share_top_y = share_top_y
       self.extra_outputs = extra_outputs
       self.pred_category = pred_category
       self.sigmoid = nn.Sigmoid()

   def forward(self, x, epoch=None, **kwargs):
       output, extra_outputs = self.model(x, **kwargs)
       for i in range(len(self.curriculum_steps)):
           if epoch is not None and epoch < self.curriculum_steps[i]:
               output[-len(self.curriculum_steps) + i] = 0
       return output, extra_outputs

   def decode(self, all_outputs, labels, conf_threshold=0.5):
       outputs, extra_outputs = all_outputs
       if extra_outputs is not None:
           extra_outputs = extra_outputs.reshape(labels.shape[0], 5, -1)
           extra_outputs = extra_outputs.argmax(dim=2)
       outputs = outputs.reshape(len(outputs), -1, 7)  # score + upper + lower + 4 coeffs = 7
       outputs[:, :, 0] = self.sigmoid(outputs[:, :, 0])
       outputs[outputs[:, :, 0] < conf_threshold] = 0

       if False and self.share_top_y:
           outputs[:, :, 0] = outputs[:, 0, 0].expand(outputs.shape[0], outputs.shape[1])

       return outputs, extra_outputs

模型训练
在这里插入图片描述

def loss(self,
        outputs,
        target,
        conf_weight=1,
        lower_weight=1,
        upper_weight=1,
        cls_weight=1,
        poly_weight=300,
        threshold=15 / 720.):
   pred, extra_outputs = outputs
   bce = nn.BCELoss()
   mse = nn.MSELoss()
   s = nn.Sigmoid()
   threshold = nn.Threshold(threshold**2, 0.)
   pred = pred.reshape(-1, target.shape[1], 1 + 2 + 4)
   target_categories, pred_confs = target[:, :, 0].reshape((-1, 1)), s(pred[:, :, 0]).reshape((-1, 1))
   target_uppers, pred_uppers = target[:, :, 2].reshape((-1, 1)), pred[:, :, 2].reshape((-1, 1))
   target_points, pred_polys = target[:, :, 3:].reshape((-1, target.shape[2] - 3)), pred[:, :, 3:].reshape(-1, 4)
   target_lowers, pred_lowers = target[:, :, 1], pred[:, :, 1]

   if self.share_top_y:
       # inexistent lanes have -1e-5 as lower
       # i'm just setting it to a high value here so that the .min below works fine
       target_lowers[target_lowers < 0] = 1
       target_lowers[...] = target_lowers.min(dim=1, keepdim=True)[0]
       pred_lowers[...] = pred_lowers[:, 0].reshape(-1, 1).expand(pred.shape[0], pred.shape[1])

   target_lowers = target_lowers.reshape((-1, 1))
   pred_lowers = pred_lowers.reshape((-1, 1))

   target_confs = (target_categories > 0).float()
   valid_lanes_idx = target_confs == 1
   valid_lanes_idx_flat = valid_lanes_idx.reshape(-1)
   lower_loss = mse(target_lowers[valid_lanes_idx], pred_lowers[valid_lanes_idx])
   upper_loss = mse(target_uppers[valid_lanes_idx], pred_uppers[valid_lanes_idx])

   # classification loss
   if self.pred_category and self.extra_outputs > 0:
       ce = nn.CrossEntropyLoss()
       pred_categories = extra_outputs.reshape(target.shape[0] * target.shape[1], -1)
       target_categories = target_categories.reshape(pred_categories.shape[:-1]).long()
       pred_categories = pred_categories[target_categories > 0]
       target_categories = target_categories[target_categories > 0]
       cls_loss = ce(pred_categories, target_categories - 1)
   else:
       cls_loss = 0

   # poly loss calc
   target_xs = target_points[valid_lanes_idx_flat, :target_points.shape[1] // 2]
   ys = target_points[valid_lanes_idx_flat, target_points.shape[1] // 2:].t()
   valid_xs = target_xs >= 0
   pred_polys = pred_polys[valid_lanes_idx_flat]
   pred_xs = pred_polys[:, 0] * ys**3 + pred_polys[:, 1] * ys**2 + pred_polys[:, 2] * ys + pred_polys[:, 3]
   pred_xs.t_()
   weights = (torch.sum(valid_xs, dtype=torch.float32) / torch.sum(valid_xs, dim=1, dtype=torch.float32))**0.5
   pred_xs = (pred_xs.t_() *
              weights).t()  # without this, lanes with more points would have more weight on the cost function
   target_xs = (target_xs.t_() * weights).t()
   poly_loss = mse(pred_xs[valid_xs], target_xs[valid_xs]) / valid_lanes_idx.sum()
   poly_loss = threshold(
       (pred_xs[valid_xs] - target_xs[valid_xs])**2).sum() / (valid_lanes_idx.sum() * valid_xs.sum())

   # applying weights to partial losses
   poly_loss = poly_loss * poly_weight
   lower_loss = lower_loss * lower_weight
   upper_loss = upper_loss * upper_weight
   cls_loss = cls_loss * cls_weight
   conf_loss = bce(pred_confs, target_confs) * conf_weight

   loss = conf_loss + lower_loss + upper_loss + poly_loss + cls_loss

   return loss, {
       'conf': conf_loss,
       'lower': lower_loss,
       'upper': upper_loss,
       'poly': poly_loss,
       'cls_loss': cls_loss
   }

实验与结果
数据集:TuSim-ple , LLAMAS ,ELAS
评价指标: frames-per-second(FPS) , MACs
在这里插入图片描述

实验配置:

# Training settings
exps_dir: 'experiments' # Path to the root for the experiments directory (not only the one you will run)
iter_log_interval: 1 # Log training iteration every N iterations
iter_time_window: 100 # Moving average iterations window for the printed loss metric
model_save_interval: 1 # Save model every N epochs
seed: 0 # Seed for randomness
backup: drive:polylanenet-experiments # The experiment directory will be automatically uploaded using rclone after the training ends. Leave empty if you do not want this.
model:
 name: PolyRegression
 parameters:
   num_outputs: 35 # (5 lanes) * (1 conf + 2 (upper & lower) + 4 poly coeffs)
   pretrained: true
   backbone: 'efficientnet-b0'
   pred_category: false
loss_parameters:
 conf_weight: 1
 lower_weight: 1
 upper_weight: 1
 cls_weight: 0
 poly_weight: 300
batch_size: 16
epochs: 2695
optimizer:
 name: Adam
 parameters:
   lr: 3.0e-4
lr_scheduler:
 name: CosineAnnealingLR
 parameters:
   T_max: 385

# Testing settings
test_parameters:
 conf_threshold: 0.5 # Set predictions with confidence lower than this to 0 (i.e., set as invalid for the metrics)

# Dataset settings
datasets:
 train:
   type: PointsDataset
   parameters:
     dataset: tusimple
     split: train
     img_size: [360, 640]
     normalize: true
     aug_chance: 0.9090909090909091 # 10/11
     augmentations: # ImgAug augmentations
      - name: Affine
        parameters:
          rotate: !!python/tuple [-10, 10]
      - name: HorizontalFlip
        parameters:
          p: 0.5
      - name: CropToFixedSize
        parameters:
          width: 1152
          height: 648
     root: "datasets/tusimple" # Dataset root

 test: &test
   type: PointsDataset
   parameters:
     dataset: tusimple
     split: val
     img_size: [360, 640]
     root: "datasets/tusimple"
     normalize: true
     augmentations: []

 # val = test
 val:
   <<: *test

对比实验
在这里插入图片描述
消融实验
在这里插入图片描述
可视化实验
在这里插入图片描述

2.2.4 BEV视角下车道线检测

近几年,BEV视角下的视角感知,一直发展迅速,各家公司都有自己的BEV方案。传统的方案,就是通过多视角的相机参数标定,得到相机平面与地面的单应性矩阵,利用逆透视变换(IPM),实现从相机平面到大地平面的转换,再把多视角的图片拼接。但是这样的方法最大的问题就是需要假设地面是平坦的,这在泊车场景下,应用的比较多,但是在开放路段,对于路面不平或者稍微远距离一些的检测任务中,就有些吃力了。所以大部分的方案,都还是基于深度学习的方法来做的。笔者没有把这部分方法归到上面的类别中,主要是因为这部分近几年比较火热,思路也与之不同。

目前比较主流的方法大体可以分为以下两种:
1.显式估计图像的深度信息,完成BEV视角的构建,e.g., LSS;
2.与transformer结合,利用BEV Query查询构建BEV特征, e.g., BEVformer;
3.作为 HD map构建的一个子任务,e.g., MapTR。

先来讲讲BEV的开山之作——Lift,Splat,Shoot(LSS)。
论文链接:https://arxiv.org/pdf/2008.05711.pdf
github链接:https://github.com/nv-tlabs/lift-splat-shoot
在这里插入图片描述
主要分为以下几步:
第一步:生成视锥,得到的是从特征图上的点,与原图上的点的映射,并根据相机内外参将视锥中的点投影到车身坐标系中;
第二步:提取图像特征,利用深度概率密度和语义信息构建图像特征点云;
第三步:将第一步得到的车身坐标系下的点与图像特征点云利用Voxel Pooling,压平构建BEV特征;
第四步:对生成的BEV features利用ResNet-18进行多尺度特征提取,再进一步的特征融合;
第五步:利用特征融合后的BEV特征完成车道线语义分割任务,做交叉熵损失;

当然,基于LSS的很多变式都有不错的效果,但是这种范式对深度的分布非常敏感,于是另一种思路诞生了,让模型自己学习如何将图像的特征转化到BEV空间,来实现车道线检测和目标检测等任务。BEVFormer,即是如此。
论文链接:https://arxiv.org/pdf/2203.17270.pdf

在这里插入图片描述

BEVFormer最吸引人的地方就是Encoder layers的设计,包含:BEV Queries, Spatial cross-attention 和Temporal self-attention。BEV Queries可以理解为可学习参数,通过attention机制在多视角图像中查询特征。Spatial cross-attention以BEV Queries作为输入的注意力层,负责获取来自多视角的特征;Temporal self-attention则是负责聚合时间维度上特征,指来自上一帧的BEV特征。

第三个部分聊一聊 HD map。传统的SLAM离线建图,成本比较昂贵,流程也比较复杂,行业里大家都在做无图NOA,但是map的信息对自动驾驶的规划及其重要的,所以,在线构建地图信息,也得到越来越多的关注,车道线、斑马线、道路路沿等等。而车道线检测可以认为是map构建的一个子任务,这也是笔者想在这里提一下的原因。MapTR 就是一种高效在线矢量化地图构建的方法。
论文链接:https://arxiv.org/pdf/2208.14437.pdf
github链接:https://github.com/hustvl/MapTR
在这里插入图片描述
地图元素有开放(e.g., 路沿)和闭合(e.g., 斑马线)两种形状,在几何上,可以表示为多边形和折线。而这两种形状,都可以被表示为一些有序的点集。但问题是点集的排列并不是唯一的,比如以矢量相连一些点,构成封闭多边形,也有顺时针和逆时针两种方式。既然如此,把点的排列顺序也作为表征地图元素的一部分就好了,于是 MapTR 的作者用 来表示一个地图元素。V = 表示点集,表示点的个数; 表示点集V的所有排列(见上图)。

这样,真值可以表示成 N 个地图元素的集合,每个元素 , 表示该元素的类别。预测的元素 , 其中 和 分别是预测的分类置信度和预测点集。为了实现结构化的地图元素建模和学习,MapTR 引入了层次双分图匹配(hierarchical bipartite matching),即按顺序进行实例级别的匹配和点级别的匹配。

实例级别的匹配会考虑到 元素的类别的 和 点的位置的两个方面,会通过匈牙利算法最优的实例级别的分配 :
在这里插入图片描述
在这里插入图片描述
在这之后,每个预测的地图元素 都会被分配给一个真值 ,对于每个被分配正标签(类别标签不为空)的预测元素,开始进行点级别匹配,以找到预测点集 和真值点集 之间的最优点对点分配 。

在这里插入图片描述
训练时的损失函数也是基于这两种分配,设计出了三个部分:classfication loss(监督地图元素类别)、point2point loss(监督每一个预测点的位置)、edge direction loss(监督相邻两点连线的方向)。

在这里插入图片描述

2.3 大统一方法

笔者前几天闲逛看到了一篇很有趣的论文(CVPR2024):Lane2Seq
论文链接:https://arxiv.org/pdf/2402.17172.pdf
在这里插入图片描述
上面提到的多个车道线检测的方法类别:segmentation-based, anchor-based, parameter-based, 但是Lane2Seq的作者,觉得这太麻烦了,要精心设计的特定于任务的头部网络和相应的损失函数。基于此,他把通过将车道检测作为序列生成任务来统一各种车道检测格式 Lane2Seq仅采用简单的基于Encoder-Decoder的Transformer架构,具有简单的交叉熵损失。

对于Segmentation序列,并不是作pixel-wise的学习,而是作为多边形(polygon)来学习,一个polygon的序列可以被表示成 [x1, y1, x2, y2, …, x28, y28,], 这里的 是一个类别的token;对于Anchor序列,会作为关键点(keypoint)的学习,一个keypoint序列可以表达成[x1, y1, x2, y2, …, x14, y14,];而对于Paramter序列,比如一个parameter序列可以被表示成 [a1, a2, a3, a4, a5, s, ], 这里的s是一个纵向的offset。

虽然说, Lane2Seq 不包含特定于任务的组件(指的是segmentation, anchor, parameter),但这些组件中包含的特定于任务的知识可以帮助模型更好地学习车道的特征。所以,作者提出了一种基于强化学习 (MFRL) 的多格式模型调整方法,将特定于任务的知识融入模型中,而无需改变模型的架构。受到 Task-Reward 的启发,MFRL 将整合特定于任务的知识的评估指标作为奖励,并使用 REINFORCE 算法来调整 Lane2Seq。作者也根据任务特定知识,为分割、锚点和参数格式提出了三种新的基于评估指标的奖励。

3. 其他方法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

周陽讀書

周陽也想繼往聖之絕學呀~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值