前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >在点云上进行深度学习:在Google Colab中实现PointNet

在点云上进行深度学习:在Google Colab中实现PointNet

作者头像
代码医生工作室
发布2020-04-20 15:35:23
2.5K0
发布2020-04-20 15:35:23
举报
文章被收录于专栏:相约机器人

作者 | Nikita Karaev

来源 | Medium

编辑 | 代码医生团队

1.简介

3D数据对于自动驾驶汽车,自动驾驶机器人,虚拟现实和增强现实至关重要。与以像素阵列表示的2D图像不同,它可以表示为多边形网格,体积像素网格,点云等。

图像来自:从PyTorch中的单个2D图像创建3D模型

在当今的计算机视觉和机器学习中,90%的进展仅涉及二维图像。

1.1.点云

点云是一种广泛使用的3D数据形式,可以由深度传感器(例如LIDAR和RGB-D相机)生成。

它是3D对象的最简单表示:仅在3D空间中指向,没有连通性。点云也可以包含点的法线。

几乎所有3d扫描设备都会产生点云。

可以捕获点云的设备(Iphone 11,Asus Zenfone AR,Sony Xperia XZ1)

此外,最近苹果公司推出了带LiDAR扫描仪的Ipad Pro,可测量距周围物体的距离(最远5米)。

1.2.点云上的深度学习

因此考虑如何处理点云。CNN适用于图像。可以将它们用于3D吗?

想法:将2D卷积泛化为常规3D网格

图片来自:arxiv

这实际上有效。

https://arxiv.org/pdf/1604.03265.pdf

主要问题是表示效率低下:大小为100的立方体素网格将具有1,000,000体素。

1.3. PointNet

但是,如果尝试使用点云代替呢?

主要有三个约束:

  • 点云是无序的。算法必须对输入集的排列保持不变。
  • 如果我们旋转椅子,它仍然是椅子,对吗?网络对于不变的转换必须是不变的。
  • 网络应捕获点之间的交互。

PointNet的作者介绍了一种将所有这些属性都考虑在内的神经网络。它设法解决分类,部分和语义分割任务。

https://arxiv.org/pdf/1612.00593.pdf

图片来自:arxiv

2.实施

在本节中,将重新实现分类模式从原来的论文在谷歌Colab使用PyTorch。

可以在以下位置找到完整的笔记本:

https://arxiv.org/pdf/1612.00593.pdf

https://github.com/nikitakaraevv/pointnet/blob/master/nbs/PointNetClass.ipynb

2.1.数据集

在原始论文中,作者在ModelNet40形状分类基准上评估了PointNet。它包含来自40个对象类别的12,311个模型,分为9,843个训练和2,468个用于测试的模型。

为了简单起见,使用同一数据集的较小版本:ModelNet10。它包含来自10个类别的对象,用于训练的3,991个模型和用于测试的908个模型。

http://3dvision.princeton.edu/projects/2014/3DShapeNets/

如果想直接开始训练,别忘了打开GPU

导入必要的库:

代码语言:javascript
复制
import numpy as npimport randomimport math!pip install path.py;from path import Path

可以将数据集直接下载到Google Colab运行时:

代码语言:javascript
复制
!wget http://3dvision.princeton.edu/projects/2014/3DShapeNets/ModelNet10.zip!unzip -q ModelNet10.zip path = Path("ModelNet10")

该数据集由.off文件组成,这些文件包含由顶点和三角形面表示的网格。顶点只是3D空间中的点,每个三角形由3个顶点索引组成。

将需要一个函数来读取.off文件:

代码语言:javascript
复制
def read_off(file):    if 'OFF' != file.readline().strip():        raise('Not a valid OFF header')    n_verts, n_faces, __ = tuple([int(s) for s in file.readline().strip().split(' ')])    verts = [[float(s) for s in file.readline().strip().split(' ')] for i_vert in range(n_verts)]    faces = [[int(s) for s in file.readline().strip().split(' ')][1:] for i_face in range(n_faces)]    return verts, faces    with open(path/"bed/train/bed_0001.off", 'r') as f:    mesh = read_off(f)

这是全网格的样子:

在.off文件之一中划分网格。使用plotly创建

如您所见,这是一张床

但是,如果摆脱了面,只保留了3D点,它看起来就不再像床了!

