/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2006-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2009-2012, Geomatys
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotoolkit.image;
import org.geotoolkit.image.color.ColorQuantization;
import java.util.Arrays;
import java.awt.image.*;
import java.awt.Transparency;
import java.awt.RenderingHints;
import java.awt.color.ColorSpace;
import javax.media.jai.*;
import javax.media.jai.operator.*;
import org.opengis.coverage.PaletteInterpretation;
import org.geotoolkit.factory.Hints;
import org.geotoolkit.image.jai.Mask;
import org.geotoolkit.image.jai.SilhouetteMask;
import org.geotoolkit.resources.Errors;
import org.geotoolkit.image.color.ColorModels;
import org.geotoolkit.image.internal.LookupTables;
import org.geotoolkit.image.color.ColorUtilities;
import org.geotoolkit.image.internal.ImageUtilities;
import static java.awt.color.ColorSpace.CS_GRAY;
import static java.awt.color.ColorSpace.CS_sRGB;
import static java.awt.image.DataBuffer.TYPE_BYTE;
import static org.apache.sis.util.ArgumentChecks.*;
/**
* Helper methods for applying JAI operations on an image. The image is specified at
* {@linkplain #ImageWorker(RenderedImage) creation time}. Successive operations can
* be applied by invoking the methods defined in this class, and the final image can
* be obtained by invoking {@link #getRenderedImage} at the end of the process.
* <p>
* This class does not really brings new functionalities, since most of its work is performed by
* chains of JAI image operations. However it makes the job easier by performing automatically
* some intermediate steps based on assumptions about what the common usage is. For example some
* methods like {@link #intensity()} may convert the image to the {@linkplain ColorSpace#CS_sRGB
* RGB color space} before to do their work. {@linkplain ColorQuantization Color Quantization}
* uses the {@link ColorCube#BYTE_496 BYTE_496} color cube, which is specific to the RGB color
* space. Methods dealing with transparency assume that the alpha channel, if presents, is the
* last band.
* <p>
* Developers who know exactly the characteristics of their image should probably use JAI
* operations directly. Developers writing prototypes, or developers who don't know much
* more about their images than what this {@code ImageWorker} assumes, can use this class
* as a convenience.
* <p>
* If an exception is thrown during a method invocation, then this {@code ImageWorker}
* is left in an undetermined state and should not be used anymore.
*
* @author Martin Desruisseaux (Geomatys)
* @author Simone Giannecchini (Geosolutions)
* @author Bryce Nordgren
* @version 3.01
*
* @since 2.3
* @module
*
* @deprecated This class is a legacy from old days and has never been seriously used in the
* Geotoolkit.org library. Its work is quite arbitrary, so we are probably better
* to let users do their work in their own way.
*/
@Deprecated
public class ImageWorker extends ImageInspector {
/**
* The {@linkplain ColorQuantization Color Quantization} method to be applied if a an image
* needs to have its color model converted to an {@link IndexColorModel}. The default value
* is {@link ColorQuantization#ERROR_DIFFUSION ERROR_DIFFUSION}.
*
* @see #setColorModelType
* @see #setRenderingHint
*/
public static final Hints.Key COLOR_QUANTIZATION = new Hints.Key(ColorQuantization.class);
/**
* If {@link Boolean#FALSE FALSE}, image operators are not allowed to produce tiled images.
* The default is value {@link Boolean#TRUE TRUE}.
*
* @see #setRenderingHint
*/
public static final Hints.Key TILING_ALLOWED = new Hints.Key(Boolean.class);
/**
* Creates a new worker for the specified image. The images to be computed (if any)
* will save their tiles in the default {@linkplain TileCache tile cache}.
*
* @param image The source image.
*/
public ImageWorker(final RenderedImage image) {
super(image);
}
/**
* Creates a new image worker initialized to the same image and hints than the given descriptor.
*/
ImageWorker(final ImageInspector base) {
super(base);
}
/**
* If the {@linkplain #image image} was not already tiled, tiles it. Note that no tiling will
* be done if {@link #getRenderingHints() getRenderingHints()} failed to suggest a tile size.
*
* see #isTiled
*/
public void tile() {
if (!isTiled()) {
final RenderingHints hints = getRenderingHints();
final ImageLayout layout = getImageLayout(hints);
if (layout.isValid(ImageLayout.TILE_WIDTH_MASK) ||
layout.isValid(ImageLayout.TILE_HEIGHT_MASK))
{
final int type = image.getSampleModel().getDataType();
image = FormatDescriptor.create(image, type, hints);
}
}
}
/**
* Formats the {@linkplain #image image} to the provided data type. If the image already
* stores its sample values in a data buffer of the given type, then this method does nothing.
* Otherwise the behavior depends on the value of the {@code rescale} argument:
*
* <ul>
* <li><p>If {@code false}, then this method casts the sample values to the given
* type - values are not changed except as a result of the cast.</p></li>
* <li><p>If {@code true}, then this method rescales the sample values in order to make
* them fit in the range supported by the given data type. It does so by computing
* the {@linkplain #getMinimums minimum} and {@linkplain #getMaximums maximum} values
* for each band, {@linkplain RescaleDescriptor rescale} them to the range of the
* given type and format the resulting image to of that type.</p></li>
* </ul>
*
* This method is often used for rescaling to bytes in the range [0 … 255],
* which can be done with {@link DataBuffer#TYPE_BYTE} as the parameter value.
*
* @param type The target type as one of the {@code TYPE_*} constant defined in
* {@link DataBuffer}.
* @param rescale {@code true} for rescaling the sample values to the range supported by
* the given type, or {@code false} for just casting them.
*
* @see #isBytes
* @see RescaleDescriptor
*/
public void format(final int type, final boolean rescale) {
final int currentType = image.getSampleModel().getDataType();
if (type == currentType) {
// Already using the requested range - nothing to do.
return;
}
if (!rescale || ImageUtilities.isFloatType(type)) {
/*
* If the target data type is floating point, do not apply any rescale.
* Invalidates the statistics only if we casted to a smallest type.
*/
image = FormatDescriptor.create(image, type, getRenderingHints());
if (ImageUtilities.typeForBoth(type, currentType) != currentType) {
invalidateStatistics();
}
} else {
/*
* Otherwise, applies a rescale operation.
*/
double minimum = ImageUtilities.minimum(type);
double maximum = ImageUtilities.maximum(type);
if (minimum == 0 && ImageUtilities.isFloatType(currentType)) {
/*
* If converting from a floating point type to an unsigned integer type,
* set the minimum to 1 in order to reserve the value 0 for NaN values.
*/
minimum = 1;
}
final double[][] extrema = getExtremas();
final int length = extrema[0].length;
final double[] scales = new double[length];
final double[] offsets = new double[length];
for (int i=0; i<length; i++) {
final double cmin = extrema[0][i];
final double cmax = extrema[1][i];
final double scale = (maximum - minimum) / (cmax - cmin);
scales [i] = scale;
offsets[i] = minimum - scale * cmin;
}
final RenderingHints hints = getRenderingHints(type);
image = RescaleDescriptor.create(
image, // The source image.
scales, // The per-band constants to multiply by.
offsets, // The per-band offsets to be added.
hints); // The rendering hints.
invalidateStatistics(); // Extremas are no longer valids.
}
// Post conditions for this method contract.
assert image.getSampleModel().getDataType() == type;
}
/**
* Reduces the color model to {@link IndexColorModel}. If the current {@linkplain #image image}
* already uses an {@code IndexColorModel}, then this method does nothing. Otherwise this method
* performs an Error Diffusion or an Ordered Dither operation according the value of the
* {@link #COLOR_QUANTIZATION} rendering hint. If this hint is not provided, then the selected
* method is implementation-dependent and may vary in future versions.
* <p>
* The current implementation performs its work on the RGB color space only.
*
* @see #isIndexed
* @see #COLOR_QUANTIZATION
* @see ErrorDiffusionDescriptor
* @see OrderedDitherDescriptor
*/
private void forceIndexColorModel() {
if (image.getColorModel() instanceof IndexColorModel) {
// Already an index color model - nothing to do.
return;
}
enableTileCache(false);
setColorSpaceType(PaletteInterpretation.RGB);
if (image.getColorModel().hasAlpha()) {
// Discarts the alpha band, which is assumed the last one.
retainBands(0, -2);
}
enableTileCache(true);
final RenderingHints hints = getRenderingHints();
ColorQuantization method = (ColorQuantization) hints.get(COLOR_QUANTIZATION);
if (method == null) {
method = ColorQuantization.ERROR_DIFFUSION; // Default value.
}
final ColorCube colorMap = ColorCube.BYTE_496; // Assumes RGB color space.
switch (method) {
case ERROR_DIFFUSION: {
final KernelJAI ditherMask = KernelJAI.ERROR_FILTER_FLOYD_STEINBERG;
image = ErrorDiffusionDescriptor.create(image, colorMap, ditherMask, hints);
break;
}
case ORDERED_DITHER: {
final KernelJAI[] ditherMask = KernelJAI.DITHER_MASK_443;
image = OrderedDitherDescriptor.create(image, colorMap, ditherMask, hints);
break;
}
default: {
throw new IllegalArgumentException(Errors.format(
Errors.Keys.IllegalArgument_2, "method", method));
}
}
invalidateStatistics();
// Post conditions for this method contract.
assert isIndexed();
}
/**
* Reduces the color model to {@link IndexColorModel} with {@linkplain Transparency#BITMASK
* bitmask} transparency. If the current {@linkplain #image image} already uses a suitable
* color model, then this method does nothing.
*
* @param transparent A pixel value to define as the transparent pixel, or -1 for the default.
* The default is to reuse the existing {@linkplain #getTransparentPixel() transparent
* pixel} if there is one, or to use the one with the smallest alpha value otherwise,
* or 0 if all colors are opaque.
*
* @see #isIndexed
* @see #isTranslucent
* @see #COLOR_QUANTIZATION
*/
public void forceBitmaskIndexColorModel(int transparent) {
final ColorModel cm = image.getColorModel();
if (cm instanceof IndexColorModel) {
final IndexColorModel oldCM = (IndexColorModel) cm;
if (transparent < 0) {
transparent = oldCM.getTransparentPixel();
}
if (oldCM.getTransparency() == Transparency.BITMASK) {
if (oldCM.getTransparentPixel() == transparent) {
// Suitable color model. There is nothing to do.
return;
}
}
if (transparent < 0) {
int min = 256;
final int mapSize = oldCM.getMapSize();
for (int i=0; i<mapSize; i++) {
final int alpha = oldCM.getAlpha(i);
if (alpha < min) {
min = alpha;
transparent = i;
}
}
}
/*
* The Index Color Model needs to be replaced. Creates a lookup table mapping from the
* old pixel values to new pixels values, with transparent colors mapped to the new
* transparent pixel value. The lookup table uses TYPE_BYTE or TYPE_USHORT, which are
* the two only types supported by IndexColorModel.
*/
final int pixelSize = oldCM.getPixelSize();
transparent &= (1 << pixelSize) - 1;
final int mapSize = oldCM.getMapSize();
final int newSize = Math.max(mapSize, transparent + 1);
final boolean wide = (newSize > 256);
final Object data = wide ? new short[mapSize] : new byte[mapSize];
boolean changed = false;
for (int i=0; i<mapSize; i++) {
final int ni = (oldCM.getAlpha(i) == 0) ? transparent : i;
if (wide) ((short[]) data)[i] = (short) ni;
else ((byte []) data)[i] = (byte) ni;
changed |= (ni != i);
}
/*
* Now we need to perform the lookup transformation. First we create the new color
* model with a bitmask transparency using the transparency index specified to this
* method. Then we perform the lookup operation.
*/
final int[] RGB = new int[newSize];
oldCM.getRGBs(RGB);
final IndexColorModel newCM = ColorModels.unique(new IndexColorModel(pixelSize, newSize,
RGB, 0, false, transparent, ColorUtilities.getTransferType(newSize)));
if (!changed) {
// Special case if the lookup don't do anything. Just replace the color model.
image = ImageUtilities.replaceColorModel(image, newCM);
return; // For preventing the call to invalidateStatistics().
} else {
final LookupTableJAI lookupTable = wide ?
new LookupTableJAI((short[]) data, true) :
new LookupTableJAI((byte []) data);
final RenderingHints hints = getRenderingHints();
setColorModel(hints, newCM);
hints.put(JAI.KEY_REPLACE_INDEX_COLOR_MODEL, Boolean.FALSE);
image = LookupDescriptor.create(image, lookupTable, hints);
}
} else {
/*
* The image is not indexed. Gets the alpha channel, assuming that it is the last
* channel. This is always the case when using the standard ComponentColorModel.
*/
RenderedImage alphaChannel = null;
if (cm.hasAlpha()) {
final ImageWorker fork = new ImageWorker(this);
fork.enableTileCache(false);
fork.retainBands(-1, -1);
fork.binarize(false);
fork.xor(new int[] {-1});
alphaChannel = fork.image;
}
/*
* Forces to index color model, loosing the alpha channel here. Note that the color
* quantization uses a ColorCube with some offset (38 in the case of BYTE_496), which
* means that index 0 is available for making it transparent.
*/
enableTileCache(false);
forceIndexColorModel();
enableTileCache(true);
if (transparent < 0) {
transparent = 0;
}
IndexColorModel icm = (IndexColorModel) image.getColorModel();
if (icm.getAlpha(transparent) != 0) {
final int[] ARGB = new int[icm.getMapSize()];
icm.getRGBs(ARGB);
icm = new IndexColorModel(icm.getPixelSize(), ARGB.length, ARGB, 0,
icm.hasAlpha(), transparent, icm.getTransferType());
}
final RenderingHints hints = getRenderingHints();
setColorModel(hints, icm);
if (alphaChannel != null) {
// Uses the alpha channel as a mask for replacing pixels by the transparent value.
image = JAI.create(Mask.OPERATION_NAME, new ParameterBlockJAI(Mask.OPERATION_NAME)
.addSource(image).addSource(alphaChannel).set(new double[] {transparent}, 0), hints);
} else {
// Replaces only the color model.
image = NullDescriptor.create(image, hints);
}
}
invalidateStatistics();
// Post conditions for this method contract.
assert isIndexed();
assert !isTranslucent();
}
/**
* Reformats the {@linkplain ColorModel color model} to a {@linkplain ComponentColorModel
* component color model} preserving transparency. This is used especially in order to go
* from {@link PackedColorModel} to {@link ComponentColorModel}, which seems to be well
* accepted by PNG and TIFF encoders.
* <p>
* <b>Tip:</b> If the source image is known to have only one tile and to use the
* {@link IndexColorModel}, then the {@link IndexColorModel#convertToIntDiscrete}
* method is an alternative that may be worth consideration.
*
* {@note This code is adapted from jai-interests mailing list archive.}
*
* @see #IGNORE_FULLY_TRANSPARENT_PIXELS
* @see FormatDescriptor
*/
@SuppressWarnings("fallthrough")
private void forceComponentColorModel() {
final ColorModel cm = image.getColorModel();
if (cm instanceof ComponentColorModel) {
// Already an component color model - nothing to do.
return;
}
/*
* The IndexColorModel case. We will expand the indexed values using a lookup table.
* They will be expanded to gray scale if the color map contains only gray colors, or
* to full RGB(A) otherwise.
*/
if (cm instanceof IndexColorModel) {
final IndexColorModel icm = (IndexColorModel) cm;
final RenderingHints hints = getRenderingHints();
Boolean ignoreTransparents = (Boolean) hints.get(IGNORE_FULLY_TRANSPARENT_PIXELS);
if (ignoreTransparents == null) {
ignoreTransparents = Boolean.TRUE;
}
final boolean isGrayScale = ColorUtilities.isGrayPalette(icm, ignoreTransparents);
final boolean hasAlpha = icm.hasAlpha();
final int numColorBands = isGrayScale ? 1 : 3;
final byte[][] data = new byte[hasAlpha ? numColorBands + 1 : numColorBands][icm.getMapSize()];
switch (numColorBands) {
default: // Fallthrough in all cases.
case 3: icm.getBlues (data[2]);
case 2: icm.getGreens(data[1]);
case 1: icm.getReds (data[0]);
case 0: break;
}
if (hasAlpha) {
icm.getAlphas(data[numColorBands]);
}
final LookupTableJAI lut = new LookupTableJAI(data);
setColorModel(hints, new ComponentColorModel(
ColorSpace.getInstance(isGrayScale ? CS_GRAY : CS_sRGB),
hasAlpha,
cm.isAlphaPremultiplied(),
cm.getTransparency(),
cm.getTransferType()));
image = LookupDescriptor.create(image, lut, hints);
} else {
/*
* For any color model other than IndexColorModel, setup an ImageLayout having
* the new ColorModel and get the "Format" operation to apply the layout change.
* Most of the code adapted from jai-interests is in 'getRenderingHints(int)'.
*/
final int type = (cm instanceof DirectColorModel) ?
TYPE_BYTE : image.getSampleModel().getTransferType();
final RenderingHints hints = getRenderingHints(type);
image = FormatDescriptor.create(image, type, hints);
}
invalidateStatistics();
// Post conditions for this method contract.
assert image.getColorModel() instanceof ComponentColorModel;
}
/**
* Sets the color model to the given target type. If the {@linkplain #image image} already uses
* a color model of the given type, then this method does nothing. Otherwise the operation
* depends on the target type, enumerated below:
*
* {@section Index Color Model}
* If the target type is {@link IndexColorModel}, then this method performs a color reduction
* using an <cite>Error Diffusion</cite> or an <cite>Ordered Dither</cite> operation depending
* the value of the {@link #COLOR_QUANTIZATION} rendering hint. If this hint is not provided,
* then the selected method is implementation-dependent and may vary in future versions.
*
* {@note The current implementation performs its work on the RGB color space only.}
*
* {@section Component Color Model}
* If the target type is {@link ComponentColorModel}, then this method reformats the current
* image preserving transparency. The result can have any number of bands from 1 to 4 inclusive,
* depending on the color space (grayscale or RGB) and the presence of alpha channel.
*
* {@note This code is adapted from jai-interests mailing list archive.}
*
* @param type The target color model type. Currently supported types are
* {@link IndexColorModel} and {@link ComponentColorModel}. More
* may be added in the future.
*
* @see #isIndexed
* @see #COLOR_QUANTIZATION
* @see #IGNORE_FULLY_TRANSPARENT_PIXELS
* @see ErrorDiffusionDescriptor
* @see OrderedDitherDescriptor
* @see FormatDescriptor
* @see IndexColorModel#convertToIntDiscrete
*/
public void setColorModelType(final Class<? extends ColorModel> type) {
ensureNonNull("type", type);
if (IndexColorModel.class.isAssignableFrom(type)) {
forceIndexColorModel();
} else if (ComponentColorModel.class.isAssignableFrom(type)) {
forceComponentColorModel();
} else {
throw new IllegalArgumentException(Errors.format(
Errors.Keys.UnknownType_1, type));
}
}
/**
* Forces the {@linkplain #image image} color model to the given {@linkplain ColorSpace color
* space} type. If the current color space is already of the given type, then this method does
* nothing.
* <p>
* If a color space change is performed, then this operation creates an opaque {@link ColorModel}
* because the {@code "ColorConvert"} operation treats data as having no alpha channel.
* Consequently the alpha channel may be lost as a result of a call to this method.
* <p>
* Integral data are assumed to occupy the full range of the respective data type;
* floating point data are assumed to be normalized to the range [0.0 … 1.0].
*
* @param type The desired Color Space type.
*
* @see #getColorSpaceType
* @see ColorConvertDescriptor
*
* @since 3.00
*/
public void setColorSpaceType(final PaletteInterpretation type) {
ensureNonNull("type", type);
if (!type.equals(getColorSpaceType())) {
forceComponentColorModel();
final ColorSpace cs;
if (type.equals(PaletteInterpretation.RGB)) {
cs = ColorSpace.getInstance(CS_sRGB);
} else if (type.equals(PaletteInterpretation.GRAY)) {
cs = ColorSpace.getInstance(CS_GRAY);
} else if (type.equals(IHS)) {
cs = IHSColorSpace.getInstance();
} else {
throw new UnsupportedOperationException(type.toString());
}
final int[] numBits = new int[cs.getNumComponents()];
final int t = image.getSampleModel().getDataType();
Arrays.fill(numBits, DataBuffer.getDataTypeSize(t));
ColorModel cm = new ComponentColorModel(cs, numBits, false, false, Transparency.OPAQUE, t);
final RenderingHints hints = getRenderingHints();
cm = setColorModel(hints, cm);
image = ColorConvertDescriptor.create(image, cm, hints);
invalidateStatistics();
}
// Post conditions for this method contract.
assert type.equals(getColorSpaceType());
}
/**
* Creates an image which represents approximatively the intensity of {@linkplain #image image}.
* The result is always a single-banded image. If the image uses a gray scale or an {@linkplain
* IHSColorSpace IHS color space}, then this method just {@linkplain #retainBands retains the
* first band} without any further processing. Otherwise, this method performs a simple
* {@linkplain BandCombineDescriptor band combine} operation on the image in order to come up
* with a simple estimation of the intensity of based on the average value of the color
* components. Note that the alpha band is stripped from the image.
*
* {@note The result is a usually a gray scale image. Nevertheless it is not the same than
* invoking <code>setColorSpaceType(PaletteInterpretation.GRAY)</code> because this
* <code>intensity()</code> method gives equal weight to all RGB bands, while
* conversion to gray color space involve a more complex combination of bands.}
*
* @see BandCombineDescriptor
*/
public void intensity() {
/*
* If the color model already uses a IHS color space or a gray scale color space,
* keep only the intensity band. Otherwise, we need a RGB color space to be sure
* to understand what we are doing.
*/
final PaletteInterpretation type = getColorSpaceType();
if (type != null && (type.equals(PaletteInterpretation.GRAY) || type.equals(IHS))) {
retainBands(0, 0);
invalidateStatistics();
return;
}
enableTileCache(false);
setColorSpaceType(PaletteInterpretation.RGB);
enableTileCache(true);
/*
* Prepares a gray scale color model for all cases, then checks for the
* IndexColorModel particular case. For that one, we will use a TableLookup.
*/
final ColorModel cm = image.getColorModel();
final RenderingHints hints = getRenderingHints();
setColorModel(hints, new ComponentColorModel(ColorSpace.getInstance(CS_GRAY),
false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE));
if (cm instanceof IndexColorModel) {
final IndexColorModel icm = (IndexColorModel) cm;
final byte[] data = new byte[icm.getMapSize()];
for (int i=0; i<data.length; i++) {
final int RGB = icm.getRGB(i) & 0xFFFFFF;
data[i] = (byte) (((RGB & 0xFF) + ((RGB >>> 8) & 0xFF) + ((RGB >>> 16) & 0xFF)) / 3);
}
if (!LookupTables.isIdentity(data)) {
final LookupTableJAI lut = new LookupTableJAI(data);
image = LookupDescriptor.create(image, lut, hints);
invalidateStatistics();
return;
}
}
/*
* If there is only one color band, there is nothing to do except removing the alpha band
* if there is one. We will nevertheless replace the color model by the gray scale one so
* the user get an image that looks like an intensity image, but the actual pixel values
* are passed unchanged.
*/
final int numColorBands = cm.getNumColorComponents();
if (numColorBands == 1) {
retainBands(0, 0); // No-op if there is no alpha band.
if (!PaletteInterpretation.GRAY.equals(getColorSpaceType())) {
image = NullDescriptor.create(image, hints);
// Statistics are sill valid, since the Null
// operation doesn't change pixel values.
}
return;
}
/*
* We have more than one band. Note that there is no need to remove the
* alpha band before to apply the "bandCombine" operation - it is
* sufficient to let the coefficient for the alpha band to the 0 value.
*/
final double[] coeff = new double[cm.getNumComponents() + 1];
Arrays.fill(coeff, 0, numColorBands, 1.0 / numColorBands);
image = BandCombineDescriptor.create(image, new double[][] {coeff}, hints);
invalidateStatistics();
// Post conditions for this method contract.
assert getNumBands() == 1;
}
/**
* Merges the bands of the given image with the bands of the current {@linkplain #image image}.
* The new bands can be merged before or after the bands of the current image. They can also be
* merged in the middle, but this is less efficient.
*
* @param toAdd Image having the bands to merge with the bands of the current image.
* @param insertAt Where to insert the new bands: 0 for inserting the new bands before the
* current ones, or {@link #getNumBands() getNumBands()} for inserting the new bands
* after the current ones. Value -1 can be used as a shortcut for {@code getNumBands()}.
* Intermediate values are also allowed, but they are less common and less efficient.
*/
public void mergeBands(RenderedImage toAdd, int insertAt) {
ensureNonNull("toAdd", toAdd);
final int numBands = getNumBands();
if (insertAt < 0) insertAt += numBands + 1;
ensureValidIndex(numBands + 1, insertAt);
if (insertAt == 0) {
image = BandMergeDescriptor.create(toAdd, image, getRenderingHints());
} else if (insertAt == numBands) {
image = BandMergeDescriptor.create(image, toAdd, getRenderingHints());
} else {
enableTileCache(false);
final RenderedImage original = image;
retainBands(0, insertAt - 1);
mergeBands(toAdd, insertAt);
toAdd = image;
image = original;
retainBands(insertAt, -1);
enableTileCache(true);
mergeBands(toAdd, 0);
}
invalidateStatistics();
}
/**
* Retains the bands in the range {@code first} to {@code last} inclusive. All other bands
* (if any) are discarded without any further processing. This method does nothing if the
* given range include all bands.
* <p>
* For convenience, negative parameter values are relative to the {@linkplain #getNumBands()
* number of bands} (i.e. the number of bands is added to negative parameter values). So -1
* stands for the last band, -2 for the band before the last one, <i>etc.</i>
* <p>
* <b>Examples:</b>
* <ul>
* <li>{@code retainBands( 0, 0)} retains the first band.</li>
* <li>{@code retainBands(-1, -1)} retains the last band.</li>
* <li>{@code retainBands( 1, -1)} retains all bands except the first one.</li>
* <li>{@code retainBands( 0, -2)} retains all bands except the last one.</li>
* </ul>
*
* @param first The first band to retain, inclusive.
* @param last The last band to retain, <strong>inclusive</strong>.
*
* @see #getNumBands
* @see BandSelectDescriptor
*/
public void retainBands(int first, int last) {
final int numBands = getNumBands();
if (first < 0) first += numBands;
if (last < 0) last += numBands;
if (first < 0 || last < first || last >= numBands) {
throw new IndexOutOfBoundsException(Errors.format(Errors.Keys.IllegalRange_2, first, last));
}
final int count = last - first + 1;
if (count != numBands) {
final int[] bands = new int[count];
for (int i=0; i<count; i++) {
bands[i] = first++;
}
image = BandSelectDescriptor.create(image, bands, getRenderingHints());
}
// Post conditions for this method contract.
assert getNumBands() <= numBands;
}
/**
* Retains the given bands of the {@linkplain #image image}. All other bands (if any)
* are discarded without any further processing.
*
* @param bands The bands to retain.
*
* @see #getNumBands
* @see BandSelectDescriptor
*/
public void retainBands(final int[] bands) {
image = BandSelectDescriptor.create(image, bands, getRenderingHints());
}
/**
* Binarizes the {@linkplain #image image}. If the image is multi-bands, then this method first
* computes an estimation of its {@linkplain #intensity intensity}. Then, the threshold value
* is set halfway between the {@linkplain #getMinimums minimal} and {@linkplain #getMaximums
* maximal} values found in the image and {@link #binarize(double)} is invoked with that
* threshold
*
* @param dynamic If {@code true}, the minimum and maximum values are computed dynamically
* from the sample values found in the image. If {@code false}, minimum and maximum
* values are determined only from the data type (for example they are always 0 and
* 255 for {@link DataBuffer#TYPE_BYTE}).
*
* @see #isBinary
* @see #binarize(double)
* @see #binarize(double,double)
* @see BinarizeDescriptor
*/
public void binarize(final boolean dynamic) {
if (!isBinary()) {
if (getNumBands() != 1) {
enableTileCache(false);
intensity();
enableTileCache(true);
}
final double minimum, maximum;
if (dynamic) {
final double[][] extremas = getExtremas();
minimum = extremas[0][0];
maximum = extremas[1][0];
} else {
final int type = image.getSampleModel().getDataType();
if (ImageUtilities.isFloatType(type)) {
// Assuming normalized alpha values.
minimum = 0;
maximum = 1;
} else {
minimum = ImageUtilities.minimum(type);
maximum = ImageUtilities.maximum(type);
}
}
binarize((minimum + maximum) / 2);
}
// Post conditions for this method contract.
assert isBinary();
}
/**
* Binarizes the {@linkplain #image image}. If the image is multi-bands, then this method first
* computes an estimation of its {@linkplain #intensity intensity}. If the image is already
* binarized, then this method does nothing.
*
* @param threshold The threshold value.
*
* @see #isBinary
* @see #binarize(boolean)
* @see #binarize(double,double)
* @see BinarizeDescriptor
*/
public void binarize(final double threshold) {
// Because binary image can contains only 0 or 1 values, only
// threshold values in the range ]0..1] can be no-op.
if (threshold <= 0 || threshold > 1 || !isBinary()) {
if (getNumBands() != 1) {
enableTileCache(false);
intensity();
enableTileCache(true);
}
final RenderingHints hints = getRenderingHints();
image = BinarizeDescriptor.create(image, threshold, hints);
invalidateStatistics();
}
// Post conditions for this method contract.
assert isBinary();
}
/**
* Binarizes the {@linkplain #image image} (if not already done) and replaces all 0 values by
* {@code value0} and all 1 values by {@code value1}. If the image should be binarized using
* a custom threshold value (instead of the automatic one), invoke {@link #binarize(double)}
* explicitly before this method.
*
* @param value0 The value to be substituted to 0 in the binarized image.
* @param value1 The value to be substituted to 1 in the binarized image.
*
* @see #isBinary
* @see #binarize(double)
*/
public void binarize(final double value0, final double value1) {
enableTileCache(false);
binarize(true);
enableTileCache(true);
final LookupTableJAI table;
final int iv0 = (int) value0;
final int iv1 = (int) value1;
if (iv0 == value0 && iv1 == value1) {
final int min = Math.min(iv0, iv1);
final int max = Math.max(iv0, iv1);
if (min >= 0) {
if (max <= 0xFF) {
table = new LookupTableJAI(new byte[] {(byte) iv0, (byte) iv1});
} else if (max <= 0xFFFF) {
table = new LookupTableJAI(new short[] {(short) iv0, (short) iv1}, true);
} else {
table = new LookupTableJAI(new int[] {iv0, iv1});
}
} else if (min >= Short.MIN_VALUE && max <= Short.MAX_VALUE) {
table = new LookupTableJAI(new short[] {(short) iv0, (short) iv1}, false);
} else {
table = new LookupTableJAI(new int[] {iv0, iv1});
}
} else {
final float fv0 = (float) value0;
final float fv1 = (float) value1;
if (Double.doubleToRawLongBits(fv0) == Double.doubleToRawLongBits(value0) &&
Double.doubleToRawLongBits(fv1) == Double.doubleToRawLongBits(value1))
{
table = new LookupTableJAI(new float[] {fv0, fv1});
} else {
table = new LookupTableJAI(new double[] {value0, value1});
}
}
final RenderingHints hints = getRenderingHints();
setColorModel(hints, new ComponentColorModel(ColorSpace.getInstance(CS_GRAY),
false, false, Transparency.OPAQUE, table.getDataType()));
image = LookupDescriptor.create(image, table, hints);
invalidateStatistics();
}
/**
* Masks the given background values, to be replaced by the given new values. This method
* computes a {@linkplain SilhouetteMask silhouette mask} of the current {@linkplain #image
* image} using the given background values, then applies this mask on the image using the
* {@link #mask(RenderedImage,double[]) mask} method below.
* <p>
* This method is appropriate for replacing the background color surrounding a rotated
* rectangular area, as illustrated in the {@link Mask} javadoc.
*
* @param background The background color, as an array of sample values, to be replaced by the
* new sampel values. The length of this array should be equals to the
* {@linkplain #getNumBands number of bands} of the image.
* @param newValues The new sample values to be given to the background pixels, or {@code null}
* for assigning the transparent color to the background.
*
* @see SilhouetteMask
*/
@SuppressWarnings("fallthrough")
public void maskBackground(final double[][] background, double[] newValues) {
ensureNonNull("background", background);
switch (background.length) {
case 0: return;
case 1: if (Arrays.equals(background[0], newValues)) return;
}
RenderedImage mask = JAI.create(SilhouetteMask.OPERATION_NAME,
new ParameterBlockJAI(SilhouetteMask.OPERATION_NAME)
.addSource(image).set(background, 0), getRenderingHints());
if (newValues == null) {
/*
* User wants to make the background transparent. If the image uses IndexColorModel,
* we will try to recycle the existing transparent pixel value (if any) and set the
* background to that value.
*/
final ColorModel cm = image.getColorModel();
if (cm instanceof IndexColorModel) {
final IndexColorModel icm = (IndexColorModel) cm;
int transparent;
switch (icm.getTransparency()) {
case Transparency.OPAQUE: {
if (background.length != 0) {
final double[] samples = background[0];
if (samples.length != 0) {
transparent = (int) samples[0];
break;
}
}
// Fall through; the end effect will be to use 0.
}
default: {
/*
* Reuses the current transparent pixel, which may still -1.
* forceBitmaskIndexColorModel(-1) will searches for the pixel
* having the smallest alpha value, or use 0 if none are found.
*/
transparent = icm.getTransparentPixel();
break;
}
}
forceBitmaskIndexColorModel(transparent);
newValues = new double[] {getTransparentPixel()};
} else if (!cm.hasAlpha()) {
/*
* If the image uses anything else than IndexColorModel, then we need to add
* an alpha band if it doesn't already have one. If an alpha band is already
* there, then we just need to set the background pixels to zero.
*/
final int type = image.getSampleModel().getDataType();
final double max = ImageUtilities.isFloatType(type) ? 1 : ImageUtilities.maximum(type);
final ImageWorker fork = new ImageWorker(this);
fork.setImage(mask);
fork.enableTileCache(false);
fork.binarize(max, 0);
mask = fork.image;
mergeBands(mask, -1);
return;
} else {
newValues = new double[cm.getNumComponents()];
// Left all values initialized to zero.
}
}
mask(mask, newValues);
}
/**
* Applies the specified mask over the current {@linkplain #image}. The mask is typically
* {@linkplain #binarize(boolean) binarized}, but this is not mandatory. For every pixels
* in the mask having a value different than zero, the corresponding pixel in the
* {@linkplain #image} will be set to the specified {@code newValues}.
*
* @param mask The mask to apply.
* @param newValues The new sample values for every pixels in the {@linkplain #image image}
* corresponding to a non-zero value in the mask. If this parameter is
* {@code null}, then the non-zero value in the mask itself will be used.
* If non-null, then the array length should be equals to the number of
* bands in the destination image.
*
* @see Mask
*/
public void mask(final RenderedImage mask, final double[] newValues) {
ensureNonNull("mask", mask);
image = JAI.create(Mask.OPERATION_NAME, new ParameterBlockJAI(Mask.OPERATION_NAME)
.addSource(image).addSource(mask).set(newValues, 0), getRenderingHints());
invalidateStatistics();
}
/**
* Adds every pixels of the given image to the current {@linkplain #image image}.
* See JAI {@link AddDescriptor} for details.
*
* @param toAdd The image to be added to the one in this worker.
*
* @see AddDescriptor
*/
public void add(final RenderedImage toAdd) {
ensureNonNull("toAdd", toAdd);
image = AddDescriptor.create(image, toAdd, getRenderingHints());
invalidateStatistics();
}
/**
* Adds every pixels in the current {@linkplain #image image} with the given constants.
* The length of the given array must be equals to the {@linkplain #getNumBands() number
* of bands}. See JAI {@link AddConstDescriptor} for details.
*
* @param values The constants to be added.
*
* @see AddConstDescriptor
*/
public void add(final double[] values) {
ensureNonNull("values", values);
image = AddConstDescriptor.create(image, values, getRenderingHints());
invalidateStatistics();
}
/**
* Subtracts every pixels of the given image from the current {@linkplain #image image}.
* See JAI {@link SubtractDescriptor} for details.
*
* @param toSubtract The image to be subtracted from the one in this worker.
*
* @see SubtractDescriptor
*/
public void subtract(final RenderedImage toSubtract) {
ensureNonNull("toSubtract", toSubtract);
image = SubtractDescriptor.create(image, toSubtract, getRenderingHints());
invalidateStatistics();
}
/**
* Subtracts the given constants from every pixels in the current {@linkplain #image image}.
* The length of the given array must be equals to the {@linkplain #getNumBands() number
* of bands}. See JAI {@link SubtractConstDescriptor} for details.
*
* @param values The constants to be subtracted.
*
* @see SubtractConstDescriptor
*/
public void subtract(final double[] values) {
ensureNonNull("values", values);
image = SubtractConstDescriptor.create(image, values, getRenderingHints());
invalidateStatistics();
}
/**
* Multiplies every pixels of the given image to the current {@linkplain #image image}.
* See JAI {@link MultiplyDescriptor} for details.
*
* @param toMultiply The image to be added to the one in this worker.
*
* @see MultiplyDescriptor
*/
public void multiply(final RenderedImage toMultiply) {
ensureNonNull("toMultiply", toMultiply);
image = MultiplyDescriptor.create(image, toMultiply, getRenderingHints());
invalidateStatistics();
}
/**
* Multiplies every pixels in the current {@linkplain #image image} with the given constants.
* The length of the given array must be equals to the {@linkplain #getNumBands() number of
* bands}. See JAI {@link MultiplyConstDescriptor} for details.
*
* @param values The constants to be multiplied.
*
* @see MultiplyConstDescriptor
*/
public void multiply(final double[] values) {
ensureNonNull("values", values);
image = MultiplyConstDescriptor.create(image, values, getRenderingHints());
invalidateStatistics();
}
/**
* Divides every pixels of the given image to the current {@linkplain #image image}.
* See JAI {@link DivideDescriptor} for details.
*
* @param toDivide The image to divide the one in this worker.
*
* @see DivideDescriptor
*/
public void divide(final RenderedImage toDivide) {
ensureNonNull("toDivide", toDivide);
image = DivideDescriptor.create(image, toDivide, getRenderingHints());
invalidateStatistics();
}
/**
* Divides every pixels in the current {@linkplain #image image} by the given constants.
* The length of the given array must be equals to the {@linkplain #getNumBands() number
* of bands}. See JAI {@link DivideByConstDescriptor} for details.
*
* @param values The divisor constants.
*
* @see DivideByConstDescriptor
*/
public void divideBy(final double[] values) {
ensureNonNull("values", values);
image = DivideByConstDescriptor.create(image, values, getRenderingHints());
invalidateStatistics();
}
/**
* Inverts the sign of pixel values in the {@linkplain #image image}.
*
* @see InvertDescriptor
*/
public void invert() {
image = InvertDescriptor.create(image, getRenderingHints());
invalidateStatistics();
}
/**
* Performs a bit-wise logical "xor" between every pixel in the same band of the
* {@linkplain #image image} and the constant from the corresponding array entry.
* See JAI {@link XorConstDescriptor} for details.
*
* @param values The constants to be xored.
*
* @see XorConstDescriptor
*/
public void xor(int[] values) {
ensureNonNull("values", values);
image = XorConstDescriptor.create(image, values, getRenderingHints());
invalidateStatistics();
}
}