Point Transformer –ICCV2021

论文:Point Transformer
作者单位:牛津大学, 港中文(贾佳亚等), Intel Labs

transformer应用到了点云任务处理中。为点云设计了自注意力层,并使用它们来构造诸如语义场景分割,object part分割和对象分类等任务的自注意力网络。

attention层设计:

这里的y是输出的feature,ϕ、ψ、α都是逐点特征变换的一种方式(比如mlp),δ是一个位置编码函数,ρ是正则化函数,简单来说,xi是点i的feature向量,先通过特征变换将点i和点j(Xj是Xi的邻域上的点,而非全局的,目的是减少计算量)的特征得到,这里的β是关系函数,通过这个函数得到两个点特征之间的关系,也就是建立每个点特征之间的关系,然后加上位置编码函数δ,γ是映射函数,也就是映射到某一维度而用。在这基础上就可以设计这里的重点,Point transformer层了

输入是(x,p)也就是每个点的位置信息,首先通过两个线性函数编码不同主次点的特征向量(也就是得到前面的key向量),再用一个MLP得到位置函数,也就是前面的查询向量),两者结合得到relation关系,然后再用一个线性函数得到它的值向量,将relation和值向量结合,也就是前面说的对于每个点既关注它的和其他点之间的语义关系,也关注它和其他点之间的位置关系,最后输出y作为点云处理结果。

位置函数也就是计算查询向量的那个函数:

在这里插入图片描述

p就是各自点的三维坐标值,θ是一个MLP层,而前面的线性函数也就是ax+b的形式(就是linear层)

定义完了transformer层,就可以定义一个block来作为基本的block(下图a):

输入是点集合x(拥有各自的三维点坐标等点特征),输出就是将每个点x的更新后的特征输出:

down的功能是根据需要减少点集的基数,简单来说就是减少点,而up就是根据两个不同数量的点来得到结合后的结果,常常使用在U型网络设计中(也就是当前层结果是结合了当前层的输入和之前某一层不同维度的输出而得到)

transition down:

step1:farthest point sample,把p1个点采样到 p个点,通过MLP改变特征向量(y,p),通过KNN算法,把p个点分成p2类,每个类内部做最大池化得到最终输出(y,p2)。

up模块:input1 如何才能扩充点数:通过线性插值算法

网络结构:

实验结果:

用于大规模语义场景分割的具有挑战性的S3DIS数据集上,Point Transformer在Area 5上的mIoU达到70.4%,比最强的现有模型高3.3个绝对百分点,并首次超过70%mIoU阈值 。

在ModelNet40和ShapeNetPart数据集上的性能表现:

目前paper with code 网站的排名:

3D Point Cloud Classification on ModelNet40
3D Point Cloud Classification on ModelNet40
3D Part Segmentation on ShapeNet-Part
3D Semantic Segmentation on SemanticKITTI

点云基础知识+ 三维计算机视觉研究内容

1)点云概念

点云是在同一空间参考系下表达目标空间分布和目标表面特性的海量点集合,在获取物体表面每个采样点的空间坐标后,得到的是点的集合,称之为“点云”(Point Cloud)。

2)点云图像是最基础也是最常见的三维图像。

那什么是三维图像呢?三维图像是一种特殊的图像信息表达形式。相比较于常见的二维图像,其最大的特征是表达了空间中三个维度(长度宽度和深度)的数据。

3)三维图像的表现形式

深度图(以灰度表达物体与相机的距离),几何模型(由CAD软件建立),点云模型(所有逆向工程设备都将物体采样成点云)。

4)点云根据测量原理主要分为两种

根据激光测量原理得到的点云,包括三维坐标(XYZ)和激光反射强度(Intensity)。强度信息与目标的表面材质、粗糙度、入射角方向,以及仪器的发射能量,激光波长有关。

根据摄影测量原理得到的点云,包括三维坐标(XYZ)和颜色信息(RGB)。

当然也有把激光和摄影相结合在一起的(多传感器融合技术),这种结合激光测量和摄影测量原理得到点云,包括三维坐标(XYZ)、激光反射强度(Intensity)和颜色信息(RGB)。

本次的文章主要讲的是基于摄像技术的点云配准。

5)点云的获取设备

RGBD设备(深度摄像机)是可以获取点云的设备。比如PrimeSense公司的PrimeSensor、微软的Kinect、华硕的XTionPRO。

6)点云的属性

空间分辨率、点位精度、表面法向量等。

7)点云存储格式

.pts; .asc ; *.dat; .stl ; [1] .imw;.xyz;.las。

8)点云的数据类型

(1)pcl::PointCloudpcl::PointXYZ

PointXYZ 成员:float x,y,z;表示了xyz3D信息,可以通过points[i].data[0]或points[i].x访问点X的坐标值

(2)pcl::PointCloudpcl::PointXYZI

PointXYZI成员:float x, y, z, intensity; 表示XYZ信息加上强度信息的类型。

(3)pcl::PointCloudpcl::PointXYZRGB

PointXYZRGB 成员:float x,y,z,rgb; 表示XYZ信息加上RGB信息,RGB存储为一个float。

(4)pcl::PointCloudpcl::PointXYZRGBA

PointXYZRGBA 成员:float x , y, z; uint32_t rgba; 表示XYZ信息加上RGBA信息,RGBA用32bit的int型存储的。

(5) PointXY 成员:float x,y;简单的二维x-y点结构

(6)Normal结构体:

表示给定点所在样本曲面上的法线方向,以及对应曲率的测量值,用第四个元素来占位,兼容SSE和高效计算

9)点云处理的三个层次

一般将图像处理分为三个层次,低层次包括图像强化,滤波,关键点/边缘检测等基本操作。中层次包括连通域标记(label),图像分割等操作。高层次包括物体识别,场景分析等操作。工程中的任务往往需要用到多个层次的图像处理手段。

低层次处理方法

①滤波方法:双边滤波、高斯滤波、条件滤波、直通滤波、随机采样一致性滤波。②关键点:ISS3D、Harris3D、NARF,SIFT3D

