synapse_net.tools.base_widget

  1import os
  2import sys
  3from pathlib import Path
  4
  5import napari
  6import numpy as np
  7import qtpy.QtWidgets as QtWidgets
  8
  9from napari.utils.notifications import show_info
 10from qtpy.QtWidgets import (
 11    QWidget, QVBoxLayout, QHBoxLayout, QLabel, QSpinBox, QComboBox, QCheckBox
 12)
 13from superqt import QCollapsible
 14
 15try:
 16    from napari_skimage_regionprops import add_table, get_table
 17except ImportError:
 18    add_table, get_table = None, None
 19
 20
 21class _SilencePrint:
 22    def __enter__(self):
 23        self._original_stdout = sys.stdout
 24        sys.stdout = open(os.devnull, "w")
 25
 26    def __exit__(self, exc_type, exc_val, exc_tb):
 27        sys.stdout.close()
 28        sys.stdout = self._original_stdout
 29
 30
 31class BaseWidget(QWidget):
 32    def __init__(self):
 33        super().__init__()
 34        self.viewer = napari.current_viewer()
 35        self.attribute_dict = {}
 36
 37    def _create_layer_selector(self, selector_name, layer_type="Image", prefer_substring=None):
 38        """Create a layer selector for an image or labels and store it in a dictionary.
 39
 40        Args:
 41            selector_name (str): The name of the selector, used as a key in the dictionary.
 42            layer_type (str): The type of layer to filter for ("Image" or "Labels").
 43            prefer_substring (str, optional): If given, the selector auto-defaults to the first
 44                layer whose name contains this substring (case-insensitive); falls back to the first
 45                layer otherwise. Re-applied whenever layers are added/removed.
 46        """
 47        if not hasattr(self, "layer_selectors"):
 48            self.layer_selectors = {}
 49
 50        # Determine the annotation type for the widget
 51        if layer_type == "Image":
 52            layer_filter = napari.layers.Image
 53        elif layer_type == "Labels":
 54            layer_filter = napari.layers.Labels
 55        elif layer_type == "Shapes":
 56            layer_filter = napari.layers.Shapes
 57        else:
 58            raise ValueError("layer_type must be either 'Image' or 'Labels'.")
 59
 60        selector_widget = QtWidgets.QWidget()
 61        image_selector = QtWidgets.QComboBox()
 62        layer_label = QtWidgets.QLabel(f"{selector_name}:")
 63
 64        # Populate initial options
 65        self._update_selector(selector=image_selector, layer_filter=layer_filter, prefer_substring=prefer_substring)
 66
 67        # Update selector on layer events
 68        self.viewer.layers.events.inserted.connect(
 69            lambda event: self._update_selector(image_selector, layer_filter, prefer_substring)
 70        )
 71        self.viewer.layers.events.removed.connect(
 72            lambda event: self._update_selector(image_selector, layer_filter, prefer_substring)
 73        )
 74
 75        # Store the selector in the dictionary
 76        self.layer_selectors[selector_name] = selector_widget
 77
 78        # Set up layout
 79        layout = QVBoxLayout()
 80        layout.addWidget(layer_label)
 81        layout.addWidget(image_selector)
 82        selector_widget.setLayout(layout)
 83        return selector_widget
 84
 85    def _update_selector(self, selector, layer_filter, prefer_substring=None):
 86        """Update a single selector with the current image layers in the viewer.
 87
 88        If ``prefer_substring`` is given, auto-select the first layer whose name contains it
 89        (case-insensitive); otherwise the first layer stays selected (QComboBox default).
 90        """
 91        selector.clear()
 92        image_layers = [layer.name for layer in self.viewer.layers if isinstance(layer, layer_filter)]
 93        selector.addItems(image_layers)
 94        if prefer_substring:
 95            needle = prefer_substring.lower()
 96            match = next((name for name in image_layers if needle in name.lower()), None)
 97            if match is not None:
 98                selector.setCurrentText(match)
 99
