写给 Baby 的 Kd-tree 实现原理

Baby 级 Kd-tree 实现原理


Kd 树是一种树形的描述空间点云的数据结构和算法.

这里主要涉及 2 维的Kd 树的构建、最近邻搜索 (Nearest neighbor search, NN search)、K 最近邻搜索 (K-nearest neighbor search, KNN search).

一. Kd 树的构建

1. Kd 树节点的数据结构

class TreeNode:
    def __init__(self, point, parent, axis):
        self.point_ = point             # 树节点的坐标
        self.parent_ = parent           # 父节点,根节点的父节点设为[nan, nan]
        self.partition_axis_ = axis     # 树节点上对应的划分轴,(X轴,Y轴) - (0,1)  
        self.below_child_ = None        # 低的子节点,X轴划分对应划分轴的左侧,Y轴划分对应划分轴的下侧
        self.above_child_ = None        # 高的子节点,X轴划分对应划分轴的右侧,Y轴划分对应划分轴的上侧
        self.explored_flag_ = 0         # 规避重复之用 (k最近领查询中避免重复搜索查询点的插入/依附点)

2. Kd 树的迭代构建

# 产生kd树,就是划分各个kd树节点
class GenerateKdTree:
    def __init__(self, pointArray):
        self.point_array_ = pointArray
        self.max_dimension_ = 2                 # 平面kd树的划分
        self.kd_tree_ = KdTree()                # 待填充的kd树节点数组结果
        root_parent = TreeNode(virtual_point, None, None)     # kd树根节点的父节点
        self.splitPoints(self.point_array_.points_, root_parent)  # 划分kd树

    def splitPoints(self, points, parent_node):
        
        """
        迭代划分kd树获得kd树的节点
        输入:
        points —— 平面点数组/点列
        parent_node —— 根节点的父节点
        输出:
        a_tree_node —— kd树的一个划分节点
        数据传输:
        self.tree_nodes_ —— 填充的kd树节点数组结果
        """
        
        if np.size(points) == 0:   # 没有数据点数组输入情况
            print("No points")
            return

        if math.isnan(parent_node.point_[0]) or math.isnan(parent_node.point_[1]):      
            # 根节点计算
            x_var = np.var(points[0,:])
            y_var = np.var(points[1,:])
            if x_var >= y_var:                          
                # 第一次划分轴按照方差大小确定,某轴对应方差大小正比于点列在该方向上的分散程度
                partition_axis_index_ = 0               
                # 0 —— X轴
            else:
                partition_axis_index_ = 1               
                # 1 —— Y轴

        else:
            partition_axis_index_ = (parent_node.partition_axis_ + 1) % self.max_dimension_         
            # 后续划分按照X-Y轴轮换次序
            
        if np.size(points, 1) == 1:
            a_tree_node = TreeNode(points[:, 0], parent_node, partition_axis_index_)        
            # 只有一个数据点情况,该点就是kd树节点
            self.kd_tree_.nodeAppend(a_tree_node)
        elif np.size(points, 1) > 1:                                                        
            # 数据点多与1个情况
            point_array_sorted = points[:,np.argsort(points[partition_axis_index_,:], 0)]   
            # 按照对应分割轴,对将要分割的点进行排序
            middle_position = math.floor(np.size(point_array_sorted, 1)/2)                  
            # 排序后点列的中间点即为分割点
            a_tree_node = TreeNode(point_array_sorted[:, middle_position], parent_node, partition_axis_index_)  
            # 产生一个kd树节点,低子节点和高子节点都暂时设定为None,待迭代计算获得后再填入
            below_point_array = point_array_sorted[:, 0 : middle_position]                  
            # 在节点左侧/下侧的数据点分为新的一组
            below_child = self.splitPoints(below_point_array, a_tree_node)                  
            # 迭代计算,产生新的低子节点
            a_tree_node.below_child_ = below_child                                          
            # 填入当前节点的低子节点

            if (middle_position + 1) < np.size(point_array_sorted, 1):                      
                # 确保当前kd树节点的右侧/上侧的数据点存在
                above_point_array = point_array_sorted[:, middle_position + 1 : np.size(point_array_sorted, 1)] 
                # 获得右侧/上侧的数据点列
                above_child = self.splitPoints(above_point_array, a_tree_node)              
                # 迭代计算,产生新的高子节点
                a_tree_node.above_child_ = above_child                                      
                # 填入当前节点的高子节点

            self.kd_tree_.nodeAppend(a_tree_node)            
            # 填充的kd树节点数组结果
        
        return a_tree_node          
    	# 返回当前树节点,迭代之用