中层次处理方法

①特征描述:法线和曲率的计算、特征值分析、SHOT、PFH、FPFH、3D Shape Context、Spin Image

②分割与分类:

分割:区域生长、Ransac线面提取、全局优化平面提取

K-Means、Normalize Cut(Context based)

3D Hough Transform(线、面提取)、连通分析

分类:基于点的分类,基于分割的分类,基于深度学习的分类(PointNet,OctNet)

高层次处理方法

①配准

点云配准分为粗配准(Coarse Registration)和精配准(Fine Registration)两个阶段。

精配准的目的是在粗配准的基础上让点云之间的空间位置差别最小化。应用最为广泛的精配准算法应该是ICP以及ICP的各种变种(稳健ICP、point to plane ICP、Point to line ICP、MBICP、GICP、NICP)。

粗配准是指在点云相对位姿完全未知的情况下对点云进行配准,可以为精配准提供良好的初始值。当前较为普遍的点云自动粗配准算法包括基于穷举搜索的配准算法和基于特征匹配的配准算法。

基于穷举搜索的配准算法:

遍历整个变换空间以选取使误差函数最小化的变换关系或者列举出使最多点对满足的变换关系。如RANSAC配准算法、四点一致集配准算法(4-Point Congruent Set, 4PCS)、Super4PCS算法等……

基于特征匹配的配准算法:

通过被测物体本身所具备的形态特性构建点云间的匹配对应,然后采用相关算法对变换关系进行估计。如基于点FPFH特征的SAC-IA、FGR等算法、基于点SHOT特征的AO算法以及基于线特征的ICL等…

②SLAM图优化

Ceres(Google的最小二乘优化库,很强大), g2o、LUM、ELCH、Toro、SPA

SLAM方法:ICP、MBICP、IDC、likehood Field、NDT

③三维重建

泊松重建、 Delaunay triangulations、表面重建,人体重建,建筑物重建,树木重建。结构化重建:不是简单的构建一个Mesh网格,而是为场景进行分割,为场景结构赋予语义信息。场景结构有层次之分,在几何层次就是点线面。实时重建:重建植被或者农作物的4D(3D+时间)生长态势;人体姿势识别;表情识别;

④点云数据管理

点云压缩,点云索引(KD、Octree),点云LOD(金字塔),海量点云的渲染。

三维计算视觉研究内容

 1)三维匹配:两帧或者多帧点云数据之间的匹配,因为激光扫描光束受物体遮挡的原因,不可能通过一次扫描完成对整个物体的三维点云的获取。因此需要从不同的位置和角度对物体进行扫描。三维匹配的目的就是把相邻扫描的点云数据拼接在一起。三维匹配重点关注匹配算法,常用的算法有最近点迭代算法 ICP和各种全局匹配算法。

 2)多视图三维重建:计算机视觉中多视图一般利用图像信息,考虑多视几何的一些约束,射影几何和多视图几何是视觉方法的基础,在摄影测量中类似的存在共线方程。光束平差法是该类研究的核心技术。这里也将点云的多视匹配放在这里,比如人体的三维重建,点云的多视重建不再是简单的逐帧的匹配,还需要考虑不同角度观测产生误差累积,因此存在一个针对三维模型进行优化或者平差的过程在里面。多视图三维重建这里指的只是静态建模,输入是一系列的图像或者点云集合。可以只使用图像,或者只使用点云,也可以两者结合(深度图像)实现,重建的结果通常是Mesh网格。

  • SFM(运动恢复结构) vs Visual SLAM  [摘抄] SFM 和 Visual SLAM
  • Multi-View Stereo (MVS)多视图立体视觉,研究图像一致性,实现稠密重建。

  3)3D SLAM

  按照传感器类型分类:可以分为基于激光的SLAM和基于视觉的SLAM。

  基于激光的SLAM可以通过点云匹配(最近点迭代算法 ICP、正态分布变换方法 NDT)+位姿图优化(g2o、LUM、ELCH、Toro、SPA)来实现;实时激光3D SLAM算法 (LOAM,Blam,CartoGrapher等);Kalman滤波方法。通常激光3D SLAM侧重于定位,在高精度定位的基础上可以产生3D点云,或者Octree Map。

  基于视觉(单目、双目、鱼眼相机、深度相机)的SLAM,根据侧重点的不同,有的侧重于定位,有的侧重于表面三维重建。不过都强调系统的实时性

  (1)侧重于定位的VSLAM系统比如orbSLAM,lsdSLAM;VINS是IMU与视觉融合的不错的开源项目。

  

  (2)侧重于表面三维重建SLAM强调构建的表面最优,或者说表面模型最优,通常包含Fusion融合过程在里面。通常SLAM是通过观测形成闭环进行整体平差实现,优先保证位姿的精确;而VSLAM通过Fusion过程同时实现了对构建的表面模型的整体优化,保证表面模型最优。最典型的例子是KinectFusion,Kinfu,BundleFusion,RatMap等等。

  (4)目标检测与识别:无人驾驶汽车中基于激光数据检测场景中的行人、汽车、自行车、道路(车道线,道路标线,路边线)以及道路设施(路灯)和道路附属设施(行道树等)。这部分工作也是高精度电子地图的主要内容。当然高精度电子地图需要考虑的内容更多。同时室内场景的目标识别的研究内容也很丰富,比如管线设施,消防设施等。

  (5)形状检测与分类:点云技术在逆向工程中有很普遍的应用。构建大量的几何模型之后,如何有效的管理,检索是一个很困难的问题。需要对点云(Mesh)模型进行特征描述,分类。根据模型的特征信息进行模型的检索。同时包括如何从场景中检索某类特定的物体,这类方法关注的重点是模型。

  (6)语义分类:获取场景点云之后,如何有效的利用点云信息,如何理解点云场景的内容,进行点云的分类很有必要,需要为每个点云进行Labeling。可以分为基于点的分类方法和基于分割的分类方法。从方法上可以分为基于监督分类的技术或者非监督分类技术,深度学习也是一个很有希望应用的技术。最近深度学习进行点云场景理解的工作多起来了,比如PointNet,各种八叉树的Net。

