【OpenCV 学习笔记】第十二章: 图像轮廓

第十二章: 图像轮廓

  • 图像边缘和图像轮廓的区别
    前面我们在图像形态学操作里,用cv2.morphologyEx()这个函数实现图像梯度的提取,就是用膨胀图像-腐蚀图像,获取一个图像中前景图像的边缘。还有我们的礼帽黑帽一定程度也能提取图像的边缘信息。 我们还在图像梯度里面详细讲了sobel算子、scharr算子、laplasian算子、canny边缘检测,这些都是检测图像中边缘线条的。
    本章讲的是图像轮廓,图像轮廓和图像边缘不是一回事,图像边缘不是图像轮廓!图像边缘是图像中的线条,这些线条是不连续的、零散的线段,只要是有梯度,我把有梯度的像素点提取出来就可以了,这是边缘检测的操作手法。而图像轮廓首先要是一个整体的,就是将边缘连接起来形成一个整体,才叫轮廓。【OpenCV 学习笔记】第十二章: 图像轮廓_第1张图片

边缘检测主要是通过一些手段检测数字图像中明暗变化剧烈(即梯度变化比较大)像素点,偏向于图像中像素点的变化。如canny边缘检测,结果通常保存在和源图片一样尺寸和类型的边缘图中。 轮廓检测指检测图像中的对象边界,更偏向于关注上层语义对象,主要用来分析物体的形态,比如物体的周长和面积等。可以说边缘包括轮廓。边缘主要是作为图像的特征使用,比如可以用边缘特征可以区分脸和手,而轮廓则是一个很好的图像目标的外部特征,这种特征对于我们进行图像分析,目标识别和理解等更深层次的处理都有很重要的意义。

一、轮廓检测函数:cv2.findContours()

  • image, contours, hierarchy = cv2.findContours(img, mode, method)
    img: 要做轮廓检测的图像,必须是8位单通道二值图像。所以,一般情况下我们都是将图像处理为二值图像后再将其作为参数传入。在很多情况下,我们是预先对图像进行阈值分割或者边缘检测处理(比如经过Canny、拉普拉斯等边缘检测算子处理过的二值图像),在得到满意的二值图像后再作为参数传入使用,这样效果会更好。
    mode: 轮廓检索模式。决定了轮廓的提取方式:
      cv2.RETR_EXTERNAL: 只检测最外面的轮廓
      cv2.RETR_LIST: 检索所有的轮廓,并将其保存到一条链表当中。对检测到的轮廓不建立等级关系
      cv2.RETR_CCOMP:检索所有轮廓并将它们组织成两级层次结构。顶层是各部分的外部边界,第二层是空洞的边界。
      cv2.RETR_TREE:检索所有轮廓,并建立一个等级树结构的轮廓,就是重构嵌套轮廓的整个层次。
      说明:一般情况下我们只用第4种模式,因为第4种模式是检测所有的轮廓并且把这些轮廓按层次保存成一个树结构,后面如果我们有需要直接调用即可。
    method: 轮廓逼近方法,就是如何表达轮廓,意思就是我是用线表示轮廓呢,还是简单点用2个点就表示了一条线的轮廓:
      cv2.CHAIN_APPROX_NONE: 以Freeman链码的方式输出轮廓。意思就是我存储了所有的轮廓点,就是相连两个点的像素位置差不超过1,我可以用完整的线条来表示轮廓,就是我可以画出一个完整的轮廓。
      cv2.CHAIN_APPROX_SIMPLE:压缩水平方向、垂直方向、对角线方向的元素,只保留该方向的终点坐标。比如一个矩形只要用4个点来保存轮廓信息即可。因为要保存信息太多了,内存计算起来都是负担,我们就用4个点表示一个矩形轮廓吧。同理,如果是一个多边形,我们就输出这个多边形的顶点序列吧。
      说明:method这个参数的参数值总共有4个,但一般情况我们只用上面这两种值就够了。

    函数返回值 :
      image:与参数img的尺寸一致的。后面高版本的opencv已经不返回这个对象了。
      contours:是返回的轮廓。这个轮廓是一个数组。
      hierarchy:是轮廓的层次信息,就是mode参数决定的返回的轮廓数据的组织结构。

  • 说明:在openCV中,我们都是从黑色背景中查找白色对象,因此,对象必须是白色的,背景必须是黑色的。

  • 小结使用轮廓检测函数cv2.findContours()要注意的点:
    1、我们检测一张彩图的轮廓时,首先我们要把彩图转换为灰度图像,然后我们要用阈值函数把灰度图像处理成二值图像,此时才能作为参数img传入函数。
    2、我检测轮廓的时候,一张图片可能有多个轮廓,也可能有一个轮廓里面套几个轮廓(就是空洞),所以我们要明确我们想检测几级轮廓。
      当我们只想检测最外面的一层轮廓时,参数mode=cv2.RETR_EXTERNAL。
      当我们想检测所有轮廓(就是轮廓里面套的轮廓,可以嵌套多层)时,参数mode可以选择其他三种,其中:
         cv2.RETR_LIST是把所有的轮廓都放在一个list列表里面,不去区分轮廓之间的等级关系;
         cv2.RETR_CCOMP是只把所有轮廓分2级;
         cv2.TETR_TREE是建立一个树结构的层次关系。
    3、当我们轮廓检测完毕后,不管是检测一个轮廓还是检测所有轮廓,检测所有轮廓不管是保存轮廓之间的等级关系还是不保存,我们的轮廓本身数据可以有两种方式存储,第一种就是method=cv2.CHAIN_APPROX_NONE就是轮廓的所有像素点都保存了,此时我们调用这个轮廓也就是这个函数返回的contours,这个contours可视化处理就是轮廓线。第二种是当method=cv2.CHAIN_APPROX_SIMPLE时,就表示我们轮廓数据不是连续的像素点而是轮廓的顶点序列,此时我们可视化coutours时就是一些顶点,就是轮廓的顶点,而不是线。
    所以返回对象contours有这些属性:
      len(contours)返回的就是我们都检测到了几个轮廓。
      len(contours[i])就是第i个轮廓长度,就是它有多少个像素点。
      contours[i].shape返回的就是轮廓内点的形状,比如(4, 1, 2)就表示轮廓i有4个轮廓点,每个点是1行2列。如下所示:
        [[[79,270]]
        [[79,383]]
        [[195,383]]
        [[195, 270]] 这其实就是一个方框轮廓的4个点的坐标值。
    4、hierarchy是我们检测到的轮廓的等级关系的数据,这个数据可以反映我们的轮廓之间是如何连接的。就是每个轮廓都有一个都是:contours[i]=[next, previous, first_child, parent],即后一个轮廓的索引编号、前一个轮廓的索引编号、第1个子轮廓的索引编号、负轮廓的索引编号。没有对应关系是设为-1。可以用print(hierarchy)来查看它的值。

  • 轮廓检测算法的原理,是satoshi suzuki在1985年发表的一篇论文《Topological structural analysis of digitized binary images by border following》,论文里面介绍了两种算法来实现轮廓的提取,我们上面的findcontour就是基于这篇论文的思路来实现的。具体实现思路感兴趣的同学可以参考下面的博文辅助理解:
    OpenCV轮廓提取算法详解findContours() - 知乎

二、轮廓绘制函数:
cv2.drawContours(img, contours, contourIdx, color [, thickness, lineType, hierarchy, maxLevel, offset] )

img:待绘制轮廓的图像
contours:需要绘制的轮廓,这个参数是list类型,就是函数cv2.findContours()的输出。
contourIdx:需要绘制的轮廓的索引号,如果contourIdx=-1,表示绘制全部轮廓;如果这个参数是零或者正整数,表示要绘制的轮廓是对应的索引号的轮廓。
color:绘制的颜色,用BGR表示。
thickness:表示轮廓的粗细,如果thickness=-1则表示要绘制实心轮廓。
lineType:轮廓的线条形状。
hierarchy:函数cv2.findContours()函数返回的层次信息。
maxLevel:要绘制的轮廓的层次的深度。
offset:偏移参数,使轮廓偏移到不同的位置展示出来。
由于该函数是在img的基础上绘制的,不会再重新生成一个带轮廓的新对象,所以该函数没有返回值!!!所以,要记得保存原图。

  • 轮廓检测小结:
    1、当我们拿到的原图是一张彩图的时候,第一步我们要把三通道的彩图转化为灰度图像。
    2、第二步,把灰度图像用阈值函数处理成二值图像。
    3、第三步,检测轮廓
    4、第四步,绘制轮廓
    #例12.1 绘制图像内的轮廓  
    import cv2
    import numpy as np
    import matplotlib.pyplot as plt
    
    img = cv2.imread(r'C:\Users\25584\Desktop\contours.bmp')  #读图像
    img_copy = img.copy()
    img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)          #将图像变成灰度图像
    t, img_binary = cv2.threshold(img_gray, 127, 255, cv2.THRESH_BINARY)  #将灰度图像变成二值图像
    contours, hierarchy = cv2.findContours(img_binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)  #生成轮廓
    img_contours = cv2.drawContours(img_copy, contours, -1, (255,0,0), 5)   #在原图上绘制轮廓
    
    #在一张黑色的背景里,分别绘制三个轮廓:
    list = ['img_contours0', 'img_contours1', 'img_contours2']
    for i in range(3):
        img_temp = np.zeros(img.shape, np.uint8)
        list[i] = cv2.drawContours(img_temp, contours, i, (255,0,0), 5) 
        
    #可视化轮廓
    fig, axes = plt.subplots(1,4, figsize=(10,6), dpi=100)
    axes[0].imshow(img_contours, cmap='gray')
    axes[1].imshow(list[0], cmap='gray')
    axes[2].imshow(list[1], cmap='gray')
    axes[3].imshow(list[2], cmap='gray')
    plt.show()

    【OpenCV 学习笔记】第十二章: 图像轮廓_第2张图片

    #例12.2 使用轮廓绘制功能,提取前景图像
    import cv2
    import numpy as np
    import matplotlib.pyplot as plt
    
    img = cv2.imread(r'C:\Users\25584\Desktop\loc3.jpg')  #读图像
    img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)   #转灰度
    t, img_binary = cv2.threshold(img_gray, 127, 255, cv2.THRESH_BINARY)  #转二值
    contours, hierarchy = cv2.findContours(img_binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)  #生成轮廓
    
    mask = np.zeros(img.shape, np.uint8)
    mask = cv2.drawContours(mask, contours, -1, (255,255,255), -1)  #最后一个参数thinkness=-1表示实心填充
    img_pre = cv2.bitwise_and(img, mask)
    
    fig, axes = plt.subplots(1,3, figsize=(10,6), dpi=100)
    axes[0].imshow(img)
    axes[1].imshow(mask)
    axes[2].imshow(img_pre)
    plt.show()

    【OpenCV 学习笔记】第十二章: 图像轮廓_第3张图片

     

    三、轮廓特征

    我们绘制图像中的物体轮廓的目的是什么?目的就是获取图像中目标的大小、位置、方向等信息。 但是怎么通过轮廓获得图像中目标的大小位置方向?通过计算轮廓的一些特征去判断。比如计算轮廓的面积、周长、质心、边界等信息去判断。所以,在OpenCV提取轮廓之后,还可以进行许多操作:

  • ContourArea():计算轮廓区域的面积
  • ArcLength():计算轮廓长度
  • BoundingRect():轮廓的外包矩形
  • MinAreaRect():轮廓的最小外包矩形
  • MinEnclosingCircle():轮廓的最小外包圆
  • fitEllipse():用椭圆拟合轮廓
  • approxPolyDP():逼近多边形拟合轮廓
  • ConvexHull():提取轮廓的凸包
  • IsContourConvex():测试轮廓的凸性

