ColorMatrix

ColorMatrix

Mixing colour channels with a 3x3 Matrix

Matrices in Nuke, the series: Convolution Matrix · Transform Matrix · ColorMatrix · Convolve & Deconvolution · 3D Matrices · Morphology · CornerPin & Homography · BlinkScript Matrices

This is the third page of my series about matrices in Nuke. The Convolution Matrix looks at the pixels around a pixel, the Transform Matrix moves pixels somewhere else. The ColorMatrix does neither: it stays on the same pixel and decides how much of the red, green and blue inputs goes into each output channel.
It looks like a boring grid of nine numbers, but with it you can build a luminance, change saturation, swap channels, warm up a plate, convert between colour primaries or rotate a normals pass into camera space. All with one node, in linear light, without clamping anything.

All the examples below use Marcie, the classic Kodak test image (you can find it on my free test images page), read as sRGB and processed in linear, exactly like Nuke does.



Useful Links



Index



Overview: how to read it

A ColorMatrix is a 3x3 grid. Read it row by row: every row builds one output channel, and the three numbers in that row say how much of the input red, green and blue to add together.

R inG inB in
R out100
G out010
B out001

So the first row reads: red out = (red in × 1) + (green in × 0) + (blue in × 0). The second row builds green, the third builds blue. With 1 on the diagonal and 0 everywhere else, every channel takes 100% of itself and nothing from the others: the image doesn't change.

Now change only the second number of the first row to 0.5. Red out becomes red + 0.5 × green: a pure green pixel (0, 1, 0) turns into (0.5, 1, 0), and all the greens of the image drift towards yellow. That is the whole idea: the diagonal is a gain on each channel, everything off the diagonal leaks one channel into another.

  • In one line

R' = m00·R + m01·G + m02·B
G' = m10·R + m11·G + m12·B
B' = m20·R + m21·G + m22·B

Rows are the outputs, columns are the inputs. In maths terms it is a matrix multiplied by the colour vector [R G B] of every pixel.



In Nuke

You can create it from Color → Math → ColorMatrix (or just press Tab and type ColorMatrix). The knobs are few:

- channels: the three channels the matrix works on. Usually rgb, but it can be any layer: normals, motion vectors, position...
- matrix: the nine values. Each row is one output channel.
- invert: uses the inverse of the matrix. Perfect to undo a conversion: same node, same numbers, one checkbox.
- mask, (un)premult by and mix: the usual friends of every colour node.

Wherever you find a dark box like the one below, click it, copy it (Ctrl+C / Cmd+C) and paste it (Ctrl+V / Cmd+V) directly in Nuke's Node Graph: you get a ColorMatrix with the values already set. The row of numbers above it is the same matrix in plain text.



Two golden rules

1. Linear light. The matrix is linear maths, so the same numbers give different results on linear, log or sRGB data. In Nuke you are normally in scene-linear, and that's where the matrices on this page are meant to be used. Colour-science matrices (primaries, XYZ, ACES) only make sense in linear.

2. Look at the sum of every row. A grey pixel has R = G = B. If every row sums to 1, a grey stays exactly the same grey: you are changing colours but not the neutrals. If a row sums to more than 1 that channel gets brighter on the greys (and the greys get tinted), if it sums to less than 1 it gets darker. Many "mysterious" casts come from a row that doesn't sum to 1.

Two more things worth knowing: negative values are allowed (they subtract a channel and can push pixels below zero), and nothing is clamped, so highlights above 1 stay above 1.



ColorMatrix Lab

Before the examples, play with it. Type any value in the grid, use the sliders or pick a preset: the image updates live. Your browser does the maths in linear light, like Nuke. Drag on the image to move the wipe between the original and the result, then copy the node and paste it in Nuke.

R inG inB insum
R out1
G out1
B out1

Every row sums to 1: greys stay grey.

The saturation slider uses the Rec.709 luminance weights, the hue slider rotates the colours around the grey axis, so both keep the neutrals untouched (look at the sums).



Matrices

Nine matrices, from the most obvious to the most useful. Each one shows the result, the matrix and the node ready to paste.

  • 01. Identity
ColorMatrix in Nuke: identity matrix, the image does not change

The default matrix. Every channel takes 100% of itself. It is the starting point of every other matrix and a good sanity check: if a ColorMatrix set to identity changes your image, the problem is somewhere upstream.

R inG inB in
R out100
G out010
B out001
  • 02. Luminance (Rec.709)
ColorMatrix in Nuke: Rec.709 luminance, black and white image

Three identical rows means three identical channels: a black & white image. The weights follow how bright our eyes perceive each primary, that's why green gets 71% and blue only 7%. A simple average (0.333 everywhere) looks wrong: blue skies become too bright and greens too dark.

The weights depend on the primaries of your working space: 0.2126 0.7152 0.0722 for linear sRGB / Rec.709, 0.2722 0.6741 0.0537 for ACEScg.

