Pointnet: Deep Learning on Point Sets for 3D Classification and Segmentation论文笔记

486 阅读7分钟

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

Pointnet: Deep Learning on Point Sets for 3D Classification and Segmentation

代码链接:github.com/Bailey-24/m…

概述: 为了解决以前的深度学习模型处理点云数据时计算量庞大的问题,作者提出了Pointnet,将2D卷积直接处理3D点云本身,具体而言,用Max-pooling解决点云无序性问题,并用T-net对输入点云和特征进行对齐,使网络预测结果对旋转变换具有不变性;Pointnet能够对点云进行分类,部件分割和语义分割。

image.png

1. 前置知识

1.1 三维数据的表现形式

常见三维数据有四种表现形式。

  • Point Cloud: 由 NNDD 维的点组成,当 D=3D = 3 代表常见的三维(x,y,z)(x, y, z) 坐标。
  • Mesh: 由三角面片或正方形面片。
  • Volumetric: 用三维栅格将物体用 0 和 1 表征。
  • Projected view: 用多角度的 RGB(D) 图像合成三维物体。 因为点云数据采集方便,而且表达形式简单,所以研究如何处理点云很重要。

image.png

1.2 点云性质

点云有两个主要性质。

  • Permutation Invariance:点云是无序的几何,点与点之间没有严格的顺序,如将两个点交换之后仍然表示同一个点云。
  • Transformation Invariance : 点云进行刚性变换(旋转 平移)后,但代表的还是同一个物体,即输出的类别或者分割结果应不变。

image.png

2. 算法原理

2.1 点云分类模型架构

首先了解一下点云分类模型是怎么设计的,然后再讲为什么这么设计。 如下图所示,对于每一个N×3N \times 3 的点云输入,网络先通过一个 T-Net 将其在空间上对齐(旋转到正面),再通过 MLP 将其映射到64 维的空间上,再一次对齐,最后映射到1024维的空间上。这时对于每一个点,都有一个1024维的向量表征,而这样的向量表征对于一个3维的点云显得冗余,因此引入最大池化,将1024维所有通道上都只保留最大的通道,这样得到1×10241 \times 1024 的向量就是NN个点云的全局特征,最后直接将这个全局特征经过MLP输出每一类的概率即可。 总而言之,分类网络就是将点云进行特征转换、映射升维、最大池化提取特征和 k 分类,得到点云属于哪个类别。

image.png

2.1.1 Max-pooling

分类网络为什么需要最大池化呢? 前面提到点云具有permutation invariance(置换不变性),即当一个N×DN \times DNN的维度上随意打乱之后,其表述的其实是同一个物体,因为设计的网络必须是一个对称的函数。常见对称函数有最大,平均和求和。

  • sum(a, b) = sum(b, a)
  • average(a, b) = average(b, a)
  • max(a, b) = max(b, a) 但是输入N×3N \times 3 的点云给max函数,输出只有1×31 \times 3 的特征,也就是每个点损失的特征太多了,输出的全局特征仅仅取三个坐标轴上最大的那个特征,所以我们先将点云上的每一个点用MLP 映射到一个高维空间,目的是做max操作时,损失的信息不会很多。初始的点云网络如下所示。

image.png

2.1.2 T-net

那为什么分类网络又需要T-net? 因为点云满足Transformation Invariance(旋转不变性),即旋转后还是代表同一个物体,但是如果对点云进行旋转后,所有的x,y,zx, y, z 坐标都变了,上面的vanilla pointnet就没办法很好地识别出是同一个物体,所以需要T-net去学习点云的旋转,将物体校准,再用vanilla pointnet对校准后的物体进行分类即可。 具体做法是,对一个N×DN \times D 的点云矩阵乘以一个D×DD \times D的旋转矩阵即可。因此对输入点云需要学习一个 3×33 \times 3的矩阵,即可将其校正;同样将点云映射到KK 维的空间后,再对 KK维的点云特征做一个校正。 总而言之,T-net通过从点云本身的位姿信息学习到一个最有利于网络进行分类的D×DD \times D 矩阵。第一个T-net作用是对空间中的点云进行调整,直观上理解是旋转出一个更有利于分类的==角度==,比如把物体旋转到正面;第二个T-net是对提取的64维特征进行对齐,即在==特征层面==对点云进行变换。

image.png

2.2 点云分割模型架构

对于点云分割,由于需要输出每个点的类别,因此需要将全局特征拼接在64维点云的局部特征上,最后通过MLP,输出每个点的分类概率。 但经过自己实验,发现pointnet的分割网络效果比较差,用pointnet++效果应该会更好。

image.png

3. 算法实现

3.1 实验环境

  • 实现框架:pytorch
  • 数据集:Modelnet10
  • 显卡:TiTAN V 12G

代码链接:github.com/Bailey-24/m… 点个star再走!

3.2 实现流程

