KD树的概念、构建和最近邻搜索算法实现(PCL/C++)

1. KD树简介

KD树(K-Dimensional Tree)是一种用于组织点在k维空间中的数据结构,它是一种带有其他约束条件的二分查找树。KD树主要用于解决多维空间搜索问题,如范围搜索(RNN)、最近邻搜索(KNN)等。以下是KD树的一些基本特性和概念:

分割维度:KD树在每个节点上根据一个维度将数据空间分割成两部分。例如,在二维空间中,节点可能根据x坐标或y坐标来分割空间。

节点结构:每个节点代表一个分割点,并且包含以下信息:

  • 一个点的坐标。
  • 一个分割维度(在多维空间中,这个维度是变化的)。
  • 左右子树,分别包含分割点的左边和右边的点。

构建过程:KD树的构建过程是递归的。首先选择一个维度和中位数点作为根节点,然后根据该点在选定维度上的值将点集分为两部分。对这两部分分别递归地构建子树。

平衡性:理想情况下,KD树是平衡的,即每个维度上的分割都是均匀的。但在实际应用中,由于数据分布的不均匀性,KD树可能会变得不平衡。

搜索效率:KD树可以显著提高搜索效率,特别是在高维空间中。对于最近邻搜索,KD树可以减少需要比较的数据点数量。

应用领域:KD树广泛应用于计算机图形学、机器学习、计算机视觉、空间数据库等领域。

变体:KD树有多种变体,如KD-B树、KD-V树等,它们在不同的应用场景下提供了不同的优化。

二叉树相关概念可参考博客二叉树的基本概念及操作

2. KD树的创建

KD树的构建过程是一个递归过程,其基本步骤如下:

  1. 选择轴和中位数:首先选择一个维度(在二维空间中,这将是x或y轴)。然后在这个维度上找到所有点的中位数,并选择这个中位数点作为当前节点。

  2. 分割空间:使用中位数点在所选维度上将点集分割成两部分。所有在中位数点左侧的点将位于左子树,所有在右侧的点将位于右子树。

  3. 递归构建子树:对于左子树,选择下一个维度(在二维空间中,如果第一个节点是沿x轴分割的,那么下一个节点将沿y轴分割),然后重复步骤1和2来构建左子树。对于右子树,同样选择下一个维度并构建。

  4. 终止条件:当递归到达一个节点,该节点没有更多的点,或者点的数量小于某个阈值(通常是1或2),则停止递归。

  5. 平衡性考虑:在理想情况下,每次分割都应该是平衡的,即左右子树中的点数大致相等。这有助于保持树的平衡,从而提高搜索效率。但在实际应用中,由于数据分布的不均匀性,可能需要采取一些策略来处理不平衡的情况。

  6. 选择中位数的策略:在某些情况下,可能需要选择不同的点作为中位数,例如选择当前点集中的最小值或最大值,或者使用更复杂的方法来选择分割点,以提高树的性能。

  7. 构建辅助数据结构:在构建KD树的过程中,可能还需要构建一些辅助数据结构,如矩形边界框(MBR),以帮助快速确定点是否位于搜索区域之内。

在C++中实现KD树的构建,我们需要定义几个基本的组件:点(Point)的数据结构、KD树节点(KDNode)的数据结构、以及构建KD树的函数。接下来,我们通过一个一个简单的示例实现,来进一步了解KD树的构建过程:

(1)定义点(Point)的数据结构:

struct Point {
    std::vector<double> coords; // 存储多维坐标

    // 构造函数
    Point(std::initializer_list<double> init) : coords(init) {}

    // 访问特定维度的坐标
    double operator[](int i) const {
        return coords[i];
    }

    // 重载比较运算符,用于排序
    bool operator<(const Point& other) const {
        for (size_t i = 0; i < coords.size(); ++i) {
            if (coords[i] < other.coords[i]) return true;
            if (coords[i] > other.coords[i]) return false;
        }
        return false;
    }
};

(2)定义KD树节点(KDNode)的数据结构:

class KDNode {
public:
    Point point;
    int axis; // 当前分割的维度
    KDNode* left;
    KDNode* right;

    KDNode(const Point& p) : point(p), axis(-1), left(nullptr), right(nullptr) {}

    ~KDNode() {
        delete left;
        delete right;
    }
};

(3)实现构建KD树的函数:

