1、3D点云-PointNet
PointNet论文:PointNet
PointNet++论文:PointNet++
关于3D点云是什么?
3D点云(Point Cloud)是一种表示三维空间数据的形式,它由一组在三维空间中的离散点组成。
如何处理3维数据?
以往的由于技术的限制,往往从多个角度拍摄,然后合成,形成3维数据,现在可以通过LiDAR、Depth Sensor等来获取3D点云数据
PointNet
本篇文章,介绍的PointNet则是使用深度学习卷积的方法来对三维离散空间中的点进行特征提取,随后并对三维带你进行分类或者部件分割和场景分割,分类是在整个部件的程度上,分割是在一个部件上对于多个进行分割。
PointNet出发点
PointNet基本出发点:由于点的无序性导致,需要模型具有置换不变性(就是点的位置交换,并不会影响他的特征结果,因为点都是离散的),下面为公式体现
基于这种特性,在神经网络中体现出来就是,取出最大值作为特征
PointNet架构图
基本思想是如何将无序的点的特征提取出来,PointNet主要是通过全连接层MLP(实际上是进行一些1DConv),将只有3维的特征信息进行升维,经过两个MLP升维之后,使得特征向量变得很长。最终通过在1024维中每个维度选择一个最大值作为全局特征。
如果是分类任务,再连接MPL进行降维,以及softmax来完成分类任务。
如果是分割任务,就不能仅仅使用最大值作为全局特征,因为这样的话特征太少了,会将第一层经过MLP,64维特征具有局部特征,同时将形成的最大值特征复制n份,进行拼接,得到一个新的特征,最总通过多个MLP进行卷积,最终恒星n*m的矩阵,也就是n个点分别属于m类别的概率。
PointNet++
从PointNet提取特征的方式可以看出,虽然提取出了单个点特征,但是对于某一个点来说,他对于附近点的特征是不清楚,那么是否可以通过一种方式,像Transformer一样,将一张图片分成多个块,然后进行卷积,这样一个点不仅包含了自己的特征,同时对于附近的点的特征也做了提取。
PointNet++就是来解决这个问题的,PointNet++通过多个中心点,围绕着选择的中心点,画球来获取中心点周围的特征,最终多个中心点提取到的特征进行汇总,完成特征提取。只是简单的原理说明,实际上的PointNet++,选择了多次中心点,在上一次的基础上,而且每次中心点画球,也是有不同的半径,不同半径对应选择的点的个数也是不一样的。
PointNet++架构图
下面代码采用分类任务作为示例,通过batch_size为4
主体架构代码
class get_model(nn.Module):
def __init__(self, num_class, normal_channel=True):
super(get_model, self).__init__()
in_channel = 3 if normal_channel else 0 # 如果有法向量特征,则输入通道数为3,否则为0
self.normal_channel = normal_channel
# 第一个Set Abstraction层,使用多尺度分组(MSG)
self.sa1 = PointNetSetAbstractionMsg(
512, # 采样点数
[0.1, 0.2, 0.4], # 三个不同尺度的球形半径
[16, 32, 128], # 每个半径对应的邻域采样点数
in_channel, # 输入特征的通道数
[
[32, 32, 64], # 对应第一个半径的MLP层参数
[64, 64, 128], # 对应第二个半径的MLP层参数
[64, 96, 128] # 对应第三个半径的MLP层参数
]
)
# 第二个Set Abstraction层,使用多尺度分组(MSG)
self.sa2 = PointNetSetAbstractionMsg(
128, # 采样点数
[0.2, 0.4, 0.8], # 三个不同尺度的球形半径
[32, 64, 128], # 每个半径对应的邻域采样点数
320, # 输入特征的通道数(上一层输出特征的总维度)
[
[64, 64, 128], # 对应第一个半径的MLP层参数
[128, 128, 256], # 对应第二个半径的MLP层参数
[128, 128, 256] # 对应第三个半径的MLP层参数
]
)
# 第三个Set Abstraction层,使用单尺度分组(SSG)
self.sa3 = PointNetSetAbstraction(
None, None, None, # 不进行采样,聚合所有点(全局特征)
640 + 3, # 输入特征的通道数(上一层输出特征的总维度 + 坐标维度3)
[256, 512, 1024], # MLP层参数
True # 是否在全局范围内进行聚合
)
# 全连接层和批归一化层的定义
self.fc1 = nn.Linear(1024, 512) # 全连接层,将特征维度从1024降到512
self.bn1 = nn.BatchNorm1d(512) # 批归一化层
self.drop1 = nn.Dropout(0.4) # Dropout层,丢弃率为40%
self.fc2 = nn.Linear(512, 256) # 全连接层,将特征维度从512降到256
self.bn2 = nn.BatchNorm1d(256) # 批归一化层
self.drop2 = nn.Dropout(0.5) # Dropout层,丢弃率为50%
self.fc3 = nn.Linear(256, num_class) # 最终分类层,输出维度为类别数
def forward(self, xyz):
B, _, _ = xyz.shape # 获取批次大小B
if self.normal_channel:
norm = xyz[:, 3:, :] # 提取特征信息(如法向量)
xyz = xyz[:, :3, :] # 提取位置信息(x, y, z坐标)
else:
norm = None
print(xyz.shape) # 输出位置信息的形状
print(norm.shape) # 输出特征信息的形状
# 第一个Set Abstraction层,得到新的点集和特征
l1_xyz, l1_points = self.sa1(xyz, norm)
# 第二个Set Abstraction层,进一步抽象特征
l2_xyz, l2_points = self.sa2(l1_xyz, l1_points)
# 第三个Set Abstraction层,提取全局特征
l3_xyz, l3_points = self.sa3(l2_xyz, l2_points)
# 将全局特征展平,准备进入全连接层
x = l3_points.view(B, 1024)
# 全连接层1,激活函数ReLU,批归一化和Dropout
x = self.drop1(F.relu(self.bn1(self.fc1(x))))
# 全连接层2,激活函数ReLU,批归一化和Dropout
x = self.drop2(F.relu(self.bn2(self.fc2(x))))
# 最终输出层,得到每个类别的分数
x = self.fc3(x)
# 对分数应用Log Softmax,得到对数概率
x = F.log_softmax(x, -1) # 分类任务,计算每个类别的概率
return x, l3_points # 返回分类结果和全局特征
特征提取主代码
完成self.sa1 = PointNetSetAbstractionMsg的一层,采样点的选择,已经对采样点画球或者附近采样点,多个半径拼接起来
class PointNetSetAbstractionMsg(nn.Module):
def forward(self, xyz, points):
"""
Input:
xyz: input points position data, [B, C, N] # B: batch size, C: 每个点的维度 (如 3 代表 x, y, z 坐标),N: 点的数量
points: input points data, [B, D, N] # D: 额外特征维度 (如点法向量特征),可以为 None 表示只有位置信息
Return:
new_xyz: sampled points position data, [B, C, S] # S: 采样后的点数量,最远距离采样后的点的位置信息
new_points_concat: sample points feature data, [B, D', S] # D': 经过多个卷积操作后的新特征维度
"""
xyz = xyz.permute(0, 2, 1) # 将输入 xyz 的维度从 [B, C, N] 变为 [B, N, C],方便后续处理,xyz 就是输入的点的位置
print(xyz.shape) # 输出维度为 ([4, 1024, 3]),4 个 batch,每个 batch 有 1024 个点,每个点 3 个维度 (x, y, z)
if points is not None:
points = points.permute(0, 2, 1) # 将 points 特征的维度也转换为 [B, N, D],以适配后续的操作
print(points.shape) # 输出 points 的维度 ([4, 1024, 3]),即每个点有额外的 3 维特征,如法向量
B, N, C = xyz.shape # B: batch size, N: 点的数量, C: 每个点的维度
S = self.npoint # 设定采样点的数量 S,这里指定为 512
# 使用最远距离采样算法,从输入的 N 个点中采样 S 个点,返回的是这些采样点的位置信息
new_xyz = index_points(xyz, farthest_point_sample(xyz, S))
print(new_xyz.shape) # 输出采样后的点位置的维度 ([4, 512, 3]),说明从 1024 个点采样到 512 个点,每个点有 3 维坐标
new_points_list = [] # 初始化一个空列表,用于存储不同半径的特征结果
for i, radius in enumerate(self.radius_list): # 遍历半径列表,采样的半径分别为 [0.1, 0.2, 0.4]
K = self.nsample_list[i] # 对应每个半径的采样点个数,分别为 [16, 32, 128]
# 基于球形区域查询点(radius 半径范围内的 K 个邻居点),返回的是邻居点的索引
group_idx = query_ball_point(radius, K, xyz, new_xyz)
# 根据返回的索引,从原始 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)
print(grouped_points.shape) # 输出拼接后的特征形状 ([4, 512, 16, 6]),6 是拼接后点的维度 (3 个法向量特征 + 3 个位置信息)
else:
grouped_points = grouped_xyz # 如果没有额外特征,则只使用位置信息
# 调整维度顺序以适应卷积层输入,转换为 [B, D, K, S] 格式
grouped_points = grouped_points.permute(0, 3, 2, 1)
print(grouped_points.shape) # 输出维度 ([4, 6, 16, 512]),这里 6 表示特征维度,16 是 K (邻居点数量),512 是采样点数量
for j in range(len(self.conv_blocks[i])): # 对 grouped_points 进行多个卷积操作
conv = self.conv_blocks[i][j]
bn = self.bn_blocks[i][j]
# 使用卷积和批归一化处理 grouped_points,激活函数使用 ReLU
grouped_points = F.relu(bn(conv(grouped_points)))
print(grouped_points.shape) # 输出经过卷积后的特征维度 ([4, 64, 16, 512]),特征维度从 6 增加到 64
# 在 K 个邻居点中,沿着特征维度 (即 max pooling 操作),提取每个邻居点的最大值,生成新的特征
new_points = torch.max(grouped_points, 2)[0] # [B, D', S] 这里的 D' 是新的特征维度
print(new_points.shape) # 输出新特征维度 ([4, 64, 512]),通过 max pooling 获得每个采样点的最终特征
new_points_list.append(new_points) # 将每个 radius 对应的特征加入列表,以便后续拼接
new_xyz = new_xyz.permute(0, 2, 1) # 重新将采样点位置信息调整为 [B, C, S] 格式
new_points_concat = torch.cat(new_points_list, dim=1) # 将不同半径提取的特征拼接在一起
print(new_points_concat.shape) # 最终特征维度为 ([4, 320, 512]),320 是三次卷积后的总特征长度 (64 + 64 + 192)
return new_xyz, new_points_concat # 返回新的采样点位置和拼接后的特征
第一步:采样点选取
farthest_point_sample
(远点采样)是一种用于点云数据的采样算法,它的主要目标是从原始点云中选择出一组均匀分布的点作为采样点,其实就是每次迭代计算距离的时候,都会选择离所有已选择点最远的点作为采样点。
# 来源 特征提取主代码 new_xyz = index_points(xyz, farthest_point_sample(xyz, S))
def farthest_point_sample(xyz, npoint):
"""
Input:
xyz: pointcloud data, [B, N, 3] # B: batch size, N: 点的数量,3: 每个点的维度 (x, y, z)
npoint: number of samples # 需要采样的点数量 S
Return:
centroids: sampled pointcloud index, [B, npoint] # 返回采样点的索引,形状为 [B, npoint]
"""
device = xyz.device # 获取输入 xyz 的设备类型(CPU 或 GPU)
B, N, C = xyz.shape # B: batch size, N: 点的数量,C: 每个点的维度 (x, y, z)
# 初始化采样点的索引存储,形状为 [B, npoint],数据类型为 long,并将其分配到设备上
centroids = torch.zeros(B, npoint, dtype=torch.long).to(device) # 采样点的索引 (初始为 0),大小为 [B, 512]
# 初始化距离矩阵,所有值为一个非常大的数 1e10,形状为 [B, N],表示每个点到最近已选采样点的距离
distance = torch.ones(B, N).to(device) * 1e10 # 距离矩阵,大小为 [B, 1024],初始值为无穷大
# 随机初始化最远点的索引,形状为 [B],即每个 batch 选择一个随机点作为第一个最远点
farthest = torch.randint(0, N, (B,), dtype=torch.long).to(device) # 在每个 batch 中随机选择一个点作为初始最远点
# 生成 batch 的索引 [0, 1, 2, ..., B-1],大小为 [B]
batch_indices = torch.arange(B, dtype=torch.long).to(device)
# 开始进行最远点采样,采样 npoint 次
for i in range(npoint):
# 将当前最远点的索引存入采样点的索引矩阵 centroids 中
centroids[:, i] = farthest # 记录每次采样得到的最远点索引
# 根据当前采样的最远点索引获取它的坐标,形状为 [B, 1, 3],即每个 batch 选择一个点
centroid = xyz[batch_indices, farthest, :].view(B, 1, 3)
# 计算所有点与当前采样点的欧氏距离,xyz 是 [B, N, 3],centroid 是 [B, 1, 3],所以广播计算后 dist 是 [B, N]
dist = torch.sum((xyz - centroid) ** 2, -1) # 欧式距离平方
# 比较当前采样点的距离与已记录的最短距离表 distance,mask 记录哪些点与当前采样点更近
mask = dist < distance # 布尔掩码,表示哪些点的距离比之前记录的最小距离更小
# 更新距离矩阵,将新的最短距离记录下来
distance[mask] = dist[mask] # 更新到最近采样点的最小距离
# 选择距离矩阵中距离最大的点作为新的最远点,即重新确定最远点
farthest = torch.max(distance, -1)[1] # 返回每个 batch 中距离最远的点的索引
return centroids # 返回采样点的索引
第二步:画球取邻居
# 来源 特征提取主代码 group_idx = query_ball_point(radius, K, xyz, new_xyz)
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] # 原始点云数据,形状为 [B, N, 3],B 是 batch size,N 是点的数量,3 是 (x, y, z) 坐标
new_xyz: query points, [B, S, 3] # 查询点数据,形状为 [B, S, 3],S 是需要采样的中心点数量
Return:
group_idx: grouped points index, [B, S, nsample] # 返回局部区域内的点索引,形状为 [B, S, nsample]
"""
device = xyz.device # 确保在与输入一致的设备上运行 (CPU/GPU)
B, N, C = xyz.shape # B 是 batch size, N 是点的数量, C 是点的维度 (3 表示 x, y, z)
_, S, _ = new_xyz.shape # S 是查询点的数量
# 初始化索引矩阵 group_idx,其大小为 [B, S, N],每个查询点对 N 个原始点的索引
group_idx = torch.arange(N, dtype=torch.long).to(device).view(1, 1, N).repeat([B, S, 1])
# [1, 1, N] -> [B, S, N] 生成每个 batch 和查询点的索引
# 计算查询点 new_xyz 与原始点 xyz 之间的平方欧氏距离,返回的形状是 [B, S, N]
sqrdists = square_distance(new_xyz, xyz) # 每个查询点与所有原始点之间的距离
# 对于距离大于 radius 的点,将其索引设置为 N (无效索引),表示这些点不在球形区域内
group_idx[sqrdists > radius ** 2] = N # 将距离超过半径的点的索引设置为 N(即 1024,表示不符合条件)
# 按距离升序排序,形状为 [B, S, N],因为已经将大于半径的点索引设为 N,这些点会排在最后
group_idx = group_idx.sort(dim=-1)[0][:, :, :nsample] # 截取最近的 nsample 个点的索引
# 处理极端情况:如果某个查询点在其半径内的点不足 nsample 个,则复制第一个点的索引以填满
group_first = group_idx[:, :, 0].view(B, S, 1).repeat([1, 1, nsample]) # 复制第一个有效的点索引,大小为 [B, S, nsample]
# 创建 mask,对于那些索引为 N 的位置(即没有足够点的地方),将其替换为第一个有效点的索引
mask = group_idx == N # 布尔掩码,标记哪些索引为 N 的点(即无效点)
group_idx[mask] = group_first[mask] # 将不足的点索引替换为第一个有效点的索引
return group_idx # 返回每个查询点局部区域内的点索引,大小为 [B, S, nsample]
第三步:卷积并且拼接特征返回
# 对 grouped_points 进行多个卷积操作也就是MLP, 初始化传入参数in_channel,[[32, 32, 64], [64, 64, 128], [64, 96, 128]]
for j in range(len(self.conv_blocks[i])):
conv = self.conv_blocks[i][j]
bn = self.bn_blocks[i][j]
new_xyz = new_xyz.permute(0, 2, 1) # 重新将采样点位置信息调整为 [B, C, S] 格式
new_points_concat = torch.cat(new_points_list, dim=1) # 将不同半径提取的特征拼接在一起
print(new_points_concat.shape) # 最终特征维度为 ([4, 320, 512]),320 是三个维度半径,组卷积后的总特征长度 (64 + 64 + 192)
以上三步是完成其中一次的特征提取,示例中
第一个Set Abstraction选择512个中心点(MSG),第二个Set Abstraction层特征提取的512个点中选择128个特征点进行特征提取(MSG)
第三个Set Abstraction层,使用单尺度分组(SSG)
MSG(Multi-Scale Grouping,多尺度聚合)和 SSG(Single-Scale Grouping,单尺度聚合)是两种不同的特征提取方法,用于处理点云数据。
局部分割任务
上面演示代码是PointNet++应用于分类任务,分割任务中的应用与分类任务不同,在于分割任务是要对图中的每一个点都进行分类,而不能像分类任务一样,对于一个图形的所有特征点的进行分类,局部分割则是要对一个物体的不同组成部分进行分类,这也意味着必须对每一个点都进行分类,但是经过PointNet++提取特征后,相当于做了一个下采样,特征点个数减少了,分割任务则需要通过上采样,将特征点个数恢复过来,在进行N*M的分类,N是特征点个数,M是属于每个部件的概率。
架构图
从图中可以看出,前面提取特征的过程都是一样的,只是在进行分割前进行插值,不是简单的插值,实际上在插值的过程,将特征提取过程中的中间结果也加入到了升维后的特征中,这样很大程度增加了特征信息,类似于Unet中的升维,和残差连接的操作,都是为了使特征更加丰富。
主要架构代码
class get_model(nn.Module):
def __init__(self, num_classes, normal_channel=False):
super(get_model, self).__init__()
# 如果使用法向量作为输入通道,增加3个通道
if normal_channel:
additional_channel = 3
else:
additional_channel = 0
self.normal_channel = normal_channel
# 第一个多尺度分组抽象层,采样512个点,使用三种半径 [0.1, 0.2, 0.4],采样点个数为 [32, 64, 128]
self.sa1 = PointNetSetAbstractionMsg(512, [0.1, 0.2, 0.4], [32, 64, 128], 3+additional_channel,
[[32, 32, 64], [64, 64, 128], [64, 96, 128]])
# 第二个多尺度分组抽象层,采样128个点,使用两种半径 [0.4, 0.8],采样点个数为 [64, 128]
self.sa2 = PointNetSetAbstractionMsg(128, [0.4, 0.8], [64, 128], 128+128+64,
[[128, 128, 256], [128, 196, 256]])
# 第三个单尺度分组抽象层,全局抽象,输入通道为 512 + 3(点的坐标+特征),MLP的输出通道为 [256, 512, 1024]
self.sa3 = PointNetSetAbstraction(npoint=None, radius=None, nsample=None, in_channel=512 + 3,
mlp=[256, 512, 1024], group_all=True)
# 特征传播层,输入通道1536,MLP输出通道为 [256, 256]
self.fp3 = PointNetFeaturePropagation(in_channel=1536, mlp=[256, 256])
# 特征传播层,输入通道576,MLP输出通道为 [256, 128]
self.fp2 = PointNetFeaturePropagation(in_channel=576, mlp=[256, 128])
# 特征传播层,输入通道150+additional_channel(如果有法向量,则需要加额外的通道),MLP输出通道为 [128, 128]
self.fp1 = PointNetFeaturePropagation(in_channel=150+additional_channel, mlp=[128, 128])
# 全连接层,1x1卷积,输入通道128,输出通道128
self.conv1 = nn.Conv1d(128, 128, 1)
self.bn1 = nn.BatchNorm1d(128)
# Dropout层,设置0.5的丢弃率
self.drop1 = nn.Dropout(0.5)
# 最后一层全连接层,1x1卷积,将128通道压缩为类别数
self.conv2 = nn.Conv1d(128, num_classes, 1)
def forward(self, xyz, cls_label):
# 输入点云数据的维度为 [B, C, N],其中 B 表示批次大小,C 表示通道数,N 表示点的数量
B, C, N = xyz.shape
# 如果使用法向量作为输入通道,l0_points 包含法向量和坐标,l0_xyz 只包含坐标
if self.normal_channel:
l0_points = xyz # l0_points 是原始点云数据
l0_xyz = xyz[:, :3, :] # l0_xyz 只包含点的坐标 (前三个通道)
else:
l0_points = xyz # 如果没有法向量,l0_points 只有位置信息
l0_xyz = xyz # l0_xyz 也是只有位置信息
# 第一次特征提取,抽象后的点云和特征
l1_xyz, l1_points = self.sa1(l0_xyz, l0_points)
# 第二次特征提取,进一步抽象
l2_xyz, l2_points = self.sa2(l1_xyz, l1_points)
# 第三次特征提取,全局抽象
l3_xyz, l3_points = self.sa3(l2_xyz, l2_points)
# 特征传播层,将 l3 层的特征传播到 l2
l2_points = self.fp3(l2_xyz, l3_xyz, l2_points, l3_points)
# 将 l2 层的特征传播到 l1
l1_points = self.fp2(l1_xyz, l2_xyz, l1_points, l2_points)
# 将类别标签进行 one-hot 编码,并扩展维度以匹配点数
cls_label_one_hot = cls_label.view(B, 16, 1).repeat(1, 1, N)
# 将类别标签与原始点云拼接,传递到 l0
l0_points = self.fp1(l0_xyz, l1_xyz, torch.cat([cls_label_one_hot, l0_xyz, l0_points], 1), l1_points)
# 使用全连接层进行特征处理
feat = F.relu(self.bn1(self.conv1(l0_points)))
# Dropout 处理,防止过拟合
x = self.drop1(feat)
# 最后一层全连接层,得到每个点的分类结果
x = self.conv2(x)
# 使用 softmax 获取每个点属于不同类别的概率
x = F.log_softmax(x, dim=1)
# 调整输出的维度以符合 [B, N, num_classes]
x = x.permute(0, 2, 1)
# 返回分类结果和 l3 层的特征
return x, l3_points
核心升维代码
在第一次插值的时候,由于特征点较少,直接将特征点复制,并且加入特征提取阶段的值,进行插值。
第二次和第三次则采用初始化需要插值点周围点的权重矩阵(类似于高斯分布),根据权重矩阵和周围点计算得到的中心点的值,同时加入特征提取阶段的中间值,进行插值。
插值过程中也会进行卷积,对特征进行提取。
def forward(self, xyz1, xyz2, points1, points2):
"""
前向传播函数,用于将特征从采样点 (xyz2, points2) 插值到原始点 (xyz1) 上。
Input:
xyz1: 原始点云的坐标数据,形状为 [B, C, N]
xyz2: 采样点云的坐标数据,形状为 [B, C, S]
points1: 原始点云的特征数据,形状为 [B, D, N]
points2: 采样点云的特征数据,形状为 [B, D, S]
Return:
new_points: 插值后的点云特征数据,形状为 [B, D', N]
"""
# 转换维度以适应后续操作
xyz1 = xyz1.permute(0, 2, 1) # [B, N, C]
xyz2 = xyz2.permute(0, 2, 1) # [B, S, C]
print(xyz1.shape) # 打印原始点云的形状
print(xyz2.shape) # 打印采样点云的形状
points2 = points2.permute(0, 2, 1) # [B, S, D]
print(points2.shape) # 打印采样点云特征的形状
B, N, C = xyz1.shape # 批量大小,点的数量,通道数
_, S, _ = xyz2.shape # 采样点的数量
if S == 1:
# 如果采样点只有一个,将其特征重复以匹配原始点云的点数
interpolated_points = points2.repeat(1, N, 1)
print(interpolated_points.shape) # 打印插值后点云特征的形状
else:
# 计算原始点云和采样点云之间的平方距离
dists = square_distance(xyz1, xyz2)
print(dists.shape) # 打印距离矩阵的形状
# 对距离进行排序,选择最近的3个点
dists, idx = dists.sort(dim=-1)
dists, idx = dists[:, :, :3], idx[:, :, :3] # [B, N, 3]
# 计算距离的倒数,得到权重
dist_recip = 1.0 / (dists + 1e-8) # 防止除零错误
norm = torch.sum(dist_recip, dim=2, keepdim=True)
weight = dist_recip / norm
print(weight.shape) # 打印权重的形状
# 根据权重加权平均计算插值特征
interpolated_points = torch.sum(index_points(points2, idx) * weight.view(B, N, 3, 1), dim=2)
print(interpolated_points.shape) # 打印插值后的特征形状
if points1 is not None:
# 如果有原始点云特征,将其与插值后的特征拼接
points1 = points1.permute(0, 2, 1) # [B, N, D]
new_points = torch.cat([points1, interpolated_points], dim=-1) # 拼接特征
else:
new_points = interpolated_points # 仅使用插值后的特征
print(new_points.shape) # 打印最终特征的形状
new_points = new_points.permute(0, 2, 1) # 转换回 [B, D', N] 形状
print(new_points.shape) # 打印处理后的特征形状
# 使用多个卷积层处理特征
for i, conv in enumerate(self.mlp_convs):
bn = self.mlp_bns[i]
new_points = F.relu(bn(conv(new_points)))
print(new_points.shape) # 打印经过卷积和激活函数处理后的特征形状
return new_points
最终完成特征点插值之后,将特征点恢复为原来输入的特征点个数,然后连接softmax进行分类即可,例如一个batch输入为(1024,6)(6,前三个是位置信息,后三个为特征向量信息),最终插值后结果为(1024,M),其中M为局部分类的类别数。