OpenCV4入门教程067:边缘检测

索引地址:系列索引

边缘检测

边缘检测的目的是标识数字图像中亮度变化明显的点。图像属性中的显著变化通常反映了属性的重要事件和变化。 这些包括
(i)深度上的不连续、
(ii)表面方向不连续、
(iii)物质属性变化、
(iv)场景照明变化。

边缘检测是图像处理和计算机视觉中,尤其是特征提取中的一个研究领域。

图像边缘检测大幅度地减少了数据量,并且剔除了可以认为不相关的信息,保留了图像重要的结构属性。有许多方法用于边缘检测,它们的绝大部分可以划分为两类:基于查找一类和基于零穿越的一类。基于查找的方法通过寻找图像一阶导数中的最大和最小值来检测边界,通常是将边界定位在梯度最大的方向。基于零穿越的方法通过寻找图像二阶导数零穿越来寻找边界,通常是Laplacian过零点或者非线性差分表示的过零点。

所谓边缘是指其周围像素灰度急剧变化的那些象素的集合,它是图像最基本的特征。边缘存在于目标、背景和区域之间,所以,它是图像分割所依赖的最重要的依据。由于边缘是位置的标志,对灰度的变化不敏感,因此,边缘也是图像匹配的重要的特征。

边缘检测和区域划分是图像分割的两种不同的方法,二者具有相互补充的特点。在边缘检测中,是提取图像中不连续部分的特征,根据闭合的边缘确定区域。而在区域划分中,是把图像分割成特征相同的区域,区域之间的边界就是边缘。由于边缘检测方法不需要将图像逐个像素地分割,因此更适合大图像的分割。

边缘大致可以分为两种,一种是阶跃状边缘,边缘两边像素的灰度值明显不同;另一种为屋顶状边缘,边缘处于灰度值由小到大再到小的变化转折点处。边缘检测的主要工具是边缘检测模板。我们以一个一维模板为例来考察边缘检测模板是如何作用的。

模板的作用是将右邻点的灰度值减去左邻点的灰度值作为该点的灰度值。在灰度相近的区域内,这么做的结果使得该点的灰度值接近于0;而在边缘附近,灰度值有明显的跳变,这么做的结果使得该点的灰度值很大,这样就出现了上面的结果。这种模板就是一种边缘检测器,它在数学上的涵义是一种基于梯度的滤波器,习惯上又称边缘算子。我们知道,梯度是有方向的,和边缘的方向总是垂直的。模板 是水平方向的,而上面那幅图象的边缘恰好是垂直方向的,使用模板 就可以将它检测出来。如果图象的边缘是水平方向的,我们可以用梯度是垂直方向的模板检测它的边缘。如果图象的边缘是45。方向的,我们可以用模板检测它的边缘。

常用的边缘检测模板有Laplacian算子、Roberts算子、Sobel算子、log(Laplacian-Gauss)算子、Kirsch算子和Prewitt算子等。

Canny

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 <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/opencv.hpp>
using namespace cv;

//-----------------------------------【main( )函数】-------------------------------------------
// 描述:控制台应用程序的入口函数,我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
int main()
{
//载入原始图
Mat srcImage = imread("lena.jpg"); //工程目录下应该有一张名为1.jpg的素材图
Mat srcImage1 = srcImage.clone();

//显示原始图
imshow("Original", srcImage);
//----------------------------------------------------------------------------------
// 二、高阶的canny用法,转成灰度图,降噪,用canny,最后将得到的边缘作为掩码,拷贝原图到效果图上,得到彩色的边缘图
//----------------------------------------------------------------------------------
Mat dstImage, edge, grayImage;

// 【1】创建与src同类型和大小的矩阵(dst)
dstImage.create(srcImage1.size(), srcImage1.type());

// 【2】将原图像转换为灰度图像
cvtColor(srcImage1, grayImage, COLOR_BGR2GRAY);

// 【3】先用使用 3x3内核来降噪
blur(grayImage, edge, Size(3, 3));

// 【4】运行Canny算子
Canny(edge, edge, 3, 9, 3);

//【5】将g_dstImage内的所有元素设置为0
dstImage = Scalar::all(0);

//【6】使用Canny算子输出的边缘图g_cannyDetectedEdges作为掩码,来将原图g_srcImage拷到目标图g_dstImage中
srcImage1.copyTo(dstImage, edge);

//【7】显示效果图
imshow("Result", dstImage);

waitKey(0);

return 0;
}

