计算机视觉cv2入门之边缘检测

发布于:2025-03-16 ⋅ 阅读:(14) ⋅ 点赞:(0)

检测原理

        边缘检测是指检测图像中的一些像素点,它们周围的像素点的灰度发生了急剧的变化,因此可以将这些像素点作为一个集合,用于标注图像中不同物体的边界。

     边缘是图像上灰度级变化很快的点的集合。这些点的梯度往往很大。因此我们可以使用一阶导数和二阶偏导数来进行求解,但图像是离散的数据并以矩阵的形式存储,并不能像数学理论中对直线或曲线一样求导,所以我们使用差分来近似微分,采用不同的差分模板来对原图像进行卷积运算进而实现对图像求导。

导数算子卷积模板推导

         这里,我们以一阶导数算子Prewitt和二阶导数算子Laplacian的卷积模板为例 进一步推导说明其由来。

我们知道,一个函数的一阶与二阶导数可以表示为:

f'(x)=\lim_{\Delta x\to0}\frac{f(x+\Delta x)-f(x)}{\Delta x}

f'(x)=\lim_{\Delta x\to0}\frac{f(x)-f(x-\Delta x)}{\Delta x}

f''(x)=\lim_{\Delta x\to0}\frac{f'(x+\Delta x)-f'(x)}{\Delta x}

f''(x)=\lim_{\Delta x\to0}\frac{f'(x)-f'(x-\Delta x)}{\Delta x}

那么对于离散的函数,其一阶差分为:

f(x+1)-f(x)  ……(1)

f(x)-f(x-1)  ……(2)

(1)式-(2)式我们可以得到其二阶差分为:

f(x-1)-2f(x)+f(x+1)……(3)

(1)式+(2)式可以得到:

f(x+1)-f(x-1) ……(4)

        这里要注意的是,(4)式并不是标准意义上的一阶差分,不过,其可看做是一种"中心差分"的形式,这里我们也近似把它看做一阶差分。

        实际上,这种中心差分的形式主要是为了和我们图像中最常用的3x3卷积模板相配合使用。

图像坐标系  

Prewitt算子

        prewitt模板分为x与y两个方向。

Prewitt算子在x,y方向上的卷积模板 

这里我们设模板中心点的坐标为(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范数,此时幅度值为: 

Magnitude=\sqrt{G_{x}^2+G_{y}^2}  

        其中,G_{x}G_{y}分别为一阶导数算子在x,y方向上的矢量。 

Canny算子 

Canny算子的计算步骤如下:

  1. 用高斯滤波器平滑图像,去除噪声。
  2. 用一阶差分偏导计算梯度方向和幅度(Sobel算子)。
  3. 对梯度值不是极大值的地方进行抑制,将不是机制的点全部置0,去掉大部分的边缘,所以图像边缘会变细。

二阶导数算子

Laplacian算子

Laplacian算子卷积模板 

Mar算子   

        Marr算子通常由两部分组成:一个高斯滤波器用于平滑图像,减少噪声的影响;一个差分算子用于检测亮度变化。在每个像素点上进行如下计算:

  1. 用一个2D的高斯平滑模板与原图像卷积。
  2. 计算卷积后图像的拉普拉斯值。
  3. 检测图像中的过零点,将其作为边缘点。

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()

结果: 

总结

        简而言之,我们这里介绍到的边缘检测算子都是基于梯度,因此其内部的数字都基于差分结果的系数。不同的区别在于,他们中有些使用的差分阶数不同,有些考虑到了使用滤波模版,有些考虑到了中心像素点的权重,有些重点关注斜对角线上像素点的差分结果……但是追根溯源,都不外乎是在利用卷积运算查找灰度值变化明显的地方,这也是边缘的概念所在。


网站公告

今日签到

点亮在社区的每一天
去签到