Coverage for src/abm_colony_collection/get_neighbors_map.py: 100%
46 statements
« prev ^ index » next coverage.py v7.1.0, created at 2025-09-15 20:34 +0000
« prev ^ index » next coverage.py v7.1.0, created at 2025-09-15 20:34 +0000
1from __future__ import annotations
3from typing import Callable
5import numpy as np
6from scipy import ndimage
7from skimage import measure
10def get_neighbors_map(array: np.ndarray) -> dict:
11 """
12 Create map of region ids to lists of neighbors.
14 Each region id is also assigned a group number, where all regions in a given
15 group are simply connected.
17 Parameters
18 ----------
19 array
20 Segmentation array.
22 Returns
23 -------
24 :
25 Map of id to group and neighbor ids.
26 """
28 neighbors_map: dict = {cell_id: {} for cell_id in np.unique(array)}
29 neighbors_map.pop(0, None)
31 # Create binary mask for array.
32 mask = np.zeros(array.shape, dtype="int")
33 mask[array != 0] = 1
35 # Label connected groups.
36 labels, groups = measure.label(mask, connectivity=2, return_num=True)
38 for group in range(1, groups + 1):
39 group_crop = get_cropped_array(array, group, labels, crop_original=False)
40 voxel_ids = [i for i in np.unique(group_crop) if i != 0]
42 # Find neighbors for each voxel id.
43 for voxel_id in voxel_ids:
44 voxel_crop = get_cropped_array(group_crop, voxel_id, crop_original=True)
46 # Apply custom filter to get border locations.
47 border_mask = ndimage.generic_filter(voxel_crop, _get_voxel_id_filter(voxel_id), size=3)
49 # Find neighbors overlapping border.
50 neighbor_list = np.unique(voxel_crop[border_mask == 1])
51 neighbor_list = [i for i in neighbor_list if i not in [0, voxel_id]]
52 neighbors_map[voxel_id] = {"group": group, "neighbors": neighbor_list}
54 return neighbors_map
57def _get_voxel_id_filter(voxel_id: int) -> Callable:
58 """Create filtering lambda for given id."""
59 return lambda v: voxel_id in v
62def get_bounding_box(array: np.ndarray) -> tuple[int, int, int, int, int, int]:
63 """
64 Find bounding box around array.
66 Bounds are calculated with a one-voxel border, if possible.
68 Parameters
69 ----------
70 array
71 Segmentation array.
73 Returns
74 -------
75 :
76 The bounding box (xmin, xmax, ymin, ymax, zmin, zmax) indices
77 """
79 x, y, z = array.shape
81 xbounds = np.any(array, axis=(1, 2))
82 ybounds = np.any(array, axis=(0, 2))
83 zbounds = np.any(array, axis=(0, 1))
85 xmin, xmax = np.where(xbounds)[0][[0, -1]]
86 ymin, ymax = np.where(ybounds)[0][[0, -1]]
87 zmin, zmax = np.where(zbounds)[0][[0, -1]]
89 xmin = max(xmin - 1, 0)
90 xmax = min(xmax + 2, x)
92 ymin = max(ymin - 1, 0)
93 ymax = min(ymax + 2, y)
95 zmin = max(zmin - 1, 0)
96 zmax = min(zmax + 2, z)
98 return xmin, xmax, ymin, ymax, zmin, zmax
101def get_cropped_array(
102 array: np.ndarray, label: int, labels: np.ndarray | None = None, *, crop_original: bool
103) -> np.ndarray:
104 """
105 Crop array around label region.
107 Parameters
108 ----------
109 array
110 Array to crop.
111 label
112 Region label.
113 labels
114 Array of all region labels.
115 crop_original
116 True to crop the original array keeping all labels, False otherwise.
118 Returns
119 -------
120 :
121 Cropped array.
122 """
124 # Set all voxels not matching label to zero.
125 array_mask = array.copy()
126 array_filter = labels if labels is not None else array_mask
127 array_mask[array_filter != label] = 0
129 # Crop array to label.
130 xmin, xmax, ymin, ymax, zmin, zmax = get_bounding_box(array_mask)
132 if crop_original:
133 return array[xmin:xmax, ymin:ymax, zmin:zmax]
135 return array_mask[xmin:xmax, ymin:ymax, zmin:zmax]