OpenCV——内置边缘检测算子:Canny,Sobel 与 Laplace

Using Canny, Sobel and Laplace operators for edge detection

 Thistledown    March 19, 2019

边缘检测(Edge Detection)是图像处理的基础内容。本文中,我从OpenCV官网上下载了最新版本的 OpenCV 4.0.1(2018-12-22),借助官方文档和网络教程完成了环境配置与测试,具体步骤不再赘述。

在 OpenCV 中可用于边缘检测的算子主要有:

  • Canny 算子
  • Sobel 算子
  • Laplace 算子

Canny 算子

理论

Canny 算法是由 John F. Canny 于 1987 年在 A computational approach to edge detection 一文提出来的,它旨在满足三个主要标准:

  • 低错误率(Low error rate):意味着只检测实际存在的边缘。
  • 高定位性(Good localization):将检测到的边缘像素与实际边缘像素之间的距离最小化。
  • 最小响应(Minimal response):每个边缘只有一个检测器响应。

Canny 算子的步骤如下:

  1. 滤掉任何噪声。这一过程将使用高斯滤波器。例如,一个 5×5 的高斯内核如下所示:
  1. 计算图像的强度梯度。为此,我们遵循类似于 Sobel 算子的方法:

    • 在 $x$ 和 $y$ 方向应用一维卷积掩模:

    • 找到梯度强度和方向:

      将梯度方向四舍五入为四个可能的角度之一(即 0°,45°,90° 或 135°)。

  2. 非极大值抑制。删除非边缘部分的像素,仅保留作为候选边缘的细线。

  3. 滞后阈值(Hysteresis)。应用两个阈值(上限 upper 和下限 lower ):

    • 如果像素梯度高于上限,则将其视为边缘
    • 如果像素梯度低于下限,则舍弃
    • 如果像素梯度在两个阈值之间,则仅当它连接到高于上阈值的像素时才将其作为边缘

    建议的阈值:$2:1 \leq upper : lower \leq 3:1​$

应用

OpenCV 中 Canny 函数的用法为:

1
2
3
4
5
6
7
void cv::Canny(	inputArray	image,
               	OutputArray	edges,
              	double		threshold1,
              	double		threshold2,
              	int		apertureSize = 3,
              	bool		L2gradient = false
)
  • image:输入图像,即源图像,Mat 类单通道8位图像。
  • edges:输出的边缘图,需要和源图片有一样的尺寸和类型。
  • threshold1:第一个滞后性阈值。
  • threshold2:第二个滞后性阈值。
  • apertureSize:表示应用 Sobel 算子的孔径大小,其有默认值 3。
  • L2gradient:一个计算图像梯度幅值的标识,有默认值 false。

具体实现:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/opencv.hpp>

using namespace cv;

int main() {
	Mat src, src_gray;
	src = imread("lenna.tif");			// 读取图像
	if( src.empty() ) {
		printf("Error: can not open the image.\n");
		return 1;
	}
	cvtColor(src, src_gray, COLOR_BGR2GRAY);

	// Canny 算子
	Mat dst_canny = src.clone();		// 创建图像副本
	Canny(src, dst_canny, 150, 100);

	Mat dst_canny_2, edges_canny;
	dst_canny_2.create(src.size(), src.type());
	blur(src_gray, edges_canny, Size(3, 3));	// 用 3×3 内核降噪
	Canny(edges_canny, edges_canny, 3, 3 * 3, 3);	// 运行 Canny 算子
	dst_canny_2 = Scalar::all(0);

	// 使用 Canny 算子输出的边缘图 edges_canny 作为掩码,
	// 来将原图 src 拷贝到输出图像 dst_canny_2 中
	src.copyTo(dst_canny_2, edges_canny);

	imshow("边缘提取-原图", src);
	waitKey(2);

	imshow("边缘提取-Canny算子", dst_canny);
	waitKey(1);

	imshow("边缘提取-Canny算子【高级实现】", dst_canny_2);
	waitKey(0);

	return 0;
}

结果如下:

Sobel 算子

理论

  • Sobel 算子是一个离散微分算子。它计算图像强度函数的梯度的近似值。
  • Sobel 算子结合了高斯平滑和差分。

假设要操作的图像为 $I$:

  1. 分别计算两个方向的导数:

    • 水平变化:将 $I$ 与一个奇数大小(size)的内核 $G_x$ 进行卷积。比如,当内核大小为 3 时,$G_x$ 为:

    • 垂直变化:将 $I$ 与一个奇数大小(size)的内核 $G_y$ 进行卷积。比如,当内核大小为 3 时,$G_y$ 为:

  2. 在图像的每个点,我们通过组合上面的两个结果来计算该点梯度的近似值:

    虽然有时会使用以下更简单的公式:

注意:

当内核的大小(size)为 3 时,上述 Sobel 内核可能会有很明显的误差(毕竟 Sobel 算子只是计算导数的近似值)。OpenCV 通过使用 Scharr() 函数解决了大小为 3 的内核存在的这种误差。这比标准 Sobel 函数更快但更准确。具体内核如下:

应用

OpenCV 中 Sobel 函数的用法为:

