三维点云处理之最近邻问题

90次阅读
没有评论

前言:本系列文章是关于三维点云处理的常用算法,深入剖析pcl库中相关算法的实现原理,并以非调库的方式实现相应的demo。

1. 最近邻问题概述

(1)最邻近问题:对于点云中的点,怎么去找离它比较近的点

(2)获取邻域点的两种方法:KNN和RNN

  • KNN:如图所示,红色点是要查找的点,蓝色点是数据库中的点,图中是找离红色点最近的3个点,显示出来就是图中的绿色点。
    三维点云处理之最近邻问题
  • Radius-NN
    ​ 以上述红色点为圆心,以所选值为半径画圆,圆内的点就是所要找的点
    三维点云处理之最近邻问题
    (3)点云最近邻查找的难点
  • 点云不规则
  • 点云是三维的,比图像高一维,由此造成的数据量是指数上升的。当然,可以建一个三维网格,把点云转化为一个类似于三维图像的东西,但是这也会带来一些矛盾。因为如果网格很多,分辨率足够大,但处理网格需要的内存就很大;如果网格很少,内存够了,但是分辨率又太低。并且,网格中大部分区域都是空白的,所以网格从原理上就不是很高效。
  • 点云数据量通常非常大。比如,一个64线的激光雷达,它每秒可产生2.2million个点,如果以20Hz的频率去处理,就意味着每50ms要处理110000个点,如果使用暴力搜索方法对这110000个点都找它最邻近的点,那么计算量为:

    110000

    ×

    110000

    ×

    0.5

    =

    6

    ×

    1

    0

    9

    110000times110000times0.5=6times10^9

    110000×110000×0.5=6×109
    (4)最近邻查找:BST、Kd-tree、Octree的共同核心思想

  • 空间分割
    ​ 将空间分割成多个部分,然后在每个小区域中去搜索
  • 搜索停止条件
    ​ 若已知目标点到某一点的距离,那么对于超过这一距离的范围就不需要进行搜索,这个距离也被称为”worst distance”

2. 二叉树(Binary Search Tree, BST)

