spacr 0.1.0__py3-none-any.whl → 0.1.11__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
@@ -15,7 +15,8 @@ from . import deep_spacr
15
15
  from . import annotate_app
16
16
  from . import annotate_app_v2
17
17
  from . import gui_utils
18
- from . import mask_app
18
+ from . import gui_make_masks_app
19
+ from . import gui_make_masks_app_v2
19
20
  from . import gui_mask_app
20
21
  from . import gui_measure_app
21
22
  from . import gui_classify_app
@@ -36,7 +37,8 @@ __all__ = [
36
37
  "annotate_app",
37
38
  "annotate_app_v2",
38
39
  "gui_utils",
39
- "mask_app",
40
+ "gui_make_masks_app",
41
+ "gui_make_masks_app_v2",
40
42
  "gui_mask_app",
41
43
  "gui_measure_app",
42
44
  "gui_classify_app",
spacr/annotate_app.py CHANGED
@@ -1,25 +1,21 @@
1
+ import sqlite3
1
2
  from queue import Queue
2
3
  from tkinter import Label
3
4
  import tkinter as tk
5
+ from tkinter import ttk
4
6
  import os, threading, time, sqlite3
5
7
  import numpy as np
6
8
  from PIL import Image, ImageOps
7
9
  from concurrent.futures import ThreadPoolExecutor
8
10
  from PIL import ImageTk
9
- from IPython.display import display, HTML
10
- import tkinter as tk
11
- from tkinter import ttk
12
- from ttkthemes import ThemedTk
13
11
  from skimage.exposure import rescale_intensity
14
- import cv2
15
- import matplotlib.pyplot as plt
16
-
17
- from .logger import log_function_call
12
+ from IPython.display import display, HTML
13
+ from tkinter import font as tkFont
18
14
 
19
- from .gui_utils import ScrollableFrame, set_default_font, set_dark_style, create_dark_mode, style_text_boxes, create_menu_bar
15
+ from .gui_utils import ScrollableFrame, CustomButton, set_dark_style, set_default_font, style_text_boxes, create_menu_bar
20
16
 
21
17
  class ImageApp:
22
- 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)):
18
+ 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):
23
19
  """
24
20
  Initializes an instance of the ImageApp class.
25
21
 
@@ -34,7 +30,10 @@ class ImageApp:
34
30
  - image_size (tuple): The size of the displayed images.
35
31
  - annotation_column (str): The column name for image annotations in the database.
36
32
  - normalize (bool): Whether to normalize images to their 2nd and 98th percentiles. Defaults to False.
33
+ - measurement (str): The measurement column to filter by.
34
+ - threshold (float): The threshold value for filtering the measurement column.
37
35
  """
36
+
38
37
  self.root = root
39
38
  self.db_path = db_path
40
39
  self.src = src
@@ -55,20 +54,115 @@ class ImageApp:
55
54
  self.update_queue = Queue()
56
55
  self.status_label = Label(self.root, text="", font=("Arial", 12))
57
56
  self.status_label.grid(row=self.grid_rows + 1, column=0, columnspan=self.grid_cols)
57
+ self.measurement = measurement
58
+ self.threshold = threshold
59
+
60
+ self.filtered_paths_annotations = []
61
+ self.prefilter_paths_annotations()
58
62
 
59
63
  self.db_update_thread = threading.Thread(target=self.update_database_worker)
60
64
  self.db_update_thread.start()
61
-
65
+
62
66
  for i in range(grid_rows * grid_cols):
63
67
  label = Label(root)
64
68
  label.grid(row=i // grid_cols, column=i % grid_cols)
65
69
  self.labels.append(label)
66
70
 
71
+ def prefilter_paths_annotations(self):
72
+ """
73
+ Pre-filters the paths and annotations based on the specified measurement and threshold.
74
+ """
75
+ from .io import _read_and_join_tables
76
+ from .utils import is_list_of_lists
77
+
78
+ if self.measurement and self.threshold is not None:
79
+ df = _read_and_join_tables(self.db_path)
80
+ df[self.annotation_column] = None
81
+ before = len(df)
82
+
83
+ if is_list_of_lists(self.measurement):
84
+ if isinstance(self.threshold, list) or is_list_of_lists(self.threshold):
85
+ if len(self.measurement) == len(self.threshold):
86
+ for idx, var in enumerate(self.measurement):
87
+ df = df[df[var[idx]] > self.threshold[idx]]
88
+ after = len(df)
89
+ elif len(self.measurement) == len(self.threshold)*2:
90
+ th_idx = 0
91
+ for idx, var in enumerate(self.measurement):
92
+ if idx % 2 != 0:
93
+ th_idx += 1
94
+ thd = self.threshold
95
+ if isinstance(thd, list):
96
+ thd = thd[0]
97
+ df[f'threshold_measurement_{idx}'] = df[self.measurement[idx]]/df[self.measurement[idx+1]]
98
+ print(f"mean threshold_measurement_{idx}: {np.mean(df['threshold_measurement'])}")
99
+ print(f"median threshold measurement: {np.median(df[self.measurement])}")
100
+ df = df[df[f'threshold_measurement_{idx}'] > thd]
101
+ after = len(df)
102
+ elif isinstance(self.measurement, list):
103
+ df['threshold_measurement'] = df[self.measurement[0]]/df[self.measurement[1]]
104
+ print(f"mean threshold measurement: {np.mean(df['threshold_measurement'])}")
105
+ print(f"median threshold measurement: {np.median(df[self.measurement])}")
106
+ df = df[df['threshold_measurement'] > self.threshold]
107
+ after = len(df)
108
+ self.measurement = 'threshold_measurement'
109
+ print(f'Removed: {before-after} rows, retained {after}')
110
+ else:
111
+ print(f"mean threshold measurement: {np.mean(df[self.measurement])}")
112
+ print(f"median threshold measurement: {np.median(df[self.measurement])}")
113
+ before = len(df)
114
+ if isinstance(self.threshold, str):
115
+ if self.threshold == 'q1':
116
+ self.threshold = df[self.measurement].quantile(0.1)
117
+ if self.threshold == 'q2':
118
+ self.threshold = df[self.measurement].quantile(0.2)
119
+ if self.threshold == 'q3':
120
+ self.threshold = df[self.measurement].quantile(0.3)
121
+ if self.threshold == 'q4':
122
+ self.threshold = df[self.measurement].quantile(0.4)
123
+ if self.threshold == 'q5':
124
+ self.threshold = df[self.measurement].quantile(0.5)
125
+ if self.threshold == 'q6':
126
+ self.threshold = df[self.measurement].quantile(0.6)
127
+ if self.threshold == 'q7':
128
+ self.threshold = df[self.measurement].quantile(0.7)
129
+ if self.threshold == 'q8':
130
+ self.threshold = df[self.measurement].quantile(0.8)
131
+ if self.threshold == 'q9':
132
+ self.threshold = df[self.measurement].quantile(0.9)
133
+ print(f"threshold: {self.threshold}")
134
+
135
+ df = df[df[self.measurement] > self.threshold]
136
+ after = len(df)
137
+ print(f'Removed: {before-after} rows, retained {after}')
138
+
139
+ df = df.dropna(subset=['png_path'])
140
+ if self.image_type:
141
+ before = len(df)
142
+ if isinstance(self.image_type, list):
143
+ for tpe in self.image_type:
144
+ df = df[df['png_path'].str.contains(tpe)]
145
+ else:
146
+ df = df[df['png_path'].str.contains(self.image_type)]
147
+ after = len(df)
148
+ print(f'image_type: Removed: {before-after} rows, retained {after}')
149
+
150
+ self.filtered_paths_annotations = df[['png_path', self.annotation_column]].values.tolist()
151
+ else:
152
+ conn = sqlite3.connect(self.db_path)
153
+ c = conn.cursor()
154
+ if self.image_type:
155
+ c.execute(f"SELECT png_path, {self.annotation_column} FROM png_list WHERE png_path LIKE ?", (f"%{self.image_type}%",))
156
+ else:
157
+ c.execute(f"SELECT png_path, {self.annotation_column} FROM png_list")
158
+ self.filtered_paths_annotations = c.fetchall()
159
+ conn.close()
160
+
67
161
  def load_images(self):
68
162
  """
69
163
  Loads and displays images with annotations.
70
164
 
71
- This method retrieves image paths and annotations from a SQLite database,
165
+ This method retrieves image paths and annotations from a pre-filtered list,
72
166
  loads the images using a ThreadPoolExecutor for parallel processing,
73
167
  adds colored borders to images based on their annotations,
74
168
  and displays the images in the corresponding labels.
@@ -79,23 +173,15 @@ class ImageApp:
79
173
  Returns:
80
174
  None
81
175
  """
176
+
82
177
  for label in self.labels:
83
178
  label.config(image='')
84
179
 
85
180
  self.images = {}
86
-
87
- conn = sqlite3.connect(self.db_path)
88
- c = conn.cursor()
89
- if self.image_type:
90
- c.execute(f"SELECT png_path, {self.annotation_column} FROM png_list WHERE png_path LIKE ? LIMIT ?, ?", (f"%{self.image_type}%", self.index, self.grid_rows * self.grid_cols))
91
- else:
92
- c.execute(f"SELECT png_path, {self.annotation_column} FROM png_list LIMIT ?, ?", (self.index, self.grid_rows * self.grid_cols))
93
-
94
- paths = c.fetchall()
95
- conn.close()
181
+ paths_annotations = self.filtered_paths_annotations[self.index:self.index + self.grid_rows * self.grid_cols]
96
182
 
97
183
  adjusted_paths = []
98
- for path, annotation in paths:
184
+ for path, annotation in paths_annotations:
99
185
  if not path.startswith(self.src):
100
186
  parts = path.split('/data/')
101
187
  if len(parts) > 1:
@@ -209,7 +295,6 @@ class ImageApp:
209
295
 
210
296
  Returns:
211
297
  PIL.Image.Image: The filtered image.
212
-
213
298
  """
214
299
  r, g, b = img.split()
215
300
  if self.channels:
@@ -354,7 +439,7 @@ class ImageApp:
354
439
  """
355
440
  Shuts down the application.
356
441
 
357
- This method sets the `terminate` flag to True, clears the pending updates,
442
+ This method sets the terminate flag to True, clears the pending updates,
358
443
  updates the database, and quits the application.
359
444
 
360
445
  """
@@ -366,10 +451,23 @@ class ImageApp:
366
451
  self.root.destroy()
367
452
  print(f'Quit application')
368
453
 
369
- def annotate(src, image_type=None, channels=None, geom="1000x1100", img_size=(200, 200), rows=5, columns=5, annotation_column='annotate', normalize=False, percentiles=(1,99)):
370
-
371
- from .io import _read_and_join_tables
372
-
454
+ def get_annotate_default_settings(settings):
455
+
456
+ settings.setdefault('image_type', 'cell_png')
457
+ settings.setdefault('channels', ['r', 'g', 'b'])
458
+ settings.setdefault('geom', "3200x2000")
459
+ settings.setdefault('img_size', (200, 200))
460
+ settings.setdefault('rows', 10)
461
+ settings.setdefault('columns', 18)
462
+ settings.setdefault('annotation_column', 'recruited_test')
463
+ settings.setdefault('normalize', False)
464
+ settings.setdefault('percentiles', (2,98))
465
+ settings.setdefault('measurement', ['cytoplasm_channel_3_mean_intensity', 'pathogen_channel_3_mean_intensity'])
466
+ settings.setdefault('threshold', 2)
467
+
468
+ return settings
469
+
470
+ def annotate(settings):
373
471
  """
374
472
  Annotates images in a database using a graphical user interface.
375
473
 
@@ -384,21 +482,26 @@ def annotate(src, image_type=None, channels=None, geom="1000x1100", img_size=(20
384
482
  columns (int, optional): The number of columns in the image grid. Defaults to 5.
385
483
  annotation_column (str, optional): The name of the annotation column in the database table. Defaults to 'annotate'.
386
484
  normalize (bool, optional): Whether to normalize images to their 2nd and 98th percentiles. Defaults to False.
485
+ measurement (str, optional): The measurement column to filter by.
486
+ threshold (float, optional): The threshold value for filtering the measurement column.
387
487
  """
488
+
489
+ settings = get_annotate_default_settings(settings)
490
+ src = settings['src']
491
+
388
492
  db = os.path.join(src, 'measurements/measurements.db')
389
493
  conn = sqlite3.connect(db)
390
494
  c = conn.cursor()
391
495
  c.execute('PRAGMA table_info(png_list)')
392
496
  cols = c.fetchall()
393
- if annotation_column not in [col[1] for col in cols]:
394
- c.execute(f'ALTER TABLE png_list ADD COLUMN {annotation_column} integer')
497
+ if settings['annotation_column'] not in [col[1] for col in cols]:
498
+ c.execute(f"ALTER TABLE png_list ADD COLUMN {settings['annotation_column']} integer")
395
499
  conn.commit()
396
500
  conn.close()
397
501
 
398
-
399
502
  root = tk.Tk()
400
- root.geometry(geom)
401
- app = ImageApp(root, db, src, image_type=image_type, channels=channels, image_size=img_size, grid_rows=rows, grid_cols=columns, annotation_column=annotation_column, normalize=normalize, percentiles=percentiles)
503
+ root.geometry(settings['geom'])
504
+ 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'])
402
505
  next_button = tk.Button(root, text="Next", command=app.next_page)
403
506
  next_button.grid(row=app.grid_rows, column=app.grid_cols - 1)
404
507
  back_button = tk.Button(root, text="Back", command=app.previous_page)
@@ -409,103 +512,159 @@ def annotate(src, image_type=None, channels=None, geom="1000x1100", img_size=(20
409
512
  app.load_images()
410
513
  root.mainloop()
411
514
 
412
- def check_for_duplicates(db):
413
- """
414
- Check for duplicates in the given SQLite database.
515
+ # Global list to keep references to PhotoImage objects
516
+ global_image_refs = []
415
517
 
416
- Args:
417
- db (str): The path to the SQLite database.
418
-
419
- Returns:
420
- None
421
- """
422
- db_path = db
423
- conn = sqlite3.connect(db_path)
424
- c = conn.cursor()
425
- c.execute('SELECT file_name, COUNT(file_name) FROM png_list GROUP BY file_name HAVING COUNT(file_name) > 1')
426
- duplicates = c.fetchall()
427
- for duplicate in duplicates:
428
- file_name = duplicate[0]
429
- count = duplicate[1]
430
- c.execute('SELECT rowid FROM png_list WHERE file_name = ?', (file_name,))
431
- rowids = c.fetchall()
432
- for rowid in rowids[:-1]:
433
- c.execute('DELETE FROM png_list WHERE rowid = ?', (rowid[0],))
434
- conn.commit()
435
- conn.close()
436
-
437
- #@log_function_call
438
- def initiate_annotation_app_root(width, height):
439
- theme = 'breeze'
440
- root = ThemedTk(theme=theme)
441
- style = ttk.Style(root)
518
+ def initiate_annotation_app_root(parent_frame):
519
+ style = ttk.Style(parent_frame)
442
520
  set_dark_style(style)
443
521
  style_text_boxes(style)
444
- set_default_font(root, font_name="Arial", size=8)
445
- root.geometry(f"{width}x{height}")
446
- root.title("Annotation App")
522
+ set_default_font(parent_frame, font_name="Arial", size=8)
447
523
 
448
- container = tk.PanedWindow(root, orient=tk.HORIZONTAL)
524
+ parent_frame.configure(bg='black')
525
+
526
+ container = tk.PanedWindow(parent_frame, orient=tk.HORIZONTAL, bg='black')
449
527
  container.pack(fill=tk.BOTH, expand=True)
450
528
 
451
- scrollable_frame = ScrollableFrame(container, bg='#333333')
529
+ scrollable_frame = ScrollableFrame(container, bg='black')
452
530
  container.add(scrollable_frame, stretch="always")
453
531
 
454
532
  # Setup input fields
455
533
  vars_dict = {
456
- 'db': ttk.Entry(scrollable_frame.scrollable_frame),
534
+ 'src': ttk.Entry(scrollable_frame.scrollable_frame),
457
535
  'image_type': ttk.Entry(scrollable_frame.scrollable_frame),
458
536
  'channels': ttk.Entry(scrollable_frame.scrollable_frame),
459
- 'annotation_column': ttk.Entry(scrollable_frame.scrollable_frame),
460
537
  'geom': ttk.Entry(scrollable_frame.scrollable_frame),
461
538
  'img_size': ttk.Entry(scrollable_frame.scrollable_frame),
462
539
  'rows': ttk.Entry(scrollable_frame.scrollable_frame),
463
- 'columns': ttk.Entry(scrollable_frame.scrollable_frame)
540
+ 'columns': ttk.Entry(scrollable_frame.scrollable_frame),
541
+ 'annotation_column': ttk.Entry(scrollable_frame.scrollable_frame),
542
+ 'normalize': ttk.Entry(scrollable_frame.scrollable_frame),
543
+ 'percentiles': ttk.Entry(scrollable_frame.scrollable_frame),
544
+ 'measurement': ttk.Entry(scrollable_frame.scrollable_frame),
545
+ 'threshold': ttk.Entry(scrollable_frame.scrollable_frame),
546
+ }
547
+
548
+ default_settings = {
549
+ 'src': 'path',
550
+ 'image_type': 'cell_png',
551
+ 'channels': 'r,g,b',
552
+ 'geom': "3200x2000",
553
+ 'img_size': '(200, 200)',
554
+ 'rows': '10',
555
+ 'columns': '18',
556
+ 'annotation_column': 'recruited_test',
557
+ 'normalize': 'False',
558
+ 'percentiles': '(2,98)',
559
+ 'measurement': 'None',
560
+ 'threshold': 'None'
464
561
  }
465
562
 
466
563
  # Arrange input fields and labels
467
564
  row = 0
468
565
  for name, entry in vars_dict.items():
469
- ttk.Label(scrollable_frame.scrollable_frame, text=f"{name.replace('_', ' ').capitalize()}:").grid(row=row, column=0)
566
+ ttk.Label(scrollable_frame.scrollable_frame, text=f"{name.replace('_', ' ').capitalize()}:",
567
+ background="black", foreground="white").grid(row=row, column=0)
568
+ entry.insert(0, default_settings[name])
470
569
  entry.grid(row=row, column=1)
471
570
  row += 1
472
571
 
473
572
  # Function to be called when "Run" button is clicked
474
573
  def run_app():
475
- db = vars_dict['db'].get()
476
- image_type = vars_dict['image_type'].get()
477
- channels = vars_dict['channels'].get()
478
- annotation_column = vars_dict['annotation_column'].get()
479
- geom = vars_dict['geom'].get()
480
- img_size_str = vars_dict['img_size'].get().split(',') # Splitting the string by comma
481
- img_size = (int(img_size_str[0]), int(img_size_str[1])) # Converting each part to an integer
482
- rows = int(vars_dict['rows'].get())
483
- columns = int(vars_dict['columns'].get())
574
+ settings = {key: entry.get() for key, entry in vars_dict.items()}
575
+ settings['channels'] = settings['channels'].split(',')
576
+ settings['img_size'] = tuple(map(int, settings['img_size'][1:-1].split(',')))
577
+ settings['percentiles'] = tuple(map(int, settings['percentiles'][1:-1].split(',')))
578
+ settings['normalize'] = settings['normalize'].lower() == 'true'
579
+ settings['rows'] = int(settings['rows'])
580
+ settings['columns'] = int(settings['columns'])
581
+ if settings['measurement'].lower() == 'none':
582
+ settings['measurement'] = None
583
+ if settings['threshold'].lower() == 'none':
584
+ settings['threshold'] = None
484
585
 
485
- # Destroy the initial settings window
486
- root.destroy()
586
+ # Clear previous content instead of destroying the root
587
+ if hasattr(parent_frame, 'winfo_children'):
588
+ for widget in parent_frame.winfo_children():
589
+ widget.destroy()
487
590
 
488
- # Create a new root window for the application
489
- new_root = tk.Tk()
490
- new_root.geometry(f"{width}x{height}")
491
- new_root.title("Mask Application")
591
+ # Start the annotate application in the same root window
592
+ annotate_app(parent_frame, settings)
492
593
 
594
+ run_button = CustomButton(scrollable_frame.scrollable_frame, text="Run", command=run_app,
595
+ font=tkFont.Font(family="Arial", size=12, weight=tkFont.NORMAL))
596
+ run_button.grid(row=row, column=0, columnspan=2, pady=10, padx=10)
493
597
 
494
- # Start the annotation application in the new root window
495
- app_instance = annotate(db, image_type, channels, annotation_column, geom, img_size, rows, columns)
496
-
497
- new_root.mainloop()
498
-
499
- create_dark_mode(root, style, console_output=None)
598
+ return parent_frame
500
599
 
501
- run_button = ttk.Button(scrollable_frame.scrollable_frame, text="Run", command=run_app)
502
- run_button.grid(row=row, column=0, columnspan=2, pady=10, padx=10)
600
+ def annotate_app(parent_frame, settings):
601
+ global global_image_refs
602
+ global_image_refs.clear()
603
+ root = parent_frame.winfo_toplevel()
604
+ annotate_with_image_refs(settings, root, lambda: load_next_app(root))
503
605
 
504
- return root
606
+ def annotate_with_image_refs(settings, root, shutdown_callback):
607
+ settings = get_annotate_default_settings(settings)
608
+ src = settings['src']
505
609
 
506
- def gui_annotation():
507
- root = initiate_annotation_app_root(500, 350)
610
+ db = os.path.join(src, 'measurements/measurements.db')
611
+ conn = sqlite3.connect(db)
612
+ c = conn.cursor()
613
+ c.execute('PRAGMA table_info(png_list)')
614
+ cols = c.fetchall()
615
+ if settings['annotation_column'] not in [col[1] for col in cols]:
616
+ c.execute(f"ALTER TABLE png_list ADD COLUMN {settings['annotation_column']} integer")
617
+ conn.commit()
618
+ conn.close()
619
+
620
+ 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'])
621
+
622
+ # Set the canvas background to black
623
+ root.configure(bg='black')
624
+
625
+ next_button = tk.Button(root, text="Next", command=app.next_page, background='black', foreground='white')
626
+ next_button.grid(row=app.grid_rows, column=app.grid_cols - 1)
627
+ back_button = tk.Button(root, text="Back", command=app.previous_page, background='black', foreground='white')
628
+ back_button.grid(row=app.grid_rows, column=app.grid_cols - 2)
629
+ exit_button = tk.Button(root, text="Exit", command=lambda: [app.shutdown(), shutdown_callback()], background='black', foreground='white')
630
+ exit_button.grid(row=app.grid_rows, column=app.grid_cols - 3)
631
+
632
+ app.load_images()
633
+
634
+ # Store the shutdown function and next app details in the root
635
+ root.current_app_exit_func = app.shutdown
636
+ root.next_app_func = None
637
+ root.next_app_args = ()
638
+
639
+ def load_next_app(root):
640
+ # Get the next app function and arguments
641
+ next_app_func = root.next_app_func
642
+ next_app_args = root.next_app_args
643
+
644
+ if next_app_func:
645
+ next_app_func(*next_app_args)
646
+
647
+ def gui_annotate():
648
+ root = tk.Tk()
649
+ width = root.winfo_screenwidth()
650
+ height = root.winfo_screenheight()
651
+ root.geometry(f"{width}x{height}")
652
+ root.title("Annotate Application")
653
+
654
+ # Clear previous content if any
655
+ if hasattr(root, 'content_frame'):
656
+ for widget in root.content_frame.winfo_children():
657
+ widget.destroy()
658
+ root.content_frame.grid_forget()
659
+ else:
660
+ root.content_frame = tk.Frame(root)
661
+ root.content_frame.grid(row=1, column=0, sticky="nsew")
662
+ root.grid_rowconfigure(1, weight=1)
663
+ root.grid_columnconfigure(0, weight=1)
664
+
665
+ initiate_annotation_app_root(root.content_frame)
666
+ create_menu_bar(root)
508
667
  root.mainloop()
509
668
 
510
669
  if __name__ == "__main__":
511
- gui_annotation()
670
+ gui_annotate()