目标检测 | Farthest Point Sampling 及其 CUDA 实现
Farthest Point Sampling 及其 CUDA 实现
概述
在深度学习中,在mesh模型(网格模型)上直接学习并预测是一个相当复杂的任务,一方面在于没有高效的模型,另一方面在于现实中难以直接获取性质良好的mesh模型。所以我们常会通过采样和扫描的形式将mesh模型或者现实中的物体转化为point cloud(点云),对于一个点云,其定义为一个\(N\times C\)的张量,\(N\)表示有多少个点,\(C\)表示每个点的信息。对于一个三维的点云,一般\(C=3\),以表示\((x,y,z)\)。如果点云还携带了更多的信息例如反射强度等,则\(C=3+C^\prime\)。而评价这个点云质量的好坏,就是评价这个点云是否尽可能保留了mesh模型中的几何特征。
现在假设我们拥有一个mesh模型,其渲染效果如下图所示:

但是我们的模型不能直接处理这个mesh模型,因此我们通过扫描或者采样的方式得到了一组\(4096\times 3\)的点云:

如果我们凭借肉眼观察,可见这组点云较好地保留了mesh的几何特征,因此这组点云可以称得上是“质量不错”。
但是我们需要把这组\(4096\times 3\)点云输入到一个输入为固定大小\(256\times 3\)的模型中,我们就必须通过下采样将点云采样为\(256\times 3\)。
均匀随机采样
首先我们想到的一种最为朴素的下采样方法就是均匀随机采样(uniform random sampling),这种下采样方法在python中可以被简单实现:
# 所有的点(1 x 4096 x 3)
total_point = ...
# 进行均匀随机采样
num_samples = 256
indices = torch.randint(0, total_point.size(1), (num_samples,))
sampled_points = total_point[0, indices]
# 可视化
plot_pointcloud(sampled_points, title="uniform random sampling")
plt.show()
我们得到采样结果为:

显然这组点云的采样效果并不能称得上“质量不错”,如下图所示,经过采样之后的点并不能很好地保留原始的几何特征,显然均匀随机采样得到的点在分布上却并不均匀。

经过简单分析,这是因为曲率更大的表面,其几何细节更多,但是在均匀随机采样中,高曲率表面的点与低曲率表面的点权重一致,导致了在曲率更大的表面上,采样的失真程度更高。
Farthest Point Sampling
Farthest Point Sampling (FPS)最早被引入深度学习领域应该是Charles R. Qi等人的文章PointNet++: Deep Hierarchical Feature Learning on Point Sets in a Metric Space,FPS算法在其中作为PointNet++特征提取的中心点采样层使用。
FPS算法的本质是贪心算法,其基本思想是从点云中选择一个起始点,然后在剩余的点中选择与已选点距离最远的点作为下一个选定点。这个过程迭代进行,直到达到所需数量的采样点为止。通过这种方式,FPS确保了选定的点在整个点云中分布均匀,从而能够较好地表示点云的特征。
FPS的关键步骤包括:
- 选择起始点: 从点云中随机选择一个点作为起始点。
- 计算距离: 计算剩余点与已选点之间的距离。
- 选择最远点: 选择距离已选点最远的点作为下一个选定点。
- 重复步骤2和步骤3: 重复计算距离和选择最远点的过程,直到达到所需数量的采样点。
如下图所示,在每一次循环迭代的开始,我们有已采样的点集(红色点),尚未被采样的点集(绿色点)。其中我们可以将红色点连接起来,可以看做一个流形(虚线),可以看到距离这个流形表面越远的绿色点,其曲率将会越大,拥有的几何细节也就越多,所以我们在迭代中将距离最远的绿色点加入已采样集合中。通过这种贪心算法的思想,我们确保每次采样都是最高几何细节保留。

