前面我们已经学习了Open3D
,并掌握了其相关应用,但我们也发现对于一些点云分割任务,我们采用聚类等方法的效果似乎并不理想,这时,我们可以想到在深度学习领域是否有相关的算法呢,今天,我们便来学习一个在点云处理领域具有代表性的算法:PointNet++
。
PointNet
在学习PointNet++
前,我们需要学习以下它的前身:PointNet
。
我们知道,点云数据具有以下特点:
- 无序性:点云的位置可以随意调换没有影响(点与点之间可以换)
- 近密远疏:扫描视角不同导致点云的稀疏性不同
- 非结构化数据:处理困难,如
NLP
的处理就要比图像复杂
那么针对这种问题,该如何解决呢,其实最主要的是我们要去提取特征。
思路:对于无序性的数据,我们要考虑能否利用置换不变性来解决问题,PointNet
便是采用这个思路,即对于点云数据求最值(无论是max
、min
还是sum
,它都与点的位置没有关系)。
同时,由于点云一般只有三维(xyz)
,其维度太少,因此可以利用神经网络(多层感知机、全连接网络等)来升维,进而再进行处理。
下图为PointNet
网络结构,其中里面的input_transform
我们无需太过在意,可以看到,其输入的是所有点云,随后进行维度变换,最终输出分类或分割结果。
PointNet++网络介绍
根据其论文中给出的介绍,Point++
是用于点云分割与分类的深度学习模型,由下图可知,该模型主要分为三部分,分别是点空间特征提取、分割模型以及分类模块。其中,Hierarchical Point set feature learning
由一系列点集抽象层(set abstraction
)组成,而每一个set abstraction
又由三个关键层组成:sampling layer
,Grouping layer
,PointNet Layer
。
Sampling和Grouping这两个操作一般是放在一起进行操作的,从数据的shape中进行分析:
(1)Input:输入是xyz坐标数据(B,N,3),分别代表点云个数B,点个数N,坐标xyz;还有一个输入特征数据是对应的N个点的特征维度D;
(2)Sampling:通过最远点采样方法从N个点中选取npoints个点作为中心点,坐标数据变为(B,npoint,3);
(3)Grouping:通过Sampling采样得到的npoint个中心点,首先对其每个中心点使用球搜索(会有一个内参r表示球搜索半径)检索出距离中心点最近的nsample个点,如果r范围内的点不足nsample个会用距离中心点的坐标进行补齐;
(4)然后通过球搜索检索得到的点来找到对应的特征数据,得到Grouping的特征数据,shape为(B,npoint,nsample,D);
(5)最后还需要将坐标数据和特征数据进行拼接,将得到shape为(B,npoint,nsample,D+3)
的数据。
最远点采样(FPS)
顾名思义,假设有一个点的集合{A1,A2,A3…An}就是选取距离彼此最远的k个点,取两个最远点好选取,那么选取k个最远点如何选取呢,通过以下例子来说明最远点采样方法:
假设有7个点P0—P6,首先初始化第一个点为P0,则距离P0的最远点为P6,得到最远点的点集为{P0,P6};
其次第三个点选取步骤:先取最小值,再取最大值,至于为什么可以反过来想,如果我先取最大值,那么我可能选取的这个最大值可能离A远,但是离B近呀,这样就没办法保证我取得的是最远点了。
(1)P1/P2/P3/P4/P5到P0和P6的最小值:
P1—>(P0,P6)的距离min(L10,L16) L10
P2—>(P0,P6)的距离min(L20,L26) L20
P3—>(P0,P6)的距离min(L30,L36) L30
P4—>(P0,P6)的距离min(L40,L46) L40
P5—>(P0,P6)的距离min(L50,L56) L50
(2)选取最短距离的最大值:
max(L10,L20,L30,L40,L50)=L50
(3)则第三个点为P5
最后重复第二个步骤,直到选取完k个点。
事实上,PointNet++相较于PointNet的创新便是在于数据的处理,其采用了分簇、分组的方式进行处理,这可以大幅减少计算量(PointNet是将所有点云输入PointNet网络,PointNet++是将数据分簇分组后输入PointNet网络)
分簇与分组
在这里,分簇是为了采样,即Sampling layer,而分组则是将每个簇的数据量统一,这样才能够输入卷积网络中运算,具体的,对于分组(Grouping layer)时,如果簇中数量多,那么就按照距离中心点距离进行排序,挑选近的留下(即删除远的点),对于簇中数量少,则将复制该簇内里离中心点最近的点,缺几个则复制几次)
PointNet++项目部署
源码下载
了解了PointNet++
的基本原理后,接下来我们便要部署该项目来完成我们的任务,这里,应领导要求,博主并没有使用PointNet++
的官方代码(官方代码是基于Tensflow
框架开发的),而是使用了Pytorch
的版本。
环境部署
将源码下载后,便是部署环境,PointNet++
所使用的包并不多且比较通用,博主直接使用了先前的conda
环境,发现可以完美运行,也就没有重新创建conda
环境。
S3Dis数据集介绍
在本次实验中,由于我们要做的任务是点云分割任务,因此我们使用的数据集为S3Dis
,该数据集是一个室内点云分割数据集, 共有6个区域,13个类别,共计217个小区域(办公室、会议室等)其内容如下:
13个类别
6个大区域
我们以Area_1
(区域1为例),其内有会议室、走廊等多个场所
再以office_9
为例,office_9.txt是整个办公室点云,Annotations
内的是office_9
的分割点云,如里面的桌子,椅子等
我们使用CloudCompare
打开可以看到其内容,数据格式为xyzrgb
格式
数据格式转换
为何要进行数据格式转换呢,因为S3DIS数据集只是存储一些点,并没有标签(标签是存储在文件名上的),而collect_indoor3d_data
脚本所做的事情就是将每一个Area
下的每一个场景的点和标签进行合并,并且保存为.npy
格式,加速读取的速度。
生成的.npy
格式的数据也有217个。
.npy
文件的内容如下,其实就是转成numpy
的格式,从而方便运算,其相比于原本的txt多了一个维度,即第7个维度,用于表示所属类别。
import numpy as np
data=np.load("stanford_indoor3d/Area_1_WC_1.npy")
print(data)
collect_indoor3d_data
代码如下,该部分主要是完成读取点云数据,并设置点云数据的保存路径,名称等
import os
import sys
from indoor3d_util import DATA_PATH, collect_point_label
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
ROOT_DIR = os.path.dirname(BASE_DIR)
sys.path.append(BASE_DIR)
anno_paths = [line.rstrip() for line in open(os.path.join(BASE_DIR, 'meta/anno_paths.txt'))]
anno_paths = [os.path.join(DATA_PATH, p) for p in anno_paths]
output_folder = os.path.join(ROOT_DIR, 'data/stanford_indoor3d/')
if not os.path.exists(output_folder):
os.mkdir(output_folder)
# Note: there is an extra character in the v1.2 data in Area_5/hallway_6. It's fixed manually.
for anno_path in anno_paths:
print(anno_path)
try:
elements = anno_path.split('/')
out_filename = elements[-3]+'_'+elements[-2]+'.npy' # Area_1_hallway_1.npy
collect_point_label(anno_path, os.path.join(output_folder, out_filename), 'numpy')
except:
print(anno_path, 'ERROR!!')
具体的,划分点云中的标签是通过collect_point_label
方法实现的,事实上,我们并不需要读懂这部分代码,要想完成数据转换,只需要将我们的数据格式转换成与S3Dis
数据集一样即可。
def collect_point_label(anno_path, out_filename, file_format='txt'):
""" Convert original dataset files to data_label file (each line is XYZRGBL).
We aggregated all the points from each instance in the room.
Args:
anno_path: path to annotations. e.g. Area_1/office_2/Annotations/
out_filename: path to save collected points and labels (each line is XYZRGBL)
file_format: txt or numpy, determines what file format to save.
Returns:
None
Note:
the points are shifted before save, the most negative point is now at origin.
"""
points_list = []
for f in glob.glob(os.path.join(anno_path, '*.txt')):
cls = os.path.basename(f).split('_')[0]
print(f)
if cls not in g_classes: # note: in some room there is 'staris' class..
cls = 'clutter'
points = np.loadtxt(f)
labels = np.ones((points.shape[0],1)) * g_class2label[cls]
points_list.append(np.concatenate([points, labels], 1)) # Nx7
data_label = np.concatenate(points_list, 0)
xyz_min = np.amin(data_label, axis=0)[0:3]
data_label[:, 0:3] -= xyz_min
if file_format=='txt':
fout = open(out_filename, 'w')
for i in range(data_label.shape[0]):
fout.write('%f %f %f %d %d %d %d\n' % \
(data_label[i,0], data_label[i,1], data_label[i,2],
data_label[i,3], data_label[i,4], data_label[i,5],
data_label[i,6]))
fout.close()
elif file_format=='numpy':
np.save(out_filename, data_label)
else:
print('ERROR!! Unknown file format: %s, please use txt or numpy.' % \
(file_format))
exit()
训练PointNet++网络
首先是模型选择,我们这里可以看到model中可供我们选择的模型,其中加了msg的代表使用了多尺度特征,其效果要比不加的好,当然,其网络也会更复杂一些,我们使用的是pointnet2_sem_seg_msg
parser.add_argument('--model', type=str, default='pointnet2_sem_seg_msg', help='model name [default: pointnet_sem_seg]')
选择使用的测试集,这里默认为Area_5
parser.add_argument('--test_area', type=int, default=5, help='Which area to use for test, option: 1-6 [default: 5]')
随后一些batch-size
设置,epoch
设置我们就不再赘述了(博主设置batch=16),同时需要注意的是需要修改以下num_workers的值,博主设置为0,这个看你服务器的性能,博主由于是在本地测试,因此也就设为0了,否则会报错:
UnpicklingError: pickle data was truncated
开启训练
加载数据集(训练集与验证集)
开启训练,输出最终的训练平均损失,以及训练平均准确度
测试模型
在测试模型时,我们指定加载的模型权重即可,即我们在训练时保存的log
文件的地址:
parser.add_argument('--log_dir', type=str,default="pointnet2_sem_seg_msg", help='experiment root')
可以看到,测试数据集为Area_5
训练时的模型显卡使用情况如下:
最终的评估结果如下:
结语
本章主要介绍了PointNet++模型的结构以及部署问题,接下来便要进行模型的应用,我们需要使用自己的数据集来完成相应的任务。