diff --git a/impy/viewer/keybinds.py b/impy/viewer/keybinds.py index c5dfabf9..cf5a0fe3 100644 --- a/impy/viewer/keybinds.py +++ b/impy/viewer/keybinds.py @@ -1,3 +1,4 @@ +from impy.viewer.widgets.dialog import ProjectionDialog from ..arrays import LabeledArray from ..core import array as ip_array from .utils import * @@ -289,51 +290,63 @@ def proj(viewer:"napari.Viewer"): """ Projection """ - layers = list(viewer.layers.selection) - for layer in layers: - data = layer.data - kwargs = {} - kwargs.update({"scale": layer.scale[-2:], - "translate": layer.translate[-2:], - "blending": layer.blending, - "opacity": layer.opacity, - "ndim": 2, - "name": layer.name+"-proj"}) - if isinstance(layer, (napari.layers.Image, napari.layers.Labels)): - raise TypeError("Projection not supported.") - elif isinstance(layer, napari.layers.Shapes): - data = [d[:,-2:] for d in data] - - if layer.nshapes > 0: - for k in ["face_color", "edge_color", "edge_width"]: - kwargs[k] = getattr(layer, k) - else: - data = None - kwargs["ndim"] = 2 - kwargs["shape_type"] = layer.shape_type - viewer.add_shapes(data, **kwargs) - elif isinstance(layer, napari.layers.Points): - data = data[:, -2:] - for k in ["face_color", "edge_color", "size", "symbol"]: - kwargs[k] = getattr(layer, k, None) - kwargs["size"] = layer.size[:,-2:] - if len(data) == 0: - data = None - kwargs["ndim"] = 2 - viewer.add_points(data, **kwargs) - elif isinstance(layer, napari.layers.Tracks): - data = data[:, [0,-2,-1]] - viewer.add_tracks(data, **kwargs) - else: - raise NotImplementedError(type(layer)) + layer = get_a_selected_layer(viewer) + + data = layer.data + kwargs = {} + kwargs.update({"scale": layer.scale[-2:], + "translate": layer.translate[-2:], + "blending": layer.blending, + "opacity": layer.opacity, + "ndim": 2, + "name": layer.name+"-proj"}) + if isinstance(layer, napari.layers.Image): + if layer.data.ndim < 3: + return None + dlg = ProjectionDialog(viewer, layer) + dlg.exec_() + elif isinstance(layer, napari.layers.Labels): + raise TypeError("Projection not supported.") + elif isinstance(layer, napari.layers.Shapes): + data = [d[:,-2:] for d in data] + if layer.nshapes > 0: + for k in ["face_color", "edge_color", "edge_width"]: + kwargs[k] = getattr(layer, k) + else: + data = None + kwargs["ndim"] = 2 + kwargs["shape_type"] = layer.shape_type + viewer.add_shapes(data, **kwargs) + elif isinstance(layer, napari.layers.Points): + data = data[:, -2:] + for k in ["face_color", "edge_color", "size", "symbol"]: + kwargs[k] = getattr(layer, k, None) + kwargs["size"] = layer.size[:,-2:] + if len(data) == 0: + data = None + kwargs["ndim"] = 2 + viewer.add_points(data, **kwargs) + elif isinstance(layer, napari.layers.Tracks): + data = data[:, [0,-2,-1]] + viewer.add_tracks(data, **kwargs) + else: + raise NotImplementedError(type(layer)) + @bind_key def duplicate_layer(viewer:"napari.Viewer"): """ Duplicate selected layer(s). """ - dlg = DuplicateDialog(viewer) - dlg.exec_() + layer = get_a_selected_layer(viewer) + + if isinstance(layer, (napari.layers.Image, napari.layers.Labels)): + dlg = DuplicateDialog(viewer, layer) + dlg.exec_() + else: + new_layer = copy_layer(layer) + viewer.add_layer(new_layer) + return None def _crop_rotated_rectangle(img, crds, dims): translate = np.min(crds, axis=0) diff --git a/impy/viewer/utils.py b/impy/viewer/utils.py index 20836be8..eff4ea53 100644 --- a/impy/viewer/utils.py +++ b/impy/viewer/utils.py @@ -11,10 +11,16 @@ def copy_layer(layer): args, kwargs, *_ = layer.as_layer_data_tuple() # linear interpolation is valid only in 3D mode. - if kwargs["interpolation"] == "linear": + if kwargs.get("interpolation", None) == "linear": kwargs = kwargs.copy() kwargs["interpolation"] = "nearest" + + # This is necessarry for text bound layers. + if "properties" in kwargs: + kwargs.pop("properties") + copy = layer.__class__(args, **kwargs) + return copy def iter_layer(viewer:"napari.Viewer", layer_type:str): @@ -391,6 +397,14 @@ def layer_to_impy_object(viewer:"napari.Viewer", layer): else: raise NotImplementedError(type(layer)) +def get_a_selected_layer(viewer:"napari.Viewer"): + selected = list(viewer.layers.selection) + if len(selected) == 0: + raise ValueError("No layer is selected.") + elif len(selected) > 1: + raise ValueError("More than one layers are selected.") + return selected[0] + class ColorCycle: def __init__(self, cmap="rainbow") -> None: diff --git a/impy/viewer/widgets/dialog.py b/impy/viewer/widgets/dialog.py index 417902e5..634a90ef 100644 --- a/impy/viewer/widgets/dialog.py +++ b/impy/viewer/widgets/dialog.py @@ -1,9 +1,12 @@ -from qtpy.QtWidgets import QDialog, QPushButton, QLabel, QGridLayout, QCheckBox, QLineEdit +from __future__ import annotations +from impy.utils.axesop import find_first_appeared +from PyQt5.QtWidgets import QVBoxLayout +from qtpy.QtWidgets import QDialog, QPushButton, QLabel, QGridLayout, QCheckBox, QLineEdit, QComboBox, QHBoxLayout import napari import numpy as np from functools import wraps -from ..utils import copy_layer, front_image, add_labels +from ..utils import add_labeledarray, copy_layer, front_image, add_labels from ..._const import SetConst from ...utils.slicer import axis_targeted_slicing @@ -82,8 +85,9 @@ def _add_widgets(self): class DuplicateDialog(QDialog): - def __init__(self, viewer:"napari.Viewer"): + def __init__(self, viewer:"napari.Viewer", layer): self.viewer = viewer + self.layer = layer super().__init__(viewer.window._qt_window) self.resize(180, 120) self.setLayout(QGridLayout()) @@ -92,15 +96,14 @@ def __init__(self, viewer:"napari.Viewer"): @close_anyway def run(self, *args): line = self.line.text() - for layer in list(self.viewer.layers.selection): - if line.strip() == "" and not self.check.isChecked(): - new_layer = copy_layer(layer) - elif line.strip(): - new_layer = self.duplicate_sliced_layer(layer) - else: - new_layer = self.duplicate_current_step(layer) - - self.viewer.add_layer(new_layer) + if line.strip() == "" and not self.check.isChecked(): + new_layer = copy_layer(self.layer) + elif line.strip(): + new_layer = self.duplicate_sliced_layer(self.layer) + else: + new_layer = self.duplicate_current_step(self.layer) + + self.viewer.add_layer(new_layer) return None def duplicate_current_step(self, layer): @@ -164,4 +167,38 @@ def _add_widgets(self): self.run_button.clicked.connect(self.run) self.layout().addWidget(self.run_button) return None - \ No newline at end of file + +class ProjectionDialog(QDialog): + def __init__(self, viewer:"napari.Viewer", layer): + self.viewer = viewer + self.layer = layer + super().__init__(viewer.window._qt_window) + self.resize(180, 120) + self.setLayout(QHBoxLayout()) + self._add_widgets() + + @close_anyway + def run(self, *args): + img = self.layer.data + out = img.proj(axis=self.axis.currentText(), method=self.method.currentText()) + translate = [t for a, t in zip(img.axes, self.layer.translate) if a in out.axes] + add_labeledarray(self.viewer, out, translate=translate) + return None + + def _add_widgets(self): + self.method = QComboBox(self) + self.method.setToolTip("Projection method") + self.method.addItems(["mean", "max", "median", "min", "std"]) + self.method.setCurrentText("mean") + self.layout().addWidget(self.method) + + axes = str(self.layer.data.axes) + self.axis = QComboBox(self) + self.axis.setToolTip("Projection axis") + self.axis.addItems(list(axes[:-2])) + self.axis.setCurrentText(find_first_appeared("ztpi