OpenCV Tutorials 05 - 形态学操作和图像梯度

1,019 阅读7分钟

形态学操作和图像梯度

一、形态学操作

主要有膨胀、腐蚀、开(先腐蚀后膨胀)、闭(线膨胀后腐蚀)操作。用到的函数有:cv.erode(), cv.dilate(), cv.morphologyEx()

用法如下:

  1. cv.erode( src, kernel[, dst[, anchor[, iterations[, borderType[, borderValue]]]]] ) -> dst
  2. cv.dilate( src, kernel[, dst[, anchor[, iterations[, borderType[, borderValue]]]]] ) -> dst
  3. cv.morphologyEx( src, op, kernel[, dst[, anchor[, iterations[, borderType[, borderValue]]]]] ) -> dst

二、概念

形态学变换是基于图像形状的一些简单操作。它通常在二值图像上执行。它需要两个输入,一个是我们的原始图像,第二个是所谓的结构元素或核心,这决定了操作的性质。两个基本的形态学算子是腐蚀和膨胀。然后它的变体形式,如开,闭操作,计算梯度等。

1. 腐蚀操作

腐蚀的基本概念就像土壤侵蚀一样,它侵蚀了前景物体的边界(一般将前景设置为白色,背景设置为黑色)。内核滑过图像(就像2D 卷积)。只有当内核下的所有像素都为1(前景)时,原始图像的扫描内核中心的一个像素(1或0)才被认为是1,否则它就会被侵蚀(变为零)。

因此,所有接近边界的像素将被丢弃。因此,前景物体的边缘被腐蚀,或者仅仅是图像中的白色区域减小。它对去除小的白噪声 ,分离两个连接的对象等很有用。

import cv2 as cv
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
%matplotlib inline
def cv_show(name, img):
    cv.imshow(name, img)
    cv.waitKey(0)
    cv.destroyAllWindows()
def compare(imgs):
  #  for i in range(len(imgs)):
 #       imgs[i][:,-3:-1,:] = [255,255,255]
    res = np.hstack(imgs)
    cv_show('Compare', res)
img = cv.imread('fur.png')

# 膨胀和腐蚀都需要自定义内核
kernel = np.ones((3, 3), np.uint8)
e = cv.erode(img,kernel)

compare([img, e])

腐蚀操作.PNG

可以看出一些毛毛刺刺被去除,但是原文本也被侵蚀了一部分

2. 膨胀操作

与腐蚀操作效果相反。如果内核下只要有一个像素是“1”,那么内核中心像素元素就是“1”。因此,它增加了图像中的白色区域或前景物体的大小增加。通常情况下,在去除噪音的情况下,腐蚀之后是膨胀(也就是开操作)。因为,侵蚀除去了白色的噪音,但它也缩小了我们的对象。所以我们扩张它。由于噪音消失了,它们不会再回来,但是我们的目标区域增大了。它也可以用来连接一个物体的破损部分。

img = cv.imread('fur.png')

# 膨胀和腐蚀都需要自定义内核
kernel = np.ones((3, 3), np.uint8)
e = cv.erode(img,kernel)

d_1 = cv.dilate(img, kernel)
d_2 = cv.dilate(e,kernel)

compare([e, d_1, d_2])

膨胀操作.PNG

3. 开操作

开操作只是先侵蚀后膨胀合并的一个名称。上面的例子表明,它在消除噪音方面很有用。这里我们使用函数cv.morphologyEx()

cv.morphologyEx函数的第二个参数一共有五种取值:

  1. cv.MORPH_OPEN : 开操作,先腐蚀后膨胀
  2. cv.MORPH_ClOSE:闭操作,先膨胀后腐蚀
  3. cv.MORPH_GRADIENT: 提取边界,利用膨胀的结果减去腐蚀的结果
  4. cv.MORPH_TOPHAT: 输入图像减去开操作结果。
  5. cv.MORPH_BLACKHAT:输入图像减去闭操作结果
img = cv.imread('fur.png')

# 第二个参数是形态学操作的取值
kernel = np.ones((3, 3), np.uint8)
res = cv.morphologyEx(img, cv.MORPH_OPEN,kernel )

compare([img, res])

开操作.PNG

4. 闭操作

闭操作是开操作的对立操作,闭操作先膨胀后腐蚀。它在修补前景物体内部的小洞或物体上的小黑点时很有用。

img = cv.imread('fur.png')

kernel = np.ones((3,3), np.uint8)
closing = cv.morphologyEx(img, cv.MORPH_CLOSE, kernel)

compare([img, closing])

闭操作.PNG

5. 求梯度

这是一个图像的膨胀操作减去腐蚀操作所得结果,结果看起来就像物体的轮廓。

img = cv.imread('fur.png')

