1.基础原理
参考自《数字图象处理》第十章
及OpenCV Tutorial Canny Edge Detector
1.1边缘检测概述
边缘检测是根据灰度突变来分割图像的一种常用方法。边缘模型可根据它们的灰度剖面来分类,通常分为台阶模型,斜坡模型和屋顶边缘模型。台阶模型多见于计算机生成的图像中,如实体建模和动画领域。实际图像中多为斜坡边缘模型。在使用二阶梯度来获取图像边缘时,二阶导数会产生一个局部极大正值和一个局部极小负值,在图像上体现为双线性效应。
1.2Canny边缘检测
1986年由John F. Canny 在A computational approach to edge detection.中提出,Canny 算法主要分为如下几个步骤:
- 使用高斯核平滑输入图像
- 计算梯度幅度图像和角度图像
- 对梯度幅度图像应用非极大值抑制
- 使用双阈值处理和连同性分析来检测和连接边缘
分别展开讨论,
-
高斯核平滑图像去除噪声 -
计算梯度幅度图和梯度方向图,令f(x,y) 表示输入图像,梯度幅度和方向分别为:
M
(
x
,
y
)
=
∣
∣
▽
f
(
x
,
y
)
∣
∣
=
g
x
2
(
x
,
y
)
+
g
y
2
(
x
,
y
)
M(x,y)=||\triangledown f(x,y)||=\sqrt{g_x^2(x,y)+g_y^2(x,y)}
M(x,y)=∣∣▽f(x,y)∣∣=gx2?(x,y)+gy2?(x,y)
?
α
(
x
,
y
)
=
a
r
c
t
a
n
[
g
y
(
x
,
y
)
g
x
(
x
,
y
)
]
\alpha(x,y)=arctan[\frac{g_y(x,y)}{g_x(x,y)}]
α(x,y)=arctan[gx?(x,y)gy?(x,y)?], 其中,
g
x
(
x
,
y
)
=
?
f
(
x
,
y
)
?
x
g_x(x,y)=\frac{\partial f(x,y)}{\partial x}
gx?(x,y)=?x?f(x,y)?,
g
y
(
x
,
y
)
=
?
f
(
x
,
y
)
?
y
g_y(x,y)=\frac{\partial f(x,y)}{\partial y}
gy?(x,y)=?y?f(x,y)?,可使用(七)图像处理中常用算子Laplacian\Sobel\Roberts\Prewitt\Kirsch中的算子来求
g
x
(
x
,
y
)
g_x(x,y)
gx?(x,y)和
g
y
(
x
,
y
)
g_y(x,y)
gy?(x,y)。 -
对梯度幅度图像应用非极大值抑制,梯度图像
∣
∣
▽
f
(
x
,
y
)
∣
∣
||\triangledown f(x,y)||
∣∣▽f(x,y)∣∣通常在局部极大值附近包含一些宽脊,可以使用非极大值抑制来细化这些宽脊。在一个3x3 区域内,可以定义4个边缘法线(梯度向量)的4个方向,水平,垂直,+45度, -45度,每个方向包含的角度范围见下图,
使用
d
1
,
d
2
,
d
3
,
d
4
d_1,d_2,d_3,d_4
d1?,d2?,d3?,d4?表示前述3x3 区域的4个基本边缘方向,对以
α
\alpha
α中的任意点
(
x
,
y
)
(x,y)
(x,y)为中心的3x3 区域,对应的非极大值抑制方案为: - 1)寻找
α
(
x
,
y
)
\alpha(x,y)
α(x,y)的方向
d
k
d_k
dk? - 2)令K表示
∣
∣
▽
f
∣
∣
||\triangledown f||
∣∣▽f∣∣ 在
(
x
,
y
)
(x,y)
(x,y)处的值。若K小于
d
k
d_k
dk?方向上点(x,y)的一个或两个邻点处的
∣
∣
▽
f
∣
∣
||\triangledown f||
∣∣▽f∣∣值,则令
g
N
(
x
,
y
)
=
0
g_N(x,y)=0
gN?(x,y)=0;否则,令
g
N
(
x
,
y
)
=
K
g_N(x,y)=K
gN?(x,y)=K 对(x,y)的所有值重复这一过程,得到一幅与
f
(
x
,
y
)
f(x,y)
f(x,y)大小相同的非极大值抑制图像
g
N
(
x
,
y
)
g_N(x,y)
gN?(x,y),图像
g
N
(
x
,
y
)
g_N(x,y)
gN?(x,y)中只包含细化后的边缘。
- 使用双阈值处理和连同性分析来检测和连接边缘,得到
g
N
(
x
,
y
)
g_N(x,y)
gN?(x,y)后对其进行阈值处理来求得边缘。
Canny 算法使用滞后阈值处理,设置两个阈值,低阈值
T
L
T_L
TL?和高阈值
T
H
T_H
TH?。可以将滞后阈值运算当作为创建两幅额外的图像:
g
N
H
(
x
,
y
)
=
g
N
(
x
,
y
)
≥
T
H
g_{NH}(x,y)=g_N(x,y)\ge T_H
gNH?(x,y)=gN?(x,y)≥TH?
g
N
L
(
x
,
y
)
=
g
N
(
x
,
y
)
≥
T
L
g_{NL}(x,y)=g_N(x,y)\ge T_L
gNL?(x,y)=gN?(x,y)≥TL? 与
g
N
L
(
x
,
y
)
g_{NL}(x,y)
gNL?(x,y)相比
g
N
H
(
x
,
y
)
g_{NH}(x,y)
gNH?(x,y)非零值更少,
g
N
H
(
x
,
y
)
g_{NH}(x,y)
gNH?(x,y)中的非零像素都在
g
N
L
(
x
,
y
)
g_{NL}(x,y)
gNL?(x,y)中,通过
g
N
L
(
x
,
y
)
=
g
N
L
(
x
,
y
)
?
g
N
H
(
x
,
y
)
g_{NL}(x,y) = g_{NL}(x,y) - g_{NH}(x,y)
gNL?(x,y)=gNL?(x,y)?gNH?(x,y)从
g
N
L
(
x
,
y
)
g_{NL}(x,y)
gNL?(x,y)中删除来自
g
N
H
(
x
,
y
)
g_{NH}(x,y)
gNH?(x,y)的非零像素点。进行上述裁减后
g
N
L
(
x
,
y
)
g_{NL}(x,y)
gNL?(x,y)和
g
N
H
(
x
,
y
)
g_{NH}(x,y)
gNH?(x,y)分别表示“弱”边缘像素和“强”边缘像素。
g
N
H
(
x
,
y
)
g_{NH}(x,y)
gNH?(x,y)中的边缘像素被假设为有效的边缘像素,但其一般存在缝隙,通常需通过如下处理来形成更长的边缘:
- 1)在
g
N
H
(
x
,
y
)
g_{NH}(x,y)
gNH?(x,y)中定位下一个未被访问的边缘像素p
- 2)将
g
N
L
(
x
,
y
)
g_{NL}(x,y)
gNL?(x,y)中与p 8 连通域中的所有弱像素点标记为有效边缘像素
- 3)若
g
N
H
(
x
,
y
)
g_{NH}(x,y)
gNH?(x,y)中所有非零像素都已被访问,则转到4,否则返回1
- 4)将
g
N
L
(
x
,
y
)
g_{NL}(x,y)
gNL?(x,y)中未标记为有效边缘像素的所有像素设置为0
经过上述步骤,将
g
N
L
(
x
,
y
)
g_{NL}(x,y)
gNL?(x,y)中的所有非零像素附加到
g
N
H
(
x
,
y
)
g_{NH}(x,y)
gNH?(x,y)上,形成Canny 算子输出的最终图像。
2.OpenCV API
Canny()
void cv::Canny (
InputArray image,
OutputArray edges,
double threshold1,
double threshold2,
int apertureSize = 3,
bool L2gradient = false
)
image :单通道灰度图edges :存储边缘像素的图像threshold1 : 滞后阈值处理的超参数threshold2 : 滞后阈值处理的超参数apertureSize :使用Sobel 算子时的卷积核大小L2gradient :计算梯度幅值时是否使用
L
2
L_2
L2?范数
3.示例
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgcodecs.hpp>
#include <iostream>
using namespace std;
using namespace cv;
Mat src, src_gray;
Mat dst, detected_edges;
int low_threshold = 0;
const int max_low_threshold = 100;
const int ratio = 3;
const int ks = 3;
const char *win_name = "Canny Edge Map";
static void CannyThreshold(int, void*)
{
blur(src_gray, detected_edges, Size(3,3));
Canny(detected_edges, detected_edges, low_threshold, low_threshold*ratio, ks);
dst = Scalar::all(0);
src.copyTo(dst, detected_edges);
imshow(win_name, dst);
imwrite("seg_res.png", dst);
}
int main(int argc, char **argv)
{
CommandLineParser parser( argc, argv, "{@input | fruits.jpg | input image}" );
src = imread( samples::findFile( parser.get<String>( "@input" ) ), IMREAD_COLOR );
if( src.empty() )
{
std::cout << "Could not open or find the image!\n" << std::endl;
std::cout << "Usage: " << argv[0] << " <Input image>" << std::endl;
return -1;
}
dst.create( src.size(), src.type() );
cvtColor( src, src_gray, COLOR_BGR2GRAY );
namedWindow( win_name, WINDOW_AUTOSIZE);
createTrackbar( "Min Threshold:", win_name, &low_threshold, max_low_threshold, CannyThreshold );
CannyThreshold(0, 0);
waitKey(0);
return 0;
}
|