作者 | 申泽邦(Adam Shan)

兰州大学在读硕士研究生,主要研究方向无人驾驶,深度学习;兰大未来计算研究院无人车团队负责人,自动驾驶全栈工程师。

之前我们提到使用SqueezeSeg进行了三维点云的分割,由于采用的是SqueezeNet作为特征提取网络,该方法的处理速度相当迅速(在单GPU加速的情况下可达到100FPS以上的效率),然而,该方法存在如下的问题:

  • 第一,虽然采用了CRF改进边界模糊的问题,但是从实践结果来看,其分割的精度仍然偏低;

  • 第二,该模型需要大量的训练集,而语义分割数据集标注困难,很难获得大规模的数据集。当然,作者在其后的文章:SqueezeSegV2: Improved Model Structure and Unsupervised Domain Adaptation for Road-Object Segmentation from a LiDAR Point Cloud 中给出了改进的方案,我将在后面的文章中继续解读。需要注意的是,在无人车环境感知问题中,很多情况下并不需要对目标进行精确的语义分割,只需将目标以一个三维的Bounding Box准确框出即可(即Detection)。

本文介绍一种基于点云的Voxel(三维体素)特征的深度学习方法,实现对点云中目标的准确检测,并提供一个简单的ROS实现,供大家参考。

VoxelNet结构

VoxelNet是一个端到端的点云目标检测网络,和图像视觉中的深度学习方法一样,其不需要人为设计的目标特征,通过大量的训练数据集,即可学习到对应的目标的特征,从而检测出点云中的目标,如下:

VoxelNet的网络结构主要包含三个功能模块:

(1)特征学习层;

(2)卷积中间层;

(3) 区域提出网络( Region Proposal Network,RPN)。

特征学习网络

特征学习网络的结构如下图所示,包括体素分块(Voxel Partition),点云分组(Grouping),随机采样(Random Sampling),多层的体素特征编码(Stacked Voxel Feature Encoding),稀疏张量表示(Sparse Tensor Representation)等步骤,具体来说:

体素分块

这是点云操作里最常见的处理,对于输入点云,使用相同尺寸的立方体对其进行划分,我们使用一个深度、高度和宽度分别为(D,H,W)的大立方体表示输入点云,每个体素的深高宽为(vD,vH,vW) ,则整个数据的三维体素化的结果在各个坐标上生成的体素格(voxel grid)的个数为:

点云分组

将点云按照上一步分出来的体素格进行分组,如上图所示。

随机采样

很显然,按照这种方法分组出来的单元会存在有些体素格点很多,有些格子点很少的情况,64线的激光雷达一次扫描包含差不多10万个点,全部处理需要的计算力和内存都很高,而且高密度的点势必会给神经网络的计算结果带来偏差。所以,该方法在这里插入了一层随机采样,对于每一个体素格,随机采样固定数目的点,T 。

多个体素特征编码(Voxel Feature Encoding,VFE)层

之后是多个体素特征编码层,简称为VFE层,这是特征学习的主要网络结构,以第一个VFE层为例说明:

对于输入:

是一个体素格内随机采样的点集,分别点的XYZ坐标以及激光束的反射强度(即intensity),我们首先计算体素内所有点的平均值 (vx,vy,vz) 作为体素格的形心(类似于Voxel Grid Filter),那么我们就可以将体素格内所有点的特征数量扩充为如下形式:

接着,每一个都会通过一个全连接网络(Fully Connected,FC,论文中用的是FCN来简称,实际上FCN更多的被用于表示全卷积网络,所以原文此处用FCN简称实际上不妥)被映射到一个特征空间,输入的特征维度为7,输出的特征维数变成m mm,全连接层包含了一个线性映射层,一个批标准化(Batch Normalization),以及一个非线性运算(ReLU),得到逐点的(point-wise)的特征表示。

接着我们采用最大池化(MaxPooling)对上一步得到的特征表示进行逐元素的聚合,这一池化操作是对元素和元素之间进行的,得到局部聚合特征(Locally Aggregated Feature),即 ,最后,将逐点特征和逐元素特征进行连接(concatenate),得到输出的特征集合:

