【C++】pcl中的Region Growing(区域生长)算法
使用PCA估计一个点云的表面法线
原理
在【机器学习】 PCA(Principal Component Analysis)——主成分分析中我们说过,对数据集
,
,
是一组基向量,
是
的另一种表现形式。那么对二维数据来说,
就是新的坐标轴,
就是数据集
在这个新坐标轴上的坐标。数据集
在这个新坐标
轴(数据方差最小的轴)上的投影就是二维数据降维到一维数据的结果。对于三维数据来说也是如此,可以想象在三维空间新建一个坐标系,原数据集在
平面(数据方差最小的面)上的投影面,就是三维数据降维到二维数据的结果。而这些点的法线,近似等于这个面的法线。
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来计算法线的方向同样具有二义性。如果实际知道视点
,那么这个问题的解决是非常简单的。对所有法线定向只需要使它们一致朝向视点方向,即满足:
但是如果数据集是从多个捕获视点中配准后集成的,那么上述方法将不再适用。
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:
掩膜后的结果:
结语
如果您有修改意见或问题,欢迎留言或者通过邮箱和我联系。
手打很辛苦,如果我的文章对您有帮助,转载请注明出处。