4 Smoothing Images
4.1 目标
学习:
- 使用各种低通滤波器(low pass filters)模糊(Blur)图像
- 对图像使用自定义滤波器(2D convolutin)
4.2 2D卷积(图像滤波)(Image Filtering)
对于一维信号,图像也可以用各种低通滤波器(LPF)、高通滤波器(HPF)等进行滤波。LPF有助于去除噪声或模糊图像。 HPF过滤器有助于查找图像中的边缘。
OpenCV提供了一个函数cv2.filter2D(),用于将内核与图像进行卷积。例如,我们将尝试对图像使用平均滤波器。 5x5平均滤波器内核可以定义如下:
\[ K = \frac{1}{25} \left[ \begin{matrix} 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 \end{matrix} \right]\]
使用上述内核进行滤波会导致执行以下操作:对于每个像素,5x5窗口以此像素为中心,将落在此窗口内的所有像素相加,然后将结果除以25。这相当于计算该窗口内像素值的平均值。对图像中的所有像素执行该操作以产生输出滤波图像。试试这段代码并检查结果:
import cv2
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
img = cv2.imread("opencv_logo.png")
kernel = np.ones((5, 5), np.float32)/25
dst = cv2.filter2D(img, -1, kernel)
plt.rcParams['figure.figsize'] = (9, 6)
plt.subplot(121), plt.imshow(img), plt.title("Original")
plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(dst), plt.title("Averaging")
plt.xticks([]), plt.yticks([])
plt.show()
4.3 图像模糊(图像平滑)Image Blurring(Image Smoothing)
通过将图像与低通滤波器内核卷积来实现图像模糊。它有助于消除噪音。它实际上从图像中去除了高频内容(例如:噪声,边缘),导致边缘在应用滤波器时模糊。 (嗯,有模糊技术不会模糊边缘)。 OpenCV主要提供四种模糊技术。
4.3.1 平均(Averaging)
这是通过将图像与标准化的箱式滤波器进行卷积来完成的。它采用内核区域下所有像素的平均值,并用这个平均值替换中心元素。这是由函数cv2.blur()或cv2.boxFilter()完成的。查看文档以获取有关内核的更多详细信息。我们应该指定内核的宽度和高度。 3x3标准化的箱式滤波器看起来像这样:
\[ K = \frac{1}{9} \left[ \begin{matrix} 1 & 1 &1 \\ 1 & 1 &1 \\ 1 & 1 &1 \end{matrix} \right] \]
注意:
如果你不想使用标准箱式滤波器,使用cv.bowFilter()时将参数normalize=False传递给函数。
检查下面的5x5内核的例子演示:
import cv2
import numpy as np
import matplotlib.pyplot as plt
img = cv2.imread('opencv_logo.png')
blur = cv2.blur(img, (5, 5))
plt.rcParams['figure.figsize'] = (9, 6)
plt.subplot(121), plt.imshow(img), plt.title("Original")
plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(dst), plt.title("Blurred")
plt.xticks([]), plt.yticks([])
plt.show()
4.3.2 高斯滤波(Gaussian Filtering)
在该方法中,使用高斯核代替由相等滤波器系数组成的箱式滤波器。它是通过函数cv2.GaussianBlur()完成的。我们应该指定内核的宽度和高度,它们应该是正数和奇数。我们还应该分别指定X和Y方向的标准差,sigmaX和sigmaY。如果仅指定了sigmaX,则sigmaY等于sigmaX。如果两者都为零,则根据内核大小计算它们。高斯滤波在从图像中去除高斯噪声方面非常有效。
如果需要,可以使用函数cv2.getGaussianKernel()创建高斯内核。
上面的代码可以修改为高斯模糊:
blur = cv2.GaussianBlur(img, (5, 5), 0)
4.3.3 中值滤波(Median Filtering)
这里,函数cv2.medianBlur()计算内核窗口下所有像素的中值,并用该中值替换中心像素。这在去除椒盐噪音(salt-and-pepper noise)方面非常有效。值得注意的一件事是,在高斯和箱式滤波器中,中心元素的滤波值可以是原始图像中可能不存在的值。然而,在中值滤波中不是这种情况,因为中心元素总是被图像中的某个像素值替换。这有效地降低了噪音。内核大小必须是正奇数。
在此演示中,我们在原始图像中添加了50%的噪点并使用中值滤波器。检查结果:
median = cv2.medianBlur(img, 5)
4.3.4 双边滤波(Bilateral Filtering)
正如我们所指出的,我们之前介绍的滤镜往往会模糊边界。双边滤波器cv2.bilateralFilter()不会这样,并且在保留边界时非常有效地去除噪声。但与其滤波器相比,操作速度较慢。我们已经看到高斯滤波器采用像素周围的邻域并找到其高斯加权平均值。该高斯滤波器仅是空间的函数,即在滤波时考虑附近的像素。它不考虑像素是否具有几乎相同的强度值,并且不考虑像素是否位于边缘上。产生的效果是高斯滤波器趋于模糊边缘,这不是所希望的。
双边滤波器在空间域中也使用高斯滤波器,但它还使用一个(乘法)高斯滤波器分量,它是像素强度差的函数。空间的高斯函数确保仅考虑像素是“空间邻居”进行滤波,而应用于强度域的高斯分量(强度差异的高斯函数)确保只有那些强度与中心强度相似的像素('强度邻居')以计算模糊强度值。结果,该方法保留了边缘,因为对于位于边缘附近的像素,放置在边缘的另一侧上的相邻像素,因此与中心像素相比表现出大的强度变化,将不包括用于模糊。
下面的示例演示了双边过滤的使用(有关参数的详细信息,请参阅OpenCV文档)。
blur = cv2.bilateralFilter(img, 9, 75, 75)
注意表面的纹理(texture)已经模糊掉了,但是边界仍然保留。
4.4 附加资源
双边滤波
5 Morphological Transformations
5.1 目标
- 学习不同的形态操作,比如Erosion, Dilation, Opening, Closing等。
- 学习不同的函数,比如cv2.erode(), cv2.dilate(), cv2.morphologyEx()等。
5.2 理论
形态变换是基于图像形状的一些简单操作,是在二进制图像上操作的。它需要两个输入,一个是原始图像,第二个叫做结构化元素(structuring element)或者核(kernel),决定着操作本质。两个基本的形态操作是Erosion和Dilation,它的变体形式如Opening, Closing, Gradient等也发挥作用。我们将在以下图片的帮助下一个个的看到:
5.2.1 腐蚀(Erosion)
腐蚀的基本观点就像土壤腐蚀,它腐蚀掉前景对象的边界(总是试图前景为白色)。所以它是怎么做到的呢?核沿着图像滑动(像2D卷积那样)。原始图像中的一个像素(0或者1)只有在核下的所有像素为1的情况下才为1,否则它被腐蚀(为0)。
所以所发生的是,依赖于核的大小,边界附近的所有像素都被抛弃。所以前景对象的厚度(或尺寸)减小或在图像中的白色区域减小。在移除小的白色噪音(在colorspace章节中已经见过)和分离两个连在一起的对象中是有用的。
这里我将用一个5x5的元素全为1的核作为例子,下面看它如何作用:
import cv2
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
img = cv2.imread("morphological.png", 0)
kernel = np.ones((5, 5), np.uint8)
erosion = cv2.erode(img, kernel, iterations=1)
plt.rcParams['figure.figsize'] = (4.5, 3.5)
plt.imshow(erosion, 'gray')
plt.xticks([]), plt.yticks([])
plt.show()
5.2.2 扩张(Dilation)
扩张是腐蚀的相反操作。这里,在核下的至少一个像素为1,则此像素为1。所以它增加了图像中的白色区域或者前景对象的尺寸增加。正常情况下,在噪音移除时,先进行腐蚀操作,再进行扩张操作。因为,腐蚀移除白色噪音,但它也缩小(shrinks)了我们的对象,所以我们扩张它。因为噪音已经去除,它们不会再恢复,但是我们的对象区域增加。它在连接一个对象的两个分离部分也是有用的。
img = cv2.imread("morphological.png", 0)
kernel = np.ones((5, 5), np.uint8)
dilation = cv2.dilate(img, kernel, iterations=1)
plt.rcParams['figure.figsize'] = (4.5, 3.5)
plt.imshow(dilation, 'gray')
plt.xticks([]), plt.yticks([])
plt.show()
5.2.3 开运算(Opening)
开运算仅仅是先腐蚀再扩张的另一个名字。正如我们上面解释的,他在移除噪音中有用。这里我们用函数cv2.morphologyEx()。
img = cv2.imread("noise.png", 0)
kernel = np.ones((5, 5), np.uint8)
opening = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
plt.rcParams['figure.figsize'] = (8, 3.5)
plt.subplot(121), plt.imshow(img, 'gray'), plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(opening, 'gray'), plt.xticks([]), plt.yticks([])
plt.show()
5.2.4 闭运算(Closing)
闭运算是开运算的相反操作,即先扩张再腐蚀。在关闭前景对象对象里的小洞,或者对象中的小黑点是有用的。
img = cv2.imread("hole.png", 0)
kernel = np.ones((5, 5), np.uint8)
closing = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)
plt.rcParams['figure.figsize'] = (8, 3.5)
plt.subplot(121), plt.imshow(img, 'gray'), plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(closing, 'gray'), plt.xticks([]), plt.yticks([])
plt.show()
5.2.5 形态梯度(Morphological Gradient)
它是一个图像的扩张和腐蚀的差。结果看起来像对象的轮廓。
img = cv2.imread("morphological.png", 0)
kernel = np.ones((5, 5), np.uint8)
gradient = cv2.morphologyEx(img, cv2.MORPH_GRADIENT, kernel)
plt.rcParams['figure.figsize'] = (8, 3.5)
plt.subplot(121), plt.imshow(img, 'gray'), plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(gradient, 'gray'), plt.xticks([]), plt.yticks([])
plt.show()
5.2.6 礼帽(Top Hat)
输入图像和图像开运算的差,下面的例子用了9x9的核。
img = cv2.imread("morphological.png", 0)
kernel = np.ones((9, 9), np.uint8)
topHat = cv2.morphologyEx(img, cv2.MORPH_TOPHAT, kernel)
plt.rcParams['figure.figsize'] = (8, 3.5)
plt.subplot(121), plt.imshow(img, 'gray'), plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(topHat, 'gray'), plt.xticks([]), plt.yticks([])
plt.show()
5.2.7 黑帽(Black Hat)
输入图像的闭运算和输入图像的差。
img = cv2.imread("morphological.png", 0)
# 为了使得和官网上图像相同,用了30x30核
kernel = np.ones((9, 9), np.uint8)
blackHat = cv2.morphologyEx(img, cv2.MORPH_BLACKHAT, kernel)
plt.rcParams['figure.figsize'] = (8, 3.5)
plt.subplot(121), plt.imshow(img, 'gray'), plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(blackHat, 'gray'), plt.xticks([]), plt.yticks([])
plt.show()
5.3 结构化元素(Structuring Element)
我们在先前的例子中借助Numpy手动创建了一个结构化元素。它是矩形,但是在某些情况下,你可能需要椭圆形/圆形的核。为了这个目的,OpenCV有函数cv2.getStructuringElement()。你仅仅需要传递核的形状和大小就可以得到想要的核。
5.4 附加资源
Morphological Operations at HIPR2
6 Image Gradients
6.1 目标
在这一章,我们将学习:
- 找到图像梯度、边界等
- 学习函数:cv2.Sobel(), cv2.Scharr(), cv2.Laplacian()等
6.2 理论
OpenCV提供了三种梯度滤波器(gradient filters)或者高通量滤波器(High-pass filter),Sobel, Scharr和Laplacian。
6.2.1 Sobel和Scharr导数
Sobel算子是高斯平滑和微分操作的联合体,所以它的抗噪音能力更好。你可以指定求导的方向,垂直或者水平(参数分别为yorder和xorder)。你也可以通过参数ksize来指定核的大小。如果ksize=-1,则会用到3x3的Scharr滤波器,可以给出比3x3的Sobel滤波器更好的结果。请看使用核的文档。
6.2.2 Laplacian导数
它计算通过关系式$ \triangle src = \frac{\partial^2 src}{\partial x^2} + \frac{\partial^2 src}{\partial y^2} $给出的图像的Laplacian,关系式中每个导数由Sobel导数给出。如果ksize=1,则下面的核被用作滤波:
\[ kernel = \left [ \begin{matrix} 0 & 1 & 0 \\ 1 & -4 & 1 \\ 0 & 1 & 0 \end{matrix} \right ] \]
6.2.3 代码
下面的代码在一个图标上展示了素有的操作。所有的核大小为5x5。输出图像的深度为-1,得到np.uint8类型的结果。
import cv2
import numpy as np
import matplotlib.pyplot as plt
img = cv2.imread('dave.jpg', 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.rcParams['figure.figsize'] = (10, 8)
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('Sobel X'), plt.xticks([]), plt.yticks([])
plt.subplot(2,2,4), plt.imshow(sobely,cmap = 'gray')
plt.title('Sobel Y'), plt.xticks([]), plt.yticks([])
plt.show()
6.3 一个重要的事情
在我们的上一个示例中,输出数据类型为cv2.CV_8U或np.uint8。 但是这有一个小问题,将黑到白转换视为正斜率(它具有正值),而将白到黑转换视为负斜率(它具有负值)。因此,当你将数据转换为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('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 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()