(7)双目立体视觉与立体匹配 ZNCC:立体视觉(也称双目视觉)主要研究的两个相机的成像几何问题,研究内容主要包括:立体标定(Stereo Calibration)、立体校正(Stereo Rectification)和立体匹配(Stereo Matching)。目前,立体标定主要研究的已经比较完善,而立体匹配是立体视觉最核心的研究问题。按照匹配点数目分类,立体匹配可分为稀疏立体匹配(sparse stereo matching)和密集立体匹配(dense stereo matching)。稀疏立体匹配由于匹配点数量稀少,一般很难达到高精度移动测量和环境感知的要求。因此,密集立体匹配是学术界和工业界的主要研究和应用方向。

参考:https://mp.weixin.qq.com/s/cOHAQX12k19eogxfpk95tA

(8)自动造型(构型),快速造型(构型)技术。对模型进行凸分割,模型剖分,以实现模型进一步的编辑修改,派生出其他的模型。

(9)摄像测量技术,视频测量


PointNet++

论文:https://arxiv.org/abs/1706.02413(NIPS 2017)

code: https://github.com/charlesq34/pointnet2

1、改进

PointNet因为是只使用了MLP和max pooling,没有能力捕获局部结构,因此在细节处理和泛化到复杂场景上能力很有限。

  1. point-wise MLP,仅仅是对每个点表征,对局部结构信息整合能力太弱 –> PointNet++的改进:sampling和grouping整合局部邻域
  2. global feature直接由max pooling获得,无论是对分类还是对分割任务,都会造成巨大的信息损失 –> PointNet++的改进:hierarchical feature learning framework,通过多个set abstraction逐级降采样,获得不同规模不同层次的local-global feature
  3. 分割任务的全局特征global feature是直接复制与local feature拼接,生成discriminative feature能力有限 –> PointNet++的改进:分割任务设计了encoder-decoder结构,先降采样再上采样,使用skip connection将对应层的local-global feature拼接

2、方法

PointNet++的网络大体是encoder-decoder结构

encoder为降采样过程,通过多个set abstraction结构实现多层次的降采样,得到不同规模的point-wise feature,最后一个set abstraction输出可以认为是global feature。其中set abstraction由sampling,grouping,pointnet三个模块构成。

decoder根据分类和分割应用,又有所不同。分类任务decoder比较简单,不介绍了。分割任务decoder为上采样过程,通过反向插值和skip connection实现在上采样的同时,还能够获得local+global的point-wise feature,使得最终的表征能够discriminative(分辩能力)。

思考:

  1. PointNet++降采样过程是怎么实现的?/PointNet++是如何表征global feature的?(关注set abstraction, sampling layer, grouping layer, pointnet layer)
  2. PointNet++用于分割任务的上采样过程是怎么实现的?/PointNet++是如何表征用于分割任务的point-wise feature的?(关注反向插值,skip connection)

 🐖:上图中的 d 表示坐标空间维度, C 表示特征空间维度

2.1 encoder

在PointNet的基础上增加了hierarchical (层级)feature learning framework的结构。这种多层次的结构由set abstraction层组成。

在每一个层次的set abstraction,点集都会被处理和抽象,而产生一个规模更小的点集,可以理解成是一个降采样表征过程,可参考上图左半部分。

set abstraction由三个部分构成(代码贴在下面):

def pointnet_sa_module(xyz, points, npoint, radius, nsample, mlp, mlp2, group_all, is_training, bn_decay, scope, bn=True, pooling='max', knn=False, use_xyz=True, use_nchw=False):
    ''' PointNet Set Abstraction (SA) Module
        Input:
            xyz: (batch_size, ndataset, 3) TF tensor
            points: (batch_size, ndataset, channel) TF tensor
            npoint: int32 -- #points sampled in farthest point sampling
            radius: float32 -- search radius in local region
            nsample: int32 -- how many points in each local region
            mlp: list of int32 -- output size for MLP on each point
            mlp2: list of int32 -- output size for MLP on each region
            group_all: bool -- group all points into one PC if set true, OVERRIDE
                npoint, radius and nsample settings
            use_xyz: bool, if True concat XYZ with local point features, otherwise just use point features
            use_nchw: bool, if True, use NCHW data format for conv2d, which is usually faster than NHWC format
        Return:
            new_xyz: (batch_size, npoint, 3) TF tensor
            new_points: (batch_size, npoint, mlp[-1] or mlp2[-1]) TF tensor
            idx: (batch_size, npoint, nsample) int32 -- indices for local regions
    '''
    data_format = 'NCHW' if use_nchw else 'NHWC'
    with tf.variable_scope(scope) as sc:
        # Sample and Grouping
        if group_all:
            nsample = xyz.get_shape()[1].value
            new_xyz, new_points, idx, grouped_xyz = sample_and_group_all(xyz, points, use_xyz)
        else:
            new_xyz, new_points, idx, grouped_xyz = sample_and_group(npoint, radius, nsample, xyz, points, knn, use_xyz)
        # Point Feature Embedding
        if use_nchw: new_points = tf.transpose(new_points, [0,3,1,2])
        for i, num_out_channel in enumerate(mlp):
            new_points = tf_util.conv2d(new_points, num_out_channel, [1,1],
                                        padding='VALID', stride=[1,1],
                                        bn=bn, is_training=is_training,
                                        scope='conv%d'%(i), bn_decay=bn_decay,
                                        data_format=data_format) 
        if use_nchw: new_points = tf.transpose(new_points, [0,2,3,1])
        # Pooling in Local Regions
        if pooling=='max':
            new_points = tf.reduce_max(new_points, axis=[2], keep_dims=True, name='maxpool')
        elif pooling=='avg':
            new_points = tf.reduce_mean(new_points, axis=[2], keep_dims=True, name='avgpool')
        elif pooling=='weighted_avg':
            with tf.variable_scope('weighted_avg'):
                dists = tf.norm(grouped_xyz,axis=-1,ord=2,keep_dims=True)
                exp_dists = tf.exp(-dists * 5)
                weights = exp_dists/tf.reduce_sum(exp_dists,axis=2,keep_dims=True) # (batch_size, npoint, nsample, 1)
                new_points *= weights # (batch_size, npoint, nsample, mlp[-1])
                new_points = tf.reduce_sum(new_points, axis=2, keep_dims=True)
        elif pooling=='max_and_avg':
            max_points = tf.reduce_max(new_points, axis=[2], keep_dims=True, name='maxpool')
            avg_points = tf.reduce_mean(new_points, axis=[2], keep_dims=True, name='avgpool')
            new_points = tf.concat([avg_points, max_points], axis=-1)
        # [Optional] Further Processing 
        if mlp2 is not None:
            if use_nchw: new_points = tf.transpose(new_points, [0,3,1,2])
            for i, num_out_channel in enumerate(mlp2):
                new_points = tf_util.conv2d(new_points, num_out_channel, [1,1],
                                            padding='VALID', stride=[1,1],
                                            bn=bn, is_training=is_training,
                                            scope='conv_post_%d'%(i), bn_decay=bn_decay,
                                            data_format=data_format) 
            if use_nchw: new_points = tf.transpose(new_points, [0,2,3,1])
        new_points = tf.squeeze(new_points, [2]) # (batch_size, npoints, mlp2[-1])
        return new_xyz, new_points, idx

