celldetective 1.2.1__py3-none-any.whl → 1.2.2.post1__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 (57) hide show
  1. celldetective/__main__.py +12 -5
  2. celldetective/events.py +28 -2
  3. celldetective/gui/about.py +0 -1
  4. celldetective/gui/analyze_block.py +3 -18
  5. celldetective/gui/btrack_options.py +126 -21
  6. celldetective/gui/classifier_widget.py +67 -111
  7. celldetective/gui/configure_new_exp.py +37 -4
  8. celldetective/gui/control_panel.py +14 -30
  9. celldetective/gui/generic_signal_plot.py +793 -0
  10. celldetective/gui/gui_utils.py +401 -226
  11. celldetective/gui/json_readers.py +0 -2
  12. celldetective/gui/layouts.py +269 -25
  13. celldetective/gui/measurement_options.py +14 -23
  14. celldetective/gui/neighborhood_options.py +3 -15
  15. celldetective/gui/plot_measurements.py +10 -23
  16. celldetective/gui/plot_signals_ui.py +53 -687
  17. celldetective/gui/process_block.py +320 -186
  18. celldetective/gui/retrain_segmentation_model_options.py +30 -47
  19. celldetective/gui/retrain_signal_model_options.py +5 -14
  20. celldetective/gui/seg_model_loader.py +129 -113
  21. celldetective/gui/signal_annotator.py +89 -99
  22. celldetective/gui/signal_annotator2.py +5 -9
  23. celldetective/gui/styles.py +32 -0
  24. celldetective/gui/survival_ui.py +49 -712
  25. celldetective/gui/tableUI.py +0 -1
  26. celldetective/gui/thresholds_gui.py +38 -11
  27. celldetective/gui/viewers.py +6 -7
  28. celldetective/io.py +60 -82
  29. celldetective/measure.py +374 -15
  30. celldetective/neighborhood.py +1 -7
  31. celldetective/preprocessing.py +2 -4
  32. celldetective/relative_measurements.py +0 -3
  33. celldetective/scripts/analyze_signals.py +0 -1
  34. celldetective/scripts/measure_cells.py +1 -3
  35. celldetective/scripts/measure_relative.py +1 -2
  36. celldetective/scripts/segment_cells.py +16 -12
  37. celldetective/scripts/segment_cells_thresholds.py +17 -10
  38. celldetective/scripts/track_cells.py +18 -18
  39. celldetective/scripts/train_segmentation_model.py +1 -2
  40. celldetective/scripts/train_signal_model.py +0 -3
  41. celldetective/segmentation.py +1 -1
  42. celldetective/signals.py +17 -8
  43. celldetective/tracking.py +2 -1
  44. celldetective/utils.py +42 -2
  45. {celldetective-1.2.1.dist-info → celldetective-1.2.2.post1.dist-info}/METADATA +19 -12
  46. celldetective-1.2.2.post1.dist-info/RECORD +86 -0
  47. {celldetective-1.2.1.dist-info → celldetective-1.2.2.post1.dist-info}/WHEEL +1 -1
  48. celldetective/models/segmentation_effectors/primNK_cfse/config_input.json +0 -29
  49. celldetective/models/segmentation_effectors/primNK_cfse/cp-cfse-transfer +0 -0
  50. celldetective/models/segmentation_effectors/primNK_cfse/training_instructions.json +0 -37
  51. celldetective/models/segmentation_effectors/ricm-bimodal/config_input.json +0 -130
  52. celldetective/models/segmentation_effectors/ricm-bimodal/ricm-bimodal +0 -0
  53. celldetective/models/segmentation_effectors/ricm-bimodal/training_instructions.json +0 -37
  54. celldetective-1.2.1.dist-info/RECORD +0 -91
  55. {celldetective-1.2.1.dist-info → celldetective-1.2.2.post1.dist-info}/LICENSE +0 -0
  56. {celldetective-1.2.1.dist-info → celldetective-1.2.2.post1.dist-info}/entry_points.txt +0 -0
  57. {celldetective-1.2.1.dist-info → celldetective-1.2.2.post1.dist-info}/top_level.txt +0 -0
celldetective/__main__.py CHANGED
@@ -27,10 +27,10 @@ class AppInitWindow(QMainWindow):
27
27
 
28
28
  try:
29
29
  subprocess.check_output('nvidia-smi')
30
- print('Nvidia GPU detected')
30
+ print('NVIDIA GPU detected (activate or disable in Memory & Threads)...')
31
31
  self.use_gpu = True
32
32
  except Exception: # this command not being found can raise quite a few different errors depending on the configuration
33
- print('No Nvidia GPU in system!')
33
+ print('No NVIDIA GPU detected...')
34
34
  self.use_gpu = False
35
35
 
36
36
  self.soft_path = get_software_location()
@@ -170,13 +170,16 @@ class AppInitWindow(QMainWindow):
170
170
 
171
171
  self.recentFileActs = []
