PointNet網絡深度學習在點雲處理上的先驅,這個團隊又提出了PointNet++模型。以下是我學習之余的總結,一是理清自己的思路,二是於無意看到這篇博文的您一起學習。
一、PointNet的問題
一般提出新的模型,總是要分析原有模型的不足,是的。
由PointNet網絡結構可以看出,網絡只是把全部點拼接在一起,提取一個全局特征,很少考慮一個點的領域結構,而領域是一個十分重要的概念。
PointNet不捕獲由度量空間點引起的局部結構,限制了它識別細粒度圖案和泛化到復雜場景的能力,簡單理解就是功能不強,實際應用效果一般。
二、PointNet++優點
1.一種分層的神經網絡,在輸入點集的嵌套分區上迭代使用PointNet。
2.利用度量空間的距離,能夠利用上下文尺度的增長學習局部特征。
3.由於不同位置采集的點雲數據的密度不一樣,能夠自適應地結合多尺度特征。
三、介紹
1.什么是分層的網絡結構?
PointNet首先把點集划分為一些重疊的局部區域(划分方法稍后介紹),類似於CNNs,從小的局部區域捕獲細粒度的局部結構來提取局部特征。之后局部特征被分組到更大的單元,迭代,已提取更高level的特征,這個過程不斷重復,直到我們獲取的整個輸入點集的特征(特征提取方法稍后介紹)。
2.如何把點集分組?
把每一個分組考慮成基礎歐幾里得空間的一個Neighborhood ball。領域球的參數就是質心的位置和尺度。采樣的算法是Farthest point sampling(FPS),最遠點采樣法優勢是可以盡可能的覆蓋空間中的所有點。,使用FPS采樣到一些中心點,然后使用K nearest neighbor(KNN)或者Ball query算法分組。
3.FPS算法:
流程很簡單,以點雲第一個點,作為查詢點,加入點集A,從剩余點中,取一個距離點集A最遠的點,一直采樣到目標數量N為止。
一個點P到到點集A距離的定義:
P點到A中距離最近的一個點的距離,\(min(dis(P,A_1),...dis(P,A_n))\)。
具體實現是存在計算優化。
- 時間復雜度:每次選一個點,需要計算 \(n\) 個距離;選 \(k\) 個點,時間復雜度可以認為是:\(nk\) ,由於 \(n\) 和 \(n\) 是常數關系,所以也可以認為是: \(n^2\) 。
- 空間復雜度:需要一個長度為 \(n\)的數組,來記錄、更新每個點的距離值,所以復雜度為: \(O(n)\)。
看一下FPS調用函數,輸入是點集,輸出是多組點。
def farthest_point_sample(npoint,inp):
'''
input:
int32
batch_size * ndataset * 3 float32
returns:
batch_size * npoint int32
'''
return sampling_module.farthest_point_sample(inp, npoint)
4.KNN和Ball query
KNN是是查找一個固定個數的領域點。Ball query是操作區域半徑范圍內的全部點(上限為K)。
5.如何確定分組尺度
一個常見的問題是,輸入點集在不同區域點的密度不同。CNN使用小的卷積核效果較好,但是PointNet++不一定,領域太小可能點的數量太少。
PointNet++利用多尺度實現模型的魯棒性,同時在訓練的時候采用dropout,網絡能夠自適應取得多尺度組合的特征。
5.如何學習局部特征?
采樣和分組都是為了特征學習,把一個領域球當作一個局部特征,使用一個小的PointNet提取特征,隨着不同level的set abstraction(下面介紹),中心點個數不斷減少,但是特征的維度越來越高。具體分類和分割網絡模型如下。
四、模型構建方法
1.PointNet
PointNet是一個全局的函數擬合。缺乏不同規模上捕捉局部上下文的能力。所以PointNet采用分層特征學習框架。
2.分層網絡的特征學習
分層網絡結構由一些set abstraction層組成,每一層包含三個關鍵layers。一個set abstraction level把\(N\times(d+C)\)矩陣為input,代表N個點,每個點d維坐標和C維特征。\(N'\times(d+C')\)矩陣為output,表示N'個子采樣的點,每個點d維坐標和C維特征。
- Sampling layer :對輸入點集采樣,選出若干中心點,定義局部區域的質心,使用FPS。
- Grouping layer : 通過查找質心周圍的“鄰近”點來構建局部區域集,使用Ball query 算法找到半徑范圍內的全部點,上限為K。
- PointNet layer :使用小型PointNet將局部區域模式編碼為特征向量,這一層的input為\(N'\)個局部區域,數據大小為\(N'\times K\times(d+C)\),output是\(N'\times(d+C')\),字母意思應該很明確了。
對照圖看一下采樣和分組的代碼,思路更加清晰:
def sample_and_group(npoint, radius, nsample, xyz, points, knn=False, use_xyz=True):
'''
Input:
npoint: int32,關鍵點個數
radius: float32
nsample: int32,一分組點的個數
xyz: (batch_size, ndataset, 3) TF tensor,ndataset表示一個size的總點數
points: (batch_size, ndataset, channel) TF tensor, if None will just use xyz as points,可以理解成每個點特征信息
knn: bool, if True use kNN instead of radius search
use_xyz: bool, if True concat XYZ with local point features, otherwise just use point features
Output:
new_xyz: (batch_size, npoint, 3) TF tensor
new_points: (batch_size, npoint, nsample, 3+channel) TF tensor
idx: (batch_size, npoint, nsample) TF tensor, indices of local points as in ndataset points
grouped_xyz: (batch_size, npoint, nsample, 3) TF tensor, normalized point XYZs,分好組的點
(subtracted by seed point XYZ) in local regions
'''
#找到中心點 (new xyz),每個group的局部特征(new points),每個group對應的下標(idx)
new_xyz = gather_point(xyz, farthest_point_sample(npoint, xyz)) # (batch_size, npoint, 3)
if knn:
_,idx = knn_point(nsample, xyz, new_xyz)
else:
idx, pts_cnt = query_ball_point(radius, nsample, xyz, new_xyz)
grouped_xyz = group_point(xyz, idx) # (batch_size, npoint, nsample, 3)
grouped_xyz -= tf.tile(tf.expand_dims(new_xyz, 2), [1,1,nsample,1]) # translation normalization
if points is not None:
# 把points按照上面分組的方法分組
grouped_points = group_point(points, idx) # (batch_size, npoint, nsample, channel)
if use_xyz:
# concat操作,也就是論文中的d+C
new_points = tf.concat([grouped_xyz, grouped_points], axis=-1) # (batch_size, npoint, nample, 3+channel)
else:
new_points = grouped_points
else:
new_points = grouped_xyz
return new_xyz, new_points, idx, grouped_xyz
下面是set abstraction的代碼,沒有加入多尺度的特征提取:
def pointnet_sa_module(xyz, points, npoint, radius, nsample, mlp, mlp2, group_all, is_training, bn_decay, scope, bn=True, pooling='max', knn=False, use_xyz=True, use_nchw=False):
''' PointNet Set Abstraction (SA) Module
Input:
xyz: (batch_size, ndataset, 3) TF tensor,輸入點集
points: (batch_size, ndataset, channel) TF tensor,輸入點集的特征
npoint: int32 -- #points sampled in farthest point sampling
radius: float32 -- search radius in local region
nsample: int32 -- how many points in each local region
mlp: list of int32 -- output size for MLP on each point
mlp2: list of int32 -- output size for MLP on each region
group_all: bool -- group all points into one PC if set true, OVERRIDE
npoint, radius and nsample settings
use_xyz: bool, if True concat XYZ with local point features, otherwise just use point features
use_nchw: bool, if True, use NCHW data format for conv2d, which is usually faster than NHWC format
Return:
new_xyz: (batch_size, npoint, 3) TF tensor
new_points: (batch_size, npoint, mlp[-1] or mlp2[-1]) TF tensor
idx: (batch_size, npoint, nsample) int32 -- indices for local regions
'''
data_format = 'NCHW' if use_nchw else 'NHWC'
with tf.variable_scope(scope) as sc:
# Sample and Grouping
# 找到中心點 (new xyz),每個group的局部特征(new points),每個group對應的下標(idx)
if group_all:
nsample = xyz.get_shape()[1].value
new_xyz, new_points, idx, grouped_xyz = sample_and_group_all(xyz, points, use_xyz)
else:
new_xyz, new_points, idx, grouped_xyz = sample_and_group(npoint, radius, nsample, xyz, points, knn, use_xyz)
# Point Feature Embedding
if use_nchw: new_points = tf.transpose(new_points, [0,3,1,2])
# pointnet 層:對 new points 提取特征的卷積層,通道數枚舉mlp
for i, num_out_channel in enumerate(mlp):
new_points = tf_util.conv2d(new_points, num_out_channel, [1,1],
padding='VALID', stride=[1,1],
bn=bn, is_training=is_training,
scope='conv%d'%(i), bn_decay=bn_decay,
data_format=data_format)
if use_nchw: new_points = tf.transpose(new_points, [0,2,3,1])
# Pooling in Local Regions
# 對每個group的feature進行pooling,得到每個中心點的local points feature,對new points進行池化
if pooling=='max':
new_points = tf.reduce_max(new_points, axis=[2], keep_dims=True, name='maxpool')
elif pooling=='avg':
new_points = tf.reduce_mean(new_points, axis=[2], keep_dims=True, name='avgpool')
elif pooling=='weighted_avg':
with tf.variable_scope('weighted_avg'):
dists = tf.norm(grouped_xyz,axis=-1,ord=2,keep_dims=True)
exp_dists = tf.exp(-dists * 5)
weights = exp_dists/tf.reduce_sum(exp_dists,axis=2,keep_dims=True) # (batch_size, npoint, nsample, 1)
new_points *= weights # (batch_size, npoint, nsample, mlp[-1])
new_points = tf.reduce_sum(new_points, axis=2, keep_dims=True)
elif pooling=='max_and_avg':
max_points = tf.reduce_max(new_points, axis=[2], keep_dims=True, name='maxpool')
avg_points = tf.reduce_mean(new_points, axis=[2], keep_dims=True, name='avgpool')
new_points = tf.concat([avg_points, max_points], axis=-1)
# [Optional] Further Processing ,考慮是否對new points進一步卷積
if mlp2 is not None:
if use_nchw: new_points = tf.transpose(new_points, [0,3,1,2])
for i, num_out_channel in enumerate(mlp2):
new_points = tf_util.conv2d(new_points, num_out_channel, [1,1],
padding='VALID', stride=[1,1],
bn=bn, is_training=is_training,
scope='conv_post_%d'%(i), bn_decay=bn_decay,
data_format=data_format)
if use_nchw: new_points = tf.transpose(new_points, [0,2,3,1])
new_points = tf.squeeze(new_points, [2]) # (batch_size, npoints, mlp2[-1])
# 得到輸出的中心點,局部區域特征和下標
return new_xyz, new_points, idx
3.在不均勻采樣下的魯班的特征學習
這里就是介紹密度自適應的特征學習,可以觀察下圖比較方法的不同。
Multi-scale grouping (MSG):
多尺度特征學習,在Grouping layer使用不同的尺度,在PointNets 中捕獲對應的尺度的特征,然后concat成一個多尺度特征。
在訓練時候使用dropout,測試的時候,全部點都使用。
Multi-resolution grouping (MRG):(more efficient)
MSG的計算成本太高。MRG:still preserves the ability to adaptively aggregate information according to the distributional properties of points。
對於不同的level中的提取的特征做一個concat。
對照上圖(b),新特征通過兩部分連接起來。左邊特征向量是通過一個set abstraction后得到的,右邊特征向量是直接對當前patch(是指數據中的一小塊)中所有點進行pointnet卷積得到。並且,當點雲密度不均時,可以通過判斷當前patch的密度對左右兩個特征向量給予不同權重。例如,當patch中密度很小,左邊向量得到的信息就沒有對所有patch中點提取的特征可信度更高,於是將右特征向量的權重提高。以此達到減少計算量的同時解決密度問題。
五、分類網絡結構
從圖中可以看出,分類網絡就是把lastest的set abstraction的特征輸出作為全連接層的第一層數據,兩層全連接之后實現40分類。下面代碼使用的是SSG,即相同尺度特征,代碼如下:
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 = {}
l0_xyz = point_cloud # (16,1024,3)
l0_points = None
end_points['l0_xyz'] = l0_xyz
# Set abstraction layers
# Note: When using NCHW for layer 2, we see increased GPU memory usage (in TF1.4).
# So we only use NCHW for layer 1 until this issue can be resolved.
l1_xyz, l1_points, l1_indices = pointnet_sa_module(l0_xyz, l0_points, npoint=512, radius=0.2, nsample=32, mlp=[64,64,128], mlp2=None, group_all=False, is_training=is_training, bn_decay=bn_decay, scope='layer1', use_nchw=True)
# l1_xyz = (16, 512, 3) 中心點
# l1_points = (16, 512, 128) local region feature
# l1_indices = (16, 512, 32) 512 center points(group), each group has 32 points
l2_xyz, l2_points, l2_indices = pointnet_sa_module(l1_xyz, l1_points, npoint=128, radius=0.4, nsample=64, mlp=[128,128,256], mlp2=None, group_all=False, is_training=is_training, bn_decay=bn_decay, scope='layer2')
# l2_xyz = (16, 128, 3)
# l2_points = (16, 128, 256) local feature
# l2_indices = (16, 128, 64)
l3_xyz, l3_points, l3_indices = pointnet_sa_module(l2_xyz, l2_points, npoint=None, radius=None, nsample=None, mlp=[256,512,1024], mlp2=None, group_all=True, is_training=is_training, bn_decay=bn_decay, scope='layer3')
# l3_xyz = (16, 1, 3)
# l3_points = (16, 1, 1024) global feature
# l3_indices = (16, 1, 128)
# Fully connected layers
# l3_points就是第三次sa的局部區域特征向量,d+C的那個
net = tf.reshape(l3_points, [batch_size, -1])
# 特征向量使用全連接1024-512
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.5, is_training=is_training, scope='dp1')
# 512-256
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.5, is_training=is_training, scope='dp2')
# 40分類
net = tf_util.fully_connected(net, 40, activation_fn=None, scope='fc3')
return net, end_points
六、分割網絡結構
在set abstraction layer中,原始點是被子采樣的。在分割任務中(如語義點標記),我們希望獲得全部點的特征。
分割網絡復雜一點,使用了skip link concatenation。
這部分先skip了。
七、說明
代碼中提到數據的存儲格式。
N代表數量, C代表channel,H代表高度,W代表寬度。NCHW其實代表的是[W H C N]。