100    def _get_layer_selector_layer(self, selector_name):
101        """Return the layer currently selected in a given selector."""
102        if selector_name in self.layer_selectors:
103            selector_widget = self.layer_selectors[selector_name]
104
105            # Retrieve the QComboBox from the QWidget's layout
106            image_selector = selector_widget.layout().itemAt(1).widget()
107
108            if isinstance(image_selector, QComboBox):
109                selected_layer_name = image_selector.currentText()
110                if selected_layer_name in self.viewer.layers:
111                    return self.viewer.layers[selected_layer_name]
112        return None  # Return None if layer not found
113
114    def _get_layer_selector_data(self, selector_name, return_metadata=False):
115        """Return the data for the layer currently selected in a given selector."""
116        if selector_name in self.layer_selectors:
117            selector_widget = self.layer_selectors[selector_name]
118
119            # Retrieve the QComboBox from the QWidget's layout
120            image_selector = selector_widget.layout().itemAt(1).widget()
121
122            if isinstance(image_selector, QComboBox):
123                selected_layer_name = image_selector.currentText()
124                if selected_layer_name in self.viewer.layers:
125                    if return_metadata:
126                        return self.viewer.layers[selected_layer_name].metadata
127                    else:
128                        return self.viewer.layers[selected_layer_name].data
129        return None  # Return None if layer not found
130
131    def _add_string_param(self, name, value, title=None, placeholder=None, layout=None, tooltip=None):
132        if layout is None:
133            layout = QtWidgets.QHBoxLayout()
134        label = QtWidgets.QLabel(title or name)
135        if tooltip:
136            label.setToolTip(tooltip)
137        layout.addWidget(label)
138        param = QtWidgets.QLineEdit()
139        param.setText(value)
140        if placeholder is not None:
141            param.setPlaceholderText(placeholder)
142        param.textChanged.connect(lambda val: setattr(self, name, val))
143        if tooltip:
144            param.setToolTip(tooltip)
145        layout.addWidget(param)
146        return param, layout
147
148    def _add_float_param(self, name, value, title=None, min_val=0.0, max_val=1.0, decimals=2,
149                         step=0.01, layout=None, tooltip=None):
150        if layout is None:
151            layout = QtWidgets.QHBoxLayout()
152        label = QtWidgets.QLabel(title or name)
153        if tooltip:
154            label.setToolTip(tooltip)
155        layout.addWidget(label)
156        param = QtWidgets.QDoubleSpinBox()
157        param.setRange(min_val, max_val)
158        param.setDecimals(decimals)
159        param.setValue(value)
160        param.setSingleStep(step)
161        param.valueChanged.connect(lambda val: setattr(self, name, val))
162        if tooltip:
163            param.setToolTip(tooltip)
164        layout.addWidget(param)
165        return param, layout
166
167    def _add_int_param(self, name, value, min_val, max_val, title=None, step=1, layout=None, tooltip=None):
168        if layout is None:
169            layout = QHBoxLayout()
170        label = QLabel(title or name)
171        if tooltip:
172            label.setToolTip(tooltip)
173        layout.addWidget(label)
174        param = QSpinBox()
175        param.setRange(min_val, max_val)
176        param.setValue(value)
177        param.setSingleStep(step)
178        param.valueChanged.connect(lambda val: setattr(self, name, val))
179        if tooltip:
180            param.setToolTip(tooltip)
181        layout.addWidget(param)
182        return param, layout
183
184    def _add_choice_param(self, name, value, options, title=None, layout=None, update=None, tooltip=None):
185        if layout is None:
186            layout = QHBoxLayout()
187        label = QLabel(title or name)
188        if tooltip:
189            label.setToolTip(tooltip)
190        layout.addWidget(label)
191
192        # Create the dropdown menu via QComboBox, set the available values.
193        dropdown = QComboBox()
194        dropdown.addItems(options)
195        if update is None:
196            dropdown.currentIndexChanged.connect(lambda index: setattr(self, name, options[index]))
197        else:
198            dropdown.currentIndexChanged.connect(update)
199
200        # Set the correct value for the value.
201        dropdown.setCurrentIndex(dropdown.findText(value))
202
203        if tooltip:
204            dropdown.setToolTip(tooltip)
205
206        layout.addWidget(dropdown)
207        return dropdown, layout
208
209    def _add_shape_param(self, names, values, min_val, max_val, step=1, title=None, tooltip=None):
210        layout = QHBoxLayout()
211
212        x_layout = QVBoxLayout()
213        x_param, _ = self._add_int_param(
214            names[0], values[0], min_val=min_val, max_val=max_val, layout=x_layout, step=step,
215            title=title[0] if title is not None else title, tooltip=tooltip
216        )
217        layout.addLayout(x_layout)
218
219        y_layout = QVBoxLayout()
220        y_param, _ = self._add_int_param(
221            names[1], values[1], min_val=min_val, max_val=max_val, layout=y_layout, step=step,
222            title=title[1] if title is not None else title, tooltip=tooltip
223        )
224        layout.addLayout(y_layout)
225
226        if len(names) == 3:
227            z_layout = QVBoxLayout()
228            z_param, _ = self._add_int_param(
229                names[2], values[2], min_val=min_val, max_val=max_val, layout=z_layout, step=step,
230                title=title[2] if title is not None else title, tooltip=tooltip
231            )
232            layout.addLayout(z_layout)
233            return x_param, y_param, z_param, layout
234
235        return x_param, y_param, layout
236
237    def _make_collapsible(self, widget, title):
238        parent_widget = QWidget()
239        parent_widget.setLayout(QVBoxLayout())
240        collapsible = QCollapsible(title, parent_widget)
241        collapsible.addWidget(widget)
242        parent_widget.layout().addWidget(collapsible)
243        return parent_widget
244
245    def _add_boolean_param(self, name, value, title=None, tooltip=None):
246        checkbox = QCheckBox(name if title is None else title)
247        checkbox.setChecked(value)
248        checkbox.stateChanged.connect(lambda val: setattr(self, name, val))
249        if tooltip:
250            checkbox.setToolTip(tooltip)
251        return checkbox
252
253    def _add_path_param(self, name, value, select_type, title=None, placeholder=None, tooltip=None):
254        assert select_type in ("directory", "file", "both")
255
256        layout = QtWidgets.QHBoxLayout()
257        label = QtWidgets.QLabel(title or name)
258        if tooltip:
259            label.setToolTip(tooltip)
260        layout.addWidget(label)
261
262        path_textbox = QtWidgets.QLineEdit()
263        path_textbox.setText(str(value))
264        if placeholder is not None:
265            path_textbox.setPlaceholderText(placeholder)
266        path_textbox.textChanged.connect(lambda val: setattr(self, name, val))
267        if tooltip:
268            path_textbox.setToolTip(tooltip)
269
270        layout.addWidget(path_textbox)
271
272        def add_path_button(select_type, tooltip=None):
273            # Adjust button text.
274            button_text = f"Select {select_type.capitalize()}"
275            path_button = QtWidgets.QPushButton(button_text)
276
277            # Call appropriate function based on select_type.
278            path_button.clicked.connect(lambda: getattr(self, f"_get_{select_type}_path")(name, path_textbox))
279            if tooltip:
280                path_button.setToolTip(tooltip)
281            layout.addWidget(path_button)
282
283        if select_type == "both":
284            add_path_button("file")
285            add_path_button("directory")
286
287        else:
288            add_path_button(select_type)
289
290        return path_textbox, layout
291
292    def _get_directory_path(self, name, textbox, tooltip=None):
293        directory = QtWidgets.QFileDialog.getExistingDirectory(
294            self, "Select Directory", "", QtWidgets.QFileDialog.ShowDirsOnly
295        )
296        if tooltip:
297            directory.setToolTip(tooltip)
298        if directory and Path(directory).is_dir():
299            textbox.setText(str(directory))
300        else:
301            # Handle the case where the selected path is not a directory
302            print("Invalid directory selected. Please try again.")
303
304    def _get_file_path(self, name, textbox, tooltip=None):
305        file_path, _ = QtWidgets.QFileDialog.getOpenFileName(
306            self, "Select File", "", "All Files (*)"
307        )
308        if tooltip:
309            file_path.setToolTip(tooltip)
310        if file_path and Path(file_path).is_file():
311            textbox.setText(str(file_path))
312        else:
313            # Handle the case where the selected path is not a file
314            print("Invalid file selected. Please try again.")
315
316    def _handle_resolution(self, metadata, voxel_size_param, ndim, return_as_list=True):
317        # Get the resolution / voxel size from the layer metadata if available.
318        resolution = metadata.get("voxel_size", None)
319
320        # If user input was given then override resolution from metadata.
321        axes = "zyx" if ndim == 3 else "yx"
322        if voxel_size_param.value() != 0.0:  # Changed from default.
323            resolution = {ax: voxel_size_param.value() for ax in axes}
324
325        if resolution is not None and return_as_list:
326            resolution = [resolution[ax] for ax in axes]
327            assert len(resolution) == ndim
328
329        return resolution
330
331    def _save_table(self, save_path, data):
332        ext = os.path.splitext(save_path)[1]
333        if ext == "":  # No file extension given, By default we save to CSV.
334            file_path = f"{save_path}.csv"
335            data.to_csv(file_path, index=False)
336        elif ext == ".csv":  # Extension was specified as csv
337            file_path = save_path
338            data.to_csv(file_path, index=False)
339        elif ext == ".xlsx":  # We also support excel.
340            file_path = save_path
341            data.to_excel(file_path, index=False)
342        else:
343            raise ValueError("Invalid extension for table: {ext}. We support .csv or .xlsx.")
344        return file_path
345
346    def _add_properties_and_table(self, layer, table_data, save_path=""):
347        layer.properties = table_data
348
349        if add_table is not None:
350            with _SilencePrint():
351                add_table(layer, self.viewer)
352
353        # Save table to file if save path is provided.
354        if save_path != "":
355            file_path = self._save_table(self.save_path.text(), table_data)
356            show_info(f"INFO: Added table and saved file to {file_path}.")
357
358    def _add_or_update_layer(self, add_fn, name, data, scale, translate, layer_kwargs):
359        """Add a layer via ``add_fn`` (e.g. ``self.viewer.add_labels``), or refresh it in place if a
360        layer with this name already exists.
361
362        On refresh the ``scale``/``translate`` are reapplied (when provided) because a persisted layer
363        keeps its original transform, which may be stale if the source layer / voxel size changed
364        between runs; any ``layer_kwargs`` (e.g. ``opacity``, ``blending``, ``colormap``) are reapplied
365        too. On first add these go through the layer constructor. Returns the (new or existing) layer.
366        """
367        if name in self.viewer.layers:
368            layer = self.viewer.layers[name]
369            layer.data = data
370            if scale is not None:
371                layer.scale = scale
372            if translate is not None:
373                layer.translate = translate
374            for key, value in layer_kwargs.items():
375                setattr(layer, key, value)
376        else:
377            ctor_kwargs = dict(layer_kwargs)
378            if scale is not None:
379                ctor_kwargs["scale"] = scale
380            if translate is not None:
381                ctor_kwargs["translate"] = translate
382            layer = add_fn(data, name=name, **ctor_kwargs)
383        return layer
384
385    def add_or_update_labels(self, name, data, *, scale=None, translate=None, **layer_kwargs):
386        """Add a Labels layer, or refresh it in place if one with this name already exists.
387
388        See :meth:`_add_or_update_layer` for the refresh/transform semantics. Extra keyword arguments
389        (``opacity``, ``blending``, ``colormap``, …) are forwarded to the layer. Returns the layer.
390        """
391        return self._add_or_update_layer(
392            self.viewer.add_labels, name, data, scale, translate, layer_kwargs
393        )
394
395    def add_or_update_surface(self, name, vertices, faces, *, scale=None, translate=None,
396                              values=None, **layer_kwargs):
397        """Add a Surface layer, or refresh it in place if one with this name already exists.
398
399        Surface layers colour by per-vertex ``values``; a constant array (the default) gives a
400        flat-coloured surface. See :meth:`_add_or_update_layer` for the refresh/transform semantics.
401        Returns the layer.
402        """
403        if values is None:
404            values = np.ones(len(vertices), dtype="float32")
405        return self._add_or_update_layer(
406            self.viewer.add_surface, name, (vertices, faces, values), scale, translate, layer_kwargs
407        )
class BaseWidget(PyQt5.QtWidgets.QWidget):
 32class BaseWidget(QWidget):
 33    def __init__(self):
 34        super().__init__()
 35        self.viewer = napari.current_viewer()
 36        self.attribute_dict = {}
 37
 38    def _create_layer_selector(self, selector_name, layer_type="Image", prefer_substring=None):
 39        """Create a layer selector for an image or labels and store it in a dictionary.
 40
 41        Args:
 42            selector_name (str): The name of the selector, used as a key in the dictionary.
 43            layer_type (str): The type of layer to filter for ("Image" or "Labels").
 44            prefer_substring (str, optional): If given, the selector auto-defaults to the first
 45                layer whose name contains this substring (case-insensitive); falls back to the first
 46                layer otherwise. Re-applied whenever layers are added/removed.
 47        """
 48        if not hasattr(self, "layer_selectors"):
 49            self.layer_selectors = {}
 50
 51        # Determine the annotation type for the widget
 52        if layer_type == "Image":
 53            layer_filter = napari.layers.Image
 54        elif layer_type == "Labels":
 55            layer_filter = napari.layers.Labels
 56        elif layer_type == "Shapes":
 57            layer_filter = napari.layers.Shapes
 58        else:
 59            raise ValueError("layer_type must be either 'Image' or 'Labels'.")
 60
 61        selector_widget = QtWidgets.QWidget()
 62        image_selector = QtWidgets.QComboBox()
 63        layer_label = QtWidgets.QLabel(f"{selector_name}:")
 64
 65        # Populate initial options
 66        self._update_selector(selector=image_selector, layer_filter=layer_filter, prefer_substring=prefer_substring)
 67
 68        # Update selector on layer events
 69        self.viewer.layers.events.inserted.connect(
 70            lambda event: self._update_selector(image_selector, layer_filter, prefer_substring)
 71        )
 72        self.viewer.layers.events.removed.connect(
 73            lambda event: self._update_selector(image_selector, layer_filter, prefer_substring)
 74        )
 75
 76        # Store the selector in the dictionary
 77        self.layer_selectors[selector_name] = selector_widget
 78
 79        # Set up layout
 80        layout = QVBoxLayout()
 81        layout.addWidget(layer_label)
 82        layout.addWidget(image_selector)
 83        selector_widget.setLayout(layout)
 84        return selector_widget
 85
 86    def _update_selector(self, selector, layer_filter, prefer_substring=None):
 87        """Update a single selector with the current image layers in the viewer.
 88
 89        If ``prefer_substring`` is given, auto-select the first layer whose name contains it
 90        (case-insensitive); otherwise the first layer stays selected (QComboBox default).
 91        """
 92        selector.clear()
 93        image_layers = [layer.name for layer in self.viewer.layers if isinstance(layer, layer_filter)]
 94        selector.addItems(image_layers)
 95        if prefer_substring:
 96            needle = prefer_substring.lower()
 97            match = next((name for name in image_layers if needle in name.lower()), None)
 98            if match is not None:
 99                selector.setCurrentText(match)
