第12章:图像轮廓

前面所提到的边缘检测虽然能够检测出图像的边缘,但是边缘是不连续的,检测到的边缘并不是一个整体。图像轮廓是指将边缘连接起来形成一个整体。

  • 边缘是不连续的,不是一个整体
  • 图像轮廓是将边缘连接起来形成一个整体。

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

OpenCV中提供了查找轮廓和绘制轮廓的函数:

  • 查找图像轮廓:cv2.findContours(),能够查找图像内所有轮廓的信息。
  • 绘制图像轮廓:cv2.drawContours()

一、查找并绘制轮廓:

​ 一条图像轮廓对应的是图像中一系列的像素点,这些点以某种方式表示图像中的一条曲线。在OpenCV中,通过cv2.findContours()来查找图像轮廓,并根据参数,以特定的方式返回表示的轮廓。

​ 通过,cv2.drawContours()来将查找到图像轮廓绘制到图像上,该函数可以根据参数在图像上绘制不同样式(实心/空心点,线条的不同粗细、颜色等)的轮廓,可以绘制全部轮廓也可以仅绘制指定的轮廓。

1. 查找图像轮廓:

OpenCV中查找图像轮廓的函数是CV2.findContours(),具体语法如下:

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

返回值为:

  • image:与参数中的原始图像image一致。
    (原始的输入图像,在OpenCV 4.X中,该返回值已经被取消。)

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

  • hierarchy:图像的拓扑信息(轮廓层次)。

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

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

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

    式中各元素的含义为:

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

    ​ 如果上述各个参数所对应的关系为空时,也就是没有对应的关系时,则将该参数所对应的值设为“-1”。使用print语句可以查看hierarchy的值:print(hierarchy)

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

参数为:

  • image:原始图像。必须为8位单通道图像,所有非零值被处理为1,所有零值保持不变。也就是说灰度图像会被自动处理为二值图像。在实际操作时,可以根据需要,预先使用阈值处理等函数将待查找轮廓的图像处理为二值图像。

  • mode:轮廓检索模式

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

    • cv2.RETR_EXTERNAL:只检测外轮廓。

      image-20211106152649182

    • cv2.RETR_LIST:对检测到的轮廓不建立等级关系。

      image-20211106152552364

    • cv2.RETR_CCOMP:检索所有轮廓并将它们组织成两级层次结构。上面的一层为外边界,下面的一层为内孔的边界。如果内孔内还有一个连通物体,那么这个物体的边界仍然位于顶层。

      image-20211106152757056

    • cv2.RETR_TREE:建立一个等级树结构的轮廓。
      image-20211106152757056

    由于,这里只有两层轮廓,所以使用参数 cv2.RETR_CCOMP 和 cv2.RETR_TREE得到的层次结构是一致的。

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

    image-20211106151908023

注意:

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

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

2. 绘制图像轮廓:

在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:偏移参数。该参数使轮廓偏移到不同的位置展示出来。

3. 绘制轮廓实例:

示例1:绘制出一幅图像内所有的轮廓

import cv2
import numpy as np

img = cv2.imread('../contour.bmp')
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
# print(contours)
print(hierarchy)

temp = np.zeros(img.shape, np.uint8)
cv2.drawContours(temp, contours, -1, (255, 255, 255), 5)
cv2.imshow('img', img)
cv2.imshow('rst', temp)
cv2.waitKey()
cv2.destroyAllWindows()

# 输出层次信息
[[[ 1 -1 -1 -1]
  [ 2  0 -1 -1]
  [-1  1 -1 -1]]]

image-20211106155945133

示例2:使用轮廓绘制功能,提取前景对象

import cv2
import numpy as np


