Python OpenCV 之图像梯度,Sobel 算子、Scharr 算子和 laplacian 算子

举报
梦想橡皮擦 发表于 2021/05/26 16:49:46 2021/05/26
【摘要】 Python OpenCV 365 天学习计划,与橡皮擦一起进入图像领域吧。 Python OpenCV 基础知识铺垫Sobel 算子和 Scharr 算子Sobel 算子说明与使用Scharr 算子说明与使用 laplacian 算子橡皮擦的小节 基础知识铺垫 图像梯度是计算图像变化速度的方法,对于图像边缘部分,灰度值如果变化幅度较大,...

Python OpenCV 365 天学习计划,与橡皮擦一起进入图像领域吧。

基础知识铺垫

图像梯度是计算图像变化速度的方法,对于图像边缘部分,灰度值如果变化幅度较大,则其对应梯度值也较大,反之,图像中比较平滑的部分,灰度值变化较小,相应的梯度值变化也小。

有以上内容就可以学习图像梯度相关计算了,该知识后面会用到获取图像边缘信息相关技术中。

OpenCV 提供三种不同的梯度滤波器,或者说高通滤波器:SobelScharrLaplacian

在数学上,SobelScharr 是求一阶或二阶导数,Scharr 是对 Sobel(使用小的卷积核求解求解梯度角度时)的优化,Laplacian是求二阶导数。

数学部分我们依旧不展开,在第一遍通识学习之后,从上帝视角去补充。

Sobel 算子和 Scharr 算子

Sobel 算子说明与使用

Sobel算子是高斯平滑与微分操作的结合体,所以它的抗噪声能力很好(具体橡皮擦没有学到精髓,先用起来)。

在使用过程中可以设定求导的方向(xorderyorder),还可以设定使用的卷积核的大小(ksize)。

函数原型如下:

dst = cv2.Sobel(src, ddepth, dx, dy[, dst[, ksize[, scale[, delta[, borderType]]]]])

  
 
  • 1

参数说明如下:

  • src:输入图像;
  • ddepth:图像颜色深度,针对不同的输入图像,输出图像有不同的深度,具体参加后文,-1 表示图像深度一致;
  • dx:x 方向求导阶数;
  • dy:y 方向求导阶数;
  • ksize:内核大小,一般取奇数,1,3,5,7等值;
  • scale:缩放大小,默认值为 1;
  • delta:增量数值,默认值为 0;
  • borderType:边界类型,默认值为 BORDER_DEFAULT

备注:如果 ksize=-1,会使用 3x3Scharr 滤波器,该效果要比 3x3Sobel 滤波器好(而且速度相同,所以在使用 3x3 滤波器时应该尽量使用 Scharr 滤波器)。

关于该函数的测试代码如下(注意选择自己的图片,并放置到同一文件夹):

import cv2
import numpy as np

img = cv2.imread('./test.jpg')

dst = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)
dst = cv2.convertScaleAbs(dst)
cv2.imshow("dst", dst)
cv2.waitKey()
cv2.destroyAllWindows()

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

以上代码中有很多要说明和学习的,接下来我们逐步分析。

Sobel 滤波器中的第二个参数,设置为 cv2.CV_64F,在互联网找了一下通用解释

我们一般处理的都是 8 位图像,当计算的梯度小于零,会自动变成 0,造成边界信息丢失,关于为什么小于零,是由于从黑到白的边界导数是整数,而从白到黑的边界点导数是负数。当图像深度是 np.uint8 的时候,负值就会变成 0,基于这个原因,需要把输出图像的数据类型设置高一些,例如 cv2.CV_16Scv2.CV_64F等值。

当然简单记忆的方式,就是设置为 -1 即可。

处理完毕之后,在通过 cv2.convertScaleAbs函数将其转回原来的 uint8 格式,如果不转换,获取的图像不正确。

对比一下,转换与没有转换两次运行的效果,右侧明显失去边界值。
Python OpenCV 之图像梯度,Sobel 算子、Scharr 算子和 laplacian 算子
注意上文代码中,还有一个细节需要说明一下

dst = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)

  
 
  • 1
  • dx:x 方向求导阶数;
  • dy:y 方向求导阶数。

这其中 dx=1,dy=0 表示计算水平方向的,不计算垂直方向,说白了就是哪个方向等于 1,计算哪个方向。

还可以对函数中的 ddepth参数进行不同取值的比对,查看差异化内容。下述代码用到了图像融合函数 cv.addWeighted

import numpy as np
import cv2 as cv
def sobel_demo1(image): grad_x = cv.Sobel(image, cv.CV_16S, 1, 0) grad_y = cv.Sobel(image, cv.CV_16S, 0, 1) gradx = cv.convertScaleAbs(grad_x) grady = cv.convertScaleAbs(grad_y) cv.imshow('sobel_demo1_gradient_x', gradx) cv.imshow('sobel_demo1_gradient_y', grady) # 合并x, y两个梯度 add_image = cv.addWeighted(gradx, 0.5, grady, 0.5, 0) cv.imshow('sobel_demo1_addWeighted', add_image)