2.1.1 sampling layer

使用FPS(最远点采样)对点集进行降采样,将输入点集从规模 N1 降到更小的规模 N2 。FPS可以理解成是使得采样的各个点之间尽可能远,这种采样的好处是可以降采样结果会比较均匀。

FPS实现方式如下:随机选择一个点作为初始点作为已选择采样点,计算未选择采样点集中每个点与已选择采样点集之间的距离distance,将距离最大的那个点加入已选择采样点集,然后更新distance,一直循环迭代下去,直至获得了目标数量的采样点。

class FarthestSampler:
    def __init__(self):
        pass
    def _calc_distances(self, p0, points):
        return ((p0 - points) ** 2).sum(axis=1)
    def __call__(self, pts, k):
        farthest_pts = np.zeros((k, 3), dtype=np.float32)
        farthest_pts[0] = pts[np.random.randint(len(pts))]
        distances = self._calc_distances(farthest_pts[0], pts)
        for i in range(1, k):
            farthest_pts[i] = pts[np.argmax(distances)]
            distances = np.minimum(
                distances, self._calc_distances(farthest_pts[i], pts))
        return farthest_pts

输入规模为 B∗N∗(d+C) ,其中 B 表示batch size, N 表示点集中点的数量, d 表示点的坐标维度, C 表示点的其他特征(比如法向量等)维度。一般 d=3 , c=0

输出规模为 B∗N1∗(d+C) , N1<N ,因为这是一个降采样过程。

sampling和grouping具体实现是写在一个函数里的:

def sample_and_group(npoint, radius, nsample, xyz, points, knn=False, use_xyz=True):
    '''
    Input:
        npoint: int32
        radius: float32
        nsample: int32
        xyz: (batch_size, ndataset, 3) TF tensor
        points: (batch_size, ndataset, channel) TF tensor, if None will just use xyz as points
        knn: bool, if True use kNN instead of radius search
        use_xyz: bool, if True concat XYZ with local point features, otherwise just use point features
    Output:
        new_xyz: (batch_size, npoint, 3) TF tensor
        new_points: (batch_size, npoint, nsample, 3+channel) TF tensor
        idx: (batch_size, npoint, nsample) TF tensor, indices of local points as in ndataset points
        grouped_xyz: (batch_size, npoint, nsample, 3) TF tensor, normalized point XYZs
            (subtracted by seed point XYZ) in local regions
    '''
    new_xyz = gather_point(xyz, farthest_point_sample(npoint, xyz)) # (batch_size, npoint, 3)
    if knn:
        _,idx = knn_point(nsample, xyz, new_xyz)
    else:
        idx, pts_cnt = query_ball_point(radius, nsample, xyz, new_xyz)
    grouped_xyz = group_point(xyz, idx) # (batch_size, npoint, nsample, 3)
    grouped_xyz -= tf.tile(tf.expand_dims(new_xyz, 2), [1,1,nsample,1]) # translation normalization
    if points is not None:
        grouped_points = group_point(points, idx) # (batch_size, npoint, nsample, channel)
        if use_xyz:
            new_points = tf.concat([grouped_xyz, grouped_points], axis=-1) # (batch_size, npoint, nample, 3+channel)
        else:
            new_points = grouped_points
    else:
        new_points = grouped_xyz
    return new_xyz, new_points, idx, grouped_xyz

其中sampling对应的部分是:

new_xyz = gather_point(xyz, farthest_point_sample(npoint, xyz)) # (batch_size, npoint, 3)

xyz既是 B∗N∗3 的点云,npoint是降采样点的规模。注意:PointNet++的FPS均是在坐标空间做的,而不是在特征空间做的。这一点很关键,因为FPS本身是不可微的,无法计算梯度反向传播。

本着刨根问题的心态,我们来看看farthest_point_sample和gather_point究竟在做什么

farthest_point_sample输入输出非常明晰,输出的是降采样点在inp中的索引,因此是 B∗N1 int32类型的张量

def farthest_point_sample(npoint,inp):
    '''
input:
    int32
    batch_size * ndataset * 3   float32
returns:
    batch_size * npoint         int32
    '''
    return sampling_module.farthest_point_sample(inp, npoint)

gather_point的作用就是将上面输出的索引,转化成真正的点云

def gather_point(inp,idx):
    '''
input:
    batch_size * ndataset * 3   float32
    batch_size * npoints        int32
returns:
    batch_size * npoints * 3    float32
    '''
    return sampling_module.gather_point(inp,idx)

2.1.2 grouping layer

上一步sampling的过程是将 N∗(d+C) 降到 N1∗(d+C) (这里论述方便先不考虑batch,就考虑单个点云),实际上可以理解成是在 N 个点中选取 N1 个中心点(key point)。

