第一部分: 基于LaneNet + H-Net的车道线检测概述
LaneNet将车道线检测问题当做实例分割问题来处理, 类似图像中并排着多人的实例分割, 即每个车道线作为独立的实例被识别,它们都是属于同一类别(车道线),只是分为不同实例, 并为每条车道线标记一个唯一ID. 如下图(图片来源网络):
如上图右所示,LaneNet分割出的车道线实例粗细不均,需要进一步优化处理. 原论文作者在LaneNet输出上述结果之后, 接了一个简单的已经训练好的H-Net网络. 该H-Net由卷积层和全连接层组成,同一原图经H-Net输出一个转换矩阵H. 当LaneNet输出车道线实例分割图后, 经换矩阵H对每个车道线实例上像素点进行修正和回归,最终拟合出一个三阶多项式作为预测车道线曲线方程. 进一步将曲线方程绘制到原始图上即可.
LaneNet + H-Net的车道线检测结构图如下(图片源于网络):
第二部分: LaneNet结构
前面讲过,LaneNet灵感来自语义分割和实例分割,所以LaneNet网络整体是一个编码器-解码器结构,类似下图(来自网络的SegNet网络结构图):
左半部分称为编码器, 为输入图像的编码阶段. 接收输入图片后,经过一层一层的卷积和池化操作,特征图尺寸越来越小(池化操作), 特征图通道数越来越多(卷积操作),特征图语义信息越来越丰富,而图像分辨率(细节)越来越少, 形成了中间的累计被加密的特征层. 右半部分称为解码器,特征层的解码阶段,这个阶段会逐渐上采样特征图,逐渐方法特征图尺寸,上采样方式有反卷积,邻近插值,双线性插值等方法, 但以反卷积效果最好, 一般上采样阶段逐层增大特征图的尺寸(反卷积), 同时采用1x1卷积逐渐减少特征图通道个数(根据需要)(1x1卷积),最终达到和原输入图一样的特征尺寸和需要的通道数.
这是的特征图一般就会包含原图中重要的边缘信息而丢失了纹理细节信息,达到分割的目的.
值得注意的是, 在上采样过程中,为了获得更丰富的边缘细节信息,都用采用从编码阶段获取一些层的pool结果,和当前解码晨刚输入一起通过通道拼接(或逐元素相加.或逐元素相乘)方式,融合浅层的特征信息后,再一起执行上采样.这样获得语义分割或实例分割效果更好.
LaneNet网络具有类似的结果,但不同的是,在解码器阶段,分为两个分支,如下图(图片来至网络):
嵌入Embedding分支:
负责对像素进行嵌入表示,训练得到嵌入向量进行聚类; 主要用于车道线的实例分割,如图所示,该网络解码得到的输出图中每一个像素对应一个N维的向量,在该N维嵌入空间中同一车道线的像素距离更接近,而不同车道线的像素的向量距离较大,从而区分像素属于哪一条车道线。
分割Segmentation分支:
对输入图像进行语义分割,并对像素点进行二分类,判断属于车道线还是背景. 主要用于获取车道像素的分割结果(得到一个二分图,即筛选出车道线像素和非车道线像素),得到一个二分图像,如上图所示。
最后, 将两个分支的结果结合聚类得到最终车道线检测的结果. 如上图,检测结果是数条粗细不规则的车道线, 可以利用一些拟合直线(曲线)方法求解出理想曲线. 原作者的方法是利用H-Net,H-Net接收上LaneNet的输出结果,拟合出一个三阶多项式作为预测车道线曲线方程.
第三部分: 基于Pytorch的LaneNet网络实现
LaneNet的特征提取编码网络(网络骨架Backbone)的实现,可以使用VGG-16或其他性能不错的特征提取网络(原论文使用的是E-Net).我的代码是以VGG-16为编码器网络.
VGG网络大家都熟悉,实现是我们可以利用Pytorch内置的torchvision.models.vgg16,也可以自己按照VGG网络结构图编写其网络骨架, 一个典型的VGG网络家族结构图如下:
如上图,无论是使用Pytorch内置的VGG16网络还是自己编写VGG16网络,只需要使用或编写上述淡黄色区域卷积层和所有的MaxPool层即可. 其中,淡蓝色标记的MaxPool层要提取出来供解码阶段上采样时融合浅层特征使用.
下面介绍用Pytorch实现VGG-16编码器的代码片段,当然也可以用Pytorch预置的tochvision.models.vgg16(pretrained=False).features, 直接取pool3,pool4和pool5三层:
#特征提取部分的编码器网络代码结构同上图中VGG-16 D列一致
class VGGEncoder(nn.Module):
def __init__(self):
super(VGGEncoder,self).__init__()
self.blocks_num = 5 #对应于上图VGG-16网络,特征提取阶段的5个卷积层块
self.in_channels = [3, 64, 128, 256, 512] #每一个卷积块的输入通道数,输入原始图像通道为为3
#每一个卷积块的输处通道数,也即下一个卷积块的输入通道数,注意第4和第5个卷积块的输出通道数都是512
self.out_channels = self.in_channels[1:] + [512] #即: [ 64, 128, 256, 512, 512]
self.conv_num_in_block = [2, 2, 3, 3, 3] #对应上图5个卷积块中每块的卷积层数
self.encode_net = self._encoder_net()
def _encoder_net(self):
net = nn.Sequential()
for i in range( self.blocks_num ):
net.add_module("ConvBlock" + str(i + 1), self._encode_block(i + 1) )
return net
#添加各个卷积块,各个卷积块内卷积层数符合上图: [2, 2, 3, 3, 3]
def _encode_block(self, block_id, kernel_size=3, stride=1):
out_channels = self.out_channels[block_id - 1]
padding = (kernel_size - 1) // 2
block = nn.Sequential()
for i in range(self.conv_num_in_block [block_id - 1]):
if i == 0:
in_channels = self.in_channels [block_id - 1]
else:
in_channels = out_channels
block.add_module("conv_{}_{}".format(block_id, i + 1), \
nn.Conv2d(in_channels, out_channels, kernel_size, \
stride=stride, padding=padding))
block.add_module("bn_{}_{}".format(block_id, i + 1), \
nn.BatchNorm2d(out_channels))
block.add_module("relu_{}_{}".format(block_id, i + 1), nn.ReLU())
block.add_module("maxpool" + str(block_id), nn.MaxPool2d(kernel_size=2, \
stride=2))
return block
#编码器前向网络部分,返回一个pool3,pool4,pool5的特征字典,供后续解码器上采样做浅层特征融合使用
def forward(self, x):
retMaxPoolLayers = OrderedDict()
#调用编码器网络的5个卷积块block
for i, block in enumerate(self.encode_net):
poolResult = block(x) #返回各个模块最大值池化后结果
retMaxPoolLayers["pool" + str(i + 1)] = poolResult
x = poolResult
return retMaxPoolLayers
第四部分: 在ROS系统进行LaneNet车道线检测实践
在ROS系统创建包的过程这里就不详细介绍了, 直接上实践结果