diff --git a/docs/images/user-generated/itk_plotting_sphere.png b/docs/images/user-generated/itk_plotting_sphere.png new file mode 100644 index 0000000000..09f32491be Binary files /dev/null and b/docs/images/user-generated/itk_plotting_sphere.png differ diff --git a/docs/plotting/index.rst b/docs/plotting/index.rst index f5c2d33b9e..a55b568026 100644 --- a/docs/plotting/index.rst +++ b/docs/plotting/index.rst @@ -10,4 +10,5 @@ Plotting plotting qt_plotting + itk_plotting widgets diff --git a/docs/plotting/itk_plotting.rst b/docs/plotting/itk_plotting.rst new file mode 100644 index 0000000000..c4e0088bb4 --- /dev/null +++ b/docs/plotting/itk_plotting.rst @@ -0,0 +1,83 @@ +.. _jupyter_ref: + +PyVista Jupyter Notebook Integration +------------------------------------ + +PyVista has an interface for visualizing plots in Jupyter. The +``pyvista.PlotterITK`` class allows you interactively visualize a mesh +within a jupyter notebook. For those who prefer plotting within +jupyter, this is an great way of visualizing using ``VTK`` and +``pyvista``. + +Special thanks to thewtex +.. _itkwidgets: https://github.com/InsightSoftwareConsortium/itkwidgets + + +Installation +~~~~~~~~~~~~ +To use `PlotterITK` you'll need to install ``itkwidgets>=0.25.2``. +Follow the installation steps here: +.. _itkwidgets: https://github.com/InsightSoftwareConsortium/itkwidgets#installation + +You can install everything with `pip` if you prefer not using conda, +but be sure your juptyerlab is up-to-date. If you encounter problems, +uninstall and reinstall jupyterlab using pip. + + +Example Plotting with ITKwidgets +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The following example shows how to create a simple plot that shows a +simple sphere. + +.. code:: python + + import pyvista as pv + + # create a mesh and identify some scalars you wish to plot + mesh = pv.Sphere() + z = mesh.points[:, 2] + + # Plot using the ITKplotter + pl = pv.PlotterITK() + pl.add_mesh(mesh, scalars=z, smooth_shading=True) + pl.show(True) + + +.. figure:: ../images/user-generated/itk_plotting_sphere.png + :width: 600pt + + ITKwidgets with pyvista + + +For convenience, figures can also be plotted using the ``plot_itk`` function: + +.. code:: python + + import pyvista as pv + + # create a mesh and identify some scalars you wish to plot + mesh = pv.Sphere() + z = mesh.points[:, 2] + + # Plot using the ITKplotter + pv.plot_itk(mesh, scalars=z) + + +Additional binder examples can be found at: + +.. _itkwidgets_binder: https://hub.gke.mybinder.org/user/insightsoftware-tium-itkwidgets-p2yw6xvh/lab + +.. rubric:: Attributes + +.. autoautosummary:: pyvista.PlotterITK + :attributes: + +.. rubric:: Methods + +.. autoautosummary:: pyvista.PlotterITK + :methods: + +.. autoclass:: pyvista.PlotterITK + :members: + :undoc-members: + :show-inheritance: diff --git a/examples/02-plot/depth-peeling.py b/examples/02-plot/depth-peeling.py index 2abf626d63..ce93a6e1ca 100644 --- a/examples/02-plot/depth-peeling.py +++ b/examples/02-plot/depth-peeling.py @@ -6,7 +6,7 @@ This is enabled by default in :any:`pyvista.rcParams`. For this example, we will showcase the difference that depth peeling provides -as the justification for why we have enabled this by defualt. +as the justification for why we have enabled this by default. """ # sphinx_gallery_thumbnail_number = 2 diff --git a/pyvista/plotting/__init__.py b/pyvista/plotting/__init__.py index b313ff6169..6cb2cbeafc 100644 --- a/pyvista/plotting/__init__.py +++ b/pyvista/plotting/__init__.py @@ -12,5 +12,5 @@ from .renderer import CameraPosition, Renderer, scale_point from .plotting import BasePlotter, Plotter, close_all from .qt_plotting import BackgroundPlotter, QtInteractor, MainWindow, Counter -from .helpers import plot, plot_arrows, plot_compare_four +from .helpers import plot, plot_arrows, plot_compare_four, plot_itk from .itkplotter import PlotterITK diff --git a/pyvista/plotting/helpers.py b/pyvista/plotting/helpers.py index e93a3daf69..207ab1a5cd 100644 --- a/pyvista/plotting/helpers.py +++ b/pyvista/plotting/helpers.py @@ -209,3 +209,54 @@ def plot_compare_four(data_a, data_b, data_c, data_d, disply_kwargs=None, p.link_views() return p.show(screenshot=screenshot, **show_kwargs) + + +def plot_itk(mesh, color=None, scalars=None, opacity=1.0, + smooth_shading=False): + """Plot a PyVista/VTK mesh or dataset. + + Adds any PyVista/VTK mesh that itkwidgets can wrap to the + scene. + + Parameters + ---------- + mesh : pyvista.Common or pyvista.MultiBlock + Any PyVista or VTK mesh is supported. Also, any dataset that + :func:`pyvista.wrap` can handle including NumPy arrays of XYZ + points. + + color : string or 3 item list, optional, defaults to white + Use to make the entire mesh have a single solid color. Either + a string, RGB list, or hex color string. For example: + ``color='white'``, ``color='w'``, ``color=[1, 1, 1]``, or + ``color='#FFFFFF'``. Color will be overridden if scalars are + specified. + + 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 both ``color`` and + ``scalars`` are ``None``, then the active scalars are used. + + opacity : float, optional + Opacity of the mesh. If a single float value is given, it will + be the global opacity of the mesh and uniformly applied + everywhere - should be between 0 and 1. Default 1.0 + + smooth_shading : bool, optional + Smooth mesh surface mesh by taking into account surface + normals. Surface will appear smoother while sharp edges will + still look sharp. Default False. + + Returns + -------- + plotter : itkwidgets.Viewer + ITKwidgets viewer. + """ + pl = pyvista.PlotterITK() + if isinstance(mesh, np.ndarray): + pl.add_points(mesh, color) + else: + pl.add_mesh(mesh, color, scalars, opacity, smooth_shading) + return pl.show() diff --git a/pyvista/plotting/itkplotter.py b/pyvista/plotting/itkplotter.py index 256064e22d..2e1339ed16 100644 --- a/pyvista/plotting/itkplotter.py +++ b/pyvista/plotting/itkplotter.py @@ -1,8 +1,6 @@ -"""PyVista-like ITKwidgets plotter. - -This is super experimental: use with caution. -""" - +"""PyVista-like ITKwidgets plotter.""" +import numpy as np +from scooby import meets_version import pyvista as pv HAS_ITK = False @@ -13,32 +11,67 @@ except ImportError: pass + class PlotterITK(): - """An EXPERIMENTAL interface for plotting in Jupyter notebooks. + """ITKwidgets plotter. + + Used for plotting interactively within a jupyter notebook. + Requires ``itkwidgets>=0.25.2``. For installation see: - Use with caution, this is an experimental/demo feature. This creates an - interface for 3D rendering with ``itkwidgets`` just like the - :class:`pyvista.Plotter` class. + https://github.com/InsightSoftwareConsortium/itkwidgets#installation + + Examples + -------- + >>> import pyvista + >>> mesh = pyvista.Sphere() + >>> pl = pyvista.PlotterITK() # doctest:+SKIP + >>> pl.add_mesh(mesh, color='w') # doctest:+SKIP + >>> pl.show() # doctest:+SKIP """ def __init__(self, **kwargs): """Initialize the itkwidgets plotter.""" + itk_import_err = ImportError("Please install `itkwidgets>=0.25.2`") if not HAS_ITK: - raise ImportError("Please install `itkwidgets`.") + raise itk_import_err + + from itkwidgets import __version__ + if not meets_version(__version__, "0.25.2"): + raise itk_import_err + self._actors = [] self._point_sets = [] self._geometries = [] self._geometry_colors = [] self._geometry_opacities = [] - self._cmap = 'Viridis (matplotlib)' self._point_set_colors = [] def add_actor(self, actor): - """Append internal list of actors.""" + """Add an actor to the plotter. + + Parameters + ---------- + uinput : vtk.vtkActor + vtk actor to be added. + """ self._actors.append(actor) def add_points(self, points, color=None): - """Add XYZ points to the scene.""" + """Add points to plotting object. + + Parameters + ---------- + points : np.ndarray or pyvista.Common + n x 3 numpy array of points or pyvista dataset with points. + + color : string or 3 item list, optional. Color of points (if visible). + Either a string, rgb list, or hex color string. For example: + + color='white' + color='w' + color=[1, 1, 1] + color='#FFFFFF' + """ if pv.is_pyvista_dataset(points): point_array = points.points else: @@ -47,42 +80,105 @@ def add_points(self, points, color=None): self._point_set_colors.append(pv.parse_color(color)) self._point_sets.append(point_array) - def add_mesh(self, mesh, color=None, scalars=None, clim=None, - opacity=1.0, n_colors=256, cmap='Viridis (matplotlib)', - **kwargs): - """Add mesh to the scene.""" + def add_mesh(self, mesh, color=None, scalars=None, + opacity=1.0, smooth_shading=False): + """Add a PyVista/VTK mesh or dataset. + + Adds any PyVista/VTK mesh that itkwidgets can wrap to the + scene. + + Parameters + ---------- + mesh : pyvista.Common or pyvista.MultiBlock + Any PyVista or VTK mesh is supported. Also, any dataset + that :func:`pyvista.wrap` can handle including NumPy arrays of XYZ + points. + + color : string or 3 item list, optional, defaults to white + Use to make the entire mesh have a single solid color. + Either a string, RGB list, or hex color string. For example: + ``color='white'``, ``color='w'``, ``color=[1, 1, 1]``, or + ``color='#FFFFFF'``. Color will be overridden if scalars are + specified. + + 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 both + ``color`` and ``scalars`` are ``None``, then the active scalars are + used. + + opacity : float, optional + Opacity of the mesh. If a single float value is given, it will be + the global opacity of the mesh and uniformly applied everywhere - + should be between 0 and 1. Default 1.0 + + smooth_shading : bool, optional + Smooth mesh surface mesh by taking into account surface + normals. Surface will appear smoother while sharp edges + will still look sharp. Default False. + + """ if not pv.is_pyvista_dataset(mesh): mesh = pv.wrap(mesh) - mesh = mesh.copy() - if scalars is None and color is None: - scalars = mesh.active_scalars_name - if scalars is not None: - array = mesh[scalars].copy() - mesh.clear_arrays() + # smooth shading requires point normals to be freshly computed + if smooth_shading: + # extract surface if mesh is exterior + if not isinstance(mesh, pv.PolyData): + grid = mesh + mesh = grid.extract_surface() + ind = mesh.point_arrays['vtkOriginalPointIds'] + # remap scalars + if isinstance(scalars, np.ndarray): + scalars = scalars[ind] + + mesh.compute_normals(cell_normals=False, inplace=True) + elif 'Normals' in mesh.point_arrays: + # if 'normals' in mesh.point_arrays: + mesh.point_arrays.pop('Normals') + + # make the scalars active + if isinstance(scalars, str): + if scalars in mesh.point_arrays or scalars in mesh.cell_arrays: + array = mesh[scalars].copy() + else: + raise ValueError('Scalars ({}) not in mesh'.format(scalars)) mesh[scalars] = array mesh.active_scalars_name = scalars + elif isinstance(scalars, np.ndarray): + array = scalars + scalar_name = '_scalars' + mesh[scalar_name] = array + mesh.active_scalars_name = scalar_name elif color is not None: - mesh.clear_arrays() - + mesh.active_scalars_name = None mesh = to_geometry(mesh) self._geometries.append(mesh) self._geometry_colors.append(pv.parse_color(color)) self._geometry_opacities.append(opacity) - self._cmap = cmap - - return - def show(self, ui_collapsed=False): - """Show in cell output.""" + """Show itkwidgets plotter in cell output. + + Parameters + ---------- + ui_collapsed : bool, optional + Plot with the user interface collapsed. UI can be enabled + when plotting. Default False. + + Returns + -------- + plotter : itkwidgets.Viewer + ITKwidgets viewer. + """ plotter = Viewer(geometries=self._geometries, geometry_colors=self._geometry_colors, geometry_opacities=self._geometry_opacities, point_set_colors=self._point_set_colors, point_sets=self._point_sets, ui_collapsed=ui_collapsed, - actors=self._actors, - cmap=self._cmap) + actors=self._actors) return plotter diff --git a/pyvista/utilities/errors.py b/pyvista/utilities/errors.py index 3e59ac94d9..63bad6fd5b 100644 --- a/pyvista/utilities/errors.py +++ b/pyvista/utilities/errors.py @@ -225,7 +225,7 @@ def __init__(self, additional=None, ncol=3, text_width=80, sort=False, optional = ['matplotlib', 'PyQt5', 'IPython', 'colorcet', 'cmocean', 'panel'] - # Information about the GPU - bare except incase there is a rendering + # Information about the GPU - bare except in case there is a rendering # bug that the user is trying to report. if gpu: try: diff --git a/pyvista/utilities/fileio.py b/pyvista/utilities/fileio.py index ec0c200426..0da85945aa 100644 --- a/pyvista/utilities/fileio.py +++ b/pyvista/utilities/fileio.py @@ -152,7 +152,7 @@ def read(filename, attrs=None, file_format=None): filename : str The string path to the file to read. If a list of files is given, a :class:`pyvista.MultiBlock` dataset is returned with each file being - a seperate block in the dataset. + a separate block in the dataset. attrs : dict, optional A dictionary of attributes to call on the reader. Keys of dictionary are the attribute/method names and values are the arguments passed to those diff --git a/requirements.txt b/requirements.txt index 7aaf1bd6f3..e71414a6de 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,5 +12,6 @@ imageio>=2.5.0 imageio-ffmpeg colorcet cmocean -scooby>=0.5.0 +scooby>=0.5.1 meshio>=3.3.0 +itkwidgets>=0.25.2 diff --git a/setup.py b/setup.py index 9a4b059e16..afe778d1e0 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ install_requires = ['numpy', 'imageio', 'appdirs', - 'scooby>=0.5.0', + 'scooby>=0.5.1', 'meshio>=3.3.0', ] diff --git a/tests/test_filters.py b/tests/test_filters.py index 234cee9178..ace3dabfab 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -20,7 +20,7 @@ def test_clip_filter(): - """This tests the clip filter on all datatypes avaialble filters""" + """This tests the clip filter on all datatypes available filters""" for i, dataset in enumerate(DATASETS): clp = dataset.clip(normal=normals[i], invert=True) assert clp is not None @@ -81,7 +81,7 @@ def test_clip_surface(): def test_slice_filter(): - """This tests the slice filter on all datatypes avaialble filters""" + """This tests the slice filter on all datatypes available filters""" for i, dataset in enumerate(DATASETS): slc = dataset.slice(normal=normals[i]) assert slc is not None @@ -102,7 +102,7 @@ def test_slice_filter_composite(): def test_slice_orthogonal_filter(): - """This tests the slice filter on all datatypes avaialble filters""" + """This tests the slice filter on all datatypes available filters""" for i, dataset in enumerate(DATASETS): slices = dataset.slice_orthogonal() diff --git a/tests/test_itk_plotting.py b/tests/test_itk_plotting.py new file mode 100644 index 0000000000..c31d40397c --- /dev/null +++ b/tests/test_itk_plotting.py @@ -0,0 +1,47 @@ +import numpy as np +import pytest +import itkwidgets + +import pyvista +from pyvista.plotting import system_supports_plotting + +NO_PLOTTING = not system_supports_plotting() + +SPHERE = pyvista.Sphere() +SPHERE['z'] = SPHERE.points[:, 2] + + +@pytest.mark.skipif(NO_PLOTTING, reason="Requires system to support plotting") +def test_itk_plotting(): + viewer = pyvista.plot_itk(SPHERE) + assert isinstance(viewer, itkwidgets.Viewer) + + +@pytest.mark.skipif(NO_PLOTTING, reason="Requires system to support plotting") +def test_itk_plotting_points(): + viewer = pyvista.plot_itk(np.random.random((100, 3))) + assert isinstance(viewer, itkwidgets.Viewer) + + +@pytest.mark.skipif(NO_PLOTTING, reason="Requires system to support plotting") +def test_itk_plotting_class(): + pl = pyvista.PlotterITK() + pl.add_mesh(SPHERE, scalars='z') + viewer = pl.show() + assert isinstance(viewer, itkwidgets.Viewer) + + +@pytest.mark.skipif(NO_PLOTTING, reason="Requires system to support plotting") +def test_itk_plotting_class_no_scalars(): + pl = pyvista.PlotterITK() + pl.add_mesh(SPHERE, color='w') + viewer = pl.show() + assert isinstance(viewer, itkwidgets.Viewer) + + +@pytest.mark.skipif(NO_PLOTTING, reason="Requires system to support plotting") +def test_itk_plotting_class_npndarray_scalars(): + pl = pyvista.PlotterITK() + pl.add_mesh(SPHERE, scalars=SPHERE.points[:, 0]) + viewer = pl.show() + assert isinstance(viewer, itkwidgets.Viewer)