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)