img = cv2.imread('../flower.jpeg')
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
t, new_img = cv2.threshold(gray_img, 50, 255, cv2.THRESH_BINARY)
contours, hierarchy = cv2.findContours(new_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
mask = np.zeros(new_img.shape, np.uint8)
mask = cv2.drawContours(mask, contours, -1, (255, 255, 255), -1)
rst = cv2.bitwise_and(img, img, mask=mask)

cv2.imshow('img', img)
cv2.imshow('gray_img', gray_img)
cv2.imshow('new_img', new_img)
cv2.imshow('mask', mask)
cv2.imshow('rst', rst)
cv2.waitKey()
cv2.destroyAllWindows()

image-20211106165101558

二、矩特征

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

1. 矩的计算:moments函数

在OpenCV中可以通过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()的m00矩判断其面积是否一致。

中心矩:

​ 中心矩具有的平移不变性,使它能够忽略两个对象的位置关系,帮助我们比较不同位置上两个对象的一致性。

​ 例如,在很多情况下,我们希望比较不同位置的两个对象的一致性。解决这一问题的方法是引入中心矩。中心矩通过减去均值而获取平移不变性,因而能够比较不同位置的两个对象是否一致。

归一化中心矩:

​ 归一化中心距具有平移、缩放不变形。

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

在OpenCV中,函数cv2.moments()会同时计算上述空间矩、中心矩和归一化中心距。

示例:

import cv2
import numpy as np

img = cv2.imread('../contour.bmp')
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
contour, hierarchy = cv2.findContours(img_gray, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)
cv2.imshow('img', img)

for i in range(len(contour)):
    temp = np.zeros(img_gray.shape, np.uint8)
    rst = cv2.drawContours(temp, contour, i, 255, 3)
    cv2.imshow(f'rst_{
      
      i}', rst)

for n, item in enumerate(contour):
    print(f'轮廓{
      
      n}的矩为:\n {
      
      cv2.moments(item)}')
    print(f'轮廓{
      
      n}的面积为: {
      
      cv2.moments(item)["m00"]}')

cv2.waitKey()
cv2.destroyAllWindows()
轮廓0的矩为:
 {
    
    'm00': 9209.5, 'm10': 1017721.8333333333, 'm01': 2389982.5, 'm20': 119678938.58333333, 'm11': 264115139.7083333, 'm02': 627149283.9166666, 'm30': 14819578435.95, 'm21': 31059127282.616665, 'm12': 69306755254.31667, 'm03': 166344041770.35, 'mu20': 7212710.227465913, 'mu11': 3366.9156102240086, 'mu02': 6918397.298907757, 'mu30': -2880.996063232422, 'mu21': 174896.51574015617, 'mu12': 103129.86562001705, 'mu03': -6266.268524169922, 'nu20': 0.08504061263542004, 'nu11': 3.9697222979357786e-05, 'nu02': 0.08157055062519235, 'nu30': -3.539586534588115e-07, 'nu21': 2.1487754181991495e-05, 'nu12': 1.2670516573109425e-05, 'nu03': -7.698726136189045e-07}
轮廓0的面积为: 9209.5
轮廓1的矩为:
 {
    
    'm00': 13572.0, 'm10': 4764940.166666666, 'm01': 3239293.833333333, 'm20': 1682579697.5, 'm11': 1137215771.8333333, 'm02': 799816300.5, 'm30': 597523663576.15, 'm21': 401501587677.93335, 'm12': 280779729659.4667, 'm03': 203776751049.05002, 'mu20': 9675571.953775883, 'mu11': -55175.564665317535, 'mu02': 26678624.500047207, 'mu30': -550033.6081542969, 'mu21': -48973895.52901983, 'mu12': 1704572.9323253632, 'mu03': 145759473.83877563, 'nu20': 0.05252776773308551, 'nu11': -0.00029954293752635475, 'nu02': 0.14483573662328061, 'nu30': -2.5631829122147227e-05, 'nu21': -0.002282206947059113, 'nu12': 7.943391363704586e-05, 'nu03': 0.006792461171429966}
轮廓1的面积为: 13572.0
轮廓2的矩为:
 {
    
    'm00': 8331.0, 'm10': 1055010.8333333333, 'm01': 757410.8333333333, 'm20': 138592698.0, 'm11': 95918474.75, 'm02': 74976013.33333333, 'm30': 18814393628.350002, 'm21': 12600844964.533333, 'm12': 9495480797.366667, 'm03': 7928377343.35, 'mu20': 4989546.103385642, 'mu11': 2422.1211806088686, 'mu02': 6116192.129312888, 'mu30': -256257.12244033813, 'mu21': 110165.07764232159, 'mu12': 321106.038520813, 'mu03': -152856.36076641083, 'nu20': 0.07188971649383602, 'nu11': 3.489808519246561e-05, 'nu02': 0.08812250835798138, 'nu30': -4.045135827151561e-05, 'nu21': 1.7390061131886812e-05, 'nu12': 5.0688056135402214e-05, 'nu03': -2.4129075338702876e-05}
轮廓2的面积为: 8331.0

image-20211107155845538

2. 计算轮廓面积:contourArea函数

在OpenCV中可以通过函数cv2.contourArea()来计算轮廓的面积。

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

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

示例:

import cv2
import numpy as np

img = cv2.imread('../contour.bmp')
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
contours, hierarchy = cv2.findContours(img_gray, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)
cv2.imshow('img', img)

for i in range(len(contours)):
    temp = np.zeros(img_gray.shape, np.uint8)
    rst = cv2.drawContours(temp, contours, i, 255, 3)
    cv2.imshow(f'rst_{
      
      i}', rst)

for n, item in enumerate(contours):
    print(f'轮廓{
      
      n}的面积为: {
      
      cv2.contourArea(item)}')

cv2.waitKey()
cv2.destroyAllWindows()

# 输出结果
轮廓0的面积为: 9209.5
轮廓1的面积为: 13572.0
轮廓2的面积为: 8331.0

image-20220123043905346

3. 计算轮廓长度:arcLength函数

在OpenCV中可以通过函数cv2.arcLength()来计算轮廓的长度。

retval=cv2.arcLength(curve,closed)

返回值retval是轮廓的长度(周长)。

参数:

  • curve是轮廓。
  • closed是布尔型值,用来表示轮廓是否是封闭的。该值为True时,表示轮廓是封闭的。
import cv2
import numpy as np

img = cv2.imread('../contour.bmp')
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
contours, hierarchy = cv2.findContours(img_gray, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)
cv2.imshow('img', img)

for i in range(len(contours)):
    temp = np.zeros(img_gray.shape, np.uint8)
    rst = cv2.drawContours(temp, contours, i, 255, 3)
    cv2.imshow(f'rst_{
      
      i}', rst)

for n, item in enumerate(contours):
    print(f'轮廓{
      
      n}的周长为: {
      
      cv2.arcLength(item, closed=True)}')

cv2.waitKey()
cv2.destroyAllWindows()
# 输出结果
轮廓0的周长为: 381.0710676908493
轮廓1的周长为: 595.4213538169861
轮廓2的周长为: 356.9360725879669

image-20220123043825337

三、Hu矩

Hu矩是归一化中心矩的线性组合。 Hu矩具有旋转、缩放、平移不变性,所以经常会使用Hu距来识别图像的特征。

Hu 矩是归一化中心矩的线性组合,每一个矩都是通过归一化中心矩的组合运算得到的。

函数cv2.moments()返回的归一化中心矩中包含:

  • 二阶Hu矩:nu20,nu11,nu02
  • 三阶Hu矩:nu30,nu21,nu12,nu03

为了表述上的方便,将上述字母“nu”表示为字母“v”,则归一化中心矩为:

  • 二阶Hu矩:v20,v11,v02

  • 三阶Hu矩:v30,v21,v12,v03

上述7个Hu矩的计算公式为:

image-20211107175805248

1. Hu矩函数:

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

具体语法:

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

示例1:验证Hu矩中的第0个矩h0=v20+v02

import cv2

img = cv2.imread('../contour.bmp')
img_grey = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
img_moment = cv2.moments(img_grey)
nu02 = img_moment['nu02']
nu20 = img_moment['nu20']
hum1 = cv2.HuMoments(cv2.moments(img_grey)).flatten()

print(f'cv2.moments(img_grey)=\n{
      
      img_moment}')
print(f'hum1=\n{
      
      hum1}')
print(f'nu20+nu02: {
      
      nu20}+{
      
      nu20} = {
      
      nu20+nu02}')
print(f'hum1[0]={
      
      hum1[0]}')
print(f'hum1[0]-(nu02+nu20) = {
      
      hum1[0]-(nu20+nu02)}')

# 输出结果
cv2.moments(img_grey)=
{
    
    'm00': 8093955.0, 'm10': 1779032490.0, 'm01': 1661859225.0, 'm20': 505157033370.0, 'm11': 389611273740.0, 'm02': 391007192445.0, 'm30': 164339255189040.0, 'm21': 115872895560000.0, 'm12': 93612106111380.0, 'm03': 98481842760465.0, 'mu20': 114129828440.44452, 'mu11': 24338480021.574287, 'mu02': 49792534874.306694, 'mu30': 3135971915955.592, 'mu21': 1454447544071.9954, 'mu12': -2324769529267.9106, 'mu03': -2247068709894.701, 'nu20': 0.0017421181018673839, 'nu11': 0.00037151117457122763, 'nu02': 0.0007600508782649917, 'nu30': 1.682558608581603e-05, 'nu21': 7.80361974403396e-06, 'nu12': -1.2473201575996879e-05, 'nu03': -1.20563095054236e-05}

hum1=
[ 2.50216898e-03  1.51653824e-06  4.20046078e-09  3.70286211e-11
 -1.41810711e-20 -2.66632116e-14  3.48673128e-21]

nu20+nu02: 0.0017421181018673839+0.0017421181018673839 = 0.0025021689801323754、
hum1[0]=0.0025021689801323754
hum1[0]-(nu02+nu20) = 0.0

2. 形状匹配:

​ 我们可以通过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矩的方法,具体如表12-1所示。

    image-20211107183757755

    A表示对象1,B表示对象2:

    image-20211107183834544

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

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

示例:使用函数cv2.matchShapes()计算三幅不同图像的匹配度。

import cv2

img1 = cv2.imread('../contour2.bmp')
img1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
img3 = cv2.imread('../lena.bmp')
img3 = cv2.cvtColor(img3, cv2.COLOR_BGR2GRAY)

# 对img1进行旋转缩放
h, w = img1.shape
m = cv2.getRotationMatrix2D((w/2, h/2), 90, 0.5)
img_rotate = cv2.warpAffine(img1, m, (w, h))
cv2.imshow('img1', img1)
cv2.imshow('img2', img_rotate)
cv2.imshow('img3', img3)

contours1, hierarchy1 = cv2.findContours(img1, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)
contours2, hierarchy2 = cv2.findContours(img_rotate, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)
contours3, hierarchy3 = cv2.findContours(img3, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)

res1 = cv2.matchShapes(contours1[0], contours1[0], 1, 0)
res2 = cv2.matchShapes(contours1[0], contours2[0], 1, 0)
res3 = cv2.matchShapes(contours1[0], contours3[0], 1, 0)
print('相同图像的matchShapes=', res1)
print('相似图像的matchShapes=', res2)
print('不相似图像的matchShapes=', res3)

cv2.waitKey()
cv2.destroyAllWindows()

# 输出结果
相同图像的matchShapes=0.0
相似图像的matchShapes=0.0252828927661784
不相似图像的matchShapes=0.6988263089300291

image-20211107234406601

从以上结果可以看出:

  • 同一幅图像的Hu矩是不变的,二者差值为0。
  • 相似的图像即使发生了平移、旋转和缩放后,函数cv2.matchShapes()的返回值仍然比较接近。
  • 不相似图像cv2.matchShapes()函数返回值的差较大。

四、轮廓拟合

在计算轮廓时,有时候可能并不需要实际的轮廓,而仅需要一个接近于轮廓的近似多边形。OpenCV提供了多种计算轮廓近似多边形的方法。

1. 矩形包围框:

在OpenCV中通过函数cv2.boundingRect()能够绘制出轮廓的矩形边界,具体语法为:
retval = cv2.boundingRect(array)

返回值:

  • retval:表示返回的矩形边界的左上角顶点的坐标值及矩形边界的宽度和高度。

参数:

  • array:灰度图像或轮廓。

示例:

import cv2
import numpy as np

img = cv2.imread('../contour2.bmp')
grey_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
contours, hierarchy = cv2.findContours(grey_img, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)

x, y, w, h = cv2.boundingRect(contours[0])
print(f'顶点及宽高分别为:{
      
      x}, {
      
      y}, {
      
      w}, {
      
      h}')
new_img = img.copy()
# brcnt = np.array([[[x, y]], [[x+w, y]], [[x+w, y+h]], [[x, y+h]]])
# rst = cv2.drawContours(new_img, [brcnt], -1, (255, 255, 255), 2)
rst = cv2.rectangle(new_img, (x, y), (x+w, y+h), (255, 255, 255), 2)
cv2.imshow('img', img)
cv2.imshow('rst', rst)
cv2.waitKey()
cv2.destroyAllWindows()

# 输出结果
顶点及宽高分别为:165, 70, 241, 121

image-20211116161955581

2. 最小包围矩形框:

在OpenCV中通过函数cv2.minAreaRect()能够绘制轮廓的最小包围矩形框。

retval = cv2.minAreaRect( points )

返回值:

  • retval:表示返回的矩形特征信息。该值的结构是(最小外接矩形的中心(x,y),(宽度,高度),旋转角度)。

参数:

  • points:轮廓。

    注意,返回值retval是不符合函数cv2.drawContours()的参数结构要求。因此,必须将其转换为符合要求的结构,才能使用。函数 cv2.boxPoints()能够将上述返回值 retval 转换为符合要求的结构。

    函数cv2.boxPoints()的语法格式是:

    • points = cv2.boxPoints(box)

      • points:返回值是轮廓点。能够用于函数cv2.drawContours()的参数。

      • box:是函数cv2.minAreaRect()返回值的类型的值。

示例:

import cv2
import numpy as np

img = cv2.imread('../contour2.bmp')
grey_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
contours, hierarchy = cv2.findContours(grey_img, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
rect = cv2.minAreaRect(contours[0])
print(f'返回值rect:{
      
      rect}')
points = cv2.boxPoints(rect)
points = np.int0(points)
new_img = img.copy()
rst = cv2.drawContours(new_img, [points], 0, (255, 255, 255), 2)
cv2.imshow('img', img)
cv2.imshow('rst', rst)
cv2.waitKey()
cv2.destroyAllWindows()

# 输出结果
返回值rect:((284.41119384765625, 132.4847412109375), (80.4281997680664, 238.48388671875), 72.19464874267578)

image-20211116170127738

3. 最小包围圆形:

在OpenCV中通过函数cv2.minEnclosingCircle()通过迭代算法构造一个对象的的面积最小包围圆形。具体语法:

  • center,radius=cv2.minEnclosingCircle(points)
  • center:返回值,最小包围圆形的中心。
  • radius:返回值:最小包围圆形的半径。
  • points:轮廓。

示例:

import cv2
import numpy as np

img = cv2.imread('../contour2.bmp')
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
contours, hierarchy = cv2.findContours(gray_img, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)

(x, y), radius = cv2.minEnclosingCircle(contours[0])
center = (int(x), int(y))
radius = int(radius)
new_img = img.copy()
rst = cv2.circle(new_img, center, radius, (255, 255, 255), 2)
cv2.imshow('img', img)
cv2.imshow('rst', rst)
cv2.waitKey()
cv2.destroyAllWindows()

image-20211116171907093

4. 最优拟合椭圆:

在OpenCV中,通过函数cv2.fitEllipse()可以用来构造最优拟合椭圆。该函数的语法格式是:

  • retval=cv2.fitEllipse(points)
    • retval:返回值,是RotatedRect类型的值。这是因为该函数返回的是拟合椭圆的外接矩形,retval包含外接矩形的质心、宽、高、旋转角度等参数信息,这些信息正好与椭圆的中心点、轴长度、旋转角度等信息吻合。
    • points:轮廓。

示例:

import cv2
import numpy as np

img = cv2.imread('../contour2.bmp')
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
contours, hierarchy = cv2.findContours(gray_img, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)

ellipse = cv2.fitEllipse(contours[0])
print(f'ellipse={
      
      ellipse}')
new_img = img.copy()
rst = cv2.ellipse(new_img, ellipse, (0, 0, 255), 2)
cv2.imshow('img', img)
cv2.imshow('rst', rst)
cv2.waitKey()
cv2.destroyAllWindows()

image-20211116172510672

5. 最优拟合直线:

在OpenCV中,函数cv2.fitLine()用来构造最优拟合直线,该函数的语法格式为:

  • line=cv2.fitLine(points,distType,param,reps,aeps)

    • line:返回值,返回的是最优拟合直线参数。(点斜式)

      斜率 k = line[1] / line[0]

      截距 b = line[3] - k * line[2]

      通过点斜式的方式来确定一条直线

    • points:轮廓。

    • distType:距离类型。拟合直线时,要使输入点到拟合直线的距离之和最小,其类型如表所示

      image-20211116174644734

    • param:距离参数,与所选的距离类型有关。当此参数被设置为0时,该函数会自动选择最优值。

    • reps:用于表示拟合直线所需要的径向精度,通常该值被设定为0.01。

    • aeps:用于表示拟合直线所需要的角度精度,通常该值被设定为0.01。

import cv2
import numpy as np

img = cv2.imread('../contour2.bmp')
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
contours, hierarchy = cv2.findContours(gray_img, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)

rows, cols = img.shape[:2]
vx, vy, x, y = cv2.fitLine(contours[0], cv2.DIST_L2, 0, 0.01, 0.01)
# 截距b
lefty = int((-x*vy/vx)+y)
righty = int(((cols-x)*vy/vx)+y)
new_img = img.copy()
rst = cv2.line(new_img, (cols-1, righty), (0, lefty), (0, 255, 0), 2)
cv2.imshow('img', img)
cv2.imshow('rst', rst)
cv2.waitKey()
cv2.destroyAllWindows()

image-20211116175904022

6. 最小外包三角形:

在OpenCV中,函数cv2.minEnclosingTriangle()用来构造最小外包三角形。该函数的语法格式为:

  • retval,triangle=cv2.minEnclosingTriangle(points)
    • retval:最小外包三角形的面积。
    • triangle:最小外包三角形的三个顶点集。
    • points:轮廓。
import cv2
import numpy as np

img = cv2.imread('../contour2.bmp', -1)
contours, hierarchy = cv2.findContours(img, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)

area, trgl = cv2.minEnclosingTriangle(contours[0])
print(f'area={
      
      area}')
print(f'trgl:{
      
      trgl}')
new_img = img.copy()
for i in range(0, 3):
    cv2.line(new_img, tuple(trgl[i][0]), tuple(trgl[(i + 1) % 3][0]), (255, 255, 255), 2)

cv2.imshow('img', img)
cv2.imshow('rst', new_img)
cv2.waitKey()
cv2.destroyAllWindows()

# 输出结果
area=26509.7265625
area=26509.7265625
trgl:
[[[ 52.674423 207.67442 ]]
 [[443.27908  156.27907 ]]
 [[323.3256    36.325584]]]

image-20211117115928254

7. 逼近多边形:

在OpenCV中,通过函数cv2.approxPolyDP()来构造指定精度的逼近多边形曲线。具体语法格式为:

  • approxCurve = cv2.approxPolyDP(curve,epsilon,closed)
    • approxCurve:返回值,逼近多边形的点集。
    • curve是轮廓。
    • psilon为精度,原始轮廓的边界点与逼近多边形边界之间的最大距离。
    • closed 是布尔型值。该值为 True 时,逼近多边形是封闭的;否则,逼近多边形是不封闭的。

​ 函数cv2.approxPolyDP()采用的是Douglas-Peucker算法(DP算法)。该算法首先从轮廓中找到距离最远的两个点,并将两点相连(见(b)图)。接下来,在轮廓上找到一个离当前直线最远的点,并将该点与原有直线连成一个封闭的多边形,此时得到一个三角形,如图(c)所示。

​ 将上述过程不断地迭代,将新找到的距离当前多边形最远的点加入到结果中。当轮廓上所有的点到当前多边形的距离都小于函数cv2.approxPolyDP()的参数epsilon的值时,就停止迭代。

​ 通过上述过程可知,epsilon是逼近多边形的精度信息。通常情况下,将该精度设置为轮廓总长度的百分比形式。

示例:

import cv2

img = cv2.imread('../contour3.bmp')
grey_img = cv2.cvtColor(img, cv2.cv2.COLOR_BGR2GRAY)
contours, hierarchy = cv2.findContours(grey_img, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
cv2.imshow('img', img)

adp = img.copy()
epsilon = 0.1 * cv2.arcLength(contours[0], True)
approx = cv2.approxPolyDP(contours[0], epsilon, True)
adp = cv2.drawContours(adp, [approx], 0, (0, 255, 0), thickness=2)
cv2.imshow('rst0.1', adp)

adp = img.copy()
epsilon = 0.08 * cv2.arcLength(contours[0], True)
approx = cv2.approxPolyDP(contours[0], epsilon, True)
adp = cv2.drawContours(adp, [approx], 0, (0, 255, 0), 2)
cv2.imshow('rst0.08', adp)

adp = img.copy()
epsilon = 0.05 * cv2.arcLength(contours[0], True)
approx = cv2.approxPolyDP(contours[0], epsilon, True)
adp = cv2.drawContours(adp, [approx], 0, (0, 255, 0), 2)
cv2.imshow('rst0.05', adp)

adp = img.copy()
epsilon = 0.04 * cv2.arcLength(contours[0], True)
approx = cv2.approxPolyDP(contours[0], epsilon, True)
adp = cv2.drawContours(adp, [approx], 0, (0, 255, 0), 2)
cv2.imshow('rst0.04', adp)

adp = img.copy()
epsilon = 0.01 * cv2.arcLength(contours[0], True)
approx = cv2.approxPolyDP(contours[0], epsilon, True)
adp = cv2.drawContours(adp, [approx], 0, (0, 255, 0), 2)
cv2.imshow('rst0.01', adp)

cv2.waitKey()
cv2.destroyAllWindows()

image-20211117163727208

五、凸包

1. 凸包:

  • 逼近多边形是在轮廓内部逼近轮廓
  • 凸包是在轮廓外部包围轮廓的凸多边形,是物体最外层的“凸”多边形。

逼近多边形是在轮廓内部按照指定精度逼近轮廓,是轮廓的高度近似。凸包是在轮廓外部包围原有轮廓,并且仅由轮廓上的点所构成的凸多边形。凸包的每一处都是凸的,即在凸包内连接任意两点的直线仍在凸包内。在凸包内,任意连续三个点的内角小于180°。

​ 例如下图,最外层的多边形为机械手的凸包,在手边缘与凸包之间的部分被称为凸缺陷(Convexity Defect),凸缺陷能够用来处理手势识别等问题。

image-20211118144609304

在OpenCV通过cv2.convexHull()函数来获取轮廓的凸包。语法格式为:

  • hull=cv2.convexHull(points[,clockwise[,returnPoints]])
    • hull:返回值,为凸包角点。
    • points:轮廓。
    • clockwise:布尔型值。该值为True时,凸包角点将按顺时针方向排列;该值为False时,则以逆时针方向排列凸包角点。
    • returnPoints:布尔型值。默认值是 True,函数返回凸包角点的x/y轴坐标;当为 False时,函数返回轮廓中凸包角点的索引。
import cv2

img = cv2.imread('../grey_hand.jpg')
grey_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
t, grey_img = cv2.threshold(grey_img, 20, 255, cv2.THRESH_BINARY)
contours, hierarchy = cv2.findContours(grey_img, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)

new_img = img.copy()
cv2.drawContours(new_img, contours, 0, (0, 0, 255), 2)
hull = cv2.convexHull(contours[0])
rst = cv2.polylines(new_img, [hull], True, (0, 255, 255), 2)
cv2.imshow('img', img)
cv2.imshow('rst', rst)
cv2.waitKey()
cv2.destroyAllWindows()

image-20211118145222884

2. 凸缺陷:

​ 凸包与轮廓之间的部分,称为凸缺陷。在OpenCV中使用函数cv2.convexityDefects()获取凸缺陷。其语法格式如下:

  • convexityDefects=cv2.convexityDefects(contour,convexhull)

    • convexityDefects:返回值,为凸缺陷点集。它是一个数组,每一行包含的值是[起点,终点,轮廓上距离凸包最远的点,最远点到凸包的近似距离]。

      注意,返回结果中[起点,终点,轮廓上距离凸包最远的点,最远点到凸包的近似距离]的前三个值是轮廓点的索引,所以需要到轮廓点中去找它们。

    • contour:轮廓。

    • convexhull:凸包。

      注意,用 cv2.convexityDefects()计算凸缺陷时,需要传入凸包作为参数。在查找该凸包时,所使用函数cv2.convexHull()的参数returnPoints的值必须是False。

import cv2

img = cv2.imread('../grey_hand.jpg')
grey_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
t, grey_img = cv2.threshold(grey_img, 20, 255, cv2.THRESH_BINARY)
contours, hierarchy = cv2.findContours(grey_img, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
contours = contours[0]

hull = cv2.convexHull(contours, returnPoints=False)
convexity_defects = cv2.convexityDefects(contours, hull)
print(convexity_defects)
new_img = img.copy()
for i in range(convexity_defects.shape[0]):
    s, e, f, d = convexity_defects[i, 0]
    start = tuple(contours[s][0])
    end = tuple(contours[e][0])
    far = tuple(contours[f][0])
    cv2.line(new_img, start, end, [0, 255, 0], 2)
    cv2.circle(new_img, start, 5, [0, 0, 255], -1)
    cv2.circle(new_img, far, 5, [255, 0, 0], -1)

cv2.imshow('img', img)
cv2.imshow('result', new_img)
cv2.waitKey()
cv2.destroyAllWindows()

# 输出结果
[[[    0     2     1   162]]
 [[    2     4     3   114]]
 [[    4   258   157 39819]]
 [[  258   260   259   142]]
 [[  261   265   262   114]]
 [[  266   268   267   162]]
 [[  268   454   331  3007]]
 [[  455   578   513  5631]]
 [[  579   585   580   114]]
 [[  586   722   693 37006]]
 [[  722   724   723   162]]
 [[  724   726   725   142]]
 [[  726   910   781 47500]]
 [[  911   915   912   114]]
 [[  915   917   916   162]]
 [[  917  1105  1028 46699]]]

image-20211122155554002

3. 几何学测试:

下面介绍几种与凸包相关的几种几何学测试

(1): 测试轮廓是否是图形的

在OpenCV中,可以用函数cv2.isContourConvex()来判断轮廓是否是凸形的,其语法格式为:
retval=cv2.isContourConvex(contour)

  • retval:返回值,布尔类型型。该值为True时,表示轮廓为凸形的;否则,不是凸形的。
  • contour:要判断的轮廓。
import cv2

img = cv2.imread('../grey_hand.jpg')
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
t, gray_img = cv2.threshold(gray_img, 20, 255, cv2.THRESH_BINARY)
contours, hierarchy = cv2.findContours(gray_img, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)

new_img = img.copy()
hull = cv2.convexHull(contours[0])
cv2.polylines(new_img, [hull], True, (0, 255, 0), 2)
print(f'判断构造出来的多边形是否是凸形的:{
      
      cv2.isContourConvex(hull)}')
cv2.imshow('rst1', new_img)

new_img = img.copy()
epsilon = 0.005 * cv2.arcLength(contours[0], True)
approx = cv2.approxPolyDP(contours[0], epsilon, True)
rst = cv2.drawContours(new_img, [approx], 0, [0, 0, 255], 2)
print(f'判断构造出来的多边形是否是凸形的:{
      
      cv2.isContourConvex(approx)}')
cv2.imshow('rst2', rst)

cv2.waitKey()
cv2.destroyAllWindows()

# 输出结果
判断构造出来的多边形是否是凸形的:True
判断构造出来的多边形是否是凸形的:False

image-20211122164938727

(2):点到轮廓的距离

在OpenCV中,通过cv2.pointPolygonTest()函数来计算点到多边形(轮廓)的最短距离(也就是垂线距离),这个计算过程又称点和多边形的关系测试。函数的语法格式为:

  • retval=cv2.pointPolygonTest(contour,pt,measureDist)
    • retval:返回值,与参数measureDist的值有关。
    • contour:为轮廓。
    • pt:为待判定的点。
    • measureDist为布尔型值,表示距离的判定方式。
      • 当值为True时,表示计算点到轮廓的距离。如果点在轮廓的外部,返回值为负数;如果点在轮廓上,返回值为0;如果点在轮廓内部,返回值为正数。
      • 当值为False时,不计算距离,只返回“-1”、“0”和“1”中的一个值,表示点相对于轮廓的位置关系。如果点在轮廓的外部,返回值为“-1”;如果点在轮廓上,返回值为“0”;如果点在轮廓内部,返回值为“1”。
import cv2

img = cv2.imread('../contour2.bmp')
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# t, gray_img = cv2.threshold(gray_img, 20, 255, cv2.THRESH_BINARY)
contours, hierarchy = cv2.findContours(gray_img, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)

new_img = img.copy()
hull = cv2.convexHull(contours[0])
rst = cv2.polylines(new_img, [hull], True, (0, 255, 0), 2)

dist_a = cv2.pointPolygonTest(hull, (300, 150), True)
font = cv2.FONT_HERSHEY_SIMPLEX
cv2.circle(rst, (300, 150), 5, [0, 0, 255], -1)
cv2.putText(rst, 'A', (305, 155), font, 1, (0, 255, 0), 3)
print(f'dist_a={
      
      dist_a}')

dist_b = cv2.pointPolygonTest(hull, (300, 250), True)
font = cv2.FONT_HERSHEY_SIMPLEX
cv2.circle(rst, (300, 250), 5, [0, 0, 255], -1)
cv2.putText(rst, 'B', (305, 255), font, 1, (0, 255, 0), 3)
print(f'dist_b={
      
      dist_b}')

dist_c = cv2.pointPolygonTest(hull, (405, 120), True)
font = cv2.FONT_HERSHEY_SIMPLEX
cv2.circle(rst, (405, 120), 5, [0, 0, 255], -1)
cv2.putText(rst, 'C', (410, 125), font, 1, (0, 255, 0), 3)
print(f'dist_c={
      
      dist_c}')

cv2.imshow('img', img)
cv2.imshow('rst', rst)
cv2.waitKey()
cv2.destroyAllWindows()

# 输出结果
dist_a=18.454874482756573
dist_b=-77.26881150094928
dist_c=-0.0

image-20211122180831798

六、利用形状场景算法比较轮廓

OpenCV中用矩特征来比较形状是一种非常有效的方法。不过从OpenCV3开始有了更有效的方法,OpenCV3中提供来专门的模块shape,该模块中的形状场景算法能够更高效的比较形状。

比较轮廓的两种方法:

  1. 矩特征
  2. 形状场景算法

注意:使用形状场景算法需要事先安装opencv-contrib-python这个库,并且opencv-contrib-python这个库的版本要和opencv的一样。

1. 计算形状场景距离:

OpenCV中形状场景算法使用距离 来作为形状比较的度量标准。 这是因为形状之间的差异值和距离有相似之处,比如二者都只能是零或正数,又比如当两个形状一模一样时距离值和差值都是零。

OpenCV中使用函数cv2.createShapeContextDistanceExtractor(),来计算形状场景距离。 其使用的“形状上下文算法”在计算距离时,会在每一个点上附加一个"形状上下文"描述符,让每个点都能够捕获其他剩余点相对于它的分布特征,从而提供全局鉴别特征。

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

  • retval=cv2.createShapeContextDistanceExtractor([,nAngularBins[, nRadialBins[, innerRadius[, outerRadius[, iterations[, comparer[, transformer]]]]]]] )
    • retval:返回值
    • nAngularBins:为形状匹配中使用的形状上下文描述符建立的角容器的数量。
    • nRadialBins:为形状匹配中使用的形状上下文描述符建立的径向容器的数量。
    • innerRadius:形状上下文描述符的内半径。
    • outerRadius:形状上下文描述符的外半径。
    • iterations:迭代次数。
    • comparer:直方图代价提取算子。该函数使用了直方图代价提取仿函数,可以直接采用直方图代价提取仿函数的算子作为参数。
    • transformer:形状变换参数。

函数cv2.createShapeContextDistanceExtractor()的参数都是可选参数,结果为retval.

该结果可以通过函数cv2.ShapeDistanceExtractor.computeDistance()计算两个不同形状之间的距离。此函数的语法格式为:

  • retval=cv2.ShapeDistanceExtractor.computeDistance(contour1,contour2)
    • coutour1和coutour2是不同的轮廓。

示例:

  1. 构建形状场景算法对象。
  2. 调用computeDistance()方法进行轮廓比较,计算不同轮廓的距离。
import cv2

img1 = cv2.imread('../contour2.bmp')
cv2.imshow('img1', img1)
gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
contours1, hierarchy1 = cv2.findContours(gray1, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
cnt1 = contours1[0]


h, w = img1.shape[:2]
m = cv2.getRotationMatrix2D((w/2, h/2), 90, 0.5)
img_rotate = cv2.warpAffine(gray1, m, (w, h))
cv2.imshow('img2', img_rotate)
contours2, hierarchy2 = cv2.findContours(img_rotate, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
cnt2 = contours2[0]

img3 = cv2.imread('../grey_hand.jpg')
gray3 = cv2.cvtColor(img3, cv2.COLOR_BGR2GRAY)
t, gray3 = cv2.threshold(gray3, 20, 255, cv2.THRESH_BINARY)
cv2.imshow('img3', gray3)
contours3, hierarchy3 = cv2.findContours(gray3, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
cnt3 = contours3[0]

# 构造距离提取算子对象
sd = cv2.createShapeContextDistanceExtractor()
# 计算距离
d1 = sd.computeDistance(cnt1, cnt1)
print(f'与自身的距离d1={
      
      d1}')
d2 = sd.computeDistance(cnt1, cnt2)
print(f'与旋转缩放后的自身图像的距离d2={
      
      d2}')
d3 = sd.computeDistance(cnt1, cnt3)
print(f'与不相似对象的距离d3={
      
      d3}')

cv2.waitKey()
cv2.destroyAllWindows()

# 数据结果
与自身的距离d1=0.00035154714714735746
与旋转缩放后的自身图像的距离d2=1.3983277082443237
与不相似对象的距离d3=293.24066162109375

image-20211127214111959

从上述结果来看:

  • 相同图像之间的形状场景距离几乎为零。
  • 相似图像之间的形状场景距离较小。
  • 不同图像之间的形状场景距离较大

2. 计算Hausdorrf距离:

Hausdorff距离的计算方法是:

(1)针对图像 A 内的每一个点,寻找其距离图像 B 的最短距离,将这个最短距离作为Hausdorff直接距离D1。

(2)针对图像 B 内的每一个点,寻找其距离图像 A 的最短距离,将这个最短距离作为Hausdorff直接距离D2。

(3)将上述D1、D2中的较大者作为Hausdorff距离。

通常情况下,Hausdorff距离H(·)是根据对象A和对象B之间的Hausdorff直接距离h(·)来定义的,用数学公式表式如下:H(A,B)=max(ℎ(A,B),ℎ(B,A))

其中:

image-20211128001059395

式中,‖∙‖表示点a和点b的某种范数,通常是欧氏距离。

麦吉尔大学的学者 Normand 和 Mikael 对 Hausdorff 距离给出了详细的说明,网址为:http://cgm.cs.mcgill.ca/~godfried/teaching/cg-projects/98/normand/main.html

OpenCV提供了函数cv2.createHausdorffDistanceExtractor()来计算Hausdorff距离。

其语法格式为:

  • retval=cv2.createHausdorffDistanceExtractor([,distanceFlag[,rankProp]])
    • retval:返回值
    • distanceFlag:距离标记,是可选参数。
    • rankProp:为一个比例值,范围在0到1之间,也是可选参数。

示例:

  1. 通过cv2.createHausdorffDistanceExtractor()构建Hausdorff对象。
  2. 调用computeDistance()方法进行轮廓比较,计算不同图像的Hausdorff距离。
import cv2

img1 = cv2.imread('../contour2.bmp')
cv2.imshow('img1', img1)
gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
contours1, hierarchy1 = cv2.findContours(gray1, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
cnt1 = contours1[0]


h, w = img1.shape[:2]
m = cv2.getRotationMatrix2D((w/2, h/2), 90, 0.5)
img_rotate = cv2.warpAffine(gray1, m, (w, h))
cv2.imshow('img2', img_rotate)
contours2, hierarchy2 = cv2.findContours(img_rotate, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
cnt2 = contours2[0]

img3 = cv2.imread('../grey_hand.jpg')
gray3 = cv2.cvtColor(img3, cv2.COLOR_BGR2GRAY)
t, gray3 = cv2.threshold(gray3, 20, 255, cv2.THRESH_BINARY)
cv2.imshow('img3', gray3)
contours3, hierarchy3 = cv2.findContours(gray3, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
cnt3 = contours3[0]


# 构造距离提取算子对象
hd = cv2.createHausdorffDistanceExtractor()
# 计算距离
d1 = hd.computeDistance(cnt1, cnt1)
print(f'与自身的hausdorff距离d1={
      
      d1}')
d2 = hd.computeDistance(cnt1, cnt2)
print(f'与旋转缩放后的自身图像的hausdorff距离d2={
      
      d2}')
d3 = hd.computeDistance(cnt1, cnt3)
print(f'与不相似对象的hausdorff距离d3={
      
      d3}')

cv2.waitKey()
cv2.destroyAllWindows()

# 输出结果
与自身的hausdorff距离d1=0.0
与旋转缩放后的自身图像的hausdorff距离d2=42.42640686035156
与不相似对象的hausdorff距离d3=146.81961059570312

image-20220123043602252

七、轮廓的特征值:

轮廓本身的属性特征及轮廓所包围对象的特征对于描述图像具有重要意义。下面介绍几种轮廓本身属性及所包围对象的特征。

1. 宽高比:

使用宽高比(AspectRation)来描述轮廓,例如矩形轮廓的宽高比为:

  • 宽高比=宽度(Width)/高度(Height)
import cv2

img = cv2.imread('../contour3.bmp')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
contours, hierarchy = cv2.findContours(gray, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)

x, y, w, h = cv2.boundingRect(contours[0])
new_img = img.copy()
cv2.rectangle(new_img, (x, y), (x+w, y+h), (255, 0, 0), 3)
aspect_ratio = float(w)/h
print(aspect_ratio)
cv2.imshow('rst', new_img)
cv2.waitKey()
cv2.destroyAllWindows()

# 输出结果
2.125

image-20211128012446917

2. Extent:

使用轮廓面积与矩形边界(矩形包围框、矩形轮廓)面积之比 Extend 来描述图像及其轮廓特征。计算方法为:

image-20211128012842088

import cv2

img = cv2.imread('../contour3.bmp')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
contours, hierarchy = cv2.findContours(gray, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)

new_img = img.copy()
x, y, w, h = cv2.boundingRect(contours[0])
cv2.drawContours(new_img, contours[0], -1, (0, 255, 255), 3)
cv2.rectangle(new_img, (x, y), (x+w, y+h), (255, 0, 0), 3)
rectArea = w * h
cntArea = cv2.contourArea(contours[0])
extend = float(cntArea) / rectArea
print(extend)
cv2.imshow('rst', new_img)
cv2.waitKey()
cv2.destroyAllWindows()

# 输出结果
0.6720588235294118

image-20211128013600525

3. Solidity:

使用轮廓面积与凸包面积之比 Solidity 来衡量图像、轮廓及凸包的特征。其计算方法为:

image-20211128015345137

import cv2

img = cv2.imread('../contour3.bmp')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
contours, hierarchy = cv2.findContours(gray, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)

new_img = img.copy()
cv2.drawContours(new_img, contours[0], -1, (0, 255, 255), 2)
cntArea = cv2.contourArea(contours[0])

hull = cv2.convexHull(contours[0])
hullArea = cv2.contourArea(hull)
cv2.polylines(new_img, [hull], True, (255, 0, 0), 2)
solidity = float(cntArea)/hullArea
print(solidity)

cv2.imshow('rst', new_img)
cv2.waitKey()
cv2.destroyAllWindows()

# 输出结果
0.8977066247605952

image-20211128015808360

4. 等效直径:

使用等效直径来衡量轮廓的特征值,该值是与轮廓面积相等的圆形的直径。其计算公式为:

image-20211128020110494

import cv2
import numpy as np

img = cv2.imread('../contour3.bmp')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
contours, hierarchy = cv2.findContours(gray, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)

new_img = img.copy()
cv2.drawContours(new_img, contours[0], -1, (0, 255, 0), 2)
cntArea = cv2.contourArea(contours[0])

equiDiameter = np.sqrt(4*cntArea/np.pi)
print(equiDiameter)
cv2.circle(new_img, (100, 100), int(equiDiameter/2), (0, 255, 255), 3)

cv2.imshow('rst', new_img)
cv2.waitKey()
cv2.destroyAllWindows()

# 结果
107.87682530960664

image-20211128020640603

5. 方向:

在 OpenCV 中,函数 cv2.fitEllipse()可以用来构造最优拟合椭圆,还可以在返回值内分别返回椭圆的中心点、轴长、旋转角度等信息。使用这种形式,能够更直观地获取椭圆的方向等信息。

函数cv2.fitEllipse()返回各个属性值的语法格式为:

(x,y), (MA,ma) ,angle=cv2.fitEllipse(cnt)

  • (x,y):椭圆的中心点。
  • (MA,ma):椭圆水平方向轴和垂直方向轴的长度。
  • angle:椭圆的旋转角度。
import cv2
import numpy as np

img = cv2.imread('../contour3.bmp')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
contours, hierarchy = cv2.findContours(gray, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)

new_img = img.copy()
cv2.drawContours(new_img, contours[0], -1, (0, 255, 0), 2)
cntArea = cv2.contourArea(contours[0])

ellipse = cv2.fitEllipse(contours[0])
cv2.ellipse(new_img, ellipse, (255,0 , 0), 2)
print(f'(x, y): {
      
      ellipse[0][0], ellipse[0][1]}')
print(f'(MA, ma): {
      
      ellipse[1][0], ellipse[1][1]}')
print(f'angle: {
      
      ellipse[2]}')

cv2.imshow('rst', new_img)
cv2.waitKey()
cv2.destroyAllWindows()

# 输出结果
(x, y): (256.41845703125, 137.38284301757812)
(MA, ma): (69.56119537353516, 179.77560424804688)
angle: 81.93647766113281

image-20211128021652914

6. 掩膜和像素点:

​ 如果希望获取某对象的掩模图像及其对应的点。可以通过将函数cv2.drawContours()的轮廓宽度参数 thickness 设置为“-1”,即可获取特定对象的实心轮廓,即特定对象的掩模。

​ 希望获取轮廓像素点的具体位置信息。一般情况下,轮廓像素点的位置就是图像内非零的像素点的位置,可以通过两种方式获取轮廓像素点的位置信息。一种是使用Numpy函数,另外一种是使用OpenCV函数。

  • 使用Numpy函数获取轮廓像素点:

    • numpy.nonzero()函数能够找出数组内非零元素的位置,但是其返回值是将行、列分别显示的。
      例如,对于如下数组a应用函数numpy.nonzero():

      image-20211128022845126

      返回的数组a内非零元素的位置信息为:

      (array([0,1,1,2,2,2,3,4,4],dtype=int64),array([3,2,4,2,3,4,0,0,4],dtype=int64))

      使用numpy.transpose()函数处理上述返回值,则得到这些点的(x,y)形式的坐标:

      image-20211128022934851

  • 使用OpenCV函数获取轮廓点

    • OpenCV提供了函数cv2.findNonZero()用于查找非零元素的索引。该函数的语法格式为:

      idx=cv2.findNonZero(src)

      • idx:为返回值,表示非零元素的索引位置。需要注意的是,在返回的索引中,每个元素对应的是(列号,行号)的格式。
      • src:为参数,表示要查找非零元素的图像。
import cv2
import numpy as np

img = np.zeros((5, 5), dtype=np.uint8)
for _ in range(10):
    i = np.random.randint(0, 5)
    j = np.random.randint(0, 5)
    img[i, j] = 1

print(f'img:\n{
      
      img}')
loc = cv2.findNonZero(img)
print(f'img:\n内非零位置{
      
      loc}')

# 输出结果
img:
[[1 1 0 0 0]
 [0 1 0 0 1]
 [0 0 1 0 1]
 [0 1 0 0 0]
 [0 0 1 0 0]]
img内非零位置:
[[[0 0]]
 [[1 0]]
 [[1 1]]
 [[4 1]]
 [[2 2]]
 [[4 2]]
 [[1 3]]
 [[2 4]]]

7. 最大值和最小值及它们的位置:

OpenCV 提供了函数 cv2.minMaxLoc(),用于在指定的对象内查找最大值、最小值及其位置。该函数的语法格式是:

  • min_val,max_val,min_loc,max_loc=cv2.minMaxLoc(imgray,mask=mask)
    • min_val:最小值。
    • max_val:最大值。
    • min_loc:最小值的位置。
    • max_loc:最大值的位置。
    • imgray:单通道图像。
    • mask:掩模。通过使用掩模图像,可以得到掩模指定区域内的最值信息。
import cv2
import numpy as np

img = cv2.imread('../lena.bmp')
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# contours, hierarchy = cv2.findContours(gray, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)

min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(gray_img)
print(f'min_val: {
      
      min_val}')
print(f'max_val: {
      
      max_val}')
print(f'min_loc: {
      
      min_loc}')
print(f'max_loc: {
      
      max_loc}')
new_img1 = img[min_loc[0]: min_loc[0]+100, min_loc[1]: min_loc[1]+100]
new_img2 = img[max_loc[0]: max_loc[0]+100, max_loc[1]: max_loc[1]+100]

cv2.imshow('img', img)
cv2.imshow('new_img1', new_img1)
cv2.imshow('new_img2', new_img2)
cv2.waitKey()
cv2.destroyAllWindows()

8. 平均颜色及平均灰度:

OpenCV 提供了函数 cv2.mean(),用于计算一个对象的平均颜色或平均灰度。该函数的语法格式为:

  • mean_val=cv2.mean(im,mask=mask)
    • mean_val:表示返回的平均值。
    • im:原图像。
    • mask:掩模。
import cv2
import numpy as np

img = cv2.imread('../lena.bmp')
mean_val = cv2.mean(img)
print(mean_val)

cv2.imshow('img', img)
cv2.waitKey()
cv2.destroyAllWindows()

# 输出结果
(124.05046081542969, 124.05046081542969, 124.05046081542969, 0.0)

注意:函数cv2.mean()能够计算各个通道的均值。上述4个值分别是RGB和A通道(alpha通道)的均值。本例中,没有A通道所以A通道为0,RGB三个通道的值相同,所以计算出的均值也是一样的。

image-20211128030222124

9. 极点:

有时,我们希望获取某个对象内的极值点,例如最左端、最右端、最上端、最下端的四个点。

OpenCV提供了相应的函数来找出这些点,通常的语法格式是:

  • leftmost=tuple(cnt [cnt[:,:,0].argmin()] [0])
  • rightmost=tuple(cnt[cnt[:,:,0].argmax()] [0])
  • topmost=tuple(cnt[cnt[:,:,1].argmin()] [0])
  • bottommost=tuple(cnt[cnt[:,:,1].argmax()] [0])
import cv2
import numpy as np

img = cv2.imread('../contour2.bmp')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
contours, hierarchy = cv2.findContours(gray, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)

new_img = img.copy()
cv2.drawContours(new_img, contours[0], -1, (250, 150, 150), 2)

cnt = contours[0]
leftmost = tuple(cnt[cnt[:, :, 0].argmin()][0])
rightmost = tuple(cnt[cnt[:, :, 0].argmax()][0])
topmost = tuple(cnt[cnt[:, :, 1].argmin()][0])
bottommost = tuple(cnt[cnt[:, :, 1].argmax()][0])
print(f'leftmost: {
      
      leftmost}')
print(f'rightmost: {
      
      rightmost}')
print(f'topmost: {
      
      topmost}')
print(f'bottommost: {
      
      bottommost}')

cv2.putText(new_img, 'A', leftmost, cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
cv2.putText(new_img, 'B', rightmost, cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
cv2.putText(new_img, 'C', topmost, cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
cv2.putText(new_img, 'D', bottommost, cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
cv2.imshow('rst', new_img)

cv2.waitKey()
cv2.destroyAllWindows()

# 结果
leftmost: (165, 141)
rightmost: (405, 127)
topmost: (348, 70)
bottommost: (180, 190)

均值。上述4个值分别是RGB和A通道(alpha通道)的均值。本例中,没有A通道所以A通道为0,RGB三个通道的值相同,所以计算出的均值也是一样的。

image-20211128031312726

猜你喜欢

转载自blog.csdn.net/weixin_57440207/article/details/122647019