autoware.ai感知随笔--地面滤波

news2025/1/19 10:22:03

autwoware.ai中点云预处理–points_preprocessor
points_preprocessor
cloud_transformer: 点云坐标转换,将输入的点云转化为velodyne坐标系下的点云。
compare_map_filter: 对比激光雷达点云和点云地图,然后提取(或去除)一致的点。

|`input_point_topic`|*String*|PointCloud source topic. Default `/points_raw`.|
|`input_map_topic`|*String*|PointCloud Map topic. Default `/points_map`.|
|`output_match_topic`|*String*|Comparing LiDAR PointCloud and PointCloud Map, topic of the PointCloud which matched. Default `/points_ground`.|
|`output_unmatch_topic`|*String*|Comparing LiDAR PointCloud and PointCloud Map, topic of the PointCloud which unmatched. Default `/points_no_ground`.|
|`distance_threshold`|*Double*|Threshold for comparing LiDAR PointCloud and PointCloud Map. Euclidean distance (mether).  Default `0.3`.|
|`min_clipping_height`|*Double*|Remove the points where the height is lower than the threshold. (Based on sensor coordinates). Default `-2.0`.|
|`max_clipping_height`|*Double*|Remove the points where the height is higher than the threshold. (Based on sensor coordinates). Default `0.5`.|

configcallback:主要是为 distance_threshold、min_clipping_height、max_clipping_height三个参数赋值。

 pcl::KdTreeFLANN<pcl::PointXYZI> tree_; // k-d树进行快速最近邻搜索。

pointsMapCallback:为map_frame_进行赋值,tree_.setInputCloud(map_cloud_ptr);
sensorPointsCallback:首先根据min_clipping_height和max_clipping_height进行一个高度的提取。

 tf::TransformListener* tf_listener_;
  tf_listener_->waitForTransform(map_frame_, sensor_frame, sensor_time, ros::Duration(3.0));
    pcl_ros::transformPointCloud(map_frame_, sensor_time, *sensorTF_clipping_height_cloud_ptr, sensor_frame,
                                 *mapTF_cloud_ptr, *tf_listener_);

利用以上代码将原始点云转化为地图坐标系下的点云。

searchMatchingCloud(mapTF_cloud_ptr, mapTF_match_cloud_ptr, mapTF_unmatch_cloud_ptr);

通过searchMatchingCloud函数得到匹配的点云和未匹配得到的点云。

void CompareMapFilter::searchMatchingCloud(const pcl::PointCloud<pcl::PointXYZI>::Ptr in_cloud_ptr,
                                           pcl::PointCloud<pcl::PointXYZI>::Ptr match_cloud_ptr,
                                           pcl::PointCloud<pcl::PointXYZI>::Ptr unmatch_cloud_ptr)
{
  match_cloud_ptr->points.clear();
  unmatch_cloud_ptr->points.clear();

  match_cloud_ptr->points.reserve(in_cloud_ptr->points.size());
  unmatch_cloud_ptr->points.reserve(in_cloud_ptr->points.size());

  std::vector<int> nn_indices(1);
  std::vector<float> nn_dists(1);
  const double squared_distance_threshold = distance_threshold_ * distance_threshold_;

  for (size_t i = 0; i < in_cloud_ptr->points.size(); ++i)
  {
    tree_.nearestKSearch(in_cloud_ptr->points[i], 1, nn_indices, nn_dists);
    if (nn_dists[0] <= squared_distance_threshold)
    {
      match_cloud_ptr->points.push_back(in_cloud_ptr->points[i]);
    }
    else
    {
      unmatch_cloud_ptr->points.push_back(in_cloud_ptr->points[i]);
    }
  }
}

主要是根据k-d树的快速最近邻搜索算法得到匹配点云和未匹配点云。

points_concat_filter: 合并点云,同步接收多个点云信息。

cloud_synchronizer_ = new message_filters::Synchronizer<SyncPolicyT>(
      SyncPolicyT(10), *cloud_subscribers_[0], *cloud_subscribers_[1], *cloud_subscribers_[2], *cloud_subscribers_[3],
      *cloud_subscribers_[4], *cloud_subscribers_[5], *cloud_subscribers_[6], *cloud_subscribers_[7]);
  cloud_synchronizer_->registerCallback(
      boost::bind(&PointsConcatFilter::pointcloud_callback, this, _1, _2, _3, _4, _5, _6, _7, _8));

ray_ground_filter:地面滤波
射线法去除地面,该算法适用于机械式旋转雷达
// Model | Horizontal | Vertical | FOV(Vertical) degrees / rads
// ----------------------------------------------------------
// HDL-64 |0.08-0.35(0.32) | 0.4 | -24.9 <=x<=2.0 (26.9 / 0.47)
// HDL-32 | 0.1-0.4 | 1.33 | -30.67<=x<=10.67 (41.33 / 0.72)
// VLP-16 | 0.1-0.4 | 2.0 | -15.0<=x<=15.0 (30 / 0.52)
// VLP-16HD| 0.1-0.4 | 1.33 | -10.0<=x<=10.0 (20 / 0.35)

