celldetective 1.4.2__py3-none-any.whl → 1.5.0b0__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 (151) hide show
  1. celldetective/__init__.py +25 -0
  2. celldetective/__main__.py +62 -43
  3. celldetective/_version.py +1 -1
  4. celldetective/extra_properties.py +477 -399
  5. celldetective/filters.py +192 -97
  6. celldetective/gui/InitWindow.py +541 -411
  7. celldetective/gui/__init__.py +0 -15
  8. celldetective/gui/about.py +44 -39
  9. celldetective/gui/analyze_block.py +120 -84
  10. celldetective/gui/base/__init__.py +0 -0
  11. celldetective/gui/base/channel_norm_generator.py +335 -0
  12. celldetective/gui/base/components.py +249 -0
  13. celldetective/gui/base/feature_choice.py +92 -0
  14. celldetective/gui/base/figure_canvas.py +52 -0
  15. celldetective/gui/base/list_widget.py +133 -0
  16. celldetective/gui/{styles.py → base/styles.py} +92 -36
  17. celldetective/gui/base/utils.py +33 -0
  18. celldetective/gui/base_annotator.py +900 -767
  19. celldetective/gui/classifier_widget.py +6 -22
  20. celldetective/gui/configure_new_exp.py +777 -671
  21. celldetective/gui/control_panel.py +635 -524
  22. celldetective/gui/dynamic_progress.py +449 -0
  23. celldetective/gui/event_annotator.py +2023 -1662
  24. celldetective/gui/generic_signal_plot.py +1292 -944
  25. celldetective/gui/gui_utils.py +899 -1289
  26. celldetective/gui/interactions_block.py +658 -0
  27. celldetective/gui/interactive_timeseries_viewer.py +447 -0
  28. celldetective/gui/json_readers.py +48 -15
  29. celldetective/gui/layouts/__init__.py +5 -0
  30. celldetective/gui/layouts/background_model_free_layout.py +537 -0
  31. celldetective/gui/layouts/channel_offset_layout.py +134 -0
  32. celldetective/gui/layouts/local_correction_layout.py +91 -0
  33. celldetective/gui/layouts/model_fit_layout.py +372 -0
  34. celldetective/gui/layouts/operation_layout.py +68 -0
  35. celldetective/gui/layouts/protocol_designer_layout.py +96 -0
  36. celldetective/gui/pair_event_annotator.py +3130 -2435
  37. celldetective/gui/plot_measurements.py +586 -267
  38. celldetective/gui/plot_signals_ui.py +724 -506
  39. celldetective/gui/preprocessing_block.py +395 -0
  40. celldetective/gui/process_block.py +1678 -1831
  41. celldetective/gui/seg_model_loader.py +580 -473
  42. celldetective/gui/settings/__init__.py +0 -7
  43. celldetective/gui/settings/_cellpose_model_params.py +181 -0
  44. celldetective/gui/settings/_event_detection_model_params.py +95 -0
  45. celldetective/gui/settings/_segmentation_model_params.py +159 -0
  46. celldetective/gui/settings/_settings_base.py +77 -65
  47. celldetective/gui/settings/_settings_event_model_training.py +752 -526
  48. celldetective/gui/settings/_settings_measurements.py +1133 -964
  49. celldetective/gui/settings/_settings_neighborhood.py +574 -488
  50. celldetective/gui/settings/_settings_segmentation_model_training.py +779 -564
  51. celldetective/gui/settings/_settings_signal_annotator.py +329 -305
  52. celldetective/gui/settings/_settings_tracking.py +1304 -1094
  53. celldetective/gui/settings/_stardist_model_params.py +98 -0
  54. celldetective/gui/survival_ui.py +422 -312
  55. celldetective/gui/tableUI.py +1665 -1701
  56. celldetective/gui/table_ops/_maths.py +295 -0
  57. celldetective/gui/table_ops/_merge_groups.py +140 -0
  58. celldetective/gui/table_ops/_merge_one_hot.py +95 -0
  59. celldetective/gui/table_ops/_query_table.py +43 -0
  60. celldetective/gui/table_ops/_rename_col.py +44 -0
  61. celldetective/gui/thresholds_gui.py +382 -179
  62. celldetective/gui/viewers/__init__.py +0 -0
  63. celldetective/gui/viewers/base_viewer.py +700 -0
  64. celldetective/gui/viewers/channel_offset_viewer.py +331 -0
  65. celldetective/gui/viewers/contour_viewer.py +394 -0
  66. celldetective/gui/viewers/size_viewer.py +153 -0
  67. celldetective/gui/viewers/spot_detection_viewer.py +341 -0
  68. celldetective/gui/viewers/threshold_viewer.py +309 -0
  69. celldetective/gui/workers.py +304 -126
  70. celldetective/log_manager.py +92 -0
  71. celldetective/measure.py +1895 -1478
  72. celldetective/napari/__init__.py +0 -0
  73. celldetective/napari/utils.py +1025 -0
  74. celldetective/neighborhood.py +1914 -1448
  75. celldetective/preprocessing.py +1620 -1220
  76. celldetective/processes/__init__.py +0 -0
  77. celldetective/processes/background_correction.py +271 -0
  78. celldetective/processes/compute_neighborhood.py +894 -0
  79. celldetective/processes/detect_events.py +246 -0
  80. celldetective/processes/measure_cells.py +565 -0
  81. celldetective/processes/segment_cells.py +760 -0
  82. celldetective/processes/track_cells.py +435 -0
  83. celldetective/processes/train_segmentation_model.py +694 -0
  84. celldetective/processes/train_signal_model.py +265 -0
  85. celldetective/processes/unified_process.py +292 -0
  86. celldetective/regionprops/_regionprops.py +358 -317
  87. celldetective/relative_measurements.py +987 -710
  88. celldetective/scripts/measure_cells.py +313 -212
  89. celldetective/scripts/measure_relative.py +90 -46
  90. celldetective/scripts/segment_cells.py +165 -104
  91. celldetective/scripts/segment_cells_thresholds.py +96 -68
  92. celldetective/scripts/track_cells.py +198 -149
  93. celldetective/scripts/train_segmentation_model.py +324 -201
  94. celldetective/scripts/train_signal_model.py +87 -45
  95. celldetective/segmentation.py +844 -749
  96. celldetective/signals.py +3514 -2861
  97. celldetective/tracking.py +30 -15
  98. celldetective/utils/__init__.py +0 -0
  99. celldetective/utils/cellpose_utils/__init__.py +133 -0
  100. celldetective/utils/color_mappings.py +42 -0
  101. celldetective/utils/data_cleaning.py +630 -0
  102. celldetective/utils/data_loaders.py +450 -0
  103. celldetective/utils/dataset_helpers.py +207 -0
  104. celldetective/utils/downloaders.py +197 -0
  105. celldetective/utils/event_detection/__init__.py +8 -0
  106. celldetective/utils/experiment.py +1782 -0
  107. celldetective/utils/image_augmenters.py +308 -0
  108. celldetective/utils/image_cleaning.py +74 -0
  109. celldetective/utils/image_loaders.py +926 -0
  110. celldetective/utils/image_transforms.py +335 -0
  111. celldetective/utils/io.py +62 -0
  112. celldetective/utils/mask_cleaning.py +348 -0
  113. celldetective/utils/mask_transforms.py +5 -0
  114. celldetective/utils/masks.py +184 -0
  115. celldetective/utils/maths.py +351 -0
  116. celldetective/utils/model_getters.py +325 -0
  117. celldetective/utils/model_loaders.py +296 -0
  118. celldetective/utils/normalization.py +380 -0
  119. celldetective/utils/parsing.py +465 -0
  120. celldetective/utils/plots/__init__.py +0 -0
  121. celldetective/utils/plots/regression.py +53 -0
  122. celldetective/utils/resources.py +34 -0
  123. celldetective/utils/stardist_utils/__init__.py +104 -0
  124. celldetective/utils/stats.py +90 -0
  125. celldetective/utils/types.py +21 -0
  126. {celldetective-1.4.2.dist-info → celldetective-1.5.0b0.dist-info}/METADATA +1 -1
  127. celldetective-1.5.0b0.dist-info/RECORD +187 -0
  128. {celldetective-1.4.2.dist-info → celldetective-1.5.0b0.dist-info}/WHEEL +1 -1
  129. tests/gui/test_new_project.py +129 -117
  130. tests/gui/test_project.py +127 -79
  131. tests/test_filters.py +39 -15
  132. tests/test_notebooks.py +8 -0
  133. tests/test_tracking.py +232 -13
  134. tests/test_utils.py +123 -77
  135. celldetective/gui/base_components.py +0 -23
  136. celldetective/gui/layouts.py +0 -1602
  137. celldetective/gui/processes/compute_neighborhood.py +0 -594
  138. celldetective/gui/processes/measure_cells.py +0 -360
  139. celldetective/gui/processes/segment_cells.py +0 -499
  140. celldetective/gui/processes/track_cells.py +0 -303
  141. celldetective/gui/processes/train_segmentation_model.py +0 -270
  142. celldetective/gui/processes/train_signal_model.py +0 -108
  143. celldetective/gui/table_ops/merge_groups.py +0 -118
  144. celldetective/gui/viewers.py +0 -1354
  145. celldetective/io.py +0 -3663
  146. celldetective/utils.py +0 -3108
  147. celldetective-1.4.2.dist-info/RECORD +0 -123
  148. /celldetective/{gui/processes → processes}/downloader.py +0 -0
  149. {celldetective-1.4.2.dist-info → celldetective-1.5.0b0.dist-info}/entry_points.txt +0 -0
  150. {celldetective-1.4.2.dist-info → celldetective-1.5.0b0.dist-info}/licenses/LICENSE +0 -0
  151. {celldetective-1.4.2.dist-info → celldetective-1.5.0b0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,537 @@
1
+ import os
2
+
3
+ from PyQt5.QtCore import QSize, Qt, QThread, pyqtSignal
4
+ from PyQt5.QtGui import QIntValidator, QDoubleValidator
5
+ from PyQt5.QtWidgets import QGridLayout, QLabel, QComboBox, QButtonGroup, QRadioButton, QPushButton, QCheckBox, \
6
+ QLineEdit, QHBoxLayout, QMessageBox, QDialog
7
+ from fonticon_mdi6 import MDI6
8
+ from superqt import QLabeledRangeSlider, QLabeledSlider, QLabeledDoubleRangeSlider
9
+ from superqt.fonticon import icon
10
+ from tifffile import imread
11
+
12
+ from celldetective.gui.base.components import CelldetectiveProgressDialog
13
+ from celldetective.gui.base.styles import Styles
14
+ from celldetective.gui.gui_utils import ThresholdLineEdit, QuickSliderLayout
15
+ from celldetective.gui.layouts.operation_layout import OperationLayout
16
+ from celldetective.processes.background_correction import BackgroundCorrectionProcess
17
+ from celldetective.utils.parsing import _extract_channel_indices_from_config
18
+ from celldetective import get_logger
19
+
20
+ logger = get_logger(__name__)
21
+
22
+ class BackgroundModelFreeCorrectionLayout(QGridLayout, Styles):
23
+ """docstring for ClassName"""
24
+
25
+ def __init__(self, parent_window=None, *args):
26
+ super().__init__(*args)
27
+
28
+ self.parent_window = parent_window
29
+
30
+ if hasattr(self.parent_window.parent_window, "exp_config"):
31
+ self.attr_parent = self.parent_window.parent_window
32
+ else:
33
+ self.attr_parent = self.parent_window.parent_window.parent_window
34
+
35
+ self.channel_names = self.attr_parent.exp_channels
36
+
37
+ self.setContentsMargins(15, 15, 15, 15)
38
+ self.generate_widgets()
39
+ self.add_to_layout()
40
+
41
+ def generate_widgets(self):
42
+
43
+ self.channel_lbl = QLabel("Channel: ")
44
+ self.channels_cb = QComboBox()
45
+ self.channels_cb.addItems(self.channel_names)
46
+
47
+ self.acquistion_lbl = QLabel("Stack mode: ")
48
+ self.acq_mode_group = QButtonGroup()
49
+ self.timeseries_rb = QRadioButton("timeseries")
50
+ self.timeseries_rb.setChecked(True)
51
+ self.tiles_rb = QRadioButton("tiles")
52
+ self.acq_mode_group.addButton(self.timeseries_rb, 0)
53
+ self.acq_mode_group.addButton(self.tiles_rb, 1)
54
+
55
+ self.frame_range_slider = QLabeledRangeSlider(parent=None)
56
+
57
+ self.timeseries_rb.toggled.connect(self.activate_time_range)
58
+ self.tiles_rb.toggled.connect(self.activate_time_range)
59
+
60
+ self.thresh_lbl = QLabel("Threshold: ")
61
+ self.thresh_lbl.setToolTip(
62
+ "Threshold on the STD-filtered image.\nPixel values above the threshold are\nconsidered as non-background and are\nmasked prior to background estimation."
63
+ )
64
+ self.threshold_viewer_btn = QPushButton()
65
+ self.threshold_viewer_btn.setIcon(icon(MDI6.image_check, color="k"))
66
+ self.threshold_viewer_btn.setStyleSheet(self.button_select_all)
67
+ self.threshold_viewer_btn.clicked.connect(self.set_threshold_graphically)
68
+
69
+ self.background_viewer_btn = QPushButton()
70
+ self.background_viewer_btn.setIcon(icon(MDI6.image_check, color="k"))
71
+ self.background_viewer_btn.setStyleSheet(self.button_select_all)
72
+ self.background_viewer_btn.setToolTip("View reconstructed background.")
73
+
74
+ self.corrected_stack_viewer_btn = QPushButton("")
75
+ self.corrected_stack_viewer_btn.setStyleSheet(self.button_select_all)
76
+ self.corrected_stack_viewer_btn.setIcon(icon(MDI6.eye_outline, color="black"))
77
+ self.corrected_stack_viewer_btn.setToolTip("View corrected image")
78
+ self.corrected_stack_viewer_btn.clicked.connect(self.preview_correction)
79
+ self.corrected_stack_viewer_btn.setIconSize(QSize(20, 20))
80
+
81
+ self.add_correction_btn = QPushButton("Add correction")
82
+ self.add_correction_btn.setStyleSheet(self.button_style_sheet_2)
83
+ self.add_correction_btn.setIcon(icon(MDI6.plus, color="#1565c0"))
84
+ self.add_correction_btn.setToolTip("Add correction.")
85
+ self.add_correction_btn.setIconSize(QSize(25, 25))
86
+ self.add_correction_btn.clicked.connect(self.add_instructions_to_parent_list)
87
+
88
+ self.threshold_le = ThresholdLineEdit(
89
+ init_value=2,
90
+ connected_buttons=[
91
+ self.threshold_viewer_btn,
92
+ self.background_viewer_btn,
93
+ self.corrected_stack_viewer_btn,
94
+ self.add_correction_btn,
95
+ ],
96
+ )
97
+
98
+ self.well_slider = QLabeledSlider(parent=None)
99
+
100
+ self.background_viewer_btn.clicked.connect(self.estimate_bg)
101
+
102
+ self.regress_cb = QCheckBox("Optimize for each frame?")
103
+ self.regress_cb.toggled.connect(self.activate_coef_options)
104
+ self.regress_cb.setChecked(False)
105
+
106
+ self.coef_range_slider = QLabeledDoubleRangeSlider(parent=None)
107
+ self.coef_range_layout = QuickSliderLayout(
108
+ label="Coef. range: ",
109
+ slider=self.coef_range_slider,
110
+ slider_initial_value=(0.95, 1.05),
111
+ slider_range=(0.75, 1.25),
112
+ slider_tooltip="Coefficient range to increase or decrease the background intensity level...",
113
+ )
114
+
115
+ self.nbr_coefs_lbl = QLabel("Nbr of coefs: ")
116
+ self.nbr_coefs_lbl.setToolTip(
117
+ "Number of coefficients to be tested within range.\nThe more, the slower."
118
+ )
119
+
120
+ self.nbr_coef_le = QLineEdit()
121
+ self.nbr_coef_le.setText("100")
122
+ self.nbr_coef_le.setValidator(QIntValidator())
123
+ self.nbr_coef_le.setPlaceholderText("nbr of coefs")
124
+
125
+ self.coef_widgets = [
126
+ self.coef_range_layout.qlabel,
127
+ self.coef_range_slider,
128
+ self.nbr_coefs_lbl,
129
+ self.nbr_coef_le,
130
+ ]
131
+ for c in self.coef_widgets:
132
+ c.setEnabled(False)
133
+
134
+ self.interpolate_check = QCheckBox("interpolate NaNs")
135
+
136
+ def add_to_layout(self):
137
+
138
+ channel_layout = QHBoxLayout()
139
+ channel_layout.addWidget(self.channel_lbl, 25)
140
+ channel_layout.addWidget(self.channels_cb, 75)
141
+ self.addLayout(channel_layout, 0, 0, 1, 3)
142
+
143
+ acquisition_layout = QHBoxLayout()
144
+ acquisition_layout.addWidget(self.acquistion_lbl, 25)
145
+ acquisition_layout.addWidget(
146
+ self.timeseries_rb, 75 // 2, alignment=Qt.AlignCenter
147
+ )
148
+ acquisition_layout.addWidget(self.tiles_rb, 75 // 2, alignment=Qt.AlignCenter)
149
+ self.addLayout(acquisition_layout, 1, 0, 1, 3)
150
+
151
+ frame_selection_layout = QuickSliderLayout(
152
+ label="Time range: ",
153
+ slider=self.frame_range_slider,
154
+ slider_initial_value=(0, 5),
155
+ slider_range=(0, self.attr_parent.len_movie),
156
+ slider_tooltip="frame [#]",
157
+ decimal_option=False,
158
+ )
159
+ frame_selection_layout.qlabel.setToolTip(
160
+ "Frame range for which the background\nis most likely to be observed."
161
+ )
162
+ self.time_range_options = [
163
+ self.frame_range_slider,
164
+ frame_selection_layout.qlabel,
165
+ ]
166
+ self.addLayout(frame_selection_layout, 2, 0, 1, 3)
167
+
168
+ threshold_layout = QHBoxLayout()
169
+ threshold_layout.addWidget(self.thresh_lbl, 25)
170
+ subthreshold_layout = QHBoxLayout()
171
+ subthreshold_layout.addWidget(self.threshold_le, 95)
172
+ subthreshold_layout.addWidget(self.threshold_viewer_btn, 5)
173
+ threshold_layout.addLayout(subthreshold_layout, 75)
174
+ self.addLayout(threshold_layout, 3, 0, 1, 3)
175
+
176
+ background_layout = QuickSliderLayout(
177
+ label="QC for well: ",
178
+ slider=self.well_slider,
179
+ slider_initial_value=1,
180
+ slider_range=(1, len(self.attr_parent.wells)),
181
+ slider_tooltip="well [#]",
182
+ decimal_option=False,
183
+ layout_ratio=(0.25, 0.70),
184
+ )
185
+ background_layout.addWidget(self.background_viewer_btn, 5)
186
+ self.addLayout(background_layout, 4, 0, 1, 3)
187
+
188
+ self.addWidget(self.regress_cb, 5, 0, 1, 3)
189
+
190
+ self.addLayout(self.coef_range_layout, 6, 0, 1, 3)
191
+
192
+ coef_nbr_layout = QHBoxLayout()
193
+ coef_nbr_layout.addWidget(self.nbr_coefs_lbl, 25)
194
+ coef_nbr_layout.addWidget(self.nbr_coef_le, 75)
195
+ self.addLayout(coef_nbr_layout, 7, 0, 1, 3)
196
+
197
+ offset_layout = QHBoxLayout()
198
+ offset_layout.addWidget(QLabel("Offset: "), 25)
199
+ self.camera_offset_le = QLineEdit("0")
200
+ self.camera_offset_le.setPlaceholderText("camera black level")
201
+ self.camera_offset_le.setValidator(QDoubleValidator())
202
+ offset_layout.addWidget(self.camera_offset_le, 75)
203
+ self.addLayout(offset_layout, 8, 0, 1, 3)
204
+
205
+ self.operation_layout = OperationLayout()
206
+ self.addLayout(self.operation_layout, 9, 0, 1, 3)
207
+
208
+ self.addWidget(self.interpolate_check, 10, 0, 1, 1)
209
+
210
+ correction_layout = QHBoxLayout()
211
+ correction_layout.addWidget(self.add_correction_btn, 95)
212
+ correction_layout.addWidget(self.corrected_stack_viewer_btn, 5)
213
+ self.addLayout(correction_layout, 11, 0, 1, 3)
214
+
215
+ # verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
216
+ # self.addItem(verticalSpacer, 5, 0, 1, 3)
217
+
218
+ def add_instructions_to_parent_list(self):
219
+
220
+ self.generate_instructions()
221
+ self.parent_window.protocols.append(self.instructions)
222
+ correction_description = ""
223
+ for index, (key, value) in enumerate(self.instructions.items()):
224
+ if index > 0:
225
+ correction_description += ", "
226
+ correction_description += str(key) + " : " + str(value)
227
+ self.parent_window.protocol_list.addItem(correction_description)
228
+
229
+ def generate_instructions(self):
230
+
231
+ if self.timeseries_rb.isChecked():
232
+ mode = "timeseries"
233
+ elif self.tiles_rb.isChecked():
234
+ mode = "tiles"
235
+
236
+ if self.regress_cb.isChecked():
237
+ optimize_option = True
238
+ opt_coef_range = self.coef_range_slider.value()
239
+ opt_coef_nbr = int(self.nbr_coef_le.text())
240
+ else:
241
+ optimize_option = False
242
+ opt_coef_range = None
243
+ opt_coef_nbr = None
244
+
245
+ if self.operation_layout.subtract_btn.isChecked():
246
+ operation = "subtract"
247
+ else:
248
+ operation = "divide"
249
+ clip = None
250
+
251
+ if (
252
+ self.operation_layout.clip_btn.isChecked()
253
+ and self.operation_layout.subtract_btn.isChecked()
254
+ ):
255
+ clip = True
256
+ else:
257
+ clip = False
258
+
259
+ if self.camera_offset_le.text() == "":
260
+ offset = None
261
+ else:
262
+ offset = float(self.camera_offset_le.text().replace(",", "."))
263
+
264
+ self.instructions = {
265
+ "target_channel": self.channels_cb.currentText(),
266
+ "correction_type": "model-free",
267
+ "threshold_on_std": self.threshold_le.get_threshold(),
268
+ "frame_range": self.frame_range_slider.value(),
269
+ "mode": mode,
270
+ "optimize_option": optimize_option,
271
+ "opt_coef_range": opt_coef_range,
272
+ "opt_coef_nbr": opt_coef_nbr,
273
+ "operation": operation,
274
+ "clip": clip,
275
+ "offset": offset,
276
+ "fix_nan": self.interpolate_check.isChecked(),
277
+ }
278
+
279
+ def set_target_channel(self):
280
+
281
+ channel_indices = _extract_channel_indices_from_config(
282
+ self.attr_parent.exp_config, [self.channels_cb.currentText()]
283
+ )
284
+ self.target_channel = channel_indices[0]
285
+
286
+ def set_threshold_graphically(self):
287
+ from celldetective.gui.viewers.threshold_viewer import (
288
+ ThresholdedStackVisualizer,
289
+ )
290
+
291
+ self.attr_parent.locate_image()
292
+ self.set_target_channel()
293
+ thresh = self.threshold_le.get_threshold()
294
+
295
+ if self.attr_parent.current_stack is not None and thresh is not None:
296
+ self.viewer = ThresholdedStackVisualizer(
297
+ initial_threshold=thresh,
298
+ parent_le=self.threshold_le,
299
+ preprocessing=[["gauss", 2], ["std", 4]],
300
+ stack_path=self.attr_parent.current_stack,
301
+ n_channels=len(self.channel_names),
302
+ target_channel=self.target_channel,
303
+ window_title="Set the exclusion threshold",
304
+ )
305
+ self.viewer.show()
306
+
307
+ def preview_correction(self):
308
+ from celldetective.gui.viewers.base_viewer import StackVisualizer
309
+
310
+ if (
311
+ self.attr_parent.well_list.isMultipleSelection()
312
+ or not self.attr_parent.well_list.isAnySelected()
313
+ or self.attr_parent.position_list.isMultipleSelection()
314
+ or not self.attr_parent.position_list.isAnySelected()
315
+ ):
316
+ msgBox = QMessageBox()
317
+ msgBox.setIcon(QMessageBox.Warning)
318
+ msgBox.setText("Please select a single position...")
319
+ msgBox.setWindowTitle("Warning")
320
+ msgBox.setStandardButtons(QMessageBox.Ok)
321
+ returnValue = msgBox.exec()
322
+ if returnValue == QMessageBox.Ok:
323
+ return None
324
+
325
+ if self.timeseries_rb.isChecked():
326
+ mode = "timeseries"
327
+ elif self.tiles_rb.isChecked():
328
+ mode = "tiles"
329
+ else:
330
+ mode = "tiles"
331
+
332
+ if self.regress_cb.isChecked():
333
+ optimize_option = True
334
+ opt_coef_range = self.coef_range_slider.value()
335
+ opt_coef_nbr = int(self.nbr_coef_le.text())
336
+ else:
337
+ optimize_option = False
338
+ opt_coef_range = None
339
+ opt_coef_nbr = None
340
+
341
+ if self.operation_layout.subtract_btn.isChecked():
342
+ operation = "subtract"
343
+ else:
344
+ operation = "divide"
345
+ clip = None
346
+
347
+ if (
348
+ self.operation_layout.clip_btn.isChecked()
349
+ and self.operation_layout.subtract_btn.isChecked()
350
+ ):
351
+ clip = True
352
+ else:
353
+ clip = False
354
+
355
+ process_args = {
356
+ "exp_dir": self.attr_parent.exp_dir,
357
+ "well_option": self.attr_parent.well_list.getSelectedIndices(),
358
+ "position_option": self.attr_parent.position_list.getSelectedIndices(),
359
+ "target_channel": self.channels_cb.currentText(),
360
+ "mode": mode,
361
+ "threshold_on_std": self.threshold_le.get_threshold(),
362
+ "frame_range": self.frame_range_slider.value(),
363
+ "optimize_option": optimize_option,
364
+ "opt_coef_range": opt_coef_range,
365
+ "opt_coef_nbr": opt_coef_nbr,
366
+ "operation": operation,
367
+ "clip": clip,
368
+ "fix_nan": self.interpolate_check.isChecked(),
369
+ "activation_protocol": [["gauss", 2], ["std", 4]],
370
+ "correction_type": "model-free",
371
+ }
372
+ from celldetective.gui.workers import ProgressWindow
373
+
374
+ self.job = ProgressWindow(
375
+ BackgroundCorrectionProcess,
376
+ parent_window=self,
377
+ title="Background Correction",
378
+ position_info=False,
379
+ process_args=process_args,
380
+ )
381
+ result = self.job.exec_()
382
+
383
+ if result == QDialog.Accepted:
384
+ temp_path = os.path.join(
385
+ self.attr_parent.exp_dir, "temp_corrected_stack.tif"
386
+ )
387
+ if os.path.exists(temp_path):
388
+ corrected_stack = imread(temp_path)
389
+ os.remove(temp_path)
390
+
391
+ self.viewer = StackVisualizer(
392
+ stack=corrected_stack,
393
+ window_title="Corrected channel",
394
+ frame_slider=True,
395
+ contrast_slider=True,
396
+ target_channel=self.channels_cb.currentIndex(),
397
+ )
398
+ self.viewer.show()
399
+ else:
400
+ print("Corrected stack could not be generated... No stack available...")
401
+ else:
402
+ print("Background correction cancelled.")
403
+
404
+ def activate_time_range(self):
405
+
406
+ if self.timeseries_rb.isChecked():
407
+ for wg in self.time_range_options:
408
+ wg.setEnabled(True)
409
+ elif self.tiles_rb.isChecked():
410
+ for wg in self.time_range_options:
411
+ wg.setEnabled(False)
412
+
413
+ def activate_coef_options(self):
414
+
415
+ if self.regress_cb.isChecked():
416
+ for c in self.coef_widgets:
417
+ c.setEnabled(True)
418
+ else:
419
+ for c in self.coef_widgets:
420
+ c.setEnabled(False)
421
+
422
+ def estimate_bg(self):
423
+
424
+ if self.timeseries_rb.isChecked():
425
+ mode = "timeseries"
426
+ elif self.tiles_rb.isChecked():
427
+ mode = "tiles"
428
+ else:
429
+ mode = "tiles"
430
+
431
+ # Create progress dialog
432
+ window_title = "Background reconstruction"
433
+ self.bg_progress = CelldetectiveProgressDialog(
434
+ "Loading libraries...", "Cancel", 0, 100, None, window_title=window_title
435
+ )
436
+
437
+ self.bg_worker = BackgroundEstimatorThread(
438
+ self.attr_parent.exp_dir,
439
+ self.well_slider.value() - 1,
440
+ self.frame_range_slider.value(),
441
+ self.channels_cb.currentText(),
442
+ self.threshold_le.get_threshold(),
443
+ mode,
444
+ )
445
+ from celldetective.gui.viewers.base_viewer import StackVisualizer
446
+
447
+ self.bg_worker.progress.connect(self.bg_progress.setValue)
448
+ self.bg_worker.status_update.connect(self.bg_progress.setLabelText)
449
+
450
+ def on_finished(bg):
451
+ self.bg_progress.blockSignals(True)
452
+ self.bg_progress.close()
453
+ if self.bg_worker._is_cancelled:
454
+ logger.info("Background estimation cancelled.")
455
+ return
456
+
457
+ if bg and len(bg) > 0:
458
+ bg_img = bg[0]["bg"]
459
+ if len(bg_img) > 0:
460
+ self.viewer = StackVisualizer(
461
+ stack=[bg_img],
462
+ window_title="Reconstructed background",
463
+ frame_slider=False,
464
+ )
465
+ self.viewer.show()
466
+ else:
467
+ QMessageBox.warning(
468
+ None, "Warning", "Background estimation returned empty image."
469
+ )
470
+ elif bg is None:
471
+ QMessageBox.critical(None, "Error", "Background estimation failed.")
472
+
473
+ self.bg_worker.finished_with_result.connect(on_finished)
474
+ self.bg_progress.canceled.connect(self.bg_worker.stop)
475
+
476
+ self.bg_worker.start()
477
+
478
+
479
+ class BackgroundEstimatorThread(QThread):
480
+ progress = pyqtSignal(int)
481
+ finished_with_result = pyqtSignal(object)
482
+ status_update = pyqtSignal(str)
483
+
484
+ def __init__(self, exp_dir, well_idx, frame_range, channel, threshold, mode):
485
+ super().__init__()
486
+ self.exp_dir = exp_dir
487
+ self.well_idx = well_idx
488
+ self.frame_range = frame_range
489
+ self.channel = channel
490
+ self.threshold = threshold
491
+ self.mode = mode
492
+ self._is_cancelled = False
493
+
494
+ def stop(self):
495
+ self._is_cancelled = True
496
+
497
+ def run(self):
498
+ from celldetective.preprocessing import estimate_background_per_condition
499
+
500
+ self.first_update = True
501
+
502
+ def callback(**kwargs):
503
+ if self._is_cancelled:
504
+ return False
505
+
506
+ if self.first_update:
507
+ self.status_update.emit("Estimating background...")
508
+ self.first_update = False
509
+
510
+ if kwargs.get("level") == "position":
511
+
512
+ iter_ = kwargs.get("iter", 0)
513
+ total = kwargs.get("total", 1)
514
+ # Avoid division by zero
515
+ if total > 0:
516
+ p = int((iter_ / total) * 100)
517
+ self.progress.emit(p)
518
+ return True
519
+
520
+ try:
521
+ bg = estimate_background_per_condition(
522
+ self.exp_dir,
523
+ well_option=self.well_idx,
524
+ frame_range=self.frame_range,
525
+ target_channel=self.channel,
526
+ show_progress_per_pos=False,
527
+ threshold_on_std=self.threshold,
528
+ mode=self.mode,
529
+ progress_callback=callback,
530
+ )
531
+ if not self._is_cancelled:
532
+ self.finished_with_result.emit(bg)
533
+ else:
534
+ self.finished_with_result.emit(None)
535
+ except Exception as e:
536
+ print(f"Error in background estimation thread: {e}")
537
+ self.finished_with_result.emit(None)
@@ -0,0 +1,134 @@
1
+ from PyQt5.QtCore import QSize
2
+ from PyQt5.QtWidgets import QVBoxLayout, QLabel, QComboBox, QPushButton, QHBoxLayout
3
+ from fonticon_mdi6 import MDI6
4
+ from superqt.fonticon import icon
5
+
6
+ from celldetective.gui.base.styles import Styles
7
+ from celldetective.gui.gui_utils import ThresholdLineEdit
8
+ from celldetective.utils.parsing import _extract_channel_indices_from_config
9
+ from celldetective import get_logger
10
+
11
+ logger = get_logger(__name__)
12
+
13
+ class ChannelOffsetOptionsLayout(QVBoxLayout, Styles):
14
+
15
+ def __init__(self, parent_window=None, *args, **kwargs):
16
+
17
+ super().__init__(*args, **kwargs)
18
+
19
+ self.parent_window = parent_window
20
+ if hasattr(self.parent_window.parent_window, "exp_config"):
21
+ self.attr_parent = self.parent_window.parent_window
22
+ else:
23
+ self.attr_parent = self.parent_window.parent_window.parent_window
24
+
25
+ self.channel_names = self.attr_parent.exp_channels
26
+
27
+ self.setContentsMargins(15, 15, 15, 15)
28
+ self.generate_widgets()
29
+ self.add_to_layout()
30
+
31
+ def generate_widgets(self):
32
+
33
+ self.channel_lbl = QLabel("Channel: ")
34
+ self.channels_cb = QComboBox()
35
+ self.channels_cb.addItems(self.channel_names)
36
+
37
+ self.shift_lbl = QLabel("Shift: ")
38
+ self.shift_h_lbl = QLabel("(h): ")
39
+ self.shift_v_lbl = QLabel("(v): ")
40
+
41
+ self.set_shift_btn = QPushButton()
42
+ self.set_shift_btn.setIcon(icon(MDI6.image_check, color="k"))
43
+ self.set_shift_btn.setStyleSheet(self.button_select_all)
44
+ self.set_shift_btn.setToolTip("Set the channel shift.")
45
+ self.set_shift_btn.clicked.connect(self.open_offset_viewer)
46
+
47
+ self.add_correction_btn = QPushButton("Add correction")
48
+ self.add_correction_btn.setStyleSheet(self.button_style_sheet_2)
49
+ self.add_correction_btn.setIcon(icon(MDI6.plus, color="#1565c0"))
50
+ self.add_correction_btn.setToolTip("Add correction.")
51
+ self.add_correction_btn.setIconSize(QSize(25, 25))
52
+ self.add_correction_btn.clicked.connect(self.add_instructions_to_parent_list)
53
+
54
+ self.vertical_shift_le = ThresholdLineEdit(
55
+ init_value=0,
56
+ connected_buttons=[self.add_correction_btn],
57
+ placeholder="vertical shift [pixels]",
58
+ value_type="float",
59
+ )
60
+ self.horizontal_shift_le = ThresholdLineEdit(
61
+ init_value=0,
62
+ connected_buttons=[self.add_correction_btn],
63
+ placeholder="vertical shift [pixels]",
64
+ value_type="float",
65
+ )
66
+
67
+ def add_to_layout(self):
68
+
69
+ channel_ch_hbox = QHBoxLayout()
70
+ channel_ch_hbox.addWidget(self.channel_lbl, 25)
71
+ channel_ch_hbox.addWidget(self.channels_cb, 75)
72
+ self.addLayout(channel_ch_hbox)
73
+
74
+ shift_hbox = QHBoxLayout()
75
+ shift_hbox.addWidget(self.shift_lbl, 25)
76
+
77
+ shift_subhbox = QHBoxLayout()
78
+ shift_subhbox.addWidget(self.shift_h_lbl, 10)
79
+ shift_subhbox.addWidget(self.horizontal_shift_le, 75 // 2)
80
+ shift_subhbox.addWidget(self.shift_v_lbl, 10)
81
+ shift_subhbox.addWidget(self.vertical_shift_le, 75 // 2)
82
+ shift_subhbox.addWidget(self.set_shift_btn, 5)
83
+
84
+ shift_hbox.addLayout(shift_subhbox, 75)
85
+ self.addLayout(shift_hbox)
86
+
87
+ btn_hbox = QHBoxLayout()
88
+ btn_hbox.addWidget(self.add_correction_btn, 95)
89
+ self.addLayout(btn_hbox)
90
+
91
+ def add_instructions_to_parent_list(self):
92
+
93
+ self.generate_instructions()
94
+ self.parent_window.protocol_layout.protocols.append(self.instructions)
95
+ correction_description = ""
96
+ for index, (key, value) in enumerate(self.instructions.items()):
97
+ if index > 0:
98
+ correction_description += ", "
99
+ correction_description += str(key) + " : " + str(value)
100
+ self.parent_window.protocol_layout.protocol_list.addItem(correction_description)
101
+
102
+ def generate_instructions(self):
103
+
104
+ self.instructions = {
105
+ "correction_type": "offset",
106
+ "target_channel": self.channels_cb.currentText(),
107
+ "correction_horizontal": self.horizontal_shift_le.get_threshold(),
108
+ "correction_vertical": self.vertical_shift_le.get_threshold(),
109
+ }
110
+
111
+ def set_target_channel(self):
112
+
113
+ channel_indices = _extract_channel_indices_from_config(
114
+ self.attr_parent.exp_config, [self.channels_cb.currentText()]
115
+ )
116
+ self.target_channel = channel_indices[0]
117
+
118
+ def open_offset_viewer(self):
119
+ from celldetective.gui.viewers.channel_offset_viewer import ChannelOffsetViewer
120
+
121
+ self.attr_parent.locate_image()
122
+ self.set_target_channel()
123
+
124
+ if self.attr_parent.current_stack is not None:
125
+ self.viewer = ChannelOffsetViewer(
126
+ parent_window=self,
127
+ stack_path=self.attr_parent.current_stack,
128
+ channel_names=self.attr_parent.exp_channels,
129
+ n_channels=len(self.channel_names),
130
+ channel_cb=True,
131
+ target_channel=self.target_channel,
132
+ window_title="offset viewer",
133
+ )
134
+ self.viewer.show()