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
@@ -0,0 +1,394 @@
1
+ import os
2
+ from glob import glob
3
+ from pathlib import Path
4
+
5
+ import numpy as np
6
+ from PyQt5.QtCore import QSize, Qt
7
+ from PyQt5.QtWidgets import (
8
+ QListWidget,
9
+ QLineEdit,
10
+ QMessageBox,
11
+ QHBoxLayout,
12
+ QPushButton,
13
+ QLabel,
14
+ )
15
+ from fonticon_mdi6 import MDI6
16
+ from natsort import natsorted
17
+ from superqt import QLabeledSlider, QLabeledDoubleSlider, QLabeledRangeSlider
18
+ from superqt.fonticon import icon
19
+ from collections import OrderedDict
20
+
21
+ from celldetective.gui.gui_utils import QuickSliderLayout
22
+ from celldetective.gui.viewers.base_viewer import StackVisualizer
23
+ from celldetective import get_logger
24
+ from tifffile import imread
25
+ import re
26
+
27
+ logger = get_logger(__name__)
28
+
29
+
30
+ class CellEdgeVisualizer(StackVisualizer):
31
+ """
32
+ A widget for visualizing cell edges with interactive sliders and channel selection.
33
+
34
+ Parameters:
35
+ - cell_type (str): Type of cells ('effectors' by default).
36
+ - edge_range (tuple): Range of edge sizes (-30, 30) by default.
37
+ - invert (bool): Flag to invert the edge size (False by default).
38
+ - parent_list_widget: The parent QListWidget instance to add edge measurements.
39
+ - parent_le: The parent QLineEdit instance to set the edge size.
40
+ - labels (array or None): Array of labels for cell segmentation.
41
+ - initial_edge (int): Initial edge size (5 by default).
42
+ - initial_mask_alpha (float): Initial mask opacity value (0.5 by default).
43
+ - args, kwargs: Additional arguments to pass to the parent class constructor.
44
+
45
+ Methods:
46
+ - load_labels(): Load the cell labels.
47
+ - locate_labels_virtual(): Locate virtual labels.
48
+ - generate_add_to_list_btn(): Generate the add to list button.
49
+ - generate_add_to_le_btn(): Generate the set measurement button for QLineEdit.
50
+ - set_measurement_in_parent_le(): Set the edge size in the parent QLineEdit.
51
+ - set_measurement_in_parent_list(): Add the edge size to the parent QListWidget.
52
+ - generate_label_imshow(): Generate the label imshow.
53
+ - generate_edge_slider(): Generate the edge size slider.
54
+ - generate_opacity_slider(): Generate the opacity slider for the mask.
55
+ - change_mask_opacity(value): Change the opacity of the mask.
56
+ - change_edge_size(value): Change the edge size.
57
+ - change_frame(value): Change the displayed frame and update the edge labels.
58
+ - compute_edge_labels(): Compute the edge labels.
59
+
60
+ Notes:
61
+ - This class extends the functionality of StackVisualizer to visualize cell edges
62
+ with interactive sliders for edge size adjustment and mask opacity control.
63
+ """
64
+
65
+ def __init__(
66
+ self,
67
+ cell_type="effectors",
68
+ edge_range=(-30, 30),
69
+ invert=False,
70
+ parent_list_widget=None,
71
+ parent_le=None,
72
+ labels=None,
73
+ initial_edge=5,
74
+ initial_mask_alpha=0.5,
75
+ *args,
76
+ **kwargs,
77
+ ):
78
+
79
+ # Initialize the widget and its attributes
80
+ super().__init__(*args, **kwargs)
81
+ self.edge_size = initial_edge
82
+ self.mask_alpha = initial_mask_alpha
83
+ self.cell_type = cell_type
84
+ self.labels = labels
85
+ self.edge_range = edge_range
86
+ self.invert = invert
87
+ self.parent_list_widget = parent_list_widget
88
+ self.parent_le = parent_le
89
+
90
+ # SDF cache (stores label + dist_in + dist_out + voronoi)
91
+ self.sdf_cache = OrderedDict()
92
+ self.max_sdf_cache_size = 128
93
+
94
+ self.load_labels()
95
+ self.generate_label_imshow()
96
+ self.generate_edge_slider()
97
+ self.generate_opacity_slider()
98
+ if isinstance(self.parent_list_widget, QListWidget):
99
+ self.generate_add_to_list_btn()
100
+ if isinstance(self.parent_le, QLineEdit):
101
+ self.generate_add_to_le_btn()
102
+
103
+ def closeEvent(self, event):
104
+ if hasattr(self, "sdf_cache") and isinstance(self.sdf_cache, OrderedDict):
105
+ self.sdf_cache.clear()
106
+ super().closeEvent(event)
107
+
108
+ def load_labels(self):
109
+ # Load the cell labels
110
+
111
+ if self.labels is not None:
112
+
113
+ if isinstance(self.labels, list):
114
+ self.labels = np.array(self.labels)
115
+
116
+ assert (
117
+ self.labels.ndim == 3
118
+ ), "Wrong dimensions for the provided labels, expect TXY"
119
+ assert len(self.labels) == self.stack_length
120
+
121
+ self.mode = "direct"
122
+ self.init_label = self.labels[self.mid_time, :, :]
123
+ else:
124
+ self.mode = "virtual"
125
+ assert isinstance(self.stack_path, str)
126
+ assert self.stack_path.endswith(".tif")
127
+ self.locate_labels_virtual()
128
+
129
+ self.compute_edge_labels()
130
+
131
+ def locate_labels_virtual(self):
132
+ # Locate virtual labels
133
+
134
+ labels_path = (
135
+ str(Path(self.stack_path).parent.parent)
136
+ + os.sep
137
+ + f"labels_{self.cell_type}"
138
+ + os.sep
139
+ )
140
+ self.mask_paths = natsorted(glob(labels_path + "*.tif"))
141
+
142
+ if len(self.mask_paths) == 0:
143
+
144
+ msgBox = QMessageBox()
145
+ msgBox.setIcon(QMessageBox.Critical)
146
+ msgBox.setText("No labels were found for the selected cells. Abort.")
147
+ msgBox.setWindowTitle("Critical")
148
+ msgBox.setStandardButtons(QMessageBox.Ok)
149
+ returnValue = msgBox.exec()
150
+ self.close()
151
+
152
+ self.label_map = {}
153
+ for path in self.mask_paths:
154
+ # Find the last number in the filename
155
+ match = re.findall(r"(\d+)", Path(path).stem)
156
+ if match:
157
+ # Assume the last number is the frame index (Use direct mapping: 0000 -> 0)
158
+ frame_num = int(match[-1])
159
+ self.label_map[frame_num] = path
160
+
161
+ current_frame = self.frame_slider.value()
162
+ if current_frame in self.label_map:
163
+ self.init_label = imread(self.label_map[current_frame])
164
+ else:
165
+ if hasattr(self, "init_frame"):
166
+ self.init_label = np.zeros_like(self.init_frame)
167
+ else:
168
+ self.init_label = imread(self.mask_paths[0])
169
+
170
+ def generate_add_to_list_btn(self):
171
+ # Generate the add to list button
172
+
173
+ add_hbox = QHBoxLayout()
174
+ self.add_measurement_btn = QPushButton("Add measurement")
175
+ self.add_measurement_btn.clicked.connect(self.set_measurement_in_parent_list)
176
+ self.add_measurement_btn.setIcon(icon(MDI6.plus, color="white"))
177
+ self.add_measurement_btn.setIconSize(QSize(20, 20))
178
+ self.add_measurement_btn.setStyleSheet(self.button_style_sheet)
179
+ add_hbox.addWidget(QLabel(""), 33)
180
+ add_hbox.addWidget(self.add_measurement_btn, 33)
181
+ add_hbox.addWidget(QLabel(""), 33)
182
+ self.canvas.layout.addLayout(add_hbox)
183
+
184
+ def generate_add_to_le_btn(self):
185
+ # Generate the set measurement button for QLineEdit
186
+
187
+ add_hbox = QHBoxLayout()
188
+ self.set_measurement_btn = QPushButton("Set")
189
+ self.set_measurement_btn.clicked.connect(self.set_measurement_in_parent_le)
190
+ self.set_measurement_btn.setStyleSheet(self.button_style_sheet)
191
+ add_hbox.addWidget(QLabel(""), 33)
192
+ add_hbox.addWidget(self.set_measurement_btn, 33)
193
+ add_hbox.addWidget(QLabel(""), 33)
194
+ self.canvas.layout.addLayout(add_hbox)
195
+
196
+ def set_measurement_in_parent_le(self):
197
+ # Set the edge size in the parent QLineEdit
198
+
199
+ self.parent_le.setText(str(int(self.edge_slider.value())))
200
+ self.close()
201
+
202
+ def set_measurement_in_parent_list(self):
203
+ # Add the edge size to the parent QListWidget
204
+
205
+ self.parent_list_widget.addItems([str(self.edge_slider.value())])
206
+ self.close()
207
+
208
+ def generate_label_imshow(self):
209
+ # Generate the label imshow
210
+
211
+ self.im_mask = self.ax.imshow(
212
+ np.ma.masked_where(self.edge_labels == 0, self.edge_labels),
213
+ alpha=self.mask_alpha,
214
+ interpolation="none",
215
+ cmap="viridis",
216
+ )
217
+ self.canvas.canvas.draw()
218
+
219
+ def generate_edge_slider(self):
220
+ # Generate the edge size slider using a Range Slider
221
+
222
+ edge_layout = QHBoxLayout()
223
+ edge_layout.setContentsMargins(15, 0, 15, 0)
224
+
225
+ # Determine range for universal slider (symmetric)
226
+ max_range = (
227
+ self.edge_range[1]
228
+ if hasattr(self, "edge_range") and self.edge_range
229
+ else 30
230
+ )
231
+ slider_min, slider_max = -max_range, max_range
232
+
233
+ self.edge_slider = QLabeledRangeSlider(Qt.Horizontal)
234
+ self.edge_slider.setRange(slider_min, slider_max)
235
+
236
+ # Initial Value: convert scalar edge_size to range
237
+ # precise initial state might need adjustment, defaulting to [0, edge_size] if positive
238
+ init_val = self.edge_size if hasattr(self, "edge_size") else 1
239
+ if init_val >= 0:
240
+ self.edge_slider.setValue((0, init_val))
241
+ else:
242
+ self.edge_slider.setValue((init_val, 0))
243
+
244
+ self.edge_slider.setEdgeLabelMode(QLabeledRangeSlider.EdgeLabelMode.NoLabel)
245
+ self.edge_slider.valueChanged.connect(self.change_edge_size)
246
+
247
+ edge_layout.addWidget(QLabel("Edge: "), 15)
248
+ edge_layout.addWidget(self.edge_slider, 85)
249
+
250
+ self.canvas.layout.addLayout(edge_layout)
251
+
252
+ def generate_opacity_slider(self):
253
+ # Generate the opacity slider for the mask
254
+
255
+ self.opacity_slider = QLabeledDoubleSlider()
256
+ opacity_layout = QuickSliderLayout(
257
+ label="Opacity: ",
258
+ slider=self.opacity_slider,
259
+ slider_initial_value=0.5,
260
+ slider_range=(0, 1),
261
+ decimal_option=True,
262
+ precision=3,
263
+ )
264
+ opacity_layout.setContentsMargins(15, 0, 15, 0)
265
+ self.opacity_slider.valueChanged.connect(self.change_mask_opacity)
266
+ self.canvas.layout.addLayout(opacity_layout)
267
+
268
+ def change_mask_opacity(self, value):
269
+ # Change the opacity of the mask
270
+
271
+ self.mask_alpha = value
272
+ self.im_mask.set_alpha(self.mask_alpha)
273
+ self.canvas.canvas.draw_idle()
274
+
275
+ def change_edge_size(self, value):
276
+ self.edge_size = value # Tuple (min, max)
277
+ self.compute_edge_labels()
278
+ mask = np.ma.masked_where(self.edge_labels == 0, self.edge_labels)
279
+ self.im_mask.set_data(mask)
280
+ self.canvas.canvas.draw_idle()
281
+
282
+ def change_frame(self, value):
283
+ # Change the displayed frame and update the edge labels
284
+
285
+ super().change_frame(value)
286
+
287
+ # Check unified cache first
288
+ if hasattr(self, "sdf_cache") and value in self.sdf_cache:
289
+ self.init_label, self.dist_in, self.dist_out, self.voronoi_map = (
290
+ self.sdf_cache[value]
291
+ )
292
+ self.sdf_cache.move_to_end(value)
293
+ else:
294
+ # Cache Miss: Load Label
295
+ if self.mode == "virtual":
296
+ if hasattr(self, "label_map") and value in self.label_map:
297
+ try:
298
+ self.init_label = imread(self.label_map[value])
299
+ except Exception:
300
+ self.init_label = np.zeros_like(self.init_frame)
301
+ else:
302
+ self.init_label = np.zeros_like(self.init_frame)
303
+ elif self.mode == "direct":
304
+ if value < len(self.labels):
305
+ self.init_label = self.labels[value, :, :]
306
+ else:
307
+ self.init_label = np.zeros_like(self.init_frame)
308
+
309
+ # Compute SDFs
310
+ self.update_sdf()
311
+
312
+ # Store in unified cache
313
+
314
+ if hasattr(self, "sdf_cache"):
315
+ self.sdf_cache[value] = (
316
+ self.init_label,
317
+ self.dist_in,
318
+ self.dist_out,
319
+ self.voronoi_map,
320
+ )
321
+ if len(self.sdf_cache) > self.max_sdf_cache_size:
322
+ self.sdf_cache.popitem(last=False)
323
+
324
+ self.compute_edge_labels()
325
+ mask = np.ma.masked_where(self.edge_labels == 0, self.edge_labels)
326
+ self.im_mask.set_data(mask)
327
+
328
+ def update_sdf(self):
329
+ # Compute Signed Distance Functions (SDFs) and Voronoi map for the current labels
330
+ from scipy.ndimage import distance_transform_edt
331
+
332
+ # Ensure init_label is not empty
333
+ if self.init_label is None or self.init_label.size == 0:
334
+ self.dist_in = np.zeros_like(self.init_label, dtype=float)
335
+ self.dist_out = np.zeros_like(self.init_label, dtype=float)
336
+ self.voronoi_map = np.zeros_like(self.init_label, dtype=int)
337
+ return
338
+
339
+ # Inner Distance
340
+ # Distance to background. Touching cells might merge, but this matches legacy behavior
341
+ # and is much faster than looping.
342
+ self.dist_in = distance_transform_edt(self.init_label > 0)
343
+
344
+ # Outer Distance & Voronoi Map
345
+ # Distance to boundaries from outside.
346
+ # return_indices=True gives us the coordinates of the nearest foreground pixel for every background pixel.
347
+ # We use this to look up the label of the nearest object (Voronoi tessellation).
348
+ self.dist_out, indices = distance_transform_edt(
349
+ self.init_label == 0, return_indices=True
350
+ )
351
+ self.voronoi_map = self.init_label[indices[0], indices[1]]
352
+
353
+ def compute_edge_labels(self):
354
+ # Compute the edge labels using Composite SDF and Range Masking
355
+ # Delegates to the unified utility in celldetective.utils
356
+
357
+ # Ensure SDFs exist
358
+ if not hasattr(self, "dist_in"):
359
+ self.update_sdf()
360
+
361
+ # Ensure array shapes
362
+ if self.dist_in.shape != self.init_label.shape:
363
+ self.edge_labels = np.zeros_like(self.init_label)
364
+ return
365
+
366
+ # Composite SDF
367
+ sdf = self.dist_in - self.dist_out
368
+
369
+ # Use unified utility
370
+ from celldetective.utils.masks import contour_of_instance_segmentation
371
+
372
+ self.edge_labels = contour_of_instance_segmentation(
373
+ self.init_label,
374
+ distance=self.edge_size,
375
+ sdf=sdf,
376
+ voronoi_map=self.voronoi_map,
377
+ )
378
+
379
+ # Handle scalar edge_size (fallback or initial init)
380
+ if isinstance(self.edge_size, (int, float)):
381
+ if self.edge_size >= 0:
382
+ r_min, r_max = 0, self.edge_size
383
+ else:
384
+ r_min, r_max = self.edge_size, 0
385
+ else:
386
+ r_min, r_max = self.edge_size
387
+
388
+ # Create Mask [r_min, r_max]
389
+ # Example: [2, 5] -> Stripe inside. [-5, -2] -> Stripe outside. [-2, 2] -> Border crossing.
390
+ mask = (sdf >= r_min) & (sdf <= r_max)
391
+
392
+ # Apply Instance Identities using Voronoi Map
393
+ # Note: Voronoi map covers entire space.
394
+ self.edge_labels = self.voronoi_map * mask
@@ -0,0 +1,153 @@
1
+ import numpy as np
2
+ from PyQt5.QtCore import QSize
3
+ from PyQt5.QtWidgets import QLineEdit, QListWidget, QHBoxLayout, QPushButton, QLabel
4
+ from fonticon_mdi6 import MDI6
5
+ from superqt import QLabeledDoubleSlider
6
+ from superqt.fonticon import icon
7
+
8
+ from celldetective.gui.gui_utils import QuickSliderLayout
9
+ from celldetective.gui.viewers.base_viewer import StackVisualizer
10
+ from celldetective import get_logger
11
+
12
+ logger = get_logger(__name__)
13
+
14
+ class CellSizeViewer(StackVisualizer):
15
+ """
16
+ A widget for visualizing cell size with interactive sliders and circle display.
17
+
18
+ Parameters:
19
+ - initial_diameter (int): Initial diameter of the circle (40 by default).
20
+ - set_radius_in_list (bool): Flag to set radius instead of diameter in the list (False by default).
21
+ - diameter_slider_range (tuple): Range of the diameter slider (0, 200) by default.
22
+ - parent_le: The parent QLineEdit instance to set the diameter.
23
+ - parent_list_widget: The parent QListWidget instance to add diameter measurements.
24
+ - args, kwargs: Additional arguments to pass to the parent class constructor.
25
+
26
+ Methods:
27
+ - generate_circle(): Generate the circle for visualization.
28
+ - generate_add_to_list_btn(): Generate the add to list button.
29
+ - set_measurement_in_parent_list(): Add the diameter to the parent QListWidget.
30
+ - on_xlims_or_ylims_change(event_ax): Update the circle position on axis limits change.
31
+ - generate_set_btn(): Generate the set button for QLineEdit.
32
+ - set_threshold_in_parent_le(): Set the diameter in the parent QLineEdit.
33
+ - generate_diameter_slider(): Generate the diameter slider.
34
+ - change_diameter(value): Change the diameter of the circle.
35
+
36
+ Notes:
37
+ - This class extends the functionality of StackVisualizer to visualize cell size
38
+ with interactive sliders for diameter adjustment and circle display.
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ initial_diameter=40,
44
+ set_radius_in_list=False,
45
+ diameter_slider_range=(0, 500),
46
+ parent_le=None,
47
+ parent_list_widget=None,
48
+ *args,
49
+ **kwargs,
50
+ ):
51
+ # Initialize the widget and its attributes
52
+
53
+ super().__init__(*args, **kwargs)
54
+ self.diameter = initial_diameter
55
+ self.parent_le = parent_le
56
+ self.diameter_slider_range = diameter_slider_range
57
+ self.parent_list_widget = parent_list_widget
58
+ self.set_radius_in_list = set_radius_in_list
59
+ self.generate_circle()
60
+ self.generate_diameter_slider()
61
+
62
+ if isinstance(self.parent_le, QLineEdit):
63
+ self.generate_set_btn()
64
+ if isinstance(self.parent_list_widget, QListWidget):
65
+ self.generate_add_to_list_btn()
66
+
67
+ def generate_circle(self):
68
+ # Generate the circle for visualization
69
+
70
+ import matplotlib.pyplot as plt
71
+
72
+ self.circ = plt.Circle(
73
+ (self.init_frame.shape[0] // 2, self.init_frame.shape[1] // 2),
74
+ self.diameter // 2 / self.PxToUm,
75
+ ec="tab:red",
76
+ fill=False,
77
+ )
78
+ self.ax.add_patch(self.circ)
79
+
80
+ self.ax.callbacks.connect("xlim_changed", self.on_xlims_or_ylims_change)
81
+ self.ax.callbacks.connect("ylim_changed", self.on_xlims_or_ylims_change)
82
+
83
+ def generate_add_to_list_btn(self):
84
+ # Generate the add to list button
85
+
86
+ add_hbox = QHBoxLayout()
87
+ self.add_measurement_btn = QPushButton("Add measurement")
88
+ self.add_measurement_btn.clicked.connect(self.set_measurement_in_parent_list)
89
+ self.add_measurement_btn.setIcon(icon(MDI6.plus, color="white"))
90
+ self.add_measurement_btn.setIconSize(QSize(20, 20))
91
+ self.add_measurement_btn.setStyleSheet(self.button_style_sheet)
92
+ add_hbox.addWidget(QLabel(""), 33)
93
+ add_hbox.addWidget(self.add_measurement_btn, 33)
94
+ add_hbox.addWidget(QLabel(""), 33)
95
+ self.canvas.layout.addLayout(add_hbox)
96
+
97
+ def set_measurement_in_parent_list(self):
98
+ # Add the diameter to the parent QListWidget
99
+
100
+ if self.set_radius_in_list:
101
+ val = int(self.diameter_slider.value() // 2)
102
+ else:
103
+ val = int(self.diameter_slider.value())
104
+
105
+ self.parent_list_widget.addItems([str(val)])
106
+ self.close()
107
+
108
+ def on_xlims_or_ylims_change(self, event_ax):
109
+ # Update the circle position on axis limits change
110
+
111
+ xmin, xmax = event_ax.get_xlim()
112
+ ymin, ymax = event_ax.get_ylim()
113
+ self.circ.center = np.mean([xmin, xmax]), np.mean([ymin, ymax])
114
+
115
+ def generate_set_btn(self):
116
+ # Generate the set button for QLineEdit
117
+
118
+ apply_hbox = QHBoxLayout()
119
+ self.apply_threshold_btn = QPushButton("Set")
120
+ self.apply_threshold_btn.clicked.connect(self.set_threshold_in_parent_le)
121
+ self.apply_threshold_btn.setStyleSheet(self.button_style_sheet)
122
+ apply_hbox.addWidget(QLabel(""), 33)
123
+ apply_hbox.addWidget(self.apply_threshold_btn, 33)
124
+ apply_hbox.addWidget(QLabel(""), 33)
125
+ self.canvas.layout.addLayout(apply_hbox)
126
+
127
+ def set_threshold_in_parent_le(self):
128
+ # Set the diameter in the parent QLineEdit
129
+
130
+ self.parent_le.set_threshold(self.diameter_slider.value())
131
+ self.close()
132
+
133
+ def generate_diameter_slider(self):
134
+ # Generate the diameter slider
135
+
136
+ self.diameter_slider = QLabeledDoubleSlider()
137
+ diameter_layout = QuickSliderLayout(
138
+ label="Diameter: ",
139
+ slider=self.diameter_slider,
140
+ slider_initial_value=self.diameter,
141
+ slider_range=self.diameter_slider_range,
142
+ decimal_option=True,
143
+ precision=5,
144
+ )
145
+ diameter_layout.setContentsMargins(15, 0, 15, 0)
146
+ self.diameter_slider.valueChanged.connect(self.change_diameter)
147
+ self.canvas.layout.addLayout(diameter_layout)
148
+
149
+ def change_diameter(self, value):
150
+ # Change the diameter of the circle
151
+ self.diameter = value
152
+ self.circ.set_radius(self.diameter // 2 / self.PxToUm)
153
+ self.canvas.canvas.draw_idle()