原文链接:http://www.juzicode.com/opencv-python-image-gradient
高斯平滑、双边平滑 和 均值平滑、中值平滑 介绍的平滑处理可以看做是图像的“低通滤波”,它会滤除掉图像的“高频”部分,使图像看起来更平滑,而图像梯度则可以看做是对图像进行“高通滤波”,它会滤除图像中的低频部分,为的是凸显出图像的突变部分。在 形态学变换~开闭操作,顶帽黑帽,形态学梯度,击中击不中(morphologyEx) 一文中我们也接触到了图像梯度的内容,今天继续介绍几种计算图像梯度的方法。
1、Sobel
Sobel 3×3尺寸的kernel如下图所示,从kernel可以看到计算Sobel梯度会有x和y 2个方向的梯度:
以上图3×3尺寸的Sobel kernel为例,x方向梯度是该点右侧像素值的2倍加上右上、右下像素值的和减去左侧像素值的2倍加上左上、左下像素值的和,该点的梯度值和自身无关,只和其左右2侧的像素值有关,y方向的梯度则是和上下方向的像素值有关。
接口形式:
dst = cv2.Sobel(src, ddepth, dx, dy[, dst[, ksize[, scale[, delta[, borderType]]]]])
- 参数含义:
- src:源图像;
- ddepth:目标图像深度;
- dx:x方向求导阶数;
- dy:y方向求导阶数;
- dst:目标图像;
- ksize:kernel尺寸,说明文档上指出必须是1,3,5,7中的一个,但是实验可以得到应该是小于31的正奇数;如果是-1表示scharr滤波;
- scale:缩放比例,默认为1;
- delta:叠加值,默认为0;
- borderType:边界填充类型;
下面这个例子中先设置dx=1,dy=0计算x方向的梯度,再设置dy=1,dx=0计算y方向的梯度,为了避免出现饱和运算,dtype设置的是比源图像数据类型CV_8U更高的CV_16S,然后将结果用convertScaleAbs()转换回CV_8U(np.unit8)类型,最后用addWeighted()将x和y方向的梯度图加权相加。
import matplotlib.pyplot as plt
import cv2
print('VX公众号: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)
plt.rc('font',family='Youyuan',size='9')
img_src = cv2.imread('..\\samples\\data\\sudoku.png' ,cv2.IMREAD_GRAYSCALE)
grad_x = cv2.Sobel(img_src, cv2.CV_16S, 1, 0, ksize=3)
grad_y = cv2.Sobel(img_src, cv2.CV_16S, 0, 1, ksize=3)
abs_grad_x = cv2.convertScaleAbs(grad_x)
abs_grad_y = cv2.convertScaleAbs(grad_y)
grad = cv2.addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0)
fig,ax = plt.subplots(2,2)
ax[0,0].set_title('原图(juzicode.com)')
ax[0,0].imshow(img_src ,cmap = 'gray')
ax[0,1].set_title('abs_grad_x')
ax[0,1].imshow(abs_grad_x,cmap = 'gray')
ax[1,0].set_title('abs_grad_y')
ax[1,0].imshow(abs_grad_y,cmap = 'gray')
ax[1,1].set_title('grad x+y')
ax[1,1].imshow(grad,cmap = 'gray')
ax[0,0].axis('off');ax[0,1].axis('off');ax[1,0].axis('off');ax[1,1].axis('off')#关闭坐标轴显示
plt.show()
运行结果:
上图右上角是沿x方向的梯度变化,左下角是沿y方向的梯度变化,右下角为2者各占一半权重组合的图片。从组合后的梯度图像可以看到:原始图像的棋盘线和数字的边沿部分变得更亮,而棋盘空白和数字中间这些亮度连续区域都变得更暗,最后起到了凸显边沿的效果。
如果dx和dy同时设置为1:grad_x_y = cv2.Sobel(img_src, cv2.CV_16S, 1, 1, ksize=3),我们来看下显示的效果:
import matplotlib.pyplot as plt
import cv2
print('VX公众号: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)
plt.rc('font',family='Youyuan',size='9')
img_src = cv2.imread('..\\samples\\data\\sudoku.png' ,cv2.IMREAD_GRAYSCALE)
grad_x = cv2.Sobel(img_src, cv2.CV_16S, 1, 0, ksize=3)
grad_y = cv2.Sobel(img_src, cv2.CV_16S, 0, 1, ksize=3)
abs_grad_x = cv2.convertScaleAbs(grad_x)
abs_grad_y = cv2.convertScaleAbs(grad_y)
grad = cv2.addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0)
#同时求x和y方向的梯度
grad_x_y = cv2.Sobel(img_src, cv2.CV_16S, 1, 1, ksize=3)
abs_grad_x_y = cv2.convertScaleAbs(grad_x_y)
fig,ax = plt.subplots(2,2)
ax[0,0].set_title('原图(juzicode.com)')
ax[0,0].imshow(img_src ,cmap = 'gray')
ax[0,1].set_title('abs_grad_x')
ax[0,1].imshow(abs_grad_x,cmap = 'gray')
ax[1,0].set_title('grad_x+y')
ax[1,0].imshow(grad,cmap = 'gray')
ax[1,1].set_title('abs_grad_dx_dy=1')
ax[1,1].imshow(abs_grad_x_y,cmap = 'gray')
ax[0,0].axis('off');ax[0,1].axis('off');ax[1,0].axis('off');ax[1,1].axis('off')#关闭坐标轴显示
plt.show()
从运行结果可以看到,当dx和dy同时设置为1对应上图右下角图像,棋盘中水平和垂直方向线条的梯度都消失了,这里dx和dy都为1表明是与的关系,只有x和y方向都有梯度的时候才能被检测出来。
接下来这个例子是设置不同的ksize:
import matplotlib.pyplot as plt
import cv2
print('VX公众号: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)
plt.rc('font',family='Youyuan',size='9')
img_src = cv2.imread('..\\samples\\data\\sudoku.png' ,cv2.IMREAD_GRAYSCALE)
print(img_src.shape)
grad_x_ksize1 = cv2.Sobel(img_src, cv2.CV_16S, 1, 0, ksize=1)
grad_x_ksize3 = cv2.Sobel(img_src, cv2.CV_16S, 1, 0, ksize=3)
grad_x_ksize5 = cv2.Sobel(img_src, cv2.CV_16S, 1, 0, ksize=5)
#设置不同的ksize值
abs_grad_x_ksize1 = cv2.convertScaleAbs(grad_x_ksize1)
abs_grad_x_ksize3 = cv2.convertScaleAbs(grad_x_ksize3)
abs_grad_x_ksize5 = cv2.convertScaleAbs(grad_x_ksize5)
#显示
fig,ax = plt.subplots(2,2)
ax[0,0].set_title('原图(juzicode.com)')
ax[0,0].imshow(img_src ,cmap = 'gray')
ax[0,1].set_title('abs_grad_x_ksize=1')
ax[0,1].imshow(abs_grad_x_ksize1,cmap = 'gray')
ax[1,0].set_title('abs_grad_x_ksize=3')
ax[1,0].imshow(abs_grad_x_ksize3,cmap = 'gray')
ax[1,1].set_title('abs_grad_x_ksize=5')
ax[1,1].imshow(abs_grad_x_ksize5,cmap = 'gray')
ax[0,0].axis('off');ax[0,1].axis('off');ax[1,0].axis('off');ax[1,1].axis('off')#关闭坐标轴显示
plt.show()
上面的例子中ksize依次设置为1,3,5,从运行结果可以看到ksize的值越大,梯度信息呈现的越多。
下面再设置不同的求导阶数,这里以x方向的dx为例,在ksize=5的时候设置dx分别为1,2,3,这里需要注意dx的值要小于ksize:
import matplotlib.pyplot as plt
import cv2
print('VX公众号: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)
plt.rc('font',family='Youyuan',size='9')
img_src = cv2.imread('..\\samples\\data\\sudoku.png' ,cv2.IMREAD_GRAYSCALE)
#相同的ksize,不同的dx
grad_order1 = cv2.Sobel(img_src, cv2.CV_16S, 1, 0, ksize=5)
grad_order2 = cv2.Sobel(img_src, cv2.CV_16S, 2, 0, ksize=5)
grad_order3 = cv2.Sobel(img_src, cv2.CV_16S, 3, 0, ksize=5)
abs_grad_order1 = cv2.convertScaleAbs(grad_order1)
abs_grad_order2 = cv2.convertScaleAbs(grad_order2)
abs_grad_order3 = cv2.convertScaleAbs(grad_order3)
fig,ax = plt.subplots(2,2)
ax[0,0].set_title('原图(juzicode.com)')
ax[0,0].imshow(img_src ,cmap = 'gray')
ax[0,1].set_title('abs_grad_order1')
ax[0,1].imshow(abs_grad_order1,cmap = 'gray')
ax[1,0].set_title('abs_grad_order2')
ax[1,0].imshow(abs_grad_order2,cmap = 'gray')
ax[1,1].set_title('abs_grad_order3')
ax[1,1].imshow(abs_grad_order3,cmap = 'gray')
ax[0,0].axis('off');ax[0,1].axis('off');ax[1,0].axis('off');ax[1,1].axis('off')#关闭坐标轴显示
plt.show()
从运行结果可以看到在相同的ksize下,dx的值越小,梯度的细节越多。
2、Scharr
当kernel的尺寸为3×3时,Sobel计算的结果不是很精确,为了得到更精确的计算结果常采用下图所示的Scharr kernel:
Scharr变换可以看做是使用了Scharr核的Sobel变换,是一种经过改进的Sobel变换,同样也要区分x和y方向分开计算梯度。
接口形式:
dst = cv2.Scharr(src, ddepth, dx, dy[, dst[, scale[, delta[, borderType]]]])
- 参数含义:
- src:源图像;
- ddepth:目标图像深度;
- dx:x方向求导阶数;
- dy:y方向求导阶数;
- dst:目标图像;
- scale:缩放比例,默认为1;
- delta:叠加值,默认为0;
- borderType:边界填充类型;
注意Scharr()没有ksize参数,因为Scharr kernel的大小固定为3×3。
下面这个例子类似前面用Sobel()计算图像梯度时,先计算x方向梯度,再计算y方向梯度,然后加权生成总的梯度图像:
import matplotlib.pyplot as plt
import cv2
print('VX公众号: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)
plt.rc('font',family='Youyuan',size='9')
img_src = cv2.imread('..\\samples\\data\\sudoku.png' ,cv2.IMREAD_GRAYSCALE)
grad_x = cv2.Scharr(img_src,cv2.CV_16S,1,0)
grad_y = cv2.Scharr(img_src,cv2.CV_16S,0,1)
abs_grad_x = cv2.convertScaleAbs(grad_x)
abs_grad_y = cv2.convertScaleAbs(grad_y)
grad = cv2.addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0)
fig,ax = plt.subplots(2,2)
ax[0,0].set_title('原图(juzicode.com)')
ax[0,0].imshow(img_src ,cmap = 'gray')
ax[0,1].set_title('abs_grad_x')
ax[0,1].imshow(abs_grad_x,cmap = 'gray')
ax[1,0].set_title('abs_grad_y')
ax[1,0].imshow(abs_grad_y,cmap = 'gray')
ax[1,1].set_title('grad-x+y')
ax[1,1].imshow(grad,cmap = 'gray')
ax[0,0].axis('off');ax[0,1].axis('off');ax[1,0].axis('off');ax[1,1].axis('off')
plt.show()
运行结果:
需要注意的是在计算Scharr梯度时,dx和dy要满足如下的关系,否则会抛异常:
CV_Assert( dx >= 0 && dy >= 0 && dx+dy == 1 )
也就是每次只能求x方向或者y方向单个方向的梯度,而且只能求一阶梯度,不像Sobel()中dx或dy可以设置为更大的值计算更高阶的梯度。
3、Laplacian
Laplacian变换是对图像求二阶导数,下图是2种3×3尺寸的kernel,这里ksize是Laplacian()的入参名称:
Laplacian()变换不需要区分图像的x和y方向计算梯度,从上图的2种kernel也可以看到其x和y方向是对称的。
在Laplacian()变换中,ksize必须是小于31的正奇数,但是当ksize等于1时,这时kernel的尺寸大小并非是1,其实际尺寸仍然为3×3,这点从源码上可以看到当ksize=1时,实际为一个包含9个元素的3×3尺寸的kernel:
if( ksize == 1 || ksize == 3 ){
float K[2][9] = {
{ 0, 1, 0, 1, -4, 1, 0, 1, 0 }, //ksize==1时的kernel
{ 2, 0, 2, 0, -8, 0, 2, 0, 2 } //ksize==3时的kernel
};
Mat kernel(3, 3, CV_32F, K[ksize == 3]);
......
}
接口形式:
dst = cv2.Laplacian(src, ddepth[, dst[, ksize[, scale[, delta[, borderType]]]]])
- 参数含义:
- src:源图像;
- ddepth:目标图像深度;
- dst:目标图像;
- ksize:kernel尺寸,小于31的正奇数;如果为1仍然是一个3×3的kernel;
- scale:缩放比例,默认为1;
- delta:叠加值,默认为0;
- borderType:边界填充类型;
Laplacian变换中没有dx或dy参数,因为Laplacian是对图像求二阶导数。
下面这个例子显示了Laplacian中不同的ksize差异,以及和Sobel的对比:
import matplotlib.pyplot as plt
import cv2
print('VX公众号: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)
plt.rc('font',family='Youyuan',size='9')
img_src = cv2.imread('..\\samples\\data\\sudoku.png' ,cv2.IMREAD_GRAYSCALE)
#Laplacian
grad_lap = cv2.Laplacian(img_src,cv2.CV_16S,ksize=1)
abs_grad_lap1 = cv2.convertScaleAbs(grad_lap)
grad_lap = cv2.Laplacian(img_src,cv2.CV_16S,ksize=3)
abs_grad_lap3 = cv2.convertScaleAbs(grad_lap)
grad_lap = cv2.Laplacian(img_src,cv2.CV_16S,ksize=5)
abs_grad_lap5 = cv2.convertScaleAbs(grad_lap)
#二阶Sobel
grad_x = cv2.Sobel(img_src, cv2.CV_16S, 2, 0, ksize=3)
grad_y = cv2.Sobel(img_src, cv2.CV_16S, 0, 2, ksize=3)
abs_grad_x = cv2.convertScaleAbs(grad_x)
abs_grad_y = cv2.convertScaleAbs(grad_y)
abs_grad_sobel = cv2.addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0)
#显示
fig,ax = plt.subplots(2,3)
ax[0,0].set_title('原图(juzicode.com)')
ax[0,0].imshow(img_src ,cmap = 'gray')
ax[0,1].set_title('abs_grad_sobel')
ax[0,1].imshow(abs_grad_sobel,cmap = 'gray')
ax[1,0].set_title('abs_grad_lap1')
ax[1,0].imshow(abs_grad_lap1,cmap = 'gray')
ax[1,1].set_title('abs_grad_lap3')
ax[1,1].imshow(abs_grad_lap3,cmap = 'gray')
ax[1,2].set_title('abs_grad_lap5')
ax[1,2].imshow(abs_grad_lap5,cmap = 'gray')
ax[0,0].axis('off');ax[0,1].axis('off');ax[0,2].axis('off')#关闭坐标轴显示
ax[1,0].axis('off');ax[1,1].axis('off');ax[1,2].axis('off')
plt.show()
运行结果:
从运行结果可以看到Laplacian()中ksize越大,梯度信息越丰富,这点和Sobel变换是一样的。另外在相同的ksize时,二阶Sobel变换和Laplacian变换对比看,Laplacian变换取得的梯度信息要更明显一些。