views:

1378

answers:

5

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?
+5  A: 

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.

cletus
Just ran a test, and this is equivalent to reusing the top 3 bits, but sometimes it adds or subtracts 1. I guess there’s no real advantage, then.
jleedev
http://codepad.org/qLvbkTO3 This is what it looks like; the bottom 3 bits are mostly following the top 3. I think the only possible way to get more clever is to somehow reverse the dithering that was used :)
jleedev
The "add or subtract one" is the important part. Not doing that will tend to amplify the low-precision banding artifacts that 16bit color produces.
Alan
+4  A: 

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 form one space to the other. It's fast, though. :)

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

calmh
Yes, this is exactly what they’re doing. And I think it’s actually the *most* linear mapping.
jleedev
It's multiplication by 8.25 with truncation, whereas cletus multiplies by a different value with rounding. Either way, there are 7 "steps" at which adding 1 to the input adds 9 to the output instead of 8. I guess you could define "lack of linearity" in terms of where the steps occur - the variance of the widths of the 8 sections of input, maybe? Or calculate a correlation coefficient on all 32 inputs against their outputs, and the maximum result is the "most linear".
Steve Jessop
+1  A: 

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).

Rex Kerr
Yes, this is the solution (but expressed in C, which we didn’t have yet). Except you meant to either divide by 8 or shift right by 3.
jleedev
Whoops--shouldn't write these things when half-asleep. Fixed now. Also, should have noticed calmh's solution doing the same thing.
Rex Kerr
A: 

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. :)

Nick Johnson
+2  A: 

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

Andy
Good catch. I think this was just a typo in writing the question, though, as the code on my laptop is correct.
jleedev