Example #1
0
class SliderWidget(HLayoutMixin, LineEditWidget):
    """
    IntItem with Slider
    """

    DATA_TYPE = int

    def __init__(self, item, parent_layout):
        super(SliderWidget, self).__init__(item, parent_layout)
        self.slider = self.vmin = self.vmax = None
        if item.get_prop_value("display", "slider"):
            self.vmin = item.get_prop_value("data", "min")
            self.vmax = item.get_prop_value("data", "max")
            assert (
                self.vmin is not None and self.vmax is not None
            ), "SliderWidget requires that item min/max have been defined"
            self.slider = QSlider()
            self.slider.setOrientation(Qt.Horizontal)
            self.setup_slider(item)
            self.slider.valueChanged.connect(self.value_changed)
            self.group.addWidget(self.slider)

    def value_to_slider(self, value):
        return value

    def slider_to_value(self, value):
        return value

    def setup_slider(self, item):
        self.slider.setRange(self.vmin, self.vmax)

    def update(self, value):
        """Reimplement LineEditWidget method"""
        LineEditWidget.update(self, value)
        if self.slider is not None and isinstance(value, self.DATA_TYPE):
            self.slider.blockSignals(True)
            self.slider.setValue(self.value_to_slider(value))
            self.slider.blockSignals(False)

    def value_changed(self, ivalue):
        """Update the lineedit"""
        value = str(self.slider_to_value(ivalue))
        self.edit.setText(value)
        self.update(value)