R inG inB in
R out0.21260.71520.0722
G out0.21260.71520.0722
B out0.21260.71520.0722

Luminance keyer. Shuffle one of the three (identical) channels into alpha and shape it with a Grade or an Expression: you get a luma key where you choose the weights. Put more red in the row to key fire and skin, more blue to key the sky. A nice discussion about it is in the Reddit link at the top of the page.

  • 03. Saturation 50%
ColorMatrix in Nuke: saturation reduced to 50 percent

Saturation is a mix between the luminance matrix L (example 02) and the identity I:

M = (1 − s) · L + s · I

With s = 0 you get black & white, with s = 1 the original. Here s = 0.5. Every row still sums to 1, so the greys don't move.

R inG inB in
R out0.60630.35760.0361
G out0.10630.85760.0361
B out0.10630.35760.5361
  • 04. Saturation 150%
ColorMatrix in Nuke: saturation increased to 150 percent

Same formula with s = 1.5. Negative numbers appear: to push a colour away from grey, the matrix subtracts the other two channels. Very saturated pixels can go below zero, so check the shadows with the Viewer's colour picker before you pass the image on.

R inG inB in
R out1.3937-0.3576-0.0361
G out-0.10631.1424-0.0361
B out-0.1063-0.35761.4639
  • 05. Swap Red and Blue
ColorMatrix in Nuke: red and blue channels swapped

Only zeros and ones, one 1 per row: a permutation. Red out reads the blue input, blue out reads the red one. It fixes images saved as BGR and it gives you crazy looks for free. A Shuffle does the same, but with a ColorMatrix you can go halfway: try 0.5 instead of 1 and mix the channels instead of swapping them.

R inG inB in
R out001
G out010
B out100
  • 06. One channel everywhere
ColorMatrix in Nuke: red channel copied in all three channels

The first column full of ones: every output reads the red input. It's like pressing R in the Viewer, but it is a real image you can use downstream. Skin is bright and smooth in red, so this is a classic starting point for beauty work and for keys; move the ones to the second or third column to use green or blue instead.

R inG inB in
R out100
G out100
B out100
  • 07. Warm gain
ColorMatrix in Nuke: warm gain, more red and less blue

Values only on the diagonal: each channel is just multiplied by a number, exactly like the gain of a Grade node with rgb split (1.15, 1, 0.85). Here the rows don't sum to 1 on purpose: we want the greys to become warmer.

R inG inB in
R out1.1500
G out010
B out000.85
  • 08. Sepia
ColorMatrix in Nuke: sepia tone

The classic sepia matrix from the image-editing world: a luminance-like mix for every row, with more weight on red and less on blue. The rows sum to 1.351, 1.203 and 0.937, so the image gets brighter and warmer. These numbers were designed for 8-bit display images: in linear they push the highlights above 1, so follow with a Grade or multiply the whole matrix by about 0.74.

R inG inB in
R out0.3930.7690.189
G out0.3490.6860.168
B out0.2720.5340.131
  • 09. Rec.709 (linear sRGB) to ACEScg
ColorMatrix in Nuke: linear sRGB to ACEScg primaries conversion

This is where the ColorMatrix really earns its place. Changing colour primaries is a 3x3 matrix in linear light: this one goes from linear sRGB / Rec.709 to ACEScg (AP1), including the white point adaptation from D65 to the ACES white.

Seen through an sRGB Viewer it looks a bit desaturated: that is correct, it's the data that changed, not the look. Tick invert and the same node goes back from ACEScg to linear sRGB.

R inG inB in
R out0.61310.33950.0474
G out0.07020.91640.0134
B out0.02060.10960.8698

