spacr 0.1.64__py3-none-any.whl → 0.1.75__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spacr/app_annotate.py +39 -524
- spacr/app_make_masks.py +30 -904
- spacr/core.py +21 -21
- spacr/deep_spacr.py +6 -6
- spacr/gui.py +4 -20
- spacr/gui_core.py +108 -123
- spacr/gui_elements.py +1180 -12
- spacr/gui_utils.py +197 -10
- spacr/gui_wrappers.py +27 -15
- spacr/measure.py +4 -4
- spacr/settings.py +331 -260
- {spacr-0.1.64.dist-info → spacr-0.1.75.dist-info}/METADATA +1 -1
- {spacr-0.1.64.dist-info → spacr-0.1.75.dist-info}/RECORD +17 -17
- {spacr-0.1.64.dist-info → spacr-0.1.75.dist-info}/LICENSE +0 -0
- {spacr-0.1.64.dist-info → spacr-0.1.75.dist-info}/WHEEL +0 -0
- {spacr-0.1.64.dist-info → spacr-0.1.75.dist-info}/entry_points.txt +0 -0
- {spacr-0.1.64.dist-info → spacr-0.1.75.dist-info}/top_level.txt +0 -0
spacr/app_make_masks.py
CHANGED
@@ -1,925 +1,51 @@
|
|
1
|
-
import os
|
2
|
-
import numpy as np
|
3
|
-
import tkinter as tk
|
4
|
-
import imageio.v2 as imageio
|
5
|
-
from collections import deque
|
6
|
-
from PIL import Image, ImageTk
|
7
|
-
from skimage.draw import polygon, line
|
8
|
-
from skimage.transform import resize
|
9
|
-
from scipy.ndimage import binary_fill_holes, label
|
10
1
|
import tkinter as tk
|
11
2
|
from tkinter import ttk
|
3
|
+
from .gui import MainApp
|
12
4
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
self.folder_path = folder_path
|
20
|
-
self.scale_factor = scale_factor
|
21
|
-
self.image_filenames = sorted([f for f in os.listdir(folder_path) if f.endswith(('.png', '.jpg', '.jpeg', '.tif', '.tiff'))])
|
22
|
-
self.masks_folder = os.path.join(folder_path, 'masks')
|
23
|
-
self.current_image_index = 0
|
24
|
-
self.initialize_flags()
|
25
|
-
self.canvas_width = self.root.winfo_screenheight() -100
|
26
|
-
self.canvas_height = self.root.winfo_screenheight() -100
|
27
|
-
self.root.configure(bg='black')
|
28
|
-
self.setup_navigation_toolbar()
|
29
|
-
self.setup_mode_toolbar()
|
30
|
-
self.setup_function_toolbar()
|
31
|
-
self.setup_zoom_toolbar()
|
32
|
-
self.setup_canvas()
|
33
|
-
self.load_first_image()
|
34
|
-
|
35
|
-
####################################################################################################
|
36
|
-
# Helper functions#
|
37
|
-
####################################################################################################
|
38
|
-
|
39
|
-
def update_display(self):
|
40
|
-
if self.zoom_active:
|
41
|
-
self.display_zoomed_image()
|
42
|
-
else:
|
43
|
-
self.display_image()
|
44
|
-
|
45
|
-
def update_original_mask_from_zoom(self):
|
46
|
-
y0, y1, x0, x1 = self.zoom_y0, self.zoom_y1, self.zoom_x0, self.zoom_x1
|
47
|
-
zoomed_mask_resized = resize(self.zoom_mask, (y1 - y0, x1 - x0), order=0, preserve_range=True).astype(np.uint8)
|
48
|
-
self.mask[y0:y1, x0:x1] = zoomed_mask_resized
|
49
|
-
|
50
|
-
def update_original_mask(self, zoomed_mask, x0, x1, y0, y1):
|
51
|
-
actual_mask_region = self.mask[y0:y1, x0:x1]
|
52
|
-
target_shape = actual_mask_region.shape
|
53
|
-
resized_mask = resize(zoomed_mask, target_shape, order=0, preserve_range=True).astype(np.uint8)
|
54
|
-
if resized_mask.shape != actual_mask_region.shape:
|
55
|
-
raise ValueError(f"Shape mismatch: resized_mask {resized_mask.shape}, actual_mask_region {actual_mask_region.shape}")
|
56
|
-
self.mask[y0:y1, x0:x1] = np.maximum(actual_mask_region, resized_mask)
|
57
|
-
self.mask = self.mask.copy()
|
58
|
-
self.mask[y0:y1, x0:x1] = np.maximum(self.mask[y0:y1, x0:x1], resized_mask)
|
59
|
-
self.mask = self.mask.copy()
|
60
|
-
|
61
|
-
def get_scaling_factors(self, img_width, img_height, canvas_width, canvas_height):
|
62
|
-
x_scale = img_width / canvas_width
|
63
|
-
y_scale = img_height / canvas_height
|
64
|
-
return x_scale, y_scale
|
65
|
-
|
66
|
-
def canvas_to_image(self, x_canvas, y_canvas):
|
67
|
-
x_scale, y_scale = self.get_scaling_factors(
|
68
|
-
self.image.shape[1], self.image.shape[0],
|
69
|
-
self.canvas_width, self.canvas_height
|
70
|
-
)
|
71
|
-
x_image = int(x_canvas * x_scale)
|
72
|
-
y_image = int(y_canvas * y_scale)
|
73
|
-
return x_image, y_image
|
74
|
-
|
75
|
-
def apply_zoom_on_enter(self, event):
|
76
|
-
if self.zoom_active and self.zoom_rectangle_start is not None:
|
77
|
-
self.set_zoom_rectangle_end(event)
|
78
|
-
|
79
|
-
def normalize_image(self, image, lower_quantile, upper_quantile):
|
80
|
-
lower_bound = np.percentile(image, lower_quantile)
|
81
|
-
upper_bound = np.percentile(image, upper_quantile)
|
82
|
-
normalized = np.clip(image, lower_bound, upper_bound)
|
83
|
-
normalized = (normalized - lower_bound) / (upper_bound - lower_bound)
|
84
|
-
max_value = np.iinfo(image.dtype).max
|
85
|
-
normalized = (normalized * max_value).astype(image.dtype)
|
86
|
-
return normalized
|
87
|
-
|
88
|
-
def resize_arrays(self, img, mask):
|
89
|
-
original_dtype = img.dtype
|
90
|
-
scaled_height = int(img.shape[0] * self.scale_factor)
|
91
|
-
scaled_width = int(img.shape[1] * self.scale_factor)
|
92
|
-
scaled_img = resize(img, (scaled_height, scaled_width), anti_aliasing=True, preserve_range=True)
|
93
|
-
scaled_mask = resize(mask, (scaled_height, scaled_width), order=0, anti_aliasing=False, preserve_range=True)
|
94
|
-
stretched_img = resize(scaled_img, (self.canvas_height, self.canvas_width), anti_aliasing=True, preserve_range=True)
|
95
|
-
stretched_mask = resize(scaled_mask, (self.canvas_height, self.canvas_width), order=0, anti_aliasing=False, preserve_range=True)
|
96
|
-
return stretched_img.astype(original_dtype), stretched_mask.astype(original_dtype)
|
97
|
-
|
98
|
-
####################################################################################################
|
99
|
-
#Initiate canvas elements#
|
100
|
-
####################################################################################################
|
101
|
-
|
102
|
-
def load_first_image(self):
|
103
|
-
self.image, self.mask = self.load_image_and_mask(self.current_image_index)
|
104
|
-
self.original_size = self.image.shape
|
105
|
-
self.image, self.mask = self.resize_arrays(self.image, self.mask)
|
106
|
-
self.display_image()
|
107
|
-
|
108
|
-
def setup_canvas(self):
|
109
|
-
self.canvas = tk.Canvas(self.root, width=self.canvas_width, height=self.canvas_height, bg='black')
|
110
|
-
self.canvas.pack()
|
111
|
-
self.canvas.bind("<Motion>", self.update_mouse_info)
|
112
|
-
|
113
|
-
def initialize_flags(self):
|
114
|
-
self.zoom_rectangle_start = None
|
115
|
-
self.zoom_rectangle_end = None
|
116
|
-
self.zoom_rectangle_id = None
|
117
|
-
self.zoom_x0 = None
|
118
|
-
self.zoom_y0 = None
|
119
|
-
self.zoom_x1 = None
|
120
|
-
self.zoom_y1 = None
|
121
|
-
self.zoom_mask = None
|
122
|
-
self.zoom_image = None
|
123
|
-
self.zoom_image_orig = None
|
124
|
-
self.zoom_scale = 1
|
125
|
-
self.drawing = False
|
126
|
-
self.zoom_active = False
|
127
|
-
self.magic_wand_active = False
|
128
|
-
self.brush_active = False
|
129
|
-
self.dividing_line_active = False
|
130
|
-
self.dividing_line_coords = []
|
131
|
-
self.current_dividing_line = None
|
132
|
-
self.lower_quantile = tk.StringVar(value="1.0")
|
133
|
-
self.upper_quantile = tk.StringVar(value="99.9")
|
134
|
-
self.magic_wand_tolerance = tk.StringVar(value="1000")
|
135
|
-
|
136
|
-
def update_mouse_info(self, event):
|
137
|
-
x, y = event.x, event.y
|
138
|
-
intensity = "N/A"
|
139
|
-
mask_value = "N/A"
|
140
|
-
pixel_count = "N/A"
|
141
|
-
if self.zoom_active:
|
142
|
-
if 0 <= x < self.canvas_width and 0 <= y < self.canvas_height:
|
143
|
-
intensity = self.zoom_image_orig[y, x] if self.zoom_image_orig is not None else "N/A"
|
144
|
-
mask_value = self.zoom_mask[y, x] if self.zoom_mask is not None else "N/A"
|
145
|
-
else:
|
146
|
-
if 0 <= x < self.image.shape[1] and 0 <= y < self.image.shape[0]:
|
147
|
-
intensity = self.image[y, x]
|
148
|
-
mask_value = self.mask[y, x]
|
149
|
-
if mask_value != "N/A" and mask_value != 0:
|
150
|
-
pixel_count = np.sum(self.mask == mask_value)
|
151
|
-
self.intensity_label.config(text=f"Intensity: {intensity}")
|
152
|
-
self.mask_value_label.config(text=f"Mask: {mask_value}, Area: {pixel_count}")
|
153
|
-
self.mask_value_label.config(text=f"Mask: {mask_value}")
|
154
|
-
if mask_value != "N/A" and mask_value != 0:
|
155
|
-
self.pixel_count_label.config(text=f"Area: {pixel_count}")
|
156
|
-
else:
|
157
|
-
self.pixel_count_label.config(text="Area: N/A")
|
158
|
-
|
159
|
-
def setup_navigation_toolbar(self):
|
160
|
-
navigation_toolbar = tk.Frame(self.root, bg='black')
|
161
|
-
navigation_toolbar.pack(side='top', fill='x')
|
162
|
-
prev_btn = tk.Button(navigation_toolbar, text="Previous", command=self.previous_image, bg='black', fg='white')
|
163
|
-
prev_btn.pack(side='left')
|
164
|
-
next_btn = tk.Button(navigation_toolbar, text="Next", command=self.next_image, bg='black', fg='white')
|
165
|
-
next_btn.pack(side='left')
|
166
|
-
save_btn = tk.Button(navigation_toolbar, text="Save", command=self.save_mask, bg='black', fg='white')
|
167
|
-
save_btn.pack(side='left')
|
168
|
-
self.intensity_label = tk.Label(navigation_toolbar, text="Image: N/A", bg='black', fg='white')
|
169
|
-
self.intensity_label.pack(side='right')
|
170
|
-
self.mask_value_label = tk.Label(navigation_toolbar, text="Mask: N/A", bg='black', fg='white')
|
171
|
-
self.mask_value_label.pack(side='right')
|
172
|
-
self.pixel_count_label = tk.Label(navigation_toolbar, text="Area: N/A", bg='black', fg='white')
|
173
|
-
self.pixel_count_label.pack(side='right')
|
174
|
-
|
175
|
-
def setup_mode_toolbar(self):
|
176
|
-
self.mode_toolbar = tk.Frame(self.root, bg='black')
|
177
|
-
self.mode_toolbar.pack(side='top', fill='x')
|
178
|
-
self.draw_btn = tk.Button(self.mode_toolbar, text="Draw", command=self.toggle_draw_mode, bg='black', fg='white')
|
179
|
-
self.draw_btn.pack(side='left')
|
180
|
-
self.magic_wand_btn = tk.Button(self.mode_toolbar, text="Magic Wand", command=self.toggle_magic_wand_mode, bg='black', fg='white')
|
181
|
-
self.magic_wand_btn.pack(side='left')
|
182
|
-
tk.Label(self.mode_toolbar, text="Tolerance:", bg='black', fg='white').pack(side='left')
|
183
|
-
self.tolerance_entry = tk.Entry(self.mode_toolbar, textvariable=self.magic_wand_tolerance, bg='black', fg='white')
|
184
|
-
self.tolerance_entry.pack(side='left')
|
185
|
-
tk.Label(self.mode_toolbar, text="Max Pixels:", bg='black', fg='white').pack(side='left')
|
186
|
-
self.max_pixels_entry = tk.Entry(self.mode_toolbar, bg='black', fg='white')
|
187
|
-
self.max_pixels_entry.insert(0, "1000")
|
188
|
-
self.max_pixels_entry.pack(side='left')
|
189
|
-
self.erase_btn = tk.Button(self.mode_toolbar, text="Erase", command=self.toggle_erase_mode, bg='black', fg='white')
|
190
|
-
self.erase_btn.pack(side='left')
|
191
|
-
self.brush_btn = tk.Button(self.mode_toolbar, text="Brush", command=self.toggle_brush_mode, bg='black', fg='white')
|
192
|
-
self.brush_btn.pack(side='left')
|
193
|
-
self.brush_size_entry = tk.Entry(self.mode_toolbar, bg='black', fg='white')
|
194
|
-
self.brush_size_entry.insert(0, "10")
|
195
|
-
self.brush_size_entry.pack(side='left')
|
196
|
-
tk.Label(self.mode_toolbar, text="Brush Size:", bg='black', fg='white').pack(side='left')
|
197
|
-
self.dividing_line_btn = tk.Button(self.mode_toolbar, text="Dividing Line", command=self.toggle_dividing_line_mode, bg='black', fg='white')
|
198
|
-
self.dividing_line_btn.pack(side='left')
|
199
|
-
|
200
|
-
def setup_function_toolbar(self):
|
201
|
-
self.function_toolbar = tk.Frame(self.root, bg='black')
|
202
|
-
self.function_toolbar.pack(side='top', fill='x')
|
203
|
-
self.fill_btn = tk.Button(self.function_toolbar, text="Fill", command=self.fill_objects, bg='black', fg='white')
|
204
|
-
self.fill_btn.pack(side='left')
|
205
|
-
self.relabel_btn = tk.Button(self.function_toolbar, text="Relabel", command=self.relabel_objects, bg='black', fg='white')
|
206
|
-
self.relabel_btn.pack(side='left')
|
207
|
-
self.clear_btn = tk.Button(self.function_toolbar, text="Clear", command=self.clear_objects, bg='black', fg='white')
|
208
|
-
self.clear_btn.pack(side='left')
|
209
|
-
self.invert_btn = tk.Button(self.function_toolbar, text="Invert", command=self.invert_mask, bg='black', fg='white')
|
210
|
-
self.invert_btn.pack(side='left')
|
211
|
-
remove_small_btn = tk.Button(self.function_toolbar, text="Remove Small", command=self.remove_small_objects, bg='black', fg='white')
|
212
|
-
remove_small_btn.pack(side='left')
|
213
|
-
tk.Label(self.function_toolbar, text="Min Area:", bg='black', fg='white').pack(side='left')
|
214
|
-
self.min_area_entry = tk.Entry(self.function_toolbar, bg='black', fg='white')
|
215
|
-
self.min_area_entry.insert(0, "100") # Default minimum area
|
216
|
-
self.min_area_entry.pack(side='left')
|
217
|
-
|
218
|
-
def setup_zoom_toolbar(self):
|
219
|
-
self.zoom_toolbar = tk.Frame(self.root, bg='black')
|
220
|
-
self.zoom_toolbar.pack(side='top', fill='x')
|
221
|
-
self.zoom_btn = tk.Button(self.zoom_toolbar, text="Zoom", command=self.toggle_zoom_mode, bg='black', fg='white')
|
222
|
-
self.zoom_btn.pack(side='left')
|
223
|
-
self.normalize_btn = tk.Button(self.zoom_toolbar, text="Apply Normalization", command=self.apply_normalization, bg='black', fg='white')
|
224
|
-
self.normalize_btn.pack(side='left')
|
225
|
-
tk.Label(self.zoom_toolbar, text="Lower Percentile:", bg='black', fg='white').pack(side='left')
|
226
|
-
self.lower_entry = tk.Entry(self.zoom_toolbar, textvariable=self.lower_quantile, bg='black', fg='white')
|
227
|
-
self.lower_entry.pack(side='left')
|
228
|
-
|
229
|
-
tk.Label(self.zoom_toolbar, text="Upper Percentile:", bg='black', fg='white').pack(side='left')
|
230
|
-
self.upper_entry = tk.Entry(self.zoom_toolbar, textvariable=self.upper_quantile, bg='black', fg='white')
|
231
|
-
self.upper_entry.pack(side='left')
|
232
|
-
|
233
|
-
|
234
|
-
def load_image_and_mask(self, index):
|
235
|
-
image_path = os.path.join(self.folder_path, self.image_filenames[index])
|
236
|
-
image = imageio.imread(image_path)
|
237
|
-
mask_path = os.path.join(self.masks_folder, self.image_filenames[index])
|
238
|
-
if os.path.exists(mask_path):
|
239
|
-
print(f'loading mask:{mask_path} for image: {image_path}')
|
240
|
-
mask = imageio.imread(mask_path)
|
241
|
-
if mask.dtype != np.uint8:
|
242
|
-
mask = (mask / np.max(mask) * 255).astype(np.uint8)
|
243
|
-
else:
|
244
|
-
mask = np.zeros(image.shape[:2], dtype=np.uint8)
|
245
|
-
print(f'loaded new mask for image: {image_path}')
|
246
|
-
return image, mask
|
247
|
-
|
248
|
-
####################################################################################################
|
249
|
-
# Image Display functions#
|
250
|
-
####################################################################################################
|
251
|
-
def display_image(self):
|
252
|
-
if self.zoom_rectangle_id is not None:
|
253
|
-
self.canvas.delete(self.zoom_rectangle_id)
|
254
|
-
self.zoom_rectangle_id = None
|
255
|
-
lower_quantile = float(self.lower_quantile.get()) if self.lower_quantile.get() else 1.0
|
256
|
-
upper_quantile = float(self.upper_quantile.get()) if self.upper_quantile.get() else 99.9
|
257
|
-
normalized = self.normalize_image(self.image, lower_quantile, upper_quantile)
|
258
|
-
combined = self.overlay_mask_on_image(normalized, self.mask)
|
259
|
-
self.tk_image = ImageTk.PhotoImage(image=Image.fromarray(combined))
|
260
|
-
self.canvas.create_image(0, 0, anchor='nw', image=self.tk_image)
|
261
|
-
|
262
|
-
def display_zoomed_image(self):
|
263
|
-
if self.zoom_rectangle_start and self.zoom_rectangle_end:
|
264
|
-
# Convert canvas coordinates to image coordinates
|
265
|
-
x0, y0 = self.canvas_to_image(*self.zoom_rectangle_start)
|
266
|
-
x1, y1 = self.canvas_to_image(*self.zoom_rectangle_end)
|
267
|
-
x0, x1 = min(x0, x1), max(x0, x1)
|
268
|
-
y0, y1 = min(y0, y1), max(y0, y1)
|
269
|
-
self.zoom_x0 = x0
|
270
|
-
self.zoom_y0 = y0
|
271
|
-
self.zoom_x1 = x1
|
272
|
-
self.zoom_y1 = y1
|
273
|
-
# Normalize the entire image
|
274
|
-
lower_quantile = float(self.lower_quantile.get()) if self.lower_quantile.get() else 1.0
|
275
|
-
upper_quantile = float(self.upper_quantile.get()) if self.upper_quantile.get() else 99.9
|
276
|
-
normalized_image = self.normalize_image(self.image, lower_quantile, upper_quantile)
|
277
|
-
# Extract the zoomed portion of the normalized image and mask
|
278
|
-
self.zoom_image = normalized_image[y0:y1, x0:x1]
|
279
|
-
self.zoom_image_orig = self.image[y0:y1, x0:x1]
|
280
|
-
self.zoom_mask = self.mask[y0:y1, x0:x1]
|
281
|
-
original_mask_area = self.mask.shape[0] * self.mask.shape[1]
|
282
|
-
zoom_mask_area = self.zoom_mask.shape[0] * self.zoom_mask.shape[1]
|
283
|
-
if original_mask_area > 0:
|
284
|
-
self.zoom_scale = original_mask_area/zoom_mask_area
|
285
|
-
# Resize the zoomed image and mask to fit the canvas
|
286
|
-
canvas_height = self.canvas.winfo_height()
|
287
|
-
canvas_width = self.canvas.winfo_width()
|
288
|
-
|
289
|
-
if self.zoom_image.size > 0 and canvas_height > 0 and canvas_width > 0:
|
290
|
-
self.zoom_image = resize(self.zoom_image, (canvas_height, canvas_width), preserve_range=True).astype(self.zoom_image.dtype)
|
291
|
-
self.zoom_image_orig = resize(self.zoom_image_orig, (canvas_height, canvas_width), preserve_range=True).astype(self.zoom_image_orig.dtype)
|
292
|
-
#self.zoom_mask = resize(self.zoom_mask, (canvas_height, canvas_width), preserve_range=True).astype(np.uint8)
|
293
|
-
self.zoom_mask = resize(self.zoom_mask, (canvas_height, canvas_width), order=0, preserve_range=True).astype(np.uint8)
|
294
|
-
combined = self.overlay_mask_on_image(self.zoom_image, self.zoom_mask)
|
295
|
-
self.tk_image = ImageTk.PhotoImage(image=Image.fromarray(combined))
|
296
|
-
self.canvas.create_image(0, 0, anchor='nw', image=self.tk_image)
|
297
|
-
|
298
|
-
def overlay_mask_on_image(self, image, mask, alpha=0.5):
|
299
|
-
if len(image.shape) == 2:
|
300
|
-
image = np.stack((image,) * 3, axis=-1)
|
301
|
-
mask = mask.astype(np.int32)
|
302
|
-
max_label = np.max(mask)
|
303
|
-
np.random.seed(0)
|
304
|
-
colors = np.random.randint(0, 255, size=(max_label + 1, 3), dtype=np.uint8)
|
305
|
-
colors[0] = [0, 0, 0] # background color
|
306
|
-
colored_mask = colors[mask]
|
307
|
-
image_8bit = (image / 256).astype(np.uint8)
|
308
|
-
# Blend the mask and the image with transparency
|
309
|
-
combined_image = np.where(mask[..., None] > 0,
|
310
|
-
np.clip(image_8bit * (1 - alpha) + colored_mask * alpha, 0, 255),
|
311
|
-
image_8bit)
|
312
|
-
# Convert the final image back to uint8
|
313
|
-
combined_image = combined_image.astype(np.uint8)
|
314
|
-
return combined_image
|
315
|
-
|
316
|
-
####################################################################################################
|
317
|
-
# Navigation functions#
|
318
|
-
####################################################################################################
|
319
|
-
|
320
|
-
def previous_image(self):
|
321
|
-
if self.current_image_index > 0:
|
322
|
-
self.current_image_index -= 1
|
323
|
-
self.initialize_flags()
|
324
|
-
self.image, self.mask = self.load_image_and_mask(self.current_image_index)
|
325
|
-
self.original_size = self.image.shape
|
326
|
-
self.image, self.mask = self.resize_arrays(self.image, self.mask)
|
327
|
-
self.display_image()
|
328
|
-
|
329
|
-
def next_image(self):
|
330
|
-
if self.current_image_index < len(self.image_filenames) - 1:
|
331
|
-
self.current_image_index += 1
|
332
|
-
self.initialize_flags()
|
333
|
-
self.image, self.mask = self.load_image_and_mask(self.current_image_index)
|
334
|
-
self.original_size = self.image.shape
|
335
|
-
self.image, self.mask = self.resize_arrays(self.image, self.mask)
|
336
|
-
self.display_image()
|
337
|
-
|
338
|
-
def save_mask(self):
|
339
|
-
if self.current_image_index < len(self.image_filenames):
|
340
|
-
original_size = self.original_size
|
341
|
-
if self.mask.shape != original_size:
|
342
|
-
resized_mask = resize(self.mask, original_size, order=0, preserve_range=True).astype(np.uint16)
|
343
|
-
else:
|
344
|
-
resized_mask = self.mask
|
345
|
-
resized_mask, _ = label(resized_mask > 0)
|
346
|
-
save_folder = os.path.join(self.folder_path, 'masks')
|
347
|
-
if not os.path.exists(save_folder):
|
348
|
-
os.makedirs(save_folder)
|
349
|
-
image_filename = os.path.splitext(self.image_filenames[self.current_image_index])[0] + '.tif'
|
350
|
-
save_path = os.path.join(save_folder, image_filename)
|
351
|
-
|
352
|
-
print(f"Saving mask to: {save_path}") # Debug print
|
353
|
-
imageio.imwrite(save_path, resized_mask)
|
354
|
-
|
355
|
-
####################################################################################################
|
356
|
-
# Zoom Functions #
|
357
|
-
####################################################################################################
|
358
|
-
def set_zoom_rectangle_start(self, event):
|
359
|
-
if self.zoom_active:
|
360
|
-
self.zoom_rectangle_start = (event.x, event.y)
|
361
|
-
|
362
|
-
def set_zoom_rectangle_end(self, event):
|
363
|
-
if self.zoom_active:
|
364
|
-
self.zoom_rectangle_end = (event.x, event.y)
|
365
|
-
if self.zoom_rectangle_id is not None:
|
366
|
-
self.canvas.delete(self.zoom_rectangle_id)
|
367
|
-
self.zoom_rectangle_id = None
|
368
|
-
self.display_zoomed_image()
|
369
|
-
self.canvas.unbind("<Motion>")
|
370
|
-
self.canvas.unbind("<Button-1>")
|
371
|
-
self.canvas.unbind("<Button-3>")
|
372
|
-
self.canvas.bind("<Motion>", self.update_mouse_info)
|
373
|
-
|
374
|
-
def update_zoom_box(self, event):
|
375
|
-
if self.zoom_active and self.zoom_rectangle_start is not None:
|
376
|
-
if self.zoom_rectangle_id is not None:
|
377
|
-
self.canvas.delete(self.zoom_rectangle_id)
|
378
|
-
# Assuming event.x and event.y are already in image coordinates
|
379
|
-
self.zoom_rectangle_end = (event.x, event.y)
|
380
|
-
x0, y0 = self.zoom_rectangle_start
|
381
|
-
x1, y1 = self.zoom_rectangle_end
|
382
|
-
self.zoom_rectangle_id = self.canvas.create_rectangle(x0, y0, x1, y1, outline="red", width=2)
|
383
|
-
|
384
|
-
####################################################################################################
|
385
|
-
# Mode activation#
|
386
|
-
####################################################################################################
|
387
|
-
|
388
|
-
def toggle_zoom_mode(self):
|
389
|
-
if not self.zoom_active:
|
390
|
-
self.brush_btn.config(text="Brush")
|
391
|
-
self.canvas.unbind("<B1-Motion>")
|
392
|
-
self.canvas.unbind("<B3-Motion>")
|
393
|
-
self.canvas.unbind("<ButtonRelease-1>")
|
394
|
-
self.canvas.unbind("<ButtonRelease-3>")
|
395
|
-
self.zoom_active = True
|
396
|
-
self.drawing = False
|
397
|
-
self.magic_wand_active = False
|
398
|
-
self.erase_active = False
|
399
|
-
self.brush_active = False
|
400
|
-
self.dividing_line_active = False
|
401
|
-
self.draw_btn.config(text="Draw")
|
402
|
-
self.erase_btn.config(text="Erase")
|
403
|
-
self.magic_wand_btn.config(text="Magic Wand")
|
404
|
-
self.zoom_btn.config(text="Zoom ON")
|
405
|
-
self.dividing_line_btn.config(text="Dividing Line")
|
406
|
-
self.canvas.unbind("<Button-1>")
|
407
|
-
self.canvas.unbind("<Button-3>")
|
408
|
-
self.canvas.unbind("<Motion>")
|
409
|
-
self.canvas.bind("<Button-1>", self.set_zoom_rectangle_start)
|
410
|
-
self.canvas.bind("<Button-3>", self.set_zoom_rectangle_end)
|
411
|
-
self.canvas.bind("<Motion>", self.update_zoom_box)
|
412
|
-
else:
|
413
|
-
self.zoom_active = False
|
414
|
-
self.zoom_btn.config(text="Zoom")
|
415
|
-
self.canvas.unbind("<Button-1>")
|
416
|
-
self.canvas.unbind("<Button-3>")
|
417
|
-
self.canvas.unbind("<Motion>")
|
418
|
-
self.zoom_rectangle_start = self.zoom_rectangle_end = None
|
419
|
-
self.zoom_rectangle_id = None
|
420
|
-
self.display_image()
|
421
|
-
self.canvas.bind("<Motion>", self.update_mouse_info)
|
422
|
-
self.zoom_rectangle_start = None
|
423
|
-
self.zoom_rectangle_end = None
|
424
|
-
self.zoom_rectangle_id = None
|
425
|
-
self.zoom_x0 = None
|
426
|
-
self.zoom_y0 = None
|
427
|
-
self.zoom_x1 = None
|
428
|
-
self.zoom_y1 = None
|
429
|
-
self.zoom_mask = None
|
430
|
-
self.zoom_image = None
|
431
|
-
self.zoom_image_orig = None
|
432
|
-
|
433
|
-
def toggle_brush_mode(self):
|
434
|
-
self.brush_active = not self.brush_active
|
435
|
-
if self.brush_active:
|
436
|
-
self.drawing = False
|
437
|
-
self.magic_wand_active = False
|
438
|
-
self.erase_active = False
|
439
|
-
self.brush_btn.config(text="Brush ON")
|
440
|
-
self.draw_btn.config(text="Draw")
|
441
|
-
self.erase_btn.config(text="Erase")
|
442
|
-
self.magic_wand_btn.config(text="Magic Wand")
|
443
|
-
self.canvas.unbind("<Button-1>")
|
444
|
-
self.canvas.unbind("<Button-3>")
|
445
|
-
self.canvas.unbind("<Motion>")
|
446
|
-
self.canvas.bind("<B1-Motion>", self.apply_brush) # Left click and drag to apply brush
|
447
|
-
self.canvas.bind("<B3-Motion>", self.erase_brush) # Right click and drag to erase with brush
|
448
|
-
self.canvas.bind("<ButtonRelease-1>", self.apply_brush_release) # Left button release
|
449
|
-
self.canvas.bind("<ButtonRelease-3>", self.erase_brush_release) # Right button release
|
450
|
-
else:
|
451
|
-
self.brush_active = False
|
452
|
-
self.brush_btn.config(text="Brush")
|
453
|
-
self.canvas.unbind("<B1-Motion>")
|
454
|
-
self.canvas.unbind("<B3-Motion>")
|
455
|
-
self.canvas.unbind("<ButtonRelease-1>")
|
456
|
-
self.canvas.unbind("<ButtonRelease-3>")
|
457
|
-
|
458
|
-
def image_to_canvas(self, x_image, y_image):
|
459
|
-
x_scale, y_scale = self.get_scaling_factors(
|
460
|
-
self.image.shape[1], self.image.shape[0],
|
461
|
-
self.canvas_width, self.canvas_height
|
462
|
-
)
|
463
|
-
x_canvas = int(x_image / x_scale)
|
464
|
-
y_canvas = int(y_image / y_scale)
|
465
|
-
return x_canvas, y_canvas
|
466
|
-
|
467
|
-
def toggle_dividing_line_mode(self):
|
468
|
-
self.dividing_line_active = not self.dividing_line_active
|
469
|
-
if self.dividing_line_active:
|
470
|
-
self.drawing = False
|
471
|
-
self.magic_wand_active = False
|
472
|
-
self.erase_active = False
|
473
|
-
self.brush_active = False
|
474
|
-
self.draw_btn.config(text="Draw")
|
475
|
-
self.erase_btn.config(text="Erase")
|
476
|
-
self.magic_wand_btn.config(text="Magic Wand")
|
477
|
-
self.brush_btn.config(text="Brush")
|
478
|
-
self.dividing_line_btn.config(text="Dividing Line ON")
|
479
|
-
self.canvas.unbind("<Button-1>")
|
480
|
-
self.canvas.unbind("<ButtonRelease-1>")
|
481
|
-
self.canvas.unbind("<Motion>")
|
482
|
-
self.canvas.bind("<Button-1>", self.start_dividing_line)
|
483
|
-
self.canvas.bind("<ButtonRelease-1>", self.finish_dividing_line)
|
484
|
-
self.canvas.bind("<Motion>", self.update_dividing_line_preview)
|
485
|
-
else:
|
486
|
-
print("Dividing Line Mode: OFF")
|
487
|
-
self.dividing_line_active = False
|
488
|
-
self.dividing_line_btn.config(text="Dividing Line")
|
489
|
-
self.canvas.unbind("<Button-1>")
|
490
|
-
self.canvas.unbind("<ButtonRelease-1>")
|
491
|
-
self.canvas.unbind("<Motion>")
|
492
|
-
self.display_image()
|
493
|
-
|
494
|
-
def start_dividing_line(self, event):
|
495
|
-
if self.dividing_line_active:
|
496
|
-
self.dividing_line_coords = [(event.x, event.y)]
|
497
|
-
self.current_dividing_line = self.canvas.create_line(event.x, event.y, event.x, event.y, fill="red", width=2)
|
498
|
-
|
499
|
-
def finish_dividing_line(self, event):
|
500
|
-
if self.dividing_line_active:
|
501
|
-
self.dividing_line_coords.append((event.x, event.y))
|
502
|
-
if self.zoom_active:
|
503
|
-
self.dividing_line_coords = [self.canvas_to_image(x, y) for x, y in self.dividing_line_coords]
|
504
|
-
self.apply_dividing_line()
|
505
|
-
self.canvas.delete(self.current_dividing_line)
|
506
|
-
self.current_dividing_line = None
|
507
|
-
|
508
|
-
def update_dividing_line_preview(self, event):
|
509
|
-
if self.dividing_line_active and self.dividing_line_coords:
|
510
|
-
x, y = event.x, event.y
|
511
|
-
if self.zoom_active:
|
512
|
-
x, y = self.canvas_to_image(x, y)
|
513
|
-
self.dividing_line_coords.append((x, y))
|
514
|
-
canvas_coords = [(self.image_to_canvas(*pt) if self.zoom_active else pt) for pt in self.dividing_line_coords]
|
515
|
-
flat_canvas_coords = [coord for pt in canvas_coords for coord in pt]
|
516
|
-
self.canvas.coords(self.current_dividing_line, *flat_canvas_coords)
|
517
|
-
|
518
|
-
def apply_dividing_line(self):
|
519
|
-
if self.dividing_line_coords:
|
520
|
-
coords = self.dividing_line_coords
|
521
|
-
if self.zoom_active:
|
522
|
-
coords = [self.canvas_to_image(x, y) for x, y in coords]
|
523
|
-
|
524
|
-
rr, cc = [], []
|
525
|
-
for (x0, y0), (x1, y1) in zip(coords[:-1], coords[1:]):
|
526
|
-
line_rr, line_cc = line(y0, x0, y1, x1)
|
527
|
-
rr.extend(line_rr)
|
528
|
-
cc.extend(line_cc)
|
529
|
-
rr, cc = np.array(rr), np.array(cc)
|
530
|
-
|
531
|
-
mask_copy = self.mask.copy()
|
532
|
-
|
533
|
-
if self.zoom_active:
|
534
|
-
# Update the zoomed mask
|
535
|
-
self.zoom_mask[rr, cc] = 0
|
536
|
-
# Reflect changes to the original mask
|
537
|
-
y0, y1, x0, x1 = self.zoom_y0, self.zoom_y1, self.zoom_x0, self.zoom_x1
|
538
|
-
zoomed_mask_resized_back = resize(self.zoom_mask, (y1 - y0, x1 - x0), order=0, preserve_range=True).astype(np.uint8)
|
539
|
-
self.mask[y0:y1, x0:x1] = zoomed_mask_resized_back
|
540
|
-
else:
|
541
|
-
# Directly update the original mask
|
542
|
-
mask_copy[rr, cc] = 0
|
543
|
-
self.mask = mask_copy
|
544
|
-
|
545
|
-
labeled_mask, num_labels = label(self.mask > 0)
|
546
|
-
self.mask = labeled_mask
|
547
|
-
self.update_display()
|
548
|
-
|
549
|
-
self.dividing_line_coords = []
|
550
|
-
self.canvas.unbind("<Button-1>")
|
551
|
-
self.canvas.unbind("<ButtonRelease-1>")
|
552
|
-
self.canvas.unbind("<Motion>")
|
553
|
-
self.dividing_line_active = False
|
554
|
-
self.dividing_line_btn.config(text="Dividing Line")
|
555
|
-
|
556
|
-
def toggle_draw_mode(self):
|
557
|
-
self.drawing = not self.drawing
|
558
|
-
if self.drawing:
|
559
|
-
self.brush_btn.config(text="Brush")
|
560
|
-
self.canvas.unbind("<B1-Motion>")
|
561
|
-
self.canvas.unbind("<B3-Motion>")
|
562
|
-
self.canvas.unbind("<ButtonRelease-1>")
|
563
|
-
self.canvas.unbind("<ButtonRelease-3>")
|
564
|
-
self.magic_wand_active = False
|
565
|
-
self.erase_active = False
|
566
|
-
self.brush_active = False
|
567
|
-
self.draw_btn.config(text="Draw ON")
|
568
|
-
self.magic_wand_btn.config(text="Magic Wand")
|
569
|
-
self.erase_btn.config(text="Erase")
|
570
|
-
self.draw_coordinates = []
|
571
|
-
self.canvas.unbind("<Button-1>")
|
572
|
-
self.canvas.unbind("<Motion>")
|
573
|
-
self.canvas.bind("<B1-Motion>", self.draw)
|
574
|
-
self.canvas.bind("<ButtonRelease-1>", self.finish_drawing)
|
575
|
-
else:
|
576
|
-
self.drawing = False
|
577
|
-
self.draw_btn.config(text="Draw")
|
578
|
-
self.canvas.unbind("<B1-Motion>")
|
579
|
-
self.canvas.unbind("<ButtonRelease-1>")
|
580
|
-
|
581
|
-
def toggle_magic_wand_mode(self):
|
582
|
-
self.magic_wand_active = not self.magic_wand_active
|
583
|
-
if self.magic_wand_active:
|
584
|
-
self.brush_btn.config(text="Brush")
|
585
|
-
self.canvas.unbind("<B1-Motion>")
|
586
|
-
self.canvas.unbind("<B3-Motion>")
|
587
|
-
self.canvas.unbind("<ButtonRelease-1>")
|
588
|
-
self.canvas.unbind("<ButtonRelease-3>")
|
589
|
-
self.drawing = False
|
590
|
-
self.erase_active = False
|
591
|
-
self.brush_active = False
|
592
|
-
self.draw_btn.config(text="Draw")
|
593
|
-
self.erase_btn.config(text="Erase")
|
594
|
-
self.magic_wand_btn.config(text="Magic Wand ON")
|
595
|
-
self.canvas.bind("<Button-1>", self.use_magic_wand)
|
596
|
-
self.canvas.bind("<Button-3>", self.use_magic_wand)
|
597
|
-
else:
|
598
|
-
self.magic_wand_btn.config(text="Magic Wand")
|
599
|
-
self.canvas.unbind("<Button-1>")
|
600
|
-
self.canvas.unbind("<Button-3>")
|
601
|
-
|
602
|
-
def toggle_erase_mode(self):
|
603
|
-
self.erase_active = not self.erase_active
|
604
|
-
if self.erase_active:
|
605
|
-
self.brush_btn.config(text="Brush")
|
606
|
-
self.canvas.unbind("<B1-Motion>")
|
607
|
-
self.canvas.unbind("<B3-Motion>")
|
608
|
-
self.canvas.unbind("<ButtonRelease-1>")
|
609
|
-
self.canvas.unbind("<ButtonRelease-3>")
|
610
|
-
self.erase_btn.config(text="Erase ON")
|
611
|
-
self.canvas.bind("<Button-1>", self.erase_object)
|
612
|
-
self.drawing = False
|
613
|
-
self.magic_wand_active = False
|
614
|
-
self.brush_active = False
|
615
|
-
self.draw_btn.config(text="Draw")
|
616
|
-
self.magic_wand_btn.config(text="Magic Wand")
|
617
|
-
else:
|
618
|
-
self.erase_active = False
|
619
|
-
self.erase_btn.config(text="Erase")
|
620
|
-
self.canvas.unbind("<Button-1>")
|
5
|
+
def initiate_make_mask_app(parent_frame):
|
6
|
+
from .gui_elements import modify_masks
|
7
|
+
# Set up the settings window
|
8
|
+
settings_window = tk.Toplevel(parent_frame)
|
9
|
+
settings_window.title("Make Masks Settings")
|
10
|
+
settings_window.configure(bg='black') # Set the background color to black
|
621
11
|
|
622
|
-
|
623
|
-
#
|
624
|
-
|
12
|
+
# Use the existing function to create the settings UI
|
13
|
+
settings_frame = tk.Frame(settings_window, bg='black') # Set the background color to black
|
14
|
+
settings_frame.pack(fill=tk.BOTH, expand=True)
|
625
15
|
|
626
|
-
def apply_brush_release(self, event):
|
627
|
-
if hasattr(self, 'brush_path'):
|
628
|
-
for x, y, brush_size in self.brush_path:
|
629
|
-
img_x, img_y = (x, y) if self.zoom_active else self.canvas_to_image(x, y)
|
630
|
-
x0 = max(img_x - brush_size // 2, 0)
|
631
|
-
y0 = max(img_y - brush_size // 2, 0)
|
632
|
-
x1 = min(img_x + brush_size // 2, self.zoom_mask.shape[1] if self.zoom_active else self.mask.shape[1])
|
633
|
-
y1 = min(img_y + brush_size // 2, self.zoom_mask.shape[0] if self.zoom_active else self.mask.shape[0])
|
634
|
-
if self.zoom_active:
|
635
|
-
self.zoom_mask[y0:y1, x0:x1] = 255
|
636
|
-
self.update_original_mask_from_zoom()
|
637
|
-
else:
|
638
|
-
self.mask[y0:y1, x0:x1] = 255
|
639
|
-
del self.brush_path
|
640
|
-
self.canvas.delete("temp_line")
|
641
|
-
self.update_display()
|
642
|
-
|
643
|
-
def erase_brush_release(self, event):
|
644
|
-
if hasattr(self, 'erase_path'):
|
645
|
-
for x, y, brush_size in self.erase_path:
|
646
|
-
img_x, img_y = (x, y) if self.zoom_active else self.canvas_to_image(x, y)
|
647
|
-
x0 = max(img_x - brush_size // 2, 0)
|
648
|
-
y0 = max(img_y - brush_size // 2, 0)
|
649
|
-
x1 = min(img_x + brush_size // 2, self.zoom_mask.shape[1] if self.zoom_active else self.mask.shape[1])
|
650
|
-
y1 = min(img_y + brush_size // 2, self.zoom_mask.shape[0] if self.zoom_active else self.mask.shape[0])
|
651
|
-
if self.zoom_active:
|
652
|
-
self.zoom_mask[y0:y1, x0:x1] = 0
|
653
|
-
self.update_original_mask_from_zoom()
|
654
|
-
else:
|
655
|
-
self.mask[y0:y1, x0:x1] = 0
|
656
|
-
del self.erase_path
|
657
|
-
self.canvas.delete("temp_line")
|
658
|
-
self.update_display()
|
659
|
-
|
660
|
-
def apply_brush(self, event):
|
661
|
-
brush_size = int(self.brush_size_entry.get())
|
662
|
-
x, y = event.x, event.y
|
663
|
-
if not hasattr(self, 'brush_path'):
|
664
|
-
self.brush_path = []
|
665
|
-
self.last_brush_coord = (x, y)
|
666
|
-
if self.last_brush_coord:
|
667
|
-
last_x, last_y = self.last_brush_coord
|
668
|
-
rr, cc = line(last_y, last_x, y, x)
|
669
|
-
for ry, rx in zip(rr, cc):
|
670
|
-
self.brush_path.append((rx, ry, brush_size))
|
671
|
-
|
672
|
-
self.canvas.create_line(self.last_brush_coord[0], self.last_brush_coord[1], x, y, width=brush_size, fill="blue", tag="temp_line")
|
673
|
-
self.last_brush_coord = (x, y)
|
674
|
-
|
675
|
-
def erase_brush(self, event):
|
676
|
-
brush_size = int(self.brush_size_entry.get())
|
677
|
-
x, y = event.x, event.y
|
678
|
-
if not hasattr(self, 'erase_path'):
|
679
|
-
self.erase_path = []
|
680
|
-
self.last_erase_coord = (x, y)
|
681
|
-
if self.last_erase_coord:
|
682
|
-
last_x, last_y = self.last_erase_coord
|
683
|
-
rr, cc = line(last_y, last_x, y, x)
|
684
|
-
for ry, rx in zip(rr, cc):
|
685
|
-
self.erase_path.append((rx, ry, brush_size))
|
686
|
-
|
687
|
-
self.canvas.create_line(self.last_erase_coord[0], self.last_erase_coord[1], x, y, width=brush_size, fill="white", tag="temp_line")
|
688
|
-
self.last_erase_coord = (x, y)
|
689
|
-
|
690
|
-
def erase_object(self, event):
|
691
|
-
x, y = event.x, event.y
|
692
|
-
if self.zoom_active:
|
693
|
-
canvas_x, canvas_y = x, y
|
694
|
-
zoomed_x = int(canvas_x * (self.zoom_image.shape[1] / self.canvas_width))
|
695
|
-
zoomed_y = int(canvas_y * (self.zoom_image.shape[0] / self.canvas_height))
|
696
|
-
orig_x = int(zoomed_x * ((self.zoom_x1 - self.zoom_x0) / self.canvas_width) + self.zoom_x0)
|
697
|
-
orig_y = int(zoomed_y * ((self.zoom_y1 - self.zoom_y0) / self.canvas_height) + self.zoom_y0)
|
698
|
-
if orig_x < 0 or orig_y < 0 or orig_x >= self.image.shape[1] or orig_y >= self.image.shape[0]:
|
699
|
-
print("Point is out of bounds in the original image.")
|
700
|
-
return
|
701
|
-
else:
|
702
|
-
orig_x, orig_y = x, y
|
703
|
-
label_to_remove = self.mask[orig_y, orig_x]
|
704
|
-
if label_to_remove > 0:
|
705
|
-
self.mask[self.mask == label_to_remove] = 0
|
706
|
-
self.update_display()
|
707
|
-
|
708
|
-
def use_magic_wand(self, event):
|
709
|
-
x, y = event.x, event.y
|
710
|
-
tolerance = int(self.magic_wand_tolerance.get())
|
711
|
-
maximum = int(self.max_pixels_entry.get())
|
712
|
-
action = 'add' if event.num == 1 else 'erase'
|
713
|
-
if self.zoom_active:
|
714
|
-
self.magic_wand_zoomed((x, y), tolerance, action)
|
715
|
-
else:
|
716
|
-
self.magic_wand_normal((x, y), tolerance, action)
|
717
|
-
|
718
|
-
def apply_magic_wand(self, image, mask, seed_point, tolerance, maximum, action='add'):
|
719
|
-
x, y = seed_point
|
720
|
-
initial_value = image[y, x].astype(np.float32)
|
721
|
-
visited = np.zeros_like(image, dtype=bool)
|
722
|
-
queue = deque([(x, y)])
|
723
|
-
added_pixels = 0
|
724
|
-
|
725
|
-
while queue and added_pixels < maximum:
|
726
|
-
cx, cy = queue.popleft()
|
727
|
-
if visited[cy, cx]:
|
728
|
-
continue
|
729
|
-
visited[cy, cx] = True
|
730
|
-
current_value = image[cy, cx].astype(np.float32)
|
731
|
-
|
732
|
-
if np.linalg.norm(abs(current_value - initial_value)) <= tolerance:
|
733
|
-
if mask[cy, cx] == 0:
|
734
|
-
added_pixels += 1
|
735
|
-
mask[cy, cx] = 255 if action == 'add' else 0
|
736
|
-
|
737
|
-
if added_pixels >= maximum:
|
738
|
-
break
|
739
|
-
|
740
|
-
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
741
|
-
nx, ny = cx + dx, cy + dy
|
742
|
-
if 0 <= nx < image.shape[1] and 0 <= ny < image.shape[0] and not visited[ny, nx]:
|
743
|
-
queue.append((nx, ny))
|
744
|
-
return mask
|
745
|
-
|
746
|
-
def magic_wand_normal(self, seed_point, tolerance, action):
|
747
|
-
try:
|
748
|
-
maximum = int(self.max_pixels_entry.get())
|
749
|
-
except ValueError:
|
750
|
-
print("Invalid maximum value; using default of 1000")
|
751
|
-
maximum = 1000
|
752
|
-
self.mask = self.apply_magic_wand(self.image, self.mask, seed_point, tolerance, maximum, action)
|
753
|
-
self.display_image()
|
754
|
-
|
755
|
-
def magic_wand_zoomed(self, seed_point, tolerance, action):
|
756
|
-
if self.zoom_image_orig is None or self.zoom_mask is None:
|
757
|
-
print("Zoomed image or mask not initialized")
|
758
|
-
return
|
759
|
-
try:
|
760
|
-
maximum = int(self.max_pixels_entry.get())
|
761
|
-
maximum = maximum * self.zoom_scale
|
762
|
-
except ValueError:
|
763
|
-
print("Invalid maximum value; using default of 1000")
|
764
|
-
maximum = 1000
|
765
|
-
|
766
|
-
canvas_x, canvas_y = seed_point
|
767
|
-
if canvas_x < 0 or canvas_y < 0 or canvas_x >= self.zoom_image_orig.shape[1] or canvas_y >= self.zoom_image_orig.shape[0]:
|
768
|
-
print("Selected point is out of bounds in the zoomed image.")
|
769
|
-
return
|
770
|
-
|
771
|
-
self.zoom_mask = self.apply_magic_wand(self.zoom_image_orig, self.zoom_mask, (canvas_x, canvas_y), tolerance, maximum, action)
|
772
|
-
y0, y1, x0, x1 = self.zoom_y0, self.zoom_y1, self.zoom_x0, self.zoom_x1
|
773
|
-
zoomed_mask_resized_back = resize(self.zoom_mask, (y1 - y0, x1 - x0), order=0, preserve_range=True).astype(np.uint8)
|
774
|
-
if action == 'erase':
|
775
|
-
self.mask[y0:y1, x0:x1] = np.where(zoomed_mask_resized_back == 0, 0, self.mask[y0:y1, x0:x1])
|
776
|
-
else:
|
777
|
-
self.mask[y0:y1, x0:x1] = np.where(zoomed_mask_resized_back > 0, zoomed_mask_resized_back, self.mask[y0:y1, x0:x1])
|
778
|
-
self.update_display()
|
779
|
-
|
780
|
-
def draw(self, event):
|
781
|
-
if self.drawing:
|
782
|
-
x, y = event.x, event.y
|
783
|
-
if self.draw_coordinates:
|
784
|
-
last_x, last_y = self.draw_coordinates[-1]
|
785
|
-
self.current_line = self.canvas.create_line(last_x, last_y, x, y, fill="yellow", width=3)
|
786
|
-
self.draw_coordinates.append((x, y))
|
787
|
-
|
788
|
-
def draw_on_zoomed_mask(self, draw_coordinates):
|
789
|
-
canvas_height = self.canvas.winfo_height()
|
790
|
-
canvas_width = self.canvas.winfo_width()
|
791
|
-
zoomed_mask = np.zeros((canvas_height, canvas_width), dtype=np.uint8)
|
792
|
-
rr, cc = polygon(np.array(draw_coordinates)[:, 1], np.array(draw_coordinates)[:, 0], shape=zoomed_mask.shape)
|
793
|
-
zoomed_mask[rr, cc] = 255
|
794
|
-
return zoomed_mask
|
795
|
-
|
796
|
-
def finish_drawing(self, event):
|
797
|
-
if len(self.draw_coordinates) > 2:
|
798
|
-
self.draw_coordinates.append(self.draw_coordinates[0])
|
799
|
-
if self.zoom_active:
|
800
|
-
x0, x1, y0, y1 = self.zoom_x0, self.zoom_x1, self.zoom_y0, self.zoom_y1
|
801
|
-
zoomed_mask = self.draw_on_zoomed_mask(self.draw_coordinates)
|
802
|
-
self.update_original_mask(zoomed_mask, x0, x1, y0, y1)
|
803
|
-
else:
|
804
|
-
rr, cc = polygon(np.array(self.draw_coordinates)[:, 1], np.array(self.draw_coordinates)[:, 0], shape=self.mask.shape)
|
805
|
-
self.mask[rr, cc] = np.maximum(self.mask[rr, cc], 255)
|
806
|
-
self.mask = self.mask.copy()
|
807
|
-
self.canvas.delete(self.current_line)
|
808
|
-
self.draw_coordinates.clear()
|
809
|
-
self.update_display()
|
810
|
-
|
811
|
-
def finish_drawing_if_active(self, event):
|
812
|
-
if self.drawing and len(self.draw_coordinates) > 2:
|
813
|
-
self.finish_drawing(event)
|
814
|
-
|
815
|
-
####################################################################################################
|
816
|
-
# Single function butons#
|
817
|
-
####################################################################################################
|
818
|
-
|
819
|
-
def apply_normalization(self):
|
820
|
-
self.lower_quantile.set(self.lower_entry.get())
|
821
|
-
self.upper_quantile.set(self.upper_entry.get())
|
822
|
-
self.update_display()
|
823
|
-
|
824
|
-
def fill_objects(self):
|
825
|
-
binary_mask = self.mask > 0
|
826
|
-
filled_mask = binary_fill_holes(binary_mask)
|
827
|
-
self.mask = filled_mask.astype(np.uint8) * 255
|
828
|
-
labeled_mask, _ = label(filled_mask)
|
829
|
-
self.mask = labeled_mask
|
830
|
-
self.update_display()
|
831
|
-
|
832
|
-
def relabel_objects(self):
|
833
|
-
mask = self.mask
|
834
|
-
labeled_mask, num_labels = label(mask > 0)
|
835
|
-
self.mask = labeled_mask
|
836
|
-
self.update_display()
|
837
|
-
|
838
|
-
def clear_objects(self):
|
839
|
-
self.mask = np.zeros_like(self.mask)
|
840
|
-
self.update_display()
|
841
|
-
|
842
|
-
def invert_mask(self):
|
843
|
-
self.mask = np.where(self.mask > 0, 0, 1)
|
844
|
-
self.relabel_objects()
|
845
|
-
self.update_display()
|
846
|
-
|
847
|
-
def remove_small_objects(self):
|
848
|
-
try:
|
849
|
-
min_area = int(self.min_area_entry.get())
|
850
|
-
except ValueError:
|
851
|
-
print("Invalid minimum area value; using default of 100")
|
852
|
-
min_area = 100
|
853
|
-
|
854
|
-
labeled_mask, num_labels = label(self.mask > 0)
|
855
|
-
for i in range(1, num_labels + 1): # Skip background
|
856
|
-
if np.sum(labeled_mask == i) < min_area:
|
857
|
-
self.mask[labeled_mask == i] = 0 # Remove small objects
|
858
|
-
self.update_display()
|
859
|
-
|
860
|
-
##@log_function_call
|
861
|
-
def initiate_mask_app_root(parent_frame):
|
862
|
-
style = ttk.Style(parent_frame)
|
863
|
-
set_dark_style(style)
|
864
|
-
set_default_font(parent_frame, font_name="Arial", size=8)
|
865
|
-
|
866
|
-
container = tk.PanedWindow(parent_frame, orient=tk.HORIZONTAL)
|
867
|
-
container.pack(fill=tk.BOTH, expand=True)
|
868
|
-
|
869
|
-
scrollable_frame = spacrFrame(container, bg='black')
|
870
|
-
container.add(scrollable_frame, stretch="always")
|
871
|
-
|
872
|
-
# Setup input fields
|
873
16
|
vars_dict = {
|
874
|
-
'folder_path': ttk.Entry(
|
875
|
-
'scale_factor': ttk.Entry(
|
17
|
+
'folder_path': ttk.Entry(settings_frame),
|
18
|
+
'scale_factor': ttk.Entry(settings_frame)
|
876
19
|
}
|
877
20
|
|
878
21
|
# Arrange input fields and labels
|
879
22
|
row = 0
|
880
23
|
for name, entry in vars_dict.items():
|
881
|
-
ttk.Label(
|
24
|
+
ttk.Label(settings_frame, text=f"{name.replace('_', ' ').capitalize()}:",
|
25
|
+
background="black", foreground="white").grid(row=row, column=0)
|
882
26
|
entry.grid(row=row, column=1)
|
883
27
|
row += 1
|
884
28
|
|
885
29
|
# Function to be called when "Run" button is clicked
|
886
|
-
def
|
30
|
+
def start_make_mask_app():
|
887
31
|
folder_path = vars_dict['folder_path'].get()
|
888
|
-
|
889
|
-
|
890
|
-
|
891
|
-
|
892
|
-
widget.destroy()
|
893
|
-
|
894
|
-
# Start the modify_masks application in the same root window
|
895
|
-
app_instance = modify_masks(parent_frame, folder_path, scale_factor)
|
896
|
-
|
897
|
-
run_button = spacrButton(scrollable_frame.scrollable_frame, text="Run", command=run_app)
|
898
|
-
run_button.grid(row=row, column=0, columnspan=2, pady=10, padx=10)
|
32
|
+
try:
|
33
|
+
scale_factor = float(vars_dict['scale_factor'].get())
|
34
|
+
except ValueError:
|
35
|
+
scale_factor = None # Handle invalid input gracefully
|
899
36
|
|
900
|
-
|
37
|
+
# Convert empty strings to None
|
38
|
+
folder_path = folder_path if folder_path != '' else None
|
901
39
|
|
902
|
-
|
903
|
-
|
904
|
-
width = root.winfo_screenwidth()
|
905
|
-
height = root.winfo_screenheight()
|
906
|
-
root.geometry(f"{width}x{height}")
|
907
|
-
root.title("Mask Application")
|
40
|
+
settings_window.destroy()
|
41
|
+
modify_masks(parent_frame, folder_path, scale_factor)
|
908
42
|
|
909
|
-
|
910
|
-
|
911
|
-
|
912
|
-
|
913
|
-
|
914
|
-
|
915
|
-
root.content_frame = tk.Frame(root)
|
916
|
-
root.content_frame.grid(row=1, column=0, sticky="nsew")
|
917
|
-
root.grid_rowconfigure(1, weight=1)
|
918
|
-
root.grid_columnconfigure(0, weight=1)
|
919
|
-
|
920
|
-
initiate_mask_app_root(root.content_frame)
|
921
|
-
create_menu_bar(root)
|
922
|
-
root.mainloop()
|
43
|
+
run_button = tk.Button(settings_window, text="Start Make Masks", command=start_make_mask_app, bg='black', fg='white')
|
44
|
+
run_button.pack(pady=10)
|
45
|
+
|
46
|
+
def start_make_mask_app():
|
47
|
+
app = MainApp(default_app="Make Masks")
|
48
|
+
app.mainloop()
|
923
49
|
|
924
50
|
if __name__ == "__main__":
|
925
|
-
|
51
|
+
start_make_mask_app()
|