基于Python的OpenCV图像处理8

May 20,2017   5867 words   21 min


图像梯度

图像梯度简单来说就是求导。OpenCV提供三种不同的梯度滤波器,或者说高通滤波器: Sobel、Scharr和Laplacian。Sobel和Scharr其实就是求一阶或二阶导数。Scharr是对Sobel(使用小的卷积核求解梯度角度时)的优化。 Laplacian是求二阶导数。

Sobel算子

Sobel算子是高斯平滑与微分操作的结合体,所以它的抗噪声能力很好。 主要用作边缘检测,在技术上,它是离散性差分算子,用来运算图像亮度函数的灰度之近似值。 在图像的任何一点使用此算子,将会产生对应的灰度矢量或是其法矢量。
该算子包含两组3x3的矩阵,分别为横向及纵向,将之与图像作平面卷积,即可分别得出横向及纵向的亮度差分近似值。 如果以A代表原始图像,Gx及Gy分别代表经横向及纵向边缘检测的图像灰度值,其公式如下:

\[G_{x} = \\ (-1)*f(x-1, y-1) + 0*f(x,y-1) + 1*f(x+1,y-1) +\\ (-2)*f(x-1,y) + 0*f(x,y)+2*f(x+1,y) +\\ (-1)*f(x-1,y+1) + 0*f(x,y+1) + 1*f(x+1,y+1)\] \[G_{y} =\\ 1* f(x-1, y-1) + 2*f(x,y-1)+ 1*f(x+1,y-1) +\\ 0*f(x-1,y)+ 0*f(x,y) + 0*f(x+1,y)+\\ (-1)*f(x-1,y+1) + (-2)*f(x,y+1) + (-1)*f(x+1, y+1)\]

其中f(a,b)表示图像(a,b)点的灰度值。 在实际计算时需要注意一点,即由于我们想获得的是x、y方向的梯度大小,对梯度变化方向(亮到暗或暗到亮)不是那么感兴趣,因此需要对上式算得的结果取绝对值。这样反映的才是不带方向的梯度幅度的大小。 图像的每一个像素的横向及纵向灰度值通过以下公式结合,来计算该点灰度的大小:

\[G=\sqrt{G_{x}^{2}+G_{y}^{2}}\]

但在实际应用中,考虑到算法效率并不会平方再开方,而是使用下面的公式:

\[\left | G \right |=\left | G_{x}^{2} \right |+\left | G_{y}^{2} \right |\]

当梯度G的绝对值大于某一阈值时,认为该点为边缘点。同时可利用如下公式计算梯度方向:

\[\theta =arctan(\frac{G_{y}}{G_{x}})\]

Sobel算子根据像素点上下、左右邻点灰度加权差,在边缘处达到极值这一现象检测边缘。 对噪声具有平滑作用,提供较为精确的边缘方向信息,边缘定位精度不够高。 当对精度要求不是很高时,是一种较为常用的边缘检测方法。

Scharr算子

与Sobel算子类似,3×3的Scharr卷积核如下: 它的效果要比3×3的Sobel滤波器好,而且速度相同。所以在使用3×3滤波器时应尽量使用Scharr滤波器。

Laplacian算子

它可以使用二阶导数的形式定义,可假设其离散实现类似于二阶Sobel导数。事实上OpenCV在计算时, 直接调用Sobel算子。卷积核如下:

OpenCV实现代码
Sobel、Scharr算子实现

在OpenCV中,可以设定Sobel算子的求导方向(x或y),以及卷积核的大小。如果ksize为1,则会使用3×3的Scharr滤波器。 在OpenCV中实现Sobel算子代码如下:

# coding=utf-8
import numpy as np
import cv2

img = cv2.imread("E:\\5202.png")

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 第一个参数是待处理图像
# 第二个参数是输出图像深度,-1表示与原图一致
# 第三个参数表示x方向求导,0表示不操作,1表示一阶导数,最高2阶导数
# 第四个参数表示y方向求导,0表示不操作,1表示一阶导数,最高2阶导数
# 第五个参数是卷积核大小,默认为3,-1表示使用Scharr滤波器
sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3)