172
172
  self.threads_config_path = os.sep.join([self.soft_path,'celldetective','threads.json'])
173
+ print('Reading previous Memory & Threads settings...')
173
174
  if os.path.exists(self.threads_config_path):
174
175
  with open(self.threads_config_path, 'r') as f:
175
176
  self.threads_config = json.load(f)
176
177
  if 'use_gpu' in self.threads_config:
177
178
  self.use_gpu = bool(self.threads_config['use_gpu'])
179
+ print(f'Use GPU: {self.use_gpu}...')
178
180
  if 'n_threads' in self.threads_config:
179
181
  self.n_threads = int(self.threads_config['n_threads'])
182
+ print(f'Number of threads: {self.n_threads}...')
180
183
 
181
184
 
182
185
  def reload_previous_experiments(self):
@@ -250,9 +253,9 @@ class AppInitWindow(QMainWindow):
250
253
  self.open_directory()
251
254
 
252
255
  def load_recent_exp(self, path):
253
- print('loading?')
254
- print('you selected path ', path)
256
+
255
257
  self.experiment_path_selection.setText(path)
258
+ print(f'Attempt to load experiment folder: {path}...')
256
259
  self.open_directory()
257
260
 
258
261
  def open_about_window(self):
@@ -351,7 +354,7 @@ class AppInitWindow(QMainWindow):
351
354
  print(f"Found {self.number_of_wells} wells...")
352
355
  number_pos = []
353
356
  for w in wells:
354
- position_folders = glob(os.sep.join([w,f"{w.split(os.sep)[-2][1]}*", os.sep]))
357
+ position_folders = glob(os.sep.join([w,f"{w.split(os.sep)[-1][1]}*", os.sep]))
355
358
  number_pos.append(len(position_folders))
356
359
  print(f"Number of positions per well: {number_pos}")
357
360
 
@@ -393,6 +396,7 @@ if __name__ == "__main__":
393
396
  # myappid = 'mycompany.myproduct.subproduct.version' # arbitrary string
394
397
  # ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
395
398
  splash=True
399
+ print('Loading the libraries...')
396
400
 
397
401
  App = QApplication(sys.argv)
398
402
  #App.setWindowIcon(QIcon(os.sep.join([get_software_location(),'celldetective','icons','mexican-hat.png'])))
@@ -425,7 +429,10 @@ if __name__ == "__main__":
425
429
  import psutil
426
430
  import subprocess
427
431
  import json
432
+ # import matplotlib
433
+ # matplotlib.rcParams.update({'font.size': 8})
428
434
 
435
+ print('Libraries successfully loaded...')
429
436
 
430
437
  window = AppInitWindow(App)
431
438
 
celldetective/events.py CHANGED
@@ -1,5 +1,5 @@
1
1
  import numpy as np
2
-
2
+ from lifelines import KaplanMeierFitter
3
3
 
4
4
  def switch_to_events(classes, event_times, max_times, origin_times=None, left_censored=True, FrameToMin=None):
5
5
 
