第9章 图像梯度 -- 9.1 Sobel 算子

《OpenCV 轻松入门 面向Python》 学习笔记

相关链接:

  • Sobel 算子
  • Laplacian 算子 (拉普拉斯算子)

Sobel 算子

      • 1. Sobel 理论基础
          • 1.1 计算水平方向偏导数的近似值
          • 1.2 计算垂直方向偏导数的近似值
      • 2. Sobel 算子及函数使用
          • 参数 ddepth, 输出图像深度
          • 方向 dx, dy

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

严格来讲,图像梯度计算需要求导数,但是图像梯度一般通过计算像素值的差来得到梯度的近似值(近似导数值)


下图左图中的A, B两条线,描述了图像的水平边界。线条A和线条B左右两侧的像素值之差不为0,所以是边界。对于其余列,左右两侧的像素值的差都为0,因此不是边界。

下图右图中的C, D两条线,描述了图像的垂直边界。线条C和线条D上下两侧的像素值之差不为0,所以是边界。对于其余行,上下两侧的像素值的差都为0,因此不是边界。
第9章 图像梯度 -- 9.1 Sobel 算子_第1张图片

在一般图像中,我们想要借助图像梯度来求图像的轮廓,也就是边界。


1. Sobel 理论基础

该算子利用局部差分寻找边缘,计算所得的是一个梯度的近似值。

(“掩模”, “核”, “模版”, “窗口”, “算子” 本质都是一样的,只不过在不同的领域,或者资料中有不同的叫法)
Sobel 算子也称Sobel 滤波器,或者Sobel 核。滤波目标像素点的值 等于 原始像素值及其周围像素值的加权和。这种基于线性的滤波,就是卷积。

第9章 图像梯度 -- 9.1 Sobel 算子_第2张图片

1.1 计算水平方向偏导数的近似值

假设图像中有一点像素P5及其邻域:
第9章 图像梯度 -- 9.1 Sobel 算子_第3张图片
我们想要利用Sobel 算子 计算像素点P5的水平方向偏导数 P 5 x P5_x P5x

公式为: P 5 x = ( P 3 − P 1 ) + 2 ⋅ ( P 6 − P 4 ) + ( P 9 − P 7 ) P5_x = (P3-P1) + 2 \cdot (P6 - P4) + (P9-P7) P5x=(P3P1)+2(P6P4)+(P9P7)

对应的图像示例:
在这里插入图片描述
说明:

  • 用P5像素点右侧的像素减去其左侧像素
  • 其中,像素点P6, P4 距离像素点P5较近,权重为2;P1, P3, P7, P9 距离像素点P5较远,权重为1。权重体现在Sobel 算子的元素值中。

1.2 计算垂直方向偏导数的近似值

想要利用Sobel 算子 计算像素点P5的垂直方向偏导数 P 5 y P5_y P5y,计算方法和求水平方向偏导数类似。

公式为: P 5 y = ( P 7 − P 1 ) + 2 ⋅ ( P 8 − P 2 ) + ( P 9 − P 3 ) P5_y = (P7-P1) + 2 \cdot (P8 - P2) + (P9-P3) P5y=(P7P1)+2(P8P2)+(P9P3)

对应的图像示例:
在这里插入图片描述

说明:

  • 用P5像素点下侧的像素减去其上侧像素
  • 其中,像素点P2, P8 距离像素点P5较近,权重为2;P1, P7, P3, P9 距离像素点P5较远,权重为1。权重体现在Sobel 算子的元素值中。

2. Sobel 算子及函数使用

函数原型:

dst_img = cv2.Sobel(src_img, ddepth, dx, dy, ksize, scale, delta)

参数:

  • dst_img:结果图像
  • src_img:原始图像
  • ddepth:输出图像的深度,
  • dx:x方向上的求导阶数,值为0时,表示在该方向上没有求导
  • dy:y方向上的求导阶数,值为0时,表示在该方向上没有求导
  • ksize:Sobel核的大小。该值为-1时,会使用Scharr算子进行计算。
  • scale:代表计算导数值时的缩放因子。默认值是1,表示没有缩放。
  • delta:代表加到目标图像上的亮度值。默认值是0。

