1. 程式人生 > 其它 >OpenCV-Python系列之影象分割與Watershed演算法

OpenCV-Python系列之影象分割與Watershed演算法

本次我們來看影象分割,同樣也是OpenCV中較為重要的一個部分。影象分割是按照一定的原則,將一幅影象分為若干個互不相交的小局域的過程,它是影象處理中最為基礎的研究領域之一。目前有很多影象分割方法,其中分水嶺演算法是一種基於區域的影象分割演算法,分水嶺演算法因實現方便,已經在醫療影象,模式識別等領域得到了廣泛的應用。

傳統分水嶺演算法基本原理

分水嶺比較經典的計算方法是L.Vincent於1991年在PAMI上提出的[1]。傳統的分水嶺分割方法,是一種基於拓撲理論的數學形態學的分割方法,其基本思想是把影象看作是測地學上的拓撲地貌,影象中每一畫素的灰度值表示該點的海拔高度,每一個區域性極小值及其影響區域稱為集水盆地,而集水盆地的邊界則形成分水嶺。分水嶺的概念和形成可以通過模擬浸入過程來說明。在每一個區域性極小值表面,刺穿一個小孔,然後把整個模型慢慢浸人水中,隨著浸入的加深,每一個區域性極小值的影響域慢慢向外擴充套件,在兩個集水盆匯合處構築大壩如下圖所示,即形成分水嶺。我們來看傳統分水嶺演算法示意圖:

然而基於梯度影象的直接分水嶺演算法容易導致影象的過分割,產生這一現象的原因主要是由於輸入的影象存在過多的極小區域而產生許多小的集水盆地,從而導致分割後的影象不能將影象中有意義的區域表示出來。所以必須對分割結果的相似區域進行合併。

改進的分水嶺演算法基本原理

因為傳統的分水嶺分割演算法會由於影象中的噪聲或其他不規則性而產生過度分割的結果。因此OpenCV實現了一個基於標記的分水嶺演算法,可以指定哪些是要合併的山谷點,哪些不是。這是一個互動式的影象分割。我們所做的是給我們知道的物件賦予不同的標籤。用一種顏色(或強度)標記我們確定為前景或物件的區域,用另一種顏色標記我們確定為背景或非物件的區域,最後用0標記我們不確定的區域。這是我們的標記。然後應用分水嶺演算法。然後我們的標記將使用我們給出的標籤進行更新,物件的邊界值將為-1。傳統的基於梯度的分水嶺演算法和改進後基於標記的分水嶺演算法示意圖如下圖所示:

從上圖可以看出,傳統基於梯度的分水嶺演算法由於區域性最小值過多造成分割後的分水嶺較多。而基於標記的分水嶺演算法,水淹過程從預先定義好的標記影象(畫素)開始,較好的克服了過度分割的不足。本質上講,基於標記點的改進演算法是利用先驗知識來幫助分割的一種方法。因此,改進演算法的關鍵在於如何獲得準確的標記影象,即如何將前景物體與背景準確的標記出來。

OpenCV中的影象分割

OpenCV提供了相關的函式API進行分水嶺分割操作,我們來看函式原型:
markers=cv.watershed(image, markers)

image輸入8位3通道影象。

markers:標記的輸入/輸出32位單通道影象(圖)。 它的大小應與image相同。

我們使用示例影象:

首先我們使用Otsu的二值化找到硬幣的近似估計值:

def watershed(img):
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)

    cv2.imshow('show', thresh)
    cv2.waitKey(0)

現在我們需要去除影象中的任何小的白噪聲,因此我們要使用形態學開運算,為了去除物體上的小洞,我們要使用形態學閉運算,所以,現在我們可以確定,靠近物體中心的區域是前景,遠離物體的區域是背景,只有硬幣的邊界區域是我們不確定的區域.

我們需要提取出我們確信它們是硬幣的區域,腐蝕邊界畫素,不管剩下的是什麼,我們都可以確定它是硬幣.如果它們不相互接觸還可以繼續,如果它們相互接觸,另一個好的選擇是找到距離變換並應用一個合適的閾值.

為此,我們對結果進行了擴張,擴張將物件邊界增加為背景,通過這種方法,我們可以確保背景中的任何區域都是真正的背景,因為邊界區域被移除.

剩下的區域是我們不知道的區域,無論是硬幣還是背景.分水嶺演算法應該找到它, 這些區域通常圍繞著前景和背景相遇的硬幣邊界(甚至兩個不同的硬幣相遇),它可以從sure_bg區域中減去sure_fg區域獲得。

來看程式碼:

# noise removal
kernel = np.ones((3,3),np.uint8)
opening = cv2.morphologyEx(thresh,cv2.MORPH_OPEN,kernel, iterations = 2)
 
# sure background area
sure_bg = cv2.dilate(opening,kernel,iterations=3)
 
# Finding sure foreground area
dist_transform = cv2.distanceTransform(opening,cv.DIST_L2,5)
ret, sure_fg = cv2.threshold(dist_transform, 0.7*dist_transform.max(),255,0)
 
# Finding unknown region
sure_fg = np.uint8(sure_fg)
unknown = cv2.subtract(sure_bg,sure_fg)

我們看處理之後的影象,sure_bg:

sure_fg:

看一下處理之後的影象:

現在我們可以確定哪些是硬幣的區域,哪些是背景,哪些是背景.因此,我們建立標記(它是一個與原始影象相同大小的陣列,但使用int32資料型別)並對其內部的區域進行標記。

cv2.connectedComponents()

將影象的背景標記為0,然後其他物件從1開始標記為整數。

我們知道,如果背景是0,那麼分水嶺將會被認為是未知的區域, 所以我們用不同的整數來標記它,用0表示由未知定義的未知區域。

# Marker labelling
ret, markers = cv2.connectedComponents(sure_fg)
 
# Add one to all labels so that sure background is not 0, but 1
markers = markers+1
 
# Now, mark the region of unknown with zero
markers[unknown==255] = 0

標記已經準備好了,現在是最後一步的時候了,應用分水嶺,最終程式碼:

def watershed(img):
     gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
     ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
 
     kernel = np.ones((3, 3), np.uint8)
     opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2)  # 
形態開運算
 
     sure_bg = cv2.dilate(opening, kernel, iterations=3)
 
     dist_transform = cv2.distanceTransform(opening, cv2.DIST_L2, 5)
     ret, sure_fg = cv2.threshold(dist_transform, 0.7 * dist_transform.max(), 255, 0)
 
     sure_fg = np.uint8(sure_fg)
     unknown = cv2.subtract(sure_bg, sure_fg)
 
     ret, markers = cv2.connectedComponents(sure_fg)
 
     markers = markers + 1
 
     markers[unknown == 255] = 0
 
     markers = cv2.watershed(img, markers)
     img[markers == -1] = [255, 0, 0]
 
     cv2.imshow('img', img)
     cv2.waitKey(0)

可以看到,最終結果顯示完美分割。

天道酬勤 循序漸進 技壓群雄