Question

I’ve got my hands on a 16-bit rgb565 image (specifically, an Android framebuffer dump), and I would like to convert it to 24-bit rgb888 for viewing on a normal monitor.

The question is, how does one convert a 5- or 6-bit channel to 8 bits? The obvious answer is to shift it. I started out by writing this:

puts("P6 320 480 255");
uint16_t buf;
while (read(0, &buf, sizeof buf)) {
    unsigned char red = (buf & 0xf800) >> 11;
    unsigned char green = (buf & 0x07e0) >> 5;
    unsigned char blue = buf & 0x001f;
    putchar(red << 3);
    putchar(green << 2);
    putchar(blue << 3);
}

However, this doesn’t have one property I would like, which is for 0xffff to map to 0xffffff, instead of 0xf8fcf8. I need to expand the value in some way, but I’m not sure how that should work.

The Android SDK comes with a tool called ddms (Dalvik Debug Monitor) that takes screen captures. As far as I can tell from reading the code, it implements the same logic; yet its screenshots are coming out different, and white is mapping to white.

Here’s the raw framebuffer, the smart conversion by ddms, and the dumb conversion by the above algorithm. Note that the latter is slightly darker and greener.

(By the way, this conversion is implemented in ffmpeg, but it’s just performing the dumb conversion listed above, leaving the LSBs at all zero.)

I guess I have two questions:

  • What’s the most sensible way to convert rgb565 to rgb888?
  • How is DDMS converting its screenshots?
Was it helpful?

Solution

You could shift and then or with the most significant bits; i.e.

Red 10101 becomes 10101000 | 101 => 10101101
    12345         12345---   123    12345123

This has the property you seek, but it's not the most linear mapping of values from one space to the other. It's fast, though. :)

Cletus' answer is more complete and probably better. :)

OTHER TIPS

You want to map each of these from a 5/6 bit space to an 8 bit space.

  • 5 bits = 32 values
  • 6 bits = 64 values
  • 8 bits = 256 values

The code you're using is taking the naive approach that x5 * 256/32 = x8 where 256/32 = 8 and multiplying by 8 is left shift 3 but, as you say, this doesn't necessarily fill the new number space "correctly". 5 to 8 for max value is 31 to 255 and therein lies your clue to the solution.

x8 = 255/31 * x5
x8 = 255/63 * x6

where x5, x6 and x8 are 5, 6 and 8 bit values respectively.

Now there is a question about the best way to implement this. It does involve division and with integer division you will lose any remainder result (round down basically) so the best solution is probably to do floating point arithmetic and then round half up back to an integer.

This can be sped up considerably by simply using this formula to generate a lookup table for each of the 5 and 6 bit conversions.

My few cents:

If you care about precise mapping, yet fast algorithm you can consider this:

R8 = ( R5 * 527 + 23 ) >> 6;
G8 = ( G6 * 259 + 33 ) >> 6;
B8 = ( B5 * 527 + 23 ) >> 6;

It uses only: MUL, ADD and SHR -> so it is pretty fast! From the other side it is compatible in 100% to floating point mapping with proper rounding:

// R8 = (int) floor( R5 * 255.0 / 31.0 + 0.5);
// G8 = (int) floor( G6 * 255.0 / 63.0 + 0.5);
// B8 = (int) floor( R5 * 255.0 / 31.0 + 0.5);

Some extra cents: If you are interested in 888 to 565 conversion, this works very well too:

R5 = ( R8 * 249 + 1014 ) >> 11;
G6 = ( G8 * 253 +  505 ) >> 10;
B5 = ( B8 * 249 + 1014 ) >> 11;

Constants were found using brute force search with somę early rejections to speed thing up a bit.

iOS vImage Conversion

The iOS Accelerate Framework documents the following algorithm for the vImageConvert_RGB565toARGB8888 function:

Pixel8 alpha = alpha
Pixel8 red   = (5bitRedChannel   * 255 + 15) / 31
Pixel8 green = (6bitGreenChannel * 255 + 31) / 63
Pixel8 blue  = (5bitBlueChannel  * 255 + 15) / 31

For a one-off conversion this will be fast enough, but if you want to process many frames you want to use something like the iOS vImage conversion or implement this yourself using NEON intrinsics.

From ARMs Community Forum Tutorial

First, we will look at converting RGB565 to RGB888. We assume there are eight 16-bit pixels in register q0, and we would like to separate reds, greens and blues into 8-bit elements across three registers d2 to d4.

 vshr.u8      q1, q0, #3      @ shift red elements right by three bits,
                                @  discarding the green bits at the bottom of
                                @  the red 8-bit elements.