100
101    def _get_layer_selector_layer(self, selector_name):
102        """Return the layer currently selected in a given selector."""
103        if selector_name in self.layer_selectors:
104            selector_widget = self.layer_selectors[selector_name]
105
106            # Retrieve the QComboBox from the QWidget's layout
107            image_selector = selector_widget.layout().itemAt(1).widget()
108
109            if isinstance(image_selector, QComboBox):
110                selected_layer_name = image_selector.currentText()
111                if selected_layer_name in self.viewer.layers:
112                    return self.viewer.layers[selected_layer_name]
113        return None  # Return None if layer not found
114
115    def _get_layer_selector_data(self, selector_name, return_metadata=False):
116        """Return the data for the layer currently selected in a given selector."""
117        if selector_name in self.layer_selectors:
118            selector_widget = self.layer_selectors[selector_name]
119
120            # Retrieve the QComboBox from the QWidget's layout
121            image_selector = selector_widget.layout().itemAt(1).widget()
122
123            if isinstance(image_selector, QComboBox):
124                selected_layer_name = image_selector.currentText()
125                if selected_layer_name in self.viewer.layers:
126                    if return_metadata:
127                        return self.viewer.layers[selected_layer_name].metadata
128                    else:
129                        return self.viewer.layers[selected_layer_name].data
130        return None  # Return None if layer not found
131
132    def _add_string_param(self, name, value, title=None, placeholder=None, layout=None, tooltip=None):
133        if layout is None:
134            layout = QtWidgets.QHBoxLayout()
135        label = QtWidgets.QLabel(title or name)
136        if tooltip:
137            label.setToolTip(tooltip)
138        layout.addWidget(label)
139        param = QtWidgets.QLineEdit()
140        param.setText(value)
141        if placeholder is not None:
142            param.setPlaceholderText(placeholder)
143        param.textChanged.connect(lambda val: setattr(self, name, val))
144        if tooltip:
145            param.setToolTip(tooltip)
146        layout.addWidget(param)
147        return param, layout
148
149    def _add_float_param(self, name, value, title=None, min_val=0.0, max_val=1.0, decimals=2,
150                         step=0.01, layout=None, tooltip=None):
151        if layout is None:
152            layout = QtWidgets.QHBoxLayout()
153        label = QtWidgets.QLabel(title or name)
154        if tooltip:
155            label.setToolTip(tooltip)
156        layout.addWidget(label)
157        param = QtWidgets.QDoubleSpinBox()
158        param.setRange(min_val, max_val)
159        param.setDecimals(decimals)
160        param.setValue(value)
161        param.setSingleStep(step)
162        param.valueChanged.connect(lambda val: setattr(self, name, val))
163        if tooltip:
164            param.setToolTip(tooltip)
165        layout.addWidget(param)
166        return param, layout
167
168    def _add_int_param(self, name, value, min_val, max_val, title=None, step=1, layout=None, tooltip=None):
169        if layout is None:
170            layout = QHBoxLayout()
171        label = QLabel(title or name)
172        if tooltip:
173            label.setToolTip(tooltip)
174        layout.addWidget(label)
175        param = QSpinBox()
176        param.setRange(min_val, max_val)
177        param.setValue(value)
178        param.setSingleStep(step)
179        param.valueChanged.connect(lambda val: setattr(self, name, val))
180        if tooltip:
181            param.setToolTip(tooltip)
182        layout.addWidget(param)
183        return param, layout
184
185    def _add_choice_param(self, name, value, options, title=None, layout=None, update=None, tooltip=None):
186        if layout is None:
187            layout = QHBoxLayout()
188        label = QLabel(title or name)
189        if tooltip:
190            label.setToolTip(tooltip)
191        layout.addWidget(label)
192
193        # Create the dropdown menu via QComboBox, set the available values.
194        dropdown = QComboBox()
195        dropdown.addItems(options)
196        if update is None:
197            dropdown.currentIndexChanged.connect(lambda index: setattr(self, name, options[index]))
198        else:
199            dropdown.currentIndexChanged.connect(update)
200
201        # Set the correct value for the value.
202        dropdown.setCurrentIndex(dropdown.findText(value))
203
204        if tooltip:
205            dropdown.setToolTip(tooltip)
206
207        layout.addWidget(dropdown)
208        return dropdown, layout
209
210    def _add_shape_param(self, names, values, min_val, max_val, step=1, title=None, tooltip=None):
211        layout = QHBoxLayout()
212
213        x_layout = QVBoxLayout()
214        x_param, _ = self._add_int_param(
215            names[0], values[0], min_val=min_val, max_val=max_val, layout=x_layout, step=step,
216            title=title[0] if title is not None else title, tooltip=tooltip
217        )
218        layout.addLayout(x_layout)
219
220        y_layout = QVBoxLayout()
221        y_param, _ = self._add_int_param(
222            names[1], values[1], min_val=min_val, max_val=max_val, layout=y_layout, step=step,
223            title=title[1] if title is not None else title, tooltip=tooltip
224        )
225        layout.addLayout(y_layout)
226
227        if len(names) == 3:
228            z_layout = QVBoxLayout()
229            z_param, _ = self._add_int_param(
230                names[2], values[2], min_val=min_val, max_val=max_val, layout=z_layout, step=step,
231                title=title[2] if title is not None else title, tooltip=tooltip
232            )
233            layout.addLayout(z_layout)
234            return x_param, y_param, z_param, layout
235
236        return x_param, y_param, layout
237
238    def _make_collapsible(self, widget, title):
239        parent_widget = QWidget()
240        parent_widget.setLayout(QVBoxLayout())
241        collapsible = QCollapsible(title, parent_widget)
242        collapsible.addWidget(widget)
243        parent_widget.layout().addWidget(collapsible)
244        return parent_widget
245
246    def _add_boolean_param(self, name, value, title=None, tooltip=None):
247        checkbox = QCheckBox(name if title is None else title)
248        checkbox.setChecked(value)
249        checkbox.stateChanged.connect(lambda val: setattr(self, name, val))
250        if tooltip:
251            checkbox.setToolTip(tooltip)
252        return checkbox
253
254    def _add_path_param(self, name, value, select_type, title=None, placeholder=None, tooltip=None):
255        assert select_type in ("directory", "file", "both")
256
257        layout = QtWidgets.QHBoxLayout()
258        label = QtWidgets.QLabel(title or name)
259        if tooltip:
260            label.setToolTip(tooltip)
261        layout.addWidget(label)
262
263        path_textbox = QtWidgets.QLineEdit()
264        path_textbox.setText(str(value))
265        if placeholder is not None:
266            path_textbox.setPlaceholderText(placeholder)
267        path_textbox.textChanged.connect(lambda val: setattr(self, name, val))
268        if tooltip:
269            path_textbox.setToolTip(tooltip)
270
271        layout.addWidget(path_textbox)
272
273        def add_path_button(select_type, tooltip=None):
274            # Adjust button text.
275            button_text = f"Select {select_type.capitalize()}"
276            path_button = QtWidgets.QPushButton(button_text)
277
278            # Call appropriate function based on select_type.
279            path_button.clicked.connect(lambda: getattr(self, f"_get_{select_type}_path")(name, path_textbox))
280            if tooltip:
281                path_button.setToolTip(tooltip)
282            layout.addWidget(path_button)
283
284        if select_type == "both":
285            add_path_button("file")
286            add_path_button("directory")
287
288        else:
289            add_path_button(select_type)
290
291        return path_textbox, layout
292
293    def _get_directory_path(self, name, textbox, tooltip=None):
294        directory = QtWidgets.QFileDialog.getExistingDirectory(
295            self, "Select Directory", "", QtWidgets.QFileDialog.ShowDirsOnly
296        )
297        if tooltip:
298            directory.setToolTip(tooltip)
299        if directory and Path(directory).is_dir():
300            textbox.setText(str(directory))
301        else:
302            # Handle the case where the selected path is not a directory
303            print("Invalid directory selected. Please try again.")
304
305    def _get_file_path(self, name, textbox, tooltip=None):
306        file_path, _ = QtWidgets.QFileDialog.getOpenFileName(
307            self, "Select File", "", "All Files (*)"
308        )
309        if tooltip:
310            file_path.setToolTip(tooltip)
311        if file_path and Path(file_path).is_file():
312            textbox.setText(str(file_path))
313        else:
314            # Handle the case where the selected path is not a file
315            print("Invalid file selected. Please try again.")
316
317    def _handle_resolution(self, metadata, voxel_size_param, ndim, return_as_list=True):
318        # Get the resolution / voxel size from the layer metadata if available.
319        resolution = metadata.get("voxel_size", None)
320
321        # If user input was given then override resolution from metadata.
322        axes = "zyx" if ndim == 3 else "yx"
323        if voxel_size_param.value() != 0.0:  # Changed from default.
324            resolution = {ax: voxel_size_param.value() for ax in axes}
325
326        if resolution is not None and return_as_list:
327            resolution = [resolution[ax] for ax in axes]
328            assert len(resolution) == ndim
329
330        return resolution
331
332    def _save_table(self, save_path, data):
333        ext = os.path.splitext(save_path)[1]
334        if ext == "":  # No file extension given, By default we save to CSV.
335            file_path = f"{save_path}.csv"
336            data.to_csv(file_path, index=False)
337        elif ext == ".csv":  # Extension was specified as csv
338            file_path = save_path
339            data.to_csv(file_path, index=False)
340        elif ext == ".xlsx":  # We also support excel.
341            file_path = save_path
342            data.to_excel(file_path, index=False)
343        else:
344            raise ValueError("Invalid extension for table: {ext}. We support .csv or .xlsx.")
345        return file_path
346
347    def _add_properties_and_table(self, layer, table_data, save_path=""):
348        layer.properties = table_data
349
350        if add_table is not None:
351            with _SilencePrint():
352                add_table(layer, self.viewer)
353
354        # Save table to file if save path is provided.
355        if save_path != "":
356            file_path = self._save_table(self.save_path.text(), table_data)
357            show_info(f"INFO: Added table and saved file to {file_path}.")
358
359    def _add_or_update_layer(self, add_fn, name, data, scale, translate, layer_kwargs):
360        """Add a layer via ``add_fn`` (e.g. ``self.viewer.add_labels``), or refresh it in place if a
361        layer with this name already exists.
362
363        On refresh the ``scale``/``translate`` are reapplied (when provided) because a persisted layer
364        keeps its original transform, which may be stale if the source layer / voxel size changed
365        between runs; any ``layer_kwargs`` (e.g. ``opacity``, ``blending``, ``colormap``) are reapplied
366        too. On first add these go through the layer constructor. Returns the (new or existing) layer.
367        """
368        if name in self.viewer.layers:
369            layer = self.viewer.layers[name]
370            layer.data = data
371            if scale is not None:
372                layer.scale = scale
373            if translate is not None:
374                layer.translate = translate
375            for key, value in layer_kwargs.items():
376                setattr(layer, key, value)
377        else:
378            ctor_kwargs = dict(layer_kwargs)
379            if scale is not None:
380                ctor_kwargs["scale"] = scale
381            if translate is not None:
382                ctor_kwargs["translate"] = translate
383            layer = add_fn(data, name=name, **ctor_kwargs)
384        return layer
385
386    def add_or_update_labels(self, name, data, *, scale=None, translate=None, **layer_kwargs):
387        """Add a Labels layer, or refresh it in place if one with this name already exists.
388
389        See :meth:`_add_or_update_layer` for the refresh/transform semantics. Extra keyword arguments
390        (``opacity``, ``blending``, ``colormap``, …) are forwarded to the layer. Returns the layer.
391        """
392        return self._add_or_update_layer(
393            self.viewer.add_labels, name, data, scale, translate, layer_kwargs
394        )
395
396    def add_or_update_surface(self, name, vertices, faces, *, scale=None, translate=None,
397                              values=None, **layer_kwargs):
398        """Add a Surface layer, or refresh it in place if one with this name already exists.
399
400        Surface layers colour by per-vertex ``values``; a constant array (the default) gives a
401        flat-coloured surface. See :meth:`_add_or_update_layer` for the refresh/transform semantics.
402        Returns the layer.
403        """
404        if values is None:
405            values = np.ones(len(vertices), dtype="float32")
406        return self._add_or_update_layer(
407            self.viewer.add_surface, name, (vertices, faces, values), scale, translate, layer_kwargs
408        )