@@ -104,4 +104,30 @@ def switch_to_events(classes, event_times, max_times, origin_times=None, left_ce
104
104
  if FrameToMin is not None:
105
105
  #print('convert to minutes!', FrameToMin)
106
106
  survival_times = [s*FrameToMin for s in survival_times]
107
- return events, survival_times
107
+ return events, survival_times
108
+
109
+ def compute_survival(df, class_of_interest, t_event, t_reference=None, FrameToMin=1):
110
+
111
+ cols = list(df.columns)
112
+ assert class_of_interest in cols,"The requested class cannot be found in the dataframe..."
113
+ assert t_event in cols,"The event time cannot be found in the dataframe..."
114
+ left_censored = False
115
+
116
+ classes = df.groupby(['position','TRACK_ID'])[class_of_interest].min().values
117
+ event_times = df.groupby(['position','TRACK_ID'])[t_event].min().values
118
+ max_times = df.groupby(['position','TRACK_ID'])['FRAME'].max().values
119
+
120
+ if t_reference is not None:
121
+ left_censored = True
122
+ assert t_reference in cols,"The reference time cannot be found in the dataframe..."
123
+ first_detections = df.groupby(['position','TRACK_ID'])[t_reference].max().values
124
+
125
+ events, survival_times = switch_to_events(classes, event_times, max_times, origin_times=first_detections, left_censored=left_censored, FrameToMin=FrameToMin)
126
+ ks = KaplanMeierFitter()
127
+ if len(events)>0:
128
+ ks.fit(survival_times, event_observed=events)
129
+ else:
130
+ ks = None
131
+
132
+ return ks
133
+
@@ -2,7 +2,6 @@ from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel
2
2
  from PyQt5.QtGui import QPixmap
3
3
  from PyQt5.QtCore import Qt
4
4
  from celldetective.utils import get_software_location
5
- import celldetective
6
5
  import os
7
6
  from celldetective.gui.gui_utils import center_window
8
7
 
@@ -1,25 +1,10 @@
1
- from PyQt5.QtWidgets import QFrame, QGridLayout, QComboBox, QLabel, QPushButton, QVBoxLayout, QHBoxLayout, QCheckBox, \
2
- QMessageBox, QSpacerItem, QSizePolicy
1
+ from PyQt5.QtWidgets import QFrame, QLabel, QPushButton, QVBoxLayout, \
2
+ QSpacerItem, QSizePolicy
3
3
  from PyQt5.QtCore import Qt, QSize
4
4
  from PyQt5.QtGui import QIcon
5
-
6
- from superqt.fonticon import icon
7
- from fonticon_mdi6 import MDI6
8
- import gc
9
-
10
5
  from celldetective.gui.plot_measurements import ConfigMeasurementsPlot
11
- from celldetective.io import get_segmentation_models_list, control_segmentation_napari, get_signal_models_list, control_tracking_btrack
12
6
  from celldetective.gui import ConfigSurvival, ConfigSignalPlot
13
- from celldetective.gui.gui_utils import QHSeperationLine
14
- from celldetective.segmentation import segment_at_position, segment_from_threshold_at_position
15
- from celldetective.tracking import track_at_position
16
- from celldetective.measure import measure_at_position
17
- from celldetective.signals import analyze_signals_at_position
18
- import numpy as np
19
- from glob import glob
20
- from natsort import natsorted
21
7
  import os
22
- import pandas as pd
23
8
  from celldetective.gui import Styles
24
9
 
25
10
  class AnalysisPanel(QFrame, Styles):
@@ -94,7 +79,7 @@ class AnalysisPanel(QFrame, Styles):
94
79
  self.configSurvival.show()
95
80
 
96
81
  def configure_plot_signals(self):
97
- print('plot signal analysis starting!!!')
82
+ print('Configure a signal collapse representation...')
98
83
  self.ConfigSignalPlot = ConfigSignalPlot(self)
99
84
  self.ConfigSignalPlot.show()
100
85
 
@@ -1,7 +1,7 @@
1
1
  from PyQt5.QtWidgets import QMainWindow, QApplication, QMessageBox, QScrollArea, QComboBox, QFrame, QCheckBox, QFileDialog, QGridLayout, QTextEdit, QLineEdit, QVBoxLayout, QWidget, QLabel, QHBoxLayout, QPushButton
2
2
  from PyQt5.QtCore import Qt, QSize
3
- from celldetective.gui.gui_utils import center_window, FeatureChoice, ListWidget, QHSeperationLine, FigureCanvas
4
- from superqt import QLabeledDoubleRangeSlider, QLabeledDoubleSlider,QLabeledSlider
3
+ from celldetective.gui.gui_utils import center_window, FeatureChoice, ListWidget, QHSeperationLine, FigureCanvas, help_generic
4
+ from superqt import QLabeledDoubleSlider,QLabeledSlider
5
5
  from superqt.fonticon import icon
6
6
  from fonticon_mdi6 import MDI6
7
7
  from celldetective.utils import extract_experiment_channels, get_software_location
@@ -47,8 +47,9 @@ class ConfigTracking(QMainWindow, Styles):
47
47
 
48
48
  center_window(self)
49
49
  self.setMinimumWidth(540)
50
- self.setMinimumHeight(int(0.3*self.screen_height))
51
- self.setMaximumHeight(int(0.8*self.screen_height))
50
+ self.minimum_height = 300
51
+ # self.setMinimumHeight(int(0.3*self.screen_height))
52
+ # self.setMaximumHeight(int(0.8*self.screen_height))
52
53
  self.populate_widget()
53
54
  self.load_previous_tracking_instructions()
54
55
 
@@ -116,7 +117,6 @@ class ConfigTracking(QMainWindow, Styles):
116
117
  self.select_post_proc_btn = QPushButton()
117
118
  self.select_post_proc_btn.clicked.connect(self.activate_post_proc_options)
118
119
  self.select_post_proc_btn.setStyleSheet(self.button_select_all)
119
- grid.addWidget(self.select_post_proc_btn, 0,0,1,4,alignment=Qt.AlignLeft)
120
120
 
121
121
  self.post_proc_lbl = QLabel("POST-PROCESSING")
122
122
  self.post_proc_lbl.setStyleSheet("""
@@ -125,19 +125,100 @@ class ConfigTracking(QMainWindow, Styles):
125
125
  """)
126
126
  grid.addWidget(self.post_proc_lbl, 0, 0, 1, 4, alignment=Qt.AlignCenter)
127
127
 
128
+ title_hbox = QHBoxLayout()
129
+
128
130
  self.collapse_post_proc_btn = QPushButton()
129
131
  self.collapse_post_proc_btn.setIcon(icon(MDI6.chevron_down,color="black"))
130
132
  self.collapse_post_proc_btn.setIconSize(QSize(20, 20))
131
133
  self.collapse_post_proc_btn.setStyleSheet(self.button_select_all)
132
- grid.addWidget(self.collapse_post_proc_btn, 0, 0, 1, 4, alignment=Qt.AlignRight)
134
+ #grid.addWidget(self.collapse_post_proc_btn, 0, 0, 1, 4, alignment=Qt.AlignRight)
135
+
136
+ self.help_post_btn = QPushButton()
137
+ self.help_post_btn.setIcon(icon(MDI6.help_circle,color=self.help_color))
138
+ self.help_post_btn.setIconSize(QSize(20, 20))
139
+ self.help_post_btn.clicked.connect(self.help_post)
140
+ self.help_post_btn.setStyleSheet(self.button_select_all)
141
+ self.help_post_btn.setToolTip("Help.")
142
+
143
+ title_hbox.addWidget(self.select_post_proc_btn, 5)
144
+ title_hbox.addWidget(QLabel(), 85, alignment=Qt.AlignCenter)
145
+ title_hbox.addWidget(self.help_post_btn, 5)
146
+ title_hbox.addWidget(self.collapse_post_proc_btn, 5)
147
+ grid.addLayout(title_hbox, 0,0,1,4)
133
148
 
134
149
  self.generate_post_proc_panel_contents()
135
150
  grid.addWidget(self.ContentsPostProc, 1, 0, 1, 4, alignment=Qt.AlignTop)
136
151
  self.collapse_post_proc_btn.clicked.connect(lambda: self.ContentsPostProc.setHidden(not self.ContentsPostProc.isHidden()))
137
- # self.collapse_post_proc_btn.clicked.connect(self.collapse_features_advanced)
152
+ self.collapse_post_proc_btn.clicked.connect(self.collapse_post_advanced)
138
153
  self.ContentsPostProc.hide()
139
154
  self.uncheck_post_proc()
140
155
 
156
+ def collapse_post_advanced(self):
157
+
158
+ features_open = not self.ContentsFeatures.isHidden()
159
+ config_open = not self.ContentsConfig.isHidden()
160
+ post_open = not self.ContentsPostProc.isHidden()
161
+ is_open = np.array([features_open, config_open, post_open])
162
+
163
+ if self.ContentsPostProc.isHidden():
164
+ self.collapse_post_proc_btn.setIcon(icon(MDI6.chevron_down,color="black"))
165
+ self.collapse_post_proc_btn.setIconSize(QSize(20, 20))
166
+ if len(is_open[is_open])==0:
167
+ self.scroll_area.setMinimumHeight(int(self.minimum_height))
168
+ self.adjustSize()
169
+ else:
170
+ self.collapse_post_proc_btn.setIcon(icon(MDI6.chevron_up,color="black"))
171
+ self.collapse_post_proc_btn.setIconSize(QSize(20, 20))
172
+ self.scroll_area.setMinimumHeight(min(int(930), int(0.9*self.screen_height)))
173
+
174
+
175
+ def help_post(self):
176
+
177
+ """
178
+ Helper for track post-processing strategy.
179
+ """
180
+
181
+ dict_path = os.sep.join([get_software_location(),'celldetective','gui','help','track-postprocessing.json'])
182
+
183
+ with open(dict_path) as f:
184
+ d = json.load(f)
185
+
186
+ suggestion = help_generic(d)
187
+ if isinstance(suggestion, str):
188
+ print(f"{suggestion=}")
189
+ msgBox = QMessageBox()
190
+ msgBox.setIcon(QMessageBox.Information)
191
+ msgBox.setTextFormat(Qt.RichText)
192
+ msgBox.setText(rf"{suggestion}")
193
+ msgBox.setWindowTitle("Info")
194
+ msgBox.setStandardButtons(QMessageBox.Ok)
195
+ returnValue = msgBox.exec()
196
+ if returnValue == QMessageBox.Ok:
197
+ return None
198
+
199
+ def help_feature(self):
200
+
201
+ """
202
+ Helper for track post-processing strategy.
203
+ """
204
+
205
+ dict_path = os.sep.join([get_software_location(),'celldetective','gui','help','feature-btrack.json'])
206
+
207
+ with open(dict_path) as f:
208
+ d = json.load(f)
209
+
210
+ suggestion = help_generic(d)
211
+ if isinstance(suggestion, str):
212
+ print(f"{suggestion=}")
213
+ msgBox = QMessageBox()
214
+ msgBox.setIcon(QMessageBox.Information)
215
+ msgBox.setTextFormat(Qt.RichText)
216
+ msgBox.setText(rf"{suggestion}")
217
+ msgBox.setWindowTitle("Info")
218
+ msgBox.setStandardButtons(QMessageBox.Ok)
219
+ returnValue = msgBox.exec()
220
+ if returnValue == QMessageBox.Ok:
221
+ return None
141
222
 
142
223
  def populate_features_frame(self):
143
224
 
@@ -146,12 +227,11 @@ class ConfigTracking(QMainWindow, Styles):
146
227
  """
147
228
 
148
229
  grid = QGridLayout(self.features_frame)
149
-
230
+ title_hbox = QHBoxLayout()
231
+
150
232
  self.select_features_btn = QPushButton()
151
233
  self.select_features_btn.clicked.connect(self.activate_feature_options)
152
234
  self.select_features_btn.setStyleSheet(self.button_select_all)
153
- grid.addWidget(self.select_features_btn, 0,0,1,4,alignment=Qt.AlignLeft)
154
-
155
235
 
156
236
  self.feature_lbl = QLabel("FEATURES")
157
237
  self.feature_lbl.setStyleSheet("""
@@ -164,7 +244,19 @@ class ConfigTracking(QMainWindow, Styles):
164
244
  self.collapse_features_btn.setIcon(icon(MDI6.chevron_down,color="black"))
165
245
  self.collapse_features_btn.setIconSize(QSize(20, 20))
166
246
  self.collapse_features_btn.setStyleSheet(self.button_select_all)
167
- grid.addWidget(self.collapse_features_btn, 0, 0, 1, 4, alignment=Qt.AlignRight)
247
+
248
+ self.help_feature_btn = QPushButton()
249
+ self.help_feature_btn.setIcon(icon(MDI6.help_circle,color=self.help_color))
250
+ self.help_feature_btn.setIconSize(QSize(20, 20))
251
+ self.help_feature_btn.clicked.connect(self.help_feature)
252
+ self.help_feature_btn.setStyleSheet(self.button_select_all)
253
+ self.help_feature_btn.setToolTip("Help.")
254
+
255
+ title_hbox.addWidget(self.select_features_btn, 5)
256
+ title_hbox.addWidget(QLabel(), 85, alignment=Qt.AlignCenter)
257
+ title_hbox.addWidget(self.help_feature_btn, 5)
258
+ title_hbox.addWidget(self.collapse_features_btn, 5)
259
+ grid.addLayout(title_hbox, 0,0,1,4)
168
260
 
169
261
  self.generate_feature_panel_contents()
170
262
  grid.addWidget(self.ContentsFeatures, 1, 0, 1, 4, alignment=Qt.AlignTop)
@@ -179,16 +271,22 @@ class ConfigTracking(QMainWindow, Styles):
179
271
  Switch the chevron icon and adjust the size for the FEATURES frame.
180
272
  """
181
273
 
274
+ features_open = not self.ContentsFeatures.isHidden()
275
+ config_open = not self.ContentsConfig.isHidden()
276
+ post_open = not self.ContentsPostProc.isHidden()
277
+ is_open = np.array([features_open, config_open, post_open])
278
+
182
279
  if self.ContentsFeatures.isHidden():
183
280
  self.collapse_features_btn.setIcon(icon(MDI6.chevron_down,color="black"))
184
281
  self.collapse_features_btn.setIconSize(QSize(20, 20))
185
- self.button_widget.adjustSize()
186
- self.adjustSize()
282
+ if len(is_open[is_open])==0:
283
+ self.scroll_area.setMinimumHeight(int(self.minimum_height))
284
+ self.adjustSize()
187
285
  else:
188
286
  self.collapse_features_btn.setIcon(icon(MDI6.chevron_up,color="black"))
189
287
  self.collapse_features_btn.setIconSize(QSize(20, 20))
190
- self.button_widget.adjustSize()
191
- self.adjustSize()
288
+ self.scroll_area.setMinimumHeight(min(int(930), int(0.9*self.screen_height)))
289
+
192
290
 
193
291
  def generate_post_proc_panel_contents(self):
194
292
 
@@ -273,7 +371,7 @@ class ConfigTracking(QMainWindow, Styles):
273
371
  self.add_feature_btn.setToolTip("Add feature")
274
372
  self.add_feature_btn.setIconSize(QSize(20, 20))
275
373
 
276
- self.features_list = ListWidget(self, FeatureChoice, initial_features=['area','intensity_mean',])
374
+ self.features_list = ListWidget(FeatureChoice, initial_features=['area','intensity_mean',])
277
375
 
278
376
  self.del_feature_btn.clicked.connect(self.features_list.removeSel)
279
377
  self.add_feature_btn.clicked.connect(self.features_list.addItem)
@@ -447,16 +545,22 @@ class ConfigTracking(QMainWindow, Styles):
447
545
  Switch the chevron icon and adjust the size for the CONFIG frame.
448
546
  """
449
547
 
548
+ features_open = not self.ContentsFeatures.isHidden()
549
+ config_open = not self.ContentsConfig.isHidden()
550
+ post_open = not self.ContentsPostProc.isHidden()
551
+ is_open = np.array([features_open, config_open, post_open])
552
+
450
553
  if self.ContentsConfig.isHidden():
451
554
  self.collapse_config_btn.setIcon(icon(MDI6.chevron_down,color="black"))
452
555
  self.collapse_config_btn.setIconSize(QSize(20, 20))
453
- self.button_widget.adjustSize()
454
- self.adjustSize()
556
+ if len(is_open[is_open])==0:
557
+ self.scroll_area.setMinimumHeight(int(self.minimum_height))
558
+ self.adjustSize()
455
559
  else:
456
560
  self.collapse_config_btn.setIcon(icon(MDI6.chevron_up,color="black"))
457
561
  self.collapse_config_btn.setIconSize(QSize(20, 20))
458
- self.button_widget.adjustSize()
459
- self.adjustSize()
562
+ self.scroll_area.setMinimumHeight(min(int(930), int(0.9*self.screen_height)))
563
+
460
564
 
461
565
  def generate_config_panel_contents(self):
462
566
 
@@ -884,7 +988,8 @@ class ConfigTracking(QMainWindow, Styles):
884
988
  self.fig, self.ax = plt.subplots(1,1,figsize=(4,3))
885
989
  self.hist_window = FigureCanvas(self.fig, title="Haralick: control digitized histogram")
886
990
  self.ax.clear()
887
- self.ax.hist(norm_img.flatten(), bins=self.haralick_options['n_intensity_bins'])
991
+ flat = norm_img.flatten()
992
+ self.ax.hist(flat[flat==flat], bins=self.haralick_options['n_intensity_bins'])
888
993
  self.ax.set_xlabel('gray level value')
889
994
  self.ax.set_ylabel('#')
890
995
  plt.tight_layout()
@@ -1,21 +1,19 @@
1
- from PyQt5.QtWidgets import QWidget, QLineEdit, QMessageBox, QHBoxLayout, QVBoxLayout, QPushButton, QLabel, QComboBox, \
1
+ from PyQt5.QtWidgets import QWidget, QLineEdit, QMessageBox, QHBoxLayout, QVBoxLayout, QPushButton, QLabel, \
2
2
  QCheckBox, QRadioButton, QButtonGroup
3
- from celldetective.gui.gui_utils import FigureCanvas, center_window, color_from_class
4
- import numpy as np
5
- import matplotlib.pyplot as plt
6
- from superqt import QLabeledSlider,QLabeledDoubleSlider
3
+ from PyQt5.QtCore import Qt, QSize
4
+ from superqt import QLabeledSlider,QLabeledDoubleSlider, QSearchableComboBox
7
5
  from superqt.fonticon import icon
8
6
  from fonticon_mdi6 import MDI6
9
- from PyQt5.QtCore import Qt, QSize
7
+
10
8
  import os
11
- from sklearn.metrics import r2_score
12
- from scipy.optimize import curve_fit
13
- from celldetective.gui import Styles
14
- from math import ceil
15
- from celldetective.utils import extract_cols_from_query
9
+ import numpy as np
10
+ import matplotlib.pyplot as plt
11
+ import json
16
12
 
17
- def step_function(t, t_shift, dt):
18
- return 1/(1+np.exp(-(t-t_shift)/dt))
13
+ from celldetective.gui.gui_utils import FigureCanvas, center_window, color_from_status, help_generic
14
+ from celldetective.gui import Styles
15
+ from celldetective.utils import get_software_location
16
+ from celldetective.measure import classify_cells_from_query, interpret_track_classification
19
17
 
20
18
  class ClassifierWidget(QWidget, Styles):
21
19
 
@@ -98,7 +96,7 @@ class ClassifierWidget(QWidget, Styles):
98
96
 
99
97
 
100
98
 
101
- self.features_cb = [QComboBox() for i in range(2)]
99
+ self.features_cb = [QSearchableComboBox() for i in range(2)]
102
100
  self.log_btns = [QPushButton() for i in range(2)]
103
101
 
104
102
  for i in range(2):
@@ -121,6 +119,7 @@ class ClassifierWidget(QWidget, Styles):
121
119
  hbox_classify.addWidget(QLabel('classify: '), 10)
122
120
  self.property_query_le = QLineEdit()
123
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").')
124
123
  hbox_classify.addWidget(self.property_query_le, 70)
125
124
  self.submit_query_btn = QPushButton('Submit...')
126
125
  self.submit_query_btn.clicked.connect(self.apply_property_query)
@@ -133,7 +132,19 @@ class ClassifierWidget(QWidget, Styles):
133
132
  self.time_corr.setEnabled(True)
134
133
  else:
135
134
  self.time_corr.setEnabled(False)
136
- layout.addWidget(self.time_corr,alignment=Qt.AlignCenter)
135
+
136
+ time_prop_hbox = QHBoxLayout()
137
+ time_prop_hbox.addWidget(self.time_corr,alignment=Qt.AlignCenter)
138
+
139
+ self.help_propagate_btn = QPushButton()
140
+ self.help_propagate_btn.setIcon(icon(MDI6.help_circle,color=self.help_color))
141
+ self.help_propagate_btn.setIconSize(QSize(20, 20))
142
+ self.help_propagate_btn.clicked.connect(self.help_propagate)
143
+ self.help_propagate_btn.setStyleSheet(self.button_select_all)
144
+ self.help_propagate_btn.setToolTip("Help.")
145
+ time_prop_hbox.addWidget(self.help_propagate_btn,5,alignment=Qt.AlignRight)
146
+
147
+ layout.addLayout(time_prop_hbox)
137
148
 
138
149
  self.irreversible_event_btn = QRadioButton('irreversible event')
139
150
  self.unique_state_btn = QRadioButton('unique state')
@@ -233,7 +244,7 @@ class ClassifierWidget(QWidget, Styles):
233
244
 
234
245
  if not self.project_times:
235
246
  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())
