function [eout,thresh,gv_45,gh_135] = edge(varargin)
% ...
if strcmp(method,'canny')
% Magic numbers(默认值)
PercentOfPixelsNotEdges = .7; % Used for selecting thresholds
ThresholdRatio = .4; % Low thresh is this fraction of the high.
% 计算高斯滤波器,获得图像梯度
[dx, dy] = smoothGradient(a, sigma); % sigma是高斯函数参数,代表标准差
% 计算梯度的幅值(sqrt(dx^2+dy^2)平方和再开方)
% 梯度的幅值就是图像的一阶微分!!!
magGrad = hypot(dx, dy);
% 标准化幅值,方便后续确定阈值
magmax = max(magGrad(:));
if magmax > 0
magGrad = magGrad / magmax;
end
% 确定高低阈值(滞后阈值)
[lowThresh, highThresh] = selectThresholds(thresh, magGrad, PercentOfPixelsNotEdges, ThresholdRatio, mfilename);
% 进行非极大值抑制以及边缘的滞后阈值化
e = thinAndThreshold(dx, dy, magGrad, lowThresh, highThresh);
thresh = [lowThresh highThresh]; % 强边缘弱边缘一起打包返回
% ...
我们知道,边缘(edge)是图像灰度变化剧烈的点构成的集合,通常用梯度(gradient)描述图像函数的变化,(即函数值变化最快的地方,就是一阶微分的极大值处,或二阶微分的零点处)。梯度是个相对的概念,两个像素之间才有“梯度”可言。边缘检测的本质是寻找梯度最大的点。
注意,边缘和梯度不可以相提并论。边缘和梯度一样,也是矢量,大小和梯度相同,方向与梯度垂直。所以可以用梯度表示边缘。
f ( x , y ) f(x, y) f(x,y)在高数里面通常表示二维函数,在可导的情况下其梯度定义为 g r a d = [ f x ′ , f y ′ ] T grad=[f'_x, f'_y]^{T} grad=[fx′,fy′]T,括号里面就是俩偏导;
f ( x , y ) f(x, y) f(x,y)在图像领域里面表示二维矩阵,每一点代表灰度值大小,其梯度仍然可以定义为 g r a d = [ f x ′ , f y ′ ] T grad=[f'_x, f'_y]^{T} grad=[fx′,fy′]T,由于图像是离散的, f x ′ f'_x fx′和 f y ′ f'_y fy′定义为 f x ′ ( i , j ) = f ( i , j ) − f ( i , j − 1 ) f'_x(i, j)=f(i, j)-f(i, j-1) fx′(i,j)=f(i,j)−f(i,j−1) f y ′ ( i , j ) = f ( i , j ) − f ( i − 1 , j ) f'_y(i,j)=f(i, j)-f(i-1,j) fy′(i,j)=f(i,j)−f(i−1,j)
当然,梯度定义的方式不唯一。梯度本质就是向量,有大小,有方向。
图像的梯度怎么算?由上式,梯度本质上就是对图像矩阵进行差分运算。你可以一个一个像素点遍历四周那样算,也可以用卷积,卷积又是什么?卷积某种意义上是原函数 f ( x , y ) f(x,y) f(x,y)和另一个函数 h ( x , y ) h(x,y) h(x,y)(卷积核,也叫滤波器(filter))进行积分运算,记作 f ( x , y ) ∗ h ( x , y ) f(x,y)*h(x,y) f(x,y)∗h(x,y)。因为图像是离散的, f ( x , y ) f(x,y) f(x,y)和 h ( x , y ) h(x,y) h(x,y)其实是矩阵,这里也不是积分运算,而是两个矩阵的某种运算:对应位置相乘,再求和。
设定合适的卷积核的值,就可以计算梯度了。卷积核有很多种,sobel算子,拉普拉斯算子……都可以,只不过它们对梯度的定义不一样。在这里,卷积的作用是将计算矩阵化,一次卷积可以一下子计算好几个梯度,省事儿。
学会了。且慢,先别急着计算梯度。因为实际图像往往存在噪点,噪点和原图像素的差异很大,容易被误认为是边缘。所以在进行边缘检测之前,往往会对图像进行平滑处理,以去除噪声。如何去噪?依旧是卷积。
目前还没学过信号与系统……简单理解一下“滤波”和“卷积”:
滤波滤波,顾名思义,过滤掉特定频率的信号。通常过滤的是噪声或者噪点,所以滤波也就是去噪;
具体为什么卷积运算就可以去噪(滤波),这里涉及傅里叶变换相关知识,略。
所以,为了获取图像边缘,需要寻找梯度最大的位置。对图像进行卷积和差分运算。微分依旧可以用卷积来实现。前者是为了去噪,后者则是直接计算函数值的变化。
补充一点,根据卷积定理,这两步可以合并成一步,原函数先卷积,再沿梯度方向微分,相当于直接与卷积核的微分做卷积,如公式中的 h ′ ( x ) h'(x) h′(x)。
用一维函数示意一下: d ( f ( x ) ∗ h ( x ) ) d x = f ( x ) ∗ h ′ ( x ) \frac{d(f(x)*h(x))}{dx}=f(x)*h'(x) dxd(f(x)∗h(x))=f(x)∗h′(x)
所以这个函数的功能就清楚了。
第一步,构造一个滤波器,与原图像做卷积运算,去噪;
第二步,再做卷积运算,计算图像的梯度(包括幅值和方向)。
function [GX, GY] = smoothGradient(I, sigma)
% 创建1阶微分滤波器
% 一阶滤波器(离散)
filterExtent = ceil(4*sigma); % 取整
x = -filterExtent:filterExtent;
% 创建高斯核
c = 1/(sqrt(2*pi)*sigma);
gaussKernel = c * exp(-(x.^2)/(2*sigma^2));
% 标准化,使核所有元素之和为1;
gaussKernel = gaussKernel/sum(gaussKernel);
% 对高斯核求导
derivGaussKernel = gradient(gaussKernel);
% 标准化求导后的核,使得各个元素之和为0
negVals = derivGaussKernel < 0;
posVals = derivGaussKernel > 0;
derivGaussKernel(posVals) = derivGaussKernel(posVals)/sum(derivGaussKernel(posVals));
derivGaussKernel(negVals) = derivGaussKernel(negVals)/abs(sum(derivGaussKernel(negVals)));
% 卷积运算(滤波)+返回图像梯度
% imfilter函数一举两得啊!!
% 计算水平方向梯度,GX即为dG/dx
GX = imfilter(I, gaussKernel', 'conv', 'replicate');
GX = imfilter(GX, derivGaussKernel, 'conv', 'replicate');
% 计算垂直方向梯度,GY即为dG/dy
GY = imfilter(I, gaussKernel, 'conv', 'replicate');
GY = imfilter(GY, derivGaussKernel', 'conv', 'replicate');
代码中的高斯核长这样:
h ( x ) = 1 2 π σ e − x 2 2 σ 2 h(x)=\frac{1}{\sqrt{2\pi}\sigma }e^{-\frac{x^2}{2\sigma^2}} h(x)=2πσ1e−2σ2x2
gradient函数就是对它求导。
imfilter函数是系统函数,就是对图像进行滤波。为什么代码里面用单变量高斯核,因为它每个方向滤波了两次,相当于是二维滤波操作了。
二维高斯函数的卷积可以分两步进行:首先图像与水平方向一维高斯函数进行卷积,然后将卷积结果与垂直方向相同的一维高斯函数卷积。
g = imfilter(f, w, filtering_mode, boundary_options, size_options)
% f为输入图像,w为滤波器,g为滤波后图像,
% filtering_mode用于指定在滤波过程中是使用“相关”还是“卷积(这里用的是'conv')”,
% boundary_options用于边界填充(这里用的是‘replicate’,即通过复制外边界的值来扩展图像)。
这个函数用来设置阈值。如果用户自己定了一高一低的两个阈值,就不管了;否则程序就帮你设定两个阈值,主要是通过直方图来定(p率阈值化)。
首先,将图像按灰度值转化为64阶的直方图;
然后,计算高阈值。高阈值就是图像中边缘像素灰度值的下限,这里默认有70%的像素不是边缘,现在要求出这70%的像素的灰度值水平(就是那个magic number里的0.7);
最后,计算低阈值。计算公式是lowThresh = ThresholdRatio*highThresh。
function [lowThresh, highThresh] = selectThresholds(thresh, magGrad, PercentOfPixelsNotEdges, ThresholdRatio, ~)
[m,n] = size(magGrad);
if isempty(thresh)
counts=imhist(magGrad, 64);
highThresh = find(cumsum(counts) > PercentOfPixelsNotEdges*m*n,1,'first') / 64;
lowThresh = ThresholdRatio*highThresh;
% ...
end
这一步处理的目的是设定高低阈值,用于判断是否是边缘,遵循如下准则:
凡是大于高阈值的一定是边缘;凡是小于低阈值的一定不是边缘;
检测结果在高低阈值之间的,看其周边8个像素中是否有超过高阈值的边缘像素,有则为边缘,否则不是边缘。
function H = thinAndThreshold(dx, dy, magGrad, lowThresh, highThresh)
% 获得强边缘索引
% step1 在梯度方向进行非极大值抑制
E = cannyFindLocalMaxima(dx,dy,magGrad,lowThresh);
% step2 滞后阈值化处理
if ~isempty(E)
[rstrong,cstrong] = find(magGrad>highThresh & E); % 大于高阈值
if ~isempty(rstrong) % 如果强边缘不存在,则强边缘索引全置0
H = bwselect(E, cstrong, rstrong, 8);
else
H = false(size(E));
end
else
H = false(size(E));
end
首先,获得强边缘的索引:
cannyFindLocalMaxima函数是系统函数,源码参考了这个链接和这个链接。
此函数进行非极大值抑制(non-maximum suppression),顾名思义,抑制不是极大值的元素、凸显极大值元素。操作如下:比较当前像素以及它梯度方向上的两个邻接像素,如果当前像素的梯度幅值是它们三个中的最大值,则获取该邻接像素索引(编号),保留它的灰度,当前像素算作边缘;如果某个邻居梯度幅值比当前像素大,那么把当前像素的灰度值设为0,不作为边缘(被抑制)。
个人理解:
源代码如下
function idxLocalMax = cannyFindLocalMaxima(direction,ix,iy,mag)
% 函数声明和MATLAB里的有点不一样,可能因为版本不同
% ix-水平方向的图像梯度
% iy-垂直方向的图像梯度
% mag-梯度幅值(矩阵)
% 梯度方向有4种
% X代表某个像素,共有8个方向,每个方向相隔45度
% 我们只用关注其中4个方向,其余4个是对称的
3 2
O----0----0
4 | | 1
| |
O X O
| |
(1)| |(4)
O----O----O
(2) (3)
[m,n] = size(mag);
% 找到各个区间的梯度,按照上面的划分,
% 第一个区间的梯度应满足(iy<=0 & ix<iy) | (iy>0 & ix>iy),其他区间以此类推。
% ix和iy都是m*n的矩阵,find会把它们都当作一个向量来处理(MATLAB矩阵按列存储)
% find将找出满足条件的梯度对应的下标,返回一个列向量,重复的下标只保留一个。
switch direction
case 1
idx = find((iy<=0 & ix>-iy) | (iy>=0 & ix<-iy));
case 2
idx = find((ix>0 & -iy>=ix) | (ix<0 & -iy<=ix));
case 3
idx = find((ix<=0 & ix>iy) | (ix>=0 & ix<iy));
case 4
idx = find((iy<0 & ix<=iy) | (iy>0 & ix>=iy));
end
% 去除矩阵最外面的一圈像素
% v==1和v==0去除第一行和最后一行
% idx<=m去除第一列,idx>(n-1)*m去除最后一列
if ~isempty(idx)
v = mod(idx,m);
extIdx = (v==1 | v==0 | idx<=m | (idx>(n-1)*m));
idx(extIdx) = [];
end
ixv = ix(idx);
iyv = iy(idx);
gradmag = mag(idx);
% 线性插值。对于下面这个矩阵:
% 1 5 9 13
% 2 6 10 14
% 3 7 11 15
% 4 8 12 16
% 比较idx与其梯度方向上相邻两个像素梯度的大小,由于idx梯度方向上不一定存在像素,
% 所以要进行线性插值(矩阵按列存储)
% 对于case1,假设idx=10,则mag(idx+m)得到的是idx右临像素14的梯度值,
% mag(idx+m-1)得到的是像素13的梯度值。
% d确定了梯度的方向,根据这个方向可以求出插值点与两个参考点的距离,
% 距离越小,这个参考点的权重越大
switch direction
case 1
d = abs(iyv./ixv);
gradmag1 = mag(idx+m).*(1-d) + mag(idx+m-1).*d; % 13、14像素的梯度幅值插值
gradmag2 = mag(idx-m).*(1-d) + mag(idx-m+1).*d;
case 2
d = abs(ixv./iyv);
gradmag1 = mag(idx-1).*(1-d) + mag(idx+m-1).*d;
gradmag2 = mag(idx+1).*(1-d) + mag(idx-m+1).*d;
case 3
d = abs(ixv./iyv);
gradmag1 = mag(idx-1).*(1-d) + mag(idx-m-1).*d;
gradmag2 = mag(idx+1).*(1-d) + mag(idx+m+1).*d;
case 4
d = abs(iyv./ixv);
gradmag1 = mag(idx-m).*(1-d) + mag(idx-m-1).*d;
gradmag2 = mag(idx+m).*(1-d) + mag(idx+m+1).*d;
end
idxLocalMax = idx(gradmag>=gradmag1 & gradmag>=gradmag2);
获得边缘之后,回到thinAndThreshold函数。
然后,去除与强边缘不连通的弱边缘:
这一步才是真正的滞后阈值化(hysteresis thresholding),用到bwselect()函数,这个也是系统函数,可以从某些特定的像素开始,在二值图中查找4连通或者8联通区域。
到此为止,canny边缘检测算法结束。
个人作业,转载需注明网址。谢谢。
https://blog.csdn.net/Wolf_AgOH/article/details/115261399