边缘检测虽然能够检测出边缘,但边缘是不连续的,检测到的边缘并不是一个整体。图像轮廓是指将边缘连接起来形成的一个整体,用于后续的计算。图像轮廓是图像中非常重要的一个特征信息,通过对图像轮廓的操作,我们能够获取目标图像的大小、位置、方向等信息。


1. 查找并绘制轮廓


1.1 查找图像轮廓:findContours函数


image, contours, hierarchy = cv2.findContours( image, mode, method)


  • image:与函数参数中的原始图像image一致。
  • contours:返回的轮廓。
  • hierarchy:图像的拓扑信息(轮廓层次)。
  • image:原始图像。8 位单通道图像,所有非零值被处理为1,所有零值保持不变。也就是说灰度图像会被自动处理为二值图像。在实际操作时,可以根据需要,预先使用阈值处理等函数将待查找轮廓的图像处理为二值图像。
  • mode:轮廓检索模式。
  • method:轮廓的近似方法。

函数 cv2.findContours()的返回值及参数的含义比较丰富,下面对上述返回值和参数逐一做出说明。


1.1.1 返回值image

该返回值与参数image 是一致的,就是原始输入图像。在OpenCV 4.X 中,该返回值已经被取消。在OpenCV 4.X 中,函数cv2.findContours()仅有两个返回值,其语法格式为:
contours, hierarchy = cv2.findContours( image, mode, method)


1.1.2 返回值contours

该返回值返回的是一组轮廓信息,每个轮廓都是由若干个点所构成的。例如,contours[i]是第i个轮廓(下标从0开始),contours[i][j]是第i个轮廓内的第j个点。


下图所示为提取的轮廓示例,函数cv2.findContours()提取出左图的3个轮廓,每一个轮廓都是由若干个像素点构成的。
在这里插入图片描述
下面针对上图来简单介绍一下contours 的基本属性。


  • type属性

返回值contours的type属性是list类型,list的每个元素都是图像的一个轮廓,用Numpy中的ndarray结构表示。
例如,使用如下语句获取轮廓contours 的类型:


print (type(contours))
结果为<class ‘list’>
  • 1
  • 2

  • 轮廓的个数

使用如下语句可以获取轮廓的个数:


print (len(contours))
结果为3
  • 1
  • 2

  • 每个轮廓的点数

每一个轮廓都是由若干个像素点构成的,点的个数不固定,具体个数取决于轮廓的形状。例如,使用如下语句,可以获取每个轮廓内点的个数:


print (len(contours[0])) #打印第0 个轮廓的长度(点的个数):4
print (len(contours[1])) #打印第1 个轮廓的长度(点的个数):60
print (len(contours[2])) #打印第2 个轮廓的长度(点的个数):184

输出结果如下:
4
60
184
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

使用如下语句,可以获取每个轮廓内点的shape属性:


print(contours[0].shape)
print(contours[1].shape)
print(contours[2].shape)

输出结果如下:
(4, 1, 2)
(60, 1, 2)
(184, 1, 2)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

  • 轮廓内的点

使用如下语句,可以获取轮廓内第0 个轮廓中具体点的位置属性:


print (contours[0]) #打印第0 个轮廓中的像素点

contours[0]对应着图12-1 中右图左下角矩形轮廓的点,输出结果如下:
[[[ 79 270]]
[[ 79 383]]
[[195 383]]
[[195 270]]]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

1.1.3 返回值hierarchy

图像内的轮廓可能位于不同的位置。比如,一个轮廓在另一个轮廓的内部。在这种情况下,我们将外部的轮廓称为父轮廓,内部的轮廓称为子轮廓。按照上述关系分类,一幅图像中所有轮廓之间就建立了父子关系。


根据轮廓之间的关系,就能够确定一个轮廓与其他轮廓是如何连接的。比如,确定一个轮廓是某个轮廓的子轮廓,或者是某个轮廓的父轮廓。上述关系被称为层次(组织结构),返回值hierarchy就包含上述层次关系。每个轮廓contours[i]对应4 个元素来说明当前轮廓的层次关系。其形式为:
[Next,Previous,First_Child,Parent]
式中各元素的含义为:


  • Next:后一个轮廓的索引编号。
  • Previous:前一个轮廓的索引编号。
  • First_Child:第1 个子轮廓的索引编号。
  • Parent:父轮廓的索引编号。

如果上述各个参数所对应的关系为空时,也就是没有对应的关系时,则将该参数所对应的值设为“-1”。


