celldetective 1.4.2__py3-none-any.whl → 1.5.0b1__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 (152) 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 +403 -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/downloader.py +137 -0
  81. celldetective/processes/measure_cells.py +565 -0
  82. celldetective/processes/segment_cells.py +760 -0
  83. celldetective/processes/track_cells.py +435 -0
  84. celldetective/processes/train_segmentation_model.py +694 -0
  85. celldetective/processes/train_signal_model.py +265 -0
  86. celldetective/processes/unified_process.py +292 -0
  87. celldetective/regionprops/_regionprops.py +358 -317
  88. celldetective/relative_measurements.py +987 -710
  89. celldetective/scripts/measure_cells.py +313 -212
  90. celldetective/scripts/measure_relative.py +90 -46
  91. celldetective/scripts/segment_cells.py +165 -104
  92. celldetective/scripts/segment_cells_thresholds.py +96 -68
  93. celldetective/scripts/track_cells.py +198 -149
  94. celldetective/scripts/train_segmentation_model.py +324 -201
  95. celldetective/scripts/train_signal_model.py +87 -45
  96. celldetective/segmentation.py +844 -749
  97. celldetective/signals.py +3514 -2861
  98. celldetective/tracking.py +30 -15
  99. celldetective/utils/__init__.py +0 -0
  100. celldetective/utils/cellpose_utils/__init__.py +133 -0
  101. celldetective/utils/color_mappings.py +42 -0
  102. celldetective/utils/data_cleaning.py +630 -0
  103. celldetective/utils/data_loaders.py +450 -0
  104. celldetective/utils/dataset_helpers.py +207 -0
  105. celldetective/utils/downloaders.py +235 -0
  106. celldetective/utils/event_detection/__init__.py +8 -0
  107. celldetective/utils/experiment.py +1782 -0
  108. celldetective/utils/image_augmenters.py +308 -0
  109. celldetective/utils/image_cleaning.py +74 -0
  110. celldetective/utils/image_loaders.py +926 -0
  111. celldetective/utils/image_transforms.py +335 -0
  112. celldetective/utils/io.py +62 -0
  113. celldetective/utils/mask_cleaning.py +348 -0
  114. celldetective/utils/mask_transforms.py +5 -0
  115. celldetective/utils/masks.py +184 -0
  116. celldetective/utils/maths.py +351 -0
  117. celldetective/utils/model_getters.py +325 -0
  118. celldetective/utils/model_loaders.py +296 -0
  119. celldetective/utils/normalization.py +380 -0
  120. celldetective/utils/parsing.py +465 -0
  121. celldetective/utils/plots/__init__.py +0 -0
  122. celldetective/utils/plots/regression.py +53 -0
  123. celldetective/utils/resources.py +34 -0
  124. celldetective/utils/stardist_utils/__init__.py +104 -0
  125. celldetective/utils/stats.py +90 -0
  126. celldetective/utils/types.py +21 -0
  127. {celldetective-1.4.2.dist-info → celldetective-1.5.0b1.dist-info}/METADATA +1 -1
  128. celldetective-1.5.0b1.dist-info/RECORD +187 -0
  129. {celldetective-1.4.2.dist-info → celldetective-1.5.0b1.dist-info}/WHEEL +1 -1
  130. tests/gui/test_new_project.py +129 -117
  131. tests/gui/test_project.py +127 -79
  132. tests/test_filters.py +39 -15
  133. tests/test_notebooks.py +8 -0
  134. tests/test_tracking.py +232 -13
  135. tests/test_utils.py +123 -77
  136. celldetective/gui/base_components.py +0 -23
  137. celldetective/gui/layouts.py +0 -1602
  138. celldetective/gui/processes/compute_neighborhood.py +0 -594
  139. celldetective/gui/processes/downloader.py +0 -111
  140. celldetective/gui/processes/measure_cells.py +0 -360
  141. celldetective/gui/processes/segment_cells.py +0 -499
  142. celldetective/gui/processes/track_cells.py +0 -303
  143. celldetective/gui/processes/train_segmentation_model.py +0 -270
  144. celldetective/gui/processes/train_signal_model.py +0 -108
  145. celldetective/gui/table_ops/merge_groups.py +0 -118
  146. celldetective/gui/viewers.py +0 -1354
  147. celldetective/io.py +0 -3663
  148. celldetective/utils.py +0 -3108
  149. celldetective-1.4.2.dist-info/RECORD +0 -123
  150. {celldetective-1.4.2.dist-info → celldetective-1.5.0b1.dist-info}/entry_points.txt +0 -0
  151. {celldetective-1.4.2.dist-info → celldetective-1.5.0b1.dist-info}/licenses/LICENSE +0 -0
  152. {celldetective-1.4.2.dist-info → celldetective-1.5.0b1.dist-info}/top_level.txt +0 -0