测试效果:

canny

Marr-Hildreth

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// 功能:代码 5-15 Marr-Hildreth 边缘检测
// 作者:朱伟 zhu1988wei@163.com
// 来源:《OpenCV图像处理编程实例》
// 博客:http://blog.csdn.net/zhuwei1988
// 更新:2016-8-1
// 说明:版权所有,引用或摘录请联系作者,并按照上面格式注明出处,谢谢。//
#include "opencv2/imgproc/imgproc.hpp"
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace std;
using namespace cv;

void marrEdge(const Mat src, Mat &result, int kerValue, double delta)
{
// 计算LOG算子
Mat kernel;
// 半径
int kerLen = kerValue / 2;
kernel = Mat_<double>(kerValue, kerValue);
// 滑窗
for (int i = -kerLen; i <= kerLen; i++)
{
for (int j = -kerLen; j <= kerLen; j++)
{
// 核因子生成
kernel.at<double>(i + kerLen, j + kerLen) =
exp(-((pow(j, 2) + pow(i, 2)) / (pow(delta, 2) * 2))) *
(((pow(j, 2) + pow(i, 2) - 2 * pow(delta, 2)) / (2 * pow(delta, 4))));
}
}
// 输出参数设置
int kerOffset = kerValue / 2;
Mat laplacian = (Mat_<double>(src.rows - kerOffset * 2, src.cols - kerOffset * 2));
result = Mat::zeros(src.rows - kerOffset * 2, src.cols - kerOffset * 2, src.type());
double sumLaplacian;
// 遍历计算卷积图像的Lapace算子
for (int i = kerOffset; i < src.rows - kerOffset; ++i)
{
for (int j = kerOffset; j < src.cols - kerOffset; ++j)
{
sumLaplacian = 0;
for (int k = -kerOffset; k <= kerOffset; ++k)
{
for (int m = -kerOffset; m <= kerOffset; ++m)
{
// 计算图像卷积
sumLaplacian += src.at<uchar>(i + k, j + m) *
kernel.at<double>(kerOffset + k, kerOffset + m);
}
}
// 生成Lapace结果
laplacian.at<double>(i - kerOffset, j - kerOffset) = sumLaplacian;
}
}
// 过零点交叉 寻找边缘像素
for (int y = 1; y < result.rows - 1; ++y)
{
for (int x = 1; x < result.cols - 1; ++x)
{
result.at<uchar>(y, x) = 0;
// 邻域判定
if (laplacian.at<double>(y - 1, x) * laplacian.at<double>(y + 1, x) < 0)
{
result.at<uchar>(y, x) = 255;
}
if (laplacian.at<double>(y, x - 1) * laplacian.at<double>(y, x + 1) < 0)
{
result.at<uchar>(y, x) = 255;
}
if (laplacian.at<double>(y + 1, x - 1) * laplacian.at<double>(y - 1, x + 1) < 0)
{
result.at<uchar>(y, x) = 255;
}
if (laplacian.at<double>(y - 1, x - 1) * laplacian.at<double>(y + 1, x + 1) < 0)
{
result.at<uchar>(y, x) = 255;
}
}
}
}
int main()
{
cv::Mat srcImage = cv::imread("lena.jpg", 0);
if (!srcImage.data)
return -1;
cv::Mat edge;
marrEdge(srcImage, edge, 9, 1.6);
cv::imshow("srcImage", srcImage);
cv::imshow("edge", edge);
cv::waitKey(0);
return 0;
}

效果:

marr-hildreth

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
//-----------------------------------【头文件包含部分】---------------------------------------
// 描述:包含程序所依赖的头文件
//----------------------------------------------------------------------------------------------
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/opencv.hpp>

//-----------------------------------【命名空间声明部分】---------------------------------------
// 描述:包含程序所使用的命名空间
//-----------------------------------------------------------------------------------------------
using namespace cv;
//-----------------------------------【main( )函数】--------------------------------------------
// 描述:控制台应用程序的入口函数,我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
int main()
{
//【0】创建 grad_x 和 grad_y 矩阵
Mat grad_x, grad_y;
Mat abs_grad_x, abs_grad_y, dst;

//【1】载入原始图
Mat src = imread("lena.jpg"); //工程目录下应该有一张名为1.jpg的素材图

//【2】显示原始图
imshow("Original", src);

//【3】求 X方向梯度
Sobel(src, grad_x, CV_16S, 1, 0, 3, 1, 1, BORDER_DEFAULT);
convertScaleAbs(grad_x, abs_grad_x);
imshow("X-Sobel", abs_grad_x);

//【4】求Y方向梯度
Sobel(src, grad_y, CV_16S, 0, 1, 3, 1, 1, BORDER_DEFAULT);
convertScaleAbs(grad_y, abs_grad_y);
imshow("Y-Sobel", abs_grad_y);

//【5】合并梯度(近似)
addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0, dst);
imshow("Sobel Result", dst);

