celldetective 1.3.9.post5__py3-none-any.whl → 1.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. celldetective/__init__.py +0 -3
  2. celldetective/_version.py +1 -1
  3. celldetective/events.py +2 -4
  4. celldetective/exceptions.py +11 -0
  5. celldetective/extra_properties.py +132 -0
  6. celldetective/filters.py +7 -1
  7. celldetective/gui/InitWindow.py +37 -46
  8. celldetective/gui/__init__.py +3 -9
  9. celldetective/gui/about.py +19 -15
  10. celldetective/gui/analyze_block.py +34 -19
  11. celldetective/gui/base_annotator.py +786 -0
  12. celldetective/gui/base_components.py +23 -0
  13. celldetective/gui/classifier_widget.py +86 -94
  14. celldetective/gui/configure_new_exp.py +163 -46
  15. celldetective/gui/control_panel.py +76 -146
  16. celldetective/gui/{signal_annotator.py → event_annotator.py} +533 -1438
  17. celldetective/gui/generic_signal_plot.py +11 -13
  18. celldetective/gui/gui_utils.py +54 -23
  19. celldetective/gui/help/neighborhood.json +2 -2
  20. celldetective/gui/json_readers.py +5 -4
  21. celldetective/gui/layouts.py +265 -31
  22. celldetective/gui/{signal_annotator2.py → pair_event_annotator.py} +433 -635
  23. celldetective/gui/plot_measurements.py +21 -17
  24. celldetective/gui/plot_signals_ui.py +125 -72
  25. celldetective/gui/process_block.py +283 -188
  26. celldetective/gui/processes/compute_neighborhood.py +594 -0
  27. celldetective/gui/processes/downloader.py +37 -34
  28. celldetective/gui/processes/measure_cells.py +19 -8
  29. celldetective/gui/processes/segment_cells.py +47 -11
  30. celldetective/gui/processes/track_cells.py +18 -13
  31. celldetective/gui/seg_model_loader.py +21 -62
  32. celldetective/gui/settings/__init__.py +7 -0
  33. celldetective/gui/settings/_settings_base.py +70 -0
  34. celldetective/gui/{retrain_signal_model_options.py → settings/_settings_event_model_training.py} +54 -109
  35. celldetective/gui/{measurement_options.py → settings/_settings_measurements.py} +54 -92
  36. celldetective/gui/{neighborhood_options.py → settings/_settings_neighborhood.py} +10 -13
  37. celldetective/gui/settings/_settings_segmentation.py +49 -0
  38. celldetective/gui/{retrain_segmentation_model_options.py → settings/_settings_segmentation_model_training.py} +38 -92
  39. celldetective/gui/{signal_annotator_options.py → settings/_settings_signal_annotator.py} +78 -103
  40. celldetective/gui/{btrack_options.py → settings/_settings_tracking.py} +85 -116
  41. celldetective/gui/styles.py +2 -1
  42. celldetective/gui/survival_ui.py +49 -95
  43. celldetective/gui/tableUI.py +53 -25
  44. celldetective/gui/table_ops/__init__.py +0 -0
  45. celldetective/gui/table_ops/merge_groups.py +118 -0
  46. celldetective/gui/thresholds_gui.py +617 -1221
  47. celldetective/gui/viewers.py +107 -42
  48. celldetective/gui/workers.py +8 -4
  49. celldetective/io.py +137 -57
  50. celldetective/links/zenodo.json +145 -144
  51. celldetective/measure.py +94 -53
  52. celldetective/neighborhood.py +342 -268
  53. celldetective/preprocessing.py +56 -35
  54. celldetective/regionprops/_regionprops.py +16 -5
  55. celldetective/relative_measurements.py +50 -29
  56. celldetective/scripts/analyze_signals.py +4 -1
  57. celldetective/scripts/measure_cells.py +5 -5
  58. celldetective/scripts/measure_relative.py +20 -12
  59. celldetective/scripts/segment_cells.py +4 -10
  60. celldetective/scripts/segment_cells_thresholds.py +3 -3
  61. celldetective/scripts/track_cells.py +10 -8
  62. celldetective/scripts/train_segmentation_model.py +18 -6
  63. celldetective/signals.py +29 -14
  64. celldetective/tracking.py +14 -3
  65. celldetective/utils.py +91 -62
  66. {celldetective-1.3.9.post5.dist-info → celldetective-1.4.1.dist-info}/METADATA +24 -16
  67. celldetective-1.4.1.dist-info/RECORD +123 -0
  68. {celldetective-1.3.9.post5.dist-info → celldetective-1.4.1.dist-info}/WHEEL +1 -1
  69. tests/gui/__init__.py +0 -0
  70. tests/gui/test_new_project.py +228 -0
  71. tests/gui/test_project.py +99 -0
  72. tests/test_preprocessing.py +2 -2
  73. celldetective/models/segmentation_effectors/ricm_bf_all_last/config_input.json +0 -79
  74. celldetective/models/segmentation_effectors/ricm_bf_all_last/ricm_bf_all_last +0 -0
  75. celldetective/models/segmentation_effectors/ricm_bf_all_last/training_instructions.json +0 -37
  76. celldetective/models/segmentation_effectors/test-transfer/config_input.json +0 -39
  77. celldetective/models/segmentation_effectors/test-transfer/test-transfer +0 -0
  78. celldetective/models/signal_detection/NucCond/classification_loss.png +0 -0
  79. celldetective/models/signal_detection/NucCond/classifier.h5 +0 -0
  80. celldetective/models/signal_detection/NucCond/config_input.json +0 -1
  81. celldetective/models/signal_detection/NucCond/log_classifier.csv +0 -126
  82. celldetective/models/signal_detection/NucCond/log_regressor.csv +0 -282
  83. celldetective/models/signal_detection/NucCond/regression_loss.png +0 -0
  84. celldetective/models/signal_detection/NucCond/regressor.h5 +0 -0
  85. celldetective/models/signal_detection/NucCond/scores.npy +0 -0
  86. celldetective/models/signal_detection/NucCond/test_confusion_matrix.png +0 -0
  87. celldetective/models/signal_detection/NucCond/test_regression.png +0 -0
  88. celldetective/models/signal_detection/NucCond/validation_confusion_matrix.png +0 -0
  89. celldetective/models/signal_detection/NucCond/validation_regression.png +0 -0
  90. celldetective-1.3.9.post5.dist-info/RECORD +0 -129
  91. tests/test_qt.py +0 -103
  92. {celldetective-1.3.9.post5.dist-info → celldetective-1.4.1.dist-info}/entry_points.txt +0 -0
  93. {celldetective-1.3.9.post5.dist-info → celldetective-1.4.1.dist-info/licenses}/LICENSE +0 -0
  94. {celldetective-1.3.9.post5.dist-info → celldetective-1.4.1.dist-info}/top_level.txt +0 -0
@@ -1,217 +1,126 @@
1
- from PyQt5.QtWidgets import QMainWindow, QComboBox, QLabel, QRadioButton, QLineEdit, QFileDialog, QApplication, \
2
- QPushButton, QWidget, QVBoxLayout, QHBoxLayout, QMessageBox, QShortcut, QLineEdit, QSlider, QCheckBox
1
+ from PyQt5.QtWidgets import QComboBox, QLabel, QRadioButton, QFileDialog, QApplication, \
2
+ QPushButton, QVBoxLayout, QHBoxLayout, QMessageBox, QShortcut, QLineEdit, QSlider
3
3
  from PyQt5.QtCore import Qt, QSize
4
4
  from PyQt5.QtGui import QKeySequence, QIntValidator
5
5
 
6
6
  from celldetective.gui.gui_utils import center_window, color_from_state
7
7
  from superqt import QLabeledDoubleSlider, QLabeledDoubleRangeSlider, QSearchableComboBox
8
- from celldetective.utils import extract_experiment_channels, get_software_location, _get_img_num_per_channel
9
- from celldetective.io import auto_load_number_of_frames, load_frames, \
10
- load_napari_data, get_experiment_metadata, get_experiment_labels
11
- from celldetective.gui.gui_utils import FigureCanvas, color_from_status, color_from_class, ExportPlotBtn
12
- import json
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
13
11
  import numpy as np
14
12
  from superqt.fonticon import icon
15
13
  from fonticon_mdi6 import MDI6
16
14
  import os
17
- from glob import glob
18
15
  import matplotlib.pyplot as plt
19
- from matplotlib.ticker import MultipleLocator
20
16
  from tqdm import tqdm
21
17
  import gc
22
18
  from matplotlib.animation import FuncAnimation
23
19
  from matplotlib.cm import tab10
24
20
  import pandas as pd
25
21
  from sklearn.preprocessing import MinMaxScaler
26
- from celldetective.gui import Styles
22
+ from celldetective.gui import CelldetectiveWidget
27
23
  from celldetective.measure import contour_of_instance_segmentation
24
+ from celldetective.utils import pretty_table
25
+ from celldetective.gui.base_annotator import BaseAnnotator
28
26
 
29
- class SignalAnnotator(QMainWindow, Styles):
27
+ class EventAnnotator(BaseAnnotator):
30
28
  """
31
29
  UI to set tracking parameters for bTrack.
32
30
 
33
31
  """
34
32
 
35
- def __init__(self, parent_window=None):
33
+ def __init__(self, *args, **kwargs):
36
34
 
37
- super().__init__()
38
-
39
- center_window(self)
40
- self.proceed = True
41
- self.setAttribute(Qt.WA_DeleteOnClose)
42
-
43
- self.parent_window = parent_window
35
+ super().__init__(*args, **kwargs)
44
36
  self.setWindowTitle("Signal annotator")
45
- self.mode = self.parent_window.mode
46
- self.pos = self.parent_window.parent_window.pos
47
- self.exp_dir = self.parent_window.exp_dir
48
- self.PxToUm = self.parent_window.parent_window.PxToUm
49
- self.n_signals = 3
50
- self.soft_path = get_software_location()
51
- self.recently_modified = False
52
- self.selection = []
53
-
54
- if self.mode == "targets":
55
- self.instructions_path = self.exp_dir + os.sep.join(['configs', 'signal_annotator_config_targets.json'])
56
- self.trajectories_path = self.pos + os.sep.join(['output','tables','trajectories_targets.csv'])
57
- elif self.mode == "effectors":
58
- self.instructions_path = self.exp_dir + os.sep.join(['configs', 'signal_annotator_config_effectors.json'])
59
- self.trajectories_path = self.pos + os.sep.join(['output','tables','trajectories_effectors.csv'])
60
-
61
- self.screen_height = self.parent_window.parent_window.parent_window.screen_height
62
- self.screen_width = self.parent_window.parent_window.parent_window.screen_width
63
- #self.setMinimumHeight(int(0.8*self.screen_height))
64
- self.value_magnitude = 1
65
37
 
66
38
  # default params
67
39
  self.class_name = 'class'
68
40
  self.time_name = 't0'
69
41
  self.status_name = 'status'
70
42
 
71
- self.locate_stack()
43
+ # self.locate_stack()
72
44
  if not self.proceed:
73
45
  self.close()
74
46
  else:
75
- self.load_annotator_config()
76
- self.locate_tracks()
47
+ #self.load_annotator_config()
48
+ #self.locate_tracks()
77
49
  self.prepare_stack()
78
50
 
79
- self.generate_signal_choices()
80
51
  self.frame_lbl = QLabel('frame: ')
81
52
  self.looped_animation()
82
- self.create_cell_signal_canvas()
83
-
84
- self.populate_widget()
85
-
86
- def resizeEvent(self, event):
87
-
88
- super().resizeEvent(event)
89
-
90
- try:
91
- self.cell_fig.tight_layout()
92
- except:
93
- pass
94
-
95
- def populate_widget(self):
96
-
97
- """
98
- Create the multibox design.
99
-
100
- """
101
-
102
- self.button_widget = QWidget()
103
- main_layout = QHBoxLayout()
104
- self.button_widget.setLayout(main_layout)
105
-
106
- main_layout.setContentsMargins(30, 30, 30, 30)
107
- self.left_panel = QVBoxLayout()
108
- self.left_panel.setContentsMargins(30, 5, 30, 5)
109
- self.left_panel.setSpacing(3)
110
-
111
- self.right_panel = QVBoxLayout()
112
-
113
- class_hbox = QHBoxLayout()
114
- class_hbox.setContentsMargins(0,0,0,0)
115
- class_hbox.addWidget(QLabel('event: '), 25)
116
- self.class_choice_cb = QComboBox()
117
-
118
- cols = np.array(self.df_tracks.columns)
119
- self.class_cols = np.array([c.startswith('class') for c in list(self.df_tracks.columns)])
120
- self.class_cols = list(cols[self.class_cols])
121
- try:
122
- self.class_cols.remove('class_id')
123
- except Exception:
124
- pass
125
- try:
126
- self.class_cols.remove('class_color')
127
- except Exception:
128
- pass
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])
129
59
 
