spacr 0.1.50__py3-none-any.whl → 0.1.61__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 CHANGED
@@ -1,75 +1,15 @@
1
- import os, spacr, inspect, traceback, io, sys, ast, ctypes, matplotlib, re, csv, requests, ast
2
- import matplotlib.pyplot as plt
3
- matplotlib.use('Agg')
4
- import numpy as np
1
+ import io, sys, ast, ctypes, re, csv, ast
5
2
  import tkinter as tk
6
- from tkinter import ttk, messagebox
7
- import tkinter.font as tkFont
8
- from tkinter import filedialog
9
- from tkinter import Checkbutton
10
- from tkinter import font as tkFont
11
- from torchvision import models
3
+ from tkinter import ttk
12
4
 
13
- from multiprocessing import Process, Value, Queue, Manager, set_start_method
14
- import multiprocessing as mp
15
-
16
- from tkinter import ttk, scrolledtext
17
- from matplotlib.figure import Figure
18
- from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
19
- import time
20
- import requests
21
- from requests.exceptions import HTTPError, Timeout
22
- from huggingface_hub import list_repo_files, hf_hub_download
23
-
24
- from .logger import log_function_call
25
- from .settings import set_default_train_test_model, get_measure_crop_settings, set_default_settings_preprocess_generate_masks
5
+ from . gui_core import initiate_root
6
+ from .gui_elements import spacrLabel, spacrCheckbutton, set_dark_style
26
7
 
27
8
  try:
28
9
  ctypes.windll.shcore.SetProcessDpiAwareness(True)
29
10
  except AttributeError:
30
11
  pass
31
12
 
32
- # Define global variables
33
- q = None
34
- console_output = None
35
- parent_frame = None
36
- vars_dict = None
37
- canvas = None
38
- canvas_widget = None
39
- scrollable_frame = None
40
- progress_label = None
41
- fig_queue = None
42
-
43
- thread_control = {"run_thread": None, "stop_requested": False}
44
-
45
- def set_dark_style(style):
46
- font_style = tkFont.Font(family="Helvetica", size=10)
47
- style.configure('TEntry', padding='5 5 5 5', borderwidth=1, relief='solid', fieldbackground='black', foreground='#ffffff', font=font_style) # Entry
48
- style.configure('TCombobox', fieldbackground='black', background='black', foreground='#ffffff', font=font_style) # Combobox
49
- style.configure('Custom.TButton', background='black', foreground='white', bordercolor='white', focusthickness=3, focuscolor='white', font=('Helvetica', 12))
50
- style.map('Custom.TButton',
51
- background=[('active', 'teal'), ('!active', 'black')],
52
- foreground=[('active', 'white'), ('!active', 'white')],
53
- bordercolor=[('active', 'white'), ('!active', 'white')])
54
- style.configure('Custom.TLabel', padding='5 5 5 5', borderwidth=1, relief='flat', background='black', foreground='#ffffff', font=font_style) # Custom Label
55
- style.configure('TCheckbutton', background='black', foreground='#ffffff', indicatoron=False, relief='flat', font=font_style) # Checkbutton
56
- style.map('TCheckbutton', background=[('selected', '#555555'), ('active', '#555555')])
57
- style.configure('TLabel', background='black', foreground='#ffffff', font=font_style) # Label
58
- style.configure('TFrame', background='black') # Frame
59
- style.configure('TPanedwindow', background='black') # PanedWindow
60
- style.configure('TNotebook', background='black', tabmargins=[2, 5, 2, 0]) # Notebook
61
- style.configure('TNotebook.Tab', background='black', foreground='#ffffff', padding=[5, 5], font=font_style)
62
- style.map('TNotebook.Tab', background=[('selected', '#555555'), ('active', '#555555')])
63
- style.configure('TButton', background='black', foreground='#ffffff', padding='5 5 5 5', font=font_style) # Button (regular)
64
- style.map('TButton', background=[('active', '#555555'), ('disabled', '#333333')])
65
- style.configure('Vertical.TScrollbar', background='black', troughcolor='black', bordercolor='black') # Scrollbar
66
- style.configure('Horizontal.TScrollbar', background='black', troughcolor='black', bordercolor='black') # Scrollbar
67
-
68
- # Define custom LabelFrame style
69
- style.configure('Custom.TLabelFrame', font=('Helvetica', 10, 'bold'), background='black', foreground='white', relief='solid', borderwidth=1)
70
- style.configure('Custom.TLabelFrame.Label', background='black', foreground='white') # Style for the Label inside LabelFrame
71
- style.configure('Custom.TLabelFrame.Label', font=('Helvetica', 10, 'bold'))
72
-
73
13
  def set_default_font(root, font_name="Helvetica", size=12):
74
14
  default_font = (font_name, size)
75
15
  root.option_add("*Font", default_font)
@@ -77,249 +17,6 @@ def set_default_font(root, font_name="Helvetica", size=12):
77
17
  root.option_add("*TLabel.Font", default_font)
78
18
  root.option_add("*TEntry.Font", default_font)
79
19
 
