celldetective 1.0.2.post1__py3-none-any.whl → 1.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. celldetective/__main__.py +7 -21
  2. celldetective/events.py +2 -44
  3. celldetective/extra_properties.py +62 -52
  4. celldetective/filters.py +4 -5
  5. celldetective/gui/__init__.py +1 -1
  6. celldetective/gui/analyze_block.py +37 -10
  7. celldetective/gui/btrack_options.py +24 -23
  8. celldetective/gui/classifier_widget.py +62 -19
  9. celldetective/gui/configure_new_exp.py +32 -35
  10. celldetective/gui/control_panel.py +120 -81
  11. celldetective/gui/gui_utils.py +674 -396
  12. celldetective/gui/json_readers.py +7 -6
  13. celldetective/gui/layouts.py +756 -0
  14. celldetective/gui/measurement_options.py +98 -513
  15. celldetective/gui/neighborhood_options.py +322 -270
  16. celldetective/gui/plot_measurements.py +1114 -0
  17. celldetective/gui/plot_signals_ui.py +21 -20
  18. celldetective/gui/process_block.py +449 -169
  19. celldetective/gui/retrain_segmentation_model_options.py +27 -26
  20. celldetective/gui/retrain_signal_model_options.py +25 -24
  21. celldetective/gui/seg_model_loader.py +31 -27
  22. celldetective/gui/signal_annotator.py +2326 -2295
  23. celldetective/gui/signal_annotator_options.py +18 -16
  24. celldetective/gui/styles.py +16 -1
  25. celldetective/gui/survival_ui.py +67 -39
  26. celldetective/gui/tableUI.py +337 -48
  27. celldetective/gui/thresholds_gui.py +75 -71
  28. celldetective/gui/viewers.py +743 -0
  29. celldetective/io.py +247 -27
  30. celldetective/measure.py +43 -263
  31. celldetective/models/segmentation_effectors/primNK_cfse/config_input.json +29 -0
  32. celldetective/models/segmentation_effectors/primNK_cfse/cp-cfse-transfer +0 -0
  33. celldetective/models/segmentation_effectors/primNK_cfse/training_instructions.json +37 -0
  34. celldetective/neighborhood.py +498 -27
  35. celldetective/preprocessing.py +1023 -0
  36. celldetective/scripts/analyze_signals.py +7 -0
  37. celldetective/scripts/measure_cells.py +12 -0
  38. celldetective/scripts/segment_cells.py +20 -4
  39. celldetective/scripts/track_cells.py +11 -0
  40. celldetective/scripts/train_segmentation_model.py +35 -34
  41. celldetective/segmentation.py +14 -9
  42. celldetective/signals.py +234 -329
  43. celldetective/tracking.py +2 -2
  44. celldetective/utils.py +602 -49
  45. celldetective-1.1.1.dist-info/METADATA +305 -0
  46. celldetective-1.1.1.dist-info/RECORD +84 -0
  47. {celldetective-1.0.2.post1.dist-info → celldetective-1.1.1.dist-info}/top_level.txt +1 -0
  48. tests/__init__.py +0 -0
  49. tests/test_events.py +28 -0
  50. tests/test_filters.py +24 -0
  51. tests/test_io.py +70 -0
  52. tests/test_measure.py +141 -0
  53. tests/test_neighborhood.py +70 -0
  54. tests/test_preprocessing.py +37 -0
  55. tests/test_segmentation.py +93 -0
  56. tests/test_signals.py +135 -0
  57. tests/test_tracking.py +164 -0
  58. tests/test_utils.py +118 -0
  59. celldetective-1.0.2.post1.dist-info/METADATA +0 -221
  60. celldetective-1.0.2.post1.dist-info/RECORD +0 -66
  61. {celldetective-1.0.2.post1.dist-info → celldetective-1.1.1.dist-info}/LICENSE +0 -0
  62. {celldetective-1.0.2.post1.dist-info → celldetective-1.1.1.dist-info}/WHEEL +0 -0
  63. {celldetective-1.0.2.post1.dist-info → celldetective-1.1.1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,743 @@
1
+ import numpy as np
2
+ from celldetective.io import auto_load_number_of_frames, load_frames
3
+ from celldetective.filters import *
4
+ from celldetective.segmentation import filter_image, threshold_image
5
+ from celldetective.measure import contour_of_instance_segmentation
6
+ from celldetective.utils import _get_img_num_per_channel, estimate_unreliable_edge
7
+ from tifffile import imread
8
+ import matplotlib.pyplot as plt
9
+ from stardist import fill_label_holes
10
+ from pathlib import Path
11
+ from natsort import natsorted
12
+ from glob import glob
13
+ import os
14
+
15
+ from PyQt5.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout, QPushButton, QLabel, QComboBox, QLineEdit, QListWidget
16
+ from PyQt5.QtCore import Qt, QSize
17
+ from celldetective.gui.gui_utils import FigureCanvas, QuickSliderLayout, center_window
18
+ from celldetective.gui import Styles
19
+ from superqt import QLabeledDoubleSlider, QLabeledSlider, QLabeledDoubleRangeSlider
20
+ from superqt.fonticon import icon
21
+ from fonticon_mdi6 import MDI6
22
+ from matplotlib_scalebar.scalebar import ScaleBar
23
+ import gc
24
+ from celldetective.utils import mask_edges
25
+
26
+ class StackVisualizer(QWidget, Styles):
27
+
28
+ """
29
+ A widget for visualizing image stacks with interactive sliders and channel selection.
30
+
31
+ Parameters:
32
+ - stack (numpy.ndarray or None): The stack of images.
33
+ - stack_path (str or None): The path to the stack of images if provided as a file.
34
+ - frame_slider (bool): Enable frame navigation slider.
35
+ - contrast_slider (bool): Enable contrast adjustment slider.
36
+ - channel_cb (bool): Enable channel selection dropdown.
37
+ - channel_names (list or None): Names of the channels if `channel_cb` is True.
38
+ - n_channels (int): Number of channels.
39
+ - target_channel (int): Index of the target channel.
40
+ - window_title (str): Title of the window.
41
+ - PxToUm (float or None): Pixel to micrometer conversion factor.
42
+ - background_color (str): Background color of the widget.
43
+ - imshow_kwargs (dict): Additional keyword arguments for imshow function.
44
+
45
+ Methods:
46
+ - show(): Display the widget.
47
+ - load_stack(): Load the stack of images.
48
+ - locate_image_virtual(): Locate the stack of images if provided as a file.
49
+ - generate_figure_canvas(): Generate the figure canvas for displaying images.
50
+ - generate_channel_cb(): Generate the channel dropdown if enabled.
51
+ - generate_contrast_slider(): Generate the contrast slider if enabled.
52
+ - generate_frame_slider(): Generate the frame slider if enabled.
53
+ - set_target_channel(value): Set the target channel.
54
+ - change_contrast(value): Change contrast based on slider value.
55
+ - set_channel_index(value): Set the channel index based on dropdown value.
56
+ - change_frame(value): Change the displayed frame based on slider value.
57
+ - closeEvent(event): Event handler for closing the widget.
58
+
59
+ Notes:
60
+ - This class provides a convenient interface for visualizing image stacks with frame navigation,
61
+ contrast adjustment, and channel selection functionalities.
62
+ """
63
+
64
+ 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={}):
65
+ super().__init__()
66
+
67
+ #self.setWindowTitle(window_title)
68
+ self.window_title = window_title
69
+
70
+ self.stack = stack
71
+ self.stack_path = stack_path
72
+ self.create_frame_slider = frame_slider
73
+ self.background_color = background_color
74
+ self.create_contrast_slider = contrast_slider
75
+ self.create_channel_cb = channel_cb
76
+ self.n_channels = n_channels
77
+ self.channel_names = channel_names
78
+ self.target_channel = target_channel
79
+ self.imshow_kwargs = imshow_kwargs
80
+ self.PxToUm = PxToUm
81
+ self.init_contrast = 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
+ center_window(self)
93
+ self.canvas.layout.setContentsMargins(15,15,15,30)
94
+ self.setAttribute(Qt.WA_DeleteOnClose)
95
+
96
+ def show(self):
97
+ # Display the widget
98
+ self.canvas.show()
99
+
100
+ def load_stack(self):
101
+ # Load the stack of images
102
+ if self.stack is not None:
103
+
104
+ if isinstance(self.stack, list):
105
+ self.stack = np.array(self.stack)
106
+
107
+ if self.stack.ndim==3:
108
+ print('No channel axis found...')
109
+ self.stack = self.stack[:,:,:,np.newaxis]
110
+ self.target_channel = 0
111
+
112
+ self.mode = 'direct'
113
+ self.stack_length = len(self.stack)
114
+ self.mid_time = self.stack_length // 2
115
+ self.init_frame = self.stack[self.mid_time,:,:,self.target_channel]
116
+ self.last_frame = self.stack[-1,:,:,self.target_channel]
117
+ else:
118
+ self.mode = 'virtual'
119
+ assert isinstance(self.stack_path, str)
120
+ assert self.stack_path.endswith('.tif')
121
+ self.locate_image_virtual()
122
+
123
+ def locate_image_virtual(self):
124
+ # Locate the stack of images if provided as a file
125
+ self.stack_length = auto_load_number_of_frames(self.stack_path)
126
+ if self.stack_length is None:
127
+ stack = imread(self.stack_path)
128
+ self.stack_length = len(stack)
129
+ del stack
130
+ gc.collect()
131
+
132
+ self.mid_time = self.stack_length // 2
133
+ self.img_num_per_channel = _get_img_num_per_channel(np.arange(self.n_channels), self.stack_length, self.n_channels)
134
+
135
+ self.init_frame = load_frames(self.img_num_per_channel[self.target_channel, self.mid_time],
136
+ self.stack_path,
137
+ normalize_input=False).astype(float)[:,:,0]
138
+ self.last_frame = load_frames(self.img_num_per_channel[self.target_channel, self.stack_length-1],
139
+ self.stack_path,
140
+ normalize_input=False).astype(float)[:,:,0]
141
+
142
+ def generate_figure_canvas(self):
143
+ # Generate the figure canvas for displaying images
144
+
145
+ self.fig, self.ax = plt.subplots(tight_layout=True) #figsize=(5, 5)
146
+ self.canvas = FigureCanvas(self.fig, title=self.window_title, interactive=True)
147
+ self.ax.clear()
148
+ self.im = self.ax.imshow(self.init_frame, cmap='gray', interpolation='none', **self.imshow_kwargs)
149
+ if self.PxToUm is not None:
150
+ scalebar = ScaleBar(self.PxToUm,
151
+ "um",
152
+ length_fraction=0.25,
153
+ location='upper right',
154
+ border_pad=0.4,
155
+ box_alpha=0.95,
156
+ color='white',
157
+ box_color='black',
158
+ )
159
+ if self.PxToUm==1:
160
+ scalebar = ScaleBar(1,
161
+ "px",
162
+ dimension="pixel-length",
163
+ length_fraction=0.25,
164
+ location='upper right',
165
+ border_pad=0.4,
166
+ box_alpha=0.95,
167
+ color='white',
168
+ box_color='black',
169
+ )
170
+ self.ax.add_artist(scalebar)
171
+ self.ax.set_xticks([])
172
+ self.ax.set_yticks([])
173
+ self.fig.set_facecolor('none') # or 'None'
174
+ self.fig.canvas.setStyleSheet(f"background-color: {self.background_color};")
175
+ self.canvas.canvas.draw()
176
+
177
+ def generate_channel_cb(self):
178
+ # Generate the channel dropdown if enabled
179
+
180
+ assert self.channel_names is not None
181
+ assert len(self.channel_names)==self.n_channels
182
+
183
+ channel_layout = QHBoxLayout()
184
+ channel_layout.setContentsMargins(15,0,15,0)
185
+ channel_layout.addWidget(QLabel('Channel: '), 25)
186
+
187
+ self.channels_cb = QComboBox()
188
+ self.channels_cb.addItems(self.channel_names)
189
+ self.channels_cb.currentIndexChanged.connect(self.set_channel_index)
190
+ channel_layout.addWidget(self.channels_cb, 75)
191
+ self.canvas.layout.addLayout(channel_layout)
192
+
193
+ def generate_contrast_slider(self):
194
+ # Generate the contrast slider if enabled
195
+
196
+ self.contrast_slider = QLabeledDoubleRangeSlider()
197
+ contrast_layout = QuickSliderLayout(
198
+ label='Contrast: ',
199
+ slider=self.contrast_slider,
200
+ slider_initial_value=[np.nanpercentile(self.init_frame, 1),np.nanpercentile(self.init_frame, 99.99)],
201
+ slider_range=(np.nanmin(self.init_frame),np.nanmax(self.init_frame)),
202
+ decimal_option=True,
203
+ precision=1.0E-05,
204
+ )
205
+ contrast_layout.setContentsMargins(15,0,15,0)
206
+ self.im.set_clim(vmin=np.nanpercentile(self.init_frame, 1),vmax=np.nanpercentile(self.init_frame, 99.99))
207
+ self.contrast_slider.valueChanged.connect(self.change_contrast)
208
+ self.canvas.layout.addLayout(contrast_layout)
209
+
210
+
211
+
212
+ def generate_frame_slider(self):
213
+ # Generate the frame slider if enabled
214
+
215
+ self.frame_slider = QLabeledSlider()
216
+ frame_layout = QuickSliderLayout(
217
+ label='Frame: ',
218
+ slider=self.frame_slider,
219
+ slider_initial_value=int(self.mid_time),
220
+ slider_range=(0,self.stack_length-1),
221
+ decimal_option=False,
222
+ )
223
+ frame_layout.setContentsMargins(15,0,15,0)
224
+ self.frame_slider.valueChanged.connect(self.change_frame)
225
+ self.canvas.layout.addLayout(frame_layout)
226
+
227
+ def set_target_channel(self, value):
228
+ # Set the target channel
229
+
230
+ self.target_channel = value
231
+ self.change_frame(self.frame_slider.value())
232
+
233
+ def change_contrast(self, value):
234
+ # Change contrast based on slider value
235
+
236
+ vmin = value[0]
237
+ vmax = value[1]
238
+ self.im.set_clim(vmin=vmin, vmax=vmax)
239
+ self.fig.canvas.draw_idle()
240
+
241
+ def set_channel_index(self, value):
242
+ # Set the channel index based on dropdown value
243
+
244
+ self.target_channel = value
245
+ self.init_contrast = True
246
+ if self.mode == 'direct':
247
+ self.last_frame = self.stack[-1,:,:,self.target_channel]
248
+ elif self.mode == 'virtual':
249
+ self.last_frame = load_frames(self.img_num_per_channel[self.target_channel, self.stack_length-1],
250
+ self.stack_path,
251
+ normalize_input=False).astype(float)[:,:,0]
252
+ self.change_frame(self.frame_slider.value())
253
+ self.init_contrast = False
254
+
255
+ def change_frame(self, value):
256
+ # Change the displayed frame based on slider value
257
+
258
+ if self.mode=='virtual':
259
+
260
+ self.init_frame = load_frames(self.img_num_per_channel[self.target_channel, value],
261
+ self.stack_path,
262
+ normalize_input=False
263
+ ).astype(float)[:,:,0]
264
+ elif self.mode=='direct':
265
+ self.init_frame = self.stack[value,:,:,self.target_channel].copy()
266
+
267
+ self.im.set_data(self.init_frame)
268
+
269
+ if self.init_contrast:
270
+ self.im.autoscale()
271
+ I_min, I_max = self.im.get_clim()
272
+ self.contrast_slider.setRange(np.nanmin([self.init_frame,self.last_frame]),np.nanmax([self.init_frame,self.last_frame]))
273
+ self.contrast_slider.setValue((I_min,I_max))
274
+
275
+ if self.create_contrast_slider:
276
+ self.change_contrast(self.contrast_slider.value())
277
+
278
+
279
+ def closeEvent(self, event):
280
+ # Event handler for closing the widget
281
+ self.canvas.close()
282
+
283
+
284
+ class ThresholdedStackVisualizer(StackVisualizer):
285
+
286
+ """
287
+ A widget for visualizing thresholded image stacks with interactive sliders and channel selection.
288
+
289
+ Parameters:
290
+ - preprocessing (list or None): A list of preprocessing filters to apply to the image before thresholding.
291
+ - parent_le: The parent QLineEdit instance to set the threshold value.
292
+ - initial_threshold (float): Initial threshold value.
293
+ - initial_mask_alpha (float): Initial mask opacity value.
294
+ - args, kwargs: Additional arguments to pass to the parent class constructor.
295
+
296
+ Methods:
297
+ - generate_apply_btn(): Generate the apply button to set the threshold in the parent QLineEdit.
298
+ - set_threshold_in_parent_le(): Set the threshold value in the parent QLineEdit.
299
+ - generate_mask_imshow(): Generate the mask imshow.
300
+ - generate_threshold_slider(): Generate the threshold slider.
301
+ - generate_opacity_slider(): Generate the opacity slider for the mask.
302
+ - change_mask_opacity(value): Change the opacity of the mask.
303
+ - change_threshold(value): Change the threshold value.
304
+ - change_frame(value): Change the displayed frame and update the threshold.
305
+ - compute_mask(threshold_value): Compute the mask based on the threshold value.
306
+ - preprocess_image(): Preprocess the image before thresholding.
307
+
308
+ Notes:
309
+ - This class extends the functionality of StackVisualizer to visualize thresholded image stacks
310
+ with interactive sliders for threshold and mask opacity adjustment.
311
+ """
312
+
313
+ def __init__(self, preprocessing=None, parent_le=None, initial_threshold=5, initial_mask_alpha=0.5, *args, **kwargs):
314
+ # Initialize the widget and its attributes
315
+ super().__init__(*args, **kwargs)
316
+ self.preprocessing = preprocessing
317
+ self.thresh = initial_threshold
318
+ self.mask_alpha = initial_mask_alpha
319
+ self.parent_le = parent_le
320
+ self.compute_mask(self.thresh)
321
+ self.generate_mask_imshow()
322
+ self.generate_threshold_slider()
323
+ self.generate_opacity_slider()
324
+ if isinstance(self.parent_le, QLineEdit):
325
+ self.generate_apply_btn()
326
+
327
+ def generate_apply_btn(self):
328
+ # Generate the apply button to set the threshold in the parent QLineEdit
329
+ apply_hbox = QHBoxLayout()
330
+ self.apply_threshold_btn = QPushButton('Apply')
331
+ self.apply_threshold_btn.clicked.connect(self.set_threshold_in_parent_le)
332
+ self.apply_threshold_btn.setStyleSheet(self.button_style_sheet)
333
+ apply_hbox.addWidget(QLabel(''),33)
334
+ apply_hbox.addWidget(self.apply_threshold_btn, 33)
335
+ apply_hbox.addWidget(QLabel(''),33)
336
+ self.canvas.layout.addLayout(apply_hbox)
337
+
338
+ def set_threshold_in_parent_le(self):
339
+ # Set the threshold value in the parent QLineEdit
340
+ self.parent_le.set_threshold(self.threshold_slider.value())
341
+ self.close()
342
+
343
+ def generate_mask_imshow(self):
344
+ # Generate the mask imshow
345
+ self.im_mask = self.ax.imshow(np.ma.masked_where(self.mask==0, self.mask), alpha=self.mask_alpha, interpolation='none')
346
+ self.canvas.canvas.draw()
347
+
348
+ def generate_threshold_slider(self):
349
+ # Generate the threshold slider
350
+ self.threshold_slider = QLabeledDoubleSlider()
351
+ thresh_layout = QuickSliderLayout(label='Threshold: ',
352
+ slider=self.threshold_slider,
353
+ slider_initial_value=self.thresh,
354
+ slider_range=(0,30),
355
+ decimal_option=True,
356
+ precision=1.0E-05,
357
+ )
358
+ thresh_layout.setContentsMargins(15,0,15,0)
359
+ self.threshold_slider.valueChanged.connect(self.change_threshold)
360
+ self.canvas.layout.addLayout(thresh_layout)
361
+
362
+ def generate_opacity_slider(self):
363
+ # Generate the opacity slider for the mask
364
+ self.opacity_slider = QLabeledDoubleSlider()
365
+ opacity_layout = QuickSliderLayout(label='Opacity: ',
366
+ slider=self.opacity_slider,
367
+ slider_initial_value=0.5,
368
+ slider_range=(0,1),
369
+ decimal_option=True,
370
+ precision=1.0E-03
371
+ )
372
+ opacity_layout.setContentsMargins(15,0,15,0)
373
+ self.opacity_slider.valueChanged.connect(self.change_mask_opacity)
374
+ self.canvas.layout.addLayout(opacity_layout)
375
+
376
+ def change_mask_opacity(self, value):
377
+ # Change the opacity of the mask
378
+ self.mask_alpha = value
379
+ self.im_mask.set_alpha(self.mask_alpha)
380
+ self.canvas.canvas.draw_idle()
381
+
382
+ def change_threshold(self, value):
383
+ # Change the threshold value
384
+ self.thresh = value
385
+ self.compute_mask(self.thresh)
386
+ mask = np.ma.masked_where(self.mask == 0, self.mask)
387
+ self.im_mask.set_data(mask)
388
+ self.canvas.canvas.draw_idle()
389
+
390
+ def change_frame(self, value):
391
+ # Change the displayed frame and update the threshold
392
+ super().change_frame(value)
393
+ self.change_threshold(self.threshold_slider.value())
394
+
395
+ def compute_mask(self, threshold_value):
396
+ # Compute the mask based on the threshold value
397
+ self.preprocess_image()
398
+ edge = estimate_unreliable_edge(self.preprocessing)
399
+ self.mask = threshold_image(self.processed_image, threshold_value, 1.0E06, foreground_value=1, edge_exclusion=edge).astype(int)
400
+
401
+ def preprocess_image(self):
402
+ # Preprocess the image before thresholding
403
+ if self.preprocessing is not None:
404
+
405
+ assert isinstance(self.preprocessing, list)
406
+ self.processed_image = filter_image(self.init_frame.copy(),filters=self.preprocessing)
407
+
408
+
409
+ class CellEdgeVisualizer(StackVisualizer):
410
+
411
+ """
412
+ A widget for visualizing cell edges with interactive sliders and channel selection.
413
+
414
+ Parameters:
415
+ - cell_type (str): Type of cells ('effectors' by default).
416
+ - edge_range (tuple): Range of edge sizes (-30, 30) by default.
417
+ - invert (bool): Flag to invert the edge size (False by default).
418
+ - parent_list_widget: The parent QListWidget instance to add edge measurements.
419
+ - parent_le: The parent QLineEdit instance to set the edge size.
420
+ - labels (array or None): Array of labels for cell segmentation.
421
+ - initial_edge (int): Initial edge size (5 by default).
422
+ - initial_mask_alpha (float): Initial mask opacity value (0.5 by default).
423
+ - args, kwargs: Additional arguments to pass to the parent class constructor.
424
+
425
+ Methods:
426
+ - load_labels(): Load the cell labels.
427
+ - locate_labels_virtual(): Locate virtual labels.
428
+ - generate_add_to_list_btn(): Generate the add to list button.
429
+ - generate_add_to_le_btn(): Generate the set measurement button for QLineEdit.
430
+ - set_measurement_in_parent_le(): Set the edge size in the parent QLineEdit.
431
+ - set_measurement_in_parent_list(): Add the edge size to the parent QListWidget.
432
+ - generate_label_imshow(): Generate the label imshow.
433
+ - generate_edge_slider(): Generate the edge size slider.
434
+ - generate_opacity_slider(): Generate the opacity slider for the mask.
435
+ - change_mask_opacity(value): Change the opacity of the mask.
436
+ - change_edge_size(value): Change the edge size.
437
+ - change_frame(value): Change the displayed frame and update the edge labels.
438
+ - compute_edge_labels(): Compute the edge labels.
439
+
440
+ Notes:
441
+ - This class extends the functionality of StackVisualizer to visualize cell edges
442
+ with interactive sliders for edge size adjustment and mask opacity control.
443
+ """
444
+
445
+ 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):
446
+
447
+ # Initialize the widget and its attributes
448
+ super().__init__(*args, **kwargs)
449
+ self.edge_size = initial_edge
450
+ self.mask_alpha = initial_mask_alpha
451
+ self.cell_type = cell_type
452
+ self.labels = labels
453
+ self.edge_range = edge_range
454
+ self.invert = invert
455
+ self.parent_list_widget = parent_list_widget
456
+ self.parent_le = parent_le
457
+
458
+ self.load_labels()
459
+ self.generate_label_imshow()
460
+ self.generate_edge_slider()
461
+ self.generate_opacity_slider()
462
+ if isinstance(self.parent_list_widget, QListWidget):
463
+ self.generate_add_to_list_btn()
464
+ if isinstance(self.parent_le, QLineEdit):
465
+ self.generate_add_to_le_btn()
466
+
467
+ def load_labels(self):
468
+ # Load the cell labels
469
+
470
+ if self.labels is not None:
471
+
472
+ if isinstance(self.labels, list):
473
+ self.labels = np.array(self.labels)
474
+
475
+ assert self.labels.ndim==3,'Wrong dimensions for the provided labels, expect TXY'
476
+ assert len(self.labels)==self.stack_length
477
+
478
+ self.mode = 'direct'
479
+ self.init_label = self.labels[self.mid_time,:,:]
480
+ else:
481
+ self.mode = 'virtual'
482
+ assert isinstance(self.stack_path, str)
483
+ assert self.stack_path.endswith('.tif')
484
+ self.locate_labels_virtual()
485
+
486
+ self.compute_edge_labels()
487
+
488
+ def locate_labels_virtual(self):
489
+ # Locate virtual labels
490
+
491
+ labels_path = str(Path(self.stack_path).parent.parent) + os.sep + f'labels_{self.cell_type}' + os.sep
492
+ self.mask_paths = natsorted(glob(labels_path + '*.tif'))
493
+
494
+ if len(self.mask_paths) == 0:
495
+
496
+ msgBox = QMessageBox()
497
+ msgBox.setIcon(QMessageBox.Critical)
498
+ msgBox.setText("No labels were found for the selected cells. Abort.")
499
+ msgBox.setWindowTitle("Critical")
500
+ msgBox.setStandardButtons(QMessageBox.Ok)
501
+ returnValue = msgBox.exec()
502
+ self.close()
503
+
504
+ self.init_label = imread(self.mask_paths[self.frame_slider.value()])
505
+
506
+ def generate_add_to_list_btn(self):
507
+ # Generate the add to list button
508
+
509
+ add_hbox = QHBoxLayout()
510
+ self.add_measurement_btn = QPushButton('Add measurement')
511
+ self.add_measurement_btn.clicked.connect(self.set_measurement_in_parent_list)
512
+ self.add_measurement_btn.setIcon(icon(MDI6.plus,color="white"))
513
+ self.add_measurement_btn.setIconSize(QSize(20, 20))
514
+ self.add_measurement_btn.setStyleSheet(self.button_style_sheet)
515
+ add_hbox.addWidget(QLabel(''),33)
516
+ add_hbox.addWidget(self.add_measurement_btn, 33)
517
+ add_hbox.addWidget(QLabel(''),33)
518
+ self.canvas.layout.addLayout(add_hbox)
519
+
520
+ def generate_add_to_le_btn(self):
521
+ # Generate the set measurement button for QLineEdit
522
+
523
+ add_hbox = QHBoxLayout()
524
+ self.set_measurement_btn = QPushButton('Set')
525
+ self.set_measurement_btn.clicked.connect(self.set_measurement_in_parent_le)
526
+ self.set_measurement_btn.setStyleSheet(self.button_style_sheet)
527
+ add_hbox.addWidget(QLabel(''),33)
528
+ add_hbox.addWidget(self.set_measurement_btn, 33)
529
+ add_hbox.addWidget(QLabel(''),33)
530
+ self.canvas.layout.addLayout(add_hbox)
531
+
532
+ def set_measurement_in_parent_le(self):
533
+ # Set the edge size in the parent QLineEdit
534
+
535
+ self.parent_le.setText(str(int(self.edge_slider.value())))
536
+ self.close()
537
+
538
+ def set_measurement_in_parent_list(self):
539
+ # Add the edge size to the parent QListWidget
540
+
541
+ self.parent_list_widget.addItems([str(self.edge_slider.value())])
542
+ self.close()
543
+
544
+ def generate_label_imshow(self):
545
+ # Generate the label imshow
546
+
547
+ 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")
548
+ self.canvas.canvas.draw()
549
+
550
+ def generate_edge_slider(self):
551
+ # Generate the edge size slider
552
+
553
+ self.edge_slider = QLabeledSlider()
554
+ edge_layout = QuickSliderLayout(label='Edge: ',
555
+ slider=self.edge_slider,
556
+ slider_initial_value=self.edge_size,
557
+ slider_range=self.edge_range,
558
+ decimal_option=False,
559
+ )
560
+ edge_layout.setContentsMargins(15,0,15,0)
561
+ self.edge_slider.valueChanged.connect(self.change_edge_size)
562
+ self.canvas.layout.addLayout(edge_layout)
563
+
564
+ def generate_opacity_slider(self):
565
+ # Generate the opacity slider for the mask
566
+
567
+ self.opacity_slider = QLabeledDoubleSlider()
568
+ opacity_layout = QuickSliderLayout(label='Opacity: ',
569
+ slider=self.opacity_slider,
570
+ slider_initial_value=0.5,
571
+ slider_range=(0,1),
572
+ decimal_option=True,
573
+ precision=1.0E-03
574
+ )
575
+ opacity_layout.setContentsMargins(15,0,15,0)
576
+ self.opacity_slider.valueChanged.connect(self.change_mask_opacity)
577
+ self.canvas.layout.addLayout(opacity_layout)
578
+
579
+ def change_mask_opacity(self, value):
580
+ # Change the opacity of the mask
581
+
582
+ self.mask_alpha = value
583
+ self.im_mask.set_alpha(self.mask_alpha)
584
+ self.canvas.canvas.draw_idle()
585
+
586
+ def change_edge_size(self, value):
587
+ # Change the edge size
588
+
589
+ self.edge_size = value
590
+ self.compute_edge_labels()
591
+ mask = np.ma.masked_where(self.edge_labels == 0, self.edge_labels)
592
+ self.im_mask.set_data(mask)
593
+ self.canvas.canvas.draw_idle()
594
+
595
+ def change_frame(self, value):
596
+ # Change the displayed frame and update the edge labels
597
+
598
+ super().change_frame(value)
599
+
600
+ if self.mode=='virtual':
601
+ self.init_label = imread(self.mask_paths[value])
602
+ elif self.mode=='direct':
603
+ self.init_label = self.labels[value,:,:]
604
+
605
+ self.compute_edge_labels()
606
+ mask = np.ma.masked_where(self.edge_labels == 0, self.edge_labels)
607
+ self.im_mask.set_data(mask)
608
+
609
+ def compute_edge_labels(self):
610
+ # Compute the edge labels
611
+
612
+ if self.invert:
613
+ edge_size = - self.edge_size
614
+ else:
615
+ edge_size = self.edge_size
616
+
617
+ self.edge_labels = contour_of_instance_segmentation(self.init_label, edge_size)
618
+
619
+ class CellSizeViewer(StackVisualizer):
620
+
621
+ """
622
+ A widget for visualizing cell size with interactive sliders and circle display.
623
+
624
+ Parameters:
625
+ - initial_diameter (int): Initial diameter of the circle (40 by default).
626
+ - set_radius_in_list (bool): Flag to set radius instead of diameter in the list (False by default).
627
+ - diameter_slider_range (tuple): Range of the diameter slider (0, 200) by default.
628
+ - parent_le: The parent QLineEdit instance to set the diameter.
629
+ - parent_list_widget: The parent QListWidget instance to add diameter measurements.
630
+ - args, kwargs: Additional arguments to pass to the parent class constructor.
631
+
632
+ Methods:
633
+ - generate_circle(): Generate the circle for visualization.
634
+ - generate_add_to_list_btn(): Generate the add to list button.
635
+ - set_measurement_in_parent_list(): Add the diameter to the parent QListWidget.
636
+ - on_xlims_or_ylims_change(event_ax): Update the circle position on axis limits change.
637
+ - generate_set_btn(): Generate the set button for QLineEdit.
638
+ - set_threshold_in_parent_le(): Set the diameter in the parent QLineEdit.
639
+ - generate_diameter_slider(): Generate the diameter slider.
640
+ - change_diameter(value): Change the diameter of the circle.
641
+
642
+ Notes:
643
+ - This class extends the functionality of StackVisualizer to visualize cell size
644
+ with interactive sliders for diameter adjustment and circle display.
645
+ """
646
+
647
+ def __init__(self, initial_diameter=40, set_radius_in_list=False, diameter_slider_range=(0,200), parent_le=None, parent_list_widget=None, *args, **kwargs):
648
+ # Initialize the widget and its attributes
649
+
650
+ super().__init__(*args, **kwargs)
651
+ self.diameter = initial_diameter
652
+ self.parent_le = parent_le
653
+ self.diameter_slider_range = diameter_slider_range
654
+ self.parent_list_widget = parent_list_widget
655
+ self.set_radius_in_list = set_radius_in_list
656
+ self.generate_circle()
657
+ self.generate_diameter_slider()
658
+
659
+ if isinstance(self.parent_le, QLineEdit):
660
+ self.generate_set_btn()
661
+ if isinstance(self.parent_list_widget, QListWidget):
662
+ self.generate_add_to_list_btn()
663
+
664
+ def generate_circle(self):
665
+ # Generate the circle for visualization
666
+
667
+ self.circ = plt.Circle((self.init_frame.shape[0]//2,self.init_frame.shape[1]//2), self.diameter//2, ec="tab:red",fill=False)
668
+ self.ax.add_patch(self.circ)
669
+
670
+ self.ax.callbacks.connect('xlim_changed',self.on_xlims_or_ylims_change)
671
+ self.ax.callbacks.connect('ylim_changed', self.on_xlims_or_ylims_change)
672
+
673
+ def generate_add_to_list_btn(self):
674
+ # Generate the add to list button
675
+
676
+ add_hbox = QHBoxLayout()
677
+ self.add_measurement_btn = QPushButton('Add measurement')
678
+ self.add_measurement_btn.clicked.connect(self.set_measurement_in_parent_list)
679
+ self.add_measurement_btn.setIcon(icon(MDI6.plus,color="white"))
680
+ self.add_measurement_btn.setIconSize(QSize(20, 20))
681
+ self.add_measurement_btn.setStyleSheet(self.button_style_sheet)
682
+ add_hbox.addWidget(QLabel(''),33)
683
+ add_hbox.addWidget(self.add_measurement_btn, 33)
684
+ add_hbox.addWidget(QLabel(''),33)
685
+ self.canvas.layout.addLayout(add_hbox)
686
+
687
+ def set_measurement_in_parent_list(self):
688
+ # Add the diameter to the parent QListWidget
689
+
690
+ if self.set_radius_in_list:
691
+ val = int(self.diameter_slider.value()//2)
692
+ else:
693
+ val = int(self.diameter_slider.value())
694
+
695
+ self.parent_list_widget.addItems([str(val)])
696
+ self.close()
697
+
698
+ def on_xlims_or_ylims_change(self, event_ax):
699
+ # Update the circle position on axis limits change
700
+
701
+ xmin,xmax = event_ax.get_xlim()
702
+ ymin,ymax = event_ax.get_ylim()
703
+ self.circ.center = np.mean([xmin,xmax]), np.mean([ymin,ymax])
704
+
705
+ def generate_set_btn(self):
706
+ # Generate the set button for QLineEdit
707
+
708
+ apply_hbox = QHBoxLayout()
709
+ self.apply_threshold_btn = QPushButton('Set')
710
+ self.apply_threshold_btn.clicked.connect(self.set_threshold_in_parent_le)
711
+ self.apply_threshold_btn.setStyleSheet(self.button_style_sheet)
712
+ apply_hbox.addWidget(QLabel(''),33)
713
+ apply_hbox.addWidget(self.apply_threshold_btn, 33)
714
+ apply_hbox.addWidget(QLabel(''),33)
715
+ self.canvas.layout.addLayout(apply_hbox)
716
+
717
+ def set_threshold_in_parent_le(self):
718
+ # Set the diameter in the parent QLineEdit
719
+
720
+ self.parent_le.set_threshold(self.diameter_slider.value())
721
+ self.close()
722
+
723
+ def generate_diameter_slider(self):
724
+ # Generate the diameter slider
725
+
726
+ self.diameter_slider = QLabeledDoubleSlider()
727
+ diameter_layout = QuickSliderLayout(label='Diameter: ',
728
+ slider=self.diameter_slider,
729
+ slider_initial_value=self.diameter,
730
+ slider_range=self.diameter_slider_range,
731
+ decimal_option=True,
732
+ precision=1.0E-05,
733
+ )
734
+ diameter_layout.setContentsMargins(15,0,15,0)
735
+ self.diameter_slider.valueChanged.connect(self.change_diameter)
736
+ self.canvas.layout.addLayout(diameter_layout)
737
+
738
+ def change_diameter(self, value):
739
+ # Change the diameter of the circle
740
+
741
+ self.diameter = value
742
+ self.circ.set_radius(self.diameter//2)
743
+ self.canvas.canvas.draw_idle()