130
- self.class_choice_cb.addItems(self.class_cols)
131
- self.class_choice_cb.currentIndexChanged.connect(self.compute_status_and_colors)
132
-
133
- class_hbox.addWidget(self.class_choice_cb, 70)
134
-
135
- self.add_class_btn = QPushButton('')
136
- self.add_class_btn.setStyleSheet(self.button_select_all)
137
- self.add_class_btn.setIcon(icon(MDI6.plus, color="black"))
138
- self.add_class_btn.setToolTip("Add a new event class")
139
- self.add_class_btn.setIconSize(QSize(20, 20))
140
- self.add_class_btn.clicked.connect(self.create_new_event_class)
141
- class_hbox.addWidget(self.add_class_btn, 5)
142
-
143
- self.del_class_btn = QPushButton('')
144
- self.del_class_btn.setStyleSheet(self.button_select_all)
145
- self.del_class_btn.setIcon(icon(MDI6.delete, color="black"))
146
- self.del_class_btn.setToolTip("Delete an event class")
147
- self.del_class_btn.setIconSize(QSize(20, 20))
148
- self.del_class_btn.clicked.connect(self.del_event_class)
149
- class_hbox.addWidget(self.del_class_btn, 5)
150
-
151
- self.left_panel.addLayout(class_hbox,5)
152
-
153
- self.cell_info = QLabel('')
154
- self.left_panel.addWidget(self.cell_info,10)
155
-
156
- # Annotation buttons
157
- options_hbox = QHBoxLayout()
158
- options_hbox.setContentsMargins(0, 0, 0, 0)
60
+ def init_event_buttons(self):
61
+
159
62
  self.event_btn = QRadioButton('event')
160
63
  self.event_btn.setStyleSheet(self.button_style_sheet_2)
161
64
  self.event_btn.toggled.connect(self.enable_time_of_interest)
162
-
65
+
163
66
  self.no_event_btn = QRadioButton('no event')
164
67
  self.no_event_btn.setStyleSheet(self.button_style_sheet_2)
165
68
  self.no_event_btn.toggled.connect(self.enable_time_of_interest)
166
-
69
+
167
70
  self.else_btn = QRadioButton('else')
168
71
  self.else_btn.setStyleSheet(self.button_style_sheet_2)
169
72
  self.else_btn.toggled.connect(self.enable_time_of_interest)
170
-
73
+
171
74
  self.suppr_btn = QRadioButton('remove')
172
75
  self.suppr_btn.setToolTip('Mark for deletion. Upon saving, the cell\nwill be removed from the tables.')
173
76
  self.suppr_btn.setStyleSheet(self.button_style_sheet_2)
174
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()
175
81
 
176
- options_hbox.addWidget(self.event_btn, 25, alignment=Qt.AlignCenter)
177
- options_hbox.addWidget(self.no_event_btn, 25, alignment=Qt.AlignCenter)
178
- options_hbox.addWidget(self.else_btn, 25, alignment=Qt.AlignCenter)
179
- options_hbox.addWidget(self.suppr_btn, 25, alignment=Qt.AlignCenter)
180
- self.left_panel.addLayout(options_hbox,5)
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
181
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
+
182
98
  time_option_hbox = QHBoxLayout()
183
99
  time_option_hbox.setContentsMargins(0, 5, 100, 10)
184
- self.time_of_interest_label = QLabel('time of interest: ')
185
100
  time_option_hbox.addWidget(self.time_of_interest_label, 10)
186
- self.time_of_interest_le = QLineEdit()
187
101
  time_option_hbox.addWidget(self.time_of_interest_le, 15)
188
102
  time_option_hbox.addWidget(QLabel(''), 75)
189
- self.left_panel.addLayout(time_option_hbox,5)
190
-
191
- main_action_hbox = QHBoxLayout()
192
- main_action_hbox.setContentsMargins(0,0,0,0)
193
- self.correct_btn = QPushButton('correct')
194
- self.correct_btn.setIcon(icon(MDI6.redo_variant, color="white"))
195
- self.correct_btn.setIconSize(QSize(20, 20))
196
- self.correct_btn.setStyleSheet(self.button_style_sheet)
197
- self.correct_btn.clicked.connect(self.show_annotation_buttons)
198
- self.correct_btn.setEnabled(False)
199
- main_action_hbox.addWidget(self.correct_btn)
200
-
201
- self.cancel_btn = QPushButton('cancel')
202
- self.cancel_btn.setStyleSheet(self.button_style_sheet_2)
203
- self.cancel_btn.setShortcut(QKeySequence("Esc"))
204
- self.cancel_btn.setEnabled(False)
205
- self.cancel_btn.clicked.connect(self.cancel_selection)
206
- main_action_hbox.addWidget(self.cancel_btn)
207
- self.left_panel.addLayout(main_action_hbox,5)
208
-
103
+
104
+ options_layout.addLayout(btn_hbox)
105
+ options_layout.addLayout(time_option_hbox)
106
+
107
+ self.options_hbox.addLayout(options_layout)
108
+
209
109
  self.annotation_btns_to_hide = [self.event_btn, self.no_event_btn,
210
110
  self.else_btn, self.time_of_interest_label,
211
111
  self.time_of_interest_le, self.suppr_btn]
212
112
  self.hide_annotation_buttons()
213
- #### End of annotation buttons
113
+
114
+ def populate_window(self):
115
+
116
+ """
117
+ Create the multibox design.
214
118
 
119
+ """
120
+
121
+ super().populate_window()
122
+ self.populate_options_layout()
123
+
215
124
  self.del_shortcut = QShortcut(Qt.Key_Delete, self) # QKeySequence("s")
216
125
  self.del_shortcut.activated.connect(self.shortcut_suppr)
217
126
  self.del_shortcut.setEnabled(False)
@@ -220,68 +129,8 @@ class SignalAnnotator(QMainWindow, Styles):
220
129
  self.no_event_shortcut.activated.connect(self.shortcut_no_event)
221
130
  self.no_event_shortcut.setEnabled(False)
222
131
 
223
- # Cell signals
224
-
225
- self.left_panel.addWidget(self.cell_fcanvas, 45)
226
-
227
- plot_buttons_hbox = QHBoxLayout()
228
- plot_buttons_hbox.setContentsMargins(0, 0, 0, 0)
229
- self.normalize_features_btn = QPushButton('')
230
- self.normalize_features_btn.setStyleSheet(self.button_select_all)
231
- self.normalize_features_btn.setIcon(icon(MDI6.arrow_collapse_vertical, color="black"))
232
- self.normalize_features_btn.setIconSize(QSize(25, 25))
233
- self.normalize_features_btn.setFixedSize(QSize(30, 30))
234
- # self.normalize_features_btn.setShortcut(QKeySequence('n'))
235
- self.normalize_features_btn.clicked.connect(self.normalize_features)
236
-
237
- plot_buttons_hbox.addWidget(QLabel(''), 90)
238
- plot_buttons_hbox.addWidget(self.normalize_features_btn, 5)
239
- self.normalized_signals = False
240
-
241
- self.log_btn = QPushButton()
242
- self.log_btn.setIcon(icon(MDI6.math_log, color="black"))
243
- self.log_btn.setStyleSheet(self.button_select_all)
244
- self.log_btn.clicked.connect(self.switch_to_log)
245
- plot_buttons_hbox.addWidget(self.log_btn, 5)
246
-
247
- self.export_plot_btn = ExportPlotBtn(self.cell_fig, export_dir = self.exp_dir)
248
- plot_buttons_hbox.addWidget(self.export_plot_btn, 5)
249
-
250
- self.left_panel.addLayout(plot_buttons_hbox,5)
251
-
252
- signal_choice_vbox = QVBoxLayout()
253
- signal_choice_vbox.setContentsMargins(30, 0, 30, 0)
254
- for i in range(len(self.signal_choice_cb)):
255
- hlayout = QHBoxLayout()
256
- hlayout.addWidget(self.signal_choice_label[i], 20)
257
- hlayout.addWidget(self.signal_choice_cb[i], 75)
258
- # hlayout.addWidget(self.log_btns[i], 5)
259
- signal_choice_vbox.addLayout(hlayout)
260
-
261
- # self.log_btns[i].setIcon(icon(MDI6.math_log,color="black"))
262
- # self.log_btns[i].setStyleSheet(self.parent.parent.parent.button_select_all)
263
- # self.log_btns[i].clicked.connect(lambda ch, i=i: self.switch_to_log(i))
264
-
265
- self.left_panel.addLayout(signal_choice_vbox,15)
266
-
267
- btn_hbox = QHBoxLayout()
268
- btn_hbox.setContentsMargins(0,10,0,0)
269
- self.save_btn = QPushButton('Save')
270
- self.save_btn.setStyleSheet(self.button_style_sheet)
271
- self.save_btn.clicked.connect(self.save_trajectories)
272
- btn_hbox.addWidget(self.save_btn, 90)
273
-
274
- self.export_btn = QPushButton('')
275
- self.export_btn.setStyleSheet(self.button_select_all)
276
- self.export_btn.clicked.connect(self.export_signals)
277
- self.export_btn.setIcon(icon(MDI6.export, color="black"))
278
- self.export_btn.setIconSize(QSize(25, 25))
279
- btn_hbox.addWidget(self.export_btn, 10)
280
- self.left_panel.addLayout(btn_hbox,5)
281
-
282
- # Animation
132
+ # Right side
283
133
  animation_buttons_box = QHBoxLayout()
284
-
285
134
  animation_buttons_box.addWidget(self.frame_lbl, 20, alignment=Qt.AlignLeft)
286
135
 
287
136
  self.first_frame_btn = QPushButton()
@@ -321,7 +170,6 @@ class SignalAnnotator(QMainWindow, Styles):
321
170
  animation_buttons_box.addWidget(self.last_frame_btn, 5, alignment=Qt.AlignRight)
322
171
 
323
172
  self.right_panel.addLayout(animation_buttons_box, 5)
324
-
325
173
  self.right_panel.addWidget(self.fcanvas, 90)
326
174
 
327
175
  if not self.rgb_mode:
@@ -330,7 +178,7 @@ class SignalAnnotator(QMainWindow, Styles):
330
178
  self.contrast_slider = QLabeledDoubleRangeSlider()
331
179
  self.contrast_slider.setSingleStep(0.001)
332
180
  self.contrast_slider.setTickInterval(0.001)
333
- self.contrast_slider.setOrientation(1)
181
+ self.contrast_slider.setOrientation(Qt.Horizontal)
334
182
  self.contrast_slider.setRange(
335
183
  *[np.nanpercentile(self.stack, 0.001), np.nanpercentile(self.stack, 99.999)])
336
184
  self.contrast_slider.setValue(
@@ -339,99 +187,16 @@ class SignalAnnotator(QMainWindow, Styles):
339
187
  contrast_hbox.addWidget(QLabel('contrast: '))
340
188
  contrast_hbox.addWidget(self.contrast_slider, 90)
341
189
  self.right_panel.addLayout(contrast_hbox, 5)
342
-
343
- # speed_hbox = QHBoxLayout()
344
- # speed_hbox.setContentsMargins(150,5,150,5)
345
- # self.interval_slider = QLabeledSlider()
346
- # self.interval_slider.setSingleStep(1)
347
- # self.interval_slider.setTickInterval(1)
348
- # self.interval_slider.setOrientation(1)
349
- # self.interval_slider.setRange(1, 10000)
350
- # self.interval_slider.setValue(self.speed)
351
- # self.interval_slider.valueChanged.connect(self.interval_slider_action)
352
- # speed_hbox.addWidget(QLabel('interval (ms): '))
353
- # speed_hbox.addWidget(self.interval_slider,90)
354
- # self.right_panel.addLayout(speed_hbox, 10)
355
-
356
- # self.populate_left_panel()
357
- # grid.addLayout(self.left_side, 0, 0, 1, 1)
358
-
359
- main_layout.addLayout(self.left_panel, 35)
360
- main_layout.addLayout(self.right_panel, 65)
361
- self.button_widget.adjustSize()
362
-
363
- self.compute_status_and_colors(0)
364
-
365
- self.setCentralWidget(self.button_widget)
366
- self.show()
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)
367
197
 
368
198
  QApplication.processEvents()
369
199
 
