/* JAI-Ext - OpenSource Java Advanced Image Extensions Library * http://www.geo-solutions.it/ * Copyright 2014 GeoSolutions * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package it.geosolutions.jaiext.convolve; import it.geosolutions.jaiext.border.BorderDescriptor; import it.geosolutions.jaiext.iterators.RandomIterFactory; import it.geosolutions.jaiext.range.Range; import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.image.DataBuffer; import java.awt.image.Raster; import java.awt.image.RenderedImage; import java.awt.image.WritableRaster; import java.util.Arrays; import javax.media.jai.AreaOpImage; import javax.media.jai.BorderExtender; import javax.media.jai.ImageLayout; import javax.media.jai.IntegerSequence; import javax.media.jai.KernelJAI; import javax.media.jai.PlanarImage; import javax.media.jai.ROI; import javax.media.jai.ROIShape; import javax.media.jai.RasterAccessor; import javax.media.jai.RasterFormatTag; import javax.media.jai.iterator.RandomIter; import com.sun.media.jai.util.ImageUtil; public abstract class ConvolveOpImage extends AreaOpImage { /** Constant indicating that the inner random iterators must pre-calculate an array of the image positions */ public static final boolean ARRAY_CALC = true; /** Constant indicating that the inner random iterators must cache the current tile position */ public static final boolean TILE_CACHED = true; /** Boolean indicating that NoData must be checked */ protected final boolean hasNoData; /** NoData Range element */ protected Range noData; /** LookupTable used for checking if an input byte sample is a NoData */ protected boolean[] lut; /** Boolean indicating that ROI must be checked */ protected final boolean hasROI; /** ROI element */ protected ROI roi; /** Boolean indicating that no roi and no data check must be done */ protected final boolean caseA; /** Boolean indicating that only roi check must be done */ protected final boolean caseB; /** Boolean indicating that only no data check must be done */ protected final boolean caseC; /** ROI bounds as a Shape */ protected final Rectangle roiBounds; /** ROI related image */ protected PlanarImage roiImage; /** Destination No Data value for Byte sources */ protected byte destNoDataByte; /** Destination No Data value for Short sources */ protected short destNoDataShort; /** Destination No Data value for Integer sources */ protected int destNoDataInt; /** Destination No Data value for Float sources */ protected float destNoDataFloat; /** Destination No Data value for Double sources */ protected double destNoDataDouble; protected boolean skipNoData; protected RenderedImage extendedIMG; protected Rectangle destBounds; protected KernelJAI kernel; protected int kw; protected int kh; protected int kx; protected int ky; public ConvolveOpImage(RenderedImage source, BorderExtender extender, RenderingHints hints, ImageLayout l, KernelJAI kernel, ROI roi, Range noData, double destinationNoData, boolean skipNoData) { super(source, l, hints, true, extender, kernel.getLeftPadding(), kernel.getRightPadding(), kernel.getTopPadding(), kernel.getBottomPadding()); this.kernel = kernel; kw = kernel.getWidth(); kh = kernel.getHeight(); kx = kernel.getXOrigin(); ky = kernel.getYOrigin(); // Check if ROI control must be done if (roi != null) { hasROI = true; // Roi object this.roi = roi; roiBounds = roi.getBounds(); } else { hasROI = false; this.roi = null; roiBounds = null; } // Check if No Data control must be done if (noData != null) { hasNoData = true; this.noData = noData; this.skipNoData = skipNoData; } else { hasNoData = false; this.skipNoData = false; } // Getting datatype int dataType = source.getSampleModel().getDataType(); // Destination No Data value is clamped to the image data type this.destNoDataDouble = destinationNoData; switch (dataType) { case DataBuffer.TYPE_BYTE: this.destNoDataByte = ImageUtil.clampRoundByte(destinationNoData); break; case DataBuffer.TYPE_USHORT: this.destNoDataShort = ImageUtil.clampRoundUShort(destinationNoData); break; case DataBuffer.TYPE_SHORT: this.destNoDataShort = ImageUtil.clampRoundShort(destinationNoData); break; case DataBuffer.TYPE_INT: this.destNoDataInt = ImageUtil.clampRoundInt(destinationNoData); break; case DataBuffer.TYPE_FLOAT: this.destNoDataFloat = ImageUtil.clampFloat(destinationNoData); break; case DataBuffer.TYPE_DOUBLE: break; default: throw new IllegalArgumentException("Wrong image data type"); } // Definition of the possible cases that can be found // caseA = no ROI nor No Data // caseB = ROI present but No Data not present // caseC = No Data present but ROI not present // Last case not defined = both ROI and No Data are present caseA = !hasNoData && !hasROI; caseB = !hasNoData && hasROI; caseC = hasNoData && !hasROI; if (hasNoData && dataType == DataBuffer.TYPE_BYTE) { initBooleanNoDataTable(); } if (this.extender != null) { extendedIMG = BorderDescriptor.create(source, leftPadding, rightPadding, topPadding, bottomPadding, extender, noData, destinationNoData, hints); this.destBounds = getBounds(); } else { int x0 = getMinX() + leftPadding; int y0 = getMinY() + topPadding; int w = getWidth() - leftPadding - rightPadding; w = Math.max(w, 0); int h = getHeight() - topPadding - bottomPadding; h = Math.max(h, 0); this.destBounds = new Rectangle(x0, y0, w, h); } } private void initBooleanNoDataTable() { // Initialization of the boolean lookup table lut = new boolean[256]; // Fill the lookuptable for (int i = 0; i < 256; i++) { boolean result = true; if (noData.contains((byte) i)) { result = false; } lut[i] = result; } } /** * Performs convolution on a specified rectangle. The sources are cobbled. * * @param sources an array of source Rasters, guaranteed to provide all necessary source data for computing the output. * @param dest a WritableRaster tile containing the area to be computed. * @param destRect the rectangle within dest to be processed. */ protected void computeRect(Raster[] sources, WritableRaster dest, Rectangle destRect) { // Retrieve format tags. RasterFormatTag[] formatTags = getFormatTags(); Raster source = sources[0]; Rectangle srcRect = mapDestRect(destRect, 0); RasterAccessor src = new RasterAccessor(source, srcRect, formatTags[0], getSourceImage(0) .getColorModel()); RasterAccessor dst = new RasterAccessor(dest, destRect, formatTags[1], getColorModel()); // ROI fields ROI roiTile = null; RandomIter roiIter = null; boolean roiContainsTile = false; boolean roiDisjointTile = false; // ROI check if (hasROI) { Rectangle srcRectExpanded = mapDestRect(destRect, 0); // The tile dimension is extended for avoiding border errors srcRectExpanded.setRect(srcRectExpanded.getMinX() - 1, srcRectExpanded.getMinY() - 1, srcRectExpanded.getWidth() + 2, srcRectExpanded.getHeight() + 2); roiTile = roi.intersect(new ROIShape(srcRectExpanded)); if (!roiBounds.intersects(srcRectExpanded)) { roiDisjointTile = true; } else { roiContainsTile = roiTile.contains(srcRectExpanded); if (!roiContainsTile) { if (!roiTile.intersects(srcRectExpanded)) { roiDisjointTile = true; } else { PlanarImage roiIMG = getImage(); roiIter = RandomIterFactory.create(roiIMG, null, TILE_CACHED, ARRAY_CALC); } } } } if (!hasROI || !roiDisjointTile) { switch (dst.getDataType()) { case DataBuffer.TYPE_BYTE: byteLoop(src, dst, roiIter, roiContainsTile); break; case DataBuffer.TYPE_USHORT: ushortLoop(src, dst, roiIter, roiContainsTile); break; case DataBuffer.TYPE_SHORT: shortLoop(src, dst, roiIter, roiContainsTile); break; case DataBuffer.TYPE_INT: intLoop(src, dst, roiIter, roiContainsTile); break; case DataBuffer.TYPE_FLOAT: floatLoop(src, dst, roiIter, roiContainsTile); break; case DataBuffer.TYPE_DOUBLE: doubleLoop(src, dst, roiIter, roiContainsTile); break; default: throw new IllegalArgumentException("Wrong Data Type defined"); } // If the RasterAccessor object set up a temporary buffer for the // op to write to, tell the RasterAccessor to write that data // to the raster no that we're done with it. if (dst.isDataCopy()) { dst.clampDataArrays(); dst.copyDataToRaster(); } } else { // Setting all as NoData double[] backgroundValues = new double[src.getNumBands()]; Arrays.fill(backgroundValues, destNoDataDouble); ImageUtil.fillBackground(dest, destRect, backgroundValues); } } protected abstract void byteLoop(RasterAccessor src, RasterAccessor dst, RandomIter roiIter, boolean roiContainsTile); protected abstract void ushortLoop(RasterAccessor src, RasterAccessor dst, RandomIter roiIter, boolean roiContainsTile); protected abstract void shortLoop(RasterAccessor src, RasterAccessor dst, RandomIter roiIter, boolean roiContainsTile); protected abstract void intLoop(RasterAccessor src, RasterAccessor dst, RandomIter roiIter, boolean roiContainsTile); protected abstract void floatLoop(RasterAccessor src, RasterAccessor dst, RandomIter roiIter, boolean roiContainsTile); protected abstract void doubleLoop(RasterAccessor src, RasterAccessor dst, RandomIter roiIter, boolean roiContainsTile); public Raster computeTile(int tileX, int tileY) { if (!cobbleSources) { return super.computeTile(tileX, tileY); } // Special handling for Border Extender /* Create a new WritableRaster to represent this tile. */ Point org = new Point(tileXToX(tileX), tileYToY(tileY)); WritableRaster dest = createWritableRaster(sampleModel, org); /* Clip output rectangle to image bounds. */ Rectangle rect = new Rectangle(org.x, org.y, sampleModel.getWidth(), sampleModel.getHeight()); Rectangle destRect = rect.intersection(destBounds); if ((destRect.width <= 0) || (destRect.height <= 0)) { return dest; } /* account for padding in srcRectangle */ PlanarImage s = getSourceImage(0); // Fix 4639755: Area operations throw exception for // destination extending beyond source bounds // The default dest image area is the same as the source // image area. However, when an ImageLayout hint is set, // this might be not true. So the destRect should be the // intersection of the provided rectangle, the destination // bounds and the source bounds. destRect = destRect.intersection(s.getBounds()); Rectangle srcRect = new Rectangle(destRect); srcRect.x -= getLeftPadding(); srcRect.width += getLeftPadding() + getRightPadding(); srcRect.y -= getTopPadding(); srcRect.height += getTopPadding() + getBottomPadding(); /* * The tileWidth and tileHeight of the source image may differ from this tileWidth and tileHeight. */ IntegerSequence srcXSplits = new IntegerSequence(); IntegerSequence srcYSplits = new IntegerSequence(); // there is only one source for an AreaOpImage s.getSplits(srcXSplits, srcYSplits, srcRect); // Initialize new sequences of X splits. IntegerSequence xSplits = new IntegerSequence(destRect.x, destRect.x + destRect.width); xSplits.insert(destRect.x); xSplits.insert(destRect.x + destRect.width); srcXSplits.startEnumeration(); while (srcXSplits.hasMoreElements()) { int xsplit = srcXSplits.nextElement(); int lsplit = xsplit - getLeftPadding(); int rsplit = xsplit + getRightPadding(); xSplits.insert(lsplit); xSplits.insert(rsplit); } // Initialize new sequences of Y splits. IntegerSequence ySplits = new IntegerSequence(destRect.y, destRect.y + destRect.height); ySplits.insert(destRect.y); ySplits.insert(destRect.y + destRect.height); srcYSplits.startEnumeration(); while (srcYSplits.hasMoreElements()) { int ysplit = srcYSplits.nextElement(); int tsplit = ysplit - getBottomPadding(); int bsplit = ysplit + getTopPadding(); ySplits.insert(tsplit); ySplits.insert(bsplit); } /* * Divide destRect into sub rectangles based on the source splits, and compute each sub rectangle separately. */ int x1, x2, y1, y2; Raster[] sources = new Raster[1]; ySplits.startEnumeration(); for (y1 = ySplits.nextElement(); ySplits.hasMoreElements(); y1 = y2) { y2 = ySplits.nextElement(); int h = y2 - y1; int py1 = y1 - getTopPadding(); int py2 = y2 + getBottomPadding(); int ph = py2 - py1; xSplits.startEnumeration(); for (x1 = xSplits.nextElement(); xSplits.hasMoreElements(); x1 = x2) { x2 = xSplits.nextElement(); int w = x2 - x1; int px1 = x1 - getLeftPadding(); int px2 = x2 + getRightPadding(); int pw = px2 - px1; // Fetch the padded src rectangle Rectangle srcSubRect = new Rectangle(px1, py1, pw, ph); sources[0] = extender != null ? extendedIMG.getData(srcSubRect) : s .getData(srcSubRect); // Make a destRectangle Rectangle dstSubRect = new Rectangle(x1, y1, w, h); computeRect(sources, dest, dstSubRect); // Recycle the source tile if (s.overlapsMultipleTiles(srcSubRect)) { recycleTile(sources[0]); } } } return dest; } /** * This method provides a lazy initialization of the image associated to the ROI. The method uses the Double-checked locking in order to maintain * thread-safety * * @return */ protected PlanarImage getImage() { PlanarImage img = roiImage; if (img == null) { synchronized (this) { img = roiImage; if (img == null) { roiImage = img = roi.getAsImage(); } } } return img; } }