我们可以写出每次迭代的数学描述:
p_1 & = 0 \\
p_k & = \text{argmax}_{p \in \text{P}} \ \min_{p_i \in \text{S}_k} \ d(p_i, p) \\
\text{S}_{k+1} & = \text{S}_k \cup \{ p_k \} \\
\end{align*}
\]
其中\(P\)是点云,为\(N\times C\)的张量,\(S\)是采样得到的点集索引,\(d(x,y)\)是距离函数。
根据上述数学描述,我们可以写出FPS算法的CPU实现:
def farthest_point_sampling(points:torch.Tensor, num_samples:int):
"""
使用Farthest Point Sampling (FPS)算法从点云中选择一组关键点。
参数:
- points (torch.Tensor): 输入点云,每一行是一个点的坐标。
- num_samples (int): 要选择的采样点的数量。
返回:
- selected_points (torch.Tensor): 选择的关键点的坐标。
"""
num_points = points.size(0)
selected_indices = points.new_empty(num_samples, dtype=torch.long)
distances = points.new_ones(num_points) * float('inf')
# 从输入点云中随机选择一个起始点
start_index = 0
selected_indices[0] = start_index
selected_point = points[start_index]
# 迭代选择最远点
for i in range(1, num_samples):
# 计算每个点与已选点之间的欧几里得距离
dist_to_selected = torch.norm(points - selected_point, dim=1)
# 更新距离,选择最远点
distances = torch.min(distances, dist_to_selected)
farthest_index = torch.argmax(distances)
selected_indices[i] = farthest_index
selected_point = points[farthest_index]
selected_points = points[selected_indices]
return selected_points
如下图所示,FPS算法得到结果明显要比均匀随机采样的“质量”更好:

最后,我们再对比一下原始图像(a),采样结果(b),均匀随机采样(c)以及FPS采样(d):

