# vispy/vispy

da75d28 Aug 21, 2018
7 contributors

### Users who have contributed to this file

1131 lines (970 sloc) 41.6 KB
 # -*- coding: utf-8 -*- # Copyright (c) Vispy Development Team. All Rights Reserved. # Distributed under the (new) BSD License. See LICENSE.txt for more info. from __future__ import division # just to be safe... import inspect import numpy as np from .color_array import ColorArray from ..ext.six import string_types from ..ext.cubehelix import cubehelix from ..ext.husl import husl_to_rgb from ..testing import has_matplotlib import vispy.gloo ############################################################################### # Color maps # Length of the texture map used for luminance to RGBA conversion LUT_len = 1024 # Utility functions for interpolation in NumPy. def _vector_or_scalar(x, type='row'): """Convert an object to either a scalar or a row or column vector.""" if isinstance(x, (list, tuple)): x = np.array(x) if isinstance(x, np.ndarray): assert x.ndim == 1 if type == 'column': x = x[:, None] return x def _vector(x, type='row'): """Convert an object to a row or column vector.""" if isinstance(x, (list, tuple)): x = np.array(x, dtype=np.float32) elif not isinstance(x, np.ndarray): x = np.array([x], dtype=np.float32) assert x.ndim == 1 if type == 'column': x = x[:, None] return x def _find_controls(x, controls=None, clip=None): x_controls = np.clip(np.searchsorted(controls, x) - 1, 0, clip) return x_controls.astype(np.int32) # Normalization def _normalize(x, cmin=None, cmax=None, clip=True): """Normalize an array from the range [cmin, cmax] to [0,1], with optional clipping.""" if not isinstance(x, np.ndarray): x = np.array(x) if cmin is None: cmin = x.min() if cmax is None: cmax = x.max() if cmin == cmax: return .5 * np.ones(x.shape) else: cmin, cmax = float(cmin), float(cmax) y = (x - cmin) * 1. / (cmax - cmin) if clip: y = np.clip(y, 0., 1.) return y # Interpolation functions in NumPy. def _mix_simple(a, b, x): """Mix b (with proportion x) with a.""" x = np.clip(x, 0.0, 1.0) return (1.0 - x)*a + x*b def _interpolate_multi(colors, x, controls): x = x.ravel() n = len(colors) # For each element in x, the control index of its bin's left boundary. x_step = _find_controls(x, controls, n-2) # The length of each bin. controls_length = np.diff(controls).astype(np.float32) # Prevent division by zero error. controls_length[controls_length == 0.] = 1. # Like x, but relative to each bin. _to_clip = x - controls[x_step] _to_clip /= controls_length[x_step] x_rel = np.clip(_to_clip, 0., 1.) return (colors[x_step], colors[x_step + 1], x_rel[:, None]) def mix(colors, x, controls=None): a, b, x_rel = _interpolate_multi(colors, x, controls) return _mix_simple(a, b, x_rel) def smoothstep(edge0, edge1, x): """ performs smooth Hermite interpolation between 0 and 1 when edge0 < x < edge1. """ # Scale, bias and saturate x to 0..1 range x = np.clip((x - edge0)/(edge1 - edge0), 0.0, 1.0) # Evaluate polynomial return x*x*(3 - 2*x) def step(colors, x, controls=None): x = x.ravel() """Step interpolation from a set of colors. x belongs in [0, 1].""" assert (controls[0], controls[-1]) == (0., 1.) ncolors = len(colors) assert ncolors == len(controls) - 1 assert ncolors >= 2 x_step = _find_controls(x, controls, ncolors-1) return colors[x_step, ...] # GLSL interpolation functions. def _glsl_mix(controls=None, colors=None, texture_map_data=None): """Generate a GLSL template function from a given interpolation patterns and control points. Parameters ---------- colors : array-like, shape (n_colors, 4) The control colors used by the colormap. Elements of colors must be convertible to an instance of Color-class. controls : list The list of control points for the given colors. It should be an increasing list of floating-point number between 0.0 and 1.0. The first control point must be 0.0. The last control point must be 1.0. The number of control points depends on the interpolation scheme. texture_map_data : ndarray, shape(texture_len, 4) Numpy array of size of 1D texture lookup data for luminance to RGBA conversion. """ assert (controls[0], controls[-1]) == (0., 1.) ncolors = len(controls) assert ncolors >= 2 assert (texture_map_data is not None) LUT = texture_map_data texture_len = texture_map_data.shape[0] # Perform linear interpolation for each RGBA color component. c_rgba = ColorArray(colors)._rgba x = np.linspace(0.0, 1.0, texture_len) LUT[:, 0, 0] = np.interp(x, controls, c_rgba[:, 0]) LUT[:, 0, 1] = np.interp(x, controls, c_rgba[:, 1]) LUT[:, 0, 2] = np.interp(x, controls, c_rgba[:, 2]) LUT[:, 0, 3] = np.interp(x, controls, c_rgba[:, 3]) s2 = "uniform sampler2D texture2D_LUT;" s = "{\n return texture2D(texture2D_LUT, \ vec2(0.0, clamp(t, 0.0, 1.0)));\n} " return "%s\nvec4 colormap(float t) {\n%s\n}" % (s2, s) def _glsl_step(controls=None, colors=None, texture_map_data=None): assert (controls[0], controls[-1]) == (0., 1.) ncolors = len(controls) - 1 assert ncolors >= 2 assert (texture_map_data is not None) LUT = texture_map_data texture_len = texture_map_data.shape[0] LUT_tex_idx = np.linspace(0.0, 1.0, texture_len) # Replicate indices to colormap texture. # The resulting matrix has size of (texture_len,len(controls)). # It is used to perform piecewise constant interpolation # for each RGBA color component. t2 = np.repeat(LUT_tex_idx[:, np.newaxis], len(controls), 1) # Perform element-wise comparison to find # control points for all LUT colors. bn = np.sum(controls.transpose() >= t2, axis=1) j = np.clip(bn-1, 0, ncolors-1) # Copying color data from ColorArray to array-like # makes data assignment to LUT faster. colors_rgba = ColorArray(colors[:])._rgba LUT[:, 0, :] = colors_rgba[j] s2 = "uniform sampler2D texture2D_LUT;" s = "{\n return texture2D(texture2D_LUT, \ vec2(0.0, clamp(t, 0.0, 1.0)));\n} " return "%s\nvec4 colormap(float t) {\n%s\n}" % (s2, s) # Mini GLSL template system for colors. def _process_glsl_template(template, colors): """Replace \$color_i by color #i in the GLSL template.""" for i in range(len(colors) - 1, -1, -1): color = colors[i] assert len(color) == 4 vec4_color = 'vec4(%.3f, %.3f, %.3f, %.3f)' % tuple(color) template = template.replace('\$color_%d' % i, vec4_color) return template class BaseColormap(object): u"""Class representing a colormap: t in [0, 1] --> rgba_color Parameters ---------- colors : list of lists, tuples, or ndarrays The control colors used by the colormap (shape = (ncolors, 4)). Notes ----- Must be overriden. Child classes need to implement: glsl_map : string The GLSL function for the colormap. Use \$color_0 to refer to the first color in `colors`, and so on. These are vec4 vectors. map(item) : function Takes a (N, 1) vector of values in [0, 1], and returns a rgba array of size (N, 4). """ # Control colors used by the colormap. colors = None # GLSL string with a function implementing the color map. glsl_map = None # Texture map data used by the 'colormap' GLSL function # for luminance to RGBA conversion. texture_map_data = None def __init__(self, colors=None): # Ensure the colors are arrays. if colors is not None: self.colors = colors if not isinstance(self.colors, ColorArray): self.colors = ColorArray(self.colors) # Process the GLSL map function by replacing \$color_i by the if len(self.colors) > 0: self.glsl_map = _process_glsl_template(self.glsl_map, self.colors.rgba) def map(self, item): """Return a rgba array for the requested items. This function must be overriden by child classes. This function doesn't need to implement argument checking on `item`. It can always assume that `item` is a (N, 1) array of values between 0 and 1. Parameters ---------- item : ndarray An array of values in [0,1]. Returns ------- rgba : ndarray An array with rgba values, with one color per item. The shape should be ``item.shape + (4,)``. Notes ----- Users are expected to use a colormap with ``__getitem__()`` rather than ``map()`` (which implements a lower-level API). """ raise NotImplementedError() def __getitem__(self, item): if isinstance(item, tuple): raise ValueError('ColorArray indexing is only allowed along ' 'the first dimension.') # Ensure item is either a scalar or a column vector. item = _vector(item, type='column') # Clip the values in [0, 1]. item = np.clip(item, 0., 1.) colors = self.map(item) return ColorArray(colors) def __setitem__(self, item, value): raise RuntimeError("It is not possible to set items to " "BaseColormap instances.") def _repr_html_(self): n = 100 html = (""" """ + '\n'.join([(("""