使用BufferedImage和ImageIO将图像转换为byte[]后,图像大小变小

3

我正在使用以下代码将图像转换为byte[]。

public static byte[] extractBytes (String ImageName) throws IOException {

   ByteArrayOutputStream baos=new ByteArrayOutputStream();
        BufferedImage img=ImageIO.read(new File(ImageName));
        ImageIO.write(img, "jpg", baos);
        return baos.toByteArray();
    }

现在当我测试我的代码时:
public static void main(String[] args) throws IOException {
    String filepath = "image_old.jpg";
    File outp=new File(filepath);
    System.out.println("Size of original image="+outp.length());
    byte[] data = extractBytes(filepath);
    System.out.println("size of byte[] data="+data.length);
    BufferedImage img = ImageIO.read(new ByteArrayInputStream(data));
    //converting the byte[] array into image again
    File outputfile = new File("image_new.jpg");
    ImageIO.write(img, "jpeg", outputfile);
    System.out.println("size of converted image="+outputfile.length());
}

我得到了非常奇怪的结果:

Size of original image=78620 
size of byte[] data=20280
size of converted image=20244

将图像转换为字节数组后,其大小减小了约四分之一,当我将byte[]转换回图像时,其大小也会改变。但是,在所需位置成功创建输出图像。在放大500-600%后,我可以看到原始图像和新图像的轻微差异。缩放后新图像有些模糊。
这里是我正在测试的图像 http://pbrd.co/1BrOVbf 请解释此大小更改的原因,并告诉我是否有任何方法可以在此之后获得相同的大小。

此外,当您使用JPEG压缩图像时,JPEG文件的质量比原始图像低。 - user253751
@immibis,为什么即使我没有添加任何压缩代码,它也被压缩了呢?你能详细解释一下这里发生了什么吗?我很困惑。 - Harsh Sharma
@immibis,是的,质量也在下降。我在对比两张图片时进行了600%的缩放后才意识到这一点。 :P - Harsh Sharma
@HarshSharma JPEG格式是一种压缩格式,也是一种有损格式(这意味着它会失去质量)。因此,每次将图像保存为JPEG时,您(或者说ImageIO系统)都在压缩图像。 - user253751
@HarshSharma 请看我的回答,这可能是发生这种情况的最有可能的原因。如果可能的话,请提供您的原始文件链接。您所看到的完全正常,不用担心。;-) PS:您还应该在您的“问题”中明确说明您想知道为什么会发生这种情况(我试图回答)还是如何修复(描述您的目标,为什么它不起作用以及预期行为)。 - Harald K
显示剩余4条评论
2个回答

5
您所拥有的图片是使用最高质量设置(“100%”或ImageIO术语中的1.0)进行压缩的。在这种高质量设置下,JPEG压缩并不是非常有效,因此比通常情况下的文件要大得多。当使用ImageIO.write(..., "JPEG", ...)时,将使用默认的质量设置。0.75是这个默认值(这样一个值确切的含义取决于编码器,不是精确的科学),因此质量较低,结果是文件更小。
(另一个导致原始图像和重新压缩图像之间文件大小显著降低的原因是元数据的移除。使用ImageIO.read(file)读取时,您实际上正在剥离JPEG文件中的任何元数据,例如XMP、Exif或ICC配置文件。在极端情况下(是的,我主要是指Photoshop;-) ),这些元数据可能占用比图像数据本身更多的空间(即元数据可以达到几兆字节)。但对于您的文件来说,情况并非如此。)
从第二次重新压缩(从byte[]到最终输出文件)可以看出,输出文件只比输入文件略小一些。这是因为质量设置(未指定,因此仍使用默认设置)在两种情况下是相同的(此外,在这一步中任何元数据也将丢失,因此不会增加文件大小)。小差异可能是由于JPEG解压缩/重新压缩过程中出现了一些小损失(舍入误差等)导致的。
重压缩JPEG时,实现最小数据损失(指与原始图像相比的变化量,而不是文件大小)的方法总是使用与原始图像相同的质量设置(使用完全相同的表应该几乎无损,但可能仍会发生小的舍入误差)。增加质量设置将使文件输出更大,但实际上会降低质量。
要想100%地确保不会损失任何数据或图像质量,唯一的方法就是首先不对图像进行编码/解码,而是直接按字节复制文件,例如:
File in = ...;
File out = ...;

