Skip to content

Latest commit

 

History

History
96 lines (83 loc) · 5.91 KB

gamuts.md

File metadata and controls

96 lines (83 loc) · 5.91 KB

% Color Space Conversion and Gamuts

Context

Scarlet works with color spaces: coordinate systems that represent color. Some color spaces are designed to match human visual perception; others are designed to match computer display of color; others aim to achieve a balance and easily translate both to human perception and actual display of color. Scarlet aims to allow the user to easily and painlessly convert between all of these spaces easily, which is often in tension with Scarlet's commitment to transparency and consistency. When in doubt, Scarlet prioritizes the explicit over the implicit: giving users more information rather than less, and avoiding silent errors at all costs.

One of the places that this philosophy is most apparent is in Scarlet's handling of conversion between color spaces. Some color spaces, particularly those that seek to model human perception (like CIELAB and CIE 1931), have a gamut (a set of representable colors) that exceeds the limits of human perception: not all valid CIELAB coordinates are visible to humans, and every color that humans can see can be represented in CIELAB.

Other spaces, particularly those seeking to model the limited range of color reproduction on computer monitors and in printers, have a gamut that does not cover all of human perception. Standard RGB (in Scarlet, RGBColor) covers only around 35% of visible colors.

CIE1931xy gamut comparison.svg
By BenRG and cmglee - http://commons.wikimedia.org/wiki/File:CIE1931xy_blank.svg, CC BY-SA 3.0, Link

The Problem With Primaries

Many of the most popular color spaces are primary-based: each axis represents the intensity of some base color that is then mixed in some fashion with the other axes' colors to get the final result. This seems like it would match human perception: we have three kinds of color receptors, and each of them has their highest affinity for a different frequency of light. Additionally, mixing visible colors always produces another visible color: mathematically, we say that the full gamut of human vision is convex.

The problem is that, although our vision's gamut is convex, it isn't a triangle: there are no three visible colors that together can mix to generate every visible color. Blues and greens are the biggest issue: as you can see from the diagram, there aren't really any purples that don't decompose nicely into red and blue. (The reason for this is because the middle-wavelength "green" receptors overlap a lot with the other receptors: there's no green that doesn't in part stimulate all of the receptors in your eye.) This means that the only primary-based systems that can represent every color don't actually use visible primaries! (Other color spaces solve this issue by using invisible primaries or using an entirely different representation of color vision, such as CIELAB or CIELCH.

What This Means For Scarlet

The takeaway is that color spaces are presented to the user as essentially different unit systems, all interconvertible. The rationale for this abstraction is that Scarlet aims to allow users to work solely within color spaces that can make this abstraction work with zero cost (for example, converting solely between RGB and HSV) without needing to worry about color spaces for which this doesn't hold. Scarlet additionally promises that any implicit conversion it does, without explicit invocation, won't cause errors or incorrect results.

If you are going to explicitly convert between color spaces that have different gamuts, Scarlet trusts that you know what you're doing. The standard convert() method that is used to change color spaces can and will clamp outputs to the gamut of the color spaces that they're in. This means that conversion can create unexpected problems!

// this color doesn't exist in sRGB! (that's probably a good thing, this can't really be represented)
let color1 = CIELABColor{l: 0.0, a: 100.0, b: 100.0};
// this color gets clamped within the sRGB gamut: it ends up being #690000
let color2: RGBColor = color1.convert();
let color3: CIELABColor = color2.convert();
println!("{} {} {}", color3.l, color3.a, color3.b);
// prints the following:
// 20.604385926623827 42.096030499561316 31.81745246025879

How to Avoid Gamut Problems

Scarlet allows you to avoid gamut problems by using inherent bounds on the color spaces. Note that XYZ is an exception to all of this: it is intended to be a complete master space and to represent anything, even colors that normally cannot be seen in nature.

Clamping

The 'clamp_color()' method can be used to modify a color so that it falls in another color space. This is explicit, because it's not always the right thing to do. Implicit clamping can occur when converting to and from spaces. This is because it is rarely the right option to have an unusable Color object. When in doubt, favor explicit over implicit and clamp your colors.

Let's see the example from above again, but this time with explicit clamping.

// this color doesn't exist in sRGB! (that's probably a good thing, this can't really be represented)
let color1 = CIELABColor{l: 0.0, a: 100.0, b: 100.0};
// let's clamp it explicity
let clamped = RGBColor::clamp_color(color1);
let color2: RGBColor = clamped.convert();
let color3: CIELABColor = color2.convert();
// now, color3 and clamped match up to floating-point error