形态学操作和图像梯度
一、形态学操作
主要有膨胀、腐蚀、开(先腐蚀后膨胀)、闭(线膨胀后腐蚀)操作。用到的函数有:cv.erode(), cv.dilate(), cv.morphologyEx()
用法如下:
- cv.erode( src, kernel[, dst[, anchor[, iterations[, borderType[, borderValue]]]]] ) -> dst
- cv.dilate( src, kernel[, dst[, anchor[, iterations[, borderType[, borderValue]]]]] ) -> dst
- 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])
可以看出一些毛毛刺刺被去除,但是原文本也被侵蚀了一部分
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])
3. 开操作
开操作只是先侵蚀后膨胀合并的一个名称。上面的例子表明,它在消除噪音方面很有用。这里我们使用函数cv.morphologyEx()
cv.morphologyEx函数的第二个参数一共有五种取值:
- cv.MORPH_OPEN : 开操作,先腐蚀后膨胀
- cv.MORPH_ClOSE:闭操作,先膨胀后腐蚀
- cv.MORPH_GRADIENT: 提取边界,利用膨胀的结果减去腐蚀的结果
- cv.MORPH_TOPHAT: 输入图像减去开操作结果。
- 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])
4. 闭操作
闭操作是开操作的对立操作,闭操作先膨胀后腐蚀。它在修补前景物体内部的小洞或物体上的小黑点时很有用。
img = cv.imread('fur.png')
kernel = np.ones((3,3), np.uint8)
closing = cv.morphologyEx(img, cv.MORPH_CLOSE, kernel)
compare([img, closing])
5. 求梯度
这是一个图像的膨胀操作减去腐蚀操作所得结果,结果看起来就像物体的轮廓。
img = cv.imread('fur.png')
kernel = np.ones((3,3), np.uint8)
gradient = cv.morphologyEx(img, cv.MORPH_GRADIENT, kernel)
compare([img, gradient])
6. 礼帽操作
输入图像减去开操作所得图像。
img = cv.imread('fur.png')
kernel = np.ones((3,3), np.uint8)
tophat = cv.morphologyEx(img, cv.MORPH_TOPHAT, kernel)
compare([img, tophat])
7. 黑帽操作
输入图像减去关操作所的结果
img = cv.imread('fur.png')
kernel = np.ones((3,3), np.uint8)
blackhat = cv.morphologyEx(img, cv.MORPH_BLACKHAT, kernel)
compare([img, blackhat])
8. 构建元素
在前面的示例中,我们借助 Numpy 手动创建了一个结构元素。它是长方形的。但在某些情况下,您可能需要椭圆形或圆形的内核。因此,为此,OpenCV 有一个函数 cv.getStructuringElement ()。您只需传递内核的形状和大小,就可以得到所需的内核。
- 内核形态有如下选项:
- cv.MORPH_RECT
- cv.MORPH_ELLIPSE
- 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])
可以看出,自定义的cross内核仍留下了以下白色斑点,而rect内核得到了较为清晰的效果
三、图像梯度
本节包括对图像梯度和边缘的一些介绍,主要由以下函数 cv.Sobel(), cv.Scharr(), cv.Laplacian()。
1. 概念
OpenCV 提供三种类型的梯度过滤器或高通过滤器:Sobel、Scharr和Laplacian,下面进行详细介绍
- 三者用法如下:
- cv.Sobel( src, ddepth, dx, dy[, dst[, ksize[, scale[, delta[, borderType]]]]] ) -> dst
- cv.Scharr( src, ddepth, dx, dy[, dst[, scale[, delta[, borderType]]]] ) -> dst
- 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过滤器结果更好。
-
三阶Sobel梯度矩阵: 计算得出负数结果或者超过了K比特上限时会进行截断:取为0或(2^k - 1),所以一般我们会分别求出对x的导数(使用cv.convertScaleAbs取绝对值),在求对y的导数(取绝对值)后,使用cv.addWeighted()将两者相加
-
三阶Scharr梯度矩阵: 计算方式同上
3. Laplacian 概念
它计算由关系给出的图像的拉普拉斯算子,其中每个导数都是使用 Sobel 导数找到的。 如果 ksize = 1,则使用以下内核进行过滤:
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])
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()
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])
# 同理我们可以得到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])
# 如果直接出入 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])
显然,分别计算后再相加所得的边缘没有重影且更加明显