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 482e27bf24 Consolidation of the handling of `PlanarImage.SAMPLE_DIMENSIONS_KEY`: - Document that some elements may be null, and adjust codes accordingly. - Ensure that `ImageProcessor.statistics(…)` never return null values. - Change some internal from `SampleDimensions[]` to `List<SampleDimension>`. It reduces the number of conversions between those two types. 482e27bf24 is described below commit 482e27bf2414a39daa5d4c6d6806c0e4f5279ad4 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Thu Dec 26 13:52:39 2024 +0100 Consolidation of the handling of `PlanarImage.SAMPLE_DIMENSIONS_KEY`: - Document that some elements may be null, and adjust codes accordingly. - Ensure that `ImageProcessor.statistics(…)` never return null values. - Change some internal from `SampleDimensions[]` to `List<SampleDimension>`. It reduces the number of conversions between those two types. --- .../coverage/grid/BandAggregateGridCoverage.java | 4 +- .../org/apache/sis/coverage/grid/GridCoverage.java | 5 +- .../sis/coverage/grid/GridCoverageProcessor.java | 4 +- .../sis/coverage/privy/SampleDimensions.java | 27 +++++---- .../org/apache/sis/image/BandAggregateImage.java | 67 ++++++++++++++++++---- .../org/apache/sis/image/BandAggregateLayout.java | 49 +++++++--------- .../main/org/apache/sis/image/BandSelectImage.java | 12 +++- .../apache/sis/image/BandedSampleConverter.java | 23 ++++---- .../main/org/apache/sis/image/Colorizer.java | 2 + .../main/org/apache/sis/image/ImageProcessor.java | 47 ++++++++++++--- .../main/org/apache/sis/image/PlanarImage.java | 20 +++++-- .../main/org/apache/sis/image/RecoloredImage.java | 3 +- .../main/org/apache/sis/image/Visualization.java | 14 +++-- .../apache/sis/image/BandAggregateImageTest.java | 34 +++++++++++ .../org/apache/sis/map/coverage/RenderingData.java | 6 +- .../storage/geotiff/writer/ReformattedImage.java | 4 +- 16 files changed, 225 insertions(+), 96 deletions(-) diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/BandAggregateGridCoverage.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/BandAggregateGridCoverage.java index 695876c2c8..c51ab5cc97 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/BandAggregateGridCoverage.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/BandAggregateGridCoverage.java @@ -136,7 +136,7 @@ final class BandAggregateGridCoverage extends GridCoverage { /** * Returns a two-dimensional slice of grid data as a rendered image. * This operation is potentially costly if the {@code sliceExtent} argument changes often because - * the previously computed images are unlikely to be reused if the coordinate systems are different. + * the previously computed images are unlikely to be reused when the coordinate systems are different. * It may result in the same bands being copied may times in different {@link RenderedImage} instances. * * <h4>Implementation note</h4> @@ -155,7 +155,7 @@ final class BandAggregateGridCoverage extends GridCoverage { if (sliceExtent == null) { sliceExtent = gridGeometry.getExtent(); } - final RenderedImage[] images = new RenderedImage[sources.length]; + final var images = new RenderedImage[sources.length]; for (int i=0; i<images.length; i++) { images[i] = sources[i].render(sliceExtent.translate(gridTranslations[i])); } diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverage.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverage.java index 5ea04a79e4..520def8287 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverage.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverage.java @@ -213,7 +213,7 @@ public abstract class GridCoverage extends BandedCoverage { * @see SampleDimension#getBackground() */ final Number[] getBackground() { - return SampleDimensions.backgrounds(sampleDimensions); + return SampleDimensions.backgrounds(getSampleDimensions()); } /** @@ -312,8 +312,9 @@ public abstract class GridCoverage extends BandedCoverage { final RenderedImage convert(final RenderedImage source, final DataType bandType, final MathTransform1D[] converters, final ImageProcessor processor) { + final List<SampleDimension> ranges = getSampleDimensions(); try { - SampleDimensions.IMAGE_PROCESSOR_ARGUMENT.set(sampleDimensions); + SampleDimensions.IMAGE_PROCESSOR_ARGUMENT.set(ranges); return processor.convert(source, getRanges(), converters, bandType); } finally { SampleDimensions.IMAGE_PROCESSOR_ARGUMENT.remove(); diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverageProcessor.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverageProcessor.java index 69fcbc0b47..9d8a33a514 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverageProcessor.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverageProcessor.java @@ -937,10 +937,10 @@ public class GridCoverageProcessor implements Cloneable { */ public RenderedImage visualize(final GridCoverage source, final GridExtent slice) { ArgumentChecks.ensureNonNull("source", source); - final SampleDimension[] bands = source.getSampleDimensions().toArray(SampleDimension[]::new); + final List<SampleDimension> ranges = source.getSampleDimensions(); final RenderedImage image = source.render(slice); try { - SampleDimensions.IMAGE_PROCESSOR_ARGUMENT.set(bands); + SampleDimensions.IMAGE_PROCESSOR_ARGUMENT.set(ranges); return imageProcessor.visualize(image); } finally { SampleDimensions.IMAGE_PROCESSOR_ARGUMENT.remove(); diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/privy/SampleDimensions.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/privy/SampleDimensions.java index 8b3a7a15b9..e76e23ce7f 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/privy/SampleDimensions.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/privy/SampleDimensions.java @@ -46,6 +46,8 @@ public final class SampleDimensions extends Static { * 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>target</em> sample dimensions of a {@link org.apache.sis.image.BandAggregateImage} image.</li> + * <li>The <em>target</em> sample dimensions of a {@link org.apache.sis.image.BandSelectImage} image.</li> * <li>The <em>source</em> sample dimensions of a {@link org.apache.sis.image.Visualization} image.</li> * </ul> * @@ -60,10 +62,9 @@ public final class SampleDimensions extends Static { * } * } * - * 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). + * The list in this thread-local variable should be unmodifiable. */ - public static final ThreadLocal<SampleDimension[]> IMAGE_PROCESSOR_ARGUMENT = new ThreadLocal<>(); + public static final ThreadLocal<List<SampleDimension>> IMAGE_PROCESSOR_ARGUMENT = new ThreadLocal<>(); /** * Do not allow instantiation of this class. @@ -114,16 +115,18 @@ public final class SampleDimensions extends Static { * @return the background values, or {@code null} if the given argument was null. * Otherwise the returned array is never null but may contain null elements. */ - public static Number[] backgrounds(final SampleDimension... bands) { + public static Number[] backgrounds(final List<SampleDimension> bands) { if (bands == null) { return null; } - final Number[] fillValues = new Number[bands.length]; + final Number[] fillValues = new Number[bands.size()]; for (int i=fillValues.length; --i >= 0;) { - final SampleDimension band = bands[i]; - final Optional<Number> bg = band.getBackground(); - if (bg.isPresent()) { - fillValues[i] = bg.get(); + final SampleDimension band = bands.get(i); + if (band != null) { + final Optional<Number> bg = band.getBackground(); + if (bg.isPresent()) { + fillValues[i] = bg.get(); + } } } return fillValues; @@ -144,13 +147,13 @@ public final class SampleDimensions extends Static { * * @see ImageProcessor#statistics(RenderedImage, Shape, DoubleUnaryOperator...) */ - public static DoubleUnaryOperator[] toSampleFilters(final SampleDimension... bands) { + public static DoubleUnaryOperator[] toSampleFilters(final List<SampleDimension> bands) { if (bands == null) { return null; } - final DoubleUnaryOperator[] sampleFilters = new DoubleUnaryOperator[bands.length]; + final DoubleUnaryOperator[] sampleFilters = new DoubleUnaryOperator[bands.size()]; for (int i = 0; i < sampleFilters.length; i++) { - final SampleDimension band = bands[i]; + final SampleDimension band = bands.get(i); if (band != null) { final List<Category> categories = band.getCategories(); final int count = categories.size(); diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandAggregateImage.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandAggregateImage.java index ca2e6c4c27..dfb8c1ae2c 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandAggregateImage.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandAggregateImage.java @@ -16,7 +16,10 @@ */ package org.apache.sis.image; +import java.util.List; import java.util.Arrays; +import java.util.Objects; +import java.util.LinkedHashSet; import java.awt.Rectangle; import java.awt.image.ColorModel; import java.awt.image.BandedSampleModel; @@ -25,6 +28,7 @@ import java.awt.image.RenderedImage; import java.awt.image.WritableRaster; import java.awt.image.WritableRenderedImage; import org.apache.sis.util.ArraysExt; +import org.apache.sis.math.Statistics; import org.apache.sis.coverage.SampleDimension; import org.apache.sis.coverage.privy.ImageUtilities; import org.apache.sis.coverage.privy.BandAggregateArgument; @@ -52,10 +56,9 @@ class BandAggregateImage extends MultiSourceImage { private final boolean allowSharing; /** - * Concatenated array of the sample dimensions declared in all sources, or {@code null} if none. - * This field is non-null only if this information is present in all sources. + * Concatenated list of the sample dimensions declared in all sources, or {@code null} if none. */ - private final SampleDimension[] sampleDimensions; + private final List<SampleDimension> sampleDimensions; /* * The method declaration order below is a little bit unusual, @@ -243,23 +246,63 @@ class BandAggregateImage extends MultiSourceImage { */ @Override public String[] getPropertyNames() { + final var names = new LinkedHashSet<String>(); if (sampleDimensions != null) { - return new String[] {SAMPLE_DIMENSIONS_KEY}; - } else { - return null; + names.add(SAMPLE_DIMENSIONS_KEY); + } + final int numSources = getNumSources(); + for (int i=0; i<numSources; i++) { + String[] more = getSource(i).getPropertyNames(); + if (more != null) { + names.addAll(Arrays.asList(more)); + } } + names.retainAll(BandSelectImage.REDUCED_PROPERTIES); + return names.isEmpty() ? null : names.toArray(String[]::new); } /** * Gets a property of this image as a value derived from all source images. */ @Override + @SuppressWarnings("SuspiciousSystemArraycopy") public Object getProperty(final String key) { - if (sampleDimensions != null && SAMPLE_DIMENSIONS_KEY.equals(key)) { - return sampleDimensions.clone(); - } else { - return super.getProperty(key); + final int numBands = sampleModel.getNumBands(); + final Object result; + switch (key) { + case SAMPLE_DIMENSIONS_KEY: { + if (sampleDimensions != null) { + return sampleDimensions.toArray(SampleDimension[]::new); + } + result = new SampleDimension[numBands]; + break; + } + case STATISTICS_KEY: { + result = new Statistics[numBands]; + break; + } + case SAMPLE_RESOLUTIONS_KEY: { + var r = new double[numBands]; + Arrays.fill(r, Double.NaN); + result = r; + break; + } + default: return super.getProperty(key); + } + int offset = 0; + boolean found = false; + final int numSources = getNumSources(); + for (int i=0; i<numSources; i++) { + final RenderedImage source = getSource(i); + final int n = ImageUtilities.getNumBands(source); + final Object value = source.getProperty(key); + if (result.getClass().isInstance(value)) { + System.arraycopy(value, 0, result, offset, n); + found = true; + } + offset += n; } + return found ? result : null; } /** @@ -366,7 +409,7 @@ class BandAggregateImage extends MultiSourceImage { if (super.equals(object)) { final var that = (BandAggregateImage) object; return that.allowSharing == allowSharing && - Arrays.equals(that.sampleDimensions, sampleDimensions); + Objects.equals(that.sampleDimensions, sampleDimensions); } return false; } @@ -378,6 +421,6 @@ class BandAggregateImage extends MultiSourceImage { public int hashCode() { return super.hashCode() + Boolean.hashCode(allowSharing) - + Arrays.hashCode(sampleDimensions); + + Objects.hashCode(sampleDimensions); } } diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandAggregateLayout.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandAggregateLayout.java index 71094b89ba..976a8e4136 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandAggregateLayout.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandAggregateLayout.java @@ -16,8 +16,7 @@ */ package org.apache.sis.image; -import java.util.ArrayList; -import java.util.Arrays; +import java.util.List; import java.util.Optional; import java.awt.Point; import java.awt.Dimension; @@ -38,6 +37,7 @@ import org.apache.sis.coverage.privy.ImageUtilities; import org.apache.sis.coverage.privy.ColorModelFactory; import org.apache.sis.coverage.privy.BandAggregateArgument; import org.apache.sis.coverage.privy.CommonDomainFinder; +import org.apache.sis.coverage.privy.SampleDimensions; /** @@ -106,7 +106,7 @@ final class BandAggregateLayout { * Concatenated array of the sample dimensions declared in all sources, or {@code null} if none. * This field is non-null only if this information is present in all sources. */ - final SampleDimension[] sampleDimensions; + final List<SampleDimension> sampleDimensions; /** * Whether to allow the sharing of data buffers (instead of copying) if possible. @@ -355,7 +355,7 @@ search: for (int i=0; i < sources.length; i++) { base += (bands != null) ? bands.length : ImageUtilities.getNumBands(source); } if (colorizer != null) { - var target = new Colorizer.Target(sampleModel, UnmodifiableArrayList.wrap(sampleDimensions), visibleBand); + var target = new Colorizer.Target(sampleModel, sampleDimensions, visibleBand); Optional<ColorModel> candidate = colorizer.apply(target); if (candidate.isPresent()) { return candidate.get(); @@ -369,32 +369,27 @@ search: for (int i=0; i < sources.length; i++) { } /** - * Gets a concatenated array of the sample dimensions declared in all sources, or {@code null} if none. - * This method returns a non-null array only if this information is present in all sources. + * Gets a concatenated list of the sample dimensions declared in all sources, or {@code null} if none. + * The returned list should not contain null element (i.e., this method does not return partial list). */ - private SampleDimension[] getSampleDimensions() { - final var selected = new ArrayList<SampleDimension>(); - for (int i=0; i < sources.length; i++) { - final Object value = sources[i].getProperty(PlanarImage.SAMPLE_DIMENSIONS_KEY); - if (!(value instanceof SampleDimension[])) { - return null; - } - final var sd = (SampleDimension[]) value; - final int[] bands = bandsPerSource[i]; - if (bands == null) { - selected.addAll(Arrays.asList(sd)); - } else for (int j=0; j < bands.length; j++) { - final int t = bands[j]; - if (t < 0 || t >= sd.length) { - return null; - } - selected.add(sd[t]); - } + private List<SampleDimension> getSampleDimensions() { + List<SampleDimension> ranges = SampleDimensions.IMAGE_PROCESSOR_ARGUMENT.get(); + if (ranges != null) { + return ranges; } + int offset = 0; final var result = new SampleDimension[bandSelect.length]; - for (int i=0; i < result.length; i++) { - result[i] = selected.get(bandSelect[i]); + for (RenderedImage source : filteredSources) { + final Object value = source.getProperty(PlanarImage.SAMPLE_DIMENSIONS_KEY); + if (value instanceof SampleDimension[]) { + final var sd = (SampleDimension[]) value; + final int n = ImageUtilities.getNumBands(source); // Do not trust the array length. + System.arraycopy(sd, 0, result, offset, Math.min(sd.length, n)); + offset += n; + } else { + return null; + } } - return result; + return UnmodifiableArrayList.wrap(result); } } diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandSelectImage.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandSelectImage.java index b9a3727cd2..c2dc3b0c68 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandSelectImage.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandSelectImage.java @@ -60,7 +60,7 @@ class BandSelectImage extends SourceAlignedImage { * Shall be a subset of {@link #INHERITED_PROPERTIES}. * All values must be arrays. */ - private static final Set<String> REDUCED_PROPERTIES = Set.of( + static final Set<String> REDUCED_PROPERTIES = Set.of( SAMPLE_DIMENSIONS_KEY, SAMPLE_RESOLUTIONS_KEY, STATISTICS_KEY); /** @@ -166,6 +166,9 @@ class BandSelectImage extends SourceAlignedImage { /** * Returns the names of all recognized properties, * or {@code null} if this image has no properties. + * This method may conservatively return the names of properties that <em>may</em> exist. + * It does not check if the property would be an array with only null values, + * because doing that check may cause potentially costly computation. */ @Override public String[] getPropertyNames() { @@ -195,10 +198,13 @@ class BandSelectImage extends SourceAlignedImage { final Class<?> componentType = value.getClass().getComponentType(); if (componentType != null) { final Object reduced = Array.newInstance(componentType, bands.length); + boolean hasValue = false; for (int i=0; i<bands.length; i++) { - Array.set(reduced, i, Array.get(value, bands[i])); + Object element = Array.get(value, bands[i]); + Array.set(reduced, i, element); + hasValue |= (element != null); } - return reduced; + return hasValue ? reduced : Image.UndefinedProperty; } } return value; diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandedSampleConverter.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandedSampleConverter.java index 9d18e48b88..8a42951f09 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandedSampleConverter.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandedSampleConverter.java @@ -16,6 +16,7 @@ */ package org.apache.sis.image; +import java.util.List; import java.util.Arrays; import java.util.Objects; import java.awt.Rectangle; @@ -37,7 +38,6 @@ import org.apache.sis.coverage.privy.SampleDimensions; import org.apache.sis.coverage.privy.ColorScaleBuilder; import org.apache.sis.util.Numbers; import org.apache.sis.util.Disposable; -import org.apache.sis.util.privy.UnmodifiableArrayList; import org.apache.sis.util.logging.Logging; import org.apache.sis.math.DecimalFunctions; import org.apache.sis.measure.NumberRange; @@ -97,7 +97,7 @@ class BandedSampleConverter extends WritableComputedImage { * * @see #getProperty(String) */ - private final SampleDimension[] sampleDimensions; + private final List<SampleDimension> sampleDimensions; /** * The sample resolutions, or {@code null} if unknown. @@ -118,7 +118,7 @@ class BandedSampleConverter extends WritableComputedImage { private BandedSampleConverter(final RenderedImage source, final BandedSampleModel sampleModel, final ColorModel colorModel, final NumberRange<?>[] ranges, final MathTransform1D[] converters, - final SampleDimension[] sampleDimensions) + final List<SampleDimension> sampleDimensions) { super(sampleModel, source); this.colorModel = colorModel; @@ -169,14 +169,15 @@ class BandedSampleConverter extends WritableComputedImage { r = Double.NaN; } /* - * The implicit source resolution if 1 on the assumption that we are converting from + * The implicit source resolution is 1 on the assumption that we are converting from * integer values. But if the source image specifies a resolution, use the specified * value instead of the implicit 1 value. */ if (i < n) { final Number v = (Number) Array.get(sr, i); if (v != null) { - r *= (v instanceof Float) ? DecimalFunctions.floatToDouble(v.floatValue()) : v.doubleValue(); + double f = (v instanceof Float) ? DecimalFunctions.floatToDouble(v.floatValue()) : v.doubleValue(); + if (f > 0) r *= f; // Ignore also NaN. } } resolutions[i] = r; @@ -214,11 +215,11 @@ class BandedSampleConverter extends WritableComputedImage { } final int numBands = converters.length; final BandedSampleModel sampleModel = layout.createBandedSampleModel(source, null, targetType, numBands, 0); - final SampleDimension[] sampleDimensions = SampleDimensions.IMAGE_PROCESSOR_ARGUMENT.get(); + final List<SampleDimension> sampleDimensions = SampleDimensions.IMAGE_PROCESSOR_ARGUMENT.get(); final int visibleBand = ImageUtilities.getVisibleBand(source); ColorModel colorModel = ColorScaleBuilder.NULL_COLOR_MODEL; if (colorizer != null) { - var target = new Colorizer.Target(sampleModel, UnmodifiableArrayList.wrap(sampleDimensions), visibleBand); + var target = new Colorizer.Target(sampleModel, sampleDimensions, visibleBand); colorModel = colorizer.apply(target).orElse(null); } if (colorModel == null) { @@ -228,8 +229,8 @@ class BandedSampleConverter extends WritableComputedImage { * If no sample dimension is specified, infer value range from data type. */ SampleDimension sd = null; - if (sampleDimensions != null && visibleBand >= 0 && visibleBand < sampleDimensions.length) { - sd = sampleDimensions[visibleBand]; + if (sampleDimensions != null && visibleBand >= 0 && visibleBand < sampleDimensions.size()) { + sd = sampleDimensions.get(visibleBand); } final var builder = new ColorScaleBuilder(ColorScaleBuilder.GRAYSCALE, null, false); if (builder.initialize(source.getSampleModel(), sd) || @@ -269,7 +270,7 @@ class BandedSampleConverter extends WritableComputedImage { switch (key) { case SAMPLE_DIMENSIONS_KEY: { if (sampleDimensions != null) { - return sampleDimensions.clone(); + return sampleDimensions.toArray(SampleDimension[]::new); } break; } @@ -426,7 +427,7 @@ class BandedSampleConverter extends WritableComputedImage { Writable(final WritableRenderedImage source, final BandedSampleModel sampleModel, final ColorModel colorModel, final NumberRange<?>[] ranges, final MathTransform1D[] converters, final MathTransform1D[] inverses, - final SampleDimension[] sampleDimensions) + final List<SampleDimension> sampleDimensions) { super(source, sampleModel, colorModel, ranges, converters, sampleDimensions); this.inverses = inverses; diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/Colorizer.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/Colorizer.java index 2c6c9e363a..35bb45ab2a 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/Colorizer.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/Colorizer.java @@ -150,6 +150,8 @@ public interface Colorizer extends Function<Colorizer.Target, Optional<ColorMode * This information may be present if the image operation is invoked by a * {@link org.apache.sis.coverage.grid.GridCoverageProcessor} operation, * or if the source image contains the {@value PlanarImage#SAMPLE_DIMENSIONS_KEY} property + * Note that in the latter case, the list may contain null elements if this information is + * missing in some bands. * * @return description of the bands of the image to colorize. * @see org.apache.sis.coverage.grid.GridCoverage#getSampleDimensions() diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ImageProcessor.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ImageProcessor.java index f3e660e94e..b547c827da 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ImageProcessor.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ImageProcessor.java @@ -603,14 +603,15 @@ public class ImageProcessor implements Cloneable { /** * Returns statistics (minimum, maximum, mean, standard deviation) on each bands of the given image. * Invoking this method is equivalent to invoking the {@link #statistics statistics(…)} method and - * extracting immediately the statistics property value, except that custom - * {@linkplain #setErrorHandler error handlers} are supported. + * extracting immediately the statistics property value, except that this method guarantees that all + * statistics are non-null and supports custom {@linkplain #setErrorHandler error handlers}. * * <p>If {@code areaOfInterest} is null and {@code sampleFilters} is {@code null} or empty, * then the default behavior is as below:</p> * <ul> * <li>If the {@value PlanarImage#STATISTICS_KEY} property value exists in the given image, - * then that value is returned. Note that they are not necessarily statistics for the whole image. + * then that value is returned with the null array elements (if any) replaced by computed values. + * Note that the returned statistics are not necessarily for the whole image. * They are whatever statistics the property provider considered as representative.</li> * <li>Otherwise statistics are computed for the whole image.</li> * </ul> @@ -629,7 +630,7 @@ public class ImageProcessor implements Cloneable { * </ul> * * <h4>Result relationship with source</h4> - * This method computes statistics immediately. + * This method fetches (from property values) or 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. @@ -637,7 +638,7 @@ public class ImageProcessor implements Cloneable { * @param sampleFilters converters to apply on sample values before to add them to statistics, or * {@code null} or an empty array if none. The array may have any length and may contain null elements. * For all {@code i < numBands}, non-null {@code sampleFilters[i]} are applied to band <var>i</var>. - * @return the statistics of sample values in each band. + * @return the statistics of sample values in each band. Guaranteed non-null and without null element. * @throws ImagingOpException if an error occurred during calculation * and the error handler is {@link ErrorHandler#THROW}. * @@ -645,16 +646,36 @@ public class ImageProcessor implements Cloneable { * @see #filterNodataValues(Number...) * @see PlanarImage#STATISTICS_KEY */ - public Statistics[] valueOfStatistics(final RenderedImage source, final Shape areaOfInterest, + public Statistics[] valueOfStatistics(RenderedImage source, final Shape areaOfInterest, final DoubleUnaryOperator... sampleFilters) { ArgumentChecks.ensureNonNull("source", source); + int[] bandsToCompute = null; + Statistics[] statistics = null; if (areaOfInterest == null && (sampleFilters == null || ArraysExt.allEquals(sampleFilters, null))) { final Object property = source.getProperty(PlanarImage.STATISTICS_KEY); if (property instanceof Statistics[]) { - return (Statistics[]) property; + statistics = ArraysExt.resize((Statistics[]) property, ImageUtilities.getNumBands(source)); + /* + * Verify that all array elements are non-null. If any null element is found, + * we will compute statistics but only for the missing bands. + */ + bandsToCompute = new int[statistics.length]; + int n = 0; + for (int i=0; i<statistics.length; i++) { + if (statistics[i] == null) { + bandsToCompute[n++] = i; + } + } + if (n == 0) return statistics; + bandsToCompute = ArraysExt.resize(bandsToCompute, n); + source = selectBands(source, bandsToCompute); } } + /* + * Compute statistics either of all bands, or on a subset + * of the bands if only some of them have null statistics. + */ final boolean parallel, failOnException; final ErrorHandler errorListener; synchronized (this) { @@ -667,10 +688,17 @@ public class ImageProcessor implements Cloneable { * The way AnnotatedImage cache mechanism is implemented, if statistics results already * exist, they will be used. */ - final AnnotatedImage calculator = new StatisticsCalculator(source, areaOfInterest, sampleFilters, parallel, failOnException); + final var calculator = new StatisticsCalculator(source, areaOfInterest, sampleFilters, parallel, failOnException); final Object property = calculator.getProperty(PlanarImage.STATISTICS_KEY); calculator.logAndClearError(ImageProcessor.class, "valueOfStatistics", errorListener); - return (Statistics[]) property; + final var computed = (Statistics[]) property; + if (bandsToCompute == null) { + return computed; + } + for (int i=0; i<bandsToCompute.length; i++) { + statistics[bandsToCompute[i]] = computed[i]; + } + return statistics; } /** @@ -803,6 +831,7 @@ public class ImageProcessor implements Cloneable { * * <b>Note:</b> if no value is associated to the {@code "sampleDimensions"} key, then the default * value will be the {@value PlanarImage#SAMPLE_DIMENSIONS_KEY} image property value if defined. + * That value can be an array, in which case the sample dimension of the visible band is taken. * * <h4>Properties used</h4> * This operation uses the following properties in addition to method parameters: diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/PlanarImage.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/PlanarImage.java index 05583a59a2..1e504d103d 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/PlanarImage.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/PlanarImage.java @@ -149,6 +149,11 @@ public abstract class PlanarImage implements RenderedImage { * Key for a property defining a conversion from pixel values to the units of measurement. * The value should be an array of {@link SampleDimension} instances. * The array length should be the number of bands. + * The array may contain null elements if this information is missing in some bands. + * + * <div class="note"><b>Example:</b> null elements may happen if this image is an + * {@linkplain ImageProcessor#aggregateBands(RenderedImage...) aggregation of bands} + * of two or more images, and some but not all images define this property.</div> * * @see org.apache.sis.coverage.grid.GridCoverage#getSampleDimensions() * @@ -169,7 +174,8 @@ public abstract class PlanarImage implements RenderedImage { * <p>Values should be instances of {@code double[]}. * The array length should be the number of bands. This property may be computed automatically during * {@linkplain org.apache.sis.coverage.grid.GridCoverage#forConvertedValues(boolean) conversions from - * integer values to floating point values}.</p> + * integer values to floating point values}. Values should be strictly positive and finite but may be + * {@link Double#NaN} if this information is unknown for a band.</p> */ public static final String SAMPLE_RESOLUTIONS_KEY = "org.apache.sis.SampleResolutions"; @@ -180,11 +186,13 @@ public abstract class PlanarImage implements RenderedImage { * actually used in an image. * * <p>Values should be instances of <code>{@linkplain org.apache.sis.math.Statistics}[]</code>. - * The array length should be the number of bands. If this property is not provided, Apache SIS - * may have to {@linkplain ImageProcessor#statistics compute statistics itself} - * (by iterating over pixel values) when needed.</p> + * The array length should be the number of bands. Some array elements may be {@code null} + * if the statistics are not available for all bands.</p> * - * <p>Statistics are only indicative. They may be computed on an image sub-region.</p> + * <p>Statistics are only indicative. They may be computed on a subset of the sample values. + * If this property is not provided, some image rendering or exportation processes may have + * to {@linkplain ImageProcessor#statistics compute statistics themselves} by iterating over + * pixel values, which can be costly.</p> * * @see ImageProcessor#statistics(RenderedImage, Shape, DoubleUnaryOperator...) */ @@ -282,6 +290,8 @@ public abstract class PlanarImage implements RenderedImage { /** * Returns the names of all recognized properties, * or {@code null} if this image has no properties. + * This method may conservatively return the names of properties that <em>may</em> exist, + * when checking if they actually exist would cause a potentially costly computation. * * <p>The default implementation returns {@code null}.</p> * diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/RecoloredImage.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/RecoloredImage.java index ac3569851d..7c6f30f023 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/RecoloredImage.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/RecoloredImage.java @@ -253,6 +253,7 @@ final class RecoloredImage extends ImageAdapter { } else if (value instanceof Statistics) { statistics = (Statistics) value; } else if (value instanceof Statistics[]) { + // Undocumented: one element per band, will keep only the visible band. statsAllBands = (Statistics[]) value; } else { throw illegalPropertyType(modifiers, "statistics", value); @@ -273,7 +274,7 @@ final class RecoloredImage extends ImageAdapter { if (Double.isNaN(minimum) || Double.isNaN(maximum)) { if (statistics == null) { if (statsAllBands == null) { - final DoubleUnaryOperator[] sampleFilters = new DoubleUnaryOperator[visibleBand + 1]; + final var sampleFilters = new DoubleUnaryOperator[visibleBand + 1]; sampleFilters[visibleBand] = ImageProcessor.filterNodataValues(nodataValues); statsAllBands = processor.valueOfStatistics(statsSource, areaOfInterest, sampleFilters); } diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/Visualization.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/Visualization.java index a712ae34d9..06d0c8caa7 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/Visualization.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/Visualization.java @@ -20,6 +20,7 @@ import java.util.Map; import java.util.List; import java.util.Arrays; import java.util.Objects; +import java.util.Collections; import java.util.function.Function; import java.util.function.DoubleUnaryOperator; import java.awt.Color; @@ -48,6 +49,7 @@ import org.apache.sis.feature.internal.Resources; import org.apache.sis.measure.NumberRange; import org.apache.sis.math.Statistics; import org.apache.sis.util.collection.BackingStoreException; +import org.apache.sis.util.privy.UnmodifiableArrayList; /** @@ -149,7 +151,7 @@ final class Visualization extends ResampledImage { private MathTransform toSource; /** Description of {@link #source} bands, or {@code null} if none. */ - private SampleDimension[] sampleDimensions; + private List<SampleDimension> sampleDimensions; // ┌─────────────────────────────────────┐ // │ Given by ImageProcesor.configure(…) │ @@ -198,7 +200,7 @@ final class Visualization extends ResampledImage { if (sampleDimensions == null) { Object ranges = source.getProperty(SAMPLE_DIMENSIONS_KEY); if (ranges instanceof SampleDimension[]) { - sampleDimensions = (SampleDimension[]) ranges; + sampleDimensions = UnmodifiableArrayList.wrap((SampleDimension[]) ranges); } } } @@ -250,8 +252,8 @@ final class Visualization extends ResampledImage { } } source = BandSelectImage.create(source, true, visibleBand); - final SampleDimension visibleSD = (sampleDimensions != null && visibleBand < sampleDimensions.length) - ? sampleDimensions[visibleBand] : null; + final SampleDimension visibleSD = (sampleDimensions != null && visibleBand < sampleDimensions.size()) + ? sampleDimensions.get(visibleBand) : null; /* * If there is no conversion of pixel coordinates, there is no need for interpolations. * In such case the `Visualization.computeTile(…)` implementation takes a shortcut which @@ -287,7 +289,7 @@ final class Visualization extends ResampledImage { * In precedence order: * * - rangeColors : Map<NumberRange<?>,Color[]> - * - sampleDimensions : SampleDimension[] + * - sampleDimensions : List<SampleDimension> * - statistics */ boolean initialized; @@ -337,7 +339,7 @@ final class Visualization extends ResampledImage { * If none of above `ColorScaleBuilder` configurations worked, use statistics in last resort. * We do that after we reduced the image to a single band in order to reduce the amount of calculation. */ - final DoubleUnaryOperator[] sampleFilters = SampleDimensions.toSampleFilters(visibleSD); + final DoubleUnaryOperator[] sampleFilters = SampleDimensions.toSampleFilters(Collections.singletonList(visibleSD)); final Statistics statistics = processor.valueOfStatistics(source, null, sampleFilters)[VISIBLE_BAND]; builder.initialize(statistics.minimum(), statistics.maximum(), sourceSM.getDataType()); } diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/BandAggregateImageTest.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/BandAggregateImageTest.java index 18bd44bb2a..af6862fcba 100644 --- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/BandAggregateImageTest.java +++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/BandAggregateImageTest.java @@ -18,11 +18,13 @@ package org.apache.sis.image; import java.util.Arrays; import java.util.HashSet; +import java.util.Hashtable; import java.util.stream.IntStream; import java.util.function.ObjIntConsumer; import java.awt.Rectangle; import java.awt.image.BandedSampleModel; import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; import java.awt.image.DataBuffer; import java.awt.image.Raster; import java.awt.image.RenderedImage; @@ -525,4 +527,36 @@ public final class BandAggregateImageTest extends TestCase { } } } + + /** + * Verifies the aggregation of property values. + */ + @Test + public void testProperties() { + final var p1 = new Hashtable<String,Object>(); + final var p2 = new Hashtable<String,Object>(); + assertNull(p1.put(PlanarImage.SAMPLE_RESOLUTIONS_KEY, new double[] {4, 1, 3, 7})); + assertNull(p2.put(PlanarImage.SAMPLE_RESOLUTIONS_KEY, new double[] {2, 8, 5, 6})); + final ColorModel cm = ColorModel.getRGBdefault(); + final WritableRaster raster = cm.createCompatibleWritableRaster(1, 1); + final RenderedImage[] sources = { + new BufferedImage(cm, raster, false, p1), + new BufferedImage(cm, raster, false, p2) + }; + RenderedImage result; + result = BandAggregateImage.create(sources, null, null, false, allowSharing, false); + assertArrayEquals(new String[] {PlanarImage.SAMPLE_RESOLUTIONS_KEY}, result.getPropertyNames()); + assertArrayEquals(new double[] {4, 1, 3, 7, 2, 8, 5, 6}, + (double[]) result.getProperty(PlanarImage.SAMPLE_RESOLUTIONS_KEY)); + /* + * Same tests, but with a subset of the bands. + * This part of the test depends on `BandSelectImage`. + */ + sources[0] = BandSelectImage.create(sources[0], false, 0, 2); + sources[1] = BandSelectImage.create(sources[1], false, 1, 3); + result = BandAggregateImage.create(sources, null, null, false, allowSharing, false); + assertArrayEquals(new String[] {PlanarImage.SAMPLE_RESOLUTIONS_KEY}, result.getPropertyNames()); + assertArrayEquals(new double[] {4, 3, 8, 6}, + (double[]) result.getProperty(PlanarImage.SAMPLE_RESOLUTIONS_KEY)); + } } diff --git a/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/map/coverage/RenderingData.java b/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/map/coverage/RenderingData.java index c43355260f..c52769e002 100644 --- a/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/map/coverage/RenderingData.java +++ b/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/map/coverage/RenderingData.java @@ -200,7 +200,7 @@ public class RenderingData implements CloneAccess { * @see #setImageSpace(GridGeometry, List, int[]) * @see #statistics() */ - private SampleDimension[] dataRanges; + private List<SampleDimension> dataRanges; /** * Conversion or transformation from {@linkplain #data} CRS to {@linkplain PlanarCanvas#getObjectiveCRS() @@ -312,7 +312,7 @@ public class RenderingData implements CloneAccess { */ @SuppressWarnings("AssignmentToCollectionOrArrayFieldFromParameter") public final void setImageSpace(final GridGeometry domain, final List<SampleDimension> ranges, final int[] xyDims) { - dataRanges = (ranges != null) ? ranges.toArray(SampleDimension[]::new) : null; + dataRanges = ranges; dataGeometry = domain; xyDimensions = xyDims; processor.setFillValues(SampleDimensions.backgrounds(dataRanges)); @@ -544,7 +544,7 @@ public class RenderingData implements CloneAccess { } statistics = processor.valueOfStatistics(image, null, SampleDimensions.toSampleFilters(dataRanges)); } - final Map<String,Object> modifiers = new HashMap<>(8); + final var modifiers = new HashMap<String,Object>(8); modifiers.put("statistics", statistics); modifiers.put("sampleDimensions", dataRanges); return modifiers; diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/ReformattedImage.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/ReformattedImage.java index e1a4218dba..d698893567 100644 --- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/ReformattedImage.java +++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/ReformattedImage.java @@ -190,7 +190,9 @@ found: if (property instanceof Statistics[]) { final var max = new double[numBands]; for (int i=0; i<numBands; i++) { final Statistics s = stats[i]; - if (s.count() == 0) break found; + if (s == null || s.count() == 0) { + break found; // Some statistics are missing. + } min[i] = s.minimum(); max[i] = s.maximum(); }