void buildKDTree(std::vector<Point>& points, KDNode*& root, int depth = 0) {
    if (points.empty()) {
        return;
    }

    int axis = depth % points[0].coords.size(); // 选择当前维度
    std::sort(points.begin(), points.end(), [axis](const Point& a, const Point& b) {
        return a[axis] < b[axis];
    });

    size_t medianIndex = points.size() / 2;
    root = new KDNode(points[medianIndex]);

    // 递归构建左右子树
    buildKDTree(std::vector<Point>(points.begin(), points.begin() + medianIndex), root->left, depth + 1);
    buildKDTree(std::vector<Point>(points.begin() + medianIndex + 1, points.end()), root->right, depth + 1);
}

(4)使用这些函数和结构来构建KD树:

int main() {
    std::vector<Point> points = {
        {1, 2},
        {3, 5},
        {2, 3},
        // ... 更多点
    };

    KDNode* root = nullptr;
    buildKDTree(points, root);

    // 现在KD树已经构建完成,root指向树的根节点

    // 清理内存
    delete root;

    return 0;
}

需要注意的是,以上的案例实现是简化的,没有考虑许多实际应用中可能需要的特性,比如平衡树、错误处理、内存管理等。在实际应用中,我们可能还需要实现更多的功能,比如搜索、插入、删除等操作。此外,对于大规模数据集,可能还需要考虑使用更高效的数据结构和算法来优化性能。

3. KD树的搜索算法介绍

了解KD树的基本概念和构建过程后,我们进一步来看KD树具体有哪些相关算法和应用。如前文所言,KD树是一种用于多维空间搜索的数据结构,常用来解决范围搜索和最近邻搜索问题。下面我们简单了解一下KD树的相关算法,并借助PCL实现一个最近邻搜索算法的demo。

(1)KD树搜索算法:

最近邻搜索:KD树的最近邻搜索算法利用了空间划分的特性,从根节点开始,根据查询点与节点的比较结果向下搜索,直至达到叶子节点。然后通过回溯的方式,判断未访问的分支是否有更近的点,通过计算查询点与子树边界的距离来决定是否需要进一步搜索 。

K近邻搜索:K近邻搜索是最近邻搜索的扩展,它需要维护一个优先队列来存储k个最近点。在搜索过程中,如果遇到新的节点,会与优先队列中最远的点进行比较,如果更近则替换 。

半径范围查询:在半径范围查询中,算法会返回所有与查询中心点距离小于等于给定半径的点。这涉及到计算查询点与子树边界的最小距离,并与当前最近点的距离进行比较,以决定是否需要进一步搜索 。

正交范围查询:正交范围查询返回给定超矩形内的所有数据点,通过检查查询区域与KD树每个节点的对应超矩形是否有交集来实现 。

BBF(Best-Bin-First)查询算法:这是一种针对高维数据的近似最近邻搜索算法,通过优先检索可能性较高的空间来提高搜索效率,并设置运行超时限制 。

KD树的性能受数据分布的影响,例如随机分布、圆形分布和对角线分布等,建树的耗时会根据数据点的数量和分布特性而有所不同 。

(2)PCL实现KD树最近邻搜索算法

PCL(Point Cloud Library)中的KD树最近邻搜索是通过pcl::KdTreeFLANN类实现的,这个类使用了FLANN(Fast Library for Approximate Nearest Neighbors)作为后端来执行高效的最近邻搜索 。最近邻搜索的基本思想是从根节点开始,根据查询点的坐标值,递归地选择左右子节点进行搜索,直至达到叶子节点。在PCL中,这个过程可以通过调用nearestKSearch函数来完成,该函数会返回查询点的K个最近邻点的索引和对应的平方距离 。在搜索过程中,PCL中的KD树利用了空间分割的原理,将点云数据分割成多个子空间,并在每个节点上选择一个维度进行分割,使得在每个节点上可以快速地判断查询点应该进入左子树还是右子树 。此外,搜索算法还包括了一种“回溯”机制,用于检查在搜索路径上可能存在的更近的点 。具体到实现,nearestKSearch函数首先检查输入点的有效性,并将其向量化,然后使用FLANN的最近邻搜索接口knnSearch来执行实际的搜索操作 。如果设置了索引映射(index_mapping_),还需要将搜索结果中的索引转换为原始点云中的对应索引。

在Point Cloud Library (PCL)中,构建KD树通常使用pcl::KdTreeFLANN类实现。以下是使用PCL构建KD树的基本步骤

  1. 包含头文件:首先需要包含KD树的头文件<pcl/kdtree/kdtree_flann.h>

  2. 创建点云对象:创建一个pcl::PointCloud对象,该对象包含了点云数据。

  3. 生成点云数据:通过某种方式填充点云对象的数据,例如随机生成点云数据。

  4. 创建KD树对象:实例化pcl::KdTreeFLANN类的对象,用于存储点云的KD树结构。

  5. 设置输入点云:使用KD树对象的setInputCloud方法,将点云数据设置为KD树的输入。

  6. 构建KD树:在设置好输入点云后,KD树将自动构建,无需显式调用构建函数。