370
- def del_event_class(self):
371
-
372
- msgBox = QMessageBox()
373
- msgBox.setIcon(QMessageBox.Warning)
374
- msgBox.setText(
375
- f"You are about to delete event class {self.class_choice_cb.currentText()}. The associated time and\nstatus will also be deleted. Do you still want to proceed?")
376
- msgBox.setWindowTitle("Warning")
377
- msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
378
- returnValue = msgBox.exec()
379
- if returnValue == QMessageBox.No:
380
- return None
381
- else:
382
- class_to_delete = self.class_choice_cb.currentText()
383
- time_to_delete = class_to_delete.replace('class', 't')
384
- status_to_delete = class_to_delete.replace('class', 'status')
385
- cols_to_delete = [class_to_delete, time_to_delete, status_to_delete]
386
- for c in cols_to_delete:
387
- try:
388
- self.df_tracks = self.df_tracks.drop([c], axis=1)
389
- except Exception as e:
390
- print(e)
391
- item_idx = self.class_choice_cb.findText(class_to_delete)
392
- self.class_choice_cb.removeItem(item_idx)
393
-
394
- def create_new_event_class(self):
395
-
396
- # display qwidget to name the event
397
- self.newClassWidget = QWidget()
398
- self.newClassWidget.setWindowTitle('Create new event class')
399
-
400
- layout = QVBoxLayout()
401
- self.newClassWidget.setLayout(layout)
402
- name_hbox = QHBoxLayout()
403
- name_hbox.addWidget(QLabel('event name: '), 25)
404
- self.class_name_le = QLineEdit('event')
405
- name_hbox.addWidget(self.class_name_le, 75)
406
- layout.addLayout(name_hbox)
407
-
408
- class_labels = ['event', 'no event', 'else']
409
- layout.addWidget(QLabel('prefill: '))
410
- radio_box = QHBoxLayout()
411
- self.class_option_rb = [QRadioButton() for i in range(3)]
412
- for i, c in enumerate(self.class_option_rb):
413
- if i == 0:
414
- c.setChecked(True)
415
- c.setText(class_labels[i])
416
- radio_box.addWidget(c, 33, alignment=Qt.AlignCenter)
417
- layout.addLayout(radio_box)
418
-
419
- btn_hbox = QHBoxLayout()
420
- submit_btn = QPushButton('submit')
421
- cancel_btn = QPushButton('cancel')
422
- btn_hbox.addWidget(cancel_btn, 50)
423
- btn_hbox.addWidget(submit_btn, 50)
424
- layout.addLayout(btn_hbox)
425
-
426
- submit_btn.clicked.connect(self.write_new_event_class)
427
- cancel_btn.clicked.connect(self.close_without_new_class)
428
-
429
- self.newClassWidget.show()
430
- center_window(self.newClassWidget)
431
-
432
- # Prefill with class value
433
- # write in table
434
-
435
200
  def write_new_event_class(self):
436
201
 
437
202
  if self.class_name_le.text() == '':
@@ -476,8 +241,8 @@ class SignalAnnotator(QMainWindow, Styles):
476
241
 
477
242
  self.newClassWidget.close()
478
243
 
479
- def close_without_new_class(self):
480
- self.newClassWidget.close()
244
+ # def close_without_new_class(self):
245
+ # self.newClassWidget.close()
481
246
 
482
247
  def compute_status_and_colors(self, i):
483
248
 
@@ -522,36 +287,14 @@ class SignalAnnotator(QMainWindow, Styles):
522
287
 
523
288
  self.fcanvas.canvas.draw()
524
289
 
525
-
526
- def contrast_slider_action(self):
527
-
528
- """
529
- Recontrast the imshow as the contrast slider is moved.
530
- """
531
-
532
- self.vmin = self.contrast_slider.value()[0]
533
- self.vmax = self.contrast_slider.value()[1]
534
- self.im.set_clim(vmin=self.vmin, vmax=self.vmax)
535
- self.fcanvas.canvas.draw_idle()
536
-
537
290
  def cancel_selection(self):
538
-
539
- self.hide_annotation_buttons()
540
- self.correct_btn.setEnabled(False)
541
- self.correct_btn.setText('correct')
542
- self.cancel_btn.setEnabled(False)
543
-
544
- try:
545
- self.selection.pop(0)
546
- except Exception as e:
547
- print(f"L 536 {e=}")
548
-
291
+
292
+ super().cancel_selection()
549
293
  try:
550
294
  for k, (t, idx) in enumerate(zip(self.loc_t, self.loc_idx)):
551
- self.colors[t][idx, 0] = self.previous_color[k][0]
552
295
  self.colors[t][idx, 1] = self.previous_color[k][1]
553
296
  except Exception as e:
554
- print(f'L 543 {e=}')
297
+ pass
555
298
 
556
299
  def hide_annotation_buttons(self):
557
300
 
@@ -657,167 +400,6 @@ class SignalAnnotator(QMainWindow, Styles):
657
400
 
658
401
  self.selection.pop(0)
659
402
 
660
- # self.fcanvas.canvas.draw()
661
-
662
- def locate_stack(self):
663
-
664
- """
665
- Locate the target movie.
666
-
667
- """
668
-
669
- movies = glob(self.pos + os.sep.join(["movie", f"{self.parent_window.parent_window.movie_prefix}*.tif"]))
670
-
671
- if len(movies) == 0:
672
- msgBox = QMessageBox()
673
- msgBox.setIcon(QMessageBox.Warning)
674
- msgBox.setText("No movie is detected in the experiment folder.\nPlease check the stack prefix...")
675
- msgBox.setWindowTitle("Warning")
676
- msgBox.setStandardButtons(QMessageBox.Ok)
677
- returnValue = msgBox.exec()
678
- if returnValue == QMessageBox.Ok:
679
- self.proceed = False
680
- self.close()
681
- else:
682
- self.close()
683
- else:
684
- self.stack_path = movies[0]
685
- self.len_movie = self.parent_window.parent_window.len_movie
686
- len_movie_auto = auto_load_number_of_frames(self.stack_path)
687
- if len_movie_auto is not None:
688
- self.len_movie = len_movie_auto
689
- exp_config = self.exp_dir + "config.ini"
690
- self.channel_names, self.channels = extract_experiment_channels(self.exp_dir)
691
- self.channel_names = np.array(self.channel_names)
692
- self.channels = np.array(self.channels)
693
- self.nbr_channels = len(self.channels)
694
-
695
- def locate_tracks(self):
696
-
697
- """
698
- Locate the tracks.
699
- """
700
-
701
- if not os.path.exists(self.trajectories_path):
702
-
703
- msgBox = QMessageBox()
704
- msgBox.setIcon(QMessageBox.Warning)
705
- msgBox.setText("The trajectories cannot be detected.")
706
- msgBox.setWindowTitle("Warning")
707
- msgBox.setStandardButtons(QMessageBox.Ok)
708
- returnValue = msgBox.exec()
709
- if returnValue == QMessageBox.Yes:
710
- self.close()
711
- else:
712
-
713
- # Load and prep tracks
714
- self.df_tracks = pd.read_csv(self.trajectories_path)
715
- self.df_tracks = self.df_tracks.sort_values(by=['TRACK_ID', 'FRAME'])
716
-
717
- cols = np.array(self.df_tracks.columns)
718
- self.class_cols = np.array([c.startswith('class') for c in list(self.df_tracks.columns)])
719
- self.class_cols = list(cols[self.class_cols])
720
- try:
721
- self.class_cols.remove('class_id')
722
- except:
723
- pass
724
- try:
725
- self.class_cols.remove('class_color')
726
- except:
727
- pass
728
- if len(self.class_cols) > 0:
729
- self.class_name = self.class_cols[0]
730
- self.expected_status = 'status'
731
- suffix = self.class_name.replace('class', '').replace('_', '')
732
- if suffix != '':
733
- self.expected_status += '_' + suffix
734
- self.expected_time = 't_' + suffix
735
- else:
736
- self.expected_time = 't0'
737
- self.time_name = self.expected_time
738
- self.status_name = self.expected_status
739
- else:
740
- self.class_name = 'class'
741
- self.time_name = 't0'
742
- self.status_name = 'status'
743
-
744
- if self.time_name in self.df_tracks.columns and self.class_name in self.df_tracks.columns and not self.status_name in self.df_tracks.columns:
745
- # only create the status column if it does not exist to not erase static classification results
746
- self.make_status_column()
747
- elif self.time_name in self.df_tracks.columns and self.class_name in self.df_tracks.columns:
748
- # all good, do nothing
749
- pass
750
- else:
751
- if not self.status_name in self.df_tracks.columns:
752
- self.df_tracks[self.status_name] = 0
753
- self.df_tracks['status_color'] = color_from_status(0)
754
- self.df_tracks['class_color'] = color_from_class(1)
755
-
756
- if not self.class_name in self.df_tracks.columns:
757
- self.df_tracks[self.class_name] = 1
758
- if not self.time_name in self.df_tracks.columns:
759
- self.df_tracks[self.time_name] = -1
760
-
761
- self.df_tracks['status_color'] = [color_from_status(i) for i in self.df_tracks[self.status_name].to_numpy()]
762
- self.df_tracks['class_color'] = [color_from_class(i) for i in self.df_tracks[self.class_name].to_numpy()]
763
-
764
- self.df_tracks = self.df_tracks.dropna(subset=['POSITION_X', 'POSITION_Y'])
765
- self.df_tracks['x_anim'] = self.df_tracks['POSITION_X'] * self.fraction
766
- self.df_tracks['y_anim'] = self.df_tracks['POSITION_Y'] * self.fraction
767
- self.df_tracks['x_anim'] = self.df_tracks['x_anim'].astype(int)
768
- self.df_tracks['y_anim'] = self.df_tracks['y_anim'].astype(int)
769
-
770
- self.extract_scatter_from_trajectories()
771
- self.track_of_interest = self.df_tracks['TRACK_ID'].min()
772
-
773
- self.loc_t = []
774
- self.loc_idx = []
775
- for t in range(len(self.tracks)):
776
- indices = np.where(self.tracks[t] == self.track_of_interest)[0]
777
- if len(indices) > 0:
778
- self.loc_t.append(t)
779
- self.loc_idx.append(indices[0])
780
-
781
- self.MinMaxScaler = MinMaxScaler()
782
- self.columns_to_rescale = list(self.df_tracks.columns)
783
- #self.columns_to_rescale = self.df_tracks.select_dtypes(exclude=['object']).columns
784
-
785
- # is_number = np.vectorize(lambda x: np.issubdtype(x, np.number))
786
- # is_number_test = is_number(self.df_tracks.dtypes)
787
- # self.columns_to_rescale = [col for t,col in zip(is_number_test,self.df_tracks.columns) if t]
788
- # print(self.columns_to_rescale)
789
-
790
- cols_to_remove = ['status', 'status_color', 'class_color', 'TRACK_ID', 'FRAME', 'x_anim', 'y_anim', 't',
791
- 'state', 'generation', 'root', 'parent', 'class_id', 'class', 't0', 'POSITION_X',
792
- 'POSITION_Y', 'position', 'well', 'well_index', 'well_name', 'pos_name', 'index',] + self.class_cols
793
-
794
- meta = get_experiment_metadata(self.exp_dir)
795
- if meta is not None:
796
- keys = list(meta.keys())
797
- cols_to_remove.extend(keys)
798
-
799
- labels = get_experiment_labels(self.exp_dir)
800
- if labels is not None:
801
- keys = list(labels.keys())
802
- cols_to_remove.extend(labels)
803
-
804
- cols = np.array(list(self.df_tracks.columns))
805
- time_cols = np.array([c.startswith('t_') for c in cols])
806
- time_cols = list(cols[time_cols])
807
- cols_to_remove += time_cols
808
- #cols_to_remove.extend(self.df_tracks.select_dtypes(include=['object']).columns)
809
-
810
- for tr in cols_to_remove:
811
- try:
812
- self.columns_to_rescale.remove(tr)
813
- except:
814
- pass
815
- # print(f'column {tr} could not be found...')
816
-
817
- x = self.df_tracks[self.columns_to_rescale].values
818
- self.MinMaxScaler.fit(x)
819
-
820
- # self.loc_t, self.loc_idx = np.where(self.tracks==self.track_of_interest)
821
403
 
822
404
  def make_status_column(self):
823
405
 
@@ -829,12 +411,14 @@ class SignalAnnotator(QMainWindow, Styles):
829
411
  cclass = group[self.class_name].to_numpy()[0]
830
412
  timeline = group['FRAME'].to_numpy()
831
413
  status = np.zeros_like(timeline)
414
+
832
415
  if t0 > 0:
833
- status[timeline >= t0] = 1.
834
- if cclass == 2:
835
- status[:] = 2
416
+ status[timeline >= t0] = 1.
417
+ # if cclass == 2:
418
+ # status[:] = 1.
836
419
  if cclass > 2:
837
420
  status[:] = 42
421
+
838
422
  status_color = [color_from_status(s) for s in status]
839
423
  class_color = [color_from_class(cclass) for i in range(len(status))]
840
424
 
@@ -852,7 +436,7 @@ class SignalAnnotator(QMainWindow, Styles):
852
436
 
853
437
  to_remove = ['TRACK_ID', 'FRAME', 'x_anim', 'y_anim', 't', 'state', 'generation', 'root', 'parent', 'class_id',
854
438
  'class', 't0', 'POSITION_X', 'POSITION_Y', 'position', 'well', 'well_index', 'well_name',
855
- 'pos_name', 'index','class_color','status_color']
439
+ 'pos_name', 'index','class_color','status_color','dummy','group_color']
856
440
 
857
441
  meta = get_experiment_metadata(self.exp_dir)
858
442
  if meta is not None:
@@ -882,7 +466,11 @@ class SignalAnnotator(QMainWindow, Styles):
882
466
  for i in range(len(self.signal_choice_cb)):
883
467
 
884
468
  signal_choice = self.signal_choice_cb[i].currentText()
885
- self.lines[i].set_label(signal_choice)
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)
886
474
 
887
475
  if signal_choice == "--":
888
476
  self.lines[i].set_xdata([])
@@ -911,9 +499,10 @@ class SignalAnnotator(QMainWindow, Styles):
911
499
  self.line_dt.set_xdata([t0, t0])
912
500
  self.line_dt.set_ydata([min_val, max_val])
913
501
 
914
- self.cell_ax.legend()
502
+ self.cell_ax.legend(fontsize=8)
915
503
  self.cell_fcanvas.canvas.draw()
916
504
  except Exception as e:
505
+ print(e)
917
506
  pass
918
507
 
919
508
  if len(range_values)>0:
@@ -942,53 +531,53 @@ class SignalAnnotator(QMainWindow, Styles):
942
531
  self.df_tracks.loc[self.df_tracks['FRAME'] == t, ['class_color', 'status_color']].to_numpy())
943
532
  self.tracks.append(self.df_tracks.loc[self.df_tracks['FRAME'] == t, 'TRACK_ID'].to_numpy())
944
533
 
945
- def load_annotator_config(self):
946
-
947
- """
948
- Load settings from config or set default values.
949
- """
950
-
951
- if os.path.exists(self.instructions_path):
952
- with open(self.instructions_path, 'r') as f:
953
-
954
- instructions = json.load(f)
955
-
956
- if 'rgb_mode' in instructions:
957
- self.rgb_mode = instructions['rgb_mode']
958
- else:
959
- self.rgb_mode = False
960
-
961
- if 'percentile_mode' in instructions:
962
- self.percentile_mode = instructions['percentile_mode']
963
- else:
964
- self.percentile_mode = True
965
-
966
- if 'channels' in instructions:
967
- self.target_channels = instructions['channels']
968
- else:
969
- self.target_channels = [[self.channel_names[0], 0.01, 99.99]]
970
-
971
- if 'fraction' in instructions:
972
- self.fraction = float(instructions['fraction'])
973
- else:
974
- self.fraction = 0.25
975
-
976
- if 'interval' in instructions:
977
- self.anim_interval = int(instructions['interval'])
978
- else:
979
- self.anim_interval = 1
980
-
981
- if 'log' in instructions:
982
- self.log_option = instructions['log']
983
- else:
984
- self.log_option = False
985
- else:
986
- self.rgb_mode = False
987
- self.log_option = False
988
- self.percentile_mode = True
989
- self.target_channels = [[self.channel_names[0], 0.01, 99.99]]
990
- self.fraction = 0.25
991
- self.anim_interval = 1
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
992
581
 
993
582
  def prepare_stack(self):
994
583
 
@@ -1074,76 +663,17 @@ class SignalAnnotator(QMainWindow, Styles):
1074
663
  self.fig.canvas.mpl_connect('pick_event', self.on_scatter_pick)
1075
664
  self.fcanvas.canvas.draw()
1076
665
 
1077
- def create_cell_signal_canvas(self):
1078
-
1079
- self.cell_fig, self.cell_ax = plt.subplots(tight_layout=True)
1080
- self.cell_fcanvas = FigureCanvas(self.cell_fig, interactive=True)
1081
- self.cell_ax.clear()
1082
-
1083
- spacing = 0.5
1084
- minorLocator = MultipleLocator(1)
1085
- self.cell_ax.xaxis.set_minor_locator(minorLocator)
1086
- self.cell_ax.xaxis.set_major_locator(MultipleLocator(5))
1087
- self.cell_ax.grid(which='major')
1088
- self.cell_ax.set_xlabel("time [frame]")
1089
- self.cell_ax.set_ylabel("signal")
666
+ def select_single_cell(self, index, timepoint):
1090
667
 
1091
- self.cell_fig.set_facecolor('none') # or 'None'
1092
- self.cell_fig.canvas.setStyleSheet("background-color: transparent;")
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)
1093
672
 
1094
- self.lines = [
1095
- self.cell_ax.plot([np.linspace(0, self.len_movie - 1, self.len_movie)], [np.zeros((self.len_movie))])[0] for
1096
- i in range(len(self.signal_choice_cb))]
1097
- for i in range(len(self.lines)):
1098
- self.lines[i].set_label(f'signal {i}')
1099
-
1100
- min_val, max_val = self.cell_ax.get_ylim()
1101
- self.line_dt, = self.cell_ax.plot([-1, -1], [min_val, max_val], c="k", linestyle="--")
1102
-
1103
- self.cell_ax.set_xlim(0, self.len_movie)
1104
- self.cell_ax.legend()
1105
- self.cell_fcanvas.canvas.draw()
1106
-
1107
- self.plot_signals()
1108
-
1109
- def on_scatter_pick(self, event):
1110
-
1111
- self.event = event
1112
-
1113
- self.correct_btn.disconnect()
1114
- self.correct_btn.clicked.connect(self.show_annotation_buttons)
1115
-
1116
- ind = event.ind
1117
-
1118
- if len(ind) > 1:
1119
- # More than one point in vicinity
1120
- datax, datay = [self.positions[self.framedata][i, 0] for i in ind], [self.positions[self.framedata][i, 1]
1121
- for i in ind]
1122
- msx, msy = event.mouseevent.xdata, event.mouseevent.ydata
1123
- dist = np.sqrt((np.array(datax) - msx) ** 2 + (np.array(datay) - msy) ** 2)
1124
- ind = [ind[np.argmin(dist)]]
1125
-
1126
- if len(ind) > 0 and (len(self.selection) == 0):
1127
-
1128
- self.selection.append([ind[0],self.framedata])
1129
- self.select_single_cell(ind[0], self.framedata)
1130
-
1131
- elif len(ind) > 0 and len(self.selection) == 1:
1132
- self.cancel_btn.click()
1133
- else:
1134
- pass
1135
-
1136
- def select_single_cell(self, index, timepoint):
1137
-
1138
- self.correct_btn.setEnabled(True)
1139
- self.cancel_btn.setEnabled(True)
1140
- self.del_shortcut.setEnabled(True)
1141
- self.no_event_shortcut.setEnabled(True)
1142
-
1143
- self.track_of_interest = self.tracks[timepoint][index]
1144
- print(f'You selected cell #{self.track_of_interest}...')
1145
- self.give_cell_information()
1146
- self.plot_signals()
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()
1147
677
 
1148
678
  self.loc_t = []
1149
679
  self.loc_idx = []
@@ -1159,11 +689,6 @@ class SignalAnnotator(QMainWindow, Styles):
1159
689
  self.colors[t][idx] = 'lime'
1160
690
 
1161
691
 
1162
- def shortcut_suppr(self):
1163
- self.correct_btn.click()
1164
- self.suppr_btn.click()
1165
- self.correct_btn.click()
1166
-
1167
692
  def shortcut_no_event(self):
1168
693
  self.correct_btn.click()
1169
694
  self.no_event_btn.click()
@@ -1232,25 +757,6 @@ class SignalAnnotator(QMainWindow, Styles):
1232
757
  self.anim.pause()
1233
758
  self.stop_btn.clicked.connect(self.start)
1234
759
 
1235
- def start(self):
1236
- '''
1237
- Starts interactive animation. Adds the draw frame command to the GUI
1238
- handler, calls show to start the event loop.
1239
- '''
1240
- self.start_btn.setShortcut(QKeySequence(""))
1241
-
1242
- self.last_frame_btn.setEnabled(True)
1243
- self.last_frame_btn.clicked.connect(self.set_last_frame)
1244
-
1245
- self.first_frame_btn.setEnabled(True)
1246
- self.first_frame_btn.clicked.connect(self.set_first_frame)
1247
-
1248
- self.start_btn.hide()
1249
- self.stop_btn.show()
1250
-
1251
- self.anim.event_source.start()
1252
- self.stop_btn.clicked.connect(self.stop)
1253
-
1254
760
  def give_cell_information(self):
1255
761
 
1256
762
  cell_selected = f"cell: {self.track_of_interest}\n"
@@ -1268,17 +774,10 @@ class SignalAnnotator(QMainWindow, Styles):
1268
774
  self.df_tracks = self.df_tracks.drop(self.df_tracks[self.df_tracks[self.class_name] > 2].index)
1269
775
  self.df_tracks.to_csv(self.trajectories_path, index=False)
1270
776
  print('Table successfully exported...')
1271
- self.compute_status_and_colors(0)
777
+ if self.class_choice_cb.currentText()!="":
778
+ self.compute_status_and_colors(0)
1272
779
  self.extract_scatter_from_trajectories()
1273
780
 
1274
- # self.give_cell_information()
1275
-
1276
- # def interval_slider_action(self):
1277
-
1278
- # print(dir(self.anim.event_source))
1279
-
1280
- # self.anim.event_source.interval = self.interval_slider.value()
1281
- # self.anim.event_source._timer_set_interval()
1282
781
 
1283
782
  def set_last_frame(self):
1284
783
 
@@ -1310,6 +809,11 @@ class SignalAnnotator(QMainWindow, Styles):
1310
809
 
1311
810
  self.first_key = 0
1312
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
+
1313
817
  self.anim._drawn_artists = sorted(self.anim._drawn_artists, key=lambda x: x.get_zorder())
1314
818
  for a in self.anim._drawn_artists:
1315
819
  a.set_visible(True)
@@ -1323,324 +827,109 @@ class SignalAnnotator(QMainWindow, Styles):
1323
827
  self.stop_btn.clicked.connect(self.start)
1324
828
  self.start_btn.setShortcut(QKeySequence("f"))
1325
829
 
1326
- def export_signals(self):
1327
-
1328
- auto_dataset_name = self.pos.split(os.sep)[-4] + '_' + self.pos.split(os.sep)[-2] + '.npy'
1329
-
1330
- if self.normalized_signals:
1331
- self.normalize_features_btn.click()
1332
-
1333
- training_set = []
1334
- cols = self.df_tracks.columns
1335
- tracks = np.unique(self.df_tracks["TRACK_ID"].to_numpy())
1336
-
1337
- for track in tracks:
1338
- # Add all signals at given track
1339
- signals = {}
1340
- for c in cols:
1341
- signals.update({c: self.df_tracks.loc[self.df_tracks["TRACK_ID"] == track, c].to_numpy()})
1342
- time_of_interest = self.df_tracks.loc[self.df_tracks["TRACK_ID"] == track, self.time_name].to_numpy()[0]
1343
- cclass = self.df_tracks.loc[self.df_tracks["TRACK_ID"] == track, self.class_name].to_numpy()[0]
1344
- signals.update({"time_of_interest": time_of_interest, "class": cclass})
1345
- # Here auto add all available channels
1346
- training_set.append(signals)
1347
-
1348
- pathsave = QFileDialog.getSaveFileName(self, "Select file name", self.exp_dir + auto_dataset_name, ".npy")[0]
1349
- if pathsave != '':
1350
- if not pathsave.endswith(".npy"):
1351
- pathsave += ".npy"
1352
- try:
1353
- np.save(pathsave, training_set)
1354
- print(f'File successfully written in {pathsave}.')
1355
- except Exception as e:
1356
- print(f"Error {e}...")
1357
-
1358
- def normalize_features(self):
1359
-
1360
- x = self.df_tracks[self.columns_to_rescale].values
1361
-
1362
- if not self.normalized_signals:
1363
- x = self.MinMaxScaler.transform(x)
1364
- self.df_tracks[self.columns_to_rescale] = x
1365
- self.plot_signals()
1366
- self.normalized_signals = True
1367
- self.normalize_features_btn.setIcon(icon(MDI6.arrow_collapse_vertical, color="#1565c0"))
1368
- self.normalize_features_btn.setIconSize(QSize(25, 25))
1369
- else:
1370
- x = self.MinMaxScaler.inverse_transform(x)
1371
- self.df_tracks[self.columns_to_rescale] = x
1372
- self.plot_signals()
1373
- self.normalized_signals = False
1374
- self.normalize_features_btn.setIcon(icon(MDI6.arrow_collapse_vertical, color="black"))
1375
- self.normalize_features_btn.setIconSize(QSize(25, 25))
1376
-
1377
- def switch_to_log(self):
1378
-
1379
- """
1380
- Better would be to create a log(quantity) and plot it...
1381
- """
1382
-
1383
- try:
1384
- if self.cell_ax.get_yscale()=='linear':
1385
- ymin,ymax = self.cell_ax.get_ylim()
1386
- self.cell_ax.set_yscale('log')
1387
- self.log_btn.setIcon(icon(MDI6.math_log,color="#1565c0"))
1388
- self.cell_ax.set_ylim(self.value_magnitude, ymax)
1389
- else:
1390
- self.cell_ax.set_yscale('linear')
1391
- self.log_btn.setIcon(icon(MDI6.math_log,color="black"))
1392
- except Exception as e:
1393
- print(e)
1394
830
 