(1)二叉搜索树的特点(一维数据

  • 结点的左子树上的值都小于该根结点的值
  • 结点的右子树上的值都大于该根节点的值
  • 每一个左右子树都是一个BST
    三维点云处理之最近邻问题
    (2)二叉树的构建——给定一串数字,如何构造出一个BST
class Node:
    def __init__(self, key, value=-1):
        self.left = None
        self.right = None
        self.key = key
        self.value = value  # 这里的value表示当前点的其他属性,比如颜色、编号等

Data generation ——  随机产生一串数字
    db_size = 10
    data = np.random.permutation(db_size).tolist()

Recursively insert each an element  ——  构造BST的具体实现
def insert(root, key, value=-1):
    if root is None:
        root = Node(key, value)
    else:
        if key  root.key:
            root.left = insert(root.left, key, value)
        elif key > root.key:
            root.right = insert(root.right, key, value)
        else:
            pass
    return root

Insert each element  ——  主函数调用
root = None
for i point in enumerate(data):
    root = insert(root, point, i)  # 这里的value(i)表示的是当前点在原始数组中的位置

(3)BST的复杂度

  • 最坏情况下,二叉树的各结点顺次链接,排成一列,此时复杂度为O

    (

    h

    )

    O(h)

     

    O(h),其中h

    h

     

    h为BST的高度,也是BST中结点个数
    三维点云处理之最近邻问题

  • 最好情况下,BST是处于平衡状态的,此时复杂度为O

    (

    l

    o

    g

    2

    n

    )

    O(log_2n)

     

    O(log2n),n

    n

     

    n为BST中结点总数
    三维点云处理之最近邻问题
    (4)二叉树查找——对于一个给定的点(数值),查找它是否在BST中

# 递归法
def search_recursive(root, key):
    if root is None or root.key == key:
        return root
    if key  root.key:    # 表明key在当前的左子树上
        return search_recursive(root.left, key)
    elif key > root.key: # 表明key在当前的右子树上
        return search_recursive(root.right, key)

# 迭代法  ——  通过栈来实现(不断迭代更新current_node)
def search_iterative(root, key):
    current_node = root
    while current_node is not None:
        if current_node.key == key:
            return current_node
        elif current_node.key  key:
            current_node = current_node.right
        elif current_node.key > key:
            current_node = current_node.left
    return current_node

(5)递归法与迭代法的特点

  • 递归

好处:实现简单,容易理解,代码简短

坏处:由于递归需要不停地去压栈,所以每一次递归就是在内存中记录当前递归的位置,因此递归需要

O

(

n

)

O(n)

O(n)的内存空间,这里的

n

n

n就是递归的次数

  • 迭代

优点:它用一个量current_node来表示当前的位置,因此它所需的存储空间为

O

(

1

)

O(1)

O(1);另外,由于GPU对于堆栈是比较困难的,往往只支持20多层的堆栈,很多时候是不够用的,可能会造成栈溢出(stack-overflow),而且在GPU上实现递归是非常慢的,迭代法可以避免这一问题

缺点:实现起来较为困难

(6)深度优先搜索 (Depth First Traversal)

# 前序遍历  ——  可用于复制一棵树
def preorder(root):
    if root is not None:
        print(root)
        preorder(root.left)
        preorder(root.right)
        
# 中序遍历  ——  可用于排序
def inorder(root):
    if root is not None:
        inorder(root.left)
        print(root)
        inorder(root.right)
        
# 后序遍历  ——  可用于删除一个结点
def postorder(root):
    if root is not None:
        postorder(root.left)
        postorder(root.right)
        print(root)

(7)KNN——寻找K个最近邻的点

​ 寻找当前点的K个最近邻点关键在于如何确定worst dist,具体步骤如下:

  • 建立一个容器container来存储当前KNN的结果,并将容器中的结果进行排序:例如,当K = 6时,当前KNN结果为[1, 2, 3, 4, 4.5, inf]
  • 容器中最后一个就是worst dist,对于新增结点,若新增结点与当前结点计算出来的dist小于当前worst dist,则将其添加到容器中,同时更新worst:例如,若新增结点计算出来的dist为6,则容器中先腾出空间[1, 2, 3, 4, 4.5, 4.5],然后再将当前dist放入到容器中,结果为[1, 2, 3, 4, 4.5, 6],此时worst dist为6
代码实现:构建容器KNNReslutSet
class DistIndex:
    def __init__(self, distance, index):
        self.distance = distance
        self.index = index
    
    def __lt__(self, other):
        return self.distance  other.distance

class KNNResultSet:
    '''
        用于存储KNN查找结果的容器
        capacity: 容器大小
        count: 当前容器中的元素个数
        worst_dist: 容器结果中的最大值(最长距离)
        dist_index_list: 容器中各个结果距离所对应的结点序号
    '''
    def __init__(self, capacity):
        self.capacity = capacity
        self.count = 0
        self.worst_dist = 1e10  # 初始时,将容器中的数据设置大一些
        self.dist_index_list = []
        for i in range(capacity):
            self.dist_index_list.append(DistIndex(self.worst_dist, 0))
        self.comparison_counter = 0
    
    def size(self):
        return self.count
    
    def full(self):
        return self.count == self.capacity
    
    def worstDist(self):
        return self.worst_dist
    
    def add_point(self, dist, index):
        self.comparison_counter += 1
        if dist > self.worst_dist:
            return 
        if self.count  self.capacity:
            self.count += 1  # 若当前容器元素个数 小于 容器容量,则新增空位
        
        i = self.count - 1  # 因为是从0开始索引的,故将i定位到新增空位处的索引上
        # 下面其实是将容器中的元素进行排序(包括新腾出来的空位),排序结果根据dist对应的index进行存储
        while i > 0:
            # 若当前容器的最大距离(最后索引) 大于 当前新增距离dist (上面的4.5与6进行比较)
            if self.dist_index_list[i-1].distance > dist:
                # 则将其往后挪一位,使得整体上是从大到小的顺序
                self.dist_index_list[i] = copy.deepcopy(self.dist_index_list[i-1])
                i -= 1
            else:
                break  # 否则,跳出循环,将当前dist放到最后一个元素后面
        
        self.dist_index_list[i].distance = dist
        self.dist_index_list[i].index = index
        self.worst_dist = self.dist_index_list[self.capacity-1].distance

        
KNN查找:在当前以Node为根节点(root)的二叉树中,查找离key最近的K个结点,并将其存储于result_set中
def knn_search(root:Node, result_set:KNNResultSet, key):
    if root is None:
        return False
    
    # 将根节点root与目标节点key进行比较,也就是将当前根节点root放到容器中,这里root.key - key表示dist,root.value表示index
    result_set.add_point(math.fabs(root.key - key), root.value)
    if result_set.worstDist() == 0:  # 若worstDist为0,表示当前根节点root就是所要找到的节点key
        return True
    # 若当前根节点 大于 目标节点
    if root.key >= key:
        # 则在当前根节点的左子树上进行同样的查找操作
        if knn_search(root.left, result_set, key):
            return True
        # 若当前根节点与目标节点的差 小于 最坏距离,那么还可以寻找较大的节点,使其与目标节点的差在最坏距离之内,因此可往当前根节点的右子树上寻找节点
        elif math.fabs(root.key - key)  result_set.worstDist():
            return knn_search(root.right, result_set, key)
        return False
    else:
        # 反之,若当前根节点 小于 目标节点,则在当前根节点的右子树上进行同样的查找操作
        if knn_search(root.right, result_set, key):
            return True
        # 若当前根节点与目标节点的差 小于 最坏距离,那么还可以寻找较小的节点,使其与目标节点的差在最坏距离之内,因此可往当前根节点的左子树上寻找节点
        elif math.fabs(root.key - key)  result_set.worstDist():
            return knn_search(root.left, result_set, key)
        return False

(8)Radius NN查找

​ Radius NN查找中worstDist是固定的,因此不需要容器存储查找结果,并通过排序维持worstDist,只需将当前节点和目标节点的差与worstDist进行判断,来确定当前节点是否为目标节点的Radius最近点。

# 新增节点
def add_point(self, dist, index):
    self.comparison_counter += 1
    if dist > self.radius:  # 若当前节点计算出的dist大于radius,则退出(不将其加入到最近点范围内)
        return 
    
    # 反之,若当前节点计算出的dist 小于 radius,则将计算出的dist及该节点的索引index存储到最近点范围
    # 同时存储该点的索引index
    self.count += 1  
    self.dist_index_list.append(DistIndex(dist, index))
    
# Radius NN查找
def radius_search(root:Node, result_set:RadiusNNResultSet, key):
    if root is None:
        return False
    
    # 计算当前根节点root.key与目标节点key的差,判断它是否可以放入容器中result_set
    result_set.add_point(math.fabs(root.key - key), root.value)
    
    # 同KNN一样,若当前根节点 大于 目标节点
    if root.key >= key:
        # 则在当前根节点的左子树上进行同样的查找操作
        if radius_search(root.left, result_set, key):
            return True
        # 若当前根节点与目标节点的差 小于 最坏距离,那么还可以寻找较大的节点,使其与目标节点的差在最坏距离之内,因此可往当前根节点的右子树上寻找节点
        elif math.fabs(root.key - key)  result_set.worstDist():
            return radius_search(root.right, result_set, key)
        return False
    else:
        # 反之,若当前根节点 小于 目标节点,则在当前根节点的右子树上进行同样的查找操作
        if radius_search(root.right, result_set, key):
            return True
        # 若当前根节点与目标节点的差 小于 最坏距离,那么还可以寻找较小的节点,使其与目标节点的差在最坏距离之内,因此可往当前根节点的左子树上寻找节点
        elif math.fabs(root.key - key)  result_set.worstDist():
            return radius_search(root.left, result_set, key)
        return False

(9)二叉树的应用

​ 二叉树一般是应用在一维数据上,不能将其应用在高维数据上。因为二叉树的查找依赖于左边小、右边大这一特性,对于一维数据来说,这种大和小可以通过直接比较查询点与当前点的大小来获取。而对于高维数据,在某一维度上的大小并不能代表查询点与当前点的大小,故不能应用于高维数据。

3. Kd-tree

​ kd-tree通常应用于高维空间(任意维)中,在每个维度上进行一次二叉树,就称为Kd-tree。每次划分时,在每个维度的某一超平面上将样本划分成两部分。另外,通过设置leaf-size来定义停止划分空间的条件,若当前结点个数小于leaf-size,则停止划分。其中,每次划分时的维度可以轮流切换,即x-y-z-x-y-z。

​ 以二维为例,设置leaf-size=1,如下图所示:
三维点云处理之最近邻问题
(1)如何表达Kd-tree的结点

class Node:
    def __init__(self, axis, value, left, right, point_indices):
        self.axis = axis  # 当前要切分的维度,比如说接下来是要垂直于哪个轴进行切分
        self.value = value  # 定义当前所要切分维度中的位置,对应上面就是y轴上的位置 
        self.left = left  # 定义当前结点的左节点
        self.right = right  # 定义当前结点的右节点
        self.point_indices = point_indices  # 存储属于当前划分区域的结点的序号
    
    # 若当前结点为一个leaf,则没有必要继续往下划分
    def is_leaf(self):
        # 因为由下面可知,在构建kdtree过程中,value是初始化为None的
        if self.value is None:
            return True
        else:
            return False
        

(2)构建kd-tree

def kdtree_recursive_build(root, db, point_indices, axis, leaf_size):
    '''
        root: 所创建的kd-tree的根节点
        db: 源数据集
        point_indices: 点的索引
        axis: 所划分的维度
        leaf_size: 最小节点的大小
    '''
    if root is None:  # 若根节点为空,则根据节点的属性创建根节点
        root = Node(axis, None, None, None, point_indices)
    
    # 若所要划分的样本数量 大于 最小样本数,则进行kd-tree划分,如下:
    if len(point_indices) > leaf_size:
        # 以某一个维度(axis)将db中的点进行排序(这里的排序算法sort_key_by_value是如何得到的?)
        point_indices_sorted, _ = sort_key_by_value(point_indices, db[point_indices, axis])
        # 通过中间靠左位置获取排序后的中间左点(索引及其值)
        middle_left_idx = math.ceil(point_indices_sorted.shape[0] / 2) - 1
        middle_left_point_idx = point_indices_sorted[middle_left_idx]
        middle_left_point_value = db[middle_left_point_idx, axis]
        
        # 通过中间靠右位置获取排序后的中间右点(索引及其值)
        middle_right_idx = middle_left_idx + 1
        middle_right_point_idx = point_indices_sorted[middle_right_idx]
        middle_right_point_value = db[middle_right_point_idx, axis]
        
        # 以中间两点的平均值作为根节点值
        root.value = (middle_left_point_value + middle_right_point_value) * 0.5
        # 通过中间左点循环构建左子树
        root.left = kdtree_recursive_build(root.left, 
                                           db, 				  															point_indices_sorted[0:middle_right_idx], 
                                        axis_round_robin(axis, dim=db.shape[1]), 
                                        leaf_size)
        # 通过中间右点循环构建右子树
        root.right = kdtree_recursive_build(root.right, 
                                           db, 				  															point_indices_sorted[middle_right_idx:], 
                                        axis_round_robin(axis, dim=db.shape[1]), 
                                        leaf_size)
        return root

    # 如何选取分割维度:这里采用的是轮换分割,即x-y-x-y
    def axis_round_robin(axis, dim):
    	if axis == dim-1:
	        return 0
    	else:
        	return axis + 1

(3)复杂度

​ 假设建立的kd-tree有

n

n

n个点,并且在每个维度上是均匀平衡的,那么总共有

l

o

g

n

logn

logn层,在每一层中进行排序,使得小于的放左边,大于的放右边,这个排序算法的实践复杂度是

O

(

n

l

o

g

n

)

O(nlogn)

O(nlogn),因此,总的时间复杂度是:

O

(

n

l

o

g

n

l

o

g

n

)

O(nlognlogn)

O(nlognlogn)。对于空间复杂度,首先,由于每一层中对于每个结点都要存储其index,所以需要

O

(

n

l

o

g

n

)

O(nlogn)

O(nlogn),总的空间复杂度是

O

(

k

n

+

n

l

o

g

n

)

O(kn+nlogn)

O(kn+nlogn)

(4)使用已经构建好的kdtree进行knn查找

​ 关键在于:对于给定查询点query,判断是否要搜索某一区域。若给定的查询点query在某一区域内,或者给定的查询点query到该区域的最小距离 小于 当前的worst dist,那么就需要在这个区域内进行knn查找

def knn_search(root: Node, db: np.ndarray, result_set: KNNResultSet, query: np.ndarray):
    '''
        root: kd-tree的根节点
        db: 源数据集
        result_set: 用于存储knn查找结果的容器,里面维持从小到大的顺序,最后面最大的作为worstDist
        query: 待查询点
    '''
    if root is None:
        return False
    # 判断当前结点是否为叶子节点,若是,则使用暴力查找方法在叶子节点区域中进行查找
    # 因为kd-tree中的叶子节点区域可能不是一个节点,而是多个节点,那么此时就在这多个节点中进行暴力查找
    if root.is_leaf():
        leaf_points = db[root.point_indices, :]
        diff = np.linalg.norm(np.expand_dims(query, 0) - leaf_points, axis=1)
        for i in range(diff.shape[0]):
            result_set.add_point(diff[i], root.point_indices[i])
        return False
    # 若在当前axis维度上,待查询点值小于根节点值,则待查询点在根节点的当前维度的左侧
    if query[root.axis]  root.value:
        # 则需要在左子树上查找
        knn_search(root.left, db, result_set, query)
        # 另外,右子树上也可能存在离待查询点query较近的点,那么也需要在当前右子树上进行查找。前提是:query在当前维度的值小于根节点的值,且二者之差比最坏距离worstDist小  
        ### 这一块还不太懂
        if math.fabs(query[root.axis] - root.value)  result_set.worstDist():
            knn_search(root.right, db, result_set, query)
    else:
        knn_search(root.right, db, result_set, query)
        if math.fabs(query[root.axis] - root.value)  result_set.worstDist():
            knn_search(root.left, db, result_set, query)
                
    return False

(5)Kd-tree中的Radius NN
​ 同KNN中的Radius NN一样,区别在于此时的worse dist是固定的,而不是在每次分割中需要根据查找点的距离动态更新的。

4. Octree

(1)Octree特点

​  针对于三维数据,每个维度进行划分,分割一次会得到8个部分。Octree的好处是:高效,不需要经过根节点就可以提前终止划分。因为:若以查询点为中心,以固定值为半径形成一个球,如果这个球完全落在了某个立方体内,那么此时的搜索范围就可以限定在这个小立方体内。而kd-tree中每一层结点只考虑了一个维度,在当前维度下不知道其他维度是如何分割的,所以只有一个维度信息是不足以确定什么时候可以终止搜索。

(2)Octree的关键流程

  • 首先,根据所有点的边界确定最大立方体
  • 设置停止搜索条件。主要是设置leaf_size(当前区域中点的个数);设置最小区域(此时最小区域中可能有多个点重叠在一起),设置最小区域的初衷是:在某些情况下,某一区域中可能存在重复点,此时不需要划分到每个区域中只包含一个点,将这些重复点划分到一个区域中即可。另外,如果在这种情况下不设置最小区域,那么在leaf_size下,相同点(重叠点)是不会划分开的,此时在划分时可能会陷入死循环

(3)创建Octree

octree中节点的结构——octant
class Octant:
    def __init__(self, children, center, extent, point_indices, is_leaf):
        self.children = children  # 每次划分时的子节点(8个)
        self.center = center      # 中心点
        self.extent = extent      # 半个边长(从中心点到其中一个面的距离)
        self.point_indices = point_indices  # 点的索引
        self.is_leaf = is_leaf    # 是否为叶子节点区域

创建octree
def octree_recursive_build(root, db, center, extent, point_indices, leaf_size, min_extent):
    '''
        root: 要构建的octree的根节点
        db:   源数据样本点
        center: 中心点
        point_indices: 各点的索引
        leaf_size: 叶子节点区域大小(叶子节点区域中节点个数)
        min_extent:  最小区域大小
    '''
    if len(point_indices) == 0:  # 若源数据样本中没有点
        return None  # 则返回一个空的octree
    
    if root is None:  # 若初始时octree的根节点为空
        # 则根据octant节点的属性,创建初始root节点
        root = Octant([None for i in range(8)], center, extent, point_indices, is_leaf=True)
    
    # 判断是否需要建立Octree —— 若结点总数小于所设置的min_extent(最小元素个数),就不需要建立
    if len(point_indices)  leaf_size or extent  min_extent:
        root.is_leaf = True
    # 否则,进行octree的划分
    else:
        root.is_leaf = False  # 首先,表明当前节点并不是leaf_size
        children_point_indices = [[] for i in range(8)]  # 准备8个子空间
        # 下面是通过for循环将每个点放入到对应的子空间中
        for point_idx in point_indices:
            point_db = db[point_idx]  # 根据索引取出当前点
            morton_code = 0  
            # 根据当前点与中心点在3个维度上的比较,将空间划分为8份
            if point_db[0] > center[0]:
                morton_code = morton_code | 1
            if point_db[1] > center[1]:
                morton_code = morton_code | 2
            if point_db[2] > center[2]:
                morton_code = morton_code | 4
            # 将当前点归属到对应的子空间中
            children_point_indices[morton_code].append(point_idx)
            
            # 创建children
            factor = [-0.5, 0.5]
            for i in range(8):
                # 计算每一个子节点的中心点的3个维度坐标
                child_center_x = center[0] + factor[(i & 1) > 0] * extent
                child_center_y = center[1] + factor[(i & 2) > 0] * extent
                child_center_z = center[2] + factor[(i & 4) > 0] * extent
                # 计算每一个子节点的边长
                child_extent = 0.5 * extent
                # 确定每一个子节点的中心点坐标
                child_center = np.asarray([child_center_x, child_center_y, child_center_z])
                # 根据octant参数,递归创建子octree
                root.children[i] = octree_recursive_build(root.children[i], 
                                                          db, 
                                                          child_center,
                                                          child_extent,
                                                          children_point_indices[i],
                                                          leaf_size, 
                                                          min_extent)
        return root

(4)Octree的KNN查找

def inside(query: np.ndarray, radius: float, octant: Octant):
    """
        功能:判断以待查询点query为球心,radius为半径的球是否在Octant中
        query: 待查询点
        radius: 球半径
        octant: 待比较的子区域
    """
    query_offset = query - octant.center
    query_offset_abs = np.fabs(query_offset)  # 当前待查询点query到octant中心的绝对距离
    possible_space = query_offset_abs + radius  # 上述绝对距离加上半径
    # 比较绝对距离加半径与 octant一半边长的大小 若小于则表示query在octant内,反之则表示query在octant外
    return np.all(possible_space  octant.extent)  

def overlaps(query: np.ndarray, radius: float, octant: Octant):
    """
        需要仔细琢磨
        功能:判断一个立方体与一个球是否有交集
        query: 待查询点
        radius: 球半径
        octant: 待比较的子区域
    """
    query_offset = query - octant.center
    query_offset_abs = np.fabs(query_offset)  # 当前待查询点query到octant中心的绝对距离
    max_dist = radius + octant.extent  # 将球半径与当前octant一半边长作为最大距离阈值
    if np.any(query_offset_abs > max_dist):   # case1  判断是否相离
        return False
    if np.sum((query_offset_abs  octant.extent).astype(np.int)) >= 2: # case2 球与面是否相交
        return True
    # case3:比较对角线+球半径 与 octant中心到球心之间的距离,来判断球与立方体角点是否相交
    # 另外,通过max来减少一个维度,使其能够判断球是否与立方体的棱边相交
    x_diff = max(query_offset_abs[0] - octant.extent, 0)
    y_diff = max(query_offset_abs[1] - octant.extent, 0)
    z_diff = max(query_offset_abs[2] - octant.extent, 0)
    
    return x_diff * x_diff + y_diff * y_diff + z_diff * z_diff  radius * radius

def octree_knn_search(root: Octant, db: np.ndarray, result_set: KNNResultSet, query: np.ndarray):
    """
        root: 创建的octree根节点
        db: 源数据样本点
        result_set: 存储搜索结果的容器
        query: 待查询点
    """
    if root is None:  # 若Octree为空,则直接返回False
        return False
    # 若当前区域为is_leaf,则直接将其中的点与待查询点进行逐个比较————暴力查找
    if root.is_leaf and len(root.point_indices) > 0:
        leaf_points = db[root.point_indices, :]  # 取出叶子节点区域中的全部点
        # 计算待查询点与所有叶子节点之间的距离
        diff = np.linalg.norm(np.expand_dims(query, 0) - leaf_points, axis=1)
        # 根据计算出的距离diff,来判断是否可以将对应的点放入到存储容器result_set中
        for i in range(diff.shape[0]):
            result_set.add_point(diff[i], root.point_indices[i])
        return inside(query, result_set.worstDist(), root)  # 这个inside函数表示的是什么意思
    # 若当前区域不是is_leaf,则要找当前结点下的8个子节点
    # 下面的3个if是找到最有可能包含待查询结点的子节点区域
    morton_code = 0
    if query[0] > root.center[0]:
        morton_code = morton_code | 1
    if query[1] > root.center[1]:
        morton_code = morton_code | 2
    if query[2] > root.center[2]:
        morton_code = morton_code | 4
    # 在找到的子节点区域中继续递归下去找,若能够返回True,则表示在当前子节点区域下找到了
    if octree_knn_search(root.children[morton_code], db, result_set, query):
        return True
    # 若返回的是False,则表示上面那个子节点区域中没有找到,需要继续在其他子节点区域中查找
    for c, child in enumerate(root.children):
        # 遍历到上面已经查找过的子节点区域(morton_code) 或者 遍历到的子节点区域为空时
        # 直接跳过,在下一个子节点区域中进行查找
        if c == morton_code or child is None:
            continue
        # overlaps是判断当前octant 与 以待查询点query为圆心、worstDist为半径的球是否有交集————相离
        # 若没有交点(返回的是False),则跳过当前子区域octant不进行查找
        if False == overlaps(query, result_set.worstDist(), child):
            continue
        # 在剩下的子区域中进行查找
        if octree_knn_search(child, db, result_set, query):
            return True
    # 若以待查询点为圆心、worstDist为半径的球被octant包围了,那么就可提前终止搜索
    # inside():表示一个球是否完全被一个立方体所包围
    return inside(query, result_set.worstDist(), root)
        

(5)Octree的KNN改进查找
​ 若可判断出以当前查询点query为球心、worseDist为半径的球包含一个子区域,那么就不必在其他子区域中查找,只需在这个被包围的子区域中查找即可

def contains(query: np.ndarray, radius: float, octant: Octant):
    """
        功能:判断以query为球心,radius为半径的球是否包围子区域octant
        query: 待查询点
        radius: 球心
        octant: 子区域
    """
    query_offset = query - octant.center
    query_offset_abs = np.fabs(query_offset)  # 当前待查询点query到octant中心的绝对距离
    
    # 将绝对距离 + 当前octant一半长度 作为 最大距离阈值,如下图所示
    query_offset_to_farthest_corner = query_offset_abs + octant.extent
    return np.linalg.norm(query_offset_to_farthest_corner)  radius

文章转载遵循CC 4.0 BY-SA版权协议,来源于互联网: 三维点云处理之最近邻问题 | https://blog.csdn.net/LDST_CSDN/article/details/130378074

正文完
 0
评论(没有评论)