ECCV2020 | 300+FPS!浙大提出一种超快速车道线检测方法

点击上方“AI算法修炼营”,选择“星标”公众号php

精选做品,第一时间送达git

本文收录于ECCV2020,速度达到300+FPS的车道线检测算法,总体的思路很简单,将车道线检测定义为寻找车道线在图像中某些行的位置的集合,即基于行方向上的位置选择、分类(row-based classification)。还提出了两个新的结构损失函数。总体方法快速高效,推荐学习!github

论文地址:https://arxiv.org/pdf/2004.11757.pdf算法

代码地址:https://github.com/cfzd/Ultra-Fast-Lane-Detection网络

现代方法主要将车道线检测视为按像素分割的问题,但这一般会受到速度慢、每一个像素的感觉野有限等问题。同时在严重遮挡和极端光照条件下对车道线的检测和识别主要须要依靠大量的上下文信息和全局信息来完成。本文提出了一种新颖、简单而有效的方法,将车道检测过程视为使用全局特征的基于行的选择问题(将车道线检测定义为寻找车道线在图像中某些行的位置的集合,即基于行方向上的位置选择、分类(row-based classification)。借助基于行的选择,车道线的公式化能够显着下降计算成本。经过使用具备普遍感觉野的全局特征,还能够应对严重遮挡和极端光照等具备挑战性的场景。此外,在此基础上,本文还提出了结构损失,以对车道线的结构进行建模。在两个车道线检测基准数据集上的大量实验代表,本文的方法能够在速度和准确性方面达到最早进的性能。轻量级版本甚至能够以相同的分辨率每秒运行300+帧,这比之前的最新方法至少快4倍。架构

简介app

车道线检测是一个基本计算机视觉问题,具备普遍的应用(例如,ADAS和自动驾驶)。对于车道线检测有两种主流方法,传统的图像处理方法和基于深度学习的图像分割方法。可是,车道线检测算法目前有两大难点:一、基于图像分割的车道线检测算法因为是逐像素的任务,计算量大,一般不适用于自动驾驶实时场景;二、no-visual-clue,在图1中,对于车道线的定位只有靠周围车流走向这种全局信息才能很好地定位。同时,具备严重遮挡和极端光照条件的挑战场景对应着车道线检测的另外一个关键问题。在这种状况下,车道线检测迫切须要更高层次的车道语义信息。基于深度学习的图像分割方法天然比传统的图像处理方法具备更强的语义表示能力,这也是传统方法没落的缘由。此外,SCNN针对这一问题,提出了相邻像素之间的消息传递机制,显著提升了分割方法的性能,因为像素间密集的通讯,这种消息传递须要更多的计算成本。并且,车道表示为被分割的二进制特征而不是直线或曲线。尽管深度分割方法在车道检测领域占主导地位,但这种表示方式使这些方法难以明确利用先验信息,如车道的平整度。ide

基于上述难点,本文提出了一种针对极快速度和no-visual-clue的新型车道检测方案。同时,基于提出的公式,设计了一种结构损失,以明确利用车道线的先验信息。具体而言,公式是使用全局特征来选择图像在预约义行上的车道线位置,而不是根据局部感觉野对车道线的每一个像素进行分割,即将车道线检测定义为寻找车道线在图像中某些行的位置的集合,即基于行方向上的位置选择、分类(row-based classification),选择的示意图如图2所示。函数

图2.左右车道的选择示意图。在右边部分,详细展现了行的选择。行anchor是预约义的行位置,本文的公式是在每一个行anchor上进行水平选择。在图像的右侧,引入了一个背景gridding cell来表示该行没有车道。
性能

对于no-visual-clue问题,由于本文的公式是基于全局特征进行行选择,所以本文方法也能够实现良好的性能。借助全局特征,使其具备整个图像的感觉野范围。与基于有限的感觉野的分割方法相比,能够学习和利用来自不一样位置的视觉线索和消息。这样,本文的方法能够同时解决速度和无视觉提示问题。此外,基于提出的公式,车道线表示为不一样行上的选定位置,而不是分割图。所以,能够经过优化选定位置的关系(即结构损失)来直接利用车道的刚度和平滑度。

本文的方法

图. 整体架构。辅助分支显示在上部,仅在训练时有效。特征提取器显示在蓝色框中。基于分类的预测和辅助分割任务分别显示在绿色和橙色框中。在每一个行anchor上进行组分类。

假设要检测一条车道线的图像大小为HxW,对于分割问题,则须要处理HxW个分类问题。

因为本文的方案是行向选择,假设在h个行上作选择,则只须要处理h个行上的分类问题,只不过每行上的分类问题是W维的。所以这样就把原来HxW个分类问题简化为了只须要h个分类问题,并且因为在哪些行上进行定位是能够人为设定的,所以h的大小能够按需设置,但通常h都是远小于图像高度H的。

这样,把分类数目从HxW直接缩减到了h,而且h远小于H,更不用说h远小于HxW了。所以本文的方法将计算复杂度缩减到了一个极小的范围内,解决了分割速度慢的问题,极大地提速的了车道线检测算法的速度,这也是可以达到300+FPS的缘由

class parsingNet(torch.nn.Module):
   def __init__(self, num_lanes=4, size=(288, 800), pretrained=True, backbone='50', cls_dim=(37, 10, 4), use_aux=False):
       super(parsingNet, self).__init__()

       self.num_lanes = num_lanes
       self.size = size
       self.w = size[0]
       self.h = size[1]
       self.cls_dim = cls_dim
       self.use_aux = use_aux
       self.total_dim = np.prod(cls_dim)

       # input : nchw,
       # outpur: (w+1) * sample_rows * 4 
       self.model = resnet(backbone, pretrained=pretrained)

       if self.use_aux:
           self.aux_header2 = torch.nn.Sequential(
               conv_bn_relu(128, 128, kernel_size=3, stride=1, padding=1) if backbone in ['34','18'] else conv_bn_relu(512, 128, kernel_size=3, stride=1, padding=1),
               conv_bn_relu(128,128,3,padding=1),
               conv_bn_relu(128,128,3,padding=1),
               conv_bn_relu(128,128,3,padding=1),
           )
           self.aux_header3 = torch.nn.Sequential(
               conv_bn_relu(256, 128, kernel_size=3, stride=1, padding=1) if backbone in ['34','18'] else conv_bn_relu(1024, 128, kernel_size=3, stride=1, padding=1),
               conv_bn_relu(128,128,3,padding=1),
               conv_bn_relu(128,128,3,padding=1),
           )
           self.aux_header4 = torch.nn.Sequential(
               conv_bn_relu(512, 128, kernel_size=3, stride=1, padding=1) if backbone in ['34','18'] else conv_bn_relu(2048, 128, kernel_size=3, stride=1, padding=1),
               conv_bn_relu(128,128,3,padding=1),
           )
           self.aux_combine = torch.nn.Sequential(
               conv_bn_relu(384, 256, 3,padding=2,dilation=2),
               conv_bn_relu(256, 128, 3,padding=2,dilation=2),
               conv_bn_relu(128, 128, 3,padding=2,dilation=2),
               conv_bn_relu(128, 128, 3,padding=4,dilation=4),
               torch.nn.Conv2d(128,num_lanes + 1,1)
           )
           initialize_weights(self.aux_header2,self.aux_header3,self.aux_header4,self.aux_combine)

       self.cls = torch.nn.Sequential(
           torch.nn.Linear(1800, 2048),
           torch.nn.ReLU(),
           torch.nn.Linear(2048, self.total_dim),
       )

       self.pool = torch.nn.Conv2d(512,8,1) if backbone in ['34','18'] else torch.nn.Conv2d(2048,8,1)
       # 1/32,2048 channel
       # 288,800 -> 9,40,2048
       # (w+1) * sample_rows * 4
       # 37 * 10 * 4
       initialize_weights(self.cls)

   def forward(self, x):
       # n c h w - > n 2048 sh sw
       # -> n 2048
       x2,x3,fea = self.model(x)
       if self.use_aux:
           x2 = self.aux_header2(x2)
           x3 = self.aux_header3(x3)
           x3 = torch.nn.functional.interpolate(x3,scale_factor = 2,mode='bilinear')
           x4 = self.aux_header4(fea)
           x4 = torch.nn.functional.interpolate(x4,scale_factor = 4,mode='bilinear')
           aux_seg = torch.cat([x2,x3,x4],dim=1)
           aux_seg = self.aux_combine(aux_seg)
       else:
           aux_seg = None

       fea = self.pool(fea).view(-1, 1800)

       group_cls = self.cls(fea).view(-1, *self.cls_dim)

       if self.use_aux:
           return group_cls, aux_seg

       return group_cls


def initialize_weights(*models):
   for model in models:
       real_init_weights(model)
def real_init_weights(m):

   if isinstance(m, list):
       for mini_m in m:
           real_init_weights(mini_m)
   else:
       if isinstance(m, torch.nn.Conv2d):    
           torch.nn.init.kaiming_normal_(m.weight, nonlinearity='relu')
           if m.bias is not None:
               torch.nn.init.constant_(m.bias, 0)
       elif isinstance(m, torch.nn.Linear):
           m.weight.data.normal_(0.0, std=0.01)
       elif isinstance(m, torch.nn.BatchNorm2d):
           torch.nn.init.constant_(m.weight, 1)
           torch.nn.init.constant_(m.bias, 0)
       elif isinstance(m,torch.nn.Module):
           for mini_m in m.children():
               real_init_weights(mini_m)
       else:
           print('unkonwn module', m)

一、 New formulation for lane detection

Definition of our formulation定义

本文提出将车道检测问题表达为基于全局图像特征的基于行的选择方法。换句话说,本文的方法是使用全局特征在每一个预约义行上选择车道的正确位置。在本文中,车道被描述为预约义行(即行anchor)处水平位置的变化。为了表示位置,第一步是网格化。在每行anchor点上,该位置分为许多单元。这样,车道的检测能够描述为在预约义的行anchor上选择某些单元,如图3(a)所示。

图3 本文的方法和常规分割的示意图。本文的公式是选择行上的位置(网格),而分割方法则对每一个像素进行分类。用于分类的尺寸也不一样,用红色标记。所提出的公式大大下降了计算成本。此外,本文提出的公式以全局特征为输入,具备比分割方法更大的感觉野,从而解决了no-visual-clue问题

假设最大车道数为C,行锚数为h,网格单元数为w。假设X是全局图像特征,而且是用于选择第i车道第j行锚点上车道位置的分类器。那么车道线的预测能够写成:

n其中Pij:向量表示为第i车道第j行锚点选择(w + 1)个网格单元的几率。假设Tij :是正确位置的选择。则公式的损失函数能够写成:

其中LCE是交叉熵损失。公式之因此由(w + 1)维分类而不是w维分类组成,是由于使用了一个维度来表示不存在车道。从图1能够看出,本文的方法基于全局特征预测了每行anchor点上全部位置的几率分布,而后能够根据几率分布选择正确的位置。

How  the  formulation  handles  no-visual-clue  problem?

为了处理no-visual-clue 问题,利用来自其余位置的信息很重要,由于无视觉线索意味着在目标位置没有信息。例如,一个车道被汽车遮挡,可是仍然能够经过来自其余车道,道路形状甚至汽车方向的信息来定位该车道。这样,利用来自其余位置的信息是解决无视觉提示问题的关键,如图1所示。

从感觉野的角度来看,本文的公式具备整个图像的感觉野,远大于分割方法。来自图像其余位置的上下文信息和消息可用于解决无视觉提示问题。从学习的角度来看,还能够根据公式使用结构损失来学习诸如车道的形状和方向之类的先验信息。局部感觉野小致使的复杂车道线检测困难问题。因为本文的方法不是分割的全卷积形式,是通常的基于全链接层的分类,它所使用的特征是全局特征。这样就直接解决了感觉野的问题,对于本文的方法,在检测某一行的车道线位置时,感觉野就是全图大小。所以也不须要复杂的信息传递机制就能够实现很好的效果。

另外一个好处是,这种公式化以基于行的方式对车道位置进行建模,这使咱们有机会明确地创建不一样行之间的关系因为分割方法获得的为车道线的二值分割图,其结构是逐像素建模,所以几乎没法实现对上述高层语义(平滑、刚性)层级的约束,能够缓解由low-level像素级建模和车道的high-level 长结构引发的原始语义鸿沟。

二、 Lane structural loss

除了分类损失外,还提出了两个损失函数,旨在模拟车道线中点的位置关系。这样,有利于学习结构信息。同时,因为有了水平行方向上直接的位置信息,还可使用这些信息来加入车道线的先验约束——平滑性和刚性。

相邻行上分类的L1范数定义为平滑性,但愿车道线位置在相邻行上是相近且平滑变化的。第一个损失函数是从车道是连续的事实得出的,也就是说,相邻行锚中的车道点应彼此靠近。在公式中,车道的位置由分类矢量表示。所以,经过限制分类向量在相邻行锚上的分布来实现平滑性。这样,类似度损失函数能够定义为:

另外一个结构损失函数着眼于车道线的形状。通常来讲,大多数车道线是直的。即便对于弯道,通过透视变换,大部分弯道仍然能够看做是直的。将相邻行间的二阶差分定义为车道线的形状。因为车道线大可能是直线,所以其二阶差分为0,因此约束其二阶差分与0的差别能够在优化过程当中使得预测出的车道线更直。

在分类表述中,类没有明显的顺序,而且很难在不一样的行锚之间创建关系。为了解决这个问题,使用预测的指望值做为位置的近似值。而预测的指望值由softmax函数获得。

其中Loci,j是第i个车道上的位置,即第j行anchor。之因此使用二阶差分而不是一阶差分,是由于一阶差分在大多数状况下并不为零。因此网络须要额外的参数来学习车道位置的一阶差分的分布。此外,二阶差分的约束相对比一阶差分的约束要弱,所以致使车道不直时影响较小。最后,总体结构损失为:

三、Feature aggregation

上节中的损失设计主要集中在通道的内部关系上。在本节中,提出一种特征聚合方法,该方法着重于全局上下文和局部特征的聚合。提出了一种利用多尺度特征的辅助分割任务来对局部特征进行建模。并使用交叉熵做为辅助分割损失。这样,本文方法的总体损失能够写成:

本文的方法仅在训练阶段使用辅助分割任务,而在测试阶段将其删除。这样,即便添加了额外的分割任务,本文的方法的运行速度也不会受到影响,与不使用辅助分割任务的网络相同。

class OhemCELoss(nn.Module):
   def __init__(self, thresh, n_min, ignore_lb=255, *args, **kwargs):
       super(OhemCELoss, self).__init__()
       self.thresh = -torch.log(torch.tensor(thresh, dtype=torch.float)).cuda()
       self.n_min = n_min
       self.ignore_lb = ignore_lb
       self.criteria = nn.CrossEntropyLoss(ignore_index=ignore_lb, reduction='none')

   def forward(self, logits, labels):
       N, C, H, W = logits.size()
       loss = self.criteria(logits, labels).view(-1)
       loss, _ = torch.sort(loss, descending=True)
       if loss[self.n_min] > self.thresh:
           loss = loss[loss>self.thresh]
       else:
           loss = loss[:self.n_min]
       return torch.mean(loss)


class SoftmaxFocalLoss(nn.Module):
   def __init__(self, gamma, ignore_lb=255, *args, **kwargs):
       super(SoftmaxFocalLoss, self).__init__()
       self.gamma = gamma
       self.nll = nn.NLLLoss(ignore_index=ignore_lb)

   def forward(self, logits, labels):
       scores = F.softmax(logits, dim=1)
       factor = torch.pow(1.-scores, self.gamma)
       log_score = F.log_softmax(logits, dim=1)
       log_score = factor * log_score
       loss = self.nll(log_score, labels)
       return loss

class ParsingRelationLoss(nn.Module):
   def __init__(self):
       super(ParsingRelationLoss, self).__init__()
   def forward(self,logits):
       n,c,h,w = logits.shape
       loss_all = []
       for i in range(0,h-1):
           loss_all.append(logits[:,:,i,:] - logits[:,:,i+1,:])
       #loss0 : n,c,w
       loss = torch.cat(loss_all)
       return torch.nn.functional.smooth_l1_loss(loss,torch.zeros_like(loss))



class ParsingRelationDis(nn.Module):
   def __init__(self):
       super(ParsingRelationDis, self).__init__()
       self.l1 = torch.nn.L1Loss()
       # self.l1 = torch.nn.MSELoss()
   def forward(self, x):
       n,dim,num_rows,num_cols = x.shape
       x = torch.nn.functional.softmax(x[:,:dim-1,:,:],dim=1)
       embedding = torch.Tensor(np.arange(dim-1)).float().to(x.device).view(1,-1,1,1)
       pos = torch.sum(x*embedding,dim = 1)

       diff_list1 = []
       for i in range(0,num_rows // 2):
           diff_list1.append(pos[:,i,:] - pos[:,i+1,:])

       loss = 0
       for i in range(len(diff_list1)-1):
           loss += self.l1(diff_list1[i],diff_list1[i+1])
       loss /= len(diff_list1) - 1
       return loss

实验与结果

数据集:  TuSimple、CULane

数据加强

因为车道线的固有结构,基于分类的网络可能容易使训练集过拟合,而且在验证集上显示出较差的性能。为了防止这种现象并得到泛化能力,利用了由旋转,垂直和水平移位组成的加强方法。此外,为了保留车道结构,车道被延伸到图像的边界。

消融实验

一、Effects of number of gridding cells.

本文使用网格化和选择来创建车道中的结构信息与基于分类的公式之间的关系。经过这种方式,进一步尝试使用具备不一样数量的网格单元格的方法来证实对方法的影响。

随着网格数量的增长,能够看到top1,top2和top3的分类精度逐渐下降。这很容易理解,由于愈来愈多的单元格须要更细粒度和更困难的分类。可是,评估精度不是严格单调的。尽管较少的网格化单元意味着更高的分类精度,可是定位偏差会更大,由于每一个网格化单元太大而没法生成精确的定位预测。在这项工做中,选择100做为Tusimple数据集上的网格单元数。

二、Effectiveness of localization methods.

CLS表示基于分类的方法,而REG表示基于回归的方法。CLS和CLS Exp之间的区别是它们的定位方法不一样。

三、Effectiveness  of  the  proposed  modules.

对比实验

能够看到在Tusimple数据集上咱们的方法比SCNN快了41.7倍,比SOTA的SAD也快了4倍。可是Tusimple数据集上你们性能也比较饱和了,没有达到SOTA的水平。

可视化实验

Tusimple数据集和CULane数据集的可视化。前四行是Tusimple数据集的结果。其他行是CULane数据集上的结果,从左到右分别是图像,预测和GT。在图像中,预测的车道点用蓝色标记,GT用红色标记。由于本文基于分类的公式仅在预约义的行锚上进行预测,因此图像和标签在垂直方向上的比例不一样。

更多细节可参考论文原文与代码。

参考:

https://zhuanlan.zhihu.com/p/157530787