celldetective 1.0.2__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 (66) hide show
  1. celldetective/__init__.py +2 -0
  2. celldetective/__main__.py +432 -0
  3. celldetective/datasets/segmentation_annotations/blank +0 -0
  4. celldetective/datasets/signal_annotations/blank +0 -0
  5. celldetective/events.py +149 -0
  6. celldetective/extra_properties.py +100 -0
  7. celldetective/filters.py +89 -0
  8. celldetective/gui/__init__.py +20 -0
  9. celldetective/gui/about.py +44 -0
  10. celldetective/gui/analyze_block.py +563 -0
  11. celldetective/gui/btrack_options.py +898 -0
  12. celldetective/gui/classifier_widget.py +386 -0
  13. celldetective/gui/configure_new_exp.py +532 -0
  14. celldetective/gui/control_panel.py +438 -0
  15. celldetective/gui/gui_utils.py +495 -0
  16. celldetective/gui/json_readers.py +113 -0
  17. celldetective/gui/measurement_options.py +1425 -0
  18. celldetective/gui/neighborhood_options.py +452 -0
  19. celldetective/gui/plot_signals_ui.py +1042 -0
  20. celldetective/gui/process_block.py +1055 -0
  21. celldetective/gui/retrain_segmentation_model_options.py +706 -0
  22. celldetective/gui/retrain_signal_model_options.py +643 -0
  23. celldetective/gui/seg_model_loader.py +460 -0
  24. celldetective/gui/signal_annotator.py +2388 -0
  25. celldetective/gui/signal_annotator_options.py +340 -0
  26. celldetective/gui/styles.py +217 -0
  27. celldetective/gui/survival_ui.py +903 -0
  28. celldetective/gui/tableUI.py +608 -0
  29. celldetective/gui/thresholds_gui.py +1300 -0
  30. celldetective/icons/logo-large.png +0 -0
  31. celldetective/icons/logo.png +0 -0
  32. celldetective/icons/signals_icon.png +0 -0
  33. celldetective/icons/splash-test.png +0 -0
  34. celldetective/icons/splash.png +0 -0
  35. celldetective/icons/splash0.png +0 -0
  36. celldetective/icons/survival2.png +0 -0
  37. celldetective/icons/vignette_signals2.png +0 -0
  38. celldetective/icons/vignette_signals2.svg +114 -0
  39. celldetective/io.py +2050 -0
  40. celldetective/links/zenodo.json +561 -0
  41. celldetective/measure.py +1258 -0
  42. celldetective/models/segmentation_effectors/blank +0 -0
  43. celldetective/models/segmentation_generic/blank +0 -0
  44. celldetective/models/segmentation_targets/blank +0 -0
  45. celldetective/models/signal_detection/blank +0 -0
  46. celldetective/models/tracking_configs/mcf7.json +68 -0
  47. celldetective/models/tracking_configs/ricm.json +203 -0
  48. celldetective/models/tracking_configs/ricm2.json +203 -0
  49. celldetective/neighborhood.py +717 -0
  50. celldetective/scripts/analyze_signals.py +51 -0
  51. celldetective/scripts/measure_cells.py +275 -0
  52. celldetective/scripts/segment_cells.py +212 -0
  53. celldetective/scripts/segment_cells_thresholds.py +140 -0
  54. celldetective/scripts/track_cells.py +206 -0
  55. celldetective/scripts/train_segmentation_model.py +246 -0
  56. celldetective/scripts/train_signal_model.py +49 -0
  57. celldetective/segmentation.py +712 -0
  58. celldetective/signals.py +2826 -0
  59. celldetective/tracking.py +974 -0
  60. celldetective/utils.py +1681 -0
  61. celldetective-1.0.2.dist-info/LICENSE +674 -0
  62. celldetective-1.0.2.dist-info/METADATA +192 -0
  63. celldetective-1.0.2.dist-info/RECORD +66 -0
  64. celldetective-1.0.2.dist-info/WHEEL +5 -0
  65. celldetective-1.0.2.dist-info/entry_points.txt +2 -0
  66. celldetective-1.0.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,2388 @@
1
+ from PyQt5.QtWidgets import QMainWindow, QComboBox, QLabel, QRadioButton, QLineEdit, QFileDialog, QApplication, \
2
+ QPushButton, QWidget, QVBoxLayout, QHBoxLayout, QMessageBox, QAction, QShortcut, QLineEdit, QSlider, QCheckBox
3
+ from PyQt5.QtCore import Qt, QSize
4
+ from PyQt5.QtGui import QKeySequence, QIntValidator
5
+ from matplotlib.widgets import Slider
6
+ from tifffile import imread
7
+
8
+ from celldetective.gui.gui_utils import center_window, QHSeperationLine, FilterChoice, color_from_state
9
+ from superqt import QLabeledDoubleSlider, QLabeledDoubleRangeSlider, QLabeledSlider
10
+ from celldetective.utils import extract_experiment_channels, get_software_location, _get_img_num_per_channel
11
+ from celldetective.io import auto_load_number_of_frames, load_frames, locate_stack
12
+ from celldetective.gui.gui_utils import FigureCanvas, color_from_status, color_from_class
13
+ import json
14
+ import numpy as np
15
+ from superqt.fonticon import icon
16
+ from fonticon_mdi6 import MDI6
17
+ import os
18
+ from glob import glob
19
+ from natsort import natsorted
20
+ import matplotlib.pyplot as plt
21
+ from matplotlib.ticker import MultipleLocator
22
+ from tqdm import tqdm
23
+ import gc
24
+ from matplotlib.animation import FuncAnimation
25
+ from matplotlib.cm import tab10
26
+ import pandas as pd
27
+ from sklearn.preprocessing import MinMaxScaler
28
+
29
+
30
+ class SignalAnnotator(QMainWindow):
31
+ """
32
+ UI to set tracking parameters for bTrack.
33
+
34
+ """
35
+
36
+ def __init__(self, parent=None):
37
+
38
+ super().__init__()
39
+ self.parent = parent
40
+ self.setWindowTitle("Signal annotator")
41
+ self.mode = self.parent.mode
42
+ self.pos = self.parent.parent.pos
43
+ self.exp_dir = self.parent.exp_dir
44
+ self.n_signals = 3
45
+ self.soft_path = get_software_location()
46
+ self.recently_modified = False
47
+ self.selection = []
48
+ if self.mode == "targets":
49
+ self.instructions_path = self.exp_dir + "configs/signal_annotator_config_targets.json"
50
+ self.trajectories_path = self.pos + 'output/tables/trajectories_targets.csv'
51
+ elif self.mode == "effectors":
52
+ self.instructions_path = self.exp_dir + "configs/signal_annotator_config_effectors.json"
53
+ self.trajectories_path = self.pos + 'output/tables/trajectories_effectors.csv'
54
+
55
+ self.screen_height = self.parent.parent.parent.screen_height
56
+ self.screen_width = self.parent.parent.parent.screen_width
57
+
58
+ # default params
59
+ self.class_name = 'class'
60
+ self.time_name = 't0'
61
+ self.status_name = 'status'
62
+
63
+ center_window(self)
64
+
65
+ self.locate_stack()
66
+ self.load_annotator_config()
67
+ self.locate_tracks()
68
+ self.prepare_stack()
69
+
70
+ self.generate_signal_choices()
71
+ self.frame_lbl = QLabel('frame: ')
72
+ self.looped_animation()
73
+ self.create_cell_signal_canvas()
74
+
75
+ self.populate_widget()
76
+
77
+ self.setMinimumWidth(int(0.8 * self.screen_width))
78
+ # self.setMaximumHeight(int(0.8*self.screen_height))
79
+ self.setMinimumHeight(int(0.8 * self.screen_height))
80
+ # self.setMaximumHeight(int(0.8*self.screen_height))
81
+
82
+ self.setAttribute(Qt.WA_DeleteOnClose)
83
+
84
+ def populate_widget(self):
85
+
86
+ """
87
+ Create the multibox design.
88
+
89
+ """
90
+
91
+ self.button_widget = QWidget()
92
+ main_layout = QHBoxLayout()
93
+ self.button_widget.setLayout(main_layout)
94
+
95
+ main_layout.setContentsMargins(30, 30, 30, 30)
96
+ self.left_panel = QVBoxLayout()
97
+ self.left_panel.setContentsMargins(30, 30, 30, 30)
98
+ self.left_panel.setSpacing(10)
99
+
100
+ self.right_panel = QVBoxLayout()
101
+
102
+ class_hbox = QHBoxLayout()
103
+ class_hbox.addWidget(QLabel('event: '), 25)
104
+ self.class_choice_cb = QComboBox()
105
+
106
+ cols = np.array(self.df_tracks.columns)
107
+ self.class_cols = np.array([c.startswith('class') for c in list(self.df_tracks.columns)])
108
+ self.class_cols = list(cols[self.class_cols])
109
+ try:
110
+ self.class_cols.remove('class_id')
111
+ except Exception:
112
+ pass
113
+ try:
114
+ self.class_cols.remove('class_color')
115
+ except Exception:
116
+ pass
117
+
118
+ self.class_choice_cb.addItems(self.class_cols)
119
+ self.class_choice_cb.currentIndexChanged.connect(self.compute_status_and_colors)
120
+ self.class_choice_cb.setCurrentIndex(0)
121
+
122
+ class_hbox.addWidget(self.class_choice_cb, 70)
123
+
124
+ self.add_class_btn = QPushButton('')
125
+ self.add_class_btn.setStyleSheet(self.parent.parent.parent.button_select_all)
126
+ self.add_class_btn.setIcon(icon(MDI6.plus, color="black"))
127
+ self.add_class_btn.setToolTip("Add a new event class")
128
+ self.add_class_btn.setIconSize(QSize(20, 20))
129
+ self.add_class_btn.clicked.connect(self.create_new_event_class)
130
+ class_hbox.addWidget(self.add_class_btn, 5)
131
+
132
+ self.del_class_btn = QPushButton('')
133
+ self.del_class_btn.setStyleSheet(self.parent.parent.parent.button_select_all)
134
+ self.del_class_btn.setIcon(icon(MDI6.delete, color="black"))
135
+ self.del_class_btn.setToolTip("Delete an event class")
136
+ self.del_class_btn.setIconSize(QSize(20, 20))
137
+ self.del_class_btn.clicked.connect(self.del_event_class)
138
+ class_hbox.addWidget(self.del_class_btn, 5)
139
+
140
+ self.left_panel.addLayout(class_hbox)
141
+
142
+ self.cell_info = QLabel('')
143
+ self.left_panel.addWidget(self.cell_info)
144
+
145
+ # Annotation buttons
146
+ options_hbox = QHBoxLayout()
147
+ options_hbox.setContentsMargins(150, 30, 50, 0)
148
+ self.event_btn = QRadioButton('event')
149
+ self.event_btn.setStyleSheet(self.parent.parent.parent.button_style_sheet_2)
150
+ self.event_btn.toggled.connect(self.enable_time_of_interest)
151
+
152
+ self.no_event_btn = QRadioButton('no event')
153
+ self.no_event_btn.setStyleSheet(self.parent.parent.parent.button_style_sheet_2)
154
+ self.no_event_btn.toggled.connect(self.enable_time_of_interest)
155
+
156
+ self.else_btn = QRadioButton('else')
157
+ self.else_btn.setStyleSheet(self.parent.parent.parent.button_style_sheet_2)
158
+ self.else_btn.toggled.connect(self.enable_time_of_interest)
159
+
160
+ self.suppr_btn = QRadioButton('mark for\nsuppression')
161
+ self.suppr_btn.setStyleSheet(self.parent.parent.parent.button_style_sheet_2)
162
+ self.suppr_btn.toggled.connect(self.enable_time_of_interest)
163
+
164
+ options_hbox.addWidget(self.event_btn, 25)
165
+ options_hbox.addWidget(self.no_event_btn, 25)
166
+ options_hbox.addWidget(self.else_btn, 25)
167
+ options_hbox.addWidget(self.suppr_btn, 25)
168
+ self.left_panel.addLayout(options_hbox)
169
+
170
+ time_option_hbox = QHBoxLayout()
171
+ time_option_hbox.setContentsMargins(100, 30, 100, 30)
172
+ self.time_of_interest_label = QLabel('time of interest: ')
173
+ time_option_hbox.addWidget(self.time_of_interest_label, 30)
174
+ self.time_of_interest_le = QLineEdit()
175
+ time_option_hbox.addWidget(self.time_of_interest_le, 70)
176
+ self.left_panel.addLayout(time_option_hbox)
177
+
178
+ main_action_hbox = QHBoxLayout()
179
+ self.correct_btn = QPushButton('correct')
180
+ self.correct_btn.setIcon(icon(MDI6.redo_variant, color="white"))
181
+ self.correct_btn.setIconSize(QSize(20, 20))
182
+ self.correct_btn.setStyleSheet(self.parent.parent.parent.button_style_sheet)
183
+ self.correct_btn.clicked.connect(self.show_annotation_buttons)
184
+ self.correct_btn.setEnabled(False)
185
+ main_action_hbox.addWidget(self.correct_btn)
186
+
187
+ self.cancel_btn = QPushButton('cancel')
188
+ self.cancel_btn.setStyleSheet(self.parent.parent.parent.button_style_sheet_2)
189
+ self.cancel_btn.setShortcut(QKeySequence("Esc"))
190
+ self.cancel_btn.setEnabled(False)
191
+ self.cancel_btn.clicked.connect(self.cancel_selection)
192
+ main_action_hbox.addWidget(self.cancel_btn)
193
+ self.left_panel.addLayout(main_action_hbox)
194
+
195
+ self.annotation_btns_to_hide = [self.event_btn, self.no_event_btn,
196
+ self.else_btn, self.time_of_interest_label,
197
+ self.time_of_interest_le, self.suppr_btn]
198
+ self.hide_annotation_buttons()
199
+ #### End of annotation buttons
200
+
201
+ self.del_shortcut = QShortcut(Qt.Key_Delete, self) # QKeySequence("s")
202
+ self.del_shortcut.activated.connect(self.shortcut_suppr)
203
+ self.del_shortcut.setEnabled(False)
204
+
205
+ self.no_event_shortcut = QShortcut(QKeySequence("n"), self) # QKeySequence("s")
206
+ self.no_event_shortcut.activated.connect(self.shortcut_no_event)
207
+ self.no_event_shortcut.setEnabled(False)
208
+
209
+ # Cell signals
210
+ self.left_panel.addWidget(self.cell_fcanvas)
211
+
212
+ plot_buttons_hbox = QHBoxLayout()
213
+ plot_buttons_hbox.setContentsMargins(0, 0, 0, 0)
214
+ self.normalize_features_btn = QPushButton('')
215
+ self.normalize_features_btn.setStyleSheet(self.parent.parent.parent.button_select_all)
216
+ self.normalize_features_btn.setIcon(icon(MDI6.arrow_collapse_vertical, color="black"))
217
+ self.normalize_features_btn.setIconSize(QSize(25, 25))
218
+ self.normalize_features_btn.setFixedSize(QSize(30, 30))
219
+ # self.normalize_features_btn.setShortcut(QKeySequence('n'))
220
+ self.normalize_features_btn.clicked.connect(self.normalize_features)
221
+
222
+ plot_buttons_hbox.addWidget(QLabel(''), 90)
223
+ plot_buttons_hbox.addWidget(self.normalize_features_btn, 5)
224
+ self.normalized_signals = False
225
+
226
+ self.log_btn = QPushButton()
227
+ self.log_btn.setIcon(icon(MDI6.math_log, color="black"))
228
+ self.log_btn.setStyleSheet(self.parent.parent.parent.button_select_all)
229
+ self.log_btn.clicked.connect(self.switch_to_log)
230
+ plot_buttons_hbox.addWidget(self.log_btn, 5)
231
+
232
+ self.left_panel.addLayout(plot_buttons_hbox)
233
+
234
+ signal_choice_vbox = QVBoxLayout()
235
+ signal_choice_vbox.setContentsMargins(30, 0, 30, 50)
236
+ for i in range(len(self.signal_choice_cb)):
237
+ hlayout = QHBoxLayout()
238
+ hlayout.addWidget(self.signal_choice_label[i], 20)
239
+ hlayout.addWidget(self.signal_choice_cb[i], 75)
240
+ # hlayout.addWidget(self.log_btns[i], 5)
241
+ signal_choice_vbox.addLayout(hlayout)
242
+
243
+ # self.log_btns[i].setIcon(icon(MDI6.math_log,color="black"))
244
+ # self.log_btns[i].setStyleSheet(self.parent.parent.parent.button_select_all)
245
+ # self.log_btns[i].clicked.connect(lambda ch, i=i: self.switch_to_log(i))
246
+
247
+ self.left_panel.addLayout(signal_choice_vbox)
248
+
249
+ btn_hbox = QHBoxLayout()
250
+ self.save_btn = QPushButton('Save')
251
+ self.save_btn.setStyleSheet(self.parent.parent.parent.button_style_sheet)
252
+ self.save_btn.clicked.connect(self.save_trajectories)
253
+ btn_hbox.addWidget(self.save_btn, 90)
254
+
255
+ self.export_btn = QPushButton('')
256
+ self.export_btn.setStyleSheet(self.parent.parent.parent.button_select_all)
257
+ self.export_btn.clicked.connect(self.export_signals)
258
+ self.export_btn.setIcon(icon(MDI6.export, color="black"))
259
+ self.export_btn.setIconSize(QSize(25, 25))
260
+ btn_hbox.addWidget(self.export_btn, 10)
261
+ self.left_panel.addLayout(btn_hbox)
262
+
263
+ # Animation
264
+ animation_buttons_box = QHBoxLayout()
265
+
266
+ animation_buttons_box.addWidget(self.frame_lbl, 20, alignment=Qt.AlignLeft)
267
+
268
+ self.first_frame_btn = QPushButton()
269
+ self.first_frame_btn.clicked.connect(self.set_first_frame)
270
+ self.first_frame_btn.setShortcut(QKeySequence('f'))
271
+ self.first_frame_btn.setIcon(icon(MDI6.page_first, color="black"))
272
+ self.first_frame_btn.setStyleSheet(self.parent.parent.parent.button_select_all)
273
+ self.first_frame_btn.setFixedSize(QSize(60, 60))
274
+ self.first_frame_btn.setIconSize(QSize(30, 30))
275
+
276
+ self.last_frame_btn = QPushButton()
277
+ self.last_frame_btn.clicked.connect(self.set_last_frame)
278
+ self.last_frame_btn.setShortcut(QKeySequence('l'))
279
+ self.last_frame_btn.setIcon(icon(MDI6.page_last, color="black"))
280
+ self.last_frame_btn.setStyleSheet(self.parent.parent.parent.button_select_all)
281
+ self.last_frame_btn.setFixedSize(QSize(60, 60))
282
+ self.last_frame_btn.setIconSize(QSize(30, 30))
283
+
284
+ self.stop_btn = QPushButton()
285
+ self.stop_btn.clicked.connect(self.stop)
286
+ self.stop_btn.setIcon(icon(MDI6.stop, color="black"))
287
+ self.stop_btn.setStyleSheet(self.parent.parent.parent.button_select_all)
288
+ self.stop_btn.setFixedSize(QSize(60, 60))
289
+ self.stop_btn.setIconSize(QSize(30, 30))
290
+
291
+ self.start_btn = QPushButton()
292
+ self.start_btn.clicked.connect(self.start)
293
+ self.start_btn.setIcon(icon(MDI6.play, color="black"))
294
+ self.start_btn.setFixedSize(QSize(60, 60))
295
+ self.start_btn.setStyleSheet(self.parent.parent.parent.button_select_all)
296
+ self.start_btn.setIconSize(QSize(30, 30))
297
+ self.start_btn.hide()
298
+
299
+ animation_buttons_box.addWidget(self.first_frame_btn, 5, alignment=Qt.AlignRight)
300
+ animation_buttons_box.addWidget(self.stop_btn, 5, alignment=Qt.AlignRight)
301
+ animation_buttons_box.addWidget(self.start_btn, 5, alignment=Qt.AlignRight)
302
+ animation_buttons_box.addWidget(self.last_frame_btn, 5, alignment=Qt.AlignRight)
303
+
304
+ self.right_panel.addLayout(animation_buttons_box, 5)
305
+
306
+ self.right_panel.addWidget(self.fcanvas, 90)
307
+
308
+ if not self.rgb_mode:
309
+ contrast_hbox = QHBoxLayout()
310
+ contrast_hbox.setContentsMargins(150, 5, 150, 5)
311
+ self.contrast_slider = QLabeledDoubleRangeSlider()
312
+ # self.contrast_slider.setSingleStep(0.001)
313
+ # self.contrast_slider.setTickInterval(0.001)
314
+ self.contrast_slider.setOrientation(1)
315
+ print('range: ',
316
+ [np.nanpercentile(self.stack.flatten(), 0.001), np.nanpercentile(self.stack.flatten(), 99.999)])
317
+ self.contrast_slider.setRange(
318
+ *[np.nanpercentile(self.stack.flatten(), 0.001), np.nanpercentile(self.stack.flatten(), 99.999)])
319
+ self.contrast_slider.setValue(
320
+ [np.percentile(self.stack.flatten(), 1), np.percentile(self.stack.flatten(), 99.99)])
321
+ self.contrast_slider.valueChanged.connect(self.contrast_slider_action)
322
+ contrast_hbox.addWidget(QLabel('contrast: '))
323
+ contrast_hbox.addWidget(self.contrast_slider, 90)
324
+ self.right_panel.addLayout(contrast_hbox, 5)
325
+
326
+ # speed_hbox = QHBoxLayout()
327
+ # speed_hbox.setContentsMargins(150,5,150,5)
328
+ # self.interval_slider = QLabeledSlider()
329
+ # self.interval_slider.setSingleStep(1)
330
+ # self.interval_slider.setTickInterval(1)
331
+ # self.interval_slider.setOrientation(1)
332
+ # self.interval_slider.setRange(1, 10000)
333
+ # self.interval_slider.setValue(self.speed)
334
+ # self.interval_slider.valueChanged.connect(self.interval_slider_action)
335
+ # speed_hbox.addWidget(QLabel('interval (ms): '))
336
+ # speed_hbox.addWidget(self.interval_slider,90)
337
+ # self.right_panel.addLayout(speed_hbox, 10)
338
+
339
+ # self.populate_left_panel()
340
+ # grid.addLayout(self.left_side, 0, 0, 1, 1)
341
+
342
+ main_layout.addLayout(self.left_panel, 35)
343
+ main_layout.addLayout(self.right_panel, 65)
344
+ self.button_widget.adjustSize()
345
+
346
+ self.setCentralWidget(self.button_widget)
347
+ self.show()
348
+
349
+ QApplication.processEvents()
350
+
351
+ def del_event_class(self):
352
+
353
+ msgBox = QMessageBox()
354
+ msgBox.setIcon(QMessageBox.Warning)
355
+ msgBox.setText(
356
+ 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?")
357
+ msgBox.setWindowTitle("Warning")
358
+ msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
359
+ returnValue = msgBox.exec()
360
+ if returnValue == QMessageBox.No:
361
+ return None
362
+ else:
363
+ class_to_delete = self.class_choice_cb.currentText()
364
+ time_to_delete = class_to_delete.replace('class', 't')
365
+ status_to_delete = class_to_delete.replace('class', 'status')
366
+ cols_to_delete = [class_to_delete, time_to_delete, status_to_delete]
367
+ for c in cols_to_delete:
368
+ try:
369
+ self.df_tracks = self.df_tracks.drop([c], axis=1)
370
+ except Exception as e:
371
+ print(e)
372
+ item_idx = self.class_choice_cb.findText(class_to_delete)
373
+ self.class_choice_cb.removeItem(item_idx)
374
+
375
+ def create_new_event_class(self):
376
+
377
+ # display qwidget to name the event
378
+ self.newClassWidget = QWidget()
379
+ self.newClassWidget.setWindowTitle('Create new event class')
380
+
381
+ layout = QVBoxLayout()
382
+ self.newClassWidget.setLayout(layout)
383
+ name_hbox = QHBoxLayout()
384
+ name_hbox.addWidget(QLabel('event name: '), 25)
385
+ self.class_name_le = QLineEdit('event')
386
+ name_hbox.addWidget(self.class_name_le, 75)
387
+ layout.addLayout(name_hbox)
388
+
389
+ class_labels = ['event', 'no event', 'else']
390
+ layout.addWidget(QLabel('prefill: '))
391
+ radio_box = QHBoxLayout()
392
+ self.class_option_rb = [QRadioButton() for i in range(3)]
393
+ for i, c in enumerate(self.class_option_rb):
394
+ if i == 0:
395
+ c.setChecked(True)
396
+ c.setText(class_labels[i])
397
+ radio_box.addWidget(c, 33, alignment=Qt.AlignCenter)
398
+ layout.addLayout(radio_box)
399
+
400
+ btn_hbox = QHBoxLayout()
401
+ submit_btn = QPushButton('submit')
402
+ cancel_btn = QPushButton('cancel')
403
+ btn_hbox.addWidget(cancel_btn, 50)
404
+ btn_hbox.addWidget(submit_btn, 50)
405
+ layout.addLayout(btn_hbox)
406
+
407
+ submit_btn.clicked.connect(self.write_new_event_class)
408
+ cancel_btn.clicked.connect(self.close_without_new_class)
409
+
410
+ self.newClassWidget.show()
411
+ center_window(self.newClassWidget)
412
+
413
+ # Prefill with class value
414
+ # write in table
415
+
416
+ def write_new_event_class(self):
417
+
418
+ if self.class_name_le.text() == '':
419
+ self.target_class = 'class'
420
+ self.target_time = 't0'
421
+ else:
422
+ self.target_class = 'class_' + self.class_name_le.text()
423
+ self.target_time = 't_' + self.class_name_le.text()
424
+
425
+ if self.target_class in list(self.df_tracks.columns):
426
+
427
+ msgBox = QMessageBox()
428
+ msgBox.setIcon(QMessageBox.Warning)
429
+ msgBox.setText(
430
+ "This event name already exists. If you proceed,\nall annotated data will be rewritten. Do you wish to continue?")
431
+ msgBox.setWindowTitle("Warning")
432
+ msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
433
+ returnValue = msgBox.exec()
434
+ if returnValue == QMessageBox.No:
435
+ return None
436
+ else:
437
+ pass
438
+
439
+ fill_option = np.where([c.isChecked() for c in self.class_option_rb])[0][0]
440
+ self.df_tracks.loc[:, self.target_class] = fill_option
441
+ if fill_option == 0:
442
+ self.df_tracks.loc[:, self.target_time] = 0.1
443
+ else:
444
+ self.df_tracks.loc[:, self.target_time] = -1
445
+
446
+ self.class_choice_cb.clear()
447
+ cols = np.array(self.df_tracks.columns)
448
+ self.class_cols = np.array([c.startswith('class') for c in list(self.df_tracks.columns)])
449
+ self.class_cols = list(cols[self.class_cols])
450
+ self.class_cols.remove('class_id')
451
+ self.class_cols.remove('class_color')
452
+ self.class_choice_cb.addItems(self.class_cols)
453
+ idx = self.class_choice_cb.findText(self.target_class)
454
+ self.class_choice_cb.setCurrentIndex(idx)
455
+
456
+ self.newClassWidget.close()
457
+
458
+ def close_without_new_class(self):
459
+ self.newClassWidget.close()
460
+
461
+ def compute_status_and_colors(self, i):
462
+ self.class_name = self.class_choice_cb.currentText()
463
+ self.expected_status = 'status'
464
+ suffix = self.class_name.replace('class', '').replace('_', '', 1)
465
+ if suffix != '':
466
+ self.expected_status += '_' + suffix
467
+ self.expected_time = 't_' + suffix
468
+ else:
469
+ self.expected_time = 't0'
470
+ self.time_name = self.expected_time
471
+ self.status_name = self.expected_status
472
+
473
+ print('selection and expected names: ', self.class_name, self.expected_time, self.expected_status)
474
+
475
+ 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:
476
+ # only create the status column if it does not exist to not erase static classification results
477
+ self.make_status_column()
478
+ elif self.time_name in self.df_tracks.columns and self.class_name in self.df_tracks.columns:
479
+ # all good, do nothing
480
+ pass
481
+ else:
482
+ if not self.status_name in self.df_tracks.columns:
483
+ self.df_tracks[self.status_name] = 0
484
+ self.df_tracks['status_color'] = color_from_status(0)
485
+ self.df_tracks['class_color'] = color_from_class(1)
486
+
487
+ if not self.class_name in self.df_tracks.columns:
488
+ self.df_tracks[self.class_name] = 1
489
+ if not self.time_name in self.df_tracks.columns:
490
+ self.df_tracks[self.time_name] = -1
491
+
492
+ self.df_tracks['status_color'] = [color_from_status(i) for i in self.df_tracks[self.status_name].to_numpy()]
493
+ self.df_tracks['class_color'] = [color_from_class(i) for i in self.df_tracks[self.class_name].to_numpy()]
494
+
495
+ self.extract_scatter_from_trajectories()
496
+
497
+ def contrast_slider_action(self):
498
+
499
+ """
500
+ Recontrast the imshow as the contrast slider is moved.
501
+ """
502
+
503
+ self.vmin = self.contrast_slider.value()[0]
504
+ self.vmax = self.contrast_slider.value()[1]
505
+ self.im.set_clim(vmin=self.vmin, vmax=self.vmax)
506
+ self.fcanvas.canvas.draw_idle()
507
+
508
+ def cancel_selection(self):
509
+
510
+ self.hide_annotation_buttons()
511
+ self.correct_btn.setEnabled(False)
512
+ self.correct_btn.setText('correct')
513
+ self.cancel_btn.setEnabled(False)
514
+
515
+ try:
516
+ self.selection.pop(0)
517
+ except Exception as e:
518
+ print(e)
519
+
520
+ try:
521
+ for k, (t, idx) in enumerate(zip(self.loc_t, self.loc_idx)):
522
+ self.colors[t][idx, 0] = self.previous_color[k][0]
523
+ self.colors[t][idx, 1] = self.previous_color[k][1]
524
+ except Exception as e:
525
+ print(f'{e=}')
526
+
527
+ def hide_annotation_buttons(self):
528
+
529
+ for a in self.annotation_btns_to_hide:
530
+ a.hide()
531
+ for b in [self.event_btn, self.no_event_btn, self.else_btn, self.suppr_btn]:
532
+ b.setChecked(False)
533
+ self.time_of_interest_label.setEnabled(False)
534
+ self.time_of_interest_le.setText('')
535
+ self.time_of_interest_le.setEnabled(False)
536
+
537
+ def enable_time_of_interest(self):
538
+
539
+ if self.event_btn.isChecked():
540
+ self.time_of_interest_label.setEnabled(True)
541
+ self.time_of_interest_le.setEnabled(True)
542
+ else:
543
+ self.time_of_interest_label.setEnabled(False)
544
+ self.time_of_interest_le.setEnabled(False)
545
+
546
+ def show_annotation_buttons(self):
547
+
548
+ for a in self.annotation_btns_to_hide:
549
+ a.show()
550
+
551
+ cclass = self.df_tracks.loc[self.df_tracks['TRACK_ID'] == self.track_of_interest, self.class_name].to_numpy()[0]
552
+ t0 = self.df_tracks.loc[self.df_tracks['TRACK_ID'] == self.track_of_interest, self.time_name].to_numpy()[0]
553
+
554
+ if cclass == 0:
555
+ self.event_btn.setChecked(True)
556
+ self.time_of_interest_le.setText(str(t0))
557
+ elif cclass == 1:
558
+ self.no_event_btn.setChecked(True)
559
+ elif cclass == 2:
560
+ self.else_btn.setChecked(True)
561
+ elif cclass > 2:
562
+ self.suppr_btn.setChecked(True)
563
+
564
+ self.enable_time_of_interest()
565
+ self.correct_btn.setText('submit')
566
+
567
+ self.correct_btn.disconnect()
568
+ self.correct_btn.clicked.connect(self.apply_modification)
569
+
570
+ def apply_modification(self):
571
+
572
+ t0 = -1
573
+ if self.event_btn.isChecked():
574
+ cclass = 0
575
+ try:
576
+ t0 = float(self.time_of_interest_le.text().replace(',', '.'))
577
+ self.line_dt.set_xdata([t0, t0])
578
+ self.cell_fcanvas.canvas.draw_idle()
579
+ except Exception as e:
580
+ print(e)
581
+ t0 = -1
582
+ cclass = 2
583
+ elif self.no_event_btn.isChecked():
584
+ cclass = 1
585
+ elif self.else_btn.isChecked():
586
+ cclass = 2
587
+ elif self.suppr_btn.isChecked():
588
+ cclass = 42
589
+
590
+ self.df_tracks.loc[self.df_tracks['TRACK_ID'] == self.track_of_interest, self.class_name] = cclass
591
+ self.df_tracks.loc[self.df_tracks['TRACK_ID'] == self.track_of_interest, self.time_name] = t0
592
+
593
+ indices = self.df_tracks.loc[self.df_tracks['TRACK_ID'] == self.track_of_interest, self.class_name].index
594
+ timeline = self.df_tracks.loc[self.df_tracks['TRACK_ID'] == self.track_of_interest, 'FRAME'].to_numpy()
595
+ status = np.zeros_like(timeline)
596
+ if t0 > 0:
597
+ status[timeline >= t0] = 1.
598
+ if cclass == 2:
599
+ status[:] = 2
600
+ if cclass > 2:
601
+ status[:] = 42
602
+ status_color = [color_from_status(s, recently_modified=True) for s in status]
603
+ class_color = [color_from_class(cclass, recently_modified=True) for i in range(len(status))]
604
+
605
+ self.df_tracks.loc[indices, self.status_name] = status
606
+ self.df_tracks.loc[indices, 'status_color'] = status_color
607
+ self.df_tracks.loc[indices, 'class_color'] = class_color
608
+
609
+ # self.make_status_column()
610
+ self.extract_scatter_from_trajectories()
611
+ self.give_cell_information()
612
+
613
+ self.correct_btn.disconnect()
614
+ self.correct_btn.clicked.connect(self.show_annotation_buttons)
615
+ # self.cancel_btn.click()
616
+
617
+ self.hide_annotation_buttons()
618
+ self.correct_btn.setEnabled(False)
619
+ self.correct_btn.setText('correct')
620
+ self.cancel_btn.setEnabled(False)
621
+ self.del_shortcut.setEnabled(False)
622
+ self.no_event_shortcut.setEnabled(False)
623
+
624
+ self.selection.pop(0)
625
+
626
+ # self.fcanvas.canvas.draw()
627
+
628
+ def locate_stack(self):
629
+
630
+ """
631
+ Locate the target movie.
632
+
633
+ """
634
+
635
+ movies = glob(self.pos + f"movie/{self.parent.parent.movie_prefix}*.tif")
636
+
637
+ if len(movies) == 0:
638
+ msgBox = QMessageBox()
639
+ msgBox.setIcon(QMessageBox.Warning)
640
+ msgBox.setText("No movies are detected in the experiment folder. Cannot load an image to test Haralick.")
641
+ msgBox.setWindowTitle("Warning")
642
+ msgBox.setStandardButtons(QMessageBox.Ok)
643
+ returnValue = msgBox.exec()
644
+ if returnValue == QMessageBox.Yes:
645
+ self.close()
646
+ else:
647
+ self.stack_path = movies[0]
648
+ self.len_movie = self.parent.parent.len_movie
649
+ len_movie_auto = auto_load_number_of_frames(self.stack_path)
650
+ if len_movie_auto is not None:
651
+ self.len_movie = len_movie_auto
652
+ exp_config = self.exp_dir + "config.ini"
653
+ self.channel_names, self.channels = extract_experiment_channels(exp_config)
654
+ self.channel_names = np.array(self.channel_names)
655
+ self.channels = np.array(self.channels)
656
+ self.nbr_channels = len(self.channels)
657
+
658
+ def locate_tracks(self):
659
+
660
+ """
661
+ Locate the tracks.
662
+ """
663
+
664
+ if not os.path.exists(self.trajectories_path):
665
+
666
+ msgBox = QMessageBox()
667
+ msgBox.setIcon(QMessageBox.Warning)
668
+ msgBox.setText("The trajectories cannot be detected.")
669
+ msgBox.setWindowTitle("Warning")
670
+ msgBox.setStandardButtons(QMessageBox.Ok)
671
+ returnValue = msgBox.exec()
672
+ if returnValue == QMessageBox.Yes:
673
+ self.close()
674
+ else:
675
+
676
+ # Load and prep tracks
677
+ self.df_tracks = pd.read_csv(self.trajectories_path)
678
+ self.df_tracks = self.df_tracks.sort_values(by=['TRACK_ID', 'FRAME'])
679
+
680
+ cols = np.array(self.df_tracks.columns)
681
+ self.class_cols = np.array([c.startswith('class') for c in list(self.df_tracks.columns)])
682
+ self.class_cols = list(cols[self.class_cols])
683
+ try:
684
+ self.class_cols.remove('class_id')
685
+ except:
686
+ pass
687
+ try:
688
+ self.class_cols.remove('class_color')
689
+ except:
690
+ pass
691
+ if len(self.class_cols) > 0:
692
+ self.class_name = self.class_cols[0]
693
+ self.expected_status = 'status'
694
+ suffix = self.class_name.replace('class', '').replace('_', '')
695
+ if suffix != '':
696
+ self.expected_status += '_' + suffix
697
+ self.expected_time = 't_' + suffix
698
+ else:
699
+ self.expected_time = 't0'
700
+ self.time_name = self.expected_time
701
+ self.status_name = self.expected_status
702
+ else:
703
+ self.class_name = 'class'
704
+ self.time_name = 't0'
705
+ self.status_name = 'status'
706
+
707
+ 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:
708
+ # only create the status column if it does not exist to not erase static classification results
709
+ self.make_status_column()
710
+ elif self.time_name in self.df_tracks.columns and self.class_name in self.df_tracks.columns:
711
+ # all good, do nothing
712
+ pass
713
+ else:
714
+ if not self.status_name in self.df_tracks.columns:
715
+ self.df_tracks[self.status_name] = 0
716
+ self.df_tracks['status_color'] = color_from_status(0)
717
+ self.df_tracks['class_color'] = color_from_class(1)
718
+
719
+ if not self.class_name in self.df_tracks.columns:
720
+ self.df_tracks[self.class_name] = 1
721
+ if not self.time_name in self.df_tracks.columns:
722
+ self.df_tracks[self.time_name] = -1
723
+
724
+ self.df_tracks['status_color'] = [color_from_status(i) for i in self.df_tracks[self.status_name].to_numpy()]
725
+ self.df_tracks['class_color'] = [color_from_class(i) for i in self.df_tracks[self.class_name].to_numpy()]
726
+
727
+ self.df_tracks = self.df_tracks.dropna(subset=['POSITION_X', 'POSITION_Y'])
728
+ self.df_tracks['x_anim'] = self.df_tracks['POSITION_X'] * self.fraction
729
+ self.df_tracks['y_anim'] = self.df_tracks['POSITION_Y'] * self.fraction
730
+ self.df_tracks['x_anim'] = self.df_tracks['x_anim'].astype(int)
731
+ self.df_tracks['y_anim'] = self.df_tracks['y_anim'].astype(int)
732
+
733
+ self.extract_scatter_from_trajectories()
734
+ self.track_of_interest = self.df_tracks['TRACK_ID'].min()
735
+
736
+ self.loc_t = []
737
+ self.loc_idx = []
738
+ for t in range(len(self.tracks)):
739
+ indices = np.where(self.tracks[t] == self.track_of_interest)[0]
740
+ if len(indices) > 0:
741
+ self.loc_t.append(t)
742
+ self.loc_idx.append(indices[0])
743
+
744
+ self.MinMaxScaler = MinMaxScaler()
745
+ self.columns_to_rescale = list(self.df_tracks.columns)
746
+
747
+ # is_number = np.vectorize(lambda x: np.issubdtype(x, np.number))
748
+ # is_number_test = is_number(self.df_tracks.dtypes)
749
+ # self.columns_to_rescale = [col for t,col in zip(is_number_test,self.df_tracks.columns) if t]
750
+ # print(self.columns_to_rescale)
751
+
752
+ cols_to_remove = ['status', 'status_color', 'class_color', 'TRACK_ID', 'FRAME', 'x_anim', 'y_anim', 't',
753
+ 'state', 'generation', 'root', 'parent', 'class_id', 'class', 't0', 'POSITION_X',
754
+ 'POSITION_Y', 'position', 'well', 'well_index', 'well_name', 'pos_name', 'index',
755
+ 'concentration', 'cell_type', 'antibody', 'pharmaceutical_agent'] + self.class_cols
756
+ cols = np.array(list(self.df_tracks.columns))
757
+ time_cols = np.array([c.startswith('t_') for c in cols])
758
+ time_cols = list(cols[time_cols])
759
+ cols_to_remove += time_cols
760
+
761
+ for tr in cols_to_remove:
762
+ try:
763
+ self.columns_to_rescale.remove(tr)
764
+ except:
765
+ pass
766
+ # print(f'column {tr} could not be found...')
767
+
768
+ x = self.df_tracks[self.columns_to_rescale].values
769
+ self.MinMaxScaler.fit(x)
770
+
771
+ # self.loc_t, self.loc_idx = np.where(self.tracks==self.track_of_interest)
772
+
773
+ def make_status_column(self):
774
+ print(self.class_name, self.time_name, self.status_name)
775
+ print('remaking the status column')
776
+ for tid, group in self.df_tracks.groupby('TRACK_ID'):
777
+
778
+ indices = group.index
779
+ t0 = group[self.time_name].to_numpy()[0]
780
+ cclass = group[self.class_name].to_numpy()[0]
781
+ timeline = group['FRAME'].to_numpy()
782
+ status = np.zeros_like(timeline)
783
+ if t0 > 0:
784
+ status[timeline >= t0] = 1.
785
+ if cclass == 2:
786
+ status[:] = 2
787
+ if cclass > 2:
788
+ status[:] = 42
789
+ status_color = [color_from_status(s) for s in status]
790
+ class_color = [color_from_class(cclass) for i in range(len(status))]
791
+
792
+ self.df_tracks.loc[indices, self.status_name] = status
793
+ self.df_tracks.loc[indices, 'status_color'] = status_color
794
+ self.df_tracks.loc[indices, 'class_color'] = class_color
795
+
796
+ def generate_signal_choices(self):
797
+
798
+ self.signal_choice_cb = [QComboBox() for i in range(self.n_signals)]
799
+ self.signal_choice_label = [QLabel(f'signal {i + 1}: ') for i in range(self.n_signals)]
800
+ # self.log_btns = [QPushButton() for i in range(self.n_signals)]
801
+
802
+ signals = list(self.df_tracks.columns)
803
+ print(signals)
804
+ to_remove = ['TRACK_ID', 'FRAME', 'x_anim', 'y_anim', 't', 'state', 'generation', 'root', 'parent', 'class_id',
805
+ 'class', 't0', 'POSITION_X', 'POSITION_Y', 'position', 'well', 'well_index', 'well_name',
806
+ 'pos_name', 'index','class_color','status_color']
807
+
808
+ for c in to_remove:
809
+ if c in signals:
810
+ signals.remove(c)
811
+
812
+ for i in range(len(self.signal_choice_cb)):
813
+ self.signal_choice_cb[i].addItems(['--'] + signals)
814
+ self.signal_choice_cb[i].setCurrentIndex(i + 1)
815
+ self.signal_choice_cb[i].currentIndexChanged.connect(self.plot_signals)
816
+
817
+ def plot_signals(self):
818
+
819
+ try:
820
+ yvalues = []
821
+ for i in range(len(self.signal_choice_cb)):
822
+
823
+ signal_choice = self.signal_choice_cb[i].currentText()
824
+ self.lines[i].set_label(signal_choice)
825
+
826
+ if signal_choice == "--":
827
+ self.lines[i].set_xdata([])
828
+ self.lines[i].set_ydata([])
829
+ else:
830
+ print(f'plot signal {signal_choice} for cell {self.track_of_interest}')
831
+ xdata = self.df_tracks.loc[self.df_tracks['TRACK_ID'] == self.track_of_interest, 'FRAME'].to_numpy()
832
+ ydata = self.df_tracks.loc[
833
+ self.df_tracks['TRACK_ID'] == self.track_of_interest, signal_choice].to_numpy()
834
+
835
+ xdata = xdata[ydata == ydata] # remove nan
836
+ ydata = ydata[ydata == ydata]
837
+
838
+ yvalues.extend(ydata)
839
+ self.lines[i].set_xdata(xdata)
840
+ self.lines[i].set_ydata(ydata)
841
+ self.lines[i].set_color(tab10(i / 3.))
842
+
843
+ self.configure_ylims()
844
+
845
+ min_val, max_val = self.cell_ax.get_ylim()
846
+ t0 = \
847
+ self.df_tracks.loc[self.df_tracks['TRACK_ID'] == self.track_of_interest, self.expected_time].to_numpy()[
848
+ 0]
849
+ self.line_dt.set_xdata([t0, t0])
850
+ self.line_dt.set_ydata([min_val, max_val])
851
+
852
+ self.cell_ax.legend()
853
+ self.cell_fcanvas.canvas.draw()
854
+ except Exception as e:
855
+ print(f"{e=}")
856
+
857
+ def extract_scatter_from_trajectories(self):
858
+
859
+ self.positions = []
860
+ self.colors = []
861
+ self.tracks = []
862
+
863
+ for t in np.arange(self.len_movie):
864
+ self.positions.append(self.df_tracks.loc[self.df_tracks['FRAME'] == t, ['x_anim', 'y_anim']].to_numpy())
865
+ self.colors.append(
866
+ self.df_tracks.loc[self.df_tracks['FRAME'] == t, ['class_color', 'status_color']].to_numpy())
867
+ self.tracks.append(self.df_tracks.loc[self.df_tracks['FRAME'] == t, 'TRACK_ID'].to_numpy())
868
+
869
+ def load_annotator_config(self):
870
+
871
+ """
872
+ Load settings from config or set default values.
873
+ """
874
+
875
+ print('Reading instructions..')
876
+ if os.path.exists(self.instructions_path):
877
+ with open(self.instructions_path, 'r') as f:
878
+
879
+ instructions = json.load(f)
880
+ print(f'Reading instructions: {instructions}')
881
+
882
+ if 'rgb_mode' in instructions:
883
+ self.rgb_mode = instructions['rgb_mode']
884
+ else:
885
+ self.rgb_mode = False
886
+
887
+ if 'percentile_mode' in instructions:
888
+ self.percentile_mode = instructions['percentile_mode']
889
+ else:
890
+ self.percentile_mode = True
891
+
892
+ if 'channels' in instructions:
893
+ self.target_channels = instructions['channels']
894
+ else:
895
+ self.target_channels = [[self.channel_names[0], 0.01, 99.99]]
896
+
897
+ if 'fraction' in instructions:
898
+ self.fraction = float(instructions['fraction'])
899
+ else:
900
+ self.fraction = 0.25
901
+
902
+ if 'interval' in instructions:
903
+ self.anim_interval = int(instructions['interval'])
904
+ else:
905
+ self.anim_interval = 1
906
+
907
+ if 'log' in instructions:
908
+ self.log_option = instructions['log']
909
+ else:
910
+ self.log_option = False
911
+ else:
912
+ self.rgb_mode = False
913
+ self.log_option = False
914
+ self.percentile_mode = True
915
+ self.target_channels = [[self.channel_names[0], 0.01, 99.99]]
916
+ self.fraction = 0.25
917
+ self.anim_interval = 1
918
+
919
+ def prepare_stack(self):
920
+
921
+ self.img_num_channels = _get_img_num_per_channel(self.channels, self.len_movie, self.nbr_channels)
922
+ self.stack = []
923
+ for ch in tqdm(self.target_channels, desc="channel"):
924
+ target_ch_name = ch[0]
925
+ if self.percentile_mode:
926
+ normalize_kwargs = {"percentiles": (ch[1], ch[2]), "values": None}
927
+ else:
928
+ normalize_kwargs = {"values": (ch[1], ch[2]), "percentiles": None}
929
+
930
+ if self.rgb_mode:
931
+ normalize_kwargs.update({'amplification': 255., 'clip': True})
932
+
933
+ chan = []
934
+ indices = self.img_num_channels[self.channels[np.where(self.channel_names == target_ch_name)][0]]
935
+ for t in tqdm(range(len(indices)), desc='frame'):
936
+ if self.rgb_mode:
937
+ f = load_frames(indices[t], self.stack_path, scale=self.fraction, normalize_input=True,
938
+ normalize_kwargs=normalize_kwargs)
939
+ f = f.astype(np.uint8)
940
+ else:
941
+ f = load_frames(indices[t], self.stack_path, scale=self.fraction, normalize_input=False)
942
+ chan.append(f[:, :, 0])
943
+
944
+ self.stack.append(chan)
945
+
946
+ self.stack = np.array(self.stack)
947
+ if self.rgb_mode:
948
+ self.stack = np.moveaxis(self.stack, 0, -1)
949
+ else:
950
+ self.stack = self.stack[0]
951
+ if self.log_option:
952
+ self.stack[np.where(self.stack > 0.)] = np.log(self.stack[np.where(self.stack > 0.)])
953
+
954
+ print(f'Load stack of shape: {self.stack.shape}.')
955
+
956
+ def closeEvent(self, event):
957
+
958
+ self.stop()
959
+ # result = QMessageBox.question(self,
960
+ # "Confirm Exit...",
961
+ # "Are you sure you want to exit ?",
962
+ # QMessageBox.Yes| QMessageBox.No,
963
+ # )
964
+ del self.stack
965
+ gc.collect()
966
+
967
+ def looped_animation(self):
968
+
969
+ """
970
+ Load an image.
971
+
972
+ """
973
+
974
+ self.framedata = 0
975
+
976
+ self.fig, self.ax = plt.subplots(tight_layout=True)
977
+ self.fcanvas = FigureCanvas(self.fig, interactive=True)
978
+ self.ax.clear()
979
+
980
+ self.im = self.ax.imshow(self.stack[0], cmap='gray')
981
+ self.status_scatter = self.ax.scatter(self.positions[0][:, 0], self.positions[0][:, 1], marker="x",
982
+ c=self.colors[0][:, 1], s=50, picker=True, pickradius=100)
983
+ self.class_scatter = self.ax.scatter(self.positions[0][:, 0], self.positions[0][:, 1], marker='o',
984
+ facecolors='none', edgecolors=self.colors[0][:, 0], s=200)
985
+
986
+ self.ax.set_xticks([])
987
+ self.ax.set_yticks([])
988
+ self.ax.set_aspect('equal')
989
+
990
+ self.fig.set_facecolor('none') # or 'None'
991
+ self.fig.canvas.setStyleSheet("background-color: black;")
992
+
993
+ self.anim = FuncAnimation(
994
+ self.fig,
995
+ self.draw_frame,
996
+ frames=self.len_movie, # better would be to cast np.arange(len(movie)) in case frame column is incomplete
997
+ interval=self.anim_interval, # in ms
998
+ blit=True,
999
+ )
1000
+
1001
+ self.fig.canvas.mpl_connect('pick_event', self.on_scatter_pick)
1002
+ self.fcanvas.canvas.draw()
1003
+
1004
+ def create_cell_signal_canvas(self):
1005
+
1006
+ self.cell_fig, self.cell_ax = plt.subplots()
1007
+ self.cell_fcanvas = FigureCanvas(self.cell_fig, interactive=False)
1008
+ self.cell_ax.clear()
1009
+
1010
+ spacing = 0.5
1011
+ minorLocator = MultipleLocator(1)
1012
+ self.cell_ax.xaxis.set_minor_locator(minorLocator)
1013
+ self.cell_ax.xaxis.set_major_locator(MultipleLocator(5))
1014
+ self.cell_ax.grid(which='major')
1015
+ self.cell_ax.set_xlabel("time [frame]")
1016
+ self.cell_ax.set_ylabel("signal")
1017
+
1018
+ self.cell_fig.set_facecolor('none') # or 'None'
1019
+ self.cell_fig.canvas.setStyleSheet("background-color: transparent;")
1020
+
1021
+ self.lines = [
1022
+ self.cell_ax.plot([np.linspace(0, self.len_movie - 1, self.len_movie)], [np.zeros((self.len_movie))])[0] for
1023
+ i in range(len(self.signal_choice_cb))]
1024
+ for i in range(len(self.lines)):
1025
+ self.lines[i].set_label(f'signal {i}')
1026
+
1027
+ min_val, max_val = self.cell_ax.get_ylim()
1028
+ self.line_dt, = self.cell_ax.plot([-1, -1], [min_val, max_val], c="k", linestyle="--")
1029
+
1030
+ self.cell_ax.set_xlim(0, self.len_movie)
1031
+ self.cell_ax.legend()
1032
+ self.cell_fcanvas.canvas.draw()
1033
+
1034
+ self.plot_signals()
1035
+
1036
+ def on_scatter_pick(self, event):
1037
+
1038
+ ind = event.ind
1039
+
1040
+ if len(ind) > 1:
1041
+ # More than one point in vicinity
1042
+ datax, datay = [self.positions[self.framedata][i, 0] for i in ind], [self.positions[self.framedata][i, 1]
1043
+ for i in ind]
1044
+ msx, msy = event.mouseevent.xdata, event.mouseevent.ydata
1045
+ dist = np.sqrt((np.array(datax) - msx) ** 2 + (np.array(datay) - msy) ** 2)
1046
+ ind = [ind[np.argmin(dist)]]
1047
+
1048
+ if len(ind) > 0 and (len(self.selection) == 0):
1049
+ ind = ind[0]
1050
+ self.selection.append(ind)
1051
+ self.correct_btn.setEnabled(True)
1052
+ self.cancel_btn.setEnabled(True)
1053
+ self.del_shortcut.setEnabled(True)
1054
+ self.no_event_shortcut.setEnabled(True)
1055
+
1056
+ self.track_of_interest = self.tracks[self.framedata][ind]
1057
+ print(f'You selected track {self.track_of_interest}.')
1058
+ self.give_cell_information()
1059
+ self.plot_signals()
1060
+
1061
+ self.loc_t = []
1062
+ self.loc_idx = []
1063
+ for t in range(len(self.tracks)):
1064
+ indices = np.where(self.tracks[t] == self.track_of_interest)[0]
1065
+ if len(indices) > 0:
1066
+ self.loc_t.append(t)
1067
+ self.loc_idx.append(indices[0])
1068
+
1069
+ self.previous_color = []
1070
+ for t, idx in zip(self.loc_t, self.loc_idx):
1071
+ self.previous_color.append(self.colors[t][idx].copy())
1072
+ self.colors[t][idx] = 'lime'
1073
+
1074
+ elif len(ind) > 0 and len(self.selection) == 1:
1075
+ self.cancel_btn.click()
1076
+ else:
1077
+ pass
1078
+
1079
+ def shortcut_suppr(self):
1080
+ self.correct_btn.click()
1081
+ self.suppr_btn.click()
1082
+ self.correct_btn.click()
1083
+
1084
+ def shortcut_no_event(self):
1085
+ self.correct_btn.click()
1086
+ self.no_event_btn.click()
1087
+ self.correct_btn.click()
1088
+
1089
+ def configure_ylims(self):
1090
+
1091
+ try:
1092
+ min_values = []
1093
+ max_values = []
1094
+ for i in range(len(self.signal_choice_cb)):
1095
+ signal = self.signal_choice_cb[i].currentText()
1096
+ if signal == '--':
1097
+ continue
1098
+ else:
1099
+ maxx = np.nanpercentile(self.df_tracks.loc[:, signal].to_numpy().flatten(), 99)
1100
+ minn = np.nanpercentile(self.df_tracks.loc[:, signal].to_numpy().flatten(), 1)
1101
+ min_values.append(minn)
1102
+ max_values.append(maxx)
1103
+
1104
+ if len(min_values) > 0:
1105
+ self.cell_ax.set_ylim(np.amin(min_values), np.amax(max_values))
1106
+ except Exception as e:
1107
+ print(e)
1108
+
1109
+ def draw_frame(self, framedata):
1110
+
1111
+ """
1112
+ Update plot elements at each timestep of the loop.
1113
+ """
1114
+
1115
+ self.framedata = framedata
1116
+ self.frame_lbl.setText(f'frame: {self.framedata}')
1117
+ self.im.set_array(self.stack[self.framedata])
1118
+ self.status_scatter.set_offsets(self.positions[self.framedata])
1119
+ self.status_scatter.set_color(self.colors[self.framedata][:, 1])
1120
+
1121
+ self.class_scatter.set_offsets(self.positions[self.framedata])
1122
+ self.class_scatter.set_edgecolor(self.colors[self.framedata][:, 0])
1123
+
1124
+ return (self.im, self.status_scatter, self.class_scatter,)
1125
+
1126
+ def stop(self):
1127
+ # # On stop we disconnect all of our events.
1128
+ self.stop_btn.hide()
1129
+ self.start_btn.show()
1130
+ self.anim.pause()
1131
+ self.stop_btn.clicked.connect(self.start)
1132
+
1133
+ def start(self):
1134
+ '''
1135
+ Starts interactive animation. Adds the draw frame command to the GUI
1136
+ handler, calls show to start the event loop.
1137
+ '''
1138
+ self.start_btn.setShortcut(QKeySequence(""))
1139
+
1140
+ self.last_frame_btn.setEnabled(True)
1141
+ self.last_frame_btn.clicked.connect(self.set_last_frame)
1142
+
1143
+ self.first_frame_btn.setEnabled(True)
1144
+ self.first_frame_btn.clicked.connect(self.set_first_frame)
1145
+
1146
+ self.start_btn.hide()
1147
+ self.stop_btn.show()
1148
+
1149
+ self.anim.event_source.start()
1150
+ self.stop_btn.clicked.connect(self.stop)
1151
+
1152
+ def give_cell_information(self):
1153
+
1154
+ cell_selected = f"cell: {self.track_of_interest}\n"
1155
+ cell_class = f"class: {self.df_tracks.loc[self.df_tracks['TRACK_ID'] == self.track_of_interest, self.class_name].to_numpy()[0]}\n"
1156
+ cell_time = f"time of interest: {self.df_tracks.loc[self.df_tracks['TRACK_ID'] == self.track_of_interest, self.time_name].to_numpy()[0]}\n"
1157
+ self.cell_info.setText(cell_selected + cell_class + cell_time)
1158
+
1159
+ def save_trajectories(self):
1160
+
1161
+ if self.normalized_signals:
1162
+ self.normalize_features_btn.click()
1163
+ if self.selection:
1164
+ self.cancel_selection()
1165
+
1166
+ self.df_tracks = self.df_tracks.drop(self.df_tracks[self.df_tracks[self.class_name] > 2].index)
1167
+ self.df_tracks.to_csv(self.trajectories_path, index=False)
1168
+ print('table saved.')
1169
+ self.extract_scatter_from_trajectories()
1170
+
1171
+ # self.give_cell_information()
1172
+
1173
+ # def interval_slider_action(self):
1174
+
1175
+ # print(dir(self.anim.event_source))
1176
+
1177
+ # self.anim.event_source.interval = self.interval_slider.value()
1178
+ # self.anim.event_source._timer_set_interval()
1179
+
1180
+ def set_last_frame(self):
1181
+
1182
+ self.last_frame_btn.setEnabled(False)
1183
+ self.last_frame_btn.disconnect()
1184
+
1185
+ self.last_key = len(self.stack) - 1
1186
+ while len(np.where(self.stack[self.last_key].flatten() == 0)[0]) > 0.99 * len(
1187
+ self.stack[self.last_key].flatten()):
1188
+ self.last_key -= 1
1189
+ print(f'Last frame is {len(self.stack) - 1}; last not black is {self.last_key}')
1190
+ self.anim._drawn_artists = self.draw_frame(self.last_key)
1191
+ self.anim._drawn_artists = sorted(self.anim._drawn_artists, key=lambda x: x.get_zorder())
1192
+ for a in self.anim._drawn_artists:
1193
+ a.set_visible(True)
1194
+
1195
+ self.fig.canvas.draw()
1196
+ self.anim.event_source.stop()
1197
+
1198
+ # self.cell_plot.draw()
1199
+ self.stop_btn.hide()
1200
+ self.start_btn.show()
1201
+ self.stop_btn.clicked.connect(self.start)
1202
+ self.start_btn.setShortcut(QKeySequence("l"))
1203
+
1204
+ def set_first_frame(self):
1205
+
1206
+ self.first_frame_btn.setEnabled(False)
1207
+ self.first_frame_btn.disconnect()
1208
+
1209
+ self.first_key = 0
1210
+ print(f'First frame is {0}')
1211
+ self.anim._drawn_artists = self.draw_frame(0)
1212
+ self.anim._drawn_artists = sorted(self.anim._drawn_artists, key=lambda x: x.get_zorder())
1213
+ for a in self.anim._drawn_artists:
1214
+ a.set_visible(True)
1215
+
1216
+ self.fig.canvas.draw()
1217
+ self.anim.event_source.stop()
1218
+
1219
+ # self.cell_plot.draw()
1220
+ self.stop_btn.hide()
1221
+ self.start_btn.show()
1222
+ self.stop_btn.clicked.connect(self.start)
1223
+ self.start_btn.setShortcut(QKeySequence("f"))
1224
+
1225
+ def export_signals(self):
1226
+
1227
+ auto_dataset_name = self.pos.split(os.sep)[-4] + '_' + self.pos.split(os.sep)[-2] + '.npy'
1228
+
1229
+ if self.normalized_signals:
1230
+ self.normalize_features_btn.click()
1231
+
1232
+ training_set = []
1233
+ cols = self.df_tracks.columns
1234
+ tracks = np.unique(self.df_tracks["TRACK_ID"].to_numpy())
1235
+
1236
+ for track in tracks:
1237
+ # Add all signals at given track
1238
+ signals = {}
1239
+ for c in cols:
1240
+ signals.update({c: self.df_tracks.loc[self.df_tracks["TRACK_ID"] == track, c].to_numpy()})
1241
+ time_of_interest = self.df_tracks.loc[self.df_tracks["TRACK_ID"] == track, self.time_name].to_numpy()[0]
1242
+ cclass = self.df_tracks.loc[self.df_tracks["TRACK_ID"] == track, self.class_name].to_numpy()[0]
1243
+ signals.update({"time_of_interest": time_of_interest, "class": cclass})
1244
+ # Here auto add all available channels
1245
+ training_set.append(signals)
1246
+
1247
+ print(training_set)
1248
+
1249
+ pathsave = QFileDialog.getSaveFileName(self, "Select file name", self.exp_dir + auto_dataset_name, ".npy")[0]
1250
+ if pathsave != '':
1251
+ if not pathsave.endswith(".npy"):
1252
+ pathsave += ".npy"
1253
+ try:
1254
+ np.save(pathsave, training_set)
1255
+ print(f'File successfully written in {pathsave}.')
1256
+ except Exception as e:
1257
+ print(f"Error {e}...")
1258
+
1259
+ def normalize_features(self):
1260
+
1261
+ x = self.df_tracks[self.columns_to_rescale].values
1262
+
1263
+ if not self.normalized_signals:
1264
+ x = self.MinMaxScaler.transform(x)
1265
+ self.df_tracks[self.columns_to_rescale] = x
1266
+ self.plot_signals()
1267
+ self.normalized_signals = True
1268
+ self.normalize_features_btn.setIcon(icon(MDI6.arrow_collapse_vertical, color="#1565c0"))
1269
+ self.normalize_features_btn.setIconSize(QSize(25, 25))
1270
+ else:
1271
+ x = self.MinMaxScaler.inverse_transform(x)
1272
+ self.df_tracks[self.columns_to_rescale] = x
1273
+ self.plot_signals()
1274
+ self.normalized_signals = False
1275
+ self.normalize_features_btn.setIcon(icon(MDI6.arrow_collapse_vertical, color="black"))
1276
+ self.normalize_features_btn.setIconSize(QSize(25, 25))
1277
+
1278
+ def switch_to_log(self):
1279
+
1280
+ """
1281
+ Better would be to create a log(quantity) and plot it...
1282
+ """
1283
+
1284
+ try:
1285
+ if self.cell_ax.get_yscale() == 'linear':
1286
+ self.cell_ax.set_yscale('log')
1287
+ self.log_btn.setIcon(icon(MDI6.math_log, color="#1565c0"))
1288
+ else:
1289
+ self.cell_ax.set_yscale('linear')
1290
+ self.log_btn.setIcon(icon(MDI6.math_log, color="black"))
1291
+ except Exception as e:
1292
+ print(e)
1293
+
1294
+ # self.cell_ax.autoscale()
1295
+ self.cell_fcanvas.canvas.draw_idle()
1296
+
1297
+
1298
+ class MeasureAnnotator(SignalAnnotator):
1299
+ def __init__(self, parent=None):
1300
+ QMainWindow.__init__(self)
1301
+ self.parent = parent
1302
+ self.setWindowTitle("Signal annotator")
1303
+ self.mode = self.parent.mode
1304
+ self.pos = self.parent.parent.pos
1305
+ self.exp_dir = self.parent.exp_dir
1306
+ self.n_signals = 3
1307
+ self.soft_path = get_software_location()
1308
+ self.recently_modified = False
1309
+ self.selection = []
1310
+ self.int_validator = QIntValidator()
1311
+ if self.mode == "targets":
1312
+ self.instructions_path = self.exp_dir + "configs/signal_annotator_config_targets.json"
1313
+ self.trajectories_path = self.pos + 'output/tables/trajectories_targets.csv'
1314
+ elif self.mode == "effectors":
1315
+ self.instructions_path = self.exp_dir + "configs/signal_annotator_config_effectors.json"
1316
+ self.trajectories_path = self.pos + 'output/tables/trajectories_effectors.csv'
1317
+
1318
+ self.screen_height = self.parent.parent.parent.screen_height
1319
+ self.screen_width = self.parent.parent.parent.screen_width
1320
+ self.current_frame = 0
1321
+ self.show_fliers = False
1322
+ self.status_name = 'group'
1323
+
1324
+ center_window(self)
1325
+
1326
+ self.locate_stack()
1327
+ self.current_channel = 0
1328
+
1329
+ self.locate_tracks()
1330
+
1331
+ self.generate_signal_choices()
1332
+ self.frame_lbl = QLabel('position: ')
1333
+ self.static_image()
1334
+ self.create_cell_signal_canvas()
1335
+
1336
+ self.populate_widget()
1337
+ self.changed_class()
1338
+
1339
+ self.setMinimumWidth(int(0.8 * self.screen_width))
1340
+ # self.setMaximumHeight(int(0.8*self.screen_height))
1341
+ self.setMinimumHeight(int(0.8 * self.screen_height))
1342
+ # self.setMaximumHeight(int(0.8*self.screen_height))
1343
+
1344
+ self.setAttribute(Qt.WA_DeleteOnClose)
1345
+ self.previous_index = None
1346
+
1347
+ def static_image(self):
1348
+
1349
+ """
1350
+ Load an image.
1351
+
1352
+ """
1353
+
1354
+ self.framedata = 0
1355
+
1356
+ self.fig, self.ax = plt.subplots(tight_layout=True)
1357
+ self.fcanvas = FigureCanvas(self.fig, interactive=True)
1358
+ self.ax.clear()
1359
+ # print(self.current_stack.shape)
1360
+ self.im = self.ax.imshow(self.img, cmap='gray')
1361
+ self.status_scatter = self.ax.scatter(self.positions[0][:, 0], self.positions[0][:, 1], marker="o",
1362
+ facecolors='none', edgecolors=self.colors[0][:, 0], s=200, picker=True)
1363
+ self.ax.set_xticks([])
1364
+ self.ax.set_yticks([])
1365
+ self.ax.set_aspect('equal')
1366
+
1367
+ self.fig.set_facecolor('none') # or 'None'
1368
+ self.fig.canvas.setStyleSheet("background-color: black;")
1369
+
1370
+ self.fig.canvas.mpl_connect('pick_event', self.on_scatter_pick)
1371
+ self.fcanvas.canvas.draw()
1372
+
1373
+ def create_cell_signal_canvas(self):
1374
+
1375
+ self.cell_fig, self.cell_ax = plt.subplots()
1376
+ self.cell_fcanvas = FigureCanvas(self.cell_fig, interactive=False)
1377
+ self.cell_ax.clear()
1378
+
1379
+ spacing = 0.5
1380
+ minorLocator = MultipleLocator(1)
1381
+ self.cell_ax.xaxis.set_minor_locator(minorLocator)
1382
+ self.cell_ax.xaxis.set_major_locator(MultipleLocator(5))
1383
+ self.cell_ax.grid(which='major')
1384
+ self.cell_ax.set_xlabel("time [frame]")
1385
+ self.cell_ax.set_ylabel("signal")
1386
+
1387
+ self.cell_fig.set_facecolor('none') # or 'None'
1388
+ self.cell_fig.canvas.setStyleSheet("background-color: transparent;")
1389
+
1390
+ self.lines = [
1391
+ self.cell_ax.plot([np.linspace(0, self.len_movie - 1, self.len_movie)], [np.zeros(self.len_movie)])[0] for
1392
+ i in range(len(self.signal_choice_cb))]
1393
+ for i in range(len(self.lines)):
1394
+ self.lines[i].set_label(f'signal {i}')
1395
+
1396
+ min_val, max_val = self.cell_ax.get_ylim()
1397
+ self.line_dt, = self.cell_ax.plot([-1, -1], [min_val, max_val], c="k", linestyle="--")
1398
+
1399
+ self.cell_ax.set_xlim(0, self.len_movie)
1400
+ self.cell_fcanvas.canvas.draw()
1401
+
1402
+ self.plot_signals()
1403
+
1404
+ def plot_signals(self):
1405
+ try:
1406
+ current_frame = self.current_frame # Assuming you have a variable for the current frame
1407
+ yvalues = []
1408
+ all_yvalues = []
1409
+ current_yvalues = []
1410
+ all_median_values = []
1411
+ labels = []
1412
+ for i in range(len(self.signal_choice_cb)):
1413
+ signal_choice = self.signal_choice_cb[i].currentText()
1414
+
1415
+ if signal_choice != "--":
1416
+ if 'TRACK_ID' in self.df_tracks.columns:
1417
+ ydata = self.df_tracks.loc[
1418
+ (self.df_tracks['TRACK_ID'] == self.track_of_interest) &
1419
+ (self.df_tracks['FRAME'] == current_frame), signal_choice].to_numpy()
1420
+ else:
1421
+ ydata = self.df_tracks.loc[
1422
+ (self.df_tracks['ID'] == self.track_of_interest), signal_choice].to_numpy()
1423
+ all_ydata = self.df_tracks.loc[:, signal_choice].to_numpy()
1424
+ ydata = ydata[ydata == ydata] # remove nan
1425
+ current_ydata = self.df_tracks.loc[
1426
+ (self.df_tracks['FRAME'] == current_frame), signal_choice].to_numpy()
1427
+ current_ydata = current_ydata[current_ydata == current_ydata]
1428
+ all_ydata = all_ydata[all_ydata == all_ydata]
1429
+ yvalues.extend(ydata)
1430
+ current_yvalues.append(current_ydata)
1431
+ all_yvalues.append(all_ydata)
1432
+ labels.append(signal_choice)
1433
+
1434
+ self.cell_ax.clear()
1435
+
1436
+ if len(yvalues) > 0:
1437
+ self.cell_ax.boxplot(all_yvalues, showfliers=self.show_fliers)
1438
+ ylim = self.cell_ax.get_ylim()
1439
+ self.cell_ax.set_ylim(ylim)
1440
+ x_pos = np.arange(len(all_yvalues)) + 1
1441
+
1442
+ for index, feature in enumerate(current_yvalues):
1443
+ x_values_strip = (index + 1) + np.random.normal(0, 0.04, size=len(
1444
+ feature))
1445
+ self.cell_ax.plot(x_values_strip, feature, marker='o', linestyle='None', color=tab10.colors[0],
1446
+ alpha=0.1)
1447
+ self.cell_ax.plot(x_pos, yvalues, marker='H', linestyle='None', color=tab10.colors[3], alpha=1)
1448
+
1449
+
1450
+ else:
1451
+ self.cell_ax.text(0.5, 0.5, "No data available", horizontalalignment='center',
1452
+ verticalalignment='center', transform=self.cell_ax.transAxes)
1453
+
1454
+ self.cell_fcanvas.canvas.draw()
1455
+
1456
+ except Exception as e:
1457
+ print(f"{e=}")
1458
+
1459
+ def configure_ylims(self):
1460
+
1461
+ try:
1462
+ min_values = []
1463
+ max_values = []
1464
+ for i in range(len(self.signal_choice_cb)):
1465
+ signal = self.signal_choice_cb[i].currentText()
1466
+ if signal == '--':
1467
+ continue
1468
+ else:
1469
+ maxx = np.max(self.df_tracks.loc[:, signal].to_numpy().flatten())
1470
+ minn = np.min(self.df_tracks.loc[:, signal].to_numpy().flatten())
1471
+ min_values.append(minn)
1472
+ max_values.append(maxx)
1473
+
1474
+ if len(min_values) > 0:
1475
+ self.cell_ax.set_ylim(np.amin(min_values), np.amax(max_values))
1476
+ except Exception as e:
1477
+ print(e)
1478
+
1479
+ def plot_red_points(self, ax):
1480
+ yvalues = []
1481
+ current_frame = self.current_frame
1482
+ for i in range(len(self.signal_choice_cb)):
1483
+ signal_choice = self.signal_choice_cb[i].currentText()
1484
+ if signal_choice != "--":
1485
+ print(f'plot signal {signal_choice} for cell {self.track_of_interest} at frame {current_frame}')
1486
+ if 'TRACK_ID' in self.df_tracks.columns:
1487
+ ydata = self.df_tracks.loc[
1488
+ (self.df_tracks['TRACK_ID'] == self.track_of_interest) &
1489
+ (self.df_tracks['FRAME'] == current_frame), signal_choice].to_numpy()
1490
+ else:
1491
+ ydata = self.df_tracks.loc[
1492
+ (self.df_tracks['ID'] == self.track_of_interest) &
1493
+ (self.df_tracks['FRAME'] == current_frame), signal_choice].to_numpy()
1494
+ ydata = ydata[ydata == ydata] # remove nan
1495
+ yvalues.extend(ydata)
1496
+ x_pos = np.arange(len(yvalues)) + 1
1497
+ ax.plot(x_pos, yvalues, marker='H', linestyle='None', color=tab10.colors[3],
1498
+ alpha=1) # Plot red points representing cells
1499
+ self.cell_fcanvas.canvas.draw()
1500
+
1501
+ def on_scatter_pick(self, event):
1502
+ ind = event.ind
1503
+ if len(ind) > 1:
1504
+ # More than one point in vicinity
1505
+ datax, datay = [self.positions[self.framedata][i, 0] for i in ind], [self.positions[self.framedata][i, 1]
1506
+ for i in ind]
1507
+ msx, msy = event.mouseevent.xdata, event.mouseevent.ydata
1508
+ dist = np.sqrt((np.array(datax) - msx) ** 2 + (np.array(datay) - msy) ** 2)
1509
+ ind = [ind[np.argmin(dist)]]
1510
+
1511
+ if len(ind) > 0 and (len(self.selection) == 0):
1512
+ ind = ind[0]
1513
+ self.selection.append(ind)
1514
+ self.correct_btn.setEnabled(True)
1515
+ self.cancel_btn.setEnabled(True)
1516
+ self.del_shortcut.setEnabled(True)
1517
+ self.no_event_shortcut.setEnabled(True)
1518
+ self.track_of_interest = self.tracks[self.framedata][ind]
1519
+ print(f'You selected track {self.track_of_interest}.')
1520
+ self.give_cell_information()
1521
+ if len(self.cell_ax.lines) > 0:
1522
+ self.cell_ax.lines[-1].remove() # Remove the last line (red points) from the plot
1523
+ self.plot_red_points(self.cell_ax)
1524
+ else:
1525
+ self.plot_signals()
1526
+
1527
+ self.loc_t = []
1528
+ self.loc_idx = []
1529
+ for t in range(len(self.tracks)):
1530
+ indices = np.where(self.tracks[t] == self.track_of_interest)[0]
1531
+ if len(indices) > 0:
1532
+ self.loc_t.append(t)
1533
+ self.loc_idx.append(indices[0])
1534
+
1535
+ self.previous_color = []
1536
+ for t, idx in zip(self.loc_t, self.loc_idx):
1537
+ self.previous_color.append(self.colors[t][idx].copy())
1538
+ self.colors[t][idx] = 'lime'
1539
+
1540
+ elif len(ind) > 0 and len(self.selection) == 1:
1541
+ self.cancel_btn.click()
1542
+ else:
1543
+ pass
1544
+ self.draw_frame(self.current_frame)
1545
+ self.fcanvas.canvas.draw()
1546
+
1547
+ def populate_widget(self):
1548
+
1549
+ """
1550
+ Create the multibox design.
1551
+
1552
+ """
1553
+
1554
+ self.button_widget = QWidget()
1555
+ main_layout = QHBoxLayout()
1556
+ self.button_widget.setLayout(main_layout)
1557
+
1558
+ main_layout.setContentsMargins(30, 30, 30, 30)
1559
+ self.left_panel = QVBoxLayout()
1560
+ self.left_panel.setContentsMargins(30, 30, 30, 30)
1561
+ self.left_panel.setSpacing(10)
1562
+
1563
+ self.right_panel = QVBoxLayout()
1564
+
1565
+ class_hbox = QHBoxLayout()
1566
+ class_hbox.addWidget(QLabel('characteristic \n group: '), 25)
1567
+ self.class_choice_cb = QComboBox()
1568
+
1569
+ cols = np.array(self.df_tracks.columns)
1570
+ self.class_cols = np.array([c.startswith('group') for c in list(self.df_tracks.columns)])
1571
+ self.class_cols = list(cols[self.class_cols])
1572
+ try:
1573
+ self.class_cols.remove('group_id')
1574
+ except Exception:
1575
+ pass
1576
+ try:
1577
+ self.class_cols.remove('group_color')
1578
+ except Exception:
1579
+ pass
1580
+
1581
+ self.class_choice_cb.addItems(self.class_cols)
1582
+ self.class_choice_cb.currentIndexChanged.connect(self.changed_class)
1583
+ class_hbox.addWidget(self.class_choice_cb, 70)
1584
+
1585
+ self.add_class_btn = QPushButton('')
1586
+ self.add_class_btn.setStyleSheet(self.parent.parent.parent.button_select_all)
1587
+ self.add_class_btn.setIcon(icon(MDI6.plus, color="black"))
1588
+ self.add_class_btn.setToolTip("Add a new characteristic group")
1589
+ self.add_class_btn.setIconSize(QSize(20, 20))
1590
+ self.add_class_btn.clicked.connect(self.create_new_event_class)
1591
+ class_hbox.addWidget(self.add_class_btn, 5)
1592
+
1593
+ self.del_class_btn = QPushButton('')
1594
+ self.del_class_btn.setStyleSheet(self.parent.parent.parent.button_select_all)
1595
+ self.del_class_btn.setIcon(icon(MDI6.delete, color="black"))
1596
+ self.del_class_btn.setToolTip("Delete a characteristic group")
1597
+ self.del_class_btn.setIconSize(QSize(20, 20))
1598
+ self.del_class_btn.clicked.connect(self.del_event_class)
1599
+ class_hbox.addWidget(self.del_class_btn, 5)
1600
+
1601
+ self.left_panel.addLayout(class_hbox)
1602
+
1603
+ self.cell_info = QLabel('')
1604
+ self.left_panel.addWidget(self.cell_info)
1605
+
1606
+ time_option_hbox = QHBoxLayout()
1607
+ time_option_hbox.setContentsMargins(100, 30, 100, 30)
1608
+ self.time_of_interest_label = QLabel('phenotype: ')
1609
+ time_option_hbox.addWidget(self.time_of_interest_label, 30)
1610
+ self.time_of_interest_le = QLineEdit()
1611
+ self.time_of_interest_le.setValidator(self.int_validator)
1612
+ time_option_hbox.addWidget(self.time_of_interest_le)
1613
+ self.del_cell_btn = QPushButton('')
1614
+ self.del_cell_btn.setStyleSheet(self.parent.parent.parent.button_select_all)
1615
+ self.del_cell_btn.setIcon(icon(MDI6.delete, color="black"))
1616
+ self.del_cell_btn.setToolTip("Delete cell")
1617
+ self.del_cell_btn.setIconSize(QSize(20, 20))
1618
+ self.del_cell_btn.clicked.connect(self.del_cell)
1619
+ time_option_hbox.addWidget(self.del_cell_btn)
1620
+ self.left_panel.addLayout(time_option_hbox)
1621
+
1622
+ main_action_hbox = QHBoxLayout()
1623
+ self.correct_btn = QPushButton('correct')
1624
+ self.correct_btn.setIcon(icon(MDI6.redo_variant, color="white"))
1625
+ self.correct_btn.setIconSize(QSize(20, 20))
1626
+ self.correct_btn.setStyleSheet(self.parent.parent.parent.button_style_sheet)
1627
+ self.correct_btn.clicked.connect(self.show_annotation_buttons)
1628
+ self.correct_btn.setEnabled(False)
1629
+ main_action_hbox.addWidget(self.correct_btn)
1630
+
1631
+ self.cancel_btn = QPushButton('cancel')
1632
+ self.cancel_btn.setStyleSheet(self.parent.parent.parent.button_style_sheet_2)
1633
+ self.cancel_btn.setShortcut(QKeySequence("Esc"))
1634
+ self.cancel_btn.setEnabled(False)
1635
+ self.cancel_btn.clicked.connect(self.cancel_selection)
1636
+ main_action_hbox.addWidget(self.cancel_btn)
1637
+ self.left_panel.addLayout(main_action_hbox)
1638
+
1639
+ self.annotation_btns_to_hide = [self.time_of_interest_label,
1640
+ self.time_of_interest_le,
1641
+ self.del_cell_btn]
1642
+ self.hide_annotation_buttons()
1643
+ #### End of annotation buttons
1644
+
1645
+ self.del_shortcut = QShortcut(Qt.Key_Delete, self) # QKeySequence("s")
1646
+ self.del_shortcut.activated.connect(self.shortcut_suppr)
1647
+ self.del_shortcut.setEnabled(False)
1648
+
1649
+ self.no_event_shortcut = QShortcut(QKeySequence("n"), self) # QKeySequence("s")
1650
+ self.no_event_shortcut.activated.connect(self.shortcut_no_event)
1651
+ self.no_event_shortcut.setEnabled(False)
1652
+
1653
+ # Cell signals
1654
+ self.left_panel.addWidget(self.cell_fcanvas)
1655
+
1656
+ plot_buttons_hbox = QHBoxLayout()
1657
+ plot_buttons_hbox.setContentsMargins(0, 0, 0, 0)
1658
+ self.outliers_check = QCheckBox('Show outliers')
1659
+ self.outliers_check.toggled.connect(self.show_outliers)
1660
+
1661
+ self.normalize_features_btn = QPushButton('')
1662
+ self.normalize_features_btn.setStyleSheet(self.parent.parent.parent.button_select_all)
1663
+ self.normalize_features_btn.setIcon(icon(MDI6.arrow_collapse_vertical, color="black"))
1664
+ self.normalize_features_btn.setIconSize(QSize(25, 25))
1665
+ self.normalize_features_btn.setFixedSize(QSize(30, 30))
1666
+ # self.normalize_features_btn.setShortcut(QKeySequence('n'))
1667
+ self.normalize_features_btn.clicked.connect(self.normalize_features)
1668
+
1669
+ plot_buttons_hbox.addWidget(QLabel(''), 90)
1670
+ plot_buttons_hbox.addWidget(self.outliers_check)
1671
+ plot_buttons_hbox.addWidget(self.normalize_features_btn, 5)
1672
+ self.normalized_signals = False
1673
+
1674
+ self.log_btn = QPushButton()
1675
+ self.log_btn.setIcon(icon(MDI6.math_log, color="black"))
1676
+ self.log_btn.setStyleSheet(self.parent.parent.parent.button_select_all)
1677
+ self.log_btn.clicked.connect(self.switch_to_log)
1678
+ plot_buttons_hbox.addWidget(self.log_btn, 5)
1679
+
1680
+ self.left_panel.addLayout(plot_buttons_hbox)
1681
+
1682
+ signal_choice_vbox = QVBoxLayout()
1683
+ signal_choice_vbox.setContentsMargins(30, 0, 30, 50)
1684
+ for i in range(len(self.signal_choice_cb)):
1685
+ hlayout = QHBoxLayout()
1686
+ hlayout.addWidget(self.signal_choice_label[i], 20)
1687
+ hlayout.addWidget(self.signal_choice_cb[i], 75)
1688
+ # hlayout.addWidget(self.log_btns[i], 5)
1689
+ signal_choice_vbox.addLayout(hlayout)
1690
+
1691
+ # self.log_btns[i].setIcon(icon(MDI6.math_log,color="black"))
1692
+ # self.log_btns[i].setStyleSheet(self.parent.parent.parent.button_select_all)
1693
+ # self.log_btns[i].clicked.connect(lambda ch, i=i: self.switch_to_log(i))
1694
+
1695
+ self.left_panel.addLayout(signal_choice_vbox)
1696
+
1697
+ btn_hbox = QHBoxLayout()
1698
+ self.save_btn = QPushButton('Save')
1699
+ self.save_btn.setStyleSheet(self.parent.parent.parent.button_style_sheet)
1700
+ self.save_btn.clicked.connect(self.save_trajectories)
1701
+ btn_hbox.addWidget(self.save_btn, 90)
1702
+ self.left_panel.addLayout(btn_hbox)
1703
+
1704
+ # Animation
1705
+ animation_buttons_box = QHBoxLayout()
1706
+
1707
+ animation_buttons_box.addWidget(self.frame_lbl, 20, alignment=Qt.AlignLeft)
1708
+
1709
+ self.first_frame_btn = QPushButton()
1710
+ self.first_frame_btn.clicked.connect(self.set_previous_frame)
1711
+ self.first_frame_btn.setShortcut(QKeySequence('f'))
1712
+ self.first_frame_btn.setIcon(icon(MDI6.page_first, color="black"))
1713
+ self.first_frame_btn.setStyleSheet(self.parent.parent.parent.button_select_all)
1714
+ self.first_frame_btn.setFixedSize(QSize(60, 60))
1715
+ self.first_frame_btn.setIconSize(QSize(30, 30))
1716
+
1717
+ self.last_frame_btn = QPushButton()
1718
+ self.last_frame_btn.clicked.connect(self.set_next_frame)
1719
+ self.last_frame_btn.setShortcut(QKeySequence('l'))
1720
+ self.last_frame_btn.setIcon(icon(MDI6.page_last, color="black"))
1721
+ self.last_frame_btn.setStyleSheet(self.parent.parent.parent.button_select_all)
1722
+ self.last_frame_btn.setFixedSize(QSize(60, 60))
1723
+ self.last_frame_btn.setIconSize(QSize(30, 30))
1724
+
1725
+ self.frame_slider = QSlider(Qt.Horizontal)
1726
+ self.frame_slider.setFixedSize(200, 30)
1727
+ self.frame_slider.setRange(0, self.len_movie - 1)
1728
+ self.frame_slider.setValue(0)
1729
+ self.frame_slider.valueChanged.connect(self.update_frame)
1730
+
1731
+ self.start_btn = QPushButton()
1732
+ self.start_btn.clicked.connect(self.start)
1733
+ self.start_btn.setIcon(icon(MDI6.play, color="black"))
1734
+ self.start_btn.setFixedSize(QSize(60, 60))
1735
+ self.start_btn.setStyleSheet(self.parent.parent.parent.button_select_all)
1736
+ self.start_btn.setIconSize(QSize(30, 30))
1737
+ self.start_btn.hide()
1738
+
1739
+ animation_buttons_box.addWidget(self.first_frame_btn, 5, alignment=Qt.AlignRight)
1740
+ animation_buttons_box.addWidget(self.frame_slider, 5, alignment=Qt.AlignCenter)
1741
+ animation_buttons_box.addWidget(self.last_frame_btn, 5, alignment=Qt.AlignLeft)
1742
+
1743
+ self.right_panel.addLayout(animation_buttons_box, 5)
1744
+
1745
+ self.right_panel.addWidget(self.fcanvas, 90)
1746
+
1747
+ contrast_hbox = QHBoxLayout()
1748
+ contrast_hbox.setContentsMargins(150, 5, 150, 5)
1749
+ self.contrast_slider = QLabeledDoubleRangeSlider()
1750
+
1751
+ # self.contrast_slider.setSingleStep(0.001)
1752
+ # self.contrast_slider.setTickInterval(0.001)
1753
+ self.contrast_slider.setOrientation(1)
1754
+ print('range: ',
1755
+ [np.nanpercentile(self.img.flatten(), 0.001), np.nanpercentile(self.img.flatten(), 99.999)])
1756
+ self.contrast_slider.setRange(
1757
+ *[np.nanpercentile(self.img.flatten(), 0.001), np.nanpercentile(self.img.flatten(), 99.999)])
1758
+ self.contrast_slider.setValue(
1759
+ [np.percentile(self.img.flatten(), 1), np.percentile(self.img.flatten(), 99.99)])
1760
+ self.contrast_slider.valueChanged.connect(self.contrast_slider_action)
1761
+ contrast_hbox.addWidget(QLabel('contrast: '))
1762
+ contrast_hbox.addWidget(self.contrast_slider, 90)
1763
+ self.right_panel.addLayout(contrast_hbox, 5)
1764
+
1765
+ channel_hbox = QHBoxLayout()
1766
+ self.choose_channel = QComboBox()
1767
+ self.choose_channel.addItems(self.channel_names)
1768
+ self.choose_channel.currentIndexChanged.connect(self.changed_channel)
1769
+ channel_hbox.addWidget(self.choose_channel)
1770
+ self.right_panel.addLayout(channel_hbox, 5)
1771
+
1772
+ main_layout.addLayout(self.left_panel, 35)
1773
+ main_layout.addLayout(self.right_panel, 65)
1774
+ self.button_widget.adjustSize()
1775
+
1776
+ self.setCentralWidget(self.button_widget)
1777
+ self.show()
1778
+
1779
+ QApplication.processEvents()
1780
+
1781
+ def closeEvent(self, event):
1782
+ # result = QMessageBox.question(self,
1783
+ # "Confirm Exit...",
1784
+ # "Are you sure you want to exit ?",
1785
+ # QMessageBox.Yes| QMessageBox.No,
1786
+ # )
1787
+ # del self.img
1788
+ gc.collect()
1789
+
1790
+ def set_next_frame(self):
1791
+
1792
+ self.current_frame = self.current_frame + 1
1793
+ if self.current_frame > self.len_movie - 1:
1794
+ self.current_frame == self.len_movie - 1
1795
+ self.frame_slider.setValue(self.current_frame)
1796
+ self.update_frame()
1797
+ self.start_btn.setShortcut(QKeySequence("f"))
1798
+
1799
+ def set_previous_frame(self):
1800
+
1801
+ self.current_frame = self.current_frame - 1
1802
+ if self.current_frame < 0:
1803
+ self.current_frame == 0
1804
+ self.frame_slider.setValue(self.current_frame)
1805
+ self.update_frame()
1806
+
1807
+ self.start_btn.setShortcut(QKeySequence("l"))
1808
+
1809
+ def write_new_event_class(self):
1810
+
1811
+ if self.class_name_le.text() == '':
1812
+ self.target_class = 'group'
1813
+ else:
1814
+ self.target_class = 'group_' + self.class_name_le.text()
1815
+
1816
+ if self.target_class in list(self.df_tracks.columns):
1817
+ msgBox = QMessageBox()
1818
+ msgBox.setIcon(QMessageBox.Warning)
1819
+ msgBox.setText(
1820
+ "This characteristic group name already exists. If you proceed,\nall annotated data will be rewritten. Do you wish to continue?")
1821
+ msgBox.setWindowTitle("Warning")
1822
+ msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
1823
+ returnValue = msgBox.exec()
1824
+ if returnValue == QMessageBox.No:
1825
+ return None
1826
+ else:
1827
+ pass
1828
+ self.df_tracks.loc[:, self.target_class] = 0
1829
+ self.class_choice_cb.clear()
1830
+ cols = np.array(self.df_tracks.columns)
1831
+ self.class_cols = np.array([c.startswith('group') for c in list(self.df_tracks.columns)])
1832
+ self.class_cols = list(cols[self.class_cols])
1833
+ self.class_cols.remove('group_color')
1834
+ self.class_choice_cb.addItems(self.class_cols)
1835
+ idx = self.class_choice_cb.findText(self.target_class)
1836
+ self.status_name = self.target_class
1837
+ self.class_choice_cb.setCurrentIndex(idx)
1838
+ self.newClassWidget.close()
1839
+
1840
+ def hide_annotation_buttons(self):
1841
+
1842
+ for a in self.annotation_btns_to_hide:
1843
+ a.hide()
1844
+ self.time_of_interest_label.setEnabled(False)
1845
+ self.time_of_interest_le.setText('')
1846
+ self.time_of_interest_le.setEnabled(False)
1847
+
1848
+ def show_annotation_buttons(self):
1849
+
1850
+ for a in self.annotation_btns_to_hide:
1851
+ a.show()
1852
+
1853
+ self.time_of_interest_label.setEnabled(True)
1854
+ self.time_of_interest_le.setEnabled(True)
1855
+ self.correct_btn.setText('submit')
1856
+
1857
+ self.correct_btn.disconnect()
1858
+ self.correct_btn.clicked.connect(self.apply_modification)
1859
+
1860
+ def give_cell_information(self):
1861
+
1862
+ cell_selected = f"cell: {self.track_of_interest}\n"
1863
+ if 'TRACK_ID' in self.df_tracks.columns:
1864
+ cell_status = f"phenotype: {self.df_tracks.loc[self.df_tracks['TRACK_ID'] == self.track_of_interest, self.status_name].to_numpy()[0]}\n"
1865
+ else:
1866
+ cell_status = f"phenotype: {self.df_tracks.loc[self.df_tracks['ID'] == self.track_of_interest, self.status_name].to_numpy()[0]}\n"
1867
+ self.cell_info.setText(cell_selected + cell_status)
1868
+
1869
+ def create_new_event_class(self):
1870
+
1871
+ # display qwidget to name the event
1872
+ self.newClassWidget = QWidget()
1873
+ self.newClassWidget.setWindowTitle('Create new characteristic group')
1874
+
1875
+ layout = QVBoxLayout()
1876
+ self.newClassWidget.setLayout(layout)
1877
+ name_hbox = QHBoxLayout()
1878
+ name_hbox.addWidget(QLabel('group name: '), 25)
1879
+ self.class_name_le = QLineEdit('group')
1880
+ name_hbox.addWidget(self.class_name_le, 75)
1881
+ layout.addLayout(name_hbox)
1882
+
1883
+ btn_hbox = QHBoxLayout()
1884
+ submit_btn = QPushButton('submit')
1885
+ cancel_btn = QPushButton('cancel')
1886
+ btn_hbox.addWidget(cancel_btn, 50)
1887
+ btn_hbox.addWidget(submit_btn, 50)
1888
+ layout.addLayout(btn_hbox)
1889
+
1890
+ submit_btn.clicked.connect(self.write_new_event_class)
1891
+ cancel_btn.clicked.connect(self.close_without_new_class)
1892
+
1893
+ self.newClassWidget.show()
1894
+ center_window(self.newClassWidget)
1895
+
1896
+ def apply_modification(self):
1897
+ if self.time_of_interest_le.text() != "":
1898
+ status = int(self.time_of_interest_le.text())
1899
+ else:
1900
+ status = 0
1901
+ if "TRACK_ID" in self.df_tracks.columns:
1902
+ self.df_tracks.loc[(self.df_tracks['TRACK_ID'] == self.track_of_interest) & (
1903
+ self.df_tracks['FRAME'] == self.current_frame), self.status_name] = status
1904
+
1905
+ indices = self.df_tracks.index[(self.df_tracks['TRACK_ID'] == self.track_of_interest) & (
1906
+ self.df_tracks['FRAME'] == self.current_frame)]
1907
+ else:
1908
+ self.df_tracks.loc[(self.df_tracks['ID'] == self.track_of_interest) & (
1909
+ self.df_tracks['FRAME'] == self.current_frame), self.status_name] = status
1910
+
1911
+ indices = self.df_tracks.index[(self.df_tracks['ID'] == self.track_of_interest) & (
1912
+ self.df_tracks['FRAME'] == self.current_frame)]
1913
+
1914
+ self.df_tracks.loc[indices, self.status_name] = status
1915
+ all_states = self.df_tracks.loc[:, self.status_name].tolist()
1916
+ all_states = np.array(all_states)
1917
+ self.state_color_map = color_from_state(all_states, recently_modified=False)
1918
+
1919
+ self.df_tracks['group_color'] = self.df_tracks[self.status_name].apply(self.assign_color_state)
1920
+
1921
+ self.extract_scatter_from_trajectories()
1922
+ self.give_cell_information()
1923
+
1924
+ self.correct_btn.disconnect()
1925
+ self.correct_btn.clicked.connect(self.show_annotation_buttons)
1926
+
1927
+ self.hide_annotation_buttons()
1928
+ self.correct_btn.setEnabled(False)
1929
+ self.correct_btn.setText('correct')
1930
+ self.cancel_btn.setEnabled(False)
1931
+ self.del_shortcut.setEnabled(False)
1932
+ self.no_event_shortcut.setEnabled(False)
1933
+ if len(self.selection) > 0:
1934
+ self.selection.pop(0)
1935
+ self.draw_frame(self.current_frame)
1936
+ self.fcanvas.canvas.draw()
1937
+
1938
+ def assign_color_state(self, state):
1939
+ if np.isnan(state):
1940
+ pass
1941
+ else:
1942
+ return self.state_color_map[state]
1943
+
1944
+ def draw_frame(self, framedata):
1945
+
1946
+ """
1947
+ Update plot elements at each timestep of the loop.
1948
+ """
1949
+
1950
+ self.framedata = framedata
1951
+ self.frame_lbl.setText(f'position: {self.framedata}')
1952
+ self.im.set_array(self.img)
1953
+ self.status_scatter.set_offsets(self.positions[self.framedata])
1954
+ self.status_scatter.set_edgecolors(self.colors[self.framedata][:, 0])
1955
+
1956
+ return (self.im, self.status_scatter,)
1957
+
1958
+ def compute_status_and_colors(self):
1959
+ if self.class_choice_cb.currentText() == '':
1960
+ self.status_name=self.target_class
1961
+ else:
1962
+ self.status_name = self.class_choice_cb.currentText()
1963
+ print('selection and expected names: ', self.status_name)
1964
+
1965
+ if self.status_name not in self.df_tracks.columns:
1966
+ self.make_status_column()
1967
+ else:
1968
+ all_states = self.df_tracks.loc[:, self.status_name].tolist()
1969
+ all_states = np.array(all_states)
1970
+ self.state_color_map = color_from_state(all_states, recently_modified=False)
1971
+ self.df_tracks['group_color'] = self.df_tracks[self.status_name].apply(self.assign_color_state)
1972
+
1973
+ def del_event_class(self):
1974
+
1975
+ msgBox = QMessageBox()
1976
+ msgBox.setIcon(QMessageBox.Warning)
1977
+ msgBox.setText(
1978
+ f"You are about to delete characteristic group {self.class_choice_cb.currentText()}. Do you still want to proceed?")
1979
+ msgBox.setWindowTitle("Warning")
1980
+ msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
1981
+ returnValue = msgBox.exec()
1982
+ if returnValue == QMessageBox.No:
1983
+ return None
1984
+ else:
1985
+ class_to_delete = self.class_choice_cb.currentText()
1986
+ cols_to_delete = [class_to_delete]
1987
+ for c in cols_to_delete:
1988
+ try:
1989
+ self.df_tracks = self.df_tracks.drop([c], axis=1)
1990
+ except Exception as e:
1991
+ print(e)
1992
+ item_idx = self.class_choice_cb.findText(class_to_delete)
1993
+ self.class_choice_cb.removeItem(item_idx)
1994
+
1995
+ def make_status_column(self):
1996
+ if self.status_name == "state_firstdetection":
1997
+ pass
1998
+ else:
1999
+ print('remaking the group column')
2000
+ self.df_tracks.loc[:, self.status_name] = 0
2001
+ all_states = self.df_tracks.loc[:, self.status_name].tolist()
2002
+ all_states = np.array(all_states)
2003
+ self.state_color_map = color_from_state(all_states, recently_modified=False)
2004
+ self.df_tracks['group_color'] = self.df_tracks[self.status_name].apply(self.assign_color_state)
2005
+
2006
+ def locate_tracks(self):
2007
+
2008
+ """
2009
+ Locate the tracks.
2010
+ """
2011
+
2012
+ if not os.path.exists(self.trajectories_path):
2013
+
2014
+ msgBox = QMessageBox()
2015
+ msgBox.setIcon(QMessageBox.Warning)
2016
+ msgBox.setText("The trajectories cannot be detected.")
2017
+ msgBox.setWindowTitle("Warning")
2018
+ msgBox.setStandardButtons(QMessageBox.Ok)
2019
+ returnValue = msgBox.exec()
2020
+ if returnValue == QMessageBox.Yes:
2021
+ self.close()
2022
+ else:
2023
+
2024
+ # Load and prep tracks
2025
+ self.df_tracks = pd.read_csv(self.trajectories_path)
2026
+ if 'TRACK_ID' in self.df_tracks.columns:
2027
+ self.df_tracks = self.df_tracks.sort_values(by=['TRACK_ID', 'FRAME'])
2028
+ else:
2029
+ self.df_tracks = self.df_tracks.sort_values(by=['ID', 'FRAME'])
2030
+
2031
+ cols = np.array(self.df_tracks.columns)
2032
+ self.class_cols = np.array([c.startswith('group') for c in list(self.df_tracks.columns)])
2033
+ self.class_cols = list(cols[self.class_cols])
2034
+ try:
2035
+ self.class_cols.remove('class_id')
2036
+ except:
2037
+ pass
2038
+ try:
2039
+ self.class_cols.remove('group_color')
2040
+ except:
2041
+ pass
2042
+ if len(self.class_cols) > 0:
2043
+ self.status = self.class_cols[0]
2044
+
2045
+ else:
2046
+
2047
+ self.status_name = 'group'
2048
+
2049
+ if self.status_name not in self.df_tracks.columns:
2050
+ # only create the status column if it does not exist to not erase static classification results
2051
+ self.make_status_column()
2052
+ else:
2053
+ # all good, do nothing
2054
+ pass
2055
+ # else:
2056
+ # if not self.status_name in self.df_tracks.columns:
2057
+ # self.df_tracks[self.status_name] = 0
2058
+ # self.df_tracks['state_color'] = color_from_status(0)
2059
+ # self.df_tracks['class_color'] = color_from_class(1)
2060
+
2061
+ # if not self.class_name in self.df_tracks.columns:
2062
+ # self.df_tracks[self.class_name] = 1
2063
+ # if not self.time_name in self.df_tracks.columns:
2064
+ # self.df_tracks[self.time_name] = -1
2065
+ all_states = self.df_tracks.loc[:, self.status_name].tolist()
2066
+ all_states = np.array(all_states)
2067
+ self.state_color_map = color_from_state(all_states, recently_modified=False)
2068
+ self.df_tracks['group_color'] = self.df_tracks[self.status_name].apply(self.assign_color_state)
2069
+ # self.df_tracks['status_color'] = [color_from_status(i) for i in self.df_tracks[self.status_name].to_numpy()]
2070
+ # self.df_tracks['class_color'] = [color_from_class(i) for i in self.df_tracks[self.class_name].to_numpy()]
2071
+
2072
+ self.df_tracks = self.df_tracks.dropna(subset=['POSITION_X', 'POSITION_Y'])
2073
+ self.df_tracks['x_anim'] = self.df_tracks['POSITION_X']
2074
+ self.df_tracks['y_anim'] = self.df_tracks['POSITION_Y']
2075
+ self.df_tracks['x_anim'] = self.df_tracks['x_anim'].astype(int)
2076
+ self.df_tracks['y_anim'] = self.df_tracks['y_anim'].astype(int)
2077
+
2078
+ self.extract_scatter_from_trajectories()
2079
+ if 'TRACK_ID' in self.df_tracks.columns:
2080
+ self.track_of_interest = self.df_tracks['TRACK_ID'].min()
2081
+ else:
2082
+ self.track_of_interest = self.df_tracks['ID'].min()
2083
+
2084
+ self.loc_t = []
2085
+ self.loc_idx = []
2086
+ for t in range(len(self.tracks)):
2087
+ indices = np.where(self.tracks[t] == self.track_of_interest)[0]
2088
+ if len(indices) > 0:
2089
+ self.loc_t.append(t)
2090
+ self.loc_idx.append(indices[0])
2091
+
2092
+ self.MinMaxScaler = MinMaxScaler()
2093
+ self.columns_to_rescale = list(self.df_tracks.columns)
2094
+
2095
+ # is_number = np.vectorize(lambda x: np.issubdtype(x, np.number))
2096
+ # is_number_test = is_number(self.df_tracks.dtypes)
2097
+ # self.columns_to_rescale = [col for t,col in zip(is_number_test,self.df_tracks.columns) if t]
2098
+ # print(self.columns_to_rescale)
2099
+
2100
+ cols_to_remove = ['group', 'group_color', 'status', 'status_color', 'class_color', 'TRACK_ID', 'FRAME',
2101
+ 'x_anim', 'y_anim', 't',
2102
+ 'state', 'generation', 'root', 'parent', 'class_id', 'class', 't0', 'POSITION_X',
2103
+ 'POSITION_Y', 'position', 'well', 'well_index', 'well_name', 'pos_name', 'index',
2104
+ 'concentration', 'cell_type', 'antibody', 'pharmaceutical_agent', 'ID'] + self.class_cols
2105
+ cols = np.array(list(self.df_tracks.columns))
2106
+ for tr in cols_to_remove:
2107
+ try:
2108
+ self.columns_to_rescale.remove(tr)
2109
+ except:
2110
+ pass
2111
+ # print(f'column {tr} could not be found...')
2112
+
2113
+ x = self.df_tracks[self.columns_to_rescale].values
2114
+ self.MinMaxScaler.fit(x)
2115
+
2116
+ def extract_scatter_from_trajectories(self):
2117
+
2118
+ self.positions = []
2119
+ self.colors = []
2120
+ self.tracks = []
2121
+
2122
+ for t in np.arange(self.len_movie):
2123
+ self.positions.append(self.df_tracks.loc[self.df_tracks['FRAME'] == t, ['POSITION_X', 'POSITION_Y']].to_numpy())
2124
+ self.colors.append(
2125
+ self.df_tracks.loc[self.df_tracks['FRAME'] == t, ['group_color']].to_numpy())
2126
+ if 'TRACK_ID' in self.df_tracks.columns:
2127
+ self.tracks.append(self.df_tracks.loc[self.df_tracks['FRAME'] == t, 'TRACK_ID'].to_numpy())
2128
+ else:
2129
+ self.tracks.append(self.df_tracks.loc[self.df_tracks['FRAME'] == t, 'ID'].to_numpy())
2130
+
2131
+ def changed_class(self):
2132
+ self.status_name = self.class_choice_cb.currentText()
2133
+ self.compute_status_and_colors()
2134
+ self.modify()
2135
+ self.draw_frame(self.current_frame)
2136
+ self.fcanvas.canvas.draw()
2137
+
2138
+ def update_frame(self):
2139
+ """
2140
+ Update the displayed frame.
2141
+ """
2142
+ self.current_frame = self.frame_slider.value()
2143
+ self.reload_frame()
2144
+ if 'ID' in self.df_tracks.columns:
2145
+ self.track_of_interest = self.df_tracks[self.df_tracks['FRAME'] == self.current_frame]['ID'].min()
2146
+ self.modify()
2147
+ self.draw_frame(self.current_frame)
2148
+ self.fcanvas.canvas.draw()
2149
+ self.plot_signals()
2150
+
2151
+ # def load_annotator_config(self):
2152
+ # self.rgb_mode = False
2153
+ # self.log_option = False
2154
+ # self.percentile_mode = True
2155
+ # self.target_channels = [[self.channel_names[0], 0.01, 99.99]]
2156
+ # self.fraction = 0.5955056179775281
2157
+ # #self.anim_interval = 1
2158
+
2159
+ def prepare_stack(self):
2160
+
2161
+ self.img_num_channels = _get_img_num_per_channel(self.channels, self.len_movie, self.nbr_channels)
2162
+ self.current_stack = []
2163
+ for ch in tqdm(self.target_channels, desc="channel"):
2164
+ target_ch_name = ch[0]
2165
+ if self.percentile_mode:
2166
+ normalize_kwargs = {"percentiles": (ch[1], ch[2]), "values": None}
2167
+ else:
2168
+ normalize_kwargs = {"values": (ch[1], ch[2]), "percentiles": None}
2169
+
2170
+ if self.rgb_mode:
2171
+ normalize_kwargs.update({'amplification': 255., 'clip': True})
2172
+
2173
+ chan = []
2174
+ indices = self.img_num_channels[self.channels[np.where(self.channel_names == target_ch_name)][0]]
2175
+ for t in tqdm(range(len(indices)), desc='frame'):
2176
+ if self.rgb_mode:
2177
+ f = load_frames(indices[t], self.stack_path, scale=self.fraction, normalize_input=True,
2178
+ normalize_kwargs=normalize_kwargs)
2179
+ f = f.astype(np.uint8)
2180
+ else:
2181
+ f = load_frames(indices[t], self.stack_path, scale=self.fraction, normalize_input=False)
2182
+ chan.append(f[:, :, 0])
2183
+
2184
+ self.current_stack.append(chan)
2185
+
2186
+ self.current_stack = np.array(self.current_stack)
2187
+ if self.rgb_mode:
2188
+ self.current_stack = np.moveaxis(self.current_stack, 0, -1)
2189
+ else:
2190
+ self.current_stack = self.current_stack[0]
2191
+ if self.log_option:
2192
+ self.current_stack[np.where(self.current_stack > 0.)] = np.log(
2193
+ self.current_stack[np.where(self.current_stack > 0.)])
2194
+
2195
+ print(f'Load stack of shape: {self.current_stack.shape}.')
2196
+
2197
+ def changed_channel(self):
2198
+
2199
+ self.reload_frame()
2200
+ self.contrast_slider.setRange(
2201
+ *[np.nanpercentile(self.img.flatten(), 0.001),
2202
+ np.nanpercentile(self.img.flatten(), 99.999)])
2203
+ self.contrast_slider.setValue(
2204
+ [np.percentile(self.img.flatten(), 1), np.percentile(self.img.flatten(), 99.99)])
2205
+ self.draw_frame(self.current_frame)
2206
+ self.fcanvas.canvas.draw()
2207
+
2208
+ def save_trajectories(self):
2209
+
2210
+ if self.normalized_signals:
2211
+ self.normalize_features_btn.click()
2212
+ if self.selection:
2213
+ self.cancel_selection()
2214
+ self.df_tracks = self.df_tracks.drop(self.df_tracks[self.df_tracks[self.status_name] == 99].index)
2215
+ #color_column = str(self.status_name) + "_color"
2216
+ try:
2217
+ self.df_tracks.drop(columns='', inplace=True)
2218
+ except:
2219
+ pass
2220
+ try:
2221
+ self.df_tracks.drop(columns='group_color', inplace=True)
2222
+ except:
2223
+ pass
2224
+ try:
2225
+ self.df_tracks.drop(columns='x_anim', inplace=True)
2226
+ except:
2227
+ pass
2228
+ try:
2229
+ self.df_tracks.drop(columns='y_anim', inplace=True)
2230
+ except:
2231
+ pass
2232
+
2233
+ self.df_tracks.to_csv(self.trajectories_path, index=False)
2234
+ print('table saved.')
2235
+ self.update_frame()
2236
+
2237
+
2238
+ # self.extract_scatter_from_trajectories()
2239
+
2240
+ def modify(self):
2241
+ all_states = self.df_tracks.loc[:, self.status_name].tolist()
2242
+ all_states = np.array(all_states)
2243
+ self.state_color_map = color_from_state(all_states, recently_modified=False)
2244
+
2245
+ self.df_tracks['group_color'] = self.df_tracks[self.status_name].apply(self.assign_color_state)
2246
+
2247
+ self.extract_scatter_from_trajectories()
2248
+ self.give_cell_information()
2249
+
2250
+ self.correct_btn.disconnect()
2251
+ self.correct_btn.clicked.connect(self.show_annotation_buttons)
2252
+
2253
+ # self.hide_annotation_buttons()
2254
+ # self.correct_btn.setEnabled(False)
2255
+ # self.correct_btn.setText('correct')
2256
+ # self.cancel_btn.setEnabled(False)
2257
+ # self.del_shortcut.setEnabled(False)
2258
+ # self.no_event_shortcut.setEnabled(False)
2259
+
2260
+ def enable_time_of_interest(self):
2261
+ if self.suppr_btn.isChecked():
2262
+ self.time_of_interest_le.setEnabled(False)
2263
+
2264
+ def cancel_selection(self):
2265
+
2266
+ self.hide_annotation_buttons()
2267
+ self.correct_btn.setEnabled(False)
2268
+ self.correct_btn.setText('correct')
2269
+ self.cancel_btn.setEnabled(False)
2270
+
2271
+ try:
2272
+ self.selection.pop(0)
2273
+ except Exception as e:
2274
+ print(e)
2275
+
2276
+ try:
2277
+ for k, (t, idx) in enumerate(zip(self.loc_t, self.loc_idx)):
2278
+ # print(self.colors[t][idx, 1])
2279
+ self.colors[t][idx, 0] = self.previous_color[k][0]
2280
+ # self.colors[t][idx, 1] = self.previous_color[k][1]
2281
+ except Exception as e:
2282
+ print(f'{e=}')
2283
+
2284
+ def locate_stack(self):
2285
+
2286
+ """
2287
+ Locate the target movie.
2288
+
2289
+ """
2290
+
2291
+ print("this is the loaded position: ", self.pos)
2292
+ if isinstance(self.pos, str):
2293
+ movies = glob(self.pos + f"movie/{self.parent.parent.movie_prefix}*.tif")
2294
+
2295
+ else:
2296
+ msgBox = QMessageBox()
2297
+ msgBox.setIcon(QMessageBox.Warning)
2298
+ msgBox.setText("Please select a unique position before launching the wizard...")
2299
+ msgBox.setWindowTitle("Warning")
2300
+ msgBox.setStandardButtons(QMessageBox.Ok)
2301
+ returnValue = msgBox.exec()
2302
+ if returnValue == QMessageBox.Ok:
2303
+ self.img = None
2304
+ self.close()
2305
+ return None
2306
+
2307
+ if len(movies) == 0:
2308
+ msgBox = QMessageBox()
2309
+ msgBox.setIcon(QMessageBox.Warning)
2310
+ msgBox.setText("No movies are detected in the experiment folder. Cannot load an image to test Haralick.")
2311
+ msgBox.setWindowTitle("Warning")
2312
+ msgBox.setStandardButtons(QMessageBox.Ok)
2313
+ returnValue = msgBox.exec()
2314
+ if returnValue == QMessageBox.Yes:
2315
+ self.close()
2316
+ else:
2317
+ self.stack_path = movies[0]
2318
+ self.len_movie = self.parent.parent.len_movie
2319
+ len_movie_auto = auto_load_number_of_frames(self.stack_path)
2320
+ if len_movie_auto is not None:
2321
+ self.len_movie = len_movie_auto
2322
+ exp_config = self.exp_dir + "config.ini"
2323
+ self.channel_names, self.channels = extract_experiment_channels(exp_config)
2324
+ self.channel_names = np.array(self.channel_names)
2325
+ self.channels = np.array(self.channels)
2326
+ self.nbr_channels = len(self.channels)
2327
+ self.current_channel = 0
2328
+ self.img = load_frames(0, self.stack_path, normalize_input=False)
2329
+ print(self.img.shape)
2330
+ print(f'{self.stack_path} successfully located.')
2331
+
2332
+ def reload_frame(self):
2333
+
2334
+ """
2335
+ Load the frame from the current channel and time choice. Show imshow, update histogram.
2336
+ """
2337
+
2338
+ # self.clear_post_threshold_options()
2339
+
2340
+ self.current_channel = self.choose_channel.currentIndex()
2341
+
2342
+ t = int(self.frame_slider.value())
2343
+ idx = t * self.nbr_channels + self.current_channel
2344
+ self.img = load_frames(idx, self.stack_path, normalize_input=False)
2345
+ if self.img is not None:
2346
+ self.refresh_imshow()
2347
+ # self.redo_histogram()
2348
+ else:
2349
+ print('Frame could not be loaded...')
2350
+
2351
+ def refresh_imshow(self):
2352
+
2353
+ """
2354
+
2355
+ Update the imshow based on the current frame selection.
2356
+
2357
+ """
2358
+
2359
+ self.vmin = np.nanpercentile(self.img.flatten(), 1)
2360
+ self.vmax = np.nanpercentile(self.img.flatten(), 99.)
2361
+
2362
+ self.contrast_slider.disconnect()
2363
+ self.contrast_slider.setRange(np.amin(self.img), np.amax(self.img))
2364
+ self.contrast_slider.setValue([self.vmin, self.vmax])
2365
+ self.contrast_slider.valueChanged.connect(self.contrast_slider_action)
2366
+
2367
+ self.im.set_data(self.img)
2368
+
2369
+ # self.im.set_clim(vmin=self.vmin, vmax=self.vmax)
2370
+ # self.fcanvas.canvas.draw_idle()
2371
+
2372
+ def show_outliers(self):
2373
+ if self.outliers_check.isChecked():
2374
+ self.show_fliers = True
2375
+ self.plot_signals()
2376
+ else:
2377
+ self.show_fliers = False
2378
+ self.plot_signals()
2379
+
2380
+ def del_cell(self):
2381
+ self.time_of_interest_le.setEnabled(False)
2382
+ self.time_of_interest_le.setText("99")
2383
+ self.apply_modification()
2384
+
2385
+ def shortcut_suppr(self):
2386
+ self.correct_btn.click()
2387
+ self.del_cell_btn.click()
2388
+ self.correct_btn.click()