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