


推文:
OpenCV學習(7) 分水嶺算法(1)(原理簡介簡單明了)
OpenCV-Python教程:31.分水嶺算法對圖像進行分割(步驟講解不錯)
使用分水嶺算法進行圖像分割
(一)獲取灰度圖像,二值化圖像,進行形態學操作,消除噪點
def watershed_demo(image): blur = cv.pyrMeanShiftFiltering(image,10,100) gray = cv.cvtColor(blur,cv.COLOR_BGR2GRAY) #獲取灰度圖像 ret,binary = cv.threshold(gray,0,255,cv.THRESH_BINARY|cv.THRESH_OTSU) #將圖像轉為黑色和白色部分 cv.imshow("binary",binary) #獲取二值化圖像

#形態學操作,進一步消除圖像中噪點 kernel = cv.getStructuringElement(cv.MORPH_RECT,(3,3)) mb = cv.morphologyEx(binary,cv.MORPH_OPEN,kernel,iterations=2) #iterations連續兩次開操作,消除圖像的噪點

(二)在距離變換前加上一步操作:通過對上面形態學去噪點后的圖像,進行膨脹操作,可以得到大部分都是背景的區域(原黑色不是我們需要的部分是背景)
sure_bg = cv.dilate(mb,kernel,iterations=3) #3次膨脹,可以獲取到大部分都是背景的區域
(三)使用距離變換distanceTransform獲取確定的前景色
根據distanceTransform獲取距離背景最小距離的結果(詳細看下面相關知識補充) 根據distanceTransform操作的結果,設置一個閾值,使用threshold決定哪些區域是前景,這樣得到正確結果的概率很高
dist = cv.distanceTransform(mb,cv.DIST_L2,5) #獲取距離數據結果 ret, sure_fg = cv.threshold(dist,dist.max()*0.6,255,cv.THRESH_BINARY) #獲取前景色

相關知識補充(重點)
(1)距離變換原理
推文:圖像識別中距離變換的原理及作用詳解,並附用OpenCV中的distanceTransform實現距離變換的代碼!(距離變換的定義講得不錯)
距離變換的處理圖像通常都是二值圖像,而二值圖像其實就是把圖像分為兩部分,即背景和物體兩部分,物體通常又稱為前景目標!
通常我們把前景目標的灰度值設為255,即白色
背景的灰度值設為0,即黑色。
所以定義中的非零像素點即為前景目標,零像素點即為背景。
所以圖像中前景目標中的像素點距離背景越遠,那么距離就越大,如果我們用這個距離值替換像素值,那么新生成的圖像中這個點越亮。

再通過設定合理的閾值對距離變換后的圖像進行二值化處理,則可得到去除手指的圖像(如下圖“bidist”窗口圖像所示),手掌重心即為該圖像的幾何中心。

主要用於計算非零像素到最近零像素點的最短距離。一般用於求解圖像的骨骼
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返回的圖像數據是浮點數值,要想在浮點數表示的顏色空間中,數值范圍必須是0-1.0,所以要將其中的數值進行歸一化處理
(重點)在整數表示的顏色空間中,數值范圍是0-255,但在浮點數表示的顏色空間中,數值范圍是0-1.0,所以要把0-255歸一化。
順便補充:若是不做歸一化處理,數值大於1的都會變為1.0處理
mb = cv.morphologyEx(binary,cv.MORPH_OPEN,kernel,iterations=2) #iterations連續兩次開操作 cv.imshow("mb", mb) #這是我們形態學開操作過濾噪點后的圖像,暫時可以看做源圖像 #距離變換 dist = cv.distanceTransform(mb,cv.DIST_L2,5) #這是我們獲取的字段距離數值,對應每個像素都有,所以數組結構和圖像數組一致 cv.imshow("dist",dist) dist_output = cv.normalize(dist,0,1.0,cv.NORM_MINMAX) #歸一化的距離圖像數組
cv.imshow("distinct-t",dist_output*50)

