up to Schedule & Notes

RGB/YUV colourspace conversion

Colour

Colour is a spectrum of amplitudes at different wavelengths.

sun spectrum between 250nm and 2500nm [ solarjourneyuse.com ]

visible light spectrum [ markkness.net ]

The eye has three (or four, in some people) types of cones which are sensitive predominantly to blue (2%), green (33%), and red (65%) wavelengths:

graph of cone sensitivities[ Gonzales & Woods ]

Colourspaces

A colourspace is a 3D space of colours. Each point in the space corresponds to a colour.

RGB colourspace 2D slice in Cb/Cr of YCbCr colourspace HSV colourspace
RGB colour cube 2D slice in Cb/Cr of YCbCr colour space HSV colourspace cylinder

In the RGB colour cube above, the origin is at the hidden corner, where R=0, G=0, and B=0. The white corner in front has R=1, G=1, B=1.

In the YCbCr colourspace, Y is the luminance (i.e. intensity), while Cb and Cr are the chroma (i.e. colour).

Variants of the YCbCr colourspace are

You will sometimes see Y' instead of Y. Y' is a "gamma-transformed" Y. We'll talk about gamma later.

Colourspace conversion

Often, we want to convert between RGB and YCbCr so that we can work on only the intensity:

[ DEMO of why its important to work on intensity only instead of three RGB channels ]

RGB

stored as [0,255]x[0,255]x[0,255]

used as [0,1]x[0,1]x[0,1] in computations

YCbCr

stored as [16,235]x[16,240]x[16,240]

used as [0,1]x[-0.5,0.5]x[-0.5,0.5] in computations

Here's some generic conversion code:

    // Convert RGB to YCbCr
    //
    // See the specification "CCIR 601"
    //
    // Computation:
    //       R     G     B         Y       Cr         Cb
    //     [0,1] [0,1] [0,1] <-> [0,1] [-0.5,0.5] [-0.5,0.5]
    //
    // Storage:
    //        R       G       B           Y        Cr       Cb
    //     [0,255] [0,255] [0,255] <-> [16,235] [16,240] [16,240]
    //
    // The 'color' type has three components:
    //
    //    color x;
    //    red(x), green(x), blue(x)
    //
    // The red/green/blue are used for the first/second/third components
    // no matter what space (RGB or YUV or something else) is used


    color RGB2YCbCr( color rgb ) {

	// Get RGB components in [0,1]x[0,1]x[0,1]

	float r = red(   rgb )/255;
	float g = green( rgb )/255;
	float b = blue(  rgb )/255;

	// Compute YCbCr components in [0,1]x[-0.5,0.5]x[-0.5,0.5]

	float y  = r *  0.299    + g * 0.587    + b * 0.114;
	float cb = r * -0.168736 - g * 0.331264 + b * 0.5;
	float cr = r *  0.5      - g * 0.418688 - b * 0.081312;

        // Convert to ranges in one byte
	      
	y  =  16 + 219 * y;     // convert y  in [0,1]      to y  in [16,235]
	cb = 128 + 224 * cb;    // convert cb in [-0.5,0.5] to cb in [16,240]
	cr = 128 + 224 * cr;    // convert cb in [-0.5,0.5] to cb in [16,240]

	return color( round(y), round(cb), round(cr) );
    }


    // Convert YUV to RGB
    //
    // See the specification "CCIR 601"

    color YCbCr2RGB( color ycrcb ) {

      // Get YCbCr components in [0,255]x[0,255]x[0,255]

      float y  = red(   ycrcb );
      float cb = green( ycrcb );
      float cr = blue(  ycrcb );

      // Denormalize YCbCr components into [0,1]x[-0.5,0.5]x[-0.5,0.5]

      y  = (y-16)/219;
      cb = (cb-128)/224;
      cr = (cr-128)/224;

      // Compute RGB components in [0,1]x[0,1]x[0,1]

      float r = y - cb * 0.0009 + cr * 1.4017;
      float g = y - cb * 0.3437 - cr * 0.7142;
      float b = y + cb * 1.7722 + cr * 0.0010;

      // Return RGB in [0,255]x[0,255]x[0,255]

      return color( round(r*255), round(g*255), round(b*255) );
    }
      

Point Processing [3.2]

$s = T(r)$ maps old pixel value $r$ to new pixel value $s$

For greyscale pixels, or if working on intensity only, the intensity is typically represented in the range [0,1].

So $T(r)$ can be drawn as a function that maps [0,1] to [0,1].

To work on intensity only, convert RGB to YCbCr, work on Y only, then convert back to RGB:

      color rgb       = image[x,y];               // in [0,255]x[0,255]x[0,255]
      color ycrcb     = RGB_to_YCbCr( rgb );      // in [16,235]x[16,240]x[16,240]
      float intensity = (Y( ycrcb )-16)/(235-16); // in [0,1]

      ... modify intensity in [0,1] ...

      color newYCbCr = color( intensity*(235-16)+16, Cb(ycbcr), Cr(ycbcr) )
      color newRGB = YCbCr_to_RGB( newYCbCr )
      image[x,y] = newRGB;
    

Combining colours

lerp means "linear interpolation":

Intuitively, lerping draws a straight line between two points in the colourspace being used.

However, a straight line in the colourspace usually does not result in a perceptually linear interpolation.

In the RGB colourspace, we could combine (r1,g1,b1) with (r2,g2,b2) by lerping each component separately. This gives poor results.

Or we could convert RGB to HSV and lerp each of the H, S, and V components. This is also pretty bad.

The best way is to lerp in another colourspace called HCL (for Hue, Chroma, Lightness), but converting to and from HCL is expensive.

Shown below is what happens when we lerp each channel independently, in each of three colourspaces. Note how RGB lerping results in dimmer intermediate colours, while HSV lerping results in the wrong intermediate colours. HCL (written as "LCH" in the figure) gives the best perceptual linear interpolation and maintains a fairly constant brightness.

colour lerping in three colourspaces [ alanzucconi.com/2016/01/06/colour­interpolation ]

A very good explanation with interactive examples can be found at www.alanzucconi.com/2016/01/06/colour-interpolation/ Be sure to look at the following pages on "Hue interpolation" and "Luminosity interpolation".

up to Schedule & Notes