OpenCV-Part3——图像处理(中)

[TOC]

图像边缘检测

边缘检测

  • 边缘是图像中的重要的结构性特征,边缘往往存在于目标和背景之间、不同的区域之间,因此它可以作为图像分割的重要依据。
  • 边缘检测是检测图像中的像素点,其周围的像素值是否发生了急剧的变化,这个剧烈的变化就是不同物体的边界。
    • 边缘其实就是图像上灰度级变化很快、梯度很大的点的集合。
    • 图像的梯度可以用一阶导数和二阶偏导数来求解。
    • 边缘检测提取的是图像中不连续部分的特征,将闭合的边缘提取出来便可以作为一个区域。
  • 与区域划分相比,边缘检测不需要逐个的对像素进行比较,比较适合大图像的处理。
  • 图像数据以二/三维矩阵的形式存储的,对一幅图像的求导相当于对一个曲面求导。
    • 对图像求导、获取一幅图像的梯度:使用模板(Roberts、Prewitt、Sobel、Lapacian算子)对原图像进行卷积。
    • OpenCV 提供的梯度滤波器(高通滤波器):Sobel、Scharr、Laplacian、Canny。
    • 使用一阶导的算子有 Prewitt、Sobel、Canny;使用二阶导的有 Lapacian 。Scharr 是对 Sobel(使用小的卷积核求解求解梯度角度时)的优化。

各种算子比较

Roberts 算子

  • Roberts 算子又称为交叉微分算子,是基于交叉差分的一阶微分算子。比较简单,计算量小。
  • Roberts 常用来处理具有陡峭的低噪声图像,当图像边缘接近于正 45° 或负 45° 时,该算法处理效果更理想。其缺点是对边缘的定位不太准确,提取的边缘线条较粗。
  • 对应的模板:img

Prewitt 算子

  • Prewitt 算子也是一种一阶微分算子。由 Roberts 的 2×2 改为 3×3 模板矩阵,增加了计算量。

  • Prewitt 在水平方向和垂直方向分别利用两个方向模板与图像进行邻域卷积,边缘检测效果比 Robert 算子更加明显。

  • Prewitt 在权重上对局部像素进行了平均,对噪声有抑制作用。但是同时像素平均也代表了对图像的低通滤波,所以 Prewitt 算子对边缘的定位不如 Roberts 算子。

  • Prewitt 会造成边缘点的误判,因为许多噪声点的灰度值也很大。而且对于幅值较小的边缘点,其边缘反而丢失了。

  • 对应的模板:image-20210830155817284

Sobel 算子

  • Sobel 是结合了高斯平滑与微分求导的一阶微分算子。在 Prewitt 基础上,将权值改为符合高斯分布。

  • Sobel 考虑了不同距离的相邻点对当前像素点的影响,距离越近的像素点对应当前像素的影响越大,从而实现锐化边缘。因此,比 Prewitt 和 Roberts 都更能准确检测图像边缘。

  • Sobel 算子根据像素点上下左右邻点灰度加权差,在边缘处达到极值这一现象检测边缘,对噪声具有平滑作用,并提供较为精确的边缘方向信息。

  • 对应的模板:image-20210830155931977

1
2
3
4
img = cv2.imread('box.png', 0)
# 使用float64为了保留负数信息
sobelx = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=5)
sobely = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=5)

Laplacian 算子

  • 拉普拉斯(Laplacian)是一个二阶微分算子,是二阶 Sobel 导数,常用于图像增强和边缘提取。

  • Laplacian 原理:在卷积邻域内,如果中心像素的灰度更高,则提升中心像素的灰度;反之则降低中心像素的灰度。

    1. 模板与图像进行卷积运算:当中心像素灰度等于邻域内其他像素的平均灰度时,结果为0;当中心像素高于平均灰度时,结果为正数;当中心像素低于平均灰度时,结果为负数。
    2. 对上述卷积运算结果用适当的衰弱因子处理后,加在原中心像素上,就可以实现图像的锐化处理。
  • Laplacian 算子模板分为四邻域和八邻域,四邻域是对邻域中心像素的四方向求梯度,八邻域是对八方向求梯度。

    1. 四邻域模板:img
    2. 八邻域模板:img
  • Laplacian 用于图像增强时,有这几个比较适合的场合。

    • 由于是通过二次微分正峰和负峰之间的过零点来确定边缘线的位置,因此对孤立点或端点更为敏感,这一特性适用于以突出图像中的孤立点、孤立线或线端点为目的的场合。
    • 用来改善因扩散效应的模糊特别有效,因为它符合降制模型。扩散效应是成像过程中经常发生的现象。
  • Laplacian 用于边缘提取时,一般不使用其原始形式。它对于边缘和噪声都非常敏感,在锐化边缘的同时也会增强图像中的噪声,所以需要先对图像进行平滑处理。

    • 原因:1. Laplacian 对噪声具有无法接受的敏感性;2. 同时其幅值产生算边缘,这是复杂的分割不希望有的结果;3. 不能检测边缘的方向。
    • 取而代之,一般使用的是高斯型拉普拉斯算子(Laplacian of a Gaussian,LoG),利用该LoG算子进行卷积 等价于 高斯模糊+拉普拉斯。所以,在 LoG 中使用高斯函数的目的就是对图像进行平滑处理,使用 Laplacian 的目的是提供一幅由零交叉确定边缘位置的图像。图像的平滑处理减少了噪声的影响,并且还抵消由 Laplacian 算子的二阶导数引起的逐渐增加的噪声影响。
  • Laplacian 用于图像分割时的作用:

    • 利用它的零交叉性质进行边缘定位。
    • 确定一个像素是在一条边缘暗的一面还是亮的一面。
  • 图像锐化处理的作用是使灰度反差增强,从而使模糊图像变得更加清晰。图像模糊的实质就是图像受到平均运算或积分运算,因此可以对图像进行逆运算,如微分运算能够突出图像细节,使图像变得更为清晰。

    由于拉普拉斯是一种微分算子,它的应用可增强图像中灰度突变的区域,减弱灰度的缓慢变化区域。因此,锐化处理可选择拉普拉斯算子对原图像进行处理,产生描述灰度突变的图像,再将拉普拉斯图像与原始图像叠加。最终结果是使图像中的各灰度值得到保留、灰度突变处的对比度得到增强,在保留图像背景的前提下,突现出图像中小的细节信息锐化图像。

1
2
img = cv2.imread('box.png', 0)
laplacian = cv2.Laplacian(img, cv2.CV_64F)

img

注意事项

  • 对于使用数据类型为cv2.CV_8Unp.uint8,会有一个小问题:黑色到白色的过渡被视为正斜率(具有正值),而白色到黑色的过渡被视为负斜率(具有负值)。因此,当将数据转换为np.uint8时,所有负斜率均设为零,即错过这一边缘信息。

  • 如果要检测两个边缘,更好的选择是将输出数据类型保留为更高的形式,例如cv2.CV_16Scv2.CV_64F等,取其绝对值,然后转换回cv2.CV_8U

1
2
3
4
5
6
7
img = cv2.imread('box.png', 0)
# Output dtype = cv2.CV_8U
sobelx8u = cv2.Sobel(img, cv2.CV_8U, 1, 0, ksize=5)
# Output dtype = cv2.CV_64F. Then take its absolute and convert to cv.CV_8U
sobelx64f = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=5)
abs_sobel64f = np.absolute(sobelx64f)
sobel_8u = np.uint8(abs_sobel64f)
img

Canny边缘检测算子

