VTKSlicer#

Download this notebook from GitHub (right-click to download).


from functools import partial

import holoviews as hv
import numpy as np
import panel as pn
import param
import pyvista as pv

from holoviews.operation.datashader import rasterize
from bokeh.util.serialization import make_globally_unique_id
from pyvista import examples
from scipy.ndimage import zoom

css = '''
.custom-wbox > div.bk {
    padding-right: 10px;
}
.scrollable {
    overflow: auto !important;
}
'''
js_files = {'jquery': 'https://code.jquery.com/jquery-1.11.1.min.js',
            'goldenlayout': 'https://golden-layout.com/files/latest/js/goldenlayout.min.js'}
css_files = ['https://golden-layout.com/files/latest/css/goldenlayout-base.css',
             'https://golden-layout.com/files/latest/css/goldenlayout-dark-theme.css']

pn.extension('vtk', js_files=js_files, raw_css=[css], css_files=css_files)

hv.renderer('bokeh').theme = 'dark_minimal'
hv.opts.defaults(hv.opts.Image(responsive=True, tools=['hover']))
---------------------------------------------------------------------------
ImportError                               Traceback (most recent call last)
Cell In [1], line 7
      5 import panel as pn
      6 import param
----> 7 import pyvista as pv
      9 from holoviews.operation.datashader import rasterize
     10 from bokeh.util.serialization import make_globally_unique_id

File /usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/pyvista/__init__.py:12
     10 # Load default theme.  Must be loaded first.
     11 from pyvista._version import __version__
---> 12 from pyvista.plotting import *
     13 from pyvista.utilities import *
     14 from pyvista.core import *

File /usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/pyvista/plotting/__init__.py:7
      4 from .colors import (color_char_to_word, get_cmap_safe, hex_to_rgb, hexcolors,
      5                      string_to_rgb, PARAVIEW_BACKGROUND)
      6 from .export_vtkjs import export_plotter_vtkjs, get_vtkjs_url
----> 7 from .helpers import plot, plot_arrows, plot_compare_four, plot_itk
      8 from .plotting import BasePlotter, Plotter, close_all
      9 from .renderer import CameraPosition, Renderer, scale_point

File /usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/pyvista/plotting/helpers.py:7
      4 import scooby
      6 import pyvista
----> 7 from pyvista.utilities import is_pyvista_dataset
      8 from .plotting import Plotter
     11 def plot(var_item, off_screen=None, full_screen=None, screenshot=None,
     12          interactive=True, cpos=None, window_size=None,
     13          show_bounds=False, show_axes=None, notebook=None, background=None,
   (...)
     17          theme=None, hidden_line_removal=None, anti_aliasing=None,
     18          zoom=None, **kwargs):

File /usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/pyvista/utilities/__init__.py:2
      1 """Utilities routines."""
----> 2 from .errors import (GPUInfo, Observer, Report,
      3                      assert_empty_kwargs, get_gpu_info, send_errors_to_logging,
      4                      set_error_output_file, check_valid_vector, VtkErrorCatcher)
      5 from .features import *
      6 from .fileio import *

File /usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/pyvista/utilities/errors.py:12
      9 import scooby
     11 import pyvista
---> 12 from pyvista import _vtk
     14 import contextlib
     15 import collections

File /usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/pyvista/_vtk.py:42
     38 from vtkmodules.vtkRenderingLabel import (vtkPointSetToLabelHierarchy,
     39                                           vtkLabelPlacementMapper)
     40 from vtkmodules.vtkRenderingVolume import (vtkFixedPointVolumeRayCastMapper,
     41                                            vtkGPUVolumeRayCastMapper)
---> 42 from vtkmodules.vtkRenderingVolumeOpenGL2 import (vtkOpenGLGPUVolumeRayCastMapper,
     43                                                   vtkSmartVolumeMapper)
     44 from vtkmodules.vtkRenderingOpenGL2 import (vtkOpenGLHardwareSelector,
     45                                             vtkRenderStepsPass,
     46                                             vtkEDLShading,
   (...)
     51                                             vtkRenderPassCollection,
     52                                             vtkOpenGLTexture)
     53 from vtkmodules.vtkIOInfovis import vtkDelimitedTextReader

ImportError: libEGL.so.1: cannot open shared object file: No such file or directory

