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