def sobel_demo2(image): grad_x = cv.Sobel(image, cv.CV_32F, 1, 0) grad_y = cv.Sobel(image, cv.CV_32F, 0, 1) gradx = cv.convertScaleAbs(grad_x) grady = cv.convertScaleAbs(grad_y) cv.imshow('sobel_demo2_gradient_x', gradx) cv.imshow('sobel_demo2_gradient_y', grady) # 合并x, y两个梯度 add_image = cv.addWeighted(gradx, 0.5, grady, 0.5, 0) cv.imshow('sobel_demo2_addWeighted', add_image)

src = cv.imread('./test.jpg', 1)
sobel_demo1(src)
sobel_demo2(src)
cv.waitKey(0)
cv.destroyAllWindows()

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

你可以不使用图像融合函数,直接通过 Sobel 函数计算 x 方向和 y 方向的导数,代码如下:

def sobel_demo3(image): grad = cv.Sobel(image, cv.CV_16S, 1, 1) grad = cv.convertScaleAbs(grad) cv.imshow('sobel_demo1_gradient', grad)

  
 
  • 1
  • 2
  • 3
  • 4

得到的结果比图像融合效果要差很多。
Python OpenCV 之图像梯度,Sobel 算子、Scharr 算子和 laplacian 算子
这样对比查阅不是很容易看清楚,可以结合下述代码进行改造,将图片整合到一起展示。

def sobel_demo1(image): grad_x = cv.Sobel(image, cv.CV_16S, 1, 0) grad_y = cv.Sobel(image, cv.CV_16S, 0, 1) gradx = cv.convertScaleAbs(grad_x) grady = cv.convertScaleAbs(grad_y) # 合并x, y两个梯度 dst = cv.addWeighted(gradx, 0.5, grady, 0.5, 0) imgs1 = np.hstack([cv.cvtColor(image, cv.COLOR_BGR2RGB), gradx]) imgs2 = np.hstack([grady, dst]) img_all = np.vstack([imgs1, imgs2]) plt.figure(figsize=(20, 10)) plt.imshow(img_all, cmap=plt.cm.gray) plt.show()

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

上述代码运行效果如下,非常容易比对效果。
Python OpenCV 之图像梯度,Sobel 算子、Scharr 算子和 laplacian 算子

Sobel算子算法的优点是计算简单,速度快。

但由于只采用了 2 个方向的模板,只能检测水平和垂直方向的边缘,因此这种算法对于纹理较为复杂的图像,其边缘检测效果就不是很理想,该算法认为:凡灰度新值大于或等于阈值的像素点时都是边缘点。这种判断不是很合理,会造成边缘点的误判,因为许多噪声点的灰度值也很大。

Scharr 算子说明与使用

Sobel算子算法函数中,如果设置 ksize=-1 就会使用 3x3Scharr滤波器。

它的原理和sobel算子原理一样,只是卷积核不一样,所以精度会更高一点。

该函数的原型如下:

# Sobel 算子算法
dst = cv2.Sobel(src, ddepth, dx, dy[, dst[, ksize[, scale[, delta[, borderType]]]]])
# Scharr 算子算法
dst = cv2.Scharr(src, ddepth, dx, dy[, dst[, scale[, delta[, borderType]]]])

  
 
  • 1
  • 2
  • 3
  • 4

它的参数与 Sobel 基本一致。

测试代码如下,可以比较两种算法的差异:

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def scharr_demo(image): grad_x = cv.Scharr(image, cv.CV_16S, 1, 0) grad_y = cv.Scharr(image, cv.CV_16S, 0, 1) gradx = cv.convertScaleAbs(grad_x) grady = cv.convertScaleAbs(grad_y) # 合并x, y两个梯度 dst = cv.addWeighted(gradx, 0.5, grady, 0.5, 0) return dst

def sobel_demo1(image): dst1 = scharr_demo(image) grad_x = cv.Sobel(image, cv.CV_16S, 1, 0) grad_y = cv.Sobel(image, cv.CV_16S, 0, 1) gradx = cv.convertScaleAbs(grad_x) grady = cv.convertScaleAbs(grad_y) # 合并x, y两个梯度 dst = cv.addWeighted(gradx, 0.5, grady, 0.5, 0) imgs = np.hstack([dst, dst1]) plt.figure(figsize=(20, 10)) plt.imshow(imgs, cmap=plt.cm.gray) plt.show()


def sobel_demo3(image): grad = cv.Sobel(image, cv.CV_16S, 1, 1) grad = cv.convertScaleAbs(grad) cv.imshow('sobel_demo1_gradient', grad)

