spacr 0.0.36__py3-none-any.whl → 0.0.62__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/annotate_app.py CHANGED
@@ -16,35 +16,14 @@ from .logger import log_function_call
16
16
  from .gui_utils import ScrollableFrame, set_default_font, set_dark_style, create_dark_mode, style_text_boxes, create_menu_bar
17
17
 
18
18
  class ImageApp:
19
- """
20
- A class representing an image application.
21
-
22
- Attributes:
23
- - root (tkinter.Tk): The root window of the application.
24
- - db_path (str): The path to the SQLite database.
25
- - index (int): The index of the current page of images.
26
- - grid_rows (int): The number of rows in the image grid.
27
- - grid_cols (int): The number of columns in the image grid.
28
- - image_size (tuple): The size of the displayed images.
29
- - annotation_column (str): The column name for image annotations in the database.
30
- - image_type (str): The type of images to display.
31
- - channels (list): The channels to filter in the images.
32
- - images (dict): A dictionary mapping labels to loaded images.
33
- - pending_updates (dict): A dictionary of pending image annotation updates.
34
- - labels (list): A list of label widgets for displaying images.
35
- - terminate (bool): A flag indicating whether the application should terminate.
36
- - update_queue (Queue): A queue for storing image annotation updates.
37
- - status_label (tkinter.Label): A label widget for displaying status messages.
38
- - db_update_thread (threading.Thread): A thread for updating the database.
39
- """
40
-
41
- def __init__(self, root, db_path, image_type=None, channels=None, grid_rows=None, grid_cols=None, image_size=(200, 200), annotation_column='annotate'):
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'):
42
20
  """
43
21
  Initializes an instance of the ImageApp class.
44
22
 
45
23
  Parameters:
46
24
  - root (tkinter.Tk): The root window of the application.
47
25
  - db_path (str): The path to the SQLite database.
26
+ - src (str): The source directory that should be upstream of 'data' in the paths.
48
27
  - image_type (str): The type of images to display.
49
28
  - channels (list): The channels to filter in the images.
50
29
  - grid_rows (int): The number of rows in the image grid.
@@ -52,9 +31,9 @@ class ImageApp:
52
31
  - image_size (tuple): The size of the displayed images.
53
32
  - annotation_column (str): The column name for image annotations in the database.
54
33
  """
55
-
56
34
  self.root = root
57
35
  self.db_path = db_path
36
+ self.src = src
58
37
  self.index = 0
59
38
  self.grid_rows = grid_rows
60
39
  self.grid_cols = grid_cols
@@ -65,7 +44,7 @@ class ImageApp:
65
44
  self.images = {}
66
45
  self.pending_updates = {}
67
46
  self.labels = []
68
- #self.updating_db = True
47
+ self.adjusted_to_original_paths = {}
69
48
  self.terminate = False
70
49
  self.update_queue = Queue()
71
50
  self.status_label = Label(self.root, text="", font=("Arial", 12))
@@ -78,6 +57,68 @@ class ImageApp:
78
57
  label = Label(root)
79
58
  label.grid(row=i // grid_cols, column=i % grid_cols)
80
59
  self.labels.append(label)
60
+
61
+ def load_images(self):
62
+ """
63
+ Loads and displays images with annotations.
64
+
65
+ This method retrieves image paths and annotations from a SQLite database,
66
+ loads the images using a ThreadPoolExecutor for parallel processing,
67
+ adds colored borders to images based on their annotations,
68
+ and displays the images in the corresponding labels.
69
+
70
+ Args:
71
+ None
72
+
73
+ Returns:
74
+ None
75
+ """
76
+ for label in self.labels:
77
+ label.config(image='')
78
+
79
+ self.images = {}
80
+
81
+ conn = sqlite3.connect(self.db_path)
82
+ c = conn.cursor()
83
+ if self.image_type:
84
+ 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))
85
+ else:
86
+ c.execute(f"SELECT png_path, {self.annotation_column} FROM png_list LIMIT ?, ?", (self.index, self.grid_rows * self.grid_cols))
87
+
88
+ paths = c.fetchall()
89
+ conn.close()
90
+
91
+ adjusted_paths = []
92
+ for path, annotation in paths:
93
+ if not path.startswith(self.src):
94
+ parts = path.split('/data/')
95
+ if len(parts) > 1:
96
+ new_path = os.path.join(self.src, 'data', parts[1])
97
+ self.adjusted_to_original_paths[new_path] = path
98
+ adjusted_paths.append((new_path, annotation))
99
+ else:
100
+ adjusted_paths.append((path, annotation))
101
+ else:
102
+ adjusted_paths.append((path, annotation))
103
+
104
+ with ThreadPoolExecutor() as executor:
105
+ loaded_images = list(executor.map(self.load_single_image, adjusted_paths))
106
+
107
+ for i, (img, annotation) in enumerate(loaded_images):
108
+ if annotation:
109
+ border_color = 'teal' if annotation == 1 else 'red'
110
+ img = self.add_colored_border(img, border_width=5, border_color=border_color)
111
+
112
+ photo = ImageTk.PhotoImage(img)
113
+ label = self.labels[i]
114
+ self.images[label] = photo
115
+ label.config(image=photo)
116
+
117
+ path = adjusted_paths[i][0]
118
+ label.bind('<Button-1>', self.get_on_image_click(path, label, img))
119
+ label.bind('<Button-3>', self.get_on_image_click(path, label, img))
120
+
121
+ self.root.update()
81
122
 
