Exemplo n.º 1
0
class ProgramWidget(QWidget):
    def __init__(self, parent):
        super().__init__(parent)

        self.image_widget = ImageWidget(parent)
        self.hist_widget = HistogramWidget(parent)
        self.coord_label = QLabel("", self)
        self.pixel_rgb_label = QLabel('', self)
        self.pixel_hsv_label = QLabel('', self)
        self.pixel_lab_label = QLabel('', self)

        self.hsv_checkbox = QCheckBox("Shift HSV")
        self.hsv_checkbox.toggled.connect(self.slider_update)

        h_slider_box, self.h_slider = self._get_slider_box(
            "H:", -180, 180, 60, self.slider_update
        )

        s_slider_box, self.s_slider = self._get_slider_box(
            "S:", -100, 100, 10, self.slider_update
        )

        v_slider_box, self.v_slider = self._get_slider_box(
            "V:", -100, 100, 10, self.slider_update
        )

        self._set_default()

        self.image_widget.selection_update.connect(self.selection_upd)

        hbox = QHBoxLayout()
        hbox.addWidget(self.image_widget, 20)

        vbox = QVBoxLayout()
        vbox.addWidget(self.hist_widget)
        vbox.addWidget(self.coord_label)
        vbox.addWidget(self.pixel_rgb_label)
        vbox.addWidget(self.pixel_hsv_label)
        vbox.addWidget(self.pixel_lab_label)

        vbox.addWidget(self.hsv_checkbox)
        vbox.addLayout(h_slider_box)
        vbox.addLayout(s_slider_box)
        vbox.addLayout(v_slider_box)

        self._filter_buttons(vbox)

        hbox.addLayout(vbox, 1)

        self.setLayout(hbox)

    def _filter_buttons(self, vbox):
        self.filter_rbtn = QButtonGroup()
        hbox = QHBoxLayout()

        radio1 = QRadioButton("Отключить", self)
        self.filter_rbtn.addButton(radio1, 0)
        hbox.addWidget(radio1)
        radio1.setChecked(True)

        radio2 = QRadioButton("Гаусса", self)
        self.filter_rbtn.addButton(radio2, 1)
        hbox.addWidget(radio2)

        radio3 = QRadioButton("Собеля", self)
        self.filter_rbtn.addButton(radio3, 2)
        hbox.addWidget(radio3)

        radio4 = QRadioButton("Габора", self)
        self.filter_rbtn.addButton(radio4, 3)
        hbox.addWidget(radio4)

        vbox.addLayout(hbox)

        slider_box, self.sigma_slider = self._get_slider_box("σ", 0, 10, 0.5, self._filter_change)

        vbox.addLayout(slider_box)
        self.filter_rbtn.buttonClicked.connect(self._filter_change)

    def _filter_change(self):
        filter_id = self.filter_rbtn.checkedId()
        self._update_sigma_slider(filter_id)
        param1 = self.sigma_slider.value() / 10
        self.image_widget.set_filter(filter_id, param1)

    def _update_sigma_slider(self, filter_id):
        if filter_id == 1:
            self.sigma_slider.setEnabled(True)
            self.sigma_slider.setText("σ")
            self.sigma_slider.setMinimum(0)
            self.sigma_slider.setMaximum(100)
            self.sigma_slider.setTickInterval(1)
        elif filter_id == 3:
            self.sigma_slider.setEnabled(True)
            self.sigma_slider.setText("θ")
            # self.sigma_slider.setValue(1)
            self.sigma_slider.setMinimum(0)
            self.sigma_slider.setMaximum(3600)
            self.sigma_slider.setTickInterval(50)
        else:
            self.sigma_slider.setText("-")
            self.sigma_slider.setDisabled(True)

    def _get_slider_box(self, label_name, _min, _max, _interval, callback, layout=Qt.Horizontal) -> Tuple[QBoxLayout, QSlider]:
        slider = QSlider(layout, self)
        slider.setMinimum(_min)
        slider.setMaximum(_max)
        slider.setTickPosition(QSlider.TicksBelow)
        slider.setTickInterval(_interval)

        slider.sliderReleased.connect(callback)

        box = QHBoxLayout()
        label = QLabel(label_name, self)
        box.addWidget(label)
        box.addWidget(slider)
        slider._label = label

        def setText(text):
            slider._label.setText(text)

        slider.setText = setText

        return box, slider

    def _set_default(self):
        self.coord_label.setText("Select pixel")
        self.pixel_rgb_label.setText("Select pixel")
        self.pixel_hsv_label.setText("Select pixel")
        self.pixel_lab_label.setText("Select pixel")

        self.h_slider.setValue(0)

    def slider_update(self):
        if self.hsv_checkbox.isChecked():
            self.image_widget.shift_hsv = self.h_slider.value(), self.s_slider.value(), self.v_slider.value()
        else:
            self.image_widget.shift_hsv = [0, 0, 0]

    def selection_upd(self):
        img = self.image_widget.selected(self.hist_widget.speed)
        print(img.width(), img.height())

        coord = self.image_widget.selection_img
        self.coord_label.setText("{}, {} x {}, {}".format(
            coord.left(), coord.top(),
            coord.right(), coord.bottom()
        ))

        if 1 == img.width() == img.height():
            pixel = QColor(img.pixel(0, 0))

            self.pixel_rgb_label.setText(
                "R:{}, G:{}, B:{}".format(pixel.red(), pixel.green(), pixel.blue())
            )

            self.pixel_hsv_label.setText(
                "H:{}, S:{}, V:{}".format(pixel.hue(), pixel.saturation(), pixel.value())
            )

            self.pixel_lab_label.setText(
                "L:{:.1f}, A:{:.1f}, B:{:.1f}".format(*pixel.lab())
            )
        else:
            self.pixel_rgb_label.setText("Select one pixel")
            self.pixel_hsv_label.setText("Select one pixel")
            self.pixel_lab_label.setText("Select one pixel")

        self.hist_widget.calc_image(img)

    def set_image(self, image: QImage):
        self.image_widget.set_image(image)