236
- colors = [color_from_class(c) for c in self.df.loc[self.df['FRAME']==self.currentFrame,self.class_name].to_numpy()]
247
+ colors = [color_from_status(c) for c in self.df.loc[self.df['FRAME']==self.currentFrame,self.class_name].to_numpy()]
237
248
  self.scat_props.set_facecolor(colors)
238
249
  self.scat_props.set_alpha(self.currentAlpha)
239
250
  self.ax_props.set_xlabel(self.features_cb[1].currentText())
@@ -264,38 +275,18 @@ class ClassifierWidget(QWidget, Styles):
264
275
  self.propscanvas.canvas.draw_idle()
265
276
 
266
277
  def apply_property_query(self):
267
-
278
+
268
279
  query = self.property_query_le.text()
269
- self.df[self.class_name] = 1
270
-
271
- cols = extract_cols_from_query(query)
272
- print(cols)
273
- cols_in_df = np.all([c in list(self.df.columns) for c in cols], axis=0)
274
- print(f'Testing if columns from query are in the dataframe: {cols_in_df}...')
275
-
276
- if query=='':
277
- print('empty query')
278
- else:
279
- try:
280
- if cols_in_df:
281
- self.selection = self.df.dropna(subset=cols).query(query).index
282
- null_selection = self.df[self.df.loc[:,cols].isna().any(axis=1)].index
283
- self.df.loc[null_selection, self.class_name] = np.nan
284
- self.df.loc[self.selection, self.class_name] = 0
285
- else:
286
- self.df.loc[:, self.class_name] = np.nan
287
-
288
- except Exception as e:
289
- print(e)
290
- print(self.df.columns)
291
- msgBox = QMessageBox()
292
- msgBox.setIcon(QMessageBox.Warning)
293
- msgBox.setText(f"The query could not be understood. No filtering was applied. {e}")
294
- msgBox.setWindowTitle("Warning")
295
- msgBox.setStandardButtons(QMessageBox.Ok)
296
- returnValue = msgBox.exec()
297
- if returnValue == QMessageBox.Ok:
298
- return None
280
+ self.df = classify_cells_from_query(self.df, self.class_name, query)
281
+ if self.df is None:
282
+ msgBox = QMessageBox()
283
+ msgBox.setIcon(QMessageBox.Warning)
284
+ msgBox.setText(f"The query could not be understood. No filtering was applied. {e}")
285
+ msgBox.setWindowTitle("Warning")
286
+ msgBox.setStandardButtons(QMessageBox.Ok)
287
+ returnValue = msgBox.exec()
288
+ if returnValue == QMessageBox.Ok:
289
+ return None
299
290
 
