本文是自动驾驶领域车道线检测的少有的开源算法,含有视频详细解读,欢迎大家多多支持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
PolyLaneNet期望从前视车辆摄像头中获取输入图像,并为每个图像输出Mmax车道线候选标记(表示为多项式)以及水平线的垂直位置,这有助于定义车道线标记的上限。PolyLaneNet的体系结构包括一个主干网络(用于特征提取),该主干网络附加有一个全连接层,具有Mmax 1个输出。PolyLaneNet采用多项式表示法而不是一组标记点。
其中,K是定义多项式阶数的参数。如图1所示,多项式具有受限域:图像的高度。除系数外,模型还针对每个车道标记j估计垂直偏移量j和预测置信度得分cj∈[0,1]。总之,PolyLaneNet模型可以表示为
其中,I为输入图像,θ为模型参数。在运行中的系统中,如图1所示,只有置信度得分大于或等于阈值的候选车道线才被视为检测到。
代码语言:javascript复制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定义为:
代码语言:javascript复制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
实验配置:
代码语言:javascript复制# 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
对比实验
消融实验
可视化实验
更多细节可参考论文原文。