图像的边缘指的是灰度值发生急剧变化的位置。在图像形成过程中,由于亮度、纹理、颜色、阴影等物理因素的不同而导致图像灰度值发生突变,从而形成边缘。边缘是通过检查每个像素的邻域并对其灰度变化进行量化的,这种灰度变化的量化相当于微积分里连续函数中方向导数或者离散数列的差分。
边缘检测大多数是通过基于方向导数掩码(梯度方向导数)求卷积的方法。计算灰度变化的卷积算子包含Roberts算子、Prewitt算子、Sobel算子、Scharr算子、Kirsch算子、Robinson算子、Laplacian算子,常用的检测方法有Canny边缘检测、Marr-Hidreth边缘检测。
大多数边缘检测算子是基于方向差分卷积核求卷积的方法,在使用由两个或者多个卷积核组成的边缘检测算子时,假设有 n 个卷积核,记 c o n v 1 , c o n v 2 , . . . , c o n v n \mathbf{conv}_1, \mathbf{conv}_2,...,\mathbf{conv}_n conv1,conv2,...,convn 为图像分别与个卷积核做卷积的结果,通常有四种方式来衡量最后输出的边缘强度。
其中取绝对值的最大值的方式,对边缘的走向有些敏感,而其他几种方式可以获得性能更一致的全方位响应。取平方和的开方的方式效果一般是最好的,但是同时会更加耗时。
Roberts 边缘检测是图像矩阵与以下两个卷积核分别做卷积(对于图像卷积的计算可查看这篇文章)。
R o b e r t s 135 = ( 1 0 0 − 1 ) , R o b e r t s 45 = ( 0 1 − 1 0 ) , \mathbf{Roberts}_{135} = \begin{pmatrix} 1 & 0 \\ 0 & -1 \end{pmatrix}, \;\;\; \mathbf{Roberts}_{45} = \begin{pmatrix} 0 & 1 \\ -1 & 0 \end{pmatrix}, Roberts135=(100−1),Roberts45=(0−110),
与 Roberts 核卷积,本质上是两个对角方向上的差分,与 R o b e r t s 135 \mathbf{Roberts}_{135} Roberts135 卷积后的结果取绝对值,反应的是 4 5 ∘ 45^{\circ} 45∘ 方向上的灰度变化率;而与 R o b e r t s 45 \mathbf{Roberts}_{45} Roberts45 卷积后的结果取绝对值,反应的是 13 5 ∘ 135^{\circ} 135∘ 方向上的灰度变化率,利用变化率对边缘强度进行数字衡量。对 Roberts 算子进行改进便可以反响在垂直方向和水平方向上的边缘。
R o b e r t s 135 = ( 1 − 1 ) , R o b e r t s 45 = ( 1 − 1 ) , \mathbf{Roberts}_{135} = \begin{pmatrix} 1 & -1 \end{pmatrix}, \;\;\; \mathbf{Roberts}_{45} = \begin{pmatrix} 1 \\ -1 \end{pmatrix}, Roberts135=(1−1),Roberts45=(1−1),
Python 示例
使用函数 convolve2d 实现图像矩阵分别与两个 Roberts 核的卷积。
import cv2 as cv
import numpy as np
from scipy import signal
def roberts(img, boundary='fill', fillvalue=0):
h, w = img.shape[:2]
h_k, w_k = 2, 2
# 卷积核1及锚点的位置
r1 = np.array([[1,0],[0,-1]], np.float32)
kr1, kc1 = 0, 0
# 计算full卷积
img_conv_r1 = signal.convolve2d(img, r1, mode='full', boundary=boundary, fillvalue=fillvalue)
# 根据锚点的位置截取 full 卷积,获得 same 卷积
img_conv_r1 = img_conv_r1[h_k-kr1-1:h+h_k-kr1-1, w_k-kc1-1:w+w_k-kc1-1]
# 卷积核2及锚点的位置
r2 = np.array([[0, 1], [-1, 0]], np.float32)
kr2, kc2 = 0, 1
# 计算full卷积
img_conv_r2 = signal.convolve2d(img, r2, mode='full', boundary=boundary, fillvalue=fillvalue)
# 根据锚点的位置截取 full 卷积,获得 same 卷积
img_conv_r2 = img_conv_r2[h_k - kr2 - 1:h + h_k - kr2 - 1, w_k - kc2 - 1:w + w_k - kc2 - 1]
return img_conv_r1, img_conv_r2
if __name__ == '__main__':
img = cv.imread("/img3.png", 0)
cv.imshow('src', img)
img_conv_r1, img_conv_r2 = roberts2(img)
# 45 方向上的边缘强度的灰度级显示
img_conv_r1 = np.abs(img_conv_r1)
edge_45 = img_conv_r1.astype(np.uint8)
cv.imshow('edge_45', edge_45)
# 135 方向上的边缘强度
img_conv_r2 = np.abs(img_conv_r2)
edge_135 = img_conv_r2.astype(np.uint8)
cv.imshow('edge_135', edge_135)
# 用平方和的开方来衡量最后输出的边缘
edge = np.sqrt(np.power(img_conv_r1, 2.0) + np.power(img_conv_r2, 2.0))
edge = np.round(edge)
edge[edge>255] = 255
edge = edge.astype(np.uint8)
# 显示边缘
cv.imshow('edge', edge)
cv.waitKey(0)
cv.destroyAllWindows()
Roberts 边缘检测因为使用了很少的邻域像素来近似边缘强度,因此对图像中的噪声具有高度敏感性。因此,先对图像做平滑处理再进行Roberts边缘检测效果会更好。
标准的 Prewitt 边缘检测算子由以下两个卷积核组成。
p r e w i t t x = ( 1 0 − 1 1 0 − 1 1 0 − 1 ) , p r e w i t t y = ( 1 1 − 1 0 0 0 − 1 − 1 − 1 ) \mathbf{prewitt}_{x} = \begin{pmatrix} 1 & 0 & -1\\ 1 & 0 & -1\\ 1 & 0 & -1 \end{pmatrix}, \;\;\; \mathbf{prewitt}_{y} = \begin{pmatrix} 1 & 1 & -1\\ 0 & 0 & 0\\ -1 & -1 & -1 \end{pmatrix} prewittx=⎝⎛111000−1−1−1⎠⎞,prewitty=⎝⎛10−110−1−10−1⎠⎞
图像与 p r e w i t t x \mathbf{prewitt}_{x} prewittx 卷积后可以反映图像垂直方向上的边缘,与 p r e w i t t y \mathbf{prewitt}_{y} prewitty 卷积后可以反映图像水平方向上的边缘。而且,这两个卷积核均是可分离的,其中
p r e w i t t x = ( 1 1 1 ) ★ ( 1 0 − 1 ) , p r e w i t t x = ( 1 1 1 ) ★ ( 1 0 − 1 ) \mathbf{prewitt}_{x} = \begin{pmatrix} 1 \\ 1 \\ 1 \end{pmatrix} \bigstar \begin{pmatrix} 1 & 0 & -1 \end{pmatrix}, \;\;\; \mathbf{prewitt}_{x} = \begin{pmatrix} 1 & 1 & 1 \end{pmatrix} \bigstar \begin{pmatrix} 1 \\ 0 \\ -1 \end{pmatrix} prewittx=⎝⎛111⎠⎞★(10−1),prewittx=(111)★⎝⎛10−1⎠⎞
从分离结果可以看出, p r e w i t t x \mathbf{prewitt}_{x} prewittx 算子实际上先对图像进行垂直方向上的非归一化的均值平滑,然后进行水平方向的差分;而 p r e w i t t y \mathbf{prewitt}_{y} prewitty 算子实际上先对图像进行水平方向上的非归一化的均值平滑,然后进行垂直方向上的差分。
由于对图像进行了平滑操作,所以对噪声较多的图像进行 Prewitt 边缘检测得到的边缘比 Roberts 要好。可以对标准的 Prewitt 算子进行改进,比如以下两个卷积核反映的是在 4 5 ∘ 45^{\circ} 45∘ 和 13 5 ∘ 135^{\circ} 135∘ 方向上的边缘。这两个卷积核是不可分离的。
p r e w i t t 135 = ( 1 1 0 1 0 − 1 0 − 1 − 1 ) , p r e w i t t y = ( 0 1 1 − 1 0 1 − 1 − 1 0 ) \mathbf{prewitt}_{135} = \begin{pmatrix} 1 & 1 & 0\\ 1 & 0 & -1\\ 0 & -1 & -1 \end{pmatrix}, \;\;\; \mathbf{prewitt}_{y} = \begin{pmatrix} 0 & 1 & 1\\ -1 & 0 & 1\\ -1 & -1 & 0 \end{pmatrix} prewitt135=⎝⎛11010−10−1−1⎠⎞,prewitty=⎝⎛0−1−110−1110⎠⎞
Python 示例
因为 Prewitt 算子是可分离的,所以为了减少耗时,在代码实现中,利用卷积运算的结合律先进性水平方向上的平滑,在进行垂直方向上的差分,或者先进行垂直方向上的平滑,再进行水平方向上的差分。
import cv2 as cv
import numpy as np
from scipy import signal
def prewitt(img, boundary='symm'):
# 垂直方向上的均值平滑
ones_y = np.array([[1], [1], [1]], np.float32)
i_conv_pre_x = signal.convolve2d(img, ones_y, mode='same', boundary=boundary)
# 水平方向的差分
diff_x = np.array([[1, 0, -1]], np.float32)
i_conv_pre_x = signal.convolve2d(i_conv_pre_x, diff_x, mode='same', boundary=boundary)
# 水平方向上的均值平滑
ones_x = np.array([[1,1,1]], np.float32)
i_conv_pre_y = signal.convolve2d(img, ones_x, mode='same', boundary=boundary)
# 垂直方向的差分
diff_y = np.array([[1], [0], [-1]], np.float32)
i_conv_pre_y = signal.convolve2d(i_conv_pre_y, diff_y, mode='same', boundary=boundary)
return i_conv_pre_x, i_conv_pre_y
if __name__ == '__main__':
img = cv.imread("/img7.jpg", 0)
cv.imshow('src', img)
i_conv_pre_x, i_conv_pre_y = prewitt(img)
# 取绝对值,分别得到水平方向和垂直方向上的边缘强度
abs_i_conv_pre_x = np.abs(i_conv_pre_x)
abs_i_conv_pre_y = np.abs(i_conv_pre_y)
# 水平方向和垂直方向上的边缘强度的灰度级显示
edge_x = abs_i_conv_pre_x.copy()
edge_y = abs_i_conv_pre_y.copy()
edge_x[edge_x>255] = 255
edge_y[edge_y>255] = 255
edge_x = edge_x.astype(np.uint8)
edge_y = edge_y.astype(np.uint8)
cv.imshow('edge_x', edge_x)
cv.imshow('edge_y', edge_y)
# 利用 abs_i_conv_pre_x 和 abs_i_conv_pre_y 求最终的边缘强度
# 求边缘强度,此处使用插值法
edge = 0.5 * abs_i_conv_pre_x + 0.5 * abs_i_conv_pre_y
edge[edge>255] = 255
edge = edge.astype(np.uint8)
cv.imshow('edge', edge)
cv.waitKey(0)
cv.destroyAllWindows()
从 Roberts 和 Prewitt 边缘检测的效果图可以清晰地理解差分方向(或称梯度方向)与得到的边缘是垂直的,如水平差分方向上的卷积放映的是垂直方向上的边缘。
在图像平滑处理中 ,高斯平滑的效果往往比均值平滑要好,因此把 Prewitt 算子的非归一化的均值卷积核替换成非归一化的高斯卷积核,就可以构建 3 阶的 Sobel 边缘检测算子。
3 阶的 Sobel 边缘检测算子
s o b e l x = ( 1 2 1 ) ★ ( 1 0 − 1 ) = ( 1 0 − 1 2 0 − 2 1 0 − 1 ) \mathbf{sobel}_{x} = \begin{pmatrix} 1 \\ 2 \\ 1 \end{pmatrix} \bigstar \begin{pmatrix} 1 & 0 & -1 \end{pmatrix} = \begin{pmatrix} 1 & 0 & -1 \\ 2 & 0 & -2\\ 1 & 0 & -1 \end{pmatrix} sobelx=⎝⎛121⎠⎞★(10−1)=⎝⎛121000−1−2−1⎠⎞
s o b e l y = ( 1 2 1 ) ★ ( 1 0 − 1 ) = ( 1 2 1 0 0 0 − 1 2 − 1 ) \mathbf{sobel}_{y} = \begin{pmatrix} 1 & 2 & 1 \end{pmatrix} \bigstar \begin{pmatrix} 1 \\ 0 \\ -1 \end{pmatrix} = \begin{pmatrix} 1 & 2 & 1\\ 0 & 0 & 0\\ -1 & 2 & -1 \end{pmatrix} sobely=(121)★⎝⎛10−1⎠⎞=⎝⎛10−120210−1⎠⎞
Sobel 的算子是可分离的,这是 Sobel 算子的标准形式,可以利用二项式展开式的系数构建窗口更大的 Sobel 算子,如 5x5、7x7等,窗口大小为奇数。
构建高阶的 Sobel 算子
Sobel 算子是在一个坐标轴方向上进行非归一化的高斯平滑,在另一个坐标轴方向上进行差分处理。 nxn 的 Sobel 算子是由平滑算子和差分算子 full 卷积而得到的,对于窗口大小为 n 的非归一化的高斯平滑算子等于 n-1 阶的二项式展开式的系数。窗口大小为 n 的差分算子是在 n-2 阶的二项式展开式的系数两侧补零,然后后向差分得到的。举例:构建 5 阶的非归一化的高斯平滑算子,取二项式的指数 n=4,然后计算展开式的系数,即
对于构建 5 阶的差分算子,令二项式的指数 n=5-2=3 ,然后计算展开式的系数,即
两侧补零,接着向后差分,得到差分后的结果即为 5 阶的差分算子,然后和 5 阶的平滑算子 full 卷积,即可得到 5x5 的 Sobel,Sobel平滑算子和差分算子的总结如下所示
n | 窗口大小 | 平滑算子 | 差分算子 |
---|---|---|---|
1 | 2 | 1 1 | 1 -1 |
2 | 3 | 1 2 1 | 1 0 -1 |
3 | 4 | 1 3 3 1 | 1 1 -1 -1 |
4 | 5 | 1 4 6 4 1 | 1 2 0 -2 -1 |
上表中的平滑算子就是帕斯卡三角形。Sobel 边缘检测算子是通过窗口大小为 k 的平滑算子和差分算子与图像卷积而得到的。高阶的 Sobel 边缘检测算子是可分离的。
Python 示例
import math
import cv2 as cv
import numpy as np
from scipy import signal
def pascal_smooth(n):
# 返回 n 阶的非归一化的高斯平滑算子
pascal_smooth = np.zeros([1, n], np.float32)
for i in range(n):
pascal_smooth[0][i] = math.factorial(n-1) / (math.factorial(i) * math.factorial(n-1-i))
return pascal_smooth
def pascal_diff(n):
# 返回 n 阶的差分算子
pascal_diff = np.zeros([1, n], np.float32)
pascal_smooth_previous = pascal_smooth(n-1)
for i in range(n):
if i == 0:
# 恒等于 1
pascal_diff[0][i] = pascal_smooth_previous[0][i]
elif i == n-1:
# 恒等于 -1
pascal_diff[0][i] = - pascal_smooth_previous[0][i-1]
else:
pascal_diff[0][i] = pascal_smooth_previous[0][i] - pascal_smooth_previous[0][i-1]
return pascal_diff
def get_sobel_kernel(n):
pascal_smooth_kernel = pascal_smooth(n)
pascal_diff_kernel = pascal_diff(n)
# 水平方向的卷积核
sobel_kerenl_x = signal.convolve2d(pascal_smooth_kernel.transpose(), pascal_diff_kernel, mode='full')
# 垂直方向的卷积核
sobel_kerenl_y = signal.convolve2d(pascal_smooth_kernel, pascal_diff_kernel.transpose(), mode='full')
return sobel_kerenl_x, sobel_kerenl_y
def sobel(img, n):
rows, cols = img.shape[:2]
# 平滑算子
pascal_smooth_kernel = pascal_smooth(n)
# 差分算子
pascal_diff_kernel = pascal_diff(n)
# 水平方向上的 sobel 核卷积
# 先进行垂直方向的平滑
img_sobel_x = signal.convolve2d(img, pascal_smooth_kernel.transpose(), mode='same')
# 再进行水平方向上的差分
img_sobel_x = signal.convolve2d(img_sobel_x, pascal_diff_kernel, mode='same')
# 垂直方向上的 sobel 核卷积
img_sobel_y = signal.convolve2d(img, pascal_smooth_kernel, mode='same')
img_sobel_y = signal.convolve2d(img_sobel_y, pascal_diff_kernel.transpose(), mode='same')
return img_sobel_x, img_sobel_y
if __name__ == '__main__':
img = cv.imread('img7.jpg', 0)
cv.imshow('src', img)
img_sobel_x, img_sobel_y = sobel(img, 3)
img_sobel_x_c, img_sobel_y_c = img_sobel_x.copy(), img_sobel_y.copy()
img_sobel_x_c, img_sobel_y_c = abs(img_sobel_x_c), abs(img_sobel_y_c)
img_sobel_x_c[img_sobel_x_c>255] = 255
img_sobel_y_c[img_sobel_y_c>255] = 255
img_sobel_x_c = img_sobel_x_c.astype(np.uint8)
img_sobel_y_c = img_sobel_y_c.astype(np.uint8)
cv.imshow('sobel x', img_sobel_x_c)
cv.imshow('sobel y', img_sobel_y_c)
# 平方和开方的方式
edge = np.sqrt(np.power(img_sobel_x, 2.0) + np.power(img_sobel_y, 2.0))
# 直接截断显示
edge_c = edge.copy()
edge_c[edge_c > 255] = 255
edge_c = edge_c.astype(np.uint8)
cv.imshow('sobel edge 1', edge_c)
# 归一化后显示,边缘强度的灰度级显示
edge = edge/np.max(edge)
edge = np.power(edge, 1)
edge *= 255
edge = edge.astype(np.uint8)
cv.imshow('sobel edge 2', edge)
cv.waitKey(0)
cv.destroyAllWindows()
使用不同尺寸的 Sobel 核边缘检测效果,可以看出,使用高阶的 Sobel 核得到的边缘信息比低阶的更加丰富。
Opencv 函数
Sobel 函数官方地址
void cv::Sobel(InputArray src,
OutputArray dst,
int ddepth,
int dx,
int dy,
int ksize = 3,
double scale = 1,
double delta = 0,
int borderType = BORDER_DEFAULT
)
//Python:
dst = cv.Sobel(src, ddepth, dx, dy[, dst[, ksize[, scale[, delta[, borderType]]]]])
参数 | 解释 |
---|---|
ddepth | 输出矩阵的深度 |
dx | 当 dx ≠ \neq = 0 时,src 与差分方向为水平方向上的 Sobel 核卷积 |
dy | 当 dx=0,dy$\neq$0时,src与差分方向为垂直方向上的 Sobel 核卷积 |
ksize | sobel 核尺寸,当ksize=-1时,使用的是 Scharr 算子 |
scale | 比例系数 |
delta | 平移系数 |
borderType | 边界扩充类型 |
对于 ksize,当 ksize=1 时,代表 Sobel 核没有平滑算子,只有差分算子,即如果设置参数 dx=1, dy=0,那么 src 只与 1x3 的水平方向上的差分算子 (1 0 -1) 卷积,没有平滑算子。通常,调用该函数时,( dx = 1,dy = 0,ksize = 3)或( dx = 0,dy = 1,ksize = 3)来分别获得与水平方向或垂直方向差分算子的卷积。
if __name__ == '__main__':
img = cv.imread('img8.jpg', 0)
cv.imshow('src', img)
img_sobel_x = cv.Sobel(img, -1, dx=0, dy=1, ksize=5) # y方向差分
img_sobel_y = cv.Sobel(img, -1, dx=1, dy=0, ksize=5) # x方向差分
# 使用平方和开方计算边缘强度(也可以使用其他方法)
img_edge = np.sqrt(np.power(img_sobel_x, 2.0) + np.power(img_sobel_y, 2.0))
img_edge = edge / np.max(img_edge)
img_edge *= 255
img_edge = img_edge.astype(np.uint8)
cv.imshow('cv sobel', img_edge)
cv.waitKey(0)
cv.destroyAllWindows()
标准的 Scharr 边缘检测算子与 Prewitt 边缘检测算子和 3 阶的 Sobel 边缘检测算子类似,由以下两个卷积核组成,不同的是,这两个卷积核均是不可分离的。图像与水平方向上的 s c h a r r x \mathbf{scharr}_{x} scharrx 卷积结果反响的是垂直方向上的边缘强度,与垂直方向上的 s c h a r r y \mathbf{scharr}_{y} scharry 卷积结果反映的是水平方向上的边缘强度。
s c h a r r x = ( 3 0 − 3 10 0 − 10 3 0 − 3 ) , s c h a r r y = ( 3 10 3 0 0 0 − 3 − 10 − 3 ) \mathbf{scharr}_{x} = \begin{pmatrix} 3 & 0 & -3\\ 10 & 0 & -10\\ 3 & 0 & -3 \end{pmatrix}, \;\;\; \mathbf{scharr}_{y} = \begin{pmatrix} 3 & 10 & 3\\ 0 & 0 & 0\\ -3 & -10 & -3 \end{pmatrix} scharrx=⎝⎛3103000−3−10−3⎠⎞,scharry=⎝⎛30−3100−1030−3⎠⎞
同样,Scharr 边缘检测算子也可以扩展到其他方向,比如以下两个反映的是 13 5 ∘ 135^{\circ} 135∘ 和 4 5 ∘ 45^{\circ} 45∘ 方向上的边缘。
s c h a r r 45 = ( 0 3 10 − 3 0 3 − 10 − 3 0 ) , s c h a r r 135 = ( 10 3 0 3 0 − 3 0 − 3 − 10 ) \mathbf{scharr}_{45} = \begin{pmatrix} 0 & 3 & 10\\ -3 & 0 & 3\\ -10 & -3 & 0 \end{pmatrix}, \;\;\; \mathbf{scharr}_{135} = \begin{pmatrix} 10 & 3 & 0\\ 3 & 0 & -3\\ 0 & -3 & -10 \end{pmatrix} scharr45=⎝⎛0−3−1030−31030⎠⎞,scharr135=⎝⎛103030−30−3−10⎠⎞
Opencv 函数
Scharr 函数官方地址
void cv::Scharr(InputArray src,
OutputArray dst,
int ddepth,
int dx,
int dy,
double scale = 1,
double delta = 0,
int borderType = BORDER_DEFAULT
)
//Python:
dst = cv.Scharr(src, ddepth, dx, dy[, dst[, scale[, delta[, borderType]]]]
参数与 Sobel 相同,等同于
Sobel(src, dst, ddepth, dx, dy, FILTER_SCHARR, scale, delta, borderType)
与 Prewitt 边缘检测相比,因为 Scharr 卷积核中系数的增大,所以灰度变化较为敏感,即是灰度变化较小的区域,也会得到较强的边缘强度,所以得到的边缘图比 Prewitt 得到的边缘图显得丰富,但是不够细化。
Krisch算子由以下 8 个卷积核组成。图像与每一个核进行卷积,然后取绝对值作为对应方向上的边缘强度的量化。对 8 个卷积结果取绝对值,然后在对应值位置取最大值作为最后输出的边缘强度。
k 1 = ( 5 5 5 − 3 0 − 3 − 3 − 3 − 3 ) , k 2 = ( − 3 − 3 − 3 − 3 0 − 3 5 5 5 ) , k 3 = ( − 3 5 5 − 3 0 5 − 3 − 3 − 3 ) , k 4 = ( − 3 − 3 − 3 5 0 − 3 5 5 − 3 ) , \mathbf{k}_{1} = \begin{pmatrix} 5 & 5 & 5\\ -3 & 0 & -3\\ -3 & -3 & -3 \end{pmatrix}, \mathbf{k}_{2} = \begin{pmatrix} -3 & -3 & -3\\ -3 & 0 & -3\\ 5 & 5 & 5\\ \end{pmatrix}, \mathbf{k}_{3} = \begin{pmatrix} -3 & 5 & 5\\ -3 & 0 & 5\\ -3 & -3 & -3 \end{pmatrix}, \mathbf{k}_{4} = \begin{pmatrix} -3 & -3 & -3\\ 5 & 0 & -3 \\ 5 & 5 & -3 \end{pmatrix}, k1=⎝⎛5−3−350−35−3−3⎠⎞,k2=⎝⎛−3−35−305−3−35⎠⎞,k3=⎝⎛−3−3−350−355−3⎠⎞,k4=⎝⎛−355−305−3−3−3⎠⎞,
k 5 = ( − 3 − 3 5 − 3 0 5 − 3 − 3 5 ) , k 6 = ( 5 − 3 − 3 5 0 − 3 5 − 3 − 3 ) , k 7 = ( − 3 − 3 − 3 − 3 0 5 − 3 5 5 ) , k 8 = ( 5 5 − 3 5 0 − 3 − 3 − 3 − 3 ) , \mathbf{k}_{5} = \begin{pmatrix} -3 & -3 & 5\\ -3 & 0 & 5\\ -3 & -3 & 5 \end{pmatrix}, \mathbf{k}_{6} = \begin{pmatrix} 5 & -3 & -3\\ 5 & 0 & -3\\ 5 & -3 & -3\\ \end{pmatrix}, \mathbf{k}_{7} = \begin{pmatrix} -3 & -3 & -3\\ -3 & 0 & 5\\ -3 & 5 & 5 \end{pmatrix}, \mathbf{k}_{8} = \begin{pmatrix} 5 & 5 & -3\\ 5 & 0 & -3 \\ -3 & -3 & -3 \end{pmatrix}, k5=⎝⎛−3−3−3−30−3555⎠⎞,k6=⎝⎛555−30−3−3−3−3⎠⎞,k7=⎝⎛−3−3−3−305−355⎠⎞,k8=⎝⎛55−350−3−3−3−3⎠⎞,
Robinson 算子也由 8 个卷积核组成。
r 1 = ( 1 1 1 1 − 2 1 − 1 − 1 − 1 ) , r 2 = ( 1 1 1 − 1 − 2 1 − 1 − 1 1 ) , r 3 = ( − 1 1 1 − 1 − 2 1 − 1 1 1 ) , r 4 = ( − 1 − 1 1 − 1 0 1 1 1 1 ) , \mathbf{r}_{1} = \begin{pmatrix} 1 & 1 & 1\\ 1 & -2 & 1\\ -1 & -1 & -1 \end{pmatrix}, \mathbf{r}_{2} = \begin{pmatrix} 1 & 1 & 1\\ -1 & -2 & 1\\ -1 & -1 & 1\\ \end{pmatrix}, \mathbf{r}_{3} = \begin{pmatrix} -1 & 1 & 1\\ -1 & -2 & 1\\ -1 & 1 & 1 \end{pmatrix}, \mathbf{r}_{4} = \begin{pmatrix} -1 & -1 & 1\\ -1 & 0 & 1 \\ 1 & 1 & 1 \end{pmatrix}, r1=⎝⎛11−11−2−111−1⎠⎞,r2=⎝⎛1−1−11−2−1111⎠⎞,r3=⎝⎛−1−1−11−21111⎠⎞,r4=⎝⎛−1−11−101111⎠⎞,
r 5 = ( − 1 − 1 − 1 1 − 2 1 1 1 1 ) , r 6 = ( 1 − 1 − 1 1 − 2 − 1 1 1 1 ) , r 7 = ( 1 1 − 1 1 − 2 − 1 1 1 − 1 ) , r 8 = ( 1 1 1 1 0 − 1 1 − 1 − 1 ) , \mathbf{r}_{5} = \begin{pmatrix} -1 & -1 & -1\\ 1 & -2 & 1\\ 1 & 1 & 1 \end{pmatrix}, \mathbf{r}_{6} = \begin{pmatrix} 1 & -1 & -1\\ 1 & -2 & -1\\ 1 & 1 & 1\\ \end{pmatrix}, \mathbf{r}_{7} = \begin{pmatrix} 1 & 1 & -1\\ 1 & -2 & -1\\ 1 & 1 & -1 \end{pmatrix}, \mathbf{r}_{8} = \begin{pmatrix} 1 & 1 & 1\\ 1 & 0 & -1 \\ 1 & -1 & -1 \end{pmatrix}, r5=⎝⎛−111−1−21−111⎠⎞,r6=⎝⎛111−1−21−1−11⎠⎞,r7=⎝⎛1111−21−1−1−1⎠⎞,r8=⎝⎛11110−11−1−1⎠⎞,
基于卷积运算的边缘检测算法,比如 Sobel、Prewitt 等,有如下两个缺点:
而 Canny 边缘检测基于这两点做了改进,提出了:
Canny 边缘检测近似算法的步骤如下:
图像矩阵分别与水平方向上的卷积核 s o b e l x \mathbf{sobel}_{x} sobelx 和垂直方向上的卷积核 s o b e l y \mathbf{sobel}_{y} sobely 卷积得到 d x \mathbf{dx} dx 和 d y \mathbf{dy} dy ,然后利用平方和的开方 m a g n i t u d e = d x 2 + d y 2 \mathbf{magnitude} = \sqrt{\mathbf{dx}^2+\mathbf{dy}^2} magnitude=dx2+dy2 得到边缘强度。这一步的过程和 Sobel 边缘检测一样,这里也可以将卷积核换为 Prewitt 核。
利用第一步计算出的 d x \mathbf{dx} dx 和 d y \mathbf{dy} dy ,计算出梯度方向 a n g l e = arctan 2 ( d y , d x ) \mathbf{angle}=\arctan2(\mathbf{dy,dx}) angle=arctan2(dy,dx) ,即对每一个位置 ( r , c ) (r,c) (r,c) , a n g l e = arctan 2 ( d y ( r , c ) , d x ( r , c ) ) \mathbf{angle}=\arctan2(\mathbf{dy}(r,c),\mathbf{dx}(r,c)) angle=arctan2(dy(r,c),dx(r,c)) 代表该位置的梯度方向,一般用角度表示 a n g l e ( r , c ) ∈ [ 0 , 180 ] ∪ [ − 180 , 0 ] \mathbf{angle}(r,c) \in [0, 180]\cup[-180,0] angle(r,c)∈[0,180]∪[−180,0]
对每一个位置进行非极大值抑制的处理,非极大值抑制操作返回的仍然是一个矩阵,假设为 n o n M a x S u p \mathbf{nonMaxSup} nonMaxSup 。如果 m a g n i t u d e ( r , c ) \mathbf{magnitude}(r,c) magnitude(r,c) 在沿着梯度方向 a n g l e ( r , c ) \mathbf{angle}(r,c) angle(r,c) 上的邻域内是最大的则为极大值;否则设置为0。
非极大值抑制的第一种方式:对于非极大值抑制的实现,将梯度方向一般离散化为以下四种情况:
邻域定义为梯度方向所在的直线更多的穿过的部分,这四种情况依次对应的邻域如下图:
非极大值抑制的第二种方式:使用梯度方向所在的直线穿过的所有部分进行插值法,来拟合梯度方向上的边缘强度,可以更加准确的衡量梯度方向上的边缘强度。可以将梯度方向离散化为以下四种情况:
这四种情况一次对应的邻域如下图:
第一种情况时需要计算左上方 ( r − 1 , c − 1 ) (r-1, c-1) (r−1,c−1) 和上方 ( r − 1 , c ) (r-1, c) (r−1,c) 的插值,右下方 ( r + 1 , c + 1 ) (r+1, c+1) (r+1,c+1) 和下方 ( r + 1 , c ) (r+1, c) (r+1,c) 的插值,在这种情况下, ∣ d y ( r , c ) > d x ( r , c ) ∣ |\mathbf{dy}(r,c) > \mathbf{dx}(r,c)| ∣dy(r,c)>dx(r,c)∣ 则比例系数为 ∣ d x ( r , c ) d y ( r , c ) ∣ : ( 1 − ∣ d x ( r , c ) d y ( r , c ) ∣ ) |\frac{\mathbf{dx}(r,c)}{\mathbf{dy}(r,c)}|:(1-|\frac{\mathbf{dx}(r,c)}{\mathbf{dy}(r,c)}|) ∣dy(r,c)dx(r,c)∣:(1−∣dy(r,c)dx(r,c)∣) ,那么两个插值分别为:
∣ d x ( r , c ) d y ( r , c ) ∣ ∗ m a g n i t u d e ( r − 1 , c − 1 ) + ( 1 − ∣ d x ( r , c ) d y ( r , c ) ∣ ) ∗ m a g n i t u d e ( r − 1 , c ) |\frac{\mathbf{dx}(r,c)}{\mathbf{dy}(r,c)}| * \mathbf{magnitude}(r-1, c-1)+(1-|\frac{\mathbf{dx}(r,c)}{\mathbf{dy}(r,c)}|) * \mathbf{magnitude}(r-1, c) ∣dy(r,c)dx(r,c)∣∗magnitude(r−1,c−1)+(1−∣dy(r,c)dx(r,c)∣)∗magnitude(r−1,c)
∣ d x ( r , c ) d y ( r , c ) ∣ ∗ m a g n i t u d e ( r + 1 , c + 1 ) + ( 1 − ∣ d x ( r , c ) d y ( r , c ) ∣ ) ∗ m a g n i t u d e ( r + 1 , c ) |\frac{\mathbf{dx}(r,c)}{\mathbf{dy}(r,c)}| * \mathbf{magnitude}(r+1, c+1)+(1-|\frac{\mathbf{dx}(r,c)}{\mathbf{dy}(r,c)}|) * \mathbf{magnitude}(r+1, c) ∣dy(r,c)dx(r,c)∣∗magnitude(r+1,c+1)+(1−∣dy(r,c)dx(r,c)∣)∗magnitude(r+1,c)
其他情况类似。
非极大值抑制因为只保留了极大值,抑制了非极大值,所以该步骤其实是对 Sobel 边缘强度图进行了细化。
双阈值的滞后阈值处理。经过非极大值抑制处理后的边缘强度图,一般需要阈值化处理,常用的方法是全局阈值分割和局部自适应阈值分割。此处使用的是滞后阈值处理,它使用两个阈值:高阈值和低阈值,按照以下三个规则进行边缘的阈值化处理
对这一过程可以理解为,首先选定边缘强度大于高阈值的所有确定边缘点,然后在边缘强度大于低阈值的情况下尽可能延长边缘。
Python 示例
import math
import cv2 as cv
import numpy as np
from scipy import signal
def nms_default(dx, dy):
# 边缘强度
edge_map = np.sqrt(np.power(dx, 2.0) + np.power(dy, 2.0))
rows, cols = dx.shape
# 梯度方向
gradient_direction = np.zeros(dx.shape)
# 边缘强度非极大值抑制
edge_map_nms = np.zeros(dx.shape)
for r in range(1, rows-1):
for c in range(1, cols-1):
# angle 的范围 [0, 180] [-180, 0]
angle = math.atan2(dy[r][c], dx[r][c]) / math.pi * 180
gradient_direction[r][c] = angle
# 左/右方向
if abs(angle)<22.5 or abs(angle)>157.5:
if edge_map[r][c]>edge_map[r][c - 1] and edge_map[r][c]>edge_map[r][c + 1]:
edge_map_nms[r][c] = edge_map[r][c]
# 左上/右下
if 22.5 <= angle < 67.5 or -157.5 < angle -112.5:
if edge_map[r][c] > edge_map[r-1][c - 1] and edge_map[r][c] > edge_map[r+1][c + 1]:
edge_map_nms[r][c] = edge_map[r][c]
# 上/下
if 67.5 <= angle < 112.5 or -112.5 < angle < -67.5:
if edge_map[r][c] > edge_map[r - 1][c] and edge_map[r][c] > edge_map[r + 1][c]:
edge_map_nms[r][c] = edge_map[r][c]
# 右上/左下
if 112.5 <= angle < 157.5 or -67.5 < angle < -22.5:
if edge_map[r][c] > edge_map[r + 1][c+1] and edge_map[r][c] > edge_map[r + 1][c+1]:
edge_map_nms[r][c] = edge_map[r][c]
return edge_map_nms
def nms_inter(dx, dy):
# 边缘强度
edge_map = np.sqrt(np.power(dx, 2.0) + np.power(dy, 2.0))
rows, cols = dx.shape
# 梯度方向
gradient_direction = np.zeros(dx.shape)
# 边缘强度非极大值抑制
edge_map_nms = np.zeros(dx.shape)
for r in range(1, rows-1):
for c in range(1, cols-1):
if dy[r][c] == 0 and dx[r][c] == 0: continue
angle = math.atan2(dy[r][c], dx[r][c]) / math.pi * 180
gradient_direction[r][c] = angle
# 左上方和上方的插值,右下方和下方的插值
if 45 < angle <= 90 or -135 < angle <= -90:
ratio = dx[r][c] / dy[r][c]
left_top_top = ratio * edge_map[r-1][c-1] + (1-ratio) * edge_map[r-1][c]
right_bottom_bottom = ratio * edge_map[r + 1][c] + (1 - ratio) * edge_map[r+1][c+1]
if edge_map[r][c] > left_top_top and edge_map[r][c] > right_bottom_bottom:
edge_map_nms[r][c] = edge_map[r][c]
# 上方和右上方的插值,左下方和下方的插值
if 90 < angle <= 135 or -90 < angle <= -45:
ratio = dx[r][c] / dy[r][c]
right_top_top = ratio * edge_map[r-1][c+1] + (1-ratio) * edge_map[r-1][c]
left_bottom_bottom = ratio * edge_map[r + 1][c-1] + (1 - ratio) * edge_map[r+1][c]
if edge_map[r][c] > right_top_top and edge_map[r][c] > left_bottom_bottom:
edge_map_nms[r][c] = edge_map[r][c]
# 左上方和左方的插值,右下方和右方的插值
if 0 < angle <= 45 or -180 < angle <= -135:
ratio = dx[r][c] / dy[r][c]
right_bottom_right = ratio * edge_map[r+1][c+1] + (1-ratio) * edge_map[r][c+1]
left_top_left = ratio * edge_map[r-1][c-1] + (1 - ratio) * edge_map[r][c-1]
if edge_map[r][c] > right_bottom_right and edge_map[r][c] > left_top_left:
edge_map_nms[r][c] = edge_map[r][c]
# 右上方和右方的插值,左下方和左方的插值
if 135 < angle <= 180 or -45 < angle < 0:
ratio = dx[r][c] / dy[r][c]
right_top_right = ratio * edge_map[r - 1][c + 1] + (1 - ratio) * edge_map[r][c + 1]
left_bottom_left = ratio * edge_map[r + 1][c - 1] + (1 - ratio) * edge_map[r][c - 1]
if edge_map[r][c] > right_top_right and edge_map[r][c] > left_bottom_left:
edge_map_nms[r][c] = edge_map[r][c]
return edge_map_nms
# 判断一个点的做笔哦啊是否在图像范围内
def check_in_range(r, c, rows, cols):
if 0 <= r < rows and 0 <= c < cols:
return True
return False
def trace(edge_map_nms, edge, low_thresh, r, c, rows, cols):
# 大于高阈值的点为确定边缘点
if edge[r][c] == 0:
edge[r][c] = 255
for i in range(-1, 2):
for j in range(-1, 2):
if check_in_range(r+i, c+j, rows, cols) and edge_map_nms[r+i][c+j] >= low_thresh:
trace(edge_map_nms, edge, low_thresh, r+i, c+j, rows, cols)
# 滞后阈值处理
def hysteresis_threshold(edge_nms, low_thresh, upper_thresh):
rows, cols = edge_nms.shape
edge = np.zeros(edge_nms.shape, np.uint8)
for r in range(1, rows-1):
for c in range(1, cols-1):
# 大于高阈值的点被设置为确定边缘点,而且以该点为起点延长边缘
if edge_nms[r][c] >= upper_thresh:
trace(edge_nms, edge, low_thresh, r, c, rows, cols)
# 小于低阈值的点被剔除
if edge_nms[r][c] < low_thresh:
edge[r][c] = 0
return edge
if __name__ == '__main__':
image = cv.imread("img9.jpg", 0)
# Canny 边缘检测
# 1.基于 sobel 核的卷积
image_sobel_x, image_sobel_y = sobel(image, 3)
# 2.边缘强度
edge = np.sqrt(np.power(image_sobel_x, 2.0) + np.power(image_sobel_y, 2.0))
edge[edge>255] = 255
edge = edge.astype(np.uint8)
cv.imwrite('./images/img9_sobel.jpg', edge)
cv.imshow("sobel edge",edge)
# 3.非极大值抑制
edge_map_nms = nms_default(image_sobel_x, image_sobel_y)
edge_map_nms[edge_map_nms>255] = 255
edge_map_nms = edge_map_nms.astype(np.uint8)
cv.imwrite('./images/img9_nms.jpg', edge_map_nms)
cv.imshow("edgeMag_nonMaxSup",edge_map_nms)
# 4. 双阈值滞后阈值处理
edge = hysteresis_threshold(edge_map_nms, 60, 180)
cv.imwrite('./images/img9_canny.jpg', edge)
cv.imshow('canny', edge)
# 单阈值
lowEdge = np.copy(edge_map_nms)
lowEdge[lowEdge > 60] = 255
lowEdge[lowEdge < 60] = 0
cv.imwrite('./images/img9_low.jpg', lowEdge)
cv.imshow("lowEdge", lowEdge)
upperEdge = np.copy(edge_map_nms)
upperEdge[upperEdge > 180] = 255
upperEdge[upperEdge <= 180] = 0
cv.imwrite('./images/img9_upper.jpg', upperEdge)
cv.imshow("upperEdge", upperEdge)
cv.waitKey(0)
cv.destroyAllWindows()
非极大值抑制的图像比Sobel边缘强度图显得细化,使用滞后阈值处理的图像与单阈值图像相比,去除了低阈值图像的细小边缘,比高阈值图像边缘更加完整。
OpenCV 函数
Canny函数官方地址
void cv::Canny(InputArray image,
OutputArray edges,
double threshold1,
double threshold2,
int apertureSize = 3,
bool L2gradient = false
)
//Python:
edges = cv.Canny(image, threshold1, threshold2[, edges[, apertureSize[, L2gradient]]] )
参数 | 解释 |
---|---|
threshold1 | 低阈值 |
threshold2 | 高阈值 |
apertureSize | Sobel 核大小,默认 3x3 |
L2gradient | 计算边缘强度的方式,true表示平方和开方,false表示绝对值和的方式 |
下边的函数是使用带有自定义图像梯度的Canny算法在图像中寻找边缘。
void cv::Canny(InputArray dx,
InputArray dy,
OutputArray edges,
double threshold1,
double threshold2,
bool L2gradient = false
)
// Python:
edges = cv.Canny(dx, dy, threshold1, threshold2[, edges[, L2gradient]])
参数 | 解释 |
---|---|
dx | 输入图像在 x 方向上导数 |
dy | 输入图像在 y 方向上导数 |