class ChannelControlsWidget(QWidget):
    def __init__(self,
                 controller: 'IMCController',
                 parent: Optional[QWidget] = None):
        super(ChannelControlsWidget, self).__init__(parent)
        self._controller = controller

        self._opacity_slider = QSlider(Qt.Horizontal, self)
        self._opacity_slider.setMinimum(0)
        self._opacity_slider.setMaximum(100)

        self._contrast_range_slider = QHRangeSlider(parent=self)

        self._gamma_slider = QSlider(Qt.Horizontal, self)
        self._gamma_slider.setMinimum(2)
        self._gamma_slider.setMaximum(200)

        self._color_picker = ColorPicker(self)

        self._blending_combo_box = QComboBox(self)
        self._blending_combo_box.addItems(Blending.keys())

        self._interpolation_combo_box = QComboBox(self)
        self._interpolation_combo_box.addItems(Interpolation.keys())

        layout = QFormLayout(self)
        layout.addRow('Opacity:', self._opacity_slider)
        layout.addRow('Contrast:', self._contrast_range_slider)
        layout.addRow('Gamma:', self._gamma_slider)
        layout.addRow('Color:', self._color_picker)
        layout.addRow('Blending:', self._blending_combo_box)
        layout.addRow('Interpolation:', self._interpolation_combo_box)
        self.setLayout(layout)

        # noinspection PyUnresolvedReferences
        @self._opacity_slider.valueChanged.connect
        def on_opacity_slider_value_changed(value: int):
            for channel in self._controller.selected_channels:
                channel.opacity = value / 100

        # noinspection PyUnresolvedReferences
        @self._contrast_range_slider.valuesChanged.connect
        def on_contrast_range_slider_values_changed(values: Tuple[float,
                                                                  float]):
            for channel in self._controller.selected_channels:
                contrast_limits_range_min = min(
                    (layer.contrast_limits_range[0] for layer in
                     channel.shown_imc_file_acquisition_layers.values()),
                    default=values[0])
                contrast_limits_range_max = max(
                    (layer.contrast_limits_range[1] for layer in
                     channel.shown_imc_file_acquisition_layers.values()),
                    default=values[1])
                channel.contrast_limits = (max(values[0],
                                               contrast_limits_range_min),
                                           min(values[1],
                                               contrast_limits_range_max))

        # noinspection PyUnresolvedReferences
        @self._gamma_slider.valueChanged.connect
        def on_gamma_slider_value_changed(value: int):
            for channel in self._controller.selected_channels:
                channel.gamma = value / 100

        # noinspection PyUnresolvedReferences
        @self._color_picker.events.color_changed.connect
        def on_color_picker_color_changed(color: QColor):
            for channel in self._controller.selected_channels:
                channel.color = (color.red() / 255, color.green() / 255,
                                 color.blue() / 255, color.alpha() / 255)

        # noinspection PyUnresolvedReferences
        blending_combo_box_activated = self._blending_combo_box.activated[str]

        # noinspection PyUnresolvedReferences
        @blending_combo_box_activated.connect
        def on_blending_combo_box_activated(text: str):
            for channel in self._controller.selected_channels:
                channel.blending = text

        # noinspection PyUnresolvedReferences
        interpolation_combo_box_activated = self._interpolation_combo_box.activated[
            str]

        # noinspection PyUnresolvedReferences
        @interpolation_combo_box_activated.connect
        def on_interpolation_combo_box_activated(text: str):
            for channel in self._controller.selected_channels:
                channel.interpolation = text

        self.refresh()

    def refresh(self):
        channels = self._controller.selected_channels
        if len(channels) > 0:
            mean_opacity = sum(channel.opacity
                               for channel in channels) / len(channels)
            self._opacity_slider.blockSignals(True)
            self._opacity_slider.setValue(int(mean_opacity * 100))
            self._opacity_slider.blockSignals(False)

            contrast_limits_channels = [
                channel for channel in channels
                if channel.contrast_limits is not None
            ]
            if len(contrast_limits_channels) > 0:
                layers = [
                    layer for channel in channels for layer in
                    channel.shown_imc_file_acquisition_layers.values()
                ]
                if len(layers) > 0:
                    contrast_limits_min = min(
                        channel.contrast_limits[0]
                        for channel in contrast_limits_channels)
                    contrast_limits_max = max(
                        channel.contrast_limits[1]
                        for channel in contrast_limits_channels)
                    contrast_limits_range_min = min(
                        layer.contrast_limits_range[0] for layer in layers)
                    contrast_limits_range_max = max(
                        layer.contrast_limits_range[1] for layer in layers)
                    contrast_limits_min = max(contrast_limits_min,
                                              contrast_limits_range_min)
                    contrast_limits_max = min(contrast_limits_max,
                                              contrast_limits_range_max)
                    self._contrast_range_slider.blockSignals(True)
                    self._contrast_range_slider.setRange(
                        (contrast_limits_range_min, contrast_limits_range_max))
                    self._contrast_range_slider.setValues(
                        (contrast_limits_min, contrast_limits_max))
                    self._contrast_range_slider.blockSignals(False)

            mean_gamma = sum(channel.gamma
                             for channel in channels) / len(channels)
            self._gamma_slider.blockSignals(True)
            self._gamma_slider.setValue(int(mean_gamma * 100))
            self._gamma_slider.blockSignals(False)

            color = tuple(int(x * 255) for x in channels[0].color)
            self._color_picker.blockSignals(True)
            self._color_picker.color = QColor(*color)
            self._color_picker.blockSignals(False)

            index = self._blending_combo_box.findText(channels[0].blending,
                                                      Qt.MatchFixedString)
            self._blending_combo_box.blockSignals(True)
            self._blending_combo_box.setCurrentIndex(index)
            self._blending_combo_box.blockSignals(False)

            index = self._interpolation_combo_box.findText(
                channels[0].interpolation, Qt.MatchFixedString)
            self._interpolation_combo_box.blockSignals(True)
            self._interpolation_combo_box.setCurrentIndex(index)
            self._interpolation_combo_box.blockSignals(False)
