【C++】pcl中的Region Growing(区域生长)算法

使用PCA估计一个点云的表面法线

原理

【机器学习】 PCA(Principal Component Analysis)——主成分分析中我们说过,对数据集 X X P X = Y PX=Y P P 是一组基向量, Y Y X X 的另一种表现形式。那么对二维数据来说, P P 就是新的坐标轴, Y Y 就是数据集 X X 在这个新坐标轴上的坐标。数据集 X X 在这个新坐标 x x 轴(数据方差最小的轴)上的投影就是二维数据降维到一维数据的结果。对于三维数据来说也是如此,可以想象在三维空间新建一个坐标系,原数据集在 x y x-y 平面(数据方差最小的面)上的投影面,就是三维数据降维到二维数据的结果。而这些点的法线,近似等于这个面的法线。
在这里插入图片描述

pcl实现

#include <pcl/io/io.h>
#include <pcl/io/pcd_io.h>
#include <pcl/features/integral_image_normal.h>
#include <pcl/visualization/cloud_viewer.h>
#include <pcl/point_types.h>
#include <pcl/features/normal_3d.h>

int main ()
{
     // 加载点云
     pcl::PointCloud<pcl::PointXYZ>::Ptr cloud (new pcl::PointCloud<pcl::PointXYZ>);
     pcl::io::loadPCDFile ("table_scene_lms400.pcd", *cloud);

     // 把点云数据传递给法线估计对象
     pcl::NormalEstimation<pcl::PointXYZ, pcl::Normal> ne;
     ne.setInputCloud (cloud);

     // 创建一个空的kd-tree
     pcl::search::KdTree<pcl::PointXYZ>::Ptr tree (new pcl::search::KdTree<pcl::PointXYZ> ());
     // 把空的kd-tree传给法线估计对象
     ne.setSearchMethod (tree);

     // 新建Normal型点云,储存估计法线的输出结果
     pcl::PointCloud<pcl::Normal>::Ptr cloud_normals (new pcl::PointCloud<pcl::Normal>);
     
     // 使用半径在查询点周围3厘米范围内的所有邻元素
     ne.setRadiusSearch (0.03);
     
     // 计算法线值,放到Normal型点云中
     ne.compute (*cloud_normals);

     // cloud_normals->points.size () 应该与 cloud->points.size ()有相同尺寸
     //法线可视化
     pcl::visualization::PCLVisualizer viewer("PCL Viewer");
     viewer.setBackgroundColor (0.0, 0.0, 0.0);
     viewer.addPointCloudNormals<pcl::PointXYZ,pcl::Normal>(cloud, cloud_normals);

     while (!viewer.wasStopped ())
     {
          viewer.spinOnce ();
     }

     return 0;
}

程序运行结果:
在这里插入图片描述通常,没有数学方法能解决法线的正负向问题。通过PCA来计算法线的方向同样具有二义性。如果实际知道视点 V p V_p ,那么这个问题的解决是非常简单的。对所有法线定向只需要使它们一致朝向视点方向,即满足:
在这里插入图片描述但是如果数据集是从多个捕获视点中配准后集成的,那么上述方法将不再适用。

pcl中的Region Growing算法

关于pcl中的直通滤波器——PassThrough

对某一维度实行一个简单的滤波,即去掉在用户指定范围内部(或外部)的点。

#include <iostream>
#include <ctime>
#include <pcl/point_types.h>
#include <pcl/filters/passthrough.h>

