【机器视觉】Canny边缘检测MATLAB源码阅读

机器视觉 第二次作业

作业要求

  1. 阅读edge.m文件,理解canny边缘检测算法中核心步骤的实现过程
  2. 熟悉matlab的矩阵操作

1、canny边缘检测

(1)算法步骤

  1. 将图像与二维高斯函数做卷积以消除噪声;
  2. 对图像中的每个像素,计算其梯度的幅值方向
  3. 非极大值抑制:根据梯度方向,遍历该像素沿梯度方向的邻接像素,找出幅值最大的像素,作为边缘;
  4. 滞后阈值化处理:设定高低阈值,对于图像中的每一个像素梯度:大于高阈值的一定是边缘;小于低阈值的一定不是边缘;梯度在高低阈值之间的,看其周边8个像素中是否有超过高阈值的边缘像素,有则为边缘,否则不是边缘。

(2)核心代码

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];  % 强边缘弱边缘一起打包返回

% ...

2、几个内置函数

(1)smoothGradient 高斯滤波+计算梯度

我们知道,边缘(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,j1) 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(i1,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π σ1e2σ2x2

gradient函数就是对它求导。

imfilter函数是系统函数,就是对图像进行滤波。为什么代码里面用单变量高斯核,因为它每个方向滤波了两次,相当于是二维滤波操作了。

二维高斯函数的卷积可以分两步进行:首先图像与水平方向一维高斯函数进行卷积,然后将卷积结果与垂直方向相同的一维高斯函数卷积。

g = imfilter(f, w, filtering_mode, boundary_options, size_options)

% f为输入图像,w为滤波器,g为滤波后图像,
% filtering_mode用于指定在滤波过程中是使用“相关”还是“卷积(这里用的是'conv')”,
% boundary_options用于边界填充(这里用的是‘replicate’,即通过复制外边界的值来扩展图像)。

(2)selectThresholds 设置高低阈值

这个函数用来设置阈值。如果用户自己定了一高一低的两个阈值,就不管了;否则程序就帮你设定两个阈值,主要是通过直方图来定(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

(3)thinAndThreshold 滞后阈值化

这一步处理的目的是设定高低阈值,用于判断是否是边缘,遵循如下准则:
凡是大于高阈值的一定是边缘;凡是小于低阈值的一定不是边缘;
检测结果在高低阈值之间的,看其周边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,不作为边缘(被抑制)。

个人理解:

  • 梯度方向的邻接像素
    我们知道像素相邻有4邻接和8邻接,在边缘检测里面采用8邻接;那么梯度方向的邻接像素指的是哪些像素呢?首先要弄明白梯度方向。
  • 把梯度的方向分为4类
    为什么要这么做?因为我们计算得到的梯度方向是一个确定的值,它本质上是一个反正切arctan,可能是小数、整数,反正总落在 − π 2 \frac{-\pi}{2} 2π π 2 \frac{\pi}{2} 2π里面。梯度方向是连续的,这里依据范围,将它们分为4类:南-北方向,东-西方向,东北-西南方向,西北-东南方向。
    可以认为,当梯度落在某个范围内时,无论它具体是什么值,该方向上的邻接像素总是一样的(因为只有那么几个像素),[0, 45]度范围内邻接像素就是左右两个,[45, 90]范围内邻接像素就是上下两个。当算得一个梯度方向时,看它属于哪个区间,就很容易得到这个方向的两个邻接像素了。
  • 接着,遍历这些像素,与中心像素比较,看中心像素是否可以作为边缘。

源代码如下

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;   % 1314像素的梯度幅值插值
   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

你可能感兴趣的:(计算机视觉,matlab,人工智能)