|`input_point_topic`|*String*|PointCloud source topic. Default `/points_raw`.|
|`ground_topic`|*String*|Comparing LiDAR PointCloud and PointCloud Map, topic of the PointCloud which matched. Default `/points_ground`.|
|`no_ground_topic`|*String*|Comparing LiDAR PointCloud and PointCloud Map, topic of the PointCloud which unmatched. Default `/points_no_ground`.|
|`general_max_slope`|*Double*|Max Slope of the ground in the entire PointCloud, used when reclassification occurs (default 5 degrees).|
|`local_max_slope`|*Double*|Max Slope of the ground between Points (default 8 degrees).|
|`min_height_threshold`|*Double*|Minimum height threshold between points (default 0.05 meters).|
|`clipping_height`|*Double*|Remove Points above this height value (default 2.0 meters).|
|`min_point_distance`|*Double*|Removes Points closer than this distance from the sensor origin (default 1.85 meters).|
|`radial_divider_angle`|*Double*|Angle of each Radial division on the XY Plane (default 0.08 degrees).|
|`reclass_distance_threshold`|*Double*|Distance between points at which re classification will occur (default 0.2 meters).|

首先经过TransformPointCloud函数将点云坐标系转化为车身坐标系下的坐标。

bool RayGroundFilter::TransformPointCloud(const std::string& in_target_frame,
                                          const sensor_msgs::PointCloud2::ConstPtr& in_cloud_ptr,
                                          const sensor_msgs::PointCloud2::Ptr& out_cloud_ptr)
{
  if (in_target_frame == in_cloud_ptr->header.frame_id)
  {
    *out_cloud_ptr = *in_cloud_ptr;
    return true;
  }

  geometry_msgs::TransformStamped transform_stamped;
  try
  {
    transform_stamped = tf_buffer_.lookupTransform(in_target_frame, in_cloud_ptr->header.frame_id,
                                                   in_cloud_ptr->header.stamp, ros::Duration(1.0));
  }
  catch (tf2::TransformException& ex)
  {
    ROS_WARN("%s", ex.what());
    return false;
  }
  // tf2::doTransform(*in_cloud_ptr, *out_cloud_ptr, transform_stamped);
  Eigen::Matrix4f mat = tf2::transformToEigen(transform_stamped.transform).matrix().cast<float>();
  pcl_ros::transformPointCloud(mat, *in_cloud_ptr, *out_cloud_ptr);
  out_cloud_ptr->header.frame_id = in_target_frame;
  return true;
}

在进行地面滤除之前裁减过高的点

void RayGroundFilter::ClipCloud(const pcl::PointCloud<pcl::PointXYZI>::Ptr in_cloud_ptr, const double in_clip_height,
                                pcl::PointCloud<pcl::PointXYZI>::Ptr out_clipped_cloud_ptr)
{
  pcl::ExtractIndices<pcl::PointXYZI> extractor;
  extractor.setInputCloud(in_cloud_ptr);
  pcl::PointIndices indices;
// #pragma omp for语法OpenMP的并行化语法,即希望通过OpenMP并行化执行这条语句后的for循环,从而起到加速的效果。
#pragma omp for
  for (size_t i = 0; i < in_cloud_ptr->points.size(); i++)
  {
    // 添加判断,去除空点
    if (in_cloud_ptr->points[i].z > in_clip_height || isnan(in_cloud_ptr->points[i].x) || isnan(in_cloud_ptr->points[i].y) || isnan(in_cloud_ptr->points[i].z))
    {
      indices.indices.push_back(i);
    }
  }
  extractor.setIndices(boost::make_shared<pcl::PointIndices>(indices));
  extractor.setNegative(true); // true removes the indices, false leaves only the indices
  extractor.filter(*out_clipped_cloud_ptr);
}

通过RemovePointsUpTo函数避免车身点云的影响

/*!
 * Removes points up to a certain distance in the XY Plane
 * @param in_cloud_ptr Input PointCloud
 * @param in_min_distance Minimum valid distance, points closer than this will be removed.
 * @param out_filtered_cloud_ptr Resulting PointCloud with the invalid points removed.
 */
void RayGroundFilter::RemovePointsUpTo(const pcl::PointCloud<pcl::PointXYZI>::Ptr in_cloud_ptr, double in_min_distance,
                                       pcl::PointCloud<pcl::PointXYZI>::Ptr out_filtered_cloud_ptr)
{
  pcl::ExtractIndices<pcl::PointXYZI> extractor;
  extractor.setInputCloud(in_cloud_ptr);
  pcl::PointIndices indices;

#pragma omp for
  for (size_t i = 0; i < in_cloud_ptr->points.size(); i++)
  {
    if (sqrt(in_cloud_ptr->points[i].x * in_cloud_ptr->points[i].x +
             in_cloud_ptr->points[i].y * in_cloud_ptr->points[i].y) < in_min_distance)
    {
      indices.indices.push_back(i);
    }
  }
  extractor.setIndices(boost::make_shared<pcl::PointIndices>(indices));
  extractor.setNegative(true);  // true removes the indices, false leaves only the indices
  extractor.filter(*out_filtered_cloud_ptr);
}

