添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

使用pyqt5开发一个带有UI界面的图像数据处理程序

问题描述:

根据业务和UI界面分离的原则,为了使大量数据处理过程不影响UI主进程,于是使用了多线程的方式,利用pyqt5中的QThread类来创建数据处理的子线程,然后将数据传回UI主线程进行展示。然而在子线程处理数据过程中,由于文件读取等操作可能会产生异常,进而引起主UI线程的崩溃。为了提高程序的鲁棒性和健壮性,使用 try...except 处理异常,然而 子线程中产生的异常无法在主线程中捕获到

# 数据处理的线程
class WorkThread(QThread):
    finish = pyqtSignal(np.ndarray) # 定义一个信号,类型是numpy数组类型的图片
    def __init__(self,path):
        super(WorkThread, self).__init__()
        self.path = path
    def __del__(self):
        self.wait()
    def run(self):
        image = cv2.imread(self.path)
        time.sleep(5) # 模拟处理数据过程
        self.finish.emit(image) # 处理好的图片数据发送出去
# 主UI线程
class MainWidget(QWidget):
    def __init__(self):
        super(MainWidget, self).__init__()
        self.label = QLabel(self)
        self.bt = QPushButton("处理",self)
        self.bt.clicked.connect(self.pocessImage)
        vBox = QVBoxLayout()
        vBox.addWidget(self.bt)
        vBox.addWidget(self.label)
        self.setLayout(vBox)
    def pocessImage(self):
        self.bt.setEnabled(False)
        path = "test.jpg"
        workThread = WorkThread(path)
        workThread.start() #启动线程
        workThread.finish.connect(self.showImage)
    def showImage(self,image):
        self.bt.setEnabled(True)
        # QImage通过numpy类型转化QImage类型时,注意后面两个参数写全
        # openCV读取的通道顺序为BGR
        self.label.setPixmap(QPixmap(QImage(image,image.shape[1], image.shape[0],image.shape[1]*3, QImage.Format_BGR888)))
if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MainWidget()
    window.show()
    sys.exit(app.exec())

原因分析:

使用start()方法启动子线程时,解释器会为子线程开辟独立的栈空间,于是主线程就无法获取子线程栈的信息。当子线程异常中止时,会在子线程中捕获处理而不会将此异常抛出给主线程

解决方案:

在子线程中定义一个异常标志,如果线程异常退出,将该标志位设置为1,正常退出为0

class WorkThread(QThread):
    finish = pyqtSignal(np.ndarray) # 定义一个信号,类型是numpy数组类型的图片
    def __init__(self,path):
        super(WorkThread, self).__init__()
        self.path = path
        self.exitcode = 0  # 如果线程异常退出,将该标志位设置为1,正常退出为0
        self.exception = None
    def run(self):
        try:
            image = cv2.imread(self.path)
            time.sleep(2) # 模拟处理数据过程
            self.finish.emit(image) # 处理好的图片数据发送出去
        except Exception as e:
            self.exitcode = 1
            self.exception = e
class MainWidget(QWidget):
    def __init__(self):
        super(MainWidget, self).__init__()
        self.label = QLabel(self)
        self.bt = QPushButton("处理",self)
        self.bt.clicked.connect(self.pocessImage)
        vBox = QVBoxLayout()
        vBox.addWidget(self.bt)
        vBox.addWidget(self.label)
        self.setLayout(vBox)
    def pocessImage(self):
        try:
            self.bt.setEnabled(False)
            path = "test.jp" # 错误路径,人为产生一个异常
            self.workThread = WorkThread(path)
            self.workThread.start() #启动线程
            self.workThread.finish.connect(self.showImage)
            if self.workThread.wait() and self.workThread.exitcode == 1: #self.workThread.wait()不可缺少,需要等主线程完成后再进行子线程活动
                raise self.workThread.exception
        except Exception as e:
            self.bt.setEnabled(True)
            QMessageBox.information(self, "提示:", '操作失败!')
    def showImage(self,image):
        self.bt.setEnabled(True)
        # QImage通过numpy类型转化QImage类型时,注意后面两个参数写全
        # openCV读取的通道顺序为BGR
        self.label.setPixmap(QPixmap(QImage(image,image.shape[1], image.shape[0],image.shape[1]*3, QImage.Format_BGR888)))

问题基本上解决! 但是关于Pyqt多线程相关的知识还是没有完全掌握和理解,后面继续学习!

在项目中有一个小功能,是用进度条来显示后台任务完成的进度。遇到的问题是,如果后台任务没有发生异常,进度条可以很好的显示进度,但是发生异常后,进度条窗口会进入“假死”状态,使用上面的方法无法解决这种问题,该进度条只能手动关闭才能显示后面的异常信息,否则进度条会一直显示。

新的解决思路

在子线程中创建一个异常信号,然后在子线程中捕获异常,如果子线程任务出现了异常就emit这个异常信号,在主UI线程中connect这个异常信号,如果接收成功了就说明子线程有异常,就可以对其进行相应的操作(直接关闭进度条)。

部分代码:

# 进度条类
class ProcessDialog(QDialog):
    def __init__(self):
        super(ProcessDialog,self).__init__()
        self.resize(300,100)
        vBox = QVBoxLayout()
        label = QLabel("处理中...",self)
        self.pbar = QProgressBar()
        self.pbar.setMaximum(100)
        self.timer = QBasicTimer()
        self.step = 0
        vBox.addWidget(label)
        vBox.addWidget(self.pbar)
        self.setLayout(vBox)
        self.setWindowTitle('提示')
        self.timer.start(10, self)
        # 新建的窗口始终位于当前屏幕的最前面
        self.setWindowFlags(Qt.WindowStaysOnTopHint)
        # 阻塞父类窗口不能点击
        self.setWindowModality(Qt.ApplicationModal)
        self.setWindowFlags(Qt.WindowCloseButtonHint)
    def timerEvent(self, e): # 监测,如果step大于等于100则关闭进度条
        if self.step >= 100:
            self.quit()
            return
        # if self.step == 0:
        #     QTimer.singleShot(10000, self.quit)
    def quit(self):
        QApplication.processEvents() #会使窗口变换流畅
        self.step = 0
        self.pbar.setValue(self.step)
        self.timer.stop()
        self.close()
# 子线程类
class Mythread(Qthread):
	finish = pyqtSignal(np.ndarray,int)  #处理完后待发送的信息信号,int用来控制进度条的进度
    excepted = pyqtSignal(str) #发送一个异常信号
    def __init__(self):
       super(Mythread, self).__init__()
    def run(self):
	    try:
	    	# 进行耗时操作
	    	for i in range(100):
	    		self.finish.emit(image,i+1) # 处理完成后发送
	    except Exception as e:
	    	self.excepted.emit(str(e)) # 将异常发送给UI进程
# 窗口UI类      
class MainWindow(QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()
        # 部分代码省略
        self.myThread = MyThread()
        self.myThread.finish.connect(self.fun)
        self.myThread.excepted.connect(self.threadException)
        self.processDiaog = ProcessDialog()
        if not self.processDiaog.exec_(): #显示进度条,如果手动关闭则终止子线程
           self.myThread.terminate()
    def fun(image,step): #正常处理函数
    	self.processDiaog.step = step
        self.processDiaog.pbar.setValue(step)
        # 对image数据进行展示
    def threadException(self,message):
    	self.processDiaog.quit()
    	print(message) #打印子线程错误日志
前面在pyqt5多线程QThread)遇到的坑(一)中提到了先实例化类,再把实例对象传参到线程类,这样的确实可行,但是还是遇到了新坑。
pyqt5多线程QThread)遇到的坑(一
被实例化的类是做数据处理的,传入和导出的的数据比较大,最少都是几万行的excel表格数据(pandas.DataFrame),而且传入的数据最少两个pandas.DataFrame表,多的时候会传入7个,而且有一些数据是公共数据,每次处理都必须处理的,直接放在数据处理类的初始化__init__中了,而这部分数据处理也是稍微有的耗时,那么坑就来了,先实例化数据处理类,那么这部分稍微耗时的数据处理也会导
                                    一、多进程 的 共享内存——Value 和 Array一般的变量在进程之间是没法进行通讯的,multiprocessing提供了Value和Array模块,他们可以在不通的进程中共同使用。主进程的内存空间中创建共享的内存,也就是Value和Array两个对象。对象Value被设置成为双精度数(d), 并初始化为0.0。而Array则类似于C语言中的数组,有固定的类型(i, 也就是整数)。fro...
                                    总结来说,QThread线程的退出原理是通过调用线程的 quit 或 exit 方法请求线程退出,然后等待线程run 方法执行完毕后发出 finished 信号,可以在该信号的槽函数中执行一些清理操作。quit 方法会请求线程退出,但不会立即终止线程的执行,而是等待线程执行完 run 方法后再退出。线程的退出信号:QThread 提供了一个 finished 信号,当线程run 方法执行完毕时,会自动发出这个信号。run 方法:QThread 中的线程逻辑主要在 run 方法中实现。
                                    问题:点击<开始>按钮后加载模型计算,pyqt界面卡住等待计算结束。目的是添加进度条,优化用户等待。一开始以为添加进度条加上时钟就行,调试后发现把计算部分写在主界面类里,计算和进度条无法一起进行。
解决:需要把计算部分分离出去另起一个线程线程类的结构
以我程序所写的线程类为参考如下
from PyQt5.QtCore import *
class MyCal(QThread):
                                    目录一、界面卡死与多线程相关1、PyQt5中使用线程QThread和Thread进行耗时操作的问题2、pyqt5多线程QThread)遇到的坑二、部件性能与展示相关1、QTextEdit、QTextBrowser添加的文字超出视图后,滑动条自动移至最底部显示最新文本的解决办法三、常用功能实现的代码技巧1、python定时程序(每隔一段时间执行指定函数)四、pycharm常见报错1、Pycharm报:Method ‘XXX’ may be ‘static‘ 的解决办法2、Pycharm报:This di
所以认真的研究了一下这个问题,下面给出一个具体实验环境(qt5),即可说明问题.
简单期间,直接上代码, 分三部分,header文件,cpp文件,pro文件, 重点在主窗口的析构,说明如何停止线程的运行.
header 文件
 cat *.h
#ifndef _CLOCK_THREAD_H
#define _CLOCK_THREAD_H
#incl
                                    如下是一个爬虫的部分代码,现在碰到的问题是,多个线程(比如10个)中的某个在爬取、解析某个页面或者存储数据的过程中发生了异常,线程的工作代码虽然捕捉了这一异常,但无法继续工作。我希望最好有办法能不让线程退出,而继续分配给他的工作;退而求其次,可以开启一个新的线程继续他的工作,但这需要发生异常的线程将退出节点的信息返回给主线程;最次的,异常退出的线程应该将退出节点的信息返回并让主线程记录,方便我后...