使用 print 语句可以查看hierarchy 的值:


print(hierarchy)
  • 1

需要注意,轮廓的层次结构是由参数mode决定的。也就是说,使用不同的mode,得到轮廓的编号是不一样的,得到的hierarchy 也不一样。


1.1.4 参数image

该参数表示输入的图像,必须是8 位单通道二值图像。一般情况下,都是将图像处理为二值图像后,再将其作为image 参数使用的。


1.1.5 参数mode

参数mode决定了轮廓的提取方式,具体有如下4种:


  • cv2.RETR_EXTERNAL:只检测外轮廓。
  • cv2.RETR_LIST:检索所有的轮廓,并将其保存到一条链表当中。
  • RETR_CCOMP:检索所有的轮廓,并将他们组织为两层:顶层是各部分的外部边界,第二层是空洞的边界。
  • RETR_TREE:检索所有的轮廓,建立一个等级树结构。

1.1.6 参数method

参数method决定了如何表达轮廓,可以为如下值:


  • cv2.CHAIN_APPROX_NONE:存储所有的轮廓点,相邻两个点的像素位置差不超过1,即max(abs(x1-x2),abs(y2-y1))=1。
  • cv2.CHAIN_APPROX_SIMPLE:压缩水平方向、垂直方向、对角线方向的元素,只保留该方向的终点坐标。例如,在极端的情况下,一个矩形只需要用4 个点来保存轮廓信息。
  • cv2.CHAIN_APPROX_TC89_L1:使用teh-Chinl chain 近似算法的一种风格。
  • cv2.CHAIN_APPROX_TC89_KCOS:使用teh-Chinl chain 近似算法的一种风格。

例如,在下图中,左图是使用参数值cv2.CHAIN_APPROX_NONE 存储的轮廓,保存了轮廓中的每一个点;右图是使用参数值cv2.CHAIN_APPROX_SIMPLE 存储的轮廓,仅仅保存了边界上的四个点。
在这里插入图片描述


在使用函数cv2.findContours()查找图像轮廓时,需要注意以下问题:


  • 待处理的源图像必须是灰度二值图。因此,在通常情况下,都要预先对图像进行阈值分割或者边缘检测处理,得到满意的二值图像后再将其作为参数使用。
  • 在 OpenCV 中,都是从黑色背景中查找白色对象。因此,对象必须是白色的,背景必须是黑色的。
  • 在 OpenCV 4.x 中,函数cv2.findContours()仅有两个返回值。

1.2 绘制图像轮廓:drawContours函数


image=cv2.drawContours( image, contours, contourIdx, color[, thickness[, lineType[, hierarchy[, maxLevel[, offset]]]]] )
其中,函数的返回值为image,表示目标图像,即绘制了边缘的原始图像。


  • image:待绘制轮廓的图像。需要注意,函数cv2.drawContours()会在图像image上直接绘制轮廓。也就是说,在函数执行完以后,image不再是原始图像,而是包含了轮廓的图像。因此,如果图像image还有其他用途的话,则需要预先复制一份,将该副本图像传递给函数cv2.drawContours()使用。
  • contours:需要绘制的轮廓。该参数的类型与函数cv2.findContours()的输出contours 相同,都是list类型。
  • contourIdx:需要绘制的边缘索引,告诉函数cv2.drawContours()要绘制某一条轮廓还是全部轮廓。如果该参数是一个整数或者为零,则表示绘制对应索引号的轮廓;如果该值为负数(通常为“-1”),则表示绘制全部轮廓。
  • color:绘制的颜色,用BGR 格式表示。
  • thickness:可选参数,表示绘制轮廓时所用画笔的粗细。如将该值设置为“-1”,则表示要绘制实心轮廓。
  • lineType:可选参数,表示绘制轮廓时所用的线型。
  • hierarchy:对应函数cv2.findContours()所输出的层次信息。
  • maxLevel:控制所绘制的轮廓层次的深度。如果值为0,表示仅仅绘制第0 层的轮廓;如果值为其他的非零正数,表示绘制最高层及以下的相同数量层级的轮廓。
  • offset:偏移参数。该参数使轮廓偏移到不同的位置展示出来。

1.3 轮廓实例


