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 9aa776c7af Last adjustements on the `Colorizer` work and addition of a convenience `GridcoverageProcessor.visualize(GridCoverage, …)` method. 9aa776c7af is described below commit 9aa776c7afbc577dd398dfa8335618af819e7e33 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Thu Mar 30 15:47:10 2023 +0200 Last adjustements on the `Colorizer` work and addition of a convenience `GridcoverageProcessor.visualize(GridCoverage, …)` method. --- .../org/apache/sis/coverage/grid/GridCoverage.java | 4 +- .../sis/coverage/grid/GridCoverageBuilder.java | 2 + .../sis/coverage/grid/GridCoverageProcessor.java | 83 +++++++++++++++++++++- .../apache/sis/coverage/grid/ImageRenderer.java | 6 +- .../apache/sis/coverage/grid/SliceGeometry.java | 2 + .../apache/sis/image/BandedSampleConverter.java | 6 +- .../java/org/apache/sis/image/ImageProcessor.java | 18 +++-- .../java/org/apache/sis/image/Visualization.java | 27 ++++--- .../sis/internal/coverage/SampleDimensions.java | 31 ++++++-- .../sis/internal/map/coverage/RenderingData.java | 24 ++----- 10 files changed, 155 insertions(+), 48 deletions(-) diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java index fbea3ecdb7..bb4c8d5e25 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java +++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java @@ -314,10 +314,10 @@ public abstract class GridCoverage extends BandedCoverage { final MathTransform1D[] converters, final ImageProcessor processor) { try { - SampleDimensions.CONVERTED_BANDS.set(sampleDimensions); + SampleDimensions.IMAGE_PROCESSOR_ARGUMENT.set(sampleDimensions); return processor.convert(source, getRanges(), converters, bandType); } finally { - SampleDimensions.CONVERTED_BANDS.remove(); + SampleDimensions.IMAGE_PROCESSOR_ARGUMENT.remove(); } } diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageBuilder.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageBuilder.java index ca04e9a09e..15a52e41ad 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageBuilder.java +++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageBuilder.java @@ -483,6 +483,8 @@ public class GridCoverageBuilder { */ if (bands != null) { properties.put(PlanarImage.SAMPLE_DIMENSIONS_KEY, bands.toArray(SampleDimension[]::new)); + } else { + properties.remove(PlanarImage.SAMPLE_DIMENSIONS_KEY); } if (raster instanceof WritableRaster) { final WritableRaster wr = (WritableRaster) raster; diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java index b915d36752..a57a73b479 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java +++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java @@ -23,7 +23,6 @@ import java.util.Objects; import java.util.function.Function; import java.awt.Shape; import java.awt.Rectangle; -import java.awt.image.ColorModel; import java.awt.image.RenderedImage; import javax.measure.Quantity; import org.opengis.util.FactoryException; @@ -39,6 +38,7 @@ import org.apache.sis.image.DataType; import org.apache.sis.image.Colorizer; import org.apache.sis.image.ImageProcessor; import org.apache.sis.image.Interpolation; +import org.apache.sis.internal.coverage.SampleDimensions; import org.apache.sis.internal.coverage.MultiSourcesArgument; import org.apache.sis.util.ArgumentChecks; import org.apache.sis.util.logging.Logging; @@ -220,6 +220,7 @@ public class GridCoverageProcessor implements Cloneable { * @param colorizer colorization algorithm to apply on computed image, or {@code null} for default. * * @see ImageProcessor#setColorizer(Colorizer) + * @see #visualize(GridCoverage, GridExtent) * * @since 1.4 */ @@ -328,6 +329,12 @@ public class GridCoverageProcessor implements Cloneable { * If {@code maskInside} is {@code false}, then the mask is reversed: * the pixels set to fill values are the ones outside the ROI. * + * <h4>Properties used</h4> + * This operation uses the following properties in addition to method parameters: + * <ul> + * <li>{@linkplain #getFillValues() Fill values} values to assign to pixels inside/outside the region of interest.</li> + * </ul> + * * @param source the coverage on which to apply a mask. * @param mask region (in arbitrary CRS) of the mask. * @param maskInside {@code true} for masking pixels inside the shape, or {@code false} for masking outside. @@ -369,13 +376,19 @@ public class GridCoverageProcessor implements Cloneable { * If the source coverage is backed by a {@link java.awt.image.WritableRenderedImage}, * then changes in the source coverage are reflected in the returned coverage and conversely. * + * <h4>Properties used</h4> + * This operation uses the following properties in addition to method parameters: + * <ul> + * <li>{@linkplain #getColorizer() Colorizer} for customizing the rendered image color model.</li> + * </ul> + * * @param source the coverage for which to convert sample values. * @param converters the transfer functions to apply on each sample dimension of the source coverage. * @param sampleDimensionModifier a callback for modifying the {@link SampleDimension.Builder} default * configuration for each sample dimension of the target coverage, or {@code null} if none. * @return the coverage which computes converted values from the given source. * - * @see ImageProcessor#convert(RenderedImage, NumberRange<?>[], MathTransform1D[], DataType, ColorModel) + * @see ImageProcessor#convert(RenderedImage, NumberRange<?>[], MathTransform1D[], DataType) * * @since 1.3 */ @@ -430,6 +443,12 @@ public class GridCoverageProcessor implements Cloneable { * of this {@code translate(…)} method into a single translation.</li> * </ul> * + * <h4>Properties used</h4> + * This operation uses the following properties in addition to method parameters: + * <ul> + * <li>(none)</li> + * </ul> + * * @param source the grid coverage to translate. * @param translation translation to apply on each grid axis in order. * @return a grid coverage whose grid coordinates (both low and high ones) and @@ -488,6 +507,15 @@ public class GridCoverageProcessor implements Cloneable { * by {@code translate(…)} when possible.</li> * </ul> * + * <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 #getFillValues() Fill values} for pixels outside source image.</li> + * <li>{@linkplain #getPositionalAccuracyHints() Positional accuracy hints} + * for enabling faster resampling at the cost of lower precision.</li> + * </ul> + * * @param source the grid coverage to resample. * @param target the desired geometry of returned grid coverage. May be incomplete. * @return a grid coverage with the characteristics specified in the given grid geometry. @@ -742,6 +770,57 @@ public class GridCoverageProcessor implements Cloneable { return new BandAggregateGridCoverage(aggregate, snapshot()); } + /** + * Renders the given grid coverage as an image suitable for displaying purpose. + * The resulting image is for visualization only and should not be used for computational purposes. + * There is no guarantee about the number of bands in returned image or about which formula is used + * for converting floating point values to integer values. + * + * <h4>How to specify colors</h4> + * The image colors can be controlled by the {@link Colorizer} set on this coverage processor. + * The recommended way is to associate colors to {@linkplain Category#getName() category names}, + * {@linkplain org.apache.sis.measure.MeasurementRange#unit() units of measurement} + * or other category properties. Example: + * + * {@snippet lang="java" : + * Map<String,Color[]> colors = Map.of( + * "Temperature", new Color[] {Color.BLUE, Color.MAGENTA, Color.RED}, + * "Wind speed", new Color[] {Color.GREEN, Color.CYAN, Color.BLUE}); + * + * processor.setColorizer(Colorizer.forCategories((category) -> + * colors.get(category.getName().toString(Locale.ENGLISH)))); + * + * RenderedImage visualization = processor.visualize(source, slice); + * } + * + * <h4>Properties used</h4> + * This operation uses the following properties in addition to method parameters: + * <ul> + * <li>{@linkplain #getColorizer() Colorizer} for customizing the rendered image color model.</li> + * </ul> + * + * @param source the grid coverage to visualize. + * @param slice the slice and extent to render, or {@code null} for the whole coverage. + * @return rendered image for visualization purposes only. + * @throws IllegalArgumentException if the given extent does not have the same number of dimensions + * than the specified coverage or does not intersect. + * + * @see ImageProcessor#visualize(RenderedImage) + * + * @since 1.4 + */ + public RenderedImage visualize(final GridCoverage source, final GridExtent slice) { + ArgumentChecks.ensureNonNull("source", source); + final SampleDimension[] bands = source.getSampleDimensions().toArray(SampleDimension[]::new); + final RenderedImage image = source.render(slice); + try { + SampleDimensions.IMAGE_PROCESSOR_ARGUMENT.set(bands); + return imageProcessor.visualize(image); + } finally { + SampleDimensions.IMAGE_PROCESSOR_ARGUMENT.remove(); + } + } + /** * Invoked when an ignorable exception occurred. * diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java index 9fde21c79b..b9005bd109 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java +++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java @@ -447,7 +447,7 @@ public class ImageRenderer { public GridGeometry getImageGeometry(final int dimCRS) { GridGeometry ig = imageGeometry; if (ig == null || dimCRS != GridCoverage2D.BIDIMENSIONAL) { - if (isSameGeometry(dimCRS)) { + if (imageUseSameGeometry(dimCRS)) { ig = geometry; } else try { ig = new SliceGeometry(geometry, sliceExtent, gridDimensions, mtFactory) @@ -515,7 +515,7 @@ public class ImageRenderer { * can return {@link #geometry} directly. This common case avoids the need for more costly computation with * {@link SliceGeometry}. */ - private boolean isSameGeometry(final int dimCRS) { + private boolean imageUseSameGeometry(final int dimCRS) { final int tgtDim = geometry.getTargetDimension(); ArgumentChecks.ensureBetween("dimCRS", GridCoverage2D.BIDIMENSIONAL, tgtDim, dimCRS); if (tgtDim == dimCRS && geometry.getDimension() == gridDimensions.length) { @@ -762,7 +762,7 @@ public class ImageRenderer { } SliceGeometry supplier = null; if (imageGeometry == null) { - if (isSameGeometry(GridCoverage2D.BIDIMENSIONAL)) { + if (imageUseSameGeometry(GridCoverage2D.BIDIMENSIONAL)) { imageGeometry = geometry; } else { supplier = new SliceGeometry(geometry, sliceExtent, gridDimensions, mtFactory); diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/SliceGeometry.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/SliceGeometry.java index dc57a98663..26efeb3bac 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/SliceGeometry.java +++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/SliceGeometry.java @@ -59,6 +59,7 @@ final class SliceGeometry implements Function<RenderedImage, GridGeometry> { /** * Extents of the slice to take in the {@linkplain #geometry}. + * May be {@code null} if unknown. */ private final GridExtent sliceExtent; @@ -189,6 +190,7 @@ final class SliceGeometry implements Function<RenderedImage, GridGeometry> { } GeneralEnvelope subArea = null; if (useSubExtent && cornerToCRS != null) try { + // `extent` is non-null if `useSubExtent` is true. subArea = extent.toEnvelope(cornerToCRS, gridToCRS, null); } catch (TransformException e) { // GridGeometry.reduce(…) is the public method invoking indirectly this method. diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/BandedSampleConverter.java b/core/sis-feature/src/main/java/org/apache/sis/image/BandedSampleConverter.java index baab75fe89..8cc2527aa5 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/image/BandedSampleConverter.java +++ b/core/sis-feature/src/main/java/org/apache/sis/image/BandedSampleConverter.java @@ -99,8 +99,8 @@ class BandedSampleConverter extends ComputedImage { /** * Description of bands, or {@code null} if unknown. * Not used by this class, but provided as a {@value #SAMPLE_DIMENSIONS_KEY} property. - * The value is fetched from {@link SampleDimensions#CONVERTED_BANDS} for avoiding to - * expose a {@code SampleDimension[]} argument in public {@link ImageProcessor} API. + * The value is fetched from {@link SampleDimensions#IMAGE_PROCESSOR_ARGUMENT} for avoiding + * to expose a {@code SampleDimension[]} argument in public {@link ImageProcessor} API. * * @see #getProperty(String) */ @@ -221,7 +221,7 @@ class BandedSampleConverter extends ComputedImage { } final int numBands = converters.length; final BandedSampleModel sampleModel = layout.createBandedSampleModel(targetType, numBands, source, null); - final SampleDimension[] sampleDimensions = SampleDimensions.CONVERTED_BANDS.get(); + final SampleDimension[] sampleDimensions = SampleDimensions.IMAGE_PROCESSOR_ARGUMENT.get(); final int visibleBand = ImageUtilities.getVisibleBand(source); ColorModel colorModel = ColorModelBuilder.NULL_COLOR_MODEL; if (colorizer != null) { 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 e0ca82b6cc..dea9f52adf 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 @@ -993,6 +993,12 @@ public class ImageProcessor implements Cloneable { * If {@code maskInside} is {@code false}, then the mask is reversed: * the pixels set to fill values are the ones outside the shape. * + * <h4>Properties used</h4> + * This operation uses the following properties in addition to method parameters: + * <ul> + * <li>{@linkplain #getFillValues() Fill values} values to assign to pixels inside/outside the shape.</li> + * </ul> + * * @param source the image on which to apply a mask. * @param mask geometric area (in pixel coordinates) of the mask. * @param maskInside {@code true} for masking pixels inside the shape, or {@code false} for masking outside. @@ -1037,7 +1043,7 @@ public class ImageProcessor implements Cloneable { * <h4>Properties used</h4> * This operation uses the following properties in addition to method parameters: * <ul> - * <li>{@linkplain #getColorizer() Colorizer}.</li> + * <li>{@linkplain #getColorizer() Colorizer} for customizing the rendered image color model.</li> * </ul> * * <h4>Result relationship with source</h4> @@ -1135,7 +1141,7 @@ public class ImageProcessor implements Cloneable { * @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. - * @param toSource conversion of pixel coordinates from resampled image to {@code source} image. + * @param toSource conversion of pixel center coordinates from resampled image to {@code source} image. * @return resampled image (may be {@code source}). * * @see GridCoverageProcessor#resample(GridCoverage, GridGeometry) @@ -1362,7 +1368,7 @@ public class ImageProcessor implements Cloneable { * <h4>Properties used</h4> * This operation uses the following properties in addition to method parameters: * <ul> - * <li>{@linkplain #getColorizer() Colorizer}.</li> + * <li>{@linkplain #getColorizer() Colorizer} for customizing the rendered image color model.</li> * </ul> * * @param source the image to recolor for visualization purposes. @@ -1406,15 +1412,17 @@ public class ImageProcessor implements Cloneable { * if {@code bounds} size is not divisible by a tile size.</li> * <li>{@linkplain #getPositionalAccuracyHints() Positional accuracy hints} * for enabling faster resampling at the cost of lower precision.</li> - * <li>{@linkplain #getColorizer() Colorizer}.</li> + * <li>{@linkplain #getColorizer() Colorizer} for customizing the rendered image color model.</li> * </ul> * * @param source the image to be resampled and recolored. * @param bounds domain of pixel coordinates of resampled image to create. * Updated by this method if {@link Resizing#EXPAND} policy is applied. - * @param toSource conversion of pixel coordinates from resampled image to {@code source} image. + * @param toSource conversion of pixel center coordinates from resampled image to {@code source} image. * @return resampled and recolored image for visualization purposes only. * + * @see #resample(RenderedImage, Rectangle, MathTransform) + * * @since 1.4 */ public RenderedImage visualize(final RenderedImage source, final Rectangle bounds, final MathTransform toSource) { 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 35229898d1..7237500a48 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 @@ -197,9 +197,12 @@ final class Visualization extends ResampledImage { this.bounds = bounds; this.source = source; this.toSource = toSource; - Object ranges = source.getProperty(SAMPLE_DIMENSIONS_KEY); - if (ranges instanceof SampleDimension[]) { - sampleDimensions = (SampleDimension[]) ranges; + sampleDimensions = SampleDimensions.IMAGE_PROCESSOR_ARGUMENT.get(); + if (sampleDimensions == null) { + Object ranges = source.getProperty(SAMPLE_DIMENSIONS_KEY); + if (ranges instanceof SampleDimension[]) { + sampleDimensions = (SampleDimension[]) ranges; + } } } @@ -247,8 +250,19 @@ final class Visualization extends ResampledImage { * Keep only the band to make visible in order to reduce the amount of calculation during * resampling and for saving memory. */ - while (source instanceof ImageAdapter) { - source = ((ImageAdapter) source).source; + if (toSource == null) { + toSource = MathTransforms.identity(BIDIMENSIONAL); + } + for (;;) { + if (source instanceof ImageAdapter) { + source = ((ImageAdapter) source).source; + } else if (source instanceof ResampledImage) { + final ResampledImage r = (ResampledImage) source; + toSource = MathTransforms.concatenate(toSource, r.toSource); + source = r.getSource(); + } else { + break; + } } source = BandSelectImage.create(source, new int[] {visibleBand}); final SampleDimension visibleSD = (sampleDimensions != null && visibleBand < sampleDimensions.length) @@ -259,9 +273,6 @@ final class Visualization extends ResampledImage { * requires the tile layout of destination image to be the same as source image. * Otherwise combine interpolation and value conversions in a single operation. */ - if (toSource == null) { - toSource = MathTransforms.identity(BIDIMENSIONAL); - } final boolean shortcut = toSource.isIdentity() && (bounds == null || ImageUtilities.getBounds(source).contains(bounds)); if (shortcut) { layout = ImageLayout.fixedSize(source); diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/SampleDimensions.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/SampleDimensions.java index d896643025..a8dcb60a16 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/SampleDimensions.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/SampleDimensions.java @@ -37,14 +37,33 @@ import org.apache.sis.util.Static; */ public final class SampleDimensions extends Static { /** - * The sample dimensions of a {@link org.apache.sis.image.BandedSampleConverter} image. - * We use this thread-local variable as an internal workaround for an parameter that we - * do not expose in the public API of {@link ImageProcessor}. + * A hidden argument passed to some {@link ImageProcessor} operations. + * Used for a parameter that we do not want to expose in the public API, + * because {@link ImageProcessor} is not supposed to know grid coverages. + * We may revisit in future Apache SIS version if we find a better way to + * pass this information. * - * <p>The content of the array in this thread-local variable shall not be modified, - * because it may be a direct reference to an internal array (not a clone).</p> + * This is used in: + * <ul> + * <li>The <em>target</em> sample dimensions of a {@link org.apache.sis.image.BandedSampleConverter} image.</li> + * <li>The <em>source</em> sample dimensions of a {@link org.apache.sis.image.Visualization} image.</li> + * </ul> + * + * Usage pattern: + * + * {@snippet lang="java" : + * try { + * SampleDimensions.IMAGE_PROCESSOR_ARGUMENT.set(dataRanges); + * return imageProcessor.doSomeStuff(...); + * } finally { + * SampleDimensions.IMAGE_PROCESSOR_ARGUMENT.remove(); + * } + * } + * + * The content of the array in this thread-local variable shall not be modified, + * because it may be a direct reference to an internal array (not a clone). */ - public static final ThreadLocal<SampleDimension[]> CONVERTED_BANDS = new ThreadLocal<>(); + public static final ThreadLocal<SampleDimension[]> IMAGE_PROCESSOR_ARGUMENT = new ThreadLocal<>(); /** * Do not allow instantiation of this class. diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java index bea3a65a64..6c51dd0946 100644 --- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java +++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java @@ -662,30 +662,16 @@ public class RenderingData implements Cloneable { */ if (CREATE_INDEX_COLOR_MODEL) { final ColorModelType ct = ColorModelType.find(recoloredImage.getColorModel()); - if (ct.isSlow || (ct.useColorRamp && processor.getColorizer() != null)) { - return processor.visualize(withSampleDimensions(recoloredImage), bounds, displayToCenter); + if (ct.isSlow || (ct.useColorRamp && processor.getColorizer() != null)) try { + SampleDimensions.IMAGE_PROCESSOR_ARGUMENT.set(dataRanges); + return processor.visualize(recoloredImage, bounds, displayToCenter); + } finally { + SampleDimensions.IMAGE_PROCESSOR_ARGUMENT.remove(); } } return processor.resample(recoloredImage, bounds, displayToCenter); } - /** - * Returns an image augmented with the sample dimensions if not already present. - * If the property is present but with a different value, the {@link #dataRanges} - * will overwrite the image property value. - * - * @param image the image for which to add a property if not already present. - * @return image augmented with the given property. - */ - private RenderedImage withSampleDimensions(RenderedImage image) { - final String key = PlanarImage.SAMPLE_DIMENSIONS_KEY; - final SampleDimension[] value = dataRanges; - if (!Objects.deepEquals(image.getProperty(key), value)) { - image = processor.addUserProperties(image, Map.of(key, value)); - } - return image; - } - /** * Conversion or transformation from {@linkplain PlanarCanvas#getObjectiveCRS() objective CRS} to * {@linkplain #data} CRS. This transform will include {@code WraparoundTransform} steps if needed.