• mina86.com

  • Categories
  • Code
  • Contact
  • Now I know my XYZ’s

    When dealing with colour spaces, one eventually encounters the XYZ colour space. It is a mathematical model that maps any visible colour into a triple of X, Y and Z coordinates. Defined in 1931, it's nearly a century old and serves as a foundation upon which other colour spaces are built. However, XYZ has one aspect that can easily confuse programmers.

    460480500520540560580600620x.0.1.2.3.4.5.6.7.8y.0.1.2.3.4.5.6.7.8.9

    You implement a conversion function and, to check it, compare its results with an existing implementation. You search for an online converter, only to realise that the coordinates you obtain differ by two orders of magnitude. Do not despair! If the ratio is exactly 1:100, your implementation is probably correct.

    This is because the XYZ colour space can use an arbitrary scale. For example, the Y component corresponds to colour’s luminance but nothing specifies whether the maximum is 1, 100 or another value. I typically use 1, such that the D65 illuminant (i.e. sRGB’s white colour) has coordinates (0.95, 1, 1.089), but a different implementation could report them as (95, 100, 108.9). (Notice that all components are scaled by the same factor).

    This is similar to sRGB. In 24-bit True Colour representation, each component is an integer in the 0–255 range. However, a 15-bit High Colour uses the 0–31 range, Rec. 709 uses the 16–235 range and high-depth standards might use the 0–1023 range.

    A closely related colour space is xyY. Its Y coordinate is the same as in XYZ and can scale arbitrarily, but x and y have well-defined ranges. They define chromaticity, i.e. hue, and can be calculated using the following formulæ: x = X / (X + Y + Z) and y = Y / (X + Y + Z). Both fall within the [0, 1) range.

    The L*u*v* and LChuv colour spaces

    I’ve written about L*a*b* so it’s only fair that I’ll also describe its twin sister: the L*u*v* colour space (a.k.a. CIELUV). The two share a lot in common. For example, they use the same luminance value, base their chromaticity on opponent process theory and each of them has a corresponding cylindrical LCh coordinate system. Yet, despite those similarities — or perhaps because of them — the CIELUV colour space is often overlooked.

    Photo of a panther chameleonu* and v* channels of the photo
    Fig. 1. Picture of a chameleon with its decomposition into L*, u* and v* channels. Photo by Dr Pratt Datta.

    Even though L*a*b* is getting all the limelight, L*u*v* model has its advantages. Before we start comparing the two colour spaces, let’s first go through the conversion formulæ.

    sRGB↔L*a*b*↔LChab conversions

    After writing about conversion between sRGB and XYZ colour spaces I’ve been asked about a related process: moving between sRGB and CIELAB (perhaps better known as L*a*b*). As this may be of interest to others, I’ve decided to go ahead and make an article out of it. I’ll also touch on CIELChab which is a closely related colour representation.

    Photo of a panther chameleona* and b* channels of the photo
    Fig. 1. Picture of a chameleon with its decomposition into L*, a* and b* channels. Photo by Dr Pratt Datta.

    The L*a*b* colour space was intended to be perceptually uniform. While it’s not truly uniform it’s nonetheless useful and widely used in the industry. For example, it’s the basis of the ΔE*00 colour difference metric. LChab aim to make L*a*b* easier to interpret by replacing a* and b* axes with more intuitive chroma and hue parameters.

    Importantly, the conversion between sRGB and L*a*b* goes through XYZ colour space. As such, the full process has multiple steps with a round trip conversion being: sRGB​→​XYZ​→​L*a*b*​→​XYZ​→​sRGB. Because of that structure I will describe each of the steps separately.

    Greyscale, you might be doing it wrong

    While working on ansi_colours crate I’ve learned about colour spaces more than I’ve ever thought I would. One of the things were intricacies of greyscale. Or rather not greyscale itself but conversion from sRGB. ‘How hard can it be?’ one might ask following it up with a helpful suggestion to, ‘just sum all components and divide by three!’

    Taking an arithmetic mean of red, green and blue coordinates is an often mentioned method. Inaccuracy of the method is usually acknowledged and justified by its simplicity and speed. That’s a fair trade-off except that equally simple and fast algorithms which are noticeably more accurate exist. One such method is built on an observation that green contributes the most to the perceived brightens of a colour. The formula is (r + 2g + b) / 4 and it increases accuracy (by taking green channel twice) as well as speed (by changing division operation into a bit shift). But that’s not all. Even better formulæ exist.

    TL;DR

    fn grey_from_rgb_avg_32bit(r: u8, g: u8, b: u8) -> u8 {
        let y = 3567664 * r as u32 + 11998547 * g as u32 + 1211005 * b as u32;
        ((y + (1 << 23)) >> 24) as u8
    }

    The above implements the best algorithm for converting sRGB into greyscale if speed and simplicity is main concern. It does not involve gamma thus forgoes most complicated and time-consuming arithmetic. It’s much more precise and as fast as arithmetic mean.

    sRGBXYZ conversion

    In an earlier post, I’ve shown how to calculate an RGBXYZ conversion matrix. It’s only natural to follow up with a code for converting between sRGB and XYZ colour spaces. While the matrix is a significant portion of the algorithm, there is one more step necessary: gamma correction.

    What is gamma correction?

    Human perception of light’s brightness approximates a power function of its intensity. This can be expressed as P=Sα where P is the perceived brightness and S is linear intensity. α has been experimentally measured to be less than one which means that people are more sensitive to changes to dark colours rather than to bright ones.

    Based on that observation, colour space’s encoding can be made more efficient by using higher precision when encoding dark colours and lower when encoding bright ones. This is akin to precision of floating-point numbers scaling with value’s magnitude. In RGB systems, the role of precision scaling is done by gamma correction. When colour is captured (for example from a digital camera) it goes through gamma compression which spaces dark colours apart and packs lighter colours more densely. When displaying an image, the opposite happens and encoded value goes through gamma expansion.

    Calculating RGBXYZ matrix

    I’ve recently found myself in need of an RGBXYZ transformation matrix expressed to the maximum possible precision. Sources on the Internet typically limit the precision to a handful decimal places so I’ve performed do the calculations myself.

    What we’re looking for is a 3-by-3 matrix M which, when multiplied by red, green and blue coordinates of a colour, produces its XYZ coordinates. In other words, a change of basis matrix from a space whose basis vectors are RGB’s primary colours: M=[XrXgXbYrYgYbZrZgZb]