基本原理

  • Canny 算子是一个具有滤波、增强、检测的多阶段的边缘检测算子。其产生的边缘很细,没有强弱之分,边缘检测性能比前面几种都要好。

  • Canny 的具体算法步骤:

    1. 用高斯滤波器平滑图像

      • 去除噪声。由于边缘检测很容易受到噪声影响,所以第一步是使用 5x5 的高斯滤波器去除噪声。
    2. 用一阶偏导的有限差分来计算并记录梯度和幅值方向

      • 对平滑后的图像使用 Sobel 算子计算水平方向和竖直方向的一阶导数(图像梯度)Gx 和 Gy 。根据得到的这两幅梯度图 Gx 和 Gy 找到边界的梯度和方向,公式如下:

        image-20210831114755186
      • 梯度的方向一般总是与边界垂直。梯度方向被归为四类:垂直,水平,和两个对角线。

    3. 对梯度幅值进行非极大值抑制

      • 在获得梯度的方向和大小之后,应该对整幅图像做一个扫描,去除那些非边界上的点。对每一个像素进行检查,看这个点的梯度是不是周围具有相同梯度方向的点中最大的。

        image-20210831115028137
      • 现在你得到的是一个包含“窄边界”的二值图像。

    4. 双阈值算法检测和连接边缘

      • 现在要确定那些边界才是真正的边界。这时我们需要设置两个阈值:minVal 和 maxVal。当图像的灰度梯度高于 maxVal 时被认为是真的边界,那些低于 minVal 的边界会被抛弃。如果介于两者之间的话,就要看这个点是否与某个被确定为真正的边界点相连,如果是就认为它也是边界点,如果不是就抛弃。

        image-20210831115538954
      • A 高于阈值 maxVal 所以是真正的边界点,C 虽然低于 maxVal 但高于minVal 并且与 A 相连,所以也被认为是真正的边界点。而 B 就会被抛弃,因为他不仅低于 maxVal 而且不与真正的边界点相连。所以选择合适的 maxVal和 minVal 对于能否得到好的结果非常重要。在这一步一些小的噪声点也会被除去,因为我们假设边界都是一些长的线段。

OpenCV中的Canny检测

  • cv2.Canny(src, threshold1, threshold2):封装了 Canny 的所有步骤。
    • threshold1, threshold2:即双阈值算法的 minVal 和 maxVal 。
    • perture_size:用于查找图像渐变的 Sobel 内核的大小。默认为3。
    • L2gradient:用于查找梯度幅度的方程式。如果为True,则使用更精确的公式,默认为False
1
edges = cv2.Canny(img, threshold1=100, threshold2=200)  # 建议放入彩色图

图像金字塔

定义

  • 图像金字塔,是同一图像、不同分辨率的图像的集合。
  • 图像金字塔,可以协助同时在不同分辨率的相同图像中进行目标检测,即同时检测不同大小的对象,因为我们不能确定对象将会以多大的尺寸显示在图像中。

构造图像金字塔

  • 构造图像金字塔一般包括二个步骤:

    1. 利用低通滤波器平滑图像
    2. 对平滑后的图像进行采样
  • 有两种采样方式:上采样(分辨率逐级升高,不会恢复细节信息)和下采样(分辨率逐级降低,会丢失细节信息)。

  • 使用函数cv2.pyrDown()cv2.pyrUp()构建图像金字塔。

    • cv2.pyrDown():从一个高分辨率大尺寸的图像向上构建一个金子塔(尺寸变小,分辨率降低)。
    • cv2.pyrUp():从一个低分辨率小尺寸的图像向下构建一个金子塔(尺寸变大,但分辨率不会增加)。
  • 下采样过后的层也称为 Octave 。

    img

高斯金字塔

  • 高斯金字塔的构造过程:
    1. 用高斯内核与图像卷积。
    2. 删除所有偶数行列。
  • 此时higher_resohigher_reso2是不一样的,因为一旦进行下采样就丢失了细节信息。
1
2
3
higher_reso = cv2.imread('messi5.jpg') 
lower_reso = cv2.pyrDown(higher_reso) # 下采样
higher_reso2 = cv.pyrUp(lower_reso) # 上采样

拉普拉斯金字塔

  • 拉普拉斯金字塔由高斯金字塔的高低层级差形成,仅为图像边缘信息。
  • 拉普拉斯金字塔可以用于图像压缩。

使用金字塔进行图像融合

  • 金字塔另一种常用的应用是图像融合。将两个不同层级或不同图像的 Octave 经过变换成相同大小并堆叠在一起,这可以使图像获得不同的特征数据(特征融合)。
  1. 加载两个图像
  2. 查找两个图像的高斯金字塔(在此示例中, 级别数为6)
  3. 在高斯金字塔中,找到其拉普拉斯金字塔
  4. 然后在每个拉普拉斯金字塔级别中加入A的左半部分和B的右半部分
  5. 最后从此联合图像金字塔中重建原始图像。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import cv2
