准备好探索3D分割的世界吧!让我们一起完成PointNet的旅程,探索一种理解3D形状的超酷方式。PointNet就像是计算机观察3D物体的智能工具,特别是对于那些在空间中漂浮的点云。与其他方法不同,PointNet直接处理这些点,不需要将它们强行转换成网格或图片。
在本文中,我们将以简单易懂的方式介绍PointNet。我们将从核心思想出发,通过Python和PyTorch的编程实践来进行3D分割。但在我们深入探讨这个有趣的主题之前,我们需要先了解一下PointNet的基本概念 —— 它是如何成为解决识别3D物体(及其部分)的重要工具的。
现在,我们一起来看一下PointNet论文的总结。我们将讨论其设计思路、背后的概念和实验,以及PointNet在实际应用中的表现。我们将以简洁明了的方式展示随机点、特殊函数以及PointNet在处理不同的3D任务时的优势。
PointNet就像是一种特殊的工具,它帮助计算机理解3D物体,特别是那些棘手的点云数据。但是,是什么让它如此炫酷呢?与其他整理数据的方法不同,PointNet直接使用点云数据本身,无需网格或图片。这使得它在3D视觉领域脱颖而出。
点集的基础知识:
想象一堆点在3D空间中漂浮。这些点没有特定的顺序,它们相互作用。PointNet通过对其旋转或移动等变化简单地处理这种随机性。当这些点的位置转换时,不会令人困惑。
PointNet的特殊能力:对称魔术
PointNet具有一种特殊的能力,称为对称性。想象一下,你有一堆点,无论你如何洗牌,PointNet仍然能够理解其中的内容。对于不遵循特定顺序的点来说,这就像是一种魔法。
收集局部和全局信息
PointNet在收集信息方面很聪明。它可以同时关注点的整体情况(全局)和细节(局部)。这有助于它完成诸如确定物体形状和其部分的任务。
对齐技巧
PointNet也擅长处理变化。如果你旋转或移动点,PointNet可以自动调整并正常理解事物。就像一个将物体对齐以清晰看到它们的机器人。
现在,让我们谈谈PointNet背后的一些大背景思想。有两个特殊的定理表明PointNet不仅在实践中很酷,而且在理论上也是一个明智的选择。
1)通用逼近:
PointNet可以学会很好地理解任何3D形状。就好像说PointNet是一个超级英雄,可以处理你投入其中的任何形状。
2)瓶颈维度和稳定性:
-PointNet是坚固的。即使你添加了一些额外的点或修改了已有的点,它也不会困惑。它坚持自己的工作,并保持稳定。
这样的大背景思想使PointNet成为理解3D形状的值得信赖的工具。
PointNet体系结构由两个主要组件组成:分类网络和扩展分割网络。
分类网络接收n个输入点,应用输入和特征变换,并通过最大池化聚合点特征。它生成k个类别的分类分数。分割网络是分类网络的自然扩展,将全局和局部特征组合起来生成每个点的分数。术语“mlp”表示多层感知器,其层大小在方括号中指定。批量归一化一致应用于所有层,并使用ReLU激活函数。此外,dropout层还巧妙地加入到分类网络的最终mlp中。
在提供的代码片段中,该类封装了对批量归一化卷积层输出应用ReLU激活函数的操作。这与体系结构图中描述的卷积层和mlp层相对应。让我们仔细看看代码:MLP_CONV
# Multi Layer Perceptron
class MLP_CONV(nn.Module):
def __init__(self, input_size, output_size):
super().__init__()
self.input_size = input_size
self.output_size = output_size
self.conv = nn.Conv1d(self.input_size, self.output_size, 1)
self.bn = nn.BatchNorm1d(self.output_size)
def forward(self, input):
return F.relu(self.bn(self.conv(input)))
该类定义对应于体系结构的构建块,其中卷积层、批量归一化和ReLU激活被组合在一起以实现所需的特征变换。此外,当使用全连接层时,下面描述的这个类可以为该体系结构提供补充。FC_BN
# Fully Connected with Batch Normalization
class FC_BN(nn.Module):
def __init__(self, input_size, output_size):
super().__init__()
self.input_size = input_size
self.output_size = output_size
self.lin = nn.Linear(self.input_size, self.output_size)
self.bn = nn.BatchNorm1d(self.output_size)
def forward(self, input):
return F.relu(self.bn(self.lin(input)))
该类进一步说明了如何将全连接层与批量归一化和ReLU激活相结合,强调了在PointNet体系结构中一致应用这些技术的重要性。
输入变换网络,也称为TNet(小型PointNet),在处理原始点云方面起到了关键作用。它通过一系列操作旨在回归到一个3×3的矩阵。网络的架构由一个共享的MLP(64,128,1024)对每个点应用,然后通过点进行最大池化,再经过两个输出大小为512和256的全连接层。生成的矩阵初始化为单位矩阵。除最后一层外,每个层都使用ReLU激活和批量归一化。第二个变换网络与第一个的架构相同,但输出一个64×64的矩阵,同样被初始化为单位矩阵。为了促进正交性,对softmax分类损失添加了一个权重为0.001的正则化损失。
该类用于根据论文中提供的规格创建变换网络:TNet。
# Transformation Network (TNet) class
class TNet(nn.Module):
def __init__(self, k=3):
super().__init__()
self.k = k
self.mlp1 = MLP_CONV(self.k, 64)
self.mlp2 = MLP_CONV(64, 128)
self.mlp3 = MLP_CONV(128, 1024)
self.fc_bn1 = FC_BN(1024, 512)
self.fc_bn2 = FC_BN(512, 256)
self.fc3 = nn.Linear(256, k*k)
def forward(self, input):
# input.shape == (batch_size, n, 3)
bs = input.size(0)
xb = self.mlp1(input)
xb = self.mlp2(xb)
xb = self.mlp3(xb)
pool = nn.MaxPool1d(xb.size(-1))(xb)
flat = nn.Flatten(1)(pool)
xb = self.fc_bn1(flat)
xb = self.fc_bn2(xb)
# initialize as identity
init = torch.eye(self.k, requires_grad=True).repeat(bs, 1, 1)
if xb.is_cuda:
init = init.cuda()
matrix = self.fc3(xb).view(-1, self.k, self.k) + init
return matrix
该类封装了将输入点云转换为3×3或64×64矩阵的过程,利用了共享的MLP、最大池化和带有批量归一化的全连接层.TNet
PointNet网络,封装在这个类中,遵循了PointNet架构图中的设计原则:PointNet
class PointNet(nn.Module):
def __init__(self):
super().__init__()
self.input_transform = TNet(k=3)
self.feature_transform = TNet(k=64)
self.mlp1 = MLP_CONV(3, 64)
self.mlp2 = MLP_CONV(64, 128)
# 1D convolutional layer with kernel size 1
self.conv = nn.Conv1d(128, 1024, 1)
# Batch normalization for stability and faster training
self.bn = nn.BatchNorm1d(1024)
def forward(self, input):
n_pts = input.size()[2]
matrix3x3 = self.input_transform(input)
input_transform_output = torch.bmm(torch.transpose(input, 1, 2), matrix3x3).transpose(1, 2)
x = self.mlp1(input_transform_output)
matrix64x64 = self.feature_transform(x)
feature_transform_output = torch.bmm(torch.transpose(x, 1, 2), matrix64x64).transpose(1, 2)
x = self.mlp2(feature_transform_output)
x = self.bn(self.conv(x))
global_feature = nn.MaxPool1d(x.size(-1))(x)
global_feature_repeated = nn.Flatten(1)(global_feature).repeat(n_pts, 1, 1).transpose(0, 2).transpose(0, 1)
return [feature_transform_output, global_feature_repeated], matrix3x3, matrix64x64
这个PointNet实现无缝地集成了TNet转换网络、多层感知器(MLP)和带有批量归一化的一维卷积层。前向传播处理输入和特征变换,然后提取全局特征。生成的张量连同变换矩阵一起作为输出返回.MLP_CONV
分割网络是在分类的PointNet基础上扩展而来的。每个点的局部点特征来自第二个转换网络和最大池化的全局特征,这些特征被串联起来。分割网络中不使用dropout,并且训练参数与分类网络保持一致。
对于形状部分分割,修改包括添加一个指示输入类别的独热向量,与最大池化层的输出连接。某些层中的神经元数量增加,添加了跳跃连接来收集不同层中的局部点特征,并将它们串联起来形成分割网络的点特征输入。
class PointNetSeg(nn.Module):
def __init__(self, classes=3):
super().__init__()
self.pointnet = PointNet()
self.mlp1 = MLP_CONV(1088, 512)
self.mlp2 = MLP_CONV(512, 256)
self.mlp3 = MLP_CONV(256, 128)
self.conv = nn.Conv1d(128, classes, 1)
self.logsoftmax = nn.LogSoftmax(dim=1)
def forward(self, input):
inputs, matrix3x3, matrix64x64 = self.pointnet(input)
stack = torch.cat(inputs, 1)
x = self.mlp1(stack)
x = self.mlp2(x)
x = self.mlp3(x)
output = self.conv(x)
return self.logsoftmax(output), matrix3x3, matrix64x64
在这个类中,前向传播将从PointNet中获取的特征进行串联,然后通过一系列的多层感知器(MLP)和卷积层进行传递。在应用LogSoftmax激活函数之后,得到最终输出.PointNetSegMLP_CONV
在我们的模型训练过程中,我们利用了著名的Semantic-Kitti数据集中的点云来发挥PointNet的威力。这个有影响力的数据集捕捉了各种城市场景,最初包含大约30个标签。然而,为了我们的目的,我们谨慎地将它们重新映射成三个类别:
· 可通行:包括道路、停车场、人行道等。
· 不可通行:包括汽车、卡车、栅栏、树木、人和各种物体。
· 未知:保留给异常值。
重新映射的过程涉及使用键值字典将原始标签转换为简化的标签。为了可视化着色的点云,我们使用了Open3D Python包。左图展示了Semantic-Kitti原始的颜色方案,而右图显示了重新映射的颜色方案。
您可以在这里找到用于加载和可视化数据的代码。(https://github.com/sepideh-shamsizadeh/3DP-Point-Cloud-Segmentation/blob/1d3a874919988c2c508ac64934566fa02f1060ce/data_processing.py)
在准备数据的关键步骤中,我们需要通过自定义转换进行归一化和张量转换。主要使用了两种转换操作:
归一化(Normalize):该操作将点云进行归中处理,通过减去其均值并进行缩放,以确保最大范数为单位。
class Normalize(object):
def __call__(self, pointcloud):
assert len(pointcloud.shape)==2
norm_pointcloud = pointcloud - np.mean(pointcloud, axis=0)
norm_pointcloud /= np.max(np.linalg.norm(norm_pointcloud, axis=1))
return norm_pointcloud
ToTensor:此转换将点云转换为 PyTorch 张量。
class ToTensor(object):
def __call__(self, pointcloud):
assert len(pointcloud.shape)==2
return torch.from_numpy(pointcloud)
这些转换的组合被封装在函数 default_transforms() 中。
然后,我们创建了一个自定义数据集 PointCloudDataset,扩展了 PyTorch 的类。该数据集表示用于训练和测试的点云集合。其结构包括:
- 使用数据集详细信息和可选的转换函数进行初始化。
- 定义数据集的长度。
- 检索一个数据项,并在指定的情况下应用转换。
class PointCloudData(Dataset):
def __init__(self, dataset_path, transform=default_transforms(), start=0, end=1000):
"""
INPUT
dataset_path: path to the dataset folder
transform : transform function to apply to point cloud
start : index of the first file that belongs to dataset
end : index of the first file that do not belong to dataset
"""
self.dataset_path = dataset_path
self.transforms = transform
self.pc_path = os.path.join(self.dataset_path, "sequences", "00", "velodyne")
self.lb_path = os.path.join(self.dataset_path, "sequences", "00", "labels")
self.pc_paths = os.listdir(self.pc_path)
self.lb_paths = os.listdir(self.lb_path)
assert(len(self.pc_paths) == len(self.lb_paths))
self.start = start
self.end = end
# clip paths according to the start and end ranges provided in input
self.pc_paths = self.pc_paths[start: end]
self.lb_paths = self.lb_paths[start: end]
def __len__(self):
return len(self.pc_paths)
def __getitem__(self, idx):
item_name = str(idx + self.start).zfill(6)
pcpath = os.path.join(self.pc_path, item_name + ".bin")
lbpath = os.path.join(self.lb_path, item_name + ".label")
# load points and labels
pointcloud, labels = readpc(pcpath, lbpath)
# transform
torch_pointcloud = torch.from_numpy(pointcloud)
torch_labels = torch.from_numpy(labels)
return torch_pointcloud, torch_labels
有了数据集类,我们实例化了训练、验证和测试数据集。这不仅提供了有结构的组织,还为使用 PyTorch 的 DataLoader 模块提供了高效的基础。
train_ds = PointCloudData(dataset_path, start=0, end=100)
val_ds = PointCloudData(dataset_path, start=100, end=120)
test_ds = PointCloudData(dataset_path, start=120, end=150)
利用 PyTorch 的 DataLoader 的功能,我们可以实现批处理、随机化和并行加载等功能。
train_loader = DataLoader(dataset=train_ds, batch_size=5, shuffle=True)
val_loader = DataLoader(dataset=val_ds, batch_size=5, shuffle=False)
test_loader = DataLoader(dataset=test_ds, batch_size=1, shuffle=False)
这种对数据集创建和加载的细致处理不仅对于基本问题有好处,而且在数据集和训练过程的复杂性增加时变得不可或缺。它为训练和测试过程中的高效、可扩展和并行化数据处理奠定了基础。
在神经网络训练领域,损失函数在引导模型参数更新方面起着至关重要的作用。我们的 PointNet 模型采用了一个精心设计的损失函数,受以下论文中提供的见解的影响:
“我们在 softmax 分类损失上添加了一个正则化损失(权重为 0.001),使得矩阵接近正交。”
该损失函数的代码表达如下:
def pointNetLoss(outputs, labels, m3x3, m64x64, alpha=0.0001):
criterion = torch.nn.NLLLoss()
bs = outputs.size(0)
id3x3 = torch.eye(3, requires_grad=True).repeat(bs, 1, 1)
id64x64 = torch.eye(64, requires_grad=True).repeat(bs, 1, 1)
# Check if outputs are on CUDA
if outputs.is_cuda:
id3x3 = id3x3.cuda()
id64x64 = id64x64.cuda()
# Calculate matrix differences
diff3x3 = id3x3 - torch.bmm(m3x3, m3x3.transpose(1, 2))
diff64x64 = id64x64 - torch.bmm(m64x64, m64x64.transpose(1, 2))
# Compute the loss
return criterion(outputs, labels) + alpha * (torch.norm(diff3x3) + torch.norm(diff64x64)) / float(bs)
· outputs:模型的预测结果。
· labels:真实标签。
· m3x3 和 m64x64:来自 PointNet 转换网络的矩阵。
· alpha:正则化项的权重。
这个损失函数将标准的负对数似然(NLL)损失与正则化项相结合。正则化项惩罚了转换矩阵与正交性的偏差,符合论文中对于实现正交性的强调。
通过精心设计,我们的 PointNet 模型不仅在分类精度上表现出色,而且符合结构约束,在训练过程中增强了其鲁棒性和泛化能力。
训练循环是一个顺序过程,迭代地更新 PointNet 模型的权重。它由一定数量的 epochs 组成,每个 epochs 包括一个训练阶段和一个可选的验证阶段。在这些阶段中,模型在训练和评估状态之间交替。
def train(pointnet, optimizer, train_loader, val_loader=None, epochs=15, save=True):
best_val_acc = -1.0
for epoch in range(epochs):
pointnet.train()
running_loss = 0.0
# Training phase
for i, data in enumerate(train_loader, 0):
inputs, labels = data
inputs = inputs.to(device).float()
labels = labels.to(device)
optimizer.zero_grad()
outputs, m3x3, m64x64 = pointnet(inputs.transpose(1, 2))
loss = pointNetLoss(outputs, labels, m3x3, m64x64)
loss.backward()
optimizer.step()
running_loss += loss.item()
if i % 10 == 9 or True:
print('[%d, %5d] loss: %.3f' % (epoch + 1, i + 1, running_loss / 10))
running_loss = 0.0
# Validation phase
pointnet.eval()
correct = total = 0
with torch.no_grad():
for data in val_loader:
inputs, labels = data
inputs = inputs.to(device).float()
labels = labels.to(device)
outputs, __, __ = pointnet(inputs.transpose(1, 2))
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0) * labels.size(1)
correct += (predicted == labels).sum().item()
print("correct", correct, "/", total)
val_acc = 100.0 * correct / total
print('Valid accuracy: %d %%' % val_acc)
# Save the model if current validation accuracy surpasses the best
if save and val_acc > best_val_acc:
best_val_acc = val_acc
path = os.path.join('', "MyDrive", "pointnetmodel.yml")
print("best_val_acc:", val_acc, "saving model at", path)
torch.save(pointnet.state_dict(), path)
# Initialize the optimizer
optimizer = torch.optim.Adam(pointnet.parameters(), lr=0.005)
# Commence the training
train(pointnet, optimizer, train_loader, val_loader, save=True)
该循环作为一个系统化的框架,用于在多次迭代中更新模型参数、监控损失并评估性能。
该函数旨在分析模型在测试阶段的性能。它计算真实标签中不同类别的出现次数(unk,trav,nontrav),计算预测的总数,并统计正确预测的数量。结果以一个元组的形式返回.compute_statsunktravnontrav(correct, total_predictions)
。
def compute_stats(true_labels, pred_labels):
unk = np.count_nonzero(true_labels == 0)
trav = np.count_nonzero(true_labels == 1)
nontrav = np.count_nonzero(true_labels == 2)
total_predictions = labels.shape[1]*labels.shape[0]
correct = (true_labels == pred_labels).sum().item()
return correct, total_predictions
PointNet 是一种突破性的用于 3D 分割的工具,克服了无序点集带来的挑战。其理论基础、架构设计和实际实现展示了其多功能性和可靠性。通过将理论与实践相结合,我们揭开了理解和利用 PointNet 进行 3D 分割的过程的神秘面纱。PyTorch 和 Python 的整合为在实际应用中探索 PointNet 的潜力提供了一个实用的框架。你可以在我的 GitHub 上找到所有的代码。
值得一提的是,这个项目是我在帕多瓦大学攻读硕士学位期间 3D 视觉课程的关键部分。大部分代码是由课程导师 Alberto Pretto 教授慷慨提供的。我负责完成代码,并添加了解释,以简化对于实现 PointNet 网络用于 3D 分割的过程感兴趣的人们。我真诚希望你们能够从这个指南中获得有益和愉快的体验。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。