那么这一步grouping的目的就是以这每个key point为中心,找其固定规模(令规模为 K)的邻点,共同组成一个局部邻域(patch)。也就是会生成 N1 个局部邻域,输出规模为 N1∗K∗(d+C)

if knn:
    _,idx = knn_point(nsample, xyz, new_xyz)
else:
    idx, pts_cnt = query_ball_point(radius, nsample, xyz, new_xyz)
    grouped_xyz = group_point(xyz, idx) # (batch_size, npoint, nsample, 3)

1)找邻域的过程也是在坐标空间进行(也就是以上代码输入输出维度都是 d ,没有 C  C 是在后面的代码拼接上的),而不是特征空间。

2)找邻域这里有两种方式:KNN和query ball point.

其中前者KNN就是大家耳熟能详的K近邻,找K个坐标空间最近的点。
后者query ball point就是划定某一半径,找在该半径球内的点作为邻点。

还有个问题:query ball point如何保证对于每个局部邻域,采样点的数量都是一样的呢?
事实上,如果query ball的点数量大于规模 K ,那么直接取前 K 个作为局部邻域;如果小于,那么直接对某个点重采样,凑够规模 K

KNN和query ball的区别:(摘自原文)Compared with kNN, ball query’s local neighborhood guarantees a fixed region scale thus making local region feature more generalizable across space, which is preferred for tasks requiring local pattern recognition (e.g. semantic point labeling).也就是query ball更加适合于应用在局部/细节识别的应用上,比如局部分割。

补充材料中也有实验来对比KNN和query ball:

sample_and_group代码的剩余部分:

sample和group操作都是在坐标空间进行的,因此如果还有特征空间信息(即point-wise feature),可以在这里将其与坐标空间拼接,组成新的point-wise feature,准备送入后面的unit point进行特征学习。

if points is not None:
    grouped_points = group_point(points, idx) # (batch_size, npoint, nsample, channel)
    if use_xyz:
        new_points = tf.concat([grouped_xyz, grouped_points], axis=-1) # (batch_size, npoint, nample, 3+channel)
    else:
        new_points = grouped_points
else:
    new_points = grouped_xyz

2.1.3 PointNet layer

使用PointNet对以上结果表征

输入 B∗N∗K∗(d+C) ,输出 B∗N∗(d+C1)

以下代码主要分成3个部分:

1)point feature embedding

这里输入是 B∗N∗K∗(d+C) ,可以类比成是batch size为 B ,宽高为 N∗K ,通道数为 d+C 的图像,这样一类比,这里的卷积就好理解多了实际上就是 1∗1 卷积,不改变feature map大小,只改变通道数,将通道数升高,实现所谓“embedding”

这部分输出是 B∗N∗K∗C1

2)pooling in local regions

pooling,只是是对每个局部邻域pooling,输出是 B∗N∗1∗C1

3)further processing

再对池化后的结果做MLP,也是简单的 1∗1 卷积。这一部分在实际实验中PointNet++并没有设置去做

# Point Feature Embedding
if use_nchw: new_points = tf.transpose(new_points, [0,3,1,2])
for i, num_out_channel in enumerate(mlp):
    new_points = tf_util.conv2d(new_points, num_out_channel, [1,1],
                                padding='VALID', stride=[1,1],
                                bn=bn, is_training=is_training,
                                scope='conv%d'%(i), bn_decay=bn_decay,
                                data_format=data_format) 
if use_nchw: new_points = tf.transpose(new_points, [0,2,3,1])

# Pooling in Local Regions
if pooling=='max':
    new_points = tf.reduce_max(new_points, axis=[2], keep_dims=True, name='maxpool')
elif pooling=='avg':
    new_points = tf.reduce_mean(new_points, axis=[2], keep_dims=True, name='avgpool')
elif pooling=='weighted_avg':
    with tf.variable_scope('weighted_avg'):
        dists = tf.norm(grouped_xyz,axis=-1,ord=2,keep_dims=True)
        exp_dists = tf.exp(-dists * 5)
        weights = exp_dists/tf.reduce_sum(exp_dists,axis=2,keep_dims=True) # (batch_size, npoint, nsample, 1)
        new_points *= weights # (batch_size, npoint, nsample, mlp[-1])
        new_points = tf.reduce_sum(new_points, axis=2, keep_dims=True)
elif pooling=='max_and_avg':
    max_points = tf.reduce_max(new_points, axis=[2], keep_dims=True, name='maxpool')
    avg_points = tf.reduce_mean(new_points, axis=[2], keep_dims=True, name='avgpool')
    new_points = tf.concat([avg_points, max_points], axis=-1)

# [Optional] Further Processing 
if mlp2 is not None:
    if use_nchw: new_points = tf.transpose(new_points, [0,3,1,2])
    for i, num_out_channel in enumerate(mlp2):
        new_points = tf_util.conv2d(new_points, num_out_channel, [1,1],
                                    padding='VALID', stride=[1,1],
                                    bn=bn, is_training=is_training,
                                    scope='conv_post_%d'%(i), bn_decay=bn_decay,
                                    data_format=data_format) 
    if use_nchw: new_points = tf.transpose(new_points, [0,2,3,1])

2.1.4 Encoder还有一个问题

pointnet++实际上就是对局部邻域表征。

那就不得不面对一个挑战:non-uniform sampling density(点云的密度不均匀),也就是在稀疏点云局部邻域训练可能不能很好挖掘点云的局部结构

PointNet++做法:learn to combine features from regions of different scales when the input sampling density changes.

因此文章提出了两个方案:

一、Multi-scale grouping(MSG)

对当前层的每个中心点,取不同radius的query ball,可以得到多个不同大小的同心球,也就是得到了多个相同中心但规模不同的局部邻域,分别对这些局部邻域表征,并将所有表征拼接。如上图所示。

该方法比较麻烦,运算较多。

代码层面其实就是加了个遍历radius_list的循环,分别处理,并最后concat

