Pre-PR proposal improve 565 to 888 color conversions (and perhaps others)

I’ve talked w/ kisvegabor and embeddedt a bit about this in a semi-unrelated PR, but I thought I would discuss this here more visibly before I start coding anything.

  1. I believe that LittlevGL’s current 565 to 888 conversion code is flawed (which opens up the possibility of others to/from)
    lv_misc/lv_color.h:
    static inline uint32_t lv_color_to32(lv_color_t color)
    {
        ...
        #elif LV_COLOR_DEPTH == 16
        lv_color32_t ret;
        ret.ch.red = color.ch.red * 8; /*(2^8 - 1)/(2^5 - 1) = 255/31 = 8*/
        ret.ch.green = color.ch.green * 4; /*(2^8 - 1)/(2^6 - 1) = 255/63 = 4*/
        ret.ch.blue = color.ch.blue * 8; /*(2^8 - 1)/(2^5 - 1) = 255/31 = 8*/
        ret.ch.alpha = 0xFF;
        return ret.full;
        ...
    }

I think the above code should cleanly convert 565 LV_COLOR_RED of 0xF800 to 888 of 0xFF0000, but it doesn’t.
I understand the important performance rationale for 255/31=8 and not using an actual floating point of 8.22580645 and the loss that integer division introduces; minimizing this loss is actually one of my points for suggesting an improvement.
The 5 bit red channel of 0xF800 is 2^5-1 or 31 or 0x1F.
Per the above code logic, 31 * 8 is 248, or 0xF8.
Thus, the above code converts 565 0xF800 to 888 of 0xF80000 instead of 0xFF0000.
I understand that 565 to 888 conversion is “lossy/imperfect”, but when mapping a number from one scale to another, I believe at least the min and max values should convert without loss.
Thus, I think that the above code needs to change.

  1. There is a mystery in the numbers of this commonly used more precise RGB 565 to 888 conversion code:
    SO Q&A: How does one convert 16-bit RGB565 to 24-bit RGB888?
    R8 = ( R5 * 527 + 23 ) >> 6;
    G8 = ( G6 * 259 + 33 ) >> 6;
    B8 = ( B5 * 527 + 23 ) >> 6;

The discussion in the above SO answer suggests 527 may have been incorrectly rounded up instead of down and should be 526 (which if true would also change the 23 addition.
Searching the internets gave me no clue as to the origin of these values.
I have created a spreadsheet exploring how these values were derived.
https://docs.google.com/spreadsheets/d/1PppX8FJpddauAPasHwlNgIPGIuPGPNvRbhilIQ8w-7g/edit#gid=0
It seems like nearly any listed multiplier and adder can be used, but it is still a mystery to me why those specific 6 bit shift values were chosen over any others.

So, with all of the above said, has anyone noticed undesirable 565 to 888 color conversions, or others, or am I the only one?

Pv

It’s unlikely that anyone paid close enough attention (until you came along :wink: ). Most people probably just fiddle with their RGB numbers (565, 888, or otherwise) until it looks good. I’m one of those people.

I agree.

I noticed it, when I use LV_COLOR_DEPTH 16 is simulator. The simulator has RGB888 color format therefore a 16->24 conversation is required. I’ve noticed that the LV_COLOR_WITHE is converter to a green-ish white. However it didn’t bother me too much as it’s only a simulator…
Anyway a fix would be welcome!

The simple 565 to 888 fix is to change the equation from the above OP’s simple * 8 and * 4 to either Apple’s version, a common/popular version on the interwebs, or one that I derived.
Apple:

    /*
     * Per https://developer.apple.com/documentation/accelerate/1533159-vimageconvert_rgb565toargb8888
     * Apple must have an ingenious reason to divide an integer by 63 or prime 31!
     */
    ret.ch.red   = ( color.ch.red * 255 + 15 ) / 31;
    ret.ch.green = ( color.ch.green * 255 + 31 ) / 63;
    ret.ch.blue  = ( color.ch.blue * 255 + 15 ) / 31;

Popular:

    /**
     * Per https://stackoverflow.com/a/9069480/252308
     * These values can be searched for and seen to be used in many places.
     */
    ret.ch.red   = ( color.ch.red * 527 + 23 ) >> 6;
    ret.ch.green = ( color.ch.green * 259 + 33 ) >> 6;
    ret.ch.blue  = ( color.ch.blue * 527 + 23 ) >> 6;

Mine:

    /**
     * Per https://docs.google.com/spreadsheets/d/1PppX8FJpddauAPasHwlNgIPGIuPGPNvRbhilIQ8w-7g/edit#gid=0
     * Truly any of the listed multipliers and adders would work
     */
    ret.ch.red   = ( color.ch.red * 526 + 14 ) >> 6;
    ret.ch.green = ( color.ch.green * 259 + 3 ) >> 6;
    ret.ch.blue  = ( color.ch.blue * 526 + 14 ) >> 6;

Which do you recommend?
In all modesty, I understand mine and not the others, so I recommend mine.
Truly I think the following are the most precise best, but they seem radically different than anything else I have seen out there, so I’m a bit wary to use them:

    ret.ch.red   = ( color.ch.red * 33 - 3 ) >> 2;
    ret.ch.green = ( color.ch.green * 4 + 3 );
    ret.ch.blue  = ( color.ch.blue * 33 - 3 ) >> 2;

Pv

3 Likes