Going up in color bit depth

November 8th, 2023

The simplest thing: how to extend a low bit depth color to a higher bit depth one? Well, there’s a trick.

The Bit Extension Trick

Let’s say we convert a greyscale intensity from 8 to 16 bits. Let’s call these values “small” and “big”, respectively. If you just do

uint16_t big = small << 8;

then you add some downward bias! The high bits are always zero. The trick is to “stretch” the new values to cover the whole 16-bit range by duplicating the high bits on top of those zero bits:

uint16_t big = (small << 8) | small;

The bits go like this:

expression             bit sequence

 small               =           1234 5678
 small << 8          = 1234 5678 0000 0000 <-- low bits are zero
(small << 8) | small = 1234 5678 1234 5678 <-- low bits are copied

For colors this technique is very important because otherwise you wouldn’t reach full white after bit extension. In color grading terms, it’s equivalent to a slight gain boost. I didn’t really get it when I first was introduced to this trick as an intern but have learned to appreciate its effectiveness :)

Going down

Truncating from higher to lower bit depth adds no bias, just do small = big >> 8.

You do get banding though which you can try to hide via dithering. For real-time rendering, see https://loopit.dk/rendering_inside.pdf slides 56–⁠73. For static images of very low bit depths, you’ll want error diffusion. Wikipedia has a neat gallery of different approaches.

There’s some subtlety here which is touched on in the slide deck linked above but that’s a story for another night.

In a discussion commenter FRIGN pointed out that the trick above is basically multiplication:

Why not just write it as a multiplication with 257, as usual and easily mathematically derivable for other depth transforms ((2^16-1)/(2^8-1))?

256+1=257, so we can see the bitshift and added original value easily. This is not magic.

Yes! Well the formula seems to work only if the higher bit depth is divisible by the lower one, like 16/8 in the example. So if you go from 5 to 8 bits for example, then you can’t do it with integer multiplication anymore.

Also, commenter roryokane brought up CSS colors, which are a good example. Color #abc gets expanded to #aabbcc, with each 4-bit nibble being duplicated.

Thanks to Pauli and Lauri for feedback on this explainer.