对于所有的非空的体素格我们都进行上述操作,并且它们都共享全连接层(FC)的参数。我们使用符号来描述经过VFE以后特征的维数变化,那么显然全连接层的参数矩阵大小为:

由于VFE层中包含了逐点特征和逐元素特征的连接,经过多层VFE以后,我们希望网络可以自动学习到每个体素内的特征表示(比如说体素格内的形状信),那么如何学习体素内的特征表示呢?原论文的方法下图所示:

通过对体素格内所有点进行最大池化,得到一个体素格内特征表示 C 。

稀疏张量表示

通过上述流程处理非空体素格,我们可以得到一系列的体素特征(Voxel Feature)。这一系列的体素特征可以使用一个4维的稀疏张量来表示:

虽然一次lidar扫描包含接近10万个点,但是超过90%的体素格都是空的,使用稀疏张量来描述非空体素格在于能够降低反向传播时的内存和计算消耗。

对于具体的车辆检测问题,我们取沿着Lidar坐标系的(Z,Y,X) (Z,Y,X)(Z,Y,X)方向取[−3,1]×[−40,40]×[0,70.4] [−3, 1] × [−40, 40] × [0, 70.4][−3,1]×[−40,40]×[0,70.4]立方体(单位为米)作为输入点云,取体素格的大小为:

那么有

我们设置随机采样的T=35 T = 35T=35,并且采用两个VFE层:VFE-1(7, 32) 和 VFE-2(32, 128),最后的全连接层将VFE-2的输出映射到 。最后,特征学习网络的输出即为一个尺寸为 (128×10×400×352) 的稀疏张量。整个特征网络的TensorFlow实现代码如下:

class VFELayer(object):

def __init__(self, out_channels, name):
        super(VFELayer, self).__init__()
        self.units = int(out_channels / 2)
        with tf.variable_scope(name, reuse=tf.AUTO_REUSE) as scope:
            self.dense = tf.layers.Dense(
                self.units, tf.nn.relu, name='dense', _reuse=tf.AUTO_REUSE, _scope=scope)
            self.batch_norm = tf.layers.BatchNormalization(
                name='batch_norm', fused=True, _reuse=tf.AUTO_REUSE, _scope=scope)

def apply(self, inputs, mask, training):
        # [K, T, 7] tensordot [7, units] = [K, T, units]
        pointwise = self.batch_norm.apply(self.dense.apply(inputs), training)

#n [K, 1, units]
        aggregated = tf.reduce_max(pointwise, axis=1, keep_dims=True)

# [K, T, units]
        repeated = tf.tile(aggregated, [1, cfg.VOXEL_POINT_COUNT, 1])

# [K, T, 2 * units]
        concatenated = tf.concat([pointwise, repeated], axis=2)

mask = tf.tile(mask, [1, 1, 2 * self.units])

concatenated = tf.multiply(concatenated, tf.cast(mask, tf.float32))

return concatenated

class FeatureNet(object):

def __init__(self, training, batch_size, name=''):
        super(FeatureNet, self).__init__()
        self.training = training

# scalar
        self.batch_size = batch_size
        # [ΣK, 35/45, 7]
        self.feature = tf.placeholder(
            tf.float32, [None, cfg.VOXEL_POINT_COUNT, 7], name='feature')
        # [ΣK]
        self.number = tf.placeholder(tf.int64, [None], name='number')
        # [ΣK, 4], each row stores (batch, d, h, w)
        self.coordinate = tf.placeholder(
            tf.int64, [None, 4], name='coordinate')

with tf.variable_scope(name, reuse=tf.AUTO_REUSE) as scope:
            self.vfe1 = VFELayer(32, 'VFE-1')
            self.vfe2 = VFELayer(128, 'VFE-2')