發現了似乎distanceTransform返回的圖像和源圖像一樣,似乎出錯了
原因:因為distanceTransform返回的是浮點型色彩空間,而dist中存放的數距離0值的最小距離,大多是大於1.0的數值,
而上面提到浮點型色彩空間數值范圍0-1.0,當數值大於1.0都會被設置為1.0,顯示白色,所以和原來的二值化圖像一致,
我們要想顯示骨骼,必須先進行歸一化處理
下面是從二值化圖像源,distanceTransform距離數組,和歸一化距離數組中獲取的一段像素數組
print(mb[150][120:140]) print(dist[150][120:140]) print(dist_output[150][120:140])
整數型色彩空間二值化圖像
[ 0 0 0 0 0 0 0 0 0 255 255 255 255 255 255 255 255 255 255 255]
浮點型色彩空間最小距離數組,由於數值大於1.0都會被設置為1.0,所以和上面二值化圖像一致 [ 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 1.4 2.1969 3.1969 4.1969 5.1969 6.1969 7.1969 8.196899 9.196899 10.187599]
浮點型色彩空間歸一化數組圖像,顯示骨骼 [0. 0. 0. 0. 0. 0. 0. 0. 0. 0.00047065 0.0006589 0.00103396 0.00150461 0.00197525 0.00244589 0.00291654 0.00338719 0.00385783 0.00432847 0.00479474]
(四)在獲取了背景區域和前景區域(其實前景區域是我們的種子,我們將從這里進行灌水,向四周漲水,但是這個需要在markers中表示)后,這兩個區域中有未重合部分(注1)怎么辦?首先確定這些區域(尋找種子)
注1:
這里是求取硬幣偏白色,使用THRESH_BINARY,所以我們獲取對象是白色區域,是獲取未重合部分
若是我們求取樹葉等偏黑,需要使用THRESH_BINARY_INV,此時我們獲取的對象是黑色區域,就變為了獲取重合部分了

開始獲取未知區域unknown(柵欄會創建在這一區域),為下一步獲取種子做准備
surface_fg = np.uint8(sure_fg) #保持色彩空間一致才能進行運算,現在是背景空間為整型空間,前景為浮點型空間,所以進行轉換 unknown = cv.subtract(sure_bg,surface_fg) cv.imshow("unkown",unknown)
使用print查看背景前景色彩空間不同 print(sure_fg[150][120:140]) print(sure_bg[150][120:140]) --------------------------------------- [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.] [ 0 0 0 0 0 0 255 255 255 255 255 255 255 255 255 255 255 255 255 255]