waitKey(0);
return 0;
}

测试效果:

sobel

laplacian

拉普拉斯算子

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
//---------------------------------【头文件、命名空间包含部分】----------------------------
// 描述:包含程序所使用的头文件和命名空间
//------------------------------------------------------------------------------------------------
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/opencv.hpp>
using namespace cv;

//-----------------------------------【main( )函数】--------------------------------------------
// 描述:控制台应用程序的入口函数,我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
int main()
{
//【0】变量的定义
Mat src, src_gray, dst, abs_dst;

//【1】载入原始图
src = imread("lena.jpg"); //工程目录下应该有一张名为1.jpg的素材图

//【2】显示原始图
imshow("Original", src);

//【3】使用高斯滤波消除噪声
GaussianBlur(src, src, Size(3, 3), 0, 0, BORDER_DEFAULT);

//【4】转换为灰度图
cvtColor(src, src_gray, COLOR_RGB2GRAY);

//【5】使用Laplace函数
Laplacian(src_gray, dst, CV_16S, 3, 1, 0, BORDER_DEFAULT);

//【6】计算绝对值,并将结果转换成8位
convertScaleAbs(dst, abs_dst);

//【7】显示效果图
imshow("Result", abs_dst);

waitKey(0);

return 0;
}

测试效果:

laplacian

scharr

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
//---------------------------------【头文件、命名空间包含部分】----------------------------
// 描述:包含程序所使用的头文件和命名空间
//------------------------------------------------------------------------------------------------
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/opencv.hpp>
using namespace cv;

//-----------------------------------【main( )函数】--------------------------------------------
// 描述:控制台应用程序的入口函数,我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
int main()
{
//【0】创建 grad_x 和 grad_y 矩阵
Mat grad_x, grad_y;
Mat abs_grad_x, abs_grad_y, dst;

//【1】载入原始图
Mat src = imread("lena.jpg"); //工程目录下应该有一张名为1.jpg的素材图

//【2】显示原始图
imshow("Original", src);

//【3】求 X方向梯度
Scharr(src, grad_x, CV_16S, 1, 0, 1, 0, BORDER_DEFAULT);
convertScaleAbs(grad_x, abs_grad_x);
imshow("X-Scharr", abs_grad_x);

//【4】求Y方向梯度
Scharr(src, grad_y, CV_16S, 0, 1, 1, 0, BORDER_DEFAULT);
convertScaleAbs(grad_y, abs_grad_y);
imshow("Y-Scharr", abs_grad_y);

//【5】合并梯度(近似)
addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0, dst);

//【6】显示效果图
imshow("Scharr-result", dst);

waitKey(0);
return 0;
}

测试效果:

scharr

Roberts边缘检测

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
// 功能:代码 5-8 Roberts 边缘检测
// 作者:朱伟 zhu1988wei@163.com
// 来源:《OpenCV图像处理编程实例》
// 博客:http://blog.csdn.net/zhuwei1988
// 更新:2016-8-1
// 说明:版权所有,引用或摘录请联系作者,并按照上面格式注明出处,谢谢。//
#include <opencv2/opencv.hpp>
// roberts算子实现
cv::Mat roberts(cv::Mat srcImage)
{
cv::Mat dstImage = srcImage.clone();
int nRows = dstImage.rows;
int nCols = dstImage.cols;
for (int i = 0; i < nRows - 1; i++)
{
for (int j = 0; j < nCols - 1; j++)
{
// 根据公式计算
int t1 = (srcImage.at<uchar>(i, j) -
srcImage.at<uchar>(i + 1, j + 1)) *
(srcImage.at<uchar>(i, j) -
srcImage.at<uchar>(i + 1, j + 1));
int t2 = (srcImage.at<uchar>(i + 1, j) -
srcImage.at<uchar>(i, j + 1)) *
(srcImage.at<uchar>(i + 1, j) -
srcImage.at<uchar>(i, j + 1));
// 计算对角线像素差
dstImage.at<uchar>(i, j) = (uchar)sqrt(t1 + t2);
}
}
return dstImage;
}
int main()
{
cv::Mat srcImage =
cv::imread("lena.jpg", 0);
if (!srcImage.data)
return -1;
cv::imshow("srcImage", srcImage);
cv::Mat dstImage = roberts(srcImage);
cv::imshow("dstImage", dstImage);
cv::waitKey(0);
return 0;
}

测试效果:

roberts

差分边缘检测

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
50
51
52
// 功能:代码 5-1 差分边缘检测实现
// 作者:朱伟 zhu1988wei@163.com
// 来源:《OpenCV图像处理编程实例》
// 博客:http://blog.csdn.net/zhuwei1988
// 更新:2016-8-1
// 说明:版权所有,引用或摘录请联系作者,并按照上面格式注明出处,谢谢。//
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/opencv.hpp>
using namespace cv;
// 图像差分操作
void diffOperation(const cv::Mat srcImage, cv::Mat &edgeXImage,
cv::Mat &edgeYImage)
{
cv::Mat tempImage = srcImage.clone();
int nRows = tempImage.rows;
int nCols = tempImage.cols;
for (int i = 0; i < nRows - 1; i++)
{
for (int j = 0; j < nCols - 1; j++)
{
// 计算垂直边边缘
edgeXImage.at<uchar>(i, j) =
abs(tempImage.at<uchar>(i + 1, j) -
tempImage.at<uchar>(i, j));
// 计算水平边缘
edgeYImage.at<uchar>(i, j) =
abs(tempImage.at<uchar>(i, j + 1) -
tempImage.at<uchar>(i, j));
}
}
}
int main()
{
cv::Mat srcImage = cv::imread("lena.jpg", 0);
if (!srcImage.data)
return -1;
cv::imshow("srcImage", srcImage);
cv::Mat edgeXImage(srcImage.size(), srcImage.type());
cv::Mat edgeYImage(srcImage.size(), srcImage.type());
// 计算查分图像
diffOperation(srcImage, edgeXImage, edgeYImage);
cv::imshow("edgeXImage", edgeXImage);
cv::imshow("edgeYImage", edgeYImage);
cv::Mat edgeImage(srcImage.size(), srcImage.type());
// 水平与垂直边缘图像叠加
addWeighted(edgeXImage, 0.5, edgeYImage,
0.5, 0.0, edgeImage);
cv::imshow("edgeImage", edgeImage);
cv::waitKey(0);
return 0;
}

测试效果:

diffuse

综合测试

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
//---------------------------------【头文件、命名空间包含部分】----------------------------
// 描述:包含程序所使用的头文件和命名空间
//------------------------------------------------------------------------------------------------
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
using namespace cv;

//-----------------------------------【全局变量声明部分】--------------------------------------
// 描述:全局变量声明
//-----------------------------------------------------------------------------------------------
//原图,原图的灰度版,目标图
Mat g_srcImage, g_srcGrayImage, g_dstImage;

// Canny边缘检测相关变量
Mat g_cannyDetectedEdges;
int g_cannyLowThreshold = 1; // TrackBar位置参数

// Sobel边缘检测相关变量
Mat g_sobelGradient_X, g_sobelGradient_Y;
Mat g_sobelAbsGradient_X, g_sobelAbsGradient_Y;
int g_sobelKernelSize = 1; // TrackBar位置参数

// Scharr滤波器相关变量
Mat g_scharrGradient_X, g_scharrGradient_Y;
Mat g_scharrAbsGradient_X, g_scharrAbsGradient_Y;

//-----------------------------------【全局函数声明部分】--------------------------------------
// 描述:全局函数声明
//-----------------------------------------------------------------------------------------------
static void on_Canny(int, void *); // Canny边缘检测窗口滚动条的回调函数
static void on_Sobel(int, void *); // Sobel边缘检测窗口滚动条的回调函数
void Scharr(); //封装了Scharr边缘检测相关代码的函数

//-----------------------------------【main( )函数】--------------------------------------------
// 描述:控制台应用程序的入口函数,我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
int main(int argc, char **argv)
{
//载入原图
g_srcImage = imread("lena.jpg");
if (!g_srcImage.data)
{
printf("Oh,no,读取srcImage错误~! \n");
return false;
}

//显示原始图
imshow("Original", g_srcImage);

// 创建与src同类型和大小的矩阵(dst)
g_dstImage.create(g_srcImage.size(), g_srcImage.type());

// 将原图像转换为灰度图像
cvtColor(g_srcImage, g_srcGrayImage, COLOR_BGR2GRAY);

// 创建显示窗口
namedWindow("Canny Result", WINDOW_AUTOSIZE);
namedWindow("Sobel Result", WINDOW_AUTOSIZE);

// 创建trackbar
createTrackbar("参数值:", "Canny Result", &g_cannyLowThreshold, 120, on_Canny);
createTrackbar("参数值:", "Sobel Result", &g_sobelKernelSize, 3, on_Sobel);

// 调用回调函数
on_Canny(0, 0);
on_Sobel(0, 0);

//调用封装了Scharr边缘检测代码的函数
Scharr();

//轮询获取按键信息,若按下Q,程序退出
while ((char(waitKey(1)) != 'q'))
{
}

return 0;
}

//-----------------------------------【on_Canny( )函数】----------------------------------
// 描述:Canny边缘检测窗口滚动条的回调函数
//-----------------------------------------------------------------------------------------------
void on_Canny(int, void *)
{
// 先使用 3x3内核来降噪
blur(g_srcGrayImage, g_cannyDetectedEdges, Size(3, 3));

// 运行我们的Canny算子
Canny(g_cannyDetectedEdges, g_cannyDetectedEdges, g_cannyLowThreshold, g_cannyLowThreshold * 3,
3);

//先将g_dstImage内的所有元素设置为0
g_dstImage = Scalar::all(0);

//使用Canny算子输出的边缘图g_cannyDetectedEdges作为掩码,来将原图g_srcImage拷到目标图g_dstImage中
g_srcImage.copyTo(g_dstImage, g_cannyDetectedEdges);

//显示效果图
imshow("Canny Result", g_dstImage);
}

//-----------------------------------【on_Sobel( )函数】----------------------------------
// 描述:Sobel边缘检测窗口滚动条的回调函数
//-----------------------------------------------------------------------------------------
void on_Sobel(int, void *)
{
// 求 X方向梯度
Sobel(g_srcImage, g_sobelGradient_X, CV_16S, 1, 0, (2 * g_sobelKernelSize + 1), 1, 1,
BORDER_DEFAULT);
convertScaleAbs(g_sobelGradient_X, g_sobelAbsGradient_X); //计算绝对值,并将结果转换成8位

// 求Y方向梯度
Sobel(g_srcImage, g_sobelGradient_Y, CV_16S, 0, 1, (2 * g_sobelKernelSize + 1), 1, 1,
BORDER_DEFAULT);
convertScaleAbs(g_sobelGradient_Y, g_sobelAbsGradient_Y); //计算绝对值,并将结果转换成8位

// 合并梯度
addWeighted(g_sobelAbsGradient_X, 0.5, g_sobelAbsGradient_Y, 0.5, 0, g_dstImage);

//显示效果图
imshow("Sobel Result", g_dstImage);
}

//-----------------------------------【Scharr( )函数】----------------------------------
// 描述:封装了Scharr边缘检测相关代码的函数
//-----------------------------------------------------------------------------------------
void Scharr()
{
// 求 X方向梯度
Scharr(g_srcImage, g_scharrGradient_X, CV_16S, 1, 0, 1, 0, BORDER_DEFAULT);
convertScaleAbs(g_scharrGradient_X, g_scharrAbsGradient_X); //计算绝对值,并将结果转换成8位

// 求Y方向梯度
Scharr(g_srcImage, g_scharrGradient_Y, CV_16S, 0, 1, 1, 0, BORDER_DEFAULT);
convertScaleAbs(g_scharrGradient_Y, g_scharrAbsGradient_Y); //计算绝对值,并将结果转换成8位

// 合并梯度
addWeighted(g_scharrAbsGradient_X, 0.5, g_scharrAbsGradient_Y, 0.5, 0, g_dstImage);

//显示效果图
imshow("Scharr Result", g_dstImage);
}

测试效果:

prewitt


OpenCV4入门教程067:边缘检测
https://feater.top/opencv/opencv-edge-detection/
作者
JackeyLea
发布于
2020年6月25日
许可协议