您现在的位置是:首页 >技术杂谈 >OpenCV中的图像处理3.9(七)轮廓线及其层次结构网站首页技术杂谈

OpenCV中的图像处理3.9(七)轮廓线及其层次结构

王者与CV 2024-06-17 10:22:10
简介OpenCV中的图像处理3.9(七)轮廓线及其层次结构


翻译及二次校对:cvtutorials.com

编辑者:廿瓶鲸(和鲸社区Siby团队成员)

3.9.4 轮廓线:更多的功能

目标

在本章中,我们将了解到:

1.凸性缺陷以及如何找到它们。
2.寻找从一个点到一个多边形的最短距离
3.匹配不同的形状

理论和代码

1.凸性缺陷

我们在第二章关于轮廓的内容中看到了什么是凸面体。任何偏离这个凸包的物体都可以被认为是凸性缺陷。

OpenCV提供了一个现成的函数来寻找这个缺陷,即cv.convexityDefects()。一个基本的函数调用看起来如下。

hull = cv.convexHull(cnt,returnPoints = False)
defects = cv.convexityDefects(cnt,hull)

注意:请记住,我们在寻找凸面体时必须传递returnPoints = False,以便找到凸性缺陷。

它返回一个数组,每一行都包含这些值 - [ 起始点,终点,最远点,到最远点的大致距离 ]。我们可以用一个图像将其可视化。我们画一条连接起点和终点的线,然后在最远点画一个圆。请记住,前三个返回值是cnt的索引。所以我们必须从cnt中获取这些值。

import cv2 as cv
import numpy as np
img = cv.imread('star.jpg')
img_gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
ret,thresh = cv.threshold(img_gray, 127, 255,0)
contours,hierarchy = cv.findContours(thresh,2,1)
cnt = contours[0]
hull = cv.convexHull(cnt,returnPoints = False)
defects = cv.convexityDefects(cnt,hull)
for i in range(defects.shape[0]):
    s,e,f,d = defects[i,0]
    start = tuple(cnt[s][0])
    end = tuple(cnt[e][0])
    far = tuple(cnt[f][0])
    cv.line(img,start,end,[0,255,0],2)
    cv.circle(img,far,5,[0,0,255],-1)
cv.imshow('img',img)
cv.waitKey(0)
cv.destroyAllWindows()

然后看看结果:

Image Name

2.点多边形测试

这个函数找出图像中的一个点和一个轮廓线之间的最短距离。它返回的距离是:当点在轮廓线外时为负数,当点在轮廓线内时为正数,如果点在轮廓线上则为零。

例如,我们可以检查点(50,50),如下所示。

dist = cv.pointPolygonTest(cnt,(50,50),True)

在这个函数中,第三个参数是measureDist,如果它是True,它找到有符号的距离。如果是False,它将发现该点是在轮廓线内还是在轮廓线外或在轮廓线上(它分别返回+1、-1、0)。

注意:如果你不想找距离,确保第三个参数是False,因为这是一个耗时的过程。所以,把它设为假值可以使速度提高2-3倍。

3.匹配形状

OpenCV有一个函数cv.matchShapes(),它使我们能够比较两个形状,或两个轮廓,并返回一个显示相似度的指标。结果越低,说明它的匹配度越高。它是根据hu-moment值来计算的。文档中解释了不同的测量方法。

import cv2 as cv
import numpy as np

img1 = cv.imread('star.jpg',0)
img2 = cv.imread('star2.jpg',0)

ret, thresh = cv.threshold(img1, 127, 255,0)
ret, thresh2 = cv.threshold(img2, 127, 255,0)
contours,hierarchy = cv.findContours(thresh,2,1)
cnt1 = contours[0]
contours,hierarchy = cv.findContours(thresh2,2,1)
cnt2 = contours[0]

ret = cv.matchShapes(cnt1,cnt2,1,0.0)
print( ret )

我试着用下面给出的不同形状来匹配形状。

Image Name

我得到了以下结果:

  • 匹配图像A与自身=0.0
  • 图像A与图像B的匹配=0.001946
  • 图片A与图片C的匹配度=0.326911

看,即使是图像旋转也不会对这个比较产生什么影响。

注意:Hu-Moments是七个对平移、旋转和缩放不变的矩。第七个是歪斜不变的。这些值可以通过cv.HuMoments()函数找到。

练习

1.查看cv.pointPolygonTest()的文档,你可以找到一个红蓝相间的漂亮图像。它代表了所有像素到上面的白色曲线的距离。曲线内的所有像素都是蓝色的,这取决于距离。同样地,外面的点是红色的。轮廓线的边缘用白色标记。所以问题很简单。写一段代码来创建这样的距离表示。
2.用cv.matchShapes()比较数字或字母的图像。( 这将是走向OCR的一个简单步骤)

3.9.5 轮廓线层次结构

目标

