celldetective 1.3.0__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 +57 -24
- 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/io.py +4 -1
- celldetective/links/zenodo.json +136 -123
- celldetective/measure.py +5 -5
- 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.dist-info → celldetective-1.3.1.dist-info}/METADATA +3 -3
- {celldetective-1.3.0.dist-info → celldetective-1.3.1.dist-info}/RECORD +26 -24
- {celldetective-1.3.0.dist-info → celldetective-1.3.1.dist-info}/WHEEL +1 -1
- {celldetective-1.3.0.dist-info → celldetective-1.3.1.dist-info}/LICENSE +0 -0
- {celldetective-1.3.0.dist-info → celldetective-1.3.1.dist-info}/entry_points.txt +0 -0
- {celldetective-1.3.0.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]:
|
|
@@ -242,38 +253,40 @@ class ClassifierWidget(QWidget, Styles):
|
|
|
242
253
|
|
|
243
254
|
def update_props_scatter(self, feature_changed=True):
|
|
244
255
|
|
|
256
|
+
class_name = self.class_name
|
|
257
|
+
|
|
245
258
|
try:
|
|
246
259
|
|
|
247
260
|
if not self.project_times:
|
|
248
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())
|
|
249
|
-
colors = [color_from_status(c) for c in self.df.loc[self.df['FRAME']==self.currentFrame,
|
|
262
|
+
colors = [color_from_status(c) for c in self.df.loc[self.df['FRAME']==self.currentFrame,class_name].to_numpy()]
|
|
250
263
|
self.scat_props.set_facecolor(colors)
|
|
251
|
-
self.scat_props.set_alpha(self.currentAlpha)
|
|
252
|
-
self.ax_props.set_xlabel(self.features_cb[1].currentText())
|
|
253
|
-
self.ax_props.set_ylabel(self.features_cb[0].currentText())
|
|
254
264
|
else:
|
|
255
265
|
self.scat_props.set_offsets(self.df[[self.features_cb[1].currentText(),self.features_cb[0].currentText()]].to_numpy())
|
|
256
|
-
colors = [color_from_status(c) for c in self.df[
|
|
266
|
+
colors = [color_from_status(c) for c in self.df[class_name].to_numpy()]
|
|
257
267
|
self.scat_props.set_facecolor(colors)
|
|
258
|
-
|
|
268
|
+
|
|
269
|
+
self.scat_props.set_alpha(self.currentAlpha)
|
|
270
|
+
|
|
271
|
+
if feature_changed:
|
|
272
|
+
|
|
259
273
|
self.ax_props.set_xlabel(self.features_cb[1].currentText())
|
|
260
274
|
self.ax_props.set_ylabel(self.features_cb[0].currentText())
|
|
261
275
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
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)
|
|
274
287
|
|
|
275
|
-
if feature_changed:
|
|
276
288
|
self.propscanvas.canvas.toolbar.update()
|
|
289
|
+
|
|
277
290
|
self.propscanvas.canvas.draw_idle()
|
|
278
291
|
|
|
279
292
|
except Exception as e:
|
|
@@ -282,7 +295,21 @@ class ClassifierWidget(QWidget, Styles):
|
|
|
282
295
|
def apply_property_query(self):
|
|
283
296
|
|
|
284
297
|
query = self.property_query_le.text()
|
|
285
|
-
|
|
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
|
+
|
|
312
|
+
self.class_name = "status_"+self.name_le.text()
|
|
286
313
|
if self.df is None:
|
|
287
314
|
msgBox = QMessageBox()
|
|
288
315
|
msgBox.setIcon(QMessageBox.Warning)
|
|
@@ -291,9 +318,10 @@ class ClassifierWidget(QWidget, Styles):
|
|
|
291
318
|
msgBox.setStandardButtons(QMessageBox.Ok)
|
|
292
319
|
returnValue = msgBox.exec()
|
|
293
320
|
if returnValue == QMessageBox.Ok:
|
|
321
|
+
self.auto_close = False
|
|
294
322
|
return None
|
|
295
323
|
|
|
296
|
-
self.update_props_scatter()
|
|
324
|
+
self.update_props_scatter(feature_changed=False)
|
|
297
325
|
|
|
298
326
|
def set_frame(self, value):
|
|
299
327
|
xlim=self.ax_props.get_xlim()
|
|
@@ -327,12 +355,14 @@ class ClassifierWidget(QWidget, Styles):
|
|
|
327
355
|
self.project_times_btn.setIcon(icon(MDI6.math_integral_box,color="black"))
|
|
328
356
|
self.project_times_btn.setIconSize(QSize(20, 20))
|
|
329
357
|
self.frame_slider.setEnabled(False)
|
|
330
|
-
self.update_props_scatter()
|
|
358
|
+
self.update_props_scatter(feature_changed=False)
|
|
331
359
|
|
|
332
360
|
def submit_classification(self):
|
|
333
361
|
|
|
334
|
-
|
|
362
|
+
self.auto_close = True
|
|
335
363
|
self.apply_property_query()
|
|
364
|
+
if not self.auto_close:
|
|
365
|
+
return None
|
|
336
366
|
|
|
337
367
|
if self.time_corr.isChecked():
|
|
338
368
|
self.class_name_user = 'class_'+self.name_le.text()
|
|
@@ -351,6 +381,7 @@ class ClassifierWidget(QWidget, Styles):
|
|
|
351
381
|
return None
|
|
352
382
|
|
|
353
383
|
name_map = {self.class_name: self.class_name_user}
|
|
384
|
+
print(f"{name_map=}")
|
|
354
385
|
self.df = self.df.drop(list(set(name_map.values()) & set(self.df.columns)), axis=1).rename(columns=name_map)
|
|
355
386
|
self.df.reset_index(inplace=True, drop=True)
|
|
356
387
|
|
|
@@ -379,6 +410,8 @@ class ClassifierWidget(QWidget, Styles):
|
|
|
379
410
|
#self.df[self.group_name_user] = self.df[self.group_name_user].replace({0: 1, 1: 0})
|
|
380
411
|
self.df.reset_index(inplace=True, drop=True)
|
|
381
412
|
|
|
413
|
+
if 'custom' in list(self.df.columns):
|
|
414
|
+
self.df = self.df.drop(['custom'],axis=1)
|
|
382
415
|
|
|
383
416
|
for pos,pos_group in self.df.groupby('position'):
|
|
384
417
|
pos_group.to_csv(pos+os.sep.join(['output', 'tables', f'trajectories_{self.mode}.csv']), index=False)
|
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)
|