Connected-component labeling turns foreground pixels in a mask into numbered objects. In SciPy, scipy.ndimage.label() counts separated regions in a binary or numeric array before measurement, filtering, or bounding-box extraction.
The default connectivity in two dimensions uses edge-touching neighbors, so diagonally touching pixels stay separate unless a different structure is supplied. A structuring element from generate_binary_structure(2, 2) switches the same mask to 8-connected labeling when diagonals should belong to the same object.
The sample mask keeps two diagonal joins visible and prints bounding slices from find_objects(). Zero remains the background in the label matrix, and positive integers identify the component that each foreground pixel belongs to.
import numpy as np from scipy import ndimage mask = np.array( [ [0, 1, 1, 0, 0, 0], [0, 0, 1, 0, 1, 0], [1, 1, 0, 0, 0, 1], [0, 0, 0, 1, 1, 1], ], dtype=bool, ) labels, component_count = ndimage.label(mask) component_slices = ndimage.find_objects(labels) diagonal_structure = ndimage.generate_binary_structure(2, 2) diagonal_labels, diagonal_count = ndimage.label( mask, structure=diagonal_structure, ) def print_grid(title, array): print(f"{title}:") for row in array.astype(int): print(" " + " ".join(str(value) for value in row)) print_grid("mask", mask) print() print_grid("default labels", labels) print(f"component_count: {component_count}") print() print("component slices:") for label_id, slices in enumerate(component_slices, start=1): row_slice, col_slice = slices pixels = np.count_nonzero(labels[slices] == label_id) print( f" label {label_id}: " f"rows {row_slice.start}:{row_slice.stop}, " f"cols {col_slice.start}:{col_slice.stop}, " f"pixels {pixels}" ) print() print_grid("8-connected labels", diagonal_labels) print(f"8_connected_component_count: {diagonal_count}")
ndimage.label() treats any nonzero input value as foreground. Convert a grayscale or probability mask to a Boolean mask first when intensity values should not affect object membership.
$ python image_components_label.py mask: 0 1 1 0 0 0 0 0 1 0 1 0 1 1 0 0 0 1 0 0 0 1 1 1 default labels: 0 1 1 0 0 0 0 0 1 0 2 0 3 3 0 0 0 4 0 0 0 4 4 4 component_count: 4 component slices: label 1: rows 0:2, cols 1:3, pixels 3 label 2: rows 1:2, cols 4:5, pixels 1 label 3: rows 2:3, cols 0:2, pixels 2 label 4: rows 2:4, cols 3:6, pixels 4 8-connected labels: 0 1 1 0 0 0 0 0 1 0 2 0 1 1 0 0 0 2 0 0 0 2 2 2 8_connected_component_count: 2
labels, component_count = ndimage.label(mask)
For a 2-D input, the default structure connects only edge-touching foreground pixels. Diagonal-only contact does not merge components.
component_slices = ndimage.find_objects(labels)
find_objects() ignores label 0 and returns label 1 at list index 0, label 2 at list index 1, and so on.
diagonal_structure = ndimage.generate_binary_structure(2, 2) diagonal_labels, diagonal_count = ndimage.label( mask, structure=diagonal_structure, )
The sample mask drops from four components to two because the diagonal neighbors join under 8-connected labeling.
$ rm image_components_label.py