Converting TYPE_INT_RGB to TYPE_BYTE_GRAY image creates wrong result

786 Views Asked by At

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:

input

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:

output

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.

2

There are 2 best solutions below

0
On BEST ANSWER

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:

enter image description here

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.

2
On

First two things I tested, I printed out the two images.

BufferedImage@544fa968: type = 5 ColorModel: #pixelBits = 24 numComponents = 3 color space = java.awt.color.ICC_ColorSpace@68e5eea7 transparency = 1 has alpha = false isAlphaPre = false ByteInterleavedRaster: width = 400 height = 400 #numDataElements 3 dataOff[0] = 2

BufferedImage@11fc564b: type = 10 ColorModel: #pixelBits = 8 numComponents = 1 color space = java.awt.color.ICC_ColorSpace@394a2528 transparency = 1 has alpha = false isAlphaPre = false ByteInterleavedRaster: width = 400 height = 400 #numDataElements 1 dataOff[0] = 0

We can see the images have a different color space, and the data offset is different.

And I used a graphics to draw the original image on the output.

Graphics g = grayImage.getGraphics();
g.drawImage(rgbImage, 0, 0, null);

This worked fine. I saved the image as png, not that it changes the effect your seeing, and when I took a difference between the two images, They were the same.

Bottom line is, the rgb values are different for the two different image types. So while you see the same value with get rgb, they're interpreted as different values when they're displayed.

Using the graphics is a bit slower, but it gets the correct image out.

I think a distinction here is setRGB/getRGB are operating on the data in a non-intuitive way.

DataBuffer rgbBuffer = rgbImage.getRaster().getDataBuffer();
DataBuffer grayBuffer = grayImage.getRaster().getDataBuffer();

System.out.println(grayBuffer.size() + ", " + rgbBuffer.size() );
for(int i = 0; i<10; i++){
    System.out.println(
        grayBuffer.getElem(i) + "\t"
        + rgbBuffer.getElem(3*i) + ", " 
        + rgbBuffer.getElem(3*i+1) + ", " 
        + rgbBuffer.getElem(3*i + 2) );
}

Shows data that we expect. The rgb buffer is 3x's the size, the pixels correspond directly.

160000, 480000
255 255, 255, 255
255 255, 255, 255
254 254, 254, 254
253 253, 253, 253
252 252, 252, 252
252 252, 252, 252
251 251, 251, 251
251 251, 251, 251
250 250, 250, 250
250 250, 250, 250

When we check the corresponding rgb values.

for(int i = 0; i<10; i++){
    System.out.println( 
        Integer.toHexString( grayImage.getRGB(i, 0) ) + ", "
        +  Integer.toHexString( rgbImage.getRGB(i, 0) ) + "  " );
}

ffffffff, ffffffff
ffffffff, ffffffff
ffffffff, fffefefe
fffefefe, fffdfdfd
fffefefe, fffcfcfc
fffefefe, fffcfcfc
fffdfdfd, fffbfbfb
fffdfdfd, fffbfbfb
fffdfdfd, fffafafa
fffdfdfd, fffafafa

So for the image to be correct, it has to have different rgb values.