spacr 0.1.0__py3-none-any.whl → 0.1.6__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/mask_app.py CHANGED
@@ -1,925 +1,249 @@
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
1
+ #import customtkinter as ctk
2
+
3
+ import sys, ctypes, matplotlib
10
4
  import tkinter as tk
11
- from tkinter import ttk
12
- from ttkthemes import ThemedTk
5
+ from tkinter import ttk, scrolledtext
6
+ from matplotlib.figure import Figure
7
+ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
8
+ matplotlib.use('Agg')
9
+ from tkinter import filedialog
10
+ from multiprocessing import Process, Queue, Value
11
+ import traceback
12
+
13
+ try:
14
+ ctypes.windll.shcore.SetProcessDpiAwareness(True)
15
+ except AttributeError:
16
+ pass
13
17
 
14
18
  from .logger import log_function_call
15
-
16
- from .gui_utils import ScrollableFrame, set_dark_style, set_default_font, create_dark_mode, style_text_boxes, create_menu_bar
17
-
18
- class modify_masks:
19
-
20
- def __init__(self, root, folder_path, scale_factor, width, height):
21
- self.root = root
22
- self.folder_path = folder_path
23
- self.scale_factor = scale_factor
24
- self.image_filenames = sorted([f for f in os.listdir(folder_path) if f.endswith(('.png', '.jpg', '.jpeg', '.tif', '.tiff'))])
25
- self.masks_folder = os.path.join(folder_path, 'masks')
26
- self.current_image_index = 0
27
- self.initialize_flags()
28
- self.canvas_width = width
29
- self.canvas_height = height
30
- self.setup_navigation_toolbar()
31
- self.setup_mode_toolbar()
32
- self.setup_function_toolbar()
33
- self.setup_zoom_toolbar()
34
- self.setup_canvas()
35
- self.load_first_image()
36
-
37
- ####################################################################################################
38
- # Helper functions#
39
- ####################################################################################################
40
-
41
- def update_display(self):
42
- if self.zoom_active:
43
- self.display_zoomed_image()
44
- else:
45
- self.display_image()
46
-
47
- def update_original_mask_from_zoom(self):
48
- y0, y1, x0, x1 = self.zoom_y0, self.zoom_y1, self.zoom_x0, self.zoom_x1
49
- zoomed_mask_resized = resize(self.zoom_mask, (y1 - y0, x1 - x0), order=0, preserve_range=True).astype(np.uint8)
50
- self.mask[y0:y1, x0:x1] = zoomed_mask_resized
51
-
52
- def update_original_mask(self, zoomed_mask, x0, x1, y0, y1):
53
- actual_mask_region = self.mask[y0:y1, x0:x1]
54
- target_shape = actual_mask_region.shape
55
- resized_mask = resize(zoomed_mask, target_shape, order=0, preserve_range=True).astype(np.uint8)
56
- if resized_mask.shape != actual_mask_region.shape:
57
- raise ValueError(f"Shape mismatch: resized_mask {resized_mask.shape}, actual_mask_region {actual_mask_region.shape}")
58
- self.mask[y0:y1, x0:x1] = np.maximum(actual_mask_region, resized_mask)
59
- self.mask = self.mask.copy()
60
- self.mask[y0:y1, x0:x1] = np.maximum(self.mask[y0:y1, x0:x1], resized_mask)
61
- self.mask = self.mask.copy()
62
-
63
- def get_scaling_factors(self, img_width, img_height, canvas_width, canvas_height):
64
- x_scale = img_width / canvas_width
65
- y_scale = img_height / canvas_height
66
- return x_scale, y_scale
67
-
68
- def canvas_to_image(self, x_canvas, y_canvas):
69
- x_scale, y_scale = self.get_scaling_factors(
70
- self.image.shape[1], self.image.shape[0],
71
- self.canvas_width, self.canvas_height
72
- )
73
- x_image = int(x_canvas * x_scale)
74
- y_image = int(y_canvas * y_scale)
75
- return x_image, y_image
76
-
77
- def apply_zoom_on_enter(self, event):
78
- if self.zoom_active and self.zoom_rectangle_start is not None:
79
- self.set_zoom_rectangle_end(event)
80
-
81
- def normalize_image(self, image, lower_quantile, upper_quantile):
82
- lower_bound = np.percentile(image, lower_quantile)
83
- upper_bound = np.percentile(image, upper_quantile)
84
- normalized = np.clip(image, lower_bound, upper_bound)
85
- normalized = (normalized - lower_bound) / (upper_bound - lower_bound)
86
- max_value = np.iinfo(image.dtype).max
87
- normalized = (normalized * max_value).astype(image.dtype)
88
- return normalized
89
-
90
- def resize_arrays(self, img, mask):
91
- original_dtype = img.dtype
92
- scaled_height = int(img.shape[0] * self.scale_factor)
93
- scaled_width = int(img.shape[1] * self.scale_factor)
94
- scaled_img = resize(img, (scaled_height, scaled_width), anti_aliasing=True, preserve_range=True)
95
- scaled_mask = resize(mask, (scaled_height, scaled_width), order=0, anti_aliasing=False, preserve_range=True)
96
- stretched_img = resize(scaled_img, (self.canvas_height, self.canvas_width), anti_aliasing=True, preserve_range=True)
97
- stretched_mask = resize(scaled_mask, (self.canvas_height, self.canvas_width), order=0, anti_aliasing=False, preserve_range=True)
98
- return stretched_img.astype(original_dtype), stretched_mask.astype(original_dtype)
99
-
100
- ####################################################################################################
101
- #Initiate canvas elements#
102
- ####################################################################################################
103
-
104
- def load_first_image(self):
105
- self.image, self.mask = self.load_image_and_mask(self.current_image_index)
106
- self.original_size = self.image.shape
107
- self.image, self.mask = self.resize_arrays(self.image, self.mask)
108
- self.display_image()
109
-
110
- def setup_canvas(self):
111
- self.canvas = tk.Canvas(self.root, width=self.canvas_width, height=self.canvas_height)
112
- self.canvas.pack()
113
- self.canvas.bind("<Motion>", self.update_mouse_info)
114
-
115
- def initialize_flags(self):
116
- self.zoom_rectangle_start = None
117
- self.zoom_rectangle_end = None
118
- self.zoom_rectangle_id = None
119
- self.zoom_x0 = None
120
- self.zoom_y0 = None
121
- self.zoom_x1 = None
122
- self.zoom_y1 = None
123
- self.zoom_mask = None
124
- self.zoom_image = None
125
- self.zoom_image_orig = None
126
- self.zoom_scale = 1
127
- self.drawing = False
128
- self.zoom_active = False
129
- self.magic_wand_active = False
130
- self.brush_active = False
131
- self.dividing_line_active = False
132
- self.dividing_line_coords = []
133
- self.current_dividing_line = None
134
- self.lower_quantile = tk.StringVar(value="1.0")
135
- self.upper_quantile = tk.StringVar(value="99.9")
136
- self.magic_wand_tolerance = tk.StringVar(value="1000")
137
-
138
- def update_mouse_info(self, event):
139
- x, y = event.x, event.y
140
- intensity = "N/A"
141
- mask_value = "N/A"
142
- pixel_count = "N/A"
143
- if self.zoom_active:
144
- if 0 <= x < self.canvas_width and 0 <= y < self.canvas_height:
145
- intensity = self.zoom_image_orig[y, x] if self.zoom_image_orig is not None else "N/A"
146
- mask_value = self.zoom_mask[y, x] if self.zoom_mask is not None else "N/A"
147
- else:
148
- if 0 <= x < self.image.shape[1] and 0 <= y < self.image.shape[0]:
149
- intensity = self.image[y, x]
150
- mask_value = self.mask[y, x]
151
- if mask_value != "N/A" and mask_value != 0:
152
- pixel_count = np.sum(self.mask == mask_value)
153
- self.intensity_label.config(text=f"Intensity: {intensity}")
154
- self.mask_value_label.config(text=f"Mask: {mask_value}, Area: {pixel_count}")
155
- self.mask_value_label.config(text=f"Mask: {mask_value}")
156
- if mask_value != "N/A" and mask_value != 0:
157
- self.pixel_count_label.config(text=f"Area: {pixel_count}")
158
- else:
159
- self.pixel_count_label.config(text="Area: N/A")
160
-
161
- def setup_navigation_toolbar(self):
162
- navigation_toolbar = tk.Frame(self.root)
163
- navigation_toolbar.pack(side='top', fill='x')
164
- prev_btn = tk.Button(navigation_toolbar, text="Previous", command=self.previous_image)
165
- prev_btn.pack(side='left')
166
- next_btn = tk.Button(navigation_toolbar, text="Next", command=self.next_image)
167
- next_btn.pack(side='left')
168
- save_btn = tk.Button(navigation_toolbar, text="Save", command=self.save_mask)
169
- save_btn.pack(side='left')
170
- self.intensity_label = tk.Label(navigation_toolbar, text="Image: N/A")
171
- self.intensity_label.pack(side='right')
172
- self.mask_value_label = tk.Label(navigation_toolbar, text="Mask: N/A")
173
- self.mask_value_label.pack(side='right')
174
- self.pixel_count_label = tk.Label(navigation_toolbar, text="Area: N/A")
175
- self.pixel_count_label.pack(side='right')
176
-
177
- def setup_mode_toolbar(self):
178
- self.mode_toolbar = tk.Frame(self.root)
179
- self.mode_toolbar.pack(side='top', fill='x')
180
- self.draw_btn = tk.Button(self.mode_toolbar, text="Draw", command=self.toggle_draw_mode)
181
- self.draw_btn.pack(side='left')
182
- self.magic_wand_btn = tk.Button(self.mode_toolbar, text="Magic Wand", command=self.toggle_magic_wand_mode)
183
- self.magic_wand_btn.pack(side='left')
184
- tk.Label(self.mode_toolbar, text="Tolerance:").pack(side='left')
185
- self.tolerance_entry = tk.Entry(self.mode_toolbar, textvariable=self.magic_wand_tolerance)
186
- self.tolerance_entry.pack(side='left')
187
- tk.Label(self.mode_toolbar, text="Max Pixels:").pack(side='left')
188
- self.max_pixels_entry = tk.Entry(self.mode_toolbar)
189
- self.max_pixels_entry.insert(0, "1000")
190
- self.max_pixels_entry.pack(side='left')
191
- self.erase_btn = tk.Button(self.mode_toolbar, text="Erase", command=self.toggle_erase_mode)
192
- self.erase_btn.pack(side='left')
193
- self.brush_btn = tk.Button(self.mode_toolbar, text="Brush", command=self.toggle_brush_mode)
194
- self.brush_btn.pack(side='left')
195
- self.brush_size_entry = tk.Entry(self.mode_toolbar)
196
- self.brush_size_entry.insert(0, "10")
197
- self.brush_size_entry.pack(side='left')
198
- tk.Label(self.mode_toolbar, text="Brush Size:").pack(side='left')
199
- self.dividing_line_btn = tk.Button(self.mode_toolbar, text="Dividing Line", command=self.toggle_dividing_line_mode)
200
- self.dividing_line_btn.pack(side='left')
201
-
202
- def setup_function_toolbar(self):
203
- self.function_toolbar = tk.Frame(self.root)
204
- self.function_toolbar.pack(side='top', fill='x')
205
- self.fill_btn = tk.Button(self.function_toolbar, text="Fill", command=self.fill_objects)
206
- self.fill_btn.pack(side='left')
207
- self.relabel_btn = tk.Button(self.function_toolbar, text="Relabel", command=self.relabel_objects)
208
- self.relabel_btn.pack(side='left')
209
- self.clear_btn = tk.Button(self.function_toolbar, text="Clear", command=self.clear_objects)
210
- self.clear_btn.pack(side='left')
211
- self.invert_btn = tk.Button(self.function_toolbar, text="Invert", command=self.invert_mask)
212
- self.invert_btn.pack(side='left')
213
- remove_small_btn = tk.Button(self.function_toolbar, text="Remove Small", command=self.remove_small_objects)
214
- remove_small_btn.pack(side='left')
215
- tk.Label(self.function_toolbar, text="Min Area:").pack(side='left')
216
- self.min_area_entry = tk.Entry(self.function_toolbar)
217
- self.min_area_entry.insert(0, "100") # Default minimum area
218
- self.min_area_entry.pack(side='left')
219
-
220
- def setup_zoom_toolbar(self):
221
- self.zoom_toolbar = tk.Frame(self.root)
222
- self.zoom_toolbar.pack(side='top', fill='x')
223
- self.zoom_btn = tk.Button(self.zoom_toolbar, text="Zoom", command=self.toggle_zoom_mode)
224
- self.zoom_btn.pack(side='left')
225
- self.normalize_btn = tk.Button(self.zoom_toolbar, text="Apply Normalization", command=self.apply_normalization)
226
- self.normalize_btn.pack(side='left')
227
- self.lower_entry = tk.Entry(self.zoom_toolbar, textvariable=self.lower_quantile)
228
- self.lower_entry.pack(side='left')
229
- tk.Label(self.zoom_toolbar, text="Lower Percentile:").pack(side='left')
230
- self.upper_entry = tk.Entry(self.zoom_toolbar, textvariable=self.upper_quantile)
231
- self.upper_entry.pack(side='left')
232
- tk.Label(self.zoom_toolbar, text="Upper Percentile:").pack(side='left')
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)
19
+ from .gui_utils import ScrollableFrame, StdoutRedirector, ToggleSwitch, CustomButton, ToolTip
20
+ from .gui_utils import clear_canvas, main_thread_update_function, set_dark_style, generate_fields, process_stdout_stderr, set_default_font, style_text_boxes
21
+ from .gui_utils import mask_variables, check_mask_gui_settings, preprocess_generate_masks_wrapper, read_settings_from_csv, update_settings_from_csv, create_menu_bar
22
+
23
+ thread_control = {"run_thread": None, "stop_requested": False}
24
+
25
+ def toggle_test_mode():
26
+ global vars_dict
27
+ current_state = vars_dict['test_mode'][2].get()
28
+ new_state = not current_state
29
+ vars_dict['test_mode'][2].set(new_state)
30
+ if new_state:
31
+ test_mode_button.config(bg="blue")
32
+ else:
33
+ test_mode_button.config(bg="gray")
34
+
35
+ def toggle_advanced_settings():
36
+ global vars_dict
37
+
38
+ timelapse_settings = ['timelapse', 'timelapse_memory', 'timelapse_remove_transient', 'timelapse_mode', 'timelapse_objects', 'timelapse_displacement', 'timelapse_frame_limits', 'fps']
39
+ misc_settings = ['examples_to_plot', 'all_to_mip', 'pick_slice', 'skip_mode']
40
+ opperational_settings = ['preprocess', 'masks', 'randomize', 'batch_size', 'custom_regex', 'merge', 'normalize_plots', 'workers', 'plot', 'remove_background', 'lower_quantile']
41
+
42
+ advanced_settings = timelapse_settings+misc_settings+opperational_settings
43
+
44
+ # Toggle visibility of advanced settings
45
+ for setting in advanced_settings:
46
+ label, widget, var = vars_dict[setting]
47
+ if advanced_var.get() is False:
48
+ label.grid_remove() # Hide the label
49
+ widget.grid_remove() # Hide the widget
412
50
  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>")
