Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Preview: YCbCr - Y: Luminance; Cb: Chrominance-Blue; and Cr: Chrominance-Red Colorspace

Stough, 202-

The YCbCr colorspace is also oriented around adjusting color according to human vision perception as opposed to mechanism (r-g-b stimulus). Where HSV is built for describing and choosing colors, YCbCr is built for transmitting and compressing them. It’s the colorspace inside JPEG images and nearly all digital video.

The idea is to separate an image into one channel of brightness, the luma YY, and two channels of color difference, the chroma CbC_b (how much bluer than gray) and CrC_r (how much redder than gray). Our visual system is far more sensitive to fine detail in brightness than in color, so once they’re separated we can afford to store the chroma channels with much less detail. We’ll see that directly at the end of this notebook.

 

The Conversion

YCbCr is just a linear transform of RGB, plus an offset: a rotation, scaling, and shift of the RGB cube. skimage uses the ITU-R BT.601 standard, which for R,G,B∈[0,1]R,G,B \in [0,1] gives

[YCbCr]=[16128128]+[65.481128.55324.966−37.797−74.203112.0112.0−93.786−18.214][RGB]\begin{bmatrix} Y \\ C_b \\ C_r \end{bmatrix} = \begin{bmatrix} 16 \\ 128 \\ 128 \end{bmatrix} + \begin{bmatrix} 65.481 & 128.553 & 24.966 \\ -37.797 & -74.203 & 112.0 \\ 112.0 & -93.786 & -18.214 \end{bmatrix} \begin{bmatrix} R \\ G \\ B \end{bmatrix}

A few things to notice:

  • The YY row is a weighted sum with green weighted most and blue least, matching the eye’s sensitivity to each. A pure green pixel is perceived as much brighter than a pure blue one of the same intensity.

  • The CbC_b and CrC_r rows each sum to zero. Any gray pixel (R=G=BR=G=B) therefore has Cb=Cr=128C_b = C_r = 128: no color, only brightness.

  • The ranges are Y∈[16,235]Y \in [16, 235] and Cb,Cr∈[16,240]C_b, C_r \in [16, 240], a convention inherited from analog video that leaves some headroom at either end. (JPEG uses a slightly different “full-range” variant over [0,255][0,255].)

Let’s check this with pure red, green, blue, white and black.

array([[[ 81.5, 90.2, 240. ], [144.6, 53.8, 34.2], [ 41. , 240. , 109.8], [235. , 128. , 128. ], [ 16. , 128. , 128. ]]])

White and black land at Y=235Y=235 and Y=16Y=16 with neutral chroma (128,128)(128,128), as promised, and green has by far the largest luma of the three primaries.

 

Cb and Cr colormaps

I’m going to be careful here with the colormap to have a perceptually constant transition between yellow and blue (Cb) and cyan and red (Cr). I don’t want one side to be perceptually brighter than the other.

<Figure size 800x600 with 9 Axes>

The YY channel looks like a natural grayscale version of the image, with nearly all the detail: every flower petal, every ridge of the mountains. The chroma channels, by contrast, look smooth and blotchy. They mostly say “this region is bluish, this region is greenish”. That observation is the whole reason YCbCr exists.

 

Let’s visualize in 3D

<Figure size 400x400 with 1 Axes>
<Figure size 400x400 with 1 Axes>

 

View the Cb-Cr plane for various Y

The visualization of the YCbCr space is a bit weird, because it looks just like the rgb color cube, but maybe rotated, translated, and stretched a little. So I’m trying here to visualize the CbCr plane for any luma Y. skimage notes that Y must be in 16-235. Also, when you transform back you could get out-of-range values for r,g,b. Normally these get clipped to show a CbCr plane like below, but a lot of that plane is invalid. So here, I’ll show the whole (clipped to 0-1) plane and then also just the valid part (within the color cube). What we’re doing here is taking slices orthogonal to the Luminance direction.

<Figure size 800x400 with 2 Axes>
<contextlib.ExitStack at 0x7f4bdb5e5d60>

 

Chroma Subsampling

Here’s the payoff. Let’s throw away most of the color information: keep only every 8th row and column of CbC_b and CrC_r (that’s 164\frac{1}{64} of the chroma samples), then scale them back up to full size and convert back to RGB. For comparison, we’ll do the same thing to YY instead, leaving the chroma alone.

<Figure size 1000x300 with 3 Axes>

Discarding 6364\frac{63}{64} of the color samples is nearly invisible. You may spot a little color bleeding at the edges of the brightest petals if you look closely. Doing the same thing to brightness wrecks the image. Measured as mean squared error, the luma-subsampled image is roughly eight times worse.

This is exactly what JPEG and video codecs do. The common “4:2:0” scheme keeps YY at full resolution but stores CbC_b and CrC_r at half resolution in each direction, cutting the total data in half before any other compression even begins.