int main (int argc, char** argv)
{ 
  srand(time(0));
  pcl::PointCloud<pcl::PointXYZ>::Ptr cloud (new pcl::PointCloud<pcl::PointXYZ>);
  pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered (new pcl::PointCloud<pcl::PointXYZ>);

  cloud->width  = 5;
  cloud->height = 1;
  cloud->points.resize (cloud->width * cloud->height);
  for (size_t i = 0; i < cloud->points.size (); ++i)
  {
    cloud->points[i].x = rand () / (RAND_MAX + 1.0f)-0.5;
    cloud->points[i].y = rand () / (RAND_MAX + 1.0f)-0.5;
    cloud->points[i].z = rand () / (RAND_MAX + 1.0f)-0.5;
  }

  std::cerr << "Cloud before filtering: " << std::endl;
  for(size_t i = 0; i < cloud->points.size (); ++i)
  {
    std::cerr << "    " << cloud->points[i].x << " " << cloud->points[i].y << " " << cloud->points[i].z << std::endl;
  }
  
  // 创建滤波对象
  pcl::PassThrough<pcl::PointXYZ> pass;
  pass.setInputCloud (cloud); // 设置需要滤波的点云数据
  pass.setFilterFieldName ("z"); // 设置需要过滤的维度
  pass.setFilterLimits (0.0, 1.0); // 设置过滤范围
  pass.setFilterLimitsNegative (true); // 为true则保留范围外的点云数据,为false则保留范围内的点云数据
  pass.filter (*cloud_filtered);

  std::cerr << "Cloud after filtering: " << std::endl;
  for (size_t i = 0; i < cloud_filtered->points.size (); ++i)
  {
    std::cerr << "    " << cloud_filtered->points[i].x << " " << cloud_filtered->points[i].y << " " << cloud_filtered->points[i].z << std::endl;
  }
  return (0);
}

程序运行结果:
在这里插入图片描述

关于pcl中的体素网格滤波器——VoxelGrid

使用体素化网格方法实现下采样,即减少点的数量,减少点云数据,并同时保持点云的形状特征。
通过输入的点云数据创建一个三维体素栅格(可把体素栅格想象为微小的空间三维立方体的集合),然后在每个体素(即三维立方体)内,用体素中所有点的重心来近似显示体素中其他点,这样体素内所有点就用一个重心点最终表示。

#include <iostream>
#include <pcl/io/pcd_io.h>
#include <pcl/point_types.h>
#include <pcl/filters/voxel_grid.h>
#include <pcl/visualization/cloud_viewer.h>

int main (int argc, char** argv)
{
     pcl::PointCloud<pcl::PointXYZ>::Ptr cloud (new pcl::PointCloud<pcl::PointXYZ>);
     pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered (new pcl::PointCloud<pcl::PointXYZ>);

     pcl::PCDReader reader;
     reader.read ("table_scene_lms400.pcd", *cloud); 
     
     pcl::VoxelGrid<pcl::PointXYZ> sor; // 创建滤波器对象
     sor.setInputCloud (cloud);
     sor.setLeafSize (0.01f, 0.01f, 0.01f); // 设置滤波时的体素大小
     sor.filter (*cloud_filtered);

     pcl::visualization::CloudViewer viewer("Cloud Viewer"); // 创建一个可视化窗口

     viewer.showCloud(cloud_filtered); // 在窗口中显示点云

     while (!viewer.wasStopped ())
     {}
     
     return (0);
}

程序运行结果:
在这里插入图片描述

pcl中的Region Growing原理

在这里插入图片描述要说明的图中有错误的一点是:

  • 搜索到邻域后,这些点先过法线夹角阈值通过的保留到聚类数据,然后再从这些通过法线夹角阈值的点中检查是否通过曲率阈值,通过的加入种子点序列。

pcl中的Region Growing实现

#include <pcl/point_types.h>
#include <pcl/io/pcd_io.h>
#include <pcl/search/search.h>
#include <pcl/search/kdtree.h>
#include <pcl/features/normal_3d.h>
#include <pcl/visualization/cloud_viewer.h>
#include <pcl/features/integral_image_normal.h>
#include <pcl/segmentation/region_growing.h>
#include <pcl/filters/voxel_grid.h>