80
- class ScrollableFrame(ttk.Frame):
81
- def __init__(self, container, width=None, *args, bg='black', **kwargs):
82
- super().__init__(container, *args, **kwargs)
83
- self.configure(style='TFrame')
84
- if width is None:
85
- screen_width = self.winfo_screenwidth()
86
- width = screen_width // 4
87
- canvas = tk.Canvas(self, bg=bg, width=width)
88
- scrollbar = ttk.Scrollbar(self, orient="vertical", command=canvas.yview)
89
-
90
- self.scrollable_frame = ttk.Frame(canvas, style='TFrame')
91
- self.scrollable_frame.bind(
92
- "<Configure>",
93
- lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
94
- )
95
- canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
96
- canvas.configure(yscrollcommand=scrollbar.set)
97
-
98
- canvas.grid(row=0, column=0, sticky="nsew")
99
- scrollbar.grid(row=0, column=1, sticky="ns")
100
-
101
- self.grid_rowconfigure(0, weight=1)
102
- self.grid_columnconfigure(0, weight=1)
103
- self.grid_columnconfigure(1, weight=0)
104
-
105
- for child in self.scrollable_frame.winfo_children():
106
- child.configure(bg='black')
107
-
108
- class StdoutRedirector:
109
- def __init__(self, text_widget):
110
- self.text_widget = text_widget
111
-
112
- def write(self, string):
113
- try:
114
- if self.text_widget.winfo_exists():
115
- self.text_widget.insert(tk.END, string)
116
- self.text_widget.see(tk.END)
117
- except tk.TclError:
118
- pass # Handle or log the error as needed
119
-
120
- def flush(self):
121
- pass
122
-
123
- class CustomButton(tk.Frame):
124
- def __init__(self, parent, text="", command=None, font=None, *args, **kwargs):
125
- super().__init__(parent, *args, **kwargs)
126
- self.text = text
127
- self.command = command
128
-
129
- # Detect screen height and calculate button dimensions
130
- screen_height = self.winfo_screenheight()
131
- button_height = screen_height // 50
132
- button_width = button_height * 3
133
-
134
- self.canvas = tk.Canvas(self, width=button_width, height=button_height, highlightthickness=0, bg="black")
135
- self.canvas.grid(row=0, column=0)
136
-
137
- self.button_bg = self.create_rounded_rectangle(0, 0, button_width, button_height, radius=20, fill="#800080")
138
-
139
- # Use the passed font or default to Helvetica if not provided
140
- self.font_style = font if font else tkFont.Font(family="Helvetica", size=12, weight=tkFont.NORMAL)
141
- self.button_text = self.canvas.create_text(button_width // 2, button_height // 2, text=self.text, fill="white", font=self.font_style)
142
-
143
- self.bind("<Enter>", self.on_enter)
144
- self.bind("<Leave>", self.on_leave)
145
- self.bind("<Button-1>", self.on_click)
146
- self.canvas.bind("<Enter>", self.on_enter)
147
- self.canvas.bind("<Leave>", self.on_leave)
148
- self.canvas.bind("<Button-1>", self.on_click)
149
-
150
- def on_enter(self, event=None):
151
- self.canvas.itemconfig(self.button_bg, fill="#993399")
152
-
153
- def on_leave(self, event=None):
154
- self.canvas.itemconfig(self.button_bg, fill="#800080")
155
-
156
- def on_click(self, event=None):
157
- if self.command:
158
- self.command()
159
-
160
- def create_rounded_rectangle(self, x1, y1, x2, y2, radius=20, **kwargs):
161
- points = [x1 + radius, y1,
162
- x1 + radius, y1,
163
- x2 - radius, y1,
164
- x2 - radius, y1,
165
- x2, y1,
166
- x2, y1 + radius,
167
- x2, y1 + radius,
168
- x2, y2 - radius,
169
- x2, y2 - radius,
170
- x2, y2,
171
- x2 - radius, y2,
172
- x2 - radius, y2,
173
- x1 + radius, y2,
174
- x1 + radius, y2,
175
- x1, y2,
176
- x1, y2 - radius,
177
- x1, y2 - radius,
178
- x1, y1 + radius,
179
- x1, y1 + radius,
180
- x1, y1]
181
-
182
- return self.canvas.create_polygon(points, **kwargs, smooth=True)
183
-
184
- class ToggleSwitch(ttk.Frame):
185
- def __init__(self, parent, text="", variable=None, command=None, *args, **kwargs):
186
- super().__init__(parent, *args, **kwargs)
187
- self.text = text
188
- self.variable = variable if variable else tk.BooleanVar()
189
- self.command = command
190
-
191
- self.canvas = tk.Canvas(self, width=40, height=20, highlightthickness=0, bd=0, bg="black")
192
- self.canvas.grid(row=0, column=1, padx=(10, 0))
193
-
194
- # Background rounded rectangle with smaller dimensions and no outline
195
- self.switch_bg = self.create_rounded_rectangle(2, 2, 38, 18, radius=9, outline="", fill="#fff")
196
-
197
- # Switch ball with no outline
198
- self.switch = self.canvas.create_oval(4, 4, 16, 16, outline="", fill="#800080") # Purple initially
199
-
200
- self.label = ttk.Label(self, text=self.text, background="black", foreground="white")
201
- self.label.grid(row=0, column=0, padx=(0, 10))
202
-
203
- self.bind("<Button-1>", self.toggle)
204
- self.canvas.bind("<Button-1>", self.toggle)
205
- self.label.bind("<Button-1>", self.toggle)
206
-
207
- self.update_switch()
208
-
209
- def toggle(self, event=None):
210
- self.variable.set(not self.variable.get())
211
- self.animate_switch()
212
- if self.command:
213
- self.command()
214
-
215
- def update_switch(self):
216
- if self.variable.get():
217
- self.canvas.itemconfig(self.switch, fill="#008080") # Teal
218
- self.canvas.coords(self.switch, 24, 4, 36, 16) # Move switch to the right
219
- else:
220
- self.canvas.itemconfig(self.switch, fill="#800080") # Purple
221
- self.canvas.coords(self.switch, 4, 4, 16, 16) # Move switch to the left
222
-
223
- def animate_switch(self):
224
- if self.variable.get():
225
- start_x, end_x = 4, 24
226
- final_color = "#008080" # Teal
227
- else:
228
- start_x, end_x = 24, 4
229
- final_color = "#800080" # Purple
230
-
231
- self.animate_movement(start_x, end_x, final_color)
232
-
233
- def animate_movement(self, start_x, end_x, final_color):
234
- step = 1 if start_x < end_x else -1
235
- for i in range(start_x, end_x, step):
236
- self.canvas.coords(self.switch, i, 4, i + 12, 16)
237
- self.canvas.update()
238
- self.after(10) # Small delay for smooth animation
239
- self.canvas.itemconfig(self.switch, fill=final_color)
240
-
241
- def get(self):
242
- return self.variable.get()
243
-
244
- def set(self, value):
245
- self.variable.set(value)
246
- self.update_switch()
247
-
248
- def create_rounded_rectangle(self, x1, y1, x2, y2, radius=9, **kwargs): # Smaller radius for smaller switch
249
- points = [x1 + radius, y1,
250
- x1 + radius, y1,
251
- x2 - radius, y1,
252
- x2 - radius, y1,
253
- x2, y1,
254
- x2, y1 + radius,
255
- x2, y1 + radius,
256
- x2, y2 - radius,
257
- x2, y2 - radius,
258
- x2, y2,
259
- x2 - radius, y2,
260
- x2 - radius, y2,
261
- x1 + radius, y2,
262
- x1 + radius, y2,
263
- x1, y2,
264
- x1, y2 - radius,
265
- x1, y2 - radius,
266
- x1, y1 + radius,
267
- x1, y1 + radius,
268
- x1, y1]
269
-
270
- return self.canvas.create_polygon(points, **kwargs, smooth=True)
271
-
272
- class ToolTip:
273
- def __init__(self, widget, text):
274
- self.widget = widget
275
- self.text = text
276
- self.tooltip_window = None
277
- widget.bind("<Enter>", self.show_tooltip)
278
- widget.bind("<Leave>", self.hide_tooltip)
279
-
280
- def show_tooltip(self, event):
281
- x = event.x_root + 20
282
- y = event.y_root + 10
283
- self.tooltip_window = tk.Toplevel(self.widget)
284
- self.tooltip_window.wm_overrideredirect(True)
285
- self.tooltip_window.wm_geometry(f"+{x}+{y}")
286
- label = tk.Label(self.tooltip_window, text=self.text, background="yellow", relief='solid', borderwidth=1)
287
- label.grid(row=0, column=0, padx=5, pady=5)
288
-
289
- def hide_tooltip(self, event):
290
- if self.tooltip_window:
291
- self.tooltip_window.destroy()
292
- self.tooltip_window = None
293
-
294
- def create_menu_bar(root):
295
- from .app_annotate import initiate_annotation_app_root
296
- from .app_make_masks import initiate_mask_app_root
297
-
298
- gui_apps = {
299
- "Mask": 'mask',
300
- "Measure": 'measure',
301
- "Annotate": initiate_annotation_app_root,
302
- "Make Masks": initiate_mask_app_root,
303
- "Classify": 'classify'
304
- }
305
-
306
- def load_app_wrapper(app_name, app_func):
307
- load_app(root, app_name, app_func)
308
-
309
- # Create the menu bar
310
- menu_bar = tk.Menu(root, bg="#008080", fg="white")
311
- # Create a "SpaCr Applications" menu
312
- app_menu = tk.Menu(menu_bar, tearoff=0, bg="#008080", fg="white")
313
- menu_bar.add_cascade(label="SpaCr Applications", menu=app_menu)
314
- # Add options to the "SpaCr Applications" menu
315
- for app_name, app_func in gui_apps.items():
316
- app_menu.add_command(label=app_name, command=lambda app_name=app_name, app_func=app_func: load_app_wrapper(app_name, app_func))
317
- # Add a separator and an exit option
318
- app_menu.add_separator()
319
- app_menu.add_command(label="Exit", command=root.quit)
320
- # Configure the menu for the root window
321
- root.config(menu=menu_bar)
322
-
323
20
  def proceed_with_app(root, app_name, app_func):
324
21
  from .app_annotate import gui_annotate
325
22
  from .app_make_masks import gui_make_masks
@@ -345,6 +42,10 @@ def proceed_with_app(root, app_name, app_func):
345
42
  initiate_root(root.content_frame, 'measure')
346
43
  elif app_name == "Classify":
347
44
  initiate_root(root.content_frame, 'classify')
45
+ elif app_name == "Sequencing":
46
+ initiate_root(root.content_frame, 'sequencing')
47
+ elif app_name == "Umap":
48
+ initiate_root(root.content_frame, 'umap')
348
49
  elif app_name == "Annotate":
349
50
  gui_annotate()
350
51
  elif app_name == "Make Masks":
@@ -367,26 +68,6 @@ def load_app(root, app_name, app_func):
367
68
  else:
368
69
  proceed_with_app(root, app_name, app_func)
369
70
 
370
- def read_settings_from_csv(csv_file_path):
371
- settings = {}
372
- with open(csv_file_path, newline='') as csvfile:
373
- reader = csv.DictReader(csvfile)
374
- for row in reader:
375
- key = row['Key']
376
- value = row['Value']
377
- settings[key] = value
378
- return settings
379
-
380
- def update_settings_from_csv(variables, csv_settings):
381
- new_settings = variables.copy() # Start with a copy of the original settings
382
- for key, value in csv_settings.items():
383
- if key in new_settings:
384
- # Get the variable type and options from the original settings
385
- var_type, options, _ = new_settings[key]
386
- # Update the default value with the CSV value, keeping the type and options unchanged
387
- new_settings[key] = (var_type, options, value)
388
- return new_settings
389
-
390
71
  def parse_list(value):
391
72
  try:
392
73
  parsed_value = ast.literal_eval(value)
@@ -397,490 +78,38 @@ def parse_list(value):
397
78
  except (ValueError, SyntaxError):
398
79
  raise ValueError("Invalid format for list")
399
80
 
81
+ def create_input_field(frame, label_text, row, var_type='entry', options=None, default_value=None):
82
+ label_column = 0
83
+ widget_column = 1
400
84
 
401
- def check_settings(vars_dict):
402
- global q
403
- settings = {}
404
- # Define the expected types for each key, including None where applicable
405
- expected_types = {
406
- "src": str,
407
- "metadata_type": str,
408
- "custom_regex": (str, type(None)),
409
- "experiment": str,
410
- "channels": list,
411
- "magnification": int,
412
- "nucleus_channel": (int, type(None)),
413
- "nucleus_background": int,
414
- "nucleus_Signal_to_noise": float,
415
- "nucleus_CP_prob": float,
416
- "nucleus_FT": float,
417
- "cell_channel": (int, type(None)),
418
- "cell_background": (int, float),
419
- "cell_Signal_to_noise": (int, float),
420
- "cell_CP_prob": (int, float),
421
- "cell_FT": (int, float),
422
- "pathogen_channel": (int, type(None)),
423
- "pathogen_background": (int, float),
424
- "pathogen_Signal_to_noise": (int, float),
425
- "pathogen_CP_prob": (int, float),
426
- "pathogen_FT": (int, float),
427
- "preprocess": bool,
428
- "masks": bool,
429
- "examples_to_plot": int,
430
- "randomize": bool,
431
- "batch_size": int,
432
- "timelapse": bool,
433
- "timelapse_displacement": int,
434
- "timelapse_memory": int,
435
- "timelapse_frame_limits": list, # This can be a list of lists
436
- "timelapse_remove_transient": bool,
437
- "timelapse_mode": str,
438
- "timelapse_objects": list,
439
- "fps": int,
440
- "remove_background": bool,
441
- "lower_percentile": (int, float),
442
- "merge_pathogens": bool,
443
- "normalize_plots": bool,
444
- "all_to_mip": bool,
445
- "pick_slice": bool,
446
- "skip_mode": str,
447
- "save": bool,
448
- "plot": bool,
449
- "workers": int,
450
- "verbose": bool,
451
- "input_folder": str,
452
- "cell_mask_dim": int,
453
- "cell_min_size": int,
454
- "cytoplasm_min_size": int,
455
- "nucleus_mask_dim": int,
456
- "nucleus_min_size": int,
457
- "pathogen_mask_dim": int,
458
- "pathogen_min_size": int,
459
- "save_png": bool,
460
- "crop_mode": list,
461
- "use_bounding_box": bool,
462
- "png_size": list, # This can be a list of lists
463
- "normalize": bool,
464
- "png_dims": list,
465
- "normalize_by": str,
466
- "save_measurements": bool,
467
- "representative_images": bool,
468
- "plot_filtration": bool,
469
- "include_uninfected": bool,
470
- "dialate_pngs": bool,
471
- "dialate_png_ratios": list,
472
- "max_workers": int,
473
- "cells": list,
474
- "cell_loc": list,
475
- "pathogens": list,
476
- "pathogen_loc": (list, list), # This can be a list of lists
477
- "treatments": list,
478
- "treatment_loc": (list, list), # This can be a list of lists
479
- "channel_of_interest": int,
480
- "compartments": list,
481
- "measurement": str,
482
- "nr_imgs": int,
483
- "um_per_pixel": (int, float),
484
- # Additional settings based on provided defaults
485
- "include_noninfected": bool,
486
- "include_multiinfected": bool,
487
- "include_multinucleated": bool,
488
- "filter_min_max": (list, type(None)),
489
- "channel_dims": list,
490
- "backgrounds": list,
491
- "outline_thickness": int,
492
- "outline_color": str,
493
- "overlay_chans": list,
494
- "overlay": bool,
495
- "normalization_percentiles": list,
496
- "print_object_number": bool,
497
- "nr": int,
498
- "figuresize": int,
499
- "cmap": str,
500
- "test_mode": bool,
501
- "test_images": int,
502
- "remove_background_cell": bool,
503
- "remove_background_nucleus": bool,
504
- "remove_background_pathogen": bool,
505
- "pathogen_model": (str, type(None)),
506
- "filter": bool,
507
- "upscale": bool,
508
- "upscale_factor": float,
509
- "adjust_cells": bool,
510
- "row_limit": int,
511
- "tables": list,
512
- "visualize": str,
513
- "image_nr": int,
514
- "dot_size": int,
515
- "n_neighbors": int,
516
- "min_dist": float,
517
- "metric": str,
518
- "eps": float,
519
- "min_samples": int,
520
- "filter_by": str,
521
- "img_zoom": float,
522
- "plot_by_cluster": bool,
523
- "plot_cluster_grids": bool,
524
- "remove_cluster_noise": bool,
525
- "remove_highly_correlated": bool,
526
- "log_data": bool,
527
- "black_background": bool,
528
- "remove_image_canvas": bool,
529
- "plot_outlines": bool,
530
- "plot_points": bool,
531
- "smooth_lines": bool,
532
- "clustering": str,
533
- "exclude": (str, type(None)),
534
- "col_to_compare": str,
535
- "pos": str,
536
- "neg": str,
537
- "embedding_by_controls": bool,
538
- "plot_images": bool,
539
- "reduction_method": str,
540
- "save_figure": bool,
541
- "color_by": (str, type(None)),
542
- "analyze_clusters": bool,
543
- "resnet_features": bool,
544
- "test_nr": int,
545
- "radial_dist": bool,
546
- "calculate_correlation": bool,
547
- "manders_thresholds": list,
548
- "homogeneity": bool,
549
- "homogeneity_distances": list,
550
- "save_arrays": bool,
551
- "cytoplasm": bool,
552
- "merge_edge_pathogen_cells": bool,
553
- "cells_per_well": int,
554
- "pathogen_size_range": list,
555
- "nucleus_size_range": list,
556
- "cell_size_range": list,
557
- "pathogen_intensity_range": list,
558
- "nucleus_intensity_range": list,
559
- "cell_intensity_range": list,
560
- "target_intensity_min": int,
561
- "model_type": str,
562
- "heatmap_feature": str,
563
- "grouping": str,
564
- "min_max": str,
565
- "minimum_cell_count": int,
566
- "n_estimators": int,
567
- "test_size": float,
568
- "location_column": str,
569
- "positive_control": str,
570
- "negative_control": str,
571
- "n_repeats": int,
572
- "top_features": int,
573
- "remove_low_variance_features": bool,
574
- "n_jobs": int,
575
- "classes": list,
576
- "schedule": str,
577
- "loss_type": str,
578
- "image_size": int,
579
- "epochs": int,
580
- "val_split": float,
581
- "train_mode": str,
582
- "learning_rate": float,
583
- "weight_decay": float,
584
- "dropout_rate": float,
585
- "init_weights": bool,
586
- "amsgrad": bool,
587
- "use_checkpoint": bool,
588
- "gradient_accumulation": bool,
589
- "gradient_accumulation_steps": int,
590
- "intermedeate_save": bool,
591
- "pin_memory": bool,
592
- "num_workers": int,
593
- "augment": bool,
594
- "target": str,
595
- "cell_types": list,
596
- "cell_plate_metadata": (list, type(None)),
597
- "pathogen_types": list,
598
- "pathogen_plate_metadata": (list, list), # This can be a list of lists
599
- "treatment_plate_metadata": (list, list), # This can be a list of lists
600
- "metadata_types": list,
601
- "cell_chann_dim": int,
602
- "nucleus_chann_dim": int,
603
- "pathogen_chann_dim": int,
604
- "plot_nr": int,
605
- "plot_control": bool,
606
- "remove_background": bool,
607
- "target": str,
608
- "upstream": str,
609
- "downstream": str,
610
- "barecode_length_1": int,
611
- "barecode_length_2": int,
612
- "chunk_size": int,
613
- "grna": str,
614
- "barcodes": str,
615
- "plate_dict": dict,
616
- "pc": str,
617
- "pc_loc": str,
618
- "nc": str,
619
- "nc_loc": str,
620
- "dependent_variable": str,
621
- "transform": (str, type(None)),
622
- "agg_type": str,
623
- "min_cell_count": int,
624
- "regression_type": str,
625
- "remove_row_column_effect": bool,
626
- "alpha": float,
627
- "fraction_threshold": float,
628
- "class_1_threshold": (float, type(None)),
629
- "batch_size": int,
630
- "CP_prob": float,
631
- "flow_threshold": float,
632
- "percentiles": (list, type(None)),
633
- "circular": bool,
634
- "invert": bool,
635
- "diameter": int,
636
- "grayscale": bool,
637
- "resize": bool,
638
- "target_height": (int, type(None)),
639
- "target_width": (int, type(None)),
640
- "rescale": bool,
641
- "resample": bool,
642
- "model_name": str,
643
- "Signal_to_noise": int,
644
- "learning_rate": float,
645
- "weight_decay": float,
646
- "batch_size": int,
647
- "n_epochs": int,
648
- "from_scratch": bool,
649
- "width_height": list,
650
- "resize": bool,
651
- "gene_weights_csv": str,
652
- "fraction_threshold": float,
653
- }
654
-
655
- for key, (label, widget, var) in vars_dict.items():
656
- if key not in expected_types:
657
- if key not in ["General","Nucleus","Cell","Pathogen","Timelapse","Plot","Object Image","Annotate Data","Measurements","Advanced","Miscellaneous","Test"]:
658
-
659
- q.put(f"Key {key} not found in expected types.")
660
- continue
661
-
662
- value = var.get()
663
- expected_type = expected_types.get(key, str)
664
-
665
- try:
666
- if key in ["png_size", "pathogen_plate_metadata", "treatment_plate_metadata"]:
667
- parsed_value = ast.literal_eval(value) if value else None
668
- if isinstance(parsed_value, list):
669
- if all(isinstance(i, list) for i in parsed_value) or all(not isinstance(i, list) for i in parsed_value):
670
- settings[key] = parsed_value
671
- else:
672
- raise ValueError("Invalid format: Mixed list and list of lists")
673
- else:
674
- raise ValueError("Invalid format for list or list of lists")
675
- elif expected_type == list:
676
- settings[key] = parse_list(value) if value else None
677
- elif expected_type == bool:
678
- settings[key] = value if isinstance(value, bool) else value.lower() in ['true', '1', 't', 'y', 'yes']
679
- elif expected_type == (int, type(None)):
680
- settings[key] = int(value) if value else None
681
- elif expected_type == (float, type(None)):
682
- settings[key] = float(value) if value else None
683
- elif expected_type == (int, float):
684
- settings[key] = float(value) if '.' in value else int(value)
685
- elif expected_type == (str, type(None)):
686
- settings[key] = str(value) if value else None
687
- elif isinstance(expected_type, tuple):
688
- for typ in expected_type:
689
- try:
690
- settings[key] = typ(value) if value else None
691
- break
692
- except (ValueError, TypeError):
693
- continue
694
- else:
695
- raise ValueError
696
- else:
697
- settings[key] = expected_type(value) if value else None
698
- except (ValueError, SyntaxError):
699
- expected_type_name = ' or '.join([t.__name__ for t in expected_type]) if isinstance(expected_type, tuple) else expected_type.__name__
700
- q.put(f"Error: Invalid format for {key}. Expected type: {expected_type_name}.")
701
- return
85
+ # Configure the column widths
86
+ frame.grid_columnconfigure(label_column, weight=0) # Allow the label column to expand
87
+ frame.grid_columnconfigure(widget_column, weight=1) # Allow the widget column to expand
702
88
 
703
- return settings
89
+ # Right-align the label text and the label itself
90
+ label = spacrLabel(frame, text=label_text, background="black", foreground="white", anchor='e', justify='right')
91
+ label.grid(column=label_column, row=row, sticky=tk.E, padx=(5, 2), pady=5) # Align label to the right
704
92
 
705
- def create_input_field(frame, label_text, row, var_type='entry', options=None, default_value=None):
706
- label = ttk.Label(frame, text=label_text, style='Custom.TLabel') # Apply Custom.TLabel style for labels
707
- label.grid(column=0, row=row, sticky=tk.W, padx=5, pady=5)
708
-
709
93
  if var_type == 'entry':
710
94
  var = tk.StringVar(value=default_value) # Set default value
711
95
  entry = ttk.Entry(frame, textvariable=var, style='TEntry') # Apply TEntry style for entries
712
- entry.grid(column=1, row=row, sticky=tk.EW, padx=5)
96
+ entry.grid(column=widget_column, row=row, sticky=tk.W, padx=(2, 5), pady=5) # Align widget to the left
713
97
  return (label, entry, var) # Return both the label and the entry, and the variable
714
98
  elif var_type == 'check':
715
99
  var = tk.BooleanVar(value=default_value) # Set default value (True/False)
716
- check = Checkbutton(frame, text="", variable=var)
717
- check.grid(column=1, row=row, sticky=tk.W, padx=5)
100
+ check = spacrCheckbutton(frame, text="", variable=var, style='TCheckbutton')
101
+ check.grid(column=widget_column, row=row, sticky=tk.W, padx=(2, 5), pady=5) # Align widget to the left
718
102
  return (label, check, var) # Return both the label and the checkbutton, and the variable
719
103
  elif var_type == 'combo':
720
104
  var = tk.StringVar(value=default_value) # Set default value
721
105
  combo = ttk.Combobox(frame, textvariable=var, values=options, style='TCombobox') # Apply TCombobox style
722
- combo.grid(column=1, row=row, sticky=tk.EW, padx=5)
106
+ combo.grid(column=widget_column, row=row, sticky=tk.W, padx=(2, 5), pady=5) # Align widget to the left
723
107
  if default_value:
724
108
  combo.set(default_value)
725
109
  return (label, combo, var) # Return both the label and the combobox, and the variable
726
110
  else:
727
111
  var = None # Placeholder in case of an undefined var_type
728
112
  return (label, None, var)
729
-
730
- def generate_fields(variables, scrollable_frame):
731
- row = 1
732
- vars_dict = {}
733
- tooltips = {
734
- "src": "Path to the folder containing the images.",
735
- "metadata_type": "Type of metadata to expect in the images. This will determine how the images are processed. If 'custom' is selected, you can provide a custom regex pattern to extract metadata from the image names.",
736
- "custom_regex": "Custom regex pattern to extract metadata from the image names. This will only be used if 'custom' is selected for 'metadata_type'.",
737
- "experiment": "Name of the experiment. This will be used to name the output files.",
738
- "channels": "List of channels to use for the analysis. The first channel is 0, the second is 1, and so on. For example, [0,1,2] will use channels 0, 1, and 2.",
739
- "magnification": "At what magnification the images were taken. This will be used to determine the size of the objects in the images.",
740
- "nucleus_channel": "The channel to use for the nucleus. If None, the nucleus will not be segmented.",
741
- "nucleus_background": "The background intensity for the nucleus channel. This will be used to remove background noise.",
742
- "nucleus_Signal_to_noise": "The signal-to-noise ratio for the nucleus channel. This will be used to determine the range of intensities to normalize images to for nucleus segmentation.",
743
- "nucleus_CP_prob": "The cellpose probability threshold for the nucleus channel. This will be used to segment the nucleus.",
744
- "nucleus_FT": "The flow threshold for nucleus objects. This will be used in nuclues segmentation.",
745
- "cell_channel": "The channel to use for the cell. If None, the cell will not be segmented.",
746
- "cell_background": "The background intensity for the cell channel. This will be used to remove background noise.",
747
- "cell_Signal_to_noise": "The signal-to-noise ratio for the cell channel. This will be used to determine the range of intensities to normalize images to for cell segmentation.",
748
- "cell_CP_prob": "The cellpose probability threshold for the cell channel. This will be used in cell segmentation.",
749
- "cell_FT": "The flow threshold for cell objects. This will be used to segment the cells.",
750
- "pathogen_channel": "The channel to use for the pathogen. If None, the pathogen will not be segmented.",
751
- "pathogen_background": "The background intensity for the pathogen channel. This will be used to remove background noise.",
752
- "pathogen_Signal_to_noise": "The signal-to-noise ratio for the pathogen channel. This will be used to determine the range of intensities to normalize images to for pathogen segmentation.",
753
- "pathogen_CP_prob": "The cellpose probability threshold for the pathogen channel. This will be used to segment the pathogen.",
754
- "pathogen_FT": "The flow threshold for pathogen objects. This will be used in pathogen segmentation.",
755
- "preprocess": "Whether to preprocess the images before segmentation. This includes background removal and normalization. Set to False only if this step has already been done.",
756
- "masks": "Whether to generate masks for the segmented objects. If True, masks will be generated for the nucleus, cell, and pathogen.",
757
- "examples_to_plot": "The number of images to plot for each segmented object. This will be used to visually inspect the segmentation results and normalization.",
758
- "randomize": "Whether to randomize the order of the images before processing. Recommended to avoid bias in the segmentation.",
759
- "batch_size": "The batch size to use for processing the images. This will determine how many images are processed at once. Images are normalized and segmented in batches. Lower if application runs out of RAM or VRAM.",
760
- "timelapse": "Whether to process the images as a timelapse.",
761
- "timelapse_displacement": "The displacement between frames in the timelapse. This will be used to align the frames before processing.",
762
- "timelapse_memory": "The number of frames to in tandem objects must be present in to be considered the same object in the timelapse.",
763
- "timelapse_frame_limits": "The frame limits to use for the timelapse. This will determine which frames are processed. For example, [5,20] will process frames 5 to 20.",
764
- "timelapse_remove_transient": "Whether to remove transient objects in the timelapse. Transient objects are present in fewer than all frames.",
765
- "timelapse_mode": "The mode to use for processing the timelapse. 'trackpy' uses the trackpy library for tracking objects, while 'btrack' uses the btrack library.",
766
- "timelapse_objects": "The objects to track in the timelapse (cell, nucleus or pathogen). This will determine which objects are tracked over time. If None, all objects will be tracked.",
767
- "fps": "Frames per second of the automatically generated timelapse movies.",
768
- "remove_background": "Whether to remove background noise from the images. This will help improve the quality of the segmentation.",
769
- "lower_percentile": "The lower quantile to use for normalizing the images. This will be used to determine the range of intensities to normalize images to.",
770
- "merge_pathogens": "Whether to merge pathogen objects that share more than 75% of their perimeter.",
771
- "normalize_plots": "Whether to normalize the plots.",
772
- "all_to_mip": "Whether to convert all images to maximum intensity projections before processing.",
773
- "pick_slice": "Whether to pick a single slice from the z-stack images. If False, the maximum intensity projection will be used.",
774
- "skip_mode": "The mode to use for skipping images. This will determine how to handle images that cannot be processed.",
775
- "save": "Whether to save the results to disk.",
776
- "merge_edge_pathogen_cells": "Whether to merge cells that share pathogen objects.",
777
- "plot": "Whether to plot the results.",
778
- "workers": "The number of workers to use for processing the images. This will determine how many images are processed in parallel. Increase to speed up processing.",
779
- "verbose": "Whether to print verbose output during processing.",
780
- "input_folder": "Path to the folder containing the images.",
781
- "cell_mask_dim": "The dimension of the array the cell mask is saved in.",
782
- "cell_min_size": "The minimum size of cell objects in pixels^2.",
783
- "cytoplasm": "Whether to segment the cytoplasm (Cell - Nucleus + Pathogen).",
784
- "cytoplasm_min_size": "The minimum size of cytoplasm objects in pixels^2.",
785
- "nucleus_mask_dim": "The dimension of the array the nucleus mask is saved in.",
786
- "nucleus_min_size": "The minimum size of nucleus objects in pixels^2.",
787
- "pathogen_mask_dim": "The dimension of the array the pathogen mask is saved in.",
788
- "pathogen_min_size": "The minimum size of pathogen objects in pixels^2.",
789
- "save_png": "Whether to save the segmented objects as PNG images.",
790
- "crop_mode": "The mode to use for cropping the images. This will determine which objects are cropped from the images (cell, nucleus, pathogen, cytoplasm).",
791
- "use_bounding_box": "Whether to use the bounding box of the objects for cropping. If False, only the object itself will be cropped.",
792
- "png_size": "The size of the PNG images to save. This will determine the size of the saved images.",
793
- "normalize": "The percentiles to use for normalizing the images. This will be used to determine the range of intensities to normalize images to. If None, no normalization is done.",
794
- "png_dims": "The dimensions of the PNG images to save. This will determine the dimensions of the saved images. Maximum of 3 dimensions e.g. [1,2,3].",
795
- "normalize_by": "Whether to normalize the images by field of view (fov) or by PNG image (png).",
796
- "save_measurements": "Whether to save the measurements to disk.",
797
- "representative_images": "Whether to save representative images of the segmented objects (Not working yet).",
798
- "plot_filtration": "Whether to plot the filtration steps.",
799
- "include_uninfected": "Whether to include uninfected cells in the analysis.",
800
- "dialate_pngs": "Whether to dilate the PNG images before saving.",
801
- "dialate_png_ratios": "The ratios to use for dilating the PNG images. This will determine the amount of dilation applied to the images before cropping.",
802
- "max_workers": "The number of workers to use for processing the images. This will determine how many images are processed in parallel. Increase to speed up processing.",
803
- "cells": "The cell types to include in the analysis.",
804
- "cell_loc": "The locations of the cell types in the images.",
805
- "pathogens": "The pathogen types to include in the analysis.",
806
- "pathogen_loc": "The locations of the pathogen types in the images.",
807
- "treatments": "The treatments to include in the analysis.",
808
- "treatment_loc": "The locations of the treatments in the images.",
809
- "channel_of_interest": "The channel of interest to use for the analysis.",
810
- "compartments": "The compartments to measure in the images.",
811
- "measurement": "The measurement to use for the analysis.",
812
- "nr_imgs": "The number of images to plot.",
813
- "um_per_pixel": "The micrometers per pixel for the images."
814
- }
815
-
816
- for key, (var_type, options, default_value) in variables.items():
817
- label, widget, var = create_input_field(scrollable_frame.scrollable_frame, key, row, var_type, options, default_value)
818
- vars_dict[key] = (label, widget, var) # Store the label, widget, and variable
819
-
820
- # Add tooltip to the label if it exists in the tooltips dictionary
821
- if key in tooltips:
822
- ToolTip(label, tooltips[key])
823
- row += 1
824
- return vars_dict
825
-
826
- class TextRedirector(object):
827
- def __init__(self, widget, queue):
828
- self.widget = widget
829
- self.queue = queue
830
-
831
- def write(self, str):
832
- self.queue.put(str)
833
-
834
- def flush(self):
835
- pass
836
-
837
- def create_dark_mode(root, style, console_output):
838
- dark_bg = 'black'
839
- light_text = 'white'
840
- dark_text = 'black'
841
- input_bg = '#555555' # Slightly lighter background for input fields
842
-
843
- # Configure ttkcompartments('TFrame', background=dark_bg)
844
- style.configure('TLabel', background=dark_bg, foreground=light_text)
845
- style.configure('TEntry', fieldbackground=input_bg, foreground=dark_text, background=dark_bg)
846
- style.configure('TButton', background=dark_bg, foreground=dark_text)
847
- style.map('TButton', background=[('active', dark_bg)], foreground=[('active', dark_text)])
848
- style.configure('Dark.TCheckbutton', background=dark_bg, foreground=dark_text)
849
- style.map('Dark.TCheckbutton', background=[('active', dark_bg)], foreground=[('active', dark_text)])
850
- style.configure('TCombobox', fieldbackground=input_bg, foreground=dark_text, background=dark_bg, selectbackground=input_bg, selectforeground=dark_text)
851
- style.map('TCombobox', fieldbackground=[('readonly', input_bg)], selectbackground=[('readonly', input_bg)], foreground=[('readonly', dark_text)])
852
-
853
- if console_output != None:
854
- console_output.config(bg=dark_bg, fg=light_text, insertbackground=light_text) #, font=("Helvetica", 12)
855
- root.configure(bg=dark_bg)
856
-
857
- ##@log_function_call
858
- def main_thread_update_function(root, q, fig_queue, canvas_widget, progress_label):
859
- try:
860
- ansi_escape_pattern = re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]')
861
- while not q.empty():
862
- message = q.get_nowait()
863
- clean_message = ansi_escape_pattern.sub('', message)
864
- if clean_message.startswith("Progress"):
865
- progress_label.config(text=clean_message)
866
- if clean_message.startswith("\rProgress"):
867
- progress_label.config(text=clean_message)
868
- elif clean_message.startswith("Successfully"):
869
- progress_label.config(text=clean_message)
870
- elif clean_message.startswith("Processing"):
871
- progress_label.config(text=clean_message)
872
- elif clean_message.startswith("scale"):
873
- pass
874
- elif clean_message.startswith("plot_cropped_arrays"):
875
- pass
876
- elif clean_message == "" or clean_message == "\r" or clean_message.strip() == "":
877
- pass
878
- else:
879
- print(clean_message)
880
- except Exception as e:
881
- print(f"Error updating GUI canvas: {e}")
882
- finally:
883
- root.after(100, lambda: main_thread_update_function(root, q, fig_queue, canvas_widget, progress_label))
884
113
 
