Colour is a spectrum of amplitudes at different wavelengths.
[ solarjourneyuse.com ]
[ 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:
[ Gonzales & Woods ]
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 |
---|---|---|
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.
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 ]
stored as [0,255]x[0,255]x[0,255]
used as [0,1]x[0,1]x[0,1] in computations
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) ); }
$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;
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.
[ alanzucconi.com/2016/01/06/colourinterpolation ]
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".