Bootstrap

【Python笔记】pyqt5进度条-多线程图像分块处理防止窗体卡顿

目录

主要功能

环境配置

实现过程

1、设计ui

主界面

弹出框

窗体文件

2、主体实现

打开文件

计算函数

代码附录

title.ui

titleok.ui

title.py

titleok.py

main多线程运算分块.py

主要功能

1、打开文件夹,读取tif文件列表,填入数据列表框

2、多线程对所有tif文件分块处理,防止阻塞窗体

3、更新进度条

环境配置

主要版本如下:

python 3.9.7

PyQt5 5.15.6

GDAL 3.4.1

numpy 1.22.3

pandas 1.4.2

较为详细的环境配置:【Python笔记】pyqt5窗体程序制作流程

实现过程

 以下主要叙述流程和关键技术点,设计部分具体过程参照:【Python笔记】pyqt5窗体程序制作流程

1、设计ui

主界面

创建一个QMainWindow,添加组件 标签QLabel、文本框LineList、数据列表框QListWeight、菜单QMenu、按钮QPushButton等,保存为

弹出框

创建一个Dialog,添加组件 进度条QProgressBar

窗体文件

分别保存title.ui、titleok.ui文件,使用拓展工具转化为对应的py文件 ,完整ui、py文件代码见代码附录

2、主体实现

打开文件

设置全局变量-字典self.maindata={},打开文件夹,将文件夹路径赋给全局字典,同时获取文件夹内的所有后缀为.tif文件列表,加入数据列表框,给数据列表框添加点击事件itemClick

    def wenjianjia(self):
        if not os.path.exists(r"E:\pyimg\tif2csv"):
            self.maindata["tifDir"] = QFileDialog.getExistingDirectory(None, "选取文件夹", "C:/")  # 返回选中的文件夹路径
        else:
            self.maindata["tifDir"] = QFileDialog.getExistingDirectory(None, "选取文件夹", "E:\\pyimg\\tif2csv")  # 返回选中的文件夹路径
        # self.maindata["tifDir"]=QFileDialog.getExistingDirectory(None,"选取文件夹","E:/")  # 返回选中的文件夹路径
        print(self.maindata)
        self.maindata["tifs"] = [i for i in os.listdir(self.maindata["tifDir"]) if i.endswith(".tif")]
        # 数据列表框添加内容
        for img in self.maindata["tifs"]:
            self.listWidget.addItem(str(img))

#数据列表框添加点击事件
    def itemClick(self, item):
        print(item.text() + " clicked!")
        QMessageBox.information(self, "ListWidget", "你选择了:" + item.text())

计算函数

多线程弹出子界面,同时开启子线程传入相关参数进行分块处理,此处略过分块代码,子线程定义信号_signal传回数据更新进度条,进度条到达100时显示完成按钮,完整代码“main多线程运算分块.py”见代码附录

#计算函数
    def start_cacu(self):
        #子窗体
        self.child = childWindow()
        self.child.pushButton.setVisible(False)
        self.child.show()

        self.maindata["分块大小"]=self.lineEdit.text()
        # 创建线程
        self.thread = Runthread(self.maindata)
        # 连接信号
        self.thread._signal.connect(self.call_backlog)  # 进程连接回传到GUI的事件
        self.child.stop_thread.connect(self.thread.terminate)
        # 开始线程
        self.thread.start()

    # 将线程的参数传入进度条事件
    def call_backlog(self, msg):
        self.child.progressBar.setValue(int(msg))
        if(msg==100):
            self.child.pushButton.setVisible(True)
# 多线程
class Runthread(QtCore.QThread):
    #  通过类成员对象定义信号对象
    _signal = pyqtSignal(int)

    def __init__(self,main):
        super(Runthread, self).__init__()
        self.main=main
#计算函数
    def run(self):
        # 分块处理代码
        print(self.main)

代码附录

title.ui

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>tile</class>
 <widget class="QMainWindow" name="tile">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>472</width>
    <height>279</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>分块</string>
  </property>
  <widget class="QWidget" name="centralwidget">
   <widget class="QListWidget" name="listWidget">
    <property name="geometry">
     <rect>
      <x>10</x>
      <y>20</y>
      <width>311</width>
      <height>211</height>
     </rect>
    </property>
    <property name="layoutDirection">
     <enum>Qt::LeftToRight</enum>
    </property>
   </widget>
   <widget class="QLabel" name="label">
    <property name="geometry">
     <rect>
      <x>330</x>
      <y>130</y>
      <width>54</width>
      <height>20</height>
     </rect>
    </property>
    <property name="text">
     <string>分块大小:</string>
    </property>
   </widget>
   <widget class="QPushButton" name="pushButton">
    <property name="geometry">
     <rect>
      <x>390</x>
      <y>210</y>
      <width>71</width>
      <height>23</height>
     </rect>
    </property>
    <property name="text">
     <string>分块</string>
    </property>
   </widget>
   <widget class="QLineEdit" name="lineEdit">
    <property name="geometry">
     <rect>
      <x>390</x>
      <y>130</y>
      <width>71</width>
      <height>20</height>
     </rect>
    </property>
    <property name="text">
     <string>256</string>
    </property>
    <property name="dragEnabled">
     <bool>false</bool>
    </property>
    <property name="placeholderText">
     <string>256</string>
    </property>
   </widget>
   <widget class="QLabel" name="label_2">
    <property name="geometry">
     <rect>
      <x>330</x>
      <y>160</y>
      <width>54</width>
      <height>12</height>
     </rect>
    </property>
    <property name="text">
     <string>输出路径:</string>
    </property>
   </widget>
   <widget class="QLabel" name="label_3">
    <property name="geometry">
     <rect>
      <x>10</x>
      <y>0</y>
      <width>54</width>
      <height>16</height>
     </rect>
    </property>
    <property name="text">
     <string>影像列表:</string>
    </property>
   </widget>
   <widget class="QLineEdit" name="lineEdit_2">
    <property name="geometry">
     <rect>
      <x>330</x>
      <y>180</y>
      <width>131</width>
      <height>20</height>
     </rect>
    </property>
    <property name="font">
     <font>
      <family>Adobe Devanagari</family>
      <pointsize>9</pointsize>
     </font>
    </property>
   </widget>
  </widget>
  <widget class="QMenuBar" name="menubar">
   <property name="geometry">
    <rect>
     <x>0</x>
     <y>0</y>
     <width>472</width>
     <height>23</height>
    </rect>
   </property>
   <widget class="QMenu" name="menu">
    <property name="title">
     <string>文件</string>
    </property>
    <addaction name="actiondakai"/>
    <addaction name="actionda"/>
   </widget>
   <addaction name="menu"/>
  </widget>
  <widget class="QStatusBar" name="statusbar"/>
  <action name="actionda">
   <property name="text">
    <string>打开文件夹</string>
   </property>
  </action>
  <action name="actiondakai">
   <property name="checkable">
    <bool>false</bool>
   </property>
   <property name="text">
    <string>打开文件</string>
   </property>
   <property name="font">
    <font>
     <family>Adobe Devanagari</family>
     <pointsize>10</pointsize>
    </font>
   </property>
   <property name="menuRole">
    <enum>QAction::TextHeuristicRole</enum>
   </property>
   <property name="priority">
    <enum>QAction::NormalPriority</enum>
   </property>
  </action>
 </widget>
 <resources/>
 <connections/>
</ui>

titleok.ui

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>Dialog</class>
 <widget class="QDialog" name="Dialog">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>423</width>
    <height>150</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>分块</string>
  </property>
  <widget class="QProgressBar" name="progressBar">
   <property name="geometry">
    <rect>
     <x>120</x>
     <y>30</y>
     <width>261</width>
     <height>23</height>
    </rect>
   </property>
   <property name="value">
    <number>0</number>
   </property>
  </widget>
  <widget class="QLabel" name="label">
   <property name="geometry">
    <rect>
     <x>30</x>
     <y>30</y>
     <width>54</width>
     <height>12</height>
    </rect>
   </property>
   <property name="text">
    <string>正在处理:</string>
   </property>
  </widget>
  <widget class="QPushButton" name="pushButton">
   <property name="geometry">
    <rect>
     <x>250</x>
     <y>110</y>
     <width>75</width>
     <height>23</height>
    </rect>
   </property>
   <property name="text">
    <string>完成</string>
   </property>
  </widget>
  <widget class="QPushButton" name="pushButton_2">
   <property name="geometry">
    <rect>
     <x>330</x>
     <y>110</y>
     <width>75</width>
     <height>23</height>
    </rect>
   </property>
   <property name="text">
    <string>取消</string>
   </property>
  </widget>
 </widget>
 <resources/>
 <connections/>
</ui>

title.py

# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'title.ui'
#
# Created by: PyQt5 UI code generator 5.15.4
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again.  Do not edit this file unless you know what you are doing.


from PyQt5 import QtCore, QtGui, QtWidgets


class Ui_tile(object):
    def setupUi(self, tile):
        tile.setObjectName("tile")
        tile.resize(472, 279)
        self.centralwidget = QtWidgets.QWidget(tile)
        self.centralwidget.setObjectName("centralwidget")
        self.listWidget = QtWidgets.QListWidget(self.centralwidget)
        self.listWidget.setGeometry(QtCore.QRect(10, 20, 311, 211))
        self.listWidget.setLayoutDirection(QtCore.Qt.LeftToRight)
        self.listWidget.setObjectName("listWidget")
        self.label = QtWidgets.QLabel(self.centralwidget)
        self.label.setGeometry(QtCore.QRect(330, 130, 54, 20))
        self.label.setObjectName("label")
        self.pushButton = QtWidgets.QPushButton(self.centralwidget)
        self.pushButton.setGeometry(QtCore.QRect(390, 210, 71, 23))
        self.pushButton.setObjectName("pushButton")
        self.lineEdit = QtWidgets.QLineEdit(self.centralwidget)
        self.lineEdit.setGeometry(QtCore.QRect(390, 130, 71, 20))
        self.lineEdit.setDragEnabled(False)
        self.lineEdit.setObjectName("lineEdit")
        self.label_2 = QtWidgets.QLabel(self.centralwidget)
        self.label_2.setGeometry(QtCore.QRect(330, 160, 54, 12))
        self.label_2.setObjectName("label_2")
        self.label_3 = QtWidgets.QLabel(self.centralwidget)
        self.label_3.setGeometry(QtCore.QRect(10, 0, 54, 16))
        self.label_3.setObjectName("label_3")
        self.lineEdit_2 = QtWidgets.QLineEdit(self.centralwidget)
        self.lineEdit_2.setGeometry(QtCore.QRect(330, 180, 131, 20))
        font = QtGui.QFont()
        font.setFamily("Adobe Devanagari")
        font.setPointSize(9)
        self.lineEdit_2.setFont(font)
        self.lineEdit_2.setObjectName("lineEdit_2")
        tile.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(tile)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 472, 23))
        self.menubar.setObjectName("menubar")
        self.menu = QtWidgets.QMenu(self.menubar)
        self.menu.setObjectName("menu")
        tile.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(tile)
        self.statusbar.setObjectName("statusbar")
        tile.setStatusBar(self.statusbar)
        self.actionda = QtWidgets.QAction(tile)
        self.actionda.setObjectName("actionda")
        self.actiondakai = QtWidgets.QAction(tile)
        self.actiondakai.setCheckable(False)
        font = QtGui.QFont()
        font.setFamily("Adobe Devanagari")
        font.setPointSize(10)
        self.actiondakai.setFont(font)
        self.actiondakai.setMenuRole(QtWidgets.QAction.TextHeuristicRole)
        self.actiondakai.setPriority(QtWidgets.QAction.NormalPriority)
        self.actiondakai.setObjectName("actiondakai")
        self.menu.addAction(self.actiondakai)
        self.menu.addAction(self.actionda)
        self.menubar.addAction(self.menu.menuAction())

        self.retranslateUi(tile)
        QtCore.QMetaObject.connectSlotsByName(tile)

    def retranslateUi(self, tile):
        _translate = QtCore.QCoreApplication.translate
        tile.setWindowTitle(_translate("tile", "分块"))
        self.label.setText(_translate("tile", "分块大小:"))
        self.pushButton.setText(_translate("tile", "分块"))
        self.lineEdit.setText(_translate("tile", "256"))
        self.lineEdit.setPlaceholderText(_translate("tile", "256"))
        self.label_2.setText(_translate("tile", "输出路径:"))
        self.label_3.setText(_translate("tile", "影像列表:"))
        self.menu.setTitle(_translate("tile", "文件"))
        self.actionda.setText(_translate("tile", "打开文件夹"))
        self.actiondakai.setText(_translate("tile", "打开文件"))

