目录
通过局部特征学习器(Local Feature Learner)抽象点集或局部特征:
Review of PointNet - PointNet回顾
Hierarchical Point Set Feature Learning - 分层点集特征学习
Multi-Resolution Grouping - 多分辨率分组
前沿
在PointNet中,网络对每一个点做低维到高维的映射进行特征学习,然后把所有点映射到高维的特征通过最大池化最终表示全局特征。从本质上来说,要么对一个点做操作,要么对所有点做操作,实际上没有局部的概念(loal context) 。同时也缺少 local context 在平移不变性上也有局限性(世界坐标系和局部坐标系)。对点云数据做平移操作后,所有的数据都将发生变化,导致所有的特征,全局特征都不一样了。对于单个的物体还好,可以将其平移到坐标系的中心,把他的大小归一化到一个球中,但是在一个场景中有多个物体时则不好办,需要对哪个物体做归一化呢?
关键问题
PointNet++ 解决了两个问题:如何生成点集的划分(Partitioning),以及如何通过局部特征学习器(Local Feature Learner)抽象点集或局部特征。
生成点集的划分(Partitioning):
点集划分是指如何将一个大的点云分割成更小的、更易于管理的子集。这个过程类似于在传统的卷积神经网络中如何处理图像的小区域(或“patches”),以便可以在这些区域上应用局部操作。PointNet++需要一种方法来有效地将点云分割成多个部分,这样可以在每个部分上独立地学习特征。
通过局部特征学习器(Local Feature Learner)抽象点集或局部特征:
一旦点云被划分成小的子集,PointNet++的下一个任务是学习这些子集(或局部区域)的特征。这需要一个“局部特征学习器”,它能够从每个子集中提取有用的信息或特征。这与在传统CNN中学习图像局部区域特征的过程相似。
两个问题是相关联的,因为:
点集的划分必须产生跨分区的共同结构:为了能够在不同的局部子集上共享权重(类似于在CNN中权重共享的概念),PointNet++在进行点集划分时,需要确保这些划分具有一定的一致性或共同结构。这意味着即使是不同的局部子集,也应该以一种方式被处理,使得在它们之间可以共享学习到的特征表示的权重。这样做的目的是提高模型的效率和泛化能力,因为学习到的特征和权重可以在多个局部区域中复用。
上述即为PointNet++设计中的两个核心挑战(如何有效地对点云进行分区,以便可以在这些分区上独立地学习特征;以及如何设计一个能够从这些局部分区中学习有用特征的机制,同时确保这些分区的处理方式允许在它们之间共享模型权重。-- 为了模仿传统卷积网络中的权重共享机制 -- 以提高学习效率和模型的泛化能力)
PointNet++选择PointNet作为局部特征学习器(他是无序点云数据特征提取的高效算法)
可以理解为:PointNet++应用PointNet递归地对输入集进行嵌套分区。
网络模型
以二维欧几里得空间为例,网络的分割和分类模型
网络的每一组set abstraction layers主要包括3个部分:Sampling layer、Grouping layer and、PointNet layer。
Sample layer | 对输入点进行采样,在这些点中选出若干个中心点; |
Grouping layer | 利用上一步得到的中心点将点集划分成若干个区域; |
PointNet layer | 对上述得到的每个区域进行编码,变成特征向量。 |
Review of PointNet - PointNet回顾
PointNet是一个处理无序点集的深度学习框架,它通过学习每个点的空间编码,然后聚合所有点的特征来生成全局点云特征。PointNet的核心思想是使用最大池化操作来聚合全局特征,这使得模型对点的排列顺序不敏感。尽管PointNet在多个基准测试上表现出色,但它缺乏捕获不同尺度上局部特征的能力。
=========================================================================
Hierarchical Point Set Feature Learning - 分层点集特征学习
多层次结构由多个set abstraction layers组成,在每个层上,一组点云被处理和抽象,以产生一个更少元素的新集合。set abstraction layers由Sampling layer、Grouping layer 和 PointNet layer 三部分组成。
Sampling layer - 采样层 从输入点中选取一组点,定义局部区域的形心。
Grouping layer - 通过查找形心点周围的“邻近点”来构建局部区域点集。
PointNet layer - 使用mini-PointNet将局部区域编码为特征向量。
=========================================================================
Sampling layer
使用farthest point sampling(FPS)选择𝑁′个点(文中提到相比于随机采样,该方法能更好的覆盖整个点集,具体选择多少个中心点以及邻域内的数量由超参数确定)
FPS是一种在点云、图像处理或其他数据集中用于抽样的算法。目的是从一个大的数据集中选出一组代表性强的点,这些点彼此之间的最小距离尽可能大。
作者通过FPS来抽样点集中较为重要的点。(即任务是找到点云集中的局部区域的中心点)
可能存在的问题:计算成本、样本分布偏差(可能导致样本在高密度区域内过度集中,低密度区域则过于稀缺)、参数依赖(依赖初始点和距离度量方式的选择)、可能无法捕捉重要的几何细节。
=========================================================================
Grouping layer
文中作者通过Ball query来查询形心的邻居点。
具体做法:给定两个超参数(每个区域中点的数量𝐾和query的半径𝑟),对于某个形心,Ball query找到该查询点在半径为𝑟范围内点,该范围确保局部区域的尺度是固定的。
与K最近邻(kNN)查询相比,Ball query通过固定区域尺度而不是固定邻居数量来定义邻域。kNN查询寻找最近的K个邻居,但这可能导致所选邻域的实际尺寸随点的密度变化而变化,这在处理非均匀采样的数据时可能不是最优的选择。相反,Ball query通过确保每个局部区域都有一个固定的尺度,提高了模型在空间上的泛化能力。在实现时,通常会设置一个上限K,以限制每个局部区域中考虑的点的数量,以保持计算的可管理性。
可改进的地方:对点云密度变换较为敏感、对参数选择依赖性高(半径太小可能无法有效捕获足够的局部详细,太大则可能导致不相关的点增多,使局部特征的表示不够精确)、计算效率问题、均匀性假设(Ball query是基于欧氏距离的均匀性假设)
欧式距离的均匀性假设:即在欧氏空间中,两点的距离反映了这两点的实际相似度或关联度。
基于以下前提:
空间均匀性:空间是均匀和各向同性的,即任何方向上的度量都是等价的,距离的度量不受空间中位置的影响。
距离直观性:在屋里空间或某些特定的抽象空间中,两个点之间的直线距离被认为是相似度或连接强度的只管表示。
规模一致性:假设空间中所有区域的尺度或特征分布具有一定的一致性,即空间中的任何距离值具有相似的含义。
(任务是通过中心点找到邻居点,并将它们组织称为局部区域集)
=========================================================================
PointNet layer
步骤
局部坐标系转换:局部区域中的点转换成相对于形心的局部坐标系。(局部区域总的每个点将相对于形心所在位置进行调整,以反映其相对位置)
实现方法:通过将局部区域中的每个点-形心点的坐标来实现。
特征编码:将转换后的坐标以及点的附加特征(文中的𝐶所表示的其他信息)一起送入mini-PointNet来提取局部区域中的特征。
输出:
利用相对坐标与点特征相结合的方式可以捕获局部区域中点与点之间的关系。
=========================================================================
非均匀密度下稳健特征学习
由于点集在不同区域可能会有不同的采样密度,这种非均匀性为点集特征学习带来了显著挑战。在密集采样的区域中学到的特征可能无法很好地泛化到稀疏采样的区域,反之亦然。因此,为了解决这一问题,PointNet++提出了密度自适应PointNet层,包含两种适应性特征学习层:多尺度分组(Multi-Scale Grouping, MSG)和多分辨率分组(Multi-Resolution Grouping, MRG)。
Multi-Scale Grouping - 多尺度分组
MSG通过应用不同尺度的分组层(按照不同的搜索半径或领域大小对点集进行分组),然后通过对应的PointNets提取每个尺度上的特征来捕获多尺度模式。不同尺度的特征被串联形成多尺度特征向量。这种方法使网络能够通过在训练期间随机丢弃输入点(称为随机输入丢弃 - random input dropout)来学习优化的策略,以结合来自不同尺度的特征。这样,网络在训练时被呈现了不同稀疏度的点集,从而学会根据输入数据的变化自适应地加权不同尺度上检测到的模式。
MSG小结
具体来说,在MSG中,网络对于每个选定的形心点,按照几个预定义的半径值来搜索周围的邻近点。每个半径定义了一个局部邻域的大小,因此每个质心将根据这些不同的半径值与其周围点形成多个点集群。这样,对于每个质心点,网络不是只捕获一个尺度上的局部特征,而是能够捕获多个尺度上的局部特征。
每个尺度(或每组邻域大小)的点集群都将独立地送入对应的PointNet网络进行特征提取,之后这些不同尺度上提取的特征被串联起来,形成一个综合的多尺度特征表示。这种方法使得网络能够在细节丰富的区域(通过较小的邻域尺度捕获细节)和稀疏采样的区域(通过较大的邻域尺度避免过度稀疏的问题)中均能有效提取特征。
MSG的关键优点在于它通过在训练期间的随机输入丢弃(即随机移除一部分输入点)来模拟不同的采样密度,从而训练网络在面对实际应用中可能遇到的各种采样密度时,能够自适应地选择最适合的特征尺度进行组合,以实现最佳的性能。这种方法大大增强了网络处理非均匀采样数据的能力,提高了模型的泛化性和稳健性。
(在训练时引入不同密度的点集情况,使网络能学习不同采样密度下局部点云特征的提取,捕获密集到稀疏采样区域内的多尺度信息 -- 通过随机丢弃来模拟不同密度的采样,使网络能够应对实际中各种密度变换的情况-提高模型的泛化性能)
MSG相当于并联了多个hierarchical structure,每个结构中心点不变,但是尺度不同。通过PointNet获取每个形心多尺度信息,之后concat形成该区域提取的总特征。在训练时引入随机丢弃形心来模拟不同密度情况,提高算法鲁棒性。
Multi-Resolution Grouping - 多分辨率分组
MSG方法虽然有效,但在计算上可能非常昂贵,尤其是在低层次上对每个质心点运行局部PointNet时。为此,MRG为一种低成本的替代方案。
MRG通过结合来自不同分辨率的特征来实现效率和适应性的平衡。具体而言,MRG策略在处理每个局部区域时,不仅考虑从当前分辨率下抽象得到的特征,还考虑了从更低分辨率(即上一层级)直接提取的特征。这两种特征被concat为一个复合特征向量,为后续的处理步骤提供信息。
在MRG中,某一层次𝐿𝑖的区域特征是通过将来自下一级𝐿𝑖−1的子区域特征总结后的向量与直接处理该局部区域所有原始点的单个PointNet得到的特征向量进行concat得到的。当局部区域的密度较低时,由于子区域在计算第一个向量时包含的点更稀疏,因此可能比第二个向量更不可靠。在这种情况下,应该更高地加权第二个向量。相反,当局部区域的密度较高时,第一个向量提供了更细致的信息,因为它能够在更低层次上递归地检视更高分辨率。
来自下一级的特征:首先,将来自下一级(更高分辨率)的特征进行汇总,形成一个特征向量。这一过程通过对每个子区域应用集合抽象层(set abstraction level)完成。
直接处理的原始点特征:另一部分特征是通过在当前分辨率直接对所有原始点应用单个PointNet得到的。
参考资源
PointNet++官方链接: PointNet++
PointNet++官方代码: GitHub - yanx27/Pointnet_Pointnet2_pytorch: PointNet and PointNet++ implemented by pytorch (pure python) and on ModelNet, ShapeNet and S3DIS.
代码走读: http://t.csdnimg.cn/jpIr8
其他资源:
PointNet作者--3D物体检测发展与未来--报告
【PointNet作者亲述】90分钟带你了解3D物体检测算法和未来方向!_哔哩哔哩_bilibili
PointNet++作者--点云上的深度学习及其在三维场景理解中的应用--报告
将门创投 | 斯坦福大学在读博士生祁芮中台:点云上的深度学习及其在三维场景理解中的应用_哔哩哔哩_bilibili
相关代码
Sampling layer代码
def farthest_point_sample(xyz, npoint):
"""
Input:
xyz: pointcloud data, [B, N, 3]
npoint: number of samples
Return:
centroids: sampled pointcloud index, [B, npoint, 3]
"""
device = xyz.device
B, N, C = xyz.shape
centroids = torch.zeros(B, npoint, dtype=torch.long).to(device) # 采样点矩阵(B, npoint)
distance = torch.ones(B, N).to(device) * 1e10 # 采样点到所有点距离(B, N)
farthest = torch.randint(0, N, (B,), dtype=torch.long).to(device) # 最远点,初试时随机选择一点点
batch_indices = torch.arange(B, dtype=torch.long).to(device) # batch_size 数组
for i in range(npoint):
centroids[:, i] = farthest # 更新第i个最远点
centroid = xyz[batch_indices, farthest, :].view(B, 1, 3) # 取出这个最远点的xyz坐标
dist = torch.sum((xyz - centroid) ** 2, -1) # 计算点集中的所有点到这个最远点的欧式距离
mask = dist < distance
distance[mask] = dist[mask] # 更新distances,记录样本中每个点距离所有已出现的采样点的最小距离
farthest = torch.max(distance, -1)[1] # 返回最远点索引
return centroids
Grouping layer代码
def square_distance(src, dst):
"""
Calculate Euclid distance between each two points.
src^T * dst = xn * xm + yn * ym + zn * zm;
sum(src^2, dim=-1) = xn*xn + yn*yn + zn*zn;
sum(dst^2, dim=-1) = xm*xm + ym*ym + zm*zm;
dist = (xn - xm)^2 + (yn - ym)^2 + (zn - zm)^2
= sum(src**2,dim=-1)+sum(dst**2,dim=-1)-2*src^T*dst
Input:
src: source points, [B, N, C]
dst: target points, [B, M, C]
Output:
dist: per-point square distance, [B, N, M]
"""
B, N, _ = src.shape
_, M, _ = dst.shape
dist = -2 * torch.matmul(src, dst.permute(0, 2, 1))
dist += torch.sum(src ** 2, -1).view(B, N, 1)
dist += torch.sum(dst ** 2, -1).view(B, 1, M)
return dist
def query_ball_point(radius, nsample, xyz, new_xyz):
"""
Input:
radius: local region radius
nsample: max sample number in local region
xyz: all points, [B, N, 3]
new_xyz: query points, [B, S, 3]
Return:
group_idx: grouped points index, [B, S, nsample]
"""
device = xyz.device
B, N, C = xyz.shape
_, S, _ = new_xyz.shape
group_idx = torch.arange(N, dtype=torch.long).to(device).view(1, 1, N).repeat([B, S, 1])
sqrdists = square_distance(new_xyz, xyz)
group_idx[sqrdists > radius ** 2] = N
group_idx = group_idx.sort(dim=-1)[0][:, :, :nsample]
group_first = group_idx[:, :, 0].view(B, S, 1).repeat([1, 1, nsample])
mask = group_idx == N
group_idx[mask] = group_first[mask]
return group_idx
PointNet layer代码
class PointNetSetAbstraction(nn.Module):
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 = nn.ModuleList()
self.mlp_bns = nn.ModuleList()
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):
"""
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]
"""
xyz = xyz.permute(0, 2, 1)
if points is not None:
points = points.permute(0, 2, 1)
if self.group_all:
new_xyz, new_points = sample_and_group_all(xyz, points)
else:
new_xyz, new_points = sample_and_group(self.npoint, self.radius, self.nsample, xyz, points)
# new_xyz: sampled points position data, [B, npoint, C]
# new_points: sampled points data, [B, npoint, nsample, C+D]
new_points = new_points.permute(0, 3, 2, 1) # [B, C+D, nsample,npoint]
for i, conv in enumerate(self.mlp_convs):
bn = self.mlp_bns[i]
new_points = F.relu(bn(conv(new_points)))
new_points = torch.max(new_points, 2)[0]
new_xyz = new_xyz.permute(0, 2, 1)
return new_xyz, new_points
MSG代码
class PointNetSetAbstractionMsg(nn.Module):
def __init__(self, npoint, radius_list, nsample_list, in_channel, mlp_list):
super(PointNetSetAbstractionMsg, self).__init__()
self.npoint = npoint
self.radius_list = radius_list
self.nsample_list = nsample_list
self.conv_blocks = nn.ModuleList()
self.bn_blocks = nn.ModuleList()
for i in range(len(mlp_list)):
convs = nn.ModuleList()
bns = nn.ModuleList()
last_channel = in_channel + 3
for out_channel in mlp_list[i]:
convs.append(nn.Conv2d(last_channel, out_channel, 1))
bns.append(nn.BatchNorm2d(out_channel))
last_channel = out_channel
self.conv_blocks.append(convs)
self.bn_blocks.append(bns)
def forward(self, xyz, points):
"""
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]
"""
xyz = xyz.permute(0, 2, 1)
if points is not None:
points = points.permute(0, 2, 1)
B, N, C = xyz.shape
S = self.npoint
new_xyz = index_points(xyz, farthest_point_sample(xyz, S))
new_points_list = []
for i, radius in enumerate(self.radius_list):
K = self.nsample_list[i]
group_idx = query_ball_point(radius, K, xyz, new_xyz)
grouped_xyz = index_points(xyz, group_idx)
grouped_xyz -= new_xyz.view(B, S, 1, C)
if points is not None:
grouped_points = index_points(points, group_idx)
grouped_points = torch.cat([grouped_points, grouped_xyz], dim=-1)
else:
grouped_points = grouped_xyz
grouped_points = grouped_points.permute(0, 3, 2, 1) # [B, D, K, S]
for j in range(len(self.conv_blocks[i])):
conv = self.conv_blocks[i][j]
bn = self.bn_blocks[i][j]
grouped_points = F.relu(bn(conv(grouped_points)))
new_points = torch.max(grouped_points, 2)[0] # [B, D', S]
new_points_list.append(new_points)
new_xyz = new_xyz.permute(0, 2, 1)
new_points_concat = torch.cat(new_points_list, dim=1)
return new_xyz, new_points_concat