celldetective 1.0.2.post1__py3-none-any.whl → 1.1.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.
Files changed (63) hide show
  1. celldetective/__main__.py +7 -21
  2. celldetective/events.py +2 -44
  3. celldetective/extra_properties.py +62 -52
  4. celldetective/filters.py +4 -5
  5. celldetective/gui/__init__.py +1 -1
  6. celldetective/gui/analyze_block.py +37 -10
  7. celldetective/gui/btrack_options.py +24 -23
  8. celldetective/gui/classifier_widget.py +62 -19
  9. celldetective/gui/configure_new_exp.py +32 -35
  10. celldetective/gui/control_panel.py +120 -81
  11. celldetective/gui/gui_utils.py +674 -396
  12. celldetective/gui/json_readers.py +7 -6
  13. celldetective/gui/layouts.py +756 -0
  14. celldetective/gui/measurement_options.py +98 -513
  15. celldetective/gui/neighborhood_options.py +322 -270
  16. celldetective/gui/plot_measurements.py +1114 -0
  17. celldetective/gui/plot_signals_ui.py +21 -20
  18. celldetective/gui/process_block.py +449 -169
  19. celldetective/gui/retrain_segmentation_model_options.py +27 -26
  20. celldetective/gui/retrain_signal_model_options.py +25 -24
  21. celldetective/gui/seg_model_loader.py +31 -27
  22. celldetective/gui/signal_annotator.py +2326 -2295
  23. celldetective/gui/signal_annotator_options.py +18 -16
  24. celldetective/gui/styles.py +16 -1
  25. celldetective/gui/survival_ui.py +67 -39
  26. celldetective/gui/tableUI.py +337 -48
  27. celldetective/gui/thresholds_gui.py +75 -71
  28. celldetective/gui/viewers.py +743 -0
  29. celldetective/io.py +247 -27
  30. celldetective/measure.py +43 -263
  31. celldetective/models/segmentation_effectors/primNK_cfse/config_input.json +29 -0
  32. celldetective/models/segmentation_effectors/primNK_cfse/cp-cfse-transfer +0 -0
  33. celldetective/models/segmentation_effectors/primNK_cfse/training_instructions.json +37 -0
  34. celldetective/neighborhood.py +498 -27
  35. celldetective/preprocessing.py +1023 -0
  36. celldetective/scripts/analyze_signals.py +7 -0
  37. celldetective/scripts/measure_cells.py +12 -0
  38. celldetective/scripts/segment_cells.py +20 -4
  39. celldetective/scripts/track_cells.py +11 -0
  40. celldetective/scripts/train_segmentation_model.py +35 -34
  41. celldetective/segmentation.py +14 -9
  42. celldetective/signals.py +234 -329
  43. celldetective/tracking.py +2 -2
  44. celldetective/utils.py +602 -49
  45. celldetective-1.1.1.dist-info/METADATA +305 -0
  46. celldetective-1.1.1.dist-info/RECORD +84 -0
  47. {celldetective-1.0.2.post1.dist-info → celldetective-1.1.1.dist-info}/top_level.txt +1 -0
  48. tests/__init__.py +0 -0
  49. tests/test_events.py +28 -0
  50. tests/test_filters.py +24 -0
  51. tests/test_io.py +70 -0
  52. tests/test_measure.py +141 -0
  53. tests/test_neighborhood.py +70 -0
  54. tests/test_preprocessing.py +37 -0
  55. tests/test_segmentation.py +93 -0
  56. tests/test_signals.py +135 -0
  57. tests/test_tracking.py +164 -0
  58. tests/test_utils.py +118 -0
  59. celldetective-1.0.2.post1.dist-info/METADATA +0 -221
  60. celldetective-1.0.2.post1.dist-info/RECORD +0 -66
  61. {celldetective-1.0.2.post1.dist-info → celldetective-1.1.1.dist-info}/LICENSE +0 -0
  62. {celldetective-1.0.2.post1.dist-info → celldetective-1.1.1.dist-info}/WHEEL +0 -0
  63. {celldetective-1.0.2.post1.dist-info → celldetective-1.1.1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,756 @@
1
+ from PyQt5.QtWidgets import QCheckBox, QLineEdit, QWidget, QListWidget, QTabWidget, QHBoxLayout,QMessageBox, QPushButton, QVBoxLayout, QRadioButton, QLabel, QButtonGroup, QSizePolicy, QComboBox,QSpacerItem, QGridLayout
2
+ from celldetective.gui.gui_utils import ThresholdLineEdit
3
+ from PyQt5.QtCore import Qt, QSize
4
+ from PyQt5.QtGui import QIntValidator
5
+
6
+ from superqt import QLabeledRangeSlider, QLabeledSlider, QLabeledDoubleRangeSlider
7
+
8
+ from superqt.fonticon import icon
9
+ from fonticon_mdi6 import MDI6
10
+ from celldetective.utils import _extract_channel_indices_from_config
11
+ from celldetective.gui.viewers import ThresholdedStackVisualizer, CellEdgeVisualizer, StackVisualizer
12
+ from celldetective.gui import Styles
13
+ from celldetective.gui.gui_utils import QuickSliderLayout
14
+ from celldetective.preprocessing import correct_background_model, correct_background_model_free, estimate_background_per_condition
15
+
16
+ class BackgroundFitCorrectionLayout(QGridLayout, Styles):
17
+
18
+ """docstring for ClassName"""
19
+
20
+ def __init__(self, parent_window=None, *args):
21
+ super().__init__(*args)
22
+
23
+ self.parent_window = parent_window
24
+
25
+ if hasattr(self.parent_window.parent_window, 'locate_image'):
26
+ self.attr_parent = self.parent_window.parent_window
27
+ elif hasattr(self.parent_window.parent_window.parent_window, 'locate_image'):
28
+ self.attr_parent = self.parent_window.parent_window.parent_window
29
+ else:
30
+ self.attr_parent = self.parent_window.parent_window.parent_window.parent_window
31
+
32
+ self.channel_names = self.attr_parent.exp_channels
33
+ self.setContentsMargins(15,15,15,15)
34
+ self.generate_widgets()
35
+ self.add_to_layout()
36
+
37
+ def generate_widgets(self):
38
+
39
+ self.channel_lbl = QLabel('Channel: ')
40
+ self.channels_cb = QComboBox()
41
+ self.channels_cb.addItems(self.channel_names)
42
+
43
+ self.thresh_lbl = QLabel('Threshold: ')
44
+ self.thresh_lbl.setToolTip('Threshold on the STD-filtered image.\nPixel values above the threshold are\nconsidered as non-background and are\nmasked prior to background estimation.')
45
+ self.threshold_viewer_btn = QPushButton()
46
+ self.threshold_viewer_btn.setIcon(icon(MDI6.image_check, color="k"))
47
+ self.threshold_viewer_btn.setStyleSheet(self.button_select_all)
48
+ self.threshold_viewer_btn.clicked.connect(self.set_threshold_graphically)
49
+ self.threshold_viewer_btn.setToolTip('Set the threshold graphically.')
50
+
51
+
52
+ self.model_lbl = QLabel('Model: ')
53
+ self.model_lbl.setToolTip('2D model to fit the background with.')
54
+ self.models_cb = QComboBox()
55
+ self.models_cb.addItems(['paraboloid', 'plane'])
56
+ self.models_cb.setToolTip('2D model to fit the background with.')
57
+
58
+ self.corrected_stack_viewer = QPushButton("")
59
+ self.corrected_stack_viewer.setStyleSheet(self.button_select_all)
60
+ self.corrected_stack_viewer.setIcon(icon(MDI6.eye_outline, color="black"))
61
+ self.corrected_stack_viewer.setToolTip("View corrected image")
62
+ self.corrected_stack_viewer.clicked.connect(self.preview_correction)
63
+ self.corrected_stack_viewer.setIconSize(QSize(20, 20))
64
+
65
+ self.add_correction_btn = QPushButton('Add correction')
66
+ self.add_correction_btn.setStyleSheet(self.button_style_sheet_2)
67
+ self.add_correction_btn.setIcon(icon(MDI6.plus, color="#1565c0"))
68
+ self.add_correction_btn.setToolTip('Add correction.')
69
+ self.add_correction_btn.setIconSize(QSize(25, 25))
70
+ self.add_correction_btn.clicked.connect(self.add_instructions_to_parent_list)
71
+
72
+ self.threshold_le = ThresholdLineEdit(init_value=2, connected_buttons=[self.threshold_viewer_btn,
73
+ self.corrected_stack_viewer,
74
+ self.add_correction_btn
75
+ ])
76
+ def add_to_layout(self):
77
+
78
+ channel_layout = QHBoxLayout()
79
+ channel_layout.addWidget(self.channel_lbl, 25)
80
+ channel_layout.addWidget(self.channels_cb, 75)
81
+ self.addLayout(channel_layout, 0, 0, 1, 3)
82
+
83
+ threshold_layout = QHBoxLayout()
84
+ threshold_layout.addWidget(self.thresh_lbl, 25)
85
+ subthreshold_layout = QHBoxLayout()
86
+ subthreshold_layout.addWidget(self.threshold_le, 95)
87
+ subthreshold_layout.addWidget(self.threshold_viewer_btn, 5)
88
+ threshold_layout.addLayout(subthreshold_layout, 75)
89
+ self.addLayout(threshold_layout, 1, 0, 1, 3)
90
+
91
+ model_layout = QHBoxLayout()
92
+ model_layout.addWidget(self.model_lbl, 25)
93
+ model_layout.addWidget(self.models_cb, 75)
94
+ self.addLayout(model_layout, 2, 0, 1, 3)
95
+
96
+ self.operation_layout = OperationLayout()
97
+ self.addLayout(self.operation_layout, 3, 0, 1, 3)
98
+
99
+ correction_layout = QHBoxLayout()
100
+ correction_layout.addWidget(self.add_correction_btn, 95)
101
+ correction_layout.addWidget(self.corrected_stack_viewer, 5)
102
+ self.addLayout(correction_layout, 4, 0, 1, 3)
103
+
104
+ verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
105
+ self.addItem(verticalSpacer, 5, 0, 1, 3)
106
+
107
+ def add_instructions_to_parent_list(self):
108
+
109
+ self.generate_instructions()
110
+ self.parent_window.protocols.append(self.instructions)
111
+ correction_description = ""
112
+ for index, (key, value) in enumerate(self.instructions.items()):
113
+ if index > 0:
114
+ correction_description += ", "
115
+ correction_description += str(key) + " : " + str(value)
116
+ self.parent_window.protocol_list.addItem(correction_description)
117
+
118
+ def generate_instructions(self):
119
+
120
+ if self.operation_layout.subtract_btn.isChecked():
121
+ operation = "subtract"
122
+ else:
123
+ operation = "divide"
124
+ clip = None
125
+
126
+ if self.operation_layout.clip_btn.isChecked() and self.operation_layout.subtract_btn.isChecked():
127
+ clip = True
128
+ else:
129
+ clip = False
130
+
131
+ self.instructions = {
132
+ "target_channel": self.channels_cb.currentText(),
133
+ "correction_type": "fit",
134
+ "model": self.models_cb.currentText(),
135
+ "threshold_on_std": self.threshold_le.get_threshold(),
136
+ "operation": operation,
137
+ "clip": clip
138
+ }
139
+
140
+ def set_target_channel(self):
141
+
142
+ channel_indices = _extract_channel_indices_from_config(self.attr_parent.exp_config, [self.channels_cb.currentText()])
143
+ self.target_channel = channel_indices[0]
144
+
145
+ def set_threshold_graphically(self):
146
+
147
+ self.attr_parent.locate_image()
148
+ self.set_target_channel()
149
+ thresh = self.threshold_le.get_threshold()
150
+
151
+ if self.attr_parent.current_stack is not None and thresh is not None:
152
+ self.viewer = ThresholdedStackVisualizer(initial_threshold=thresh,
153
+ parent_le = self.threshold_le,
154
+ preprocessing=[['gauss',2],["std",4]],
155
+ stack_path=self.attr_parent.current_stack,
156
+ n_channels=len(self.channel_names),
157
+ target_channel=self.target_channel,
158
+ window_title='Set the exclusion threshold',
159
+ )
160
+ self.viewer.show()
161
+
162
+ def preview_correction(self):
163
+
164
+ if self.attr_parent.well_list.currentText()=="*" or self.attr_parent.position_list.currentText()=="*":
165
+ msgBox = QMessageBox()
166
+ msgBox.setIcon(QMessageBox.Critical)
167
+ msgBox.setText("Please select a single position...")
168
+ msgBox.setWindowTitle("Critical")
169
+ msgBox.setStandardButtons(QMessageBox.Ok)
170
+ returnValue = msgBox.exec()
171
+ if returnValue == QMessageBox.Ok:
172
+ return None
173
+
174
+ if self.operation_layout.subtract_btn.isChecked():
175
+ operation = "subtract"
176
+ else:
177
+ operation = "divide"
178
+ clip = None
179
+
180
+ if self.operation_layout.clip_btn.isChecked() and self.operation_layout.subtract_btn.isChecked():
181
+ clip = True
182
+ else:
183
+ clip = False
184
+
185
+ corrected_stack = correct_background_model(self.attr_parent.exp_dir,
186
+ well_option=self.attr_parent.well_list.currentIndex(), #+1 ??
187
+ position_option=self.attr_parent.position_list.currentIndex()-1, #+1??
188
+ target_channel=self.channels_cb.currentText(),
189
+ model = self.models_cb.currentText(),
190
+ threshold_on_std = self.threshold_le.get_threshold(),
191
+ operation = operation,
192
+ clip = clip,
193
+ export= False,
194
+ return_stacks=True,
195
+ activation_protocol=[['gauss',2],['std',4]],
196
+ show_progress_per_well = True,
197
+ show_progress_per_pos = False,
198
+ )
199
+
200
+ self.viewer = StackVisualizer(
201
+ stack=corrected_stack[0],
202
+ window_title='Corrected channel',
203
+ target_channel=self.channels_cb.currentIndex(),
204
+ frame_slider = True,
205
+ contrast_slider = True
206
+ )
207
+ self.viewer.show()
208
+
209
+
210
+ class LocalCorrectionLayout(BackgroundFitCorrectionLayout):
211
+
212
+ """docstring for ClassName"""
213
+
214
+ def __init__(self, *args):
215
+
216
+ super().__init__(*args)
217
+
218
+ if hasattr(self.parent_window.parent_window, 'locate_image'):
219
+ self.attr_parent = self.parent_window.parent_window
220
+ elif hasattr(self.parent_window.parent_window.parent_window, 'locate_image'):
221
+ self.attr_parent = self.parent_window.parent_window.parent_window
222
+ else:
223
+ self.attr_parent = self.parent_window.parent_window.parent_window.parent_window
224
+
225
+ self.thresh_lbl.setText('Distance: ')
226
+ self.thresh_lbl.setToolTip('Distance from the cell mask over which to estimate local intensity.')
227
+
228
+ self.models_cb.clear()
229
+ self.models_cb.addItems(['mean','median'])
230
+
231
+ self.threshold_le.set_threshold(5)
232
+ self.threshold_le.connected_buttons = [self.threshold_viewer_btn,self.add_correction_btn]
233
+ self.threshold_le.setValidator(QIntValidator())
234
+
235
+ self.threshold_viewer_btn.disconnect()
236
+ self.threshold_viewer_btn.clicked.connect(self.set_distance_graphically)
237
+
238
+ self.corrected_stack_viewer.hide()
239
+
240
+ def set_distance_graphically(self):
241
+
242
+ self.attr_parent.locate_image()
243
+ self.set_target_channel()
244
+ thresh = self.threshold_le.get_threshold()
245
+
246
+ if self.attr_parent.current_stack is not None and thresh is not None:
247
+
248
+ self.viewer = CellEdgeVisualizer(cell_type=self.parent_window.parent_window.mode,
249
+ stack_path=self.attr_parent.current_stack,
250
+ parent_le = self.threshold_le,
251
+ n_channels=len(self.channel_names),
252
+ target_channel=self.channels_cb.currentIndex(),
253
+ edge_range = (0,30),
254
+ initial_edge=int(thresh),
255
+ invert=True,
256
+ window_title='Set an edge distance to estimate local intensity',
257
+ channel_cb=False,
258
+ PxToUm = 1,
259
+ )
260
+ self.viewer.show()
261
+
262
+ def generate_instructions(self):
263
+
264
+ if self.operation_layout.subtract_btn.isChecked():
265
+ operation = "subtract"
266
+ else:
267
+ operation = "divide"
268
+ clip = None
269
+
270
+ if self.operation_layout.clip_btn.isChecked() and self.operation_layout.subtract_btn.isChecked():
271
+ clip = True
272
+ else:
273
+ clip = False
274
+
275
+ self.instructions = {
276
+ "target_channel": self.channels_cb.currentText(),
277
+ "correction_type": "local",
278
+ "model": self.models_cb.currentText(),
279
+ "distance": int(self.threshold_le.get_threshold()),
280
+ "operation": operation,
281
+ "clip": clip,
282
+ }
283
+
284
+
285
+ class OperationLayout(QVBoxLayout):
286
+
287
+ """docstring for ClassName"""
288
+
289
+ def __init__(self, ratio=(0.25,0.75), *args):
290
+
291
+ super().__init__(*args)
292
+
293
+ self.ratio = ratio
294
+ self.generate_widgets()
295
+ self.generate_layout()
296
+
297
+ def generate_widgets(self):
298
+
299
+ self.operation_lbl = QLabel('Operation: ')
300
+ self.operation_group = QButtonGroup()
301
+ self.subtract_btn = QRadioButton('Subtract')
302
+ self.divide_btn = QRadioButton('Divide')
303
+ self.subtract_btn.toggled.connect(self.activate_clipping_options)
304
+ self.divide_btn.toggled.connect(self.activate_clipping_options)
305
+
306
+ self.operation_group.addButton(self.subtract_btn)
307
+ self.operation_group.addButton(self.divide_btn)
308
+
309
+ self.clip_group = QButtonGroup()
310
+ self.clip_btn = QRadioButton('Clip')
311
+ self.clip_not_btn = QRadioButton('Do not clip')
312
+
313
+ self.clip_group.addButton(self.clip_btn)
314
+ self.clip_group.addButton(self.clip_not_btn)
315
+
316
+ def generate_layout(self):
317
+
318
+ operation_layout = QHBoxLayout()
319
+ operation_layout.addWidget(self.operation_lbl, 100*int(self.ratio[0]))
320
+ operation_layout.addWidget(self.subtract_btn, 100*int(self.ratio[1])//2, alignment=Qt.AlignCenter)
321
+ operation_layout.addWidget(self.divide_btn, 100*int(self.ratio[1])//2, alignment=Qt.AlignCenter)
322
+ self.addLayout(operation_layout)
323
+
324
+ clip_layout = QHBoxLayout()
325
+ clip_layout.addWidget(QLabel(''), 100*int(self.ratio[0]))
326
+ clip_layout.addWidget(self.clip_btn, 100*int(self.ratio[1])//4, alignment=Qt.AlignCenter)
327
+ clip_layout.addWidget(self.clip_not_btn, 100*int(self.ratio[1])//4, alignment=Qt.AlignCenter)
328
+ clip_layout.addWidget(QLabel(''), 100*int(self.ratio[1])//2)
329
+ self.addLayout(clip_layout)
330
+
331
+ self.subtract_btn.click()
332
+ self.clip_not_btn.click()
333
+
334
+ def activate_clipping_options(self):
335
+
336
+ if self.subtract_btn.isChecked():
337
+ self.clip_btn.setEnabled(True)
338
+ self.clip_not_btn.setEnabled(True)
339
+ else:
340
+ self.clip_btn.setEnabled(False)
341
+ self.clip_not_btn.setEnabled(False)
342
+
343
+ class ProtocolDesignerLayout(QVBoxLayout, Styles):
344
+
345
+ """Multi tabs and list widget configuration for background correction
346
+ in preprocessing and measurements
347
+ """
348
+
349
+ def __init__(self, parent_window=None, tab_layouts=[], tab_names=[], title='',list_title='',*args):
350
+
351
+ super().__init__(*args)
352
+
353
+ self.title = title
354
+ self.parent_window = parent_window
355
+ self.channel_names = self.parent_window.channel_names
356
+ self.tab_layouts = tab_layouts
357
+ self.tab_names = tab_names
358
+ self.list_title = list_title
359
+ self.protocols = []
360
+ assert len(self.tab_layouts)==len(self.tab_names)
361
+
362
+ self.generate_widgets()
363
+ self.generate_layout()
364
+
365
+ def generate_widgets(self):
366
+
367
+ self.title_lbl = QLabel(self.title)
368
+ self.title_lbl.setStyleSheet("""
369
+ font-weight: bold;
370
+ padding: 0px;
371
+ """)
372
+
373
+ self.tabs = QTabWidget()
374
+ self.tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
375
+
376
+ for k in range(len(self.tab_layouts)):
377
+ wg = QWidget()
378
+ print('almost there',self.channel_names)
379
+ self.tab_layouts[k].parent_window = self
380
+ wg.setLayout(self.tab_layouts[k])
381
+ self.tabs.addTab(wg, self.tab_names[k])
382
+
383
+ self.protocol_list_lbl = QLabel(self.list_title)
384
+ self.protocol_list = QListWidget()
385
+
386
+ self.delete_protocol_btn = QPushButton('')
387
+ self.delete_protocol_btn.setStyleSheet(self.button_select_all)
388
+ self.delete_protocol_btn.setIcon(icon(MDI6.trash_can, color="black"))
389
+ self.delete_protocol_btn.setToolTip("Remove.")
390
+ self.delete_protocol_btn.setIconSize(QSize(20, 20))
391
+ self.delete_protocol_btn.clicked.connect(self.remove_protocol_from_list)
392
+
393
+ def generate_layout(self):
394
+
395
+ self.addWidget(self.title_lbl, alignment=Qt.AlignCenter)
396
+ self.addWidget(self.tabs)
397
+
398
+ list_header_layout = QHBoxLayout()
399
+ list_header_layout.addWidget(self.protocol_list_lbl)
400
+ list_header_layout.addWidget(self.delete_protocol_btn, alignment=Qt.AlignRight)
401
+ self.addLayout(list_header_layout)
402
+
403
+ self.addWidget(self.protocol_list)
404
+
405
+
406
+ def remove_protocol_from_list(self):
407
+
408
+ current_item = self.protocol_list.currentRow()
409
+ if current_item > -1:
410
+ del self.protocols[current_item]
411
+ self.protocol_list.takeItem(current_item)
412
+
413
+
414
+ class BackgroundModelFreeCorrectionLayout(QGridLayout, Styles):
415
+
416
+ """docstring for ClassName"""
417
+
418
+ def __init__(self, parent_window=None, *args):
419
+ super().__init__(*args)
420
+
421
+ self.parent_window = parent_window
422
+
423
+ if hasattr(self.parent_window.parent_window, 'exp_config'):
424
+ self.attr_parent = self.parent_window.parent_window
425
+ else:
426
+ self.attr_parent = self.parent_window.parent_window.parent_window
427
+
428
+ self.channel_names = self.attr_parent.exp_channels
429
+
430
+ self.setContentsMargins(15,15,15,15)
431
+ self.generate_widgets()
432
+ self.add_to_layout()
433
+
434
+ def generate_widgets(self):
435
+
436
+ self.channel_lbl = QLabel('Channel: ')
437
+ self.channels_cb = QComboBox()
438
+ self.channels_cb.addItems(self.channel_names)
439
+
440
+ self.acquistion_lbl = QLabel('Stack mode: ')
441
+ self.acq_mode_group = QButtonGroup()
442
+ self.timeseries_rb = QRadioButton('timeseries')
443
+ self.timeseries_rb.setChecked(True)
444
+ self.tiles_rb = QRadioButton('tiles')
445
+ self.acq_mode_group.addButton(self.timeseries_rb, 0)
446
+ self.acq_mode_group.addButton(self.tiles_rb, 1)
447
+
448
+ from PyQt5.QtWidgets import QSlider
449
+ from superqt import QRangeSlider
450
+ self.frame_range_slider = QLabeledRangeSlider(parent=None)
451
+ print('here ok')
452
+
453
+ self.timeseries_rb.toggled.connect(self.activate_time_range)
454
+ self.tiles_rb.toggled.connect(self.activate_time_range)
455
+
456
+ self.thresh_lbl = QLabel('Threshold: ')
457
+ self.thresh_lbl.setToolTip('Threshold on the STD-filtered image.\nPixel values above the threshold are\nconsidered as non-background and are\nmasked prior to background estimation.')
458
+ self.threshold_viewer_btn = QPushButton()
459
+ self.threshold_viewer_btn.setIcon(icon(MDI6.image_check, color="k"))
460
+ self.threshold_viewer_btn.setStyleSheet(self.button_select_all)
461
+ self.threshold_viewer_btn.clicked.connect(self.set_threshold_graphically)
462
+
463
+ self.background_viewer_btn = QPushButton()
464
+ self.background_viewer_btn.setIcon(icon(MDI6.image_check, color="k"))
465
+ self.background_viewer_btn.setStyleSheet(self.button_select_all)
466
+ self.background_viewer_btn.setToolTip('View reconstructed background.')
467
+
468
+ self.corrected_stack_viewer_btn = QPushButton("")
469
+ self.corrected_stack_viewer_btn.setStyleSheet(self.button_select_all)
470
+ self.corrected_stack_viewer_btn.setIcon(icon(MDI6.eye_outline, color="black"))
471
+ self.corrected_stack_viewer_btn.setToolTip("View corrected image")
472
+ self.corrected_stack_viewer_btn.clicked.connect(self.preview_correction)
473
+ self.corrected_stack_viewer_btn.setIconSize(QSize(20, 20))
474
+
475
+ self.add_correction_btn = QPushButton('Add correction')
476
+ self.add_correction_btn.setStyleSheet(self.button_style_sheet_2)
477
+ self.add_correction_btn.setIcon(icon(MDI6.plus, color="#1565c0"))
478
+ self.add_correction_btn.setToolTip('Add correction.')
479
+ self.add_correction_btn.setIconSize(QSize(25, 25))
480
+ self.add_correction_btn.clicked.connect(self.add_instructions_to_parent_list)
481
+
482
+ self.threshold_le = ThresholdLineEdit(init_value=2, connected_buttons=[self.threshold_viewer_btn,
483
+ self.background_viewer_btn, self.corrected_stack_viewer_btn, self.add_correction_btn])
484
+
485
+ self.well_slider = QLabeledSlider(parent=None)
486
+
487
+ self.background_viewer_btn.clicked.connect(self.estimate_bg)
488
+
489
+ self.regress_cb = QCheckBox('Optimize for each frame?')
490
+ self.regress_cb.toggled.connect(self.activate_coef_options)
491
+ self.regress_cb.setChecked(False)
492
+
493
+ self.coef_range_slider = QLabeledDoubleRangeSlider(parent=None)
494
+ self.coef_range_layout = QuickSliderLayout(label='Coef. range: ',
495
+ slider = self.coef_range_slider,
496
+ slider_initial_value=(0.95,1.05),
497
+ slider_range=(0.75,1.25),
498
+ slider_tooltip='Coefficient range to increase or decrease the background intensity level...',
499
+ )
500
+
501
+ self.nbr_coefs_lbl = QLabel("Nbr of coefs: ")
502
+ self.nbr_coefs_lbl.setToolTip('Number of coefficients to be tested within range.\nThe more, the slower.')
503
+
504
+ self.nbr_coef_le = QLineEdit()
505
+ self.nbr_coef_le.setText('100')
506
+ self.nbr_coef_le.setValidator(QIntValidator())
507
+ self.nbr_coef_le.setPlaceholderText('nbr of coefs')
508
+
509
+ self.coef_widgets = [self.coef_range_layout.qlabel, self.coef_range_slider, self.nbr_coefs_lbl, self.nbr_coef_le]
510
+ for c in self.coef_widgets:
511
+ c.setEnabled(False)
512
+
513
+ def add_to_layout(self):
514
+
515
+ channel_layout = QHBoxLayout()
516
+ channel_layout.addWidget(self.channel_lbl, 25)
517
+ channel_layout.addWidget(self.channels_cb, 75)
518
+ self.addLayout(channel_layout, 0, 0, 1, 3)
519
+
520
+ acquisition_layout = QHBoxLayout()
521
+ acquisition_layout.addWidget(self.acquistion_lbl, 25)
522
+ acquisition_layout.addWidget(self.timeseries_rb, 75//2, alignment=Qt.AlignCenter)
523
+ acquisition_layout.addWidget(self.tiles_rb, 75//2, alignment=Qt.AlignCenter)
524
+ self.addLayout(acquisition_layout, 1, 0, 1, 3)
525
+
526
+ frame_selection_layout = QuickSliderLayout(label='Time range: ',
527
+ slider = self.frame_range_slider,
528
+ slider_initial_value=(0,5),
529
+ slider_range=(0,self.attr_parent.len_movie),
530
+ slider_tooltip='frame [#]',
531
+ decimal_option = False,
532
+ )
533
+ frame_selection_layout.qlabel.setToolTip('Frame range for which the background\nis most likely to be observed.')
534
+ self.time_range_options = [self.frame_range_slider, frame_selection_layout.qlabel]
535
+ self.addLayout(frame_selection_layout, 2, 0, 1, 3)
536
+
537
+
538
+ threshold_layout = QHBoxLayout()
539
+ threshold_layout.addWidget(self.thresh_lbl, 25)
540
+ subthreshold_layout = QHBoxLayout()
541
+ subthreshold_layout.addWidget(self.threshold_le, 95)
542
+ subthreshold_layout.addWidget(self.threshold_viewer_btn, 5)
543
+ threshold_layout.addLayout(subthreshold_layout, 75)
544
+ self.addLayout(threshold_layout, 3, 0, 1, 3)
545
+
546
+ background_layout = QuickSliderLayout(label='QC for well: ',
547
+ slider = self.well_slider,
548
+ slider_initial_value=1,
549
+ slider_range=(1,len(self.attr_parent.wells)),
550
+ slider_tooltip='well [#]',
551
+ decimal_option = False,
552
+ layout_ratio=(0.25,0.70)
553
+ )
554
+ background_layout.addWidget(self.background_viewer_btn, 5)
555
+ self.addLayout(background_layout, 4, 0, 1, 3)
556
+
557
+ self.addWidget(self.regress_cb, 5, 0, 1, 3)
558
+
559
+ self.addLayout(self.coef_range_layout, 6, 0, 1, 3)
560
+
561
+ coef_nbr_layout = QHBoxLayout()
562
+ coef_nbr_layout.addWidget(self.nbr_coefs_lbl, 25)
563
+ coef_nbr_layout.addWidget(self.nbr_coef_le, 75)
564
+ self.addLayout(coef_nbr_layout, 7,0,1,3)
565
+
566
+ self.operation_layout = OperationLayout()
567
+ self.addLayout(self.operation_layout, 8, 0, 1, 3)
568
+
569
+ correction_layout = QHBoxLayout()
570
+ correction_layout.addWidget(self.add_correction_btn, 95)
571
+ correction_layout.addWidget(self.corrected_stack_viewer_btn, 5)
572
+ self.addLayout(correction_layout, 9, 0, 1, 3)
573
+
574
+ # verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
575
+ # self.addItem(verticalSpacer, 5, 0, 1, 3)
576
+
577
+ def add_instructions_to_parent_list(self):
578
+
579
+ self.generate_instructions()
580
+ self.parent_window.protocols.append(self.instructions)
581
+ correction_description = ""
582
+ for index, (key, value) in enumerate(self.instructions.items()):
583
+ if index > 0:
584
+ correction_description += ", "
585
+ correction_description += str(key) + " : " + str(value)
586
+ self.parent_window.protocol_list.addItem(correction_description)
587
+
588
+ def generate_instructions(self):
589
+
590
+ if self.timeseries_rb.isChecked():
591
+ mode = "timeseries"
592
+ elif self.tiles_rb.isChecked():
593
+ mode = "tiles"
594
+
595
+ if self.regress_cb.isChecked():
596
+ optimize_option = True
597
+ opt_coef_range = self.coef_range_slider.value()
598
+ opt_coef_nbr = int(self.nbr_coef_le.text())
599
+ else:
600
+ optimize_option = False
601
+ opt_coef_range = None
602
+ opt_coef_nbr = None
603
+
604
+ if self.operation_layout.subtract_btn.isChecked():
605
+ operation = "subtract"
606
+ else:
607
+ operation = "divide"
608
+ clip = None
609
+
610
+ if self.operation_layout.clip_btn.isChecked() and self.operation_layout.subtract_btn.isChecked():
611
+ clip = True
612
+ else:
613
+ clip = False
614
+
615
+ self.instructions = {
616
+ "target_channel": self.channels_cb.currentText(),
617
+ "correction_type": "model-free",
618
+ "threshold_on_std": self.threshold_le.get_threshold(),
619
+ "frame_range": self.frame_range_slider.value(),
620
+ "mode": mode,
621
+ "optimize_option": optimize_option,
622
+ "opt_coef_range": opt_coef_range,
623
+ "opt_coef_nbr": opt_coef_nbr,
624
+ "operation": operation,
625
+ "clip": clip
626
+ }
627
+
628
+ def set_target_channel(self):
629
+
630
+ channel_indices = _extract_channel_indices_from_config(self.attr_parent.exp_config, [self.channels_cb.currentText()])
631
+ self.target_channel = channel_indices[0]
632
+
633
+ def set_threshold_graphically(self):
634
+
635
+ self.attr_parent.locate_image()
636
+ self.set_target_channel()
637
+ thresh = self.threshold_le.get_threshold()
638
+
639
+ if self.attr_parent.current_stack is not None and thresh is not None:
640
+ self.viewer = ThresholdedStackVisualizer(initial_threshold=thresh,
641
+ parent_le = self.threshold_le,
642
+ preprocessing=[['gauss',2],["std",4]],
643
+ stack_path=self.attr_parent.current_stack,
644
+ n_channels=len(self.channel_names),
645
+ target_channel=self.target_channel,
646
+ window_title='Set the exclusion threshold',
647
+ )
648
+ self.viewer.show()
649
+
650
+ def preview_correction(self):
651
+
652
+ if self.attr_parent.well_list.currentText()=="*" or self.attr_parent.position_list.currentText()=="*":
653
+ msgBox = QMessageBox()
654
+ msgBox.setIcon(QMessageBox.Warning)
655
+ msgBox.setText("Please select a single position...")
656
+ msgBox.setWindowTitle("Warning")
657
+ msgBox.setStandardButtons(QMessageBox.Ok)
658
+ returnValue = msgBox.exec()
659
+ if returnValue == QMessageBox.Ok:
660
+ return None
661
+
662
+ if self.timeseries_rb.isChecked():
663
+ mode = "timeseries"
664
+ elif self.tiles_rb.isChecked():
665
+ mode = "tiles"
666
+
667
+ if self.regress_cb.isChecked():
668
+ optimize_option = True
669
+ opt_coef_range = self.coef_range_slider.value()
670
+ opt_coef_nbr = int(self.nbr_coef_le.text())
671
+ else:
672
+ optimize_option = False
673
+ opt_coef_range = None
674
+ opt_coef_nbr = None
675
+
676
+ if self.operation_layout.subtract_btn.isChecked():
677
+ operation = "subtract"
678
+ else:
679
+ operation = "divide"
680
+ clip = None
681
+
682
+ if self.operation_layout.clip_btn.isChecked() and self.operation_layout.subtract_btn.isChecked():
683
+ clip = True
684
+ else:
685
+ clip = False
686
+
687
+ corrected_stacks = correct_background_model_free(self.attr_parent.exp_dir,
688
+ well_option=self.attr_parent.well_list.currentIndex(), #+1 ??
689
+ position_option=self.attr_parent.position_list.currentIndex()-1, #+1??
690
+ target_channel=self.channels_cb.currentText(),
691
+ mode = mode,
692
+ threshold_on_std = self.threshold_le.get_threshold(),
693
+ frame_range = self.frame_range_slider.value(),
694
+ optimize_option = optimize_option,
695
+ opt_coef_range = opt_coef_range,
696
+ opt_coef_nbr = opt_coef_nbr,
697
+ operation = operation,
698
+ clip = clip,
699
+ export= False,
700
+ return_stacks=True,
701
+ show_progress_per_well = True,
702
+ show_progress_per_pos = False,
703
+ )
704
+
705
+
706
+ self.viewer = StackVisualizer(
707
+ stack=corrected_stacks[0],
708
+ window_title='Corrected channel',
709
+ frame_slider = True,
710
+ contrast_slider = True
711
+ )
712
+ self.viewer.show()
713
+
714
+ def activate_time_range(self):
715
+
716
+ if self.timeseries_rb.isChecked():
717
+ for wg in self.time_range_options:
718
+ wg.setEnabled(True)
719
+ elif self.tiles_rb.isChecked():
720
+ for wg in self.time_range_options:
721
+ wg.setEnabled(False)
722
+
723
+ def activate_coef_options(self):
724
+
725
+ if self.regress_cb.isChecked():
726
+ for c in self.coef_widgets:
727
+ c.setEnabled(True)
728
+ else:
729
+ for c in self.coef_widgets:
730
+ c.setEnabled(False)
731
+
732
+ def estimate_bg(self):
733
+
734
+ if self.timeseries_rb.isChecked():
735
+ mode = "timeseries"
736
+ elif self.tiles_rb.isChecked():
737
+ mode = "tiles"
738
+
739
+ bg = estimate_background_per_condition(
740
+ self.attr_parent.exp_dir,
741
+ well_option = self.well_slider.value() - 1,
742
+ frame_range = self.frame_range_slider.value(),
743
+ target_channel = self.channels_cb.currentText(),
744
+ show_progress_per_pos = True,
745
+ threshold_on_std = self.threshold_le.get_threshold(),
746
+ mode = mode,
747
+ )
748
+ bg = bg[0]
749
+ bg = bg['bg']
750
+
751
+ self.viewer = StackVisualizer(
752
+ stack=[bg],
753
+ window_title='Reconstructed background',
754
+ frame_slider = False,
755
+ )
756
+ self.viewer.show()