在二进制图像中查找连接斑点坐标的有效方法

Efficient way to find coordinates of connected blobs in binary image

我正在寻找二进制图像(0 或 1 的 2d numpy 数组)中连接的斑点的坐标。

skimage 库提供了一种非常快速的方法来标记数组中的斑点(这是我从类似的 SO 帖子中找到的)。但是我想要一个 blob 的坐标列表,而不是一个带标签的数组。我有一个从标记图像中提取坐标的解决方案。但它很慢。比初始标签慢得多。

最小可重现示例:

import timeit
from skimage import measure
import numpy as np

binary_image = np.array([
        [0,1,0,0,1,1,0,1,1,0,0,1],
        [0,1,0,1,1,1,0,1,1,1,0,1],
        [0,0,0,0,0,0,0,1,1,1,0,0],
        [0,1,1,1,1,0,0,0,0,1,0,0],
        [0,0,0,0,0,0,0,1,1,1,0,0],
        [0,0,1,0,0,0,0,0,0,0,0,0],
        [0,1,0,0,1,1,0,1,1,0,0,1],
        [0,0,0,0,0,0,0,1,1,1,0,0],
        [0,1,1,1,1,0,0,0,0,1,0,0],
        ])

print(f"\n\n2d array of type: {type(binary_image)}:")
print(binary_image)

labels = measure.label(binary_image)

print(f"\n\n2d array with connected blobs labelled of type {type(labels)}:")
print(labels)

def extract_blobs_from_labelled_array(labelled_array):
    # The goal is to obtain lists of the coordinates
    # Of each distinct blob.

    blobs = []

    label = 1
    while True:
        indices_of_label = np.where(labelled_array==label)
        if not indices_of_label[0].size > 0:
            break
        else:
            blob =list(zip(*indices_of_label))
            label+=1
            blobs.append(blob)


if __name__ == "__main__":
    print("\n\nBeginning extract_blobs_from_labelled_array timing\n")
    print("Time taken:")
    print(
        timeit.timeit(
            'extract_blobs_from_labelled_array(labels)', 
            globals=globals(),
            number=1
            )
        )
    print("\n\n")

输出:

2d array of type: <class 'numpy.ndarray'>:
[[0 1 0 0 1 1 0 1 1 0 0 1]
 [0 1 0 1 1 1 0 1 1 1 0 1]
 [0 0 0 0 0 0 0 1 1 1 0 0]
 [0 1 1 1 1 0 0 0 0 1 0 0]
 [0 0 0 0 0 0 0 1 1 1 0 0]
 [0 0 1 0 0 0 0 0 0 0 0 0]
 [0 1 0 0 1 1 0 1 1 0 0 1]
 [0 0 0 0 0 0 0 1 1 1 0 0]
 [0 1 1 1 1 0 0 0 0 1 0 0]]


2d array with connected blobs labelled of type <class 'numpy.ndarray'>:
[[ 0  1  0  0  2  2  0  3  3  0  0  4]
 [ 0  1  0  2  2  2  0  3  3  3  0  4]
 [ 0  0  0  0  0  0  0  3  3  3  0  0]
 [ 0  5  5  5  5  0  0  0  0  3  0  0]
 [ 0  0  0  0  0  0  0  3  3  3  0  0]
 [ 0  0  6  0  0  0  0  0  0  0  0  0]
 [ 0  6  0  0  7  7  0  8  8  0  0  9]
 [ 0  0  0  0  0  0  0  8  8  8  0  0]
 [ 0 10 10 10 10  0  0  0  0  8  0  0]]


Beginning extract_blobs_from_labelled_array timing

Time taken:
9.346099977847189e-05

9e-05 很小,但示例图像也很小。实际上,我正在处理非常高分辨率的图像,该函数大约需要 10 分钟。

有更快的方法吗?

旁注:我只使用 list(zip()) 来尝试将 numpy 坐标转换为我习惯的东西(我不怎么使用 numpy,只是 Python)。我应该跳过这个并只使用坐标按原样索引吗?这会加快速度吗?

慢的部分代码在这里:

    while True:
        indices_of_label = np.where(labelled_array==label)
        if not indices_of_label[0].size > 0:
            break
        else:
            blob =list(zip(*indices_of_label))
            label+=1
            blobs.append(blob)

首先,一个完整的旁白:当您知道要迭代的元素数量时,您应该避免使用 while True。这是难以发现的无限循环错误的秘诀。

相反,您应该使用:

    for label in range(np.max(labels)):

然后你可以忽略if ...: break

第二个问题确实是您正在使用 list(zip(*)),与 NumPy 函数相比速度较慢。在这里你可以得到与 np.transpose(indices_of_label) 大致相同的结果,这将得到一个形状为 (n_coords, n_dim) 的二维数组,即 (n_coords, 2).

但最大的问题是表达式 labelled_array == label。这将为每个标签检查图像的每个像素一次。 (实际上是两次,因为那时你 运行 np.where(),它需要另一遍。)这是很多不必要的工作,因为坐标可以在一次遍中找到。

scikit-image 函数 skimage.measure.regionprops 可以为您做到这一点。 regionprops 检查图像一次,returns 每个标签包含一个 RegionProps 对象的列表。该对象有一个 .coords 属性,其中包含 blob 中每个像素的坐标。因此,这是您的代码,已修改为使用该功能:

import timeit
from skimage import measure
import numpy as np

binary_image = np.array([
        [0,1,0,0,1,1,0,1,1,0,0,1],
        [0,1,0,1,1,1,0,1,1,1,0,1],
        [0,0,0,0,0,0,0,1,1,1,0,0],
        [0,1,1,1,1,0,0,0,0,1,0,0],
        [0,0,0,0,0,0,0,1,1,1,0,0],
        [0,0,1,0,0,0,0,0,0,0,0,0],
        [0,1,0,0,1,1,0,1,1,0,0,1],
        [0,0,0,0,0,0,0,1,1,1,0,0],
        [0,1,1,1,1,0,0,0,0,1,0,0],
        ])

print(f"\n\n2d array of type: {type(binary_image)}:")
print(binary_image)

labels = measure.label(binary_image)

print(f"\n\n2d array with connected blobs labelled of type {type(labels)}:")
print(labels)

def extract_blobs_from_labelled_array(labelled_array):
    """Return a list containing coordinates of pixels in each blob."""
    props = measure.regionprops(labelled_array)
    blobs = [p.coords for p in props]
    return blobs


if __name__ == "__main__":
    print("\n\nBeginning extract_blobs_from_labelled_array timing\n")
    print("Time taken:")
    print(
        timeit.timeit(
            'extract_blobs_from_labelled_array(labels)', 
            globals=globals(),
            number=1
            )
        )
    print("\n\n")