边缘检测算法一般基于图像强度的一阶和二阶导数进行计算,这些导数通常对噪声很敏感,因此在检测前一般会对图像进行平滑去噪,参见图像降噪、图像滤波。
Sobel 算子
Sobel算子,通过计算图像的一阶倒数进行图像边缘检测。
检测步骤:
-
假定输入图像矩阵为 I I I,卷积核大小为 3 × 3 3 \times 3 3×3,分别计算图像的水平一阶导数 G x Gx Gx 和垂直一阶导数 G y Gy Gy:
G x = [ − 1 0 1 − 2 0 2 − 1 0 1 ] ∗ I G y = [ − 1 − 2 − 1 0 0 0 1 2 1 ] ∗ I \quad G_x = \begin{bmatrix} -1 & 0 & 1 \\ -2 & 0 & 2 \\ -1 & 0 & 1 \\ \end{bmatrix} * I \qquad G_y = \begin{bmatrix} -1 & -2 & -1 \\ 0 & 0 & 0 \\ 1 & 2 & 1 \\ \end{bmatrix} * I Gx=⎣⎡−1−2−1000121⎦⎤∗IGy=⎣⎡−101−202−101⎦⎤∗I -
对输出 G x Gx Gx和 G y Gy Gy梯度强度合并为图像梯度 G G G:
G = G x 2 + G y 2 \quad G = \sqrt{G_{x}^2 + G_{y}^2 } \qquad G=Gx2+Gy2
Sobel 算子计算时,简化为:
G = ∣ G x ∣ + ∣ G y ∣ \qquad G = |G_x| + |G_y| G=∣Gx∣+∣Gy∣
函数原型:
def Sobel(src, ddepth, dx, dy, dst=None, ksize=None, scale=None, delta=None, borderType=None)
参数:
-
src:需要处理的原图像
-
ddepth:输出图像的深度
-
dx:x 方向上的差分阶数
-
dy:y方向上的差分阶数
-
dst:返回边缘图像
-
ksize: 默认值3,表示Sobel核的大小;必须取1,3,5或7。
-
scale:计算导数值时可选的缩放因子,默认值是1,没有应用缩放
-
delta:表示在结果存入目标图之前可选的delta值,有默认值0。
-
borderType:边界模式,默认值为BORDER_DEFAULT
返回:检测出的边缘图像
dx 和 dy 表示阶数,一般取 0 或 1,但不超过 2
Scharr 卷积核
当卷积核大小为 3x3 时,使用 sobel 卷积核来计算并不是很精确,此时常用 Scharr 卷积核来代替,如下:
G x = [ − 3 0 3 − 10 0 10 − 3 0 3 ] G y = [ − 3 − 10 − 3 0 0 0 3 10 3 ] \quad G_x = \begin{bmatrix} -3 & 0 & 3 \\ -10 & 0 & 10 \\ -3 & 0 & 3 \\ \end{bmatrix}\qquad G_y = \begin{bmatrix} -3 & -10 & -3 \\ 0 & 0 & 0 \\ 3 & 10 & 3 \\ \end{bmatrix} Gx=⎣⎡−3−10−30003103⎦⎤Gy=⎣⎡−303−10010−303⎦⎤
而 Sharr 函数,本质上就是令 ksize = 3 且使用 Scharr 卷积核的 Sobel 函数。
函数原型:
def Scharr(src, ddepth, dx, dy, dst=None, scale=None, delta=None, borderType=None)
参数:
-
src:需要处理的原图像
-
ddepth:输出图像的深度
-
dx:x 方向上的差分阶数
-
dy:y方向上的差分阶数
-
dst:返回边缘图像
-
scale:计算导数值时可选的缩放因子,默认值是1,没有应用缩放
-
delta:表示在结果存入目标图之前可选的delta值,有默认值0。
-
borderType:边界模式,默认值为BORDER_DEFAULT
返回:检测出的边缘图像
Laplace 算子
Sobel 算子和Laplace算子都可以进行边缘检测,不同之处在于,前者求一阶导,后者求二阶导。
Laplace算子的计算方式如下:
L
a
p
l
a
c
e
(
f
)
=
∂
2
f
∂
x
2
+
∂
2
f
∂
y
2
\quad Laplace(f) = \frac{\partial^2f}{\partial x^2} + \frac{\partial^2f}{\partial y^2}
Laplace(f)=∂x2∂2f+∂y2∂2f
表示为离散形式:
L a p l a c e ( f ) = f ( x + 1 , y ) + f ( x − 1 , y ) + f ( x , y + 1 ) + f ( x , y − 1 ) − 4 f ( x , y ) \quad Laplace(f) = f(x+1, y) + f(x-1, y) + f(x, y+1) + f(x, y-1) - 4f(x, y) Laplace(f)=f(x+1,y)+f(x−1,y)+f(x,y+1)+f(x,y−1)−4f(x,y)
而当ksize=1时,Laplacian 函数采用以下 3 × 3 3 \times 3 3×3的卷积核:
[ 0 1 0 1 − 4 1 0 1 0 ] \quad \begin{bmatrix} 0 & 1 & 0 \\ 1 & -4 & 1 \\ 0 & 1 & 0\\ \end{bmatrix}\qquad ⎣⎡0101−41010⎦⎤
函数原型:
def Laplacian(src, ddepth, dst=None, ksize=None, scale=None, delta=None, borderType=None)
参数:
-
src:需要处理的原图像,单通道8位灰度图
-
ddept:目标图像的深度
-
dst:输出的边缘图
-
ksize:用于计算二阶导数的卷积核大小,必须为正奇数,默认值1
-
scale:计算拉普拉斯值的时候可选的比例因子,默认值1
-
delta:表示在结果存入目标图之前可选的delta值,默认值0
-
borderType:边界模式,默认值为BORDER_DEFAULT
返回:检测出的边缘图像
Canny 边缘检测
检测步骤:
-
对平滑后的图像使用Sobel算子计算水平方向和竖直方向的一阶导数 G x Gx Gx和 G y Gy Gy:
G x = [ − 1 0 1 − 2 0 2 − 1 0 1 ] G y = [ − 1 − 2 − 1 0 0 0 1 2 1 ] \quad G_x = \begin{bmatrix} -1 & 0 & 1 \\ -2 & 0 & 2 \\ -1 & 0 & 1 \\ \end{bmatrix} \qquad G_y = \begin{bmatrix} -1 & -2 & -1 \\ 0 & 0 & 0 \\ 1 & 2 & 1 \\ \end{bmatrix} Gx=⎣⎡−1−2−1000121⎦⎤Gy=⎣⎡−101−202−101⎦⎤
根据Gx、Gy梯度图计算整体的梯度强度和角度方向:
G = G x 2 + G y 2 θ = arctan ( G y G x ) \quad G = \sqrt{G_{x}^2 + G_{y}^2 } \qquad \theta = \arctan(\dfrac{ G_y }{ G_x }) G=Gx2+Gy2θ=arctan(GxGy)
角度方向近似为四个可能值,即: 0, 45, 90, 135 -
对图像的梯度强度进行非极大抑制,只有候选边缘点被保留,其余的点被移除,细化图像边缘。
-
使用两个滞后阀值minVal和maxVal检测和连接边缘。灰度梯度高于maxVal的候选点被认为是边界点,低于minVal的候选点会被抛弃。介于两者之间的候选点,如果与边界点相连,就认为它也是边界点,如果不是就抛弃。
-
通过抑制孤立的弱边缘最终完成边缘检测。
函数原型:
def Canny(image, threshold1, threshold2, edges=None, apertureSize=None, L2gradient=None)
参数:
-
image:原图像,单通道8位灰度图
-
threshold1:第一个滞后阈值
-
threshold2:第二个滞后阈值
-
edges:返回边缘图像
-
apertureSize:Sobel算子的卷积核大小
-
L2gradient:默认值false,如果为真,则使用更精确的L2范数进行计算(即两个方向的倒数的平方和再开放),否则使用L1范数(直接将两个方向导数的绝对值相加)。
返回:检测出的边缘图像
检测效果对比
卷积核 k s i z e = 3 ksize = 3 ksize=3时:
卷积核 k s i z e = 5 ksize = 5 ksize=5时:
import cv2
def edge_detect(x):
threshold = cv2.getTrackbarPos('threshold', 'edgeDetect')
kSize = cv2.getTrackbarPos('kSize', 'edgeDetect')
if kSize % 2 == 0 or kSize < 3:
return
canny = cv2.Canny(blur, threshold, threshold * 3, apertureSize=kSize)
sobel_x = cv2.Sobel(blur, -1, 1, 0, None, kSize)
sobel_y = cv2.Sobel(blur, -1, 0, 1, None, kSize)
sobel_x = cv2.convertScaleAbs(sobel_x)
sobel_y = cv2.convertScaleAbs(sobel_y)
sobel = cv2.addWeighted(sobel_x, 1, sobel_y, 1, 0)
laplacian = cv2.Laplacian(blur, -1, None, kSize)
laplacian = cv2.convertScaleAbs(laplacian)
scharr_x = cv2.Scharr(blur, -1, 1, 0)
scharr_y = cv2.Sobel(blur, -1, 0, 1)
scharr_x = cv2.convertScaleAbs(scharr_x)
scharr_y = cv2.convertScaleAbs(scharr_y)
scharr = cv2.addWeighted(scharr_x, 1, scharr_y, 1, 0)
cv2.imshow('canny', canny)
cv2.imshow('sobel', sobel)
cv2.imshow('laplacian', laplacian)
cv2.imshow('scharr', scharr)
threshold = 42
kSize = 3
img = cv2.imread("./91_frame.png")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (5, 5), 0)
cv2.namedWindow('edgeDetect')
cv2.imshow('edgeDetect', img)
cv2.createTrackbar('threshold', 'edgeDetect', threshold, 100, edge_detect)
cv2.createTrackbar('kSize', 'edgeDetect', kSize, 7, edge_detect)
edge_detect(0)
if cv2.waitKey(0) == 27:
cv2.destroyAllWindows()