当前位置: 首页 > news >正文

OpenCV-PyQT项目实战(12)项目案例08:多线程视频播放

欢迎关注『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:多线程视频播放

文章目录

  • OpenCV-PyQT项目实战(12)项目案例08:多线程视频播放
    • 1. 线程处理
      • 1.1 线程与进程
      • 1.2 为什么使用多线程
      • 1.3 单线程与多线程的基本例程
    • 2. Python自定义线程
      • 2.1 线程的创建步骤
      • 2.2 通过线程类 threading.Thread() 创建线程对象
      • 2.4 继承 threading.Thread 来自定义线程类
      • 2.5 使用 moveToThread 方法
    • 3. 多线程视频解码和视频播放
      • 3.1 自定义线程类
      • 3.2 视频解码子程序
      • 3.3 视频播放子程序
      • 3.4 主程序
      • 3.5 UI 界面 uiDemo13.ui
    • 4. 项目实战:PyQt多线程视频处理
      • 4.1 程序说明
      • 4.2 完整例程

OpenCV-PyQT项目实战(12)项目案例08:多线程视频播放

在之前的案例中,我们使用 QTime 定时器和 QThread 的方式来控制 QLabel 中的图像更新,实现视频播放或实时监控。但是,如果需要同时播放多路视频,或者在视频播放的同时进行图像处理,这种处理方法容易产生卡顿或阻塞,因此需要使用多线程解决这个问题。


1. 线程处理

1.1 线程与进程

进程(Process)是操作系统进行资源分配和调度运行的基本单位,可以理解为操作系统中正在执行的程序。每个应用程序都有一个自己的进程。

线程是一个基本的CPU执行单元,是程序执行的最小单元。 线程自己不拥有系统资源,必须依托于进程存活。一个线程是一个execution context,即一个CPU执行时所需要的一串指令。

每一个进程启动时都会最先产生一个线程,即主线程。然后主线程会再创建其他的子线程。

  • 线程与进程的区别:
    • 线程必须在某个进程中执行。
    • 一个进程可包含多个线程,其中有且只有一个主线程。
    • 多线程共享同个地址空间、打开的文件以及其他资源。
    • 多进程共享物理内存、磁盘、打印机以及其他资源。

1.2 为什么使用多线程

线程在程序中是独立的、并发的执行流。与分隔的进程相比,进程中线程之间的隔离程度要小,它们共享内存、文件句柄和其他进程应有的状态。

因为线程的划分尺度小于进程,使得多线程程序的并发性高。进程在执行过程之中拥有独立的内存单元,而多个线程共享内存,从而极大的提升了程序的运行效率。

线程比进程具有更高的性能,这是由于同一个进程中的线程都有共性,多个线程共享一个进程的虚拟空间。线程的共享环境包括进程代码段、进程的共有数据等,利用这些共享的数据,线程之间很容易实现通信。

操作系统在创建进程时,必须为改进程分配独立的内存空间,并分配大量的相关资源,但创建线程则简单得多。因此,使用多线程来实现并发比使用多进程的性能高得要多。

多线程(multithreading)是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。

线程从创建到消亡的过程为:

  1. new(新建):新创建的线程经过初始化后,进入Runnable状态。
  2. Runnable(就绪):等待线程调度。调度后进入运行状态。
  3. Running(运行)。
  4. Blocked(阻塞):暂停运行,解除阻塞后进入Runnable状态重新等待调度。
  5. Dead(消亡):线程方法执行完毕返回或者异常终止。

从运行状态(Running)进入阻塞状态(Blocked)有三种情况:

  • 同步:线程中获取同步锁,但是资源已经被其他线程锁定时,进入Locked状态,直到该资源可获取(获取的顺序由Lock队列控制)
  • 睡眠:线程运行sleep()或join()方法后,线程进入Sleeping状态。区别在于sleep等待固定的时间,而join是等待子线程执行完。当然join也可以指定一个“超时时间”。从语义上来说,如果两个线程a,b, 在a中调用b.join(),相当于合并(join)成一个线程。最常见的情况是在主线程中join所有的子线程。
  • 等待:线程中执行wait()方法后,线程进入Waiting状态,等待其他线程的通知(notify)。

1.3 单线程与多线程的基本例程

例程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

主线程执行完毕后,还要等待所有子线程执行完毕后才结束程序运行。



2. Python自定义线程

2.1 线程的创建步骤

(1) 导入线程模块import threading
(2) 通过线程类创建进程对象线程对象 = threading.Thread(target = 任务名) 
(3) 启动线程执行任务线程对象.start()

2.2 通过线程类 threading.Thread() 创建线程对象

语法:

thread.Thread(group=Nore,targt=None,args=(),kwargs={},*,daemon=None)

参数说明:

  • group:必须为None,于ThreadGroup类相关,一般不使用。
  • target:线程调用的对象,就是目标函数。
  • name:为线程起这个名字。默认是Tread-x,x是序号,由1开始,第一个创建的线程名字就是Tread-1。
  • args:为目标函数传递关键字参数,元组。
  • kwargs:为目标函数传递关键字参数,字典。
  • daemon:用来设置线程是否随主线程退出而退出。

例程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")

2.4 继承 threading.Thread 来自定义线程类

例程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")

2.5 使用 moveToThread 方法

moveToThread 函数给多个任务(如显示多个界面)各分配一个线程去执行,避免自定义多个类继承自 QThread类,从而可以避免冗余。

使用moveToThread函数的流程:

  1. 创建一个类继承自QObject类或其子类,在其中定义所要执行的多个任务。
  2. 任务通过moveToThread指定所要执行的线程。
  3. 线程通过start启动。
  4. 通过信号与槽机制触发线程的执行。

该方法通过创建一个线程,将创建的线程与类方法进行绑定来实现,相当于在线程中操作类方法。

例程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_())


3. 多线程视频解码和视频播放

3.1 自定义线程类

    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)  # "抓拍"按钮 不可用

3.2 视频解码子程序

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)

3.3 视频播放子程序

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)

3.4 主程序

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)  # "处理"按钮 不可用

3.5 UI 界面 uiDemo13.ui

本例的 UI 继承自 uiDemo4.ui ,并进行修改如下:

在这里插入图片描述

完成了本项目的图形界面设计,将其保存为 uiDemo13.ui文件。

在 PyCharm中,使用 PyUIC 将选中的 uiDemo13.ui 文件转换为 .py 文件,就得到了 uiDemo13.py 文件。



4. 项目实战:PyQt多线程视频处理

4.1 程序说明

(1)“打开视频”按钮用于从文件夹选择播放的视频文件。

在这里插入图片描述

导入视频前,“暂停播放”、”抓拍图像“、”处理图像“ 按钮都不可用。


(2)“播放”按钮用于播放打开的视频文件,播放结束后自动关闭。

导入视频后开始播放,“暂停播放”、”抓拍图像“按钮可用,”处理图像“不可用。

在这里插入图片描述


(3)“暂停/播放”按钮用于暂停/播放视频文件。按钮初始显示为“暂停播放”,按下“暂停”按钮后暂停播放,按钮显示切换为“播放视频”;再次按下“播放”按钮后继续播放,按钮显示切换为“暂停播放”。


(4)”抓拍图像“按钮用于抓拍图像,并显示在窗口右侧的显示控件 Label_2。左侧窗口的视频播放不受影响。

在这里插入图片描述

抓拍图像完成后,“图像处理”按钮可用。


(5)”图像处理“按钮用于处理所抓拍图像,并将处理后图像显示在窗口右侧的显示控件 Label_2。

在这里插入图片描述

为了简化例程,本例中的图像处理仅将抓拍图像转换为灰度图像进行显示。在实际应用中,也可以根据需要编写图像处理程序。

由于采用多线程处理机制,不论图像处理程序耗时如何,都不会影响左侧窗口中的视频播放。


4.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

相关文章:

OpenCV-PyQT项目实战(12)项目案例08:多线程视频播放

欢迎关注『OpenCV-PyQT项目实战 Youcans』系列&#xff0c;持续更新中 OpenCV-PyQT项目实战&#xff08;1&#xff09;安装与环境配置 OpenCV-PyQT项目实战&#xff08;2&#xff09;QtDesigner 和 PyUIC 快速入门 OpenCV-PyQT项目实战&#xff08;3&#xff09;信号与槽机制 …...

面向对象设计模式:结构型模式之装饰器模式

文章目录一、引入二、装饰器模式2.1 Intent 意图2.2 Applicability 适用性2.3 类图2.4 优缺点2.5 应用实例&#xff1a;Java IO 类2.6 应用实例&#xff1a;咖啡馆订购系统一、引入 咖啡馆订购系统 Initial 初始 4 种咖啡 House blend (混合咖啡)Dark Roast (深度烘培)Decaf (…...

Unity iOS 无服务器做一个排行榜 GameCenter

排行榜需求解决方案一(嗯目前只有一)UnityEngine.SocialPlatformsiOS GameCenterAppStoreConnect配置Unity 调用(如果使用GameCenter系统的面板&#xff0c;看到这里就可以了&#xff09;坑(需要获取数据做自定义面板的看这里)iOS代码Unity 代码吐槽需求 需求&#xff1a;接入…...

现在招个会自动化测试的人是真难呀~你会个锤子的自动化测试

现在招个会自动化测试的人是真难呀~ 前一段时间公司计划要招2个自动化测试到岗&#xff0c;同事面试了十几个来应聘的人&#xff0c;发现一个很奇怪的现象&#xff0c;在面试的时候&#xff0c;如果问的是框架API、脚本编写这些问题&#xff0c;基本上所有人都能对答如流&…...

OracleDatabase——数据库表空间dmp导出与导入

由于公司的程序一直部署在客户现场内网&#xff0c;内网调试难度高&#xff0c;一般是有备份还原数据库的需求&#xff0c;这里简记备份&#xff08;导出&#xff09;数据库dmp文件与恢复&#xff08;导入&#xff09;的步骤。 一、导出dmp文件 exp与expdp命令异同 相同点&a…...

20张图带你彻底了解ReentrantLock加锁解锁的原理

哈喽大家好&#xff0c;我是阿Q。 最近是上班忙项目&#xff0c;下班带娃&#xff0c;忙的不可开交&#xff0c;连摸鱼的时间都没有了。今天趁假期用图解的方式从源码角度给大家说一下ReentrantLock加锁解锁的全过程。系好安全带&#xff0c;发车了。 简单使用 在聊它的源码…...

Dockerfile构建Springboot镜像

Dockerfile构建Springboot镜像 文章目录 Dockerfile构建Springboot镜像 简介实例演示 前期准备 Docker环境Springboot项目Dockerfile文件 Windows 要求构建镜像启动测试 Linux 要求构建镜像启动测试 简介 容器技术大流行的时代&#xff0c;也是docker大流行的时代。 此文…...

从深分页查询到覆盖索引

最近看到一道面试题&#xff0c;如何优化深分页查询 最简单的例子是 select * from web_bill_main limit 30000,10;分页达到30000行&#xff0c;需要把前面29999行都过滤掉&#xff0c;才能找到这10条数据 所以整体时间花了80ms(工具显示时间) 我当时的第一反应是&#xff0…...

Go语言学习的第三天--下部分(Gin框架的基础了解)

每天都会分享Go的知识&#xff0c;喜欢的朋友关注一下。每天的学习分成两部分基础&#xff08;必要的&#xff0c;基础不牢地动山摇&#xff09;&#xff0c;另一部分是Go的一些框架知识&#xff08;会不定时发布&#xff0c;因为小Wei也是一名搬砖人&#xff09;。但是可以保证…...

JDK的动态代理(powernode 文档)(内含源代码)

JDK的动态代理&#xff08;powernode 文档&#xff09;&#xff08;内含源代码&#xff09; 源代码下载链接地址&#xff1a;https://download.csdn.net/download/weixin_46411355/87546086 一、动态代理 目录JDK的动态代理&#xff08;powernode 文档&#xff09;&#xff0…...

第1章 多线程基础

第1章 多线程基础 1.1.2 线程与进程的关系 进程可以看成是线程的容器&#xff0c;而线程又可以看成是进程中的执行路径。 1.2 多线程启动 线程有两种启动方式&#xff1a;实现Runnable接口&#xff1b;继承Thread类并重写run()方法。 执行进程中的任务时才会产生线程&a…...

Linux基本指令(一)

文章目录文件操作文档操作系统管理网络通信备份压缩Ctrl Alt T 打开终端 文件操作 1.复制文件 cp afile bfile &#xff08;将名为afile的文件复制到名为bfile的文件夹中&#xff0c;如果bfile文件不存在&#xff0c;系统将会创建此文件&#xff0c;如果bfile文件已经存在&a…...

el-dialog子组件在mounted周期内获取不到dom?

el-dialog子组件在mounted周期内获取不到dom&#xff1f;一、问题描述二、分析原因三、猜测正常父子组件在mounted生命周期内可以获得dom 父created—子created—子mounted—父mounted----子updated—父updated 一、问题描述 ** el-dialog控制显示隐藏是css控制的display&…...

第九章 opengl之光照(光照贴图)

OpenGL光照贴图漫反射贴图镜面光贴图光照贴图 一个物体的不同部分是不同的材质&#xff0c;那么会有不同的环境光和漫反射颜色表现。 漫反射贴图 原理就是&#xff1a;纹理。 是对同样的原理使用了不同的名字&#xff1a;其实都是使用一张覆盖物体的图像&#xff0c;让我们能…...

JDK动态代理(powernode CD2207 video)(内含教学视频+源代码)

JDK动态代理&#xff08;powernode CD2207 video&#xff09;&#xff08;内含教学视频源代码&#xff09; 教学视频原代码下载链接地址&#xff1a;https://download.csdn.net/download/weixin_46411355/87545977 目录JDK动态代理&#xff08;powernode CD2207 video&#xf…...

【Linux】Sudo的隐晦bug引发的一次业务问题排查

Sudo的隐晦bug引发的一次业务问题排查写在前面问题描述问题排查高负载现象排查日志排查跟踪任务调度过程Sudo引发的问题手动复现问题分析处理方案写在前面 记录一次生产环境sudo启动进程频繁被Kill且不报错的异常处理过程&#xff0c;如果遇到同样的问题只想要解决方案&#x…...

Java VisualVM 安装 Visual GC 插件图文教程

文章目录1. 通过运行打开 Java VisualVM 监控工具2. 菜单栏初始视图说明3. 工具插件菜单说明4. 手工安装插件5. 重启监控工具查看 Visual GC1. 通过运行打开 Java VisualVM 监控工具 首先确保已安装 Java 环境&#xff0c;如此处安装版本 JDK 1.8.0_161 C:\Users\niaonao>j…...

【C语言】详解静态变量static

关键字static 在C语言中&#xff1a;static是用来修饰变量和函数的static主要作用为:1. 修饰局部变量-静态局部变量 2. 修饰全局变量-静态全局变量3. 修饰函数-静态函数在讲解静态变量之前&#xff0c;我们应该了解静态变量和其他变量的区别: 修饰局部变量 //代码1 #include &l…...

SpringBoot整合ElasticSearch实现模糊查询,排序,分页,高亮

目录 前言 1.框架集成-SpringData-整体介绍 1.1Spring Data Elasticsearch 介绍 2.框架集成Spring Data Elasticsearch 2.1版本说明 2.2.idea创建一个springboot项目 2.3.导入依懒 2.3.增加配置文件 2.4Spring Boot 主程序。 2.5.数据实体类 2.6.配置类 2.7.DAO 数据…...

YARN基本架构

主要由ResourceManager、NodeManager、ApplicationMaster和Container等组件构成&#xff0c;如图所YA示。 ResourceManager&#xff08;RM&#xff09; RM是全局资源管理器&#xff0c;负责整个系统的资源管理和分配 主要由两个组件构成&#xff1a;Scheduler调度器和应用程序…...

【C++复习】类和对象全知识点总结

类和对象写在前面类和对象面向对象类类的定义类的访问限定符类的作用域类的实例化类对象大小this指针类的默认成员函数构造函数析构函数拷贝构造函数运算符重载赋值运算符重载前置后置重载取地址及const取地址操作符重载const 成员static 成员友元友元函数有元类内部类匿名对象…...

基于轻量级YOLOv5开发构建汉字检测识别分析系统

汉字检测、字母检测、手写数字检测、藏文检测、甲骨文检测在我之前的文章中都有做过了&#xff0c;今天主要是因为实际项目的需要&#xff0c;之前的汉字检测模型较为古老了还使用的yolov3时期的模型&#xff0c;检测精度和推理速度都有不小的滞后了&#xff0c;这里要基于yolo…...

leetcode-每日一题-66(简单题,数组)

这道题其实还没那么简单&#xff0c;中间还是有的绕的。。。。给定一个由 整数 组成的 非空 数组所表示的非负整数&#xff0c;在该数的基础上加一。最高位数字存放在数组的首位&#xff0c; 数组中每个元素只存储单个数字。你可以假设除了整数 0 之外&#xff0c;这个整数不会…...

LeetCode295之数据流的中位数(相关话题:优先队列)

题目描述 中位数是有序整数列表中的中间值。如果列表的大小是偶数&#xff0c;则没有中间值&#xff0c;中位数是两个中间值的平均值。 例如 arr [2,3,4] 的中位数是 3 。例如 arr [2,3] 的中位数是 (2 3) / 2 2.5 。 实现 MedianFinder 类: MedianFinder() 初始化 Media…...

助你加速开发效率!告别IDEA卡顿困扰的性能优化技巧

在现代软件开发中&#xff0c;IDE&#xff08;集成开发环境&#xff09;是一个必不可少的工具。IntelliJ IDEA是一个广受欢迎的IDE&#xff0c;但有时候IDE的性能可能会受到影响&#xff0c;导致开发人员的工作效率降低。本文将介绍一些可以提高IDE性能的技巧&#xff0c;帮助开…...

Java设计模式-适配器模式

1、简介 适配器模式是作为两个不兼容的接口之间的桥梁。这种类型的设计模式属于结构型模式&#xff0c;它结合了两个独立接口的功能。 这种模式涉及到一个单一的类&#xff0c;该类负责加入独立的或不兼容的接口功能。 2、适配器模式分类 目标接口&#xff08;Target&#x…...

Linux 练习六 (IPC 管道)

文章目录1 标准管道流2 无名管道&#xff08;PIPE&#xff09;3 命名管道&#xff08;FIFO&#xff09;3.1 创建删除管道文件3.2 打开和关闭FIFO文件3.3 管道案例&#xff1a;基于管道的客服端服务器程序使用环境&#xff1a;Ubuntu18.04 使用工具&#xff1a;VMWare workstati…...

合并两个有序链表(精美图示详解哦)

全文目录引言合并两个有序链表题目描述方法一&#xff1a;将第二个链表合并到第一个思路实现方法二&#xff1a;尾插到哨兵位的头节点思路实现总结引言 在前面两篇文章中&#xff0c;我们介绍了几道链表的习题&#xff1a;反转链表、链表的中间结点、链表的倒数第k个结点&…...

33 JSON操作

目录 一、介绍 二、JSON的特点 三、JSON语法 1、json中的数据类型 四、JSON文件的定义 五、读取JSON文件 1、读取json文件的两种方式 &#xff08;1&#xff09;read、write &#xff08;2&#xff09;json.load 2、使用json.load读取json文件的步骤 3、练习读取json文件 六、练…...

三八妇女节快乐----IT女神活动随笔

献丑了&#xff0c;一首小小散文诗&#xff0c;请大家轻喷 O(≧口≦)O 我的答案 天下芸芸众生&#xff0c;好似夜幕漫天繁星。 与你相识&#xff0c;只是偶然。 简单的一个招呼&#xff0c;于是开始了一段故事。 我们或是诉说&#xff0c;或是分享&#xff1b; 我们彼此倾听&…...

帮忙做ppt的网站/广告营销推广

1、在 Python2.x 中 raw_input( ) 和 input( )&#xff0c;两个函数都存在&#xff0c;其中区别为: raw_input( ) 将所有输入作为字符串看待&#xff0c;返回字符串类型。input( ) 只能接收"数字"的输入&#xff0c;在对待纯数字输入时具有自己的特性&#xff0c;它…...

在哪个网站买做性的人/广告策划

情景&#xff1a;uni-app使用vue框架开发混合APP&#xff0c;虽然APP或者小程序没有跨域&#xff0c;但希望就是写完这个既有H5&#xff0c;又有APP&#xff0c;小程序等&#xff0c;所以能通过后端解决跨域最好。但是不知道是vue的原因还是什么&#xff0c;在PHP接口基类中添加…...

网站优化推广多少钱/网站建设与网站设计

用JAVA实现一个类层次&#xff1a;Shape、Rectangle、Triangle&#xff0c;其中&#xff1a;Shape类中定义了一个抽象方法area().mip版 关注:281 答案:3 悬赏:50解决时间 2021-01-26 21:52已解决2021-01-26 01:40用JAVA实现一个类层次&#xff1a;Shape、Rectangle、Triangl…...

网站文字编辑怎么做/专业做网站官网

摘要&#xff1a;本节介绍各种SQL语句。本节介绍有关数据库级的SQL以及相关操作&#xff0c;查看、建立和删除等操作。从本节开始正式介绍各种SQL语句。本节介绍有关数据库级的SQL以及相关操作&#xff0c;查看、建立和删除等操作。用SHOW显示已有的数据库句法&#xff1a;SHOW…...

大连网站流量优化定制/公司网站设计要多少钱

本次使用到的模块大概有&#xff1a;超声波测距、EEPROM读和写、时钟DS1302、矩阵键盘、数码管显示 这一届国赛感觉不是特别难&#xff0c;但是还是有几点需要注意&#xff1a; 1.不要把超声波程序写在中断里面。如果把超声波程序写在中断里面的话&#xff0c;可能就不会及时的…...

广告公司营销策划方案/常用的seo网站优化排名

oracle 笔记一专有连接和共享连接(Dedicated Server and Shared Server)oracle 响应客户端请求有两种方式&#xff1a;1 专有连接&#xff1a;用一个服务器进程响应一个客户端请求2 共享连接&#xff1a;用一个分派器(dispatcher)响应一个客户端请求&#xff0c;服务器端有多个…...