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 7090cb3ada Complete the migration to `Colorizer` in the `Visualization` class. Deprecate the `Map<NumberRange,Color[]>` argument in `ImageProcessor`. This is replaced by `Colorizer.forRanges(Map)`. 7090cb3ada is described below commit 7090cb3ada08646cf9cc9e277c54b68235900c01 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Tue Mar 28 14:09:54 2023 +0200 Complete the migration to `Colorizer` in the `Visualization` class. Deprecate the `Map<NumberRange,Color[]>` argument in `ImageProcessor`. This is replaced by `Colorizer.forRanges(Map)`. --- .../apache/sis/internal/gui/ImageConverter.java | 13 +- .../main/java/org/apache/sis/image/Colorizer.java | 62 +++++--- .../java/org/apache/sis/image/ImageProcessor.java | 87 ++++++++-- .../java/org/apache/sis/image/Interpolation.java | 6 +- .../java/org/apache/sis/image/Visualization.java | 177 +++++++++++++-------- 5 files changed, 237 insertions(+), 108 deletions(-) diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImageConverter.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImageConverter.java index 19a3e5e6cc..eebf6c0227 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImageConverter.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImageConverter.java @@ -32,6 +32,7 @@ import javafx.scene.image.ImageView; import javafx.scene.image.PixelFormat; import javafx.scene.image.PixelWriter; import javafx.scene.image.WritableImage; +import org.apache.sis.image.Colorizer; import org.apache.sis.image.ImageProcessor; import org.apache.sis.image.PlanarImage; import org.apache.sis.internal.map.coverage.RenderingWorkaround; @@ -67,9 +68,9 @@ final class ImageConverter extends Task<Statistics[]> { * Colors to apply on the mask image when that image is overlay on top of another image. * Current value is a transparent yellow color. */ - private static final Map<NumberRange<?>,Color[]> MASK_TRANSPARENCY = Map.of( + private static final Colorizer MASK_TRANSPARENCY = Colorizer.forRanges(Map.of( NumberRange.create(0, true, 0, true), new Color[] {ColorModelFactory.TRANSPARENT}, - NumberRange.create(1, true, 1, true), new Color[] {new Color(0x30FFFF00, true)}); + NumberRange.create(1, true, 1, true), new Color[] {new Color(0x30FFFF00, true)})); /** * The Java2D image to convert. @@ -207,9 +208,13 @@ final class ImageConverter extends Task<Statistics[]> { private RenderedImage getMask(final ImageProcessor processor) { final Object mask = source.getProperty(PlanarImage.MASK_KEY); if (mask instanceof RenderedImage) try { - return processor.visualize((RenderedImage) mask, MASK_TRANSPARENCY); + processor.setColorizer(MASK_TRANSPARENCY); + return processor.visualize((RenderedImage) mask, (java.util.List) null); } catch (IllegalArgumentException e) { - // Ignore, we will not apply any mask. Declare PropertyView.setImage(…) as the public method. + /* + * Ignore, we will not apply any mask over the thumbnail image. + * `PropertyView.setImage(…)` is declared as the public method. + */ Logging.recoverableException(LOGGER, PropertyView.class, "setImage", e); } return null; diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/Colorizer.java b/core/sis-feature/src/main/java/org/apache/sis/image/Colorizer.java index 7f454b6cf6..330e7170e4 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/image/Colorizer.java +++ b/core/sis-feature/src/main/java/org/apache/sis/image/Colorizer.java @@ -26,6 +26,7 @@ import java.awt.Color; import java.awt.image.ColorModel; import java.awt.image.SampleModel; import java.awt.image.IndexColorModel; +import java.awt.image.RenderedImage; import org.apache.sis.coverage.Category; import org.apache.sis.coverage.SampleDimension; import org.apache.sis.internal.coverage.j2d.ColorModelBuilder; @@ -81,7 +82,7 @@ public interface Colorizer extends Function<Colorizer.Target, Optional<ColorMode private final int visibleBand; /** - * Creates a new record with the sample model of the image to colorize. + * Creates a new target with the sample model of the image to colorize. * * @param model sample model of the computed image to colorize (mandatory). * @param ranges description of the bands of the computed image to colorize, or {@code null} if none. @@ -135,6 +136,17 @@ public interface Colorizer extends Function<Colorizer.Target, Optional<ColorMode public OptionalInt getVisibleBand() { return (visibleBand >= 0) ? OptionalInt.of(visibleBand) : OptionalInt.empty(); } + + /** + * Returns {@code true} if {@code orElse(…)} should not try alternative colorizers. + * This is used only for {@link Visualization} operation, which is a special case + * because it merges 3 operations in a single one. + * + * @return whether {@link #orElse(Colorizer)} should not try alternative. + */ + boolean isConsumed() { + return false; + } } /** @@ -175,18 +187,24 @@ public interface Colorizer extends Function<Colorizer.Target, Optional<ColorMode * * @param colors the colors to use for the specified range of sample values. * @return a colorizer which will interpolate the given colors in the given range of values. + * + * @see ImageProcessor#visualize(RenderedImage, List) */ public static Colorizer forRanges(final Map<NumberRange<?>,Color[]> colors) { ArgumentChecks.ensureNonEmpty("colors", colors.entrySet()); final var factory = ColorModelFactory.piecewise(colors); return (target) -> { - final OptionalInt visibleBand = target.getVisibleBand(); - if (visibleBand.isEmpty()) { - return Optional.empty(); + if (target instanceof Visualization.Target) { + ((Visualization.Target) target).rangeColors = colors; + } else { + final OptionalInt visibleBand = target.getVisibleBand(); + if (!visibleBand.isEmpty()) { + final SampleModel model = target.getSampleModel(); + final int numBands = model.getNumBands(); + return Optional.ofNullable(factory.createColorModel(model.getDataType(), numBands, visibleBand.getAsInt())); + } } - final SampleModel model = target.getSampleModel(); - final int numBands = model.getNumBands(); - return Optional.ofNullable(factory.createColorModel(model.getDataType(), numBands, visibleBand.getAsInt())); + return Optional.empty(); }; } @@ -202,18 +220,24 @@ public interface Colorizer extends Function<Colorizer.Target, Optional<ColorMode * * @param colors colors to use for arbitrary categories of sample values. * @return a colorizer which will apply colors determined by the {@link Category} of sample values. + * + * @see ImageProcessor#visualize(RenderedImage, List) */ public static Colorizer forCategories(final Function<Category,Color[]> colors) { ArgumentChecks.ensureNonNull("colors", colors); return (target) -> { - final int visibleBand = target.getVisibleBand().orElse(-1); - if (visibleBand >= 0) { - final List<SampleDimension> ranges = target.getRanges().orElse(null); - if (visibleBand < ranges.size()) { - final SampleModel model = target.getSampleModel(); - final var c = new ColorModelBuilder(colors); - c.initialize(model, ranges.get(visibleBand)); - return Optional.ofNullable(c.createColorModel(model.getDataType(), model.getNumBands(), visibleBand)); + if (target instanceof Visualization.Target) { + ((Visualization.Target) target).categoryColors = colors; + } else { + final int visibleBand = target.getVisibleBand().orElse(-1); + if (visibleBand >= 0) { + final List<SampleDimension> ranges = target.getRanges().orElse(null); + if (visibleBand < ranges.size()) { + final SampleModel model = target.getSampleModel(); + final var c = new ColorModelBuilder(colors); + c.initialize(model, ranges.get(visibleBand)); + return Optional.ofNullable(c.createColorModel(model.getDataType(), model.getNumBands(), visibleBand)); + } } } return Optional.empty(); @@ -255,10 +279,10 @@ public interface Colorizer extends Function<Colorizer.Target, Optional<ColorMode */ default Colorizer orElse(final Colorizer alternative) { ArgumentChecks.ensureNonNull("alternative", alternative); - return (model) -> { - var result = apply(model); - if (result.isEmpty()) { - result = alternative.apply(model); + return (target) -> { + var result = apply(target); + if (result.isEmpty() && !target.isConsumed()) { + result = alternative.apply(target); } return result; }; diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java b/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java index 78a52cb585..f1f30fd2d4 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java +++ b/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java @@ -164,6 +164,7 @@ public class ImageProcessor implements Cloneable { /** * Properties (size, tile size, sample model, <i>etc.</i>) of destination images. + * Shall never be null. Default value is {@link ImageLayout#DEFAULT}. * * @see #getImageLayout() * @see #setImageLayout(ImageLayout) @@ -1213,12 +1214,24 @@ public class ImageProcessor implements Cloneable { * * @param source the image to recolor for visualization purposes. * @param colors colors to use for each range of values in the source image. - * @return recolored image for visualization purposes only. + * @deprecated Replaced by {@link #visualize(RenderedImage, List)} with {@code null} list argument + * and colors map inferred from the {@link Colorizer}. */ - public RenderedImage visualize(final RenderedImage source, final Map<NumberRange<?>,Color[]> colors) { + @Deprecated(since="1.4", forRemoval=true) + public synchronized RenderedImage visualize(final RenderedImage source, final Map<NumberRange<?>,Color[]> colors) { + /* + * TODO: after removal of this method, search for usages of + * `visualize(RenderedImage, List)` and remove unecessary `(List) null` cast. + */ ArgumentChecks.ensureNonNull("source", source); ArgumentChecks.ensureNonNull("colors", colors); - return visualize(new Visualization.Builder(source, colors.entrySet())); + final Colorizer old = colorizer; + try { + colorizer = Colorizer.forRanges(colors); + return visualize(new Visualization.Builder(null, source, null, null)); + } finally { + colorizer = old; + } } /** @@ -1227,31 +1240,75 @@ public class ImageProcessor implements Cloneable { * are used as-is (they are not copied or converted). Otherwise this operation will convert sample * values to unsigned bytes in order to enable the use of {@link IndexColorModel}. * - * <p>This method is similar to {@link #visualize(RenderedImage, Map)} - * except that the {@link Map} argument is splitted in two parts: the ranges (map keys) are - * {@linkplain Category#getSampleRange() encapsulated in <code>Category</code>} objects, themselves - * {@linkplain SampleDimension#getCategories() encapsulated in <code>SampleDimension</code>} objects. - * The colors (map values) are determined by a function receiving {@link Category} inputs. + * <p>The resulting image is suitable for visualization purposes, but should not be used for computation purposes. + * There is no guarantee about the number of bands in returned image or about which formula is used for converting + * floating point values to integer values.</p> + * + * <h4>Specifying colors for ranges of pixel values</h4> + * When no {@link SampleDimension} information is available, the recommended way to specify colors is like below. + * In this example, <var>min</var> and <var>max</var> are minimum and maximum values + * (inclusive in this example, but they could be exclusive as well) in the <em>source</em> image. + * Those extrema can be floating point values. This example specifies only one range of values, + * but arbitrary numbers of non-overlapping ranges are allowed. + * + * {@snippet lang="java" : + * NumberRange<?> range = NumberRange.create(min, true, max, true); + * Color[] colors = {Color.BLUE, Color.MAGENTA, Color.RED}; + * processor.setColorizer(Colorizer.forRanges(Map.of(range, colors))); + * RenderedImage visualization = processor.visualize(source, null); + * } + * + * The map given to the colorizer specifies the colors to use for different ranges of values in the source image. + * The ranges of values in the returned image may not be the same; this method is free to rescale them. + * The {@link Color} arrays may have any length; colors will be interpolated as needed for fitting + * the ranges of values in the destination image. + * + * <h4>Specifying colors for sample dimension categories</h4> + * If {@link SampleDimension} information is available, a more flexible way to specify colors + * is to associate colors to category names instead of predetermined ranges of pixel values. + * The ranges will be inferred indirectly, {@linkplain Category#getSampleRange() from the categories} + * themselves {@linkplain SampleDimension#getCategories() encapsulated in sample dimensions}. + * The colors are determined by a function receiving {@link Category} inputs. + * + * {@snippet lang="java" : + * Map<String,Color[]> colors = Map.of( + * "Temperature", new Color[] {Color.BLUE, Color.MAGENTA, Color.RED}, + * "Wind speed", new Color[] {Color.GREEN, Color.CYAN, Color.BLUE}); + * + * processor.setColorizer(Colorizer.forCategories((category) -> + * colors.get(category.getName().toString(Locale.ENGLISH)))); + * + * RenderedImage visualization = processor.visualize(source, ranges); + * } + * * This separation makes easier to apply colors based on criterion other than numerical values. * For example, colors could be determined from {@linkplain Category#getName() category name} such as "Temperature", * or {@linkplain org.apache.sis.measure.MeasurementRange#unit() units of measurement}. * The {@link Color} arrays may have any length; colors will be interpolated as needed for fitting - * the ranges of values in the destination image.</p> + * the ranges of values in the destination image. * - * <p>The resulting image is suitable for visualization purposes, but should not be used for computation purposes. - * There is no guarantee about the number of bands in returned image or about which formula is used for converting - * floating point values to integer values.</p> + * <p>The two approaches can be combined. For example the following colorizer will choose colors based + * on sample dimensions if available, or fallback on predefined ranges of pixel values otherwise:</p> + * + * {@snippet lang="java" : + * Function<Category,Color[]> flexible = ...; + * Map<NumberRange<?>,Color[]> predefined = ...; + * processor.setColorizer(Colorizer.forCategories(flexible).orElse(Colorizer.forRanges(predefined))); + * } * * <h4>Properties used</h4> * This operation uses the following properties in addition to method parameters: * <ul> - * <li>{@linkplain #getCategoryColors() Category colors}.</li> + * <li>{@linkplain #getColorizer() Colorizer}.</li> * </ul> * * @param source the image to recolor for visualization purposes. * @param ranges description of {@code source} bands, or {@code null} if none. This is typically * obtained by {@link org.apache.sis.coverage.grid.GridCoverage#getSampleDimensions()}. * @return recolored image for visualization purposes only. + * + * @see Colorizer#forRanges(Map) + * @see Colorizer#forCategories(Function) */ public RenderedImage visualize(final RenderedImage source, final List<SampleDimension> ranges) { ArgumentChecks.ensureNonNull("source", source); @@ -1285,7 +1342,7 @@ public class ImageProcessor implements Cloneable { * if {@code bounds} size is not divisible by a tile size.</li> * <li>{@linkplain #getPositionalAccuracyHints() Positional accuracy hints} * for enabling faster resampling at the cost of lower precision.</li> - * <li>{@linkplain #getCategoryColors() Category colors}.</li> + * <li>{@linkplain #getColorizer() Colorizer}.</li> * </ul> * * @param source the image to be resampled and recolored. @@ -1313,7 +1370,7 @@ public class ImageProcessor implements Cloneable { synchronized (this) { builder.layout = layout; builder.interpolation = interpolation; - builder.categoryColors = colors; + builder.colorizer = colorizer; builder.fillValues = fillValues; builder.positionalAccuracyHints = positionalAccuracyHints; } diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/Interpolation.java b/core/sis-feature/src/main/java/org/apache/sis/image/Interpolation.java index 87bcd4442d..196c1c2e65 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/image/Interpolation.java +++ b/core/sis-feature/src/main/java/org/apache/sis/image/Interpolation.java @@ -110,13 +110,13 @@ public abstract class Interpolation { * If the given image uses an index color model, interpolating the indexed values does not produce the * expected colors. Safest approach is to disable completely interpolations in that case. * - * <div class="note"><b>Note:</b> - * we could interpolate if we knew that all index values, without exception (i.e. no index for missing values), + * <h4>Design note</h4> + * We could interpolate if we knew that all index values, without exception (i.e. no index for missing values), * are related to measurements by a linear function. In practice it rarely happens, because there is usually * at least one index value reserved for missing values. Scientific data in SIS are usually stored as floating * point type (with missing values mapped to NaN), which cannot be associated to {@link IndexColorModel}. * For now we do not try to perform a more sophisticated detection of which interpolations are allowed, - * but a future SIS version may revisit this policy if needed.</div> + * but a future SIS version may revisit this policy if needed. * * @return {@link #NEAREST} if interpolations should be restricted to nearest-neighbor, or {@code this} otherwise. */ diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java b/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java index 914b847f0d..4b8f38804f 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java +++ b/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java @@ -20,7 +20,6 @@ import java.util.Map; import java.util.List; import java.util.Arrays; import java.util.Objects; -import java.util.Collection; import java.util.function.Function; import java.util.function.DoubleUnaryOperator; import java.awt.Color; @@ -34,7 +33,6 @@ import java.awt.image.WritableRaster; import java.awt.image.RenderedImage; import java.nio.DoubleBuffer; import javax.measure.Quantity; -import org.apache.sis.coverage.Category; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.MathTransform1D; import org.opengis.referencing.operation.TransformException; @@ -43,10 +41,12 @@ import org.apache.sis.referencing.operation.transform.MathTransforms; import org.apache.sis.internal.coverage.SampleDimensions; import org.apache.sis.internal.coverage.CompoundTransform; import org.apache.sis.internal.coverage.j2d.ColorModelBuilder; +import org.apache.sis.internal.coverage.j2d.ColorModelFactory; import org.apache.sis.internal.coverage.j2d.ImageLayout; import org.apache.sis.internal.coverage.j2d.ImageUtilities; import org.apache.sis.internal.feature.Resources; import org.apache.sis.coverage.SampleDimension; +import org.apache.sis.coverage.Category; import org.apache.sis.measure.NumberRange; import org.apache.sis.math.Statistics; import org.apache.sis.util.collection.BackingStoreException; @@ -60,10 +60,56 @@ import org.apache.sis.util.collection.BackingStoreException; * {@link WritableRaster#setPixel(int, int, int[])} has more efficient implementations for integers. * * @author Martin Desruisseaux (Geomatys) - * @version 1.3 + * @version 1.4 * @since 1.1 */ final class Visualization extends ResampledImage { + /** + * A colorization target handled in a special way by {@link Colorizer} factory methods. + * The fields in this class are set when the {@link Colorizer#apply(Target)} work needs + * to be done by the caller instead. This special case is needed because the ranges and + * categories specified by the user are relative to the source image while colorization + * operation needs ranges and categories relative to the target image. + */ + static final class Target extends Colorizer.Target { + /** + * Colors to apply on the sample value ranges, as supplied by user. + */ + Map<NumberRange<?>,Color[]> rangeColors; + + /** + * Colors to apply on the sample dimensions, as supplied by user. + */ + Function<Category,Color[]> categoryColors; + + /** + * Whether the {@link Builder} has information about {@link SampleDimension} categories. + */ + private final boolean hasCategories; + + /** + * Creates a new target with the sample model of the image to colorize. + * + * @param model sample model of the computed image to colorize (mandatory). + * @param visibleBand the band to colorize if the colorization algorithm uses only one band, or -1 if none. + * @param hasCategories whether the builder has information about {@link SampleDimension} categories. + */ + Target(final SampleModel model, final int visibleBand, final boolean hasCategories) { + super(model, null, visibleBand); + this.hasCategories = hasCategories; + } + + /** + * Returns {@code true} if {@code orElse(…)} should not try alternative colorizers. + * + * @return whether {@link #orElse(Colorizer)} should not try alternative. + */ + @Override + boolean isConsumed() { + return (rangeColors != null) || (hasCategories && categoryColors != null); + } + } + /** * Builds an image where all sample values are indices of colors in an {@link IndexColorModel}. * If the given image stores sample values as unsigned bytes or short integers, then those values @@ -72,8 +118,8 @@ final class Visualization extends ResampledImage { * * <p>This builder accepts two kinds of input:</p> * <ul> - * <li>Non-null {@code sourceBands} and {@link ImageProcessor#getCategoryColors()}.</li> - * <li>Non-null {@code rangesAndColors}.</li> + * <li>Non-null {@link #sourceBands} and {@link Target#categoryColors}.</li> + * <li>Non-null {@link Target#rangeColors}.</li> * </ul> * * The resulting image is suitable for visualization purposes but should not be used for computation purposes. @@ -91,7 +137,7 @@ final class Visualization extends ResampledImage { private static final int NUM_BANDS = 1; /** Band to make visible. */ - private static final int VISIBLE_BAND = 0; + private static final int VISIBLE_BAND = ColorModelFactory.DEFAULT_VISIBLE_BAND; //// ┌─────────────────────────────────────┐ //// │ Arguments given by user │ @@ -109,9 +155,6 @@ final class Visualization extends ResampledImage { /** Description of {@link #source} bands, or {@code null} if none. */ private List<SampleDimension> sourceBands; - /** Colors to apply for range of sample values in source image, or {@code null} if none. */ - private Collection<Map.Entry<NumberRange<?>,Color[]>> rangesAndColors; - //// ┌─────────────────────────────────────┐ //// │ Given by ImageProcesor.configure(…) │ //// └─────────────────────────────────────┘ @@ -122,8 +165,8 @@ final class Visualization extends ResampledImage { /** Object to use for performing interpolations. */ Interpolation interpolation; - /** The colors to use for given categories of sample values, or {@code null} is unspecified. */ - Function<Category,Color[]> categoryColors; + /** Provider of colors to apply for range of sample values in source image, or {@code null} if none. */ + Colorizer colorizer; /** Values to use for pixels in this image that cannot be mapped to pixels in source image. */ Number[] fillValues; @@ -161,20 +204,6 @@ final class Visualization extends ResampledImage { this.sourceBands = sourceBands; } - /** - * Creates a builder for a visualization image with colors specified for range of values. - * Current version assumes that target image bounds are the same than source image bounds - * and that there is no change of pixel coordinates, but this is not a real restriction. - * The {@code bounds} and {@code toSource} arguments could be added back in the future if useful. - * - * @param source the image for which to replace the color model. - * @param rangesAndColors range of sample values in source image associated to colors to apply. - */ - Builder(final RenderedImage source, final Collection<Map.Entry<NumberRange<?>,Color[]>> rangesAndColors) { - this.source = source; - this.rangesAndColors = rangesAndColors; - } - /** * Returns an image where all sample values are indices of colors in an {@link IndexColorModel}. * If the source image stores sample values as unsigned bytes or short integers, then those values @@ -195,59 +224,91 @@ final class Visualization extends ResampledImage { * cannot be converted to sample values in the recolored image. */ RenderedImage create(final ImageProcessor processor) throws NoninvertibleTransformException { - final int visibleBand = ImageUtilities.getVisibleBand(source); + final RenderedImage coloredSource = source; + final int visibleBand = ImageUtilities.getVisibleBand(coloredSource); if (visibleBand < 0) { // This restriction may be relaxed in a future version if we implement conversion to RGB images. throw new IllegalArgumentException(Resources.format(Resources.Keys.OperationRequiresSingleBand)); } + /* + * Skip any previous `RecoloredImage` since we will replace the `ColorModel` by a new one. + * Keep only the band to make visible in order to reduce the amount of calculation during + * resampling and for saving memory. + */ + while (source instanceof RecoloredImage) { + source = ((RecoloredImage) source).source; + } + source = BandSelectImage.create(source, new int[] {visibleBand}); + /* + * 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 + * requires the tile layout of destination image to be the same as source image. + * Otherwise combine interpolation and value conversions in a single operation. + */ + if (toSource == null) { + toSource = MathTransforms.identity(BIDIMENSIONAL); + } + final boolean shortcut = toSource.isIdentity() && (bounds == null || ImageUtilities.getBounds(source).contains(bounds)); + if (shortcut) { + layout = ImageLayout.fixedSize(source); + } + /* + * Sample values will be unconditionally converted to integers in the [0 … 255] range. + * The sample model is a mandatory argument before we invoke user-supplied colorizer, + * which must be done before to build the color model. + */ + sampleModel = layout.createBandedSampleModel(ColorModelBuilder.TYPE_COMPACT, NUM_BANDS, source, bounds); + final Target target = new Target(sampleModel, visibleBand, sourceBands != null); + if (colorizer != null) { + colorModel = colorizer.apply(target).orElse(null); + } /* * Get a `ColorModelBuilder` which will compute the `ColorModel` of destination image. - * There is different ways to create colorizer, depending on which arguments were supplied by user. + * There is different ways to setup the builder, depending on which `Colorizer` is used. * In precedence order: * - * - rangesAndColor : Collection<Map.Entry<NumberRange<?>,Color[]>> - * - sourceBands : List<SampleDimension> + * - rangeColors : Map<NumberRange<?>,Color[]> + * - sourceBands : List<SampleDimension> * - statistics */ boolean initialized; - final ColorModelBuilder colorizer; - if (rangesAndColors != null) { - colorizer = new ColorModelBuilder(rangesAndColors); + final ColorModelBuilder builder; + if (target.rangeColors != null) { + builder = new ColorModelBuilder(target.rangeColors.entrySet()); initialized = true; } else { /* * Ranges of sample values were not specified explicitly. Instead, we will try to infer them - * in various ways: sample dimensions, scaled color model, statistics in last resort. + * in various ways: sample dimensions, scaled color model, or image statistics in last resort. */ - colorizer = new ColorModelBuilder(categoryColors); - initialized = (sourceBands != null) && colorizer.initialize(source.getSampleModel(), sourceBands.get(visibleBand)); + builder = new ColorModelBuilder(target.categoryColors); + initialized = (sourceBands != null) && builder.initialize(coloredSource.getSampleModel(), sourceBands.get(visibleBand)); if (initialized) { /* * If we have been able to configure ColorModelBuilder using SampleDimension, apply an adjustment * based on the ScaledColorModel if it exists. Use case: image is created with an IndexColorModel - * determined by the SampleModel, then user enhanced contrast by a call to `stretchColorRamp(…)` - * above. We want to preserve that contrast enhancement. + * determined by the SampleModel, then user enhanced contrast by a call to `stretchColorRamp(…)`. + * We want to preserve that contrast enhancement. */ - colorizer.rescaleMainRange(source.getColorModel()); + builder.rescaleMainRange(coloredSource.getColorModel()); } else { /* * If we have not been able to use the SampleDimension, try to use the ColorModel or SampleModel. * There is no call to `rescaleMainRange(…)` because the following code already uses the range * specified by the ColorModel, if available. */ - initialized = colorizer.initialize(source.getColorModel()); + initialized = builder.initialize(coloredSource.getColorModel()); if (!initialized) { - if (source instanceof RecoloredImage) { - final RecoloredImage colored = (RecoloredImage) source; - colorizer.initialize(colored.minimum, colored.maximum); + if (coloredSource instanceof RecoloredImage) { + final RecoloredImage colored = (RecoloredImage) coloredSource; + builder.initialize(colored.minimum, colored.maximum); initialized = true; } else { - initialized = colorizer.initialize(source.getSampleModel(), visibleBand); + initialized = builder.initialize(coloredSource.getSampleModel(), visibleBand); } } } } - source = BandSelectImage.create(source, new int[] {visibleBand}); // Make single-banded. if (!initialized) { /* * If none of above `ColorModelBuilder` configurations worked, use statistics in last resort. @@ -255,38 +316,20 @@ final class Visualization extends ResampledImage { */ final DoubleUnaryOperator[] sampleFilters = SampleDimensions.toSampleFilters(processor, sourceBands); final Statistics statistics = processor.valueOfStatistics(source, null, sampleFilters)[VISIBLE_BAND]; - colorizer.initialize(statistics.minimum(), statistics.maximum()); + builder.initialize(statistics.minimum(), statistics.maximum()); } - /* - * If we reach this point, sample values need to be converted to integers in [0 … 255] range. - * Skip any previous `RecoloredImage` since we are replacing the `ColorModel` by a new one. - */ - while (source instanceof RecoloredImage) { - source = ((RecoloredImage) source).source; + if (colorModel == null) { + colorModel = builder.compactColorModel(NUM_BANDS, VISIBLE_BAND); } - colorModel = colorizer.compactColorModel(NUM_BANDS, VISIBLE_BAND); converters = new MathTransform1D[] { - colorizer.getSampleToIndexValues() // Must be after `compactColorModel(…)`. + builder.getSampleToIndexValues() // Must be after `compactColorModel(…)`. }; - /* - * 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 - * requires the tile layout of destination image to be the same as source image. - */ - if (toSource == null) { - toSource = MathTransforms.identity(BIDIMENSIONAL); - } - if (toSource.isIdentity() && (bounds == null || ImageUtilities.getBounds(source).contains(bounds))) { - layout = ImageLayout.fixedSize(source); + if (shortcut) { interpolation = Interpolation.NEAREST; } else { interpolation = combine(interpolation.toCompatible(source), converters); converters = null; } - /* - * Final image creation after the tile layout has been chosen. - */ - sampleModel = layout.createBandedSampleModel(ColorModelBuilder.TYPE_COMPACT, NUM_BANDS, source, bounds); if (bounds == null) { bounds = ImageUtilities.getBounds(source); }