# boolean mask [K, T, 2 * units]
        mask = tf.not_equal(tf.reduce_max(
            self.feature, axis=2, keep_dims=True), 0)
        x = self.vfe1.apply(self.feature, mask, self.training)
        x = self.vfe2.apply(x, mask, self.training)

# [ΣK, 128]
        voxelwise = tf.reduce_max(x, axis=1)

# car: [N * 10 * 400 * 352 * 128]
        # pedestrian/cyclist: [N * 10 * 200 * 240 * 128]
        self.outputs = tf.scatter_nd(
            self.coordinate, voxelwise, [self.batch_size, 10, cfg.INPUT_HEIGHT, cfg.INPUT_WIDTH, 128])

卷积中间层

每一个卷积中间层包含一个3维卷积,一个BN层(批标准化),一个非线性层(ReLU),我们用:

来描述一个卷积中间层,Conv3D表示是三维卷积,cin,cout分别表示输入和输出的通道数,k是卷积核的大小,它是一个向量,对于三维卷积而言,卷积核的大小为(k,k,k);s即stride,卷积操作的步长;p即padding,填充的尺寸。

对于车辆检测而言,设计的卷积中间层如下:

Conv3D(128, 64, 3,(2,1,1), (1,1,1))
Conv3D(64, 64, 3, (1,1,1), (0,1,1))
Conv3D(64, 64, 3, (2,1,1), (1,1,1))

卷积中间层的TensorFlow代码如下:

def ConvMD(M, Cin, Cout, k, s, p, input, training=True, activation=True, bn=True, name='conv'):
    temp_p = np.array(p)
    temp_p = np.lib.pad(temp_p, (1, 1), 'constant', constant_values=(0, 0))
    with tf.variable_scope(name) as scope:
        if(M == 2):
            paddings = (np.array(temp_p)).repeat(2).reshape(4, 2)
            pad = tf.pad(input, paddings, "CONSTANT")
            temp_conv = tf.layers.conv2d(
                pad, Cout, k, strides=s, padding="valid", reuse=tf.AUTO_REUSE, name=scope)
        if(M == 3):
            paddings = (np.array(temp_p)).repeat(2).reshape(5, 2)
            pad = tf.pad(input, paddings, "CONSTANT")
            temp_conv = tf.layers.conv3d(
                pad, Cout, k, strides=s, padding="valid", reuse=tf.AUTO_REUSE, name=scope)
        if bn:
            temp_conv = tf.layers.batch_normalization(
                temp_conv, axis=-1, fused=True, training=training, reuse=tf.AUTO_REUSE, name=scope)
        if activation:
            return tf.nn.relu(temp_conv)
        else:
            return temp_conv

# convolutinal middle layers
temp_conv = ConvMD(3, 128, 64, 3, (2, 1, 1),
                    (1, 1, 1), self.input, name='conv1')
temp_conv = ConvMD(3, 64, 64, 3, (1, 1, 1),
                    (0, 1, 1), temp_conv, name='conv2')
temp_conv = ConvMD(3, 64, 64, 3, (2, 1, 1),
                    (1, 1, 1), temp_conv, name='conv3')
temp_conv = tf.transpose(temp_conv, perm=[0, 2, 3, 4, 1])
temp_conv = tf.reshape(temp_conv, [-1, cfg.INPUT_HEIGHT, cfg.INPUT_WIDTH, 128])

区域提出网络(RPN)

RPN实际上是目标检测网络中常用的一种网络,下图是VoxelNet中使用的RPN:

如图所示,该网络包含三个全卷积层块(Block),每个块的第一层通过步长为2的卷积将特征图采样为一半,之后是三个步长为1的卷积层,每个卷积层都包含BN层和ReLU操作。将每一个块的输出都上采样到一个固定的尺寸并串联构造高分辨率的特征图。最后,该特征图通过两种二维卷积被输出到期望的学习目标:

  • 概率评分图(Probability Score Map )

  • 回归图(Regression Map)

使用TensorFlow实现该RPN如下(非完整代码,完整代码请见文末链接)):