cv2.imshow("sobelx", sobelx)
cv2.imshow("gray", gray)
cv2.waitKey(0)

结果如下: 有以下几点需要注意:
1.对x方向和y方向求导不能都为0,否则会报错。1,0表示对x方向求导, 0,1表示对y方向求导。而1,1则表示同时对x、y方向求导。 换句话说,即只有某点在x、y方向同时有变化才会显示。演示代码及效果如下:

img = cv2.imread("E:\\520.png")

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3)
sobely = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3)
sobel = cv2.Sobel(gray, cv2.CV_64F, 1, 1, ksize=3)

cv2.imshow("gray", gray)
cv2.imshow("sobelx", sobelx)
cv2.imshow("sobely", sobely)
cv2.imshow("sobel", sobel)
cv2.waitKey(0)

结果如下: 可以看到,同时对x、y方向求导的结果就是,提取出了矩形的角点。其实细心点你就会发现这个结果是有问题的, 提取的边界都少了一半。这个问题在后面会说到。

2.第二个问题是不同的ksize在图像上最直接的反映就是边缘的粗细。卷积核越大,其对边界越敏感。以下是对比图。 可以看出,ksize分别是3、5、7、9的边缘粗细。因为卷积核越大越敏感,所以会检测出更多边界,所以边缘就会粗了。 下面是一幅真实图片的Sobel算子不同卷积核大小的情况。随着卷积核不断增大,图片中的白色也越来越多(也即边界 越来越多),而当卷积核过大之后,反而得不到正确结果了。

3.第三个需要注意的是,我们可以通过参数设定输出图像的深度(数据类型)与原图保持一致。但 如果一个从黑道白的边界的导数是正数,而一个从白到黑的边界的导数是负数。如果图像的 深度是uint8时,那么所有的负值都会被截断成0,换句话说即把一半的边界丢失掉。所以解决这个问题 的办法是,将输出的数据类型设置的更高,如cv2.CV_16Scv2.CV_64F等。取绝对值后再把 它们转回到cv2.CV_8U。下面代码演示了这种现象:

img = cv2.imread("E:\\520.png")

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# sobel8u与原图保持一致,所以所有的负数会变为0
sobel8u = cv2.Sobel(gray, -1, 1, 0, ksize=5)

# 首先设置输出的数据类型为64位float
sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=5)
# 然后对其取绝对值,变成正数
abs_sobelx = np.absolute(sobelx)
# 最后进行类型转换
abs_sobelx_8u = np.uint8(abs_sobelx)

cv2.imshow("gray", gray)
cv2.imshow("sobel8u", sobel8u)
cv2.imshow("abs_sobelx_8u", abs_sobelx_8u)
cv2.waitKey(0)

结果如下: 可以明显的看到,如果直接指定与原图类型相同,那么直接会丢失边界。所以需要先输出成“支持负数”的类型, 然后再取绝对值,再转换回来。这个解决办法不仅对于Sobel算子,对于所有出现负数的情况都适用。
事实上,不同的数据类型之间存在着很大差别。如下代码演示:

img = cv2.imread("E:\\5202.png")

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 拉普拉斯算子,这时的结果是float类型
laplacian = cv2.Laplacian(gray, cv2.CV_64F)
# 对其取绝对值,还是float类型
laplacianf = np.absolute(laplacian)
# 对绝对值转换到uint8
abs_laplacian_8u = np.uint8(laplacianf)

cv2.imshow("gray", gray)
cv2.imshow("laplacian", laplacian)
cv2.imshow("laplacianf", laplacianf)
cv2.imshow("laplacian", abs_laplacian_8u)
cv2.waitKey(0)

对比如下: 可以看到差别是巨大的。所以最终结果不要忘了转成uint8,否则可能和你想象的差很远。

