Canny算子理解,及Matlab實現

JohnCanny於1986年提出Canny算子,它與Marr(LoG)邊緣檢測方法類似,也屬於是先平滑後求導數的方法。本文對根據上述的邊緣檢測過程對Canny檢測算法的原理進行介紹。 並結合實驗,對canny算法進行理解,指出參數選取中容易產生的問題。

canny邊緣檢測一共四個部分:
1. 用高斯濾波器平滑圖像;(圖像去噪)
2. 用一階偏導有限差分計算梯度幅值和方向;(特徵增強)
3. 對梯度幅值進行非極大值抑制 ;(邊緣檢測)
4. 用雙閾值算法檢測和連接邊緣。(形態學處理)

平滑去噪

canny邊緣檢測的前兩步相對不復雜,所以我就直接調用系統函數了。

IM=imread('2.tif');
IM=imnoise(IM); %imnoise對uint8類型加噪爲0-255,e對double類型加噪爲0-1,
[m,n]=size(IM);
IM=double(IM);
%高斯濾波
w=fspecial('gaussian',[9 9]);
img=imfilter(IM,w,'replicate');

值得注意的是,在選取濾波窗口w時,根據我的實驗(無噪聲情況實驗,若有噪聲,也會影響後續的非極大值抑制),濾波窗口w邊長爲偶數時,將導致得到的梯度只有單像素最大幅值。而對邊緣分析可知,應該產生雙像素的梯度幅值才符合實際情況,所以濾波窗口w的選取一定要爲基數邊長。

實例如下,當選偶數邊長時,幅值結果如下
邊界(200、201)處梯度的幅值,只有一個最大值

當選基數邊長時,幅值結果如下
邊界(200、201)處梯度的幅值,有兩個最大值

計算幅值

%%sobel邊緣檢測
w_h=[1,2,1;0,0,0;-1,-2,-1];              %Sobel算子,梯度方向朝上爲正
img_h=imfilter(img,w_h,'replicate');       %梯度是豎着的邊緣,即橫邊緣
w_w=[-1,0,1;-2,0,2;-1,0,1];              %Sobel算子,梯度方向朝右爲正
img_w=imfilter(img,w_w,'replicate');       %梯度是橫着的邊緣,,即豎邊緣
img_grad=sqrt(img_w.^2+img_h.^2);             %梯度的絕對值
grad_max=max(max(img_grad));
img_grad=img_grad/grad_max; %歸一化

閾值選取

閾值用於後續的邊緣連接處理,高閾值代表強邊緣,低閾值代表弱邊緣,在OpenCV中,閾值需要人工選取。在Matlab中,閾值根據梯度幅值的直方圖選取,默認選擇幅值大小在前70%的最後一個幅值作爲高閾值,低閾值與高閾值的比例爲4:10.在我的程序中,由於需要檢測很強的邊緣,故直方圖比例設置得很高。

值得一提的是,強邊緣的閾值設置後並不需要包括所有邊緣,只需要在弱邊緣上有“點狀”的分佈即可,後續會根據與弱邊緣的8連通域情況,得到效果很好的邊緣。 另外,對得到的強邊緣進行開操作和閉操作可以得到很好的去噪效果,而同樣的操作卻不能對弱邊緣使用。

弱邊緣圖像:
得到的弱邊緣

強邊緣圖像:
得到的強邊緣,只需要點狀分佈

代碼如下:

k=1.2;
PercentOfPixelsNotEdges = 1-k*(m+n)/(m*n);%0.995; %強邊緣的比例
ThresholdRatio = 0.52;  %強弱邊緣的比例
thresh=[ ];
if isempty(thresh)%通過直方圖自動計算高低閾值大小
    counts=imhist(img_grad, 256);
    highThresh = find(cumsum(counts) > PercentOfPixelsNotEdges*m*n,1,'first') / 256; %PercentOfPixelsNotEdges=0.8,即不是邊界的比例
    lowThresh = ThresholdRatio*highThresh;
    thresh = [lowThresh highThresh];
elseif length(thresh)==1
    highThresh = thresh;
    if thresh>=1
        error(message('images:edge:thresholdMustBeLessThanOne'))
    end
    lowThresh = ThresholdRatio*thresh;
    thresh = [lowThresh highThresh];
elseif length(thresh)==2
    lowThresh = thresh(1);
    highThresh = thresh(2);
    if (lowThresh >= highThresh) || (highThresh >= 1)
        error(message('images:edge:thresholdOutOfRange'))
    end
end

非極大值抑制

在John Canny提出的Canny算子的論文中,非最大值抑制就只是在0、90、45、135四個梯度方向上進行的,每個像素點梯度方向按照相近程度用這四個方向來代替。這種情況下,非最大值抑制所比較的相鄰兩個像素就是:

1) 0:左邊 和 右邊
2)45:右上 和 左下
3)90: 上邊 和 下邊
4)135: 左上 和 右下