def Deconv2D(Cin, Cout, k, s, p, input, training=True, bn=True, name='deconv'):
    temp_p = np.array(p)
    temp_p = np.lib.pad(temp_p, (1, 1), 'constant', constant_values=(0, 0))
    paddings = (np.array(temp_p)).repeat(2).reshape(4, 2)
    pad = tf.pad(input, paddings, "CONSTANT")
    with tf.variable_scope(name) as scope:
        temp_conv = tf.layers.conv2d_transpose(
            pad, Cout, k, strides=s, padding="SAME", reuse=tf.AUTO_REUSE, name=scope)
        if bn:
            temp_conv = tf.layers.batch_normalization(
                temp_conv, axis=-1, fused=True, training=training, reuse=tf.AUTO_REUSE, name=scope)
        return tf.nn.relu(temp_conv)

# rpn
# block1:
temp_conv = ConvMD(2, 128, 128, 3, (2, 2), (1, 1),
                    temp_conv, training=self.training, name='conv4')
temp_conv = ConvMD(2, 128, 128, 3, (1, 1), (1, 1),
                    temp_conv, training=self.training, name='conv5')
temp_conv = ConvMD(2, 128, 128, 3, (1, 1), (1, 1),
                    temp_conv, training=self.training, name='conv6')
temp_conv = ConvMD(2, 128, 128, 3, (1, 1), (1, 1),
                    temp_conv, training=self.training, name='conv7')
deconv1 = Deconv2D(128, 256, 3, (1, 1), (0, 0),
                    temp_conv, training=self.training, name='deconv1')

# block2:
temp_conv = ConvMD(2, 128, 128, 3, (2, 2), (1, 1),
                    temp_conv, training=self.training, name='conv8')
temp_conv = ConvMD(2, 128, 128, 3, (1, 1), (1, 1),
                    temp_conv, training=self.training, name='conv9')
temp_conv = ConvMD(2, 128, 128, 3, (1, 1), (1, 1),
                    temp_conv, training=self.training, name='conv10')
temp_conv = ConvMD(2, 128, 128, 3, (1, 1), (1, 1),
                    temp_conv, training=self.training, name='conv11')
temp_conv = ConvMD(2, 128, 128, 3, (1, 1), (1, 1),
                    temp_conv, training=self.training, name='conv12')
temp_conv = ConvMD(2, 128, 128, 3, (1, 1), (1, 1),
                    temp_conv, training=self.training, name='conv13')
deconv2 = Deconv2D(128, 256, 2, (2, 2), (0, 0),
                    temp_conv, training=self.training, name='deconv2')

# block3:
temp_conv = ConvMD(2, 128, 256, 3, (2, 2), (1, 1),
                    temp_conv, training=self.training, name='conv14')
temp_conv = ConvMD(2, 256, 256, 3, (1, 1), (1, 1),
                    temp_conv, training=self.training, name='conv15')
temp_conv = ConvMD(2, 256, 256, 3, (1, 1), (1, 1),
                    temp_conv, training=self.training, name='conv16')
temp_conv = ConvMD(2, 256, 256, 3, (1, 1), (1, 1),
                    temp_conv, training=self.training, name='conv17')
temp_conv = ConvMD(2, 256, 256, 3, (1, 1), (1, 1),
                    temp_conv, training=self.training, name='conv18')
temp_conv = ConvMD(2, 256, 256, 3, (1, 1), (1, 1),
                    temp_conv, training=self.training, name='conv19')
deconv3 = Deconv2D(256, 256, 4, (4, 4), (0, 0),
                    temp_conv, training=self.training, name='deconv3')

# final:
temp_conv = tf.concat([deconv3, deconv2, deconv1], -1)
# Probability score map, scale = [None, 200/100, 176/120, 2]
p_map = ConvMD(2, 768, 2, 1, (1, 1), (0, 0), temp_conv,
                training=self.training, activation=False, bn=False, name='conv20')
