三维计算机视觉(三)--点云分割

转自:http://www.cnblogs.com/ironstark/p/5000147.html

点云分割

  点云分割可谓点云处理的精髓,也是三维图像相对二维图像最大优势的体现。

  点云分割的目的提取点云中的不同物体,从而实现分而治之,突出重点,单独处理的目的。而在现实点云数据中,往往对场景中的物体有一定先验知识。比如:桌面墙面多半是大平面,桌上的罐子应该是圆柱体,长方体的盒子可能是牛奶盒......对于复杂场景中的物体,其几何外形可以归结于简单的几何形状。这为分割带来了巨大的便利,因为简单几何形状是可以用方程来描述的,或者说,可以用有限的参数来描述复杂的物体。而方程则代表的物体的拓扑抽象。于是,RanSaC算法可以很好的将此类物体分割出来。

RanSaC算法

  RanSaC算法(随机采样一致)原本是用于数据处理的一种经典算法,其作用是在大量噪声情况下,提取物体中特定的成分。下图是对RanSaC算法效果的说明。图中有一些点显然是满足某条直线的,另外有一团点是纯噪声。目的是在大量噪声的情况下找到直线方程,此时噪声数据量是直线的3倍。

三维计算机视觉(三)--点云分割_第1张图片

  如果用最小二乘法是无法得到这样的效果的,直线大约会在图中直线偏上一点。关于随机采样一致性算法的原理,在wiki百科上讲的很清楚,甚至给出了伪代码和matlab,C代码。见网址https://en.wikipedia.org/wiki/RANSAC. 我想换一个不那么严肃或者说不那么学术的方式来解释这个算法。

  实际上这个算法就是从一堆数据里挑出自己最心仪的数据。所谓心仪当然是有个标准(目标的形式:满足直线方程?满足圆方程?以及能容忍的误差e)。平面中确定一条直线需要2点,确定一个圆则需要3点。随机采样算法,其实就和小女生找男朋友差不多。

  1. 从人群中随便找个男生,看看他条件怎么样,然后和他谈恋爱,(平面中随机找两个点,拟合一条直线,并计算在容忍误差e中有多少点满足这条直线)
  2. 第二天,再重新找个男生,看看他条件怎么样,和男朋友比比,如果更好就换新的(重新随机选两点,拟合直线,看看这条直线是不是能容忍更多的点,如果是则记此直线为结果)
  3. 第三天,重复第二天的行为(循环迭代)
  4. 终于到了某个年龄,和现在的男朋友结婚(迭代结束,记录当前结果)

  显然,如果一个女生按照上面的方法找男朋友,最后一定会嫁一个好的(我们会得到心仪的分割结果)。只要这个模型在直观上存在,该算法就一定有机会把它找到。优点是噪声可以分布的任意广,噪声可以远大于模型信息。

  这个算法有两个缺点,第一,必须先指定一个合适的容忍误差e。第二,必须指定迭代次数作为收敛条件。

  综合以上特性,本算法非常适合从杂乱点云中检测某些具有特殊外形的物体。(作者这个例子,个人感觉有不太合适,有兴趣的可以查查具体的算法原理

PCL中基于RanSaC的点云分割方法

  PCL支持了大量几何模型的RanSaC检测,可以非常方便的对点云进行分割。其调用方法如下:

  

复制代码
  //创建一个模型参数对象,用于记录结果
  pcl::ModelCoefficients::Ptr coefficients (new pcl::ModelCoefficients);
  //inliers表示误差能容忍的点 记录的是点云的序号
  pcl::PointIndices::Ptr inliers (new pcl::PointIndices);
  // 创建一个分割器
  pcl::SACSegmentation seg;
  // Optional
  seg.setOptimizeCoefficients (true);
  // Mandatory-设置目标几何形状
  seg.setModelType (pcl::SACMODEL_PLANE);
  //分割方法:随机采样法
  seg.setMethodType (pcl::SAC_RANSAC);
  //设置误差容忍范围
  seg.setDistanceThreshold (0.01);
  //输入点云
  seg.setInputCloud (cloud);
  //分割点云
  seg.segment (*inliers, *coefficients);
复制代码

  除了平面以外,PCL几乎支持所有的几何形状。作为点云分割的基础算法,RanSaC很强大且必收敛,可以作为机器人抓取,识别等后续任务的前处理。

分割给人最直观的影响大概就是邻居和我不一样。比如某条界线这边是中华文明,界线那边是西方文,最简单的分割方式就是在边界上找些居民问:"小伙子,你到底能不能上油管啊?”。然后把能上油管的居民坐标连成一条线,自然就区分开了两个地区。也就是说,除了之前提到的基于采样一致的分割方式以外,应该还存在基于邻近搜索的分割方式。通过对比某点和其最近一点的某些特征,来实现点云的分割。图像所能提供的分割信息仅是灰度或RGB向量,而三维点云却能够提供更多的信息。故点云在分割上的优势是图像所无法比拟的(重要的事情要说三遍)。

1.谁是我邻居--kdTree&OcTree

  由于分割工作需要对点云的邻近点进行操作,不断对比和访问某个点的邻居,所以决定点云的相邻关系是非常重要的。对于Scan来说,邻居关系是天然的。但对于很多杂乱点云,或者滤波,分割后的点云来说,邻居关系就已经被破坏了。确定一个点云之间的相邻关系可以通过“树”来完成,目前比较主流的方法包括:kdTree和OcTree,这两种方法各有特点。

1.1.kdTree---一种递归的邻近搜索策略

  关于kdTree到底是怎么工作的https://en.wikipedia.org/wiki/K-d_tree这里有非常详细的说明,我不再赘述。但是kdTree实际上包括两个部分:1.建立kdTree,2.在kdTree中查找。建立kdTree实际上是一个不断划分的过程,首先选择最sparse的维度,然后找到该维度上的中间点,垂直该维度做第一次划分。此时k维超平面被一分为二,在两个子平面中再找最sparse的维度,依次类推知道最后一个点也被划分。那么就形了一个不断二分的树。如图所示。

三维计算机视觉(三)--点云分割_第2张图片  

  显然,一般情况下一个点的邻近点只需要在其父节点和子节点中搜索即可,大大缩小了邻近点的搜索规模。并且kdtree可以有效的对插入点进行判断其最近点在哪个位置。对于低层次视觉来说kdTree算法是非常重要的。在很多情况下需要给出某个点,再查k临近点的编号,或者差某半径范围内的点。PCL已经实现了kdtree算法,其调用接口如下:

复制代码
  #include 
  #include 



   //创建kdtree 结构
  pcl::KdTreeFLANN kdtree;
  //传入点云
  kdtree.setInputCloud (cloud);
  //设置输入点
  pcl::PointXYZ searchPoint;
   //k邻近搜索
   int K = 10;
   //设置两个容器,第一个放点的标号,第二个点到SearchPoint的距离
   std::vector<int> pointIdxNKNSearch(K);
   std::vector<float> pointNKNSquaredDistance(K);
   //进行搜索,注意,此函数有返回值>0为找到,<0则没找到
   kdtree.nearestKSearch (searchPoint, K, pointIdxNKNSearch, pointNKNSquaredDistance)
    


   //    基于距离的搜索    //
  //两个未知大小的容器,作用同上
  std::vector<int> pointIdxRadiusSearch;
  std::vector<float> pointRadiusSquaredDistance;
  // 搜索半径
  float radius = 3;
  //搜索,效果同上
  kdtree.radiusSearch (searchPoint, radius, pointIdxRadiusSearch, pointRadiusSquaredDistance)
复制代码

   显然,我们还需要一个算法把Idx里的点云数据提取出来进行重新着色之类的工作,代码可以写作:

  

复制代码
    pcl::PointCloud::Ptr Npoints(new pcl::PointCloud);
    Npoints->height=1;
    Npoints->width=searchindice.size();
    Npoints->resize (searchindice.size());
    //注意此清空操作非常极其以及特别重要,否则Npoints中会有莫名奇妙的点。
    Npoints->clear();
    int PointNUM = 0;
    for(int i=0;ii)
    {   
        PointNUM = searchindice[i];
        Npoints->push_back(cloud->points[PointNUM]);
    //    cout<points[PointNUM].x<<"  "<points[PointNUM].y<<"  "<points[PointNUM].z<<"  "<
    }

    pcl::visualization::PointCloudColorHandlerCustom Npoints_color_handler (Npoints, 0, 255, 0);
    viewer->addPointCloud(Npoints,Npoints_color_handler,"Npoints");
    viewer->setPointCloudRenderingProperties (pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 5, "Npoints");
复制代码

 

1.2 OcTree

  OcTree是一种更容易理解也更自然的思想。对于一个空间,如果某个角落里有个盒子我们却不知道在哪儿。但是"神"可以告诉我们这个盒子在或者不在某范围内,显而易见的方法就是把空间化成8个卦限,然后询问在哪个卦限内。再将存在的卦限继续化成8个。意思大概就是太极生两仪,两仪生四象,四象生八卦,就这么一直划分下去,最后一定会确定一个非常小的空间。对于点云而言,只要将点云的立方体凸包用octree生成很多很多小的卦限,那么在相邻卦限里的点则为相邻点。

  显然,对于不同点云应该采取不同的搜索策略,如果点云是疏散的,分布很广泛,且每什么规律(如lidar测得的点云或双目视觉捕捉的点云)kdTree能更好的划分,而octree则很难决定最小立方体应该是多少。太大则一个立方体里可能有很多点云,太小则可能立方体之间连不起来。如果点云分布非常规整,是某个特定物体的点云模型,则应该使用ocTree,因为很容易求解凸包并且点与点之间相对距离无需再次比对父节点和子节点,更加明晰。典型的例子是斯坦福的兔子。

2.邻居,咱俩关系近么---欧几里得与区域生长算法

  基于欧式距离的分割和基于区域生长的分割本质上都是用区分邻里关系远近来完成的。由于点云数据提供了更高维度的数据,故有很多信息可以提取获得。欧几里得算法使用邻居之间距离作为判定标准,而区域生长算法则利用了法线,曲率,颜色等信息来判断点云是否应该聚成一类。

2.1.欧几里得算法

  算法的原理在PCL相关的教程中已经说的比较清楚了,我不再给出伪代码。我想用一个故事来讲讲这个问题。从前有一个脑筋急转弯,说一个锅里有两粒豆子,如果不用手,要怎么把它们分开。当时的答案是豆子本来就是分开的,又没黏在一起,怎么不叫分开。OK,实际上欧几里德算法就是这个意思。两团点云就像是两粒豆子,只要找到某个合适的度量方式,就有办法把点云和点云分开。区分豆子我们用的方法可以归结于,两个豆子之间的距离小于分子距离,所以它们并没有连在一起。如果两团点云之间最近两点的距离小于单个点云内部点之间的距离,则可以由算法判断其分为两类。假设总点云集合为A,聚类所得点云团为Q

  具体的实现方法大致是:

  1. 找到空间中某点p10,有kdTree找到离他最近的n个点,判断这n个点到p的距离。将距离小于阈值r的点p12,p13,p14....放在类Q里
  2. 在 Q\p10 里找到一点p12,重复1
  3. 在 Q\p10,p12 找到找到一点,重复1,找到p22,p23,p24....全部放进Q里
  4. 当 Q 再也不能有新点加入了,则完成搜索了

  听起来好像这个算法并没什么用,因为点云总是连成片的,很少有什么东西会浮在空中让你来分。但是如果和前面介绍的内容联系起来就会发现这个算法威力巨大了。比如

  1. 半径滤波删除离群点
  2. 采样一致找到桌面
  3. 抽掉桌面。。。。。

  显然,一旦桌面被抽,桌上的物体就自然成了一个个的浮空点云团。就能够直接用欧几里德算法进行分割了。如图所示。

三维计算机视觉(三)--点云分割_第3张图片

  PCL对欧几里德算法进行了很好的封装,其代码如下:

  

复制代码
  //被分割出来的点云团(标号队列)
  std::vector cluster_indices;
  //欧式分割器
  pcl::EuclideanClusterExtraction ec;
  ec.setClusterTolerance (0.02); // 2cm
  ec.setMinClusterSize (100);
  ec.setMaxClusterSize (25000);
  //搜索策略树
  ec.setSearchMethod (tree);
  ec.setInputCloud (cloud_filtered);
  ec.extract (cluster_indices);
复制代码

2.2 区域生长算法

  区域生长算法直观感觉上和欧几里德算法相差不大,都是从一个点出发,最终占领整个被分割区域。毛主席说:“星星之火,可以燎原” 就是这个意思。欧几里德算法是通过距离远近,来判断烧到哪儿。区域生长算法则不然,烧到哪儿靠燃料(点)的性质是否类似来决定。对于普通点云,其可由法线、曲率估计算法获得其法线和曲率值。通过法线和曲率来判断某点是否属于该类。其算法可以总结为:

  1. 种子周围的点和种子相比
  2. 法线方向是否足够相近
  3. 曲率是否足够小
  4. 如果满足1,2则该点可用做种子
  5. 如果只满足1,则归类而不做种
  6. 从某个种子出发,其“子种子”不再出现则一类聚集完成
  7. 类的规模既不能太大也不能太小

  显然,上述算法是针对小曲率变化面设计的。尤其适合对连续阶梯平面进行分割:比如SLAM算法所获得的建筑走廊。

  三维计算机视觉(三)--点云分割_第4张图片

  PCL对区域生长算法有如下封装:

复制代码
  //一个点云团队列,用于存放聚类结果
  std::vector  clusters;
  //区域生长分割器
  pcl::RegionGrowing reg;
  
  //输入分割目标
  reg.setSearchMethod (tree);
  reg.setNumberOfNeighbours (30);
  reg.setInputCloud (cloud);
  //reg.setIndices (indices);
  reg.setInputNormals (normals);
  
  //设置限制条件及先验知识
  reg.setMinClusterSize (50);
  reg.setMaxClusterSize (1000000);
  reg.setSmoothnessThreshold (3.0 / 180.0 * M_PI);
  reg.setCurvatureThreshold (1.0);

  reg.extract (clusters);
复制代码

   除了普通点云之外,还有一种特殊的点云,成为RGB点云。显而易见,这种点云除了结构信息之外,还存在颜色信息。将物体通过颜色分类,是人类在辨认果实的过程中进化出的能力,颜色信息可以很好的将复杂场景中的特殊物体分割出来。而颜色点云也并不那么遥不可及,Xbox Kinect就可以轻松的捕捉颜色点云。基于颜色的区域生长分割原理上和基于曲率,法线的分割方法是一致的。只不过比较目标换成了颜色,去掉了点云规模上限的限制。可以认为,同一个颜色且挨得近,是一类的可能性很大,不需要上限来限制。所以这种方式比较适合用于室内场景分割。尤其是复杂室内场景,颜色分割可以轻松的将连续的场景点云变成不同的物体。哪怕是高低不平的地面,没法用采样一致分割器抽掉,颜色分割算法同样能完成分割任务。

三维计算机视觉(三)--点云分割_第5张图片

  基于PCL的实现方式如下:

复制代码
  //用于存放点云团的容器
  std::vector  clusters;
  //颜色分割器
  pcl::RegionGrowingRGB reg;
  reg.setInputCloud (cloud);
  //点云经过了滤波器的预处理,提取了indices
  reg.setIndices (indices);
  reg.setSearchMethod (tree);
  reg.setDistanceThreshold (10);
  //点与点之间颜色容差
  reg.setPointColorThreshold (6);
  //苹果都是红的,哪怕离散的苹果也应该是一类
  reg.setRegionColorThreshold (5);
  reg.setMinClusterSize (600);

  reg.extract (clusters);
复制代码
最小割算法

 

1.点云分割的精度

  在之前的两个章节里介绍了基于采样一致的点云分割和基于临近搜索的点云分割算法。基于采样一致的点云分割算法显然是意识流的,它只能割出大概的点云(可能是杯子的一部分,但杯把儿肯定没分割出来)。基于欧式算法的点云分割面对有牵连的点云就无力了(比如风筝和人,在不用三维形态学去掉中间的线之前,是无法分割风筝和人的)。基于法线等信息的区域生长算法则对平面更有效,没法靠它来分割桌上的碗和杯子。也就是说,上述算法更关注能不能分割,除此之外,我们还需要一个方法来解决分割的“好不好”这个问题。也就是说,有没有哪种方法,可以在一个点不多,一个点不少的情况下,把目标和“其他”分开。

  答案是有,也就是这篇博文要解决的最小割算法。

2.最小割算法

  最小割(min-cut)并不是一个什么很新鲜的东西。它早就用在网络规划,求解桥问题,图像分割等领域,被移植到点云分割上也不足为奇。最小割算法是图论中的一个概念,其作用是以某种方式,将两个点分开,当然这两个点中间可能是通过无数的点再相连的。如图所示。

三维计算机视觉(三)--点云分割_第6张图片

  如果要分开最左边的点和最右边的点,红绿两种割法都是可行的,但是红线跨过了三条线,绿线只跨过了两条。单从跨线数量上来论可以得出绿线这种切割方法更优的结论。但假设线上有不同的权值,那么最优切割则和权值有关了。它到底是怎么找到那条绿线的暂且不论。总而言之,就是有那么一个算法,当你给出了点之间的 “图” (广义的),以及连线的权值时,最小割算法就能按照你的要求把图分开。

3.点云 “图”

  显而易见,切割有两个非常重要的因素,第一个是获得点与点之间的拓扑关系,也就是生成一张“图”。第二个是给图中的连线赋予合适的权值。只要这两个要素合适,最小割算法就会办好剩下的事情。点云是一种非常适合分割的对象(我第三次强调这个事情了),点云有天然分开的点。有了点之后,只要把点云中所有的点连起来就可以了。连接算法如下:

  1. 找到每个点最近的n个点
  2. 将这n个点和父点连接
  3. 找到距离最小的两个块(A块中某点与B块中某点距离最小),并连接
  4. 重复3,直至只剩一个块

  现在已经有了“图”,只要给图附上合适的权值,就完成了所有任务。物体分割给人一个直观印象就是属于该物体的点,应该相互之间不会太远。也就是说,可以用点与点之间的欧式距离来构造权值。所有线的权值可映射为线长的函数。

  貌似我们现在已经搞定一切了,其实不然。分割总是有一个目标的,而这种精准打击的算法,显然你要告诉我打击对象是谁,打击范围多大——目标需要人为指定(center),尺寸需要提前给出(radius)。

  OK,我们现在有了打击对象了(指定了目标物体上的一个点),接下来要做的,就是让除此对象之外的物体被保护起来,不受到打击。保护的方法就是认为加重目标范围之外的权值(罚函数)

 

  

  上述过程其实看起来还不够智能,如果有办法让我只需要点一下鼠标,选中要分割的物体,接下来电脑替我操心其他事情,那就太好了。这其实是可以实现的,称为AutoMatic Regime.但PCL并没有封装这个算法,忽略不表。

4.PCL对最小割算法的实现

  

复制代码
  //生成分割器
  pcl::MinCutSegmentation seg;
  //分割输入分割目标
  seg.setInputCloud (cloud);
  //指定打击目标(目标点)
  pcl::PointCloud::Ptr foreground_points(new    pcl::PointCloud ());
  pcl::PointXYZ point;
  point.x = 68.97;
  point.y = -18.55;
  point.z = 0.57;
  foreground_points->points.push_back(point);
  seg.setForegroundPoints (foreground_points);
  //指定权函数sigma
  seg.setSigma (0.25);
  //物体大概范围
  seg.setRadius (3.0433856);
  //用多少生成图
  seg.setNumberOfNeighbours (14);
  //和目标点相连点的权值(至少有14个)
  seg.setSourceWeight (0.8);
  //分割结果
  std::vector  clusters;
  seg.extract (clusters);
复制代码

  显然,最小割算法更注重分割的精确性而不是分割自动进行。最小割算法用于半自动分割识别有着巨大的优势,适合用于计算机视觉,城市场景点云分析一类。但对机器人来说,或许和特征点检测算法联合起来能获得较好的效果。

三维计算机视觉(三)--点云分割_第7张图片  

  图中显示,最小割算法成功找到了靠的很近的汽车。显然欧式算法r取太大则无法区分左右汽车,r取太小则无法区分车头和车身(玻璃不反光,是没有点云的)。

1.超体聚类——一种来自图像的分割方法

  超体(supervoxel)是一种集合,集合的元素是“体”。与体素滤波器中的体类似,其本质是一个个的小方块。与之前提到的所有分割手段不同,超体聚类的目的并不是分割出某种特定物体,其对点云实施过分割(over segmentation),将场景点云化成很多小块,并研究每个小块之间的关系。这种将更小单元合并的分割思路已经出现了有些年份了,在图像分割中,像素聚类形成超像素,以超像素关系来理解图像已经广为研究。本质上这种方法是对局部的一种总结,纹理,材质,颜色类似的部分会被自动的分割成一块,有利于后续识别工作。比如对人的识别,如果能将头发,面部,四肢,躯干分开,则能更好的对各种姿态,性别的人进行识别。

  点云和图像不一样,其不存在像素邻接关系。所以,超体聚类之前,必须以八叉树对点云进行划分,获得不同点团之间的邻接关系。与图像相似点云的邻接关系也有很多,如面邻接,线邻接,点邻接。其具体解释如下图:

三维计算机视觉(三)--点云分割_第8张图片

  基于超体聚类的点云分割,使用点邻接(蓝色)作为相邻判据。

2.超体聚类的实现步骤

 举个简单的例子来体会下超体聚类,其过程和结晶类似。但不是水结晶成冰,而是盐溶液过饱和状态下的多晶核结晶。所有的晶核(seed)同时开始生长,最终填满整个空间,使物质具有晶体结构。 超体聚类实际上是一种特殊的区域生长算法,和无限制的生长不同,超体聚类首先需要规律的布置区域生长“晶核”。晶核在空间中实际上是均匀分布的,并指定晶核距离(Rseed)。再指定粒子距离(Rvoxel)。再指定最小晶粒(MOV),过小的晶粒需要融入最近的大晶粒。关系如图所示:

三维计算机视觉(三)--点云分割_第9张图片

  有了晶粒和结晶范围之后,我们只需要控制结晶过程,就能将整个空间划分开了。结晶过程的本质就是不断吸纳类似的粒子(八分空间)。类似是一个比较模糊的概念,关于类似的定义有以下公式:

 三维计算机视觉(三)--点云分割_第10张图片

  公式中的Dc,表示颜色上的差异,Dn表示法线上的差异,Ds代表点距离上的差异。w_*表示一系列权重。用于控制结晶形状。在晶核周围寻找一圈,D最小的体素被认为是下一个“被发展的党员”。需要注意的是,结晶过程并不是长完一个晶核再长下一个,二是所有的晶核同时开始生长(虽然计算机计算时必然有先后,但从层次上来说是同时的)。其生长顺序如下图所示:

三维计算机视觉(三)--点云分割_第11张图片

  接下来所有晶核继续公平竞争,发展第二个“党员”,以此循环,最终所有晶体应该几乎同时完成生长。整个点云也被晶格所分割开来。并且保证了一个晶包里的粒子都是类似的。

3.PCL对超体聚类的实现

复制代码
  //设定结晶参数
  float voxel_resolution = 0.008f;
  float seed_resolution = 0.1f;
  float color_importance = 0.2f;
  float spatial_importance = 0.4f;
  float normal_importance = 1.0f;
  
  //生成结晶器
  pcl::SupervoxelClustering super (voxel_resolution, seed_resolution);
  //和点云形式有关
  if (disable_transform)
    super.setUseSingleCameraTransform (false);
  //输入点云及结晶参数
  super.setInputCloud (cloud);
  super.setColorImportance (color_importance);
  super.setSpatialImportance (spatial_importance);
  super.setNormalImportance (normal_importance);
  //输出结晶分割结果:结果是一个映射表
 std::map ::Ptr > supervoxel_clusters;
  super.extract (supervoxel_clusters);
  //获得晶体中心
  PointCloudT::Ptr voxel_centroid_cloud = super.getVoxelCentroidCloud (); //获得晶体   PointLCloudT::Ptr labeled_voxel_cloud = super.getLabeledVoxelCloud (); 
复制代码

   执行上诉过程后,会将晶体映射成一系列数。数代表的是指向各个晶体的指针。可以通过getter函数,把晶体有关的信息拖出来。拖出来的是点云。

复制代码
  //将相连的晶体中心连起来并显示 
  std::multimap<uint32_t, uint32_t> supervoxel_adjacency; super.getSupervoxelAdjacency (supervoxel_adjacency);   std::multimap::iterator label_itr = supervoxel_adjacency.begin ();
for ( ; label_itr != supervoxel_adjacency.end (); )
  {
    //First get the label
    uint32_t supervoxel_label = label_itr->first;
    //Now get the supervoxel corresponding to the label
    pcl::Supervoxel::Ptr supervoxel = supervoxel_clusters.at (supervoxel_label);

    //Now we need to iterate through the adjacent supervoxels and make a point cloud of them
    PointCloudT adjacent_supervoxel_centers;
    std::multimap::iterator adjacent_itr = supervoxel_adjacency.equal_range (supervoxel_label).first;
    for ( ; adjacent_itr!=supervoxel_adjacency.equal_range (supervoxel_label).second; ++adjacent_itr)
    {
      pcl::Supervoxel::Ptr neighbor_supervoxel = supervoxel_clusters.at (adjacent_itr->second);
      adjacent_supervoxel_centers.push_back (neighbor_supervoxel->centroid_);
    }
    //Now we make a name for this polygon
    std::stringstream ss;
    ss << "supervoxel_" << supervoxel_label;
    //This function is shown below, but is beyond the scope of this tutorial - basically it just generates a "star" polygon mesh from the points given
    addSupervoxelConnectionsToViewer (supervoxel->centroid_, adjacent_supervoxel_centers, ss.str (), viewer);
    //Move iterator forward to next label
    label_itr = supervoxel_adjacency.upper_bound (supervoxel_label);
  }
复制代码

       至此,生成了不同的晶体之间的邻接关系。结果如下所示(不同晶核距离0.1m,0.05m)

三维计算机视觉(三)--点云分割_第12张图片

  此方法主要为识别做前期准备,但我认为,这种东西用在三维视觉+有限元倒是极好的。可以在不使用应变片的前提下对物体各个部分应变进行直接测量。在已知力的情况下可以建立物体刚度和应变的关系,貌似钢包回转台的手里分析可以这样解决。蛋疼的是实际工业机械哪有那么多花花绿绿的给你分割,很难形成有效的对应点匹配。

1.航空测量与点云的形态学

  航空测量是对地形地貌进行测量的一种高效手段。生成地形三维形貌一直是地球学,测量学的研究重点。但对于城市,森林,等独特地形来说,航空测量会受到影响。因为土地表面的树,地面上的房子都认为的改变了地貌,可以认为是地貌上的噪声点。设计一种有效的手段去除地面噪声对地形测量的影响显得非常重要。这种工作可以认为是一种特殊的点云分割,一般情况下点云分割的目标是去除地面,而这种方法需要在不使用地面平整假设的前提下获得地面。

  形态学是图像处理中非常重要的概念,对二值图像而言,可由简单的膨胀运算和腐蚀运算组成一个完整的图像处理族。但是想要将这个算法移植到三维点云上是比较难的,首先一般的点云没有明显的映射值,也没有清晰的定义域,很难设计形态学处理的基理。但是LIDAR点云例外。由于LIDAR点云由飞机获得,飞机距离地面相对较远,且测量方向和地面垂直。这就形成了比较完整的xy->z映射(z方向的范围远远小于xy方向),z方向代表地面物体的高度,x,y方向为平行与地面且相互垂直的两个轴。有了明确的定义域以及单值映射关系就有了设计形态学算法的基本要素。实际上除了形态学算法之外,许多图像处理算法都可以用来分割LIDAR点云了,本质上这就是一幅大图像。

2.三维形态学算子

  对于图像而言,形态学运算一般是针对二值图像而言的。当然也有针对灰度的形态学运算,其原理应该和针对点云的形态学运算类似(我猜的)。形态学算子的设计实际上非常简单,只要能设计出基础的膨胀和腐蚀算子就可以组合得到一系列的处理。

  其中,d表示膨胀算子,e表示腐蚀算子。算子的原理有些像中值滤波,通过选取一个窗w中最高点或最低点来完成图像的膨胀和腐蚀,其效果如下图所示:

  三维计算机视觉(三)--点云分割_第13张图片

  在航拍图的横截面上可以很清楚的看出膨胀与腐蚀的效果。对于房子和树可以用不同尺度窗(从小到大)先腐蚀至地面。但是这会导致一个巨大的问题。。。如果地面上有个土包(比如秦始皇陵),那么这个土包也会在一次次的腐蚀中被消耗。那岂不是秦始皇陵就发现不了?所以还有一个补偿算法用于解决这个问题,称为线性补偿算法。

  建筑物和土包有一个巨大的区别,建筑物往往相对比较陡峭,而土包却是变化比较平缓的。这个可以作为一个判据,用于判断物体是否需要被腐蚀,也作为窗收敛的判据。

  式中k称为斜率,代表下一个窗的大小是上一个窗的2^k倍

  s是一个因子

  dh是切深判据,每一次腐蚀大于切深判据才认为是有效的,小于切深判据则是土包。

  上述公式是怎么发现的就需要问论文作者了,所有材料都被收录于文章:

  A Progressive Morphological Filter for Removing Nonground Measurements From Airborne LIDAR Data

 

3.PCL对本算法的实现

  

复制代码
  //生成形态滤波器
  pcl::ProgressiveMorphologicalFilter pmf;
  pmf.setInputCloud (cloud);
  //设置窗的大小以及切深,斜率信息
  pmf.setMaxWindowSize (20);
  pmf.setSlope (1.0f);
  pmf.setInitialDistance (0.5f);
  pmf.setMaxDistance (3.0f);
  //提取地面
  pmf.extract (ground->indices);

  // 从标号到点云
  pcl::ExtractIndices extract;
  extract.setInputCloud (cloud);
  extract.setIndices (ground);
  extract.filter (*cloud_filtered);
复制代码
View Code

  算法效果如图:

  三维计算机视觉(三)--点云分割_第14张图片

  

1.图像分割的两条思路

  场景分割是机器视觉中的重要任务,尤其对家庭机器人而言,优秀的场景分割算法是实现复杂功能的基础。但是大家搞了几十年也还没搞定——不是我说的,是接下来要介绍的这篇论文说的。图像分割的搞法大概有两种:剑宗——自低向上:先将图像聚类成小的像素团再慢慢合并,气宗——自顶向下:用多尺度模板分割图像,再进一步将图像优化分割成不同物体。当然,还有将二者合而为一的方法:training with data set. 这第三种方法也不好,太依赖于已知的物体而失去了灵活性。家庭机器人面对家里越来越多的东西需要一种非训练且效果很好的分割法。  Object Partitioning using Local Convexity 一文的作者从古籍中(也不老,1960s左右吧),找到了一种基于凹凸性的分割方法。实际上基于凹凸的图像理解在之前是被研究过的,但是随着神经网络的出现,渐渐这种从明确物理意义入手的图像"理解"方法就被淹没了。对于二维图像而言,其凹凸性较难描述,但对于三维图像而言,凹凸几乎是与生俱来的性质。

2.LCCP方法 

  LCCP是Locally Convex Connected Patches的缩写,翻译成中文叫做 ”局部凸连接打包一波带走“~~~算法大致可以分成两个部分:1.基于超体聚类的过分割。2.在超体聚类的基础上再聚类。超体聚类作为一种过分割方法,在理想情况下是不会引入错误信息的,也就是说适合在此基础上再进行处理。LCCP方法并不依赖于点云颜色,所以只使用空间信息和法线信息,wc=0。ws=1,wn=4。

2.1算法理论

  点云完成超体聚类之后,对于过分割的点云需要计算不同的块之间凹凸关系。凹凸关系通过 CC(Extended Convexity Criterion) SC (Sanity criterion)判据来进行判断。其中 CC 利用相邻两片中心连线向量与法向量夹角来判断两片是凹是凸。显然,如果图中a1>a2则为凹,反之则为凸。

三维计算机视觉(三)--点云分割_第15张图片

  考虑到测量噪声等因素,需要在实际使用过程中引入门限值(a1需要比a2大出一定量)来滤出较小的凹凸误判。此外,为去除一些小噪声引起的误判,还需要引入“第三方验证”,如果某块和相邻两块都相交,则其凹凸关系必相同。CC 判据最终如CCe:

  

  如果相邻两面中,有一个面是单独的,cc判据是无法将其分开的。举个简单的例子,两本厚度不同的书并排放置,视觉算法应该将两本书分割开。如果是台阶,则视觉算法应该将台阶作为一个整体。本质上就是因为厚度不同的书存在surface-singularities。为此需要引入SC判据,来对此进行区分。

  三维计算机视觉(三)--点云分割_第16张图片

  如图所示,相邻两面是否真正联通,是否存在单独面,与θ角有关,θ角越大,则两面真的形成凸关系的可能性就越大。据此,可以设计SC判据:

  其中S(向量)为两平面法向量的叉积。

  最终,两相邻面之间凸边判据为:

  

  在标记完各个小区域的凹凸关系后,则采用区域增长算法将小区域聚类成较大的物体。此区域增长算法受到小区域凹凸性限制,既:

  只允许区域跨越凸边增长。

  至此,分割完成,在滤去多余噪声后既获得点云分割结果。此外:考虑到RGB-D图像随深度增加而离散,难以确定八叉树尺寸,故在z方向使用对数变换以提高精度。分割结果如图:

  三维计算机视觉(三)--点云分割_第17张图片

                        三维计算机视觉(三)--点云分割_第18张图片

  从图中可知,纠缠在一起,颜色形状相近的物体完全被分割开了,如果是图像分割要达到这个效果,那就。。。。。呵呵呵。。。。

 

2.2 PCL的实现

  官网并未给出具体实现并测试,我不对以下代码有效性负责。

  1.超体聚类

复制代码
  //设定结晶参数
  float voxel_resolution = 0.008f;
  float seed_resolution = 0.1f;
  float color_importance = 0.2f;
  float spatial_importance = 0.4f;
  float normal_importance = 1.0f;
  
  //生成结晶器
  pcl::SupervoxelClustering super (voxel_resolution, seed_resolution);
  //和点云形式有关
  if (disable_transform)
    super.setUseSingleCameraTransform (false);
  //输入点云及结晶参数
  super.setInputCloud (cloud);
  super.setColorImportance (color_importance);
  super.setSpatialImportance (spatial_importance);
  super.setNormalImportance (normal_importance);
  //输出结晶分割结果:结果是一个映射表
  std::map ::Ptr > supervoxel_clusters;
  super.extract (supervoxel_clusters);
  std::multimap<uint32_t, uint32_t> supervoxel_adjacency; super.getSupervoxelAdjacency (supervoxel_adjacency);
复制代码

  2.LCCP

  

复制代码
//生成LCCP分割器
pcl::LCCPSegmentation::LCCPSegmentation LCCPseg;
//输入超体聚类结果
seg.setInputSupervoxels(supervoxel_clusters,supervoxel_adjacency);
//CC效验beta值
seg.setConcavityToleranceThreshold (concavity_tolerance_threshold);
//CC效验的k邻点
seg.setKFactor (k_factor_arg)
//
seg.setSmoothnessCheck (bool_use_smoothness_check_arg,voxel_res_arg,seed_res_arg,smoothness_threshold_arg = 0.1);
//SC效验
seg.setSanityCheck (bool_use_sanity_criterion_arg);
//最小分割尺寸
seg.setMinSegmentSize (min_segment_size_arg)

seg.segment();
seg.relabelCloud (pcl::PointCloud &labeled_cloud_arg);
复制代码

  综上所述,LCCP算法在相似物体场景分割方面有着较好的表现,对于颜色类似但棱角分明的物体可使用该算法。(比如X同学仓库里那一堆纸箱)

 

3.CPC方法

  CPC方法的全称为Constrained Planar Cuts(约束平面分割),出自论文:Constrained Planar Cuts - Object Partitioning for Point Clouds 。和LCCP方法不同,此方法的分割对象是object。此方法能够将物体分成有意义的块:比如人的肢体等。CPC方法可作为AI的前处理,作为RobotVision还是显得有些不合适。但此方法不需要额外训练,自底向上的将三维图像分割 成有明确意义部分,是非常admirable的。

3.1 CPC方法原理

  和其他基于凹凸性的方法相同,本方法也需要先进行超体聚类。在完成超体聚类之后,采用和LCCP相同的凹凸性判据获得各个块之间的凹凸关系。在获得凹凸性之后,CPC方法所采取的措施是不同的。其操作称为半全局分割 

  在分割之前,首先需要生成 EEC(Euclidean edge cloud), EEC的想法比较神奇,因为凹凸性定义在相邻两个”片“上,换言之,定义在连接相邻两“片”的edge上。将每个edge抽象成一个点云,则得到了附带凹凸信息的点云。如图所示,左图是普通点云,但附带了邻接和凹凸信息。右边是EEC,对凹边赋权值1,其他为0。

      此方法称作  weighted RanSac

三维计算机视觉(三)--点云分割_第19张图片

  

  显而易见,某处如果蓝色的点多,那么就越 ,就越应该切开(所谓切开实际上是用平面划分)。问题就转化为利用蓝点求平面了。利用点云求一个最可能的平面当然需要请出我们的老朋友RanSaC . 但此处引入一个评价函数,用于评价此次分割的优良程度Sm,Pm 是EEC中的点.

  单纯的weighted RanSac算法并不够。其会导致对某些图形的错误分割,所以作者对此做了第一次“修补".错误的分割如下图所示

三维计算机视觉(三)--点云分割_第20张图片

  此修补方法称作 directional weighted RanSac

  方法的原理很简单,垂直于凹边表面的点具有更高的权重,显然,对于EEC中的凹点,只要取其少量邻点即可估计垂直方向。

  这种修补后还有一个问题,如果这个分割面过长的情况下,有可能会误伤。如图所示:

三维计算机视觉(三)--点云分割_第21张图片

  于是有了第二种修补方法,称为:Locally constrained cutting

  这种修补方法的原理就更加简单粗暴了,对凹点先进行欧式分割(限制增长上限),之后再分割所得的子域里进行分割。

  在修修补补之后,CPC算法终于可以投入使用了,从测试集的结果来看,效果还是很好的。

三维计算机视觉(三)--点云分割_第22张图片

3.2 PCL的实现

  在PCL中CPC类继承自 LCCP 类,但是这个继承我觉得不好,这两个类之间并不存在抽象与具体的关系,只是存在某些函数相同而已。不如多设一个 凹凸分割类 作为CPC类与LCCP类的父类,所有的输入接口等都由凹凸分割类提供。由CPC算法和LCCP算法继承凹凸类,作为 凹凸分割 的具体实现。毕竟和 凹凸分割 有关的算法多半是对整体进行分割,和其他点云分割算法区别较大。

  

复制代码
//生成CPC分割器
pcl::CPCSegmentation::CPCSegmentation seg;
//输入超体聚类结果
seg.setInputSupervoxels(supervoxel_clusters,supervoxel_adjacency);
//设置分割参数
setCutting (max_cuts = 20,
                 cutting_min_segments = 0,
                 cutting_min_score = 0.16,
                 locally_constrained = true,
                 directed_cutting = true,
                 clean_cutting = false);
seg.setRANSACIterations (ransac_iterations);
seg.segment();
seg.relabelCloud (pcl::PointCloud &labeled_cloud_arg);
复制代码

你可能感兴趣的:(立体视觉)