1395
- #self.cell_ax.autoscale()
1396
- self.cell_fcanvas.canvas.draw_idle()
831
+ class MeasureAnnotator(BaseAnnotator):
1397
832
 
1398
- class MeasureAnnotator(SignalAnnotator):
833
+ def __init__(self, *args, **kwargs):
834
+
835
+ super().__init__(read_config=False, *args, **kwargs)
1399
836
 
1400
- def __init__(self, parent_window=None):
837
+ self.setWindowTitle("Static annotator")
1401
838
 
1402
- QMainWindow.__init__(self)
1403
- self.parent_window = parent_window
1404
- self.setWindowTitle("Signal annotator")
1405
- self.mode = self.parent_window.mode
1406
- self.pos = self.parent_window.parent_window.pos
1407
- self.exp_dir = self.parent_window.exp_dir
1408
- self.n_signals = 3
1409
- self.soft_path = get_software_location()
1410
- self.recently_modified = False
1411
- self.selection = []
1412
839
  self.int_validator = QIntValidator()
1413
840
  self.current_alpha=0.5
1414
- if self.mode == "targets":
1415
- self.instructions_path = self.exp_dir + os.sep.join(['configs','signal_annotator_config_targets.json'])
1416
- self.trajectories_path = self.pos + os.sep.join(['output','tables','trajectories_targets.csv'])
1417
- elif self.mode == "effectors":
1418
- self.instructions_path = self.exp_dir + os.sep.join(['configs','signal_annotator_config_effectors.json'])
1419
- self.trajectories_path = self.pos + os.sep.join(['output','tables','trajectories_effectors.csv'])
1420
-
1421
- self.screen_height = self.parent_window.parent_window.parent_window.screen_height
1422
- self.screen_width = self.parent_window.parent_window.parent_window.screen_width
841
+ self.value_magnitude = 1
842
+
843
+ epsilon = 0.01
844
+ self.observed_min_intensity = 0
845
+ self.observed_max_intensity = 0 + epsilon
846
+
1423
847
  self.current_frame = 0
1424
848
  self.show_fliers = False
1425
849
  self.status_name = 'group'
1426
850
 
1427
- center_window(self)
1428
-
1429
- self.locate_stack()
1430
-
1431
- data, properties, graph, labels, _ = load_napari_data(self.pos, prefix=None, population=self.mode,return_stack=False)
1432
- # if data is not None:
1433
- # self.labels = relabel_segmentation(labels,data,properties)
1434
- # else:
1435
- self.labels = labels
1436
-
1437
- self.current_channel = 0
1438
-
1439
- self.locate_tracks()
1440
-
1441
- self.generate_signal_choices()
1442
- self.frame_lbl = QLabel('position: ')
1443
- self.static_image()
1444
- self.create_cell_signal_canvas()
1445
-
1446
- self.populate_widget()
1447
- self.changed_class()
1448
-
1449
- self.setMinimumWidth(int(0.8 * self.screen_width))
1450
- # self.setMaximumHeight(int(0.8*self.screen_height))
1451
- self.setMinimumHeight(int(0.8 * self.screen_height))
1452
- # self.setMaximumHeight(int(0.8*self.screen_height))
1453
-
1454
- self.setAttribute(Qt.WA_DeleteOnClose)
1455
- self.previous_index = None
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])
1456
865
 
1457
- def static_image(self):
866
+ else:
867
+ self.close()
868
+
869
+ def locate_tracks(self):
1458
870
 
1459
871
  """
1460
- Load an image.
1461
-
872
+ Locate the tracks.
1462
873
  """
1463
874
 
1464
- self.framedata = 0
1465
- self.current_label=self.labels[self.current_frame]
1466
- self.fig, self.ax = plt.subplots(tight_layout=True)
1467
- self.fcanvas = FigureCanvas(self.fig, interactive=True)
1468
- self.ax.clear()
1469
- # print(self.current_stack.shape)
1470
- self.im = self.ax.imshow(self.img, cmap='gray')
1471
- self.status_scatter = self.ax.scatter(self.positions[0][:, 0], self.positions[0][:, 1], marker="o",
1472
- facecolors='none', edgecolors=self.colors[0][:, 0], s=200, picker=True)
1473
- self.im_mask = self.ax.imshow(np.ma.masked_where(self.current_label == 0, self.current_label),
1474
- cmap='viridis', interpolation='none',alpha=self.current_alpha,vmin=0,vmax=np.nanmax(self.labels.flatten()))
1475
- self.ax.set_xticks([])
1476
- self.ax.set_yticks([])
1477
- self.ax.set_aspect('equal')
1478
-
1479
- self.fig.set_facecolor('none') # or 'None'
1480
- self.fig.canvas.setStyleSheet("background-color: black;")
1481
-
1482
- self.fig.canvas.mpl_connect('pick_event', self.on_scatter_pick)
1483
- self.fcanvas.canvas.draw()
1484
-
1485
- def create_cell_signal_canvas(self):
1486
-
1487
- self.cell_fig, self.cell_ax = plt.subplots()
1488
- self.cell_fcanvas = FigureCanvas(self.cell_fig, interactive=False)
1489
- self.cell_ax.clear()
1490
-
1491
- # spacing = 0.5
1492
- # minorLocator = MultipleLocator(1)
1493
- # self.cell_ax.xaxis.set_minor_locator(minorLocator)
1494
- # self.cell_ax.xaxis.set_major_locator(MultipleLocator(5))
1495
- self.cell_ax.grid(which='major')
1496
- self.cell_ax.set_xlabel("time [frame]")
1497
- self.cell_ax.set_ylabel("signal")
1498
-
1499
- self.cell_fig.set_facecolor('none') # or 'None'
1500
- self.cell_fig.canvas.setStyleSheet("background-color: transparent;")
1501
-
1502
- self.lines = [
1503
- self.cell_ax.plot([np.linspace(0, self.len_movie - 1, self.len_movie)], [np.zeros(self.len_movie)])[0] for
1504
- i in range(len(self.signal_choice_cb))]
1505
- for i in range(len(self.lines)):
1506
- self.lines[i].set_label(f'signal {i}')
1507
-
1508
- min_val, max_val = self.cell_ax.get_ylim()
1509
- self.line_dt, = self.cell_ax.plot([-1, -1], [min_val, max_val], c="k", linestyle="--")
1510
-
1511
- self.cell_ax.set_xlim(0, self.len_movie)
1512
- self.cell_fcanvas.canvas.draw()
1513
-
1514
- self.plot_signals()
1515
-
1516
- def plot_signals(self):
1517
-
1518
- #try:
1519
- current_frame = self.current_frame # Assuming you have a variable for the current frame
1520
-
1521
- yvalues = []
1522
- all_yvalues = []
1523
- current_yvalues = []
1524
- all_median_values = []
1525
- labels = []
1526
-
1527
- for i in range(len(self.signal_choice_cb)):
1528
-
1529
- signal_choice = self.signal_choice_cb[i].currentText()
1530
-
1531
- if signal_choice != "--":
1532
- if 'TRACK_ID' in self.df_tracks.columns:
1533
- ydata = self.df_tracks.loc[
1534
- (self.df_tracks['TRACK_ID'] == self.track_of_interest) &
1535
- (self.df_tracks['FRAME'] == current_frame), signal_choice].to_numpy()
1536
- else:
1537
- ydata = self.df_tracks.loc[
1538
- (self.df_tracks['ID'] == self.track_of_interest), signal_choice].to_numpy()
1539
- all_ydata = self.df_tracks.loc[:, signal_choice].to_numpy()
1540
- ydataNaN = ydata
1541
- ydata = ydata[ydata == ydata] # remove nan
1542
- current_ydata = self.df_tracks.loc[
1543
- (self.df_tracks['FRAME'] == current_frame), signal_choice].to_numpy()
1544
- current_ydata = current_ydata[current_ydata == current_ydata]
1545
- all_ydata = all_ydata[all_ydata == all_ydata]
1546
- yvalues.extend(ydataNaN)
1547
- current_yvalues.append(current_ydata)
1548
- all_yvalues.append(all_ydata)
1549
- labels.append(signal_choice)
1550
-
1551
- self.cell_ax.clear()
1552
-
1553
- if len(yvalues) > 0:
1554
- self.cell_ax.boxplot(all_yvalues, showfliers=self.show_fliers)
1555
- ylim = self.cell_ax.get_ylim()
1556
- self.cell_ax.set_ylim(ylim)
1557
- x_pos = np.arange(len(all_yvalues)) + 1
1558
-
1559
- for index, feature in enumerate(current_yvalues):
1560
- x_values_strip = (index + 1) + np.random.normal(0, 0.04, size=len(
1561
- feature))
1562
- self.cell_ax.plot(x_values_strip, feature, marker='o', linestyle='None', color=tab10.colors[0],
1563
- alpha=0.1)
1564
-
1565
- self.cell_ax.plot(x_pos, yvalues, marker='H', linestyle='None', color=tab10.colors[3], alpha=1)
1566
-
875
+ if not os.path.exists(self.trajectories_path):
1567
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()
1568
885
  else:
1569
- self.cell_ax.text(0.5, 0.5, "No data available", horizontalalignment='center',
1570
- verticalalignment='center', transform=self.cell_ax.transAxes)
1571
886
 
1572
- self.cell_fcanvas.canvas.draw()
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'])
1573
893
 
1574
- # except Exception as e:
1575
- # print("plot_signals: ",f"{e=}")
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])
1576
897
 
1577
- def configure_ylims(self):
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'
1578
909
 
1579
- try:
1580
- min_values = []
1581
- max_values = []
1582
- for i in range(len(self.signal_choice_cb)):
1583
- signal = self.signal_choice_cb[i].currentText()
1584
- if signal == '--':
1585
- continue
1586
- else:
1587
- maxx = np.max(self.df_tracks.loc[:, signal].to_numpy().flatten())
1588
- minn = np.min(self.df_tracks.loc[:, signal].to_numpy().flatten())
1589
- min_values.append(minn)
1590
- max_values.append(maxx)
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
1591
916
 
1592
- if len(min_values) > 0:
1593
- self.cell_ax.set_ylim(np.amin(min_values), np.amax(max_values))
1594
- except Exception as e:
1595
- print(e)
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)
1596
921
 
1597
- def plot_red_points(self, ax):
1598
- yvalues = []
1599
- current_frame = self.current_frame
1600
- for i in range(len(self.signal_choice_cb)):
1601
- signal_choice = self.signal_choice_cb[i].currentText()
1602
- if signal_choice != "--":
1603
- #print(f'plot signal {signal_choice} for cell {self.track_of_interest} at frame {current_frame}')
1604
- if 'TRACK_ID' in self.df_tracks.columns:
1605
- ydata = self.df_tracks.loc[
1606
- (self.df_tracks['TRACK_ID'] == self.track_of_interest) &
1607
- (self.df_tracks['FRAME'] == current_frame), signal_choice].to_numpy()
1608
- else:
1609
- ydata = self.df_tracks.loc[
1610
- (self.df_tracks['ID'] == self.track_of_interest) &
1611
- (self.df_tracks['FRAME'] == current_frame), signal_choice].to_numpy()
1612
- ydata = ydata[ydata == ydata] # remove nan
1613
- yvalues.extend(ydata)
1614
- x_pos = np.arange(len(yvalues)) + 1
1615
- ax.plot(x_pos, yvalues, marker='H', linestyle='None', color=tab10.colors[3],
1616
- alpha=1) # Plot red points representing cells
1617
- self.cell_fcanvas.canvas.draw()
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)
1618
927
 
