OpenCV-边缘检测

Canny算子

Canny边缘检测算子是John F.Canny于 1986 年开发出来的一个多级边缘检测算法。

三个主要评价标准

其中,Canny 的目标是找到一个最优的边缘检测算法,最优边缘检测的评价标准如下:

  1. 低错误率: 标识出尽可能多的实际边缘,同时尽可能的减少噪声产生的误报。
  2. 高定位性: 标识出的边缘要与图像中的实际边缘尽可能接近。
  3. 最小响应: 图像中的边缘只能标识一次,并且可能存在的图像噪声不应标识为边缘。

Canny算子的步骤

  1. 消除噪声。一般情况下,使用高斯平滑滤波器卷积降噪。
  2. 计算梯度幅值和方向,此处,按照Sobel滤波器的步骤
    1. 运用一对卷积阵列(分别作用于 x 和 y 方向)
    2. 使用下列公式计算梯度幅值和方向,梯度方向近似到四个可能角度之一(一般为0, 45, 90, 135)
1
2
3
G = \sqrt{Gx^2+Gy^2}

θ = arctan(\frac {Gy}{Gx})
  1. 非极大值抑制。这一步排除非边缘像素,仅仅保留了一些细线条(候选边缘)。
  2. 滞后阈值。最后一步,Canny 使用了滞后阈值,滞后阈值需要两个阈值(高阈值和低阈值):
    1. 如果某一像素位置的幅值超过高阈值, 该像素被保留为边缘像素。
    2. 如果某一像素位置的幅值小于低阈值, 该像素被排除。
    3. 如果某一像素位置的幅值在两个阈值之间,该像素仅仅在连接到一高于 高阈值的像素时被保留。

函数原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
* image 输入图像,需为单通道 8 位图像
* edges 输出图像,size 与 type 与原始图像相同
* threshold1 第一个滞后阈值
* threshold2 第二个滞后阈值
* apertureSize 表示应用 Sobel 算子的孔径大小,其有默认值3
* L2gradient 一个计算图像梯度幅值的标识,有默认值 false
*/
void Canny( InputArray image,
OutputArray edges,
double threshold1,
double threshold2,
int apertureSize = 3,
bool L2gradient = false );

实例

1
2
3
Mat mat_base = imread("fish.png");
Mat mat_dst;
Canny(mat_base, mat_dst, 3, 9);

Sobel算子

Sobel 算子是一个主要用作边缘检测的离散微分算子,他结合了高斯平滑和微分求导,用来计算图像灰度函数的近似梯度。在图像的任何一点使用此算子,将会产生对应的梯度矢量或是其法矢量。

步骤

假设被操作的图像为I

  1. 分别在x和y两个方向求导
    1. 水平变化: 将 I 与一个奇数大小的内核进行卷积。
    2. 垂直变化:将I与一个奇数大小的内核进行卷积。比如,当内核大小为3时, 的计算结果为:
1
2
3
4
5
6
7
8
9
10
11
12
13
Gx = \begin{bmatrix}
-1&0&+1\\
-2&0&+2\\
-1&0&+1\\
\end{bmatrix}
* I

Gy = \begin{bmatrix}
-1&-2&-1\\
0&0&0\\
-1&-2&-1\\
\end{bmatrix}
* I

2.在图像的每一点,结合以上两个结果求出近似梯度:

1
G = \sqrt{Gx^2+Gy^2}

函数原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*
* src 输入图像
* dst 输出图像,size 和 channels 与输入图像相同
* ddepth 输出图像深度,参照下表
Input depth (src.depth()) | Output depth (ddepth)
--------------------------|----------------------
CV_8U | -1/CV_16S/CV_32F/CV_64F
CV_16U/CV_16S | -1/CV_32F/CV_64F
CV_32F | -1/CV_32F/CV_64F
CV_64F | -1/CV_64F
* dx y 方向上的差分阶数
* dy y 方向上的差分阶数
* ksize 扩展 Sobel 核的大小;它必须是1、3、5或7。
* scale 计算导数值的可选比例因子;默认情况下,不应用缩放
* delta 在将结果存储到dst之前添加到结果中的可选增量值。
* borderType 推断边缘类型
*/
void Sobel( InputArray src,
OutputArray dst,
int ddepth,
int dx,
int dy,
int ksize = 3,
double scale = 1,
double delta = 0,
int borderType = BORDER_DEFAULT );

补充

  1. 当内核大小为 3 时, 我们的 Sobel 内核可能产生比较明显的误差(毕竟, Sobel 算子只是求取了导数的近似值而已)。 为解决这一问题,OpenCV 提供了 Scharr 函数,但该函数仅作用于大小为3的内核。该函数的运算与 Sobel 函数一样快,但结果却更加精确,其内核是这样的:
1
2
3
4
5
6
7
8
9
10
Gx = 
\begin{bmatrix}
-3&0&+3\\
-10&0&+10\\
-3&0&+3\\
\end{bmatrix} Gy = \begin{bmatrix}
-3&-10&-3\\
0&0&0\\
+3&+10&+3\\
\end{bmatrix}
  1. 因为 Sobel 算子结合了高斯平滑和分化,因此结果会具有更多的抗噪性。大多数情况下,我们使用 Sobel 函数时,取[dx = 1,dy = 0,ksize = 3]来计算图像 x 方向的导数,[dx = 0,dy = 1,ksize = 3]来计算图像 y方向的导数。

实例

1
2
3
4
5
6
7
Mat mat_base = imread("fish.png");
Mat grad_x, grad_y, abs_grad_x, abs_grad_y, mat_dst;
Sobel(mat_base, grad_x, CV_16S, 1, 0);
convertScaleAbs(grad_x, abs_grad_x); //x方向Sobel
Sobel(mat_base, grad_y, CV_16S, 0, 1);
convertScaleAbs(grad_y, abs_grad_y); //y方向Sobel
addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0, mat_dst); //整体方向Sobel

Laplace算子

Laplacian 算子是n维欧几里德空间中的一个二阶微分算子,定义为梯度 grad()的散度 div()。因此如果f是二阶可微的实函数,则f的 Laplacian 算子定义为:

  1. f的拉普拉斯算子也是笛卡儿坐标系xi中的所有非混合二阶偏导数求和:

  2. 作为一个二阶微分算子,拉普拉斯算子把C函数映射到C函数,对于 k ≥ 2。表达式(1)(或(2))定义了一个算子 Δ :C(R) → C(R),或更一般地,定义了一个算子 Δ :C(Ω) → C(Ω),对于任何开集 Ω。

需要点破的是,由于 Laplacian 使用了图像梯度,它内部的代码其实是调用了 Sobel 算子的。

另附一个小tips:让一幅图像减去它的Laplacian可以增强对比度。

Laplacian 算子的定义:

1
Laplacian(f) = \frac{∂^2f}{∂^2x} + \frac{∂^2f}{∂^2y}

函数原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
* src 输入图像
* dst 输出图像,size 和 channels 与输入图像相同
* ddepth 输出图像深度
* ksize 用于计算二阶导数的滤波器的孔径尺寸,它必须是正奇数。
* scale 计算拉普拉斯值的时候可选的比例因子
* delta 在将结果存储到dst之前添加到结果中的可选增量值
* borderType 推断边缘类型
*/
void Laplacian( InputArray src,
OutputArray dst,
int ddepth,
int ksize = 1,
double scale = 1,
double delta = 0,
int borderType = BORDER_DEFAULT );

补充

Laplacian 函数其实主要是利用 sobel 算子的运算。它通过加上 sobel 算子运算出的图像 x 方向和 y 方向上的导数,来得到我们载入图像的拉普拉斯变换结果。

其中,sobel算子(ksize>1)如下

1
dst = Δ src = \frac{∂^2src}{∂^x} + \frac{∂^2src}{∂^y}

而当ksize = 1 时,Laplacian 函数采用以下 3 × 3 的孔径

1
2
3
4
5
\begin{bmatrix}
0&1&0\\
1&-4&1\\
0&1&0\\
\end{bmatrix}

实例

1
2
3
4
5
6
Mat mat_base = imread("fish.png");
Mat mat_gray, mat_dst;
GaussianBlur(mat_base, mat_base, Size(3, 3), 0);
cvtColor(mat_base, mat_gray, COLOR_RGB2GRAY);
Laplacian(mat_gray, mat_dst, CV_16S);
convertScaleAbs(mat_dst, mat_dst);

Scharr滤波器

scharr 一般直接称为滤波器,而不是算子。它在OpenCV中主要是配合 Sobel 算子的运算而存在的。

函数原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
* src 输入图像
* dst 输出图像,size 和 channels 与输入图像相同
* ddepth 输出图像深度,参照下表
Input depth (src.depth()) | Output depth (ddepth)
--------------------------|----------------------
CV_8U | -1/CV_16S/CV_32F/CV_64F
CV_16U/CV_16S | -1/CV_32F/CV_64F
CV_32F | -1/CV_32F/CV_64F
CV_64F | -1/CV_64F
* dx y 方向上的差分阶数
* dy y 方向上的差分阶数
* scale 计算导数值的可选比例因子;默认情况下,不应用缩放
* delta 在将结果存储到dst之前添加到结果中的可选增量值。
* borderType 推断边缘类型
*/
void Scharr( InputArray src,
OutputArray dst,
int ddepth,
int dx,
int dy,
double scale = 1,
double delta = 0,
int borderType = BORDER_DEFAULT );

实例

1
2
3
4
5
6
7
Mat mat_base = imread("fish.png");
Mat grad_x, grad_y, abs_grad_x, abs_grad_y, mat_dst;
Scharr(mat_base, grad_x, CV_16S, 1, 0);
convertScaleAbs(grad_x, abs_grad_x);
Scharr(mat_base, grad_y, CV_16S, 0, 1);
convertScaleAbs(grad_y, abs_grad_y);
addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0, mat_dst);