def sobel_demo2(image): grad_x = cv.Sobel(image, cv.CV_32F, 1, 0) grad_y = cv.Sobel(image, cv.CV_32F, 0, 1) gradx = cv.convertScaleAbs(grad_x) grady = cv.convertScaleAbs(grad_y) cv.imshow('sobel_demo2_gradient_x', gradx) cv.imshow('sobel_demo2_gradient_y', grady) # 合并x, y两个梯度 add_image = cv.addWeighted(gradx, 0.5, grady, 0.5, 0) cv.imshow('sobel_demo2_addWeighted', add_image)

src = cv.imread('./test.jpg', 1)
sobel_demo1(src)
# sobel_demo2(src)
# sobel_demo3(src)
cv.waitKey(0)
cv.destroyAllWindows()

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50

Python OpenCV 之图像梯度,Sobel 算子、Scharr 算子和 laplacian 算子
sobel算子和scharr算子差异

  • sobel算子系数:[1 2 1] ; scharr算子[3 10 3] ;
  • scharr算子要比sobel算子拥有更高的精确度;
  • scharr算子可以把比较细小的边界也检测出来。

laplacian 算子

Laplace函数实现的方法:先用Sobel 算子计算二阶 x 和 y 导数,再求和。

应用层面,我们先看与一下该函数的原型:

dst = cv2.Laplacian(src, ddepth[, dst[, ksize[, scale[, delta[, borderType]]]]])

  
 
  • 1

无特殊参数,可以直接进入测试代码的学习。

import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt

image = cv.imread('./test1.png',cv.IMREAD_GRAYSCALE)

laplacian = cv.Laplacian(image, cv.CV_64F)
laplacian = cv.convertScaleAbs(laplacian)

imgs = np.hstack([image, laplacian])

plt.figure(figsize=(20, 10))
plt.imshow(imgs, cmap=plt.cm.gray)
plt.show()

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

为了测试方便,我将图片提前转换为了灰度图。

Python OpenCV 之图像梯度,Sobel 算子、Scharr 算子和 laplacian 算子

拉普拉斯卷积核,也可以自行进行定义。默认使用的是四邻域 [[0, 1, 0], [1, -4, 1], [0, 1, 0]],修改为八邻域 [[1, 1, 1], [1, -8, 1], [1, 1, 1]]

自定义卷积核代码如下,这里用到了我们之前学习的 2D 卷积操作:

import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt

image = cv.imread('./test1.png',cv.IMREAD_GRAYSCALE)

# 定义卷积和
kernel = np.array([[1, 1, 1], [1, -8, 1], [1, 1, 1]])
dst = cv.filter2D(image, cv.CV_32F, kernel=kernel)
lapalian = cv.convertScaleAbs(dst)

imgs = np.hstack([image, lapalian])

plt.figure(figsize=(20, 10))
plt.imshow(imgs, cmap=plt.cm.gray)
plt.show()

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

Python OpenCV 之图像梯度,Sobel 算子、Scharr 算子和 laplacian 算子
这里还学习到一句话,拉普拉斯对噪声敏感,会产生双边效果,但是不能检测出边的方向。并且它不直接用于边的检测,只起辅助的角色,检测一个像素是在边的亮的一边还是暗的一边利用零跨越,确定边的位置

梯度简单来说就是求导,就是这关键的求导,要去学习数学知识了,本部分在第一遍学习完毕,将会展开学习。梯度在图像上表现出来的就是提取图像的边缘(无论是横向的、纵向的、斜方向的等等),所需要的是一个核模板,模板的不同结果也不同。基于此,上文涉及的算子函数,都能够用函数cv2.filter2D()来表示,不同的方法给予不同的核模板,然后演化为不同的算子。

橡皮擦的小节

希望今天的 1 个小时(今天内容有点多,不一定可以看完),你有所收获,我们下篇博客见~

相关阅读


技术专栏

  1. Python 爬虫 100 例教程,超棒的爬虫教程,立即订阅吧
  2. Python 爬虫小课,精彩 9 讲

逗趣程序员

  1. 曝光,程序员的 10 个摸鱼神器
  2. 5 个无聊 Python 程序,用 Python 整蛊你的朋友们吧
  3. 10 年老程序员教你甩锅必杀技,论【如何优雅的甩锅】
  4. 你想被开除吗?来看看程序员【离职小技巧】吧

今天是持续写作的第 72 / 100 天。
如果你有想要交流的想法、技术,欢迎在评论区留言。


如果你想跟博主建立亲密关系,可以关注同名公众号 梦想橡皮擦,近距离接触一个逗趣的互联网高级网虫。
博主 ID:梦想橡皮擦,希望大家点赞评论收藏

文章来源: dream.blog.csdn.net,作者:梦想橡皮擦,版权归原作者所有,如需转载,请联系作者。

原文链接:dream.blog.csdn.net/article/details/113530930

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。