地面分割--Fast Segmentation of 3D Point Clouds for Ground Vehicles论文阅读与源码分析

news2024/12/22 22:21:06

文章目录

  • 1写在前面的话
  • 2点云投影分块
  • 3地面点云分割
  • 4核心代码阅读
    • 投影分块
    • 直线拟合代码
    • 分割地面点云
  • 5实验效果
  • 参考

1写在前面的话

这篇文章属于地面分割领域非常经典的一篇论文,论文具有速度快,在一定程度能适应有坡度的地形,文章主要分为两个阶段:数据投影分块,在分块中寻找地面点。

2点云投影分块

我们首先将点云投影到xy平面,并将其划分为一定数量的点云块,以此来实现地面分割,如图1所示。我们引入了参数 Δ a \Delta a Δa,该参数描述了每个分割块的角度。因此,我们得出了 M = 2 π / Δ a M=2\pi/Δa M=2πa个分割块。分割块对应的原始点云索引用segment(pi)表示,很容易计算:
segment ⁡ ( p i ) = atan ⁡ 2 ( y i , x i ) Δ α \operatorname{segment}\left(p_i\right)=\frac{\operatorname{atan} 2\left(y_i, x_i\right)}{\Delta \alpha} segment(pi)=Δαatan2(yi,xi)
atan ⁡ 2 ( y i , x i ) \operatorname{atan} 2\left(y_i, x_i\right) atan2(yi,xi)表示点云与原点构成的线段与x正方向的夹角,其值在 [ 0 , 2 π ] [0,2\pi] [0,2π]之间。其过程如下图:
在这里插入图片描述

我们将segment(pi)对应的3d点云集合表示为:
P s = { p i ∈ P ∣ segment ⁡ ( p i ) = s } P_s=\left\{p_i \in P \mid \operatorname{segment}\left(p_i\right)=s\right\} Ps={piPsegment(pi)=s}
这里的 P s P_s Ps是通过分块的索引对应的原始点云为3d点云,而不是2d点云。
为了获得适合于地面平面估计,我们将同一分割块Ps中的所有点云继续划分为许多 b i n j s , j = 1... bin^s_j,j=1... binjs,j=1...。我们用 r m i n j r^min_j rminj r m a x j r^max_j rmaxj来表示某一bin覆盖的最小和最大距离。然后如果某一点云满足 r j min ⁡ ≤ x i 2 + y i 2 < r j max ⁡ r_j^{\min } \leq \sqrt{x_i^2+y_i^2}<r_j^{\max } rjminxi2+yi2 <rjmax,将其加入到该bin中,直至划分为整个分割块。如下图:
在这里插入图片描述

对于一个bin中的点云,我们重新定义一个二维集合:
P b j s ′ = { p i ′ = ( x i 2 + y i 2 z i ) T ∣ p i ∈ P b j s } P_{b_j^s}^{\prime}=\left\{p_i^{\prime}=\left(\sqrt{x_i^2+y_i^2} \quad z_i\right)^T \mid p_i \in P_{b_j^s}\right\} Pbjs={pi=(xi2+yi2 zi)TpiPbjs}
一般而言每个bin( P b j s ′ P_{b_j^s}^{\prime} Pbjs)中包含很多点,只取一个点表示bin,这个点称为prototype point, 记为 p b j s ′ p_{b_j^s}^{\prime} pbjs,本文取得z值最小的点。选择prototype point能够简化3d点云提取地面点提取过程,更重要的是,点云地面提取过程和点云数量无关,只和Δa以及bin的数量有关。

3地面点云分割

通过对每个分割块中的prototype points拟合直线提取地面点。这里采用文献[11]所描述的增量式算法。对于直线 y = m x + b y=mx+b y=mx+b,当其满足以下几个条件时,考虑其是地面平面的一部分:

  • 1 直线的斜率m不超过阈值 T m T_m Tm,即地面不可能是完全水平的。
  • 2 对于小斜率的直线,即$ m<T_{m_{small}} 时,截距 时,截距 时,截距b 不能超过阈值 不能超过阈值 不能超过阈值T_b 。因为如果 。因为如果 。因为如果b$太大,拟合出来的平面就不在地面上了。
  • 3 拟合的直线误差不能超过阈值 T r m s e T_rmse Trmse
  • 4 当前直线的起始点,距离上一条拟合出来的直线的距离,不能超过阈值,确保两条直线之间是平滑连接的。
    下图为拟合直线的具体方法:
    在这里插入图片描述

