# -*- coding: utf-8 -*- # Copyright 2014 by Forschungszentrum Juelich GmbH # Author: J. Caron # # TODO: Own small package? Use viscm (with colorspacious)? # TODO: Also add cmoceaon "phase" colormap? Make optional (try importing, fall back to RdBu!) """This module provides a number of custom colormaps, which also have capabilities for 3D plotting. If this is the case, the :class:`~.Colormap3D` colormap class is a parent class. In `cmaps`, a number of specialised colormaps is available for convenience. If the default for circular colormaps (used for 3D plotting) should be changed, set it via `CMAP_CMAP_ANGULAR_DEFAULT`. For general questions about colors see: http://www.poynton.com/PDFs/GammaFAQ.pdf http://www.poynton.com/PDFs/ColorFAQ.pdf """ import logging import matplotlib.pyplot as plt from matplotlib.ticker import FuncFormatter as FuncForm from matplotlib.ticker import MaxNLocator, IndexLocator, FixedLocator from mpl_toolkits.mplot3d import Axes3D from mpl_toolkits.axes_grid1 import ImageGrid from matplotlib import gridspec from matplotlib.patches import Circle import numpy as np from PIL import Image from matplotlib import colors from skimage import color as skcolor import colorsys import abc from . import plottools __all__ = ['Colormap3D', 'ColormapCubehelix', 'ColormapPerception', 'ColormapHLS', 'ColormapClassic', 'ColormapTransparent', 'cmaps', 'CMAP_CIRCULAR_DEFAULT', 'ColorspaceCIELab', 'ColorspaceCIELuv', 'ColorspaceCIExyY', 'ColorspaceYPbPr', 'interpolate_color', 'rgb_to_brightness', 'colormap_brightness_comparison'] _log = logging.getLogger(__name__) # TODO: DOCSTRINGS!!! class Colormap3D(colors.Colormap, metaclass=abc.ABCMeta): """Colormap subclass for encoding directions with colors. This abstract class is used as a superclass/interface for 3D vector plotting capabilities. In general, a circular colormap should be used to encode the in-plane angle (hue). The perpendicular angle is encoded via luminance variation (up: white, down: black). Finally, the length of a vector is encoded via saturation. Decreasing vector length causes a desaturated color. Subclassing colormaps get access to routines to plot a colorwheel (which should ideally be located in the 50% luminance plane, which depends strongly on the underlying map), a convenience function to interpolate color tuples and a function to return rgb triples for a given vector. The :class:`~.Colormap3D` class itself subclasses the matplotlib base colormap. """ _log = logging.getLogger(__name__ + '.Colormap3D') def rgb_from_vector(self, vector): """Construct a hls tuple from three coordinates representing a 3D direction. Parameters ---------- vector: tuple (N=3) or :class:`~numpy.ndarray` Vector containing the x, y and z component, or a numpy array encompassing the components as three lists. Returns ------- rgb: :class:`~numpy.ndarray` Numpy array containing the calculated color tuples. """ self._log.debug('Calling rgb_from_vector') x, y, z = np.asarray(vector) # Calculate spherical coordinates: r = np.sqrt(x ** 2 + y ** 2 + z ** 2) phi = np.asarray(np.arctan2(y, x)) phi[phi < 0] += 2 * np.pi theta = np.arccos(z / (r + 1E-30)) # Calculate color deterministics: hue = phi / (2 * np.pi) lum = 1 - theta / np.pi sat = r / (r.max() + 1E-30) # Calculate RGB from hue with colormap: rgba = np.asarray(self(hue)) r, g, b = rgba[..., 0], rgba[..., 1], rgba[..., 2] # Interpolate saturation: r, g, b = interpolate_color(sat, (0.5, 0.5, 0.5), np.stack((r, g, b), axis=-1)) # Interpolate luminance: lum_target = np.where(lum < 0.5, 0, 1) lum_target = np.stack([lum_target] * 3, axis=-1) fraction = np.where(lum < 0.5, 1 - 2 * lum, 2 * (lum - 0.5)) r, g, b = interpolate_color(fraction, np.stack((r, g, b), axis=-1), lum_target) # Return RGB: return np.asarray(255 * np.stack((r, g, b), axis=-1), dtype=np.uint8) def make_colorwheel(self, size=256, alpha=1, bgcolor=None): # TODO: Strange arrows are not straight... self._log.debug('Calling make_colorwheel') # Construct the colorwheel: yy, xx = (np.indices((size, size)) - size/2 + 0.5) rr = np.hypot(xx, yy) xx = np.where(rr <= size/2-2, xx, 0) yy = np.where(rr <= size/2-2, yy, 0) zz = np.where(rr <= size/2-2, 0, -1) # color inside, black outside aa = np.where(rr >= size/2-2, 255*alpha, 255).astype(dtype=np.uint8) rgba = np.dstack((self.rgb_from_vector(np.asarray((xx, yy, zz))), aa)) if bgcolor: if bgcolor == 'w': # TODO: Matplotlib get color tuples from string? bgcolor = (1, 1, 1) if len(bgcolor) == 3 and not isinstance(bgcolor, str): # Only you have tuple! r, g, b = rgba[..., 0], rgba[..., 1], rgba[..., 2] r = np.where(rr <= size / 2 - 2, r, 255*bgcolor[0]).astype(dtype=np.uint8) g = np.where(rr <= size / 2 - 2, g, 255*bgcolor[1]).astype(dtype=np.uint8) b = np.where(rr <= size / 2 - 2, b, 255*bgcolor[2]).astype(dtype=np.uint8) rgba[..., 0], rgba[..., 1], rgba[..., 2] = r, g, b # Create color wheel: return Image.fromarray(rgba) def plot_colorwheel(self, axis=None, size=512, alpha=1, arrows=False, greyscale=False, figsize=(4, 4), bgcolor=None, **kwargs): """Display a color wheel to illustrate the color coding of vector gradient directions. Parameters ---------- figsize : tuple of floats (N=2) Size of the plot figure. Returns ------- None """ self._log.debug('Calling plot_colorwheel') # Construct the colorwheel: color_wheel = self.make_colorwheel(size=size, alpha=alpha, bgcolor=bgcolor) if greyscale: color_wheel = color_wheel.convert('L') # Plot the color wheel: if axis is None: fig = plt.figure(figsize=figsize) axis = fig.add_subplot(1, 1, 1, aspect='equal') axis.imshow(color_wheel, origin='lower', **kwargs) axis.add_artist(Circle(xy=(size/2-0.5, size/2-0.5), radius=size/2-2, linewidth=2, edgecolor='k', facecolor='none')) if arrows: plt.tick_params(axis='both', which='both', labelleft='off', labelbottom='off', left='off', right='off', top='off', bottom='off') axis.arrow(size/2, size/2, 0, 0.15*size, head_width=9, head_length=20, fc='k', ec='k', lw=1, width=2) axis.arrow(size/2, size/2, 0, -0.15*size, head_width=9, head_length=20, fc='k', ec='k', lw=1, width=2) axis.arrow(size/2, size/2, 0.15*size, 0, head_width=9, head_length=20, fc='k', ec='k', lw=1, width=2) axis.arrow(size/2, size/2, -0.15*size, 0, head_width=9, head_length=20, fc='k', ec='k', lw=1, width=2) # Return axis: axis.xaxis.set_visible(False) axis.yaxis.set_visible(False) for tic in axis.xaxis.get_major_ticks(): tic.tick1On = tic.tick2On = False tic.label1On = tic.label2On = False for tic in axis.yaxis.get_major_ticks(): tic.tick1On = tic.tick2On = False tic.label1On = tic.label2On = False return axis class ColormapCubehelix(colors.LinearSegmentedColormap, Colormap3D): """A full implementation of Dave Green's "cubehelix" for Matplotlib. Based on the FORTRAN 77 code provided in D.A. Green, 2011, BASI, 39, 289. http://adsabs.harvard.edu/abs/2011arXiv1108.5083G Also see: http://www.mrao.cam.ac.uk/~dag/CUBEHELIX/ http://davidjohnstone.net/pages/cubehelix-gradient-picker User can adjust all parameters of the cubehelix algorithm. This enables much greater flexibility in choosing color maps. Default color map settings produce the standard cubehelix. Create color map in only blues by setting rot=0 and start=0. Create reverse (white to black) backwards through the rainbow once by setting rot=1 and reverse=True, etc. Furthermore, the algorithm was tuned, so that constant luminance values can be used (e.g. to create a truly isoluminant colorwheel). The `rot` parameter is also tuned to hold true for these cases. Of the here presented colorwheels, only this one manages to solely navigate through the L*=50 plane, which can be seen here: https://upload.wikimedia.org/wikipedia/commons/2/21/Lab_color_space.png Parameters ---------- start : scalar, optional Sets the starting position in the color space. 0=blue, 1=red, 2=green. Defaults to 0.5. rot : scalar, optional The number of rotations through the rainbow. Can be positive or negative, indicating direction of rainbow. Negative values correspond to Blue->Red direction. Defaults to -1.5. gamma : scalar, optional The gamma correction for intensity. Defaults to 1.0. reverse : boolean, optional Set to True to reverse the color map. Will go from black to white. Good for density plots where shade~density. Defaults to False. nlev : scalar, optional Defines the number of discrete levels to render colors at. Defaults to 256. sat : scalar, optional The saturation intensity factor. Defaults to 1.2 NOTE: this was formerly known as `hue` parameter minSat : scalar, optional Sets the minimum-level saturation. Defaults to 1.2. maxSat : scalar, optional Sets the maximum-level saturation. Defaults to 1.2. startHue : scalar, optional Sets the starting color, ranging from [0, 360], as in D3 version by @mbostock. NOTE: overrides values in start parameter. endHue : scalar, optional Sets the ending color, ranging from [0, 360], as in D3 version by @mbostock NOTE: overrides values in rot parameter. minLight : scalar, optional Sets the minimum lightness value. Defaults to 0. maxLight : scalar, optional Sets the maximum lightness value. Defaults to 1. Returns ------- matplotlib.colors.LinearSegmentedColormap object Revisions --------- 2014-04 (@jradavenport) Ported from IDL version 2014-04 (@jradavenport) Added kwargs to enable similar to D3 version, changed name of `hue` parameter to `sat`. 2016-11 (@jan.caron) Added support for isoluminant cubehelices while making sure `rot` works as intended. Decoded the plane-vectors a bit. """ _log = logging.getLogger(__name__ + '.ColormapCubehelix') def __init__(self, start=0.5, rot=-1.5, gamma=1.0, reverse=False, nlev=256, minSat=1.2, maxSat=1.2, minLight=0., maxLight=1., **kwargs): self._log.debug('Calling __init__') # Override start and rot if startHue and endHue are set: if kwargs is not None: if 'startHue' in kwargs: start = (kwargs.get('startHue') / 360. - 1.) * 3. if 'endHue' in kwargs: rot = kwargs.get('endHue') / 360. - start / 3. - 1. if 'sat' in kwargs: minSat = kwargs.get('sat') maxSat = kwargs.get('sat') self.nlev = nlev # Set up the parameters: self.fract = np.linspace(minLight, maxLight, nlev) angle = 2.0 * np.pi * (start / 3.0 + rot * np.linspace(0, 1, nlev)) self.fract = self.fract**gamma satar = np.linspace(minSat, maxSat, nlev) amp = np.asarray(satar * self.fract * (1. - self.fract) / 2) # Set RGB color coefficients (Luma is calculated in RGB Rec.601, so choose those), # the original version of Dave green used (0.30, 0.59, 0.11) and REc.709 is # c709 = (0.2126, 0.7152, 0.0722) but would not produce correct YPbPr Luma. c601 = (0.299, 0.587, 0.114) cr, cg, cb = c601 cw = -0.90649 # Chosen to comply with Dave Greens implementation. k = -1.6158 / cr / cw # k has to balance out cw so nothing gets out of RGB gamut (> 1). # Calculate the vectors v and w spanning the plane of constant perceived intensity. # v and w have to solve v x w = k(cr, cg, cb) (normal vector of the described plane) and # v * w = 0 (scalar product, v and w have to be perpendicular). # 6 unknown and 4 equations --> Chose wb = 0 and wg = cw (constant). v = np.array((k * cr ** 2 * cb / (cw * (cr ** 2 + cg ** 2)), k * cr * cg * cb / (cw * (cr ** 2 + cg ** 2)), -k * cr / cw)) w = np.array((-cw * cg / cr, cw, 0)) # Calculate components: self.red = self.fract + amp * (v[0] * np.cos(angle) + w[0] * np.sin(angle)) self.grn = self.fract + amp * (v[1] * np.cos(angle) + w[1] * np.sin(angle)) self.blu = self.fract + amp * (v[2] * np.cos(angle) + w[2] * np.sin(angle)) # Original formulas with original v and w: # self.red = self.fract + amp * (-0.14861 * np.cos(angle) + 1.78277 * np.sin(angle)) # self.grn = self.fract + amp * (-0.29227 * np.cos(angle) - 0.90649 * np.sin(angle)) # self.blu = self.fract + amp * (1.97294 * np.cos(angle)) # Find where RBG are outside the range [0,1], clip: self.red = np.clip(self.red, 0, 1) self.grn = np.clip(self.grn, 0, 1) self.blu = np.clip(self.blu, 0, 1) # Optional color reverse: if reverse is True: self.red = self.red[::-1] self.blu = self.blu[::-1] self.grn = self.grn[::-1] # Put in to tuple & dictionary structures needed: rr, bb, gg = [], [], [] for k in range(0, int(nlev)): rr.append((float(k) / (nlev - 1), self.red[k], self.red[k])) bb.append((float(k) / (nlev - 1), self.blu[k], self.blu[k])) gg.append((float(k) / (nlev - 1), self.grn[k], self.grn[k])) cdict = {'red': rr, 'blue': bb, 'green': gg} super().__init__('cubehelix', cdict, N=256) self._log.debug('Created ' + str(self)) def plot_helix(self, figsize=None, **kwargs): """Display the RGB and luminance plots for the chosen cubehelix. Parameters ---------- figsize : tuple of floats (N=2) Size of the plot figure. Returns ------- None """ self._log.debug('Calling plot_helix') if figsize is None: figsize = plottools.FIGSIZE_DEFAULT plt.figure(figsize=figsize) gs = gridspec.GridSpec(2, 1, height_ratios=[8, 1]) # Main plot: axis = plt.subplot(gs[0]) axis.plot(self.fract, 'k', linewidth=2) axis.plot(self.red, 'r', linewidth=2) axis.plot(self.grn, 'g', linewidth=2) axis.plot(self.blu, 'b', linewidth=2) axis.set_xlim(0, self.nlev) axis.set_ylim(0, 1) axis.set_title('Cubehelix', fontsize=18) axis.set_xlabel('Color index', fontsize=15) axis.set_ylabel('Brightness / RGB', fontsize=15) axis.xaxis.set_major_locator(FixedLocator(locs=np.linspace(0, self.nlev, 5))) axis.yaxis.set_major_locator(FixedLocator(locs=[0, 0.5, 1])) # Colorbar horizontal: caxis = plt.subplot(gs[1], sharex=axis) rgb = self(np.linspace(0, 1, 256))[None, ...] rgb = np.asarray(255.9999 * rgb, dtype=np.uint8) rgb = np.repeat(rgb, 30, axis=0) im = Image.fromarray(rgb) caxis.imshow(im) plt.tick_params(axis='both', which='both', labelleft='off', labelbottom='off', left='off', right='off', top='on', bottom='on') return plottools.format_axis(axis, scalebar=False, keep_labels=True, **kwargs) class ColormapPerception(colors.LinearSegmentedColormap, Colormap3D): """A perceptual colormap based on face-based luminance matching. Based on a publication by Kindlmann et. al. http://www.cs.utah.edu/~gk/papers/vis02/FaceLumin.pdf This colormap tries to achieve an isoluminant perception by using a list of colors acquired through face recognition studies. It is a lot better than the HLS colormap, but still not completely isoluminant (despite its name). Also it appears a bit dark. """ _log = logging.getLogger(__name__ + '.ColormapPerception') CDICT = {'red': [(0/6, 0.847, 0.847), (1/6, 0.527, 0.527), (2/6, 0.000, 0.000), (3/6, 0.000, 0.000), (4/6, 0.316, 0.316), (5/6, 0.718, 0.718), (6/6, 0.847, 0.847)], 'green': [(0/6, 0.057, 0.057), (1/6, 0.527, 0.527), (2/6, 0.592, 0.592), (3/6, 0.559, 0.559), (4/6, 0.316, 0.316), (5/6, 0.000, 0.000), (6/6, 0.057, 0.057)], 'blue': [(0/6, 0.057, 0.057), (1/6, 0.000, 0.000), (2/6, 0.000, 0.000), (3/6, 0.559, 0.559), (4/6, 0.991, 0.991), (5/6, 0.718, 0.718), (6/6, 0.057, 0.057)]} def __init__(self): self._log.debug('Calling __init__') super().__init__('perception', self.CDICT, N=256) self._log.debug('Created ' + str(self)) class ColormapHLS(colors.ListedColormap, Colormap3D): """Colormap subclass for encoding directions with colors. This class is a subclass of the :class:`~matplotlib.pyplot.colors.ListedColormap` class. The class follows the HSL ('hue', 'saturation', 'lightness') 'Double Hexcone' Model with the saturation always set to 1 (moving on the surface of the color cylinder) with a lightness of 0.5 (full color). The three prime colors (`rgb`) are spaced equidistant with 120° space in between, according to a triadic arrangement. Even though the lightness is constant in the plane, the luminance (which is a weighted sum of the RGB components which encompasses human perception) is not, which can lead to artifacts like reliefs. Converting the map to a grayscale show spokes at the secondary colors. For more information see: https://vis4.net/blog/posts/avoid-equidistant-hsv-colors/ http://www.workwithcolor.com/color-luminance-2233.htm http://blog.asmartbear.com/color-wheels.html """ _log = logging.getLogger(__name__ + '.ColormapHLS') def __init__(self): self._log.debug('Calling __init__') h = np.linspace(0, 1, 256) l = 0.5 * np.ones_like(h) s = np.ones_like(h) r, g, b = np.vectorize(colorsys.hls_to_rgb)(h, l, s) colors = [(r[i], g[i], b[i]) for i in range(len(r))] super().__init__(colors, 'hls', N=256) self._log.debug('Created ' + str(self)) class ColormapClassic(colors.LinearSegmentedColormap, Colormap3D): """Colormap subclass for encoding directions with colors. This class is a subclass of the :class:`~matplotlib.pyplot.colors.LinearSegmentedColormap` class. The class follows the HSL ('hue', 'saturation', 'lightness') 'Double Hexcone' Model with the saturation always set to 1 (moving on the surface of the color cylinder) with a luminance of 0.5 (full color). The colors follow a tetradic arrangement with four colors (red, green, blue and yellow) arranged with 90° spacing in between. """ _log = logging.getLogger(__name__ + '.ColormapClassic') CDICT = {'red': [(0.00, 1.0, 1.0), (0.25, 0.0, 0.0), (0.50, 0.0, 0.0), (0.75, 1.0, 1.0), (1.00, 1.0, 1.0)], 'green': [(0.00, 0.0, 0.0), (0.25, 0.0, 0.0), (0.50, 1.0, 1.0), (0.75, 1.0, 1.0), (1.00, 0.0, 0.0)], 'blue': [(0.00, 0.0, 0.0), (0.25, 1.0, 1.0), (0.50, 0.0, 0.0), (0.75, 0.0, 0.0), (1.00, 0.0, 0.0)]} def __init__(self): self._log.debug('Calling __init__') super().__init__('classic', self.CDICT, N=256) self._log.debug('Created ' + str(self)) class ColormapTransparent(colors.LinearSegmentedColormap): """Colormap subclass for including transparency. This class is a subclass of the :class:`~matplotlib.pyplot.colors.LinearSegmentedColormap` class with integrated support for transparency. The colormap is unicolor and varies only in transparency. Attributes ---------- r: float, optional Intensity of red in the colormap. Has to be between 0. and 1. g: float, optional Intensity of green in the colormap. Has to be between 0. and 1. b: float, optional Intensity of blue in the colormap. Has to be between 0. and 1. alpha_range : list (N=2) of float, optional Start and end alpha value. Has to be between 0. and 1. """ _log = logging.getLogger(__name__ + '.ColormapTransparent') def __init__(self, r=0., g=0., b=0., alpha_range=None): self._log.debug('Calling __init__') if alpha_range is None: alpha_range = [0., 1.] red = [(0., 0., r), (1., r, 1.)] green = [(0., 0., g), (1., g, 1.)] blue = [(0., 0., b), (1., b, 1.)] alpha = [(0., 0., alpha_range[0]), (1., alpha_range[1], 1.)] cdict = {'red': red, 'green': green, 'blue': blue, 'alpha': alpha} super().__init__('transparent', cdict, N=256) self._log.debug('Created ' + str(self)) class ColorspaceCIELab(object): # TODO: Superclass? """Class representing the CIELab colorspace.""" _log = logging.getLogger(__name__ + '.ColorspaceCIELab') def __init__(self, dim=(500, 500), extent=(-100, 100, -100, 100), cut_gamut=False, clip=True): self._log.debug('Calling __init__') self.dim = dim self.extent = extent self.cut_out_gamut = cut_gamut self.clip = clip self._log.debug('Created ' + str(self)) def plot(self, L=53.4, axis=None, figsize=None, **kwargs): self._log.debug('Calling plot') if figsize is None: figsize = plottools.FIGSIZE_DEFAULT dim, ext = self.dim, self.extent # Create Lab colorspace: a = np.linspace(ext[0], ext[1], dim[1]) b = np.linspace(ext[2], ext[3], dim[0]) aa, bb = np.meshgrid(a, b) LL = np.full(dim, L, dtype=int) Lab = np.stack((LL, aa, bb), axis=-1) # Convert to XYZ colorspace: XYZ = skcolor.lab2xyz(Lab) # Convert to RGB colorspace following algorithm from http://www.easyrgb.com/index.php: rgb = skcolor.colorconv._convert(skcolor.colorconv.rgb_from_xyz, XYZ) # Gamma correction (gamma encoding) rgb are now nonlinear (R'G'B')!: mask = rgb > 0.0031308 rgb[mask] = 1.055 * np.power(rgb[mask], 1 / 2.4) - 0.055 rgb[~mask] *= 12.92 # Determine gamut: gamut = np.logical_or(rgb < 0, rgb > 1) gamut = np.sum(gamut, axis=-1, dtype=bool) gamut_mask = np.stack((gamut, gamut, gamut), axis=-1) # Cut out gamut (set out of bound colors to gray) if necessary: if self.cut_out_gamut: rgb[gamut_mask] = 0.5 # Clip out of gamut colors: if self.clip: rgb[rgb < 0] = 0 rgb[rgb > 1] = 1 # Plot colorspace: if axis is None: fig = plt.figure(figsize=figsize) axis = fig.add_subplot(1, 1, 1, aspect='equal') axis.imshow(rgb, origin='lower', interpolation='none', extent=(0, dim[0], 0, dim[1])) axis.contour(gamut, levels=[0], colors='k', linewidths=1.5) axis.set_xlabel('a', fontsize=15) axis.set_ylabel('b', fontsize=15) axis.set_title('CIELab (L = {:g})'.format(L), fontsize=18) axis.xaxis.set_major_locator(FixedLocator(np.linspace(0, dim[1], 5))) axis.yaxis.set_major_locator(FixedLocator(np.linspace(0, dim[0], 5))) fx = FuncForm(lambda x, pos: '{:.3g}'.format(x / dim[1] * (ext[1] - ext[0]) + ext[0])) axis.xaxis.set_major_formatter(fx) fy = FuncForm(lambda y, pos: '{:.3g}'.format(y / dim[0] * (ext[3] - ext[2]) + ext[2])) axis.yaxis.set_major_formatter(fy) plottools.format_axis(axis, scalebar=False, keep_labels=True, **kwargs) def plot_colormap(self, cmap, N=256, L='auto', figsize=None, cbar_lim=None, brightness=True, input_rec=None): self._log.debug('Calling plot_colormap') dim, ext = self.dim, self.extent # Calculate rgb values: rgb = cmap(np.linspace(0, 1, N))[None, :, :3] # These are R'G'B' values! if input_rec == 601: rgb = RGBConverter('Rec601', 'Rec709')(rgb) # Convert to Lab space: Lab = np.squeeze(skcolor.rgb2lab(rgb)) LL, aa, bb = Lab.T aa = (aa - ext[0]) / (ext[1] - ext[0]) * dim[1] bb = (bb - ext[2]) / (ext[3] - ext[2]) * dim[0] # Determine number of images / luma levels: LL_min, LL_max = np.round(np.min(LL), 1), np.round(np.max(LL), 1) if L == 'auto': if LL_max - LL_min < 0.1: # Just one image: L = LL_min else: # Two images: L = np.asarray((LL_max, np.mean(LL), LL_min)) L_list = np.atleast_1d(L) # Determine colorbar limits: if cbar_lim is not None: # Overwrite limits! LL_min, LL_max = cbar_lim elif not brightness or LL_max - LL_min < 0.1: # Just one value, full range for colormap: LL_min, LL_max = 0, 1 # Creat grid: if figsize is None: figsize = (len(L_list) * 5 + 2, 7) fig = plt.figure(figsize=figsize) grid = ImageGrid(fig, 111, nrows_ncols=(1, len(L_list)), axes_pad=0.4, share_all=False, cbar_location="right", cbar_mode="single", cbar_size="5%", cbar_pad=0.25) # Plot: if brightness: c = LL cmap = 'gray' else: c = np.linspace(0, 1, N) for i, axis in enumerate(grid): self.plot(L=L_list[i], axis=axis) im = axis.scatter(aa, bb, c=c, cmap=cmap, edgecolors='none', vmin=LL_min, vmax=LL_max) axis.set_xlim(0, self.dim[1]) axis.set_ylim(0, self.dim[0]) axis.cax.colorbar(im, ticks=np.linspace(LL_min, LL_max, 9)) def plot3d(self, N=9): self._log.debug('Calling plot3d') dim, ext = self.dim, self.extent # Create Lab colorspace: a = np.linspace(ext[0], ext[1], dim[1]) b = np.linspace(ext[2], ext[3], dim[0]) aa, bb = np.meshgrid(a, b) import visvis # TODO: If VisPy is ever ready, switch every plot to that! for i in range(1, N): LL = np.full(dim, i * 100 / N, dtype=int) Lab = np.stack((LL, aa, bb), axis=-1) # Convert to XYZ colorspace: XYZ = skcolor.lab2xyz(Lab) # Convert to RGB colorspace following algorithm from http://www.easyrgb.com/index.php: rgb = skcolor.colorconv._convert(skcolor.colorconv.rgb_from_xyz, XYZ) # Gamma correction (gamma encoding) rgb are now nonlinear (R'G'B')!: mask = rgb > 0.0031308 rgb[mask] = 1.055 * np.power(rgb[mask], 1 / 2.4) - 0.055 rgb[~mask] *= 12.92 # Determine gamut: gamut = np.logical_or(rgb < 0, rgb > 1) gamut = np.sum(gamut, axis=-1, dtype=bool) # Alpha: alpha = 1. a = np.full(dim + (1,), alpha) a *= np.logical_not(gamut[..., None]) rgba = np.asarray(255 * np.dstack((rgb, a)), dtype=np.uint8) # Visvis plot: obj = visvis.functions.surf(aa, bb, i * 100. / N * np.ones_like(aa), rgba, aa=0) obj.parent.light0.ambient = 1. obj.parent.light0.diffuse = 0. class ColorspaceCIELuv(object): """Class representing the CIELuv colorspace.""" _log = logging.getLogger(__name__ + '.ColorspaceCIELuv') def __init__(self, dim=(500, 500), extent=(-100, 100, -100, 100), cut_gamut=False, clip=True): self._log.debug('Calling __init__') self.dim = dim self.extent = extent self.cut_out_gamut = cut_gamut self.clip = clip self._log.debug('Created ' + str(self)) def plot(self, L=53.4, axis=None, figsize=None, **kwargs): self._log.debug('Calling plot') if figsize is None: figsize = plottools.FIGSIZE_DEFAULT dim, ext = self.dim, self.extent # Create Lab colorspace: u = np.linspace(ext[0], ext[1], dim[1]) v = np.linspace(ext[2], ext[3], dim[0]) uu, vv = np.meshgrid(u, v) LL = np.full(dim, L, dtype=int) Luv = np.stack((LL, uu, vv), axis=-1) # Convert to XYZ colorspace: XYZ = skcolor.luv2xyz(Luv) # Convert to RGB colorspace following algorithm from http://www.easyrgb.com/index.php: rgb = skcolor.colorconv._convert(skcolor.colorconv.rgb_from_xyz, XYZ) # Gamma correction (gamma encoding) rgb are now nonlinear (R'G'B')!: mask = rgb > 0.0031308 rgb[mask] = 1.055 * np.power(rgb[mask], 1 / 2.4) - 0.055 rgb[~mask] *= 12.92 # Determine gamut: gamut = np.logical_or(rgb < 0, rgb > 1) gamut = np.sum(gamut, axis=-1, dtype=bool) gamut_mask = np.stack((gamut, gamut, gamut), axis=-1) # Cut out gamut (set out of bound colors to gray) if necessary: if self.cut_out_gamut: rgb[gamut_mask] = 0.5 # Clip out of gamut colors: if self.clip: rgb[rgb < 0] = 0 rgb[rgb > 1] = 1 # Plot colorspace: if axis is None: fig = plt.figure(figsize=figsize) axis = fig.add_subplot(1, 1, 1, aspect='equal') axis.imshow(rgb, origin='lower', interpolation='none', extent=(0, dim[0], 0, dim[1])) axis.contour(gamut, levels=[0], colors='k', linewidths=1.5) axis.set_xlabel('u', fontsize=15) axis.set_ylabel('v', fontsize=15) axis.set_title('CIELuv (L = {:g})'.format(L), fontsize=18) axis.xaxis.set_major_locator(FixedLocator(np.linspace(0, dim[1], 5))) axis.yaxis.set_major_locator(FixedLocator(np.linspace(0, dim[0], 5))) fx = FuncForm(lambda x, pos: '{:.3g}'.format(x / dim[1] * (ext[1] - ext[0]) + ext[0])) axis.xaxis.set_major_formatter(fx) fy = FuncForm(lambda y, pos: '{:.3g}'.format(y / dim[0] * (ext[3] - ext[2]) + ext[2])) axis.yaxis.set_major_formatter(fy) plottools.format_axis(axis, scalebar=False, keep_labels=True, **kwargs) def plot_colormap(self, cmap, N=256, L='auto', figsize=None, cbar_lim=None, brightness=True, input_rec=None): self._log.debug('Calling plot_colormap') dim, ext = self.dim, self.extent # Calculate rgb values: rgb = cmap(np.linspace(0, 1, N))[None, :, :3] if input_rec == 601: rgb = RGBConverter('Rec601', 'Rec709')(rgb) # Convert to Lab space: Luv = np.squeeze(skcolor.rgb2luv(rgb)) LL, uu, vv = Luv.T uu = (uu - ext[0]) / (ext[1] - ext[0]) * dim[1] vv = (vv - ext[2]) / (ext[3] - ext[2]) * dim[0] # Determine number of images / luma levels: LL_min, LL_max = np.round(np.min(LL), 1), np.round(np.max(LL), 1) if L == 'auto': if LL_max - LL_min < 0.1: # Just one image: L = LL_min else: # Two images: L = np.asarray((LL_max, np.mean(LL), LL_min)) L_list = np.atleast_1d(L) # Determine colorbar limits: if cbar_lim is not None: # Overwrite limits! LL_min, LL_max = cbar_lim elif not brightness or LL_max - LL_min < 0.1: # Just one value, full range for colormap: LL_min, LL_max = 0, 1 # Creat grid: if figsize is None: figsize = (len(L_list) * 5 + 2, 7) fig = plt.figure(figsize=figsize) grid = ImageGrid(fig, 111, nrows_ncols=(1, len(L_list)), axes_pad=0.4, share_all=False, cbar_location="right", cbar_mode="single", cbar_size="5%", cbar_pad=0.25) # Plot: if brightness: c = LL cmap = 'gray' else: c = np.linspace(0, 1, N) for i, axis in enumerate(grid): self.plot(L=L_list[i], axis=axis) im = axis.scatter(uu, vv, c=c, cmap=cmap, edgecolors='none', vmin=LL_min, vmax=LL_max) axis.set_xlim(0, self.dim[1]) axis.set_ylim(0, self.dim[0]) axis.cax.colorbar(im, ticks=np.linspace(LL_min, LL_max, 9)) def plot3d(self, N=9): self._log.debug('Calling plot3d') dim, ext = self.dim, self.extent # Create Lab colorspace: u = np.linspace(ext[0], ext[1], dim[1]) v = np.linspace(ext[2], ext[3], dim[0]) uu, vv = np.meshgrid(u, v) import visvis for i in range(1, N): LL = np.full(dim, i * 100 / N, dtype=int) Luv = np.stack((LL, uu, vv), axis=-1) # Convert to XYZ colorspace: XYZ = skcolor.luv2xyz(Luv) # Convert to RGB colorspace following algorithm from http://www.easyrgb.com/index.php: rgb = skcolor.colorconv._convert(skcolor.colorconv.rgb_from_xyz, XYZ) # Gamma correction (gamma encoding) rgb are now nonlinear (R'G'B')!: mask = rgb > 0.0031308 rgb[mask] = 1.055 * np.power(rgb[mask], 1 / 2.4) - 0.055 rgb[~mask] *= 12.92 # Determine gamut: gamut = np.logical_or(rgb < 0, rgb > 1) gamut = np.sum(gamut, axis=-1, dtype=bool) # Alpha: alpha = 1. a = np.full(dim + (1,), alpha) a *= np.logical_not(gamut[..., None]) rgba = np.asarray(255 * np.dstack((rgb, a)), dtype=np.uint8) # Visvis plot: obj = visvis.functions.surf(uu, vv, i * 100. / N * np.ones_like(uu), rgba, aa=0) obj.parent.light0.ambient = 1. obj.parent.light0.diffuse = 0. class ColorspaceCIExyY(object): """Class representing the CIExyY colorspace.""" _log = logging.getLogger(__name__ + '.ColorspaceCIExyY') def __init__(self, dim=(500, 500), extent=(0, 0.8, 0, 0.8), cut_gamut=False, clip=True): self._log.debug('Calling __init__') self.dim = dim self.extent = extent self.cut_out_gamut = cut_gamut self.clip = clip self._log.debug('Created ' + str(self)) def plot(self, Y=0.214, axis=None, figsize=None, **kwargs): self._log.debug('Calling plot') if figsize is None: figsize = plottools.FIGSIZE_DEFAULT dim, ext = self.dim, self.extent # Create Lab colorspace: x = np.linspace(ext[0], ext[1], dim[1]) y = np.linspace(ext[2], ext[3], dim[0]) xx, yy = np.meshgrid(x, y) YY = np.full(dim, Y) # Convert to XYZ: XX = YY / (yy + 1e-30) * xx ZZ = YY / (yy + 1e-30) * (1 - xx - yy) XYZ = np.stack((XX, YY, ZZ), axis=-1) # Convert to RGB colorspace following algorithm from http://www.easyrgb.com/index.php: rgb = skcolor.colorconv._convert(skcolor.colorconv.rgb_from_xyz, XYZ) # Gamma correction (gamma encoding) rgb are now nonlinear (R'G'B')!: mask = rgb > 0.0031308 rgb[mask] = 1.055 * np.power(rgb[mask], 1 / 2.4) - 0.055 rgb[~mask] *= 12.92 # Determine gamut: gamut = np.logical_or(rgb < 0, rgb > 1) gamut = np.sum(gamut, axis=-1, dtype=bool) gamut_mask = np.stack((gamut, gamut, gamut), axis=-1) # Cut out gamut (set out of bound colors to gray) if necessary: if self.cut_out_gamut: rgb[gamut_mask] = 0.5 # Clip out of gamut colors: if self.clip: rgb[rgb < 0] = 0 rgb[rgb > 1] = 1 # Plot colorspace: if axis is None: fig = plt.figure(figsize=figsize) axis = fig.add_subplot(1, 1, 1, aspect='equal') axis.imshow(rgb, origin='lower', interpolation='none', extent=(0, dim[0], 0, dim[1])) axis.contour(gamut, levels=[0], colors='k', linewidths=1.5) axis.set_xlabel('x', fontsize=15) axis.set_ylabel('y', fontsize=15) axis.set_title('CIExyY (Y = {:g})'.format(Y), fontsize=18) axis.xaxis.set_major_locator(FixedLocator(np.linspace(0, dim[1], 5))) axis.yaxis.set_major_locator(FixedLocator(np.linspace(0, dim[0], 5))) fx = FuncForm(lambda x, pos: '{:.3g}'.format(x / dim[1] * (ext[1] - ext[0]) + ext[0])) axis.xaxis.set_major_formatter(fx) fy = FuncForm(lambda y, pos: '{:.3g}'.format(y / dim[0] * (ext[3] - ext[2]) + ext[2])) axis.yaxis.set_major_formatter(fy) plottools.format_axis(axis, scalebar=False, keep_labels=True, **kwargs) def plot_colormap(self, cmap, N=256, Y='auto', figsize=None, cbar_lim=None, brightness=True, input_rec=None): self._log.debug('Calling plot_colormap') dim, ext = self.dim, self.extent # Calculate rgb values: rgb = cmap(np.linspace(0, 1, N))[None, :, :3] if input_rec == 601: rgb = RGBConverter('Rec601', 'Rec709')(rgb) # Convert to XYZ space: XYZ = np.squeeze(skcolor.rgb2xyz(rgb)) XX, YY, ZZ = XYZ.T # Convert to xyY space: xx = XX / (XX + YY + ZZ) yy = YY / (XX + YY + ZZ) xx = (xx - ext[0]) / (ext[1] - ext[0]) * dim[1] yy = (yy - ext[2]) / (ext[3] - ext[2]) * dim[0] # Determine number of images / luma levels: YY_min, YY_max = np.round(np.min(YY), 2), np.round(np.max(YY), 2) if Y == 'auto': if YY_max - YY_min < 0.01: # Just one image: Y = YY_min else: # Two images: Y = np.asarray((YY_max, np.mean(YY), YY_min)) Y_list = np.atleast_1d(Y) # Determine colorbar limits: if cbar_lim is not None: # Overwrite limits! YY_min, YY_max = cbar_lim elif not brightness or YY_max - YY_min < 0.01: # Just one value, full range for colormap: YY_min, YY_max = 0, 1 # Creat grid: if figsize is None: figsize = (len(Y_list) * 5 + 2, 7) fig = plt.figure(figsize=figsize) grid = ImageGrid(fig, 111, nrows_ncols=(1, len(Y_list)), axes_pad=0.4, share_all=False, cbar_location="right", cbar_mode="single", cbar_size="5%", cbar_pad=0.25) # Plot: if brightness: c = YY cmap = 'gray' else: c = np.linspace(0, 1, N) for i, axis in enumerate(grid): self.plot(Y=Y_list[i], axis=axis) im = axis.scatter(xx, yy, c=c, cmap=cmap, edgecolors='none', vmin=YY_min, vmax=YY_max) axis.set_xlim(0, self.dim[1]) axis.set_ylim(0, self.dim[0]) axis.cax.colorbar(im, ticks=np.linspace(YY_min, YY_max, 9)) def plot3d(self, N=9): self._log.debug('Calling plot3d') dim, ext = self.dim, self.extent # Create Lab colorspace: x = np.linspace(ext[0], ext[1], dim[1]) y = np.linspace(ext[2], ext[3], dim[0]) xx, yy = np.meshgrid(x, y) import visvis for i in range(1, N): YY = np.full(dim, i * 1. / N) # Convert to XYZ: XX = YY / (yy + 1e-30) * xx ZZ = YY / (yy + 1e-30) * (1 - xx - yy) XYZ = np.stack((XX, YY, ZZ), axis=-1) # Convert to RGB colorspace following algorithm from http://www.easyrgb.com/index.php: rgb = skcolor.colorconv._convert(skcolor.colorconv.rgb_from_xyz, XYZ) # Gamma correction (gamma encoding) rgb are now nonlinear (R'G'B')!: mask = rgb > 0.0031308 rgb[mask] = 1.055 * np.power(rgb[mask], 1 / 2.4) - 0.055 rgb[~mask] *= 12.92 # Determine gamut: gamut = np.logical_or(rgb < 0, rgb > 1) gamut = np.sum(gamut, axis=-1, dtype=bool) # Alpha: alpha = 1. a = np.full(dim + (1,), alpha) a *= np.logical_not(gamut[..., None]) rgba = np.asarray(255 * np.dstack((rgb, a)), dtype=np.uint8) # Visvis plot: obj = visvis.functions.surf(xx, yy, i / N * np.ones_like(xx), rgba, aa=0) obj.parent.light0.ambient = 1. obj.parent.light0.diffuse = 0. class ColorspaceYPbPr(object): """Class representing the YPbPr colorspace.""" _log = logging.getLogger(__name__ + '.ColorspaceYPbPr') def __init__(self, dim=(500, 500), extent=(-0.8, 0.8, -0.8, 0.8), cut_gamut=False, clip=True): self._log.debug('Calling __init__') self.dim = dim self.extent = extent self.cut_out_gamut = cut_gamut self.clip = clip self._log.debug('Created ' + str(self)) def plot(self, Y=0.5, axis=None, figsize=None, **kwargs): self._log.debug('Calling plot') if figsize is None: figsize = plottools.FIGSIZE_DEFAULT dim, ext = self.dim, self.extent # Create YPbPr colorspace: pb = np.linspace(ext[0], ext[1], dim[1]) pr = np.linspace(ext[2], ext[3], dim[0]) ppb, ppr = np.meshgrid(pb, pr) YY = np.full(dim, Y) # This is luma, not relative luminance (Y', not Y)! # Convert to RGB colorspace (this is the nonlinear R'G'B' space!): rr = YY + 1.402 * ppr gg = YY - 0.344136 * ppb - 0.7141136 * ppr bb = YY + 1.772 * ppb rgb = np.stack((rr, gg, bb), axis=-1) # Determine gamut: gamut = np.logical_or(rgb < 0, rgb > 1) gamut = np.sum(gamut, axis=-1, dtype=bool) gamut_mask = np.stack((gamut, gamut, gamut), axis=-1) # Cut out gamut (set out of bound colors to gray) if necessary: if self.cut_out_gamut: rgb[gamut_mask] = 0.5 # Clip out of gamut colors: if self.clip: rgb[rgb < 0] = 0 rgb[rgb > 1] = 1 # Plot colorspace: if axis is None: fig = plt.figure(figsize=figsize) axis = fig.add_subplot(1, 1, 1, aspect='equal') axis.imshow(rgb, origin='lower', interpolation='none', extent=(0, dim[0], 0, dim[1])) axis.contour(gamut, levels=[0], colors='k', linewidths=1.5) axis.set_xlabel('Pb', fontsize=15) axis.set_ylabel('Pr', fontsize=15) axis.set_title("Y'PbPr (Y' = {:g})".format(Y), fontsize=18) axis.xaxis.set_major_locator(FixedLocator(np.linspace(0, dim[1], 5))) axis.yaxis.set_major_locator(FixedLocator(np.linspace(0, dim[0], 5))) fx = FuncForm(lambda x, pos: '{:.3g}'.format(x / dim[1] * (ext[1] - ext[0]) + ext[0])) axis.xaxis.set_major_formatter(fx) fy = FuncForm(lambda y, pos: '{:.3g}'.format(y / dim[0] * (ext[3] - ext[2]) + ext[2])) axis.yaxis.set_major_formatter(fy) plottools.format_axis(axis, scalebar=False, keep_labels=True, **kwargs) def plot_colormap(self, cmap, N=256, Y='auto', figsize=None, cbar_lim=None, brightness=True, input_rec=None): self._log.debug('Calling plot_colormap') dim, ext = self.dim, self.extent # Calculate rgb values: rgb = cmap(np.linspace(0, 1, N))[None, :, :3] if input_rec == 709: rgb = RGBConverter('Rec709', 'Rec601')(rgb) rr, gg, bb = rgb.T # Convert to YPbPr space: k_r, k_g, k_b = 0.299, 0.587, 0.114 # Constants Rec.601! YY = k_r * rr + k_g * gg + k_b * bb ppb = (bb - YY) / (2 * (1 - k_b)) ppr = (rr - YY) / (2 * (1 - k_r)) ppb = (ppb - ext[0]) / (ext[1] - ext[0]) * dim[1] ppr = (ppr - ext[2]) / (ext[3] - ext[2]) * dim[0] # Determine number of images / luma levels: YY_min, YY_max = np.round(np.min(YY), 2), np.round(np.max(YY), 2) if Y == 'auto': if YY_max - YY_min < 0.01: # Just one image: Y = YY_min else: # Two images: Y = np.asarray((YY_max, np.mean(YY), YY_min)) Y_list = np.atleast_1d(Y) # Determine colorbar limits: if cbar_lim is not None: # Overwrite limits! YY_min, YY_max = cbar_lim elif not brightness or YY_max - YY_min < 0.01: # Just one value, full range for colormap: YY_min, YY_max = 0, 1 # Creat grid: if figsize is None: figsize = (len(Y_list) * 5 + 2, 7) fig = plt.figure(figsize=figsize) grid = ImageGrid(fig, 111, nrows_ncols=(1, len(Y_list)), axes_pad=0.4, share_all=False, cbar_location="right", cbar_mode="single", cbar_size="5%", cbar_pad=0.25) # Plot: if brightness: c = YY cmap = 'gray' else: c = np.linspace(0, 1, N) for i, axis in enumerate(grid): self.plot(Y=Y_list[i], axis=axis) im = axis.scatter(ppb, ppr, c=c, cmap=cmap, edgecolors='none', vmin=YY_min, vmax=YY_max) axis.set_xlim(0, self.dim[1]) axis.set_ylim(0, self.dim[0]) axis.cax.colorbar(im, ticks=np.linspace(YY_min, YY_max, 9)) def plot3d(self, N=9): self._log.debug('Calling plot3d') dim, ext = self.dim, self.extent # Create YPbPr colorspace: pb = np.linspace(ext[0], ext[1], dim[1]) pr = np.linspace(ext[2], ext[3], dim[0]) ppb, ppr = np.meshgrid(pb, pr) import visvis for i in range(1, N): YY = np.full(dim, i * 1. / N) # This is luma, not relative luminance (Y', not Y)! # Convert to RGB colorspace (this is the nonlinear R'G'B' space!): rr = YY + 1.402 * ppr gg = YY - 0.344136 * ppb - 0.7141136 * ppr bb = YY + 1.772 * ppb rgb = np.stack((rr, gg, bb), axis=-1) # Determine gamut: gamut = np.logical_or(rgb < 0, rgb > 1) gamut = np.sum(gamut, axis=-1, dtype=bool) # Alpha: alpha = 1. a = np.full(dim + (1,), alpha) a *= np.logical_not(gamut[..., None]) rgba = np.asarray(255 * np.dstack((rgb, a)), dtype=np.uint8) # Visvis plot: obj = visvis.functions.surf(ppb, ppr, i / N * np.ones_like(ppb), rgba, aa=0) obj.parent.light0.ambient = 1. obj.parent.light0.diffuse = 0. class RGBConverter(object): """Class for the conversion of RGB values from one RGB space to another. Notes ----- This operates only on NONLINEAR R'G'B' values, normalised to a range of [0, 1]! Convert from linear RGB values beforehand, if necessary! """ rgb601_to_ypbpr = np.array([[+0.299000, +0.587000, +0.114000], [-0.168736, -0.331264, +0.500000], [+0.500000, -0.418688, -0.081312]]) ypbr_to_rgb709 = np.array([[1, +0.0000, +1.5701], [1, -0.1870, -0.4664], [1, +1.8556, +0.0000]]) rgb601_to_rgb709 = ypbr_to_rgb709.dot(rgb601_to_ypbpr) rgb709_to_rgb601 = np.linalg.inv(rgb601_to_rgb709) def __init__(self, source='Rec601', target='Rec709'): if source == 'Rec601' and target == 'Rec709': self.convert_matrix = self.rgb601_to_rgb709 elif source == 'Rec709' and target == 'Rec601': self.convert_matrix = self.rgb709_to_rgb601 else: raise KeyError('Conversion from {} to {} not found!'.format(source, target)) def __call__(self, rgb): """Convert from one RGB space to another. Parameters ---------- rgb: :class:`~numpy.ndarray` Numpy array containing the RGB source values (last dimension: 3). Returns ------- rgb_result: :class:`~numpy.ndarray` The resulting RGB values in the target space. """ rgb_out = rgb.reshape((-1, 3)).T rgb_out = self.convert_matrix.dot(rgb_out) return rgb_out.T.reshape(rgb.shape) def interpolate_color(fraction, start, end): """Interpolate linearly between two color tuples (e.g. RGB). Parameters ---------- fraction: float or :class:`~numpy.ndarray` Interpolation fraction between 0 and 1, which determines the position of the interpolation between `start` and `end`. start: tuple (N=3) or :class:`~numpy.ndarray` Start of the interpolation as a tuple of three numbers or a numpy array, where the last dimension should have length 3 and contain the color tuples. end: tuple (N=3) or :class:`~numpy.ndarray` End of the interpolation as a tuple of three numbers or a numpy array, where the last dimension should have length 3 and contain the color tuples. Returns ------- result: tuple (N=3) or :class:`~numpy.ndarray` Result of the interpolation as a tuple of three numbers or a numpy array, where the last dimension should has length 3 and contains the color tuples. """ _log.debug('Calling interpolate_color') start, end = np.asarray(start), np.asarray(end) r1 = start[..., 0] + (end[..., 0] - start[..., 0]) * fraction r2 = start[..., 1] + (end[..., 1] - start[..., 1]) * fraction r3 = start[..., 2] + (end[..., 2] - start[..., 2]) * fraction return r1, r2, r3 def rgb_to_brightness(rgb, mode="Y'", input_rec=None): import colorspacious # TODO: Use for everything! c = {601: [0.299, 0.587, 0.114], 709: [0.2125, 0.7154, 0.0721]} # Image.convert('L') uses 601! if input_rec is None: # Not specified, use in all cases: rgbp601 = rgb rgbp709 = rgb elif input_rec == 601: rgbp601 = rgb rgbp709 = RGBConverter('Rec601', 'Rec709')(rgb) elif input_rec == 709: rgbp601 = RGBConverter('Rec601', 'Rec709')(rgb) rgbp709 = rgb else: raise KeyError('Input RGB type {} not understood!'.format(input_rec)) if mode in ("Y'", 'Luma'): rp601, gp601, bp601 = rgbp601.T brightness = c[601][0] * rp601 + c[601][1] * gp601 + c[601][2] * bp601 elif mode in ('Y', 'Luminance'): rgb709 = colorspacious.cspace_converter('sRGB1', 'sRGB1-linear')(rgbp709) r709, g709, b709 = rgb709.T brightness = c[709][0] * r709 + c[709][1] * g709 + c[709][2] * b709 elif mode in ('L*', 'LightnessLab'): lab = colorspacious.cspace_converter('sRGB1', 'CIELab')(rgbp709) brightness = lab[0, :, 0] elif mode in ('I', 'Intensity', 'Average'): brightness = np.mean(rgb, axis=-1) elif mode in ('V', 'Value', 'Maximum'): brightness = np.max(rgb, axis=-1) elif mode in ('L', 'LightnessHSL'): brightness = (np.max(rgb, axis=-1) + np.min(rgb, axis=-1)) / 2 else: raise KeyError('Brightness request {} not understood!'.format(mode)) return brightness def colormap_brightness_comparison(cmap, input_rec=None, figsize=(18, 8)): # Create R'G'B' values from colormap: x = np.linspace(0, 1, 1000) rgbp = cmap(x)[None, :, :3] # Calculate different brightness measures: luma = rgb_to_brightness(rgbp, mode="Y'", input_rec=input_rec) luminance = rgb_to_brightness(rgbp, mode='Y', input_rec=input_rec) lightness_lab = rgb_to_brightness(rgbp, mode='L*', input_rec=input_rec) intensity = rgb_to_brightness(rgbp, mode='I', input_rec=input_rec) value = rgb_to_brightness(rgbp, mode='V', input_rec=input_rec) lightness_hls = rgb_to_brightness(rgbp, mode='L', input_rec=input_rec) # Plot: fig, grid = plt.subplots(2, 3, figsize=figsize) plt.title(cmap.name) axis = grid[0, 0] axis.scatter(x, luma, c=x, cmap=cmap, s=200, linewidths=0.) axis.axhline(y=0.5, color='k', ls='--') axis.set_xlim(0, 1) axis.set_ylim(0, 1) axis.set_title("Luma $Y$ '") axis = grid[0, 1] axis.scatter(x, luminance, c=x, cmap=cmap, s=200, linewidths=0.) axis.axhline(y=0.214, color='k', ls='--') axis.set_xlim(0, 1) axis.set_ylim(0, 1) axis.set_title('Relative Luminance $Y$') axis = grid[0, 2] axis.scatter(x, lightness_lab, c=x, cmap=cmap, s=200, linewidths=0.) axis.axhline(y=53.39, color='k', ls='--') axis.set_xlim(0, 1) axis.set_ylim(0, 100) axis.set_title('Lightness $L^*$ (CIELab)') axis = grid[1, 0] axis.scatter(x, intensity, c=x, cmap=cmap, s=200, linewidths=0.) axis.axhline(y=53.39, color='k', ls='--') axis.set_xlim(0, 1) axis.set_ylim(0, 1) axis.set_title('Intensity $I$ (HSI Component Average)') axis = grid[1, 1] axis.scatter(x, value, c=x, cmap=cmap, s=200, linewidths=0.) axis.axhline(y=53.39, color='k', ls='--') axis.set_xlim(0, 1) axis.set_ylim(0, 1) axis.set_title('Value $V$ (HSV Component Maximum)') axis = grid[1, 2] axis.scatter(x, lightness_hls, c=x, cmap=cmap, s=200, linewidths=0.) axis.axhline(y=53.39, color='k', ls='--') axis.set_xlim(0, 1) axis.set_ylim(0, 1) axis.set_title('Lightness $L$ (HSL Min-Max-Average)') cmaps = {'cubehelix_standard': ColormapCubehelix(), 'cubehelix_reverse': ColormapCubehelix(reverse=True), 'cubehelix_circular': ColormapCubehelix(start=1, rot=1, minLight=0.5, maxLight=0.5, sat=2), 'perception_circular': ColormapPerception(), 'hls_circular': ColormapHLS(), 'classic_circular': ColormapClassic(), 'transparent_black': ColormapTransparent(0, 0, 0, [0, 1.]), 'transparent_white': ColormapTransparent(1, 1, 1, [0, 1.]), 'transparent_confidence': ColormapTransparent(0.2, 0.3, 0.2, [0.75, 0.])} CMAP_CIRCULAR_DEFAULT = cmaps['cubehelix_circular']