zoukankan      html  css  js  c++  java
  • 点云处理算法核心-八叉树

             一直想写一篇关于八叉树的博客,我的博客大概快一年也没更新了,当然这之间的原因跟疫情或多或少还是存在关系的,2020注定是让人压抑的一年,所以这一年也慢下了脚步。八叉树的重要性其实不用我再次强调了吧,它涉及到算法的方方面面吧,也是三维点云数据处理的一个重要基石,从显示到交互再到算法八叉树无不扮演着极其重要的角色。当然并不是任何算法都会涉及到八叉树,将三维点云数据处理成二维有时也是一种比较常见的处理手段,毕竟二维的邻域处理要比三维的邻域处理简单的多。对于八叉树的研究博主本人其实很早就提上了日程,也付诸了相应的行动,一直想写一篇博客分享这方面的知识,一方面它涉及到的篇幅较大,所以每次一想到要写很久就放弃了,另一方面受疫情的影响、工作忙碌以及自己的一些事情导致学习的热情也大不如以往,今年似乎大部分时间都在刷B站,这里表示惭愧。

             对于八叉树,我想大家首先可能接触到的就是pcl的八叉树的邻域搜索,短短几句代码就实现了查找邻域的功能,我一开始觉得还挺好用的,但是用久了,不免产生了一个疑问,八叉树的具体实现机制是什么,但是网上似乎很少有关于八叉树的精细讲解,见得最多就是那个八叉树的树状图,此时唯一可行的办法,那就只能硬着头皮上源码了。

           对于八叉树的印象,我想下面这个图大家见得是最多的。

                                                                               

                    typedef pcl::octree::OctreeLeafNode<OctreeContainerPointIndices> LeafNode;
                    typedef pcl::octree::OctreeBranchNode<OctreeContainerEmpty>BranchNode;

                   节点分为两种类型,根据英文名,博主姑且将其翻译为分支节点以及叶子节点,上图有文字标注,其中绿色的分支节点为空节点。就是我们常说的八叉树的子节点要么为8个,要么为0个,就是因为空节点的存在,空节点之所以存在就是因为其里面不存在数据点,这样也提高了遍历的效率,不然在满树的情况下,节点会多很多。

             一、第一部分分块功能

              第一步就是八叉树的分层,就是分几层,当然也可以指定八叉树叶子节点(就是盒子套盒子,那个最小的盒子)所对应包围盒的大小。当然分层的逻辑似乎更直观更好理解点。分层的思想其实也不难理解,大家可以理解为盒子里刚好填满8个盒子,直到设定的深度为止。举个例子,如果八叉树的深度为3,先将外包的大盒子分成八个,然后对这8个盒子又分别分成8个,然后再次分成8个。执行3次为止,当然空节点直接跳过。

                              

                           深度为1                                                                                  深度为2                                                                     深度为3    

                   可以从上图看到,单从z方向这个维度来看,深度每增加一层,在维度上就会一分为二。

                    二、第二部分邻域搜索

                                                                                                       

                    为了使得逻辑清晰点,博主在这里附上了一个流程图,其实就是一个迭代的过程,对待每个分支节点重复流程1的操作。

                    1.一点包围盒邻域的获取

                    对于邻域搜索的问题,这里先以一个易于理解的例子切入,我们求一点包围盒内的邻域点。求一点包围盒范围内的邻域点算法实现的思路,即从根节点开始判断,判断每一个根节点是否与所设置的包围盒有交集,如果有继续查找其子节点,同样判断其子节点与该包围盒是否存在交集,直到访问叶子节点,如果该叶子节点与所设置包围盒有交集,便获取该叶子节点里的索引执行判定条件3(对于每个节点的判断无论是分支节点还是叶子节点都会执行一个判定条件,即这里的判定条件1条件2,这里的条件1与条件2都是2个包围盒是否存在交集)。

                流程:

                a.这里我们设 预先设置的包围盒为boundingbox1(即我们所需获取的索引的包围盒),当前节点的包围盒为boundingbox2。

                b.判定boundingbox1与boundingbox2是否存在交集,即流程图里的判定条件1条件2(这里的条件是一致的)

                c.如果到了叶子节点,且满足b,那么则要执行判定条件3,即判定Pi是否在boundingbox1里

                              

              与包围盒有交集的叶子节点                                                  包围盒里的数据点                                                                          整体效果

                   2.一点半径邻域的获取

                   类似于这类思想,我们便可以求一点半径邻域内的数据点,所以可以先将球处理成包围盒子,唯一不同的是处理到叶子节点时,判定条件即为两点之间的点间距

                                 

                  与包围盒有交集的叶子节点                                                   包围盒里的数据点                                                                  整体效果

                3.一点K邻域的获取

               对于k近邻会相对比较复杂一些,因为并不知道那些点距离自己最近,而且这些点可能来自于周围的数个叶子节点,所以算法处理一开始只能通过访问到的第一个叶子节点,然后执行以下操作

    std::sort(point_candidates.begin(), point_candidates.end());
    if (point_candidates.size() > K)
    point_candidates.resize(K);
    if (point_candidates.size() == K)
    smallest_squared_dist = point_candidates.back().point_distance_;
    

      看这段代码就很直观了,升序排序,取到第K个值

           对于下一个节点就会以smallest_squared_dist作为一个判断依据,因为总不能所有节点都去访问吧

    float disThread = smallest_squared_dist + voxelSquaredDiameter / 4.0 + sqrt(smallest_squared_dist * voxelSquaredDiameter);
    if(search_heap.back().point_distance < disThread)
     search_heap.back().point_distance<disThread 满足此条件

            voxelSquaredDiameter 代表当前深度节点的包围盒的长度smallest_squared_dist代表上次叶子节点获取的邻域点里第K个距离值(已经升序排序了,如果没找到K个邻域,那么smallest_squared_dist的值为初始化的值,一般设置较大) 

       search_heap.back().point_distance 代表当前层次的根节点包围盒的中心点与搜索点的距离
      这个公式曾经在一篇论文看的过,具体是什么原理还真忘了,知道的小伙伴评论区留言赐教一番,感激不尽。
    当然自己也大概抽象的理解了一下,大概要满足以下这种形式:

            search_heap.back().point_distance - δ<smallest_squared_dist  =>  search_heap.back().point_distance<smallest_squared_dist + δ

            所以这个判定条件完全是可以自定义的,所以我也没去纠结这个问题了。亦步亦趋也不太好,还是得有一定的原创性。
           以往对待一个问题比较爱刨根问底,因为这篇博文在数个月前就琢磨再写了,可惜一拖再拖,今年整个精神状态都不在线,所以这里请允许我偷个懒(惭愧惭愧),这篇博文的插图有点多,所以感觉确实是有点棘手。我对于整个八叉树的理解也远没到细致入微的境界,所以很多细节的地方都得去看源码。
           在空闲的时间里,我尽量养成看源码的习惯,争取面面俱到做到有始有终,后续可能会陆续更新我对八叉树的一些理解。
           下面来一张插图,不过没什么代表性吧,就是一张很常规的K近邻搜索图。

          K近邻搜索结果

             三、第三部分一点所在叶子节点的父节点及其兄弟节点

              描述:获取一点所在的叶子节点,然后获取该叶子节点所在的父亲节点,并且获取该叶子节点的兄弟节点

               1.这里定义八叉树的最大深度为 maxDepth,查询点为Pt

               2.那么其父亲节点必定在maxDepth-1层,那么判定条件-----Pt在此层根节点的包围盒内。假设得到该父亲节点且命名为parentNode.

               3.获取parentNode的子节点,判断子节点与Pt的空间关系,包含关系的属于当前节点,否则属于其兄弟节点

               当然这个原理不是pcl库里面的,是博主自己的一个简单应用吧,逻辑也很简单,包括上文的半径内搜索,也是博主的一个简单应用,期待有更简单更高效的搜索方式的小伙伴们在评论区里共享一番,感激不尽

               下面贴上几个示意图吧

                       

        红色部分为该节点,剩下为其兄弟节点                                              白色为父节点                                                                       整体效果

                

                        到此这篇博文已经结束,属于千呼万唤始出来系列,差不多一年没更新博客了,其实更新博客的速度也代表自己当前的一种状态,不过对于知识技术的认可不应该在何种情况下去打折扣,所以写这篇博客的目的一方面分享一下我在八叉树方面的一些浅显的认知,另一方面也提醒自己知识技能如尊严般重要,毕竟是自己的安身立命之本,因为你永远不知道未来等待自己的会是什么,所以就得把主动权早早的掌握在自己手里。生活得靠自己而不是靠施舍,所以如我一般的草根们还是得清醒的认知这一点。对未来是否无所畏惧其实很大程度取决于当前自己迈出的每一步。

                   接下来我会继续更新一些知识,当然大部分都来自于一些开源库,例如pcl或者Cloudcompare,又或者meshlab这些。期待下一次能有大的更新,这段时间我会抽本来不多的休息时间去多看看源码跟大家分享相关知识。

  • 相关阅读:
    How to disable ipv6 in ubuntu
    git 暂存区问题
    linux 自动输入密码脚本避免密码确认
    【Linux学习简记 】数据流重定向<,<<,>,>>,2>,2>>
    【Gradle教程】Gradle 基础入门
    vsftpd配置匿名下载,登录上传
    Thunar左侧边栏不完全显示PLACES的解决
    【Jenkins系列教程】流水线通过SSH方式操作Git仓库
    害你加班的bug就是我写的,记一次升级Jenkins插件引发的加班
    Linux 临时终端连接无线网
  • 原文地址:https://www.cnblogs.com/z-web-2017/p/14111071.html
Copyright © 2011-2022 走看看