论文阅读+代码解读

论文标题:Siamese KPConv: 3D multiple change detection from raw point clouds using deep learning
期刊:2023,ISPRS Journal of Photogrammetry and Remote Sensing
代码:https://github.com/IdeGelis/torch-points3d-SiameseKPConv

写在前面:博客内容为个人观点和个人总结,并非完全的论文翻译,如果想了解论文具体内容,请看原论文

摘要

这篇文章是点云变化检测的,主要是针对城市场景。
本文提出了一种对原始3D数据直接进行变化检测的方法(栅格化数据(Rasterized Data)会导致信息丢失,因此更倾向于使用原始3D数据)----Siamese KPConv网络.

栅格化数据(Rasterized Data)
栅格化是将点云数据转换为规则的二维或三维网格的过程。每个网格单元(称为像素或体素)包含一个或多个点的信息。在二维栅格化中,点云数据通常被投影到一个平面上,生成类似于图像的二维栅格(例如数字表面模型DSM)。在三维栅格化中,点云数据被分割成三维体素(Voxel),每个体素表示一个小立方体空间内的点云信息

introduction

3D点云变化检测(change detection)可以分为几种类型。
二分类还是多分类、点云尺度还是点尺度。
如下图所示,输入一对点云数据,类型A输出一个二进制的结果(变化/没变化),类型B不光检测到变化,对变化的物体进行了分类,New building,所以它的结果是一个多分类结果(没变化、变化的物体是什么)。但是这两个都属于点云尺度上的变化检测,因为他们的检测对象是整个点云对,输出结果是整个点云对的检测结果。
C和D则是点尺度的检测,可以看到C中对每一个点进行了分类,紫色的是没有发生改变的,黄色的是发生改变的,这其实也是一个二分类结果,但是他对每一个点都进行了二分类,这就属于针对变化的分割了。D也是对每个点的分类,但是他不是二分类,而是多个类别(没变化、new building、new clutter等)
在这里插入图片描述

那么这几类变化检测,我们从实用的角度很容易可以看出,D是我们想要实现的效果。因为在较大规模的点云数据中,通常物体不止一类,且变化的物体也不止一类,所以二分类的检测不能满足要求,只知道变化或者没变化对我们来说还不够,因此需要多分类的检测。并且点云尺度的检测只适用于小规模的点云数据,物体较为单一的,检测对象较为单一的情况,复杂的真实的场景还是需要点尺度上的多分类变化检测。

本文的主要贡献:

  1. Siamese KPConv网络:一种能够直接从原始3D点云中进行多类变化分割的深度学习方法。
  2. 三个新的数据集:包括合成的Urb3DCD-V2数据集及其分类版本,以及基于真实数据的AHN-CD数据集

Related work

3D点云变化检测:
现有方法分类两类:

  1. 后分类方法:首先对每个点云进行语义分割,然后比较不同时间点的标签以检测变化。这类方法容易累积每个步骤的误差。
  2. 预分类方法:直接检测变化,然后对变化进行分类。这类方法同样存在误差累积的问题。
    Siamese架构:
    Siamese网络最初用于计算机视觉任务,属于双流架构,由两个相同的分支组成,每个分支处理一个输入数据(如图像或点云),然后通过特征差异来检测变化。Siamese网络可以分为:
  3. 纯Siamese网络:两个分支共享权重。
  4. 伪Siamese网络:两个分支不共享权重,灵活性更高,但训练参数更多。
    Siamese全卷积网络(FCN)在变化检测中表现出色,通常结合U-Net架构,通过跳跃连接融合编码器和解码器的信息。
    3D点云的深度学习方法:
    处理3D点云的深度学习方法主要分为三类:
  5. 基于投影的方法:将3D点云投影到2D平面(如栅格或球面),然后应用传统的2D图像处理方法。
  6. 基于离散化的方法:将3D点云转换为3D体素网格,便于使用3D卷积操作,但会丢失信息且计算成本高。
  7. 基于点的方法:直接处理原始点云,避免了投影和离散化的缺点。PointNet是这类方法的代表,能够学习逐点特征和全局特征,但其在处理大规模点云时存在局限性。
    近年来,基于点卷积的方法逐渐流行,例如Kernel Point Convolution (KPConv),它在点云分割和分类任务中表现出色,尤其是在大规模城市点云数据集上。KPConv通过定义核点来对点云进行卷积操作,能够处理点云的稀疏性和不规则分布。

