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

2020-07-14 14:43:46 浏览数 (3)

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

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

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

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

简介

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

基于上述难点,本文提出了一种针对极快速度和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的原因。

代码语言:javascript复制
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)

1、 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 长结构引起的原始语义鸿沟。

2、 Lane structural loss

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

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

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

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

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

3、Feature aggregation

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

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

代码语言:javascript复制
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

数据增强

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

消融实验

1、Effects of number of gridding cells.

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

随着网格数量的增加,可以看到top1,top2和top3的分类精度逐渐降低。这很容易理解,因为越来越多的单元格需要更细粒度和更困难的分类。但是,评估精度不是严格单调的。尽管较少的网格化单元意味着更高的分类精度,但是定位误差会更大,因为每个网格化单元太大而无法生成精确的定位预测。在这项工作中,选择100作为Tusimple数据集上的网格单元数。

2、Effectiveness of localization methods.

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

3、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

0 人点赞