blob: ef4923f5d7b39f19824f06d121a1f2247016b3be [file] [log] [blame]
/*
* 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.Map;
import java.util.List;
import java.util.Arrays;
import java.util.Objects;
import java.util.NavigableMap;
import java.util.function.Function;
import java.util.logging.LogRecord;
import java.util.function.DoubleUnaryOperator;
import java.awt.Color;
import java.awt.Shape;
import java.awt.Rectangle;
import java.awt.image.ColorModel;
import java.awt.image.SampleModel;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.awt.image.ImagingOpException;
import java.awt.image.IndexColorModel;
import java.awt.image.WritableRenderedImage;
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;
import org.opengis.referencing.operation.NoninvertibleTransformException;
import org.apache.sis.referencing.operation.transform.MathTransforms;
import org.apache.sis.coverage.SampleDimension;
import org.apache.sis.internal.coverage.j2d.ImageLayout;
import org.apache.sis.internal.coverage.j2d.ImageUtilities;
import org.apache.sis.math.Statistics;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.collection.WeakHashSet;
import org.apache.sis.internal.system.Modules;
import org.apache.sis.internal.coverage.j2d.TiledImage;
import org.apache.sis.internal.processing.image.Isolines;
import org.apache.sis.internal.feature.Resources;
import org.apache.sis.measure.NumberRange;
import org.apache.sis.measure.Units;
// For javadoc
import org.apache.sis.coverage.RegionOfInterest;
import org.apache.sis.coverage.grid.GridCoverage;
import org.apache.sis.coverage.grid.GridGeometry;
import org.apache.sis.coverage.grid.GridCoverageProcessor;
/**
* A predefined set of operations on images as convenience methods.
* After instantiation, {@code ImageProcessor} can be configured for the following aspects:
*
* <ul class="verbose">
* <li>
* {@linkplain #setInterpolation(Interpolation) Interpolation method} to use during resampling operations.
* </li><li>
* {@linkplain #setFillValues(Number...) Fill values} to use for pixels that can not be computed.
* </li><li>
* {@linkplain #setCategoryColors(Function) Category colors} for mapping sample values
* (identified by their range, name or unit of measurement) to colors.
* </li><li>
* {@linkplain #setImageResizingPolicy(Resizing) Image resizing policy} to apply
* if a requested image size prevent the image to be tiled.
* </li><li>
* {@linkplain #setPositionalAccuracyHints(Quantity...) Positional accuracy hints}
* for enabling the use of faster algorithm when a lower accuracy is acceptable.
* </li><li>
* Whether operations can be executed in parallel. By default operations on unknown
* {@link RenderedImage} implementations are executed sequentially in the caller thread, for safety reasons.
* Some operations can be parallelized, but it should be enabled only if the {@link RenderedImage} is known
* to be thread-safe and has concurrent (or fast) implementation of {@link RenderedImage#getTile(int, int)}.
* Apache SIS implementations of {@link RenderedImage} can be parallelized, but it may not be the case of
* images from other libraries.
* </li><li>
* Whether the operations should fail if an exception is thrown while processing a tile.
* By default errors during calculation are propagated as an {@link ImagingOpException},
* in which case no result is available. But errors can also be notified as a {@link LogRecord} instead,
* in which case partial results may be available.
* </li>
* </ul>
*
* For each image operations, above properties are combined with parameters given to the operation method.
* Each method in this {@code ImageProcessor} class documents the properties used in addition of method parameters.
*
* <div class="note"><b>API design:</b>
* properties (setter methods) are used for values that can be applied unchanged on many different images.
* For example the {@linkplain #getInterpolation() interpolation method} can be specified once and used
* unchanged for many {@link #resample resample(…)} operations.
* On the other hand, method arguments are used for values that are usually specific to the image to process.
* For example the {@link MathTransform} argument given to the {@link #resample resample(…)} operation depends
* tightly on the source image and destination bounds (also given in arguments); those information usually need
* to be recomputed for each image.</div>
*
* <h2>Deferred calculations</h2>
* Methods in this class may compute the result at some later time after the method returned, instead of computing
* the result immediately on method call. Consequently unless otherwise specified, {@link RenderedImage} arguments
* should be <em>stable</em>, i.e. pixel values should not be modified after method return.
*
* <h2>Area of interest</h2>
* Some operations accept an optional <cite>area of interest</cite> argument specified as a {@link Shape} instance in
* pixel coordinates. If a shape is given, it should not be modified after {@code ImageProcessor} method call because
* the given object may be retained directly (i.e. the {@code Shape} is not always cloned; it depends on its class).
* In addition, the {@code Shape} implementation shall be thread-safe (assuming its state stay unmodified)
* unless the execution mode is set to {@link Mode#PARALLEL}.
*
* <h2>Error handling</h2>
* If an exception occurs during the computation of a tile, then the {@code ImageProcessor} behavior
* is controlled by the {@link #getErrorHandler() errorHandler} property:
*
* <ul>
* <li>If {@link ErrorHandler#THROW}, the exception is wrapped in an {@link ImagingOpException} and thrown.</li>
* <li>If {@link ErrorHandler#LOG}, the exception is logged and a partial result is returned.</li>
* <li>If any other value, the exception is wrapped in a {@link LogRecord} and sent to that filter.
* The filter can store the log record, for example for showing later in a graphical user interface (GUI).
* If the filter returns {@code true}, the log record is also logged, otherwise it is silently discarded.
* In both cases a partial result is returned.</li>
* </ul>
*
* <h2>Thread-safety</h2>
* {@code ImageProcessor} is safe for concurrent use in multi-threading environment.
* Note however that {@code ImageProcessor} instances are mutable;
* consider {@linkplain #clone() cloning} if setter methods are invoked on a shared instance.
*
* @author Martin Desruisseaux (Geomatys)
* @version 1.2
*
* @see org.apache.sis.coverage.grid.GridCoverageProcessor
*
* @since 1.1
* @module
*/
public class ImageProcessor implements Cloneable {
/**
* Cache of previously created images. We use this cache only for images of known implementations,
* especially the ones which may be costly to compute. Reusing an existing instance avoid to repeat
* the computation.
*/
private static final WeakHashSet<RenderedImage> CACHE = new WeakHashSet<>(RenderedImage.class);
/**
* Returns an unique instance of the given image. This method should be invoked only for images
* of known implementation, especially the ones which are costly to compute. The implementation
* shall override {@link Object#equals(Object)} and {@link Object#hashCode()} methods.
*/
static RenderedImage unique(final RenderedImage image) {
return CACHE.unique(image);
}
/**
* Properties (size, tile size, sample model, <i>etc.</i>) of destination images.
*
* @see #getImageLayout()
* @see #setImageLayout(ImageLayout)
*/
private ImageLayout layout;
/**
* Whether {@code ImageProcessor} can produce an image of different size compared to requested size.
* An image may be resized if the requested size can not be subdivided into tiles of reasonable size.
* For example if the image width is a prime number, there is no way to divide the image horizontally with
* an integer number of tiles. The only way to get an integer number of tiles is to change the image size.
*
* <p>The image resizing policy may be used by any operation that involve a {@linkplain #resample resampling}.
* If a resizing is applied, the new size will be written in the {@code bounds} argument (a {@link Rectangle}).</p>
*
* @see #getImageResizingPolicy()
* @see #setImageResizingPolicy(Resizing)
*/
public enum Resizing {
/**
* Image size is unmodified; the requested value is used unconditionally.
* It may result in big tiles (potentially a single tile for the whole image)
* if the image size is not divisible by a tile size.
*/
NONE,
/**
* Image size can be increased. {@code ImageProcessor} will try to increase
* by the smallest amount of pixels allowing the image to be subdivided in tiles.
*/
EXPAND
}
/**
* Interpolation to use during resample operations.
*
* @see #getInterpolation()
* @see #setInterpolation(Interpolation)
*/
private Interpolation interpolation;
/**
* The values to use for pixels that can not be computed.
* This array may be {@code null} or may contain {@code null} elements.
* This is a "copy on write" array (elements are not modified).
*
* @see #getFillValues()
* @see #setFillValues(Number...)
*/
private Number[] fillValues;
/**
* Colors to use for arbitrary categories of sample values. This function can return {@code null}
* or empty arrays for some categories, which are interpreted as fully transparent pixels.
*
* @see #getCategoryColors()
* @see #setCategoryColors(Function)
*/
private Function<Category,Color[]> colors;
/**
* Hints about the desired positional accuracy (in "real world" units or in pixel units),
* or {@code null} if unspecified. In order to avoid the need to clone this array in the
* {@link #clone()} method, the content of this array should not be modified.
* For setting new values, a new array should be created.
*
* @see #getPositionalAccuracyHints()
* @see #setPositionalAccuracyHints(Quantity...)
*/
private Quantity<?>[] positionalAccuracyHints;
/**
* Whether the operations can be executed in parallel.
*
* @see #getExecutionMode()
* @see #setExecutionMode(Mode)
*/
private Mode executionMode;
/**
* Execution modes specifying whether operations can be executed in parallel.
* If {@link #SEQUENTIAL}, operations are executed sequentially in the caller thread.
* If {@link #PARALLEL}, some operations may be parallelized using an arbitrary number of threads.
*
* @see #getExecutionMode()
* @see #setExecutionMode(Mode)
*/
public enum Mode {
/**
* Operations executed in multi-threaded mode if possible.
* This mode can be used if the {@link RenderedImage} instances are thread-safe and provide
* a concurrent (or very fast) implementation of {@link RenderedImage#getTile(int, int)}.
*/
PARALLEL,
/**
* Operations executed in the caller thread, without parallelization.
* Sequential operations may be useful for processing {@link RenderedImage}
* implementations that may not be thread-safe.
*/
SEQUENTIAL,
/**
* Operations are executed in multi-threaded mode if the {@link RenderedImage} instance
* is an implementation known to be thread-safe. All operations on image implementations
* unknown to Apache SIS are executed in sequential mode.
*/
DEFAULT
}
/**
* Whether errors occurring during computation should be propagated or wrapped in a {@link LogRecord}.
* If errors are wrapped in a {@link LogRecord}, this field specifies what to do with the record.
* Only one log record is created for all tiles that failed for the same operation on the same image.
*
* @see #getErrorHandler()
* @see #setErrorHandler(ErrorHandler)
*/
private ErrorHandler errorHandler;
/**
* Creates a new processor with default configuration.
* The execution mode is initialized to {@link Mode#DEFAULT}
* and the error handler to {@link ErrorHandler#THROW}.
*/
public ImageProcessor() {
layout = ImageLayout.DEFAULT;
executionMode = Mode.DEFAULT;
errorHandler = ErrorHandler.THROW;
interpolation = Interpolation.BILINEAR;
}
/**
* Returns the properties (size, tile size, sample model, <i>etc.</i>) of destination images.
* This method is not yet public because {@link ImageLayout} is not a public class.
*/
final synchronized ImageLayout getImageLayout() {
return layout;
}
/**
* Sets the properties (size, tile size, sample model, <i>etc.</i>) of destination images.
* This method is not yet public because {@link ImageLayout} is not a public class.
*/
final synchronized void setImageLayout(final ImageLayout layout) {
ArgumentChecks.ensureNonNull("layout", layout);
this.layout = layout;
}
/**
* Returns the interpolation method to use during resample operations.
*
* @return interpolation method to use during resample operations.
*
* @see #resample(RenderedImage, Rectangle, MathTransform)
*/
public synchronized Interpolation getInterpolation() {
return interpolation;
}
/**
* Sets the interpolation method to use during resample operations.
*
* @param method interpolation method to use during resample operations.
*
* @see #resample(RenderedImage, Rectangle, MathTransform)
*/
public synchronized void setInterpolation(final Interpolation method) {
ArgumentChecks.ensureNonNull("method", method);
interpolation = method;
}
/**
* Returns the values to use for pixels that can not be computed.
* This method returns a copy of the array set by the last call to {@link #setFillValues(Number...)}.
*
* @return fill values to use for pixels that can not be computed, or {@code null} for the defaults.
*/
public synchronized Number[] getFillValues() {
return (fillValues != null) ? fillValues.clone() : null;
}
/**
* Sets the values to use for pixels that can not be computed. The given array may be {@code null} or may contain
* {@code null} elements for default values. Those defaults are zero for images storing sample values as integers,
* or {@link Float#NaN} or {@link Double#NaN} for images storing sample values as floating point numbers. If the
* given array contains less elements than the number of bands in an image, missing elements will be assumed null.
* If the given array contains more elements than the number of bands, extraneous elements will be ignored.
*
* @param values fill values to use for pixels that can not be computed, or {@code null} for the defaults.
*/
public synchronized void setFillValues(final Number... values) {
fillValues = (values != null) ? values.clone() : null;
}
/**
* Returns the colors to use for given categories of sample values, or {@code null} is unspecified.
* This method returns the function set by the last call to {@link #setCategoryColors(Function)}.
*
* @return colors to use for arbitrary categories of sample values, or {@code null} for default.
*/
public synchronized Function<Category,Color[]> getCategoryColors() {
return colors;
}
/**
* Sets the colors to use for given categories in image, or {@code null} is unspecified.
* This function provides a way to colorize images without knowing in advance the numerical values of pixels.
* For example instead of specifying <cite>"pixel value 0 in blue, 1 in green, 2 in yellow"</cite>,
* this function allows to specify <cite>"Lakes in blue, Forests in green, Sand in yellow"</cite>.
* It is still possible however to use numerical values if the function desires to do so,
* since this information is available with {@link Category#getSampleRange()}.
*
* <p>This function is used by methods expecting {@link SampleDimension} arguments such as
* {@link #visualize(RenderedImage, List)}. The given function can return {@code null} or
* empty arrays for some categories, which are interpreted as fully transparent pixels.</p>
*
* @param colors colors to use for arbitrary categories of sample values, or {@code null} for default.
*/
public synchronized void setCategoryColors(final Function<Category,Color[]> colors) {
this.colors = colors;
}
/**
* Returns whether {@code ImageProcessor} can produce an image of different size compared to requested size.
* If this processor can use a different size, the enumeration value specifies what kind of changes may be applied.
*
* @return the image resizing policy.
*/
public synchronized Resizing getImageResizingPolicy() {
return layout.isBoundsAdjustmentAllowed ? Resizing.EXPAND : Resizing.NONE;
}
/**
* Sets whether {@code ImageProcessor} can produce an image of different size compared to requested size.
*
* @param policy the new image resizing policy.
*/
public synchronized void setImageResizingPolicy(final Resizing policy) {
ArgumentChecks.ensureNonNull("policy", policy);
layout = (policy == Resizing.EXPAND) ? ImageLayout.SIZE_ADJUST : ImageLayout.DEFAULT;
}
/**
* Returns hints about the desired positional accuracy, in "real world" units or in pixel units.
* This is an empty array by default, which means that {@code ImageProcessor} aims for the best
* accuracy it can produce. If the returned array is non-empty and contains accuracies large enough,
* {@code ImageProcessor} may use some slightly faster algorithms at the expense of accuracy.
*
* @return desired accuracy in no particular order, or an empty array if none.
*/
public synchronized Quantity<?>[] getPositionalAccuracyHints() {
return (positionalAccuracyHints != null) ? positionalAccuracyHints.clone() : new Quantity<?>[0];
}
/**
* Sets hints about desired positional accuracy, in "real world" units or in pixel units.
* More than one hint can be specified for allowing the use of different units.
* For example the given array can contain an accuracy in metres and an accuracy in seconds,
* for specifying desired accuracies in both spatial dimensions and in the temporal dimension.
* Accuracy can also be specified in both real world units such as {@linkplain Units#METRE metres}
* and in {@linkplain Units#PIXEL pixel units}, which are converted to real world units depending
* on image resolution. If more than one value is applicable to a dimension
* (after unit conversion if needed), the smallest value is taken.
*
* <p>Those values are only hints, the {@code ImageProcessor} is free to ignore them.
* In any cases there is no guarantee that computed images will met those accuracies.
* The given values are honored on a <em>best effort</em> basis only.</p>
*
* <p>In current implementation, {@code ImageProcessor} recognizes only accuracies in {@link Units#PIXEL}.
* A value such as 0.125 pixel may cause {@code ImageProcessor} to use some a slightly faster algorithm
* at the expense of accuracy during {@linkplain #resample resampling operations}.</p>
*
* @param hints desired accuracy in no particular order, or a {@code null} array if none.
* Null elements in the array are ignored.
*/
public synchronized void setPositionalAccuracyHints(final Quantity<?>... hints) {
if (hints != null) {
final Quantity<?>[] copy = new Quantity<?>[hints.length];
int n = 0;
for (final Quantity<?> hint : hints) {
if (hint != null) copy[n++] = hint;
}
if (n != 0) {
positionalAccuracyHints = ArraysExt.resize(copy, n);
return;
}
}
positionalAccuracyHints = null;
}
/**
* Returns whether operations can be executed in parallel.
* If {@link Mode#SEQUENTIAL}, operations are executed sequentially in the caller thread.
* If {@link Mode#PARALLEL}, some operations may be parallelized using an arbitrary number of threads.
*
* @return whether the operations can be executed in parallel.
*/
public synchronized Mode getExecutionMode() {
return executionMode;
}
/**
* Sets whether operations can be executed in parallel.
* This value can be set to {@link Mode#PARALLEL} if the {@link RenderedImage} instances are thread-safe
* and provide a concurrent (or very fast) implementation of {@link RenderedImage#getTile(int, int)}.
* If {@link Mode#SEQUENTIAL}, only the caller thread is used. Sequential operations may be useful
* for processing {@link RenderedImage} implementations that may not be thread-safe.
*
* <p>It is safe to set this flag to {@link Mode#PARALLEL} with {@link java.awt.image.BufferedImage}
* or with Apache SIS implementations of {@link RenderedImage}.</p>
*
* @param mode whether the operations can be executed in parallel.
*/
public synchronized void setExecutionMode(final Mode mode) {
ArgumentChecks.ensureNonNull("mode", mode);
executionMode = mode;
}
/**
* Whether the operations can be executed in parallel for the specified image.
* This method shall be invoked in a method synchronized on {@code this}.
*/
private boolean parallel(final RenderedImage source) {
assert Thread.holdsLock(this);
switch (executionMode) {
case PARALLEL: return true;
case SEQUENTIAL: return false;
default: {
if (source instanceof BufferedImage) {
return true;
}
return source.getClass().getName().startsWith(Modules.CLASSNAME_PREFIX);
}
}
}
/**
* Returns whether exceptions occurring during computation are propagated or logged.
* If {@link ErrorHandler#THROW} (the default), exceptions are wrapped in {@link ImagingOpException} and thrown.
* If {@link ErrorHandler#LOG}, exceptions are wrapped in a {@link LogRecord}, filtered then eventually logged.
*
* @return whether exceptions occurring during computation are propagated or logged.
*/
public synchronized ErrorHandler getErrorHandler() {
return errorHandler;
}
/**
* Sets whether exceptions occurring during computation are propagated or logged.
* The default behavior is to wrap exceptions in {@link ImagingOpException} and throw them.
* If this property is set to {@link ErrorHandler#LOG}, then exceptions will be wrapped in
* {@link LogRecord} instead, in which case a partial result may be available.
* Only one log record is created for all tiles that failed for the same operation on the same image.
*
* <h4>Limitations</h4>
* In current {@code ImageProcessor} implementation, the error handler is not honored by all operations.
* Some operations may continue to throw an exception on failure (the behavior of default error handler)
* even if a different handler has been specified.
* Each operation specifies in its Javadoc whether the operation uses error handler or not.
*
* @param handler handler to notify when an operation failed on one or more tiles,
* or {@link ErrorHandler#THROW} for propagating the exception.
*/
public synchronized void setErrorHandler(final ErrorHandler handler) {
ArgumentChecks.ensureNonNull("handler", handler);
errorHandler = handler;
}
/**
* Whether errors occurring during computation should be propagated instead of wrapped in a {@link LogRecord}.
* This method shall be invoked in a method synchronized on {@code this}.
*/
private boolean failOnException() {
assert Thread.holdsLock(this);
return errorHandler == ErrorHandler.THROW;
}
/**
* Builds an operator which can be used for filtering "no data" sample values.
* Calls to the operator {@code applyAsDouble(x)} will return {@link Double#NaN}
* if the <var>x</var> value is equal to one of the given no-data {@code values},
* and will return <var>x</var> unchanged otherwise.
*
* <h4>Usage</h4>
* This operator can be used as a {@code sampleFilters} argument in calls to
* {@link #statistics statistics(…)} or {@link #valueOfStatistics valueOfStatistics(…)} methods.
* It is redundant with {@linkplain SampleDimension#getTransferFunction() transfer function} work,
* but can be useful for images not managed by a {@link org.apache.sis.coverage.grid.GridCoverage}.
*
* @param values the "no data" values, or {@code null} if none. Null and NaN elements are ignored.
* @return an operator for filtering the given "no data" values,
* or {@code null} if there is no non-NaN value to filter.
*
* @see #statistics(RenderedImage, Shape, DoubleUnaryOperator[])
* @see SampleDimension#getTransferFunction()
*
* @since 1.2
*/
public DoubleUnaryOperator filterNodataValues(final Number... values) {
return (values != null) ? StatisticsCalculator.filterNodataValues(values) : null;
}
/**
* 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.
*
* <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.
* They are whatever statistics the property provider considered as representative.</li>
* <li>Otherwise statistics are computed for the whole image.</li>
* </ul>
*
* <h4>Sample converters</h4>
* An arbitrary {@link DoubleUnaryOperator} can be applied on sample values before to add them to statistics.
* The main purpose is to replace "no-data values" by {@link Double#NaN} values for instructing
* {@link Statistics#accept(double)} to ignore them. The {@link #filterNodataValues(Number...)}
* convenience method can be used for building an operator filtering "no data" sample values.
*
* <h4>Properties used</h4>
* This operation uses the following properties in addition to method parameters:
* <ul>
* <li>{@linkplain #getExecutionMode() Execution mode} (parallel or sequential).</li>
* <li>{@linkplain #getErrorHandler() Error handler} (custom action executed if an exception is thrown).</li>
* </ul>
*
* <h4>Result relationship with source</h4>
* This method 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.
* @param areaOfInterest pixel coordinates of the area of interest, or {@code null} for the default.
* @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.
* @throws ImagingOpException if an error occurred during calculation
* and the error handler is {@link ErrorHandler#THROW}.
*
* @see #statistics(RenderedImage, Shape, DoubleUnaryOperator...)
* @see #filterNodataValues(Number...)
* @see PlanarImage#STATISTICS_KEY
*/
public Statistics[] valueOfStatistics(final RenderedImage source, final Shape areaOfInterest,
final DoubleUnaryOperator... sampleFilters)
{
ArgumentChecks.ensureNonNull("source", source);
if (areaOfInterest == null && (sampleFilters == null || ArraysExt.allEquals(sampleFilters, null))) {
final Object property = source.getProperty(PlanarImage.STATISTICS_KEY);
if (property instanceof Statistics[]) {
return (Statistics[]) property;
}
}
final boolean parallel, failOnException;
final ErrorHandler errorListener;
synchronized (this) {
parallel = parallel(source);
failOnException = failOnException();
errorListener = errorHandler;
}
/*
* No need to check if the given source is already an instance of StatisticsCalculator.
* 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 Object property = calculator.getProperty(PlanarImage.STATISTICS_KEY);
calculator.logAndClearError(ImageProcessor.class, "valueOfStatistics", errorListener);
return (Statistics[]) property;
}
/**
* Returns an image with statistics (minimum, maximum, mean, standard deviation) on each bands.
* The property value will be computed when first requested (it is not computed immediately by this method).
*
* <p>If {@code areaOfInterest} is null and {@code sampleFilters} is {@code null} or empty,
* then the default is as below:</p>
* <ul>
* <li>If the {@value PlanarImage#STATISTICS_KEY} property value exists in the given image,
* then that image is returned as-is. Note that the existing property value is not necessarily
* statistics for the whole image.
* They are whatever statistics the property provider considers as representative.</li>
* <li>Otherwise an image augmented with a {@value PlanarImage#STATISTICS_KEY} property value
* is returned.</li>
* </ul>
*
* <h4>Sample converters</h4>
* An arbitrary {@link DoubleUnaryOperator} can be applied on sample values before to add them to statistics.
* The main purpose is to replace "no-data values" by {@link Double#NaN} values for instructing
* {@link Statistics#accept(double)} to ignore them. The {@link #filterNodataValues(Number...)}
* convenience method can be used for building an operator filtering "no data" sample values.
*
* <div class="note"><b>API design note:</b>
* the {@code areaOfInterest} and {@code sampleFilters} arguments are complementary.
* Both of them filter the data accepted for statistics. In ISO 19123 terminology,
* the {@code areaOfInterest} argument filters the <cite>coverage domain</cite> while
* the {@code sampleFilters} argument filters the <cite>coverage range</cite>.
* Another connection with OGC/ISO standards is that {@link DoubleUnaryOperator} in this context
* does the same work than {@linkplain SampleDimension#getTransferFunction() transfer function}.
* It can be useful for images not managed by a {@link org.apache.sis.coverage.grid.GridCoverage}.
* </div>
*
* <h4>Properties used</h4>
* This operation uses the following properties in addition to method parameters:
* <ul>
* <li>{@linkplain #getExecutionMode() Execution mode} (parallel or sequential).</li>
* <li>{@linkplain #getErrorHandler() Error handler} (whether to fail if an exception is thrown).</li>
* </ul>
*
* @param source the image for which to provide statistics.
* @param areaOfInterest pixel coordinates of the area of interest, or {@code null} for the default.
* @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 an image with an {@value PlanarImage#STATISTICS_KEY} property.
* May be {@code image} if the given argument already has a statistics property.
*
* @see #valueOfStatistics(RenderedImage, Shape, DoubleUnaryOperator...)
* @see #filterNodataValues(Number...)
* @see PlanarImage#STATISTICS_KEY
*/
public RenderedImage statistics(final RenderedImage source, final Shape areaOfInterest,
final DoubleUnaryOperator... sampleFilters)
{
ArgumentChecks.ensureNonNull("source", source);
if (areaOfInterest == null && (sampleFilters == null || ArraysExt.allEquals(sampleFilters, null))
&& ArraysExt.contains(source.getPropertyNames(), PlanarImage.STATISTICS_KEY))
{
return source;
}
final boolean parallel, failOnException;
synchronized (this) {
parallel = parallel(source);
failOnException = failOnException();
}
return new StatisticsCalculator(source, areaOfInterest, sampleFilters, parallel, failOnException).unique();
}
/**
* Returns an image with the same sample values than the given image, but with its color ramp stretched between
* specified or inferred 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>The minimum and maximum value can be either specified explicitly,
* or determined from {@link #valueOfStatistics statistics} on the image.
* In the latter case 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>
*
* <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>
*
* <p>The range of values for the color ramp can be narrowed with following modifiers
* (a {@link Map} is used for allowing addition of more modifiers in future Apache SIS versions).
* All unrecognized modifiers are silently ignored. If no modifier is specified, then the color ramp
* will be stretched from minimum to maximum values found in specified image.</p>
*
* <table class="sis">
* <caption>Value range modifiers</caption>
* <tr>
* <th>Key</th>
* <th>Purpose</th>
* <th>Values</th>
* </tr><tr>
* <td>{@code "minimum"}</td>
* <td>Minimum value (omitted if computed from statistics).</td>
* <td>{@link Number}</td>
* </tr><tr>
* <td>{@code "maximum"}</td>
* <td>Maximum value (omitted if computed from statistics).</td>
* <td>{@link Number}</td>
* </tr><tr>
* <td>{@code "multStdDev"}</td>
* <td>Multiple of the standard deviation.</td>
* <td>{@link Number} (typical values: 1.5, 2 or 3)</td>
* </tr><tr>
* <td>{@code "statistics"}</td>
* <td>Statistics or image from which to get statistics.</td>
* <td>{@link Statistics} or {@link RenderedImage}</td>
* </tr><tr>
* <td>{@code "areaOfInterest"}</td>
* <td>Pixel coordinates of the region for which to compute statistics.</td>
* <td>{@link Shape}</td>
* </tr><tr>
* <td>{@code "nodataValues"}</td>
* <td>Values to ignore in statistics.</td>
* <td>{@link Number} or {@code Number[]}</td>
* </tr><tr>
* <td>{@code "sampleDimensions"}</td>
* <td>Meaning of pixel values.</td>
* <td>{@link SampleDimension}</td>
* </tr>
* </table>
*
* <h4>Properties used</h4>
* This operation uses the following properties in addition to method parameters:
* <ul>
* <li>(none)</li>
* </ul>
*
* <h4>Limitation</h4>
* Current implementation can stretch only gray scale images (a future version may extend support to images
* using {@linkplain java.awt.image.IndexColorModel index color models}). If this method can not stretch the
* color ramp, for example because the given image is an RGB image, then the image is returned unchanged.
*
* @param source the image to recolor.
* @param modifiers modifiers for narrowing the range of values, or {@code null} if none.
* @return the image with color ramp stretched between the specified or calculated bounds,
* or {@code image} unchanged if the operation can not be applied on the given image.
* @throws IllegalArgumentException if the value associated to one of about keys is not of expected type.
*/
public RenderedImage stretchColorRamp(final RenderedImage source, final Map<String,?> modifiers) {
ArgumentChecks.ensureNonNull("source", source);
return RecoloredImage.stretchColorRamp(this, source, modifiers);
}
/**
* Selects a subset of bands in the given image. This method can also be used for changing band order
* or repeating the same band from the source image. If the specified {@code bands} are the same than
* the source image bands in the same order, then {@code source} is returned directly.
*
* <p>This method returns an image sharing the same data buffer than the source image;
* pixel values are not copied. Consequently changes in the source image are reflected
* immediately in the returned image.</p>
*
* <h4>Properties used</h4>
* This operation uses the following properties in addition to method parameters:
* <ul>
* <li>(none)</li>
* </ul>
*
* @param source the image in which to select bands.
* @param bands indices of bands to retain.
* @return image width selected bands.
* @throws IllegalArgumentException if a band index is invalid.
*/
public RenderedImage selectBands(final RenderedImage source, final int... bands) {
ArgumentChecks.ensureNonNull("source", source);
return BandSelectImage.create(source, bands);
}
/**
* Applies a mask defined by a geometric shape. If {@code maskInside} is {@code true},
* then all pixels inside the given shape are set to the {@linkplain #getFillValues() fill values}.
* If {@code maskInside} is {@code false}, then the mask is reversed:
* the pixels set to fill values are the ones outside the shape.
*
* @param source the image on which to apply a mask.
* @param mask geometric area (in pixel coordinates) of the mask.
* @param maskInside {@code true} for masking pixels inside the shape, or {@code false} for masking outside.
* @return an image with mask applied.
*
* @see GridCoverageProcessor#mask(GridCoverage, RegionOfInterest, boolean)
*
* @since 1.2
*/
public RenderedImage mask(final RenderedImage source, final Shape mask, final boolean maskInside) {
ArgumentChecks.ensureNonNull("source", source);
ArgumentChecks.ensureNonNull("mask", mask);
final Number[] fillValues;
synchronized (this) {
fillValues = this.fillValues;
}
return unique(new MaskedImage(source, mask, maskInside, fillValues));
}
/**
* Returns an image with sample values converted by the given functions. The results can be stored as
* {@code byte}, {@code short}, {@code int}, {@code float} or {@code double} values, not necessarily
* the same type than the source values. If the result values are stored as integers, then they are
* {@linkplain Math#round(double) rounded to nearest integers} and clamped in the valid range of the
* target integer type.
*
* <p>If the source image is a {@link WritableRenderedImage} and the given converters are invertible, then
* the returned image will also be a {@link WritableRenderedImage} instance. In such case values written in
* the returned image will be reflected in the source image, with {@linkplain Math#round(double) rounding}
* and clamping if the source values are stored as integers.</p>
*
* <p>The number of bands in the returned image is the length of the {@code converters} array,
* which must be greater than 0 and not greater than the number of bands in the source image.
* If the {@code converters} array length is less than the number of source bands, all source
* bands at index ≥ {@code converters.length} will be ignored.</p>
*
* <p>The {@code sourceRanges} array is only a hint for this method. The array may be {@code null}
* or contain {@code null} elements, and may be of any length. Missing elements are considered null
* and extraneous elements are ignored. Those ranges do not need to encompass all possible values;
* it is sufficient to provide only typical or "most interesting" ranges.</p>
*
* <h4>Properties used</h4>
* This operation uses the following properties in addition to method parameters:
* <ul>
* <li>(none)</li>
* </ul>
*
* <h4>Result relationship with source</h4>
* Changes in the source image are reflected in the returned image
* if the source image notifies {@linkplain java.awt.image.TileObserver tile observers}.
*
* @param source the image for which to convert sample values.
* @param sourceRanges approximate ranges of values for each band in source image, or {@code null} if unknown.
* @param converters the transfer functions to apply on each band of the source image.
* @param targetType the type of data in the image resulting from conversions.
* @param colorModel color model of resulting image, or {@code null}.
* @return the image which computes converted values from the given source.
*
* @see GridCoverageProcessor#convert(GridCoverage, MathTransform1D[], Function)
*/
public RenderedImage convert(final RenderedImage source, final NumberRange<?>[] sourceRanges,
MathTransform1D[] converters, final DataType targetType, final ColorModel colorModel)
{
ArgumentChecks.ensureNonNull("source", source);
ArgumentChecks.ensureNonNull("converters", converters);
ArgumentChecks.ensureNonNull("targetType", targetType);
ArgumentChecks.ensureSizeBetween("converters", 1, ImageUtilities.getNumBands(source), converters.length);
converters = converters.clone();
for (int i=0; i<converters.length; i++) {
ArgumentChecks.ensureNonNullElement("converters", i, converters[i]);
}
final ImageLayout layout;
synchronized (this) {
layout = this.layout;
}
// No need to clone `sourceRanges` because it is not stored by `BandedSampleConverter`.
return unique(BandedSampleConverter.create(source, layout,
sourceRanges, converters, targetType.toDataBufferType(), colorModel));
}
/**
* Verifies that the given rectangle, if non-null, is non-empty.
* This method assumes that the argument name is "bounds".
*/
private static void ensureNonEmpty(final Rectangle bounds) {
if (bounds != null && bounds.isEmpty()) {
throw new IllegalArgumentException(Errors.format(Errors.Keys.EmptyArgument_1, "bounds"));
}
}
/**
* Creates a new image which will resample the given image. The resampling operation is defined
* by a potentially non-linear transform from the <em>new</em> image to the specified <em>source</em> image.
* That transform should map {@linkplain org.opengis.referencing.datum.PixelInCell#CELL_CENTER pixel centers}.
* If that transform produces coordinates that are outside source envelope bounds, then the corresponding pixels
* in the new image are set to {@linkplain #getFillValues() fill values}. Otherwise sample values are interpolated
* using the method given by {@link #getInterpolation()}.
*
* <p>If the given source is an instance of {@link ResampledImage},
* then this method will use {@linkplain PlanarImage#getSources() the source} of the given source.
* The intent is to avoid resampling a resampled image; instead this method works on the original data.</p>
*
* <h4>Properties used</h4>
* This operation uses the following properties in addition to method parameters:
* <ul>
* <li>{@linkplain #getInterpolation() Interpolation method} (nearest neighbor, bilinear, <i>etc</i>).</li>
* <li>{@linkplain #getFillValues() Fill values} for pixels outside source image.</li>
* <li>{@linkplain #getImageResizingPolicy() Image resizing policy} to apply
* 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>
* </ul>
*
* <h4>Result relationship with source</h4>
* Changes in the source image are reflected in the returned images
* if the source image notifies {@linkplain java.awt.image.TileObserver tile observers}.
*
* @param source the image to be resampled.
* @param bounds domain of pixel coordinates of resampled image to create.
* Updated by this method if {@link Resizing#EXPAND} policy is applied.
* @param toSource conversion of pixel coordinates from resampled image to {@code source} image.
* @return resampled image (may be {@code source}).
*
* @see GridCoverageProcessor#resample(GridCoverage, GridGeometry)
*/
public RenderedImage resample(RenderedImage source, final Rectangle bounds, MathTransform toSource) {
ArgumentChecks.ensureNonNull("source", source);
ArgumentChecks.ensureNonNull("bounds", bounds);
ArgumentChecks.ensureNonNull("toSource", toSource);
ensureNonEmpty(bounds);
final RenderedImage colored = source;
final SampleModel sm = source.getSampleModel();
boolean isIdentity = toSource.isIdentity();
RenderedImage resampled = null;
for (;;) {
if (isIdentity && bounds.x == source.getMinX() && bounds.y == source.getMinY() &&
bounds.width == source.getWidth() && bounds.height == source.getHeight())
{
resampled = source;
break;
}
if (Objects.equals(sm, source.getSampleModel())) {
if (source instanceof ImageAdapter) {
source = ((ImageAdapter) source).source;
continue;
}
if (source instanceof ResampledImage) {
final List<RenderedImage> sources = source.getSources();
if (sources != null && sources.size() == 1) { // Paranoiac check.
toSource = MathTransforms.concatenate(toSource, ((ResampledImage) source).toSource);
isIdentity = toSource.isIdentity();
source = sources.get(0);
continue;
}
}
}
/*
* All accesses to ImageProcessor fields done by this method should be isolated in this single
* synchronized block. All arrays are "copy on write", so they do not need to be cloned.
*/
final ImageLayout layout;
final Interpolation interpolation;
final Number[] fillValues;
final Quantity<?>[] positionalAccuracyHints;
synchronized (this) {
layout = this.layout;
interpolation = this.interpolation;
fillValues = this.fillValues;
positionalAccuracyHints = this.positionalAccuracyHints;
}
resampled = unique(new ResampledImage(source,
layout.createCompatibleSampleModel(source, bounds), layout.getMinTile(),
bounds, toSource, interpolation, fillValues, positionalAccuracyHints));
break;
}
/*
* Preserve the color model of the original image, including information about how it
* has been constructed. If the source image was not an instance of `RecoloredImage`,
* then above call should return `resampled` unchanged.
*/
return RecoloredImage.applySameColors(resampled, colored);
}
/**
* Computes immediately all tiles in the given region of interest, then return an image will those tiles ready.
* Computations will use many threads if {@linkplain #getExecutionMode() execution mode} is parallel.
*
* <h4>Properties used</h4>
* This operation uses the following properties in addition to method parameters:
* <ul>
* <li>{@linkplain #getExecutionMode() Execution mode} (parallel or sequential).</li>
* <li>{@linkplain #getErrorHandler() Error handler} (whether to fail if an exception is thrown).</li>
* </ul>
*
* @param source the image to compute immediately (may be {@code null}).
* @param areaOfInterest pixel coordinates of the region to prefetch, or {@code null} for the whole image.
* @return image with all tiles intersecting the AOI computed, or {@code null} if the given image was null.
* @throws ImagingOpException if an exception occurred during {@link RenderedImage#getTile(int, int)} call.
* This exception wraps the original exception as its {@linkplain ImagingOpException#getCause() cause}.
*/
public RenderedImage prefetch(RenderedImage source, final Rectangle areaOfInterest) {
if (source == null || source instanceof BufferedImage || source instanceof TiledImage) {
return source;
}
while (source instanceof PrefetchedImage) {
source = ((PrefetchedImage) source).source;
}
final boolean parallel;
final ErrorHandler errorListener;
synchronized (this) {
parallel = parallel(source);
errorListener = errorHandler;
}
final PrefetchedImage image = new PrefetchedImage(source, areaOfInterest, errorListener, parallel);
return image.isEmpty() ? source : image;
}
/**
* Returns 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
* 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>The given map specifies the color 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.</p>
*
* <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>Properties used</h4>
* This operation uses the following properties in addition to method parameters:
* <ul>
* <li>(none)</li>
* </ul>
*
* @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.
*/
public RenderedImage visualize(final RenderedImage source, final Map<NumberRange<?>,Color[]> colors) {
ArgumentChecks.ensureNonNull("source", source);
ArgumentChecks.ensureNonNull("colors", colors);
return visualize(new Visualization.Builder(source, colors.entrySet()));
}
/**
* Returns 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
* 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.
* 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>
*
* <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>Properties used</h4>
* This operation uses the following properties in addition to method parameters:
* <ul>
* <li>{@linkplain #getCategoryColors() Category colors}.</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.
*/
public RenderedImage visualize(final RenderedImage source, final List<SampleDimension> ranges) {
ArgumentChecks.ensureNonNull("source", source);
return visualize(new Visualization.Builder(null, source, null, ranges));
}
/**
* Returns an image as the resampling of the given image followed by a conversion to integer sample values.
* This is a combination of the following methods, as a single image operation for avoiding creation of an
* intermediate image step:
*
* <ol>
* <li><code>{@linkplain #resample(RenderedImage, Rectangle, MathTransform) resample}(source, bounds, toSource)</code></li>
* <li><code>{@linkplain #visualize(RenderedImage, List) visualize}(resampled, ranges)</code></li>
* </ol>
*
* Combining above steps may be advantageous when the {@code resample(…)} result is not needed for anything
* else than visualization. If the same resampling may be needed for computational purposes, then it may be
* more advantageous to keep above method calls separated instead of using this {@code visualize(…)} method.
*
* <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>Properties used</h4>
* This operation uses the following properties in addition to method parameters:
* <ul>
* <li>{@linkplain #getInterpolation() Interpolation method} (nearest neighbor, bilinear, <i>etc</i>).</li>
* <li>{@linkplain #getFillValues() Fill values} for pixels outside source image.</li>
* <li>{@linkplain #getImageResizingPolicy() Image resizing policy} to apply
* 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>
* </ul>
*
* @param source the image to be resampled and recolored.
* @param bounds domain of pixel coordinates of resampled image to create.
* Updated by this method if {@link Resizing#EXPAND} policy is applied.
* @param toSource conversion of pixel coordinates from resampled image to {@code source} image.
* @param 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 resampled and recolored image for visualization purposes only.
*/
public RenderedImage visualize(final RenderedImage source, final Rectangle bounds, final MathTransform toSource,
final List<SampleDimension> ranges)
{
ArgumentChecks.ensureNonNull("source", source);
ArgumentChecks.ensureNonNull("bounds", bounds);
ArgumentChecks.ensureNonNull("toSource", toSource);
ensureNonEmpty(bounds);
return visualize(new Visualization.Builder(bounds, source, toSource, ranges));
}
/**
* Finishes builder configuration and creates the {@link Visualization} image.
*/
private RenderedImage visualize(final Visualization.Builder builder) {
synchronized (this) {
builder.layout = layout;
builder.interpolation = interpolation;
builder.categoryColors = colors;
builder.fillValues = fillValues;
builder.positionalAccuracyHints = positionalAccuracyHints;
}
try {
return builder.create(this);
} catch (IllegalStateException | NoninvertibleTransformException e) {
throw new IllegalArgumentException(Resources.format(Resources.Keys.UnconvertibleSampleValues), e);
}
}
/**
* Generates isolines at the specified levels computed from data provided by the given image.
* Isolines will be computed for every bands in the given image.
* For each band, the result is given as a {@code Map} where keys are the specified {@code levels}
* and values are the isolines at the associated level.
* If there are no isolines for a given level, there will be no corresponding entry in the map.
*
* <h4>Properties used</h4>
* This operation uses the following properties in addition to method parameters:
* <ul>
* <li>{@linkplain #getExecutionMode() Execution mode} (parallel or sequential).</li>
* </ul>
*
* @param data image providing source values.
* @param levels values for which to compute isolines. An array should be provided for each band.
* If there is more bands than {@code levels.length}, the last array is reused for
* all remaining bands.
* @param gridToCRS transform from pixel coordinates to geometry coordinates, or {@code null} if none.
* Integer source coordinates are located at pixel centers.
* @return the isolines for specified levels in each band. The {@code List} size is the number of bands.
* For each band, the {@code Map} size is equal or less than {@code levels[band].length}.
* Map keys are the specified levels, excluding those for which there are no isolines.
* Map values are the isolines as a Java2D {@link Shape}.
* @throws ImagingOpException if an error occurred during calculation.
*/
public List<NavigableMap<Double,Shape>> isolines(final RenderedImage data, final double[][] levels, final MathTransform gridToCRS) {
final boolean parallel;
synchronized (this) {
parallel = parallel(data);
}
if (parallel) {
return Isolines.toList(Isolines.parallelGenerate(data, levels, gridToCRS));
} else try {
return Isolines.toList(Isolines.generate(data, levels, gridToCRS));
} catch (TransformException e) {
throw (ImagingOpException) new ImagingOpException(null).initCause(e);
}
}
/**
* Returns {@code true} if the given object is an image processor
* of the same class with the same configuration.
*
* @param object the other object to compare with this processor.
* @return whether the other object is an image processor of the same class with the same configuration.
*/
@Override
public boolean equals(final Object object) {
if (object != null && object.getClass() == getClass()) {
final ImageProcessor other = (ImageProcessor) object;
final Mode executionMode;
final ErrorHandler errorHandler;
final Interpolation interpolation;
final Number[] fillValues;
final Function<Category,Color[]> colors;
final Quantity<?>[] positionalAccuracyHints;
synchronized (this) {
executionMode = this.executionMode;
errorHandler = this.errorHandler;
interpolation = this.interpolation;
fillValues = this.fillValues;
colors = this.colors;
positionalAccuracyHints = this.positionalAccuracyHints;
}
synchronized (other) {
return errorHandler.equals(other.errorHandler) &&
executionMode.equals(other.executionMode) &&
interpolation.equals(other.interpolation) &&
Objects.equals(colors, other.colors) &&
Arrays.equals(fillValues, other.fillValues) &&
Arrays.equals(positionalAccuracyHints, other.positionalAccuracyHints);
}
}
return false;
}
/**
* Returns a hash code value for this image processor based on its current configuration.
*
* @return a hash code value for this processor.
*/
@Override
public synchronized int hashCode() {
return Objects.hash(getClass(), errorHandler, executionMode, interpolation)
+ 37 * Arrays.hashCode(fillValues) + 31 * Objects.hashCode(colors)
+ 39 * Arrays.hashCode(positionalAccuracyHints);
}
/**
* Returns an image processor with the same configuration than this processor.
*
* @return a clone of this image processor.
*/
@Override
public synchronized ImageProcessor clone() {
try {
return (ImageProcessor) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
}