1619
- def on_scatter_pick(self, event):
1620
- ind = event.ind
1621
- if len(ind) > 1:
1622
- # More than one point in vicinity
1623
- datax, datay = [self.positions[self.framedata][i, 0] for i in ind], [self.positions[self.framedata][i, 1]
1624
- for i in ind]
1625
- msx, msy = event.mouseevent.xdata, event.mouseevent.ydata
1626
- dist = np.sqrt((np.array(datax) - msx) ** 2 + (np.array(datay) - msy) ** 2)
1627
- ind = [ind[np.argmin(dist)]]
1628
-
1629
- if len(ind) > 0 and (len(self.selection) == 0):
1630
- ind = ind[0]
1631
- self.selection.append(ind)
1632
- self.correct_btn.setEnabled(True)
1633
- self.cancel_btn.setEnabled(True)
1634
- self.del_shortcut.setEnabled(True)
1635
- self.no_event_shortcut.setEnabled(True)
1636
- self.track_of_interest = self.tracks[self.framedata][ind]
1637
- print(f'You selected cell #{self.track_of_interest}...')
1638
- self.give_cell_information()
1639
- if len(self.cell_ax.lines) > 0:
1640
- self.cell_ax.lines[-1].remove() # Remove the last line (red points) from the plot
1641
- self.plot_red_points(self.cell_ax)
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()
1642
931
  else:
1643
- self.plot_signals()
932
+ self.track_of_interest = self.df_tracks.dropna(subset='ID')['ID'].min()
1644
933
 
1645
934
  self.loc_t = []
1646
935
  self.loc_idx = []
@@ -1650,196 +939,103 @@ class MeasureAnnotator(SignalAnnotator):
1650
939
  self.loc_t.append(t)
1651
940
  self.loc_idx.append(indices[0])
1652
941
 
1653
- self.previous_color = []
1654
- for t, idx in zip(self.loc_t, self.loc_idx):
1655
- self.previous_color.append(self.colors[t][idx].copy())
1656
- self.colors[t][idx] = 'lime'
1657
-
1658
- elif len(ind) > 0 and len(self.selection) == 1:
1659
- self.cancel_btn.click()
1660
- else:
1661
- pass
1662
- self.draw_frame(self.current_frame)
1663
- self.fcanvas.canvas.draw()
1664
-
1665
- def populate_widget(self):
1666
-
1667
- """
1668
- Create the multibox design.
1669
-
1670
- """
1671
-
1672
- self.button_widget = QWidget()
1673
- main_layout = QHBoxLayout()
1674
- self.button_widget.setLayout(main_layout)
1675
-
1676
- main_layout.setContentsMargins(30, 30, 30, 30)
1677
-
1678
- self.left_panel = QVBoxLayout()
1679
- self.left_panel.setContentsMargins(30, 5, 30, 5)
1680
- #self.left_panel.setSpacing(3)
1681
-
1682
- self.right_panel = QVBoxLayout()
1683
-
1684
- class_hbox = QHBoxLayout()
1685
- class_hbox.setContentsMargins(0,0,0,0)
1686
- class_hbox.setSpacing(0)
1687
-
1688
- class_hbox.addWidget(QLabel('characteristic \n group: '), 25)
1689
- self.class_choice_cb = QComboBox()
1690
-
1691
- cols = np.array(self.df_tracks.columns)
1692
- self.class_cols = np.array([c.startswith('group') or c.startswith('status') for c in list(self.df_tracks.columns)])
1693
- self.class_cols = list(cols[self.class_cols])
1694
-
1695
- try:
1696
- self.class_cols.remove('group_id')
1697
- except Exception:
1698
- pass
1699
- try:
1700
- self.class_cols.remove('group_color')
1701
- except Exception:
1702
- pass
942
+ self.MinMaxScaler = MinMaxScaler()
943
+ self.columns_to_rescale = list(self.df_tracks.columns)
1703
944
 
1704
- self.class_choice_cb.addItems(self.class_cols)
1705
- self.class_choice_cb.currentIndexChanged.connect(self.changed_class)
1706
- class_hbox.addWidget(self.class_choice_cb, 70)
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
1707
950
 
1708
- self.add_class_btn = QPushButton('')
1709
- self.add_class_btn.setStyleSheet(self.button_select_all)
1710
- self.add_class_btn.setIcon(icon(MDI6.plus, color="black"))
1711
- self.add_class_btn.setToolTip("Add a new characteristic group")
1712
- self.add_class_btn.setIconSize(QSize(20, 20))
1713
- self.add_class_btn.clicked.connect(self.create_new_event_class)
1714
- class_hbox.addWidget(self.add_class_btn, 5)
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)
1715
955
 
1716
- self.del_class_btn = QPushButton('')
1717
- self.del_class_btn.setStyleSheet(self.button_select_all)
1718
- self.del_class_btn.setIcon(icon(MDI6.delete, color="black"))
1719
- self.del_class_btn.setToolTip("Delete a characteristic group")
1720
- self.del_class_btn.setIconSize(QSize(20, 20))
1721
- self.del_class_btn.clicked.connect(self.del_event_class)
1722
- class_hbox.addWidget(self.del_class_btn, 5)
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)
1723
960
 
1724
- self.left_panel.addLayout(class_hbox,5)
961
+ for tr in cols_to_remove:
962
+ try:
963
+ self.columns_to_rescale.remove(tr)
964
+ except:
965
+ pass
1725
966
 
1726
- self.cell_info = QLabel('')
1727
- self.left_panel.addWidget(self.cell_info,5)
967
+ x = self.df_tracks[self.columns_to_rescale].values
968
+ self.MinMaxScaler.fit(x)
1728
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
+
1729
976
  time_option_hbox = QHBoxLayout()
1730
977
  time_option_hbox.setContentsMargins(100, 0, 100, 0)
1731
978
  time_option_hbox.setSpacing(0)
1732
979
 
1733
980
  self.time_of_interest_label = QLabel('phenotype: ')
1734
981
  time_option_hbox.addWidget(self.time_of_interest_label, 30)
982
+
1735
983
  self.time_of_interest_le = QLineEdit()
1736
984
  self.time_of_interest_le.setValidator(self.int_validator)
1737
985
  time_option_hbox.addWidget(self.time_of_interest_le)
1738
- self.del_cell_btn = QPushButton('')
1739
- self.del_cell_btn.setStyleSheet(self.button_select_all)
1740
- self.del_cell_btn.setIcon(icon(MDI6.delete, color="black"))
1741
- self.del_cell_btn.setToolTip("Delete cell")
1742
- self.del_cell_btn.setIconSize(QSize(20, 20))
1743
- self.del_cell_btn.clicked.connect(self.del_cell)
1744
- time_option_hbox.addWidget(self.del_cell_btn)
1745
- self.left_panel.addLayout(time_option_hbox,5)
1746
-
1747
- main_action_hbox = QHBoxLayout()
1748
- main_action_hbox.setContentsMargins(0,0,0,0)
1749
- main_action_hbox.setSpacing(0)
1750
-
1751
- self.correct_btn = QPushButton('correct')
1752
- self.correct_btn.setIcon(icon(MDI6.redo_variant, color="white"))
1753
- self.correct_btn.setIconSize(QSize(20, 20))
1754
- self.correct_btn.setStyleSheet(self.button_style_sheet)
1755
- self.correct_btn.clicked.connect(self.show_annotation_buttons)
1756
- self.correct_btn.setEnabled(False)
1757
- main_action_hbox.addWidget(self.correct_btn)
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])
1758
1014
 
1759
- self.cancel_btn = QPushButton('cancel')
1760
- self.cancel_btn.setStyleSheet(self.button_style_sheet_2)
1761
- self.cancel_btn.setShortcut(QKeySequence("Esc"))
1762
- self.cancel_btn.setEnabled(False)
1763
- self.cancel_btn.clicked.connect(self.cancel_selection)
1764
- main_action_hbox.addWidget(self.cancel_btn)
1765
- self.left_panel.addLayout(main_action_hbox,5)
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)
1766
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
+
1767
1032
  self.annotation_btns_to_hide = [self.time_of_interest_label,
1768
1033
  self.time_of_interest_le,
1769
- self.del_cell_btn]
1034
+ self.suppr_btn]
1770
1035
  self.hide_annotation_buttons()
1771
- #### End of annotation buttons
1772
-
1773
- self.del_shortcut = QShortcut(Qt.Key_Delete, self) # QKeySequence("s")
1774
- self.del_shortcut.activated.connect(self.shortcut_suppr)
1775
- self.del_shortcut.setEnabled(False)
1776
-
1777
- self.no_event_shortcut = QShortcut(QKeySequence("n"), self) # QKeySequence("s")
1778
- self.no_event_shortcut.activated.connect(self.shortcut_no_event)
1779
- self.no_event_shortcut.setEnabled(False)
1780
-
1781
- # Cell signals
1782
- self.cell_fcanvas.setMinimumHeight(int(0.2*self.screen_height))
1783
- self.left_panel.addWidget(self.cell_fcanvas,90)
1784
-
1785
- plot_buttons_hbox = QHBoxLayout()
1786
- plot_buttons_hbox.setContentsMargins(0, 0, 0, 0)
1787
- self.outliers_check = QCheckBox('Show outliers')
1788
- self.outliers_check.toggled.connect(self.show_outliers)
1789
-
1790
- self.normalize_features_btn = QPushButton('')
1791
- self.normalize_features_btn.setStyleSheet(self.button_select_all)
1792
- self.normalize_features_btn.setIcon(icon(MDI6.arrow_collapse_vertical, color="black"))
1793
- self.normalize_features_btn.setIconSize(QSize(25, 25))
1794
- self.normalize_features_btn.setFixedSize(QSize(30, 30))
1795
- # self.normalize_features_btn.setShortcut(QKeySequence('n'))
1796
- self.normalize_features_btn.clicked.connect(self.normalize_features)
1797
-
1798
- plot_buttons_hbox.addWidget(QLabel(''), 90)
1799
- plot_buttons_hbox.addWidget(self.outliers_check)
1800
- plot_buttons_hbox.addWidget(self.normalize_features_btn, 5)
1801
- self.normalized_signals = False
1802
-
1803
- self.log_btn = QPushButton()
1804
- self.log_btn.setIcon(icon(MDI6.math_log, color="black"))
1805
- self.log_btn.setStyleSheet(self.button_select_all)
1806
- self.log_btn.clicked.connect(self.switch_to_log)
1807
- plot_buttons_hbox.addWidget(self.log_btn, 5)
1808
-
1809
- self.left_panel.addLayout(plot_buttons_hbox,5)
1810
-
1811
- signal_choice_vbox = QVBoxLayout()
1812
- signal_choice_vbox.setContentsMargins(30, 0, 30, 50)
1813
- for i in range(len(self.signal_choice_cb)):
1814
- hlayout = QHBoxLayout()
1815
- hlayout.addWidget(self.signal_choice_label[i], 20)
1816
- hlayout.addWidget(self.signal_choice_cb[i], 75)
1817
- # hlayout.addWidget(self.log_btns[i], 5)
1818
- signal_choice_vbox.addLayout(hlayout)
1819
-
1820
- # self.log_btns[i].setIcon(icon(MDI6.math_log,color="black"))
1821
- # self.log_btns[i].setStyleSheet(self.parent.parent.parent.button_select_all)
1822
- # self.log_btns[i].clicked.connect(lambda ch, i=i: self.switch_to_log(i))
1823
-
1824
- self.left_panel.addLayout(signal_choice_vbox,10)
1825
-
1826
- btn_hbox = QHBoxLayout()
1827
- self.save_btn = QPushButton('Save')
1828
- self.save_btn.setStyleSheet(self.button_style_sheet)
1829
- self.save_btn.clicked.connect(self.save_trajectories)
1830
- btn_hbox.addWidget(self.save_btn, 90)
1831
-
1832
- self.export_btn = QPushButton('')
1833
- self.export_btn.setStyleSheet(self.button_select_all)
1834
- self.export_btn.clicked.connect(self.export_measurements)
1835
- self.export_btn.setIcon(icon(MDI6.export, color="black"))
1836
- self.export_btn.setIconSize(QSize(25, 25))
1837
- btn_hbox.addWidget(self.export_btn, 10)
1838
- self.left_panel.addLayout(btn_hbox,5)
1839
-
1840
- # Animation
1036
+
1037
+ # Right panel
1841
1038
  animation_buttons_box = QHBoxLayout()
