激光雷达定位与建图-最近邻问题2

一、问题引出

最近邻问题:假设有两个点云集合, χ 1 = { x 1 , ⋯ x n } \chi _{1} = \left \{ x_{1},\cdots x_{n} \right \} χ1={x1,xn} χ 2 = { x 1 , ⋯ x n } \chi _{2} = \left \{ x_{1},\cdots x_{n} \right \} χ2={x1,xn} ,求点云集合 χ 2 \chi _{2} χ2中某个点,在点云集合 χ 1 \chi _{1} χ1中与它最近的点是?或者在点云集合中与它最近的K个点是?

常用解决方法有:暴力最近邻法、栅格与体素方法、二叉树与k-d树、四叉树与八叉树和其它树类方法。

该篇文章主要介绍:二叉树与k-d树,当然k-d树是二叉树的高维版本,而点云属于三维空间,所以本文主要介绍k-d树。

二、k-d树

k-d树是二叉树的一种,任意一个k-d树的节点由左右两侧组成。

1.k-d树创建

在构建KD树时,我们通常有两种主要的方法来选择分割轴和分割点:
1.以固定顺序交替坐标值;
2.计算当前点云在各轴上的分散程度,并取分散程度最大的轴作为分割轴。
考虑到了点云数据的实际分布情况,本文选择第二种方式构建k-d树。
要按照计算当前点云在各轴上的分散程度,并取分散程度最大的轴作为分割轴的方法来构建KD树,我们可以遵循以下步骤:

a. 准备阶段
  • 输入数据:假设有点云数据集为 ( X = { x 1 , x 2 , . . . , x N } ) ( X = \{x_1, x_2, ... , x_N\} ) (X={x1,x2,...,xN})
b. 计算每个轴的分散程度
  • 对于每个维度(轴),计算数据点在该维度上的方差或其他分散程度的度量。方差越大,表示数据在该维度上的分散程度越高。
c. 选择分割轴
  • 选择维度:选择方差最大的维度作为分割轴。
d. 确定分割点
  • 在选定的分割轴上,找到中位数作为分割点。
e. 划分数据集
  • 根据分割点,将数据集划分为两个子集:
    • 左子集:所有在分割轴上小于分割点的点。
    • 右子集:所有在分割轴上大于等于分割点的点。
f. 递归构建子树
  • 对左右子集递归执行步骤2-5,直到每个子集中只有一个数据点或达到预设的终止条件。
g. 终止条件
  • 当某一子集为空或只包含一个数据点时,递归终止。
h. 构建完成
  • 所有递归调用完成后,KD树构建完成。

根据kd树构建步骤,给出代码示例如下:

/// Kd树节点,二叉树结构,内部用祼指针,对外一个root的shared_ptr
struct KdTreeNode {int id_ = -1;int point_idx_ = 0;            // 点的索引int axis_index_ = 0;           // 分割轴float split_thresh_ = 0.0;     // 分割位置KdTreeNode* left_ = nullptr;   // 左子树KdTreeNode* right_ = nullptr;  // 右子树bool IsLeaf() const { return left_ == nullptr && right_ == nullptr; }  // 是否为叶子
};bool KdTree::BuildTree(const CloudPtr &cloud) {if (cloud->empty()) {return false;}cloud_.clear();cloud_.resize(cloud->size());for (size_t i = 0; i < cloud->points.size(); ++i) {cloud_[i] = ToVec3f(cloud->points[i]);}Clear();Reset();IndexVec idx(cloud->size());for (int i = 0; i < cloud->points.size(); ++i) {idx[i] = i;}Insert(idx, root_.get());return true;
}void KdTree::Insert(const IndexVec &points, KdTreeNode *node) {nodes_.insert({node->id_, node});if (points.empty()) {return;}if (points.size() == 1) {size_++;node->point_idx_ = points[0];return;}IndexVec left, right;if (!FindSplitAxisAndThresh(points, node->axis_index_, node->split_thresh_, left, right)) {size_++;node->point_idx_ = points[0];return;}const auto create_if_not_empty = [&node, this](KdTreeNode *&new_node, const IndexVec &index) {if (!index.empty()) {new_node = new KdTreeNode;new_node->id_ = tree_node_id_++;Insert(index, new_node);}};create_if_not_empty(node->left_, left);create_if_not_empty(node->right_, right);
}bool KdTree::FindSplitAxisAndThresh(const IndexVec &point_idx, int &axis, float &th, IndexVec &left, IndexVec &right) {// 计算三个轴上的散布情况,我们使用math_utils.h里的函数Vec3f var;Vec3f mean;math::ComputeMeanAndCovDiag(point_idx, mean, var, [this](int idx) { return cloud_[idx]; });int max_i, max_j;var.maxCoeff(&max_i, &max_j);axis = max_i;th = mean[axis];for (const auto &idx : point_idx) {if (cloud_[idx][axis] < th) {// 中位数可能向左取整left.emplace_back(idx);} else {right.emplace_back(idx);}}// 边界情况检查:输入的points等于同一个值,上面的判定是>=号,所以都进了右侧// 这种情况不需要继续展开,直接将当前节点设为叶子就行if (point_idx.size() > 1 && (left.empty() || right.empty())) {return false;}return true;
}