KPConv是一种专门为点云数据设计的卷积操作。
类似于图像,图像中的卷积操作依靠卷积核,对一定范围内的像素点进行相关运算。
KPConv定义了点云中点的卷积操作,它依赖于核点对一定范围内的点进行运算。
KPConv 使用一组核点来定义卷积权重,核点是预定义的一组点,核点的数量可以任意设置,它们的位置和形状可以自适应地进行调整。KPConv通过计算输入点云中的每个点与核点之间的关系,根据关系进行加权聚合等步骤,实现了一种适应点云数据特点的卷积操作。
关于KPConv的详细做法,可以阅读KPConv: Flexible and Deformable Convolution for Point Clouds

Method

大致的思路就是,参考2D图像变化检测,使用Siamese FCN,将其中涉及的标准2D卷积替换为3D点云卷积(KPConv),来实现点云变化检测。
在这里插入图片描述
如图所示,输入为两个点云,两个点云都将通过编码器,在过程中会计算两个点云的差异,其特征会保存下来,最后送入解码器。
整个结构就是一个Unet结构,

def forward(self, compute_loss = True, *args, **kwargs) -> Any:
        """Run forward pass. This will be called by both functions <optimize_parameters> and <test>."""
        stack_down = [] ## 用于存储下采样阶段的中间特征,这些特征将在上采样阶段使用
        # 两个输入点云的数据
        data0 = self.input0
        data1 = self.input1
        ## 遍历所有的下采样模块(编码器)--除最后一层外(后面会单独处理)
        for i in range(len(self.down_modules) - 1):
            ## 对于每一对点云(data0 和 data1),分别通过下采样模块处理
            data0 = self.down_modules[i](data0, precomputed=self.pre_computed)
            data1 = self.down_modules[i](data1, precomputed=self.pre_computed_target)
            # 使用 knn(k-最近邻)查找 data1 中每个点在 data0 中的最近邻点
            nn_list = knn(data0.pos, data1.pos, 1, data0.batch, data1.batch)
            # 计算 data1 和 data0 的特征差异(diff.x),并将差异特征存储到 stack_down 中
            diff = data1.clone()
            diff.x = data1.x - data0.x[nn_list[1,:],:]
            stack_down.append(diff)
        #1024 : last layer  最后一层下采样模块单独处理
        # 与循环中的操作一样 将最后一层data1和data0的特征差异结果存储到data中
        data0 = self.down_modules[-1](data0, precomputed=self.pre_computed)
        data1 = self.down_modules[-1](data1, precomputed=self.pre_computed_target)
        nn_list = knn(data0.pos, data1.pos, 1, data0.batch, data1.batch)
        data = data1.clone()
        data.x = data1.x - data0.x[nn_list[1,:],:]
        ## 内层模块
        innermost = False
        ## 如果存在内层模块(self.inner_modules),并且该模块不是 Identity(即非空操作)
        if not isinstance(self.inner_modules[0], Identity):
            stack_down.append(data) # 则将最后一层特征差异存储到stack_down中
            data = self.inner_modules[0](data) # 将差异data输入内层模块进一步处理
            innermost = True # 设置innermost标志,表示是否处理了内层模块
        # 遍历所有上采样模块(解码器)
        # 每次从stack_down弹出一个特征,与data一起输入上采样模块,采样后的结果存入data
        for i in range(len(self.up_modules)):
            if i == 0 and innermost: # 如果当前是第一层且处理了内层模块
                data = self.up_modules[i]((data, stack_down.pop())) #则直接将data和stack_down中的最后一个特征传递给上采样模块。
            else:
                data = self.up_modules[i]((data, stack_down.pop()), precomputed=self.upsample_target)
        # 将解码器输出的特征存储到self.last_feature中
        self.last_feature = data.x
        # 分类层 根据是否启用类别信息,通过全连接层(self.FC_layer)生成最终的分类结果
        if self._use_category:
            self.output = self.FC_layer(self.last_feature, self.category)
        else:
            self.output = self.FC_layer(self.last_feature)
        # 如果提供了真实标签(self.labels)并且需要计算损失
        if self.labels is not None and compute_loss:
            self.compute_loss()
        # 可视化数据 将预测结果(torch.max(self.output, -1)[1])存储到 self.data_visual.pred 中
        self.data_visual = self.input1
        self.data_visual.pred = torch.max(self.output, -1)[1]

        return self.output