new_xyz = gather_point(xyz, farthest_point_sample(npoint, xyz))
new_points_list = []
for i in range(len(radius_list)):
    radius = radius_list[i]
    nsample = nsample_list[i]
    idx, pts_cnt = query_ball_point(radius, nsample, xyz, new_xyz)
    grouped_xyz = group_point(xyz, idx)
    grouped_xyz -= tf.tile(tf.expand_dims(new_xyz, 2), [1,1,nsample,1])
    if points is not None:
        grouped_points = group_point(points, idx)
        if use_xyz:
            grouped_points = tf.concat([grouped_points, grouped_xyz], axis=-1)
    else:
        grouped_points = grouped_xyz
    if use_nchw: grouped_points = tf.transpose(grouped_points, [0,3,1,2])
    for j,num_out_channel in enumerate(mlp_list[i]):
        grouped_points = tf_util.conv2d(grouped_points, num_out_channel, [1,1],
                                        padding='VALID', stride=[1,1], bn=bn, is_training=is_training,
                                        scope='conv%d_%d'%(i,j), bn_decay=bn_decay)
    if use_nchw: grouped_points = tf.transpose(grouped_points, [0,2,3,1])
    new_points = tf.reduce_max(grouped_points, axis=[2])
    new_points_list.append(new_points)
new_points_concat = tf.concat(new_points_list, axis=-1)

二、Multi-resolution grouping(MRG)

(摘自原文)features of a region at some level Li is a concatenation of two vectors.

One vector (left in figure) is obtained by summarizing the features at each subregion from the lower level Li−1 using the set abstraction level.

The other vector (right) is the feature that is obtained by directly processing all raw points in the local region using a single PointNet.

简单来说,就是当前set abstraction的局部邻域表征由两部分构成:

左边表征:对上一层set abstraction(还记得上一层的点规模是更大的吗?)各个局部邻域(或者说中心点)的特征进行聚合。 右边表征:使用一个单一的PointNet直接在局部邻域处理原始点云

2.2 decoder:

2.2.1 分类任务的decoder

比较简单,将encoder降采样得到的global feature送入几层全连接网络,最后通过一个softmax分类。

2.2.2 分割任务的decoder

经过前半部分的encoder,我们得到的是global feature,或者是极少数点的表征(其实也就是global feature)

而如果做分割,我们需要的是point-wise feature,这可怎么办呢?

PointNet处理思路很简单,直接把global feature复制并与之前的local feature拼接,使得这个新point-wise feature能够获得一定程度的“邻域”信息。这种简单粗暴的方法显然并不能得到很discriminative的表征

别急,PointNet++来了。

PointNet++设计了一种反向插值的方法来实现上采样的decoder结构,通过反向插值和skip connection来获得discriminative point-wise feature:

设红色矩形点集 P1 : N1∗C ,蓝色矩形点集 P2 : N2∗C2 ,因为decoder是上采样过程,因此 N2>N1

一、反向插值具体做法:

对于 P2 中的每个点 x ,找在原始点云坐标空间下, P1 中与其最接近的 k 个点 x1,…,xk

当前我们想通过反向插值的方式用较少的点把更多的点的特征插出来,实现上采样

此时 x1,…,xk 的特征我们是知道的,我们想得到 x 的特征。如上公式,实际上就是将 x1,…,xk 的特征加权求和,得到x的特征。其中这个权重是与x和 x1,…,xk 的距离成反向相关的,意思就是距离越远的点,对x特征的贡献程度越小。P2 中其他点以此类推,从而实现了特征的上采样回传

skip connection具体做法:

回传得到的point-wise feature是从decoder的上一层得到的,因此算是global级别的信息,这对于想得到discriminative还是不够,因为我们还缺少local级别的信息!!!

如上图就是我们反向插值只得到了 C2 ,但是我们还需要提供local级别信息的 C1 特征!!!

这时skip connection来了!!!

skip connection其实就是将之前encoder对应层的表征直接拼接了过来

因为上图中encoder蓝色矩形点集的 C1 表征是来自于规模更大的绿色矩形点集的表征,这在一定程度上其实是实现了local级别的信息

我们通过反向插值和skip connection在decoder中逐级上采样得到local + global point-wise feature,得到了discriminative feature,应用于分割任务。

2.3 loss

无论是分类还是分割应用,本质上都是分类问题,因此loss就是分类任务中常用的交叉熵loss

2.4 其他的问题

Q:PointNet++梯度是如何回传的???

A:PointNet++ fps实际上并没有参与梯度计算和反向传播。

可以理解成是PointNet++将点云进行不同规模的fps降采样,事先将这些数据准备好,再送到网络中去训练的

3 dataset and experiments

3.1 dataset

  • MNIST: Images of handwritten digits with 60k training and 10k testing samples.(用于分类)
  • ModelNet40: CAD models of 40 categories (mostly man-made). We use the official split with 9,843 shapes for training and 2,468 for testing. (用于分类)
  • SHREC15: 1200 shapes from 50 categories. Each category contains 24 shapes which are mostly organic ones with various poses such as horses, cats, etc. We use five fold cross validation to acquire classification accuracy on this dataset. (用于分类)
  • ScanNet: 1513 scanned and reconstructed indoor scenes. We follow the experiment setting in [5] and use 1201 scenes for training, 312 scenes for test. (用于分割)

3.2 experiments

主要关心的实验结果是2个:

  1. ModelNet40分类结果
  2. ShapeNet Part分割结果
PointNet++也做了ShapeNet part数据集上的part segmentation:

4、 conclusion

PointNet++是PointNet的续作,在一定程度上弥补了PointNet的一些缺陷,表征网络基本和PN类似,还是MLP、 1∗1 卷积、pooling那一套,核心创新点在于设计了局部邻域的采样表征方法和这种多层次的encoder-decoder结合的网络结构。

第一次看到PointNet++网络结构,觉得设计得非常精妙,特别是设计了上采样和下采样的具体实现方法,并以此用于分割任务的表征,觉得设计得太漂亮了。但其实无论是分类还是分割任务,提升幅度较PointNet也就是1-2个点而已。

