/* * Geotoolkit - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2016, Geomatys * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotoolkit.coverage.landsat; import java.awt.Dimension; import java.awt.Point; import java.awt.Rectangle; import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import java.awt.image.DataBuffer; import java.awt.image.DataBufferByte; import java.awt.image.DataBufferDouble; import java.awt.image.DataBufferFloat; import java.awt.image.DataBufferInt; import java.awt.image.DataBufferShort; import java.awt.image.DataBufferUShort; import java.awt.image.Raster; import java.awt.image.SampleModel; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; import javax.imageio.ImageReadParam; import javax.imageio.ImageReader; import org.opengis.referencing.datum.PixelInCell; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.TransformException; import org.apache.sis.internal.referencing.j2d.AffineTransform2D; import org.apache.sis.util.ArgumentChecks; import org.apache.sis.util.logging.Logging; import org.geotoolkit.image.internal.ImageUtils; import org.geotoolkit.image.internal.PhotometricInterpretation; import org.geotoolkit.image.internal.PlanarConfiguration; import org.geotoolkit.image.internal.SampleType; import org.geotoolkit.image.interpolation.InterpolationCase; import org.geotoolkit.image.interpolation.Resample; import org.geotoolkit.image.io.XImageIO; import org.geotoolkit.image.io.large.AbstractLargeRenderedImage; import org.geotoolkit.metadata.iso.spatial.PixelTranslation; /** * Special {@link AbstractLargeRenderedImage} implementation for Landsat 8 image reading.<br> * Aim of this class is to aggregate on the fly all band during tile reading.<br> * Now, internal reader need to be updated for this use.<br> * For an efficient way reader must know use ImageReadParam.setSrcRenderSize() * to avoid unneccessary resampling for each tile. * * @author Remi Marechal (Geomatys). * @version 1.0 * @since 1.0 */ public class Landsat8RenderedImage extends AbstractLargeRenderedImage { /* * TODO : * Maybe performance should be improve with just a limited cache * of 9 tiles to avoid requesting tile during interpolation or other .... */ private final static Logger LOGGER = Logging.getLogger("org.geotoolkit.coverage.landsat"); /** * Different aggregated image bands which compose image. * Moreother image numBands equals size of this array. */ private final Path[] bands; /** * Subsampling offset. * Offset of the view in the real image space. */ private final double trsx; private final double trsy; /** * Subsampling scale. * Scale of the view in the real image space. */ private final double scaleX; private final double scaleY; /** * Define if the rasters at extremum image boundary, * should be clipped or not to to the image boundary size. */ private final boolean clipped; private SampleType sampleType; private final ColorModel oneBandColorModel; /** * Define an image which has outImgDimension as its boundary, which represent * a view of the srcImgBoundary into original image space.<br> * Moreover a subsampling is directly effectuate and aggregate all band on the fly during read tile. * * @param srcImgBoundary original source read image region. * @param bands array of {@link Path} to read all bands. * @param sampleModel {@link SampleModel} of this image. * @param outImgDimension size of this image. * @param colorModel {@link ColorModel} of this image. */ public Landsat8RenderedImage(final Rectangle srcImgBoundary, final Dimension outImgDimension, final SampleModel sampleModel, final ColorModel colorModel, final Path ...bands) { super((int)outImgDimension.getWidth(), (int)outImgDimension.getHeight(), sampleModel, colorModel); ArgumentChecks.ensureNonNull("bands", bands); if (bands.length == 0) throw new IllegalArgumentException("Impossible to define appropriate bands with empty bands paths array."); clipped = true; this.bands = bands; scaleX = srcImgBoundary.getWidth() / outImgDimension.getWidth() ; scaleY = srcImgBoundary.getHeight() / outImgDimension.getHeight(); //-- verif scale > 1 trsx = srcImgBoundary.getMinX(); trsy = srcImgBoundary.getMinY(); sampleType = SampleType.valueOf(sampleModel.getDataType()); oneBandColorModel = ImageUtils.createColorModel(sampleType, 1, PhotometricInterpretation.GRAYSCALE, null); } @Override public Raster getTile(int tileX, int tileY) { final int tileWidth = getTileWidth(); final int tileHeight = getTileHeight(); //-- clip raster maximum boundary values with the current image size int brx = Math.min(getWidth(), (tileX + 1) * tileWidth); int bry = Math.min(getHeight(), (tileY + 1) * tileHeight); //-- size of the current requested tile. final int tlx = tileX * tileWidth; final int tly = tileY * tileHeight; final int rasterWidth = (brx - tlx); final int rasterHeight = (bry - tly); final int srcRegionMinX = (int) Math.floor(tlx * scaleX + trsx); final int srcRegionMinY = (int) Math.floor(tly * scaleY + trsy); final int srcRegionMaxX = (int) Math.ceil(brx * scaleX + trsx); final int srcRegionMaxY = (int) Math.ceil(bry * scaleY + trsy); final Dimension srcRenderSize = new Dimension(rasterWidth, rasterHeight); final Rectangle srcRegion = new Rectangle(srcRegionMinX, srcRegionMinY, srcRegionMaxX - srcRegionMinX, srcRegionMaxY - srcRegionMinY); if (!clipped && (rasterWidth < tileWidth || rasterHeight < tileHeight)) { //-- will do re-copy return null; } final SampleModel oneBandSampleModel = ImageUtils.createSampleModel(PlanarConfiguration.BANDED, sampleType, rasterWidth, rasterHeight, 1); //-- create array bank container final Object bankData = createBankData(sampleType, bands.length); /* * Build Callable to multi thread reading band. */ final ExecutorService poule = Executors.newFixedThreadPool(bands.length); try { int b = 0; for (final Path band : bands) { if (!Files.exists(band)) throw new IllegalStateException("The data at current path : "+band+" doesn't exist."); //-- submit reading into thread poule.submit(new BandReader(sampleType, band, oneBandSampleModel, b++, bankData, srcRegion, srcRenderSize)); } //-- waiting end of reading poule.shutdown(); poule.awaitTermination(2, TimeUnit.MINUTES); //-- all bands are read final DataBuffer dataBuffer = createDatabuffer(sampleType, bankData, rasterWidth * rasterHeight); final SampleModel createSampleModel = ImageUtils.createSampleModel(PlanarConfiguration.BANDED, sampleType, rasterWidth, rasterHeight, bands.length); return Raster.createWritableRaster(createSampleModel, dataBuffer, new Point((int) tlx, (int) tly)); } catch (UnsupportedOperationException | InterruptedException ex) { throw new IllegalStateException(ex); } } /** * If read tile is not at the requested size, effectuate resampling to ajust it. * * @param tile * @param destImg * @throws TransformException */ private void resampleTile(final BufferedImage tile, final BufferedImage destImg) throws TransformException { MathTransform affineTransform2D = new AffineTransform2D(tile.getWidth() / (double) destImg.getWidth(), 0, 0, destImg.getHeight() / (double) tile.getHeight(), 0, 0); affineTransform2D = PixelTranslation.translate(affineTransform2D, PixelInCell.CELL_CORNER, PixelInCell.CELL_CENTER); final Resample resample = new Resample(affineTransform2D, destImg, tile, InterpolationCase.NEIGHBOR); resample.fillImage(); } /** * Build a 2D array, initialized at the asked {@link SampleType}. * * @param sampleType * @param bandNumber * @return */ private Object createBankData(final SampleType sampleType, final int bandNumber) { switch (sampleType) { case BYTE : { return new byte[bandNumber][]; } case SHORT : case USHORT : { return new short[bandNumber][]; } case INTEGER : { return new int[bandNumber][]; } case FLOAT : { return new float[bandNumber][]; } case DOUBLE : { return new double[bandNumber][]; } default : throw new IllegalStateException("Current SampleType : "+sampleType+" is not known."); //-- should never append } } /** * Create a {@link DataBuffer} initialized at correct sample type and filled by bankData array. * * @param sampleType * @param bankDatas * @param buffersize * @return */ private DataBuffer createDatabuffer(final SampleType sampleType, final Object bankDatas, final int buffersize) { switch (sampleType) { case BYTE : { return new DataBufferByte((byte[][]) bankDatas, buffersize); } case SHORT : { return new DataBufferShort((short[][]) bankDatas, buffersize); } case USHORT : { return new DataBufferUShort((short[][]) bankDatas, buffersize); } case INTEGER : { return new DataBufferInt((int[][]) bankDatas, buffersize); } case FLOAT : { return new DataBufferFloat((float[][]) bankDatas, buffersize); } case DOUBLE : { return new DataBufferDouble((double[][]) bankDatas, buffersize); } default : throw new IllegalStateException("Current SampleType : "+sampleType+" is not known."); //-- should never append } } /** * * @param sampleType * @param bandID * @param destinationArray * @param datas */ private void mergeRasters(final SampleType sampleType, final int bandID, final Object destinationArray, final BufferedImage datas) { switch (sampleType) { case BYTE : { final byte[][] da = (byte[][]) destinationArray; final DataBufferByte dbb = (DataBufferByte) datas.getData().getDataBuffer(); da[bandID] = dbb.getData(); break; } case SHORT : { final short[][] da = (short[][]) destinationArray; final DataBufferShort dbb = (DataBufferShort) datas.getData().getDataBuffer(); da[bandID] = dbb.getData(); break; } case USHORT : { final short[][] da = (short[][]) destinationArray; final DataBufferUShort dbb = (DataBufferUShort) datas.getData().getDataBuffer(); da[bandID] = dbb.getData(); break; } case INTEGER : { final int[][] da = (int[][]) destinationArray; final DataBufferInt dbb = (DataBufferInt) datas.getData().getDataBuffer(); da[bandID] = dbb.getData(); break; } case FLOAT : { final float[][] da = (float[][]) destinationArray; final DataBufferFloat dbb = (DataBufferFloat) datas.getData().getDataBuffer(); da[bandID] = dbb.getData(); break; } case DOUBLE : { final double[][] da = (double[][]) destinationArray; final DataBufferDouble dbb = (DataBufferDouble) datas.getData().getDataBuffer(); da[bandID] = dbb.getData(); break; } } } @Override protected void finalize() throws Throwable { super.finalize(); //close decoder : not needed, the jni binding has a fallback on finalize. // calling a close explicitly here provokes a JVM crash } /** * Private class to multi-Thread band reading. */ private class BandReader implements Runnable { private final Object bankDatas; private final Path bandPath; private final Rectangle sourceRegion; private final Dimension renderSize; private final int bandIndex; private final SampleModel oneBandSampleModel; public BandReader(final SampleType sampleType, final Path bandPath, final SampleModel oneBandSampleModel, final int bandIndex, final Object bankDatas, final Rectangle sourceRegion, final Dimension renderSize) { this.bankDatas = bankDatas; this.bandPath = bandPath; this.sourceRegion = sourceRegion; this.renderSize = renderSize; this.bandIndex = bandIndex; this.oneBandSampleModel = oneBandSampleModel; } @Override public void run() { final ImageReader reader; try { reader = XImageIO.getReader(bandPath, false, true); if (!sampleType.equals(SampleType.valueOf(reader.getRawImageType(0).getSampleModel().getDataType()))) { throw new IllegalArgumentException("Expected datatype : "+sampleType+", found : " +SampleType.valueOf(reader.getRawImageType(0).getSampleModel().getDataType())); } final ImageReadParam defaultReadParam = reader.getDefaultReadParam(); defaultReadParam.setSourceRegion(sourceRegion); if (defaultReadParam.canSetSourceRenderSize()) { defaultReadParam.setSourceRenderSize(renderSize); } else { //-- cast scale into int to increase out image size compared to math.ceil() defaultReadParam.setSourceSubsampling((int) Math.max(1, sourceRegion.width / renderSize.width), (int) Math.max(1, sourceRegion.height / renderSize.height), 0, 0); } BufferedImage read = reader.read(0, defaultReadParam); reader.dispose(); //-- do resampling if necessary if (read.getWidth() != renderSize.width || read.getHeight() != renderSize.height) { final BufferedImage destImg = new BufferedImage(oneBandColorModel, Raster.createWritableRaster(oneBandSampleModel, new Point()), false, null); resampleTile(read, destImg); read = destImg; } mergeRasters(sampleType, bandIndex, bankDatas, read); } catch (IOException | TransformException ex) { throw new IllegalStateException(ex); } } } }