【翻译: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教程】图像金字塔

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