Ray_groud_filter算法主要是以射线的方式(RAY)来组织点云,现在将点云(x,y,z)三维空间降到(x,y)平面去看,计算每一个点到车辆x正方向的平面夹角θ,我们对360度进行微分,分成若干等份,每一份的角度为0.18,这个微分可以看作一条射线,每条射线上的点又根据水平距离(点到lidar的水平距离,半径)0.001再进行微分。
在这里插入图片描述
为了方便对点进行半径和夹角的表示,我们使用如下数据结构代替 PointXYZIRTColor

  struct PointXYZIRTColor
  {
    pcl::PointXYZI point;

    float radius;  // cylindric coords on XY Plane
    float theta;   // angle deg on XY plane

    size_t radial_div;      // index of the radial divsion to which this point belongs to
    size_t concentric_div;  // index of the concentric division to which this points belongs to

    size_t red;    // Red component  [0-255]
    size_t green;  // Green Component[0-255]
    size_t blue;   // Blue component [0-255]

    size_t original_index;  // index of this point in the source pointcloud
  };

其中radius表示点到lidar的水平距离(半径),即:sqrt(pow(x,2) + pow(y,2))
theta是点相对于车头正方向(即x正方向)的夹角,计算公式为 theta = arctan(y/x) * 180/pi
radial_divconcentric_div 分别描述角度微分和距离微分。对点云进行水平角度微分之后,可得到: 360/0.18=2000条射线,将这些射线中的点按照距离的远近进行排序,如下所示:

/*!
 *
 * @param[in] in_cloud Input Point Cloud to be organized in radial segments
 * @param[out] out_organized_points Custom Point Cloud filled with XYZRTZColor data
 * @param[out] out_radial_divided_indices Indices of the points in the original cloud for each radial segment
 * @param[out] out_radial_ordered_clouds Vector of Points Clouds, each element will contain the points ordered
 */
void RayGroundFilter::ConvertXYZIToRTZColor(
    const pcl::PointCloud<pcl::PointXYZI>::Ptr in_cloud,
    const std::shared_ptr<PointCloudXYZIRTColor>& out_organized_points,
    const std::shared_ptr<std::vector<pcl::PointIndices> >& out_radial_divided_indices,
    const std::shared_ptr<std::vector<PointCloudXYZIRTColor> >& out_radial_ordered_clouds)
{
  out_organized_points->resize(in_cloud->points.size());
  out_radial_divided_indices->clear();
  out_radial_divided_indices->resize(radial_dividers_num_);
  out_radial_ordered_clouds->resize(radial_dividers_num_);

  for (size_t i = 0; i < in_cloud->points.size(); i++)
  {
    PointXYZIRTColor new_point;
    auto radius = static_cast<float>(
        sqrt(in_cloud->points[i].x * in_cloud->points[i].x + in_cloud->points[i].y * in_cloud->points[i].y));
    auto theta = static_cast<float>(atan2(in_cloud->points[i].y, in_cloud->points[i].x) * 180 / M_PI);
    if (theta < 0)
    {
      theta += 360;
    }
    if (theta >= 360)
    {
      theta -= 360;
    }

    auto radial_div = (size_t)floor(theta / radial_divider_angle_);

    auto concentric_div = (size_t)floor(fabs(radius / concentric_divider_distance_));

    new_point.point = in_cloud->points[i];
    new_point.radius = radius;
    new_point.theta = theta;
    new_point.radial_div = radial_div;
    new_point.concentric_div = concentric_div;
    new_point.red = (size_t)colors_[new_point.radial_div % color_num_].val[0];
    new_point.green = (size_t)colors_[new_point.radial_div % color_num_].val[1];
    new_point.blue = (size_t)colors_[new_point.radial_div % color_num_].val[2];
    new_point.original_index = i;

    out_organized_points->at(i) = new_point;

    // radial divisions
    out_radial_divided_indices->at(radial_div).indices.push_back(i);

    out_radial_ordered_clouds->at(radial_div).push_back(new_point);
  }  // end for

// order radial points on each division
#pragma omp for
  for (size_t i = 0; i < radial_dividers_num_; i++)
  {
    std::sort(out_radial_ordered_clouds->at(i).begin(), out_radial_ordered_clouds->at(i).end(),
              [](const PointXYZIRTColor& a, const PointXYZIRTColor& b) { return a.radius < b.radius; });  // NOLINT
  }
}

其中颜色的上色是根据cv::generateColors(colors_, color_num_); 计算得到的,这个函数是个非常方便的辅助函数,当我们需要生成固定数目的不同颜色时,可以考虑使用它。