300
291
  self.update_props_scatter()
301
292
 
@@ -358,37 +349,8 @@ class ClassifierWidget(QWidget, Styles):
358
349
  self.df = self.df.drop(list(set(name_map.values()) & set(self.df.columns)), axis=1).rename(columns=name_map)
359
350
  self.df.reset_index(inplace=True, drop=True)
360
351
 
361
- #self.df.reset_index(inplace=True)
362
- if 'TRACK_ID' in self.df.columns:
363
- print('Tracks detected... save a status column...')
364
- stat_col = self.class_name_user.replace('class','status')
365
- self.df.loc[:,stat_col] = 1 - self.df[self.class_name_user].values
366
- for tid,track in self.df.groupby(['position','TRACK_ID']):
367
- indices = track[self.class_name_user].index
368
- status_values = track[stat_col].to_numpy()
369
- if self.irreversible_event_btn.isChecked():
370
- if np.all([s==0 for s in status_values]):
371
- self.df.loc[indices, self.class_name_user] = 1
372
- elif np.all([s==1 for s in status_values]):
373
- self.df.loc[indices, self.class_name_user] = 2
374
- self.df.loc[indices, self.class_name_user.replace('class','status')] = 2
375
- else:
376
- self.df.loc[indices, self.class_name_user] = 2
377
- elif self.unique_state_btn.isChecked():
378
- frames = track['FRAME'].to_numpy()
379
- t_first = track['t_firstdetection'].to_numpy()[0]
380
- median_status = np.nanmedian(status_values[frames>=t_first])
381
- if median_status==median_status:
382
- c = ceil(median_status)
383
- if c==0:
384
- self.df.loc[indices, self.class_name_user] = 1
385
- self.df.loc[indices, self.class_name_user.replace('class','t')] = -1
386
- elif c==1:
387
- self.df.loc[indices, self.class_name_user] = 2
388
- self.df.loc[indices, self.class_name_user.replace('class','t')] = -1
389
- if self.irreversible_event_btn.isChecked():
390
- self.df.loc[self.df[self.class_name_user]!=2, self.class_name_user.replace('class', 't')] = -1
391
- self.estimate_time()
352
+ 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(), r2_threshold=self.r2_slider.value())
353
+
392
354
  else:
393
355
  self.group_name_user = 'group_' + self.name_le.text()
394
356
  print(f'User defined characteristic group name: {self.group_name_user}.')
@@ -409,19 +371,41 @@ class ClassifierWidget(QWidget, Styles):
409
371
  name_map = {self.class_name: self.group_name_user}
410
372
  self.df = self.df.drop(list(set(name_map.values()) & set(self.df.columns)), axis=1).rename(columns=name_map)
411
373
  print(self.df.columns)
412
- self.df[self.group_name_user] = self.df[self.group_name_user].replace({0: 1, 1: 0})
374
+ #self.df[self.group_name_user] = self.df[self.group_name_user].replace({0: 1, 1: 0})
413
375
  self.df.reset_index(inplace=True, drop=True)
414
376
 
415
377
 
416
378
  for pos,pos_group in self.df.groupby('position'):
417
379
  pos_group.to_csv(pos+os.sep.join(['output', 'tables', f'trajectories_{self.mode}.csv']), index=False)
418
380
 
419
- # reset
420
- #self.init_class()
421
- #self.update_props_scatter()
422
381
  self.parent_window.parent_window.update_position_options()
423
382
  self.close()
424
383
 
384
+
385
+ def help_propagate(self):
386
+
387
+ """
388
+ Helper for segmentation strategy between threshold-based and Deep learning.
389
+ """
390
+
391
+ dict_path = os.sep.join([get_software_location(),'celldetective','gui','help','propagate-classification.json'])
392
+
393
+ with open(dict_path) as f:
394
+ d = json.load(f)
395
+
396
+ suggestion = help_generic(d)
397
+ if isinstance(suggestion, str):
398
+ print(f"{suggestion=}")
399
+ msgBox = QMessageBox()
400
+ msgBox.setIcon(QMessageBox.Information)
401
+ msgBox.setTextFormat(Qt.RichText)
402
+ msgBox.setText(rf"{suggestion}")
403
+ msgBox.setWindowTitle("Info")
404
+ msgBox.setStandardButtons(QMessageBox.Ok)
405
+ returnValue = msgBox.exec()
406
+ if returnValue == QMessageBox.Ok:
407
+ return None
408
+
425
409
  def switch_to_log(self, i):
