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
@@ -1,13 +1,37 @@
1
- from PyQt5.QtWidgets import QComboBox, QLabel, QRadioButton, QFileDialog, QApplication, \
2
- QPushButton, QVBoxLayout, QHBoxLayout, QMessageBox, QShortcut, QLineEdit, QSlider
3
- from PyQt5.QtCore import Qt, QSize
1
+ from PyQt5.QtWidgets import (
2
+ QComboBox,
3
+ QLabel,
4
+ QRadioButton,
5
+ QFileDialog,
6
+ QApplication,
7
+ QPushButton,
8
+ QVBoxLayout,
9
+ QHBoxLayout,
10
+ QMessageBox,
11
+ QShortcut,
12
+ QLineEdit,
13
+ QSlider,
14
+ )
15
+ from PyQt5.QtCore import Qt, QSize, QThread, pyqtSignal, QTimer
4
16
  from PyQt5.QtGui import QKeySequence, QIntValidator
5
17
 
6
- from celldetective.gui.gui_utils import center_window, color_from_state
18
+ from celldetective.gui.gui_utils import color_from_state
19
+ from celldetective.gui.base.utils import center_window, pretty_table
7
20
  from superqt import QLabeledDoubleSlider, QLabeledDoubleRangeSlider, QSearchableComboBox
8
- from celldetective.utils import _get_img_num_per_channel
9
- from celldetective.io import load_frames, get_experiment_metadata, get_experiment_labels, locate_labels
10
- from celldetective.gui.gui_utils import FigureCanvas, color_from_status, color_from_class
21
+ from celldetective.utils.image_loaders import (
22
+ locate_labels,
23
+ load_frames,
24
+ _get_img_num_per_channel,
25
+ )
26
+ from celldetective.utils.experiment import (
27
+ get_experiment_metadata,
28
+ get_experiment_labels,
29
+ )
30
+ from celldetective.gui.gui_utils import (
31
+ color_from_status,
32
+ color_from_class,
33
+ )
34
+ from celldetective.gui.base.figure_canvas import FigureCanvas
11
35
  import numpy as np
12
36
  from superqt.fonticon import icon
13
37
  from fonticon_mdi6 import MDI6
@@ -19,1667 +43,2004 @@ from matplotlib.animation import FuncAnimation
19
43
  from matplotlib.cm import tab10
20
44
  import pandas as pd
21
45
  from sklearn.preprocessing import MinMaxScaler
22
- from celldetective.gui import CelldetectiveWidget
23
- from celldetective.measure import contour_of_instance_segmentation
24
- from celldetective.utils import pretty_table
46
+ from celldetective.gui.base.components import CelldetectiveWidget
47
+ from celldetective.utils.masks import contour_of_instance_segmentation
25
48
  from celldetective.gui.base_annotator import BaseAnnotator
26
49
 
27
- class EventAnnotator(BaseAnnotator):
28
- """
29
- UI to set tracking parameters for bTrack.
30
-
31
- """
32
-
33
- def __init__(self, *args, **kwargs):
34
-
35
- super().__init__(*args, **kwargs)
36
- self.setWindowTitle("Signal annotator")
37
-
38
- # default params
39
- self.class_name = 'class'
40
- self.time_name = 't0'
41
- self.status_name = 'status'
42
-
43
- # self.locate_stack()
44
- if not self.proceed:
45
- self.close()
46
- else:
47
- #self.load_annotator_config()
48
- #self.locate_tracks()
49
- self.prepare_stack()
50
-
51
- self.frame_lbl = QLabel('frame: ')
52
- self.looped_animation()
53
- self.init_event_buttons()
54
- self.populate_window()
55
-
56
- self.outliers_check.hide()
57
- if hasattr(self, "contrast_slider"):
58
- self.im.set_clim(self.contrast_slider.value()[0], self.contrast_slider.value()[1])
59
-
60
- def init_event_buttons(self):
61
-
62
- self.event_btn = QRadioButton('event')
63
- self.event_btn.setStyleSheet(self.button_style_sheet_2)
64
- self.event_btn.toggled.connect(self.enable_time_of_interest)
65
-
66
- self.no_event_btn = QRadioButton('no event')
67
- self.no_event_btn.setStyleSheet(self.button_style_sheet_2)
68
- self.no_event_btn.toggled.connect(self.enable_time_of_interest)
69
-
70
- self.else_btn = QRadioButton('else')
71
- self.else_btn.setStyleSheet(self.button_style_sheet_2)
72
- self.else_btn.toggled.connect(self.enable_time_of_interest)
73
-
74
- self.suppr_btn = QRadioButton('remove')
75
- self.suppr_btn.setToolTip('Mark for deletion. Upon saving, the cell\nwill be removed from the tables.')
76
- self.suppr_btn.setStyleSheet(self.button_style_sheet_2)
77
- self.suppr_btn.toggled.connect(self.enable_time_of_interest)
78
-
79
- self.time_of_interest_label = QLabel('time of interest: ')
80
- self.time_of_interest_le = QLineEdit()
81
-
82
-
83
- def populate_options_layout(self):
84
-
85
- # clear options hbox
86
- for i in reversed(range(self.options_hbox.count())):
87
- self.options_hbox.itemAt(i).widget().setParent(None)
88
-
89
- options_layout = QVBoxLayout()
90
- # add new widgets
91
-
92
- btn_hbox = QHBoxLayout()
93
- btn_hbox.addWidget(self.event_btn, 25, alignment=Qt.AlignCenter)
94
- btn_hbox.addWidget(self.no_event_btn, 25, alignment=Qt.AlignCenter)
95
- btn_hbox.addWidget(self.else_btn, 25, alignment=Qt.AlignCenter)
96
- btn_hbox.addWidget(self.suppr_btn, 25, alignment=Qt.AlignCenter)
97
-
98
- time_option_hbox = QHBoxLayout()
99
- time_option_hbox.setContentsMargins(0, 5, 100, 10)
100
- time_option_hbox.addWidget(self.time_of_interest_label, 10)
101
- time_option_hbox.addWidget(self.time_of_interest_le, 15)
102
- time_option_hbox.addWidget(QLabel(''), 75)
103
-
104
- options_layout.addLayout(btn_hbox)
105
- options_layout.addLayout(time_option_hbox)
106
-
107
- self.options_hbox.addLayout(options_layout)
108
-
109
- self.annotation_btns_to_hide = [self.event_btn, self.no_event_btn,
110
- self.else_btn, self.time_of_interest_label,
111
- self.time_of_interest_le, self.suppr_btn]
112
- self.hide_annotation_buttons()
113
-
114
- def populate_window(self):
115
-
116
- """
117
- Create the multibox design.
118
-
119
- """
120
-
121
- super().populate_window()
122
- self.populate_options_layout()
123
-
124
- self.del_shortcut = QShortcut(Qt.Key_Delete, self) # QKeySequence("s")
125
- self.del_shortcut.activated.connect(self.shortcut_suppr)
126
- self.del_shortcut.setEnabled(False)
127
-
128
- self.no_event_shortcut = QShortcut(QKeySequence("n"), self) # QKeySequence("s")
129
- self.no_event_shortcut.activated.connect(self.shortcut_no_event)
130
- self.no_event_shortcut.setEnabled(False)
131
-
132
- # Right side
133
- animation_buttons_box = QHBoxLayout()
134
- animation_buttons_box.addWidget(self.frame_lbl, 20, alignment=Qt.AlignLeft)
135
-
136
- self.first_frame_btn = QPushButton()
137
- self.first_frame_btn.clicked.connect(self.set_first_frame)
138
- self.first_frame_btn.setShortcut(QKeySequence('f'))
139
- self.first_frame_btn.setIcon(icon(MDI6.page_first, color="black"))
140
- self.first_frame_btn.setStyleSheet(self.button_select_all)
141
- self.first_frame_btn.setFixedSize(QSize(60, 60))
142
- self.first_frame_btn.setIconSize(QSize(30, 30))
143
-
144
- self.last_frame_btn = QPushButton()
145
- self.last_frame_btn.clicked.connect(self.set_last_frame)
146
- self.last_frame_btn.setShortcut(QKeySequence('l'))
147
- self.last_frame_btn.setIcon(icon(MDI6.page_last, color="black"))
148
- self.last_frame_btn.setStyleSheet(self.button_select_all)
149
- self.last_frame_btn.setFixedSize(QSize(60, 60))
150
- self.last_frame_btn.setIconSize(QSize(30, 30))
151
-
152
- self.stop_btn = QPushButton()
153
- self.stop_btn.clicked.connect(self.stop)
154
- self.stop_btn.setIcon(icon(MDI6.stop, color="black"))
155
- self.stop_btn.setStyleSheet(self.button_select_all)
156
- self.stop_btn.setFixedSize(QSize(60, 60))
157
- self.stop_btn.setIconSize(QSize(30, 30))
158
-
159
- self.start_btn = QPushButton()
160
- self.start_btn.clicked.connect(self.start)
161
- self.start_btn.setIcon(icon(MDI6.play, color="black"))
162
- self.start_btn.setFixedSize(QSize(60, 60))
163
- self.start_btn.setStyleSheet(self.button_select_all)
164
- self.start_btn.setIconSize(QSize(30, 30))
165
- self.start_btn.hide()
166
-
167
- animation_buttons_box.addWidget(self.first_frame_btn, 5, alignment=Qt.AlignRight)
168
- animation_buttons_box.addWidget(self.stop_btn, 5, alignment=Qt.AlignRight)
169
- animation_buttons_box.addWidget(self.start_btn, 5, alignment=Qt.AlignRight)
170
- animation_buttons_box.addWidget(self.last_frame_btn, 5, alignment=Qt.AlignRight)
171
-
172
- self.right_panel.addLayout(animation_buttons_box, 5)
173
- self.right_panel.addWidget(self.fcanvas, 90)
174
-
175
- if not self.rgb_mode:
176
- contrast_hbox = QHBoxLayout()
177
- contrast_hbox.setContentsMargins(150, 5, 150, 5)
178
- self.contrast_slider = QLabeledDoubleRangeSlider()
179
- self.contrast_slider.setSingleStep(0.001)
180
- self.contrast_slider.setTickInterval(0.001)
181
- self.contrast_slider.setOrientation(Qt.Horizontal)
182
- self.contrast_slider.setRange(
183
- *[np.nanpercentile(self.stack, 0.001), np.nanpercentile(self.stack, 99.999)])
184
- self.contrast_slider.setValue(
185
- [np.nanpercentile(self.stack, 1), np.nanpercentile(self.stack, 99.99)])
186
- self.contrast_slider.valueChanged.connect(self.contrast_slider_action)
187
- contrast_hbox.addWidget(QLabel('contrast: '))
188
- contrast_hbox.addWidget(self.contrast_slider, 90)
189
- self.right_panel.addLayout(contrast_hbox, 5)
190
-
191
- if self.class_choice_cb.currentText()!="":
192
- self.compute_status_and_colors(0)
193
-
194
- self.no_event_shortcut = QShortcut(QKeySequence("n"), self) # QKeySequence("s")
195
- self.no_event_shortcut.activated.connect(self.shortcut_no_event)
196
- self.no_event_shortcut.setEnabled(False)
197
-
198
- QApplication.processEvents()
199
-
200
- def write_new_event_class(self):
201
-
202
- if self.class_name_le.text() == '':
203
- self.target_class = 'class'
204
- self.target_time = 't0'
205
- else:
206
- self.target_class = 'class_' + self.class_name_le.text()
207
- self.target_time = 't_' + self.class_name_le.text()
208
-
209
- if self.target_class in list(self.df_tracks.columns):
210
-
211
- msgBox = QMessageBox()
212
- msgBox.setIcon(QMessageBox.Warning)
213
- msgBox.setText(
214
- "This event name already exists. If you proceed,\nall annotated data will be rewritten. Do you wish to continue?")
215
- msgBox.setWindowTitle("Warning")
216
- msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
217
- returnValue = msgBox.exec()
218
- if returnValue == QMessageBox.No:
219
- return None
220
- else:
221
- pass
222
-
223
- fill_option = np.where([c.isChecked() for c in self.class_option_rb])[0][0]
224
- self.df_tracks.loc[:, self.target_class] = fill_option
225
- if fill_option == 0:
226
- self.df_tracks.loc[:, self.target_time] = 0.1
227
- else:
228
- self.df_tracks.loc[:, self.target_time] = -1
229
-
230
- self.class_choice_cb.clear()
231
- cols = np.array(self.df_tracks.columns)
232
- self.class_cols = np.array([c.startswith('class') for c in list(self.df_tracks.columns)])
233
- self.class_cols = list(cols[self.class_cols])
234
- if 'class_id' in self.class_cols:
235
- self.class_cols.remove('class_id')
236
- if 'class_color' in self.class_cols:
237
- self.class_cols.remove('class_color')
238
- self.class_choice_cb.addItems(self.class_cols)
239
- idx = self.class_choice_cb.findText(self.target_class)
240
- self.class_choice_cb.setCurrentIndex(idx)
241
-
242
- self.newClassWidget.close()
243
-
244
- # def close_without_new_class(self):
245
- # self.newClassWidget.close()
246
-
247
- def compute_status_and_colors(self, i):
248
-
249
- self.class_name = self.class_choice_cb.currentText()
250
- self.expected_status = 'status'
251
- suffix = self.class_name.replace('class', '').replace('_', '', 1)
252
- if suffix != '':
253
- self.expected_status += '_' + suffix
254
- self.expected_time = 't_' + suffix
255
- else:
256
- self.expected_time = 't0'
257
- self.time_name = self.expected_time
258
- self.status_name = self.expected_status
259
-
260
- cols = list(self.df_tracks.columns)
261
-
262
- if self.time_name in cols and self.class_name in cols and not self.status_name in cols:
263
- # only create the status column if it does not exist to not erase static classification results
264
- self.make_status_column()
265
- elif self.time_name in cols and self.class_name in cols and self.df_tracks[self.status_name].isnull().all():
266
- self.make_status_column()
267
- elif self.time_name in cols and self.class_name in cols:
268
- # all good, do nothing
269
- pass
270
- else:
271
- if not self.status_name in self.df_tracks.columns:
272
- self.df_tracks[self.status_name] = 0
273
- self.df_tracks['status_color'] = color_from_status(0)
274
- self.df_tracks['class_color'] = color_from_class(1)
275
-
276
- if not self.class_name in self.df_tracks.columns:
277
- self.df_tracks[self.class_name] = 1
278
- if not self.time_name in self.df_tracks.columns:
279
- self.df_tracks[self.time_name] = -1
280
-
281
- self.df_tracks['status_color'] = [color_from_status(i) for i in self.df_tracks[self.status_name].to_numpy()]
282
- self.df_tracks['class_color'] = [color_from_class(i) for i in self.df_tracks[self.class_name].to_numpy()]
283
-
284
- self.extract_scatter_from_trajectories()
285
- if len(self.selection)>0:
286
- self.select_single_cell(self.selection[0][0], self.selection[0][1])
287
-
288
- self.fcanvas.canvas.draw()
289
-
290
- def cancel_selection(self):
291
-
292
- super().cancel_selection()
293
- try:
294
- for k, (t, idx) in enumerate(zip(self.loc_t, self.loc_idx)):
295
- self.colors[t][idx, 1] = self.previous_color[k][1]
296
- except Exception as e:
297
- pass
298
-
299
- def hide_annotation_buttons(self):
300
-
301
- for a in self.annotation_btns_to_hide:
302
- a.hide()
303
- for b in [self.event_btn, self.no_event_btn, self.else_btn, self.suppr_btn]:
304
- b.setChecked(False)
305
- self.time_of_interest_label.setEnabled(False)
306
- self.time_of_interest_le.setText('')
307
- self.time_of_interest_le.setEnabled(False)
308
-
309
- def enable_time_of_interest(self):
310
-
311
- if self.event_btn.isChecked():
312
- self.time_of_interest_label.setEnabled(True)
313
- self.time_of_interest_le.setEnabled(True)
314
- else:
315
- self.time_of_interest_label.setEnabled(False)
316
- self.time_of_interest_le.setEnabled(False)
317
-
318
- def show_annotation_buttons(self):
319
-
320
- for a in self.annotation_btns_to_hide:
321
- a.show()
322
-
323
- cclass = self.df_tracks.loc[self.df_tracks['TRACK_ID'] == self.track_of_interest, self.class_name].to_numpy()[0]
324
- t0 = self.df_tracks.loc[self.df_tracks['TRACK_ID'] == self.track_of_interest, self.time_name].to_numpy()[0]
325
-
326
- if cclass == 0:
327
- self.event_btn.setChecked(True)
328
- self.time_of_interest_le.setText(str(t0))
329
- elif cclass == 1:
330
- self.no_event_btn.setChecked(True)
331
- elif cclass == 2:
332
- self.else_btn.setChecked(True)
333
- elif cclass > 2:
334
- self.suppr_btn.setChecked(True)
335
-
336
- self.enable_time_of_interest()
337
- self.correct_btn.setText('submit')
338
-
339
- self.correct_btn.disconnect()
340
- self.correct_btn.clicked.connect(self.apply_modification)
341
-
342
- def apply_modification(self):
343
-
344
- t0 = -1
345
- if self.event_btn.isChecked():
346
- cclass = 0
347
- try:
348
- t0 = float(self.time_of_interest_le.text().replace(',', '.'))
349
- self.line_dt.set_xdata([t0, t0])
350
- self.cell_fcanvas.canvas.draw_idle()
351
- except Exception as e:
352
- print(f"L 598 {e=}")
353
- t0 = -1
354
- cclass = 2
355
- elif self.no_event_btn.isChecked():
356
- cclass = 1
357
- elif self.else_btn.isChecked():
358
- cclass = 2
359
- elif self.suppr_btn.isChecked():
360
- cclass = 42
361
-
362
- self.df_tracks.loc[self.df_tracks['TRACK_ID'] == self.track_of_interest, self.class_name] = cclass
363
- self.df_tracks.loc[self.df_tracks['TRACK_ID'] == self.track_of_interest, self.time_name] = t0
364
-
365
- indices = self.df_tracks.loc[self.df_tracks['TRACK_ID'] == self.track_of_interest, self.class_name].index
366
- timeline = self.df_tracks.loc[self.df_tracks['TRACK_ID'] == self.track_of_interest, 'FRAME'].to_numpy()
367
- status = np.zeros_like(timeline)
368
- if t0 > 0:
369
- status[timeline >= t0] = 1.
370
- if cclass == 2:
371
- status[:] = 2
372
- if cclass > 2:
373
- status[:] = 42
374
-
375
- status_color = [color_from_status(s, recently_modified=True) for s in status]
376
- class_color = [color_from_class(cclass, recently_modified=True) for i in range(len(status))]
377
-
378
-
379
- # self.df_tracks['status_color'] = [color_from_status(i) for i in self.df_tracks[self.status_name].to_numpy()]
380
- # self.df_tracks['class_color'] = [color_from_class(i) for i in self.df_tracks[self.class_name].to_numpy()]
381
-
382
- self.df_tracks.loc[indices, self.status_name] = status
383
- self.df_tracks.loc[indices, 'status_color'] = status_color
384
- self.df_tracks.loc[indices, 'class_color'] = class_color
385
-
386
- # self.make_status_column()
387
- self.extract_scatter_from_trajectories()
388
- self.give_cell_information()
389
-
390
- self.correct_btn.disconnect()
391
- self.correct_btn.clicked.connect(self.show_annotation_buttons)
392
- # self.cancel_btn.click()
393
-
394
- self.hide_annotation_buttons()
395
- self.correct_btn.setEnabled(False)
396
- self.correct_btn.setText('correct')
397
- self.cancel_btn.setEnabled(False)
398
- self.del_shortcut.setEnabled(False)
399
- self.no_event_shortcut.setEnabled(False)
400
-
401
- self.selection.pop(0)
402
-
403
-
404
- def make_status_column(self):
405
-
406
- print(f'Generating status information for class `{self.class_name}` and time `{self.time_name}`...')
407
- for tid, group in self.df_tracks.groupby('TRACK_ID'):
408
-
409
- indices = group.index
410
- t0 = group[self.time_name].to_numpy()[0]
411
- cclass = group[self.class_name].to_numpy()[0]
412
- timeline = group['FRAME'].to_numpy()
413
- status = np.zeros_like(timeline)
414
-
415
- if t0 > 0:
416
- status[timeline >= t0] = 1.
417
- # if cclass == 2:
418
- # status[:] = 1.
419
- if cclass > 2:
420
- status[:] = 42
421
-
422
- status_color = [color_from_status(s) for s in status]
423
- class_color = [color_from_class(cclass) for i in range(len(status))]
424
-
425
- self.df_tracks.loc[indices, self.status_name] = status
426
- self.df_tracks.loc[indices, 'status_color'] = status_color
427
- self.df_tracks.loc[indices, 'class_color'] = class_color
428
-
429
- def generate_signal_choices(self):
430
-
431
- self.signal_choice_cb = [QSearchableComboBox() for i in range(self.n_signals)]
432
- self.signal_choice_label = [QLabel(f'signal {i + 1}: ') for i in range(self.n_signals)]
433
- # self.log_btns = [QPushButton() for i in range(self.n_signals)]
434
-
435
- signals = list(self.df_tracks.columns)
436
-
437
- to_remove = ['TRACK_ID', 'FRAME', 'x_anim', 'y_anim', 't', 'state', 'generation', 'root', 'parent', 'class_id',
438
- 'class', 't0', 'POSITION_X', 'POSITION_Y', 'position', 'well', 'well_index', 'well_name',
439
- 'pos_name', 'index','class_color','status_color','dummy','group_color']
440
-
441
- meta = get_experiment_metadata(self.exp_dir)
442
- if meta is not None:
443
- keys = list(meta.keys())
444
- to_remove.extend(keys)
445
-
446
- labels = get_experiment_labels(self.exp_dir)
447
- if labels is not None:
448
- keys = list(labels.keys())
449
- to_remove.extend(labels)
450
-
451
- for c in to_remove:
452
- if c in signals:
453
- signals.remove(c)
454
-
455
- for i in range(len(self.signal_choice_cb)):
456
- self.signal_choice_cb[i].addItems(['--'] + signals)
457
- self.signal_choice_cb[i].setCurrentIndex(i + 1)
458
- self.signal_choice_cb[i].currentIndexChanged.connect(self.plot_signals)
459
-
460
- def plot_signals(self):
461
-
462
- range_values = []
463
-
464
- try:
465
- yvalues = []
466
- for i in range(len(self.signal_choice_cb)):
467
-
468
- signal_choice = self.signal_choice_cb[i].currentText()
469
- lbl = signal_choice
470
- n_cut = 35
471
- if len(lbl)>n_cut:
472
- lbl = lbl[:(n_cut-3)]+'...'
473
- self.lines[i].set_label(lbl)
474
-
475
- if signal_choice == "--":
476
- self.lines[i].set_xdata([])
477
- self.lines[i].set_ydata([])
478
- else:
479
- xdata = self.df_tracks.loc[self.df_tracks['TRACK_ID'] == self.track_of_interest, 'FRAME'].to_numpy()
480
- ydata = self.df_tracks.loc[
481
- self.df_tracks['TRACK_ID'] == self.track_of_interest, signal_choice].to_numpy()
482
-
483
- range_values.extend(ydata)
484
-
485
- xdata = xdata[ydata == ydata] # remove nan
486
- ydata = ydata[ydata == ydata]
487
-
488
- yvalues.extend(ydata)
489
- self.lines[i].set_xdata(xdata)
490
- self.lines[i].set_ydata(ydata)
491
- self.lines[i].set_color(tab10(i / 3.))
492
-
493
- self.configure_ylims()
494
-
495
- min_val, max_val = self.cell_ax.get_ylim()
496
- t0 = \
497
- self.df_tracks.loc[self.df_tracks['TRACK_ID'] == self.track_of_interest, self.expected_time].to_numpy()[
498
- 0]
499
- self.line_dt.set_xdata([t0, t0])
500
- self.line_dt.set_ydata([min_val, max_val])
501
-
502
- self.cell_ax.legend(fontsize=8)
503
- self.cell_fcanvas.canvas.draw()
504
- except Exception as e:
505
- print(e)
506
- pass
507
-
508
- if len(range_values)>0:
509
- range_values = np.array(range_values)
510
- if len(range_values[range_values==range_values])>0:
511
- if len(range_values[range_values>0])>0:
512
- self.value_magnitude = np.nanpercentile(range_values, 1)
513
- else:
514
- self.value_magnitude = 1
515
- self.non_log_ymin = 0.98*np.nanmin(range_values)
516
- self.non_log_ymax = np.nanmax(range_values)*1.02
517
- if self.cell_ax.get_yscale()=='linear':
518
- self.cell_ax.set_ylim(self.non_log_ymin, self.non_log_ymax)
519
- else:
520
- self.cell_ax.set_ylim(self.value_magnitude, self.non_log_ymax)
521
-
522
- def extract_scatter_from_trajectories(self):
523
-
524
- self.positions = []
525
- self.colors = []
526
- self.tracks = []
527
-
528
- for t in np.arange(self.len_movie):
529
- self.positions.append(self.df_tracks.loc[self.df_tracks['FRAME'] == t, ['x_anim', 'y_anim']].to_numpy())
530
- self.colors.append(
531
- self.df_tracks.loc[self.df_tracks['FRAME'] == t, ['class_color', 'status_color']].to_numpy())
532
- self.tracks.append(self.df_tracks.loc[self.df_tracks['FRAME'] == t, 'TRACK_ID'].to_numpy())
533
-
534
- # def load_annotator_config(self):
535
- #
536
- # """
537
- # Load settings from config or set default values.
538
- # """
539
- #
540
- # if os.path.exists(self.instructions_path):
541
- # with open(self.instructions_path, 'r') as f:
542
- #
543
- # instructions = json.load(f)
544
- #
545
- # if 'rgb_mode' in instructions:
546
- # self.rgb_mode = instructions['rgb_mode']
547
- # else:
548
- # self.rgb_mode = False
549
- #
550
- # if 'percentile_mode' in instructions:
551
- # self.percentile_mode = instructions['percentile_mode']
552
- # else:
553
- # self.percentile_mode = True
554
- #
555
- # if 'channels' in instructions:
556
- # self.target_channels = instructions['channels']
557
- # else:
558
- # self.target_channels = [[self.channel_names[0], 0.01, 99.99]]
559
- #
560
- # if 'fraction' in instructions:
561
- # self.fraction = float(instructions['fraction'])
562
- # else:
563
- # self.fraction = 0.25
564
- #
565
- # if 'interval' in instructions:
566
- # self.anim_interval = int(instructions['interval'])
567
- # else:
568
- # self.anim_interval = 1
569
- #
570
- # if 'log' in instructions:
571
- # self.log_option = instructions['log']
572
- # else:
573
- # self.log_option = False
574
- # else:
575
- # self.rgb_mode = False
576
- # self.log_option = False
577
- # self.percentile_mode = True
578
- # self.target_channels = [[self.channel_names[0], 0.01, 99.99]]
579
- # self.fraction = 0.25
580
- # self.anim_interval = 1
581
-
582
- def prepare_stack(self):
583
-
584
- self.img_num_channels = _get_img_num_per_channel(self.channels, self.len_movie, self.nbr_channels)
585
- self.stack = []
586
- disable_tqdm = not len(self.target_channels)>1
587
- for ch in tqdm(self.target_channels, desc="channel",disable=disable_tqdm):
588
- target_ch_name = ch[0]
589
- if self.percentile_mode:
590
- normalize_kwargs = {"percentiles": (ch[1], ch[2]), "values": None}
591
- else:
592
- normalize_kwargs = {"values": (ch[1], ch[2]), "percentiles": None}
593
-
594
- if self.rgb_mode:
595
- normalize_kwargs.update({'amplification': 255., 'clip': True})
596
-
597
- chan = []
598
- indices = self.img_num_channels[self.channels[np.where(self.channel_names == target_ch_name)][0]]
599
- for t in tqdm(range(len(indices)), desc='frame'):
600
- if self.rgb_mode:
601
- f = load_frames(indices[t], self.stack_path, scale=self.fraction, normalize_input=True,
602
- normalize_kwargs=normalize_kwargs)
603
- f = f.astype(np.uint8)
604
- else:
605
- f = load_frames(indices[t], self.stack_path, scale=self.fraction, normalize_input=False)
606
-
607
- chan.append(f[:, :, 0])
608
-
609
- self.stack.append(chan)
610
-
611
- self.stack = np.array(self.stack)
612
- if self.rgb_mode:
613
- self.stack = np.moveaxis(self.stack, 0, -1)
614
- else:
615
- self.stack = self.stack[0]
616
- if self.log_option:
617
- self.stack[np.where(self.stack > 0.)] = np.log(self.stack[np.where(self.stack > 0.)])
618
-
619
- def closeEvent(self, event):
620
-
621
- try:
622
- self.stop()
623
- del self.stack
624
- gc.collect()
625
- except:
626
- pass
627
-
628
- def looped_animation(self):
629
-
630
- """
631
- Load an image.
632
-
633
- """
634
-
635
- self.framedata = 0
636
-
637
- self.fig, self.ax = plt.subplots(tight_layout=True)
638
- self.fcanvas = FigureCanvas(self.fig, interactive=True)
639
- self.ax.clear()
640
-
641
- self.im = self.ax.imshow(self.stack[0], cmap='gray', interpolation='none')
642
- self.status_scatter = self.ax.scatter(self.positions[0][:, 0], self.positions[0][:, 1], marker="x",
643
- c=self.colors[0][:, 1], s=50, picker=True, pickradius=100)
644
- self.class_scatter = self.ax.scatter(self.positions[0][:, 0], self.positions[0][:, 1], marker='o',
645
- facecolors='none', edgecolors=self.colors[0][:, 0], s=200)
646
-
647
-
648
- self.ax.set_xticks([])
649
- self.ax.set_yticks([])
650
- self.ax.set_aspect('equal')
651
-
652
- self.fig.set_facecolor('none') # or 'None'
653
- self.fig.canvas.setStyleSheet("background-color: black;")
654
-
655
- self.anim = FuncAnimation(
656
- self.fig,
657
- self.draw_frame,
658
- frames=self.len_movie, # better would be to cast np.arange(len(movie)) in case frame column is incomplete
659
- interval=self.anim_interval, # in ms
660
- blit=True,
661
- )
662
-
663
- self.fig.canvas.mpl_connect('pick_event', self.on_scatter_pick)
664
- self.fcanvas.canvas.draw()
665
-
666
- def select_single_cell(self, index, timepoint):
667
-
668
- self.correct_btn.setEnabled(True)
669
- self.cancel_btn.setEnabled(True)
670
- self.del_shortcut.setEnabled(True)
671
- self.no_event_shortcut.setEnabled(True)
672
-
673
- self.track_of_interest = self.tracks[timepoint][index]
674
- print(f'You selected cell #{self.track_of_interest}...')
675
- self.give_cell_information()
676
- self.plot_signals()
677
-
678
- self.loc_t = []
679
- self.loc_idx = []
680
- for t in range(len(self.tracks)):
681
- indices = np.where(self.tracks[t] == self.track_of_interest)[0]
682
- if len(indices) > 0:
683
- self.loc_t.append(t)
684
- self.loc_idx.append(indices[0])
685
-
686
- self.previous_color = []
687
- for t, idx in zip(self.loc_t, self.loc_idx):
688
- self.previous_color.append(self.colors[t][idx].copy())
689
- self.colors[t][idx] = 'lime'
690
-
691
-
692
- def shortcut_no_event(self):
693
- self.correct_btn.click()
694
- self.no_event_btn.click()
695
- self.correct_btn.click()
696
-
697
- def configure_ylims(self):
698
-
699
- try:
700
- min_values = []
701
- max_values = []
702
- feats = []
703
- for i in range(len(self.signal_choice_cb)):
704
- signal = self.signal_choice_cb[i].currentText()
705
- if signal == '--':
706
- continue
707
- else:
708
- maxx = np.nanpercentile(self.df_tracks.loc[:, signal].to_numpy().flatten(), 99)
709
- minn = np.nanpercentile(self.df_tracks.loc[:, signal].to_numpy().flatten(), 1)
710
- min_values.append(minn)
711
- max_values.append(maxx)
712
- feats.append(signal)
713
-
714
- smallest_value = np.amin(min_values)
715
- feat_smallest_value = feats[np.argmin(min_values)]
716
- min_feat = self.df_tracks[feat_smallest_value].min()
717
- max_feat = self.df_tracks[feat_smallest_value].max()
718
- pad_small = (max_feat - min_feat) * 0.05
719
- if pad_small==0:
720
- pad_small = 0.05
721
-
722
- largest_value = np.amax(max_values)
723
- feat_largest_value = feats[np.argmax(max_values)]
724
- min_feat = self.df_tracks[feat_largest_value].min()
725
- max_feat = self.df_tracks[feat_largest_value].max()
726
- pad_large = (max_feat - min_feat) * 0.05
727
- if pad_large==0:
728
- pad_large = 0.05
729
-
730
- if len(min_values) > 0:
731
- self.cell_ax.set_ylim(smallest_value - pad_small, largest_value + pad_large)
732
- except Exception as e:
733
- print(f"L1170 {e=}")
734
- pass
735
-
736
- def draw_frame(self, framedata):
737
-
738
- """
739
- Update plot elements at each timestep of the loop.
740
- """
741
-
742
- self.framedata = framedata
743
- self.frame_lbl.setText(f'frame: {self.framedata}')
744
- self.im.set_array(self.stack[self.framedata])
745
- self.status_scatter.set_offsets(self.positions[self.framedata])
746
- self.status_scatter.set_color(self.colors[self.framedata][:, 1])
747
-
748
- self.class_scatter.set_offsets(self.positions[self.framedata])
749
- self.class_scatter.set_edgecolor(self.colors[self.framedata][:, 0])
750
-
751
- return (self.im, self.status_scatter, self.class_scatter,)
752
-
753
- def stop(self):
754
- # # On stop we disconnect all of our events.
755
- self.stop_btn.hide()
756
- self.start_btn.show()
757
- self.anim.pause()
758
- self.stop_btn.clicked.connect(self.start)
759
-
760
- def give_cell_information(self):
761
-
762
- cell_selected = f"cell: {self.track_of_interest}\n"
763
- cell_class = f"class: {self.df_tracks.loc[self.df_tracks['TRACK_ID'] == self.track_of_interest, self.class_name].to_numpy()[0]}\n"
764
- cell_time = f"time of interest: {self.df_tracks.loc[self.df_tracks['TRACK_ID'] == self.track_of_interest, self.time_name].to_numpy()[0]}\n"
765
- self.cell_info.setText(cell_selected + cell_class + cell_time)
766
-
767
- def save_trajectories(self):
768
-
769
- if self.normalized_signals:
770
- self.normalize_features_btn.click()
771
- if self.selection:
772
- self.cancel_selection()
773
-
774
- self.df_tracks = self.df_tracks.drop(self.df_tracks[self.df_tracks[self.class_name] > 2].index)
775
- self.df_tracks.to_csv(self.trajectories_path, index=False)
776
- print('Table successfully exported...')
777
- if self.class_choice_cb.currentText()!="":
778
- self.compute_status_and_colors(0)
779
- self.extract_scatter_from_trajectories()
780
-
781
-
782
- def set_last_frame(self):
783
-
784
- self.last_frame_btn.setEnabled(False)
785
- self.last_frame_btn.disconnect()
786
-
787
- self.last_key = len(self.stack) - 1
788
- while len(np.where(self.stack[self.last_key].flatten() == 0)[0]) > 0.99 * len(
789
- self.stack[self.last_key].flatten()):
790
- self.last_key -= 1
791
- self.anim._drawn_artists = self.draw_frame(self.last_key)
792
- self.anim._drawn_artists = sorted(self.anim._drawn_artists, key=lambda x: x.get_zorder())
793
- for a in self.anim._drawn_artists:
794
- a.set_visible(True)
795
-
796
- self.fig.canvas.draw()
797
- self.anim.event_source.stop()
798
-
799
- # self.cell_plot.draw()
800
- self.stop_btn.hide()
801
- self.start_btn.show()
802
- self.stop_btn.clicked.connect(self.start)
803
- self.start_btn.setShortcut(QKeySequence("l"))
804
-
805
- def set_first_frame(self):
806
-
807
- self.first_frame_btn.setEnabled(False)
808
- self.first_frame_btn.disconnect()
809
-
810
- self.first_key = 0
811
- self.anim._drawn_artists = self.draw_frame(0)
812
- self.vmin = self.contrast_slider.value()[0]
813
- self.vmax = self.contrast_slider.value()[1]
814
- self.im.set_clim(vmin=self.vmin, vmax=self.vmax)
815
-
816
50
 
