opencv之图像轮廓

文章目录


前言

图像轮廓指的是图像中物体边缘或形状的外部线条,通常通过图像处理技术来检测和提取。轮廓是用于描述物体形状、进行目标识别、图像分割等操作的重要特征之一。

边缘检测虽然能够检测出边缘,但边缘是不连续的,检测到的边缘并不是一个整体。图像轮廓是指将边缘连接起来形成的一个整体,用于后续的计算。

OpenCV提供了查找图像轮廓的函数cv2.findContours(),该函数能够查找图像内的轮廓信息,而函数cv2.drawContours()能够将轮廓绘制出来。

图像轮廓是图像中非常重要的一个特征信息,通过对图像轮廓的操作,我们能够获取目标图像的大小、位置、方向等信息。

查找并绘制轮廓

一个轮廓对应着一系列的点,这些点以某种方式表示图像中的一条曲线。在OpenCV中,函数cv2.findContours()用于查找图像的轮廓,并能够根据参数返回特定表示方式的轮廓(曲线)。函数cv2.drawContours()能够将查找到的轮廓绘制到图像上,该函数可以根据参数在图像上绘制不同样式(实心/空心点,以及线条的不同粗细、颜色等)的轮廓,可以绘制全部轮廓也可以仅绘制指定的轮廓。

查找图像轮廓:findContours函数

函数cv2.findContours()的语法格式为:

python 复制代码
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()仅有两个返回值,其语法格式为:

python 复制代码
contours, hierarchy = cv2.findContours( image, mode, method)

2.返回值contours

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

图1所示为提取的轮廓示例,函数cv2.findContours()提取出左图的3个轮廓,每一个轮廓都是由若干个像素点构成的。

图1

下面针对图1来简单介绍一下contours的基本属性。

(1)type属性

返回值contours的type属性是list类型,list的每个元素都是图像的一个轮廓,用Numpy中的ndarray结构表示。

例如,使用如下语句获取轮廓contours的类型:

python 复制代码
print (type(contours))

结果为<class 'list'>。

使用如下语句获取轮廓contours中每个元素的类型:

python 复制代码
print (type(contours[0]))

结果为<class 'numpy.ndarray'>。

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

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

python 复制代码
print (len(contours))

结果为"3",表示在图1中,存在3个轮廓

(3)每个轮廓的点数每一个轮廓都是由若干个像素点构成的,点的个数不固定,具体个数取决于轮廓的形状。

例如,使用如下语句,可以获取每个轮廓内点的个数

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

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

python 复制代码
print(contours[0].shape)
print(contours[1].shape)
print(contours[2].shape)

(4)轮廓内的点

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

python 复制代码
print (contours[0])   #打印第0个轮廓中的像素点

3.返回值hierarchy

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

根据轮廓之间的关系,就能够确定一个轮廓与其他轮廓是如何连接的。比如,确定一个轮廓是某个轮廓的子轮廓,或者是某个轮廓的父轮廓。上述关系被称为层次(组织结构),返回值hierarchy就包含上述层次关系。

每个轮廓contours[i]对应4个元素来说明当前轮廓的层次关系。其形式为:

python 复制代码
[Next, Previous, First_Child, Parent]

式中各元素的含义为:

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

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

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

python 复制代码
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中仅检测到两个外轮廓,轮廓的序号如图中的数字标注所示。

图2

python 复制代码
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中检测到三个轮廓,各个轮廓的序号如图中数字的标注所示。

图3

python 复制代码
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中检测到三个轮廓,各轮廓的序号如图中数字的标注所示。

图4

python 复制代码
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中检测到三个轮廓,各个轮廓的序号如图中的数字标注所示。

图5

python 复制代码
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存储的轮廓,仅仅保存了边界上的四个点。

图6

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

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

绘制图像轮廓:drawContours函数

在OpenCV中,可以使用函数cv2.drawContours()绘制图像轮廓。该函数的语法格式是:

python 复制代码
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()写为没有返回值的形式:
python 复制代码
cv2.drawContours(
image,
contours,
contourIdx,
color[,
thickness[,
lineType[,
hierarchy[,
maxLevel[,
offset]]]]] )

轮廓实例