这一次,我们学习了轮廓的层次结构,即轮廓的父子关系。

理论

在过去的几篇关于轮廓线的文章中,我们已经使用了OpenCV提供的几个与轮廓线有关的函数。但是当我们使用cv.findContours()函数在图像中找到轮廓时,我们传递了一个参数,即轮廓检索模式。我们通常传递cv.RETR_LIST或cv.RETR_TREE,而且效果不错。但它实际上是什么意思?

另外,在输出中,我们得到了三个数组,第一个是图像,第二个是我们的轮廓,还有一个我们命名为层次的输出(请查看以前文章中的代码)。但我们从未在任何地方使用过这个层次结构。那么这个层次结构是什么,它的作用是什么?它与前面提到的函数参数有什么关系?

这就是我们将在本文中讨论的问题。

什么是层次结构?

通常我们使用cv.findContours()函数来检测图像中的物体,有时物体在不同的位置。但在某些情况下,有些形状是在其他形状里面的。就像嵌套的图形。在这种情况下,我们称外部的为父,内部的为子。这样一来,图像中的轮廓就有了一些相互之间的关系。我们可以指定一个轮廓是如何相互连接的,比如,它是另一个轮廓的孩子,或者它是一个父母等等。这种关系的表现形式被称为层次结构(Hierarchy)。

请看下面的一个例子:

Image Name

在这张图片中,有几个形状,我把它们编号为0-5。2和2a表示最外层盒子的外部和内部轮廓线。

这里,0,1,2是外部或最外层的轮廓。我们可以说,它们是在层次结构0中,或者简单地说,它们是在同一层次中。

接下来是轮廓2a。它可以被认为是轮廓线2的孩子(或者反过来说,轮廓线2是轮廓线2a的父母)。因此,让它在层次结构1中。同样地,轮廓3是轮廓2的孩子,它属于下一个层次。最后,轮廓线4、5是轮廓线3a的子女,它们位于最后一个层次。从我给盒子编号的方式来看,我认为轮廓线4是轮廓线3a的第一个孩子(也可以是轮廓线5)。

我提到这些东西是为了理解像同一层次结构水平、外部轮廓、子轮廓、父轮廓、第一子轮廓等术语。现在让我们来了解一下OpenCV。

OpenCV中的层次结构表示法

所以每个轮廓都有自己的信息,关于它是什么层次,谁是它的孩子,谁是它的父母等等。OpenCV将其表示为一个由四个值组成的数组。[下一个,上一个,第一个孩子,父母] 。

"下一个表示同一层次的下一个轮廓 "。

例如,以我们图片中的轮廓线0为例。谁是它同一层次的下一个轮廓?它就是轮廓1。所以简单地说,Next=1。同样地,对于轮廓1,下一个是轮廓2。所以Next=2。

那么轮廓线2呢?在同一层面上没有下一个轮廓。所以简单地说,把Next = -1。那么轮廓线4呢?它与轮廓线5在同一层次。所以它的下一个轮廓是轮廓5,所以Next = 5。

"上一个表示同一层次的前一个轮廓 "。

这一点与上述相同。轮廓1的上一个轮廓是同一层次的轮廓0。同样,对于轮廓2,它是轮廓1。而对于轮廓线0,没有前一个,所以把它列为-1。

"First_Child表示其第一个子轮廓 "。

不需要任何解释。对于轮廓线2,子线是轮廓线2a。所以它得到轮廓2a的相应索引值。轮廓线3a呢?它有两个孩子。但我们只取第一个孩子。它是轮廓4。所以First_Child = 4为轮廓线3a。

"父代表示其父代轮廓的索引 "。

这与First_Child正好相反。对于轮廓4和轮廓5,父轮廓都是轮廓3a。对轮廓线3a,它是3轮廓线,依此类推。

注意:如果没有子代或父代,该字段将被视为-1。

现在我们知道了OpenCV中使用的层次结构风格,我们可以在上面给出的相同图片的帮助下检查OpenCV中的轮廓检索模式,即像cv.RETR_LIST, cv.RETR_TREE, cv.RETR_CCOMP, cv.RETR_EXTERNAL等标志是什么意思?

轮廓线检索模式

1.RETR_LIST

这是四个标志中最简单的一个(从解释的角度看)。它只是检索所有的轮廓,但不创建任何父子关系。在这个规则下,父母和孩子是平等的,他们只是轮廓,即他们都属于同一层次的水平。

所以在这里,层次结构数组中的第三和第四项总是-1。但是很明显,下一个和上一个项会有其相应的值。你可以自己检查并验证一下。

下面是我得到的结果,每一行都是相应轮廓的层次结构细节。例如,第一行对应的是轮廓线0,下一个轮廓线是轮廓线1,所以Next=1。没有上一个轮廓,所以Previous=-1。而剩下的两个,如前所述,是-1。

