最远点采样(Farthest Point Sampling)是一种非常常用的采样算法,由于能够保证对样本的均匀采样,被广泛使用,像3D点云深度学习框架中的PointNet++对样本点进行FPS采样再聚类作为感受野,3D目标检测网络VoteNet对投票得到的散乱点进行FPS采样再进行聚类,6D位姿估计算法PVN3D中用于选择物体的8个特征点进行投票并计算位姿。
对于FPS算法的原理,两篇博文:PointNet++的pytorch实现代码阅读,以及 PointNet++详解与代码
已经介绍的非常详细了,一句话概括就是不断迭代地选择距离已有采样点集合的最远点。这里再介绍一遍,并实测分析一下;
FPS算法原理:
- 输入点云有N个点,从点云中选取一个点P0作为起始点,得到采样点集合S={P0};
- 计算所有点到P0的距离,构成N维数组L,从中选择最大值对应的点作为P1,更新采样点集合S={P0,P1};
- 计算所有点到P1的距离,对于每一个点Pi,其距离P1的距离如果小于L[i],则更新L[i] = d(Pi, P1),因此,数组L中存储的一直是每一个点到采样点集合S的最近距离;
- 选取L中最大值对应的点作为P2,更新采样点集合S={P0,P1,P2};
- 重复2-4步,一直采样到N’个目标采样点为止。
有两个问题,一个是初始点选择,一个是采用的距离度量;
- 初始点选择:
- 随机选择一个点,每次结果不同;
- 选择距离点云重心的最远点,每次结果相同,一般位于局部极值点,具有刻画能力;
- 距离度量
- 欧氏距离:主要对于点云,在3D体空间均匀采样;
- 测地距离:主要对于三角网格,在三角网格面上进行均匀采样;
下面给出测试样例,也来自上面两篇博文:
from __future__ import print_function
import torch
from torch.autograd import Variable
def farthest_point_sample(xyz, npoint):
"""
Input:
xyz: pointcloud data, [B, N, 3]
npoint: number of samples
Return:
centroids: sampled pointcloud index, [B, npoint]
"""
xyz = xyz.transpose(2,1)
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)
batch_indices = torch.arange(B, dtype=torch.long).to(device) # batch_size 数组
#farthest = torch.randint(0, N, (B,), dtype=torch.long).to(device) # 初始时随机选择一点
barycenter = torch.sum((xyz), 1) #计算重心坐标 及 距离重心最远的点
barycenter = barycenter/xyz.shape[1]
barycenter = barycenter.view(B, 1, 3)
dist = torch.sum((xyz - barycenter) ** 2, -1)
farthest = torch.max(dist,1)[1] #将距离重心最远的点作为第一个点
for i in range(npoint):
print("-------------------------------------------------------")
print("The %d farthest pts %s " % (i, farthest))
centroids[:, i] = farthest # 更新第i个最远点
centroid = xyz[batch_indices, farthest, :].view(B, 1, 3) # 取出这个最远点的xyz坐标
dist = torch.sum((xyz - centroid) ** 2, -1) # 计算点集中的所有点到这个最远点的欧式距离
print("dist : ", dist)
mask = dist < distance
print("mask %i : %s" % (i,mask))
distance[mask] = dist[mask] # 更新distance,记录样本中每个点距离所有已出现的采样点的最小距离
print("distance: ", distance)
farthest = torch.max(distance, -1)[1] # 返回最远点索引
return centroids
if __name__ == '__main__':
sim_data = Variable(torch.rand(1,3,8))
print(sim_data)
centroids = farthest_point_sample(sim_data, 4)
print("Sampled pts: ", centroids)
输出结果如下,可以看到得到的mask只有True的情况下,才会用当前计算的dist替换distance,这样distance中存储的一直是每一个点到采样点集合的最近距离,从中选择最大值就找到了下一个采样点。
tensor([[[0.4442, 0.7545, 0.8402, 0.1476, 0.6654, 0.1622, 0.1818, 0.3758],
[0.0328, 0.3010, 0.0910, 0.4768, 0.6825, 0.3810, 0.1923, 0.4230],
[0.5849, 0.4576, 0.5159, 0.0993, 0.3962, 0.1519, 0.0429, 0.3123]]])
-------------------------------------------------------
The 0 farthest pts tensor([2])
dist : tensor([[0.1649, 0.0548, 0.0000, 0.8021, 0.3947, 0.6762, 0.6675, 0.3673]])
mask 0 : tensor([[True, True, True, True, True, True, True, True]])
distance: tensor([[0.1649, 0.0548, 0.0000, 0.8021, 0.3947, 0.6762, 0.6675, 0.3673]])
-------------------------------------------------------
The 1 farthest pts tensor([3])
dist : tensor([[0.5210, 0.5276, 0.8021, 0.0000, 0.3985, 0.0122, 0.0853, 0.1003]])
mask 1 : tensor([[False, False, False, True, False, True, True, True]])
distance: tensor([[0.1649, 0.0548, 0.0000, 0.0000, 0.3947, 0.0122, 0.0853, 0.1003]])
-------------------------------------------------------
The 2 farthest pts tensor([4])
dist : tensor([[0.5067, 0.1572, 0.3947, 0.3985, 0.0000, 0.4038, 0.5989, 0.1583]])
mask 2 : tensor([[False, False, False, False, True, False, False, False]])
distance: tensor([[0.1649, 0.0548, 0.0000, 0.0000, 0.0000, 0.0122, 0.0853, 0.1003]])
-------------------------------------------------------
The 3 farthest pts tensor([0])
dist : tensor([[0.0000, 0.1844, 0.1649, 0.5210, 0.5067, 0.3882, 0.3881, 0.2312]])
mask 3 : tensor([[ True, False, False, False, False, False, False, False]])
distance: tensor([[0.0000, 0.0548, 0.0000, 0.0000, 0.0000, 0.0122, 0.0853, 0.1003]])
Sampled pts: tensor([[2, 3, 4, 0]])