网格顶点

实际上曲面的平坦部分不需要任何点即可进行网格构建。这就是为什么点主要位于床的角度和圆形部分的原因。

2.2.点采样

因此由于点在对象表面上的分布不均匀,因此PointNet可能难以对其进行分类。(特别是知道该点云甚至看起来都不像床)。

一个解决方案可能非常简单:统一采样对象表面上的点。

不应忘记面可以具有不同的区域。

因此可以按比例分配选择特定面的概率。这是可以做到的:

代码语言:javascript
复制
verts, faces = meshareas = np.zeros((len(faces)))verts = np.array(verts) # function to calculate triangle area by its vertices# https://en.wikipedia.org/wiki/Heron%27s_formuladef triangle_area(pt1, pt2, pt3):    side_a = np.linalg.norm(pt1 - pt2)    side_b = np.linalg.norm(pt2 - pt3)    side_c = np.linalg.norm(pt3 - pt1)    s = 0.5 * ( side_a + side_b + side_c)    return max(s * (s - side_a) * (s - side_b) * (s - side_c), 0)**0.5 # we calculate areas of all faces in our meshfor i in range(len(areas)):    areas[i] = (triangle_area(verts[faces[i][0]],                              verts[faces[i][1]],                              verts[faces[i][2]]))

的网络架构中将包含密集的层。这就是为什么要在点云中固定数量的点。从构造的分布中采样面。之后为每个选定的面采样一个点:

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

一些面可能有多个采样点,而另一些根本没有点。

通过采样网格表面上的点创建的点云

这个点云看起来更像一张床!

2.3.扩充

考虑其他可能的问题。知道对象可以具有不同的大小,并且可以放置在坐标系统的不同部分中。

所以翻译的对象原点从它的所有点减去均值和正常化的点到单位球。为了增加训练期间的数据,绕着Z轴随机旋转对象并添加高斯噪声,如本文所述:

代码语言:javascript
复制
# normalizenorm_pointcloud = pointcloud - np.mean(pointcloud, axis=0)norm_pointcloud /= np.max(np.linalg.norm(norm_pointcloud, axis=1)) # rotation around z-axistheta = random.random() * 2. * math.pi # rotation anglerot_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 noisenoise = np.random.normal(0, 0.02, (pointcloud.shape))noisy_pointcloud = rot_pointcloud + noise

这是归一化的床,带有旋转和噪音:

旋转点云增加了噪声

2.4.模型

好的已经完成了数据集和预处理。考虑一下模型架构。

记得结果应该对输入点排列和几何变换(如刚性变换)是不变的。

图片来自:arxiv

开始在PyTorch中实现它:

首先,张量将具有大小(batch_size, num_of_points, 3)。在这种情况下,具有共享权重的MLP只是1维卷积,内核大小为1。

为了确保变换的不变性,将T-Net预测的3x3变换矩阵应用于输入点的坐标。有趣的是,无法通过3维矩阵在3D空间中对翻译进行编码。无论如何,已经在预处理过程中将点云转换为原点。

这里重要的一点是输出矩阵的初始化。希望默认情况下它是身份,以开始训练而无需进行任何转换。因此只需向输出添加一个单位矩阵:

代码语言:javascript
复制
import torchimport torch.nn as nnimport torch.nn.functional as F class Tnet(nn.Module):   def __init__(self, k=3):      super().__init__()      self.k=k      self.conv1 = nn.Conv1d(k,64,1)      self.conv2 = nn.Conv1d(64,128,1)      self.conv3 = nn.Conv1d(128,1024,1)      self.fc1 = nn.Linear(1024,512)      self.fc2 = nn.Linear(512,256)      self.fc3 = nn.Linear(256,k*k)       self.bn1 = nn.BatchNorm1d(64)      self.bn2 = nn.BatchNorm1d(128)      self.bn3 = nn.BatchNorm1d(1024)      self.bn4 = nn.BatchNorm1d(512)      self.bn5 = nn.BatchNorm1d(256)           def forward(self, input):      # input.shape == (bs,n,3)      bs = input.size(0)      xb = F.relu(self.bn1(self.conv1(input)))      xb = F.relu(self.bn2(self.conv2(xb)))      xb = F.relu(self.bn3(self.conv3(xb)))      pool = nn.MaxPool1d(xb.size(-1))(xb)      flat = nn.Flatten(1)(pool)      xb = F.relu(self.bn4(self.fc1(flat)))      xb = F.relu(self.bn5(self.fc2(xb)))            # initialize as identity      init = torch.eye(self.k, requires_grad=True).repeat(bs,1,1)      if xb.is_cuda:        init=init.cuda()      # add identity to the output      matrix = self.fc3(xb).view(-1,self.k,self.k) + init      return matrix