通过上述方法对于每个分割块,我们能得到很多直线方程,其集合记为 L s = ( m i , b i ) L_s={(m_i,b_i)} Ls=(mi,bi). L s L_s Ls表示分割块的ground plane。现在我们可以通过 L s L_s Ls判断点云是否属于地面点。在分割块的每个点云中计算点云到所有直线端点的距离:
(1)如果该点云到最近的直线端点距离都很大,说明点云距离直线很远,此时采用保守的方法判断该点是否是地面点。
(1)否则,如果点到直线的距离小于阈值,则判断为地面点。

4核心代码阅读

投影分块

//start_index,end_index当前segment中点云起始和终止索引
//cloud所有3d点云
void GroundSegmentation::insertionThread(const PointCloud& cloud,
                                         const size_t start_index,
                                         const size_t end_index) 
{
  const double segment_step = 2*M_PI / params_.n_segments;//计算Δa
  //根据参数指定的每个分割块最大距离和最小距离、bin的数目,计算bin的步长
  const double bin_step = (sqrt(params_.r_max_square) - sqrt(params_.r_min_square))
      / params_.n_bins;
  const double r_min = sqrt(params_.r_min_square);

  for (unsigned int i = start_index; i < end_index; ++i) 
  {
      //计算平面距离range
    pcl::PointXYZ point(cloud[i]);
    const double range_square = point.x * point.x + point.y * point.y;
    const double range = sqrt(range_square);
    //如果range在指定的分割块(segment)的范围内再进行别的计算
    if (range_square < params_.r_max_square && range_square > params_.r_min_square) 
    {
        //计算平面夹角
      const double angle = std::atan2(point.y, point.x);
      //计算当前点云所有哪个bin
      const unsigned int bin_index = (range - r_min) / bin_step;
      //计算当前点云所有哪个分割块
      const unsigned int segment_index = (angle + M_PI) / segment_step;
      //防止越界
      const unsigned int segment_index_clamped = segment_index == params_.n_segments ? 0 : segment_index;
     //把这个点云放到对应的分割块的对应的bin中,这里addPoint存的是最低点
      segments_[segment_index_clamped][bin_index].addPoint(range, point.z);
      //记录该点云所在的分割块及bin
      bin_index_[i] = std::make_pair(segment_index_clamped, bin_index);
    }
    //如果range不在指定的分割块(segment)的范围内不计算
    else 
    {
      bin_index_[i] = std::make_pair<int, int>(-1, -1);
    }
    //构建二维的集合,存放平面距离和z值
    segment_coordinates_[i] = Bin::MinZPoint(range, point.z);
  }
}

直线拟合代码


//增量式拟合直线,输出为lines_:保存直线的两个端点(d,z)
void Segment::fitSegmentLines() 
{
  // Find first point.bins_:当前分割块segment对应的所有二维集合(d,z),每个bin只有一个点
  auto line_start = bins_.begin();
  while (!line_start->hasPoint()) {
    ++line_start;
    // Stop if we reached last point.
    if (line_start == bins_.end()) return;
  }
  // Fill lines.
  bool is_long_line = false;
  double cur_ground_height = -sensor_height_;//地面高度
  //当前直线集合中的所有二维点
  std::list<Bin::MinZPoint> current_line_points(1, line_start->getMinZPoint());
  LocalLine cur_line = std::make_pair(0,0);
  //从第二个bin开始遍历分割块的所有bin
  for (auto line_iter = line_start+1; line_iter != bins_.end(); ++line_iter)
  {
    if (line_iter->hasPoint()) 
    {
        //找到该bin的二维点,d,z
      Bin::MinZPoint cur_point = line_iter->getMinZPoint();
      //计算当前bin与上个bin的d之差,如果大于阈值,则认为是一个长直线
      if (cur_point.d - current_line_points.back().d > long_threshold_)
      {
          is_long_line = true;
      }
      //如果当前直线多于两个点
      if (current_line_points.size() >= 2)
      {
        // Get expected z value to possibly reject far away points.
        double expected_z = std::numeric_limits<double>::max();
        if (is_long_line && current_line_points.size() > 2) 
        {
          expected_z = cur_line.first * cur_point.d + cur_line.second;
        }
        //存放bin(二维点)
        current_line_points.push_back(cur_point);
        //利用所有点去拟合直线,返回斜率m和截距b
        cur_line = fitLocalLine(current_line_points);
        //计算点到直线的最大距离(误差)
        const double error = getMaxError(current_line_points, cur_line);
        // Check if not a good line.
        //如果不是一个符合条件的直线
        if (error > max_error_ ||
            std::fabs(cur_line.first) > max_slope_ ||
            (current_line_points.size() > 2 && std::fabs(cur_line.first) < min_slope_) ||
            is_long_line && std::fabs(expected_z - cur_point.z) > max_long_height_) 
        {
          // Add line until previous point as ground.
            //删除刚刚放进来的点
          current_line_points.pop_back();
          //这里是干嘛的??
          // Don't let lines with 2 base points through.
          if (current_line_points.size() >= 3)
          {
              //删除当前bin之后重新拟合直线
            const LocalLine new_line = fitLocalLine(current_line_points);
            //重新拟合的直线也不一定是好的,为什么还pushback?
            lines_.push_back(localLineToLine(new_line, current_line_points));
            cur_ground_height = new_line.first * current_line_points.back().d + new_line.second;
          }
          // Start new line.
          is_long_line = false;
          //清空当前直线所有bin
          current_line_points.erase(current_line_points.begin(), --current_line_points.end());
         //回到上个bin
          --line_iter;
        }
        // Good line, continue.
        else { }
      }
      //如果点云数量少于2,添加点
      else 
      {
        // Not enough points.
          //添加的点要求距离不能太远,并且不能距离地面太远
        if (cur_point.d - current_line_points.back().d < long_threshold_ &&
            std::fabs(current_line_points.back().z - cur_ground_height) < std::abs(max_start_height_)) 
        {
          // Add point if valid.
          current_line_points.push_back(cur_point);
        }
        else 
        {
          // Start new line.
          current_line_points.clear();
          current_line_points.push_back(cur_point);
        }
      }
    }
  }
  // Add last line.
  //如果bin没有点云则中断该次直线拟合
  if (current_line_points.size() > 2) {
    const LocalLine new_line = fitLocalLine(current_line_points);
    lines_.push_back(localLineToLine(new_line, current_line_points));
  }
}

