celldetective 1.4.1.post1__py3-none-any.whl → 1.5.0b0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. celldetective/__init__.py +25 -0
  2. celldetective/__main__.py +62 -43
  3. celldetective/_version.py +1 -1
  4. celldetective/extra_properties.py +477 -399
  5. celldetective/filters.py +192 -97
  6. celldetective/gui/InitWindow.py +541 -411
  7. celldetective/gui/__init__.py +0 -15
  8. celldetective/gui/about.py +44 -39
  9. celldetective/gui/analyze_block.py +120 -84
  10. celldetective/gui/base/__init__.py +0 -0
  11. celldetective/gui/base/channel_norm_generator.py +335 -0
  12. celldetective/gui/base/components.py +249 -0
  13. celldetective/gui/base/feature_choice.py +92 -0
  14. celldetective/gui/base/figure_canvas.py +52 -0
  15. celldetective/gui/base/list_widget.py +133 -0
  16. celldetective/gui/{styles.py → base/styles.py} +92 -36
  17. celldetective/gui/base/utils.py +33 -0
  18. celldetective/gui/base_annotator.py +900 -767
  19. celldetective/gui/classifier_widget.py +642 -554
  20. celldetective/gui/configure_new_exp.py +777 -671
  21. celldetective/gui/control_panel.py +635 -524
  22. celldetective/gui/dynamic_progress.py +449 -0
  23. celldetective/gui/event_annotator.py +2023 -1662
  24. celldetective/gui/generic_signal_plot.py +1292 -944
  25. celldetective/gui/gui_utils.py +899 -1289
  26. celldetective/gui/interactions_block.py +658 -0
  27. celldetective/gui/interactive_timeseries_viewer.py +447 -0
  28. celldetective/gui/json_readers.py +48 -15
  29. celldetective/gui/layouts/__init__.py +5 -0
  30. celldetective/gui/layouts/background_model_free_layout.py +537 -0
  31. celldetective/gui/layouts/channel_offset_layout.py +134 -0
  32. celldetective/gui/layouts/local_correction_layout.py +91 -0
  33. celldetective/gui/layouts/model_fit_layout.py +372 -0
  34. celldetective/gui/layouts/operation_layout.py +68 -0
  35. celldetective/gui/layouts/protocol_designer_layout.py +96 -0
  36. celldetective/gui/pair_event_annotator.py +3130 -2435
  37. celldetective/gui/plot_measurements.py +586 -267
  38. celldetective/gui/plot_signals_ui.py +724 -506
  39. celldetective/gui/preprocessing_block.py +395 -0
  40. celldetective/gui/process_block.py +1678 -1831
  41. celldetective/gui/seg_model_loader.py +580 -473
  42. celldetective/gui/settings/__init__.py +0 -7
  43. celldetective/gui/settings/_cellpose_model_params.py +181 -0
  44. celldetective/gui/settings/_event_detection_model_params.py +95 -0
  45. celldetective/gui/settings/_segmentation_model_params.py +159 -0
  46. celldetective/gui/settings/_settings_base.py +77 -65
  47. celldetective/gui/settings/_settings_event_model_training.py +752 -526
  48. celldetective/gui/settings/_settings_measurements.py +1133 -964
  49. celldetective/gui/settings/_settings_neighborhood.py +574 -488
  50. celldetective/gui/settings/_settings_segmentation_model_training.py +779 -564
  51. celldetective/gui/settings/_settings_signal_annotator.py +329 -305
  52. celldetective/gui/settings/_settings_tracking.py +1304 -1094
  53. celldetective/gui/settings/_stardist_model_params.py +98 -0
  54. celldetective/gui/survival_ui.py +422 -312
  55. celldetective/gui/tableUI.py +1665 -1700
  56. celldetective/gui/table_ops/_maths.py +295 -0
  57. celldetective/gui/table_ops/_merge_groups.py +140 -0
  58. celldetective/gui/table_ops/_merge_one_hot.py +95 -0
  59. celldetective/gui/table_ops/_query_table.py +43 -0
  60. celldetective/gui/table_ops/_rename_col.py +44 -0
  61. celldetective/gui/thresholds_gui.py +382 -179
  62. celldetective/gui/viewers/__init__.py +0 -0
  63. celldetective/gui/viewers/base_viewer.py +700 -0
  64. celldetective/gui/viewers/channel_offset_viewer.py +331 -0
  65. celldetective/gui/viewers/contour_viewer.py +394 -0
  66. celldetective/gui/viewers/size_viewer.py +153 -0
  67. celldetective/gui/viewers/spot_detection_viewer.py +341 -0
  68. celldetective/gui/viewers/threshold_viewer.py +309 -0
  69. celldetective/gui/workers.py +304 -126
  70. celldetective/log_manager.py +92 -0
  71. celldetective/measure.py +1895 -1478
  72. celldetective/napari/__init__.py +0 -0
  73. celldetective/napari/utils.py +1025 -0
  74. celldetective/neighborhood.py +1914 -1448
  75. celldetective/preprocessing.py +1620 -1220
  76. celldetective/processes/__init__.py +0 -0
  77. celldetective/processes/background_correction.py +271 -0
  78. celldetective/processes/compute_neighborhood.py +894 -0
  79. celldetective/processes/detect_events.py +246 -0
  80. celldetective/processes/measure_cells.py +565 -0
  81. celldetective/processes/segment_cells.py +760 -0
  82. celldetective/processes/track_cells.py +435 -0
  83. celldetective/processes/train_segmentation_model.py +694 -0
  84. celldetective/processes/train_signal_model.py +265 -0
  85. celldetective/processes/unified_process.py +292 -0
  86. celldetective/regionprops/_regionprops.py +358 -317
  87. celldetective/relative_measurements.py +987 -710
  88. celldetective/scripts/measure_cells.py +313 -212
  89. celldetective/scripts/measure_relative.py +90 -46
  90. celldetective/scripts/segment_cells.py +165 -104
  91. celldetective/scripts/segment_cells_thresholds.py +96 -68
  92. celldetective/scripts/track_cells.py +198 -149
  93. celldetective/scripts/train_segmentation_model.py +324 -201
  94. celldetective/scripts/train_signal_model.py +87 -45
  95. celldetective/segmentation.py +844 -749
  96. celldetective/signals.py +3514 -2861
  97. celldetective/tracking.py +1332 -1011
  98. celldetective/utils/__init__.py +0 -0
  99. celldetective/utils/cellpose_utils/__init__.py +133 -0
  100. celldetective/utils/color_mappings.py +42 -0
  101. celldetective/utils/data_cleaning.py +630 -0
  102. celldetective/utils/data_loaders.py +450 -0
  103. celldetective/utils/dataset_helpers.py +207 -0
  104. celldetective/utils/downloaders.py +197 -0
  105. celldetective/utils/event_detection/__init__.py +8 -0
  106. celldetective/utils/experiment.py +1782 -0
  107. celldetective/utils/image_augmenters.py +308 -0
  108. celldetective/utils/image_cleaning.py +74 -0
  109. celldetective/utils/image_loaders.py +926 -0
  110. celldetective/utils/image_transforms.py +335 -0
  111. celldetective/utils/io.py +62 -0
  112. celldetective/utils/mask_cleaning.py +348 -0
  113. celldetective/utils/mask_transforms.py +5 -0
  114. celldetective/utils/masks.py +184 -0
  115. celldetective/utils/maths.py +351 -0
  116. celldetective/utils/model_getters.py +325 -0
  117. celldetective/utils/model_loaders.py +296 -0
  118. celldetective/utils/normalization.py +380 -0
  119. celldetective/utils/parsing.py +465 -0
  120. celldetective/utils/plots/__init__.py +0 -0
  121. celldetective/utils/plots/regression.py +53 -0
  122. celldetective/utils/resources.py +34 -0
  123. celldetective/utils/stardist_utils/__init__.py +104 -0
  124. celldetective/utils/stats.py +90 -0
  125. celldetective/utils/types.py +21 -0
  126. {celldetective-1.4.1.post1.dist-info → celldetective-1.5.0b0.dist-info}/METADATA +1 -1
  127. celldetective-1.5.0b0.dist-info/RECORD +187 -0
  128. {celldetective-1.4.1.post1.dist-info → celldetective-1.5.0b0.dist-info}/WHEEL +1 -1
  129. tests/gui/test_new_project.py +129 -117
  130. tests/gui/test_project.py +127 -79
  131. tests/test_filters.py +39 -15
  132. tests/test_notebooks.py +8 -0
  133. tests/test_tracking.py +425 -144
  134. tests/test_utils.py +123 -77
  135. celldetective/gui/base_components.py +0 -23
  136. celldetective/gui/layouts.py +0 -1602
  137. celldetective/gui/processes/compute_neighborhood.py +0 -594
  138. celldetective/gui/processes/measure_cells.py +0 -360
  139. celldetective/gui/processes/segment_cells.py +0 -499
  140. celldetective/gui/processes/track_cells.py +0 -303
  141. celldetective/gui/processes/train_segmentation_model.py +0 -270
  142. celldetective/gui/processes/train_signal_model.py +0 -108
  143. celldetective/gui/table_ops/merge_groups.py +0 -118
  144. celldetective/gui/viewers.py +0 -1354
  145. celldetective/io.py +0 -3663
  146. celldetective/utils.py +0 -3108
  147. celldetective-1.4.1.post1.dist-info/RECORD +0 -123
  148. /celldetective/{gui/processes → processes}/downloader.py +0 -0
  149. {celldetective-1.4.1.post1.dist-info → celldetective-1.5.0b0.dist-info}/entry_points.txt +0 -0
  150. {celldetective-1.4.1.post1.dist-info → celldetective-1.5.0b0.dist-info}/licenses/LICENSE +0 -0
  151. {celldetective-1.4.1.post1.dist-info → celldetective-1.5.0b0.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,15 @@
1
- from PyQt5.QtWidgets import QLineEdit, QMessageBox, QHBoxLayout, QVBoxLayout, QPushButton, QLabel, \
2
- QCheckBox, QRadioButton, QButtonGroup, QComboBox
1
+ from PyQt5.QtWidgets import (
2
+ QLineEdit,
3
+ QMessageBox,
4
+ QHBoxLayout,
5
+ QVBoxLayout,
6
+ QPushButton,
7
+ QLabel,
8
+ QCheckBox,
9
+ QRadioButton,
10
+ QButtonGroup,
11
+ QComboBox,
12
+ )
3
13
  from PyQt5.QtCore import Qt, QSize
4
14
  from superqt import QLabeledSlider, QLabeledDoubleSlider, QSearchableComboBox
5
15
  from superqt.fonticon import icon
@@ -11,558 +21,636 @@ import matplotlib.pyplot as plt
11
21
  import json
12
22
 
13
23
  from celldetective.exceptions import EmptyQueryError, MissingColumnsError, QueryError
14
- from celldetective.gui.gui_utils import FigureCanvas, center_window, color_from_status, help_generic
15
- from celldetective.gui.base_components import CelldetectiveWidget
16
- from celldetective.utils import get_software_location
17
- from celldetective.measure import classify_cells_from_query, interpret_track_classification
24
+ from celldetective.gui.gui_utils import color_from_status, help_generic
25
+ from celldetective.gui.base.figure_canvas import FigureCanvas
26
+ from celldetective.gui.base.components import CelldetectiveWidget
27
+ from celldetective import get_software_location
28
+ from celldetective.measure import (
29
+ classify_cells_from_query,
30
+ interpret_track_classification,
31
+ )
32
+
18
33
 
19
34
  class ClassifierWidget(CelldetectiveWidget):
20
35
 
21
- def __init__(self, parent_window):
22
-
23
- super().__init__()
24
-
25
- self.parent_window = parent_window
26
- self.screen_height = self.parent_window.parent_window.parent_window.screen_height
27
- self.screen_width = self.parent_window.parent_window.parent_window.screen_width
28
- self.currentAlpha = 1.0
29
-
30
- self.setWindowTitle("Custom classification")
31
-
32
- self.mode = self.parent_window.mode
33
- self.df = self.parent_window.df
34
-
35
- is_number = np.vectorize(lambda x: np.issubdtype(x, np.number))
36
- is_number_test = is_number(self.df.dtypes)
37
- self.cols = [col for t, col in zip(is_number_test, self.df.columns) if t]
38
-
39
- self.class_name = 'custom'
40
- self.name_le = QLineEdit(self.class_name)
41
- self.init_class()
42
-
43
- # Create the QComboBox and add some items
44
- center_window(self)
45
-
46
-
47
- layout = QVBoxLayout(self)
48
- layout.setContentsMargins(30, 30, 30, 30)
49
-
50
- name_layout = QHBoxLayout()
51
- name_layout.addWidget(QLabel('class name: '), 33)
52
- name_layout.addWidget(self.name_le, 66)
53
- layout.addLayout(name_layout)
54
-
55
- fig_btn_hbox = QHBoxLayout()
56
- fig_btn_hbox.addWidget(QLabel(''), 95)
57
- self.project_times_btn = QPushButton('')
58
- self.project_times_btn.setStyleSheet(self.parent_window.parent_window.parent_window.button_select_all)
59
- self.project_times_btn.setIcon(icon(MDI6.math_integral, color="black"))
60
- self.project_times_btn.setToolTip("Project measurements at all times.")
61
- self.project_times_btn.setIconSize(QSize(20, 20))
62
- self.project_times = False
63
- self.project_times_btn.clicked.connect(self.switch_projection)
64
- fig_btn_hbox.addWidget(self.project_times_btn, 5)
65
- layout.addLayout(fig_btn_hbox)
66
-
67
- # Figure
68
- self.initalize_props_scatter()
69
- layout.addWidget(self.propscanvas)
70
-
71
- # slider
72
- self.frame_slider = QLabeledSlider()
73
- self.frame_slider.setSingleStep(1)
74
- self.frame_slider.setOrientation(Qt.Horizontal)
75
- self.frame_slider.setRange(0, int(self.df.FRAME.max()) - 1)
76
- self.frame_slider.setValue(0)
77
- self.currentFrame = 0
78
-
79
- slider_hbox = QHBoxLayout()
80
- slider_hbox.addWidget(QLabel('frame: '), 10)
81
- slider_hbox.addWidget(self.frame_slider, 90)
82
- layout.addLayout(slider_hbox)
83
-
84
-
85
- # transparency slider
86
- self.alpha_slider = QLabeledDoubleSlider()
87
- self.alpha_slider.setSingleStep(0.001)
88
- self.alpha_slider.setOrientation(Qt.Horizontal)
89
- self.alpha_slider.setRange(0, 1)
90
- self.alpha_slider.setValue(1.0)
91
- self.alpha_slider.setDecimals(3)
92
-
93
- slider_alpha_hbox = QHBoxLayout()
94
- slider_alpha_hbox.addWidget(QLabel('transparency: '), 10)
95
- slider_alpha_hbox.addWidget(self.alpha_slider, 90)
96
- layout.addLayout(slider_alpha_hbox)
97
-
98
-
99
- self.features_cb = [QSearchableComboBox() for _ in range(2)]
100
- self.log_btns = [QPushButton() for _ in range(2)]
101
-
102
- for i in range(2):
103
- hbox_feat = QHBoxLayout()
104
- hbox_feat.addWidget(QLabel(f'feature {i}: '), 20)
105
- hbox_feat.addWidget(self.features_cb[i], 75)
106
- hbox_feat.addWidget(self.log_btns[i], 5)
107
- layout.addLayout(hbox_feat)
108
-
109
- self.features_cb[i].clear()
110
- self.features_cb[i].addItems(sorted(list(self.cols), key=str.lower))
111
- self.features_cb[i].currentTextChanged.connect(self.update_props_scatter)
112
- self.features_cb[i].setCurrentIndex(i)
113
-
114
- self.log_btns[i].setIcon(icon(MDI6.math_log,color="black"))
115
- self.log_btns[i].setStyleSheet(self.button_select_all)
116
- self.log_btns[i].clicked.connect(lambda ch, i=i: self.switch_to_log(i))
117
-
118
- hbox_classify = QHBoxLayout()
119
- hbox_classify.addWidget(QLabel('classify: '), 10)
120
- self.property_query_le = QLineEdit()
121
- self.property_query_le.setPlaceholderText('classify points using a query such as: area > 100 or eccentricity > 0.95')
122
- self.property_query_le.setToolTip('Classify points using a query on measurements.\nYou can use "and" and "or" conditions to combine\nmeasurements (e.g. "area > 100 or eccentricity > 0.95").')
123
- self.property_query_le.textChanged.connect(self.activate_submit_btn)
124
- hbox_classify.addWidget(self.property_query_le, 70)
125
- self.submit_query_btn = QPushButton('Submit...')
126
- self.submit_query_btn.clicked.connect(self.apply_property_query)
127
- self.submit_query_btn.setEnabled(False)
128
- hbox_classify.addWidget(self.submit_query_btn, 20)
129
- layout.addLayout(hbox_classify)
130
-
131
- self.time_corr = QCheckBox('Time correlated')
132
- if "TRACK_ID" in self.df.columns:
133
- self.time_corr.setEnabled(True)
134
- else:
135
- self.time_corr.setEnabled(False)
136
-
137
- time_prop_hbox = QHBoxLayout()
138
- time_prop_hbox.addWidget(self.time_corr, alignment=Qt.AlignCenter)
139
-
140
- self.help_propagate_btn = QPushButton()
141
- self.help_propagate_btn.setIcon(icon(MDI6.help_circle, color=self.help_color))
142
- self.help_propagate_btn.setIconSize(QSize(20, 20))
143
- self.help_propagate_btn.clicked.connect(self.help_propagate)
144
- self.help_propagate_btn.setStyleSheet(self.button_select_all)
145
- self.help_propagate_btn.setToolTip("Help.")
146
- time_prop_hbox.addWidget(self.help_propagate_btn, 5, alignment=Qt.AlignRight)
147
-
148
- layout.addLayout(time_prop_hbox)
149
-
150
- self.irreversible_event_btn = QRadioButton('irreversible event')
151
- self.unique_state_btn = QRadioButton('unique state')
152
- self.transient_event_btn = QRadioButton('transient event')
153
- time_corr_btn_group = QButtonGroup()
154
- self.unique_state_btn.click()
155
-
156
- time_corr_layout = QHBoxLayout()
157
- time_corr_layout.addWidget(self.unique_state_btn, 33, alignment=Qt.AlignCenter)
158
- time_corr_layout.addWidget(self.irreversible_event_btn, 33, alignment=Qt.AlignCenter)
159
- time_corr_layout.addWidget(self.transient_event_btn, 33, alignment=Qt.AlignCenter)
160
- layout.addLayout(time_corr_layout)
161
-
162
- self.prereq_event_check = QCheckBox('prerequisite event:')
163
- self.prereq_event_check.toggled.connect(self.activate_prereq_cb)
164
- self.prereq_event_cb = QComboBox()
165
- event_cols = ['--'] + [c.replace('t_', '') for c in self.cols if c.startswith('t_')]
166
- self.prereq_event_cb.addItems(event_cols)
167
- self.prereq_event_check.setEnabled(False)
168
- self.prereq_event_cb.setEnabled(False)
169
-
170
- self.r2_slider = QLabeledDoubleSlider()
171
- self.r2_slider.setValue(0.75)
172
- self.r2_slider.setRange(0, 1)
173
- self.r2_slider.setSingleStep(0.01)
174
- self.r2_slider.setOrientation(Qt.Horizontal)
175
- self.r2_label = QLabel('R2 tolerance:')
176
- self.r2_label.setToolTip('Minimum R2 between the fit sigmoid and the binary response to the filters to accept the event.')
177
-
178
- r2_threshold_layout = QHBoxLayout()
179
- r2_threshold_layout.addWidget(QLabel(''), 33)
180
- r2_threshold_layout.addWidget(self.r2_label, 13)
181
- r2_threshold_layout.addWidget(self.r2_slider, 20)
182
- r2_threshold_layout.addWidget(QLabel(''), 33)
183
-
184
- layout.addLayout(r2_threshold_layout)
185
-
186
- self.time_corr_options = [self.irreversible_event_btn, self.unique_state_btn, self.prereq_event_check, self.prereq_event_cb, self.transient_event_btn]
187
- for btn in [self.irreversible_event_btn, self.unique_state_btn, self.transient_event_btn]:
188
- time_corr_btn_group.addButton(btn)
189
- btn.setEnabled(False)
190
- self.time_corr.toggled.connect(self.activate_time_corr_options)
191
-
192
- self.irreversible_event_btn.clicked.connect(self.activate_r2)
193
- self.unique_state_btn.clicked.connect(self.activate_r2)
194
- self.transient_event_btn.clicked.connect(self.activate_r2)
195
-
196
- for wg in [self.r2_slider, self.r2_label]:
197
- wg.setEnabled(False)
198
-
199
- prereq_layout = QHBoxLayout()
200
- prereq_layout.setContentsMargins(30, 0, 0, 0)
201
- prereq_layout.addWidget(self.prereq_event_check, 20)
202
- prereq_layout.addWidget(self.prereq_event_cb, 80)
203
- layout.addLayout(prereq_layout)
204
-
205
- layout.addWidget(QLabel())
206
-
207
- self.submit_btn = QPushButton('apply')
208
- self.submit_btn.setStyleSheet(self.button_style_sheet)
209
- self.submit_btn.clicked.connect(self.submit_classification)
210
- self.submit_btn.setEnabled(False)
211
- layout.addWidget(self.submit_btn, 30)
212
-
213
- self.frame_slider.valueChanged.connect(self.set_frame)
214
- self.alpha_slider.valueChanged.connect(self.set_transparency)
215
-
216
-
217
- def activate_prereq_cb(self):
218
- if self.prereq_event_check.isChecked():
219
- self.prereq_event_cb.setEnabled(True)
220
- else:
221
- self.prereq_event_cb.setEnabled(False)
222
-
223
- def activate_submit_btn(self):
224
-
225
- if self.property_query_le.text()=='':
226
- self.submit_query_btn.setEnabled(False)
227
- self.submit_btn.setEnabled(False)
228
- else:
229
- self.submit_query_btn.setEnabled(True)
230
- self.submit_btn.setEnabled(True)
231
-
232
- def activate_r2(self):
233
- if self.irreversible_event_btn.isChecked() and self.time_corr.isChecked():
234
- for wg in [self.r2_slider, self.r2_label]:
235
- wg.setEnabled(True)
236
- else:
237
- for wg in [self.r2_slider, self.r2_label]:
238
- wg.setEnabled(False)
239
-
240
- def activate_time_corr_options(self):
241
-
242
- if self.time_corr.isChecked():
243
- for btn in self.time_corr_options:
244
- btn.setEnabled(True)
245
- if self.irreversible_event_btn.isChecked():
246
- for wg in [self.r2_slider, self.r2_label]:
247
- wg.setEnabled(True)
248
- else:
249
- for wg in [self.r2_slider, self.r2_label]:
250
- wg.setEnabled(False)
251
- else:
252
- for btn in self.time_corr_options:
253
- btn.setEnabled(False)
254
- for wg in [self.r2_slider, self.r2_label]:
255
- wg.setEnabled(False)
256
-
257
- def init_class(self):
258
-
259
- self.class_name = 'custom'
260
- i=1
261
- while self.class_name in self.df.columns:
262
- self.class_name = f'custom_{i}'
263
- i+=1
264
- self.name_le.setText(self.class_name)
265
- self.df.loc[:,self.class_name] = 1
266
-
267
- def initalize_props_scatter(self):
268
-
269
- """
270
- Define properties scatter.
271
- """
272
-
273
- self.fig_props, self.ax_props = plt.subplots(figsize=(4, 4), tight_layout=True)
274
- self.propscanvas = FigureCanvas(self.fig_props, interactive=True)
275
- self.fig_props.set_facecolor('none')
276
- self.scat_props = self.ax_props.scatter([], [], color="k", alpha=self.currentAlpha)
277
- self.propscanvas.canvas.draw_idle()
278
- self.propscanvas.canvas.setMinimumHeight(self.screen_height//5)
279
-
280
- def closeEvent(self, event):
281
- self.ax_props.cla()
282
- self.fig_props.clf()
283
- plt.close(self.fig_props)
284
- super().closeEvent(event)
285
-
286
- def update_props_scatter(self, feature_changed=True):
287
-
288
- try:
289
- if np.any(self.df[self.features_cb[0].currentText()].to_numpy() <= 0.):
290
- if self.ax_props.get_yscale()=='log':
291
- self.log_btns[0].click()
292
- self.log_btns[0].setEnabled(False)
293
- else:
294
- self.log_btns[0].setEnabled(True)
295
-
296
- if np.any(self.df[self.features_cb[1].currentText()].to_numpy() <= 0.):
297
- if self.ax_props.get_xscale() == 'log':
298
- self.log_btns[1].click()
299
- self.log_btns[1].setEnabled(False)
300
- else:
301
- self.log_btns[1].setEnabled(True)
302
- except Exception as e:
303
- print(e)
304
-
305
- class_name = self.class_name
306
-
307
- try:
308
-
309
- if not self.project_times:
310
- self.scat_props.set_offsets(self.df.loc[self.df['FRAME'] == self.currentFrame, [self.features_cb[1].currentText(), self.features_cb[0].currentText()]].to_numpy())
311
- colors = [color_from_status(c) for c in self.df.loc[self.df['FRAME'] == self.currentFrame, class_name].to_numpy()]
312
- self.scat_props.set_facecolor(colors)
313
- else:
314
- self.scat_props.set_offsets(self.df[[self.features_cb[1].currentText(), self.features_cb[0].currentText()]].to_numpy())
315
- colors = [color_from_status(c) for c in self.df[class_name].to_numpy()]
316
- self.scat_props.set_facecolor(colors)
317
-
318
- self.scat_props.set_alpha(self.currentAlpha)
319
-
320
- if feature_changed:
321
-
322
- self.ax_props.set_xlabel(self.features_cb[1].currentText())
323
- self.ax_props.set_ylabel(self.features_cb[0].currentText())
324
-
325
- feat_x = self.features_cb[1].currentText()
326
- feat_y = self.features_cb[0].currentText()
327
- min_x = self.df.dropna(subset=feat_x)[feat_x].min()
328
- max_x = self.df.dropna(subset=feat_x)[feat_x].max()
329
- min_y = self.df.dropna(subset=feat_y)[feat_y].min()
330
- max_y = self.df.dropna(subset=feat_y)[feat_y].max()
331
-
332
- x_padding = (max_x - min_x) * 0.05
333
- y_padding = (max_y - min_y) * 0.05
334
- if x_padding == 0:
335
- x_padding = 0.05
336
- if y_padding == 0:
337
- y_padding = 0.05
338
-
339
- if min_x == min_x and max_x == max_x:
340
- if self.ax_props.get_xscale() == 'linear':
341
- self.ax_props.set_xlim(min_x - x_padding, max_x + x_padding)
342
- else:
343
- self.ax_props.set_xlim(min_x, max_x)
344
- if min_y == min_y and max_y == max_y:
345
- if self.ax_props.get_yscale() == 'linear':
346
- self.ax_props.set_ylim(min_y - y_padding, max_y + y_padding)
347
- else:
348
- self.ax_props.set_ylim(min_y, max_y)
349
-
350
- self.propscanvas.canvas.toolbar.update()
351
-
352
- self.propscanvas.canvas.draw_idle()
353
-
354
- except Exception as e:
355
- print("Exception L355 ", e)
356
-
357
- def show_warning(self, message: str):
358
- msgBox = QMessageBox()
359
- msgBox.setIcon(QMessageBox.Warning)
360
- link = "https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.query.html"
361
- msgBox.setText(f"{message}<br><br>See <a href='{link}'>documentation</a>.")
362
- msgBox.setWindowTitle("Warning")
363
- msgBox.setStandardButtons(QMessageBox.Ok)
364
- msgBox.exec()
365
-
366
- def apply_property_query(self):
367
-
368
- query = self.property_query_le.text()
369
- try:
370
- self.df = classify_cells_from_query(self.df, self.name_le.text(), query)
371
- except EmptyQueryError as e:
372
- self.show_warning(str(e))
373
- except MissingColumnsError as e:
374
- self.show_warning(f"{e}. Please check your column names.")
375
- except QueryError as e:
376
- self.show_warning(f"{e}. Wrap features in backticks if needed.")
377
- except Exception as e:
378
- self.show_warning(f"Unexpected error: {e}")
379
-
380
- self.class_name = "status_"+self.name_le.text()
381
- if self.df is None:
382
- msgBox = QMessageBox()
383
- msgBox.setIcon(QMessageBox.Warning)
384
- msgBox.setText(f"The query could not be understood. No filtering was applied.")
385
- msgBox.setWindowTitle("Warning")
386
- msgBox.setStandardButtons(QMessageBox.Ok)
387
- returnValue = msgBox.exec()
388
- if returnValue == QMessageBox.Ok:
389
- self.auto_close = False
390
- return None
391
-
392
- self.update_props_scatter(feature_changed=False)
393
-
394
- def set_frame(self, value):
395
- xlim = self.ax_props.get_xlim()
396
- ylim = self.ax_props.get_ylim()
397
- self.currentFrame = value
398
- self.update_props_scatter(feature_changed=False)
399
- self.ax_props.set_xlim(xlim)
400
- self.ax_props.set_ylim(ylim)
401
-
402
- def set_transparency(self, value):
403
- xlim = self.ax_props.get_xlim()
404
- ylim = self.ax_props.get_ylim()
405
- self.currentAlpha = value
406
-
407
- self.update_props_scatter(feature_changed=False)
408
- self.ax_props.set_xlim(xlim)
409
- self.ax_props.set_ylim(ylim)
410
-
411
- def switch_projection(self):
412
- if self.project_times:
413
- self.project_times = False
414
- self.project_times_btn.setIcon(icon(MDI6.math_integral, color="black"))
415
- self.project_times_btn.setIconSize(QSize(20, 20))
416
- self.frame_slider.setEnabled(True)
417
- else:
418
- self.project_times = True
419
- self.project_times_btn.setIcon(icon(MDI6.math_integral_box, color="black"))
420
- self.project_times_btn.setIconSize(QSize(20, 20))
421
- self.frame_slider.setEnabled(False)
422
- self.update_props_scatter(feature_changed=False)
423
-
424
- def submit_classification(self):
425
-
426
- self.auto_close = True
427
- self.apply_property_query()
428
- if not self.auto_close:
429
- return None
430
-
431
- if self.time_corr.isChecked():
432
- self.class_name_user = 'class_'+self.name_le.text()
433
- print(f'User defined class name: {self.class_name_user}...')
434
- if self.class_name_user in self.df.columns:
435
-
436
- msgBox = QMessageBox()
437
- msgBox.setIcon(QMessageBox.Information)
438
- msgBox.setText(f"The class column {self.class_name_user} already exists in the table.\nProceeding will "
439
- f"reclassify. Do you want to continue?")
440
- msgBox.setWindowTitle("Warning")
441
- msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
442
- returnValue = msgBox.exec()
443
- if returnValue == QMessageBox.Yes:
444
- pass
445
- else:
446
- return None
447
-
448
- name_map = {self.class_name: self.class_name_user}
449
- self.df = self.df.drop(list(set(name_map.values()) & set(self.df.columns)), axis=1).rename(columns=name_map)
450
- self.df.reset_index(inplace=True, drop=True)
451
-
452
- pre_event = None
453
- if self.prereq_event_check.isChecked() and "t_"+self.prereq_event_cb.currentText() in self.cols:
454
- pre_event = self.prereq_event_cb.currentText()
455
-
456
- self.df = interpret_track_classification(self.df, self.class_name_user, irreversible_event=self.irreversible_event_btn.isChecked(), unique_state=self.unique_state_btn.isChecked(), transient_event=self.transient_event_btn.isChecked(), r2_threshold=self.r2_slider.value(), pre_event=pre_event)
457
-
458
- else:
459
- self.group_name_user = 'group_' + self.name_le.text()
460
- print(f'User defined characteristic group name: {self.group_name_user}.')
461
- if self.group_name_user in self.df.columns:
462
-
463
- msgBox = QMessageBox()
464
- msgBox.setIcon(QMessageBox.Information)
465
- msgBox.setText(
466
- f"The group column {self.group_name_user} already exists in the table.\nProceeding will "
467
- f"reclassify. Do you want to continue?")
468
- msgBox.setWindowTitle("Warning")
469
- msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
470
- returnValue = msgBox.exec()
471
- if returnValue == QMessageBox.Yes:
472
- pass
473
- else:
474
- return None
475
-
476
- name_map = {self.class_name: self.group_name_user}
477
- self.df = self.df.drop(list(set(name_map.values()) & set(self.df.columns)), axis=1).rename(columns=name_map)
478
- print(self.df.columns)
479
- # self.df[self.group_name_user] = self.df[self.group_name_user].replace({0: 1, 1: 0})
480
- self.df.reset_index(inplace=True, drop=True)
481
-
482
- if 'custom' in list(self.df.columns):
483
- self.df = self.df.drop(['custom'], axis=1)
484
-
485
- self.fig_props.set_size_inches(4, 3)
486
- self.fig_props.suptitle(self.property_query_le.text(), fontsize=10)
487
- self.fig_props.tight_layout()
488
- for pos, pos_group in self.df.groupby('position'):
489
- self.fig_props.savefig(str(pos)+os.sep.join(['output', f'{self.class_name}.png']), bbox_inches='tight', dpi=300)
490
- pos_group.to_csv(str(pos)+os.sep.join(['output', 'tables', f'trajectories_{self.mode}.csv']), index=False)
491
-
492
- self.parent_window.parent_window.update_position_options()
493
- self.close()
494
-
495
-
496
- def help_propagate(self):
497
-
498
- """
499
- Helper for segmentation strategy between threshold-based and Deep learning.
500
- """
501
-
502
- dict_path = os.sep.join([get_software_location(), 'celldetective', 'gui', 'help', 'propagate-classification.json'])
503
-
504
- with open(dict_path) as f:
505
- d = json.load(f)
506
-
507
- suggestion = help_generic(d)
508
- if isinstance(suggestion, str):
509
- print(f"{suggestion=}")
510
- msgBox = QMessageBox()
511
- msgBox.setIcon(QMessageBox.Information)
512
- msgBox.setTextFormat(Qt.RichText)
513
- msgBox.setText(rf"{suggestion}")
514
- msgBox.setWindowTitle("Info")
515
- msgBox.setStandardButtons(QMessageBox.Ok)
516
- returnValue = msgBox.exec()
517
- if returnValue == QMessageBox.Ok:
518
- return None
519
-
520
- def switch_to_log(self, i):
521
-
522
- """
523
- Switch threshold histogram to log scale. Auto adjust.
524
- """
525
-
526
- if i == 1:
527
- try:
528
- feat_x = self.features_cb[1].currentText()
529
- min_x = self.df.dropna(subset=feat_x)[feat_x].min()
530
- max_x = self.df.dropna(subset=feat_x)[feat_x].max()
531
- x_padding = (max_x - min_x) * 0.05
532
- if x_padding == 0:
533
- x_padding = 0.05
534
-
535
- if self.ax_props.get_xscale() == 'linear':
536
- self.ax_props.set_xlim(min_x, max_x)
537
- self.ax_props.set_xscale('log')
538
- self.log_btns[i].setIcon(icon(MDI6.math_log, color="#1565c0"))
539
- else:
540
- self.ax_props.set_xscale('linear')
541
- self.ax_props.set_xlim(min_x - x_padding, max_x + x_padding)
542
- self.log_btns[i].setIcon(icon(MDI6.math_log, color="black"))
543
- except Exception as e:
544
- print(e)
545
- elif i == 0:
546
- try:
547
- feat_y = self.features_cb[0].currentText()
548
- min_y = self.df.dropna(subset=feat_y)[feat_y].min()
549
- max_y = self.df.dropna(subset=feat_y)[feat_y].max()
550
- y_padding = (max_y - min_y) * 0.05
551
- if y_padding == 0:
552
- y_padding = 0.05
553
-
554
- if self.ax_props.get_yscale() == 'linear':
555
- self.ax_props.set_ylim(min_y, max_y)
556
- self.ax_props.set_yscale('log')
557
- self.log_btns[i].setIcon(icon(MDI6.math_log, color="#1565c0"))
558
- else:
559
- self.ax_props.set_yscale('linear')
560
- self.ax_props.set_ylim(min_y - y_padding, max_y + y_padding)
561
- self.log_btns[i].setIcon(icon(MDI6.math_log, color="black"))
562
- except Exception as e:
563
- print(e)
564
-
565
- self.ax_props.autoscale()
566
- self.propscanvas.canvas.draw_idle()
567
-
568
- print('Done.')
36
+ def __init__(self, parent_window):
37
+
38
+ super().__init__()
39
+
40
+ self.parent_window = parent_window
41
+ self.screen_height = (
42
+ self.parent_window.parent_window.parent_window.screen_height
43
+ )
44
+ self.screen_width = self.parent_window.parent_window.parent_window.screen_width
45
+ self.currentAlpha = 1.0
46
+
47
+ self.setWindowTitle("Custom classification")
48
+
49
+ self.mode = self.parent_window.mode
50
+ self.df = self.parent_window.df
51
+
52
+ self.cols = self.df.select_dtypes(include=[np.number]).columns.tolist()
53
+
54
+ self.class_name = "custom"
55
+ self.name_le = QLineEdit(self.class_name)
56
+ self.init_class()
57
+
58
+ layout = QVBoxLayout(self)
59
+ layout.setContentsMargins(30, 30, 30, 30)
60
+
61
+ name_layout = QHBoxLayout()
62
+ name_layout.addWidget(QLabel("class name: "), 33)
63
+ name_layout.addWidget(self.name_le, 66)
64
+ layout.addLayout(name_layout)
65
+
66
+ fig_btn_hbox = QHBoxLayout()
67
+ fig_btn_hbox.addWidget(QLabel(""), 95)
68
+ self.project_times_btn = QPushButton("")
69
+ self.project_times_btn.setStyleSheet(
70
+ self.parent_window.parent_window.parent_window.button_select_all
71
+ )
72
+ self.project_times_btn.setIcon(icon(MDI6.math_integral, color="black"))
73
+ self.project_times_btn.setToolTip("Project measurements at all times.")
74
+ self.project_times_btn.setIconSize(QSize(20, 20))
75
+ self.project_times = False
76
+ self.project_times_btn.clicked.connect(self.switch_projection)
77
+ fig_btn_hbox.addWidget(self.project_times_btn, 5)
78
+ layout.addLayout(fig_btn_hbox)
79
+
80
+ # Figure
81
+ self.initalize_props_scatter()
82
+ layout.addWidget(self.propscanvas)
83
+
84
+ # slider
85
+ self.frame_slider = QLabeledSlider()
86
+ self.frame_slider.setSingleStep(1)
87
+ self.frame_slider.setOrientation(Qt.Horizontal)
88
+ self.frame_slider.setRange(0, int(self.df.FRAME.max()) - 1)
89
+ self.frame_slider.setValue(0)
90
+ self.currentFrame = 0
91
+
92
+ slider_hbox = QHBoxLayout()
93
+ slider_hbox.addWidget(QLabel("frame: "), 10)
94
+ slider_hbox.addWidget(self.frame_slider, 90)
95
+ layout.addLayout(slider_hbox)
96
+
97
+ # transparency slider
98
+ self.alpha_slider = QLabeledDoubleSlider()
99
+ self.alpha_slider.setSingleStep(0.001)
100
+ self.alpha_slider.setOrientation(Qt.Horizontal)
101
+ self.alpha_slider.setRange(0, 1)
102
+ self.alpha_slider.setValue(1.0)
103
+ self.alpha_slider.setDecimals(3)
104
+
105
+ slider_alpha_hbox = QHBoxLayout()
106
+ slider_alpha_hbox.addWidget(QLabel("transparency: "), 10)
107
+ slider_alpha_hbox.addWidget(self.alpha_slider, 90)
108
+ layout.addLayout(slider_alpha_hbox)
109
+
110
+ self.features_cb = [QSearchableComboBox() for _ in range(2)]
111
+ self.log_btns = [QPushButton() for _ in range(2)]
112
+
113
+ for i in range(2):
114
+ hbox_feat = QHBoxLayout()
115
+ hbox_feat.addWidget(QLabel(f"feature {i}: "), 20)
116
+ hbox_feat.addWidget(self.features_cb[i], 75)
117
+ hbox_feat.addWidget(self.log_btns[i], 5)
118
+ layout.addLayout(hbox_feat)
119
+
120
+ self.features_cb[i].clear()
121
+ self.features_cb[i].addItems(sorted(list(self.cols), key=str.lower))
122
+ self.features_cb[i].currentTextChanged.connect(self.update_props_scatter)
123
+ self.features_cb[i].setCurrentIndex(i)
124
+
125
+ self.log_btns[i].setIcon(icon(MDI6.math_log, color="black"))
126
+ self.log_btns[i].setStyleSheet(self.button_select_all)
127
+ self.log_btns[i].clicked.connect(lambda ch, i=i: self.switch_to_log(i))
128
+
129
+ hbox_classify = QHBoxLayout()
130
+ hbox_classify.addWidget(QLabel("classify: "), 10)
131
+ self.property_query_le = QLineEdit()
132
+ self.property_query_le.setPlaceholderText(
133
+ "classify points using a query such as: area > 100 or eccentricity > 0.95"
134
+ )
135
+ self.property_query_le.setToolTip(
136
+ 'Classify points using a query on measurements.\nYou can use "and" and "or" conditions to combine\nmeasurements (e.g. "area > 100 or eccentricity > 0.95").'
137
+ )
138
+ self.property_query_le.textChanged.connect(self.activate_submit_btn)
139
+ hbox_classify.addWidget(self.property_query_le, 70)
140
+ self.submit_query_btn = QPushButton("Preview")
141
+ self.submit_query_btn.clicked.connect(self.apply_property_query)
142
+ self.submit_query_btn.setEnabled(False)
143
+ hbox_classify.addWidget(self.submit_query_btn, 20)
144
+ layout.addLayout(hbox_classify)
145
+
146
+ self.time_corr = QCheckBox("Time correlated")
147
+ if "TRACK_ID" in self.df.columns:
148
+ self.time_corr.setEnabled(True)
149
+ else:
150
+ self.time_corr.setEnabled(False)
151
+
152
+ time_prop_hbox = QHBoxLayout()
153
+ time_prop_hbox.addWidget(self.time_corr, alignment=Qt.AlignCenter)
154
+
155
+ self.help_propagate_btn = QPushButton()
156
+ self.help_propagate_btn.setIcon(icon(MDI6.help_circle, color=self.help_color))
157
+ self.help_propagate_btn.setIconSize(QSize(20, 20))
158
+ self.help_propagate_btn.clicked.connect(self.help_propagate)
159
+ self.help_propagate_btn.setStyleSheet(self.button_select_all)
160
+ self.help_propagate_btn.setToolTip("Help.")
161
+ time_prop_hbox.addWidget(self.help_propagate_btn, 5, alignment=Qt.AlignRight)
162
+
163
+ layout.addLayout(time_prop_hbox)
164
+
165
+ self.irreversible_event_btn = QRadioButton("irreversible event")
166
+ self.unique_state_btn = QRadioButton("unique state")
167
+ self.transient_event_btn = QRadioButton("transient event")
168
+ time_corr_btn_group = QButtonGroup()
169
+ self.unique_state_btn.click()
170
+
171
+ time_corr_layout = QHBoxLayout()
172
+ time_corr_layout.addWidget(self.unique_state_btn, 33, alignment=Qt.AlignCenter)
173
+ time_corr_layout.addWidget(
174
+ self.irreversible_event_btn, 33, alignment=Qt.AlignCenter
175
+ )
176
+ time_corr_layout.addWidget(
177
+ self.transient_event_btn, 33, alignment=Qt.AlignCenter
178
+ )
179
+ layout.addLayout(time_corr_layout)
180
+
181
+ self.prereq_event_check = QCheckBox("prerequisite event:")
182
+ self.prereq_event_check.toggled.connect(self.activate_prereq_cb)
183
+ self.prereq_event_cb = QComboBox()
184
+ event_cols = ["--"] + [
185
+ c.replace("t_", "") for c in self.cols if c.startswith("t_")
186
+ ]
187
+ self.prereq_event_cb.addItems(event_cols)
188
+ self.prereq_event_check.setEnabled(False)
189
+ self.prereq_event_cb.setEnabled(False)
190
+
191
+ self.r2_slider = QLabeledDoubleSlider()
192
+ self.r2_slider.setValue(0.75)
193
+ self.r2_slider.setRange(0, 1)
194
+ self.r2_slider.setSingleStep(0.01)
195
+ self.r2_slider.setOrientation(Qt.Horizontal)
196
+ self.r2_label = QLabel("R2 tolerance:")
197
+ self.r2_label.setToolTip(
198
+ "Minimum R2 between the fit sigmoid and the binary response to the filters to accept the event."
199
+ )
200
+
201
+ r2_threshold_layout = QHBoxLayout()
202
+ r2_threshold_layout.addWidget(QLabel(""), 33)
203
+ r2_threshold_layout.addWidget(self.r2_label, 13)
204
+ r2_threshold_layout.addWidget(self.r2_slider, 20)
205
+ r2_threshold_layout.addWidget(QLabel(""), 33)
206
+
207
+ layout.addLayout(r2_threshold_layout)
208
+
209
+ self.time_corr_options = [
210
+ self.irreversible_event_btn,
211
+ self.unique_state_btn,
212
+ self.prereq_event_check,
213
+ self.prereq_event_cb,
214
+ self.transient_event_btn,
215
+ ]
216
+ for btn in [
217
+ self.irreversible_event_btn,
218
+ self.unique_state_btn,
219
+ self.transient_event_btn,
220
+ ]:
221
+ time_corr_btn_group.addButton(btn)
222
+ btn.setEnabled(False)
223
+ self.time_corr.toggled.connect(self.activate_time_corr_options)
224
+
225
+ self.irreversible_event_btn.clicked.connect(self.activate_r2)
226
+ self.unique_state_btn.clicked.connect(self.activate_r2)
227
+ self.transient_event_btn.clicked.connect(self.activate_r2)
228
+
229
+ for wg in [self.r2_slider, self.r2_label]:
230
+ wg.setEnabled(False)
231
+
232
+ prereq_layout = QHBoxLayout()
233
+ prereq_layout.setContentsMargins(30, 0, 0, 0)
234
+ prereq_layout.addWidget(self.prereq_event_check, 20)
235
+ prereq_layout.addWidget(self.prereq_event_cb, 80)
236
+ layout.addLayout(prereq_layout)
237
+
238
+ layout.addWidget(QLabel())
239
+
240
+ self.submit_btn = QPushButton("apply")
241
+ self.submit_btn.setStyleSheet(self.button_style_sheet)
242
+ self.submit_btn.clicked.connect(self.submit_classification)
243
+ self.submit_btn.setEnabled(False)
244
+ layout.addWidget(self.submit_btn, 30)
245
+
246
+ self.frame_slider.valueChanged.connect(self.set_frame)
247
+ self.alpha_slider.valueChanged.connect(self.set_transparency)
248
+
249
+ def activate_prereq_cb(self):
250
+ if self.prereq_event_check.isChecked():
251
+ self.prereq_event_cb.setEnabled(True)
252
+ else:
253
+ self.prereq_event_cb.setEnabled(False)
254
+
255
+ def activate_submit_btn(self):
256
+
257
+ if self.property_query_le.text() == "":
258
+ self.submit_query_btn.setEnabled(False)
259
+ self.submit_btn.setEnabled(False)
260
+ else:
261
+ self.submit_query_btn.setEnabled(True)
262
+ self.submit_btn.setEnabled(True)
263
+
264
+ def activate_r2(self):
265
+ if self.irreversible_event_btn.isChecked() and self.time_corr.isChecked():
266
+ for wg in [self.r2_slider, self.r2_label]:
267
+ wg.setEnabled(True)
268
+ else:
269
+ for wg in [self.r2_slider, self.r2_label]:
270
+ wg.setEnabled(False)
271
+
272
+ def activate_time_corr_options(self):
273
+
274
+ if self.time_corr.isChecked():
275
+ for btn in self.time_corr_options:
276
+ btn.setEnabled(True)
277
+ if self.irreversible_event_btn.isChecked():
278
+ for wg in [self.r2_slider, self.r2_label]:
279
+ wg.setEnabled(True)
280
+ else:
281
+ for wg in [self.r2_slider, self.r2_label]:
282
+ wg.setEnabled(False)
283
+ else:
284
+ for btn in self.time_corr_options:
285
+ btn.setEnabled(False)
286
+ for wg in [self.r2_slider, self.r2_label]:
287
+ wg.setEnabled(False)
288
+
289
+ def init_class(self):
290
+
291
+ self.class_name = "custom"
292
+ i = 1
293
+ while self.class_name in self.df.columns:
294
+ self.class_name = f"custom_{i}"
295
+ i += 1
296
+ self.name_le.setText(self.class_name)
297
+ self.df.loc[:, self.class_name] = 1
298
+
299
+ def initalize_props_scatter(self):
300
+ """
301
+ Define properties scatter.
302
+ """
303
+
304
+ self.fig_props, self.ax_props = plt.subplots(figsize=(4, 4), tight_layout=True)
305
+ self.propscanvas = FigureCanvas(self.fig_props, interactive=True)
306
+ self.fig_props.set_facecolor("none")
307
+ self.scat_props = self.ax_props.scatter(
308
+ [], [], color="k", alpha=self.currentAlpha
309
+ )
310
+ self.propscanvas.canvas.draw_idle()
311
+ self.propscanvas.canvas.setMinimumHeight(self.screen_height // 5)
312
+
313
+ def closeEvent(self, event):
314
+ self.ax_props.cla()
315
+ self.fig_props.clf()
316
+ plt.close(self.fig_props)
317
+ super().closeEvent(event)
318
+
319
+ def update_props_scatter(self, feature_changed=True):
320
+
321
+ try:
322
+ if np.any(self.df[self.features_cb[0].currentText()].to_numpy() <= 0.0):
323
+ if self.ax_props.get_yscale() == "log":
324
+ self.log_btns[0].click()
325
+ self.log_btns[0].setEnabled(False)
326
+ else:
327
+ self.log_btns[0].setEnabled(True)
328
+
329
+ if np.any(self.df[self.features_cb[1].currentText()].to_numpy() <= 0.0):
330
+ if self.ax_props.get_xscale() == "log":
331
+ self.log_btns[1].click()
332
+ self.log_btns[1].setEnabled(False)
333
+ else:
334
+ self.log_btns[1].setEnabled(True)
335
+ except Exception as e:
336
+ print(e)
337
+
338
+ class_name = self.class_name
339
+
340
+ try:
341
+
342
+ if not self.project_times:
343
+ self.scat_props.set_offsets(
344
+ self.df.loc[
345
+ self.df["FRAME"] == self.currentFrame,
346
+ [
347
+ self.features_cb[1].currentText(),
348
+ self.features_cb[0].currentText(),
349
+ ],
350
+ ].to_numpy()
351
+ )
352
+ colors = [
353
+ color_from_status(c)
354
+ for c in self.df.loc[
355
+ self.df["FRAME"] == self.currentFrame, class_name
356
+ ].to_numpy()
357
+ ]
358
+ self.scat_props.set_facecolor(colors)
359
+ else:
360
+ self.scat_props.set_offsets(
361
+ self.df[
362
+ [
363
+ self.features_cb[1].currentText(),
364
+ self.features_cb[0].currentText(),
365
+ ]
366
+ ].to_numpy()
367
+ )
368
+ colors = [color_from_status(c) for c in self.df[class_name].to_numpy()]
369
+ self.scat_props.set_facecolor(colors)
370
+
371
+ self.scat_props.set_alpha(self.currentAlpha)
372
+
373
+ if feature_changed:
374
+
375
+ self.ax_props.set_xlabel(self.features_cb[1].currentText())
376
+ self.ax_props.set_ylabel(self.features_cb[0].currentText())
377
+
378
+ feat_x = self.features_cb[1].currentText()
379
+ feat_y = self.features_cb[0].currentText()
380
+ min_x = self.df.dropna(subset=feat_x)[feat_x].min()
381
+ max_x = self.df.dropna(subset=feat_x)[feat_x].max()
382
+ min_y = self.df.dropna(subset=feat_y)[feat_y].min()
383
+ max_y = self.df.dropna(subset=feat_y)[feat_y].max()
384
+
385
+ x_padding = (max_x - min_x) * 0.05
386
+ y_padding = (max_y - min_y) * 0.05
387
+ if x_padding == 0:
388
+ x_padding = 0.05
389
+ if y_padding == 0:
390
+ y_padding = 0.05
391
+
392
+ if min_x == min_x and max_x == max_x:
393
+ if self.ax_props.get_xscale() == "linear":
394
+ self.ax_props.set_xlim(min_x - x_padding, max_x + x_padding)
395
+ else:
396
+ self.ax_props.set_xlim(min_x, max_x)
397
+ if min_y == min_y and max_y == max_y:
398
+ if self.ax_props.get_yscale() == "linear":
399
+ self.ax_props.set_ylim(min_y - y_padding, max_y + y_padding)
400
+ else:
401
+ self.ax_props.set_ylim(min_y, max_y)
402
+
403
+ self.propscanvas.canvas.toolbar.update()
404
+
405
+ self.propscanvas.canvas.draw_idle()
406
+
407
+ except Exception as e:
408
+ print("Exception L355 ", e)
409
+
410
+ def show_warning(self, message: str):
411
+ msgBox = QMessageBox()
412
+ msgBox.setIcon(QMessageBox.Warning)
413
+ link = (
414
+ "https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.query.html"
415
+ )
416
+ msgBox.setText(f"{message}<br><br>See <a href='{link}'>documentation</a>.")
417
+ msgBox.setWindowTitle("Warning")
418
+ msgBox.setStandardButtons(QMessageBox.Ok)
419
+ msgBox.exec()
420
+
421
+ def apply_property_query(self):
422
+
423
+ query = self.property_query_le.text()
424
+ try:
425
+ self.df = classify_cells_from_query(self.df, self.name_le.text(), query)
426
+ except EmptyQueryError as e:
427
+ self.show_warning(str(e))
428
+ except MissingColumnsError as e:
429
+ self.show_warning(f"{e}. Please check your column names.")
430
+ except QueryError as e:
431
+ self.show_warning(f"{e}. Wrap features in backticks if needed.")
432
+ except Exception as e:
433
+ self.show_warning(f"Unexpected error: {e}")
434
+
435
+ self.class_name = "status_" + self.name_le.text()
436
+ if self.df is None:
437
+ msgBox = QMessageBox()
438
+ msgBox.setIcon(QMessageBox.Warning)
439
+ msgBox.setText(
440
+ f"The query could not be understood. No filtering was applied."
441
+ )
442
+ msgBox.setWindowTitle("Warning")
443
+ msgBox.setStandardButtons(QMessageBox.Ok)
444
+ returnValue = msgBox.exec()
445
+ if returnValue == QMessageBox.Ok:
446
+ self.auto_close = False
447
+ return None
448
+
449
+ self.update_props_scatter(feature_changed=False)
450
+
451
+ def set_frame(self, value):
452
+ xlim = self.ax_props.get_xlim()
453
+ ylim = self.ax_props.get_ylim()
454
+ self.currentFrame = value
455
+ self.update_props_scatter(feature_changed=False)
456
+ self.ax_props.set_xlim(xlim)
457
+ self.ax_props.set_ylim(ylim)
458
+
459
+ def set_transparency(self, value):
460
+ xlim = self.ax_props.get_xlim()
461
+ ylim = self.ax_props.get_ylim()
462
+ self.currentAlpha = value
463
+
464
+ self.update_props_scatter(feature_changed=False)
465
+ self.ax_props.set_xlim(xlim)
466
+ self.ax_props.set_ylim(ylim)
467
+
468
+ def switch_projection(self):
469
+ if self.project_times:
470
+ self.project_times = False
471
+ self.project_times_btn.setIcon(icon(MDI6.math_integral, color="black"))
472
+ self.project_times_btn.setIconSize(QSize(20, 20))
473
+ self.frame_slider.setEnabled(True)
474
+ else:
475
+ self.project_times = True
476
+ self.project_times_btn.setIcon(icon(MDI6.math_integral_box, color="black"))
477
+ self.project_times_btn.setIconSize(QSize(20, 20))
478
+ self.frame_slider.setEnabled(False)
479
+ self.update_props_scatter(feature_changed=False)
480
+
481
+ def submit_classification(self):
482
+
483
+ self.auto_close = True
484
+ self.apply_property_query()
485
+ if not self.auto_close:
486
+ return None
487
+
488
+ if self.time_corr.isChecked():
489
+ self.class_name_user = "class_" + self.name_le.text()
490
+ print(f"User defined class name: {self.class_name_user}...")
491
+ if self.class_name_user in self.df.columns:
492
+
493
+ msgBox = QMessageBox()
494
+ msgBox.setIcon(QMessageBox.Information)
495
+ msgBox.setText(
496
+ f"The class column {self.class_name_user} already exists in the table.\nProceeding will "
497
+ f"reclassify. Do you want to continue?"
498
+ )
499
+ msgBox.setWindowTitle("Warning")
500
+ msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
501
+ returnValue = msgBox.exec()
502
+ if returnValue == QMessageBox.Yes:
503
+ pass
504
+ else:
505
+ return None
506
+
507
+ name_map = {self.class_name: self.class_name_user}
508
+ self.df = self.df.drop(
509
+ list(set(name_map.values()) & set(self.df.columns)), axis=1
510
+ ).rename(columns=name_map)
511
+ self.df.reset_index(inplace=True, drop=True)
512
+
513
+ pre_event = None
514
+ if (
515
+ self.prereq_event_check.isChecked()
516
+ and "t_" + self.prereq_event_cb.currentText() in self.cols
517
+ ):
518
+ pre_event = self.prereq_event_cb.currentText()
519
+
520
+ self.df = interpret_track_classification(
521
+ self.df,
522
+ self.class_name_user,
523
+ irreversible_event=self.irreversible_event_btn.isChecked(),
524
+ unique_state=self.unique_state_btn.isChecked(),
525
+ transient_event=self.transient_event_btn.isChecked(),
526
+ r2_threshold=self.r2_slider.value(),
527
+ pre_event=pre_event,
528
+ )
529
+
530
+ else:
531
+ self.group_name_user = "group_" + self.name_le.text()
532
+ print(f"User defined characteristic group name: {self.group_name_user}.")
533
+ if self.group_name_user in self.df.columns:
534
+
535
+ msgBox = QMessageBox()
536
+ msgBox.setIcon(QMessageBox.Information)
537
+ msgBox.setText(
538
+ f"The group column {self.group_name_user} already exists in the table.\nProceeding will "
539
+ f"reclassify. Do you want to continue?"
540
+ )
541
+ msgBox.setWindowTitle("Warning")
542
+ msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
543
+ returnValue = msgBox.exec()
544
+ if returnValue == QMessageBox.Yes:
545
+ pass
546
+ else:
547
+ return None
548
+
549
+ name_map = {self.class_name: self.group_name_user}
550
+ self.df = self.df.drop(
551
+ list(set(name_map.values()) & set(self.df.columns)), axis=1
552
+ ).rename(columns=name_map)
553
+ print(self.df.columns)
554
+ # self.df[self.group_name_user] = self.df[self.group_name_user].replace({0: 1, 1: 0})
555
+ self.df.reset_index(inplace=True, drop=True)
556
+
557
+ if "custom" in list(self.df.columns):
558
+ self.df = self.df.drop(["custom"], axis=1)
559
+
560
+ self.fig_props.set_size_inches(4, 3)
561
+ self.fig_props.suptitle(self.property_query_le.text(), fontsize=10)
562
+ self.fig_props.tight_layout()
563
+ for pos, pos_group in self.df.groupby("position"):
564
+ self.fig_props.savefig(
565
+ str(pos) + os.sep.join(["output", f"{self.class_name}.png"]),
566
+ bbox_inches="tight",
567
+ dpi=300,
568
+ )
569
+ pos_group.to_csv(
570
+ str(pos)
571
+ + os.sep.join(["output", "tables", f"trajectories_{self.mode}.csv"]),
572
+ index=False,
573
+ )
574
+
575
+ self.parent_window.parent_window.update_position_options()
576
+ self.close()
577
+
578
+ def help_propagate(self):
579
+ """
580
+ Helper for segmentation strategy between threshold-based and Deep learning.
581
+ """
582
+
583
+ dict_path = os.sep.join(
584
+ [
585
+ get_software_location(),
586
+ "celldetective",
587
+ "gui",
588
+ "help",
589
+ "propagate-classification.json",
590
+ ]
591
+ )
592
+
593
+ with open(dict_path) as f:
594
+ d = json.load(f)
595
+
596
+ suggestion = help_generic(d)
597
+ if isinstance(suggestion, str):
598
+ print(f"{suggestion=}")
599
+ msgBox = QMessageBox()
600
+ msgBox.setIcon(QMessageBox.Information)
601
+ msgBox.setTextFormat(Qt.RichText)
602
+ msgBox.setText(rf"{suggestion}")
603
+ msgBox.setWindowTitle("Info")
604
+ msgBox.setStandardButtons(QMessageBox.Ok)
605
+ returnValue = msgBox.exec()
606
+ if returnValue == QMessageBox.Ok:
607
+ return None
608
+
609
+ def switch_to_log(self, i):
610
+ """
611
+ Switch threshold histogram to log scale. Auto adjust.
612
+ """
613
+
614
+ if i == 1:
615
+ try:
616
+ feat_x = self.features_cb[1].currentText()
617
+ min_x = self.df.dropna(subset=feat_x)[feat_x].min()
618
+ max_x = self.df.dropna(subset=feat_x)[feat_x].max()
619
+ x_padding = (max_x - min_x) * 0.05
620
+ if x_padding == 0:
621
+ x_padding = 0.05
622
+
623
+ if self.ax_props.get_xscale() == "linear":
624
+ self.ax_props.set_xlim(min_x, max_x)
625
+ self.ax_props.set_xscale("log")
626
+ self.log_btns[i].setIcon(icon(MDI6.math_log, color="#1565c0"))
627
+ else:
628
+ self.ax_props.set_xscale("linear")
629
+ self.ax_props.set_xlim(min_x - x_padding, max_x + x_padding)
630
+ self.log_btns[i].setIcon(icon(MDI6.math_log, color="black"))
631
+ except Exception as e:
632
+ print(e)
633
+ elif i == 0:
634
+ try:
635
+ feat_y = self.features_cb[0].currentText()
636
+ min_y = self.df.dropna(subset=feat_y)[feat_y].min()
637
+ max_y = self.df.dropna(subset=feat_y)[feat_y].max()
638
+ y_padding = (max_y - min_y) * 0.05
639
+ if y_padding == 0:
640
+ y_padding = 0.05
641
+
642
+ if self.ax_props.get_yscale() == "linear":
643
+ self.ax_props.set_ylim(min_y, max_y)
644
+ self.ax_props.set_yscale("log")
645
+ self.log_btns[i].setIcon(icon(MDI6.math_log, color="#1565c0"))
646
+ else:
647
+ self.ax_props.set_yscale("linear")
648
+ self.ax_props.set_ylim(min_y - y_padding, max_y + y_padding)
649
+ self.log_btns[i].setIcon(icon(MDI6.math_log, color="black"))
650
+ except Exception as e:
651
+ print(e)
652
+
653
+ self.ax_props.autoscale()
654
+ self.propscanvas.canvas.draw_idle()
655
+
656
+ print("Done.")