应用MLP之后,将使用相同但64维的T-Net对齐提取的点要素。

为了提供置换不变性,对提取和转换的特征应用对称函数(最大池化),因此结果不再依赖于输入点的顺序。

将它们结合在一起:

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

然后,将其全部包装在一个类中,并在输出中包含最后一个MLP和LogSoftmax:

代码语言:javascript
复制
class PointNet(nn.Module):    def __init__(self, classes=10):        super().__init__()        self.transform = Transform()        self.fc1 = nn.Linear(1024, 512)        self.fc2 = nn.Linear(512, 256)        self.fc3 = nn.Linear(256, classes)         self.bn1 = nn.BatchNorm1d(512)        self.bn2 = nn.BatchNorm1d(256)        self.dropout = nn.Dropout(p=0.3)        self.logsoftmax = nn.LogSoftmax(dim=1)     def forward(self, input):        xb, matrix3x3, matrix64x64 = self.transform(input)        xb = F.relu(self.bn1(self.fc1(xb)))        xb = F.relu(self.bn2(self.dropout(self.fc2(xb))))        output = self.fc3(xb)        return self.logsoftmax(output), matrix3x3, matrix64x64

最后,将定义损失函数。由于使用LogSoftmax 来保持稳定性,因此应该应用NLLLoss而不是CrossEntropyLoss。另外,将在阶跃变换矩阵中添加两个正则化项以使其接近正交(AAᵀ= I):

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

2.5.训练

最后一步!只能使用经典的PyTorch训练循环。

同样,可以在此链接后找到带有训练循环的完整Google Colab笔记本。

https://github.com/nikitakaraevv/pointnet/blob/master/nbs/PointNetClass.ipynb

看一下在GPU上训练15个纪元后的结果。训练本身大约需要3个小时,但可能会因Colab分配给当前会话的GPU的类型而异。

通过一个简单的训练循环,在13个历时之后,可以达到85%的总体验证准确性,而原始工作中 40个班级的验证准确性为89%。这里的重点是实施完整模型,而不是真正获得最佳分数。因此,将调整训练循环和其他实验作为练习。

有趣的是,模型有时会使梳妆台与床头柜,厕所与椅子,桌子与桌子混淆,这是可以理解的(厕所除外)。

3.最后的话

实现了PointNet,这是一种深度学习架构,可用于各种3D识别任务。即使在这里实现了分类模型,分段,正态估计或其他任务也只需要在模型和数据集类中进行较小的更改。

完整的笔记本

https://github.com/nikitakaraevv/pointnet/blob/master/nbs/PointNetClass.ipynb

参考文献:

[1] 查尔斯·R·齐,苏昊,莫凯春,莱昂尼达斯· 吉巴斯,PointNet:针对3D分类和分割的点集深度学习(2017),CVPR 2017

http://stanford.edu/~rqi/pointnet/

[2] 亚当·康纳·西蒙斯(Adam Conner-Simons),《基于点云的深度学习》(2019年),麻省理工学院计算机科学与人工智能实验室

http://news.mit.edu/2019/deep-learning-point-clouds-1021

[3] Loic Landrieu,3D点云的语义分割(2019年),巴黎埃斯特大学—机器学习和优化工作组

http://bezout.univ-paris-est.fr/wp-content/uploads/2019/04/Landrieu_GT_appr_opt.pdf

[4] Charles R. Qi等人,《基于3D数据的对象分类的体积和多视图CNN》(2016年),arxiv.org。

https://arxiv.org/pdf/1604.03265.pdf

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-04-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 相约机器人 微信公众号,前往查看

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

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
图像处理
图像处理基于腾讯云深度学习等人工智能技术,提供综合性的图像优化处理服务,包括图像质量评估、图像清晰度增强、图像智能裁剪等。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档