PointNet:基于深度學習的3D點云分類和分割模型 詳解

轉(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ù)(x_i,y_i,z_i),也可以有額外的通道比如顏色赘淮、法向量等辕录,輸出整體的類別/每個點所處的部分/每個點的類別。對于目標分類任務(wù)梢卸,輸出為k個分數(shù)踏拜,分別對應(yīng)k個可能的類別。對于語義分割任務(wù)低剔,輸出n\times m個分數(shù)速梗,分別對應(yīng)n個點相對于m各類別的分數(shù)。

PointNet應(yīng)用場景

點云特征

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ò)結(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ù)攘乒,將n個向量變?yōu)橐粋€新的、與輸入順序無關(guān)的向量惋鹅。(例如则酝,+\times是能處理兩個輸入的對稱函數(shù))。
將點云排序是一個可能的對稱函數(shù)闰集,不過作者在這里采用一個微型網(wǎng)絡(luò)(T-Net)學習一個獲得3\times 3變換矩陣的函數(shù)沽讹,并對初始點云應(yīng)用這個變換矩陣,這一部分被稱為輸入變換武鲁。隨后通過一個mlp多層感知機后爽雄,再應(yīng)用一次變換矩陣(特征變換)和多層感知機,最后進行一次最大池化沐鼠。
作者認為以上這個階段學習到的變換函數(shù)是如下圖所表示的函數(shù)gh挚瘟,保證了模型對特定空間轉(zhuǎn)換的不變性(注意到深度學習實際上是對復雜函數(shù)的擬合)。
個人的理解是其中g作為一個對稱函數(shù)饲梭,是由最大池化實現(xiàn)的(注意到映射(x_1,x_2,...,x_n)\rightarrow ||(x_1,x_2,...,x_n)||_\infty是n-對稱的)乘盖;而h是mlp結(jié)構(gòu),代表了一個復雜函數(shù)(在圖中是將一個3維向量映射成1024維向量的函數(shù))憔涉。
(這里變換矩陣的學習過程個人認為有一些玄學订框,我自己并不能很好地理解其如何獲得旋轉(zhuǎn)不變性。不過深度學習領(lǐng)域有很多無法解釋的東西兜叨。感興趣可以參考一下文末的源碼)

學習到的對稱函數(shù)

整合局部和全局信息
對于點云分割任務(wù)穿扳,我們需要將局部很全局信息結(jié)合起來衩侥。
這里,作者將經(jīng)過特征變換后的信息稱作局部信息矛物,它們是與每一個點緊密相關(guān)的茫死;我們將局部信息和全局信息簡單地連接起來,就得到用于分割的全部信息泽谨。

理論分析

除了模型的介紹璧榄,作者還引入了兩個相關(guān)的定理:

定理1

定理1證明了PointNet的網(wǎng)絡(luò)結(jié)構(gòu)能夠擬合任意的連續(xù)集合函數(shù)。
定理2

定理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ù)、關(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)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市战授,隨后出現(xiàn)的幾起案子页藻,更是在濱河造成了極大的恐慌,老刑警劉巖植兰,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件份帐,死亡現(xiàn)場離奇詭異,居然都是意外死亡楣导,警方通過查閱死者的電腦和手機废境,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來筒繁,“玉大人噩凹,你說我怎么就攤上這事≌庇剑” “怎么了驮宴?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長呕缭。 經(jīng)常有香客問我堵泽,道長,這世上最難降的妖魔是什么臊旭? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任落恼,我火速辦了婚禮箩退,結(jié)果婚禮上离熏,老公的妹妹穿的比我還像新娘。我一直安慰自己戴涝,他們只是感情好滋戳,可當我...
    茶點故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著啥刻,像睡著了一般奸鸯。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上可帽,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天娄涩,我揣著相機與錄音,去河邊找鬼。 笑死蓄拣,一個胖子當著我的面吹牛扬虚,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播球恤,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼辜昵,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了咽斧?” 一聲冷哼從身側(cè)響起堪置,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎张惹,沒想到半個月后舀锨,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡诵叁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年雁竞,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拧额。...
    茶點故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡碑诉,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出侥锦,到底是詐尸還是另有隱情进栽,我是刑警寧澤,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布恭垦,位于F島的核電站快毛,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏番挺。R本人自食惡果不足惜唠帝,卻給世界環(huán)境...
    茶點故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望玄柏。 院中可真熱鬧襟衰,春花似錦、人聲如沸粪摘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽徘意。三九已至苔悦,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間椎咧,已是汗流浹背玖详。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蟋座。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓劳澄,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蜈七。 傳聞我的和親對象是個殘疾皇子秒拔,可洞房花燭夜當晚...
    茶點故事閱讀 43,486評論 2 348