第九章: 图像梯度
图像梯度是用来做边缘检测的一种方法。
为什么要检测边缘?
比如自动驾驶里面,我们至少要做的一个工作就是道路的边缘检测,只有正确的检测到道路的边缘我们的车才会行驶在道路上而不是开到马路牙子外。
或者从另一个角度解释,我们做边缘检测不是让人眼去欣赏一张道路图片里面的道路边缘的,我们正确检测出一张图像的边缘是为了让模型更好的去认识这张图片中的道路。所以精确的边缘检测可以帮助电脑模型很好的识别这是道路还是道路外面,从而做出正确的反馈——指导汽车正确行使。
- 图像梯度的原理:
梯度是微积分中的概念,就是导数,表达式是:
其几何意义就是,当自变量x或者y在各自的方向上改变一点点,函数值z随之改变了多少。导数值有大小有方向。
当我们把导数的概念引入图像处理中,我们就简化成当像素点在x轴或者y轴跳动一步,就是移动一个像素点,我们像素值的改变量,我们把这个该变量叫做梯度。
公式是:
可视化后是:
说得更通俗一点就是:图像梯度就是图像中左右像素点或者上下像素点之间的差值。所以,梯度值越大就表示这个像素点左右或者上下像素之间的差值越大。所以,差值越大越大越说明这个像素点是图像中的边缘信息。所以,提取了图像中的梯度,就是提取了图像的轮廓,就是图像边缘检测的操作。
- 如何提取图像的梯度值
首先要说明的是,我们之前将的各种滤波器、掩膜、掩码、核、卷积核、模板、窗口、算子等,其实都是一个东西。在信号领域称为滤波器,在数学里称为核、算子,在图像处理领域称为掩膜、掩码,在深度学习领域称为卷积核,在本章的梯度提取中我们称为算子。
根据上面原理的解释,最直接的提取图像梯度的方法就是,分别进行水平方向梯度的提取和垂直方向梯度的提取。其中,水平方向梯度就用水平方向上后一个像素点值-前一个像素点值即可。垂直方向同理。
但是这种提取方法太简单粗暴,完全不考虑邻域的像素点,如果我们把邻域像素点也考虑进来,就是用本章之前讲的各式各样的kernel对原图像进行卷积操作了。
而这其中最著名的提取算子有:sobel算子、scharr算子、laplacian算子。由于参考教材的本章就是讲这三个算子,所以本课件也按照教材的思路。
一、Sobel算子
问题:
1、用这个算子和原图进行卷积运算后会出现负值。
2、用这个算子和原图进行卷积运算后会出现绝对值大于255的情况。
- opencv中,实现sobel算子运算的API:
cv2.Sobel(img, ddepth, dx, dy)
img: 要运算的原图
ddepth: 输出图像的表示位数,默认-1,表示输出的是8位图类型。但是!!!这个参数一般要设置为cv2.CV_64F,因为我们在用sobel算子进行运算时会经常出现负数和绝对值大于255的情况。
如果我们把参数ddepth设置为-1,当出现负数时函数就就直接返回0值,但是负数也代表是梯度呀,这不是我们想要的结果。当出现大于255的情况就直接返回255,这种情况这样处理是没问题。
所以我们把ddepth设置为cv2.CV_64F后,原图和sobel算子进行卷积运算后,返回结果就可以是64位的一个数值,就是[-255, 255],这样如果出现负梯度,我们就把负梯度也保留了,此时我们再用
cv2.convertScaleAbs()函数把负梯度变正,就是我们的边缘信息了。
dx:表示在x方向上的求导阶数
dy:表示在y方向上的求导阶数
- 取绝对值函数cv2.convertScaleAbs()
#例9.1 练习取绝对值函数 import cv2 import numpy as np num = np.random.randint(-255, 256, (4,5), np.int16) num num_abs = cv2.convertScaleAbs(num) num_abs
array([[ -52, -216, -113, 163, -31], [ -91, -107, -93, 89, 91], [ -44, -112, -8, 248, 84], [-125, 97, -135, 252, -10]], dtype=int16) array([[ 52, 216, 113, 163, 31], [ 91, 107, 93, 89, 91], [ 44, 112, 8, 248, 84], [125, 97, 135, 252, 10]], dtype=uint8)
#例9.2 使用sobel算子获取图像的边缘信息 import cv2 import numpy as np import matplotlib.pyplot as plt img = cv2.imread(r'C:\Users\25584\Desktop\sobel4.bmp') img_sobel_x1 = cv2.Sobel(img, -1, 1, 0) #用8位图,如果出现负数,直接让负数等于0 img_sobel_x11 = cv2.Sobel(img, cv2.CV_64F, 1, 0) #用64位图,这样图片像素的取值范围就在[-255, 255]之间 img_sobel_x11 = cv2.convertScaleAbs(img_sobel_x11) img_sobel_y1 = cv2.Sobel(img, -1, 0, 1) img_sobel_y11 = cv2.Sobel(img, cv2.CV_64F, 0, 1) img_sobel_y11 = cv2.convertScaleAbs(img_sobel_y11) img_sobel_x_y = cv2.addWeighted(img_sobel_x11, 1, img_sobel_y11, 1, 0) #cv2.addWeighted(img1, alpha, img2, beta, gamma) fig, axes = plt.subplots(1,6, figsize=(14,6), dpi=100) axes[0].imshow(img) axes[1].imshow(img_sobel_x1) axes[2].imshow(img_sobel_x11) axes[3].imshow(img_sobel_y1) axes[4].imshow(img_sobel_y11) axes[5].imshow(img_sobel_x_y) plt.show()
#例9.3 使用sobel算子获取lena的边缘信息 import cv2 import numpy as np import matplotlib.pyplot as plt lena = cv2.imread(r'C:\Users\25584\Desktop\lena.bmp', 0) lenax = cv2.Sobel(lena, cv2.CV_64F, 1, 0) lenay = cv2.Sobel(lena, cv2.CV_64F, 0, 1) lenax = cv2.convertScaleAbs(lenax) lenay = cv2.convertScaleAbs(lenay) lenaxy = cv2.addWeighted(lenax, 0.5, lenay, 0.5, 0) fig, axes = plt.subplots(1,4, figsize=(10,6), dpi=100) axes[0].imshow(lena, cmap='gray') axes[1].imshow(lenax, cmap='gray') axes[2].imshow(lenay, cmap='gray') axes[3].imshow(lenaxy, cmap='gray') plt.show()
二、Scharr算子
Scharr算子和Sobel算子的原理一模一样,但是为什么还要Scharr算子?因为sobel算子结构较小,计算精度不高,所以科学家又找出一个精度高的算子就是scharr算子。
#例9.4 对比sobel算子和scharr算子的效果 import cv2 import numpy as np import matplotlib.pyplot as plt lena = cv2.imread(r'C:\Users\25584\Desktop\lena.bmp', 0) lenax = cv2.Sobel(lena, cv2.CV_64F, 1, 0) lenay = cv2.Sobel(lena, cv2.CV_64F, 0, 1) lenax = cv2.convertScaleAbs(lenax) lenay = cv2.convertScaleAbs(lenay) lenaxy = cv2.addWeighted(lenax, 0.5, lenay, 0.5, 0) lenax1 = cv2.Scharr(lena, cv2.CV_64F, 1, 0) lenay1 = cv2.Scharr(lena, cv2.CV_64F, 0, 1) lenax1 = cv2.convertScaleAbs(lenax1) lenay1 = cv2.convertScaleAbs(lenay1) lenaxy1 = cv2.addWeighted(lenax1, 0.5, lenay1, 0.5, 0) fig, axes = plt.subplots(1,3, figsize=(10,6), dpi=100) axes[0].imshow(lena, cmap='gray') axes[1].imshow(lenaxy, cmap='gray') axes[2].imshow(lenaxy1, cmap='gray') plt.show()
三、Laplacian算子
-
laplacian算子是一个二阶导数算子,具有旋转不变性,可以满足不同方向的图像边缘检测的要求。
-
什么是二阶导数算子?
-
从上面的图像说明和数学公式说明,我们可以看出:
1、前面讲的sobel算子和scharr算子(一阶运算)运算的结果是黄色的图像,黄色的图像就是一个方向上的像素值差的结果,这个结果就是原图(蓝色像素)图像的轮廓。但是,这个轮廓不是很细腻,因为左边可能是噪声,此时噪声的边缘也被检测出来了。
2、我们从图像上看,二阶运算出来的图像是红色的图像,从红色的图像上看,是不是在梯度明显的地方,二阶运算结果的数值要明显比一阶运算结果的数值要大,并且在梯度不明显的左边,二阶运算的结果比一阶运算结果要小很多,多数都是0,这说明用二阶运算是不是更能提取边缘!也说明二阶运算对原图的边缘响应更敏感!也说明二阶运算提取图像边缘效果比一阶运算效果好! -
为啥旋转不变性?为啥是不同方向的边缘检测?
#例9.5 对比sobel算子和scharr算子、laplacian算子的效果 import cv2 import numpy as np import matplotlib.pyplot as plt lena = cv2.imread(r'C:\Users\25584\Desktop\lena.bmp', 0) lenax = cv2.Sobel(lena, cv2.CV_64F, 1, 0) lenay = cv2.Sobel(lena, cv2.CV_64F, 0, 1) lenax = cv2.convertScaleAbs(lenax) lenay = cv2.convertScaleAbs(lenay) lena_sobel = cv2.addWeighted(lenax, 0.5, lenay, 0.5, 0) lenax1 = cv2.Scharr(lena, cv2.CV_64F, 1, 0) lenay1 = cv2.Scharr(lena, cv2.CV_64F, 0, 1) lenax1 = cv2.convertScaleAbs(lenax1) lenay1 = cv2.convertScaleAbs(lenay1) lena_scharr = cv2.addWeighted(lenax1, 0.5, lenay1, 0.5, 0) lena_laplacian = cv2.Laplacian(lena, cv2.CV_64F) lena_laplacian = cv2.convertScaleAbs(lena_laplacian) fig, axes = plt.subplots(1,4, figsize=(10,6), dpi=100) axes[0].imshow(lena, cmap='gray') axes[1].imshow(lena_sobel, cmap='gray') axes[2].imshow(lena_scharr, cmap='gray') axes[3].imshow(lena_laplacian, cmap='gray') plt.show()