1、计算轮廓的面积
cv2.contourArea(contour [, oriented])返回轮廓的面积值
contour:轮廓,就是findContours()的返回值
oriented:布尔型值,默认是False。当oriented=True时,返回值包含正负号,正号表示轮廓是顺时针的,负号表示轮廓是逆时针的。默认值表示返回的是一个绝对值。 

#例12.3 计算轮廓的面积,并将面积大于5000的轮廓筛选出来
#倒库
import cv2
import numpy as np
import matplotlib.pyplot as plt

#生成图像轮廓
img = cv2.imread(r'C:\Users\25584\Desktop\moments.bmp')  #读图像
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)   #转灰度
t, img_binary = cv2.threshold(img_gray, 127, 255, cv2.THRESH_BINARY)  #转二值
contours, hierarchy = cv2.findContours(img_binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)  #生成轮廓

#计算轮廓的面积
for i in range(len(contours)):
    print("轮廓{}的面积是:{}".format(i+1, cv2.contourArea(contours[i])))

#筛选面积大于5000的轮廓
area_5000 = []
for i in range(len(contours)):
    if cv2.contourArea(contours[i])>=5000:
        area_5000.append(contours[i])
    
#可视化所有轮廓
fig, axes = plt.subplots(1,6, figsize=(10,6), dpi=100) 
axes[0].imshow(img, cmap='gray')

for i in range(3):
        tmp = np.zeros(img.shape, np.uint8)
        cv2.drawContours(tmp, contours, i, (255, 0, 0), 3)
        axes[i+1].imshow(tmp, cmap='gray')
        
tmp1 = np.zeros(img.shape, np.uint8)
cv2.drawContours(tmp1, contours, -1, (255, 0, 0), 3)
axes[4].imshow(tmp1, cmap='gray')

tmp2 = np.zeros(img.shape, np.uint8)
cv2.drawContours(tmp2, area_5000, -1, (255, 0, 0), 3)
axes[5].imshow(tmp2, cmap='gray')
plt.show()
轮廓1的面积是:14900.0
轮廓2的面积是:34314.0
轮廓3的面积是:3900.0

说明:计算轮廓的面积就是计算轮廓里面都有多少个像素点,也是把轮廓里面的像素点的值全部加和,而每个像素点的值都是1,因为我们生成的轮廓图像都是0-1二值图像。
比如上例中的轮廓3就是:(96-44)x(729-654)=3900,就是轮廓的行的像素个数x列的像素个数

2、计算轮廓的周长
cv2.arcLength(contour, closed)返回轮廓的周长
contour:轮廓,就是findContours()的返回值
closed:布尔型值,用来表示轮廓是否是封闭的,当closed=True时,表示轮廓是封闭的。

#例12.4 计算轮廓的周长  
import cv2
import numpy as np
import matplotlib.pyplot as plt

img = cv2.imread(r'C:\Users\25584\Desktop\contours.bmp')  #读图像
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)   #转灰度
t, img_binary = cv2.threshold(img_gray, 127, 255, cv2.THRESH_BINARY)  #转二值
contours, hierarchy = cv2.findContours(img_binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)  #生成轮廓