//计算直线的端点,这里的端点是通过直线方程计算得到的理论值,不是点云的实际值
Segment::Line Segment::localLineToLine(const LocalLine& local_line,const std::list<Bin::MinZPoint>& line_points) 
{
  Line line;
  const double first_d = line_points.front().d;
  const double second_d = line_points.back().d;
  const double first_z = local_line.first * first_d + local_line.second;
  const double second_z = local_line.first * second_d + local_line.second;
  line.first.z = first_z;
  line.first.d = first_d;
  line.second.z = second_z;
  line.second.d = second_d;
  return line;
}

分割地面点云

void GroundSegmentation::assignClusterThread(const unsigned int &start_index,
                                             const unsigned int &end_index,
                                             std::vector<int> *segmentation) 
{
  const double segment_step = 2*M_PI/params_.n_segments;
  for (unsigned int i = start_index; i < end_index; ++i) 
  {
      //segment_coordinates_存放的是所有点的二维坐标(在投影分块过程中已经进行处理了)
    //找到点云对应的二维坐标(d,z)
      Bin::MinZPoint point_2d = segment_coordinates_[i];
      //找到点云所在的分块segment的索引
    const int segment_index = bin_index_[i].first;
    if (segment_index >= 0) 
    {
        //计算点云到所在的分块segment的直线的距离
      double dist = segments_[segment_index].verticalDistanceToLine(point_2d.d, point_2d.z);
      // Search neighboring segments.
      int steps = 1;
      while (dist < 0 && steps * segment_step < params_.line_search_angle) {
        // Fix indices that are out of bounds.
        int index_1 = segment_index + steps;
        while (index_1 >= params_.n_segments) index_1 -= params_.n_segments;
        int index_2 = segment_index - steps;
        while (index_2 < 0) index_2 += params_.n_segments;
        // Get distance to neighboring lines.
         //计算点云到所在的分块segment的相邻两个segment的直线的距离(这里和论文稍有不同)
        const double dist_1 = segments_[index_1].verticalDistanceToLine(point_2d.d, point_2d.z);
        const double dist_2 = segments_[index_2].verticalDistanceToLine(point_2d.d, point_2d.z);
        //经过上述计算一共有3个dist,取最小的dist
        if (dist_1 >= 0) {
          dist = dist_1;
        }
        if (dist_2 >= 0) {
          // Select smaller distance if both segments return a valid distance.
          if (dist < 0 || dist_2 < dist) {
            dist = dist_2;
          }
        }
        ++steps;
      }
      //距离小于阈值则判定为地面点
      if (dist < params_.max_dist_to_line && dist != -1) {
        segmentation->at(i) = 1;
      }
    }
  }
}


