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

VTK volume viewer export color mapper informations to python side #1073

Merged
merged 22 commits into from
Feb 15, 2020

Conversation

xavArtley
Copy link
Collaborator

@xavArtley xavArtley commented Feb 5, 2020

With this PR we begin to have a real 3d slicer

import panel as pn
pn.extension('vtk')
import holoviews as hv
hv.extension('bokeh')
import pyvista as pv
from pyvista import examples
# Download a volumetric dataset
head = examples.download_head()
arr = head.active_scalar.reshape(head.dimensions, order='F')
interpolation_level = pn.widgets.IntSlider(value=5, start=1, end=20, name='Interpolation Level')
vol = pn.panel(head, sizing_mode='stretch_both', height=400, display_slices=True, rescale=True, display_volume=False,
              orientation_widget=True)
import scipy.ndimage as sn

@pn.depends(vol.param.slice_i,vol.param.slice_j,vol.param.slice_k,vol.param.mapper, interpolation_level.param.value)
def print_mapper(si, sj, sk, mapper, interp_level):
    im_interp = lambda x: sn.zoom(x, zoom=interp_level, order=1)
    low = mapper['low'] if mapper else arr.min()
    high = mapper['high'] if mapper else arr.max()
    cmap = mapper['palette'] if mapper else 'fire'
    
    im_i = hv.Image(im_interp(arr[si,::-1,:]), bounds=(head.extent[4],head.extent[2],head.extent[5],head.extent[3]), kdims=['z','y'], vdims='Intensity')
    im_j = hv.Image(im_interp(arr[::-1,sj,:]), bounds=(head.extent[4],head.extent[0],head.extent[5],head.extent[1]), kdims=['z','x'], vdims='Intensity')
    im_k = hv.Image(im_interp(arr[::-1,:,sk]), bounds=(head.extent[2],head.extent[0],head.extent[3],head.extent[1]), kdims=['y','x'], vdims='Intensity')
    return (im_i + im_j + im_k).opts(hv.opts.Image(clim=(low, high), cmap=cmap, invert_axes=True, data_aspect=1))
    
    
pn.Column(
    pn.Row(
        pn.Column(vol.controls(jslink=False,
                               parameters=['render_background', 'rescale', 'slice_i', 'slice_j', 
                                           'slice_k', 'display_volume', 'display_slices']),
                  interpolation_level),
        vol
    ),
    print_mapper
)

ezgif com-video-to-gif (1)

@philippjfr
Copy link
Member

Ready to merge?

@xavArtley
Copy link
Collaborator Author

No I'd like to modify documentation before

@xavArtley
Copy link
Collaborator Author

What do you think to had something like that to the gallery?

import param
import panel as pn
import holoviews as hv
import pyvista as pv
from pyvista import examples
from scipy.ndimage import zoom

pn.extension('vtk')
hv.extension('bokeh')


class ImageSmoother(param.Parameterized):
    
    smooth_fun = param.Parameter(default=None)
    order = param.Selector(default=1, objects=[1,2,3])
    zoom = param.Integer(default=2, bounds=(1,10))
    
    def __init__(self, **params):
        super().__init__(**params)
        self._update_fun()
    
    @param.depends('order', 'zoom', watch=True)
    def _update_fun(self):
        self.smooth_fun = lambda x: zoom(x, zoom=self.zoom, order=self.order)
    
    @property
    def stream(self):
        return hv.streams.Params(self, parameters=['smooth_fun'])



# Download a volumetric dataset
vol = examples.download_head()
volume = pn.panel(vol, sizing_mode='stretch_both', height=400, display_slices=True, orientation_widget=True)
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)
def update_camera_projection(*evts):
    volume.camera['parallelProjection'] = evts[0].new
    volume.param.trigger('camera')
toggle_parallel_proj.param.watch(update_camera_projection, ['value'], onlychanged=True)