0.18度是VLP32C雷达的水平光束发散间隔。

通过判断射线中前后两点的坡度是否大于我们事先设定的坡度阈值,从而判定是否为地面点。

/*!
 * Classifies Points in the PointCoud as Ground and Not Ground
 * @param in_radial_ordered_clouds Vector of an Ordered PointsCloud ordered by radial distance from the origin
 * @param out_ground_indices Returns the indices of the points classified as ground in the original PointCloud
 * @param out_no_ground_indices Returns the indices of the points classified as not ground in the original PointCloud
 */
void RayGroundFilter::ClassifyPointCloud(const std::vector<PointCloudXYZIRTColor>& in_radial_ordered_clouds,
                                         const pcl::PointIndices::Ptr& out_ground_indices,
                                         const pcl::PointIndices::Ptr& out_no_ground_indices)
{
  out_ground_indices->indices.clear();
  out_no_ground_indices->indices.clear();
#pragma omp for
  for (size_t i = 0; i < in_radial_ordered_clouds.size(); i++)  // sweep through each radial division
  {
    float prev_radius = 0.f;
    float prev_height = 0.f;
    bool prev_ground = false;
    bool current_ground = false;
    for (size_t j = 0; j < in_radial_ordered_clouds[i].size(); j++)  // loop through each point in the radial div
    {
      float points_distance = in_radial_ordered_clouds[i][j].radius - prev_radius;
      float height_threshold = tan(DEG2RAD(local_max_slope_)) * points_distance;
      float current_height = in_radial_ordered_clouds[i][j].point.z;
      float general_height_threshold = tan(DEG2RAD(general_max_slope_)) * in_radial_ordered_clouds[i][j].radius;

      // for points which are very close causing the height threshold to be tiny, set a minimum value
      if (points_distance > concentric_divider_distance_ && height_threshold < min_height_threshold_)
      {
        height_threshold = min_height_threshold_;
      }

      // check current point height against the LOCAL threshold (previous point)
      if (current_height <= (prev_height + height_threshold) && current_height >= (prev_height - height_threshold))
      {
        // Check again using general geometry (radius from origin) if previous points wasn't ground
        if (!prev_ground)
        {
          if (current_height <= general_height_threshold && current_height >= -general_height_threshold)
          {
            current_ground = true;
          }
          else
          {
            current_ground = false;
          }
        }
        else
        {
          current_ground = true;
        }
      }
      else
      {
        // check if previous point is too far from previous one, if so classify again
        if (points_distance > reclass_distance_threshold_ &&
            (current_height <= height_threshold && current_height >= -height_threshold))
        {
          current_ground = true;
        }
        else
        {
          current_ground = false;
        }
      }

      if (current_ground)
      {
        out_ground_indices->indices.push_back(in_radial_ordered_clouds[i][j].original_index);
        prev_ground = true;
      }
      else
      {
        out_no_ground_indices->indices.push_back(in_radial_ordered_clouds[i][j].original_index);
        prev_ground = false;
      }

      prev_radius = in_radial_ordered_clouds[i][j].radius;
      prev_height = in_radial_ordered_clouds[i][j].point.z;
    }
  }
}

这里有两个重要参数,一个是local_max_slope_,是我们设定的同条射线上邻近两点的坡度阈值,一个是general_max_slope_,表示整个地面的坡度阈值,这两个坡度阈值的单位为度(degree),我们通过这两个坡度阈值以及当前点的半径(到lidar的水平距离)求得高度阈值,通过判断当前点的高度(即点的z值)是否在地面加减高度阈值范围内来判断当前点是为地面。

该算法的整体流程为:通过当前点和上一个点的距离计算得出高度阈值,判断当前点的z值是否之前点加减高度阈值内:如果不在,则判断是否和上一个点距离太远,如果距离太远则比较当前z点的高度和高度阈值的比较,如果在范围内则设定为地面点,如果不在则为非地面点。 如果判断当前点的z值是否之前点加减高度阈值内,若上一个点是地面点,则当前点也为地面点。如果不是,则判断当前点高度相对于全局高度差的范围内,若在则为地面点,否则为非地面点。
ring_ground_filter: 地面滤波