3. Kd 树节点的区域计算

# 每一个kd树节点对应的区域范围
class TreeZone:
    def __init__(self, node_point, start_point, end_point, leftbottom_corner, righttop_corner, parent_zone):
        self.node_point_ = node_point              
        # kd树节点坐标
        self.start_point_ = start_point             
        # 经过对应kd树节点的kd树区域的划分线的起始点,X轴划分时对应垂直线的最下点,Y轴划分时对应水平线的最左点
        self.end_point_ = end_point                 
        # 经过对应kd树节点的kd树区域的划分线的终止点,X轴划分时对应垂直线的最上点,Y轴划分时对应水平线的最右点
        self.leftbottom_corner_ = leftbottom_corner         
        # kd树节点代表的矩形区域的左下点
        self.righttop_corner_ = righttop_corner             
        # kd树节点代表的矩形区域的右上点
        self.parent_zone_ = parent_zone                     
        # 包含当前kd树节点矩形区域的父区域
 
        
# 产生每一个kd树节点对应的区域范围以及划分线段
class GenerateKdTreeZones:
    def __init__(self, kd_tree, leftbottom_corner, righttop_corner):
        self.tree_nodes_ = kd_tree.getKdNodes()                 
        # kd树节点数列,已包含kd树的完整信息
        self.root_index_ = kd_tree.getRootIndex()          
        # 根节点在kd树节点数列的位置指标
        self.tree_zones_ = []                                   
        # 待计算填充的kd树区域数列
        self.leftbottom_corner_ = leftbottom_corner             
        # 左下角坐标
        self.righttop_corner_ = righttop_corner                 
        # 右上角坐标

    def computeTreeZone_root(self):                             
    # 计算根节点对应矩形区域,即全部区域
        root_point = self.tree_nodes_[self.root_index_].point_
        if self.tree_nodes_[self.root_index_].partition_axis_ == 0:                             
        # 如果将全部区域在X轴方向上分为左右两半
            start_point = np.array([self.tree_nodes_[self.root_index_].point_[0], min_range])   
            # 垂直的划分线段,经过根节点,Y坐标在上下限之间
            end_point = np.array([self.tree_nodes_[self.root_index_].point_[0], max_range])
        else:                                                                                   
        # 如果将全部区域在Y轴方向上分为上下两半
            start_point = np.array([min_range, self.tree_nodes_[self.root_index_].point_[1]])   
            # 水平的划分线段,经过根节点,X坐标在左右限之间
            end_point = np.array([max_range, self.tree_nodes_[self.root_index_].point_[1]])

        a_tree_zone = tree_zone.TreeZone(root_point, start_point, end_point, self.leftbottom_corner_, self.righttop_corner_, None)
        # 产生根节点对应区域以及该区域的划分线段
        self.tree_zones_.append(a_tree_zone)
        
        return a_tree_zone


    def computeTreeZone(self, parent_tree_node, parent_tree_zone):
        """
        迭代计算根节点以下各个节点对应的区域
        输入:
        parent_tree_node —— kd树架构中的父节点
        parent_tree_zone —— 父节点对应的区域
        输出:
        无
        数据传输:
        self.tree_zones_ —— 填充的kd树对应几何区域的数组结果
        """
        if (parent_tree_node.below_child_ is not None):                     
        # 通过kd树判断父节点是否有低子节点,如有则计算低子节点对应的区域几何和划分线段
            current_tree_node = parent_tree_node.below_child_               
            # 父节点的低子节点 (左侧或者下侧) 作为要计算对应区域几何的当前节点
            if (current_tree_node.partition_axis_ == 0):                    
            # 如果当前kd树节点上的划分轴是X轴
                """ 参考图形
                -----------
                |         |
                |         |
                -----+-----
                |    |    |
                |    |    |
                -----+-----
                """
                start_point = np.array([current_tree_node.point_[0], parent_tree_zone.leftbottom_corner_[1]])   
                # 当前kd树节点上的划分线段的起始点, X坐标同经过的当前节点的X坐标,Y坐标同父区域左下角的Y坐标
                end_point = np.array([current_tree_node.point_[0], parent_tree_zone.node_point_[1]])
                # 当前kd树节点上的划分线段的终止点, X坐标同经过的当前节点的X坐标,Y坐标同父节点的Y坐标
                a_tree_zone = tree_zone.TreeZone(current_tree_node.point_, 
                                                 start_point, end_point,
                                                 parent_tree_zone.leftbottom_corner_, parent_tree_zone.end_point_, parent_tree_zone)
                # 当前kd树节点代表的区域,左下角就是父节点区域的左下角,右上角就是父节点的区域划分线段的终止点
                self.tree_zones_.append(a_tree_zone)    
                #写入区域划分数组

            else:                
            # 如果当前kd树节点上的划分轴是Y轴
                """ 参考图形
                -----------
                |    |    |
                |    |    |
                +----+    |
                |    |    |
                |    |    |
                -----------
                """
                start_point = np.array([parent_tree_zone.leftbottom_corner_[0], current_tree_node.point_[1]])   
                # 当前kd树节点上的划分线段起始点,X坐标同父区域左下角的X坐标,Y坐标同当前节点的Y坐标 (水平方向,通过当前kd树节点)
                end_point = np.array([parent_tree_zone.node_point_[0], current_tree_node.point_[1]])
                # 当前kd树节点上的划分线段终止点,X坐标同父节点的X坐标,Y坐标同当前节点的Y坐标 (水平方向,通过当前kd树节点)
                a_tree_zone = tree_zone.TreeZone(current_tree_node.point_, 
                                                 start_point, end_point,
                                                 parent_tree_zone.leftbottom_corner_, parent_tree_zone.end_point_, parent_tree_zone)
                # 当前kd树节点代表的区域,左下角就是父节点区域的左下角,右上角就是父节点的区域划分线段的终止点
                self.tree_zones_.append(a_tree_zone)    
                #写入区域划分数组
            
            self.computeTreeZone(current_tree_node, a_tree_zone)    
            # 迭代计算,当前节点作为父节点,当前区域作为父区域

        if (parent_tree_node.above_child_ is not None):             
        # 通过kd树判断父节点是否有高子节点,如有则计算高子节点对应的区域几何和划分线段
            current_tree_node = parent_tree_node.above_child_       
            # 父节点的高子节点 (右侧或者上侧) 作为要计算对应区域几何的当前节点
            if (current_tree_node.partition_axis_ == 0):            
            # 如果当前kd树节点上的划分轴是X轴
                """ 参考图形
                -----+-----
                |    |    |
                |    |    |
                -----+-----
                |         |
                |         |
                -----------
                """
                start_point = np.array([current_tree_node.point_[0], parent_tree_zone.node_point_[1]])
                # 当前kd树节点上的划分线段的起始点, X坐标同当前节点的X坐标,Y坐标同父节点的Y坐标 (垂直方向,通过当前kd树节点)
                end_point = np.array([current_tree_node.point_[0], parent_tree_zone.righttop_corner_[1]])
                # 当前kd树节点上的划分线段的终止点, X坐标同当前节点的X坐标,Y坐标同父区域右上角的Y坐标 (垂直方向,通过当前kd树节点)
                a_tree_zone = tree_zone.TreeZone(current_tree_node.point_, 
                                                 start_point, end_point,
                                                 parent_tree_zone.start_point_, parent_tree_zone.righttop_corner_, parent_tree_zone)
                # 当前kd树节点代表的区域,左下角是父节点的区域划分线段的起始点,右上角是父区域的右上角
                self.tree_zones_.append(a_tree_zone)    
                #写入区域划分数组

            else:           
            # 如果当前kd树节点上的划分轴是Y轴
                """ 参考图形
                -----------
                |    |    |
                |    |    |
                |    +----+
                |    |    |
                |    |    |
                -----------
                """
                start_point = np.array([parent_tree_zone.node_point_[0], current_tree_node.point_[1]])
                # 当前kd树节点上的划分线段起始点,X坐标同父节点的X坐标,Y坐标同当前节点的Y坐标 (水平方向,通过当前kd树节点)
                end_point = np.array([parent_tree_zone.righttop_corner_[0], current_tree_node.point_[1]])
                # 当前kd树节点上的划分线段终止点,X坐标同父区域右上角的X坐标,Y坐标同当前节点的Y坐标 (水平方向,通过当前kd树节点)
                a_tree_zone = tree_zone.TreeZone(current_tree_node.point_, 
                                                 start_point, end_point,
                                                 parent_tree_zone.start_point_, parent_tree_zone.righttop_corner_, parent_tree_zone)
                # 当前kd树节点代表的区域,左下角就是父节点的区域划分线段的起始点,右上角就是父区域的右上角
                self.tree_zones_.append(a_tree_zone)    
                #写入区域划分数组
            
            self.computeTreeZone(current_tree_node, a_tree_zone)    
            # 迭代计算,当前节点作为父节点,当前区域作为父区域

