本文已参与「新人创作礼」活动, 一起开启掘金创作之路。
Pointnet: Deep Learning on Point Sets for 3D Classification and Segmentation
概述: 为了解决以前的深度学习模型处理点云数据时计算量庞大的问题,作者提出了Pointnet,将2D卷积直接处理3D点云本身,具体而言,用Max-pooling解决点云无序性问题,并用T-net对输入点云和特征进行对齐,使网络预测结果对旋转变换具有不变性;Pointnet能够对点云进行分类,部件分割和语义分割。
1. 前置知识
1.1 三维数据的表现形式
常见三维数据有四种表现形式。
- Point Cloud: 由 个 维的点组成,当 代表常见的三维 坐标。
- Mesh: 由三角面片或正方形面片。
- Volumetric: 用三维栅格将物体用 0 和 1 表征。
- Projected view: 用多角度的 RGB(D) 图像合成三维物体。 因为点云数据采集方便,而且表达形式简单,所以研究如何处理点云很重要。
1.2 点云性质
点云有两个主要性质。
- Permutation Invariance:点云是无序的几何,点与点之间没有严格的顺序,如将两个点交换之后仍然表示同一个点云。
- Transformation Invariance : 点云进行刚性变换(旋转 平移)后,但代表的还是同一个物体,即输出的类别或者分割结果应不变。
2. 算法原理
2.1 点云分类模型架构
首先了解一下点云分类模型是怎么设计的,然后再讲为什么这么设计。 如下图所示,对于每一个 的点云输入,网络先通过一个 T-Net 将其在空间上对齐(旋转到正面),再通过 MLP 将其映射到64 维的空间上,再一次对齐,最后映射到1024维的空间上。这时对于每一个点,都有一个1024维的向量表征,而这样的向量表征对于一个3维的点云显得冗余,因此引入最大池化,将1024维所有通道上都只保留最大的通道,这样得到 的向量就是个点云的全局特征,最后直接将这个全局特征经过MLP输出每一类的概率即可。 总而言之,分类网络就是将点云进行特征转换、映射升维、最大池化提取特征和 k 分类,得到点云属于哪个类别。
2.1.1 Max-pooling
分类网络为什么需要最大池化呢? 前面提到点云具有permutation invariance(置换不变性),即当一个 在 的维度上随意打乱之后,其表述的其实是同一个物体,因为设计的网络必须是一个对称的函数。常见对称函数有最大,平均和求和。
- sum(a, b) = sum(b, a)
- average(a, b) = average(b, a)
- max(a, b) = max(b, a) 但是输入 的点云给max函数,输出只有 的特征,也就是每个点损失的特征太多了,输出的全局特征仅仅取三个坐标轴上最大的那个特征,所以我们先将点云上的每一个点用MLP 映射到一个高维空间,目的是做max操作时,损失的信息不会很多。初始的点云网络如下所示。
2.1.2 T-net
那为什么分类网络又需要T-net? 因为点云满足Transformation Invariance(旋转不变性),即旋转后还是代表同一个物体,但是如果对点云进行旋转后,所有的 坐标都变了,上面的vanilla pointnet就没办法很好地识别出是同一个物体,所以需要T-net去学习点云的旋转,将物体校准,再用vanilla pointnet对校准后的物体进行分类即可。 具体做法是,对一个 的点云矩阵乘以一个的旋转矩阵即可。因此对输入点云需要学习一个 的矩阵,即可将其校正;同样将点云映射到 维的空间后,再对 维的点云特征做一个校正。 总而言之,T-net通过从点云本身的位姿信息学习到一个最有利于网络进行分类的 矩阵。第一个T-net作用是对空间中的点云进行调整,直观上理解是旋转出一个更有利于分类的==角度==,比如把物体旋转到正面;第二个T-net是对提取的64维特征进行对齐,即在==特征层面==对点云进行变换。
2.2 点云分割模型架构
对于点云分割,由于需要输出每个点的类别,因此需要将全局特征拼接在64维点云的局部特征上,最后通过MLP,输出每个点的分类概率。 但经过自己实验,发现pointnet的分割网络效果比较差,用pointnet++效果应该会更好。
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,所以就会分类错误。
4. 心得体会
- 学会处理点云数据给网络。