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 <[email protected]>
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();
}