使BufferedImage使用更少的RAM?

时间:2010-07-20 21:11:07

标签: java memory image awt

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

BufferedImage background
background = ImageIO.read(file)

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

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

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

6 个答案:

答案 0 :(得分:10)

我的一个项目我只是在动态从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;
  }

要从File获取ImageInputStream,请使用:

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

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

为什么这与您的情况特别相关?它永远不会将整个图像读入内存,就像您需要它一样,以便它可以以所需的分辨率显示。对于巨大的图像非常有效,即使是磁盘上10英寸的图像也是如此。

答案 1 :(得分:3)

  

我认为这一切都是因为图像   作为原始RGB存储在ram中   数据,未压缩或优化   无论如何。

确实......假设一个1920x1200的JPG在内存中可以容纳300 KB,在(典型的)RGB + alpha中,每个组件8位(因此每像素32位)它应该占用内存:

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

所以你的300 KB文件变成需要近9 MB RAM的图片(请注意,根据您使用的图像类型,取决于JVM和OS,这有时可能是GFX卡RAM)。 / p>

如果您想将图片用作1920x1200桌面的背景,您可能不需要比内存中的图片更大(除非您想要一些特殊效果,例如sub-rgb decimation / color anti -aliasing / etc.)。

所以你必须做出选择:

  1. 使您的文件不那么宽,不太高(以像素为单位)在磁盘上
  2. 动态缩小图像尺寸
  3. 我通常使用数字2,因为减少硬盘上的文件大小意味着你正在丢失细节(1920x1200图片的细节不如3940x2400的“相同”:你会通过缩减来“丢失信息”)。

    现在,Java有点大肆操纵大图片(从性能角度来看,从内存使用的角度来看,从质量的角度来看[*])。回到过去,我会先用Java调用ImageMagick来调整磁盘上的图片大小,然后加载调整大小的图片(比如说我的屏幕大小)。

    现在有Java桥/ API直接与ImageMagick接口。

    [*] NO WAY 您正在使用Java的内置API缩小图像的速度,并且质量与ImageMagick提供的质量一样好,开始时。

答案 2 :(得分:2)

你必须使用BufferedImage吗?你能编写自己的Image实现,将jpg字节存储在内存中,并根据需要转换为BufferedImage,然后丢弃吗?

这适用于一些显示感知逻辑(在将您的字节数组存储为jpg之前使用JAI重新缩放图像),将使其比每次解码大型jpg更快,并且占用的空间比您当前拥有的更小(处理内存)要求除外)。

答案 3 :(得分:2)

使用imgscalr:

http://www.thebuzzmedia.com/software/imgscalr-java-image-scaling-library/

为什么?

  1. 遵循最佳做法
  2. 愚蠢简单
  3. 插值,抗锯齿支持
  4. 所以你没有自己动手缩放库
  5. 代码:

    BufferedImage thumbnail = Scalr.resize(image, 150);
    
    or
    
    BufferedImage thumbnail = Scalr.resize(image, Scalr.Method.SPEED, Scalr.Mode.FIT_TO_WIDTH, 150, 100, Scalr.OP_ANTIALIAS);
    

    此外,转换后在较大的图片上使用image.flush()以帮助提高内存利用率。

答案 4 :(得分:1)

磁盘上JPG的文件大小完全 无关
文件的像素尺寸是。如果您的图像是15百万像素,则需要使用垃圾填充RAM来加载原始未压缩版本 将图像尺寸重新调整为您需要的尺寸,这是您可以做的最好的选择,而无需使用不太丰富的色彩空间表示。

答案 5 :(得分:0)

您可以将图像的像素复制到另一个缓冲区,看看它是否比BufferedImage对象占用更少的内存。可能是这样的:

BufferedImage background = new BufferedImage(
   width, 
   height, 
   BufferedImage.TYPE_INT_RGB
);

int[] pixels = background.getRaster().getPixels(
    0, 
    0, 
    imageBuffer.getWidth(), 
    imageBuffer.getHeight(), 
    (int[]) null
);