sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 03/03: Add a public API for isolines.
Date Thu, 11 Feb 2021 00:31:33 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

commit 2e68ca4afac0f184e9266d7897c2ea13ffc17ac3
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Thu Feb 11 01:30:43 2021 +0100

    Add a public API for isolines.
---
 .../java/org/apache/sis/image/ImageProcessor.java  | 72 +++++++++++++++++-
 .../sis/internal/processing/image/Isolines.java    | 88 ++++++++++++++++++++++
 .../org/apache/sis/image/ImageProcessorTest.java   | 58 ++++++++++++++
 .../internal/processing/image/IsolinesTest.java    | 12 +--
 .../apache/sis/test/suite/FeatureTestSuite.java    |  1 +
 5 files changed, 222 insertions(+), 9 deletions(-)

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 808a425..94e1d36 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
@@ -20,6 +20,7 @@ import java.util.Map;
 import java.util.List;
 import java.util.Arrays;
 import java.util.Objects;
+import java.util.NavigableMap;
 import java.util.function.Function;
 import java.util.logging.LogRecord;
 import java.awt.Color;
@@ -36,6 +37,7 @@ import javax.measure.Quantity;
 import org.apache.sis.coverage.Category;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransform1D;
+import org.opengis.referencing.operation.TransformException;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.coverage.SampleDimension;
@@ -47,6 +49,7 @@ import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.collection.WeakHashSet;
 import org.apache.sis.internal.system.Modules;
 import org.apache.sis.internal.coverage.j2d.TiledImage;
+import org.apache.sis.internal.processing.image.Isolines;
 import org.apache.sis.internal.feature.Resources;
 import org.apache.sis.measure.NumberRange;
 import org.apache.sis.measure.Units;
@@ -97,6 +100,11 @@ import org.apache.sis.measure.Units;
  * tightly on the source image and destination bounds (also given in arguments); those information
usually need
  * to be recomputed for each image.</div>
  *
+ * <h2>Deferred calculations</h2>
+ * Methods in this class may compute the result at some later time after the method returned,
instead of computing
+ * the result immediately on method call. Consequently unless otherwise specified, {@link
RenderedImage} arguments
+ * should be <em>stable</em>, i.e. pixel values should not be modified after
method return.
+ *
  * <h2>Area of interest</h2>
  * Some operations accept an optional <cite>area of interest</cite> argument
specified as a {@link Shape} instance in
  * pixel coordinates. If a shape is given, it should not be modified after {@code ImageProcessor}
