前言
图像轮廓指的是图像中物体边缘或形状的外部线条,通常通过图像处理技术来检测和提取。轮廓是用于描述物体形状、进行目标识别、图像分割等操作的重要特征之一。
边缘检测虽然能够检测出边缘,但边缘是不连续的,检测到的边缘并不是一个整体。图像轮廓是指将边缘连接起来形成的一个整体,用于后续的计算。
OpenCV提供了查找图像轮廓的函数cv2.findContours(),该函数能够查找图像内的轮廓信息,而函数cv2.drawContours()能够将轮廓绘制出来。
图像轮廓是图像中非常重要的一个特征信息,通过对图像轮廓的操作,我们能够获取目标图像的大小、位置、方向等信息。
查找并绘制轮廓
一个轮廓对应着一系列的点,这些点以某种方式表示图像中的一条曲线。在OpenCV中,函数cv2.findContours()用于查找图像的轮廓,并能够根据参数返回特定表示方式的轮廓(曲线)。函数cv2.drawContours()能够将查找到的轮廓绘制到图像上,该函数可以根据参数在图像上绘制不同样式(实心/空心点,以及线条的不同粗细、颜色等)的轮廓,可以绘制全部轮廓也可以仅绘制指定的轮廓。
查找图像轮廓:findContours函数
函数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个点。
图1所示为提取的轮廓示例,函数cv2.findContours()提取出左图的3个轮廓,每一个轮廓都是由若干个像素点构成的。
下面针对图1来简单介绍一下contours的基本属性。
(1)type属性
返回值contours的type属性是list类型,list的每个元素都是图像的一个轮廓,用Numpy中的ndarray结构表示。
例如,使用如下语句获取轮廓contours的类型:
print (type(contours))
结果为<class ‘list’>。
使用如下语句获取轮廓contours中每个元素的类型:
print (type(contours[0]))
结果为<class ‘numpy.ndarray’>。
(2)轮廓的个数使用如下语句可以获取轮廓的个数:
使用如下语句可以获取轮廓的个数:
print (len(contours))
结果为“3”,表示在图1中,存在3个轮廓
(3)每个轮廓的点数每一个轮廓都是由若干个像素点构成的,点的个数不固定,具体个数取决于轮廓的形状。
例如,使用如下语句,可以获取每个轮廓内点的个数
print (len(contours[0])) #打印第0个轮廓的长度(点的个数):4
print (len(contours[1])) #打印第1个轮廓的长度(点的个数):60
print (len(contours[2])) #打印第2个轮廓的长度(点的个数):184
使用如下语句,可以获取每个轮廓内点的shape属性:
print(contours[0].shape)
print(contours[1].shape)
print(contours[2].shape)
(4)轮廓内的点
使用如下语句,可以获取轮廓内第0个轮廓中具体点的位置属性:
print (contours[0]) #打印第0个轮廓中的像素点
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(只检测外轮廓)例如,在图2中仅检测到两个外轮廓,轮廓的序号如图中的数字标注所示。
import cv2
# 读取图像
image = cv2.imread('C:\\Users\\Administrator\\Desktop\\res2.png', cv2.IMREAD_GRAYSCALE)
# 检测轮廓
# contours, hierarchy = cv2.findContours(image, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
contours, hierarchy = cv2.findContours(image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 打印轮廓的hierarchy信息
print("Hierarchy:\n", hierarchy)
# 显示图像和轮廓
cv2.imshow("Contours", image)
cv2.waitKey(0)
cv2.destroyAllWindows()
其中:
- 输出值“[ 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(对检测到的轮廓不建立等级关系)
例如,在图3中检测到三个轮廓,各个轮廓的序号如图中数字的标注所示。
import cv2
# 读取图像
image = cv2.imread('C:\\Users\\Administrator\\Desktop\\res2.png', cv2.IMREAD_GRAYSCALE)
# 检测轮廓
contours, hierarchy = cv2.findContours(image, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# contours, hierarchy = cv2.findContours(image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# cv2.RETR_LIST
contours, hierarchy = cv2.findContours(image, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
# 打印轮廓的hierarchy信息
print("Hierarchy:\n", hierarchy)
# 显示图像和轮廓
cv2.imshow("Contours", image)
cv2.waitKey(0)
cv2.destroyAllWindows()
其中:
- 输出值“[ 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时,建立两个等级的轮廓。上面的一层为外边界,下面的一层为内孔边界。
例如,在图4中检测到三个轮廓,各轮廓的序号如图中数字的标注所示。
import cv2
# 读取图像
image = cv2.imread('C:\\Users\\Administrator\\Desktop\\res2.png', cv2.IMREAD_GRAYSCALE)
# 检测轮廓
contours, hierarchy = cv2.findContours(image, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# contours, hierarchy = cv2.findContours(image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# cv2.RETR_LIST
contours, hierarchy = cv2.findContours(image, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
# 打印轮廓的hierarchy信息
print("Hierarchy:\n", hierarchy)
# 显示图像和轮廓
cv2.imshow("Contours", image)
cv2.waitKey(0)
cv2.destroyAllWindows()
其中:
- 输出值“[ 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(建立一个等级树结构的轮廓)
例如,在图5中检测到三个轮廓,各个轮廓的序号如图中的数字标注所示。
import cv2
# 读取图像
image = cv2.imread('C:\\Users\\Administrator\\Desktop\\res2.png', cv2.IMREAD_GRAYSCALE)
# 检测轮廓
# contours, hierarchy = cv2.findContours(image, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# contours, hierarchy = cv2.findContours(image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# cv2.RETR_LIST
contours, hierarchy = cv2.findContours(image, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# 打印轮廓的hierarchy信息
print("Hierarchy:\n", hierarchy)
# 显示图像和轮廓
cv2.imshow("Contours", image)
cv2.waitKey(0)
cv2.destroyAllWindows()
其中:
- 输出值“[ 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近似算法的一种风格。
例如,在图6中,左图是使用参数值cv2.CHAIN_APPROX_NONE存储的轮廓,保存了轮廓中的每一个点;右图是使用参数值cv2.CHAIN_APPROX_SIMPLE存储的轮廓,仅仅保存了边界上的四个点。
在使用函数cv2.findContours()查找图像轮廓时,需要注意以下问题:
- 待处理的源图像必须是灰度二值图。因此,在通常情况下,都要预先对图像进行阈值分割或者边缘检测处理,得到满意的二值图像后再将其作为参数使用。
- 在OpenCV中,都是从黑色背景中查找白色对象。因此,对象必须是白色的,背景必须是黑色的。
- 在OpenCV 4.x中,函数cv2.findContours()仅有两个返回值。
绘制图像轮廓:drawContours函数
在OpenCV中,可以使用函数cv2.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:偏移参数。该参数使轮廓偏移到不同的位置展示出来。函数cv2.drawContours()的参数image和返回值image,在函数运算后的值是相同的。因此,也可以将函数cv2.drawContours()写为没有返回值的形式:
cv2.drawContours(
image,
contours,
contourIdx,
color[,
thickness[,
lineType[,
hierarchy[,
maxLevel[,
offset]]]]] )
轮廓实例
【例1】绘制一幅图像内的所有轮廓。如果要绘制图像内的所有轮廓,需要将函数cv2.drawContours()的参数contourIdx的值设置为“-1”。根据题目的要求及分析,编写代码如下:
import cv2
# 读取图像
o = cv2.imread(r'C:\\Users\\Administrator\\Desktop\\res3.png')
cv2.imshow("original", o)
# 转换为灰度图像
gray = cv2.cvtColor(o, cv2.COLOR_BGR2GRAY)
# 应用二值化阈值处理
ret, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
# 检测轮廓
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()
在本程序中,轮廓的颜色被设置为红色(由于黑白印刷的原因,在纸质书中显示为灰色),即(0, 0, 255),参数thickness(轮廓线条的粗细)被设置为“5”。
运行上述程序,结果如图7所示,图像内的所有轮廓都被绘制出来了。
【例2】逐个显示一幅图像内的边缘信息。如果要绘制图像内的某个具体轮廓,需要将函数cv2.drawContours()的参数contourIdx设置为具体的索引值。本例通过循环语句逐一绘制轮廓。根据题目要求及分析,编写代码如下:
import cv2
import numpy as np
o = cv2.imread(r'C:\\Users\\Administrator\\Desktop\\res3.png')
cv2.imshow("original", o)
resized_img = cv2.resize(o, (300, 300)) # 调整大小到 300x300
cv2.imshow("original", resized_img)
gray = cv2.cvtColor(o, cv2.COLOR_BGR2GRAY)
ret, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
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)
# 缩放图像
resized_img = cv2.resize(contoursImg[i], (300, 300)) # 调整大小到 300x300
cv2.imshow(f"contours[{i}]", resized_img)
# cv2.imshow("contours[" + str(i) + "]", contoursImg[i])
cv2.waitKey()
cv2.destroyAllWindows()
运行上述程序,结果如图8所示,图像内的轮廓被逐一绘制出来。
【例3】使用轮廓绘制功能,提取前景对象。将函数cv2.drawContours()的参数thickness的值设置为“-1”,可以绘制前景对象的实心轮廓。将该实心轮廓与原始图像进行“按位与”操作,即可将前景对象从原始图像中提取出来。根据题目的要求及分析,编写代码如下:
# test.jpg
import cv2
import numpy as np
o = cv2.imread(r'C:\\Users\\Administrator\\Desktop\\test.jpg')
# cv2.imshow("original", o)
resized_img = cv2.resize(o, (350, 500)) # 调整大小到 300x300
cv2.imshow("original", resized_img)
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)
contours, hierarchy = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
mask = np.zeros(o.shape, np.uint8)
mask = cv2.drawContours(mask, contours, -1, (255, 255, 255), -1)
mask = cv2.resize(mask, (350, 500)) # 调整大小到 300x300
cv2.imshow("mask", mask)
loc = cv2.bitwise_and(resized_img, mask)
loc = cv2.resize(loc, (350, 500)) # 调整大小到 300x300
cv2.imshow("location", loc)
cv2.waitKey()
cv2.destroyAllWindows()
本例中将函数cv2.drawContours()的参数thickness设置为“-1”,得到了前景对象的实心轮廓mask。接下来,通过语句“cv2.bitwise_and(o, mask)”,将原始图像o与实心轮廓mask进行“按位与”运算,就得到了原始图像的前景对象。运行上述程序,结果如图9所示。其中:
矩特征
比较两个轮廓最简单的方法是比较二者的轮廓矩。轮廓矩代表了一个轮廓、一幅图像、一组点集的全局特征。矩信息包含了对应对象不同类型的几何特征,例如大小、位置、角度、形状等。矩特征被广泛地应用在模式识别、图像识别等方面。
矩的计算:moments函数
OpenCV提供了函数cv2.moments()来获取图像的moments特征。通常情况下,我们将使用函数cv2.moments()获取的轮廓特征称为“轮廓矩”。轮廓矩描述了一个轮廓的重要特征,使用轮廓矩可以方便地比较两个轮廓。函数cv2.moments()的语法格式为:
retval = cv2.moments( array[, binaryImage] )
式中有两个参数:
- array:可以是点集,也可以是灰度图像或者二值图像。当array是点集时,函数会把这些点集当成轮廓中的顶点,把整个点集作为一条轮廓,而不是把它们当成独立的点来看待。
- binaryImage:该参数为True时,array内所有的非零值都被处理为1。该参数仅在参数array为图像时有效。
该函数的返回值retval是矩特征,主要包括:
(1)空间矩
- 零阶矩:m00
- 一阶矩:m10, m01
- 二阶矩:m20, m11, m02
- 三阶矩:m30, m21, m12, m03
(2)中心矩
- ● 二阶中心矩:mu20, mu11, mu02
- ● 三阶中心矩:mu30, mu21, mu12, mu03
(3)归一化中心矩
- ● 二阶Hu矩:nu20, nu11, nu02
- ● 三阶Hu矩:nu30, nu21, nu12, nu03
上述矩都是根据公式计算得到的,大多数矩比较抽象。但是很明显,如果两个轮廓的矩一致,那么这两个轮廓就是一致的。虽然大多数矩都是通过数学公式计算得到的抽象特征,但是零阶矩“m00”的含义比较直观,它表示一个轮廓的面积。
矩特征函数cv2.moments()所返回的特征值,能够用来比较两个轮廓是否相似。例如,有两个轮廓,不管它们出现在图像的哪个位置,我们都可以通过函数cv2.moments()的m00矩判断其面积是否一致。
在位置发生变化时,虽然轮廓的面积、周长等特征不变,但是更高阶的特征会随着位置的变化而发生变化。在很多情况下,我们希望比较不同位置的两个对象的一致性。解决这一问题的方法是引入中心矩。中心矩通过减去均值而获取平移不变性,因而能够比较不同位置的两个对象是否一致。很明显,中心矩具有的平移不变性,使它能够忽略两个对象的位置关系,帮助我们比较不同位置上两个对象的一致性。
除了考虑平移不变性外,我们还会考虑经过缩放后大小不一致的对象的一致性。也就是说,我们希望图像在缩放前后能够拥有一个稳定的特征值。也就是说,让图像在缩放前后具有同样的特征值。显然,中心矩不具有这个属性。例如,两个形状一致、大小不一的对象,其中心矩是有差异的。
归一化中心矩通过除以物体总尺寸而获得缩放不变性。它通过上述计算提取对象的归一化中心矩属性值,该属性值不仅具有平移不变性,还具有缩放不变性。
在OpenCV中,函数cv2.moments()会同时计算上述空间矩、中心矩和归一化中心距。
【例4】使用函数cv2.moments()提取一幅图像的特征。根据题目的要求及分析,编写代码如下:
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()
本例中,首先使用函数cv2.moments()提取各个轮廓的特征;接下来,通过语句cv2.moments(contours[i])[‘m00’])提取各个轮廓矩的面积信息。
运行上述程序,会显示如图10所示的图像。其中:
- ● (a)图是原始图像。
- ● (b)图是原始图像的第0个轮廓。
- ● ©图是原始图像的第1个轮廓。
- ● (d)图是原始图像的第2个轮廓。
同时,程序会显示如下输出结果:
观察各个轮廓的矩(moments):
轮廓 0 的矩:
{'m00': 2953.5, 'm10': 141914.0, 'm01': 674953.8333333333, 'm20': 7297301.083333333, 'm11': 32432175.458333332, 'm02': 155353008.91666666, 'm30': 396608955.90000004, 'm21': 1667753129.2333333, 'm12': 7465179622.3, 'm03': 36008721801.950005, 'mu20': 478413.86613340117, 'mu11': 1026.5490167029202, 'mu02': 1107985.3407868445, 'mu30': 2747.422912657261, 'mu21': 25787.93059720099, 'mu12': 86460.36071126908, 'mu03': -9125.510330200195, 'nu20': 0.05484408663078589, 'nu11': 0.00011768083491774101, 'nu02': 0.12701647740036529, 'nu30': 5.795395481220832e-06, 'nu21': 5.439688799439629e-05, 'nu12': 0.0001823789055053718, 'nu03': -1.924929033962443e-05}
轮廓 0 的面积:2953.5
轮廓 1 的矩:
{'m00': 6956.5, 'm10': 1133880.6666666665, 'm01': 834706.1666666666, 'm20': 192610842.25, 'm11': 136054500.54166666, 'm02': 102240819.08333333, 'm30': 33935221179.300003, 'm21': 23111377235.933334, 'm12': 16665018223.266666, 'm03': 12768156053.050001, 'mu20': 7792993.2971582115, 'mu11': 855.0689752697945, 'mu02': 2084938.299697727, 'mu30': 1012.8543243408203, 'mu21': -158289.87633812428, 'mu12': -16717.500845849514, 'mu03': 1969.0043239593506, 'nu20': 0.16103590702740464, 'nu11': 1.7669309179795806e-05, 'nu02': 0.0430835646054556, 'nu30': 2.5094006894295327e-07, 'nu21': -3.921716235660208e-05, 'nu12': -4.141850129870927e-06, 'nu03': 4.878313385539436e-07}
轮廓 1 的面积:6956.5
轮廓 2 的矩:
{'m00': 779.0, 'm10': 233716.5, 'm01': 18296.0, 'm20': 70194404.66666666, 'm11': 5489360.916666666, 'm02': 463822.5, 'm30': 21104513696.25, 'm21': 1648724552.9166667, 'm12': 139165821.08333334, 'm03': 12496190.0, 'mu20': 74504.3171801418, 'mu11': 173.3890671795234, 'mu02': 34113.10847240052, 'mu30': -237.9855079650879, 'mu21': -1859.8601903982926, 'mu12': 1102.246367705986, 'mu03': 216.54015137441456, 'nu20': 0.12277403336317387, 'nu11': 0.00028572404827545173, 'nu02': 0.05621424470726354, 'nu30': -1.4050988149079054e-05, 'nu21': -0.00010980867582098307, 'nu12': 6.507812506076738e-05, 'nu03': 1.2784825121402051e-05}
轮廓 2 的面积:779.0
补充:
计算轮廓的面积:contourArea函数
计算轮廓的长度:arcLength函数
Hu矩
Hu矩是归一化中心矩的线性组合。Hu矩在图像旋转、缩放、平移等操作后,仍能保持矩的不变性,所以经常会使用Hu距来识别图像的特征。
在OpenCV中,使用函数cv2.HuMoments()可以得到Hu距。该函数使用cv2.moments()函数的返回值作为参数,返回7个Hu矩值。
Hu矩函数
函数cv2.HuMoments()的语法格式为:
hu = cv2.HuMoments( m )
式中返回值hu,表示返回的Hu矩值;参数m,是由函数cv2.moments()计算得到矩特征值。
【例8】计算图像的Hu矩,对其中第0个矩的关系进行演示。
Hu矩是归一化中心矩的线性组合,每一个矩都是通过归一化中心矩的组合运算得到的。函数cv2.moments()返回的归一化中心矩中包含:
- 二阶Hu矩: n u 20 , n u 11 , n u 02 nu_{20}, nu_{11}, nu_{02} nu20,nu11,nu02
- 三阶Hu矩: n u 30 , n u 21 , n u 12 , n u 03 nu_{30}, nu_{21}, nu_{12}, nu_{03} nu30,nu21,nu12,nu03
为了表述上的方便,将上述字母nu
表示为字母v
,则归一化中心矩为:
- 二阶Hu矩: v 20 , v 11 , v 02 v20, v11, v02 v20,v11,v02
- 三阶Hu矩: v 30 , v 21 , v 12 , v 03 v30, v21, v12, v03 v30,v21,v12,v03
上述7个Hu矩的计算公式为:
本例对Hu矩中的第0个矩 h 0 = v 20 + v 02 ℎ_0=v_{20}+v_{02} h0=v20+v02的关系进行验证,即Hu矩中第0个矩对应的函数cv2.moments()的返回值为:
h 0 = n u 20 + n u 02 ℎ0=nu20+nu02 h0=nu20+nu02
根据题目的要求及分析,编写代码如下:
import cv2
# o1 = cv2.imread('cs1.bmp')
o1 = cv2.imread(r'C:\\Users\\Administrator\\Desktop\\moment.png')
gray = cv2.cvtColor(o1, cv2.COLOR_BGR2GRAY)
HuM1=cv2.HuMoments(cv2.moments(gray)).flatten()
print("cv2.moments(gray)=\n", cv2.moments(gray))
print("\nHuM1=\n", HuM1)
print("\ncv2.moments(gray)['nu20']+cv2.moments(gray)['nu02']=%f+%f=%f\n"
%(cv2.moments(gray)['nu20'], cv2.moments(gray)['nu02'],
cv2.moments(gray)['nu20']+cv2.moments(gray)['nu02']))
print("HuM1[0]=", HuM1[0])
print("\nHu[0]-(nu02+nu20)=",
HuM1[0]-(cv2.moments(gray)['nu20']+cv2.moments(gray)['nu02']))
程序运行结果显示Hu[0]-(nu02+nu20)=0.0
。从该结果可知,关系
h
0
=
n
u
20
+
n
u
02
ℎ_0=nu_{20}+nu_{02}
h0=nu20+nu02成立。
cv2.moments(gray)=
{'m00': 2819590.0, 'm10': 397570476.0, 'm01': 403860142.0, 'm20': 71305223188.0, 'm11': 45735796942.0, 'm02': 68512957510.0, 'm30': 14697923289666.0, 'm21': 6934198814642.0, 'm12': 6404215738518.0, 'm03': 13009490405824.0, 'mu20': 15246617721.649727, 'mu11': -11209669913.949831, 'mu02': 10666598891.810768, 'mu30': 344041464963.81635, 'mu21': -117915129251.81013, 'mu12': -45108937401.78011, 'mu03': 140498447760.5766, 'nu20': 0.0019177923774410773, 'nu11': -0.0014100058063420402, 'nu02': 0.0013416957400911801, 'nu30': 2.57718671649104e-05, 'nu21': -8.832926717512085e-06, 'nu12': -3.3790739229389187e-06, 'nu03': 1.0524624794695901e-05}
HuM1=
[ 3.25948812e-03 8.28435283e-06 2.66019518e-09 5.04299032e-10
3.02264955e-19 7.35730936e-14 -4.99811415e-19]
cv2.moments(gray)['nu20']+cv2.moments(gray)['nu02']=0.001918+0.001342=0.003259
HuM1[0]= 0.0032594881175322574
Hu[0]-(nu02+nu20)= 0.0
Process finished with exit code 0
【例9】计算三幅不同图像的Hu矩,并进行比较。根据题目的要求,编写代码如下:
import cv2
#----------------计算图像o1的Hu矩-------------------
o1 = cv2.imread('cs1.bmp')
gray1 = cv2.cvtColor(o1, cv2.COLOR_BGR2GRAY)
HuM1=cv2.HuMoments(cv2.moments(gray1)).flatten()
#----------------计算图像o2的Hu矩-------------------
o2 = cv2.imread('cs3.bmp')
gray2 = cv2.cvtColor(o2, cv2.COLOR_BGR2GRAY)
HuM2=cv2.HuMoments(cv2.moments(gray2)).flatten()
#----------------计算图像o3的Hu矩-------------------
o3 = cv2.imread('lena.bmp')
gray3 = cv2.cvtColor(o3, cv2.COLOR_BGR2GRAY)
HuM3=cv2.HuMoments(cv2.moments(gray3)).flatten()
#---------打印图像o1、图像o2、图像o3的特征值------------
print("o1.shape=", o1.shape)
print("o2.shape=", o2.shape)
print("o3.shape=", o3.shape)
print("cv2.moments(gray1)=\n", cv2.moments(gray1))
print("cv2.moments(gray2)=\n", cv2.moments(gray2))
print("cv2.moments(gray3)=\n", cv2.moments(gray3))
print("\nHuM1=\n", HuM1)
print("\nHuM2=\n", HuM2)
print("\nHuM3=\n", HuM3)
#---------计算图像o1与图像o2、图像o3的Hu矩之差----------------
print("\nHuM1-HuM2=", HuM1-HuM2)
print("\nHuM1-HuM3=", HuM1-HuM3)
#---------显示图像----------------
cv2.imshow("original1", o1)
cv2.imshow("original2", o2)
cv2.imshow("original3", o3)
cv2.waitKey()
cv2.destroyAllWindows()
运行上述程序,会显示各个图像的shape属性、moments属性、HuMoments属性,以及不同图像的Hu矩之差。
D:\Code\py_demo\.venv\Scripts\python.exe D:\Code\py_demo\operator_cv\sb_hu.py
o1.shape= (400, 500, 3)
o2.shape= (400, 500, 3)
o3.shape= (400, 500, 3)
cv2.moments(gray1)=
{'m00': 23410570.0, 'm10': 6894702439.0, 'm01': 4322923538.0, 'm20': 2490728253949.0, 'm11': 1296698767586.0, 'm02': 1124670909868.0, 'm30': 975876474148585.0, 'm21': 473799154186464.0, 'm12': 342275113887012.0, 'm03': 337006213255976.0, 'mu20': 460153102538.6518, 'mu11': 23544313804.690384, 'mu02': 326413203396.332, 'mu30': -28715366381641.523, 'mu21': 785697984.9051791, 'mu12': 2350378948735.8237, 'mu03': 8779049674558.577, 'nu20': 0.0008396115002459006, 'nu11': 4.2959781270095615e-05, 'nu02': 0.0005955849865874677, 'nu30': -1.0828901211539646e-05, 'nu21': 2.962959186229814e-10, 'nu12': 8.863554484129715e-07, 'nu03': 3.3106825242450395e-06}
cv2.moments(gray2)=
{'m00': 28034550.0, 'm10': 6816434088.0, 'm01': 5445534050.0, 'm20': 2251390234926.0, 'm11': 1331515245914.0, 'm02': 1388596230890.0, 'm30': 841561841935968.0, 'm21': 438638459033146.0, 'm12': 340557983142876.0, 'm03': 400222877167790.0, 'mu20': 594014829362.2085, 'mu11': 7466030721.150593, 'mu02': 330835678653.22784, 'mu30': 5287351850554.212, 'mu21': -2310498476300.1636, 'mu12': 28610218122.137913, 'mu03': 1971363833879.6504, 'nu20': 0.0007558056051916368, 'nu11': 9.4995403963859e-06, 'nu02': 0.0004209448114148325, 'nu30': 1.270586505153425e-06, 'nu21': -5.552284521895067e-07, 'nu12': 6.8752294310948995e-09, 'nu03': 4.7373209781990474e-07}
cv2.moments(gray3)=
{'m00': 26617062.0, 'm10': 6626512283.0, 'm01': 4619614220.0, 'm20': 2204631080519.0, 'm11': 1158027266838.0, 'm02': 1224946110016.0, 'm30': 825815455682453.0, 'm21': 385764754114036.0, 'm12': 307812229426686.0, 'm03': 378612525048194.0, 'mu20': 554912376149.9802, 'mu11': 7940515281.743969, 'mu02': 423173339541.70013, 'mu30': 657431568603.1985, 'mu21': -821148323850.9675, 'mu12': 96649147636.55907, 'mu03': 19122299481335.758, 'nu20': 0.0007832568081619774, 'nu11': 1.1208008547027264e-05, 'nu02': 0.0005973076353573567, 'nu30': 1.7986649164798885e-07, 'nu21': -2.2465770612065727e-07, 'nu12': 2.6442209252425936e-08, 'nu03': 5.231663771877604e-06}
HuM1=
[1.43519649e-03 6.69311106e-08 1.92880003e-10 1.09816797e-10
5.71662342e-21 1.57910213e-14 1.49252448e-20]
HuM2=
[ 1.17675042e-03 1.12492716e-07 6.13950909e-12 1.63855014e-12
3.42641324e-24 5.40282209e-16 -3.90755201e-24]
HuM3=
[ 1.38056444e-03 3.50795727e-08 3.48866553e-11 2.51126730e-11
7.35977227e-22 -4.60754057e-15 1.04139331e-22]
HuM1-HuM2= [ 2.58446070e-04 -4.55616057e-08 1.86740494e-10 1.08178247e-10
5.71319700e-21 1.52507391e-14 1.49291523e-20]
HuM1-HuM3= [5.46320433e-05 3.18515379e-08 1.57993347e-10 8.47041240e-11
4.98064619e-21 2.03985618e-14 1.48211054e-20]
同时,还会显示三幅原始图像,如图11所示:
从上述输出结果可以看到,由于Hu矩的值本身就非常小,因此在这里并没有发现两个对象的Hu矩差值的特殊意义。
形状匹配
我们可以通过Hu矩来判断两个对象的一致性。例如,前面计算了两个对象Hu矩的差,但是结果比较抽象。为了更直观方便地比较Hu矩值,OpenCV提供了函数cv2.matchShapes(),对两个对象的Hu矩进行比较。
函数cv2.matchShapes()允许我们提供两个对象,对二者的Hu矩进行比较。这两个对象可以是轮廓,也可以是灰度图。不管是什么,cv2.matchShapes()都会提前计算好对象的Hu矩值。
函数cv2.matchShapes()的语法格式为:
retval = cv2.matchShapes( contour1, contour2, method, parameter )
式中retval是返回值。
该函数有如下4个参数:
- contour1:第1个轮廓或者灰度图像。
- contour2:第2个轮廓或者灰度图像。
- method:比较两个对象的Hu矩的方法,具体如表1所示。
在表1中,A表示对象1, B表示对象2:
式中,和分别是对象A和对象B的Hu矩。
- parameter:应用于method的特定参数,该参数为扩展参数,目前(截至OpenCV 4.1.0版本)暂不支持该参数,因此将该值设置为0。
【例10】使用函数cv2.matchShapes()计算三幅不同图像的匹配度。根据题目要求,编写代码如下:
import cv2
o1 = cv2.imread('aa1.jpeg')
o2 = cv2.imread('aa2.png')
o3 = cv2.imread('aa3.jpeg')
gray1 = cv2.cvtColor(o1, cv2.COLOR_BGR2GRAY)
gray2 = cv2.cvtColor(o2, cv2.COLOR_BGR2GRAY)
gray3 = cv2.cvtColor(o3, cv2.COLOR_BGR2GRAY)
cv2.imshow('o1',gray1)
cv2.imshow('o2',gray2)
cv2.imshow('o3',gray3)
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)
contours1, hierarchy = cv2.findContours(binary1,
cv2.RETR_LIST,
cv2.CHAIN_APPROX_SIMPLE)
contours2, hierarchy = cv2.findContours(binary2,
cv2.RETR_LIST,
cv2.CHAIN_APPROX_SIMPLE)
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)
###################################### result #################################
D:\Code\py_demo\.venv\Scripts\python.exe D:\Code\py_demo\operator_cv\sb_hu2.py
相同图像的matchShape= 0.0
相似图像的matchShape= 0.9588739815881875
不相似图像的matchShape= 1.7976931348623157e+308
从以上结果可以看出:
- 同一幅图像的Hu矩是不变的,二者差值为0。
- 相似的图像即使发生了平移、旋转和缩放后,函数cv2.matchShapes()的返回值仍然比较接近。例如,图像o1和图像o2, o2是对o1经过缩放、旋转和平移后得到的,但是对二者应用cv2.matchShapes()函数后,返回值的差较小。
- 不相似图像cv2.matchShapes()函数返回值的差较大。例如,图像o1和图像o3的差别较大,因此对二者应用cv2.matchShapes()函数后,返回值的差也较大。