/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2001-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.coverage.grid; import java.awt.Rectangle; import java.awt.geom.Point2D; import java.util.ArrayList; import java.util.List; import javax.media.jai.BorderExtender; import javax.media.jai.BorderExtenderCopy; import javax.media.jai.Interpolation; import javax.media.jai.InterpolationNearest; import javax.media.jai.iterator.RectIter; import javax.media.jai.iterator.RectIterFactory; import org.opengis.coverage.CannotEvaluateException; import org.opengis.coverage.PointOutsideCoverageException; import org.opengis.metadata.spatial.PixelOrientation; import org.opengis.referencing.operation.MathTransform2D; import org.opengis.referencing.operation.NoninvertibleTransformException; import org.opengis.referencing.operation.TransformException; /** * A grid coverage using an {@linkplain Interpolation interpolation} for evaluating points. This * interpolator is not used for {@linkplain InterpolationNearest nearest-neighbor interpolation} * (use the plain {@link GridCoverage2D} class for that). It should work for other kinds of * interpolation however. * * @since 2.2 * * @source $URL$ * @version $Id$ * @author Martin Desruisseaux (IRD) */ public final class Interpolator2D extends Calculator2D { /** * For cross-version compatibility. */ private static final long serialVersionUID = 9028980295030908004L; /** * The greatest value smaller than 1 representable as a {@code float} number. * This value can be obtained with {@code org.geotools.resources.XMath.previous(1f)}. */ private static final float ONE_EPSILON = 0.99999994f; /** * Default interpolations, in preference order. Will be constructed only when first needed. */ private static Interpolation[] DEFAULTS; /** * Transform from "real world" coordinates to grid coordinates. * This transform maps coordinates to pixel <em>centers</em>. */ private final MathTransform2D toGrid; /** * The interpolation method. */ private final Interpolation interpolation; /** * Second interpolation method to use if this one failed. May be {@code null} if there * is no fallback. By convention, {@code this} means that interpolation should fallback * on {@code super.evaluate(...)} (i.e. nearest neighbor). */ private final Interpolator2D fallback; /** * Image bounds. Bounds have been reduced by {@link Interpolation}'s padding. */ private final int xmin, ymin, xmax, ymax; /** * Interpolation padding. */ private final int top, left; /** * The interpolation bounds. Interpolation will use pixel inside this rectangle. * This rectangle is passed as an argument to {@link RectIterFactory}. */ private final Rectangle bounds; /** * Arrays to use for passing arguments to interpolation. * This array will be constructed only when first needed. */ private transient double[][] doubles; /** * Arrays to use for passing arguments to interpolation. * This array will be constructed only when first needed. */ private transient float[][] floats; /** * Arrays to use for passing arguments to interpolation. * This array will be constructed only when first needed. */ private transient int[][] ints; /** The {@link BorderExtender} for this {@link Interpolator2D} instance .*/ private final BorderExtender borderExtender; /** * Default {@link BorderExtender} is {@link BorderExtenderCopy}. */ public static int DEFAULT_BORDER_EXTENDER_TYPE=BorderExtender.BORDER_COPY; /** * Constructs a new interpolator using default interpolations. * * @param coverage The coverage to interpolate. */ public static GridCoverage2D create(final GridCoverage2D coverage) { // No need to synchronize: not a big deal if two arrays are created. if (DEFAULTS == null) { DEFAULTS = new Interpolation[] { Interpolation.getInstance(Interpolation.INTERP_BICUBIC), Interpolation.getInstance(Interpolation.INTERP_BILINEAR), Interpolation.getInstance(Interpolation.INTERP_NEAREST) }; } return create(coverage, DEFAULTS); } /** * Constructs a new interpolator for a single interpolation. * * @param coverage The coverage to interpolate. * @param interpolation The interpolation to use. */ public static GridCoverage2D create(final GridCoverage2D coverage, final Interpolation interpolation) { return create(coverage, new Interpolation[] {interpolation}); } /** * Constructs a new interpolator for an interpolation and its fallbacks. The fallbacks * are used if the primary interpolation failed because of {@linkplain Float#NaN NaN} * values in the interpolated point neighbor. * * @param coverage The coverage to interpolate. * @param interpolations The interpolation to use and its fallback (if any). */ public static GridCoverage2D create(GridCoverage2D coverage, final Interpolation[] interpolations) { return create(coverage, interpolations, null); } /** * Constructs a new interpolator for an interpolation and its fallbacks. The fallbacks * are used if the primary interpolation failed because of {@linkplain Float#NaN NaN} * values in the interpolated point neighbor. * * @param coverage The coverage to interpolate. * @param interpolations The interpolation to use and its fallback (if any). */ public static GridCoverage2D create(GridCoverage2D coverage, final Interpolation[] interpolations, final BorderExtender be) { while (coverage instanceof Calculator2D) { coverage = ((Calculator2D) coverage).source; } if (interpolations.length==0 || (interpolations[0] instanceof InterpolationNearest)) { return coverage; } return new Interpolator2D(coverage, interpolations, 0,be); } /** * Constructs a new interpolator for the specified interpolation. * * @param coverage The coverage to interpolate. * @param interpolations The interpolations to use and its fallback * (if any). This array must have at least 1 element. * @param index The index of interpolation to use in the {@code interpolations} array. * @param be the {@link BorderExtender} instance to use for this operation. */ private Interpolator2D(final GridCoverage2D coverage, final Interpolation[] interpolations, final int index, BorderExtender be) { super(null, coverage); this.interpolation = interpolations[index]; //border extender if(be== null){ this.borderExtender= BorderExtender.createInstance(DEFAULT_BORDER_EXTENDER_TYPE); } else this.borderExtender=be; if (index+1 < interpolations.length) { if (interpolations[index+1] instanceof InterpolationNearest) { // By convention, 'fallback==this' is for 'super.evaluate(...)' // (i.e. "NearestNeighbor"). this.fallback = this; } else { this.fallback = new Interpolator2D(coverage, interpolations, index+1,be); } } else { this.fallback = null; } /* * Computes the affine transform from "real world" coordinates to grid coordinates. * This transform maps coordinates to pixel <em>centers</em>. If this transform has * already be created during fallback construction, reuse the fallback's instance * instead of creating a new identical one. */ if (fallback!=null && fallback!=this) { this.toGrid = fallback.toGrid; } else try { final MathTransform2D transform = gridGeometry.getGridToCRS2D(PixelOrientation.UPPER_LEFT); toGrid = transform.inverse(); } catch (NoninvertibleTransformException exception) { throw new IllegalArgumentException(exception); } final int left = interpolation.getLeftPadding(); final int top = interpolation.getTopPadding(); this.top = top; this.left = left; final int x = image.getMinX(); final int y = image.getMinY(); this.xmin = x + left; this.ymin = y + top; this.xmax = x + image.getWidth(); this.ymax = y + image.getHeight(); bounds = new Rectangle(0, 0, interpolation.getWidth(), interpolation.getHeight()); } /** * Invoked by <code>{@linkplain #view view}(type)</code> when the {@linkplain ViewType#PACKED * packed}, {@linkplain ViewType#GEOPHYSICS geophysics} or {@linkplain ViewType#PHOTOGRAPHIC * photographic} view of this grid coverage needs to be created. This method applies to the * new grid coverage the same {@linkplain #getInterpolations interpolations} than this grid * coverage. * * @param view A view derived from the {@linkplain #source source} coverage. * @return The grid coverage to be returned by {@link #view view}. * * @since 2.5 */ @Override protected GridCoverage2D specialize(final GridCoverage2D view) { return create(view, getInterpolations()); } /** * Returns the class of the view returned by {@link #specialize}. */ @Override Class<Interpolator2D> getViewClass() { return Interpolator2D.class; } /** * Returns interpolations. The first array's element is the interpolation for * this grid coverage. Other elements (if any) are fallbacks. */ public Interpolation[] getInterpolations() { final List<Interpolation> interp = new ArrayList<Interpolation>(4); Interpolator2D scan = this; do { interp.add(interpolation); if (scan.fallback == scan) { interp.add(Interpolation.getInstance(Interpolation.INTERP_NEAREST)); break; } scan = scan.fallback; } while (scan != null); return interp.toArray(new Interpolation[interp.size()]); } /** * Returns the primary interpolation used by this {@code Interpolator2D}. */ @Override public Interpolation getInterpolation() { return interpolation; } /** * Returns a sequence of integer values for a given two-dimensional point in the coverage. * * @param coord The coordinate point where to evaluate. * @param dest An array in which to store values, or {@code null}. * @return An array containing values. * @throws CannotEvaluateException if the values can't be computed at the specified coordinate. * More specifically, {@link PointOutsideCoverageException} is thrown if the evaluation * failed because the input point has invalid coordinates. */ @Override public int[] evaluate(final Point2D coord, int[] dest) throws CannotEvaluateException { if (fallback != null) { dest = super.evaluate(coord, dest); } try { final Point2D pixel = toGrid.transform(coord, null); final double x = pixel.getX(); final double y = pixel.getY(); if (!Double.isNaN(x) && !Double.isNaN(y)) { dest = interpolate(x, y, dest, 0, image.getNumBands()); if (dest != null) { return dest; } } } catch (TransformException exception) { throw new CannotEvaluateException(formatEvaluateError(coord, false), exception); } throw new PointOutsideCoverageException(formatEvaluateError(coord, true)); } /** * Returns a sequence of float values for a given two-dimensional point in the coverage. * * @param coord The coordinate point where to evaluate. * @param dest An array in which to store values, or {@code null}. * @return An array containing values. * @throws CannotEvaluateException if the values can't be computed at the specified coordinate. * More specifically, {@link PointOutsideCoverageException} is thrown if the evaluation * failed because the input point has invalid coordinates. */ @Override public float[] evaluate(final Point2D coord, float[] dest) throws CannotEvaluateException { if (fallback != null) { dest = super.evaluate(coord, dest); } try { final Point2D pixel = toGrid.transform(coord, null); final double x = pixel.getX(); final double y = pixel.getY(); if (!Double.isNaN(x) && !Double.isNaN(y)) { dest = interpolate(x, y, dest, 0, image.getNumBands()); if (dest != null) { return dest; } } } catch (TransformException exception) { throw new CannotEvaluateException(formatEvaluateError(coord, false), exception); } throw new PointOutsideCoverageException(formatEvaluateError(coord, true)); } /** * Returns a sequence of double values for a given two-dimensional point in the coverage. * * @param coord The coordinate point where to evaluate. * @param dest An array in which to store values, or {@code null}. * @return An array containing values. * @throws CannotEvaluateException if the values can't be computed at the specified coordinate. * More specifically, {@link PointOutsideCoverageException} is thrown if the evaluation * failed because the input point has invalid coordinates. */ @Override public double[] evaluate(final Point2D coord, double[] dest) throws CannotEvaluateException { if (fallback != null) { dest = super.evaluate(coord, dest); } try { final Point2D pixel = toGrid.transform(coord, null); final double x = pixel.getX(); final double y = pixel.getY(); if (!Double.isNaN(x) && !Double.isNaN(y)) { dest = interpolate(x, y, dest, 0, image.getNumBands()); if (dest != null) { return dest; } } } catch (TransformException exception) { throw new CannotEvaluateException(formatEvaluateError(coord, false), exception); } throw new PointOutsideCoverageException(formatEvaluateError(coord, true)); } /** * Interpolate at the specified position. If {@code fallback!=null}, * then {@code dest} <strong>must</strong> have been initialized with * {@code super.evaluate(...)} prior to invoking this method. * * @param x The x position in pixel's coordinates. * @param y The y position in pixel's coordinates. * @param dest The destination array, or null. * @param band The first band's index to interpolate. * @param bandUp The last band's index+1 to interpolate. * @return {@code null} if point is outside grid coverage. */ private synchronized double[] interpolate(final double x, final double y, double[] dest, int band, final int bandUp) { final double x0 = Math.floor(x); final double y0 = Math.floor(y); final int ix = (int)x0; final int iy = (int)y0; if (!(ix>=xmin && ix<=xmax && iy>=ymin && iy<=ymax)) return null; /* * Creates buffers, if not already created. */ double[][] samples = doubles; if (samples == null) { final int rowCount = interpolation.getHeight(); final int colCount = interpolation.getWidth(); doubles = samples = new double[rowCount][]; for (int i=0; i<rowCount; i++) { samples[i] = new double[colCount]; } } if (dest == null) { dest = new double[bandUp]; } /* * Builds up a RectIter and use it for interpolating all bands. * There is very few points, so the cost of creating a RectIter * may be important. But it seems to still lower than query tiles * many time (which may involve more computation than necessary). */ bounds.x = ix - left; bounds.y = iy - top; final RectIter iter = RectIterFactory.create(image.getExtendedData(bounds, this.borderExtender), bounds); for (; band<bandUp; band++) { iter.startLines(); int j=0; do { iter.startPixels(); final double[] row=samples[j++]; int i=0; do { row[i++] = iter.getSampleDouble(band); } while (!iter.nextPixelDone()); assert i == row.length; } while (!iter.nextLineDone()); assert j == samples.length; float dx = (float)(x-x0); if (dx==1) dx=ONE_EPSILON; float dy = (float)(y-y0); if (dy==1) dy=ONE_EPSILON; final double value = interpolation.interpolate(samples, dx, dy); if (Double.isNaN(value)) { if (fallback == this) continue; // 'dest' was set by 'super.evaluate(...)'. if (fallback != null) { fallback.interpolate(x, y, dest, band, band+1); continue; } // If no fallback was specified, then 'dest' is not required to // have been initialized. It may contains random value. Set it // to the NaN value... } dest[band] = value; } return dest; } /** * Interpolates at the specified position. If {@code fallback!=null}, * then {@code dest} <strong>must</strong> have been initialized with * {@code super.evaluate(...)} prior to invoking this method. * * @param x The x position in pixel's coordinates. * @param y The y position in pixel's coordinates. * @param dest The destination array, or null. * @param band The first band's index to interpolate. * @param bandUp The last band's index+1 to interpolate. * @return {@code null} if point is outside grid coverage. */ private synchronized float[] interpolate(final double x, final double y, float[] dest, int band, final int bandUp) { final double x0 = Math.floor(x); final double y0 = Math.floor(y); final int ix = (int)x0; final int iy = (int)y0; if (!(ix>=xmin && ix<xmax && iy>=ymin && iy<ymax)) return null; /* * Create buffers, if not already created. */ float[][] samples = floats; if (samples == null) { final int rowCount = interpolation.getHeight(); final int colCount = interpolation.getWidth(); floats = samples = new float[rowCount][]; for (int i=0; i<rowCount; i++) { samples[i] = new float[colCount]; } } if (dest == null) { dest = new float[bandUp]; } /* * Builds up a RectIter and use it for interpolating all bands. * There is very few points, so the cost of creating a RectIter * may be important. But it seems to still lower than query tiles * many time (which may involve more computation than necessary). */ bounds.x = ix - left; bounds.y = iy - top; final RectIter iter = RectIterFactory.create(image.getExtendedData(bounds, this.borderExtender), bounds); for (; band<bandUp; band++) { iter.startLines(); int j=0; do { iter.startPixels(); final float[] row=samples[j++]; int i=0; do { row[i++] = iter.getSampleFloat(band); } while (!iter.nextPixelDone()); assert i == row.length; } while (!iter.nextLineDone()); assert j == samples.length; float dx = (float)(x-x0); if (dx==1) dx=ONE_EPSILON; float dy = (float)(y-y0); if (dy==1) dy=ONE_EPSILON; final float value = interpolation.interpolate(samples, dx, dy); if (Float.isNaN(value)) { if (fallback == this) continue; // 'dest' was set by 'super.evaluate(...)'. if (fallback != null) { fallback.interpolate(x, y, dest, band, band+1); continue; } // If no fallback was specified, then 'dest' is not required to // have been initialized. It may contains random value. Set it // to the NaN value... } dest[band] = value; } return dest; } /** * Interpolates at the specified position. If {@code fallback!=null}, * then {@code dest} <strong>must</strong> have been initialized with * {@code super.evaluate(...)} prior to invoking this method. * * @param x The x position in pixel's coordinates. * @param y The y position in pixel's coordinates. * @param dest The destination array, or null. * @param band The first band's index to interpolate. * @param bandUp The last band's index+1 to interpolate. * @return {@code null} if point is outside grid coverage. */ private synchronized int[] interpolate(final double x, final double y, int[] dest, int band, final int bandUp) { final double x0 = Math.floor(x); final double y0 = Math.floor(y); final int ix = (int)x0; final int iy = (int)y0; if (!(ix>=xmin && ix<xmax && iy>=ymin && iy<ymax)) return null; /* * Creates buffers, if not already created. */ int[][] samples = ints; if (samples == null) { final int rowCount = interpolation.getHeight(); final int colCount = interpolation.getWidth(); ints = samples = new int[rowCount][]; for (int i=0; i<rowCount; i++) { samples[i] = new int[colCount]; } } if (dest == null) { dest = new int[bandUp]; } /* * Builds up a RectIter and use it for interpolating all bands. * There is very few points, so the cost of creating a RectIter * may be important. But it seems to still lower than query tiles * many time (which may involve more computation than necessary). */ bounds.x = ix - left; bounds.y = iy - top; final RectIter iter = RectIterFactory.create(image.getExtendedData(bounds, this.borderExtender), bounds); for (; band<bandUp; band++) { iter.startLines(); int j=0; do { iter.startPixels(); final int[] row=samples[j++]; int i=0; do { row[i++] = iter.getSample(band); } while (!iter.nextPixelDone()); assert i==row.length; } while (!iter.nextLineDone()); assert j == samples.length; final int xfrac = (int) ((x-x0) * (1 << interpolation.getSubsampleBitsH())); final int yfrac = (int) ((y-y0) * (1 << interpolation.getSubsampleBitsV())); dest[band] = interpolation.interpolate(samples, xfrac, yfrac); } return dest; } }