PCL滤波工具之StatisticalOutlierRemoval深度分析

PCL中用于点云滤波的一个工具是StatisticalOutlierRemoval

其大致的原理思路是:

1. 对每个点,我们计算它到它的K邻域内所有点的平均距离d。对输入点云中的每个点都进行这样的计算,即每个点都可以求得其对应的d,因此可以得到一个包含各点d值的数组,记为distance。

2. 对于输入点云中的所有点,假设得到distance数组中的各元素构成一个高斯分布,该数组即为一个样本,样本容量为点云包含的点数目。高斯分布曲线的形状由样本的均值和标准差决定,d值在标准范围(由样本的均值和方差定义〉之外的对应点,可被定义为离群点并可从数据集中去除掉。

 具体的,下面贴上PCL源码进行分析

//该函数包含了上述算法的主要操作
template <typename PointT> void
pcl::StatisticalOutlierRemoval<PointT>::applyFilterIndices (std::vector<int> &indices)
{
  // Initialize the search class —— —— 初始化
  if (!searcher_)
  {
    if (input_->isOrganized ())
      searcher_.reset (new pcl::search::OrganizedNeighbor<PointT> ());
    else
      searcher_.reset (new pcl::search::KdTree<PointT> (false));
  }
  searcher_->setInputCloud (input_);

  // The arrays to be used
  std::vector<int> nn_indices (mean_k_); //mean_k_为定义的K邻域搜索的K值
  std::vector<float> nn_dists (mean_k_);
  std::vector<float> distances (indices_->size ());
  indices.resize (indices_->size ());
  removed_indices_->resize (indices_->size ());
  int oii = 0, rii = 0;  // oii = output indices iterator, rii = removed indices iterator

  // First pass: Compute the mean distances for all points with respect to their k nearest neighbors
  int valid_distances = 0; //记录共有多少点
  //下面开始遍历点云中各点,计算各点到其K邻域点的平均距离,每个点的结果放入distance数组中
  for (int iii = 0; iii < static_cast<int> (indices_->size ()); ++iii)  // iii = input indices iterator
  {
    if (!pcl_isfinite (input_->points[(*indices_)[iii]].x) ||
        !pcl_isfinite (input_->points[(*indices_)[iii]].y) ||
        !pcl_isfinite (input_->points[(*indices_)[iii]].z))
    {
      distances[iii] = 0.0;
      continue;
    }

    // Perform the nearest k search
    //searcher_->nearestKSearch函数是PCL中定义的K邻域搜索模块中的函数
    //searcher_->nearestKSearch有四个参数:
    //para1:查找点; para2:搜索范围K+1;
    //para3:k个邻域点的下标;
    //para4:k个邻域点到该查找点的距离的平方(是两点之间距离的平方)
    if (searcher_->nearestKSearch ((*indices_)[iii], mean_k_ + 1, nn_indices, nn_dists) == 0)
    {
      distances[iii] = 0.0;
      PCL_WARN ("[pcl::%s::applyFilter] Searching for the closest %d neighbors failed.\n", getClassName ().c_str (), mean_k_);
      continue;
    }

    // Calculate the mean distance to its neighbors
    double dist_sum = 0.0;
    for (int k = 1; k < mean_k_ + 1; ++k)  // k = 0 is the query point
      dist_sum += sqrt (nn_dists[k]);
    distances[iii] = static_cast<float> (dist_sum / mean_k_);
    valid_distances++;
  } //结束循环,得到点云中各点对应的K邻域平均距离

  // Estimate the mean and the standard deviation of the distance vector
  //计算distance数组的均值mean和标准偏差stddev——反映了整片点云的性质
  double sum = 0, sq_sum = 0;
  for (size_t i = 0; i < distances.size (); ++i)
  {
    sum += distances[i];
    sq_sum += distances[i] * distances[i];
  }
  double mean = sum / static_cast<double>(valid_distances);
  double variance = (sq_sum - sum * sum / static_cast<double>(valid_distances)) / (static_cast<double>(valid_distances) - 1);
  double stddev = sqrt (variance);
  

  //基于上述计算得到的整片点云的mean和stddev,设置判断阈值distance_threshold
  //distance_threshold是用于判断点云中点是否为离群点的阈值
  double distance_threshold = mean + std_mul_ * stddev;

  //比较各点的k邻域平均距离与整片点云的distance_threshold,若超过阈值则作为离群点舍弃
  // Second pass: Classify the points on the computed distance threshold
  for (int iii = 0; iii < static_cast<int> (indices_->size ()); ++iii)  // iii = input indices iterator
  {
    // Points having a too high average distance are outliers and are passed to removed indices
    // Unless negative was set, then it's the opposite condition
    if ((!negative_ && distances[iii] > distance_threshold) || (negative_ && distances[iii] <= distance_threshold))
    {
      if (extract_removed_indices_)
        (*removed_indices_)[rii++] = (*indices_)[iii];
      continue;
    }

    // Otherwise it was a normal point for output (inlier)
    indices[oii++] = (*indices_)[iii];
  }

  // Resize the output arrays
  indices.resize (oii);
  removed_indices_->resize (rii);
}

 通过代码中的细节可以看到,

1. 对每个点,计算它到它的K邻域内所有点的平均距离d,最终放到数组distance中;

2. 数组distance所构成的样本,其mean均值计算方式就是简单的元素相加除以样本数量

3. 数组distance所构成的样本,其stddev标准偏差(standard deviation)计算方式是:

for (size_t i = 0; i < distances.size (); ++i)
  {
    sum += distances[i];
    sq_sum += distances[i] * distances[i];
  }
double variance = (sq_sum - sum * sum / static_cast<double>(valid_distances)) / (static_cast<double>(valid_distances) - 1);

double stddev = sqrt (variance);

4. 作为离群点判断的阈值distance_threshold,其计算方式是:

double distance_threshold = mean + std_mul_ * stddev;

其中,std_mul_是一个标准偏差的倍乘参数,用于调整阈值大小,在使用PCL的StatisticalOutlierRemoval类时,该参数std_mul_由用户设置

5.关于std_mul_的选取:由于只有当某个点的K邻域平均距离大于该阈值distance_threshold 时才会被认定为离群点,而且distance_threshold = mean + std_mul_ * stddev。又因为对于同一片点云,其mean和stddev是不变的,因此std_mul_ 的大小决定了滤波的效果。

std_mul_越小,则distance_threshold 越小,则过滤效果越明显(大于阈值的点被过滤)

反之,std_mul_越大,则distance_threshold 越大,则过滤效果越不明显

至于选取std_mul_时是否需要考虑其对应的几何意义,本人认为不需要,只需要遵循上面规律,根据滤波测试结果对std_mul_参数调试选择最合适的即可,似乎不好根据几何意义选取std_mul_的值。

在上述分析后,进行一个实际的测试。使用PCL的StatisticalOutlierRemoval类的代码如下(需要使用头文件:#include <pcl/filters/statistical_outlier_removal.h>):

pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered(new pcl::PointCloud<pcl::PointXYZ>);
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;   //创建滤波器对象
sor.setInputCloud(cloud);                           //设置待滤波的点云
sor.setMeanK(50);                               //设置在进行统计时考虑的临近点个数
sor.setStddevMulThresh(1.0);                      //设置判断是否为离群点的阀值,用来倍乘标准差,也就是上面的std_mul
sor.filter(*cloud_filtered);                    //滤波结果存储到cloud_filtered

对输入的一片点云进行滤波,设置std_mul_值分别为2.0,1.0,结果如下:

std_mul_=2.0时,初始点有14722个,过滤后还剩14043个,滤掉了679个,占原有点的4.6%;

std_mul_=1.0时,过滤后还剩13125个,滤掉了1597个,占原有点的10.8%;

 最后,对PCL的StatisticalOutlierRemoval类进行了简单分析:

该滤波方法的实质为,对查找点到其K邻域点的平均距离这一值进行判断。判断的依据(即阈值)与全局点的分布相关,但是对于一片点云来说,该阈值是固定不变的。因此,某查找点的邻域点分布情况,就决定了它是否容易被认定为离群点。

因此,容易被该方法认定为离群点过滤掉的点,一般是以下几类点:

1. 相对孤单的离群点,由于是离群的,其K邻域的平均距离会比较大,因此会被认定为离群点——这是滤波算法想要的结果。但是,有的情况是这样的,离群的这些点具有聚集性,例如一大片平面点外,有一小簇(不只有几个点)噪声点,为了将这部分离群点去除,需要设置的K邻域搜索范围K足够大(应该要比这一簇噪声点的包含点数尽量大一些),这样就可以去除这部分离群点。不过随之带来的问题是,K搜索范围增大会减慢运行速度。

2. 边缘点。边缘点的K邻域不像非边缘点,其K近邻点的平均距离肯定是要大一些的,因此上述PCL的StatisticalOutlierRemoval类在过滤离群点时,也会将点云的边缘的点过滤一部分——这是算法所不想要的结果。

所以,该方法存在一些弊端,一是对于离群点具有小范围聚集的情况,算法为保证过滤效果需要更大的耗时;二是在过滤离群点的同时,会把边界点过滤。

  • 12
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Sure! Here's an example code snippet for using the `StatisticalOutlierRemoval` filter in PCL: ```cpp #include <iostream> #include <pcl/io/pcd_io.h> #include <pcl/filters/statistical_outlier_removal.h> int main(int argc, char** argv) { pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>); if (pcl::io::loadPCDFile<pcl::PointXYZ>(argv[1], *cloud) == -1) { PCL_ERROR("Couldn't read file\n"); return (-1); } pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor; sor.setInputCloud(cloud); sor.setMeanK(50); // number of points to use for mean distance calculation sor.setStddevMulThresh(1.0); // standard deviation multiplier threshold sor.filter(*cloud); pcl::io::savePCDFileASCII("output.pcd", *cloud); std::cout << "Filtered cloud saved as output.pcd\n"; return 0; } ``` In this example, we first load a point cloud from a PCD file using `pcl::io::loadPCDFile`. Then we create a `StatisticalOutlierRemoval` object and set the input cloud using `setInputCloud`. We also set two parameters: `setMeanK` sets the number of nearest neighbors to use for mean distance calculation, and `setStddevMulThresh` sets the threshold for removing outliers based on standard deviation. Finally, we call `filter` to apply the filter to the input cloud and save the filtered cloud to a new PCD file using `pcl::io::savePCDFileASCII`. Note that `StatisticalOutlierRemoval` works by computing the mean distance of each point to its k nearest neighbors, and then removing any points whose distance is greater than a certain number of standard deviations from the mean. By adjusting the values of `setMeanK` and `setStddevMulThresh`, you can control the sensitivity of the filter to outliers.

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值