stream_i = hv.streams.Params(volume, parameters=['slice_i'], rename={'slice_i':'si'})
stream_j = hv.streams.Params(volume, parameters=['slice_j'], rename={'slice_j':'sj'})
stream_k = hv.streams.Params(volume, parameters=['slice_k'], rename={'slice_k':'sk'})
stream_mapper = hv.streams.Params(volume, parameters=['mapper'])
smoother = ImageSmoother()
stream_smooth = smoother.stream

arr = vol.active_scalar.reshape(vol.dimensions, order='F')

def image_slice_i(si, mapper, smooth_fun):
    low = mapper['low'] if mapper else arr.min()
    high = mapper['high'] if mapper else arr.max()
    cmap = mapper['palette'] if mapper else 'fire'
    lbrt = vol.extent[4], vol.extent[2], vol.extent[5], vol.extent[3]
    im_i = hv.Image(smooth_fun(arr[si,::-1,:]), bounds=lbrt, kdims=['z','y'], vdims='Intensity')
    frame_width = 500
    frame_height = int(frame_width * (lbrt[2]-lbrt[0])/(lbrt[3]-lbrt[1]))
    return im_i.opts(hv.opts.Image(clim=(low, high), cmap=cmap, frame_width=frame_width, frame_height=frame_height))
    
def image_slice_j(sj, mapper, smooth_fun):
    low = mapper['low'] if mapper else arr.min()
    high = mapper['high'] if mapper else arr.max()
    cmap = mapper['palette'] if mapper else 'fire'
    lbrt = vol.extent[4], vol.extent[0], vol.extent[5], vol.extent[1]
    im_j = hv.Image(smooth_fun(arr[::-1,sj,:]), bounds=lbrt, kdims=['z','x'], vdims='Intensity')
    frame_width = 500
    frame_height = int(frame_width * (lbrt[2]-lbrt[0])/(lbrt[3]-lbrt[1]))
    return im_j.opts(hv.opts.Image(clim=(low, high), cmap=cmap, frame_width=frame_width, frame_height=frame_height))
                     
def image_slice_k(sk, mapper, smooth_fun):
    low = mapper['low'] if mapper else arr.min()
    high = mapper['high'] if mapper else arr.max()
    cmap = mapper['palette'] if mapper else 'fire'
    lbrt = vol.extent[2], vol.extent[0], vol.extent[3], vol.extent[1]
    im_k = hv.Image(smooth_fun(arr[::-1,:,sk]), bounds=lbrt, kdims=['y','x'], vdims='Intensity')
    frame_width = 500
    frame_height = int(frame_width * (lbrt[2]-lbrt[0])/(lbrt[3]-lbrt[1]))
    return im_k.opts(hv.opts.Image(clim=(low, high), cmap=cmap, frame_width=frame_width, frame_height=frame_height))

hv.opts.defaults(hv.opts.Image(invert_axes=True, data_aspect=1))
dmap_i = hv.DynamicMap(image_slice_i, streams=[stream_i, stream_mapper, stream_smooth])
dmap_j = hv.DynamicMap(image_slice_j, streams=[stream_j, stream_mapper, stream_smooth])
dmap_k = hv.DynamicMap(image_slice_k, streams=[stream_k, stream_mapper, stream_smooth])


controller = pn.WidgetBox(toggle_parallel_proj, *volume_controls[1:], pn.Param(smoother, parameters=['zoom', 'order']))
pn.Row(pn.Column(pn.Row(controller, volume), pn.Row(dmap_i, dmap_j)), dmap_k, sizing_mode='stretch_height').show()

@codecov
Copy link

codecov bot commented Feb 6, 2020

Codecov Report

Merging #1073 into master will decrease coverage by 0.32%.
The diff coverage is 84.37%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1073      +/-   ##
==========================================
- Coverage    88.1%   87.77%   -0.33%     
==========================================
  Files         105      105              
  Lines       12380    12590     +210     
