Source code for kwimage.im_color

"""
A class to make it easier to work with single colors.
"""

import numpy as np
import ubelt as ub
from . import im_core
from . import _im_color_data

__all__ = ['Color']

__todo__ = """

    - [ ] This class badly needs a rewrite to conform to the fast init / easy
          coerce constructor paradigm.

    - [ ] Make init faster an coerce more general

    - [ ] The init should take a color tuple / list / array, unmodified
          as well requiring a format argument, which might be inferred by
          coerce.

    - [ ] Should have operations to convert to/from different internal formats.

    - [ ] Keep old auto-coercing init, but have a way to disable it by default.

"""

BASE_COLORS = _im_color_data.BASE_COLORS
TABLEAU_COLORS = _im_color_data.TABLEAU_COLORS
XKCD_COLORS = _im_color_data.XKCD_COLORS
CSS4_COLORS = _im_color_data.CSS4_COLORS
KITWARE_COLORS = _im_color_data.KITWARE_COLORS


def _lookup_colorspace_object(space):
    from colormath import color_objects
    if space == 'rgb':
        cls = color_objects.AdobeRGBColor
    elif space == 'lab':
        cls = color_objects.LabColor
    elif space == 'hsv':
        cls = color_objects.HSVColor
    elif space == 'luv':
        cls = color_objects.LuvColor
    elif space == 'cmyk':
        cls = color_objects.CMYKColor
    elif space == 'cmy':
        cls = color_objects.CMYColor
    elif space == 'xyz':
        cls = color_objects.XYZColor
    else:
        raise KeyError(space)
    return cls


def _colormath_convert(src_color, src_space, dst_space):
    """
    Uses colormath to convert colors

    Example:
        >>> # xdoctest: +REQUIRES(module:colormath)
        >>> import kwimage
        >>> from kwimage.im_color import _colormath_convert
        >>> src_color = kwimage.Color('turquoise').as01()
        >>> print('src_color = {}'.format(ub.urepr(src_color, nl=0, precision=2)))
        >>> src_space = 'rgb'
        >>> dst_space = 'lab'
        >>> lab_color = _colormath_convert(src_color, src_space, dst_space)
        ...
        >>> print('lab_color = {}'.format(ub.urepr(lab_color, nl=0, precision=2)))
        lab_color = (78.11, -70.09, -9.33)
        >>> rgb_color = _colormath_convert(lab_color, 'lab', 'rgb')
        >>> print('rgb_color = {}'.format(ub.urepr(rgb_color, nl=0, precision=2)))
        rgb_color = (0.29, 0.88, 0.81)
        >>> hsv_color = _colormath_convert(lab_color, 'lab', 'hsv')
        >>> print('hsv_color = {}'.format(ub.urepr(hsv_color, nl=0, precision=2)))
        hsv_color = (175.39, 1.00, 0.88)
    """
    from colormath.color_conversions import convert_color
    src_cls = _lookup_colorspace_object(src_space)
    dst_cls = _lookup_colorspace_object(dst_space)
    src = src_cls(*src_color)
    dst = convert_color(src, dst_cls)
    dst_color = dst.get_value_tuple()
    return dst_color


[docs] class Color(ub.NiceRepr): """ Used for converting a single color between spaces and encodings. This should only be used when handling small numbers of colors(e.g. 1), don't use this to represent an image. Args: space (str): colorspace of wrapped color. Assume RGB if not specified and it cannot be inferred CommandLine: xdoctest -m ~/code/kwimage/kwimage/im_color.py Color Example: >>> print(Color('g')) >>> print(Color('orangered')) >>> print(Color('#AAAAAA').as255()) >>> print(Color([0, 255, 0])) >>> print(Color([1, 1, 1.])) >>> print(Color([1, 1, 1])) >>> print(Color(Color([1, 1, 1])).as255()) >>> print(Color(Color([1., 0, 1, 0])).ashex()) >>> print(Color([1, 1, 1], alpha=255)) >>> print(Color([1, 1, 1], alpha=255, space='lab')) """ def __init__(self, color, alpha=None, space=None, coerce=True): """ Args: color (Color | Iterable[int | float] | str): something coercable into a color alpha (float | None): if psecified adds an alpha value space (str): The colorspace to interpret this color as. Defaults to rgb. coerce (bool): The exsting init is not lightweight. This is a design problem that will need to be fixed in future versions. Setting coerce=False will disable all magic and use imputed color and space args directly. Alpha will be ignored. """ if coerce: try: # Hack for ipython reload is_color_cls = color.__class__.__name__ == 'Color' except Exception: is_color_cls = isinstance(color, Color) if is_color_cls: assert alpha is None assert space is None space = color.space color = color.color01 else: # FIXME: This is a bad check, and it's hard to fix given that there # is lots of code that likely depends on this now. We should not be # doing coercion in an `__init__`, which should alway be # lightweight. The check_inputs=False can be used to disable this # explicitly as a workaround. color = self._ensure_color01(color) if alpha is not None: alpha = self._ensure_color01([alpha])[0] if space is None: space = 'rgb' # always normalize the color down to 01 color01 = list(color) if alpha is not None: if len(color01) not in [1, 3]: raise ValueError('alpha already in color') color01 = color01 + [alpha] # correct space if alpha is given if len(color01) in [2, 4]: if not space.endswith('a'): space += 'a' else: color01 = color space = space # FIXME: color01 is not a good name because the data wont be between 0 # and 1 for non-rgb spaces. We should differentiate between rgb01 and # rgb255. self.color01 = color01 self.space = space
[docs] @classmethod def coerce(cls, data, **kwargs): return cls(data, **kwargs)
def __nice__(self): colorpart = ', '.join(['{:.2f}'.format(c) for c in self.color01]) return self.space + ': ' + colorpart
[docs] def forimage(self, image, space='auto'): """ Return a numeric value for this color that can be used in the given image. Create a numeric color tuple that agrees with the format of the input image (i.e. float or int, with 3 or 4 channels). Args: image (ndarray): image to return color for space (str): colorspace of the input image. Defaults to 'auto', which will choose rgb or rgba Returns: Tuple[Number, ...]: the color value Example: >>> import kwimage >>> img_f3 = np.zeros([8, 8, 3], dtype=np.float32) >>> img_u3 = np.zeros([8, 8, 3], dtype=np.uint8) >>> img_f4 = np.zeros([8, 8, 4], dtype=np.float32) >>> img_u4 = np.zeros([8, 8, 4], dtype=np.uint8) >>> kwimage.Color('red').forimage(img_f3) (1.0, 0.0, 0.0) >>> kwimage.Color('red').forimage(img_f4) (1.0, 0.0, 0.0, 1.0) >>> kwimage.Color('red').forimage(img_u3) (255, 0, 0) >>> kwimage.Color('red').forimage(img_u4) (255, 0, 0, 255) >>> kwimage.Color('red', alpha=0.5).forimage(img_f4) (1.0, 0.0, 0.0, 0.5) >>> kwimage.Color('red', alpha=0.5).forimage(img_u4) (255, 0, 0, 127) >>> kwimage.Color('red').forimage(np.uint8) (255, 0, 0) """ if space == 'auto': space = 'rgb' try: kind = image.dtype.kind except AttributeError: kind = np.dtype(image).kind if len(self.color01) == 4: if not space.endswith('a'): space = space + 'a' else: if im_core.num_channels(image) == 4: if not space.endswith('a'): space = space + 'a' if kind == 'f': color = self.as01(space) else: color = self.as255(space) return color
[docs] def _forimage(self, image, space='rgb'): """ backwards compat, deprecate """ ub.schedule_deprecation( 'kwimage', 'Color._forimage', 'method', migration='Use forimage instead', deprecate='0.10.0', error='1.0.0', remove='1.1.0') return self.forimage(image, space)
[docs] def ashex(self, space=None): """ Convert to hex values Args: space (None | str): if specified convert to this colorspace before returning Returns: str: the hex representation """ c255 = self.as255(space) return '#' + ''.join(['{:02x}'.format(c) for c in c255])
[docs] def as255(self, space=None): """ Convert to byte values Args: space (None | str): if specified convert to this colorspace before returning Returns: Tuple[int, int, int] | Tuple[int, int, int, int]: The uint8 tuple of color values between 0 and 255. """ # TODO: be more efficient about not changing to 01 space color = tuple(int(c * 255) for c in self.as01(space)) return color
[docs] def as01(self, space=None): """ Convert to float values Args: space (None | str): if specified convert to this colorspace before returning Returns: Tuple[float, float, float] | Tuple[float, float, float, float]: The float tuple of color values between 0 and 1 Note: This function is only guarenteed to return 0-1 values for rgb values. For HSV and LAB, the native spaces are used. This is not ideal, and we may create a new function that fixes this - at least conceptually - and deprate this for that in the future. For HSV, H is between 0 and 360. S, and V are in [0, 1] """ color = tuple(map(float, self.color01)) if space is not None: if space == self.space: pass elif space == 'rgb' and self.space == 'rgba': color = color[0:3] elif space == 'rgba' and self.space == 'rgb': color = color + (1.0,) elif space == 'bgr' and self.space == 'rgb': color = color[::-1] elif space == 'rgb' and self.space == 'bgr': color = color[::-1] elif space == 'lab' and self.space == 'rgb': # Note: in this case we will not get a 0-1 normalized color. # because lab does not natively exist in the 0-1 space. color = _colormath_convert(color, 'rgb', 'lab') elif space == 'hsv' and self.space == 'rgb': color = _colormath_convert(color, 'rgb', 'hsv') elif space == 'hsv' and self.space == 'rgba': rgba = color color = _colormath_convert(rgba[0:3], 'rgb', 'hsv') + rgba[3:4] elif space == 'rgb' and self.space == 'hsv': color = _colormath_convert(color, 'hsv', 'rgb') elif space == 'rgb' and self.space == 'lab': color = _colormath_convert(color, 'lab', 'rgb') else: # from colormath import color_conversions raise NotImplementedError('{} -> {}'.format(self.space, space)) return color
[docs] @classmethod def _is_base01(channels): """ check if a color is in base 01 """ def _test_base01(channels): tests01 = { 'is_float': all([isinstance(c, (float, np.float64)) for c in channels]), 'is_01': all([c >= 0.0 and c <= 1.0 for c in channels]), } return tests01 if isinstance(channels, str): return False return all(_test_base01(channels).values())
[docs] @classmethod def _is_base255(Color, channels): """ there is a one corner case where all pixels are 1 or less """ if (all(c > 0.0 and c <= 255.0 for c in channels) and any(c > 1.0 for c in channels)): # Definately in 255 space return True else: # might be in 01 or 255 return all(isinstance(c, int) for c in channels)
[docs] @classmethod def _hex_to_01(Color, hex_color): """ hex_color = '#6A5AFFAF' """ assert hex_color.startswith('#'), 'not a hex string %r' % (hex_color,) parts = hex_color[1:].strip() color255 = tuple(int(parts[i: i + 2], 16) for i in range(0, len(parts), 2)) assert len(color255) in [3, 4], 'must be length 3 or 4' return Color._255_to_01(color255)
[docs] def _ensure_color01(Color, color): """ Infer what type color is and normalize to 01 """ if isinstance(color, str): color = Color._string_to_01(color) elif Color._is_base255(color): color = Color._255_to_01(color) return color
[docs] @classmethod def _255_to_01(Color, color255): """ converts base 255 color to base 01 color """ return [channel / 255.0 for channel in color255]
[docs] @classmethod def _string_to_01(Color, color): """ Ignore: mplutil.Color._string_to_01('green') mplutil.Color._string_to_01('red') """ if color == 'random': import random ub.schedule_deprecation( 'kwimage', 'Color._string_to_01 with random', 'method', migration='Use Color.random instead', deprecate='0.10.0', error='1.0.0', remove='1.1.0') color = random.choice(Color.named_colors()) if color in BASE_COLORS: color01 = BASE_COLORS[color] elif color in CSS4_COLORS: color_hex = CSS4_COLORS[color] color01 = Color._hex_to_01(color_hex) elif color in XKCD_COLORS: color_hex = XKCD_COLORS[color] color01 = Color._hex_to_01(color_hex) elif color in KITWARE_COLORS: color_hex = KITWARE_COLORS[color] color01 = Color._hex_to_01(color_hex) elif color.startswith('#'): color01 = Color._hex_to_01(color) else: raise ValueError('unknown color=%r' % (color,)) return color01
[docs] @classmethod def named_colors(cls): """ Returns: List[str]: names of colors that Color accepts Example: >>> import kwimage >>> named_colors = kwimage.Color.named_colors() >>> color_lut = {name: kwimage.Color(name).as01() for name in named_colors} >>> # xdoctest: +REQUIRES(module:kwplot) >>> # xdoctest: +REQUIRES(--show) >>> import kwplot >>> kwplot.autompl() >>> # This is a very big table if we let it be, reduce it >>> color_lut =dict(list(color_lut.items())[0:10]) >>> canvas = kwplot.make_legend_img(color_lut) >>> kwplot.imshow(canvas) """ NAMED_COLORS = set(BASE_COLORS) | set(CSS4_COLORS) | set(XKCD_COLORS) | set(KITWARE_COLORS) names = sorted(NAMED_COLORS) return names
[docs] @classmethod def distinct(Color, num, existing=None, space='rgb', legacy='auto', exclude_black=True, exclude_white=True): """ Make multiple distinct colors. The legacy variant is based on a stack overflow post [HowToDistinct]_, but the modern variant is based on the :mod:`distinctipy` package. References: .. [HowToDistinct] https://stackoverflow.com/questions/470690/how-to-automatically-generate-n-distinct-colors .. [ColorLimits] https://graphicdesign.stackexchange.com/questions/3682/where-can-i-find-a-large-palette-set-of-contrasting-colors-for-coloring-many-d .. [WikiDistinguish] https://en.wikipedia.org/wiki/Help:Distinguishable_colors .. [Disinct2] https://ux.stackexchange.com/questions/17964/how-many-visually-distinct-colors-can-accurately-be-associated-with-a-separate TODO: - [ ] If num is more than a threshold we should switch to a different strategy to generating colors that just samples uniformly from some colormap and then shuffles. We have no hope of making things distinguishable when num starts going over 10 or so. See [ColorLimits]_ [WikiDistinguish]_ [Disinct2]_ for more ideas. Returns: List[Tuple]: list of distinct float color values Example: >>> # xdoctest: +REQUIRES(module:matplotlib) >>> from kwimage.im_color import * # NOQA >>> import kwimage >>> colors1 = kwimage.Color.distinct(5, legacy=False) >>> colors2 = kwimage.Color.distinct(3, existing=colors1) >>> # xdoctest: +REQUIRES(module:kwplot) >>> # xdoctest: +REQUIRES(--show) >>> from kwimage.im_color import _draw_color_swatch >>> swatch1 = _draw_color_swatch(colors1, cellshape=9) >>> swatch2 = _draw_color_swatch(colors1 + colors2, cellshape=9) >>> import kwplot >>> kwplot.autompl() >>> kwplot.imshow(swatch1, pnum=(1, 2, 1), fnum=1) >>> kwplot.imshow(swatch2, pnum=(1, 2, 2), fnum=1) >>> kwplot.show_if_requested() """ if legacy == 'auto': legacy = (existing is None) if legacy: import matplotlib as mpl import matplotlib._cm as _cm assert existing is None # Old behavior cm = mpl.colors.LinearSegmentedColormap.from_list( 'gist_rainbow', _cm.datad['gist_rainbow'], mpl.rcParams['image.lut']) distinct_colors = [ np.array(cm(i / num)).tolist()[0:3] for i in range(num) ] if space == 'rgb': return distinct_colors else: return [Color(c, space='rgb').as01(space=space) for c in distinct_colors] else: from distinctipy import distinctipy if space != 'rgb': raise NotImplementedError exclude_colors = existing if exclude_colors is None: exclude_colors = [] if exclude_black: exclude_colors = exclude_colors + [(0., 0., 0.)] if exclude_white: exclude_colors = exclude_colors + [(1., 1., 1.)] # convert string to int for seed seed = int(ub.hash_data(exclude_colors, base=10)) + num distinct_colors = distinctipy.get_colors( num, exclude_colors=exclude_colors, rng=seed) distinct_colors = [tuple(map(float, c)) for c in distinct_colors] return distinct_colors if space == 'rgb': return distinct_colors else: return [Color(c, space='rgb').as01(space=space) for c in distinct_colors]
[docs] @classmethod def random(Color, pool='named', with_alpha=0, rng=None): """ Returns: Color """ import kwarray rng = kwarray.ensure_rng(rng, api='python') if pool == 'named': color_name = rng.choice(Color.named_colors()) color = Color._string_to_01(color_name) else: raise NotImplementedError if with_alpha: color = color + [rng.random()] return Color(color)
[docs] def distance(self, other, space='lab'): """ Distance between self an another color Args: other (Color): the color to compare space (str): the colorspace to comapre in Returns: float Ignore: import kwimage self = kwimage.Color((0.16304347826086973, 0.0, 1.0)) other = kwimage.Color('purple') hard_coded_colors = { 'a': (1.0, 0.0, 0.16), 'b': (1.0, 0.918918918918919, 0.0), 'c': (0.0, 1.0, 0.0), 'd': (0.0, 0.9239130434782604, 1.0), 'e': (0.16304347826086973, 0.0, 1.0) } # Find grays names = kwimage.Color.named_colors() grays = {} for name in names: color = kwimage.Color(name) r, g, b = color.as01() if r == g and g == b: grays[name] = (r, g, b) print(ub.urepr(ub.sorted_vals(grays), nl=-1)) for k, v in hard_coded_colors.items(): self = kwimage.Color(v) distances = [] for name in names: other = kwimage.Color(name) dist = self.distance(other) distances.append(dist) idxs = ub.argsort(distances)[0:5] dists = list(ub.take(distances, idxs)) names = list(ub.take(names, idxs)) print('k = {!r}'.format(k)) print('names = {!r}'.format(names)) print('dists = {!r}'.format(dists)) """ vec1 = np.array(self.as01(space)) vec2 = np.array(other.as01(space)) return np.linalg.norm(vec1 - vec2)
[docs] def interpolate(self, other, alpha=0.5, ispace=None, ospace=None): """ Interpolate between colors Args: other (Color): A coercable Color alpha (float | List[float]): one or more interpolation values ispace (str | None): colorspace to interpolate in ospace (str | None): colorspace of returned color Returns: Color | List[Color] Example: >>> import kwimage >>> color1 = self = kwimage.Color.coerce('orangered') >>> color2 = other = kwimage.Color.coerce('dodgerblue') >>> alpha = np.linspace(0, 1, 6) >>> ispace = 'rgb' >>> ospace = 'rgb' >>> colorBs = self.interpolate(other, alpha, ispace=ispace, ospace=ospace) >>> # xdoctest: +REQUIRES(module:kwplot) >>> # xdoctest: +REQUIRES(--show) >>> from kwimage.im_color import _draw_color_swatch >>> swatch_colors = [color1] + colorBs + [color2] >>> print('swatch_colors = {}'.format(ub.urepr(swatch_colors, nl=1))) >>> swatch1 = _draw_color_swatch(swatch_colors, cellshape=(8, 8)) >>> import kwplot >>> kwplot.autompl() >>> kwplot.imshow(swatch1, pnum=(1, 1, 1), fnum=1) >>> kwplot.show_if_requested() """ import kwimage other = kwimage.Color.coerce(other) vec1 = np.array(self.as01(ispace)) vec2 = np.array(other.as01(ispace)) if ub.iterable(alpha): alpha = np.asarray(alpha).ravel() vecB = vec1[None, :] * (1 - alpha)[:, None] + (vec2[None, :] * alpha[:, None]) new = [ kwimage.Color( kwimage.Color(c, space=ispace, coerce=False).as01(ospace), space=ospace, coerce=False) for c in vecB ] else: vecB = vec1 * (1 - alpha) + (vec2 * alpha) c = vecB new = kwimage.Color( kwimage.Color(c, space=ispace, coerce=False).as01(ospace), space=ospace, coerce=False) return new
[docs] def to_image(self, dsize=(8, 8)): """ Create an solid-color image with this color Args: dsize (Tuple[int, int]): the desired width / height of the image (defaults to 8x8) """ w, h = dsize color_arr = np.array(self.as01()).astype(np.float32) cell_pixel = color_arr[None, None] cell = np.tile(cell_pixel, (h, w, 1)) return cell
[docs] def adjust(self, saturate=0, lighten=0): """ Adjust the saturation or value of a color. Requires that :mod:`colormath` is installed. Args: saturate (float): between +1 and -1, when positive saturates the color, when negative desaturates the color. lighten (float): between +1 and -1, when positive lightens the color, when negative darkens the color. Example: >>> # xdoctest: +REQUIRES(module:colormath) >>> import kwimage >>> self = kwimage.Color.coerce('salmon') >>> new = self.adjust(saturate=+0.2) >>> cell1 = self.to_image() >>> cell2 = new.to_image() >>> # xdoctest: +REQUIRES(--show) >>> import kwplot >>> kwplot.autompl() >>> canvas = kwimage.stack_images([cell1, cell2], axis=1) >>> kwplot.imshow(canvas) Example: >>> # xdoctest: +REQUIRES(module:colormath) >>> import kwimage >>> self = kwimage.Color.coerce('salmon', alpha=0.5) >>> new = self.adjust(saturate=+0.2) >>> cell1 = self.to_image() >>> cell2 = new.to_image() >>> # xdoctest: +REQUIRES(--show) >>> import kwplot >>> kwplot.autompl() >>> canvas = kwimage.stack_images([cell1, cell2], axis=1) >>> kwplot.imshow(canvas) Example: >>> # xdoctest: +REQUIRES(module:colormath) >>> import kwimage >>> adjustments = [ >>> {'saturate': -0.2}, >>> {'saturate': +0.2}, >>> {'lighten': +0.2}, >>> {'lighten': -0.2}, >>> {'saturate': -0.9}, >>> {'saturate': +0.9}, >>> {'lighten': +0.9}, >>> {'lighten': -0.9}, >>> ] >>> self = kwimage.Color.coerce('kitware_green') >>> dsize = (256, 64) >>> to_show = [] >>> to_show.append(self.to_image(dsize)) >>> for kwargs in adjustments: >>> new = self.adjust(**kwargs) >>> cell = new.to_image(dsize=dsize) >>> text = ub.urepr(kwargs, compact=1, nobr=1) >>> cell, info = kwimage.draw_text_on_image(cell, text, return_info=1, border={'thickness': 2}, color='white', fontScale=1.0) >>> to_show.append(cell) >>> # xdoctest: +REQUIRES(--show) >>> # xdoctest: +REQUIRES(module:kwplot) >>> import kwplot >>> kwplot.autompl() >>> canvas = kwimage.stack_images_grid(to_show) >>> canvas = kwimage.draw_header_text(canvas, 'kwimage.Color.adjust') >>> kwplot.imshow(canvas) """ h, s, v, *a = self.as01(space='hsv') assert 0 <= h <= 360 assert 0 <= s <= 1 assert 0 <= v <= 1 if saturate: s = max(min(1, s + saturate), 0) if lighten: v = max(min(1, v + lighten), 0) hsv = (h, s, v) rgb = _colormath_convert(hsv, 'hsv', 'rgb') color = list(rgb) + a new_rgb = self.__class__.coerce(color) return new_rgb
def _draw_color_swatch(colors, cellshape=9): """ Draw colors in a grid Ignore: # https://seaborn.pydata.org/tutorial/color_palettes.html import kwplot sns = kwplot.sns from kwimage.im_color import * # NOQA from kwimage.im_color import _draw_color_swatch colors = sns.palettes.color_palette('deep', n_colors=10) swatch = _draw_color_swatch(colors) kwplot.imshow(swatch) """ import kwimage import math if not ub.iterable(cellshape): cellshape = [cellshape, cellshape] cell_h = cellshape[0] cell_w = cellshape[1] cells = [] for color in colors: cell = kwimage.Color(color).to_image(dsize=(cell_w, cell_h)) cells.append(cell) num_colors = len(colors) num_cells_side0 = max(1, int(np.sqrt(num_colors))) num_cells_side1 = math.ceil(num_colors / num_cells_side0) num_cells = num_cells_side1 * num_cells_side0 num_null_cells = num_cells - num_colors if num_null_cells > 0: null_cell = np.zeros((cell_h, cell_w, 3), dtype=np.float32) pts1 = np.array([(0, 0), (cell_w - 1, 0)]) pts2 = np.array([(cell_w - 1, cell_h - 1), (0, cell_h - 1)]) null_cell = kwimage.draw_line_segments_on_image( null_cell, pts1, pts2, color='red') # null_cell = kwimage.draw_text_on_image( # {'width': cell_w, 'height': cell_h}, text='X', color='red', # halign='center', valign='center') null_cell = kwimage.ensure_float01(null_cell) cells.extend([null_cell] * num_null_cells) swatch = kwimage.stack_images_grid( cells, chunksize=num_cells_side0, axis=0) return swatch