82
123
  @staticmethod
83
124
  def normalize_image(img):
@@ -148,55 +189,6 @@ class ImageApp:
148
189
 
149
190
  return Image.merge("RGB", (r, g, b))
150
191
 
151
- def load_images(self):
152
- """
153
- Loads and displays images with annotations.
154
-
155
- This method retrieves image paths and annotations from a SQLite database,
156
- loads the images using a ThreadPoolExecutor for parallel processing,
157
- adds colored borders to images based on their annotations,
158
- and displays the images in the corresponding labels.
159
-
160
- Args:
161
- None
162
-
163
- Returns:
164
- None
165
- """
166
- for label in self.labels:
167
- label.config(image='')
168
-
169
- self.images = {}
170
-
171
- conn = sqlite3.connect(self.db_path)
172
- c = conn.cursor()
173
- if self.image_type:
174
- 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))
175
- else:
176
- c.execute(f"SELECT png_path, {self.annotation_column} FROM png_list LIMIT ?, ?", (self.index, self.grid_rows * self.grid_cols))
177
-
178
- paths = c.fetchall()
179
- conn.close()
180
-
181
- with ThreadPoolExecutor() as executor:
182
- loaded_images = list(executor.map(self.load_single_image, paths))
183
-
184
- for i, (img, annotation) in enumerate(loaded_images):
185
- if annotation:
186
- border_color = 'teal' if annotation == 1 else 'red'
187
- img = self.add_colored_border(img, border_width=5, border_color=border_color)
188
-
189
- photo = ImageTk.PhotoImage(img)
190
- label = self.labels[i]
191
- self.images[label] = photo
192
- label.config(image=photo)
193
-
194
- path = paths[i][0]
195
- label.bind('<Button-1>', self.get_on_image_click(path, label, img))
196
- label.bind('<Button-3>', self.get_on_image_click(path, label, img))
197
-
198
- self.root.update()
199
-
200
192
  def load_single_image(self, path_annotation_tuple):
201
193
  """
202
194
  Loads a single image from the given path and annotation tuple.
@@ -230,14 +222,15 @@ class ImageApp:
230
222
  function: The callback function for the image click event.
231
223
  """
232
224
  def on_image_click(event):
233
-
234
225
  new_annotation = 1 if event.num == 1 else (2 if event.num == 3 else None)
235
226
 
236
- if path in self.pending_updates and self.pending_updates[path] == new_annotation:
237
- self.pending_updates[path] = None
227
+ original_path = self.adjusted_to_original_paths.get(path, path)
228
+
229
+ if original_path in self.pending_updates and self.pending_updates[original_path] == new_annotation:
230
+ self.pending_updates[original_path] = None
238
231
  new_annotation = None
239
232
  else:
240
- self.pending_updates[path] = new_annotation
233
+ self.pending_updates[original_path] = new_annotation
241
234
 
242
235
  print(f"Image {os.path.split(path)[1]} annotated: {new_annotation}")
243
236
 