1842
-
1843
1039
  animation_buttons_box.addWidget(self.frame_lbl, 20, alignment=Qt.AlignLeft)
1844
1040
 
1845
1041
  self.first_frame_btn = QPushButton()
@@ -1886,7 +1082,7 @@ class MeasureAnnotator(SignalAnnotator):
1886
1082
 
1887
1083
  self.contrast_slider.setSingleStep(0.001)
1888
1084
  self.contrast_slider.setTickInterval(0.001)
1889
- self.contrast_slider.setOrientation(1)
1085
+ self.contrast_slider.setOrientation(Qt.Horizontal)
1890
1086
  self.contrast_slider.setRange(
1891
1087
  *[np.nanpercentile(self.img, 0.001), np.nanpercentile(self.img, 99.999)])
1892
1088
  self.contrast_slider.setValue(
@@ -1897,7 +1093,7 @@ class MeasureAnnotator(SignalAnnotator):
1897
1093
  self.right_panel.addLayout(contrast_hbox, 5)
1898
1094
  self.alpha_slider = QLabeledDoubleSlider()
1899
1095
  self.alpha_slider.setSingleStep(0.001)
1900
- self.alpha_slider.setOrientation(1)
1096
+ self.alpha_slider.setOrientation(Qt.Horizontal)
1901
1097
  self.alpha_slider.setRange(0, 1)
1902
1098
  self.alpha_slider.setValue(self.current_alpha)
1903
1099
  self.alpha_slider.setDecimals(3)
@@ -1915,25 +1111,180 @@ class MeasureAnnotator(SignalAnnotator):
1915
1111
  self.choose_channel.currentIndexChanged.connect(self.changed_channel)
1916
1112
  channel_hbox.addWidget(self.choose_channel)
1917
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):
1918
1124
 
1919
- main_layout.addLayout(self.left_panel, 35)
1920
- main_layout.addLayout(self.right_panel, 65)
1921
- self.button_widget.adjustSize()
1125
+ """
1126
+ Load an image.
1922
1127
 
1923
- self.setCentralWidget(self.button_widget)
1924
- self.show()
1128
+ """
1925
1129
 
1926
- QApplication.processEvents()
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()
1927
1250
 
1928
- def closeEvent(self, event):
1929
- # result = QMessageBox.question(self,
1930
- # "Confirm Exit...",
1931
- # "Are you sure you want to exit ?",
1932
- # QMessageBox.Yes| QMessageBox.No,
1933
- # )
1934
- # del self.img
1935
- gc.collect()
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()
1936
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()
1937
1288
 
1938
1289
  def export_measurements(self):
1939
1290
 
@@ -1994,13 +1345,11 @@ class MeasureAnnotator(SignalAnnotator):
1994
1345
  return None
1995
1346
  else:
1996
1347
  pass
1348
+
1997
1349
  self.df_tracks.loc[:, self.target_class] = 0
1998
- self.class_choice_cb.clear()
1999
- cols = np.array(self.df_tracks.columns)
2000
- self.class_cols = np.array([c.startswith('group') for c in list(self.df_tracks.columns)])
2001
- self.class_cols = list(cols[self.class_cols])
2002
- self.class_cols.remove('group_color')
2003
- self.class_choice_cb.addItems(self.class_cols)
1350
+
1351
+ self.update_class_cb()
1352
+
2004
1353
  idx = self.class_choice_cb.findText(self.target_class)
2005
1354
  self.status_name = self.target_class
2006
1355
  self.class_choice_cb.setCurrentIndex(idx)
@@ -2046,7 +1395,7 @@ class MeasureAnnotator(SignalAnnotator):
2046
1395
  def create_new_event_class(self):
2047
1396
 
2048
1397
  # display qwidget to name the event
2049
- self.newClassWidget = QWidget()
1398
+ self.newClassWidget = CelldetectiveWidget()
2050
1399
  self.newClassWidget.setWindowTitle('Create new characteristic group')
2051
1400
 
2052
1401
  layout = QVBoxLayout()
@@ -2094,7 +1443,6 @@ class MeasureAnnotator(SignalAnnotator):
2094
1443
  self.state_color_map = color_from_state(all_states, recently_modified=False)
2095
1444
 
2096
1445
  self.df_tracks['group_color'] = self.df_tracks[self.status_name].apply(self.assign_color_state)
2097
-
2098
1446
  self.extract_scatter_from_trajectories()
2099
1447
  self.give_cell_information()
2100
1448
 
@@ -2106,15 +1454,17 @@ class MeasureAnnotator(SignalAnnotator):
2106
1454
  self.correct_btn.setText('correct')
2107
1455
  self.cancel_btn.setEnabled(False)
2108
1456
  self.del_shortcut.setEnabled(False)
2109
- self.no_event_shortcut.setEnabled(False)
1457
+
2110
1458
  if len(self.selection) > 0:
2111
1459
  self.selection.pop(0)
1460
+
2112
1461
  self.draw_frame(self.current_frame)
2113
1462
  self.fcanvas.canvas.draw()
2114
1463
 
2115
1464
  def assign_color_state(self, state):
1465
+
2116
1466
  if np.isnan(state):
2117
- state = "nan"
1467
+ state = "nan"
2118
1468
  return self.state_color_map[state]
2119
1469
 
2120
1470
  def draw_frame(self, framedata):
@@ -2141,45 +1491,26 @@ class MeasureAnnotator(SignalAnnotator):
2141
1491
  return (self.im, self.status_scatter,self.im_mask,)
2142
1492
 
2143
1493
  def compute_status_and_colors(self):
2144
- print('compute status and colors!')
1494
+
1495
+ self.cancel_selection()
1496
+
2145
1497
  if self.class_choice_cb.currentText() == '':
2146
- self.status_name=self.target_class
1498
+ pass
2147
1499
  else:
2148
1500
  self.status_name = self.class_choice_cb.currentText()
2149
1501
 
2150
- print(f'{self.status_name=}')
2151
1502
  if self.status_name not in self.df_tracks.columns:
2152
- print('not in df, make column')
1503
+ print('Creating a new status for visualization...')
2153
1504
  self.make_status_column()
2154
1505
  else:
1506
+ print(f'Generating per-state colors for the status "{self.status_name}"...')
2155
1507
  all_states = self.df_tracks.loc[:, self.status_name].tolist()
2156
1508
  all_states = np.array(all_states)
2157
1509
  self.state_color_map = color_from_state(all_states, recently_modified=False)
2158
- print(f'{self.state_color_map=}')
1510
+ print(f'Color mapping for "{self.status_name}":')
1511
+ pretty_table(self.state_color_map)
2159
1512
  self.df_tracks['group_color'] = self.df_tracks[self.status_name].apply(self.assign_color_state)
2160
- print(self.df_tracks['group_color'])
2161
-
2162
- def del_event_class(self):
2163
-
2164
- msgBox = QMessageBox()
2165
- msgBox.setIcon(QMessageBox.Warning)
2166
- msgBox.setText(
2167
- f"You are about to delete characteristic group {self.class_choice_cb.currentText()}. Do you still want to proceed?")
2168
- msgBox.setWindowTitle("Warning")
2169
- msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
2170
- returnValue = msgBox.exec()
2171
- if returnValue == QMessageBox.No:
2172
- return None
2173
- else:
2174
- class_to_delete = self.class_choice_cb.currentText()
2175
- cols_to_delete = [class_to_delete]
2176
- for c in cols_to_delete:
2177
- try:
2178
- self.df_tracks = self.df_tracks.drop([c], axis=1)
2179
- except Exception as e:
2180
- print(e)
2181
- item_idx = self.class_choice_cb.findText(class_to_delete)
2182
- self.class_choice_cb.removeItem(item_idx)
1513
+
2183
1514
 
2184
1515
  def make_status_column(self):
2185
1516
  if self.status_name == "state_firstdetection":
@@ -2191,126 +1522,6 @@ class MeasureAnnotator(SignalAnnotator):
2191
1522
  self.state_color_map = color_from_state(all_states, recently_modified=False)
2192
1523
  self.df_tracks['group_color'] = self.df_tracks[self.status_name].apply(self.assign_color_state)
2193
1524
 
2194
- def locate_tracks(self):
2195
-
2196
- """
2197
- Locate the tracks.
2198
- """
2199
-
2200
- if not os.path.exists(self.trajectories_path):
2201
-
2202
- msgBox = QMessageBox()
2203
- msgBox.setIcon(QMessageBox.Warning)
2204
- msgBox.setText("The trajectories cannot be detected.")
2205
- msgBox.setWindowTitle("Warning")
2206
- msgBox.setStandardButtons(QMessageBox.Ok)
2207
- returnValue = msgBox.exec()
2208
- if returnValue == QMessageBox.Yes:
2209
- self.close()
2210
- else:
2211
-
2212
- # Load and prep tracks
2213
- self.df_tracks = pd.read_csv(self.trajectories_path)
2214
- if 'TRACK_ID' in self.df_tracks.columns:
2215
- self.df_tracks = self.df_tracks.sort_values(by=['TRACK_ID', 'FRAME'])
2216
- else:
2217
- self.df_tracks = self.df_tracks.sort_values(by=['ID', 'FRAME'])
2218
-
2219
- cols = np.array(self.df_tracks.columns)
2220
- self.class_cols = np.array([c.startswith('group') for c in list(self.df_tracks.columns)])
2221
- self.class_cols = list(cols[self.class_cols])
2222
- try:
2223
- self.class_cols.remove('class_id')
2224
- except:
2225
- pass
2226
- try:
2227
- self.class_cols.remove('group_color')
2228
- except:
2229
- pass
2230
- if len(self.class_cols) > 0:
2231
- self.status = self.class_cols[0]
2232
-
2233
- else:
2234
-
2235
- self.status_name = 'group'
2236
-
2237
- if self.status_name not in self.df_tracks.columns:
2238
- # only create the status column if it does not exist to not erase static classification results
2239
- self.make_status_column()
2240
- else:
2241
- # all good, do nothing
2242
- pass
2243
- # else:
2244
- # if not self.status_name in self.df_tracks.columns:
2245
- # self.df_tracks[self.status_name] = 0
2246
- # self.df_tracks['state_color'] = color_from_status(0)
2247
- # self.df_tracks['class_color'] = color_from_class(1)
2248
-
2249
- # if not self.class_name in self.df_tracks.columns:
2250
- # self.df_tracks[self.class_name] = 1
2251
- # if not self.time_name in self.df_tracks.columns:
2252
- # self.df_tracks[self.time_name] = -1
2253
- all_states = self.df_tracks.loc[:, self.status_name].tolist()
2254
- all_states = np.array(all_states)
2255
- self.state_color_map = color_from_state(all_states, recently_modified=False)
2256
- self.df_tracks['group_color'] = self.df_tracks[self.status_name].apply(self.assign_color_state)
2257
- # self.df_tracks['status_color'] = [color_from_status(i) for i in self.df_tracks[self.status_name].to_numpy()]
2258
- # self.df_tracks['class_color'] = [color_from_class(i) for i in self.df_tracks[self.class_name].to_numpy()]
2259
-
2260
- self.df_tracks = self.df_tracks.dropna(subset=['POSITION_X', 'POSITION_Y'])
2261
- self.df_tracks['x_anim'] = self.df_tracks['POSITION_X']
2262
- self.df_tracks['y_anim'] = self.df_tracks['POSITION_Y']
2263
- self.df_tracks['x_anim'] = self.df_tracks['x_anim'].astype(int)
2264
- self.df_tracks['y_anim'] = self.df_tracks['y_anim'].astype(int)
2265
-
2266
- self.extract_scatter_from_trajectories()
2267
- if 'TRACK_ID' in self.df_tracks.columns:
2268
- self.track_of_interest = self.df_tracks.dropna(subset='TRACK_ID')['TRACK_ID'].min()
2269
- else:
2270
- self.track_of_interest = self.df_tracks.dropna(subset='ID')['ID'].min()
2271
-
2272
- self.loc_t = []
2273
- self.loc_idx = []
2274
- for t in range(len(self.tracks)):
2275
- indices = np.where(self.tracks[t] == self.track_of_interest)[0]
2276
- if len(indices) > 0:
2277
- self.loc_t.append(t)
2278
- self.loc_idx.append(indices[0])
2279
-
2280
- self.MinMaxScaler = MinMaxScaler()
2281
- self.columns_to_rescale = list(self.df_tracks.columns)
2282
-
2283
- # is_number = np.vectorize(lambda x: np.issubdtype(x, np.number))
2284
- # is_number_test = is_number(self.df_tracks.dtypes)
2285
- # self.columns_to_rescale = [col for t,col in zip(is_number_test,self.df_tracks.columns) if t]
2286
- # print(self.columns_to_rescale)
2287
-
2288
- cols_to_remove = ['group', 'group_color', 'status', 'status_color', 'class_color', 'TRACK_ID', 'FRAME',
2289
- 'x_anim', 'y_anim', 't',
2290
- 'state', 'generation', 'root', 'parent', 'class_id', 'class', 't0', 'POSITION_X',
2291
- 'POSITION_Y', 'position', 'well', 'well_index', 'well_name', 'pos_name', 'index',
2292
- 'concentration', 'cell_type', 'antibody', 'pharmaceutical_agent', 'ID'] + self.class_cols
2293
-
2294
- meta = get_experiment_metadata(self.exp_dir)
2295
- if meta is not None:
2296
- keys = list(meta.keys())
2297
- cols_to_remove.extend(keys)
2298
-
2299
- labels = get_experiment_labels(self.exp_dir)
2300
- if labels is not None:
2301
- keys = list(labels.keys())
2302
- cols_to_remove.extend(labels)
2303
-
2304
- for tr in cols_to_remove:
2305
- try:
2306
- self.columns_to_rescale.remove(tr)
2307
- except:
2308
- pass
2309
-
2310
- # print(f'column {tr} could not be found...')
2311
-
2312
- x = self.df_tracks[self.columns_to_rescale].values
2313
- self.MinMaxScaler.fit(x)
2314
1525
 
