OpenCV15-图像边缘检测:Sobel、Scharr、Laplace、Canny-CSDN博客

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

OpenCV15-图像边缘检测Sobel、Scharr、Laplace、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(x1,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(x1,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=[1001]YX=[0110]

图像的边缘有可能是由高像素值变为低像素值也有可能是由低像素值变成高像素值。通过上面的梯度公式得到正数值表示像素值突然由低变高得到的负数值表示像素值由高到低这两种都是图像的边缘因此为了在图像中同时表示出这两种边缘信息需要将计算的结果求取绝对值。

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} 121000121
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} 101202101
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= 31030003103 Gy= 30310010303
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)=x22f+y22f
对于二维离散图像而言图像的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(x1,y)+f(x,y+1)+f(x,y1)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= 010141010 G2= 111181111
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:13: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;
}

Canny边缘检测

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6