InputStream input = new FileInputStream(in);
try {
    OutputStream output = new FileOutputStream(out);
    try {
        copy(input, output);
    }
    finally {
        output.close();
    }
}
finally {
    input.close();
}

还有一个复制方法:

public void copy(final InputStream in, final OutputStream out) {
    byte[] buffer = new byte[1024]; 
    int count;

    while ((count = in.read(buffer)) != -1) {
        out.write(buffer, 0, count);
    }

    // Flush out stream, to write any remaining buffered data
    out.flush();
}

嘿,我已经编辑了我的答案,并添加了我正在测试的图像。 - Harsh Sharma
是的,那是我现在正在使用的方法。谢谢(Y) - Harsh Sharma
我来晚了,但是有另一个信息。当使用ImageIO.write时,我得到了一个倒置的图像文件和一个大尺寸的图像文件(2.3 Mb --> 23 Mb)。我知道我做错了什么,但是找不到原因... https://stackoverflow.com/questions/62473381/java-byte-array-to-image-file-creation-results-a-rotated-upside-down-image-file - Bhanuchander Udhayakumar
@BhanuchanderUdhayakumar 很可能您的原始文件具有Exif信息,指定图像方向。大多数软件将根据此为您旋转图像。ImageIO不会。因此,当您再次写入没有Exif的图像时,方向将不再应用,图像将出现倒置或仅旋转90度。 - Harald K
@haraldK 感谢您的回复。我也得到了尺寸增加了的图像文件(大约增加了10倍)。这是如何在字节数组中发生的? - Bhanuchander Udhayakumar

1
当您调用ImageIO.write(img, "jpeg", outputfile);时,ImageIO库会使用自己的压缩参数编写JPEG图像。输出图像似乎比输入图像更压缩。您可以通过更改下面对jpegParams.setCompressionQuality的调用中的参数来调整压缩级别。由此产生的文件可能比原始文件更大或更小,具体取决于每个文件的相对压缩级别。
public static ImageWriter getImageWriter() throws IOException {
    IIORegistry registry = IIORegistry.getDefaultInstance();
    Iterator<ImageWriterSpi> services = registry.getServiceProviders(ImageWriterSpi.class, (provider) -> {
        if (provider instanceof ImageWriterSpi) {
            return Arrays.stream(((ImageWriterSpi) provider).getFormatNames()).anyMatch(formatName -> formatName.equalsIgnoreCase("JPEG"));
        }
        return false;
    }, true);
    ImageWriterSpi writerSpi = services.next();
    ImageWriter writer = writerSpi.createWriterInstance();
    return writer;
}

public static void main(String[] args) throws IOException {
    String filepath = "old.jpg";
    File outp = new File(filepath);
    System.out.println("Size of original image=" + outp.length());
    byte[] data = extractBytes(filepath);
    System.out.println("size of byte[] data=" + data.length);
    BufferedImage img = ImageIO.read(new ByteArrayInputStream(data));
    File outputfile = new File("new.jpg");
    JPEGImageWriteParam jpegParams = new JPEGImageWriteParam(null);
    jpegParams.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
    jpegParams.setCompressionQuality(1f);
    ImageWriter writer = getImageWriter();
    outputfile.delete();
    try (final ImageOutputStream stream = createImageOutputStream(outputfile)) {
        writer.setOutput(stream);
        try {
            writer.write(null, new IIOImage(img, null, null), jpegParams);
        } finally {
            writer.dispose();
            stream.flush();
        }
    }
    System.out.println("size of converted image=" + outputfile.length());
}

这个解决方案是从JeanValjean在这里给出的答案中改编而来 使用Java中的ImageIO设置jpg压缩级别

嘿,感谢您的精彩回复,但我仍然困惑,如果我使用jpegParams.setCompressionQuality(1f),为什么我的图像大小仍在改变(在这种情况下增加)?为什么我的新图像大小与原始图像大小不同? - Harsh Sharma
@HarshSharma 因为您仍在解码/重新编码JPEG。相反,如果您想要一个完全的副本,请按照我的答案建议进行操作。只需复制数据,就像任何其他二进制数据一样。 - Harald K

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接