OpenCV15-图像边缘检测:Sobel、Scharr、Laplace、Canny
- 1.边缘检测原理
- 2.Sobel算子
- 3.Scharr算子
- 4.生成边缘检测滤波器
- 5.Laplacian算子
- 6.Canny算法
1.边缘检测原理
图像的边缘指的是图像中像素灰度值突然发生变化的区域,如果将图像中的每一行像素和每一列像素都描述成一个关于灰度值的函数,那么图像的边缘对应在灰度值函数中是函数值突然变大的区域。函数值得变化趋势可以用导数描述,当函数值突然变大时,导数也必然会变大,而函数值变化较为平缓时,导数值也比较小,因此可以通过寻找导数值较大的区域寻找函数中突然变化的区域,进而确定图像中的边缘位置。
由于图像是练得信号,因此我们可以用临近的两个像素值来表示像素灰度值函数的导数,求导形式表示如下:
d
f
(
x
,
y
)
d
x
=
f
(
x
,
y
)
−
f
(
x
−
1
,
y
)
\frac{df(x,y)}{dx} = f(x,y) - f(x-1,y)
dxdf(x,y)=f(x,y)−f(x−1,y)
这种对x轴方向的滤波器为
[
−
1
1
]
\begin{bmatrix} -1 & 1 \end{bmatrix}
[−11],同样对y轴方向的求导对应的滤波器为
[
−
1
1
]
T
\begin{bmatrix} -1 & 1 \end{bmatrix}^T
[−11]T 。
而表示某个像素处的梯度,最接近的方式是求取前一个像素和后一个像素的差值,于是修改上式为:
d
f
(
x
,
y
)
d
x
=
f
(
x
+
1
,
y
)
−
f
(
x
−
1
,
y
)
2
\frac{df(x,y)}{dx} = \frac{f(x+1,y) - f(x-1,y)}{2}
dxdf(x,y)=2f(x+1,y)−f(x−1,y)
改进的求导方式对应的滤波器在 X 方向和 Y 方向分别为
[
−
0.5
0
0.5
]
\begin{bmatrix} -0.5 & 0 & 0.5 \end{bmatrix}
[−0.500.5] 和
[
−
0.5
0
0.5
]
T
\begin{bmatrix} -0.5 & 0 & 0.5 \end{bmatrix}^T
[−0.500.5]T 。
根据这种方式,也可以使用下面的滤波器计算 4 5 ∘ 45^\circ 45∘ 方向的梯度:
X Y = [ 1 0 0 − 1 ] Y X = [ 0 1 − 1 0 ] XY = \begin{bmatrix} 1 & 0 \\ 0 & -1 \\ \end{bmatrix} YX = \begin{bmatrix} 0 & 1 \\ -1 & 0 \\ \end{bmatrix} XY=[100−1]YX=[0−110]
图像的边缘有可能是由高像素值变为低像素值,也有可能是由低像素值变成高像素值。通过上面的梯度公式,得到正数值表示像素值突然由低变高,得到的负数值表示像素值由高到低,这两种都是图像的边缘,因此,为了在图像中同时表示出这两种边缘信息,需要将计算的结果求取绝对值。
OpenCV中提供了 convertScaleAbs()
函数用于计算矩阵中的所有数据的的绝对值:
void convertScaleAbs(
InputArray src, // 输入图像
OutputArray dst, // 输出图像
double alpha = 1, // 缩放因子
double beta = 0 // 偏置值
);
下面代码中给出了利用 filter2D()
函数实现图像边缘检测的算法,需要说明的是,由于求取边缘的结果可能会有负值,不在原始图像的 CV_8U 数据范围内,因此滤波后的图像数据类型不要用 “-1” ,而应该为 CV_16S。
#include <opencv2\opencv.hpp>
#include <opencv2/core/utils/logger.hpp> // debug no log
using namespace cv;
using namespace std;
int main()
{
cout << "OpenCV Version: " << CV_VERSION << endl;
utils::logging::setLogLevel(utils::logging::LOG_LEVEL_SILENT);
//创建边缘检测滤波器
Mat kernel1 = (Mat_<float>(1, 2) << 1, -1); //X方向边缘检测滤波器
Mat kernel2 = (Mat_<float>(1, 3) << 1, 0, -1); //X方向边缘检测滤波器
Mat kernel3 = (Mat_<float>(3, 1) << 1, 0, -1); //X方向边缘检测滤波器
Mat kernelXY = (Mat_<float>(2, 2) << 1, 0, 0, -1); //由左上到右下方向边缘检测滤波器
Mat kernelYX = (Mat_<float>(2, 2) << 0, -1, 1, 0); //由右上到左下方向边缘检测滤波器
//读取图像,黑白图像边缘检测结果较为明显
Mat img = imread("equalLena.png", IMREAD_ANYCOLOR);
if (img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
Mat result1, result2, result3, result4, result5, result6;
//检测图像边缘
//以[1 -1]检测水平方向边缘
filter2D(img, result1, CV_16S, kernel1);
convertScaleAbs(result1, result1);
//以[1 0 -1]检测水平方向边缘
filter2D(img, result2, CV_16S, kernel2);
convertScaleAbs(result2, result2);
//以[1 0 -1]'检测由垂直方向边缘
filter2D(img, result3, CV_16S, kernel3);
convertScaleAbs(result3, result3);
//整幅图像的边缘
result6 = result2 + result3;
//检测由左上到右下方向边缘
filter2D(img, result4, CV_16S, kernelXY);
convertScaleAbs(result4, result4);
//检测由右上到左下方向边缘
filter2D(img, result5, CV_16S, kernelYX);
convertScaleAbs(result5, result5);
//显示边缘检测结果
imshow("result1", result1);
imshow("result2", result2);
imshow("result3", result3);
imshow("result4", result4);
imshow("result5", result5);
imshow("result6", result6);
waitKey(0);
return 0;
}
2.Sobel算子
使用Sobel边缘检测算子提取图像边缘的过程:
1.提取 X 方向的边缘,X方向的一阶 Sobel 边缘检测算子:
[
−
1
0
1
−
2
0
2
−
1
0
1
]
\begin{bmatrix} -1 & 0 & 1 \\ -2 & 0 & 2 \\ -1 & 0 & 1 \\ \end{bmatrix}
−1−2−1000121
2.提取 Y 方向的边缘,Y方向的一阶 Sobel 边缘检测算子:
[
−
1
−
2
−
1
0
0
0
1
2
1
]
\begin{bmatrix} -1 & -2 & -1 \\ 0 & 0 & 0 \\ 1 & 2 & 1 \\ \end{bmatrix}
−101−202−101
3.综合两个方向的边缘信息得到整幅图像的边缘。由两个方向的边缘得到整幅图像的边缘有两种计算方式,第一种是求取两幅图像对应像素的像素值的绝对值之和,第二种是求取两幅图像对应像素值的平方和的二次方根:
I
(
x
,
y
)
=
I
x
(
x
,
y
)
2
+
I
y
(
x
,
y
)
2
I
(
x
,
y
)
=
∣
I
x
(
x
,
y
)
∣
+
∣
I
y
(
x
,
y
)
∣
\begin{align} I(x,y) &= \sqrt{I_x(x,y)^2 + I_y(x,y)^2} \\ I(x,y) &= |I_x(x,y)| + |I_y(x,y)| \\ \end{align}
I(x,y)I(x,y)=Ix(x,y)2+Iy(x,y)2=∣Ix(x,y)∣+∣Iy(x,y)∣
OpenCV提供了对图像提取 Sobel 边缘的 Sobel()
函数:
void Sobel(
InputArray src,
OutputArray dst,
int ddepth, // 输出图像的数据类型,-1表示自动选择
int dx, // X方向差分阶数,即使用几阶Sobel算子
int dy, // Y方向差分阶数,即使用几阶Sobel算子
int ksize = 3, // Sobel算子尺寸,必须是1,3,5,7
double scale = 1, // 对导数计算结果进行缩放
double delta = 0, // 偏置值
int borderType = BORDER_DEFAULT
);
dx、dy、ksize:任意一个方向的差分阶数都需要小于算子的尺寸。但有以下特殊情况:当ksize=1时,任意一个方向的阶数都需要小于3。
一般情况下,当差分阶数的最大值取1时,ksize取3;当差分阶数的最大值取2时,ksize取5;当差分阶数的最大值取3时,ksize取7;
当ksize=1时,程序中使用的算子尺寸不再是正方形,而是3x1或者1x3。
#include <opencv2\opencv.hpp>
#include <opencv2/core/utils/logger.hpp> // debug no log
using namespace cv;
using namespace std;
int main()
{
cout << "OpenCV Version: " << CV_VERSION << endl;
utils::logging::setLogLevel(utils::logging::LOG_LEVEL_SILENT);
//读取图像,黑白图像边缘检测结果较为明显
Mat img = imread("equalLena.png", IMREAD_ANYCOLOR);
if (img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
Mat resultX, resultY, resultXY;
//X方向一阶边缘
Sobel(img, resultX, CV_16S, 1, 0, 1);
convertScaleAbs(resultX, resultX);
//Y方向一阶边缘
Sobel(img, resultY, CV_16S, 0, 1, 3);
convertScaleAbs(resultY, resultY);
//整幅图像的一阶边缘
resultXY = resultX + resultY;
//显示图像
imshow("resultX", resultX);
imshow("resultY", resultY);
imshow("resultXY", resultXY);
waitKey(0);
return 0;
}
3.Scharr算子
虽然Sobel算子可以有效地提取图像边缘,但是对于图像中较弱的边缘提取效果较差。因此,为了能够有效地提出较弱的边缘,需要将像素值间的差距值增大。
Socharr算子是对Sobel算子差异性的增强,两者在检测图像边缘的原理和使用方式上相同,Scharr算子在X方向和Y方向的边缘检测算子:
G
x
=
[
−
3
0
3
−
10
0
10
−
3
0
3
]
G
y
=
[
−
3
−
10
−
3
0
0
0
3
10
3
]
G_x = \begin{bmatrix} -3 & 0 & 3 \\ -10 & 0 & 10 \\ -3 & 0 & 3 \\ \end{bmatrix} G_y = \begin{bmatrix} -3 & -10 & -3 \\ 0 & 0 & 0 \\ 3 & 10 & 3 \\ \end{bmatrix}
Gx=
−3−10−30003103
Gy=
−303−10010−303
OpenCV提供了对图像提取Scharr边缘的 Scharr()
函数:
void Scharr(
InputArray src,
OutputArray dst,
int ddepth,
int dx,
int dy,
double scale = 1,
double delta = 0,
int borderType = BORDER_DEFAULT
);
dx和dy只能有一个为1,并且不能同时为0。
下面代码中,分别提取X方向和Y方向边缘,并利用这两个方向的边缘求取整幅图像的边缘。可以看出Scharr算子比Sobel算子提取到更微弱的边缘。
#include <opencv2\opencv.hpp>
#include <opencv2/core/utils/logger.hpp> // debug no log
using namespace cv;
using namespace std;
int main()
{
cout << "OpenCV Version: " << CV_VERSION << endl;
utils::logging::setLogLevel(utils::logging::LOG_LEVEL_SILENT);
//读取图像,黑白图像边缘检测结果较为明显
Mat img = imread("equalLena.png", IMREAD_ANYDEPTH);
if (img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
Mat resultX, resultY, resultXY;
//X方向一阶边缘
Scharr(img, resultX, CV_16S, 1, 0);
convertScaleAbs(resultX, resultX);
//Y方向一阶边缘
Scharr(img, resultY, CV_16S, 0, 1);
convertScaleAbs(resultY, resultY);
//整幅图像的一阶边缘
resultXY = resultX + resultY;
//显示图像
imshow("resultX", resultX);
imshow("resultY", resultY);
imshow("resultXY", resultXY);
waitKey(0);
return 0;
}
4.生成边缘检测滤波器
Scharr算子只有上面的两种,而Sobel算子有不同的尺寸、不同阶数。OpenCV中提供了 getDerivKernels()
函数可以得到不同尺寸、不同阶数的Sobel算子和Scharr算子滤波器。
void getDerivKernels(
OutputArray kx, // 行滤波器系数输出矩阵 ksize x 1
OutputArray ky, // 列滤波器系数输出矩阵 ksize x 1
int dx, // X方向导数的阶次
int dy, // Y方向导数的阶次
int ksize, // 滤波器的大小可以为FILTER_SCHARR、1、3、5、7
bool normalize = false, // 是否归一化
int ktype = CV_32F // 滤波器系数类型,CV_32F、CV_64F
);
下面的例子中给出利用 getDerivKernels()
函数生成Sobel算子和Scharr算子的代码:
#include <opencv2\opencv.hpp>
#include <opencv2/core/utils/logger.hpp> // debug no log
using namespace cv;
using namespace std;
int main()
{
cout << "OpenCV Version: " << CV_VERSION << endl;
utils::logging::setLogLevel(utils::logging::LOG_LEVEL_SILENT);
Mat sobel_x1, sobel_y1, sobel_x2, sobel_y2, sobel_x3, sobel_y3; //存放分离的Sobel算子
Mat scharr_x, scharr_y; //存放分离的Scharr算子
Mat sobelX1, sobelX2, sobelX3, scharrX; //存放最终算子
//一阶X方向Sobel算子
getDerivKernels(sobel_x1, sobel_y1, 1, 0, 3);
sobel_x1 = sobel_x1.reshape(CV_8U, 1);
sobelX1 = sobel_y1 * sobel_x1; //计算滤波器
//二阶X方向Sobel算子
getDerivKernels(sobel_x2, sobel_y2, 2, 0, 5);
sobel_x2 = sobel_x2.reshape(CV_8U, 1);
sobelX2 = sobel_y2 * sobel_x2; //计算滤波器
//三阶X方向Sobel算子
getDerivKernels(sobel_x3, sobel_y3, 3, 0, 7);
sobel_x3 = sobel_x3.reshape(CV_8U, 1);
sobelX3 = sobel_y3 * sobel_x3; //计算滤波器
//X方向Scharr算子
getDerivKernels(scharr_x, scharr_y, 1, 0, FILTER_SCHARR);
scharr_x = scharr_x.reshape(CV_8U, 1);
scharrX = scharr_y * scharr_x; //计算滤波器
//输出结果
cout << "X方向一阶Sobel算子:" << endl << sobelX1 << endl;
cout << "X方向二阶Sobel算子:" << endl << sobelX2 << endl;
cout << "X方向三阶Sobel算子:" << endl << sobelX3 << endl;
cout << "X方向Scharr算子:" << endl << scharrX << endl;
waitKey(0);
return 0;
}
/*
X方向一阶Sobel算子:
[-1, 0, 1;
-2, 0, 2;
-1, 0, 1]
X方向二阶Sobel算子:
[1, 0, -2, 0, 1;
4, 0, -8, 0, 4;
6, 0, -12, 0, 6;
4, 0, -8, 0, 4;
1, 0, -2, 0, 1]
X方向三阶Sobel算子:
[-1, 0, 3, 0, -3, 0, 1;
-6, 0, 18, 0, -18, 0, 6;
-15, 0, 45, 0, -45, 0, 15;
-20, 0, 60, 0, -60, 0, 20;
-15, 0, 45, 0, -45, 0, 15;
-6, 0, 18, 0, -18, 0, 6;
-1, 0, 3, 0, -3, 0, 1]
X方向Scharr算子:
[-3, 0, 3;
-10, 0, 10;
-3, 0, 3]
*/
5.Laplacian算子
上述的边缘检测算子都具有方向性,因此都需要分别求取X方向的边缘和Y方向的边缘,之后将两个方向的边缘综合得到图像的整体边缘。Laplacian算子具有各个方向同性的特点,能够对任意方向的边缘进行提取,具有无方向性的优点。
Laplacian算子是一种二阶导数算子,对噪声比较敏感,因此常需要配合高斯滤波一起使用。
二维图像函数
f
(
x
,
y
)
f(x,y)
f(x,y) ,图像的Laplace运算二阶导数定义:
∇
2
f
(
x
,
y
)
=
∂
2
f
∂
x
2
+
∂
2
f
∂
y
2
\nabla^2 f(x,y) = \frac{\partial^2 f}{\partial x^2} + \frac{\partial^2 f}{\partial y^2}
∇2f(x,y)=∂x2∂2f+∂y2∂2f
对于二维离散图像而言,图像的Laplace可表示吐下:
∇
2
f
(
x
,
y
)
=
f
(
x
+
1
,
y
)
+
f
(
x
−
1
,
y
)
+
f
(
x
,
y
+
1
)
+
f
(
x
,
y
−
1
)
−
4
f
(
x
,
y
)
\nabla^2 f(x,y) = f(x+1,y) + f(x-1,y) + f(x,y+1) + f(x,y-1) - 4f(x,y)
∇2f(x,y)=f(x+1,y)+f(x−1,y)+f(x,y+1)+f(x,y−1)−4f(x,y)
根据离散Laplace表达式,可以得到其滤波器:
G
1
=
[
0
1
0
1
−
4
1
0
1
0
]
G
2
=
[
1
1
1
1
−
8
1
1
1
1
]
G_1 = \begin{bmatrix} 0 & 1 & 0 \\ 1 & -4 & 1 \\ 0 & 1 & 0 \\ \end{bmatrix} G_2 = \begin{bmatrix} 1 & 1 & 1 \\ 1 & -8 & 1 \\ 1 & 1 & 1 \\ \end{bmatrix}
G1=
0101−41010
G2=
1111−81111
G
1
G_1
G1 与
G
2
G_2
G2 分别为离散拉普拉斯算子的模版与拓展模版,利用函数模版可以将图像中的奇异点如亮点变得更亮。对于图像中灰度变化剧烈的区域,拉普拉斯算子能实现其边缘检测。
OpenCV提供了Laplacian算子提取图像边缘的 Laplacian()
函数:
void Laplacian(
InputArray src,
OutputArray dst,
int ddepth,
int ksize = 1, // 滤波器大小,必须为正奇数
double scale = 1,
double delta = 0,
int borderType = BORDER_DEFAULT
);
ksize=1时,采用 G 1 G_1 G1 拉普拉斯算子。
下面代码中,采用图像去噪后通过拉普拉斯算子提取边缘变得更加准确:
#include <opencv2\opencv.hpp>
#include <opencv2/core/utils/logger.hpp> // debug no log
using namespace cv;
using namespace std;
int main()
{
cout << "OpenCV Version: " << CV_VERSION << endl;
utils::logging::setLogLevel(utils::logging::LOG_LEVEL_SILENT);
//读取图像,黑白图像边缘检测结果较为明显
Mat img = imread("equalLena.png", IMREAD_ANYDEPTH);
if (img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
Mat result, result_g, result_G;
//未滤波提取边缘
Laplacian(img, result, CV_16S, 3, 1, 0);
convertScaleAbs(result, result);
//滤波后提取Laplacian边缘
GaussianBlur(img, result_g, Size(3, 3), 5, 0); //高斯滤波
Laplacian(result_g, result_G, CV_16S, 3, 1, 0);
convertScaleAbs(result_G, result_G);
//显示图像
imshow("result", result);
imshow("result_G", result_G);
waitKey(0);
return 0;
}
6.Canny算法
Canny算法不容易受到噪声的影响,能够识别图像中的若边缘和强边缘,并结合强弱边缘的位置关系,综合给出图像整体的边缘信息。Canny边缘检测算法是目前最优越的边缘检测算法之一。该方法的检测过程:
1.使用高斯滤波去噪,平滑图像,下面是5x5的高斯滤波器:
G
=
1
139
[
2
4
5
4
2
4
9
12
9
4
5
12
15
12
5
4
9
12
9
4
2
4
5
4
2
]
G = \frac{1}{139} \begin{bmatrix} 2 & 4 & 5 & 4 & 2 \\ 4 & 9 & 12 & 9 & 4 \\ 5 & 12 & 15 & 12 & 5 \\ 4 & 9 & 12 & 9 & 4 \\ 2 & 4 & 5 & 4 & 2 \\ \end{bmatrix}
G=1391
245424912945121512549129424542
2.计算图像中每个像素的梯度幅值与方向。可以通过Sobel算子分别检测图像X方向和Y方向边缘,之后利用下面公式计算梯度的方向和幅值:
θ
=
a
r
c
t
a
n
(
I
y
I
x
)
G
=
a
r
c
t
a
n
I
x
2
+
I
y
2
\theta = arctan(\frac{I_y}{I_x}) \\ G = arctan\sqrt{I_x^2 + I_y^2}
θ=arctan(IxIy)G=arctanIx2+Iy2
其中梯度方向近似到下面4个取值:
0
∘
0^\circ
0∘、
4
5
∘
45^\circ
45∘、
9
0
∘
90^\circ
90∘、
13
5
∘
135^\circ
135∘
3.利用非极大值抑制算法消除边缘检测带来的杂散响应。通俗意义上是指寻找像素点局部最大值,将非极大值点所对应的灰度值设置为背景像素点,如像素领域区域满足梯度值的局部最优值判断为该像素的边缘,对其余非极大值的相关信息进行抑制。
4.应用双阈值法划分强边缘和弱边缘。如果某一像素位置的幅值超过高阈值,该像素被保留为边缘;如果某一像素位置的幅值小于低阈值,该像素被排除;如果某一像素位置的幅值在两个阈值之间,该像素仅仅在连接到一个高于阈值的像素时被保留。推荐高与低阈值比在 2:1
到 3:1
之间。
Canny算法流程复杂,好在OpenCV中提供了 Canny()
函数实现Canny算法检测图像中的边缘:
void Canny(
InputArray image, // CV_8U
OutputArray edges,
double threshold1, // 第一个滞后阈值
double threshold2, // 第二个滞后阈值
int apertureSize = 3, // Sobel算子直径
bool L2gradient = false // 计算图像梯度幅值的方法是否使用L2范数
);
下面的代码中,通过设置不同的阈值来比较阈值的大小对图像边缘检测效果的影响,可以发现,较高的阈值会降低噪声信息的影响,但是也会减少边缘信息。
#include <opencv2\opencv.hpp>
#include <opencv2/core/utils/logger.hpp> // debug no log
using namespace cv;
using namespace std;
int main()
{
cout << "OpenCV Version: " << CV_VERSION << endl;
utils::logging::setLogLevel(utils::logging::LOG_LEVEL_SILENT);
//读取图像,黑白图像边缘检测结果较为明显
Mat img = imread("equalLena.png", IMREAD_ANYDEPTH);
if (img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
Mat resultHigh, resultLow, resultG;
//大阈值检测图像边缘
Canny(img, resultHigh, 100, 200, 3);
//小阈值检测图像边缘
Canny(img, resultLow, 20, 40, 3);
//高斯模糊后检测图像边缘
GaussianBlur(img, resultG, Size(3, 3), 5);
Canny(resultG, resultG, 100, 200, 3);
//显示图像
imshow("resultHigh", resultHigh);
imshow("resultLow", resultLow);
imshow("resultG", resultG);
waitKey(0);
return 0;
}