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