@@ -261,38 +254,38 @@ class ImageApp:
261
254
  """))
262
255
 
263
256
  def update_database_worker(self):
264
- """
265
- Worker function that continuously updates the database with pending updates from the update queue.
266
- It retrieves the pending updates from the queue, updates the corresponding records in the database,
267
- and resets the text in the HTML and status label.
268
- """
269
- conn = sqlite3.connect(self.db_path)
270
- c = conn.cursor()
271
-
272
- display(HTML("<div id='unique_id'>Initial Text</div>"))
273
-
274
- while True:
275
- if self.terminate:
276
- conn.close()
277
- break
278
-
279
- if not self.update_queue.empty():
280
- ImageApp.update_html("Do not exit, Updating database...")
281
- self.status_label.config(text='Do not exit, Updating database...')
282
-
283
- pending_updates = self.update_queue.get()
284
- for path, new_annotation in pending_updates.items():
285
- if new_annotation is None:
286
- c.execute(f'UPDATE png_list SET {self.annotation_column} = NULL WHERE png_path = ?', (path,))
287
- else:
288
- c.execute(f'UPDATE png_list SET {self.annotation_column} = ? WHERE png_path = ?', (new_annotation, path))
289
- conn.commit()
290
-
291
- # Reset the text
292
- ImageApp.update_html('')
293
- self.status_label.config(text='')
294
- self.root.update()
295
- time.sleep(0.1)
257
+ """
258
+ Worker function that continuously updates the database with pending updates from the update queue.
259
+ It retrieves the pending updates from the queue, updates the corresponding records in the database,
260
+ and resets the text in the HTML and status label.
261
+ """
262
+ conn = sqlite3.connect(self.db_path)
263
+ c = conn.cursor()
264
+
265
+ display(HTML("<div id='unique_id'>Initial Text</div>"))
266
+
267
+ while True:
268
+ if self.terminate:
269
+ conn.close()
270
+ break
271
+
272
+ if not self.update_queue.empty():
273
+ ImageApp.update_html("Do not exit, Updating database...")
274
+ self.status_label.config(text='Do not exit, Updating database...')
275
+
276
+ pending_updates = self.update_queue.get()
277
+ for path, new_annotation in pending_updates.items():
278
+ if new_annotation is None:
279
+ c.execute(f'UPDATE png_list SET {self.annotation_column} = NULL WHERE png_path = ?', (path,))
280
+ else:
281
+ c.execute(f'UPDATE png_list SET {self.annotation_column} = ? WHERE png_path = ?', (new_annotation, path))
282
+ conn.commit()
283
+
284
+ # Reset the text
285
+ ImageApp.update_html('')
286
+ self.status_label.config(text='')
287
+ self.root.update()
288
+ time.sleep(0.1)
296
289
 
297
290
  def update_gui_text(self, text):
298
291
  """
@@ -356,12 +349,13 @@ class ImageApp:
356
349
  self.root.destroy()
357
350
  print(f'Quit application')
358
351
 
359
- def annotate(db, image_type=None, channels=None, geom="1000x1100", img_size=(200, 200), rows=5, columns=5, annotation_column='annotate'):
352
+ def annotate(src, image_type=None, channels=None, geom="1000x1100", img_size=(200, 200), rows=5, columns=5, annotation_column='annotate'):
360
353
  """
361
354
  Annotates images in a database using a graphical user interface.
362
355
 
363
356
  Args:
364
357
  db (str): The path to the SQLite database.
358
+ src (str): The source directory that should be upstream of 'data' in the paths.
365
359
  image_type (str, optional): The type of images to load from the database. Defaults to None.
366
360
  channels (str, optional): The channels of the images to load from the database. Defaults to None.
367
361
  geom (str, optional): The geometry of the GUI window. Defaults to "1000x1100".
@@ -370,7 +364,10 @@ def annotate(db, image_type=None, channels=None, geom="1000x1100", img_size=(200
370
364
  columns (int, optional): The number of columns in the image grid. Defaults to 5.
371
365
  annotation_column (str, optional): The name of the annotation column in the database table. Defaults to 'annotate'.
372
366
  """
373
- #display(HTML("<div id='unique_id'>Initial Text</div>"))
367
+ db = os.path.join(src, 'measurements/measurements.db')
368
+ #print('src', src)
369
+ #print('db', db)
370
+
374
371
  conn = sqlite3.connect(db)
375
372
  c = conn.cursor()
376
373
  c.execute('PRAGMA table_info(png_list)')
@@ -382,8 +379,7 @@ def annotate(db, image_type=None, channels=None, geom="1000x1100", img_size=(200
382
379
 
383
380
  root = tk.Tk()
384
381
  root.geometry(geom)
385
- app = ImageApp(root, db, image_type=image_type, channels=channels, image_size=img_size, grid_rows=rows, grid_cols=columns, annotation_column=annotation_column)
386
- #app = ImageApp()
382
+ 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)
387
383
  next_button = tk.Button(root, text="Next", command=app.next_page)
388
384
  next_button.grid(row=app.grid_rows, column=app.grid_cols - 1)
389
385
  back_button = tk.Button(root, text="Back", command=app.previous_page)
@@ -394,6 +390,7 @@ def annotate(db, image_type=None, channels=None, geom="1000x1100", img_size=(200
394
390
  app.load_images()
395
391
  root.mainloop()
396
392
 
393
+
397
394
  def check_for_duplicates(db):
398
395
  """
399
396
  Check for duplicates in the given SQLite database.
@@ -493,5 +490,4 @@ def gui_annotation():
493
490
  root.mainloop()
494
491
 
495
492
  if __name__ == "__main__":
496
- gui_annotation()
497
-
493
+ gui_annotation()