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