蘭州大學在讀碩士研究生,主要研究方向無人駕駛,深度學習;蘭大未來計算研究院無人車團隊負責人,自動駕駛全棧工程師。
之前我們提到使用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實現代碼如下:
classVFELayer(object):def__init__(self,out_channels,name):super(VFELayer,self).__init__()self.units=int(out_channels/2)withtf.variable_scope(name,reuse=tf.AUTO_REUSE)asscope: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)defapply(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))returnconcatenatedclassFeatureNet(object):def__init__(self,training,batch_size,name=''):super(FeatureNet,self).__init__()self.training=training#scalarself.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],eachrowstores(batch,d,h,w)self.coordinate=tf.placeholder(tf.int64,[None,4],name='coordinate')withtf.variable_scope(name,reuse=tf.AUTO_REUSE)asscope:self.vfe1=VFELayer(32,'VFE-1')self.vfe2=VFELayer(128,'VFE-2')#booleanmask[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代碼如下:
defConvMD(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))withtf.variable_scope(name)asscope: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)ifbn:temp_conv=tf.layers.batch_normalization(temp_conv,axis=-1,fused=True,training=training,reuse=tf.AUTO_REUSE,name=scope)ifactivation:returntf.nn.relu(temp_conv)else:returntemp_conv#convolutinalmiddlelayerstemp_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如下(非完整代碼,完整代碼請見文末鏈接)):
defDeconv2D(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")withtf.variable_scope(name)asscope:temp_conv=tf.layers.conv2d_transpose(pad,Cout,k,strides=s,padding="SAME",reuse=tf.AUTO_REUSE,name=scope)ifbn:temp_conv=tf.layers.batch_normalization(temp_conv,axis=-1,fused=True,training=training,reuse=tf.AUTO_REUSE,name=scope)returntf.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)#Probabilityscoremap,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可視化
存在的問題
實例的模型的性能不佳,由于論文作者沒有開源其代碼,許多參數仍然有待調整
調整速度慢,沒有實現作者提出的高效策略
-
數據集
+關注
關注
4文章
1218瀏覽量
25157 -
深度學習
+關注
關注
73文章
5540瀏覽量
122200
原文標題:無人駕駛汽車系統入門:基于VoxelNet的激光雷達點云車輛檢測及ROS實現
文章出處:【微信號:rgznai100,微信公眾號:rgznai100】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
深入分析深度學習三維重建的網絡架構和訓練技巧

評論