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 version of ImageCombiner.
Date Thu, 20 Aug 2020 18:19:37 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 6ff9dd1  First version of ImageCombiner.
6ff9dd1 is described below

commit 6ff9dd197675124f7371904f70dea6a0d04f7937
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Thu Aug 20 20:19:10 2020 +0200

    First version of ImageCombiner.
---
 .../java/org/apache/sis/image/ComputedImage.java   | 100 +++++++-
 .../java/org/apache/sis/image/ImageCombiner.java   | 270 +++++++++++++++++++++
 .../java/org/apache/sis/image/ImageProcessor.java  |  40 ++-
 .../java/org/apache/sis/image/ResampledImage.java  |  42 +++-
 .../java/org/apache/sis/image/Visualization.java   |   2 +-
 .../sis/internal/coverage/j2d/ImageLayout.java     |  11 +
 .../sis/image/BandedSampleConverterTest.java       |   8 +-
 .../org/apache/sis/image/ImageCombinerTest.java    | 161 ++++++++++++
 .../java/org/apache/sis/image/ImageTestCase.java   |   2 +-
 .../org/apache/sis/image/ResampledImageTest.java   |   2 +-
 .../java/org/apache/sis/image/TiledImageMock.java  |  13 +-
 .../java/org/apache/sis/test/FeatureAssert.java    |   2 +-
 .../apache/sis/test/suite/FeatureTestSuite.java    |   1 +
 13 files changed, 635 insertions(+), 19 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java
index 4d32f7d..2309976 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java
@@ -37,6 +37,7 @@ import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.Disposable;
 import org.apache.sis.util.Exceptions;
+import org.apache.sis.util.resources.Errors;
 import org.apache.sis.coverage.grid.GridExtent;     // For javadoc
 import org.apache.sis.internal.feature.Resources;
 
