java - 将 TYPE_INT_RGB 转换为 TYPE_BYTE_GRAY 图像会产生错误的结果

标签 java jpeg bufferedimage javax.imageio color-channel

我正在尝试将 24 位 RGB 格式的灰度图像转换为 8 位格式的灰度图像。换句话说,输入和输出在视觉上应该是相同的,只有 channel 数发生变化。这是输入图像:

input

用于将其转换为 8 位的代码:

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

这是输出图像:

output

如您所见,略有不同。但它们应该是相同的。对于那些看不到的人,这里是 the difference between the two images (当在 Gimp 中使用“差异”混合模式查看时,全黑表示没有差异)。如果我使用 PNG 代替输入和输出,也会出现同样的问题。

完成grayImage.setRGB后,我尝试比较两个图像中相同像素的颜色值:

int color1 = rgbImage.getRGB(230, 150);  // This returns 0xFF6D6D6D.
int color2 = grayImage.getRGB(230, 150);  // This also returns 0xFF6D6D6D.

两者颜色相同。但是,如果我与 Gimp 中的图像进行相同的比较,我会分别得到 0xFF6D6D6D0xFF272727...巨大的差异。

这里发生了什么?有什么方法可以从灰度 24 位图像获得相同的 8 位图像吗?为了记录,我使用的是 Oracle JDK 1.8。

最佳答案

我深入研究了 Open JDK 实现并发现了这一点:

调用setRGB时,值会根据图像颜色模型进行修改。在本例中,应用了以下公式:

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

这基本上是试图找到给定颜色的亮度来找到它的灰色阴影。但由于我的值已经是灰色的,这应该给出相同的灰色阴影,对吧? 0.2125 + 0.7154 + 0.0721 = 1 因此,输入 0xFF1E1E1E 应该会产生 0xFE 的灰度值。

除了,使用的 fromsRGB8LUT16 数组不会线性映射值......这是我制作的一个图:

enter image description here

因此输入0xFF1E1E1E实际上会产生0x03的灰度值!我不完全确定为什么它不是线性的,但它确实解释了为什么我的输出图像与原始图像相比如此暗。

使用Graphics2D适用于我给出的示例。但这个例子已经被简化了,实际上我需要调整一些值,所以我不能使用Graphics2D。这是我找到的解决方案。我们完全避免颜色模型重新映射值,而是直接在栅格上设置它们。

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

为什么这有效? TYPE_BYTE_ARRAY 类型的图像具有 ByteInterleavedRaster 类型的栅格,其中数据存储在 byte[] 中,每个像素值占用一个字节。在栅格上调用 setPixels 时,传递的数组的值将简单地转换为字节。因此,0xFF1E1E1E 实际上变成了 0x1E(仅保留最低位),这就是我想要的。

编辑:我刚刚看到this question显然非线性只是标准公式的一部分。

关于java - 将 TYPE_INT_RGB 转换为 TYPE_BYTE_GRAY 图像会产生错误的结果,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/61596094/

相关文章:

java - 小程序 - KeyListener 不工作

c - 解码 JPEG 霍夫曼 block (表格)

php - exif 在移动设备上使用 php 上传图片时剥离 GPS header ?

c++ - 将图像保存为 JPG 库?

java - 将 JavaFX 图像转换为 BufferedImage

java - 适用于 Android 的在线广播流媒体应用

java - 将 SNAPSHOT 部署到 oss.jfrog.org (JCenter)

java - 如何在处理时禁用按钮

java - Windows 64 上的 DICOM 图像到 BufferedImage

Java - 删除低于某个 alpha 值的像素