PyQt5 GUI 开发基础

PyQt5 是 Python 中用于构建图形用户界面 (GUI) 的强大工具包。它提供了丰富的功能来设计和实现复杂的桌面应用。在本文中,我们将介绍如何使用 PyQt5 和 OpenCV 构建一个简单的图像处理工具。这个工具允许用户加载图像、进行基本的图像处理操作(如灰度化、去噪和锐化),并保存处理后的图像。我们将逐步解析代码,展示如何实现这些功能。

安装 PyQt5

为了使用 PyQt5,首先需要将其安装到本地环境中。可以通过 pip 工具完成安装过程:

pip install PyQt5

这一步骤确保了开发者能够访问 PyQt5 提供的各种模块和类。

创建第一个 PyQt5 应用程序

# 导入必要的库
import sys  # 用于与Python解释器交互,例如退出程序
import cv2  # OpenCV库,用于图像处理
import numpy as np  # NumPy库,用于数值计算
from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QPushButton, QVBoxLayout, QWidget, QFileDialog  # PyQt5的GUI组件
from PyQt5.QtGui import QPixmap, QImage  # 用于图像显示
from PyQt5.QtCore import Qt  # 包含Qt的常量和枚举

# 定义一个类,继承自QMainWindow,表示主窗口
class ImageProcessingApp(QMainWindow):
    def __init__(self):
        super().__init__()  # 调用父类的构造函数
        self.setWindowTitle("图像处理系统")  # 设置窗口标题
        self.setGeometry(100, 100, 800, 600)  # 设置窗口的位置和大小(x, y, width, height)

        # 创建中心部件,所有布局和控件都将放置在这个部件中
        self.central_widget = QWidget()
        self.setCentralWidget(self.central_widget)  # 将中心部件设置为窗口的中心部件

        # 创建一个垂直布局,用于管理控件的排列
        self.layout = QVBoxLayout(self.central_widget)

        # 创建一个标签,用于显示原始图像
        self.original_label = QLabel("原始图像", self)  # 初始化标签,显示文本"原始图像"
        self.original_label.setAlignment(Qt.AlignCenter)  # 设置标签内容居中对齐
        self.layout.addWidget(self.original_label)  # 将标签添加到布局中

        # 创建一个标签,用于显示处理后的图像
        self.processed_label = QLabel("处理后的图像", self)  # 初始化标签,显示文本"处理后的图像"
        self.processed_label.setAlignment(Qt.AlignCenter)  # 设置标签内容居中对齐
        self.layout.addWidget(self.processed_label)  # 将标签添加到布局中

        # 创建一个按钮,用于加载图像
        self.load_button = QPushButton("加载图像", self)  # 初始化按钮,显示文本"加载图像"
        self.layout.addWidget(self.load_button)  # 将按钮添加到布局中
        self.load_button.clicked.connect(self.load_image)  # 将按钮的点击事件连接到load_image方法

        # 创建一个按钮,用于对图像进行灰度化处理
        self.process_button = QPushButton("灰度化处理", self)  # 初始化按钮,显示文本"灰度化处理"
        self.layout.addWidget(self.process_button)  # 将按钮添加到布局中
        self.process_button.clicked.connect(self.process_image)  # 将按钮的点击事件连接到process_image方法

        # 创建一个按钮,用于对图像进行高斯模糊处理
        self.blur_button = QPushButton("高斯模糊", self)  # 初始化按钮,显示文本"高斯模糊"
        self.layout.addWidget(self.blur_button)  # 将按钮添加到布局中
        self.blur_button.clicked.connect(self.blur_image)  # 将按钮的点击事件连接到blur_image方法

        # 初始化图像变量,用于存储原始图像和处理后的图像
        self.original_image = None
        self.processed_image = None

    def load_image(self):
        """加载图像"""
        # 使用QFileDialog获取用户选择的图像文件路径
        file_path, _ = QFileDialog.getOpenFileName(self, "选择图像文件", "", "Image Files (*.png *.jpg *.bmp *.gif)")
        if file_path:  # 如果用户选择了文件
            # 使用OpenCV加载图像
            self.original_image = cv2.imread(file_path)
            if self.original_image is not None:  # 如果图像加载成功
                # 将OpenCV图像转换为QPixmap并显示在原始图像标签中
                self.display_image(self.original_label, self.original_image)

    def process_image(self):
        """对图像进行灰度化处理"""
        if self.original_image is not None:  # 如果原始图像已加载
            # 使用OpenCV进行灰度化处理
            self.processed_image = cv2.cvtColor(self.original_image, cv2.COLOR_BGR2GRAY)
            # 将处理后的图像转换为QPixmap并显示在处理后图像标签中
            self.display_image(self.processed_label, self.processed_image)

    def blur_image(self):
        """对图像进行高斯模糊处理"""
        if self.original_image is not None:  # 如果原始图像已加载
            # 使用OpenCV进行高斯模糊处理
            self.processed_image = cv2.GaussianBlur(self.original_image, (15, 15), 0)
            # 将处理后的图像转换为QPixmap并显示在处理后图像标签中
            self.display_image(self.processed_label, self.processed_image)

    def display_image(self, label, image):
        """将OpenCV图像转换为QPixmap并显示在QLabel中"""
        # 判断图像是否为彩色图像
        if len(image.shape) == 3:  # 彩色图像
            height, width, channel = image.shape  # 获取图像的高度、宽度和通道数
            bytesPerLine = 3 * width  # 计算每行的字节数
            # 将OpenCV图像转换为QImage
            qImg = QImage(image.data, width, height, bytesPerLine, QImage.Format_RGB888).rgbSwapped()
        else:  # 灰度图像
            height, width = image.shape  # 获取图像的高度和宽度
            bytesPerLine = width  # 计算每行的字节数
            # 将OpenCV图像转换为QImage
            qImg = QImage(image.data, width, height, bytesPerLine, QImage.Format_Grayscale8)

        # 将QImage转换为QPixmap
        pixmap = QPixmap.fromImage(qImg)
        # 将QPixmap设置到标签中,并保持宽高比
        label.setPixmap(pixmap.scaled(400, 400, Qt.KeepAspectRatio))
        label.show()  # 显示标签

