sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] branch geoapi-4.0 updated: First implementation of ResampledImage for raster reprojection support. https://issues.apache.org/jira/browse/SIS-493
Date Wed, 25 Mar 2020 22:50:28 GMT
This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 3ccfaea  First implementation of ResampledImage for raster reprojection support.
https://issues.apache.org/jira/browse/SIS-493
3ccfaea is described below

commit 3ccfaea625427b8274025fb8b44d053229e3a6ca
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Wed Mar 25 23:49:41 2020 +0100

    First implementation of ResampledImage for raster reprojection support.
    https://issues.apache.org/jira/browse/SIS-493
---
 .../java/org/apache/sis/image/Interpolator.java    | 134 ++++++
 .../java/org/apache/sis/image/ResampledImage.java  | 457 +++++++++++++++++++++
 .../sis/internal/coverage/j2d/ImageUtilities.java  |  21 +
 .../org/apache/sis/internal/feature/Resources.java |   6 +
 .../sis/internal/feature/Resources.properties      |   1 +
 .../sis/internal/feature/Resources_fr.properties   |   1 +
 6 files changed, 620 insertions(+)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/Interpolator.java b/core/sis-feature/src/main/java/org/apache/sis/image/Interpolator.java
new file mode 100644
index 0000000..4d2ff94
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/Interpolator.java
@@ -0,0 +1,134 @@
+/*
+ * 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.awt.Dimension;
+import java.nio.DoubleBuffer;
+
+
+/**
+ * Algorithm for image interpolation (resampling). Interpolations are performed by sampling
on a regular grid
+ * of pixels using a local neighborhood. The sampling is performed by the {@link ResampledImage}
class, which
+ * gives the sample values to the {@code interpolate(…)} method of this interpolator.
+ *
+ * <p>This interface is designed for interpolations in a two-dimensional space only.</p>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @author  Johann Sorel (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public interface Interpolator {
+    /**
+     * Returns the size of the area over which the resampling function needs to provide values.
+     * Common values are:
+     *
+     * <table class="sis">
+     *   <caption>Common support sizes</caption>
+     *   <tr><th>Interpolation</th>    <th>Width</th> <th>Height</th></tr>
+     *   <tr><td>Nearest-neighbor</td> <td>1</td>     <td>1</td></tr>
+     *   <tr><td>Bilinear</td>         <td>2</td>     <td>2</td></tr>
+     *   <tr><td>Bicubic</td>          <td>4</td>     <td>4</td></tr>
+     *   <tr><td>Lanczos</td>          <td>4</td>     <td>4</td></tr>
+     * </table>
+     *
+     * @return number of sample values required for interpolations.
+     */
+    Dimension getSupportSize();
+
+    /**
+     * Interpolates sample values for all bands using the given pixel values in local neighborhood.
+     * The given {@code source} is a buffer with the number of elements shown below, where
+     * <var>support width</var> and <var>support height</var> are
given by {@link #getSupportSize()}:
+     *
+     * <blockquote>
+     * <var>(number of bands)</var> × <var>(support width)</var>
× <var>(support height)</var>
+     * </blockquote>
+     *
+     * Values in {@code source} buffer are always given with band index varying fastest,
then column index,
+     * then row index. Columns are traversed from left to right and rows are traversed from
top to bottom
+     * ({@link org.opengis.coverage.grid.SequenceType#LINEAR} iteration order).
+     *
+     * <p>The interpolation point is in the middle. For example if the {@linkplain
#getSupportSize() support size}
+     * is 4×4 pixels, then the interpolation point is the dot below and the fractional coordinates
are relative to
+     * the horizontal and vertical lines drawn below. This figure is for an image with only
one band, otherwise all
+     * indices between brackets would need to be multiplied by {@code numBands}.</p>
+     *
+     * {@preformat text
+     *   s[0]   s[1]   s[2]   s[3]
+     *
+     *   s[4]   s[5]───s[6]   s[7]  ← yfrac = 0
+     *           │   ●              ← yfrac given
+     *   s[8]   s[9]   s[10]  s[11] ← yfrac = 1
+     *
+     *   s[12]  s[13]  s[14]  s[15]
+     *               ↑
+     *             xfrac
+     * }
+     *
+     * <p><b>Output:</b> this method shall write the interpolation results
as {@code numBands} consecutive
+     * values in the supplied {@code writeTo} array, starting at {@code writeToOffset} index.</p>
+     *
+     * @param  source         pixel values from the source image to use for interpolation.
+     * @param  numBands       number of bands. This is the number of values to put in the
{@code writeTo} array.
+     * @param  xfrac          the X subsample position, usually (but not always) in the range
[0 … 1).
+     * @param  yfrac          the Y subsample position, usually (but not always) in the range
[0 … 1).
+     * @param  writeTo        the array where this method shall write interpolated values.
+     * @param  writeToOffset  index of the first value to put in the {@code writeTo} array.
+     * @return {@code true} on success, or {@code false} if the caller should paint some
fill value instead.
+     */
+    boolean interpolate(DoubleBuffer source, int numBands, double xfrac, double yfrac, double[]
writeTo, int writeToOffset);
+
+    /**
+     * A bilinear interpolation using 2×2 pixels.
+     * If the interpolation result is NaN, this method fallbacks on nearest-neighbor.
+     */
+    Interpolator BILINEAR = new Interpolator() {
+        /** Interpolation name for debugging purpose. */
+        @Override public String toString() {
+            return "BILINEAR";
+        }
+
+        /** Size of the area over which to provide values.*/
+        @Override public Dimension getSupportSize() {
+            return new Dimension(2,2);
+        }
+
+        /** Applies bilinear interpolation. */
+        @Override public boolean interpolate(final DoubleBuffer source, final int numBands,
+                final double xfrac, final double yfrac, final double[] writeTo, int writeToOffset)
+        {
+            final double mx = (1 - xfrac);
+            final double my = (1 - yfrac);
+            for (int b=0; b<numBands; b++) {
+                int p = source.position() + b;
+                double y = (source.get(p            )*mx + source.get(p += numBands)*xfrac)
* my
+                         + (source.get(p += numBands)*mx + source.get(p +  numBands)*xfrac)
* yfrac;
+                if (Double.isNaN(y)) {
+                    // Fallback on nearest-neighbor.
+                    p = source.position() + b;
+                    if (xfrac >= 0.5) p += numBands;
+                    if (yfrac >= 0.5) p += numBands*2;
+                    y = source.get(p);
+                }
+                writeTo[writeToOffset++] = y;
+            }
+            return true;
+        }
+    };
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/ResampledImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/ResampledImage.java
new file mode 100644
index 0000000..e57420f
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ResampledImage.java
@@ -0,0 +1,457 @@
+/*
+ * 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.nio.DoubleBuffer;
+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 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.internal.coverage.j2d.ImageLayout;
+import org.apache.sis.internal.coverage.j2d.ImageUtilities;
+import org.apache.sis.internal.feature.Resources;
+import org.apache.sis.internal.system.Modules;
+import org.apache.sis.util.ArgumentChecks;
+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.NumberRange;
+
+
+/**
+ * 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.1
+ *
+ * @see Interpolator
+ * @see java.awt.image.AffineTransformOp
+ *
+ * @since 1.1
+ * @module
+ */
+public class ResampledImage extends ComputedImage {
+    /**
+     * The {@value} value for identifying code expecting exactly 2 dimensions.
+     */
+    private static final int BIDIMENSIONAL = 2;
+
+    /**
+     * Domain of pixel coordinates in this image.
+     */
+    private final int minX, minY, width, height;
+
+    /**
+     * 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;
+
+    /**
+     * The object to use for performing interpolations.
+     */
+    protected final Interpolator interpolator;
+
+    /**
+     * The values to use if a pixel in this image can not 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. Can not be null.
+     */
+    private final Object fillValues;
+
+    /**
+     * Creates a new image which will resample the given image. The resampling operation
is defined
+     * by a 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}.
+     *
+     * @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  source        the image to be resampled.
+     * @param  interpolator  the object to use for performing interpolations.
+     * @param  fillValues    the values to use for pixels in this image that can not be mapped
to pixels in source image.
+     *                       The array length must be equal to the number of bands. If the
array is {@code null},
+     *                       the default value is zero in all bands. If any element in the
array is {@code null},
+     *                       the default value is zero for the corresponding band.
+     */
+    public ResampledImage(final Rectangle bounds, final MathTransform toSource, final RenderedImage
source,
+                          final Interpolator interpolator, final Number[] fillValues)
+    {
+        super(ImageLayout.DEFAULT.createCompatibleSampleModel(source), source);
+        ArgumentChecks.ensureNonNull("bounds", bounds);
+        ArgumentChecks.ensureNonNull("toSource", toSource);
+        ArgumentChecks.ensureNonNull("interpolator", interpolator);
+        ArgumentChecks.ensureStrictlyPositive("width",  width  = bounds.width);
+        ArgumentChecks.ensureStrictlyPositive("height", height = bounds.height);
+        minX = bounds.x;
+        minY = bounds.y;
+        /*
+         * 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.
+         */
+        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 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.
+         */
+        this.interpolator = interpolator;
+        final Dimension s = interpolator.getSupportSize();
+        final double[] offset = new double[numDim];
+        offset[0] = interpolationSupportOffset(s.width);
+        offset[1] = interpolationSupportOffset(s.height);
+        this.toSource = MathTransforms.concatenate(toSource, MathTransforms.translation(offset));
+        final int numBands = ImageUtilities.getNumBands(source);
+        if (fillValues != null && fillValues.length != numBands) {
+            throw new IllegalArgumentException(Resources.format(
+                    Resources.Keys.MismatchedArrayLengthForBands_3, "fillValues", numBands,
fillValues.length));
+        }
+        /*
+         * Copy the `fillValues` either as an `int[]` or `double[]` array, depending on
+         * whether the data type is an integer type or not. Null elements default to zero.
+         */
+        final int dataType = ImageUtilities.getDataType(source);
+        if (ImageUtilities.isIntegerType(dataType)) {
+            final int[] fill = new int[numBands];
+            if (fillValues != null) {
+                for (int i=0; i<numBands; i++) {
+                    final Number f = fillValues[i];
+                    if (f != null) fill[i] = f.intValue();
+                }
+            }
+            this.fillValues = fill;
+        } else {
+            final double[] fill = new double[numBands];
+            if (fillValues != null) {
+                for (int i=0; i<numBands; i++) {
+                    final Number f = fillValues[i];
+                    if (f != null) fill[i] = f.doubleValue();
+                }
+            }
+            this.fillValues = fill;
+        }
+    }
+
+    /**
+     * 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>
+     *
+     * @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.
+     */
+    private static double interpolationSupportOffset(final int span) {
+        return -Math.max(0, (span - 1) / 2);        // Round toward 0.
+    }
+
+    /**
+     * Verifies whether image layout information are consistent. This method performs all
verifications
+     * {@linkplain ComputedImage#verify() documented in parent class}, then 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.
+     *
+     * @return {@code null} if image layout information are consistent,
+     *         or the name of inconsistent attribute if a problem is found.
+     */
+    @Override
+    public String verify() {
+        String error = super.verify();
+        if (error == null && toSource instanceof MathTransform2D) try {
+            final Rectangle bounds = getBounds();
+            final Rectangle2D tb = Shapes2D.transform((MathTransform2D) toSource, bounds,
bounds);
+            if (!ImageUtilities.getBounds(getSource(0)).intersects(tb)) {
+                return "toSource";
+            }
+        } catch (TransformException e) {
+            Logging.recoverableException(Logging.getLogger(Modules.RASTER), getClass(), "verify",
e);
+            return "toSource";
+        }
+        return error;
+    }
+
+    /**
+     * Returns the same color model than the source image.
+     *
+     * @return the color model, or {@code null} if unspecified.
+     */
+    @Override
+    public ColorModel getColorModel() {
+        return getSource(0).getColorModel();
+    }
+
+    /**
+     * 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   = toSource.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 = interpolator.getSupportSize();
+            it = new PixelIterator.Builder().setWindowSize(support).create(getSource(0));
+            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 = xmax + support.width  - 0.5;         // Limit of coordinates where we
can interpolate.
+            ylim = ymax + support.height - 0.5;
+            xoff = interpolationSupportOffset(support.width)  - 0.5;    // Always negative.
+            yoff = interpolationSupportOffset(support.height) - 0.5;
+        }
+        /*
+         * 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[]`.
+         */
+        final double[] values;
+        final int[] intValues;
+        final Object valuesArray;
+        final long[] minValues, maxValues;
+        final boolean isInteger = (fillValues instanceof int[]);
+        if (isInteger) {
+            values      = new double[numBands];
+            intValues   = new int[scanline * numBands];
+            valuesArray = intValues;
+            final NumberRange<?>[] ranges = it.getSampleRanges();
+            minValues = new long[ranges.length];
+            maxValues = new long[ranges.length];
+            for (int i=0; i<ranges.length; i++) {
+                final NumberRange<?> r = ranges[i];
+                minValues[i] = r.getMinValue().longValue();
+                maxValues[i] = r.getMaxValue().longValue();
+            }
+        } 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
+         * can not 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;
+            }
+            toSource.transform(coordinates, 0, coordinates, 0, scanline);
+            /*
+             * 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 than 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, vi+=numBands) {
+                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.
+                                 */
+                                if (interpolator.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])));
+                                        }
+                                    }
+                                    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 are outside the source image bounds)
+                 * and no values have been set in the `values` or `intValues` array.
+                 */
+                System.arraycopy(fillValues, 0, valuesArray, vi, numBands);
+            }
+            /*
+             * At this point we finished to compute the value of a scanline.
+             * Copy to its final destination then move to next line.
+             */
+            if (isInteger) {
+                tile.setPixels(tileMinX, ty, scanline, 1, intValues);
+            } else {
+                tile.setPixels(tileMinX, ty, scanline, 1, values);
+            }
+        }
+        return tile;
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java
index ce4d1b9..584895b 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java
@@ -157,6 +157,9 @@ public final class ImageUtilities extends Static {
      *
      * @param  image  the image for which to get the data type, or {@code null}.
      * @return the data type, or {@link DataBuffer#TYPE_UNDEFINED} if unknown.
+     *
+     * @see #getDataType(Raster)
+     * @see #isIntegerType(int)
      */
     public static int getDataType(final RenderedImage image) {
         if (image != null) {
@@ -171,6 +174,9 @@ public final class ImageUtilities extends Static {
      *
      * @param  raster  the raster for which to get the data type, or {@code null}.
      * @return the data type, or {@link DataBuffer#TYPE_UNDEFINED} if unknown.
+     *
+     * @see #getDataType(RenderedImage)
+     * @see #isIntegerType(int)
      */
     public static int getDataType(final Raster raster) {
         if (raster != null) {
@@ -360,6 +366,21 @@ public final class ImageUtilities extends Static {
     }
 
     /**
+     * Returns {@code true} if the given data buffer type is an integer type.
+     * Returns {@code false} if the type is a floating point type or in case
+     * of doubt (e.g. for {@link DataBuffer#TYPE_UNDEFINED}).
+     *
+     * @param  dataType  one of {@link DataBuffer} constants.
+     * @return whether the given constant is for an integer type.
+     *
+     * @see #getDataType(RenderedImage)
+     * @see #getDataType(Raster)
+     */
+    public static boolean isIntegerType(final int dataType) {
+        return dataType >= DataBuffer.TYPE_BYTE && dataType <= DataBuffer.TYPE_INT;
+    }
+
+    /**
      * Suggests the height of a transfer region for a tile of the given size. The given region
should be
      * contained inside {@link Raster#getBounds()}. This method modifies {@link Rectangle#height}
in-place.
      * The {@link Rectangle#width} value is never modified, so caller can iterate on all
raster rows without
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.java
b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.java
index 5a790bf..257ab9b 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.java
@@ -248,6 +248,12 @@ public final class Resources extends IndexedResourceBundle {
         public static final short IterationNotStarted = 37;
 
         /**
+         * The “{0}” array length is {2,number} while {1,number} was expected for matching
the number
+         * of bands.
+         */
+        public static final short MismatchedArrayLengthForBands_3 = 73;
+
+        /**
          * Image number of bands {0,number} does not match the number of sample dimensions
          * ({1,number}).
          */
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.properties
b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.properties
index 3cb001c..836b71b 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.properties
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.properties
@@ -56,6 +56,7 @@ InsufficientBufferCapacity_3      = Data buffer capacity is insufficient
for a g
 InvalidExpression_2               = Invalid or unsupported \u201c{1}\u201d expression at
index {0}.
 IterationIsFinished               = Iteration is finished.
 IterationNotStarted               = Iteration did not started.
+MismatchedArrayLengthForBands_3   = The \u201c{0}\u201d array length is {2,number} while
{1,number} was expected for matching the number of bands.
 MismatchedBandCount_2             = Image number of bands {0,number} does not match the number
of sample dimensions ({1,number}).
 MismatchedBandSize                = The bands have different number of sample values.
 MismatchedDataType                = The bands store sample values using different data types.
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources_fr.properties
b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources_fr.properties
index 324af81..06c12c8 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources_fr.properties
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources_fr.properties
@@ -61,6 +61,7 @@ InsufficientBufferCapacity_3      = La capacit\u00e9 du buffer est insuffisante
 InvalidExpression_2               = Expression \u00ab\u202f{1}\u202f\u00bb invalide ou non-support\u00e9e
\u00e0 l\u2019index {0}.
 IterationIsFinished               = L\u2019it\u00e9ration est termin\u00e9e.
 IterationNotStarted               = L\u2019it\u00e9ration n\u2019a pas commenc\u00e9e.
+MismatchedArrayLengthForBands_3   = La longueur du tableau \u00ab\u202f{0}\u202f\u00bb est
{2,number} alors qu\u2019on attendait {1,number} pour correspondre au nombre de bandes.
 MismatchedBandCount_2             = Le nombre de bandes de l\u2019image ({0,number}) ne correspond
pas au nombre de dimensions d\u2019\u00e9chantillonnage ({1,number}).
 MismatchedBandSize                = Les bandes ont un nombre diff\u00e9rent de valeurs.
 MismatchedDataType                = Les bandes stockent leurs valeurs en utilisant des types
de donn\u00e9es diff\u00e9rents.


Mime
View raw message