使用OpenCV和PyQT编写的图片显示器

简要

为了避免重复编写pyqt控件,先写好此代码,方便使用。实现了以下基础功能:

  • 选择图片
  • 显示图片
  • 保存图片

效果

选择图片:
使用OpenCV和PyQT编写的图片显示器,实现图片选择显示和保存-LMLPHP
显示图片:
使用OpenCV和PyQT编写的图片显示器,实现图片选择显示和保存-LMLPHP
保存图片:
使用OpenCV和PyQT编写的图片显示器,实现图片选择显示和保存-LMLPHP

代码

import copy
import cv2
import sys

from PyQt5.QtWidgets import (
    QApplication,
    QMainWindow,
    QFileDialog,
    QMessageBox,
    QWidget,
    QVBoxLayout,
    QHBoxLayout,
    QFrame,
    QToolButton,
    QSpacerItem,
    QLabel,
    QSizePolicy,
)

from PyQt5.QtGui import (
    QImage,
    QPixmap,
    QIcon,
    QPainter,
    QPen,
)

from PyQt5.QtCore import (
    QSize,
    QRect,
    Qt,
)

from PyQt5 import QtWidgets,QtGui
class ImageLabel(QLabel):
    """"
    用于显示图片的 Label
    """

    def __init__(self, parent=None):
        super().__init__(parent)
        self.x0 = 0
        self.y0 = 0
        self.x1 = 0
        self.y1 = 0
        self.flag = False  # 标记是否能够绘制矩形
        self.__isClear = False  # 标记是否是清除矩形
        self.setAlignment(Qt.AlignCenter)  # 居中对齐
        self.setFrameShape(QtWidgets.QFrame.Box)  # 设置边框
        self.setStyleSheet("border-width: 1px;border-style: solid;border-color: rgb(218, 218, 218)")
        self.setText("")
        self.__w, self.__h = 0, 0
        self.pixmap_width, self.pixmap_height = 0, 0  # pixmap 的宽度、高度
        self.pixmap_x_start, self.pixmap_y_start = 0, 0  # pixmap 在 label 中的起点位置
        self.pixmap_x_end, self.pixmap_y_end = 0, 0  # pixamp 在 label 中的终点位置
        self.img_x_start, self.img_y_start = 0, 0  # 图片中选择的矩形区域的起点位置
        self.img_x_end, self.img_y_end = 0, 0  # 图片中选择的矩形区域的终点位置
        self.autoFillBackground()

    # 鼠标点击事件
    def mousePressEvent(self, event):
        # self.flag = True
        # 鼠标点击,相当于开始绘制矩形,将 isClear 置为 False
        self.__isClear = False
        self.x0 = event.x()
        self.y0 = event.y()
        # 计算 Pixmap 在 Label 中的位置
        self.__w, self.__h = self.width(), self.height()
        self.pixmap_x_start = (self.__w - self.pixmap_width) / 2
        self.pixmap_y_start = (self.__h - self.pixmap_height) / 2
        self.pixmap_x_end = self.pixmap_x_start + self.pixmap_width
        self.pixmap_y_end = self.pixmap_y_start + self.pixmap_height

    # 鼠标释放事件
    def mouseReleaseEvent(self, event):
        # self.flag = False
        self.setCursor(Qt.ArrowCursor)  # 鼠标释放,矩形已经绘制完毕,恢复鼠标样式

    # 鼠标移动事件
    def mouseMoveEvent(self, event):
        if self.flag:
            self.x1 = event.x()
            self.y1 = event.y()
            self.update()

    def setPixmap(self, pixmap):
        super().setPixmap(pixmap)
        self.pixmap_width, self.pixmap_height = pixmap.width(), pixmap.height()

    # 绘制事件
    def paintEvent(self, event):
        super().paintEvent(event)

        # 判断是否是清除
        if self.__isClear:
            return  # 是清除,则不需要执行下面的绘制操作。即此次 paint 事件没有绘制操作,因此界面中没有绘制的图形(从而相当于清除整个界面中已有的图形)

        # 判断用户起始位置是否在图片区域,只有在图片区域才画选择的矩形图
        if (self.pixmap_x_start <= self.x0 <= self.pixmap_x_end) \
                and (self.pixmap_y_start <= self.y0 <= self.pixmap_y_end):
            # 判断结束位置是否在图片区域内,如果超过,则直接设置成图片区域的终点
            if self.x1 > self.pixmap_x_end:
                self.x1 = self.pixmap_x_end
            elif self.x1 < self.pixmap_x_start:
                self.x1 = self.pixmap_x_start

            if self.y1 > self.pixmap_y_end:
                self.y1 = self.pixmap_y_end
            elif self.y1 < self.pixmap_y_start:
                self.y1 = self.pixmap_y_start
            rect = QRect(self.x0, self.y0, self.x1 - self.x0, self.y1 - self.y0)
            painter = QPainter(self)
            painter.setPen(QPen(Qt.red, 2, Qt.SolidLine))
            painter.drawRect(rect)
            # 计算矩形区域在图片中的位置
            self.img_x_start = int(self.x0 - self.pixmap_x_start)
            self.img_x_end = int(self.x1 - self.pixmap_x_start)
            self.img_y_start = int(self.y0 - self.pixmap_y_start)
            self.img_y_end = int(self.y1 - self.pixmap_y_start)

    def clearRect(self):
        # 清除
        self.__isClear = True
        self.update()
class MyWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initialize()

    def initialize(self):
        """
        Initialize the user interface and set initial states.
        """
        self.setup_ui()
        self.set_initial_states()

    def setup_ui(self):
        """
        Set up the main window layout and components.
        """
        self.setWindowTitle("Image Editor")
        self.resize(926, 806)

        self.central_widget = QWidget(self)
        self.central_layout = QVBoxLayout(self.central_widget)
        self.setCentralWidget(self.central_widget)

        # Title bar with open, save, and undo buttons
        self.title_bar = QFrame(self.central_widget)
        self.title_bar.setFrameShape(QFrame.StyledPanel)
        self.title_bar.setFrameShadow(QFrame.Raised)
        self.title_layout = QHBoxLayout(self.title_bar)

        self.btn_open = QToolButton(self.title_bar)
        self.btn_save = QToolButton(self.title_bar)
        self.btn_undo = QToolButton(self.title_bar)

        self.title_layout.addWidget(self.btn_open)
        self.title_layout.addWidget(self.btn_save)
        self.title_layout.addWidget(self.btn_undo)

        # Control bar with confirm and cancel buttons
        self.control_bar = QFrame(self.title_bar)
        self.control_layout = QHBoxLayout(self.control_bar)
        self.btn_confirm = QToolButton(self.control_bar)
        self.btn_cancel = QToolButton(self.control_bar)

        self.control_layout.addWidget(self.btn_confirm)
        self.control_layout.addWidget(self.btn_cancel)
        self.title_layout.addWidget(self.control_bar)

        # Add a spacer item to the title layout
        self.title_layout.addItem(QSpacerItem(100, 20, QSizePolicy.Expanding, QSizePolicy.Minimum))

        # Image frame with an ImageLabel widget to display images
        self.img_frame = QFrame(self.central_widget)
        self.img_frame.setFrameShape(QFrame.StyledPanel)
        self.img_frame.setFrameShadow(QFrame.Raised)
        self.img_layout = QHBoxLayout(self.img_frame)
        self.img_display = ImageLabel(self.img_frame)
        self.img_layout.addWidget(self.img_display)

        self.central_layout.addWidget(self.title_bar)
        self.central_layout.addWidget(self.img_frame)

        # Set button text, icons, styles, and layout styles
        self.set_buttons_text_icons()
        self.set_buttons_styles()
        self.set_layout_styles()

    def set_initial_states(self):
        """
        Set initial states for UI components.
        """
        self.control_bar.setVisible(False)

    def set_buttons_text_icons(self):
        """
        Set button texts, icons, and style for open and save buttons.
        """
        self.btn_open.setText("打开")
        self.btn_open.setIcon(QIcon("./icon/open.png"))
        self.btn_open.setIconSize(QSize(36, 36))
        self.btn_open.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
        self.btn_open.clicked.connect(self.open_img)

        self.btn_save.setText("保存")
        self.btn_save.setIcon(QIcon("./icon/save.png"))
        self.btn_save.setIconSize(QSize(36, 36))
        self.btn_save.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
        self.btn_save.clicked.connect(self.save_img)

    def set_buttons_styles(self):
        """
        Set styles for all buttons and the control bar.
        """
        transparent_button_style = "background: rgba(0, 0, 0, 0); color: rgb(255, 255, 255);"
        gray_button_style = "background: rgb(80, 80, 80); color: rgb(255, 255, 255);"

        buttons = [self.btn_open, self.btn_save, self.btn_undo, self.btn_confirm, self.btn_cancel]
        for btn in buttons:
            btn.setStyleSheet(transparent_button_style)

        self.control_bar.setStyleSheet(gray_button_style)

    def set_layout_styles(self):
        """
        Set styles for central layout and title bar.
        """
        self.central_layout.setContentsMargins(0, 0, 0, 0)
        self.central_layout.setSpacing(0)

        self.title_bar.setMinimumSize(QSize(0, 55))
        self.title_bar.setMaximumSize(QSize(188888, 55))

        self.control_bar.setMinimumSize(QSize(0, 45))
        self.control_bar.setMaximumSize(QSize(120, 45))

        self.img_frame.setMinimumSize(QSize(100, 0))

        font = QtGui.QFont()
        font.setPointSize(8)
        self.setFont(font)
        self.btn_open.setFont(font)
        self.btn_save.setFont(font)

        self.central_widget.setStyleSheet("background: rgb(252, 255, 255);")
        self.title_bar.setStyleSheet("background: rgb(60, 60, 60);")

    def open_img(self):
        """
        Open an image file using a file dialog and display it.
        """
        img_name, img_type = QFileDialog.getOpenFileName(
            self, "打开图片", "", "*.jpg;*.png;*.jpeg"
        )
        if img_name == "" or img_name is None:
            self.show_warning_message_box("未选择图片")
            return

        img = cv2.imread(img_name)
        self.show_image(img)
        self.current_img = img
        self.last_img = self.current_img
        self.original_img = copy.deepcopy(self.current_img)
        self.original_img_path = img_name

    def show_image(self, img, is_grayscale=False):
        """
        Display an image in the ImageLabel widget.

        Args:
            img (numpy.ndarray): The image to display (in BGR or grayscale format).
            is_grayscale (bool, optional): Whether the image is grayscale. Defaults to False.
        """
        if len(img.shape) == 3:  # Color image
            img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)  # Convert BGR to RGB

        height, width, channels = img.shape
        bytes_per_line = channels * width

        if len(img.shape) == 2:  # Grayscale image
            format = QImage.Format_Grayscale8
            bytes_per_line *= 1  # Treat grayscale image as having one channel
        else:  # RGB image
            format = QImage.Format_RGB888

        qimage = QImage(img.data, width, height, bytes_per_line, format)
        pixmap = QPixmap.fromImage(qimage)
        if pixmap.width() > 600 or pixmap.height() > 600:
            pixmap = pixmap.scaled(600, 600, Qt.KeepAspectRatio, Qt.SmoothTransformation)
        self.img_display.setPixmap(pixmap)
        self.img_display.repaint()

    def crop_image(self, src_img, x_start, x_end, y_start, y_end):
        """
        Crop an image.

        Args:
            src_img (numpy.ndarray): The source image to crop.
            x_start (int): Starting x-coordinate of the crop region.
            x_end (int): Ending x-coordinate of the crop region.
            y_start (int): Starting y-coordinate of the crop region.
            y_end (int): Ending y-coordinate of the crop region.

        Returns:
            numpy.ndarray: The cropped image.
        """
        return src_img[y_start:y_end, x_start:x_end]

    def show_warning_message_box(self, msg):
        """
        Show a warning message box with the given message.

        Args:
            msg (str): The message to display.
        """
        QMessageBox.warning(self, "警告", msg, QMessageBox.Ok)

    def show_info_message_box(self, msg):
        """
        Show an information message box with the given message.

        Args:
            msg (str): The message to display.
        """
        QMessageBox.information(self, "提示", msg, QMessageBox.Ok)

    def save_img(self):
        """
        Save the current image to a file using a file dialog.
        """
        if self.current_img is None:
            self.show_warning_message_box("未选择图片")
            return

        ext_name = self.original_img_path[self.original_img_path.rindex(".") :]
        img_path, img_type = QFileDialog.getSaveFileName(
            self, "保存图片", self.original_img_path, f"*{ext_name}"
        )
        cv2.imwrite(img_path, self.current_img)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MyWindow()
    window.show()
    sys.exit(app.exec_())
04-06 15:23