titleok.py

# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'titleok.ui'
#
# Created by: PyQt5 UI code generator 5.15.4
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again.  Do not edit this file unless you know what you are doing.


from PyQt5 import QtCore, QtGui, QtWidgets


class Ui_Dialog(object):
    def setupUi(self, Dialog):
        Dialog.setObjectName("Dialog")
        Dialog.resize(423, 150)
        self.progressBar = QtWidgets.QProgressBar(Dialog)
        self.progressBar.setGeometry(QtCore.QRect(120, 30, 261, 23))
        self.progressBar.setProperty("value", 0)
        self.progressBar.setObjectName("progressBar")
        self.label = QtWidgets.QLabel(Dialog)
        self.label.setGeometry(QtCore.QRect(30, 30, 54, 12))
        self.label.setObjectName("label")
        self.pushButton = QtWidgets.QPushButton(Dialog)
        self.pushButton.setGeometry(QtCore.QRect(250, 110, 75, 23))
        self.pushButton.setObjectName("pushButton")
        self.pushButton_2 = QtWidgets.QPushButton(Dialog)
        self.pushButton_2.setGeometry(QtCore.QRect(330, 110, 75, 23))
        self.pushButton_2.setObjectName("pushButton_2")

        self.retranslateUi(Dialog)
        QtCore.QMetaObject.connectSlotsByName(Dialog)

    def retranslateUi(self, Dialog):
        _translate = QtCore.QCoreApplication.translate
        Dialog.setWindowTitle(_translate("Dialog", "分块"))
        self.label.setText(_translate("Dialog", "正在处理:"))
        self.pushButton.setText(_translate("Dialog", "完成"))
        self.pushButton_2.setText(_translate("Dialog", "取消"))

main多线程运算分块.py

import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import QIcon
from title import *
from titleok import *
from osgeo import gdal
import os


# 多线程
class Runthread(QtCore.QThread):
    #  通过类成员对象定义信号对象
    _signal = pyqtSignal(int)

    def __init__(self,main):
        super(Runthread, self).__init__()
        self.main=main