3.2.1 预处理数据

因为我用modelnet10数据集进行分类,里面是用顶点和面片表示的网格.off文件,如果只保持点,去除面片,就会看到点集中在物体的边缘角落,这样网络很难分类这种初始文件,所以需要对数据进行预处理。

  • 采样点 解决以上问题就是对物体表面进行均匀采样点。
k = 3000
# we sample 'k' faces with probabilities proportional to their areas
# weights are used to create a distribution.
# they don't have to sum up to one.
sampled_faces = (random.choices(faces, 
                                weights=areas,
                                k=k))

# function to sample points on a triangle surface
def sample_point(pt1, pt2, pt3):
    # barycentric coordinates on a triangle
    # https://mathworld.wolfram.com/BarycentricCoordinates.html
    s, t = sorted([random.random(), random.random()])
    f = lambda i: s * pt1[i] + (t-s) * pt2[i] + (1-t) * pt3[i]
    return (f(0), f(1), f(2))
 
pointcloud = np.zeros((k, 3))

# sample points on chosen faces for the point cloud of size 'k'
for i in range(len(sampled_faces)):
    pointcloud[i] = (sample_point(verts[sampled_faces[i][0]],
                                  verts[sampled_faces[i][1]],
                                  verts[sampled_faces[i][2]]))
  • 数据增强 为了训练更快,需要对点云进行正则化。而且论文提到对点云绕z轴旋转和添加高斯噪声来数据增强。
# normalize
norm_pointcloud = pointcloud - np.mean(pointcloud, axis=0) 
norm_pointcloud /= np.max(np.linalg.norm(norm_pointcloud, axis=1))

# rotation around z-axis
theta = random.random() * 2. * math.pi # rotation angle
rot_matrix = np.array([[ math.cos(theta), -math.sin(theta),    0],
                       [ math.sin(theta),  math.cos(theta),    0],
                       [0,                             0,      1]])

rot_pointcloud = rot_matrix.dot(pointcloud.T).T

# add some noise
noise = np.random.normal(0, 0.02, (pointcloud.shape))
noisy_pointcloud = rot_pointcloud + noise

3.2.2 模型

只展示pointnet分类模型最关键的部分,即从输入到得到全局特征这一部分。

class Transform(nn.Module):
   def __init__(self):
        super().__init__()
        self.input_transform = Tnet(k=3)
        self.feature_transform = Tnet(k=64)
        self.conv1 = nn.Conv1d(3,64,1)

        self.conv2 = nn.Conv1d(64,128,1)
        self.conv3 = nn.Conv1d(128,1024,1)

        self.bn1 = nn.BatchNorm1d(64)
        self.bn2 = nn.BatchNorm1d(128)
        self.bn3 = nn.BatchNorm1d(1024)
       
   def forward(self, input):
        matrix3x3 = self.input_transform(input)
        # batch matrix multiplication
        xb = torch.bmm(torch.transpose(input,1,2), matrix3x3).transpose(1,2)
        xb = F.relu(self.bn1(self.conv1(xb)))

        matrix64x64 = self.feature_transform(xb)
        xb = torch.bmm(torch.transpose(xb,1,2), matrix64x64).transpose(1,2)

        xb = F.relu(self.bn2(self.conv2(xb)))
        xb = self.bn3(self.conv3(xb))
        xb = nn.MaxPool1d(xb.size(-1))(xb)
        output = nn.Flatten(1)(xb)
        return output, matrix3x3, matrix64x64

3.2.3 损失函数

因为训练的过程很普通,所以只写一个损失函数。 为了输出结果更加稳定,采用LogSoftmax,用NLLLoss代替CrossEntropyLoss;最后还要加上两个旋转矩阵的正则化,使它们更接近正交矩阵。

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)
    if outputs.is_cuda:
        id3x3 = id3x3.cuda()
        id64x64 = id64x64.cuda()
    diff3x3 = id3x3 - torch.bmm(m3x3, m3x3.transpose(1, 2))
    diff64x64 = id64x64 - torch.bmm(m64x64, m64x64.transpose(1, 2))
    return criterion(outputs, labels) + alpha * (torch.norm(diff3x3) + torch.norm(diff64x64)) / float(bs)

3.3 实验结果

通过混淆矩阵来看pointnet的分类结果。横轴代表网络输出的结果,纵轴代表真实类别,对角线上的数值代表网络预测的标签与真实标签相同的概率,除了对角线上,网络预测都是错误的。 从结果可以看到网络预测desk这个类只有0.66的准确率,其中有0.22概率预测到table这个类中,可视化一下这两个类别。desk一般指书桌,table是桌子的统称,区别在于有无抽屉。但网络在预测desk时,出现一些类似有抽屉的table,所以就会分类错误。

image.png

image.png

4. 心得体会

  • 学会处理点云数据给网络。

5. 参考文献