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

[WIP] FEATURE: Multivolume rendering for multi-dimensional data #186

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
c8bb77a
Add convenience functions for volume rendering transfer functions.
GenevieveBuckley Sep 23, 2018
47caedb
Fix typo
GenevieveBuckley Sep 23, 2018
8de9b84
Better matplotlib colormap namespace coverage with dir(matplotlib.cm)
GenevieveBuckley Sep 23, 2018
9bc0e9f
Travis CI, add matplotlib to test environment.
GenevieveBuckley Sep 24, 2018
68b330e
No single line if statements.
GenevieveBuckley Sep 24, 2018
89a6d79
Transfer functions - allow length of rgba array to be passed as kwarg
GenevieveBuckley Sep 24, 2018
a4d809c
More flexible color input for transfer functions with matplotlib.colo…
GenevieveBuckley Sep 24, 2018
2321bf7
Simplify predefined_transfer_functions() discovery of all the matplot…
GenevieveBuckley Sep 26, 2018
f6b0986
Merge branch 'transfer-functions' into multivolume
GenevieveBuckley Sep 30, 2018
2054248
import transfer functions into pylab.py
GenevieveBuckley Sep 30, 2018
3cf0733
Remove volshow arguments that are related to the transfer function in…
GenevieveBuckley Sep 30, 2018
11b1108
Update import of transfer function for conciseness.
GenevieveBuckley Sep 30, 2018
6aca2a7
volshow loop added to display multiple volumes.
GenevieveBuckley Sep 30, 2018
678c9b8
Multivolume rendering, added color picker widget (python callback).
GenevieveBuckley Sep 30, 2018
18a1e3e
Remove transfer function specific keyword args from quickvolshow conv…
GenevieveBuckley Sep 30, 2018
e1d09db
Merge recent commit of PR pep8-imports from ipyvolume master branch.
GenevieveBuckley Oct 1, 2018
68fc135
More useful to return the figure object with all volumes attached, ra…
GenevieveBuckley Oct 1, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ before_install:
- conda config --set always_yes yes --set changeps1 no
- conda update -q conda
- conda info -a
- conda create -q -n test-environment python=$PYTHON_VERSION numpy scipy runipy
- conda create -q -n test-environment python=$PYTHON_VERSION numpy scipy runipy matplotlib
- source activate test-environment
- conda install -c conda-forge pytest pytest-cov bokeh matplotlib scikit-image shapely
- pip install PyChromeDevTools
Expand Down
93 changes: 48 additions & 45 deletions ipyvolume/pylab.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
from ipyvolume import utils
from ipyvolume import examples
from ipyvolume import headless

from ipyvolume.transferfunction import linear_transfer_function

_last_figure = None

Expand Down Expand Up @@ -657,12 +657,11 @@ def recompute(*_ignore):


def volshow(data, lighting=False, data_min=None, data_max=None,
max_shape=256, tf=None, stereo=False,
max_shape=256, tf_colornames=None, stereo=False,
ambient_coefficient=0.5, diffuse_coefficient=0.8,
specular_coefficient=0.5, specular_exponent=5,
downscale=1,
level=[0.1, 0.5, 0.9], opacity=[0.01, 0.05, 0.1], level_width=0.1,
controls=True, max_opacity=0.2, memorder='C', extent=None):
controls=True, memorder='C', extent=None):
"""Visualize a 3d array using volume rendering.

Currently only 1 volume can be rendered.
Expand All @@ -675,67 +674,71 @@ def volshow(data, lighting=False, data_min=None, data_max=None,
:param float data_min: minimum value to consider for data, if None, computed using np.nanmin
:param float data_max: maximum value to consider for data, if None, computed using np.nanmax
:parap int max_shape: maximum shape for the 3d cube, if larger, the data is reduced by skipping/slicing (data[::N]), set to None to disable.
:param tf: transfer function (or a default one)
:param tf_colornames: transfer function (or a default one)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about letting tf be a string or tuple, like in this PR where the icon can be a string or Icon widget.

:param bool stereo: stereo view for virtual reality (cardboard and similar VR head mount)
:param ambient_coefficient: lighting parameter
:param diffuse_coefficient: lighting parameter
:param specular_coefficient: lighting parameter
:param specular_exponent: lighting parameter
:param float downscale: downscale the rendering for better performance, for instance when set to 2, a 512x512 canvas will show a 256x256 rendering upscaled, but it will render twice as fast.
:param level: level(s) for the where the opacity in the volume peaks, maximum sequence of length 3
:param opacity: opacity(ies) for each level, scalar or sequence of max length 3
:param level_width: width of the (gaussian) bumps where the opacity peaks, scalar or sequence of max length 3
:param bool controls: add controls for lighting and transfer function or not
:param float max_opacity: maximum opacity for transfer function controls
:param extent: list of [[xmin, xmax], [ymin, ymax], [zmin, zmax]] values that define the bounds of the volume, otherwise the viewport is used
:return:
"""
fig = gcf()

