spacr 0.1.11__py3-none-any.whl → 0.1.12__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/__init__.py CHANGED
@@ -12,14 +12,13 @@ from . import sim
12
12
  from . import sequencing
13
13
  from . import timelapse
14
14
  from . import deep_spacr
15
- from . import annotate_app
16
- from . import annotate_app_v2
15
+ from . import app_annotate
17
16
  from . import gui_utils
18
- from . import gui_make_masks_app
19
- from . import gui_make_masks_app_v2
20
- from . import gui_mask_app
21
- from . import gui_measure_app
22
- from . import gui_classify_app
17
+ from . import app_make_masks
18
+ from . import app_make_masks_v2
19
+ from . import app_mask
20
+ from . import app_measure
21
+ from . import app_classify
23
22
  from . import logger
24
23
 
25
24
 
@@ -34,14 +33,13 @@ __all__ = [
34
33
  "sequencing"
35
34
  "timelapse",
36
35
  "deep_spacr",
37
- "annotate_app",
38
- "annotate_app_v2",
36
+ "app_annotate",
39
37
  "gui_utils",
40
- "gui_make_masks_app",
41
- "gui_make_masks_app_v2",
42
- "gui_mask_app",
43
- "gui_measure_app",
44
- "gui_classify_app",
38
+ "app_make_masks",
39
+ "app_make_masks_v2",
40
+ "app_mask",
41
+ "app_measure",
42
+ "app_classify",
45
43
  "logger"
46
44
  ]
47
45
 
