纲要
一、简介
二、PointNet++(SSG)网络简介
三、Sampling层
四、Grouping层
五、Sampling + Grouping组合层
六、Sampling + Grouping + PointNet组合层 (SetAbstraction层)
一、简介
在这一节中,我们将会一起来拆解PointNet++,并实现PointNet++中比较重要的几个基础部分的搭建,包括Sampling层,Grouping层等的搭建。
在下一节点云处理:基于Paddle2.0实现PointNet++对点云进行分类处理②中,我们将会一起来实现PointNet++(SSG)整体网络搭建,并进行训练和测试。
二、PointNet++(SSG)网络简介
前言
在之前提出的Pointnet中,由于网络在最后使用了MaxPooling,将所有的点最大池化为了一个全局特征,因此局部点与点之间的联系并没有被网络学习到,导致网络的输出缺乏点云的局部结构特征。而我们知道在图像处理中的CNN,使用卷积核作为局部感受野,每层的卷积核共享权值,进过多层的特征学习,提取图像中的局部特征信息。采取分层特征学习。所以,PointNet++借鉴CNN的采样思路,采取分层特征学习,即在区域内使用SetAbstraction层(采样(Sampling层)+聚合(Grouping层)+特征提取(PointNet层))
进行学习,实际上SetAbstraction层
相当于一层卷积层
,其中的一个采样(Sampling层)+聚合(Grouping层)+特征提取(PointNet层)
相当于一个卷积核
。
网络拆解
采样(Sampling层):随机选择一个初始点,然后依次利用FPS(最远点采样)进行采样,直到达到目标点数;
聚合(Grouping层):以采样点为中心,划一个R为半径的球,将里面包含的点云作为一簇成组;
特征提取(PointNet层):对Sampling+Grouping以后的点云进行局部的全局特征提取。
三、Sampling层
Sampling层主要由farthest_point_sample函数实现,farthest_point_sample函数实现了从一个输入点云中,按照所需要的点的个数npoint(可以看作一个层中卷积核个数)采样出足够多的点,并且点与点之间的距离要足够远。
FPS的逻辑如下:
假设一共有n个点,整个点集为N = {f1, f2,…,fn}, 目标是选取n1个起始点做为下一步的中心点:
- 随机选取一个点fi为起始点,并写入起始点集 B = {fi};
- 选取剩余n-1个点计算和fi点的距离,选择最远点fj写入起始点集B={fi,fj};
- 选取剩余n-2个点计算和点集B中每个点的距离, 将最短的那个距离作为该点到点集的距离, 这样得到n-2个到点集的距离,选取最远的那个点写入起始点B = {fi, fj ,fk},同时剩下n-3个点, 如果n1=3 则到此选择完毕;
- 如果n1 > 3则重复上面步骤直到选取n1个起始点为止.
具体实现步骤如下:
- 先随机初始化一个centroids矩阵,后面用于存储npoint个采样点的索引位置,大小为B×npoint,其中B为BatchSize的个数,即B个样本;
- 利用distance矩阵记录某个样本中所有点到某一个点的距离,初始化为B×N矩阵,初值给个比较大的值,后面会迭代更新;
- 利用farthest表示当前最远的点,也是随机初始化,范围为0~N,初始化B个,对应到每个样本都随机有一个初始最远点;
- batch_indices初始化为0~(B-1)的数组;
- 直到采样点达到npoint,否则进行如下迭代:
- (1)设当前的采样点centroids为当前的最远点farthest;
- (2)取出这个中心点centroid的坐标;
- (3)求出所有点到这个farthest点的欧式距离,存在dist矩阵中;
- (4) 建立一个mask,如果dist中的元素小于distance矩阵中保存的距离值,则更新distance中的对应值,随着迭代的继续distance矩阵中的值会慢慢变小,其相当于记录着某个样本中每个点距离所有已出现的采样点的最小距离;
- (5)最后从distance矩阵取出最远的点为farthest,继续下一轮迭代.
farthest_point_sample函数:
Input:
xyz: pointcloud data, [B, N, 3]
npoint: number of samples
Return:
centroids: sampled pointcloud index, [B, npoint]
def farthest_point_sample(xyz, npoint):
B, N, C = xyz.shape
centroids = paddle.zeros([B, npoint])
distance = paddle.ones([B, N])
farthest = paddle.randint(0, N, (B,)) #每个样本随机初始化一个最远点
batch_indices = paddle.arange(B)
for i in range(npoint):
centroids[:, i] = farthest #更新第i个最远点
xyz_np = xyz.numpy()
batch_indices_np = batch_indices.numpy().astype('int64')
farthest_np = farthest.numpy().astype('int64')
centroid = xyz_np[batch_indices_np, farthest_np, :]#取出最远点的x,y,z坐标
centroid = paddle.to_tensor(centroid).unsqueeze(1) #(B,1,3)
#A**2是对A中每个元素求幂的运算
# 计算点集中的所有点到这个最远点的欧式距离
dist = paddle.sum((xyz - centroid) ** 2, -1) #(B,N)
# 更新distances,记录样本中每个点距离所有已出现的采样点的最小距离
mask = dist < distance
distance_np = distance.numpy()
dist_np = dist.numpy()
mask_np = mask.numpy()
distance_np[mask_np] = dist_np[mask_np]
# 从更新后的distances矩阵中找出距离最远的点,作为最远点用于下一轮迭代
#取出每一行的最大值构成列向量,等价于torch.max(x,2)
distance = paddle.to_tensor(distance_np)
farthest = paddle.argmax(distance, -1)#(B,1)
return centroids
补充注释:
1.randint
paddle.randint(low=0, high=None, shape=[1], dtype=None, name=None)
该OP返回服从均匀分布的、范围在[low
, high
)的随机Tensor,形状为 shape
,数据类型为 dtype
。当 high
为None时(默认),均匀采样的区间为[0, low
)。
2.arange
paddle.arange(start=0, end=None, step=1, dtype=None, name=None)
该OP返回以步长 step
均匀分隔给定数值区间[start
, end
)的1-D Tensor,数据类型为 dtype
。
四、Grouping层
Grouping层主要由query_ball_point函数实现,在得到由Sampling层得出的npoint个采样点后,query_ball_point函数实现了在npoint个采样点的附近的球形领域中,寻找合适的点进行聚合为一组,这一组实际上类似于图像处理中的一个patch,后面的pointnet将会对这一patch进行特征提取(类比于卷积核进行卷积操作提取特征)。
query_ball_point函数:
new_xyz为S个球形领域的中心(由最远点采样在前面得出),xyz为所有的点云;输出为每个样本的每个球形领域的nsample个采样点集的索引[B,S,nsample]。
Input:
radius: local region radius
nsample: max sample number in local region
#会在某个半径的球内找点,上限是nsample
xyz: all points, [B, N, 3]
new_xyz: query points, [B, S, 3]
Return:
group_idx: grouped points index, [B, S, nsample]
def query_ball_point(radius, nsample, xyz, new_xyz):
B, N, C = xyz.shape
_, S, _ = new_xyz.shape
group_idx = paddle.tile(paddle.arange(N).reshape([1, 1, N]), [B, S, 1]) #(B,S,N) 每一行为0---n-1
sqrdists = square_distance(new_xyz, xyz) #(B,S,N)
#square_distance函数主要用来在ball query过程中确定每一个点距离采样点的距离。
mask = sqrdists > radius ** 2
group_idx_np = group_idx.numpy()
mask_np = mask.numpy()
# 找到所有距离大于radius^2的,其group_idx直接置为N;其余的保留原来的值
group_idx_np[mask_np] = N
group_idx = paddle.to_tensor(group_idx_np)
group_idx = group_idx.sort(axis=-1)[:, :, :nsample] #(B,S,nsample)
## 做升序排列,前面大于radius^2的都是N,会是最大值,所以会直接在剩下的点中取出前nsample个点
## 考虑到有可能前nsample个点中也有被赋值为N的点(即球形区域内不足nsample个点),这种点需要
#舍弃,直接用第一个点来代替即可
group_first = paddle.tile(group_idx[:, :, 0].reshape([B, S, 1]), [1, 1, nsample])
mask = group_idx == N
## 找到group_idx中值等于N的点,会输出0,1构成的三维Tensor,维度为[B,S,nsample]
group_idx_np = group_idx.numpy()
group_first_np = group_first.numpy()
mask_np = mask.numpy()
group_idx_np[mask_np] = group_first_np[mask_np]
group_idx = paddle.to_tensor(group_idx_np)
return group_idx
补充注释:
1.tile
paddle.tile(x, repeat_times, name=None)
根据参数 repeat_times
对输入 x
的各维度进行复制。 平铺后,输出的第 i
个维度的值等于 x.shape[i]*repeat_times[i]
。
x
的维数和 repeat_times
中的元素数量应小于等于6。
五、Sampling + Grouping组合层
Sampling + Grouping组合层主要用于将整个点云分散成局部的group(相当于图像处理中的一个个patch),对每一个group都可以用PointNet单独的提取局部的全局特征(PointNet单独的提取局部的全局特征相当于卷积操作)
Sampling + Grouping组合层有两种操作,一种是sample_and_group,其将整个点云分散成局部的group,对每一个group都可以用PointNet单独的提取局部的全局特征;另一种是sample_and_group_all,其直接将所有点作为一个group,做一个类似全局最大池化的操作。
sample_and_group函数:
sample_and_group的实现步骤如下:
先用farthest_point_sample函数实现最远点采样FPS得到采样点的索引,再通过index_points将这些点的从原始点中挑出来,作为new_xyz
利用query_ball_point和index_points将原始点云通过new_xyz 作为中心分为npoint个球形区域其中每个区域有nsample个采样点
每个区域的点减去区域的中心值
如果每个点上面有新的特征的维度,则用新的特征与旧的特征拼接,否则直接返回旧的特征
Input:
npoint: #FPS采样点的数量
radius: #球形区域所定义的半径
nsample:#球形区域所能包围的最大的点数量
xyz: input points position data, [B, N, 3]
points: input points data, [B, N, D] #D不包含坐标数据x,y,z
Return:
new_xyz: sampled points position data, [B, npoint, nsample, 3]
new_points: sampled points data, [B, npoint, nsample, 3+D]
sample_and_group_all函数:
Input:
xyz: input points position data, [B, N, 3]
points: input points data, [B, N, D]
Return:
new_xyz: sampled points position data, [B, 1, 3]
new_points: sampled points data, [B, 1, N, 3+D]
sample_and_group函数
def sample_and_group(npoint, radius, nsample, xyz, points, returnfps=False):
B, N, C = xyz.shape
S = npoint
fps_idx = farthest_point_sample(xyz, npoint)
# 从原点云中挑出最远点采样的采样点为new_xyz
new_xyz = index_points(xyz, fps_idx) #(B,S,3)
idx = query_ball_point(radius, nsample, xyz, new_xyz)
grouped_xyz = index_points(xyz, idx)
# grouped_xyz:[B, S, nsample, C],
#通过index_points将所有group内的nsample个采样点从原始点中挑出来
grouped_xyz_norm = grouped_xyz - new_xyz.reshape([B, S, 1, C])
#每个区域的点减去区域的中心值
#不包含坐标,只包含了每个点经过之前层后提取的除坐标外的其他特征,所以第一层没有
# 如果每个点上面有新的特征的维度,则用新的特征与旧的特征拼接,否则直接返回旧的特征
if points is not None:
grouped_points = index_points(points, idx)#[B, S, nsample, D]
##axis=-1代表按照最后的维度进行拼接,即相当于axis=3
new_points = paddle.concat([grouped_xyz_norm, grouped_points], axis=-1) # [B, npoint, nsample, C+D]
else:
new_points = grouped_xyz_norm
if returnfps:
return new_xyz, new_points, grouped_xyz, fps_idx
else:
return new_xyz, new_points
sample_and_group_all函数
def sample_and_group_all(xyz, points):
B, N, C = xyz.shape
#new_xyz代表中心点,用原点表示
new_xyz = paddle.zeros([B, 1, C])
grouped_xyz = xyz.reshape([B, 1, N, C])
if points is not None:
new_points = paddle.concat([grouped_xyz, points.reshape([B, 1, N, -1])], axis=-1)
#points.reshape([B, 1, N, -1])相当于points.reshape([B, 1, N, D])
else:
new_points = grouped_xyz
return new_xyz, new_points
六、Sampling + Grouping + PointNet组合层 (SetAbstraction层)
SetAbstraction层先通过sample_and_group的操作形成局部的group,最后通过PointNet层 (实际上就是前面提出的PointNet网络:对局部的group中的每一个点做MLP操作,最后进行局部的最大池化)。
SetAbstraction层:
Input:
xyz: input points position data, [B, C, N]
points: input points data, [B, D, N]
Return:
new_xyz: sampled points position data, [B, C, S]
new_points_concat: sample points feature data, [B, D', S]
class PointNetSetAbstraction(nn.Layer):
def __init__(self, npoint, radius, nsample, in_channel, mlp, group_all):
super(PointNetSetAbstraction, self).__init__()
self.npoint = npoint
self.radius = radius
self.nsample = nsample
self.mlp_convs = []
self.mlp_bns = []
last_channel = in_channel
for out_channel in mlp:
self.mlp_convs.append(nn.Conv2D(last_channel, out_channel, 1))
self.mlp_bns.append(nn.BatchNorm2D(out_channel))
last_channel = out_channel
self.group_all = group_all
def forward(self, xyz, points):
xyz = xyz.transpose([0, 2, 1])#(B,N,3)
if points is not None:
points = points.transpose([0, 2, 1])#(B,N,D)
if self.group_all:
new_xyz, new_points = sample_and_group_all(xyz, points)
else:
#new_xyz:[B, npoint, 3], new_points:[B, npoint, nsample, 3+D]
new_xyz, new_points = sample_and_group(self.npoint, self.radius, self.nsample, xyz, points)
new_points = new_points.transpose([0, 3, 2, 1]) #(B,S,nsample,C+D) --->#(B,C+D,nsample,S)
# 利用1x1的2d的卷积相当于把每个group当成一个通道,共npoint个通道,
#对[3+D, nsample]的维度上做逐像素的卷积,结果相当于对单个C+D维度做1d的卷积
for i, conv in enumerate(self.mlp_convs):
bn = self.mlp_bns[i]
new_points = F.relu(bn(conv(new_points)))
#对每个group做一个max pooling得到局部的全局特征,得到的new_points:[B,3+D,S]
new_points = paddle.max(new_points, 2)
new_xyz = new_xyz.transpose([0, 2, 1])#(B,3,S)
return new_xyz, new_points