/* 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.piecewise;
import it.geosolutions.jaiext.iterators.RandomIterFactory;
import it.geosolutions.jaiext.range.Range;
import it.geosolutions.jaiext.range.RangeFactory;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.image.DataBuffer;
import java.awt.image.Raster;
import java.awt.image.RasterFormatException;
import java.awt.image.RenderedImage;
import java.awt.image.WritableRaster;
import java.util.Arrays;
import javax.media.jai.ColormapOpImage;
import javax.media.jai.ImageLayout;
import javax.media.jai.PlanarImage;
import javax.media.jai.ROI;
import javax.media.jai.ROIShape;
import javax.media.jai.iterator.RandomIter;
import javax.media.jai.iterator.RectIter;
import javax.media.jai.iterator.RectIterFactory;
import javax.media.jai.iterator.WritableRectIter;
import com.sun.media.jai.util.ImageUtil;
/**
* Images are created using the {@code GenericPiecewise.CRIF} inner class, where "CRIF" stands for
* {@link java.awt.image.renderable.ContextualRenderedImageFactory} . The image operation name is "GenericPiecewise".
*
* @author Simone Giannecchini - GeoSolutions
*/
public class GenericPiecewiseOpImage<T extends PiecewiseTransform1DElement> extends ColormapOpImage {
/**
* The operation name.
*/
public static final String OPERATION_NAME = "GenericPiecewise";
/** 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;
/**
* DefaultPiecewiseTransform1D that we'll use to transform this image. We'll apply it ato all of its bands.
*/
private final PiecewiseTransform1D<T> piecewise;
/** Boolean indicating if the output samplemodel has a Byte dataType */
protected boolean isByteData;
/** Byte LookupTable used for quickly calculating piecewise operation for Byte data */
private byte[][] lut;
/** Output NoData */
private double gapsValue = Double.NaN;
/** Boolean indicating if output nodata has been defined */
private boolean hasGapsValue = false;
/** Boolean indicating that the Last DefaultDomain can be used */
private final boolean useLast;
/** Boolean indicating that NoData Range is present */
private final boolean hasNoData;
/** Boolean indicating that ROI is present */
private final boolean hasROI;
/** NoData Range used for checking input NoData */
private Range nodata;
/** ROI object used for reducing calculations */
private ROI roi;
/** Rectangle containing ROI bounds */
private Rectangle roiBounds;
/** {@link PlanarImage} containing ROI data */
private PlanarImage roiImage;
/** Output NoData for Bytes */
private byte gapsValueByte;
/** Boolean indicating that there No Data and ROI are not used */
private final boolean caseA;
/** Boolean indicating that only ROI is used */
private final boolean caseB;
/** Boolean indicating that only No Data are used */
private final boolean caseC;
/** Optional value used for indicating that the calculations are made only on one band */
private Integer bandIndex;
/**
* Constructs a new {@code RasterClassifier}.
*
* @param image The source image.
* @param lic The DefaultPiecewiseTransform1D.
* @param bandIndex index used for defining the band to calculate
* @param roi {@link ROI} used for reducing computation area
* @param nodata {@link Range} used for defining NoData values
* @param hints The rendering hints.
*/
public GenericPiecewiseOpImage(final RenderedImage image, final PiecewiseTransform1D<T> lic,
ImageLayout layout, Integer bandIndex, ROI roi, Range nodata,
final RenderingHints hints, boolean cobbleSources) {
super(image, layout, hints, cobbleSources);
this.piecewise = lic;
// Ensure that the number of sets of breakpoints is either unity
// or equal to the number of bands.
final int numBands = sampleModel.getNumBands();
// Check the bandIndex value
if (bandIndex != null) {
this.bandIndex = bandIndex;
}
// Set the byte data flag.
isByteData = sampleModel.getTransferType() == DataBuffer.TYPE_BYTE;
// ////////////////////////////////////////////////////////////////////
//
// Check if we can make good use of a default piecewise element for filling gaps
// in the input range
//
// ////////////////////////////////////////////////////////////////////
if (this.piecewise.hasDefaultValue()) {
gapsValue = piecewise.getDefaultValue();
hasGapsValue = true;
gapsValueByte = ImageUtil.clampRoundByte(gapsValue);
}
// Handling NoData
hasNoData = nodata != null;
if (hasNoData) {
this.nodata = RangeFactory.convertToDoubleRange(nodata);
}
// Handling ROI (Notice that ROI is not considered when the ColorModel is IndexColorModel, source
// image must have a component colormodel)
hasROI = roi != null;
if (hasROI) {
this.roi = roi;
roiBounds = roi.getBounds();
}
// 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 (!caseA && !hasGapsValue) {
throw new IllegalArgumentException(
"Unable to set input Gap valuein presence of NoData and ROI");
}
// ////////////////////////////////////////////////////////////////////
//
// Check if we can optimize this operation by reusing the last used
// piecewise element first. The speed up we get can be substantial since we avoid
// an explicit search in the piecewise element list for the fitting piecewise element given
// a certain sample value.
//
//
// ////////////////////////////////////////////////////////////////////
useLast = piecewise instanceof DefaultDomain1D;
// Perform byte-specific initialization.
if (isByteData) {
// Initialize the lookup table.
try {
createLUT(numBands);
} catch (final TransformationException e) {
final RuntimeException re = new RuntimeException(e);
throw re;
}
}
// Set flag to permit in-place operation.
permitInPlaceOperation();
// Initialize the colormap if necessary.
initializeColormapOperation();
}
@Override
protected void computeRect(Raster[] sources, WritableRaster dest, Rectangle destRect) {
// ROI Parameters initialization
ROI roiTile = null;
RandomIter roiIter = null;
boolean roiContainsTile = false;
boolean roiDisjointTile = false;
// If a ROI is present, then only the part contained inside the current tile bounds is taken.
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);
}
}
}
}
// Check on the num bands
if (sources[0].getNumBands() != dest.getNumBands()) {
throw new IllegalArgumentException(
"Sourc and Destination image must have the same Bands");
}
// Creating the RasterAccessors and testing
if (!hasROI || !roiDisjointTile) {
if (isByteData) {
computeRectByte(sources[0], dest, destRect, roiIter, roiContainsTile);
} else {
computeRectGeneral(sources[0], dest, destRect, roiIter, roiContainsTile);
}
} else {
// //
//
// if the tile is outside ROI
// we use domain nodata for filling the tile bounds
//
// //
double[] background = new double[dest.getSampleModel().getNumBands()];
Arrays.fill(background, gapsValue);
ImageUtil.fillBackground(dest, destRect, background);
}
}
private void computeRectByte(final Raster source, final WritableRaster dest,
final Rectangle destRect, RandomIter roiIter, boolean roiContainsTile) {
int srcX = destRect.x;
int srcY = destRect.y;
// Input position parameters
int x0 = srcX;
int y0 = srcY;
WritableRectIter dstIter = RectIterFactory.createWritable(dest, destRect);
RectIter srcIter = RectIterFactory.create(source, destRect);
// ////////////////////////////////////////////////////////////////////
//
// Prepare the iterator to work on the correct bands, if this is
// requested.
//
// ////////////////////////////////////////////////////////////////////
if (!dstIter.finishedBands() && !srcIter.finishedBands()) {
for (int i = 0; i < bandIndex; i++) {
dstIter.nextBand();
srcIter.nextBand();
}
}
if (hasROI) {
int bandNumber = 0;
do {
try {
dstIter.startLines();
srcIter.startLines();
if (!dstIter.finishedLines() && !srcIter.finishedLines())
do {
dstIter.startPixels();
srcIter.startPixels();
if (!dstIter.finishedPixels() && !srcIter.finishedPixels())
do {
// //
//
// get the input value to be transformed
//
// //
final int in = srcIter.getSample() & 0xff;
if (!(roiBounds.contains(x0, y0) && roiIter
.getSample(x0, y0, 0) > 0)) {
dstIter.setSample(gapsValue);
} else {
// //
//
// Transform this element
//
// //
final int out = 0xff & lut[bandNumber][in];
// //
//
// Set the result
//
// //
dstIter.setSample(out);
}
x0++;
} while (!dstIter.nextPixelDone() && !srcIter.nextPixelDone());
y0++;
x0 = srcX;
} while (!dstIter.nextLineDone() && !srcIter.nextLineDone());
} catch (final Exception cause) {
final RasterFormatException exception = new RasterFormatException(
cause.getLocalizedMessage());
exception.initCause(cause);
throw exception;
}
bandNumber++;
y0 = srcY;
x0 = srcX;
if (bandIndex != -1)
break;
} while (dstIter.finishedBands() && srcIter.finishedBands());
} else {
int bandNumber = 0;
do {
try {
dstIter.startLines();
srcIter.startLines();
if (!dstIter.finishedLines() && !srcIter.finishedLines())
do {
dstIter.startPixels();
srcIter.startPixels();
if (!dstIter.finishedPixels() && !srcIter.finishedPixels())
do {
// //
//
// get the input value to be transformed
//
// //
final int in = srcIter.getSample() & 0xff;
// //
//
// Transform this element
//
// //
final int out = 0xff & lut[bandNumber][in];
// //
//
// Set the result
//
// //
dstIter.setSample(out);
} while (!dstIter.nextPixelDone() && !srcIter.nextPixelDone());
} while (!dstIter.nextLineDone() && !srcIter.nextLineDone());
} catch (final Exception cause) {
final RasterFormatException exception = new RasterFormatException(
cause.getLocalizedMessage());
exception.initCause(cause);
throw exception;
}
bandNumber++;
if (bandIndex != -1)
break;
} while (dstIter.finishedBands() && srcIter.finishedBands());
}
}
private void computeRectGeneral(final Raster source, final WritableRaster dest,
final Rectangle destRect, RandomIter roiIter, boolean roiContainsTile) {
int srcX = destRect.x;
int srcY = destRect.y;
// Input position parameters
int x0 = srcX;
int y0 = srcY;
PiecewiseTransform1DElement element = null;
WritableRectIter dstIter = RectIterFactory.createWritable(dest, destRect);
RectIter srcIter = RectIterFactory.create(source, destRect);
if (!dstIter.finishedBands() && !srcIter.finishedBands()) {
for (int i = 0; i < bandIndex; i++) {
dstIter.nextBand();
srcIter.nextBand();
}
}
if (caseA || (caseB && roiContainsTile)) {
do {
try {
dstIter.startLines();
srcIter.startLines();
if (!dstIter.finishedLines() && !srcIter.finishedLines())
do {
dstIter.startPixels();
srcIter.startPixels();
if (!dstIter.finishedPixels() && !srcIter.finishedPixels())
do {
// //
//
// get the input value to be transformed
//
// //
final double value = srcIter.getSampleDouble();
// //
//
// get the correct piecewise element for this
// transformation
//
// //
element = domainSearch(element, value);
// //
//
// in case everything went fine let's apply the
// transform.
//
// //
if (element != null)
dstIter.setSample(element.transform(value));
else {
// //
//
// if we did not find one let's try to use
// one of the nodata ones to fill the gaps,
// if we are allowed to (see above).
//
// //
if (hasGapsValue)
dstIter.setSample(gapsValue);
else
// //
//
// if we did not find one let's throw a
// nice error message
//
// //
throw new IllegalArgumentException(
"Unable to set input Gap value");
}
} while (!dstIter.nextPixelDone() && !srcIter.nextPixelDone());
} while (!dstIter.nextLineDone() && !srcIter.nextLineDone());
} catch (final Exception cause) {
final RasterFormatException exception = new RasterFormatException(
cause.getLocalizedMessage());
exception.initCause(cause);
throw exception;
}
if (bandIndex != -1)
break;
} while (dstIter.finishedBands() && srcIter.finishedBands());
} else if (caseB) {
do {
try {
dstIter.startLines();
srcIter.startLines();
if (!dstIter.finishedLines() && !srcIter.finishedLines())
do {
dstIter.startPixels();
srcIter.startPixels();
if (!dstIter.finishedPixels() && !srcIter.finishedPixels())
do {
if (!(roiBounds.contains(x0, y0) && roiIter
.getSample(x0, y0, 0) > 0)) {
dstIter.setSample(gapsValue);
} else {
final double value = srcIter.getSampleDouble();
element = domainSearch(element, value);
if (element != null)
dstIter.setSample(element.transform(value));
else {
if (hasGapsValue)
dstIter.setSample(gapsValue);
else
throw new IllegalArgumentException(
"Unable to set input Gap value");
}
}
x0++;
} while (!dstIter.nextPixelDone() && !srcIter.nextPixelDone());
y0++;
x0 = srcX;
} while (!dstIter.nextLineDone() && !srcIter.nextLineDone());
} catch (final Exception cause) {
final RasterFormatException exception = new RasterFormatException(
cause.getLocalizedMessage());
exception.initCause(cause);
throw exception;
}
y0 = srcY;
x0 = srcX;
if (bandIndex != -1)
break;
} while (dstIter.finishedBands() && srcIter.finishedBands());
} else if (caseC || (hasROI && hasNoData && roiContainsTile)) {
do {
try {
dstIter.startLines();
srcIter.startLines();
if (!dstIter.finishedLines() && !srcIter.finishedLines())
do {
dstIter.startPixels();
srcIter.startPixels();
if (!dstIter.finishedPixels() && !srcIter.finishedPixels())
do {
final double value = srcIter.getSampleDouble();
if (nodata.contains(value)) {
dstIter.setSample(gapsValue);
} else {
element = domainSearch(element, value);
if (element != null)
dstIter.setSample(element.transform(value));
else {
if (hasGapsValue)
dstIter.setSample(gapsValue);
else
throw new IllegalArgumentException(
"Unable to set input Gap value");
}
}
} while (!dstIter.nextPixelDone() && !srcIter.nextPixelDone());
} while (!dstIter.nextLineDone() && !srcIter.nextLineDone());
} catch (final Exception cause) {
final RasterFormatException exception = new RasterFormatException(
cause.getLocalizedMessage());
exception.initCause(cause);
throw exception;
}
if (bandIndex != -1)
break;
} while (dstIter.finishedBands() && srcIter.finishedBands());
} else {
do {
try {
dstIter.startLines();
srcIter.startLines();
if (!dstIter.finishedLines() && !srcIter.finishedLines())
do {
dstIter.startPixels();
srcIter.startPixels();
if (!dstIter.finishedPixels() && !srcIter.finishedPixels())
do {
if (!(roiBounds.contains(x0, y0) && roiIter
.getSample(x0, y0, 0) > 0)) {
dstIter.setSample(gapsValue);
} else {
final double value = srcIter.getSampleDouble();
if (nodata.contains(value)) {
dstIter.setSample(gapsValue);
} else {
element = domainSearch(element, value);
if (element != null)
dstIter.setSample(element.transform(value));
else {
if (hasGapsValue)
dstIter.setSample(gapsValue);
else
throw new IllegalArgumentException(
"Unable to set input Gap value");
}
}
}
x0++;
} while (!dstIter.nextPixelDone() && !srcIter.nextPixelDone());
y0++;
x0 = srcX;
} while (!dstIter.nextLineDone() && !srcIter.nextLineDone());
} catch (final Exception cause) {
final RasterFormatException exception = new RasterFormatException(
cause.getLocalizedMessage());
exception.initCause(cause);
throw exception;
}
y0 = srcY;
x0 = srcX;
if (bandIndex != -1)
break;
} while (dstIter.finishedBands() && srcIter.finishedBands());
}
}
private PiecewiseTransform1DElement domainSearch(PiecewiseTransform1DElement last, double value)
throws TransformationException {
// //
//
// get the correct piecewise element for this
// transformation
//
// //
final PiecewiseTransform1DElement transformElement;
if (useLast) {
if (last != null && last.contains(value))
transformElement = last;
else {
last = transformElement = (PiecewiseTransform1DElement) piecewise
.findDomainElement(value);
}
} else
transformElement = (PiecewiseTransform1DElement) piecewise.findDomainElement(value);
return transformElement;
}
/**
* Create a lookup table to be used in the case of byte data.
*
* @param numBands
* @throws TransformationException
*/
private void createLUT(final int numBands) throws TransformationException {
// Allocate memory for the data array references.
final byte[][] data = new byte[numBands][];
// Generate the data for each band.
for (int band = 0; band < numBands; band++) {
// Allocate memory for this band.
data[band] = new byte[256];
// Cache the references to avoid extra indexing.
final byte[] table = data[band];
// Initialize the lookup table data.
PiecewiseTransform1DElement lastPiecewiseElement = null;
for (int value = 0; value < 256; value++) {
boolean isNoData = hasNoData && nodata.contains((byte) value);
if (isNoData) {
// //
//
// if we did find a nodata let's try to use
// one of the destination nodata to fill the gaps,
//
// //
table[value] = gapsValueByte;
continue;
}
// //
//
// get the correct piecewise element for this
// transformation
//
// //
final PiecewiseTransform1DElement piecewiseElement;
if (useLast) {
if (lastPiecewiseElement != null && lastPiecewiseElement.contains(value))
piecewiseElement = lastPiecewiseElement;
else {
lastPiecewiseElement = piecewiseElement = (PiecewiseTransform1DElement) piecewise
.findDomainElement(value);
}
} else
piecewiseElement = (PiecewiseTransform1DElement) piecewise
.findDomainElement(value);
// //
//
// in case everything went fine let's apply the
// transform.
//
// //
if (piecewiseElement != null)
table[value] = ImageUtil.clampRoundByte(piecewiseElement.transform(value));
else {
// //
//
// if we did not find one let's try to use
// one of the nodata ones to fill the gaps,
// if we are allowed to (see above).
//
// //
if (hasGapsValue)
table[value] = ImageUtil.clampRoundByte(gapsValue);
else
// //
//
// if we did not find one let's throw a
// nice error message
//
// //
throw new IllegalArgumentException("Unable to set the Gap value");
}
}
}
// Construct the lookup table.
lut = data;
}
/**
* Transform the colormap according to the rescaling parameters.
*/
protected void transformColormap(final byte[][] colormap) {
for (int b = 0; b < 3; b++) {
final byte[] map = colormap[b];
final byte[] luTable = lut[b >= lut.length ? 0 : b];
final int mapSize = map.length;
for (int i = 0; i < mapSize; i++) {
map[i] = luTable[(map[i] & 0xFF)];
}
}
}
/**
* 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
*/
private PlanarImage getImage() {
PlanarImage img = roiImage;
if (img == null) {
synchronized (this) {
img = roiImage;
if (img == null) {
roiImage = img = roi.getAsImage();
}
}
}
return img;
}
}