sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] branch geoapi-4.0 updated: Use statistics for adjusting the minimum and maximum values between which the gray scale tones are applied.
Date Sun, 01 Mar 2020 23:10:37 GMT
This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 626cf06  Use statistics for adjusting the minimum and maximum values between which
the gray scale tones are applied.
626cf06 is described below

commit 626cf060399e4fa57cc76227bb392651374726dc
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Mon Mar 2 00:09:04 2020 +0100

    Use statistics for adjusting the minimum and maximum values between which the gray scale
tones are applied.
---
 .../org/apache/sis/gui/coverage/CoverageView.java  |  23 +++-
 .../apache/sis/internal/gui/ImageRenderings.java   |  58 +++++++++
 .../java/org/apache/sis/image/AnnotatedImage.java  |  71 +++--------
 .../java/org/apache/sis/image/ImageAdapter.java    | 138 +++++++++++++++++++++
 .../java/org/apache/sis/image/ImageOperations.java |  74 +++++++++++
 .../java/org/apache/sis/image/PixelIterator.java   |   4 +-
 .../java/org/apache/sis/image/RecoloredImage.java  |  94 ++++++++++++++
 .../internal/coverage/j2d/ColorModelFactory.java   |  46 ++++++-
 .../sis/internal/coverage/j2d/ImageUtilities.java  |  27 ++++
 .../sis/internal/coverage/j2d/RasterFactory.java   |  14 +--
 10 files changed, 482 insertions(+), 67 deletions(-)

diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageView.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageView.java
index 77381a9..76f629f 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageView.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageView.java
@@ -17,6 +17,7 @@
 package org.apache.sis.gui.coverage;
 
 import java.util.Locale;
+import java.util.EnumMap;
 import java.nio.IntBuffer;
 import java.awt.Graphics2D;
 import java.awt.geom.AffineTransform;
@@ -43,6 +44,7 @@ import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.internal.coverage.j2d.ColorModelFactory;
+import org.apache.sis.internal.gui.ImageRenderings;
 import org.apache.sis.internal.map.PlanarCanvas;
 import org.apache.sis.internal.util.Numerics;
 
