Python實現邊緣提取的示例代碼

復習

(1)梯度: 梯度的本意是一個向量(矢量),表示某一函數在該點處的方向導數沿著該方向取得最大值,即函數在該點處沿著該方向(此梯度的方向)變化最快,變化率最大(為該梯度的模)

(2)線性濾波 可以說是 圖像處理 最基本的方法,它可以允許我們對圖像進行處理,產生很多不同的效果

一、邊緣提取

1、什麼是邊緣

圖象的邊緣是指圖象局部區域亮度變化顯著的部分,該區域的灰度剖面一般可以看作是一個階躍,一個灰度值在很小的緩沖區域內急劇變化到另一個灰度相差較大的灰度值。

邊緣有正負之分,就像導數有正值也有負值一樣:由暗到亮為正,由亮到暗為負.

求邊緣幅度的算法: sobel、 Roberts、 prewitt、 Laplacian、 Canny算子,Canny算子效果比其他的都要好,但是實現起來有點麻煩

2、什麼是邊緣提取

邊緣提取,指數字圖像處理中,對於圖片輪廓的一個處理。對於邊界處,灰度值變化比較劇烈的地方,就定義為邊緣。也就是拐點,拐點是指函數發生凹凸性變化的點。二階導數為零的地方。並不是一階導數,因為一階導數為零,表示是極值點。

邊緣提取:邊緣檢測的基本思想首先是利用邊緣增強算子,突出圖像中的局部邊緣,然後定義象素的“邊緣強度”,通過設置閾值的方法提取邊緣點集。由於噪聲和模糊的存在,監測到的邊界可能會變寬或在某點處發生間斷。因此,邊界檢測包括兩個基本內容:

(1)用邊緣算子提取出反映灰度變化的邊緣點集。

(2)在邊緣點集合中剔除某些邊界點或填補邊界間斷點,並將這些邊緣連接成完整的線。

邊緣定義:圖像灰度變化率最大的地方(圖像灰度值變化最劇烈的地方)。圖像灰度在表面法向變化的不連續造成的邊緣。一般認為邊緣提取是要保留圖像的灰度變化劇烈的區域,這從數學上看,最直觀的方法就是微分(對於數字圖像來說就是差分),在信號處理的角度來看,也可以說是用高通濾波器,即保留高頻信號。

邊緣信息包含兩個方面:

1.像素的坐標

2.邊緣的方向

(1)邊緣檢測

邊緣檢測 主要 是圖象的灰度變化的度量、檢測和定位。下邊兩幅圖展現出瞭邊緣檢測的效果。

邊緣檢測的應用:語義分割和實例分割

語義分割:

實例分割:

(2)高頻信號&低頻信號

圖像中的低頻信號和高頻信號也叫做低頻分量和高頻分量。

簡單一點說,圖像中的高頻分量,指的是圖像強度(亮度/灰度)變化劇烈的地方,也就是邊緣(輪廓);

圖像中的低頻分量,指的是圖像強度(亮度/灰度)變換平緩的地方,也就是大片色塊的地方。

人眼對圖像中的高頻信號更為敏感。

(3)邊緣檢測的原理和步驟

1)濾波:邊緣檢測的算法主要是基於圖像強度的一階和二階導數,但導數通常對噪聲很敏感,因此必須采用濾波器來改善與噪聲有關的邊緣檢測器的性能。常見的濾波方法主要有高斯濾波。

2)增強:增強邊緣的基礎是確定圖像各點鄰域強度的變化值。增強算法可以將圖像灰度點鄰域強度值有顯著變化的點凸顯出來。在具體編程實現時,可通過計算梯度幅值來確定。

3)檢測:經過增強的圖像,往往鄰域中有很多點的梯度值比較大,而在特定的應用中,這些點並不是我們要找的邊緣點,所以應該采用某種方法來對這些點進行取舍。實際工程中,常用的方法是通過閾值化方法來檢測。

檢測原理:關於邊緣檢測的基礎來自於一個事實, 即在邊緣部分,像素值出現”跳躍“或者較大的變化。 如果在此邊緣部分求取一階導數,就會看到極值的出現。而在一階導數為極值的地方,二階導數為0,基於這個原理,就可以進行邊緣檢測。

在邊緣部分求取一階導數,你會看到極值的出現:

如果在邊緣部分求二階導數會出現什麼情況?

這裡插入圖片描述

從上例中我們可以推論檢測邊緣可以通過定位梯度值大於鄰域的相素的方法找到(或者推廣到大於一個閥值).從以上分析中,我們推論二階導數可以用來檢測邊緣 。因為圖像是 “2維”, 我們需要在兩個方向求導。

(4)圖像銳化

圖像銳化(image sharpening)是補償圖像的輪廓,增強圖像的邊緣及灰度跳變的部分,使圖像變得清晰。

圖像銳化是為瞭突出圖像上的物的邊緣、輪廓,或某些線性目標要素的特征。這種濾波方法提高瞭地物邊緣與周圍像元之間的反差,因此也被稱為邊緣增強。

圖像銳化常使用的是拉普拉斯變換核函數:

圖中右邊的模板它是根據上下左右四個90度方向的像素值來計算二階導的。如果想要加入斜對角上的像素來計算,可使用左邊的模板。

(5)圖像平滑

圖像平滑是指用於突出圖像的寬大區域、低頻成分、主幹部分或抑制圖像噪聲和幹擾高頻成分的圖像處理方法,目的是使圖像亮度平緩漸變,減小突變梯度,改善圖像質量。

用Gx來卷積下面這張圖的話,就會在中間黑白邊界獲得比較大的值。

二、Sobel算子

Sobel算子是典型的基於一階導數的邊緣檢測算子,由於該算子中引入瞭類似局部平均的運算,因此對噪聲具有平滑作用,能很好的消除噪聲的影響。Sobel算子對於像素的位置的影響做瞭加權,因此與Prewitt算子相比效果更好。

Sobel算子包含兩組3×3的矩陣,分別為橫向及縱向模板,將之與圖像作平面卷積,即可分別得出橫向及縱向的亮度差分近似值。實際使用中,常用如下兩個模板來檢測圖像邊緣

缺點是Sobel算子並沒有將圖像的主題與背景嚴格地區分開來,換言之就是Sobel算子並沒有基於圖像灰度進行處理,由於Sobel算子並沒有嚴格地模擬人的視覺生理特征,所以提取的圖像輪廓有時並不能令人滿意。

噪聲和邊緣都是灰度變化劇烈的地方,所以本質相同,濾波不能過度,否則邊緣就減損瞭

Prewitt算子是一種一階微分算子的邊緣檢測,利用像素點上下、左右鄰點的灰度差,在邊緣處達到極值檢測邊緣,去掉部分偽邊緣,對噪聲具有平滑作用 。其原理是在圖像空間利用兩個方向模板與圖像進行鄰域卷積來完成的,這兩個方向模板一個檢測水平邊緣,一個檢測垂直邊緣。其原理與sobel算子一樣

三、Canny邊緣檢測算法

Canny是目前最優秀的邊緣檢測算法( 在傳統的人工智能中不包括深度學習 ),其目標為找到一個最優的邊緣,其最優邊緣的定義為:

1、好的檢測:算法能夠盡可能的標出圖像中的實際邊緣

2、好的定位:標識出的邊緣要與實際圖像中的邊緣盡可能接近

3、最小響應:圖像中的邊緣隻能標記一次

1、算法步驟

1. 對圖像進行灰度化

2. 對圖像進行高斯濾波:

 根據待濾波的像素點及其鄰域點的灰度值按照一定的參數規則進行加權平均。這樣

可以有效濾去理想圖像中疊加的高頻噪聲。

3. 檢測圖像中的水平、垂直和對角邊緣(如Prewitt, Sobel算子等)。

4. 對梯度幅值進行非極大值抑制

5. 用雙閾值算法檢測和連接邊緣

2、高斯平滑

高斯平滑水平和垂直方向呈現高斯分佈,更突出瞭中心點在像素平滑後的權重,相比於均值濾波而言,有著更好的平滑效果。

重要的是需要理解, 高斯卷積核大小的選擇將影響Canny檢測器的性能:尺寸越大,檢測器對噪聲的敏感度越低,但是邊緣檢測的定位誤差也將略有增加。 一般5×5是一個比較不錯的trade off

3、非極大值抑制

非極大值抑制,簡稱為NMS算法,英文為Non-Maximum Suppression。其思想是搜素局部最大值,抑制非極大值。NMS算法在不同應用中的具體實現不太一樣,但思想是一樣的。

為什麼要采用非極大值抑制?以目標檢測為例:目標檢測的過程中在同一目標的位置上會產生大量的候選框,這些候選框相互之間可能會有重疊,此時我們需要利用非極大值抑制找到最佳的目標邊界框,消除冗餘的邊界框。

對於重疊的候選框,計算他們的重疊部分,若大於規定閾值,則刪除;低於閾值則保留。對於無重疊的候選框,都保留。

非極大值抑制:通俗意義上是指尋找像素點局部最大值,將非極大值點所對應的灰度值置為0,這樣可以剔除掉一大部分非邊緣的點。

1) 將當前像素的梯度強度與沿正負梯度方向上的兩個像素進行比較。

2) 如果當前像素的梯度強度與另外兩個像素相比最大,則該像素點保留為邊緣點,否則該像素點將被抑制(灰度值置為0)。

dTmp1和dTmp2是兩個虛擬像素,也有稱亞像素點,其由像素點通過雙線性插值算法求得的。

4、雙閾值檢測

用雙閾值算法檢測(滯後閾值):完成非極大值抑制後,會得到一個二值圖像,非邊緣的點灰度值均為0,可能為邊緣的局部灰度極大值點可設置其灰度為128(或其他)。這樣一個檢測結果還是包含瞭很多由噪聲及其他原因造成的假邊緣。因此還需要進一步的處理。

• 如果邊緣像素的梯度值高於高閾值,則將其標記為強邊緣像素

• 如果邊緣像素的梯度值小於高閾值並且大於低閾值,則將其標記為弱邊緣像素

• 如果邊緣像素的梯度值小於低閾值,則會被抑制

大於高閾值為強邊緣,小於低閾值不是邊緣。介於中間是弱邊緣。閾值的選擇取決於給定輸入圖像的內容,也無法指定確定的值,隻能去調試得到效果比較好的閾值。

抑制孤立低閾值點:到目前為止,被劃分為強邊緣的像素點已經被確定為邊緣,因為它們是從圖像中的真實邊緣中提取出來的。然而,對於弱邊緣像素,將會有一些爭論,因為這些像素可以從真實邊緣提取也可以是因噪聲或顏色變化引起的。

為瞭獲得準確的結果,應該抑制由後者引起的弱邊緣:

• 通常,由真實邊緣引起的弱邊緣像素將連接到強邊緣像素,而噪聲響應未連接。

• 為瞭跟蹤邊緣連接,通過查看弱邊緣像素及其8個鄰域像素,隻要其中一個為強邊緣像素,則該弱邊緣點就可以保留為真實的邊緣。

四、相關代碼

1、canny_detail.py

import numpy as np
import matplotlib.pyplot as plt
import math
 
if __name__ == '__main__':
    pic_path = 'lenna.png' 
    img = plt.imread(pic_path)
    if pic_path[-4:] == '.png':  # .png圖片在這裡的存儲格式是0到1的浮點數,所以要擴展到255再計算
        img = img * 255  # 還是浮點數類型
    img = img.mean(axis=-1)  # 取均值就是灰度化瞭
 
    # 1、高斯平滑
    #sigma = 1.52  # 高斯平滑時的高斯核參數,標準差,可調
    sigma = 0.5  # 高斯平滑時的高斯核參數,標準差,可調
    dim = int(np.round(6 * sigma + 1))  # round是四舍五入函數,根據標準差求高斯核是幾乘幾的,也就是維度
    if dim % 2 == 0:  # 最好是奇數,不是的話加一
        dim += 1
    Gaussian_filter = np.zeros([dim, dim])  # 存儲高斯核,這是數組不是列表瞭
    tmp = [i-dim//2 for i in range(dim)]  # 生成一個序列
    n1 = 1/(2*math.pi*sigma**2)  # 計算高斯核
    n2 = -1/(2*sigma**2)
    for i in range(dim):
        for j in range(dim):
            Gaussian_filter[i, j] = n1*math.exp(n2*(tmp[i]**2+tmp[j]**2))
    Gaussian_filter = Gaussian_filter / Gaussian_filter.sum()
    dx, dy = img.shape
    img_new = np.zeros(img.shape)  # 存儲平滑之後的圖像,zeros函數得到的是浮點型數據
    tmp = dim//2
    img_pad = np.pad(img, ((tmp, tmp), (tmp, tmp)), 'constant')  # 邊緣填補
    for i in range(dx):
        for j in range(dy):
            img_new[i, j] = np.sum(img_pad[i:i+dim, j:j+dim]*Gaussian_filter)
    plt.figure(1)
    plt.imshow(img_new.astype(np.uint8), cmap='gray')  # 此時的img_new是255的浮點型數據,強制類型轉換才可以,gray灰階
    plt.axis('off')
 
    # 2、求梯度。以下兩個是濾波求梯度用的sobel矩陣(檢測圖像中的水平、垂直和對角邊緣)
    sobel_kernel_x = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]])
    sobel_kernel_y = np.array([[1, 2, 1], [0, 0, 0], [-1, -2, -1]])
    img_tidu_x = np.zeros(img_new.shape)  # 存儲梯度圖像
    img_tidu_y = np.zeros([dx, dy])
    img_tidu = np.zeros(img_new.shape)
    img_pad = np.pad(img_new, ((1, 1), (1, 1)), 'constant')  # 邊緣填補,根據上面矩陣結構所以寫1
    for i in range(dx):
        for j in range(dy):
            img_tidu_x[i, j] = np.sum(img_pad[i:i+3, j:j+3]*sobel_kernel_x)  # x方向
            img_tidu_y[i, j] = np.sum(img_pad[i:i+3, j:j+3]*sobel_kernel_y)  # y方向
            img_tidu[i, j] = np.sqrt(img_tidu_x[i, j]**2 + img_tidu_y[i, j]**2)
    img_tidu_x[img_tidu_x == 0] = 0.00000001
    angle = img_tidu_y/img_tidu_x
    plt.figure(2)
    plt.imshow(img_tidu.astype(np.uint8), cmap='gray')
    plt.axis('off')
 
    # 3、非極大值抑制
    img_yizhi = np.zeros(img_tidu.shape)
    for i in range(1, dx-1):
        for j in range(1, dy-1):
            flag = True  # 在8鄰域內是否要抹去做個標記
            temp = img_tidu[i-1:i+2, j-1:j+2]  # 梯度幅值的8鄰域矩陣
            if angle[i, j] <= -1:  # 使用線性插值法判斷抑制與否
                num_1 = (temp[0, 1] - temp[0, 0]) / angle[i, j] + temp[0, 1]
                num_2 = (temp[2, 1] - temp[2, 2]) / angle[i, j] + temp[2, 1]
                if not (img_tidu[i, j] > num_1 and img_tidu[i, j] > num_2):
                    flag = False
            elif angle[i, j] >= 1:
                num_1 = (temp[0, 2] - temp[0, 1]) / angle[i, j] + temp[0, 1]
                num_2 = (temp[2, 0] - temp[2, 1]) / angle[i, j] + temp[2, 1]
                if not (img_tidu[i, j] > num_1 and img_tidu[i, j] > num_2):
                    flag = False
            elif angle[i, j] > 0:
                num_1 = (temp[0, 2] - temp[1, 2]) * angle[i, j] + temp[1, 2]
                num_2 = (temp[2, 0] - temp[1, 0]) * angle[i, j] + temp[1, 0]
                if not (img_tidu[i, j] > num_1 and img_tidu[i, j] > num_2):
                    flag = False
            elif angle[i, j] < 0:
                num_1 = (temp[1, 0] - temp[0, 0]) * angle[i, j] + temp[1, 0]
                num_2 = (temp[1, 2] - temp[2, 2]) * angle[i, j] + temp[1, 2]
                if not (img_tidu[i, j] > num_1 and img_tidu[i, j] > num_2):
                    flag = False
            if flag:
                img_yizhi[i, j] = img_tidu[i, j]
    plt.figure(3)
    plt.imshow(img_yizhi.astype(np.uint8), cmap='gray')
    plt.axis('off')
 
    # 4、雙閾值檢測,連接邊緣。遍歷所有一定是邊的點,查看8鄰域是否存在有可能是邊的點,進棧
    lower_boundary = img_tidu.mean() * 0.5
    high_boundary = lower_boundary * 3  # 這裡我設置高閾值是低閾值的三倍
    zhan = []
    for i in range(1, img_yizhi.shape[0]-1):  # 外圈不考慮瞭
        for j in range(1, img_yizhi.shape[1]-1):
            if img_yizhi[i, j] >= high_boundary:  # 取,一定是邊的點
                img_yizhi[i, j] = 255
                zhan.append([i, j])
            elif img_yizhi[i, j] <= lower_boundary:  # 舍
                img_yizhi[i, j] = 0
 
    while not len(zhan) == 0:
        temp_1, temp_2 = zhan.pop()  # 出棧
        a = img_yizhi[temp_1-1:temp_1+2, temp_2-1:temp_2+2]
        if (a[0, 0] < high_boundary) and (a[0, 0] > lower_boundary):
            img_yizhi[temp_1-1, temp_2-1] = 255  # 這個像素點標記為邊緣
            zhan.append([temp_1-1, temp_2-1])  # 進棧
        if (a[0, 1] < high_boundary) and (a[0, 1] > lower_boundary):
            img_yizhi[temp_1 - 1, temp_2] = 255
            zhan.append([temp_1 - 1, temp_2])
        if (a[0, 2] < high_boundary) and (a[0, 2] > lower_boundary):
            img_yizhi[temp_1 - 1, temp_2 + 1] = 255
            zhan.append([temp_1 - 1, temp_2 + 1])
        if (a[1, 0] < high_boundary) and (a[1, 0] > lower_boundary):
            img_yizhi[temp_1, temp_2 - 1] = 255
            zhan.append([temp_1, temp_2 - 1])
        if (a[1, 2] < high_boundary) and (a[1, 2] > lower_boundary):
            img_yizhi[temp_1, temp_2 + 1] = 255
            zhan.append([temp_1, temp_2 + 1])
        if (a[2, 0] < high_boundary) and (a[2, 0] > lower_boundary):
            img_yizhi[temp_1 + 1, temp_2 - 1] = 255
            zhan.append([temp_1 + 1, temp_2 - 1])
        if (a[2, 1] < high_boundary) and (a[2, 1] > lower_boundary):
            img_yizhi[temp_1 + 1, temp_2] = 255
            zhan.append([temp_1 + 1, temp_2])
        if (a[2, 2] < high_boundary) and (a[2, 2] > lower_boundary):
            img_yizhi[temp_1 + 1, temp_2 + 1] = 255
            zhan.append([temp_1 + 1, temp_2 + 1])
 
    for i in range(img_yizhi.shape[0]):
        for j in range(img_yizhi.shape[1]):
            if img_yizhi[i, j] != 0 and img_yizhi[i, j] != 255:
                img_yizhi[i, j] = 0
 
    # 繪圖
    plt.figure(4)
    plt.imshow(img_yizhi.astype(np.uint8), cmap='gray')
    plt.axis('off')  # 關閉坐標刻度值
    plt.show()

2、借助opencv庫實現的:canny.py

#!/usr/bin/env python
# encoding=gbk

import cv2
import numpy as np

'''
cv2.Canny(image, threshold1, threshold2[, edges[, apertureSize[, L2gradient ]]])   
必要參數:
第一個參數是需要處理的原圖像,該圖像必須為單通道的灰度圖;
第二個參數是滯後閾值1;
第三個參數是滯後閾值2。
'''

img = cv2.imread("lenna.png", 1)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
cv2.imshow("canny", cv2.Canny(gray, 200, 300))
cv2.waitKey()
cv2.destroyAllWindows()

3、優化後的程序:canny_track.py

#!/usr/bin/env python
# encoding=gbk
 
'''
Canny邊緣檢測:優化的程序
'''
import cv2
import numpy as np 
 
def CannyThreshold(lowThreshold):  
    detected_edges = cv2.GaussianBlur(gray,(3,3),0) #高斯濾波 
    detected_edges = cv2.Canny(detected_edges,
            lowThreshold,
            lowThreshold*ratio,
            apertureSize = kernel_size)  #邊緣檢測
 
     # just add some colours to edges from original image.  
    dst = cv2.bitwise_and(img,img,mask = detected_edges)  #用原始顏色添加到檢測的邊緣上
    cv2.imshow('canny demo',dst)  
  
 
lowThreshold = 0  
max_lowThreshold = 100  
ratio = 3  
kernel_size = 3  
  
img = cv2.imread('lenna.png')  
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)  #轉換彩色圖像為灰度圖
  
cv2.namedWindow('canny demo')  
  
#設置調節杠,
'''
下面是第二個函數,cv2.createTrackbar()
共有5個參數,其實這五個參數看變量名就大概能知道是什麼意思瞭
第一個參數,是這個trackbar對象的名字
第二個參數,是這個trackbar對象所在面板的名字
第三個參數,是這個trackbar的默認值,也是調節的對象
第四個參數,是這個trackbar上調節的范圍(0~count)
第五個參數,是調節trackbar時調用的回調函數名
'''
cv2.createTrackbar('Min threshold','canny demo',lowThreshold, max_lowThreshold, CannyThreshold)  
  
CannyThreshold(0)  # initialization  
if cv2.waitKey(0) == 27:  #wait for ESC key to exit cv2
    cv2.destroyAllWindows()  

到此這篇關於Python實現邊緣提取的示例代碼的文章就介紹到這瞭,更多相關Python邊緣提取內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: