无人驾驶汽车系统入门(三十)——基于深度神经网络LaneNet的车道线检测及ROS实现
前面的博文介绍了基于传统视觉的车道线检测方法,传统视觉车道线检测方法主要分为提取特征、车道像素聚类和车道线多项式拟合三个步骤。然而,无论是颜色特征还是梯度特征,人为设计的特征阈值存在鲁棒性差的问题,深度学习方法为车道线的检测带来了高鲁棒性的解决思路,在近年来逐步替代了传统视觉方法,本文介绍一种用于车道线检测的典型神经网络LaneNet,并且基于其开源的实现代码编写了一个ROS程序。
论文原文地址:Towards End-to-End Lane Detection: an Instance Segmentation Approach
Tensorflow实现代码原地址:lanenet-lane-detection
本文ROS实现代码地址:LaneNetRos
如果你想在Python2环境下训练LaneNet,试试我修改的版本:lanenet-lane-detection-py2
LaneNet 网络结构
LaneNet将图像中的车道线检测看作是一个实例分割(Instance Segmentation)问题,是一个端到端的模型,输入图像,网络直接输出车道线像素以及每个像素对应的车道线ID,无需人为设计特征,在提取出车道像素以及车道ID以后,通常我们将图像投射到鸟瞰视角,以进一步完成车道线拟合(主要是三次多项式拟合),传统的做法是使用一个固定的变换矩阵对图像进行透视变换,这种方法在路面不平的情况下存在较大的偏差,而论文作者的做法是训练一个名为H-Net的简单神经网络,输入当前图像到H-Net,这个简单神经网络输出相应的鸟瞰变换矩阵。完成鸟瞰变换以后,使用最小二乘法拟合一个2次(或者3次)多项式,从而完成车道线检测。
如上图是LaneNet的网络结构,和传统的语义分割网络类似,包含编码网络(Encoder)和解码网络(Decoder),解码网络包含两个分支,对应的是两类分割:
- 嵌入分支(Embedding branch):主要用于车道线的实例分割,如图所示,该网络解码得到的输出图中每一个像素对应一个N维的向量,在该N维嵌入空间中同一车道线的像素距离更接近,而不同车道线的像素的向量距离较大,从而区分像素属于哪一条车道线,该分支使用基于one-shot的方法做距离度量学习。
- 分割分支(Segmentation branch):主要用于获取车道像素的分割结果(得到一个二分图,即筛选出车道线像素和非车道线像素),得到一个二分图像,如上图所示。为了训练一个这样的分割网络,原论文使用了tuSample数据集,在数据集中,车道线可能被其他车辆阻挡,在这种情况下,将车道线的标注(Ground truth)贯穿障碍物,如下图所示,从而使得分割网络在车道线被其他障碍物阻挡的情况下,依然可以完整检测出完整的车道线像素。分割网络使用标准的交叉熵损失函数进行训练,对于这个逐像素分类任务而言(车道线像素/非车道线像素分类),由于两个类别的像素极度不均衡,为了处理此问题,作者使用了bounded inverse class weighting方法(详见论文)。
那么通过叠加嵌入分支和分割分支,在使用神经网络提取出车道线像素的同时,还能够对每个车道线实现聚类(即像素属于哪一根车道线)。为了训练这样的聚类嵌入网络,聚类损失函数(嵌入网络)包含两部分,方差项 L v a r L_{var} Lvar 和距离项 L d i s t L_{dist} Ldist ,其中 L v a r L_{var} Lvar 将每个嵌入的向量往某条车道线聚类中心(均值)方向拉,这种“拉力”激活的前提是嵌入向量到平均嵌入向量的距离过远,大于阈值 δ v \delta_v δv ; L d i s t L_{dist} Ldist 使两个类别的车道线越远越好,激活这个“推力”的前提是两条车道线聚类中心的距离过近,近于阈值 δ d \delta_d δd 。最后总的损失函数L的公式如下:
其中 C C C 表示聚类的数量(也就是车道线的数量), N c N_c Nc 表示聚类 c c c 中的元素数量, x i x_i xi 表示一个像素嵌入向量, μ c \mu_{c} μc 表示聚类 c c c 的均值向量, [ x ] + = m a x ( 0 , x ) [x]_{+} = max(0, x) [x]+=max(0,x),最后的损失函数为 L = L v a r + L d i s t L = L_{var} + L_{dist} L=Lvar+Ldist 。该损失函数在实际实现(TensorFlow)中代码如下:
def discriminative_loss_single(
prediction,
correct_label,
feature_dim,
label_shape,
delta_v,
delta_d,
param_var,
param_dist,
param_reg):
"""
实例分割损失函数
:param prediction: inference of network
:param correct_label: instance label
:param feature_dim: feature dimension of prediction
:param label_shape: shape of label
:param delta_v: cut off variance distance
:param delta_d: cut off cluster distance
:param param_var: weight for intra cluster variance
:param param_dist: weight for inter cluster distances
:param param_reg: weight regularization
"""
# 像素对齐为一行
correct_label = tf.reshape(
correct_label, [
label_shape[1] * label_shape[0]])
reshaped_pred = tf.reshape(
prediction, [
label_shape[1] * label_shape[0], feature_dim])
# 统计实例个数
unique_labels, unique_id, counts = tf.unique_with_counts(correct_label)
counts = tf.cast(counts, tf.float32)
num_instances = tf.size(unique_labels)
# 计算pixel embedding均值向量
segmented_sum = tf.unsorted_segment_sum(
reshaped_pred, unique_id, num_instances)
mu = tf.div(segmented_sum, tf.reshape(counts, (-1, 1)))
mu_expand = tf.gather(mu, unique_id)
# 计算公式的loss(var)
distance = tf.norm(tf.subtract(mu_expand, reshaped_pred), axis=1)
distance = tf.subtract(distance, delta_v)
distance = tf.clip_by_value(distance, 0., distance)
distance = tf.square(distance)
l_var = tf.unsorted_segment_sum(distance, unique_id, num_instances)
l_var = tf.div(l_var, counts)
l_var = tf.reduce_sum(l_var)
l_var = tf.divide(l_var, tf.cast(num_instances, tf.float32))
# 计算公式的loss(dist)
mu_interleaved_rep = tf.tile(mu, [num_instances, 1])
mu_band_rep = tf.tile(mu, [1, num_instances])
mu_band_rep = tf.reshape(
mu_band_rep,
(num_instances *
num_instances,
feature_dim))
mu_diff = tf.subtract(mu_band_rep, mu_interleaved_rep)
# 去除掩模上的零点
intermediate_tensor = tf.reduce_sum(tf.abs(mu_diff), axis=1)
zero_vector = tf.zeros(1, dtype=tf.float32)
bool_mask = tf.not_equal(intermediate_tensor, zero_vector)
mu_diff_bool = tf.boolean_mask(mu_diff, bool_mask)
mu_norm = tf.norm(mu_diff_bool, axis=1)
mu_norm = tf.subtract(2. * delta_d, mu_norm)
mu_norm = tf.clip_by_value(mu_norm, 0., mu_norm)
mu_norm = tf.square(mu_norm)
l_dist = tf.reduce_mean(mu_norm)
# 计算原始Discriminative Loss论文中提到的正则项损失
l_reg = tf.reduce_mean(tf.norm(mu, axis=1))
# 合并损失按照原始Discriminative Loss论文中提到的参数合并
param_scale = 1.
l_var = param_var * l_var
l_dist = param_dist * l_dist
l_reg = param_reg * l_reg
loss = param_scale * (l_var + l_dist + l_reg)
return loss, l_var, l_dist, l_reg
在上述代码中,首先统计了聚类的个数num_instances
(即 C C C ) 和每个聚类中像素的数量 counts
(即 N c