//计算到最近的直线的距离,这里利用直线拟合(void Segment::fitSegmentLines())的结果line_变量
double Segment::verticalDistanceToLine(const double &d, const double &z) {
  static const double kMargin = 0.1;
  double distance = -1;
  //这里写的有点疑问
  for (auto it = lines_.begin(); it != lines_.end(); ++it) 
  {
    if (it->first.d - kMargin < d && it->second.d + kMargin > d) 
    {
      const double delta_z = it->second.z - it->first.z;
      const double delta_d = it->second.d - it->first.d;
      const double expected_z = (d - it->first.d)/delta_d *delta_z + it->first.z;
      distance = std::fabs(z - expected_z);
    }
  }
  return distance;
}

5实验效果

原始点云:(存在一定坡度)
在这里插入图片描述

地面分割:(对坡度适用性不是很好)

参考

原文:《Fast Segmentation of 3D Point Clouds for Ground Vehicles》
github
博客1
2
3
4

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

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

相关文章

学习使用ansible自动化运维工具

目录 一、虚拟机环境 二、yum方式部署 三、ansible使用 &#xff08;一&#xff09;将ansible服务器上文件分发给各节点 1. 创建一个要复制的文件&#xff0c;并复制到Ansible管理主机上 2.编辑Ansible的playbook文件&#xff0c;将copy模块添加到任务列表中 3. 运行play…

【c++迭代器模拟实现】

目录&#xff1a; 前言一、STL初始二、六大组件之迭代器迭代器初始迭代器的模拟实现&#xff08;1&#xff09;victor正向迭代器反向迭代器1反向迭代器2反向迭代器3 &#xff08;2&#xff09;list正向迭代器反向迭代器 总结 前言 打怪升级&#xff1a;第52天 一、STL初始 什…

和chatgpt一样的大模型LLaMA可以运行在pc上?

未来已来,大模型依据压缩模型的方式,可以在普通的PC上运行. LLaMA Facebook的LLaMA 模型和Georgi Gerganov 的llama.cpp的结合。 LLaMA&#xff0c;这是一组包含 7B 到 65B 参数的基础语言模型。我们在数万亿个令牌上训练我们的模型&#xff0c;并表明可以仅使用公开可用的数…

【Android入门到项目实战-- 9.1】—— 传感器的使用教程

目录 传感器的定义 三大类型传感器 1、运动传感器 2、环境传感器 3、位置传感器 传感器开发框架 1、SensorManager 2、Sensor 3、SensorEvent 4、SensorEventListener 一、使用传感器开发步骤 1、获取传感器信息 1)、获取传感器管理器 2)、获取设备的传感器对象列…

Java红黑树

概述 红黑树是一种自平衡的二叉查找树&#xff0c;是计算机科学中用到的一种数据结构。1972年出现的&#xff0c;当时被称之为平衡二叉B树。在1978年被修改为红黑树。红黑树是一种特殊的二叉查找树&#xff0c;红黑树上的每一个节点都有存储位表示节点的颜色。每一个节点可以是…

Java枚举:为什么它是单例模式的最佳选择?

前言 单例模式&#xff0c;是工作中比较常见的一种设计模式&#xff0c;通常有两种实现方式&#xff0c;懒汉式和饿汉式。但是这两种实现方式存在一些问题。懒汉式需要在多线程环境下使用同步锁机制来保证只有一个实例被创建&#xff0c;这会影响程序的性能。而饿汉式在类加载时…

《发展心理学——儿童与青少年》读书笔记

这是我读的第一本关于育儿教育类的书&#xff0c;该书的作者是David R. Shaffer&#xff0c;由北京师范大学博士生导师邹泓审校&#xff0c;由其底下的博士生们翻译。我看的是中文第九版。下面是我在阅读此书时做的关键摘录和部分感想&#xff1a; 第1章 导论:发展心理学及其研…

Java基础(二十一):集合源码

Java基础系列文章 Java基础(一)&#xff1a;语言概述 Java基础(二)&#xff1a;原码、反码、补码及进制之间的运算 Java基础(三)&#xff1a;数据类型与进制 Java基础(四)&#xff1a;逻辑运算符和位运算符 Java基础(五)&#xff1a;流程控制语句 Java基础(六)&#xff1…

耗时2.5h含泪打造windows10家庭版docker安装

文章目录 一、事出有因二、安装流程Problem1Problem2Problem3 三、胜利的曙光 一、事出有因 由于最近需要跑通github上的一个代码&#xff0c;那个github上的代码需要通过docker部署到本地&#xff0c;但是我的电脑上并没有docker,真的是含泪历时2.5h才把docker在我的windows电…

