Open3D 基于kdtree树的邻近点搜索(10)

本文介绍了如何使用Open3D库在点云数据中进行K邻近点搜索和R邻域搜索。详细阐述了KD树的数据结构及其在点云处理中的应用,同时提供了Python代码示例来演示这两种搜索方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、算法简介

KD 树(k-dimensional tree)是一种用于组织 k 维空间中点的数据结构,旨在提供高效的 k
最近邻搜索和范围搜索(如半径邻域搜索)。KD 树通过递归地将空间划分为多个超立方体区域来组织数据。这种分割方式使得 KD
树能够在搜索时快速地排除大部分不相关的数据点。

在构建 KD
树时,选择一个维度作为切分平面,然后根据这个平面将数据划分成两个子集。在每个子集中,选取下一个维度,再次以该维度上的中值为界限切分数据。重复这个过程,直到每个子集中只包含一个数据点,或者达到了预先设定的最小节点数。这样就构建出了一棵
KD 树。

对于给定的查询点,在 KD 树中可以快速地找到距离最近的点(k 最近邻搜索),或者在指定半径范围内找到所有邻域内的点(范围搜索)。KD
树在计算机图形学、模式识别、机器学习等领域中被广泛应用,可以加速众多与空间相关的算法和问题的求解过程。

二、算法实现

完整代码如下:

import open3d as o3d
import numpy as np

