Coverage for src/abm_shape_collection/extract_voxel_contours.py: 100%
59 statements
« prev ^ index » next coverage.py v7.1.0, created at 2024-09-25 19:34 +0000
« prev ^ index » next coverage.py v7.1.0, created at 2024-09-25 19:34 +0000
1import numpy as np
4def extract_voxel_contours(
5 all_voxels: list[tuple[int, int, int]],
6 projection: str,
7 box: tuple[int, int, int],
8) -> list[list[tuple[int, int]]]:
9 """
10 Extract contours from list of voxels in specified projection direction.
12 Parameters
13 ----------
14 all_voxels
15 List of all voxels in region.
16 projection
17 Projection direction.
18 box
19 Bounding box.
21 Returns
22 -------
23 :
24 List of list of contour points.
25 """
27 voxels = set()
28 length, width, height = box
30 if projection == "top":
31 voxels.update({(x, y) for x, y, _ in all_voxels})
32 x_bounds = length
33 y_bounds = width
34 elif projection == "side1":
35 voxels.update({(x, z) for x, _, z in all_voxels})
36 x_bounds = length
37 y_bounds = height
38 else:
39 voxels.update({(y, z) for _, y, z in all_voxels})
40 x_bounds = width
41 y_bounds = height
43 array = np.full((x_bounds, y_bounds), fill_value=False)
44 array[tuple(np.transpose(list(voxels)))] = True
46 edges = get_array_edges(array)
47 contours = connect_array_edges(edges)
49 return [merge_contour_edges(contour) for contour in contours]
52def get_array_edges(array: np.ndarray) -> list[list[tuple[int, int]]]:
53 """
54 Get edges of region in binary array.
56 Parameters
57 ----------
58 array
59 Binary array.
61 Returns
62 -------
63 :
64 List of unconnected region edges.
65 """
67 edges = []
68 x, y = np.nonzero(array)
70 for i, j in zip(x.tolist(), y.tolist()):
71 if j == array.shape[1] - 1 or not array[i, j + 1]:
72 edges.append([(i, j + 1), (i + 1, j + 1)])
74 if i == array.shape[0] - 1 or not array[i + 1, j]:
75 edges.append([(i + 1, j), (i + 1, j + 1)])
77 if j == 0 or not array[i, j - 1]:
78 edges.append([(i, j), (i + 1, j)])
80 if i == 0 or not array[i - 1, j]:
81 edges.append([(i, j), (i, j + 1)])
83 return edges
86def connect_array_edges(edges: list[list[tuple[int, int]]]) -> list[list[tuple[int, int]]]:
87 """
88 Group unconnected region edges into connected contour edges.
90 Parameters
91 ----------
92 edges
93 List of unconnected region edges.
95 Returns
96 -------
97 :
98 List of connected contour edges.
99 """
101 contours: list[list[tuple[int, int]]] = []
103 while edges:
104 contour = edges[0]
105 contour_length = 0
106 edges.remove(contour)
108 while contour_length != len(contour):
109 contour_length = len(contour)
111 forward = list(filter(lambda edge: contour[-1] == edge[0], edges))
113 if len(forward) > 0:
114 edges.remove(forward[0])
115 contour.extend(forward[0][1:])
117 backward = list(filter(lambda edge: contour[-1] == edge[-1], edges))
119 if len(backward) > 0:
120 edges.remove(backward[0])
121 contour.extend(list(reversed(backward[0]))[1:])
123 if contour_length == len(contour):
124 contours.append(list(contour))
126 return sorted(contours, key=len)
129def merge_contour_edges(contour: list[tuple[int, int]]) -> list[tuple[int, int]]:
130 """
131 Merge connected contour edges.
133 Parameters
134 ----------
135 contour
136 List of connected contour edges.
138 Returns
139 -------
140 :
141 List of connected contours.
142 """
144 merged = contour.copy()
146 for (x0, y0), (x1, y1), (x2, y2) in zip(contour, contour[1:], contour[2:]):
147 area = x0 * (y1 - y2) + x1 * (y2 - y0) + x2 * (y0 - y1)
148 if area == 0:
149 merged.remove((x1, y1))
151 return merged