检测原理
边缘检测是指检测图像中的一些像素点,它们周围的像素点的灰度发生了急剧的变化,因此可以将这些像素点作为一个集合,用于标注图像中不同物体的边界。
边缘是图像上灰度级变化很快的点的集合。这些点的梯度往往很大。因此我们可以使用一阶导数和二阶偏导数来进行求解,但图像是离散的数据并以矩阵的形式存储,并不能像数学理论中对直线或曲线一样求导,所以我们使用差分来近似微分,采用不同的差分模板来对原图像进行卷积运算进而实现对图像求导。
导数算子卷积模板推导
这里,我们以一阶导数算子Prewitt和二阶导数算子Laplacian的卷积模板为例 进一步推导说明其由来。
我们知道,一个函数的一阶与二阶导数可以表示为:
那么对于离散的函数,其一阶差分为:
……(1)
……(2)
(1)式-(2)式我们可以得到其二阶差分为:
……(3)
(1)式+(2)式可以得到:
……(4)
这里要注意的是,(4)式并不是标准意义上的一阶差分,不过,其可看做是一种"中心差分"的形式,这里我们也近似把它看做一阶差分。
实际上,这种中心差分的形式主要是为了和我们图像中最常用的3x3卷积模板相配合使用。
图像坐标系
Prewitt算子
prewitt模板分为x与y两个方向。
Prewitt算子在x,y方向上的卷积模板
这里我们设模板中心点的坐标为 ,那么其在x方向上的一阶差分利用(4)式可以近似表示为:
提取系数-1,0,1并应用到3x3模板中的每一行便得到了Prewiitt算子在x方向上的卷积模板。
同理,将x变换为y后应用到3x3模板中的每一列便得到了Prewitt算子在y方向上的卷积模板。
Laplacian算子
Laplacian算子模版同时考虑x与y两个方向
图(a) 图(b)
因此其在差分时是二元差分,我们设3x3卷积模板中心坐标为(x,y),那么在4邻域范围内
对于,其二阶差分结果为:
提取系数并填入3x3卷积模板中便得到了图(a)所示的Laplacian卷积模板,同时考虑到对角线上的元素,我们也进行二阶差分,然后便得到了如图(b)更通用的Laplacian算子。
Laplacian卷积模板有个特点是中心处为负,周边为正,且模板内所有元素之和为0。
常见算子
一阶导数算子
Roberts(重点关注斜对角线)
Prewitt
Sobel(Prewitt基础上给予中心位置更大权重)
对于一阶导数算子而言,我们最后还需要使用不同的范数来计算其在x,y方向上梯度矢量的幅度。一般而言,我们可以使用L2范数,此时幅度值为:
其中,,
分别为一阶导数算子在x,y方向上的矢量。
Canny算子
Canny算子的计算步骤如下:
- 用高斯滤波器平滑图像,去除噪声。
- 用一阶差分偏导计算梯度方向和幅度(Sobel算子)。
- 对梯度值不是极大值的地方进行抑制,将不是机制的点全部置0,去掉大部分的边缘,所以图像边缘会变细。
二阶导数算子
Laplacian算子
Laplacian算子卷积模板
Mar算子
Marr算子通常由两部分组成:一个高斯滤波器用于平滑图像,减少噪声的影响;一个差分算子用于检测亮度变化。在每个像素点上进行如下计算:
- 用一个2D的高斯平滑模板与原图像卷积。
- 计算卷积后图像的拉普拉斯值。
- 检测图像中的过零点,将其作为边缘点。
LOG算子
LOG算子的全称是Laplacian of Gaussian,即高斯拉普拉斯算子。它结合了拉普拉斯算子(用于边缘增强)和高斯滤波器(用于去噪的特点)
常用边缘检测算子检测特点比较
cv2实现边缘检测
注意上述表格中提到的边缘检测算子,只有Sobel,Canny,Laplacian是cv2内置的,其余的算子需要我们手动实现。
手动实现的原理也很简单,就是我们自己定义卷积核然后使用cv2.filter2D()函数进行卷积。
注意事项:
上述几种模板的卷积核部分含有负数,读取进来的图像原始数据默认是cv2.CV_8U(8位无符号整数),卷积后的图像梯度值可能为负数或超出0~255范围,若使用该类型,这些梯度值将被截断。 因此,我们需要在这些算子或卷积运算函数内部指定图像深度ddepth为cv2.CV_64F 或cv2.CV_32F(32位或64位浮点数)。
在进行运算前,我们先介绍几个函数,以及计算结果的处理方式。
cv2.convertScaleAbs()函数:
- 用于自动处理截断和类型转换。
- 处理方式:按照alpha与beta的值,对图像数据进行线性变换。
- 然后使用绝对值运算将负数转为正数,若大于255则截断至255,否则保留。
参数详解:
src | 原始图像数据 |
alpha | 线性变化斜率,默认值为1 |
beta | 线性变化截距,默认值为0 |
cv2.normalize()函数
- 用于保留全局比例,无信息丢失的进行标准化操作(0~255)
- 处理方式:默认使用cv2.NORM_L2,若要放缩至0~255需要使用指定norm_type为cv2.NORM_MINMAX使用MinMax最小最大值放缩。
参数详解:
src | 原始图像数据 |
alpha | 当norm_type为cv2.NORM_MINMAX时是MIN最小值,当norm_type是其他类型时,是最后运算结果前的系数。 |
beta | 只在norm_type为cv2.NORM_MINMAX时有效,是MAX最大值 |
dtype | 图像数据类型,通常是cv2.CV_8U(8为无符号整数) |
norm_type | 指定的标准化类型 |
mask | 一个掩码蒙版,与原图像大小一致内部只有0,1两种数字,只有值为1的地方参与标准化操作,值为0的位置不参与标准化操作。默认为None,此时所有元素参与标准化操作。 |
norm_type类型:
cv2.NORM_MINMAX |
将数据线性映射到指定范围(如 0-255)。默认是 alpha 和 beta 指定范围 |
cv2.NORM_L1 |
每个元素除以L1 范数归一化后乘以alpha。L1范数是所有元素的绝对值之和 |
cv2.NORM_L2 |
每个元素除以 L2 范数归一化后乘以alpha。L2范数是所有元素平方和的平方根 |
cv2.NORM_INF |
每个元素除以无穷范数将数据据归一化后乘以alpha。无穷范数是所有元素中的最大值 |
cv2filter2D()函数:
用于实现二维图像的卷积运算。
参数详解:
src |
原始图像数据 |
ddepth |
指定卷积运算时的图像深度,通常会选择cv2.CV_32F或cv2.CV_64F |
kernal |
自定义卷积核矩阵,使用numpy自定义即可,必须是3x3,5x5奇数大小 |
anchor |
锚点,默认在卷积核中心点 |
delta |
偏移量,加在卷积运算的结果上 |
borderType |
指定边界像素的填充方式,与cv2.copyMakeBorder()函数中的bordertype一致 |
cv2.magnitude()函数:
使用L2范数计算X,Y方向的梯度矢量合成的幅值。
相当于np.sqrt(x**2+y**2)
Roberts交叉算子实现
代码:
#Roberts算子使用cv2.filter2D()函数实现
'''
cv2.filter2D()函数参数详解:
src:原始图像数据
ddepth:指定卷积运算时的图像深度,通常会选择cv2.CV_32F或cv2.CV_64F
kernal:自定义卷积核矩阵,使用numpy自定义即可,必须是3x3,5x5奇数大小
anchor:锚点,默认在卷积核中心点
delta:偏移量,加在卷积运算的结果上
borderType:指定边界像素的填充方式,与cv2.copyMakeBorder()函数中的bordertype一致
'''
'''
cv2.normalize()函数参数详解:
src:原始图像数据
alpha:当norm_type为cv2.NORM_MINMAX时是MIN最小值,当norm_type是其他类型时,是最后运算结果前的系数。
beta:只在norm_type为cv2.NORM_MINMAX时有效,是MAX最大值
dtype:图像数据类型,通常是cv2.CV_8U(8为无符号整数)
norm_type:指定的标准化类型
mask:一个掩码蒙版,与原图像大小一致内部只有0,1两种数字,只有值为1的地方参与标准化操作,值为0的位置不参与标准化操作。默认为None,
此时所有元素参与标准化操作。
norm_type:
cv2.NORM_MINMAX:将数据线性映射到指定范围(如0-255)默认是alpha和beta指定范围。
cv2.NORM_L1:每个元素除以L1范数归一化后乘以alpha。L1范数是所有元素的绝对值之和。
cv2.NORM_L2:每个元素除以L2范数归一化后乘以alpha。L2范数是所有元素平方和的平方根
cv2.NORM_INF:每个元素除以无穷范数将数据据归一化后乘以alpha。无穷范数是所有元素中的最大值。
'''
'''
cv2.convertScaleAbs()函数参数详解:
src:原始图像数据
alpha:线性变化斜率,默认值为1
beta:线性变化截距,默认值为0
'''
import cv2
import numpy as np
image=cv2.imread('ultraman.jpg',cv2.IMREAD_GRAYSCALE)
image=cv2.resize(image,dsize=(500,500))
#自定义的Roberts卷积核
RobertsKernelX=np.array([[1,0],[0,-1]])
RobertsKernelY=np.array([[0,1],[-1,0]])
#两个方向的梯度
GradX=cv2.filter2D(src=image,ddepth=cv2.CV_64F,kernel=RobertsKernelX)
GradY=cv2.filter2D(src=image,ddepth=cv2.CV_64F,kernel=RobertsKernelY)
Roberts_magnitude=cv2.magnitude(GradX,GradY)#计算幅值,使用L2范数
#使用MINMAX幅值归一化
Roberts_magnitude=cv2.normalize(src=Roberts_magnitude,dst=None,alpha=0,beta=255,norm_type=cv2.NORM_MINMAX,dtype=cv2.CV_8U)
#对于X,Y方向来说其梯度存在负值,但magnitude不存在负值,因为其运算是根号下平方和
#因此对于magnitude我们使用cv2.normalzie()线性映射至(0,255)归一化即可
#,但是X,Y方向梯度运算结果存在负值,我们直接使用normalize()会导致其将负值也映射到(0,255)区间,
#比如 [-100, 100]使用cv2.normalize()将其映射到[0, 255]结果中负值会被映射到[0,127.5],正值会被映射到[127.5,255]
#这可能会造成图像显示异常,因此对于X,Y方向上的梯度我们直接使用convertScaleAbs取绝对值即可
RobertsX=cv2.convertScaleAbs(src=GradX)
RobertsY=cv2.convertScaleAbs(src=GradY)
cv2.imshow('SRC',image)
cv2.imshow('RobertsX',RobertsX)
cv2.imshow('RobertsY',RobertsY)
cv2.imshow('RobertsMagnitude',Roberts_magnitude)
cv2.waitKey(0)
结果:
Prewitt算子实现
代码:
#Prewitt算子使用cv2.filter2D()函数实现
import cv2
import numpy as np
image=cv2.imread('ultraman.jpg',cv2.IMREAD_GRAYSCALE)
image=cv2.resize(image,dsize=(500,500))
#自定义的Prewitt卷积核
PrewittKernelX=np.array([[-1,0,1],[-1,0,1],[-1,0,1]])
PrewittKernelY=np.array([[1,1,1],[0,0,0],[-1,-1,-1]])
#两个方向的梯度
GradX=cv2.filter2D(src=image,ddepth=cv2.CV_64F,kernel=PrewittKernelX)
GradY=cv2.filter2D(src=image,ddepth=cv2.CV_64F,kernel=PrewittKernelY)
Prewitt_magnitude=cv2.magnitude(GradX,GradY)#计算幅值,使用L2范数
#使用MINMAX幅值归一化
Prewitt_magnitude=cv2.normalize(src=Prewitt_magnitude,dst=None,alpha=0,beta=255,norm_type=cv2.NORM_MINMAX,dtype=cv2.CV_8U)
#对于X,Y方向来说其梯度存在负值,但magnitude不存在负值,因为其运算是根号下平方和
#因此对于magnitude我们使用cv2.normalzie()线性映射至(0,255)归一化即可
#,但是X,Y方向梯度运算结果存在负值,我们直接使用normalize()会导致其将负值也映射到(0,255)区间,
#比如 [-100, 100]使用cv2.normalize()将其映射到[0, 255]结果中负值会被映射到[0,127.5],正值会被映射到[127.5,255]
#这可能会造成图像显示异常,因此对于X,Y方向上的梯度我们直接使用convertScaleAbs取绝对值即可
PrewittX=cv2.convertScaleAbs(src=GradX)
PrewittY=cv2.convertScaleAbs(src=GradY)
cv2.imshow('SRC',image)
cv2.imshow('PrewittX',PrewittX)
cv2.imshow('PrewittY',PrewittY)
cv2.imshow('PrewittMagnitude',Prewitt_magnitude)
cv2.waitKey(0)
结果:
Sobel算子实现
#边缘检测Sobel算子cv2.Sobel()
'''
cv2.Sobel()函数参数详解:
src:原始图像数据。
ddepth:图像深度,计算梯度的幅值,通常会选择 cv2.CV_32F或cv2.CV_64F。
因为梯度值可能为负数使用无符号整数类型会导致负数被截断。
dx:x方向上的导数阶数,即Sobel算子在x方向上的差分阶数。取值为非负整数,常见的取值有0、1、2。
dy:y方向上的导数阶数,即Sobel算子在y方向上的差分阶数。取值为非负整数,常见的取值有0、1、2。
ksize:Sobel算子的大小。即卷积核的大小。必须是正奇数,常见的取值有1、3、5、7等。特别的,当ksize=1时
Sobel算子退化为简单的差分算子。
scale:对结果进行放缩。
delta:用于在计算结果上加上一个常数偏移量。默认值为None,不添加偏移量。如果指定了该参数,计算结果会在返回之前加上这个值。
bodertype:指定边界像素的填充方式,与cv2.copyMakeBorder()函数中的bordertype一致。
'''
import cv2
image=cv2.imread('ultraman.jpg',cv2.IMREAD_GRAYSCALE)
image=cv2.resize(image,dsize=(500,500))#变换一下大小
#使用cv2.64F是为了确保梯度计算过程中保留了所有负值和高幅值信息。
GradX=cv2.Sobel(src=image,ddepth=cv2.CV_64F,dx=1,dy=0,ksize=3)
GradY=cv2.Sobel(src=image,ddepth=cv2.CV_64F,dx=0,dy=1,ksize=3)
Sobel_magnitude=cv2.magnitude(GradX,GradY)#计算幅值,相当于np.sqrt(x**2+y**2)
Sobel=cv2.normalize(Sobel_magnitude,None,alpha=0,beta=255,norm_type=cv2.NORM_MINMAX,dtype=cv2.CV_8U)
#对于X,Y方向来说其梯度存在负值,但magnitude不存在负值,因为其运算是根号下平方和
#因此对于magnitude我们使用cv2.normalzie()线性映射至(0,255)归一化即可
#,但是X,Y方向梯度运算结果存在负值,我们直接使用normalize()会导致其将负值也映射到(0,255)区间,
#比如 [-100, 100]使用cv2.normalize()将其映射到[0, 255]结果中负值会被映射到[0,127.5],正值会被映射到[127.5,255]
#这可能会造成图像显示异常,因此对于X,Y方向上的梯度我们直接使用convertScaleAbs取绝对值即可
Sobel_x=cv2.convertScaleAbs(src=GradX)
Sobel_y=cv2.convertScaleAbs(src=GradY)
cv2.imshow('SRC_image',image)
cv2.imshow('Sobel X',Sobel_x)
cv2.imshow('Sobel Y',Sobel_y)
cv2.imshow('Sobel Magnitude',Sobel)
cv2.waitKey(0)
结果:
Canny算子实现
代码:
#边缘检测cv2.Canny()算子
'''
cv2.Canny()函数参数详解:
image:原始图像数据
threshold1:第一个阈值(低阈值),用于边缘检测的滞后阈值处理
threshold2:第二个阈值(高阈值),用于边缘检测的滞后阈值处理
aperturesize:Sobel算子的卷积核大小,用于计算图像的梯度幅值和方向
L2gradient:是否使用L2范数计算梯度幅值即cv2.magnitude()函数。
'''
#这里需要注意的是threshold1与threshold2一般为2倍时效果明显
#因此我们可以使用cv2.threshold()函数自动计算threashold2,然后threshold1=0.5*threshold1
import cv2
bgr_image=cv2.imread('ultraman.jpg')
bgr_image=cv2.resize(src=bgr_image,dsize=(500,500))
# 计算图像的直方图并应用 Otsu'阈值
gray_image=cv2.cvtColor(bgr_image,cv2.COLOR_BGR2GRAY)
#使用的是cv2.THRESH_BINARY+cv2.THRESH_OTSU自动计算阈值
thresh,_= cv2.threshold(gray_image, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)
high_thresh=thresh
low_thresh=0.5 * high_thresh
Canny_image=cv2.Canny(image=gray_image,threshold1=low_thresh,threshold2=high_thresh,apertureSize=3,L2gradient=True)
cv2.imshow('BgrImage',bgr_image)
cv2.imshow('GrayImage',gray_image)
cv2.imshow('CannyImage',Canny_image)
cv2.waitKey(0)
cv2.destroyAllWindows()
结果:
Laplacian算子实现
代码:
#边缘检测cv2.Laplacian()算子
'''
cv2.Canny()函数参数详解:
image:原始图像数据
threshold1:第一个阈值(低阈值),用于边缘检测的滞后阈值处理
threshold2:第二个阈值(高阈值),用于边缘检测的滞后阈值处理
aperturesize:Sobel算子的卷积核大小,用于计算图像的梯度幅值和方向
L2gradient:是否使用L2范数计算梯度幅值即cv2.magnitude()函数。
'''
import cv2
bgr_image=cv2.imread('ultraman.jpg')
bgr_image=cv2.resize(src=bgr_image,dsize=(500,500))
gray_image=cv2.cvtColor(bgr_image,cv2.COLOR_BGR2GRAY)
Laplacian_image=cv2.Laplacian(src=gray_image,ddepth=cv2.CV_64F,dst=None,ksize=3,scale=1,delta=0)
#laplacian算子是二阶导数算子,它同时卷积x与y反向不需要计算幅值,其结果中含有负数
#我们直接使用normalize()会导致其将负值也映射到(0,255)区间,
#比如[-100, 100]使用cv2.normalize()将其映射到[0, 255]结果中负值会被映射到[0,127.5],正值会被映射到[127.5,255]
#这可能会造成图像显示异常,因此我们直接使用convertScaleAbs取绝对值即可
Laplacian_image=cv2.convertScaleAbs(src=Laplacian_image)
cv2.imshow('BgrImage',bgr_image)
cv2.imshow('GrayImage',gray_image)
cv2.imshow('LaplacianImage',Laplacian_image)
cv2.waitKey(0)
cv2.destroyAllWindows()
结果:
LOG算子实现
代码:
#LOG算子实现边缘检测
import cv2
image=cv2.imread('ultraman.jpg', cv2.IMREAD_GRAYSCALE)
image=cv2.resize(image,dsize=(500,500))
#高斯滤波(降噪)
sigma=2.0#高斯核标准差
blurred=cv2.GaussianBlur(image, (0, 0), sigma)
#使用Laplacian算子计算二阶导数
laplacian=cv2.Laplacian(blurred, cv2.CV_64F, ksize=3)
laplacian_abs=cv2.convertScaleAbs(laplacian)
#零交叉点检测(通过阈值化提取边缘)
#这里使用自适应阈值
edges=cv2.adaptiveThreshold(src=laplacian_abs,maxValue=255,adaptiveMethod=cv2.ADAPTIVE_THRESH_GAUSSIAN_C,thresholdType=cv2.THRESH_BINARY_INV,C=4,blockSize=11)
cv2.imshow("SRCImage", image)
cv2.imshow("LOGEdges", edges)
cv2.waitKey(0)
cv2.destroyAllWindows()
结果:
总结
简而言之,我们这里介绍到的边缘检测算子都是基于梯度,因此其内部的数字都基于差分结果的系数。不同的区别在于,他们中有些使用的差分阶数不同,有些考虑到了使用滤波模版,有些考虑到了中心像素点的权重,有些重点关注斜对角线上像素点的差分结果……但是追根溯源,都不外乎是在利用卷积运算查找灰度值变化明显的地方,这也是边缘的概念所在。