---
title: "Color Spaces: Classes and Utilities"
bibliography: ../references.bib
---

## Overview

At the core of the *colorspace* package are various utilities for computing with
color spaces @color:Wiki+Colorspace, as the name conveys. Thus, the package helps to
map various three-dimensional representations of color to each other @color:Ihaka:2003.
A particularly important mapping is the one from the perceptually-based
and device-independent color model HCL (Hue-Chroma-Luminance) to standard
Red-Green-Blue (sRGB) which is the basis for color specifications in many
systems based on the corresponding hex codes @color:Wiki+Webcolors, e.g., in HTML
but also in Python. For completeness further standard color models are included as
well in the package. Their connections are illustrated in the following graph:


```{mermaid}
flowchart LR
  X[CIEXYZ]
  PLUV((polarLUV<br/>HCL)) --- LUV[CIELUV] --- X
  PLAB((polarLAB)) --- LAB[CIELAB] --- X
  X ---|white point<br>= D65| RGB[RGB] ---|gamma<br>=2.4| sRGB[sRGB]
  RGB --- HSV
  sRGB --- HSV((HSV))
  sRGB --- hex{HEX}
  sRGB --- HLS((HLS))
  RGB --- HLS

  linkStyle 6 stroke-width:2px,fill:none,stroke:LightBlue;
  linkStyle 10 stroke-width:2px,fill:none,stroke:LightBlue;
```

Color models that are (or try to be) perceptually-based are displayed with
circles and models that are not are displayed with rectangles. The
corresponding classes in *colorspace* are:


* [`RGB`](/man/colorspace.colorlib.RGB.qmd)
  for the classic Red-Green-Blue color model, which mixes three primary
  colors with different intensities to obtain a spectrum of colors. The
  advantage of this color model is (or was) that it corresponded to how
  computer and TV screens generated colors, hence it was widely adopted and
  still is the basis for color specifications in many systems. For example, hex
  color codes are employed in HTML but also in Python. However, the RGB model also
  has some important drawbacks: It does not take into account the output device
  properties, it is not perceptually uniform (a unit step within RGB does not
  produce a constant perceptual change in color), and it is unintuitive for
  humans to specify colors (say brown or pink) in this space.
  @color:Wiki+Colorspace
* [`sRGB`](/man/colorspace.colorlib.sRGB.qmd)
  addresses the issue of device dependency by adopting a so-called
  gamma correction. Therefore, the gamma-corrected standard RGB (sRGB), as
  opposed to the linearized RGB above, is a good model for specifying colors in
  software and for hardware. But it is still unintuitive for humans to work
  directly with this color space. Therefore, sRGB is a good place to end up in
  a color space manipulation but it is not a good place to start.
  @color:Wiki+sRGB
* [`HSV`](/man/colorspace.colorlib.HSV.qmd)
  is a simple transformation of the (s)RGB space that tries to capture
  the perceptual axes: hue (dominant wavelength, the type of color), saturation
  (colorfulness), and value (brightness, i.e., light vs. dark). Unfortunately,
  the three axes in the HSV model are confounded so that, e.g., brightness
  changes dramatically with hue. @color:Wiki+Webcolors
* [`HSL`](/man/colorspace.colorlib.HLS.qmd)
  (Hue-Lightness-Saturation) is another transformation of (s)RGB that
  tries to capture the perceptual axes. It does a somewhat better job but the
  dimensions are still strongly confounded.
  @color:Wiki+HSV
* [`CIEXYZ`](/man/colorspace.colorlib.CIEXYZ.qmd)
  was established by the CIE (Commission Internationale de l’Eclairage)
  based on experiments with human subjects. It provides a unique triplet of XYZ
  values, coding the standard observer’s perception of the color. It is
  device-independent but it is not perceptually uniform and the XYZ coordinates
  have no intuitive meaning.
  @color:Wiki+CIEXYZ
* [`CIELUV`](/man/colorspace.colorlib.CIELUV.qmd) and
  [`CIELAB`](/man/colorspace.colorlib.CIELAB.qmd)
  were therefore proposed by the CIE as perceptually
  uniform color spaces where the former is typically preferred for emissive
  technologies (such as screens and monitors) whereas the latter is usually
  preferred when working with dyes and pigments. The L coordinate in both
  spaces has the same meaning and captures luminance (light-dark contrasts).
  Both the U and V coordinates as well as the A and B coordinates measure
  positions on red/green and yellow/blue axes, respectively, albeit in somewhat
  different ways. While this corresponds to how human color vision likely
  evolved (see the next section), these two color models still not correspond
  to perceptual axes that humans use to describe colors.
  @color:Wiki+HSV,@color:Wiki+CIELAB
* [`polarLUV`](/man/colorspace.colorlib.polarLUV.qmd) (=HCL) and
  [`polarLAB`](/man/colorspace.colorlib.polarLAB.qmd) 
  therefore take polar coordinates in the UV plane
  and AB plane, respectively. Specifically, the polar coordinates of the LUV
  model are known as the HCL (Hue-Chroma-Luminance) model (see @color:Wiki+HCL,
  which points out that the LAB-based polar coordinates are also sometimes
  referred to as HCL). The HCL model captures the human perceptual axes very
  well without confounding effects as in the HSV or HLS approaches (more
  details follow below).


