【论文阅读】Siamese KPConv: 3D multiple change detection from raw point clouds using deep learning
论文阅读+代码解读
论文标题: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是我们想要实现的效果。因为在较大规模的点云数据中,通常物体不止一类,且变化的物体也不止一类,所以二分类的检测不能满足要求,只知道变化或者没变化对我们来说还不够,因此需要多分类的检测。并且点云尺度的检测只适用于小规模的点云数据,物体较为单一的,检测对象较为单一的情况,复杂的真实的场景还是需要点尺度上的多分类变化检测。
本文的主要贡献:
- Siamese KPConv网络:一种能够直接从原始3D点云中进行多类变化分割的深度学习方法。
- 三个新的数据集:包括合成的Urb3DCD-V2数据集及其分类版本,以及基于真实数据的AHN-CD数据集
Related work
3D点云变化检测:
现有方法分类两类:
- 后分类方法:首先对每个点云进行语义分割,然后比较不同时间点的标签以检测变化。这类方法容易累积每个步骤的误差。
- 预分类方法:直接检测变化,然后对变化进行分类。这类方法同样存在误差累积的问题。
Siamese架构:
Siamese网络最初用于计算机视觉任务,属于双流架构,由两个相同的分支组成,每个分支处理一个输入数据(如图像或点云),然后通过特征差异来检测变化。Siamese网络可以分为: - 纯Siamese网络:两个分支共享权重。
- 伪Siamese网络:两个分支不共享权重,灵活性更高,但训练参数更多。
Siamese全卷积网络(FCN)在变化检测中表现出色,通常结合U-Net架构,通过跳跃连接融合编码器和解码器的信息。
3D点云的深度学习方法:
处理3D点云的深度学习方法主要分为三类: - 基于投影的方法:将3D点云投影到2D平面(如栅格或球面),然后应用传统的2D图像处理方法。
- 基于离散化的方法:将3D点云转换为3D体素网格,便于使用3D卷积操作,但会丢失信息且计算成本高。
- 基于点的方法:直接处理原始点云,避免了投影和离散化的缺点。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
更多推荐



所有评论(0)