1
2
3
4
5
6
7
8
9
10
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 
)	
  • src:输入图像

  • dst:与 src 具有相同大小和相同通道数的输出图像

  • ddepth:输出图像的深度,支持如下 src.depth()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:x 方向上的差分阶数。

  • dy:y 方向上的差分阶数。

  • ksize:Sobel 内核的大小;必须取 1,3,5 或 7。

  • scale:计算导数值时可选的缩放因子,默认值是1,表示默认情况下是没有应用缩放的。

  • delta:表示在结果存入 dst 之前可选的 delta 值。

  • borderType:边界模式,默认值为BORDER_DEFAULT。详见 BorderTypes

因为 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/opencv.hpp>

using namespace cv;

int main() {
	Mat src = imread("lenna.tif");			// 读取图像
	if( src.empty() ) {
		printf("Error: can not open the image.\n");
		return 1;
	}

	Mat src_blur, src_blur_gray, dst_sobel;

	// 高斯滤波器去噪(kernel size = 3)
	GaussianBlur(src, src_blur, Size(3, 3), 0, 0, BORDER_DEFAULT);

	// 将图像转换为灰度图像
	cvtColor(src_blur, src_blur_gray, COLOR_BGR2GRAY);

	Mat grad_x, grad_y;
	Mat abs_grad_x, abs_grad_y;

	// 分别求 X、Y 方向梯度
	Sobel(src_blur_gray, grad_x, CV_16S, 1, 0, 3, 1, 1, BORDER_DEFAULT);	
	Sobel(src_blur_gray, grad_y, CV_16S, 0, 1, 3, 1, 1, BORDER_DEFAULT);

	// 转换回 CV_8U
	convertScaleAbs(grad_x, abs_grad_x);
	convertScaleAbs(grad_y, abs_grad_y);

	// 合并梯度
	addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0, dst_sobel);

	imshow("边缘提取-原图", src);
	waitKey(3);

	imshow("边缘提取-Sobel算子-X方向", abs_grad_x);
	waitKey(2);

	imshow("边缘提取-Sobel算子-Y方向", abs_grad_y);
	waitKey(1);

	imshow("边缘提取-Sobel算子-整体方向", dst_sobel);
	waitKey(0);

	return 0;
}

Laplace 算子

理论

  1. 根据数字图像处理相关知识,我们知道二阶导数可用于检测边缘。由于图像是二维(2D)的,我们需要在两个方向上进行求导。

  2. Laplace 算子定义为:

  3. 在 OpenCV 中,Laplace 算子通过函数 Laplacian() 实现。实际上,由于拉普拉斯算子利用了图像的梯度,因此它在内部调用 Sobel 算子来执行其运算,

应用

OpenCV 中 Laplacian() 函数的用法为:

1
2
3
4
5
6
7
8
void cv::Laplacian( InputArray 	src,
                    OutputArray dst,
                    int 	ddepth,
                    int 	ksize = 1,
                    double 	scale = 1,
                    double 	delta = 0,
                    int 	borderType = BORDER_DEFAULT 
)	
  • src:输入图像
  • dst:与 src 具有相同大小和相同通道数的输出图像
  • ddepth:输出图像的深度。
  • ksize:用于计算二阶导数的滤波器的孔径尺寸;它可以是 FILTER_SCHARR、1、3、5 或 7。
  • scale:计算拉普拉斯值时可选的比例因子,默认值是1,表示默认情况下是没有应用缩放的。
  • delta:表示在结果存入 dst 之前可选的 delta 值。
  • borderType:边界模式,默认值为BORDER_DEFAULT。详见 BorderTypes

Laplacian() 函数主要利用 Sobel 算子进行运算。它通过加上 Sobel 算子运算出的图像 $x$ 方向和 $y$ 方向上的导数,来得到输入图像的拉普拉斯变换结果。当 ksize == 1 时,拉普拉斯函数的滤波窗口为:

具体实现:

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
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/opencv.hpp>

using namespace cv;

int main() {
	// 读取图像
	Mat src = imread("lenna.tif");			
	if( src.empty() ) {
		printf("Error: can not open the image.\n");
		return 1;
	}

	// Laplace 算子
	Mat src_blur, src_blur_gray;
	Mat dst_laplace, abs_dst_laplace;

	// 高斯滤波器去噪(kernel size = 3)
	GaussianBlur(src, src_blur, Size(3, 3), 0, 0, BORDER_DEFAULT);

	// 将图像转换为灰度图像
	cvtColor(src_blur, src_blur_gray, COLOR_BGR2GRAY);

	// 应用 Laplace 算子
	Laplacian(src_blur_gray, dst_laplace, CV_16S, 3, 1, 0, BORDER_DEFAULT);

	// 计算绝对值,将结果转换为 8 位
	convertScaleAbs(dst_laplace, abs_dst_laplace);

	imshow("边缘提取-原图", src);
	waitKey(1);

	imshow("边缘检测-Laplace算子", abs_dst_laplace);
	waitKey(0);

	return 0;
}

参考:

  1. 【OpenCV入门教程之十二】OpenCV边缘检测 - 毛星云(浅墨)的专栏 - CSDN博客
  2. OpenCV: Image Filtering
  3. OpenCV: Image Processing (imgproc module)
  4. OpenCV: Canny Edge Detector
  5. OpenCV: Sobel Derivatives
  6. OpenCV: Laplace Operator

Thistledown