## Human color vision and the HCL color model

It has been hypothesized that human color vision has evolved in three distinct stages:

1. Perception of light/dark contrasts (monochrome only).
2. Yellow/blue contrasts (usually associated with our notion of warm/cold colors).
3. Green/red contrasts (helpful for assessing the ripeness of fruit).

See @color:Kaiser+Boynton:1996, @color:Knoblauch:2002, @color:Ihaka:2003, @color:dichromat
and/or @color:Zeileis+Hornik+Murrell:2009 for more details and references.
Thus, colors can be described using a 3-dimensional space:

![](/images/human-axes.svg)

However, for describing colors in such a space, it is more natural for humans
to employ polar coordinates in the color plane (yellow/blue vs. green/red,
visualized by the dashed circle above) plus a third light/dark axis. Hence,
color models that attempt to capture these perceptual axes are also called
perceptually-based color spaces. As already argued above, the HCL model
captures these dimensions very well, calling them: hue, chroma, and luminance.
The corresponding sRGB gamut, i.e., the HCL colors that can also be represented
in sRGB, is visualized in the animation below [@color:Horvath+Lipka:2016].

![](https://upload.wikimedia.org/wikipedia/commons/thumb/8/8d/SRGB_gamut_within_CIELCHuv_color_space_mesh.webm/512px--SRGB_gamut_within_CIELCHuv_color_space_mesh.webm.jpg)

The shape of the HCL space is a distorted double cone which is seen best by
looking at vertical slices, i.e., chroma-luminance planes for given hues. For
example, the left panel below depicts the chroma-luminance plane for a certain
blue (hue = 255). Along with luminance the colors change from dark to light.
With increasing chroma the colors become more colorful, where the highest
chroma is possible for intermediate luminance.

As some colors are relatively dark (e.g., blue and red assume their maximum
chroma for relatively low luminances) while others are relatively light (e.g.,
yellow and green), horizontal slices of hue-chroma planes for given hue have
somewhat irregular shapes. The right panel below shows such a hue-chroma plane
for moderately light colors (luminance = 70). At that luminance, green and
orange can become much more colorful compared to blue or red.


```{python}
#| echo: false
#| layout-ncol: 1

from colorspace import sequential_hcl, qualitative_hcl, hclplot
import matplotlib.pyplot as plt

fig, (ax1, ax2) = plt.subplots(1, 2, figsize = (10, 5))
hclplot(sequential_hcl(h = 255), h = 255, ax = ax1, s = None, linewidth = None)
hclplot(qualitative_hcl(l = 70), l = 70,  ax = ax2, s = None, linewidth = None)
plt.show()
```


## Illustration of basic colorspace functionality

As an example an object containing a series of colors can be specified
in the HCL (i.e., [`polarLUV`](/man/colorspace.colorlib.polarLUV.qmd))
color model:

```{python}
#| echo: true
#| layout-ncol: 1

from colorspace.colorlib import HCL
x = HCL(H = [0, 120, 240], C = [50.] * 3, L = [70.] * 3)
x
```

```{python}
#| echo: false

from colorspace.colorlib import hexcols
hexcols(x.colors())
```

The resulting three colors are pastel red (Hue = 0), green (Hue = 120), and
blue (Hue = 240) with moderate chroma and luminance as shown above. For display
in other systems an sRGB representation might be needed. 
Objects based on the internal [`colorlib`](/man/colorspace.colorlib.colorlib.qmd)
can easily be transformed from one color space to another (see
[Overview](#overview)) using the `.to()` method.

```{python}
#| echo: true
#| layout-ncol: 1

x.to("sRGB")    # Convert to sRGB coordinates
x
```


The displayed coordinates can also be extracted as numeric numpy arrays the
calling the `.get()` method on a `colorobject`. As an example we convert the
three colors to the [`HSV`](/man/colorspace.colorlib.HSV.qmd) color space
and extract the saturation coordinate only by calling
[`x.get("S")`](/man/colorspace.colorlib.HSV.get.qmd).


```{python}
#| echo: true
#| layout-ncol: 1

# Convert from sRGB to HSV
x.to("HSV") # Convert from sRGB to HSV
x
```

```{python}
# Extracting saturation dimension
x.get("S")
```

For display in many systems hex color codes based on the
sRGB coordinates can be created:

```{python}
#| echo: true
#| layout-ncol: 1

x.colors()   # Automatically converts to hex colors (list of str)
```


## Color library

The workhorse of these transformations is the
[`colorlib`](/man/colorspace.colorlib.colorlib.qmd) class which allows to transform
`colorobject` objects into each other.

* colorlib
* colorobject

The following classes (all inheriting from `colorobject`) are
available to create colors in different color spaces:
[`polarLUV`](/man/colorspace.colorlib.polarLUV.qmd) (HCL),
[`CIELUV`](/man/colorspace.colorlib.CIELUV.qmd),
[`polarLAB`](/man/colorspace.colorlib.polarLAB.qmd),
[`CIELAB`](/man/colorspace.colorlib.CIELAB.qmd),
[`CIEXYZ`](/man/colorspace.colorlib.CIEXYZ.qmd),
[`RGB`](/man/colorspace.colorlib.RGB.qmd),
[`sRGB`](/man/colorspace.colorlib.sRGB.qmd),
[`HSV`](/man/colorspace.colorlib.HSV.qmd),
[`HLS`](/man/colorspace.colorlib.HLS.qmd),
[`hexcols`](/man/colorspace.colorlib.hexcols.qmd),
[`colorlib`](/man/colorspace.colorlib.colorlib.qmd).

Colors can be transformed from and to (mostly all) color spaces using the
`.to(<name of color space>)` method (see e.g.,
[`hexcols.to`](/man/colorspace.colorlib.hexcols.to.qmd)).


## Matplotlib color maps

In addition many objects provided by the *colorspace* package allow to convert a
series of colors (color palette) into a matplotlib "cmap"
(`matplotlib.colors.LinearSegmentedColormap`)
As an example using the object `x` from above:

```{python}
#| echo: true

from colorspace import palette
cmap = palette(x, "custom cmap palette").cmap()
cmap.N # Number of colors the cmap is based on
```

```{python}
#| layout-ncol: 1
cmap
```

Please note that matplotlib performs linear interpolation in the sRGB color
space between the colors specified. This allows us to draw more than 3 colors
from the color map above. However, this may result in skewed color gradients
a shown in the example below.

Two cmaps are defined (`cmap4`, `cmap`) where the first one is based on
solely $4$ colors, the latter on $101$ colors (default) based on the same
sequential palette. From both cmaps, $50$ equally distanced colors are drawn
resulting in the following two sets of colors:


```{python}
from numpy import arange
from colorspace import sequential_hcl, specplot
from colorspace.colorlib import sRGB

cmap4  = sequential_hcl(5).cmap(4) # Cmap based on only 4 colors
cmap   = sequential_hcl(5).cmap()  # Cmap based on only 101 colors (default)

seq     = arange(0.0, 1.0, 0.02) # Sequence from 0 to 1 by 0.02 (length 50)
coords4 = cmap4(seq).transpose() # sRGB coordinates from cmap4
coords  = cmap(seq).transpose()  # sRGB coordinates from cmap

# Convert via sRGB to hex colors
cols4   = sRGB(R = coords4[0], G = coords4[1], B = coords4[2]).colors()
cols    = sRGB(R = coords[0],  G = coords[1],  B = coords[2]).colors()

specplot(cols4, cols);
```

The better way to go is to define/design your custom palette in the
HCL color space using [`qualitative_hcl`](/man/colorspace.palettes.qualitative_hcl.qmd),
[`diverging_hcl`](/man/colorspace.palettes.diverging_hcl.qmd),
[`divergingx_hcl`](/man/colorspace.palettes.divergingx_hcl.qmd), and
[`sequential_hcl`](/man/colorspace.palettes.sequential_hcl.qmd)
among others (see :ref:`HCL-Based Color Palettes <article-hcl_palettes>` for details).

These classes define color palettes via functions in the HCL color space
and allow to draw large numbers of colors along the function space.
The `.cmap()` will still return a 
`matplotlib.colors.LinearSegmentedColormap`
but (by default) based on `n = 101` distinct colors which will require
less linear interpolation.

```{python}
#| echo: true
#| layout-ncol: 1

from colorspace import diverging_hcl
pal  = diverging_hcl("Green-Orange")
cmap = pal.cmap()
print(cmap.N)
cmap
```

A simple example using `matplotlib.pyplot.contourf`
with a custom HCL based color palette
(`sequential_hcl("Rocket").cmap()`):

```{python}
#| echo: true
#| layout-ncol: 1

import matplotlib.pyplot as plt
import numpy as np
from numpy import ma
from matplotlib import ticker, cm

# Custom color map
from colorspace import sequential_hcl
cmap = sequential_hcl("Rocket").cmap()

N = 100
x = np.linspace(-3.0, 3.0, N)
y = np.linspace(-2.0, 2.0, N)
X, Y = np.meshgrid(x, y)

# A low hump with a spike coming out.
# Needs to have z/colour axis on a log scale so we see both hump and spike.
# linear scale only shows the spike.
Z1 = np.exp(-X**2 - Y**2)
Z2 = np.exp(-(X * 10)**2 - (Y * 10)**2)
z = Z1 + 50 * Z2

# Put in some negative values (lower left corner) to cause trouble with logs:
z[:5, :5] = -1

# The following is not strictly essential, but it will eliminate
# a warning.  Comment it out to see the warning.
z = ma.masked_where(z <= 0, z)

# Automatic selection of levels works; setting the
# log locator tells contourf to use a log scale:
# Matlplotlib.contourf decides to draw 9 levels
fig, ax = plt.subplots()
cs = ax.contourf(X, Y, z, locator=ticker.LogLocator(), cmap=cmap)

plt.show()
```