其中关于地面滤除算法:LeGO-LOAM中也有代码是通过将点云投影到Rang Image进行地面滤除–imageProjection.cpp。
整体思路:copyPointCloud(laserCloudMsg)->将点云信息转化为pcl格式;-> findStartEndAngle()->找到点云的起始角和终止角;->projectPointCloud()->将点云投影到Range Image;-> groundRemoval()-> 去除地面点。

    void findStartEndAngle(){
        // atan2(y,x)函数的返回值范围(-PI,PI],表示与复数x+yi的幅角
        // segMsg.startOrientation范围为(-PI,PI]
        // segMsg.endOrientation范围为(PI,3PI]
        segMsg.startOrientation = -atan2(laserCloudIn->points[0].y, laserCloudIn->points[0].x);//第一个点的起始角度
        segMsg.endOrientation   = -atan2(laserCloudIn->points[laserCloudIn->points.size() - 1].y,
                                                     laserCloudIn->points[laserCloudIn->points.size() - 2].x) + 2 * M_PI;//最后一个点的角度
		// 开始和结束的角度差一般是多少?
		// 一个velodyne 雷达数据包转过的角度多大?
		// segMsg.endOrientation - segMsg.startOrientation范围为(0,4PI)
        if (segMsg.endOrientation - segMsg.startOrientation > 3 * M_PI) {
            segMsg.endOrientation -= 2 * M_PI;
        } else if (segMsg.endOrientation - segMsg.startOrientation < M_PI)
            segMsg.endOrientation += 2 * M_PI;
		// segMsg.orientationDiff的范围为(PI,3PI),一圈大小为2PI,应该在2PI左右
        segMsg.orientationDiff = segMsg.endOrientation - segMsg.startOrientation;//整个激光扫描的水平角度
    }

对于雷达旋转一圈,其实并不是从0度按某一方向旋转到360度,而是在起始时有一定偏差,而旋转也不是严格的360度,一般情况下要比360度稍微多几度。注意,我们的雷达坐标系为右前上(xyz),所以我们要找到点云的起始角和结束角。这里是以x轴为基准,采用的办法是使用收到的第一个点云点和结束的点云点的角度作为该帧点云的起始角和结束角。
点云重投影projectPointCloud

void projectPointCloud(){
        float verticalAngle, horizonAngle, range;
        size_t rowIdn, columnIdn, index, cloudSize; 
        PointType thisPoint;

        cloudSize = laserCloudIn->points.size();
        fullCloud->points.resize(N_SCAN*Horizon_SCAN);
        visit_flag_= std::vector<bool>(N_SCAN*Horizon_SCAN, false);
        maxhorizon_num_ = 0, minhorizon_num_ = Horizon_SCAN;
        for (size_t i = 0; i < cloudSize; ++i){

            thisPoint.x = laserCloudIn->points[i].x;
            thisPoint.y = laserCloudIn->points[i].y;
            thisPoint.z = laserCloudIn->points[i].z;
            thisPoint.intensity=laserCloudIn->points[i].intensity;
           //verticalAngle,垂直方向的角度
            verticalAngle = atan2(thisPoint.z, sqrt(thisPoint.x * thisPoint.x + thisPoint.y * thisPoint.y)) * 180 / M_PI;
			// rowIdn计算出该点是激光雷达的第几线的, 从下往上计数,-15度记为初始线为第0线,一共16线(N_SCAN=16)
            rowIdn = (verticalAngle + ang_bottom) / ang_res_y;
            if (rowIdn < 0 || rowIdn >= N_SCAN)
                continue;
            //horizonAngle水平方向的角度
            horizonAngle = atan2(thisPoint.x, thisPoint.y) * 180 / M_PI;

			// round函数进行四舍五入取整
			// 这边确定不是减去180度???不是
			// 雷达水平方向上某个角度和水平第几线的关联关系???关系如下:
			// horizonAngle:(-PI,PI],columnIdn:[H/4,5H/4]-->[0,H] (H:Horizon_SCAN)
			// 下面是把坐标系绕z轴旋转顺时针旋转90度,
			// x+==>3*Horizon_SCAN/4,x-==>Horizon_SCAN/4
			// y+==>Horizon_SCAN/2,y-==>0
            //水平的一圈有1800个点,分辨率为0.2
            //columnIdn为水平的哪一个点
            columnIdn = -round((horizonAngle-90.0)/ang_res_x) + Horizon_SCAN/2;
            if (columnIdn >= Horizon_SCAN)
                columnIdn -= Horizon_SCAN;

            if (columnIdn < 0 || columnIdn >= Horizon_SCAN)
                continue;

            range = sqrt(thisPoint.x * thisPoint.x + thisPoint.y * thisPoint.y + thisPoint.z * thisPoint.z);
            if (range < 0.1)
                continue;
            if(maxhorizon_num_ < columnIdn)maxhorizon_num_ = columnIdn;
            if(minhorizon_num_ > columnIdn)minhorizon_num_ = columnIdn;
            //rangeMat存的是,垂直上的那根线,水平上的那个点,这个位置的长度
            rangeMat.at<float>(rowIdn, columnIdn) = range;

			// columnIdn:[0,H] (H:Horizon_SCAN)==>[0,1800]
           // thisPoint.intensity = (float)rowIdn + (float)columnIdn / 10000.0;

            index = columnIdn  + rowIdn * Horizon_SCAN;//index存成一行,这一整个激光的那个点
            fullCloud->points[index] = thisPoint;
            visit_flag_[index] = true;

            fullInfoCloud->points[index].intensity =  range;
        }
    }

