{ "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from functools import partial\n", "\n", "import holoviews as hv\n", "import numpy as np\n", "import panel as pn\n", "import param\n", "\n", "from holoviews.operation.datashader import rasterize\n", "from bokeh.util.serialization import make_globally_unique_id\n", "from pyvista import examples\n", "from scipy.ndimage import zoom\n", "\n", "import pyvista as pv # Import after datashader to avoid segfault on some systems\n", "\n", "js_files = {'jquery': 'https://code.jquery.com/jquery-1.11.1.min.js',\n", " 'goldenlayout': 'https://golden-layout.com/files/latest/js/goldenlayout.min.js'}\n", "css_files = ['https://golden-layout.com/files/latest/css/goldenlayout-base.css',\n", " 'https://golden-layout.com/files/latest/css/goldenlayout-dark-theme.css']\n", "\n", "pn.extension('vtk', js_files=js_files, css_files=css_files, design='material', theme='dark', sizing_mode=\"stretch_width\")\n", "\n", "hv.renderer('bokeh').theme = 'dark_minimal'\n", "hv.opts.defaults(hv.opts.Image(responsive=True, tools=['hover']))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Declare callbacks" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class ImageSmoother(param.Parameterized):\n", " \n", " smooth_fun = param.Parameter(default=None)\n", " smooth_level = param.Integer(default=5, bounds=(1,10))\n", " order = param.Selector(default=1, objects=[1,2,3])\n", " \n", " def __init__(self, **params):\n", " super(ImageSmoother, self).__init__(**params)\n", " self._update_fun()\n", "\n", " @param.depends('order', 'smooth_level', watch=True)\n", " def _update_fun(self):\n", " self.smooth_fun = lambda x: zoom(x, zoom=self.smooth_level, order=self.order)\n", "\n", "def update_camera_projection(*evts):\n", " volume.camera['parallelProjection'] = evts[0].new\n", " volume.param.trigger('camera')\n", "\n", "def hook_reset_range(plot, elem, lbrt):\n", " bkplot = plot.handles['plot']\n", " x_range = lbrt[0], lbrt[2]\n", " y_range = lbrt[1], lbrt[3]\n", " old_x_range_reset = bkplot.x_range.reset_start, bkplot.x_range.reset_end\n", " old_y_range_reset = bkplot.y_range.reset_start, bkplot.y_range.reset_end \n", " if x_range != old_x_range_reset or y_range != old_y_range_reset:\n", " bkplot.x_range.reset_start, bkplot.x_range.reset_end = x_range\n", " bkplot.x_range.start, bkplot.x_range.end = x_range\n", " bkplot.y_range.reset_start, bkplot.y_range.reset_end = y_range\n", " bkplot.y_range.start, bkplot.y_range.end = y_range\n", " \n", "def image_slice(dims, array, lbrt, mapper, smooth_fun):\n", " array = np.asarray(array)\n", " low = mapper['low'] if mapper else array.min()\n", " high = mapper['high'] if mapper else array.max()\n", " cmap = mapper['palette'] if mapper else 'fire'\n", " img = hv.Image(smooth_fun(array), bounds=lbrt, kdims=dims, vdims='Intensity')\n", " reset_fun = partial(hook_reset_range, lbrt=lbrt)\n", " return img.opts(clim=(low, high), cmap=cmap, hooks=[reset_fun])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Declare Panel" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Download datasets\n", "head = examples.download_head()\n", "brain = examples.download_brain()\n", "\n", "dataset_selection = pn.widgets.Select(name='Dataset', value=head, options={'Head': head, 'Brain': brain})\n", "\n", "volume = pn.pane.VTKVolume(\n", " dataset_selection.value, sizing_mode='stretch_both', min_height=400, \n", " display_slices=True, orientation_widget=True, render_background=\"#222222\",\n", " colormap='Rainbow Desaturated'\n", ")\n", "\n", "@pn.depends(dataset_selection, watch=True)\n", "def update_volume_object(value):\n", " controller.loading=True\n", " volume.object = value\n", " controller.loading=False\n", "\n", "volume_controls = volume.controls(jslink=False, parameters=[\n", " 'render_background', 'display_volume', 'display_slices',\n", " 'slice_i', 'slice_j', 'slice_k', 'rescale'\n", "])\n", "\n", "toggle_parallel_proj = pn.widgets.Toggle(name='Parallel Projection', value=False)\n", "\n", "toggle_parallel_proj.param.watch(update_camera_projection, ['value'], onlychanged=True)\n", "\n", "smoother = ImageSmoother()\n", "\n", "def image_slice_i(si, mapper, smooth_fun, vol):\n", " arr = vol.active_scalars.reshape(vol.dimensions, order='F')\n", " lbrt = vol.bounds[2], vol.bounds[4], vol.bounds[3], vol.bounds[5]\n", " return image_slice(['y','z'], arr[si,:,::-1].T, lbrt, mapper, smooth_fun)\n", "\n", "def image_slice_j(sj, mapper, smooth_fun, vol):\n", " arr = vol.active_scalars.reshape(vol.dimensions, order='F')\n", " lbrt = vol.bounds[0], vol.bounds[4], vol.bounds[1], vol.bounds[5]\n", " return image_slice(['x','z'], arr[:,sj,::-1].T, lbrt, mapper, smooth_fun)\n", "\n", "def image_slice_k(sk, mapper, smooth_fun, vol):\n", " arr = vol.active_scalars.reshape(vol.dimensions, order='F')\n", " lbrt = vol.bounds[0], vol.bounds[2], vol.bounds[1], vol.bounds[3]\n", " return image_slice(['x', 'y'], arr[:,::-1,sk].T, lbrt, mapper, smooth_fun)\n", "\n", "common = dict(\n", " mapper=volume.param.mapper,\n", " smooth_fun=smoother.param.smooth_fun,\n", " vol=volume.param.object,\n", ")\n", "\n", "dmap_i = rasterize(hv.DynamicMap(pn.bind(image_slice_i, si=volume.param.slice_i, **common)))\n", "dmap_j = rasterize(hv.DynamicMap(pn.bind(image_slice_j, sj=volume.param.slice_j, **common)))\n", "dmap_k = rasterize(hv.DynamicMap(pn.bind(image_slice_k, sk=volume.param.slice_k, **common)))\n", "\n", "controller = pn.Column(\n", " pn.Column(dataset_selection, toggle_parallel_proj, *volume_controls[1:]),\n", " pn.Param(smoother, parameters=['smooth_level', 'order']),\n", " pn.panel(\"This app demos **advanced 3D visualisation** using [Panel](https://panel.holoviz.org/) and [PyVista](https://docs.pyvista.org/).\", margin=(5,15)),\n", " pn.layout.VSpacer(),\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Set up template" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "template = \"\"\"\n", "{%% extends base %%}\n", "\n", "{%% block contents %%}\n", "{%% set context = '%s' %%}\n", "{%% if context == 'notebook' %%}\n", " {%% set slicer_id = get_id() %%}\n", "
\n", "{%% endif %%}\n", "\n", "\n", "\n", "{%% endblock %%}\n", "\"\"\"\n", "\n", "\n", "tmpl = pn.Template(template=(template % 'server'), nb_template=(template % 'notebook'))\n", "tmpl.nb_template.globals['get_id'] = make_globally_unique_id\n", "\n", "tmpl.add_panel('controller', controller)\n", "tmpl.add_panel('scene3d', volume)\n", "tmpl.add_panel('slice_i', pn.panel(dmap_i, sizing_mode='stretch_both'))\n", "tmpl.add_panel('slice_j', pn.panel(dmap_j, sizing_mode='stretch_both'))\n", "tmpl.add_panel('slice_k', pn.panel(dmap_k, sizing_mode='stretch_both'))\n", "\n", "tmpl.servable(title='VTKSlicer')" ] } ], "metadata": { "language_info": { "name": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 4 }