celldetective 1.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. celldetective/__init__.py +2 -0
  2. celldetective/__main__.py +432 -0
  3. celldetective/datasets/segmentation_annotations/blank +0 -0
  4. celldetective/datasets/signal_annotations/blank +0 -0
  5. celldetective/events.py +149 -0
  6. celldetective/extra_properties.py +100 -0
  7. celldetective/filters.py +89 -0
  8. celldetective/gui/__init__.py +20 -0
  9. celldetective/gui/about.py +44 -0
  10. celldetective/gui/analyze_block.py +563 -0
  11. celldetective/gui/btrack_options.py +898 -0
  12. celldetective/gui/classifier_widget.py +386 -0
  13. celldetective/gui/configure_new_exp.py +532 -0
  14. celldetective/gui/control_panel.py +438 -0
  15. celldetective/gui/gui_utils.py +495 -0
  16. celldetective/gui/json_readers.py +113 -0
  17. celldetective/gui/measurement_options.py +1425 -0
  18. celldetective/gui/neighborhood_options.py +452 -0
  19. celldetective/gui/plot_signals_ui.py +1042 -0
  20. celldetective/gui/process_block.py +1055 -0
  21. celldetective/gui/retrain_segmentation_model_options.py +706 -0
  22. celldetective/gui/retrain_signal_model_options.py +643 -0
  23. celldetective/gui/seg_model_loader.py +460 -0
  24. celldetective/gui/signal_annotator.py +2388 -0
  25. celldetective/gui/signal_annotator_options.py +340 -0
  26. celldetective/gui/styles.py +217 -0
  27. celldetective/gui/survival_ui.py +903 -0
  28. celldetective/gui/tableUI.py +608 -0
  29. celldetective/gui/thresholds_gui.py +1300 -0
  30. celldetective/icons/logo-large.png +0 -0
  31. celldetective/icons/logo.png +0 -0
  32. celldetective/icons/signals_icon.png +0 -0
  33. celldetective/icons/splash-test.png +0 -0
  34. celldetective/icons/splash.png +0 -0
  35. celldetective/icons/splash0.png +0 -0
  36. celldetective/icons/survival2.png +0 -0
  37. celldetective/icons/vignette_signals2.png +0 -0
  38. celldetective/icons/vignette_signals2.svg +114 -0
  39. celldetective/io.py +2050 -0
  40. celldetective/links/zenodo.json +561 -0
  41. celldetective/measure.py +1258 -0
  42. celldetective/models/segmentation_effectors/blank +0 -0
  43. celldetective/models/segmentation_generic/blank +0 -0
  44. celldetective/models/segmentation_targets/blank +0 -0
  45. celldetective/models/signal_detection/blank +0 -0
  46. celldetective/models/tracking_configs/mcf7.json +68 -0
  47. celldetective/models/tracking_configs/ricm.json +203 -0
  48. celldetective/models/tracking_configs/ricm2.json +203 -0
  49. celldetective/neighborhood.py +717 -0
  50. celldetective/scripts/analyze_signals.py +51 -0
  51. celldetective/scripts/measure_cells.py +275 -0
  52. celldetective/scripts/segment_cells.py +212 -0
  53. celldetective/scripts/segment_cells_thresholds.py +140 -0
  54. celldetective/scripts/track_cells.py +206 -0
  55. celldetective/scripts/train_segmentation_model.py +246 -0
  56. celldetective/scripts/train_signal_model.py +49 -0
  57. celldetective/segmentation.py +712 -0
  58. celldetective/signals.py +2826 -0
  59. celldetective/tracking.py +974 -0
  60. celldetective/utils.py +1681 -0
  61. celldetective-1.0.2.dist-info/LICENSE +674 -0
  62. celldetective-1.0.2.dist-info/METADATA +192 -0
  63. celldetective-1.0.2.dist-info/RECORD +66 -0
  64. celldetective-1.0.2.dist-info/WHEEL +5 -0
  65. celldetective-1.0.2.dist-info/entry_points.txt +2 -0
  66. celldetective-1.0.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,386 @@