==========================================
+ Hits        10907    11051     +144     
- Misses       1473     1539      +66
Impacted Files Coverage Δ
panel/pane/vtk/__init__.py 100% <ø> (ø) ⬆️
panel/models/vtk.py 100% <100%> (ø) ⬆️
panel/pane/vtk/vtk.py 86.45% <80%> (-3.77%) ⬇️
panel/util.py 86.16% <0%> (-3.15%) ⬇️
panel/interact.py 73.57% <0%> (-2.5%) ⬇️
panel/tests/test_reactive.py 98.76% <0%> (-1.24%) ⬇️
panel/io/notebook.py 61.13% <0%> (-1.22%) ⬇️
panel/pane/vtk/vtkjs_serializer.py 82.05% <0%> (-0.65%) ⬇️
panel/viewable.py 83.42% <0%> (+0.94%) ⬆️
... and 1 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update ef31fb4...5577a2b. Read the comment docs.

@xavArtley
Copy link
Collaborator Author

xavArtley commented Feb 6, 2020

ezgif com-video-to-gif (2)

@philippjfr
Copy link
Member

Wow, how did you do the draggable layout?

@xavArtley
Copy link
Collaborator Author

xavArtley commented Feb 6, 2020

I used http://golden-layout.com/
In the ipynb you can view the template.
It was not to difficult to learn it (just for the purpose of this example)

@philippjfr
Copy link
Member

Very cool, you used it as a custom template?

@xavArtley
Copy link
Collaborator Author

xavArtley commented Feb 6, 2020 via email

@philippjfr
Copy link
Member

Going to merge and get this in 0.8.1 pretending it's a simple enhancement 😄

@philippjfr
Copy link
Member

@xavArtley The VTKVolume.mapper isn't syncing for me in the notebook, any idea what's going on there?

@xavArtley
Copy link
Collaborator Author

xavArtley commented Feb 8, 2020 via email

@xavArtley
Copy link
Collaborator Author

xavArtley commented Feb 8, 2020

I haven't read all the template documentation to handle template in notebook. I will make some modifications instead of testing the document title

@xavArtley
Copy link
Collaborator Author

@xavArtley The VTKVolume.mapper isn't syncing for me in the notebook, any idea what's going on there?

I found something strange when looking at volume.model after displaying the template there is 2 reférences
Capture d’écran de 2020-02-08 13-16-08

@philippjfr
Copy link
Member

I found something strange when looking at volume.model after displaying the template there is 2 reférences

This is in fact expected, templates have to do some funny stuff to linking set up.

@xavArtley
Copy link
Collaborator Author

xavArtley commented Feb 13, 2020

@philippjfr Do you think it should be possible to set data_aspect=1 to images while keeping layout responsivness?

@philippjfr
Copy link
Member

@philippjfr Do you think it should be possible to set data_aspect=1 to images while keeping layout responsivness?

Would definitely need some special handling I think only bokeh figures currently support aspect handling out of the box.

@philippjfr philippjfr merged commit 25a734c into master Feb 15, 2020
@xavArtley
Copy link
Collaborator Author

@philippjfr Python 2 failed on master because the property .active_scalars was introduced on pyvista 0.23.1 and for the python 2 build the version 0.22.4 seems to be installed.
2 solutions to correct it:

  • use the old property (trigger deprection warnings) .active_scalar
  • update pyvista version to >= 0.23for python 2 build however I don't know why it's not the same version on python 3 and python 2.

@philippjfr
Copy link
Member

@xavArtley I'm happy to defer to your judgement here. Given that we are dropping python 2 support in about a week and there will be only one more release, I'm happy to skip this test for py2 if pyvista>=0.23 doesn't support py2.

@xavArtley xavArtley deleted the xav/export_colormap branch February 21, 2020 16:00
@JeffreyWardman
Copy link

The notebook for this (VTKSlicer.ipynb) does not successfully link the controls with the image. E.g. slicing does not work, neither does selecting the background.

@xavArtley
Copy link
Collaborator Author

Are you talking about it in the panel documentation or when you run the notebook locally?
It is not working in the documentation because it needs a server
however locally I tested it and it seems ok:
vtkslicer

@JeffreyWardman
Copy link

@xavArtley I was testing locally. It had to do with the Jupyter extensions. It didn't return anything in Jupyter notebook. In Jupyter lab it showed the window but didn't have any interactivity. Turns out the reason was because I hadn't installed the pyviz extension. Problem solved! Thanks for your help.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants