基于PyQt5的UI界面开发——图像与视频的加载与显示
介绍
这里我们的主要目标是实现一个基于PyQt5和OpenCV的图像浏览和视频播放应用。用户可以选择本地的图像或视频文件夹,进行图像自动播放和图像切换以及视频播放和调用摄像头等操作,并且支持图像保存功能。项目的核心设计包括文件路径选择、图像或视频的显示、自动播放、图像保存等功能。
UI的初步设计
整体的设计如图所示,主体的窗口名我命名为ImageLoadWindow,其他控件的标记我已经写在了上方图片中,大家可以和下面的代码进行对比。
视频的加载
文件路径的读取
当我们在开发视频处理或视频播放应用时,经常需要让用户选择视频文件以及指定保存路径。
SetPFilePath 方法用于选择视频文件。它使用 QFileDialog.getOpenFileName 弹出一个文件选择对话框,允许用户从文件系统中选择一个视频文件。如果用户选择了文件,该方法将更新界面上的文本和内部变量。
def SetPFilePath(self):
filename, _ = QFileDialog.getOpenFileName(self, "选择视频文件", '.', "Video Files (*.mp4 *.avi *.mkv)")
if filename:
self.PFilePathLiEd.setText(filename)
self.PFilePath = filename
SetSFilePath 方法用于设置保存路径。它使用 QFileDialog.getExistingDirectory 弹出一个目录选择对话框,允许用户选择一个目录作为保存路径。如果用户选择了目录,该方法将更新界面上的文本和内部变量。
def SetSFilePath(self):
dirname = QFileDialog.getExistingDirectory(self, "选择保存目录", '.')
if dirname:
self.SFilePathLiEd.setText(dirname)
self.SFilePath = dirname + '/'
这是一个很好用的框架,以后的ui设计可以直接对着我这里命名,这样在创建新的时候,可以实现代码的移植。
视频文件的读取和显示
这里,我们想让用户选择本地的视频文件或者通过摄像头实时获取视频流,并将视频帧显示在 PyQt5 的窗口中。
SetPFilePath,用于选择视频文件,并将文件路径显示在输入框中。此处的SetSFilePath,用于选择保存目录路径,但实际在下面的代码中并没有添加相关的逻辑。
当我们选择了视频文件,并点击运行按钮后,在左侧的OutputLab(QLable控件)显示了视频的播放。当不没有选择视频文件,就会根据LoadWayCBox(Combo Box控件)的索引去选择我们的摄像头,请注意此处是下拉框选项,索引是从0开始,符合摄像头的读取顺序,如果要用在其他项目时,要注意是否正确。
from PyQt5.QtWidgets import QApplication, QMainWindow, QFileDialog, QMessageBox
from PyQt5.QtGui import QPixmap
from PyQt5.QtCore import QTimer
import qimage2ndarray
import cv2
from load_image import Ui_ImageLoadWindow
class ImageLoadWindow(QMainWindow, Ui_ImageLoadWindow):
def __init__(self):
super().__init__()
self.setupUi(self)
self.PrepParameters()
self.CallBackFunctions()
self.timer = QTimer(self)
self.timer.timeout.connect(self.update_frame)
self.capture = None # 用于存储视频捕获对象
self.frame = None # 用于存储当前帧
def PrepParameters(self):
# 选择文件路径
self.PFilePath = ""
# 保存文件路径
self.SFilePath = ""
self.PFilePathLiEd.setText(self.PFilePath)
self.SFilePathLiEd.setText(self.SFilePath)
def CallBackFunctions(self):
self.PFilePathBt.clicked.connect(self.SetPFilePath)
self.SFilePathBt.clicked.connect(self.SetSFilePath)
self.RunBt.clicked.connect(self.start_video_playback)
self.ExitBt.clicked.connect(self.close_application)
def SetPFilePath(self):
filename, _ = QFileDialog.getOpenFileName(self, "选择视频文件", '.', "Video Files (*.mp4 *.avi *.mkv)")
if filename:
self.PFilePathLiEd.setText(filename)
self.PFilePath = filename
def SetSFilePath(self):
dirname = QFileDialog.getExistingDirectory(self, "选择保存目录", '.')
if dirname:
self.SFilePathLiEd.setText(dirname)
self.SFilePath = dirname + '/'
def start_video_playback(self):
# 选择视频文件后,启动视频播放
if self.PFilePath:
self.capture = cv2.VideoCapture(self.PFilePath)
else:
load_way = self.LoadWayCBox.currentIndex()
self.capture = cv2.VideoCapture(load_way)
if self.capture.isOpened():
self.timer.start(30) # 每30毫秒更新一次帧
else:
print("无法打开视频文件或摄像头!")
def update_frame(self):
ret, self.frame = self.capture.read()
if ret:
rgb_image = cv2.cvtColor(self.frame, cv2.COLOR_BGR2RGB)
# 使用 qimage2ndarray 将 NumPy 数组转换为 QImage
qimg = qimage2ndarray.array2qimage(rgb_image)
pixmap = QPixmap.fromImage(qimg)
self.OutputLab.setPixmap(pixmap)
else:
self.timer.stop() # 停止定时器,当视频播放完时
def close_application(self):
"""关闭应用程序"""
reply = QMessageBox.question(self, '退出', '您确定要退出程序吗?', QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.Yes:
if self.capture:
self.capture.release()
QApplication.quit()
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
window = ImageLoadWindow()
window.show()
sys.exit(app.exec_())
视频文件的处理
如下所示,即为选择了视频文件后运行的情况。如果不选就会调用本地的摄像头。
关于对视频处理的一块,你可以在下面注释的地方完成逻辑的实现。
def update_frame(self):
ret, self.frame = self.capture.read()
if ret:
# self.frame = ... 此处实现
rgb_image = cv2.cvtColor(self.frame, cv2.COLOR_BGR2RGB)
qimg = qimage2ndarray.array2qimage(rgb_image)
pixmap = QPixmap.fromImage(qimg)
self.OutputLab.setPixmap(pixmap)
else:
self.timer.stop()
图像的加载
文件路径的读取
SetPFilePath方法允许用户选择一个目录,目录中的所有文件都会被读取并存储在 self.pimage_list 中。为了仅获取图片文件,我过滤了所有非图片文件,确保 self.pimage_list 只包含有,比如.jpg、.png、.jpeg、.bmp 和 .gif 等图片格式。
def SetPFilePath(self):
dirname = QFileDialog.getExistingDirectory(self, "浏览", '.')
if dirname:
self.PFilePathLiEd.setText(dirname)
self.PFilePath=dirname+'/'
self.pimage_list = [f for f in os.listdir(self.PFilePath) if
f.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif'))]
SetSFilePath方法选择一个保存目录。选择完成后,保存路径会被显示在 SFilePathLiEd 文本框中,并将 SFilePath 变量更新为所选目录的路径。
def SetSFilePath(self):
dirname = QFileDialog.getExistingDirectory(self, "浏览", '.')
if dirname:
self.SFilePathLiEd.setText(dirname)
self.SFilePath=dirname+'/'
图像的切换
为了进一步丰富我们ui的功能,这里我们提供了两个方式去显示图像,第一种是通过控件显示图片,另外一种可以实现自动播放的功能。
下面为更新设计后的UI:
这里我们先要理清楚我们接下来要做什么,首先选择处理文件的路径,以及保存的路径,并点击运行按钮,显示第一张图像,如果没有选择保存路径,则只显示图像而不进行保存。
通过Last和Next键去控制图像的显示,并且第一张图像Last键被禁用,最后一张图像Next键被禁用。图像的显示和保存都通过opencv实现。
from PyQt5.QtWidgets import QApplication, QMainWindow, QFileDialog, QMessageBox
from PyQt5.QtGui import QPixmap
import qimage2ndarray
import cv2
import os
from load_image import Ui_ImageLoadWindow
class ImageLoadWindow2(QMainWindow, Ui_ImageLoadWindow):
def __init__(self):
super().__init__()
self.setupUi(self)
self.PrepParameters()
self.CallBackFunctions()
def PrepParameters(self):
self.PFilePath = r'' # 初始化路径为空
self.SFilePath = r'' # 初始化保存路径为空
self.PFilePathLiEd.setText(self.PFilePath)
self.SFilePathLiEd.setText(self.SFilePath)
self.pimage_list = [] # 存储图像文件列表
self.current_image_idx = -1 # 用于追踪当前显示的图像索引
self.current_image = None # 用于存储当前显示的图像数据
def CallBackFunctions(self):
self.PFilePathBt.clicked.connect(self.SetPFilePath)
self.SFilePathBt.clicked.connect(self.SetSFilePath)
self.RunBt.clicked.connect(self.start_image_show) # 运行按钮,开始显示图像
self.LastBt.clicked.connect(self.show_last_image) # 上一张按钮
self.NextBt.clicked.connect(self.show_next_image) # 下一张按钮
self.ExitBt.clicked.connect(self.close_application) # 退出按钮
def SetPFilePath(self):
dirname = QFileDialog.getExistingDirectory(self, "浏览", '.')
if dirname:
self.PFilePathLiEd.setText(dirname)
self.PFilePath = dirname + '/'
self.pimage_list = [f for f in os.listdir(self.PFilePath)
if f.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif'))]
self.current_image_idx = -1 # 重置图像索引
def SetSFilePath(self):
dirname = QFileDialog.getExistingDirectory(self, "浏览", '.')
if dirname:
self.SFilePathLiEd.setText(dirname)
self.SFilePath = dirname + '/'
def start_image_show(self):
if not self.pimage_list:
QMessageBox.warning(self, '警告', '没有找到图像文件!', QMessageBox.Ok)
return
# 如果保存路径为空,弹出警告窗口询问是否继续
if not self.SFilePath:
reply = QMessageBox.question(
self, '保存路径未选择', '保存路径未选择,是否继续显示图像?',
QMessageBox.Yes | QMessageBox.No, QMessageBox.No
)
if reply == QMessageBox.No:
return # 如果用户选择 No,返回不继续
self.current_image_idx = 0 # 从第一张图像开始显示
self.show_image(self.current_image_idx)
# 启用和禁用按钮
self.update_navigation_buttons()
def show_image(self, idx):
if 0 <= idx < len(self.pimage_list):
# 获取图像文件路径
img_path = os.path.join(self.PFilePath, self.pimage_list[idx])
image = cv2.imread(img_path)
if image is not None:
# 转换为RGB模式
rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
self.current_image = image
qimg = qimage2ndarray.array2qimage(rgb_image)
pixmap = QPixmap.fromImage(qimg)
self.OutputLab.setPixmap(pixmap)
# 如果有保存路径,才进行自动保存
if self.SFilePath:
self.auto_save_image(self.pimage_list[idx])
else:
print(f"无法读取图像: {img_path}")
def auto_save_image(self, image_filename):
if self.current_image is None:
return
save_path = os.path.join(self.SFilePath, image_filename)
os.makedirs(self.SFilePath, exist_ok=True)
cv2.imwrite(save_path, self.current_image)
print(f"图像已自动保存到: {save_path}")
def show_last_image(self):
if self.current_image_idx > 0:
self.current_image_idx -= 1
self.show_image(self.current_image_idx)
self.update_navigation_buttons()
def show_next_image(self):
if self.current_image_idx < len(self.pimage_list) - 1:
self.current_image_idx += 1
self.show_image(self.current_image_idx)
self.update_navigation_buttons()
def update_navigation_buttons(self):
if self.current_image_idx == 0:
self.LastBt.setEnabled(False) # 禁用上一张按钮
else:
self.LastBt.setEnabled(True) # 启用上一张按钮
if self.current_image_idx == len(self.pimage_list) - 1:
self.NextBt.setEnabled(False) # 禁用下一张按钮
else:
self.NextBt.setEnabled(True) # 启用下一张按钮
def close_application(self):
"""关闭应用程序"""
reply = QMessageBox.question(self, '退出', '您确定要退出程序吗?', QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.Yes:
QApplication.quit()
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
window = ImageLoadWindow2()
window.show()
sys.exit(app.exec_())
逻辑流程概述:
- 用户选择图像文件夹和保存路径(可选)。
- 点击运行按钮开始显示第一张图像。
- 如果保存路径未选择,程序会弹出提示,询问用户是否继续显示图像。
- 用户可以通过上一张(
LastBt
)和下一张(NextBt
)按钮切换图像。- 当图像显示时,若保存路径已选择,程序会自动保存当前显示的图像。
- 如果用户关闭程序,会弹出确认退出的对话框。
实现自动播放功能
下面就是完整的设计了,由于考虑到自动播放,所以我在这里添加了暂停恢复键,用于播放过程中的暂停。
好的,这里我们还是需要理清楚我们需要做什么。首先在选择好图像文件夹路径和保存路径后,如果勾选了自动播放就会禁用掉Last和Next键,点击运行会弹出窗口是否进行循环播放,如果选择进行循环播放,则图像会显示在OutputLab上,如果选择了否也会进行自动播放,只是在播放完成之后,可以切换到Last和Next键进行图片的切换。
在自动播放过程中,如果点击了暂停键,文字变为恢复,再点击文字变为暂停,在点击了暂停后,再点击运行可以重新进行选择。
from PyQt5.QtWidgets import QApplication, QMainWindow, QFileDialog, QMessageBox
from PyQt5.QtGui import QPixmap
from PyQt5.QtCore import QTimer
import qimage2ndarray
import cv2
import os
from load_image import Ui_ImageLoadWindow
class ImageLoadWindow2(QMainWindow, Ui_ImageLoadWindow):
def __init__(self):
super().__init__()
self.setupUi(self)
self.PrepParameters()
self.CallBackFunctions()
def PrepParameters(self):
self.PFilePath = r'' # 初始化路径为空
self.SFilePath = r'' # 初始化保存路径为空
self.PFilePathLiEd.setText(self.PFilePath)
self.SFilePathLiEd.setText(self.SFilePath)
self.pimage_list = [] # 存储图像文件列表
self.current_image_idx = -1 # 用于追踪当前显示的图像索引
self.current_image = None # 用于存储当前显示的图像数据
self.is_autoplay = False # 是否启用自动播放标志
self.is_loop = False # 是否循环播放标志
self.is_paused = False # 是否暂停自动播放标志
def CallBackFunctions(self):
self.PFilePathBt.clicked.connect(self.SetPFilePath)
self.SFilePathBt.clicked.connect(self.SetSFilePath)
self.RunBt.clicked.connect(self.start_image_show) # 运行按钮,开始显示图像
self.LastBt.clicked.connect(self.show_last_image) # 上一张按钮
self.NextBt.clicked.connect(self.show_next_image) # 下一张按钮
self.ExitBt.clicked.connect(self.close_application) # 退出按钮
self.AutoplaycheckBox.stateChanged.connect(self.toggle_autoplay) # 连接自动播放勾选框
self.StopRecoverBt.clicked.connect(self.pause_or_resume_autoplay) # 连接暂停/恢复按钮
def SetPFilePath(self):
dirname = QFileDialog.getExistingDirectory(self, "浏览", '.')
if dirname:
self.PFilePathLiEd.setText(dirname)
self.PFilePath = dirname + '/'
self.pimage_list = [f for f in os.listdir(self.PFilePath)
if f.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif'))]
self.current_image_idx = -1 # 重置图像索引
def SetSFilePath(self):
dirname = QFileDialog.getExistingDirectory(self, "浏览", '.')
if dirname:
self.SFilePathLiEd.setText(dirname)
self.SFilePath = dirname + '/'
def toggle_autoplay(self, state):
"""根据勾选状态设置是否启用自动播放"""
self.is_autoplay = state == 2 # 2表示勾选状态
self.StopRecoverBt.setEnabled(self.is_autoplay) # 只有启用自动播放时才启用暂停/恢复按钮
def start_image_show(self):
if not self.pimage_list:
QMessageBox.warning(self, '警告', '没有找到图像文件!', QMessageBox.Ok)
return
# 如果保存路径为空,弹出警告窗口询问是否继续
if not self.SFilePath:
reply = QMessageBox.question(
self, '保存路径未选择', '保存路径未选择,是否继续显示图像?',
QMessageBox.Yes | QMessageBox.No, QMessageBox.No
)
if reply == QMessageBox.No:
return # 如果用户选择 No,返回不继续
self.current_image_idx = 0 # 从第一张图像开始显示
self.show_image(self.current_image_idx)
if self.is_autoplay:
# 如果是自动播放模式,弹出对话框确认是否循环播放
reply = QMessageBox.question(
self, '循环播放', '是否循环播放图像?',
QMessageBox.Yes | QMessageBox.No, QMessageBox.No
)
if reply == QMessageBox.Yes:
self.is_loop = True # 启用循环播放
else:
self.is_loop = False # 不循环播放
# 启动自动播放
self.start_autoplay()
else:
# 启用和禁用按钮
self.update_navigation_buttons()
# 如果是自动播放并且已经暂停了,点击运行时,确保按钮文本恢复为'暂停'
if self.is_paused:
self.StopRecoverBt.setText('暂停') # 恢复为暂停按钮文本
def start_autoplay(self):
"""启动自动播放模式"""
if self.is_paused:
# 如果当前是暂停状态,直接恢复定时器
self.is_paused = False
self.StopRecoverBt.setText('暂停') # 修改按钮文本为 '暂停'
self.LastBt.setEnabled(False) # 禁用上一张按钮
self.NextBt.setEnabled(False) # 禁用下一张按钮
# 使用QTimer定时器进行自动播放
if not hasattr(self, 'autoplay_timer'): # 如果定时器不存在,则创建
self.autoplay_timer = QTimer(self)
self.autoplay_timer.timeout.connect(self.next_image_in_autoplay)
self.autoplay_timer.start(1000) # 每1秒切换一张图像
def next_image_in_autoplay(self):
"""自动播放下一张图像"""
if self.is_paused:
return # 如果已暂停,不进行任何操作
if self.current_image_idx < len(self.pimage_list) - 1:
self.current_image_idx += 1
self.show_image(self.current_image_idx)
else:
if self.is_loop:
self.current_image_idx = 0 # 如果是循环播放,回到第一张
self.show_image(self.current_image_idx)
else:
self.stop_autoplay() # 自动播放完成后停止并恢复按钮
def stop_autoplay(self):
"""停止自动播放"""
if hasattr(self, 'autoplay_timer'):
self.autoplay_timer.stop()
self.update_navigation_buttons() # 恢复按钮状态
def pause_or_resume_autoplay(self):
"""暂停或恢复自动播放"""
if self.is_paused:
self.is_paused = False
self.StopRecoverBt.setText('暂停') # 修改按钮文本为 '暂停'
self.start_autoplay() # 恢复播放
else:
self.is_paused = True
self.StopRecoverBt.setText('恢复') # 修改按钮文本为 '恢复'
self.autoplay_timer.stop() # 暂停定时器
def show_image(self, idx):
if 0 <= idx < len(self.pimage_list):
# 获取图像文件路径
img_path = os.path.join(self.PFilePath, self.pimage_list[idx])
image = cv2.imread(img_path)
if image is not None:
# 转换为RGB模式
rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
self.current_image = image
qimg = qimage2ndarray.array2qimage(rgb_image)
pixmap = QPixmap.fromImage(qimg)
self.OutputLab.setPixmap(pixmap)
# 如果有保存路径,才进行自动保存
if self.SFilePath:
self.auto_save_image(self.pimage_list[idx])
else:
print(f"无法读取图像: {img_path}")
def auto_save_image(self, image_filename):
if self.current_image is None:
return
save_path = os.path.join(self.SFilePath, image_filename)
os.makedirs(self.SFilePath, exist_ok=True)
cv2.imwrite(save_path, self.current_image)
print(f"图像已自动保存到: {save_path}")
def show_last_image(self):
if self.current_image_idx > 0:
self.current_image_idx -= 1
self.show_image(self.current_image_idx)
self.update_navigation_buttons()
def show_next_image(self):
if self.current_image_idx < len(self.pimage_list) - 1:
self.current_image_idx += 1
self.show_image(self.current_image_idx)
self.update_navigation_buttons()
def update_navigation_buttons(self):
"""更新上一张和下一张按钮的状态"""
if self.current_image_idx == 0:
self.LastBt.setEnabled(False) # 禁用上一张按钮
else:
self.LastBt.setEnabled(True) # 启用上一张按钮
if self.current_image_idx == len(self.pimage_list) - 1:
self.NextBt.setEnabled(False) # 禁用下一张按钮
else:
self.NextBt.setEnabled(True) # 启用下一张按钮
def close_application(self):
"""关闭应用程序"""
reply = QMessageBox.question(self, '退出', '您确定要退出程序吗?', QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.Yes:
QApplication.quit()
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
window = ImageLoadWindow2()
window.show()
sys.exit(app.exec_())
图像文件的处理
可以在注释处进行图像处理。
def show_image(self, idx):
if 0 <= idx < len(self.pimage_list):
# 获取图像文件路径
img_path = os.path.join(self.PFilePath, self.pimage_list[idx])
image = cv2.imread(img_path)
# image = ... 图像处理
if image is not None:
# 转换为RGB模式
rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
self.current_image = image
qimg = qimage2ndarray.array2qimage(rgb_image)
pixmap = QPixmap.fromImage(qimg)
self.OutputLab.setPixmap(pixmap)
# 如果有保存路径,才进行自动保存
if self.SFilePath:
self.auto_save_image(self.pimage_list[idx])
else:
print(f"无法读取图像: {img_path}")
其他
这里记录一下使用plainTextEdit显示信息。由于内容有限,所以不另外再写一篇博客了。
名字就叫plainTextEdit,这里我就不修改了。
这里只需要做两个修改就好了。
在初始化的时候PrepParameters,添加一个
self.plainTextEdit.clear() # 清除文本框内容
你也可以和按钮结合在一起。
还有一个是将内容显示在plainTextEdit上面:
运行效果如下所示:
总结
本篇展示了如何使用PyQt5和OpenCV实现一个图像浏览与视频播放应用,涵盖了文件路径选择、图像/视频显示、自动播放或切换、暂停与恢复、图像保存等多种功能。本篇的代码具有较高的灵活性,你只需要按照相同的命名方式就能够实现代码的移植。希望通过本篇,为其他大家提供有用的参考和实现思路。
另外,需要注意的一点是,在循环播放的时候图像会重复的保存,关于这一部分的相关逻辑,我不想再修改,也不怎么碍事。最后,我这里不提供原来设计ui文件,因为,我写的已经很清楚了,大家能够自己简单的设计,没必要再找我要。