#例:绘制一幅图像内的所有轮廓。
#如果要绘制图像内的所有轮廓,需要将函数cv2.drawContours()的参数contourIdx 的值设置为“-1”。
import cv2
o = cv2.imread(‘contours.bmp’)
cv2.imshow(“original”,o)
gray = cv2.cvtColor(o,cv2.COLOR_BGR2GRAY)
ret, binary = cv2.threshold(gray,127,255,cv2.THRESH_BINARY)
image,contours, hierarchy = cv2.findContours(binary,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
o=cv2.drawContours(o,contours,-1,(0,0,255),5)
cv2.imshow(“result”,o)
cv2.waitKey()
cv2.destroyAllWindows()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

在这里插入图片描述


#逐个显示一幅图像内的边缘信息。
#如果要绘制图像内的某个具体轮廓,需要将函数cv2.drawContours()的参数contourIdx设置为具体的索引值。本例通过循环语句逐一绘制轮廓。
import cv2
import numpy as np
o = cv2.imread(‘contours.bmp’)
cv2.imshow(“original”,o)
gray = cv2.cvtColor(o,cv2.COLOR_BGR2GRAY)
ret, binary = cv2.threshold(gray,127,255,cv2.THRESH_BINARY)
image,contours, hierarchy = cv2.findContours(binary,
cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
n=len(contours)
contoursImg=[]
for i in range(n):
temp=np.zeros(o.shape,np.uint8)
contoursImg.append(temp)
contoursImg[i]=cv2.drawContours(contoursImg[i],contours,i,(255,255,255),5)
cv2.imshow(“contours[“ + str(i)+“]”,contoursImg[i])
cv2.waitKey()
cv2.destroyAllWindows()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

在这里插入图片描述


#使用轮廓绘制功能,提取前景对象。
#将函数 cv2.drawContours()的参数thickness 的值设置为“-1”,可以绘制前景对象的实心轮廓。将该实心轮廓与原始图像进行“按位与”操作,即可将前景对象从原始图像中提取出来。

import cv2
import numpy as np
o = cv2.imread(‘loc3.jpg’)
cv2.imshow(“original”,o)
gray = cv2.cvtColor(o,cv2.COLOR_BGR2GRAY)
ret, binary = cv2.threshold(gray,127,255,cv2.THRESH_BINARY)
image,contours, hierarchy = cv2.findContours(binary,cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE)
mask=np.zeros(o.shape,np.uint8)
mask=cv2.drawContours(mask,contours,-1,(255,255,255),-1)
cv2.imshow(“mask” ,mask)
loc=cv2.bitwise_and(o,mask)
cv2.imshow(“location” ,loc)
cv2.waitKey()
cv2.destroyAllWindows()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

在这里插入图片描述


2. 矩特性


比较两个轮廓最简单的方法是比较二者的轮廓矩。轮廓矩代表了一个轮廓、一幅图像、一组点集的全局特征。矩信息包含了对应对象不同类型的几何特征,例如大小、位置、角度、形状等。矩特征被广泛地应用在模式识别、图像识别等方面。


2.1 矩的计算


OpenCV 提供了函数cv2.moments()来获取图像的moments 特征。通常情况下,我们将使用函数cv2.moments()获取的轮廓特征称为“轮廓矩”。轮廓矩描述了一个轮廓的重要特征,使用轮廓矩可以方便地比较两个轮廓。
retval = cv2.moments( array[, binaryImage] )


  • array:可以是点集,也可以是灰度图像或者二值图像。当array 是点集时,函数会把这些点集当成轮廓中的顶点,把整个点集作为一条轮廓,而不是把它们当成独立的点来看待。
  • binaryImage:该参数为True 时,array 内所有的非零值都被处理为1。该参数仅在参数array为图像时有效。

该函数的返回值retval 是矩特征,主要包括:
在这里插入图片描述
上述矩都是根据公式计算得到的,大多数矩比较抽象。但是很明显,如果两个轮廓的矩一致,那么这两个轮廓就是一致的。虽然大多数矩都是通过数学公式计算得到的抽象特征,但是零阶矩“m00”的含义比较直观,它表示一个轮廓的面积。


矩特征函数cv2.moments()所返回的特征值,能够用来比较两个轮廓是否相似。例如,有两个轮廓,不管它们出现在图像的哪个位置,我们都可以通过函数cv2.moments()的m00矩判断其面积是否一致。


在位置发生变化时,虽然轮廓的面积、周长等特征不变,但是更高阶的特征会随着位置的变化而发生变化。在很多情况下,我们希望比较不同位置的两个对象的一致性。解决这一问题的方法是引入中心矩。中心矩通过减去均值而获取平移不变性,因而能够比较不同位置的两个对象是否一致。很明显,中心矩具有的平移不变性,使它能够忽略两个对象的位置关系,帮助我们比较不同位置上两个对象的一致性。


除了考虑平移不变性外,我们还会考虑经过缩放后大小不一致的对象的一致性。也就是说,我们希望图像在缩放前后能够拥有一个稳定的特征值。也就是说,让图像在缩放前后具有同样的特征值。显然,中心矩不具有这个属性。例如,两个形状一致、大小不一的对象,其中心矩是有差异的。


归一化中心矩通过除以物体总尺寸而获得缩放不变性。它通过上述计算提取对象的归一化中心矩属性值,该属性值不仅具有平移不变性,还具有缩放不变性。


import cv2
import numpy as np
o = cv2.imread(‘moments.bmp’)
cv2.imshow(“original”,o)
gray = cv2.cvtColor(o,cv2.COLOR_BGR2GRAY)
ret, binary = cv2.threshold(gray,127,255,cv2.THRESH_BINARY)
image,contours, hierarchy = cv2.findContours(binary,
cv2.RETR_LIST,
cv2.CHAIN_APPROX_SIMPLE)
n=len(contours)
contoursImg=[]
for i in range(n):
temp=np.zeros(image.shape,np.uint8)
contoursImg.append(temp)
contoursImg[i]=cv2.drawContours(contoursImg[i],contours,i,255,3)
cv2.imshow(“contours[“ + str(i)+“]”,contoursImg[i])
print(“观察各个轮廓的矩(moments):”)
for i in range(n):
print(“轮廓”+str(i)+“的矩:\n”,cv2.moments(contours[i]))
print(“观察各个轮廓的面积:”)
for i in range(n):
print(“轮廓”+str(i)+“的面积:%d” %cv2.moments(contours[i])[‘m00’])
cv2.waitKey()
cv2.destroyAllWindows()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

本 例 中, 首先使用函数cv2.moments() 提取各个轮廓的特征; 接下来, 通过语句cv2.moments(contours[i])[‘m00’])提取各个轮廓矩的面积信息。


在这里插入图片描述
由于篇幅过长,这里显示输出的第一个轮廓的矩及其面积:


轮廓 0 的矩:
{‘m00’: 14900.0, ‘m10’: 1996600.0, ‘m01’: 7800150.0, ‘m20’: 279961066.6666666,
‘m11’: 1045220100.0, ‘m02’: 4110944766.6666665, ‘m30’: 40842449600.0, ‘m21’:
146559618400.0, ‘m12’: 550866598733.3334, ‘m03’: 2180941440375.0, ‘mu20’:
12416666.666666627, ‘mu11’: 0.0, ‘mu02’: 27566241.666666508, ‘mu30’:
1.52587890625e-05, ‘mu21’: 2.09808349609375e-05, ‘mu12’: 6.198883056640625e-05,
‘mu03’: 0.000244140625, ‘nu20’: 0.05592841163310942, ‘nu11’: 0.0, ‘nu02’:
0.12416666666666591, ‘nu30’: 5.630596400372416e-16, ‘nu21’: 7.742070050512072e-16,
‘nu12’: 2.2874297876512943e-15, ‘nu03’: 9.008954240595866e-15}
轮廓 0 的面积:14900
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

2.2 计算轮廓的面积:contourArea函数


retval =cv2.contourArea(contour [, oriented] ))


  • retval 为轮廓面积。
  • contour 是其中一个轮廓。
  • oriented 是布尔型值。当它为True 时,返回的值包含正/负号,用来表示轮廓是顺时针还是逆时针的。该参数的默认值是False,表示返回的retval 是一个绝对值。

#输出三个轮廓的面积,将面积大于15 000 的轮廓筛选出来。
import cv2
import numpy as np
o = cv2.imread(‘contours.bmp’)
cv2.imshow(“original”,o)
gray = cv2.cvtColor(o,cv2.COLOR_BGR2GRAY)
ret, binary = cv2.threshold(gray,127,255,cv2.THRESH_BINARY)
image,contours, hierarchy = cv2.findContours(binary,cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE)
n=len(contours)
contoursImg=[]
for i in range(n):
print(“contours[“+str(i)+“]面积=”,cv2.contourArea(contours[i]))
temp=np.zeros(o.shape,np.uint8)
contoursImg.append(temp)
contoursImg[i]=cv2.drawContours(contoursImg[i],contours,i,(255,255,255),3)
if cv2.contourArea(contours[i])>15000:
cv2.imshow(“contours[“ + str(i)+“]”,contoursImg[i])
cv2.waitKey()
cv2.destroyAllWindows()

#输出以下信息
contours[0]面积= 13108.0
contours[1]面积= 19535.0
contours[2]面积= 12058.0
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

在这里插入图片描述


2.3 计算轮廓的周长:arcLength函数


retval = cv2.arcLength( curve, closed )


  • retval 是轮廓的周长
  • curve 是轮廓。
  • closed 是布尔型值,用来表示轮廓是否是封闭的。该值为True时,表示轮廓是封闭的。

