欢迎关注『OpenCV-PyQT项目实战 @ Youcans』系列,持续更新中
OpenCV-PyQT项目实战(1)安装与环境配置
OpenCV-PyQT项目实战(2)QtDesigner 和 PyUIC 快速入门
OpenCV-PyQT项目实战(3)信号与槽机制
OpenCV-PyQT项目实战(4)OpenCV 与PyQt的图像转换
OpenCV-PyQT项目实战(5)项目案例01:图像模糊
OpenCV-PyQT项目实战(6)项目案例02:滚动条应用
OpenCV-PyQT项目实战(7)项目案例03:鼠标框选
OpenCV-PyQT项目实战(8)项目案例04:鼠标定位
OpenCV-PyQT项目实战(9)项目案例04:视频播放
OpenCV-PyQT项目实战(10)项目案例06:键盘事件与视频抓拍
OpenCV-PyQT项目实战(11)项目案例07:摄像头操作与拍摄视频
OpenCV-PyQT项目实战(12)项目案例08:多线程视频播放
在之前的案例中,我们使用 QTime 定时器和 QThread 的方式来控制 QLabel 中的图像更新,实现视频播放或实时监控。但是,如果需要同时播放多路视频,或者在视频播放的同时进行图像处理,这种处理方法容易产生卡顿或阻塞,因此需要使用多线程解决这个问题。
进程(Process)是操作系统进行资源分配和调度运行的基本单位,可以理解为操作系统中正在执行的程序。每个应用程序都有一个自己的进程。
线程是一个基本的CPU执行单元,是程序执行的最小单元。 线程自己不拥有系统资源,必须依托于进程存活。一个线程是一个execution context,即一个CPU执行时所需要的一串指令。
每一个进程启动时都会最先产生一个线程,即主线程。然后主线程会再创建其他的子线程。
线程在程序中是独立的、并发的执行流。与分隔的进程相比,进程中线程之间的隔离程度要小,它们共享内存、文件句柄和其他进程应有的状态。
因为线程的划分尺度小于进程,使得多线程程序的并发性高。进程在执行过程之中拥有独立的内存单元,而多个线程共享内存,从而极大的提升了程序的运行效率。
线程比进程具有更高的性能,这是由于同一个进程中的线程都有共性,多个线程共享一个进程的虚拟空间。线程的共享环境包括进程代码段、进程的共有数据等,利用这些共享的数据,线程之间很容易实现通信。
操作系统在创建进程时,必须为改进程分配独立的内存空间,并分配大量的相关资源,但创建线程则简单得多。因此,使用多线程来实现并发比使用多进程的性能高得要多。
多线程(multithreading)是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。
线程从创建到消亡的过程为:
从运行状态(Running)进入阻塞状态(Blocked)有三种情况:
例程1:单线程
import timedef sayHello():print("Hello")time.sleep(1) if __name__ == '__main__':for i in range(10):sayHello()
运行时间为:10秒。
例程2:多线程
通过多线程可以完成多任务。
import time,threadingdef sayHello():print("Hello")time.sleep(1)if __name__ == '__main__':for i in range(10):# 创建子线程对象thread_obj = threading.Thread(target=sayHello)# 启动子线程对象thread_obj.start()
运行时间为:1秒。
例程3:线程的执行顺序
import time,threadingdef sing():for i in range(3):print("is singing... %d" % i)time.sleep(1)def dance():for i in range(3):print("is dancing... %d" % i)time.sleep(1)if __name__ == '__main__':print("The main thread starts to execute")t1 = threading.Thread(target=sing)t2 = threading.Thread(target=dance)t1.start()t2.start()print("Main thread execution completed")
运行结果:
The main thread starts to execute
is singing... 0
is dancing... 0
Main thread execution completed
is dancing... 1is singing... 1is singing... 2
is dancing... 2
Process finished with exit code 0
主线程执行完毕后,还要等待所有子线程执行完毕后才结束程序运行。
(1) 导入线程模块import threading
(2) 通过线程类创建进程对象线程对象 = threading.Thread(target = 任务名)
(3) 启动线程执行任务线程对象.start()
语法:
thread.Thread(group=Nore,targt=None,args=(),kwargs={},*,daemon=None)
参数说明:
例程4:子线程的创建与开启
线程对象 = threading.Thread(target=任务名)
import threadingdef run1(n):print("current task:run1")def run2(n):print("current task:run2")if __name__ == "__main__":t1 = threading.Thread(target=run1) # 创建子线程1t2 = threading.Thread(target=run2) # 创建子线程2t1.start() # 开启线程 t1t2.start() # 开启线程 t2
例程5:给线程执行的任务传递参数
线程可以以元组args 的方式或字典 kwargs 的方式给执行的任务传递参数。
线程对象 = threading.Thread(target=任务名, args=(arg1,…))
线程对象 = threading.Thread(target=任务名, kwargs={k1:num1,…})
import threadingdef run1(n):print("current task:", n)def run2(n):print("current task:", n)if __name__ == "__main__":t1 = threading.Thread(target=run1, args=("thread 1",)) # 创建子线程1t2 = threading.Thread(target=run2, args=("thread 2",)) # 创建子线程2t1.start() # 开启线程 t1t2.start() # 开启线程 t2print("end")
例程6:自定义线程类 MyThread
import threading
import timeclass MyThread(threading.Thread):def __init__(self, num):threading.Thread.__init__(self)self.num = numdef run(self):print("current task:", self.num)time.sleep(self.num)if __name__ == "__main__":t1 = MyThread(100) # 创建子线程1t2 = MyThread(200) # 创建子线程2t1.start() # 开启线程 t1t2.start() # 开启线程 t2print("end")
moveToThread 函数给多个任务(如显示多个界面)各分配一个线程去执行,避免自定义多个类继承自 QThread类,从而可以避免冗余。
使用moveToThread函数的流程:
该方法通过创建一个线程,将创建的线程与类方法进行绑定来实现,相当于在线程中操作类方法。
例程6:自定义线程类 MyThread
# coding:UTF-8
from PyQt5 import QtWidgets, QtCore
import sys
from PyQt5.QtCore import *
import time# 继承 QObject
class Runthread(QtCore.QObject):# 通过类成员对象定义信号对象signal = pyqtSignal(str)def __init__(self):super(Runthread, self).__init__()self.flag = Truedef __del__(self):print ">>> __del__"def run(self):i = 0while self.flag:time.sleep(1)if i <= 100:self.signal.emit(str(i)) # 注意这里与_signal = pyqtSignal(str)中的类型相同i += 1print ">>> run end: "class Example(QtWidgets.QWidget):# 通过类成员对象定义信号对象_startThread = pyqtSignal()def __init__(self):super(Example, self).__init__()# 按钮初始化self.button_start = QtWidgets.QPushButton('开始', self)self.button_stop = QtWidgets.QPushButton('停止', self)self.button_start.move(60, 80)self.button_stop.move(160, 80)self.button_start.clicked.connect(self.start) # 绑定多线程触发事件self.button_stop.clicked.connect(self.stop) # 绑定多线程触发事件# 进度条设置self.pbar = QtWidgets.QProgressBar(self)self.pbar.setGeometry(50, 50, 210, 25)self.pbar.setValue(0)# 窗口初始化self.setGeometry(300, 300, 300, 200)self.show()self.myT = Runthread() # 创建线程对象self.thread = QThread(self) # 初始化QThread子线程# 把自定义线程加入到QThread子线程中self.myT.moveToThread(self.thread)self._startThread.connect(self.myT.run) # 只能通过信号-槽启动线程处理函数self.myT.signal.connect(self.call_backlog)def start(self):if self.thread.isRunning(): # 如果该线程正在运行,则不再重新启动return# 先启动QThread子线程self.myT.flag = Trueself.thread.start()# 发送信号,启动线程处理函数# 不能直接调用,否则会导致线程处理函数和主线程是在同一个线程,同样操作不了主界面self._startThread.emit()def stop(self):if not self.thread.isRunning(): # 如果该线程已经结束,则不再重新关闭returnself.myT.flag = Falseself.stop_thread()def call_backlog(self, msg):self.pbar.setValue(int(msg)) # 将线程的参数传入进度条def stop_thread(self):print ">>> stop_thread... "if not self.thread.isRunning():returnself.thread.quit() # 退出self.thread.wait() # 回收资源print ">>> stop_thread end... "if __name__ == "__main__":app = QtWidgets.QApplication(sys.argv)myshow = Example()myshow.show()sys.exit(app.exec_())
def openVideo(self): # 导入视频文件,点击 btn_1 触发if self.btn_1.text() == "打开视频":# 打开视频文件self.videoPath, _ = QFileDialog.getOpenFileName(self, "Open Video", "../images/", "*.mp4 *.avi *.flv")print("Open Video: ", self.videoPath)# 实例化 cvDecode 类self.decodework = cvDecode()self.decodework.start()self.decodework.threadFlag = Trueself.decodework.changeFlag = Trueself.decodework.videoPath = r"" + self.videoPath# 实例化 playVedio 类self.playwork = playVedio()self.playwork.playLabel = self.label_1 # 设置显示控件 label_1self.playwork.threadFlag = Trueself.playwork.playFlag = True # 控制标识:播放# 创建视频播放线程self.playthread = QThread()self.playwork.moveToThread(self.playthread)self.playthread.started.connect(self.playwork.play) # 线程与类方法进行绑定self.playthread.start() # 启动视频播放线程# 视频/摄像头,准备播放self.btn_1.setText("关闭视频")self.btn_2.setEnabled(True) # "播放"按钮 可用self.btn_3.setEnabled(True) # "抓拍"按钮 可用else:self.closeEvent(self.close) # 关闭线程self.btn_1.setText("打开视频")self.btn_2.setText("播放视频")self.btn_2.setEnabled(False) # "播放"按钮 不可用self.btn_3.setEnabled(False) # "抓拍"按钮 不可用
class cvDecode(QThread): # 视频解码def __init__(self):super(cvDecode, self).__init__()self.threadFlag = False # 控制线程退出self.videoPath = "" # 视频文件路径Pself.changeFlag = 0 # 判断视频文件路径是否更改self.cap = cv.VideoCapture()def run(self):while self.threadFlag: # 线程开启状态if self.changeFlag == 1 and self.videoPath !="":self.changeFlag = 0self.cap = cv.VideoCapture(r""+self.videoPath)if self.videoPath !="":if self.cap.isOpened():ret, frame = self.cap.read()time.sleep(0.01) # 读取时间控制,读取视频文件取 0.01,读取实时摄像取 0.001if frame is None: # 控制循环播放self.cap = cv.VideoCapture(r"" + self.videoPath)if ret:Decode2Play.put(frame) # 解码后的数据放到队列中del frame # 释放资源else:# 控制重连self.cap = cv.VideoCapture(r"" + self.videoPath)time.sleep(0.01)
class playVedio(QObject): # 视频播放类def __init__(self):super(playVedio, self).__init__()self.playLabel = QLabel() # 初始化QLabel对象self.threadFlag = False # 控制线程退出self.playFlag = False # 控制播放/暂停标识def play(self):while self.threadFlag: # 线程开启状态if not Decode2Play.empty():self.frame = Decode2Play.get() # 从队列中读取视频帧if self.playFlag: # 当前状态播放image = cv.resize(self.frame, (400, 320)) # 调整为显示尺寸# qImg = self.cvToQImage(frame) # OpenCV 转为 PyQt 图像格式qImg = QImage(image, image.shape[1], image.shape[0], QImage.Format_RGB888).rgbSwapped()self.playLabel.setPixmap(QPixmap.fromImage(qImg)) # 图像在QLabel上展示time.sleep(0.001)
class MyMainWindow(QMainWindow, Ui_MainWindow): # 继承 QMainWindow 类和 Ui_MainWindow 界面类def __init__(self, parent=None):super(MyMainWindow, self).__init__(parent) # 初始化父类self.setupUi(self) # 继承 Ui_MainWindow 界面类# 菜单栏self.actionOpen.triggered.connect(self.openVideo) # 连接并执行 openSlot 子程序self.actionHelp.triggered.connect(self.trigger_actHelp) # 连接并执行 trigger_actHelp 子程序self.actionQuit.triggered.connect(self.close) # 连接并执行 trigger_actHelp 子程序## # 通过 connect 建立信号/槽连接,点击按钮事件发射 triggered 信号,执行相应的子程序 click_pushButtonself.btn_1.clicked.connect(self.openVideo) # 打开视频文件self.btn_2.clicked.connect(self.playPause) # 播放/暂停视频self.btn_3.clicked.connect(self.captureFrame) # 抓拍视频图像self.btn_4.clicked.connect(self.imageProcess) # 图像处理程序self.btn_5.clicked.connect(self.close) # 点击关闭按钮触发:关闭程序# self.timerCam.timeout.connect(self.refreshFrame) # 计时器结束时调用槽函数刷新当前帧# 初始化self.btn_2.setEnabled(False) # "播放"按钮 不可用self.btn_3.setEnabled(False) # "抓拍"按钮 不可用self.btn_4.setEnabled(False) # "处理"按钮 不可用
本例的 UI 继承自 uiDemo4.ui ,并进行修改如下:
完成了本项目的图形界面设计,将其保存为 uiDemo13.ui文件。
在 PyCharm中,使用 PyUIC 将选中的 uiDemo13.ui 文件转换为 .py 文件,就得到了 uiDemo13.py 文件。
(1)“打开视频”按钮用于从文件夹选择播放的视频文件。
导入视频前,“暂停播放”、”抓拍图像“、”处理图像“ 按钮都不可用。
(2)“播放”按钮用于播放打开的视频文件,播放结束后自动关闭。
导入视频后开始播放,“暂停播放”、”抓拍图像“按钮可用,”处理图像“不可用。
(3)“暂停/播放”按钮用于暂停/播放视频文件。按钮初始显示为“暂停播放”,按下“暂停”按钮后暂停播放,按钮显示切换为“播放视频”;再次按下“播放”按钮后继续播放,按钮显示切换为“暂停播放”。
(4)”抓拍图像“按钮用于抓拍图像,并显示在窗口右侧的显示控件 Label_2。左侧窗口的视频播放不受影响。
抓拍图像完成后,“图像处理”按钮可用。
(5)”图像处理“按钮用于处理所抓拍图像,并将处理后图像显示在窗口右侧的显示控件 Label_2。
为了简化例程,本例中的图像处理仅将抓拍图像转换为灰度图像进行显示。在实际应用中,也可以根据需要编写图像处理程序。
由于采用多线程处理机制,不论图像处理程序耗时如何,都不会影响左侧窗口中的视频播放。
# OpenCVPyqt13.py
# Demo05 of GUI by PyQt5
# Copyright 2023 Youcans, XUPT
# Crated:2023-03-02import sys, time
from PyQt5.QtWidgets import *
from PyQt5.QtCore import QObject, QThread, Qt
from PyQt5.QtGui import *
import cv2 as cv
from queue import Queue
from uiDemo13 import Ui_MainWindow # 导入 uiDemo10.py 中的 Ui_MainWindow 界面类
Decode2Play = Queue()class cvDecode(QThread): # 视频解码def __init__(self):super(cvDecode, self).__init__()self.threadFlag = False # 控制线程退出self.videoPath = "" # 视频文件路径Pself.changeFlag = 0 # 判断视频文件路径是否更改self.cap = cv.VideoCapture()def run(self):while self.threadFlag: # 线程开启状态if self.changeFlag == 1 and self.videoPath !="":self.changeFlag = 0self.cap = cv.VideoCapture(r""+self.videoPath)if self.videoPath !="":if self.cap.isOpened():ret, frame = self.cap.read()time.sleep(0.01) # 读取时间控制,读取视频文件取 0.01,读取实时摄像取 0.001if frame is None: # 控制循环播放self.cap = cv.VideoCapture(r"" + self.videoPath)if ret:Decode2Play.put(frame) # 解码后的数据放到队列中del frame # 释放资源else:# 控制重连self.cap = cv.VideoCapture(r"" + self.videoPath)time.sleep(0.01)class playVedio(QObject): # 视频播放类def __init__(self):super(playVedio, self).__init__()self.playLabel = QLabel() # 初始化QLabel对象self.threadFlag = False # 控制线程退出self.playFlag = False # 控制播放/暂停标识def play(self):while self.threadFlag: # 线程开启状态if not Decode2Play.empty():self.frame = Decode2Play.get() # 从队列中读取视频帧if self.playFlag: # 当前状态播放image = cv.resize(self.frame, (400, 320)) # 调整为显示尺寸# qImg = self.cvToQImage(frame) # OpenCV 转为 PyQt 图像格式qImg = QImage(image, image.shape[1], image.shape[0], QImage.Format_RGB888).rgbSwapped()self.playLabel.setPixmap(QPixmap.fromImage(qImg)) # 图像在QLabel上展示time.sleep(0.001)class MyMainWindow(QMainWindow, Ui_MainWindow): # 继承 QMainWindow 类和 Ui_MainWindow 界面类def __init__(self, parent=None):super(MyMainWindow, self).__init__(parent) # 初始化父类self.setupUi(self) # 继承 Ui_MainWindow 界面类# 菜单栏self.actionOpen.triggered.connect(self.openVideo) # 连接并执行 openSlot 子程序self.actionHelp.triggered.connect(self.trigger_actHelp) # 连接并执行 trigger_actHelp 子程序self.actionQuit.triggered.connect(self.close) # 连接并执行 trigger_actHelp 子程序## # 通过 connect 建立信号/槽连接,点击按钮事件发射 triggered 信号,执行相应的子程序 click_pushButtonself.btn_1.clicked.connect(self.openVideo) # 打开视频文件self.btn_2.clicked.connect(self.playPause) # 播放/暂停视频self.btn_3.clicked.connect(self.captureFrame) # 抓拍视频图像self.btn_4.clicked.connect(self.imageProcess) # 图像处理程序self.btn_5.clicked.connect(self.close) # 点击关闭按钮触发:关闭程序# self.timerCam.timeout.connect(self.refreshFrame) # 计时器结束时调用槽函数刷新当前帧# 初始化self.btn_2.setEnabled(False) # "播放"按钮 不可用self.btn_3.setEnabled(False) # "抓拍"按钮 不可用self.btn_4.setEnabled(False) # "处理"按钮 不可用def openVideo(self): # 导入视频文件,点击 btn_1 触发if self.btn_1.text() == "打开视频":# 打开视频文件self.videoPath, _ = QFileDialog.getOpenFileName(self, "Open Video", "../images/", "*.mp4 *.avi *.mov")print("Open Video: ", self.videoPath)# 实例化 cvDecode 类self.decodework = cvDecode()self.decodework.start()self.decodework.threadFlag = Trueself.decodework.changeFlag = Trueself.decodework.videoPath = r"" + self.videoPath# 实例化 playVedio 类self.playwork = playVedio()self.playwork.playLabel = self.label_1 # 设置显示控件 label_1self.playwork.threadFlag = Trueself.playwork.playFlag = True # 控制标识:播放# 创建视频播放线程self.playthread = QThread()self.playwork.moveToThread(self.playthread)self.playthread.started.connect(self.playwork.play) # 线程与类方法进行绑定self.playthread.start() # 启动视频播放线程# 视频/摄像头,准备播放self.btn_1.setText("关闭视频")self.btn_2.setEnabled(True) # "播放"按钮 可用self.btn_3.setEnabled(True) # "抓拍"按钮 可用else:self.closeEvent(self.close) # 关闭线程self.btn_1.setText("打开视频")self.btn_2.setText("播放视频")self.btn_2.setEnabled(False) # "播放"按钮 不可用self.btn_3.setEnabled(False) # "抓拍"按钮 不可用def playPause(self): # 暂停/播放控制,点击 btn_2 触发if self.btn_2.text() == "暂停播放":self.playwork.playFlag = False # 控制标识:暂停self.btn_2.setText("播放视频")else:self.btn_2.setText("暂停播放")self.playwork.playFlag = True # 控制标识:播放def captureFrame(self): # 抓拍视频图像,点击 btn_3 触发wLabel, hLabel = 400, 320self.image = cv.resize(self.playwork.frame, (wLabel, hLabel)) # 调整为显示尺寸qImg = QImage(self.image, wLabel, hLabel, QImage.Format_RGB888).rgbSwapped() # OpenCV 转为 PyQt 图像格式self.label_2.setPixmap((QPixmap.fromImage(qImg))) # 加载 PyQt 图像self.btn_4.setEnabled(True) # "处理"按钮 可用def imageProcess(self): # 抓拍视频图像,点击 btn_4 触发gray = cv.cvtColor(self.image, cv.COLOR_BGR2GRAY) # 转为灰度图像row, col, pix = gray.shape[0], gray.shape[1], gray.strides[0]qImg = QImage(gray.data, col, row, pix, QImage.Format_Indexed8)# qImg = QImage(self.image, row, col, QImage.Format_RGB888) # OpenCV 转为 PyQt 图像格式self.label_2.setPixmap((QPixmap.fromImage(qImg))) # 加载 PyQt 图像def closeEvent(self, event): # 关闭线程print("关闭线程") # 先退出循环才能关闭线程if self.decodework.isRunning(): # 关闭解码self.decodework.threadFlag = Falseself.decodework.quit()if self.playthread.isRunning(): # 关闭播放线程self.playwork.threadFlag = Falseself.playthread.quit()def refreshShow(self, img, label): # 刷新显示图像qImg = self.cvToQImage(img) # OpenCV 转为 PyQt 图像格式label.setPixmap((QPixmap.fromImage(qImg))) # 加载 PyQt 图像returndef trigger_actHelp(self): # 动作 actHelp 触发QMessageBox.about(self, "About","""多线程视频播放器 v1.0\nCopyright YouCans, XUPT 2023""")returnif __name__ == '__main__':app = QApplication(sys.argv) # 在 QApplication 方法中使用,创建应用程序对象myWin = MyMainWindow() # 实例化 MyMainWindow 类,创建主窗口myWin.show() # 在桌面显示控件 myWinsys.exit(app.exec_()) # 结束进程,退出程序
【本节完】
版权声明:
Copyright 2023 youcans, XUPT
Crated:2023-03-02
上一篇:day10 字符串总结
下一篇:vue3视频播放插件