if tf is None:
tf = transfer_function(level, opacity, level_width, controls=controls, max_opacity=max_opacity)
if data.ndim == 3: # input data has only one channel
data = np.expand_dims(data, -1)
if tf_colornames is None:
default_colors = ['red', 'green', 'blue', 'grey', 'cyan', 'magenta', 'yellow']
n_volumes = data.shape[-1]
colors = default_colors[:n_volumes]
if data_min is None:
data_min = np.nanmin(data)
if data_max is None:
data_max = np.nanmax(data)
if memorder is 'F':
data = data.T

if extent is None:
extent = [(0, k) for k in data.shape[::-1]]

extent = [(0, k) for k in data[..., -1].shape[::-1]]
if extent:
_grow_limits(*extent)

vol = ipv.Volume(data_original = data,
tf=tf,
data_min = data_min,
data_max = data_max,
show_min = data_min,
show_max = data_max,
extent_original = extent,
data_max_shape = max_shape,
ambient_coefficient = ambient_coefficient,
diffuse_coefficient = diffuse_coefficient,
specular_coefficient = specular_coefficient,
specular_exponent = specular_exponent,
rendering_lighting = lighting)

vol._listen_to(fig)
data = np.moveaxis(data, -1, 0) # for more convenient looping
for i, (subdata, color) in enumerate(zip(data, colors)):
tf = linear_transfer_function(color)
vol = ipv.Volume(data_original = subdata,
tf=tf,
data_min = data_min,
data_max = data_max,
show_min = data_min,
show_max = data_max,
extent_original = extent,
data_max_shape = max_shape,
ambient_coefficient = ambient_coefficient,
diffuse_coefficient = diffuse_coefficient,
specular_coefficient = specular_coefficient,
specular_exponent = specular_exponent,
rendering_lighting = lighting)

vol._listen_to(fig)

if controls:
widget_opacity_scale = ipywidgets.FloatLogSlider(base=10, min=-2, max=2,
description="opacity")
widget_brightness = ipywidgets.FloatLogSlider(base=10, min=-1, max=1,
description="brightness")
ipywidgets.jslink((vol, 'opacity_scale'), (widget_opacity_scale, 'value'))
ipywidgets.jslink((vol, 'brightness'), (widget_brightness, 'value'))
widgets_bottom = [ipywidgets.HBox([widget_opacity_scale, widget_brightness])]
current.container.children += tuple(widgets_bottom, )

fig.volumes = fig.volumes + [vol]

