8.1 PyQt
PyQt是一个功能强大且成熟的GUI框架,基于Qt库。它提供了丰富的组件、布局和主题选项,以及强大的功能和灵活性。PyQt的优点是它具有现代化的外观和丰富的功能,适用于复杂的GUI应用程序。
8.2 代码示例及原理解释
我们根据功能将代码分为导入必要的库、定义主窗口类ImageProcessor、
创建界面组件、创建分割线方法、加载图像方法、保存图像方法、图像处理方法、显示图像方法、主程序入口九个部分并依次进行解释。
①导入必要的库,代码示例及解释如下:
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
该部分导入了代码运行所需的各类库。sys
用于处理Python解释器相关的系统操作;cv2
用于图像的读取、处理和保存;numpy
用于数值计算;PyQt5
库中的各个模块用于创建图形用户界面。
②定义主窗口类ImageProcessor:
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;
}
""")
该部分定义了一个名为ImageProcessor
的类,继承自QMainWindow
。在 __init__
方法中,对窗口进行初始化设置,包括设置窗口标题、大小,创建存储图像数据的字典image_data
,创建主窗口小部件和布局,并设置窗口的样式。
③创建界面组件:
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
这两个方法分别用于创建水平和垂直分割线,通过 QFrame
来实现,并设置其样式。
⑤加载图像方法:
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) # 显示原图像
load_image
方法用于从文件系统中选择并加载图像。若选择的文件有效,将其读取为OpenCV图像对象,存储原始图像和处理后的图像(初始为原图),并在界面上显示原始图像。若加载失败,弹出警告消息框。
⑥保存图像方法:
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}") # 提示保存成功
save_image
方法用于将处理后的图像保存到文件系统。若没有处理后的图像,弹出提示消息框;若用户选择了保存路径,将处理后的图像保存到指定文件,并弹出保存成功的消息框。
⑦图像处理方法:
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) # 显示处理后的图像
process
方法根据用户选择的模式对原始图像进行处理。若未加载图像,弹出提示消息框;处理完成后,存储处理后的图像并在界面上显示。
⑧显示图像方法:
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)) # 设置显示图像
show_image
方法用于将OpenCV图像对象转换为QPixmap
格式,并在指定的QLabel
上显示图像。
⑨主程序入口:
if __name__ == "__main__":
app = QApplication(sys.argv)
window = ImageProcessor()
window.show() # 显示主窗口
sys.exit(app.exec())
该部分是程序的入口,创建QApplication
实例,实例化ImageProcessor
类的对象,显示主窗口,并启动应用程序的事件循环。
代码示例如上,下面我们来看一下运行结果如何。以灰度化功能为例,运行结果如图1。


8.3 拓展
知道了代码及其原理,我们便可以将以往所学功能全部加入到该界面中。我们只需要在图2部分添加功能按钮并在图3部分添加上实现该功能的代码即可。

代码示例如下:
import sys
import cv2
import skimage
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())
# 创建底部按钮布局,分为五行
button_groups = [
[("灰度化", "gray"), ("均衡化", "histogram_equalization"), ("绘制灰度直方图", "histogram")],
[("高斯噪声", "gaussian_noise"), ("椒盐噪声", "salt_pepper_noise")],
[("均值滤波", "mean_filter"), ("高斯滤波", "gaussian_filter"), ("中值滤波", "median_filter")],
[("锐化", "sharpen"), ("Roberts算子", "roberts_filter"), ("Prewitt算子", "Prewitt_filter"), ("Sobel算子", "Sobel_filter")],
[("DFT", "dft"), ("离散余弦变换", "dct"), ("理想低通滤波", "ideal_lowpass_filter"), ("理想高通滤波", "highpass_filter")]
]
for group in button_groups:
group_layout = QHBoxLayout()
for text, func in group:
btn = QPushButton(text)
btn.clicked.connect(lambda _, f=func: self.process(f))
group_layout.addWidget(btn)
group_layout.addStretch()
main_layout.addLayout(group_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'] # 获取原始图像
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
if mode == "gray": # 灰度化处理
result = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
result = cv2.cvtColor(result, cv2.COLOR_GRAY2BGR) # 转回三通道
elif mode == "histogram_equalization":
result = cv2.equalizeHist(gray_img)
result = cv2.cvtColor(result, cv2.COLOR_GRAY2BGR)
elif mode == "histogram":
hist = cv2.calcHist([gray_img], [0], None, [256], [0, 256])
hist_img = np.zeros((400, 256, 3), dtype=np.uint8)
cv2.normalize(hist, hist, 0, 400, cv2.NORM_MINMAX)
for i in range(256):
cv2.line(hist_img, (i, 400), (i, 400 - int(hist[i, 0])), (255, 255, 255))
result = hist_img
elif mode == "gaussian_noise":
img_float = img.astype(np.float64) / 255.0
img_g = skimage.util.random_noise(img_float, 'gaussian', mean=0, var=0.05)
result = (img_g * 255).astype(np.uint8)
elif mode == "salt_pepper_noise":
img_float = gray_img.astype(np.float64) / 255.0
img_s = skimage.util.random_noise(img_float, 'salt')
result = (img_s * 255).astype(np.uint8)
result = cv2.cvtColor(result, cv2.COLOR_GRAY2BGR)
elif mode == "mean_filter":
result = cv2.blur(img, (3, 3))
elif mode == "gaussian_filter":
result = cv2.GaussianBlur(img, (5, 5), 0)
elif mode == "median_filter":
result = cv2.medianBlur(img, 5)
elif mode == "sharpen":
kernel = np.array([[-1, -1, -1], [-1, 9, -1], [-1, -1, -1]])
result = cv2.filter2D(img, -1, kernel)
elif mode == "roberts_filter":
Robertx = np.array([[1, 0], [0, -1]], dtype=np.float32)
Roberty = np.array([[0, 1], [-1, 0]], dtype=np.float32)
img_x = cv2.filter2D(gray_img, -1, Robertx)
img_y = cv2.filter2D(gray_img, -1, Roberty)
result = abs(img_x) + abs(img_y)
result = cv2.cvtColor(result, cv2.COLOR_GRAY2BGR)
elif mode == "Prewitt_filter":
Prewittx = np.array([[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]], dtype=np.float32)
Prewitty = np.array([[-1, -1, -1], [0, 0, 0], [1, 1, 1]], dtype=np.float32)
img_x = cv2.filter2D(gray_img, -1, Prewittx)
img_y = cv2.filter2D(gray_img, -1, Prewitty)
result = abs(img_x) + abs(img_y)
result = cv2.cvtColor(result, cv2.COLOR_GRAY2BGR)
elif mode == "Sobel_filter":
result = cv2.Sobel(gray_img, -1, 1, 1)
result = cv2.cvtColor(result, cv2.COLOR_GRAY2BGR)
elif mode == "dft":
img_dft = cv2.dft(gray_img / 255, flags=cv2.DFT_COMPLEX_OUTPUT)
img_cen = np.fft.fftshift(img_dft)
img_dft = cv2.magnitude(img_dft[:, :, 0], img_dft[:, :, 1])
img_cen = cv2.magnitude(img_cen[:, :, 0], img_cen[:, :, 1])
result = 20 * np.log(img_cen)
result = cv2.normalize(result, None, 0, 255, cv2.NORM_MINMAX, dtype=cv2.CV_8U)
result = cv2.cvtColor(result, cv2.COLOR_GRAY2BGR)
elif mode == "dct":
img_dct = cv2.dct(np.float32(gray_img))
img_dct = 20 * np.log(abs(img_dct))
result = cv2.normalize(np.abs(img_dct), None, 0, 255, cv2.NORM_MINMAX, dtype=cv2.CV_8U)
result = cv2.cvtColor(result, cv2.COLOR_GRAY2BGR)
elif mode == "ideal_lowpass_filter":
img_f = cv2.dft(gray_img / 255, flags=cv2.DFT_COMPLEX_OUTPUT)
img_f = np.fft.fftshift(img_f)
D0 = 30
H = np.zeros(gray_img.shape)
w, h = gray_img.shape[0] // 2, gray_img.shape[1] // 2
for i in range(gray_img.shape[0]):
for j in range(gray_img.shape[1]):
if (np.sqrt((i - w) ** 2 + (j - h) ** 2)) <= D0:
H[i, j] = 1
else:
H[i, j] = 0
img_f[:, :, 0] = img_f[:, :, 0] * H
img_f[:, :, 1] = img_f[:, :, 1] * H
img_f = np.fft.ifftshift(img_f)
img_n = cv2.idft(img_f)
img_n = cv2.magnitude(img_n[:, :, 0], img_n[:, :, 1])
result = cv2.normalize(img_n, None, 0, 255, cv2.NORM_MINMAX, dtype=cv2.CV_8U)
result = cv2.cvtColor(result, cv2.COLOR_GRAY2BGR)
elif mode == "highpass_filter":
img_f = cv2.dft(gray_img / 255, flags=cv2.DFT_COMPLEX_OUTPUT)
img_f = np.fft.fftshift(img_f)
D0 = 50
H = np.zeros(gray_img.shape)
w, h = gray_img.shape[0] // 2, gray_img.shape[1] // 2
for i in range(gray_img.shape[0]):
for j in range(gray_img.shape[1]):
if (np.sqrt((i - w) ** 2 + (j - h) ** 2)) <= D0:
H[i, j] = 0
else:
H[i, j] = 1
img_f[:, :, 0] = img_f[:, :, 0] * H
img_f[:, :, 1] = img_f[:, :, 1] * H
img_f = np.fft.ifftshift(img_f)
img_n = cv2.idft(img_f)
img_n = cv2.magnitude(img_n[:, :, 0], img_n[:, :, 1])
result = cv2.normalize(img_n, None, 0, 255, cv2.NORM_MINMAX, dtype=cv2.CV_8U)
result = cv2.cvtColor(result, cv2.COLOR_GRAY2BGR)
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())
以添加高斯噪声功能为例,运行结果如图4。

8.4 总结
通过本次实验,利用PyQt5设计了图像处理的界面,使我受益匪浅。在提升我编写代码能力的同时也让我对以往所学知识进行了又一遍的巩固复习。