我试图将BufferedImage的byte []从32位RGBA转换为24位RGB.根据this answer,从图像中获取byte []的最快方法是:
byte[] pixels = ((DataBufferByte) bufferedImage.getRaster().getDataBuffer()).getData();
所以我迭代所有字节,假设它们的顺序是R G B A,并且对于每4个字节,我在输出字节[]中写入前3个(即忽略alpha值).
从Eclipse运行并且字节转换正确时,这很好用.但是,当我从命令行运行相同的程序时,返回相同的字节顺序相同的字节!
我用于测试的测试图像是一个5×5黑色图像,其左上角只有RGBA颜色[aa cc ee ff]:
并且放大了一个方便的版本:
我的文件夹结构是:
- src/
- test.png
- test/
- TestBufferedImage.java
SSCCE如下:
package test;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.io.IOException;
import java.io.InputStream;
import javax.imageio.ImageIO;
public class TestBufferedImage {
private static void log(String s) {
System.out.println(s);
}
private static String toByteString(byte b) {
// Perform a bitwise AND for convenience while printing.
// Otherwise Integer.toHexString() interprets values as integers and a negative byte 0xFF will be printed as "ffffffff"
return Integer.toHexString(b & 0xFF);
}
/**
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
InputStream stream = TestBufferedImage.class.getClassLoader().getResourceAsStream("test.png");
BufferedImage image = ImageIO.read(stream);
stream.close();
log("Image loaded succesfully, width=" + image.getWidth() + " height=" + image.getHeight());
log("Converting from 32-bit to 24-bit...");
DataBufferByte buffer = (DataBufferByte)image.getRaster().getDataBuffer();
byte[] input = buffer.getData();
byte[] output = convertTo24Bit(input);
log("Converted total of " + input.length + " bytes to " + output.length + " bytes");
}
private static byte[] convertTo24Bit(byte[] input) {
int dataLength = input.length;
byte[] convertedData = new byte[ dataLength * 3 / 4 ];
for (int i = 0, j = 0; i < dataLength; i+=4, j+=3) {
convertIntByteToByte(input, i, convertedData, j);
}
return convertedData;
}
private static void convertIntByteToByte(byte[] src, int srcIndex, byte[] out, int outIndex) {
byte r = src[srcIndex];
byte g = src[srcIndex+1];
byte b = src[srcIndex+2];
byte a = src[srcIndex+3];
out[outIndex] = r;
out[outIndex+1] = g;
out[outIndex+2] = b;
log("i=" + srcIndex
+ " Converting [" + toByteString(r) + ", " + toByteString(g)
+ ", " + toByteString(b) + ", " + toByteString(a) + "] --> ["
+ toByteString(out[outIndex]) + ", " + toByteString(out[outIndex+1])
+ ", " + toByteString(out[outIndex+2]) + "]"
);
}
}
从Eclipse运行时的输出(版本:Juno Service Release 2 Build id:20130225-0426):
Image loaded succesfully, width=5 height=5
Converting from 32-bit to 24-bit...
i=0 Converting [aa, cc, ee, ff] --> [aa, cc, ee] // <-- Bytes have the correct order
i=4 Converting [0, 0, 0, ff] --> [0, 0, 0]
i=8 Converting [0, 0, 0, ff] --> [0, 0, 0]
.....
i=96 Converting [0, 0, 0, ff] --> [0, 0, 0]
Converted total of 100 bytes to 75 bytes
使用java test.TestBufferedImage从命令行(Windows Vista)运行时输出:
Image loaded succesfully, width=5 height=5
Converting from 32-bit to 24-bit...
i=0 Converting [ff, ee, cc, aa] --> [ff, ee, cc] // <-- Bytes are returned with a different byte order!
i=4 Converting [ff, 0, 0, 0] --> [ff, 0, 0]
i=8 Converting [ff, 0, 0, 0] --> [ff, 0, 0]
.....
i=96 Converting [ff, 0, 0, 0] --> [ff, 0, 0]
Converted total of 100 bytes to 75 bytes
那么有没有人遇到类似的问题和/或可以解释实际发生的事情?从Eclipse内部运行时为什么字节顺序不同?
解决方法:
在回答我自己的问题之前,我真的非常非常感谢@Joni和@haraldK向我指出了正确的方向.我对BufferedImage,ColorModel,SampleModel等内部的了解并不是很好,所以他们帮助了我.
所以这是发生的事情:
首先,不同的行为是由不同的JRE引起的.用于打印java版本的日志语句显示Eclipse打印1.6.0_16-b01,而命令行打印1.6.0_31-b05.显然,图像加载的实现(这将是PNGImageReader类)在版本之间发生了变化,我怀疑它是在this change期间完成的.
尽管两个版本都使用相同的ColorModel和SampleModel,所以我无法理解这种变化(这对我来说似乎是一个真正的代码破坏者),所以我进一步调查了.
两个版本的PNGImageReader之间的重要变化是在Iterator< ImageTypeSpecifier>中. getImageTypes()方法,用于决定可用于创建新映像的可用兼容格式:
版本1.6.0_16-b01:
...
case PNG_COLOR_RGB_ALPHA:
// Component R, G, B, A (non-premultiplied)
rgb = ColorSpace.getInstance(ColorSpace.CS_sRGB);
bandOffsets = new int[4];
bandOffsets[0] = 0;
bandOffsets[1] = 1;
bandOffsets[2] = 2;
bandOffsets[3] = 3;
l.add(ImageTypeSpecifier.createInterleaved(rgb,
bandOffsets,
dataType,
true,
false));
break;
版本1.6.0_31-b05:
...
case PNG_COLOR_RGB_ALPHA:
if (bitDepth == 8) {
// some standard types of buffered images
// wich can be used as destination
l.add(ImageTypeSpecifier.createFromBufferedImageType(
BufferedImage.TYPE_4BYTE_ABGR));
l.add(ImageTypeSpecifier.createFromBufferedImageType(
BufferedImage.TYPE_INT_ARGB));
}
// Component R, G, B, A (non-premultiplied)
rgb = ColorSpace.getInstance(ColorSpace.CS_sRGB);
bandOffsets = new int[4];
bandOffsets[0] = 0;
bandOffsets[1] = 1;
bandOffsets[2] = 2;
bandOffsets[3] = 3;
l.add(ImageTypeSpecifier.createInterleaved(rgb,
bandOffsets,
dataType,
true,
false));
break;
所以在较新的版本中,新的ImageTypeSpecifiers对PNG加载器说,内部表示的兼容图像是BufferedImage.TYPE_4BYTE_ABGR,因为它是列表中的第一个类型,所以加载器继续使用它.这就是为什么颜色通道的频带(以及字节)被反转的原因.
意识到这一点,我突然想到这不是一个bug,也不像我想的那样是一个代码破坏者.原因是因为我使用了byte [] pixels =((DataBufferByte)bufferedImage.getRaster().getDataBuffer()).getData();为了获得字节,我实际上正在攻击图像的内部数据结构(即忽略SampleModel). ImageReader的合同中没有任何内容保证字节的顺序.如果需要,可以*更改其内部数据结构(这是封装的权利吗?).如果您需要除默认行为之外的其他任何内容,则正确配置ImageReader的方法是获取其默认ImageReadParam并对其进行配置.然后使用reader.read(imageIndex,param)将其传递给读者;
因为我实际上希望读者返回图像字节的特定格式,所以我应该这样做:
log("Java version: " + System.getProperty("java.runtime.version"));
// get the reader
ImageReader ir = ImageIO.getImageReadersByFormatName("png").next();
// get the default param
ImageReadParam p = ir.getDefaultReadParam();
p.setDestinationType(
// define the image type to return if supported
ImageTypeSpecifier.createInterleaved(
ColorSpace.getInstance(ColorSpace.CS_sRGB),
new int[] {0, 1, 2, 3}, // <-- the order of the color bands to return so the bytes are in the desired order
DataBuffer.TYPE_BYTE,
true, false)
);
InputStream stream = TestBufferedImage.class.getClassLoader().getResourceAsStream("test.png");
ImageInputStream imageStream = ImageIO.createImageInputStream(stream);
ir.setInput(imageStream);
BufferedImage image = ir.read(0, p);
现在两个版本都将以相同的RGBA形式返回字节的顺序,即在两种情况下都会打印不同颜色的输出:
...
i=0 Converting [aa, cc, ee, ff] --> [aa, cc, ee]
...