celldetective 1.3.9.post5__py3-none-any.whl → 1.4.1__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 (94) hide show
  1. celldetective/__init__.py +0 -3
  2. celldetective/_version.py +1 -1
  3. celldetective/events.py +2 -4
  4. celldetective/exceptions.py +11 -0
  5. celldetective/extra_properties.py +132 -0
  6. celldetective/filters.py +7 -1
  7. celldetective/gui/InitWindow.py +37 -46
  8. celldetective/gui/__init__.py +3 -9
  9. celldetective/gui/about.py +19 -15
  10. celldetective/gui/analyze_block.py +34 -19
  11. celldetective/gui/base_annotator.py +786 -0
  12. celldetective/gui/base_components.py +23 -0
  13. celldetective/gui/classifier_widget.py +86 -94
  14. celldetective/gui/configure_new_exp.py +163 -46
  15. celldetective/gui/control_panel.py +76 -146
  16. celldetective/gui/{signal_annotator.py → event_annotator.py} +533 -1438
  17. celldetective/gui/generic_signal_plot.py +11 -13
  18. celldetective/gui/gui_utils.py +54 -23
  19. celldetective/gui/help/neighborhood.json +2 -2
  20. celldetective/gui/json_readers.py +5 -4
  21. celldetective/gui/layouts.py +265 -31
  22. celldetective/gui/{signal_annotator2.py → pair_event_annotator.py} +433 -635
  23. celldetective/gui/plot_measurements.py +21 -17
  24. celldetective/gui/plot_signals_ui.py +125 -72
  25. celldetective/gui/process_block.py +283 -188
  26. celldetective/gui/processes/compute_neighborhood.py +594 -0
  27. celldetective/gui/processes/downloader.py +37 -34
  28. celldetective/gui/processes/measure_cells.py +19 -8
  29. celldetective/gui/processes/segment_cells.py +47 -11
  30. celldetective/gui/processes/track_cells.py +18 -13
  31. celldetective/gui/seg_model_loader.py +21 -62
  32. celldetective/gui/settings/__init__.py +7 -0
  33. celldetective/gui/settings/_settings_base.py +70 -0
  34. celldetective/gui/{retrain_signal_model_options.py → settings/_settings_event_model_training.py} +54 -109
  35. celldetective/gui/{measurement_options.py → settings/_settings_measurements.py} +54 -92
  36. celldetective/gui/{neighborhood_options.py → settings/_settings_neighborhood.py} +10 -13
  37. celldetective/gui/settings/_settings_segmentation.py +49 -0
  38. celldetective/gui/{retrain_segmentation_model_options.py → settings/_settings_segmentation_model_training.py} +38 -92
  39. celldetective/gui/{signal_annotator_options.py → settings/_settings_signal_annotator.py} +78 -103
  40. celldetective/gui/{btrack_options.py → settings/_settings_tracking.py} +85 -116
  41. celldetective/gui/styles.py +2 -1
  42. celldetective/gui/survival_ui.py +49 -95
  43. celldetective/gui/tableUI.py +53 -25
  44. celldetective/gui/table_ops/__init__.py +0 -0
  45. celldetective/gui/table_ops/merge_groups.py +118 -0
  46. celldetective/gui/thresholds_gui.py +617 -1221
  47. celldetective/gui/viewers.py +107 -42
  48. celldetective/gui/workers.py +8 -4
  49. celldetective/io.py +137 -57
  50. celldetective/links/zenodo.json +145 -144
  51. celldetective/measure.py +94 -53
  52. celldetective/neighborhood.py +342 -268
  53. celldetective/preprocessing.py +56 -35
  54. celldetective/regionprops/_regionprops.py +16 -5
  55. celldetective/relative_measurements.py +50 -29
  56. celldetective/scripts/analyze_signals.py +4 -1
  57. celldetective/scripts/measure_cells.py +5 -5
  58. celldetective/scripts/measure_relative.py +20 -12
  59. celldetective/scripts/segment_cells.py +4 -10
  60. celldetective/scripts/segment_cells_thresholds.py +3 -3
  61. celldetective/scripts/track_cells.py +10 -8
  62. celldetective/scripts/train_segmentation_model.py +18 -6
  63. celldetective/signals.py +29 -14
  64. celldetective/tracking.py +14 -3
  65. celldetective/utils.py +91 -62
  66. {celldetective-1.3.9.post5.dist-info → celldetective-1.4.1.dist-info}/METADATA +24 -16
  67. celldetective-1.4.1.dist-info/RECORD +123 -0
  68. {celldetective-1.3.9.post5.dist-info → celldetective-1.4.1.dist-info}/WHEEL +1 -1
  69. tests/gui/__init__.py +0 -0
  70. tests/gui/test_new_project.py +228 -0
  71. tests/gui/test_project.py +99 -0
  72. tests/test_preprocessing.py +2 -2
  73. celldetective/models/segmentation_effectors/ricm_bf_all_last/config_input.json +0 -79
  74. celldetective/models/segmentation_effectors/ricm_bf_all_last/ricm_bf_all_last +0 -0
  75. celldetective/models/segmentation_effectors/ricm_bf_all_last/training_instructions.json +0 -37
  76. celldetective/models/segmentation_effectors/test-transfer/config_input.json +0 -39
  77. celldetective/models/segmentation_effectors/test-transfer/test-transfer +0 -0
  78. celldetective/models/signal_detection/NucCond/classification_loss.png +0 -0
  79. celldetective/models/signal_detection/NucCond/classifier.h5 +0 -0
  80. celldetective/models/signal_detection/NucCond/config_input.json +0 -1
  81. celldetective/models/signal_detection/NucCond/log_classifier.csv +0 -126
  82. celldetective/models/signal_detection/NucCond/log_regressor.csv +0 -282
  83. celldetective/models/signal_detection/NucCond/regression_loss.png +0 -0
  84. celldetective/models/signal_detection/NucCond/regressor.h5 +0 -0
  85. celldetective/models/signal_detection/NucCond/scores.npy +0 -0
  86. celldetective/models/signal_detection/NucCond/test_confusion_matrix.png +0 -0
  87. celldetective/models/signal_detection/NucCond/test_regression.png +0 -0
  88. celldetective/models/signal_detection/NucCond/validation_confusion_matrix.png +0 -0
  89. celldetective/models/signal_detection/NucCond/validation_regression.png +0 -0
  90. celldetective-1.3.9.post5.dist-info/RECORD +0 -129
  91. tests/test_qt.py +0 -103
  92. {celldetective-1.3.9.post5.dist-info → celldetective-1.4.1.dist-info}/entry_points.txt +0 -0
  93. {celldetective-1.3.9.post5.dist-info → celldetective-1.4.1.dist-info/licenses}/LICENSE +0 -0
  94. {celldetective-1.3.9.post5.dist-info → celldetective-1.4.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,786 @@
1
+ from glob import glob
2
+ import os
3
+ import numpy as np
4
+ import pandas as pd
5
+ from PyQt5.QtGui import QKeySequence
6
+ from PyQt5.QtWidgets import QApplication, QCheckBox, QFileDialog, QLineEdit, QMessageBox, QHBoxLayout, QRadioButton, \
7
+ QShortcut, \
8
+ QVBoxLayout, \
9
+ QPushButton, \
10
+ QComboBox, \
11
+ QLabel
12
+ from PyQt5.QtCore import QSize, Qt
13
+ import matplotlib.pyplot as plt
14
+ from matplotlib.ticker import MultipleLocator
15
+ from sklearn.preprocessing import MinMaxScaler
16
+ from superqt import QSearchableComboBox
17
+
18
+ from superqt.fonticon import icon
19
+ from fonticon_mdi6 import MDI6
20
+ import json
21
+
22
+ from celldetective.gui import Styles, CelldetectiveWidget, CelldetectiveMainWindow
23
+ from celldetective.utils import get_software_location
24
+ from celldetective.io import auto_load_number_of_frames, extract_experiment_channels, get_experiment_metadata, \
25
+ get_experiment_labels, load_frames
26
+ from celldetective.gui.gui_utils import FigureCanvas, center_window, color_from_status, color_from_class, ExportPlotBtn
27
+ import gc
28
+
29
+ class BaseAnnotator(CelldetectiveMainWindow, Styles):
30
+
31
+ def __init__(self, parent_window=None, read_config=True):
32
+
33
+ super().__init__()
34
+ self.parent_window = parent_window
35
+ self.read_config = read_config
36
+
37
+ self.mode = self.parent_window.mode
38
+ self.pos = self.parent_window.parent_window.pos
39
+ self.exp_dir = self.parent_window.exp_dir
40
+ self.PxToUm = self.parent_window.parent_window.PxToUm
41
+ self.n_signals = 3
42
+ self.soft_path = get_software_location()
43
+ self.recently_modified = False
44
+ self.selection = []
45
+ self.MinMaxScaler = MinMaxScaler()
46
+ self.fraction = 1
47
+ self.log_scale = False
48
+
49
+ self.instructions_path = self.exp_dir + os.sep.join(['configs', f'signal_annotator_config_{self.mode}.json'])
50
+ self.trajectories_path = self.pos + os.sep.join(['output','tables',f'trajectories_{self.mode}.csv'])
51
+
52
+ self.screen_height = self.parent_window.parent_window.parent_window.screen_height
53
+ self.screen_width = self.parent_window.parent_window.parent_window.screen_width
54
+ self.value_magnitude = 1
55
+
56
+ self.setMinimumWidth(int(0.8 * self.screen_width))
57
+ self.setMinimumHeight(int(0.8 * self.screen_height))
58
+
59
+ self.proceed = True
60
+ self.locate_stack()
61
+ if not self.proceed:
62
+ self.close()
63
+ else:
64
+ if self.read_config:
65
+ self.load_annotator_config()
66
+ self.locate_tracks()
67
+ self._init_base_widgets()
68
+
69
+ def _init_base_widgets(self):
70
+ self._init_base_widgets_left()
71
+ self._init_base_widgets_right()
72
+
73
+ def _init_base_widgets_right(self):
74
+ pass
75
+
76
+ def _init_base_widgets_left(self):
77
+
78
+ self.class_label = QLabel('event: ')
79
+ self.class_choice_cb = QComboBox()
80
+
81
+ cols = np.array(self.df_tracks.columns)
82
+ self.class_cols = np.array([c.startswith('class') for c in list(self.df_tracks.columns)])
83
+
84
+ self.class_cols = list(cols[self.class_cols])
85
+ try:
86
+ self.class_cols.remove('class_id')
87
+ except Exception:
88
+ pass
89
+ try:
90
+ self.class_cols.remove('class_color')
91
+ except Exception:
92
+ pass
93
+
94
+ self.class_choice_cb.addItems(self.class_cols)
95
+ self.class_choice_cb.currentIndexChanged.connect(self.compute_status_and_colors)
96
+
97
+ self.add_class_btn = QPushButton('')
98
+ self.add_class_btn.setStyleSheet(self.button_select_all)
99
+ self.add_class_btn.setIcon(icon(MDI6.plus, color="black"))
100
+ self.add_class_btn.setToolTip("Add a new event class")
101
+ self.add_class_btn.setIconSize(QSize(20, 20))
102
+ self.add_class_btn.clicked.connect(self.create_new_event_class)
103
+
104
+ self.del_class_btn = QPushButton('')
105
+ self.del_class_btn.setStyleSheet(self.button_select_all)
106
+ self.del_class_btn.setIcon(icon(MDI6.delete, color="black"))
107
+ self.del_class_btn.setToolTip("Delete an event class")
108
+ self.del_class_btn.setIconSize(QSize(20, 20))
109
+ self.del_class_btn.clicked.connect(self.del_event_class)
110
+
111
+ self.cell_info = QLabel('')
112
+
113
+ self.correct_btn = QPushButton('correct')
114
+ self.correct_btn.setIcon(icon(MDI6.redo_variant, color="white"))
115
+ self.correct_btn.setIconSize(QSize(20, 20))
116
+ self.correct_btn.setStyleSheet(self.button_style_sheet)
117
+ self.correct_btn.clicked.connect(self.show_annotation_buttons)
118
+ self.correct_btn.setEnabled(False)
119
+
120
+ self.cancel_btn = QPushButton('cancel')
121
+ self.cancel_btn.setStyleSheet(self.button_style_sheet_2)
122
+ self.cancel_btn.setShortcut(QKeySequence("Esc"))
123
+ self.cancel_btn.setEnabled(False)
124
+ self.cancel_btn.clicked.connect(self.cancel_selection)
125
+
126
+ self._init_cell_fig_widgets()
127
+
128
+ self.save_btn = QPushButton('Save')
129
+ self.save_btn.setStyleSheet(self.button_style_sheet)
130
+ self.save_btn.clicked.connect(self.save_trajectories)
131
+
132
+ self.export_btn = QPushButton('')
133
+ self.export_btn.setStyleSheet(self.button_select_all)
134
+ self.export_btn.clicked.connect(self.export_signals)
135
+ self.export_btn.setIcon(icon(MDI6.export, color="black"))
136
+ self.export_btn.setIconSize(QSize(25, 25))
137
+
138
+ def _init_cell_fig_widgets(self):
139
+
140
+ self.generate_signal_choices()
141
+ self.create_cell_signal_canvas()
142
+ self.cell_fcanvas.setMinimumHeight(int(0.2 * self.screen_height))
143
+
144
+ self.outliers_check = QCheckBox('Show outliers')
145
+ self.outliers_check.toggled.connect(self.show_outliers)
146
+
147
+ self.normalize_features_btn = QPushButton('')
148
+ self.normalize_features_btn.setStyleSheet(self.button_select_all)
149
+ self.normalize_features_btn.setIcon(icon(MDI6.arrow_collapse_vertical, color="black"))
150
+ self.normalize_features_btn.setIconSize(QSize(25, 25))
151
+ self.normalize_features_btn.setFixedSize(QSize(30, 30))
152
+ self.normalize_features_btn.clicked.connect(self.normalize_features)
153
+ self.normalized_signals = False
154
+
155
+ self.log_btn = QPushButton()
156
+ self.log_btn.setIcon(icon(MDI6.math_log, color="black"))
157
+ self.log_btn.setStyleSheet(self.button_select_all)
158
+ self.log_btn.clicked.connect(self.switch_to_log)
159
+
160
+ self.export_plot_btn = ExportPlotBtn(self.cell_fig, export_dir = self.exp_dir)
161
+
162
+ def close_without_new_class(self):
163
+ self.newClassWidget.close()
164
+
165
+ def init_class_selection_block(self):
166
+
167
+ self.class_hbox = QHBoxLayout()
168
+ self.class_hbox.setContentsMargins(0,0,0,0)
169
+
170
+ self.class_hbox.addWidget(self.class_label, 25)
171
+ self.class_hbox.addWidget(self.class_choice_cb, 70)
172
+ self.class_hbox.addWidget(self.add_class_btn, 5)
173
+ self.class_hbox.addWidget(self.del_class_btn, 5)
174
+
175
+ def init_options_block(self):
176
+ self.options_hbox = QHBoxLayout()
177
+ self.options_hbox.setContentsMargins(0, 0, 0, 0)
178
+
179
+ def init_correction_block(self):
180
+ self.action_hbox = QHBoxLayout()
181
+ self.action_hbox.setContentsMargins(0,0,0,0)
182
+
183
+ self.action_hbox.addWidget(self.correct_btn)
184
+ self.action_hbox.addWidget(self.cancel_btn)
185
+
186
+ def init_plot_buttons_block(self):
187
+ self.plot_buttons_hbox = QHBoxLayout()
188
+ self.plot_buttons_hbox.setContentsMargins(0, 0, 0, 0)
189
+ self.plot_buttons_hbox.addWidget(QLabel(''), 90)
190
+ self.plot_buttons_hbox.addWidget(self.outliers_check, 5)
191
+ self.plot_buttons_hbox.addWidget(self.normalize_features_btn, 5)
192
+ self.plot_buttons_hbox.addWidget(self.log_btn, 5)
193
+ self.plot_buttons_hbox.addWidget(self.export_plot_btn, 5)
194
+
195
+ def init_save_btn_block(self):
196
+
197
+ self.btn_hbox = QHBoxLayout()
198
+ self.btn_hbox.setContentsMargins(0,10,0,0)
199
+ self.btn_hbox.addWidget(self.save_btn, 90)
200
+ self.btn_hbox.addWidget(self.export_btn, 10)
201
+
202
+ def populate_window(self):
203
+
204
+ """
205
+ Create the multibox design.
206
+
207
+ """
208
+
209
+ # Main layout
210
+ self.button_widget = CelldetectiveWidget()
211
+ self.main_layout = QHBoxLayout()
212
+ self.button_widget.setLayout(self.main_layout)
213
+ self.main_layout.setContentsMargins(30, 30, 30, 30)
214
+
215
+ # Left panel
216
+ self.left_panel = QVBoxLayout()
217
+ self.left_panel.setContentsMargins(30, 5, 30, 5)
218
+ self.left_panel.setSpacing(3)
219
+
220
+ self.init_class_selection_block()
221
+ self.left_panel.addLayout(self.class_hbox, 5)
222
+ self.left_panel.addWidget(self.cell_info,10)
223
+
224
+ self.init_options_block()
225
+ self.init_correction_block()
226
+ self.left_panel.addLayout(self.options_hbox, 5)
227
+ self.left_panel.addLayout(self.action_hbox, 5)
228
+
229
+ self.left_panel.addWidget(self.cell_fcanvas, 45)
230
+
231
+ self.init_plot_buttons_block()
232
+ self.left_panel.addLayout(self.plot_buttons_hbox, 5)
233
+
234
+ signal_choice_vbox = QVBoxLayout()
235
+ signal_choice_vbox.setContentsMargins(30, 0, 30, 0)
236
+ for i in range(len(self.signal_choice_cb)):
237
+ hlayout = QHBoxLayout()
238
+ hlayout.addWidget(self.signal_choice_label[i], 20)
239
+ hlayout.addWidget(self.signal_choice_cb[i], 75)
240
+ signal_choice_vbox.addLayout(hlayout)
241
+
242
+ self.left_panel.addLayout(signal_choice_vbox, 15)
243
+
244
+ self.init_save_btn_block()
245
+ self.left_panel.addLayout(self.btn_hbox, 5)
246
+
247
+ # Right panel
248
+ self.right_panel = QVBoxLayout()
249
+
250
+ # add left/right to main layout
251
+ self.main_layout.addLayout(self.left_panel, 35)
252
+ self.main_layout.addLayout(self.right_panel, 65)
253
+ self.button_widget.adjustSize()
254
+ #self.compute_status_and_colors(0)
255
+
256
+ self.setCentralWidget(self.button_widget)
257
+ # self.show()
258
+
259
+ self.del_shortcut = QShortcut(Qt.Key_Delete, self) # QKeySequence("s")
260
+ self.del_shortcut.activated.connect(self.shortcut_suppr)
261
+ self.del_shortcut.setEnabled(False)
262
+
263
+ QApplication.processEvents()
264
+
265
+ def generate_signal_choices(self):
266
+
267
+ self.signal_choice_cb = [QSearchableComboBox() for i in range(self.n_signals)]
268
+ self.signal_choice_label = [QLabel(f'signal {i + 1}: ') for i in range(self.n_signals)]
269
+ # self.log_btns = [QPushButton() for i in range(self.n_signals)]
270
+
271
+ signals = list(self.df_tracks.columns)
272
+
273
+ to_remove = ['FRAME', 'ID', 'POSITION_X', 'POSITION_Y', 'TRACK_ID', 'antibody', 'cell_type',
274
+ 'class', 'class_color', 'class_id', 'concentration', 'dummy', 'generation',
275
+ 'group', 'group_color', 'index', 'parent', 'pharmaceutical_agent', 'pos_name',
276
+ 'position', 'root', 'state', 'status', 'status_color', 't', 't0', 'well',
277
+ 'well_index', 'well_name', 'x_anim', 'y_anim',
278
+ ]
279
+
280
+ meta = get_experiment_metadata(self.exp_dir)
281
+ if meta is not None:
282
+ keys = list(meta.keys())
283
+ to_remove.extend(keys)
284
+
285
+ labels = get_experiment_labels(self.exp_dir)
286
+ if labels is not None:
287
+ keys = list(labels.keys())
288
+ to_remove.extend(labels)
289
+
290
+ for c in to_remove:
291
+ if c in signals:
292
+ signals.remove(c)
293
+
294
+ for i in range(len(self.signal_choice_cb)):
295
+ self.signal_choice_cb[i].addItems(['--'] + signals)
296
+ self.signal_choice_cb[i].setCurrentIndex(i + 1)
297
+ self.signal_choice_cb[i].currentIndexChanged.connect(self.plot_signals)
298
+
299
+ def on_scatter_pick(self, event):
300
+
301
+ self.event = event
302
+
303
+ self.correct_btn.disconnect()
304
+ self.correct_btn.clicked.connect(self.show_annotation_buttons)
305
+
306
+ print(f"{self.selection=}")
307
+
308
+ ind = event.ind
309
+
310
+ if len(ind) > 1:
311
+ # More than one point in vicinity
312
+ datax, datay = [self.positions[self.framedata][i, 0] for i in ind], [self.positions[self.framedata][i, 1]
313
+ for i in ind]
314
+ msx, msy = event.mouseevent.xdata, event.mouseevent.ydata
315
+ dist = np.sqrt((np.array(datax) - msx) ** 2 + (np.array(datay) - msy) ** 2)
316
+ ind = [ind[np.argmin(dist)]]
317
+
318
+ if len(ind) > 0 and (len(self.selection) == 0):
319
+
320
+ self.selection.append([ind[0], self.framedata])
321
+ self.select_single_cell(ind[0], self.framedata)
322
+
323
+ elif len(ind) > 0 and len(self.selection) == 1:
324
+ self.cancel_btn.click()
325
+ else:
326
+ pass
327
+
328
+ # self.draw_frame(self.current_frame)
329
+ # self.fcanvas.canvas.draw()
330
+ #
331
+ def load_annotator_config(self):
332
+
333
+ """
334
+ Load settings from config or set default values.
335
+ """
336
+
337
+ if os.path.exists(self.instructions_path):
338
+ with open(self.instructions_path, 'r') as f:
339
+
340
+ instructions = json.load(f)
341
+
342
+ if 'rgb_mode' in instructions:
343
+ self.rgb_mode = instructions['rgb_mode']
344
+ else:
345
+ self.rgb_mode = False
346
+
347
+ if 'percentile_mode' in instructions:
348
+ self.percentile_mode = instructions['percentile_mode']
349
+ else:
350
+ self.percentile_mode = True
351
+
352
+ if 'channels' in instructions:
353
+ self.target_channels = instructions['channels']
354
+ else:
355
+ self.target_channels = [[self.channel_names[0], 0.01, 99.99]]
356
+
357
+ if 'fraction' in instructions:
358
+ self.fraction = float(instructions['fraction'])
359
+ else:
360
+ self.fraction = 0.25
361
+
362
+ if 'interval' in instructions:
363
+ self.anim_interval = int(instructions['interval'])
364
+ else:
365
+ self.anim_interval = 1
366
+
367
+ if 'log' in instructions:
368
+ self.log_option = instructions['log']
369
+ else:
370
+ self.log_option = False
371
+ else:
372
+ self.rgb_mode = False
373
+ self.log_option = False
374
+ self.percentile_mode = True
375
+ self.target_channels = [[self.channel_names[0], 0.01, 99.99]]
376
+ self.fraction = 0.25
377
+ self.anim_interval = 1
378
+
379
+
380
+ def locate_stack(self):
381
+
382
+ """
383
+ Locate the target movie.
384
+
385
+ """
386
+
387
+ movies = glob(self.pos + os.sep.join(["movie", f"{self.parent_window.parent_window.movie_prefix}*.tif"]))
388
+
389
+ if len(movies) == 0:
390
+ msgBox = QMessageBox()
391
+ msgBox.setIcon(QMessageBox.Warning)
392
+ msgBox.setText("No movie is detected in the experiment folder.\nPlease check the stack prefix...")
393
+ msgBox.setWindowTitle("Warning")
394
+ msgBox.setStandardButtons(QMessageBox.Ok)
395
+ returnValue = msgBox.exec()
396
+ if returnValue == QMessageBox.Ok:
397
+ self.proceed = False
398
+ self.close()
399
+ else:
400
+ self.close()
401
+ else:
402
+ self.stack_path = movies[0]
403
+ self.len_movie = self.parent_window.parent_window.len_movie
404
+ len_movie_auto = auto_load_number_of_frames(self.stack_path)
405
+ if len_movie_auto is not None:
406
+ self.len_movie = len_movie_auto
407
+ exp_config = self.exp_dir + "config.ini"
408
+ self.channel_names, self.channels = extract_experiment_channels(self.exp_dir)
409
+ self.channel_names = np.array(self.channel_names)
410
+ self.channels = np.array(self.channels)
411
+ self.nbr_channels = len(self.channels)
412
+ self.img = load_frames(0, self.stack_path, normalize_input=False)
413
+ def create_cell_signal_canvas(self):
414
+
415
+ self.cell_fig, self.cell_ax = plt.subplots(tight_layout=True)
416
+ self.cell_fcanvas = FigureCanvas(self.cell_fig, interactive=True)
417
+ self.cell_ax.clear()
418
+
419
+ spacing = 0.5
420
+ minorLocator = MultipleLocator(1)
421
+ self.cell_ax.xaxis.set_minor_locator(minorLocator)
422
+ self.cell_ax.xaxis.set_major_locator(MultipleLocator(5))
423
+ self.cell_ax.grid(which='major')
424
+ self.cell_ax.set_xlabel("time [frame]")
425
+ self.cell_ax.set_ylabel("signal")
426
+
427
+ self.cell_fig.set_facecolor('none') # or 'None'
428
+ self.cell_fig.canvas.setStyleSheet("background-color: transparent;")
429
+
430
+
431
+ self.lines = [
432
+ self.cell_ax.plot([np.linspace(0, self.len_movie - 1, self.len_movie)], [np.zeros((self.len_movie))])[0] for
433
+ i in range(len(self.signal_choice_cb))]
434
+ for i in range(len(self.lines)):
435
+ self.lines[i].set_label(f'signal {i}')
436
+
437
+ min_val, max_val = self.cell_ax.get_ylim()
438
+ self.line_dt, = self.cell_ax.plot([-1, -1], [min_val, max_val], c="k", linestyle="--")
439
+
440
+ self.cell_ax.set_xlim(0, self.len_movie)
441
+ self.cell_ax.legend(fontsize=8)
442
+ self.cell_fcanvas.canvas.draw()
443
+
444
+ def resizeEvent(self, event):
445
+
446
+ super().resizeEvent(event)
447
+ try:
448
+ self.cell_fig.tight_layout()
449
+ except:
450
+ pass
451
+
452
+ def locate_tracks(self):
453
+
454
+ """
455
+ Locate the tracks.
456
+ """
457
+
458
+ if not os.path.exists(self.trajectories_path):
459
+
460
+ msgBox = QMessageBox()
461
+ msgBox.setIcon(QMessageBox.Warning)
462
+ msgBox.setText("The trajectories cannot be detected.")
463
+ msgBox.setWindowTitle("Warning")
464
+ msgBox.setStandardButtons(QMessageBox.Ok)
465
+ returnValue = msgBox.exec()
466
+ if returnValue == QMessageBox.Yes:
467
+ self.close()
468
+ else:
469
+
470
+ # Load and prep tracks
471
+ self.df_tracks = pd.read_csv(self.trajectories_path)
472
+ self.df_tracks = self.df_tracks.sort_values(by=['TRACK_ID', 'FRAME'])
473
+ self.df_tracks.replace([np.inf, -np.inf], np.nan, inplace=True)
474
+
475
+ cols = np.array(self.df_tracks.columns)
476
+ self.class_cols = np.array([c.startswith('class') for c in list(self.df_tracks.columns)])
477
+ self.class_cols = list(cols[self.class_cols])
478
+ try:
479
+ self.class_cols.remove('class_id')
480
+ except:
481
+ pass
482
+ try:
483
+ self.class_cols.remove('class_color')
484
+ except:
485
+ pass
486
+ if len(self.class_cols) > 0:
487
+ self.class_name = self.class_cols[0]
488
+ self.expected_status = 'status'
489
+ suffix = self.class_name.replace('class', '').replace('_', '')
490
+ if suffix != '':
491
+ self.expected_status += '_' + suffix
492
+ self.expected_time = 't_' + suffix
493
+ else:
494
+ self.expected_time = 't0'
495
+ self.time_name = self.expected_time
496
+ self.status_name = self.expected_status
497
+ else:
498
+ self.class_name = 'class'
499
+ self.time_name = 't0'
500
+ self.status_name = 'status'
501
+
502
+ if self.time_name in self.df_tracks.columns and self.class_name in self.df_tracks.columns and not self.status_name in self.df_tracks.columns:
503
+ # only create the status column if it does not exist to not erase static classification results
504
+ self.make_status_column()
505
+ elif self.time_name in self.df_tracks.columns and self.class_name in self.df_tracks.columns:
506
+ # all good, do nothing
507
+ pass
508
+ else:
509
+ if not self.status_name in self.df_tracks.columns:
510
+ self.df_tracks[self.status_name] = 0
511
+ self.df_tracks['status_color'] = color_from_status(0)
512
+ self.df_tracks['class_color'] = color_from_class(1)
513
+
514
+ if not self.class_name in self.df_tracks.columns:
515
+ self.df_tracks[self.class_name] = 1
516
+ if not self.time_name in self.df_tracks.columns:
517
+ self.df_tracks[self.time_name] = -1
518
+
519
+ self.df_tracks['status_color'] = [color_from_status(i) for i in self.df_tracks[self.status_name].to_numpy()]
520
+ self.df_tracks['class_color'] = [color_from_class(i) for i in self.df_tracks[self.class_name].to_numpy()]
521
+
522
+ self.df_tracks = self.df_tracks.dropna(subset=['POSITION_X', 'POSITION_Y'])
523
+ self.df_tracks['x_anim'] = self.df_tracks['POSITION_X'] * self.fraction
524
+ self.df_tracks['y_anim'] = self.df_tracks['POSITION_Y'] * self.fraction
525
+ self.df_tracks['x_anim'] = self.df_tracks['x_anim'].astype(int)
526
+ self.df_tracks['y_anim'] = self.df_tracks['y_anim'].astype(int)
527
+
528
+ self.extract_scatter_from_trajectories()
529
+ self.track_of_interest = self.df_tracks['TRACK_ID'].min()
530
+
531
+ self.loc_t = []
532
+ self.loc_idx = []
533
+ for t in range(len(self.tracks)):
534
+ indices = np.where(self.tracks[t] == self.track_of_interest)[0]
535
+ if len(indices) > 0:
536
+ self.loc_t.append(t)
537
+ self.loc_idx.append(indices[0])
538
+
539
+ self.MinMaxScaler = MinMaxScaler()
540
+ self.columns_to_rescale = list(self.df_tracks.columns)
541
+
542
+ cols_to_remove = ['group', 'group_color', 'status', 'status_color', 'class_color', 'TRACK_ID', 'FRAME',
543
+ 'x_anim', 'y_anim', 't','dummy','group_color',
544
+ 'state', 'generation', 'root', 'parent', 'class_id', 'class', 't0', 'POSITION_X',
545
+ 'POSITION_Y', 'position', 'well', 'well_index', 'well_name', 'pos_name', 'index',
546
+ 'concentration', 'cell_type', 'antibody', 'pharmaceutical_agent', 'ID'] + self.class_cols
547
+
548
+ meta = get_experiment_metadata(self.exp_dir)
549
+ if meta is not None:
550
+ keys = list(meta.keys())
551
+ cols_to_remove.extend(keys)
552
+
553
+ labels = get_experiment_labels(self.exp_dir)
554
+ if labels is not None:
555
+ keys = list(labels.keys())
556
+ cols_to_remove.extend(labels)
557
+
558
+ cols = np.array(list(self.df_tracks.columns))
559
+ time_cols = np.array([c.startswith('t_') for c in cols])
560
+ time_cols = list(cols[time_cols])
561
+ cols_to_remove += time_cols
562
+
563
+ status_cols = np.array([c.startswith('status_') for c in cols])
564
+ status_cols = list(cols[status_cols])
565
+ cols_to_remove += status_cols
566
+
567
+ for tr in cols_to_remove:
568
+ try:
569
+ self.columns_to_rescale.remove(tr)
570
+ except:
571
+ pass
572
+
573
+ x = self.df_tracks[self.columns_to_rescale].values
574
+ self.MinMaxScaler.fit(x)
575
+
576
+ def del_event_class(self):
577
+
578
+ msgBox = QMessageBox()
579
+ msgBox.setIcon(QMessageBox.Warning)
580
+ msgBox.setText(
581
+ f"You are about to delete the class {self.class_choice_cb.currentText()}. The associated time and\nstatus will also be deleted. Do you still want to proceed?")
582
+ msgBox.setWindowTitle("Warning")
583
+ msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
584
+ returnValue = msgBox.exec()
585
+ if returnValue == QMessageBox.No:
586
+ return None
587
+ else:
588
+ class_to_delete = self.class_choice_cb.currentText()
589
+ time_to_delete = class_to_delete.replace('class', 't')
590
+ status_to_delete = class_to_delete.replace('class', 'status')
591
+ cols_to_delete = [class_to_delete, time_to_delete, status_to_delete]
592
+ for c in cols_to_delete:
593
+ try:
594
+ self.df_tracks = self.df_tracks.drop([c], axis=1)
595
+ except Exception as e:
596
+ print(e)
597
+ item_idx = self.class_choice_cb.findText(class_to_delete)
598
+ self.class_choice_cb.removeItem(item_idx)
599
+
600
+ def normalize_features(self):
601
+
602
+ x = self.df_tracks[self.columns_to_rescale].values
603
+
604
+ if not self.normalized_signals:
605
+ x = self.MinMaxScaler.transform(x)
606
+ self.df_tracks[self.columns_to_rescale] = x
607
+ self.plot_signals()
608
+ self.normalized_signals = True
609
+ self.normalize_features_btn.setIcon(icon(MDI6.arrow_collapse_vertical, color="#1565c0"))
610
+ self.normalize_features_btn.setIconSize(QSize(25, 25))
611
+ else:
612
+ x = self.MinMaxScaler.inverse_transform(x)
613
+ self.df_tracks[self.columns_to_rescale] = x
614
+ self.plot_signals()
615
+ self.normalized_signals = False
616
+ self.normalize_features_btn.setIcon(icon(MDI6.arrow_collapse_vertical, color="black"))
617
+ self.normalize_features_btn.setIconSize(QSize(25, 25))
618
+
619
+ def switch_to_log(self):
620
+
621
+ """
622
+ Better would be to create a log(quantity) and plot it...
623
+ """
624
+
625
+ try:
626
+ if self.cell_ax.get_yscale()=='linear':
627
+ ymin,ymax = self.cell_ax.get_ylim()
628
+ self.cell_ax.set_yscale('log')
629
+ self.log_btn.setIcon(icon(MDI6.math_log,color="#1565c0"))
630
+ self.cell_ax.set_ylim(self.value_magnitude, ymax)
631
+ self.log_scale = True
632
+ else:
633
+ self.cell_ax.set_yscale('linear')
634
+ self.log_btn.setIcon(icon(MDI6.math_log,color="black"))
635
+ self.log_scale = False
636
+ except Exception as e:
637
+ print(e)
638
+
639
+ #self.cell_ax.autoscale()
640
+ self.cell_fcanvas.canvas.draw_idle()
641
+
642
+ def start(self):
643
+ '''
644
+ Starts interactive animation. Adds the draw frame command to the GUI
645
+ handler, calls show to start the event loop.
646
+ '''
647
+ self.start_btn.setShortcut(QKeySequence(""))
648
+
649
+ self.last_frame_btn.setEnabled(True)
650
+ self.last_frame_btn.clicked.connect(self.set_last_frame)
651
+
652
+ self.first_frame_btn.setEnabled(True)
653
+ self.first_frame_btn.clicked.connect(self.set_first_frame)
654
+
655
+ self.start_btn.hide()
656
+ self.stop_btn.show()
657
+
658
+ self.anim.event_source.start()
659
+ self.stop_btn.clicked.connect(self.stop)
660
+
661
+ def contrast_slider_action(self):
662
+
663
+ """
664
+ Recontrast the imshow as the contrast slider is moved.
665
+ """
666
+
667
+ self.vmin = self.contrast_slider.value()[0]
668
+ self.vmax = self.contrast_slider.value()[1]
669
+ self.im.set_clim(vmin=self.vmin, vmax=self.vmax)
670
+ self.fcanvas.canvas.draw_idle()
671
+
672
+ def show_outliers(self):
673
+ if self.outliers_check.isChecked():
674
+ self.show_fliers = True
675
+ self.plot_signals()
676
+ else:
677
+ self.show_fliers = False
678
+ self.plot_signals()
679
+
680
+ def export_signals(self):
681
+
682
+ auto_dataset_name = self.pos.split(os.sep)[-4] + '_' + self.pos.split(os.sep)[-2] + '.npy'
683
+
684
+ if self.normalized_signals:
685
+ self.normalize_features_btn.click()
686
+
687
+ training_set = []
688
+ cols = self.df_tracks.columns
689
+ tracks = np.unique(self.df_tracks["TRACK_ID"].to_numpy())
690
+
691
+ for track in tracks:
692
+ # Add all signals at given track
693
+ signals = {}
694
+ for c in cols:
695
+ signals.update({c: self.df_tracks.loc[self.df_tracks["TRACK_ID"] == track, c].to_numpy()})
696
+ time_of_interest = self.df_tracks.loc[self.df_tracks["TRACK_ID"] == track, self.time_name].to_numpy()[0]
697
+ cclass = self.df_tracks.loc[self.df_tracks["TRACK_ID"] == track, self.class_name].to_numpy()[0]
698
+ signals.update({"time_of_interest": time_of_interest, "class": cclass})
699
+ # Here auto add all available channels
700
+ training_set.append(signals)
701
+
702
+ pathsave = QFileDialog.getSaveFileName(self, "Select file name", self.exp_dir + auto_dataset_name, ".npy")[0]
703
+ if pathsave != '':
704
+ if not pathsave.endswith(".npy"):
705
+ pathsave += ".npy"
706
+ try:
707
+ np.save(pathsave, training_set)
708
+ print(f'File successfully written in {pathsave}.')
709
+ except Exception as e:
710
+ print(f"Error {e}...")
711
+
712
+ def create_new_event_class(self):
713
+
714
+ # display qwidget to name the event
715
+ self.newClassWidget = CelldetectiveWidget()
716
+ self.newClassWidget.setWindowTitle('Create new event class')
717
+
718
+ layout = QVBoxLayout()
719
+ self.newClassWidget.setLayout(layout)
720
+ name_hbox = QHBoxLayout()
721
+ name_hbox.addWidget(QLabel('event name: '), 25)
722
+ self.class_name_le = QLineEdit('event')
723
+ name_hbox.addWidget(self.class_name_le, 75)
724
+ layout.addLayout(name_hbox)
725
+
726
+ class_labels = ['event', 'no event', 'else']
727
+ layout.addWidget(QLabel('prefill: '))
728
+ radio_box = QHBoxLayout()
729
+ self.class_option_rb = [QRadioButton() for i in range(3)]
730
+ for i, c in enumerate(self.class_option_rb):
731
+ if i == 0:
732
+ c.setChecked(True)
733
+ c.setText(class_labels[i])
734
+ radio_box.addWidget(c, 33, alignment=Qt.AlignCenter)
735
+ layout.addLayout(radio_box)
736
+
737
+ btn_hbox = QHBoxLayout()
738
+ submit_btn = QPushButton('submit')
739
+ cancel_btn = QPushButton('cancel')
740
+ btn_hbox.addWidget(cancel_btn, 50)
741
+ btn_hbox.addWidget(submit_btn, 50)
742
+ layout.addLayout(btn_hbox)
743
+
744
+ submit_btn.clicked.connect(self.write_new_event_class)
745
+ cancel_btn.clicked.connect(self.close_without_new_class)
746
+
747
+ self.newClassWidget.show()
748
+ center_window(self.newClassWidget)
749
+
750
+ def shortcut_suppr(self):
751
+ self.correct_btn.click()
752
+ self.suppr_btn.click()
753
+ self.correct_btn.click()
754
+
755
+ def closeEvent(self, event):
756
+ # result = QMessageBox.question(self,
757
+ # "Confirm Exit...",
758
+ # "Are you sure you want to exit ?",
759
+ # QMessageBox.Yes| QMessageBox.No,
760
+ # )
761
+ # del self.img
762
+ gc.collect()
763
+
764
+ def save_trajectories(self):
765
+ # specific to signal/static annotator
766
+ print('Save trajectory function not implemented for BaseAnnotator class...')
767
+
768
+ def cancel_selection(self):
769
+ print('we are in cancel selection...')
770
+ self.hide_annotation_buttons()
771
+ self.correct_btn.setEnabled(False)
772
+ self.correct_btn.setText('correct')
773
+ self.cancel_btn.setEnabled(False)
774
+
775
+ try:
776
+ self.selection.pop(0)
777
+ except Exception as e:
778
+ pass
779
+
780
+ try:
781
+ for k, (t, idx) in enumerate(zip(self.loc_t, self.loc_idx)):
782
+ self.colors[t][idx, 0] = self.previous_color[k][0]
783
+ #self.colors[t][idx, 1] = self.previous_color[k][1]
784
+ except Exception as e:
785
+ pass
786
+