QWidget(parent: Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

viewer
attribute_dict
def add_or_update_labels(self, name, data, *, scale=None, translate=None, **layer_kwargs):
386    def add_or_update_labels(self, name, data, *, scale=None, translate=None, **layer_kwargs):
387        """Add a Labels layer, or refresh it in place if one with this name already exists.
388
389        See :meth:`_add_or_update_layer` for the refresh/transform semantics. Extra keyword arguments
390        (``opacity``, ``blending``, ``colormap``, …) are forwarded to the layer. Returns the layer.
391        """
392        return self._add_or_update_layer(
393            self.viewer.add_labels, name, data, scale, translate, layer_kwargs
394        )

Add a Labels layer, or refresh it in place if one with this name already exists.

See _add_or_update_layer() for the refresh/transform semantics. Extra keyword arguments (opacity, blending, colormap, …) are forwarded to the layer. Returns the layer.

def add_or_update_surface( self, name, vertices, faces, *, scale=None, translate=None, values=None, **layer_kwargs):
396    def add_or_update_surface(self, name, vertices, faces, *, scale=None, translate=None,
397                              values=None, **layer_kwargs):
398        """Add a Surface layer, or refresh it in place if one with this name already exists.
399
400        Surface layers colour by per-vertex ``values``; a constant array (the default) gives a
401        flat-coloured surface. See :meth:`_add_or_update_layer` for the refresh/transform semantics.
402        Returns the layer.
403        """
404        if values is None:
405            values = np.ones(len(vertices), dtype="float32")
406        return self._add_or_update_layer(
407            self.viewer.add_surface, name, (vertices, faces, values), scale, translate, layer_kwargs
408        )

Add a Surface layer, or refresh it in place if one with this name already exists.

Surface layers colour by per-vertex values; a constant array (the default) gives a flat-coloured surface. See _add_or_update_layer() for the refresh/transform semantics. Returns the layer.