Qt with VTK and Loading Files

I’ve seen some pyvista questions posted here, so I’m posting my question. I am trying to create a Qt GUI with pyvista and would like to implement Progress Bars when loading large file stacks. I have succeeded with the code below, but it is much slower than ParaView.

Does my implementation need improvement, or could it be that ParaView is somehow loading files in parallel? I have a folder of 1,174 DICOM files, each 5.96 MB. These are simply raw DICOM files (*.dcm), so they have not been preprocessed in any way.

Below are the times using my code vs. ParaView (including rendering it as a volume). I used time.time for my code and my stopwatch for ParaView:

My Code: ~13 minutes, 9 seconds
ParaView 5.9.1 (installed pre-built binary): ~24 seconds

This may not be related to the progress bar code at all, so if anyone can tell me how to make my load times on par with ParaView, that’s what I want! Thanks in advance!

Setup

OS: Windows 10 Professional x64-bit, Build 1909
Python: 3.8.10 x64-bit
PyQt: 5.15.4
pyvista: 0.31.3
IDE: VSCode 1.59.0

Project Directory

gui/
├───gui/
│   │   main.py
│   │   __init__.py
│   │   
│   ├───controller/
│   │       controller.py
│   │       __init__.py
│   │
│   ├───model/
│   │      model.py
│   │       __init__.py
│   │
│   └───view/
│           view.py
│            __init__.py
├───resources/
│   │    __init__.py
│   │   
│   └───icons
│       │   main.ico
│       │   __init__.py
│       │   
│       └───toolbar
│               new.png
│               __init__.py
└───tests/
    │   conftest.py
    │   __init__.py
    │
    └───unit_tests
            test_view.py
            __init__.py

Code

gui/main.py:

from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication

from gui.controller.controller import Controller
from gui.model.model import Model
from gui.view.view import View


class MainApp:
    def __init__(self) -> None:
        self.controller = Controller()
        self.model = self.controller.model
        self.view = self.controller.view

    def show(self) -> None:
        self.view.showMaximized()


if __name__ == "__main__":
    app = QApplication([])
    app.setStyle("fusion")
    app.setAttribute(Qt.AA_DontShowIconsInMenus, True)
    root = MainApp()
    root.show()
    app.exec_()

gui/view.py:

import os
import threading
from typing import Any

import numpy as np
import pyvista as pv
import SimpleITK as sitk
from PyQt5.QtCore import QObject, QPoint, QSize, Qt, pyqtSignal
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import (QAction, QDialog, QDockWidget, QFileDialog,
                             QFrame, QHBoxLayout, QMainWindow, QProgressBar,
                             QSplitter, QStatusBar, QTabWidget, QToolBar,
                             QTreeWidget, QTreeWidgetItem, QVBoxLayout,
                             QWidget)
from pyvistaqt import MainWindow, QtInteractor
from resources import icons
from resources.icons import project_explorer, toolbar
from SimpleITK import ImageFileReader, ImageSeriesReader

