blob: 0827f8aef94d610011f053165df9f64daa36ce36 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.sis.image;
import java.util.Objects;
import java.lang.ref.Reference;
import java.nio.DoubleBuffer;
import java.awt.Point;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.geom.Rectangle2D;
import java.awt.image.ColorModel;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.awt.image.WritableRaster;
import java.awt.image.ImagingOpException;
import java.awt.image.SampleModel;
import javax.measure.Quantity;
import javax.measure.Unit;
import javax.measure.quantity.Length;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransform2D;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.referencing.operation.transform.MathTransforms;
import org.apache.sis.coverage.privy.ImageUtilities;
import org.apache.sis.coverage.privy.FillValues;
import org.apache.sis.feature.internal.Resources;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.Disposable;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.privy.Numerics;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.geometry.Shapes2D;
import org.apache.sis.measure.Quantities;
import org.apache.sis.measure.Units;
import static org.apache.sis.coverage.privy.ImageUtilities.LOGGER;
/**
* An image which is the result of resampling the pixel values of another image.
* Resampling is the action of computing pixel values at possibly non-integral positions of a source image.
* It can be used for projecting an image to another coordinate reference system,
* for example from (<var>latitude</var>, <var>longitude</var>) to World Mercator.
* The resampling is defined by a non-linear {@link MathTransform} (for example a map projection)
* which converts pixel center coordinates from <em>this</em> image to pixel center coordinates
* in the <em>source</em> image.
* The converted coordinates usually contain fraction digits, in which case an interpolation is applied.
*
* <h2>Usage note</h2>
* This class should be used with non-linear transforms such as map projections. It is technically
* possible to use this class with linear transforms such as {@link java.awt.geom.AffineTransform},
* but there is more efficient alternatives for linear cases (for example
* {@linkplain java.awt.Graphics2D#drawRenderedImage(RenderedImage, java.awt.geom.AffineTransform)
* specifying the affine transform at rendering time}).
*
* @author Martin Desruisseaux (Geomatys)
* @author Johann Sorel (Geomatys)
* @version 1.4
*
* @see Interpolation
* @see java.awt.image.AffineTransformOp
*
* @since 1.1
*/
public class ResampledImage extends ComputedImage {
/**
* Key of a property providing an estimation of positional error for each pixel.
* Values shall be instances of {@link RenderedImage} with same size and origin than this image.
* The image should contain a single band where all sample values are error estimations in pixel units
* (relative to pixels of this image). The value should be small, for example between 0 and 0.2.
*
* <p>The default implementation transforms all pixel coordinates {@linkplain #toSource to source},
* then convert them back to pixel coordinates in this image. The result is compared with expected
* coordinates and the distance is stored in the image.</p>
*
* @see #POSITIONAL_ACCURACY_KEY
*/
public static final String POSITIONAL_CONSISTENCY_KEY = "org.apache.sis.PositionalConsistency";
/**
* The {@value} value for identifying code expecting exactly 2 dimensions.
*/
static final int BIDIMENSIONAL = 2;
/**
* Domain of pixel coordinates in this image.
*/
private final int minX, minY, width, height;
/**
* Index of the first tile.
*/
private final int minTileX, minTileY;
/**
* Conversion from pixel center coordinates of <em>this</em> image to pixel center coordinates of <em>source</em>
* image. This transform should be an instance of {@link MathTransform2D}, but this is not required by this class
* (a future version may allow interpolations in a <var>n</var>-dimensional cube).
*
* @see org.opengis.referencing.datum.PixelInCell#CELL_CENTER
*/
protected final MathTransform toSource;
/**
* Same as {@link #toSource} but with the addition of a shift for taking in account the number of pixels required
* for interpolations. For example if a bicubic interpolation needs 4×4 pixels, then the source coordinates that
* we need are not the coordinates of the pixel we want to interpolate, but 1 or 2 pixels before for making room
* for interpolation support.
*
* <p>This transform may be an instance of {@link ResamplingGrid} if the usage of such grid has been authorized.
* That transform may be non-invertible. Consequently, this transform should not be used for inverse operations
* and should not be made accessible to the user.</p>
*
* <p>This transform maps pixel centers of both images, except in the case of nearest-neighbor interpolation.
* In that special case only, the transform maps target pixel <em>center</em> to source pixel <em>corner</em>.
* We have to map corners in source images because source pixel coordinates are computed by taking the integer
* parts of {@code toSourceSupport} results, without rounding.</p>
*
* @see #interpolationSupportOffset(int)
* @see Interpolation#interpolate(DoubleBuffer, int, double, double, double[], int)
*/
private final MathTransform toSourceSupport;
/**
* The object to use for performing interpolations.
*/
protected final Interpolation interpolation;
/**
* The values to use if a pixel in this image cannot be mapped to a pixel in the source image.
* Must be an {@code int[]} or {@code double[]} array (no other type allowed). The array length
* must be equal to the number of bands. Cannot be null.
*/
private final Object fillValues;
/**
* The largest accuracy declared in the {@code accuracy} argument given to constructor,
* or {@code null} if none. This is for information purpose only.
*
* @see #getPositionalAccuracy()
*/
private final Quantity<Length> linearAccuracy;
/**
* {@link #POSITIONAL_CONSISTENCY_KEY} value, computed when first requested.
*
* @see #getPositionalConsistency()
* @see #getProperty(String)
*/
private Reference<ComputedImage> positionalConsistency;
/**
* {@link #MASK_KEY} value, computed when first requested.
*
* @see #getMask()
* @see #getProperty(String)
*/
private Reference<ComputedImage> mask;
/**
* Creates a new image which will resample the given image. The resampling operation is defined
* by a potentially non-linear transform from <em>this</em> image to the specified <em>source</em> image.
* That transform should map {@linkplain org.opengis.referencing.datum.PixelInCell#CELL_CENTER pixel centers}.
*
* <p>The {@code sampleModel} determines the tile size and the target data type. This is often the same sample
* model than the one used by the {@code source} image, but may also be different for forcing a different tile
* size or a different data type (e.g. {@code byte} versus {@code float}) for storing resampled values.
* If the specified sample model is not the same as the one used by the source image,
* then subclass should override {@link #getColorModel()} for returning a color model which is
* {@linkplain ColorModel#isCompatibleSampleModel(SampleModel) compatible with the sample model}.</p>
*
* <p>If a pixel in this image cannot be mapped to a pixel in the source image, then the sample values are set
* to {@code fillValues}. If the given array is {@code null}, or if any element in the given array is {@code null},
* then the default fill value is NaN for floating point data types or zero for integer data types.
* If the array is shorter than the number of bands, then above-cited default values are used for missing values.
* If longer than the number of bands, extraneous values are ignored.</p>
*
* @param source the image to be resampled.
* @param sampleModel the sample model shared by all tiles in this resampled image.
* @param minTile indices of the first tile ({@code minTileX}, {@code minTileY}), or {@code null} for (0,0).
* @param bounds domain of pixel coordinates of this resampled image.
* @param toSource conversion of pixel coordinates of this image to pixel coordinates of {@code source} image.
* @param interpolation the object to use for performing interpolations.
* @param fillValues the values to use for pixels in this image that cannot be mapped to pixels in source image.
* May be {@code null} or contain {@code null} elements, and may have any length
* (see above for more details).
* @param accuracy values of {@value #POSITIONAL_ACCURACY_KEY} property, or {@code null} if none.
* This constructor may retain only a subset of specified values or replace some of them.
* If an accuracy is specified in {@linkplain Units#PIXEL pixel units}, then a value such as
* 0.125 pixel may enable the use of a slightly faster algorithm at the expense of accuracy.
* This is only a hint honored on a <em>best-effort</em> basis.
*
* @see ImageProcessor#resample(RenderedImage, Rectangle, MathTransform)
*/
protected ResampledImage(final RenderedImage source, final SampleModel sampleModel, final Point minTile,
final Rectangle bounds, final MathTransform toSource, Interpolation interpolation,
final Number[] fillValues, final Quantity<?>[] accuracy)
{
super(sampleModel, source);
if (source.getWidth() <= 0 || source.getHeight() <= 0) {
throw new IllegalArgumentException(Resources.format(Resources.Keys.EmptyImage));
}
ArgumentChecks.ensureNonNull("interpolation", interpolation);
ArgumentChecks.ensureStrictlyPositive("width", width = bounds.width);
ArgumentChecks.ensureStrictlyPositive("height", height = bounds.height);
minX = bounds.x;
minY = bounds.y;
if (minTile != null) {
minTileX = minTile.x;
minTileY = minTile.y;
} else {
minTileX = 0;
minTileY = 0;
}
/*
* The transform from this image to source image must have exactly two coordinates in input
* (otherwise we would not know what to put in extra coordinates), but may have more values
* in output. The two first output coordinate values will be used for interpolation between
* pixels of source image. Supplemental coordinates can be used for selecting an image in a
* n-dimensional data cube.
*/
this.toSource = toSource;
int numDim = toSource.getSourceDimensions();
if (numDim != BIDIMENSIONAL || (numDim = toSource.getTargetDimensions()) < BIDIMENSIONAL) {
throw new IllegalArgumentException(Errors.format(
Errors.Keys.MismatchedDimension_3, "toSource", BIDIMENSIONAL, numDim));
}
/*
* If the image uses an index color model, interpolating the indexed values does not produce
* the expected colors. Safest approach is to disable completely interpolations in that case.
*/
interpolation = interpolation.toCompatible(source);
/*
* If the interpolation requires more than 2×2 pixels, we will need to shift the transform
* to source image. For example if the interpolation requires 4×4 pixels, the interpolation
* point will be between the second and third pixel, so there is one more pixel on the left
* to grab. We shift to the left because we need the coordinates of the first pixel.
*/
Dimension s = interpolation.getSupportSize();
if (s.width > source.getWidth() || s.height > source.getHeight()) {
interpolation = Interpolation.NEAREST;
s = interpolation.getSupportSize();
}
this.interpolation = interpolation;
final double[] offset = new double[numDim];
offset[0] = interpolationSupportOffset(s.width);
offset[1] = interpolationSupportOffset(s.height);
MathTransform toSourceSupport = MathTransforms.concatenate(toSource, MathTransforms.translation(offset));
/*
* If the desired accuracy is large enough, try using a grid of precomputed values for faster operations.
* This is optional; it is okay to abandon the grid if we cannot compute it.
*/
Boolean canUseGrid = null;
Quantity<Length> linearAccuracy = null;
if (accuracy != null) {
for (final Quantity<?> hint : accuracy) {
if (hint != null) {
final Unit<?> unit = hint.getUnit();
if (Units.PIXEL.equals(unit)) {
final boolean c = Math.abs(hint.getValue().doubleValue()) >= ResamplingGrid.TOLERANCE;
if (canUseGrid == null) canUseGrid = c;
else canUseGrid &= c;
} else if (Units.isLinear(unit)) {
linearAccuracy = Quantities.max(linearAccuracy, hint.asType(Length.class));
}
}
}
}
if (canUseGrid != null && canUseGrid) try {
toSourceSupport = ResamplingGrid.getOrCreate(MathTransforms.bidimensional(toSourceSupport), bounds);
} catch (TransformException | ImagingOpException e) {
recoverableException("<init>", e);
}
this.toSourceSupport = toSourceSupport;
this.linearAccuracy = linearAccuracy;
this.fillValues = new FillValues(sampleModel, fillValues, false).asPrimitiveArray;
}
/**
* The relative index of the first pixel needed on the left or top side of the region to use for interpolation.
* The index is relative to the two central pixels needed for a bilinear interpolation. This value is negative
* or zero. A value of 0 means that we need no pixels in addition of the two central pixels:
*
* <blockquote>
* sample[0] … (position where to interpolate) … sample[1]
* </blockquote>
*
* A value of -1 means that we need one more pixel on the left or top side.
* It often means that we also need one more pixel on the right or bottom
* sides, but not necessarily; those sides are not this method business.
*
* <blockquote>
* sample[-1] … sample[0] … (position where to interpolate) … sample[1] … sample[2]
* </blockquote>
*
* <h4>Nearest-neighbor special case</h4>
* The nearest-neighbor interpolation (identified by {@code span == 1}) is handled in a special way.
* The return value should be 0 according above contract, but this method returns 0.5 instead.
* This addition of a 0.5 offset allows the following substitution:
*
* {@snippet lang="java" :
* Math.round(x) ≈ (long) Math.floor(x + 0.5)
* }
*
* {@link Math#round(double)} is the desired behavior for nearest-neighbor interpolation, but the buffer given
* to {@link Interpolation#interpolate(DoubleBuffer, int, double, double, double[], int)} is filled with values
* at coordinates determined by {@link Math#floor(double)} semantic. Because the buffer has only one value,
* {@code interpolate(…)} has no way to look at neighbor values for the best match (contrarily to what other
* interpolation implicitly do, through mathematics). The 0.5 offset is necessary for compensating.
*
* @param span the width or height of the support region for interpolations.
* @return relative index of the first pixel needed on the left or top sides,
* as a value ≤ 0 (except in nearest-neighbor special case).
*
* @see #toSourceSupport
* @see Interpolation#interpolate(DoubleBuffer, int, double, double, double[], int)
*/
static double interpolationSupportOffset(final int span) {
if (span <= 1) return 0.5; // Nearest-neighbor (special case).
return -((span - 1) / 2); // Round toward 0.
}
/**
* Returns the upper limit (inclusive) where an interpolation is possible. The given {@code max} value is
* the maximal coordinate value (inclusive) traversed by {@link PixelIterator}. Note that this is not the
* image size because of margin required by interpolation methods.
*
* <p>Since interpolator will receive data at coordinates {@code max} to {@code max + span - 1} inclusive
* and since those coordinates are pixel centers, the points to interpolate are on the surface of a valid
* pixel until {@code (max + span - 1) + 0.5}. Consequently, this method computes {@code max + span - 0.5}.
* An additional 0.5 offset is added in the special case of nearest-neighbor interpolation for consistency
* with {@link #interpolationSupportOffset(int)}.</p>
*
* @param max the maximal coordinate value, inclusive.
* @param span the width or height of the support region for interpolations.
* @return {@code max + span - 0.5} (except in nearest-neighbor special case).
*
* @see PixelIterator#getDomain()
*/
private static double interpolationLimit(double max, final int span) {
max += span;
if (span > 1) max -= 0.5; // Must be consistent with `interpolationSupportOffset(int)`.
return max;
}
/**
* Returns the number of quantities in the array returned by {@link #getPositionalAccuracy()}.
*/
private int getPositionalAccuracyCount() {
int n = 0;
if (linearAccuracy != null) n++;
if (toSourceSupport instanceof ResamplingGrid) n++;
return n;
}
/**
* Computes the {@value #POSITIONAL_ACCURACY_KEY} value. This method is invoked by {@link #getProperty(String)}
* when the {@link #POSITIONAL_ACCURACY_KEY} property value is requested.
*/
@SuppressWarnings("rawtypes")
private Quantity<?>[] getPositionalAccuracy() {
final Quantity<?>[] accuracy = new Quantity[getPositionalAccuracyCount()];
int n = 0;
if (linearAccuracy != null) {
accuracy[n++] = linearAccuracy;
}
if (toSourceSupport instanceof ResamplingGrid) {
accuracy[n++] = Quantities.create(ResamplingGrid.TOLERANCE, Units.PIXEL);
}
return accuracy;
}
/**
* Computes the {@value #POSITIONAL_CONSISTENCY_KEY} value. This method is invoked by {@link #getProperty(String)}
* when the {@link #POSITIONAL_CONSISTENCY_KEY} property value is requested. The result is saved by weak reference
* since recomputing this image is rarely requested, and if needed can be recomputed easily.
*/
private synchronized RenderedImage getPositionalConsistency() throws TransformException {
ComputedImage image = (positionalConsistency != null) ? positionalConsistency.get() : null;
if (image == null) {
positionalConsistency = null;
final Dimension s = interpolation.getSupportSize();
final double[] offset = new double[toSourceSupport.getSourceDimensions()];
offset[0] = -interpolationSupportOffset(s.width);
offset[1] = -interpolationSupportOffset(s.height);
final MathTransform tr = MathTransforms.concatenate(toSourceSupport, MathTransforms.translation(offset));
image = new PositionalConsistencyImage(this, tr);
positionalConsistency = image.reference();
}
return image;
}
/**
* Computes the {@value #MASK_KEY} value. This method is invoked by {@link #getProperty(String)} when the
* {@link #MASK_KEY} property value is requested. The result is saved by weak reference since recomputing
* this image is rarely requested, and if needed can be recomputed easily.
*/
private synchronized RenderedImage getMask() {
ComputedImage image = (mask != null) ? mask.get() : null;
if (image == null) {
mask = null; // Cleared first in case an error occurs below.
image = new MaskImage(this);
mask = image.reference();
}
return image;
}
/**
* Returns {@code true} if this image cannot have mask.
*/
boolean hasNoMask() {
return fillValues instanceof int[];
}
/**
* Verifies whether image layout information are consistent. This method verifies that source coordinates
* required by this image (computed by converting {@linkplain #getBounds() this image bounds} using the
* {@link #toSource} transform) intersects the bounds of the source image. If this is not the case, then
* this method returns {@code "toSource"} for signaling that the transform may have a problem.
* Otherwise this method completes the check with all verifications
* {@linkplain ComputedImage#verify() documented in parent class}
*
* @return {@code null} if image layout information are consistent,
* or the name of inconsistent attribute if a problem is found.
*/
@Override
public String verify() {
if (toSource instanceof MathTransform2D) try {
final Rectangle bounds = getBounds();
final Rectangle2D tb = Shapes2D.transform((MathTransform2D) toSource, bounds, bounds);
if (!ImageUtilities.getBounds(getSource()).intersects(tb)) {
return "toSource";
}
} catch (TransformException e) {
recoverableException("verify", e);
return "toSource";
}
return super.verify(); // "width" and "height" properties should be checked last.
}
/**
* Invoked when a non-fatal error occurred.
*
* @param method the method where the ignorable error occurred.
* @param error the ignore which can be ignored.
*/
private static void recoverableException(final String method, final Exception error) {
Logging.recoverableException(LOGGER, ResampledImage.class, method, error);
}
/**
* Returns the color model of this resampled image.
* Default implementation assumes that this image has the same color model as the source image.
*
* @return the color model, or {@code null} if unspecified.
*/
@Override
public ColorModel getColorModel() {
RenderedImage image = getDestination();
if (image == null) image = getSource();
return image.getColorModel();
}
/**
* Gets a property from this image. Current default implementation supports the following keys
* (more properties may be added to this list in any future Apache SIS versions):
*
* <ul>
* <li>{@value #POSITIONAL_ACCURACY_KEY}</li>
* <li>{@value #POSITIONAL_CONSISTENCY_KEY}</li>
* <li>{@value #SAMPLE_DIMENSIONS_KEY} (forwarded to the source image)</li>
* <li>{@value #SAMPLE_RESOLUTIONS_KEY} (forwarded to the source image)</li>
* <li>{@value #MASK_KEY} if the image uses floating point numbers.</li>
* </ul>
*
* <h4>Note on sample values</h4>
* The sample resolutions are retained because they should have approximately the same values before and after
* resampling. {@linkplain #STATISTICS_KEY Statistics} are not in this list because, while minimum and maximum
* values should stay approximately the same, the average value and standard deviation may be quite different.
*/
@Override
public Object getProperty(final String key) {
switch (key) {
case SAMPLE_DIMENSIONS_KEY:
case SAMPLE_RESOLUTIONS_KEY: {
return getSource().getProperty(key);
}
case POSITIONAL_ACCURACY_KEY: {
return getPositionalAccuracy();
}
case POSITIONAL_CONSISTENCY_KEY: try {
return getPositionalConsistency();
} catch (TransformException | IllegalArgumentException e) {
throw (ImagingOpException) new ImagingOpException(e.getMessage()).initCause(e);
}
case MASK_KEY: {
if (hasNoMask()) break;
return getMask();
}
}
return super.getProperty(key);
}
/**
* Returns the names of all recognized properties, or {@code null} if this image has no properties.
* The returned array contains the properties listed in {@link #getProperty(String)} if the source
* image has those properties.
*
* @return names of all recognized properties, or {@code null} if none.
*/
@Override
public String[] getPropertyNames() {
final String[] inherited = getSource().getPropertyNames();
final String[] names = {
SAMPLE_DIMENSIONS_KEY,
SAMPLE_RESOLUTIONS_KEY,
POSITIONAL_ACCURACY_KEY,
POSITIONAL_CONSISTENCY_KEY,
MASK_KEY
};
int n = 0;
for (final String name : names) {
if (name != POSITIONAL_CONSISTENCY_KEY) { // Identity comparisons are okay for this method.
if (name == POSITIONAL_ACCURACY_KEY) {
if (getPositionalAccuracyCount() == 0) {
continue; // Exclude PositionalAccuracy change.
}
} else if (name == MASK_KEY) {
if (hasNoMask()) {
continue;
}
} else if (!ArraysExt.contains(inherited, name)) {
continue; // Exclude inherited property not defined by source.
}
}
names[n++] = name;
}
return ArraysExt.resize(names, n);
}
/**
* Returns the minimum tile index in the <var>x</var> direction.
* This is often 0.
*
* @return the minimum tile index in the <var>x</var> direction.
*/
@Override
public final int getMinTileX() {
return minTileX;
}
/**
* Returns the minimum tile index in the <var>y</var> direction.
* This is often 0.
*
* @return the minimum tile index in the <var>y</var> direction.
*/
@Override
public final int getMinTileY() {
return minTileY;
}
/**
* Returns the minimum <var>x</var> coordinate (inclusive) of this image.
* This is the {@link Rectangle#x} value of the {@code bounds} specified at construction time.
*
* @return the minimum <var>x</var> coordinate (column) of this image.
*/
@Override
public final int getMinX() {
return minX;
}
/**
* Returns the minimum <var>y</var> coordinate (inclusive) of this image.
* This is the {@link Rectangle#y} value of the {@code bounds} specified at construction time.
*
* @return the minimum <var>y</var> coordinate (row) of this image.
*/
@Override
public final int getMinY() {
return minY;
}
/**
* Returns the number of columns in this image.
* This is the {@link Rectangle#width} value of the {@code bounds} specified at construction time.
*
* @return number of columns in this image.
*/
@Override
public final int getWidth() {
return width;
}
/**
* Returns the number of rows in this image.
* This is the {@link Rectangle#height} value of the {@code bounds} specified at construction time.
*
* @return number of rows in this image.
*/
@Override
public final int getHeight() {
return height;
}
/**
* Invoked when a tile need to be computed or updated. This method fills all pixel values of the tile
* with values interpolated from the source image. It may be invoked concurrently in different threads.
*
* @param tileX the column index of the tile to compute.
* @param tileY the row index of the tile to compute.
* @param tile if the tile already exists but needs to be updated, the tile to update. Otherwise {@code null}.
* @return computed tile for the given indices.
* @throws TransformException if an error occurred while computing pixel coordinates.
*/
@Override
@SuppressWarnings("SuspiciousSystemArraycopy")
protected Raster computeTile(final int tileX, final int tileY, WritableRaster tile) throws TransformException {
if (tile == null) {
tile = createTile(tileX, tileY);
}
final int numBands = tile.getNumBands();
final int scanline = tile.getWidth();
final int tileMinX = tile.getMinX();
final int tileMinY = tile.getMinY();
final int tileMaxX = Math.addExact(tileMinX, scanline);
final int tileMaxY = Math.addExact(tileMinY, tile.getHeight());
final int tgtDim = toSourceSupport.getTargetDimensions();
final double[] coordinates = new double[scanline * Math.max(BIDIMENSIONAL, tgtDim)];
/*
* Compute the bounds of pixel coordinates that we can use for setting iterator positions in the source image.
* The iterator bounds are slightly smaller than the image bounds because it needs to keep a margin for giving
* enough pixels to interpolators (for example bilinear interpolations require 2×2 pixels).
*
* The (xmin, ymin) and (xmax, ymax) coordinates are integers and inclusive. Because integer pixel coordinates
* are located at pixel centers, the image area is actually wider by 0.5 pixel (or 1.5, 2.5, …) on image sides.
* This expansion is taken in account in (xlim, ylim), which are the limit values than we can interpolate.
*/
final double xmin, ymin, xmax, ymax, xlim, ylim, xoff, yoff;
final PixelIterator it;
{ // For keeping temporary variables locale.
final Dimension support = interpolation.getSupportSize();
it = new PixelIterator.Builder().setWindowSize(support).create(getSource());
final Rectangle domain = it.getDomain(); // Source image bounds.
xmin = domain.getMinX(); // We will tolerate 0.5 pixels before (from center to border).
ymin = domain.getMinY();
xmax = domain.getMaxX() - 1; // Iterator limit (inclusive) because of interpolation support.
ymax = domain.getMaxY() - 1;
xlim = interpolationLimit(xmax, support.width); // Upper limit of coordinates where we can interpolate.
ylim = interpolationLimit(ymax, support.height);
xoff = interpolationSupportOffset(support.width) - 0.5; // Always negative (or 0 for nearest-neighbor).
yoff = interpolationSupportOffset(support.height) - 0.5;
}
/*
* In the special case of nearest-neighbor interpolation with no precision lost, the code inside the loop
* can take a shorter path were data are just copied. The lossless criterion allows us to omit the checks
* for minimal and maximal values. Shortcut may apply to both integer values and floating point values.
*/
final boolean useFillValues = (getDestination() == null);
final boolean shortcut = useFillValues && Interpolation.NEAREST.equals(interpolation) &&
ImageUtilities.isLosslessConversion(sampleModel, tile.getSampleModel());
/*
* Prepare a buffer where to store a line of interpolated values. We use this buffer for transferring
* many pixels in a single `WritableRaster.setPixels(…)` call, which is faster than invoking `setPixel(…)`
* for each pixel. We use integer values if possible because `WritableRaster.setPixels(…)` implementations
* have optimizations for this case. If data are not integers, then we fallback on non-optimized `double[]`.
*/
double[] transfer = null;
int[] intTransfer = null;
final double[] values;
final int[] intValues;
final Object valuesArray;
final long[] minValues, maxValues;
final boolean isInteger = (fillValues instanceof int[]);
if (isInteger) {
valuesArray = intValues = new int[scanline * numBands];
if (shortcut) {
values = null; // No floating point values to transfer.
minValues = null; // Min/max checks are not needed in shortcut case.
maxValues = null;
} else {
values = new double[numBands];
minValues = new long [numBands];
maxValues = new long [numBands];
final SampleModel sm = tile.getSampleModel();
for (int b=0; b<numBands; b++) {
maxValues[b] = Numerics.bitmask(sm.getSampleSize(b)) - 1;
}
if (!ImageUtilities.isUnsignedType(sm)) {
for (int b=0; b<numBands; b++) {
minValues[b] = ~(maxValues[b] >>>= 1); // Convert unsigned type to signed type range.
}
}
}
} else {
intValues = null;
values = new double[scanline * numBands];
valuesArray = values;
minValues = null; // Not used for floating point types.
maxValues = null;
}
/*
* The (sx,sy) values are iterator position, remembered for detecting if the window buffer
* needs to be updated. The `Integer.MAX_VALUE` initial value is safe because the iterator
* cannot have that position (its construction would have failed with ArithmeticException
* if the image position was so high).
*/
int sx = Integer.MAX_VALUE;
int sy = Integer.MAX_VALUE;
final PixelIterator.Window<DoubleBuffer> buffer = it.createWindow(TransferType.DOUBLE);
for (int ty = tileMinY; ty < tileMaxY; ty++) {
/*
* Transform a block of coordinates in one `transform(…)` method call.
* This is faster than doing a method call for each coordinates tuple.
*/
for (int ci=0, tx=tileMinX; tx<tileMaxX; tx++) {
coordinates[ci++] = tx;
coordinates[ci++] = ty;
}
toSourceSupport.transform(coordinates, 0, coordinates, 0, scanline);
/*
* Pixel coordinate along X axis where to start writing the `values` or `intValues` array.
* This is usually the first column of the tile, and the number of pixels to write is the
* tile width (i.e. we write a full tile row). However, those values may be modified below
* if we avoid writing pixels that are outside the source image.
*/
int posX = tileMinX;
if (shortcut) {
/*
* Special case for nearest-neighbor interpolation without the need to check for min/max values.
* In this case values will be copied as `int` or `double` type without further processing.
*/
int ci = 0; // Index in `coordinates` array.
int vi = 0; // Index in `values` or `intValues` array.
for (int tx=tileMinX; tx<tileMaxX; tx++, ci+=tgtDim, vi+=numBands) {
final long x = (long) Math.floor(coordinates[ci]);
if (x >= it.lowerX && x < it.upperX) {
final long y = (long) Math.floor(coordinates[ci+1]);
if (y >= it.lowerY && y < it.upperY) {
if (sx != (sx = (int) x) | // Really |, not ||.
sy != (sy = (int) y))
{
it.moveTo(sx, sy);
}
if (isInteger) {
intTransfer = it.getPixel(intTransfer);
System.arraycopy(intTransfer, 0, intValues, vi, numBands);
} else {
transfer = it.getPixel(transfer);
System.arraycopy(transfer, 0, values, vi, numBands);
}
continue; // Values have been set, move to next pixel.
}
}
System.arraycopy(fillValues, 0, valuesArray, vi, numBands);
}
} else {
/*
* Interpolate values for all bands in current scanline. The (x,y) values are coordinates
* in the source image and (xf,yf) are their fractional parts. Those fractional parts are
* between 0 inclusive and 1 exclusive except on the image borders: on the left and upper
* sides the fractional parts can go down to -0.5, because 0 is for pixel center and -0.5
* is at image border. On the right and bottom sides the fractional parts are constrained
* to +0.5 in nearest-neighbor interpolation case, for the same reason as other borders.
* However if the interpolation is bilinear, then the fractional parts on the bottom and
* right borders can go up to 1.5 because `PixelIterator` has reduced the (xmax, ymax)
* values by 1 (for taking in account the padding needed for interpolation support).
* This tolerance can be generalized (2.5, 3.5, etc.) depending on interpolation method.
*/
int ci = 0; // Index in `coordinates` array.
int vi = 0; // Index in `values` or `intValues` array.
for (int tx=tileMinX; tx<tileMaxX; tx++, ci+=tgtDim) {
double x = coordinates[ci];
if (x <= xlim) {
// Separate integer and fractional parts with 0 ≤ xf < 1 except on borders.
final double xf = x - (x = Math.max(xmin, Math.min(xmax, Math.floor(x))));
if (xf >= xoff) { // Negative only on left image border.
double y = coordinates[ci+1];
if (y <= ylim) {
// Separate integer and fractional parts with 0 ≤ yf < 1 except on borders.
final double yf = y - (y = Math.max(ymin, Math.min(ymax, Math.floor(y))));
if (yf >= yoff) { // Negative only on upper image border.
/*
* At this point we determined that (x,y) coordinates are inside source image domain.
* Those coordinates may have been slightly shifted for interpolation support if they
* were close to an image border. If the MathTransform produced 3 or more coordinates,
* current implementation does not yet use those coordinates. But if we want to use
* them in a future version (e.g. for interpolation in 3D cube), it would be there.
*/
if (sx != (sx = (int) x) | // Really |, not ||.
sy != (sy = (int) y))
{
it.moveTo(sx, sy);
buffer.update();
}
/*
* Interpolate the values at current position. We don't do any special processing
* for NaN values because we want to keep them if output type is floating point,
* and NaN values should not occur if data type (input and output) is integer.
*/
interpolation.interpolate(buffer.values, numBands, xf, yf, values, isInteger ? 0 : vi);
if (isInteger) {
for (int b=0; b<numBands; b++) {
intValues[vi+b] = (int) Math.max(minValues[b],
Math.min(maxValues[b], Math.round(values[b])));
}
}
vi += numBands;
continue; // Values have been set, move to next pixel.
}
}
}
}
/*
* If we reach this point then any of the "if" conditions above failed
* (i.e. the point to interpolate is outside the source image bounds)
* and no values have been set in the `values` or `intValues` array.
* If we are writing in an existing image, do not write anything
* (i.e. keep the existing value). Otherwise write the fill values.
*/
if (useFillValues) {
System.arraycopy(fillValues, 0, valuesArray, vi, numBands);
vi += numBands;
} else {
if (vi != 0) {
final int numX = vi / numBands;
if (isInteger) {
tile.setPixels(posX, ty, numX, 1, intValues);
} else {
tile.setPixels(posX, ty, numX, 1, values);
}
posX += numX;
vi = 0;
}
posX++;
}
}
}
/*
* At this point we finished to compute the value of a scanline.
* Copy to its final destination then move to next line.
*/
final int numX = scanline - (posX - tileMinX);
if (numX != 0) {
if (isInteger) {
tile.setPixels(posX, ty, numX, 1, intValues);
} else {
tile.setPixels(posX, ty, numX, 1, values);
}
}
}
return tile;
}
/**
* Notifies the source image that tiles will be computed soon in the given region.
* If the source image is an instance of {@link ComputedImage}, then this method
* forwards the notification to it. Otherwise default implementation does nothing.
*
* @since 1.2
*/
@Override
protected Disposable prefetch(final Rectangle tiles) {
final RenderedImage source = getSource();
if (source instanceof PlanarImage) try {
final Dimension s = interpolation.getSupportSize();
Rectangle pixels = ImageUtilities.tilesToPixels(this, tiles);
final Rectangle2D bounds = new Rectangle2D.Double(
pixels.x - 0.5 * s.width,
pixels.y - 0.5 * s.height,
pixels.width + (double) s.width,
pixels.height + (double) s.height);
pixels = (Rectangle) Shapes2D.transform(MathTransforms.bidimensional(toSource), bounds, pixels);
ImageUtilities.clipBounds(source, pixels);
return ((PlanarImage) source).prefetch(ImageUtilities.pixelsToTiles(source, pixels));
} catch (TransformException e) {
recoverableException("prefetch", e);
}
return super.prefetch(tiles);
}
/**
* Compares the given object with this image for equality. This method returns {@code true}
* if the given object is non-null, is an instance of the exact same class as this image,
* has equal sources and do the same resampling operation (same interpolation method,
* same fill values, same coordinates).
*
* @param object the object to compare with this image.
* @return {@code true} if the given object is an image performing the same resampling as this image.
*/
@Override
public boolean equals(final Object object) {
if (equalsBase(object)) {
final ResampledImage other = (ResampledImage) object;
return minX == other.minX &&
minY == other.minY &&
width == other.width &&
height == other.height &&
minTileX == other.minTileX &&
minTileY == other.minTileY &&
interpolation.equals(other.interpolation) &&
Objects.deepEquals(fillValues, other.fillValues) &&
toSource.equals(other.toSource);
}
return false;
}
/**
* Returns a hash code value for this image.
*
* @return a hash code value based on a description of the operation performed by this image.
*/
@Override
public int hashCode() {
return hashCodeBase() + minX + 31*(minY + 31*(width + 31*height))
+ interpolation.hashCode() + toSource.hashCode();
}
}