kernel = np.ones((3,3), np.uint8)
gradient = cv.morphologyEx(img, cv.MORPH_GRADIENT, kernel)

compare([img, gradient])

形态学梯度.PNG

6. 礼帽操作

输入图像减去开操作所得图像。

img = cv.imread('fur.png')

kernel = np.ones((3,3), np.uint8)
tophat = cv.morphologyEx(img, cv.MORPH_TOPHAT, kernel)

compare([img, tophat])

礼帽.PNG

7. 黑帽操作

输入图像减去关操作所的结果

img = cv.imread('fur.png')

kernel = np.ones((3,3), np.uint8)
blackhat = cv.morphologyEx(img, cv.MORPH_BLACKHAT, kernel)

compare([img, blackhat])

黑帽.PNG

8. 构建元素

在前面的示例中,我们借助 Numpy 手动创建了一个结构元素。它是长方形的。但在某些情况下,您可能需要椭圆形或圆形的内核。因此,为此,OpenCV 有一个函数 cv.getStructuringElement ()。您只需传递内核的形状和大小,就可以得到所需的内核。

  • 内核形态有如下选项:
  1. cv.MORPH_RECT
  2. cv.MORPH_ELLIPSE
  3. cv.MORPH_CROSS
# Rectangular Kernel
cv.getStructuringElement(cv.MORPH_RECT,(5,5))
array([[1, 1, 1, 1, 1],
       [1, 1, 1, 1, 1],
       [1, 1, 1, 1, 1],
       [1, 1, 1, 1, 1],
       [1, 1, 1, 1, 1]], dtype=uint8)
# Elliptical Kernel 椭圆内核
cv.getStructuringElement(cv.MORPH_ELLIPSE,(5,5))
array([[0, 0, 1, 0, 0],
       [1, 1, 1, 1, 1],
       [1, 1, 1, 1, 1],
       [1, 1, 1, 1, 1],
       [0, 0, 1, 0, 0]], dtype=uint8)
# Cross-shaped Kernel
cv.getStructuringElement(cv.MORPH_CROSS,(5,5))
array([[0, 0, 1, 0, 0],
       [0, 0, 1, 0, 0],
       [1, 1, 1, 1, 1],
       [0, 0, 1, 0, 0],
       [0, 0, 1, 0, 0]], dtype=uint8)
# 效果对比
img = cv.imread('fur.png')

kernel_cross = cv.getStructuringElement(cv.MORPH_CROSS,(3,3))
kernel_rect = cv.getStructuringElement(cv.MORPH_RECT,(3,3))

cross = cv.morphologyEx(img, cv.MORPH_OPEN, kernel_cross)
rect = cv.morphologyEx(img, cv.MORPH_OPEN, kernel_rect)

compare([img, cross, rect])

自定义内核.PNG

可以看出,自定义的cross内核仍留下了以下白色斑点,而rect内核得到了较为清晰的效果

三、图像梯度

本节包括对图像梯度和边缘的一些介绍,主要由以下函数 cv.Sobel(), cv.Scharr(), cv.Laplacian()。

1. 概念

OpenCV 提供三种类型的梯度过滤器或高通过滤器:Sobel、Scharr和Laplacian,下面进行详细介绍

  • 三者用法如下:
  1. cv.Sobel( src, ddepth, dx, dy[, dst[, ksize[, scale[, delta[, borderType]]]]] ) -> dst
  2. cv.Scharr( src, ddepth, dx, dy[, dst[, scale[, delta[, borderType]]]] ) -> dst
  3. cv.Laplacian( src, ddepth[, dst[, ksize[, scale[, delta[, borderType]]]]] ) -> dst

其中的ddepth代表了结果图像的通道数,-1则和输入图像保持一致。dx、dy分别代表了对x、y的导数阶数。

2. Sobel和Scharr概念

Sobel算子是联合高斯平滑加微分运算,所以抗噪声能力更强。 您可以指定要采用的导数方向,垂直或水平(分别通过参数 yorder 和 xorder)。 您还可以通过参数 ksize 指定内核的大小。 如果 ksize = -1,则使用 3x3 Scharr 过滤器的得到的结果要比 3 x 3 Sobel过滤器结果更好。

  1. 三阶Sobel梯度矩阵:Gx=[10+120+210+1]A and Gy=[121000+1+2+1]A\mathbf{G}_{x}=\left[\begin{array}{ccc} -1 & 0 & +1 \\ -2 & 0 & +2 \\ -1 & 0 & +1 \end{array}\right] * \mathbf{A} \quad \text { and } \quad \mathbf{G}_{y}=\left[\begin{array}{ccc} -1 & -2 & -1 \\ 0 & 0 & 0 \\ +1 & +2 & +1 \end{array}\right] * \mathbf{A} 计算得出负数结果或者超过了K比特上限时会进行截断:取为0或(2^k - 1),所以一般我们会分别求出对x的导数(使用cv.convertScaleAbs取绝对值),在求对y的导数(取绝对值)后,使用cv.addWeighted()将两者相加

  2. 三阶Scharr梯度矩阵:Gx=[30310010303]Gy=[31030003103]G_{x}=\left[\begin{array}{ccc} -3 & 0 & 3 \\ -10 & 0 & 10 \\ -3 & 0 & 3 \end{array}\right] \\ G_{y}=\left[\begin{array}{ccc} -3 & -10 & -3 \\ 0 & 0 & 0 \\ 3 & 10 & 3 \end{array}\right] 计算方式同上