621
-
622
- ####################################################################################################
623
- # Mode functions#
624
- ####################################################################################################
625
-
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(width, height):
862
- theme = 'breeze'
863
- root = ThemedTk(theme=theme)
864
- style = ttk.Style(root)
51
+ label.grid() # Show the label
52
+ widget.grid() # Show the widget
53
+
54
+ #@log_function_call
55
+ def initiate_abort():
56
+ global thread_control
57
+ if thread_control.get("stop_requested") is not None:
58
+ thread_control["stop_requested"].value = 1
59
+
60
+ if thread_control.get("run_thread") is not None:
61
+ thread_control["run_thread"].join(timeout=5)
62
+ if thread_control["run_thread"].is_alive():
63
+ thread_control["run_thread"].terminate()
64
+ thread_control["run_thread"] = None
65
+
66
+ #@log_function_call
67
+ def run_mask_gui(q, fig_queue, stop_requested):
68
+ global vars_dict
69
+ process_stdout_stderr(q)
70
+ try:
71
+ settings = check_mask_gui_settings(vars_dict)
72
+ preprocess_generate_masks_wrapper(settings, q, fig_queue)
73
+ except Exception as e:
74
+ q.put(f"Error during processing: {e}")
75
+ traceback.print_exc()
76
+ finally:
77
+ stop_requested.value = 1
78
+
79
+ #@log_function_call
80
+ def start_process(q, fig_queue):
81
+ global thread_control
82
+ if thread_control.get("run_thread") is not None:
83
+ initiate_abort()
84
+
85
+ stop_requested = Value('i', 0) # multiprocessing shared value for inter-process communication
86
+ thread_control["stop_requested"] = stop_requested
87
+ thread_control["run_thread"] = Process(target=run_mask_gui, args=(q, fig_queue, stop_requested))
88
+ thread_control["run_thread"].start()
89
+
90
+ def import_settings(scrollable_frame):
91
+ global vars_dict
92
+
93
+ csv_file_path = filedialog.askopenfilename(filetypes=[("CSV files", "*.csv")])
94
+ csv_settings = read_settings_from_csv(csv_file_path)
95
+ variables = mask_variables()
96
+ new_settings = update_settings_from_csv(variables, csv_settings)
97
+ vars_dict = generate_fields(new_settings, scrollable_frame)
98
+
99
+ #@log_function_call
100
+ def initiate_mask_root(parent_frame):
101
+ global vars_dict, q, canvas, fig_queue, canvas_widget, thread_control, advanced_var, scrollable_frame
102
+
103
+ style = ttk.Style(parent_frame)
865
104
  set_dark_style(style)
866
-
867
105
  style_text_boxes(style)
868
- set_default_font(root, font_name="Arial", size=8)
869
- root.geometry(f"{width}x{height}")
870
- root.title("Mask App")
871
- create_menu_bar(root)
106
+ set_default_font(parent_frame, font_name="Helvetica", size=8)
107
+ parent_frame.configure(bg='black')
108
+ parent_frame.grid_rowconfigure(0, weight=1)
109
+ parent_frame.grid_columnconfigure(0, weight=1)
110
+
111
+ fig_queue = Queue()
112
+
113
+ # Initialize after_tasks if not already done
114
+ if not hasattr(parent_frame, 'after_tasks'):
115
+ parent_frame.after_tasks = []
872
116
 
873
- container = tk.PanedWindow(root, orient=tk.HORIZONTAL)
874
- container.pack(fill=tk.BOTH, expand=True)
875
-
876
- scrollable_frame = ScrollableFrame(container, bg='#333333')
877
- container.add(scrollable_frame, stretch="always")
878
-
879
- # Setup input fields
880
- vars_dict = {
881
- 'folder_path': ttk.Entry(scrollable_frame.scrollable_frame),
882
- 'scale_factor': ttk.Entry(scrollable_frame.scrollable_frame),
883
- 'width': ttk.Entry(scrollable_frame.scrollable_frame),
884
- 'height': ttk.Entry(scrollable_frame.scrollable_frame)
885
- }
886
-
887
- # Arrange input fields and labels
888
- row = 0
889
- for name, entry in vars_dict.items():
890
- ttk.Label(scrollable_frame.scrollable_frame, text=f"{name.replace('_', ' ').capitalize()}:").grid(row=row, column=0)
891
- entry.grid(row=row, column=1)
892
- row += 1
893
-
894
- # Function to be called when "Run" button is clicked
895
- def run_app():
896
- folder_path = vars_dict['folder_path'].get()
897
- scale_factor = float(vars_dict['scale_factor'].get())
898
- width = int(vars_dict['width'].get())
899
- height = int(vars_dict['height'].get())
900
-
901
- # Destroy input fields and the "Run" button
902
- root.destroy()
903
-
904
- # Since the original root window is destroyed, create a new root window for the application
905
- new_root = tk.Tk()
906
- new_root.geometry(f"{width}x{height}")
907
- new_root.title("Mask Application")
908
-
909
- # Start the modify_masks application in the new root window
910
- app_instance = modify_masks(new_root, folder_path, scale_factor, width, height)
911
- new_root.mainloop()
912
-
913
- create_dark_mode(root, style, console_output=None)
914
-
915
- run_button = ttk.Button(scrollable_frame.scrollable_frame, text="Run", command=run_app)
916
- run_button.grid(row=row, column=0, columnspan=2, pady=10, padx=10)
917
-
918
- return root
919
-
920
- def gui_make_masks():
921
- root = initiate_mask_app_root(400, 200)
117
+ def _process_fig_queue():
118
+ global canvas
119
+ try:
120
+ while not fig_queue.empty():
121
+ clear_canvas(canvas)
122
+ fig = fig_queue.get_nowait()
123
+ for ax in fig.get_axes():
124
+ ax.set_xticks([]) # Remove x-axis ticks
125
+ ax.set_yticks([]) # Remove y-axis ticks
126
+ ax.xaxis.set_visible(False) # Hide the x-axis
127
+ ax.yaxis.set_visible(False) # Hide the y-axis
128
+ fig.tight_layout()
129
+ fig.set_facecolor('black')
130
+ canvas.figure = fig
131
+ fig_width, fig_height = canvas_widget.winfo_width(), canvas_widget.winfo_height()
132
+ fig.set_size_inches(fig_width / fig.dpi, fig_height / fig.dpi, forward=True)
133
+ canvas.draw_idle()
134
+ except Exception as e:
135
+ traceback.print_exc()
136
+ finally:
137
+ after_id = canvas_widget.after(100, _process_fig_queue)
138
+ parent_frame.after_tasks.append(after_id)
139
+
140
+ def _process_console_queue():
141
+ while not q.empty():
142
+ message = q.get_nowait()
143
+ console_output.insert(tk.END, message)
144
+ console_output.see(tk.END)
145
+ after_id = console_output.after(100, _process_console_queue)
146
+ parent_frame.after_tasks.append(after_id)
147
+
148
+ # Clear previous content if any
149
+ for widget in parent_frame.winfo_children():
150
+ widget.destroy()
151
+
152
+ vertical_container = tk.PanedWindow(parent_frame, orient=tk.HORIZONTAL)
153
+ vertical_container.grid(row=0, column=0, sticky=tk.NSEW)
154
+ parent_frame.grid_rowconfigure(0, weight=1)
155
+ parent_frame.grid_columnconfigure(0, weight=1)
156
+
157
+ # Settings Section
158
+ settings_frame = tk.Frame(vertical_container, bg='black')
159
+ vertical_container.add(settings_frame, stretch="always")
160
+ settings_label = ttk.Label(settings_frame, text="Settings", style="Custom.TLabel")
161
+ settings_label.grid(row=0, column=0, pady=10, padx=10)
162
+ scrollable_frame = ScrollableFrame(settings_frame, width=600)
163
+ scrollable_frame.grid(row=1, column=0, sticky="nsew")
164
+ settings_frame.grid_rowconfigure(1, weight=1)
165
+ settings_frame.grid_columnconfigure(0, weight=1)
166
+
167
+ # Create advanced settings checkbox
168
+ advanced_var = tk.BooleanVar(value=False)
169
+ advanced_Toggle = ToggleSwitch(scrollable_frame.scrollable_frame, text="Advanced Settings", variable=advanced_var, command=toggle_advanced_settings)
170
+ advanced_Toggle.grid(row=48, column=0, pady=10, padx=10)
171
+ variables = mask_variables()
172
+ vars_dict = generate_fields(variables, scrollable_frame)
173
+ toggle_advanced_settings()
174
+ vars_dict['Test mode'] = (None, None, tk.BooleanVar(value=False))
175
+
176
+ # Button section
177
+ test_mode_button = CustomButton(scrollable_frame.scrollable_frame, text="Test Mode", command=toggle_test_mode, font=('Helvetica', 10))
178
+ #CustomButton(buttons_frame, text=app_name, command=lambda app_name=app_name: self.load_app(app_name, app_func), font=('Helvetica', 12))
179
+
180
+ test_mode_button.grid(row=47, column=1, pady=10, padx=10)
181
+ import_btn = CustomButton(scrollable_frame.scrollable_frame, text="Import", command=lambda: import_settings(scrollable_frame), font=('Helvetica', 10))
182
+ import_btn.grid(row=47, column=0, pady=10, padx=10)
183
+ run_button = CustomButton(scrollable_frame.scrollable_frame, text="Run", command=lambda: start_process(q, fig_queue))
184
+ run_button.grid(row=45, column=0, pady=10, padx=10)
185
+ abort_button = CustomButton(scrollable_frame.scrollable_frame, text="Abort", command=initiate_abort, font=('Helvetica', 10))
186
+ abort_button.grid(row=45, column=1, pady=10, padx=10)
187
+ progress_label = ttk.Label(scrollable_frame.scrollable_frame, text="Processing: 0%", background="black", foreground="white")
188
+ progress_label.grid(row=50, column=0, columnspan=2, sticky="ew", pady=(5, 0), padx=10)
189
+
190
+ # Plot Canvas Section
191
+ plot_frame = tk.PanedWindow(vertical_container, orient=tk.VERTICAL)
192
+ vertical_container.add(plot_frame, stretch="always")
193
+ figure = Figure(figsize=(30, 4), dpi=100, facecolor='black')
194
+ plot = figure.add_subplot(111)
195
+ plot.plot([], []) # This creates an empty plot.
196
+ plot.axis('off')
197
+ canvas = FigureCanvasTkAgg(figure, master=plot_frame)
198
+ canvas.get_tk_widget().configure(cursor='arrow', background='black', highlightthickness=0)
199
+ canvas_widget = canvas.get_tk_widget()
200
+ plot_frame.add(canvas_widget, stretch="always")
201
+ canvas.draw()
202
+ canvas.figure = figure
203
+
204
+ # Console Section
205
+ console_frame = tk.Frame(vertical_container, bg='black')
206
+ vertical_container.add(console_frame, stretch="always")
207
+ console_label = ttk.Label(console_frame, text="Console", background="black", foreground="white")
208
+ console_label.grid(row=0, column=0, pady=10, padx=10)
209
+ console_output = scrolledtext.ScrolledText(console_frame, height=10, bg='black', fg='white', insertbackground='white')
210
+ console_output.grid(row=1, column=0, sticky="nsew")
211
+ console_frame.grid_rowconfigure(1, weight=1)
212
+ console_frame.grid_columnconfigure(0, weight=1)
213
+
214
+ q = Queue()
215
+ sys.stdout = StdoutRedirector(console_output)
216
+ sys.stderr = StdoutRedirector(console_output)
217
+
218
+ _process_console_queue()
219
+ _process_fig_queue()
220
+
221
+ after_id = parent_frame.after(100, lambda: main_thread_update_function(parent_frame, q, fig_queue, canvas_widget, progress_label))
222
+ parent_frame.after_tasks.append(after_id)
223
+
224
+ return parent_frame, vars_dict
225
+
226
+ def gui_mask():
227
+ root = tk.Tk()
228
+ width = root.winfo_screenwidth()
229
+ height = root.winfo_screenheight()
230
+ root.geometry(f"{width}x{height}")
231
+ root.title("SpaCr: generate masks")
232
+
233
+ # Clear previous content if any
234
+ if hasattr(root, 'content_frame'):
235
+ for widget in root.content_frame.winfo_children():
236
+ widget.destroy()
237
+ root.content_frame.grid_forget()
238
+ else:
239
+ root.content_frame = tk.Frame(root)
240
+ root.content_frame.grid(row=1, column=0, sticky="nsew")
241
+ root.grid_rowconfigure(1, weight=1)
242
+ root.grid_columnconfigure(0, weight=1)
243
+
244
+ initiate_mask_root(root.content_frame)
245
+ create_menu_bar(root)
922
246
  root.mainloop()
923
247
 
924
248
  if __name__ == "__main__":
925
- gui_make_masks()
249
+ gui_mask()