示例代码是一个Kd树的构建过程的C++实现,其中包含了一些关键的函数和结构体。

结构体 KdTreeNode

这个结构体定义了Kd树中的一个节点,包含节点的ID、点的索引、分割轴、分割阈值以及指向左右子树的指针。IsLeaf函数用于判断一个节点是否为叶子节点。

KdTree

这个类包含了构建Kd树的方法。BuildTree函数是构建Kd树的入口点,它首先清空当前的树节点,并复制点云数据到内部存储。然后,它调用Insert函数来递归地构建树。

函数 BuildTree

这个函数接收一个点云的智能指针cloud,如果点云为空,则返回false。它将点云数据复制到内部存储cloud_,然后清空树并重置节点ID。之后,它使用Insert函数开始构建树。

函数 Insert

这个函数递归地构建Kd树。如果传入的点集为空,它直接返回。如果点集中只有一个点,它将该点设置为当前节点的索引。如果点集大小大于1,它调用FindSplitAxisAndThresh函数来找到最佳的分割轴和阈值,然后递归地在左右子树上构建树。

函数 FindSplitAxisAndThresh

这个函数计算三个轴上的分散情况,并选择方差最大的轴作为分割轴。它还计算了该轴上的中位数作为分割阈值,并将点集分为左右两个子集。如果所有点都在分割阈值的同一侧,或者点集只有一个点,它返回false,表示不需要进一步分割。

2.k-d树k近邻查找

基于您提供的步骤,以下是KD树k近邻查找的详细算法描述:

a. 输入
  • KD树 ( T ),查找点 ( x ),最近邻数 ( k )。
b. 输出
  • k近邻集合 ( N )。
c. 初始化
  • 设置当前节点 ( n_c ) 为根节点。
  • 定义一个优先队列 ( PQ ) 来保存可能的最近邻点,按照它们到查找点 ( x ) 的距离排序。
  • 初始化一个集合 ( N ) 来保存最终的k个最近邻点。
d. 递归搜索函数 ( S(n_c) )
  • 计算距离:计算查找点 ( x ) 到当前节点 ( n_c ) 的距离 ( d(n_c, x) )。
  • 更新优先队列:如果 ( PQ ) 的大小小于 ( k ),将 ( n_c ) 添加到 ( PQ ) 中。如果 ( PQ ) 的大小等于 ( k ) 并且 ( d(n_c, x) ) 小于 ( PQ ) 中最大距离,替换 ( PQ ) 中的最大距离点。
  • 分割轴判断:如果 ( n_c ) 不是叶子节点,根据 ( x ) 在分割轴上的值决定搜索哪个子树。
    • 选择子树:如果 ( x ) 在分割轴上的值小于 ( n_c ) 的分割值,先搜索左子树,然后搜索右子树。否则,先搜索右子树,然后搜索左子树。
    • 递归搜索:对选定的子树递归调用 ( S ) 函数。
    • 回溯搜索:在回溯到 ( n_c ) 后,检查另一侧子树是否有可能包含更近的点。这可以通过比较 ( x ) 到分割轴的距离与 ( PQ ) 中最大距离来判断。如果 ( x ) 到分割轴的距离小于 ( PQ ) 中最大距离,递归搜索另一侧子树,否则不对右侧子树进行展开。
e. 处理叶子节点
  • 当 ( n_c ) 是叶子节点时,计算 ( x ) 到 ( n_c ) 代表的点的距离。
  • 如果这个距离小于 ( PQ ) 中最大距离,将 ( n_c ) 添加到 ( PQ ) 中并更新 ( N )。
f. 完成搜索
  • 当所有节点都被访问后,将 ( PQ ) 中的点复制到 ( N ) 中。