3. Laplacian 概念

它计算由关系Δsr=2srcx2+2srcy2\Delta sr =\frac{\partial^{2}src}{\partial x^{2}}+\frac{\partial^{2} src}{\partial y^{2}}给出的图像的拉普拉斯算子,其中每个导数都是使用 Sobel 导数找到的。 如果 ksize = 1,则使用以下内核进行过滤:kernel=[010141010]kernel = \begin{bmatrix} 0 & 1 &0 \\ 1 & -4 &1 \\ 0 &1 &0 \end{bmatrix}

img = cv.imread('chessboard.jpg')
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
laplacian = cv.Laplacian(gray,cv.CV_64F)
sobelx = cv.Sobel(gray,cv.CV_64F,1,0,ksize=5)
sobely = cv.Sobel(gray,cv.CV_64F,0,1,ksize=5)

compare([ sobelx, sobely, laplacian])

三种算子的比较.PNG

4. 一个重要示例

在上一个示例中,输出数据类型为 cv_8U 或 np.uint8。但是有一个小小的问题。黑白过渡视为正斜率(具有正值) ,白白过渡视为负斜率(具有负值)。所以当你把数据转换成 np.uint8时,所有的负斜率都变成了零。简单地说,你错过了这个边缘。

如果你想检测两个边缘,更好的选择是将输出数据类型保持为更高的形式,如 cv.CV_16S、cv.CV_64F 等,取其绝对值,然后转换回 cv.CV_8U。 下面的代码演示了水平 Sobel 滤波器的此过程和结果差异。

img = cv.imread('round.png',0)
# 导致负值归0
sobelx8u = cv.Sobel(img,cv.CV_8U,1,0,ksize=3)
# Output dtype = cv.CV_64F. Then take its absolute and convert to cv.CV_8U
sobelx64f = cv.Sobel(img,cv.CV_64F,1,0,ksize=3)
abs_sobel64f = np.absolute(sobelx64f)
sobel_8u = np.uint8(abs_sobel64f)

plt.subplot(1,3,1),plt.imshow(img,cmap = 'gray')
plt.title('Original'), plt.xticks([]), plt.yticks([])
plt.subplot(1,3,2),plt.imshow(sobelx8u,cmap = 'gray')
plt.title('Sobel CV_8U'), plt.xticks([]), plt.yticks([])
plt.subplot(1,3,3),plt.imshow(sobel_8u,cmap = 'gray')
plt.title('Sobel abs(CV_64F)'), plt.xticks([]), plt.yticks([])
plt.show()

output_64_0.png

5. Sobel算子的绝对值转化示例

首先我们直接使用Sobel算子计算梯度时,由于sobel算子的计算方向是左减右,下减上。故有时候会造成负值现象,当传入的ddepth为 cv.CV_64F时,会对图像进行截断,导致负数显示不出来,所以我们进行绝对值转换。

# 计算梯度和阈值处理时,最好输入灰度图像
img = cv.imread('round.png',0)

sobel_x = cv.Sobel(img, cv.CV_64F, 1,0 ,ksize = 3)
sobel_X = cv.convertScaleAbs(sobel_x)
compare([img, sobel_x, sobel_X])

Sobel绝对值演示.PNG

# 同理我们可以得到y方向的绝对值
sobel_y = cv.Sobel(img, cv.CV_64F, 0, 1,ksize = 3)
sobel_Y = cv.convertScaleAbs(sobel_y)
compare([img, sobel_y, sobel_Y])

y方向绝对值.PNG

# 如果直接出入 dx =1 ,dy =1 则计算出来的结果不如分别计算后相加来得明了
Sobel_xy = cv.convertScaleAbs(cv.Sobel(img, cv.CV_64F, 1, 1 , ksize = 3))
Sobel_XY = cv.addWeighted(sobel_X, 0.5, sobel_Y, 0.5, 0)
compare([Sobel_xy, Sobel_XY])

xy一起和分开的区别.PNG

显然,分别计算后再相加所得的边缘没有重影且更加明显