Color-space conversions and colorblindness diagnostics.
State-of-the-art color science, exposed directly rather than buried in the
colormap machinery: sRGB <-> linear-light RGB <-> CIE XYZ <-> CIELAB <->
Oklab/Oklch <-> CAM16-UCS, plus color-vision-deficiency (CVD) simulation and
two colormap-quality diagnostics. Everything runs in Rust
(pyplotrs._pyplotrs_core, backed by the pyplotrs-color crate,
itself built on the palette <https://github.com/Ogeon/palette>_ crate).
Oklab (Ottosson, 2020) is what pyplotrs.colormaps.Colormap uses to
interpolate custom stops= by default - it is the practical, contemporary
choice for perceptually smooth gradients. CAM16-UCS is the more rigorous (and
more expensive) full color-appearance model; it backs distance and
the two diagnostics below rather than everyday interpolation::
from pyplotrs import color, colormaps
color.to_oklab((255, 0, 0)) # (0.628, 0.225, 0.126)
color.simulate_cvd((220, 20, 20), "deuteranopia") # (141, 125, 0)
cm = colormaps.get_cmap("coolwarm")
color.cvd_safe_report(cm) # {"protanopia": 0.77, "deuteranopia": 0.77, "tritanopia": 0.94}
color.perceptual_uniformity(cm) # 0.25 - lower is more uniform
to_oklab
to_oklab(rgb: _RGB) -> _Triple
sRGB -> Oklab (L in [0, 1]; a/b roughly [-0.4, 0.4]).
Source code in python/pyplotrs/color.py
| def to_oklab(rgb: _RGB) -> _Triple:
"""sRGB -> Oklab (`L` in ``[0, 1]``; `a`/`b` roughly ``[-0.4, 0.4]``)."""
return _core.srgb_to_oklab(rgb)
|
from_oklab
from_oklab(lab: _Triple) -> _RGB
Oklab -> sRGB (out-of-gamut input is clipped per channel).
Source code in python/pyplotrs/color.py
| def from_oklab(lab: _Triple) -> _RGB:
"""Oklab -> sRGB (out-of-gamut input is clipped per channel)."""
return _core.oklab_to_srgb(lab)
|
to_oklch
to_oklch(rgb: _RGB) -> _Triple
sRGB -> Oklch (L in [0, 1], chroma >= 0, hue in degrees).
Source code in python/pyplotrs/color.py
| def to_oklch(rgb: _RGB) -> _Triple:
"""sRGB -> Oklch (`L` in ``[0, 1]``, `chroma` >= 0, `hue` in degrees)."""
return _core.srgb_to_oklch(rgb)
|
from_oklch
from_oklch(lch: _Triple) -> _RGB
Oklch -> sRGB.
Source code in python/pyplotrs/color.py
| def from_oklch(lch: _Triple) -> _RGB:
"""Oklch -> sRGB."""
return _core.oklch_to_srgb(lch)
|
to_lab
to_lab(rgb: _RGB) -> _Triple
sRGB -> CIELAB (D65 white point; L* in [0, 100]).
Source code in python/pyplotrs/color.py
| def to_lab(rgb: _RGB) -> _Triple:
"""sRGB -> CIELAB (D65 white point; `L*` in ``[0, 100]``)."""
return _core.srgb_to_lab(rgb)
|
from_lab
from_lab(lab: _Triple) -> _RGB
CIELAB (D65) -> sRGB.
Source code in python/pyplotrs/color.py
| def from_lab(lab: _Triple) -> _RGB:
"""CIELAB (D65) -> sRGB."""
return _core.lab_to_srgb(lab)
|
to_xyz
to_xyz(rgb: _RGB) -> _Triple
sRGB -> CIE 1931 XYZ (D65 white point, Y in [0, 1]).
Source code in python/pyplotrs/color.py
| def to_xyz(rgb: _RGB) -> _Triple:
"""sRGB -> CIE 1931 XYZ (D65 white point, `Y` in ``[0, 1]``)."""
return _core.srgb_to_xyz(rgb)
|
from_xyz
from_xyz(xyz: _Triple) -> _RGB
CIE 1931 XYZ (D65) -> sRGB.
Source code in python/pyplotrs/color.py
| def from_xyz(xyz: _Triple) -> _RGB:
"""CIE 1931 XYZ (D65) -> sRGB."""
return _core.xyz_to_srgb(xyz)
|
to_linear
to_linear(rgb: _RGB) -> _Triple
Encoded (gamma) sRGB -> linear-light RGB (each component [0, 1]).
Source code in python/pyplotrs/color.py
| def to_linear(rgb: _RGB) -> _Triple:
"""Encoded (gamma) sRGB -> linear-light RGB (each component ``[0, 1]``)."""
return _core.srgb_to_linear(rgb)
|
from_linear
from_linear(rgb: _Triple) -> _RGB
Linear-light RGB -> encoded (gamma) sRGB.
Source code in python/pyplotrs/color.py
| def from_linear(rgb: _Triple) -> _RGB:
"""Linear-light RGB -> encoded (gamma) sRGB."""
return _core.linear_to_srgb(rgb)
|
to_cam16ucs
to_cam16ucs(rgb: _RGB) -> _Triple
sRGB -> CAM16-UCS (Jmh form: lightness, colorfulness, hue-degrees),
under pyplotrs' fixed viewing conditions (a static D65 white point,
40 cd/m^2 adapting luminance - CAM16 is an appearance model, so results
are only meaningful relative to one consistent choice of conditions).
Source code in python/pyplotrs/color.py
| def to_cam16ucs(rgb: _RGB) -> _Triple:
"""sRGB -> CAM16-UCS (Jmh form: lightness, colorfulness, hue-degrees),
under pyplotrs' fixed viewing conditions (a static D65 white point,
40 cd/m^2 adapting luminance - CAM16 is an *appearance* model, so results
are only meaningful relative to one consistent choice of conditions)."""
return _core.srgb_to_cam16ucs(rgb)
|
from_cam16ucs
from_cam16ucs(ucs: _Triple) -> _RGB
CAM16-UCS (Jmh form) -> sRGB.
Source code in python/pyplotrs/color.py
| def from_cam16ucs(ucs: _Triple) -> _RGB:
"""CAM16-UCS (Jmh form) -> sRGB."""
return _core.cam16ucs_to_srgb(ucs)
|
distance
distance(a: _RGB, b: _RGB) -> float
Perceptual (CAM16-UCS) distance between two sRGB colors - a "how
different do these look" metric, more reliable than Euclidean RGB or even
CIE76 Lab distance.
Source code in python/pyplotrs/color.py
| def distance(a: _RGB, b: _RGB) -> float:
"""Perceptual (CAM16-UCS) distance between two sRGB colors - a "how
different do these look" metric, more reliable than Euclidean RGB or even
CIE76 Lab distance."""
return _core.cam16ucs_distance(a, b)
|
simulate_cvd
simulate_cvd(rgb: _RGB, kind: CvdKind) -> _RGB
How rgb appears to someone with kind dichromacy (the
Machado/Oliveira/Fernandes 2009 model).
Source code in python/pyplotrs/color.py
| def simulate_cvd(rgb: _RGB, kind: CvdKind) -> _RGB:
"""How `rgb` appears to someone with `kind` dichromacy (the
Machado/Oliveira/Fernandes 2009 model)."""
return _core.simulate_cvd(rgb, kind)
|
cvd_safe_report
cvd_safe_report(cmap) -> dict[CvdKind, float]
Worst-case distinguishability of a colormap under each CVD kind.
cmap is anything pyplotrs.colormaps.get_cmap accepts (a name
or a Colormap). Each value is 1.0 (CVD
doesn't shrink the map's worst-case contrast at all) down to 0.0 (some
pair of colors that reads as distinct normally becomes visually identical
under that CVD). Below ~0.5 is worth treating as a real accessibility
concern for that deficiency.
Source code in python/pyplotrs/color.py
| def cvd_safe_report(cmap) -> dict[CvdKind, float]:
"""Worst-case distinguishability of a colormap under each CVD kind.
``cmap`` is anything [`pyplotrs.colormaps.get_cmap`][pyplotrs.colormaps.get_cmap] accepts (a name
or a [`Colormap`][pyplotrs.colormaps.Colormap]). Each value is `1.0` (CVD
doesn't shrink the map's worst-case contrast at all) down to `0.0` (some
pair of colors that reads as distinct normally becomes visually identical
under that CVD). Below ~0.5 is worth treating as a real accessibility
concern for that deficiency.
"""
table = get_cmap(cmap)._table
return {kind: _core.cvd_safety_ratio(table, kind) for kind in _CVD_KINDS}
|
perceptual_uniformity(cmap) -> float
Perceptual-uniformity roughness of a colormap (cmap: a name or a
Colormap): the coefficient of variation of
the CAM16-UCS step size between consecutive table entries. 0.0 means
every step looks equally large (ideal for mapping continuous data, where
visual step size should track data step size); larger values mean some
regions of the map compress more data range into less visual change than
others.
Source code in python/pyplotrs/color.py
| def perceptual_uniformity(cmap) -> float:
"""Perceptual-uniformity roughness of a colormap (`cmap`: a name or a
[`Colormap`][pyplotrs.colormaps.Colormap]): the coefficient of variation of
the CAM16-UCS step size between consecutive table entries. `0.0` means
every step looks equally large (ideal for mapping continuous data, where
visual step size should track data step size); larger values mean some
regions of the map compress more data range into less visual change than
others."""
return _core.perceptual_uniformity(get_cmap(cmap)._table)
|