前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >解码PointNet:使用Python和PyTorch进行3D分割的实用指南

解码PointNet:使用Python和PyTorch进行3D分割的实用指南

原创
作者头像
一点人工一点智能
发布2023-12-29 09:37:26
6310
发布2023-12-29 09:37:26
举报

准备好探索3D分割的世界吧!让我们一起完成PointNet的旅程,探索一种理解3D形状的超酷方式。PointNet就像是计算机观察3D物体的智能工具,特别是对于那些在空间中漂浮的点云。与其他方法不同,PointNet直接处理这些点,不需要将它们强行转换成网格或图片。

在本文中,我们将以简单易懂的方式介绍PointNet。我们将从核心思想出发,通过Python和PyTorch的编程实践来进行3D分割。但在我们深入探讨这个有趣的主题之前,我们需要先了解一下PointNet的基本概念 —— 它是如何成为解决识别3D物体(及其部分)的重要工具的。

现在,我们一起来看一下PointNet论文的总结。我们将讨论其设计思路、背后的概念和实验,以及PointNet在实际应用中的表现。我们将以简洁明了的方式展示随机点、特殊函数以及PointNet在处理不同的3D任务时的优势。

01  理解PointNet:核心概念

PointNet就像是一种特殊的工具,它帮助计算机理解3D物体,特别是那些棘手的点云数据。但是,是什么让它如此炫酷呢?与其他整理数据的方法不同,PointNet直接使用点云数据本身,无需网格或图片。这使得它在3D视觉领域脱颖而出。

点集的基础知识:

想象一堆点在3D空间中漂浮。这些点没有特定的顺序,它们相互作用。PointNet通过对其旋转或移动等变化简单地处理这种随机性。当这些点的位置转换时,不会令人困惑。

PointNet的特殊能力:对称魔术

PointNet具有一种特殊的能力,称为对称性。想象一下,你有一堆点,无论你如何洗牌,PointNet仍然能够理解其中的内容。对于不遵循特定顺序的点来说,这就像是一种魔法。

收集局部和全局信息

PointNet在收集信息方面很聪明。它可以同时关注点的整体情况(全局)和细节(局部)。这有助于它完成诸如确定物体形状和其部分的任务。

对齐技巧

PointNet也擅长处理变化。如果你旋转或移动点,PointNet可以自动调整并正常理解事物。就像一个将物体对齐以清晰看到它们的机器人。

1.1 理论的魔力

现在,让我们谈谈PointNet背后的一些大背景思想。有两个特殊的定理表明PointNet不仅在实践中很酷,而且在理论上也是一个明智的选择。

1)通用逼近:

PointNet可以学会很好地理解任何3D形状。就好像说PointNet是一个超级英雄,可以处理你投入其中的任何形状。

2)瓶颈维度和稳定性:

-PointNet是坚固的。即使你添加了一些额外的点或修改了已有的点,它也不会困惑。它坚持自己的工作,并保持稳定。

这样的大背景思想使PointNet成为理解3D形状的值得信赖的工具。

1.2 PointNet体系结构概述

PointNet体系结构由两个主要组件组成:分类网络和扩展分割网络。

分类网络接收n个输入点,应用输入和特征变换,并通过最大池化聚合点特征。它生成k个类别的分类分数。分割网络是分类网络的自然扩展,将全局和局部特征组合起来生成每个点的分数。术语“mlp”表示多层感知器,其层大小在方括号中指定。批量归一化一致应用于所有层,并使用ReLU激活函数。此外,dropout层还巧妙地加入到分类网络的最终mlp中。

图片
图片

在提供的代码片段中,该类封装了对批量归一化卷积层输出应用ReLU激活函数的操作。这与体系结构图中描述的卷积层和mlp层相对应。让我们仔细看看代码:MLP_CONV

代码语言:javascript
复制
# 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

代码语言:javascript
复制
# 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体系结构中一致应用这些技术的重要性。

1.3 输入和特征变换

输入变换网络,也称为TNet(小型PointNet),在处理原始点云方面起到了关键作用。它通过一系列操作旨在回归到一个3×3的矩阵。网络的架构由一个共享的MLP(64,128,1024)对每个点应用,然后通过点进行最大池化,再经过两个输出大小为512和256的全连接层。生成的矩阵初始化为单位矩阵。除最后一层外,每个层都使用ReLU激活和批量归一化。第二个变换网络与第一个的架构相同,但输出一个64×64的矩阵,同样被初始化为单位矩阵。为了促进正交性,对softmax分类损失添加了一个权重为0.001的正则化损失。

该类用于根据论文中提供的规格创建变换网络:TNet。

代码语言:javascript
复制
# 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

1.4 PointNet网络

PointNet网络,封装在这个类中,遵循了PointNet架构图中的设计原则:PointNet

代码语言:javascript
复制
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

1.5 PointNet分割网络

分割网络是在分类的PointNet基础上扩展而来的。每个点的局部点特征来自第二个转换网络和最大池化的全局特征,这些特征被串联起来。分割网络中不使用dropout,并且训练参数与分类网络保持一致。

对于形状部分分割,修改包括添加一个指示输入类别的独热向量,与最大池化层的输出连接。某些层中的神经元数量增加,添加了跳跃连接来收集不同层中的局部点特征,并将它们串联起来形成分割网络的点特征输入。

代码语言:javascript
复制
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

02  训练和测试PointNet模型

