Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java ImageIO Grayscale PNG Issue

I have a grayscale image ("lena" actually) which I want to experiment with. I got it as a 512x512 PNG file with 216 shades of gray.

What happens is, when I read it with Java ImageIO, like that:

    String name = args[0];
    File fi = new File(name);
    BufferedImage img = ImageIO.read(fi);

I get a BufferedImage with only 154 colours! I only realized this, cause my processed images which looked sallow, lacking deep black.

Even more irritating, when I use XnView convert the PNG to a GIF, which is a lossless procedure in this case, read the GIF with above code, I get all 216 colours in my BufferedImage.

Is there some kind of documentation or description, what happens to my PNG, when ImageIO reads it? Are there settings to fix that? I did these experiments on a fairly recent JDK1.8. It is just that my trust in Java PNG support is lost now and I will use coloured PNG later.

like image 474
InI4 Avatar asked Dec 14 '22 13:12

InI4


2 Answers

Welcome to Java's "great" world of implicit color management!

For Java (at least ImageIO) everything internally is sRGB and it implicitely does color management, which often is quite counter-productive for what one actually wants to do. For gray scale images, at least using ImageIO with most readers and at least for gray scale images without an embedded ICC profile (I haven't tested others yet), Java automatically "assigns" an ICC profile with WhitePoint=D50, Gamma=1.0. I stumbled across this as well.

And then, when you access pixels (I assume you use img.getRGB() or something similar?), you actually access sRGB values (Java's default color space on Windows).

The result is, when converting to sRGB, which has a gamma of ~2.2 (sRGB's gamma is actually a bit more complicated, but close to 2.2 overall), this affectively applies a gamma correction with (1/Gamma)=2.2 to the image, (a) making your image appear "light", and (b) due to the gamma correction from 256 to 256 discrete values, you also effectively loose some of your shades of gray.

You also can see the effect if you access your BufferedImage's data in different ways: a) access the profile:

ColorSpace colorSpace = img.getColorModel().getColorSpace();
if ( colorSpace instanceof ICC_ColorSpace ) {
    ICC_Profile profile = ((ICC_ColorSpace)colorSpace).getProfile();
    if ( profile instanceof ICC_ProfileGray ) {
        float gamma = ((ICC_ProfileGray)profile).getGamma();
        system.out.println("Gray Profile Gamma: "+gamma); // 1.0 !
    }
}

b) access some pixel values in different ways ...

//access sRGB values (Colors translated from img's ICC profile to sRGB)
System.out.println( "pixel 0,0 value (sRGB): " + Integer.toHexString(img.getRGB(0,0)) ); // getRGB() actually means "getSRGB()"
//access raw raster data, this will give you the uncorrected gray value
//as it is in the image file
Raster raster = image.getRaster();
System.out.println( "pixel 0,0 value (RAW gray value): " + Integer.toHexString(raster.getSample(0,0,0)) );

If your pixel (0,0) is not by chance 100% black or 100% white, you will see that the sRGB value is "higher" than the gray value, for example gray = d1 -> sRGB = ffeaeaea (alpha, Red, Green, Blue).

From my point of view, it does not only reduce your gray levels, but also makes your image lighter (about the same as applying gamma correction with 1/gamma value of 2.2). It would be more logical if Java for gray images without embedded ICC Profile either translates gray to sRGB with R=G=B=grayValue or would assign an ICC Gray Profile WhitePoint=D50, Gamma=2.2 (at least on Windows). The latter still would make you loose a couple of gray tones due to sRGB not being exactly Gamma 2.2.

Regarding why it works with GIF: the GIF format has no concept of "gray scales" or ICC profiles, so your image is a 256 color palette image (the 256 colors happen to be 256 shades of gray). On opening a GIF, Java assumes the RGB values are sRGB.

Solution: Depending on what your actual use case is, the solution for you might be that you access the Raster data of each of your image's pixel (gray=raster.getSample(x,y,0)) and put it into an sRGB image setting R=G=B=gray. There might be an more elegant way, though.

Regarding your trust in java or PNG: I'm struggling with java ImageIO in many ways due to the implicite color conversions it does. The idea is to have color management built in without the developers need much knowledge about color management. This works to some extend as long as you work with sRGB only (and your input is sRGB, too, or has no color profile and thus could legitimately considered to be sRGB). Trouble starts if you have other color spaces in your input images (for example AdobeRGB). Gray is another thing as well, especially the fact that ImageIO assumes an (unusual) Gray Profile with Gamma=1.0. Now to understand what ImageIO is doing, you don't only need to know your ABC in color management, but also need to figure out what java is doing. I didn't find this info in any documentation! Bottom line: ImageIO does things that certainly could be considered correct. It's just often not what you expect and you might to dig deeper to find out why or to change the behaviour if it isn't what you want to do.

like image 51
Marco Freudenberger Avatar answered Dec 17 '22 03:12

Marco Freudenberger


Somehow you have converted the image from a linear grayscale (gamma=1.0) to an sRGB grayscale (gamma=1/2.2). This can be demonstrated with GraphicsMagick. Start with Lenna.png downloaded from Wikipedia, then remove the sRGB chunk to create lena.png, then

gm convert lena.png -colorspace gray -depth 8 -strip lena-gray.png

lena-gray.png has 216 colors

gm convert lena-gray.png -gamma 2.2 -depth 8 -strip lena-gray-gm22.png

lena-gray-gm22.png has 154 colors and appears washed-out or faded.

I'm using a recent beta of graphicsmagick (version 1.4) with libpng-1.6.17.

To count the colors I used ImageMagick:

identify -verbose file.png | grep Colors

I used

pngcheck -v file.png

to verify that Lenna.png contains IHDR, sRGB, IDAT, and IEND chunks, while lena-gray.png and lena-gray-gm22.png contain only IHDR, IDAT, and IEND chunks.

like image 23
Glenn Randers-Pehrson Avatar answered Dec 17 '22 03:12

Glenn Randers-Pehrson