2315
1526
  def extract_scatter_from_trajectories(self):
2316
1527
 
@@ -2329,10 +1540,11 @@ class MeasureAnnotator(SignalAnnotator):
2329
1540
 
2330
1541
  def changed_class(self):
2331
1542
  self.status_name = self.class_choice_cb.currentText()
2332
- self.compute_status_and_colors()
2333
- self.modify()
2334
- self.draw_frame(self.current_frame)
2335
- self.fcanvas.canvas.draw()
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()
2336
1548
 
2337
1549
  def update_frame(self):
2338
1550
  """
@@ -2348,53 +1560,14 @@ class MeasureAnnotator(SignalAnnotator):
2348
1560
  self.modify()
2349
1561
 
2350
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
+
2351
1568
  self.fcanvas.canvas.draw()
2352
1569
  self.plot_signals()
2353
1570
 
2354
- # def load_annotator_config(self):
2355
- # self.rgb_mode = False
2356
- # self.log_option = False
2357
- # self.percentile_mode = True
2358
- # self.target_channels = [[self.channel_names[0], 0.01, 99.99]]
2359
- # self.fraction = 0.5955056179775281
2360
- # #self.anim_interval = 1
2361
-
2362
- def prepare_stack(self):
2363
-
2364
- self.img_num_channels = _get_img_num_per_channel(self.channels, self.len_movie, self.nbr_channels)
2365
- self.current_stack = []
2366
- for ch in tqdm(self.target_channels, desc="channel"):
2367
- target_ch_name = ch[0]
2368
- if self.percentile_mode:
2369
- normalize_kwargs = {"percentiles": (ch[1], ch[2]), "values": None}
2370
- else:
2371
- normalize_kwargs = {"values": (ch[1], ch[2]), "percentiles": None}
2372
-
2373
- if self.rgb_mode:
2374
- normalize_kwargs.update({'amplification': 255., 'clip': True})
2375
-
2376
- chan = []
2377
- indices = self.img_num_channels[self.channels[np.where(self.channel_names == target_ch_name)][0]]
2378
- for t in tqdm(range(len(indices)), desc='frame'):
2379
- if self.rgb_mode:
2380
- f = load_frames(indices[t], self.stack_path, scale=self.fraction, normalize_input=True,
2381
- normalize_kwargs=normalize_kwargs)
2382
- f = f.astype(np.uint8)
2383
- else:
2384
- f = load_frames(indices[t], self.stack_path, scale=self.fraction, normalize_input=False)
2385
- chan.append(f[:, :, 0])
2386
-
2387
- self.current_stack.append(chan)
2388
-
2389
- self.current_stack = np.array(self.current_stack)
2390
- if self.rgb_mode:
2391
- self.current_stack = np.moveaxis(self.current_stack, 0, -1)
2392
- else:
2393
- self.current_stack = self.current_stack[0]
2394
- if self.log_option:
2395
- self.current_stack[np.where((self.current_stack > 0.)&(self.current_stack==self.current_stack))] = np.log(
2396
- self.current_stack[np.where((self.current_stack > 0.)&(self.current_stack==self.current_stack))])
2397
-
2398
1571
  def changed_channel(self):
2399
1572
 
2400
1573
  self.reload_frame()
@@ -2402,12 +1575,12 @@ class MeasureAnnotator(SignalAnnotator):
2402
1575
  *[np.nanpercentile(self.img, 0.001),
2403
1576
  np.nanpercentile(self.img, 99.999)])
2404
1577
  self.contrast_slider.setValue(
2405
- [np.nanpercentile(self.img, 1), np.nanpercentile(self.img, 99.99)])
1578
+ [np.nanpercentile(self.img, 0.1), np.nanpercentile(self.img, 99.99)])
2406
1579
  self.draw_frame(self.current_frame)
2407
1580
  self.fcanvas.canvas.draw()
2408
1581
 
2409
1582
  def save_trajectories(self):
2410
-
1583
+ print(f"Saving trajectories !!")
2411
1584
  if self.normalized_signals:
2412
1585
  self.normalize_features_btn.click()
2413
1586
  if self.selection:
@@ -2433,17 +1606,14 @@ class MeasureAnnotator(SignalAnnotator):
2433
1606
 
2434
1607
  self.df_tracks.to_csv(self.trajectories_path, index=False)
2435
1608
  print('Table successfully exported...')
2436
- self.update_frame()
2437
-
2438
-
2439
- # self.extract_scatter_from_trajectories()
1609
+
1610
+ self.locate_tracks()
1611
+ self.changed_class()
2440
1612
 
2441
1613
  def modify(self):
2442
-
2443
1614
  all_states = self.df_tracks.loc[:, self.status_name].tolist()
2444
1615
  all_states = np.array(all_states)
2445
1616
  self.state_color_map = color_from_state(all_states, recently_modified=False)
2446
- print(f'{self.state_color_map=}')
2447
1617
 
2448
1618
  self.df_tracks['group_color'] = self.df_tracks[self.status_name].apply(self.assign_color_state)
2449
1619
 
@@ -2453,84 +1623,6 @@ class MeasureAnnotator(SignalAnnotator):
2453
1623
  self.correct_btn.disconnect()
2454
1624
  self.correct_btn.clicked.connect(self.show_annotation_buttons)
2455
1625
 
2456
- # self.hide_annotation_buttons()
2457
- # self.correct_btn.setEnabled(False)
2458
- # self.correct_btn.setText('correct')
2459
- # self.cancel_btn.setEnabled(False)
2460
- # self.del_shortcut.setEnabled(False)
2461
- # self.no_event_shortcut.setEnabled(False)
2462
-
2463
- def enable_time_of_interest(self):
2464
- if self.suppr_btn.isChecked():
2465
- self.time_of_interest_le.setEnabled(False)
2466
-
2467
- def cancel_selection(self):
2468
-
2469
- self.hide_annotation_buttons()
2470
- self.correct_btn.setEnabled(False)
2471
- self.correct_btn.setText('correct')
2472
- self.cancel_btn.setEnabled(False)
2473
-
2474
- try:
2475
- self.selection.pop(0)
2476
- except Exception as e:
2477
- print('Cancel selection: ',e)
2478
-
2479
- try:
2480
- for k, (t, idx) in enumerate(zip(self.loc_t, self.loc_idx)):
2481
- # print(self.colors[t][idx, 1])
2482
- self.colors[t][idx, 0] = self.previous_color[k][0]
2483
- # self.colors[t][idx, 1] = self.previous_color[k][1]
2484
- except Exception as e:
2485
- print("cancel_selection: ",f'{e=}')
2486
-
2487
- self.event = None
2488
-
2489
- def locate_stack(self):
2490
-
2491
- """
2492
- Locate the target movie.
2493
-
2494
- """
2495
-
2496
- if isinstance(self.pos, str):
2497
- movies = glob(self.pos + os.sep.join(['movie',f"{self.parent_window.parent_window.movie_prefix}*.tif"]))
2498
-
2499
- else:
2500
- msgBox = QMessageBox()
2501
- msgBox.setIcon(QMessageBox.Warning)
2502
- msgBox.setText("Please select a unique position before launching the wizard...")
2503
- msgBox.setWindowTitle("Warning")
2504
- msgBox.setStandardButtons(QMessageBox.Ok)
2505
- returnValue = msgBox.exec()
2506
- if returnValue == QMessageBox.Ok:
2507
- self.img = None
2508
- self.close()
2509
- return None
2510
-
2511
- if len(movies) == 0:
2512
- msgBox = QMessageBox()
2513
- msgBox.setIcon(QMessageBox.Warning)
2514
- msgBox.setText("No movie is detected in the experiment folder.\nPlease check the stack prefix...")
2515
- msgBox.setWindowTitle("Warning")
2516
- msgBox.setStandardButtons(QMessageBox.Ok)
2517
- returnValue = msgBox.exec()
2518
- if returnValue == QMessageBox.Ok:
2519
- self.close()
2520
- else:
2521
- self.stack_path = movies[0]
2522
- self.len_movie = self.parent_window.parent_window.len_movie
2523
- len_movie_auto = auto_load_number_of_frames(self.stack_path)
2524
- if len_movie_auto is not None:
2525
- self.len_movie = len_movie_auto
2526
- exp_config = self.exp_dir + "config.ini"
2527
- self.channel_names, self.channels = extract_experiment_channels(self.exp_dir)
2528
- self.channel_names = np.array(self.channel_names)
2529
- self.channels = np.array(self.channels)
2530
- self.nbr_channels = len(self.channels)
2531
- self.current_channel = 0
2532
- self.img = load_frames(0, self.stack_path, normalize_input=False)
2533
-
2534
1626
  def reload_frame(self):
2535
1627
 
2536
1628
  """
@@ -2538,13 +1630,26 @@ class MeasureAnnotator(SignalAnnotator):
2538
1630
  """
2539
1631
 
2540
1632
  # self.clear_post_threshold_options()
2541
-
1633
+ self.previous_channel = self.current_channel
2542
1634
  self.current_channel = self.choose_channel.currentIndex()
2543
1635
 
2544
1636
  t = int(self.frame_slider.value())
2545
1637
  idx = t * self.nbr_channels + self.current_channel
2546
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
+
2547
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
2548
1653
  self.refresh_imshow()
2549
1654
  # self.redo_histogram()
2550
1655
  else:
@@ -2558,33 +1663,23 @@ class MeasureAnnotator(SignalAnnotator):
2558
1663
 
2559
1664
  """
2560
1665
 
2561
- self.vmin = np.nanpercentile(self.img.flatten(), 1)
2562
- self.vmax = np.nanpercentile(self.img.flatten(), 99.)
2563
-
2564
- self.contrast_slider.disconnect()
2565
- self.contrast_slider.setRange(np.nanmin(self.img), np.nanmax(self.img))
2566
- self.contrast_slider.setValue([self.vmin, self.vmax])
2567
- self.contrast_slider.valueChanged.connect(self.contrast_slider_action)
2568
-
2569
- self.im.set_data(self.img)
1666
+ if self.previous_channel != self.current_channel:
2570
1667
 
2571
- # self.im.set_clim(vmin=self.vmin, vmax=self.vmax)
2572
- # self.fcanvas.canvas.draw_idle()
1668
+ self.vmin = np.nanpercentile(self.img.flatten(), 0.1)
1669
+ self.vmax = np.nanpercentile(self.img.flatten(), 99.99)
2573
1670
 
2574
- def show_outliers(self):
2575
- if self.outliers_check.isChecked():
2576
- self.show_fliers = True
2577
- self.plot_signals()
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)
2578
1675
  else:
2579
- self.show_fliers = False
2580
- self.plot_signals()
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)
2581
1681
 
2582
1682
  def del_cell(self):
2583
1683
  self.time_of_interest_le.setEnabled(False)
2584
1684
  self.time_of_interest_le.setText("99")
2585
1685
  self.apply_modification()
2586
-
2587
- def shortcut_suppr(self):
2588
- self.correct_btn.click()
2589
- self.del_cell_btn.click()
2590
- self.correct_btn.click()