817
- self.anim._drawn_artists = sorted(self.anim._drawn_artists, key=lambda x: x.get_zorder())
818
- for a in self.anim._drawn_artists:
819
- a.set_visible(True)
51
+ class StackLoaderThread(QThread):
52
+ progress = pyqtSignal(int)
53
+ status_update = pyqtSignal(str)
54
+ finished = pyqtSignal()
55
+
56
+ def __init__(self, annotator):
57
+ super().__init__()
58
+ self.annotator = annotator
59
+ self._is_cancelled = False
60
+
61
+ def stop(self):
62
+ self._is_cancelled = True
820
63
 
821
- self.fig.canvas.draw()
822
- self.anim.event_source.stop()
64
+ def run(self):
65
+ def callback(progress, status=""):
66
+ if self._is_cancelled:
67
+ return False
68
+ self.progress.emit(progress)
69
+ if status:
70
+ self.status_update.emit(status)
71
+ return True
823
72
 
824
- # self.cell_plot.draw()
825
- self.stop_btn.hide()
826
- self.start_btn.show()
827
- self.stop_btn.clicked.connect(self.start)
828
- self.start_btn.setShortcut(QKeySequence("f"))
73
+ try:
74
+ self.annotator.prepare_stack(progress_callback=callback)
75
+ if not self._is_cancelled:
76
+ self.finished.emit()
77
+ except Exception as e:
78
+ print(f"Error in loader thread: {e}")
79
+ self.finished.emit()
80
+
81
+
82
+ class EventAnnotator(BaseAnnotator):
83
+ """
84
+ UI to set tracking parameters for bTrack.
85
+
86
+ """
87
+
88
+ def __init__(self, *args, lazy_load=False, **kwargs):
89
+ super().__init__(*args, **kwargs)
90
+ self.setWindowTitle("Signal annotator")
91
+
92
+ # default params
93
+ self.class_name = "class"
94
+ self.time_name = "t0"
95
+ self.status_name = "status"
96
+
97
+ # self.locate_stack()
98
+ if not self.proceed:
99
+ self.close()
100
+ else:
101
+ if not lazy_load:
102
+ self.prepare_stack()
103
+ self.finalize_init()
104
+
105
+ def finalize_init(self):
106
+ self.frame_lbl = QLabel("frame: ")
107
+ self.looped_animation()
108
+ self.init_event_buttons()
109
+ self.populate_window()
110
+
111
+ self.outliers_check.hide()
112
+ if hasattr(self, "contrast_slider"):
113
+ self.im.set_clim(
114
+ self.contrast_slider.value()[0], self.contrast_slider.value()[1]
115
+ )
116
+
117
+ def init_event_buttons(self):
118
+
119
+ self.event_btn = QRadioButton("event")
120
+ self.event_btn.setStyleSheet(self.button_style_sheet_2)
121
+ self.event_btn.toggled.connect(self.enable_time_of_interest)
122
+
123
+ self.no_event_btn = QRadioButton("no event")
124
+ self.no_event_btn.setStyleSheet(self.button_style_sheet_2)
125
+ self.no_event_btn.toggled.connect(self.enable_time_of_interest)
126
+
127
+ self.else_btn = QRadioButton("else")
128
+ self.else_btn.setStyleSheet(self.button_style_sheet_2)
129
+ self.else_btn.toggled.connect(self.enable_time_of_interest)
130
+
131
+ self.suppr_btn = QRadioButton("remove")
132
+ self.suppr_btn.setToolTip(
133
+ "Mark for deletion. Upon saving, the cell\nwill be removed from the tables."
134
+ )
135
+ self.suppr_btn.setStyleSheet(self.button_style_sheet_2)
136
+ self.suppr_btn.toggled.connect(self.enable_time_of_interest)
137
+
138
+ self.time_of_interest_label = QLabel("time of interest: ")
139
+ self.time_of_interest_le = QLineEdit()
140
+
141
+ def populate_options_layout(self):
142
+
143
+ # clear options hbox
144
+ for i in reversed(range(self.options_hbox.count())):
145
+ self.options_hbox.itemAt(i).widget().setParent(None)
146
+
147
+ options_layout = QVBoxLayout()
148
+ # add new widgets
149
+
150
+ btn_hbox = QHBoxLayout()
151
+ btn_hbox.addWidget(self.event_btn, 25, alignment=Qt.AlignCenter)
152
+ btn_hbox.addWidget(self.no_event_btn, 25, alignment=Qt.AlignCenter)
153
+ btn_hbox.addWidget(self.else_btn, 25, alignment=Qt.AlignCenter)
154
+ btn_hbox.addWidget(self.suppr_btn, 25, alignment=Qt.AlignCenter)
155
+
156
+ time_option_hbox = QHBoxLayout()
157
+ time_option_hbox.setContentsMargins(0, 5, 100, 10)
158
+ time_option_hbox.addWidget(self.time_of_interest_label, 10)
159
+ time_option_hbox.addWidget(self.time_of_interest_le, 15)
160
+ time_option_hbox.addWidget(QLabel(""), 75)
161
+
162
+ options_layout.addLayout(btn_hbox)
163
+ options_layout.addLayout(time_option_hbox)
164
+
165
+ self.options_hbox.addLayout(options_layout)
166
+
167
+ self.annotation_btns_to_hide = [
168
+ self.event_btn,
169
+ self.no_event_btn,
170
+ self.else_btn,
171
+ self.time_of_interest_label,
172
+ self.time_of_interest_le,
173
+ self.suppr_btn,
174
+ ]
175
+ self.hide_annotation_buttons()
176
+
177
+ def populate_window(self):
178
+ """
179
+ Create the multibox design.
180
+
181
+ """
182
+
183
+ super().populate_window()
184
+ self.populate_options_layout()
185
+
186
+ self.del_shortcut = QShortcut(Qt.Key_Delete, self) # QKeySequence("s")
187
+ self.del_shortcut.activated.connect(self.shortcut_suppr)
188
+ self.del_shortcut.setEnabled(False)
189
+
190
+ self.no_event_shortcut = QShortcut(QKeySequence("n"), self) # QKeySequence("s")
191
+ self.no_event_shortcut.activated.connect(self.shortcut_no_event)
192
+ self.no_event_shortcut.setEnabled(False)
193
+
194
+ # Right side
195
+ animation_buttons_box = QHBoxLayout()
196
+ animation_buttons_box.addWidget(self.frame_lbl, 20, alignment=Qt.AlignLeft)
197
+
198
+ self.first_frame_btn = QPushButton()
199
+ self.first_frame_btn.clicked.connect(self.set_first_frame)
200
+ self.first_frame_btn.setShortcut(QKeySequence("f"))
201
+ self.first_frame_btn.setIcon(icon(MDI6.page_first, color="black"))
202
+ self.first_frame_btn.setStyleSheet(self.button_select_all)
203
+ self.first_frame_btn.setFixedSize(QSize(60, 60))
204
+ self.first_frame_btn.setIconSize(QSize(30, 30))
205
+
206
+ self.last_frame_btn = QPushButton()
207
+ self.last_frame_btn.clicked.connect(self.set_last_frame)
208
+ self.last_frame_btn.setShortcut(QKeySequence("l"))
209
+ self.last_frame_btn.setIcon(icon(MDI6.page_last, color="black"))
210
+ self.last_frame_btn.setStyleSheet(self.button_select_all)
211
+ self.last_frame_btn.setFixedSize(QSize(60, 60))
212
+ self.last_frame_btn.setIconSize(QSize(30, 30))
213
+
214
+ self.stop_btn = QPushButton()
215
+ self.stop_btn.clicked.connect(self.stop)
216
+ self.stop_btn.setIcon(icon(MDI6.stop, color="black"))
217
+ self.stop_btn.setStyleSheet(self.button_select_all)
218
+ self.stop_btn.setFixedSize(QSize(60, 60))
219
+ self.stop_btn.setIconSize(QSize(30, 30))
220
+
221
+ self.start_btn = QPushButton()
222
+ self.start_btn.clicked.connect(self.start)
223
+ self.start_btn.setIcon(icon(MDI6.play, color="black"))
224
+ self.start_btn.setFixedSize(QSize(60, 60))
225
+ self.start_btn.setStyleSheet(self.button_select_all)
226
+ self.start_btn.setIconSize(QSize(30, 30))
227
+ self.start_btn.hide()
228
+
229
+ animation_buttons_box.addWidget(
230
+ self.first_frame_btn, 5, alignment=Qt.AlignRight
231
+ )
232
+ animation_buttons_box.addWidget(self.stop_btn, 5, alignment=Qt.AlignRight)
233
+ animation_buttons_box.addWidget(self.start_btn, 5, alignment=Qt.AlignRight)
234
+ animation_buttons_box.addWidget(self.last_frame_btn, 5, alignment=Qt.AlignRight)
235
+
236
+ self.right_panel.addLayout(animation_buttons_box, 5)
237
+ self.right_panel.addWidget(self.fcanvas, 90)
238
+
239
+ if not self.rgb_mode:
240
+ contrast_hbox = QHBoxLayout()
241
+ contrast_hbox.setContentsMargins(150, 5, 150, 5)
242
+ self.contrast_slider = QLabeledDoubleRangeSlider()
243
+ self.contrast_slider.setSingleStep(0.001)
244
+ self.contrast_slider.setTickInterval(0.001)
245
+ self.contrast_slider.setOrientation(Qt.Horizontal)
246
+ self.contrast_slider.setRange(
247
+ *[
248
+ np.nanpercentile(self.stack, 0.001),
249
+ np.nanpercentile(self.stack, 99.999),
250
+ ]
251
+ )
252
+ self.contrast_slider.setValue(
253
+ [np.nanpercentile(self.stack, 1), np.nanpercentile(self.stack, 99.99)]
254
+ )
255
+ self.contrast_slider.valueChanged.connect(self.contrast_slider_action)
256
+ contrast_hbox.addWidget(QLabel("contrast: "))
257
+ contrast_hbox.addWidget(self.contrast_slider, 90)
258
+ self.right_panel.addLayout(contrast_hbox, 5)
259
+
260
+ if self.class_choice_cb.currentText() != "":
261
+ self.compute_status_and_colors(0)
262
+
263
+ self.no_event_shortcut = QShortcut(QKeySequence("n"), self) # QKeySequence("s")
264
+ self.no_event_shortcut.activated.connect(self.shortcut_no_event)
265
+ self.no_event_shortcut.setEnabled(False)
266
+
267
+ QApplication.processEvents()
268
+
269
+ def write_new_event_class(self):
270
+
271
+ if self.class_name_le.text() == "":
272
+ self.target_class = "class"
273
+ self.target_time = "t0"
274
+ else:
275
+ self.target_class = "class_" + self.class_name_le.text()
276
+ self.target_time = "t_" + self.class_name_le.text()
277
+
278
+ if self.target_class in list(self.df_tracks.columns):
279
+
280
+ msgBox = QMessageBox()
281
+ msgBox.setIcon(QMessageBox.Warning)
282
+ msgBox.setText(
283
+ "This event name already exists. If you proceed,\nall annotated data will be rewritten. Do you wish to continue?"
284
+ )
285
+ msgBox.setWindowTitle("Warning")
286
+ msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
287
+ returnValue = msgBox.exec()
288
+ if returnValue == QMessageBox.No:
289
+ return None
290
+ else:
291
+ pass
292
+
293
+ fill_option = np.where([c.isChecked() for c in self.class_option_rb])[0][0]
294
+ self.df_tracks.loc[:, self.target_class] = fill_option
295
+ if fill_option == 0:
296
+ self.df_tracks.loc[:, self.target_time] = 0.1
297
+ else:
298
+ self.df_tracks.loc[:, self.target_time] = -1
299
+
300
+ self.class_choice_cb.clear()
301
+ cols = np.array(self.df_tracks.columns)
302
+ self.class_cols = np.array(
303
+ [c.startswith("class") for c in list(self.df_tracks.columns)]
304
+ )
305
+ self.class_cols = list(cols[self.class_cols])
306
+ if "class_id" in self.class_cols:
307
+ self.class_cols.remove("class_id")
308
+ if "class_color" in self.class_cols:
309
+ self.class_cols.remove("class_color")
310
+ self.class_choice_cb.addItems(self.class_cols)
311
+ idx = self.class_choice_cb.findText(self.target_class)
312
+ self.class_choice_cb.setCurrentIndex(idx)
313
+
314
+ self.newClassWidget.close()
315
+
316
+ # def close_without_new_class(self):
317
+ # self.newClassWidget.close()
318
+
319
+ def compute_status_and_colors(self, i):
320
+
321
+ self.class_name = self.class_choice_cb.currentText()
322
+ self.expected_status = "status"
323
+ suffix = self.class_name.replace("class", "").replace("_", "", 1)
324
+ if suffix != "":
325
+ self.expected_status += "_" + suffix
326
+ self.expected_time = "t_" + suffix
327
+ else:
328
+ self.expected_time = "t0"
329
+ self.time_name = self.expected_time
330
+ self.status_name = self.expected_status
331
+
332
+ cols = list(self.df_tracks.columns)
333
+
334
+ if (
335
+ self.time_name in cols
336
+ and self.class_name in cols
337
+ and not self.status_name in cols
338
+ ):
339
+ # only create the status column if it does not exist to not erase static classification results
340
+ self.make_status_column()
341
+ elif (
342
+ self.time_name in cols
343
+ and self.class_name in cols
344
+ and self.df_tracks[self.status_name].isnull().all()
345
+ ):
346
+ self.make_status_column()
347
+ elif self.time_name in cols and self.class_name in cols:
348
+ # all good, do nothing
349
+ pass
350
+ else:
351
+ if not self.status_name in self.df_tracks.columns:
352
+ self.df_tracks[self.status_name] = 0
353
+ self.df_tracks["status_color"] = color_from_status(0)
354
+ self.df_tracks["class_color"] = color_from_class(1)
355
+
356
+ if not self.class_name in self.df_tracks.columns:
357
+ self.df_tracks[self.class_name] = 1
358
+ if not self.time_name in self.df_tracks.columns:
359
+ self.df_tracks[self.time_name] = -1
360
+
361
+ self.df_tracks["status_color"] = [
362
+ color_from_status(i) for i in self.df_tracks[self.status_name].to_numpy()
363
+ ]
364
+ self.df_tracks["class_color"] = [
365
+ color_from_class(i) for i in self.df_tracks[self.class_name].to_numpy()
366
+ ]
367
+
368
+ self.extract_scatter_from_trajectories()
369
+ if len(self.selection) > 0:
370
+ self.select_single_cell(self.selection[0][0], self.selection[0][1])
371
+
372
+ self.fcanvas.canvas.draw()
373
+
374
+ def cancel_selection(self):
375
+
376
+ super().cancel_selection()
377
+ try:
378
+ for k, (t, idx) in enumerate(zip(self.loc_t, self.loc_idx)):
379
+ self.colors[t][idx, 1] = self.previous_color[k][1]
380
+ except Exception as e:
381
+ pass
382
+
383
+ def hide_annotation_buttons(self):
384
+
385
+ for a in self.annotation_btns_to_hide:
386
+ a.hide()
387
+ for b in [self.event_btn, self.no_event_btn, self.else_btn, self.suppr_btn]:
388
+ b.setChecked(False)
389
+ self.time_of_interest_label.setEnabled(False)
390
+ self.time_of_interest_le.setText("")
391
+ self.time_of_interest_le.setEnabled(False)
392
+
393
+ def enable_time_of_interest(self):
394
+
395
+ if self.event_btn.isChecked():
396
+ self.time_of_interest_label.setEnabled(True)
397
+ self.time_of_interest_le.setEnabled(True)
398
+ else:
399
+ self.time_of_interest_label.setEnabled(False)
400
+ self.time_of_interest_le.setEnabled(False)
401
+
402
+ def show_annotation_buttons(self):
403
+
404
+ for a in self.annotation_btns_to_hide:
405
+ a.show()
406
+
407
+ cclass = self.df_tracks.loc[
408
+ self.df_tracks["TRACK_ID"] == self.track_of_interest, self.class_name
409
+ ].to_numpy()[0]
410
+ t0 = self.df_tracks.loc[
411
+ self.df_tracks["TRACK_ID"] == self.track_of_interest, self.time_name
412
+ ].to_numpy()[0]
413
+
414
+ if cclass == 0:
415
+ self.event_btn.setChecked(True)
416
+ self.time_of_interest_le.setText(str(t0))
417
+ elif cclass == 1:
418
+ self.no_event_btn.setChecked(True)
419
+ elif cclass == 2:
420
+ self.else_btn.setChecked(True)
421
+ elif cclass > 2:
422
+ self.suppr_btn.setChecked(True)
423
+
424
+ self.enable_time_of_interest()
425
+ self.correct_btn.setText("submit")
426
+
427
+ self.correct_btn.disconnect()
428
+ self.correct_btn.clicked.connect(self.apply_modification)
429
+
430
+ def apply_modification(self):
431
+
432
+ t0 = -1
433
+ if self.event_btn.isChecked():
434
+ cclass = 0
435
+ try:
436
+ t0 = float(self.time_of_interest_le.text().replace(",", "."))
437
+ self.line_dt.set_xdata([t0, t0])
438
+ self.cell_fcanvas.canvas.draw_idle()
439
+ except Exception as e:
440
+ print(f"L 598 {e=}")
441
+ t0 = -1
442
+ cclass = 2
443
+ elif self.no_event_btn.isChecked():
444
+ cclass = 1
445
+ elif self.else_btn.isChecked():
446
+ cclass = 2
447
+ elif self.suppr_btn.isChecked():
448
+ cclass = 42
449
+
450
+ self.df_tracks.loc[
451
+ self.df_tracks["TRACK_ID"] == self.track_of_interest, self.class_name
452
+ ] = cclass
453
+ self.df_tracks.loc[
454
+ self.df_tracks["TRACK_ID"] == self.track_of_interest, self.time_name
455
+ ] = t0
456
+
457
+ indices = self.df_tracks.loc[
458
+ self.df_tracks["TRACK_ID"] == self.track_of_interest, self.class_name
459
+ ].index
460
+ timeline = self.df_tracks.loc[
461
+ self.df_tracks["TRACK_ID"] == self.track_of_interest, "FRAME"
462
+ ].to_numpy()
463
+ status = np.zeros_like(timeline)
464
+ if t0 > 0:
465
+ status[timeline >= t0] = 1.0
466
+ if cclass == 2:
467
+ status[:] = 2
468
+ if cclass > 2:
469
+ status[:] = 42
470
+
471
+ status_color = [color_from_status(s, recently_modified=True) for s in status]
472
+ class_color = [
473
+ color_from_class(cclass, recently_modified=True) for i in range(len(status))
474
+ ]
475
+
476
+ # self.df_tracks['status_color'] = [color_from_status(i) for i in self.df_tracks[self.status_name].to_numpy()]
477
+ # self.df_tracks['class_color'] = [color_from_class(i) for i in self.df_tracks[self.class_name].to_numpy()]
478
+
479
+ self.df_tracks.loc[indices, self.status_name] = status
480
+ self.df_tracks.loc[indices, "status_color"] = status_color
481
+ self.df_tracks.loc[indices, "class_color"] = class_color
482
+
483
+ # self.make_status_column()
484
+ self.extract_scatter_from_trajectories()
485
+ self.give_cell_information()
486
+
487
+ self.correct_btn.disconnect()
488
+ self.correct_btn.clicked.connect(self.show_annotation_buttons)
489
+ # self.cancel_btn.click()
490
+
491
+ self.hide_annotation_buttons()
492
+ self.correct_btn.setEnabled(False)
493
+ self.correct_btn.setText("correct")
494
+ self.cancel_btn.setEnabled(False)
495
+ self.del_shortcut.setEnabled(False)
496
+ self.no_event_shortcut.setEnabled(False)
497
+
498
+ self.selection.pop(0)
499
+
500
+ def make_status_column(self):
501
+
502
+ print(
503
+ f"Generating status information for class `{self.class_name}` and time `{self.time_name}`..."
504
+ )
505
+ for tid, group in self.df_tracks.groupby("TRACK_ID"):
506
+
507
+ indices = group.index
508
+ t0 = group[self.time_name].to_numpy()[0]
509
+ cclass = group[self.class_name].to_numpy()[0]
510
+ timeline = group["FRAME"].to_numpy()
511
+ status = np.zeros_like(timeline)
512
+
513
+ if t0 > 0:
514
+ status[timeline >= t0] = 1.0
515
+ # if cclass == 2:
516
+ # status[:] = 1.
517
+ if cclass > 2:
518
+ status[:] = 42
519
+
520
+ status_color = [color_from_status(s) for s in status]
521
+ class_color = [color_from_class(cclass) for i in range(len(status))]
522
+
523
+ self.df_tracks.loc[indices, self.status_name] = status
524
+ self.df_tracks.loc[indices, "status_color"] = status_color
525
+ self.df_tracks.loc[indices, "class_color"] = class_color
526
+
527
+ def generate_signal_choices(self):
528
+
529
+ self.signal_choice_cb = [QSearchableComboBox() for i in range(self.n_signals)]
530
+ self.signal_choice_label = [
531
+ QLabel(f"signal {i + 1}: ") for i in range(self.n_signals)
532
+ ]
533
+ # self.log_btns = [QPushButton() for i in range(self.n_signals)]
534
+
535
+ signals = list(self.df_tracks.columns)
536
+
537
+ to_remove = [
538
+ "TRACK_ID",
539
+ "FRAME",
540
+ "x_anim",
541
+ "y_anim",
542
+ "t",
543
+ "state",
544
+ "generation",
545
+ "root",
546
+ "parent",
547
+ "class_id",
548
+ "class",
549
+ "t0",
550
+ "POSITION_X",
551
+ "POSITION_Y",
552
+ "position",
553
+ "well",
554
+ "well_index",
555
+ "well_name",
556
+ "pos_name",
557
+ "index",
558
+ "class_color",
559
+ "status_color",
560
+ "dummy",
561
+ "group_color",
562
+ ]
563
+
564
+ meta = get_experiment_metadata(self.exp_dir)
565
+ if meta is not None:
566
+ keys = list(meta.keys())
567
+ to_remove.extend(keys)
568
+
569
+ labels = get_experiment_labels(self.exp_dir)
570
+ if labels is not None:
571
+ keys = list(labels.keys())
572
+ to_remove.extend(labels)
573
+
574
+ for c in to_remove:
575
+ if c in signals:
576
+ signals.remove(c)
577
+
578
+ for i in range(len(self.signal_choice_cb)):
579
+ self.signal_choice_cb[i].addItems(["--"] + signals)
580
+ self.signal_choice_cb[i].setCurrentIndex(i + 1)
581
+ self.signal_choice_cb[i].currentIndexChanged.connect(self.plot_signals)
582
+
583
+ def plot_signals(self):
584
+
585
+ range_values = []
586
+
587
+ try:
588
+ yvalues = []
589
+ for i in range(len(self.signal_choice_cb)):
590
+
591
+ signal_choice = self.signal_choice_cb[i].currentText()
592
+ lbl = signal_choice
593
+ n_cut = 35
594
+ if len(lbl) > n_cut:
595
+ lbl = lbl[: (n_cut - 3)] + "..."
596
+ self.lines[i].set_label(lbl)
597
+
598
+ if signal_choice == "--":
599
+ self.lines[i].set_xdata([])
600
+ self.lines[i].set_ydata([])
601
+ else:
602
+ xdata = self.df_tracks.loc[
603
+ self.df_tracks["TRACK_ID"] == self.track_of_interest, "FRAME"
604
+ ].to_numpy()
605
+ ydata = self.df_tracks.loc[
606
+ self.df_tracks["TRACK_ID"] == self.track_of_interest,
607
+ signal_choice,
608
+ ].to_numpy()
609
+
610
+ range_values.extend(ydata)
611
+
612
+ xdata = xdata[ydata == ydata] # remove nan
613
+ ydata = ydata[ydata == ydata]
614
+
615
+ yvalues.extend(ydata)
616
+ self.lines[i].set_xdata(xdata)
617
+ self.lines[i].set_ydata(ydata)
618
+ self.lines[i].set_color(tab10(i / 3.0))
619
+
620
+ self.configure_ylims()
621
+
622
+ min_val, max_val = self.cell_ax.get_ylim()
623
+ t0 = self.df_tracks.loc[
624
+ self.df_tracks["TRACK_ID"] == self.track_of_interest, self.expected_time
625
+ ].to_numpy()[0]
626
+ self.line_dt.set_xdata([t0, t0])
627
+ self.line_dt.set_ydata([min_val, max_val])
628
+
629
+ self.cell_ax.legend(fontsize=8)
630
+ self.cell_fcanvas.canvas.draw()
631
+ except Exception as e:
632
+ print(e)
633
+ pass
634
+
635
+ if len(range_values) > 0:
636
+ range_values = np.array(range_values)
637
+ if len(range_values[range_values == range_values]) > 0:
638
+ if len(range_values[range_values > 0]) > 0:
639
+ self.value_magnitude = np.nanpercentile(range_values, 1)
640
+ else:
641
+ self.value_magnitude = 1
642
+ self.non_log_ymin = 0.98 * np.nanmin(range_values)
643
+ self.non_log_ymax = np.nanmax(range_values) * 1.02
644
+ if self.cell_ax.get_yscale() == "linear":
645
+ self.cell_ax.set_ylim(self.non_log_ymin, self.non_log_ymax)
646
+ else:
647
+ self.cell_ax.set_ylim(self.value_magnitude, self.non_log_ymax)
648
+
649
+ def extract_scatter_from_trajectories(self):
650
+
651
+ self.positions = []
652
+ self.colors = []
653
+ self.tracks = []
654
+
655
+ for t in np.arange(self.len_movie):
656
+ self.positions.append(
657
+ self.df_tracks.loc[
658
+ self.df_tracks["FRAME"] == t, ["x_anim", "y_anim"]
659
+ ].to_numpy()
660
+ )
661
+ self.colors.append(
662
+ self.df_tracks.loc[
663
+ self.df_tracks["FRAME"] == t, ["class_color", "status_color"]
664
+ ].to_numpy()
665
+ )
666
+ self.tracks.append(
667
+ self.df_tracks.loc[self.df_tracks["FRAME"] == t, "TRACK_ID"].to_numpy()
668
+ )
669
+
670
+ # def load_annotator_config(self):
671
+ #
672
+ # """
673
+ # Load settings from config or set default values.
674
+ # """
675
+ #
676
+ # if os.path.exists(self.instructions_path):
677
+ # with open(self.instructions_path, 'r') as f:
678
+ #
679
+ # instructions = json.load(f)
680
+ #
681
+ # if 'rgb_mode' in instructions:
682
+ # self.rgb_mode = instructions['rgb_mode']
683
+ # else:
684
+ # self.rgb_mode = False
685
+ #
686
+ # if 'percentile_mode' in instructions:
687
+ # self.percentile_mode = instructions['percentile_mode']
688
+ # else:
689
+ # self.percentile_mode = True
690
+ #
691
+ # if 'channels' in instructions:
692
+ # self.target_channels = instructions['channels']
693
+ # else:
694
+ # self.target_channels = [[self.channel_names[0], 0.01, 99.99]]
695
+ #
696
+ # if 'fraction' in instructions:
697
+ # self.fraction = float(instructions['fraction'])
698
+ # else:
699
+ # self.fraction = 0.25
700
+ #
701
+ # if 'interval' in instructions:
702
+ # self.anim_interval = int(instructions['interval'])
703
+ # else:
704
+ # self.anim_interval = 1
705
+ #
706
+ # if 'log' in instructions:
707
+ # self.log_option = instructions['log']
708
+ # else:
709
+ # self.log_option = False
710
+ # else:
711
+ # self.rgb_mode = False
712
+ # self.log_option = False
713
+ # self.percentile_mode = True
714
+ # self.target_channels = [[self.channel_names[0], 0.01, 99.99]]
715
+ # self.fraction = 0.25
716
+ # self.anim_interval = 1
717
+
718
+ def prepare_stack(self, progress_callback=None):
719
+
720
+ self.img_num_channels = _get_img_num_per_channel(
721
+ self.channels, self.len_movie, self.nbr_channels
722
+ )
723
+ self.stack = []
724
+ disable_tqdm = not len(self.target_channels) > 1
725
+
726
+ # Calculate total frames for progress
727
+ total_frames = 0
728
+ for ch in self.target_channels:
729
+ target_ch_name = ch[0]
730
+ indices = self.img_num_channels[
731
+ self.channels[np.where(self.channel_names == target_ch_name)][0]
732
+ ]
733
+ total_frames += len(indices)
734
+
735
+ current_frame = 0
736
+
737
+ for ch in tqdm(self.target_channels, desc="channel", disable=disable_tqdm):
738
+ target_ch_name = ch[0]
739
+ if self.percentile_mode:
740
+ normalize_kwargs = {"percentiles": (ch[1], ch[2]), "values": None}
741
+ else:
742
+ normalize_kwargs = {"values": (ch[1], ch[2]), "percentiles": None}
743
+
744
+ if self.rgb_mode:
745
+ normalize_kwargs.update({"amplification": 255.0, "clip": True})
746
+
747
+ chan = []
748
+ indices = self.img_num_channels[
749
+ self.channels[np.where(self.channel_names == target_ch_name)][0]
750
+ ]
751
+
752
+ if progress_callback:
753
+ if not progress_callback(
754
+ int((current_frame / total_frames) * 100),
755
+ f"Loading channel {target_ch_name}...",
756
+ ):
757
+ return
758
+
759
+ for t in tqdm(range(len(indices)), desc="frame"):
760
+
761
+ if progress_callback:
762
+ if not progress_callback(int((current_frame / total_frames) * 100)):
763
+ return
764
+ current_frame += 1
765
+
766
+ if self.rgb_mode:
767
+ f = load_frames(
768
+ indices[t],
769
+ self.stack_path,
770
+ scale=self.fraction,
771
+ normalize_input=True,
772
+ normalize_kwargs=normalize_kwargs,
773
+ )
774
+ f = f.astype(np.uint8)
775
+ else:
776
+ f = load_frames(
777
+ indices[t],
778
+ self.stack_path,
779
+ scale=self.fraction,
780
+ normalize_input=False,
781
+ )
782
+
783
+ chan.append(f[:, :, 0])
784
+
785
+ self.stack.append(chan)
786
+
787
+ self.stack = np.array(self.stack)
788
+ if self.rgb_mode:
789
+ self.stack = np.moveaxis(self.stack, 0, -1)
790
+ else:
791
+ self.stack = self.stack[0]
792
+ if self.log_option:
793
+ self.stack[np.where(self.stack > 0.0)] = np.log(
794
+ self.stack[np.where(self.stack > 0.0)]
795
+ )
796
+
797
+ def closeEvent(self, event):
798
+
799
+ try:
800
+ self.stop()
801
+ del self.stack
802
+ gc.collect()
803
+ except:
804
+ pass
805
+
806
+ def looped_animation(self):
807
+ """
808
+ Load an image.
809
+
810
+ """
811
+
812
+ self.framedata = 0
813
+
814
+ self.fig, self.ax = plt.subplots(tight_layout=True)
815
+ self.fcanvas = FigureCanvas(self.fig, interactive=True)
816
+ self.ax.clear()
817
+
818
+ self.im = self.ax.imshow(self.stack[0], cmap="gray", interpolation="none")
819
+ self.status_scatter = self.ax.scatter(
820
+ self.positions[0][:, 0],
821
+ self.positions[0][:, 1],
822
+ marker="x",
823
+ c=self.colors[0][:, 1],
824
+ s=50,
825
+ picker=True,
826
+ pickradius=100,
827
+ )
828
+ self.class_scatter = self.ax.scatter(
829
+ self.positions[0][:, 0],
830
+ self.positions[0][:, 1],
831
+ marker="o",
832
+ facecolors="none",
833
+ edgecolors=self.colors[0][:, 0],
834
+ s=200,
835
+ )
836
+
837
+ self.ax.set_xticks([])
838
+ self.ax.set_yticks([])
839
+ self.ax.set_aspect("equal")
840
+
841
+ self.fig.set_facecolor("none") # or 'None'
842
+ self.fig.canvas.setStyleSheet("background-color: black;")
843
+
844
+ self.anim = FuncAnimation(
845
+ self.fig,
846
+ self.draw_frame,
847
+ frames=self.len_movie, # better would be to cast np.arange(len(movie)) in case frame column is incomplete
848
+ interval=self.anim_interval, # in ms
849
+ blit=True,
850
+ )
851
+
852
+ self.fig.canvas.mpl_connect("pick_event", self.on_scatter_pick)
853
+ self.fcanvas.canvas.draw()
854
+
855
+ def select_single_cell(self, index, timepoint):
856
+
857
+ self.correct_btn.setEnabled(True)
858
+ self.cancel_btn.setEnabled(True)
859
+ self.del_shortcut.setEnabled(True)
860
+ self.no_event_shortcut.setEnabled(True)
861
+
862
+ self.track_of_interest = self.tracks[timepoint][index]
863
+ print(f"You selected cell #{self.track_of_interest}...")
864
+ self.give_cell_information()
865
+ self.plot_signals()
866
+
867
+ self.loc_t = []
868
+ self.loc_idx = []
869
+ for t in range(len(self.tracks)):
870
+ indices = np.where(self.tracks[t] == self.track_of_interest)[0]
871
+ if len(indices) > 0:
872
+ self.loc_t.append(t)
873
+ self.loc_idx.append(indices[0])
874
+
875
+ self.previous_color = []
876
+ for t, idx in zip(self.loc_t, self.loc_idx):
877
+ self.previous_color.append(self.colors[t][idx].copy())
878
+ self.colors[t][idx] = "lime"
879
+
880
+ def shortcut_no_event(self):
881
+ self.correct_btn.click()
882
+ self.no_event_btn.click()
883
+ self.correct_btn.click()
884
+
885
+ def configure_ylims(self):
886
+
887
+ try:
888
+ min_values = []
889
+ max_values = []
890
+ feats = []
891
+ for i in range(len(self.signal_choice_cb)):
892
+ signal = self.signal_choice_cb[i].currentText()
893
+ if signal == "--":
894
+ continue
895
+ else:
896
+ maxx = np.nanpercentile(
897
+ self.df_tracks.loc[:, signal].to_numpy().flatten(), 99
898
+ )
899
+ minn = np.nanpercentile(
900
+ self.df_tracks.loc[:, signal].to_numpy().flatten(), 1
901
+ )
902
+ min_values.append(minn)
903
+ max_values.append(maxx)
904
+ feats.append(signal)
905
+
906
+ smallest_value = np.amin(min_values)
907
+ feat_smallest_value = feats[np.argmin(min_values)]
908
+ min_feat = self.df_tracks[feat_smallest_value].min()
909
+ max_feat = self.df_tracks[feat_smallest_value].max()
910
+ pad_small = (max_feat - min_feat) * 0.05
911
+ if pad_small == 0:
912
+ pad_small = 0.05
913
+
914
+ largest_value = np.amax(max_values)
915
+ feat_largest_value = feats[np.argmax(max_values)]
916
+ min_feat = self.df_tracks[feat_largest_value].min()
917
+ max_feat = self.df_tracks[feat_largest_value].max()
918
+ pad_large = (max_feat - min_feat) * 0.05
919
+ if pad_large == 0:
920
+ pad_large = 0.05
921
+
922
+ if len(min_values) > 0:
923
+ self.cell_ax.set_ylim(
924
+ smallest_value - pad_small, largest_value + pad_large
925
+ )
926
+ except Exception as e:
927
+ print(f"L1170 {e=}")
928
+ pass
929
+
930
+ def draw_frame(self, framedata):
931
+ """
932
+ Update plot elements at each timestep of the loop.
933
+ """
934
+
935
+ self.framedata = framedata
936
+ self.frame_lbl.setText(f"frame: {self.framedata}")
937
+ self.im.set_array(self.stack[self.framedata])
938
+ self.status_scatter.set_offsets(self.positions[self.framedata])
939
+ self.status_scatter.set_color(self.colors[self.framedata][:, 1])
940
+
941
+ self.class_scatter.set_offsets(self.positions[self.framedata])
942
+ self.class_scatter.set_edgecolor(self.colors[self.framedata][:, 0])
943
+
944
+ return (
945
+ self.im,
946
+ self.status_scatter,
947
+ self.class_scatter,
948
+ )
949
+
950
+ def stop(self):
951
+ # # On stop we disconnect all of our events.
952
+ self.stop_btn.hide()
953
+ self.start_btn.show()
954
+ self.anim.pause()
955
+ self.stop_btn.clicked.connect(self.start)
956
+
957
+ def give_cell_information(self):
958
+
959
+ cell_selected = f"cell: {self.track_of_interest}\n"
960
+ cell_class = f"class: {self.df_tracks.loc[self.df_tracks['TRACK_ID'] == self.track_of_interest, self.class_name].to_numpy()[0]}\n"
961
+ cell_time = f"time of interest: {self.df_tracks.loc[self.df_tracks['TRACK_ID'] == self.track_of_interest, self.time_name].to_numpy()[0]}\n"
962
+ self.cell_info.setText(cell_selected + cell_class + cell_time)
963
+
964
+ def save_trajectories(self):
965
+
966
+ if self.normalized_signals:
967
+ self.normalize_features_btn.click()
968
+ if self.selection:
969
+ self.cancel_selection()
970
+
971
+ self.df_tracks = self.df_tracks.drop(
972
+ self.df_tracks[self.df_tracks[self.class_name] > 2].index
973
+ )
974
+ self.df_tracks.to_csv(self.trajectories_path, index=False)
975
+ print("Table successfully exported...")
976
+ if self.class_choice_cb.currentText() != "":
977
+ self.compute_status_and_colors(0)
978
+ self.extract_scatter_from_trajectories()
979
+
980
+ def set_last_frame(self):
981
+
982
+ self.last_frame_btn.setEnabled(False)
983
+ self.last_frame_btn.disconnect()
984
+
985
+ self.last_key = len(self.stack) - 1
986
+ while len(np.where(self.stack[self.last_key].flatten() == 0)[0]) > 0.99 * len(
987
+ self.stack[self.last_key].flatten()
988
+ ):
989
+ self.last_key -= 1
990
+ self.anim._drawn_artists = self.draw_frame(self.last_key)
991
+ self.anim._drawn_artists = sorted(
992
+ self.anim._drawn_artists, key=lambda x: x.get_zorder()
993
+ )
994
+ for a in self.anim._drawn_artists:
995
+ a.set_visible(True)
996
+
997
+ self.fig.canvas.draw()
998
+ self.anim.event_source.stop()
999
+
1000
+ # self.cell_plot.draw()
1001
+ self.stop_btn.hide()
1002
+ self.start_btn.show()
1003
+ self.stop_btn.clicked.connect(self.start)
1004
+ self.start_btn.setShortcut(QKeySequence("l"))
1005
+
1006
+ def set_first_frame(self):
1007
+
1008
+ self.first_frame_btn.setEnabled(False)
1009
+ self.first_frame_btn.disconnect()
1010
+
1011
+ self.first_key = 0
1012
+ self.anim._drawn_artists = self.draw_frame(0)
1013
+ self.vmin = self.contrast_slider.value()[0]
1014
+ self.vmax = self.contrast_slider.value()[1]
1015
+ self.im.set_clim(vmin=self.vmin, vmax=self.vmax)
1016
+
1017
+ self.anim._drawn_artists = sorted(
1018
+ self.anim._drawn_artists, key=lambda x: x.get_zorder()
1019
+ )
1020
+ for a in self.anim._drawn_artists:
1021
+ a.set_visible(True)
1022
+
1023
+ self.fig.canvas.draw()
1024
+ self.anim.event_source.stop()
1025
+
1026
+ # self.cell_plot.draw()
1027
+ self.stop_btn.hide()
1028
+ self.start_btn.show()
1029
+ self.stop_btn.clicked.connect(self.start)
1030
+ self.start_btn.setShortcut(QKeySequence("f"))
829
1031
 