# 主程序入口
if __name__ == "__main__":
    app = QApplication(sys.argv)  # 创建应用程序对象
    window = ImageProcessingApp()  # 创建窗口实例
    window.show()  # 显示窗口
    sys.exit(app.exec_())  # 启动应用程序的事件循环

 里面详细解释了代码的用途。主要就是信号与槽是PyQt5 里处理事件的关键机制。信号代表事件的发生,槽则是对该事件做出响应的函数。当信号被发射时,与之连接的槽函数就会被调用。

结果展示

这里有一点要注意,博主小晨在加载原始图像的时候会异常,一开始以为是代码的问题,反复调整还是不行,最后原因是因为博主将python文件储存时的路径有中文,python就会异常的加载错误,将这个问题矫正之后就可以正常的使用了。

这是主播加载了一个数字图像处理中最经典的照片并进行了高斯模糊处理后的结果。

创一个更高级的PyQt5 应用程序

import sys
import cv2
import numpy as np
from PyQt5.QtWidgets import (
    QApplication, QMainWindow, QWidget, QLabel, QPushButton,
    QFileDialog, QMessageBox, QVBoxLayout, QHBoxLayout, QFrame
)
from PyQt5.QtGui import QPixmap, QImage
from PyQt5.QtCore import Qt


class ImageProcessor(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("PyQt演示")
        self.resize(900, 600)

        # 存储原始图像和处理后的图像数据
        self.image_data = {}

        # 创建主窗口小部件并设置布局
        main_widget = QWidget()
        self.setCentralWidget(main_widget)
        main_layout = QVBoxLayout(main_widget)

        # 设置主窗口的样式
        main_widget.setStyleSheet("""
            QWidget {
                background-color: #f0f4f8;
            }
            QLabel {
                border: 2px solid #aaa;
                border-radius: 10px;
                background-color: white;
                padding: 5px;
                box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
            }
            QPushButton {
                font-size: 15px;
                padding: 8px 18px;
                min-width: 100px;
            }
        """)

        # 创建顶部布局:加载按钮和保存按钮
        top_layout = QHBoxLayout()
        load_btn = QPushButton("📂 加载图片")
        save_btn = QPushButton("💾 保存图像")
        load_btn.clicked.connect(self.load_image)  # 连接加载按钮的事件
        save_btn.clicked.connect(self.save_image)  # 连接保存按钮的事件
        top_layout.addWidget(load_btn)
        top_layout.addWidget(save_btn)
        top_layout.addStretch()
        main_layout.addLayout(top_layout)
        # 添加水平分割线
        main_layout.addWidget(self._h_line())

        # 创建用于显示原始图像和处理后图像的布局
        img_layout = QHBoxLayout()
        self.original_label = QLabel()  # 用于显示原始图像
        self.processed_label = QLabel()  # 用于显示处理后的图像

        # 设置标签的固定大小和对齐方式
        for label in (self.original_label, self.processed_label):
            label.setFixedSize(400, 400)
            label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        # 添加原始图像和处理图像标签
        img_layout.addWidget(self.original_label)
        img_layout.addWidget(self._v_line())
        img_layout.addWidget(self.processed_label)
        img_layout.setSpacing(0)
        main_layout.addLayout(img_layout)
        main_layout.addWidget(self._h_line())

        # 创建底部按钮布局:灰度化、去噪、锐化
        bottom_layout = QHBoxLayout()
        for text, func in [("⚫灰度化", "gray"), ("🔍去噪", "denoise"),
                           ("✨锐化", "sharpen")]:
            btn = QPushButton(text)
            # 绑定按钮点击事件
            btn.clicked.connect(lambda _, f=func: self.process(f))
            bottom_layout.addWidget(btn)
        bottom_layout.addStretch()
        main_layout.addLayout(bottom_layout)

    def _h_line(self):
        line = QFrame()
        line.setFrameShape(QFrame.Shape.HLine)
        line.setFrameShadow(QFrame.Shadow.Sunken)
        line.setStyleSheet("color: #ccc;")
        return line

    def _v_line(self):
        line = QFrame()
        line.setFrameShape(QFrame.Shape.VLine)
        line.setFrameShadow(QFrame.Shadow.Sunken)
        line.setStyleSheet("color: #ccc;")
        return line

    def load_image(self):
        """加载图像"""
        file, _ = QFileDialog.getOpenFileName(self, "选择图片", "", "图片文件 (*.png *.jpg *.bmp)")
        if file:
            img = cv2.imread(file)
            if img is None:
                QMessageBox.warning(self, "错误", "无法加载图像")  # 显示错误消息
                return
            self.image_data['original'] = img  # 存储原始图像
            self.image_data['processed'] = img.copy()  # 存储处理后的图像(初始为原图)
            self.show_image(img, self.original_label)  # 显示原图像

    def save_image(self):
        """保存图像"""
        if 'processed' not in self.image_data:
            QMessageBox.warning(self, "提示", "没有可保存的图像")  # 没有处理图像时提示
            return
        file, _ = QFileDialog.getSaveFileName(self, "保存图像", "", "PNG (*.png);;JPG (*.jpg)")
        if file:
            cv2.imwrite(file, self.image_data['processed'])  # 保存处理图像
            QMessageBox.information(self, "成功", f"图像已保存:{file}")  # 提示保存成功

    def process(self, mode):
        """根据选择的模式处理图像"""
        if 'original' not in self.image_data:
            QMessageBox.warning(self, "提示", "请先加载图片")  # 提示用户加载图像
            return

        img = self.image_data['original']  # 获取原始图像
        if mode == "gray":  # 灰度化处理
            result = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
            result = cv2.cvtColor(result, cv2.COLOR_GRAY2BGR)  # 转回三通道
        elif mode == "denoise":  # 去噪处理
            result = cv2.fastNlMeansDenoisingColored(img, None, 10, 10, 7, 21)
        elif mode == "sharpen":  # 锐化处理
            kernel = np.array([[-1, -1, -1], [-1, 9, -1], [-1, -1, -1]])
            result = cv2.filter2D(img, -1, kernel)

        else:
            return

        self.image_data['processed'] = result  # 存储处理后的图像
        self.show_image(result, self.processed_label)  # 显示处理后的图像

    def show_image(self, img, label):
        """显示图像"""
        rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)  # 将BGR转为RGB格式
        h, w, ch = rgb.shape
        bytes_per_line = ch * w
        q_img = QImage(rgb.data, w, h, bytes_per_line, QImage.Format.Format_RGB888)  # 转为QImage格式
        label.setPixmap(QPixmap.fromImage(q_img).scaled(400, 400, Qt.AspectRatioMode.KeepAspectRatio))  # 设置显示图像


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = ImageProcessor()
    window.show()  # 显示主窗口
    sys.exit(app.exec())