【Python成长之路】基于Flask-admin库,结合html+vue,实现前后端数据传递

一、前言 前面已经做了Flask-admin库的基本介绍和几个库常用功能如何使用&#xff0c;若不了解请移步到以下博客&#xff1a; 1、?《【Python成长之路】基于Flask-admin库&#xff0c;编写个人工作平台代码详述》 2、?《【Python成长之路】基于Flask-admin库&#xff0c;编…

DP练习题

1.减操作(ACWING) 若有 a b c d e f g 几个数&#xff0c; 先对位置d操作 变成 a b c d - e f g 再对c操作 变成 a b c - (d-e) f g 仔细分析后得出结论&#xff1a;对于第一个数如a, 它一定为正数&#xff0c;第二个数b,一定为负数&#…

Java并发(四)----线程运行原理

1、线程运行原理 1.1 栈与栈帧   Java Virtual Machine Stacks &#xff08;Java 虚拟机栈 JVM&#xff09; 我们都知道 JVM 中由堆、栈、方法区所组成&#xff0c;其中栈内存是给谁用的呢&#xff1f;其实就是线程&#xff0c;每个线程启动后&#xff0c;虚拟机就会为其分…

java 解密springboot的WEB端口是谁启动的之内嵌tomcat

找到项目的 pom.xml 看到下面的spring-boot-starter-web 我们按住 Ctrl 点击进去 里面就有一个 tomcat 简单说 我们的程序能启动起tomcat端口 就是靠的这个东西 简单说 就是在程序中嵌了一个tomcat服务器 这里 可能就有小伙伴蒙了 不是把程序放在服务器上运行吗&#xff1f…

Linux Driver 和Device匹配过程分析(2)

Linux Driver 和Device匹配过程分析&#xff08;2&#xff09; 1 device注册流程2&#xff0c;driver注册匹配过程&#xff1a;2.1 pci_register_driver2.1.1 nvme_init2.1.2 pci_register_driver2.1.3 __pci_register_driver2.1.4 driver_register2.1.5 bus_add_driver2.1.6 d…

读书笔记——《2001太空漫游》

阿瑟克拉克神作&#xff0c;任何一个科幻迷都绕不开的一部作品。很早就听说过其大名&#xff0c;因为之前看过电影版的&#xff0c;总感觉少了点新鲜感&#xff0c;这本书就一直在书架上没有拿出来看。但是看过这本书后&#xff0c;我可以很负责任的说&#xff0c;全书都充满新…

【递推专题】常见的递推“模型”总结

目录 1.斐波那契数列分析&#xff1a;代码&#xff1a; 2.平面分割问题分析&#xff1a; 3.汉诺塔问题分析&#xff1a; 4.卡特兰数分析&#xff1a; 5.第二类斯特林数总结&#xff1a; 1.斐波那契数列 分析&#xff1a; 斐波那契数列又称兔子数列&#xff0c;其原理来源于兔子…

dangerousRemoteUrlIpcAccess

问题描述&#xff1a; 在使用Tauri窗口加载外部链接时&#xff0c;需要也能继续使用Tauri API与Rust交互。按照官方发布通告中的代码添加配置&#xff1a; "security": {"dangerousRemoteUrlIpcAccess": [ { "windows": ["main", &qu…

在Linux中进行Jenkins部署(maven-3.9.1+jdk8)

Jenkins部署在公网IP为x.x.x.x的服务器上 maven-3.9.1要安装在jdk8环境中 环境准备 第一步&#xff0c;下载server-jre-8u202-linux-x64.tar.gz安装包。 登录地址&#xff1a;https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html下载server-j…

Maven命令和配置详解

Maven命令和配置详解 1. pom基本结构2. build基本结构3. Maven命令详解3.1 打包命令3.2 常用命令3.3 批量修改版本-父子pom4. Maven配置详解4.1 settings.xml4.2 项目内的maven工程结构Maven POM构建生命周期工程实践1. pom基本结构 <?xml versi

《程序员面试金典(第6版)》面试题 16.13. 平分正方形(直线的斜截式方程,C++)

题目描述 给定两个正方形及一个二维平面。请找出将这两个正方形分割成两半的一条直线。假设正方形顶边和底边与 x 轴平行。 每个正方形的数据square包含3个数值&#xff0c;正方形的左下顶点坐标[X,Y] [square[0],square[1]]&#xff0c;以及正方形的边长square[2]。所求直线穿…