使BufferedImage使用更少的RAM?

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

 BufferedImage background background = ImageIO.read(file) 

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

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

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

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

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

 public static BufferedImage subsampleImage( ImageInputStream inputStream, int x, int y, IIOReadProgressListener progressListener) throws IOException { BufferedImage resampledImage = null; Iterator 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英寸的图像。

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

确切地说……假设一个1920×1200 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)。

如果你想使用一张图片作为1920×1200桌面的背景,你可能不需要比内存更大的图片(除非你想要一些特殊效果,比如sub-rgb decimation / color anti-aliasing /等等。)。

所以你必须选择:

  1. 使您的文件在磁盘上的宽度更小,更高(以像素为单位)
  2. 动态缩小图像大小

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

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

现在有Java桥/ API可以直接与ImageMagick连接。

[*]没有办法使用Java的内置API缩小图像的速度,并且质量和ImageMagick提供的质量一样好。

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

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

使用imgscalr:

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

为什么?

  1. 遵循最佳实践
  2. 愚蠢的简单
  3. 插值,抗锯齿支持
  4. 所以你没有滚动你自己的缩放库

码:

 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()以帮助提高内存利用率。

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

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

 BufferedImage background= new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); int[] pixels = background.getRaster().getPixels(0, 0, imageBuffer.getWidth(), imageBuffer.getHeight(), (int[]) null);