diff --git a/.travis.yml b/.travis.yml index 20aa5a41..5e0ea567 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/ipyvolume/pylab.py b/ipyvolume/pylab.py index 95d2d7c6..049fb597 100644 --- a/ipyvolume/pylab.py +++ b/ipyvolume/pylab.py @@ -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 @@ -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. @@ -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) :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') + + widgets_bottom = [ipywidgets.HBox([widget_colorpicker, widget_opacity_scale, widget_brightness])] + current.container.children += tuple(widgets_bottom, ) + + fig.volumes = fig.volumes + [vol] + + return fig def save(filepath, makedirs=True, title=u'IPyVolume Widget', all_states=False, diff --git a/ipyvolume/transferfunction.py b/ipyvolume/transferfunction.py index cc589422..4b30805e 100644 --- a/ipyvolume/transferfunction.py +++ b/ipyvolume/transferfunction.py @@ -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 @@ -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 diff --git a/ipyvolume/widgets.py b/ipyvolume/widgets.py index 783c6eed..d49a0530 100644 --- a/ipyvolume/widgets.py +++ b/ipyvolume/widgets.py @@ -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 @@ -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):