Search code examples
calgorithmrgbdata-conversion

Proper RGB888 to RGB565 conversion


I've found three RGB888 to RGB565 conversion algorithms, but I'm not sure which is the most correct. The first two produce the same results. The third produces a different result, but does agree with an online calculator. And the third one makes sense, since it does scaling. But the first two are going to be faster since there's no integer multiplication or division.

uint8_t r8, g8, b8;
uint16_t r5, g6, b5, val;

r8 = someredvalue;
g8 = somegreenvalue;
b8 = somebluevalue;


// first method:
b5 =  (b8  >> 3)        & 0x001F;
g6 = ((g8  >> 2) <<  5) & 0x07E0;
r5 = ((r8  >> 3) << 11) & 0xF800;
val = (r5 | g6 | b5);
printf("%d, %d, %d, %u\n", r8, g8, b8, val);


// second method:
val = ((r8 & 0b11111000) << 8) | ((g8 & 0b11111100) << 3) | (b8 >> 3);
printf("%d, %d, %d, %u\n", r8, g8, b8, val);


// third method:
r5 = r8 * 31 / 255;
g6 = g8 * 63 / 255;
b5 = b8 * 31 / 255;     
val =  (r5 << 11) | (g6 << 5) | (b5);
printf("%d, %d, %d, %u\n", r8, g8, b8, val);

Solution

  • The first two produce the same results.

    Because they are equivalent, at least for compilers that support 0bxxx-style binary constants, which is an extension to standard C. Both take the color values of the 565 format as the correct number of the most significant bits of the corresponding color value in the 888 format.

    The third produces a different result, but does agree with an online calculator.

    And the third one makes sense, since it does scaling.

    They all do scaling. The third one just uses slightly different scale factors than the others do.

    Consider the red channel. Five bits can represent values from 0 to 31. Eight bits can represent values from 0 to 255. Mathematically, the factor for scaling linearly from the latter range to the former is 31 / 255, as the third approach uses. The other two are dividing by 8 (via shift), which is the same arithmetically as scaling by 32 / 256. Those are not the same scale factor, of course, but they are close.

    But before you take method 3 to be more correct, you should consider the effects of integer arithmetic. For example, which 8-bit red values map to 31 in 5-bit representation? With method 3, only one does: 255. Value 254 scales to slightly less than 31, which is truncated to 30. This variation on the conversion is thus non-uniform with respect to how many RGB888 colors corespond to each RGB565 color.

    The bit-shifting approach, on the other hand, is uniform in this sense. When converting this way, each RGB565 color corresponds to the same number (256) of RGB888 colors.

    I'm not sure which is the most correct.

    Overall, the two rescalings produce very similar results, within 1 unit per channel of each other. Both are linear, up to truncation to integer. Neither is more correct than the other in an absolute sense. The bit-shifting alternatives are distinguished by distributing the results uniformly over the RGB565 space. The other alternative is distinguished by mapping only maximum RGB888 intensities to maximum RGB565 intensities. There might also be a performance difference, but if that's important to you then you should measure. For most purposes, I would be inclined to choose the uniformity of one of the bit-shifting approaches, but you may think differently.