for i in range(len(contours)):
    print('第{}个轮廓的周长是:{}'.format(i+1, cv2.arcLength(contours[i], True)))

#可视化所有轮廓
fig, axes = plt.subplots(1,5, figsize=(10,6), dpi=100)  #可视化一下轮廓
axes[0].imshow(img, cmap='gray')
for i in range(1,5):
    temp = np.zeros(img.shape, np.uint8)
    if i != 4:
        cv2.drawContours(temp, contours, i-1, (255,0,0), 3)
        axes[i].imshow(temp, cmap='gray')
    else:
        cv2.drawContours(temp, contours, -1, (255,0,0), 3)
        axes[i].imshow(temp, cmap='gray')
plt.show()
第1个轮廓的周长是:458.0
第2个轮廓的周长是:685.0437242984772
第3个轮廓的周长是:411.6467487812042

说明:计算轮廓周长就是就是轮廓线上的像素的值的和,而像素的值都是1。上例中的第一个轮廓周长就是:((383-270)+(195-79))x2=458

3、矩特征
图像识别的一个核心问题是图像的特征提取,提取特征就是用一组简单的数据来描述(或者说代替)整个图像,这组数据越简单越有代表性,也就越好。 一张图片即使有光线、噪点、几何形变等的干扰,但是只要我们能提取出一个好的目标轮廓,然后计算目标轮廓的各种矩,就能判断这个目标的大小、方向、形状等几何特征。比如一张图片中的目标由于拍摄角度造成变形,但只要我们计算目标轮廓的不变矩,就还能识别出这个目标,所以图像矩广泛应用于模式识别、目标分类、目标识别与防伪估计、图像编码与重构等领域。

