Relation-Shape Convolutional Neural Network for Point Cloud Analysis 阅读笔记
论文:https://arxiv.org/abs/1904.07601 CVPR2019 oral
Pytorch代码:https://github.com/Yochengliu/Relation-Shape-CNN
作者说8月会上传,因此自己复现了一下(由于自己实现的结果一般就不开源了)
一、论文背景
近年来,3D 点云分析在自动驾驶和机器人等领域有着诸多的应用,因此得到了各界广泛的关注。主流的点云分析任务都需要对点云的 3D 形状进行高级别的理解,比如点云识别。但从不规则分布的 3D 点中很难推理学习其隐含的 3D 形状。随着以卷积神经网络(CNN)为代表的深度学习方法的兴起,大量的研究工作致力于将 CNN 在图像分析上的巨大成功复制到点云处理领域。然而由于点云数据的不规则性,经典的图像网格卷积难以适用。
一般来说,在点云上进行卷积学习主要面临三大挑战:
-
点云由无序的点集构成,因此卷积需要对点的输入顺序具有置换不变性。
-
点云分布于 3D 几何空间,因此卷积应当对点云的刚体变换具有鲁棒性,比如平移、旋转等。
-
点云形成一个隐含的形状,因此卷积得到的表征应当具备有区分力的。
上面的第一个挑战可以通过pointnet提出的对称函数去解决,而后面两个挑战基本上就是近些年来大家不断探讨和改进的地方了。本文提出 Relation-Shape CNN(RS-CNN)来缓解上述挑战。RS-CNN 的核心是从几何关系中推理学习 3D 形状,因为在本文看来,3D 点之间的几何关系能够有表现力的编码其隐含的形状信息。所提出的 RS-CNN 在点云分类、部件分割和法向估计三个任务上均达到了最佳水平。并且得益于对几何关系的建模,RS-CNN 能够自然地实现置换不变性以及刚体变换鲁棒性。为了验证 RS-CNN 的几何形状推理能力,本文还在 3D 点云的 2D 投影空间中做了测试,实验表明,RS-CNN 依然具备优秀的形状识别性能。
二、模型设计
2.1 模型创新
本文主要是设计了一种在点云上面可以运用的卷积方式,并且刷新了点云分类、部件分割和法向量估计等很多任务的成绩。
2.2 模型表达
这篇文章认为对于一个局部的点云子集,可以构建一个如下的j卷积操作:
其中
T
T
T为转换函数,使得每个点上的特征都进行了改变,
A
A
A为聚集函数,使得所有的特征聚集成一个点的特征,而
σ
\sigma
σ为激活函数。
N
(
x
i
)
N(x_i)
N(xi)为点
x
i
x_i
xi的邻域的点的集合。
我们可以先来对比一下传统图像上的2DCNN:
假设卷积核是
3
×
3
3\times 3
3×3的。转换函数
T
T
T相当于将卷积核对应位置的值乘在了图像的对应位置,比如说卷积核左上角乘以图像对应的左上角,然后聚集函数
A
A
A相当于相加求和的操作。因此在点云上也是一样,只不过乘上去的这个值(卷积核的值)不是随机初始化的,而是通过这个位置的点与中心点(即i和j)之间的一些关系算出来的:
其中
h
i
j
h_{ij}
hij就是点云中
x
i
x_i
xi与
x
j
x_j
xj的关系,这个关系可以人为地先验给出,可以是XYZ的距离(欧氏距离),也可以是特征维度上的距离,等等…因此最后模型的表达式可以写成这个样子:
2.3 模型结构
讲完数学公式再来看这个图就很清楚了:
我们能看到模型实际上包含了三个操作:
- 先随机采样,对于每一个采样点建立一个邻域,并在里面挑选足够的邻居点,即上图的最左边的球就是由采样点得到的邻域集合。具体的采样细节同pointnet++,使用了最远点采样(FPS)加球星区域随机采样(ball query )。
- 第二个操作即计算每个邻域中邻居点与中心点之间的关系,即上图左侧那条紫色的线,这个关系首先是low level的先验关系,即两个点之间的欧式距离、特征维度的距离;再通过这样的先验关系带入神经网络中算出高维high level的关系,即模型的这一部分,它的输出维度是和点云第j个点一样的维度,因此可以直接乘在第j个点上作为转换函数
T
T
T。
然后可以通过聚合函数 A A A将邻域的点全部聚集到中心点上,类似于CNN的卷积操作。
- 第三个部分就是聚合后点的维度增加,这个也模仿了CNN中卷积之后通道数不断增加,即上图的这一部分
至此就完成了所有的卷积操作,其实这篇文章的想法比较像GATConv在graph上的操作,都是通过有关系(也就是文章中的relation)的两个结点的一些特征去计算点与点之间的关系权重,再利用这样的关系权重去更新中心皆点的特征,具体的文章可以参考我写的Graph Attention Networks网络结构+代码
三、实验效果
3.1 实验结果
虽然想法比较简单,但是这篇文章的实验结果还是很出色的,在分类和分割上基本上碾压了所有的现有算法:
3.2 模型参数
同样文章也给出了不同的参数设定的实验效果,例如点与点之间的关系选择,会发现同时使用欧氏距离、特征距离与点本身的特征,效果会最好:
聚集函数的选择上max pooling效果最好:
邻域内点的选择上随机筛选会优于KNN筛选:
3.3 模型鲁棒性
最后文章还做了一些旋转鲁棒性、缺失鲁棒性和投影的实验,特别说一下这个投影,就是将一些3D点云映射到2D,运用学习出来的网络,都用到92%左右的正确率,说明这个卷积确实是学到了一些东西:
四、代码
Relation-Shape CNN层的结构大致如下
class RSConv(nn.Module):
def __init__(self, in_channel, out_channel, hidden_channel):
super().__init__()
self.in_channel = in_channel
self.out_channel = out_channel
'''
Relation convs, 3-layer mlp.
The input channel is 10.
The output channel is the same as the input feature channel
'''
mlp_convs = []
last_channel = 10
mlp_layers = [hidden_channel,in_channel]
rasing_layers = [out_channel]
for i, num_channel in enumerate(mlp_layers):
mlp_convs.append(nn.Conv2d(in_channels=last_channel, out_channels=num_channel, kernel_size=1))
if i < len(mlp_layers) - 1:
mlp_convs.append(nn.ReLU())
mlp_convs.append(nn.BatchNorm2d(num_channel))
last_channel = num_channel
self.mlp_convs = nn.Sequential(*mlp_convs)
'''
Channel-raising mapping. Map the input channel to the output channel.
'''
channel_raising = []
last_channel = in_channel
for num_channel in rasing_layers:
channel_raising.append(nn.Conv2d(in_channels=last_channel, out_channels=num_channel, kernel_size=1))
channel_raising.append(nn.ReLU())
channel_raising.append(nn.BatchNorm2d(num_channel))
last_channel = num_channel
self.channel_raising = nn.Sequential(*channel_raising)
def forward(self, centroid_xyz, neighbors):
"""
:param centroid_xyz: [B, npoint, C]
:param neighbors: [B, npoint, nsample, C+D]
:return:
"""
C = centroid_xyz.shape[-1]
nsample = neighbors.shape[2]
assert C == 3
'''Calculate relation h'''
neighbors_xyz = neighbors[:, :, :, :C] # [B, npoint, nsample, C]
centroid_xyz = centroid_xyz.unsqueeze(dim=2) # [B, npoint, 1, C]
delta = centroid_xyz - neighbors_xyz # [B, npoint, nsample, C]
euclidean = torch.sqrt((delta ** 2).sum(dim=-1, keepdim=True)) # [B, npoint, nsample, 1]
h = torch.cat([euclidean, delta, centroid_xyz.repeat(1, 1, nsample, 1), neighbors_xyz],
dim=-1) # [B, npoint, nsample, 10]
h = h.permute(0, 3, 1, 2) # [B, 10, npoint, nsample]
'''Apply RS-convolution'''
h = self.mlp_convs(h)
neighbors = neighbors.permute(0, 3, 1, 2) # [B, C+D, npoint, nsample]
aggregated = F.relu(torch.max(neighbors * h, dim=-1, keepdim=True)[0]) # [B, C+D, npoint, 1]
'''Channel raising'''
raised = self.channel_raising(aggregated) # [B, out_channel, npoint, 1]
raised = raised.squeeze(dim=-1) # [B, out_channel, npoint]
return raised