在我们的模型训练过程中,我们利用了著名的Semantic-Kitti数据集中的点云来发挥PointNet的威力。这个有影响力的数据集捕捉了各种城市场景,最初包含大约30个标签。然而,为了我们的目的,我们谨慎地将它们重新映射成三个类别:

· 可通行:包括道路、停车场、人行道等。

· 不可通行:包括汽车、卡车、栅栏、树木、人和各种物体。

· 未知:保留给异常值。

重新映射的过程涉及使用键值字典将原始标签转换为简化的标签。为了可视化着色的点云,我们使用了Open3D Python包。左图展示了Semantic-Kitti原始的颜色方案,而右图显示了重新映射的颜色方案。

图片
图片

您可以在这里找到用于加载和可视化数据的代码。(https://github.com/sepideh-shamsizadeh/3DP-Point-Cloud-Segmentation/blob/1d3a874919988c2c508ac64934566fa02f1060ce/data_processing.py)

2.1 数据转换

在准备数据的关键步骤中,我们需要通过自定义转换进行归一化和张量转换。主要使用了两种转换操作:

归一化(Normalize):该操作将点云进行归中处理,通过减去其均值并进行缩放,以确保最大范数为单位。

代码语言:javascript
复制
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 张量。

代码语言:javascript
复制
class ToTensor(object):
    def __call__(self, pointcloud):
        assert len(pointcloud.shape)==2

        return torch.from_numpy(pointcloud)

这些转换的组合被封装在函数 default_transforms() 中。

2.2 点云数据集

然后,我们创建了一个自定义数据集 PointCloudDataset,扩展了 PyTorch 的类。该数据集表示用于训练和测试的点云集合。其结构包括:

- 使用数据集详细信息和可选的转换函数进行初始化。

- 定义数据集的长度。

- 检索一个数据项,并在指定的情况下应用转换。

代码语言:javascript
复制
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

2.3 数据集创建

有了数据集类,我们实例化了训练、验证和测试数据集。这不仅提供了有结构的组织,还为使用 PyTorch 的 DataLoader 模块提供了高效的基础。

代码语言:javascript
复制
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)

2.4 DataLoader 的使用

利用 PyTorch 的 DataLoader 的功能,我们可以实现批处理、随机化和并行加载等功能。

代码语言:javascript
复制
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)

这种对数据集创建和加载的细致处理不仅对于基本问题有好处,而且在数据集和训练过程的复杂性增加时变得不可或缺。它为训练和测试过程中的高效、可扩展和并行化数据处理奠定了基础。

2.5 损失函数

在神经网络训练领域,损失函数在引导模型参数更新方面起着至关重要的作用。我们的 PointNet 模型采用了一个精心设计的损失函数,受以下论文中提供的见解的影响:

“我们在 softmax 分类损失上添加了一个正则化损失(权重为 0.001),使得矩阵接近正交。”

该损失函数的代码表达如下:

代码语言:javascript
复制
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)

2.6 细分各个组件

· outputs:模型的预测结果。

· labels:真实标签。

· m3x3 和 m64x64:来自 PointNet 转换网络的矩阵。

· alpha:正则化项的权重。

这个损失函数将标准的负对数似然(NLL)损失与正则化项相结合。正则化项惩罚了转换矩阵与正交性的偏差,符合论文中对于实现正交性的强调。

通过精心设计,我们的 PointNet 模型不仅在分类精度上表现出色,而且符合结构约束,在训练过程中增强了其鲁棒性和泛化能力。

2.7 训练循环

训练循环是一个顺序过程,迭代地更新 PointNet 模型的权重。它由一定数量的 epochs 组成,每个 epochs 包括一个训练阶段和一个可选的验证阶段。在这些阶段中,模型在训练和评估状态之间交替。

代码语言:javascript
复制
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)

该循环作为一个系统化的框架,用于在多次迭代中更新模型参数、监控损失并评估性能。

2.8 测试

该函数旨在分析模型在测试阶段的性能。它计算真实标签中不同类别的出现次数(unk,trav,nontrav),计算预测的总数,并统计正确预测的数量。结果以一个元组的形式返回.compute_statsunktravnontrav(correct, total_predictions)

代码语言:javascript
复制
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

03  结论

PointNet 是一种突破性的用于 3D 分割的工具,克服了无序点集带来的挑战。其理论基础、架构设计和实际实现展示了其多功能性和可靠性。通过将理论与实践相结合,我们揭开了理解和利用 PointNet 进行 3D 分割的过程的神秘面纱。PyTorch 和 Python 的整合为在实际应用中探索 PointNet 的潜力提供了一个实用的框架。你可以在我的 GitHub 上找到所有的代码。

值得一提的是,这个项目是我在帕多瓦大学攻读硕士学位期间 3D 视觉课程的关键部分。大部分代码是由课程导师 Alberto Pretto 教授慷慨提供的。我负责完成代码,并添加了解释,以简化对于实现 PointNet 网络用于 3D 分割的过程感兴趣的人们。我真诚希望你们能够从这个指南中获得有益和愉快的体验。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 01  理解PointNet:核心概念
    • 1.1 理论的魔力
      • 1.2 PointNet体系结构概述
        • 1.3 输入和特征变换
          • 1.4 PointNet网络
            • 1.5 PointNet分割网络
            • 02  训练和测试PointNet模型
              • 2.1 数据转换
                • 2.2 点云数据集
                  • 2.3 数据集创建
                    • 2.4 DataLoader 的使用
                      • 2.5 损失函数
                        • 2.6 细分各个组件
                          • 2.7 训练循环
                            • 2.8 测试
                            • 03  结论
                            领券
                            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档