特征矩分:

  (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  #所以该值可以用来比较图像中不同位置不同大小的轮廓之间的相似性

 (4) Hu距:Hu矩主要是利用归一化中心矩,通过线性组合构造了7个不变特征矩:【OpenCV 学习笔记】第十二章: 图像轮廓_第4张图片        

Hu矩又叫Hu不变矩,在图像旋转、缩放、平移等操作后,仍能保持矩的不变性,所以有时候用Hu不变距更能识别图像的特征。          

  • API:
    cv2.moments(contours[i]) #里面参数是第i个轮廓
    cv2.HuMoments(cv2.moments[i]).flatten() #里面的参数是moments函数的返回值
    #例12.5 计算轮廓的矩特征值  
    import cv2
    import numpy as np
    import matplotlib.pyplot as plt
    
    img = cv2.imread(r'C:\Users\25584\Desktop\moments.bmp')  #读图像
    img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)   #转灰度
    t, img_binary = cv2.threshold(img_gray, 127, 255, cv2.THRESH_BINARY)  #转二值
    contours, hierarchy = cv2.findContours(img_binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)  #生成轮廓
    #可视化轮廓
    fig1 = np.zeros(img.shape, np.uint8)   #生成一个画布把轮廓1画出来
    cv2.drawContours(fig1, contours, 0, (255,0,0),3)
    fig2 = np.zeros(img.shape, np.uint8)    #生成一个画布把轮廓2画出来
    cv2.drawContours(fig2, contours, 1, (255,0,0),3)
    fig3 = np.zeros(img.shape, np.uint8)    #生成一个画布把轮廓3画出来
    cv2.drawContours(fig3, contours, 2, (255,0,0),3)
    fig4 = np.zeros(img.shape, np.uint8)    #生成一个画布把所有轮廓画出来
    cv2.drawContours(fig4, contours, -1, (255,0,0),3)
    fig, axes = plt.subplots(1,5, figsize=(10,6), dpi=100)  #可视化一下轮廓
    axes[0].imshow(img, cmap='gray')
    axes[1].imshow(fig1, cmap='gray')
    axes[2].imshow(fig2, cmap='gray')
    axes[3].imshow(fig3, cmap='gray')
    axes[4].imshow(fig4, cmap='gray')
    plt.show()
    #计算每个轮廓的10个空间矩、7个中心距、7个归一化中心距
    print(cv2.moments(contours[0]), '\n')  #计算轮廓1的特征矩
    print(cv2.moments(contours[1]), '\n')  #计算轮廓2的特征矩
    print(cv2.moments(contours[2]), '\n')  #计算轮廓3的特征矩
    #计算7个Hu不变矩
    print(cv2.HuMoments(cv2.moments(contours[0])).flatten(), '\n')
    print(cv2.HuMoments(cv2.moments(contours[1])).flatten(), '\n')
    print(cv2.HuMoments(cv2.moments(contours[2])).flatten(), '\n')

    【OpenCV 学习笔记】第十二章: 图像轮廓_第5张图片

     

    四、根据轮廓的矩特征进行图像匹配

    我们上面讲了轮廓的特征:面积特征、周长特征、矩特征。其中矩特征中不变矩特征是最好的,因为即使图像平移、旋转、缩放了,不变矩特征值都不变,所以我们本节用这个特征进行图像匹配。
    但是,上例中计算的不变矩,从数值上看,都非常非常小,也就是非常非常接近,我们光从数值上几乎无法看出明显的不同,所以我们非要用这个指标的话,我们就得把这个指标用-log映射一下再对比,opencv给我们提供了cv2.matchShapes()接口来对两个对象的Hu矩进行比较:

  • cv2.matchShapes(contour1, contour2, method, parameter)
    method:表示对比方法。method=1,对比的方法是先取-log,再取倒,再取差的绝对值,再7个值取和。method=2是负对数后直接算差的绝对值,再求和即可。method=3此处省略,看公式即可,文字描述晦涩。一般情况下我们都设这个参数为1。
    parameter:该参数是一个扩展参数,目前暂不支持这个参数,设为0即可。
    #例12.6 使用Hu矩计算三张图的匹配度    
    import cv2
    import numpy as np
    import matplotlib.pyplot as plt
    
    img1 = cv2.imread(r'C:\Users\25584\Desktop\cs1.bmp')
    img2 = cv2.imread(r'C:\Users\25584\Desktop\cs2.bmp')
    img3 = cv2.imread(r'C:\Users\25584\Desktop\cc.bmp')
    
    img1_gray = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)   
    img2_gray = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)   
    img3_gray = cv2.cvtColor(img3, cv2.COLOR_BGR2GRAY)   
    
    t, img1_binary = cv2.threshold(img1_gray, 127, 255, cv2.THRESH_BINARY)
    t, img2_binary = cv2.threshold(img2_gray, 127, 255, cv2.THRESH_BINARY)
    t, img3_binary = cv2.threshold(img3_gray, 127, 255, cv2.THRESH_BINARY)
    
    contours1, hierarchy1 = cv2.findContours(img1_binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) 
    contours2, hierarchy2 = cv2.findContours(img2_binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) 
    contours3, hierarchy3 = cv2.findContours(img3_binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) 
    
    matchshape1 = cv2.matchShapes(contours1[0], contours1[0], 1, 0)
    matchshape2 = cv2.matchShapes(contours1[0], contours2[0], 1, 0)
    matchshape3 = cv2.matchShapes(contours1[0], contours3[0], 1, 0)
    print(matchshape1)
    print(matchshape2)
    print(matchshape3)
    
    #可视化图像和轮廓
    fig, axes = plt.subplots(1,6, figsize=(10,6), dpi=100) 
    axes[0].imshow(img1, cmap='gray')
    axes[1].imshow(img2, cmap='gray')
    axes[2].imshow(img3, cmap='gray')
    for i in range(3):
        contours = [contours1,contours2,contours3]
        tmp = np.zeros(img1.shape, np.uint8)
        cv2.drawContours(tmp, contours[i], 0, (255, 0, 0), 3)
        axes[i+3].imshow(tmp, cmap='gray')
    plt.show()
    0.0
    0.10720296440067095
    0.5338506830800509

    说明:第一幅图自己和自己的hu矩匹配度为0,就是二者的hu矩差值=0,就是匹配。 第一幅图和第二幅图的hu矩差值要小于第一幅图和第三幅图之间的差值。因为第二幅图就是第一幅图经过缩放、旋转和平移得到了一张很小的小图,但二者接近。
    所以,用matchshape函数做图像轮廓匹配的时候,返回值越小越匹配,越大表示两张轮廓的HU矩差值越大,就说明两个轮廓的HU矩越不一样,就是两个轮廓越不接近,就是两个目标图越不是一个物体。
    结论:图像越相似,返回值越小,图像越不相似,返回值越大。

    五、轮廓拟合

    有时我们可能不需要特别准确的轮廓,而是需要一个接近于轮廓的近似多边形。opencv提供了多种计算轮廓近似多边形的方法:

    1、生成矩形包围框:x,y,w,h = cv2.boundingRect(arry)
    x,y是矩形边界框左上角点的坐标值,w是x方向的长度,h是y方向的长度。这个函数还可以只返回一个对象,这个对象就是xywh组成的元组

  • 绘制边框的函数:
    (1)drawContours() 函数来绘制 #A 我们需要对boundingRect()函数返回的四个对象进行处理,处理成array类型,然后再套上[]转成list,传入绘制函数。
    (2)cv2.rectangle(img, pt1, pt2, color, thickness) 专门用来绘制矩形边框的函数
    #例12.7 绘制矩形包围框      
    import cv2
    import numpy as np
    import matplotlib.pyplot as plt
    
    img = cv2.imread(r'C:\Users\25584\Desktop\cc.bmp')
    img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    thr, img_binary = cv2.threshold(img_gray, 127, 255, cv2.THRESH_BINARY)
    contours, hierarchy = cv2.findContours(img_binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    
    #生成矩形框
    x,y,w,h = cv2.boundingRect(contours[0])   #返回4个对象
    #bound_rect = cv2.boundingRect(contours[0])  #返回1个对象,就是一个元组(202, 107, 157, 73),分别是x,y,w,h  
    
    #用drawContours绘制矩形包围框  #A
    bound = np.array([[[x,y]], [[x+w, y]], [[x+w, y+h]], [[x, y+h]]])   #bound.shape返回:(4, 1, 2)
    result = img.copy()
    result = cv2.drawContours(result, [bound], -1, (255,255,255), 2)   #注意这里参数bound如果不加[]就绘制出来的是4个点
    
    #用cv2.rectangle绘制包围框
    result1 = img.copy()
    result1 = cv2.rectangle(result1, (x,y), (x+w, y+h), (255,255,255), 2) 
    
    #可视化
    fig, axes = plt.subplots(1,3, figsize=(8,5), dpi=100) 
    axes[0].imshow(img, cmap='gray')
    axes[1].imshow(result, cmap='gray')
    axes[2].imshow(result1, cmap='gray')
    plt.show()

    【OpenCV 学习笔记】第十二章: 图像轮廓_第6张图片

    2、生成最小包围矩形框:(最小外接矩形的中心(x,y),(宽度,高度),旋转角度) = cv2.minAreaRect(arry)

  • 边框绘制函数
    cv2.minAreaRect()函数的返回值是一个元组:(最小外接矩形的中心(x,y),(宽度,高度),旋转角度),并且这个元组里面的元素都是浮点型的。这个结构和数据类型都不符合cv2.drawContours()的参数结构要求,所以需要转化。
    转化函数:ponts = cv2.boxPoints(box)
         box是cv2.minAreaRect()的返回值元组
         points是一个数组,就是把cv2.minAreaRect函数的返回值元组转化为一个数组,但是这个数组里面的元素还是浮点型的,所以我们还要用np.int0(points)把它转化成整型就可以传入绘图函数cv2.drawContours了。
    #例12.8 绘制最小矩形包围框      
    img = cv2.imread(r'C:\Users\25584\Desktop\cc.bmp')
    img0 = img.copy()
    img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    thr, img_binary = cv2.threshold(img_gray, 127, 255, cv2.THRESH_BINARY)
    contours, hierarchy = cv2.findContours(img_binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)  #生成轮廓
    
    rect = cv2.minAreaRect(contours[0])  #生成最小包围矩形框, contours是一个list,所以要[0]切出来一个array当作参数出入
    points = cv2.boxPoints(rect)  #将最小包围框的数据结构转化为drawContours()要求的参数结构   
    result = cv2.drawContours(img0, [np.int0(points)], -1, (255,255,255), 2)  
    
    #可视化
    fig, axes = plt.subplots(1,2, figsize=(5,3), dpi=100) 
    axes[0].imshow(img, cmap='gray')
    axes[1].imshow(result, cmap='gray')
    plt.show()

    【OpenCV 学习笔记】第十二章: 图像轮廓_第7张图片

    3、最小包围圆形:center, radius = cv2.minEnclosingCircle(arry)
       这个函数返回的center是圆的是一个元组,radius是一个浮点型标量。所以也需要专门的绘制函数绘制:
       cv2.circle(img, (x,y), radius, (255,255,255),3)
        img:要绘制的画布图像,(x,y)是原点坐标,数据类型必须是整型, radius是半径,也必须是整型, 后面两个参数是颜色和粗细。

    4、最优拟合椭圆:retval = cv2.fitEllipse(arry)
       这个函数的返回值retval是:椭圆的中心点(x,y)、椭圆的宽高(w,h)也就是椭圆的轴长、椭圆的旋转角度
       所以要可视化这个结果还得需要转化函数:
      cv2.ellipse(img, retval, (255,255,255), 3)
        img是要画的画布,retval是cv2.fitEllipse的返回值,后面是颜色和线条粗细。

    5、最小外包三角形:retval = cv2.minEnclosingTriangle(arry)
       返回值是一个元组,元组第一个元素是这个三角形的面积,第二个元素是一个array数组,数组包含了三角形3个顶点的坐标。 制函数用:
      cv2.line(img, (x1,y1), (x2,y2), (255,255,255), 2)
        img是要绘制的画布图像,后面2个参数是两个顶点的坐标, 后面绘制的颜色和粗细。

    #例12.9 绘制圆形包围框、椭圆包围框 、最小外包三角形    
    import cv2
    import numpy as np
    import matplotlib.pyplot as plt
    
    img = cv2.imread(r'C:\Users\25584\Desktop\cc.bmp')
    img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    thr, img_binary = cv2.threshold(img_gray, 127, 255, cv2.THRESH_BINARY)
    contours, hierarchy = cv2.findContours(img_binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)  
    
    #生成圆形包围框
    circle = cv2.minEnclosingCircle(contours[0])  
    result = img.copy()
    result = cv2.circle(result, np.int0(circle[0]), np.int0(circle[1]), (255,0,0), 2)  #要把原点和半径全部转化为整型
    print('circle是:', circle)
    
    #生成椭圆包围框
    ellipse = cv2.fitEllipse(contours[0])  #返回的是一个元组
    result1 = img.copy()
    result1 = cv2.ellipse(result1, ellipse, (255,0,0), 2)
    print('ellipse是:', ellipse)
    
    #生成最小外包三角形
    triangle = cv2.minEnclosingTriangle(contours[0])  
    print('最小三角形的面积是:', np.int0(triangle[0]))
    result2 = img.copy()
    cv2.line(result2, np.int0(triangle[1][0]).ravel(), np.int0(triangle[1][1].ravel()), (255,0,0),2)  #要先把顶点坐标切出来,取整、拉平(从二维降到一维)
    cv2.line(result2, np.int0(triangle[1][0]).ravel(), np.int0(triangle[1][2].ravel()), (255,0,0),2)
    cv2.line(result2, np.int0(triangle[1][1]).ravel(), np.int0(triangle[1][2].ravel()), (255,0,0),2)
    
    #可视化
    fig, axes = plt.subplots(1,4, figsize=(12,4), dpi=100) 
    axes[0].imshow(img, cmap='gray')
    axes[1].imshow(result, cmap='gray')
    axes[2].imshow(result1, cmap='gray')
    axes[3].imshow(result2, cmap='gray')
    plt.show()
    circle是: ((280.01922607421875, 137.5), 78.05937957763672)
    ellipse是: ((276.2112731933594, 139.6067352294922), (63.01350021362305, 166.72308349609375), 82.60102844238281)
    最小三角形的面积是: 12904

    6、生成逼近多边形:approxCurve = cv2.approxPolyDP(curve, epsilon, closed)
    approxCurve:为逼近的多边形的点集
    curve:轮廓
    epsilon:指定的精度,也即是原始曲线与近似曲线之间的最大距离。该参数一般设为轮廓周长的百分比形式。
    closed:布尔型值,若为true,则逼近多边形是封闭的;反之,若为false,则不封闭。

    该函数是采用道格拉斯-普克算法(Douglas-Peucker)算法(DP算法)来实现的。主要功能是把一个连续光滑曲线折线化,就是找到较少的点去近似曲线。
    经典的Douglas-Peucker算法描述如下:
    (1)在曲线首尾两点A,B之间连接一条直线AB,该直线为曲线的弦;
    (2)得到曲线上离该直线段距离最大的点C,计算其与AB的距离d;
    (3)比较该距离与预先给定的阈值threshold的大小,如果小于threshold,则该直线段作为曲线的近似,该段曲线处理完毕。
    (4)如果距离大于阈值,则用C将曲线分为两段AC和BC,并分别对两段取信进行1~3的处理。
    (5)当所有曲线都处理完毕时,依次连接各个分割点形成的折线,即可以作为曲线的近似。

    #例12.10 绘制不同精度的逼近多边形      
    import cv2
    import numpy as np
    import matplotlib.pyplot as plt
    
    img = cv2.imread(r'C:\Users\25584\Desktop\cc.bmp')
    img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    thr, img_binary = cv2.threshold(img_gray, 127, 255, cv2.THRESH_BINARY)
    contours, hierarchy = cv2.findContours(img_binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)  
    
    length = cv2.arcLength(contours[0], True)
    
    result_list = []
    for i in [0.1, 0.09, 0.055, 0.05, 0.02]:
        approxCurve = cv2.approxPolyDP(contours[0], length*i, True)
        temp = img.copy()
        temp = cv2.drawContours(temp, [approxCurve], -1, (255,0,0),2)
        result_list.append(temp)
        
    #可视化
    fig, axes = plt.subplots(1,5, figsize=(16,6), dpi=100) 
    axes[0].imshow(result_list[0], cmap='gray')
    axes[1].imshow(result_list[1], cmap='gray')
    axes[2].imshow(result_list[2], cmap='gray')
    axes[3].imshow(result_list[3], cmap='gray')
    axes[4].imshow(result_list[4], cmap='gray')
    plt.show()

    【OpenCV 学习笔记】第十二章: 图像轮廓_第8张图片

     

    六、凸包

    逼近多边形是在图像轮廓的里面,而且多边形可能有超过180度的角。
    而凸包是包在轮廓最外面的凸多边形,是降轮廓上的像素点全部包住,凸包内任意两点的连线都在凸包内部,凸包内的角都是小于180度的。

  • 1、生成凸包:hull = cv2.convexHull(points [, clockwise, returnPoints])
    hull:返回的是凸包角点
    points:轮廓
    clockwise:这个参数=True时,凸包角点按顺时针方向排列,反之按逆时针排列
    returnPoints:默认值是True,返回凸包角点的xy轴坐标,否则返回轮廓中凸包角点的索引。
    说明:函数的原理参考:凸包 —— 五种解法_lxt_Lucia的博客-CSDN博客_凸包

  • 凸包绘制函数:cv2.polylines(img, pts, isClosed, color[, thickness[, lineType[, shift]]])
    凸包绘制也可以用cv2.drawContours()函数绘制

    #例12.11 练习cv2.convexHull()函数  
    import cv2
    import numpy as np
    import matplotlib.pyplot as plt
    
    img = cv2.imread(r'C:\Users\25584\Desktop\hand.bmp')
    img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    thr, img_binary = cv2.threshold(img_gray, 127, 255, cv2.THRESH_BINARY)
    contours, hierarchy = cv2.findContours(img_binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)  #contours有490个点
    
    hull = cv2.convexHull(contours[0])   #生成凸包,有16个点
    result = img.copy()
    result = cv2.polylines(result, [hull], True, (0,255,0), 2)  #绘制凸包
    result1 = result.copy()
    result1 = cv2.drawContours(result1, contours, -1, (255,0,0),2)  #绘制轮廓
    
    #可视化
    fig, axes = plt.subplots(1,3, figsize=(8,4), dpi=100) 
    axes[0].imshow(img, cmap='gray')
    axes[1].imshow(result, cmap='gray')
    axes[2].imshow(result1, cmap='gray')
    plt.show()

    【OpenCV 学习笔记】第十二章: 图像轮廓_第9张图片

  • 2、凸缺陷
    凸缺陷是凸包与轮廓之间的部分。凸缺陷能够用来识别动作、姿势等。
    获取凸缺陷:convexityDefects = cv2.convexityDefects(contour, convexhull)
    contour:是轮廓
    convexhull:是凸包,是cv2.convexHull()函数的返回值,但是cv2.convexHull()的参数returnPoints必须是False,也就是凸包角点对应在轮廓上的点的索引。
    返回值convexityDefects是凸缺陷点集,是一个数组,每一行包含的值是[起点,终点,轮廓上距离凸包最远的点,最远点到凸包的近似距离] ,其中,前三个值是轮廓点的索引。

  • 小结:
    一幅3通道彩图 --> cv2.imread()读入 --> cv2.cvtColor()转灰度图 --> cv2.threshold()转二值图 --> cv2.findContours()生成轮廓 --> cv2.convexHull()生成凸包 --> cv2.convexityDefects()生成凸缺陷
       生成的轮廓cv2.findContours()的返回是一个list,list里面的元素是一个个array,每个array是一个轮廓,每个array都是一个三维的数组,意思就是一个压缩的点集。
       生成的凸包cv2.convexHull(),如果参数returnPoints等于默认值True时,生成的这个凸包,也就是凸包函数的返回值就是很多用xy轴表示的角点,所以这些角点也是一个三维的数组。此时这些角点也是轮廓上的点。如果returnPoints=False,生成的凸包就是就是一个二维的数组,就是一列数组,每个数是轮廓点集的索引值。
       生成凸缺陷cv2.convexityDefects()的第一个参数就是生成的轮廓对象,第二个参数是生成的凸包对象,但这个凸包对象所有的凸包角点对应的轮廓点在轮廓点集中的索引值。

    #例12.12 练习cv2.convexityDefects()函数,将每个凸缺陷的起点和终点用一条线连接,在最远点画一个圆圈  
    import cv2
    import numpy as np
    import matplotlib.pyplot as plt
    
    img = cv2.imread(r'C:\Users\25584\Desktop\hand.bmp')
    img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    thr, img_binary = cv2.threshold(img_gray, 127, 255, cv2.THRESH_BINARY)
    contours, hierarchy = cv2.findContours(img_binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)  #生成轮廓
    hull = cv2.convexHull(contours[0], returnPoints=False)   #生成凸包
    convexityDefects = cv2.convexityDefects(contours[0], hull)  #生成凸缺陷,有12个元素
    
    result = img.copy()
    for i in range(convexityDefects.shape[0]):  #.shape返回的是(12, 1, 4)
        s,e,f,d = convexityDefects[i][0]
        start = tuple(contours[0][s][0])
        end = tuple(contours[0][e][0])
        far = tuple(contours[0][f][0])
        cv2.line(result, start, end, [0,0,255],2)
        cv2.circle(result, far, 3, [255,0,0], -1)
        
    #可视化
    fig, axes = plt.subplots(1,2, figsize=(8,4), dpi=100) 
    axes[0].imshow(img, cmap='gray')
    axes[1].imshow(result, cmap='gray')
    plt.show()    

    【OpenCV 学习笔记】第十二章: 图像轮廓_第10张图片

  • 3、几种与凸包有关的几何学测试
  • (1)测试轮廓是否是凸形:retval = cv2.isContourConvex(contour)
       参数contour表示要判断的轮廓,返回值retval是布尔型值,=true表示轮廓为凸形,否则,不是凸形。

    (2)计算点到轮廓的距离/判断点与轮廓的位置关系:retval = cv2.pointPolygonTest(contour, pt, measureDist)
       contour表示轮廓,pt表示要判定的点,函数返回值retval与参数measureDist有关
       当measureDist=True时,计算点到轮廓的距离。当点在轮廓外面返回负数距离,当点在轮廓上返回0,当点在轮廓里面返回正数距离值。
       当measureDist=False,不计算距离,只返回-1,0,1,表示点相对于轮廓的位置关系。

    在图像上标注某个点的位置:cv2.putText(img, '点的名称', (点的坐标),cv2.FONT_HERSHEY_SIMPLEX, 1, (255,0,0), 2)

    #例12.13 判断轮廓是否是凸形的,计算点到轮廓的距离  
    import cv2
    import numpy as np
    import matplotlib.pyplot as plt
    
    img = cv2.imread(r'C:\Users\25584\Desktop\hand.bmp')
    img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    thr, img_binary = cv2.threshold(img_gray, 127, 255, cv2.THRESH_BINARY)
    contours, hierarchy = cv2.findContours(img_binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)  #生成轮廓,contours是一个list,len(contours)返回1, len(contours[0])返回490
    
    #-----------------------------逼近多边形--------------------------------------
    length = cv2.arcLength(contours[0], True)  #生成轮廓周长,返回值:1156.974737048149
    approxCurve = cv2.approxPolyDP(contours[0], length*0.01, True)  #生成逼近多边形;approxCurve是个array; approxCurve.shape返回(16, 1, 2); len(approxCurve)返回16
    result = img.copy()
    result = cv2.drawContours(result, [approxCurve], -1, (0,0,255),2) #绘制逼近多边形
    
    print('逼近多边形是凸形的吗?', cv2.isContourConvex(approxCurve))
    
    result = cv2.putText(result, 'A', (200, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (255,0,0), 2)   #在result上画出A点坐标
    result = cv2.putText(result, 'B', (71,  83), cv2.FONT_HERSHEY_SIMPLEX, 1, (255,0,0), 2)   #在result上画出B点坐标
    result = cv2.putText(result, 'C', (100, 200), cv2.FONT_HERSHEY_SIMPLEX, 1, (255,0,0), 2)   #在result上画出C点坐标
    distA = cv2.pointPolygonTest(approxCurve, (200, 50), True)   #计算A点与逼近多边形之间的距离
    distB = cv2.pointPolygonTest(approxCurve, (71,  83), True)   #计算B点与逼近多边形之间的距离
    distC = cv2.pointPolygonTest(approxCurve, (100, 200), True)  #计算C点与逼近多边形之间的距离
    print(distA,'\n',distB,'\n',distC,'\n')
    
    #-----------------------------凸包--------------------------------------
    hull = cv2.convexHull(contours[0])   #生成凸包,hull是个array, len(hull)返回16
    result1 = img.copy()
    result1 = cv2.polylines(result1, [hull], True, (0,255,0), 2) #绘制凸包
    print('凸包是凸形的吗?', cv2.isContourConvex(hull),'\n')
    #凸包也可以用drawContours()函数绘制
    result2 = img.copy()
    result2 = cv2.drawContours(result2, [hull], -1, (0,0,255),2)
    
    result1 = cv2.putText(result1, 'A1', (200, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (255,0,0), 2) 
    result1 = cv2.putText(result1, 'B1', (71,  83), cv2.FONT_HERSHEY_SIMPLEX, 1, (255,0,0), 2)  
    result1 = cv2.putText(result1, 'C1', (100, 200), cv2.FONT_HERSHEY_SIMPLEX, 1, (255,0,0), 2) 
    distA1 = cv2.pointPolygonTest(hull, (200, 50), True)   #计算A点与凸包之间的距离
    distB1 = cv2.pointPolygonTest(hull, (71,  83), True)   #计算B点与凸包之间的距离
    distC1 = cv2.pointPolygonTest(hull, (100, 200), True)  #计算C点与凸包之间的距离
    distA11 = cv2.pointPolygonTest(hull, (200, 50), False)   #判断A点与凸包之间的关系
    distB11 = cv2.pointPolygonTest(hull, (71,  83), False)   #判断B点与凸包之间的关系
    distC11 = cv2.pointPolygonTest(hull, (100, 200), False)  #判断C点与凸包之间的关系
    print(distA1,'\n',distB1,'\n',distC1,'\n','\n',distA11,distB11,distC11)
    
    #可视化
    fig, axes = plt.subplots(1,4, figsize=(8,4), dpi=100) 
    axes[0].imshow(img, cmap='gray')
    axes[1].imshow(result, cmap='gray')
    axes[2].imshow(result1, cmap='gray')
    axes[3].imshow(result2, cmap='gray')
    plt.show()    

逼近多边形是凸形的吗? False
-48.834425408438825 
 -0.0 
 30.4138126514911 
凸包是凸形的吗? True 
-41.6427498056862 
 0.0 
 45.85320737343205  
 -1.0 0.0 1.0

好的参考博文:cv2.findContours() - 轮廓提取算法 -- OpenCV | We all are data.

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

前面我们学习了如何计算轮廓的矩特征,并且学了如何用矩特征去对比两个轮廓是否相似。本节用更高效的方法:形状场景算法,来比较轮廓是否相似。
opencv有一个专门的模块shape模块,这个模块中封装很多形状场景算法,可以实现高效的形状比较。

  • 1、根据形状上下文算法,计算形状场景距离
    retval = cv2.createShapeContextDistanceExtractor([nAngularBins, nRadialBins, innerRadius, outerRadius, iterations, comparer, transformer])
    这些参数都是可选参数
    nAngularBins:为形状匹配中使用的形状上下文描述符建立的角容器的数量
    aRadialBins:为形状匹配中使用的形状上下文描述符建立的径向容器的数量
    innerRadius:形状上下文描述符的内半径
    outerRadius:形状上下文描述符的外半径
    iterations:迭代次数
    comparer:直方图代价提取算子。该函数使用了直方图代价提取仿函数,可以直接采用直方图代价提取仿函数的算子作为参数
    transformer:形状变换参数。

  • 2、计算形状之间的Hausdorff距离
    retval = cv2.createHausdorffDistanceExtractor()

上面的结果都可以通过函数retval = cv2.ShapeDistanceExtractor.computeDistance(contour1, contour2)计算两个不同形状之间的距离。

#例12.14 使用cv2.createShapeContextDistanceExtractor()和cv2.createHausdorffDistanceExtractor()计算形状之间的距离  
import cv2
import numpy as np
import matplotlib.pyplot as plt


img1 = cv2.imread(r'C:\Users\25584\Desktop\cs.bmp', 0)
img2 = cv2.imread(r'C:\Users\25584\Desktop\cs3.bmp', 0)
img3 = cv2.imread(r'C:\Users\25584\Desktop\hand.bmp', 0)

ret1,binary1 = cv2.threshold(img1, 127, 255, cv2.THRESH_BINARY)
ret2,binary2 = cv2.threshold(img2, 127, 255, cv2.THRESH_BINARY)
ret3,binary3 = cv2.threshold(img3, 127, 255, cv2.THRESH_BINARY)

contours1,hierarchy1 = cv2.findContours(binary1, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
contours2,hierarchy2 = cv2.findContours(binary2, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
contours3,hierarchy3 = cv2.findContours(binary3, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

#-----------利用‘形状上下文算法’计算形状之间的距离-----------------------------
sd = cv2.createShapeContextDistanceExtractor()   #构造距离提取算子
d1 = sd.computeDistance(contours1[0], contours1[0])   #相同形状之间的距离为0
d2 = sd.computeDistance(contours1[0], contours2[0])   #相似形状之间的距离较小
d3 = sd.computeDistance(contours1[0], contours3[0])   #不相似形状之间的距离较大
print(d1,'\n', d2, '\n', d3, '\n')

#-----------------计算两个形状之间的Hausdorff距离-------------
hd = cv2.createHausdorffDistanceExtractor()    #构造Hausdorff距离提取算子
d11 = hd.computeDistance(contours1[0], contours1[0])  #相同形状之间的Hausdorff距离为0
d12 = hd.computeDistance(contours1[0], contours2[0])  #相似形状之间的Hausdorff距离较小
d13 = hd.computeDistance(contours1[0], contours3[0])  #不相似形状之间的Hausdorff距离较大
print(d11,'\n', d12, '\n', d13)

#可视化
fig, axes = plt.subplots(1,3, figsize=(6,4), dpi=100) 
axes[0].imshow(img1, cmap='gray')
axes[1].imshow(img2, cmap='gray')
axes[2].imshow(img3, cmap='gray')
plt.show()    
0.0 
 0.7913379669189453 
 2.75199031829834 

0.0 
 18.357559204101562 
 57.27128601074219

八、计算轮廓的特征(续)

轮廓自身的一些特征以及轮廓所包围的对象的特征,对描述图像有非常重要的意义,所以本部分继续讲轮廓自身的特征及轮廓所包围的对象的特征。

  • 1、宽高比AspectRation:矩形轮廓的宽高比
    宽高比 = 宽度(width)/高度(height)

  • 2、Extend:轮廓面积(对象面积)/矩形边界面积
    描述图像及其轮廓特征

  • 3、Solidity:轮廓面积(对象面积)/凸包面积
    描述图像、轮廓及凸包的特征

  • 4、等效直径(equivalent diameter):2x根号(轮廓面积/pi)
    等效直径是与轮廓面积相等的圆形的直径,用来衡量轮廓的特征值。

  • 5、方向:由函数cv2.fitEllipse()拟合的最优椭圆的返回值去获取轮廓的方向
    cv2.fitEllipse()的返回值是:中心点、轴长、旋转角度 = (x,y), (MA,ma), angle

    #例12.15 计算形状的特征    
    import cv2
    import numpy as np
    import matplotlib.pyplot as plt
    img = cv2.imread(r'C:\Users\25584\Desktop\cc.bmp', 0)
    thre, binary = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)
    contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    img1 = np.zeros(img.shape, np.uint8)                #画出轮廓图
    img1 = cv2.drawContours(img1, contours[0], -1, (255,255,255), 2)
    #-----------------------------宽高比----------------------------
    img2 = img1.copy()
    x,y,w,h = cv2.boundingRect(contours[0])  #生成矩形包围框
    img2 = cv2.rectangle(img2, (x,y), (x+w, y+h), (255,255), 2)
    aspectRatio = float(w/h)
    print((x,y,w,h), ',',  aspectRatio, '\n')
    
    #------------------------Extend=轮廓面积/矩形边界面积------------------------
    rectArea = w*h  
    contourArea = cv2.contourArea(contours[0])
    extent = float(contourArea/rectArea)
    print(rectArea,',', contourArea,',',  extent, '\n')
    
    #-------------------------等效直径 = 2x根号(轮廓面积/pi)------------------------------
    equiDiameter = 2*np.sqrt(contourArea/np.pi)
    print(equiDiameter, '\n')
    img2 = cv2.circle(img2,(100,100), int(equiDiameter/2), (255,255,255),2)  #把等效直径的圆画出来
    
    #---------------------------方向-------------------
    ellipse1 = cv2.fitEllipse(contours[0])
    (x,y),(MA,ma),angle = cv2.fitEllipse(contours[0])    #不同形式的返回值,但实质都是一样的
    print(ellipse)
    print((x,y),(MA,ma),angle, '\n')
    img3 = img1.copy()
    img3 = cv2.ellipse(img3, ellipse1, (255,255,255),2)
    
    #可视化
    fig, axes = plt.subplots(1,4, figsize=(10,5), dpi=100) 
    axes[0].imshow(img, cmap='gray'),  axes[0].set_title('yuan tu')
    axes[1].imshow(img1, cmap='gray'), axes[1].set_title('lunkuo tu')
    axes[2].imshow(img2, cmap='gray'), axes[2].set_title('lunkuo+juxing+circle')
    axes[3].imshow(img3, cmap='gray'), axes[3].set_title('ellipse')
    plt.show()    
    (202, 107, 157, 73) , 2.1506849315068495 
    11461 , 7698.5 , 0.6717127650292296 
    99.00522529212108 
    ((276.2112731933594, 139.6067352294922), (63.01350021362305, 166.72308349609375), 82.60102844238281)
    (276.2112731933594, 139.6067352294922) (63.01350021362305, 166.72308349609375) 82.60102844238281 
    
  • 6、掩膜和像素点
    掩膜非常有用,我们可以通过轮廓制作轮廓掩膜提取轮廓里面的图像。

    (1) 用这两个函数提取轮廓点:np.nonzero()和np.transpose()
    假设有一个二维数组a,np.nonzero(a)就返回一个元组,这个元组有2个array元素,第一个array是a中非零元素的行标,第二个array是a中非零元素的列标。
    np.transpose(np.nonzero(a))就返回一个array对象,这个对象里的每个元素就是a中非零元素的行标和列标。 说明:因为cv2.findContours()返回的对象contours是轮廓点的压缩对象,所以我们先要把参数contours传入cv2.drawContours()画出轮廓图,然后在轮廓图上用np.transpose(np.nonzero(轮廓图))找到所有的轮廓点,这些轮廓点就是全部的轮廓点。

    (2) 使用idx = cv2.findNonZero(轮廓图)提取轮廓点
    返回值idx是轮廓点图上的非零元素的索引位置,每个元素是(列号,行号)的格式,并且是一个三维的array。

    #例12.16 练习np.nonzero()、np.transpose()和np.cv2.findNonZero()函数  
    import numpy as np
    a = np.zeros((5,5), np.uint8)
    for times in range(10):
        i = np.random.randint(0,5)
        j = np.random.randint(0,5)
        a[i,j]=1
    loc = np.transpose(np.nonzero(a))
    a
    np.nonzero(a)
    loc
    
    idx = cv2.findNonZero(a)
    idx
    #例12.17 练习np.nonzero()、np.transpose()和np.cv2.findNonZero()函数  
    import cv2
    import numpy as np
    import matplotlib.pyplot as plt
    img = cv2.imread(r'C:\Users\25584\Desktop\cc.bmp', 0)
    thre, binary = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)
    contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)  
    #-----------contours是一个元组,里面只有一个array元素,len(contours)返回1,len(contours[0])返回191, contours[0].shape返回(191, 1, 2)----------------
    #画出轮廓图:
    img1 = np.zeros(img.shape, np.uint8)                
    img1 = cv2.drawContours(img1, contours[0], -1, (255,255,255), 2)
    #找出轮廓图里面的非零点:
    loc = np.transpose(np.nonzero(img1)) #方法1
    #-----loc是一个array对象,loc.shape返回(689, 2), cv2.arcLength(contours[0], True)返回436.7766921520233------------
    idx = cv2.findNonZero(img1)   #方法2 
    #---idx也是一个array对象,idx.shape返回(689, 1, 2)
    
    #生成掩膜图像:
    mask1 = np.zeros(img.shape, np.uint8)
    mask1 = cv2.drawContours(mask1, contours, -1, (255,255,255), -1)
    #找出掩膜图像中的非零点:
    loc1 = np.transpose(np.nonzero(mask1)) #loc1.shape返回(7892, 2)
    idx1 = cv2.findNonZero(mask1)  #idx1.shape返回(7892, 1, 2), contourArea = cv2.contourArea(contours[0])返回7698.5
    
    #可视化
    fig, axes = plt.subplots(1,3, figsize=(6,4), dpi=100) 
    axes[0].imshow(img, cmap='gray')
    axes[1].imshow(img1, cmap='gray')
    axes[2].imshow(mask1, cmap='gray')
    plt.show()    

    【OpenCV 学习笔记】第十二章: 图像轮廓_第11张图片

  • 7、最大值和最小值及它们的位置
    min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(imgray, mask=mask)
    用于在指定的对象内查找最大值、最小值、最小值的位置、最大值的位置。imgray:单通道图像

  • 8、平均颜色及平均灰度
    mean_val = cv2.mean(img, mask=mask)用于计算一个对象的平均颜色或平均灰度。

  • 9、极点
    获取某个对象的极值点,比如最右端点、最左端点、最上端点、最下端点
    leftmost = tuple(contours[0][contours[0][:,:,0].argmin()][0])
    rightmost = tuple(contours[0][contours[0][:,:,0].argmax()][0])
    topmost = tuple(contours[0][contours[0][:,:,1].argmin()][0])
    bottommost = tuple(contours[0][contours[0][:,:,1].argmax()][0])
     

    #例12.18 计算形状特征(续)  
    import cv2
    import numpy as np
    import matplotlib.pyplot as plt
    img = cv2.imread(r'C:\Users\25584\Desktop\ct.png')
    img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    thre,binary = cv2.threshold(img_gray, 127, 255, cv2.THRESH_BINARY)
    contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)  #len(contours)=4
    
    #把原图和所有轮廓都画出来看看:
    fig, axes = plt.subplots(1,5, figsize=(10,4), dpi=100) 
    axes[0].imshow(img)
    for i in range(len(contours)):
        temp = np.zeros(img_gray.shape, np.uint8)
        temp = cv2.drawContours(temp, contours, i, (255,255,255), 2)
        axes[i+1].imshow(temp, cmap='gray')
    plt.show()    #---------------说明4个轮廓分别是字母A的外轮廓、内轮廓,和图像中的亮点的外轮廓和内轮廓-------------
    
    #使用掩膜获取我们感兴趣的区域(要原彩色图的区域):
    mask_color = np.zeros(img.shape, np.uint8)
    mask_color = cv2.drawContours(mask_color, contours, 2, (255,255,255), -1)  #制作掩膜
    img_color_roi = cv2.bitwise_and(img, mask_color)    #生成我们感兴趣的区域
    
    #使用掩膜获取我们感兴趣的区域(要灰度图的区域):
    mask_gray = np.zeros(img_gray.shape, np.uint8)
    mask_gray = cv2.drawContours(mask_gray, contours, 2, 255, -1)
    img_gray_roi = cv2.bitwise_and(img_gray, mask_gray)
    
    #---------------------在灰度图上计算最大值最小值以及它们的位置----------------------
    min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(img_gray, mask=mask_gray)
    print(min_val, max_val, min_loc, max_loc, '\n')
    mark = img_gray_roi.copy()
    mark = cv2.putText(mark, 'm', (87, 90), cv2.FONT_HERSHEY_SIMPLEX, 1, 255, 2) 
    mark = cv2.putText(mark, 'M', (88, 69), cv2.FONT_HERSHEY_SIMPLEX, 1, 255, 2) 
    
    #---------------------计算平均颜色和平均灰度----------------------有点问题!!!
    meanVal_color = cv2.mean(img, mask=mask_gray)
    meanVal_gray = cv2.mean(img_gray, mask=mask_gray)
    print(meanVal_color)
    print(meanVal_gray)
    
    #---------------------计算4个极点----------------------
    leftmost = tuple(contours[2][contours[2][:,:,0].argmin()][0])    #contours[2].shape返回(76, 1, 2)
    righmost = tuple(contours[2][contours[2][:,:,0].argmax()][0])
    topmost = tuple(contours[2][contours[2][:,:,1].argmin()][0])
    bottommost = tuple(contours[2][contours[2][:,:,1].argmax()][0])
    print(leftmost, righmost, topmost, bottommost)
    #把4个极点在原图上标注出来:
    mark1 = img.copy()
    mark1 = cv2.putText(mark1, 'l', (64, 87), cv2.FONT_HERSHEY_SIMPLEX, 1, (255,0,0), 2) 
    mark1 = cv2.putText(mark1, 'r', (117, 103), cv2.FONT_HERSHEY_SIMPLEX, 1, (255,0,0), 2) 
    mark1 = cv2.putText(mark1, 't', (92, 67), cv2.FONT_HERSHEY_SIMPLEX, 1, (255,0,0), 2) 
    mark1 = cv2.putText(mark1, 'b', (79, 113), cv2.FONT_HERSHEY_SIMPLEX, 1, (255,0,0), 2) 
    
    #可视化
    fig, axes = plt.subplots(1,5, figsize=(10,4), dpi=100) 
    axes[0].imshow(img_color_roi)
    axes[1].imshow(img_gray, cmap='gray')
    axes[2].imshow(img_gray_roi, cmap='gray')
    axes[3].imshow(mark, cmap='gray')
    axes[4].imshow(mark1)
    plt.show()    

    【OpenCV 学习笔记】第十二章: 图像轮廓_第12张图片

     

你可能感兴趣的:(opencv,计算机视觉,学习)