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

    }
}
Kategória: Java

Támogasd munkánkat!

Blogunkat nonprofit módon, teljesen ingyen készítjük, már 7 éve. Ingyenes szoftvereket biztosítunk több száz cég számára, és természetvédelmi tevékenységet is végzünk. Ezekből semmilyen bevételünk nem származik. De mi is csak pénzből tudjuk magunkat fenntartani. Azért gyűjtünk, hogy tovább tudjuk folytatni hasznos értékteremtő munkánkat, ezért kérünk, ha van 1000 - 5000 Ft-od, amit fel tudsz ajánlani számunkra, támogass minket!

Cégünk: Völgyerdő Nonprofit Kft.

Számlaszámunk: 11749015-28535807 (OTP Bank)

 

Látogatók

134496
Ma34
Tegnap43
Ezen a héten76
Ebben a hónapban1001
Összesen134496
Statistik created: 2019-10-22T13:39:11+00:00
Bejelentkezett felhasználók 0
Regisztrált felhasználók 1
Ma regisztráltak 0