图像梯度
要学习图像边缘检测,要先了解图像梯度的概念,我们正是通过梯度值来区分边缘像素点的
处于边缘附近的像素点与周围像素点的差距很大(不然不会有边缘呈现),所以给边缘附近的的梯度之变化很快,通过计算梯度值,来进行边缘检测。
通常有如下两种方式处理计算得到的新值:
- 截断处理:将小于0的值设置为0,将大于255的值设置为255。这种方法简单直接,但可能会导致图像在极端值处出现不自然的截断。
- 归一化处理:将计算得到的值线性映射到0到255的范围内。这种方法可以保留更多的细节信息,但可能需要额外的计算。
梯度处理方式
cv2.filter2D()函数
**功能:**用于对图像进行卷积操作。卷积是图像处理中的一个基本操作,它通过一个称为卷积核(或滤波器)的小矩阵在图像上滑动,并对每个位置进行加权求和,从而得到新的图像。
参数:
- src:输入图像,可以是灰度图像或彩色图像。
- ddepth:输出图像的所需深度。对于输入图像和输出图像具有相同深度的情况,该值通常设置为 -1。否则,你可以选择一个特定的深度,如 cv2.CV_8U、cv2.CV_16U、cv2.CV_32F 等。
- kernel:卷积核,一个二维数组或矩阵。卷积核的大小通常是奇数,如 3x3、5x5 等。卷积核中的每个元素都是一个权重,用于在卷积过程中与图像像素相乘。
- dst:输出图像(可选)。
- anchor:卷积核的锚点(可选)。
- delta:一个可选的附加值,它将被加到卷积结果上。这可以用于调整结果的亮度或对比度。
- borderType:边界填充。
进行边缘检测的方向取决于选取的卷积核kernel
当kernel 取k1 时,检测方向为垂直方向
当kernel 取k2 时,检测方向为水平方向
当然kernel的取值并非只有上面两种,该函数通过应用自定义的卷积核对图像进行滤波处理,可以实现各种线性滤波效果。卷积核可以看作是一种特殊的算子,但本质是函数。
注意:上面的k1,k2 ,正是Sobel算子 (下面会讲)需要使用的卷积核,所以cv2.filter2D()函数选用上面的k1,k2 作为参数kernel 的取值,那么它与Sobel算子的作用效果是一样的。
那么卷积核是如何运作的呢,还跟之前一样要提前填充边缘吗?放心,不需要,计算很简单。
如下图,以垂直方向为例:
卷积核进行操作的是单信道图像,灰度图或二值图,两个极端0(黑),255(白),该卷积核进行操作目的是为进行边缘检测,所以将两个极端进行重新赋值,以便及那个边缘区分开来,下面介绍的Sobel算子,也是相同的操作,不在赘述。
实线框部分是第一次卷积,左右两边(中间列除外)各自与对赢得系数相乘,然后进行相加
(196-247)+2*(199-241)+(35-190)= -290 结果<=0,归零,将中间一列重新赋值为零
卷积核滑动,虚线部分为第二次卷积,
(243-0)+2*(245-0)+(197-0)=930 结果>0,归255,将中间列重新赋值为255
示例代码
python
import cv2
import numpy as np
# 读取一张图
img = cv2.imread("./shudu.png")
# 进行垂直梯度处理
kernel = np.array([
[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]
])
img_filter = cv2.filter2D(img, -1, kernel)
# 进行水平梯度处理
kernel1 = np.array([
[-1, -2, -1],
[0, 0, 0],
[1, 2, 1]
])
img_filter_level = cv2.filter2D(img, -1, kernel1)
cv2.imshow('image', img)
cv2.imshow('img_filter', img_filter)
cv2.imshow('img_filter_level', img_filter_level)
cv2.waitKey(0)
效果对比
Sobel算子
上面的两个卷积核都叫做Sobel算子,只是方向不同,它先在垂直方向计算梯度:
cv2.Sobel()函数
**功能:**用于计算图像梯度(gradient)的函数
参数:
- src: 输入图像,它应该是灰度图像。
- ddepth: 输出图像的所需深度(数据类型)。通常,你可以使用 -1 来表示与输入图像相同的深度,或者使用如 cv2.CV_64F 等来指定特定的深度。由于梯度计算可能产生负值,因此建议使用能够包含负数的数据类型。
- dx: x 方向上的导数阶数。如果你想要计算 x 方向上的梯度,设置这个参数为 1;如果你不关心 x 方向上的梯度,设置这个参数为 0。
- dy: y 方向上的导数阶数。如果你想要计算 y 方向上的梯度,设置这个参数为 1;如果你不关心 y 方向上的梯度,设置这个参数为 0。通常,你不会同时设置 dx 和 dy 都为 0。
- ksize: Sobel 核的大小。它必须是 1、3、5、7 或 9 之一。这个参数决定了用于计算梯度的滤波器的大小。大小为 1 时表示使用最小的滤波器,但通常你会使用更大的滤波器来平滑梯度计算。
- scale: 可选参数,表示计算梯度时的缩放因子。默认值为 1,表示不进行缩放。你可以通过调整这个参数来放大或缩小梯度的结果。
- delta: 可选参数,表示在将结果存储到目标图像之前要添加到结果中的可选增量值。默认值为 0,表示不添加增量。
- borderType: 像素外推方法,例如 cv2.BORDER_DEFAULT、cv2.BORDER_REFLECT 等。这个参数决定了在图像边界处如何处理像素外推。
示例代码
python
import cv2
# 读取一张图
img = cv2.imread("./shudu.png")
# 使用sobel算子
# 水平梯度
img_sobel = cv2.Sobel(img, -1, 0, 1, ksize=3)
# 垂直梯度
img_sobel_2 = cv2.Sobel(img, -1, 1, 0, ksize=3)
cv2.imshow('image', img)
cv2.imshow('img_sobel', img_sobel)
cv2.imshow('img_sobel_2', img_sobel_2)
cv2.waitKey(0)
效果对比
其他算子
Laplacian算子
下面为推导过程,了解即可可直接跳过,我们只关心最后的卷积核
在此基础上考虑斜对角情况,该算子的图像卷积模板如下:
cv2.Laplacian()函数
**功能:**用于计算图像的拉普拉斯算子(Laplacian)
参数:
- src: 输入图像,它应该是灰度图像。
- ddepth: 输出图像的所需深度。这个参数决定了输出图像的深度(数据类型)。通常,你可以使用 -1 来表示与输入图像相同的深度,或者使用 cv2.CV_64F 等来指定特定的深度。由于拉普拉斯算子可能产生负值,因此通常建议使用能够包含负数的数据类型,如 cv2.CV_64F。
- ksize: 算子的大小。它必须是 1、3、5 或 7 之一。这个参数决定了用于计算拉普拉斯算子的滤波器的大小。大小为 1 时表示使用 4 邻域拉普拉斯算子,其他大小则使用更大的滤波器。
- scale: 可选参数,表示计算拉普拉斯算子时的缩放因子。默认值为 1,表示不进行缩放。你可以通过调整这个参数来放大或缩小拉普拉斯算子的结果。
- delta: 可选参数,表示在将结果存储到目标图像之前要添加到结果中的可选增量值。默认值为 0,表示不添加增量。
- borderType: 像素外推方法,例如 cv2.BORDER_DEFAULT、cv2.BORDER_REFLECT 等。这个参数决定了在图像边界处如何处理像素外推。当 ksize 大于 1 时,这个参数才有意义。
示例代码
python
import cv2
# 读取一张图
img = cv2.imread("./shudu.png")
# 使用拉普拉斯算子
img_lap = cv2.Laplacian(img, -1, ksize=3)
cv2.imshow('image', img)
cv2.imshow('img_lap', img_lap)
cv2.waitKey(0)
效果对比
小结
Sobel算子是二阶边缘检测的典型代表
Laplacian算子是二阶边缘检测的典型代表
不过 一 / 二阶边缘检测各有优缺点,大家可自行了解。
图像边缘检测
边缘检测要用到Canny算法 ,Canny边缘检测方法 常被誉为 边缘检测 的最优方法。
首先,Canny算法 处理的是 图像的二值化结果,接收到二值化图像后,需要按照如下步骤进行:
- 高斯滤波。
- 计算图像的梯度和方向。
- 非极大值抑制。
- 双阈值筛选。
下面我来介绍一下这四步
1. 高斯滤波
在前面的文章里历经详细介绍了高斯滤波,遗忘的同学,链接如下:
之前提到过,低通滤波器是模糊 ,高通滤波器是锐化。
而边缘检测本身属于锐化操作,对噪点比较敏感,需要进行平滑处理。所以用到的高斯滤波 为高通滤波器。这里使用的是一个5*5的高斯核对图像进行消除噪声:
2. 计算图像的梯度与方向
2.1 计算梯度
这里使用了Sobel算子(核值固定的卷积核)来计算图像的梯度值,如下所示:
这些是高数中二阶偏导数相关的概念,就不做赘述了。不理解也没关系,重点不在这,接着往下看
2.2 计算方向
这个角度值其实就是当前边缘的梯度的方向,与边缘的方向刚好垂直。
通过这个公式我们就可以计算出图片中所有的像素点的梯度值与梯度方向,然后根据梯度方向获取边缘的方向,获得θ。得到θ的值之后,就可以对边缘方向进行分类,一般将其归为四个方向:
水平方向、垂直方向、45°方向、135°方向:
- 当θ值为-22.5°~22.5°,或-157.5°~157.5°,则认为边缘为水平边缘;
- 当法线方向为22.5°~67.5°,或-112.5°~-157.5°,则认为边缘为45°边缘;
- 当法线方向为67.5°~112.5°,或-67.5°~-112.5°,则认为边缘为垂直边缘;
- 当法线方向为112.5°~157.5°,或-22.5°~-67.5°,则认为边缘为135°边缘;
3. 非极大值抑制
通过上面的操作,已经初步筛选出了边缘,把他们连起来不久OK了,齐活?
NO,NO,NO!本系列第10篇提到过,锐化都容易损坏边缘信息,使边缘模糊,导致经过第二步后得到的边缘像素点非常多,因此我们需要对其进行一些过滤操作。其中非极大值抑制就是一个很好的方法。
在边缘检测中,非极大值抑制的主要目的是细化边缘。具体来说,它通过对梯度图像中的像素值进行比较和筛选,只保留梯度方向上局部最大的像素值,而将其他非最大的像素值抑制为零。这样,边缘就变得更加细化和清晰,减少了冗余的边缘信息。假设当前像素点为(x,y),其梯度方向是0°,梯度值为G(x,y),那么我们就需要比较G(x,y)与两个相邻像素的梯度值:G(x-1,y)和G(x+1,y)。如果G(x,y)是三个值里面最大的,就保留该像素值,否则将其抑制为零。
并且如果梯度方向不是0°、45°、90°、135°这种特定角度,那么就要用到插值算法来计算当前像素点在其方向上进行插值的结果了,然后进行比较并判断是否保留该像素点。这里使用的是单线性插值,通过A1和A2两个像素点获得dTmp1与dTmp2处的插值,然后与中心点C进行比较。
4. 双阈值筛选
经过非极大值抑制之后,我们还需要设置阈值来进行筛选。
- 当阈值设的太低,就会出现假边缘
- 而阈值设的太高,一些较弱的边缘就会被丢掉
因此使用了双阈值来进行筛选,推荐高低阈值的比例为2 : 1到3 : 1之间,其原理如下图所示:
- 当某一像素位置的幅值超过最高阈值时,该像素必是边缘像素;
- 幅值处于最高像素与最低像素之间时,如果它能连接到一个高于阈值的边缘时,则被认为是边缘像素,否则就不会被认为是边缘;
- 当幅值低于最低像素时,该像素必不是边缘像素。
也就是说,上图中的A和C是边缘,B不是边缘。因为C虽然不超过最高阈值,但其与A相连,所以C就是边缘。
至此,Canny边缘检测就完成了。
cv2.Canny()函数
**功能:**用于边缘检测的函数
参数:
- image: 输入图像,它应该是一个灰度图像(单通道)。
- threshold1: 第一个阈值,用于边缘检测的滞后过程。这个值较低,用于确定边缘的初始点。
- threshold2: 第二个阈值,用于边缘检测的滞后过程。这个值较高,用于确定边缘的最终点。如果某个像素点的梯度值高于这个阈值,它被认为是边缘;如果低于这个值但高于threshold1,并且与高于threshold2的像素点相连,它也被认为是边缘。
- edges: 输出图像,与输入图像大小相同,但通常是二值图像(即只包含边缘和非边缘的像素)。
- apertureSize(可选,默认为3): Sobel算子的大小,它决定了梯度计算的邻域大小。它必须是1、3、5或7之一。
- L2gradient(可选,默认为False): 一个布尔值,指示是否使用更精确的L2范数进行梯度计算。如果为True,则使用L2范数(即欧几里得距离);如果为False,则使用L1范数(即曼哈顿距离)。L2范数通常更精确,但计算成本也更高。
示例代码
python
import cv2
img = cv2.imread("kabuto.jpg")
# 灰度化
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 二值化
_, img_binary = cv2.threshold(img_gray, 127, 255,
cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# 进行高斯滤波
img_blur = cv2.GaussianBlur(img_binary, (3,3), 3)
# 边缘检测
img_canny = cv2.Canny(img_blur, 10, 70)
cv2.imshow('img', img)
cv2.imshow('img_canny', img_canny)
cv2.waitKey(0)