参数 ddepth, 输出图像深度

在函数 cv2.Sobel() 的语法中规定,可以将函数 cv2.Sobel()内参数ddepth的值设置为-1,让结果图像的深度与原始图像保持一致。但是如果直接将参数ddepth的值设置为-1,在计算得到的结果图像可能是错误的。
第9章 图像梯度 -- 9.1 Sobel 算子_第4张图片

指出问题:

上面原始图像是一副二值图像,图中黑色部分的像素值为0, 白色部分的像素值为1。在求水平偏导数时:

  • A线条所在列,右侧像素值减去左侧像素值所得近似偏导数为1
  • B线条所在列,右侧像素值减去左侧像素值所得近似偏导数为-1
  • C线条所在列,下方像素值减去上方像素值所得近似偏导数为-1
  • D线条所在列,下方像素值减去上方像素值所得近似偏导数为1

针对B线条,C线条偏导数为-1的情况,若指定ddepth的值为-1,那么结果图像会是8位图类型(因为通常情况下,原始图像会是8位图类型),所有的负数会自动截断为0,发生信息丢失。

解决问题手段:

如上所述,偏导数截断为0,会导致信息丢失,不是我们想要的。所以,我们要把得到的负数偏导数做绝对值处理。以保证偏导数总能正确的显示出来。
具体操作为:
我们在计算时使用cv2.CV_64F,再通过取绝对值将其映射为cv2.CV_8U(8位图类型)。

语法为:

dst_img = cv2.convertScaleAbs(src_img, alpha, beta)

# 参数:
# - dst_img:结果图像
# - src_img:原始图像
# - alpha:调节系数,默认为1
# - beta:调节亮度值,默认为0

# 函数作用为:
# dst = saturate(src*alpha + beta)
# saturate表示计算结果的最大值取饱和值,若src*alpha + beta<=255, 其值取(src*alpha + beta);若src*alpha + beta>255, 其值取255.

举例:

import cv2
import numpy as np

src_img = np.random.randint(-256, 256, (4, 5), dtype=np.int16)
dst_img = cv2.convertScaleAbs(src_img)

print(src_img)
print('\n', dst_img)


# 输出为:
# [[ -49 -109   98 -173 -189]
#  [  91    5  195  213   86]
#  [   1   10  178  -60    5]
#  [ -50  -79 -180   34    8]]
# 
#  [[ 49 109  98 173 189]
#  [ 91   5 195 213  86]
#  [  1  10 178  60   5]
#  [ 50  79 180  34   8]]


方向 dx, dy

函数cv2.Sobel()中,dx表示x方向上的求导阶数,值为0时,表示在该方向上没有求导;dy表示y方向上的求导阶数,值为0时,表示在该方向上没有求导。参数dx和dy通常值为0或1, dx和dy不能同时为0。

dx 和 dy 可以有多种形式的组合:
计算x方向边缘梯度: dx=1,dy=0
计算y方向边缘梯度: dx=0,dy=1
参数dx和dy的值均为1: dx=1,dy=1
计算x方向和y方向的边缘叠加: 通过组合方式实现

下面一个一个举例说明。

举例 1 (deppth=-1, dx=1,dy=0):

import cv2
import numpy as np

src_img = cv2.imread("/Users/manmi/Desktop/sobel.bmp", 0)
sobel_x = cv2.Sobel(src_img, -1, 1, 0)

cv2.imshow('src_img', src_img)
cv2.imshow('sobel_x', sobel_x)
cv2.waitKey()
cv2.destroyAllWindows()

第9章 图像梯度 -- 9.1 Sobel 算子_第5张图片


举例 2 (deppth=cv2.CV_64F, dx=1,dy=0):

import cv2
import numpy as np