Example #3
0
class FitParam(object):
    def __init__(
        self,
        name,
        value,
        min,
        max,
        logscale=False,
        steps=5000,
        format="%.3f",
        size_offset=0,
        unit="",
    ):
        self.name = name
        self.value = value
        self.min = min
        self.max = max
        self.logscale = logscale
        self.steps = steps
        self.format = format
        self.unit = unit
        self.prefix_label = None
        self.lineedit = None
        self.unit_label = None
        self.slider = None
        self.button = None
        self._widgets = []
        self._size_offset = size_offset
        self._refresh_callback = None
        self.dataset = FitParamDataSet(title=_("Curve fitting parameter"))

    def copy(self):
        """Return a copy of this fitparam"""
        return self.__class__(
            self.name,
            self.value,
            self.min,
            self.max,
            self.logscale,
            self.steps,
            self.format,
            self._size_offset,
            self.unit,
        )

    def create_widgets(self, parent, refresh_callback):
        self._refresh_callback = refresh_callback
        self.prefix_label = QLabel()
        font = self.prefix_label.font()
        font.setPointSize(font.pointSize() + self._size_offset)
        self.prefix_label.setFont(font)
        self.button = QPushButton()
        self.button.setIcon(get_icon("settings.png"))
        self.button.setToolTip(
            _("Edit '%s' fit parameter properties") % self.name)
        self.button.clicked.connect(lambda: self.edit_param(parent))
        self.lineedit = QLineEdit()
        self.lineedit.editingFinished.connect(self.line_editing_finished)
        self.unit_label = QLabel(self.unit)
        self.slider = QSlider()
        self.slider.setOrientation(Qt.Horizontal)
        self.slider.setRange(0, self.steps - 1)
        self.slider.valueChanged.connect(self.slider_value_changed)
        self.update(refresh=False)
        self.add_widgets([
            self.prefix_label,
            self.lineedit,
            self.unit_label,
            self.slider,
            self.button,
        ])

    def add_widgets(self, widgets):
        self._widgets += widgets

    def get_widgets(self):
        return self._widgets

    def set_scale(self, state):
        self.logscale = state > 0
        self.update_slider_value()

    def set_text(self, fmt=None):
        style = "<span style='color: #444444'><b>%s</b></span>"
        self.prefix_label.setText(style % self.name)
        if self.value is None:
            value_str = ""
        else:
            if fmt is None:
                fmt = self.format
            value_str = fmt % self.value
        self.lineedit.setText(value_str)
        self.lineedit.setDisabled(self.value == self.min
                                  and self.max == self.min)

    def line_editing_finished(self):
        try:
            self.value = float(self.lineedit.text())
        except ValueError:
            self.set_text()
        self.update_slider_value()
        self._refresh_callback()

    def slider_value_changed(self, int_value):
        if self.logscale:
            total_delta = np.log10(1 + self.max - self.min)
            self.value = (self.min + 10**(total_delta * int_value /
                                          (self.steps - 1)) - 1)
        else:
            total_delta = self.max - self.min
            self.value = self.min + total_delta * int_value / (self.steps - 1)
        self.set_text()
        self._refresh_callback()

    def update_slider_value(self):
        if self.value is None or self.min is None or self.max is None:
            self.slider.setEnabled(False)
            if self.slider.parent() and self.slider.parent().isVisible():
                self.slider.show()
        elif self.value == self.min and self.max == self.min:
            self.slider.hide()
        else:
            self.slider.setEnabled(True)
            if self.slider.parent() and self.slider.parent().isVisible():
                self.slider.show()
            if self.logscale:
                value_delta = max([np.log10(1 + self.value - self.min), 0.0])
                total_delta = np.log10(1 + self.max - self.min)
            else:
                value_delta = self.value - self.min
                total_delta = self.max - self.min
            intval = int(self.steps * value_delta / total_delta)
            self.slider.blockSignals(True)
            self.slider.setValue(intval)
            self.slider.blockSignals(False)

    def edit_param(self, parent):
        update_dataset(self.dataset, self)
        if self.dataset.edit(parent=parent):
            restore_dataset(self.dataset, self)
            if self.value > self.max:
                self.max = self.value
            if self.value < self.min:
                self.min = self.value
            self.update()

    def update(self, refresh=True):
        self.unit_label.setText(self.unit)
        self.slider.setRange(0, self.steps - 1)
        self.update_slider_value()
        self.set_text()
        if refresh:
            self._refresh_callback()
