有人可能认为BufferedImage
是在Java中处理图像的最佳选项。虽然它很方便,但当阅读巨大的图像时,它通常会出现:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
增加虚拟机大小不是一个解决方案,因为在我的情况下,一些输入文件确实很大。
因此,我正在寻找如何从流中逐步读取图像的方法。
我怀疑ImageIO
中的ImageIO.createImageInputStream()
可能符合要求,但我不确定如何使用它来逐步读取块。还有类PNGMetadata
&JDK的rt.jar
上提供了PNGImageReader
,这似乎很有用,但我没有找到它们使用的简单示例。
这是一条路,还是有更好的选择?
用于读取和操作图像的Java API并不像您想象的那样真正基于流。ImageInputStream
只是一个方便的包装器,它允许从不同的输入(RandomAccessFile
s、InputStream
s等)读取byte
s和其他基元类型。
我一直在考虑创建一个用于读取"像素流"的API,以允许在不使用太多内存的情况下链接处理过滤器。但这种需求从未严重到值得付出努力的程度。如果你想听到更多的想法或有一个有效的实现,请随时雇用我。;-)
尽管如此,在我看来,你有多种选择来实现你的最终目标,即能够处理大图像:
-
按原样使用
BufferedImage
和ImageIO
API读取较小部分的图像以节省内存。由于实现的原因,这对于某些格式将非常有效,而对于其他格式则效率较低(即,默认的JPEGImageReader
将在将较小的区域移交给Java堆之前读取本地内存中的整个映像,但PNGImageReader
可能还可以)。大致如下:
ImageInputStream stream = ImageIO.createImageInputStream(input); ImageReader reader = ImageIO.getImageReaders(stream).next(); // TODO: Test hasNext() reader.setInput(stream); int width = reader.getWidth(0); int height = reader.getHeight(0); ImageReadParam param = reader.getDefaultReadParam(); for (int y = 0; y < height; y += 100) { for (int x = 0; x < width; x += 100) { param.setSourceRegion(new Rectangle(x, y, 100, 100)); // TODO: Bounds check // Read a 100 x 100 tile from the image BufferedImage region = reader.read(0, param); // ...process region as needed... } }
-
一次将整个图像读取到内存映射的缓冲区中。请随意尝试我为此目的制作的一些实验课程(使用
nio
)。读取将比读取纯内存图像慢,并且处理也将更慢。但是,如果你一次对图像的较小区域进行计算,经过一些优化,速度可能与内存中的速度一样快。我已经使用这些类将>1GB的映像读取到32MB的JVM中(实际内存消耗当然要大得多)。再次举一个例子:
ImageInputStream stream = ImageIO.createImageInputStream(input); ImageReader reader = ImageIO.getImageReaders(stream).next(); // TODO: Test hasNext() reader.setInput(stream); int width = reader.getWidth(0); int height = reader.getHeight(0); ImageTypeSpecifier spec = reader.getImageTypes(0).next(); // TODO: Test hasNext(); BufferedImage image = MappedImageFactory.createCompatibleMappedImage(width, height, spec) ImageReadParam param = reader.getDefaultReadParam(); param.setDestination(image); image = reader.read(0, param); // Will return same image as created above // ...process image as needed...
-
某些格式,如未压缩的TIFF、BMP、PPM等,可以将像素保存在文件中,从而可以直接对其进行内存映射以进行操作。需要一些工作,但应该是可能的。TIFF还支持可能有帮助的平铺。我将把这个选项作为练习,可以随意使用我上面链接的课程作为起点或灵感。;-)
-
JAI可能有什么可以帮助你的。我不是一个大粉丝,因为它有很多未解决的错误,而且缺乏对Oracle的热爱和开发。但值得一看。我认为他们也支持可平铺和基于磁盘的
RenderedImage
。同样,我将把这个选项留给您进一步探索。
内存问题肯定与解码过程本身无关,而是与将完整图像作为BufferedImage
存储在内存中有关。可以逐步读取PNG图像,但是:
-
这与组织的"块"关系不大,尤其是PNG文件是逐行编码的,因此原则上可以逐行读取。
-
上述假设在交错PNG的情况下被打破,但人们不应该期望以交错格式存储巨大的PNG图像
-
虽然一些PNG库允许渐进式(逐行)解码(例如:libpng),但标准的Java API并没有提供这种功能。
我遇到了这个问题,最后我编写了自己的Java库:PNGJ。它非常成熟,可以逐行读取PNG图像,最大限度地减少内存消耗,并以相同的方式写入(即使是交错的PNG也可以读取,但在这种情况下,内存问题不会消失。)如果您只需要进行一些"局部"图像处理(根据当前值和邻居修改每个像素值,然后将其写回),这应该会有所帮助。