让缓冲图像使用更少的 RAM?

2022-09-03 01:45:50

我有java程序,可以从硬盘驱动器读取jpeg文件,并将其用作各种其他事物的背景图像。图像本身存储在一个对象中,如下所示:BufferImage

BufferedImage background
background = ImageIO.read(file)

这很有效 - 问题是物体本身是巨大的。例如,一个 215k jpeg 文件会变成一个 4 兆的 e 对象并进行更改。有问题的应用程序可以加载一些相当大的背景图像,但是jpeg永远不会超过一两兆,但用于存储的内存可以很快超过100兆字节。BufferedImageBufferedImagBufferedImage

我假设所有这些都是因为图像作为原始RGB数据存储在ram中,而不是以任何方式压缩或优化。

有没有办法让它以较小的格式将图像存储在RAM中?我处于CPU方面比RAM更多的松弛的情况下,因此将图像对象的大小降低到jpeg压缩的轻微性能损失将是非常值得的。


答案 1

我的一个项目是,我只是在从ImageStream动态读取图像时对图像进行下采样。下采样将图像的尺寸减小到所需的宽度和高度,同时不需要昂贵的调整大小计算或修改磁盘上的图像。

由于我将图像下采样到较小的尺寸,因此它还显着降低了显示图像所需的处理能力和RAM。为了进行额外的优化,我还在图块中渲染缓冲图像...但这有点超出本次讨论的范围。请尝试以下操作:

public static BufferedImage subsampleImage(
    ImageInputStream inputStream,
    int x,
    int y,
    IIOReadProgressListener progressListener) throws IOException {
    BufferedImage resampledImage = null;

    Iterator<ImageReader> readers = ImageIO.getImageReaders(inputStream);

    if(!readers.hasNext()) {
      throw new IOException("No reader available for supplied image stream.");
    }

    ImageReader reader = readers.next();

    ImageReadParam imageReaderParams = reader.getDefaultReadParam();
    reader.setInput(inputStream);

    Dimension d1 = new Dimension(reader.getWidth(0), reader.getHeight(0));
    Dimension d2 = new Dimension(x, y);
    int subsampling = (int)scaleSubsamplingMaintainAspectRatio(d1, d2);
    imageReaderParams.setSourceSubsampling(subsampling, subsampling, 0, 0);

    reader.addIIOReadProgressListener(progressListener);
    resampledImage = reader.read(0, imageReaderParams);
    reader.removeAllIIOReadProgressListeners();

    return resampledImage;
  }

 public static long scaleSubsamplingMaintainAspectRatio(Dimension d1, Dimension d2) {
    long subsampling = 1;

    if(d1.getWidth() > d2.getWidth()) {
      subsampling = Math.round(d1.getWidth() / d2.getWidth());
    } else if(d1.getHeight() > d2.getHeight()) {
      subsampling = Math.round(d1.getHeight() / d2.getHeight());
    }

    return subsampling;
  }

要从文件中获取图像输入流,请使用:

ImageIO.createImageInputStream(new File("C:\\image.jpeg"));

如您所见,此实现还尊重图像的原始纵横比。您可以选择注册 IIOReadProgressListener,以便可以跟踪到目前为止已读取了多少图像。这对于显示进度条非常有用,例如,如果图像是通过网络读取的...但不是必需的,您可以只指定 null。

为什么这与你的情况特别相关?它永远不会将整个图像读取到内存中,就像您需要的那样多,以便它可以以所需的分辨率显示。对于巨大的图像,即使是那些在磁盘上是10 MB的图像,也非常有效。


答案 2

我假设所有这些都是因为图像作为原始RGB数据存储在ram中,而不是以任何方式压缩或优化。

完全。。。假设一个1920x1200 JPG可以容纳,比如说,在内存中,在(典型的)RGB + alpha中,每个组件8位(因此每像素32位)它应该在内存中占据:

1920 x 1200 x 32 / 8 = 9 216 000 bytes 

因此,您的300 KB文件将成为需要近9 MB RAM的图片(请注意,根据您从Java使用的图像类型以及JVM和操作系统,有时可能是GFX卡RAM)。

如果要使用图片作为1920x1200桌面的背景,则可能不需要在内存中拥有大于该图片的图片(除非您想要一些特殊效果,例如sub-rgb抽取/颜色抗锯齿/等)。

因此,您必须选择:

  1. 使磁盘上的文件不那么宽,越来越低(以像素为单位)
  2. 动态减小图像大小

我通常使用数字2,因为减小硬盘上的文件大小意味着您正在丢失细节(1920x1200的图片不如3940x2400的“相同”图片详细:通过缩小它来“丢失信息”)。

现在,Java在处理这么大的图片方面有点糟糕(无论是从性能的角度来看,从内存使用的角度来看,还是从质量的角度来看[*])。以前,我会从Java调用ImageMagick,首先调整磁盘上的图片大小,然后加载调整后的图像(比如说适合我的屏幕大小)。

如今,有Java桥接器/API可以直接与ImageMagick接口。

[*]首先您无法使用Java的内置API快速缩小图像,并且质量与ImageMagick提供的API一样好。


推荐