4.第四个需要注意的是,如何提取完整边界。通过第三点,我们对第一点的代码进行修正,进行边界提取。

img = cv2.imread("E:\\520.png")

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# x方向
sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=5)
abs_sobelx_8u = np.uint8(np.absolute(sobelx))

# y方向
sobely = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=5)
abs_sobely_8u = np.uint8(np.absolute(sobely))

# x、y方向
sobelxy = cv2.Sobel(gray, cv2.CV_64F, 1, 1, ksize=5)
abs_sobelxy_8u = np.uint8(np.absolute(sobelxy))

cv2.imshow("gray", gray)
cv2.imshow("sobelx", abs_sobelx_8u)
cv2.imshow("sobely", abs_sobely_8u)
cv2.imshow("sobelxy", abs_sobelxy_8u)
cv2.waitKey(0)

结果如下: 可以看到,x、y方向的提取结果和我们预想的一样,但是x、y方向的就不同了。我们预想的将讲个方向一起提取 应该是完整边界,但实际上,函数里的1,1参数也正如前面所说。表示的是对某一点同时对x、y求导,而并不是 先对某个方向求导,再对另一个方向求导,完了之后再按照前面说的公式加起来得到结果。那么这该怎么办呢? 其实也很简单。因为我们在之前就对x、y方向结果取了绝对值,我们只需要将这两幅提取结果相加即可(不用平方 再开方的原因前面已经说了)。这里不需要对角点处的值再单独加上了。因为x或y方向的边界中就已经包含角点了。 所以获取完整边界只需要下面这行代码:

sobel = cv2.add(abs_sobelx_8u, abs_sobely_8u)

结果如下所示: 之所以不用sobel = abs_sobelx_8u + abs_sobely_8u,其原因在之前也说过了。OpenCV的加法是一种饱和操作, Numpy的加法是一种模操作。对于越界的情况,OpenCV的加法更加可靠。

Laplacian算子

实现代码如下:

img = cv2.imread("E:\\edge.png")

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 拉普拉斯算子
# 第一个参数是待处理的图像
# 第二个参数是输出图像的数据类型
# 第三个参数是卷积核的大小,默认为3
laplacian = cv2.Laplacian(gray, cv2.CV_64F, ksize=3)
abs_laplacian_8u = np.uint8(np.absolute(laplacian))

cv2.imshow("gray", gray)
cv2.imshow("laplacian", abs_laplacian_8u)
cv2.waitKey(0)

结果如下: 在之前的处理中,我们都是先对图像进行了灰度化。如果不进行灰度化,那么得到的会是彩色的边界,如下所示。

对比

下面是用三种滤波器对同一幅图片进行操作对比,卷积核大小是5×5。

# coding=utf-8
import numpy as np
import cv2

img = cv2.imread("E:\\5202.png")

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# Sobel算子
# x方向
sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3)
abs_sobelx_8u = np.uint8(np.absolute(sobelx))
# y方向
sobely = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3)
abs_sobely_8u = np.uint8(np.absolute(sobely))
# 合并
sobel = cv2.add(abs_sobelx_8u, abs_sobely_8u)

# Scharr算子
# x方向
scharrx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=-1)
abs_scharrx_8u = np.uint8(np.absolute(scharrx))
# y方向
scharry = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=-1)
abs_scharry_8u = np.uint8(np.absolute(scharry))
# 合并
scharr = cv2.add(abs_scharrx_8u, abs_scharry_8u)

# Laplacian算子
laplacian = cv2.Laplacian(gray, cv2.CV_64F, ksize=3)
abs_laplacian_8u = np.uint8(np.absolute(laplacian))

cv2.imshow("gray", gray)
cv2.imshow("laplacian", abs_laplacian_8u)
cv2.imshow("sobel", sobel)
cv2.imshow("scharr", scharr)
cv2.waitKey(0)

对比图如下:

本文作者原创,未经许可不得转载,谢谢配合

返回顶部