return vol
if controls:
widget_opacity_scale = ipywidgets.FloatLogSlider(base=10, min=-2, max=2,
description="opacity")
widget_brightness = ipywidgets.FloatLogSlider(base=10, min=-1, max=1,
description="brightness")
widget_colorpicker = ipywidgets.ColorPicker(value=color,
layout=ipywidgets.Layout(width='15%'))
ipywidgets.jslink((vol, 'opacity_scale'), (widget_opacity_scale, 'value'))
ipywidgets.jslink((vol, 'brightness'), (widget_brightness, 'value'))
def change_transfer_function(vol, color):
vol.tf = linear_transfer_function(color.new)
widget_colorpicker.observe(lambda x, vol=vol: change_transfer_function(vol, x), names='value')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would create a new widget for each color picked, I think this is a good reason to make this happen on the javascript side.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to do that. What's the javascript equivalent of observe?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


widgets_bottom = [ipywidgets.HBox([widget_colorpicker, widget_opacity_scale, widget_brightness])]
current.container.children += tuple(widgets_bottom, )

fig.volumes = fig.volumes + [vol]

return fig
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm against returning figure, since it does not follow conventions/intuition.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return the full list of volumes, then? I'll do that.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, check my above comment, why would volshow do this? Why not repeatedly call volshow?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw, for a quick chat, https://gitter.im/maartenbreddels/ipyvolume may be useful, and we could also do a videochat if that speeds things up.



