天天看點

分水嶺算法用于實作分割兩個挨在一起的物體

參考部落格:https://zhuanlan.zhihu.com/p/67741538

前提:任何兩個相鄰連接配接的物體不一定能被分水嶺邊界(marker标記為-1的像素)分開,比如在傳遞給 watershed 函數的初始标記圖像中的前景是互相接觸的話是分不開的。

分水嶺算法原理:

       灰階圖像可以看成是一個地形表面,高強度值表示山峰,低強度值及較低強度值表示山谷及其影響區域。用不同顔色的水(标簽)填充每個孤立的山谷(局部極小值)。當水上升時,根據附近的山峰(圖像的梯度),不同山谷不同顔色的水顯然會開始融合。為了避免這種情況,在水就要融合的地方及時增加屏障(增高水壩)。然後繼續漲水,建立屏障,直到所有的山峰都被淹沒,最後建立的屏障形成的線就是分割結果,是以分水嶺表示的其實是輸入圖像的極大值點。

       分水嶺就是分隔相鄰兩個山谷的山嶺或高地,見下圖所示(https://baike.baidu.com/item/流域分水嶺/22183313):

分水嶺算法用于實作分割兩個挨在一起的物體

由于opencv的實作中用到了距離變換,這裡先簡介距離變換的原理:

距離變換原理:

定義:計算二值圖像中非零像素點到最近的零像素點的距離。

  距離變換的處理圖像通常都是二值圖像,而二值圖像其實就是把圖像分為兩部分,即背景和物體兩部分,物體通常又稱為前景目标,通常前景目标的灰階值設定為255(白色),背景的灰階值設定為0(黑色),是以定義中的非零像素點即為前景目标,零像素點即為背景,是以圖像中前景目标中的像素點距離背景越遠,那麼距離就越大,如果用這個距離值替代像素值,那麼新生成的圖像中這個點就越亮。

  如果想顯示距離變換後的圖,需要對經過距離變換後的圖進行歸一化處理,因為經過距離變換傳回的圖像資料是浮點數值,要想在浮點數表示的顔色空間(注:在整數表示的顔色空間中,數值範圍是0-255)中,數值範圍必須是0-1,是以要将其中的數值進行歸一化處理,若是不做歸一化處理,數值大于1的都會變為1.0處理(結果與二值化結果一樣)。

對互相接觸的硬币利用分水嶺算法分割opencv實作:

Opencv是基于輸入圖像及其标記圖像找到分水嶺實作分割的。

在OpenCV中,我們需要給不同區域貼上不同的标簽。用大于1的整數表示我們确定為前景或對象的區域,用1表示我們确定為背景或非對象的區域,最後用0表示我們無法确定的區域,相當于是分水嶺中的注水點位置,從這些點開始注水使得水準面上升。然後應用分水嶺算法,我們的标記圖像将被更新,更新後的标記圖像的邊界像素值為-1,也就是分割結果。

0、原始圖像:

分水嶺算法用于實作分割兩個挨在一起的物體

1、利用門檻值法分割得到二值圖像:

import cv2 as cv

image = cv.imread(r'C:\Users\Desktop\a.jpg')
gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
ret, threshod_image = cv.threshold(gray, 0, 255, cv.THRESH_BINARY_INV+cv.THRESH_OTSU)
cv.imwrite(r'C:\Users\Desktop\threshod_image.jpg',threshod_image)
cv.imshow('threshod_image',threshod_image)
cv.waitKey(0)
           
分水嶺算法用于實作分割兩個挨在一起的物體

2、使用開運算去除圖像中的細小白色噪點

kernel = cv.getStructuringElement(cv.MORPH_RECT, (3,3))
opening = cv.morphologyEx(threshod_image, cv.MORPH_OPEN, kernel, iterations=2)
cv.imwrite(r'C:\Users\Desktop\opening.jpg',opening)
cv.imshow('opening_image',opening)
cv.waitKey(0)
           
分水嶺算法用于實作分割兩個挨在一起的物體

3、距離變換後設定門檻值分割确定确切的前景區域

dist_transform = cv.distanceTransform(opening, cv.DIST_L2, 5)
# Normalize the distance image for range = {0.0, 1.0}
cv.normalize(dist_transform, dist_transform, 0, 1.0, cv.NORM_MINMAX)
cv.imwrite(r'C:\Users\Desktop\dist_transform.jpg',dist_transform)
cv.imshow('dist_transform_image',dist_transform)
cv.waitKey(0)
           
分水嶺算法用于實作分割兩個挨在一起的物體
分水嶺算法用于實作分割兩個挨在一起的物體

4、對開操作後進行膨脹運算使得一部分背景成為了硬币的邊界,得到的圖像中的黑色區域肯定是真實背景,即遠離硬币的區域

dilate_image = cv.dilate(opening, kernel, iterations=2)
cv.imwrite(r'C:\Users\Desktop\dilate_image.jpg',dilate_image)
cv.imshow('dilate_image',dilate_image)
cv.waitKey(0)
           
分水嶺算法用于實作分割兩個挨在一起的物體

5、膨脹後圖像減去确切的前景區域圖像得到硬币邊界處不确定是背景還是硬币本身的區域,即是未知的區域

unknown = cv.subtract(dilate_image,dist_transform_threshold_image)
cv.imwrite(r'C:\Users\Desktop\unknown.jpg',unknown)
cv.imshow('unknown',unknown)
cv.waitKey(0)
           
分水嶺算法用于實作分割兩個挨在一起的物體

6、建立标記圖像

現在我們可以确定哪些是硬币區域,哪些是背景區域。然後建立标記(marker,它是一個與原始圖像大小相同的矩陣,int32資料類型),opencv中的分水嶺算法将标記的0的區域視為不确定區域,将标記為1的區域視為背景區域,将标記大于1的正整數表示我們想得到的前景,使用 cv2.connectedComponents() 來實作标記,但它是用0标記圖像的背景,用大于0的整數标記其他對象,是以我們需要對其進行加一,用1來标記圖像的背景。

ret2, markers = cv.connectedComponents(dist_transform_threshold_image)
markers = markers+1
markers[unknown==255] = 0
markers_copy = markers.copy()
markers_copy[markers==0] = 150  # 灰色表示未知區域
markers_copy[markers==1] = 0    # 黑色表示背景
markers_copy[markers>1] = 255   # 白色表示前景
markers_copy = np.uint8(markers_copy)
cv.imwrite(r'C:\Users\Desktop\markers.jpg',markers_copy)
cv.imshow('markers_copy',markers_copy)
cv.waitKey(0)
           
分水嶺算法用于實作分割兩個挨在一起的物體

注意:初始标記圖像中的物體不能互相接觸,不然用分水嶺分不開

分水嶺算法用于實作分割兩個挨在一起的物體

7、應用分水嶺算法

markers = cv.watershed(image, markers)
markers[0:1,:]=1
markers[-1,:]=1
markers[:,0:1]=1
markers[:,-1]=1
print(markers)
image[markers==-1] = [0,0,255]  # 将邊界标記為紅色
cv.imwrite(r'C:\Users\Desktop\image.jpg',image)
cv.imshow('image',image)
cv.waitKey(0)
           
分水嶺算法用于實作分割兩個挨在一起的物體

注意:THRESH_BINARY_INV是THRESH_BINARY的取反結果

繼續閱讀