那么这里面的核心就是关于编码器和解码器的设置,也就是下采样self.down_modules、上采样self.up_modules以及中间层self.inner_modules的设置
关于这三部分的设置如下:其根据配置文件来创建相关模块

# Loal module
        contains_global = hasattr(opt, "innermost") and opt.innermost is not None
        if contains_global:
            inners = self._create_inner_modules(opt.innermost, modules_lib)
            for inner in inners:
                self.inner_modules.append(inner)
        else:
            self.inner_modules.append(Identity())

        # Down modules
        for i in range(len(opt.down_conv.down_conv_nn)):
            args = self._fetch_arguments(opt.down_conv, i, "DOWN")
            conv_cls = self._get_from_kwargs(args, "conv_cls")
            down_module = conv_cls(**args)
            self._save_sampling_and_search(down_module)
            self.down_modules.append(down_module)

        # Up modules
        if up_conv_cls_name:
            for i in range(len(opt.up_conv.up_conv_nn)):
                args = self._fetch_arguments(opt.up_conv, i, "UP")
                conv_cls = self._get_from_kwargs(args, "conv_cls")
                up_module = conv_cls(**args)
                self._save_upsample(up_module)
                self.up_modules.append(up_module)

配置文件如下:

models:
  # Siamese KPConv:
  # KPConv: Flexible and Deformable Convolution for Point Clouds (https://arxiv.org/abs/1904.08889)
  # KPConv support format_type="[PARTIAL_DENSE | MESSAGE_PASSING]"
  SiameseKPConv:
    path_pretrained: ""
    weight_name: "best_miou_ch"
    class: siamesekpconv.SiameseKPConv
    conv_type: "PARTIAL_DENSE"
    use_category: ${data.use_category}
    define_constants:
      in_grid_size: ${data.first_subsampling}
      FEAT: 0
      in_feat: 64
      bn_momentum: 0.02
    down_conv:
      n_kernel_points: 25
      down_conv_nn:
        [
          [[FEAT + 1, in_feat], [in_feat, 2*in_feat]],
          [[2*in_feat, 2*in_feat], [2*in_feat, 4*in_feat]],
          [[4*in_feat, 4*in_feat], [4*in_feat, 8*in_feat]],
          [[8*in_feat, 8*in_feat], [8*in_feat, 16*in_feat]],
          [[16*in_feat, 16*in_feat], [16*in_feat, 32*in_feat]],
        ]
      grid_size:
        [
          [in_grid_size, in_grid_size],
          [2*in_grid_size, 2*in_grid_size],
          [4*in_grid_size, 4*in_grid_size],
          [8*in_grid_size, 8*in_grid_size],
          [16*in_grid_size, 16*in_grid_size],
        ]
      prev_grid_size:
        [
          [in_grid_size, in_grid_size],
          [in_grid_size, 2*in_grid_size],
          [2*in_grid_size, 4*in_grid_size],
          [4*in_grid_size, 8*in_grid_size],
          [8*in_grid_size, 16*in_grid_size],
        ]
      block_names:
        [
          ["SimpleBlock", "ResnetBBlock"],
          ["ResnetBBlock", "ResnetBBlock"],
          ["ResnetBBlock", "ResnetBBlock"],
          ["ResnetBBlock", "ResnetBBlock"],
          ["ResnetBBlock", "ResnetBBlock"],
        ]
      has_bottleneck:
        [[False, True], [True, True], [True, True], [True, True], [True, True]]
      deformable:
        [
          [False, False],
          [False, False],
          [False, False],
          [False, False],
          [False, False],
        ]
      max_num_neighbors: [[25, 25], [25, 30], [30, 38], [38, 38], [38, 38]]
      module_name: KPDualBlock
    up_conv:
      module_name: FPModule_PD
      up_conv_nn:
        [
          [32*in_feat + 16*in_feat, 8*in_feat],
          [8*in_feat + 8*in_feat, 4*in_feat],
          [4*in_feat + 4*in_feat, 2*in_feat],
          [2*in_feat + 2*in_feat, in_feat],
        ]
      skip: True
      up_k: [1, 1, 1, 1]
      bn_momentum:
        [bn_momentum, bn_momentum, bn_momentum, bn_momentum, bn_momentum]
    mlp_cls:
      nn: [in_feat, in_feat]
      dropout: 0.5
      bn_momentum: bn_momentum
    loss_weights:
      lambda_reg: 1e-6

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