Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PyVista add_volume: Garbled Output, Much Slower than ParaView, & Memory Hog #500

Closed
adam-grant-hendry opened this issue Sep 6, 2021 · 25 comments

Comments

@adam-grant-hendry
Copy link

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)
@adam-grant-hendry
Copy link
Author

Cc @banesullivan

@adam-grant-hendry
Copy link
Author

adam-grant-hendry commented Sep 6, 2021

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)

@adam-grant-hendry
Copy link
Author

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

@adam-grant-hendry
Copy link
Author

@adeak @akaszynski @MatthewFlamm

I have updated my issue on my Stack Overflow post here:

PyVista add_volume: Garbled Output & Slower than ParaView

I have spoken with David Gobbi (creator of vtkDICOM), and he mentioned on my VTK issue vtkDICOM Deprecation Warning:

The vtk-dicom handling of files is different. More or less, it scans a folder for DICOM files, and then you choose the series that you want. This is handled by the vtkDICOMDirectory class.
Then the series of files is passed to vtkDICOMReader as a vtkStringArray, via the reader.SetFileNames() method. The reader expects a series that corresponds to a 3D, 4D or 5D volume (dimensions above 3D are handled in tricky ways, the docs describe the details).
One thing to be careful about with the reader, is that by default it outputs "Modality" values instead of "Stored" values, meaning that it converts the raw numbers into Hounsfield units if RescaleSlope/RescaleIntercept are present in the file. This sometimes results in floating-point numbers, which give terrible performance for volume rendering. The reader.AutoRescaleOff() option undoes this behavior to provide the raw values (usually 16-bit ints) stored in the file.
I generally aim for either unsigned 8-bit or unsigned 16-bit image data for best overall VTK performance. Using float not only doubles the needed memory, it can also slow down the computations.

It turns out that I've realized my problem is actually two-fold: Plotting is slow, but my output is also garbled AND plotted in the wrong direction:

(NOTE: Again, to reiterate, I cannot share the actual data because it is confidential. However, below you can see pyvista orients my data along the z-axis, when in fact it should be along the x-axis, and that the output is a mess. For ParaView, I show the bounding box (the image is fine here).

The results are the same regardless if I use the fixed_point vs. smart volume mappers. I use fixed_point since I am on Windows.)

pyvista:

pyvista

ParaView:

ParaView

My MRE is as follows:

import cProfile
import io
import os
import pstats

import numpy as np
import pyvista as pv
import SimpleITK as sitk
from SimpleITK import ImageSeriesReader
from trimesh import points

pv.rcParams["volume_mapper"] = "fixed_point"  # Windows
folder = "C:\\path\\to\\DICOM\\stack\\folder"


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 plot_volume(folder):
    p = pv.Plotter()

    dicom_reader = ImageSeriesReader()
    dicom_files = dicom_reader.GetGDCMSeriesFileNames(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)  # Cast 16-bit to 8-bit

    volume = pv.UniformGrid(data.shape)

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

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

    p.add_volume(
        volume,
        opacity="sigmoid",
        reset_camera=True,
    )
    p.add_axes()

    p.show()


if __name__ == "__main__":
    plot_volume(folder)

Can you please assist? As always, thank you in advance!

@adam-grant-hendry
Copy link
Author

adam-grant-hendry commented Sep 14, 2021

Update: 14-SEP-2021

Interestingly, when trying to print out the shapes of data for debugging purposes as follows:

    data_flattened = data.flatten(order="F")

    volume.point_data["Values"] = data_flattened
    volume.set_active_scalars("Values")

    print(f"Points Shape: {volume.points.shape}")
    print(f"Data Shape: {data.shape}")
    print(f"Flattened Data Shape: {data_flattened.shape}")

I get the following error when trying to print volume.points.shape:

Error:

numpy.core._exceptions.MemoryError: Unable to allocate 81.9 GiB for an array with shape (3662502344, 3) and data type float64

Output:

Traceback (most recent call last):
  File "C:\Program Files\Python38\lib\runpy.py", line 194, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "C:\Program Files\Python38\lib\runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "c:\Users\user\.vscode\extensions\ms-python.python-2021.9.1191016588\pythonFiles\lib\python\debugpy\__main__.py", line 45, in <module>
    cli.main()
  File "c:\Users\user\.vscode\extensions\ms-python.python-2021.9.1191016588\pythonFiles\lib\python\debugpy/..\debugpy\server\cli.py", line 444, in main    
    run()
  File "c:\Users\user\.vscode\extensions\ms-python.python-2021.9.1191016588\pythonFiles\lib\python\debugpy/..\debugpy\server\cli.py", line 285, in run_file
    runpy.run_path(target_as_str, run_name=compat.force_str("__main__"))
  File "C:\Program Files\Python38\lib\runpy.py", line 265, in run_path
    return _run_module_code(code, init_globals, run_name,
  File "C:\Program Files\Python38\lib\runpy.py", line 97, in _run_module_code
    _run_code(code, mod_globals, init_globals,
  File "C:\Program Files\Python38\lib\runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "c:\Users\user\Code\gui\gui\main.py", line 81, in <module>
    plot_volume(folder)
  File "c:\Users\user\Code\gui\gui\main.py", line 22, in inner
    retval = fnc(*args, **kwargs)
  File "c:\Users\user\Code\gui\gui\main.py", line 65, in plot_volume
    print(f"Points Shape: {volume.points.shape}")
  File "c:\Users\user\Code\gui\.venv\lib\site-packages\pyvista\core\grid.py", line 368, in points
    return np.c_[xx.ravel(order='F'), yy.ravel(order='F'), zz.ravel(order='F')]
  File "c:\Users\user\Code\gui\.venv\lib\site-packages\numpy\lib\index_tricks.py", line 413, in __getitem__
    res = self.concatenate(tuple(objs), axis=axis)
  File "<__array_function__ internals>", line 5, in concatenate
numpy.core._exceptions.MemoryError: Unable to allocate 81.9 GiB for an array with shape (3662502344, 3) and data type float64

At the start...

image

...and by the end...

image

...until crash

image

@adam-grant-hendry adam-grant-hendry changed the title How to Implement Qt Progress Bars when Loading Data PyVista add_volume: Garbled Output, Much Slower than ParaView, & Memory Hog Sep 14, 2021
@adam-grant-hendry
Copy link
Author

@akaszynski @adeak @banesullivan @MatthewFlamm

THOUGHT: I don't see CopyImportVoidPointer being used in the pyvista code to make an internal copy of data. Would it help?

See this gist

@adam-grant-hendry
Copy link
Author

adam-grant-hendry commented Sep 15, 2021

@akaszynski @adeak @banesullivan @MatthewFlamm

I'm pretty sure this is a huge problem: in pyvista/plotting/plotting.py function add_volume

scalars = scalars.astype(np.float_)
        with np.errstate(invalid='ignore'):
            idxs0 = scalars < clim[0]
            idxs1 = scalars > clim[1]
        scalars[idxs0] = clim[0]
        scalars[idxs1] = clim[1]
        scalars = ((scalars - np.nanmin(scalars)) / (np.nanmax(scalars) - np.nanmin(scalars))) * 255
        # scalars = scalars.astype(np.uint8)
        volume[title] = scalars

you are taking my data, which was already

Shape: (1172, 2402, 1301)
dtype: 16-bit int (2 bytes)

Total Size = 1172 * 2402 * 1301 * 2 = 7.325 GB

and exploding it into a 64-bit float

Shape: (1172, 2402, 1301)
dtype: 16-bit int (8 bytes)

Total Size = 1172 * 2402 * 1301 * 8 = 58.6 GB!

Instead, you should cast clim to the dtype of scalars and perform the comparison on that since you're doing a simple <, > comparison here! In fact, you'll get more errors with floating point if you're not using np.isclose, so I don't see any value in casting this to a float: it's wasting a ton of memory.

Then, everyting is cast everything back to 0-255 range, but the data is still 64-bit float! The end value is still a floating point number and the dtype hasn't been changed to uint8...but why are we doing this in the first place? Not everyone's scalars are in the range of 0-255.

@adam-grant-hendry
Copy link
Author

adam-grant-hendry commented Sep 16, 2021

@akaszynski @adeak @banesullivan @MatthewFlamm

I found the answer. Indeed, this line:

scalars = scalars.astype(np.float_)

is too expensive a memory copy and these two lines:

scalars[idxs0] = clim[0]
scalars[idxs1] = clim[1]

cause memory to explode.

Furthermore, SimpleITK's DICOM reader is too slow. pyvista doesn't support reading folder with image stacks although vtkDICOMImageReader does, so I changed my code to the following:

reader = vtkDICOMImageReader()
reader.SetDirectoryName(folder)
reader.Update()

volume = pv.wrap(reader.GetOutputDataObject(0))

which creates a UniformGrid. Also, setting the spacing to anything other than (1, 1, 1) causes the output to be garbled, so I removed setting the spacing (this doesn't make sense to me as the actual dimensional spacing of my data is not (1, 1, 1)).

Lastly, I clip and rescale my scalars data before passing it to add_volume because:

  1. I have a memory efficient way of doing it that is fast and doesn't cause memory usage to explode, and
  2. Even putting these lines of code in add_volume causes memory to explode, so I suspect there are multiple references to the same data all trying to be updated simultaneously by making expensive array copies.

My efficient code is this:

import cProfile
import io
import os
import pstats

import numpy as np
import pyvista as pv
from memory_profiler import profile as memory_profile
from vtkmodules.vtkIOImage import vtkDICOMImageReader

pv.rcParams["volume_mapper"] = "fixed_point"  # Windows
folder = r"C:\path\to\DICOM\folder"

def time_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

@time_profile
def load_data(folder):
    reader = vtkDICOMImageReader()
    reader.SetDirectoryName(folder)
    reader.Update()

    volume = pv.wrap(reader.GetOutputDataObject(0))

    del reader  # Why keep double memory?

    clim_16bit = [23000, 65481]  # Original 16-bit values
    clim_8bit = [int(clim_16bit[0] // 256), int(clim_16bit[1] // 256)]  # Scaled 8-bit values

    scalars = volume["DICOMImage"]
    scalars.clip(clim_16bit[0], clim_16bit[1], out=scalars)

    min_ = np.nanmin(scalars)
    max_ = np.nanmax(scalars)

    np.true_divide((scalars - min_), (max_ - min_) / 255, out=scalars, casting="unsafe")

    volume["DICOMImage"] = np.array(scalars, dtype=np.uint8)

    volume.spacing = (1, 1, 1)  # Be sure to set; Otherwise, the DICOM stack spacing will be used and results will be garbled

    return volume

if __name__ == "__main__":
    print("Load Data Profile")
    print("=================")

    volume = load_data(folder)

    print()

    p = pv.Plotter()

    print("Add Volume Profile")
    print("=================")

    p.add_volume(
        volume,
        blending="composite",
        scalars="DICOMImage",
        reset_camera=True,
        rescale_scalars=False,
        copy_cell_to_point_data=False,
    )

    print()

    p.add_axes()

    p.show()

Finally, I changed pyvista/plotting/plotting.py function add_volume() to the following, commenting out unnecessary code:

@time_profile
def add_volume(
    self,
    volume,
    scalars=None,
    clim=None,
    resolution=None,
    opacity="linear",
    n_colors=256,
    cmap=None,
    flip_scalars=False,
    reset_camera=None,
    name=None,
    ambient=0.0,
    categories=False,
    culling=False,
    multi_colors=False,
    blending="composite",
    mapper=None,
    scalar_bar_args=None,
    show_scalar_bar=None,
    annotations=None,
    pickable=True,
    preference="point",
    opacity_unit_distance=None,
    shade=False,
    diffuse=0.7,
    specular=0.2,
    specular_power=10.0,
    render=True,
    rescale_scalars=True,
    copy_cell_to_point_data=True,
    **kwargs,
):
    """Add a volume, rendered using a smart mapper by default.
    Requires a 3D :class:`numpy.ndarray` or :class:`pyvista.UniformGrid`.
    Parameters
    ----------
    volume : 3D numpy.ndarray or pyvista.UniformGrid
        The input volume to visualize. 3D numpy arrays are accepted.
    scalars : str or numpy.ndarray, optional
        Scalars used to "color" the mesh.  Accepts a string name of an
        array that is present on the mesh or an array equal
        to the number of cells or the number of points in the
        mesh.  Array should be sized as a single vector. If ``scalars`` is
        ``None``, then the active scalars are used.
    clim : 2 item list, optional
        Color bar range for scalars.  Defaults to minimum and
        maximum of scalars array.  Example: ``[-1, 2]``. ``rng``
        is also an accepted alias for this.
    resolution : list, optional
        Block resolution.
    opacity : str or numpy.ndarray, optional
        Opacity mapping for the scalars array.
        A string can also be specified to map the scalars range to a
        predefined opacity transfer function (options include: 'linear',
        'linear_r', 'geom', 'geom_r'). Or you can pass a custom made
        transfer function that is an array either ``n_colors`` in length or
        shorter.
    n_colors : int, optional
        Number of colors to use when displaying scalars. Defaults to 256.
        The scalar bar will also have this many colors.
    cmap : str, optional
        Name of the Matplotlib colormap to us when mapping the ``scalars``.
        See available Matplotlib colormaps.  Only applicable for when
        displaying ``scalars``. Requires Matplotlib to be installed.
        ``colormap`` is also an accepted alias for this. If ``colorcet`` or
        ``cmocean`` are installed, their colormaps can be specified by name.
    flip_scalars : bool, optional
        Flip direction of cmap. Most colormaps allow ``*_r`` suffix to do
        this as well.
    reset_camera : bool, optional
        Reset the camera after adding this mesh to the scene.
    name : str, optional
        The name for the added actor so that it can be easily
        updated.  If an actor of this name already exists in the
        rendering window, it will be replaced by the new actor.
    ambient : float, optional
        When lighting is enabled, this is the amount of light from
        0 to 1 that reaches the actor when not directed at the
        light source emitted from the viewer.  Default 0.0.
    categories : bool, optional
        If set to ``True``, then the number of unique values in the scalar
        array will be used as the ``n_colors`` argument.
    culling : str, optional
        Does not render faces that are culled. Options are ``'front'`` or
        ``'back'``. This can be helpful for dense surface meshes,
        especially when edges are visible, but can cause flat
        meshes to be partially displayed.  Defaults ``False``.
    multi_colors : bool, optional
        Whether or not to use multiple colors when plotting MultiBlock
        object. Blocks will be colored sequentially as 'Reds', 'Greens',
        'Blues', and 'Grays'.
    blending : str, optional
        Blending mode for visualisation of the input object(s). Can be
        one of 'additive', 'maximum', 'minimum', 'composite', or
        'average'. Defaults to 'additive'.
    mapper : str, optional
        Volume mapper to use given by name. Options include:
        ``'fixed_point'``, ``'gpu'``, ``'open_gl'``, and
        ``'smart'``.  If ``None`` the ``"volume_mapper"`` in the
        ``self._theme`` is used.
    scalar_bar_args : dict, optional
        Dictionary of keyword arguments to pass when adding the
        scalar bar to the scene. For options, see
        :func:`pyvista.BasePlotter.add_scalar_bar`.
    show_scalar_bar : bool
        If ``False``, a scalar bar will not be added to the
        scene. Defaults to ``True``.
    annotations : dict, optional
        Pass a dictionary of annotations. Keys are the float
        values in the scalars range to annotate on the scalar bar
        and the values are the the string annotations.
    pickable : bool, optional
        Set whether this mesh is pickable.
    preference : str, optional
        When ``mesh.n_points == mesh.n_cells`` and setting
        scalars, this parameter sets how the scalars will be
        mapped to the mesh.  Default ``'points'``, causes the
        scalars will be associated with the mesh points.  Can be
        either ``'points'`` or ``'cells'``.
    opacity_unit_distance : float
        Set/Get the unit distance on which the scalar opacity
        transfer function is defined. Meaning that over that
        distance, a given opacity (from the transfer function) is
        accumulated. This is adjusted for the actual sampling
        distance during rendering. By default, this is the length
        of the diagonal of the bounding box of the volume divided
        by the dimensions.
    shade : bool
        Default off. If shading is turned on, the mapper may
        perform shading calculations - in some cases shading does
        not apply (for example, in a maximum intensity projection)
        and therefore shading will not be performed even if this
        flag is on.
    diffuse : float, optional
        The diffuse lighting coefficient. Default ``1.0``.
    specular : float, optional
        The specular lighting coefficient. Default ``0.0``.
    specular_power : float, optional
        The specular power. Between ``0.0`` and ``128.0``.
    render : bool, optional
        Force a render when True.  Default ``True``.
    rescale_scalars : bool, optional
        Rescale scalar data. This is an expensive memory and time
        operation, especially for large data. In that case, it is
        best to set this to ``False``, clip and scale scalar data
        of ``volume`` beforehand, and pass that to ``add_volume``.
        Default ``True``.
    copy_cell_to_point_data : bool, optional
        Make a copy of the original ``volume``, passing cell data
        to point data. This is an expensive memory and time
        operation, especially for large data. In that case, it is
        best to choose ``False``. However, this copy is a current
        workaround to ensure original object data is not altered
        and volume rendering on cells exhibits some issues. Use
        with caution. Default ``True``.
    **kwargs : dict, optional
        Optional keyword arguments.
    Returns
    -------
    vtk.vtkActor
        VTK actor of the volume.
    """
    # Handle default arguments

    # Supported aliases
    clim = kwargs.pop("rng", clim)
    cmap = kwargs.pop("colormap", cmap)
    culling = kwargs.pop("backface_culling", culling)

    if "scalar" in kwargs:
        raise TypeError(
            "`scalar` is an invalid keyword argument for `add_mesh`. Perhaps you mean `scalars` with an s?"
        )
    assert_empty_kwargs(**kwargs)

    # Avoid mutating input
    if scalar_bar_args is None:
        scalar_bar_args = {}
    else:
        scalar_bar_args = scalar_bar_args.copy()
    # account for legacy behavior
    if "stitle" in kwargs:  # pragma: no cover
        warnings.warn(USE_SCALAR_BAR_ARGS, PyvistaDeprecationWarning)
        scalar_bar_args.setdefault("title", kwargs.pop("stitle"))

    if show_scalar_bar is None:
        show_scalar_bar = self._theme.show_scalar_bar

    if culling is True:
        culling = "backface"

    if mapper is None:
        mapper = self._theme.volume_mapper

    # only render when the plotter has already been shown
    if render is None:
        render = not self._first_time

    # Convert the VTK data object to a pyvista wrapped object if necessary
    if not is_pyvista_dataset(volume):
        if isinstance(volume, np.ndarray):
            volume = wrap(volume)
            if resolution is None:
                resolution = [1, 1, 1]
            elif len(resolution) != 3:
                raise ValueError("Invalid resolution dimensions.")
            volume.spacing = resolution
        else:
            volume = wrap(volume)
            if not is_pyvista_dataset(volume):
                raise TypeError(
                    f"Object type ({type(volume)}) not supported for plotting in PyVista."
                )
    else:
        if copy_cell_to_point_data:
            # HACK: Make a copy so the original object is not altered.
            #       Also, place all data on the nodes as issues arise when
            #       volume rendering on the cells.
            volume = volume.cell_data_to_point_data()

    if name is None:
        name = f"{type(volume).__name__}({volume.memory_address})"

    if isinstance(volume, pyvista.MultiBlock):
        from itertools import cycle

        cycler = cycle(["Reds", "Greens", "Blues", "Greys", "Oranges", "Purples"])
        # Now iteratively plot each element of the multiblock dataset
        actors = []
        for idx in range(volume.GetNumberOfBlocks()):
            if volume[idx] is None:
                continue
            # Get a good name to use
            next_name = f"{name}-{idx}"
            # Get the data object
            block = wrap(volume.GetBlock(idx))
            if resolution is None:
                try:
                    block_resolution = block.GetSpacing()
                except AttributeError:
                    block_resolution = resolution
            else:
                block_resolution = resolution
            if multi_colors:
                color = next(cycler)
            else:
                color = cmap

            a = self.add_volume(
                block,
                resolution=block_resolution,
                opacity=opacity,
                n_colors=n_colors,
                cmap=color,
                flip_scalars=flip_scalars,
                reset_camera=reset_camera,
                name=next_name,
                ambient=ambient,
                categories=categories,
                culling=culling,
                clim=clim,
                mapper=mapper,
                pickable=pickable,
                opacity_unit_distance=opacity_unit_distance,
                shade=shade,
                diffuse=diffuse,
                specular=specular,
                specular_power=specular_power,
                render=render,
            )

            actors.append(a)
        return actors

    if not isinstance(volume, pyvista.UniformGrid):
        raise TypeError(
            f"Type {type(volume)} not supported for volume rendering at this time. Use `pyvista.UniformGrid`."
        )

    if opacity_unit_distance is None:
        opacity_unit_distance = volume.length / (np.mean(volume.dimensions) - 1)

    if scalars is None:
        # Make sure scalars components are not vectors/tuples
        scalars = volume.active_scalars
        # Don't allow plotting of string arrays by default
        if scalars is not None and np.issubdtype(scalars.dtype, np.number):
            scalar_bar_args.setdefault("title", volume.active_scalars_info[1])
        else:
            raise ValueError("No scalars to use for volume rendering.")
    # NOTE: AGH, 16-SEP-2021; Remove this as it is unnecessary
    # elif isinstance(scalars, str):
    #     pass

    # NOTE: AGH, 16-SEP-2021; Why this comment block
    ##############

    title = "Data"
    if isinstance(scalars, str):
        title = scalars
        scalars = get_array(volume, scalars, preference=preference, err=True)
        scalar_bar_args.setdefault("title", title)

    if not isinstance(scalars, np.ndarray):
        scalars = np.asarray(scalars)

    if not np.issubdtype(scalars.dtype, np.number):
        raise TypeError(
            "Non-numeric scalars are currently not supported for volume rendering."
        )

    if scalars.ndim != 1:
        scalars = scalars.ravel()

    # NOTE: AGH, 16-SEP-2021; An expensive unnecessary memory copy. Remove this.
    # if scalars.dtype == np.bool_ or scalars.dtype == np.uint8:
    #     scalars = scalars.astype(np.float_)

    # Define mapper, volume, and add the correct properties
    mappers = {
        "fixed_point": _vtk.vtkFixedPointVolumeRayCastMapper,
        "gpu": _vtk.vtkGPUVolumeRayCastMapper,
        "open_gl": _vtk.vtkOpenGLGPUVolumeRayCastMapper,
        "smart": _vtk.vtkSmartVolumeMapper,
    }
    if not isinstance(mapper, str) or mapper not in mappers.keys():
        raise TypeError(
            f"Mapper ({mapper}) unknown. Available volume mappers include: {', '.join(mappers.keys())}"
        )
    self.mapper = make_mapper(mappers[mapper])

    # Scalars interpolation approach
    if scalars.shape[0] == volume.n_points:
        # NOTE: AGH, 16-SEP-2021; Why the extra copy?
        # volume.point_data.set_array(scalars, title, True)
        self.mapper.SetScalarModeToUsePointData()
    elif scalars.shape[0] == volume.n_cells:
        # NOTE: AGH, 16-SEP-2021; Why the extra copy?
        # volume.cell_data.set_array(scalars, title, True)
        self.mapper.SetScalarModeToUseCellData()
    else:
        raise_not_matching(scalars, volume)

    # Set scalars range
    if clim is None:
        clim = [np.nanmin(scalars), np.nanmax(scalars)]
    elif isinstance(clim, float) or isinstance(clim, int):
        clim = [-clim, clim]

    # NOTE: AGH, 16-SEP-2021; Why this comment block
    ###############

    # NOTE: AGH, 16-SEP-2021; Expensive and inneffecient code. Replace with below
    # scalars = scalars.astype(np.float_)
    # with np.errstate(invalid="ignore"):
    #     idxs0 = scalars < clim[0]
    #     idxs1 = scalars > clim[1]
    # scalars[idxs0] = clim[0]
    # scalars[idxs1] = clim[1]
    # scalars = (
    #     (scalars - np.nanmin(scalars)) / (np.nanmax(scalars) - np.nanmin(scalars))
    # ) * 255
    # # scalars = scalars.astype(np.uint8)
    # volume[title] = scalars
    
    if rescale_scalars:
        clim = np.asarray(clim, dtype=scalars.dtype)
        
        scalars.clip(clim[0], clim[1], out=scalars)

        min_ = np.nanmin(scalars)
        max_ = np.nanmax(scalars)

        np.true_divide((scalars - min_), (max_ - min_) / 255, out=scalars, casting="unsafe")

        volume[title] = np.array(scalars, dtype=np.uint8)

        self.mapper.scalar_range = clim

    # Set colormap and build lookup table
    table = _vtk.vtkLookupTable()
    # table.SetNanColor(nan_color) # NaN's are chopped out with current implementation
    # above/below colors not supported with volume rendering

    if isinstance(annotations, dict):
        for val, anno in annotations.items():
            table.SetAnnotation(float(val), str(anno))

    if cmap is None:  # Set default map if matplotlib is available
        if _has_matplotlib():
            cmap = self._theme.cmap

    if cmap is not None:
        if not _has_matplotlib():
            raise ImportError("Please install matplotlib for volume rendering.")

        cmap = get_cmap_safe(cmap)
        if categories:
            if categories is True:
                n_colors = len(np.unique(scalars))
            elif isinstance(categories, int):
                n_colors = categories
    if flip_scalars:
        cmap = cmap.reversed()

    color_tf = _vtk.vtkColorTransferFunction()
    for ii in range(n_colors):
        color_tf.AddRGBPoint(ii, *cmap(ii)[:-1])

    # Set opacities
    if isinstance(opacity, (float, int)):
        opacity_values = [opacity] * n_colors
    elif isinstance(opacity, str):
        opacity_values = pyvista.opacity_transfer_function(opacity, n_colors)
    elif isinstance(opacity, (np.ndarray, list, tuple)):
        opacity = np.array(opacity)
        opacity_values = opacity_transfer_function(opacity, n_colors)

    opacity_tf = _vtk.vtkPiecewiseFunction()
    for ii in range(n_colors):
        opacity_tf.AddPoint(ii, opacity_values[ii] / n_colors)

    # Now put color tf and opacity tf into a lookup table for the scalar bar
    table.SetNumberOfTableValues(n_colors)
    lut = cmap(np.array(range(n_colors))) * 255
    lut[:, 3] = opacity_values
    lut = lut.astype(np.uint8)
    table.SetTable(_vtk.numpy_to_vtk(lut))
    table.SetRange(*clim)
    self.mapper.lookup_table = table

    self.mapper.SetInputData(volume)

    blending = blending.lower()
    if blending in ["additive", "add", "sum"]:
        self.mapper.SetBlendModeToAdditive()
    elif blending in ["average", "avg", "average_intensity"]:
        self.mapper.SetBlendModeToAverageIntensity()
    elif blending in ["composite", "comp"]:
        self.mapper.SetBlendModeToComposite()
    elif blending in ["maximum", "max", "maximum_intensity"]:
        self.mapper.SetBlendModeToMaximumIntensity()
    elif blending in ["minimum", "min", "minimum_intensity"]:
        self.mapper.SetBlendModeToMinimumIntensity()
    else:
        raise ValueError(
            f"Blending mode '{blending}' invalid. "
            + "Please choose one "
            + "of 'additive', "
            "'composite', 'minimum' or " + "'maximum'."
        )
    self.mapper.Update()

    self.volume = _vtk.vtkVolume()
    self.volume.SetMapper(self.mapper)

    prop = _vtk.vtkVolumeProperty()
    prop.SetColor(color_tf)
    prop.SetScalarOpacity(opacity_tf)
    prop.SetAmbient(ambient)
    prop.SetScalarOpacityUnitDistance(opacity_unit_distance)
    prop.SetShade(shade)
    prop.SetDiffuse(diffuse)
    prop.SetSpecular(specular)
    prop.SetSpecularPower(specular_power)
    self.volume.SetProperty(prop)

    actor, prop = self.add_actor(
        self.volume,
        reset_camera=reset_camera,
        name=name,
        culling=culling,
        pickable=pickable,
        render=render,
    )

    # Add scalar bar if scalars are available
    if show_scalar_bar and scalars is not None:
        self.add_scalar_bar(**scalar_bar_args)

    self.renderer.Modified()

    return actor

With these changes, loading is as fast as ParaView and is not garbled. I recommend the following updates to add_volume:

1. Add support for reading folders containing image stacks to `pyvista.read()`

2. Give users the option to choose whether to have `add_volume` clip and rescale scalar data for them. If they set it to `False`, they will need to ensure they clipped and rescaled their data. Furthermore, update the segment of the code that performs this rescaling to a memory and speed efficient version as I have shown.

3. Give the users the option to copy cell data to point data.

Below are the time and memory profiles for load_data() and add_volume(). I'll add the answer to my SO Post as well:

Time Profiles

Load Data

Load Data Profile
=================
         232 function calls in 60.670 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1   12.473   12.473   60.670   60.670 c:\Users\hendra11\Code\Artisse\emmy\emmy\main5.py:101(load_data)
        1   31.892   31.892   31.892   31.892 {method 'Update' of 'vtkmodules.vtkCommonExecutionModel.vtkAlgorithm' objects}
        8    0.000    0.000    5.231    0.654 {built-in method numpy.core._multiarray_umath.implement_array_function}       
        4    5.231    1.308    5.231    1.308 {method 'reduce' of 'numpy.ufunc' objects}
        1    0.000    0.000    4.137    4.137 {method 'clip' of 'numpy.ndarray' objects}
        1    0.000    0.000    4.137    4.137 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\numpy\core\_methods.py:125(_clip)
        1    4.137    4.137    4.137    4.137 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\numpy\core\_methods.py:106(_clip_dep_invoke_with_casting)
        1    0.000    0.000    3.477    3.477 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\utilities\helpers.py:797(wrap)
        1    0.000    0.000    3.477    3.477 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\core\grid.py:291(__init__)
        1    0.000    0.000    3.477    3.477 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\core\dataobject.py:53(deep_copy)
        1    3.476    3.476    3.476    3.476 {method 'DeepCopy' of 'vtkmodules.vtkCommonDataModel.vtkImageData' objects}
        1    3.417    3.417    3.417    3.417 {built-in method numpy.array}
        1    0.000    0.000    3.121    3.121 <__array_function__ internals>:2(nanmax)
        1    0.000    0.000    3.121    3.121 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\numpy\lib\nanfunctions.py:343(nanmax)
        1    0.000    0.000    2.111    2.111 <__array_function__ internals>:2(nanmin)
        1    0.000    0.000    2.110    2.110 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\numpy\lib\nanfunctions.py:228(nanmin)
        1    0.000    0.000    0.043    0.043 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\core\dataset.py:1637(__setitem__)
        1    0.000    0.000    0.042    0.042 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\core\datasetattributes.py:212(__setitem__)
        1    0.000    0.000    0.042    0.042 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\core\datasetattributes.py:539(set_array)
        4    0.000    0.000    0.042    0.011 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\utilities\helpers.py:132(convert_array)
        1    0.000    0.000    0.042    0.042 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\core\datasetattributes.py:730(_prepare_array)
        1    0.000    0.000    0.042    0.042 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\vtkmodules\util\numpy_support.py:104(numpy_to_vtk)
        1    0.035    0.035    0.035    0.035 {method 'SetVoidArray' of 'vtkmodules.vtkCommonCore.vtkAbstractArray' objects}
        1    0.006    0.006    0.006    0.006 {method 'SetNumberOfTuples' of 'vtkmodules.vtkCommonCore.vtkAbstractArray' objects}
        1    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\core\dataset.py:1622(__getitem__)
        1    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\core\dataset.py:1520(get_array)
        1    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\utilities\helpers.py:319(get_array)
        1    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\utilities\helpers.py:201(point_array)
        1    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\vtkmodules\util\numpy_support.py:200(vtk_to_numpy)
        3    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\vtkmodules\util\numpy_support.py:72(get_vtk_to_numpy_typemap)
        2    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\core\pyvista_ndarray.py:14(__new__)
       25    0.000    0.000    0.000    0.000 {built-in method builtins.isinstance}
        4    0.000    0.000    0.000    0.000 C:\Program Files\Python38\lib\abc.py:96(__instancecheck__)
        4    0.000    0.000    0.000    0.000 {built-in method _abc._abc_instancecheck}
        2    0.000    0.000    0.000    0.000 {method 'view' of 'numpy.ndarray' objects}
        2    0.000    0.000    0.000    0.000 {method 'any' of 'numpy.generic' objects}
        1    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\core\grid.py:22(__init__)
        2    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\core\pyvista_ndarray.py:34(__array_finalize__)
        2    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\numpy\core\_methods.py:91(_clip_dep_is_scalar_nan)
        3    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\numpy\__init__.py:276(__getattr__)
        2    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\vtkmodules\util\numpy_support.py:92(get_numpy_array_type)
        1    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\core\datasetattributes.py:236(__contains__)
        1    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\core\dataset.py:91(__init__)
        3    0.000    0.000    0.000    0.000 C:\Program Files\Python38\lib\abc.py:100(__subclasscheck__)
        1    0.000    0.000    0.000    0.000 <__array_function__ internals>:2(ravel)
        1    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\core\dataset.py:1199(point_data)
        3    0.000    0.000    0.000    0.000 {built-in method _abc._abc_subclasscheck}
        1    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\core\datasetattributes.py:926(keys)
        3    0.000    0.000    0.000    0.000 {built-in method _warnings.warn}
        3    0.000    0.000    0.000    0.000 <__array_function__ internals>:2(ndim)
        2    0.000    0.000    0.000    0.000 {method 'GetPointData' of 'vtkmodules.vtkCommonDataModel.vtkDataSet' objects}
        1    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\vtkmodules\util\numpy_support.py:97(create_vtk_array)
        2    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\numpy\core\_methods.py:54(_any)
        2    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\vtkmodules\numpy_interface\dataset_adapter.py:275(__array_finalize__)
        1    0.000    0.000    0.000    0.000 {method 'GetOutputDataObject' of 'vtkmodules.vtkCommonExecutionModel.vtkAlgorithm' objects}
        2    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\numpy\core\numerictypes.py:358(issubdtype)
        1    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\core\grid.py:415(spacing)
        3    0.000    0.000    0.000    0.000 C:\Program Files\Python38\lib\_collections_abc.py:252(__subclasshook__)
        1    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\core\dataset.py:1378(n_points)
        4    0.000    0.000    0.000    0.000 {method 'GetAbstractArray' of 'vtkmodules.vtkCommonDataModel.vtkFieldData' objects}
        1    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\utilities\helpers.py:222(field_array)
        2    0.000    0.000    0.000    0.000 <__array_function__ internals>:2(shares_memory)
        1    0.000    0.000    0.000    0.000 {method 'AddArray' of 'vtkmodules.vtkCommonDataModel.vtkFieldData' objects}
        1    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\numpy\core\fromnumeric.py:1718(ravel)
        1    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\core\datasetattributes.py:125(__init__)
        1    0.000    0.000    0.000    0.000 {method 'SetSpacing' of 'vtkmodules.vtkCommonDataModel.vtkImageData' objects}
        1    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\utilities\helpers.py:243(cell_array)
        3    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\numpy\core\fromnumeric.py:3127(ndim)
        1    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\vtkmodules\util\numpy_support.py:49(get_vtk_array_type)
        2    0.000    0.000    0.000    0.000 {method 'Modified' of 'vtkmodules.vtkCommonCore.vtkObject' objects}
        4    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\numpy\core\numerictypes.py:284(issubclass_)
        2    0.000    0.000    0.000    0.000 {method 'GetNumberOfPoints' of 'vtkmodules.vtkCommonDataModel.vtkImageData' objects}
        5    0.000    0.000    0.000    0.000 {built-in method numpy.asarray}
        1    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\vtkmodules\numpy_interface\dataset_adapter.py:128(__getattr__)
        1    0.000    0.000    0.000    0.000 {built-in method numpy.frombuffer}
        1    0.000    0.000    0.000    0.000 {method 'GetFieldData' of 'vtkmodules.vtkCommonDataModel.vtkDataObject' objects}
        3    0.000    0.000    0.000    0.000 C:\Program Files\Python38\lib\_collections_abc.py:72(_check_methods)
        1    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\utilities\helpers.py:285(parse_field_choice)
        1    0.000    0.000    0.000    0.000 {method 'ravel' of 'numpy.ndarray' objects}
        1    0.000    0.000    0.000    0.000 {method 'GetName' of 'vtkmodules.vtkCommonCore.vtkAbstractArray' objects}
        1    0.000    0.000    0.000    0.000 {method 'GetDataType' of 'vtkmodules.vtkCommonCore.vtkUnsignedShortArray' objects}
        1    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\core\dataobject.py:31(__init__)
        4    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\vtkmodules\numpy_interface\dataset_adapter.py:152(_make_tensor_array_contiguous)
        1    0.000    0.000    0.000    0.000 {method 'GetNumberOfTuples' of 'vtkmodules.vtkCommonCore.vtkAbstractArray' objects}
        1    0.000    0.000    0.000    0.000 {method 'SetDirectoryName' of 'vtkmodules.vtkIOImage.vtkDICOMImageReader' objects}
        2    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\numpy\core\_methods.py:101(_clip_dep_is_byte_swapped)
       13    0.000    0.000    0.000    0.000 {built-in method builtins.getattr}
        1    0.000    0.000    0.000    0.000 {method 'GetCellData' of 'vtkmodules.vtkCommonDataModel.vtkDataSet' objects}
        1    0.000    0.000    0.000    0.000 {method 'SetName' of 'vtkmodules.vtkCommonCore.vtkAbstractArray' objects}
        6    0.000    0.000    0.000    0.000 {built-in method builtins.issubclass}
        3    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\core\dataset.py:36(__init__)
        2    0.000    0.000    0.000    0.000 {buffer_shared}
        1    0.000    0.000    0.000    0.000 {built-in method builtins.hasattr}
        1    0.000    0.000    0.000    0.000 {method 'GetClassName' of 'vtkmodules.vtkCommonCore.vtkObjectBase' objects}
        3    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\numpy\core\fromnumeric.py:3123(_ndim_dispatcher)
        1    0.000    0.000    0.000    0.000 {built-in method builtins.sum}
        5    0.000    0.000    0.000    0.000 {built-in method builtins.len}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
        2    0.000    0.000    0.000    0.000 {built-in method numpy.ascontiguousarray}
        1    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\vtkmodules\numpy_interface\dataset_adapter.py:125(__init__)
        2    0.000    0.000    0.000    0.000 {method 'Set' of 'vtkmodules.vtkCommonCore.vtkWeakReference' objects}
        1    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\utilities\helpers.py:355(<listcomp>)
        1    0.000    0.000    0.000    0.000 {method 'SetNumberOfComponents' of 'vtkmodules.vtkCommonCore.vtkAbstractArray' objects}
        2    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\numpy\core\multiarray.py:1293(shares_memory)
        1    0.000    0.000    0.000    0.000 {method 'strip' of 'str' objects}
        1    0.000    0.000    0.000    0.000 {method 'GetNumberOfArrays' of 'vtkmodules.vtkCommonDataModel.vtkFieldData' objects}
        1    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\numpy\lib\nanfunctions.py:224(_nanmin_dispatcher)
        1    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\numpy\lib\nanfunctions.py:339(_nanmax_dispatcher)
        1    0.000    0.000    0.000    0.000 {method 'append' of 'list' objects}
        1    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\numpy\core\fromnumeric.py:1714(_ravel_dispatcher)
        1    0.000    0.000    0.000    0.000 {method 'GetNumberOfComponents' of 'vtkmodules.vtkCommonCore.vtkAbstractArray' objects}
        1    0.000    0.000    0.000    0.000 {method 'keys' of 'dict' objects}
        1    0.000    0.000    0.000    0.000 {method 'items' of 'dict' objects}
        1    0.000    0.000    0.000    0.000 {method 'lower' of 'str' objects}
        1    0.000    0.000    0.000    0.000 {built-in method numpy.asanyarray}

Add Volume

Add Volume Profile
=================
         6719 function calls (6534 primitive calls) in 5.403 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.002    0.002    5.403    5.403 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\plotting\plotting.py:2414(add_volume)
    69/18    0.000    0.000    5.352    0.297 {built-in method numpy.core._multiarray_umath.implement_array_function}
       23    5.351    0.233    5.351    0.233 {method 'reduce' of 'numpy.ufunc' objects}
        1    0.000    0.000    3.038    3.038 <__array_function__ internals>:2(nanmax)
        1    0.000    0.000    3.038    3.038 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\numpy\lib\nanfunctions.py:343(nanmax)
        1    0.000    0.000    2.313    2.313 <__array_function__ internals>:2(nanmin)
        1    0.000    0.000    2.313    2.313 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\numpy\lib\nanfunctions.py:228(nanmin)
        1    0.037    0.037    0.037    0.037 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\plotting\mapper.py:4(make_mapper)
      257    0.003    0.000    0.005    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\matplotlib\colors.py:588(__call__)
        1    0.000    0.000    0.003    0.003 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\plotting\colors.py:397(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 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\plotting\plotting.py:3006(add_scalar_bar)
        1    0.001    0.001    0.002    0.002 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\plotting\scalar_bars.py:128(add_scalar_bar)
        1    0.000    0.000    0.001    0.001 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\plotting\tools.py:348(opacity_transfer_function)
       22    0.000    0.000    0.001    0.000 <frozen importlib._bootstrap_external>:135(_path_stat)
       22    0.001    0.000    0.001    0.000 {built-in method nt.stat}
      110    0.001    0.000    0.001    0.000 <frozen importlib._bootstrap_external>:91(_path_join)
       12    0.000    0.000    0.001    0.000 <__array_function__ internals>:2(linspace)
       12    0.000    0.000    0.001    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\numpy\core\function_base.py:23(linspace)
        2    0.000    0.000    0.001    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\plotting\plotting.py:908(add_actor)
        2    0.000    0.000    0.001    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\plotting\renderer.py:448(add_actor)
        1    0.000    0.000    0.001    0.001 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\matplotlib\colors.py:1052(_init)
        1    0.000    0.000    0.001    0.001 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\matplotlib\colors.py:302(to_rgba_array)
      328    0.000    0.000    0.001    0.000 {built-in method builtins.isinstance}
      257    0.000    0.000    0.001    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\numpy\ma\core.py:6566(is_masked)
        2    0.000    0.000    0.001    0.000 <__array_function__ internals>:2(geomspace)
        2    0.000    0.000    0.000    0.000 C:\Program Files\Python38\lib\abc.py:96(__instancecheck__)
        2    0.000    0.000    0.000    0.000 {built-in method _abc._abc_instancecheck}
        2    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\numpy\core\function_base.py:286(geomspace)
     69/2    0.000    0.000    0.000    0.000 C:\Program Files\Python38\lib\abc.py:100(__subclasscheck__)
     69/2    0.000    0.000    0.000    0.000 {built-in method _abc._abc_subclasscheck}
        2    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\plotting\renderer.py:1694(remove_actor)
       18    0.000    0.000    0.000    0.000 <__array_function__ internals>:2(any)
      260    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\numpy\lib\function_base.py:244(iterable)
      256    0.000    0.000    0.000    0.000 {method 'AddRGBPoint' of 'vtkmodules.vtkRenderingCore.vtkColorTransferFunction' objects}
      256    0.000    0.000    0.000    0.000 {method 'AddPoint' of 'vtkmodules.vtkCommonDataModel.vtkPiecewiseFunction' objects}
      257    0.000    0.000    0.000    0.000 {method 'copy' of 'numpy.ndarray' objects}
      257    0.000    0.000    0.000    0.000 {method 'take' of 'numpy.ndarray' objects}
       18    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\numpy\core\fromnumeric.py:2268(any)
        1    0.000    0.000    0.000    0.000 <__array_function__ internals>:2(column_stack)
        2    0.000    0.000    0.000    0.000 <__array_function__ internals>:2(logspace)
       18    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\numpy\core\fromnumeric.py:69(_wrapreduction)
        1    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\numpy\lib\shape_base.py:612(column_stack)
       20    0.000    0.000    0.000    0.000 {method 'any' of 'numpy.generic' objects}
        2    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\numpy\core\function_base.py:183(logspace)
       32    0.000    0.000    0.000    0.000 {built-in method numpy.asanyarray}
        3    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\plotting\tools.py:490(parse_font_family)
        1    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\matplotlib\colors.py:377(<setcomp>)
      268    0.000    0.000    0.000    0.000 {built-in method numpy.array}
        1    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\utilities\helpers.py:319(get_array)
      257    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\numpy\ma\core.py:1357(getmask)
        1    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\vtkmodules\util\numpy_support.py:104(numpy_to_vtk)
       17    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\numpy\core\numerictypes.py:358(issubdtype)
        9    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\plotting\tools.py:399(<lambda>)
      110    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap_external>:114(<listcomp>)
        1    0.000    0.000    0.000    0.000 <__array_function__ internals>:2(mean)
        3    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\plotting\tools.py:493(<listcomp>)
        1    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\utilities\helpers.py:201(point_array)
      260    0.000    0.000    0.000    0.000 {built-in method builtins.iter}
       20    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\numpy\core\_methods.py:54(_any)
      260    0.000    0.000    0.000    0.000 {built-in method numpy.empty}
        1    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\plotting\renderer.py:1880(reset_camera)
        1    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\numpy\core\fromnumeric.py:3322(mean)
        3    0.000    0.000    0.000    0.000 c:\Users\hendra11\Code\Artisse\emmy\.venv\lib\site-packages\pyvista\utilities\helpers.py:132(convert_array)
       14    0.000    0.000    0.000    0.000 <__array_function__ internals>:2(result_type)

Memory Profiles

Load Data

Filename: c:\gui\main.py

Line #    Mem usage    Increment  Occurences   Line Contents
============================================================
    33    115.2 MiB    115.2 MiB           1   @memory_profile
    34                                         def load_data(folder):
    35    115.3 MiB      0.1 MiB           1       reader = vtkDICOMImageReader()
    36    115.4 MiB      0.0 MiB           1       reader.SetDirectoryName(folder)
    37   7108.8 MiB   6993.4 MiB           1       reader.Update()
    38
    39  14094.5 MiB   6985.7 MiB           1       volume = pv.wrap(reader.GetOutputDataObject(0))
    40
    41   7102.9 MiB  -6991.6 MiB           1       del reader  # Why keep double memory?
    42
    43   7102.9 MiB      0.0 MiB           1       clim_16bit = [23000, 65481]  # Original 16-bit values
    44   7102.9 MiB      0.0 MiB           1       clim_8bit = [
    45   7102.9 MiB      0.0 MiB           1           int(clim_16bit[0] // 256),
    46   7102.9 MiB      0.0 MiB           1           int(clim_16bit[1] // 256),
    47                                             ]  # Scaled 8-bit values
    48
    49   7102.9 MiB      0.0 MiB           1       scalars = volume["DICOMImage"]
    50   7102.9 MiB      0.0 MiB           1       scalars.clip(clim_16bit[0], clim_16bit[1], out=scalars)
    51
    52   7103.0 MiB      0.1 MiB           1       min_ = np.nanmin(scalars)
    53   7103.0 MiB      0.0 MiB           1       max_ = np.nanmax(scalars)
    54
    55   7103.1 MiB      0.0 MiB           1       np.true_divide((scalars - min_), (max_ - min_) / 255, out=scalars, casting="unsafe")
    56
    57  10596.0 MiB   3492.9 MiB           1       volume["DICOMImage"] = np.array(scalars, dtype=np.uint8)
    58
    59  10596.0 MiB      0.0 MiB           1       volume.spacing = (1, 1, 1)
    60
    61  10596.0 MiB      0.0 MiB           1       return volume

Add Volume

Add Volume Profile
=================
Filename: c:\gui\.venv\lib\site-packages\pyvista\plotting\plotting.py

Line #    Mem usage    Increment  Occurences   Line Contents
============================================================
  2415   3611.9 MiB   3611.9 MiB           1       @memory_profile
  2416                                             def add_volume(
  2417                                                 self,
  2418                                                 volume,
  2419                                                 scalars=None,
  2420                                                 clim=None,
  2421                                                 resolution=None,
  2422                                                 opacity="linear",
  2423                                                 n_colors=256,
  2424                                                 cmap=None,
  2425                                                 flip_scalars=False,
  2426                                                 reset_camera=None,
  2427                                                 name=None,
  2428                                                 ambient=0.0,
  2429                                                 categories=False,
  2430                                                 culling=False,
  2431                                                 multi_colors=False,
  2432                                                 blending="composite",
  2433                                                 mapper=None,
  2434                                                 scalar_bar_args=None,
  2435                                                 show_scalar_bar=None,
  2436                                                 annotations=None,
  2437                                                 pickable=True,
  2438                                                 preference="point",
  2439                                                 opacity_unit_distance=None,
  2440                                                 shade=False,
  2441                                                 diffuse=0.7,
  2442                                                 specular=0.2,
  2443                                                 specular_power=10.0,
  2444                                                 render=True,
  2445                                                 rescale_scalars=True,
  2446                                                 copy_cell_to_point_data=True,
  2447                                                 **kwargs,
  2448                                             ):
  2449                                                 """Add a volume, rendered using a smart mapper by default.
  2450                                                 Requires a 3D :class:`numpy.ndarray` or :class:`pyvista.UniformGrid`.
  2451                                                 Parameters
  2452                                                 ----------
  2453                                                 volume : 3D numpy.ndarray or pyvista.UniformGrid
  2454                                                     The input volume to visualize. 3D numpy arrays are accepted.
  2455                                                 scalars : str or numpy.ndarray, optional
  2456                                                     Scalars used to "color" the mesh.  Accepts a string name of an
  2457                                                     array that is present on the mesh or an array equal
  2458                                                     to the number of cells or the number of points in the
  2459                                                     mesh.  Array should be sized as a single vector. If ``scalars`` is
  2460                                                     ``None``, then the active scalars are used.
  2461                                                 clim : 2 item list, optional
  2462                                                     Color bar range for scalars.  Defaults to minimum and
  2463                                                     maximum of scalars array.  Example: ``[-1, 2]``. ``rng``
  2464                                                     is also an accepted alias for this.
  2465                                                 resolution : list, optional
  2466                                                     Block resolution.
  2467                                                 opacity : str or numpy.ndarray, optional
  2468                                                     Opacity mapping for the scalars array.
  2469                                                     A string can also be specified to map the scalars range to a
  2470                                                     predefined opacity transfer function (options include: 'linear',
  2471                                                     'linear_r', 'geom', 'geom_r'). Or you can pass a custom made
  2472                                                     transfer function that is an array either ``n_colors`` in length or
  2473                                                     shorter.
  2474                                                 n_colors : int, optional
  2475                                                     Number of colors to use when displaying scalars. Defaults to 256.
  2476                                                     The scalar bar will also have this many colors.
  2477                                                 cmap : str, optional
  2478                                                     Name of the Matplotlib colormap to us when mapping the ``scalars``.
  2479                                                     See available Matplotlib colormaps.  Only applicable for when
  2480                                                     displaying ``scalars``. Requires Matplotlib to be installed.
  2481                                                     ``colormap`` is also an accepted alias for this. If ``colorcet`` or
  2482                                                     ``cmocean`` are installed, their colormaps can be specified by name.
  2483                                                 flip_scalars : bool, optional
  2484                                                     Flip direction of cmap. Most colormaps allow ``*_r`` suffix to do
  2485                                                     this as well.
  2486                                                 reset_camera : bool, optional
  2487                                                     Reset the camera after adding this mesh to the scene.
  2488                                                 name : str, optional
  2489                                                     The name for the added actor so that it can be easily
  2490                                                     updated.  If an actor of this name already exists in the
  2491                                                     rendering window, it will be replaced by the new actor.
  2492                                                 ambient : float, optional
  2493                                                     When lighting is enabled, this is the amount of light from
  2494                                                     0 to 1 that reaches the actor when not directed at the
  2495                                                     light source emitted from the viewer.  Default 0.0.
  2496                                                 categories : bool, optional
  2497                                                     If set to ``True``, then the number of unique values in the scalar
  2498                                                     array will be used as the ``n_colors`` argument.
  2499                                                 culling : str, optional
  2500                                                     Does not render faces that are culled. Options are ``'front'`` or
  2501                                                     ``'back'``. This can be helpful for dense surface meshes,
  2502                                                     especially when edges are visible, but can cause flat
  2503                                                     meshes to be partially displayed.  Defaults ``False``.
  2504                                                 multi_colors : bool, optional
  2505                                                     Whether or not to use multiple colors when plotting MultiBlock
  2506                                                     object. Blocks will be colored sequentially as 'Reds', 'Greens',
  2507                                                     'Blues', and 'Grays'.
  2508                                                 blending : str, optional
  2509                                                     Blending mode for visualisation of the input object(s). Can be
  2510                                                     one of 'additive', 'maximum', 'minimum', 'composite', or
  2511                                                     'average'. Defaults to 'additive'.
  2512                                                 mapper : str, optional
  2513                                                     Volume mapper to use given by name. Options include:
  2514                                                     ``'fixed_point'``, ``'gpu'``, ``'open_gl'``, and
  2515                                                     ``'smart'``.  If ``None`` the ``"volume_mapper"`` in the
  2516                                                     ``self._theme`` is used.
  2517                                                 scalar_bar_args : dict, optional
  2518                                                     Dictionary of keyword arguments to pass when adding the
  2519                                                     scalar bar to the scene. For options, see
  2520                                                     :func:`pyvista.BasePlotter.add_scalar_bar`.
  2521                                                 show_scalar_bar : bool
  2522                                                     If ``False``, a scalar bar will not be added to the
  2523                                                     scene. Defaults to ``True``.
  2524                                                 annotations : dict, optional
  2525                                                     Pass a dictionary of annotations. Keys are the float
  2526                                                     values in the scalars range to annotate on the scalar bar
  2527                                                     and the values are the the string annotations.
  2528                                                 pickable : bool, optional
  2529                                                     Set whether this mesh is pickable.
  2530                                                 preference : str, optional
  2531                                                     When ``mesh.n_points == mesh.n_cells`` and setting
  2532                                                     scalars, this parameter sets how the scalars will be
  2533                                                     mapped to the mesh.  Default ``'points'``, causes the
  2534                                                     scalars will be associated with the mesh points.  Can be
  2535                                                     either ``'points'`` or ``'cells'``.
  2536                                                 opacity_unit_distance : float
  2537                                                     Set/Get the unit distance on which the scalar opacity
  2538                                                     transfer function is defined. Meaning that over that
  2539                                                     distance, a given opacity (from the transfer function) is
  2540                                                     accumulated. This is adjusted for the actual sampling
  2541                                                     distance during rendering. By default, this is the length
  2542                                                     of the diagonal of the bounding box of the volume divided
  2543                                                     by the dimensions.
  2544                                                 shade : bool
  2545                                                     Default off. If shading is turned on, the mapper may
  2546                                                     perform shading calculations - in some cases shading does
  2547                                                     not apply (for example, in a maximum intensity projection)
  2548                                                     and therefore shading will not be performed even if this
  2549                                                     flag is on.
  2550                                                 diffuse : float, optional
  2551                                                     The diffuse lighting coefficient. Default ``1.0``.
  2552                                                 specular : float, optional
  2553                                                     The specular lighting coefficient. Default ``0.0``.
  2554                                                 specular_power : float, optional
  2555                                                     The specular power. Between ``0.0`` and ``128.0``.
  2556                                                 render : bool, optional
  2557                                                     Force a render when True.  Default ``True``.
  2558                                                 rescale_scalars : bool, optional
  2559                                                     Rescale scalar data. This is an expensive memory and time
  2560                                                     operation, especially for large data. In that case, it is
  2561                                                     best to set this to ``False``, clip and scale scalar data
  2562                                                     of ``volume`` beforehand, and pass that to ``add_volume``.
  2563                                                     Default ``True``.
  2564                                                 copy_cell_to_point_data : bool, optional
  2565                                                     Make a copy of the original ``volume``, passing cell data
  2566                                                     to point data. This is an expensive memory and time
  2567                                                     operation, especially for large data. In that case, it is
  2568                                                     best to choose ``False``. However, this copy is a current
  2569                                                     workaround to ensure original object data is not altered
  2570                                                     and volume rendering on cells exhibits some issues. Use
  2571                                                     with caution. Default ``True``.
  2572                                                 **kwargs : dict, optional
  2573                                                     Optional keyword arguments.
  2574                                                 Returns
  2575                                                 -------
  2576                                                 vtk.vtkActor
  2577                                                     VTK actor of the volume.
  2578                                                 """
  2579                                                 # Handle default arguments
  2580
  2581                                                 # Supported aliases
  2582   3611.9 MiB      0.0 MiB           1           clim = kwargs.pop("rng", clim)
  2583   3611.9 MiB      0.0 MiB           1           cmap = kwargs.pop("colormap", cmap)
  2584   3611.9 MiB      0.0 MiB           1           culling = kwargs.pop("backface_culling", culling)
  2585
  2586   3611.9 MiB      0.0 MiB           1           if "scalar" in kwargs:
  2587                                                     raise TypeError(
  2588                                                         "`scalar` is an invalid keyword argument for `add_mesh`. Perhaps you mean `scalars` with an s?"
  2589                                                     )
  2590   3611.9 MiB      0.0 MiB           1           assert_empty_kwargs(**kwargs)
  2591
  2592                                                 # Avoid mutating input
  2593   3611.9 MiB      0.0 MiB           1           if scalar_bar_args is None:
  2594   3611.9 MiB      0.0 MiB           1               scalar_bar_args = {}
  2595                                                 else:
  2596                                                     scalar_bar_args = scalar_bar_args.copy()
  2597                                                 # account for legacy behavior
  2598   3611.9 MiB      0.0 MiB           1           if "stitle" in kwargs:  # pragma: no cover
  2599                                                     warnings.warn(USE_SCALAR_BAR_ARGS, PyvistaDeprecationWarning)
  2600                                                     scalar_bar_args.setdefault("title", kwargs.pop("stitle"))
  2601
  2602   3611.9 MiB      0.0 MiB           1           if show_scalar_bar is None:
  2603   3611.9 MiB      0.0 MiB           1               show_scalar_bar = self._theme.show_scalar_bar
  2604
  2605   3611.9 MiB      0.0 MiB           1           if culling is True:
  2606                                                     culling = "backface"
  2607
  2608   3611.9 MiB      0.0 MiB           1           if mapper is None:
  2609   3611.9 MiB      0.0 MiB           1               mapper = self._theme.volume_mapper
  2610
  2611                                                 # only render when the plotter has already been shown
  2612   3611.9 MiB      0.0 MiB           1           if render is None:
  2613                                                     render = not self._first_time
  2614
  2615                                                 # Convert the VTK data object to a pyvista wrapped object if necessary
  2616   3611.9 MiB      0.0 MiB           1           if not is_pyvista_dataset(volume):
  2617                                                     if isinstance(volume, np.ndarray):
  2618                                                         volume = wrap(volume)
  2619                                                         if resolution is None:
  2620                                                             resolution = [1, 1, 1]
  2621                                                         elif len(resolution) != 3:
  2622                                                             raise ValueError("Invalid resolution dimensions.")
  2623                                                         volume.spacing = resolution
  2624                                                     else:
  2625                                                         volume = wrap(volume)
  2626                                                         if not is_pyvista_dataset(volume):
  2627                                                             raise TypeError(
  2628                                                                 f"Object type ({type(volume)}) not supported for plotting in PyVista."
  2629                                                             )
  2630                                                 else:
  2631   3611.9 MiB      0.0 MiB           1               if copy_cell_to_point_data:
  2632                                                         # HACK: Make a copy so the original object is not altered.
  2633                                                         #       Also, place all data on the nodes as issues arise when
  2634                                                         #       volume rendering on the cells.
  2635                                                         volume = volume.cell_data_to_point_data()
  2636
  2637   3611.9 MiB      0.0 MiB           1           if name is None:
  2638   3611.9 MiB      0.0 MiB           1               name = f"{type(volume).__name__}({volume.memory_address})"
  2639
  2640   3611.9 MiB      0.0 MiB           1           if isinstance(volume, pyvista.MultiBlock):
  2641                                                     from itertools import cycle
  2642
  2643                                                     cycler = cycle(["Reds", "Greens", "Blues", "Greys", "Oranges", "Purples"])
  2644                                                     # Now iteratively plot each element of the multiblock dataset
  2645                                                     actors = []
  2646                                                     for idx in range(volume.GetNumberOfBlocks()):
  2647                                                         if volume[idx] is None:
  2648                                                             continue
  2649                                                         # Get a good name to use
  2650                                                         next_name = f"{name}-{idx}"
  2651                                                         # Get the data object
  2652                                                         block = wrap(volume.GetBlock(idx))
  2653                                                         if resolution is None:
  2654                                                             try:
  2655                                                                 block_resolution = block.GetSpacing()
  2656                                                             except AttributeError:
  2657                                                                 block_resolution = resolution
  2658                                                         else:
  2659                                                             block_resolution = resolution
  2660                                                         if multi_colors:
  2661                                                             color = next(cycler)
  2662                                                         else:
  2663                                                             color = cmap
  2664
  2665                                                         a = self.add_volume(
  2666                                                             block,
  2667                                                             resolution=block_resolution,
  2668                                                             opacity=opacity,
  2669                                                             n_colors=n_colors,
  2670                                                             cmap=color,
  2671                                                             flip_scalars=flip_scalars,
  2672                                                             reset_camera=reset_camera,
  2673                                                             name=next_name,
  2674                                                             ambient=ambient,
  2675                                                             categories=categories,
  2676                                                             culling=culling,
  2677                                                             clim=clim,
  2678                                                             mapper=mapper,
  2679                                                             pickable=pickable,
  2680                                                             opacity_unit_distance=opacity_unit_distance,
  2681                                                             shade=shade,
  2682                                                             diffuse=diffuse,
  2683                                                             specular=specular,
  2684                                                             specular_power=specular_power,
  2685                                                             render=render,
  2686                                                         )
  2687
  2688                                                         actors.append(a)
  2689                                                     return actors
  2690
  2691   3611.9 MiB      0.0 MiB           1           if not isinstance(volume, pyvista.UniformGrid):
  2692                                                     raise TypeError(
  2693                                                         f"Type {type(volume)} not supported for volume rendering at this time. Use `pyvista.UniformGrid`."
  2694                                                     )
  2695
  2696   3611.9 MiB      0.0 MiB           1           if opacity_unit_distance is None:
  2697   3611.9 MiB      0.1 MiB           1               opacity_unit_distance = volume.length / (np.mean(volume.dimensions) - 1)
  2698
  2699   3611.9 MiB      0.0 MiB           1           if scalars is None:
  2700                                                     # Make sure scalars components are not vectors/tuples
  2701                                                     scalars = volume.active_scalars
  2702                                                     # Don't allow plotting of string arrays by default
  2703                                                     if scalars is not None and np.issubdtype(scalars.dtype, np.number):
  2704                                                         scalar_bar_args.setdefault("title", volume.active_scalars_info[1])
  2705                                                     else:
  2706                                                         raise ValueError("No scalars to use for volume rendering.")
  2707                                                 # NOTE: AGH, 16-SEP-2021; Remove this as it is unnecessary
  2708                                                 # elif isinstance(scalars, str):
  2709                                                 #     pass
  2710
  2711                                                 # NOTE: AGH, 16-SEP-2021; Why this comment block
  2712                                                 ##############
  2713
  2714   3611.9 MiB      0.0 MiB           1           title = "Data"
  2715   3611.9 MiB      0.0 MiB           1           if isinstance(scalars, str):
  2716   3611.9 MiB      0.0 MiB           1               title = scalars
  2717   3611.9 MiB      0.0 MiB           1               scalars = get_array(volume, scalars, preference=preference, err=True)
  2718   3611.9 MiB      0.0 MiB           1               scalar_bar_args.setdefault("title", title)
  2719
  2720   3611.9 MiB      0.0 MiB           1           if not isinstance(scalars, np.ndarray):
  2721                                                     scalars = np.asarray(scalars)
  2722
  2723   3611.9 MiB      0.0 MiB           1           if not np.issubdtype(scalars.dtype, np.number):
  2724                                                     raise TypeError(
  2725                                                         "Non-numeric scalars are currently not supported for volume rendering."
  2726                                                     )
  2727
  2728   3611.9 MiB      0.0 MiB           1           if scalars.ndim != 1:
  2729                                                     scalars = scalars.ravel()
  2730
  2731                                                 # NOTE: AGH, 16-SEP-2021; An expensive unnecessary memory copy. Remove this.
  2732                                                 # if scalars.dtype == np.bool_ or scalars.dtype == np.uint8:
  2733                                                 #     scalars = scalars.astype(np.float_)
  2734
  2735                                                 # Define mapper, volume, and add the correct properties
  2736   3611.9 MiB      0.0 MiB           1           mappers = {
  2737   3611.9 MiB      0.0 MiB           1               "fixed_point": _vtk.vtkFixedPointVolumeRayCastMapper,
  2738   3611.9 MiB      0.0 MiB           1               "gpu": _vtk.vtkGPUVolumeRayCastMapper,
  2739   3611.9 MiB      0.0 MiB           1               "open_gl": _vtk.vtkOpenGLGPUVolumeRayCastMapper,
  2740   3611.9 MiB      0.0 MiB           1               "smart": _vtk.vtkSmartVolumeMapper,
  2741                                                 }
  2742   3611.9 MiB      0.0 MiB           1           if not isinstance(mapper, str) or mapper not in mappers.keys():
  2743                                                     raise TypeError(
  2744                                                         f"Mapper ({mapper}) unknown. Available volume mappers include: {', '.join(mappers.keys())}"
  2745                                                     )
  2746   3612.8 MiB      0.9 MiB           1           self.mapper = make_mapper(mappers[mapper])
  2747
  2748                                                 # Scalars interpolation approach
  2749   3612.8 MiB      0.0 MiB           1           if scalars.shape[0] == volume.n_points:
  2750                                                     # NOTE: AGH, 16-SEP-2021; Why the extra copy?
  2751                                                     # volume.point_data.set_array(scalars, title, True)
  2752   3612.8 MiB      0.0 MiB           1               self.mapper.SetScalarModeToUsePointData()
  2753                                                 elif scalars.shape[0] == volume.n_cells:
  2754                                                     # NOTE: AGH, 16-SEP-2021; Why the extra copy?
  2755                                                     # volume.cell_data.set_array(scalars, title, True)
  2756                                                     self.mapper.SetScalarModeToUseCellData()
  2757                                                 else:
  2758                                                     raise_not_matching(scalars, volume)
  2759
  2760                                                 # Set scalars range
  2761   3612.8 MiB      0.0 MiB           1           if clim is None:
  2762   3612.8 MiB      0.0 MiB           1               clim = [np.nanmin(scalars), np.nanmax(scalars)]
  2763                                                 elif isinstance(clim, float) or isinstance(clim, int):
  2764                                                     clim = [-clim, clim]
  2765
  2766                                                 # NOTE: AGH, 16-SEP-2021; Why this comment block
  2767                                                 ###############
  2768
  2769                                                 # NOTE: AGH, 16-SEP-2021; Expensive and inneffecient code. Replace with below
  2770                                                 # scalars = scalars.astype(np.float_)
  2771                                                 # with np.errstate(invalid="ignore"):
  2772                                                 #     idxs0 = scalars < clim[0]
  2773                                                 #     idxs1 = scalars > clim[1]
  2774                                                 # scalars[idxs0] = clim[0]
  2775                                                 # scalars[idxs1] = clim[1]
  2776                                                 # scalars = (
  2777                                                 #     (scalars - np.nanmin(scalars)) / (np.nanmax(scalars) - np.nanmin(scalars))
  2778                                                 # ) * 255
  2779                                                 # # scalars = scalars.astype(np.uint8)
  2780                                                 # volume[title] = scalars
  2781
  2782   3612.8 MiB      0.0 MiB           1           if rescale_scalars:
  2783                                                     clim = np.asarray(clim, dtype=scalars.dtype)
  2784
  2785                                                     scalars.clip(clim[0], clim[1], out=scalars)
  2786
  2787                                                     min_ = np.nanmin(scalars)
  2788                                                     max_ = np.nanmax(scalars)
  2789
  2790                                                     np.true_divide(
  2791                                                         (scalars - min_), (max_ - min_) / 255, out=scalars, casting="unsafe"
  2792                                                     )
  2793
  2794                                                     volume[title] = np.array(scalars, dtype=np.uint8)
  2795
  2796                                                     self.mapper.scalar_range = clim
  2797
  2798                                                 # Set colormap and build lookup table
  2799   3612.9 MiB      0.0 MiB           1           table = _vtk.vtkLookupTable()
  2800                                                 # table.SetNanColor(nan_color) # NaN's are chopped out with current implementation
  2801                                                 # above/below colors not supported with volume rendering
  2802
  2803   3612.9 MiB      0.0 MiB           1           if isinstance(annotations, dict):
  2804                                                     for val, anno in annotations.items():
  2805                                                         table.SetAnnotation(float(val), str(anno))
  2806
  2807   3612.9 MiB      0.0 MiB           1           if cmap is None:  # Set default map if matplotlib is available
  2808   3619.1 MiB      6.3 MiB           1               if _has_matplotlib():
  2809   3619.1 MiB      0.0 MiB           1                   cmap = self._theme.cmap
  2810
  2811   3619.1 MiB      0.0 MiB           1           if cmap is not None:
  2812   3619.1 MiB      0.0 MiB           1               if not _has_matplotlib():
  2813                                                         raise ImportError("Please install matplotlib for volume rendering.")
  2814
  2815   3620.4 MiB      1.3 MiB           1               cmap = get_cmap_safe(cmap)
  2816   3620.4 MiB      0.0 MiB           1               if categories:
  2817                                                         if categories is True:
  2818                                                             n_colors = len(np.unique(scalars))
  2819                                                         elif isinstance(categories, int):
  2820                                                             n_colors = categories
  2821   3620.4 MiB      0.0 MiB           1           if flip_scalars:
  2822                                                     cmap = cmap.reversed()
  2823
  2824   3620.4 MiB      0.0 MiB           1           color_tf = _vtk.vtkColorTransferFunction()
  2825   3620.4 MiB      0.0 MiB         257           for ii in range(n_colors):
  2826   3620.4 MiB      0.0 MiB         256               color_tf.AddRGBPoint(ii, *cmap(ii)[:-1])
  2827
  2828                                                 # Set opacities
  2829   3620.4 MiB      0.0 MiB           1           if isinstance(opacity, (float, int)):
  2830                                                     opacity_values = [opacity] * n_colors
  2831   3620.4 MiB      0.0 MiB           1           elif isinstance(opacity, str):
  2832   3620.5 MiB      0.0 MiB           1               opacity_values = pyvista.opacity_transfer_function(opacity, n_colors)
  2833                                                 elif isinstance(opacity, (np.ndarray, list, tuple)):
  2834                                                     opacity = np.array(opacity)
  2835                                                     opacity_values = opacity_transfer_function(opacity, n_colors)
  2836
  2837   3620.5 MiB      0.0 MiB           1           opacity_tf = _vtk.vtkPiecewiseFunction()
  2838   3620.5 MiB      0.0 MiB         257           for ii in range(n_colors):
  2839   3620.5 MiB      0.0 MiB         256               opacity_tf.AddPoint(ii, opacity_values[ii] / n_colors)
  2840
  2841                                                 # Now put color tf and opacity tf into a lookup table for the scalar bar
  2842   3620.5 MiB      0.0 MiB           1           table.SetNumberOfTableValues(n_colors)
  2843   3620.6 MiB      0.1 MiB           1           lut = cmap(np.array(range(n_colors))) * 255
  2844   3620.6 MiB      0.0 MiB           1           lut[:, 3] = opacity_values
  2845   3620.6 MiB      0.0 MiB           1           lut = lut.astype(np.uint8)
  2846   3620.6 MiB      0.0 MiB           1           table.SetTable(_vtk.numpy_to_vtk(lut))
  2847   3620.6 MiB      0.0 MiB           1           table.SetRange(*clim)
  2848   3620.6 MiB      0.0 MiB           1           self.mapper.lookup_table = table
  2849
  2850   3620.7 MiB      0.0 MiB           1           self.mapper.SetInputData(volume)
  2851
  2852   3620.7 MiB      0.0 MiB           1           blending = blending.lower()
  2853   3620.7 MiB      0.0 MiB           1           if blending in ["additive", "add", "sum"]:
  2854                                                     self.mapper.SetBlendModeToAdditive()
  2855   3620.7 MiB      0.0 MiB           1           elif blending in ["average", "avg", "average_intensity"]:
  2856                                                     self.mapper.SetBlendModeToAverageIntensity()
  2857   3620.7 MiB      0.0 MiB           1           elif blending in ["composite", "comp"]:
  2858   3620.7 MiB      0.0 MiB           1               self.mapper.SetBlendModeToComposite()
  2859                                                 elif blending in ["maximum", "max", "maximum_intensity"]:
  2860                                                     self.mapper.SetBlendModeToMaximumIntensity()
  2861                                                 elif blending in ["minimum", "min", "minimum_intensity"]:
  2862                                                     self.mapper.SetBlendModeToMinimumIntensity()
  2863                                                 else:
  2864                                                     raise ValueError(
  2865                                                         f"Blending mode '{blending}' invalid. "
  2866                                                         + "Please choose one "
  2867                                                         + "of 'additive', "
  2868                                                         "'composite', 'minimum' or " + "'maximum'."
  2869                                                     )
  2870   3620.7 MiB      0.0 MiB           1           self.mapper.Update()
  2871
  2872   3620.7 MiB      0.0 MiB           1           self.volume = _vtk.vtkVolume()
  2873   3620.7 MiB      0.0 MiB           1           self.volume.SetMapper(self.mapper)
  2874
  2875   3620.7 MiB      0.0 MiB           1           prop = _vtk.vtkVolumeProperty()
  2876   3620.7 MiB      0.0 MiB           1           prop.SetColor(color_tf)
  2877   3620.7 MiB      0.0 MiB           1           prop.SetScalarOpacity(opacity_tf)
  2878   3620.7 MiB      0.0 MiB           1           prop.SetAmbient(ambient)
  2879   3620.7 MiB      0.0 MiB           1           prop.SetScalarOpacityUnitDistance(opacity_unit_distance)
  2880   3620.7 MiB      0.0 MiB           1           prop.SetShade(shade)
  2881   3620.7 MiB      0.0 MiB           1           prop.SetDiffuse(diffuse)
  2882   3620.7 MiB      0.0 MiB           1           prop.SetSpecular(specular)
  2883   3620.7 MiB      0.0 MiB           1           prop.SetSpecularPower(specular_power)
  2884   3620.7 MiB      0.0 MiB           1           self.volume.SetProperty(prop)
  2885
  2886   3620.8 MiB      0.0 MiB           2           actor, prop = self.add_actor(
  2887   3620.7 MiB      0.0 MiB           1               self.volume,
  2888   3620.7 MiB      0.0 MiB           1               reset_camera=reset_camera,
  2889   3620.7 MiB      0.0 MiB           1               name=name,
  2890   3620.7 MiB      0.0 MiB           1               culling=culling,
  2891   3620.7 MiB      0.0 MiB           1               pickable=pickable,
  2892   3620.7 MiB      0.0 MiB           1               render=render,
  2893                                                 )
  2894
  2895                                                 # Add scalar bar if scalars are available
  2896   3620.8 MiB      0.0 MiB           1           if show_scalar_bar and scalars is not None:
  2897   3621.3 MiB      0.6 MiB           1               self.add_scalar_bar(**scalar_bar_args)
  2898
  2899   3621.3 MiB      0.0 MiB           1           self.renderer.Modified()
  2900
  2901   3621.3 MiB      0.0 MiB           1           return actor

@adam-grant-hendry
Copy link
Author

@akaszynski @adeak @banesullivan @MatthewFlamm

Can one of you help me turn this into a PR? Closing the issue as the problem as been solved.

@akaszynski
Copy link
Member

akaszynski commented Sep 16, 2021

Repoening so we can link this as an open issue.

Yes, @adamgranthendry, I can help turn this into a PR, just busy with the day job. I'll need your help creating a unit test that validates that this approach is working correctly that isn't a huge memory hog (ideally completes in a few hundred milliseconds. Feel free to post here.

@akaszynski akaszynski reopened this Sep 16, 2021
@MatthewFlamm
Copy link

MatthewFlamm commented Sep 16, 2021

Let's piece this apart into separate PR's when possible, especially if you need help in getting going. I think there are low hanging fruits and potentially tricky ones. The reading in of DICOM data seems like a separate and easily implementable feature. This would also be required if you want to use DICOM data for the plotting too.

The steps would be:

  1. Add a DICOM dataset to pyvista/vtk-data. This is a nice to have for testing. vtk has a bunch of permissively licensed data located https://data.kitware.com/#collection/55f17f758d777f6ddc7895b7/folder/5afd93708d777f15ebe1b515.
  2. Add support for vtkDICOMImageReader to https://github.com/pyvista/pyvista/blob/main/pyvista/utilities/reader.py. It should be straightforward to follow the other readers to implement this. You can ping me for help on doing so.
  3. We should also consider adding this to the pyvista.read functionality, but I have some ideas for how this can be taken directly from the Readers definition.
  4. Add tests to https://github.com/pyvista/pyvista/blob/main/tests/utilities/test_reader.py

I've been curious why PyVista has all of this custom scalars handling both in add_volume and add_mesh. I think it is there for handling in other backends other than vtk. If that is a fair reading, I would also love to have vtk native implementations which would align with the stated goals for PyVista. Maybe this would require splitting the logic for different purposes.

@MatthewFlamm
Copy link

MatthewFlamm commented Sep 16, 2021

From a preliminary read through of your code, have you tested whether there is data in the reader object?

 reader = vtkDICOMImageReader()
 reader.SetDirectoryName(folder)
 reader.Update()
 
volume = pv.wrap(reader.GetOutputDataObject(0))
 
del reader  # Why keep double memory

In my testing, the reader object does not hold any data, just information about the files/folder you set. Maybe this reader is different? If my testing on the other readers was flawed, we should point this out in the documentation.

EDIT: I see it is in one of your profiling results. Ouch. I need to double check the other readers now.

@adam-grant-hendry
Copy link
Author

@MatthewFlamm

I'm not sure if there is "useful" data in the reader (i.e. that it is not just an empty data set), but it certainly is taking up an equal amount of memory as my original data set per my memory profiler:

Filename: c:\gui\main.py

Line #    Mem usage    Increment  Occurences   Line Contents
============================================================
    33    115.2 MiB    115.2 MiB           1   @memory_profile
    34                                         def load_data(folder):
    35    115.3 MiB      0.1 MiB           1       reader = vtkDICOMImageReader()
    36    115.4 MiB      0.0 MiB           1       reader.SetDirectoryName(folder)
    37   7108.8 MiB   6993.4 MiB           1       reader.Update()
    38
    39  14094.5 MiB   6985.7 MiB           1       volume = pv.wrap(reader.GetOutputDataObject(0))
    40
    41   7102.9 MiB  -6991.6 MiB           1       del reader  # Why keep double memory?

@adam-grant-hendry
Copy link
Author

EDIT: I see it is in one of your profiling results. Ouch. I need to double check the other readers now.

I have an inkling suspicion this is why memory blows up in add_volume(). Perhaps there are so many links to different copies of data that when we try to change one, they all change, and memory usage goes through the roof.

@adamgranthendry
Copy link

@MatthewFlamm To test volume rendering, we'll also need corresponding image stacks. A quick search led me here:

https://www.v7labs.com/blog/healthcare-datasets-for-computer-vision

Some questions:

  1. I'm not sure about licensing for these. Are all of the "open source" licenses (e.g. MIT, Apache-2.0, GPL, BSD, etc.) considered fair game, or only certain ones?

  2. Some of these datasets are a couple hundred megs or more? What is the storage limit currently for pyvista/vtk-data? Does it make sense to add some of these to pyvista/vtk-data, or should we point to their corresponding GitHub repos or create another repo for this data?

If my data set is 7 GB and takes 60 seconds, I'm hopeful we can find a publicly licensed image stack dataset for testing that only takes a few hundred milliseconds to run.

Maybe ITK or SimpleITK has had to test similar functionality and we could use or run some of their test data? Do you have any thoughts?

@MatthewFlamm
Copy link

We probably don't need any special dataset for volume rendering, there are existing datasets in vtk-data, including a DICOM dataset. But, this dataset is a single file, so I think your use case may require a different dataset for testing the reader itself? I'm not sure as I don't know much about this type of data.

@adamgranthendry
Copy link

adamgranthendry commented Sep 16, 2021

But, this dataset is a single file, so I think your use case may require a different dataset for testing the reader itself?

Yes because we need to test loading a stack of images. (Raw (volumetric) CT/micro-cT/MRI scan data are often output as multiple DICOM and/or TIFF images, which is what I'm testing: that image stack data can be loaded and processed).

The image is 3D/Volumetric, so raw data form is usually multiple images (DICOM or otherwise) as a stack, as opposed to say, a DICOM image that is a single 2D xray of a chest, arm, or other body part (just for example).

@adam-grant-hendry
Copy link
Author

adam-grant-hendry commented Nov 8, 2021

My apologies for the long hiatus on this. I have gotten started on part 1: creating a pull request to pyvista/vtk-data to add a DICOM stack dataset that can be used for testing. Please see pull request #5 on pyvista/vtk-data.

@adam-grant-hendry
Copy link
Author

adam-grant-hendry commented Nov 8, 2021

@MatthewFlamm

The steps would be:

  1. Add a DICOM dataset to pyvista/vtk-data. This is a nice to have for testing. vtk has a bunch of permissively licensed data located https://data.kitware.com/#collection/55f17f758d777f6ddc7895b7/folder/5afd93708d777f15ebe1b515.
  2. Add support for vtkDICOMImageReader to https://github.com/pyvista/pyvista/blob/main/pyvista/utilities/reader.py. It should be straightforward to follow the other readers to implement this. You can ping me for help on doing so.
  3. We should also consider adding this to the pyvista.read functionality, but I have some ideas for how this can be taken directly from the Readers definition.
  4. Add tests to https://github.com/pyvista/pyvista/blob/main/tests/utilities/test_reader.py

Regarding step 2 above, I have questions:

  1. Why is self.filename set in both __init__() and _set_filename() if filename is a non-optional argument to __init__(). Is it reset in different places internally? Can I remove it from _set_filename()?
  2. Since _vtk.vtkDICOMReader takes either a file or directory, I'm considering refactoring __init__() and adding a _set_dirname(). Let me know your thoughts:
# pyvista/utilities/reader.py

# BaseReader

    def __init__(self, path):
        """Initialize Reader by setting path."""
        self._reader = self._class_reader()
        
        if os.isfile(path):
            self.filename = path
            self._set_filename(path)
        elif os.isdir(path):
            self.dirname = path
            self._set_dirname(path)

    ...

    def _set_dirname(self, dirname):
        """Set dirname and update reader."""
        self.dirname = dirname  # Do I need to set here if already set in `__init__()`?
        self.reader.SetDirectoryName(dirname)
        self._update_information()
  1. Do you want to make filename and dirname settable/gettable properties?
    @property
    def filename(self):
        return self._filename

    @filename.setter
    def filename(self, name):
        self._filename = name

    # ..., etc. (add for dirname)
  1. Why is there _update_information() as well as _update()? All I gather from the vtk docs is that the method is for backwards compatibility. Does it do the same thing as Update() or something different?:
virtual void vtkAlgorithm::UpdateInformation()
    Backward compatibility method to invoke UpdateInformation on executive.

@MatthewFlamm
Copy link

  1. Why is self.filename set in both __init__() and _set_filename() if filename is a non-optional argument to __init__(). Is it reset in different places internally? Can I remove it from _set_filename()?

This is an oversight in implementation. I like your suggestion.

  1. Since _vtk.vtkDICOMReader takes either a file or directory, I'm considering refactoring init() and adding a _set_dirname(). Let me know your thoughts:

I like this idea, but probably only very specific readers implement this functionality. I think it shouldn't be in the base class if a small minority support directories. I would suggest that we keep it simple and just override _set_filename to implement this functionality in this reader.

  1. Do you want to make filename and dirname settable/gettable properties?

I'm not sure about this. One issue is that we'd have to check whether the file is in the right format type (possibly addressable). Sounds like a good future PR if readers can reset the file after reading one. In my testing some are not so robust outside the pipeline model, but maybe I missed a trick.

  1. Why is there _update_information() as well as _update()? All I gather from the vtk docs is that the method is for backwards compatibility. Does it do the same thing as Update() or something different?:

This was also empirical testing that some readers required this form of update. Maybe it should be the other way around and have those readers override the method used?

@adam-grant-hendry
Copy link
Author

One issue is that we'd have to check whether the file is in the right format type (possibly addressable).

I see now the comment says changing file type requires a different subclass. I agree; best left to a different PR if needed.

Maybe it should be the other way around and have those readers override the method used?

I'm actually not sure either...I'd like to try and figure out what UpdateInformation() does first (if there are significant differences between it and Update(), for example)

@adam-grant-hendry
Copy link
Author

adam-grant-hendry commented Nov 9, 2021

@MatthewFlamm

One issue I run into is the CLASS_READERS rely on a file extension to determine the class to use in get_reader(), so I cannot use it because I pass a folder to my DICOMReader. Currently, I have working code and a test that doesn't use get_reader(). Are you okay with that, or do you want me to modify get_reader() to do another check if filename is a folder that contains .dcm files?

Currently, get_reader() only works with files, whereas _vtk.vtkDICOMImageReader accepts both single .dcm files and folders of .dcm files.

@MatthewFlamm
Copy link

My perspective is that the get_reader functionality is for ease of use. Not all use cases might work there. I think what you have is probably fine.

@adam-grant-hendry
Copy link
Author

@MatthewFlamm @akaszynski

Thanks! So I can create my first PR for the DICOMReader, but I need pyvista/vtk-data PR #5 to be accepted and approved first so I can run the test on the dataset.

Would you please kindly review at your earliest convenience?

@banesullivan
Copy link
Member

banesullivan commented Nov 28, 2021

Drive by comment:

We have support for DICOM as I mention here pyvista/pyvista#1790 (comment)

@pyvista pyvista locked and limited conversation to collaborators May 15, 2022
@tkoyama010 tkoyama010 converted this issue into a discussion May 15, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants