spacr 0.2.1__py3-none-any.whl → 0.2.21__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.
Files changed (90) hide show
  1. spacr/gui.py +2 -1
  2. spacr/gui_elements.py +2 -7
  3. spacr/resources/icons/abort.png +0 -0
  4. spacr/resources/icons/classify.png +0 -0
  5. spacr/resources/icons/make_masks.png +0 -0
  6. spacr/resources/icons/mask.png +0 -0
  7. spacr/resources/icons/measure.png +0 -0
  8. spacr/resources/icons/recruitment.png +0 -0
  9. spacr/resources/icons/regression.png +0 -0
  10. spacr/resources/icons/run.png +0 -0
  11. spacr/resources/icons/umap.png +0 -0
  12. {spacr-0.2.1.dist-info → spacr-0.2.21.dist-info}/METADATA +1 -1
  13. spacr-0.2.21.dist-info/RECORD +56 -0
  14. spacr/alpha.py +0 -807
  15. spacr/annotate_app.py +0 -670
  16. spacr/annotate_app_v2.py +0 -670
  17. spacr/app_make_masks_v2.py +0 -686
  18. spacr/classify_app.py +0 -201
  19. spacr/cli.py +0 -41
  20. spacr/foldseek.py +0 -779
  21. spacr/get_alfafold_structures.py +0 -72
  22. spacr/gui_2.py +0 -157
  23. spacr/gui_annotate.py +0 -145
  24. spacr/gui_classify_app.py +0 -201
  25. spacr/gui_make_masks_app.py +0 -927
  26. spacr/gui_make_masks_app_v2.py +0 -688
  27. spacr/gui_mask_app.py +0 -249
  28. spacr/gui_measure_app.py +0 -246
  29. spacr/gui_run.py +0 -58
  30. spacr/gui_sim_app.py +0 -0
  31. spacr/gui_wrappers.py +0 -149
  32. spacr/icons/abort.png +0 -0
  33. spacr/icons/abort.svg +0 -1
  34. spacr/icons/download.png +0 -0
  35. spacr/icons/download.svg +0 -1
  36. spacr/icons/download_for_offline_100dp_E8EAED_FILL0_wght100_GRAD-25_opsz48.png +0 -0
  37. spacr/icons/download_for_offline_100dp_E8EAED_FILL0_wght100_GRAD-25_opsz48.svg +0 -1
  38. spacr/icons/logo_spacr.png +0 -0
  39. spacr/icons/make_masks.png +0 -0
  40. spacr/icons/make_masks.svg +0 -1
  41. spacr/icons/map_barcodes.png +0 -0
  42. spacr/icons/map_barcodes.svg +0 -1
  43. spacr/icons/mask.png +0 -0
  44. spacr/icons/mask.svg +0 -1
  45. spacr/icons/measure.png +0 -0
  46. spacr/icons/measure.svg +0 -1
  47. spacr/icons/play_circle_100dp_E8EAED_FILL0_wght100_GRAD-25_opsz48.png +0 -0
  48. spacr/icons/play_circle_100dp_E8EAED_FILL0_wght100_GRAD-25_opsz48.svg +0 -1
  49. spacr/icons/run.png +0 -0
  50. spacr/icons/run.svg +0 -1
  51. spacr/icons/sequencing.png +0 -0
  52. spacr/icons/sequencing.svg +0 -1
  53. spacr/icons/settings.png +0 -0
  54. spacr/icons/settings.svg +0 -1
  55. spacr/icons/settings_100dp_E8EAED_FILL0_wght100_GRAD-25_opsz48.png +0 -0
  56. spacr/icons/settings_100dp_E8EAED_FILL0_wght100_GRAD-25_opsz48.svg +0 -1
  57. spacr/icons/stop_circle_100dp_E8EAED_FILL0_wght100_GRAD-25_opsz48.png +0 -0
  58. spacr/icons/stop_circle_100dp_E8EAED_FILL0_wght100_GRAD-25_opsz48.svg +0 -1
  59. spacr/icons/theater_comedy_100dp_E8EAED_FILL0_wght100_GRAD200_opsz48.png +0 -0
  60. spacr/icons/theater_comedy_100dp_E8EAED_FILL0_wght100_GRAD200_opsz48.svg +0 -1
  61. spacr/make_masks_app.py +0 -929
  62. spacr/make_masks_app_v2.py +0 -688
  63. spacr/mask_app.py +0 -249
  64. spacr/measure_app.py +0 -246
  65. spacr/models/cp/toxo_plaque_cyto_e25000_X1120_Y1120.CP_model +0 -0
  66. spacr/models/cp/toxo_plaque_cyto_e25000_X1120_Y1120.CP_model_settings.csv +0 -23
  67. spacr/models/cp/toxo_pv_lumen.CP_model +0 -0
  68. spacr/old_code.py +0 -358
  69. spacr/resources/icons/abort.svg +0 -1
  70. spacr/resources/icons/annotate.svg +0 -1
  71. spacr/resources/icons/classify.svg +0 -1
  72. spacr/resources/icons/download.svg +0 -1
  73. spacr/resources/icons/icon.psd +0 -0
  74. spacr/resources/icons/make_masks.svg +0 -1
  75. spacr/resources/icons/map_barcodes.svg +0 -1
  76. spacr/resources/icons/mask.svg +0 -1
  77. spacr/resources/icons/measure.svg +0 -1
  78. spacr/resources/icons/run.svg +0 -1
  79. spacr/resources/icons/run_2.png +0 -0
  80. spacr/resources/icons/run_2.svg +0 -1
  81. spacr/resources/icons/sequencing.svg +0 -1
  82. spacr/resources/icons/settings.svg +0 -1
  83. spacr/resources/icons/train_cellpose.svg +0 -1
  84. spacr/test_gui.py +0 -0
  85. spacr-0.2.1.dist-info/RECORD +0 -126
  86. /spacr/resources/icons/{cellpose.png → cellpose_all.png} +0 -0
  87. {spacr-0.2.1.dist-info → spacr-0.2.21.dist-info}/LICENSE +0 -0
  88. {spacr-0.2.1.dist-info → spacr-0.2.21.dist-info}/WHEEL +0 -0
  89. {spacr-0.2.1.dist-info → spacr-0.2.21.dist-info}/entry_points.txt +0 -0
  90. {spacr-0.2.1.dist-info → spacr-0.2.21.dist-info}/top_level.txt +0 -0