vshrn.i16    d2, q1, #5      @ shift red elements right and narrow,
                                @  discarding the blue and green bits.
vshrn.i16    d3, q0, #5      @ shift green elements right and narrow,
                                @  discarding the blue bits and some red bits
                                @  due to narrowing.
vshl.i8      d3, d3, #2      @ shift green elements left, discarding the
                                @  remaining red bits, and placing green bits
                                @  in the correct place.
vshl.i16  q0, q0, #3      @ shift blue elements left to most-significant
                                @  bits of 8-bit color channel.
vmovn.i16    d4, q0          @ remove remaining red and green bits by
                                @  narrowing to 8 bits.

The effects of each instruction are described in the comments above, but in summary, the operation performed on each channel is: Remove color data for adjacent channels using shifts to push the bits off either end of the element. Use a second shift to position the color data in the most-significant bits of each element, and narrow to reduce element size from 16 to eight bits.

Note the use of element sizes in this sequence to address 8 and 16 bit elements, in order to achieve some of the masking operations.

A small problem

You may notice that, if you use the code above to convert to RGB888 format, your whites aren't quite white. This is because, for each channel, the lowest two or three bits are zero, rather than one; a white represented in RGB565 as (0x1F, 0x3F, 0x1F) becomes (0xF8, 0xFC, 0xF8) in RGB888. This can be fixed using shift with insert to place some of the most-significant bits into the lower bits.

For an Android specific example I found a YUV-to-RGB conversion written in intrinsics.

Try this:

red5 = (buf & 0xF800) >> 11;
red8 = (red5 << 3) | (red5 >> 2);

This will map all zeros into all zeros, all 1's into all 1's, and everything in between into everything in between. You can make it more efficient by shifting the bits into place in one step:

redmask = (buf & 0xF800);
rgb888 = (redmask << 8) | ((redmask<<3)&0x070000) | /* green, blue */

Do likewise for green and blue (for 6 bits, shift left 2 and right 4 respectively in the top method).

The general solution is to treat the numbers as binary fractions - thus, the 6 bit number 63/63 is the same as the 8 bit number 255/255. You can calculate this using floating point math initially, then compute a lookup table, as other posters suggest. This also has the advantage of being more intuitive than bit-bashing solutions. :)

There is an error jleedev !!!

unsigned char green = (buf & 0x07c0) >> 5;
unsigned char blue = buf & 0x003f;

the good code

unsigned char green = (buf & 0x07e0) >> 5;
unsigned char blue = buf & 0x001f;

Cheers, Andy

I used the following and got good results. Turned out my Logitek cam was 16bit RGB555 and using the following to convert to 24bit RGB888 allowed me to save as a jpeg using the smaller animals ijg: Thanks for the hint found here on stackoverflow.

// Convert a 16 bit inbuf array to a 24 bit outbuf array
BOOL JpegFile::ByteConvert(BYTE* inbuf, BYTE* outbuf, UINT width, UINT height)
{     UINT row_cnt, pix_cnt;     
      ULONG off1 = 0, off2 = 0;
      BYTE  tbi1, tbi2, R5, G5, B5, R8, G8, B8;

      if (inbuf==NULL)
          return FALSE;

      for (row_cnt = 0; row_cnt <= height; row_cnt++) 
      {     off1 = row_cnt * width * 2;
            off2 = row_cnt * width * 3;
            for(pix_cnt=0; pix_cnt < width; pix_cnt++)
            {    tbi1 = inbuf[off1 + (pix_cnt * 2)];
                 tbi2 = inbuf[off1 + (pix_cnt * 2) + 1];
                 B5 = tbi1 & 0x1F;
                 G5 = (((tbi1 & 0xE0) >> 5) | ((tbi2 & 0x03) << 3)) & 0x1F;
                 R5 = (tbi2 >> 2) & 0x1F;
                 R8 = ( R5 * 527 + 23 ) >> 6;
                 G8 = ( G5 * 527 + 23 ) >> 6;
                 B8 = ( B5 * 527 + 23 ) >> 6;
                 outbuf[off2 + (pix_cnt * 3)] = R8;
                 outbuf[off2 + (pix_cnt * 3) + 1] = G8;
                 outbuf[off2 + (pix_cnt * 3) + 2] = B8;
            }
       }
       return TRUE;
}        
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top