@@ -1,1354 +0,0 @@
1
- from celldetective.io import auto_load_number_of_frames, load_frames
2
- from celldetective.filters import *
3
- from celldetective.segmentation import filter_image, threshold_image
4
- from celldetective.measure import contour_of_instance_segmentation, extract_blobs_in_image
5
- from celldetective.utils import _get_img_num_per_channel, estimate_unreliable_edge, is_integer_array
6
- from tifffile import imread
7
- import matplotlib.pyplot as plt
8
- from pathlib import Path
9
- from natsort import natsorted
10
- from glob import glob
11
- import os
12
-
13
- from PyQt5.QtWidgets import QHBoxLayout, QMessageBox, QPushButton, QLabel, QComboBox, QLineEdit, QListWidget, QShortcut
14
- from PyQt5.QtCore import Qt, QSize
15
- from PyQt5.QtGui import QKeySequence, QDoubleValidator
16
- from celldetective.gui.gui_utils import FigureCanvas, QuickSliderLayout, QHSeperationLine, ThresholdLineEdit, PreprocessingLayout2
17
- from celldetective.gui import CelldetectiveWidget
18
- from superqt import QLabeledDoubleSlider, QLabeledSlider, QLabeledDoubleRangeSlider
19
- from superqt.fonticon import icon
20
- from fonticon_mdi6 import MDI6
21
- from matplotlib_scalebar.scalebar import ScaleBar
22
- import gc
23
- from scipy.ndimage import shift
24
-
25
- class StackVisualizer(CelldetectiveWidget):
26
-
27
- """
28
- A widget for visualizing image stacks with interactive sliders and channel selection.
29
-
30
- Parameters:
31
- - stack (numpy.ndarray or None): The stack of images.
32
- - stack_path (str or None): The path to the stack of images if provided as a file.
33
- - frame_slider (bool): Enable frame navigation slider.
34
- - contrast_slider (bool): Enable contrast adjustment slider.
35
- - channel_cb (bool): Enable channel selection dropdown.
36
- - channel_names (list or None): Names of the channels if `channel_cb` is True.
37
- - n_channels (int): Number of channels.
38
- - target_channel (int): Index of the target channel.
39
- - window_title (str): Title of the window.
40
- - PxToUm (float or None): Pixel to micrometer conversion factor.
41
- - background_color (str): Background color of the widget.
42
- - imshow_kwargs (dict): Additional keyword arguments for imshow function.
43
-
44
- Methods:
45
- - show(): Display the widget.
46
- - load_stack(): Load the stack of images.
47
- - locate_image_virtual(): Locate the stack of images if provided as a file.
48
- - generate_figure_canvas(): Generate the figure canvas for displaying images.
49
- - generate_channel_cb(): Generate the channel dropdown if enabled.
50
- - generate_contrast_slider(): Generate the contrast slider if enabled.
51
- - generate_frame_slider(): Generate the frame slider if enabled.
52
- - set_target_channel(value): Set the target channel.
53
- - change_contrast(value): Change contrast based on slider value.
54
- - set_channel_index(value): Set the channel index based on dropdown value.
55
- - change_frame(value): Change the displayed frame based on slider value.
56
- - closeEvent(event): Event handler for closing the widget.
57
-
58
- Notes:
59
- - This class provides a convenient interface for visualizing image stacks with frame navigation,
60
- contrast adjustment, and channel selection functionalities.
61
- """
62
-
63
- def __init__(self, stack=None, stack_path=None, frame_slider=True, contrast_slider=True, channel_cb=False, channel_names=None, n_channels=1, target_channel=0, window_title='View', PxToUm=None, background_color='transparent',imshow_kwargs={}):
64
- super().__init__()
65
-
66
- #self.setWindowTitle(window_title)
67
- self.window_title = window_title
68
-
69
- self.stack = stack
70
- self.stack_path = stack_path
71
- self.create_frame_slider = frame_slider
72
- self.background_color = background_color
73
- self.create_contrast_slider = contrast_slider
74
- self.create_channel_cb = channel_cb
75
- self.n_channels = n_channels
76
- self.channel_names = channel_names
77
- self.target_channel = target_channel
78
- self.imshow_kwargs = imshow_kwargs
79
- self.PxToUm = PxToUm
80
- self.init_contrast = False
81
- self.channel_trigger = False
82
-
83
- self.load_stack() # need to get stack, frame etc
84
- self.generate_figure_canvas()
85
- if self.create_channel_cb:
86
- self.generate_channel_cb()
87
- if self.create_contrast_slider:
88
- self.generate_contrast_slider()
89
- if self.create_frame_slider:
90
- self.generate_frame_slider()
91
-
92
- self.canvas.layout.setContentsMargins(15,15,15,30)
93
- #center_window(self)
94
-
95
- def show(self):
96
- # Display the widget
97
- self.canvas.show()
98
-
99
- def load_stack(self):
100
- # Load the stack of images
101
- if self.stack is not None:
102
-
103
- if isinstance(self.stack, list):
104
- self.stack = np.array(self.stack)
105
-
106
- if self.stack.ndim==3:
107
- print('No channel axis found...')
108
- self.stack = self.stack[:,:,:,np.newaxis]
109
- self.target_channel = 0
110
-
111
- self.mode = 'direct'
112
- self.stack_length = len(self.stack)
113
- self.mid_time = self.stack_length // 2
114
- self.init_frame = self.stack[self.mid_time,:,:,self.target_channel]
115
- self.last_frame = self.stack[-1,:,:,self.target_channel]
116
- else:
117
- self.mode = 'virtual'
118
- assert isinstance(self.stack_path, str)
119
- assert self.stack_path.endswith('.tif')
120
- self.locate_image_virtual()
121
-
122
- def locate_image_virtual(self):
123
- # Locate the stack of images if provided as a file
124
-
125
- self.stack_length = auto_load_number_of_frames(self.stack_path)
126
- self.mid_time = self.stack_length // 2
127
- self.img_num_per_channel = _get_img_num_per_channel(np.arange(self.n_channels), self.stack_length, self.n_channels)
128
-
129
- self.init_frame = load_frames(self.img_num_per_channel[self.target_channel, self.mid_time],
130
- self.stack_path,
131
- normalize_input=False).astype(float)[:,:,0]
132
- self.last_frame = load_frames(self.img_num_per_channel[self.target_channel, self.stack_length-1],
133
- self.stack_path,
134
- normalize_input=False).astype(float)[:,:,0]
135
-
136
-
137
- def generate_figure_canvas(self):
138
- # Generate the figure canvas for displaying images
139
-
140
- self.fig, self.ax = plt.subplots(figsize=(5,5),tight_layout=True) #figsize=(5, 5)
141
- self.canvas = FigureCanvas(self.fig, title=self.window_title, interactive=True)
142
- self.ax.clear()
143
- self.im = self.ax.imshow(self.init_frame, cmap='gray', interpolation='none', zorder=0, **self.imshow_kwargs)
144
- if self.PxToUm is not None:
145
- scalebar = ScaleBar(self.PxToUm,
146
- "um",
147
- length_fraction=0.25,
148
- location='upper right',
149
- border_pad=0.4,
150
- box_alpha=0.95,
151
- color='white',
152
- box_color='black',
153
- )
154
- if self.PxToUm==1:
155
- scalebar = ScaleBar(1,
156
- "px",
157
- dimension="pixel-length",
158
- length_fraction=0.25,
159
- location='upper right',
160
- border_pad=0.4,
161
- box_alpha=0.95,
162
- color='white',
163
- box_color='black',
164
- )
165
- self.ax.add_artist(scalebar)
166
- self.ax.set_xticks([])
167
- self.ax.set_yticks([])
168
- self.fig.set_facecolor('none') # or 'None'
169
- self.fig.canvas.setStyleSheet(f"background-color: {self.background_color};")
170
- self.canvas.canvas.draw()
171
-
172
- def generate_channel_cb(self):
173
- # Generate the channel dropdown if enabled
174
-
175
- assert self.channel_names is not None
176
- assert len(self.channel_names)==self.n_channels
177
-
178
- channel_layout = QHBoxLayout()
179
- channel_layout.setContentsMargins(15,0,15,0)
180
- channel_layout.addWidget(QLabel('Channel: '), 25)
181
-
182
- self.channels_cb = QComboBox()
183
- self.channels_cb.addItems(self.channel_names)
184
- self.channels_cb.currentIndexChanged.connect(self.set_channel_index)
185
- channel_layout.addWidget(self.channels_cb, 75)
186
- self.canvas.layout.addLayout(channel_layout)
187
-
188
- def set_contrast_decimals(self):
189
- if is_integer_array(self.init_frame):
190
- self.contrast_slider.setDecimals(0)
191
- self.contrast_slider.setSingleStep(1.0)
192
- self.contrast_slider.setTickInterval(1.0)
193
- else:
194
- self.contrast_slider.setDecimals(3)
195
- self.contrast_slider.setSingleStep(1.0E-03)
196
- self.contrast_slider.setTickInterval(1.0E-03)
197
-
198
- def generate_contrast_slider(self):
199
- # Generate the contrast slider if enabled
200
-
201
- self.contrast_slider = QLabeledDoubleRangeSlider()
202
- contrast_layout = QuickSliderLayout(
203
- label='Contrast: ',
204
- slider=self.contrast_slider,
205
- slider_initial_value=[np.nanpercentile(self.init_frame, 0.1),np.nanpercentile(self.init_frame, 99.99)],
206
- slider_range=(np.nanmin(self.init_frame),np.nanmax(self.init_frame)),
207
- decimal_option=True,
208
- precision=2,
209
- )
210
- self.set_contrast_decimals()
211
-
212
- contrast_layout.setContentsMargins(15,0,15,0)
213
- self.im.set_clim(vmin=np.nanpercentile(self.init_frame, 0.1),vmax=np.nanpercentile(self.init_frame, 99.99))
214
- self.contrast_slider.valueChanged.connect(self.change_contrast)
215
- self.canvas.layout.addLayout(contrast_layout)
216
-
217
- def generate_frame_slider(self):
218
- # Generate the frame slider if enabled
219
-
220
- self.frame_slider = QLabeledSlider()
221
- frame_layout = QuickSliderLayout(
222
- label='Frame: ',
223
- slider=self.frame_slider,
224
- slider_initial_value=int(self.mid_time),
225
- slider_range=(0,self.stack_length-1),
226
- decimal_option=False,
227
- )
228
- frame_layout.setContentsMargins(15,0,15,0)
229
- self.frame_slider.valueChanged.connect(self.change_frame)
230
- self.canvas.layout.addLayout(frame_layout)
231
-
232
- def set_target_channel(self, value):
233
- # Set the target channel
234
-
235
- self.target_channel = value
236
- self.change_frame(self.frame_slider.value())
237
-
238
- def change_contrast(self, value):
239
- # Change contrast based on slider value
240
-
241
- vmin = value[0]
242
- vmax = value[1]
243
- self.im.set_clim(vmin=vmin, vmax=vmax)
244
- self.fig.canvas.draw_idle()
245
-
246
- def set_channel_index(self, value):
247
- # Set the channel index based on dropdown value
248
-
249
- self.target_channel = value
250
- self.init_contrast = True
251
- if self.mode == 'direct':
252
- self.last_frame = self.stack[-1,:,:,self.target_channel]
253
- elif self.mode == 'virtual':
254
- self.last_frame = load_frames(self.img_num_per_channel[self.target_channel, self.stack_length-1],
255
- self.stack_path,
256
- normalize_input=False).astype(float)[:,:,0]
257
- self.change_frame_from_channel_switch(self.frame_slider.value())
258
- self.channel_trigger = False
259
- self.init_contrast = False
260
-
261
- self.set_contrast_decimals()
262
-
263
- def change_frame_from_channel_switch(self, value):
264
-
265
- self.channel_trigger = True
266
- self.change_frame(value)
267
-
268
- def change_frame(self, value):
269
-
270
- # Change the displayed frame based on slider value
271
- if self.channel_trigger:
272
- self.switch_from_channel = True
273
- else:
274
- self.switch_from_channel = False
275
-
276
- if self.mode=='virtual':
277
-
278
- self.init_frame = load_frames(self.img_num_per_channel[self.target_channel, value],
279
- self.stack_path,
280
- normalize_input=False
281
- ).astype(float)[:,:,0]
282
- elif self.mode=='direct':
283
- self.init_frame = self.stack[value,:,:,self.target_channel].copy()
284
-
285
- self.im.set_data(self.init_frame)
286
-
287
- if self.init_contrast:
288
- imgs = np.array([self.init_frame,self.last_frame])
289
- vmin = np.nanpercentile(imgs.flatten(), 1.0)
290
- vmax = np.nanpercentile(imgs.flatten(), 99.99)
291
- self.contrast_slider.setRange(np.nanmin(imgs),np.nanmax(imgs))
292
- self.contrast_slider.setValue((vmin,vmax))
293
- self.im.set_clim(vmin,vmax)
294
-
295
- if self.create_contrast_slider:
296
- self.change_contrast(self.contrast_slider.value())
297
-
298
-
299
- def closeEvent(self, event):
300
- # Event handler for closing the widget
301
- self.canvas.close()
302
-
303
-
304
- class ThresholdedStackVisualizer(StackVisualizer):
305
-
306
- """
307
- A widget for visualizing thresholded image stacks with interactive sliders and channel selection.
308
-
309
- Parameters:
310
- - preprocessing (list or None): A list of preprocessing filters to apply to the image before thresholding.
311
- - parent_le: The parent QLineEdit instance to set the threshold value.
312
- - initial_threshold (float): Initial threshold value.
313
- - initial_mask_alpha (float): Initial mask opacity value.
314
- - args, kwargs: Additional arguments to pass to the parent class constructor.
315
-
316
- Methods:
317
- - generate_apply_btn(): Generate the apply button to set the threshold in the parent QLineEdit.
318
- - set_threshold_in_parent_le(): Set the threshold value in the parent QLineEdit.
319
- - generate_mask_imshow(): Generate the mask imshow.
320
- - generate_threshold_slider(): Generate the threshold slider.
321
- - generate_opacity_slider(): Generate the opacity slider for the mask.
322
- - change_mask_opacity(value): Change the opacity of the mask.
323
- - change_threshold(value): Change the threshold value.
324
- - change_frame(value): Change the displayed frame and update the threshold.
325
- - compute_mask(threshold_value): Compute the mask based on the threshold value.
326
- - preprocess_image(): Preprocess the image before thresholding.
327
-
328
- Notes:
329
- - This class extends the functionality of StackVisualizer to visualize thresholded image stacks
330
- with interactive sliders for threshold and mask opacity adjustment.
331
- """
332
-
333
- def __init__(self, preprocessing=None, parent_le=None, initial_threshold=5, initial_mask_alpha=0.5, show_opacity_slider=True, show_threshold_slider=True, *args, **kwargs):
334
- # Initialize the widget and its attributes
335
- super().__init__(*args, **kwargs)
336
- self.preprocessing = preprocessing
337
- self.thresh = initial_threshold
338
- self.mask_alpha = initial_mask_alpha
339
- self.parent_le = parent_le
340
- self.show_opacity_slider = show_opacity_slider
341
- self.show_threshold_slider = show_threshold_slider
342
- self.thresholded = False
343
- self.mask = np.zeros_like(self.init_frame)
344
- self.thresh_min = 0.0
345
- self.thresh_max = 30.0
346
-
347
- self.generate_threshold_slider()
348
-
349
- if self.thresh is not None:
350
- self.compute_mask(self.thresh)
351
-
352
- self.generate_mask_imshow()
353
- self.generate_scatter()
354
- self.generate_opacity_slider()
355
- if isinstance(self.parent_le, QLineEdit):
356
- self.generate_apply_btn()
357
-
358
- def generate_apply_btn(self):
359
- # Generate the apply button to set the threshold in the parent QLineEdit
360
- apply_hbox = QHBoxLayout()
361
- self.apply_threshold_btn = QPushButton('Apply')
362
- self.apply_threshold_btn.clicked.connect(self.set_threshold_in_parent_le)
363
- self.apply_threshold_btn.setStyleSheet(self.button_style_sheet)
364
- apply_hbox.addWidget(QLabel(''),33)
365
- apply_hbox.addWidget(self.apply_threshold_btn, 33)
366
- apply_hbox.addWidget(QLabel(''),33)
367
- self.canvas.layout.addLayout(apply_hbox)
368
-
369
- def set_threshold_in_parent_le(self):
370
- # Set the threshold value in the parent QLineEdit
371
- self.parent_le.set_threshold(self.threshold_slider.value())
372
- self.close()
373
-
374
- def generate_mask_imshow(self):
375
- # Generate the mask imshow
376
-
377
- self.im_mask = self.ax.imshow(np.ma.masked_where(self.mask==0, self.mask), alpha=self.mask_alpha, interpolation='none')
378
- self.canvas.canvas.draw()
379
-
380
- def generate_scatter(self):
381
- self.scat_markers = self.ax.scatter([], [], color="tab:red")
382
-
383
- def generate_threshold_slider(self):
384
- # Generate the threshold slider
385
- self.threshold_slider = QLabeledDoubleSlider()
386
- if self.thresh is None:
387
- init_value = 1.0E5
388
- else:
389
- init_value = self.thresh
390
- thresh_layout = QuickSliderLayout(label='Threshold: ',
391
- slider=self.threshold_slider,
392
- slider_initial_value=init_value,
393
- slider_range=(self.thresh_min,np.amax([self.thresh_max, init_value])),
394
- decimal_option=True,
395
- precision=4,
396
- )
397
- thresh_layout.setContentsMargins(15,0,15,0)
398
- self.threshold_slider.valueChanged.connect(self.change_threshold)
399
- if self.show_threshold_slider:
400
- self.canvas.layout.addLayout(thresh_layout)
401
-
402
- def generate_opacity_slider(self):
403
- # Generate the opacity slider for the mask
404
- self.opacity_slider = QLabeledDoubleSlider()
405
- opacity_layout = QuickSliderLayout(label='Opacity: ',
406
- slider=self.opacity_slider,
407
- slider_initial_value=0.5,
408
- slider_range=(0,1),
409
- decimal_option=True,
410
- precision=3,
411
- )
412
- opacity_layout.setContentsMargins(15,0,15,0)
413
- self.opacity_slider.valueChanged.connect(self.change_mask_opacity)
414
- if self.show_opacity_slider:
415
- self.canvas.layout.addLayout(opacity_layout)
416
-
417
- def change_mask_opacity(self, value):
418
- # Change the opacity of the mask
419
- self.mask_alpha = value
420
- self.im_mask.set_alpha(self.mask_alpha)
421
- self.canvas.canvas.draw_idle()
422
-
423
- def change_threshold(self, value):
424
- # Change the threshold value
425
- self.thresh = value
426
- if self.thresh is not None:
427
- self.compute_mask(self.thresh)
428
- mask = np.ma.masked_where(self.mask == 0, self.mask)
429
- self.im_mask.set_data(mask)
430
- self.canvas.canvas.draw_idle()
431
-
432
- def change_frame(self, value):
433
- # Change the displayed frame and update the threshold
434
- if self.thresholded:
435
- self.init_contrast = True
436
- super().change_frame(value)
437
- self.change_threshold(self.threshold_slider.value())
438
- if self.thresholded:
439
- self.thresholded = False
440
- self.init_contrast = False
441
-
442
- def compute_mask(self, threshold_value):
443
- # Compute the mask based on the threshold value
444
- self.preprocess_image()
445
- edge = estimate_unreliable_edge(self.preprocessing)
446
- if isinstance(threshold_value, (list,np.ndarray,tuple)):
447
- self.mask = threshold_image(self.processed_image, threshold_value[0], threshold_value[1], foreground_value=1, fill_holes=True, edge_exclusion=edge).astype(int)
448
- else:
449
- self.mask = threshold_image(self.processed_image, threshold_value, np.inf, foreground_value=1, fill_holes=True, edge_exclusion=edge).astype(int)
450
-
451
- def preprocess_image(self):
452
- # Preprocess the image before thresholding
453
- if self.preprocessing is not None:
454
-
455
- assert isinstance(self.preprocessing, list)
456
- self.processed_image = filter_image(self.init_frame.copy().astype(float),filters=self.preprocessing)
457
- min_ = np.amin(self.processed_image)
458
- max_ = np.amax(self.processed_image)
459
-
460
- if min_ < self.thresh_min:
461
- self.thresh_min = min_
462
- if max_ > self.thresh_max:
463
- self.thresh_max = max_
464
-
465
- self.threshold_slider.setRange(self.thresh_min, self.thresh_max)
466
-
467
- def set_preprocessing(self, activation_protocol):
468
-
469
- self.preprocessing = activation_protocol
470
- self.preprocess_image()
471
-
472
- self.im.set_data(self.processed_image)
473
- vmin = np.nanpercentile(self.processed_image, 1.0)
474
- vmax = np.nanpercentile(self.processed_image, 99.99)
475
- self.contrast_slider.setRange(np.nanmin(self.processed_image),
476
- np.nanmax(self.processed_image))
477
- self.contrast_slider.setValue((vmin, vmax))
478
- self.im.set_clim(vmin,vmax)
479
- self.canvas.canvas.draw_idle()
480
- self.thresholded = True
481
-
482
-
483
- class CellEdgeVisualizer(StackVisualizer):
484
-
485
- """
486
- A widget for visualizing cell edges with interactive sliders and channel selection.
487
-
488
- Parameters:
489
- - cell_type (str): Type of cells ('effectors' by default).
490
- - edge_range (tuple): Range of edge sizes (-30, 30) by default.
491
- - invert (bool): Flag to invert the edge size (False by default).
492
- - parent_list_widget: The parent QListWidget instance to add edge measurements.
493
- - parent_le: The parent QLineEdit instance to set the edge size.
494
- - labels (array or None): Array of labels for cell segmentation.
495
- - initial_edge (int): Initial edge size (5 by default).
496
- - initial_mask_alpha (float): Initial mask opacity value (0.5 by default).
497
- - args, kwargs: Additional arguments to pass to the parent class constructor.
498
-
499
- Methods:
500
- - load_labels(): Load the cell labels.
501
- - locate_labels_virtual(): Locate virtual labels.
502
- - generate_add_to_list_btn(): Generate the add to list button.
503
- - generate_add_to_le_btn(): Generate the set measurement button for QLineEdit.
504
- - set_measurement_in_parent_le(): Set the edge size in the parent QLineEdit.
505
- - set_measurement_in_parent_list(): Add the edge size to the parent QListWidget.
506
- - generate_label_imshow(): Generate the label imshow.
507
- - generate_edge_slider(): Generate the edge size slider.
508
- - generate_opacity_slider(): Generate the opacity slider for the mask.
509
- - change_mask_opacity(value): Change the opacity of the mask.
510
- - change_edge_size(value): Change the edge size.
511
- - change_frame(value): Change the displayed frame and update the edge labels.
512
- - compute_edge_labels(): Compute the edge labels.
513
-
514
- Notes:
515
- - This class extends the functionality of StackVisualizer to visualize cell edges
516
- with interactive sliders for edge size adjustment and mask opacity control.
517
- """
518
-
519
- def __init__(self, cell_type="effectors", edge_range=(-30,30), invert=False, parent_list_widget=None, parent_le=None, labels=None, initial_edge=5, initial_mask_alpha=0.5, *args, **kwargs):
520
-
521
- # Initialize the widget and its attributes
522
- super().__init__(*args, **kwargs)
523
- self.edge_size = initial_edge
524
- self.mask_alpha = initial_mask_alpha
525
- self.cell_type = cell_type
526
- self.labels = labels
527
- self.edge_range = edge_range
528
- self.invert = invert
529
- self.parent_list_widget = parent_list_widget
530
- self.parent_le = parent_le
531
-
532
- self.load_labels()
533
- self.generate_label_imshow()
534
- self.generate_edge_slider()
535
- self.generate_opacity_slider()
536
- if isinstance(self.parent_list_widget, QListWidget):
537
- self.generate_add_to_list_btn()
538
- if isinstance(self.parent_le, QLineEdit):
539
- self.generate_add_to_le_btn()
540
-
541
- def load_labels(self):
542
- # Load the cell labels
543
-
544
- if self.labels is not None:
545
-
546
- if isinstance(self.labels, list):
547
- self.labels = np.array(self.labels)
548
-
549
- assert self.labels.ndim==3,'Wrong dimensions for the provided labels, expect TXY'
550
- assert len(self.labels)==self.stack_length
551
-
552
- self.mode = 'direct'
553
- self.init_label = self.labels[self.mid_time,:,:]
554
- else:
555
- self.mode = 'virtual'
556
- assert isinstance(self.stack_path, str)
557
- assert self.stack_path.endswith('.tif')
558
- self.locate_labels_virtual()
559
-
560
- self.compute_edge_labels()
561
-
562
- def locate_labels_virtual(self):
563
- # Locate virtual labels
564
-
565
- labels_path = str(Path(self.stack_path).parent.parent) + os.sep + f'labels_{self.cell_type}' + os.sep
566
- self.mask_paths = natsorted(glob(labels_path + '*.tif'))
567
-
568
- if len(self.mask_paths) == 0:
569
-
570
- msgBox = QMessageBox()
571
- msgBox.setIcon(QMessageBox.Critical)
572
- msgBox.setText("No labels were found for the selected cells. Abort.")
573
- msgBox.setWindowTitle("Critical")
574
- msgBox.setStandardButtons(QMessageBox.Ok)
575
- returnValue = msgBox.exec()
576
- self.close()
577
-
578
- self.init_label = imread(self.mask_paths[self.frame_slider.value()])
579
-
580
- def generate_add_to_list_btn(self):
581
- # Generate the add to list button
582
-
583
- add_hbox = QHBoxLayout()
584
- self.add_measurement_btn = QPushButton('Add measurement')
585
- self.add_measurement_btn.clicked.connect(self.set_measurement_in_parent_list)
586
- self.add_measurement_btn.setIcon(icon(MDI6.plus,color="white"))
587
- self.add_measurement_btn.setIconSize(QSize(20, 20))
588
- self.add_measurement_btn.setStyleSheet(self.button_style_sheet)
589
- add_hbox.addWidget(QLabel(''),33)
590
- add_hbox.addWidget(self.add_measurement_btn, 33)
591
- add_hbox.addWidget(QLabel(''),33)
592
- self.canvas.layout.addLayout(add_hbox)
593
-
594
- def generate_add_to_le_btn(self):
595
- # Generate the set measurement button for QLineEdit
596
-
597
- add_hbox = QHBoxLayout()
598
- self.set_measurement_btn = QPushButton('Set')
599
- self.set_measurement_btn.clicked.connect(self.set_measurement_in_parent_le)
600
- self.set_measurement_btn.setStyleSheet(self.button_style_sheet)
601
- add_hbox.addWidget(QLabel(''),33)
602
- add_hbox.addWidget(self.set_measurement_btn, 33)
603
- add_hbox.addWidget(QLabel(''),33)
604
- self.canvas.layout.addLayout(add_hbox)
605
-
606
- def set_measurement_in_parent_le(self):
607
- # Set the edge size in the parent QLineEdit
608
-
609
- self.parent_le.setText(str(int(self.edge_slider.value())))
610
- self.close()
611
-
612
- def set_measurement_in_parent_list(self):
613
- # Add the edge size to the parent QListWidget
614
-
615
- self.parent_list_widget.addItems([str(self.edge_slider.value())])
616
- self.close()
617
-
618
- def generate_label_imshow(self):
619
- # Generate the label imshow
620
-
621
- self.im_mask = self.ax.imshow(np.ma.masked_where(self.edge_labels==0, self.edge_labels), alpha=self.mask_alpha, interpolation='none', cmap="viridis")
622
- self.canvas.canvas.draw()
623
-
624
- def generate_edge_slider(self):
625
- # Generate the edge size slider
626
-
627
- self.edge_slider = QLabeledSlider()
628
- edge_layout = QuickSliderLayout(label='Edge: ',
629
- slider=self.edge_slider,
630
- slider_initial_value=self.edge_size,
631
- slider_range=self.edge_range,
632
- decimal_option=False,
633
- )
634
- edge_layout.setContentsMargins(15,0,15,0)
635
- self.edge_slider.valueChanged.connect(self.change_edge_size)
636
- self.canvas.layout.addLayout(edge_layout)
637
-
638
- def generate_opacity_slider(self):
639
- # Generate the opacity slider for the mask
640
-
641
- self.opacity_slider = QLabeledDoubleSlider()
642
- opacity_layout = QuickSliderLayout(label='Opacity: ',
643
- slider=self.opacity_slider,
644
- slider_initial_value=0.5,
645
- slider_range=(0,1),
646
- decimal_option=True,
647
- precision=3,
648
- )
649
- opacity_layout.setContentsMargins(15,0,15,0)
650
- self.opacity_slider.valueChanged.connect(self.change_mask_opacity)
651
- self.canvas.layout.addLayout(opacity_layout)
652
-
653
- def change_mask_opacity(self, value):
654
- # Change the opacity of the mask
655
-
656
- self.mask_alpha = value
657
- self.im_mask.set_alpha(self.mask_alpha)
658
- self.canvas.canvas.draw_idle()
659
-
660
- def change_edge_size(self, value):
661
- # Change the edge size
662
-
663
- self.edge_size = value
664
- self.compute_edge_labels()
665
- mask = np.ma.masked_where(self.edge_labels == 0, self.edge_labels)
666
- self.im_mask.set_data(mask)
667
- self.canvas.canvas.draw_idle()
668
-
669
- def change_frame(self, value):
670
- # Change the displayed frame and update the edge labels
671
-
672
- super().change_frame(value)
673
-
674
- if self.mode=='virtual':
675
- self.init_label = imread(self.mask_paths[value])
676
- elif self.mode=='direct':
677
- self.init_label = self.labels[value,:,:]
678
-
679
- self.compute_edge_labels()
680
- mask = np.ma.masked_where(self.edge_labels == 0, self.edge_labels)
681
- self.im_mask.set_data(mask)
682
-
683
- def compute_edge_labels(self):
684
- # Compute the edge labels
685
-
686
- if self.invert:
687
- edge_size = - self.edge_size
688
- else:
689
- edge_size = self.edge_size
690
-
691
- self.edge_labels = contour_of_instance_segmentation(self.init_label, edge_size)
692
-
693
- class SpotDetectionVisualizer(StackVisualizer):
694
-
695
- def __init__(self, parent_channel_cb=None, parent_diameter_le=None, parent_threshold_le=None, parent_preprocessing_list=None, cell_type='targets', labels=None, *args, **kwargs):
696
-
697
- super().__init__(*args, **kwargs)
698
-
699
- self.cell_type = cell_type
700
- self.labels = labels
701
- self.detection_channel = self.target_channel
702
-
703
- self.parent_channel_cb = parent_channel_cb
704
- self.parent_diameter_le = parent_diameter_le
705
- self.parent_threshold_le = parent_threshold_le
706
- self.parent_preprocessing_list = parent_preprocessing_list
707
-
708
- self.spot_sizes = []
709
- self.floatValidator = QDoubleValidator()
710
- self.init_scatter()
711
-
712
- self.generate_detection_channel()
713
- self.detection_channel = self.detection_channel_cb.currentIndex()
714
-
715
- self.generate_spot_detection_params()
716
- self.generate_add_measurement_btn()
717
- self.load_labels()
718
- self.change_frame(self.mid_time)
719
-
720
- self.ax.callbacks.connect('xlim_changed', self.update_marker_sizes)
721
- self.ax.callbacks.connect('ylim_changed', self.update_marker_sizes)
722
-
723
- self.apply_diam_btn.clicked.connect(self.detect_and_display_spots)
724
- self.apply_thresh_btn.clicked.connect(self.detect_and_display_spots)
725
-
726
- self.channels_cb.setCurrentIndex(self.target_channel)
727
- self.detection_channel_cb.setCurrentIndex(self.target_channel)
728
-
729
- def update_marker_sizes(self, event=None):
730
-
731
- # Get axis bounds
732
- xlim = self.ax.get_xlim()
733
- ylim = self.ax.get_ylim()
734
-
735
- # Data-to-pixel scale
736
- ax_width_in_pixels = self.ax.bbox.width
737
- ax_height_in_pixels =self.ax.bbox.height
738
-
739
- x_scale = (float(xlim[1]) - float(xlim[0])) / ax_width_in_pixels
740
- y_scale = (float(ylim[1]) - float(ylim[0])) / ax_height_in_pixels
741
-
742
- # Choose the smaller scale for square pixels
743
- scale = min(x_scale, y_scale)
744
-
745
- # Convert radius_px to data units
746
- if len(self.spot_sizes)>0:
747
-
748
- radius_data_units = self.spot_sizes / float(scale)
749
-
750
- # Convert to scatter `s` size (points squared)
751
- radius_pts = radius_data_units * (72. / self.fig.dpi )
752
- size = np.pi * (radius_pts ** 2)
753
-
754
- # Update scatter sizes
755
- self.spot_scat.set_sizes(size)
756
- self.fig.canvas.draw_idle()
757
-
758
- def init_scatter(self):
759
- self.spot_scat = self.ax.scatter([],[], s=50, facecolors='none', edgecolors='tab:red',zorder=100)
760
- self.canvas.canvas.draw()
761
-
762
- def change_frame(self, value):
763
-
764
- super().change_frame(value)
765
- if not self.switch_from_channel:
766
- self.reset_detection()
767
-
768
- if self.mode=='virtual':
769
- self.init_label = imread(self.mask_paths[value])
770
- self.target_img = load_frames(self.img_num_per_channel[self.detection_channel, value],
771
- self.stack_path,normalize_input=False).astype(float)[:,:,0]
772
- elif self.mode=='direct':
773
- self.init_label = self.labels[value,:,:]
774
- self.target_img = self.stack[value,:,:,self.detection_channel].copy()
775
-
776
- def detect_and_display_spots(self):
777
-
778
- self.reset_detection()
779
- self.control_valid_parameters() # set current diam and threshold
780
- #self.change_frame(self.frame_slider.value())
781
- #self.set_detection_channel_index(self.detection_channel_cb.currentIndex())
782
-
783
- image_preprocessing = self.preprocessing.list.items
784
- if image_preprocessing==[]:
785
- image_preprocessing = None
786
-
787
- blobs_filtered = extract_blobs_in_image(self.target_img, self.init_label,threshold=self.thresh, diameter=self.diameter, image_preprocessing=image_preprocessing)
788
- if blobs_filtered is not None:
789
- self.spot_positions = np.array([[x,y] for y,x,_ in blobs_filtered])
790
- if len(self.spot_positions)>0:
791
- self.spot_sizes = np.sqrt(2)*np.array([sig for _,_,sig in blobs_filtered])
792
- #radius_pts = self.spot_sizes * (self.fig.dpi / 72.0)
793
- #sizes = np.pi*(radius_pts**2)
794
- if len(self.spot_positions)>0:
795
- self.spot_scat.set_offsets(self.spot_positions)
796
- else:
797
- empty_offset = np.ma.masked_array([0, 0], mask=True)
798
- self.spot_scat.set_offsets(empty_offset)
799
- #self.spot_scat.set_sizes(sizes)
800
- if len(self.spot_positions)>0:
801
- self.update_marker_sizes()
802
- self.canvas.canvas.draw()
803
-
804
- def reset_detection(self):
805
-
806
- self.ax.scatter([], []).get_offsets()
807
- empty_offset = np.ma.masked_array([0, 0], mask=True)
808
- self.spot_scat.set_offsets(empty_offset)
809
- self.canvas.canvas.draw()
810
-
811
- def load_labels(self):
812
-
813
- # Load the cell labels
814
- if self.labels is not None:
815
-
816
- if isinstance(self.labels, list):
817
- self.labels = np.array(self.labels)
818
-
819
- assert self.labels.ndim==3,'Wrong dimensions for the provided labels, expect TXY'
820
- assert len(self.labels)==self.stack_length
821
-
822
- self.mode = 'direct'
823
- self.init_label = self.labels[self.mid_time,:,:]
824
- else:
825
- self.mode = 'virtual'
826
- assert isinstance(self.stack_path, str)
827
- assert self.stack_path.endswith('.tif')
828
- self.locate_labels_virtual()
829
-
830
- def locate_labels_virtual(self):
831
- # Locate virtual labels
832
-
833
- labels_path = str(Path(self.stack_path).parent.parent) + os.sep + f'labels_{self.cell_type}' + os.sep
834
- self.mask_paths = natsorted(glob(labels_path + '*.tif'))
835
-
836
- if len(self.mask_paths) == 0:
837
-
838
- msgBox = QMessageBox()
839
- msgBox.setIcon(QMessageBox.Critical)
840
- msgBox.setText("No labels were found for the selected cells. Abort.")
841
- msgBox.setWindowTitle("Critical")
842
- msgBox.setStandardButtons(QMessageBox.Ok)
843
- returnValue = msgBox.exec()
844
- self.close()
845
-
846
- self.init_label = imread(self.mask_paths[self.frame_slider.value()])
847
-
848
- def generate_detection_channel(self):
849
-
850
- assert self.channel_names is not None
851
- assert len(self.channel_names)==self.n_channels
852
-
853
- channel_layout = QHBoxLayout()
854
- channel_layout.setContentsMargins(15,0,15,0)
855
- channel_layout.addWidget(QLabel('Detection\nchannel: '), 25)
856
-
857
- self.detection_channel_cb = QComboBox()
858
- self.detection_channel_cb.addItems(self.channel_names)
859
- self.detection_channel_cb.currentIndexChanged.connect(self.set_detection_channel_index)
860
- channel_layout.addWidget(self.detection_channel_cb, 75)
861
-
862
- # self.invert_check = QCheckBox('invert')
863
- # if self.invert:
864
- # self.invert_check.setChecked(True)
865
- # self.invert_check.toggled.connect(self.set_invert)
866
- # channel_layout.addWidget(self.invert_check, 10)
867
-
868
- self.canvas.layout.addLayout(channel_layout)
869
-
870
- self.preprocessing = PreprocessingLayout2(fraction=25, parent_window=self)
871
- self.preprocessing.setContentsMargins(15,0,15,0)
872
- self.canvas.layout.addLayout(self.preprocessing)
873
-
874
-
875
- # def set_invert(self):
876
- # if self.invert_check.isChecked():
877
- # self.invert = True
878
- # else:
879
- # self.invert = False
880
-
881
- def set_detection_channel_index(self, value):
882
-
883
- self.detection_channel = value
884
- if self.mode == 'direct':
885
- self.target_img = self.stack[-1,:,:,self.detection_channel]
886
- elif self.mode == 'virtual':
887
- self.target_img = load_frames(self.img_num_per_channel[self.detection_channel, self.frame_slider.value()],
888
- self.stack_path,normalize_input=False).astype(float)[:,:,0]
889
-
890
- def generate_spot_detection_params(self):
891
-
892
- self.spot_diam_le = QLineEdit('1')
893
- self.spot_diam_le.setValidator(self.floatValidator)
894
- self.apply_diam_btn = QPushButton('Set')
895
- self.apply_diam_btn.setStyleSheet(self.button_style_sheet_2)
896
-
897
- self.spot_thresh_le = QLineEdit('0')
898
- self.spot_thresh_le.setValidator(self.floatValidator)
899
- self.apply_thresh_btn = QPushButton('Set')
900
- self.apply_thresh_btn.setStyleSheet(self.button_style_sheet_2)
901
-
902
- self.spot_diam_le.textChanged.connect(self.control_valid_parameters)
903
- self.spot_thresh_le.textChanged.connect(self.control_valid_parameters)
904
-
905
- spot_diam_layout = QHBoxLayout()
906
- spot_diam_layout.setContentsMargins(15,0,15,0)
907
- spot_diam_layout.addWidget(QLabel('Spot diameter: '), 25)
908
- spot_diam_layout.addWidget(self.spot_diam_le, 65)
909
- spot_diam_layout.addWidget(self.apply_diam_btn, 10)
910
- self.canvas.layout.addLayout(spot_diam_layout)
911
-
912
- spot_thresh_layout = QHBoxLayout()
913
- spot_thresh_layout.setContentsMargins(15,0,15,0)
914
- spot_thresh_layout.addWidget(QLabel('Detection\nthreshold: '), 25)
915
- spot_thresh_layout.addWidget(self.spot_thresh_le, 65)
916
- spot_thresh_layout.addWidget(self.apply_thresh_btn, 10)
917
- self.canvas.layout.addLayout(spot_thresh_layout)
918
-
919
- def generate_add_measurement_btn(self):
920
-
921
- add_hbox = QHBoxLayout()
922
- self.add_measurement_btn = QPushButton('Add measurement')
923
- self.add_measurement_btn.clicked.connect(self.set_measurement_in_parent_list)
924
- self.add_measurement_btn.setIcon(icon(MDI6.plus,color="white"))
925
- self.add_measurement_btn.setIconSize(QSize(20, 20))
926
- self.add_measurement_btn.setStyleSheet(self.button_style_sheet)
927
- add_hbox.addWidget(QLabel(''),33)
928
- add_hbox.addWidget(self.add_measurement_btn, 33)
929
- add_hbox.addWidget(QLabel(''),33)
930
- self.canvas.layout.addLayout(add_hbox)
931
-
932
- def control_valid_parameters(self):
933
-
934
- valid_diam = False
935
- try:
936
- self.diameter = float(self.spot_diam_le.text().replace(',','.'))
937
- valid_diam = True
938
- except:
939
- valid_diam = False
940
-
941
- valid_thresh = False
942
- try:
943
- self.thresh = float(self.spot_thresh_le.text().replace(',','.'))
944
- valid_thresh = True
945
- except:
946
- valid_thresh = False
947
-
948
- if valid_diam and valid_thresh:
949
- self.apply_diam_btn.setEnabled(True)
950
- self.apply_thresh_btn.setEnabled(True)
951
- self.add_measurement_btn.setEnabled(True)
952
- else:
953
- self.apply_diam_btn.setEnabled(False)
954
- self.apply_thresh_btn.setEnabled(False)
955
- self.add_measurement_btn.setEnabled(False)
956
-
957
- def set_measurement_in_parent_list(self):
958
-
959
- if self.parent_channel_cb is not None:
960
- self.parent_channel_cb.setCurrentIndex(self.detection_channel)
961
- if self.parent_diameter_le is not None:
962
- self.parent_diameter_le.setText(self.spot_diam_le.text())
963
- if self.parent_threshold_le is not None:
964
- self.parent_threshold_le.setText(self.spot_thresh_le.text())
965
- if self.parent_preprocessing_list is not None:
966
- self.parent_preprocessing_list.clear()
967
- items = self.preprocessing.list.getItems()
968
- for item in items:
969
- self.parent_preprocessing_list.addItemToList(item)
970
- self.parent_preprocessing_list.items = self.preprocessing.list.items
971
- self.close()
972
-
973
- class CellSizeViewer(StackVisualizer):
974
-
975
- """
976
- A widget for visualizing cell size with interactive sliders and circle display.
977
-
978
- Parameters:
979
- - initial_diameter (int): Initial diameter of the circle (40 by default).
980
- - set_radius_in_list (bool): Flag to set radius instead of diameter in the list (False by default).
981
- - diameter_slider_range (tuple): Range of the diameter slider (0, 200) by default.
982
- - parent_le: The parent QLineEdit instance to set the diameter.
983
- - parent_list_widget: The parent QListWidget instance to add diameter measurements.
984
- - args, kwargs: Additional arguments to pass to the parent class constructor.
985
-
986
- Methods:
987
- - generate_circle(): Generate the circle for visualization.
988
- - generate_add_to_list_btn(): Generate the add to list button.
989
- - set_measurement_in_parent_list(): Add the diameter to the parent QListWidget.
990
- - on_xlims_or_ylims_change(event_ax): Update the circle position on axis limits change.
991
- - generate_set_btn(): Generate the set button for QLineEdit.
992
- - set_threshold_in_parent_le(): Set the diameter in the parent QLineEdit.
993
- - generate_diameter_slider(): Generate the diameter slider.
994
- - change_diameter(value): Change the diameter of the circle.
995
-
996
- Notes:
997
- - This class extends the functionality of StackVisualizer to visualize cell size
998
- with interactive sliders for diameter adjustment and circle display.
999
- """
1000
-
1001
- def __init__(self, initial_diameter=40, set_radius_in_list=False, diameter_slider_range=(0,500), parent_le=None, parent_list_widget=None, *args, **kwargs):
1002
- # Initialize the widget and its attributes
1003
-
1004
- super().__init__(*args, **kwargs)
1005
- self.diameter = initial_diameter
1006
- self.parent_le = parent_le
1007
- self.diameter_slider_range = diameter_slider_range
1008
- self.parent_list_widget = parent_list_widget
1009
- self.set_radius_in_list = set_radius_in_list
1010
- self.generate_circle()
1011
- self.generate_diameter_slider()
1012
-
1013
- if isinstance(self.parent_le, QLineEdit):
1014
- self.generate_set_btn()
1015
- if isinstance(self.parent_list_widget, QListWidget):
1016
- self.generate_add_to_list_btn()
1017
-
1018
- def generate_circle(self):
1019
- # Generate the circle for visualization
1020
-
1021
- self.circ = plt.Circle((self.init_frame.shape[0]//2,self.init_frame.shape[1]//2), self.diameter//2 / self.PxToUm, ec="tab:red",fill=False)
1022
- self.ax.add_patch(self.circ)
1023
-
1024
- self.ax.callbacks.connect('xlim_changed',self.on_xlims_or_ylims_change)
1025
- self.ax.callbacks.connect('ylim_changed', self.on_xlims_or_ylims_change)
1026
-
1027
- def generate_add_to_list_btn(self):
1028
- # Generate the add to list button
1029
-
1030
- add_hbox = QHBoxLayout()
1031
- self.add_measurement_btn = QPushButton('Add measurement')
1032
- self.add_measurement_btn.clicked.connect(self.set_measurement_in_parent_list)
1033
- self.add_measurement_btn.setIcon(icon(MDI6.plus,color="white"))
1034
- self.add_measurement_btn.setIconSize(QSize(20, 20))
1035
- self.add_measurement_btn.setStyleSheet(self.button_style_sheet)
1036
- add_hbox.addWidget(QLabel(''),33)
1037
- add_hbox.addWidget(self.add_measurement_btn, 33)
1038
- add_hbox.addWidget(QLabel(''),33)
1039
- self.canvas.layout.addLayout(add_hbox)
1040
-
1041
- def set_measurement_in_parent_list(self):
1042
- # Add the diameter to the parent QListWidget
1043
-
1044
- if self.set_radius_in_list:
1045
- val = int(self.diameter_slider.value()//2)
1046
- else:
1047
- val = int(self.diameter_slider.value())
1048
-
1049
- self.parent_list_widget.addItems([str(val)])
1050
- self.close()
1051
-
1052
- def on_xlims_or_ylims_change(self, event_ax):
1053
- # Update the circle position on axis limits change
1054
-
1055
- xmin,xmax = event_ax.get_xlim()
1056
- ymin,ymax = event_ax.get_ylim()
1057
- self.circ.center = np.mean([xmin,xmax]), np.mean([ymin,ymax])
1058
-
1059
- def generate_set_btn(self):
1060
- # Generate the set button for QLineEdit
1061
-
1062
- apply_hbox = QHBoxLayout()
1063
- self.apply_threshold_btn = QPushButton('Set')
1064
- self.apply_threshold_btn.clicked.connect(self.set_threshold_in_parent_le)
1065
- self.apply_threshold_btn.setStyleSheet(self.button_style_sheet)
1066
- apply_hbox.addWidget(QLabel(''),33)
1067
- apply_hbox.addWidget(self.apply_threshold_btn, 33)
1068
- apply_hbox.addWidget(QLabel(''),33)
1069
- self.canvas.layout.addLayout(apply_hbox)
1070
-
1071
- def set_threshold_in_parent_le(self):
1072
- # Set the diameter in the parent QLineEdit
1073
-
1074
- self.parent_le.set_threshold(self.diameter_slider.value())
1075
- self.close()
1076
-
1077
- def generate_diameter_slider(self):
1078
- # Generate the diameter slider
1079
-
1080
- self.diameter_slider = QLabeledDoubleSlider()
1081
- diameter_layout = QuickSliderLayout(label='Diameter: ',
1082
- slider=self.diameter_slider,
1083
- slider_initial_value=self.diameter,
1084
- slider_range=self.diameter_slider_range,
1085
- decimal_option=True,
1086
- precision=5,
1087
- )
1088
- diameter_layout.setContentsMargins(15,0,15,0)
1089
- self.diameter_slider.valueChanged.connect(self.change_diameter)
1090
- self.canvas.layout.addLayout(diameter_layout)
1091
-
1092
- def change_diameter(self, value):
1093
- # Change the diameter of the circle
1094
- self.diameter = value
1095
- self.circ.set_radius(self.diameter//2 / self.PxToUm)
1096
- self.canvas.canvas.draw_idle()
1097
-
1098
-
1099
- class ChannelOffsetViewer(StackVisualizer):
1100
-
1101
- def __init__(self, parent_window=None, *args, **kwargs):
1102
-
1103
- self.parent_window = parent_window
1104
- self.overlay_target_channel = -1
1105
- self.shift_vertical = 0
1106
- self.shift_horizontal = 0
1107
- super().__init__(*args, **kwargs)
1108
-
1109
- self.load_stack()
1110
- self.canvas.layout.addWidget(QHSeperationLine())
1111
-
1112
- self.generate_overlay_channel_cb()
1113
- self.generate_overlay_imshow()
1114
-
1115
- self.generate_overlay_alpha_slider()
1116
- self.generate_overlay_contrast_slider()
1117
-
1118
- self.generate_overlay_shift()
1119
- self.generate_add_to_parent_btn()
1120
-
1121
- if self.overlay_target_channel==-1:
1122
- index = len(self.channel_names) -1
1123
- else:
1124
- index = self.overlay_target_channel
1125
- self.channels_overlay_cb.setCurrentIndex(index)
1126
- self.frame_slider.valueChanged.connect(self.change_overlay_frame)
1127
-
1128
- self.define_keyboard_shortcuts()
1129
-
1130
- self.channels_overlay_cb.setCurrentIndex(self.parent_window.channels_cb.currentIndex())
1131
- self.set_channel_index(0)
1132
-
1133
- self.setAttribute(Qt.WA_DeleteOnClose)
1134
-
1135
- def generate_overlay_imshow(self):
1136
- self.im_overlay = self.ax.imshow(self.overlay_init_frame, cmap='Blues', interpolation='none',alpha=0.5, **self.imshow_kwargs)
1137
-
1138
- def generate_overlay_alpha_slider(self):
1139
- # Generate the contrast slider if enabled
1140
-
1141
- self.overlay_alpha_slider = QLabeledDoubleSlider()
1142
- alpha_layout = QuickSliderLayout(
1143
- label='Overlay\ntransparency: ',
1144
- slider=self.overlay_alpha_slider,
1145
- slider_initial_value=0.5,
1146
- slider_range=(0,1.0),
1147
- decimal_option=True,
1148
- precision=5,
1149
- )
1150
- alpha_layout.setContentsMargins(15,0,15,0)
1151
- self.overlay_alpha_slider.valueChanged.connect(self.change_alpha_overlay)
1152
- self.canvas.layout.addLayout(alpha_layout)
1153
-
1154
-
1155
- def generate_overlay_contrast_slider(self):
1156
- # Generate the contrast slider if enabled
1157
-
1158
- self.overlay_contrast_slider = QLabeledDoubleRangeSlider()
1159
- contrast_layout = QuickSliderLayout(
1160
- label='Overlay contrast: ',
1161
- slider=self.overlay_contrast_slider,
1162
- slider_initial_value=[np.nanpercentile(self.overlay_init_frame, 0.1),np.nanpercentile(self.overlay_init_frame, 99.99)],
1163
- slider_range=(np.nanmin(self.overlay_init_frame),np.nanmax(self.overlay_init_frame)),
1164
- decimal_option=True,
1165
- precision=5,
1166
- )
1167
- contrast_layout.setContentsMargins(15,0,15,0)
1168
- self.im_overlay.set_clim(vmin=np.nanpercentile(self.overlay_init_frame, 0.1),vmax=np.nanpercentile(self.overlay_init_frame, 99.99))
1169
- self.overlay_contrast_slider.valueChanged.connect(self.change_contrast_overlay)
1170
- self.canvas.layout.addLayout(contrast_layout)
1171
-
1172
- def set_overlay_channel_index(self, value):
1173
- # Set the channel index based on dropdown value
1174
-
1175
- self.overlay_target_channel = value
1176
- self.overlay_init_contrast = True
1177
- if self.mode == 'direct':
1178
- self.overlay_last_frame = self.stack[-1,:,:,self.overlay_target_channel]
1179
- elif self.mode == 'virtual':
1180
- self.overlay_last_frame = load_frames(self.img_num_per_channel[self.overlay_target_channel, self.stack_length-1],
1181
- self.stack_path,
1182
- normalize_input=False).astype(float)[:,:,0]
1183
- self.change_overlay_frame(self.frame_slider.value())
1184
- self.overlay_init_contrast = False
1185
-
1186
- def generate_overlay_channel_cb(self):
1187
-
1188
- assert self.channel_names is not None
1189
- assert len(self.channel_names)==self.n_channels
1190
-
1191
- channel_layout = QHBoxLayout()
1192
- channel_layout.setContentsMargins(15,0,15,0)
1193
- channel_layout.addWidget(QLabel('Overlay channel: '), 25)
1194
-
1195
- self.channels_overlay_cb = QComboBox()
1196
- self.channels_overlay_cb.addItems(self.channel_names)
1197
- self.channels_overlay_cb.currentIndexChanged.connect(self.set_overlay_channel_index)
1198
- channel_layout.addWidget(self.channels_overlay_cb, 75)
1199
- self.canvas.layout.addLayout(channel_layout)
1200
-
1201
- def generate_overlay_shift(self):
1202
-
1203
- shift_layout = QHBoxLayout()
1204
- shift_layout.setContentsMargins(15,0,15,0)
1205
- shift_layout.addWidget(QLabel('shift (h): '), 20, alignment=Qt.AlignRight)
1206
-
1207
- self.apply_shift_btn = QPushButton('Apply')
1208
- self.apply_shift_btn.setStyleSheet(self.button_style_sheet_2)
1209
- self.apply_shift_btn.setToolTip('Apply the shift to the overlay channel.')
1210
- self.apply_shift_btn.clicked.connect(self.shift_generic)
1211
-
1212
- self.set_shift_btn = QPushButton('Set')
1213
-
1214
- self.horizontal_shift_le = ThresholdLineEdit(init_value=self.shift_horizontal, connected_buttons=[self.apply_shift_btn, self.set_shift_btn],placeholder='horizontal shift [pixels]', value_type='float')
1215
- shift_layout.addWidget(self.horizontal_shift_le, 20)
1216
-
1217
- shift_layout.addWidget(QLabel('shift (v): '), 20, alignment=Qt.AlignRight)
1218
-
1219
- self.vertical_shift_le = ThresholdLineEdit(init_value=self.shift_vertical, connected_buttons=[self.apply_shift_btn, self.set_shift_btn],placeholder='vertical shift [pixels]', value_type='float')
1220
- shift_layout.addWidget(self.vertical_shift_le, 20)
1221
-
1222
- shift_layout.addWidget(self.apply_shift_btn, 20)
1223
-
1224
- self.canvas.layout.addLayout(shift_layout)
1225
-
1226
-
1227
- def change_overlay_frame(self, value):
1228
- # Change the displayed frame based on slider value
1229
-
1230
- if self.mode=='virtual':
1231
-
1232
- self.overlay_init_frame = load_frames(self.img_num_per_channel[self.overlay_target_channel, value],
1233
- self.stack_path,
1234
- normalize_input=False
1235
- ).astype(float)[:,:,0]
1236
- elif self.mode=='direct':
1237
- self.overlay_init_frame = self.stack[value,:,:,self.overlay_target_channel].copy()
1238
-
1239
- self.im_overlay.set_data(self.overlay_init_frame)
1240
-
1241
- if self.overlay_init_contrast:
1242
- self.im_overlay.autoscale()
1243
- I_min, I_max = self.im_overlay.get_clim()
1244
- self.overlay_contrast_slider.setRange(np.nanmin([self.overlay_init_frame,self.overlay_last_frame]),np.nanmax([self.overlay_init_frame,self.overlay_last_frame]))
1245
- self.overlay_contrast_slider.setValue((I_min,I_max))
1246
-
1247
- if self.create_contrast_slider:
1248
- self.change_contrast_overlay(self.overlay_contrast_slider.value())
1249
-
1250
- def locate_image_virtual(self):
1251
- # Locate the stack of images if provided as a file
1252
- self.stack_length = auto_load_number_of_frames(self.stack_path)
1253
- if self.stack_length is None:
1254
- stack = imread(self.stack_path)
1255
- self.stack_length = len(stack)
1256
- del stack
1257
- gc.collect()
1258
-
1259
- self.mid_time = self.stack_length // 2
1260
- self.img_num_per_channel = _get_img_num_per_channel(np.arange(self.n_channels), self.stack_length, self.n_channels)
1261
-
1262
- self.init_frame = load_frames(self.img_num_per_channel[self.target_channel, self.mid_time],
1263
- self.stack_path,
1264
- normalize_input=False).astype(float)[:,:,0]
1265
- self.last_frame = load_frames(self.img_num_per_channel[self.target_channel, self.stack_length-1],
1266
- self.stack_path,
1267
- normalize_input=False).astype(float)[:,:,0]
1268
- self.overlay_init_frame = load_frames(self.img_num_per_channel[self.overlay_target_channel, self.mid_time],
1269
- self.stack_path,
1270
- normalize_input=False).astype(float)[:,:,0]
1271
- self.overlay_last_frame = load_frames(self.img_num_per_channel[self.overlay_target_channel, self.stack_length-1],
1272
- self.stack_path,
1273
- normalize_input=False).astype(float)[:,:,0]
1274
-
1275
- def change_contrast_overlay(self, value):
1276
- # Change contrast based on slider value
1277
-
1278
- vmin = value[0]
1279
- vmax = value[1]
1280
- self.im_overlay.set_clim(vmin=vmin, vmax=vmax)
1281
- self.fig.canvas.draw_idle()
1282
-
1283
- def change_alpha_overlay(self, value):
1284
- # Change contrast based on slider value
1285
-
1286
- alpha = value
1287
- self.im_overlay.set_alpha(alpha)
1288
- self.fig.canvas.draw_idle()
1289
-
1290
-
1291
- def define_keyboard_shortcuts(self):
1292
-
1293
- self.shift_up_shortcut = QShortcut(QKeySequence(Qt.Key_Up), self.canvas)
1294
- self.shift_up_shortcut.activated.connect(self.shift_overlay_up)
1295
-
1296
- self.shift_down_shortcut = QShortcut(QKeySequence(Qt.Key_Down), self.canvas)
1297
- self.shift_down_shortcut.activated.connect(self.shift_overlay_down)
1298
-
1299
- self.shift_left_shortcut = QShortcut(QKeySequence(Qt.Key_Left), self.canvas)
1300
- self.shift_left_shortcut.activated.connect(self.shift_overlay_left)
1301
-
1302
- self.shift_right_shortcut = QShortcut(QKeySequence(Qt.Key_Right), self.canvas)
1303
- self.shift_right_shortcut.activated.connect(self.shift_overlay_right)
1304
-
1305
-
1306
- def shift_overlay_up(self):
1307
- self.shift_vertical -= 2
1308
- self.vertical_shift_le.set_threshold(self.shift_vertical)
1309
- #self.shift_generic()
1310
- self.apply_shift_btn.click()
1311
-
1312
- def shift_overlay_down(self):
1313
- self.shift_vertical += 2
1314
- self.vertical_shift_le.set_threshold(self.shift_vertical)
1315
- #self.shift_generic()
1316
- self.apply_shift_btn.click()
1317
-
1318
- def shift_overlay_left(self):
1319
- self.shift_horizontal -= 2
1320
- self.horizontal_shift_le.set_threshold(self.shift_horizontal)
1321
- #self.shift_generic()
1322
- self.apply_shift_btn.click()
1323
-
1324
- def shift_overlay_right(self):
1325
- self.shift_horizontal += 2
1326
- self.horizontal_shift_le.set_threshold(self.shift_horizontal)
1327
- #self.shift_generic()
1328
- self.apply_shift_btn.click()
1329
-
1330
- def shift_generic(self):
1331
- self.shift_vertical = self.vertical_shift_le.get_threshold()
1332
- self.shift_horizontal = self.horizontal_shift_le.get_threshold()
1333
- self.shifted_frame = shift(self.overlay_init_frame, [self.shift_vertical, self.shift_horizontal],prefilter=False)
1334
- self.im_overlay.set_data(self.shifted_frame)
1335
- self.fig.canvas.draw_idle()
1336
-
1337
- def generate_add_to_parent_btn(self):
1338
-
1339
- add_hbox = QHBoxLayout()
1340
- add_hbox.setContentsMargins(0,5,0,5)
1341
- self.set_shift_btn.clicked.connect(self.set_parent_attributes)
1342
- self.set_shift_btn.setStyleSheet(self.button_style_sheet)
1343
- add_hbox.addWidget(QLabel(''),33)
1344
- add_hbox.addWidget(self.set_shift_btn, 33)
1345
- add_hbox.addWidget(QLabel(''),33)
1346
- self.canvas.layout.addLayout(add_hbox)
1347
-
1348
- def set_parent_attributes(self):
1349
-
1350
- idx = self.channels_overlay_cb.currentIndex()
1351
- self.parent_window.channels_cb.setCurrentIndex(idx)
1352
- self.parent_window.vertical_shift_le.set_threshold(self.vertical_shift_le.get_threshold())
1353
- self.parent_window.horizontal_shift_le.set_threshold(self.horizontal_shift_le.get_threshold())
1354
- self.close()