(三)OpenCV中的图像处理之形态转换和图像梯度
注释:本文翻译自OpenCV3.0.0 document->OpenCV-Python Tutorials,包括对原文档种错误代码的纠正
3.5 形态转换
3.5.1 目标:
- 学会不同的形态学操作,如:腐蚀、膨胀、开运算、闭运算等
- 学会这些函数:cv2.erode()、cv2.dilate()、cv2.morphologyEx()
3.5.2 原理
形态学操作是指一些基于图像形状的基本操作。它通常在灰度图像上执行。该操作需要两个输入,一个是二值图像,另一个是结构元素(内核),它决定了操作的性质。两个最基本的形态学操作是腐蚀和膨胀,它们的变体是开运算、闭运算、渐变等。将以下面的图像为例逐个看到每个操作的结果。
3.5.3 腐蚀
腐蚀的基本思想就像土壤侵蚀,它侵蚀了前景物体的边缘(总是尽量保持前景物体白色)。那么它有什么作用呢?内核滑过图像,只有当内核下的所有像素都为1时,才能使原始图像中(1或0)中的像素置为1,否则将被侵蚀(置为0).
所以情况是,边界附近的的所有像素将被丢弃,这取决于内核的大小,因此前景物体的厚度或尺寸将会变小。有助于分离两个连接的对象。
erosion = cv2.erode(img,kernel,iterations =1)
3.5.4 膨胀
正好与侵蚀相反,如果内核下至少有一个元素为“1”,那么像素元素置为“1”。它增加了前景对象的大小。通常,在噪声消除的情况下,腐蚀操作后是膨胀运算,因为腐蚀消除了白色的噪音,缩小了我们的对象,所以用膨胀扩大它,而噪声已经被消除了便不会再回来,但我们的对象面积增加了。该操作可以连接物体的断链部分。
dilation = cv2.dilate(img,kernel,iterations = 1)
3.5.5 开运算
先腐蚀后膨胀,有利于去除噪声,cv2.morphologyEx()
opening = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
3.5.6 闭运算
先膨胀后腐蚀,有助于去除前景物体上的小斑点或噪音。
closing = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)
3.5.7 形态学梯度
这是被腐蚀图像和被膨胀图像的区别。使结果看起来像找到对象的轮廓。
gradient = cv2.morphologyEx(img, cv2.MORPH_GRADIENT, kernel)
3.5.8 顶帽
这是输入图像和执行了开运算图像的区别。
tophat = cv2.morphologyEx(img, cv2.MORPH_TOPHAT, kernel)
3.5.9 黑帽
这是执行了闭运算的图像和输入图像之间的区别。
blackhat = cv2.morphologyEx(img, cv2.MORPH_BLACKHAT, kernel)
3.5.10 结构元素
在Numpy的帮助下,我们在前面的栗子中手动创建了一个矩形的结构化元素,但在某些情况下,我们可能需要圆形/椭圆的结构化元素(内核)。OpenCV中有一个函数cv2.getStructingElement(),只需要传入内核的形状和大小,就可以得到想要的内核。
# Rectangular Kernel
>>> cv2.getStructuringElement(cv2.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
>>> cv2.getStructuringElement(cv2.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
>>> cv2.getStructuringElement(cv2.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)
下面的栗子结合了腐蚀、膨胀、开运算、闭运算、形态学梯度
# -*- coding: utf-8 -*-
'''
形态转换:腐蚀、膨胀、开运算、闭运算
1.形态转换需要两个输入,一个是二值图像(灰度图像),另一个是内核;
2.两个最基本的形态学操作是腐蚀和膨胀,它们的变体是开运算、闭运算、渐变等
以下栗子结合腐蚀、膨胀、开运算、闭运算,形态学梯度
'''
import cv2
import numpy as np
from matplotlib import pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
img = cv2.imread('14.png')
# 构造5*5的内核
kernel = np.ones((5, 5), np.uint8)
# 腐蚀:cv2.erode()
erosion = cv2.erode(img, kernel, iterations=1)
# 膨胀:cv2.dilate()
dilation = cv2.dilate(img, kernel, iterations=1)
# 开运算:先腐蚀膨胀,有助于去除噪声 cv2.morphologyEx()
opening = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
# 闭运算:先膨胀后腐蚀,有助于去除前景物体上的小斑点或噪声
closing = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)
# 形态学梯度:这是被腐蚀和被膨胀图像的区别,看起来更像找到轮廓的对象
gradient = cv2.morphologyEx(img, cv2.MORPH_GRADIENT, kernel)
# 顶帽:这是输入图像和执行了开运算图像的区别
tophat = cv2.morphologyEx(img, cv2.MORPH_TOPHAT, kernel)
# 黑帽:这是执行了闭运算和输入图像之间的区别
blackhat = cv2.morphologyEx(img, cv2.MORPH_BLACKHAT, kernel)
titles = ["原图", "腐蚀", "膨胀", "开运算", "闭运算", "形态学梯度", "顶帽", "黑帽"]
images = [img, erosion, dilation, opening, closing, gradient, tophat, blackhat]
for i in range(8):
plt.subplot(2, 4, (i + 1)), plt.imshow(images[i]), plt.title(titles[i])
plt.xticks([]), plt.yticks([])
plt.show()
结果如下:
3.6 图像梯度
3.6.1 目标:
- 找到图像的梯度、边缘等
- 学会这些函数:cv2.Sobel()、cv2.Scharr()、cv2.Laplacian()
3.6.2 原理
OpenCV提供了三种类型的梯度滤波器/高通滤波器,Sobel、Scharr和laplacian,这里会一一介绍。
1.Sobel导数和Scharr导数
Sobel操作数是高斯平滑加分散操作的联合,因此更能抵抗噪音。 您可以指定要采取的导数的方向,垂直方向或水平方向(分别为参数yorder和xorder)。 您还可以通过参数ksize指定内核的大小。 如果ksize = -1,则使用3x3 Scharr滤波器,该滤波器比3x3 Sobel滤波器有更好的结果。请参阅使用的内核文档。
2.拉普拉斯导数
它计算由关系式 的拉普拉斯算子,该表达式中的导数都是Scharr导数,如果kszie=-1,那么下面的内核会用于滤波:
以下代码显示了在一张图上的所有操作,所有内核大小为5*5,输出图像的深度被置为-1,结果图的类型是np.unit8.
# -*- coding: utf-8 -*-
'''
图像梯度:OpenCV提供了三种类型的梯度滤波器/高通滤波器,Sobel、Scharr和laplacian
'''
import cv2
import numpy as np
from matplotlib import pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
img = cv2.imread('sudoku.png',0)
laplacian = cv2.Laplacian(img, cv2.CV_64F)
sobelX = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=5)
sobelY = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=5)
plt.subplot(2, 2, 1), plt.imshow(img, cmap='gray')
plt.title("Original"), plt.xticks([]), plt.yticks([])
plt.subplot(2, 2, 2), plt.imshow(laplacian, cmap='gray')
plt.title("Laplacian"), plt.xticks([]), plt.yticks([])
plt.subplot(2, 2, 3), plt.imshow(sobelX, cmap='gray')
plt.title("sobelX"), plt.xticks([]), plt.yticks([])
plt.subplot(2, 2, 4), plt.imshow(sobelY, cmap='gray')
plt.title("sobelY"), plt.xticks([]), plt.yticks([])
plt.show()
结果如下:
3.6.3 一件重要的事
在我们的最后一个栗子中,输出数据类型是cv2.CV_8U或np.unit8.但是有一个小问题,做黑白转换时具有正斜率(它具有正值),而做白黑转换时具有负斜率(它具有负值)。所以当你将数据转换为np.uint8时,所有的负斜率均为零。简单来说,你错过了这个边缘。
如果要检测到这两个边,更好的选择是将输出数据类型保持为一些较高的形式,如cv2.CV_16S,cv2.CV_64F等,取其绝对值,然后转换回cv2.CV_8U。下面的代码演示了水平Sobel滤波器的这个过程以及结果的差异。
代码如下:
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('sudoku.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 cv2.CV_8U
sobelx64f = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=5)
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()
结果: