轉(zhuǎn)載請注明出處
如果學習深度學習在點云處理上的應(yīng)用粉洼,那PointNet一定是你躲不開的一個模型颖榜。這個模型由斯坦福大學的Charles R. Qi等人在PointNet:Deep Learning on Point Sets for 3D Classification and Segmentation一文中提出。下面我將結(jié)合手中的一些資料談一談我對這篇文章的理解拍摇。
簡介
深度學習已經(jīng)成為了計算機視覺領(lǐng)域的一大強有力的工具亮钦,尤其在圖像領(lǐng)域,基于卷積神經(jīng)網(wǎng)絡(luò)的深度學習方法已經(jīng)攻占了絕大多數(shù)問題的高點充活。然而針對無序點云數(shù)據(jù)的深度學習方法研究則進展相對緩慢蜂莉。這主要是因為點云具有三個特征:無序性、稀疏性混卵、信息量有限映穗。
以往學者用深度學習方法在處理點云時,往往將其轉(zhuǎn)換為特定視角下的深度圖像或者體素(Voxel)等更為規(guī)整的格式以便于定義權(quán)重共享的卷積操作等幕随。
PointNet則允許我們直接輸入點云進行處理蚁滋。
輸入輸出
輸入為三通道點云數(shù)據(jù),也可以有額外的通道比如顏色赘淮、法向量等辕录,輸出整體的類別/每個點所處的部分/每個點的類別。對于目標分類任務(wù)梢卸,輸出為個分數(shù)踏拜,分別對應(yīng)個可能的類別。對于語義分割任務(wù)低剔,輸出個分數(shù)速梗,分別對應(yīng)個點相對于各類別的分數(shù)。
點云特征
PointNet網(wǎng)絡(luò)結(jié)構(gòu)的靈感來自于歐式空間里的點云的特點襟齿。對于一個歐式空間里的點云姻锁,有三個主要特征:
無序性:雖然輸入的點云是有順序的,但是顯然這個順序不應(yīng)當影響結(jié)果猜欺。
點之間的交互:每個點不是獨立的位隶,而是與其周圍的一些點共同蘊含了一些信息,因而模型應(yīng)當能夠抓住局部的結(jié)構(gòu)和局部之間的交互开皿。
變換不變性:比如點云整體的旋轉(zhuǎn)和平移不應(yīng)該影響它的分類或者分割
網(wǎng)絡(luò)結(jié)構(gòu)
如圖所示涧黄,分類網(wǎng)絡(luò)對于輸入的點云進行輸入變換(input transform)和特征變換(feature transform)篮昧,隨后通過最大池化將特征整合在一起。分割網(wǎng)絡(luò)則是分類網(wǎng)絡(luò)的延伸笋妥,其將整體和局部特征連接在一起出入每個點的分數(shù)懊昨。圖片中"mpl"代表"multi-layer perceptron"(多層感知機)。
其中春宣,mlp是通過共享權(quán)重的卷積實現(xiàn)的酵颁,第一層卷積核大小是1x3(因為每個點的維度是xyz),之后的每一層卷積核大小都是1x1月帝。即特征提取層只是把每個點連接起來而已躏惋。經(jīng)過兩個空間變換網(wǎng)絡(luò)和兩個mlp之后盯质,對每一個點提取1024維特征跟畅,經(jīng)過maxpool變成1x1024的全局特征。再經(jīng)過一個mlp(代碼中運用全連接)得到k個score喇嘱。分類網(wǎng)絡(luò)最后接的loss是softmax簸搞。
網(wǎng)絡(luò)特點
針對無序輸入的對稱函數(shù)
為了讓模型具有輸入排列不變性(結(jié)果不受輸入排列順序的影響)扁位,一種思路是利用所有可能的排列順序訓練一個RNN。作者在這里采用的思路是使用一個對稱函數(shù)攘乒,將個向量變?yōu)橐粋€新的、與輸入順序無關(guān)的向量惋鹅。(例如则酝,和是能處理兩個輸入的對稱函數(shù))。
將點云排序是一個可能的對稱函數(shù)闰集,不過作者在這里采用一個微型網(wǎng)絡(luò)(T-Net)學習一個獲得變換矩陣的函數(shù)沽讹,并對初始點云應(yīng)用這個變換矩陣,這一部分被稱為輸入變換武鲁。隨后通過一個mlp多層感知機后爽雄,再應(yīng)用一次變換矩陣(特征變換)和多層感知機,最后進行一次最大池化沐鼠。
作者認為以上這個階段學習到的變換函數(shù)是如下圖所表示的函數(shù)和挚瘟,保證了模型對特定空間轉(zhuǎn)換的不變性(注意到深度學習實際上是對復雜函數(shù)的擬合)。
個人的理解是其中作為一個對稱函數(shù)饲梭,是由最大池化實現(xiàn)的(注意到映射是n-對稱的)乘盖;而是mlp結(jié)構(gòu),代表了一個復雜函數(shù)(在圖中是將一個3維向量映射成1024維向量的函數(shù))憔涉。
(這里變換矩陣的學習過程個人認為有一些玄學订框,我自己并不能很好地理解其如何獲得旋轉(zhuǎn)不變性。不過深度學習領(lǐng)域有很多無法解釋的東西兜叨。感興趣可以參考一下文末的源碼)
整合局部和全局信息
對于點云分割任務(wù)穿扳,我們需要將局部很全局信息結(jié)合起來衩侥。
這里,作者將經(jīng)過特征變換后的信息稱作局部信息矛物,它們是與每一個點緊密相關(guān)的茫死;我們將局部信息和全局信息簡單地連接起來,就得到用于分割的全部信息泽谨。
理論分析
除了模型的介紹璧榄,作者還引入了兩個相關(guān)的定理:
定理1證明了PointNet的網(wǎng)絡(luò)結(jié)構(gòu)能夠擬合任意的連續(xù)集合函數(shù)。
定理2(a)說明對于任何輸入數(shù)據(jù)集吧雹,都存在一個關(guān)鍵集和一個最大集骨杂,使得對和之間的任何集合,其網(wǎng)絡(luò)輸出都和一樣雄卷。這也就是說搓蚪,模型對輸入數(shù)據(jù)在有噪聲和有數(shù)據(jù)損壞的情況都是魯棒的。定理2(b)說明了關(guān)鍵集的數(shù)據(jù)多少由maxpooling操作輸出數(shù)據(jù)的維度K給出上界(框架圖中為1024)丁鹉。個角度來講妒潭,PointNet能夠總結(jié)出表示某類物體形狀的關(guān)鍵點,基于這些關(guān)鍵點PointNet能夠判別物體的類別揣钦。這樣的能力決定了PointNet對噪聲和數(shù)據(jù)缺失的魯棒性雳灾。[引自美團知乎專欄]
下圖給出了一些關(guān)鍵集和最大集的樣例:
后記
我們知道冯凹,激光雷達所采集到的數(shù)據(jù)是3D點云谎亩。點云的處理應(yīng)用也越來越廣泛,比較常見的應(yīng)用場景是自動駕駛和工業(yè)機器人宇姚。
盡管激光雷達(Lidar)的成本依然很高匈庭,但考慮到它具有更高的距離測量精度,越來越多的無人駕駛公司開始研究基于激光雷達的無人駕駛方案浑劳。據(jù)筆者了解阱持,Momenta、Nullmax于近期(2018年)開始組建激光雷達團隊魔熏,而Pony衷咽、阿里巴巴AI lab、美團蒜绽、Waymo等則一直研發(fā)集成激光雷達的無人駕駛方案兵罢。盡管Tesla的馬斯克曾經(jīng)對Lidar大為嘲諷,筆者在與VisLab負責人Alberto Broggi的交流時對方也表示Lidar與圖像互補性不是很高(比如他們都會在強光或者雨霧中失效)滓窍。我的感覺是卖词,純粹基于圖像的自動駕駛感知方案還不能達到技術(shù)落地的要求,或者無法提供足夠的安全冗余。因而了解一下點云的處理技術(shù)對于自動駕駛從業(yè)者還是很有幫助的此蜈,畢竟隨著更多資本的涌入即横,激光雷達的成本也會有所降低。
此外我也了解到有一些利用激光雷達進行目標識別和定位的機械臂裆赵,應(yīng)該也是一個比較火的方向东囚。
參考:
Momenta高級研究員陳亮論文解讀
美團無人配送的知乎專欄:PointNet系列論文解讀
hitrjj的CSDN博客:三維點云網(wǎng)絡(luò)——PointNet論文解讀
github源碼
痛并快樂著呦西的CSDN博客:三維深度學習之pointnet系列詳解
作者的其他相關(guān)文章:
圖像分割:全卷積神經(jīng)網(wǎng)絡(luò)(FCN)詳解
基于視覺的機器人室內(nèi)定位
目標檢測:YOLO和SSD 簡介
論文閱讀:InLoc:基于稠密匹配和視野合成的室內(nèi)定位
論文閱讀:StreetMap-基于向下攝像頭的視覺建圖與定位方案
模型源碼(部分)
輸入變換
def input_transform_net(point_cloud, is_training, bn_decay=None, K=3):
""" Input (XYZ) Transform Net, input is BxNx3 gray image
Return:
Transformation matrix of size 3xK """
batch_size = point_cloud.get_shape()[0].value
num_point = point_cloud.get_shape()[1].value
input_image = tf.expand_dims(point_cloud, -1)
net = tf_util.conv2d(input_image, 64, [1,3],
padding='VALID', stride=[1,1],
bn=True, is_training=is_training,
scope='tconv1', bn_decay=bn_decay)
net = tf_util.conv2d(net, 128, [1,1],
padding='VALID', stride=[1,1],
bn=True, is_training=is_training,
scope='tconv2', bn_decay=bn_decay)
net = tf_util.conv2d(net, 1024, [1,1],
padding='VALID', stride=[1,1],
bn=True, is_training=is_training,
scope='tconv3', bn_decay=bn_decay)
net = tf_util.max_pool2d(net, [num_point,1],
padding='VALID', scope='tmaxpool')
net = tf.reshape(net, [batch_size, -1])
net = tf_util.fully_connected(net, 512, bn=True, is_training=is_training,
scope='tfc1', bn_decay=bn_decay)
net = tf_util.fully_connected(net, 256, bn=True, is_training=is_training,
scope='tfc2', bn_decay=bn_decay)
with tf.variable_scope('transform_XYZ') as sc:
assert(K==3)
weights = tf.get_variable('weights', [256, 3*K],
initializer=tf.constant_initializer(0.0),
dtype=tf.float32)
biases = tf.get_variable('biases', [3*K],
initializer=tf.constant_initializer(0.0),
dtype=tf.float32)
biases += tf.constant([1,0,0,0,1,0,0,0,1], dtype=tf.float32)
transform = tf.matmul(net, weights)
transform = tf.nn.bias_add(transform, biases)
transform = tf.reshape(transform, [batch_size, 3, K])
return transform
主體部分
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.append(BASE_DIR)
sys.path.append(os.path.join(BASE_DIR, '../utils'))
import tf_util
from transform_nets import input_transform_net, feature_transform_net
def placeholder_inputs(batch_size, num_point):
pointclouds_pl = tf.placeholder(tf.float32, shape=(batch_size, num_point, 3))
labels_pl = tf.placeholder(tf.int32, shape=(batch_size))
return pointclouds_pl, labels_pl
def get_model(point_cloud, is_training, bn_decay=None):
""" Classification PointNet, input is BxNx3, output Bx40 """
batch_size = point_cloud.get_shape()[0].value
num_point = point_cloud.get_shape()[1].value
end_points = {}
with tf.variable_scope('transform_net1') as sc:
transform = input_transform_net(point_cloud, is_training, bn_decay, K=3)
point_cloud_transformed = tf.matmul(point_cloud, transform)
input_image = tf.expand_dims(point_cloud_transformed, -1)
net = tf_util.conv2d(input_image, 64, [1,3],
padding='VALID', stride=[1,1],
bn=True, is_training=is_training,
scope='conv1', bn_decay=bn_decay)
net = tf_util.conv2d(net, 64, [1,1],
padding='VALID', stride=[1,1],
bn=True, is_training=is_training,
scope='conv2', bn_decay=bn_decay)
with tf.variable_scope('transform_net2') as sc:
transform = feature_transform_net(net, is_training, bn_decay, K=64)
end_points['transform'] = transform
net_transformed = tf.matmul(tf.squeeze(net, axis=[2]), transform)
net_transformed = tf.expand_dims(net_transformed, [2])
net = tf_util.conv2d(net_transformed, 64, [1,1],
padding='VALID', stride=[1,1],
bn=True, is_training=is_training,
scope='conv3', bn_decay=bn_decay)
net = tf_util.conv2d(net, 128, [1,1],
padding='VALID', stride=[1,1],
bn=True, is_training=is_training,
scope='conv4', bn_decay=bn_decay)
net = tf_util.conv2d(net, 1024, [1,1],
padding='VALID', stride=[1,1],
bn=True, is_training=is_training,
scope='conv5', bn_decay=bn_decay)
# Symmetric function: max pooling
net = tf_util.max_pool2d(net, [num_point,1],
padding='VALID', scope='maxpool')
net = tf.reshape(net, [batch_size, -1])
net = tf_util.fully_connected(net, 512, bn=True, is_training=is_training,
scope='fc1', bn_decay=bn_decay)
net = tf_util.dropout(net, keep_prob=0.7, is_training=is_training,
scope='dp1')
net = tf_util.fully_connected(net, 256, bn=True, is_training=is_training,
scope='fc2', bn_decay=bn_decay)
net = tf_util.dropout(net, keep_prob=0.7, is_training=is_training,
scope='dp2')
net = tf_util.fully_connected(net, 40, activation_fn=None, scope='fc3')
return net, end_points
def get_loss(pred, label, end_points, reg_weight=0.001):
""" pred: B*NUM_CLASSES,
label: B, """
loss = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=pred, labels=label)
classify_loss = tf.reduce_mean(loss)
tf.summary.scalar('classify loss', classify_loss)
# Enforce the transformation as orthogonal matrix
transform = end_points['transform'] # BxKxK
K = transform.get_shape()[1].value
mat_diff = tf.matmul(transform, tf.transpose(transform, perm=[0,2,1]))
mat_diff -= tf.constant(np.eye(K), dtype=tf.float32)
mat_diff_loss = tf.nn.l2_loss(mat_diff)
tf.summary.scalar('mat loss', mat_diff_loss)
return classify_loss + mat_diff_loss * reg_weight
if __name__=='__main__':
with tf.Graph().as_default():
inputs = tf.zeros((32,1024,3))
outputs = get_model(inputs, tf.constant(True))
print(outputs)