11.1. 图像分割#

图像分割是对图像中感兴趣物体的像素进行标记的任务。

在本教程中,我们将学习如何从背景中分割物体。我们使用来自 skimage.data.coins() 的图像。这张图像显示了几枚硬币在较暗的背景下被勾勒出来。由于背景与硬币共享足够的灰度级别,因此无法直接通过灰度值直方图进行分割,阈值分割不足以完成这项任务。

../_images/sphx_glr_plot_coins_segmentation_001.png
>>> from skimage.exposure import histogram
>>> coins = ski.data.coins()
>>> hist, hist_centers = ski.exposure.histogram(coins)

简单地对图像进行阈值处理会导致要么遗漏硬币的重要部分,要么将背景的部分与硬币合并。这是由于图像的光照不均匀造成的。

../_images/sphx_glr_plot_coins_segmentation_002.png

第一个想法是利用局部对比度,也就是说,使用梯度而不是灰度值。

11.1.1. 基于边缘的分割#

首先,我们尝试检测包围硬币的边缘。对于边缘检测,我们使用 skimage.feature.canny()Canny 检测器

>>> edges = ski.feature.canny(coins / 255.)

由于背景非常平滑,几乎所有的边缘都位于硬币的边界上,或在硬币内部。

>>> import scipy as sp
>>> fill_coins = sp.ndimage.binary_fill_holes(edges)
../_images/sphx_glr_plot_coins_segmentation_003.png

既然我们已经有了描绘硬币外部边界的轮廓,我们使用 scipy.ndimage.binary_fill_holes() 函数来填充硬币的内部部分,该函数使用数学形态学来填充孔洞。

../_images/sphx_glr_plot_coins_segmentation_004.png

大多数硬币都能很好地从背景中分割出来。可以使用 ndi.label 函数轻松移除背景中的小物体,通过移除小于某个小阈值的物体。

>>> label_objects, nb_labels = sp.ndimage.label(fill_coins)
>>> sizes = np.bincount(label_objects.ravel())
>>> mask_sizes = sizes > 20
>>> mask_sizes[0] = 0
>>> coins_cleaned = mask_sizes[label_objects]

然而,分割效果并不令人满意,因为其中一枚硬币根本没有被正确分割。原因是,我们从Canny检测器得到的轮廓没有完全闭合,因此填充函数没有填充硬币的内部部分。

../_images/sphx_glr_plot_coins_segmentation_005.png

因此,这种分割方法不是很稳健:如果我们错过物体轮廓的一个像素,我们将无法填充它。当然,我们可以尝试膨胀轮廓以闭合它们。然而,尝试一种更稳健的方法是更好的选择。

11.1.2. 基于区域的分割#

首先,我们来确定硬币和背景的标记。这些标记是我们可以明确标记为物体或背景的像素。在这里,标记位于灰度值直方图的两个极端部分:

>>> markers = np.zeros_like(coins)
>>> markers[coins < 30] = 1
>>> markers[coins > 150] = 2

我们将在分水岭分割中使用这些标记。分水岭这个名字来源于水文学的类比。分水岭变换 从一个标记开始淹没高程图像,以确定这些标记的集水盆地。分水岭线分隔这些集水盆地,并对应于所需的分割。

选择高程图对于良好的分割至关重要。这里,梯度的幅度提供了一个良好的高程图。我们使用Sobel算子来计算梯度的幅度:

>>> elevation_map = ski.filters.sobel(coins)

从下图所示的3-D表面图中,我们可以看到高屏障有效地将硬币与背景分隔开来。

../_images/elevation_map.jpg

这里是相应的二维图:

../_images/sphx_glr_plot_coins_segmentation_006.png

下一步是根据灰度值直方图的极端部分找到背景和硬币的标记:

>>> markers = np.zeros_like(coins)
>>> markers[coins < 30] = 1
>>> markers[coins > 150] = 2
../_images/sphx_glr_plot_coins_segmentation_007.png

现在让我们计算分水岭变换:

>>> segmentation = ski.segmentation.watershed(elevation_map, markers)
../_images/sphx_glr_plot_coins_segmentation_008.png

通过这种方法,所有硬币的结果都令人满意。即使背景的标记分布不均匀,高程图中的障碍物也足够高,使得这些标记能够淹没整个背景。

我们使用数学形态学去除一些小孔:

>>> segmentation = sp.ndimage.binary_fill_holes(segmentation - 1)

我们现在可以使用 ndi.label 逐一标记所有硬币:

>>> labeled_coins, _ = sp.ndimage.label(segmentation)
../_images/sphx_glr_plot_coins_segmentation_009.png