def save(filepath, makedirs=True, title=u'IPyVolume Widget', all_states=False,
Expand Down
116 changes: 116 additions & 0 deletions ipyvolume/transferfunction.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import traitlets
from traitlets import Unicode, validate
from traittypes import Array
import matplotlib.colors
import matplotlib.cm

import ipyvolume._version
from ipyvolume import serialize
Expand Down Expand Up @@ -169,3 +171,117 @@ def control(self, max_opacity=0.2):
return ipywidgets.VBox(
[ipywidgets.HBox([ipywidgets.Label(value="levels:"), l1, l2, l3]), ipywidgets.HBox([ipywidgets.Label(value="opacities:"), o1, o2, o3])]
)


def linear_transfer_function(color,
min_opacity=0,
max_opacity=0.05,
reverse_opacity=False,
n_elements = 256):
"""Transfer function of a single color and linear opacity.

:param color: Listlike RGB, or string with hexidecimal or named color.
RGB values should be within 0-1 range.
:param min_opacity: Minimum opacity, default value is 0.0.
Lowest possible value is 0.0, optional.
:param max_opacity: Maximum opacity, default value is 0.05.
Highest possible value is 1.0, optional.
:param reverse_opacity: Linearly decrease opacity, optional.
:param n_elements: Length of rgba array transfer function attribute.
:type color: listlike or string
:type min_opacity: float, int
:type max_opacity: float, int
:type reverse_opacity: bool
:type n_elements: int
:return: transfer_function
:rtype: ipyvolume TransferFunction

:Example:
>>> import ipyvolume as ipv
>>> green_tf = ipv.transfer_function.linear_transfer_function('green')
>>> ds = ipv.datasets.aquariusA2.fetch()
>>> ipv.volshow(ds.data[::4,::4,::4], tf=green_tf)
>>> ipv.show()

.. seealso:: matplotlib_transfer_function()
"""
r, g, b = matplotlib.colors.to_rgb(color)
opacity = np.linspace(min_opacity, max_opacity, num=n_elements)
if reverse_opacity:
opacity = np.flip(opacity, axis=0)
rgba = np.transpose(np.stack([[r] * n_elements,
[g] * n_elements,
[b] * n_elements,
opacity]))
transfer_function = TransferFunction(rgba=rgba)
return transfer_function


def matplotlib_transfer_function(colormap_name,
min_opacity=0,
max_opacity=0.05,
reverse_colormap=False,
reverse_opacity=False,
n_elements=256):
"""Transfer function from matplotlib colormaps.

:param colormap_name: name of matplotlib colormap
:param min_opacity: Minimum opacity, default value is 0.
Lowest possible value is 0, optional.
:param max_opacity: Maximum opacity, default value is 0.05.
Highest possible value is 1.0, optional.
:param reverse_colormap: reversed matplotlib colormap, optional.
:param reverse_opacity: Linearly decrease opacity, optional.
:param n_elements: Length of rgba array transfer function attribute.
:type colormap_name: str
:type min_opacity: float, int
:type max_opacity: float, int
:type reverse_colormap: bool
:type reverse_opacity: bool
:type n_elements: int
:return: transfer_function
:rtype: ipyvolume TransferFunction

:Example:
>>> import ipyvolume as ipv
>>> rgb = (0, 255, 0) # RGB value for green
>>> green_tf = ipv.transfer_function.matplotlib_transfer_function('bone')
>>> ds = ipv.datasets.aquariusA2.fetch()
>>> ipv.volshow(ds.data[::4,::4,::4], tf=green_tf)
>>> ipv.show()

.. seealso:: linear_transfer_function()
"""
cmap = matplotlib.cm.get_cmap(name=colormap_name)
rgba = np.array([cmap(i) for i in np.linspace(0, 1, n_elements)])
if reverse_colormap:
rgba = np.flip(rgba, axis=0)
# Create opacity values to overwrite default matplotlib opacity=1.0
opacity = np.linspace(min_opacity, max_opacity, num=n_elements)
if reverse_opacity:
opacity = np.flip(opacity, axis=0)
rgba[:,-1] = opacity # replace opacity=1 with actual opacity
transfer_function = TransferFunction(rgba=rgba)
return transfer_function


def predefined_transfer_functions():
"""Load predefined transfer functions into a dictionary.

:return: dictionary of predefined transfer functions.
:rtype: dict of ipyvolume TransferFunction instances
"""
transfer_functions = {}
# RGB primary and secondary colors
colors = ['red', 'green', 'blue', 'yellow', 'magenta', 'cyan',
'black', 'gray', 'white']
for color in colors:
tf = linear_transfer_function(color)
transfer_functions[color] = tf
tf_reversed = linear_transfer_function(rgb, reverse_opacity=True)
transfer_functions[color_key + '_r'] = tf_reversed
# All matplotlib colormaps
matplotlib_colormaps = matplotlib.cm.cmap_d.keys()
for colormap in matplotlib_colormaps:
transfer_functions[colormap] = matplotlib_transfer_function(colormap)
return transfer_functions
7 changes: 2 additions & 5 deletions ipyvolume/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ def quickscatter(x, y, z, **kwargs):


def quickvolshow(data, lighting=False, data_min=None, data_max=None, max_shape=256,
level=[0.1, 0.5, 0.9], opacity=[0.01, 0.05, 0.1], level_width=0.1, extent=None, memorder='C', **kwargs):
extent=None, memorder='C', **kwargs):
"""
Visualize a 3d array using volume rendering

Expand All @@ -352,16 +352,13 @@ def quickvolshow(data, lighting=False, data_min=None, data_max=None, max_shape=
:param data_max: maximum value to consider for data, if None, computed using np.nanmax
:parap int max_shape: maximum shape for the 3d cube, if larger, the data is reduced by skipping/slicing (data[::N]), set to None to disable.
:param extent: list of [[xmin, xmax], [ymin, ymax], [zmin, zmax]] values that define the bounds of the volume, otherwise the viewport is used
:param level: level(s) for the where the opacity in the volume peaks, maximum sequence of length 3
:param opacity: opacity(ies) for each level, scalar or sequence of max length 3
:param level_width: width of the (gaussian) bumps where the opacity peaks, scalar or sequence of max length 3
:param kwargs: extra argument passed to Volume and default transfer function
:return:

"""
ipv.figure()
ipv.volshow(data, lighting=lighting, data_min=data_min, data_max=data_max, max_shape=max_shape,
level=level, opacity=opacity, level_width=level_width, extent=extent, memorder=memorder, **kwargs)
extent=extent, memorder=memorder, **kwargs)
return ipv.gcc()

def scatter(x, y, z, color=(1,0,0), s=0.01):
Expand Down