/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2003-2008, Open Source Geospatial Foundation (OSGeo)
*
* 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.geotools.image.jai;
// J2SE dependencies
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.image.ColorModel;
import java.awt.image.RasterFormatException;
import java.awt.image.WritableRaster;
import java.util.Vector;
// JAI and Vecmath dependencies
import javax.media.jai.ImageLayout;
import javax.media.jai.JAI;
import javax.media.jai.PlanarImage;
import javax.media.jai.PointOpImage;
import javax.media.jai.iterator.RectIter;
import javax.media.jai.iterator.RectIterFactory;
import javax.media.jai.iterator.WritableRectIter;
import javax.media.jai.operator.BandCombineDescriptor;
import javax.vecmath.MismatchedSizeException;
// Geotools dependencies
import org.geotools.resources.XArray;
import org.geotools.resources.image.ImageUtilities;
/**
* Computes a set of arbitrary linear combinations of the bands of many rendered source images,
* using a specified matrix. The matrix size ({@code numRows}×{@code numColumns}) must be
* equals to the following:
* <p>
* <ul>
* <li>{@code numRows}: the number of desired destination bands.</li>
* <li>{@code numColumns}: the total number of source bands (i.e. the
* sum of the number of source bands in all source images) plus one.</li>
* </ul>
* <p>
* The number of source bands used to determine the matrix dimensions is given by the
* following code regardless of the type of {@link ColorModel} the sources have:
*
* <blockquote><pre>
* int sourceBands = 0;
* for (int i=0; i<sources.length; i++) {
* sourceBands += sources[i].getSampleModel().getNumBands();
* }
* </blockquote></pre>
*
* The extra column in the matrix contains constant values each of which is added to the
* respective band of the destination. The transformation is therefore defined by the pseudocode:
*
* <blockquote><pre>
* // s = source pixel (not all from the same source image)
* // d = destination pixel
* for (int i=0; i<destBands; i++) {
* d[i] = matrix[i][sourceBands];
* for (int j=0; j<sourceBands; j++) {
* d[i] += matrix[i][j]*s[j];
* }
* }
* </blockquote></pre>
*
* In the special case where there is only one source, this method is equivalent to JAI's
* "{@link BandCombineDescriptor BandCombine}" operation.
*
* @since 2.1
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (IRD)
* @author Remi Eve
*/
public class Combine extends PointOpImage {
/**
* The linear combinaison coefficients as a matrix. This matrix may not be the same
* than the one specified to the constructor, in that the zero coefficients may have
* been purged (and {@link #sources} and {@link #bands} arrays adjusted accordingly})
* for performance reason.
*/
final double[][] matrix;
/**
* The source to use for each elements in {@link #matrix}.
* This matrix size must be the same than {@code matrix}.
*/
final int[][] sources;
/**
* The band to use for each elements in {@link #matrix}.
* This matrix size must be the same than {@code matrix}.
*/
final int[][] bands;
/**
* The number of source samples. This is the sum of the number of bands in
* all source images. Each {@link #matrix} row must have this length plus 1.
*/
final int numSamples;
/**
* The transform to apply on sample values before the linear combinaison,
* or {@code null} if none.
*/
protected final CombineTransform transform;
/**
* Construct an image with the specified matrix.
*
* @param images The rendered sources.
* @param matrix The linear combinaison coefficients as a matrix.
* @param transform The transform to apply on sample values before the linear combinaison,
* or {@code null} if none.
* @param hints The rendering hints.
*
* @throws MismatchedSizeException if some rows in the {@code matrix} argument doesn't
* have the expected length.
*/
public Combine(final Vector images,
double[][] matrix,
final CombineTransform transform,
final RenderingHints hints) throws MismatchedSizeException
{
super(images, ImageUtilities.createIntersection(
(ImageLayout)hints.get(JAI.KEY_IMAGE_LAYOUT), images), hints, false);
final int numRows = matrix.length;
this.matrix = matrix = (double[][]) matrix.clone();
this.sources = new int[numRows][];
this.bands = new int[numRows][];
this.transform = transform;
int numSamples = 0;
for (int i=getNumSources(); --i>=0;) {
numSamples += getSourceImage(i).getNumBands();
}
this.numSamples = numSamples;
final boolean isSeparable = (transform==null) || transform.isSeparable();
for (int j=0; j<numRows; j++) {
final double[] row = matrix[j];
final int numColumns = row.length;
if (numColumns != numSamples+1) {
throw new MismatchedSizeException();
}
int source = -1;
int band = -1;
int numBands = 0;
int count = 0; // Number of non-zero coefficients.
final double[] copy = new double[numColumns ];
final int[] sources = new int [numColumns - 1];
final int[] bands = new int [numColumns - 1];
final int numSources = sources.length;
for (int i=0; i<numSources; i++) {
if (++band >= numBands) {
band = 0;
numBands = getSourceImage(++source).getNumBands();
}
if (row[i]!=0 || !isSeparable) {
copy [count] = row[i];
sources[count] = source;
bands [count] = band;
count++;
}
}
copy[count] = row[row.length-1];
this.matrix [j] = XArray.resize(copy, count+1);
this.sources[j] = XArray.resize(sources, count );
this.bands [j] = XArray.resize(bands, count );
}
/*
* Set the sample model according the number of destination bands.
*/
if (getNumBands() != numRows) {
throw new UnsupportedOperationException(
"Automatic derivation of SampleModel not yet implemented.");
}
permitInPlaceOperation();
}
/**
* Compute one tile.
*
* @param images An array of PlanarImage sources.
* @param dest A WritableRaster to be filled in.
* @param destRect The Rectangle within the destination to be written.
*/
public void computeRect(final PlanarImage[] images,
final WritableRaster dest,
final Rectangle destRect)
{
/*
* Create the iterators. The 'iterRef' array will contains a copy of 'iters' where
* the reference to an iterator is duplicated for each band in the source image:
*
* iterRef[i] = iters[sources[band][i]]
*/
final RectIter[] iters = new RectIter[images.length];
final RectIter[] iterRef = new RectIter[numSamples];
double[] samples = null;
final int length = iters.length;
for (int i=0; i<length; i++) {
iters[i] = RectIterFactory.create(images[i], mapDestRect(destRect, i));
}
final WritableRectIter iTarget = RectIterFactory.createWritable(dest, destRect);
/*
* Iterates over all destination bands. In many case, the destination image
* will have only one band. Consequently, it is more efficient to iterates
* through bands in the outer loop rather than the inner loop.
*/
int band = 0;
iTarget.startBands();
boolean finished = iTarget.finishedBands();
while (!finished) {
final double[] row = this.matrix [band];
final int[] bands = this.bands [band];
final int[] sources = this.sources[band];
final int numSamples = sources.length;
if (numSamples>this.numSamples || numSamples>bands.length || numSamples>=row.length) {
// Should not happen if the constructor checks was right. We performs this
// check unconditionnaly since it is cheap, and in the hope to help the JIT
// to remove some array bound checks later in inner loops.
throw new AssertionError(numSamples);
}
for (int i=0; i<numSamples; i++) {
iterRef[i] = iters[sources[i]];
}
if (samples==null || samples.length!=numSamples) {
samples = new double[numSamples];
}
/*
* Iterates over all lines, then over all pixels. The 'finished' flag is reset
* to 'nextXXXDone()' at the end of each loop.
*/
iTarget.startLines();
finished = iTarget.finishedLines();
for (int i=0; i<iters.length; i++) {
iters[i].startLines();
if (iters[i].finishedLines() != finished) {
// Should not happen, since constructor computed
// the intersection of all source images.
throw new RasterFormatException("Missing lines");
}
}
while (!finished) {
iTarget.startPixels();
finished = iTarget.finishedPixels();
for (int i=0; i<iters.length; i++) {
iters[i].startPixels();
if (iters[i].finishedPixels() != finished) {
// Should not happen, since constructor computed
// the intersection of all source images.
throw new RasterFormatException("Missing pixels");
}
}
while (!finished) {
/*
* Computes the sample values.
*/
for (int i=0; i<numSamples; i++) {
samples[i] = iterRef[i].getSampleDouble(bands[i]);
}
if (transform != null) {
transform.transformSamples(samples);
}
double value = row[numSamples];
for (int i=0; i<numSamples; i++) {
value += row[i] * samples[i];
}
iTarget.setSample(value);
finished = iTarget.nextPixelDone();
for (int i=0; i<iters.length; i++) {
if (iters[i].nextPixelDone() != finished) {
// Should not happen, since constructor computed
// the intersection of all source images.
throw new RasterFormatException("Missing pixels");
}
}
}
finished = iTarget.nextLineDone();
for (int i=0; i<iters.length; i++) {
if (iters[i].nextLineDone() != finished) {
// Should not happen, since constructor computed
// the intersection of all source images.
throw new RasterFormatException("Missing lines");
}
}
}
band++;
finished = iTarget.nextBandDone();
}
}
/**
* Optimized {@code Combine} operation for dyadic (two sources) image. This operation
* performs a linear combinaison of two images ({@code src0} and {@code src1}).
* The parameters {@code scale0} and {@code scale1} indicate the scale of source
* images {@code src0} and {@code src1}. If we consider pixel at coordinate
* (<var>x</var>,<var>y</var>), its value is determinate by the pseudo-code:
*
* <blockquote><pre>
* value = src0[x][y]*scale0 + src1[x][y]*scale1 + offset
* </pre></blockquote>
*
* @version $Id$
* @author Remi Eve
* @author Martin Desruisseaux (IRD)
*/
final static class Dyadic extends Combine {
/**
* The scale of image {@code src0} for each bands.
*/
private final double[] scales0;
/**
* The scale of image {@code src1} for each bands.
*/
private final double[] scales1;
/**
* The offset for each bands.
*/
private final double[] offsets;
/**
* Construct a new instance of {@code Combine.Dyadic}.
*
* @param images The rendered sources. This vector must contains exactly 2 sources.
* @param matrix The linear combinaison coefficients as a matrix.
* @param hints The rendering hints.
*
* @throws MismatchedSizeException if some rows in the {@code matrix} argument doesn't
* have the expected length.
*/
public Dyadic(final Vector images,
final double[][] matrix,
final RenderingHints hints) throws MismatchedSizeException
{
super(images, matrix, null, hints);
if (getNumSources() != 2) {
throw new IllegalArgumentException();
}
final int numBands = getNumBands();
scales0 = new double[numBands];
scales1 = new double[numBands];
offsets = new double[numBands];
for (int j=0; j<numBands; j++) {
final double[] row = this.matrix [j];
final int[] sources = this.sources[j];
final int[] bands = this.bands [j];
for (int i=0; i<sources.length; i++) {
final double coeff = row[i];
final int band = bands[i];
final int source = sources[i];
if (band != j) {
throw new AssertionError(band); // Should not happen.
}
switch (source) {
case 0: scales0[band] = coeff; break;
case 1: scales1[band] = coeff; break;
default: throw new AssertionError(source); // Should not happen.
}
}
offsets[j] = row[sources.length];
}
}
/**
* Computes one tile.
*
* @param images An array of PlanarImage sources.
* @param dest A WritableRaster to be filled in.
* @param destRect The Rectangle within the destination to be written.
*/
public void computeRect(final PlanarImage[] images,
final WritableRaster dest,
final Rectangle destRect)
{
final RectIter iSrc0 = RectIterFactory.create(images[0], mapDestRect(destRect, 0));
final RectIter iSrc1 = RectIterFactory.create(images[1], mapDestRect(destRect, 1));
final WritableRectIter iTarget = RectIterFactory.createWritable(dest, destRect);
int band = 0;
iSrc0 .startBands();
iSrc1 .startBands();
iTarget.startBands();
if (!iTarget.finishedBands() &&
!iSrc0 .finishedBands() &&
!iSrc1 .finishedBands())
{
final double scale0 = scales0[Math.min(band, scales0.length-1)];
final double scale1 = scales1[Math.min(band, scales1.length-1)];
final double offset = offsets[Math.min(band, offsets.length-1)];
do {
iSrc0 .startLines();
iSrc1 .startLines();
iTarget.startLines();
if (!iTarget.finishedLines() &&
!iSrc0 .finishedLines() &&
!iSrc1 .finishedLines())
{
do {
iSrc0 .startPixels();
iSrc1 .startPixels();
iTarget.startPixels();
if (!iTarget.finishedPixels() &&
!iSrc0 .finishedPixels() &&
!iSrc1 .finishedPixels())
{
do {
iTarget.setSample(iSrc0.getSampleDouble() * scale0 +
iSrc1.getSampleDouble() * scale1 + offset);
} while (!iSrc0.nextPixelDone() &&
!iSrc1.nextPixelDone() &&
!iTarget.nextPixelDone());
}
} while (!iSrc0.nextLineDone() &&
!iSrc1.nextLineDone() &&
!iTarget.nextLineDone());
}
band++;
} while (!iSrc0.nextBandDone() &&
!iSrc1.nextBandDone() &&
!iTarget.nextBandDone());
}
}
}
}