Java ImageIO 灰度 PNG 问题

Java ImageIO Grayscale PNG Issue

我有一张灰度图像(实际上 "lena"),我想对其进行试验。我得到的是 512x512 PNG 文件,有 216 种灰度

发生的事情是,当我用 Java ImageIO 读取它时,就像那样:

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

我得到一个 只有 154 种颜色 的 BufferedImage!我才意识到这一点,因为我处理过的图像看起来灰黄,缺乏深黑色。

更令人恼火的是,当我使用 XnView 将 PNG 转换为 GIF 时,在这种情况下这是一个无损过程,使用上面的代码读取 GIF,我在 BufferedImage 中获得了所有 216 种颜色。

是否有某种文档或描述,当 ImageIO 读取我的 PNG 时会发生什么?有没有设置可以解决这个问题?我在相当新的 JDK1.8 上做了这些实验。只是我现在失去了对 Java PNG 支持的信任,稍后我将使用彩色 PNG。

您以某种方式将图像从线性灰度 (gamma=1.0) 转换为 sRGB 灰度 (gamma=1/2.2)。这可以用 GraphicsMagick 来演示。从从维基百科下载的 Lenna.png 开始,然后删除 sRGB 块以创建 lena.png,然后

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

lena-gray.png 有 216 种颜色

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

lena-gray-gm22.png 有 154 种颜色,看起来像褪色或褪色。

我正在使用带有 libpng-1.6.17 的 graphicsmagick(版本 1.4)的最新测试版。

计算我使用 ImageMagick 的颜色:

identify -verbose file.png | grep Colors

我用过

pngcheck -v file.png

验证 Lenna.png 包含 IHDR、sRGB、IDAT 和 IEND 块,而 lena-gray.png 和 lena-gray-gm22.png 仅包含 IHDR、IDAT 和 IEND大块。

欢迎来到 Java 的 "great" 隐式色彩管理世界!

对于 Java(至少是 ImageIO),内部的所有内容都是 sRGB 并且它隐含地进行颜色管理,这通常会适得其反。 对于灰度图像,至少对大多数阅读器使用 ImageIO,至少对于没有嵌入式 ICC 配置文件的灰度图像(我还没有测试过其他人),Java 自动 "assigns" 带有 WhitePoint 的 ICC 配置文件=D50,伽玛=1.0。我也偶然发现了这个。

然后,当您访问像素时(我假设您使用 img.getRGB() 或类似的东西?),您实际上访问了 sRGB 值(Java 的默认颜色 space Windows).

结果是,当转换为 sRGB 时,其伽马约为 2.2(sRGB 的伽马实际上有点复杂,但总体上接近 2.2),这有效地应用了 (1/Gamma) 的伽马校正=2.2 到图像,(a) 使您的图像显示 "light",以及 (b) 由于从 256 到 256 离散值的伽玛校正,您还有效地失去了一些灰色阴影。

如果您以不同的方式访问 BufferedImage 的数据,您也可以看到效果: a) 访问个人资料:

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

如果您的像素 ​​(0,0) 不是偶然的 100% 黑色或 100% 白色,您会看到 sRGB 值比灰色值 "higher",例如 gray = d1 -> sRGB = ffeaeaea(alpha、红、绿、蓝)。

从我的角度来看,它不仅会降低您的灰度级,还会使您的图像更亮(与应用 1/gamma 值为 2.2 的 gamma 校正大致相同)。如果 Java 没有嵌入 ICC 配置文件的灰色图像将灰色转换为 sRGB 且 R=G=B=grayValue 或分配 ICC 灰色配置文件 WhitePoint=D50, Gamma=2.2(至少在 Windows).由于 sRGB 不完全是 Gamma 2.2,后者仍然会让你失去一些灰色调。

关于它与 GIF 一起工作的原因:GIF 格式没有 "gray scales" 或 ICC 配置文件的概念,因此您的图像是一个 256 色调色板图像(256 种颜色恰好是 256 种灰色阴影)。打开 GIF 时,Java 假定 RGB 值为 sRGB。

解法: 根据您的实际用例,您的解决方案可能是访问每个图像像素的栅格数据 (gray=raster.getSample(x,y,0)) 并将其放入 sRGB 图像中设置 R=G=B=灰色。不过,可能还有更优雅的方法。

关于您对 java 或 PNG 的信任: 由于它所做的隐式颜色转换,我在很多方面都在与 java ImageIO 作斗争。这个想法是在开发人员不需要太多颜色管理知识的情况下内置颜色管理。只要您仅使用 sRGB(并且您的输入也是 sRGB,或者没有颜色配置文件,因此可以合理地认为是 sRGB),这在一定程度上起作用。如果您的输入图像中有其他颜色 space(例如 Adob​​eRGB),就会出现问题。灰色也是另一回事,尤其是 ImageIO 假定 Gamma=1.0 的(不寻常的)灰色配置文件这一事实。现在要了解ImageIO是干什么的,不仅需要了解色彩管理的ABC,还需要搞清楚java是干什么的。我没有在任何文档中找到此信息!底线:ImageIO 做的事情当然可以被认为是正确的。这通常不是您所期望的,您可能会更深入地挖掘以找出原因,或者如果这不是您想要做的,则可以改变行为。