int main(int argc, char** argv)
{
	pcl::PointCloud<pcl::PointXYZRGBA>::Ptr cloud (new pcl::PointCloud<pcl::PointXYZRGBA>); // PointXYZRGBA型点云

    pcl::PCDReader reader;
    reader.read("pig2.pcd", *cloud); 

	pcl::visualization::CloudViewer viewer("Cloud Viewer"); // 创建一个可视化窗口

    viewer.showCloud(cloud); // 在窗口中显示点云
    
    while (!viewer.wasStopped ())
    {
    }

	// 体素网格滤波
	pcl::VoxelGrid<pcl::PointXYZRGBA> sor;
	sor.setInputCloud(cloud);
	sor.setLeafSize(0.005f, 0.005f, 0.005f);
	sor.filter(*cloud);

	pcl::visualization::CloudViewer viewer2("Cloud Viewer"); // 创建一个可视化窗口
    viewer2.showCloud(cloud); // 在窗口中显示点云
    
    while (!viewer2.wasStopped ())
    {
    }

	// 创建一个空的kd-tree
	pcl::search::KdTree<pcl::PointXYZRGBA>::Ptr tree (new pcl::search::KdTree<pcl::PointXYZRGBA> ());
  	// 创建一个normal点云
	pcl::PointCloud <pcl::Normal>::Ptr cloud_normals (new pcl::PointCloud <pcl::Normal>);

  	pcl::NormalEstimation<pcl::PointXYZRGBA, pcl::Normal> normal_estimator; // 创建法线估计对象
  	normal_estimator.setSearchMethod (tree); // 设置搜索方法
  	normal_estimator.setInputCloud (cloud); // 设置法线估计对象输入点集
  	normal_estimator.setRadiusSearch (0.02); // 使用半径在查询点周围2厘米范围内的所有邻元素
  	normal_estimator.compute (*cloud_normals); // 计算并输出法向量

  	pcl::RegionGrowing<pcl::PointXYZRGBA, pcl::Normal> reg; // 创造区域生长分割对象
  	reg.setMinClusterSize (50); // 设置一个聚类需要的最小点数,聚类小于阈值的结果将被舍弃
  	reg.setMaxClusterSize (1000000); //设置一个聚类需要的最大点数,聚类大于阈值的结果将被舍弃
  	reg.setSearchMethod (tree); // 设置搜索方法
  	reg.setResidualThreshold (0.03); // 设置搜索的近邻点数目
  	
	reg.setInputCloud (cloud); // 设置输入点云 
	reg.setInputNormals (cloud_normals); // 设置输入点云

  	reg.setSmoothnessThreshold (5 / 180.0 * M_PI); //设置平滑阀值
  	reg.setCurvatureThreshold (1); //设置曲率阀值


  	// 以下两行用于启动分割算法,并返回聚类向量
  	std::vector <pcl::PointIndices> clusters;
  	reg.extract (clusters); // 获取聚类的结果,分割结果保存在点云索引的向量中
 
  	//区域生长结果可视化

  	pcl::PointCloud <pcl::PointXYZRGB>::Ptr colored_cloud = reg.getColoredCloud ();
  	pcl::visualization::CloudViewer viewer3("Cloud Viewer"); // 创建一个可视化窗口
	viewer3.showCloud(colored_cloud);
  	while (!viewer3.wasStopped ())
  	{
	}
}

原始点云:
在这里插入图片描述
下采样后的点云:
在这里插入图片描述
区域生长分割后的点云:
在这里插入图片描述

二维图片中的Region Growing算法

原理

在这里插入图片描述
与三维区域生长算法不同的地方:

  • 不需要kd-tree搜索邻域,2D图片的区域生长算法生长方向为八个且方向固定,如上图所示。
  • 三维区域生长,是否为同一类看法线阈值,是否可以作为种子点看曲率阈值。而2D图片的区域生长这两个阈值为同一个灰度阈值。

opencv实现

#include <iostream>
#include <opencv2/opencv.hpp>

using namespace std;
using namespace cv;