src_img = cv2.imread("/Users/manmi/Desktop/sobel.bmp", 0)
sobel_x = cv2.Sobel(src_img, cv2.CV_64F, 1, 0)
sobel_x = cv2.convertScaleAbs(sobel_x)

cv2.imshow('src_img', src_img)
cv2.imshow('sobel_x', sobel_x)
cv2.waitKey()
cv2.destroyAllWindows()

输出为:
第9章 图像梯度 -- 9.1 Sobel 算子_第6张图片


举例 3(deppth=cv2.CV_64F, dx=0,dy=1):

import cv2
import numpy as np

src_img = cv2.imread("/Users/manmi/Desktop/sobel.bmp", 0)
sobel_y = cv2.Sobel(src_img, cv2.CV_64F, 0, 1)
sobel_y = cv2.convertScaleAbs(sobel_y)

cv2.imshow('src_img', src_img)
cv2.imshow('sobel_y', sobel_y)
cv2.waitKey()
cv2.destroyAllWindows()

输出为:
第9章 图像梯度 -- 9.1 Sobel 算子_第7张图片


举例 4(deppth=cv2.CV_64F, dx=1,dy=1):

import cv2
import numpy as np

src_img = cv2.imread("/Users/manmi/Desktop/sobel.bmp", 0)

sobel_xy = cv2.Sobel(src_img, cv2.CV_64F, 1, 1)
sobel_xy = cv2.convertScaleAbs(sobel_xy)

cv2.imshow('src_img', src_img)
cv2.imshow('sobel_xy', sobel_xy)
cv2.waitKey()
cv2.destroyAllWindows()

输出为:(有几个白色的点,是水平边缘线和垂直边缘线的交点)
第9章 图像梯度 -- 9.1 Sobel 算子_第8张图片


举例 5(deppth=cv2.CV_64F,x方向和y方向的边缘叠加: 通过组合方式实现):

import cv2
import numpy as np

src_img = cv2.imread("/Users/manmi/Desktop/sobel.bmp", 0)

sobel_x = cv2.Sobel(src_img, cv2.CV_64F, 1, 0)
sobel_x = cv2.convertScaleAbs(sobel_x)

sobel_y = cv2.Sobel(src_img, cv2.CV_64F, 0, 1)
sobel_y = cv2.convertScaleAbs(sobel_y)

sobel_xy = cv2.addWeighted(sobel_x, 0.5, sobel_y, 0.5, 0)


cv2.imshow('src_img', src_img)
cv2.imshow('sobel_xy', sobel_xy)
cv2.waitKey()
cv2.destroyAllWindows()

输出为:
第9章 图像梯度 -- 9.1 Sobel 算子_第9张图片


以实际图像举例:

import cv2
import numpy as np

src_img = cv2.imread("/Users/manmi/Desktop/lena.bmp", 0)

# 方法1
sobel_x = cv2.Sobel(src_img, cv2.CV_64F, 1, 0)
sobel_x = cv2.convertScaleAbs(sobel_x)
sobel_y = cv2.Sobel(src_img, cv2.CV_64F, 0, 1)
sobel_y = cv2.convertScaleAbs(sobel_y)
sobel_xy = cv2.addWeighted(sobel_x, 0.5, sobel_y, 0.5, 0)

# 方法2
sobel_xy11 = cv2.Sobel(src_img, cv2.CV_64F, 1, 1)
sobel_xy11 = cv2.convertScaleAbs(sobel_xy11)


cv2.imshow('sobel_xy', sobel_xy)
cv2.imshow('sobel_xy11', sobel_xy11)
cv2.waitKey()
cv2.destroyAllWindows()

输出为:

总结:
用 cv2.Sobel(src_img, cv2.CV_64F, 1, 1) 得到的图像轮廓是水平边界和垂直边界的or,得到的边界值(边界点)较少。
通过x方向和y方向的边缘叠加 得到的图像轮廓是水平边界和垂直边界的and,得到的边界值(边界点)较多。

你可能感兴趣的:(OpenCV,边缘检测,opencv,计算机视觉,机器学习)