>>> hierarchy
array([[[ 1, -1, -1, -1],
        [ 2,  0, -1, -1],
        [ 3,  1, -1, -1],
        [ 4,  2, -1, -1],
        [ 5,  3, -1, -1],
        [ 6,  4, -1, -1],
        [ 7,  5, -1, -1],
        [-1,  6, -1, -1]]])

如果你不使用任何层次结构特征,这是在你的代码中使用的好选择。

2.RETR_EXTERNAL

如果你使用这个标志,它只返回最外部标志。所有child的轮廓都被留下了。我们可以说,在这个法则下,每个家庭中只有最年长的人得到照顾。它并不关心家庭的其他成员。

那么,在我们的图像中,有多少个最外轮廓?即在层次0的水平?只有3个,即0,1,2号轮廓线,对吗?现在试着用这个标志找到这些轮廓。在这里,给每个元素的值也和上面一样。将其与上述结果进行比较。下面是我得到的结果。

>>> hierarchy
array([[[ 1, -1, -1, -1],
        [ 2,  0, -1, -1],
        [-1,  1, -1, -1]]])

如果你想只提取外轮廓,你可以使用这个标志。这在某些情况下可能是有用的。

3.RETR_CCOMP

这个标志检索所有的轮廓线,并将它们排列成一个2级的层次结构。物体内部的孔洞轮廓(如果有的话)被放在层次结构2中。如果有任何物体在里面,它的轮廓又只被放在层次结构1中。而它的洞则放在层次结构2中,以此类推。

只需考虑一个黑色背景上的 "大的白色的零 "的图像。零的外圈属于第一层次,而零的内圈属于第二层次。

我们可以用一个简单的图像来解释它。这里我用红色标出了轮廓的顺序,用绿色标出了它们所属的层次(1或2)。这个顺序与OpenCV检测轮廓的顺序相同。

Image Name

所以考虑第一个轮廓,即轮廓0。它是层次结构1。它有两个洞,轮廓线1&2,它们属于层次结构2。因此,对于轮廓线0,同一层次的下一个轮廓线是轮廓线3。而没有前一个。它的第一个孩子是层次结构2中的轮廓1。它没有父级,因为它是在层次结构1中。所以它的层次结构数组是[3,-1,1,-1] 。

现在取轮廓线1。它是在层次结构2中。在同一层次中的下一个(在轮廓线1的亲属关系下)是轮廓线2。没有前一个。没有子代,但是父代是轮廓线0。所以数组是[2,-1,-1,0]。

同理,轮廓线2:它在层次结构2中。在轮廓0下的同一层次中没有下一个轮廓。所以没有下一个。上一个是轮廓1。没有子代,父代是轮廓0。所以数组是[-1,1,-1,0]。

轮廓3 : 层次结构1中的下一个是轮廓5。上一个是轮廓线0。子代是轮廓线4,没有父代。所以数组是[5,0,4,-1]。

轮廓4 : 它在层次结构2中位于轮廓3之下,没有兄弟姐妹。所以没有下一个,没有上一个,没有子代,父代是轮廓3。所以数组是[-1,-1,-1,3]。

剩下的你可以填满。这就是我得到的最终答案。

>>> hierarchy
array([[[ 3, -1,  1, -1],
        [ 2, -1, -1,  0],
        [-1,  1, -1,  0],
        [ 5,  0,  4, -1],
        [-1, -1, -1,  3],
        [ 7,  3,  6, -1],
        [-1, -1, -1,  5],
        [ 8,  5, -1, -1],
        [-1,  7, -1, -1]]])

4.RETR_TREE

RETR_TREE检索了所有的轮廓线,并创建了一个完整的家庭层次列表。它甚至可以告诉你,谁是爷爷、父亲、儿子、孙子,甚至更多…?。

例如,我取了上面的图片,重写了cv.RETR_TREE的代码,按照OpenCV给出的结果重新排列了轮廓线,并进行了分析。同样,红色的字母给出了轮廓线的编号,绿色的字母给出了层次的顺序。

Image Name

以0号轮廓线为例:它在第0层。同一层次中的下一个轮廓是轮廓7。没有前一个轮廓线。子代是轮廓线1。也没有父代。所以数组是[7,-1,1,-1]。

拿轮廓线2来说:它在层次结构1中。在同一层次中没有轮廓线。没有前一个。子代是轮廓线3。父代是轮廓线1。所以数组是[-1,-1,3,1]。

剩下的,自己试试。下面是完整的答案。

>>> hierarchy
array([[[ 7, -1,  1, -1],
        [-1, -1,  2,  0],
        [-1, -1,  3,  1],
        [-1, -1,  4,  2],
        [-1, -1,  5,  3],
        [ 6, -1, -1,  4],
        [-1,  5, -1,  4],
        [ 8,  0, -1, -1],
        [-1,  7, -1, -1]]])
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。