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 <[email protected]> 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,