830
1032
 
831
1033
  class MeasureAnnotator(BaseAnnotator):
832
1034
 
833
- def __init__(self, *args, **kwargs):
834
-
835
- super().__init__(read_config=False, *args, **kwargs)
836
-
837
- self.setWindowTitle("Static annotator")
838
-
839
- self.int_validator = QIntValidator()
840
- self.current_alpha=0.5
841
- self.value_magnitude = 1
842
-
843
- epsilon = 0.01
844
- self.observed_min_intensity = 0
845
- self.observed_max_intensity = 0 + epsilon
846
-
847
- self.current_frame = 0
848
- self.show_fliers = False
849
- self.status_name = 'group'
850
-
851
- if self.proceed:
852
-
853
- self.labels = locate_labels(self.pos, population=self.mode)
854
-
855
- self.current_channel = 0
856
- self.frame_lbl = QLabel('position: ')
857
- self.static_image()
858
-
859
- self.populate_window()
860
- self.changed_class()
861
-
862
- self.previous_index = None
863
- if hasattr(self, "contrast_slider"):
864
- self.im.set_clim(self.contrast_slider.value()[0], self.contrast_slider.value()[1])
865
-
866
- else:
867
- self.close()
868
-
869
- def locate_tracks(self):
870
-
871
- """
872
- Locate the tracks.
873
- """
874
-
875
- if not os.path.exists(self.trajectories_path):
876
-
877
- msgBox = QMessageBox()
878
- msgBox.setIcon(QMessageBox.Warning)
879
- msgBox.setText("The trajectories cannot be detected.")
880
- msgBox.setWindowTitle("Warning")
881
- msgBox.setStandardButtons(QMessageBox.Ok)
882
- returnValue = msgBox.exec()
883
- if returnValue == QMessageBox.Yes:
884
- self.close()
885
- else:
886
-
887
- # Load and prep tracks
888
- self.df_tracks = pd.read_csv(self.trajectories_path)
889
- if 'TRACK_ID' in self.df_tracks.columns:
890
- self.df_tracks = self.df_tracks.sort_values(by=['TRACK_ID', 'FRAME'])
891
- else:
892
- self.df_tracks = self.df_tracks.sort_values(by=['ID', 'FRAME'])
893
-
894
- cols = np.array(self.df_tracks.columns)
895
- self.class_cols = np.array([c.startswith('group') or c.startswith('class') for c in list(self.df_tracks.columns)])
896
- self.class_cols = list(cols[self.class_cols])
897
-
898
- to_remove = ['class_id','group_color','class_color']
899
- for col in to_remove:
900
- try:
901
- self.class_cols.remove(col)
902
- except:
903
- pass
904
-
905
- if len(self.class_cols) > 0:
906
- self.status_name = self.class_cols[0]
907
- else:
908
- self.status_name = 'group'
909
-
910
- if self.status_name not in self.df_tracks.columns:
911
- # only create the status column if it does not exist to not erase static classification results
912
- self.make_status_column()
913
- else:
914
- # all good, do nothing
915
- pass
916
-
917
- all_states = self.df_tracks.loc[:, self.status_name].tolist()
918
- all_states = np.array(all_states)
919
- self.state_color_map = color_from_state(all_states, recently_modified=False)
920
- self.df_tracks['group_color'] = self.df_tracks[self.status_name].apply(self.assign_color_state)
921
-
922
- self.df_tracks = self.df_tracks.dropna(subset=['POSITION_X', 'POSITION_Y'])
923
- self.df_tracks['x_anim'] = self.df_tracks['POSITION_X']
924
- self.df_tracks['y_anim'] = self.df_tracks['POSITION_Y']
925
- self.df_tracks['x_anim'] = self.df_tracks['x_anim'].astype(int)
926
- self.df_tracks['y_anim'] = self.df_tracks['y_anim'].astype(int)
927
-
928
- self.extract_scatter_from_trajectories()
929
- if 'TRACK_ID' in self.df_tracks.columns:
930
- self.track_of_interest = self.df_tracks.dropna(subset='TRACK_ID')['TRACK_ID'].min()
931
- else:
932
- self.track_of_interest = self.df_tracks.dropna(subset='ID')['ID'].min()
933
-
934
- self.loc_t = []
935
- self.loc_idx = []
936
- for t in range(len(self.tracks)):
937
- indices = np.where(self.tracks[t] == self.track_of_interest)[0]
938
- if len(indices) > 0:
939
- self.loc_t.append(t)
940
- self.loc_idx.append(indices[0])
941
-
942
- self.MinMaxScaler = MinMaxScaler()
943
- self.columns_to_rescale = list(self.df_tracks.columns)
944
-
945
- cols_to_remove = ['group', 'group_color', 'status', 'status_color', 'class_color', 'TRACK_ID', 'FRAME',
946
- 'x_anim', 'y_anim', 't','dummy','group_color',
947
- 'state', 'generation', 'root', 'parent', 'class_id', 'class', 't0', 'POSITION_X',
948
- 'POSITION_Y', 'position', 'well', 'well_index', 'well_name', 'pos_name', 'index',
949
- 'concentration', 'cell_type', 'antibody', 'pharmaceutical_agent', 'ID'] + self.class_cols
950
-
951
- meta = get_experiment_metadata(self.exp_dir)
952
- if meta is not None:
953
- keys = list(meta.keys())
954
- cols_to_remove.extend(keys)
955
-
956
- labels = get_experiment_labels(self.exp_dir)
957
- if labels is not None:
958
- keys = list(labels.keys())
959
- cols_to_remove.extend(labels)
960
-
961
- for tr in cols_to_remove:
962
- try:
963
- self.columns_to_rescale.remove(tr)
964
- except:
965
- pass
966
-
967
- x = self.df_tracks[self.columns_to_rescale].values
968
- self.MinMaxScaler.fit(x)
969
-
970
-
971
- def populate_options_layout(self):
972
- # clear options hbox
973
- for i in reversed(range(self.options_hbox.count())):
974
- self.options_hbox.itemAt(i).widget().setParent(None)
975
-
976
- time_option_hbox = QHBoxLayout()
977
- time_option_hbox.setContentsMargins(100, 0, 100, 0)
978
- time_option_hbox.setSpacing(0)
979
-
980
- self.time_of_interest_label = QLabel('phenotype: ')
981
- time_option_hbox.addWidget(self.time_of_interest_label, 30)
982
-
983
- self.time_of_interest_le = QLineEdit()
984
- self.time_of_interest_le.setValidator(self.int_validator)
985
- time_option_hbox.addWidget(self.time_of_interest_le)
986
-
987
- self.suppr_btn = QPushButton('')
988
- self.suppr_btn.setStyleSheet(self.button_select_all)
989
- self.suppr_btn.setIcon(icon(MDI6.delete, color="black"))
990
- self.suppr_btn.setToolTip("Delete cell")
991
- self.suppr_btn.setIconSize(QSize(20, 20))
992
- self.suppr_btn.clicked.connect(self.del_cell)
993
- time_option_hbox.addWidget(self.suppr_btn)
994
-
995
- self.options_hbox.addLayout(time_option_hbox)
996
-
997
- def update_widgets(self):
998
-
999
- self.class_label.setText('characteristic \n group: ')
1000
- self.update_class_cb()
1001
- self.add_class_btn.setToolTip("Add a new characteristic group")
1002
- self.del_class_btn.setToolTip("Delete a characteristic group")
1003
-
1004
- self.export_btn.disconnect()
1005
- self.export_btn.clicked.connect(self.export_measurements)
1006
-
1007
- def update_class_cb(self):
1008
-
1009
- self.class_choice_cb.disconnect()
1010
- self.class_choice_cb.clear()
1011
- cols = np.array(self.df_tracks.columns)
1012
- self.class_cols = np.array([c.startswith('group') or c.startswith('status') for c in list(self.df_tracks.columns)])
1013
- self.class_cols = list(cols[self.class_cols])
1014
-
1015
- to_remove = ['group_id', 'group_color', 'class_id', 'class_color', 'status_color']
1016
- for col in to_remove:
1017
- try:
1018
- self.class_cols.remove(col)
1019
- except Exception:
1020
- pass
1021
-
1022
- self.class_choice_cb.addItems(self.class_cols)
1023
- self.class_choice_cb.currentIndexChanged.connect(self.changed_class)
1024
-
1025
- def populate_window(self):
1026
-
1027
- super().populate_window()
1028
- # Left panel updates
1029
- self.populate_options_layout()
1030
- self.update_widgets()
1031
-
1032
- self.annotation_btns_to_hide = [self.time_of_interest_label,
1033
- self.time_of_interest_le,
1034
- self.suppr_btn]
1035
- self.hide_annotation_buttons()
1036
-
1037
- # Right panel
1038
- animation_buttons_box = QHBoxLayout()
1039
- animation_buttons_box.addWidget(self.frame_lbl, 20, alignment=Qt.AlignLeft)
1040
-
1041
- self.first_frame_btn = QPushButton()
1042
- self.first_frame_btn.clicked.connect(self.set_previous_frame)
1043
- self.first_frame_btn.setShortcut(QKeySequence('f'))
1044
- self.first_frame_btn.setIcon(icon(MDI6.page_first, color="black"))
1045
- self.first_frame_btn.setStyleSheet(self.button_select_all)
1046
- self.first_frame_btn.setFixedSize(QSize(60, 60))
1047
- self.first_frame_btn.setIconSize(QSize(30, 30))
1048
-
1049
- self.last_frame_btn = QPushButton()
1050
- self.last_frame_btn.clicked.connect(self.set_next_frame)
1051
- self.last_frame_btn.setShortcut(QKeySequence('l'))
1052
- self.last_frame_btn.setIcon(icon(MDI6.page_last, color="black"))
1053
- self.last_frame_btn.setStyleSheet(self.button_select_all)
1054
- self.last_frame_btn.setFixedSize(QSize(60, 60))
1055
- self.last_frame_btn.setIconSize(QSize(30, 30))
1056
-
1057
- self.frame_slider = QSlider(Qt.Horizontal)
1058
- self.frame_slider.setFixedSize(200, 30)
1059
- self.frame_slider.setRange(0, self.len_movie - 1)
1060
- self.frame_slider.setValue(0)
1061
- self.frame_slider.valueChanged.connect(self.update_frame)
1062
-
1063
- self.start_btn = QPushButton()
1064
- self.start_btn.clicked.connect(self.start)
1065
- self.start_btn.setIcon(icon(MDI6.play, color="black"))
1066
- self.start_btn.setFixedSize(QSize(60, 60))
1067
- self.start_btn.setStyleSheet(self.button_select_all)
1068
- self.start_btn.setIconSize(QSize(30, 30))
1069
- self.start_btn.hide()
1070
-
1071
- animation_buttons_box.addWidget(self.first_frame_btn, 5, alignment=Qt.AlignRight)
1072
- animation_buttons_box.addWidget(self.frame_slider, 5, alignment=Qt.AlignCenter)
1073
- animation_buttons_box.addWidget(self.last_frame_btn, 5, alignment=Qt.AlignLeft)
1074
-
1075
- self.right_panel.addLayout(animation_buttons_box, 5)
1076
-
1077
- self.right_panel.addWidget(self.fcanvas, 90)
1078
-
1079
- contrast_hbox = QHBoxLayout()
1080
- contrast_hbox.setContentsMargins(150, 5, 150, 5)
1081
- self.contrast_slider = QLabeledDoubleRangeSlider()
1082
-
1083
- self.contrast_slider.setSingleStep(0.001)
1084
- self.contrast_slider.setTickInterval(0.001)
1085
- self.contrast_slider.setOrientation(Qt.Horizontal)
1086
- self.contrast_slider.setRange(
1087
- *[np.nanpercentile(self.img, 0.001), np.nanpercentile(self.img, 99.999)])
1088
- self.contrast_slider.setValue(
1089
- [np.nanpercentile(self.img, 1), np.nanpercentile(self.img, 99.99)])
1090
- self.contrast_slider.valueChanged.connect(self.contrast_slider_action)
1091
- contrast_hbox.addWidget(QLabel('contrast: '))
1092
- contrast_hbox.addWidget(self.contrast_slider, 90)
1093
- self.right_panel.addLayout(contrast_hbox, 5)
1094
- self.alpha_slider = QLabeledDoubleSlider()
1095
- self.alpha_slider.setSingleStep(0.001)
1096
- self.alpha_slider.setOrientation(Qt.Horizontal)
1097
- self.alpha_slider.setRange(0, 1)
1098
- self.alpha_slider.setValue(self.current_alpha)
1099
- self.alpha_slider.setDecimals(3)
1100
- self.alpha_slider.valueChanged.connect(self.set_transparency)
1101
-
1102
- slider_alpha_hbox = QHBoxLayout()
1103
- slider_alpha_hbox.setContentsMargins(150, 5, 150, 5)
1104
- slider_alpha_hbox.addWidget(QLabel('transparency: '), 10)
1105
- slider_alpha_hbox.addWidget(self.alpha_slider, 90)
1106
- self.right_panel.addLayout(slider_alpha_hbox)
1107
-
1108
- channel_hbox = QHBoxLayout()
1109
- self.choose_channel = QComboBox()
1110
- self.choose_channel.addItems(self.channel_names)
1111
- self.choose_channel.currentIndexChanged.connect(self.changed_channel)
1112
- channel_hbox.addWidget(self.choose_channel)
1113
- self.right_panel.addLayout(channel_hbox, 5)
1114
-
1115
- self.draw_frame(0)
1116
- self.vmin = self.contrast_slider.value()[0]
1117
- self.vmax = self.contrast_slider.value()[1]
1118
- self.im.set_clim(vmin=self.vmin, vmax=self.vmax)
1119
-
1120
- self.fcanvas.canvas.draw()
1121
- self.plot_signals()
1122
-
1123
- def static_image(self):
1124
-
1125
- """
1126
- Load an image.
1127
-
1128
- """
1129
-
1130
- self.framedata = 0
1131
- self.current_label=self.labels[self.current_frame]
1132
- self.fig, self.ax = plt.subplots(tight_layout=True)
1133
- self.fcanvas = FigureCanvas(self.fig, interactive=True)
1134
- self.ax.clear()
1135
- # print(self.current_stack.shape)
1136
- self.im = self.ax.imshow(self.img, cmap='gray')
1137
- self.status_scatter = self.ax.scatter(self.positions[0][:, 0], self.positions[0][:, 1], marker="o",
1138
- facecolors='none', edgecolors=self.colors[0][:, 0], s=200, picker=True)
1139
- self.im_mask = self.ax.imshow(np.ma.masked_where(self.current_label == 0, self.current_label),
1140
- cmap='viridis', interpolation='none',alpha=self.current_alpha,vmin=0,vmax=np.nanmax(self.labels.flatten()))
1141
- self.ax.set_xticks([])
1142
- self.ax.set_yticks([])
1143
- self.ax.set_aspect('equal')
1144
-
1145
- self.fig.set_facecolor('none') # or 'None'
1146
- self.fig.canvas.setStyleSheet("background-color: black;")
1147
-
1148
- self.fig.canvas.mpl_connect('pick_event', self.on_scatter_pick)
1149
- self.fcanvas.canvas.draw()
1150
-
1151
-
1152
- def plot_signals(self):
1153
-
1154
- #try:
1155
- current_frame = self.current_frame # Assuming you have a variable for the current frame
1156
-
1157
- yvalues = []
1158
- all_yvalues = []
1159
- current_yvalues = []
1160
- labels = []
1161
- range_values = []
1162
-
1163
- for i in range(len(self.signal_choice_cb)):
1164
-
1165
- signal_choice = self.signal_choice_cb[i].currentText()
1166
-
1167
- if signal_choice != "--":
1168
- if 'TRACK_ID' in self.df_tracks.columns:
1169
- ydata = self.df_tracks.loc[
1170
- (self.df_tracks['TRACK_ID'] == self.track_of_interest) &
1171
- (self.df_tracks['FRAME'] == current_frame), signal_choice].to_numpy()
1172
- else:
1173
- ydata = self.df_tracks.loc[
1174
- (self.df_tracks['ID'] == self.track_of_interest), signal_choice].to_numpy()
1175
- all_ydata = self.df_tracks.loc[:, signal_choice].to_numpy()
1176
- ydataNaN = ydata
1177
- ydata = ydata[ydata == ydata] # remove nan
1178
-
1179
- current_ydata = self.df_tracks.loc[
1180
- (self.df_tracks['FRAME'] == current_frame), signal_choice].to_numpy()
1181
- current_ydata = current_ydata[current_ydata == current_ydata]
1182
- all_ydata = all_ydata[all_ydata == all_ydata]
1183
- yvalues.extend(ydataNaN)
1184
- current_yvalues.append(current_ydata)
1185
- all_yvalues.append(all_ydata)
1186
- range_values.extend(all_ydata)
1187
- labels.append(signal_choice)
1188
-
1189
- self.cell_ax.clear()
1190
- if self.log_scale:
1191
- self.cell_ax.set_yscale('log')
1192
- else:
1193
- self.cell_ax.set_yscale('linear')
1194
-
1195
- if len(yvalues) > 0:
1196
- try:
1197
- self.cell_ax.boxplot(all_yvalues, showfliers=self.show_fliers)
1198
- except Exception as e:
1199
- print(f"{e=}")
1200
-
1201
- x_pos = np.arange(len(all_yvalues)) + 1
1202
- for index, feature in enumerate(current_yvalues):
1203
- x_values_strip = (index + 1) + np.random.normal(0, 0.04, size=len(
1204
- feature))
1205
- self.cell_ax.plot(x_values_strip, feature, marker='o', linestyle='None', color=tab10.colors[0],
1206
- alpha=0.1)
1207
- self.cell_ax.plot(x_pos, yvalues, marker='H', linestyle='None', color=tab10.colors[3], alpha=1)
1208
- range_values = np.array(range_values)
1209
- range_values = range_values[range_values==range_values]
1210
-
1211
- if len(range_values[range_values > 0]) > 0:
1212
- self.value_magnitude = np.nanmin(range_values[range_values > 0]) - 0.03 * (
1213
- np.nanmax(range_values[range_values > 0]) - np.nanmin(range_values[range_values > 0]))
1214
- else:
1215
- self.value_magnitude = 1
1216
-
1217
- self.non_log_ymin = np.nanmin(range_values) - 0.03 * (np.nanmax(range_values) - np.nanmin(range_values))
1218
- self.non_log_ymax = np.nanmax(range_values) + 0.03 * (np.nanmax(range_values) - np.nanmin(range_values))
1219
- if self.cell_ax.get_yscale() == 'linear':
1220
- self.cell_ax.set_ylim(self.non_log_ymin, self.non_log_ymax)
1221
- else:
1222
- self.cell_ax.set_ylim(self.value_magnitude, self.non_log_ymax)
1223
- else:
1224
- self.cell_ax.text(0.5, 0.5, "No data available", horizontalalignment='center',
1225
- verticalalignment='center', transform=self.cell_ax.transAxes)
1226
-
1227
- self.cell_fcanvas.canvas.draw()
1228
-
1229
- def plot_red_points(self, ax):
1230
- yvalues = []
1231
- current_frame = self.current_frame
1232
- for i in range(len(self.signal_choice_cb)):
1233
- signal_choice = self.signal_choice_cb[i].currentText()
1234
- if signal_choice != "--":
1235
- #print(f'plot signal {signal_choice} for cell {self.track_of_interest} at frame {current_frame}')
1236
- if 'TRACK_ID' in self.df_tracks.columns:
1237
- ydata = self.df_tracks.loc[
1238
- (self.df_tracks['TRACK_ID'] == self.track_of_interest) &
1239
- (self.df_tracks['FRAME'] == current_frame), signal_choice].to_numpy()
1240
- else:
1241
- ydata = self.df_tracks.loc[
1242
- (self.df_tracks['ID'] == self.track_of_interest) &
1243
- (self.df_tracks['FRAME'] == current_frame), signal_choice].to_numpy()
1244
- ydata = ydata[ydata == ydata] # remove nan
1245
- yvalues.extend(ydata)
1246
- x_pos = np.arange(len(yvalues)) + 1
1247
- ax.plot(x_pos, yvalues, marker='H', linestyle='None', color=tab10.colors[3],
1248
- alpha=1) # Plot red points representing cells
1249
- self.cell_fcanvas.canvas.draw()
1250
-
1251
- def select_single_cell(self, index, timepoint):
1252
-
1253
- self.correct_btn.setEnabled(True)
1254
- self.cancel_btn.setEnabled(True)
1255
- self.del_shortcut.setEnabled(True)
1256
-
1257
- self.track_of_interest = self.tracks[timepoint][index]
1258
- print(f'You selected cell #{self.track_of_interest}...')
1259
- self.give_cell_information()
1260
-
1261
- if len(self.cell_ax.lines) > 0:
1262
- self.cell_ax.lines[-1].remove() # Remove the last line (red points) from the plot
1263
- self.plot_red_points(self.cell_ax)
1264
- else:
1265
- self.plot_signals()
1266
-
1267
- self.loc_t = []
1268
- self.loc_idx = []
1269
- for t in range(len(self.tracks)):
1270
- indices = np.where(self.tracks[t] == self.track_of_interest)[0]
1271
- if len(indices) > 0:
1272
- self.loc_t.append(t)
1273
- self.loc_idx.append(indices[0])
1274
-
1275
- self.previous_color = []
1276
- for t, idx in zip(self.loc_t, self.loc_idx):
1277
- self.previous_color.append(self.colors[t][idx].copy())
1278
- self.colors[t][idx] = 'lime'
1279
-
1280
- self.draw_frame(self.current_frame)
1281
- self.fcanvas.canvas.draw()
1282
-
1283
- def cancel_selection(self):
1284
- super().cancel_selection()
1285
- self.event = None
1286
- self.draw_frame(self.current_frame)
1287
- self.fcanvas.canvas.draw()
1288
-
1289
- def export_measurements(self):
1290
-
1291
- auto_dataset_name = self.pos.split(os.sep)[-4] + '_' + self.pos.split(os.sep)[-2] + f'_{str(self.current_frame).zfill(3)}' + f'_{self.status_name}.npy'
1292
-
1293
- if self.normalized_signals:
1294
- self.normalize_features_btn.click()
1295
-
1296
- subdf = self.df_tracks.loc[self.df_tracks['FRAME']==self.current_frame,:]
1297
- subdf['class'] = subdf[self.status_name]
1298
- dico = subdf.to_dict('records')
1299
-
1300
- pathsave = QFileDialog.getSaveFileName(self, "Select file name", self.exp_dir + auto_dataset_name, ".npy")[0]
1301
- if pathsave != '':
1302
- if not pathsave.endswith(".npy"):
1303
- pathsave += ".npy"
1304
- try:
1305
- np.save(pathsave, dico)
1306
- print(f'File successfully written in {pathsave}.')
1307
- except Exception as e:
1308
- print(f"Error {e}...")
1309
-
1310
- def set_next_frame(self):
1311
-
1312
- self.current_frame = self.current_frame + 1
1313
- if self.current_frame > self.len_movie - 1:
1314
- self.current_frame == self.len_movie - 1
1315
- self.frame_slider.setValue(self.current_frame)
1316
- self.update_frame()
1317
- self.start_btn.setShortcut(QKeySequence("f"))
1318
-
1319
- def set_previous_frame(self):
1320
-
1321
- self.current_frame = self.current_frame - 1
1322
- if self.current_frame < 0:
1323
- self.current_frame == 0
1324
- self.frame_slider.setValue(self.current_frame)
1325
- self.update_frame()
1326
-
1327
- self.start_btn.setShortcut(QKeySequence("l"))
1328
-
1329
- def write_new_event_class(self):
1330
-
1331
- if self.class_name_le.text() == '':
1332
- self.target_class = 'group'
1333
- else:
1334
- self.target_class = 'group_' + self.class_name_le.text()
1335
-
1336
- if self.target_class in list(self.df_tracks.columns):
1337
- msgBox = QMessageBox()
1338
- msgBox.setIcon(QMessageBox.Warning)
1339
- msgBox.setText(
1340
- "This characteristic group name already exists. If you proceed,\nall annotated data will be rewritten. Do you wish to continue?")
1341
- msgBox.setWindowTitle("Warning")
1342
- msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
1343
- returnValue = msgBox.exec()
1344
- if returnValue == QMessageBox.No:
1345
- return None
1346
- else:
1347
- pass
1348
-
1349
- self.df_tracks.loc[:, self.target_class] = 0
1350
-
1351
- self.update_class_cb()
1352
-
1353
- idx = self.class_choice_cb.findText(self.target_class)
1354
- self.status_name = self.target_class
1355
- self.class_choice_cb.setCurrentIndex(idx)
1356
- self.newClassWidget.close()
1357
-
1358
- def hide_annotation_buttons(self):
1359
-
1360
- for a in self.annotation_btns_to_hide:
1361
- a.hide()
1362
- self.time_of_interest_label.setEnabled(False)
1363
- self.time_of_interest_le.setText('')
1364
- self.time_of_interest_le.setEnabled(False)
1365
-
1366
- def set_transparency(self):
1367
- self.current_alpha = self.alpha_slider.value()
1368
- self.im_mask.set_alpha(self.current_alpha)
1369
- self.fcanvas.canvas.draw()
1370
-
1371
- def show_annotation_buttons(self):
1372
-
1373
- for a in self.annotation_btns_to_hide:
1374
- a.show()
1375
-
1376
- self.time_of_interest_label.setEnabled(True)
1377
- self.time_of_interest_le.setEnabled(True)
1378
- self.correct_btn.setText('submit')
1379
-
1380
- self.correct_btn.disconnect()
1381
- self.correct_btn.clicked.connect(self.apply_modification)
1382
-
1383
- def give_cell_information(self):
1384
-
1385
- try:
1386
- cell_selected = f"cell: {self.track_of_interest}\n"
1387
- if 'TRACK_ID' in self.df_tracks.columns:
1388
- cell_status = f"phenotype: {self.df_tracks.loc[(self.df_tracks['FRAME']==self.current_frame)&(self.df_tracks['TRACK_ID'] == self.track_of_interest), self.status_name].to_numpy()[0]}\n"
1389
- else:
1390
- cell_status = f"phenotype: {self.df_tracks.loc[self.df_tracks['ID'] == self.track_of_interest, self.status_name].to_numpy()[0]}\n"
1391
- self.cell_info.setText(cell_selected + cell_status)
1392
- except Exception as e:
1393
- print(e)
1394
-
1395
- def create_new_event_class(self):
1396
-
1397
- # display qwidget to name the event
1398
- self.newClassWidget = CelldetectiveWidget()
1399
- self.newClassWidget.setWindowTitle('Create new characteristic group')
1400
-
1401
- layout = QVBoxLayout()
1402
- self.newClassWidget.setLayout(layout)
1403
- name_hbox = QHBoxLayout()
1404
- name_hbox.addWidget(QLabel('group name: '), 25)
1405
- self.class_name_le = QLineEdit('group')
1406
- name_hbox.addWidget(self.class_name_le, 75)
1407
- layout.addLayout(name_hbox)
1408
-
1409
- btn_hbox = QHBoxLayout()
1410
- submit_btn = QPushButton('submit')
1411
- cancel_btn = QPushButton('cancel')
1412
- btn_hbox.addWidget(cancel_btn, 50)
1413
- btn_hbox.addWidget(submit_btn, 50)
1414
- layout.addLayout(btn_hbox)
1415
-
1416
- submit_btn.clicked.connect(self.write_new_event_class)
1417
- cancel_btn.clicked.connect(self.close_without_new_class)
1418
-
1419
- self.newClassWidget.show()
1420
- center_window(self.newClassWidget)
1421
-
1422
- def apply_modification(self):
1423
- if self.time_of_interest_le.text() != "":
1424
- status = int(self.time_of_interest_le.text())
1425
- else:
1426
- status = 0
1427
- if "TRACK_ID" in self.df_tracks.columns:
1428
- self.df_tracks.loc[(self.df_tracks['TRACK_ID'] == self.track_of_interest) & (
1429
- self.df_tracks['FRAME'] == self.current_frame), self.status_name] = status
1430
-
1431
- indices = self.df_tracks.index[(self.df_tracks['TRACK_ID'] == self.track_of_interest) & (
1432
- self.df_tracks['FRAME'] == self.current_frame)]
1433
- else:
1434
- self.df_tracks.loc[(self.df_tracks['ID'] == self.track_of_interest) & (
1435
- self.df_tracks['FRAME'] == self.current_frame), self.status_name] = status
1436
-
1437
- indices = self.df_tracks.index[(self.df_tracks['ID'] == self.track_of_interest) & (
1438
- self.df_tracks['FRAME'] == self.current_frame)]
1439
-
1440
- self.df_tracks.loc[indices, self.status_name] = status
1441
- all_states = self.df_tracks.loc[:, self.status_name].tolist()
1442
- all_states = np.array(all_states)
1443
- self.state_color_map = color_from_state(all_states, recently_modified=False)
1444
-
1445
- self.df_tracks['group_color'] = self.df_tracks[self.status_name].apply(self.assign_color_state)
1446
- self.extract_scatter_from_trajectories()
1447
- self.give_cell_information()
1448
-
1449
- self.correct_btn.disconnect()
1450
- self.correct_btn.clicked.connect(self.show_annotation_buttons)
1451
-
1452
- self.hide_annotation_buttons()
1453
- self.correct_btn.setEnabled(False)
1454
- self.correct_btn.setText('correct')
1455
- self.cancel_btn.setEnabled(False)
1456
- self.del_shortcut.setEnabled(False)
1457
-
1458
- if len(self.selection) > 0:
1459
- self.selection.pop(0)
1460
-
1461
- self.draw_frame(self.current_frame)
1462
- self.fcanvas.canvas.draw()
1463
-
1464
- def assign_color_state(self, state):
1465
-
1466
- if np.isnan(state):
1467
- state = "nan"
1468
- return self.state_color_map[state]
1469
-
1470
- def draw_frame(self, framedata):
1471
-
1472
- """
1473
- Update plot elements at each timestep of the loop.
1474
- """
1475
- self.framedata = framedata
1476
- self.frame_lbl.setText(f'position: {self.framedata}')
1477
- self.im.set_array(self.img)
1478
- self.status_scatter.set_offsets(self.positions[self.framedata])
1479
- # try:
1480
- self.status_scatter.set_edgecolors(self.colors[self.framedata][:, 0])
1481
- # except Exception as e:
1482
- # pass
1483
-
1484
- self.current_label = self.labels[self.current_frame]
1485
- self.current_label = contour_of_instance_segmentation(self.current_label, 5)
1486
-
1487
- self.im_mask.remove()
1488
- self.im_mask = self.ax.imshow(np.ma.masked_where(self.current_label == 0, self.current_label),
1489
- cmap='viridis', interpolation='none',alpha=self.current_alpha,vmin=0,vmax=np.nanmax(self.labels.flatten()))
1490
-
1491
- return (self.im, self.status_scatter,self.im_mask,)
1492
-
1493
- def compute_status_and_colors(self):
1494
-
1495
- self.cancel_selection()
1496
-
1497
- if self.class_choice_cb.currentText() == '':
1498
- pass
1499
- else:
1500
- self.status_name = self.class_choice_cb.currentText()
1501
-
1502
- if self.status_name not in self.df_tracks.columns:
1503
- print('Creating a new status for visualization...')
1504
- self.make_status_column()
1505
- else:
1506
- print(f'Generating per-state colors for the status "{self.status_name}"...')
1507
- all_states = self.df_tracks.loc[:, self.status_name].tolist()
1508
- all_states = np.array(all_states)
1509
- self.state_color_map = color_from_state(all_states, recently_modified=False)
1510
- print(f'Color mapping for "{self.status_name}":')
1511
- pretty_table(self.state_color_map)
1512
- self.df_tracks['group_color'] = self.df_tracks[self.status_name].apply(self.assign_color_state)
1513
-
1514
-
1515
- def make_status_column(self):
1516
- if self.status_name == "state_firstdetection":
1517
- pass
1518
- else:
1519
- self.df_tracks.loc[:, self.status_name] = 0
1520
- all_states = self.df_tracks.loc[:, self.status_name].tolist()
1521
- all_states = np.array(all_states)
1522
- self.state_color_map = color_from_state(all_states, recently_modified=False)
1523
- self.df_tracks['group_color'] = self.df_tracks[self.status_name].apply(self.assign_color_state)
1524
-
1525
-
1526
- def extract_scatter_from_trajectories(self):
1527
-
1528
- self.positions = []
1529
- self.colors = []
1530
- self.tracks = []
1531
-
1532
- for t in np.arange(self.len_movie):
1533
- self.positions.append(self.df_tracks.loc[self.df_tracks['FRAME'] == t, ['POSITION_X', 'POSITION_Y']].to_numpy())
1534
- self.colors.append(
1535
- self.df_tracks.loc[self.df_tracks['FRAME'] == t, ['group_color']].to_numpy())
1536
- if 'TRACK_ID' in self.df_tracks.columns:
1537
- self.tracks.append(self.df_tracks.loc[self.df_tracks['FRAME'] == t, 'TRACK_ID'].to_numpy())
1538
- else:
1539
- self.tracks.append(self.df_tracks.loc[self.df_tracks['FRAME'] == t, 'ID'].to_numpy())
1540
-
1541
- def changed_class(self):
1542
- self.status_name = self.class_choice_cb.currentText()
1543
- if self.status_name!="":
1544
- self.compute_status_and_colors()
1545
- self.modify()
1546
- self.draw_frame(self.current_frame)
1547
- self.fcanvas.canvas.draw()
1548
-
1549
- def update_frame(self):
1550
- """
1551
- Update the displayed frame.
1552
- """
1553
- self.current_frame = self.frame_slider.value()
1554
- self.reload_frame()
1555
- if 'TRACK_ID' in list(self.df_tracks.columns):
1556
- pass
1557
- elif 'ID' in list(self.df_tracks.columns):
1558
- print('ID in cols... change class of interest... ')
1559
- self.track_of_interest = self.df_tracks[self.df_tracks['FRAME'] == self.current_frame]['ID'].min()
1560
- self.modify()
1561
-
1562
- self.draw_frame(self.current_frame)
1563
- self.vmin = self.contrast_slider.value()[0]
1564
- self.vmax = self.contrast_slider.value()[1]
1565
- self.im.set_clim(vmin=self.vmin, vmax=self.vmax)
1566
- self.give_cell_information()
1567
-
1568
- self.fcanvas.canvas.draw()
1569
- self.plot_signals()
1570
-
1571
- def changed_channel(self):
1572
-
1573
- self.reload_frame()
1574
- self.contrast_slider.setRange(
1575
- *[np.nanpercentile(self.img, 0.001),
1576
- np.nanpercentile(self.img, 99.999)])
1577
- self.contrast_slider.setValue(
1578
- [np.nanpercentile(self.img, 0.1), np.nanpercentile(self.img, 99.99)])
1579
- self.draw_frame(self.current_frame)
1580
- self.fcanvas.canvas.draw()
1581
-
1582
- def save_trajectories(self):
1583
- print(f"Saving trajectories !!")
1584
- if self.normalized_signals:
1585
- self.normalize_features_btn.click()
1586
- if self.selection:
1587
- self.cancel_selection()
1588
- self.df_tracks = self.df_tracks.drop(self.df_tracks[self.df_tracks[self.status_name] == 99].index)
1589
- #color_column = str(self.status_name) + "_color"
1590
- try:
1591
- self.df_tracks.drop(columns='', inplace=True)
1592
- except:
1593
- pass
1594
- try:
1595
- self.df_tracks.drop(columns='group_color', inplace=True)
1596
- except:
1597
- pass
1598
- try:
1599
- self.df_tracks.drop(columns='x_anim', inplace=True)
1600
- except:
1601
- pass
1602
- try:
1603
- self.df_tracks.drop(columns='y_anim', inplace=True)
1604
- except:
1605
- pass
1606
-
1607
- self.df_tracks.to_csv(self.trajectories_path, index=False)
1608
- print('Table successfully exported...')
1609
-
1610
- self.locate_tracks()
1611
- self.changed_class()
1612
-
1613
- def modify(self):
1614
- all_states = self.df_tracks.loc[:, self.status_name].tolist()
1615
- all_states = np.array(all_states)
1616
- self.state_color_map = color_from_state(all_states, recently_modified=False)
1617
-
1618
- self.df_tracks['group_color'] = self.df_tracks[self.status_name].apply(self.assign_color_state)
1619
-
1620
- self.extract_scatter_from_trajectories()
1621
- self.give_cell_information()
1622
-
1623
- self.correct_btn.disconnect()
1624
- self.correct_btn.clicked.connect(self.show_annotation_buttons)
1625
-
1626
- def reload_frame(self):
1627
-
1628
- """
1629
- Load the frame from the current channel and time choice. Show imshow, update histogram.
1630
- """
1631
-
1632
- # self.clear_post_threshold_options()
1633
- self.previous_channel = self.current_channel
1634
- self.current_channel = self.choose_channel.currentIndex()
1635
-
1636
- t = int(self.frame_slider.value())
1637
- idx = t * self.nbr_channels + self.current_channel
1638
- self.img = load_frames(idx, self.stack_path, normalize_input=False)
1639
-
1640
- if self.previous_channel != self.current_channel:
1641
- # reinitialize intensity bounds
1642
- epsilon = 0.01
1643
- self.observed_min_intensity = 0
1644
- self.observed_max_intensity = 0 + epsilon
1645
-
1646
- if self.img is not None:
1647
- max_img = np.nanmax(self.img)
1648
- min_img = np.nanmin(self.img)
1649
- if max_img > self.observed_max_intensity:
1650
- self.observed_max_intensity = max_img
1651
- if min_img < self.observed_min_intensity:
1652
- self.observed_min_intensity = min_img
1653
- self.refresh_imshow()
1654
- # self.redo_histogram()
1655
- else:
1656
- print('Frame could not be loaded...')
1657
-
1658
- def refresh_imshow(self):
1659
-
1660
- """
1661
-
1662
- Update the imshow based on the current frame selection.
1663
-
1664
- """
1665
-
1666
- if self.previous_channel != self.current_channel:
1667
-
1668
- self.vmin = np.nanpercentile(self.img.flatten(), 0.1)
1669
- self.vmax = np.nanpercentile(self.img.flatten(), 99.99)
1670
-
1671
- self.contrast_slider.disconnect()
1672
- self.contrast_slider.setRange(np.nanmin(self.img), np.nanmax(self.img))
1673
- self.contrast_slider.setValue([self.vmin, self.vmax])
1674
- self.contrast_slider.valueChanged.connect(self.contrast_slider_action)
1675
- else:
1676
- #self.contrast_slider.disconnect()
1677
- self.contrast_slider.setRange(self.observed_min_intensity, self.observed_max_intensity)
1678
- #self.contrast_slider.valueChanged.connect(self.contrast_slider_action)
1679
-
1680
- self.im.set_data(self.img)
1681
-
1682
- def del_cell(self):
1683
- self.time_of_interest_le.setEnabled(False)
1684
- self.time_of_interest_le.setText("99")
1685
- self.apply_modification()
1035
+ def __init__(self, *args, **kwargs):
1036
+
1037
+ super().__init__(read_config=False, *args, **kwargs)
1038
+
1039
+ self.setWindowTitle("Static annotator")
1040
+
1041
+ self.int_validator = QIntValidator()
1042
+ self.current_alpha = 0.5
1043
+ self.value_magnitude = 1
1044
+
1045
+ epsilon = 0.01
1046
+ self.observed_min_intensity = 0
1047
+ self.observed_max_intensity = 0 + epsilon
1048
+
1049
+ self.current_frame = 0
1050
+ self.show_fliers = False
1051
+ self.status_name = "group"
1052
+
1053
+ if self.proceed:
1054
+
1055
+ self.labels = locate_labels(self.pos, population=self.mode)
1056
+
1057
+ self.current_channel = 0
1058
+ self.frame_lbl = QLabel("position: ")
1059
+ self.static_image()
1060
+
1061
+ self.populate_window()
1062
+ self.changed_class()
1063
+
1064
+ self.previous_index = None
1065
+ if hasattr(self, "contrast_slider"):
1066
+ self.im.set_clim(
1067
+ self.contrast_slider.value()[0], self.contrast_slider.value()[1]
1068
+ )
1069
+
1070
+ else:
1071
+ self.close()
1072
+
1073
+ def locate_tracks(self):
1074
+ """
1075
+ Locate the tracks.
1076
+ """
1077
+
1078
+ if not os.path.exists(self.trajectories_path):
1079
+
1080
+ msgBox = QMessageBox()
1081
+ msgBox.setIcon(QMessageBox.Warning)
1082
+ msgBox.setText("The trajectories cannot be detected.")
1083
+ msgBox.setWindowTitle("Warning")
1084
+ msgBox.setStandardButtons(QMessageBox.Ok)
1085
+ returnValue = msgBox.exec()
1086
+ if returnValue == QMessageBox.Yes:
1087
+ self.close()
1088
+ else:
1089
+
1090
+ # Load and prep tracks
1091
+ self.df_tracks = pd.read_csv(self.trajectories_path)
1092
+ if "TRACK_ID" in self.df_tracks.columns:
1093
+ self.df_tracks = self.df_tracks.sort_values(by=["TRACK_ID", "FRAME"])
1094
+ else:
1095
+ self.df_tracks = self.df_tracks.sort_values(by=["ID", "FRAME"])
1096
+
1097
+ cols = np.array(self.df_tracks.columns)
1098
+ self.class_cols = np.array(
1099
+ [
1100
+ c.startswith("group") or c.startswith("class")
1101
+ for c in list(self.df_tracks.columns)
1102
+ ]
1103
+ )
1104
+ self.class_cols = list(cols[self.class_cols])
1105
+
1106
+ to_remove = ["class_id", "group_color", "class_color"]
1107
+ for col in to_remove:
1108
+ try:
1109
+ self.class_cols.remove(col)
1110
+ except:
1111
+ pass
1112
+
1113
+ if len(self.class_cols) > 0:
1114
+ self.status_name = self.class_cols[0]
1115
+ else:
1116
+ self.status_name = "group"
1117
+
1118
+ if self.status_name not in self.df_tracks.columns:
1119
+ # only create the status column if it does not exist to not erase static classification results
1120
+ self.make_status_column()
1121
+ else:
1122
+ # all good, do nothing
1123
+ pass
1124
+
1125
+ all_states = self.df_tracks.loc[:, self.status_name].tolist()
1126
+ all_states = np.array(all_states)
1127
+ self.state_color_map = color_from_state(all_states, recently_modified=False)
1128
+ self.df_tracks["group_color"] = self.df_tracks[self.status_name].apply(
1129
+ self.assign_color_state
1130
+ )
1131
+
1132
+ self.df_tracks = self.df_tracks.dropna(subset=["POSITION_X", "POSITION_Y"])
1133
+ self.df_tracks["x_anim"] = self.df_tracks["POSITION_X"]
1134
+ self.df_tracks["y_anim"] = self.df_tracks["POSITION_Y"]
1135
+ self.df_tracks["x_anim"] = self.df_tracks["x_anim"].astype(int)
1136
+ self.df_tracks["y_anim"] = self.df_tracks["y_anim"].astype(int)
1137
+
1138
+ self.extract_scatter_from_trajectories()
1139
+ if "TRACK_ID" in self.df_tracks.columns:
1140
+ self.track_of_interest = self.df_tracks.dropna(subset="TRACK_ID")[
1141
+ "TRACK_ID"
1142
+ ].min()
1143
+ else:
1144
+ self.track_of_interest = self.df_tracks.dropna(subset="ID")["ID"].min()
1145
+
1146
+ self.loc_t = []
1147
+ self.loc_idx = []
1148
+ for t in range(len(self.tracks)):
1149
+ indices = np.where(self.tracks[t] == self.track_of_interest)[0]
1150
+ if len(indices) > 0:
1151
+ self.loc_t.append(t)
1152
+ self.loc_idx.append(indices[0])
1153
+
1154
+ self.MinMaxScaler = MinMaxScaler()
1155
+ self.columns_to_rescale = list(self.df_tracks.columns)
1156
+
1157
+ cols_to_remove = [
1158
+ "group",
1159
+ "group_color",
1160
+ "status",
1161
+ "status_color",
1162
+ "class_color",
1163
+ "TRACK_ID",
1164
+ "FRAME",
1165
+ "x_anim",
1166
+ "y_anim",
1167
+ "t",
1168
+ "dummy",
1169
+ "group_color",
1170
+ "state",
1171
+ "generation",
1172
+ "root",
1173
+ "parent",
1174
+ "class_id",
1175
+ "class",
1176
+ "t0",
1177
+ "POSITION_X",
1178
+ "POSITION_Y",
1179
+ "position",
1180
+ "well",
1181
+ "well_index",
1182
+ "well_name",
1183
+ "pos_name",
1184
+ "index",
1185
+ "concentration",
1186
+ "cell_type",
1187
+ "antibody",
1188
+ "pharmaceutical_agent",
1189
+ "ID",
1190
+ ] + self.class_cols
1191
+
1192
+ meta = get_experiment_metadata(self.exp_dir)
1193
+ if meta is not None:
1194
+ keys = list(meta.keys())
1195
+ cols_to_remove.extend(keys)
1196
+
1197
+ labels = get_experiment_labels(self.exp_dir)
1198
+ if labels is not None:
1199
+ keys = list(labels.keys())
1200
+ cols_to_remove.extend(labels)
1201
+
1202
+ for tr in cols_to_remove:
1203
+ try:
1204
+ self.columns_to_rescale.remove(tr)
1205
+ except:
1206
+ pass
1207
+
1208
+ x = self.df_tracks[self.columns_to_rescale].values
1209
+ self.MinMaxScaler.fit(x)
1210
+
1211
+ def populate_options_layout(self):
1212
+ # clear options hbox
1213
+ for i in reversed(range(self.options_hbox.count())):
1214
+ self.options_hbox.itemAt(i).widget().setParent(None)
1215
+
1216
+ time_option_hbox = QHBoxLayout()
1217
+ time_option_hbox.setContentsMargins(100, 0, 100, 0)
1218
+ time_option_hbox.setSpacing(0)
1219
+
1220
+ self.time_of_interest_label = QLabel("phenotype: ")
1221
+ time_option_hbox.addWidget(self.time_of_interest_label, 30)
1222
+
1223
+ self.time_of_interest_le = QLineEdit()
1224
+ self.time_of_interest_le.setValidator(self.int_validator)
1225
+ time_option_hbox.addWidget(self.time_of_interest_le)
1226
+
1227
+ self.suppr_btn = QPushButton("")
1228
+ self.suppr_btn.setStyleSheet(self.button_select_all)
1229
+ self.suppr_btn.setIcon(icon(MDI6.delete, color="black"))
1230
+ self.suppr_btn.setToolTip("Delete cell")
1231
+ self.suppr_btn.setIconSize(QSize(20, 20))
1232
+ self.suppr_btn.clicked.connect(self.del_cell)
1233
+ time_option_hbox.addWidget(self.suppr_btn)
1234
+
1235
+ self.options_hbox.addLayout(time_option_hbox)
1236
+
1237
+ def update_widgets(self):
1238
+
1239
+ self.class_label.setText("characteristic \n group: ")
1240
+ self.update_class_cb()
1241
+ self.add_class_btn.setToolTip("Add a new characteristic group")
1242
+ self.del_class_btn.setToolTip("Delete a characteristic group")
1243
+
1244
+ self.export_btn.disconnect()
1245
+ self.export_btn.clicked.connect(self.export_measurements)
1246
+
1247
+ def update_class_cb(self):
1248
+
1249
+ self.class_choice_cb.disconnect()
1250
+ self.class_choice_cb.clear()
1251
+ cols = np.array(self.df_tracks.columns)
1252
+ self.class_cols = np.array(
1253
+ [
1254
+ c.startswith("group") or c.startswith("status")
1255
+ for c in list(self.df_tracks.columns)
1256
+ ]
1257
+ )
1258
+ self.class_cols = list(cols[self.class_cols])
1259
+
1260
+ to_remove = [
1261
+ "group_id",
1262
+ "group_color",
1263
+ "class_id",
1264
+ "class_color",
1265
+ "status_color",
1266
+ ]
1267
+ for col in to_remove:
1268
+ try:
1269
+ self.class_cols.remove(col)
1270
+ except Exception:
1271
+ pass
1272
+
1273
+ self.class_choice_cb.addItems(self.class_cols)
1274
+ self.class_choice_cb.currentIndexChanged.connect(self.changed_class)
1275
+
1276
+ def populate_window(self):
1277
+
1278
+ super().populate_window()
1279
+ # Left panel updates
1280
+ self.populate_options_layout()
1281
+ self.update_widgets()
1282
+
1283
+ self.annotation_btns_to_hide = [
1284
+ self.time_of_interest_label,
1285
+ self.time_of_interest_le,
1286
+ self.suppr_btn,
1287
+ ]
1288
+ self.hide_annotation_buttons()
1289
+
1290
+ # Right panel
1291
+ animation_buttons_box = QHBoxLayout()
1292
+ animation_buttons_box.addWidget(self.frame_lbl, 20, alignment=Qt.AlignLeft)
1293
+
1294
+ self.first_frame_btn = QPushButton()
1295
+ self.first_frame_btn.clicked.connect(self.set_previous_frame)
1296
+ self.first_frame_btn.setShortcut(QKeySequence("f"))
1297
+ self.first_frame_btn.setIcon(icon(MDI6.page_first, color="black"))
1298
+ self.first_frame_btn.setStyleSheet(self.button_select_all)
1299
+ self.first_frame_btn.setFixedSize(QSize(60, 60))
1300
+ self.first_frame_btn.setIconSize(QSize(30, 30))
1301
+
1302
+ self.last_frame_btn = QPushButton()
1303
+ self.last_frame_btn.clicked.connect(self.set_next_frame)
1304
+ self.last_frame_btn.setShortcut(QKeySequence("l"))
1305
+ self.last_frame_btn.setIcon(icon(MDI6.page_last, color="black"))
1306
+ self.last_frame_btn.setStyleSheet(self.button_select_all)
1307
+ self.last_frame_btn.setFixedSize(QSize(60, 60))
1308
+ self.last_frame_btn.setIconSize(QSize(30, 30))
1309
+
1310
+ self.frame_slider = QSlider(Qt.Horizontal)
1311
+ self.frame_slider.setFixedSize(200, 30)
1312
+ self.frame_slider.setRange(0, self.len_movie - 1)
1313
+ self.frame_slider.setValue(0)
1314
+ self.frame_slider.valueChanged.connect(self.update_frame)
1315
+
1316
+ self.start_btn = QPushButton()
1317
+ self.start_btn.clicked.connect(self.start)
1318
+ self.start_btn.setIcon(icon(MDI6.play, color="black"))
1319
+ self.start_btn.setFixedSize(QSize(60, 60))
1320
+ self.start_btn.setStyleSheet(self.button_select_all)
1321
+ self.start_btn.setIconSize(QSize(30, 30))
1322
+ self.start_btn.hide()
1323
+
1324
+ animation_buttons_box.addWidget(
1325
+ self.first_frame_btn, 5, alignment=Qt.AlignRight
1326
+ )
1327
+ animation_buttons_box.addWidget(self.frame_slider, 5, alignment=Qt.AlignCenter)
1328
+ animation_buttons_box.addWidget(self.last_frame_btn, 5, alignment=Qt.AlignLeft)
1329
+
1330
+ self.right_panel.addLayout(animation_buttons_box, 5)
1331
+
1332
+ self.right_panel.addWidget(self.fcanvas, 90)
1333
+
1334
+ contrast_hbox = QHBoxLayout()
1335
+ contrast_hbox.setContentsMargins(150, 5, 150, 5)
1336
+ self.contrast_slider = QLabeledDoubleRangeSlider()
1337
+
1338
+ self.contrast_slider.setSingleStep(0.001)
1339
+ self.contrast_slider.setTickInterval(0.001)
1340
+ self.contrast_slider.setOrientation(Qt.Horizontal)
1341
+ self.contrast_slider.setRange(
1342
+ *[np.nanpercentile(self.img, 0.001), np.nanpercentile(self.img, 99.999)]
1343
+ )
1344
+ self.contrast_slider.setValue(
1345
+ [np.nanpercentile(self.img, 1), np.nanpercentile(self.img, 99.99)]
1346
+ )
1347
+ self.contrast_slider.valueChanged.connect(self.contrast_slider_action)
1348
+ contrast_hbox.addWidget(QLabel("contrast: "))
1349
+ contrast_hbox.addWidget(self.contrast_slider, 90)
1350
+ self.right_panel.addLayout(contrast_hbox, 5)
1351
+ self.alpha_slider = QLabeledDoubleSlider()
1352
+ self.alpha_slider.setSingleStep(0.001)
1353
+ self.alpha_slider.setOrientation(Qt.Horizontal)
1354
+ self.alpha_slider.setRange(0, 1)
1355
+ self.alpha_slider.setValue(self.current_alpha)
1356
+ self.alpha_slider.setDecimals(3)
1357
+ self.alpha_slider.valueChanged.connect(self.set_transparency)
1358
+
1359
+ slider_alpha_hbox = QHBoxLayout()
1360
+ slider_alpha_hbox.setContentsMargins(150, 5, 150, 5)
1361
+ slider_alpha_hbox.addWidget(QLabel("transparency: "), 10)
1362
+ slider_alpha_hbox.addWidget(self.alpha_slider, 90)
1363
+ self.right_panel.addLayout(slider_alpha_hbox)
1364
+
1365
+ channel_hbox = QHBoxLayout()
1366
+ self.choose_channel = QComboBox()
1367
+ self.choose_channel.addItems(self.channel_names)
1368
+ self.choose_channel.currentIndexChanged.connect(self.changed_channel)
1369
+ channel_hbox.addWidget(self.choose_channel)
1370
+ self.right_panel.addLayout(channel_hbox, 5)
1371
+
1372
+ self.draw_frame(0)
1373
+ self.vmin = self.contrast_slider.value()[0]
1374
+ self.vmax = self.contrast_slider.value()[1]
1375
+ self.im.set_clim(vmin=self.vmin, vmax=self.vmax)
1376
+
1377
+ self.fcanvas.canvas.draw()
1378
+ self.plot_signals()
1379
+
1380
+ def static_image(self):
1381
+ """
1382
+ Load an image.
1383
+
1384
+ """
1385
+
1386
+ self.framedata = 0
1387
+ self.current_label = self.labels[self.current_frame]
1388
+ self.fig, self.ax = plt.subplots(tight_layout=True)
1389
+ self.fcanvas = FigureCanvas(self.fig, interactive=True)
1390
+ self.ax.clear()
1391
+ # print(self.current_stack.shape)
1392
+ self.im = self.ax.imshow(self.img, cmap="gray")
1393
+ self.status_scatter = self.ax.scatter(
1394
+ self.positions[0][:, 0],
1395
+ self.positions[0][:, 1],
1396
+ marker="o",
1397
+ facecolors="none",
1398
+ edgecolors=self.colors[0][:, 0],
1399
+ s=200,
1400
+ picker=True,
1401
+ )
1402
+ self.im_mask = self.ax.imshow(
1403
+ np.ma.masked_where(self.current_label == 0, self.current_label),
1404
+ cmap="viridis",
1405
+ interpolation="none",
1406
+ alpha=self.current_alpha,
1407
+ vmin=0,
1408
+ vmax=np.nanmax(self.labels.flatten()),
1409
+ )
1410
+ self.ax.set_xticks([])
1411
+ self.ax.set_yticks([])
1412
+ self.ax.set_aspect("equal")
1413
+
1414
+ self.fig.set_facecolor("none") # or 'None'
1415
+ self.fig.canvas.setStyleSheet("background-color: black;")
1416
+
1417
+ self.fig.canvas.mpl_connect("pick_event", self.on_scatter_pick)
1418
+ self.fcanvas.canvas.draw()
1419
+
1420
+ def plot_signals(self):
1421
+
1422
+ # try:
1423
+ current_frame = (
1424
+ self.current_frame
1425
+ ) # Assuming you have a variable for the current frame
1426
+
1427
+ yvalues = []
1428
+ all_yvalues = []
1429
+ current_yvalues = []
1430
+ labels = []
1431
+ range_values = []
1432
+
1433
+ for i in range(len(self.signal_choice_cb)):
1434
+
1435
+ signal_choice = self.signal_choice_cb[i].currentText()
1436
+
1437
+ if signal_choice != "--":
1438
+ if "TRACK_ID" in self.df_tracks.columns:
1439
+ ydata = self.df_tracks.loc[
1440
+ (self.df_tracks["TRACK_ID"] == self.track_of_interest)
1441
+ & (self.df_tracks["FRAME"] == current_frame),
1442
+ signal_choice,
1443
+ ].to_numpy()
1444
+ else:
1445
+ ydata = self.df_tracks.loc[
1446
+ (self.df_tracks["ID"] == self.track_of_interest), signal_choice
1447
+ ].to_numpy()
1448
+ all_ydata = self.df_tracks.loc[:, signal_choice].to_numpy()
1449
+ ydataNaN = ydata
1450
+ ydata = ydata[ydata == ydata] # remove nan
1451
+
1452
+ current_ydata = self.df_tracks.loc[
1453
+ (self.df_tracks["FRAME"] == current_frame), signal_choice
1454
+ ].to_numpy()
1455
+ current_ydata = current_ydata[current_ydata == current_ydata]
1456
+ all_ydata = all_ydata[all_ydata == all_ydata]
1457
+ yvalues.extend(ydataNaN)
1458
+ current_yvalues.append(current_ydata)
1459
+ all_yvalues.append(all_ydata)
1460
+ range_values.extend(all_ydata)
1461
+ labels.append(signal_choice)
1462
+
1463
+ self.cell_ax.clear()
1464
+ if self.log_scale:
1465
+ self.cell_ax.set_yscale("log")
1466
+ else:
1467
+ self.cell_ax.set_yscale("linear")
1468
+
1469
+ if len(yvalues) > 0:
1470
+ try:
1471
+ self.cell_ax.boxplot(all_yvalues, showfliers=self.show_fliers)
1472
+ except Exception as e:
1473
+ print(f"{e=}")
1474
+
1475
+ x_pos = np.arange(len(all_yvalues)) + 1
1476
+ for index, feature in enumerate(current_yvalues):
1477
+ x_values_strip = (index + 1) + np.random.normal(
1478
+ 0, 0.04, size=len(feature)
1479
+ )
1480
+ self.cell_ax.plot(
1481
+ x_values_strip,
1482
+ feature,
1483
+ marker="o",
1484
+ linestyle="None",
1485
+ color=tab10.colors[0],
1486
+ alpha=0.1,
1487
+ )
1488
+ self.cell_ax.plot(
1489
+ x_pos,
1490
+ yvalues,
1491
+ marker="H",
1492
+ linestyle="None",
1493
+ color=tab10.colors[3],
1494
+ alpha=1,
1495
+ )
1496
+ range_values = np.array(range_values)
1497
+ range_values = range_values[range_values == range_values]
1498
+
1499
+ if len(range_values[range_values > 0]) > 0:
1500
+ self.value_magnitude = np.nanmin(
1501
+ range_values[range_values > 0]
1502
+ ) - 0.03 * (
1503
+ np.nanmax(range_values[range_values > 0])
1504
+ - np.nanmin(range_values[range_values > 0])
1505
+ )
1506
+ else:
1507
+ self.value_magnitude = 1
1508
+
1509
+ self.non_log_ymin = np.nanmin(range_values) - 0.03 * (
1510
+ np.nanmax(range_values) - np.nanmin(range_values)
1511
+ )
1512
+ self.non_log_ymax = np.nanmax(range_values) + 0.03 * (
1513
+ np.nanmax(range_values) - np.nanmin(range_values)
1514
+ )
1515
+ if self.cell_ax.get_yscale() == "linear":
1516
+ self.cell_ax.set_ylim(self.non_log_ymin, self.non_log_ymax)
1517
+ else:
1518
+ self.cell_ax.set_ylim(self.value_magnitude, self.non_log_ymax)
1519
+ else:
1520
+ self.cell_ax.text(
1521
+ 0.5,
1522
+ 0.5,
1523
+ "No data available",
1524
+ horizontalalignment="center",
1525
+ verticalalignment="center",
1526
+ transform=self.cell_ax.transAxes,
1527
+ )
1528
+
1529
+ self.cell_fcanvas.canvas.draw()
1530
+
1531
+ def plot_red_points(self, ax):
1532
+ yvalues = []
1533
+ current_frame = self.current_frame
1534
+ for i in range(len(self.signal_choice_cb)):
1535
+ signal_choice = self.signal_choice_cb[i].currentText()
1536
+ if signal_choice != "--":
1537
+ # print(f'plot signal {signal_choice} for cell {self.track_of_interest} at frame {current_frame}')
1538
+ if "TRACK_ID" in self.df_tracks.columns:
1539
+ ydata = self.df_tracks.loc[
1540
+ (self.df_tracks["TRACK_ID"] == self.track_of_interest)
1541
+ & (self.df_tracks["FRAME"] == current_frame),
1542
+ signal_choice,
1543
+ ].to_numpy()
1544
+ else:
1545
+ ydata = self.df_tracks.loc[
1546
+ (self.df_tracks["ID"] == self.track_of_interest)
1547
+ & (self.df_tracks["FRAME"] == current_frame),
1548
+ signal_choice,
1549
+ ].to_numpy()
1550
+ ydata = ydata[ydata == ydata] # remove nan
1551
+ yvalues.extend(ydata)
1552
+ x_pos = np.arange(len(yvalues)) + 1
1553
+ ax.plot(
1554
+ x_pos, yvalues, marker="H", linestyle="None", color=tab10.colors[3], alpha=1
1555
+ ) # Plot red points representing cells
1556
+ self.cell_fcanvas.canvas.draw()
1557
+
1558
+ def select_single_cell(self, index, timepoint):
1559
+
1560
+ self.correct_btn.setEnabled(True)
1561
+ self.cancel_btn.setEnabled(True)
1562
+ self.del_shortcut.setEnabled(True)
1563
+
1564
+ self.track_of_interest = self.tracks[timepoint][index]
1565
+ print(f"You selected cell #{self.track_of_interest}...")
1566
+ self.give_cell_information()
1567
+
1568
+ if len(self.cell_ax.lines) > 0:
1569
+ self.cell_ax.lines[
1570
+ -1
1571
+ ].remove() # Remove the last line (red points) from the plot
1572
+ self.plot_red_points(self.cell_ax)
1573
+ else:
1574
+ self.plot_signals()
1575
+
1576
+ self.loc_t = []
1577
+ self.loc_idx = []
1578
+ for t in range(len(self.tracks)):
1579
+ indices = np.where(self.tracks[t] == self.track_of_interest)[0]
1580
+ if len(indices) > 0:
1581
+ self.loc_t.append(t)
1582
+ self.loc_idx.append(indices[0])
1583
+
1584
+ self.previous_color = []
1585
+ for t, idx in zip(self.loc_t, self.loc_idx):
1586
+ self.previous_color.append(self.colors[t][idx].copy())
1587
+ self.colors[t][idx] = "lime"
1588
+
1589
+ self.draw_frame(self.current_frame)
1590
+ self.fcanvas.canvas.draw()
1591
+
1592
+ def cancel_selection(self):
1593
+ super().cancel_selection()
1594
+ self.event = None
1595
+ self.draw_frame(self.current_frame)
1596
+ self.fcanvas.canvas.draw()
1597
+
1598
+ def export_measurements(self):
1599
+
1600
+ auto_dataset_name = (
1601
+ self.pos.split(os.sep)[-4]
1602
+ + "_"
1603
+ + self.pos.split(os.sep)[-2]
1604
+ + f"_{str(self.current_frame).zfill(3)}"
1605
+ + f"_{self.status_name}.npy"
1606
+ )
1607
+
1608
+ if self.normalized_signals:
1609
+ self.normalize_features_btn.click()
1610
+
1611
+ subdf = self.df_tracks.loc[self.df_tracks["FRAME"] == self.current_frame, :]
1612
+ subdf["class"] = subdf[self.status_name]
1613
+ dico = subdf.to_dict("records")
1614
+
1615
+ pathsave = QFileDialog.getSaveFileName(
1616
+ self, "Select file name", self.exp_dir + auto_dataset_name, ".npy"
1617
+ )[0]
1618
+ if pathsave != "":
1619
+ if not pathsave.endswith(".npy"):
1620
+ pathsave += ".npy"
1621
+ try:
1622
+ np.save(pathsave, dico)
1623
+ print(f"File successfully written in {pathsave}.")
1624
+ except Exception as e:
1625
+ print(f"Error {e}...")
1626
+
1627
+ def set_next_frame(self):
1628
+
1629
+ self.current_frame = self.current_frame + 1
1630
+ if self.current_frame > self.len_movie - 1:
1631
+ self.current_frame == self.len_movie - 1
1632
+ self.frame_slider.setValue(self.current_frame)
1633
+ self.update_frame()
1634
+ self.start_btn.setShortcut(QKeySequence("f"))
1635
+
1636
+ def set_previous_frame(self):
1637
+
1638
+ self.current_frame = self.current_frame - 1
1639
+ if self.current_frame < 0:
1640
+ self.current_frame == 0
1641
+ self.frame_slider.setValue(self.current_frame)
1642
+ self.update_frame()
1643
+
1644
+ self.start_btn.setShortcut(QKeySequence("l"))
1645
+
1646
+ def write_new_event_class(self):
1647
+
1648
+ if self.class_name_le.text() == "":
1649
+ self.target_class = "group"
1650
+ else:
1651
+ self.target_class = "group_" + self.class_name_le.text()
1652
+
1653
+ if self.target_class in list(self.df_tracks.columns):
1654
+ msgBox = QMessageBox()
1655
+ msgBox.setIcon(QMessageBox.Warning)
1656
+ msgBox.setText(
1657
+ "This characteristic group name already exists. If you proceed,\nall annotated data will be rewritten. Do you wish to continue?"
1658
+ )
1659
+ msgBox.setWindowTitle("Warning")
1660
+ msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
1661
+ returnValue = msgBox.exec()
1662
+ if returnValue == QMessageBox.No:
1663
+ return None
1664
+ else:
1665
+ pass
1666
+
1667
+ self.df_tracks.loc[:, self.target_class] = 0
1668
+
1669
+ self.update_class_cb()
1670
+
1671
+ idx = self.class_choice_cb.findText(self.target_class)
1672
+ self.status_name = self.target_class
1673
+ self.class_choice_cb.setCurrentIndex(idx)
1674
+ self.newClassWidget.close()
1675
+
1676
+ def hide_annotation_buttons(self):
1677
+
1678
+ for a in self.annotation_btns_to_hide:
1679
+ a.hide()
1680
+ self.time_of_interest_label.setEnabled(False)
1681
+ self.time_of_interest_le.setText("")
1682
+ self.time_of_interest_le.setEnabled(False)
1683
+
1684
+ def set_transparency(self):
1685
+ self.current_alpha = self.alpha_slider.value()
1686
+ self.im_mask.set_alpha(self.current_alpha)
1687
+ self.fcanvas.canvas.draw()
1688
+
1689
+ def show_annotation_buttons(self):
1690
+
1691
+ for a in self.annotation_btns_to_hide:
1692
+ a.show()
1693
+
1694
+ self.time_of_interest_label.setEnabled(True)
1695
+ self.time_of_interest_le.setEnabled(True)
1696
+ self.correct_btn.setText("submit")
1697
+
1698
+ self.correct_btn.disconnect()
1699
+ self.correct_btn.clicked.connect(self.apply_modification)
1700
+
1701
+ def give_cell_information(self):
1702
+
1703
+ try:
1704
+ cell_selected = f"cell: {self.track_of_interest}\n"
1705
+ if "TRACK_ID" in self.df_tracks.columns:
1706
+ cell_status = f"phenotype: {self.df_tracks.loc[(self.df_tracks['FRAME']==self.current_frame)&(self.df_tracks['TRACK_ID'] == self.track_of_interest), self.status_name].to_numpy()[0]}\n"
1707
+ else:
1708
+ cell_status = f"phenotype: {self.df_tracks.loc[self.df_tracks['ID'] == self.track_of_interest, self.status_name].to_numpy()[0]}\n"
1709
+ self.cell_info.setText(cell_selected + cell_status)
1710
+ except Exception as e:
1711
+ print(e)
1712
+
1713
+ def create_new_event_class(self):
1714
+
1715
+ # display qwidget to name the event
1716
+ self.newClassWidget = CelldetectiveWidget()
1717
+ self.newClassWidget.setWindowTitle("Create new characteristic group")
1718
+
1719
+ layout = QVBoxLayout()
1720
+ self.newClassWidget.setLayout(layout)
1721
+ name_hbox = QHBoxLayout()
1722
+ name_hbox.addWidget(QLabel("group name: "), 25)
1723
+ self.class_name_le = QLineEdit("group")
1724
+ name_hbox.addWidget(self.class_name_le, 75)
1725
+ layout.addLayout(name_hbox)
1726
+
1727
+ btn_hbox = QHBoxLayout()
1728
+ submit_btn = QPushButton("submit")
1729
+ cancel_btn = QPushButton("cancel")
1730
+ btn_hbox.addWidget(cancel_btn, 50)
1731
+ btn_hbox.addWidget(submit_btn, 50)
1732
+ layout.addLayout(btn_hbox)
1733
+
1734
+ submit_btn.clicked.connect(self.write_new_event_class)
1735
+ cancel_btn.clicked.connect(self.close_without_new_class)
1736
+
1737
+ self.newClassWidget.show()
1738
+ center_window(self.newClassWidget)
1739
+
1740
+ def apply_modification(self):
1741
+ if self.time_of_interest_le.text() != "":
1742
+ status = int(self.time_of_interest_le.text())
1743
+ else:
1744
+ status = 0
1745
+ if "TRACK_ID" in self.df_tracks.columns:
1746
+ self.df_tracks.loc[
1747
+ (self.df_tracks["TRACK_ID"] == self.track_of_interest)
1748
+ & (self.df_tracks["FRAME"] == self.current_frame),
1749
+ self.status_name,
1750
+ ] = status
1751
+
1752
+ indices = self.df_tracks.index[
1753
+ (self.df_tracks["TRACK_ID"] == self.track_of_interest)
1754
+ & (self.df_tracks["FRAME"] == self.current_frame)
1755
+ ]
1756
+ else:
1757
+ self.df_tracks.loc[
1758
+ (self.df_tracks["ID"] == self.track_of_interest)
1759
+ & (self.df_tracks["FRAME"] == self.current_frame),
1760
+ self.status_name,
1761
+ ] = status
1762
+
1763
+ indices = self.df_tracks.index[
1764
+ (self.df_tracks["ID"] == self.track_of_interest)
1765
+ & (self.df_tracks["FRAME"] == self.current_frame)
1766
+ ]
1767
+
1768
+ self.df_tracks.loc[indices, self.status_name] = status
1769
+ all_states = self.df_tracks.loc[:, self.status_name].tolist()
1770
+ all_states = np.array(all_states)
1771
+ self.state_color_map = color_from_state(all_states, recently_modified=False)
1772
+
1773
+ self.df_tracks["group_color"] = self.df_tracks[self.status_name].apply(
1774
+ self.assign_color_state
1775
+ )
1776
+ self.extract_scatter_from_trajectories()
1777
+ self.give_cell_information()
1778
+
1779
+ self.correct_btn.disconnect()
1780
+ self.correct_btn.clicked.connect(self.show_annotation_buttons)
1781
+
1782
+ self.hide_annotation_buttons()
1783
+ self.correct_btn.setEnabled(False)
1784
+ self.correct_btn.setText("correct")
1785
+ self.cancel_btn.setEnabled(False)
1786
+ self.del_shortcut.setEnabled(False)
1787
+
1788
+ if len(self.selection) > 0:
1789
+ self.selection.pop(0)
1790
+
1791
+ self.draw_frame(self.current_frame)
1792
+ self.fcanvas.canvas.draw()
1793
+
1794
+ def assign_color_state(self, state):
1795
+
1796
+ if np.isnan(state):
1797
+ state = "nan"
1798
+ return self.state_color_map[state]
1799
+
1800
+ def draw_frame(self, framedata):
1801
+ """
1802
+ Update plot elements at each timestep of the loop.
1803
+ """
1804
+ self.framedata = framedata
1805
+ self.frame_lbl.setText(f"position: {self.framedata}")
1806
+ self.im.set_array(self.img)
1807
+ self.status_scatter.set_offsets(self.positions[self.framedata])
1808
+ # try:
1809
+ self.status_scatter.set_edgecolors(self.colors[self.framedata][:, 0])
1810
+ # except Exception as e:
1811
+ # pass
1812
+
1813
+ self.current_label = self.labels[self.current_frame]
1814
+ self.current_label = contour_of_instance_segmentation(self.current_label, 5)
1815
+
1816
+ self.im_mask.remove()
1817
+ self.im_mask = self.ax.imshow(
1818
+ np.ma.masked_where(self.current_label == 0, self.current_label),
1819
+ cmap="viridis",
1820
+ interpolation="none",
1821
+ alpha=self.current_alpha,
1822
+ vmin=0,
1823
+ vmax=np.nanmax(self.labels.flatten()),
1824
+ )
1825
+
1826
+ return (
1827
+ self.im,
1828
+ self.status_scatter,
1829
+ self.im_mask,
1830
+ )
1831
+
1832
+ def compute_status_and_colors(self):
1833
+
1834
+ self.cancel_selection()
1835
+
1836
+ if self.class_choice_cb.currentText() == "":
1837
+ pass
1838
+ else:
1839
+ self.status_name = self.class_choice_cb.currentText()
1840
+
1841
+ if self.status_name not in self.df_tracks.columns:
1842
+ print("Creating a new status for visualization...")
1843
+ self.make_status_column()
1844
+ else:
1845
+ print(f'Generating per-state colors for the status "{self.status_name}"...')
1846
+ all_states = self.df_tracks.loc[:, self.status_name].tolist()
1847
+ all_states = np.array(all_states)
1848
+ self.state_color_map = color_from_state(all_states, recently_modified=False)
1849
+ print(f'Color mapping for "{self.status_name}":')
1850
+ pretty_table(self.state_color_map)
1851
+ self.df_tracks["group_color"] = self.df_tracks[self.status_name].apply(
1852
+ self.assign_color_state
1853
+ )
1854
+
1855
+ def make_status_column(self):
1856
+ if self.status_name == "state_firstdetection":
1857
+ pass
1858
+ else:
1859
+ self.df_tracks.loc[:, self.status_name] = 0
1860
+ all_states = self.df_tracks.loc[:, self.status_name].tolist()
1861
+ all_states = np.array(all_states)
1862
+ self.state_color_map = color_from_state(all_states, recently_modified=False)
1863
+ self.df_tracks["group_color"] = self.df_tracks[self.status_name].apply(
1864
+ self.assign_color_state
1865
+ )
1866
+
1867
+ def extract_scatter_from_trajectories(self):
1868
+
1869
+ self.positions = []
1870
+ self.colors = []
1871
+ self.tracks = []
1872
+
1873
+ for t in np.arange(self.len_movie):
1874
+ self.positions.append(
1875
+ self.df_tracks.loc[
1876
+ self.df_tracks["FRAME"] == t, ["POSITION_X", "POSITION_Y"]
1877
+ ].to_numpy()
1878
+ )
1879
+ self.colors.append(
1880
+ self.df_tracks.loc[
1881
+ self.df_tracks["FRAME"] == t, ["group_color"]
1882
+ ].to_numpy()
1883
+ )
1884
+ if "TRACK_ID" in self.df_tracks.columns:
1885
+ self.tracks.append(
1886
+ self.df_tracks.loc[
1887
+ self.df_tracks["FRAME"] == t, "TRACK_ID"
1888
+ ].to_numpy()
1889
+ )
1890
+ else:
1891
+ self.tracks.append(
1892
+ self.df_tracks.loc[self.df_tracks["FRAME"] == t, "ID"].to_numpy()
1893
+ )
1894
+
1895
+ def changed_class(self):
1896
+ self.status_name = self.class_choice_cb.currentText()
1897
+ if self.status_name != "":
1898
+ self.compute_status_and_colors()
1899
+ self.modify()
1900
+ self.draw_frame(self.current_frame)
1901
+ self.fcanvas.canvas.draw()
1902
+
1903
+ def update_frame(self):
1904
+ """
1905
+ Update the displayed frame.
1906
+ """
1907
+ self.current_frame = self.frame_slider.value()
1908
+ self.reload_frame()
1909
+ if "TRACK_ID" in list(self.df_tracks.columns):
1910
+ pass
1911
+ elif "ID" in list(self.df_tracks.columns):
1912
+ print("ID in cols... change class of interest... ")
1913
+ self.track_of_interest = self.df_tracks[
1914
+ self.df_tracks["FRAME"] == self.current_frame
1915
+ ]["ID"].min()
1916
+ self.modify()
1917
+
1918
+ self.draw_frame(self.current_frame)
1919
+ self.vmin = self.contrast_slider.value()[0]
1920
+ self.vmax = self.contrast_slider.value()[1]
1921
+ self.im.set_clim(vmin=self.vmin, vmax=self.vmax)
1922
+ self.give_cell_information()
1923
+
1924
+ self.fcanvas.canvas.draw()
1925
+ self.plot_signals()
1926
+
1927
+ def changed_channel(self):
1928
+
1929
+ self.reload_frame()
1930
+ self.contrast_slider.setRange(
1931
+ *[np.nanpercentile(self.img, 0.001), np.nanpercentile(self.img, 99.999)]
1932
+ )
1933
+ self.contrast_slider.setValue(
1934
+ [np.nanpercentile(self.img, 0.1), np.nanpercentile(self.img, 99.99)]
1935
+ )
1936
+ self.draw_frame(self.current_frame)
1937
+ self.fcanvas.canvas.draw()
1938
+
1939
+ def save_trajectories(self):
1940
+ print(f"Saving trajectories !!")
1941
+ if self.normalized_signals:
1942
+ self.normalize_features_btn.click()
1943
+ if self.selection:
1944
+ self.cancel_selection()
1945
+ self.df_tracks = self.df_tracks.drop(
1946
+ self.df_tracks[self.df_tracks[self.status_name] == 99].index
1947
+ )
1948
+ # color_column = str(self.status_name) + "_color"
1949
+ try:
1950
+ self.df_tracks.drop(columns="", inplace=True)
1951
+ except:
1952
+ pass
1953
+ try:
1954
+ self.df_tracks.drop(columns="group_color", inplace=True)
1955
+ except:
1956
+ pass
1957
+ try:
1958
+ self.df_tracks.drop(columns="x_anim", inplace=True)
1959
+ except:
1960
+ pass
1961
+ try:
1962
+ self.df_tracks.drop(columns="y_anim", inplace=True)
1963
+ except:
1964
+ pass
1965
+
1966
+ self.df_tracks.to_csv(self.trajectories_path, index=False)
1967
+ print("Table successfully exported...")
1968
+
1969
+ self.locate_tracks()
1970
+ self.changed_class()
1971
+
1972
+ def modify(self):
1973
+ all_states = self.df_tracks.loc[:, self.status_name].tolist()
1974
+ all_states = np.array(all_states)
1975
+ self.state_color_map = color_from_state(all_states, recently_modified=False)
1976
+
1977
+ self.df_tracks["group_color"] = self.df_tracks[self.status_name].apply(
1978
+ self.assign_color_state
1979
+ )
1980
+
1981
+ self.extract_scatter_from_trajectories()
1982
+ self.give_cell_information()
1983
+
1984
+ self.correct_btn.disconnect()
1985
+ self.correct_btn.clicked.connect(self.show_annotation_buttons)
1986
+
1987
+ def reload_frame(self):
1988
+ """
1989
+ Load the frame from the current channel and time choice. Show imshow, update histogram.
1990
+ """
1991
+
1992
+ # self.clear_post_threshold_options()
1993
+ self.previous_channel = self.current_channel
1994
+ self.current_channel = self.choose_channel.currentIndex()
1995
+
1996
+ t = int(self.frame_slider.value())
1997
+ idx = t * self.nbr_channels + self.current_channel
1998
+ self.img = load_frames(idx, self.stack_path, normalize_input=False)
1999
+
2000
+ if self.previous_channel != self.current_channel:
2001
+ # reinitialize intensity bounds
2002
+ epsilon = 0.01
2003
+ self.observed_min_intensity = 0
2004
+ self.observed_max_intensity = 0 + epsilon
2005
+
2006
+ if self.img is not None:
2007
+ max_img = np.nanmax(self.img)
2008
+ min_img = np.nanmin(self.img)
2009
+ if max_img > self.observed_max_intensity:
2010
+ self.observed_max_intensity = max_img
2011
+ if min_img < self.observed_min_intensity:
2012
+ self.observed_min_intensity = min_img
2013
+ self.refresh_imshow()
2014
+ # self.redo_histogram()
2015
+ else:
2016
+ print("Frame could not be loaded...")
2017
+
2018
+ def refresh_imshow(self):
2019
+ """
2020
+
2021
+ Update the imshow based on the current frame selection.
2022
+
2023
+ """
2024
+
2025
+ if self.previous_channel != self.current_channel:
2026
+
2027
+ self.vmin = np.nanpercentile(self.img.flatten(), 0.1)
2028
+ self.vmax = np.nanpercentile(self.img.flatten(), 99.99)
2029
+
2030
+ self.contrast_slider.disconnect()
2031
+ self.contrast_slider.setRange(np.nanmin(self.img), np.nanmax(self.img))
2032
+ self.contrast_slider.setValue([self.vmin, self.vmax])
2033
+ self.contrast_slider.valueChanged.connect(self.contrast_slider_action)
2034
+ else:
2035
+ # self.contrast_slider.disconnect()
2036
+ self.contrast_slider.setRange(
2037
+ self.observed_min_intensity, self.observed_max_intensity
2038
+ )
2039
+ # self.contrast_slider.valueChanged.connect(self.contrast_slider_action)
2040
+
2041
+ self.im.set_data(self.img)
2042
+
2043
+ def del_cell(self):
2044
+ self.time_of_interest_le.setEnabled(False)
2045
+ self.time_of_interest_le.setText("99")
2046
+ self.apply_modification()