11.1. 图像分割#
图像分割的任务是标记图像中感兴趣对象的像素。
在本教程中,我们将了解如何从背景中分割对象。我们使用来自 skimage.data.coins()
的图像。此图像显示了几个在较暗背景上勾勒出的硬币。硬币的分割不能直接从灰度值直方图进行,因为背景与硬币共享足够的灰度级,以至于阈值分割是不够的。
>>> from skimage.exposure import histogram
>>> coins = ski.data.coins()
>>> hist, hist_centers = ski.exposure.histogram(coins)
简单地对图像进行阈值处理会导致要么丢失硬币的显着部分,要么将背景的一部分与硬币合并。这是由于图像的照明不均匀造成的。
一个想法是利用局部对比度,即使用梯度而不是灰度值。
11.1.1. 基于边缘的分割#
让我们首先尝试检测包围硬币的边缘。对于边缘检测,我们使用 Canny 检测器,它来自 skimage.feature.canny()
>>> edges = ski.feature.canny(coins / 255.)
由于背景非常光滑,几乎所有边缘都在硬币的边界处或硬币内部找到。
>>> import scipy as sp
>>> fill_coins = sp.ndimage.binary_fill_holes(edges)
现在我们有了勾勒出硬币外围边界的轮廓,我们使用 scipy.ndimage.binary_fill_holes()
函数填充硬币的内部部分,该函数使用数学形态学来填充孔洞。
大多数硬币都从背景中很好地分割出来。来自背景的小物体可以使用 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 检测器得到的轮廓没有完全闭合,因此填充函数没有填充硬币的内部部分。
因此,这种分割方法并不十分稳健:如果我们错过了对象的轮廓的一个像素,我们就无法填充它。当然,我们可以尝试膨胀轮廓以闭合它们。但是,最好尝试更稳健的方法。
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 表面图中,我们看到高障碍物有效地将硬币与背景隔开。
这是相应的 2-D 图
下一步是根据灰度值直方图的极端部分找到背景和硬币的标记
>>> markers = np.zeros_like(coins)
>>> markers[coins < 30] = 1
>>> markers[coins > 150] = 2
现在让我们计算分水岭变换
>>> segmentation = ski.segmentation.watershed(elevation_map, markers)
使用这种方法,结果对于所有硬币都令人满意。即使背景的标记没有很好地分布,高程图中的障碍物也足够高,以至于这些标记可以淹没整个背景。
我们使用数学形态学移除一些小孔
>>> segmentation = sp.ndimage.binary_fill_holes(segmentation - 1)
现在我们可以使用 ndi.label
一次一个地标记所有硬币
>>> labeled_coins, _ = sp.ndimage.label(segmentation)