spacr/app_annotate.py ADDED
@@ -0,0 +1,539 @@
1
+ import sqlite3
2
+ from queue import Queue
3
+ from tkinter import Label
4
+ import tkinter as tk
5
+ from tkinter import ttk
6
+ import os, threading, time, sqlite3
7
+ import numpy as np
8
+ from PIL import Image, ImageOps
9
+ from concurrent.futures import ThreadPoolExecutor
10
+ from PIL import ImageTk
11
+ from skimage.exposure import rescale_intensity
12
+ from IPython.display import display, HTML
13
+ from tkinter import font as tkFont
14
+ from tkinter import TclError
15
+
16
+ from .gui_utils import ScrollableFrame, CustomButton, set_dark_style, set_default_font, style_text_boxes, create_menu_bar
17
+
18
+ class ImageApp:
19
+ def __init__(self, root, db_path, src, image_type=None, channels=None, grid_rows=None, grid_cols=None, image_size=(200, 200), annotation_column='annotate', normalize=False, percentiles=(1,99), measurement=None, threshold=None):
20
+ self.root = root
21
+ self.db_path = db_path
22
+ self.src = src
23
+ self.index = 0
24
+ self.grid_rows = grid_rows
25
+ self.grid_cols = grid_cols
26
+ self.image_size = image_size
27
+ self.annotation_column = annotation_column
28
+ self.image_type = image_type
29
+ self.channels = channels
30
+ self.normalize = normalize
31
+ self.percentiles = percentiles
32
+ self.images = {}
33
+ self.pending_updates = {}
34
+ self.labels = []
35
+ self.adjusted_to_original_paths = {}
36
+ self.terminate = False
37
+ self.update_queue = Queue()
38
+ self.status_label = Label(self.root, text="", font=("Arial", 12))
39
+ self.status_label.grid(row=self.grid_rows + 1, column=0, columnspan=self.grid_cols)
40
+ self.measurement = measurement
41
+ self.threshold = threshold
42
+
43
+ self.filtered_paths_annotations = []
44
+ self.prefilter_paths_annotations()
45
+
46
+ self.db_update_thread = threading.Thread(target=self.update_database_worker)
47
+ self.db_update_thread.start()
48
+
49
+ for i in range(grid_rows * grid_cols):
50
+ label = Label(root)
51
+ label.grid(row=i // grid_cols, column=i % grid_cols)
52
+ self.labels.append(label)
53
+
54
+ def prefilter_paths_annotations(self):
55
+ from .io import _read_and_join_tables
56
+ from .utils import is_list_of_lists
57
+
58
+ if self.measurement and self.threshold is not None:
59
+ df = _read_and_join_tables(self.db_path)
60
+ df[self.annotation_column] = None
61
+ before = len(df)
62
+
63
+ if is_list_of_lists(self.measurement):
64
+ if isinstance(self.threshold, list) or is_list_of_lists(self.threshold):
65
+ if len(self.measurement) == len(self.threshold):
66
+ for idx, var in enumerate(self.measurement):
67
+ df = df[df[var[idx]] > self.threshold[idx]]
68
+ after = len(df)
69
+ elif len(self.measurement) == len(self.threshold)*2:
70
+ th_idx = 0
71
+ for idx, var in enumerate(self.measurement):
72
+ if idx % 2 != 0:
73
+ th_idx += 1
74
+ thd = self.threshold
75
+ if isinstance(thd, list):
76
+ thd = thd[0]
77
+ df[f'threshold_measurement_{idx}'] = df[self.measurement[idx]]/df[self.measurement[idx+1]]
78
+ print(f"mean threshold_measurement_{idx}: {np.mean(df['threshold_measurement'])}")
79
+ print(f"median threshold measurement: {np.median(df[self.measurement])}")
80
+ df = df[df[f'threshold_measurement_{idx}'] > thd]
81
+ after = len(df)
82
+ elif isinstance(self.measurement, list):
83
+ df['threshold_measurement'] = df[self.measurement[0]]/df[self.measurement[1]]
84
+ print(f"mean threshold measurement: {np.mean(df['threshold_measurement'])}")
85
+ print(f"median threshold measurement: {np.median(df[self.measurement])}")
86
+ df = df[df['threshold_measurement'] > self.threshold]
87
+ after = len(df)
88
+ self.measurement = 'threshold_measurement'
89
+ print(f'Removed: {before-after} rows, retained {after}')
90
+ else:
91
+ print(f"mean threshold measurement: {np.mean(df[self.measurement])}")
92
+ print(f"median threshold measurement: {np.median(df[self.measurement])}")
93
+ before = len(df)
94
+ if isinstance(self.threshold, str):
95
+ if self.threshold == 'q1':
96
+ self.threshold = df[self.measurement].quantile(0.1)
97
+ if self.threshold == 'q2':
98
+ self.threshold = df[self.measurement].quantile(0.2)
99
+ if self.threshold == 'q3':
100
+ self.threshold = df[self.measurement].quantile(0.3)
101
+ if self.threshold == 'q4':
102
+ self.threshold = df[self.measurement].quantile(0.4)
103
+ if self.threshold == 'q5':
104
+ self.threshold = df[self.measurement].quantile(0.5)
105
+ if self.threshold == 'q6':
106
+ self.threshold = df[self.measurement].quantile(0.6)
107
+ if self.threshold == 'q7':
108
+ self.threshold = df[self.measurement].quantile(0.7)
109
+ if self.threshold == 'q8':
110
+ self.threshold = df[self.measurement].quantile(0.8)
111
+ if self.threshold == 'q9':
112
+ self.threshold = df[self.measurement].quantile(0.9)
113
+ print(f"threshold: {self.threshold}")
114
+
115
+ df = df[df[self.measurement] > self.threshold]
116
+ after = len(df)
117
+ print(f'Removed: {before-after} rows, retained {after}')
118
+
119
+ df = df.dropna(subset=['png_path'])
120
+ if self.image_type:
121
+ before = len(df)
122
+ if isinstance(self.image_type, list):
123
+ for tpe in self.image_type:
124
+ df = df[df['png_path'].str.contains(tpe)]
125
+ else:
126
+ df = df[df['png_path'].str.contains(self.image_type)]
127
+ after = len(df)
128
+ print(f'image_type: Removed: {before-after} rows, retained {after}')
129
+
130
+ self.filtered_paths_annotations = df[['png_path', self.annotation_column]].values.tolist()
131
+ else:
132
+ conn = sqlite3.connect(self.db_path)
133
+ c = conn.cursor()
134
+ if self.image_type:
135
+ c.execute(f"SELECT png_path, {self.annotation_column} FROM png_list WHERE png_path LIKE ?", (f"%{self.image_type}%",))
136
+ else:
137
+ c.execute(f"SELECT png_path, {self.annotation_column} FROM png_list")
138
+ self.filtered_paths_annotations = c.fetchall()
139
+ conn.close()
140
+
141
+ def load_images(self):
142
+ for label in self.labels:
143
+ label.config(image='')
144
+
145
+ self.images = {}
146
+ paths_annotations = self.filtered_paths_annotations[self.index:self.index + self.grid_rows * self.grid_cols]
147
+
148
+ adjusted_paths = []
149
+ for path, annotation in paths_annotations:
150
+ if not path.startswith(self.src):
151
+ parts = path.split('/data/')
152
+ if len(parts) > 1:
153
+ new_path = os.path.join(self.src, 'data', parts[1])
154
+ self.adjusted_to_original_paths[new_path] = path
155
+ adjusted_paths.append((new_path, annotation))
156
+ else:
157
+ adjusted_paths.append((path, annotation))
158
+ else:
159
+ adjusted_paths.append((path, annotation))
160
+
161
+ with ThreadPoolExecutor() as executor:
162
+ loaded_images = list(executor.map(self.load_single_image, adjusted_paths))
163
+
164
+ for i, (img, annotation) in enumerate(loaded_images):
165
+ if annotation:
166
+ border_color = 'teal' if annotation == 1 else 'red'
167
+ img = self.add_colored_border(img, border_width=5, border_color=border_color)
168
+
169
+ photo = ImageTk.PhotoImage(img)
170
+ label = self.labels[i]
171
+ self.images[label] = photo
172
+ label.config(image=photo)
173
+
174
+ path = adjusted_paths[i][0]
175
+ label.bind('<Button-1>', self.get_on_image_click(path, label, img))
176
+ label.bind('<Button-3>', self.get_on_image_click(path, label, img))
177
+
178
+ self.root.update()
179
+
180
+ def load_single_image(self, path_annotation_tuple):
181
+ path, annotation = path_annotation_tuple
182
+ img = Image.open(path)
183
+ img = self.normalize_image(img, self.normalize, self.percentiles)
184
+ img = img.convert('RGB')
185
+ img = self.filter_channels(img)
186
+ img = img.resize(self.image_size)
187
+ return img, annotation
188
+
189
+ @staticmethod
190
+ def normalize_image(img, normalize=False, percentiles=(1, 99)):
191
+ img_array = np.array(img)
192
+
193
+ if normalize:
194
+ if img_array.ndim == 2: # Grayscale image
195
+ p2, p98 = np.percentile(img_array, percentiles)
196
+ img_array = rescale_intensity(img_array, in_range=(p2, p98), out_range=(0, 255))
197
+ else: # Color image or multi-channel image
198
+ for channel in range(img_array.shape[2]):
199
+ p2, p98 = np.percentile(img_array[:, :, channel], percentiles)
200
+ img_array[:, :, channel] = rescale_intensity(img_array[:, :, channel], in_range=(p2, p98), out_range=(0, 255))
201
+
202
+ img_array = np.clip(img_array, 0, 255).astype('uint8')
203
+
204
+ return Image.fromarray(img_array)
205
+
206
+ def add_colored_border(self, img, border_width, border_color):
207
+ top_border = Image.new('RGB', (img.width, border_width), color=border_color)
208
+ bottom_border = Image.new('RGB', (img.width, border_width), color=border_color)
209
+ left_border = Image.new('RGB', (border_width, img.height), color=border_color)
210
+ right_border = Image.new('RGB', (border_width, img.height), color=border_color)
211
+
212
+ bordered_img = Image.new('RGB', (img.width + 2 * border_width, img.height + 2 * border_width), color='white')
213
+ bordered_img.paste(top_border, (border_width, 0))
214
+ bordered_img.paste(bottom_border, (border_width, img.height + border_width))
215
+ bordered_img.paste(left_border, (0, border_width))
216
+ bordered_img.paste(right_border, (img.width + border_width, border_width))
217
+ bordered_img.paste(img, (border_width, border_width))
218
+
219
+ return bordered_img
220
+
221
+ def filter_channels(self, img):
222
+ r, g, b = img.split()
223
+ if self.channels:
224
+ if 'r' not in self.channels:
225
+ r = r.point(lambda _: 0)
226
+ if 'g' not in self.channels:
227
+ g = g.point(lambda _: 0)
228
+ if 'b' not in self.channels:
229
+ b = b.point(lambda _: 0)
230
+
231
+ if len(self.channels) == 1:
232
+ channel_img = r if 'r' in self.channels else (g if 'g' in self.channels else b)
233
+ return ImageOps.grayscale(channel_img)
234
+
235
+ return Image.merge("RGB", (r, g, b))
236
+
237
+ def get_on_image_click(self, path, label, img):
238
+ def on_image_click(event):
239
+ new_annotation = 1 if event.num == 1 else (2 if event.num == 3 else None)
240
+
241
+ original_path = self.adjusted_to_original_paths.get(path, path)
242
+
243
+ if original_path in self.pending_updates and self.pending_updates[original_path] == new_annotation:
244
+ self.pending_updates[original_path] = None
245
+ new_annotation = None
246
+ else:
247
+ self.pending_updates[original_path] = new_annotation
248
+
249
+ print(f"Image {os.path.split(path)[1]} annotated: {new_annotation}")
250
+
251
+ img_ = img.crop((5, 5, img.width-5, img.height-5))
252
+ border_fill = 'teal' if new_annotation == 1 else ('red' if new_annotation == 2 else None)
253
+ img_ = ImageOps.expand(img_, border=5, fill=border_fill) if border_fill else img_
254
+
255
+ photo = ImageTk.PhotoImage(img_)
256
+ self.images[label] = photo
257
+ label.config(image=photo)
258
+ self.root.update()
259
+
260
+ return on_image_click
261
+
262
+ @staticmethod
263
+ def update_html(text):
264
+ display(HTML(f"""
265
+ <script>
266
+ document.getElementById('unique_id').innerHTML = '{text}';
267
+ </script>
268
+ """))
269
+
270
+ def update_database_worker(self):
271
+ conn = sqlite3.connect(self.db_path)
272
+ c = conn.cursor()
273
+
274
+ display(HTML("<div id='unique_id'>Initial Text</div>"))
275
+
276
+ while True:
277
+ if self.terminate:
278
+ conn.close()
279
+ break
280
+
281
+ if not self.update_queue.empty():
282
+ ImageApp.update_html("Do not exit, Updating database...")
283
+ self.status_label.config(text='Do not exit, Updating database...')
284
+
285
+ pending_updates = self.update_queue.get()
286
+ for path, new_annotation in pending_updates.items():
287
+ if new_annotation is None:
288
+ c.execute(f'UPDATE png_list SET {self.annotation_column} = NULL WHERE png_path = ?', (path,))
289
+ else:
290
+ c.execute(f'UPDATE png_list SET {self.annotation_column} = ? WHERE png_path = ?', (new_annotation, path))
291
+ conn.commit()
292
+
293
+ ImageApp.update_html('')
294
+ self.status_label.config(text='')
295
+ self.root.update()
296
+ time.sleep(0.1)
297
+
298
+ def update_gui_text(self, text):
299
+ self.status_label.config(text=text)
300
+ self.root.update()
301
+
302
+ def next_page(self):
303
+ if self.pending_updates:
304
+ self.update_queue.put(self.pending_updates.copy())
305
+ self.pending_updates.clear()
306
+ self.index += self.grid_rows * self.grid_cols
307
+ self.load_images()
308
+
309
+ def previous_page(self):
310
+ if self.pending_updates:
311
+ self.update_queue.put(self.pending_updates.copy())
312
+ self.pending_updates.clear()
313
+ self.index -= self.grid_rows * self.grid_cols
314
+ if self.index < 0:
315
+ self.index = 0
316
+ self.load_images()
317
+
318
+ def shutdown(self):
319
+ self.terminate = True
320
+ self.update_queue.put(self.pending_updates.copy())
321
+ self.pending_updates.clear()
322
+ self.db_update_thread.join()
323
+ self.root.quit()
324
+ self.root.destroy()
325
+ print(f'Quit application')
326
+
327
+ def get_annotate_default_settings(settings):
328
+ settings.setdefault('image_type', 'cell_png')
329
+ settings.setdefault('channels', ['r', 'g', 'b'])
330
+ settings.setdefault('geom', "3200x2000")
331
+ settings.setdefault('img_size', (200, 200))
332
+ settings.setdefault('rows', 10)
333
+ settings.setdefault('columns', 18)
334
+ settings.setdefault('annotation_column', 'recruited_test')
335
+ settings.setdefault('normalize', False)
336
+ settings.setdefault('percentiles', (2,98))
337
+ settings.setdefault('measurement', ['cytoplasm_channel_3_mean_intensity', 'pathogen_channel_3_mean_intensity'])
338
+ settings.setdefault('threshold', 2)
339
+
340
+ return settings
341
+
342
+ def annotate(settings):
343
+ settings = get_annotate_default_settings(settings)
344
+ src = settings['src']
345
+
346
+ db = os.path.join(src, 'measurements/measurements.db')
347
+ conn = sqlite3.connect(db)
348
+ c = conn.cursor()
349
+ c.execute('PRAGMA table_info(png_list)')
350
+ cols = c.fetchall()
351
+ if settings['annotation_column'] not in [col[1] for col in cols]:
352
+ c.execute(f"ALTER TABLE png_list ADD COLUMN {settings['annotation_column']} integer")
353
+ conn.commit()
354
+ conn.close()
355
+
356
+ root = tk.Tk()
357
+ root.geometry(settings['geom'])
358
+ app = ImageApp(root, db, src, image_type=settings['image_type'], channels=settings['channels'], image_size=settings['img_size'], grid_rows=settings['rows'], grid_cols=settings['columns'], annotation_column=settings['annotation_column'], normalize=settings['normalize'], percentiles=settings['percentiles'], measurement=settings['measurement'], threshold=settings['threshold'])
359
+ next_button = tk.Button(root, text="Next", command=app.next_page)
360
+ next_button.grid(row=app.grid_rows, column=app.grid_cols - 1)
361
+ back_button = tk.Button(root, text="Back", command=app.previous_page)
362
+ back_button.grid(row=app.grid_rows, column=app.grid_cols - 2)
363
+ exit_button = tk.Button(root, text="Exit", command=app.shutdown)
364
+ exit_button.grid(row=app.grid_rows, column=app.grid_cols - 3)
365
+
366
+ app.load_images()
367
+ root.mainloop()
368
+
369
+ # Global list to keep references to PhotoImage objects
370
+ global_image_refs = []
371
+
372
+ def initiate_annotation_app_root(parent_frame):
373
+ style = ttk.Style(parent_frame)
374
+ set_dark_style(style)
375
+ style_text_boxes(style)
376
+ set_default_font(parent_frame, font_name="Arial", size=8)
377
+
378
+ parent_frame.configure(bg='black')
379
+
380
+ container = tk.PanedWindow(parent_frame, orient=tk.HORIZONTAL, bg='black')
381
+ container.pack(fill=tk.BOTH, expand=True)
382
+
383
+ scrollable_frame = ScrollableFrame(container, bg='black')
384
+ container.add(scrollable_frame, stretch="always")
385
+
386
+ # Setup input fields
387
+ vars_dict = {
388
+ 'src': ttk.Entry(scrollable_frame.scrollable_frame),
389
+ 'image_type': ttk.Entry(scrollable_frame.scrollable_frame),
390
+ 'channels': ttk.Entry(scrollable_frame.scrollable_frame),
391
+ 'geom': ttk.Entry(scrollable_frame.scrollable_frame),
392
+ 'img_size': ttk.Entry(scrollable_frame.scrollable_frame),
393
+ 'rows': ttk.Entry(scrollable_frame.scrollable_frame),
394
+ 'columns': ttk.Entry(scrollable_frame.scrollable_frame),
395
+ 'annotation_column': ttk.Entry(scrollable_frame.scrollable_frame),
396
+ 'normalize': ttk.Entry(scrollable_frame.scrollable_frame),
397
+ 'percentiles': ttk.Entry(scrollable_frame.scrollable_frame),
398
+ 'measurement': ttk.Entry(scrollable_frame.scrollable_frame),
399
+ 'threshold': ttk.Entry(scrollable_frame.scrollable_frame),
400
+ }
401
+
402
+ default_settings = {
403
+ 'src': 'path',
404
+ 'image_type': 'cell_png',
405
+ 'channels': 'r,g,b',
406
+ 'geom': "3200x2000",
407
+ 'img_size': '(200, 200)',
408
+ 'rows': '10',
409
+ 'columns': '18',
410
+ 'annotation_column': 'recruited_test',
411
+ 'normalize': 'False',
412
+ 'percentiles': '(2,98)',
413
+ 'measurement': 'None',
414
+ 'threshold': 'None'
415
+ }
416
+
417
+ # Arrange input fields and labels
418
+ row = 0
419
+ for name, entry in vars_dict.items():
420
+ ttk.Label(scrollable_frame.scrollable_frame, text=f"{name.replace('_', ' ').capitalize()}:",
421
+ background="black", foreground="white").grid(row=row, column=0)
422
+ entry.insert(0, default_settings[name])
423
+ entry.grid(row=row, column=1)
424
+ row += 1
425
+
426
+ # Function to be called when "Run" button is clicked
427
+ def run_app():
428
+ settings = {key: entry.get() for key, entry in vars_dict.items()}
429
+ settings['channels'] = settings['channels'].split(',')
430
+ settings['img_size'] = tuple(map(int, settings['img_size'][1:-1].split(',')))
431
+ settings['percentiles'] = tuple(map(int, settings['percentiles'][1:-1].split(',')))
432
+ settings['normalize'] = settings['normalize'].lower() == 'true'
433
+ settings['rows'] = int(settings['rows'])
434
+ settings['columns'] = int(settings['columns'])
435
+ if settings['measurement'].lower() == 'none':
436
+ settings['measurement'] = None
437
+ if settings['threshold'].lower() == 'none':
438
+ settings['threshold'] = None
439
+
440
+ # Clear previous content instead of destroying the root
441
+ if hasattr(parent_frame, 'winfo_children'):
442
+ for widget in parent_frame.winfo_children():
443
+ widget.destroy()
444
+
445
+ # Start the annotate application in the same root window
446
+ annotate_app(parent_frame, settings)
447
+
448
+ run_button = CustomButton(scrollable_frame.scrollable_frame, text="Run", command=run_app,
449
+ font=tkFont.Font(family="Arial", size=12, weight=tkFont.NORMAL))
450
+ run_button.grid(row=row, column=0, columnspan=2, pady=10, padx=10)
451
+
452
+ return parent_frame
453
+
454
+ def annotate_app(parent_frame, settings):
455
+ global global_image_refs
456
+ global_image_refs.clear()
457
+ root = parent_frame.winfo_toplevel()
458
+ annotate_with_image_refs(settings, root, lambda: load_next_app(root))
459
+
460
+ def annotate_with_image_refs(settings, root, shutdown_callback):
461
+ from .gui_utils import proceed_with_app
462
+ from .gui import gui_app
463
+
464
+ settings = get_annotate_default_settings(settings)
465
+ src = settings['src']
466
+
467
+ db = os.path.join(src, 'measurements/measurements.db')
468
+ conn = sqlite3.connect(db)
469
+ c = conn.cursor()
470
+ c.execute('PRAGMA table_info(png_list)')
471
+ cols = c.fetchall()
472
+ if settings['annotation_column'] not in [col[1] for col in cols]:
473
+ c.execute(f"ALTER TABLE png_list ADD COLUMN {settings['annotation_column']} integer")
474
+ conn.commit()
475
+ conn.close()
476
+
477
+ app = ImageApp(root, db, src, image_type=settings['image_type'], channels=settings['channels'], image_size=settings['img_size'], grid_rows=settings['rows'], grid_cols=settings['columns'], annotation_column=settings['annotation_column'], normalize=settings['normalize'], percentiles=settings['percentiles'], measurement=settings['measurement'], threshold=settings['threshold'])
478
+
479
+ # Set the canvas background to black
480
+ root.configure(bg='black')
481
+
482
+ next_button = tk.Button(root, text="Next", command=app.next_page, background='black', foreground='white')
483
+ next_button.grid(row=app.grid_rows, column=app.grid_cols - 1)
484
+ back_button = tk.Button(root, text="Back", command=app.previous_page, background='black', foreground='white')
485
+ back_button.grid(row=app.grid_rows, column=app.grid_cols - 2)
486
+ exit_button = tk.Button(root, text="Exit", command=lambda: [app.shutdown(), shutdown_callback()], background='black', foreground='white')
487
+ exit_button.grid(row=app.grid_rows, column=app.grid_cols - 3)
488
+
489
+ app.load_images()
490
+
491
+ # Store the shutdown function and next app details in the root
492
+ root.current_app_exit_func = lambda: [app.shutdown(), shutdown_callback()]
493
+ root.next_app_func = proceed_with_app
494
+ root.next_app_args = ("Main App", gui_app) # Specify the main app function
495
+
496
+ def load_next_app(root):
497
+ # Get the next app function and arguments
498
+ next_app_func = root.next_app_func
499
+ next_app_args = root.next_app_args
500
+
501
+ if next_app_func:
502
+ try:
503
+ if not root.winfo_exists():
504
+ raise tk.TclError
505
+ next_app_func(root, *next_app_args)
506
+ except tk.TclError:
507
+ # Reinitialize root if it has been destroyed
508
+ new_root = tk.Tk()
509
+ width = new_root.winfo_screenwidth()
510
+ height = new_root.winfo_screenheight()
511
+ new_root.geometry(f"{width}x{height}")
512
+ new_root.title("SpaCr Application")
513
+ next_app_func(new_root, *next_app_args)
514
+
515
+
516
+ def gui_annotate():
517
+ root = tk.Tk()
518
+ width = root.winfo_screenwidth()
519
+ height = root.winfo_screenheight()
520
+ root.geometry(f"{width}x{height}")
521
+ root.title("Annotate Application")
522
+
523
+ # Clear previous content if any
524
+ if hasattr(root, 'content_frame'):
525
+ for widget in root.content_frame.winfo_children():
526
+ widget.destroy()
527
+ root.content_frame.grid_forget()
528
+ else:
529
+ root.content_frame = tk.Frame(root)
530
+ root.content_frame.grid(row=1, column=0, sticky="nsew")
531
+ root.grid_rowconfigure(1, weight=1)
532
+ root.grid_columnconfigure(0, weight=1)
533
+
534
+ initiate_annotation_app_root(root.content_frame)
535
+ create_menu_bar(root)
536
+ root.mainloop()
537
+
538
+ if __name__ == "__main__":
539
+ gui_annotate()