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,2 @@
1
+ from .tracking import track, clean_trajectories
2
+
@@ -0,0 +1,432 @@
1
+ #!/usr/bin/env python3
2
+ import sys
3
+ from PyQt5.QtWidgets import QApplication, QSplashScreen, QMainWindow
4
+ from PyQt5.QtGui import QPixmap
5
+ from os import sep
6
+ from celldetective.utils import get_software_location
7
+ #from PyQt5.QtCore import QEventLoop
8
+ from time import time, sleep
9
+ import os
10
+ #os.environ['QT_DEBUG_PLUGINS'] = '1'
11
+
12
+ class AppInitWindow(QMainWindow):
13
+
14
+ """
15
+ Initial window to set the experiment folder or create a new one.
16
+ """
17
+
18
+ def __init__(self, parent=None):
19
+ super().__init__()
20
+
21
+ self.parent = parent
22
+ self.Styles = Styles()
23
+ self.init_styles()
24
+ self.setWindowTitle("celldetective")
25
+
26
+ self.n_threads = min([1,psutil.cpu_count()])
27
+
28
+ try:
29
+ subprocess.check_output('nvidia-smi')
30
+ print('Nvidia GPU detected')
31
+ self.use_gpu = True
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!')
34
+ self.use_gpu = False
35
+
36
+ self.soft_path = get_software_location()
37
+ self.onlyInt = QIntValidator()
38
+ self.setWindowIcon(QIcon(os.sep.join([self.soft_path,'celldetective','icons','logo.png'])))
39
+ center_window(self)
40
+ self._createActions()
41
+ self._createMenuBar()
42
+
43
+ app = QApplication.instance()
44
+ self.screen = app.primaryScreen()
45
+ self.geometry = self.screen.availableGeometry()
46
+ self.screen_width, self.screen_height = self.geometry.getRect()[-2:]
47
+
48
+ central_widget = QWidget()
49
+ self.vertical_layout = QVBoxLayout(central_widget)
50
+ self.vertical_layout.setContentsMargins(15,15,15,15)
51
+ self.vertical_layout.addWidget(QLabel("Experiment folder:"))
52
+ self.create_locate_exp_hbox()
53
+ self.create_buttons_hbox()
54
+ self.setCentralWidget(central_widget)
55
+ self.reload_previous_gpu_threads()
56
+ self.show()
57
+
58
+ def create_locate_exp_hbox(self):
59
+
60
+ self.locate_exp_layout = QHBoxLayout()
61
+ self.locate_exp_layout.setContentsMargins(0,5,0,0)
62
+ self.experiment_path_selection = QLineEdit()
63
+ self.experiment_path_selection.setAlignment(Qt.AlignLeft)
64
+ self.experiment_path_selection.setEnabled(True)
65
+ self.experiment_path_selection.setDragEnabled(True)
66
+ self.experiment_path_selection.setFixedWidth(430)
67
+ self.experiment_path_selection.textChanged[str].connect(self.check_path_and_enable_opening)
68
+ self.foldername = os.getcwd()
69
+ self.experiment_path_selection.setPlaceholderText('/path/to/experiment/folder/')
70
+ self.locate_exp_layout.addWidget(self.experiment_path_selection, 90)
71
+
72
+ self.browse_button = QPushButton("Browse...")
73
+ self.browse_button.clicked.connect(self.browse_experiment_folder)
74
+ self.browse_button.setStyleSheet(self.button_style_sheet)
75
+ self.browse_button.setIcon(icon(MDI6.folder, color="white"))
76
+ self.locate_exp_layout.addWidget(self.browse_button, 10)
77
+ self.vertical_layout.addLayout(self.locate_exp_layout)
78
+
79
+
80
+ def _createMenuBar(self):
81
+
82
+ menuBar = self.menuBar()
83
+ menuBar.clear()
84
+ # Creating menus using a QMenu object
85
+
86
+ fileMenu = QMenu("File", self)
87
+ fileMenu.clear()
88
+ fileMenu.addAction(self.newExpAction)
89
+ fileMenu.addAction(self.openAction)
90
+
91
+ fileMenu.addMenu(self.OpenRecentAction)
92
+ self.OpenRecentAction.clear()
93
+ if len(self.recentFileActs)>0:
94
+ for i in range(len(self.recentFileActs)):
95
+ self.OpenRecentAction.addAction(self.recentFileActs[i])
96
+
97
+ fileMenu.addAction(self.openModels)
98
+ fileMenu.addSeparator()
99
+ fileMenu.addAction(self.exitAction)
100
+ menuBar.addMenu(fileMenu)
101
+
102
+ OptionsMenu = QMenu("Options", self)
103
+ OptionsMenu.addAction(self.MemoryAndThreadsAction)
104
+ menuBar.addMenu(OptionsMenu)
105
+
106
+ helpMenu = QMenu("Help", self)
107
+ helpMenu.clear()
108
+ helpMenu.addAction(self.DocumentationAction)
109
+ helpMenu.addAction(self.SoftwareAction)
110
+ helpMenu.addSeparator()
111
+ helpMenu.addAction(self.AboutAction)
112
+ menuBar.addMenu(helpMenu)
113
+
114
+ #editMenu = menuBar.addMenu("&Edit")
115
+ #helpMenu = menuBar.addMenu("&Help")
116
+
117
+ def _createActions(self):
118
+ # Creating action using the first constructor
119
+ #self.newAction = QAction(self)
120
+ #self.newAction.setText("&New")
121
+ # Creating actions using the second constructor
122
+ self.openAction = QAction('Open...', self)
123
+ self.openAction.setShortcut("Ctrl+O")
124
+ self.openAction.setShortcutVisibleInContextMenu(True)
125
+
126
+ self.MemoryAndThreadsAction = QAction('Memory & Threads...')
127
+
128
+ self.newExpAction = QAction('New', self)
129
+ self.newExpAction.setShortcut("Ctrl+N")
130
+ self.newExpAction.setShortcutVisibleInContextMenu(True)
131
+ self.exitAction = QAction('Exit', self)
132
+
133
+ self.openModels = QAction('Open Models Location')
134
+ self.openModels.setShortcut("Ctrl+L")
135
+ self.openModels.setShortcutVisibleInContextMenu(True)
136
+
137
+ self.OpenRecentAction = QMenu('Open Recent')
138
+ self.reload_previous_experiments()
139
+
140
+ self.DocumentationAction = QAction("Documentation", self)
141
+ self.DocumentationAction.setShortcut("Ctrl+D")
142
+ self.DocumentationAction.setShortcutVisibleInContextMenu(True)
143
+
144
+ self.SoftwareAction = QAction("Software", self) #1st arg icon(MDI6.information)
145
+ self.AboutAction = QAction("About celldetective", self)
146
+
147
+ #self.DocumentationAction.triggered.connect(self.load_previous_config)
148
+ self.openAction.triggered.connect(self.open_experiment)
149
+ self.newExpAction.triggered.connect(self.create_new_experiment)
150
+ self.exitAction.triggered.connect(self.close)
151
+ self.openModels.triggered.connect(self.open_models_folder)
152
+ self.AboutAction.triggered.connect(self.open_about_window)
153
+ self.MemoryAndThreadsAction.triggered.connect(self.set_memory_and_threads)
154
+
155
+ self.DocumentationAction.triggered.connect(self.open_documentation)
156
+
157
+ def reload_previous_gpu_threads(self):
158
+
159
+ self.recentFileActs = []
160
+ self.threads_config_path = os.sep.join([self.soft_path,'celldetective','threads.json'])
161
+ if os.path.exists(self.threads_config_path):
162
+ with open(self.threads_config_path, 'r') as f:
163
+ self.threads_config = json.load(f)
164
+ if 'use_gpu' in self.threads_config:
165
+ self.use_gpu = bool(self.threads_config['use_gpu'])
166
+ if 'n_threads' in self.threads_config:
167
+ self.n_threads = int(self.threads_config['n_threads'])
168
+
169
+
170
+ def reload_previous_experiments(self):
171
+
172
+ recentExps = []
173
+ self.recentFileActs = []
174
+ if os.path.exists(os.sep.join([self.soft_path,'celldetective','recent.txt'])):
175
+ recentExps = open(os.sep.join([self.soft_path,'celldetective','recent.txt']), 'r')
176
+ recentExps = recentExps.readlines()
177
+ recentExps = [r.strip() for r in recentExps]
178
+ recentExps.reverse()
179
+ recentExps = list(dict.fromkeys(recentExps))
180
+ self.recentFileActs = [QAction(r,self) for r in recentExps]
181
+ for r in self.recentFileActs:
182
+ r.triggered.connect(lambda checked, item=r: self.load_recent_exp(item.text()))
183
+
184
+ def set_memory_and_threads(self):
185
+
186
+ print('setting memory and threads')
187
+
188
+ self.ThreadsWidget = QWidget()
189
+ self.ThreadsWidget.setWindowTitle("Threads")
190
+ layout = QVBoxLayout()
191
+ self.ThreadsWidget.setLayout(layout)
192
+
193
+ self.threads_le = QLineEdit(str(self.n_threads))
194
+ self.threads_le.setValidator(self.onlyInt)
195
+
196
+ hbox = QHBoxLayout()
197
+ hbox.addWidget(QLabel('Parallel threads: '), 33)
198
+ hbox.addWidget(self.threads_le, 66)
199
+ layout.addLayout(hbox)
200
+
201
+ self.use_gpu_checkbox = QCheckBox()
202
+ hbox2 = QHBoxLayout()
203
+ hbox2.addWidget(QLabel('Use GPU: '), 33)
204
+ hbox2.addWidget(self.use_gpu_checkbox, 66)
205
+ layout.addLayout(hbox2)
206
+ if self.use_gpu:
207
+ self.use_gpu_checkbox.setChecked(True)
208
+
209
+ self.validateThreadBtn = QPushButton('Submit')
210
+ self.validateThreadBtn.setStyleSheet(self.button_style_sheet)
211
+ self.validateThreadBtn.clicked.connect(self.set_threads)
212
+ layout.addWidget(self.validateThreadBtn)
213
+ center_window(self.ThreadsWidget)
214
+ self.ThreadsWidget.show()
215
+
216
+ def set_threads(self):
217
+ self.n_threads = int(self.threads_le.text())
218
+ self.use_gpu = bool(self.use_gpu_checkbox.isChecked())
219
+ dico = {"use_gpu": self.use_gpu, "n_threads": self.n_threads}
220
+ with open(self.threads_config_path, 'w') as f:
221
+ json.dump(dico, f, indent=4)
222
+ self.ThreadsWidget.close()
223
+
224
+
225
+ def open_experiment(self):
226
+ print('ok')
227
+ self.browse_experiment_folder()
228
+ if self.experiment_path_selection.text()!='':
229
+ self.open_directory()
230
+
231
+ def load_recent_exp(self, path):
232
+ print('loading?')
233
+ print('you selected path ', path)
234
+ self.experiment_path_selection.setText(path)
235
+ self.open_directory()
236
+
237
+ def open_about_window(self):
238
+ self.about_wdw = AboutWidget()
239
+ self.about_wdw.show()
240
+
241
+ def open_documentation(self):
242
+ doc_url = QUrl('https://celldetective.readthedocs.io/')
243
+ QDesktopServices.openUrl(doc_url)
244
+
245
+ def open_models_folder(self):
246
+ path = os.sep.join([self.soft_path,'celldetective','models',os.sep])
247
+ try:
248
+ subprocess.Popen(f'explorer {os.path.realpath(path)}')
249
+ except:
250
+
251
+ try:
252
+ os.system('xdg-open "%s"' % path)
253
+ except:
254
+ return None
255
+
256
+
257
+ #os.system(f'start {os.path.realpath(path)}')
258
+
259
+ def create_buttons_hbox(self):
260
+
261
+ self.buttons_layout = QHBoxLayout()
262
+ self.buttons_layout.setContentsMargins(30,15,30,5)
263
+ self.new_exp_button = QPushButton("New")
264
+ self.new_exp_button.clicked.connect(self.create_new_experiment)
265
+ self.new_exp_button.setStyleSheet(self.button_style_sheet_2)
266
+ self.buttons_layout.addWidget(self.new_exp_button, 50)
267
+
268
+ self.validate_button = QPushButton("Open")
269
+ self.validate_button.clicked.connect(self.open_directory)
270
+ self.validate_button.setStyleSheet(self.button_style_sheet)
271
+ self.validate_button.setEnabled(False)
272
+ self.validate_button.setShortcut("Return")
273
+ self.buttons_layout.addWidget(self.validate_button, 50)
274
+ self.vertical_layout.addLayout(self.buttons_layout)
275
+
276
+ def check_path_and_enable_opening(self):
277
+
278
+ """
279
+ Enable 'Open' button if the text is a valid path.
280
+ """
281
+
282
+ text = self.experiment_path_selection.text()
283
+ if (os.path.exists(text)) and os.path.exists(os.sep.join([text,"config.ini"])):
284
+ self.validate_button.setEnabled(True)
285
+ else:
286
+ self.validate_button.setEnabled(False)
287
+
288
+ def init_styles(self):
289
+
290
+ """
291
+ Initialize styles.
292
+ """
293
+
294
+ self.qtab_style = self.Styles.qtab_style
295
+ self.button_style_sheet = self.Styles.button_style_sheet
296
+ self.button_style_sheet_2 = self.Styles.button_style_sheet_2
297
+ self.button_style_sheet_2_not_done = self.Styles.button_style_sheet_2_not_done
298
+ self.button_style_sheet_3 = self.Styles.button_style_sheet_3
299
+ self.button_select_all = self.Styles.button_select_all
300
+
301
+ def set_experiment_path(self, path):
302
+ self.experiment_path_selection.setText(path)
303
+
304
+ def create_new_experiment(self):
305
+
306
+ print("Configuring new experiment...")
307
+ self.new_exp_window = ConfigNewExperiment(self)
308
+ self.new_exp_window.show()
309
+
310
+ def open_directory(self):
311
+
312
+ self.exp_dir = self.experiment_path_selection.text().replace('/', os.sep)
313
+ print(f"Setting current directory to {self.exp_dir}...")
314
+
315
+ wells = glob(os.sep.join([self.exp_dir,"W*"]))
316
+ self.number_of_wells = len(wells)
317
+ if self.number_of_wells==0:
318
+ msgBox = QMessageBox()
319
+ msgBox.setIcon(QMessageBox.Critical)
320
+ msgBox.setText("No well was found in the experiment folder.\nPlease respect the W*/ nomenclature...")
321
+ msgBox.setWindowTitle("Error")
322
+ msgBox.setStandardButtons(QMessageBox.Ok)
323
+ returnValue = msgBox.exec()
324
+ if returnValue == QMessageBox.Ok:
325
+ return None
326
+ else:
327
+ if self.number_of_wells==1:
328
+ print(f"Found {self.number_of_wells} well...")
329
+ elif self.number_of_wells>1:
330
+ print(f"Found {self.number_of_wells} wells...")
331
+ number_pos = []
332
+ for w in wells:
333
+ position_folders = glob(os.sep.join([w,f"{w.split(os.sep)[-2][1]}*", os.sep]))
334
+ number_pos.append(len(position_folders))
335
+ print(f"Number of positions per well: {number_pos}")
336
+
337
+ with open(os.sep.join([self.soft_path,'celldetective','recent.txt']), 'a+') as f:
338
+ f.write(self.exp_dir+'\n')
339
+
340
+ self.control_panel = ControlPanel(self, self.exp_dir)
341
+ self.control_panel.show()
342
+
343
+ self.reload_previous_experiments()
344
+ self._createMenuBar()
345
+
346
+
347
+ def browse_experiment_folder(self):
348
+
349
+ """
350
+ Locate an experiment folder. If no configuration file is in the experiment, display a warning.
351
+ """
352
+
353
+ self.foldername = str(QFileDialog.getExistingDirectory(self, 'Select directory'))
354
+ if self.foldername!='':
355
+ self.experiment_path_selection.setText(self.foldername)
356
+ else:
357
+ return None
358
+ if not os.path.exists(self.foldername+"/config.ini"):
359
+ msgBox = QMessageBox()
360
+ msgBox.setIcon(QMessageBox.Warning)
361
+ msgBox.setText("No configuration can be found in the selected folder...")
362
+ msgBox.setWindowTitle("Warning")
363
+ msgBox.setStandardButtons(QMessageBox.Ok)
364
+ returnValue = msgBox.exec()
365
+ if returnValue == QMessageBox.Ok:
366
+ self.experiment_path_selection.setText('')
367
+ return None
368
+
369
+ def closeEvent(self, event):
370
+
371
+ """
372
+ Close child windows if closed.
373
+ """
374
+
375
+ try:
376
+ if self.control_panel:
377
+ self.control_panel.close()
378
+ except:
379
+ pass
380
+ try:
381
+ if self.new_exp_window:
382
+ self.new_exp_window.close()
383
+ except:
384
+ pass
385
+
386
+ gc.collect()
387
+
388
+ if __name__ == "__main__":
389
+
390
+ # import ctypes
391
+ # myappid = 'mycompany.myproduct.subproduct.version' # arbitrary string
392
+ # ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
393
+ splash=True
394
+
395
+ App = QApplication(sys.argv)
396
+ #App.setWindowIcon(QIcon(os.sep.join([get_software_location(),'celldetective','icons','mexican-hat.png'])))
397
+ App.setStyle("Fusion")
398
+
399
+ if splash:
400
+ start = time()
401
+ splash_pix = QPixmap(sep.join([get_software_location(),'celldetective','icons','splash.png']))
402
+ splash = QSplashScreen(splash_pix)
403
+ splash.setMask(splash_pix.mask())
404
+ splash.show()
405
+ #App.processEvents(QEventLoop.AllEvents, 300)
406
+ while time() - start < 1:
407
+ sleep(0.001)
408
+ App.processEvents()
409
+
410
+ from PyQt5.QtWidgets import QFileDialog, QWidget, QVBoxLayout, QCheckBox, QHBoxLayout, QLabel, QLineEdit, QPushButton, QMessageBox, QMenu, QAction
411
+ from PyQt5.QtCore import Qt, QUrl
412
+ from PyQt5.QtGui import QIcon, QDesktopServices, QIntValidator
413
+ from glob import glob
414
+ from superqt.fonticon import icon
415
+ from fonticon_mdi6 import MDI6
416
+ import gc
417
+ from celldetective.gui import Styles, ControlPanel, ConfigNewExperiment
418
+ from celldetective.gui.gui_utils import center_window
419
+ import subprocess
420
+ import os
421
+ from celldetective.gui.about import AboutWidget
422
+ import psutil
423
+ import subprocess
424
+ import json
425
+
426
+
427
+ window = AppInitWindow(App)
428
+
429
+ if splash:
430
+ splash.finish(window)
431
+
432
+ sys.exit(App.exec())
File without changes
File without changes
@@ -0,0 +1,149 @@
1
+ import numpy as np
2
+
3
+ def switch_to_events(classes, times, max_times, first_detections=None, left_censored=False, FrameToMin=None):
4
+
5
+ events = []
6
+ survival_times = []
7
+ if first_detections is None:
8
+ first_detections = np.zeros_like(max_times)
9
+
10
+ for c,t,mt,ft in zip(classes, times, max_times, first_detections):
11
+
12
+ if left_censored:
13
+ #print('left censored is True: exclude cells that exist in first frame')
14
+ if ft>0.:
15
+ if c==0:
16
+ if t>0:
17
+ dt = t - ft
18
+ #print('event: dt = ',dt, t, ft)
19
+ if dt>0:
20
+ events.append(1)
21
+ survival_times.append(dt)
22
+ elif c==1:
23
+ dt = mt - ft
24
+ if dt>0:
25
+ events.append(0)
26
+ survival_times.append(dt)
27
+ else:
28
+ pass
29
+ else:
30
+ if c==0:
31
+ if t>0:
32
+ events.append(1)
33
+ survival_times.append(t - ft)
34
+ elif c==1:
35
+ events.append(0)
36
+ survival_times.append(mt - ft)
37
+ else:
38
+ pass
39
+
40
+ if FrameToMin is not None:
41
+ print('convert to minutes!', FrameToMin)
42
+ survival_times = [s*FrameToMin for s in survival_times]
43
+ return events, survival_times
44
+
45
+
46
+ def switch_to_events_v2(classes, event_times, max_times, origin_times=None, left_censored=True, FrameToMin=None):
47
+
48
+
49
+ """
50
+ Converts time-to-event data into a format suitable for survival analysis, optionally adjusting for left censorship
51
+ and converting time units.
52
+
53
+ This function processes event data by classifying each event based on whether it occurred or was censored by the end
54
+ of the observation period. It calculates the survival time for each event, taking into account the possibility of left
55
+ censorship and the option to convert time units (e.g., from frames to minutes).
56
+
57
+ Parameters
58
+ ----------
59
+ classes : array_like
60
+ An array indicating the class of each event (e.g., 0 for event, 1 for non-event, 2 for else).
61
+ event_times : array_like
62
+ An array of times at which events occurred. For non-events, this might represent the time of last observation.
63
+ max_times : array_like
64
+ An array of maximum observation times for each event.
65
+ origin_times : array_like, optional
66
+ An array of origin times for each event. If None, origin times are assumed to be zero, and `left_censored` is
67
+ automatically set to False (default is None).
68
+ left_censored : bool, optional
69
+ Indicates whether to adjust for left censorship. If True, events with origin times are considered left-censored
70
+ if the origin time is zero (default is True).
71
+ FrameToMin : float, optional
72
+ A conversion factor to transform survival times from frames (or any other unit) to minutes. If None, no conversion
73
+ is applied (default is None).
74
+
75
+ Returns
76
+ -------
77
+ tuple of lists
78
+ A tuple containing two lists: `events` and `survival_times`. `events` is a list of binary indicators (1 for event
79
+ occurrence, 0 for censorship), and `survival_times` is a list of survival times corresponding to each event or
80
+ censorship.
81
+
82
+ Notes
83
+ -----
84
+ - The function assumes that `classes`, `event_times`, `max_times`, and `origin_times` (if provided) are all arrays of
85
+ the same length.
86
+ - This function is particularly useful in preparing time-to-event data for survival analysis models, especially when
87
+ dealing with censored data and needing to adjust time units.
88
+
89
+ Examples
90
+ --------
91
+ >>> classes = [0, 1, 0]
92
+ >>> event_times = [5, 10, 15]
93
+ >>> max_times = [20, 20, 20]
94
+ >>> origin_times = [0, 0, 5]
95
+ >>> events, survival_times = switch_to_events_v2(classes, event_times, max_times, origin_times, FrameToMin=0.5)
96
+ # This would process the events considering left censorship and convert survival times to minutes.
97
+
98
+ """
99
+
100
+ events = []
101
+ survival_times = []
102
+
103
+ if origin_times is None:
104
+ # then origin is zero
105
+ origin_times = np.zeros_like(max_times)
106
+ left_censored = False
107
+
108
+ for c,t,mt,ot in zip(classes, event_times, max_times, origin_times):
109
+
110
+ if left_censored:
111
+
112
+ if ot>=0. and ot==ot:
113
+ # origin time is larger than zero, no censorship
114
+ if c==0 and t>0:
115
+ delta_t = t - ot
116
+ if delta_t>0:
117
+ events.append(1)
118
+ survival_times.append(delta_t)
119
+ else:
120
+ # negative delta t, invalid cell
121
+ pass
122
+ elif c==1:
123
+ delta_t = mt - ot
124
+ if delta_t>0:
125
+ events.append(0)
126
+ survival_times.append(delta_t)
127
+ else:
128
+ # negative delta t, invalid cell
129
+ pass
130
+ else:
131
+ pass
132
+ else:
133
+ # origin time is zero, the event is left censored (we did not observe it start)
134
+ pass
135
+
136
+ else:
137
+ if c==0 and t>0:
138
+ events.append(1)
139
+ survival_times.append(t - ot)
140
+ elif c==1:
141
+ events.append(0)
142
+ survival_times.append(mt - ot)
143
+ else:
144
+ pass
145
+
146
+ if FrameToMin is not None:
147
+ print('convert to minutes!', FrameToMin)
148
+ survival_times = [s*FrameToMin for s in survival_times]
149
+ return events, survival_times
@@ -0,0 +1,100 @@
1
+ """
2
+ Add extra properties to directly to regionprops
3
+ Functions must take regionmask as first argument and optionally intensity_image as second argument
4
+ If intensity is in function name, it will be replaced by the name of the channel. These measurements are applied automatically to all channels
5
+
6
+ """
7
+ import warnings
8
+
9
+ import numpy as np
10
+ from scipy.ndimage import distance_transform_edt
11
+ from scipy.spatial.distance import euclidean
12
+
13
+
14
+ # Percentiles
15
+
16
+ def intensity_percentile_ninety_nine(regionmask, intensity_image):
17
+ return np.nanpercentile(intensity_image[regionmask],99)
18
+
19
+ def intensity_percentile_ninety_five(regionmask, intensity_image):
20
+ return np.nanpercentile(intensity_image[regionmask],95)
21
+
22
+ def intensity_percentile_ninety(regionmask, intensity_image):
23
+ return np.nanpercentile(intensity_image[regionmask],90)
24
+
25
+ def intensity_percentile_seventy_five(regionmask, intensity_image):
26
+ return np.nanpercentile(intensity_image[regionmask],75)
27
+
28
+ def intensity_percentile_fifty(regionmask, intensity_image):
29
+ return np.nanpercentile(intensity_image[regionmask],50)
30
+
31
+ def intensity_percentile_twenty_five(regionmask, intensity_image):
32
+ return np.nanpercentile(intensity_image[regionmask],25)
33
+
34
+ # STD
35
+
36
+ def intensity_std(regionmask, intensity_image):
37
+ return np.nanstd(intensity_image[regionmask])
38
+
39
+
40
+ def intensity_median(regionmask, intensity_image):
41
+ return np.nanmedian(intensity_image[regionmask])
42
+
43
+
44
+ def intensity_centre_of_mass_displacement(regionmask, intensity_image):
45
+ y, x = np.mgrid[:regionmask.shape[0], :regionmask.shape[1]]
46
+ xtemp = x.copy()
47
+ ytemp = y.copy()
48
+
49
+ centroid_x = np.sum(xtemp * intensity_image) / np.sum(intensity_image)
50
+ centroid_y = np.sum(ytemp * intensity_image) / np.sum(intensity_image)
51
+ geometric_centroid_x = np.sum(xtemp * regionmask) / np.sum(regionmask)
52
+ geometric_centroid_y = np.sum(ytemp * regionmask) / np.sum(regionmask)
53
+ distance = euclidean(np.array((geometric_centroid_y, geometric_centroid_x)), np.array((centroid_y, centroid_x)))
54
+ delta_x = geometric_centroid_x - centroid_x
55
+ delta_y = geometric_centroid_y - centroid_y
56
+ direction_arctan = np.arctan2(delta_x, delta_y) * 180 / np.pi
57
+ return distance, direction_arctan
58
+
59
+
60
+ def intensity_radial_gradient(regionmask, intensity_image):
61
+ warnings.filterwarnings('ignore', message="Polyfit may be poorly conditioned")
62
+ cell_mask = regionmask.copy()
63
+ intensity = intensity_image.copy()
64
+ y = intensity[cell_mask].flatten()
65
+ x = distance_transform_edt(cell_mask)
66
+ x = x[cell_mask].flatten()
67
+ params = np.polyfit(x, y, 1)
68
+ line = np.poly1d(params)
69
+
70
+ return line.coefficients[0], line.coefficients[1]
71
+
72
+
73
+ def intensity_centre_of_mass_displacement_edge(regionmask, intensity_image):
74
+ edt = distance_transform_edt(regionmask)
75
+ min_distance = 0
76
+ max_distance = 0.1*edt.max()
77
+ thresholded = (edt <= max_distance) * (edt > min_distance)
78
+ edge_mask = np.copy(regionmask)
79
+ edge_mask[np.where(thresholded == 0)] = 0
80
+ y, x = np.mgrid[:edge_mask.shape[0], :edge_mask.shape[1]]
81
+ xtemp = x.copy()
82
+ ytemp = y.copy()
83
+ intensity_edge = intensity_image.copy()
84
+ intensity_edge[np.where(edge_mask == 0)] = 0.
85
+ sum_intensity_edge = np.sum(intensity_edge)
86
+ sum_regionmask = np.sum(regionmask)
87
+
88
+ if sum_intensity_edge != 0 and sum_regionmask != 0:
89
+ centroid_x = np.sum(xtemp * intensity_edge) / sum_intensity_edge
90
+ centroid_y = np.sum(ytemp * intensity_edge) / sum_intensity_edge
91
+ geometric_centroid_x = np.sum(xtemp * regionmask) / sum_regionmask
92
+ geometric_centroid_y = np.sum(ytemp * regionmask) / sum_regionmask
93
+
94
+ distance = euclidean((geometric_centroid_y, geometric_centroid_x), (centroid_y, centroid_x))
95
+ delta_x = geometric_centroid_x - centroid_x
96
+ delta_y = geometric_centroid_y - centroid_y
97
+ direction_arctan = np.arctan2(delta_y, delta_x) * 180 / np.pi
98
+ return distance, direction_arctan
99
+ else:
100
+ return np.nan, np.nan