In a real pipeline let OCIO do the conversions (OCIOColorSpace, the Read node's colorspace), but these matrices are handy for checks, for a gizmo that must work without OCIO, or for a CG render that came out in the wrong primaries. Two more you may need:

ACEScg → linear sRGB (the inverse, if you prefer numbers to the checkbox)

R inG inB in
R out1.7050510-0.6217921-0.0832590
G out-0.13025641.1408047-0.0105483
B out-0.0240034-0.12896901.1529724

linear sRGB → CIE XYZ (D65), the middle row is the luminance of example 02

R inG inB in
R out0.41245640.35757610.1804375
G out0.21267290.71515220.0721750
B out0.01933390.11919200.9503041


Normals: from world space to camera space

The ColorMatrix doesn't know it's working on colours. Point the channels knob to a normals pass and the three channels become the X, Y and Z of a direction. And rotating a direction is, again, a 3x3 matrix.

This trick comes from Masahiro Teraoka's blog (link at the top). Renderers often give you normals in world space, while many relighting tools want them in camera space. The camera already knows the rotation we need: it's inside its world_matrix.

- 1. Use the same Camera that rendered the pass (the classic Camera node), with scale 1.
- 2. Create a ColorMatrix and set channels to your normals layer.
- 3. Link the nine values to the rotation part of the camera's world_matrix: elements 0, 1, 2, 4, 5, 6, 8, 9, 10 (3, 7 and 11 are the translation, not needed for a direction).
- 4. Tick invert: world → camera space. Untick it: camera → world space.

The few lines of Python below do the linking for you. Select the ColorMatrix and run them in the Script Editor:

import nuke

cam = "Camera1"   # the camera that rendered the normals
cm = nuke.selectedNode()

# rotation part of the 4x4 world matrix, row by row
for i, element in enumerate([0, 1, 2, 4, 5, 6, 8, 9, 10]):
    cm["matrix"].setExpression("%s.world_matrix.%d" % (cam, element), i)

cm["invert"].setValue(True)   # world space -> camera space

Or paste this node, already linked to Camera1:

Normals from world to camera space. Set channels to your normals layer and rename Camera1 if needed.

Why does it work? The upper 3x3 of the world matrix rotates a direction from camera space to world space; the inverse rotates it back. For a position pass (P) a 3x3 isn't enough, because positions also need the translation: for that you need a 4x4 colour matrix, like C44Matrix by Ivan Busquets or the BlinkScript C44Kernel by Adrian Pueyo, which can also read the matrix straight from a Camera or an Axis.



Combining matrices with Python

Two ColorMatrix nodes in a row are the same as one ColorMatrix with the two matrices multiplied. And like in the Transform Matrix page, the order matters: the matrix applied last goes on the left. Desaturating and then converting to ACEScg is not the same as converting and then desaturating (the luminance weights are different in the two spaces).

A few lines to set a matrix from a list of nine values and to bake two matrices into one node:

import nuke

def mat_mul(a, b):
    """a * b, both 3x3 matrices as flat lists of 9 values, row by row."""
    return [sum(a[r * 3 + k] * b[k * 3 + c] for k in range(3))
            for r in range(3) for c in range(3)]

def set_matrix(node, values):
    for i, v in enumerate(values):
        node["matrix"].setValue(v, i)

sat_050 = [0.6063, 0.3576, 0.0361,
           0.1063, 0.8576, 0.0361,
           0.1063, 0.3576, 0.5361]
to_acescg = [0.6131, 0.3395, 0.0474,
             0.0702, 0.9164, 0.0134,
             0.0206, 0.1096, 0.8698]

# first desaturate, then convert: the last one goes on the left
cm = nuke.createNode("ColorMatrix")
set_matrix(cm, mat_mul(to_acescg, sat_050))

# read the values back
print([cm["matrix"].getValue(i) for i in range(9)])


Where is the offset? (4x5 matrices)

If you look for "colour matrix" online, you'll find many pages about 4x5 or 5x5 matrices: Flash's ColorMatrixFilter, SVG's feColorMatrix (the one running in the Lab above), Rainmeter skins, Android... They have an extra row and column for alpha and an extra column for an offset, a value added after the multiplication. The maths is the same as Nuke's, with two extras.

Nuke's ColorMatrix is a pure 3x3: no alpha and no offset. You don't miss anything, you just use the right node for each part:

- Offset → an Add node after the ColorMatrix (or the offset of a Grade).
- Alpha row → a Shuffle or Copy after the matrix, or an Expression node.
- Invert (−1 on the diagonal + offset 1 in those guides) → the Invert node, or ColorMatrix with −1 followed by Add 1.
- Brightness → Multiply. Contrast around a pivot → Grade or ColorCorrect.
- Threshold: the Tuts+ article multiplies a channel by −1 and adds an offset, so everything above the offset goes to black. In Nuke: ColorMatrix + Add + Clamp.

Remember that those guides work in 8 bit (0 to 255) or display-referred 0 to 1, while Nuke works in float linear: the coefficients can be reused, the offsets must be scaled.



The other matrices in Nuke

With this page the 2D family is complete, but matrices are everywhere in Nuke. Here is the full list:

- Matrix (Filter → Matrix): the convolution kernel, it mixes a pixel with its neighbours. → Convolution Matrix
- Transform and CornerPin: under the hood a 2D transform is a 3x3 (in Nuke a 4x4) matrix, and CornerPin2D shows it in its extra matrix tab. → Transform Matrix
- ColorMatrix: the channel mixer, this page.
- Convolve: like the Matrix node, but the kernel is an image (great for bokeh and real lens shapes). → Convolve & Deconvolution
- 3D nodes: Camera, Axis, TransformGeo, Card... all have a 4x4 local_matrix and world_matrix, and the camera has a projection matrix. You can read them with expressions, exactly as we did for the normals. → 3D Matrices
- Python: the nuke.math module has Matrix3, Matrix4 and Vector3 classes to do the maths in your scripts.
- Colour management: Colorspace and OCIO nodes convert primaries with 3x3 matrices like example 09.



The matrix series

If you are in love with Matrices :) have a look at the other pages:
Convolution Matrix Transform Matrix Convolve & Deconvolution 3D Matrices

Leave a comment