4. 结果显示

Figure_1
Fig.1 20 个点的 Kd 树分割结果图示
Figure_2
Fig.2 20 个点的 Kd 树分割结果及其区域划分
Figure_3
Fig.3 10 个点的 Kd 树分割结果图示

二. 最近邻搜索 Nearest Neighbor Search

1. 最近邻搜索的实现

关键就两点最小距离的更新、分支界定的剪枝.

class NearestNeighborQuery:
    def __init__(self, kd_tree, query_point):
        self.kd_tree_ = kd_tree
        self.query_point_ = query_point
        self.nearest_point_ = None
        self.min_distance_ = -1
        
           
    def insertQueryPoint(self):
        """
        从根节点开始遍历kd树, 找到查询点query_point插入/依附的最末一级节点last_node
        最末一级节点last_node是某一侧不再可以深入的kd树节点 (可能是两侧都没有子节点的叶节点, 也可能是只有一侧子节点的中间节点)
        插入/依附点作为最近邻查询的初始猜测的最近邻点, 相应的距离作为初始猜测的最近邻距离
        初始猜测的最近邻距离离真实最近距离越接近, 后面分支界定的剪枝效果越好
        输入:
         无
        输出:
        1. last_node —— query_point插入/依附的最末一级节点
        2. min_distance —— query_point和last_node之间的距离, 作为最近邻查询的最近距离猜测值
        数据传输:
        1. self.kd_tree_ —— kd树
        2. self.query_point_ —— 查询点
        """
        
        root_index = self.kd_tree_.root_index_              
        # kd树的根节点位置指标
        tree_node = self.kd_tree_.kd_nodes_[root_index]     
        # kd树的根节点作为下面迭代的起始节点
        while (tree_node is not None):      
            # 每一轮循环先判断当前kd树节点不为空/None, 如为None则不再执行下去
            last_node = tree_node           
            # 先以当前节点作为查询点依附的节点last_node, 再去判断当前节点的低子节点和高子节点是否可以作为深入下去的依附节点
            if (self.query_point_[tree_node.partition_axis_] > tree_node.point_[tree_node.partition_axis_]): 			 # 以kd树节点的分割轴对应坐标进行判断
                tree_node = tree_node.above_child_  
                # 如果query_point在当前节点的高处(右或上), 则当前kd树节点的高子节点作为新的当前kd树节点
            else:
                tree_node = tree_node.below_child_  
                # 如果query_point在当前节点的低处(左或下), 则当前kd树节点的低子节点作为新的当前kd树节点
            
        print("\nquery point {} belongs to the kd-tree node {}".format(self.query_point_, last_node.point_))

        min_distance = math.sqrt(math.pow((last_node.point_[0]-self.query_point_[0]), 2) + math.pow((last_node.point_[1]-self.query_point_[1]), 2))
        # query_point和last_node之间的欧式距离, 作为最近邻查询的最近距离的猜测值

        last_node.explored_flag_ = 1

        return last_node, min_distance  
        # 通过插入查询点到最末一级节点,返回最近邻查询的初始猜测的最近邻点及相应的距离
    

    def subNearestNeighborQuery(self, nearest_node, min_distance, tree_node):
        """
        从根节点开始计算距离, 和现有最小距离作比较
        更新计算: 如果当前节点与查询点之间的距离 (当前距离) 比现有最小距离还小, 更新最近邻点为当前节点, 更新最小距离为当前距离
        分支界定: 计算查询点到当前节点的分割线段之间的垂直距离, 通过垂直距离与现有最小距离之间比较, 进行分支界定的剪枝
                 如果垂直距离小于现有最小距离, 则树节点高低两侧都有可能存在与查询点更近的kd树节点, 需要迭代进入当前节点的高子节点和低子节点
                 如果垂直距离大于现有最小距离, 当前节点画分线段另一侧子节点及其子节点的子节点都不能是潜在的解, 可以被剪枝以减少计算
                 如果垂直距离大于现有最小距离, 迭代进入与查询点同侧的当前节点的子节点进行迭代
        
        输入: 
        1. nearest_node —— 现有已知最近邻点
        2. min_distance —— 查询点与现有已知最近邻点之间的最近距离
        3. tree_node —— 将要计算距离的kd树节点
        输出:
        1. nearest_node —— 更新后的最近邻点
        min_distance —— 更新后的最近距离
        """

        if (tree_node is None):         
            # 确定将要计算距离的树节点不为空
            print("tree_node is None")
            return 
        
        # 更新计算
        node_distance = math.sqrt(math.pow((tree_node.point_[0]-self.query_point_[0]), 2) + math.pow((tree_node.point_[1]-self.query_point_[1]), 2))
        # 计算当前树节点与查询点之间的欧式距离 (当前距离)
        print("current node: {},\t current distance: {},\t nearest_node: {},\t min_distance: {:.4f}".format(tree_node.point_, node_distance, nearest_node.point_, min_distance))
        if (node_distance < min_distance):  
            # 如果当前距离小于现有最小距离, 则进行更新: 当前树节点作为查询点的最近邻点, 当前距离作为最小距离
            nearest_node = tree_node
            min_distance = node_distance
            print("- [Update the nearest neighor] - nearest_node: {},\t min_distance: {}".format(nearest_node.point_, min_distance))
        
        # 分支界定
        perpendicular_distance = abs(tree_node.point_[tree_node.partition_axis_] - self.query_point_[tree_node.partition_axis_])
        # 计算查询点与当前kd树节点的划分线段之间的垂直距离, 作为剪枝的依据
        if perpendicular_distance < min_distance:   
            # 如果垂直距离小于现有最小距离, 则当前kd树节点的两侧都有可能存在最近邻点, 需要分别进入两侧的子节点继续迭代计算
            print("current node: {},\t perpendicular_distance: {:.4f},\t nearest_node: {},\t min_distance: {:.4f},\t goto BOTH sides".format(tree_node.point_, perpendicular_distance, nearest_node.point_, min_distance))
            if (tree_node.below_child_ is not None):    
            # 当前节点的低子节点对应当前节点的左侧或者下侧, 需要进入迭代计算, 除非低子节点为空
                nearest_node, min_distance = self.subNearestNeighborQuery(nearest_node, min_distance, tree_node.below_child_)
            if (tree_node.above_child_ is not None):    
            # 当前节点的高子节点对应当前节点的右侧或者上侧, 需要进入迭代计算, 除非高子节点为空
                nearest_node, min_distance = self.subNearestNeighborQuery(nearest_node, min_distance, tree_node.above_child_)
        else:       
        # 如果垂直距离大于现有最小距离, 则可以剪除一侧枝以减少运算, 只需要进入其中一侧的子节点继续迭代计算
            print("current node: {},\t perpendicular_distance: {:.4f},\t nearest_node: {},\t min_distance: {:.4f},\t goto SINGLE side".format(tree_node.point_, perpendicular_distance, nearest_node.point_, min_distance))
            if  (self.query_point_[tree_node.partition_axis_] < tree_node.point_[tree_node.partition_axis_]): 
                # 查询点在当前kd树节点的低侧
                if (tree_node.below_child_ is not None):    
                # 进入当前kd树节点的低侧子节点继续迭代计算, 除非低侧子节点为空
                    nearest_node, min_distance = self.subNearestNeighborQuery(nearest_node, min_distance, tree_node.below_child_)
            else: 
            # 查询点在当前kd树节点的高侧
                if (tree_node.above_child_ is not None):    
                # 进入当前kd树节点的高侧子节点继续迭代计算, 除非高侧子节点为空
                    nearest_node, min_distance = self.subNearestNeighborQuery(nearest_node, min_distance, tree_node.above_child_)

        return nearest_node, min_distance


    def doNearestNeighborQuery(self):
        """
        以查询点插入/依附的树节点作为初始猜测的最近邻点
        调用self.subNearestNeighborQuery函数, 获得最近邻距离
        """
        self.checkData()
        nearest_node, min_distance = self.insertQueryPoint()

        root_index = self.kd_tree_.root_index_              
        # kd树的根节点位置指标
        tree_node = self.kd_tree_.kd_nodes_[root_index]     
        # kd树的根节点作为下面迭代的起始节点
            
        nearest_node, min_distance = self.subNearestNeighborQuery(nearest_node, min_distance, tree_node)
        self.nearest_point_ = nearest_node.point_
        self.min_distance_ = min_distance
        print("- [result of the nearest neighor] - query_point: {},\t nearest_point: {},\t min_distance: {}".format(self.query_point_, self.nearest_point_, self.min_distance_))
        return self.nearest_point_, self.min_distance_

2. 结果显示

Figure_4-nn
Fig.4 20 个点的最近邻搜索结果图示
Figure_5
Fig.5 20 个点的最近邻搜索及剪枝过程
Figure_6
Fig.6 100 个点的最近邻搜索结果图示

三. K 最近邻搜索 K-Nearest Neighbor Search

1. K 最近邻搜索的实现

K 最近邻搜索与最近邻搜索的思想和实现都差不多, 两个实现之间可以继承, 只是要在计算过程中维护一个前 K 个最近邻的数组.

class MulitNearestNeighborQuery(nearest_query.NearestNeighborQuery):  
    # KNN - MulitNearestNeighborQuery 继承自 NN - NearestNeighborQuery
    def __init__(self, kd_tree, query_point, k_size):
        super(MulitNearestNeighborQuery, self).__init__(kd_tree, query_point)
        self.k_size_ = k_size               
        # MulitNearestNeighborQuery 新增属性
        self.k_nearest_neighbors_ = []      
        # MulitNearestNeighborQuery 新增属性


    def subMultiNearestNeighborQuery(self, tree_node):
        """
        从根节点开始计算距离, 和现有第k_size个最近距离作比较
        更新计算: 维护一个k_size个最近距离的数组self.k_nearest_neighbors_.
                 初始时刻的近邻点数组k_nearest_neighbors_包括按照查询点在kd树中插入/依附节点及对应距离
                 其余为最大范围值作为其他k_size个初始距离
                 如果当前节点与查询点之间的距离比近邻点数组k_nearest_neighbors_中维护的现有第k_size个最近距离还小, 
                 则将近邻点数组k_nearest_neighbors_中的有第k_size个最近距离对应项更新为当前节点项
        分支界定: 计算查询点到当前节点的分割线段之间的垂直距离, 通过垂直距离与现有第k_size个最近距离之间比较,
        		 进行分支界定的剪枝
                 如果垂直距离小于现有第k_size个最近距离, 则树节点高低两侧都有可能存在与查询点最接近的前k_size个kd树节点, 
                 需要迭代进入当前节点的高子节点和低子节点
                 如果垂直距离大于现有第k_size个最近距离, 当前节点画分线段另一侧子节点及其子节点的子节点都不能是潜在的解, 
                 可以被剪枝以减少计算
                 如果垂直距离大于现有第k_size个最近距离, 迭代进入与查询点同侧的当前节点的子节点进行新的迭代
        
        输入: 
        1. tree_node —— 将要计算距离的kd树节点
        输出:
            无
        数据传输:
        1. self.k_size_ —— k_size个近邻点
        2. self.k_nearest_neighbors_ —— k_size个近邻点数组, 包括近邻点与查询点之间的距离、近邻点节点对象
        """

        if (tree_node is None):         
            # 确定将要计算距离的树节点不为空
            print("tree_node is None")
            return 

        if (tree_node.explored_flag_ == 0):                 
            # 如果当前就是查询点插入的节点, 为了不重复计算, 直接跳过
            
            # 更新最近邻数组
            node_distance = math.sqrt(math.pow((tree_node.point_[0]-self.query_point_[0]), 2) + math.pow((tree_node.point_[1]-self.query_point_[1]), 2))
            # 计算当前树节点与查询点之间的欧式距离 (当前距离)
            max_index = np.argmax(self.k_nearest_neighbors_[:,0])
            # 最近邻数组中的最大距离, 就是第k_size个最近邻距离

            if (node_distance < self.k_nearest_neighbors_[max_index, 0]):   
                # 如果当前距离小于第k_size个最近邻距离, 则更新最近邻数组中的第k_size个最近邻及距离
                self.k_nearest_neighbors_ = np.delete(self.k_nearest_neighbors_, max_index, axis = 0)
                # 删除老的第k_size个最近邻项
                self.k_nearest_neighbors_ = np.append(self.k_nearest_neighbors_, np.array([[node_distance, tree_node]]), axis = 0)
                # 在最近邻数组中增加新的近邻节点及对应距离
        
        # 分支界定
        perpendicular_distance = abs(tree_node.point_[tree_node.partition_axis_] - self.query_point_[tree_node.partition_axis_])
        # 计算查询点与当前kd树节点的划分线段之间的垂直距离, 作为剪枝的依据

        max_index = np.argmax(self.k_nearest_neighbors_[:,0])  
        # 重新获得最近邻数组中的第k_size个最近邻距离的数组指标
        if perpendicular_distance < self.k_nearest_neighbors_[max_index, 0]:   
        # 如果垂直距离小于现有第k_size个最小距离, 则当前kd树节点的两侧都有可能存在前k_szie个最近邻点, 需要分别进入两侧的子节点继续迭代计算
            print("current node: {},\t perpendicular_distance: {:.4f},\t threshold_node: {},\t threshold_distance: {:.4f},\t goto BOTH sides".format(tree_node.point_, perpendicular_distance, self.k_nearest_neighbors_[max_index, 1].point_ if (self.k_nearest_neighbors_[max_index, 1] is not None) else None, self.k_nearest_neighbors_[max_index, 0]))
            if (tree_node.below_child_ is not None):    
            # 当前节点的低子节点对应当前节点的左侧或者下侧, 需要进入迭代计算, 除非低子节点为空
                self.subMultiNearestNeighborQuery(tree_node.below_child_)
            if (tree_node.above_child_ is not None):    
            # 当前节点的高子节点对应当前节点的右侧或者上侧, 需要进入迭代计算, 除非高子节点为空
                self.subMultiNearestNeighborQuery(tree_node.above_child_)
        else:       
        # 如果垂直距离大于现有第k_size个最小距离, 则可以剪除一侧枝以减少运算, 只需要进入其中一侧的子节点继续迭代计算
            print("current node: {},\t perpendicular_distance: {:.4f},\t threshold_node: {},\t threshold_distance: {:.4f},\t goto SINGLE side".format(tree_node.point_, perpendicular_distance, self.k_nearest_neighbors_[max_index, 1].point_ if (self.k_nearest_neighbors_[max_index, 1] is not None) else None, self.k_nearest_neighbors_[max_index, 0]))
            if  (self.query_point_[tree_node.partition_axis_] < tree_node.point_[tree_node.partition_axis_]): 
                # 查询点在当前kd树节点的低侧
                if (tree_node.below_child_ is not None):    
                # 进入当前kd树节点的低侧子节点继续迭代计算, 除非低侧子节点为空
                    self.subMultiNearestNeighborQuery(tree_node.below_child_)
            else: 
            # 查询点在当前kd树节点的高侧
                if (tree_node.above_child_ is not None):    
                # 进入当前kd树节点的高侧子节点继续迭代计算, 除非高侧子节点为空
                    self.subMultiNearestNeighborQuery(tree_node.above_child_)


    def doMultiNearestNeighborQuery(self):
        """
        以查询点插入/依附的树节点作为初始猜测的最近邻点之一, 其他k_szie-1个近零距离设置为最大所搜距离
        调用self.subMultiNearestNeighborQuery函数, 获得k_size个最近邻距离
        """
        self.checkData()
        last_node, node_distance = self.insertQueryPoint() # 以查询点插入/依附的树节点作为初始猜测的最近邻点之一
        self.k_nearest_neighbors_.append(np.array([node_distance, last_node])) # 加入最近邻数组

        threshold_distance = math.sqrt(2) * abs(max_range - min_range)
        while (np.shape(self.k_nearest_neighbors_)[0] < self.k_size_):
            self.k_nearest_neighbors_.append(np.array([threshold_distance, None])) 
            # 其他k_szie-1个近零距离设置为最大所搜距离

        self.k_nearest_neighbors_ = np.array(self.k_nearest_neighbors_)  
        # 把列表list变成数组array,便于索引和排序
        
        root_index = self.kd_tree_.root_index_              
        # kd树的根节点位置指标
        tree_node = self.kd_tree_.kd_nodes_[root_index]     
        # kd树的根节点作为下面迭代的起始节点
        self.subMultiNearestNeighborQuery(tree_node)

        self.k_nearest_neighbors_ = self.k_nearest_neighbors_[np.argsort(self.k_nearest_neighbors_[:, 0], axis = 0), :]  
        # 对k_size个最近邻节点按照与查询点之间的距离进行排序

2. 结果显示

Figure_7
Fig.7 20 个点中查找 4 个最近邻节点的结果图示
Figure_8
Fig.8 20 个点中查找 4 个最近邻节点的搜索与剪枝过程
Figure_9-100Points-4KNN
Fig.9 100 个点中查找 4 个最近邻节点的结果显示

附件

完整 demo 程序见本文绑定资源.
https://download.csdn.net/download/woyaomaishu2/88203742?spm=1001.2101.3001.9500

请提宝贵意见!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值