@@ -161,6 +162,39 @@ public abstract class ComputedImage extends PlanarImage implements Disposable
{
     private final RenderedImage[] sources;
 
     /**
+     * If the computed image shall be written in an existing image, that image. Otherwise
{@code null}.
+     * If non-null, the sample model of this image shall be equal to {@link #sampleModel}
and the tile
+     * indices &amp; pixel coordinate systems shall be aligned.
+     *
+     * <p>The destination image may be larger or smaller than this {@code ComputedImage},
by containing
+     * more or less tiles (the presence or absence of a tile is a "all or nothing" decision).
When this
+     * class needs to compute a tile, one of the following choices is executed:</p>
+     *
+     * <ul class="verbose">
+     *   <li>If the ({@code tileX}, {@code tileY}) indices of the tile to compute are
valid tile indices of
+     *       {@code destination} image, then the {@linkplain WritableRenderedImage#getWritableTile(int,int)
+     *       destination tile is acquired}, given to {@link #computeTile(int, int, WritableRaster)}
method
+     *       and finally {@linkplain WritableRenderedImage#releaseWritableTile(int,int) released}.</li>
+     *   <li>Otherwise {@link #computeTile(int, int, WritableRaster)} is invoked with
a {@code null} tile.</li>
+     * </ul>
+     *
+     * If this field is set to a non-null value, then this assignment should be done
+     * soon after construction time before any tile computation started.
+     *
+     * <div class="note"><b>Note on interaction with tile cache</b><br>
+     * The use of a destination image may produce unexpected result if {@link #computeTile(int,
int, WritableRaster)}
+     * is invoked two times or more for the same destination tile. It may look like a problem
because computed tiles
+     * can be discarded and recomputed at any time. However this problem should not happen
because tiles computed by
+     * this {@code ComputedImage} will not be discarded as long as {@code destination} has
a reference to that tile.
+     * If a {@code ComputedImage} tile has been discarded, then it implies that the corresponding
{@code destination}
+     * tile has been discarded as well, in which case the tile computation will restart from
scratch; it will not be
+     * a recomputation of only this {@code ComputedImage} on top of an old {@code destination}
tile.</div>
+     *
+     * @see #setDestination(WritableRenderedImage)
+     */
+    private WritableRenderedImage destination;
+
+    /**
      * The sample model shared by all tiles in this image.
      * The {@linkplain SampleModel#getWidth() sample model width}
      * determines this {@linkplain #getTileWidth() image tile width},
@@ -233,6 +267,39 @@ public abstract class ComputedImage extends PlanarImage implements Disposable
{
     }
 
     /**
+     * Sets an existing image where to write the computation result. The sample model of
specified image shall
+     * be equal to {@link #sampleModel} and the tile indices &amp; pixel coordinate systems
shall be aligned.
+     * However the target image may be larger or smaller than this {@code ComputedImage},
by containing more
+     * or less tiles (the presence or absence of a tile is a "all or nothing" decision).
When this class needs
+     * to compute a tile, one of the following choices is executed:
+     *
+     * <ul class="verbose">
+     *   <li>If the ({@code tileX}, {@code tileY}) indices of the tile to compute are
valid tile indices
+     *       of {@code target} image, then the {@linkplain WritableRenderedImage#getWritableTile(int,int)
+     *       destination tile is acquired}, given to {@link #computeTile(int, int, WritableRaster)}
method
+     *       and finally {@linkplain WritableRenderedImage#releaseWritableTile(int,int) released}.</li>
+     *   <li>Otherwise {@link #computeTile(int, int, WritableRaster)} is invoked with
a {@code null} tile.</li>
+     * </ul>
+     *
+     * If this method is invoked, then is should be done soon after construction time
+     * before any tile computation starts.
+     */
+    final void setDestination(final WritableRenderedImage target) {
+        if (destination != null) {
+            throw new IllegalStateException(Errors.format(Errors.Keys.AlreadyInitialized_1,
"destination"));
+        }
+        if (!sampleModel.equals(target.getSampleModel())) {
+            throw new IllegalArgumentException(Resources.format(Resources.Keys.MismatchedSampleModel));
+        }
+        if (target.getTileGridXOffset() != getTileGridXOffset() ||
+            target.getTileGridYOffset() != getTileGridYOffset())
+        {
+            throw new IllegalArgumentException(Resources.format(Resources.Keys.MismatchedTileGrid));
+        }
+        destination = target;
+    }
+
+    /**
      * Returns the source at the given index.
      *
      * @param  index  index of the desired source.
@@ -371,6 +438,12 @@ public abstract class ComputedImage extends PlanarImage implements Disposable
{
         final Cache<TileCache.Key,Raster> cache = TileCache.GLOBAL;
         Raster tile = cache.peek(key);
         if (tile == null || reference.isTileDirty(key)) {
+            /*
+             * Tile not found in the cache or in need to be recomputed. Validate given arguments
+             * only now (if the tile was found, it would have implied that the indices are
valid).
+             * We will need to check the cache again after we got the lock in case computation
has
+             * happened in the short time between above check and lock acquisition.
+             */
             int min;
             ArgumentChecks.ensureBetween("tileX", (min = getMinTileX()), min + getNumXTiles()
- 1, tileX);
             ArgumentChecks.ensureBetween("tileY", (min = getMinTileY()), min + getNumYTiles()
- 1, tileY);
@@ -380,12 +453,37 @@ public abstract class ComputedImage extends PlanarImage implements Disposable
{
                 tile = handler.peek();
                 final boolean marked = reference.trySetComputing(key);              // May
throw ImagingOpException.
                 if (marked || tile == null) {
-                    final WritableRaster previous = (tile instanceof WritableRaster) ? (WritableRaster)
tile : null;
+                    /*
+                     * The requested tile needs to be computed. If a destination image has
been specified
+                     * and the tile indices are valid for that destination, we will use the
tile provided
+                     * by that destination. The write operation shall happen between `getWritableTile(…)`
+                     * and `releaseWritableTile(…)` method calls.
+                     */
+                    final WritableRenderedImage destination = this.destination;     // Protect
from change (paranoiac).
+                    final boolean writeInDestination = (destination != null)
+                            && (tileX >= (min = destination.getMinTileX()) &&
tileX < min + destination.getNumXTiles())
+                            && (tileY >= (min = destination.getMinTileY()) &&
tileY < min + destination.getNumYTiles());
+
+                    final WritableRaster previous;
+                    if (writeInDestination) {
+                        previous = destination.getWritableTile(tileX, tileY);
+                    } else if (tile instanceof WritableRaster) {
+                        previous = (WritableRaster) tile;
+                    } else {
+                        previous = null;
+                    }
+                    /*
+                     * Actual computation.
+                     */
                     try {
                         tile = computeTile(tileX, tileY, previous);
                     } catch (Exception e) {
                         tile = null;
                         error = Exceptions.unwrap(e);
+                    } finally {
+                        if (writeInDestination) {
+                            destination.releaseWritableTile(tileX, tileY);
+                        }
                     }
                     if (marked) {
                         reference.endWrite(key, error == null);
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/ImageCombiner.java b/core/sis-feature/src/main/java/org/apache/sis/image/ImageCombiner.java
new file mode 100644
index 0000000..c503e0e
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ImageCombiner.java
@@ -0,0 +1,270 @@
+/*
+ * 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.Point;
+import java.awt.Rectangle;
+import java.awt.image.Raster;
+import java.awt.image.SampleModel;
+import java.awt.image.RenderedImage;
+import java.awt.image.WritableRenderedImage;
+import java.util.function.Consumer;
+import javax.measure.Quantity;
+import org.opengis.referencing.operation.MathTransform;
+import org.apache.sis.internal.coverage.j2d.ImageLayout;
+import org.apache.sis.internal.coverage.j2d.ImageUtilities;
+import org.apache.sis.internal.coverage.j2d.TileOpExecutor;
+import org.apache.sis.internal.util.Numerics;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.measure.Units;
+
+
+/**
+ * Combines an arbitrary amount of images into a single one.
+ * The combined mages may use different coordinate systems if a resampling operation is specified.
+ * The workflow is as below:
+ *
+ * <ol>
+ *   <li>Creates an {@code ImageCombiner} with the destination image where to write.</li>
+ *   <li>Configure with methods such as {@link #setInterpolation setInterpolation(…)}.</li>
+ *   <li>Invoke {@link #accept accept(…)} or {@link #resample resample(…)}
+ *       methods for each image to combine.</li>
+ *   <li>Get the combined image with {@link #result()}.</li>
+ * </ol>
+ *
+ * Images are combined in the order they are specified.
+ * If the same pixel is written by many images, then the final value is the pixel of the
last image specified.
+ * In current implementation, the last pixel values win even if those pixels are transparent
+ * (i.e. {@code ImageCombiner} does not yet handle alpha values).
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public class ImageCombiner implements Consumer<RenderedImage> {
+    /**
+     * The image processor for resampling operation.
+     */
+    private final ImageProcessor processor;
+
+    /**
+     * The destination image where to write the images given to this {@code ImageCombiner}.
+     */
+    private final WritableRenderedImage destination;
+
+    /**
+     * Creates an image combiner which will write in the given image. That image is not cleared;
+     * pixels that are not overwritten by another image given to {@code ImageCombiner} methods
+     * will be left unchanged.
+     *
+     * @param  destination  the image where to combine images.
+     */
+    public ImageCombiner(final WritableRenderedImage destination) {
+        ArgumentChecks.ensureNonNull("destination", destination);
+        this.destination = destination;
+        processor = new ImageProcessor();
+        processor.setImageLayout(new Layout(destination.getSampleModel()));
+    }
+
+    /**
+     * Provides sample model of images created by resample operations.
+     * It must be the sample model of destination image, with the same tile size.
+     */
+    private static final class Layout extends ImageLayout {
+        /** Sample model of destination image. */
+        private final SampleModel sampleModel;
+
+        /** Indices of the first tile ({@code minTileX}, {@code minTileY}). */
+        final Point minTile;
+
+        /** Creates a new layout which will request the specified sample model. */
+        Layout(final SampleModel sampleModel) {
+            super(null);
+            ArgumentChecks.ensureNonNull("sampleModel", sampleModel);
+            this.sampleModel = sampleModel;
+            minTile = new Point();
+        }
+
+        /** Returns the target sample model for {@link ResampledImage} or other operations.
*/
+        @Override public SampleModel createCompatibleSampleModel(RenderedImage image, Rectangle
bounds) {
+            return sampleModel;
+        }
+
+        /** Returns indices of the first tile, which must have been set in the {@link #minTile}
field in advance. */
+        @Override public Point getMinTile() {
+            return minTile;
+        }
+    }
+
+    /**
+     * Returns the interpolation method to use during resample operations.
+     *
+     * @return interpolation method to use during resample operations.
+     *
+     * @see #resample(RenderedImage, Rectangle, MathTransform)
+     */
+    public Interpolation getInterpolation() {
+        return processor.getInterpolation();
+    }
+
+    /**
+     * Sets the interpolation method to use during resample operations.
+     *
+     * @param  method  interpolation method to use during resample operations.
+     *
+     * @see #resample(RenderedImage, Rectangle, MathTransform)
+     */
+    public void setInterpolation(final Interpolation method) {
+        processor.setInterpolation(method);
+    }
+
+    /**
+     * Returns hints about the desired positional accuracy, in "real world" units or in pixel
units.
+     * If the returned array is non-empty and contains accuracies large enough,
+     * {@code ImageCombiner} may use some slightly faster algorithms at the expense of accuracy.
+     *
+     * @return desired accuracy in no particular order, or an empty array if none.
+     *
+     * @see ImageProcessor#getPositionalAccuracyHints()
+     */
+    public Quantity<?>[] getPositionalAccuracyHints() {
+        return processor.getPositionalAccuracyHints();
+    }
+
+    /**
+     * Sets hints about desired positional accuracy, in "real world" units or in pixel units.
+     * Accuracy can be specified in real world units such as {@linkplain Units#METRE metres}
+     * or in {@linkplain Units#PIXEL pixel units}, which are converted to real world units
depending
+     * on image resolution. If more than one value is applicable to a dimension
+     * (after unit conversion if needed), the smallest value is taken.
+     *
+     * @param  hints  desired accuracy in no particular order, or a {@code null} array if
none.
+     *                Null elements in the array are ignored.
+     *
+     * @see ImageProcessor#setPositionalAccuracyHints(Quantity...)
+     */
+    public void setPositionalAccuracyHints(final Quantity<?>... hints) {
+        processor.setPositionalAccuracyHints(hints);
+    }
+
+    /**
+     * Writes the given image on top of destination image. The given source image shall use
the same pixel
+     * coordinate system than the destination image (but not necessarily the same tile indices).
+     * For every (<var>x</var>,<var>y</var>) pixel coordinates in
the destination image:
+     *
+     * <ul>
+     *   <li>If (<var>x</var>,<var>y</var>) are valid {@code
source} pixel coordinates,
+     *       then the source pixel values overwrite the destination pixel values.</li>
+     *   <li>Otherwise the destination pixel is left unchanged.</li>
+     * </ul>
+     *
+     * Note that source pixels overwrite destination pixels even if they are transparent
+     * (i.e. {@code ImageCombiner} does not yet handle alpha values).
+     *
+     * @param  source  the image to write on top of destination image.
+     */
+    @Override
+    public void accept(final RenderedImage source) {
+        ArgumentChecks.ensureNonNull("source", source);
+        final WritableRenderedImage destination = this.destination;
+        final Rectangle bounds = ImageUtilities.getBounds(source);
+        ImageUtilities.clipBounds(destination, bounds);
+        final TileOpExecutor executor = new TileOpExecutor(source, bounds) {
+            @Override protected void readFrom(final Raster tile) {
+                destination.setData(tile);
+            }
+        };
+        executor.readFrom(processor.prefetch(source, bounds));
+    }
+
+    /**
+     * Combines the result of resampling the given image. The resampling operation is defined
by a potentially
+     * non-linear transform from the <em>destination</em> image to the specified
<em>source</em> image.
+     * That transform should map {@linkplain org.opengis.referencing.datum.PixelInCell#CELL_CENTER
pixel centers}.
+     *
+     * <h4>Properties used</h4>
+     * This operation uses the following properties in addition to method parameters:
+     * <ul>
+     *   <li>{@linkplain #getInterpolation() Interpolation method} (nearest neighbor,
bilinear, <i>etc</i>).</li>
+     *   <li>{@linkplain #getPositionalAccuracyHints() Positional accuracy hints}
+     *       for enabling faster resampling at the cost of lower precision.</li>
+     * </ul>
+     *
+     * Contrarily to {@link ImageProcessor}, this method does not use {@linkplain ImageProcessor#getFillValues()
fill values}.
+     * Destination pixels that can not be mapped to source pixels are left unchanged.
+     *
+     * @param  source    the image to be resampled.
+     * @param  bounds    domain of pixel coordinates in the destination image.
+     * @param  toSource  conversion of pixel coordinates from destination image to {@code
source} image.
+     *
+     * @see ImageProcessor#resample(RenderedImage, Rectangle, MathTransform)
+     */
+    public void resample(final RenderedImage source, Rectangle bounds, final MathTransform
toSource) {
+        final int  tileWidth       = destination.getTileWidth();
+        final int  tileHeight      = destination.getTileHeight();
+        final long tileGridXOffset = destination.getTileGridXOffset();
+        final long tileGridYOffset = destination.getTileGridYOffset();
+        final int  minTileX        = Math.toIntExact(Math.floorDiv((bounds.x - tileGridXOffset),
tileWidth));
+        final int  minTileY        = Math.toIntExact(Math.floorDiv((bounds.y - tileGridYOffset),
tileHeight));
+        final int  minX            = Math.toIntExact((minTileX * (long) tileWidth)  + tileGridXOffset);
+        final int  minY            = Math.toIntExact((minTileY * (long) tileHeight) + tileGridYOffset);
+        /*
+         * Exand the target bounds until it contains an integer number of tiles, computed
using the size
+         * of destination tiles. We have to do that because the resample operation below
is not free to
+         * choose a tile size suiting the given bounds.
+         */
+        long maxX = (bounds.x + (long) bounds.width)  - 1;                             //
Inclusive.
+        long maxY = (bounds.y + (long) bounds.height) - 1;
+        maxX = Numerics.ceilDiv((maxX - tileGridXOffset), tileWidth) * tileWidth  + tileGridXOffset;
+        maxY = Numerics.ceilDiv((maxY - tileGridYOffset), tileWidth) * tileHeight + tileGridYOffset;
+        bounds = new Rectangle(minX, minY,
+                Math.toIntExact(maxX - minX + 1),
+                Math.toIntExact(maxY - minY + 1));
+        /*
+         * Values of (minTileX, minTileY) computed above will cause `ResampledImage.getTileGridOffset()`
+         * to return the exact same value than `destination.getTileGridOffset()`. This is
a requirement
+         * of `setDestination(…)` method.
+         */
+        final RenderedImage result;
+        synchronized (processor) {
+            final Point minTile = ((Layout) processor.getImageLayout()).minTile;
+            minTile.x = minTileX;
+            minTile.y = minTileY;
+            result = processor.resample(source, bounds, toSource);
+        }
+        if (result instanceof ComputedImage) {
+            ((ComputedImage) result).setDestination(destination);
+            processor.prefetch(result, ImageUtilities.getBounds(destination));
+        } else {
+            accept(result);
+        }
+    }
+
+    /**
+     * Returns the combination of destination image with all images specified to {@code ImageCombiner}
methods.
+     * This may be the destination image specified at construction time, but may also be
a larger image if the
+     * destination has been dynamically expanded for accommodating larger sources.
+     *
+     * <p><b>Note:</b> dynamic expansion is not yet implemented in current
version.</p>
+     *
+     * @return the combination of destination image with all source images.
+     */
+    public RenderedImage result() {
+        return destination;
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java b/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java
index e36a6c8..eb95181 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java
@@ -146,6 +146,14 @@ public class ImageProcessor implements Cloneable {
     }
 
     /**
+     * Properties (size, tile size, sample model, <i>etc.</i>) of destination
images.
+     *
+     * @see #getImageLayout()
+     * @see #setImageLayout(ImageLayout)
+     */
+    private ImageLayout layout;
+
+    /**
      * Interpolation to use during resample operations.
      *
      * @see #getInterpolation()
@@ -276,12 +284,30 @@ public class ImageProcessor implements Cloneable {
      * The execution mode is initialized to {@link Mode#DEFAULT} and the error action to
{@link ErrorAction#THROW}.
      */
     public ImageProcessor() {
+        layout        = ImageLayout.DEFAULT;
         executionMode = Mode.DEFAULT;
         errorAction   = ErrorAction.THROW;
         interpolation = Interpolation.BILINEAR;
     }
 
     /**
+     * Returns the properties (size, tile size, sample model, <i>etc.</i>) of
destination images.
+     * This method is not yet public because {@link ImageLayout} is not a public class.
+     */
+    final synchronized ImageLayout getImageLayout() {
+        return layout;
+    }
+
+    /**
+     * Sets the properties (size, tile size, sample model, <i>etc.</i>) of destination
images.
+     * This method is not yet public because {@link ImageLayout} is not a public class.
+     */
+    final synchronized void setImageLayout(final ImageLayout layout) {
+        ArgumentChecks.ensureNonNull("layout", layout);
+        this.layout = layout;
+    }
+
+    /**
      * Returns the interpolation method to use during resample operations.
      *
      * @return interpolation method to use during resample operations.
@@ -735,8 +761,12 @@ public class ImageProcessor implements Cloneable {
         for (int i=0; i<converters.length; i++) {
             ArgumentChecks.ensureNonNullElement("converters", i, converters[i]);
         }
+        final ImageLayout   layout;
+        synchronized (this) {
+            layout = this.layout;
+        }
         // No need to clone `sourceRanges` because it is not stored by `BandedSampleConverter`.
-        return unique(BandedSampleConverter.create(source, ImageLayout.DEFAULT,
+        return unique(BandedSampleConverter.create(source, layout,
                 sourceRanges, converters, targetType.ordinal(), colorModel));
     }
 
@@ -800,16 +830,18 @@ public class ImageProcessor implements Cloneable {
              * All accesses to ImageProcessor fields done by this method should be isolated
in this single
              * synchronized block. All arrays are "copy on write", so they do not need to
be cloned.
              */
+            final ImageLayout   layout;
             final Interpolation interpolation;
             final Number[]      fillValues;
             final Quantity<?>[] positionalAccuracyHints;
             synchronized (this) {
+                layout                  = this.layout;
                 interpolation           = this.interpolation;
                 fillValues              = this.fillValues;
                 positionalAccuracyHints = this.positionalAccuracyHints;
             }
             resampled = unique(new ResampledImage(source,
-                    ImageLayout.DEFAULT.createCompatibleSampleModel(source, bounds),
+                    layout.createCompatibleSampleModel(source, bounds), layout.getMinTile(),
                     bounds, toSource, interpolation, fillValues, positionalAccuracyHints));
             break;
         }
@@ -988,15 +1020,17 @@ public class ImageProcessor implements Cloneable {
     final RenderedImage resampleAndConvert(final RenderedImage source, final MathTransform
toSource,
             final MathTransform1D[] converters, final Rectangle bounds, final ColorModel
colorModel)
     {
+        final ImageLayout   layout;
         final Interpolation interpolation;
         final Number[]      fillValues;
         final Quantity<?>[] positionalAccuracyHints;
         synchronized (this) {
+            layout                  = this.layout;
             interpolation           = this.interpolation;
             fillValues              = this.fillValues;
             positionalAccuracyHints = this.positionalAccuracyHints;
         }
-        return unique(new Visualization(source, ImageLayout.DEFAULT, bounds, toSource, toSource.isIdentity(),
+        return unique(new Visualization(source, layout, bounds, toSource, toSource.isIdentity(),
                       interpolation, converters, fillValues, colorModel, positionalAccuracyHints));
     }
 
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
index d00ceb1..e8652f9 100644
--- 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
@@ -20,6 +20,7 @@ import java.util.Arrays;
 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;
@@ -102,6 +103,11 @@ public class ResampledImage extends ComputedImage {
     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).
@@ -180,6 +186,7 @@ public class ResampledImage extends ComputedImage {
      *
      * @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.
@@ -194,9 +201,9 @@ public class ResampledImage extends ComputedImage {
      *
      * @see ImageProcessor#resample(RenderedImage, Rectangle, MathTransform)
      */
-    protected ResampledImage(final RenderedImage source, final SampleModel sampleModel, final
Rectangle bounds,
-            final MathTransform toSource, final Interpolation interpolation, final Number[]
fillValues,
-            final Quantity<?>[] accuracy)
+    protected ResampledImage(final RenderedImage source, final SampleModel sampleModel, final
Point minTile,
+            final Rectangle bounds, final MathTransform toSource, final Interpolation interpolation,
+            final Number[] fillValues, final Quantity<?>[] accuracy)
     {
         super(sampleModel, source);
         if (source.getWidth() <= 0 || source.getHeight() <= 0) {
@@ -207,6 +214,13 @@ public class ResampledImage extends ComputedImage {
         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
@@ -504,6 +518,28 @@ public class ResampledImage extends ComputedImage {
     }
 
     /**
+     * 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.
      *
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java b/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java
index 3852859..62ba955 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java
@@ -95,7 +95,7 @@ final class Visualization extends ResampledImage {
     {
         super(source,
               layout.createBandedSampleModel(Colorizer.TYPE_COMPACT, converters.length, source,
bounds),
-              (bounds != null) ? bounds : ImageUtilities.getBounds(source),
+              layout.getMinTile(), (bounds != null) ? bounds : ImageUtilities.getBounds(source),
               toSource,
               isIdentity ? Interpolation.NEAREST : combine(interpolation, converters),
               fillValues,
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageLayout.java
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageLayout.java
index a76b089..449b1de 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageLayout.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageLayout.java
@@ -17,6 +17,7 @@
 package org.apache.sis.internal.coverage.j2d;
 
 import java.util.Arrays;
+import java.awt.Point;
 import java.awt.Dimension;
 import java.awt.Rectangle;
 import java.awt.image.ColorModel;
@@ -274,6 +275,16 @@ public class ImageLayout {
     }
 
     /**
+     * Returns indices of the first tile ({@code minTileX}, {@code minTileY}), or {@code
null} for (0,0).
+     * The default implementation returns {@code null}.
+     *
+     * @return indices of the first tile ({@code minTileX}, {@code minTileY}), or {@code
null} for (0,0).
+     */
+    public Point getMinTile() {
+        return null;
+    }
+
+    /**
      * Returns a string representation for debugging purpose.
      *
      * @return a string representation for debugging purpose.
diff --git a/core/sis-feature/src/test/java/org/apache/sis/image/BandedSampleConverterTest.java
b/core/sis-feature/src/test/java/org/apache/sis/image/BandedSampleConverterTest.java
index 526064f..df355f9 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/image/BandedSampleConverterTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/image/BandedSampleConverterTest.java
@@ -22,7 +22,6 @@ import org.opengis.referencing.operation.MathTransform1D;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.internal.coverage.j2d.ImageLayout;
 import org.apache.sis.test.TestUtilities;
-import org.apache.sis.test.TestCase;
 import org.junit.Test;
 
 import static org.apache.sis.test.FeatureAssert.assertValuesEqual;
@@ -36,7 +35,7 @@ import static org.apache.sis.test.FeatureAssert.assertValuesEqual;
  * @since   1.1
  * @module
  */
-public final strictfp class BandedSampleConverterTest extends TestCase {
+public final strictfp class BandedSampleConverterTest extends ImageTestCase {
     /**
      * Size of tiles in this test. The width should be different than the height
      * for increasing the chances to detect errors in index calculations.
@@ -44,11 +43,6 @@ public final strictfp class BandedSampleConverterTest extends TestCase
{
     private static final int TILE_WIDTH = 4, TILE_HEIGHT = 3;
 
     /**
-     * The image to test.
-     */
-    private BandedSampleConverter image;
-
-    /**
      * Creates a converted image with arbitrary tiles.
      * The created image is assigned to the {@link #image} field.
      *
diff --git a/core/sis-feature/src/test/java/org/apache/sis/image/ImageCombinerTest.java b/core/sis-feature/src/test/java/org/apache/sis/image/ImageCombinerTest.java
new file mode 100644
index 0000000..61dfefa
--- /dev/null
+++ b/core/sis-feature/src/test/java/org/apache/sis/image/ImageCombinerTest.java
@@ -0,0 +1,161 @@
+/*
+ * 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.Rectangle;
+import java.awt.image.DataBuffer;
+import java.awt.image.RenderedImage;
+import org.opengis.referencing.operation.MathTransform;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.junit.Test;
+
+import static org.apache.sis.test.FeatureAssert.*;
+
+
+/**
+ * Tests {@link ImageCombiner}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public final strictfp class ImageCombinerTest extends ImageTestCase {
+    /**
+     * The image to add to the {@link ImageCombiner}.
+     */
+    private PlanarImage toAdd;
+
+    /**
+     * Creates a rendered image with arbitrary tiles.
+     */
+    private ImageCombiner initialize() {
+        final TiledImageMock destination = new TiledImageMock(
+                DataBuffer.TYPE_USHORT, 1,      // dataType, numBands
+                 3,  4,                         // minX, minY
+                12,  8,                         // width, height
+                 4,  4,                         // tileWidth, tileHeight
+                -2,  3);                        // minTileX, minTileY
+        /*
+         * An image intersecting the destination, with a small part outside.
+         * Intentionally use a different data type and different tile layout.
+         */
+        final TiledImageMock source = new TiledImageMock(
+                DataBuffer.TYPE_FLOAT, 1,       // dataType, numBands
+                 5,  3,                         // minX, minY
+                 9,  6,                         // width, height
+                 3,  2,                         // tileWidth, tileHeight
+                 5,  9);                        // minTileX, minTileY
+
+        source.validate();
+        source.initializeAllTiles(0);
+        destination.validate();
+        destination.initializeAllTiles(0);
+        toAdd = source;
+        return new ImageCombiner(destination.toWritableTiledImage());
+    }
+
+    /**
+     * Tests {@link ImageCombiner#accept(RenderedImage)}.
+     */
+    @Test
+    public void testAccept() {
+        final ImageCombiner combiner = initialize();
+        /*
+         * Verify initial state, before combine operation.
+         * We expect 3×2 tiles, numbered as below:
+         *
+         *    1xy, 2xy, 3xy,
+         *    4xy, 5xy, 6xy
+         *
+         * where x and y are pixel coordinates in a single tile of size 4×4.
+         */
+        assertValuesEqual(image = combiner.result(), 0, new double[][] {
+            {100, 101, 102, 103, 200, 201, 202, 203, 300, 301, 302, 303},
+            {110, 111, 112, 113, 210, 211, 212, 213, 310, 311, 312, 313},
+            {120, 121, 122, 123, 220, 221, 222, 223, 320, 321, 322, 323},
+            {130, 131, 132, 133, 230, 231, 232, 233, 330, 331, 332, 333},
+            {400, 401, 402, 403, 500, 501, 502, 503, 600, 601, 602, 603},
+            {410, 411, 412, 413, 510, 511, 512, 513, 610, 611, 612, 613},
+            {420, 421, 422, 423, 520, 521, 522, 523, 620, 621, 622, 623},
+            {430, 431, 432, 433, 530, 531, 532, 533, 630, 631, 632, 633},
+        });
+        /*
+         * Verify source image, before combine operation.
+         * We expect 3×3 tiles, numbered as below:
+         *
+         *    1xy, 2xy, 3xy,
+         *    4xy, 5xy, 6xy,
+         *    7xy, 8xy, 9xy
+         *
+         * where x and y are pixel coordinates in a single tile of size 3×3.A
+         * A + sign is added in front of pixel values that we expect to find
+         * in the destination image at the end of this test method.
+         */
+        assertValuesEqual(toAdd, 0, new double[][] {
+            { 100,  101,  102,  200,  201,  202,  300,  301,  302},
+            {+110, +111, +112, +210, +211, +212, +310, +311, +312},
+            {+400, +401, +402, +500, +501, +502, +600, +601, +602},
+            {+410, +411, +412, +510, +511, +512, +610, +611, +612},
+            {+700, +701, +702, +800, +801, +802, +900, +901, +902},
+            {+710, +711, +712, +810, +811, +812, +910, +911, +912}
+        });
+        /*
+         * Write an image on top of destination image and verify. The expected result
+         * is same as the first table at the beginning of this test method, with above
+         * source image replacing destination values starting from row 0 column 3.
+         * A + sign is added in front of those values for making easier to recognize.
+         */
+        combiner.accept(toAdd);
+        assertValuesEqual(image = combiner.result(), 0, new double[][] {
+            { 100,  101, +110, +111, +112, +210, +211, +212, +310, +311, +312, +303},
+            { 110,  111, +400, +401, +402, +500, +501, +502, +600, +601, +602, +313},
+            { 120,  121, +410, +411, +412, +510, +511, +512, +610, +611, +612, +323},
+            { 130,  131, +700, +701, +702, +800, +801, +802, +900, +901, +902, +333},
+            { 400,  401, +710, +711, +712, +810, +811, +812, +910, +911, +912, +603},
+            { 410,  411,  412,  413,  510,  511,  512,  513,  610,  611,  612,  613},
+            { 420,  421,  422,  423,  520,  521,  522,  523,  620,  621,  622,  623},
+            { 430,  431,  432,  433,  530,  531,  532,  533,  630,  631,  632,  633},
+        });
+    }
+
+    /**
+     * Tests {@link ImageCombiner#resample(RenderedImage, Rectangle, MathTransform)}.
+     * The transform used in this test is a simple translation. The expected result is
+     * similar to the {@link #testAccept()} one, with the new pixel values (identified
+     * by a + sign in source code) shifted by 2 rows and 2 columns.
+     */
+    @Test
+    public void testResample() {
+        final ImageCombiner combiner = initialize();
+        final Rectangle bounds = toAdd.getBounds();
+        bounds.translate(2, 2);
+        final MathTransform toSource = MathTransforms.translation(-2, -2);
+        combiner.resample(toAdd, bounds, toSource);
+        assertValuesEqual(image = combiner.result(), 0, new double[][] {
+            { 100,  101,  102,  103,    0,    0,    0,    0,    0,    0,    0,    0},
+            { 110,  111,  112,  113, +100, +101, +102, +200, +201, +202, +300, +301},
+            { 120,  121,  122,  123, +110, +111, +112, +210, +211, +212, +310, +311},
+            { 130,  131,  132,  133, +400, +401, +402, +500, +501, +502, +600, +601},
+            { 400,  401,  402,  403, +410, +411, +412, +510, +511, +512, +610, +611},
+            { 410,  411,  412,  413, +700, +701, +702, +800, +801, +802, +900, +901},
+            { 420,  421,  422,  423, +710, +711, +712, +810, +811, +812, +910, +911},
+            { 430,  431,  432,  433,    0,    0,    0,    0,    0,    0,    0,    0},
+        });
+        // TODO: value 0 above should not overwrite previous values.
+    }
+}
diff --git a/core/sis-feature/src/test/java/org/apache/sis/image/ImageTestCase.java b/core/sis-feature/src/test/java/org/apache/sis/image/ImageTestCase.java
index 2216b2a..ba595b2 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/image/ImageTestCase.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/image/ImageTestCase.java
@@ -68,7 +68,7 @@ public abstract strictfp class ImageTestCase extends TestCase {
     protected boolean viewEnabled;
 
     /**
-     * Set to {@code true} if we have show at least one image.
+     * Set to {@code true} if we have shown at least one image.
      * This is used for avoiding useless {@link TestViewer} class loading in {@link #waitForFrameDisposal()}.
      */
     private static volatile boolean viewUsed;
diff --git a/core/sis-feature/src/test/java/org/apache/sis/image/ResampledImageTest.java b/core/sis-feature/src/test/java/org/apache/sis/image/ResampledImageTest.java
index 8550b68..9502cbc 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/image/ResampledImageTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/image/ResampledImageTest.java
@@ -284,7 +284,7 @@ public final strictfp class ResampledImageTest extends TestCase {
         final Rectangle bounds = new Rectangle(9, 9);
         target = new ResampledImage(source,
                 ImageLayout.DEFAULT.createCompatibleSampleModel(source, bounds),
-                bounds, toSource, interpolation, null, null);
+                null, bounds, toSource, interpolation, null, null);
 
         assertEquals("numXTiles", 1, target.getNumXTiles());
         assertEquals("numYTiles", 1, target.getNumYTiles());
diff --git a/core/sis-feature/src/test/java/org/apache/sis/image/TiledImageMock.java b/core/sis-feature/src/test/java/org/apache/sis/image/TiledImageMock.java
index 6c13ba8..334656e 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/image/TiledImageMock.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/image/TiledImageMock.java
@@ -29,6 +29,7 @@ import java.awt.image.WritableRenderedImage;
 import java.util.Random;
 import java.util.concurrent.atomic.AtomicInteger;
 import org.apache.sis.internal.coverage.j2d.ImageUtilities;
+import org.apache.sis.internal.coverage.j2d.WritableTiledImage;
 import org.apache.sis.internal.util.Numerics;
 import org.apache.sis.util.ArraysExt;
 
@@ -369,7 +370,7 @@ public final strictfp class TiledImageMock extends PlanarImage implements
Writab
     /**
      * Sets a rectangle of this image to the contents of given raster.
      * The raster is assumed to be in the same coordinate space as this image.
-     * Current implementation can set raster covering only only one tile.
+     * Current implementation can set raster covering only one tile.
      */
     @Override
     public void setData(final Raster r) {
@@ -381,4 +382,14 @@ public final strictfp class TiledImageMock extends PlanarImage implements
Writab
         assertEquals("Unsupported operation.", ty, ImageUtilities.pixelToTileX(this, minY
+ r.getHeight() - 1));
         tile(tx, ty, true).setRect(r);
     }
+
+    /**
+     * Returns this image as a {@link WritableTiledImage} implementation.
+     * This is useful if a more complete implementation of {@link #setData(Raster)} (for
example) is needed.
+     *
+     * @return this image as a more complete implementation.
+     */
+    public WritableTiledImage toWritableTiledImage() {
+        return new WritableTiledImage(null, null, width, height, minTileX, minTileY, tiles);
+    }
 }
diff --git a/core/sis-feature/src/test/java/org/apache/sis/test/FeatureAssert.java b/core/sis-feature/src/test/java/org/apache/sis/test/FeatureAssert.java
index 3d0781c..58c8d65 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/test/FeatureAssert.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/test/FeatureAssert.java
@@ -89,7 +89,7 @@ public strictfp class FeatureAssert extends ReferencingAssert {
      */
     public static void assertValuesEqual(final RenderedImage image, final int band, final
double[][] expected) {
         assertEquals("Height", expected.length, image.getHeight());
-        final PixelIterator it = PixelIterator.create(image);
+        final PixelIterator it = new PixelIterator.Builder().setIteratorOrder(SequenceType.LINEAR).create(image);
         for (int j=0; j<expected.length; j++) {
             final double[] row = expected[j];
             assertEquals("Width", row.length, image.getWidth());
diff --git a/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
b/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
index 0ebc6d6..d757336 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
@@ -90,6 +90,7 @@ import org.junit.runners.Suite;
     org.apache.sis.image.ResamplingGridTest.class,
     org.apache.sis.image.ResampledImageTest.class,
     org.apache.sis.image.BandedSampleConverterTest.class,
+    org.apache.sis.image.ImageCombinerTest.class,
     org.apache.sis.coverage.CategoryTest.class,
     org.apache.sis.coverage.CategoryListTest.class,
     org.apache.sis.coverage.SampleDimensionTest.class,


Mime
View raw message