3. Hu矩


Hu 矩是归一化中心矩的线性组合。Hu 矩在图像旋转、缩放、平移等操作后,仍能保持矩的不变性,所以经常会使用Hu 距来识别图像的特征。


在 OpenCV 中,使用函数cv2.HuMoments()可以得到Hu 距。该函数使用cv2.moments()函数的返回值作为参数,返回7个Hu矩值。


3.1 Hu矩函数


hu = cv2.HuMoments( m )


  • hu 表示返回的Hu矩值。
  • m 是由函数cv2.moments()计算得到矩特征值。

<a id=”32_%09_330”>3.2 形状匹配


我们可以通过Hu矩来判断两个对象的一致性。


函数 cv2.matchShapes()允许我们提供两个对象,对二者的Hu矩进行比较。这两个对象可以是轮廓,也可以是灰度图。不管是什么,cv2.matchShapes()都会提前计算好对象的Hu矩值。


retval = cv2.matchShapes( contour1, contour2, method, parameter )


  • contour1:第1个轮廓或者灰度图像。
  • contour2:第2个轮廓或者灰度图像。
  • method:比较两个对象的Hu 矩的方法。

在这里插入图片描述
在这里插入图片描述


  • parameter:应用于method 的特定参数,该参数为扩展参数,目前(截至OpenCV 4.1.0版本)暂不支持该参数,因此将该值设置为0。

#使用函数cv2.matchShapes()计算三幅不同图像的匹配度。
import cv2
o1 = cv2.imread(‘cs1.bmp’)
o2 = cv2.imread(‘cs2.bmp’)
o3 = cv2.imread(‘cc.bmp’)
gray1 = cv2.cvtColor(o1,cv2.COLOR_BGR2GRAY)
gray2 = cv2.cvtColor(o2,cv2.COLOR_BGR2GRAY)
gray3 = cv2.cvtColor(o3,cv2.COLOR_BGR2GRAY)
ret, binary1 = cv2.threshold(gray1,127,255,cv2.THRESH_BINARY)
ret, binary2 = cv2.threshold(gray2,127,255,cv2.THRESH_BINARY)
ret, binary3 = cv2.threshold(gray3,127,255,cv2.THRESH_BINARY)
image,contours1, hierarchy = cv2.findContours(binary1,cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE)
image,contours2, hierarchy = cv2.findContours(binary2,cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE)
image,contours3, hierarchy = cv2.findContours(binary3,cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE)
cnt1 = contours1[0]
cnt2 = contours2[0]
cnt3 = contours3[0]
ret0 = cv2.matchShapes(cnt1,cnt1,1,0.0)
ret1 = cv2.matchShapes(cnt1,cnt2,1,0.0)
ret2 = cv2.matchShapes(cnt1,cnt3,1,0.0)
print(“相同图像的matchShape=”,ret0)
print(“相似图像的matchShape=”,ret1)
print(“不相似图像的matchShape=”,ret2)

#运行后输出以下信息:
o1.shape= (425, 514, 3)
o2.shape= (42, 51, 3)
o3.shape= (425, 514, 3)
相同图像的 matchShape= 0.0
相似图像的 matchShape= 0.0001154058519395873
不相似图像的matchShape= 0.012935752303635306

在这里插入图片描述
从以上结果可以看出:


  • 同一幅图像的Hu 矩是不变的,二者差值为0。
  • 相似的图像即使发生了平移、旋转和缩放后,函数cv2.matchShapes()的返回值仍然比较接近。例如,图像o1和图像o2,o2是对o1经过缩放、旋转和平移后得到的,但是对二者应用cv2.matchShapes()函数后,返回值的差较小。
  • 不相似图像cv2.matchShapes()函数返回值的差较大。例如,图像o1和图像o3的差别较大,因此对二者应用cv2.matchShapes()函数后,返回值的差也较大。

4-5 轮廓拟合及凸包


OpenCV学习笔记(九)——图像轮廓(中)


6-7 利用形状场景算法比较轮廓及轮廓的特征值


OpenCV学习笔记(九)——图像轮廓(下)