# Regression(residual) map, scale = [None, 200/100, 176/120, 14]
r_map = ConvMD(2, 768, 14, 1, (1, 1), (0, 0),
                temp_conv, training=self.training, activation=False, bn=False, name='conv21')

损失函数

我们首先定义为正样本集合,为负样本集合,使用来表示一个真实的3D标注框,其中

表示标注框中心的坐标,表示标注框的长宽高,表示偏航角(Yaw)。相应的,表示正样本框。那么回归的目标为一下七个量:

其中

是正样本框的对角线。我们定义损失函数为:

其中分别表示正样本和负样本的Softmax输出,分别表示神经网络的正样本输出的标注框和真实标注框。损失函数的前两项表示对于正样本输出和负样本输出的分类损失(已经进行了正规化),其中表示交叉熵,是两个常数,它们作为权重来平衡正负样本损失对于最后的损失函数的影响。表示回归损失,这里采用的是Smooth L1函数。

ROS实践

我们仍然使用第二十六篇博客的数据(截取自KITTI),下载地址:https://pan.baidu.com/s/1kxZxrjGHDmTt-9QRMd_kOA

我们直接采用qianguih提供的训练好的模型(参考:https://github.com/qianguih/voxelnet ,大家也可以基于该项目自己训练模型)。

安装项目依赖环境:


  • python3.5+

  • TensorFlow (tested on 1.4)

  • opencv

  • shapely

  • numba

  • easydict

  • ROS

  • jsk package

准备数据

下载上面的数据集,解压到项目(源码地址见文末)的data文件夹下,目录结构为:

data
----lidar_2d
--------0000...1.npy
--------0000...2.npy
--------.......

运行

  • catkin_make

  • roscd voxelnet/script/

  • python3 voxelnet_ros.py & python3 pub_kitti_point_cloud.py

注意不能使用rosrun,因为VoxelNet代码为Python 3.x

rqt节点图

使用Rviz可视化



存在的问题

  • 实例的模型的性能不佳,由于论文作者没有开源其代码,许多参数仍然有待调整

  • 调整速度慢,没有实现作者提出的高效策略

感兴趣的同学可以试着调整参数提高模型性能,欢迎大家留言、私信交流。

源码地址:

https://github.com/AbangLZU/VoxelNetRos

数据地址:

https://pan.baidu.com/s/1kxZxrjGHDmTt-9QRMd_kOA

原文地址:

https://blog.csdn.net/AdamShan/article/details/84837211

2018 中国大数据技术大会

BDTC 2018

2018中国大数据技术大会今天在北京新云南正式拉开序幕,产学研大伽年度精彩分享,现场前沿观点频现,思维碰撞精彩迭起~~大会第一天精彩观点总结,分享给没有到会场的小伙伴们~(另外,目前大会官网已更新部分PPT,扫描海报二维码即可下载)

无人驾驶汽车系统入门:基于VoxelNet的激光雷达点云车辆检测及ROS实现相关推荐

  1. 无人驾驶汽车系统入门(二十八)——基于VoxelNet的激光雷达点云车辆检测及ROS实现

    无人驾驶汽车系统入门(二十八)--基于VoxelNet的激光雷达点云车辆检测及ROS实现 前文我们提到使用SqueezeSeg进行了三维点云的分割,由于采用的是SqueezeNet作为特征提取网络,该 ...

  2. 无人驾驶汽车系统入门——基于Frenet优化轨迹的无人车动作规划方法

    作者简介:申泽邦(Adam Shan),兰州大学在读硕士研究生,主攻无人驾驶,深度学习: 动作规划动作在无人车规划模块的最底层,它负责根据当前配置和目标配置生成一序列的动作.本文介绍一种基于Frene ...

  3. 无人驾驶汽车系统入门(二十六)——基于深度学习的实时激光雷达点云目标检测及ROS实现

    无人驾驶汽车系统入门(二十六)--基于深度学习的实时激光雷达点云目标检测及ROS实现 在前两篇文章中,我们使用PCL实现了在点云中对地面的过滤和点云的分割聚类,通常来说,在这两步以后我们将对分割出来的 ...

  4. 无人驾驶汽车系统入门(二十五)——基于欧几里德聚类的激光雷达点云分割及ROS实现

    无人驾驶汽车系统入门(二十五)--基于欧几里德聚类的激光雷达点云分割及ROS实现 上一篇文章中我们介绍了一种基于射线坡度阈值的地面分割方法,并且我们使用pcl_ros实现了一个简单的节点,在完成了点云 ...

  5. 无人驾驶汽车系统入门(六)——基于传统计算机视觉的车道线检测(1)

    无人驾驶汽车系统入门(六)--基于传统计算机视觉的车道线检测(1) 感知,作为无人驾驶汽车系统中的"眼睛",是目前无人驾驶汽车量产和商用化的最大障碍之一(技术角度), 目前,高等级 ...

  6. 无人驾驶汽车系统入门(二十二)——使用Autoware实践激光雷达与摄像机组合标定

    无人驾驶汽车系统入门(二十二)--使用Autoware实践激光雷达与摄像机组合标定 单目相机分辨率高,我们可以使用各种深度学习算法完成对目标检测,但是缺乏深度,坐标等信息.激光雷达能够获得目标相当精确 ...

  7. 无人驾驶汽车系统入门(十二)——卷积神经网络入门,基于深度学习的车辆实时检测

    无人驾驶汽车系统入门(十二)--卷积神经网络入门,基于深度学习的车辆实时检测 上篇文章我们讲到能否尽可能利用上图像的二维特征来设计神经网络,以此来进一步提高识别的精度.在这篇博客中,我们学习一类专门用 ...

  8. 无人驾驶汽车系统入门(七)——基于传统计算机视觉的车道线检测(2)

    无人驾驶汽车系统入门(七)--基于传统计算机视觉的车道线检测(2) 原创不易,转载请注明来源:http://blog.csdn.net/adamshan/article/details/7873330 ...

  9. 无人驾驶汽车系统入门(十)——基于运动学模型的模型预测控制

    无人驾驶汽车系统入门(十)--基于运动学模型的模型预测控制 在前面的第五篇博客中,我们介绍了两种常见的车辆模型--运动学自行车模型和动力学自行车模型,本节我们基于运动学车辆模型引入一种新的控制理论-- ...

最新文章

  1. 程序员的身体一定要好
  2. python读取单元格的值csv,比较单元格值的csv文件python
  3. ExtAspNet v3.1.1
  4. 获取远程服务器代码到本地文件,获取远程服务器代码到本地文件
  5. 美赛最新通知:论文接收状态已更新完毕!
  6. java中日期怎么_Java 中的日期处理
  7. mes实施顾问前景如何_国匠智能制造培训|MES的两种实施顾问类型
  8. Hyper-v 2016 VHD Set
  9. 格式化网上复制过来的源代码
  10. 漫步微积分十九——牛顿法解方程
  11. 【C++探索之旅】第二部分第一课:面向对象初探,string的惊天内幕
  12. LeetCode-11-Container With Most Water
  13. 手柄测试Debug记录
  14. 关于学习的过程:痛并快乐着是一种收获
  15. 使用接口实现打印机功能
  16. elasticsearch 数据类型
  17. 中国电信CTWing物联网平台接入指南(一)之开发流程
  18. 自学Python:快速查找文件或文件夹
  19. css旋转,附源代码
  20. 今时不同往日:VS2010十大绝技让VS6叹“.NET研究”服

热门文章

  1. java中的移位运算符
  2. 菜鸟学***——菜鸟的旅程
  3. Mysql将SQL查询结果以字符串形式返回
  4. Jvm常见面试题及答案汇总1000道(春招+秋招+社招)
  5. Jupyter Notebook学习笔记
  6. 电路设计中电阻电容电感的选择原则
  7. 用Python深入理解跳跃表原理及实现
  8. java下输出中文的一点研究
  9. 如何实现显示超过10个字符就显示省略号?
  10. 2017高级软件工程第1次作业