426
410
 
427
411
  """
@@ -441,6 +425,7 @@ class ClassifierWidget(QWidget, Styles):
441
425
  elif i==0:
442
426
  try:
443
427
  if self.ax_props.get_yscale()=='linear':
428
+ ymin,ymax = self.ax_props.get_ylim()
444
429
  self.ax_props.set_yscale('log')
445
430
  self.log_btns[i].setIcon(icon(MDI6.math_log,color="#1565c0"))
446
431
  else:
@@ -452,33 +437,6 @@ class ClassifierWidget(QWidget, Styles):
452
437
  self.ax_props.autoscale()
453
438
  self.propscanvas.canvas.draw_idle()
454
439
 
455
- def estimate_time(self):
456
-
457
- self.df = self.df.sort_values(by=['position','TRACK_ID'],ignore_index=True)
458
- for tid,group in self.df.loc[self.df[self.class_name_user]==2].groupby(['position','TRACK_ID']):
459
- indices = group.index
460
- status_col = self.class_name_user.replace('class','status')
461
- status_signal = group[status_col].values
462
- timeline = group['FRAME'].values
463
-
464
- try:
465
- popt, pcov = curve_fit(step_function, timeline.astype(int), status_signal, p0=[self.df['FRAME'].max()//2, 0.8],maxfev=30000)
466
- values = [step_function(t, *popt) for t in timeline]
467
- r2 = r2_score(status_signal,values)
468
- except Exception as e:
469
- print(e)
470
- self.df.loc[indices, self.class_name_user] = 2.0
471
- self.df.loc[indices, self.class_name_user.replace('class','t')] = -1
472
- continue
473
-
474
- if r2 > float(self.r2_slider.value()):
475
- t0 = popt[0]
476
- self.df.loc[indices, self.class_name_user.replace('class','t')] = t0
477
- self.df.loc[indices, self.class_name_user] = 0.0
478
- else:
479
- self.df.loc[indices, self.class_name_user.replace('class','t')] = -1
480
- self.df.loc[indices, self.class_name_user] = 2.0
481
-
482
440
  print('Done.')
483
441
 
484
442
 
@@ -487,5 +445,3 @@ class ClassifierWidget(QWidget, Styles):
487
445
 
488
446
 
489
447
 
490
-
491
-