代码解析

主窗口设计

我们的工具基于 PyQt5 的 QMainWindow 类。主窗口包含以下部分:

  • 顶部按钮:用于加载图像和保存图像。

  • 图像显示区域:分别显示原始图像和处理后的图像。

  • 底部按钮:提供灰度化、去噪和锐化三种图像处理功能。

以下是主窗口的初始化代码:

class ImageProcessor(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("PyQt演示")
        self.resize(900, 600)

        # 存储原始图像和处理后的图像数据
        self.image_data = {}

        # 创建主窗口小部件并设置布局
        main_widget = QWidget()
        self.setCentralWidget(main_widget)
        main_layout = QVBoxLayout(main_widget)

界面布局

我们使用 PyQt5 的布局管理器来组织界面元素。以下是界面布局的主要部分:

顶部按钮

顶部有两个按钮:一个用于加载图像,另一个用于保存图像。

load_btn = QPushButton("📂 加载图片")
save_btn = QPushButton("💾 保存图像")
load_btn.clicked.connect(self.load_image)
save_btn.clicked.connect(self.save_image)
top_layout.addWidget(load_btn)
top_layout.addWidget(save_btn)

图像显示区域

图像显示区域包含两个标签(QLabel),分别用于显示原始图像和处理后的图像。

self.original_label = QLabel()  # 用于显示原始图像
self.processed_label = QLabel()  # 用于显示处理后的图像

底部按钮

底部有三个按钮,分别对应三种图像处理功能。

for text, func in [("⚫灰度化", "gray"), ("🔍去噪", "denoise"), ("✨锐化", "sharpen")]:
    btn = QPushButton(text)
    btn.clicked.connect(lambda _, f=func: self.process(f))
    bottom_layout.addWidget(btn)

图像处理功能

我们使用 OpenCV 实现了三种基本的图像处理功能:灰度化、去噪和锐化。

灰度化

灰度化是将彩色图像转换为灰度图像。我们使用 OpenCV 的 cvtColor 函数实现这一功能。

result = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
result = cv2.cvtColor(result, cv2.COLOR_GRAY2BGR)  # 转回三通道

去噪

去噪是去除图像中的噪声。我们使用 OpenCV 的 fastNlMeansDenoisingColored 函数实现这一功能。

result = cv2.fastNlMeansDenoisingColored(img, None, 10, 10, 7, 21)

锐化

锐化是增强图像的边缘。我们使用 OpenCV 的 filter2D 函数实现这一功能。

kernel = np.array([[-1, -1, -1], [-1, 9, -1], [-1, -1, -1]])
result = cv2.filter2D(img, -1, kernel)

结果演示

创新增加程序的模块

在进行了以上的介绍之后,我们可以自己来编辑一个交互界面,结合之前几周我们所学的各种数字图像处理方法,我们都可以把他们浓缩到这一个非常便捷的页面里进行操作。

我们先放入“旋转”“反转”和“模糊”三项功能。

代码

import sys
import cv2
import numpy as np
from PyQt5.QtWidgets import (
    QApplication, QMainWindow, QWidget, QLabel, QPushButton,
    QFileDialog, QMessageBox, QVBoxLayout, QHBoxLayout, QFrame
)
from PyQt5.QtGui import QPixmap, QImage
from PyQt5.QtCore import Qt


class ImageProcessor(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("PyQt演示")
        self.resize(900, 600)

        # 存储原始图像和处理后的图像数据
        self.image_data = {}

        # 创建主窗口小部件并设置布局
        main_widget = QWidget()
        self.setCentralWidget(main_widget)
        main_layout = QVBoxLayout(main_widget)

        # 设置主窗口的样式
        main_widget.setStyleSheet("""
            QWidget {
                background-color: #f0f4f8;
            }
            QLabel {
                border: 2px solid #aaa;
                border-radius: 10px;
                background-color: white;
                padding: 5px;
                box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
            }
            QPushButton {
                font-size: 15px;
                padding: 8px 18px;
                min-width: 100px;
            }
        """)

        # 创建顶部布局:加载按钮和保存按钮
        top_layout = QHBoxLayout()
        load_btn = QPushButton("📂 加载图片")
        save_btn = QPushButton("💾 保存图像")
        load_btn.clicked.connect(self.load_image)  # 连接加载按钮的事件
        save_btn.clicked.connect(self.save_image)  # 连接保存按钮的事件
        top_layout.addWidget(load_btn)
        top_layout.addWidget(save_btn)
        top_layout.addStretch()
        main_layout.addLayout(top_layout)
        # 添加水平分割线
        main_layout.addWidget(self._h_line())

        # 创建用于显示原始图像和处理后图像的布局
        img_layout = QHBoxLayout()
        self.original_label = QLabel()  # 用于显示原始图像
        self.processed_label = QLabel()  # 用于显示处理后的图像

        # 设置标签的固定大小和对齐方式
        for label in (self.original_label, self.processed_label):
            label.setFixedSize(400, 400)
            label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        # 添加原始图像和处理图像标签
        img_layout.addWidget(self.original_label)
        img_layout.addWidget(self._v_line())
        img_layout.addWidget(self.processed_label)
        img_layout.setSpacing(0)
        main_layout.addLayout(img_layout)
        main_layout.addWidget(self._h_line())

        # 创建底部按钮布局:灰度化、去噪、锐化
        bottom_layout = QHBoxLayout()
        for text, func in [("⚫灰度化", "gray"), ("🔍去噪", "denoise"),
                           ("✨锐化", "sharpen"), ("🔄旋转", "rotate"),
                           ("🔀反转", "flip"), ("🌫️模糊", "blur")]:
            btn = QPushButton(text)
            # 绑定按钮点击事件
            btn.clicked.connect(lambda _, f=func: self.process(f))
            bottom_layout.addWidget(btn)
        bottom_layout.addStretch()
        main_layout.addLayout(bottom_layout)

    def _h_line(self):
        line = QFrame()
        line.setFrameShape(QFrame.Shape.HLine)
        line.setFrameShadow(QFrame.Shadow.Sunken)
        line.setStyleSheet("color: #ccc;")
        return line

    def _v_line(self):
        line = QFrame()
        line.setFrameShape(QFrame.Shape.VLine)
        line.setFrameShadow(QFrame.Shadow.Sunken)
        line.setStyleSheet("color: #ccc;")
        return line

    def load_image(self):
        """加载图像"""
        file, _ = QFileDialog.getOpenFileName(self, "选择图片", "", "图片文件 (*.png *.jpg *.bmp)")
        if file:
            img = cv2.imread(file)
            if img is None:
                QMessageBox.warning(self, "错误", "无法加载图像")  # 显示错误消息
                return
            self.image_data['original'] = img  # 存储原始图像
            self.image_data['processed'] = img.copy()  # 存储处理后的图像(初始为原图)
            self.show_image(img, self.original_label)  # 显示原图像

    def save_image(self):
        """保存图像"""
        if 'processed' not in self.image_data:
            QMessageBox.warning(self, "提示", "没有可保存的图像")  # 没有处理图像时提示
            return
        file, _ = QFileDialog.getSaveFileName(self, "保存图像", "", "PNG (*.png);;JPG (*.jpg)")
        if file:
            cv2.imwrite(file, self.image_data['processed'])  # 保存处理图像
            QMessageBox.information(self, "成功", f"图像已保存:{file}")  # 提示保存成功

    def process(self, mode):
        """根据选择的模式处理图像"""
        if 'original' not in self.image_data:
            QMessageBox.warning(self, "提示", "请先加载图片")  # 提示用户加载图像
            return

        img = self.image_data['original']  # 获取原始图像
        if mode == "gray":  # 灰度化处理
            result = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
            result = cv2.cvtColor(result, cv2.COLOR_GRAY2BGR)  # 转回三通道
        elif mode == "denoise":  # 去噪处理
            result = cv2.fastNlMeansDenoisingColored(img, None, 10, 10, 7, 21)
        elif mode == "sharpen":  # 锐化处理
            kernel = np.array([[-1, -1, -1], [-1, 9, -1], [-1, -1, -1]])
            result = cv2.filter2D(img, -1, kernel)
        elif mode == "rotate":  # 旋转处理
            rows, cols = img.shape[:2]
            M = cv2.getRotationMatrix2D((cols / 2, rows / 2), 90, 1)
            result = cv2.warpAffine(img, M, (cols, rows))
        elif mode == "flip":  # 反转处理
            result = cv2.flip(img, 1)  # 水平反转
        elif mode == "blur":  # 模糊处理
            result = cv2.GaussianBlur(img, (5, 5), 0)

        else:
            return

        self.image_data['processed'] = result  # 存储处理后的图像
        self.show_image(result, self.processed_label)  # 显示处理后的图像

    def show_image(self, img, label):
        """显示图像"""
        rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)  # 将BGR转为RGB格式
        h, w, ch = rgb.shape
        bytes_per_line = ch * w
        q_img = QImage(rgb.data, w, h, bytes_per_line, QImage.Format.Format_RGB888)  # 转为QImage格式
        label.setPixmap(QPixmap.fromImage(q_img).scaled(400, 400, Qt.AspectRatioMode.KeepAspectRatio))  # 设置显示图像


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = ImageProcessor()
    window.show()  # 显示主窗口
    sys.exit(app.exec())

结果展示

好了这篇博客就到这里了,有什么不会的可以和博主在评论区探讨哦~

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