Connected component labeling for arrays / quasi-images with many dimension

Viewed 1136

Problem

I am trying to do connected component labling for arrays of more than 3 dimensions. What I mean by that is that my boolean array has a .shape e.g. like (5,2,3,6,10) which would be 5 dimensions.

For 2D images (instead of my >3D problem), connected component labling would is putting labels to connected areas (hyper-volumes in my case). Two (hpyer-)pixels are connected if the are next to each other and both are True in the boolean array.

enter image description here enter image description here

What I already tried

For 2 dimensions this can be done with OpenCV and with up to 3 dimensions this can be done with scikit-image's skimage.measure.label. However, I am not sure how to it for my case.


Further material for the interested reader (but it does not help my question):

2 Answers

If what would be a 4-connectivity in 2D is enough, you can get the neighbouring pixels that are also foreground in n log n time using a nearest neighbour tree. Then it is matter of constructing the graph and finding the connected components (also n log n, IIRC).

#!/usr/bin/env python
"""
https://stackoverflow.com/questions/66724201/connected-component-labling-for-arrays-quasi-images-with-many-dimension
"""
import numpy as np
import networkx as nx

from scipy.spatial import cKDTree


def get_components(boolean_array):
    # find neighbours
    coordinates = list(zip(*np.where(boolean_array)))
    tree = cKDTree(coordinates)
    neighbours_by_pixel = tree.query_ball_tree(tree, r=1, p=1) # p=1 -> Manhatten distance; r=1 -> what would be 4-connectivity in 2D

    # create graph and find components
    G = nx.Graph()
    for ii, neighbours in enumerate(neighbours_by_pixel):
        if len(neighbours) > 1:
            G.add_edges_from([(ii, jj) for jj in neighbours[1:]]) # skip first neighbour as that is a self-loop
    components = nx.connected_components(G)

    # create output image
    output = np.zeros_like(data, dtype=np.int)
    for ii, component in enumerate(components):
        for idx in component:
            output[coordinates[idx]] = ii+1

    return output


if __name__ == '__main__':

    shape = (5, 2, 3, 6, 10)
    D = len(shape)
    data = np.random.rand(*shape) < 0.1
    output = get_components(data)

For an array with shape (50, 50, 50, 50), I get the following timings on my laptop:

In [48]: %timeit output = get_components(data)
5.85 s ± 279 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

scipy.ndimage.label does what you want directly:

In [1]: import numpy as np
In [2]: arr = np.random.random((5,2,3,6,10)) > 0.5
In [3]: from scipy import ndimage as ndi
In [4]: labeled, n = ndi.label(arr)
In [5]: n
Out[5]: 11
Related