Skip to content

Commit

Permalink
vtk volume rendering directly on numpy 3d arrays
Browse files Browse the repository at this point in the history
  • Loading branch information
ARTUSI authored and xavArtley committed Sep 25, 2019
1 parent 7c4f702 commit 8ac4125
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 66 deletions.
16 changes: 11 additions & 5 deletions panel/models/vtk.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"""
import os

from bokeh.core.properties import String, Bool, Dict, Any, Override, List, Instance
from bokeh.models import HTMLBox, ColumnDataSource
from bokeh.core.properties import String, Bool, Dict, Any, Override, Seq, Int, Float
from bokeh.models import HTMLBox

from ..compiler import CUSTOM_MODELS

Expand Down Expand Up @@ -37,8 +37,6 @@ class VTKPlot(HTMLBox):

renderer_el = Any(readonly=True)

data_sources = List(Instance(ColumnDataSource))

height = Override(default=300)

width = Override(default=300)
Expand All @@ -60,7 +58,15 @@ class VTKVolumePlot(HTMLBox):

__implementation__ = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'vtkvolume.ts')

data = String(help="""The serialized data""")
actor = Any(readonly=True)

data = String()

dtype = String()

dims = Seq(Int)

spacing = Seq(Float)

height = Override(default=300)

Expand Down
63 changes: 39 additions & 24 deletions panel/models/vtkvolume.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@

import * as p from "core/properties"
import {ARRAY_TYPES, DType} from "core/util/serialization"
import {HTMLBox, HTMLBoxView} from "models/layouts/html_box"
import {div} from "core/dom"
const vtk = (window as any).vtk
Expand Down Expand Up @@ -29,6 +30,10 @@ export class VTKVolumePlotView extends HTMLBoxView {
height: "100%"
}
});
this._controllerWidget = vtk.Interaction.UI.vtkVolumeController.newInstance({
size: [400, 150],
rescaleColorMap: true,
})
}


Expand All @@ -40,10 +45,6 @@ export class VTKVolumePlotView extends HTMLBoxView {
render() {
//create transfer color ui widgets
super.render()
this._controllerWidget = vtk.Interaction.UI.vtkVolumeController.newInstance({
size: [400, 150],
rescaleColorMap: true,
})
this._controllerWidget.setContainer(this.el)
if (!(this._container === this.el.childNodes[1]))
this.el.appendChild(this._container)
Expand All @@ -55,22 +56,39 @@ export class VTKVolumePlotView extends HTMLBoxView {
rootContainer: this.el,
container: this._container
});
this._rendererEl.getRenderWindow().getInteractor().setDesiredUpdateRate(45)
this._plot()
}
this._plot()

this._rendererEl.getRenderer().resetCamera()
this._rendererEl.getRenderWindow().render()
}

_create_source(): any{
debugger
const source = vtk.Common.DataModel.vtkImageData.newInstance({
spacing: this.model.spacing
})
source.setDimensions(this.model.dims)
source.setOrigin(this.model.dims.map((v: number) => v/2))
const dataArray = vtk.Common.Core.vtkDataArray.newInstance({
name: "scalars",
numberOfComponents: 1,
values: new ARRAY_TYPES[this.model.dtype as DType](utf8ToAB(atob(this.model.data)))
})
source.getPointData().setScalars(dataArray)
return source
}

_plot(): void {
//Read data
const vtiReader = vtk.IO.XML.vtkXMLImageDataReader.newInstance()
vtiReader.parseAsArrayBuffer(utf8ToAB(atob(this.model.data)))

this._rendererEl.getRenderWindow().getInteractor().setDesiredUpdateRate(15)

//Create vtk volume and add it to the scene
const source = this._create_source()
const actor = vtk.Rendering.Core.vtkVolume.newInstance()
const source = vtiReader.getOutputData(0)
const mapper = vtk.Rendering.Core.vtkVolumeMapper.newInstance()



actor.setMapper(mapper)
mapper.setInputData(source)

Expand Down Expand Up @@ -120,25 +138,18 @@ export class VTKVolumePlotView extends HTMLBoxView {
actor.getProperty().setSpecularPower(8.0);

this._rendererEl.getRenderer().addVolume(actor)
this._rendererEl.setResizeCallback(({ width }: {width: number, height: number}) => {
// 2px padding + 2x1px boder + 5px edge = 14
if (width > 414) {
this._controllerWidget.setSize(400, 150)
} else {
this._controllerWidget.setSize(width - 14, 150)
}
this._controllerWidget.render()
})
this._controllerWidget.setupContent(this._rendererEl.getRenderWindow(), actor, true)
this._rendererEl.getRenderer().resetCamera()
this._rendererEl.getRenderWindow().render()
}
}

export namespace VTKVolumePlot {
export type Attrs = p.AttrsOf<Props>
export type Props = HTMLBox.Props & {
data: p.Property<string>
data: p.Property<string>,
dtype: p.Property<string>,
spacing: p.Property<any>,
dims: p.Property<any>,
actor: p.Property<any>
}
}

Expand All @@ -156,7 +167,11 @@ export class VTKVolumePlot extends HTMLBox {
this.prototype.default_view = VTKVolumePlotView

this.define<VTKVolumePlot.Props>({
data: [ p.Instance ],
data: [ p.String ],
dtype: [ p.String ],
dims: [ p.Any ],
spacing: [ p.Any ],
actor: [ p.Any ],
})

this.override({
Expand Down
54 changes: 23 additions & 31 deletions panel/pane/vtk/vtk.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from six import string_types

import param
import numpy as np

from pyviz_comms import JupyterComm

Expand All @@ -33,8 +34,8 @@ class VTKVolume(PaneBase):

@classmethod
def applies(cls, obj):
if (isinstance(obj, string_types) and obj.endswith('.vti') or
any([isinstance(obj, k) for k in cls._serializers.keys()])):

if isinstance(obj, np.ndarray) and obj.ndim == 3:
return True
elif 'vtk' not in sys.modules:
return False
Expand All @@ -57,10 +58,14 @@ def _get_model(self, doc, root=None, parent=None, comm=None):
else:
VTKVolumePlot = getattr(sys.modules['panel.models.vtk'], 'VTKVolumePlot')

volume_serial = self._serialize()
data = base64encode(volume_serial) if volume_serial is not None else volume_serial
props = self._process_param_change(self._init_properties())
model = VTKVolumePlot(data=data, **props)
data_array = self._get_data_array()

model = VTKVolumePlot(data=base64encode(data_array.ravel(order='F' if data_array.flags['F_CONTIGUOUS'] else 'C')),
dims=data_array.shape if data_array.flags['F_CONTIGUOUS'] else data_array.shape[::-1],
spacing=(1, 1, 1),
dtype=data_array.dtype.name,
** props)
if root is None:
root = model
self._link_props(model, ['data'], doc, root, comm)
Expand All @@ -75,47 +80,35 @@ def _init_properties(self):
return {k: v for k, v in self.param.get_param_values()
if v is not None and k not in ['default_layout', 'object']}

def _update(self, model):
volume_serial = self._serialize()
model.data = base64encode(volume_serial) if volume_serial is not None else volume_serial

@classmethod
def register_serializer(cls, class_type, serializer):
"""
Register a seriliazer for a given type of class.
A serializer is a function which take an instance of `class_type`
(like a vtk.vtkRenderWindow) as input and return the binary zip
stream of the corresponding `vtkjs` file
(like a vtk.vtkImageData) as input and return a numpy array of the data
"""
cls._serializers.update({class_type:serializer})

def _serialize(self):
if self.object is None:
volume_serial = None
elif isinstance(self.object, string_types) and self.object.endswith('.vti'):
if os.path.isfile(self.object):
with open(self.object, 'rb') as f:
volume_serial = f.read()
else:
data_url = urlopen(self.object)
volume_serial = data_url.read()
elif hasattr(self.object, 'read'):
volume_serial = self.object.read()
def _get_data_array(self):
if self.object is np.ndarray:
data_array = self.object
else:
available_serializer = [v for k, v in VTK._serializers.items() if isinstance(self.object, k)]
if len(available_serializer) == 0:
import vtk
if isinstance(self.object, vtk.vtkImageData):
from .vtkjs_serializer import volume_serializer

VTK.register_serializer(vtk.vtkImageData, volume_serializer)
serializer = volume_serializer
from .vtkjs_serializer import volume_serializer

VTK.register_serializer(vtk.vtkRenderWindow, volume_serializer)
serializer = volume_serializer
else:
serializer = available_serializer[0]
volume_serial = serializer(self.object)

return volume_serial
data_array = serializer(self.object)

def _update(self, model):
volume_serial = self._serialize()
model.data = base64encode(volume_serial) if volume_serial is not None else volume_serial
return data_array


class VTK(PaneBase):
Expand Down Expand Up @@ -196,7 +189,6 @@ def construct_colorbars(self, orientation='horizontal'):
except:
self._legend = {}
if self._legend:
import numpy as np
from bokeh.plotting import figure
from bokeh.models import LinearColorMapper, ColorBar, FixedTicker
if orientation == 'horizontal':
Expand Down
9 changes: 3 additions & 6 deletions panel/pane/vtk/vtkjs_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import vtk
import os, sys, json, random, string, hashlib, zipfile

from vtk.util import numpy_support
from io import BytesIO

from .enums import SCALAR_MODE, ACCESS_MODE
Expand Down Expand Up @@ -377,12 +378,8 @@ def construct_palettes(render_window):


def volume_serializer(imageData):
writer = vtk.vtkXMLImageDataWriter()
writer.SetCompressorTypeToZLib()
writer.SetInputData(imageData)
writer.WriteToOutputStringOn()
writer.Write()
return writer.GetOutputString().encode()
data = numpy_support.vtk_to_numpy(imageData.GetPointData().GetScalars())
return data.reshape(*imageData.GetDimensions(), order='F')


def render_window_serializer(render_window):
Expand Down

0 comments on commit 8ac4125

Please sign in to comment.