点击上方“AI算法修炼营”,选择“星标”公众号php
精选做品,第一时间送达python
本文是自动驾驶领域车道线检测的少有的开源算法,含有视频详细解读,欢迎你们多多支持UP主,一键三连。git
论文地址:https://arxiv.org/pdf/2004.10924.pdfgithub
代码地址:https://github.com/lucastabelini/PolyLaneNet算法
对于更安全的自动驾驶汽车而言,还没有彻底解决的问题之一就是车道线检测。因为自动驾驶场景的特殊性,完成此任务的方法必须作到实时(+30 FPS),所以车道线检测算法不只须要有效(即具备较高的准确性),并且还须要高效(即快速)。在这项工做中,提出了一种用于车道线检测的新方法,该方法未来自安装在车辆中的前视摄像头的图像用做输入,并经过深度多项式回归输出表明图像中每一个车道标记的多项式。在TuSimple数据集上本文的方法与现有的最新方法相比具备必定的竞争力,同时保持了效率(115 FPS)。此外,本文还介绍了另外两个公共数据集上的大量定性结果,以及最近的车道线检测工做所使用的评估指标的局限性。安全
已得到原UP主受权,转载请联系。地址:https://www.bilibili.com/video/BV1NC4y1h77c?t=25。超专业,超良心,各位看官记得去B站一键三连。
网络
简介app
自动驾驶汽车应该可以估计行车道,由于除了做为空间限制以外,每一个车道还提供了特定的视觉提示来决定行进路线。此外,检测相邻的车道可能会颇有用,这样系统的决策可能基于对交通场景的更好理解通道估计(或检测),乍看之下彷佛微不足道,但可能很是具备挑战性。尽管车道标记至关标准化,但其形状和颜色却有所不一样。当出现虚线或部分遮挡的车道标记时,估计车道须要对场景进行语义理解。此外,环境自己具备多种多样的特征:可能有不少交通,人流过路,或者可能只是一条免费的高速公路。此外,这些环境还受多种天气(例如,雨,雪,晴天等)和照明(例如白天,黑夜,黎明,隧道等)的条件的影响。dom
车道线估计(或检测)任务的传统方法包括提取手工特征而后进行曲线拟合。尽管这种方法在正常和有限的状况下每每会很好地起做用,但在不利条件下(如上述状况)一般不如所需的那样鲁棒。所以,随着许多计算机视觉问题的发展,最近开始使用深度学习来学习强大的功能并改善车道线标记估计过程。尽管如此,仍有一些限制须要解决。首先,许多基于深度学习的模型将车道标记估计分为两个步骤:特征提取和曲线拟合。大多数工做都是经过基于分割的模型来提取特征的,这些模型一般效率低下,而且难以自动驾驶所需的实时运行。另外,分割步骤不足以提供车道标记估计,由于必须对分割图进行后处理才能输出交通线。此外,这两个步骤的过程可能会忽略全局信息,当缺乏视觉提示时(例如在强烈的阴影和遮挡中),这尤为重要。其次,其中一些工做是由私人公司执行的,这些公司一般不提供复制其结果的手段,而且在私人数据集上开发其方法,这阻碍了研究的进展。最后,评估标准还有改进的余地。这些方法一般仅在美国的数据集上进行测试(一般对发展中国家的道路维护得不太好),而且评估指标过于宽松(它们容许出现错误,从而妨碍了适当的比较)。在这种状况下,专一于消除两步过程的方法可进一步下降处理成本,这将有利于一般依赖于低能耗和嵌入式硬件的高级驾驶员辅助系统(ADAS)。ide
本文工做提出了PolyLaneNet,一种用于端到端车道线检测估计的卷积神经网络。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
模型训练
对于输入图像,令M为给定输入图像的带标注的车道标记的数量。一般,交通场景包含的车道线不多,可用数据集中的大多数图像的M≤4。为了进行训练(和度量评估),将每一个带标注的车道标记j,j = 1,...,M与输出的神经元关联。所以,在损失函数中应忽略与输出M + 1,...,Mmax有关的预测。对于每一个车道标记j,将垂直偏移量设置为;置信度定义为:
对于单个图像,使用多任务损失函数进行训练。
其中,Lreg和Lcls分别是均方偏差(MSE)和二进制交叉熵(BCE)函数。Lp损失函数测量多项式pj(式1)对标注点的调整程度。
其中τloss是根据经验定义的阈值,试图减小损失的焦点在已经很好对齐的点上。之因此出现这种效果,是由于车道标记包含具备不一样采样差别的几个点(即,距离摄像机较近的点比距离较远的点更密集)。最后,Lp定义为:
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
对比实验
消融实验
可视化实验
更多细节可参考论文原文。