import numpy as np
from scipy.ndimage import label, generate_binary_structure
from .base import volume_fraction
[docs]
def label_periodic(field, grayscale_value, neighbour_structure, periodic, debug=False):
"""Label connected components with periodic boundary conditions.
Wraps the image in periodic directions, labels connected components
equal to ``grayscale_value``, then merges labels that touch across
periodic boundaries. Finally, crops back to the original shape.
Args:
field (numpy.ndarray): Input array (2D or 3D).
grayscale_value (int | float): Target value to label.
neighbour_structure (numpy.ndarray): Structuring element as from
``scipy.ndimage.generate_binary_structure``.
periodic (Sequence[bool]): Periodicity flags per axis (e.g. ``(True, False, True)``).
debug (bool, optional): Print simple diagnostics. Defaults to ``False``.
Returns:
tuple[numpy.ndarray, int]: Tuple ``(labels, num_labels)`` where ``labels`` is the
cropped labeled array and ``num_labels`` is the number of connected components
after periodic merging.
"""
padx = int(periodic[0])
pady = int(periodic[1])
padz = int(periodic[2])
mask = np.pad(field, ((padx, padx), (pady, pady), (padz, padz)), mode='wrap')
labeled_mask, num_labels = label(mask==grayscale_value, structure=neighbour_structure)
count = 1
for k in range(100):
# Find indices where labels are different at the boundaries and create swaplist
swap_list = np.zeros((1,2))
if periodic[0]:
# right x
indices = np.where((labeled_mask[0,:,:]!=labeled_mask[-2,:,:]) & (labeled_mask[0,:,:]!=0) & (labeled_mask[-2,:,:]!=0))
additional_swaps = np.column_stack((labeled_mask[0,:,:][indices], labeled_mask[-2,:,:][indices]))
swap_list = np.vstack((swap_list,additional_swaps))
# left x
indices = np.where((labeled_mask[1,:,:]!=labeled_mask[-1,:,:]) & (labeled_mask[1,:,:]!=0) & (labeled_mask[-1,:,:]!=0))
additional_swaps = np.column_stack((labeled_mask[1,:,:][indices], labeled_mask[-1,:,:][indices]))
swap_list = np.vstack((swap_list,additional_swaps))
if periodic[1]:
# top y
indices = np.where((labeled_mask[:,0,:]!=labeled_mask[:,-2,:]) & (labeled_mask[:,0,:]!=0) & (labeled_mask[:,-2,:]!=0))
additional_swaps = np.column_stack((labeled_mask[:,0,:][indices], labeled_mask[:,-2,:][indices]))
swap_list = np.vstack((swap_list,additional_swaps))
# bottom y
indices = np.where((labeled_mask[:,1,:]!=labeled_mask[:,-1,:]) & (labeled_mask[:,1,:]!=0) & (labeled_mask[:,-1,:]!=0))
additional_swaps = np.column_stack((labeled_mask[:,1,:][indices], labeled_mask[:,-1,:][indices]))
swap_list = np.vstack((swap_list,additional_swaps))
if periodic[2]:
# front z
indices = np.where((labeled_mask[:,:,0]!=labeled_mask[:,:,-2]) & (labeled_mask[:,:,0]!=0) & (labeled_mask[:,:,-2]!=0))
additional_swaps = np.column_stack((labeled_mask[:,:,0][indices], labeled_mask[:,:,-2][indices]))
swap_list = np.vstack((swap_list,additional_swaps))
# back z
indices = np.where((labeled_mask[:,:,1]!=labeled_mask[:,:,-1]) & (labeled_mask[:,:,1]!=0) & (labeled_mask[:,:,-1]!=0))
additional_swaps = np.column_stack((labeled_mask[:,:,1][indices], labeled_mask[:,:,-1][indices]))
swap_list = np.vstack((swap_list,additional_swaps))
swap_list = swap_list[1:,:]
# Sort swap list columns to ensure consistent ordering
swap_list = np.sort(swap_list, axis=1)
# Remove duplicates from swap_list
swap_list = np.unique(swap_list, axis=0)
# print(f"swap_list contains {swap_list.shape[0]} elements.")
if (swap_list.shape[0]==0):
break
for i in range(swap_list.shape[0]):
index = swap_list.shape[0] - i -1
labeled_mask[labeled_mask == swap_list[index][1]] = swap_list[index][0]
count += 1
if(debug):
print(f"Did {count} iterations for periodic labelling.")
dim = labeled_mask.shape
return labeled_mask[padx:dim[0]-padx,pady:dim[1]-pady,padz:dim[2]-padz], np.unique(labeled_mask).size-1
[docs]
def find_spanning_labels(labelled_array, axis):
"""Find labels that span the domain along an axis.
A label is considered spanning if it appears on both opposing faces
along the specified axis; background label ``0`` is ignored.
Args:
labelled_array (numpy.ndarray): Labeled 3D array.
axis (str): One of ``'x'``, ``'y'``, or ``'z'``.
Returns:
set[int]: Set of labels that appear on both faces along ``axis``.
Raises:
ValueError: If ``axis`` is not one of ``'x'``, ``'y'``, ``'z'``.
"""
if axis == "x":
front = np.s_[0,:,:]
end = np.s_[-1,:,:]
elif axis == "y":
front = np.s_[:,0,:]
end = np.s_[:,-1,:]
elif axis == "z":
front = np.s_[:,:,0]
end = np.s_[:,:,-1]
else:
raise ValueError("Axis should be x, y or z!")
first_slice_labels = np.unique(labelled_array[front])
last_slice_labels = np.unique(labelled_array[end])
spanning_labels = set(first_slice_labels) & set(last_slice_labels)
spanning_labels.discard(0) # Remove the background label if it exists
return spanning_labels
[docs]
def find_front_labels(labelled_array, axis):
"""Find features that are connected to the front of given axis
Returns:
set: Labels that appear in the first slice of the given axis.
"""
if axis == "x":
front = np.s_[0,:,:]
elif axis == "y":
front = np.s_[:,0,:]
elif axis == "z":
front = np.s_[:,:,0]
else:
raise ValueError("Axis should be x, y or z!")
first_slice_labels = set(np.unique(labelled_array[front]))
first_slice_labels.discard(0) # Remove the background label if it exists
return first_slice_labels