当前位置: 主页 > 图像 >

PointNet论文及代码

时间:2019-05-09  作者:haden   点击:
【摘要】本文开创性地提出了将深度学习直接用于三维点云数据上的方法。三维点云由于其无序性,无法直接使用卷积等操作。本文提出了对称函数解决点的无序性问题,并给出了理论证明。设
本文开创性地提出了将深度学习直接用于三维点云数据上的方法。三维点云由于其无序性,无法直接使用卷积等操作。本文提出了对称函数解决点的无序性问题,并给出了理论证明。设计了能够进行分类和分割任务的网络结构,认为提取出的特征点是三维物体的骨架结构。
 
问题说明

输入一个无序点集,包含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()
 
BATCH_SIZE = FLAGS.batch_size # 训练批次大小
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,对网络结构进行训练。


 

顶一下
(0)
0%
踩一下
(0)
0%
发表评论
请自觉遵守互联网相关的政策法规,严禁发布色情、暴力、反动的言论。
验证码: 点击我更换图片