import numpy as np

A = cv2.imread('test2.png')
B = cv2.imread('test.jpg')
# 注意:为了使后面可以逐渐减半,这里的尺寸必须为2的次幂
A = cv2.resize(A, (256, 256), interpolation=cv2.INTER_CUBIC)
B = cv2.resize(B, (256, 256), interpolation=cv2.INTER_CUBIC)

# 生成高斯金字塔
G = A.copy()
gpA = [G]
for i in range(5):
G = cv2.pyrDown(G)
gpA.append(G)

G = B.copy()
gpB = [G]
for i in range(5):
G = cv2.pyrDown(G)
gpB.append(G)

# 产生Laplacian金字塔
lpA = [gpA[5]]
for i in range(5, 0, -1):
GE = cv2.pyrUp(gpA[i])
L = cv2.subtract(gpA[i - 1], GE)
lpA.append(L)

lpB = [gpB[5]]
for i in range(5, 0, -1):
GE = cv2.pyrUp(gpB[i])
L = cv2.subtract(gpB[i - 1], GE)
lpB.append(L)

# 合并
LS = []
for la, lb in zip(lpA, lpB):
rows, cols, dpt = la.shape
ls = np.hstack((la[:, 0:cols // 2], lb[:, cols // 2:]))
LS.append(ls)

# 重新构建图像
ls_ = LS[0]
for i in range(1, 6):
ls_ = cv2.pyrUp(ls_)
ls_ = cv2.add(ls_, LS[i])

# 连接
real = np.hstack((A[:, :cols // 2], B[:, cols // 2:]))

cv2.imshow("LS", ls_)
cv2.imshow("Real", real)

cv2.waitKey(0)
cv2.destroyAllWindows()

图像轮廓

轮廓定义

  • 轮廓,是连接具有相同颜色或强度的所有连续点(沿边界)的曲线。轮廓是用于形状分析以及对象检测和识别的有用工具。
  • 为了找到轮廓,通常应用阈值或Canny边缘检测。

查找轮廓

  • findContours(image, mode, method):从黑色背景中找到白色物体的轮廓。
    • image:仅接受二值图。
    • mode:轮廓检索模式。
      • cv2.RETR_EXTERNAL:表示只检测外轮廓。
      • cv2.RETR_LIST:检测的轮廓不建立等级关系。
      • cv2.RETR_CCOMP:建立两个等级的轮廓,上面的一层为外边界,里面的一层为内孔的边界信息。如果内孔内还有一个连通物体,这个物体的边界也在顶层。
      • cv2.RETR_TREE:建立一个等级树结构的轮廓。
    • method:轮廓近似方法。
      • cv2.CHAIN_APPROX_NONE:存储所有的轮廓点,相邻的两个点的像素位置差不超过1。
      • cv2.CHAIN_APPROX_SIMPLE:压缩水平、垂直、对角线方向的元素,只保留该方向的终点坐标,例如一个矩形轮廓只需4个点来保存轮廓信息
      • cv2.CHAIN_APPROX_TC89_L1/CV_CHAIN_APPROX_TC89_KCOS:使用 teh-Chinl chain 近似算法
    • contours:返回的第一个值,图像中所有轮廓组成的list。
      • 每个轮廓的类型为ndarray,本质是轮廓上的点的集合。
    • hierarchy:返回的第二个可选值,是一个ndarray,其元素个数和轮廓个数相同。
      • 每个轮廓contours[i]对应4个轮廓层级属性hierarchy[i][0] ~ hierarchy[i][3],分别表示后一个轮廓、前一个轮廓、父轮廓、内嵌轮廓的索引。如果没有对应项,则该值为负数。
  • 在OpenCV中,findContours()的版本区别:
    • OpenCV3.2 之前,函数会修改源图像。OpenCV3.2 之后,不再修改源图像。
    • OpenCV2 返回两个值:contourshierarchy。OpenCV3 返回三个值:imgcountourshierarchy。这里可以用 try-except 解决版本兼容问题。
1
2
3
4
5
6
7
im = cv2.imread('test.jpg')
imgray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(imgray, 127, 255, 0) # 转二值
try:
img, contours, hierarchy = cv2.findContours(thresh, mode=cv2.RETR_EXTERNAL, method=cv2.CHAIN_APPROX_SIMPLE)
except:
contours, hierarchy = cv2.findContours(thresh, mode=cv2.RETR_EXTERNAL, method=cv2.CHAIN_APPROX_SIMPLE)

绘制轮廓

  • cv2.drawContours(image, contours, contourIdx, color, thickness, lineType, hierarchy, maxLevel, offset):绘制任何形状的轮廓。

    • image:指明在哪幅图像上绘制轮廓。
    • contours:轮廓集合。
    • contourIdx:指定绘制轮廓集合中的哪条轮廓,如果是-1,则绘制其中的所有轮廓。
    • thickness:表明轮廓线的宽度,如果是-1(cv2.FILLED),则为填充。
  • 在图像中绘制所有轮廓:

    1
    cv2.drawContours(img, contours, -1, (0, 255, 0), 3)
  • 绘制单个轮廓,如第四个轮廓:

    1
    2
    3
    4
    cv2.drawContours(img, contours, 3, (0, 255, 0), 3)
    # 更好用的等价方法
    cnt = contours[4]
    cv2.drawContours(img, [cnt], 0, (0,255,0), 3)
  • 绘制面积最大的轮廓:

    1
    2
    3
    4
    5
    6
    7
    # 找到最大的轮廓
    area = []
    for k in range(len(contours)):
    area.append(cv2.contourArea(contours[k]))
    max_idx = np.argmax(np.array(area))
    cnt = contours[max_idx]
    cv2.drawContours(img, [cnt], 0, (0,255,0), 3)

轮廓特征

特征矩

  • 特征矩可以帮助计算目标的特征,例如物体的质心、面积等。
  • cv2.moments():提供所有计算出的矩值字典。
1
2
3
4
5
6
7
img = cv2.imread('test.jpg', 0)
thresh = cv2.Canny(img, 100, 200)
image, contours, hierarchy = cv2.findContours(thresh, 1, 2)
cnt = contours[0]
M = cv2.moments(cnt)
print(M)
# {'m00': 6.0, 'm10': 3258.0, 'm01': 3474.0, 'm20': 1769095.6666666665, 'm11': 1886382.0, 'm02': 2011451.0, 'm30': 960620757.0, 'm21': 1024306391.0, 'm12': 1092217893.0, 'm03': 1164635919.0, 'mu20': 1.6666666665114462, 'mu11': 0.0, 'mu02': 5.0, 'mu30': 2.384185791015625e-07, 'mu21': 8.987262845039368e-08, 'mu12': 0.0, 'mu03': 0.0, 'nu20': 0.04629629629198462, 'nu11': 0.0, 'nu02': 0.1388888888888889, 'nu30': 2.7037215925843386e-09, 'nu21': 1.0191763034546432e-09, 'nu12': 0.0, 'nu03': 0.0}
  • 计算质心:
1
2
3
4
cx = int(M['m10'] / M['m00'])
cy = int(M['m01'] / M['m00'])
print(cx, cy)
# 543 579

轮廓面积

  • cv2.contourArea(curve):轮廓面积,等价于矩M['m00']
1
area = cv2.contourArea(cnt) 

轮廓周长

  • cv2.arcLength(curve, closed):轮廓弧长。
    • closedTrue指定形状是闭合轮廓,False为曲线。
1
perimeter = cv2.arcLength(cnt, closed=True)

轮廓凹凸状况

轮廓近似

  • 例如试图在图像中找到一个正方形,但是由于图像问题,没能得到一个完美的正方形,则可以近似形状。
  • cv2.approxPolyDP(curve, epsilon, closed):根据指定的精度,将轮廓形状近似为顶点数量较少的其他形状。由Douglas-Peucker算法实现。
    • epsilon:是一个精度参数,表示从轮廓到近似轮廓的最大距离。需要正确选择epsilon才能获得正确的输出。
    • closed:指定曲线是否闭合。
1
2
epsilon = 0.3 * cv2.arcLength(cnt, True)
approx = cv2.approxPolyDP(cnt, epsilon, True)
  • 在第二张图片中,绿线显示了ε=周长×10%时的近似曲线。第三张图中,显示了ε=周长×1%时的情况。

    img

轮廓凸包

  • 凸包与轮廓逼近相似,在某些情况下两者可能提供相同的结果。

  • cv2.convexHull(points, hull, clockwise, returnPoints):检查曲线是否存在凸凹缺陷并进行校正。

    • points:传入轮廓点集。
    • hull:输出,通常不需要。
    • clockwise:方向标志。True表示输出的凸包是顺时针方向的,否则为逆时针。
    • returnPoints:默认值为True。返回凸包点的(x, y)坐标。如果设置为 False,则返回凸包点在轮廓中相应的索引。
1
hull = cv2.convexHull(cnt) 

检查凸度

  • cv2.isContourConvex(point):检查曲线是否凸出。只返回True或False。
1
k = cv2.isContourConvex(cnt) 

凸性缺陷

  • 凸包的任何偏差都可以被认为是凸性缺陷。
  • cv2.convexityDefects():查找凸性缺陷。
    • 在寻找凸包时,对cv2.convexHull(points, returnPoints)必须传递returnPoints=False
    • 返回一个数组,其中每行包含这些值:**[起点、终点、最远点、到最远点的近似距离]**。
    • 该函数返回的前三个值是cnt的索引。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import cv2

img = cv2.imread('test.jpg')
thresh = cv2.Canny(img, 100, 200)
_, contours, hierarchy = cv2.findContours(thresh, 2, 1)
cnt = contours[0]

# 查找凸性缺陷
hull = cv2.convexHull(cnt, returnPoints=False)
defects = cv2.convexityDefects(cnt, hull)

for i in range(defects.shape[0]):
s, e, f, d = defects[i, 0]
start = tuple(cnt[s][0])
end = tuple(cnt[e][0])
far = tuple(cnt[f][0])
cv2.line(img, start, end, [0, 255, 0], 2)
cv2.circle(img, far, 5, [0, 0, 255], -1)
cv2.imshow('img', img)
cv2.waitKey(0)
  • img

点多边形测试

  • cv2.pointPolygonTest(contour, pt, measureDist):计算图像中某一点到轮廓线的最短距离。
    • measureDist:如果不想找到距离,则设置为False,因为设置为False可使速度提高2-3倍。
      • True:计算有符号距离。点在轮廓线外时为负数,点在轮廓线内时为正数,点在轮廓线上时为零。
      • False:判断该点是在轮廓线外部还是内部。点在轮廓线外时为-1,点在轮廓线内时为+1,点在轮廓线上时为0
1
dist = cv2.pointPolygonTest(contour=cnt, pt=(50, 50), measureDist=True)  # 检查(50, 50)到轮廓线的最短距离

形状匹配

  • cv2.matchShapes():比较两个形状或两个轮廓,返回一个显示相似性的度量。结果越低,匹配越好。
1
2
3
4
5
6
7
8
9
10
11
12
13
import cv2

img1 = cv2.imread('star.jpg', 0)
img2 = cv2.imread('star2.jpg', 0)
ret, thresh = cv2.threshold(img1, 127, 255, 0)
ret, thresh2 = cv2.threshold(img2, 127, 255, 0)
contours, hierarchy = cv2.findContours(thresh, 2, 1)
cnt1 = contours[0]
contours, hierarchy = cv2.findContours(thresh2, 2, 1)
cnt2 = contours[0]

ret = cv2.matchShapes(cnt1, cnt2, 1, 0.0)
print(ret)
  • 在以下案例中:匹配图像A与本身=0.0;匹配图像A与图像B=0.001946;匹配图像A与图像C=0.326911。

    img

  • 即使是图像旋转也不会对这个比较产生很大的影响。

拟合轮廓

直角矩形

  • cv2.boundingRect(points):不考虑物体旋转,拟合最小矩形框。注意,其面积不是最小的。
    • points:目标轮廓的点集。
    • (x,y)为矩形的左上角坐标,而(w,h)为矩形的宽度和高度。
1
2
x, y, w, h = cv2.boundingRect(cnt)
cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2)

旋转矩形

  • cv2.minAreaRect(point):考虑旋转,拟合面积最小的外接矩形。
    • point:目标轮廓的点集。
    • 返回一个Box2D结构:(中心坐标(x, y), (宽, 高), 旋转角度)
  • cv2.boxPoints():(中心坐标(x, y), (宽, 高), 旋转角度) -> [[x, y] * 4]
    • 注意版本区别:OpenCV2中为cv2.cv.BoxPoints,OpenCV3中为cv2.boxPoints()。同样可以用 try-except 兼容版本问题。
1
2
3
4
5
rect = cv2.minAreaRect(cnt)
box = cv2.boxPoints(rect)
box = np.int0(box)
cv2.drawContours(img, [box], contourIdx=0, color=(0, 0, 255), thickness=2) # 画(拟合后矩形的)轮廓
cv2.polylines(img, [box], isClosed=True, color=(0, 255, 255), thickness=2) # 画多边形
  • image-20210831214555327

最小闭合圈

  • cv2.minEnclosingCircle(point):拟合最小外接圆。
1
2
3
4
(x, y), radius = cv2.minEnclosingCircle(cnt)
center = (int(x), int(y))
radius = int(radius)
cv2.circle(img, center, radius, (0, 255, 0), 2)

拟合椭圆

  • cv2.fitEllipse(point):内接椭圆的旋转矩形。
  • 很多时候效果并不是非常理想,并没有做到外接。
  • 需要5个点以上才能拟合椭圆,否则只能用圆形。
1
2
3
4
ellipse = cv2.fitEllipse(cnt)
(x, y), (w, h), t = ellipse
e_x, e_y, e_a, e_b, e_t = int(x), int(y), int(w / 2), int(h / 2), int(t)
cv2.ellipse(img, (e_x, e_y), (e_a, e_b), e_t, 0, 360, (255, 0, 0), 5)
  • image-20210831221246120

拟合直线

  • cv2.fitLine(points, distType, param, reps, aeps):在一组点集上近似一条直线。
  • 也没啥用。
1
2
3
4
5
rows, cols = img.shape[:2]
[vx, vy, x, y] = cv2.fitLine(cnt, cv2.DIST_L2, 0, 0.01, 0.01)
lefty = int((-x * vy / vx) + y)
righty = int(((cols - x) * vy / vx) + y)
cv2.line(img, (cols - 1, righty), (0, lefty), (0, 255, 0), 2)

轮廓性质

长宽比

  • 长宽比:对象边界矩形的宽高比。
1
2
x, y, w, h = cv2.boundingRect(cnt)
aspect_ratio = float(w) / h

范围

  • 范围:轮廓区域与边界矩形区域的比值。
1
2
3
4
area = cv2.contourArea(cnt)
x, y, w, h = cv2.boundingRect(cnt)
rect_area = w * h
extent = float(area) / rect_area

坚实度

  • 坚实度:等高线面积与其凸包面积之比。
1
2
3
4
area = cv2.contourArea(cnt)
hull = cv2.convexHull(cnt)
hull_area = cv2.contourArea(hull)
solidity = float(area) / hull_area

等效直径

  • 等效直径:面积与轮廓面积相同的圆的直径。
1
2
area = cv2.contourArea(cnt)
equi_diameter = np.sqrt(4 * area / np.pi)

取向

  • 取向:物体指向的角度。
1
(x, y), (MA, ma), angle = cv2.fitEllipse(cnt)

掩码

  • 掩码:构成该对象的所有点。
  • Numpy给出的坐标是(行、列)格式,而OpenCV给出的坐标是(x,y)格式。注意,row = x, column = y
1
2
3
4
mask = np.zeros(imgray.shape, np.uint8)
cv2.drawContours(mask, [cnt], 0, 255, -1)
pixelpoints = np.transpose(np.nonzero(mask))
# pixelpoints = cv2.findNonZero(mask) # 等价方法

最大值、最小值及其位置

  • 可以使用掩码图像找到这些参数。
1
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(imgray, mask=mask)

平均颜色、平均强度

  • 找到对象的平均颜色,或者灰度模式下物体的平均强度。
1
mean_val = cv2.mean(img, mask=mask)

极端点

  • 极端点:指对象的最顶部,最底部,最右侧和最左侧的点。
1
2
3
4
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])