Óriásképek hatékony kezelése Java-ban

    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);
            }
    
        }
    }