本文开创性地提出了将深度学习直接用于三维点云数据上的方法。三维点云由于其无序性,无法直接使用卷积等操作。本文提出了对称函数解决点的无序性问题,并给出了理论证明。设计了能够进行分类和分割任务的网络结构,认为提取出的特征点是三维物体的骨架结构。
问题说明
输入一个无序点集,包含n个点,每个点是其坐标表示(x,y,z),也可以添加其他手动提取的特征。对于分类问题,输出1*k的向量,表示k个类别的得分。对于分割问题,输出n * m的得分矩阵,表示每个点(或每个子区域)的类别标签。
文章使用的主要方法可以概括如下:
f是全局特征的提取函数,h是每一个点的特征提取函数,g是文章提出的对称函数。在数据量很大的情况下,对一系列点通过f提取特征,可以与对单个点提取特征的点集,与对称函数的作用结果相近似。文章证明了这一理论。 而单个点的特征提取函数,本文选用了mlp。权值共享的mlp作用于每一个输入特征点,进行单点的特征提取操作,在最后使用max pooling进行全局特征的统一。实现中,使用1x1的conv代替了mlp。
点的无序性问题
不同顺序的点云数据实际上表示了同一组三维物体,因此,想要从点云中抽取共有特征,文章提出了三个方法:
1. 对于无序点集进行排序。point cnn就是使用了这个方法。
2. 像RNN一样把点集当做一个序列进行处理。这样做需要进行数据增强,对输入点集做所有的排列变换。
3. 使用对称函数。
本文就是要了对称函数的方法。文章称对顺序不敏感的函数为对称函数。如加法、点乘,以及本文使用的max pooling等操作
设输入特征为NxD,使用max pooling作用在N维度上,得到1xD的向量,每一维特征都与其顺序无关,这样便保证了对于点云输入顺序的鲁棒性。
刚体运动的不变性问题
刚体运动的即旋转平移,文章的解决方法也很简单,即学习一个变换矩阵T,称作STN网络。在训练过程中,由于loss的约束,使得T矩阵学习到最有利于最终分类的变换,如把物体旋转到正面。实际架构中,分别在输入数据和第一层特征中使用了T矩阵,大小为3x3和64x64。第二个T矩阵由于参数过多,添加了正则项,使其接近于正交矩阵。
不过最终实验结果和后续的论文point net++表示,这个STN网络并没有什么大的用处。
点之间的相关性问题
即网络应用到分割物体上的问题。在分类任务中,特征经过max pooling得到一维特征向量,此时包含了全局信息,只要再经过全连接网络,得到1*K的k个类别预测得分向量即可。而在分割任务中,需要对每一个点输出所属类别,需要一个类似图像分割的上采样过程。
本文也使用了类似图像分割任务的,高层全局信息与底层局部特征结合的思想。经过max pooling后的1D特征向量,复制n份(n个特征点),与之前网络得到的 n * 64特征矩阵分别concat。得到一个n(64+D)的特征矩阵,再经一系列的特征变换操作,得到每个点的分类结果。
(以上内容摘自:https://blog.csdn.net/pikachu_777/article/details/82993179) 代码分析 可以通过https://github.com/charlesq34/pointnet获取源码。 输入是nx3点云数据,点云的点个数是n,包含x、y、z 3个坐标信息。 train.py # 参数输入处理
parser = argparse.ArgumentParser()
parser.add_argument('--gpu', type=int, default=0,
help='GPU to use [default: GPU 0]')
parser.add_argument('--model', default='pointnet_cls',
help='Model name: pointnet_cls or pointnet_cls_basic [default: pointnet_cls]')
parser.add_argument('--log_dir', default='log', help='Log dir [default: log]')
parser.add_argument('--num_point', type=int, default=1024,
help='Point Number [256/512/1024/2048] [default: 1024]')
parser.add_argument('--max_epoch', type=int, default=250,
help='Epoch to run [default: 250]')
parser.add_argument('--batch_size', type=int, default=32,
help='Batch Size during training [default: 32]')
parser.add_argument('--learning_rate', type=float, default=0.001,
help='Initial learning rate [default: 0.001]')
parser.add_argument('--momentum', type=float, default=0.9,
help='Initial learning rate [default: 0.9]')
parser.add_argument('--optimizer', default='adam',
help='adam or momentum [default: adam]')
parser.add_argument('--decay_step', type=int, default=200000,
help='Decay step for lr decay [default: 200000]')
parser.add_argument('--decay_rate', type=float, default=0.7,
help='Decay rate for lr decay [default: 0.8]')
FLAGS = parser.parse_args()
NUM_POINT = FLAGS.num_point # 训练点云点个数
MAX_EPOCH = FLAGS.max_epoch # 最大训练次数
BASE_LEARNING_RATE = FLAGS.learning_rate # 初始学习率
GPU_INDEX = FLAGS.gpu # 默认GPU使用数量
MOMENTUM = FLAGS.momentum # 初始学习率
OPTIMIZER = FLAGS.optimizer # 优化器
DECAY_STEP = FLAGS.decay_step # 衰变步长
DECAY_RATE = FLAGS.decay_rate # 衰变率
# some code ...
# 获取模型
pred, end_points = MODEL.get_model(pointclouds_pl, is_training_pl, bn_decay=bn_decay)
模型第一步,3x3的input transform pointnet_cls.py def get_model(point_cloud, is_training, bn_decay=None):
# some code ...
with tf.variable_scope('transform_net1') as sc:
transform = input_transform_net(point_cloud, is_training, bn_decay, K=3)
上面分析所说,通过T-Net训练得到一个3x3的旋转矩阵,对点云数据进行旋转,得到最好的观察角度。结构如下: 将输入的nx3x1的点云数据作为nx3的图像,单通道,做三次卷积和一次池化后,reshape为1024个节点,全连接网络: 最后将9个节点reshape为3x3的旋转矩阵。 transform_nets.py def input_transform_net(point_cloud, is_training, bn_decay=None, K=3):
""" Input (XYZ) Transform Net, input is BxNx3 gray image
Return:
Transformation matrix of size 3xK """
batch_size = point_cloud.get_shape()[0].value
num_point = point_cloud.get_shape()[1].value
input_image = tf.expand_dims(point_cloud, -1) # 扩展一维,BxNx3x1
# 输入BxNx3x1
# 卷积核 1x3 (参数[1,3]定义)
# 移动步长 1x1 (stride=[1,1]定义)
# 输出 BxNx1x64
net = tf_util.conv2d(input_image, 64, [1,3],
padding='VALID', stride=[1,1],
bn=True, is_training=is_training,
scope='tconv1', bn_decay=bn_decay)
# 输入 BxNx1x64
# 卷积核 1x1
# 步长 1x1
# 输出 BxNx1x128
net = tf_util.conv2d(net, 128, [1,1],
padding='VALID', stride=[1,1],
bn=True, is_training=is_training,
scope='tconv2', bn_decay=bn_decay)
# 输入 BxNx1x128
# 输出 BxNx1x1024
net = tf_util.conv2d(net, 1024, [1,1],
padding='VALID', stride=[1,1],
bn=True, is_training=is_training,
scope='tconv3', bn_decay=bn_decay)
# 池化
# 输入 BxNx1x1024
# 池化核 Nx1
# 输出 Bx1x1x1024
net = tf_util.max_pool2d(net, [num_point,1],
padding='VALID', scope='tmaxpool')
# 输出 Bx1024
net = tf.reshape(net, [batch_size, -1])
# 全连接
# 输入 Bx1024
# 权重 1024x512
# 偏置 512
# 输出 Nx512
net = tf_util.fully_connected(net, 512, bn=True, is_training=is_training,
scope='tfc1', bn_decay=bn_decay)
# 全连接
# 输出 Nx256
net = tf_util.fully_connected(net, 256, bn=True, is_training=is_training,
scope='tfc2', bn_decay=bn_decay)
# 再次全连接
# 输出 Nx9
with tf.variable_scope('transform_XYZ') as sc:
assert(K==3)
weights = tf.get_variable('weights', [256, 3*K],
initializer=tf.constant_initializer(0.0),
dtype=tf.float32)
biases = tf.get_variable('biases', [3*K],
initializer=tf.constant_initializer(0.0),
dtype=tf.float32)
biases += tf.constant([1,0,0,0,1,0,0,0,1], dtype=tf.float32)
transform = tf.matmul(net, weights)
transform = tf.nn.bias_add(transform, biases)
# reshape
# 输出 Nx3x3
transform = tf.reshape(transform, [batch_size, 3, K])
return transform
原始的点云nx3乘T-Net训练后得到的3x3旋转矩阵后,得到新的坐标下的点云数据。 pointnet_cls.py with tf.variable_scope('transform_net1') as sc:
transform = input_transform_net(point_cloud, is_training, bn_decay, K=3)
point_cloud_transformed = tf.matmul(point_cloud, transform)
通过2次卷积,及图中的mlp(64,64): pointnet_cls.py input_image = tf.expand_dims(point_cloud_transformed, -1)
# 输入 Bxnx3x1
# 输出 Bxnx1x64
net = tf_util.conv2d(input_image, 64, [1,3],
padding='VALID', stride=[1,1],
bn=True, is_training=is_training,
scope='conv1', bn_decay=bn_decay)
# 输入 Bxnx1x64
# 输出 Bxnx1x64
net = tf_util.conv2d(net, 64, [1,1],
padding='VALID', stride=[1,1],
bn=True, is_training=is_training,
scope='conv2', bn_decay=bn_decay)
T-net处理特征得到转换矩阵,矩阵相乘对齐特征。 和上一步的T-Net相比,只是最后一次全连接的输出改为了64x64。 pointnet_cls.py with tf.variable_scope('transform_net2') as sc:
transform = feature_transform_net(net, is_training, bn_decay, K=64)
end_points['transform'] = transform
# 将上一步的net Bxnx1x64转为 Bxnx64 和 T-Net的Bx64x64 向乘
net_transformed = tf.matmul(tf.squeeze(net, axis=[2]), transform)
再经过3次卷积和1次池化,对应图中的mlp(64,128,1024),得到nx1024特征。 pointnet_cls.py # Bxnx64 扩展为 Bxnx1x64
net_transformed = tf.expand_dims(net_transformed, [2])
# 输入 Bxnx1x64
# 输出 Bxnx1x64
net = tf_util.conv2d(net_transformed, 64, [1,1],
padding='VALID', stride=[1,1],
bn=True, is_training=is_training,
scope='conv3', bn_decay=bn_decay)
# 输入 Bxnx1x64
# 输出 Bxnx1x128
net = tf_util.conv2d(net, 128, [1,1],
padding='VALID', stride=[1,1],
bn=True, is_training=is_training,
scope='conv4', bn_decay=bn_decay)
# 输入 Bxnx1x128
# 输出 Bxnx1x1024
net = tf_util.conv2d(net, 1024, [1,1],
padding='VALID', stride=[1,1],
bn=True, is_training=is_training,
scope='conv5', bn_decay=bn_decay)
经过池化和reshape后得到提取的特征,即global feature: pointnet_cls.py # Symmetric function: max pooling
net = tf_util.max_pool2d(net, [num_point,1],
padding='VALID', scope='maxpool')
net = tf.reshape(net, [batch_size, -1]) # 输出 batch_size x 1024
最后通过3次带dropout的全连接,池化层作为对称函数,得到1024位的特征向量,解决点云的无序性 输出分类结果: pointnet_cls.py # 全连接 + dropout
# 输出 batch_size x 512
net = tf_util.fully_connected(net, 512, bn=True, is_training=is_training,
scope='fc1', bn_decay=bn_decay)
net = tf_util.dropout(net, keep_prob=0.7, is_training=is_training,
scope='dp1')
# 全连接 + dropout
# 输出 batch_size x 256
net = tf_util.fully_connected(net, 256, bn=True, is_training=is_training,
scope='fc2', bn_decay=bn_decay)
net = tf_util.dropout(net, keep_prob=0.7, is_training=is_training,
scope='dp2')
# 全连接
# 输出 32 x 40
net = tf_util.fully_connected(net, 40, activation_fn=None, scope='fc3')
利用交叉熵作为loss,对网络结构进行训练。 |