将点云投影到16*1800的图片上,16线,水平的角分辨率是0.2,因此360/0.2=1800。Rang Image中存储点云的深度信息,将点云信息存入fullCloud和fullInfoCloud,两者的区别在于第一个中的强度信息填入了行号和列号相关的信息,而后者填入的是深度信息。
提取地面点groundRemoval:
关于groundMat的说明:
=-1:无效值,表示无法判断是否为地面点;
=0: 代表初始值,不是地面点;
=1: 表示是地面点。

关于labelMat的说明:
=-1:无效值,不进行点云分割;
=0: 初始值;
=labelCount: 平面点;
=999999: 舍弃点。

关于rangeMat的说明:
FLT_MAX: 初始化值;
range: 点云的深度值。

void groundRemoval(){
        size_t lowerInd, upperInd;
        float diffX, diffY, diffZ, angle;

        for (size_t j = 0; j < Horizon_SCAN; ++j){
        // for (size_t j = minhorizon_num_+5; j < maxhorizon_num_-5; ++j){
            groundMat.at<int8_t>(0,j) = 1;
            for (size_t i = 1; i < 16; ++i){//groundScanInd=7 只看下面的7根线
           
                lowerInd = j + ( i )*Horizon_SCAN;
                upperInd = j + (i-1)*Horizon_SCAN;
                if(j == 0 || j == Horizon_SCAN - 1)
                {
                    groundMat.at<int8_t>(i,j) = 1;
                    continue;
                }
           
                if(!visit_flag_[lowerInd] || !visit_flag_[upperInd])
                {
                    groundMat.at<int8_t>(i,j) = 1;
                    continue;
                }
         
				// 由上下两线之间点的XYZ位置得到两线之间的俯仰角
				// 如果俯仰角在10度以内,则判定(i,j)为地面点,groundMat[i][j]=1
				// 否则,则不是地面点,进行后续操作
                diffX = fullCloud->points[upperInd].x - fullCloud->points[lowerInd].x;
                diffY = fullCloud->points[upperInd].y - fullCloud->points[lowerInd].y;
                diffZ = fullCloud->points[upperInd].z - fullCloud->points[lowerInd].z;

                
                if(fabs(diffX) > 1 || fabs(diffY) > 1)
                {
                    groundMat.at<int8_t>(i,j) = 1;
                    continue;
                }
                if(fabs(fullCloud->points[upperInd].x) > 12 || fabs(fullCloud->points[upperInd].x) > 12)
                {
                    groundMat.at<int8_t>(i,j) = 1;
                    continue;
                }
                angle = atan2(diffZ, sqrt(diffX*diffX + diffY*diffY) ) * 180 / M_PI;

    
                if (abs(angle) <= segment_dis_){//10度以内认为是地面点
                //ROS_INFO("I=%d",i);
                    groundMat.at<int8_t>(i,j) = 1;//不仅仅把当前点认为是地面点,还会把当前点下一根线上的点也认为是地面点
                    // groundMat.at<int8_t>(i+1,j) = 1;
               }
               else
               if(isnan(angle))
                   { //ROS_INFO("angle=%f",angle);
                         groundMat.at<int8_t>(i,j) = 1;//不仅仅把当前点认为是地面点,还会把当前点下一根线上的点也认为是地面点
                    groundMat.at<int8_t>(i+1,j) = 1; 
                   }else if(i==0)
                   {
                    groundMat.at<int8_t>(i,j) = 1;//不仅仅把当前点认为是地面点,还会把当前点下一根线上的点也认为是地面点
                    groundMat.at<int8_t>(i+1,j) = 1; 
                   }else if(i==14)
                   {
                    groundMat.at<int8_t>(i,j) = 1;//不仅仅把当前点认为是地面点,还会把当前点下一根线上的点也认为是地面点
                    groundMat.at<int8_t>(i+1,j) = 1;
                   }else
                   {
                      //ROS_INFO("angle=%f",angle);
                   }
            }
            int obs_num = 0;
            int obs_line_num = -1;
            for (size_t i = 0; i < 16; ++i)
            {
                if(groundMat.at<int8_t>(i,j) != 1)
                {
                    obs_num ++;
                    obs_line_num = i;
                }
            }
            if(obs_num == 1 && obs_line_num < 5)
            {
                for (size_t i = 0; i < 16; ++i)
                {
                    groundMat.at<int8_t>(i,j) = 1;
                }
            }
        }

		// 找到所有点中的地面点,并在labelMat将他们标记为-1
		// rangeMat[i][j]==FLT_MAX??? 判定为地面点的另一个条件
        for (size_t i = 0; i < N_SCAN; ++i){
            for (size_t j = 0; j < Horizon_SCAN; ++j){
                if (groundMat.at<int8_t>(i,j) == 1 || rangeMat.at<float>(i,j) == FLT_MAX){
                    labelMat.at<int>(i,j) = -1;
                }
            }
        }
		
        if (pubGroundCloud.getNumSubscribers() != 0){	// 如果有节点订阅groundCloud,那么就需要把地面点发布出来
            for (size_t i = 0; i <= groundScanInd; ++i){
                for (size_t j = 0; j < Horizon_SCAN; ++j){
                    if (groundMat.at<int8_t>(i,j) == 1)
                        groundCloud->push_back(fullCloud->points[j + i*Horizon_SCAN]);// 具体实现过程:把点放到groundCloud队列中去
                }
            }
        }
    }

