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 CHANGED
@@ -1 +1 @@
1
- __version__ = "1.3.0"
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
- if delta_t>0:
75
- events.append(1)
76
- survival_times.append(delta_t)
77
- else:
78
- # negative delta t, invalid cell
79
- pass
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
- events.append(1)
97
- survival_times.append(t - ot)
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
- return np.nanmean(intensity_image[regionmask])
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
 
@@ -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...', self)
144
+ self.openAction = QAction('Open Project', self)
141
145
  self.openAction.setShortcut("Ctrl+O")
142
146
  self.openAction.setShortcutVisibleInContextMenu(True)
143
147
 
144
- self.MemoryAndThreadsAction = QAction('Memory & Threads...')
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
- print('ok')
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,self.class_name].to_numpy()]
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[self.class_name].to_numpy()]
266
+ colors = [color_from_status(c) for c in self.df[class_name].to_numpy()]
257
267
  self.scat_props.set_facecolor(colors)
258
- self.scat_props.set_alpha(self.currentAlpha)
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
- feat_x = self.features_cb[1].currentText()
264
- feat_y = self.features_cb[0].currentText()
265
- min_x = self.df.dropna(subset=feat_x)[feat_x].min()
266
- max_x = self.df.dropna(subset=feat_x)[feat_x].max()
267
- min_y = self.df.dropna(subset=feat_y)[feat_y].min()
268
- max_y = self.df.dropna(subset=feat_y)[feat_y].max()
269
-
270
- if min_x==min_x and max_x==max_x:
271
- self.ax_props.set_xlim(min_x, max_x)
272
- if min_y==min_y and max_y==max_y:
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
- self.df = classify_cells_from_query(self.df, self.class_name, query)
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
- print('submit')
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)
@@ -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.addLayout(self.title_layout)
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.addWidget(self.protocol_list)
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("ISOTROPIC MEASUREMENTS")
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("FEATURES")
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', 'intensity_mean', ])
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)