# 创建点云数据
point_c
<think>我们参考C++代码的实现逻辑,逐遍历并标记已处理,避免随机选。具体步骤如下:1.将点云转换为numpy数组,并创建一个布尔数组`retain_mask`(初始全为False,表示初始都不保留),注意:在C++代码中,初始标记为false,然后对于每个,如果未被标记,则保留该,然后标记其邻近为true(表示这些邻近不再被保留)。但是注意C++代码中,我们保留当前,然后标记其邻近(除了自身)为true(在C++代码中,标记为true的在后续会被跳过)。这里我们需要注意,在C++代码中,初始的retain_mask全为false,然后循环中,如果当前i未被标记(即false),则将其加入输出,然后将其邻近(除自身外)标记为true(这样这些就不会被加入了)。2.在Python中,我们同样构建一个KDTree,然后初始化一个布尔数组`visited`(相当于C++中的retain_mask),初始全为False。注意:在C++中,retain_mask[i]=true表示该被标记为“已处理”(即邻近已被加入过,或者它本身是某个采样邻近,因此不应该被保留)。因此,在循环中,我们遍历每个,如果当前未被访问(即`visited[i]`为False),则:a.将该加入采样集b.标记该为已访问(也可以不标记自己?注意C++代码中并没有标记自己,因为它在循环中只处理未被标记的,而自己已经被加入,所以后续不会再处理自己。但是,在标记邻近时,它排除了自己,所以自己不会被标记。那么下一次循环,自己已经被处理过(因为已经加入输出)但自己并没有被标记为true,这会导致重复处理?实际上,在C++代码中,当处理一个时,它并没有将自己标记为true,但是因为它在输出中只加入一次,而且后续循环中,由于该已经被加入输出(但visited还是false),所以可能会再次处理?这显然是有问题的。重新审视C++代码:```cppfor(size_ti=0;i<input->size();++i){if(!retain_mask[i])//如果当前没有被标记(即没有被排除){//这里没有标记retain_mask[i]为true,而是直接将其加入输出//然后查找邻近,将邻近(除自己外)标记为true(即排除)std::vector<int>neighbor_indices;std::vector<float>neighbor_dists;kdtree.radiusSearch(input->points[i],min_spacing,neighbor_indices,neighbor_dists);for(intidx:neighbor_indices){if(idx!=static_cast<int>(i)){retain_mask[idx]=true;//标记这些邻近为排除(后续跳过)}}output->push_back(input->points[i]);//保留当前}}```注意:这里当前i并没有被标记为true,那么如果后续有一个j,它不在任何已保留的min_spacing范围内,那么它也会被保留。但是,当前i被保留后,其邻近都被标记为true(排除),所以这些邻近不会再被保留。而当前i在循环中只出现一次,所以不会重复加入。但是,这里有一个问题:如果两个相互之间距离大于min_spacing,但它们在循环中出现的顺序会导致它们都被保留。这符合要求,因为我们要保留所有相互之间距离大于min_spacing的。然而,这里存在一个顺序依赖:先处理的会被保留,后处理的如果在其邻近范围内则被排除。因此,不同的遍历顺序可能导致不同的结果。为了避免顺序依赖,通常我们会随机化顺序,但原C++代码是按索引顺序。在Open3D中,我们可以按照点云索引顺序处理,也可以随机顺序。但原C++代码是按顺序处理的。为了保持一致,我们也按顺序处理。所以,我们按照以下步骤:1.创建Open3DKDTree。2.初始化一个visited数组(即retain_mask),长度为点云数,全为False。3.初始化一个空列表sampled_indices,用于保存采样的索引。4.遍历每个(从0到n-1):a.如果当前索引i对应的visited[i]为False(表示该尚未被排除):-将i加入sampled_indices-使用KDTree的`search_radius_vector_3d`函数搜索i周围半径min_spacing内的所有(包括自己)-对于搜索到的每个邻居索引idx(除了i自己):将visited[idx]设置为True(排除这些)5.用sampled_indices从原始点云中提取。注意:这里我们标记邻居(排除)时,也包括了当前自己吗?不,我们排除邻居(不包括自己),因为自己已经被加入采样,并且我们不需要排除自己(因为自己已经被处理,而且后续不会再被处理,因为循环到i时已经处理过了,而且自己不会被标记为True,所以不会影响自己?但是,如果自己也被标记为True,那么在循环到自己的时候(也就是i)已经处理过了,所以不会再次处理。但是,如果我们排除了自己,那么自己不会被标记,这样如果后面有另一个距离自己大于min_spacing,它还是会被保留?实际上,自己已经被加入采样,所以不需要排除自己,因为自己不会被再次处理(因为循环是按顺序的,每个只处理一次)。但是,这里有一个问题:如果两个是同一个?不会。所以,我们只排除邻居(不包括自己)。但是,在搜索半径时,返回的邻居包括自己吗?在Open3D的`search_radius_vector_3d`函数中,第一个就是查询自己。所以,我们需要跳过自己。然而,在C++代码中,PCL的radiusSearch返回的邻居包括自己吗?查看PCL文档:radiusSearch会返回半径内的所有,包括查询本身。所以,在C++代码中,它用了一个条件`if(idx!=static_cast<int>(i))`来排除自己。因此,在Python中,我们也要排除自己。实现代码:```pythonimportopen3daso3dimportnumpyasnpdefkdtree_uniform_downsample(pcd,min_spacing):points=np.asarray(pcd.points)n=points.shape[0]#创建KDTreekdtree=o3d.geometry.KDTreeFlann(pcd)#初始化标记数组,False表示该尚未被排除(即可以候选)visited=np.zeros(n,dtype=bool)#存储采样的索引sampled_indices=[]#按顺序遍历每个foriinrange(n):ifnotvisited[i]:#将当前加入采样sampled_indices.append(i)#搜索半径内的所有(包括自己)[k,idxs,_]=kdtree.search_radius_vector_3d(pcd.points[i],min_spacing)#标记这些(排除),除了当前自己forjinrange(k):idx=idxs[j]ifidx!=i:#排除其他,但不排除自己visited[idx]=True#创建采样点云sampled_pcd=o3d.geometry.PointCloud()sampled_pcd.points=o3d.utility.Vector3dVector(points[sampled_indices])returnsampled_pcd#使用示例if__name__=="__main__":#读取点云pcd=o3d.io.read_point_cloud("input.pcd")#请替换为你的点云文件路径#设置最小间距(根据点云尺度调整)min_spacing=0.1#执行采样sampled_pcd=kdtree_uniform_downsample(pcd,min_spacing)#保存结果o3d.io.write_point_cloud("sampled_cloud.pcd",sampled_pcd)#打印原始点云和采样后点云的数量print("原始点云数:",len(pcd.points))print("采样后数:",len(sampled_pcd.points))```注意:这个实现与C++版本逻辑一致,按顺序遍历点云,每个如果未被排除(即visited[i]为False)则被保留,然后排除其周围min_spacing内的所有其他(标记为True,这样后续遍历到这些时会跳过)。但是,这种顺序遍历的方式可能会导致结果受点云顺序影响。例如,如果点云中两个距离小于min_spacing,那么先遍历到的会被保留,后遍历到的会被排除。因此,如果希望结果更均匀,可以考虑随机打乱点云顺序,但这里为了与C++代码一致,我们按原始顺序。另外,这种方法的效率在点云较大时可能不高,因为每个采样都要进行一次半径搜索,但半径搜索KDTree上的复杂度是O(logn)加上返回的数量。在密集点云中,每个邻近数量可能很大,但采样后的数量会减少。不过,最坏情况下(比如所有都相互远离),我们需要对每个都进行一次半径搜索。如果点云非常大,可以考虑优化,比如使用更高效的数据结构或并行计算,但这里我们按照原始逻辑实现。###相关问题§§相关问题§§1.点云采样顺序对等间距均匀采样的结果有什么影响?2.如何提高大规模点云等间距采样的计算效率?3.除了等间距均匀采样,Open3D还有哪些常用的点云采样方法?4.点云采样在三维重建和点云配准中有什么具体应用?</think>以下是根据您的要求,将C++代码逐行转换为Python Open3D实现的基于KDTree点云等间距均匀采样方案。此实现保留了原始C++代码的逐遍历和标记机制,确保采样间距精确控制: ```python import open3d as o3d import numpy as np def kdtree_uniform_downsample(pcd, min_spacing): """ 基于KDTree点云等间距均匀采样 :param pcd: 输入点云(Open3D PointCloud对象) :param min_spacing: 采样间最小间距 :return: 采样后的点云 """ points = np.asarray(pcd.points) n_points = len(points) # 创建KDTree并构建索引 kdtree = o3d.geometry.KDTreeFlann(pcd) # 标记保留的(初始全为False) retain_mask = np.zeros(n_points, dtype=bool) # 存储采样索引 sampled_indices = [] # 主处理循环 - 逐遍历 for i in range(n_points): if not retain_mask[i]: # 将当前标记为保留 retain_mask[i] = True sampled_indices.append(i) # 查找半径范围内的邻近 [k, idxs, _] = kdtree.search_radius_vector_3d(pcd.points[i], min_spacing) # 标记邻近为已处理(除自身外) for idx in idxs: if idx != i: # 排除自身 retain_mask[idx] = True # 创建采样点云 sampled_pcd = o3d.geometry.PointCloud() sampled_pcd.points = o3d.utility.Vector3dVector(points[sampled_indices]) return sampled_pcd if __name__ == "__main__": # 读取点云 input_path = "bunny.pcd" # 替换为实际路径 pcd = o3d.io.read_point_cloud(input_path) if not pcd.has_points(): print("无法读取输入点云文件") exit(1) print(f"原始点云数: {len(pcd.points)}") # 参数设置(根据点云尺度调整) min_spacing = 0.002 # 执行KDTree均匀采样 sampled_pcd = kdtree_uniform_downsample(pcd, min_spacing) print(f"采样后数: {len(sampled_pcd.points)}") # 保存结果 o3d.io.write_point_cloud("sampled_cloud.pcd", sampled_pcd) # 可视化对比 pcd.paint_uniform_color([1, 0, 0]) # 原始点云红色 sampled_pcd.paint_uniform_color([0, 0, 1]) # 采样点云蓝色 o3d.visualization.draw_geometries([pcd, sampled_pcd]) ``` ### 关键实现细节说明: 1. **逐遍历机制**: - 使用`for i in range(n_points)`循环遍历每个 - 通过`retain_mask`布尔数组标记处理状态 - 符合原始C++代码的逐处理逻辑 2. **KDTree半径搜索**: ```python [k, idxs, _] = kdtree.search_radius_vector_3d(pcd.points[i], min_spacing) ``` 等价于PCL的`radiusSearch`函数,返回半径$r=min\_spacing$内的所有索引 3. **间距控制原理**: - 当选择$p_i$作为采样时,其周围$r$范围内的所有$p_j$满足$||p_i - p_j|| \leq r$ - 这些被标记为已处理,确保最终采样集中任意两距离$>r$ 4. **标记处理逻辑**: ```python if not retain_mask[i]: # 仅处理未标记 retain_mask[i] = True # 标记当前为保留 for idx in idxs: # 标记邻近为已处理 if idx != i: # 排除自身 retain_mask[idx] = True ``` ### 参数调整建议: - **min_spacing**:决定采样密度,可根据点云尺度计算: $$r = \alpha \cdot \sqrt[3]{\frac{V}{N}}$$ 其中$V$是点云包围盒体积,$N$是数,$\alpha$是密度因子(通常0.8-1.2)[^1] ### 与其他采样方法对比: | 方法 | 优 | 适用场景 | |------|------|----------| | **KDTree等间距采样** | 精确控制间距,保持原始分布 | 配准预处理、特征提取 | | [体素下采样](^3) | 计算效率高($O(n)$) | 大规模点云快速简化 | | [随机采样](^3) | 简单快速 | 数据增强、初步简化 | ### 实际应用场景: 1. **点云配准预处理**:减少数同时保持关键特征 2. **三维重建**:生成均匀分布的曲面采样 3. **特征提取**:为PointNet++等网络提供规范化输入[^1]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

点云学徒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值