885
114
  def process_stdout_stderr(q):
886
115
  """
@@ -903,623 +132,8 @@ class WriteToQueue(io.TextIOBase):
903
132
  def flush(self):
904
133
  pass
905
134
 
906
- def clear_canvas(canvas):
907
- # Clear each plot (axes) in the figure
908
- for ax in canvas.figure.get_axes():
909
- ax.clear()
910
-
911
- # Redraw the now empty canvas without changing its size
912
- canvas.draw_idle()
913
-
914
- def measure_crop_wrapper(settings, q, fig_queue):
915
- """
916
- Wraps the measure_crop function to integrate with GUI processes.
917
-
918
- Parameters:
919
- - settings: dict, The settings for the measure_crop function.
920
- - q: multiprocessing.Queue, Queue for logging messages to the GUI.
921
- - fig_queue: multiprocessing.Queue, Queue for sending figures to the GUI.
922
- """
923
-
924
- def my_show():
925
- """
926
- Replacement for plt.show() that queues figures instead of displaying them.
927
- """
928
- fig = plt.gcf()
929
- fig_queue.put(fig) # Queue the figure for GUI display
930
- plt.close(fig) # Prevent the figure from being shown by plt.show()
931
-
932
- # Temporarily override plt.show
933
- original_show = plt.show
934
- plt.show = my_show
935
-
936
- try:
937
- print('start')
938
- spacr.measure.measure_crop(settings=settings)
939
- except Exception as e:
940
- errorMessage = f"Error during processing: {e}"
941
- q.put(errorMessage) # Send the error message to the GUI via the queue
942
- traceback.print_exc()
943
- finally:
944
- plt.show = original_show # Restore the original plt.show function
945
-
946
- #@log_function_call
947
- def preprocess_generate_masks_wrapper(settings, q, fig_queue):
948
- """
949
- Wraps the measure_crop function to integrate with GUI processes.
950
-
951
- Parameters:
952
- - settings: dict, The settings for the measure_crop function.
953
- - q: multiprocessing.Queue, Queue for logging messages to the GUI.
954
- - fig_queue: multiprocessing.Queue, Queue for sending figures to the GUI.
955
- """
956
-
957
- def my_show():
958
- """
959
- Replacement for plt.show() that queues figures instead of displaying them.
960
- """
961
- fig = plt.gcf()
962
- fig_queue.put(fig) # Queue the figure for GUI display
963
- plt.close(fig) # Prevent the figure from being shown by plt.show()
964
-
965
- # Temporarily override plt.show
966
- original_show = plt.show
967
- plt.show = my_show
968
-
969
- try:
970
- spacr.core.preprocess_generate_masks(src=settings['src'], settings=settings)
971
- except Exception as e:
972
- errorMessage = f"Error during processing: {e}"
973
- q.put(errorMessage) # Send the error message to the GUI via the queue
974
- traceback.print_exc()
975
- finally:
976
- plt.show = original_show # Restore the original plt.show function
977
-
978
- def train_test_model_wrapper(settings, q, fig_queue):
979
- """
980
- Wraps the measure_crop function to integrate with GUI processes.
981
-
982
- Parameters:
983
- - settings: dict, The settings for the measure_crop function.
984
- - q: multiprocessing.Queue, Queue for logging messages to the GUI.
985
- - fig_queue: multiprocessing.Queue, Queue for sending figures to the GUI.
986
- """
987
-
988
- def my_show():
989
- """
990
- Replacement for plt.show() that queues figures instead of displaying them.
991
- """
992
- fig = plt.gcf()
993
- fig_queue.put(fig) # Queue the figure for GUI display
994
- plt.close(fig) # Prevent the figure from being shown by plt.show()
995
-
996
- # Temporarily override plt.show
997
- original_show = plt.show
998
- plt.show = my_show
999
-
1000
- try:
1001
- spacr.core.train_test_model(settings['src'], settings=settings)
1002
- except Exception as e:
1003
- errorMessage = f"Error during processing: {e}"
1004
- q.put(errorMessage) # Send the error message to the GUI via the queue
1005
- traceback.print_exc()
1006
- finally:
1007
- plt.show = original_show # Restore the original plt.show function
1008
-
1009
-
1010
- def run_multiple_simulations_wrapper(settings, q, fig_queue):
1011
- """
1012
- Wraps the run_multiple_simulations function to integrate with GUI processes.
1013
-
1014
- Parameters:
1015
- - settings: dict, The settings for the run_multiple_simulations function.
1016
- - q: multiprocessing.Queue, Queue for logging messages to the GUI.
1017
- - fig_queue: multiprocessing.Queue, Queue for sending figures to the GUI.
1018
- """
1019
-
1020
- def my_show():
1021
- """
1022
- Replacement for plt.show() that queues figures instead of displaying them.
1023
- """
1024
- fig = plt.gcf()
1025
- fig_queue.put(fig) # Queue the figure for GUI display
1026
- plt.close(fig) # Prevent the figure from being shown by plt.show()
1027
-
1028
- # Temporarily override plt.show
1029
- original_show = plt.show
1030
- plt.show = my_show
1031
-
1032
- try:
1033
- spacr.sim.run_multiple_simulations(settings=settings)
1034
- except Exception as e:
1035
- errorMessage = f"Error during processing: {e}"
1036
- q.put(errorMessage) # Send the error message to the GUI via the queue
1037
- traceback.print_exc()
1038
- finally:
1039
- plt.show = original_show # Restore the original plt.show function
1040
-
1041
- def convert_settings_dict_for_gui(settings):
1042
- variables = {}
1043
- special_cases = {
1044
- 'metadata_type': ('combo', ['cellvoyager', 'cq1', 'nikon', 'zeis', 'custom'], 'cellvoyager'),
1045
- 'channels': ('combo', ['[0,1,2,3]', '[0,1,2]', '[0,1]', '[0]'], '[0,1,2,3]'),
1046
- 'cell_mask_dim': ('combo', ['0', '1', '2', '3', '4', '5', '6', '7', '8', None], None),
1047
- 'nucleus_mask_dim': ('combo', ['0', '1', '2', '3', '4', '5', '6', '7', '8', None], None),
1048
- 'pathogen_mask_dim': ('combo', ['0', '1', '2', '3', '4', '5', '6', '7', '8', None], None),
1049
- #'crop_mode': ('combo', ['cell', 'nucleus', 'pathogen', '[cell, nucleus, pathogen]', '[cell,nucleus, pathogen]'], ['cell']),
1050
- 'magnification': ('combo', [20, 40, 60], 20),
1051
- 'nucleus_channel': ('combo', [0, 1, 2, 3, None], None),
1052
- 'cell_channel': ('combo', [0, 1, 2, 3, None], None),
1053
- 'pathogen_channel': ('combo', [0, 1, 2, 3, None], None),
1054
- 'timelapse_mode': ('combo', ['trackpy', 'btrack'], 'trackpy'),
1055
- 'timelapse_objects': ('combo', ['cell', 'nucleus', 'pathogen', 'cytoplasm', None], None),
1056
- 'model_type': ('combo', ['resnet50', 'other_model'], 'resnet50'),
1057
- 'optimizer_type': ('combo', ['adamw', 'adam'], 'adamw'),
1058
- 'schedule': ('combo', ['reduce_lr_on_plateau', 'step_lr'], 'reduce_lr_on_plateau'),
1059
- 'loss_type': ('combo', ['focal_loss', 'binary_cross_entropy_with_logits'], 'focal_loss'),
1060
- 'normalize_by': ('combo', ['fov', 'png'], 'png'),
1061
- }
1062
-
1063
- for key, value in settings.items():
1064
- if key in special_cases:
1065
- variables[key] = special_cases[key]
1066
- elif isinstance(value, bool):
1067
- variables[key] = ('check', None, value)
1068
- elif isinstance(value, int) or isinstance(value, float):
1069
- variables[key] = ('entry', None, value)
1070
- elif isinstance(value, str):
1071
- variables[key] = ('entry', None, value)
1072
- elif value is None:
1073
- variables[key] = ('entry', None, value)
1074
- elif isinstance(value, list):
1075
- variables[key] = ('entry', None, str(value))
1076
- else:
1077
- variables[key] = ('entry', None, str(value))
1078
- return variables
1079
-
1080
- def setup_settings_panel(vertical_container, settings_type='mask', frame_height=500, frame_width=1000):
1081
- global vars_dict, scrollable_frame
1082
- from .settings import set_default_settings_preprocess_generate_masks, get_measure_crop_settings, set_default_train_test_model
1083
-
1084
- print("Setting up settings panel")
1085
-
1086
- # Create settings frame
1087
- settings_frame = tk.Frame(vertical_container, bg='black', height=frame_height, width=frame_width)
1088
- vertical_container.add(settings_frame, stretch="always")
1089
-
1090
- # Add settings label
1091
- settings_label = ttk.Label(settings_frame, text="Settings", style="Custom.TLabel", background="black", foreground="white")
1092
- settings_label.grid(row=0, column=0, pady=10, padx=10)
1093
-
1094
- # Create a ScrollableFrame inside the settings_frame
1095
- scrollable_frame = ScrollableFrame(settings_frame, bg='black', width=frame_width)
1096
- scrollable_frame.grid(row=1, column=0, sticky="nsew")
1097
-
1098
- # Configure the weights for resizing
1099
- settings_frame.grid_rowconfigure(1, weight=1)
1100
- settings_frame.grid_columnconfigure(0, weight=1)
1101
-
1102
- # Load settings based on type
1103
- if settings_type == 'mask':
1104
- settings = set_default_settings_preprocess_generate_masks(src='path', settings={})
1105
- elif settings_type == 'measure':
1106
- settings = get_measure_crop_settings(settings={})
1107
- elif settings_type == 'classify':
1108
- settings = set_default_train_test_model(settings={})
1109
- else:
1110
- raise ValueError(f"Invalid settings type: {settings_type}")
1111
-
1112
- # Generate fields for settings
1113
- variables = convert_settings_dict_for_gui(settings)
1114
- vars_dict = generate_fields(variables, scrollable_frame)
1115
-
1116
- print("Settings panel setup complete")
1117
- return scrollable_frame, vars_dict
1118
-
1119
-
1120
- def setup_plot_section(vertical_container):
1121
- global canvas, canvas_widget
1122
- plot_frame = tk.PanedWindow(vertical_container, orient=tk.VERTICAL)
1123
- vertical_container.add(plot_frame, stretch="always")
1124
- figure = Figure(figsize=(30, 4), dpi=100, facecolor='black')
1125
- plot = figure.add_subplot(111)
1126
- plot.plot([], []) # This creates an empty plot.
1127
- plot.axis('off')
1128
- canvas = FigureCanvasTkAgg(figure, master=plot_frame)
1129
- canvas.get_tk_widget().configure(cursor='arrow', background='black', highlightthickness=0)
1130
- canvas_widget = canvas.get_tk_widget()
1131
- plot_frame.add(canvas_widget, stretch="always")
1132
- canvas.draw()
1133
- canvas.figure = figure
1134
- return canvas, canvas_widget
1135
-
1136
- def download_hug_dataset():
1137
- global vars_dict, q
1138
- repo_id = "einarolafsson/toxo_mito"
1139
- subfolder = "plate1"
1140
- local_dir = os.path.join(os.path.expanduser("~"), "datasets") # Set to the home directory
1141
- try:
1142
- local_path = download_dataset(repo_id, subfolder, local_dir)
1143
- if 'src' in vars_dict:
1144
- vars_dict['src'][2].set(local_path) # Assuming vars_dict['src'] is a tuple and the 3rd element is a StringVar
1145
- q.put(f"Set source path to: {vars_dict['src'][2].get()}\n")
1146
- q.put(f"Dataset downloaded to: {local_path}\n")
1147
- except Exception as e:
1148
- q.put(f"Failed to download dataset: {e}\n")
1149
-
1150
- def download_dataset(repo_id, subfolder, local_dir=None, retries=5, delay=5):
1151
- global q
1152
- """
1153
- Downloads a dataset from Hugging Face and returns the local path.
1154
-
1155
- Args:
1156
- repo_id (str): The repository ID (e.g., 'einarolafsson/toxo_mito').
1157
- subfolder (str): The subfolder path within the repository (e.g., 'plate1').
1158
- local_dir (str): The local directory where the dataset will be saved. Defaults to the user's home directory.
1159
- retries (int): Number of retry attempts in case of failure.
1160
- delay (int): Delay in seconds between retries.
1161
-
1162
- Returns:
1163
- str: The local path to the downloaded dataset.
1164
- """
1165
- if local_dir is None:
1166
- local_dir = os.path.join(os.path.expanduser("~"), "datasets")
1167
-
1168
- local_subfolder_dir = os.path.join(local_dir, subfolder)
1169
- if not os.path.exists(local_subfolder_dir):
1170
- os.makedirs(local_subfolder_dir)
1171
- elif len(os.listdir(local_subfolder_dir)) == 40:
1172
- q.put(f"Dataset already downloaded to: {local_subfolder_dir}")
1173
- return local_subfolder_dir
1174
-
1175
- attempt = 0
1176
- while attempt < retries:
1177
- try:
1178
- files = list_repo_files(repo_id, repo_type="dataset")
1179
- subfolder_files = [file for file in files if file.startswith(subfolder)]
1180
-
1181
- for file_name in subfolder_files:
1182
- for attempt in range(retries):
1183
- try:
1184
- url = f"https://huggingface.co/datasets/{repo_id}/resolve/main/{file_name}?download=true"
1185
- response = requests.get(url, stream=True)
1186
- response.raise_for_status()
1187
-
1188
- local_file_path = os.path.join(local_subfolder_dir, os.path.basename(file_name))
1189
- with open(local_file_path, 'wb') as file:
1190
- for chunk in response.iter_content(chunk_size=8192):
1191
- file.write(chunk)
1192
- q.put(f"Downloaded file: {file_name}")
1193
- break
1194
- except (requests.HTTPError, requests.Timeout) as e:
1195
- q.put(f"Error downloading {file_name}: {e}. Retrying in {delay} seconds...")
1196
- time.sleep(delay)
1197
- else:
1198
- raise Exception(f"Failed to download {file_name} after multiple attempts.")
1199
-
1200
- return local_subfolder_dir
1201
-
1202
- except (requests.HTTPError, requests.Timeout) as e:
1203
- q.put(f"Error downloading dataset: {e}. Retrying in {delay} seconds...")
1204
- attempt += 1
1205
- time.sleep(delay)
1206
-
1207
- raise Exception("Failed to download dataset after multiple attempts.")
1208
-
1209
- def setup_button_section(horizontal_container, settings_type='mask', btn_row=1, settings_row=5, run=True, abort=True, download=True, import_btn=True, progress=True):
1210
- global button_frame, run_button, abort_button, download_dataset_button, import_button, progress_label, q, fig_queue, vars_dict
1211
-
1212
- button_frame = tk.Frame(horizontal_container, bg='black')
1213
- horizontal_container.add(button_frame, stretch="always", sticky="nsew")
1214
- button_frame.grid_rowconfigure(0, weight=0)
1215
- button_frame.grid_rowconfigure(1, weight=1)
1216
- button_frame.grid_columnconfigure(0, weight=1)
1217
-
1218
- categories_label = ttk.Label(button_frame, text="Categories", style="Custom.TLabel", background="black", foreground="white")
1219
- categories_label.grid(row=0, column=0, pady=10, padx=10)
1220
-
1221
- button_scrollable_frame = ScrollableFrame(button_frame, bg='black')
1222
- button_scrollable_frame.grid(row=1, column=0, sticky="nsew")
1223
-
1224
- button_scrollable_frame.scrollable_frame.grid_columnconfigure(0, weight=1, minsize=100)
1225
- button_scrollable_frame.scrollable_frame.grid_columnconfigure(1, weight=1, minsize=100)
1226
- button_scrollable_frame.scrollable_frame.grid_columnconfigure(2, weight=1, minsize=100)
1227
-
1228
- if run:
1229
- run_button = ttk.Button(button_scrollable_frame.scrollable_frame, text="Run", command=lambda: start_process(q, fig_queue, settings_type), style='Custom.TButton')
1230
- run_button.grid(row=btn_row, column=0, pady=5, padx=5, sticky='ew')
1231
- if abort:
1232
- abort_button = ttk.Button(button_scrollable_frame.scrollable_frame, text="Abort", command=initiate_abort, style='Custom.TButton')
1233
- abort_button.grid(row=btn_row, column=1, pady=5, padx=5, sticky='ew')
1234
- btn_row += 1
1235
- if download:
1236
- download_dataset_button = ttk.Button(button_scrollable_frame.scrollable_frame, text="Download", command=download_hug_dataset, style='Custom.TButton')
1237
- download_dataset_button.grid(row=btn_row, column=0, pady=5, padx=5, sticky='ew')
1238
- if import_btn:
1239
- import_button = ttk.Button(button_scrollable_frame.scrollable_frame, text="Import", command=lambda: import_settings(settings_row, settings_type), style='Custom.TButton')
1240
- import_button.grid(row=btn_row, column=1, pady=5, padx=5, sticky='ew')
1241
- btn_row += 1
1242
- if progress:
1243
- progress_label = ttk.Label(button_scrollable_frame.scrollable_frame, text="Processing: 0%", background="black", foreground="white")
1244
- progress_label.grid(row=btn_row, column=0, columnspan=2, sticky="ew", pady=(5, 0), padx=10)
1245
-
1246
- # Call toggle_settings after vars_dict is initialized
1247
- if vars_dict is not None:
1248
- toggle_settings(button_scrollable_frame)
1249
-
1250
- return progress_label
1251
-
1252
-
1253
- def setup_console(vertical_container):
1254
- global console_output
1255
- print("Setting up console frame")
1256
- console_frame = tk.Frame(vertical_container, bg='black')
1257
- vertical_container.add(console_frame, stretch="always")
1258
- console_label = ttk.Label(console_frame, text="Console", background="black", foreground="white")
1259
- console_label.grid(row=0, column=0, pady=10, padx=10)
1260
- console_output = scrolledtext.ScrolledText(console_frame, height=10, bg='black', fg='white', insertbackground='white')
1261
- console_output.grid(row=1, column=0, sticky="nsew")
1262
- console_frame.grid_rowconfigure(1, weight=1)
1263
- console_frame.grid_columnconfigure(0, weight=1)
1264
- print("Console setup complete")
1265
- return console_output
1266
-
1267
- def toggle_test_mode():
1268
- global vars_dict, test_mode_button
1269
- current_state = vars_dict['test_mode'][2].get()
1270
- new_state = not current_state
1271
- vars_dict['test_mode'][2].set(new_state)
1272
- if new_state:
1273
- test_mode_button.config(bg="blue")
1274
- else:
1275
- test_mode_button.config(bg="gray")
1276
-
1277
- def toggle_settings(button_scrollable_frame):
1278
- global vars_dict
1279
-
1280
- if vars_dict is None:
1281
- raise ValueError("vars_dict is not initialized.")
1282
-
1283
- categories = {
1284
- "General": ["src", "input_folder", "metadata_type", "custom_regex", "experiment", "channels", "magnification"],
1285
- "Nucleus": ["nucleus_channel", "nucleus_background", "nucleus_Signal_to_noise", "nucleus_CP_prob", "nucleus_FT", "remove_background_nucleus", "nucleus_min_size", "nucleus_mask_dim", "nucleus_loc"],
1286
- "Cell": ["cell_channel", "cell_background", "cell_Signal_to_noise", "cell_CP_prob", "cell_FT", "remove_background_cell", "cell_min_size", "cell_mask_dim", "cytoplasm", "cytoplasm_min_size", "include_uninfected", "merge_edge_pathogen_cells", "adjust_cells"],
1287
- "Pathogen": ["pathogen_channel", "pathogen_background", "pathogen_Signal_to_noise", "pathogen_CP_prob", "pathogen_FT", "pathogen_model", "remove_background_pathogen", "pathogen_min_size", "pathogen_mask_dim"],
1288
- "Timelapse": ["timelapse", "fps", "timelapse_displacement", "timelapse_memory", "timelapse_frame_limits", "timelapse_remove_transient", "timelapse_mode", "timelapse_objects", "compartments"],
1289
- "Plot": ["plot_filtration", "examples_to_plot", "normalize_plots", "normalize", "cmap", "figuresize", "plot"],
1290
- "Object Image": ["save_png", "dialate_pngs", "dialate_png_ratios", "png_size", "png_dims", "save_arrays", "normalize_by", "dialate_png_ratios", "crop_mode", "dialate_pngs", "normalize", "use_bounding_box"],
1291
- "Annotate Data": ["treatment_loc", "cells", "cell_loc", "pathogens", "pathogen_loc", "channel_of_interest", "measurement", "treatments", "representative_images", "um_per_pixel", "nr_imgs"],
1292
- "Measurements": ["homogeneity", "homogeneity_distances", "radial_dist", "calculate_correlation", "manders_thresholds", "save_measurements"],
1293
- "Advanced": ["preprocess", "remove_background", "normalize", "lower_percentile", "merge_pathogens", "batch_size", "filter", "save", "masks", "verbose", "randomize", "max_workers", "workers"],
1294
- "Miscellaneous": ["all_to_mip", "pick_slice", "skip_mode", "upscale", "upscale_factor"],
1295
- "Test": ["test_mode", "test_images", "random_test", "test_nr"]
1296
- }
1297
-
1298
- def toggle_category(settings, var):
1299
- for setting in settings:
1300
- if setting in vars_dict:
1301
- label, widget, _ = vars_dict[setting]
1302
- if var.get() == 0:
1303
- label.grid_remove()
1304
- widget.grid_remove()
1305
- else:
1306
- label.grid()
1307
- widget.grid()
1308
-
1309
- row = 1
1310
- col = 2 # Start from column 2 to avoid overlap with buttons
1311
- category_idx = 0
1312
-
1313
- for category, settings in categories.items():
1314
- if any(setting in vars_dict for setting in settings):
1315
- category_var = tk.IntVar(value=0)
1316
- vars_dict[category] = (None, None, category_var)
1317
- toggle = ttk.Checkbutton(
1318
- button_scrollable_frame.scrollable_frame,
1319
- text=category,
1320
- variable=category_var,
1321
- command=lambda cat=settings, var=category_var: toggle_category(cat, var),
1322
- style='TCheckbutton'
1323
- )
1324
- toggle.grid(row=row, column=col, sticky="w", pady=2, padx=2)
1325
- col += 1
1326
- category_idx += 1
1327
-
1328
- if category_idx % 4 == 0:
1329
- row += 1
1330
- col = 2 # Reset column to 2
1331
-
1332
- for settings in categories.values():
1333
- for setting in settings:
1334
- if setting in vars_dict:
1335
- label, widget, _ = vars_dict[setting]
1336
- label.grid_remove()
1337
- widget.grid_remove()
1338
-
1339
- def initiate_abort():
1340
- global thread_control
1341
- if thread_control.get("stop_requested") is not None:
1342
- thread_control["stop_requested"].value = 1
1343
-
1344
- if thread_control.get("run_thread") is not None:
1345
- thread_control["run_thread"].join(timeout=5)
1346
- if thread_control["run_thread"].is_alive():
1347
- thread_control["run_thread"].terminate()
1348
- thread_control["run_thread"] = None
1349
-
1350
- def run_mask_gui(settings, q, fig_queue, stop_requested):
1351
- process_stdout_stderr(q)
1352
- try:
1353
- preprocess_generate_masks_wrapper(settings, q, fig_queue)
1354
- except Exception as e:
1355
- q.put(f"Error during processing: {e}")
1356
- traceback.print_exc()
1357
- finally:
1358
- stop_requested.value = 1
1359
-
1360
- def start_process(q, fig_queue, settings_type='mask'):
1361
- global thread_control, vars_dict
1362
- settings = check_settings(vars_dict)
1363
- if thread_control.get("run_thread") is not None:
1364
- initiate_abort()
1365
- stop_requested = Value('i', 0) # multiprocessing shared value for inter-process communication
1366
- thread_control["stop_requested"] = stop_requested
1367
- if settings_type == 'mask':
1368
- thread_control["run_thread"] = Process(target=run_mask_gui, args=(settings, q, fig_queue, stop_requested))
1369
- elif settings_type == 'measure':
1370
- thread_control["run_thread"] = Process(target=run_measure_gui, args=(settings, q, fig_queue, stop_requested))
1371
- elif settings_type == 'classify':
1372
- thread_control["run_thread"] = Process(target=run_classify_gui, args=(settings, q, fig_queue, stop_requested))
1373
- thread_control["run_thread"].start()
1374
-
1375
- def import_settings(settings_type='mask'):
1376
- global vars_dict, scrollable_frame
1377
- csv_file_path = filedialog.askopenfilename(filetypes=[("CSV files", "*.csv")])
1378
- csv_settings = read_settings_from_csv(csv_file_path)
1379
- if settings_type == 'mask':
1380
- settings = set_default_settings_preprocess_generate_masks(src='path', settings={})
1381
- elif settings_type == 'measure':
1382
- settings = get_measure_crop_settings(settings={})
1383
- elif settings_type == 'classify':
1384
- settings = set_default_train_test_model(settings={})
1385
- else:
1386
- raise ValueError(f"Invalid settings type: {settings_type}")
1387
-
1388
- variables = convert_settings_dict_for_gui(settings)
1389
- new_settings = update_settings_from_csv(variables, csv_settings)
1390
- vars_dict = generate_fields(new_settings, scrollable_frame)
1391
-
1392
- def process_fig_queue():
1393
- global canvas, fig_queue, canvas_widget, parent_frame
1394
- try:
1395
- while not fig_queue.empty():
1396
- clear_canvas(canvas)
1397
- fig = fig_queue.get_nowait()
1398
- for ax in fig.get_axes():
1399
- ax.set_xticks([]) # Remove x-axis ticks
1400
- ax.set_yticks([]) # Remove y-axis ticks
1401
- ax.xaxis.set_visible(False) # Hide the x-axis
1402
- ax.yaxis.set_visible(False) # Hide the y-axis
1403
- fig.tight_layout()
1404
- fig.set_facecolor('black')
1405
- canvas.figure = fig
1406
- fig_width, fig_height = canvas_widget.winfo_width(), canvas_widget.winfo_height()
1407
- fig.set_size_inches(fig_width / fig.dpi, fig_height / fig.dpi, forward=True)
1408
- canvas.draw_idle()
1409
- except Exception as e:
1410
- traceback.print_exc()
1411
- finally:
1412
- after_id = canvas_widget.after(100, process_fig_queue)
1413
- parent_frame.after_tasks.append(after_id)
1414
-
1415
- def process_console_queue():
1416
- global q, console_output, parent_frame
1417
- while not q.empty():
1418
- message = q.get_nowait()
1419
- console_output.insert(tk.END, message)
1420
- console_output.see(tk.END)
1421
- after_id = console_output.after(100, process_console_queue)
1422
- parent_frame.after_tasks.append(after_id)
1423
-
1424
- def setup_frame(parent_frame):
1425
- style = ttk.Style(parent_frame)
1426
- set_dark_style(style)
1427
- set_default_font(parent_frame, font_name="Helvetica", size=8)
1428
- parent_frame.configure(bg='black')
1429
- parent_frame.grid_rowconfigure(0, weight=1)
1430
- parent_frame.grid_columnconfigure(0, weight=1)
1431
- vertical_container = tk.PanedWindow(parent_frame, orient=tk.VERTICAL, bg='black')
1432
- vertical_container.grid(row=0, column=0, sticky=tk.NSEW)
1433
- horizontal_container = tk.PanedWindow(vertical_container, orient=tk.HORIZONTAL, bg='black')
1434
- vertical_container.add(horizontal_container, stretch="always")
1435
- horizontal_container.grid_columnconfigure(0, weight=1)
1436
- horizontal_container.grid_columnconfigure(1, weight=1)
1437
- settings_frame = tk.Frame(horizontal_container, bg='black')
1438
- settings_frame.grid_rowconfigure(0, weight=0)
1439
- settings_frame.grid_rowconfigure(1, weight=1)
1440
- settings_frame.grid_columnconfigure(0, weight=1)
1441
- horizontal_container.add(settings_frame, stretch="always", sticky="nsew")
1442
- return parent_frame, vertical_container, horizontal_container
1443
-
1444
- def run_measure_gui(settings, q, fig_queue, stop_requested):
1445
- process_stdout_stderr(q)
1446
- try:
1447
- settings['input_folder'] = settings['src']
1448
- measure_crop_wrapper(settings=settings, q=q, fig_queue=fig_queue)
1449
- except Exception as e:
1450
- q.put(f"Error during processing: {e}")
1451
- traceback.print_exc()
1452
- finally:
1453
- stop_requested.value = 1
1454
-
1455
- def run_classify_gui(settings, q, fig_queue, stop_requested):
1456
- process_stdout_stderr(q)
1457
- try:
1458
- train_test_model_wrapper(settings['src'], settings)
1459
- except Exception as e:
1460
- q.put(f"Error during processing: {e}")
1461
- traceback.print_exc()
1462
- finally:
1463
- stop_requested.value = 1
1464
-
1465
- def set_globals(q_var, console_output_var, parent_frame_var, vars_dict_var, canvas_var, canvas_widget_var, scrollable_frame_var, progress_label_var, fig_queue_var):
1466
- global q, console_output, parent_frame, vars_dict, canvas, canvas_widget, scrollable_frame, progress_label, fig_queue
1467
- q = q_var
1468
- console_output = console_output_var
1469
- parent_frame = parent_frame_var
1470
- vars_dict = vars_dict_var
1471
- canvas = canvas_var
1472
- canvas_widget = canvas_widget_var
1473
- scrollable_frame = scrollable_frame_var
1474
- progress_label = progress_label_var
1475
- fig_queue = fig_queue_var
1476
-
1477
- def initiate_root(parent, settings_type='mask'):
1478
- global q, fig_queue, parent_frame, scrollable_frame, button_frame, vars_dict, canvas, canvas_widget, progress_label
1479
- print("Initializing root with settings_type:", settings_type)
1480
- parent_frame = parent
1481
-
1482
- if not hasattr(parent_frame, 'after_tasks'):
1483
- parent_frame.after_tasks = []
1484
-
1485
- for widget in parent_frame.winfo_children():
1486
- if widget.winfo_exists():
1487
- try:
1488
- widget.destroy()
1489
- except tk.TclError as e:
1490
- print(f"Error destroying widget: {e}")
1491
-
1492
- q = Queue()
1493
- fig_queue = Queue()
1494
- parent_frame, vertical_container, horizontal_container = setup_frame(parent_frame)
1495
- scrollable_frame, vars_dict = setup_settings_panel(horizontal_container, settings_type) # Adjust height and width as needed
1496
- progress_label = setup_button_section(horizontal_container, settings_type)
1497
- canvas, canvas_widget = setup_plot_section(vertical_container)
1498
- console_output = setup_console(vertical_container)
1499
- set_globals(q, console_output, parent_frame, vars_dict, canvas, canvas_widget, scrollable_frame, progress_label, fig_queue)
1500
- process_console_queue()
1501
- process_fig_queue()
1502
- after_id = parent_frame.after(100, lambda: main_thread_update_function(parent_frame, q, fig_queue, canvas_widget, progress_label))
1503
- parent_frame.after_tasks.append(after_id)
1504
- print("Root initialization complete")
1505
- return parent_frame, vars_dict
1506
-
1507
-
1508
135
  def cancel_after_tasks(frame):
1509
136
  if hasattr(frame, 'after_tasks'):
1510
137
  for task in frame.after_tasks:
1511
138
  frame.after_cancel(task)
1512
139
  frame.after_tasks.clear()
1513
-
1514
- def start_gui_app(settings_type='mask'):
1515
- global q, fig_queue, parent_frame, scrollable_frame, vars_dict, canvas, canvas_widget, progress_label
1516
- root = tk.Tk()
1517
- width = root.winfo_screenwidth()
1518
- height = root.winfo_screenheight()
1519
- root.geometry(f"{width}x{height}")
1520
- root.title(f"SpaCr: {settings_type.capitalize()}")
1521
- root.content_frame = tk.Frame(root)
1522
- print("Starting GUI app with settings_type:", settings_type)
1523
- initiate_root(root.content_frame, settings_type)
1524
- create_menu_bar(root)
1525
- root.mainloop()