PointNet論文理解和代碼分析(詳解)


簡介

3D展示有以下幾種常見情況:

  1. multi-view images(多視角的圖片)+2D CNN:圖片表示3D數據存在失真。
  2. vulmetric data(3D體素)+3D CNN:voxel的分辨率太小,不同物體區別不大,分辨率太高,復雜度太高。
  3. mesh data+GNN:圖卷積神經網絡處理網格數據。
  4. image depth+ CNN:帶有深度的圖片信息
  5. point cloud +DL:點雲數據運用深度學習的方法
    在這里插入圖片描述

點雲數據優勢:

  1. 原始數據
  2. 包含\((x,y,z),RGB,normal\)等信息
  3. 比較好描述3D形狀

論文的Abstract中介紹到,點雲是一種重要的幾何數據結構。不同於以往的研究,作者設計了一種新型直接處理點雲的神經網絡結構PointNet。PointNet能提供統一的結構在分類,語義分割等應用上。

介紹

輸入為三通道點雲數據,\((x,y,z)\),輸出整體的類別或者每個點所處的部分或者每個點的類別。對於目標分類任務,輸出為\(k\)個分數,實現\(k\)分類。對於語義分割任務,輸出\(n*m\)個分數,分別對應\(n\)個點相對於\(m\)類別的分數。

點雲特征:

無序性:雖然輸入的點雲是有順序的,但是顯然這個順序不應當影響結果。所以PointNet使用了對稱函數,類型\(max(x_1, x_2 ....x_n)函數\),不管怎么變化\(x_i\)的位置,函數結果都是不變的。
點之間的交互:每個點不是獨立的,而是與其周圍的一些點共同蘊含了一些信息,Pointnet分類網絡並沒有考慮很多點的周圍信息,當然卷積本來就是操作鄰域的,分類網絡提取的是一組全局feature,語義分割考慮了每個點的特征。
變換不變性:比如點雲整體的旋轉和平移不應該影響它的分類或者分割結果,對於這個特征,PointNet首先對輸入點集做了一個輸入變換,變化到一個規范空間。mlp之后又做了一個特征變換。

空間變換網絡--spatial transform network

CNN分類時,通常需要考慮輸入樣本的局部性、平移不變性、縮小不變性,旋轉不變性等,以提高分類的准確度。這些不變性的本質就是圖像處理的經典方法,即圖像的裁剪、平移、縮放、旋轉,而這些方法實際上就是對圖像進行空間坐標變換,我們所熟悉的一種空間變換就是仿射變換,就類似計算機圖形學中學習到的三維坐標變換,使用矩陣乘法。
空間變換網絡,實際上是在神經網絡的某兩層之間引入一個空間變換網絡,這個網絡的參數也是需要學習得,該空間變換網絡包括兩個部分。
第一部分為為”localization net”,網絡中的參數則為空間變換網絡需要訓練的參數;第二部分就是空間變換即仿射變換,“Grid generator”。
可以結合下圖理解,我們的目標是把原始圖片(Sampling Grid)中的點轉化到規范空間(Regular Grid)中,使用反向傳播更新參數,即”localization net”,得到參數,一個矩陣乘法即可變化原始坐標點。
在這里插入圖片描述

網絡結構

在這里插入圖片描述

分類網絡:

