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});
+ }
}
|