目录
- 3.9.4 轮廓线:更多的功能
- 目标
- 理论和代码
- 练习
- 3.9.5 轮廓线层次结构
- 目标
- 理论
- 什么是层次结构?
- OpenCV中的层次结构表示法
- 轮廓线检索模式
翻译及二次校对: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()
然后看看结果:
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 )
我试着用下面给出的不同形状来匹配形状。
我得到了以下结果:
- 匹配图像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)。
请看下面的一个例子:
在这张图片中,有几个形状,我把它们编号为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检测轮廓的顺序相同。
所以考虑第一个轮廓,即轮廓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给出的结果重新排列了轮廓线,并进行了分析。同样,红色的字母给出了轮廓线的编号,绿色的字母给出了层次的顺序。
以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]]])