Python版本是Python3.7.3,OpenCV版本OpenCV3.4.1,开发环境为PyCharm
边缘检测虽然能够检测出边缘,但边缘是不连续的,检测到的边缘并不是一个整体。图像轮廓是指将边缘连接起来形成的一个整体,用于后续的计算。
OpenCV提供了查找图像轮廓的函数cv2.findContours(),该函数能够查找图像内的轮廓信息,而函数cv2.drawContours()能够将轮廓绘制出来。
图像轮廓是图像中非常重要的一个特征信息,通过对图像轮廓的操作,我们能够获取目标图像的大小、位置、方向等信息。
一个轮廓对应着一系列的点,这些点以某种方式表示图像中的一条曲线。在OpenCV中,函数cv2.findContours()用于查找图像的轮廓,并能够根据参数返回特定表示方式的轮廓(曲线)。函数cv2.drawContours()能够将查找到的轮廓绘制到图像上,该函数可以根据参数在图像上绘制不同样式(实心/空心点,以及线条的不同粗细、颜色等)的轮廓,可以绘制全部轮廓也可以仅绘制指定的轮廓。
函数cv2.findContours()的语法格式为:
image, contours, hierarchy = cv2.findContours( image, mode, method)
式中的返回值为:
● image:与函数参数中的原始图像image一致。
● contours:返回的轮廓。
● hierarchy:图像的拓扑信息(轮廓层次)。
式中的参数为:
● image:原始图像。8位单通道图像,所有非零值被处理为1,所有零值保持不变。也就是说灰度图像会被自动处理为二值图像。在实际操作时,可以根据需要,预先使用阈值处理等函数将待查找轮廓的图像处理为二值图像。
● mode:轮廓检索模式。
● method:轮廓的近似方法。
函数cv2.findContours()的返回值及参数的含义比较丰富,下面对上述返回值和参数逐一做出说明。
1.返回值image
该返回值与参数image是一致的,就是原始输入图像。在OpenCV 4.X中,该返回值已经被取消。在OpenCV 4.X中,函数cv2.findContours()仅有两个返回值,其语法格式为:
contours, hierarchy = cv2.findContours( image, mode, method)
2.返回值contours
该返回值返回的是一组轮廓信息,每个轮廓都是由若干个点所构成的。例如,contours[i]是第i个轮廓(下标从0开始), contours[i][j]是第i个轮廓内的第j个点。
下图所示为提取的轮廓示例,函数cv2.findContours()提取出左图的3个轮廓,每一个轮廓都是由若干个像素点构成的。
下面针对上图来简单介绍一下contours的基本属性。
(1)type属性
返回值contours的type属性是list类陌上花开 21:57:25
型,list的每个元素都是图像的一个轮廓,用Numpy中的ndarray结构表示。
例如,使用如下语句获取轮廓contours的类型:
print (type(contours))
结果为
使用如下语句获取轮廓contours中每个元素的类型:
print (type(contours[0]))
结果为
(2)轮廓的个数
使用如下语句可以获取轮廓的个数:
print (len(contours))
结果为“3”,表示在上图中,存在3个轮廓。
(3)每个轮廓的点数
每一个轮廓都是由若干个像素点构成的,点的个数不固定,具体个数取决于轮廓的形状。
例如,使用如下语句,可以获取每个轮廓内点的个数:
print (len(contours[0])) #打印第0个轮廓的长度(点的个数):4
print (len(contours[1])) #打印第1个轮廓的长度(点的个数):60
print (len(contours[2])) #打印第2个轮廓的长度(点的个数):184
输出结果如下:
4
60
184
使用如下语句,可以获取每个轮廓内点的shape属性:
print(contours[0].shape)
print(contours[1].shape)
print(contours[2].shape)
输出结果如下:
(4, 1, 2)
(60, 1, 2)
(184, 1, 2)
(4)轮廓内的点
使用如下语句,可以获取轮廓内第0个轮廓中具体点的位置属性:
print (contours[0]) #打印第0个轮廓中的像素点
contours[0]对应着上图中右图左下角矩形轮廓的点,输出结果如下:
[[[ 79270]]
[[ 79383]]
[[195383]]
[[195270]]]
3.返回值hierarchy
图像内的轮廓可能位于不同的位置。比如,一个轮廓在另一个轮廓的内部。在这种情况下,我们将外部的轮廓称为父轮廓,内部的轮廓称为子轮廓。按照上述关系分类,一幅图像中所有轮廓之间就建立了父子关系。
根据轮廓之间的关系,就能够确定一个轮廓与其他轮廓是如何连接的。比如,确定一个轮廓是某个轮廓的子轮廓,或者是某个轮廓的父轮廓。上述关系被称为层次(组织结构),返回值hierarchy就包含上述层次关系。
每个轮廓contours[i]对应4个元素来说明当前轮廓的层次关系。其形式为:
[Next, Previous, First_Child, Parent]
式中各元素的含义为:
● Next:后一个轮廓的索引编号。
● Previous:前一个轮廓的索引编号。
● First_Child:第1个子轮廓的索引编号。
● Parent:父轮廓的索引编号。
如果上述各个参数所对应的关系为空时,也就是没有对应的关系时,则将该参数所对应的值设为“-1”。
使用print语句可以查看hierarchy的值:
print(hierarchy)
需要注意,轮廓的层次结构是由参数 mode决定的。也就是说,使用不同的mode,得到轮廓的编号是不一样的,得到的hierarchy也不一样。
4.参数image
该参数表示输入的图像,必须是8位单通道二值图像。一般情况下,都是将图像处理为二值图像后,再将其作为image参数使用的。
5.参数mode
参数mode决定了轮廓的提取方式,具体有如下4种:
● cv2.RETR_EXTERNAL:只检测外轮廓。
● cv2.RETR_LIST:对检测到的轮廓不建立等级关系。
● cv2.RETR_CCOMP:检索所有轮廓并将它们组织成两级层次结构。上面的一层为外边界,下面的一层为内孔的边界。如果内孔内还有一个连通物体,那么这个物体的边界仍然位于顶层。
● cv2.RETR_TREE:建立一个等级树结构的轮廓。
下面分别对这四种情况进行简单的说明。
(1)cv2.RETR_EXTERNAL(只检测外轮廓)
例如,在下图中仅检测到两个外轮廓,轮廓的序号如图中的数字标注所示。
print(hierarchy)
[[[ 1-1-1-1]
[-1 0-1-1]]]
其中:
● 输出值“[ 1-1-1-1]”,表示的是第0个轮廓的层次。
● 它(即第0个轮廓)的后一个轮廓就是第1个轮廓,因此第1个元素的值是“1”。
● 它的前一个轮廓不存在,因此第2个元素的值是“-1”。
● 它不存在子轮廓,因此第3个元素的值是“-1”。
● 它不存在父轮廓,因此第4个元素的值是“-1”。
● 输出值“[-1 0-1-1]”,表示的是第1个轮廓的层次。
● 它(即第1个轮廓)的后一个轮廓是不存在的,因此第1个元素的值是“-1”。
● 它的前一个轮廓是第0个轮廓,因此第2个元素的值是“0”。
● 它不存在子轮廓,因此第3个元素的值是“-1”。
● 它不存在父轮廓,因此第4个元素的值是“-1”。
此时,轮廓之间的关系为:
(2)cv2.RETR_LIST(对检测到的轮廓不建立等级关系)
例如,在下图中检测到三个轮廓,各个轮廓的序号如图中数字的标注所示。
使用print语句,可以查看hierarchy的值:
print(hierarchy)
[[[ 1-1-1-1]
[ 2 0-1-1]
[-1 1-1-1]]]
其中:
● 输出值“[ 1-1-1-1]”,表示的是第0个轮廓的层次。
● 它(即第0个轮廓)的后一个轮廓是第1个轮廓,因此第1个元素的值是“1”。
● 它的前一个轮廓不存在,因此第2个元素的值是“-1”。
● 它不存在子轮廓,因此第3个元素的值是“-1”。
● 它不存在父轮廓,因此第4个元素的值是“-1”。
● 输出值“[2 0-1-1]”,表示的是第1个轮廓的层次。
● 它(即第1个轮廓)的后一个轮廓是第2个轮廓,因此第1个元素的值是“2”。
● 它的前一个轮廓是第0个轮廓,因此第2个元素的值是“0”。
● 它不存在子轮廓,因此第3个元素的值是“-1”。
● 它不存在父轮廓,因此第4个元素的值是“-1”。
● 输出值“[-1 1-1-1]”,表示的是第2个轮廓的层次。
● 它(即第2个轮廓)的后一个轮廓是不存在的,因此第1个元素的值是“-1”。
● 它的前一个轮廓是第1个轮廓,因此第2个元素的值是“1”。
● 它不存在子轮廓,因此第3个元素的值是“-1”。
● 它不存在父轮廓,因此第4个元素的值是“-1”。
从上述分析可以看出,当参数mode为cv2.RETR_LIST时,各个轮廓之间没有建立父子关系。
此时,轮廓之间的关系为:
(3)cv2.RETR_CCOMP(建立两个等级的轮廓)
当参数mode为cv2.RETR_CCOMP时,建立两个等级的轮廓。上面的一层为外边界,下面的一层为内孔边界。
例如,在下图中检测到三个轮廓,各轮廓的序号如图中数字的标注所示。
print(hierarchy)
[[[ 1-1-1-1]
[-1 0 2-1]
[-1-1-1 1]]]
其中:
● 输出值“[ 1-1-1-1]”,表示的是第0个轮廓的层次。
● 它(即第0个轮廓)的后一个轮廓是第1个轮廓,因此第1个元素的值是“1”。
● 它的前一个轮廓不存在,因此第2个元素的值是“-1”。
● 它不存在子轮廓,因此第3个元素的值是“-1”。
● 它不存在父轮廓,因此第4个元素的值是“-1”。
● 输出值“[-1 0 2-1]”,表示的是第1个轮廓的层次。
● 它(即第1个轮廓)的后一个轮廓不存在,因此第1个元素的值是“-1”。
● 它的前一个轮廓是第0个轮廓,因此第2个元素的值是“0”。
● 它的第1个子轮廓是第2个轮廓,因此第3个元素的值是“2”。
● 它不存在父轮廓,因此第4个元素的值是“-1”。
● 输出值“[-1-1-1 1]”,表示的是第2个轮廓的层次。
● 它(即第2个轮廓)的后一个轮廓不存在,因此第1个元素的值是“-1”。
● 它的前一个轮廓也不存在,因此第2个元素的值是“-1”。
● 它不存在子轮廓,因此第3个元素的值是“-1”。
● 它的父轮廓是第1个轮廓,因此第4个元素的值是“1”。
此时,轮廓关系为:
(4)cv2.RETR_TREE(建立一个等级树结构的轮廓)
例如,在下图中检测到三个轮廓,各个轮廓的序号如图中的数字标注所示。
print(hierarchy)
[[[ 1-1-1-1]
[-1 0 2-1]
[-1-1-1 1]]]
其中:
● 输出值“[ 1-1-1-1]”,表示的是第0个轮廓的层次。
● 它(即第0个轮廓)的后一个轮廓是第1个轮廓,因此第1个元素的值为“1”。
● 它的前一个轮廓不存在,因此第2个元素的值是“-1”。
● 它不存在子轮廓,因此第3个元素的值是“-1”。
● 它不存在父轮廓,因此第4个元素的值是“-1”。
● 输出值“[-1 0 2-1]”,表示的是第1个轮廓的层次。
● 它(即第1个轮廓)的后一个轮廓不存在,因此第1个元素的值是“-1”。
● 它的前一个轮廓是第0个轮廓,因此第2个元素的值是“0”。
● 它的第1个子轮廓是第2个轮廓,因此第3个元素的值是“2”。
● 它的父轮廓不存在,因此第4个元素的值是“-1”。
● 输出值“[-1-1-1 1]”,表示的是第2个轮廓的层次。
● 它(即第2个轮廓)的后一个轮廓不存在,因此第1个元素的值是“-1”。
● 它的前一个轮廓是不存在的,因此第2个元素的值是“-1”。
● 它的子轮廓是不存在的,因此第3个元素的值是“-1”。
● 它的父轮廓是第1个轮廓,因此第1个元素的值是“1”。
此时,轮廓之间的关系为:
需要注意,本例中仅有两层轮廓,所以使用参数cv2.RETR_CCOMP和cv2.RETR_TREE得到的层次结构是一致的。当有多层轮廓时,使用参数cv2.RETR_CCOMP也会得到仅有两层的层次结构;而使用参数cv2.RETR_TREE会得到含有多个层次的结构。
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()仅有两个返回值。