- Megjelent: 2016. május 13
Alapvető probléma a Java képkezelésével, hogy a képeket teljes egészében be kell tölteni a memóriába ahhoz, hogy valamit lehessen velük kezdeni. A BufferedImage esetében a teljes kép a memóriába kerül, ami gátat szab a képméretnek. Erre kínál megoldást a Puli Space Technologies által kifejlesztett módszer.
A megoldás
A nagy méretű képek kezelésére a legtöbb fórumban a képek feldarabolását ajánlják megoldásként: a képek több darabban való betöltését több különböző BufferedImage-be. Ennek az a hátránya, hogy nem lehet egységesen kezelni a képet. Viszont van ennél egy sokkal hatékonyabb megoldás. A BigBufferedImage lehetővé teszi a képek memóriakorlátok nélküli betöltését a BufferedImage objektumba.
Képtárolás és -hozzáférés
A megoldás lényege, hogy a BufferedImage bufferét képező DataBuffer-t kicseréljük egy olyan speciális DataBuffer-re, amely a memória helyett a háttértáron, fájlokban tárolja a kép pixeleit. A fájl alapú bufferhez való hozzáférést pedig egy MappedByteBuffer osztállyal oldjuk meg, amely lehetővé teszi a fájlban tárolt pixelek rendkívül hatékony olvasását és írását.
Kép betöltése
A kép betöltésére szerencsére megoldást kínál az ImageIO osztály. Segítségével a képet részenként is be lehet olvasni, így a képnek egyszerre csak egy meghatározott része töltődik be a memóriába. A képdarabokat pedig azonnal bemásoljuk a fájl alapú DataBuffer-be, így kikerülnek a memóriából.
Korlátok
A BigBufferedImage maximum 2 147 483 647 pixel (46 340 x 46 340 pixel) tárolására képes. 4 színcsatorna (RGBA) esetén ez 8 GByte nyers adatot jelent. Ez megegyezik a hagyományos BufferedImage kapacitásával, viszont a BigBufferedImage csak a kép betöltéskor használja a memóriát és akkor is csak korlátolt mértékben, így a heap limit vagy a korlátozott fizikai memória nem jelent korlátot.
Letöltés
![]() |
BigBufferedImage.java letöltése |
Licenc: Creative Commons CC0 Fejlesztő: Puli Space Technologies Csatlakozz te is: Kis Lépés Klub |
Használat
A BigBufferedImage példányosítást követően ugyanúgy használható, mint a BufferedImage osztály. Egyetlen megkötés, hogy a kép típusa csak TYPE_INT_RGB vagy TYPE_INT_ARGB lehet. A betöltött képet a munkakönyvtárban tárolja, amit használat után érdemes törölni.
A képek fájlból történő betöltésekor ideiglenesen használt memória maximális méretét a BigBufferedImage.java osztály MAX_PIXELS_IN_MEMORY konstansával lehet megadni.
BigBufferedImage image = BigBufferedImage.create( // Üres kép létrehozása
[munkakönyvtár útvonala],
[kép szélessége],
[kép magassága],
[képtípus]);
BigBufferedImage image = BigBufferedImage.create( // Kép betöltése fájlból
[betöltendő fájl],
[munkakönyvtár útvonala],
[képtípus]);
Forráskód
/*
* This class is part of MCFS (Mission Control - Flight Software) a development
* of Team Puli Space, official Google Lunar XPRIZE contestant.
* This class is released under Creative Commons CC0.
* @author Zsolt Pocze
* Please like us on facebook, and/or join our Small Step Club.
* http://www.pulispace.com
* https://www.facebook.com/pulispace
* http://nyomdmegteis.hu/en/
*/
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.color.ColorSpace;
import java.awt.image.BandedSampleModel;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.ComponentColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.Raster;
import java.awt.image.SampleModel;
import java.awt.image.WritableRaster;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
public class BigBufferedImage extends BufferedImage {
private static final int MAX_PIXELS_IN_MEMORY = 50000000;
public static BigBufferedImage create(File tempDir, int width, int height, int imageType) throws IOException {
FileDataBuffer buffer = new FileDataBuffer(tempDir, width * height, 4);
ColorModel colorModel = null;
BandedSampleModel sampleModel = null;
switch (imageType) {
case TYPE_INT_RGB:
colorModel = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB),
new int[]{8, 8, 8, 0},
false,
false,
ComponentColorModel.TRANSLUCENT,
DataBuffer.TYPE_BYTE);
sampleModel = new BandedSampleModel(DataBuffer.TYPE_BYTE, width, height, 3);
break;
case TYPE_INT_ARGB:
colorModel = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB),
new int[]{8, 8, 8, 8},
true,
false,
ComponentColorModel.TRANSLUCENT,
DataBuffer.TYPE_BYTE);
sampleModel = new BandedSampleModel(DataBuffer.TYPE_BYTE, width, height, 4);
break;
default:
throw new IllegalArgumentException("Unsupported image type: " + imageType);
}
SimpleRaster raster = new SimpleRaster(sampleModel, buffer, new Point(0, 0));
BigBufferedImage image = new BigBufferedImage(colorModel, raster, colorModel.isAlphaPremultiplied(), null);
return image;
}
public static BigBufferedImage create(File inputFile, File tempDir, int imageType) throws IOException {
ImageInputStream stream = ImageIO.createImageInputStream(inputFile);
Iterator<ImageReader> readers = ImageIO.getImageReaders(stream);
if (readers.hasNext()) {
try {
ImageReader reader = readers.next();
reader.setInput(stream, true, true);
int width = reader.getWidth(reader.getMinIndex());
int height = reader.getHeight(reader.getMinIndex());
BigBufferedImage image = create(tempDir, width, height, imageType);
int cores = Math.max(1, Runtime.getRuntime().availableProcessors() / 2);
int block = Math.min(MAX_PIXELS_IN_MEMORY / cores / width, (int) (Math.ceil(height / (double) cores)));
ExecutorService generalExecutor = Executors.newFixedThreadPool(cores);
List<Callable<ImagePartLoader>> partLoaders = new ArrayList<>();
for (int y = 0; y < height; y += block) {
partLoaders.add(new ImagePartLoader(
y, width, Math.min(block, height - y), inputFile, image));
}
generalExecutor.invokeAll(partLoaders);
generalExecutor.shutdown();
return image;
} catch (InterruptedException ex) {
Logger.getLogger(BigBufferedImage.class.getName()).log(Level.SEVERE, null, ex);
}
}
return null;
}
private static class ImagePartLoader implements Callable<ImagePartLoader> {
private final int y;
private final BigBufferedImage image;
private final Rectangle region;
private final File file;
public ImagePartLoader(int y, int width, int height, File file, BigBufferedImage image) {
this.y = y;
this.image = image;
this.file = file;
region = new Rectangle(0, y, width, height);
}
@Override
public ImagePartLoader call() throws Exception {
Thread.currentThread().setPriority((Thread.MIN_PRIORITY + Thread.NORM_PRIORITY) / 2);
ImageInputStream stream = ImageIO.createImageInputStream(file);
Iterator<ImageReader> readers = ImageIO.getImageReaders(stream);
if (readers.hasNext()) {
ImageReader reader = readers.next();
reader.setInput(stream, true, true);
ImageReadParam param = reader.getDefaultReadParam();
param.setSourceRegion(region);
BufferedImage part = reader.read(0, param);
Raster source = part.getRaster();
WritableRaster target = image.getRaster();
target.setRect(0, y, source);
}
return ImagePartLoader.this;
}
}
private BigBufferedImage(ColorModel cm, WritableRaster raster, boolean isRasterPremultiplied, Hashtable<?, ?> properties) {
super(cm, raster, isRasterPremultiplied, properties);
}
private static class SimpleRaster extends WritableRaster {
public SimpleRaster(SampleModel sampleModel, DataBuffer dataBuffer, Point origin) {
super(sampleModel, dataBuffer, origin);
}
}
public Rectangle getRectangle() {
return new Rectangle(0, 0, getWidth(), getHeight());
}
private static class FileDataBuffer extends DataBuffer {
private final String id = "buffer-" + System.currentTimeMillis() + "-" + ((int) (Math.random() * 1000));
private File dir;
private String path;
private MappedByteBuffer[] buffer;
public FileDataBuffer(File dir, int size) throws FileNotFoundException, IOException {
super(TYPE_BYTE, size);
this.dir = dir;
init();
}
public FileDataBuffer(File dir, int size, int numBanks) throws FileNotFoundException, IOException {
super(TYPE_BYTE, size, numBanks);
this.dir = dir;
init();
}
private void init() throws FileNotFoundException, IOException {
if (dir == null) {
dir = new File(".");
}
if (!dir.exists()) {
throw new RuntimeException("FileDataBuffer constructor parameter dir does not exist: " + dir);
}
if (!dir.isDirectory()) {
throw new RuntimeException("FileDataBuffer constructor parameter dir is not a directory: " + dir);
}
path = dir.getPath() + "/" + id;
File subDir = new File(path);
subDir.mkdir();
subDir.deleteOnExit();
buffer = new MappedByteBuffer[banks];
for (int i = 0; i < banks; i++) {
File file = new File(path + "/bank" + i + ".dat");
file.deleteOnExit();
buffer[i] = new RandomAccessFile(file, "rw")
.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, getSize());
}
}
@Override
public int getElem(int bank, int i) {
return buffer[bank].get(i) & 0xff;
}
@Override
public void setElem(int bank, int i, int val) {
buffer[bank].put(i, (byte) val);
}
}
}