PointNet++,特别是其前半部分encoder,提供了非常好的表征网络,后面很多点云处理应用的论文都会使用到PointNet++作为它们的表征器。

pointnet–基于点云的分类和分割深度学习算法

论文:https://arxiv.org/abs/1612.00593(cvpr2017)

code:https://github.com/charlesq34/pointnet

基础知识:

1、什么是点云?

简单来说就是一堆三维点的集合,必须包括各个点的三维坐标信息,其他信息比如各个点的法向量、颜色等均是可选。点云的文件格式可以有很多种,包括xyz,npy,ply,obj,off等(有些是mesh不过问题不大,因为mesh可以通过泊松采样等方式转化成点云)。对于单个点云,如果你使用np.loadtxt得到的实际上就是一个维度为 (num_points,num_channels) 的张量,num_channels一般为3,表示点云的三维坐标。

这里以horse.xyz文件为例,实际就是文本文件,打开后数据长这样(局部,总共有2048个点):

实际就是一堆点的信息,这里只有三维坐标,将其可视化出来长这样:

2、点云处理任务是重要的

三维图形具有多种表现形式,包括了mesh、体素、点云等,甚至还有些方法使用多视图来对三维图形表征。而点云在以上各种形式的数据中算是日常生活中最能够大规模获取和使用的数据结构了,包括自动驾驶、增强现实等在内的应用需要直接或间接从点云中提取信息,点云处理也逐渐成为计算机视觉非常重要的一部分。

正文:

PointNet所作的事情就是对点云做特征学习,并将学习到的特征去做不同的应用:分类(shape-wise feature)、分割(point-wise feature)等。

PointNet之所以影响力巨大,就是因为它为点云处理提供了一个简单、高效、强大的特征提取器(encoder),几乎可以应用到点云处理的各个应用中,其地位类似于图像领域的AlexNet。

1、动机

点云或者mesh,大多数研究人员都是将其转化成3D体素或者多视图来做特征学习的,这其中的工作包括了VoxelNet, MVCNN等。这些工作都或多或少存在了一些问题。

直接对点云做特征学习也不是不可以,但有几个问题需要考虑:特征学习需要对点云中各个点的排列保持不变性、特征学习需要对rigid transformation保持不变性等。虽然有挑战,但是深度学习强大的表征能力以及其在图像领域取得的巨大成功,因此是很有必要直接在点云上进行尝试的。

2、贡献

  1. 我们设计了一个新颖的深层网络架构来处理三维中的无序点集
  2. 我们设计的网络表征可以做三维图形分类、图形的局部分割以及场景的语义分割等任务
  3. 我们提供了完备的经验和理论分析来证明PointNet的稳定和高效。
  4. 充分的消融实验,证明网络各个部分对于表征的有效性。

3、方法

3.1 点云的几个特点:

  1. 无序性 –> 对称函数设计用于表征
  2. 点不是孤立的,需要考虑局部结构 –> 局部全局特征结合
  3. 仿射变换无关性 –> alignment network

(重要)关于第三点:相同的点云在空间中经过一定的刚性变化(旋转或平移),坐标发生变化。其实对于点云分类or分割任务来说(分割可以认为是点的分类),例如,整体的旋转和平移不应修改全局点云类别和每个点的类别,也不应修改点的分割因此需要保证仿射变换无关性(简单来说,“仿射变换”就是:“线性变换”+“平移”),但是对于位置 敏感的 任务:点云配准、点云补全任务,对于位置敏感,就不需要保证 仿射变换的无关性

我们希望不论点云在怎样的坐标系下呈现,网络都能正确的识别出。这个问题可以通过STN(spacial transform netw)来解决。三维不太一样的是点云是一个不规则的结构(无序,无网格),不需要重采样的过程。pointnet通过学习一个矩阵来达到对目标最有效的变换。

解决方法

  1. 空间变换网络解决旋转问题:三维的STN可以通过学习点云本身的位姿信息学习到一个最有利于网络进行分类或分割的DxD旋转矩阵(D代表特征维度,pointnet中D采用3和64)。至于其中的原理,我的理解是,通过控制最后的loss来对变换矩阵进行调整,pointnet并不关心最后真正做了什么变换,只要有利于最后的结果都可以。pointnet采用了两次STN,第一次input transform是对空间中点云进行调整,直观上理解是旋转出一个更有利于分类或分割的角度,比如把物体转到正面;第二次feature transform是对提取出的64维特征进行对齐,即在特征层面对点云进行变换。
  2. maxpooling解决无序性问题:网络对每个点进行了一定程度的特征提取之后,maxpooling可以对点云的整体提取出global feature。

3.2 网络结构:

batchnormal对于上采样任务来说效果不好

网络分成了分类网络和分割网络2个部分,大体思路类似,都是设计表征的过程分类网络设计global feature,分割网络设计point-wise feature。两者都是为了让表征尽可能discriminative,也就是同类的能分到一类,不同类的距离能拉开。

输入 n*3 n是点数

inputtransform:放射变换(为了保证仿射变换的不变性):直接预测一个变换矩阵(3*3)来处理输入点的坐标(对所有坐标进行变换)。因为会有数据增强的操作存在,这样做可以在一定程度上保证网络可以学习到变换无关性。T-Net模型,它的主要作用是学习出变化矩阵来对输入的点云或特征进行规范化处理。

MLP:

有两种实现方法:

1、输入 B,N,3 —- nn.liner层 — B,N,64

2、输入 B,3,N —- conv1d(1×1) — B,64,N

Pooling:

为了解决无序性(点云本质上是一长串点(nx3矩阵,其中n是点数)。在几何上,点的顺序不影响它在空间中对整体形状的表示,例如,相同的点云可以由两个完全不同的矩阵表示。)使用 maxpooling或sumpooling,也就是说,最后的D维特征对每一维都选取N个点中对应的最大特征值或特征值总和,这样就可以通过g来解决无序性问题。

最后再经过一个mlp(代码中运用全连接)得到k个score。分类网络最后接的loss是softmax。

分割网络:

将池化后的特征和前一阶段特征拼接,池化后的特征有全局信息,在和之前的拼接,以此得到同时对局部信息和全局信息感知的point-wise特征,提升表征效果。然后最后输出n*m, m为类别数量,表示每个点的类别信息。

损失函数:

分类中常用的交叉熵+alignment network中用于约束生成的alignment matrix的loss

dataset and experiments

evaluate metric

分类:分类准确率acc
分割:mIoU

dataset

分类:ModelNet40
分割:ShapeNet Part dataset和Stanford 3D semantic parsing dataset

experiments

1、分类:

2、局部分割:

code:

1. 如何对点云使用MLP?
2. alignment network怎么做的?
3. 对称函数如何实现来提取global feature的?
4. loss?

def get_model(point_cloud, is_training, bn_decay=None):
    """ Classification PointNet, input is BxNx3, output Bx40 """
    batch_size = point_cloud.get_shape()[0].value
    num_point = point_cloud.get_shape()[1].value
    end_points = {}
    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)
    input_image = tf.expand_dims(point_cloud_transformed, -1)
    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)
    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)
    with tf.variable_scope('transform_net2') as sc:
        transform = feature_transform_net(net, is_training, bn_decay, K=64)
    end_points['transform'] = transform
    net_transformed = tf.matmul(tf.squeeze(net, axis=[2]), transform)
    net_transformed = tf.expand_dims(net_transformed, [2])
    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)
    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)
    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)
    # Symmetric function: max pooling
    net = tf_util.max_pool2d(net, [num_point,1],
                             padding='VALID', scope='maxpool')
    net = tf.reshape(net, [batch_size, -1])
    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')
    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')
    net = tf_util.fully_connected(net, 40, activation_fn=None, scope='fc3')
    return net, end_points

MLP的核心做法:

input_image = tf.expand_dims(point_cloud_transformed, -1)
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)
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)

这里input_image维度是 B×N×3×1 ,因此将点云看成是W和H分为N和3的2D图像,维度是 1

然后直接基于这个“2D图像”做卷积,第一个卷积核size是 [1,3] ,正好对应的就是“2D图像”的一行,也就是一个点(三维坐标),输出通道数是64,因此输出张量维度应该是 B×N×1×64

第二个卷积核size是 [1,1] , 1∗1 卷积只改变通道数,输出张量维度是 B×N×1×64

conv2d就是将卷积封装了一下,核心部分也就是调用tf.nn.conv2d,实现如下:

def conv2d(inputs,
           num_output_channels,
           kernel_size,
           scope,
           stride=[1, 1],
           padding='SAME',
           use_xavier=True,
           stddev=1e-3,
           weight_decay=0.0,
           activation_fn=tf.nn.relu,
           bn=False,
           bn_decay=None,
           is_training=None):
  """ 2D convolution with non-linear operation.
  Args:
    inputs: 4-D tensor variable BxHxWxC
    num_output_channels: int
    kernel_size: a list of 2 ints
    scope: string
    stride: a list of 2 ints
    padding: 'SAME' or 'VALID'
    use_xavier: bool, use xavier_initializer if true
    stddev: float, stddev for truncated_normal init
    weight_decay: float
    activation_fn: function
    bn: bool, whether to use batch norm
    bn_decay: float or float tensor variable in [0,1]
    is_training: bool Tensor variable
  Returns:
    Variable tensor
  """
  with tf.variable_scope(scope) as sc:
      kernel_h, kernel_w = kernel_size
      num_in_channels = inputs.get_shape()[-1].value
      kernel_shape = [kernel_h, kernel_w,
                      num_in_channels, num_output_channels]
      kernel = _variable_with_weight_decay('weights',
                                           shape=kernel_shape,
                                           use_xavier=use_xavier,
                                           stddev=stddev,
                                           wd=weight_decay)
      stride_h, stride_w = stride
      outputs = tf.nn.conv2d(inputs, kernel,
                             [1, stride_h, stride_w, 1],
                             padding=padding)
      biases = _variable_on_cpu('biases', [num_output_channels],
                                tf.constant_initializer(0.0))
      outputs = tf.nn.bias_add(outputs, biases)
      if bn:
        outputs = batch_norm_for_conv2d(outputs, is_training,
                                        bn_decay=bn_decay, scope='bn')
      if activation_fn is not None:
        outputs = activation_fn(outputs)
      return outputs

alignment network :

input_transform_net为例:

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)
    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)
    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)
    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)
    net = tf_util.max_pool2d(net, [num_point,1],
                             padding='VALID', scope='tmaxpool')
    net = tf.reshape(net, [batch_size, -1])
    net = tf_util.fully_connected(net, 512, bn=True, is_training=is_training,
                                  scope='tfc1', bn_decay=bn_decay)
    net = tf_util.fully_connected(net, 256, bn=True, is_training=is_training,
                                  scope='tfc2', bn_decay=bn_decay)
    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)
    transform = tf.reshape(transform, [batch_size, 3, K])
    return transform

实际上,前半部分就是通过卷积和max_pooling对batch内各个点云提取global feature,再将global feature降到 3×K 维度,并reshape成 3×3 ,得到transform matrix

通过数据增强丰富训练数据集,网络确实应该学习到有效的transform matrix,用来实现transformation invariance

loss

监督分类任务中常用的交叉熵loss + alignment network中的mat_diff_loss

 对于特征空间的alignment network,由于特征空间维度比较高,因此直接生成的alignment matrix会维度特别大,不好优化,因此这里需要加个loss约束一下。

总结:

PointNet之所以影响力巨大,并不仅仅是因为它是第一篇,更重要的是它的网络很简洁(简洁中蕴含了大量的工作来探寻出简洁这条路)却非常的work,这也就使得它能够成为一个工具,一个为点云表征的encoder工具,应用到更广阔的点云处理任务中。

MLP+max pooling竟然就击败了众多SOTA,令人惊讶。另外PointNet在众多细节设计也都进行了理论分析和消融实验验证,保证了严谨性,这也为PointNet后面能够大规模被应用提供了支持。