method call because
@@ -459,8 +467,7 @@ public class ImageProcessor implements Cloneable {
      * for processing {@link RenderedImage} implementations that may not be thread-safe.
      *
      * <p>It is safe to set this flag to {@link Mode#PARALLEL} with {@link java.awt.image.BufferedImage}
-     * (it will actually have no effect in this particular case) or with Apache SIS implementations
of
-     * {@link RenderedImage}.</p>
+     * or with Apache SIS implementations of {@link RenderedImage}.</p>
      *
      * @param  mode  whether the operations can be executed in parallel.
      */
@@ -478,7 +485,12 @@ public class ImageProcessor implements Cloneable {
         switch (executionMode) {
             case PARALLEL:   return true;
             case SEQUENTIAL: return false;
-            default:         return source.getClass().getName().startsWith(Modules.CLASSNAME_PREFIX);
+            default: {
+                if (source instanceof BufferedImage) {
+                    return true;
+                }
+                return source.getClass().getName().startsWith(Modules.CLASSNAME_PREFIX);
+            }
         }
     }
 
@@ -504,6 +516,7 @@ public class ImageProcessor implements Cloneable {
      * In current {@code ImageProcessor} implementation, the error handler is not honored
by all operations.
      * Some operations may continue to throw an exception on failure (the behavior of default
error handler)
      * even if a different handler has been specified.
+     * Each operation specifies in its Javadoc whether the operation uses error handler or
not.
      *
      * @param  handler  handler to notify when an operation failed on one or more tiles,
      *                  or {@link ErrorHandler#THROW} for propagating the exception.
@@ -543,6 +556,10 @@ public class ImageProcessor implements Cloneable {
      *   <li>{@linkplain #getErrorHandler() Error handler} (custom action executed
if an exception is thrown).</li>
      * </ul>
      *
+     * <h4>Result relationship with source</h4>
+     * This method computes statistics immediately.
+     * Changes in the {@code source} image after this method call do not change the results.
+     *
      * @param  source          the image for which to compute statistics.
      * @param  areaOfInterest  pixel coordinates of the area of interest, or {@code null}
for the default.
      * @return the statistics of sample values in each band.
@@ -748,6 +765,10 @@ public class ImageProcessor implements Cloneable {
      *   <li>(none)</li>
      * </ul>
      *
+     * <h4>Result relationship with source</h4>
+     * Changes in the source image are reflected in the returned images
+     * if the source image notifies {@linkplain java.awt.image.TileObserver tile observers}.
+     *
      * @param  source        the image for which to convert sample values.
      * @param  sourceRanges  approximate ranges of values for each band in source image,
or {@code null} if unknown.
      * @param  converters    the transfer functions to apply on each band of the source image.
@@ -766,7 +787,7 @@ public class ImageProcessor implements Cloneable {
         for (int i=0; i<converters.length; i++) {
             ArgumentChecks.ensureNonNullElement("converters", i, converters[i]);
         }
-        final ImageLayout   layout;
+        final ImageLayout layout;
         synchronized (this) {
             layout = this.layout;
         }
@@ -798,6 +819,10 @@ public class ImageProcessor implements Cloneable {
      *       for enabling faster resampling at the cost of lower precision.</li>
      * </ul>
      *
+     * <h4>Result relationship with source</h4>
+     * Changes in the source image are reflected in the returned images
+     * if the source image notifies {@linkplain java.awt.image.TileObserver tile observers}.
+     *
      * @param  source    the image to be resampled.
      * @param  bounds    domain of pixel coordinates of resampled image to create.
      *                   Updated by this method if {@link Resizing#EXPAND} policy is applied.
@@ -1044,6 +1069,45 @@ public class ImageProcessor implements Cloneable {
     }
 
     /**
+     * Generates isolines at the specified levels computed from data provided by the given
image.
+     * Isolines will be computed for every bands in the given image.
+     * For each band, the result is given as a {@code Map} where keys are the specified {@code
levels}
+     * and values are the isolines at the associated level.
+     * If there is no isoline for a given level, there will be no corresponding entry in
the map.
+     *
+     * <h4>Properties used</h4>
+     * This operation uses the following properties in addition to method parameters:
+     * <ul>
+     *   <li>{@linkplain #getExecutionMode() Execution mode} (parallel or sequential).</li>
+     * </ul>
+     *
+     * @param  data       image providing source values.
+     * @param  levels     values for which to compute isolines. An array should be provided
for each band.
+     *                    If there is more bands than {@code levels.length}, the last array
is reused for
+     *                    all remaining bands.
+     * @param  gridToCRS  transform from pixel coordinates to geometry coordinates, or {@code
null} if none.
+     *                    Integer source coordinates are located at pixel centers.
+     * @return the isolines for specified levels in each band. The {@code List} size is the
number of bands.
+     *         For each band, the {@code Map} size is equal or less than {@code levels[band].length}.
+     *         Map keys are the specified levels, excluding those for which there is no isoline.
+     *         Map values are the isolines as a Java2D {@link Shape}.
+     * @throws ImagingOpException if an error occurred during calculation.
+     */
+    public List<NavigableMap<Double,Shape>> isolines(final RenderedImage data,
final double[][] levels, final MathTransform gridToCRS) {
+        final boolean parallel;
+        synchronized (this) {
+            parallel = parallel(data);
+        }
+        if (parallel) {
+            return Isolines.toList(Isolines.parallelGenerate(data, levels, gridToCRS));
+        } else try {
+            return Isolines.toList(Isolines.generate(data, levels, gridToCRS));
+        } catch (TransformException e) {
+            throw (ImagingOpException) new ImagingOpException(null).initCause(e);
+        }
+    }
+
+    /**
      * Returns {@code true} if the given object is an image processor
      * of the same class with the same configuration.
      *
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/processing/image/Isolines.java
b/core/sis-feature/src/main/java/org/apache/sis/internal/processing/image/Isolines.java
index 4f0a060..a99062e 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/processing/image/Isolines.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/processing/image/Isolines.java
@@ -16,10 +16,14 @@
  */
 package org.apache.sis.internal.processing.image;
 
+import java.util.AbstractList;
 import java.util.Arrays;
+import java.util.List;
 import java.util.TreeMap;
 import java.util.NavigableMap;
 import java.util.concurrent.Future;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.CompletionException;
 import java.awt.Shape;
 import java.awt.geom.Path2D;
 import java.awt.image.RenderedImage;
@@ -409,4 +413,88 @@ abort:  while (iterator.next()) {
         }
         return paths;
     }
+
+    /**
+     * Returns the isolines for each band, then for each values in the band.
+     *
+     * @param  isolines  result of {@code generate(…)} or {@code parallelGenerate(…)}
method call.
+     * @return isoline shapes for each values in each band.
+     */
+    private static NavigableMap<Double,Shape>[] toArray(final Isolines[] isolines)
{
+        @SuppressWarnings({"rawtypes", "unchecked"})
+        final NavigableMap<Double,Shape>[] result = new NavigableMap[isolines.length];
+        for (int i=0; i<result.length; i++) {
+            result[i] = isolines[i].polylines();
+        }
+        return result;
+    }
+
+    /**
+     * Returns the isolines for each band, then for each values in the band.
+     *
+     * @param  isolines  result of {@code generate(…)} or {@code parallelGenerate(…)}
method call.
+     * @return isoline shapes for each values in each band.
+     */
+    public static List<NavigableMap<Double,Shape>> toList(final Isolines[] isolines)
{
+        return Arrays.asList(toArray(isolines));
+    }
+
+    /**
+     * Returns deferred isolines for each band, then for each values in the band.
+     * The {@link Future} result is requested the first time that {@link List#get(int)} is
invoked.
+     *
+     * @param  isolines  result of {@code generate(…)} or {@code parallelGenerate(…)}
method call.
+     * @return isoline shapes for each values in each band.
+     */
+    public static List<NavigableMap<Double,Shape>> toList(final Future<Isolines[]>
isolines) {
+        return new Result(isolines);
+    }
+
+    /**
+     * Deferred isoline result, created when computation is continuing in background.
+     * The {@link Future} result is requested the first time that {@link #get(int)} is invoked.
+     */
+    private static final class Result extends AbstractList<NavigableMap<Double,Shape>>
{
+        /** The task computing isolines result. Reset to {@code null} when no longer needed.
*/
+        private Future<Isolines[]> task;
+
+        /** The result of {@link Future#get()} fetched when first needed. */
+        private NavigableMap<Double,Shape>[] isolines;
+
+        /** Creates a new list for the given future isolines. */
+        Result(final Future<Isolines[]> task) {
+            this.task = task;
+        }
+
+        /** Fetches the isolines from the {@link Future} if not already done. */
+        @SuppressWarnings("ReturnOfCollectionOrArrayField")
+        private NavigableMap<Double,Shape>[] isolines() {
+            if (isolines == null) {
+                if (task == null) {
+                    throw new CompletionException(null);
+                }
+                try {
+                    isolines = Isolines.toArray(task.get());
+                    task = null;
+                } catch (InterruptedException e) {
+                    // Do not clear `task`: the result may become available later.
+                    throw new CompletionException(e);
+                } catch (ExecutionException e) {
+                    task = null;
+                    throw new CompletionException(e.getCause());
+                }
+            }
+            return isolines;
+        }
+
+        /** Returns the list length, which is the number of bands. */
+        @Override public int size() {
+            return isolines().length;
+        }
+
+        /** Returns the isolines in the given band. */
+        @Override public NavigableMap<Double,Shape> get(final int band) {
+            return isolines()[band];
+        }
+    }
 }
diff --git a/core/sis-feature/src/test/java/org/apache/sis/image/ImageProcessorTest.java b/core/sis-feature/src/test/java/org/apache/sis/image/ImageProcessorTest.java
new file mode 100644
index 0000000..8158953
--- /dev/null
+++ b/core/sis-feature/src/test/java/org/apache/sis/image/ImageProcessorTest.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.image;
+
+import java.util.Map;
+import java.awt.Shape;
+import java.awt.image.BufferedImage;
+import java.awt.image.RenderedImage;
+import org.apache.sis.internal.processing.image.IsolinesTest;
+import org.opengis.referencing.operation.MathTransform;
+import org.apache.sis.test.TestCase;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+import static org.apache.sis.test.TestUtilities.getSingleton;
+
+
+/**
+ * Tests {@link ImageProcessor}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public final strictfp class ImageProcessorTest extends TestCase {
+    /**
+     * Tests {@link ImageProcessor#isolines(RenderedImage, double[][], MathTransform)}.
+     */
+    @Test
+    public void testIsolines() {
+        final BufferedImage image = new BufferedImage(3, 3, BufferedImage.TYPE_BYTE_BINARY);
+        image.getRaster().setSample(1, 1, 0, 1);
+
+        final ImageProcessor processor = new ImageProcessor();
+        boolean parallel = false;
+        do {
+            processor.setExecutionMode(parallel ? ImageProcessor.Mode.SEQUENTIAL : ImageProcessor.Mode.PARALLEL);
+            final Map<Double,Shape> r = getSingleton(processor.isolines(image, new
double[][] {{0.5}}, null));
+            assertEquals(0.5, getSingleton(r.keySet()), STRICT);
+            IsolinesTest.verifyIsolineFromMultiCells(getSingleton(r.values()));
+        } while ((parallel = !parallel) == true);
+    }
+}
diff --git a/core/sis-feature/src/test/java/org/apache/sis/internal/processing/image/IsolinesTest.java
b/core/sis-feature/src/test/java/org/apache/sis/internal/processing/image/IsolinesTest.java
index 8e70e89..14568a0 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/internal/processing/image/IsolinesTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/internal/processing/image/IsolinesTest.java
@@ -186,14 +186,16 @@ public final strictfp class IsolinesTest extends TestCase {
              0,0,0,
              0,1,0,
              0,0,0);
-        verifyIsolineFromMultiCells();
+        verifyIsolineFromMultiCells(isoline);
     }
 
     /**
-     * Verifies the result of {@link #testMultiCells()}.
-     * The shape to verify shall be stored in the {@link #isoline} field.
+     * Verifies the isoline generated for level 0.5 on an image of 3×3 pixels having value
1 in the center
+     * and value zero everywhere else. This is the isoline tested by {@link #testMultiCells()}.
+     *
+     * @param  isoline  the isoline to verify.
      */
-    private void verifyIsolineFromMultiCells() {
+    public static void verifyIsolineFromMultiCells(final Shape isoline) {
         /*
          * Expected coordinates:
          *
@@ -278,7 +280,7 @@ public final strictfp class IsolinesTest extends TestCase {
         assertTrue(isolines[0].polylines().isEmpty());
         assertTrue(isolines[2].polylines().isEmpty());
         isoline =  isolines[1].polylines().get(threshold);
-        verifyIsolineFromMultiCells();
+        verifyIsolineFromMultiCells(isoline);
     }
 
     /**
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 095cf62..20a2f4f 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
@@ -93,6 +93,7 @@ import org.junit.runners.Suite;
     org.apache.sis.image.ResampledImageTest.class,
     org.apache.sis.image.BandedSampleConverterTest.class,
     org.apache.sis.image.ImageCombinerTest.class,
+    org.apache.sis.image.ImageProcessorTest.class,
     org.apache.sis.coverage.CategoryTest.class,
     org.apache.sis.coverage.CategoryListTest.class,
     org.apache.sis.coverage.SampleDimensionTest.class,


Mime
View raw message