spacr 0.1.63__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_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
- from .gui_elements import spacrFrame, spacrButton, set_dark_style, create_menu_bar, set_default_font
14
-
15
- class modify_masks:
16
-
17
- def __init__(self, root, folder_path, scale_factor):
18
- self.root = root
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
- # Mode functions#
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(scrollable_frame.scrollable_frame),
875
- 'scale_factor': ttk.Entry(scrollable_frame.scrollable_frame)
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(scrollable_frame.scrollable_frame, text=f"{name.replace('_', ' ').capitalize()}:").grid(row=row, column=0)
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 run_app():
30
+ def start_make_mask_app():
887
31
  folder_path = vars_dict['folder_path'].get()
888
- scale_factor = float(vars_dict['scale_factor'].get())
889
-
890
- # Clear previous content instead of destroying the root
891
- for widget in parent_frame.winfo_children():
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
- return parent_frame
37
+ # Convert empty strings to None
38
+ folder_path = folder_path if folder_path != '' else None
901
39
 
902
- def gui_make_masks():
903
- root = tk.Tk()
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
- # Clear previous content if any
910
- if hasattr(root, 'content_frame'):
911
- for widget in root.content_frame.winfo_children():
912
- widget.destroy()
913
- root.content_frame.grid_forget()
914
- else:
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
- gui_make_masks()
51
+ start_make_mask_app()