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: Missing interpolation values in last row and last column of destination image when interpolation is nearest-neighbor and the optimized path is not used.
Date Fri, 04 Sep 2020 14:04:09 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 7c96618  Missing interpolation values in last row and last column of destination
image when interpolation is nearest-neighbor and the optimized path is not used.
7c96618 is described below

commit 7c966189c67c85cbb02209464d490758418f24ca
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Fri Sep 4 16:02:26 2020 +0200

    Missing interpolation values in last row and last column of destination image when interpolation
is nearest-neighbor and the optimized path is not used.
---
 .../java/org/apache/sis/image/ResampledImage.java  | 51 +++++++++++++++++++---
 .../org/apache/sis/image/ImageCombinerTest.java    | 25 +++++++++++
 2 files changed, 71 insertions(+), 5 deletions(-)

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 c0f9cb4..2e54c40 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
@@ -132,6 +132,7 @@ public class ResampledImage extends ComputedImage {
      * parts of {@code toSourceSupport} results, without rounding.</p>
      *
      * @see #interpolationSupportOffset(int)
+     * @see Interpolation#interpolate(DoubleBuffer, int, double, double, double[], int)
      */
     private final MathTransform toSourceSupport;
 
@@ -246,7 +247,7 @@ public class ResampledImage extends ComputedImage {
          * to grab. We shift to the left because we need the coordinates of the first pixel.
          */
         Dimension s = interpolation.getSupportSize();
-        if (s.width > width || s.height > height) {
+        if (s.width > source.getWidth() || s.height > source.getHeight()) {
             interpolation = Interpolation.NEAREST;
             s = interpolation.getSupportSize();
         }
@@ -326,14 +327,54 @@ public class ResampledImage extends ComputedImage {
      * 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:
+     *
+     * {@preformat 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.
+     * @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 -Math.max(0, (span - 1) / 2);        // Round toward 0.
+        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;
     }
 
     /**
@@ -638,8 +679,8 @@ public class ResampledImage extends ComputedImage {
             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;
+            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;
         }
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
index 2e64157..710e7ea 100644
--- 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
@@ -19,8 +19,11 @@ package org.apache.sis.image;
 import java.awt.Rectangle;
 import java.awt.image.DataBuffer;
 import java.awt.image.RenderedImage;
+import java.awt.image.BufferedImage;
 import org.opengis.referencing.operation.MathTransform;
+import org.apache.sis.internal.referencing.j2d.AffineTransform2D;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.test.DependsOn;
 import org.junit.Test;
 
 import static org.apache.sis.test.FeatureAssert.*;
@@ -30,10 +33,12 @@ import static org.apache.sis.test.FeatureAssert.*;
  * Tests {@link ImageCombiner}.
  *
  * @author  Martin Desruisseaux (Geomatys)
+ * @author  Johann Sorel (Geomatys)
  * @version 1.1
  * @since   1.1
  * @module
  */
+@DependsOn(ResampledImageTest.class)
 public final strictfp class ImageCombinerTest extends ImageTestCase {
     /**
      * The image to add to the {@link ImageCombiner}.
@@ -187,4 +192,24 @@ public final strictfp class ImageCombinerTest extends ImageTestCase {
             { 430,  431,  432,  433,  530,  531,  532,  533,  630,  631,  632,  633}
         });
     }
+
+    /**
+     * Tests a resampling which requires a correct {@link ResampledImage#interpolationLimit(double,
int)} computation.
+     * The source image has only one row while the target image has two rows. But the {@code
toSource} transform has
+     * translation terms of -0.25 pixel, which causes the two destination rows to map to
the single source row.
+     */
+    @Test
+    public void testResampleOneToTwo() {
+        final double[] inputs   = {3,    5,    1   };
+        final double[] expected = {3, 3, 5, 5, 1, 1};
+        final BufferedImage source = new BufferedImage(inputs.length,   1, BufferedImage.TYPE_BYTE_GRAY);
+        final BufferedImage target = new BufferedImage(expected.length, 2, BufferedImage.TYPE_BYTE_GRAY);
+        source.getRaster().setPixels(0, 0, inputs.length, 1, inputs);
+        final ImageCombiner combiner = new ImageCombiner(target);
+        combiner.setInterpolation(Interpolation.NEAREST);
+        combiner.resample(source, null, new AffineTransform2D(0.5, 0, 0, 0.5, -0.25, -0.25));
+        assertSame(target, combiner.result());
+        assertValuesEqual(source, 0, new double[][] {inputs});
+        assertValuesEqual(target, 0, new double[][] {expected, expected});
+    }
 }


Mime
View raw message