英文:
Converting TYPE_INT_RGB to TYPE_BYTE_GRAY image creates wrong result
问题
我正在尝试将一个24位RGB格式的灰度图像转换为8位格式的灰度图像。换句话说,输入和输出在视觉上应该是相同的,只是通道的数量发生了变化。这是输入图像:
用于将其转换为8位的代码:
File input = new File("input.jpg");
File output = new File("output.jpg");
// 读取24位RGB输入JPEG。
BufferedImage rgbImage = ImageIO.read(input);
int w = rgbImage.getWidth();
int h = rgbImage.getHeight();
// 从输入创建8位灰度输出图像。
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);
// 保存输出。
ImageIO.write(grayImage, "jpg", output);
这是输出图像:
正如您所看到的,存在轻微的差异。但它们应该是相同的。对于那些看不到差异的人,这里是两个图像之间的差异(在Gimp中使用差异混合模式查看,完全的黑色表示没有差异)。如果我将输入和输出都改用PNG格式,同样的问题也会发生。
在执行了grayImage.setRGB
之后,我尝试了比较两个图像中相同像素的颜色值:
int color1 = rgbImage.getRGB(230, 150); // 这返回0xFF6D6D6D。
int color2 = grayImage.getRGB(230, 150); // 这也返回0xFF6D6D6D。
对于两者来说,颜色都是相同的。然而,如果我在Gimp中对这些图像进行相同的比较,我得到的分别是0xFF6D6D6D
和0xFF272727
... 差异巨大。
这里发生了什么?有没有办法从灰度24位图像获取相同的8位图像?值得注意的是,我使用的是Oracle JDK 1.8。
英文:
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:
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:
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.
答案1
得分: 3
我稍微研究了一下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;
int pixel[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
(只保留了最低位),这正是我想要的。
编辑:我刚刚看到了这个问题,显然非线性部分是标准公式的一部分。
英文:
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:
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
得分: 2
首先,我测试了两件事,我打印出了这两幅图像。
>BufferedImage@544fa968: 类型 = 5 ColorModel: #pixelBits = 24 numComponents = 3 color space = java.awt.color.ICC_ColorSpace@68e5eea7 透明度 = 1 是否具有 alpha 通道 = false isAlphaPre = false ByteInterleavedRaster: 宽度 = 400 高度 = 400 #numDataElements 3 dataOff[0] = 2
>
>BufferedImage@11fc564b: 类型 = 10 ColorModel: #pixelBits = 8 numComponents = 1 color space = java.awt.color.ICC_ColorSpace@394a2528 透明度 = 1 是否具有 alpha 通道 = false isAlphaPre = false ByteInterleavedRaster: 宽度 = 400 高度 = 400 #numDataElements 1 dataOff[0] = 0
我们可以看到这两幅图像具有不同的颜色空间,数据偏移也不同。
然后我使用图形对象在输出上绘制了原始图像。
Graphics g = grayImage.getGraphics();
g.drawImage(rgbImage, 0, 0, null);
这个操作效果不错。我将图像保存为了 PNG 格式,尽管这不会改变你所看到的效果,当我比较这两幅图像的差异时,它们是相同的。
总的来说,对于这两种不同类型的图像,RGB 值是不同的。因此,当你使用 getRGB 方法获取相同的值时,它们在显示时被解释为不同的值。
使用图形对象会稍微慢一些,但它能够正确地输出图像。
我认为这里的一个区别在于 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 缓冲区的大小是三倍,像素直接对应。
>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
ffffffff, fffefefe
fffefefe, fffdfdfd
fffefefe, fffcfcfc
fffefefe, fffcfcfc
fffdfdfd, fffbfbfb
fffdfdfd, fffbfbfb
fffdfdfd, fffafafa
fffdfdfd, fffafafa
因此,为了图像正确显示,它们的 RGB 值必须不同。
英文:
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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论