celldetective 1.3.0.post1__py3-none-any.whl → 1.3.1__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/_version.py +1 -1
- celldetective/events.py +86 -11
- celldetective/extra_properties.py +5 -1
- celldetective/gui/InitWindow.py +35 -9
- celldetective/gui/classifier_widget.py +50 -22
- celldetective/gui/layouts.py +128 -7
- celldetective/gui/measurement_options.py +3 -3
- celldetective/gui/process_block.py +46 -12
- celldetective/gui/retrain_segmentation_model_options.py +24 -10
- celldetective/gui/survival_ui.py +19 -2
- celldetective/gui/viewers.py +263 -3
- celldetective/links/zenodo.json +136 -123
- celldetective/models/tracking_configs/biased_motion.json +68 -0
- celldetective/models/tracking_configs/no_z_motion.json +202 -0
- celldetective/preprocessing.py +172 -3
- celldetective/signals.py +5 -2
- celldetective/tracking.py +7 -3
- celldetective/utils.py +6 -6
- {celldetective-1.3.0.post1.dist-info → celldetective-1.3.1.dist-info}/METADATA +3 -3
- {celldetective-1.3.0.post1.dist-info → celldetective-1.3.1.dist-info}/RECORD +24 -22
- {celldetective-1.3.0.post1.dist-info → celldetective-1.3.1.dist-info}/WHEEL +1 -1
- {celldetective-1.3.0.post1.dist-info → celldetective-1.3.1.dist-info}/LICENSE +0 -0
- {celldetective-1.3.0.post1.dist-info → celldetective-1.3.1.dist-info}/entry_points.txt +0 -0
- {celldetective-1.3.0.post1.dist-info → celldetective-1.3.1.dist-info}/top_level.txt +0 -0
celldetective/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "1.3.
|
|
1
|
+
__version__ = "1.3.1"
|
celldetective/events.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import numpy as np
|
|
2
2
|
from lifelines import KaplanMeierFitter
|
|
3
3
|
|
|
4
|
-
def switch_to_events(classes, event_times, max_times, origin_times=None, left_censored=True, FrameToMin=None):
|
|
4
|
+
def switch_to_events(classes, event_times, max_times, origin_times=None, left_censored=True, FrameToMin=None, cut_observation_time=None):
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
"""
|
|
@@ -29,6 +29,8 @@ def switch_to_events(classes, event_times, max_times, origin_times=None, left_ce
|
|
|
29
29
|
FrameToMin : float, optional
|
|
30
30
|
A conversion factor to transform survival times from frames (or any other unit) to minutes. If None, no conversion
|
|
31
31
|
is applied (default is None).
|
|
32
|
+
cut_observation_time : float or None, optional
|
|
33
|
+
A cutoff time to artificially reduce the observation window and exclude late events. If None, uses all available data (default is None).
|
|
32
34
|
|
|
33
35
|
Returns
|
|
34
36
|
-------
|
|
@@ -70,13 +72,36 @@ def switch_to_events(classes, event_times, max_times, origin_times=None, left_ce
|
|
|
70
72
|
if ot>=0. and ot==ot:
|
|
71
73
|
# origin time is larger than zero, no censorship
|
|
72
74
|
if c==0 and t>0:
|
|
75
|
+
|
|
73
76
|
delta_t = t - ot
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
|
|
78
|
+
# Special case: observation cut at arbitrary time
|
|
79
|
+
if cut_observation_time is not None:
|
|
80
|
+
if t>=cut_observation_time:
|
|
81
|
+
# event time larger than cut, becomes no event
|
|
82
|
+
delta_t = cut_observation_time - ot # new time
|
|
83
|
+
if delta_t > 0:
|
|
84
|
+
events.append(0)
|
|
85
|
+
survival_times.append(delta_t)
|
|
86
|
+
else:
|
|
87
|
+
# negative delta t, invalid cell
|
|
88
|
+
pass
|
|
89
|
+
else:
|
|
90
|
+
# still event
|
|
91
|
+
if delta_t > 0:
|
|
92
|
+
events.append(1)
|
|
93
|
+
survival_times.append(delta_t)
|
|
94
|
+
else:
|
|
95
|
+
# negative delta t, invalid cell
|
|
96
|
+
pass
|
|
97
|
+
else:
|
|
98
|
+
# standard mode
|
|
99
|
+
if delta_t>0:
|
|
100
|
+
events.append(1)
|
|
101
|
+
survival_times.append(delta_t)
|
|
102
|
+
else:
|
|
103
|
+
# negative delta t, invalid cell
|
|
104
|
+
pass
|
|
80
105
|
elif c==1:
|
|
81
106
|
delta_t = mt - ot
|
|
82
107
|
if delta_t>0:
|
|
@@ -93,10 +118,20 @@ def switch_to_events(classes, event_times, max_times, origin_times=None, left_ce
|
|
|
93
118
|
|
|
94
119
|
else:
|
|
95
120
|
if c==0 and t>0:
|
|
96
|
-
|
|
97
|
-
|
|
121
|
+
if cut_observation_time is not None:
|
|
122
|
+
if t>cut_observation_time:
|
|
123
|
+
events.append(0)
|
|
124
|
+
survival_times.append(cut_observation_time - ot)
|
|
125
|
+
else:
|
|
126
|
+
events.append(1)
|
|
127
|
+
survival_times.append(t - ot)
|
|
128
|
+
else:
|
|
129
|
+
events.append(1)
|
|
130
|
+
survival_times.append(t - ot)
|
|
98
131
|
elif c==1:
|
|
99
132
|
events.append(0)
|
|
133
|
+
if cut_observation_time is not None:
|
|
134
|
+
mt = cut_observation_time
|
|
100
135
|
survival_times.append(mt - ot)
|
|
101
136
|
else:
|
|
102
137
|
pass
|
|
@@ -106,7 +141,47 @@ def switch_to_events(classes, event_times, max_times, origin_times=None, left_ce
|
|
|
106
141
|
survival_times = [s*FrameToMin for s in survival_times]
|
|
107
142
|
return events, survival_times
|
|
108
143
|
|
|
109
|
-
def compute_survival(df, class_of_interest, t_event, t_reference=None, FrameToMin=1):
|
|
144
|
+
def compute_survival(df, class_of_interest, t_event, t_reference=None, FrameToMin=1, cut_observation_time=None):
|
|
145
|
+
|
|
146
|
+
"""
|
|
147
|
+
Computes survival analysis for a specific class of interest within a dataset, returning a fitted Kaplan-Meier
|
|
148
|
+
survival curve based on event and reference times.
|
|
149
|
+
|
|
150
|
+
Parameters
|
|
151
|
+
----------
|
|
152
|
+
df : pandas.DataFrame
|
|
153
|
+
The dataset containing tracking data, event times, and other relevant columns for survival analysis.
|
|
154
|
+
class_of_interest : str
|
|
155
|
+
The name of the column that specifies the class for which survival analysis is to be computed.
|
|
156
|
+
t_event : str
|
|
157
|
+
The column indicating the time of the event of interest (e.g., cell death or migration stop).
|
|
158
|
+
t_reference : str or None, optional
|
|
159
|
+
The reference column indicating the start or origin time for each track (e.g., detection time). If None,
|
|
160
|
+
events are not left-censored (default is None).
|
|
161
|
+
FrameToMin : float, optional
|
|
162
|
+
Conversion factor to scale the frame time to minutes (default is 1, assuming no scaling).
|
|
163
|
+
cut_observation_time : float or None, optional
|
|
164
|
+
A cutoff time to artificially reduce the observation window and exclude late events. If None, uses all available data (default is None).
|
|
165
|
+
Returns
|
|
166
|
+
-------
|
|
167
|
+
ks : lifelines.KaplanMeierFitter or None
|
|
168
|
+
A fitted Kaplan-Meier estimator object. If there are no events, returns None.
|
|
169
|
+
|
|
170
|
+
Notes
|
|
171
|
+
-----
|
|
172
|
+
- The function groups the data by 'position' and 'TRACK_ID', extracting the minimum `class_of_interest` and `t_event`
|
|
173
|
+
values for each track.
|
|
174
|
+
- If `t_reference` is provided, the analysis assumes left-censoring and will use `t_reference` as the origin time for
|
|
175
|
+
each track.
|
|
176
|
+
- The function calls `switch_to_events` to determine the event occurrences and their associated survival times.
|
|
177
|
+
- A Kaplan-Meier estimator (`KaplanMeierFitter`) is fitted to the data to compute the survival curve.
|
|
178
|
+
|
|
179
|
+
Example
|
|
180
|
+
-------
|
|
181
|
+
>>> ks = compute_survival(df, class_of_interest="class_custom", t_event="time_custom", t_reference="t_firstdetection")
|
|
182
|
+
>>> ks.plot_survival_function()
|
|
183
|
+
|
|
184
|
+
"""
|
|
110
185
|
|
|
111
186
|
cols = list(df.columns)
|
|
112
187
|
assert class_of_interest in cols,"The requested class cannot be found in the dataframe..."
|
|
@@ -127,7 +202,7 @@ def compute_survival(df, class_of_interest, t_event, t_reference=None, FrameToMi
|
|
|
127
202
|
assert t_reference in cols,"The reference time cannot be found in the dataframe..."
|
|
128
203
|
first_detections = df.groupby(['position','TRACK_ID'])[t_reference].max().values
|
|
129
204
|
|
|
130
|
-
events, survival_times = switch_to_events(classes, event_times, max_times, origin_times=first_detections, left_censored=left_censored, FrameToMin=FrameToMin)
|
|
205
|
+
events, survival_times = switch_to_events(classes, event_times, max_times, origin_times=first_detections, left_censored=left_censored, FrameToMin=FrameToMin, cut_observation_time=cut_observation_time)
|
|
131
206
|
ks = KaplanMeierFitter()
|
|
132
207
|
if len(events)>0:
|
|
133
208
|
ks.fit(survival_times, event_observed=events)
|
|
@@ -57,7 +57,11 @@ def intensity_median(regionmask, intensity_image):
|
|
|
57
57
|
return np.nanmedian(intensity_image[regionmask])
|
|
58
58
|
|
|
59
59
|
def intensity_nanmean(regionmask, intensity_image):
|
|
60
|
-
|
|
60
|
+
|
|
61
|
+
if np.all(intensity_image==0):
|
|
62
|
+
return np.nan
|
|
63
|
+
else:
|
|
64
|
+
return np.nanmean(intensity_image[regionmask])
|
|
61
65
|
|
|
62
66
|
def intensity_centre_of_mass_displacement(regionmask, intensity_image):
|
|
63
67
|
|
celldetective/gui/InitWindow.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from PyQt5.QtWidgets import QApplication, QMainWindow
|
|
2
|
-
from celldetective.utils import get_software_location
|
|
2
|
+
from celldetective.utils import get_software_location, download_zenodo_file
|
|
3
3
|
import os
|
|
4
4
|
from PyQt5.QtWidgets import QFileDialog, QWidget, QVBoxLayout, QCheckBox, QHBoxLayout, QLabel, QLineEdit, QPushButton, QMessageBox, QMenu, QAction
|
|
5
5
|
from PyQt5.QtCore import Qt, QUrl
|
|
@@ -108,6 +108,10 @@ class AppInitWindow(QMainWindow):
|
|
|
108
108
|
for i in range(len(self.recentFileActs)):
|
|
109
109
|
self.OpenRecentAction.addAction(self.recentFileActs[i])
|
|
110
110
|
|
|
111
|
+
fileMenu.addMenu(self.openDemo)
|
|
112
|
+
self.openDemo.addAction(self.openSpreadingAssayDemo)
|
|
113
|
+
self.openDemo.addAction(self.openCytotoxicityAssayDemo)
|
|
114
|
+
|
|
111
115
|
fileMenu.addAction(self.openModels)
|
|
112
116
|
fileMenu.addSeparator()
|
|
113
117
|
fileMenu.addAction(self.exitAction)
|
|
@@ -124,7 +128,7 @@ class AppInitWindow(QMainWindow):
|
|
|
124
128
|
helpMenu = QMenu("Help", self)
|
|
125
129
|
helpMenu.clear()
|
|
126
130
|
helpMenu.addAction(self.DocumentationAction)
|
|
127
|
-
helpMenu.addAction(self.SoftwareAction)
|
|
131
|
+
#helpMenu.addAction(self.SoftwareAction)
|
|
128
132
|
helpMenu.addSeparator()
|
|
129
133
|
helpMenu.addAction(self.AboutAction)
|
|
130
134
|
menuBar.addMenu(helpMenu)
|
|
@@ -137,13 +141,17 @@ class AppInitWindow(QMainWindow):
|
|
|
137
141
|
#self.newAction = QAction(self)
|
|
138
142
|
#self.newAction.setText("&New")
|
|
139
143
|
# Creating actions using the second constructor
|
|
140
|
-
self.openAction = QAction('Open
|
|
144
|
+
self.openAction = QAction('Open Project', self)
|
|
141
145
|
self.openAction.setShortcut("Ctrl+O")
|
|
142
146
|
self.openAction.setShortcutVisibleInContextMenu(True)
|
|
143
147
|
|
|
144
|
-
self.
|
|
148
|
+
self.openDemo = QMenu('Open Demo')
|
|
149
|
+
self.openSpreadingAssayDemo = QAction('Spreading Assay Demo', self)
|
|
150
|
+
self.openCytotoxicityAssayDemo = QAction('Cytotoxicity Assay Demo', self)
|
|
151
|
+
|
|
152
|
+
self.MemoryAndThreadsAction = QAction('Threads')
|
|
145
153
|
|
|
146
|
-
self.CorrectAnnotationAction = QAction('Correct a segmentation annotation
|
|
154
|
+
self.CorrectAnnotationAction = QAction('Correct a segmentation annotation')
|
|
147
155
|
|
|
148
156
|
self.newExpAction = QAction('New', self)
|
|
149
157
|
self.newExpAction.setShortcut("Ctrl+N")
|
|
@@ -154,14 +162,14 @@ class AppInitWindow(QMainWindow):
|
|
|
154
162
|
self.openModels.setShortcut("Ctrl+L")
|
|
155
163
|
self.openModels.setShortcutVisibleInContextMenu(True)
|
|
156
164
|
|
|
157
|
-
self.OpenRecentAction = QMenu('Open Recent')
|
|
165
|
+
self.OpenRecentAction = QMenu('Open Recent Project')
|
|
158
166
|
self.reload_previous_experiments()
|
|
159
167
|
|
|
160
168
|
self.DocumentationAction = QAction("Documentation", self)
|
|
161
169
|
self.DocumentationAction.setShortcut("Ctrl+D")
|
|
162
170
|
self.DocumentationAction.setShortcutVisibleInContextMenu(True)
|
|
163
171
|
|
|
164
|
-
self.SoftwareAction = QAction("Software", self) #1st arg icon(MDI6.information)
|
|
172
|
+
#self.SoftwareAction = QAction("Software", self) #1st arg icon(MDI6.information)
|
|
165
173
|
self.AboutAction = QAction("About celldetective", self)
|
|
166
174
|
|
|
167
175
|
#self.DocumentationAction.triggered.connect(self.load_previous_config)
|
|
@@ -172,9 +180,27 @@ class AppInitWindow(QMainWindow):
|
|
|
172
180
|
self.AboutAction.triggered.connect(self.open_about_window)
|
|
173
181
|
self.MemoryAndThreadsAction.triggered.connect(self.set_memory_and_threads)
|
|
174
182
|
self.CorrectAnnotationAction.triggered.connect(self.correct_seg_annotation)
|
|
175
|
-
|
|
176
183
|
self.DocumentationAction.triggered.connect(self.open_documentation)
|
|
177
184
|
|
|
185
|
+
self.openSpreadingAssayDemo.triggered.connect(self.download_spreading_assay_demo)
|
|
186
|
+
self.openCytotoxicityAssayDemo.triggered.connect(self.download_cytotoxicity_assay_demo)
|
|
187
|
+
|
|
188
|
+
def download_spreading_assay_demo(self):
|
|
189
|
+
|
|
190
|
+
self.target_dir = str(QFileDialog.getExistingDirectory(self, 'Select Folder for Download'))
|
|
191
|
+
if not os.path.exists(os.sep.join([self.target_dir,'demo_ricm'])):
|
|
192
|
+
download_zenodo_file('demo_ricm', self.target_dir)
|
|
193
|
+
self.experiment_path_selection.setText(os.sep.join([self.target_dir, 'demo_ricm']))
|
|
194
|
+
self.validate_button.click()
|
|
195
|
+
|
|
196
|
+
def download_cytotoxicity_assay_demo(self):
|
|
197
|
+
|
|
198
|
+
self.target_dir = str(QFileDialog.getExistingDirectory(self, 'Select Folder for Download'))
|
|
199
|
+
if not os.path.exists(os.sep.join([self.target_dir,'demo_adcc'])):
|
|
200
|
+
download_zenodo_file('demo_adcc', self.target_dir)
|
|
201
|
+
self.experiment_path_selection.setText(os.sep.join([self.target_dir, 'demo_adcc']))
|
|
202
|
+
self.validate_button.click()
|
|
203
|
+
|
|
178
204
|
def reload_previous_gpu_threads(self):
|
|
179
205
|
|
|
180
206
|
self.recentFileActs = []
|
|
@@ -256,7 +282,7 @@ class AppInitWindow(QMainWindow):
|
|
|
256
282
|
|
|
257
283
|
|
|
258
284
|
def open_experiment(self):
|
|
259
|
-
|
|
285
|
+
|
|
260
286
|
self.browse_experiment_folder()
|
|
261
287
|
if self.experiment_path_selection.text()!='':
|
|
262
288
|
self.open_directory()
|
|
@@ -120,9 +120,11 @@ class ClassifierWidget(QWidget, Styles):
|
|
|
120
120
|
self.property_query_le = QLineEdit()
|
|
121
121
|
self.property_query_le.setPlaceholderText('classify points using a query such as: area > 100 or eccentricity > 0.95')
|
|
122
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").')
|
|
123
|
+
self.property_query_le.textChanged.connect(self.activate_submit_btn)
|
|
123
124
|
hbox_classify.addWidget(self.property_query_le, 70)
|
|
124
125
|
self.submit_query_btn = QPushButton('Submit...')
|
|
125
126
|
self.submit_query_btn.clicked.connect(self.apply_property_query)
|
|
127
|
+
self.submit_query_btn.setEnabled(False)
|
|
126
128
|
hbox_classify.addWidget(self.submit_query_btn, 20)
|
|
127
129
|
layout.addLayout(hbox_classify)
|
|
128
130
|
|
|
@@ -182,15 +184,24 @@ class ClassifierWidget(QWidget, Styles):
|
|
|
182
184
|
|
|
183
185
|
layout.addWidget(QLabel())
|
|
184
186
|
|
|
185
|
-
|
|
186
187
|
self.submit_btn = QPushButton('apply')
|
|
187
188
|
self.submit_btn.setStyleSheet(self.button_style_sheet)
|
|
188
189
|
self.submit_btn.clicked.connect(self.submit_classification)
|
|
190
|
+
self.submit_btn.setEnabled(False)
|
|
189
191
|
layout.addWidget(self.submit_btn, 30)
|
|
190
192
|
|
|
191
193
|
self.frame_slider.valueChanged.connect(self.set_frame)
|
|
192
194
|
self.alpha_slider.valueChanged.connect(self.set_transparency)
|
|
193
195
|
|
|
196
|
+
def activate_submit_btn(self):
|
|
197
|
+
|
|
198
|
+
if self.property_query_le.text()=='':
|
|
199
|
+
self.submit_query_btn.setEnabled(False)
|
|
200
|
+
self.submit_btn.setEnabled(False)
|
|
201
|
+
else:
|
|
202
|
+
self.submit_query_btn.setEnabled(True)
|
|
203
|
+
self.submit_btn.setEnabled(True)
|
|
204
|
+
|
|
194
205
|
def activate_r2(self):
|
|
195
206
|
if self.irreversible_event_btn.isChecked() and self.time_corr.isChecked():
|
|
196
207
|
for wg in [self.r2_slider, self.r2_label]:
|
|
@@ -250,32 +261,32 @@ class ClassifierWidget(QWidget, Styles):
|
|
|
250
261
|
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())
|
|
251
262
|
colors = [color_from_status(c) for c in self.df.loc[self.df['FRAME']==self.currentFrame,class_name].to_numpy()]
|
|
252
263
|
self.scat_props.set_facecolor(colors)
|
|
253
|
-
self.scat_props.set_alpha(self.currentAlpha)
|
|
254
|
-
self.ax_props.set_xlabel(self.features_cb[1].currentText())
|
|
255
|
-
self.ax_props.set_ylabel(self.features_cb[0].currentText())
|
|
256
264
|
else:
|
|
257
265
|
self.scat_props.set_offsets(self.df[[self.features_cb[1].currentText(),self.features_cb[0].currentText()]].to_numpy())
|
|
258
266
|
colors = [color_from_status(c) for c in self.df[class_name].to_numpy()]
|
|
259
267
|
self.scat_props.set_facecolor(colors)
|
|
260
|
-
|
|
268
|
+
|
|
269
|
+
self.scat_props.set_alpha(self.currentAlpha)
|
|
270
|
+
|
|
271
|
+
if feature_changed:
|
|
272
|
+
|
|
261
273
|
self.ax_props.set_xlabel(self.features_cb[1].currentText())
|
|
262
274
|
self.ax_props.set_ylabel(self.features_cb[0].currentText())
|
|
263
275
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
self.ax_props.set_ylim(min_y, max_y)
|
|
276
|
+
feat_x = self.features_cb[1].currentText()
|
|
277
|
+
feat_y = self.features_cb[0].currentText()
|
|
278
|
+
min_x = self.df.dropna(subset=feat_x)[feat_x].min()
|
|
279
|
+
max_x = self.df.dropna(subset=feat_x)[feat_x].max()
|
|
280
|
+
min_y = self.df.dropna(subset=feat_y)[feat_y].min()
|
|
281
|
+
max_y = self.df.dropna(subset=feat_y)[feat_y].max()
|
|
282
|
+
|
|
283
|
+
if min_x==min_x and max_x==max_x:
|
|
284
|
+
self.ax_props.set_xlim(min_x, max_x)
|
|
285
|
+
if min_y==min_y and max_y==max_y:
|
|
286
|
+
self.ax_props.set_ylim(min_y, max_y)
|
|
276
287
|
|
|
277
|
-
if feature_changed:
|
|
278
288
|
self.propscanvas.canvas.toolbar.update()
|
|
289
|
+
|
|
279
290
|
self.propscanvas.canvas.draw_idle()
|
|
280
291
|
|
|
281
292
|
except Exception as e:
|
|
@@ -284,7 +295,20 @@ class ClassifierWidget(QWidget, Styles):
|
|
|
284
295
|
def apply_property_query(self):
|
|
285
296
|
|
|
286
297
|
query = self.property_query_le.text()
|
|
287
|
-
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
self.df = classify_cells_from_query(self.df, self.name_le.text(), query)
|
|
301
|
+
except Exception as e:
|
|
302
|
+
msgBox = QMessageBox()
|
|
303
|
+
msgBox.setIcon(QMessageBox.Warning)
|
|
304
|
+
msgBox.setText(f"The query could not be understood. No filtering was applied. {e}")
|
|
305
|
+
msgBox.setWindowTitle("Warning")
|
|
306
|
+
msgBox.setStandardButtons(QMessageBox.Ok)
|
|
307
|
+
returnValue = msgBox.exec()
|
|
308
|
+
if returnValue == QMessageBox.Ok:
|
|
309
|
+
self.auto_close = False
|
|
310
|
+
return None
|
|
311
|
+
|
|
288
312
|
self.class_name = "status_"+self.name_le.text()
|
|
289
313
|
if self.df is None:
|
|
290
314
|
msgBox = QMessageBox()
|
|
@@ -294,8 +318,10 @@ class ClassifierWidget(QWidget, Styles):
|
|
|
294
318
|
msgBox.setStandardButtons(QMessageBox.Ok)
|
|
295
319
|
returnValue = msgBox.exec()
|
|
296
320
|
if returnValue == QMessageBox.Ok:
|
|
321
|
+
self.auto_close = False
|
|
297
322
|
return None
|
|
298
|
-
|
|
323
|
+
|
|
324
|
+
self.update_props_scatter(feature_changed=False)
|
|
299
325
|
|
|
300
326
|
def set_frame(self, value):
|
|
301
327
|
xlim=self.ax_props.get_xlim()
|
|
@@ -329,12 +355,14 @@ class ClassifierWidget(QWidget, Styles):
|
|
|
329
355
|
self.project_times_btn.setIcon(icon(MDI6.math_integral_box,color="black"))
|
|
330
356
|
self.project_times_btn.setIconSize(QSize(20, 20))
|
|
331
357
|
self.frame_slider.setEnabled(False)
|
|
332
|
-
self.update_props_scatter()
|
|
358
|
+
self.update_props_scatter(feature_changed=False)
|
|
333
359
|
|
|
334
360
|
def submit_classification(self):
|
|
335
361
|
|
|
336
|
-
|
|
362
|
+
self.auto_close = True
|
|
337
363
|
self.apply_property_query()
|
|
364
|
+
if not self.auto_close:
|
|
365
|
+
return None
|
|
338
366
|
|
|
339
367
|
if self.time_corr.isChecked():
|
|
340
368
|
self.class_name_user = 'class_'+self.name_le.text()
|
celldetective/gui/layouts.py
CHANGED
|
@@ -8,7 +8,7 @@ from superqt import QLabeledRangeSlider, QLabeledDoubleSlider, QLabeledSlider, Q
|
|
|
8
8
|
from superqt.fonticon import icon
|
|
9
9
|
from fonticon_mdi6 import MDI6
|
|
10
10
|
from celldetective.utils import _extract_channel_indices_from_config
|
|
11
|
-
from celldetective.gui.viewers import ThresholdedStackVisualizer, CellEdgeVisualizer, StackVisualizer, CellSizeViewer
|
|
11
|
+
from celldetective.gui.viewers import ThresholdedStackVisualizer, CellEdgeVisualizer, StackVisualizer, CellSizeViewer, ChannelOffsetViewer
|
|
12
12
|
from celldetective.gui import Styles
|
|
13
13
|
from celldetective.preprocessing import correct_background_model, correct_background_model_free, estimate_background_per_condition
|
|
14
14
|
from functools import partial
|
|
@@ -535,7 +535,6 @@ class BackgroundFitCorrectionLayout(QGridLayout, Styles):
|
|
|
535
535
|
self.threshold_viewer_btn.clicked.connect(self.set_threshold_graphically)
|
|
536
536
|
self.threshold_viewer_btn.setToolTip('Set the threshold graphically.')
|
|
537
537
|
|
|
538
|
-
|
|
539
538
|
self.model_lbl = QLabel('Model: ')
|
|
540
539
|
self.model_lbl.setToolTip('2D model to fit the background with.')
|
|
541
540
|
self.models_cb = QComboBox()
|
|
@@ -560,6 +559,8 @@ class BackgroundFitCorrectionLayout(QGridLayout, Styles):
|
|
|
560
559
|
self.corrected_stack_viewer,
|
|
561
560
|
self.add_correction_btn
|
|
562
561
|
])
|
|
562
|
+
|
|
563
|
+
|
|
563
564
|
def add_to_layout(self):
|
|
564
565
|
|
|
565
566
|
channel_layout = QHBoxLayout()
|
|
@@ -572,6 +573,7 @@ class BackgroundFitCorrectionLayout(QGridLayout, Styles):
|
|
|
572
573
|
subthreshold_layout = QHBoxLayout()
|
|
573
574
|
subthreshold_layout.addWidget(self.threshold_le, 95)
|
|
574
575
|
subthreshold_layout.addWidget(self.threshold_viewer_btn, 5)
|
|
576
|
+
|
|
575
577
|
threshold_layout.addLayout(subthreshold_layout, 75)
|
|
576
578
|
self.addLayout(threshold_layout, 1, 0, 1, 3)
|
|
577
579
|
|
|
@@ -878,18 +880,26 @@ class ProtocolDesignerLayout(QVBoxLayout, Styles):
|
|
|
878
880
|
|
|
879
881
|
def generate_layout(self):
|
|
880
882
|
|
|
883
|
+
self.correction_layout = QVBoxLayout()
|
|
884
|
+
|
|
885
|
+
self.background_correction_layout = QVBoxLayout()
|
|
886
|
+
self.background_correction_layout.setContentsMargins(0,0,0,0)
|
|
881
887
|
self.title_layout = QHBoxLayout()
|
|
882
888
|
self.title_layout.addWidget(self.title_lbl, 100, alignment=Qt.AlignCenter)
|
|
889
|
+
self.background_correction_layout.addLayout(self.title_layout)
|
|
890
|
+
self.background_correction_layout.addWidget(self.tabs)
|
|
891
|
+
self.correction_layout.addLayout(self.background_correction_layout)
|
|
892
|
+
|
|
893
|
+
self.addLayout(self.correction_layout)
|
|
883
894
|
|
|
884
|
-
self.
|
|
885
|
-
self.addWidget(self.tabs)
|
|
886
|
-
|
|
895
|
+
self.list_layout = QVBoxLayout()
|
|
887
896
|
list_header_layout = QHBoxLayout()
|
|
888
897
|
list_header_layout.addWidget(self.protocol_list_lbl)
|
|
889
898
|
list_header_layout.addWidget(self.delete_protocol_btn, alignment=Qt.AlignRight)
|
|
890
|
-
self.addLayout(list_header_layout)
|
|
899
|
+
self.list_layout.addLayout(list_header_layout)
|
|
900
|
+
self.list_layout.addWidget(self.protocol_list)
|
|
891
901
|
|
|
892
|
-
self.
|
|
902
|
+
self.addLayout(self.list_layout)
|
|
893
903
|
|
|
894
904
|
|
|
895
905
|
def remove_protocol_from_list(self):
|
|
@@ -899,6 +909,117 @@ class ProtocolDesignerLayout(QVBoxLayout, Styles):
|
|
|
899
909
|
del self.protocols[current_item]
|
|
900
910
|
self.protocol_list.takeItem(current_item)
|
|
901
911
|
|
|
912
|
+
class ChannelOffsetOptionsLayout(QVBoxLayout, Styles):
|
|
913
|
+
|
|
914
|
+
def __init__(self, parent_window=None, *args, **kwargs):
|
|
915
|
+
|
|
916
|
+
super().__init__(*args, **kwargs)
|
|
917
|
+
|
|
918
|
+
self.parent_window = parent_window
|
|
919
|
+
if hasattr(self.parent_window.parent_window, 'exp_config'):
|
|
920
|
+
self.attr_parent = self.parent_window.parent_window
|
|
921
|
+
else:
|
|
922
|
+
self.attr_parent = self.parent_window.parent_window.parent_window
|
|
923
|
+
|
|
924
|
+
self.channel_names = self.attr_parent.exp_channels
|
|
925
|
+
|
|
926
|
+
self.setContentsMargins(15,15,15,15)
|
|
927
|
+
self.generate_widgets()
|
|
928
|
+
self.add_to_layout()
|
|
929
|
+
|
|
930
|
+
def generate_widgets(self):
|
|
931
|
+
|
|
932
|
+
self.channel_lbl = QLabel('Channel: ')
|
|
933
|
+
self.channels_cb = QComboBox()
|
|
934
|
+
self.channels_cb.addItems(self.channel_names)
|
|
935
|
+
|
|
936
|
+
self.shift_lbl = QLabel('Shift: ')
|
|
937
|
+
self.shift_h_lbl = QLabel('(h): ')
|
|
938
|
+
self.shift_v_lbl = QLabel('(v): ')
|
|
939
|
+
|
|
940
|
+
self.set_shift_btn = QPushButton()
|
|
941
|
+
self.set_shift_btn.setIcon(icon(MDI6.image_check, color="k"))
|
|
942
|
+
self.set_shift_btn.setStyleSheet(self.button_select_all)
|
|
943
|
+
self.set_shift_btn.setToolTip('Set the channel shift.')
|
|
944
|
+
self.set_shift_btn.clicked.connect(self.open_offset_viewer)
|
|
945
|
+
|
|
946
|
+
self.add_correction_btn = QPushButton('Add correction')
|
|
947
|
+
self.add_correction_btn.setStyleSheet(self.button_style_sheet_2)
|
|
948
|
+
self.add_correction_btn.setIcon(icon(MDI6.plus, color="#1565c0"))
|
|
949
|
+
self.add_correction_btn.setToolTip('Add correction.')
|
|
950
|
+
self.add_correction_btn.setIconSize(QSize(25, 25))
|
|
951
|
+
self.add_correction_btn.clicked.connect(self.add_instructions_to_parent_list)
|
|
952
|
+
|
|
953
|
+
self.vertical_shift_le = ThresholdLineEdit(init_value=0, connected_buttons=[self.add_correction_btn],placeholder='vertical shift [pixels]', value_type='float')
|
|
954
|
+
self.horizontal_shift_le = ThresholdLineEdit(init_value=0, connected_buttons=[self.add_correction_btn],placeholder='vertical shift [pixels]', value_type='float')
|
|
955
|
+
|
|
956
|
+
def add_to_layout(self):
|
|
957
|
+
|
|
958
|
+
channel_ch_hbox = QHBoxLayout()
|
|
959
|
+
channel_ch_hbox.addWidget(self.channel_lbl, 25)
|
|
960
|
+
channel_ch_hbox.addWidget(self.channels_cb, 75)
|
|
961
|
+
self.addLayout(channel_ch_hbox)
|
|
962
|
+
|
|
963
|
+
shift_hbox = QHBoxLayout()
|
|
964
|
+
shift_hbox.addWidget(self.shift_lbl, 25)
|
|
965
|
+
|
|
966
|
+
shift_subhbox = QHBoxLayout()
|
|
967
|
+
shift_subhbox.addWidget(self.shift_h_lbl, 10)
|
|
968
|
+
shift_subhbox.addWidget(self.horizontal_shift_le, 75//2)
|
|
969
|
+
shift_subhbox.addWidget(self.shift_v_lbl, 10)
|
|
970
|
+
shift_subhbox.addWidget(self.vertical_shift_le, 75//2)
|
|
971
|
+
shift_subhbox.addWidget(self.set_shift_btn, 5)
|
|
972
|
+
|
|
973
|
+
shift_hbox.addLayout(shift_subhbox, 75)
|
|
974
|
+
self.addLayout(shift_hbox)
|
|
975
|
+
|
|
976
|
+
btn_hbox = QHBoxLayout()
|
|
977
|
+
btn_hbox.addWidget(self.add_correction_btn, 95)
|
|
978
|
+
self.addLayout(btn_hbox)
|
|
979
|
+
|
|
980
|
+
def add_instructions_to_parent_list(self):
|
|
981
|
+
|
|
982
|
+
self.generate_instructions()
|
|
983
|
+
self.parent_window.protocol_layout.protocols.append(self.instructions)
|
|
984
|
+
correction_description = ""
|
|
985
|
+
for index, (key, value) in enumerate(self.instructions.items()):
|
|
986
|
+
if index > 0:
|
|
987
|
+
correction_description += ", "
|
|
988
|
+
correction_description += str(key) + " : " + str(value)
|
|
989
|
+
self.parent_window.protocol_layout.protocol_list.addItem(correction_description)
|
|
990
|
+
|
|
991
|
+
def generate_instructions(self):
|
|
992
|
+
|
|
993
|
+
self.instructions = {
|
|
994
|
+
"correction_type": "offset",
|
|
995
|
+
"target_channel": self.channels_cb.currentText(),
|
|
996
|
+
"correction_horizontal": self.horizontal_shift_le.get_threshold(),
|
|
997
|
+
"correction_vertical": self.vertical_shift_le.get_threshold(),
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
def set_target_channel(self):
|
|
1002
|
+
|
|
1003
|
+
channel_indices = _extract_channel_indices_from_config(self.attr_parent.exp_config, [self.channels_cb.currentText()])
|
|
1004
|
+
self.target_channel = channel_indices[0]
|
|
1005
|
+
|
|
1006
|
+
def open_offset_viewer(self):
|
|
1007
|
+
|
|
1008
|
+
self.attr_parent.locate_image()
|
|
1009
|
+
self.set_target_channel()
|
|
1010
|
+
|
|
1011
|
+
if self.attr_parent.current_stack is not None:
|
|
1012
|
+
self.viewer = ChannelOffsetViewer(
|
|
1013
|
+
parent_window = self,
|
|
1014
|
+
stack_path=self.attr_parent.current_stack,
|
|
1015
|
+
channel_names=self.attr_parent.exp_channels,
|
|
1016
|
+
n_channels=len(self.channel_names),
|
|
1017
|
+
channel_cb=True,
|
|
1018
|
+
target_channel=self.target_channel,
|
|
1019
|
+
window_title='offset viewer',
|
|
1020
|
+
)
|
|
1021
|
+
self.viewer.show()
|
|
1022
|
+
|
|
902
1023
|
|
|
903
1024
|
class BackgroundModelFreeCorrectionLayout(QGridLayout, Styles):
|
|
904
1025
|
|
|
@@ -154,7 +154,7 @@ class ConfigMeasurements(QMainWindow, Styles):
|
|
|
154
154
|
|
|
155
155
|
grid = QGridLayout(self.iso_frame)
|
|
156
156
|
|
|
157
|
-
self.iso_lbl = QLabel("
|
|
157
|
+
self.iso_lbl = QLabel("Position-based measurements".upper())
|
|
158
158
|
self.iso_lbl.setStyleSheet("""
|
|
159
159
|
font-weight: bold;
|
|
160
160
|
padding: 0px;
|
|
@@ -171,7 +171,7 @@ class ConfigMeasurements(QMainWindow, Styles):
|
|
|
171
171
|
|
|
172
172
|
grid = QGridLayout(self.features_frame)
|
|
173
173
|
|
|
174
|
-
self.feature_lbl = QLabel("
|
|
174
|
+
self.feature_lbl = QLabel("Mask-based measurements".upper())
|
|
175
175
|
self.feature_lbl.setStyleSheet("""
|
|
176
176
|
font-weight: bold;
|
|
177
177
|
padding: 0px;
|
|
@@ -262,7 +262,7 @@ class ConfigMeasurements(QMainWindow, Styles):
|
|
|
262
262
|
self.add_feature_btn.setToolTip("Add feature")
|
|
263
263
|
self.add_feature_btn.setIconSize(QSize(20, 20))
|
|
264
264
|
|
|
265
|
-
self.features_list = ListWidget(FeatureChoice, initial_features=['area', '
|
|
265
|
+
self.features_list = ListWidget(FeatureChoice, initial_features=['area', 'intensity_nanmean', ])
|
|
266
266
|
|
|
267
267
|
self.del_feature_btn.clicked.connect(self.features_list.removeSel)
|
|
268
268
|
self.add_feature_btn.clicked.connect(self.features_list.addItem)
|