celldetective 1.2.1__py3-none-any.whl → 1.2.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/__main__.py +12 -5
- celldetective/events.py +28 -2
- celldetective/gui/about.py +0 -1
- celldetective/gui/analyze_block.py +3 -18
- celldetective/gui/btrack_options.py +126 -21
- celldetective/gui/classifier_widget.py +67 -111
- celldetective/gui/configure_new_exp.py +37 -4
- celldetective/gui/control_panel.py +14 -30
- celldetective/gui/generic_signal_plot.py +793 -0
- celldetective/gui/gui_utils.py +401 -226
- celldetective/gui/json_readers.py +0 -2
- celldetective/gui/layouts.py +269 -25
- celldetective/gui/measurement_options.py +14 -23
- celldetective/gui/neighborhood_options.py +3 -15
- celldetective/gui/plot_measurements.py +10 -23
- celldetective/gui/plot_signals_ui.py +53 -687
- celldetective/gui/process_block.py +320 -186
- celldetective/gui/retrain_segmentation_model_options.py +30 -47
- celldetective/gui/retrain_signal_model_options.py +5 -14
- celldetective/gui/seg_model_loader.py +129 -113
- celldetective/gui/signal_annotator.py +89 -99
- celldetective/gui/signal_annotator2.py +5 -9
- celldetective/gui/styles.py +32 -0
- celldetective/gui/survival_ui.py +49 -712
- celldetective/gui/tableUI.py +0 -1
- celldetective/gui/thresholds_gui.py +38 -11
- celldetective/gui/viewers.py +6 -7
- celldetective/io.py +60 -82
- celldetective/measure.py +374 -15
- celldetective/neighborhood.py +1 -7
- celldetective/preprocessing.py +2 -4
- celldetective/relative_measurements.py +0 -3
- celldetective/scripts/analyze_signals.py +0 -1
- celldetective/scripts/measure_cells.py +1 -3
- celldetective/scripts/measure_relative.py +1 -2
- celldetective/scripts/segment_cells.py +16 -12
- celldetective/scripts/segment_cells_thresholds.py +17 -10
- celldetective/scripts/track_cells.py +18 -18
- celldetective/scripts/train_segmentation_model.py +1 -2
- celldetective/scripts/train_signal_model.py +0 -3
- celldetective/segmentation.py +1 -1
- celldetective/signals.py +17 -8
- celldetective/tracking.py +2 -1
- celldetective/utils.py +42 -2
- {celldetective-1.2.1.dist-info → celldetective-1.2.2.dist-info}/METADATA +19 -12
- celldetective-1.2.2.dist-info/RECORD +92 -0
- {celldetective-1.2.1.dist-info → celldetective-1.2.2.dist-info}/WHEEL +1 -1
- celldetective-1.2.1.dist-info/RECORD +0 -91
- {celldetective-1.2.1.dist-info → celldetective-1.2.2.dist-info}/LICENSE +0 -0
- {celldetective-1.2.1.dist-info → celldetective-1.2.2.dist-info}/entry_points.txt +0 -0
- {celldetective-1.2.1.dist-info → celldetective-1.2.2.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('
|
|
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
|
|
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
|
-
|
|
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)[-
|
|
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
|
+
|
celldetective/gui/about.py
CHANGED
|
@@ -1,25 +1,10 @@
|
|
|
1
|
-
from PyQt5.QtWidgets import QFrame,
|
|
2
|
-
|
|
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('
|
|
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
|
|
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.
|
|
51
|
-
self.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
186
|
-
|
|
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.
|
|
191
|
-
|
|
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(
|
|
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
|
-
|
|
454
|
-
|
|
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.
|
|
459
|
-
|
|
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
|
-
|
|
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,
|
|
1
|
+
from PyQt5.QtWidgets import QWidget, QLineEdit, QMessageBox, QHBoxLayout, QVBoxLayout, QPushButton, QLabel, \
|
|
2
2
|
QCheckBox, QRadioButton, QButtonGroup
|
|
3
|
-
from
|
|
4
|
-
import
|
|
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
|
-
|
|
7
|
+
|
|
10
8
|
import os
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
18
|
-
|
|
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 = [
|
|
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
|
-
|
|
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 = [
|
|
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
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
362
|
-
|
|
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
|
-
|