Source code for plotpy.widgets.fliprotate

# -*- coding: utf-8 -*-
#
# Licensed under the terms of the BSD 3-Clause
# (see plotpy/LICENSE for details)

"""
Flip/rotate dialog
------------------

The `FlipRotate` module provides a dialog box providing essential GUI elements
for rotating (arbitrary angle) and cropping an image:

    * :py:class:`.widgets.fliprotate.FlipRotateDialog`: dialog box
    * :py:class:`.widgets.fliprotate.FlipRotateWidget`: equivalent widget

Reference
~~~~~~~~~

.. autoclass:: FlipRotateDialog
   :members:
.. autoclass:: FlipRotateWidget
   :members:
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

import numpy as np
from guidata.configtools import get_icon
from guidata.qthelpers import create_toolbutton, win32_fix_title_bar_background
from qtpy import QtCore as QC
from qtpy import QtWidgets as QW
from qtpy.QtWidgets import QWidget  # Helping out python_qt_documentation

from plotpy.config import _
from plotpy.widgets import basetransform

if TYPE_CHECKING:
    from plotpy.plot import PlotOptions
    from plotpy.plot.manager import PlotManager


class FlipRotateTransform(basetransform.BaseTransform):
    """Rotate & Crop mixin class, to be mixed with a class providing the
    get_plot method, like PlotDialog or FlipRotateWidget (see below)

    Args:
        parent (QWidget): Parent widget
        manager (PlotManager): Plot manager
    """

    def __init__(self, parent: FlipRotateWidget, manager: PlotManager) -> None:
        super().__init__(parent, manager)
        self.parent = parent
        self.manager = manager

    # ------BaseTransformMixin API----------------------------------------------
    def apply_transformation(self) -> None:
        """Apply transformation, e.g. crop or rotate"""
        angle, hflip, vflip = self.parent.get_parameters()
        x, y, _a, px, py, _hf, _vf = self.item.get_transform()
        self.item.set_transform(x, y, angle * np.pi / 180, px, py, hflip, vflip)
        self.manager.get_plot().replot()

    def compute_transformation(self) -> np.ndarray:
        """Compute transformation, return compute output array

        Returns:
            Output array
        """
        angle, hflip, vflip = self.parent.get_parameters()
        data = self.item.data.copy()
        if hflip:
            data = np.fliplr(data)
        if vflip:
            data = np.flipud(data)
        if angle:
            k = int((-angle % 360) / 90)
            data = np.rot90(data, k)
        return data


[docs] class FlipRotateDialog(QW.QDialog): """Flip & Rotate Dialog Flip and rotate a :py:class:`.TrImageItem` plot item Args: parent: Parent widget title: Window title options: Options resize_to: Resize to (width, height) edit: Edit mode toolbar: Show toolbar """ def __init__( self, parent: QWidget, title: str | None = None, options: PlotOptions | dict[str, Any] | None = None, resize_to: tuple[int, int] | None = None, edit: bool = True, toolbar: bool = False, ) -> None: super().__init__(parent) win32_fix_title_bar_background(self) if resize_to is not None: width, height = resize_to self.resize(width, height) self.button_box = None if title is None: title = _("Flip & Rotate") self.setWindowTitle(title) self.widget = FlipRotateWidget(parent=parent, options=options, toolbar=toolbar) self.setWindowFlags(QC.Qt.WindowType.Window) buttonhlayout = QW.QHBoxLayout() self.add_buttons_to_layout(buttonhlayout, edit) dialogvlayout = QW.QVBoxLayout() dialogvlayout.addWidget(self.widget.plot_widget.toolbar) dialogvlayout.addWidget(self.widget) dialogvlayout.addLayout(buttonhlayout) self.setLayout(dialogvlayout) self.transform = self.widget.transform self.plot_widget = plot_widget = self.widget.plot_widget self.manager = plot_widget.manager self.toolbar = plot_widget.toolbar self.accepted.connect(self.transform.accept_changes) self.rejected.connect(self.transform.reject_changes)
[docs] def add_buttons_to_layout(self, layout: QW.QBoxLayout, edit: bool) -> None: """Add buttons to layout Args: layout: Layout edit: Edit mode """ if edit: self.button_box = bbox = QW.QDialogButtonBox( QW.QDialogButtonBox.Ok | QW.QDialogButtonBox.Cancel ) bbox.accepted.connect(self.accept) bbox.rejected.connect(self.reject) layout.addWidget(bbox)
[docs] class FlipRotateWidget(basetransform.BaseTransformWidget): """Flip & Rotate Widget Flip and rotate a :py:class:`.TrImageItem` plot item Args: parent: Parent widget toolbar: Show toolbar options: Plot options """ ROTATION_ANGLES = [str((i - 1) * 90) for i in range(4)] def __init__( self, parent: QWidget, toolbar: bool = False, options: PlotOptions | dict[str, Any] | None = None, ): self.angle_combo = None self.hflip_btn = None self.vflip_btn = None super().__init__(parent, toolbar=toolbar, options=options) self.transform = FlipRotateTransform(self, self.plot_widget.manager) self.manager = self.plot_widget.manager
[docs] def add_buttons_to_layout(self, layout: QW.QBoxLayout) -> None: """Add tool buttons to layout Args: layout: Layout """ # Image orientation angle_label = QW.QLabel(_("Angle %s:") % "(°)") layout.addWidget(angle_label) self.angle_combo = QW.QComboBox(self) self.angle_combo.addItems(self.ROTATION_ANGLES) self.angle_combo.setCurrentIndex(1) self.angle_combo.currentIndexChanged.connect( lambda index: self.transform.apply_transformation() ) layout.addWidget(self.angle_combo) layout.addSpacing(10) # Image flipping flip_label = QW.QLabel(_("Flip:")) layout.addWidget(flip_label) hflip = create_toolbutton( self, text="", icon=get_icon("hflip.png"), toggled=lambda state: self.transform.apply_transformation(), autoraise=False, ) self.hflip_btn = hflip layout.addWidget(hflip) vflip = create_toolbutton( self, text="", icon=get_icon("vflip.png"), toggled=lambda state: self.transform.apply_transformation(), autoraise=False, ) self.vflip_btn = vflip layout.addWidget(vflip) layout.addSpacing(15) # self.add_reset_button(layout) basetransform.BaseTransformWidget.add_buttons_to_layout( self, layout, apply=False )
[docs] def apply_transformation(self) -> None: """Apply transformation""" self.transform.apply_transformation()
[docs] def reset(self) -> None: """Reset transformation""" self.angle_combo.setCurrentIndex(1) self.hflip_btn.setChecked(False) self.vflip_btn.setChecked(False)
[docs] def set_parameters(self, angle: float, hflip: bool, vflip: bool) -> None: """Set transform parameters Args: angle: Angle hflip: Horizontal flip vflip: Vertical flip """ angle_index = self.ROTATION_ANGLES.index(str(angle)) self.angle_combo.setCurrentIndex(angle_index) self.hflip_btn.setChecked(hflip) self.vflip_btn.setChecked(vflip)
[docs] def get_parameters(self) -> tuple[float, bool, bool]: """Return transform parameters Returns: Tuple of angle, horizontal flip, vertical flip """ angle = int(str(self.angle_combo.currentText())) hflip = self.hflip_btn.isChecked() vflip = self.vflip_btn.isChecked() return angle, hflip, vflip
class MultipleFlipRotateWidget(basetransform.BaseMultipleTransformWidget): """Multiple Flip & Rotate Widget Flip and rotate several :py:class:`.image.TrImageItem` plot items""" TRANSFORM_WIDGET_CLASS = FlipRotateWidget