【问题标题】:Converting TYPE_INT_RGB to TYPE_BYTE_GRAY image creates wrong result将 TYPE_INT_RGB 转换为 TYPE_BYTE_GRAY 图像会产生错误的结果
【发布时间】:2020-05-04 15:41:53
【问题描述】:

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

用于将其转换为 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);

这是输出图像:

如您所见,略有不同。但它们应该是相同的。对于那些看不到它的人,这里是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 作为记录。

【问题讨论】:

    标签: java jpeg bufferedimage javax.imageio color-channel


    【解决方案1】:

    我测试的前两件事,我打印了两张图片。

    BufferedImage@544fa968: type = 5 ColorModel: #pixelBits = 24 numComponents = 3 color space = java.awt.color.ICC_ColorSpace@68e5eea7 透明度 = 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 透明度 = 1 has alpha = false isAlphaPre = false ByteInterleavedRaster: width = 400 height = 400 # numDataElements 1 dataOff[0] = 0

    我们可以看到图片的色彩空间不同,数据偏移也不同。

    我使用图形在输出上绘制原始图像。

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

    这很好用。我将图像保存为 png,并不是说它会改变您看到的效果,当我对两张图像进行区分时,它们是相同的。

    底线是,两种不同图像类型的 rgb 值不同。因此,当您使用 get rgb 看到相同的值时,它们在显示时会被解释为不同的值。

    使用图形有点慢,但可以得到正确的图像。

    我认为这里的一个区别是 setRGB/getRGB 以非直观的方式对数据进行操作。

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

    显示我们期望的数据。 rgb缓冲区是3x的大小,像素直接对应。

    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

    当我们检查对应的rgb值时。

    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
    呸呸呸,呸呸呸
    fffefefe, fffdfdfd
    fffefefe, fffcfcfc
    fffefefe, fffcfcfc
    fffdfdfd, fffbfbfb
    fffdfdfd, fffbfbfb
    fffdfdfd, fffafafa
    fffdfdfd, fffafafa

    因此,要使图像正确,它必须具有不同的 rgb 值。

    【讨论】:

    • 感谢您抽出宝贵时间回答。使用Graphics2D 确实可以解决我的问题,但我忘了说我的真实用例更复杂。我需要能够设置单个像素的值。所以我检查了 Open JDK implementation 以找到原因和更简单的解决方案,请参阅我的答案。
    • @Nicolas 有趣的是,setRGB 与 Graphics 不同。我几乎希望如果我使用 Graphics 绘制一个值,得到的 RGB 值将与我使用 setRGB 相同:即将支持数据设置为代表提供的 rgb 值的值。
    【解决方案2】:

    我深入研究了 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 使用的数组不会线性映射值...这是我制作的图:

    所以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,显然非线性只是标准公式的一部分。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-04-21
      • 1970-01-01
      • 2015-10-31
      • 2018-05-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多