spacr 0.0.1__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/gui_utils.py ADDED
@@ -0,0 +1,488 @@
1
+ import tkinter as tk
2
+ from tkinter import ttk, messagebox
3
+ from tkinter.font import nametofont
4
+ import matplotlib
5
+ #matplotlib.use('TkAgg')
6
+ matplotlib.use('Agg')
7
+ import ctypes
8
+ import ast
9
+ import matplotlib.pyplot as plt
10
+ import sys
11
+ import io
12
+ import traceback
13
+ import spacr
14
+
15
+ try:
16
+ ctypes.windll.shcore.SetProcessDpiAwareness(True)
17
+ except AttributeError:
18
+ pass
19
+
20
+ from .logger import log_function_call
21
+
22
+ def safe_literal_eval(value):
23
+ try:
24
+ # First, try to evaluate as a literal
25
+ return ast.literal_eval(value)
26
+ except (ValueError, SyntaxError):
27
+ # If it fails, return the value as it is (a string)
28
+ return value
29
+
30
+ def disable_interactivity(fig):
31
+ if hasattr(fig.canvas, 'toolbar'):
32
+ fig.canvas.toolbar.pack_forget()
33
+
34
+ event_handlers = fig.canvas.callbacks.callbacks
35
+ for event, handlers in list(event_handlers.items()):
36
+ for handler_id in list(handlers.keys()):
37
+ fig.canvas.mpl_disconnect(handler_id)
38
+
39
+ def set_default_font(app, font_name="Arial Bold", size=10):
40
+ default_font = nametofont("TkDefaultFont")
41
+ text_font = nametofont("TkTextFont")
42
+ fixed_font = nametofont("TkFixedFont")
43
+
44
+ # Set the family to Open Sans and size as desired
45
+ for font in (default_font, text_font, fixed_font):
46
+ font.config(family=font_name, size=size)
47
+
48
+ class ScrollableFrame(ttk.Frame):
49
+ def __init__(self, container, *args, bg='#333333', **kwargs):
50
+ super().__init__(container, *args, **kwargs)
51
+ self.configure(style='TFrame') # Ensure this uses the styled frame from dark mode
52
+
53
+ canvas = tk.Canvas(self, bg=bg) # Set canvas background to match dark mode
54
+ scrollbar = ttk.Scrollbar(self, orient="vertical", command=canvas.yview)
55
+
56
+ self.scrollable_frame = ttk.Frame(canvas, style='TFrame') # Ensure it uses the styled frame
57
+ self.scrollable_frame.bind(
58
+ "<Configure>",
59
+ lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
60
+ )
61
+
62
+ canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
63
+ canvas.configure(yscrollcommand=scrollbar.set)
64
+
65
+ canvas.pack(side="left", fill="both", expand=True)
66
+ scrollbar.pack(side="right", fill="y")
67
+
68
+ class StdoutRedirector(object):
69
+ def __init__(self, text_widget):
70
+ self.text_widget = text_widget
71
+
72
+ def write(self, string):
73
+ self.text_widget.insert(tk.END, string)
74
+ self.text_widget.see(tk.END)
75
+
76
+ def flush(self):
77
+ pass
78
+
79
+ def check_mask_gui_settings(vars_dict):
80
+ settings = {}
81
+ for key, var in vars_dict.items():
82
+ value = var.get()
83
+
84
+ # Handle conversion for specific types if necessary
85
+ if key in ['channels', 'timelapse_frame_limits']: # Assuming these should be lists
86
+ try:
87
+ # Convert string representation of a list into an actual list
88
+ settings[key] = eval(value)
89
+ except:
90
+ messagebox.showerror("Error", f"Invalid format for {key}. Please enter a valid list.")
91
+ return
92
+ elif key in ['nucleus_channel', 'cell_channel', 'pathogen_channel', 'examples_to_plot', 'batch_size', 'timelapse_memory', 'workers', 'fps', 'magnification']: # Assuming these should be integers
93
+ try:
94
+ settings[key] = int(value) if value else None
95
+ except ValueError:
96
+ messagebox.showerror("Error", f"Invalid number for {key}.")
97
+ return
98
+ elif key in ['nucleus_background', 'cell_background', 'pathogen_background', 'nucleus_Signal_to_noise', 'cell_Signal_to_noise', 'pathogen_Signal_to_noise', 'nucleus_CP_prob', 'cell_CP_prob', 'pathogen_CP_prob', 'lower_quantile']: # Assuming these should be floats
99
+ try:
100
+ settings[key] = float(value) if value else None
101
+ except ValueError:
102
+ messagebox.showerror("Error", f"Invalid number for {key}.")
103
+ return
104
+ else:
105
+ settings[key] = value
106
+ return settings
107
+
108
+ def check_measure_gui_settings_v1(vars_dict):
109
+ settings = {}
110
+ for key, var in vars_dict.items():
111
+ value = var.get() # This retrieves the string representation for entries or the actual value for checkboxes and combos
112
+
113
+ try:
114
+ # Special handling for 'channels' to convert into a list of integers
115
+ if key == 'channels':
116
+ settings[key] = [int(chan) for chan in eval(value)] if value else []
117
+
118
+ elif key == 'png_size':
119
+ temp_val = ast.literal_eval(value) if value else []
120
+ settings[key] = [list(map(int, dim)) for dim in temp_val] if temp_val else None
121
+
122
+ elif key in ['cell_loc', 'pathogen_loc', 'treatment_loc']:
123
+ settings[key] = ast.literal_eval(value) if value else None
124
+
125
+ elif key == 'dialate_png_ratios':
126
+ settings[key] = [float(num) for num in eval(value)] if value else None
127
+
128
+ elif key in ['pathogens', 'treatments', 'cells', 'crop_mode']:
129
+ settings[key] = eval(value) if value else None
130
+
131
+ elif key == 'timelapse_objects':
132
+ # Ensure it's a list of strings
133
+ settings[key] = eval(value) if value else []
134
+
135
+ # Handling for keys that should be treated as strings directly
136
+ elif key in ['normalize_by', 'experiment', 'measurement']:
137
+ settings[key] = str(value) if value else None
138
+
139
+ # Handling for single values that are not strings (int, float, bool)
140
+ elif key in ['cell_mask_dim', 'cell_min_size', 'nucleus_mask_dim', 'nucleus_min_size', 'pathogen_mask_dim', 'pathogen_min_size', 'cytoplasm_min_size', 'max_workers', 'channel_of_interest', 'nr_imgs']:
141
+ settings[key] = int(value) if value else None
142
+
143
+ elif key == 'um_per_pixel':
144
+ settings[key] = float(value) if value else None
145
+
146
+ # Direct handling of boolean values based on checkboxes
147
+ elif key in ['save_png', 'use_bounding_box', 'save_measurements', 'plot', 'plot_filtration', 'include_uninfected', 'dialate_pngs', 'timelapse', 'representative_images']:
148
+ settings[key] = bool(value)
149
+
150
+ else:
151
+ settings[key] = value
152
+
153
+ except SyntaxError as e:
154
+ messagebox.showerror("Error", f"Syntax error processing {key}: {str(e)}")
155
+ return None
156
+ except Exception as e:
157
+ messagebox.showerror("Error", f"Error processing {key}: {str(e)}")
158
+ return None
159
+
160
+ return settings
161
+
162
+ def check_measure_gui_settings(vars_dict):
163
+ settings = {}
164
+ for key, var in vars_dict.items():
165
+ value = var.get() # This retrieves the string representation for entries or the actual value for checkboxes and combos
166
+
167
+ try:
168
+ if key == 'channels' or key == 'png_dims':
169
+ # Converts string representation to a list of integers
170
+ settings[key] = [int(chan) for chan in ast.literal_eval(value)] if value else []
171
+
172
+ elif key in ['cell_loc', 'pathogen_loc', 'treatment_loc']:
173
+ settings[key] = ast.literal_eval(value) if value else None
174
+
175
+ elif key == 'dialate_png_ratios':
176
+ settings[key] = [float(num) for num in eval(value)] if value else None
177
+
178
+ elif key == 'normalize':
179
+ # Converts 'normalize' into a list of two integers
180
+ settings[key] = [int(num) for num in ast.literal_eval(value)] if value else None
181
+
182
+ elif key == 'normalize_by':
183
+ # 'normalize_by' is a string, so directly assign the value
184
+ settings[key] = value
185
+
186
+ elif key == 'png_size':
187
+ # Converts string representation into a list of lists of integers
188
+ temp_val = ast.literal_eval(value) if value else []
189
+ settings[key] = [list(map(int, dim)) for dim in temp_val] if temp_val else None
190
+
191
+ # Handling for other keys as in your original function...
192
+
193
+ elif key in ['pathogens', 'treatments', 'cells', 'crop_mode', 'timelapse_objects']:
194
+ # Ensuring these are evaluated correctly as lists or other structures
195
+ settings[key] = ast.literal_eval(value) if value else None
196
+
197
+ elif key == 'timelapse_objects':
198
+ # Ensure it's a list of strings
199
+ settings[key] = eval(value) if value else []
200
+
201
+ # Handling for keys that should be treated as strings directly
202
+ elif key in ['normalize_by', 'experiment', 'measurement', 'input_folder']:
203
+ settings[key] = str(value) if value else None
204
+
205
+ # Handling for single values that are not strings (int, float, bool)
206
+ elif key in ['cell_mask_dim', 'cell_min_size', 'nucleus_mask_dim', 'nucleus_min_size', 'pathogen_mask_dim', 'pathogen_min_size', 'cytoplasm_min_size', 'max_workers', 'channel_of_interest', 'nr_imgs']:
207
+ settings[key] = int(value) if value else None
208
+
209
+ elif key == 'um_per_pixel':
210
+ settings[key] = float(value) if value else None
211
+
212
+ # Direct handling of boolean values based on checkboxes
213
+ elif key in ['save_png', 'use_bounding_box', 'save_measurements', 'plot', 'plot_filtration', 'include_uninfected', 'dialate_pngs', 'timelapse', 'representative_images']:
214
+ settings[key] = bool(value)
215
+
216
+ except SyntaxError as e:
217
+ messagebox.showerror("Error", f"Syntax error processing {key}: {str(e)}")
218
+ return None
219
+ except Exception as e:
220
+ messagebox.showerror("Error", f"Error processing {key}: {str(e)}")
221
+ return None
222
+
223
+ return settings
224
+
225
+ def measure_variables():
226
+ variables = {
227
+ 'input_folder':('entry', None, '/mnt/data/CellVoyager/40x/einar/mitotrackerHeLaToxoDsRed_20240224_123156/test_gui/merged'),
228
+ 'channels': ('combo', ['[0,1,2,3]','[0,1,2]','[0,1]','[0]'], '[0,1,2,3]'),
229
+ 'cell_mask_dim':('entry', None, 4),
230
+ 'cell_min_size':('entry', None, 0),
231
+ 'nucleus_mask_dim':('entry', None, 5),
232
+ 'nucleus_min_size':('entry', None, 0),
233
+ 'pathogen_mask_dim':('entry', None, 6),
234
+ 'pathogen_min_size':('entry', None, 0),
235
+ 'cytoplasm_min_size':('entry', None, 0),
236
+ 'save_png':('check', None, True),
237
+ 'crop_mode':('entry', None, '["cell"]'),
238
+ 'use_bounding_box':('check', None, True),
239
+ 'png_size': ('entry', None, '[[224,224]]'),
240
+ 'normalize':('entry', None, '[2,98]'),
241
+ 'png_dims':('entry', None, '[1,2,3]'),
242
+ 'normalize_by':('combo', ['fov', 'png'], 'png'),
243
+ 'save_measurements':('check', None, True),
244
+ 'representative_images':('check', None, True),
245
+ 'plot':('check', None, True),
246
+ 'plot_filtration':('check', None, True),
247
+ 'include_uninfected':('check', None, True),
248
+ 'dialate_pngs':('check', None, False),
249
+ 'dialate_png_ratios':('entry', None, '[0.2]'),
250
+ 'timelapse':('check', None, False),
251
+ 'timelapse_objects':('combo', ['["cell"]', '["nucleus"]', '["pathogen"]', '["cytoplasm"]'], '["cell"]'),
252
+ 'max_workers':('entry', None, 30),
253
+ 'experiment':('entry', None, 'experiment name'),
254
+ 'cells':('entry', None, ['HeLa']),
255
+ 'cell_loc': ('entry', None, '[["c1","c2"], ["c3","c4"]]'),
256
+ 'pathogens':('entry', None, '["wt","mutant"]'),
257
+ 'pathogen_loc': ('entry', None, '[["c1","c2"], ["c3","c4"]]'),
258
+ 'treatments': ('entry', None, '["cm","lovastatin_20uM"]'),
259
+ 'treatment_loc': ('entry', None, '[["c1","c2"], ["c3","c4"]]'),
260
+ 'channel_of_interest':('entry', None, 3),
261
+ 'measurement':('entry', None, 'mean_intensity'),
262
+ 'nr_imgs':('entry', None, 32),
263
+ 'um_per_pixel':('entry', None, 0.1)
264
+ }
265
+ return variables
266
+
267
+
268
+ @log_function_call
269
+ def create_input_field(frame, label_text, row, var_type='entry', options=None, default_value=None):
270
+ label = ttk.Label(frame, text=label_text, style='TLabel') # Assuming you have a dark mode style for labels too
271
+ label.grid(column=0, row=row, sticky=tk.W, padx=5, pady=5)
272
+
273
+ if var_type == 'entry':
274
+ var = tk.StringVar(value=default_value) # Set default value
275
+ entry = ttk.Entry(frame, textvariable=var, style='TEntry') # Assuming you have a dark mode style for entries
276
+ entry.grid(column=1, row=row, sticky=tk.EW, padx=5)
277
+ elif var_type == 'check':
278
+ var = tk.BooleanVar(value=default_value) # Set default value (True/False)
279
+ # Use the custom style for Checkbutton
280
+ check = ttk.Checkbutton(frame, variable=var, style='Dark.TCheckbutton')
281
+ check.grid(column=1, row=row, sticky=tk.W, padx=5)
282
+ elif var_type == 'combo':
283
+ var = tk.StringVar(value=default_value) # Set default value
284
+ combo = ttk.Combobox(frame, textvariable=var, values=options, style='TCombobox') # Assuming you have a dark mode style for comboboxes
285
+ combo.grid(column=1, row=row, sticky=tk.EW, padx=5)
286
+ if default_value:
287
+ combo.set(default_value)
288
+ else:
289
+ var = None # Placeholder in case of an undefined var_type
290
+
291
+ return var
292
+
293
+ def add_measure_gui_defaults(settings):
294
+ settings['compartments'] = ['pathogen', 'cytoplasm']
295
+ return settings
296
+
297
+ def mask_variables():
298
+ variables = {
299
+ 'src': ('entry', None, '/mnt/data/CellVoyager/40x/einar/mitotrackerHeLaToxoDsRed_20240224_123156/test_gui'),
300
+ 'metadata_type': ('combo', ['cellvoyager', 'cq1', 'nikon', 'zeis', 'custom'], 'cellvoyager'),
301
+ 'custom_regex': ('entry', None, None),
302
+ 'experiment': ('entry', None, 'exp'),
303
+ 'channels': ('combo', ['[0,1,2,3]','[0,1,2]','[0,1]','[0]'], '[0,1,2,3]'),
304
+ 'magnification': ('combo', [20, 40, 60,], 40),
305
+ 'nucleus_channel': ('combo', [0,1,2,3, None], 0),
306
+ 'nucleus_background': ('entry', None, 100),
307
+ 'nucleus_Signal_to_noise': ('entry', None, 5),
308
+ 'nucleus_CP_prob': ('entry', None, 0),
309
+ 'cell_channel': ('combo', [0,1,2,3, None], 3),
310
+ 'cell_background': ('entry', None, 100),
311
+ 'cell_Signal_to_noise': ('entry', None, 5),
312
+ 'cell_CP_prob': ('entry', None, 0),
313
+ 'pathogen_channel': ('combo', [0,1,2,3, None], 2),
314
+ 'pathogen_background': ('entry', None, 100),
315
+ 'pathogen_Signal_to_noise': ('entry', None, 3),
316
+ 'pathogen_CP_prob': ('entry', None, 0),
317
+ #'preprocess': ('check', None, True),
318
+ #'masks': ('check', None, True),
319
+ #'examples_to_plot': ('entry', None, 1),
320
+ #'randomize': ('check', None, True),
321
+ 'batch_size': ('entry', None, 50),
322
+ 'timelapse': ('check', None, False),
323
+ 'timelapse_displacement': ('entry', None, None),
324
+ 'timelapse_memory': ('entry', None, 0),
325
+ 'timelapse_frame_limits': ('entry', None, '[0,1000]'),
326
+ 'timelapse_remove_transient': ('check', None, True),
327
+ 'timelapse_mode': ('combo', ['trackpy', 'btrack'], 'trackpy'),
328
+ 'timelapse_objects': ('combo', ['cell','nucleus','pathogen','cytoplasm', None], None),
329
+ #'fps': ('entry', None, 2),
330
+ #'remove_background': ('check', None, True),
331
+ 'lower_quantile': ('entry', None, 0.01),
332
+ #'merge': ('check', None, False),
333
+ #'normalize_plots': ('check', None, True),
334
+ #'all_to_mip': ('check', None, False),
335
+ #'pick_slice': ('check', None, False),
336
+ #'skip_mode': ('entry', None, None),
337
+ 'save': ('check', None, True),
338
+ 'plot': ('check', None, True),
339
+ 'workers': ('entry', None, 30),
340
+ 'verbose': ('check', None, True),
341
+ }
342
+ return variables
343
+
344
+ def add_mask_gui_defaults(settings):
345
+ settings['remove_background'] = True
346
+ settings['fps'] = 2
347
+ settings['all_to_mip'] = False
348
+ settings['pick_slice'] = False
349
+ settings['merge'] = False
350
+ settings['skip_mode'] = ''
351
+ settings['verbose'] = False
352
+ settings['normalize_plots'] = True
353
+ settings['randomize'] = True
354
+ settings['preprocess'] = True
355
+ settings['masks'] = True
356
+ settings['examples_to_plot'] = 1
357
+ return settings
358
+
359
+ def generate_fields(variables, scrollable_frame):
360
+ vars_dict = {}
361
+ row = 0
362
+ for key, (var_type, options, default_value) in variables.items():
363
+ vars_dict[key] = create_input_field(scrollable_frame.scrollable_frame, key, row, var_type, options, default_value)
364
+ row += 1
365
+ return vars_dict
366
+
367
+ class TextRedirector(object):
368
+ def __init__(self, widget, queue):
369
+ self.widget = widget
370
+ self.queue = queue
371
+
372
+ def write(self, str):
373
+ self.queue.put(str)
374
+
375
+ def flush(self):
376
+ pass
377
+
378
+ def create_dark_mode(root, style, console_output):
379
+ dark_bg = '#333333'
380
+ light_text = 'white'
381
+ dark_text = 'black'
382
+ input_bg = '#555555' # Slightly lighter background for input fields
383
+
384
+ # Configure ttkcompartments('TFrame', background=dark_bg)
385
+ style.configure('TLabel', background=dark_bg, foreground=light_text)
386
+ style.configure('TEntry', fieldbackground=input_bg, foreground=dark_text, background=dark_bg)
387
+ style.configure('TButton', background=dark_bg, foreground=dark_text)
388
+ style.map('TButton', background=[('active', dark_bg)], foreground=[('active', dark_text)])
389
+ style.configure('Dark.TCheckbutton', background=dark_bg, foreground=dark_text)
390
+ style.map('Dark.TCheckbutton', background=[('active', dark_bg)], foreground=[('active', dark_text)])
391
+ style.configure('TCombobox', fieldbackground=input_bg, foreground=dark_text, background=dark_bg, selectbackground=input_bg, selectforeground=dark_text)
392
+ style.map('TCombobox', fieldbackground=[('readonly', input_bg)], selectbackground=[('readonly', input_bg)], foreground=[('readonly', dark_text)])
393
+
394
+ if console_output != None:
395
+ console_output.config(bg=dark_bg, fg=light_text, insertbackground=light_text) #, font=("Arial", 12)
396
+ root.configure(bg=dark_bg)
397
+
398
+ def set_dark_style(style):
399
+ style.configure('TFrame', background='#333333')
400
+ style.configure('TLabel', background='#333333', foreground='white')
401
+ style.configure('TEntry', background='#333333', foreground='white')
402
+ style.configure('TCheckbutton', background='#333333', foreground='white')
403
+
404
+
405
+
406
+ @log_function_call
407
+ def main_thread_update_function(root, q, fig_queue, canvas_widget, progress_label):
408
+ try:
409
+ while not q.empty():
410
+ message = q.get_nowait()
411
+ if message.startswith("Progress"):
412
+ progress_label.config(text=message)
413
+ elif message == "" or message == "\r":
414
+ pass
415
+ else:
416
+ print(message) # Or handle other messages differently
417
+ # For non-progress messages, you can still print them to the console or handle them as needed.
418
+
419
+ while not fig_queue.empty():
420
+ fig = fig_queue.get_nowait()
421
+ clear_canvas(canvas_widget)
422
+ except Exception as e:
423
+ print(f"Error updating GUI: {e}")
424
+ finally:
425
+ root.after(100, lambda: main_thread_update_function(root, q, fig_queue, canvas_widget, progress_label))
426
+
427
+ def process_stdout_stderr(q):
428
+ """
429
+ Redirect stdout and stderr to the queue q.
430
+ """
431
+ sys.stdout = WriteToQueue(q)
432
+ sys.stderr = WriteToQueue(q)
433
+
434
+ class WriteToQueue(io.TextIOBase):
435
+ """
436
+ A custom file-like class that writes any output to a given queue.
437
+ This can be used to redirect stdout and stderr.
438
+ """
439
+ def __init__(self, q):
440
+ self.q = q
441
+
442
+ def write(self, msg):
443
+ self.q.put(msg)
444
+
445
+ def flush(self):
446
+ pass
447
+
448
+ def clear_canvas(canvas):
449
+ # Clear each plot (axes) in the figure
450
+ for ax in canvas.figure.get_axes():
451
+ ax.clear()
452
+
453
+ # Redraw the now empty canvas without changing its size
454
+ canvas.draw_idle()
455
+
456
+ @log_function_call
457
+ def measure_crop_wrapper(settings, q, fig_queue):
458
+ """
459
+ Wraps the measure_crop function to integrate with GUI processes.
460
+
461
+ Parameters:
462
+ - settings: dict, The settings for the measure_crop function.
463
+ - q: multiprocessing.Queue, Queue for logging messages to the GUI.
464
+ - fig_queue: multiprocessing.Queue, Queue for sending figures to the GUI.
465
+ """
466
+
467
+ def my_show():
468
+ """
469
+ Replacement for plt.show() that queues figures instead of displaying them.
470
+ """
471
+ fig = plt.gcf()
472
+ fig_queue.put(fig) # Queue the figure for GUI display
473
+ plt.close(fig) # Prevent the figure from being shown by plt.show()
474
+
475
+ # Temporarily override plt.show
476
+ original_show = plt.show
477
+ plt.show = my_show
478
+
479
+ try:
480
+ # Assuming spacr.measure.measure_crop is your function that potentially generates matplotlib figures
481
+ # Pass settings as a named argument, along with any other needed arguments
482
+ spacr.measure.measure_crop(settings=settings, annotation_settings={}, advanced_settings={})
483
+ except Exception as e:
484
+ errorMessage = f"Error during processing: {e}"
485
+ q.put(errorMessage) # Send the error message to the GUI via the queue
486
+ traceback.print_exc()
487
+ finally:
488
+ plt.show = original_show # Restore the original plt.show function