使用OpenCV库识别3D点云的2D截面中的椭圆弧

随着能够接收3D点云(3dOT的价格实惠的激光扫描仪(激光雷达)的广泛传播以及该技术在各个领域(从机械工程到安全性,从石油工业到建筑)的广泛应用,人们对处理算法的兴趣重新燃起。点云。3d在工业

上的流行应用之一是仅针对已建造,旧的或改装的设备创建设计文档,该设备通常由管道和其他圆柱形几何结构组成。 为了检测3dOT中的几何图元,通常使用专用的3D库(例如Microsoft PCL)

。使用现成的库的方法以及优点都有缺点。例如,很难将它们合并到通常具有2D尺寸的现有Kadov处理方案中。

让我们考虑如何处理3dOT,例如泵站,从2D剖面开始并使用整个2D处理库,可在可靠且优化的图像处理库(例如OpenCV)中找到该库


图1.泵站的3D OT模型

通过扫描各种管道结构获得的截面的主要元素是椭圆弧


图2.平均水平的泵站3D模型的水平横截面

对于本文,我们将考虑的范围限制为一个允许检测任意椭圆弧的关键算法-这是用于弧段增长和区域绑定区域增长和边缘链接的迭代算法

增长算法是最明显且易于验证的算法,尽管与统计算法相比是耗时的,但它更适合场景包含一个椭圆形的松散耦合远距离对象的情况。这些算法将在以后的文章中讨论。


现在,为简单起见,我们省略了从源3dOT文件中获取部分,对部分进行预处理,将其聚类以隔离几何图元的过程,以及随后进行的绑定,校正和其他需要获得模型参数的摄影测量操作的过程。我们将不会以相同的方式讨论启发式搜索算法的参数化。让我们描述构建算法的所有基本操作

我们假设需要在该图像中检测(识别,分类)椭圆弧(即计算椭圆的参数以及椭圆弧的初始和最终角度),该椭圆是从点云的水平部分切出的。


图3. 3D模型横截面的椭圆弧之一(平滑后)

为了盲目地减少光栅的工作,我们将通过概述进行光栅的所有操作

OpenCV的findContours程序上的发现光栅所有外部(不带内部形状)的轮廓在整数点的载体(在光栅坐标)的载体的形式:
 Mat mat(size);
 vector<vector<Point>> contours;
 findContours(mat, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);

这是我们的关键操作,在某些简单情况下可以完全解决任务但是由于并非总是可以找到退化的情况,因此让我们通过轮廓更详细地考虑加工技术。

反向操作使用OpenCV函数根据现有外部电路生成栅格,看起来也很简单:
 drawContours(mat, contours, -1, Scalar(255), -1);

它还经常用于遮罩轮廓,绘制或计算面积。

因此,在初始阶段,我们需要将一组补丁(一条特定曲线的片段)连接成椭圆弧,从而消除结构的其他组件(例如紧固件)的部分或扫描过程中因阴影产生的光学噪声以及其他原因。

让我们创建一个判别函数,该函数将返回轮廓的类型(椭圆,线性线段,阴影或其他),以及轮廓的端点及其旋转的轮廓矩形:
 contourTypeSearch(
   const vector<Point> &contour, Vec4i &def, RotatedRect &rc);

矩形的长与宽之比有助于快速区分接近线性段的轮廓以及较小的噪声轮廓OpenCV中

旋转的矩形具有复杂的坐标系。如果不是所需的角度本身,而是所需的三角函数,那么从上下文来看,它就变得不那么明显了。如果使用角度绝对值,则必须考虑角度是从水平方向到矩形的第一边缘沿逆时针方向计数的,并且具有负值椭圆轮廓的端点是使用我们的过程找到的,该过程会接收到Mat栅格

通过遮罩从原始图像中提取出具有区别的轮廓,并返回最大缺陷
 contourConvFeature(mat, &def, … );

该函数的主要代码是调用两个OpenCV过程

 vector<int> *hull = new vector<int>();
 convexHull(contour, *hull);
 vector<Vec4i> *defs = new vector<Vec4i>();
 convexityDefects(contour, *hull, *defs);

第一个过程为研究中的轮廓找到凸多边形,第二个过程计算所有凸缺陷考虑到它决定了轮廓的终点,

我们只考虑最大的凸度缺陷。如果轮廓的外部或内部边界具有特征,则可能不是这种情况。为了使它们平滑,我们对研究中的轮廓(而不是整个图像)进行了额外的平滑处理,以免“模糊”轮廓之间的地峡并且不违反原始拓扑。


图4.凸起缺陷的计算

选项(a)错误地定义了红色终点。选项(b)正确定义端点。选项(c)在原始形状上重新定义了端点。

由于在采用的技术中,每次都会重新生成电路,因此我们必须通过详尽的搜索过程来重新搜索对应点(或更确切地说,是它们的索引)
 nearestContourPtIdx(const vector<Point> &contour, const Point& pt);

对于无法完全摆脱这些特征的情况,还实施了另一种电弧分离模式(与内部/外部电弧分开工作)。例如,在轮廓的外弧与其他物体接触或有噪声的情况下,这一点很重要。在这种情况下,您可以使用内部弧。在这种情况下,不必分别处理外部电弧和内部电弧。

同样,根据众所周知的圆弧凸率比公式,可以近似估计半径,并拒绝太大的椭圆:
R = bulge / 2 + SQR(hypot) / (8 * bulge);

因此,对于所有轮廓,找到了它们的凸度缺陷度量(或将它们分类为线性或较小并从过程中删除)。在最后阶段,将其他参数(例如旋转尺寸参数等)添加到原始指标,并且按大小对要研究的完整指标集进行排序
 typedef tuple<int , // 
   RotatedRect, //  
   Vec4i, //  
   int> // 
   RectDefMetric;


端点连接弧段的算法


生长算法清晰明了:我们以最大的轮廓为种子,然后尝试对其进行生长,即找到最接近的斑块并将其附着到满足生长条件的端点。在成长的图中,我们输入所需的椭圆弧。遮罩图形并从原始图形中减去图形。我们重复增长过程,直到初始设置用完

增长算法的基本过程如下所示:
 vector<Point> *patch =
    growingContours(contour, def, tmp, hull);

其中轮廓是所研究的轮廓,def是其凸度缺陷,船体是整个区域的凸多边形,tmp是辅助缓冲区矩阵。在输出中,我们得到一个矢量增长的轮廓。

该过程包括尝试进行种子生长的一个循环,最后以耗尽可用的补丁进行生长或以最大迭代次数参数为限制


图5.许多没有种子的生长斑块

主要困难是选择最接近轮廓终点的斑块,这样图形只能向前生长。对于切线方向我们取端点附近的弧线的平均线。在图6中显示了在某个迭代中用于连接到种子的候选对象。


图6.被多个生长候选补丁包围的种子。

对于每个候选补丁,将计算以下度量:
typedef tuple<
   double, //    2      2   
   bool, bool, //,  4   
   int, // 
   Vec4i> //  
   DistMetric;

仅考虑落入切向锥的面片。然后,选择距离最小的面片,然后通过将连接部分压印到栅格中,连接到种子的相应末端。对于种子的另一端,将搜索与参数匹配的补丁,如果找到,则还将其连接到种子。然后,将种子屏蔽并从许多补丁中减去。从头开始重复该过程。

在增长过程的最后,我们得到了一个椭圆弧,还有待验证。

首先,使用标准的OpenCV补丁接收的过程(以路径的形式,我们记得路径和栅格可以与我们互换)并返回旋转的标注,即完整的椭圆。
 RotatedRect box = fitEllipse(patch);

然后,我们拒绝过大和过小的椭圆,然后应用我们的原始过程来比较生成的椭圆弧和栅格形式的初始生长斑的面积。此过程包括一些伪装的技巧,因此我们现在将省略其描述。

最后,我们找到检测到的椭圆的其余参数- 起始角度和终止角度(我们已经知道fitEllipse的半轴)。

要确定起始角和终止角,请按以下步骤进行:将完整的椭圆转换为多边形,并通过直接枚举找到最接近端点的点。他们的角坐标(实际上是索引),并且将是椭圆弧的开始和结束角度。在代码中,它看起来像这样(有点简化):
 pair<int, int>
   ellipseAngles(const RotatedRect &box,
   vector<Point> &ell, const Point &ps, 
   const Point &pe, const Point &pm) 
 {
    vector<Point> ell0;
    ellipse2Poly(Point(box.center.x, box.center.y), 
      Size(box.size.width / 2, box.size.height / 2),
      box.angle, 0, 355, 5, ell0);
    int i0 = nearestContourPtIdx(ell0, ps);
    int i1 = nearestContourPtIdx(ell0, pe);
    cutSides(ell0, i0, i1, i2, ell, nullptr);
    return pair<int, int>(i0, i1);
}

我们的cutSides过程考虑了椭圆弧遍历拓扑总共应考虑绕过索引i0,i1,i2的八种可能情况我们是沿着外轮廓还是沿着内轮廓,并且哪个指数更大,是初始的还是最终的?

较容易看到代码:
 void cutSides(
   const vector<Point> &ell0, int i0, int i1, int i2, 
   vector<Point> *ell_in, vector<Point> *ell_out)
 {
   if (i0 < i1) {
      if (i2 > i0 && i2 < i1) {
         if (ell_in) {...}
            if (ell_out) {...}
        } else {
            if (ell_in) {...}
            if (ell_out) {...}
        }}
    else {
        if (i2 > i1 && i2 < i0) {
            if (ell_in) {...}
            if (ell_out) {...}
        } else {
            if (ell_in) {...}
            if (ell_out) {...}
        }}}

在复杂情况下检测椭圆的一些结果如图7所示



在以下文章中,将考虑统计检测方法。

All Articles