I'm trying to convert a grayscale image in 24-bit RGB format to a grayscale image in 8-bit format. In other words, input and output should be visually identical, only the number of channels changes. Here's the input image:
The code used to convert it to 8-bit:
File input = new File("input.jpg");
File output = new File("output.jpg");
// Read 24-bit RGB input JPEG.
BufferedImage rgbImage = ImageIO.read(input);
int w = rgbImage.getWidth();
int h = rgbImage.getHeight();
// Create 8-bit gray output image from input.
BufferedImage grayImage = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_GRAY);
int[] rgbArray = rgbImage.getRGB(0, 0, w, h, null, 0, w);
grayImage.setRGB(0, 0, w, h, rgbArray, 0, w);
// Save output.
ImageIO.write(grayImage, "jpg", output);
And here's the output image:
As you can see, there's a slight difference. But they should be identical. For those who can't see it, here's the difference between the two images (when viewed with Difference blending mode in Gimp, full black would indicate no difference). The same problem happens if I use PNG instead for input and output.
After doing grayImage.setRGB
, I tried comparing color values for the same pixel in both images:
int color1 = rgbImage.getRGB(230, 150); // This returns 0xFF6D6D6D.
int color2 = grayImage.getRGB(230, 150); // This also returns 0xFF6D6D6D.
Same color for both. However, if I do the same comparison with the images in Gimp, I get 0xFF6D6D6D
and 0xFF272727
respectively... huge difference.
What's happening here? Is there any way I can obtain an identical 8-bit image from a grayscale 24-bit image? I'm using Oracle JDK 1.8 for the record.
I dived a little into Open JDK implementation and found this:
When calling setRGB
, values are modified by the image color model. In this case, the following formula was being applied:
float red = fromsRGB8LUT16[red] & 0xffff;
float grn = fromsRGB8LUT16[grn] & 0xffff;
float blu = fromsRGB8LUT16[blu] & 0xffff;
float gray = ((0.2125f * red) +
(0.7154f * grn) +
(0.0721f * blu)) / 65535.0f;
intpixel[0] = (int) (gray * ((1 << nBits[0]) - 1) + 0.5f);
This basically tries to find the luminosity of a given color to find its gray shade. But with my values already being gray, this should give the same gray shade, right? 0.2125 + 0.7154 + 0.0721 = 1
so with an input of 0xFF1E1E1E
should result in a gray value of 0xFE
.
Except, the fromsRGB8LUT16
array used doesn't map values linearly... Here's a plot I made:
So an input of 0xFF1E1E1E
actually results in a gray value of 0x03
! I'm not entirely sure why it's not linear, but it certainly explains why my output image was so dark compared with the original.
Using Graphics2D
works for the example I gave. But this example had been simplified and in reality I needed to tweak some values, so I can't used Graphics2D
. Here's the solution I found. We completely avoid the color model remapping the values and instead sets them directly on the raster.
BufferedImage grayImage = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_GRAY);
int[] rgbArray = buffImage.getRGB(0, 0, w, h, null, 0, w);
grayImage.getRaster().setPixels(0, 0, w, h, rgbArray);
Why does this work? An image of type TYPE_BYTE_ARRAY
has a raster of type ByteInterleavedRaster
where data is stored in byte[]
and each pixel value take a single byte. When calling setPixels
on the raster, the values of the passed array are simply cast to a byte. So 0xFF1E1E1E
effectively becomes 0x1E
(only lowest bits are kept), which is what I wanted.
EDIT: I just saw this question and apparently the non linearity is just part of the standard formula.