当k = 1即为最近邻搜索,根据上述步骤给出代码示例如下:

/// 用于记录knn结果
struct NodeAndDistance {NodeAndDistance(KdTreeNode* node, float dis2) : node_(node), distance2_(dis2) {}KdTreeNode* node_ = nullptr;float distance2_ = 0;  // 平方距离,用于比较bool operator<(const NodeAndDistance& other) const { return distance2_ < other.distance2_; }
};bool KdTree::GetClosestPoint(const PointType &pt, std::vector<int> &closest_idx, int k) {if (k > size_) {LOG(ERROR) << "cannot set k larger than cloud size: " << k << ", " << size_;return false;}k_ = k;std::priority_queue<NodeAndDistance> knn_result;Knn(ToVec3f(pt), root_.get(), knn_result);// 排序并返回结果closest_idx.resize(knn_result.size());for (int i = closest_idx.size() - 1; i >= 0; --i) {// 倒序插入closest_idx[i] = knn_result.top().node_->point_idx_;knn_result.pop();}return true;
}void KdTree::Knn(const Vec3f &pt, KdTreeNode *node, std::priority_queue<NodeAndDistance> &knn_result) const {if (node->IsLeaf()) {// 如果是叶子,检查叶子是否能插入ComputeDisForLeaf(pt, node, knn_result);return;}// 看pt落在左还是右,优先搜索pt所在的子树// 然后再看另一侧子树是否需要搜索KdTreeNode *this_side, *that_side;if (pt[node->axis_index_] < node->split_thresh_) {this_side = node->left_;that_side = node->right_;} else {this_side = node->right_;that_side = node->left_;}Knn(pt, this_side, knn_result);if (NeedExpand(pt, node, knn_result)) {  // 注意这里是跟自己比Knn(pt, that_side, knn_result);}
}void KdTree::ComputeDisForLeaf(const Vec3f &pt, KdTreeNode *node,std::priority_queue<NodeAndDistance> &knn_result) const {// 比较与结果队列的差异,如果优于最远距离,则插入float dis2 = Dis2(pt, cloud_[node->point_idx_]);if (knn_result.size() < k_) {// results 不足kknn_result.emplace(node, dis2);} else {// results等于k,比较current与max_dis_iter之间的差异if (dis2 < knn_result.top().distance2_) {knn_result.emplace(node, dis2);knn_result.pop();}}
}bool KdTree::NeedExpand(const Vec3f &pt, KdTreeNode *node, std::priority_queue<NodeAndDistance> &knn_result) const {if (knn_result.size() < k_) {return true;}if (approximate_) {float d = pt[node->axis_index_] - node->split_thresh_;if ((d * d) < knn_result.top().distance2_ * alpha_) {return true;} else {return false;}} else {// 检测切面距离,看是否有比现在更小的float d = pt[node->axis_index_] - node->split_thresh_;if ((d * d) < knn_result.top().distance2_) {return true;} else {return false;}}
}

代码段主要组成部分和功能如下:

结构体 NodeAndDistance

这个结构体用于存储KD树节点和对应的平方距离。它重载了小于运算符,以便可以在优先队列中使用。

函数 GetClosestPoint

这个函数是k近邻查找的入口点,它接收一个点pt和一个整数k,返回k个最近邻的索引。它首先检查k是否大于点云的大小,然后调用Knn函数进行查找,并将结果存储在优先队列knn_result中。最后,它将优先队列中的结果复制到closest_idx向量中,并返回结果。

函数 Knn

这是一个递归函数,用于在KD树中查找与查询点pt最近的点。它首先检查当前节点是否为叶子节点,如果是,则调用ComputeDisForLeaf函数。如果不是叶子节点,它根据查询点在分割轴上的值决定搜索哪个子树,并在必要时搜索另一侧子树。

函数 ComputeDisForLeaf

这个函数用于处理叶子节点,计算查询点与叶子节点代表的点的距离,并将其与优先队列中的最远距离进行比较。如果优先队列的大小小于k或者新计算的距离小于优先队列中的最大距离,则将新点添加到优先队列中。

函数 NeedExpand

这个函数用于决定是否需要搜索当前节点的另一侧子树。它基于查询点到分割平面的距离与优先队列中最大距离的比较。如果查询点到分割平面的距离小于优先队列中最大距离,并且考虑到一定的容差(如果启用了近似搜索),则搜索另一侧子树。

代码中的一些关键点:
  • approximate_alpha_:这些变量用于控制近似搜索,alpha_是一个容差参数,用于确定是否需要搜索另一侧子树。

3. 关于approximate_alpha_的解释

通过k-d树最近邻查找过程,显而易见k-d树最近邻算法的关键部分是剪枝,而剪枝成立的条件是树形结构另一侧不存在比现有结果更近的最近邻。假设当前最远的最近邻为 d m a x d_{max} dmax,分割平面的距离为 d s p l i t d_{split} dsplit。故剪枝条件可记为 d m a x > d s p l i t d_{max} > d_{split} dmax>dsplit

但如果当前找到的最近邻很差,那么可能要去远处的分枝上继续查找一个可能存在的最近邻。为了避免这种情况,添加一个比例因子alpha_,当alpha_设置小于1时,整个k近邻的查找就加快了,但不再保证能找到严格的最近邻,将这种方法成为近似最近邻approximate_

从表现形势上看,k-d树通过设置alpha_参数,可以在性能和表现上(最近邻准确率和召回率)取得一定的平衡。

三、参考

<<自动驾驶与机器人中的SLAM技术从理论到实践>>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/62403.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

redis中的哨兵

redis中的哨兵 一、哨兵机制的概念二、redis哨兵的部署2.1 docker的安装2.2 编排redis主从节点2.3 配置哨兵节点 三、redis哨兵的选举机制3.1 redis-master宕机之后的情况3.2 重启redis-master后的情况 四、redis哨兵机制的原理4.1主观下线4.2客观下线4.3选举leader节点4.4选出…

如何在 IIS 上部署 .NET Core 应用程序 ?

在 Internet 信息服务 (IIS) 上部署 .NET Core 应用程序起初可能看起来令人生畏&#xff0c;但只要步骤正确&#xff0c;它就是一个简单的过程。本指南将引导您在 IIS 上部署 .NET Core 应用程序。 Step 1: 安装 .NET Core Hosting Bundle (1) 前往官方下载页面 .NET downloa…

蓝桥杯每日真题 - 第24天

题目&#xff1a;&#xff08;货物摆放&#xff09; 题目描述&#xff08;12届 C&C B组D题&#xff09; 解题思路&#xff1a; 这道题的核心是求因数以及枚举验证。具体步骤如下&#xff1a; 因数分解&#xff1a; 通过逐一尝试小于等于的数&#xff0c;找到 n 的所有因数…

【前端】Next.js 服务器端渲染(SSR)与客户端渲染(CSR)的最佳实践

关于Next.js 服务器端渲染&#xff08;SSR&#xff09;与客户端渲染&#xff08;CSR&#xff09;的实践内容方面&#xff0c;我们按下面几点进行阐述。 1. 原理 服务器端渲染 (SSR): 在服务器上生成完整的HTML页面&#xff0c;然后发送给客户端。这使得用户在首次访问时能够…

【机器学习】机器学习的基本分类-监督学习-逻辑回归-对数似然损失函数(Log-Likelihood Loss Function)

对数似然损失函数&#xff08;Log-Likelihood Loss Function&#xff09; 对数似然损失函数是机器学习和统计学中广泛使用的一种损失函数&#xff0c;特别是在分类问题&#xff08;例如逻辑回归、神经网络&#xff09;中应用最为广泛。它基于最大似然估计原理&#xff0c;通过…

【Qt】QDateTimeEdit控件实现清空(不保留默认时间/最小时间)

一、QDateTimeEdit控件 QDateTimeEdit 提供了一个用于编辑日期和时间的控件。用户可以通过键盘或使用上下箭头键来增加或减少日期和时间值。日期和时间的显示格式根据设置的格式显示&#xff0c;可以通过 setDisplayFormat() 方法来设置。 二、如何清空 我在使用的时候&#…

基于BERT的语义分析实现

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…

操作系统存储器相关习题

1 为什么要配置层次式存储器? 设置多个存储器可以使存储器两端的硬件能并行工作&#xff1b; 采用多级存储系统特别是Cache技术&#xff0c;是减轻存储器带宽对系统性能影响的最佳结构方案&#xff1b; 在微处理机内部设置各种缓冲存储器&#xff0c;减轻对存储器存取的压力。…

HarmonyOS NEXT应用开发,关于useNormalizedOHMUrl选项的坑

起因是这样的&#xff1a;我这库打包发布出问题了&#xff0c;这个有遇到的吗&#xff1f; 源码里面就没有 request .d.ts,这打包后哪来个这文件&#xff1f;且漏掉了其他文件。 猫哥csdn.yyz_1987 为啥我打包的har里面&#xff0c;只有接口&#xff0c;没有具体实现呢&#x…

单点登录原理

允许跨域–>单点登录。 例如https://www.jd.com/ 同一个浏览器下&#xff1a;通过登录页面产生的cookie里的一个随机字符串的标识&#xff0c;在其他子域名下访问共享cookie获取标识进行单点登录&#xff0c;如果没有该标识则返回登录页进行登录。 在hosts文件下面做的域名…

基于Java的小程序电商商城开源设计源码

近年来电商模式的发展越来越成熟&#xff0c;基于 Java 开发的小程序电商商城开源源码&#xff0c;为众多开发者和企业提供了构建个性化电商平台的有力工具。 基于Java的电子商城购物平台小程序的设计在手机上运行&#xff0c;可以实现管理员&#xff1b;首页、个人中心、用户…

Linux查看网络基础命令

文章目录 Linux网络基础命令1. ifconfig 和 ip一、ifconfig命令二、ip命令 2. ss命令一、基本用法二、常用选项三、输出信息四、使用示例 3. sar 命令一、使用sar查看网络使用情况 4. ping 命令一、基本用法二、常用选项三、输出结果四、使用示例 Linux网络基础命令 1. ifconf…

程序设计 26种设计模式,如何分类?

1. 创建型模式 (Creational Patterns) 这些模式关注如何实例化对象。它们通过各种方式封装对象的创建过程&#xff0c;从而提供灵活性和可扩展性。 单例模式 (Singleton)&#xff1a;确保某个类只有一个实例&#xff0c;并提供全局访问点。工厂方法模式 (Factory Method)&…

右值引用和移动语义:

C 右值引用和移动语义详解 在 C 的发展历程中&#xff0c;右值引用和移动语义的引入带来了显著的性能提升和编程灵活性。本文将深入探讨右值引用和移动语义的概念、用法以及重要性。 一、引言 C 作为一门高效的编程语言&#xff0c;一直在不断演进以满足现代软件编程的需求。…

图形渲染性能优化

variable rate shading conditional render 设置可见性等&#xff0c; 不需要重新build command buffer indirect draw glMultiDraw* - 直接支持多次绘制glMultiDrawIndirect - 间接多次绘制multithreading 多线程录制 实例化渲染 lod texture array 小对象剔除 投影到…

SpringMVC工作原理【流程图+文字详解SpringMVC工作原理】

SpringMVC工作原理 前端控制器&#xff1a;DispactherServlet处理器映射器&#xff1a;HandlerMapping处理器适配器&#xff1a;HandlerAdapter处理器&#xff1a;Handler&#xff0c;视图解析器&#xff1a;ViewResolver视图&#xff1a;View 首先用户通过浏览器发起HTTP请求…

12寸先进封装设备之-晶圆减薄一体机

晶圆减薄一体机在先进封装厂中的主要作用是对已完成功能的晶圆(主要是硅晶片)的背面基体材料进行磨削,去掉一定厚度的材料,以满足后续封装工艺的要求以及芯片的物理强度、散热性和尺寸要求。随着3D封装技术的发展,晶圆厚度需要减薄至50-100μm甚至更薄,以实现更好的散热效…

CTF之WEB(php弱类型绕过)

PHP 的弱类型特性有时会导致意外的行为&#xff0c;特别是在类型比较时。这些特性可以被利用来绕过一些预期的安全检查。以下是一些常见的 PHP 弱类型绕过技巧及其解释&#xff1a; 类型介绍 1. 类型比较 ( vs ) 在 PHP 中&#xff0c; 是松散比较&#xff0c;而 是严格比较…

【mysql】字段区分大小写,设置字符集SET utf8mb4 COLLATE utf8mb4_bin

1. 背景 由于 varchar(100) 不区分字段大小写 2. 解决办法 SET utf8mb4 COLLATE utf8mb4_bin 需要设置字符集就可以实现区分大小写

Online Judge——【前端项目初始化】项目通用布局开发及初始化

目录 一、新建layouts二、更新App.vue文件三、选择一个布局&#xff08;Layout&#xff09;四、通用菜单Menu的实现菜单路由改为读取路由文件 五、绑定跳转事件六、同步路由到菜单项 一、新建layouts 这里新建一个专门存放布局的布局文件layouts&#xff1a; 然后在该文件夹&…