下面是一个简单的示例代码,演示了如何在PCL中构建KD树并执行最近邻搜索:

#include <pcl/point_cloud.h>
#include <pcl/kdtree/kdtree_flann.h>
#include <iostream>
#include <vector>
#include <ctime>

int main() {
    // 初始化随机种子
    srand(static_cast<unsigned>(time(0)));

    // 创建点云对象并填充数据
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
    cloud->width = 1000;
    cloud->height = 1;
    cloud->points.resize(cloud->width * cloud->height);
    for (size_t i = 0; i < cloud->points.size(); ++i) {
        cloud->points[i].x = 1024.0f * rand() / (RAND_MAX + 1.0f);
        cloud->points[i].y = 1024.0f * rand() / (RAND_MAX + 1.0f);
        cloud->points[i].z = 1024.0f * rand() / (RAND_MAX + 1.0f);
    }

    // 创建KD树对象
    pcl::KdTreeFLANN<pcl::PointXYZ> kdtree;

    // 设置输入点云
    kdtree.setInputCloud(cloud);

    // 定义搜索点,并执行K近邻搜索
    pcl::PointXYZ searchPoint;
    searchPoint.x = 1024.0f * rand() / (RAND_MAX + 1.0f);
    searchPoint.y = 1024.0f * rand() / (RAND_MAX + 1.0f);
    searchPoint.z = 1024.0f * rand() / (RAND_MAX + 1.0f);

    // 存储搜索结果
    int K = 10;
    std::vector<int> pointIdxNKNSearch(K);
    std::vector<float> pointNKNSquaredDistance(K);

    // 执行最近K个最近邻搜索
    if (kdtree.nearestKSearch(searchPoint, K, pointIdxNKNSearch, pointNKNSquaredDistance) > 0) {
        for (size_t i = 0; i < pointIdxNKNSearch.size(); ++i) {
            std::cout << "Point " << i << ": " 
                      << cloud->points[pointIdxNKNSearch[i]].x << " "
                      << cloud->points[pointIdxNKNSearch[i]].y << " "
                      << cloud->points[pointIdxNKNSearch[i]].z << " (squared distance: "
                      << pointNKNSquaredDistance[i] << ")" << std::endl;
        }
    }

    return 0;
}

在PCL中,KD树的构建过程是自动的,开发者只需要设置输入点云即可。在实际应用中,KD树经常结合PCL库使用,在处理大规模点云数据时,会极大提高算法处理的效率。

4.KD树的优缺点

(1)优点

  • 构建速度快,时间复杂度为O(nlogn)。
  • 搜索效率高,对于平衡的KD树,搜索时间复杂度为O(logn)。

(2)缺点

  • 对于不平衡的KD树,搜索效率可能降低。
  • 空间复杂度较高,需要存储额外的节点信息。

5. 总结

经过上面的介绍,大家大概能了解到关于KD树的基本概念、特性、创建过程和最近邻搜索算法的实现。总的来说,KD树是一种强大的数据结构,能够高效地处理多维空间中的数据,是解决空间搜索问题的关键工具之一。KD树算法在机器学习和计算机视觉领域有着广泛的应用,特别是在处理大规模高维数据时,能够有效提高搜索效率。而在激光雷达点云处理方面,如点云聚类、地面分割和特征提取等方面都有KD树应用的身影。

  • 21
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
由于KD树构建需要考虑数据的维度和切分方式,因此具体步骤如下: 1. 定义节点结构体 首先,我们先定义一个节点的结构体,包括了节点的位置、分割维度和左右子节点。为了便于查找,我们还可以添加一个标识节点是否为叶子节点的字段。 ```c typedef struct node { double pos[3]; int splitDim; bool isLeaf; struct node *left, *right; }Node; ``` 2. 选择切分维度和位置 对于每个节点,我们需要选择一个切分维度和位置。一般采用以下两种方式之一: - 均匀采样 对于数据点中的所有维度,计算出其最大、最小值的范围,并对每个维度均匀采样若干个值。然后,对所有采样点分别计算分割维度和位置,选择能够使分割点左右数据量最平衡的点。 - 方差最大化 对于数据点中的所有维度,计算出其方差。选择方差最大的维度作为分割维度,然后选择该维度上的中位数作为分割点。 以下是基于均匀采样的实现方法: ```c int sample_num = 6; // 采样点数 int splitDim = -1; double splitPos = -1; double range[3][2]; // 存储每个维度的最大、最小值 for (int i = 0; i < 3; i++) { range[i][0] = INFINITY; range[i][1] = -INFINITY; } for (int i = l; i < r; i++) { for (int j = 0; j < 3; j++) { range[j][0] = fmin(range[j][0], arr[i].pos[j]); range[j][1] = fmax(range[j][1], arr[i].pos[j]); } } for (int i = 0; i < sample_num; i++) { double pos[3]; for (int j = 0; j < 3; j++) { pos[j] = range[j][0] + (double)i / (sample_num - 1) * (range[j][1] - range[j][0]); } int dim = i % 3; double sum1 = 0, sum2 = 0; for (int j = 0; j < r - l; j++) { sum1 += arr[l + j].pos[dim]; sum2 += arr[l + j].pos[dim] * arr[l + j].pos[dim]; } double mean = sum1 / (r - l); double var = sum2 / (r - l) - mean * mean; int left = 0, right = 0; for (int j = l; j < r; j++) { if (arr[j].pos[dim] < pos[dim]) left++; if (arr[j].pos[dim] > pos[dim]) right++; } if (fmin(left, right) < fmin(bestLeft, bestRight)) { bestLeft = left; bestRight = right; splitDim = dim; splitPos = pos[dim]; } } ``` 3. 递归构建KD树 在选择了分割维度和位置之后,我们就可以将数据分成左右两部分,并将该节点设为分割点。然后,递归地构建左右子节点。 为了使叶子内点的数量大于100,我们可以在每个节点上设一个阈值,当节点内的数据量小于该阈值时,将该节点标记为叶子节点。同时,如果数据量较小,也可以直接使用暴力搜索的方式来查找最近邻。 以下是递归构建KD树实现方法: ```c Node* buildKDTree(Point* arr, int l, int r, int depth) { if (r - l < THRESHOLD) { Node* node = new Node(); node->pos[0] = node->pos[1] = node->pos[2] = 0; for (int i = l; i < r; i++) { for (int j = 0; j < 3; j++) { node->pos[j] += arr[i].pos[j]; } } for (int j = 0; j < 3; j++) { node->pos[j] /= (r - l); } node->isLeaf = true; return node; } int splitDim = -1; double splitPos = -1; findSplitPos(arr, l, r, splitDim, splitPos); int mid = l + (r - l) / 2; std::nth_element(arr + l, arr + mid, arr + r, compare(splitDim)); Node* node = new Node(); node->pos[0] = arr[mid].pos[0]; node->pos[1] = arr[mid].pos[1]; node->pos[2] = arr[mid].pos[2]; node->splitDim = splitDim; node->isLeaf = false; node->left = buildKDTree(arr, l, mid, depth + 1); node->right = buildKDTree(arr, mid + 1, r, depth + 1); return node; } ``` 4. 近邻搜索 最后,我们还需要实现一个近邻搜索算法,用于根据查询点查找最近的数据点。 具体实现可以使用递归的方式,按照KD树的结构进行搜索。具体步骤如下: - 1. 从根节点开始,找到查询点在KD树中的位置。 - 2. 如果该位置是一个叶子节点,那么直接返回该节点的位置。 - 3. 否则,根据查询点与分割点的位置关系,确定应该往左子树还是右子树搜索。 - 4. 递归搜索左子树或右子树,并返回离查询点最近的数据点。 - 5. 对于每个子树,判断其是否和另一侧的子树可能存在更近的点。如果存在,则递归搜索另一侧的子树。 以下是实现该算法的核心代码: ```c void search(Node* node, Point* arr, Point& query, Point& nearest) { if (node == NULL) return; if (node->isLeaf) { for (int i = 0; i < THRESHOLD && node + i != NULL; i++) { double dist = distance(node[i].pos, query.pos); if (dist < distance(nearest.pos, query.pos)) { nearest.pos[0] = node[i].pos[0]; nearest.pos[1] = node[i].pos[1]; nearest.pos[2] = node[i].pos[2]; } } return; } int cur = node->splitDim; if (query.pos[cur] < node->pos[cur]) { search(node->left, arr, query, nearest); if (distance(node->pos, query.pos) < distance(nearest.pos, query.pos)) { search(node->right, arr, query, nearest); } } else { search(node->right, arr, query, nearest); if (distance(node->pos, query.pos) < distance(nearest.pos, query.pos)) { search(node->left, arr, query, nearest); } } } ``` 完整代码如下:
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

良辰与日月

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

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

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

打赏作者

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

抵扣说明:

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

余额充值