Declare callbacks#

class ImageSmoother(param.Parameterized):
    
    smooth_fun = param.Parameter(default=None)
    smooth_level = param.Integer(default=5, bounds=(1,10))
    order = param.Selector(default=1, objects=[1,2,3])
    
    def __init__(self, **params):
        super(ImageSmoother, self).__init__(**params)
        self._update_fun()

    @param.depends('order', 'smooth_level', watch=True)
    def _update_fun(self):
        self.smooth_fun = lambda x: zoom(x, zoom=self.smooth_level, order=self.order)

def update_camera_projection(*evts):
    volume.camera['parallelProjection'] = evts[0].new
    volume.param.trigger('camera')

def hook_reset_range(plot, elem, lbrt):
    bkplot = plot.handles['plot']
    x_range = lbrt[0], lbrt[2]
    y_range = lbrt[1], lbrt[3]
    old_x_range_reset = bkplot.x_range.reset_start, bkplot.x_range.reset_end
    old_y_range_reset = bkplot.y_range.reset_start, bkplot.y_range.reset_end  
    if x_range != old_x_range_reset or y_range != old_y_range_reset:
        bkplot.x_range.reset_start, bkplot.x_range.reset_end = x_range
        bkplot.x_range.start, bkplot.x_range.end = x_range
        bkplot.y_range.reset_start, bkplot.y_range.reset_end = y_range
        bkplot.y_range.start, bkplot.y_range.end = y_range
    
def image_slice(dims, array, lbrt, mapper, smooth_fun):
    array = np.asarray(array)
    low = mapper['low'] if mapper else array.min()
    high = mapper['high'] if mapper else array.max()
    cmap = mapper['palette'] if mapper else 'fire'
    img = hv.Image(smooth_fun(array), bounds=lbrt, kdims=dims, vdims='Intensity')
    reset_fun = partial(hook_reset_range, lbrt=lbrt)
    return img.opts(clim=(low, high), cmap=cmap, hooks=[reset_fun])

Declare Panel#

# Download datasets
head = examples.download_head()
brain = examples.download_brain()

dataset_selection = pn.widgets.Select(name='Dataset', value=head, options={'Head': head, 'Brain': brain})

volume = pn.pane.VTKVolume(
    dataset_selection.value, sizing_mode='stretch_both', height=400, 
    display_slices=True, orientation_widget=True, render_background="#222222",
    colormap='blue2cyan'
)

dataset_selection.link(target=volume, value='object')

volume_controls = volume.controls(jslink=False, parameters=[
    'render_background', 'display_volume', 'display_slices',
    'slice_i', 'slice_j', 'slice_k', 'rescale'
])

toggle_parallel_proj = pn.widgets.Toggle(name='Parallel Projection', value=False)

toggle_parallel_proj.param.watch(update_camera_projection, ['value'], onlychanged=True)

smoother = ImageSmoother()

@pn.depends(si=volume.param.slice_i, mapper=volume.param.mapper,
            smooth_fun=smoother.param.smooth_fun, vol=volume.param.object)
def image_slice_i(si, mapper, smooth_fun, vol):
    arr = vol.active_scalars.reshape(vol.dimensions, order='F')
    lbrt = vol.bounds[2], vol.bounds[4], vol.bounds[3], vol.bounds[5]
    return image_slice(['y','z'], arr[si,:,::-1].T, lbrt, mapper, smooth_fun)

@pn.depends(sj=volume.param.slice_j, mapper=volume.param.mapper,
            smooth_fun=smoother.param.smooth_fun, vol=volume.param.object)
def image_slice_j(sj, mapper, smooth_fun, vol):
    arr = vol.active_scalars.reshape(vol.dimensions, order='F')
    lbrt = vol.bounds[0], vol.bounds[4], vol.bounds[1], vol.bounds[5]
    return image_slice(['x','z'], arr[:,sj,::-1].T, lbrt, mapper, smooth_fun)

@pn.depends(sk=volume.param.slice_k, mapper=volume.param.mapper,
            smooth_fun=smoother.param.smooth_fun, vol=volume.param.object)
def image_slice_k(sk, mapper, smooth_fun, vol):
    arr = vol.active_scalars.reshape(vol.dimensions, order='F')
    lbrt = vol.bounds[0], vol.bounds[2], vol.bounds[1], vol.bounds[3]
    return image_slice(['x', 'y'], arr[:,::-1,sk].T, lbrt, mapper, smooth_fun)

