【翻譯:OpenCV-Python教程】用分水嶺算法分割圖像

⚠️由於自己的拖延症,3.4.3翻到一半,OpenCV發佈了4.0.0了正式版,所以接下來是按照4.0.0翻譯的。

⚠️除了版本之外,其他還是照舊,Image Segmentation with Watershed Algorithm,原文

目標

在本章,

  • 我們會學到使用分水嶺算法來做基於標記的圖像分割。
  • 我們會遇到:cv.watershed()

原理

任何灰度圖像都可以看做是一張地理圖,高強度的地方就好比是山峯,而低強度的地方就好比是深谷。現在你開始把每一個孤立的深谷(本地強度極小值)用不同顏色的水(標籤)填充,在水位上漲的同時,依賴於附近的山峯(梯度),很明顯,來自不同低谷不同顏色的水會開始融合。爲了避免這些水相互融合,你在這些不同顏色的水的交匯處建立起一些柵欄。然後你繼續倒入水、建立柵欄。直到所有的山峯都被水淹沒。然後,你建立起來的這些柵欄就是你分割的結果。這就是分水嶺算法背後的哲理。你可以訪問 CMM webpage on watershed 在一些動畫的幫助下理解它。

但是,由於圖像中的噪聲和一些不規則形狀,這種方法會給出一個“分割的太過了”的結果。因此 OpenCV 實現了一個基於標記的分水嶺算法,指定了哪些低谷點是需要合併的,哪些不需要合併。這是一個交互式的圖像分割算法,我們是這麼做的,爲我們已知的物體給出不同的標籤,用一種顏色(或者(在灰度圖中是)強度)標記出我們確信是前景或者物體的區域,用另外一種顏色來標記出我們確信爲背景或者非物體的區域,而對於我們不確信的區域,我們就標記爲零。這就是我們的標記,然後再應用分水嶺算法。然後我們的標記將用我們給出的標籤進行更新,對象的邊界值將爲-1。

代碼

下面我們將看到一個例子,關於如何使用距離變換與分水嶺分割相互接觸的對象。

看下面的硬幣圖片,硬幣是相互接觸的。即使你對它用各種閾值法,它還是相互接觸。

water_coins.jpg

我們先找出硬幣的近似值,爲了達到目的,我們可以用大津閾值法。

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('coins.png')
gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
ret, thresh = cv.threshold(gray,0,255,cv.THRESH_BINARY_INV+cv.THRESH_OTSU)

結果:

water_thresh.jpg

現在,我們需要去掉圖像中所有的小噪聲。Now we need to remove any small white noises in the image. For that we can use morphological opening. To remove any small holes in the object, we can use morphological closing. So, now we know for sure that region near to center of objects are foreground and region much away from the object are background. Only region we are not sure is the boundary region of coins.

So we need to extract the area which we are sure they are coins. Erosion removes the boundary pixels. So whatever remaining, we can be sure it is coin. That would work if objects were not touching each other. But since they are touching each other, another good option would be to find the distance transform and apply a proper threshold. Next we need to find the area which we are sure they are not coins. For that, we dilate the result. Dilation increases object boundary to background. This way, we can make sure whatever region in background in result is really a background, since boundary region is removed. See the image below.

water_fgbg.jpg

image

The remaining regions are those which we don't have any idea, whether it is coins or background. Watershed algorithm should find it. These areas are normally around the boundaries of coins where foreground and background meet (Or even two different coins meet). We call it border. It can be obtained from subtracting sure_fg area from sure_bg area.

# noise removal

kernel = np.ones((3,3),np.uint8)

opening = cv.morphologyEx(thresh,cv.MORPH_OPEN,kernel, iterations = 2)

# sure background area

sure_bg = cv.dilate(opening,kernel,iterations=3)

# Finding sure foreground area

dist_transform = cv.distanceTransform(opening,cv.DIST_L2,5)

ret, sure_fg = cv.threshold(dist_transform,0.7*dist_transform.max(),255,0)

# Finding unknown region

sure_fg = np.uint8(sure_fg)

unknown = cv.subtract(sure_bg,sure_fg)

See the result. In the thresholded image, we get some regions of coins which we are sure of coins and they are detached now. (In some cases, you may be interested in only foreground segmentation, not in separating the mutually touching objects. In that case, you need not use distance transform, just erosion is sufficient. Erosion is just another method to extract sure foreground area, that's all.)

water_dt.jpg

image

Now we know for sure which are region of coins, which are background and all. So we create marker (it is an array of same size as that of original image, but with int32 datatype) and label the regions inside it. The regions we know for sure (whether foreground or background) are labelled with any positive integers, but different integers, and the area we don't know for sure are just left as zero. For this we use cv.connectedComponents(). It labels background of the image with 0, then other objects are labelled with integers starting from 1.

But we know that if background is marked with 0, watershed will consider it as unknown area. So we want to mark it with different integer. Instead, we will mark unknown region, defined by unknown, with 0.

# Marker labelling

ret, markers = cv.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

See the result shown in JET colormap. The dark blue region shows unknown region. Sure coins are colored with different values. Remaining area which are sure background are shown in lighter blue compared to unknown region.

water_marker.jpg

image

Now our marker is ready. It is time for final step, apply watershed. Then marker image will be modified. The boundary region will be marked with -1.

markers = cv.watershed(img,markers)

img[markers == -1] = [255,0,0]

See the result below. For some coins, the region where they touch are segmented properly and for some, they are not.

water_result.jpg

image

Additional Resources

  1. CMM page on Watershed Transformation

Exercises

  1. OpenCV samples has an interactive sample on watershed segmentation, watershed.py. Run it, Enjoy it, then learn it.

上篇:【翻譯:OpenCV-Python教程】霍夫圓變換

下篇:【翻譯:OpenCV-Python教程】圖像金字塔

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章