Task06 OpenCV框架实现常用边缘检测方法
一、前言
图像的特征可分为三种类型:(1)边缘、(2)角点、(3)区域,其中图像的边缘没有明确的定义,一般是指:两个具有不同灰度的均匀图像区域的边界称为边缘。一般来说,图像边缘信息主要集中在高频段,这代表图像在边缘处会产生梯度突变,我们可以利用这个特征对图像进行高频滤波或图像锐化来获取边缘信息,这也就是边缘检测的过程,所以图像边缘检测实质上就是高频滤波,它与图像平滑(高斯滤波等)是正好相反的过程。
【注】说明一下图像锐化和平滑:图像锐化处理的作用是使灰度反差增强,从而使模糊图像变得更加清晰。图像平滑的实质就是图像受到平均运算或积分运算,从而使得图像更加模糊,因此图像锐化和平滑是一对逆运算,例如:用积分运算可以模糊图像特征,反过来微分运算能够突出图像细节,使图像变得更为清晰。
二、边缘检测的原理和理解
2.1 边缘检测的定义
百度词条的定义为:
边缘检测是图像处理和计算机视觉中的基本问题,边缘检测的目的是标识数字图像中亮度变化明显的点。图像属性中的显著变化通常反映了属性的重要事件和变化。 这些包括(i)深度上的不连续、(ii)表面方向不连续、(iii)物质属性变化和(iv)场景照明变化。 边缘检测是图像处理和计算机视觉中,尤其是特征提取中的一个研究领域。
简单来说,就是一个寻找一些在灰度上显著变化的边缘像素的过程,传统边缘检测的方法通常是计算一阶微分和二阶微分来表示边缘特征。所以,边缘检测算子可分为一阶微分算子和二阶微分算子:
- 一阶微分算子主要有:Robert、Sobel、Prewitt算子等;
- 二阶微分算子主要有:Canny、Laplacian算子等。
微分或导数通常对噪声很敏感,因此必须采用平滑滤波处理去除噪声点,其一般步骤如下:
- 滤波:消除噪声
- 增强:使边界轮廓更加明显
- 检测:选出边缘点
2.2 Sobel算子
Sobel 算子是一阶导数的边缘检测算子,结合了高斯平滑和微分求导,使用卷积核与图像中的每个像素点做卷积和运算,然后采用合适的阈值提取边缘。Soble算子有两个卷积核,分别对应的是x与y两个方向。
假设输入图像的矩阵为
I
\mathbf {I}
I,卷积核大小为
3
×
3
3 \times 3
3×3 ,则水平一阶导
G
′
x
\mathbf{G}'{x}
G′x和垂直一阶导
G
′
y
\mathbf {G}'{y}
G′y可分别表示为:
G
′
y
=
[
+
1
+
2
+
1
0
0
0
−
1
−
2
−
1
]
∗
I
和
G
′
x
=
[
+
1
0
−
1
+
2
0
−
2
+
1
0
−
1
]
∗
I
\mathbf {G}'{y}={ \begin{bmatrix}+1&+2&+1\\~~0&~~0&~~0\\-1&-2&-1\end{bmatrix}}*\mathbf {I} \quad {\mathrm{和}}\quad \mathbf{G}'{x}={\begin{bmatrix}+1&0&-1\\+2&0&-2\\+1&0&-1\end{bmatrix}}*\mathbf {I}
G′y=⎣⎡+1 0−1+2 0−2+1 0−1⎦⎤∗I和G′x=⎣⎡+1+2+1000−1−2−1⎦⎤∗I
其中
∗
\ast
∗表示卷积操作。
联合XY方向梯度,输出图像矩阵
G
\mathbf {G}
G可表示为:
G
=
G
x
2
+
G
y
2
简
化
为
G
=
∣
G
x
∣
+
∣
G
y
∣
\mathbf {G}=\sqrt{\mathbf{G}^2_{x}+\mathbf{G}^2_{y}} \quad {\mathrm{简化为}}\quad \mathbf{G}=\left |\mathbf{G}_{x} \right |+\left |\mathbf{G}_{y} \right |
G=Gx2+Gy2简化为G=∣Gx∣+∣Gy∣
【例1】下面以Sobel算子为例讲述如何计算梯度
x和y方向的Sobel算子分别为:
G
x
=
[
−
1
0
1
−
2
0
2
−
1
0
1
]
和
G
y
=
[
1
2
1
0
0
0
−
1
−
2
−
1
]
\mathbf {G}_{x}={ \begin{bmatrix}-1 & 0 & 1 \\-2 & 0 & 2 \\-1 & 0 & 1\end{bmatrix}} \quad 和 \quad \mathbf {G}_{y}={\begin{bmatrix}~~1 & ~~2 & ~~1 \\~~0 & ~~0 & ~~0 \\-1 & -2 & -1\end{bmatrix}}
Gx=⎣⎡−1−2−1000121⎦⎤和Gy=⎣⎡ 1 0−1 2 0−2 1 0−1⎦⎤
若图像
G
\mathbf {G}
G中一个
3
×
3
3 \times 3
3×3的窗口为A,要计算梯度的像素点为e,则和Sobel算子进行卷积之后,像素点e在x和y方向的梯度值分别为:
G
′
x
=
G
x
∗
A
=
[
−
1
0
1
−
2
0
2
−
1
0
1
]
∗
[
a
b
c
d
e
f
g
h
i
]
=
sum
(
[
−
a
0
c
−
2
d
0
2
f
−
g
0
i
]
)
G
′
y
=
G
y
∗
A
=
[
1
2
1
0
0
0
−
1
−
2
−
1
]
∗
[
a
b
c
d
e
f
g
h
i
]
=
sum
(
[
a
2
b
c
0
0
0
−
g
−
2
h
−
i
]
)
\begin{aligned} {G}'{x}&=G{x} * A=\left[\begin{array}{ccc}-1 & ~~0 & ~~~1 \\-2 & ~~0 & ~~~2 \\-1 & ~~0 & ~~~1\end{array}\right] *\left[\begin{array}{ccc}a & b & c \\d & e & f \\g & h & i\end{array}\right]=\operatorname{sum} \left(\left[\begin{array}{ccc}~~-a & ~~0 & ~~c \\-2 d & ~~0 & 2 f \\~~-g & ~~0 & ~~i\end{array}\right]\right) \\ {G}'{y}&=G{y} * A=\left[\begin{array}{ccc}~~1 &~~ 2 & ~1 \\~~0 &~~ 0 & ~0 \\-1 & -2 & -1\end{array}\right] *\left[\begin{array}{ccc}a & b & c \\d & e & f \\g & h & i\end{array}\right]=\operatorname{sum}\left(\left[\begin{array}{ccc}~~a & ~2 b & ~~c \\~~0 & ~~0 & ~~0 \\-g & -2 h & -i\end{array}\right]\right) \end{aligned}
G′xG′y=Gx∗A=⎣⎡−1−2−1 0 0 0 1 2 1⎦⎤∗⎣⎡adgbehcfi⎦⎤=sum⎝⎛⎣⎡ −a−2d −g 0 0 0 c2f i⎦⎤⎠⎞=Gy∗A=⎣⎡ 1 0−1 2 0−2 1 0−1⎦⎤∗⎣⎡adgbehcfi⎦⎤=sum⎝⎛⎣⎡ a 0−g 2b 0−2h c 0−i⎦⎤⎠⎞
其中 ∗ \ast ∗为卷积符号,sum()表示矩阵中所有元素相加求和。
在OpenCV中使用函数sobel() 来实现sobel边缘检测,其函数模型如下:
void cv::Sobel(InputArray src, // 原始图像
OutputArray dst, // 目标图像
int ddepth, // 输出图像深度,-1 表示等于 src.depth()
int dx, // 水平方向的阶数
int dy, // 垂直方向的阶数
int ksize = 3, // 卷积核的大小,常取 1, 3, 5, 7 等奇数
double scale = 1, // 缩放因子,应用于计算结果
double delta = 0, // 增量数值,应用于计算结果
int borderType = BORDER_DEFAULT // 边界模式
)
2.3 Canny算子
Canny算子是二阶导数的边缘检测算子,基本思想是寻找梯度的局部最大值。
Canny算子进行边缘检测的一般标准包括高斯平滑处理、梯度强度和角度计算、非极大值抑制和双阈值边缘检测4个步骤:
- 用高斯滤波器对输入图像做平滑处理 ,
此步骤将使图像稍微平滑,以减少边缘检测器上明显噪声的影响。大小为 ( 2 k + 1 ) × ( 2 k + 1 ) (2k + 1)×(2k + 1) (2k+1)×(2k+1)的高斯滤波器核的方程式为:
H
i
j
=
1
2
π
σ
2
exp
(
−
(
i
−
(
k
+
1
)
)
2
+
(
j
−
(
k
+
1
)
)
2
2
σ
2
)
;
1
≤
i
,
j
≤
(
2
k
+
1
)
{\displaystyle H_{ij}={\frac {1}{2\pi \sigma ^{2}}}\exp \left(-{\frac {(i-(k+1))^{2}+(j-(k+1))^{2}}{2\sigma ^{2}}}\right);1\leq i,j\leq (2k+1)}
Hij=2πσ21exp(−2σ2(i−(k+1))2+(j−(k+1))2);1≤i,j≤(2k+1)【例2】当k=2时,就表示5×5高斯卷积核,用于创建相邻图像,
σ
=
1.4
\sigma = 1.4
σ=1.4,公式如下:
B
=
1
159
[
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
]
A
\mathbf {B} ={\frac {1}{159}}{\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}}\mathbf {A}
B=1591⎣⎢⎢⎢⎢⎡245424912945121512549129424542⎦⎥⎥⎥⎥⎤A 注意,选择高斯核的大小会影响检测器的性能。 尺寸越大,检测器对噪声的灵敏度越低。 此外,随着高斯滤波器核大小的增加,所以一般选择5×5的高斯卷积核。
- 计算图像的梯度强度和角度方向 ( x 和 y 方向上的卷积核)
进行高斯滤波后,图像中的边缘可以指向各个方向,接下来使用四个算子来检测图像中的水平、垂直和对角边缘。边缘检测的算子(如Roberts,Prewitt,Sobel等)返回水平
G
x
G_x
Gx和垂直
G
y
G_y
Gy方向的一阶导数值,由此便可以确定像素点的梯度
G
G
G和方向
θ
\theta
θ 。
G
=
G
x
2
+
G
y
2
和
θ
=
arctan
(
G
y
/
G
x
)
\mathbf {G} ={\sqrt {{\mathbf {G} _{x}}^{2}+{\mathbf {G} _{y}}^{2}}} \quad {\mathrm{和}}\quad \mathbf {\theta } =\operatorname {arctan} \left(\mathbf {G} _{y}/\mathbf {G} _{x}\right)
G=Gx2+Gy2和θ=arctan(Gy/Gx)
通过上式我们可以得到一个梯度矩阵 G \mathbf {G} G和方向矩阵 θ \theta θ。其中,角度方向近似为四个可能值,即 0, 45, 90, 135
- 非极大值抑制(NMS)
在每一点上,邻域中心与沿着其对应的梯度方向的两个像素相比,若中心像素为最大值,则保留,否则中心置0,这样可以抑制非极大值,保留局部梯度最大的点,以得到细化的边缘。
【注】对图像进行梯度计算后,仅仅基于梯度值提取的边缘仍然很模糊。对边缘有且应当只有一个准确的响应。而非极大值抑制则可以帮助将局部最大值之外的所有梯度值抑制为0。非极大值抑制是一种边缘稀疏技术,非极大值抑制的作用在于“瘦”边。直观上地看,对第二步得到的图像,边缘由粗变细了。
- 用双阈值算法检测和连接边缘
假设两类边缘经过非极大值抑制之后的边缘点中,梯度值超过TH的称为强边缘即高阈值,梯度值小于TH大于TL的称为弱边缘即低阈值,梯度小于TL的不是边缘,对于高低阈值和中间值可以通过双阈值(滞后阈值)来处理:
(1)若某一像素位置的幅值超过高阈值,该像素被保留为边缘像素。
(2)若某一像素位置的幅值小于低阈值,该像素被排除。
(3)若某一像素位置的幅值在两个阈值之间,该像素仅仅在8邻域内有一个高于高阈值的像素时才被保留为边缘像素。
在OpenCV中使用函数Canny() 来实现canny边缘检测,其函数模型如下:
void Canny(inputArray image, //输入图像(灰度图像)
outputArray edges, //输出边缘图像
double threshold1, //第一个滞后阈值(边缘连接)
double threshold2, //第二个滞后阈值(控制强边缘的初始段),一般高低阈值比在2:1-3:1之间
int apertureSize=3, //Sobel算子直径,默认为3
bool L2gradient=false //计算图像梯度幅值的标识,默认false
)
2.4 Laplace算子
拉普拉斯算子是一种二阶微分算子。在图像中的边缘区域,像素值会发生比较大的变化,对这些像素求导,会看到极值出现,在这些极值位置,其二阶导数为0,所以也可以用二阶导数来检测图像边缘。一个二维图像函数的拉普拉斯变换是各向同性的二阶导数,定义为:
如下图所示,图像一阶导数的极值位置,二阶导数为0,所以我们也可以用这个特点来作为检测图像边缘的方法。 但是, 二阶导数的0值不仅仅出现在边缘(它们也可能出现在无意义的位置),但是我们可以过滤掉这些点。
从图像二维矩阵角度出发,图像二阶导数的离散形式为:
▽
2
f
=
[
f
(
x
+
1
,
y
)
+
f
(
x
−
1
,
y
)
+
f
(
x
,
y
+
1
)
+
f
(
x
,
y
−
1
)
]
−
4
f
(
x
,
y
)
\triangledown ^{2}f=[f(x+1,y)+f(x-1,y)+f(x,y+1)+f(x,y-1)]-4f(x,y)
▽2f=[f(x+1,y)+f(x−1,y)+f(x,y+1)+f(x,y−1)]−4f(x,y)
用滤波器模板可表示为:
其中,(a)和(b)分别表示4-领域和8-邻域Laplace模板,取反后可得(c)模板。从模板形式容易看出,模板可以增强或锐化中心像素点,使得中心像素点更亮,从而检测出灰度突变的边缘点。对于一阶微分算子,对于陡峭的边缘和变化缓慢的边缘很难确定边缘线位置,但Laplace算子却可以通过二次微分正峰、负峰之间过零点来判断(如上图曲线所示),对孤立点更加敏感,所以特别适用于突出图像中的孤立点、线为目的的场合。和一阶微分算子相同,Laplace算子也会增强孤立噪声点,所以在进行边缘检测时,也需要先进行平滑滤波处理!
在OpenCV中使用函数Laplacian() 来实现Laplace边缘检测,其函数模型如下:
void Laplacian(InputArray drc, //原始图像
OutputArray dst, //边缘图像
int depth, //目标图像深度
int ksize, //二阶导数滤波器直径,奇数
double scale=1, //比例因子,默认为1
double delta=0, //表示结果存入dst,默认值为0
int borderType=BORDER_DEFAULT //边界模式,默认值为BORDER_DEFAULT
)
2.5 对比总结
在实际边缘检测过程中,没有一种边缘检测算子是“万能”的,根据不同类型图像需要选择不同的算子,同时还要充分结合常用图像处理知识,比如说灰度化、平滑、阈值处理、增强等操作,才能更好的检测出图像的边缘特征,方便后续图像的处理!所以下面总结了一下常用边缘检测算子的优缺,便于实际应用。
- Sobel算子在边缘检测的同时尽量的削除了噪声。比较容易实现,受噪声的影响力比较小。它对于像素位置的影响作了加权,因此效果更好、应用广泛。
- Laplace算子是一种各向同性算子具有旋转不变性,在只关心边缘的位置而不考虑其周围的象素灰度差值时比较合适。Laplace算子对孤立象素的响应要比对边缘或线的响应要更强烈,因此只适用于无噪声图象。存在噪声情况下,使用Laplacian算子检测边缘之前需要先进行低通滤波。
- Canny算子是目前理论上相对最完善的一种边缘检测算法。也存在不足之处: 为了得到较好的边缘检测结果,它通常需要使用较大的滤波尺度,这样容易丢失一些细节。算子的双阈值要人为的选取,这需要根据实际经验)
三、基于OpenCV的C++代码实现
Sobel算子:
#include<opencv2/opencv.hpp>
using namespace cv;
//宏定义
#define WIN_NAME "SlideBar_Sobel"
#define TRACKBAR_NAME "Trackbar"
const int g_MaxSliderValue=100; //设置滚动条最大值为100
int g_SilderValue;
Mat src;
Mat abs_gradx,abs_grady;
void on_trackbar(int,void*){
//第一个变量为滑动条对应的位置,就是回调函数传入的第三个参数。第二个为用户变量的指针。
Mat dst;
//求出alpha相对于最大值的比例
double g_alpha=(double)g_SilderValue/g_MaxSliderValue;
double g_beta=1.0-g_alpha;
//根据alpha和beta的比例混合梯度,得到Sobel整体检测图
addWeighted(abs_gradx,g_alpha,abs_grady,g_beta,0,dst);
imshow(WIN_NAME,dst);
}
int main()
{
//加载预处理
src=imread("C:/Users/Administrator/Desktop/beauty.jpg");
cvtColor(src,src,CV_RGB2GRAY);
Size dsize=Size(round(src.cols*0.3),round(src.rows*0.3));
resize(src,src,dsize,0,0,CV_INTER_LINEAR);
imshow("gray",src);
Mat grad_x,grad_y;
//x方向梯度
Sobel(src,grad_x,-1,1,0,3,1,1,BORDER_DEFAULT);
convertScaleAbs(grad_x,abs_gradx); //微分运算后会有负值,取绝对值
//y方向梯度
Sobel(src,grad_y,-1,0,1,3,1,1,BORDER_DEFAULT);
convertScaleAbs(grad_y,abs_grady);
//合并梯度
// addWeighted(abs_gradx,0.5,abs_grady,0.5,0,dst);
// imshow("XY_sobel",dst);
namedWindow(WIN_NAME,1); //这里一定要先创建一个WIN_NAME,不然滚动条无法显示
g_SilderValue=30;//设置滚动条初始值
//创建滚动条
createTrackbar(TRACKBAR_NAME,WIN_NAME,&g_SilderValue,g_MaxSliderValue,on_trackbar);
on_trackbar(g_SilderValue,0); //在回调函数中显示
cvWaitKey(0);
return 0;
}
显示效果:
代码通过滚动条的方式将X方向梯度和Y方向梯度进行线性融合,可以清晰看到不同比例下混合X、Y方向梯度的Sobel边缘检测图。
Canny算子:
#include<opencv2/opencv.hpp>
#include "Header/addGaussianNoise.h"
using namespace cv;
int threshold1=1,threshold2=1;
//定义:回调函数数据集合类
class Callback_set{
public:
Mat edge;
Mat edge_c;
int max_value=100;
};
void on_Track(int ,void* my_data){ //传入Callback_set自定义类型指针
Callback_set data=*(Callback_set*) my_data; //解指针转换成Callback_set类
Canny(data.edge,data.edge_c,threshold1,threshold2,3);
imshow("CannyWithTrackbar",data.edge_c);
}
int main()
{
Mat img=imread("C:/Users/Administrator/Desktop/beauty.jpg",0); //灰度图读入
Size dsize = Size(round(0.25 * img.cols), round(0.25 * img.rows));//Size型 改变尺寸
resize(img, img, dsize, 0, 0, INTER_LINEAR); //使用双线性插值缩放一下尺寸
addGaussianNoise(img,10,20); //添加高斯噪声:均值为10,方差为20
imshow("src",img);
Mat edge,edge_c;
blur(img,edge,Size(5,5)); imshow("edge_blur",edge);
//Canny(edge,edge_c,1,2,3);imshow("edge_canny",edge_c);
//设置双滚动条最大值初始位置
int maxValue=10;
//通过类将回调函数需要的几个数据打包成类指针传入createTrackBar中,避免设置大量全局变量,消耗内存
Callback_set my_data;
my_data.edge=edge;
my_data.edge_c=edge_c;
my_data.max_value=maxValue;
namedWindow("CannyWithTrackbar",1);
createTrackbar("Trackbar_TH1","CannyWithTrackbar",&threshold1,maxValue,on_Track,&my_data);
createTrackbar("Trackbar_TH2","CannyWithTrackbar",&threshold2,maxValue,on_Track,&my_data);
on_Track(0,&my_data);
cv::waitKey();
return 0;
}
通过设置两个滚动条,调节滞后阈值,实现效果如下:
其中滚动条的参数设置可以参考blog:opencv滑动条Trackbar使用(很全面)
Laplace算子代码实现:
#include<opencv2/opencv.hpp>
#include "Header/addGaussianNoise.h"
using namespace cv;
int main()
{
Mat img=imread("C:/Users/Administrator/Desktop/beauty.jpg",0); //灰度图读入
Size dsize = Size(round(0.25 * img.cols), round(0.25 * img.rows));//Size型 改变尺寸
resize(img, img, dsize, 0, 0, INTER_LINEAR); //使用双线性插值缩放一下尺寸
addGaussianNoise(img,10,20); //均值为10,方差为20
imshow("src",img);
GaussianBlur(img, img, Size(3, 3), 0, 0, BORDER_DEFAULT);
Mat dst,abs_dst;
Laplacian(img, dst, CV_16S, 3, 1, 0, BORDER_DEFAULT);
convertScaleAbs(dst, abs_dst); //二阶导可为负,取绝对值
imshow("Laplace",abs_dst);
cv::waitKey();
return 0;
}
实现效果如下:
从上面的实现效果来看,Soble算子提取的边缘特征“太细致”,而laplace算子提取的边缘较少且模糊,Canny算子得整体效果是相对较好的。
参考博客:
【1】OpenCV 之 边缘检测
【2】OpenCV学习笔记(十三)边缘检测
【3】差分近似图像导数算子之Laplace算子
参考书籍:
【1】《OpenCV3编程入门》_毛星云、冷雪飞等著