目标
在本章中,我们将学习 - 凸性缺陷以及如何找到它们 - 查找点到多边形的最短距离 - 匹配不同的形状
理论和代码
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)
在函数中, 参数1:轮廓。参数2:点,参数3:measureDist。如果它是真的,它会找到有符号的距离。如果为假,则查找该点是在轮廓线内部还是外部(分别返回+1、-1和0)。
注意 如果您不想找到距离,请确保第三个参数为False,因为这是一个耗时的过程。因此,将其设置为False可使速度提高2-3倍。
3. 形状匹配
OpenCV附带一个函数**cv.matchShapes**(),该函数使我们能够比较两个形状或两个轮廓,并返回一个显示相似性的度量。结果越低,匹配越好。它是根据矩值计算出来的。不同的测量方法在文档中有解释。
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
看,即使是图像旋转也不会对这个比较产生很大的影响。
理论
在前几篇关于轮廓的文章中,我们已经讨论了与OpenCV提供的轮廓相关的几个函数。但是当我们使用**cv.findcontour**()函数在图像中找到轮廓时,我们已经传递了一个参数,轮廓检索模式。我们通常通过了**cv.RETR_LIST**或**cv.RETR_TREE**,效果很好。但这到底意味着什么呢?
另外,在输出中,我们得到了三个数组,第一个是图像,第二个是轮廓,还有一个我们命名为**hierarchy**的输出(请检查前面文章中的代码)。但我们从未在任何地方使用过这种层次结构。那么这个层级是什么?它是用来做什么的?它与前面提到的函数参数有什么关系?
这就是我们在本文中要讨论的内容。
层次结构是什么?
通常我们使用**cv.findcontour**()函数来检测图像中的对象,对吧?有时对象在不同的位置。但在某些情况下,某些形状在其他形状中。就像嵌套的图形一样。在这种情况下,我们把外部的称为**父类**,把内部的称为**子类**。这样,图像中的轮廓就有了一定的相互关系。我们可以指定一个轮廓是如何相互连接的,比如,它是另一个轮廓的子轮廓,还是父轮廓等等。这种关系的表示称为**Hierarchy**。
下面是一个例子:
在这张图中,有一些形状我已经从**0-5**开始编号。*2*和*2a*表示最外层盒子的外部和内部轮廓。
这里,等高线0,1,2在**外部或最外面**。我们可以说,它们在**层级-0**中,或者简单地说,它们在**同一个层级**中。
其次是**contour-2a**。它可以被认为是**contour-2的子级**(或者反过来,contour-2是contour-2a的父级)。假设它在**层级-1**中。类似地,contour-3是contour-2的子级,它位于下一个层次结构中。最后,轮廓4,5是contour-3a的子级,他们在最后一个层级。从对方框的编号来看,我认为contour-4是contour-3a的第一个子级(它也可以是contour-5)。
我提到这些是为了理解一些术语,比如**相同层级 same hierarchy level**, , , , ,外部轮廓 external contour,子轮廓 child contour,父轮廓 parent contour,**第一个子轮廓 first child**等等。现在让我们进入OpenCV。
OpenCV中的分级表示
所以每个轮廓都有它自己的信息关于它是什么层次,谁是它的孩子,谁是它的父母等等。OpenCV将它表示为一个包含四个值的数组:[Next, Previous, First_Child, Parent]
“Next表示同一层次的下一个轮廓。”
例如,在我们的图片中取contour-0。谁是下一个同级别的等高线?这是contour-1。简单地令Next = 1
。类似地,Contour-1也是contour-2。所以Next = 2
。 contour-2呢?同一水平线上没有下一条等高线。简单地,让Next = -1
。contour-4呢?它与contour-5处于同一级别。它的下一条等高线是contour-5,所以next = 5
。
“Previous表示同一层次上的先前轮廓。”
和上面一样。contour-1之前的等值线为同级别的contour-0。类似地,contour-2也是contour-1。对于contour-0,没有前项,所以设为-1。
“First_Child表示它的第一个子轮廓。”
没有必要作任何解释。对于contour-2, child是contour-2a。从而得到contour-2a对应的指标值。contour-3a呢?它有两个孩子。但我们只关注第一个孩子。它是contour-4。那么First_Child = 4
对contour-3a而言。
“Parent表示其父轮廓的索引。”
它与**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
这是四个标志中最简单的一个(从解释的角度来看)。它只是检索所有的轮廓,但不创建任何亲子关系。在这个规则下,父轮廓和子轮廓是平等的,他们只是轮廓。他们都属于同一层级。
这里,第3和第4项总是-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
如果使用此标志,它只返回极端外部标志。所有孩子的轮廓都被留下了。我们可以说,根据这项规则,每个家庭只有长子得到关注。它不关心家庭的其他成员:)。
所以在我们的图像中,有多少个极端的外轮廓?在等级0级?有3个,即等值线是0 1 2,对吧?现在试着用这个标志找出等高线。这里,给每个元素的值与上面相同。并与上述结果进行了比较。以下是我得到的:
>>> hierarchy
array([[[ 1, -1, -1, -1],
[ 2, 0, -1, -1],
[-1, 1, -1, -1]]])
如果只想提取外部轮廓,可以使用此标志。它在某些情况下可能有用。
3. RETR_CCOMP
此标志检索所有轮廓并将其排列为2级层次结构。物体的外部轮廓(即物体的边界)放在层次结构1中。对象内部孔洞的轮廓(如果有)放在层次结构2中。如果其中有任何对象,则其轮廓仅在层次结构1中重新放置。以及它在层级2中的漏洞等等。
只需考虑在黑色背景上的“白色的零”图像。零的外圆属于第一级,零的内圆属于第二级。
我们可以用一个简单的图像来解释它。这里我用红色标注了等高线的顺序和它们所属的层次,用绿色标注(1或2),顺序与OpenCV检测等高线的顺序相同。
考虑第一个轮廓,即contour 0。这是hierarchy- 1。它有两个孔,分别是等高线1和2,属于第二级。因此,对于轮廓-0,在同一层次的下一个轮廓是轮廓-3。previous也没有。在hierarchy-2中的它的第一个子结点是contour-1。它没有父类,因为它在hierarchy-1中。所以它的层次数组是[3,-1,1,-1]
现在contour-1。它在层级-2中。相同层次结构中的下一个(在contour-1的父母关系下)是contour-2。没有previous。没有child
,但是parent
是contour-0。所以数组是[2,-1,-1,0]
类似的contour-2:它在hierarchy-2中。在contour-0下,同一层次结构中没有下一个轮廓。所以没有Next
。previous
是contour-1。没有child
,parent
是contour0。所以数组是[-1,1,-1,0]
contour-3:层次-1的下一个是轮廓-5。以前是contour-0。child
是contour4,没有parent
。所以数组是[5,0,4,-1]
contour-4:它在contour-3下的层次结构2中,它没有兄弟姐妹。没有next
,没有previous
,没有child
,parent
是contour-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
这是最后一个家伙,完美先生。它检索所有的轮廓并创建一个完整的家族层次结构列表。它甚至告诉,谁是爷爷,父亲,儿子,孙子,甚至更多…:)。
例如,我拿上面的图片,重写了cv的代码。RETR_TREE,根据OpenCV给出的结果重新排序等高线并进行分析。同样,红色的字母表示轮廓数,绿色的字母表示层次顺序。
取contour-0
:它在hierarchy-0
中。同一层次结构的next
轮廓是轮廓-7。没有previous
的轮廓。child
是contour-1,没有parent
。所以数组是[7,-1,1,-1]
以contour-2
为例:它在hierarchy-1
中。没有轮廓在同一水平。没有previous
。child
是contour-3
。父母是contour-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]]])
目标
学会 - 使用OpenCV和Numpy函数查找直方图 - 使用OpenCV和Matplotlib函数绘制直方图 - 你将看到以下函数:cv.calcHist(),np.histogram()等。
理论
那么直方图是什么?您可以将直方图视为图形或绘图,从而可以总体了解图像的强度分布。它是在X轴上具有像素值(不总是从0到255的范围),在Y轴上具有图像中相应像素数的图。
这只是理解图像的另一种方式。通过查看图像的直方图,您可以直观地了解该图像的对比度,亮度,强度分布等。当今几乎所有图像处理工具都提供直方图功能。以下是剑桥彩色网站的图片,我建议您访问该网站以获取更多详细信息。
您可以看到图像及其直方图。(请记住,此直方图是针对灰度图像而非彩色图像绘制的)。直方图的左侧区域显示图像中较暗像素的数量,而右侧区域则显示明亮像素的数量。从直方图中,您可以看到暗区域多于亮区域,而中间调的数量(中间值的像素值,例如127附近)则非常少。
寻找直方图
现在我们有了一个关于直方图的想法,我们可以研究如何找到它。OpenCV和Numpy都为此内置了功能。在使用这些功能之前,我们需要了解一些与直方图有关的术语。
BINS:上面的直方图显示每个像素值的像素数,即从0到255。即,您需要256个值来显示上面的直方图。但是考虑一下,如果您不需要分别找到所有像素值的像素数,而是找到像素值间隔中的像素数怎么办? 例如,您需要找到介于0到15之间的像素数,然后找到16到31之间,…,240到255之间的像素数。只需要16个值即可表示直方图。这就是在OpenCV教程中有关直方图的示例中显示的内容。
因此,您要做的就是将整个直方图分成16个子部分,每个子部分的值就是其中所有像素数的总和。 每个子部分都称为“ BIN”。在第一种情况下,bin的数量为256个(每个像素一个),而在第二种情况下,bin的数量仅为16个。BINS由OpenCV文档中的**histSize**术语表示。
DIMS:这是我们为其收集数据的参数的数量。在这种情况下,我们仅收集关于强度值的一件事的数据。所以这里是1。
RANGE:这是您要测量的强度值的范围。通常,它是[0,256]
,即所有强度值。
1. OpenCV中的直方图计算
因此,现在我们使用**cv.calcHist**()函数查找直方图。让我们熟悉一下该函数及其参数:
cv.calcHist(images,channels,mask,histSize,ranges [,hist [,accumulate]])
- images:它是uint8或float32类型的源图像。它应该放在方括号中,即“ [img]”。
- channels:也以方括号给出。它是我们计算直方图的通道的索引。例如,如果输入为灰度图像,则其值为[0]。对于彩色图像,您可以传递[0],[1]或[2]分别计算蓝色,绿色或红色通道的直方图。
- mask:图像掩码。为了找到完整图像的直方图,将其指定为“None”。但是,如果要查找图像特定区域的直方图,则必须为此创建一个掩码图像并将其作为掩码。(我将在后面显示一个示例。)
- histSize:这表示我们的BIN计数。需要放在方括号中。对于全尺寸,我们通过[256]。
- ranges:这是我们的RANGE。通常为[0,256]。
因此,让我们从示例图像开始。只需以灰度模式加载图像并找到其完整直方图即可。
img = cv.imread('home.jpg',0)
hist = cv.calcHist([img],[0],None,[256],[0,256])
hist是256x1的数组,每个值对应于该图像中具有相应像素值的像素数。
2. numpy的直方图计算
Numpy还为您提供了一个函数**np.histogram**()。因此,除了**calcHist**()函数外,您可以尝试下面的代码:
hist,bins = np.histogram(img.ravel(),256,[0,256])
hist与我们之前计算的相同。但是bin将具有257个元素,因为Numpy计算出bin的范围为0-0.99
、1-1.99
、2-2.99
等。因此最终范围为255-255.99
。为了表示这一点,他们还在最后添加了256。但是我们不需要256。最多255就足够了。
- 另外 Numpy还有另一个函数**np.bincount**(),它比np.histogram()快10倍左右。因此,对于一维直方图,您可以更好地尝试一下。不要忘记在np.bincount中设置minlength = 256。例如,
hist = np.bincount(img.ravel(),minlength = 256)
注意 OpenCV函数比np.histogram()快大约40倍。因此,尽可能使用OpenCV函数。
现在我们应该绘制直方图,但是怎么绘制?
绘制直方图
有两种方法, 1. 简短的方法:使用Matplotlib绘图功能 2. 稍长的方法:使用OpenCV绘图功能
1. 使用Matplotlib
Matplotlib带有直方图绘图功能:matplotlib.pyplot.hist()
它直接找到直方图并将其绘制。您无需使用**calcHist**()或np.histogram()函数来查找直方图。请参见下面的代码:
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('home.jpg',0)
plt.hist(img.ravel(),256,[0,256]); plt.show()
你将得到如下的结果:
或者,您可以使用matplotlib的法线图,这对于BGR图是很好的。为此,您需要首先找到直方图数据。试试下面的代码:
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('home.jpg')
color = ('b','g','r')
for i,col in enumerate(color):
histr = cv.calcHist([img],[i],None,[256],[0,256])
plt.plot(histr,color = col)
plt.xlim([0,256])
plt.show()
'''plt.xlim() (即limit) 和 plt.xticks()
plt.xlim() 显示的是x轴的作图范围,同时plt.ylim() 显示的是y轴的作图范围,而 plt.xticks() 表达的是x轴的刻度内容的范围
plt.xlim()有两个参数输入:
1 plt.xlim(num1, num2)
2 plt.xlim(xmin=num1,xmax=num2)
用于设置x轴的范围
但是如果图片已经画好了,plt.xlim在图片完成之后,则会直接输出x轴的范围值
plt.xticks()
在matplotlib中ticks表示的是刻度,而刻度有两层意思,一个是刻标(locs),一个是刻度标签(tick labels)。在作图时,x轴y轴都是连续的,所以刻标可以随意指定,就是在连续变量上找寻位置,而刻度标签则可以对应替换
xticks(rotation:旋转度数):更改绘制x轴标签方向(与水平方向的逆时针夹角度数)
复制代码
#plt.xticks()返回了两个对象,一个是刻标(locs),另一个是刻度标签
locs, labels = plt.xticks()
# 显示x轴的刻标
plt.xticks( arange(6) )
# 显示x轴的刻标以及对应的标签
pltxticks( arange(5), ('Tom', 'Dick', 'Harry', 'Sally', 'Sue') )'''
结果:
您可以从上图中得出,蓝色在图像中具有一些高值域(显然这应该是由于天空)
2. 使用 OpenCV
好吧,在这里您可以调整直方图的值及其bin值,使其看起来像x,y坐标,以便您可以使用**cv.line**()或cv.polyline()函数绘制它以生成与上述相同的图像。OpenCV-Python2官方示例已经提供了此功能。检查示例/python/hist.py中的代码。
掩码的应用
我们使用了cv.calcHist()
来查找整个图像的直方图。如果你想找到图像某些区域的直方图呢?只需创建一个掩码图像,在你要找到直方图为白色,否则黑色。然后把这个作为掩码传递。
img = cv.imread('home.jpg',0)
# create a mask
mask = np.zeros(img.shape[:2], np.uint8)
mask[100:300, 100:400] = 255
masked_img = cv.bitwise_and(img,img,mask = mask)
# 计算掩码区域和非掩码区域的直方图
# 检查作为掩码的第三个参数
hist_full = cv.calcHist([img],[0],None,[256],[0,256])
hist_mask = cv.calcHist([img],[0],mask,[256],[0,256])
plt.subplot(221), plt.imshow(img, 'gray')
plt.subplot(222), plt.imshow(mask,'gray')
plt.subplot(223), plt.imshow(masked_img, 'gray')
plt.subplot(224), plt.plot(hist_full), plt.plot(hist_mask)
plt.xlim([0,256])
plt.show()
查看结果。在直方图中,蓝线表示完整图像的直方图,绿线表示掩码区域的直方图。