spacr/annotate_app_v2.py DELETED
@@ -1,670 +0,0 @@
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
-
15
- from .gui_utils import ScrollableFrame, CustomButton, set_dark_style, set_default_font, style_text_boxes, create_menu_bar
16
-
17
- class ImageApp:
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):
19
- """
20
- Initializes an instance of the ImageApp class.
21
-
22
- Parameters:
23
- - root (tkinter.Tk): The root window of the application.
24
- - db_path (str): The path to the SQLite database.
25
- - src (str): The source directory that should be upstream of 'data' in the paths.
26
- - image_type (str): The type of images to display.
27
- - channels (list): The channels to filter in the images.
28
- - grid_rows (int): The number of rows in the image grid.
29
- - grid_cols (int): The number of columns in the image grid.
30
- - image_size (tuple): The size of the displayed images.
31
- - annotation_column (str): The column name for image annotations in the database.
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.
35
- """
36
-
37
- self.root = root
38
- self.db_path = db_path
39
- self.src = src
40
- self.index = 0
41
- self.grid_rows = grid_rows
42
- self.grid_cols = grid_cols
43
- self.image_size = image_size
44
- self.annotation_column = annotation_column
45
- self.image_type = image_type
46
- self.channels = channels
47
- self.normalize = normalize
48
- self.percentiles = percentiles
49
- self.images = {}
50
- self.pending_updates = {}
51
- self.labels = []
52
- self.adjusted_to_original_paths = {}
53
- self.terminate = False
54
- self.update_queue = Queue()
55
- self.status_label = Label(self.root, text="", font=("Arial", 12))
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()
62
-
63
- self.db_update_thread = threading.Thread(target=self.update_database_worker)
64
- self.db_update_thread.start()
65
-
66
- for i in range(grid_rows * grid_cols):
67
- label = Label(root)
68
- label.grid(row=i // grid_cols, column=i % grid_cols)
69
- self.labels.append(label)
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
-
161
- def load_images(self):
162
- """
163
- Loads and displays images with annotations.
164
-
165
- This method retrieves image paths and annotations from a pre-filtered list,
166
- loads the images using a ThreadPoolExecutor for parallel processing,
167
- adds colored borders to images based on their annotations,
168
- and displays the images in the corresponding labels.
169
-
170
- Args:
171
- None
172
-
173
- Returns:
174
- None
175
- """
176
-
177
- for label in self.labels:
178
- label.config(image='')
179
-
180
- self.images = {}
181
- paths_annotations = self.filtered_paths_annotations[self.index:self.index + self.grid_rows * self.grid_cols]
182
-
183
- adjusted_paths = []
184
- for path, annotation in paths_annotations:
185
- if not path.startswith(self.src):
186
- parts = path.split('/data/')
187
- if len(parts) > 1:
188
- new_path = os.path.join(self.src, 'data', parts[1])
189
- self.adjusted_to_original_paths[new_path] = path
190
- adjusted_paths.append((new_path, annotation))
191
- else:
192
- adjusted_paths.append((path, annotation))
193
- else:
194
- adjusted_paths.append((path, annotation))
195
-
196
- with ThreadPoolExecutor() as executor:
197
- loaded_images = list(executor.map(self.load_single_image, adjusted_paths))
198
-
199
- for i, (img, annotation) in enumerate(loaded_images):
200
- if annotation:
201
- border_color = 'teal' if annotation == 1 else 'red'
202
- img = self.add_colored_border(img, border_width=5, border_color=border_color)
203
-
204
- photo = ImageTk.PhotoImage(img)
205
- label = self.labels[i]
206
- self.images[label] = photo
207
- label.config(image=photo)
208
-
209
- path = adjusted_paths[i][0]
210
- label.bind('<Button-1>', self.get_on_image_click(path, label, img))
211
- label.bind('<Button-3>', self.get_on_image_click(path, label, img))
212
-
213
- self.root.update()
214
-
215
- def load_single_image(self, path_annotation_tuple):
216
- """
217
- Loads a single image from the given path and annotation tuple.
218
-
219
- Args:
220
- path_annotation_tuple (tuple): A tuple containing the image path and its annotation.
221
-
222
- Returns:
223
- img (PIL.Image.Image): The loaded image.
224
- annotation: The annotation associated with the image.
225
- """
226
- path, annotation = path_annotation_tuple
227
- img = Image.open(path)
228
- img = self.normalize_image(img, self.normalize, self.percentiles)
229
- img = img.convert('RGB')
230
- img = self.filter_channels(img)
231
- img = img.resize(self.image_size)
232
- return img, annotation
233
-
234
- @staticmethod
235
- def normalize_image(img, normalize=False, percentiles=(1, 99)):
236
- """
237
- Normalize the pixel values of an image based on the 2nd and 98th percentiles or the image min and max values,
238
- and ensure the image is exported as 8-bit.
239
-
240
- Parameters:
241
- - img: PIL.Image.Image. The input image to be normalized.
242
- - normalize: bool. Whether to normalize based on the 2nd and 98th percentiles.
243
- - percentiles: tuple. The percentiles to use for normalization.
244
-
245
- Returns:
246
- - PIL.Image.Image. The normalized and 8-bit converted image.
247
- """
248
- img_array = np.array(img)
249
-
250
- if normalize:
251
- if img_array.ndim == 2: # Grayscale image
252
- p2, p98 = np.percentile(img_array, percentiles)
253
- img_array = rescale_intensity(img_array, in_range=(p2, p98), out_range=(0, 255))
254
- else: # Color image or multi-channel image
255
- for channel in range(img_array.shape[2]):
256
- p2, p98 = np.percentile(img_array[:, :, channel], percentiles)
257
- img_array[:, :, channel] = rescale_intensity(img_array[:, :, channel], in_range=(p2, p98), out_range=(0, 255))
258
-
259
- img_array = np.clip(img_array, 0, 255).astype('uint8')
260
-
261
- return Image.fromarray(img_array)
262
-
263
- def add_colored_border(self, img, border_width, border_color):
264
- """
265
- Adds a colored border to an image.
266
-
267
- Args:
268
- img (PIL.Image.Image): The input image.
269
- border_width (int): The width of the border in pixels.
270
- border_color (str): The color of the border in RGB format.
271
-
272
- Returns:
273
- PIL.Image.Image: The image with the colored border.
274
- """
275
- top_border = Image.new('RGB', (img.width, border_width), color=border_color)
276
- bottom_border = Image.new('RGB', (img.width, border_width), color=border_color)
277
- left_border = Image.new('RGB', (border_width, img.height), color=border_color)
278
- right_border = Image.new('RGB', (border_width, img.height), color=border_color)
279
-
280
- bordered_img = Image.new('RGB', (img.width + 2 * border_width, img.height + 2 * border_width), color='white')
281
- bordered_img.paste(top_border, (border_width, 0))
282
- bordered_img.paste(bottom_border, (border_width, img.height + border_width))
283
- bordered_img.paste(left_border, (0, border_width))
284
- bordered_img.paste(right_border, (img.width + border_width, border_width))
285
- bordered_img.paste(img, (border_width, border_width))
286
-
287
- return bordered_img
288
-
289
- def filter_channels(self, img):
290
- """
291
- Filters the channels of an image based on the specified channels.
292
-
293
- Args:
294
- img (PIL.Image.Image): The input image.
295
-
296
- Returns:
297
- PIL.Image.Image: The filtered image.
298
- """
299
- r, g, b = img.split()
300
- if self.channels:
301
- if 'r' not in self.channels:
302
- r = r.point(lambda _: 0)
303
- if 'g' not in self.channels:
304
- g = g.point(lambda _: 0)
305
- if 'b' not in self.channels:
306
- b = b.point(lambda _: 0)
307
-
308
- if len(self.channels) == 1:
309
- channel_img = r if 'r' in self.channels else (g if 'g' in self.channels else b)
310
- return ImageOps.grayscale(channel_img)
311
-
312
- return Image.merge("RGB", (r, g, b))
313
-
314
- def get_on_image_click(self, path, label, img):
315
- """
316
- Returns a callback function that handles the click event on an image.
317
-
318
- Parameters:
319
- path (str): The path of the image file.
320
- label (tkinter.Label): The label widget to update with the annotated image.
321
- img (PIL.Image.Image): The image object.
322
-
323
- Returns:
324
- function: The callback function for the image click event.
325
- """
326
- def on_image_click(event):
327
- new_annotation = 1 if event.num == 1 else (2 if event.num == 3 else None)
328
-
329
- original_path = self.adjusted_to_original_paths.get(path, path)
330
-
331
- if original_path in self.pending_updates and self.pending_updates[original_path] == new_annotation:
332
- self.pending_updates[original_path] = None
333
- new_annotation = None
334
- else:
335
- self.pending_updates[original_path] = new_annotation
336
-
337
- print(f"Image {os.path.split(path)[1]} annotated: {new_annotation}")
338
-
339
- img_ = img.crop((5, 5, img.width-5, img.height-5))
340
- border_fill = 'teal' if new_annotation == 1 else ('red' if new_annotation == 2 else None)
341
- img_ = ImageOps.expand(img_, border=5, fill=border_fill) if border_fill else img_
342
-
343
- photo = ImageTk.PhotoImage(img_)
344
- self.images[label] = photo
345
- label.config(image=photo)
346
- self.root.update()
347
-
348
- return on_image_click
349
-
350
- @staticmethod
351
- def update_html(text):
352
- display(HTML(f"""
353
- <script>
354
- document.getElementById('unique_id').innerHTML = '{text}';
355
- </script>
356
- """))
357
-
358
- def update_database_worker(self):
359
- """
360
- Worker function that continuously updates the database with pending updates from the update queue.
361
- It retrieves the pending updates from the queue, updates the corresponding records in the database,
362
- and resets the text in the HTML and status label.
363
- """
364
- conn = sqlite3.connect(self.db_path)
365
- c = conn.cursor()
366
-
367
- display(HTML("<div id='unique_id'>Initial Text</div>"))
368
-
369
- while True:
370
- if self.terminate:
371
- conn.close()
372
- break
373
-
374
- if not self.update_queue.empty():
375
- ImageApp.update_html("Do not exit, Updating database...")
376
- self.status_label.config(text='Do not exit, Updating database...')
377
-
378
- pending_updates = self.update_queue.get()
379
- for path, new_annotation in pending_updates.items():
380
- if new_annotation is None:
381
- c.execute(f'UPDATE png_list SET {self.annotation_column} = NULL WHERE png_path = ?', (path,))
382
- else:
383
- c.execute(f'UPDATE png_list SET {self.annotation_column} = ? WHERE png_path = ?', (new_annotation, path))
384
- conn.commit()
385
-
386
- # Reset the text
387
- ImageApp.update_html('')
388
- self.status_label.config(text='')
389
- self.root.update()
390
- time.sleep(0.1)
391
-
392
- def update_gui_text(self, text):
393
- """
394
- Update the text of the status label in the GUI.
395
-
396
- Args:
397
- text (str): The new text to be displayed in the status label.
398
-
399
- Returns:
400
- None
401
- """
402
- self.status_label.config(text=text)
403
- self.root.update()
404
-
405
- def next_page(self):
406
- """
407
- Moves to the next page of images in the grid.
408
-
409
- If there are pending updates in the dictionary, they are added to the update queue.
410
- The pending updates dictionary is then cleared.
411
- The index is incremented by the number of rows multiplied by the number of columns in the grid.
412
- Finally, the images are loaded for the new page.
413
- """
414
- if self.pending_updates: # Check if the dictionary is not empty
415
- self.update_queue.put(self.pending_updates.copy())
416
- self.pending_updates.clear()
417
- self.index += self.grid_rows * self.grid_cols
418
- self.load_images()
419
-
420
- def previous_page(self):
421
- """
422
- Move to the previous page in the grid.
423
-
424
- If there are pending updates in the dictionary, they are added to the update queue.
425
- The dictionary of pending updates is then cleared.
426
- The index is decremented by the number of rows multiplied by the number of columns in the grid.
427
- If the index becomes negative, it is set to 0.
428
- Finally, the images are loaded for the new page.
429
- """
430
- if self.pending_updates: # Check if the dictionary is not empty
431
- self.update_queue.put(self.pending_updates.copy())
432
- self.pending_updates.clear()
433
- self.index -= self.grid_rows * self.grid_cols
434
- if self.index < 0:
435
- self.index = 0
436
- self.load_images()
437
-
438
- def shutdown(self):
439
- """
440
- Shuts down the application.
441
-
442
- This method sets the terminate flag to True, clears the pending updates,
443
- updates the database, and quits the application.
444
-
445
- """
446
- self.terminate = True # Set terminate first
447
- self.update_queue.put(self.pending_updates.copy())
448
- self.pending_updates.clear()
449
- self.db_update_thread.join() # Join the thread to make sure database is updated
450
- self.root.quit()
451
- self.root.destroy()
452
- print(f'Quit application')
453
-
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):
471
- """
472
- Annotates images in a database using a graphical user interface.
473
-
474
- Args:
475
- db (str): The path to the SQLite database.
476
- src (str): The source directory that should be upstream of 'data' in the paths.
477
- image_type (str, optional): The type of images to load from the database. Defaults to None.
478
- channels (str, optional): The channels of the images to load from the database. Defaults to None.
479
- geom (str, optional): The geometry of the GUI window. Defaults to "1000x1100".
480
- img_size (tuple, optional): The size of the images to display in the GUI. Defaults to (200, 200).
481
- rows (int, optional): The number of rows in the image grid. Defaults to 5.
482
- columns (int, optional): The number of columns in the image grid. Defaults to 5.
483
- annotation_column (str, optional): The name of the annotation column in the database table. Defaults to 'annotate'.
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.
487
- """
488
-
489
- settings = get_annotate_default_settings(settings)
490
- src = settings['src']
491
-
492
- db = os.path.join(src, 'measurements/measurements.db')
493
- conn = sqlite3.connect(db)
494
- c = conn.cursor()
495
- c.execute('PRAGMA table_info(png_list)')
496
- cols = c.fetchall()
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")
499
- conn.commit()
500
- conn.close()
501
-
502
- root = tk.Tk()
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'])
505
- next_button = tk.Button(root, text="Next", command=app.next_page)
506
- next_button.grid(row=app.grid_rows, column=app.grid_cols - 1)
507
- back_button = tk.Button(root, text="Back", command=app.previous_page)
508
- back_button.grid(row=app.grid_rows, column=app.grid_cols - 2)
509
- exit_button = tk.Button(root, text="Exit", command=app.shutdown)
510
- exit_button.grid(row=app.grid_rows, column=app.grid_cols - 3)
511
-
512
- app.load_images()
513
- root.mainloop()
514
-
515
- # Global list to keep references to PhotoImage objects
516
- global_image_refs = []
517
-
518
- def initiate_annotation_app_root(parent_frame):
519
- style = ttk.Style(parent_frame)
520
- set_dark_style(style)
521
- style_text_boxes(style)
522
- set_default_font(parent_frame, font_name="Arial", size=8)
523
-
524
- parent_frame.configure(bg='black')
525
-
526
- container = tk.PanedWindow(parent_frame, orient=tk.HORIZONTAL, bg='black')
527
- container.pack(fill=tk.BOTH, expand=True)
528
-
529
- scrollable_frame = ScrollableFrame(container, bg='black')
530
- container.add(scrollable_frame, stretch="always")
531
-
532
- # Setup input fields
533
- vars_dict = {
534
- 'src': ttk.Entry(scrollable_frame.scrollable_frame),
535
- 'image_type': ttk.Entry(scrollable_frame.scrollable_frame),
536
- 'channels': ttk.Entry(scrollable_frame.scrollable_frame),
537
- 'geom': ttk.Entry(scrollable_frame.scrollable_frame),
538
- 'img_size': ttk.Entry(scrollable_frame.scrollable_frame),
539
- 'rows': 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'
561
- }
562
-
563
- # Arrange input fields and labels
564
- row = 0
565
- for name, entry in vars_dict.items():
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])
569
- entry.grid(row=row, column=1)
570
- row += 1
571
-
572
- # Function to be called when "Run" button is clicked
573
- def run_app():
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
585
-
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()
590
-
591
- # Start the annotate application in the same root window
592
- annotate_app(parent_frame, settings)
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)
597
-
598
- return parent_frame
599
-
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))
605
-
606
- def annotate_with_image_refs(settings, root, shutdown_callback):
607
- settings = get_annotate_default_settings(settings)
608
- src = settings['src']
609
-
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)
667
- root.mainloop()
668
-
669
- if __name__ == "__main__":
670
- gui_annotate()