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: Convenience functions for volume rendering transfer functions #178

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
4 changes: 2 additions & 2 deletions .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
- pip install coveralls
Expand All @@ -42,4 +42,4 @@ script:
# - runipy examples/examples.ipynb

after_success:
coveralls
coveralls
139 changes: 139 additions & 0 deletions ipyvolume/transferfunction.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from traittypes import Array
import traitlets
import numpy as np
import matplotlib.cm
from . import serialize
from .serialize import array_rgba_png_serialization, array_serialization
N = 1024
Expand Down Expand Up @@ -164,3 +165,141 @@ 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(rgb_values,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Instead of rgb_color, we can just name it color, and let matplotlib deal with it:
https://github.com/glue-viz/glue-jupyter/blob/7254451a2a671cdea07296861abd3f0cf50c6409/glue_jupyter/ipyvolume/volume.py#L43
It seems to accept named colors like 'red', hex values '#f0e6a4', and float tuples.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's much more elegant, thanks. Also means we can probably use this in conjunction with the color picker ipywidget, too! (Let me check that last part, but that would be great).

Copy link
Collaborator

Choose a reason for hiding this comment

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

It would be great if we do it on the frontend, and indeed use the color picker!

Copy link
Collaborator

Choose a reason for hiding this comment

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

What I've done in a previous project, was to put all the colormaps of matplotlib in 1 texture, of #texture x 1024 pixels, and then the index selected the row of the texture.

min_opacity=0,
max_opacity=0.05,
reverse_opacity=False):
"""Transfer function from a single RGB value with a linear opacity.

:param rgb_values: Tuple or list containing the RGB values
of red, green and blue, respectively.
: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.
:type rgb: listlike
:type min_opacity: float, int
:type max_opacity: float, int
:type reverse_opacity: bool
:return: transfer_function
:rtype: ipyvolume TransferFunction

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

.. seealso:: matplotlib_transfer_function()
"""
_num_elements = 256 # length of rgba transfer function array
GenevieveBuckley marked this conversation as resolved.
Show resolved Hide resolved
r, g, b = [value/255. for value in rgb_values] # rescales 0-255 to float
opacity = np.linspace(min_opacity, max_opacity, num=_num_elements)
if reverse_opacity: opacity = np.flip(opacity, axis=0)
GenevieveBuckley marked this conversation as resolved.
Show resolved Hide resolved
rgba = np.transpose(np.stack([[r] * _num_elements,
[g] * _num_elements,
[b] * _num_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):
"""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.
:type colormap_name: str
:type min_opacity: float, int
:type max_opacity: float, int
:type reverse_colormap: bool
:type reverse_opacity: bool
: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('viridis')
>>> ds = ipv.datasets.aquariusA2.fetch()
>>> ipv.volshow(ds.data[::4,::4,::4], tf=green_tf)
>>> ipv.show()

.. seealso:: linear_transfer_function()
"""
_num_elements = 256 # length of rgba transfer function array
GenevieveBuckley marked this conversation as resolved.
Show resolved Hide resolved
cmap = matplotlib.cm.get_cmap(name=colormap_name)
rgba = np.array([cmap(i) for i in np.linspace(0, 1, _num_elements)])
if reverse_colormap: rgba = np.flip(rgba, axis=0)
GenevieveBuckley marked this conversation as resolved.
Show resolved Hide resolved
# Create opacity values to overwrite default matplotlib opacity=1.0
opacity = np.linspace(min_opacity, max_opacity, num=_num_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 load_transfer_functions(include_rgb_linear=True,
Copy link
Collaborator

Choose a reason for hiding this comment

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

I would not call it load (get?), and I'd make it 'singleton' like, so it will always return the same widgets, what do you think? (it will basically cache the widgets).
My guess is this will be used as part of a gui to select a particular colormap?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let's call it 'predefined_transfer_functions'.

I'm not sure I understand correctly: do you want this to stay a function but always return the same dictionary, or have me make a singleton class for it?

I have no major plans for a colormap gui. But I do want to set future efforts at a GUI up for success as much as possible, so if you have advice on things I'm doing (or not doing here I'm happy to hear it.

What I want in the short term is a bunch of transfer function widgets so I can easily switch between them programmatically for the multivolume rendering I'm working on now.

Copy link
Collaborator

Choose a reason for hiding this comment

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

What I would like to see, is when a transfer function is created with the same set of parameters (same key for the dictionary), that it returns the same transfer function. That will limit the number of widgets created, what do you think?

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'm not sure how best to implement the idea, but I think it's a good strategy/goal.

Copy link
Collaborator

@maartenbreddels maartenbreddels Oct 1, 2018

Choose a reason for hiding this comment

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

sth like this:

tf_cache = {}
def get_transfer_functions(...)
  tf = tf_cache.get(key)
  if not tf:
    tf = ....
    tf_cache[key] = tf
  ...

include_matplotlib=True,
include_matplotlib_reversed=True):
"""Load predefined transfer functions into a dictionary.

:param include_rgb_linear: load transfer functions from individual
RGB values & linear opacity, optional.
:param include_matplotlib: load transfer functions from matplotlib
colormaps & linear opacity, optional.
:param include_matplotlib_reversed: load transfer functions from
REVERSED matplotlib colormaps, optional.
:type include_rgb_linear: bool
:type include_matplotlib: bool
:type include_matplotlib_reversed: bool
:return: dictionary of predefined transfer functions.
:rtype: dict of ipyvolume TransferFunction instances
"""
transfer_functions = {}
# RGB primary and secondary colors
if include_rgb_linear:
colors = {'grey': (0, 0, 0),
'red': (255, 0, 0),
'green': (0, 255, 0),
'blue': (0, 0, 255),
'yellow': (255, 255, 0),
'magenta': (255, 0, 255),
'cyan': (0, 255, 255)}
for color_key in colors:
rgb = colors[color_key]
tf = linear_transfer_function(rgb)
transfer_functions[color_key] = tf
tf_reversed = linear_transfer_function(rgb, reverse_opacity=True)
transfer_functions[color_key + '_r'] = tf_reversed
# matplotlib colormaps
if include_matplotlib:
matplotlib_colormaps = [m for m in dir(matplotlib.cm) if not m.endswith("_r")]
for colormap in matplotlib_colormaps:
try:
transfer_functions[colormap] = matplotlib_transfer_function(colormap)
except ValueError:
continue
# reversed matplotlib colormaps
if include_matplotlib_reversed:
reversed_matplotlib_colormaps = [m for m in dir(matplotlib.cm) if m.endswith("_r")]
for colormap in reversed_matplotlib_colormaps:
try:
transfer_functions[colormap] = matplotlib_transfer_function(colormap)
except ValueError:
continue
return transfer_functions