dmap_i = rasterize(hv.DynamicMap(image_slice_i))
dmap_j = rasterize(hv.DynamicMap(image_slice_j))
dmap_k = rasterize(hv.DynamicMap(image_slice_k))

controller = pn.WidgetBox(
    pn.Column(dataset_selection, toggle_parallel_proj, *volume_controls[1:], sizing_mode='fixed'),
    pn.Param(smoother, parameters=['smooth_level', 'order']),
    pn.layout.VSpacer(),
    css_classes=['panel-widget-box', 'custom-wbox'], sizing_mode='stretch_height'
)

Set up template#

template = """
{%% extends base %%}
<!-- goes in body -->
{%% block contents %%}
{%% set context = '%s' %%}
{%% if context == 'notebook' %%}
    {%% set slicer_id = get_id() %%}
    <div id='{{slicer_id}}'></div>
{%% endif %%}

<script>
var config = {
    settings: {
        hasHeaders: true,
        constrainDragToContainer: true,
        reorderEnabled: true,
        selectionEnabled: false,
        popoutWholeStack: false,
        blockedPopoutsThrowError: true,
        closePopoutsOnUnload: true,
        showPopoutIcon: false,
        showMaximiseIcon: true,
        showCloseIcon: false
    },
    content: [{
        type: 'row',
        content:[
            {
                type: 'component',
                componentName: 'view',
                componentState: { model: '{{ embed(roots.controller) }}',
                                  title: 'Controls',
                                  width: 350,
                                  css_classes:['scrollable']},
                isClosable: false,
            },
            {
                type: 'column',
                content: [
                    {
                        type: 'row',
                        content:[
                            {
                                type: 'component',
                                componentName: 'view',
                                componentState: { model: '{{ embed(roots.scene3d) }}', title: '3D View'},
                                isClosable: false,
                            },
                            {
                                type: 'component',
                                componentName: 'view',
                                componentState: { model: '{{ embed(roots.slice_i) }}', title: 'Slice I'},
                                isClosable: false,
                            }
                        ]
                    },
                    {
                        type: 'row',
                        content:[
                            {
                                type: 'component',
                                componentName: 'view',
                                componentState: { model: '{{ embed(roots.slice_j) }}', title: 'Slice J'},
                                isClosable: false,
                            },
                            {
                                type: 'component',
                                componentName: 'view',
                                componentState: { model: '{{ embed(roots.slice_k) }}', title: 'Slice K'},
                                isClosable: false,
                            }
                        ]
                    }
                ]
            }
        ]
    }]
};

{%% if context == 'notebook' %%}
    var myLayout = new GoldenLayout( config, '#' + '{{slicer_id}}' );
    $('#' + '{{slicer_id}}').css({width: '100%%', height: '800px', margin: '0px'})
{%% else %%}
    var myLayout = new GoldenLayout( config );
{%% endif %%}

myLayout.registerComponent('view', function( container, componentState ){
    const {width, css_classes} = componentState
    if(width)
      container.on('open', () => container.setSize(width, container.height))
    if (css_classes)
      css_classes.map((item) => container.getElement().addClass(item))
    container.setTitle(componentState.title)
    container.getElement().html(componentState.model);
    container.on('resize', () => window.dispatchEvent(new Event('resize')))
});

myLayout.init();
</script>
{%% endblock %%}
"""


tmpl = pn.Template(template=(template % 'server'), nb_template=(template % 'notebook'))
tmpl.nb_template.globals['get_id'] = make_globally_unique_id

tmpl.add_panel('controller', controller)
tmpl.add_panel('scene3d', volume)
tmpl.add_panel('slice_i', pn.panel(dmap_i, sizing_mode='stretch_both'))
tmpl.add_panel('slice_j', pn.panel(dmap_j, sizing_mode='stretch_both'))
tmpl.add_panel('slice_k', pn.panel(dmap_k, sizing_mode='stretch_both'))

tmpl.servable(title='VTKSlicer')
This web page was generated from a Jupyter notebook and not all interactivity will work on this website. Right click to download and run locally for full Python-backed interactivity.

Download this notebook from GitHub (right-click to download).