-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdrawing_util.py
More file actions
236 lines (193 loc) · 7.23 KB
/
drawing_util.py
File metadata and controls
236 lines (193 loc) · 7.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
import numpy as np
def rgb_to_hex(rgb_color):
"""
Converts a rgb color to hex color
Args:
rgb_color (tuple)
Returns:
hex_color (str)
"""
return "#{:02x}{:02x}{:02x}".format(*rgb_color)
def mask_shifting(mask_data,deleted_mask_id:int,slice_id:int|None = None):
"""
Shifts the mask when a mask got deleted to restore an order without gaps.
Args:
mask_data (np.array): the mask data.
deleted_mask_id (int): the id of the deleted mask.
slice_id (int): the id of the slice when the mask is 3d.
Raises:
ValueError: if the deleted_mask_id is smaller or equal to 0.
"""
if deleted_mask_id < 1:
raise ValueError("deleted_mask_id must be greater than 0")
mask = mask_data["masks"]
outline = mask_data["outlines"]
if mask.ndim == 3:
if slice_id < 0:
raise ValueError("slice_id should be non-negative")
mask_slice = np.take(mask, slice_id, axis=0).astype(np.uint16)
mask_slice[mask_slice>deleted_mask_id] -= 1
mask[slice_id, :, :] = mask_slice
else:
mask[mask > deleted_mask_id] -= 1
if outline.ndim == 3:
if slice_id < 0:
raise ValueError("slice_id should be non-negative")
outline_slice = np.take(outline, slice_id, axis=0).astype(np.uint16)
outline_slice[outline_slice>deleted_mask_id] -= 1
outline[slice_id, :, :] = outline_slice
else:
outline[outline>deleted_mask_id] -= 1
def search_free_id(mask,outline):
"""
Search in a NumPy array of integers (e.g., [1,1,2,2,3,4,5,5,7,7]) for the first missing number (in this case, 6).
If no gap is found, return the highest value + 1.
"""
combined = np.concatenate((mask.ravel(),outline.ravel()))
unique_vals = np.unique(combined).astype(np.uint16)
if unique_vals.size == 0:
return 1
diffs = np.diff(unique_vals)
gap_index = np.where(diffs > 1)[0]
if gap_index.size > 0:
missed_value = unique_vals[gap_index[0]] + 1
else:
missed_value = unique_vals[-1] + 1
return missed_value
def bresenham_line(start: tuple, end: tuple):
"""
Calculates all pixel coordinates along a line from start to end using the Bresenham algorithm.
"""
x0, y0 = int(round(start[0])), int(round(start[1]))
x1, y1 = int(round(end[0])), int(round(end[1]))
pixels = []
dx = abs(x1 - x0)
dy = abs(y1 - y0)
sx = 1 if x0 < x1 else -1
sy = 1 if y0 < y1 else -1
err = dx - dy
while True:
pixels.append((x0, y0))
if x0 == x1 and y0 == y1:
break
e2 = 2 * err
if e2 > -dy:
err -= dy
x0 += sx
if e2 < dx:
err += dx
y0 += sy
return pixels
def trace_contour(binary_mask):
"""
This method traces the contours of the inputted mask and returns the outline
Attributes:
binary_mask (np.array): the binary mask.
Returns:
contour: the outline of the cells in the mask
"""
y_indices, x_indices = np.where(binary_mask)
start_idx = np.lexsort((x_indices, y_indices))[0]
start_x, start_y = x_indices[start_idx], y_indices[start_idx]
directions = [(0, -1), (1, -1), (1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1)]
contour = []
current = (start_x, start_y)
prev = (start_x - 1, start_y)
while True:
contour.append(current)
dir_idx = directions.index((prev[0] - current[0], prev[1] - current[1]))
for i in range(8):
next_dir = (dir_idx + 1 + i) % 8
dx, dy = directions[next_dir]
nx, ny = current[0] + dx, current[1] + dy
if 0 <= ny < binary_mask.shape[0] and 0 <= nx < binary_mask.shape[1]:
if binary_mask[ny, nx]:
prev = current
current = (nx, ny)
break
if current == (start_x, start_y):
break
return contour
def fill_polygon_from_outline(contour, mask_shape):
"""
Fills the given polygon in contour with the scanline technique.
Attributes:
contour (np.array): the contour
mask_shape (np.array): the mask dimensions
Returns:
mask: the adapted mask with newly filled cells
"""
mask = np.zeros(mask_shape, dtype=np.uint16)
if not contour:
return mask
edges = []
num_vertices = len(contour)
for i in range(num_vertices):
x0, y0 = contour[i]
x1, y1 = contour[(i + 1) % num_vertices]
if y0 == y1:
continue # Skip horizontal edges
if y0 > y1:
x0, y0, x1, y1 = x1, y1, x0, y0 # Ensure y0 < y1
slope = (x1 - x0) / (y1 - y0) if (y1 - y0) != 0 else 0
edges.append((y0, y1, x0, slope))
if not edges:
return mask
global_edge_table = sorted(edges, key=lambda e: (e[0], e[2]))
scan_line = min(e[0] for e in global_edge_table)
active_edges = []
while global_edge_table or active_edges:
# Add edges starting at the current scan line
while global_edge_table and global_edge_table[0][0] == scan_line:
e = global_edge_table.pop(0)
active_edges.append([e[1], e[2], e[3]])
active_edges.sort(key=lambda e: e[1])
# Fill between pairs of edges
for i in range(0, len(active_edges), 2):
if i + 1 >= len(active_edges):
break
e1 = active_edges[i]
e2 = active_edges[i + 1]
x_start = int(np.ceil(e1[1]))
x_end = int(np.floor(e2[1]))
for x in range(x_start, x_end + 1):
if 0 <= x < mask_shape[1] and 0 <= scan_line < mask_shape[0]:
mask[scan_line, x] = 1
scan_line += 1
active_edges = [e for e in active_edges if e[0] > scan_line]
for e in active_edges:
e[1] += e[2]
return mask
def find_border_pixels(mask, outline, cell_id,):
"""
Finds edge pixels in a given mask matrix, considering the outline and only searching for pixels that match the specified cell_id.
Edge pixels are those whose neighboring pixels have a different ID or are marked as edges in the outline.
Attributes:
mask: A 2D mask representing the ID of each pixel in the area.
outline: A 2D mask marking the edge pixels (typically with a value of 100).
cell_id: The ID of the cell whose edge pixels should be found.
Returns:
border_pixels: all neighbours of the cell
"""
border_pixels = []
rows, cols = mask.shape
for y in range(rows):
for x in range(cols):
# ID of current pixel
current_id = mask[y, x]
if current_id != cell_id:
continue
# neighboring positions
neighbors = [
(y - 1, x), (y + 1, x), # up, down
(y, x - 1), (y, x + 1) # left, right
]
is_border_pixel = False
for ny, nx in neighbors:
if 0 <= ny < rows and 0 <= nx < cols:
if mask[ny, nx] != current_id and outline[ny, nx] != current_id:
is_border_pixel = True
break
if is_border_pixel:
border_pixels.append((y,x))
return border_pixels