可以看到網絡輸入是\(n\times3\),首先經過一個input transform網絡,網絡具體細節如下,其實可以看出一個輸入變化網絡結構和分類網絡的卷積層和全連接層結構高度類似:
T-Net模型,卷積:64--128--1024 全連接:1024--512--256--3*K。代碼注釋十分清晰了。

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 """
    # K表示數據的維數,所以這里是3
    batch_size = point_cloud.get_shape()[0].value
    num_point = point_cloud.get_shape()[1].value

    input_image = tf.expand_dims(point_cloud, -1)  # 轉為4D張量,-1表示在最后加入一維,比如shape[2,3]->shape[2,3,1]
    # 構建T-Net模型,64--128--1024
    # tf_util.conv2d是對數據做卷積,使用[1,3]的模板卷積,就變成了num_point*1*64。生成64個通道
    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)
    # 每一個卷積核的大小為 [kernel_h, kernel_w,num_in_channels, num_output_channels]
    # 使用[1,1]的模板卷積。生成128個通道
    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)
    # 使用[1,1]的模板卷積。生成1024個通道
    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)
    # 最大池化,模板大小[num_point,1],也就是一組點的一個通道僅保留一個feature,實現對稱。
    net = tf_util.max_pool2d(net, [num_point,1],
                             padding='VALID', scope='tmaxpool')
    # 注意上面模板都是對一組batch做的卷積,下面這句話應該就是參數拉直,之前大小應該是[batch_size,1,1024]做下面的fc層
    net = tf.reshape(net, [batch_size, -1])
    # net的大小應該是[bacth_size,1024],即每組點只保留1024個feature
    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)
    # 兩個fc,最后輸出256維特征
    # 生成點雲旋轉矩陣 T=3*3
    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)
        # [batch_size,256]*[256,3*k]->[bacth_size,3*k]變化矩陣
        transform = tf.matmul(net, weights)
        # 加上bias
        transform = tf.nn.bias_add(transform, biases)

    transform = tf.reshape(transform, [batch_size, 3, K])
    return transform

feature transform 網絡結構和input transform基本一樣,就是變化矩陣式KK,PointNet網絡結構就是6464,因為待變化矩陣大小是\(n*64\)

def feature_transform_net(inputs, is_training, bn_decay=None, K=64):
    """ Feature Transform Net, input is BxNx1xK
        Return:
            Transformation matrix of size KxK """
    batch_size = inputs.get_shape()[0].value
    num_point = inputs.get_shape()[1].value

    net = tf_util.conv2d(inputs, 64, [1,1],
                         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_feat') as sc:
        weights = tf.get_variable('weights', [256, K*K],
                                  initializer=tf.constant_initializer(0.0),
                                  dtype=tf.float32)
        biases = tf.get_variable('biases', [K*K],
                                 initializer=tf.constant_initializer(0.0),
                                 dtype=tf.float32)
        biases += tf.constant(np.eye(K).flatten(), dtype=tf.float32)
        transform = tf.matmul(net, weights)
        transform = tf.nn.bias_add(transform, biases)

    transform = tf.reshape(transform, [batch_size, K, K])
    return transform

分析完兩個transform網絡之后,就十分清晰了,我們看一下分類網絡的代碼:

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)
    # 轉為4D張量
    input_image = tf.expand_dims(point_cloud_transformed, -1)
    # 卷積核[1,3],輸出通道64
    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)
    # 卷積核[1,1],輸出通道64
    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])
    # 卷積層,[1,1]卷積核,輸出通道64
    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)
    #   卷積層,[1,1]卷積核,輸出通道128
    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)
    #   卷積層,[1,1]卷積核,輸出通道1024
    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])
    # fc
    net = tf_util.fully_connected(net, 512, bn=True, is_training=is_training,
                                  scope='fc1', bn_decay=bn_decay)
    # dropout,防止過擬合
    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')
    # 40,應該是實現40分類
    net = tf_util.fully_connected(net, 40, activation_fn=None, scope='fc3')
    # return 的是分類網絡的結果和n*64的原始特征,可以用分割網絡
    return net, end_points

文章提到的參數共享的mlp就是卷積運算。以上就是分類網絡模型的建立,網絡結構還是十分簡潔的。
分割網絡結構:
整合局部和全局信息,對於點雲分割任務,我們需要將局部和全局信息結合起來。
這里,PointNet將經過特征變換后的信息稱作局部信息,它們是與每一個點緊密相關的;我們將局部信息和全局信息簡單地連接起來,就得到用於分割的全部信息。

def get_model(point_cloud, is_training, bn_decay=None):
    """ Classification PointNet, input is BxNx3, output BxNx50 """
    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)
    point_feat = tf.expand_dims(net_transformed, [2])
    print(point_feat)

    net = tf_util.conv2d(point_feat, 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)
    global_feat = tf_util.max_pool2d(net, [num_point,1],
                                     padding='VALID', scope='maxpool')
    # 得到1024的全局特征
    print(global_feat)
    # expand到點的特征上,構成拓展的全局特征
    global_feat_expand = tf.tile(global_feat, [1, num_point, 1, 1])
    concat_feat = tf.concat(3, [point_feat, global_feat_expand])
    print(concat_feat)
    # 卷積層
    net = tf_util.conv2d(concat_feat, 512, [1,1],
                         padding='VALID', stride=[1,1],
                         bn=True, is_training=is_training,
                         scope='conv6', bn_decay=bn_decay)
    net = tf_util.conv2d(net, 256, [1,1],
                         padding='VALID', stride=[1,1],
                         bn=True, is_training=is_training,
                         scope='conv7', bn_decay=bn_decay)
    net = tf_util.conv2d(net, 128, [1,1],
                         padding='VALID', stride=[1,1],
                         bn=True, is_training=is_training,
                         scope='conv8', bn_decay=bn_decay)
    net = tf_util.conv2d(net, 128, [1,1],
                         padding='VALID', stride=[1,1],
                         bn=True, is_training=is_training,
                         scope='conv9', bn_decay=bn_decay)
    # 50分類,就是網絡圖中的m
    net = tf_util.conv2d(net, 50, [1,1],
                         padding='VALID', stride=[1,1], activation_fn=None,
                         scope='conv10')
    net = tf.squeeze(net, [2]) # BxNxC

    return net, end_points

以上就是PointNet網絡結構,如果只想了解PointNet網絡結構,看到這里已經差不多了。我們已經了解了為什么需要變化網絡,為什么需要對稱函數。
我們再看論文中的如下表示,就能理解了,\(x\)理解為每一個點,\(h(x)\)理解為提取這點的k維特征,\(g(h(x_1),....,h(x_n))\)理解成一個從\(n*k\)空間中映射到實數域的對稱函數。
在這里插入圖片描述

魯班性分析

說明對於任何輸入數據集,都存在一個關鍵集和一個最大集,使得對和之間的任何集合,其網絡輸出都和一樣。這也就是說,模型對輸入數據在有噪聲和有數據損壞的情況都是魯棒的。
關鍵集和最大集的樣例:
在這里插入圖片描述
參考:
PointNet:基於深度學習的3D點雲分類和分割模型
空間變換網絡--spatial transform network


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM