二十一 分水嶺演算法
一、原理
分水嶺演算法主要用於影象分段,通常是把一副彩色影象灰度化,然後再求梯度圖,最後在梯度圖的基礎上進行分水嶺演算法,求得分段影象的邊緣線。
任意的灰度影象可以被看做是地質學表面,高亮度的地方是山峰,低亮度的地方是山谷。給每個孤立的山谷(區域性最小值)不同顏色的水(標籤),當水漲起來,根據周圍的山峰(梯度),不同的山谷也就是不同的顏色會開始合併,要避免這個,你可以在水要合併的地方建立障礙,直到所有山峰都被淹沒。你所建立的障礙就是分割結果,這個就是分水嶺的原理,但是這個方法會分割過度,因為有噪點,或者其他影象上的錯誤。所以OpenCV實現了一個機遇標記的分水嶺演算法,你可以指定哪些是要合併的點,哪些不是,這是一個互動式的影象分割,我們要做的是給不同的標籤。給我們知道是前景或者是目標用一種顏色加上標籤,給我們知道是背景或者非目標加上另一個顏色,最後不知道是什麼的區域標記為0. 然後使用分水嶺演算法。對灰度圖的地形學解釋,我們我們考慮三類點:
1. 區域性最小值點,該點對應一個盆地的最低點,當我們在盆地裡滴一滴水的時候,由於重力作用,水最終會匯聚到該點。注意:可能存在一個最小值面,該平面內的都是最小值點。
2. 盆地的其它位置點,該位置滴的水滴會匯聚到區域性最小點。
3. 盆地的邊緣點,是該盆地和其它盆地交接點,在該點滴一滴水,會等概率的流向任何一個盆地。
假設我們在盆地的最小值點,打一個洞,然後往盆地裡面注水,並阻止兩個盆地的水彙集,我們會在兩個盆地的水彙集的時刻,在交接的邊緣線上(也即分水嶺線),建一個壩,來阻止兩個盆地的水彙集成一片水域。這樣影象就被分成2個畫素集,一個是注水盆地畫素集,一個是分水嶺線畫素集。
下面的gif圖很好的演示了分水嶺演算法的效果:
為了解決過度分割的問題,可以使用基於標記(mark)影象的分水嶺演算法,就是通過先驗知識,來指導分水嶺演算法,以便獲得更好的影象分段效果。通常的mark影象,都是在某個區域定義了一些灰度層級,在這個區域的洪水淹沒過程中,水平面都是從定義的高度開始的,這樣可以避免一些很小的噪聲極值區域的分割。
下面的gif圖很好的演示了基於mark的分水嶺演算法過程:
上面的過度分段影象,我們通過指定mark區域,可以得到很好的分段效果:
二、步驟
1、獲取灰度影象,二值化影象,進行形態學操作,消除噪點
1 #去除噪點 2 blur = cv.pyrMeanShiftFiltering(image,10,100) 3 4 #二值影象 5 gray = cv.cvtColor(blur,cv.COLOR_BGR2GRAY) 6 ret,binary = cv.threshold(gray,0,255,cv.THRESH_BINARY_INV|cv.THRESH_OTSU) 7 cv.imshow('binary',binary) 8 9 #形態學操作,開操作兩次,進一步消除噪點 10 kernel = cv.getStructuringElement(cv.MORPH_RECT,(3,3)) 11 mb = cv.morphologyEx(binary,cv.MORPH_OPEN,kernel,iterations=2) 12
2、在距離變換前加上一步操作:通過對上面形態學去噪點後的影象,進行膨脹操作,可以得到大部分都是背景的區域(原黑色不是我們需要的部分是背景)
#膨脹 背景色 dilate_image = cv.dilate(mb,kernel,iterations=3) cv.imshow('background',dilate_image)
3、使用距離變換distanceTransform獲取確定的前景色
(1)距離變換原理
距離變換的定義是計算一個影象中非零畫素點到最近的零畫素點的距離,也就是到零畫素點的最短距離。
這個定義對於初接觸距離變換的人來說,完全不知所云啊~那是因為缺乏一些知識背景,下面聽我慢慢道來吧!
距離變換的處理影象通常都是二值影象,而二值影象其實就是把影象分為兩部分,即背景和物體兩部分,物體通常又稱為前景目標!通常我們把前景目標的灰度值設為255,即白色,背景的灰度值設為0,即黑色。所以定義中的非零畫素點即為前景目標,零畫素點即為背景。所以影象中前景目標中的畫素點距離背景越遠,那麼距離就越大,如果我們用這個距離值替換畫素值,那麼新生成的影象中這個點越亮。具體的應用就是找前景目標的中心~下面給一個具體的例子。
下面這個例子是確認手掌中心的例子:
由於伸出的手指相對於手掌來說比較細(如下圖“src”視窗影象所示),也就是說手指上的畫素距離零畫素距離很短,所以經過距離變換後的影象在手指部位的畫素值較小(如下圖“dst”視窗影象所示),通過設定合理的閾值對距離變換後的影象進行二值化處理,則可得到去除手指的影象(如下圖“bidist”視窗影象所示),手掌重心即為該影象的幾何中心。
其它需要注意的:
從定義中我們可以看出距離變換中其實只計算前景目標區域(即非零畫素點)的距離值!
(2)distancetransform函式
主要用於計算非零畫素到最近零畫素點的最短距離。一般用於求解影象的骨骼,得到的是距離影象陣列!
def distanceTransform(src, distanceType, maskSize, dst=None, dstType=None): # real signature unknown; restored from __doc__
src:輸入的影象,一般為二值影象
distanceType:所用的求解距離的型別,有CV_DIST_L1, CV_DIST_L2 , or CV_DIST_C
mask_size:距離變換掩模的大小,可以是 3 或 5. 對 CV_DIST_L1 或 CV_DIST_C 的情況,引數值被強制設定為 3, 因為 3×3 mask 給出 5×5 mask 一樣的結果,而且速度還更快。
(3)若是想骨骼顯示(對我們的分水嶺流程無影響),我們需要對distanceTransform返回的結果進行歸一化處理,使用normalize
distanceTransform(InputArray src, OutputArray dst, int distanceType, int maskSize)
InputArray src:輸入的影象,一般為二值影象
OutputArray dst:輸出的影象
int distanceType:所用的求解距離的型別、
It can be CV_DIST_L1, CV_DIST_L2 , or CV_DIST_C
mask_size 距離變換掩模的大小,可以是 3 或 5. 對 CV_DIST_L1 或 CV_DIST_C 的情況,引數值被強制設定為 3, 因為 3×3 mask 給出 5×5 mask 一樣的結果,而且速度還更快。
mask
使用者自定義距離情況下的 mask。 在 3×3 mask 下它由兩個數(水平/垂直位量,對角線位移量)組成, 5×5 mask 下由三個陣列成(水平/垂直位移量,對角位移和 國際象棋裡的馬步(馬走日))
函式 cvDistTransform 二值影象每一個象素點到它最鄰近零象素點的距離。對零象素,函式設定 0 距離,對其它象素,它尋找由基本位移(水平、垂直、對角線或knight's move,最後一項對 5×5 mask 有用)構成的最短路徑。 全部的距離被認為是基本距離的和。由於距離函式是對稱的,所有水平和垂直位移具有同樣的代價 (表示為 a ), 所有的對角位移具有同樣的代價 (表示為 b), 所有的 knight's 移動具有同樣的代價 (表示為 c). 對型別 CV_DIST_C 和 CV_DIST_L1,距離的計算是精確的,而型別 CV_DIST_L2 (歐式距離) 距離的計算有某些相對誤差 (5×5 mask 給出更精確的結果), OpenCV 使用 [Borgefors86] 推薦的值:
CV_DIST_C (3×3):a=1, b=1
CV_DIST_L1 (3×3):a=1, b=2
CV_DIST_L2 (3×3):a=0.955, b=1.3693
CV_DIST_L2 (5×5):a=1, b=1.4, c=2.1969
(4)程式碼實現
1 #j距離變換,獲取前景色 2 dist = cv.distanceTransform(mb,cv.DIST_L2,5) #這是我們獲取的欄位距離數值,對應每個畫素都有,所以陣列結構和影象陣列一致 3 cv.imshow('distance',dist) 4 dist_output = cv.normalize(dist,0,1.0,cv.NORM_MINMAX) #歸一化的距離影象陣列 5 cv.imshow('distance_norm',dist_output*100) #因為是0-1,所以顏色對比不明顯,所以需要*100,*50也可以 6 ret,front = cv.threshold(dist,dist.max()*0.6,255,cv.THRESH_BINARY) 7 cv.imshow('front',front) 8 9 #開始獲取未知區域unknown(柵欄會建立在這一區域),為下一步獲取種子做準備 10 front_image = np.uint8(front) 11 unknown = cv.subtract(background,front_image) 12 cv.imshow('unknown',unknown)
4、設定種子:獲取了這些區域,我們可以獲取種子,這是通過connectedComponents實現,獲取masker標籤,確定的前景區域會在其中顯示為以1開始的資料,這就是我們的種子,會從這裡開始漫水
(1)connectedComponents函式
1 def connectedComponents(image, labels=None, connectivity=None, ltype=None): # real signature unknown; restored from __doc__
引數image是需要進行連通域處理的二值影象,其他的這裡用不到
ret是連通域處理的邊緣條數,是上面提到的確定區域(出去背景外的其他確定區域:就是前景),就是種子數,我們會從種子開始向外漲水
markers是我們創 建的一個標籤(一個與原影象大小相同,資料型別為 in32 的陣列),其中包含有我們原影象的確認區域的資料(前景區域)
(2)程式碼實現
根據種子開始漫水,讓水漫起來找到最後的漫出點(柵欄邊界),越過這個點後各個山谷中水開始合併。注意watershed會將找到的柵欄在markers中設定為-1
1 #獲取marker 2 ret,markers = cv.connectedComponents(front_image) 3 # markers = markers + 1 不知道有什麼用 4 # markers[unknown==255] = 0 5 markers = cv.watershed(image,markers=markers) 6 image[markers==-1] = [0,0,255] 7 cv.imshow('finally',image)