在提取地面点之前首先需要明确的一点,在激光雷达安装时是水平的,一般认为最中间的激光线束与水平面平行,所以对于一个16线的激光雷达而言,地面点在前7束激光线之中提取。即groundScanInd=7,遍历前7束所有点云:
对于地面点的判定:对于i 束中的激光点,在i + 1 束找到对应点,这两点与雷达的连线所形成的向量的差向量如下图如所示,若该差向量与水平面所形成的夹角小于10度,则认为是一个地面点。

还有一种常见的地面滤除算法:GPF
1.对于每个点云数据段,会先提取一组具有低高度值的种子点,然后用这些种子点来估计出一个地面的初始模型。

2.根据估计出的平面模型,点云段P中的每个点都要参与计算,计算从该点到其在候选平面上的正交投影的距离(简单理解成垂线段的长度)。
3.将该距离与用户定义的阈值dist 进行比较,该阈值决定该点是否属于地面。
4.属于地面的点被用作新模型的种子,该过程重复N次。
5.最后,由该算法产生的每个点云段的地面点连接起来,就是整个地面。

我们选择初始种子点的方法叫做最低点代表(LPR),该点定义为点云的N个最低高度值点的平均值。LPR保证了噪声不会影响平面估计结果。一旦计算出了LPR,则将其视为点云P的最低高度值点,并且高度阈值Thseeds内的点会成为初始种子点,用作后续的平面模型估计。

space_filter: 点云裁切 通过y方向和z方向的限定值进行裁减
另外地面滤除还有patchwork++算法 等后续更新。

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

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

相关文章

联通面试题

一、GC 1.1、目标 GC的主要作用是自动识别和释放不再使用的对象&#xff0c;回收其所占用的内存&#xff0c;以防止内存泄漏和内存溢出的问题。 1.2、如何实现 1.2.1、标记阶段 GC从根对象&#xff08;如线程栈中的引用、静态变量等&#xff09;开始&#xff0c;通过可达性…

CnosDB 签约京清能源,助力分布式光伏发电解决监测系统难题。

近日&#xff0c;京清能源采购CnosDB&#xff0c;升级其“太阳能光伏电站一体化监控平台”。该平台可以实现电站设备统一运行监控&#xff0c;数据集中管理&#xff0c;为操作人员、维护人员、管理人员提供全面、便捷、差异化的数据和服务。 京清能源集团有限公司&#xff08;…

【LeetCode】35.复杂链表的复制

题目 请实现 copyRandomList 函数&#xff0c;复制一个复杂链表。在复杂链表中&#xff0c;每个节点除了有一个 next 指针指向下一个节点&#xff0c;还有一个 random 指针指向链表中的任意节点或者 null。 示例 1&#xff1a; 输入&#xff1a;head [[7,null],[13,0],[11,4]…

并发-Executor框架笔记

Executor框架 jdk5开始&#xff0c;把工作单元与执行机制分离开来&#xff0c;工作单元包括Runable和Callable&#xff0c;执行机制由Executor框架来提供。 Executor框架简介 Executor框架的两级调度模型 Java线程被一对一映射为本地操作系统线程 java线程启动会创建一个本…

Linux单列模式实现线程池

目录 一、单列模式 1.1 单列模式概念以及实现条件 1.2 饿汉模式 1.1.1 饿汉模式代码实现 1.1.2 饿汉模式特征和优缺点 1.3 懒汉模式 1.3.1 懒汉模式代码实现 1.3.2 懒汉模式特征以及优缺点 二、线程池 2.1 线程池概念 2.2 实现简单线程池逻辑 2.3 模拟实现懒汉模式线程…

【八大经典排序算法】:直接插入排序、希尔排序实现 ---> 性能大比拼!!!

【八大经典排序算法】&#xff1a;直接插入排序、希尔排序实现 ---> 性能大比拼&#xff01;&#xff01;&#xff01; 一、 直接插入排序1.1 插入排序原理1.2 代码实现1.3 直接插入排序特点总结 二、希尔排序 ( 缩小增量排序 )2.1 希尔排序原理2.2 代码实现2.3 希尔排序特点…

UE5、CesiumForUnreal实现瓦片坐标信息图层效果

文章目录 1.实现目标2.实现过程2.1 原理简介2.2 cesium-native改造2.3 CesiumForUnreal改造2.4 运行测试3.参考资料1.实现目标 参考CesiumJs的TileCoordinatesImageryProvider,在CesiumForUnreal中也实现瓦片坐标信息图层的效果,便于后面在调试地形和影像瓦片的加载调度等过…

【C++入门到精通】C++入门 ——搜索二叉树(二叉树进阶)

阅读导航 前言一、搜索二叉树简介1. 概念2. 基本操作⭕搜索操作&#x1f36a;搜索操作基本代码&#xff08;非递归&#xff09; ⭕插入操作&#x1f36a;插入操作基本代码&#xff08;非递归&#xff09; ⭕删除操作&#x1f36a;删除操作基本代码&#xff08;非递归&#xff0…

给老婆写的,每日自动推送暖心消息

文章の目录 一、起因二、环境准备三、创建nestjs项目四、控制器五、service服务层1、获取Access token2、组装模板消息数据3、获取下次发工资还有多少天4、获取距离下次结婚纪念日还有多少天5、获取距离下次生日还有多少天6、获取时间日期7、获取是第几个结婚纪念日8、获取相恋…

前端面试题JS篇(4)

浏览器缓存 浏览器缓存分为强缓存和协商缓存&#xff0c;当客户端请求某个资源时&#xff0c;获取缓存的流程如下&#xff1a; 先根据这个资源的一些 http header 判断它是否命中强缓存&#xff0c;如果命中&#xff0c;则直接从本地获取缓存资源&#xff0c;不会发请求到服务…

vivado xpm 使用和封装

vivado xpm 使用和封装 tools -> language templates

【JavaScript】WebAPI入门到实战

文章目录 一、WebAPI背景知识1. 什么是WebAPI&#xff1f;2. 什么是API&#xff1f; 二、DOM基本概念三、获取元素三、事件初识1. 点击事件2. 键盘事件 四、操作元素1. 获取/修改元素内容2. 获取/修改元素属性3. 获取/修改表单元素属性4. 获取/修改样式属性 五、操作节点1. 新增…

scratch还原轨迹 2023年5月中国电子学会图形化编程 少儿编程 scratch编程等级考试四级真题和答案解析

目录 scratch还原轨迹 一、题目要求 1、准备工作 2、功能实现 二、案例分析

Python:安装Flask web框架hello world

安装easy_install pip install distribute 安装pip easy_install pip 安装 virtualenv pip install virtualenv 激活Flask pip install Flask 创建web页面demo.py from flask import Flask app Flask(__name__)app.route(/) def hello_world():return Hello World! 2023if _…

基于springboot实现的rabbitmq消息确认

概述 RabbitMQ的消息确认有两种。 一种是消息发送确认。这种是用来确认生产者将消息发送给交换器&#xff0c;交换器传递给队列的过程中&#xff0c;消息是否成功投递。发送确认分为两步&#xff0c;一是确认是否到达交换器&#xff0c;二是确认是否到达队列。 第二种是消费接…

【APUE】标准I/O库

目录 1、简介 2、FILE对象 3、打开和关闭文件 3.1 fopen 3.2 fclose 4、输入输出流 4.1 fgetc 4.2 fputc 4.3 fgets 4.4 fputs 4.5 fread 4.6 fwrite 4.7 printf 族函数 4.8 scanf 族函数 5、文件指针操作 5.1 fseek 5.2 ftell 5.3 rewind 6、缓冲相关 6.…

安装samba服务器

1.实验目的 &#xff08;1&#xff09;了解SMB和NETBIOS的基本原理 &#xff08;2&#xff09;掌握Windows和Linux之间&#xff0c;Linux系统之间文件共享的基本方法。 2.实验内容 &#xff08;1&#xff09;安装samba服务器。 &#xff08;2&#xff09;配置samba服务器的…

Visual Studio 线性表的链式存储节点输出引发异常:读取访问权限冲突

问题&#xff1a; 写了一个线性表的链式存储想要输出&#xff0c;能够输出&#xff0c;但是会报错&#xff1a;读取访问权限冲突 分析&#xff1a; 当我们输出到最后倒数第二个节点时&#xff0c;p指向倒数第二个节点并输出&#xff1b; 下一轮循环&#xff1a;p指向倒数第二…

Helm Kubernetes Offline Deploy Rancher v2.7.5 Demo (helm 离线部署 rancher 实践)

文章目录 1. 简介2. 预备条件3. 选择 SSL 配置4. 离线安装的 Helm Chart 选项5. 下载介质6. 生成证书7. 镜像入库8. 安装 rancher9. 配置 nodeport10. 配置 ingress11. 界面访问11.1 首页预览11.2 查看集群信息11.3 查看项目空间11.4 查看节点信息 1. 简介 Rancher 是一个开源…

17-数据结构-查找-(顺序、折半、分块)

简介&#xff1a;查找&#xff0c;顾名思义&#xff0c;是我们处理数据时常用的操作之一。大概就是我们从表格中去搜索我们想要的东西&#xff0c;这个表格&#xff0c;就是所谓的查找表&#xff08;存储数据的表&#xff09;。而我们怎么设计查找&#xff0c;才可以让计算机更…