RED Colourspaces Derivation

Introduction

The purpose of this document is to illustrate the steps we followed to compute RED colourspaces primaries.

The most important attribute used to specify an RGB colourspace is the set of 3 chromaticities or primaries encompassing the gamut of colours that the RGB colourspace can represent.

A convenient way to view an RGB colourspace gamut is to project it onto a chromaticity diagram such as the CIE 1931 Chromaticity Diagram where the primaries define the triangle encompassing the gamut.

In [1]:
% matplotlib inline
In [2]:
import numpy as np
import warnings

import colour
from colour.plotting import *

warnings.simplefilter("ignore")

# Plotting a few selected *RGB* colourspaces in 
# *CIE 1931 Chromaticity Diagram*.
RGB_colourspaces_CIE_1931_chromaticity_diagram_plot(
    ['ACES2065-1', 'ACEScg', 'Rec. 2020', 'Rec. 709'])
Out[2]:
True

There have been some notable efforts by VideoVillage to solve through image analysis the various RED colourspaces primaries as suggested in this CocoaLUT commit.

We present in this document an improved set of primaries for the following RED colourspaces:

  • DRAGONcolor
  • DRAGONcolor2
  • REDcolor
  • REDcolor2
  • REDcolor3
  • REDcolor4

Methodology

RED has recently publicised various RED colourspaces to ACES2065-1 colourspace conversion matrices, which are available in the OpenColorIO-Configs repository.

We will use those matrices and some free online RED footage (videoandfilmmaker, iris32, cinematography.net) to respectively perform and validate the various RED colourspaces primaries computations.

Normalised Primary Matrix to Primaries & Whitepoint

Computing the primaries and whitepoint of a given RGB colourspace is possible if the normalised primary matrix converting RGB values to CIE XYZ tristimulus values is known. [1]

The primaries CIE XYZ tristimulus values are calculated as follows:

$$ \begin{equation} XYZ_P = (M_{npm}\cdot I)^T \end{equation} $$

where $M_{npm}$ is the 3x3 normalised primary matrix of the RGB colourspace and $I$ is a 3x3 identity matrix.

The whitepoint CIE XYZ tristimulus values are calculated as follows:

$$ \begin{equation} XYZ_W = (M_{npm}\cdot O)^T \end{equation} $$

where $O$ is a 3x1 ones filled matrix such as $O = [1, 1, 1]$.

The final step is to convert $XYZ_P$ and $XYZ_W$ CIE XYZ tristimulus values to xy chromaticity coordinates:

$$ \begin{equation} xy_P = XYZ\_to\_xy(XYZ_P)\\ xy_W = XYZ\_to\_xy(XYZ_W) \end{equation} $$

where $XYZ\_to\_xy$ function equations are expressed as follows:

$$ \begin{equation} x = \cfrac{X}{X+Y+Z}\\ y = \cfrac{Y}{X+Y+Z} \end{equation} $$

Colour implements the colour.primaries_whitepoint definition to compute the primaries and whitepoint from a given RGB colourspace normalised primary matrix:

In [3]:
# Computing *ACES2065-1* primaries and whitepoint.
npm = np.array([[9.52552396e-01, 0.00000000e+00, 9.36786317e-05],
                [3.43966450e-01, 7.28166097e-01, -7.21325464e-02],
                [0.00000000e+00, 0.00000000e+00, 1.00882518e+00]])

print(colour.primaries_whitepoint(npm))
print(colour.RGB_COLOURSPACES['ACES2065-1'].primaries)
print(colour.RGB_COLOURSPACES['ACES2065-1'].whitepoint)
(array([[  7.34700000e-01,   2.65300000e-01],
       [  0.00000000e+00,   1.00000000e+00],
       [  1.00000001e-04,  -7.70000004e-02]]), array([ 0.32168,  0.33767]))
[[  7.34700000e-01   2.65300000e-01]
 [  0.00000000e+00   1.00000000e+00]
 [  1.00000000e-04  -7.70000000e-02]]
(0.32168, 0.33767)

RGB to RGB Matrix to Normalised Primary Matrix

Given $M_{RED\rightarrow RGB}$, the publicised RED colourspace to ACES2065-1 colourspace matrix, and $M_{XYZ\rightarrow RGB}$ the known CIE XYZ colourspace to ACES2065-1 colourspace matrix, converting from CIE XYZ tristimulus values to RED colourspace is expressed by this equation:

$$ \begin{equation} RGB_{RED} = M_{RED\rightarrow RGB}^{-1}\cdot(M_{XYZ\rightarrow RGB}\cdot XYZ) \end{equation} $$

which can be rewritten as follows:

$$ \begin{equation} RGB_{RED} = M_{XYZ\rightarrow RED}\cdot XYZ \end{equation} $$

where $M_{XYZ\rightarrow RED}$ is the inverse of the $M_{npm}$ normalised primary matrix we would like to solve for, thus:

$$ \begin{equation} RGB_{RED} = M_{npm}^{-1}\cdot XYZ \end{equation} $$

It is possible to solve for $M_{npm}^{-1}$ by replacing the 3x1 $XYZ$ matrix with a 3x3 identity matrix $I$ such as:

$$ \begin{equation} M_{npm}^{-1} = M_{RED\rightarrow RGB}^{-1}\cdot(M_{XYZ\rightarrow RGB}\cdot I) \end{equation} $$

Implementation

