OpenCV-Python教程:图像梯度(Sobel,Scharr,Laplacian)

原文链接:http://www.juzicode.com/opencv-python-image-gradient

返回Opencv-Python教程

高斯平滑、双边平滑 均值平滑、中值平滑 介绍的平滑处理可以看做是图像的“低通滤波”,它会滤除掉图像的“高频”部分,使图像看起来更平滑,而图像梯度则可以看做是对图像进行“高通滤波”,它会滤除图像中的低频部分,为的是凸显出图像的突变部分。在 形态学变换~开闭操作,顶帽黑帽,形态学梯度,击中击不中(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变换取得的梯度信息要更明显一些。

扩展阅读:

  1. OpenCV-Python教程
  2. 论如何把自己变成卡通人物(OpenCV制作卡通化头像)

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注