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.
- celldetective/__init__.py +2 -0
- celldetective/__main__.py +432 -0
- celldetective/datasets/segmentation_annotations/blank +0 -0
- celldetective/datasets/signal_annotations/blank +0 -0
- celldetective/events.py +149 -0
- celldetective/extra_properties.py +100 -0
- celldetective/filters.py +89 -0
- celldetective/gui/__init__.py +20 -0
- celldetective/gui/about.py +44 -0
- celldetective/gui/analyze_block.py +563 -0
- celldetective/gui/btrack_options.py +898 -0
- celldetective/gui/classifier_widget.py +386 -0
- celldetective/gui/configure_new_exp.py +532 -0
- celldetective/gui/control_panel.py +438 -0
- celldetective/gui/gui_utils.py +495 -0
- celldetective/gui/json_readers.py +113 -0
- celldetective/gui/measurement_options.py +1425 -0
- celldetective/gui/neighborhood_options.py +452 -0
- celldetective/gui/plot_signals_ui.py +1042 -0
- celldetective/gui/process_block.py +1055 -0
- celldetective/gui/retrain_segmentation_model_options.py +706 -0
- celldetective/gui/retrain_signal_model_options.py +643 -0
- celldetective/gui/seg_model_loader.py +460 -0
- celldetective/gui/signal_annotator.py +2388 -0
- celldetective/gui/signal_annotator_options.py +340 -0
- celldetective/gui/styles.py +217 -0
- celldetective/gui/survival_ui.py +903 -0
- celldetective/gui/tableUI.py +608 -0
- celldetective/gui/thresholds_gui.py +1300 -0
- celldetective/icons/logo-large.png +0 -0
- celldetective/icons/logo.png +0 -0
- celldetective/icons/signals_icon.png +0 -0
- celldetective/icons/splash-test.png +0 -0
- celldetective/icons/splash.png +0 -0
- celldetective/icons/splash0.png +0 -0
- celldetective/icons/survival2.png +0 -0
- celldetective/icons/vignette_signals2.png +0 -0
- celldetective/icons/vignette_signals2.svg +114 -0
- celldetective/io.py +2050 -0
- celldetective/links/zenodo.json +561 -0
- celldetective/measure.py +1258 -0
- celldetective/models/segmentation_effectors/blank +0 -0
- celldetective/models/segmentation_generic/blank +0 -0
- celldetective/models/segmentation_targets/blank +0 -0
- celldetective/models/signal_detection/blank +0 -0
- celldetective/models/tracking_configs/mcf7.json +68 -0
- celldetective/models/tracking_configs/ricm.json +203 -0
- celldetective/models/tracking_configs/ricm2.json +203 -0
- celldetective/neighborhood.py +717 -0
- celldetective/scripts/analyze_signals.py +51 -0
- celldetective/scripts/measure_cells.py +275 -0
- celldetective/scripts/segment_cells.py +212 -0
- celldetective/scripts/segment_cells_thresholds.py +140 -0
- celldetective/scripts/track_cells.py +206 -0
- celldetective/scripts/train_segmentation_model.py +246 -0
- celldetective/scripts/train_signal_model.py +49 -0
- celldetective/segmentation.py +712 -0
- celldetective/signals.py +2826 -0
- celldetective/tracking.py +974 -0
- celldetective/utils.py +1681 -0
- celldetective-1.0.2.dist-info/LICENSE +674 -0
- celldetective-1.0.2.dist-info/METADATA +192 -0
- celldetective-1.0.2.dist-info/RECORD +66 -0
- celldetective-1.0.2.dist-info/WHEEL +5 -0
- celldetective-1.0.2.dist-info/entry_points.txt +2 -0
- celldetective-1.0.2.dist-info/top_level.txt +1 -0
|
@@ -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
|
celldetective/events.py
ADDED
|
@@ -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
|