#计算函数
    def run(self):
        # 分块影像所在文件夹,不能有中文
        print(self.main)
        tifDir = self.main["tifDir"]
        # 输出的文件夹,不能有中文,如果文件夹不存在则会被创建
        outPath =self.main["outPath"]
        if not os.path.exists(outPath):
            os.makedirs(outPath)

        tifs = [i for i in os.listdir(tifDir) if i.endswith(".tif")]

        print("有 %s 个tif文件" % len(tifs))
        print("tifs", tifs)
        datelist1 = []
        for i in tifs:
            datelist1.append(i[:-4])
        datelist = list(set(datelist1))
        datelist.sort(key=datelist1.index)
        print("有 %s 个日期" % len(datelist))
        print("datelist", datelist)
        # 定义切图的大小(矩形框)
        size = 256
        # len(datelist)

        for img in range(3):
            print("正在分割:", tifs[img])
            in_ds = gdal.Open(tifDir + "\\" + tifs[img])  # 读取要切的原图

            width = in_ds.RasterXSize  # 获取数据宽度
            height = in_ds.RasterYSize  # 获取数据高度
            outbandsize = in_ds.RasterCount  # 获取数据波段数
            im_geotrans = in_ds.GetGeoTransform()  # 获取仿射矩阵信息
            im_proj = in_ds.GetProjection()  # 获取投影信息
            datatype = in_ds.GetRasterBand(1).DataType
            im_data = in_ds.ReadAsArray()  # 获取数据

            col_num = int(width / size)  # 宽度可以分成几块
            row_num = int(height / size)  # 高度可以分成几块
            if (width % size != 0):
                col_num += 1
            if (height % size != 0):
                row_num += 1

            # print("row_num:%d   col_num:%d" % (row_num, col_num))
            for i in range(row_num):  # 从高度下手!!! 可以分成几块!
                for j in range(col_num):
                    offset_x = i * size
                    offset_y = j * size
                    ## 从每个波段中切需要的矩形框内的数据(注意读取的矩形框不能超过原图大小)
                    b_ysize = min(width - offset_y, size)
                    b_xsize = min(height - offset_x, size)

                    # print("width:%d     height:%d    offset_x:%d    offset_y:%d     b_xsize:%d     b_ysize:%d" % (width, height, offset_x, offset_y, b_xsize, b_ysize))
                    # print(im_data.shape)

                    out_allband = im_data[:, offset_x:offset_x + b_xsize, offset_y:offset_y + b_ysize]
                    # print(out_allband.shape)

                    # 获取Tif的驱动,为创建切出来的图文件做准备
                    gtif_driver = gdal.GetDriverByName("GTiff")
                    file = outPath + "\\" + tifs[img][:-4] + "-" + str(offset_x).zfill(10) + "-" + str(offset_y).zfill(
                        10) + ".tif"

                    # 创建切出来的要存的文件
                    out_ds = gtif_driver.Create(file, b_ysize, b_xsize, outbandsize, datatype)
                    # print("create new tif file succeed")

                    # 获取原图的原点坐标信息
                    ori_transform = in_ds.GetGeoTransform()
                    # if ori_transform:
                    # print(ori_transform)
                    # print("Origin = ({}, {})".format(ori_transform[0], ori_transform[3]))
                    # print("Pixel Size = ({}, {})".format(ori_transform[1], ori_transform[5]))

                    # 读取原图仿射变换参数值
                    top_left_x = ori_transform[0]  # 左上角x坐标
                    w_e_pixel_resolution = ori_transform[1]  # 东西方向像素分辨率
                    top_left_y = ori_transform[3]  # 左上角y坐标
                    n_s_pixel_resolution = ori_transform[5]  # 南北方向像素分辨率

                    # 根据反射变换参数计算新图的原点坐标
                    top_left_x = top_left_x + offset_y * w_e_pixel_resolution
                    top_left_y = top_left_y + offset_x * n_s_pixel_resolution

                    # 将计算后的值组装为一个元组,以方便设置
                    dst_transform = (
                        top_left_x, ori_transform[1], ori_transform[2], top_left_y, ori_transform[4], ori_transform[5])

                    # 设置裁剪出来图的原点坐标
                    out_ds.SetGeoTransform(dst_transform)

                    # 设置SRS属性(投影信息)
                    out_ds.SetProjection(in_ds.GetProjection())

                    # 写入目标文件
                    for ii in range(outbandsize):
                        out_ds.GetRasterBand(ii + 1).WriteArray(out_allband[ii])

                    # 将缓存写入磁盘
                    out_ds.FlushCache()
                    # print("FlushCache succeed")
                    del out_ds
                # print(i/row_num)
                # self.progressBar.setValue(int(100*(i+1)/row_num))
                # self._signal.emit(int(100*(i+1)/row_num))  # 注意这里与_signal = pyqtSignal(str)中的类型相同
                self._signal.emit(int(100 * (img/3+(i + 1) / (row_num*3))) ) # 注意这里与_signal = pyqtSignal(str)中的类型相同

class MyWindow(QtWidgets.QMainWindow, Ui_tile):
    def __init__(self):
        super(MyWindow, self).__init__()
        self.setupUi(self)
        self.maindata={}

        #绑定事件
        self.pushButton.clicked.connect(self.start_cacu)
        #打开文件夹
        self.menu.triggered[QAction].connect(self.wenjianjia)

        #选择文件夹
        act = QAction(self)  # 定义一个行为
        act.setIcon(QIcon('image/select.png'))  # 设置行为icon,
        act.triggered.connect(self.outdir)  # 绑定行为槽函数,这里槽函数为一个QMessageBox信息弹窗
        self.lineEdit_2.addAction(act, QLineEdit.TrailingPosition)  # 将该行为添加到lineEdit最右端

        # 数据列表添加点击事件
        self.listWidget.itemClicked.connect(self.itemClick)

    def outdir(self):
        if not os.path.exists(r"E:\pyimg\tif2csv"):
            self.maindata["outPath"] = QFileDialog.getExistingDirectory(None, "输出文件夹", "C:/")  # 返回选中的文件夹路径
            # QFileDialog.getOpenFileName()  # 返回选中的文件路径
            # QFileDialog.getOpenFileNames()  # 返回选中的多个文件路径
            # QFileDialog.getSaveFileName()  # 存储文件
        else:
            self.maindata["outPath"] = QFileDialog.getExistingDirectory(None, "输出文件夹", "E:\\pyimg\\tif2csv")  # 返回选中的文件夹路径
        self.lineEdit_2.setText((QtCore.QCoreApplication.translate("tile", self.maindata["outPath"])))
        print(self.maindata)

    def wenjianjia(self):
        if not os.path.exists(r"E:\pyimg\tif2csv"):
            self.maindata["tifDir"] = QFileDialog.getExistingDirectory(None, "选取文件夹", "C:/")  # 返回选中的文件夹路径
        else:
            self.maindata["tifDir"] = QFileDialog.getExistingDirectory(None, "选取文件夹", "E:\\pyimg\\tif2csv")  # 返回选中的文件夹路径
        # self.maindata["tifDir"]=QFileDialog.getExistingDirectory(None,"选取文件夹","E:/")  # 返回选中的文件夹路径
        print(self.maindata)
        self.maindata["tifs"] = [i for i in os.listdir(self.maindata["tifDir"]) if i.endswith(".tif")]
        # 数据列表框添加内容
        for img in self.maindata["tifs"]:
            self.listWidget.addItem(str(img))


    #数据列表框添加点击事件
    def itemClick(self, item):
        print(item.text() + " clicked!")
        QMessageBox.information(self, "ListWidget", "你选择了:" + item.text())

    #计算函数
    def start_cacu(self):
        #子窗体
        self.child = childWindow()
        self.child.pushButton.setVisible(False)
        self.child.show()

        self.maindata["分块大小"]=self.lineEdit.text()
        # 创建线程
        self.thread = Runthread(self.maindata)
        # 连接信号
        self.thread._signal.connect(self.call_backlog)  # 进程连接回传到GUI的事件
        self.child.stop_thread.connect(self.thread.terminate)
        # 开始线程
        self.thread.start()

    # 将线程的参数传入进度条事件
    def call_backlog(self, msg):
        self.child.progressBar.setValue(int(msg))
        if(msg==100):
            self.child.pushButton.setVisible(True)

    # 重写closeEvent方法,关闭窗口时触发
    def closeEvent(self, QCloseEvent):
        reply = QtWidgets.QMessageBox.question(self, '分块程序', "是否要退出程序?",QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,QtWidgets.QMessageBox.No)
        if reply == QtWidgets.QMessageBox.Yes:
            QCloseEvent.accept()
        else:
            QCloseEvent.ignore()

class childWindow(QDialog,Ui_Dialog):
    stop_thread = pyqtSignal()  # 定义关闭子线程的信号
    def __init__(self):
        super(childWindow, self).__init__()
        self.setupUi(self)
        # self.pushButton.clicked.connect(self.btn1)
        self.pushButton.clicked.connect(self.accept)
        self.pushButton_2.clicked.connect(self.accept)

    #窗口关闭就关闭线程
    def closeEvent(self, event):
        self.stop_thread.emit()

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    myWin = MyWindow()
    myWin.show()
    sys.exit(app.exec_())

;