(五)獲取了這些區域,我們可以獲取種子,這是通過connectedComponents實現,獲取masker標簽,確定的前景區域會在其中顯示為以1開始的數據,這就是我們的種子,會從這里開始漫水
推文:http://m.imooc.com/article/32675
推文:基於矩陣實現的Connected Components算法
利用connectedComponents求圖中的連通圖
重點:
現在知道了那些是背景那些是硬幣(確定的前景區域)了。
那我們就可以創建標簽(一個與原圖像大小相同,數據類型為 in32 的數組),並標記其中的區域了。
對我們已經確定分類的區域(無論是前景還是背景)使用不同的正整數標記,對我們不確定的區域(unknown區域)使用 0 標記。
我們可以使用函數 cv2.connectedComponents()來做這件事。
它會把對標簽進行操作,將背景標記為 0,其他的對象使用從 1 開始的正整數標記(其實這就是我們的種子,水漫時會從這里漫出)。然后將這個標簽返回給我們markers 但是,我們知道如果背景標記為 0,那分水嶺算法就會把它當成未知區域了。(我們要將未知區域標記為0,所以我們要將背景區域變為其他整數,例如+1)
所以我們想使用不同的整數標記它們。
而對不確定的區域(函數cv2.connectedComponents 輸出的結果中使用 unknown 定義未知區域)標記為 0。
#獲取mask
ret,markers = cv.connectedComponents(surface_fg)
函數原型:
def connectedComponents(image, labels=None, connectivity=None, ltype=None): # real signature unknown; restored from __doc__
參數:
參數image是需要進行連通域處理的二值圖像,其他的這里用不到
返回值:
ret是連通域處理的邊緣條數,是上面提到的確定區域(出去背景外的其他確定區域:就是前景),就是種子數,我們會從種子開始向外漲水 markers是我們創建的一個標簽(一個與原圖像大小相同,數據類型為 in32 的數組),其中包含有我們原圖像的確認區域的數據(前景區域)
查看部分markers:(0代表的是背景色,)
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 #0是我們的背景區域 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 2 #像這些以1開始的整數就是我們確定的前景區域,就是我們要找的種子 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
(六)根據未知區域unknown在markers中設置柵欄,並將背景區域加入種子區域,一起漫水
注意:
watershed漫水算法需要我們將柵欄區域設置為0,所以我們需要將markers中背景區域(原來為0,會干擾算法)設置為其他整數。
解決方法將markers整體加一 #此時種子區域不止我們原來的前景區域,有增加了一個背景區域,我們將從這些區域一起灌水
markers = markers + 1 markers[unknown==255] = 0
(七)根據種子開始漫水,讓水漫起來找到最后的漫出點(柵欄邊界),越過這個點后各個山谷中水開始合並。注意watershed會將找到的柵欄在markers中設置為-1
markers = cv.watershed(image,markers=markers) #獲取柵欄 image[markers==-1] = [0,0,255] #根據柵欄,我們對原圖像進行操作,對柵欄區域設置為紅色
markers再次查看
[-1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 -1 -1 -1 -1 -1 -1 -1 #漫水算法會將找到的柵欄設置為-1 -1 -1 -1 -1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 -1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 -1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 -1]
(八)結果查看

(九)全部代碼
import cv2 as cv import numpy as np def watershed_demo(image): blur = cv.pyrMeanShiftFiltering(image,10,100) gray = cv.cvtColor(blur,cv.COLOR_BGR2GRAY) #獲取灰度圖像 ret,binary = cv.threshold(gray,0,255,cv.THRESH_BINARY|cv.THRESH_OTSU) #形態學操作,進一步消除圖像中噪點 kernel = cv.getStructuringElement(cv.MORPH_RECT,(3,3)) mb = cv.morphologyEx(binary,cv.MORPH_OPEN,kernel,iterations=2) #iterations連續兩次開操作 sure_bg = cv.dilate(mb,kernel,iterations=3) #3次膨脹,可以獲取到大部分都是背景的區域 cv.imshow("sure_bg",sure_bg) #距離變換 dist = cv.distanceTransform(mb,cv.DIST_L2,5) cv.imshow("dist",dist) dist_output = cv.normalize(dist,0,1.0,cv.NORM_MINMAX) # print(mb[150][120:140]) # print(dist[150][120:140]) # print(dist_output[150][120:140]) cv.imshow("distinct-t",dist_output*50) ret, sure_fg = cv.threshold(dist,dist.max()*0.6,255,cv.THRESH_BINARY) cv.imshow("sure_fg",sure_fg) # print(sure_fg[150][120:140]) # print(sure_bg[150][120:140]) #獲取未知區域 surface_fg = np.uint8(sure_fg) #保持色彩空間一致才能進行運算,現在是背景空間為整型空間,前景為浮點型空間,所以進行轉換 unknown = cv.subtract(sure_bg,surface_fg) cv.imshow("unkown",unknown) #獲取maskers,在markers中含有種子區域 ret,markers = cv.connectedComponents(surface_fg) #print(ret) #分水嶺變換 markers = markers + 1 markers[unknown==255] = 0 markers = cv.watershed(image,markers=markers) image[markers==-1] = [0,0,255] cv.imshow("result",image) src = cv.imread("./c.png") #讀取圖片 cv.namedWindow("input image",cv.WINDOW_AUTOSIZE) #創建GUI窗口,形式為自適應 cv.imshow("input image",src) #通過名字將圖像和窗口聯系 watershed_demo(src) cv.waitKey(0) #等待用戶操作,里面等待參數是毫秒,我們填寫0,代表是永遠,等待用戶操作 cv.destroyAllWindows() #銷毀所有窗口