In [4]:
DRAGONCOLOR_TO_ACES_2065_1 = np.array(
    [[0.532279, 0.376648, 0.091073],
     [0.046344, 0.974513, -0.020860],
     [-0.053976, -0.000320, 1.054267]])

DRAGONCOLOR2_TO_ACES_2065_1 = np.array(
    [[0.468452, 0.331484, 0.200064],
     [0.040787, 0.857658, 0.101553],
     [-0.047504, -0.000282, 1.047756]])

REDCOLOR_TO_ACES_2065_1 = np.array(
    [[0.451464, 0.388498, 0.160038],
     [0.062716, 0.866790, 0.070491],
     [-0.017541, 0.086921, 0.930590]])

REDCOLOR2_TO_ACES_2065_1 = np.array(
    [[0.480997, 0.402289, 0.116714],
     [-0.004938, 1.000154, 0.004781],
     [-0.105257, 0.025320, 1.079907]])

REDCOLOR3_TO_ACES_2065_1 = np.array(
    [[0.512136, 0.360370, 0.127494],
     [0.070377, 0.903884, 0.025737],
     [-0.020824, 0.017671, 1.003123]])

REDCOLOR4_TO_ACES_2065_1 = np.array(
    [[0.474202, 0.333677, 0.192121],
     [0.065164, 0.836932, 0.097901],
     [-0.019281, 0.016362, 1.002889]])

ACES2065_1_TO_sRGB = np.array(
    [[2.5217167, -1.1341655, -0.3875512],
     [-0.276476, 1.3727113, -0.0962348],
     [-0.015382, -0.1529940, 1.1683768]])

RED_TO_ACES_2065_1_MATRICES = {
    'DRAGONcolor': DRAGONCOLOR_TO_ACES_2065_1,
    'DRAGONcolor2': DRAGONCOLOR2_TO_ACES_2065_1,
    'REDcolor': REDCOLOR_TO_ACES_2065_1,
    'REDcolor2': REDCOLOR2_TO_ACES_2065_1,
    'REDcolor3': REDCOLOR3_TO_ACES_2065_1,
    'REDcolor4': REDCOLOR4_TO_ACES_2065_1}


def RGB_to_RGB_matrix_to_normalised_primary_matrix(RGB_to_RGB_matrix,
                                                   XYZ_to_RGB_matrix):
    M = np.einsum('...ij,...jk->...ik', RGB_to_RGB_matrix, M)

    M = np.linalg.inv(M)

    return M


def RED_colourspaces_derivation():
    RED_colourspaces = {}
    for name, RGB_to_RGB_matrix in RED_TO_ACES_2065_1_MATRICES.items():
        NPM = RGB_to_RGB_matrix_to_normalised_primary_matrix(
            np.linalg.inv(RGB_to_RGB_matrix),
            colour.RGB_COLOURSPACES['ACES2065-1'].XYZ_to_RGB_matrix)

        P, W = colour.primaries_whitepoint(NPM)

        RED_colourspaces[name] = colour.RGB_Colourspace(
            name,
            primaries=P,
            whitepoint=W,
            illuminant='D60',
            RGB_to_XYZ_matrix=NPM,
            XYZ_to_RGB_matrix=np.linalg.inv(NPM))
    return RED_colourspaces


RED_COLOURSPACES = RED_colourspaces_derivation()
colour.RGB_COLOURSPACES.update(RED_COLOURSPACES)

# Computing a derived *sRGB* colourspace as methodology 
# validation. Notice that the derived primaries are not exactly
# the same, which is likely to be the result of the input 
# matrices being rounded and chromatic adaptation precision.
NPM = RGB_to_RGB_matrix_to_normalised_primary_matrix(
    ACES2065_1_TO_sRGB,
    colour.RGB_COLOURSPACES['ACES2065-1'].XYZ_to_RGB_matrix)
P, W = colour.primaries_whitepoint(NPM)
colour.RGB_COLOURSPACES['sRGB Derived'] = colour.RGB_Colourspace(
    'sRGB Derived',
    primaries=P,
    whitepoint=W)

RGB_colourspaces_CIE_1931_chromaticity_diagram_plot(
    ['sRGB', 'sRGB Derived'] + sorted(RED_COLOURSPACES.keys()))
Out[4]:
True

Analysis

Analysis of the various RED colourspaces primaries performance is done by converting a CIE XYZ reference image to the various RED colourspaces using our set of primaries and the VideoVillage ones and comparing those outputs to the native The Foundry Nuke R3D reader output.

Note: The derived whitepoint is D60 because it is the one used by ACES2065-1 colourspace specification.

Nuke R3D Reader CIE XYZ Reference

The CIE XYZ Reference images below have been generated within The Foundry Nuke using the following Read node settings:

  • decode resolution: EighthGood
  • color space: REDcolor3
  • gamma space: Half Float Linear

The publicised REDcolor3 colourspace to ACES2065-1 colourspace matrix and the known matrix converting from ACES2065-1 colourspace to CIE XYZ colourspace were used to convert from REDcolor3 colourspace to CIE XYZ colourspace.

In [5]:
IMAGES_BASENAME = ('A014_C018_1227WK', 'A028_C158_09065U')

OECF = colour.RGB_COLOURSPACES['sRGB'].transfer_function

for basename in IMAGES_BASENAME:
    image_plot(OECF(colour.read_image(
    'resources/images/red/{0}_XYZ.exr'.format(basename))),
               label='{0} - CIE XYZ Reference'.format(basename))