class AnimationWidget(QWidget):
    """Widget for interatviely making animations using the napari viewer.

    Parameters
    ----------
    viewer : napari.Viewer
        napari viewer.

    Attributes
    ----------
    viewer : napari.Viewer
        napari viewer.
    animation : napari_animation.Animation
        napari-animation animation in sync with the GUI.
    """
    def __init__(self, viewer: Viewer, parent=None):
        super().__init__(parent=parent)

        # Store reference to viewer and create animation
        self.viewer = viewer
        self.animation = Animation(self.viewer)

        # Initialise User Interface
        self.keyframesListControlWidget = KeyFrameListControlWidget(
            animation=self.animation, parent=self)
        self.keyframesListWidget = KeyFramesListWidget(
            self.animation.key_frames, parent=self)
        self.frameWidget = FrameWidget(parent=self)
        self.saveButton = QPushButton("Save Animation", parent=self)
        self.animationSlider = QSlider(Qt.Horizontal, parent=self)
        self.animationSlider.setToolTip("Scroll through animation")
        self.animationSlider.setRange(0, len(self.animation._frames) - 1)

        # Create layout
        self.setLayout(QVBoxLayout())
        self.layout().addWidget(self.keyframesListControlWidget)
        self.layout().addWidget(self.keyframesListWidget)
        self.layout().addWidget(self.frameWidget)
        self.layout().addWidget(self.saveButton)
        self.layout().addWidget(self.animationSlider)

        # establish key bindings and callbacks
        self._add_keybind_callbacks()
        self._add_callbacks()

    def _add_keybind_callbacks(self):
        """Bind keys"""
        self._keybindings = [
            ("Alt-f", self._capture_keyframe_callback),
            ("Alt-r", self._replace_keyframe_callback),
            ("Alt-d", self._delete_keyframe_callback),
            ("Alt-a", lambda e: self.animation.key_frames.select_next()),
            ("Alt-b", lambda e: self.animation.key_frames.select_previous()),
        ]
        for key, cb in self._keybindings:
            self.viewer.bind_key(key, cb)

    def _add_callbacks(self):
        """Establish callbacks"""
        self.keyframesListControlWidget.deleteButton.clicked.connect(
            self._delete_keyframe_callback)
        self.keyframesListControlWidget.captureButton.clicked.connect(
            self._capture_keyframe_callback)
        self.saveButton.clicked.connect(self._save_callback)
        self.animationSlider.valueChanged.connect(self._on_slider_moved)
        self.animation._frames.events.n_frames.connect(self._nframes_changed)

        keyframe_list = self.animation.key_frames
        keyframe_list.events.inserted.connect(self._on_keyframes_changed)
        keyframe_list.events.removed.connect(self._on_keyframes_changed)
        keyframe_list.events.changed.connect(self._on_keyframes_changed)
        keyframe_list.selection.events.active.connect(
            self._on_active_keyframe_changed)

    def _input_state(self):
        """Get current state of input widgets as {key->value} parameters."""
        return {
            "steps": int(self.frameWidget.stepsSpinBox.value()),
            "ease": self.frameWidget.get_easing_func(),
        }

    def _capture_keyframe_callback(self, event=None):
        """Record current key-frame"""
        self.animation.capture_keyframe(**self._input_state())

    def _replace_keyframe_callback(self, event=None):
        """Replace current key-frame with new view"""
        self.animation.capture_keyframe(**self._input_state(), insert=False)

    def _delete_keyframe_callback(self, event=None):
        """Delete current key-frame"""
        if self.animation.key_frames.selection.active:
            self.animation.key_frames.remove_selected()
        else:
            raise ValueError("No selected keyframe to delete !")

    def _on_keyframes_changed(self, event=None):
        has_frames = bool(self.animation.key_frames)

        self.keyframesListControlWidget.deleteButton.setEnabled(has_frames)
        self.keyframesListWidget.setEnabled(has_frames)
        self.frameWidget.setEnabled(has_frames)

    def _on_active_keyframe_changed(self, event=None):
        n_frames = len(self.animation._frames)
        active_keyframe = event.value

        if active_keyframe and n_frames:
            self.animationSlider.blockSignals(True)
            kf1_list = [
                self.animation._frames._frame_index[n][0]
                for n in range(n_frames)
            ]
            frame_index = kf1_list.index(active_keyframe)
            self.animationSlider.setValue(frame_index)
            self.animationSlider.blockSignals(False)

        self.keyframesListControlWidget.deleteButton.setEnabled(
            bool(active_keyframe))

    def _on_slider_moved(self, event=None):
        frame_index = event
        if frame_index < len(self.animation._frames) - 1:
            with self.animation.key_frames.selection.events.active.blocker():
                self.animation.set_movie_frame_index(frame_index)

    def _save_callback(self, event=None):

        if len(self.animation.key_frames) < 2:
            error_dialog = QErrorMessage()
            error_dialog.showMessage(
                f"You need at least two key frames to generate \
                an animation. Your only have {len(self.animation.key_frames)}")
            error_dialog.exec_()

        else:
            filters = (
                "Video files (*.mp4 *.gif *.mov *.avi *.mpg *.mpeg *.mkv *.wmv)"
                ";;Folder of PNGs (*)"  # sep filters with ";;"
            )
            filename, _filter = QFileDialog.getSaveFileName(
                self, "Save animation", str(Path.home()), filters)
            if filename:
                self.animation.animate(filename)

    def _nframes_changed(self, event):
        has_frames = bool(event.value)
        self.animationSlider.setEnabled(has_frames)
        self.animationSlider.blockSignals(has_frames)
        self.animationSlider.setMaximum(event.value - 1 if has_frames else 0)

    def closeEvent(self, ev) -> None:
        # release callbacks
        for key, _ in self._keybindings:
            self.viewer.bind_key(key, None)
        return super().closeEvent(ev)