Farthest Point Sampling 的并行版本
在上一节中,我们给出了FPS算法的数学描述:
p_1 & = 0 \\
p_k & = \text{argmax}_{p \in \text{P}} \ \min_{p_i \in \text{S}_k} \ d(p_i, p) \\
\text{S}_{k+1} & = \text{S}_k \cup \{ p_k \} \\
\end{align*}
\]
其中\(P\)是点云,为\(N\times C\)的张量,\(S\)是采样得到的点集索引,\(d(x,y)\)是距离函数。
其中
\]
可以被写成并行的版本
D_k&=\begin{cases}
d_{k1} = \min_{p_i \in \text{S}_k} \ d(p_i, p_1) \\
\cdots\\
d_{kn} = \min_{p_i \in \text{S}_k} \ d(p_i, p_n)\\
\end{cases} & \text{parallel operation}\\
p_k & = \text{argmax}_{p \in \text{P}} D_k\ \\
\end{align*}
\]
注意到\(p_k = \text{argmax}_{p \in \text{P}} D_k\)需要前一步操作的线程同步,所以形成了算法中的一个瓶颈
当然在实际操作中,CUDA核心提供的thread数目有限(单个block的大小有限),为了方便对齐,我们往往会将点云划分为多个子集。每个thread都可以看作一个分治的分支,在内部串行地计算分配给它的子集,最后在线程同步阶段完成所有子集的归并
我们以实际中使用最多的FPS算法CUDA实现为例进行解读,源码可以在这里找到:
整段代码可以被划分为两个部分,其中第二部分还可以划分为子集计算与子集归并:
- 参数初始化
- 循环采样
- 子集计算
- 子集归并
参数初始化
如下述代码所示,这段代码负责为thread开辟内存,计算当前batch id以及确定当前thread的id,并确定起始点(设为0)
if (m <= 0) return;
__shared__ float dists[block_size];
__shared__ int dists_i[block_size];
int batch_index = blockIdx.x;
dataset += batch_index * n * 3;
temp += batch_index * n;
idxs += batch_index * m;
int tid = threadIdx.x;
const int stride = block_size;
int old = 0;
if (threadIdx.x == 0) idxs[0] = old;
__syncthreads();
子集计算
循环采样分为外循环与内循环,外循环是一个\((1,M)\)的循环,其中\(M\)是采样的点数量。
内循负责计算当前迭代的距离,我们为每个thread分配一个子集,其子集的定义为:
\]
,其中\(S\)为block的thread数量,\(\text{thread}_\text{id}\)为当前trhread的id。
例如当\(S=512\)时,\(id\)为\(0\)则\(\text{thread}_0=\{0,512,1024,\cdots\}\),\(id\)为\(1\)则\(\text{thread}_1=\{1,513,1025,\cdots\},\cdots\)
随后求出每个thread中的最大距离,即:
\]
for (int j = 1; j < m; j++) {
int besti = 0;
float best = -1;
float x1 = dataset[old * 3 + 0];
float y1 = dataset[old * 3 + 1];
float z1 = dataset[old * 3 + 2];
for (int k = tid; k < n; k += stride) {
float x2, y2, z2;
x2 = dataset[k * 3 + 0];
y2 = dataset[k * 3 + 1];
z2 = dataset[k * 3 + 2];
float mag = (x2 * x2) + (y2 * y2) + (z2 * z2);
if (mag <= 1e-3) continue;
float d =
(x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1) + (z2 - z1) * (z2 - z1);
float d2 = min(d, temp[k]);
temp[k] = d2;
besti = d2 > best ? k : besti;
best = d2 > best ? d2 : best;
}
dists[tid] = best;
dists_i[tid] = besti;
__syncthreads();
}
子集归并
完成所有的\(\text{best}_k\)的计算之后,进入子集归并阶段。这里的子集归并是一个硬编码的二路归并,归并规则如下:
\]
例如,当thread数量\(S=512\),第一轮\(n=256\)
算法将归并\(\text{best}_0\)与\(\text{best}_{256}\),\(\text{best}_1\)与\(\text{best}_{257}\),\(\text{best}_2\)与\(\text{best}_{258}\),\(\cdots\),\(\text{best}_{255}\)与\(\text{best}_{511}\)。
经过第一轮归并后,子集数量减半,只剩下\(256\)个子集。
同理继续归并,直到只剩下最后一个子集\(\text{best}_0\),将\(\text{best}_0\)的点并入已采样点集中,完成一轮采样。
if (block_size >= 512) {
if (tid < 256) {
__update(dists, dists_i, tid, tid + 256);
}
__syncthreads();
}
if (block_size >= 256) {
if (tid < 128) {
__update(dists, dists_i, tid, tid + 128);
}
__syncthreads();
}
if (block_size >= 128) {
if (tid < 64) {
__update(dists, dists_i, tid, tid + 64);
}
__syncthreads();
}
if (block_size >= 64) {
if (tid < 32) {
__update(dists, dists_i, tid, tid + 32);
}
__syncthreads();
}
if (block_size >= 32) {
if (tid < 16) {
__update(dists, dists_i, tid, tid + 16);
}
__syncthreads();
}
if (block_size >= 16) {
if (tid < 8) {
__update(dists, dists_i, tid, tid + 8);
}
__syncthreads();
}
if (block_size >= 8) {
if (tid < 4) {
__update(dists, dists_i, tid, tid + 4);
}
__syncthreads();
}
if (block_size >= 4) {
if (tid < 2) {
__update(dists, dists_i, tid, tid + 2);
}
__syncthreads();
}
if (block_size >= 2) {
if (tid < 1) {
__update(dists, dists_i, tid, tid + 1);
}
__syncthreads();
}
old = dists_i[0];
if (tid == 0) idxs[j] = old;
综上所述,并行版本的FPS算法将时间复杂度从\(O(MN)\)压缩为\(O(M\frac{N}{S})\),其中\(M\)为采样数目,\(N\)为待采样点云大小,\(S\)为thread数量(单个block的大小)。
目标检测 | Farthest Point Sampling 及其 CUDA 实现的更多相关文章
- ubuntu16.04 Detectron目标检测库配置(包含GPU驱动,Cuda,Caffee2等配置梳理)
Detectron概述 Detectron是Facebook FAIR开源了的一个目标检测(Object Detection)平台. 用一幅图简单说明下Object Detection.如Mask R ...
- 论文笔记:目标检测算法(R-CNN,Fast R-CNN,Faster R-CNN,FPN,YOLOv1-v3)
R-CNN(Region-based CNN) motivation:之前的视觉任务大多数考虑使用SIFT和HOG特征,而近年来CNN和ImageNet的出现使得图像分类问题取得重大突破,那么这方面的 ...
- GPU端到端目标检测YOLOV3全过程(下)
GPU端到端目标检测YOLOV3全过程(下) Ubuntu18.04系统下最新版GPU环境配置 安装显卡驱动 安装Cuda 10.0 安装cuDNN 1.安装显卡驱动 (1)这里采用的是PPA源的安装 ...
- 一阶段目标检测网络-RetinaNet 详解
摘要 1,引言 2,相关工作 3,网络架构 3.1,Backbone 3.2,Neck 3.3,Head 4,Focal Loss 4.1,Cross Entropy 4.2,Balanced Cro ...
- 目标检测网络之 YOLOv3
本文逐步介绍YOLO v1~v3的设计历程. YOLOv1基本思想 YOLO将输入图像分成SxS个格子,若某个物体 Ground truth 的中心位置的坐标落入到某个格子,那么这个格子就负责检测出这 ...
- 目标检测(三)Fast R-CNN
作者:Ross Girshick 该论文提出的目标检测算法Fast Region-based Convolutional Network(Fast R-CNN)能够single-stage训练,并且可 ...
- 目标检测(二)SSPnet--Spatial Pyramid Pooling in Deep Convolutional Networks for Visual Recognotion
作者:Kaiming He, Xiangyu Zhang, Shaoqing Ren, and Jian Sun 以前的CNNs都要求输入图像尺寸固定,这种硬性要求也许会降低识别任意尺寸图像的准确度. ...
- Faster-rcnn实现目标检测
Faster-rcnn实现目标检测 前言:本文浅谈目标检测的概念,发展过程以及RCNN系列的发展.为了实现基于Faster-RCNN算法的目标检测,初步了解了RCNN和Fast-RCNN实现目标检 ...
- 皮卡丘检测器-CNN目标检测入门教程
目标检测通俗的来说是为了找到图像或者视频里的所有目标物体.在下面这张图中,两狗一猫的位置,包括它们所属的类(狗/猫),需要被正确的检测到. 所以和图像分类不同的地方在于,目标检测需要找到尽量多的目标物 ...
- 【转】目标检测之YOLO系列详解
本文逐步介绍YOLO v1~v3的设计历程. YOLOv1基本思想 YOLO将输入图像分成SxS个格子,若某个物体 Ground truth 的中心位置的坐标落入到某个格子,那么这个格子就负责检测出这 ...
随机推荐
- 第一个 milestone:内推 50 人拿到微软 offer!
就在昨天,我的一位候选人和我说,他已经通过面试,正在 offer 沟通阶段.由此,我达成第一个小里程碑:成功内推 50 人拿到了微软 offer! 内推了多少人? 从 2019 年年初开始内推,到现在 ...
- CompilerGenerated与GeneratedCode区别
前言 最近在捣鼓代码生成器,基于 Roslyn,我们可以让生成器项目生成我们的目标 C# 代码,这个也是MVVM Toolkit的实现方式,在查看生成代码的过程中,我们经常会遇到一些特殊的特性,如 G ...
- Qt编写物联网管理平台32-表格数据
一.前言 用表格来展示采集到的数据,是很多组态系统中最常见的方法,一个表格能够展示的数据特别多,在本系统中,默认做的也是通过表格的形式来展示数据,目前是将所有的设备放在一个表格中,后期可能按照不同控制 ...
- Selenium 自动化浏览器,解决懒加载的网页获取问题
Selenium 自动化浏览器,解决懒加载的网页获取问题.可以用于爬虫这些 在使用 Selenium WebDriver 进行自动化测试时,可以通过设置日志级别来控制输出的日志信息.在 C# 中,可以 ...
- 一篇复杂的研究🤔A Comprehensive Study of 😨Jailbreak Attack versus Defense for Large Language Models
本认为,这篇文章的亮点在于对攻击和防御技术的多维评估,另外通过比较不同模型(如Vicuna.LLama和GPT-3.5 Turbo)对攻击和防御策略的反应,文章提供了对模型间差异的深入理解. 对现有的 ...
- 前端之canvas实现电子签约完成线上签署功能
最近发现现在租房还是签合同,越来越多采用电子签约的方式进行,好处不用多说节约成本,节约时间.抱着好奇的心理,尝试自己动手实现一个电子签.原来并不复杂主要通过了canvas绘画能力进行实现的. 主要功能 ...
- ClickHouse-3引擎
引擎 数据库引擎 index 表引擎 数据库引擎 数据库引擎允许您处理数据表. 默认情况下,ClickHouse使用Atomic数据库引擎.它提供了可配置的table engines和SQL dial ...
- args = parser.parse_args()报错 解决
args = parser.parse_args()报错 解决 问题: 如题. 解决方法: args = parser.parse_args(args=[]) args = parser.parse_ ...
- 【T+重要提示】日期不在业务期间范围内
2021年就来了,今天是新年的第二个工作日.相信很多会计朋友们在打开T+软件的时候,会看到这么个提示框: 用友T+2020年没有做完账务,提前建立2021年度帐的步骤 (1)用账套主管选择2020年1 ...
- 近期最值得关注的AI技术报告与Agent综述!
写在前面 如题,近期优秀的大模型层出不穷.作为技术人,需要阅读高质量的AI技术报告或论文,并且掌握未来应用趋势.本文将推荐一些高质量的AI技术报告,以及Agent智能体综述. 大模型技术报告 Deep ...