1
+ from PyQt5.QtWidgets import QWidget, QLineEdit, QMessageBox, QHBoxLayout, QVBoxLayout, QPushButton, QLabel, QComboBox, \
2
+ QCheckBox
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
7
+ from superqt.fonticon import icon
8
+ from fonticon_mdi6 import MDI6
9
+ from PyQt5.QtCore import Qt, QSize
10
+ import os
11
+ from sklearn.metrics import r2_score
12
+ from scipy.optimize import curve_fit
13
+
14
+ def step_function(t, t_shift, dt):
15
+ return 1/(1+np.exp(-(t-t_shift)/dt))
16
+
17
+ class ClassifierWidget(QWidget):
18
+
19
+ def __init__(self, parent):
20
+
21
+ super().__init__()
22
+
23
+ self.parent = parent
24
+ self.screen_height = self.parent.parent.parent.screen_height
25
+ self.screen_width = self.parent.parent.parent.screen_width
26
+ self.currentAlpha = 1.0
27
+
28
+ self.setWindowTitle("Custom classification")
29
+
30
+ self.mode = self.parent.mode
31
+ self.df = self.parent.df
32
+
33
+ is_number = np.vectorize(lambda x: np.issubdtype(x, np.number))
34
+ is_number_test = is_number(self.df.dtypes)
35
+ self.cols = [col for t,col in zip(is_number_test,self.df.columns) if t]
36
+
37
+ self.class_name = 'custom'
38
+ self.name_le = QLineEdit(self.class_name)
39
+ self.init_class()
40
+
41
+ # Create the QComboBox and add some items
42
+ center_window(self)
43
+
44
+
45
+ layout = QVBoxLayout(self)
46
+ layout.setContentsMargins(30,30,30,30)
47
+
48
+ name_layout = QHBoxLayout()
49
+ name_layout.addWidget(QLabel('class name: '), 33)
50
+ name_layout.addWidget(self.name_le, 66)
51
+ layout.addLayout(name_layout)
52
+
53
+ fig_btn_hbox = QHBoxLayout()
54
+ fig_btn_hbox.addWidget(QLabel(''), 95)
55
+ self.project_times_btn = QPushButton('')
56
+ self.project_times_btn.setStyleSheet(self.parent.parent.parent.button_select_all)
57
+ self.project_times_btn.setIcon(icon(MDI6.math_integral,color="black"))
58
+ self.project_times_btn.setToolTip("Project measurements at all times.")
59
+ self.project_times_btn.setIconSize(QSize(20, 20))
60
+ self.project_times = False
61
+ self.project_times_btn.clicked.connect(self.switch_projection)
62
+ fig_btn_hbox.addWidget(self.project_times_btn, 5)
63
+ layout.addLayout(fig_btn_hbox)
64
+
65
+ # Figure
66
+ self.initalize_props_scatter()
67
+ layout.addWidget(self.propscanvas)
68
+
69
+ # slider
70
+ self.frame_slider = QLabeledSlider()
71
+ self.frame_slider.setSingleStep(1)
72
+ self.frame_slider.setOrientation(1)
73
+ self.frame_slider.setRange(0,int(self.df.FRAME.max()) - 1)
74
+ self.frame_slider.setValue(0)
75
+ self.currentFrame = 0
76
+
77
+ slider_hbox = QHBoxLayout()
78
+ slider_hbox.addWidget(QLabel('frame: '), 10)
79
+ slider_hbox.addWidget(self.frame_slider, 90)
80
+ layout.addLayout(slider_hbox)
81
+
82
+
83
+ # transparency slider
84
+ self.alpha_slider = QLabeledDoubleSlider()
85
+ self.alpha_slider.setSingleStep(0.001)
86
+ self.alpha_slider.setOrientation(1)
87
+ self.alpha_slider.setRange(0,1)
88
+ self.alpha_slider.setValue(1.0)
89
+ self.alpha_slider.setDecimals(3)
90
+
91
+ slider_alpha_hbox = QHBoxLayout()
92
+ slider_alpha_hbox.addWidget(QLabel('transparency: '), 10)
93
+ slider_alpha_hbox.addWidget(self.alpha_slider, 90)
94
+ layout.addLayout(slider_alpha_hbox)
95
+
96
+
97
+
98
+ self.features_cb = [QComboBox() for i in range(2)]
99
+ self.log_btns = [QPushButton() for i in range(2)]
100
+
101
+ for i in range(2):
102
+ hbox_feat = QHBoxLayout()
103
+ hbox_feat.addWidget(QLabel(f'feature {i}: '), 20)
104
+ hbox_feat.addWidget(self.features_cb[i], 75)
105
+ hbox_feat.addWidget(self.log_btns[i], 5)
106
+ layout.addLayout(hbox_feat)
107
+
108
+ self.features_cb[i].clear()
109
+ self.features_cb[i].addItems(sorted(list(self.cols),key=str.lower))
110
+ self.features_cb[i].currentTextChanged.connect(self.update_props_scatter)
111
+ self.features_cb[i].setCurrentIndex(i)
112
+
113
+ self.log_btns[i].setIcon(icon(MDI6.math_log,color="black"))
114
+ self.log_btns[i].setStyleSheet(self.parent.parent.parent.button_select_all)
115
+ self.log_btns[i].clicked.connect(lambda ch, i=i: self.switch_to_log(i))
116
+
117
+ hbox_classify = QHBoxLayout()
118
+ hbox_classify.addWidget(QLabel('classify: '), 10)
119
+ self.property_query_le = QLineEdit()
120
+ self.property_query_le.setPlaceholderText('classify points using a query such as: area > 100 or eccentricity > 0.95')
121
+ hbox_classify.addWidget(self.property_query_le, 70)
122
+ self.submit_query_btn = QPushButton('Submit...')
123
+ self.submit_query_btn.clicked.connect(self.apply_property_query)
124
+ hbox_classify.addWidget(self.submit_query_btn, 20)
125
+ layout.addLayout(hbox_classify)
126
+
127
+ self.time_corr = QCheckBox('Time correlated event')
128
+ if "TRACK_ID" in self.df.columns:
129
+ self.time_corr.setEnabled(True)
130
+ else:
131
+ self.time_corr.setEnabled(False)
132
+ layout.addWidget(self.time_corr,alignment=Qt.AlignCenter)
133
+ self.submit_btn = QPushButton('apply')
134
+ self.submit_btn.clicked.connect(self.submit_classification)
135
+ layout.addWidget(self.submit_btn, 30)
136
+
137
+ self.frame_slider.valueChanged.connect(self.set_frame)
138
+ self.alpha_slider.valueChanged.connect(self.set_transparency)
139
+
140
+ def init_class(self):
141
+
142
+ self.class_name = 'custom'
143
+ i=1
144
+ while self.class_name in self.df.columns:
145
+ self.class_name = f'custom_{i}'
146
+ i+=1
147
+ self.name_le.setText(self.class_name)
148
+ self.df.loc[:,self.class_name] = 1
149
+
150
+ def initalize_props_scatter(self):
151
+
152
+ """
153
+ Define properties scatter.
154
+ """
155
+
156
+ self.fig_props, self.ax_props = plt.subplots(figsize=(4,4),tight_layout=True)
157
+ self.propscanvas = FigureCanvas(self.fig_props, interactive=True)
158
+ self.fig_props.set_facecolor('none')
159
+ self.fig_props.canvas.setStyleSheet("background-color: transparent;")
160
+ self.scat_props = self.ax_props.scatter([],[], color="k", alpha=self.currentAlpha)
161
+ self.propscanvas.canvas.draw_idle()
162
+ self.propscanvas.canvas.setMinimumHeight(self.screen_height//5)
163
+
164
+ def update_props_scatter(self, feature_changed=True):
165
+
166
+ if not self.project_times:
167
+ 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())
168
+ colors = [color_from_class(c) for c in self.df.loc[self.df['FRAME']==self.currentFrame,self.class_name].to_numpy()]
169
+ self.scat_props.set_facecolor(colors)
170
+ self.scat_props.set_alpha(self.currentAlpha)
171
+ self.ax_props.set_xlabel(self.features_cb[1].currentText())
172
+ self.ax_props.set_ylabel(self.features_cb[0].currentText())
173
+ else:
174
+ self.scat_props.set_offsets(self.df[[self.features_cb[1].currentText(),self.features_cb[0].currentText()]].to_numpy())
175
+ colors = [color_from_class(c) for c in self.df[self.class_name].to_numpy()]
176
+ self.scat_props.set_facecolor(colors)
177
+ self.scat_props.set_alpha(self.currentAlpha)
178
+ self.ax_props.set_xlabel(self.features_cb[1].currentText())
179
+ self.ax_props.set_ylabel(self.features_cb[0].currentText())
180
+ self.ax_props.set_xlim(1*self.df[self.features_cb[1].currentText()].min(),1.0*self.df[self.features_cb[1].currentText()].max())
181
+ self.ax_props.set_ylim(1*self.df[self.features_cb[0].currentText()].min(),1.0*self.df[self.features_cb[0].currentText()].max())
182
+ if feature_changed:
183
+ self.propscanvas.canvas.toolbar.update()
184
+ self.propscanvas.canvas.draw_idle()
185
+
186
+ def apply_property_query(self):
187
+ query = self.property_query_le.text()
188
+ self.df[self.class_name] = 1
189
+
190
+ print(query, self.class_name)
191
+
192
+ if query=='':
193
+ print('empty query')
194
+ else:
195
+ try:
196
+ self.selection = self.df.query(query).index
197
+ print(self.selection)
198
+ self.df.loc[self.selection, self.class_name] = 0
199
+ except Exception as e:
200
+ print(e)
201
+ print(self.df.columns)
202
+ msgBox = QMessageBox()
203
+ msgBox.setIcon(QMessageBox.Warning)
204
+ msgBox.setText(f"The query could not be understood. No filtering was applied. {e}")
205
+ msgBox.setWindowTitle("Warning")
206
+ msgBox.setStandardButtons(QMessageBox.Ok)
207
+ returnValue = msgBox.exec()
208
+ if returnValue == QMessageBox.Ok:
209
+ return None
210
+
211
+ self.update_props_scatter()
212
+
213
+ def set_frame(self, value):
214
+ xlim=self.ax_props.get_xlim()
215
+ ylim=self.ax_props.get_ylim()
216
+ self.currentFrame = value
217
+ self.update_props_scatter(feature_changed=False)
218
+ self.ax_props.set_xlim(xlim)
219
+ self.ax_props.set_ylim(ylim)
220
+
221
+
222
+ def set_transparency(self, value):
223
+ xlim=self.ax_props.get_xlim()
224
+ ylim=self.ax_props.get_ylim()
225
+ self.currentAlpha = value
226
+ #fc = self.scat_props.get_facecolors()
227
+ #fc[:, 3] = value
228
+ #self.scat_props.set_facecolors(fc)
229
+ #self.propscanvas.canvas.draw_idle()
230
+ self.update_props_scatter(feature_changed=False)
231
+ self.ax_props.set_xlim(xlim)
232
+ self.ax_props.set_ylim(ylim)
233
+
234
+ def switch_projection(self):
235
+ if self.project_times:
236
+ self.project_times = False
237
+ self.project_times_btn.setIcon(icon(MDI6.math_integral,color="black"))
238
+ self.project_times_btn.setIconSize(QSize(20, 20))
239
+ self.frame_slider.setEnabled(True)
240
+ else:
241
+ self.project_times = True
242
+ self.project_times_btn.setIcon(icon(MDI6.math_integral_box,color="black"))
243
+ self.project_times_btn.setIconSize(QSize(20, 20))
244
+ self.frame_slider.setEnabled(False)
245
+ self.update_props_scatter()
246
+
247
+ def submit_classification(self):
248
+
249
+ print('submit')
250
+ self.apply_property_query()
251
+
252
+ if self.time_corr.isChecked():
253
+ self.class_name_user = 'class_'+self.name_le.text()
254
+ print(f'User defined class name: {self.class_name_user}.')
255
+ if self.class_name_user in self.df.columns:
256
+
257
+ msgBox = QMessageBox()
258
+ msgBox.setIcon(QMessageBox.Information)
259
+ msgBox.setText(f"The class column {self.class_name_user} already exists in the table.\nProceeding will reclassify. Do you want to continue?")
260
+ msgBox.setWindowTitle("Warning")
261
+ msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
262
+ returnValue = msgBox.exec()
263
+ if returnValue == QMessageBox.Yes:
264
+ pass
265
+ else:
266
+ return None
267
+
268
+ name_map = {self.class_name: self.class_name_user}
269
+ self.df = self.df.drop(list(set(name_map.values()) & set(self.df.columns)), axis=1).rename(columns=name_map)
270
+ self.df.reset_index(inplace=True, drop=True)
271
+
272
+ #self.df.reset_index(inplace=True)
273
+ if 'TRACK_ID' in self.df.columns:
274
+ print('Tracks detected... save a status column...')
275
+ stat_col = self.class_name_user.replace('class','status')
276
+ self.df.loc[:,stat_col] = 1 - self.df[self.class_name_user].values
277
+ for tid,track in self.df.groupby(['position','TRACK_ID']):
278
+ indices = track[self.class_name_user].index
279
+ status_values = track[stat_col].to_numpy()
280
+ if np.all([s==0 for s in status_values]):
281
+ self.df.loc[indices, self.class_name_user] = 1
282
+ elif np.all([s==1 for s in status_values]):
283
+ self.df.loc[indices, self.class_name_user] = 2
284
+ self.df.loc[indices, self.class_name_user.replace('class','status')] = 2
285
+ else:
286
+ self.df.loc[indices, self.class_name_user] = 2
287
+ self.estimate_time()
288
+ else:
289
+ self.group_name_user = 'group_' + self.name_le.text()
290
+ print(f'User defined characteristic group name: {self.group_name_user}.')
291
+ if self.group_name_user in self.df.columns:
292
+
293
+ msgBox = QMessageBox()
294
+ msgBox.setIcon(QMessageBox.Information)
295
+ msgBox.setText(
296
+ f"The group column {self.group_name_user} already exists in the table.\nProceeding will reclassify. Do you want to continue?")
297
+ msgBox.setWindowTitle("Warning")
298
+ msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
299
+ returnValue = msgBox.exec()
300
+ if returnValue == QMessageBox.Yes:
301
+ pass
302
+ else:
303
+ return None
304
+
305
+ name_map = {self.class_name: self.group_name_user}
306
+ self.df = self.df.drop(list(set(name_map.values()) & set(self.df.columns)), axis=1).rename(columns=name_map)
307
+ print(self.df.columns)
308
+ self.df[self.group_name_user] = self.df[self.group_name_user].replace({0: 1, 1: 0})
309
+ self.df.reset_index(inplace=True, drop=True)
310
+
311
+
312
+ for pos,pos_group in self.df.groupby('position'):
313
+ pos_group.to_csv(pos+os.sep.join(['output', 'tables', f'trajectories_{self.mode}.csv']), index=False)
314
+
315
+ # reset
316
+ #self.init_class()
317
+ #self.update_props_scatter()
318
+ self.close()
319
+
320
+ def switch_to_log(self, i):
321
+
322
+ """
323
+ Switch threshold histogram to log scale. Auto adjust.
324
+ """
325
+
326
+ if i==1:
327
+ try:
328
+ if self.ax_props.get_xscale()=='linear':
329
+ self.ax_props.set_xscale('log')
330
+ self.log_btns[i].setIcon(icon(MDI6.math_log,color="#1565c0"))
331
+ else:
332
+ self.ax_props.set_xscale('linear')
333
+ self.log_btns[i].setIcon(icon(MDI6.math_log,color="black"))
334
+ except Exception as e:
335
+ print(e)
336
+ elif i==0:
337
+ try:
338
+ if self.ax_props.get_yscale()=='linear':
339
+ self.ax_props.set_yscale('log')
340
+ self.log_btns[i].setIcon(icon(MDI6.math_log,color="#1565c0"))
341
+ else:
342
+ self.ax_props.set_yscale('linear')
343
+ self.log_btns[i].setIcon(icon(MDI6.math_log,color="black"))
344
+ except Exception as e:
345
+ print(e)
346
+
347
+ self.ax_props.autoscale()
348
+ self.propscanvas.canvas.draw_idle()
349
+
350
+ def estimate_time(self):
351
+
352
+ self.df = self.df.sort_values(by=['position','TRACK_ID'],ignore_index=True)
353
+ for tid,group in self.df.loc[self.df[self.class_name_user]==2].groupby(['position','TRACK_ID']):
354
+ indices = group.index
355
+ status_col = self.class_name_user.replace('class','status')
356
+ status_signal = group[status_col].values
357
+ timeline = group['FRAME'].values
358
+
359
+ try:
360
+ popt, pcov = curve_fit(step_function, timeline, status_signal,p0=[self.df['FRAME'].max()//2, 0.5],maxfev=10000)
361
+ r2 = r2_score(status_signal, step_function(timeline, *popt))
362
+ except Exception as e:
363
+ print(e)
364
+ self.df.loc[indices, self.class_name_user] = 2.0
365
+ self.df.loc[indices, self.class_name_user.replace('class','t')] = -1
366
+ continue
367
+
368
+ if r2 > 0.7:
369
+ t0 = popt[0]
370
+ self.df.loc[indices, self.class_name_user.replace('class','t')] = t0
371
+ self.df.loc[indices, self.class_name_user] = 0.0
372
+ else:
373
+ self.df.loc[indices, self.class_name_user.replace('class','t')] = -1
374
+ self.df.loc[indices, self.class_name_user] = 2.0
375
+
376
+ print('Done.')
377
+
378
+
379
+
380
+
381
+
382
+
383
+
384
+
385
+
386
+