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.
- celldetective/__main__.py +7 -21
- celldetective/events.py +2 -44
- celldetective/extra_properties.py +62 -52
- celldetective/filters.py +4 -5
- celldetective/gui/__init__.py +1 -1
- celldetective/gui/analyze_block.py +37 -10
- celldetective/gui/btrack_options.py +24 -23
- celldetective/gui/classifier_widget.py +62 -19
- celldetective/gui/configure_new_exp.py +32 -35
- celldetective/gui/control_panel.py +120 -81
- celldetective/gui/gui_utils.py +674 -396
- celldetective/gui/json_readers.py +7 -6
- celldetective/gui/layouts.py +756 -0
- celldetective/gui/measurement_options.py +98 -513
- celldetective/gui/neighborhood_options.py +322 -270
- celldetective/gui/plot_measurements.py +1114 -0
- celldetective/gui/plot_signals_ui.py +21 -20
- celldetective/gui/process_block.py +449 -169
- celldetective/gui/retrain_segmentation_model_options.py +27 -26
- celldetective/gui/retrain_signal_model_options.py +25 -24
- celldetective/gui/seg_model_loader.py +31 -27
- celldetective/gui/signal_annotator.py +2326 -2295
- celldetective/gui/signal_annotator_options.py +18 -16
- celldetective/gui/styles.py +16 -1
- celldetective/gui/survival_ui.py +67 -39
- celldetective/gui/tableUI.py +337 -48
- celldetective/gui/thresholds_gui.py +75 -71
- celldetective/gui/viewers.py +743 -0
- celldetective/io.py +247 -27
- celldetective/measure.py +43 -263
- celldetective/models/segmentation_effectors/primNK_cfse/config_input.json +29 -0
- celldetective/models/segmentation_effectors/primNK_cfse/cp-cfse-transfer +0 -0
- celldetective/models/segmentation_effectors/primNK_cfse/training_instructions.json +37 -0
- celldetective/neighborhood.py +498 -27
- celldetective/preprocessing.py +1023 -0
- celldetective/scripts/analyze_signals.py +7 -0
- celldetective/scripts/measure_cells.py +12 -0
- celldetective/scripts/segment_cells.py +20 -4
- celldetective/scripts/track_cells.py +11 -0
- celldetective/scripts/train_segmentation_model.py +35 -34
- celldetective/segmentation.py +14 -9
- celldetective/signals.py +234 -329
- celldetective/tracking.py +2 -2
- celldetective/utils.py +602 -49
- celldetective-1.1.1.dist-info/METADATA +305 -0
- celldetective-1.1.1.dist-info/RECORD +84 -0
- {celldetective-1.0.2.post1.dist-info → celldetective-1.1.1.dist-info}/top_level.txt +1 -0
- tests/__init__.py +0 -0
- tests/test_events.py +28 -0
- tests/test_filters.py +24 -0
- tests/test_io.py +70 -0
- tests/test_measure.py +141 -0
- tests/test_neighborhood.py +70 -0
- tests/test_preprocessing.py +37 -0
- tests/test_segmentation.py +93 -0
- tests/test_signals.py +135 -0
- tests/test_tracking.py +164 -0
- tests/test_utils.py +118 -0
- celldetective-1.0.2.post1.dist-info/METADATA +0 -221
- celldetective-1.0.2.post1.dist-info/RECORD +0 -66
- {celldetective-1.0.2.post1.dist-info → celldetective-1.1.1.dist-info}/LICENSE +0 -0
- {celldetective-1.0.2.post1.dist-info → celldetective-1.1.1.dist-info}/WHEEL +0 -0
- {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()
|