Rocky X · 2020年07月07日

PolyLaneNet:最新车道线检测开源算法,多项式回归实时高效

作者:SFXiang
首发:AI算法修炼营
本文是自动驾驶领域车道线检测的少有的开源算法,含有视频详细解读,欢迎大家多多支持UP主,一键三连。

论文地址: https://arxiv.org/pdf/2004.10924.pdf
代码地址: https://github.com/lucastabelini/PolyLaneNet

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

已获得原UP主授权,转载请联系。视频地址:https://www.bilibili.com/video/BV1NC4y1h77c?t=25
超专业,超良心,各位看官记得去B站一键三连

简介

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

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

本文工作提出了PolyLaneNet,一种用于端到端车道线检测估计的卷积神经网络。PolyLaneNet从安装在车辆中的前视摄像头获取输入图像,并输出代表图像中每个车道标记的多项式,以及域车道多项式和每个车道的置信度得分。该方法与现有的最新方法相比具有竞争优势,同时速度更快,不需要后处理即可获得车道估算值。并公开发布了源代码(用于训练和推理)和经过训练的模型,从而可以复制本文中介绍的所有结果。

本文的方法:POLYLANENET

640-1.png

PolyLaneNet期望从前视车辆摄像头中获取输入图像,并为每个图像输出Mmax车道线候选标记(表示为多项式)以及水平线的垂直位置,这有助于定义车道线标记的上限。PolyLaneNet的体系结构包括一个主干网络(用于特征提取),该主干网络附加有一个全连接层,具有Mmax + 1个输出。PolyLaneNet采用多项式表示法而不是一组标记点

640-2.png
其中,K是定义多项式阶数的参数。如图1所示,多项式具有受限域:图像的高度。除系数外,模型还针对每个车道标记j估计垂直偏移量j和预测置信度得分cj∈[0,1]。总之,PolyLaneNet模型可以表示为

640-3.png

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

   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  

模型训练

对于输入图像,令M为给定输入图像的带标注的车道标记的数量。通常,交通场景包含的车道线很少,可用数据集中的大多数图像的M≤4。为了进行训练(和度量评估),将每个带标注的车道标记j,j = 1,...,M与输出的神经元关联。因此,在损失函数中应忽略与输出M + 1,...,Mmax有关的预测。对于每个车道标记j,将垂直偏移量设置为640-4.png;置信度定义为:
640-5.png

对于单个图像,使用多任务损失函数进行训练。
640-6.png

其中,Lreg和Lcls分别是均方误差(MSE)和二进制交叉熵(BCE)函数。Lp损失函数测量多项式pj(式1)对标注点的调整程度。
640-7.png
其中τloss是根据经验定义的阈值,试图减少损失的焦点在已经很好对齐的点上。之所以出现这种效果,是因为车道标记包含具有不同采样差异的几个点(即,距离摄像机较近的点比距离较远的点更密集)。最后,Lp定义为:
640-8.png

        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

640-9.png

实验配置:

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  

对比实验

640-10.png

消融实验

640-11.png

640-12.png

可视化实验

640-13.png

更多细节可参考论文原文。

推荐阅读


更多机器学习、深度学习、计算机视觉、自动驾驶、机器人等领域最新最前沿的科技请关注微信号AI算法修炼营。
WX20200303-134844.png
推荐阅读
关注数
16563
内容数
1230
嵌入式端AI,包括AI算法在推理框架Tengine,MNN,NCNN,PaddlePaddle及相关芯片上的实现。欢迎加入微信交流群,微信号:aijishu20(备注:嵌入式)
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息