【例1】绘制一幅图像内的所有轮廓。如果要绘制图像内的所有轮廓,需要将函数cv2.drawContours()的参数contourIdx的值设置为"-1"。根据题目的要求及分析,编写代码如下:

python 复制代码
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所示,图像内的所有轮廓都被绘制出来了。

图7

【例2】逐个显示一幅图像内的边缘信息。如果要绘制图像内的某个具体轮廓,需要将函数cv2.drawContours()的参数contourIdx设置为具体的索引值。本例通过循环语句逐一绘制轮廓。根据题目要求及分析,编写代码如下:

python 复制代码
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所示,图像内的轮廓被逐一绘制出来。

图8

【例3】使用轮廓绘制功能,提取前景对象。将函数cv2.drawContours()的参数thickness的值设置为"-1",可以绘制前景对象的实心轮廓。将该实心轮廓与原始图像进行"按位与"操作,即可将前景对象从原始图像中提取出来。根据题目的要求及分析,编写代码如下:

python 复制代码
# 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所示。其中:

图9

矩特征

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

矩的计算:moments函数

OpenCV提供了函数cv2.moments()来获取图像的moments特征。通常情况下,我们将使用函数cv2.moments()获取的轮廓特征称为"轮廓矩"。轮廓矩描述了一个轮廓的重要特征,使用轮廓矩可以方便地比较两个轮廓。函数cv2.moments()的语法格式为:

python 复制代码
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()提取一幅图像的特征。根据题目的要求及分析,编写代码如下:

python 复制代码
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个轮廓。

图10

同时,程序会显示如下输出结果:

python 复制代码
观察各个轮廓的矩(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()的语法格式为:

python 复制代码
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

根据题目的要求及分析,编写代码如下:

python 复制代码
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成立。

python 复制代码
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矩,并进行比较。根据题目的要求,编写代码如下:

python 复制代码
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矩之差。

python 复制代码
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所示:

图11

从上述输出结果可以看到,由于Hu矩的值本身就非常小,因此在这里并没有发现两个对象的Hu矩差值的特殊意义。

形状匹配

我们可以通过Hu矩来判断两个对象的一致性。例如,前面计算了两个对象Hu矩的差,但是结果比较抽象。为了更直观方便地比较Hu矩值,OpenCV提供了函数cv2.matchShapes(),对两个对象的Hu矩进行比较。

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

函数cv2.matchShapes()的语法格式为:

python 复制代码
retval = cv2.matchShapes( contour1, contour2, method, parameter )

式中retval是返回值。

该函数有如下4个参数:

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


表1

​ 在表1中,A表示对象1, B表示对象2:

​ 式中,和分别是对象A和对象B的Hu矩。

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

【例10】使用函数cv2.matchShapes()计算三幅不同图像的匹配度。根据题目要求,编写代码如下:

python 复制代码
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()函数后,返回值的差也较大。
相关推荐
雍凉明月夜2 分钟前
深度学习网络笔记Ⅱ(常见网络分类1)
人工智能·笔记·深度学习
北岛寒沫3 分钟前
北京大学国家发展研究院 经济学辅修 经济学原理课程笔记(第十三课 垄断竞争)
人工智能·经验分享·笔记
AI营销实验室4 分钟前
AI 工具何高质量的为销售线索打分?
大数据·人工智能
Wang201220135 分钟前
RNN和LSTM对比
人工智能·算法·架构
xueyongfu9 分钟前
从Diffusion到VLA pi0(π0)
人工智能·算法·stable diffusion
jackylzh25 分钟前
配置pytorch环境,并调试YOLO
人工智能·pytorch·yolo
杜子不疼.33 分钟前
AI Ping双款新模型同步免费解锁:GLM-4.7与MiniMax M2.1实测
人工智能
打码人的日常分享34 分钟前
企业数据资产管控和数据治理解决方案
大数据·运维·网络·人工智能·云计算
百***787535 分钟前
小米MiMo-V2-Flash深度解析:国产开源大模型标杆与海外AI接入方案
人工智能·开源
大数据追光猿37 分钟前
【Prompt】Prompt Caching:原理、实现与高并发价值
人工智能·大模型·prompt·agent