class FileSeriesWorker(QObject):
    started = pyqtSignal()
    finished = pyqtSignal()
    result = pyqtSignal(dict)

    def __init__(self, folder, plotter):
        super().__init__()
        self.folder = folder
        self.plotter = plotter
        self.thread = threading.Thread(target=self._load_files, daemon=True)

    def execute(self):
        self.thread.start()

    def _load_files(self):
        self.started.emit()

        dicom_reader = ImageSeriesReader()
        dicom_files = dicom_reader.GetGDCMSeriesFileNames(self.folder)
        dicom_reader.SetFileNames(dicom_files)
        scan = dicom_reader.Execute()

        origin = scan.GetOrigin()
        spacing = scan.GetSpacing()
        direction = scan.GetDirection()

        data = sitk.GetArrayFromImage(scan)
        data = (data // 256).astype(np.uint8)

        data_values = data[data > 0]
        pct_low, pct_high = np.percentile(data_values, [1, 99])
        clim = [pct_low, pct_high]

        volume = pv.UniformGrid(data.shape)

        volume.origin = origin
        volume.spacing = spacing
        volume.direction = direction

        volume.point_arrays["Values"] = data.flatten(order="F")
        volume.set_active_scalars("Values")

        self.plotter.add_volume(
            volume,
            clim=clim,
            opacity="sigmoid",
            reset_camera=True,
        )

        self.finished.emit()

        self.result.emit(
            {
                "origin": origin,
                "spacing": spacing,
                "direction": direction,
                "data": data,
                "volume": volume,
            }
        )


class ProgressBarDialog(QDialog):
    def __init__(self, parent=None, message=None):
        super().__init__(parent)

        self.setWindowTitle(message)

        self.progressbar = QProgressBar()

        layout = QVBoxLayout(self)
        layout.addWidget(self.progressbar)

        self.setLayout(layout)


class View(MainWindow):
    def __init__(
        self, controller, parent: QWidget = None, *args: Any, **kwargs: Any
    ) -> None:
        super().__init__(parent, *args, **kwargs)
        self.controller = controller

        # Set the window name
        self.setWindowTitle("Demo")

        # Create the container frame
        self.container = QFrame()

        # Create the layout
        self.layout = QGridLayout()
        self.layout.setContentsMargins(0, 0, 0, 0)

        # Set the layout
        self.container.setLayout(self.layout)
        self.setCentralWidget(self.container)

        # Set project variables
        self.project_is_open = False

        # Create and position widgets
        self._create_actions()
        self._create_menubar()
        self._create_toolbar()
        self._create_progressbar()

    def _create_actions(self):
        # New
        self.new_action = QAction(QIcon(toolbar.NEW_ICO), "&New Project...", self)
        self.new_action.setShortcut("Ctrl+N")
        self.new_action.setStatusTip("Create a new project...")
        self.new_action.triggered.connect(self._create_new_project)

        # Open Folder
        self.open_folder_action = QAction(
            QIcon(toolbar.OPEN_FOLDER_ICO), "&Open Folder...", self
        )
        self.open_folder_action.setShortcut("Ctrl+Shift+O")
        self.open_folder_action.setStatusTip("Open folder...")
        self.open_folder_action.triggered.connect(self._open_folder)

    def _create_menubar(self):
        self.menubar = self.menuBar()

        # File menu
        self.file_menu = self.menubar.addMenu("&File")

        self.file_menu.addAction(self.new_action)
        self.file_menu.addAction(self.open_folder_action)

    def _create_toolbar(self):
        self.toolbar = QToolBar("Main Toolbar")
        self.toolbar.setIconSize(QSize(16, 16))

        self.addToolBar(self.toolbar)

        self.toolbar.addAction(self.new_action)

    def _create_progressbar(self):
        self.progressbar_dialog = ProgressBarDialog(parent=self)

        self.progressbar_dialog.setGeometry(25, 250, 400, 50)
        self.progressbar_dialog.hide()

    def _open_folder(self):
        if not self.project_is_open:
            self.interactor_layout = QHBoxLayout()
            
            self.plotter = QtInteractor()

            self.plotter.add_axes()
            self.plotter.reset_camera()
            self.signal_close.connect(self.plotter.close)

            self.layout.addLayout(self.interactor_layout)

            self.container.setLayout(self.layout)
            self.setCentralWidget(self.container)

            self.project_is_open = True

        # Create "Open Folder..." Dialog
        dialog = QFileDialog()

        filters = ["DICOM Files (*.dcm)",]

        dialog.setFileMode(QFileDialog.Directory)
        dialog.setWindowTitle("Open Folder...")
        dialog.setNameFilters(filters)
        dialog.setLabelText(dialog.FileName, "Folder: ")
        dialog.setOptions(QFileDialog.DontUseNativeDialog)

        dialog.exec_()

        root = dialog.directory().path()
        directory = dialog.selectedFiles()[0]

        if directory:
            folder = os.path.join(root, directory)

            self.file_series_worker = FileSeriesWorker(folder, self.plotter)

            self.file_series_worker.started.connect(self._on_import_start)
            self.file_series_worker.finished.connect(self._on_import_finish)
            self.file_series_worker.result.connect(self._on_import_result)

            self.file_series_worker.execute()

    def _on_import_start(self):
        self.progressbar_dialog.setWindowTitle("Loading data...")

        x = self.x() + (self.width() - self.progressbar_dialog.width()) // 2
        y = self.y() + (self.height() - self.progressbar_dialog.height()) // 2

        self.progressbar_dialog.move(QPoint(x, y))
        self.progressbar_dialog.progressbar.setRange(0, 0)

        self.progressbar_dialog.setModal(True)
        self.progressbar_dialog.show()

    def _on_import_finish(self):
        pass

    def _on_import_result(self, result):
        self.origin = result["origin"]
        self.spacing = result["spacing"]
        self.direction = result["direction"]
        self.data = result["data"]
        self.volume = result["volume"]

        self.progressbar_dialog.hide()
        self.progressbar_dialog.progressbar.setRange(0, 1)
        self.progressbar_dialog.setModal(False)

gui/model.py:

from typing import Any


class Model(object):
    def __init__(self, controller, *args: Any, **kwargs: Any):
        self.controller = controller

gui/controller.py:

from typing import Any

from gui.model.model import Model
from gui.view.view import View


class Controller(object):
    def __init__(self, *args: Any, **kwargs: Any):
        self.model = Model(controller=self, *args, **kwargs)
        self.view = View(controller=self, *args, **kwargs)

Cc @banesullivan

Update: 06-SEP-2021

I’ve added a helper function print_time to my module view.py and broke down the time each function takes to execute. From this, it is clear volume plotting takes the longest. Why is volume plotting so much slower than in ParaView?

New Code:

def print_time(msg, start, finish):
    total_time = finish - start
    hours = int(total_time // 3600)
    total_time -= 3600 * hours
    minutes = int(total_time // 60)
    total_time -= 60 * minutes
    seconds = int(total_time)

    print(f"{msg} - Run Time: {hours}::{minutes}::{seconds} (hrs::mins::sec)")


class FileSeriesWorker(QObject):
    started = pyqtSignal()
    finished = pyqtSignal()
    result = pyqtSignal(dict)

    def __init__(self, folder, plotter):
        super().__init__()
        self.folder = folder
        self.plotter = plotter
        self.thread = threading.Thread(target=self._load_files, daemon=True)

    def execute(self):
        self.thread.start()

    def _load_files(self):
        self.started.emit()

        start = time.time()

        dicom_reader = ImageSeriesReader()
        dicom_files = dicom_reader.GetGDCMSeriesFileNames(self.folder)
        dicom_reader.SetFileNames(dicom_files)
        scan = dicom_reader.Execute()

        finish = time.time()

        print_time("ImageSeriesReader Execute", start, finish)

        origin = scan.GetOrigin()
        spacing = scan.GetSpacing()
        direction = scan.GetDirection()

        start = time.time()

        data = sitk.GetArrayFromImage(scan)

        data = (data // 256).astype(np.uint8)

        finish = time.time()

        print_time("GetArrayFromImage", start, finish)

        data_values = data[data > 0]
        pct_low, pct_high = np.percentile(data_values, [1, 99])
        clim = [pct_low, pct_high]

        start = time.time()

        volume = pv.UniformGrid(data.shape)

        finish = time.time()

        print_time("Create UniformGrid", start, finish)

        volume.origin = origin
        volume.spacing = spacing
        volume.direction = direction

        volume.point_arrays["Attenuation Coefficients"] = data.flatten(order="F")

        volume.set_active_scalars("Attenuation Coefficients")

        start = time.time()

        self.plotter.add_volume(
            volume,
            clim=clim,
            opacity="sigmoid",
            reset_camera=True,
        )

        finish = time.time()

        print_time("Plot Volume", start, finish)

        self.finished.emit()

        self.result.emit(
            {
                "origin": origin,
                "spacing": spacing,
                "direction": direction,
                "data": data,
                "volume": volume,
            }
        )

Output:

From this, it is quite clear now that plotting is taking the longest amount of time

WARNING: In d:\a\1\sitk-build\itk-prefix\include\itk-5.2\itkImageSeriesReader.hxx, line 480
ImageSeriesReader (0000017376F972B0): Non uniform sampling or missing slices detected,  maximum nonuniformity:7.39539e-07

ImageSeriesReader Execute - Run Time: 0::0::24 (hrs::mins::sec)
GetArrayFromImage - Run Time: 0::0::10 (hrs::mins::sec)
Create UniformGrid - Run Time: 0::0::0 (hrs::mins::sec)
2021-09-06 14:30:57.308 ( 761.842s) [                ]vtkWin32OpenGLRenderWin:217    ERR| vtkWin32OpenGLRenderWindow (000001736C95CDC0): wglMakeCurrent failed in MakeCurrent(), error: The requested resource is in use.

ERROR:root:wglMakeCurrent failed in MakeCurrent(), error: The requested resource is in use.
Plot Volume - Run Time: 0::10::49 (hrs::mins::sec)

Update #2: 06-SEP-2021

I used cProfile to break down issues in the plotting (see Profiling and optimizing your Python code | Python tricks by Sebastiaan Mathôt for details.

I pulled plotting into the View class _on_import_result method and added a top level decorator function profile in view.py

Code:

def profile(fnc):
    """Wrapper for cProfile"""
    def inner(*args, **kwargs):
        pr = cProfile.Profile()
        pr.enable()
        retval = fnc(*args, **kwargs)
        pr.disable()
        s = io.StringIO()
        sortby = "cumulative"
        ps = pstats.Stats(pr, stream=s).sort_stats(sortby)
        ps.print_stats()
        print(s.getvalue())
        return retval

    return inner
@profile
def _on_import_result(self, result):
    self.origin = result["origin"]
    self.spacing = result["spacing"]
    self.direction = result["direction"]
    self.data = result["data"]
    self.volume = result["volume"]

    data_values = self.data[self.data > 0]
    pct_low, pct_high = np.percentile(data_values, [1, 99])
    clim = [pct_low, pct_high]

    start = time.time()

    self.plotter.add_volume(
        self.volume,
        clim=clim,
        opacity="linear",
        reset_camera=True,
    )

    self.progressbar_dialog.hide()
    self.progressbar_dialog.progressbar.setRange(0, 1)
    self.progressbar_dialog.setModal(False)

Output:

WARNING: In d:\a\1\sitk-build\itk-prefix\include\itk-5.2\itkImageSeriesReader.hxx, line 480
ImageSeriesReader (00000206D88B8100): Non uniform sampling or missing slices detected,  maximum nonuniformity:7.40137e-07

ImageSeriesReader Execute - Run Time: 0::1::18 (hrs::mins::sec)
GetArrayFromImage - Run Time: 0::0::10 (hrs::mins::sec)
Create UniformGrid - Run Time: 0::0::0 (hrs::mins::sec)
         8284 function calls (7945 primitive calls) in 714.899 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    8.142    8.142  714.899  714.899 gui\view\view.py:571(_on_import_result)
        1   96.223   96.223  693.131  693.131 .venv\lib\site-packages\pyvista\plotting\plotting.py:1817(add_volume)        
        1    0.001    0.001  394.513  394.513 .venv\lib\site-packages\pyvista\core\filters.py:1483(cell_data_to_point_data)
        2  392.239  196.119  392.239  196.119 {method 'Update' of 'vtkmodules.vtkCommonExecutionModel.vtkAlgorithm' objects}
   121/38    9.357    0.077  137.771    3.626 {built-in method numpy.core._multiarray_umath.implement_array_function}
        3   19.357    6.452  103.425   34.475 .venv\lib\site-packages\numpy\lib\nanfunctions.py:68(_replace_nan)
      272   82.231    0.302   82.231    0.302 {built-in method numpy.array}
        2    0.001    0.000   77.979   38.990 <__array_function__ internals>:2(nanmin)
        2    0.941    0.471   72.894   36.447 .venv\lib\site-packages\numpy\lib\nanfunctions.py:228(nanmin)
        1    0.000    0.000   46.175   46.175 <__array_function__ internals>:2(nanmax)
        1    0.274    0.274   43.740   43.740 .venv\lib\site-packages\numpy\lib\nanfunctions.py:343(nanmax)
       24   37.062    1.544   37.062    1.544 {method 'astype' of 'numpy.ndarray' objects}
        2    0.000    0.000   27.030   13.515 .venv\lib\site-packages\pyvista\core\datasetattributes.py:171(append)        
        6    0.000    0.000   24.055    4.009 .venv\lib\site-packages\pyvista\utilities\helpers.py:83(convert_array)       
        3    0.001    0.000   24.055    8.018 .venv\lib\site-packages\vtkmodules\util\numpy_support.py:104(numpy_to_vtk)   
        1   23.339   23.339   23.339   23.339 {method 'DeepCopy' of 'vtkmodules.vtkCommonCore.vtkDataArray' objects}
        1    0.000    0.000   13.616   13.616 <__array_function__ internals>:2(percentile)
        1    0.000    0.000   13.616   13.616 .venv\lib\site-packages\numpy\lib\function_base.py:3724(percentile)
        1    0.000    0.000   13.616   13.616 .venv\lib\site-packages\numpy\lib\function_base.py:3983(_quantile_unchecked)
        1    0.450    0.450   13.616   13.616 .venv\lib\site-packages\numpy\lib\function_base.py:3513(_ureduce)
        1    0.000    0.000   13.166   13.166 .venv\lib\site-packages\numpy\lib\function_base.py:4018(_quantile_ureduce_func)
       29    0.000    0.000   11.994    0.414 .venv\lib\site-packages\numpy\core\fromnumeric.py:69(_wrapreduction)
       30   11.993    0.400   11.994    0.400 {method 'reduce' of 'numpy.ufunc' objects}
        2    0.000    0.000   11.450    5.725 .venv\lib\site-packages\pyvista\plotting\plotting.py:477(add_actor)
        2    0.000    0.000   11.450    5.725 .venv\lib\site-packages\pyvista\plotting\renderer.py:342(add_actor)
        2    0.000    0.000   11.449    5.724 .venv\lib\site-packages\pyvistaqt\plotting.py:336(render)
        2    0.000    0.000   11.449    5.724 {method 'emit' of 'PyQt5.QtCore.pyqtBoundSignal' objects}
        2    0.000    0.000   11.449    5.724 .venv\lib\site-packages\pyvistaqt\plotting.py:331(_render)
        2    0.000    0.000   11.449    5.724 .venv\lib\site-packages\pyvista\plotting\plotting.py:808(render)
        2   11.449    5.724   11.449    5.724 {method 'Render' of 'vtkmodules.vtkRenderingOpenGL2.vtkOpenGLRenderWindow' objects}
        1    0.000    0.000   11.405   11.405 .venv\lib\site-packages\pyvista\plotting\renderer.py:1362(reset_camera)
        1   11.236   11.236   11.236   11.236 {method 'partition' of 'numpy.ndarray' objects}
        2    0.000    0.000    6.644    3.322 <__array_function__ internals>:2(amin)
        2    0.000    0.000    6.644    3.322 .venv\lib\site-packages\numpy\core\fromnumeric.py:2763(amin)
        2    0.000    0.000    6.644    3.322 {method 'min' of 'numpy.ndarray' objects}
        2    0.000    0.000    6.644    3.322 .venv\lib\site-packages\numpy\core\_methods.py:42(_amin)
        1    0.000    0.000    4.964    4.964 <__array_function__ internals>:2(amax)
        1    0.000    0.000    4.964    4.964 .venv\lib\site-packages\numpy\core\fromnumeric.py:2638(amax)
        1    0.000    0.000    4.964    4.964 {method 'max' of 'numpy.ndarray' objects}
        1    0.000    0.000    4.964    4.964 .venv\lib\site-packages\numpy\core\_methods.py:38(_amax)
        1    0.000    0.000    3.330    3.330 .venv\lib\site-packages\pyvista\core\dataset.py:637(__setitem__)
        1    0.000    0.000    3.330    3.330 .venv\lib\site-packages\pyvista\core\datasetattributes.py:55(__setitem__)
        2    2.974    1.487    2.974    1.487 {method 'AddArray' of 'vtkmodules.vtkCommonDataModel.vtkFieldData' objects}
        2    2.648    1.324    2.648    1.324 .venv\lib\site-packages\pyvista\core\pyvista_ndarray.py:50(__setitem__)
        1    0.000    0.000    2.273    2.273 .venv\lib\site-packages\pyvista\core\filters.py:51(_get_output)
        1    0.000    0.000    2.272    2.272 .venv\lib\site-packages\pyvista\utilities\helpers.py:528(wrap)
        1    0.000    0.000    2.272    2.272 .venv\lib\site-packages\pyvista\core\grid.py:281(__init__)
        1    0.000    0.000    2.272    2.272 .venv\lib\site-packages\pyvista\core\dataobject.py:44(deep_copy)
        1    2.271    2.271    2.271    2.271 {method 'DeepCopy' of 'vtkmodules.vtkCommonDataModel.vtkImageData' objects}
        1    1.929    1.929    1.929    1.929 {method 'flatten' of 'numpy.ndarray' objects}
        6    0.000    0.000    1.837    0.306 <__array_function__ internals>:2(copyto)
        3    0.609    0.203    0.609    0.203 {method 'SetVoidArray' of 'vtkmodules.vtkCommonCore.vtkAbstractArray' objects}
        3    0.000    0.000    0.386    0.129 <__array_function__ internals>:2(all)
        3    0.000    0.000    0.386    0.129 .venv\lib\site-packages\numpy\core\fromnumeric.py:2367(all)
        3    0.000    0.000    0.385    0.128 {method 'all' of 'numpy.ndarray' objects}
        3    0.000    0.000    0.385    0.128 .venv\lib\site-packages\numpy\core\_methods.py:60(_all)
        3    0.105    0.035    0.105    0.035 {method 'SetNumberOfTuples' of 'vtkmodules.vtkCommonCore.vtkAbstractArray' objects}
        1    0.000    0.000    0.046    0.046 .venv\lib\site-packages\pyvista\plotting\plotting.py:2317(add_scalar_bar)
        1    0.001    0.001    0.046    0.046 .venv\lib\site-packages\pyvista\plotting\scalar_bars.py:115(add_scalar_bar)
        1    0.037    0.037    0.037    0.037 .venv\lib\site-packages\pyvista\plotting\mapper.py:4(make_mapper)
        1    0.010    0.010    0.010    0.010 {built-in method hide}
      257    0.003    0.000    0.005    0.000 .venv\lib\site-packages\matplotlib\colors.py:588(__call__)
        1    0.000    0.000    0.004    0.004 .venv\lib\site-packages\pyvista\plotting\colors.py:393(get_cmap_safe)
        2    0.000    0.000    0.003    0.002 <frozen importlib._bootstrap>:986(_find_and_load)
        2    0.000    0.000    0.003    0.002 <frozen importlib._bootstrap>:956(_find_and_load_unlocked)
        2    0.000    0.000    0.003    0.002 <frozen importlib._bootstrap>:890(_find_spec)
        2    0.000    0.000    0.003    0.002 <frozen importlib._bootstrap_external>:1399(find_spec)
        2    0.000    0.000    0.003    0.002 <frozen importlib._bootstrap_external>:1367(_get_spec)
       22    0.001    0.000    0.003    0.000 <frozen importlib._bootstrap_external>:1498(find_spec)
        1    0.000    0.000    0.002    0.002 .venv\lib\site-packages\pyvista\plotting\tools.py:239(opacity_transfer_function)
        7    0.000    0.000    0.001    0.000 .venv\lib\site-packages\pyvista\core\dataset.py:98(active_scalars_info)
       22    0.000    0.000    0.001    0.000 <frozen importlib._bootstrap_external>:135(_path_stat)
        9    0.000    0.000    0.001    0.000 .venv\lib\site-packages\pyvista\core\dataset.py:46(_namedtuple)
       22    0.001    0.000    0.001    0.000 {built-in method nt.stat}
       28    0.000    0.000    0.001    0.000 .venv\lib\site-packages\pyvista\core\pyvista_ndarray.py:31(__array_finalize__)
        8    0.000    0.000    0.001    0.000 .venv\lib\site-packages\pyvista\core\dataset.py:52(__iter__)
      110    0.001    0.000    0.001    0.000 <frozen importlib._bootstrap_external>:91(_path_join)
        9    0.001    0.000    0.001    0.000 C:\Program Files\Python38\lib\collections\__init__.py:313(namedtuple)
      371    0.000    0.000    0.001    0.000 {built-in method builtins.isinstance}
        1    0.000    0.000    0.001    0.001 .venv\lib\site-packages\pyvista\core\dataset.py:413(active_scalars)
        9    0.000    0.000    0.001    0.000 C:\Program Files\Python38\lib\abc.py:96(__instancecheck__)
        9    0.000    0.000    0.001    0.000 {built-in method _abc._abc_instancecheck}
    131/3    0.000    0.000    0.001    0.000 C:\Program Files\Python38\lib\abc.py:100(__subclasscheck__)
    131/3    0.001    0.000    0.001    0.000 {built-in method _abc._abc_subclasscheck}
       12    0.000    0.000    0.001    0.000 <__array_function__ internals>:2(linspace)
        5    0.000    0.000    0.001    0.000 .venv\lib\site-packages\pyvista\core\pyvista_ndarray.py:14(__new__)
       12    0.000    0.000    0.001    0.000 .venv\lib\site-packages\numpy\core\function_base.py:23(linspace)
        1    0.000    0.000    0.001    0.001 .venv\lib\site-packages\pyvista\core\dataset.py:516(copy_meta_from)
       22    0.000    0.000    0.001    0.000 <__array_function__ internals>:2(any)
        1    0.000    0.000    0.001    0.001 .venv\lib\site-packages\pyvista\core\datasetattributes.py:48(__getitem__)
        1    0.000    0.000    0.001    0.001 .venv\lib\site-packages\pyvista\core\datasetattributes.py:142(get_array)
        3    0.000    0.000    0.001    0.000 .venv\lib\site-packages\pyvista\core\dataset.py:179(active_scalars_name)
       22    0.000    0.000    0.001    0.000 .venv\lib\site-packages\numpy\core\fromnumeric.py:2268(any)
        1    0.000    0.000    0.001    0.001 .venv\lib\site-packages\matplotlib\colors.py:1052(_init)
        2    0.000    0.000    0.001    0.000 <__array_function__ internals>:2(geomspace)
        1    0.000    0.000    0.001    0.001 .venv\lib\site-packages\matplotlib\colors.py:302(to_rgba_array)
        2    0.000    0.000    0.000    0.000 .venv\lib\site-packages\numpy\core\function_base.py:286(geomspace)
        9    0.000    0.000    0.000    0.000 {built-in method builtins.exec}
      257    0.000    0.000    0.000    0.000 .venv\lib\site-packages\numpy\ma\core.py:6566(is_masked)
        1    0.000    0.000    0.000    0.000 .venv\lib\site-packages\pyvista\core\dataset.py:326(set_active_scalars)
       28    0.000    0.000    0.000    0.000 .venv\lib\site-packages\vtkmodules\numpy_interface\dataset_adapter.py:275(__array_finalize__)
        2    0.000    0.000    0.000    0.000 .venv\lib\site-packages\pyvista\plotting\renderer.py:1231(remove_actor)
        1    0.000    0.000    0.000    0.000 .venv\lib\site-packages\pyvista\core\dataset.py:119(active_vecto

Make sure you’re not updating the progress bar too often. I have a large file loader too in which I had to update the progress bar just once every 1MB or so in it’s file reading loop. GUI updating takes quite a while and should be done very sparingly from inside performance critical loops.