Java's Primitive Datatypes are Signed!
It took me a whole 6 hours to write a color conversion from the YUV420P color space to the RGB color space. I thought I had a fairly good understanding of the various color formats (YUV420P, SP, 422, etc etc) and how to access individual Y, U, and V components; but I spent the better half of the 6 hours reading and re-reading the conversions and theory. The converted output's colors were completely messed up, and the images looked something like this:
Note, you needn't know what each of these really is or the formula of the conversion behind this. The crux of the problem was converting the given byte[] array input, to an int[] array output. I struggled because of a very simple yet hair-pulling gotcha; but it took me hours to realize where I was going wrong. All primitives in Java are signed! If you come from a Python-like world, or are ignorant of signed-ness, you'd expect the byte 0x0 to print 0, and 0xFF to print 255.
In Java however, where everything is signed, ((byte) 0xFF) is -1, and not 0x255. Further, simply trying to print 0xFF prints 255; which is because 0xFF is an int. Now, what I wanted to do was simply access a signed byte as an unsigned byte.
I am however super embarrassed at spending 6 hours on this. This shows how developers tend to forget the underlying mechanisms because of all the abstractions high-level languages. On the upside, I recapped all color formats and the formulas behind them!
Here's the source I've put on Github Gists. Note, there's room for more optimization; the conversion to int and then back to byte for each R, G, and B component isn't necessary.
The colors are all messed up; but notice you don't see the 4 quadrant ghosts as you'd usually see in YUV conversions that go wrong. This hinted that my conversions/element access wasn't wrong.*
In Java however, where everything is signed, ((byte) 0xFF) is -1, and not 0x255. Further, simply trying to print 0xFF prints 255; which is because 0xFF is an int. Now, what I wanted to do was simply access a signed byte as an unsigned byte.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
sout(0xFF); // outputs 255 | |
sout(((byte)(0xFF))); // outputs -1 | |
sout((0xFF & (byte)0xFF)); // outputs 255 |
Here's the source I've put on Github Gists. Note, there's room for more optimization; the conversion to int and then back to byte for each R, G, and B component isn't necessary.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* It took me a whole 6 hours to finally get the color conversion right. | |
* I have a fairly good understanding of the various color formats (YUV420P, SP, 422, etc etc), | |
* and how to access individual Y, U, and V components. | |
* I however struggled because of a very simple yet hair-pulling gotcha. | |
* All primitives in Java are signed! If you come from a Python-like world where 0xFF prints 255, | |
* you see yourself struggle just the same. I am however embarrassed at spending 6 hours on this. | |
* | |
* @author rish | |
*/ | |
public class ColorConversions { | |
/** | |
* Converts yuv420p to rgba 8888 | |
* The tricky bit here is: | |
* Java has signed primitives only. This means 0xFF is -1, and not 0x255, as you'd expect | |
* in an unsigned python like world. | |
* <p> | |
* And-ing with 0xFF (0xFF is an int, not a byte ;)) converts the individual Y, U, V, bits | |
* from an signed byte to a signed int, but for our purposes behaves as an unsigned byte. | |
* This means, (byte)0xFF=-1, whereas 0xFF & (byte)0xFF = 255. | |
**/ | |
public void yuv420pToRGBA8888(int[] out, byte[] yuv, int width, int height) { | |
if (out.length < width * height) { | |
throw new IllegalArgumentException("Size of out must be " + width * height); | |
} | |
if (yuv.length < width * height * 3.0 / 2) { | |
throw new IllegalArgumentException("Size of yuv must be " + width * height * 3.0 / 2); | |
} | |
int size = width * height; | |
for (int j = 0; j < height; j++) { | |
for (int i = 0; i < width; i++) { | |
/*accessing YUV420P elements*/ | |
int indexY = j * width + i; | |
int indexU = (size + (j / 2) * (width / 2) + i / 2); | |
int indexV = (int) (size * 1.25 + (j / 2) * (width / 2) + i / 2); | |
// todo; this conversion to int and then later back to int really isn't required. | |
// There's room for better work here. | |
int Y = 0xFF & yuv[indexY]; | |
int U = 0xFF & yuv[indexU]; | |
int V = 0xFF & yuv[indexV]; | |
/*constants picked up from http://www.fourcc.org/fccyvrgb.php*/ | |
int R = (int) (Y + 1.402f * (V - 128)); | |
int G = (int) (Y - 0.344f * (U - 128) - 0.714f * (V - 128)); | |
int B = (int) (Y + 1.772f * (U - 128)); | |
/*clamping values*/ | |
R = R < 0 ? 0 : R; | |
G = G < 0 ? 0 : G; | |
B = B < 0 ? 0 : B; | |
R = R > 255 ? 255 : R; | |
G = G > 255 ? 255 : G; | |
B = B > 255 ? 255 : B; | |
out[width * j + i] = 0xff000000 + (R << 16) + (G << 8) + B; | |
} | |
} | |
} | |
} |
Comments
Post a Comment