@@ -82,6 +84,11 @@ final class CoverageView extends PlanarCanvas {
     public final ObjectProperty<GridExtent> sliceExtentProperty;
 
     /**
+     * Different ways to represent the data. The {@link #data} field shall be one value from
this map.
+     */
+    private final EnumMap<RangeType,RenderedImage> dataAlternatives;
+
+    /**
      * The data to shown, or {@code null} if not yet specified. This image may be tiled,
      * and fetching tiles may require computations to be performed in background thread.
      * The size of this image is not necessarily {@link #buffer} or {@link #image} size.
@@ -133,6 +140,7 @@ final class CoverageView extends PlanarCanvas {
         super(Locale.getDefault());
         coverageProperty    = new SimpleObjectProperty<>(this, "coverage");
         sliceExtentProperty = new SimpleObjectProperty<>(this, "sliceExtent");
+        dataAlternatives    = new EnumMap<>(RangeType.class);
         dataToImage = new AffineTransform();
         view = new Pane() {
             @Override protected void layoutChildren() {
@@ -224,9 +232,11 @@ final class CoverageView extends PlanarCanvas {
         image.setImage(null);
         data   = null;
         buffer = null;
+        dataAlternatives.clear();
         final GridCoverage coverage = getCoverage();
         if (coverage != null) {
             data = coverage.render(getSliceExtent());     // TODO: background thread.
+            dataAlternatives.put(RangeType.DECLARED, data);
             view.requestLayout();
         }
     }
@@ -244,6 +254,17 @@ final class CoverageView extends PlanarCanvas {
     final void onRangeTypeChanged(final ObservableValue<? extends RangeType> property,
                                   final RangeType previous, final RangeType value)
     {
+        RenderedImage alt = dataAlternatives.get(value);
+        if (alt == null) {
+            alt = dataAlternatives.get(RangeType.DECLARED);     // Source needed by all alternatives.
+            if (value == RangeType.AUTOMATIC) {
+                alt = ImageRenderings.automaticScale(alt);
+            }
+        }
+        if (alt != data) {
+            data = alt;
+            view.requestLayout();
+        }
     }
 
     /**
@@ -292,7 +313,7 @@ final class CoverageView extends PlanarCanvas {
     }
 
     /**
-     * The task for updating an image be reusing existing resources (JavaFX image and Java2D
buffered image).
+     * The task for updating an image by reusing existing resources (JavaFX image and Java2D
buffered image).
      * This task is used when the image size did not changed.
      *
      * @todo Extend {@code Task}, write in a background thread in a {@link VolatileImage}
(may be long especially
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImageRenderings.java
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImageRenderings.java
new file mode 100644
index 0000000..2de16be
--- /dev/null
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImageRenderings.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.gui;
+
+import java.awt.image.RenderedImage;
+import org.apache.sis.image.ImageOperations;
+
+
+/**
+ * Operations on images for rendering purposes. The methods defined in this class delegate
+ * to methods in the rest of SIS library with some arbitrary parameter value choices.
+ * We use this class as a way to centralize where those choices are made for GUI purposes.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public final class ImageRenderings {
+    /**
+     * The set of operations to use.
+     *
+     * @todo Creates our own instance which listen to logging messages.
+     *       We need to create a logging panel first.
+     */
+    private static final ImageOperations OPERATIONS = ImageOperations.LENIENT;
+
+    /**
+     * Do not allow instantiation of this class.
+     */
+    private ImageRenderings() {
+    }
+
+    /**
+     * Rescale the given image between a minimum and maximum values determined from statistics.
+     * If the given image is null or can not be rescaled, then it is returned as-is.
+     *
+     * @param  image  the image to rescale, or {@code null}.
+     * @return the rescaled image.
+     */
+    public static RenderedImage automaticScale(final RenderedImage image) {
+        return OPERATIONS.automaticColorRamp(image, 3);
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/AnnotatedImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/AnnotatedImage.java
index 94c0c37..e8e7016 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/AnnotatedImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/AnnotatedImage.java
@@ -17,7 +17,6 @@
 package org.apache.sis.image;
 
 import java.util.Locale;
-import java.util.Vector;
 import java.util.WeakHashMap;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
@@ -25,11 +24,8 @@ import java.util.logging.Filter;
 import java.util.stream.Collector;
 import java.awt.Image;
 import java.awt.Rectangle;
-import java.awt.image.ColorModel;
-import java.awt.image.SampleModel;
 import java.awt.image.RenderedImage;
 import java.awt.image.Raster;
-import java.awt.image.WritableRaster;
 import java.awt.image.ImagingOpException;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.logging.Logging;
@@ -63,7 +59,7 @@ import org.apache.sis.internal.coverage.j2d.TileOpExecutor;
  * @since   1.1
  * @module
  */
-abstract class AnnotatedImage implements RenderedImage {
+abstract class AnnotatedImage extends ImageAdapter {
     /**
      * The suffix to add to property name for errors that occurred during computation.
      * A property with suffix is automatically created if an exception is thrown during
@@ -103,11 +99,6 @@ abstract class AnnotatedImage implements RenderedImage {
     private final Cache<String,Object> cache;
 
     /**
-     * The source image from which to compute the property.
-     */
-    protected final RenderedImage source;
-
-    /**
      * The errors that occurred while computing the result, or {@code null} if none or not
yet determined.
      * This field is never set if {@link #failOnException} is {@code true}.
      */
@@ -132,8 +123,8 @@ abstract class AnnotatedImage implements RenderedImage {
      * @param  parallel         whether parallel execution is authorized.
      * @param  failOnException  whether errors occurring during computation should be propagated.
      */
-    protected AnnotatedImage(final RenderedImage source, final boolean parallel, final boolean
failOnException) {
-        this.source          = source;
+    protected AnnotatedImage(RenderedImage source, final boolean parallel, final boolean
failOnException) {
+        super(source);
         this.parallel        = parallel;
         this.failOnException = failOnException;
         /*
@@ -143,27 +134,19 @@ abstract class AnnotatedImage implements RenderedImage {
          * cache is shared by all images using the same data. This is okay if calculation
depends
          * only on sample value, not on other data.
          */
-        if (source instanceof AnnotatedImage) {
-            cache = ((AnnotatedImage) source).cache;        // Cache for the source of the
source.
-        } else synchronized (CACHE) {
+        while (source instanceof ImageAdapter) {
+            if (source instanceof AnnotatedImage) {
+                cache = ((AnnotatedImage) source).cache;        // Cache for the source of
the source.
+                return;
+            }
+            source = ((ImageAdapter) source).source;
+        }
+        synchronized (CACHE) {
             cache = CACHE.computeIfAbsent(source, (k) -> new Cache<>(8, 1000, true));
         }
     }
 
     /**
-     * Returns the {@linkplain #source} of this image in an vector of length 1.
-     *
-     * @return the unique {@linkplain #source} of this image.
-     */
-    @Override
-    @SuppressWarnings("UseOfObsoleteCollectionType")
-    public final Vector<RenderedImage> getSources() {
-        final Vector<RenderedImage> sources = new Vector<>(1);
-        sources.add(source);
-        return sources;
-    }
-
-    /**
      * Returns the name of the property which is computed by this image.
      *
      * @return name of property computed by this image. Shall not be null.
@@ -411,35 +394,17 @@ abstract class AnnotatedImage implements RenderedImage {
         return value;
     }
 
-    /** Delegates to the wrapped image. */
-    @Override public final ColorModel     getColorModel()            {return source.getColorModel();}
-    @Override public final SampleModel    getSampleModel()           {return source.getSampleModel();}
-    @Override public final int            getWidth()                 {return source.getWidth();}
-    @Override public final int            getHeight()                {return source.getHeight();}
-    @Override public final int            getMinX()                  {return source.getMinX();}
-    @Override public final int            getMinY()                  {return source.getMinY();}
-    @Override public final int            getNumXTiles()             {return source.getNumXTiles();}
-    @Override public final int            getNumYTiles()             {return source.getNumYTiles();}
-    @Override public final int            getMinTileX()              {return source.getMinTileX();}
-    @Override public final int            getMinTileY()              {return source.getMinTileY();}
-    @Override public final int            getTileWidth()             {return source.getTileWidth();}
-    @Override public final int            getTileHeight()            {return source.getTileHeight();}
-    @Override public final int            getTileGridXOffset()       {return source.getTileGridXOffset();}
-    @Override public final int            getTileGridYOffset()       {return source.getTileGridYOffset();}
-    @Override public final Raster         getTile(int tx, int ty)    {return source.getTile(tx,
ty);}
-    @Override public final Raster         getData()                  {return source.getData();}
-    @Override public final Raster         getData(Rectangle region)  {return source.getData(region);}
-    @Override public final WritableRaster copyData(WritableRaster r) {return source.copyData(r);}
-
     /**
-     * Returns a string representation of this image for debugging purpose.
+     * Appends the name of the computed property in the {@link #toString()} representation,
+     * after the class name and before the string representation of the wrapped image.
      */
     @Override
-    public String toString() {
-        final StringBuilder buffer = new StringBuilder(100).append(AnnotatedImage.class.getSimpleName()).append('[');
-        if (cache.containsKey(getComputedPropertyName())) {
+    final Class<AnnotatedImage> stringStart(final StringBuilder buffer) {
+        final String key = getComputedPropertyName();
+        if (cache.containsKey(key)) {
             buffer.append("Cached ");
         }
-        return buffer.append("[\"").append(getComputedPropertyName()).append("\" on ").append(source).append(']').toString();
+        buffer.append('"').append(key).append('"');
+        return AnnotatedImage.class;
     }
 }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/ImageAdapter.java b/core/sis-feature/src/main/java/org/apache/sis/image/ImageAdapter.java
new file mode 100644
index 0000000..5d5871f
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ImageAdapter.java
@@ -0,0 +1,138 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.image;
+
+import java.util.Vector;
+import java.awt.Image;
+import java.awt.Rectangle;
+import java.awt.image.ColorModel;
+import java.awt.image.SampleModel;
+import java.awt.image.RenderedImage;
+import java.awt.image.Raster;
+import java.awt.image.WritableRaster;
+
+
+/**
+ * An image which wraps an existing image unchanged, except for properties and/or color model.
+ * All {@link RenderedImage} methods related to coordinate systems (pixel coordinates or
tile
+ * indices), and all methods fetching tiles, delegate to the wrapped image.
+ *
+ * <div class="note"><b>Design note:</b>
+ * most non-abstract methods are final because {@link PixelIterator} (among others) relies
+ * on the fact that it can unwrap this image and still get the same pixel values.</div>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+abstract class ImageAdapter extends PlanarImage {
+    /**
+     * The source image wrapped by this adapter.
+     */
+    protected final RenderedImage source;
+
+    /**
+     * Creates a new wrapper for the given image.
+     *
+     * @param  source  the image to wrap.
+     */
+    protected ImageAdapter(final RenderedImage source) {
+        this.source = source;
+    }
+
+    /**
+     * Returns the {@linkplain #source} of this image in an vector of length 1.
+     *
+     * @return the unique {@linkplain #source} of this image.
+     */
+    @Override
+    @SuppressWarnings("UseOfObsoleteCollectionType")
+    public final Vector<RenderedImage> getSources() {
+        final Vector<RenderedImage> sources = new Vector<>(1);
+        sources.add(source);
+        return sources;
+    }
+
+    /**
+     * Returns the names of properties of wrapped image.
+     *
+     * @return all recognized property names.
+     */
+    @Override
+    public String[] getPropertyNames() {
+        return source.getPropertyNames();
+    }
+
+    /**
+     * Gets a property from this image or from its source.
+     *
+     * @param  name  name of the property to get.
+     * @return the property for the given name ({@code null} is a valid result),
+     *         or {@link Image#UndefinedProperty} if the given name is not a recognized property
name.
+     */
+    @Override
+    public Object getProperty(final String name) {
+        return source.getProperty(name);
+    }
+
+    /**
+     * Returns the color model of this image.
+     */
+    @Override
+    public ColorModel getColorModel() {
+        return source.getColorModel();
+    }
+
+    /** Delegates to the wrapped image. */
+    @Override public final SampleModel    getSampleModel()           {return source.getSampleModel();}
+    @Override public final int            getWidth()                 {return source.getWidth();}
+    @Override public final int            getHeight()                {return source.getHeight();}
+    @Override public final int            getMinX()                  {return source.getMinX();}
+    @Override public final int            getMinY()                  {return source.getMinY();}
+    @Override public final int            getNumXTiles()             {return source.getNumXTiles();}
+    @Override public final int            getNumYTiles()             {return source.getNumYTiles();}
+    @Override public final int            getMinTileX()              {return source.getMinTileX();}
+    @Override public final int            getMinTileY()              {return source.getMinTileY();}
+    @Override public final int            getTileWidth()             {return source.getTileWidth();}
+    @Override public final int            getTileHeight()            {return source.getTileHeight();}
+    @Override public final int            getTileGridXOffset()       {return source.getTileGridXOffset();}
+    @Override public final int            getTileGridYOffset()       {return source.getTileGridYOffset();}
+    @Override public final Raster         getTile(int tx, int ty)    {return source.getTile(tx,
ty);}
+    @Override public final Raster         getData()                  {return source.getData();}
+    @Override public final Raster         getData(Rectangle region)  {return source.getData(region);}
+    @Override public final WritableRaster copyData(WritableRaster r) {return source.copyData(r);}
+
+    /**
+     * Returns a string representation of this image for debugging purpose.
+     */
+    @Override
+    public String toString() {
+        final StringBuilder buffer = new StringBuilder(100);
+        final Class<?> subtype = stringStart(buffer.append('['));
+        return buffer.insert(0, subtype.getSimpleName()).append(" on ").append(source).append(']').toString();
+    }
+
+    /**
+     * Appends a content to show in the {@link #toString()} representation,
+     * after the class name and before the string representation of the wrapped image.
+     *
+     * @param  buffer  where to start writing content of {@link #toString()} representation.
+     * @return name of the class to show in the {@link #toString()} representation.
+     */
+    abstract Class<? extends ImageAdapter> stringStart(StringBuilder buffer);
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/ImageOperations.java b/core/sis-feature/src/main/java/org/apache/sis/image/ImageOperations.java
index 5d7c2b4..e926c3c 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/ImageOperations.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ImageOperations.java
@@ -22,7 +22,9 @@ import java.awt.image.RenderedImage;
 import java.awt.image.ImagingOpException;
 import org.apache.sis.math.Statistics;
 import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.resources.Errors;
 import org.apache.sis.internal.system.Modules;
+import org.apache.sis.internal.coverage.j2d.ImageUtilities;
 
 
 /**
@@ -139,4 +141,76 @@ public class ImageOperations {
         }
         return null;
     }
+
+    /**
+     * Returns an image with the same sample values than the given image, but with its color
ramp rescaled between the specified bounds.
+     * For example in a gray scale image, pixels with the minimum value will be black and
pixels with the maximum value will be white.
+     * This operation is a kind of <cite>tone mapping</cite>, a technique used
in image processing to map one set of colors to another.
+     * The mapping applied by this method is conceptually a simple linear transform (a scale
and an offset)
+     * applied on sample values before they are mapped to their colors.
+     *
+     * <p>Current implementation can remap only gray scale images (it may be extended
to indexed color models
+     * in a future version). If this method can not rescale the color ramp, for example because
the given image
+     * is an RGB image, then the image is returned unchanged.</p>
+     *
+     * @param  source    the image to recolor (may be {@code null}).
+     * @param  minimum   the sample value to display with the first color of the color ramp
(black in a grayscale image).
+     * @param  maximum   the sample value to display with the last color of the color ramp
(white in a grayscale image).
+     * @return the image with color ramp rescaled between the given bounds, or {@code image}
unchanged if the operation
+     *         can not be applied on the given image.
+     */
+    public RenderedImage rescaleColorRamp(final RenderedImage source, final double minimum,
final double maximum) {
+        ArgumentChecks.ensureFinite("minimum", minimum);
+        ArgumentChecks.ensureFinite("maximum", maximum);
+        if (!(minimum < maximum)) {
+            throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalRange_2,
minimum, maximum));
+        }
+        final int visibleBand = ImageUtilities.getVisibleBand(source);
+        if (visibleBand >= 0) {
+            return RecoloredImage.rescale(source, visibleBand, minimum, maximum);
+        }
+        return source;
+    }
+
+    /**
+     * Returns an image with the same sample values than the given image, but with its color
ramp rescaled between
+     * automatically determined bounds. This is the same operation than {@link #rescaleColorRamp
rescaleColorRamp(…)}
+     * except that the minimum and maximum values are determined by {@linkplain #statistics(RenderedImage)
statistics}
+     * on the image: a range of value is determined first from the {@linkplain Statistics#minimum()
minimum} and
+     * {@linkplain Statistics#maximum() maximum} values found in the image, optionally narrowed
to an interval
+     * of some {@linkplain Statistics#standardDeviation(boolean) standard deviations} around
the mean value.
+     *
+     * <p>Narrowing with standard deviations is useful for data having a Gaussian distribution,
as often seen in nature.
+     * In such distribution, 99.9% of the data are between the mean ± 3×<var>standard
deviation</var>, but some values
+     * may still appear much further. The minimum and maximum values alone are not a robust
way to compute a range of
+     * values for the color ramp because a single value very far from other values is sufficient
for making the colors
+     * difficult to distinguish for 99.9% of the data.</p>
+     *
+     * @param  source      the image to recolor (may be {@code null}).
+     * @param  deviations  multiple of standard deviations around the mean, of {@link Double#POSITIVE_INFINITY}
+     *                     for not using standard deviation for narrowing the range of values.
+     *                     Some values giving good results for a Gaussian distribution are
1.5, 2 or 3.
+     * @return the image with color ramp rescaled between the automatic bounds,
+     *         or {@code image} unchanged if the operation can not be applied on the given
image.
+     */
+    public RenderedImage automaticColorRamp(final RenderedImage source, double deviations)
{
+        ArgumentChecks.ensureStrictlyPositive("deviations", deviations);
+        final int visibleBand = ImageUtilities.getVisibleBand(source);
+        if (visibleBand >= 0) {
+            final Statistics[] statistics = statistics(source);
+            if (statistics != null && visibleBand < statistics.length) {
+                final Statistics s = statistics[visibleBand];
+                if (s != null) {
+                    deviations *= s.standardDeviation(true);
+                    final double mean    = s.mean();
+                    final double minimum = Math.max(s.minimum(), mean - deviations);
+                    final double maximum = Math.min(s.maximum(), mean + deviations);
+                    if (minimum < maximum) {
+                        return RecoloredImage.rescale(source, visibleBand, minimum, maximum);
+                    }
+                }
+            }
+        }
+        return source;
+    }
 }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/PixelIterator.java b/core/sis-feature/src/main/java/org/apache/sis/image/PixelIterator.java
index a90d5c7..5923ed8 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/PixelIterator.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/PixelIterator.java
@@ -290,8 +290,8 @@ public abstract class PixelIterator {
          * also necessary for allowing the builder to recognize the {@link BufferedImage}
case.
          */
         private static RenderedImage unwrap(RenderedImage image) {
-            while (image instanceof AnnotatedImage) {
-                image = ((AnnotatedImage) image).source;
+            while (image instanceof ImageAdapter) {
+                image = ((ImageAdapter) image).source;
             }
             return image;
         }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/RecoloredImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/RecoloredImage.java
new file mode 100644
index 0000000..7dd00e0
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/RecoloredImage.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.image;
+
+import java.awt.image.ColorModel;
+import java.awt.image.SampleModel;
+import java.awt.image.RenderedImage;
+import org.apache.sis.internal.coverage.j2d.ColorModelFactory;
+
+
+/**
+ * An image with the same sample values than the wrapped image but a different color model.
+ * Current implementation can only apply a gray scale. Future implementations may detect
+ * the existing color model and try to preserve colors (for example by building an indexed
+ * color model).
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+final class RecoloredImage extends ImageAdapter {
+    /**
+     * The color model to associate with this recolored image.
+     */
+    private final ColorModel colors;
+
+    /**
+     * Creates a new recolored image with the given colors.
+     */
+    private RecoloredImage(final RenderedImage source, final ColorModel colors) {
+        super(source);
+        this.colors = colors;
+    }
+
+    /**
+     * Wraps the given image with its colors ramp scaled between the given bounds. If the
given image is
+     * already using a color ramp for the given range of values, then that image is returned
unchanged.
+     *
+     * @param  source       the image to recolor.
+     * @param  visibleBand  the band to make visible.
+     * @param  minimum      the sample value to display with the first color of the color
ramp (black in a grayscale image).
+     * @param  maximum      the sample value to display with the last color of the color
ramp (white in a grayscale image).
+     * @return the image with color ramp rescaled between the given bounds. May be the given
image returned as-is.
+     */
+    static RenderedImage rescale(RenderedImage source, final int visibleBand, final double
minimum, final double maximum) {
+        final SampleModel sm = source.getSampleModel();
+        final int dataType = sm.getDataType();
+        final ColorModel colors = ColorModelFactory.createGrayScale(dataType, sm.getNumBands(),
visibleBand, minimum, maximum);
+        for (;;) {
+            if (colors.equals(source.getColorModel())) {
+                return source;
+            }
+            if (source instanceof RecoloredImage) {
+                source = ((RecoloredImage) source).source;
+            } else {
+                break;
+            }
+        }
+        return new RecoloredImage(source, colors);
+    }
+
+    /**
+     * Returns the color model of this image.
+     */
+    @Override
+    public ColorModel getColorModel() {
+        return colors;
+    }
+
+    /**
+     * Appends a content to show in the {@link #toString()} representation,
+     * after the class name and before the string representation of the wrapped image.
+     */
+    @Override
+    final Class<RecoloredImage> stringStart(final StringBuilder buffer) {
+        buffer.append(colors.getColorSpace());
+        return RecoloredImage.class;
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelFactory.java
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelFactory.java
index 2bc73d4..5d98a56 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelFactory.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelFactory.java
@@ -407,9 +407,9 @@ public final class ColorModelFactory {
             final int visibleBand, final double minimum, final double maximum)
     {
         final ColorModel cm;
-        if (numComponents == 1 && minimum == 0 && maximum == 1) {
+        if (numComponents == 1 && isStandardRange(dataType, minimum, maximum)) {
             final ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_GRAY);
-            cm = new ComponentColorModel(cs, false, false, Transparency.OPAQUE, dataType);
+            cm = new ComponentColorModel(cs, false, true, Transparency.OPAQUE, dataType);
         } else {
             final ScaledColorSpace cs = new ScaledColorSpace(numComponents, visibleBand,
minimum, maximum);
             cm = new ScaledColorModel(cs, dataType);
@@ -418,6 +418,48 @@ public final class ColorModelFactory {
     }
 
     /**
+     * Returns {@code true} if the given range of values is the standard range for the given
data type.
+     * In such case it may be possible to use a Java standard color model, which sometime
benefit from
+     * acceleration in Java2D rendering pipe.
+     *
+     * <p>This method does not clamp the given values to the maximum range supported
by the given type.
+     * For example even if {@code TYPE_BYTE} can not represent values outside the [0 …
255] range,
+     * we do not clamp the minimum and maximum values to that range because it would change
the visual
+     * appearance (because of different color scale).</p>
+     *
+     * @param  dataType  one of {@link DataBuffer} constants.
+     * @param  minimum   the minimal sample value expected.
+     * @param  maximum   the maximal sample value expected.
+     * @return whether the given minimum and maximum are the standard range for the given
type.
+     */
+    static boolean isStandardRange(final int dataType, double minimum, final double maximum)
{
+        final boolean signed;
+        switch (dataType) {
+            case DataBuffer.TYPE_BYTE:
+            case DataBuffer.TYPE_USHORT: signed = false; break;
+            case DataBuffer.TYPE_SHORT:
+            case DataBuffer.TYPE_INT:    signed = true; break;
+            case DataBuffer.TYPE_FLOAT:
+            case DataBuffer.TYPE_DOUBLE: return ((float) minimum) == 0f && ((float)
maximum) == 1f;
+            default: return false;
+        }
+        final int numBits = DataBuffer.getDataTypeSize(dataType);
+        final double upper;     // Exclusive (e.g. 256 or 65536)
+        if (signed) {
+            upper = 1L << (numBits - 1);        // E.g. 128 for TYPE_BYTE.
+            minimum += upper;                   // Convert e.g. -128 to 0.
+        } else {
+            upper = 1L << numBits;              // E.g. 256 for TYPE_BYTE.
+        }
+        /*
+         * Since sample values are integers, take a tolerance of 1. But for the upper bounds,
+         * we take a slightly larger tolerance in case the caller confused "inclusive" versus
+         * "exclusive" values. For example 255.5 ± 1.5 accepts the |254.001 … 256.999]
range.
+         */
+        return Math.abs(minimum) < 1 && Math.abs(maximum - (upper - 0.5)) <
1.5;
+    }
+
+    /**
      * Appends a description of the given color space in the given buffer.
      * This is used for {@code toString()} method implementations.
      *
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java
index 0009b8a..d5feb93 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java
@@ -109,6 +109,33 @@ public final class ImageUtilities extends Static {
     }
 
     /**
+     * If the given image is showing only one band, returns the index of that band.
+     * Otherwise returns 0. Image showing only one band are SIS-specific (usually an
+     * image show all its bands).
+     *
+     * @param  image  the image for which to get the visible band, or {@code null}.
+     * @return index of the visible band, or -1 if there is none or more than one.
+     */
+    public static int getVisibleBand(final RenderedImage image) {
+        if (image != null) {
+            final ColorModel cm = image.getColorModel();
+            if (cm != null) {
+                final ColorSpace cs = cm.getColorSpace();
+                if (cs instanceof ScaledColorSpace) {
+                    return ((ScaledColorSpace) cs).visibleBand;
+                }
+                if (cm instanceof MultiBandsIndexColorModel) {
+                    return ((MultiBandsIndexColorModel) cm).visibleBand;
+                }
+                if (cm.getNumComponents() == 1) {
+                    return 0;
+                }
+            }
+        }
+        return -1;
+    }
+
+    /**
      * Returns the data type of the given image.
      *
      * @param  image  the image for which to get the data type, or {@code null}.
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/RasterFactory.java
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/RasterFactory.java
index 28d26ba..a2ced94 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/RasterFactory.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/RasterFactory.java
@@ -89,20 +89,16 @@ public final class RasterFactory extends Static {
             final int numComponents, final int visibleBand, final double minimum, final double
maximum)
     {
         switch (dataType) {
-            case DataBuffer.TYPE_BYTE: {
-                if (numComponents == 1 && minimum <= 0 && maximum >=
0xFF) {
-                    return new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
-                }
-                break;
-            }
+            case DataBuffer.TYPE_BYTE:
             case DataBuffer.TYPE_USHORT: {
-                if (numComponents == 1 && minimum <= 0 && maximum >=
0xFFFF) {
-                    return new BufferedImage(width, height, BufferedImage.TYPE_USHORT_GRAY);
+                if (numComponents == 1 && ColorModelFactory.isStandardRange(dataType,
minimum, maximum)) {
+                    return new BufferedImage(width, height, (dataType == DataBuffer.TYPE_BYTE)
+                                ? BufferedImage.TYPE_BYTE_GRAY : BufferedImage.TYPE_USHORT_GRAY);
                 }
                 break;
             }
         }
-        final ColorModel cm = ColorModelFactory.createGrayScale(DataBuffer.TYPE_INT, 1, 0,
-10, 10);
+        final ColorModel cm = ColorModelFactory.createGrayScale(dataType, numComponents,
visibleBand, minimum, maximum);
         return new BufferedImage(cm, cm.createCompatibleWritableRaster(width, height), false,
null);
     }
 


Mime
View raw message