Mat RegionGrow(Mat src, Point2i pt, int th)
{
    /*
    src:Mat类,输入图像
    pt:Point2i类,初始生长点
    th:int型,与初始生长点的灰度差在这个范围内则进行生长
    */
    
    int ptValue = 0;      /*当前生长点的灰度值*/
    Point2i pToGrowing;   /*待生长点位置*/
    int pToGrowLabel = 0; /*判断待生长点是否生长过*/
    int pToGrowValue = 0; /*当前生长点灰度值*/
    
    Mat growLabelImage = Mat::zeros(src.size(), CV_8UC1); /*创建和输入src等尺寸的空白区域,填充为黑色*/
    int DIR[8][2] = {{-1,-1}, {0,-1}, {1,-1}, {1,0}, {1,1}, {0,1}, {-1,1}, {-1,0}}; /*生长方向顺序数据*/
    vector<Point2i> ptVector; /*生长点栈*/
    ptVector.push_back(pt);   /*将初始生长点压入栈中*/
    growLabelImage.at<uchar>(pt.y, pt.x) = 255; /*初始生长点位置填充为白色*/
    ptValue = src.at<uchar>(pt.y, pt.x); /*记录初始生长点的灰度值*/

    while (!ptVector.empty()) /*生长栈为空则停止循环*/
    {
        pt = ptVector.back(); /*取出一个生长点*/
        ptVector.pop_back();  /*删除生长点栈中的当前生长点*/

        /*分别对八个方向上的点进行生长*/
        for (int i=0; i<8; ++i)
        {   
            pToGrowing.x = pt.x + DIR[i][0]; /*得到待生长点的位置*/
            pToGrowing.y = pt.y + DIR[i][1];
            
            /*检查是否是边界点,如果是,则跳出循环*/
            if (pToGrowing.x < 0 || pToGrowing.y < 0 || pToGrowing.x > (src.cols-1) || (pToGrowing.y > src.rows -1))
            {
                continue;
            }
            
            /*判断当前待生长点是否生长过*/
            pToGrowLabel = growLabelImage.at<uchar>(pToGrowing.y, pToGrowing.x);
            ptValue = src.at<uchar>(pt.y, pt.x) /*得到当前生长点的灰度值*/; 
            
            /*如果当前待生长点还没生长,进行区域生长,否则跳过*/
            if (pToGrowLabel == 0)                    
            {
                pToGrowValue = src.at<uchar>(pToGrowing.y, pToGrowing.x); /*得到当前待生长点的灰度值*/

                if (abs(ptValue - pToGrowValue) < th) /*在阈值范围内则生长*/
                {
                    growLabelImage.at<uchar>(pToGrowing.y, pToGrowing.x) = 255; /*生长过的点标记为白色*/
                    ptVector.push_back(pToGrowing); /*将下一个生长点压入栈中*/
                }
            }
        }
    }
    
    return growLabelImage.clone(); /*同一类的255,不是用一类是0*/
}

int main(int argc, char** argv)
{
    Mat srcImg = imread("test.jpg"); /*用opencv读取图片*/
    Mat graySrcImg;
    Mat srcImgClone = srcImg.clone();
    cvtColor(srcImgClone, graySrcImg, CV_BGR2GRAY); /*转化为灰度图像*/

    Mat LabelMask;

    /*得到区域生长算法返回的mask*/    
    LabelMask = RegionGrow(graySrcImg, Point2i(134, 70), 24); /*设置初始种子位置和阈值*/  
    
    Mat outputImage = srcImg.clone();

    for(int j=0; j<LabelMask.rows; ++j)
    {
		for(int k=0; k<LabelMask.cols; ++k)
		{
			if(LabelMask.at<uchar>(j,k) == 0)
            {
				outputImage.at<Vec3b>(j, k) = 0;
            }
		}
        
    }
    
    imshow("src", srcImg);
    imshow("gray", graySrcImg);
    imshow("mask", LabelMask);
    imshow("result", outputImage);
    
    waitKey(0);
    return 0;
}

原图:
在这里插入图片描述
灰度图:
在这里插入图片描述
区域生长算法返回的mask:
在这里插入图片描述
掩膜后的结果:
在这里插入图片描述

结语

如果您有修改意见或问题,欢迎留言或者通过邮箱和我联系。
手打很辛苦,如果我的文章对您有帮助,转载请注明出处。

发布了57 篇原创文章 · 获赞 19 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/Zhang_Chen_/article/details/101228569
今日推荐