《OpenCV 轻松入门 面向Python》 学习笔记
相关链接:
图像梯度计算的是图像变化的速度。对于图像边缘的部分,其灰度值变化较大,梯度值也较大。相反,图像中比较平滑的部分,其灰度值较小,梯度值也较小。
严格来讲,图像梯度计算需要求导数,但是图像梯度一般通过计算像素值的差来得到梯度的近似值(近似导数值)
下图左图中的A, B两条线,描述了图像的水平边界。线条A和线条B左右两侧的像素值之差不为0,所以是边界。对于其余列,左右两侧的像素值的差都为0,因此不是边界。
下图右图中的C, D两条线,描述了图像的垂直边界。线条C和线条D上下两侧的像素值之差不为0,所以是边界。对于其余行,上下两侧的像素值的差都为0,因此不是边界。
在一般图像中,我们想要借助图像梯度来求图像的轮廓,也就是边界。
该算子利用局部差分寻找边缘,计算所得的是一个梯度的近似值。
(“掩模”, “核”, “模版”, “窗口”, “算子” 本质都是一样的,只不过在不同的领域,或者资料中有不同的叫法)
Sobel 算子也称Sobel 滤波器,或者Sobel 核。滤波目标像素点的值 等于 原始像素值及其周围像素值的加权和。这种基于线性的滤波,就是卷积。
假设图像中有一点像素P5及其邻域:
我们想要利用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=(P3−P1)+2⋅(P6−P4)+(P9−P7)
想要利用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=(P7−P1)+2⋅(P8−P2)+(P9−P3)
说明:
函数原型:
dst_img = cv2.Sobel(src_img, ddepth, dx, dy, ksize, scale, delta)
参数:
在函数 cv2.Sobel() 的语法中规定,可以将函数 cv2.Sobel()内参数ddepth的值设置为-1,让结果图像的深度与原始图像保持一致。但是如果直接将参数ddepth的值设置为-1,在计算得到的结果图像可能是错误的。
指出问题:
上面原始图像是一副二值图像,图中黑色部分的像素值为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]]
函数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()
举例 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()
举例 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()
举例 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()
举例 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()
以实际图像举例:
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,得到的边界值(边界点)较多。