Exemplo n.º 2
0
class View(Subject, QMainWindow):
    """Main window class, and the View of the MVC pattern.

    Attributes:
        model: The model of the MVC pattern.
        menu_bar: A menu bar derived from QMenuBar.
        tool_bar: A tool bar derived from QToolBar.
        exif_area: The widget containing any available EXIF data.
        image_area: The widget containing the displayed image.
        status_bar: A status bar derived from QStatusBar.
        about: The classic "About" informative widget (actually a new, separate window).
    """
    def __init__(self, model):
        """Inits the class."""
        Subject.__init__(self)
        QMainWindow.__init__(self)

        # Set model
        self.model = model

        # Create interface elements
        self.menu_bar = MenuBar(self)
        self.tool_bar = ToolBar(self)
        self.exif_area = ExifWidget(self)
        self.image_area = ImageWidget(self)
        self.status_bar = StatusBar()
        about_text = 'IEViewer 1.0' \
                     '<br><br>' \
                     'Copyright © 2021 by' \
                     '<br>' \
                     'Paula Mihalcea' \
                     '<br>' \
                     '<a href="mailto:[email protected]">[email protected]</a>' \
                     '<br><br>' \
                     'This program uses PyQt5, a comprehensive set of Python bindings for Qt v5. Qt is a set of cross-platform C++ libraries that implement high-level APIs for accessing many aspects of modern desktop and mobile systems.\n' \
                     '<br><br>' \
                     'PyQt5 is copyright © Riverbank Computing Limited. Its homepage is <a href="https://www.riverbankcomputing.com/software/pyqt/">https://www.riverbankcomputing.com/software/pyqt/</a>.' \
                     '<br><br>' \
                     'No genasi were harmed in the making of this application. <a href="https://www.dndbeyond.com/races/genasi#WaterGenasi">#GenasiLivesMatter#NereisThalian</a>'
        self.about = AboutWidget('About IEViewer',
                                 about_text,
                                 image_path='icons/about_img.png')

        # Disable GUI elements that are unavailable when no image is opened
        self.menu_bar.disable_widgets()
        self.exif_area.hide()

        # Set layout
        self.setCentralWidget(Layout(self).central_widget)

        # Set window properties
        self.set_window_properties()

        # Install additional event filters
        self.image_area.installEventFilter(self)

    def set_window_properties(self):
        """Sets some main window properties."""
        self.statusBar()
        self.setStatusTip('Ready.')
        self.setWindowTitle(
            'IEViewer')  # Window title (the one in the title bar)
        self.resize(
            512,
            256)  # These default dimensions should be fine for most displays

    def get_open_file_dialog(self, caption, filter):
        """Opens an "Open File" dialog and returns a file path. If no file has been selected and the user has pressed "Cancel" or closed the dialog, then it returns None."""
        file_path, _ = QFileDialog.getOpenFileName(caption=caption,
                                                   directory='',
                                                   filter=filter)
        if file_path == '':
            return None
        else:
            return file_path

    def get_save_file_dialog(self, caption, filter):
        """Opens a "Save As" dialog and returns a file path and format. If no name has been entered and the user has pressed "Cancel" or closed the dialog, then it returns None."""
        file_path, format = QFileDialog.getSaveFileName(
            caption=caption, directory='', filter=filter
        )  # By omitting the directory argument (empty string, ''), the dialog should remember the last directory (depends on operating system)
        if file_path == '':
            return None, None
        else:
            format = format.split(' ')
            return file_path, format[0]

    def show_message_box(self, title, text, icon=QMessageBox.Information):
        """Opens a simple message box window, with some text and a customizable icon."""
        info_box = QMessageBox(self)

        if icon is not None:
            info_box.setIcon(icon)
        info_box.setWindowTitle(title)
        info_box.setText(text)

        info_box.show()

    def event(self, e):
        """Defines the default status bar message (when nothing else is displayed)."""
        if e.type() == QEvent.StatusTip:
            if e.tip() == '':
                e = QStatusTipEvent('Ready.')
        return super().event(e)

    def open(self):
        """Only needed to notify observers (i.e. the controller) that they should load an image."""
        self.set_state('open')

    def load_image(self):
        """Displays the image contained in the model."""
        # Get original image dimensions
        width = self.model.image.width()
        height = self.model.image.height()

        # Recalculate image dimensions so as to have a maximum dimension (height or width) of 512 pixels
        if width >= height and width > 512:
            w = 512
            h = int(512 * height / width)
        elif height >= width and height > 512:
            w = int(512 * width / height)
            h = 512
        else:
            w = width
            h = height

        # Generate and set a pixmap from the image
        self.image_area.set_image(self.model.image, w, h)

        # Resize the window and pixmap (generated from the image),
        # so as to display correctly the window title, menu bar and image (actually, its pixmap)
        #
        # The diff parameter is another piece of PyQt5 magic.
        # Without it, an image larger than 512 pixels might be displayed smaller
        # than the maximum 512 that it has been resized to
        # (despite having been resized to 512 pixels).

        diff = 130  # This value is good for both Windows 10 and Ubuntu 20.04
        if w < 280:  # Again, this should suffice for both OSs
            self.resize(280, h + diff)
        else:
            self.resize(w, h + diff)
        self.image_area.pixmap.scaled(w, h)

        # Add EXIF data from the model
        self.exif_area.load_exif()

        # Update window title
        self.setWindowTitle('IEViewer - ' + self.model.filename)

        # Enable menus
        self.menu_bar.enable_widgets()

    def save(self):
        """Saves the modified image. Uses the stored original image, not the modified version currently displayed in the window."""
        self.model.set_image(
            self.model.image.transformed(
                QTransform().rotate(self.image_area.rot),
                Qt.SmoothTransformation))
        self.set_state('save')

    def saveas(self):
        """Saves the modified image with another name. Uses the stored original image, not the modified version currently displayed in the window."""
        self.model.set_image(
            self.model.image.transformed(
                QTransform().rotate(self.image_area.rot),
                Qt.SmoothTransformation))
        self.set_state('saveas')

    def eventFilter(self, widget, event):
        """Adds new behavior for certain events."""

        # Resize event
        # Or what should happen to the image widget when the user resizes the main window
        if event.type(
        ) == QEvent.Resize and widget is self.image_area and self.image_area.pixmap is not None:
            self.image_area.setPixmap(
                QPixmap.fromImage(self.model.modified_image).scaled(
                    self.image_area.width(),
                    self.image_area.height(),
                    aspectRatioMode=Qt.KeepAspectRatio,
                    transformMode=Qt.SmoothTransformation))
            # Update new dimensions for later use
            self.image_area.w = self.image_area.width()
            self.image_area.h = self.image_area.height()
            return True

        return QMainWindow.eventFilter(self, widget, event)

    def close(self):
        """Closes an image."""
        self.set_state('close')

        # Clear image area widget and adjust layout
        self.image_area.clear_image()
        self.exif_area = ExifWidget(self)
        self.setCentralWidget(Layout(self).central_widget)

        # Disable unavailable menus
        self.menu_bar.disable_widgets()

        # Hide EXIF area (if visible) and show blank image widget
        self.exif_area.hide()
        self.image_area.show()

        # Update window title
        self.setWindowTitle('IEViewer')

    def exit(self):
        """Only needed to notify observers (i.e. the controller) that they should exit the application."""
        self.set_state('exit')

    def rotate(self, degree):
        """Base function for rotation.

        Subsequent rotation functions are only needed as hooks for menu actions; actual rotations happen here.
        Here, the View directly changes the Model. Arguably, this is a violation of the MVC pattern. It happens that, for some reason, returning a reference to the rotated image to the Controller in order to have the Controller itself change the Model does not work, probably because it only returns a reference and not a copy of the rotated image. PyQt5 does not allow deep copies of its objects, so the following seems to be the only way to make it work.
        """
        self.model.set_modified_image(
            self.model.image.transformed(
                QTransform().rotate(self.image_area.rot + degree),
                Qt.SmoothTransformation))  # Rotate image
        self.image_area.set_image(self.model.modified_image, self.image_area.w,
                                  self.image_area.h)  # Display rotated image
        self.image_area.rot += degree  # Update rotation history

    def rotate180(self):
        """Rotate the displayed image by 180 degrees."""
        self.rotate(180)

    def rotate90c(self):
        """Rotate the displayed image by 90 degrees clock wise."""
        self.rotate(90)

    def rotate90cc(self):
        """Rotate the displayed image by 90 degrees clock wise."""
        self.rotate(-90)

    def reset_image(self):
        """Reset the displayed image to its original orientation."""
        self.rotate(-self.image_area.rot)

    def show_exif(self):
        """Display EXIF data (and at the same time hide the image)."""
        if self.exif_area.isHidden():
            self.image_area.hide()
            self.exif_area.show()
        else:
            self.image_area.show()
            self.exif_area.hide()

    def about(self):
        """Display info about the program."""
        self.about.show()