這樣做的好處是簡單, 但是這種簡化的方法無法達到最好的效果, 因爲,自然圖像中的邊緣梯度方向不一定是沿着這四個方向的。因此,就有很大的必要進行插值,找出在一個像素點上最能吻合其所在梯度方向的兩側的像素值。

非極大值抑制是canny算法的精髓,對此理解不深的同學,可以參考(http://blog.csdn.net/kezunhai/article/details/11620357) 這裏的解釋,解釋得非常到位。

值得一提的是,爲了在噪聲中得到真實的雙像素邊緣,可以在非極大值抑制時不選取最大值爲1,只要大小爲周圍最大值的80%即可。

代碼如下:

%%下面是非極大值抑制
weak_edge2=zeros(m+2,n+2);
img_grad2 = padarray(img_grad,[1 1],'replicate'); %爲了計算邊界上的點,要擴充一下
img_h2 = padarray(img_h,[1 1],'replicate');
img_w2 = padarray(img_w,[1 1],'replicate');
for i=2:m+1         % 圖像邊緣的像素不能計算
    for j=2:n+1

        if img_grad2(i,j)<=lowThresh
            M1=2;
            M2=2;%這時檢測點肯定是不大於1的,故isbigger=0

        else %img_grad(i,j)~=0
            Mx=img_w2(i,j);%梯度是橫着的
            My=img_h2(i,j);%梯度是豎着的
            if abs(My)>=abs(Mx) %y方向梯度比x方向梯度大,梯度方向“豎着”的情況
                g1=img_grad2(i-1,j);%g1是檢測點正上方一行
                g3=img_grad2(i+1,j);%g3是檢測點正下方一行
                weight=(abs(Mx)/abs(My));
                if Mx*My>=0 %x,y方向梯度同號,梯度線分佈在第1、3象限,當Mx*My==0時,Mx=0,weight=0
                    g2=img_grad2(i-1,j+1);%g2是檢測點上右方
                    g4=img_grad2(i+1,j-1);%g4是檢測點下左方
                end
                if Mx*My<0 %x,y方向梯度異號,梯度線分佈在第2、4象限
                    g2=img_grad2(i-1,j-1);%g2是檢測點上左方
                    g4=img_grad2(i+1,j+1);%g4是檢測點下右方
                end
                M1=g1*(1-weight)+g2*weight;%M1是上方的插值
                M2=g3*(1-weight)+g4*weight;%M2是下方的插值 
            end
            if  abs(My)<abs(Mx)%x方向梯度比y方向梯度大,梯度方向“橫着”的情況
                g1=img_grad2(i,j+1);%g1是檢測點正右方
                g3=img_grad2(i,j-1);%g3是檢測點正左方
                weight=(abs(My)/abs(Mx));
                if Mx*My>=0 %x,y方向梯度同號,梯度線分佈在第1、3象限
                    g2=img_grad2(i-1,j+1);%g2是檢測點上右方
                    g4=img_grad2(i+1,j-1);%g4是檢測點下左方
                end
                if Mx*My<0 %x,y方向梯度異號,梯度線分佈在第2、4象限
                    g2=img_grad2(i+1,j+1);%g2是檢測點下右方
                    g4=img_grad2(i-1,j-1);%g4是檢測點上左方
                end
                M1=g1*(1-weight)+g2*weight;%M1是上方的插值
                M2=g3*(1-weight)+g4*weight;%M2是下方的插值 
            end
        end
        %此處選擇與經典canny不同,考慮到噪聲干擾,爲了找到“雙線邊緣”,假定誤差不超過0.9的都同爲最大值
        %也可根據加入噪聲水平確定該值
        isbigger=(img_grad2(i,j)>=(0.8*M1))&&(img_grad2(i,j)>=(0.8*M2)); %如果當前點比兩邊點都大,則置爲白色 
        if isbigger
           weak_edge2(i,j)=255;  
        end        
   end
end
weak_edge=weak_edge2(2:m+1,2:n+1);
figure(1);
imshow(weak_edge)
[rstrong,cstrong] = find(img_grad>highThresh & weak_edge);
strong_edge=zeros(m,n);
for i=1:length(rstrong)
    r=rstrong(i);
    c=cstrong(i);
    strong_edge(r,c)=weak_edge(r,c);
end
se=strel('square',2);
strong_edge=imopen(strong_edge,se);
se=strel('square',3);
strong_edge=imclose(strong_edge,se);
figure(2);
imshow(strong_edge)

形態學處理

最後,以強邊緣爲種子,在弱邊緣中尋找其8連通域,作爲邊緣即可。

原圖:
原圖:

canny算法檢測到的邊界:
最後得到的邊界

代碼如下
[rstrong,cstrong] = find(strong_edge);
edge = bwselect(weak_edge, cstrong, rstrong, 8);

在Matlab “edge()”函數中,最後還做了一個形態學瘦化處理,edge= bwmorph(edge, ‘thin’, 1); 但是我覺得得到的邊緣已經足夠細了,雙像素邊緣在之後對邊緣區域的操作中也會更加有用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章