/*
* Copyright 2010-2015 Institut Pasteur.
*
* This file is part of Icy.
*
* Icy is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Icy 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Icy. If not, see <http://www.gnu.org/licenses/>.
*/
package icy.image;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.util.List;
import javax.media.jai.BorderExtender;
import javax.media.jai.Interpolation;
import javax.media.jai.JAI;
import javax.media.jai.RenderedOp;
import javax.media.jai.operator.RotateDescriptor;
import javax.media.jai.operator.ScaleDescriptor;
import javax.swing.SwingConstants;
import icy.image.lut.LUT;
import icy.math.Scaler;
import icy.type.DataType;
import icy.type.collection.array.Array1DUtil;
import icy.type.collection.array.ArrayType;
import icy.type.collection.array.ArrayUtil;
/**
* {@link IcyBufferedImage} utilities class.<br>
* You can find here tools to clone, manipulate the image data type, its size...
*
* @author Stephane
*/
public class IcyBufferedImageUtil
{
public static enum FilterType
{
NEAREST, BILINEAR, BICUBIC
};
/**
* used for getARGBImage method (fast conversion)
*/
private static ARGBImageBuilder argbImageBuilder = new ARGBImageBuilder();
/**
* @deprecated Use {@link IcyBufferedImage#createFrom(BufferedImage)} instead.
*/
@Deprecated
public static IcyBufferedImage toIcyBufferedImage(BufferedImage image)
{
return IcyBufferedImage.createFrom(image);
}
/**
* @deprecated Use {@link IcyBufferedImage#createFrom(List)} instead.
*/
@Deprecated
public static IcyBufferedImage toIcyBufferedImage(List<BufferedImage> images)
{
return IcyBufferedImage.createFrom(images);
}
/**
* Draw the source {@link IcyBufferedImage} into the destination {@link BufferedImage}<br>
* If <code>dest</code> is <code>null</code> then a new TYPE_INT_ARGB {@link BufferedImage} is
* returned.<br>
*
* @param source
* source image
* @param dest
* destination image
* @param lut
* {@link LUT} is used for color calculation (internal lut is used if null).
*/
public static BufferedImage toBufferedImage(IcyBufferedImage source, BufferedImage dest, LUT lut)
{
final BufferedImage result;
if (dest == null)
result = new BufferedImage(source.getWidth(), source.getHeight(), BufferedImage.TYPE_INT_ARGB);
else
result = dest;
// else we need to convert to wanted type...
final Graphics2D g = result.createGraphics();
g.drawImage(getARGBImage(source, lut), 0, 0, null);
g.dispose();
return result;
}
/**
* Draw the source {@link IcyBufferedImage} into the destination {@link BufferedImage}<br>
* If <code>dest</code> is null then a new TYPE_INT_ARGB {@link BufferedImage} is returned.
*
* @param source
* source image
* @param dest
* destination image
*/
public static BufferedImage toBufferedImage(IcyBufferedImage source, BufferedImage dest)
{
return toBufferedImage(source, dest, null);
}
/**
* Convert the current {@link IcyBufferedImage} into a {@link BufferedImage} of the specified
* type.
*
* @param source
* source image
* @param imageType
* wanted image type, only the following is accepted :<br>
* BufferedImage.TYPE_INT_ARGB<br>
* BufferedImage.TYPE_INT_RGB<br>
* BufferedImage.TYPE_BYTE_GRAY<br>
* @param lut
* lut used for color calculation (source image lut is used if null)
* @return BufferedImage
*/
public static BufferedImage toBufferedImage(IcyBufferedImage source, int imageType, LUT lut)
{
if (source == null)
return null;
final BufferedImage outImg = new BufferedImage(source.getWidth(), source.getHeight(), imageType);
toBufferedImage(source, outImg, lut);
return outImg;
}
/**
* Convert the current {@link IcyBufferedImage} into a {@link BufferedImage} of the specified
* type.
*
* @param source
* source image
* @param imageType
* wanted image type, only the following is accepted :<br>
* BufferedImage.TYPE_INT_ARGB<br>
* BufferedImage.TYPE_INT_RGB<br>
* BufferedImage.TYPE_BYTE_GRAY<br>
* @return BufferedImage
*/
public static BufferedImage toBufferedImage(IcyBufferedImage source, int imageType)
{
return toBufferedImage(source, imageType, null);
}
/**
* Draw the source {@link IcyBufferedImage} into the destination ARGB {@link BufferedImage}<br>
* If <code>dest</code> is <code>null</code> then a new ARGB {@link BufferedImage} is
* returned.<br>
* This function is faster for ARGB conversion than
* {@link #toBufferedImage(IcyBufferedImage, BufferedImage, LUT)}
* but the output {@link BufferedImage} is fixed to ARGB type (TYPE_INT_ARGB) and the image
* cannot be volatile, use {@link #toBufferedImage(IcyBufferedImage, BufferedImage, LUT)} for no
* ARGB image or if you want volatile accelerated image.
*
* @param source
* source image
* @param dest
* destination image. Note that we access image data so it can't be volatile anymore
* which may result in slower drawing
* @param lut
* {@link LUT} is used for color calculation (internal lut is used if null).
* @deprecated Use {@link #toBufferedImage(IcyBufferedImage, BufferedImage, LUT)} instead.
*/
@Deprecated
public static BufferedImage getARGBImage(IcyBufferedImage source, LUT lut, BufferedImage dest)
{
if (source == null)
return null;
// use image lut when no specific lut
if (lut == null)
{
// manually update bounds if needed before doing RGB conversion from internal LUT
if (!source.getAutoUpdateChannelBounds())
source.updateChannelsBounds();
return argbImageBuilder.buildARGBImage(source, source.createCompatibleLUT(false), dest);
}
return argbImageBuilder.buildARGBImage(source, lut, dest);
}
/**
* Draw the source {@link IcyBufferedImage} into the destination ARGB {@link BufferedImage}<br>
* If <code>dest</code> is null then a new ARGB {@link BufferedImage} is returned.<br>
* <br>
* This function is faster for ARGB conversion than
* {@link #toBufferedImage(IcyBufferedImage, BufferedImage)}
* but the output {@link BufferedImage} is fixed to ARGB type (TYPE_INT_ARGB) and the image
* cannot be volatile, use {@link #toBufferedImage(IcyBufferedImage, BufferedImage)} for no
* ARGB image or if you want volatile accelerated image.
*
* @param source
* source image
* @param dest
* destination image. Note that we access image data so it can't be volatile anymore
* which may result in slower drawing
* @deprecated Use {@link #toBufferedImage(IcyBufferedImage, BufferedImage)} instead.
*/
@Deprecated
public static BufferedImage getARGBImage(IcyBufferedImage source, BufferedImage dest)
{
return getARGBImage(source, null, dest);
}
/**
* Convert the current {@link IcyBufferedImage} into a ARGB {@link BufferedImage}.<br>
* Note that we access image data so it can't be volatile anymore which may result in slower
* drawing.
*
* @param source
* source image
* @param lut
* {@link LUT} is used for color calculation (internal lut is used if null).
*/
public static BufferedImage getARGBImage(IcyBufferedImage source, LUT lut)
{
if (source == null)
return null;
// use image lut when no specific lut
if (lut == null)
{
// manually update bounds if needed before doing RGB conversion from internal LUT
if (!source.getAutoUpdateChannelBounds())
source.updateChannelsBounds();
return argbImageBuilder.buildARGBImage(source, source.createCompatibleLUT(false));
}
return argbImageBuilder.buildARGBImage(source, lut);
}
/**
* Convert the current {@link IcyBufferedImage} into a ARGB {@link BufferedImage}.<br>
* Note that we access image data so it can't be volatile anymore which may result in slower
* drawing.
*
* @param source
* source image
*/
public static BufferedImage getARGBImage(IcyBufferedImage source)
{
return getARGBImage(source, (LUT) null);
}
/**
* Convert the source image to the specified data type.<br>
* This method returns a new image (the source image is not modified).
*
* @param source
* source image
* @param dataType
* data type wanted
* @param scalers
* scalers for scaling internal data during conversion (1 scaler per channel).<br>
* Can be set to <code>null</code> to avoid value conversion.
* @return converted image
*/
public static IcyBufferedImage convertType(IcyBufferedImage source, DataType dataType, Scaler[] scalers)
{
if (source == null)
return null;
final DataType srcDataType = source.getDataType_();
// can't convert
if ((srcDataType == null) || (srcDataType == DataType.UNDEFINED) || (dataType == null)
|| (dataType == DataType.UNDEFINED))
return null;
final boolean srcSigned = srcDataType.isSigned();
final boolean dstSigned = dataType.isSigned();
final int sizeC = source.getSizeC();
final IcyBufferedImage result = new IcyBufferedImage(source.getSizeX(), source.getSizeY(), sizeC, dataType);
for (int c = 0; c < sizeC; c++)
{
// no rescale ?
if ((scalers == null) || (c >= scalers.length) || scalers[c].isNull())
// simple type change
ArrayUtil.arrayToSafeArray(source.getDataXY(c), result.getDataXY(c), srcSigned, dstSigned);
else
{
// first we convert in double
final double[] darray = Array1DUtil.arrayToDoubleArray(source.getDataXY(c), srcSigned);
// then we scale data
scalers[c].scale(darray);
// and finally we convert in wanted datatype
Array1DUtil.doubleArrayToSafeArray(darray, result.getDataXY(c), dstSigned);
}
}
// copy colormap from source image
result.setColorMaps(source);
// notify we modified data
result.dataChanged();
return result;
}
/**
* @deprecated Use {@link #convertType(IcyBufferedImage, DataType, Scaler[])} instead.
*/
@Deprecated
public static IcyBufferedImage convertToType(IcyBufferedImage source, DataType dataType, Scaler scaler)
{
if (source == null)
return null;
final DataType srcDataType = source.getDataType_();
// can't convert
if ((srcDataType == DataType.UNDEFINED) || (dataType == DataType.UNDEFINED))
return null;
final boolean srcSigned = srcDataType.isSigned();
final boolean dstSigned = dataType.isSigned();
final int sizeC = source.getSizeC();
final IcyBufferedImage result = new IcyBufferedImage(source.getSizeX(), source.getSizeY(), sizeC, dataType);
for (int c = 0; c < sizeC; c++)
{
// no rescale ?
if ((scaler == null) || scaler.isNull())
// simple type change
ArrayUtil.arrayToSafeArray(source.getDataXY(c), result.getDataXY(c), srcSigned, dstSigned);
else
{
// first we convert in double
final double[] darray = Array1DUtil.arrayToDoubleArray(source.getDataXY(c), srcSigned);
// then we scale data
scaler.scale(darray);
// and finally we convert in wanted datatype
Array1DUtil.doubleArrayToSafeArray(darray, result.getDataXY(c), dstSigned);
}
}
// copy colormap from source image
result.setColorMaps(source);
// notify we modified data
result.dataChanged();
return result;
}
/**
* Convert the source image to the specified data type.<br>
* This method returns a new image (the source image is not modified).
*
* @param dataType
* Data type wanted
* @param rescale
* Indicate if we want to scale data value according to data (or data type) range
* @param useDataBounds
* Only used when <code>rescale</code> parameter is true.<br>
* Specify if we use the data bounds for rescaling instead of data type bounds.
* @return converted image
*/
public static IcyBufferedImage convertToType(IcyBufferedImage source, DataType dataType, boolean rescale,
boolean useDataBounds)
{
if (source == null)
return null;
if (!rescale)
return convertType(source, dataType, null);
// convert with rescale
final double boundsDst[] = dataType.getDefaultBounds();
final int sizeC = source.getSizeC();
final Scaler[] scalers = new Scaler[sizeC];
// build scalers
for (int c = 0; c < sizeC; c++)
{
final double boundsSrc[];
if (useDataBounds)
boundsSrc = source.getChannelBounds(c);
else
boundsSrc = source.getChannelTypeBounds(c);
scalers[c] = new Scaler(boundsSrc[0], boundsSrc[1], boundsDst[0], boundsDst[1], false);
}
// use scaler to scale data
return convertType(source, dataType, scalers);
}
/**
* Convert the source image to the specified data type.<br>
* This method returns a new image (the source image is not modified).
*
* @param dataType
* data type wanted
* @param rescale
* indicate if we want to scale data value according to data type range
* @return converted image
*/
public static IcyBufferedImage convertToType(IcyBufferedImage source, DataType dataType, boolean rescale)
{
return convertToType(source, dataType, rescale, false);
}
/**
* Create a copy of the specified image.
*/
public static IcyBufferedImage getCopy(IcyBufferedImage source)
{
if (source == null)
return null;
// create a compatible image
final IcyBufferedImage result = new IcyBufferedImage(source.getSizeX(), source.getSizeY(), source.getSizeC(),
source.getDataType_());
// copy data from this image
result.copyData(source);
return result;
}
/**
* Creates a new image from the specified region of the source image.
*/
public static IcyBufferedImage getSubImage(IcyBufferedImage source, Rectangle region, int c, int sizeC)
{
if (source == null)
return null;
final int startX;
final int endX;
final int startY;
final int endY;
final int startC;
final int endC;
// infinite X dimension ?
if ((region.x == Integer.MIN_VALUE) && (region.width == Integer.MAX_VALUE))
{
startX = 0;
endX = source.getSizeX();
}
else
{
startX = Math.max(0, region.x);
endX = Math.min(source.getSizeX(), region.x + region.width);
}
// infinite Y dimension ?
if ((region.y == Integer.MIN_VALUE) && (region.height == Integer.MAX_VALUE))
{
startY = 0;
endY = source.getSizeY();
}
else
{
startY = Math.max(0, region.y);
endY = Math.min(source.getSizeY(), region.y + region.height);
}
// infinite C dimension ?
if ((c == Integer.MIN_VALUE) && (sizeC == Integer.MAX_VALUE))
{
startC = 0;
endC = source.getSizeC();
}
else
{
startC = Math.max(0, c);
endC = Math.min(source.getSizeC(), c + sizeC);
}
final int sizeX = endX - startX;
final int sizeY = endY - startY;
final int adjSizeC = endC - startC;
if ((sizeX <= 0) || (sizeY <= 0) || (adjSizeC <= 0))
return null;
// adjust rectangle
final DataType dataType = source.getDataType_();
final boolean signed = dataType.isSigned();
final IcyBufferedImage result = new IcyBufferedImage(sizeX, sizeY, adjSizeC, dataType);
final int srcSizeX = source.getSizeX();
for (int ch = startC; ch < endC; ch++)
{
final Object src = source.getDataXY(ch);
final Object dst = result.getDataXY(ch - startC);
int srcOffset = source.getOffset(startX, startY);
int dstOffset = 0;
for (int curY = 0; curY < sizeY; curY++)
{
Array1DUtil.arrayToArray(src, srcOffset, dst, dstOffset, sizeX, signed);
srcOffset += srcSizeX;
dstOffset += sizeX;
}
}
result.dataChanged();
return result;
}
/**
* Creates a new image which is a sub part of the source image from the specified region.
*
* @see #getSubImage(IcyBufferedImage, Rectangle, int, int)
*/
public static IcyBufferedImage getSubImage(IcyBufferedImage source, Rectangle region)
{
return getSubImage(source, region, 0, source.getSizeC());
}
/**
* @deprecated Use {@link #getSubImage(IcyBufferedImage, Rectangle, int, int)} instead.
*/
@Deprecated
public static IcyBufferedImage getSubImage(IcyBufferedImage source, int x, int y, int c, int sizeX, int sizeY,
int sizeC)
{
return getSubImage(source, new Rectangle(x, y, sizeX, sizeY), c, sizeC);
}
/**
* Creates a new image which is a sub part of the source image from the specified
* coordinates and dimensions.
*/
public static IcyBufferedImage getSubImage(IcyBufferedImage source, int x, int y, int w, int h)
{
return getSubImage(source, new Rectangle(x, y, w, h), 0, source.getSizeC());
}
/**
* Build a new single channel image (greyscale) from the specified source image channel.
*/
public static IcyBufferedImage extractChannel(IcyBufferedImage source, int channel)
{
return extractChannels(source, channel);
}
/**
* @deprecated Use {@link #extractChannels(IcyBufferedImage, int[])} instead.
*/
@Deprecated
public static IcyBufferedImage extractChannels(IcyBufferedImage source, List<Integer> channelNumbers)
{
if (source == null)
return null;
// create output
final IcyBufferedImage result = new IcyBufferedImage(source.getSizeX(), source.getSizeY(),
channelNumbers.size(), source.getDataType_());
final int sizeC = source.getSizeC();
// set data from specified band
for (int i = 0; i < channelNumbers.size(); i++)
{
final int channel = channelNumbers.get(i).intValue();
if (channel < sizeC)
result.setDataXY(i, source.getDataXY(channel));
}
return result;
}
/**
* Build a new image from the specified source image channels.
*/
public static IcyBufferedImage extractChannels(IcyBufferedImage source, int... channels)
{
if ((source == null) || (channels == null) || (channels.length == 0))
return null;
// create output
final IcyBufferedImage result = new IcyBufferedImage(source.getSizeX(), source.getSizeY(), channels.length,
source.getDataType_());
final int sizeC = source.getSizeC();
// set data from specified band
for (int i = 0; i < channels.length; i++)
{
final int channel = channels[i];
if (channel < sizeC)
result.setDataXY(i, source.getDataXY(channel));
}
return result;
}
/**
* Add empty channel(s) to the specified image and return result as a new image.
*
* @param source
* source image.
* @param index
* position where we want to add channel(s).
* @param num
* number of channel(s) to add.
*/
public static IcyBufferedImage addChannels(IcyBufferedImage source, int index, int num)
{
if (source == null)
return null;
final IcyBufferedImage result = new IcyBufferedImage(source.getSizeX(), source.getSizeY(),
source.getSizeC() + num, source.getDataType_());
for (int c = 0; c < index; c++)
result.copyData(source, c, c);
for (int c = index; c < source.getSizeC(); c++)
result.copyData(source, c, c + num);
return result;
}
/**
* Add an empty channel to the specified image and return result as a new image.
*
* @param source
* source image.
* @param index
* position where we want to add channel.
*/
public static IcyBufferedImage addChannel(IcyBufferedImage source, int index)
{
return addChannels(source, index, 1);
}
/**
* Add an empty channel to the specified image and return result as a new image.
*
* @param source
* source image.
*/
public static IcyBufferedImage addChannel(IcyBufferedImage source)
{
return addChannels(source, source.getSizeC(), 1);
}
/**
* Return a rotated version of the source image with specified parameters.
*
* @param source
* source image
* @param xOrigin
* X origin for the rotation
* @param yOrigin
* Y origin for the rotation
* @param angle
* rotation angle in radian
* @param filterType
* filter resampling method used
*/
public static IcyBufferedImage rotate(IcyBufferedImage source, double xOrigin, double yOrigin, double angle,
FilterType filterType)
{
if (source == null)
return null;
final Interpolation interpolation;
switch (filterType)
{
default:
case NEAREST:
interpolation = Interpolation.getInstance(Interpolation.INTERP_NEAREST);
break;
case BILINEAR:
interpolation = Interpolation.getInstance(Interpolation.INTERP_BILINEAR);
break;
case BICUBIC:
interpolation = Interpolation.getInstance(Interpolation.INTERP_BICUBIC);
break;
}
// use JAI scaler (use a copy to avoid source alteration)
final RenderedOp renderedOp = RotateDescriptor.create(getCopy(source), Float.valueOf((float) xOrigin),
Float.valueOf((float) yOrigin), Float.valueOf((float) angle), interpolation, null,
new RenderingHints(JAI.KEY_BORDER_EXTENDER, BorderExtender.createInstance(BorderExtender.BORDER_ZERO)));
return IcyBufferedImage.createFrom(renderedOp, source.isSignedDataType());
}
/**
* Return a rotated version of the source image with specified parameters.
*
* @param source
* source image
* @param angle
* rotation angle in radian
* @param filterType
* filter resampling method used
*/
public static IcyBufferedImage rotate(IcyBufferedImage source, double angle, FilterType filterType)
{
if (source == null)
return null;
return rotate(source, source.getSizeX() / 2d, source.getSizeY() / 2d, angle, filterType);
}
/**
* Return a rotated version of the source image with specified parameters.
*
* @param source
* source image
* @param angle
* rotation angle in radian
*/
public static IcyBufferedImage rotate(IcyBufferedImage source, double angle)
{
if (source == null)
return null;
return rotate(source, source.getSizeX() / 2d, source.getSizeY() / 2d, angle, FilterType.BILINEAR);
}
/**
* Returns a down scaled by a factor of 2 of the input image data (X and Y resolution are
* divided by 2).<br>
* This function is specifically optimized for factor 2 down scaling.
*
* @param input
* input image byte data array
* @param sizeX
* width of source image
* @param sizeY
* height of source image
* @param signed
* consider input byte data as signed (only meaningful when filter is enabled)
* @param filter
* enable pixel blending for better representation of the down sampled result image
* (otherwise nearest
* neighbor is used)
* @param output
* output buffer (single dimension array with same data type as source image data array).
*/
static void downscaleBy2(byte[] input, int sizeX, int sizeY, boolean signed, boolean filter, byte[] output)
{
final int halfSizeX = sizeX / 2;
final int halfSizeY = sizeY / 2;
if (filter)
{
int inOff = 0;
int outOff = 0;
for (int y = 0; y < halfSizeY; y++)
{
for (int x = 0, inOffset = inOff; x < halfSizeX; x++, inOffset += 2)
{
int val;
if (signed)
{
// accumulate 4 pixels
val = input[inOffset + 0];
val += input[inOffset + 1];
val += input[inOffset + sizeX + 0];
val += input[inOffset + sizeX + 1];
}
else
{
// accumulate 4 pixels
val = input[inOffset + 0] & 0xFF;
val += input[inOffset + 1] & 0xFF;
val += input[inOffset + sizeX + 0] & 0xFF;
val += input[inOffset + sizeX + 1] & 0xFF;
}
// divide by 4
val >>= 2;
// store result
output[outOff++] = (byte) val;
}
inOff += sizeX * 2;
}
}
else
{
int inOff = 0;
int outOff = 0;
for (int y = 0; y < halfSizeY; y++)
{
for (int x = 0, inOffset = inOff; x < halfSizeX; x++, inOffset += 2)
output[outOff++] = input[inOffset];
inOff += sizeX * 2;
}
}
}
/**
* Returns a down scaled by a factor of 2 of the input image data (X and Y resolution are
* divided by 2).<br>
* This function is specifically optimized for factor 2 down scaling.
*
* @param input
* input image byte data array
* @param sizeX
* width of source image
* @param sizeY
* height of source image
* @param signed
* consider input byte data as signed (only meaningful when filter is enabled)
* @param filter
* enable pixel blending for better representation of the down sampled result image
* (otherwise nearest
* neighbor is used)
* @param output
* output buffer (single dimension array with same data type as source image data array).
*/
static void downscaleBy2(short[] input, int sizeX, int sizeY, boolean signed, boolean filter, short[] output)
{
final int halfSizeX = sizeX / 2;
final int halfSizeY = sizeY / 2;
if (filter)
{
int inOff = 0;
int outOff = 0;
for (int y = 0; y < halfSizeY; y++)
{
for (int x = 0, inOffset = inOff; x < halfSizeX; x++, inOffset += 2)
{
int val;
if (signed)
{
// accumulate 4 pixels
val = input[inOffset + 0];
val += input[inOffset + 1];
val += input[inOffset + sizeX + 0];
val += input[inOffset + sizeX + 1];
}
else
{
// accumulate 4 pixels
val = input[inOffset + 0] & 0xFFFF;
val += input[inOffset + 1] & 0xFFFF;
val += input[inOffset + sizeX + 0] & 0xFFFF;
val += input[inOffset + sizeX + 1] & 0xFFFF;
}
// divide by 4
val >>= 2;
// store result
output[outOff++] = (short) val;
}
inOff += sizeX * 2;
}
}
else
{
int inOff = 0;
int outOff = 0;
for (int y = 0; y < halfSizeY; y++)
{
for (int x = 0, inOffset = inOff; x < halfSizeX; x++, inOffset += 2)
output[outOff++] = input[inOffset];
inOff += sizeX * 2;
}
}
}
/**
* Returns a down scaled by a factor of 2 of the input image data (X and Y resolution are
* divided by 2).<br>
* This function is specifically optimized for factor 2 down scaling.
*
* @param input
* input image byte data array
* @param sizeX
* width of source image
* @param sizeY
* height of source image
* @param signed
* consider input byte data as signed (only meaningful when filter is enabled)
* @param filter
* enable pixel blending for better representation of the down sampled result image
* (otherwise nearest
* neighbor is used)
* @param output
* output buffer (single dimension array with same data type as source image data array).
*/
static void downscaleBy2(int[] input, int sizeX, int sizeY, boolean signed, boolean filter, int[] output)
{
final int halfSizeX = sizeX / 2;
final int halfSizeY = sizeY / 2;
if (filter)
{
int inOff = 0;
int outOff = 0;
for (int y = 0; y < halfSizeY; y++)
{
for (int x = 0, inOffset = inOff; x < halfSizeX; x++, inOffset += 2)
{
long val;
if (signed)
{
// accumulate 4 pixels
val = input[inOffset + 0];
val += input[inOffset + 1];
val += input[inOffset + sizeX + 0];
val += input[inOffset + sizeX + 1];
}
else
{
// accumulate 4 pixels
val = input[inOffset + 0] & 0xFFFFFFFFL;
val += input[inOffset + 1] & 0xFFFFFFFFL;
val += input[inOffset + sizeX + 0] & 0xFFFFFFFFL;
val += input[inOffset + sizeX + 1] & 0xFFFFFFFFL;
}
// divide by 4
val >>= 2;
// store result
output[outOff++] = (int) val;
}
inOff += sizeX * 2;
}
}
else
{
int inOff = 0;
int outOff = 0;
for (int y = 0; y < halfSizeY; y++)
{
for (int x = 0, inOffset = inOff; x < halfSizeX; x++, inOffset += 2)
output[outOff++] = input[inOffset];
inOff += sizeX * 2;
}
}
}
/**
* Returns a down scaled by a factor of 2 of the input image data (X and Y resolution are
* divided by 2).<br>
* This function is specifically optimized for factor 2 down scaling.
*
* @param input
* input image byte data array
* @param sizeX
* width of source image
* @param sizeY
* height of source image
* @param filter
* enable pixel blending for better representation of the down sampled result image
* (otherwise nearest
* neighbor is used)
* @param output
* output buffer (single dimension array with same data type as source image data array).
*/
static void downscaleBy2(float[] input, int sizeX, int sizeY, boolean filter, float[] output)
{
final int halfSizeX = sizeX / 2;
final int halfSizeY = sizeY / 2;
if (filter)
{
int inOff = 0;
int outOff = 0;
for (int y = 0; y < halfSizeY; y++)
{
for (int x = 0, inOffset = inOff; x < halfSizeX; x++, inOffset += 2)
{
float val;
// accumulate 4 pixels
val = input[inOffset + 0];
val += input[inOffset + 1];
val += input[inOffset + sizeX + 0];
val += input[inOffset + sizeX + 1];
// divide by 4
val /= 4f;
// store result
output[outOff++] = val;
}
inOff += sizeX * 2;
}
}
else
{
int inOff = 0;
int outOff = 0;
for (int y = 0; y < halfSizeY; y++)
{
for (int x = 0, inOffset = inOff; x < halfSizeX; x++, inOffset += 2)
output[outOff++] = input[inOffset];
inOff += sizeX * 2;
}
}
}
/**
* Returns a down scaled by a factor of 2 of the input image data (X and Y resolution are
* divided by 2).<br>
* This function is specifically optimized for factor 2 down scaling.
*
* @param input
* input image byte data array
* @param sizeX
* width of source image
* @param sizeY
* height of source image
* @param filter
* enable pixel blending for better representation of the down sampled result image
* (otherwise nearest
* neighbor is used)
* @param output
* output buffer (single dimension array with same data type as source image data array).
*/
static void downscaleBy2(double[] input, int sizeX, int sizeY, boolean filter, double[] output)
{
final int halfSizeX = sizeX / 2;
final int halfSizeY = sizeY / 2;
if (filter)
{
int inOff = 0;
int outOff = 0;
for (int y = 0; y < halfSizeY; y++)
{
for (int x = 0, inOffset = inOff; x < halfSizeX; x++, inOffset += 2)
{
double val;
// accumulate 4 pixels
val = input[inOffset + 0];
val += input[inOffset + 1];
val += input[inOffset + sizeX + 0];
val += input[inOffset + sizeX + 1];
// divide by 4
val /= 4d;
// store result
output[outOff++] = val;
}
inOff += sizeX * 2;
}
}
else
{
int inOff = 0;
int outOff = 0;
for (int y = 0; y < halfSizeY; y++)
{
for (int x = 0, inOffset = inOff; x < halfSizeX; x++, inOffset += 2)
output[outOff++] = input[inOffset];
inOff += sizeX * 2;
}
}
}
/**
* Returns a down scaled by a factor of 2 of the input image data (X and Y resolution are
* divided by 2).<br>
* This function is specifically optimized for factor 2 down scaling.
*
* @param input
* input image data array (single dimension)
* @param sizeX
* width of source image
* @param sizeY
* height of source image
* @param signed
* consider input byte data as signed (only meaningful when filter is enabled)
* @param filter
* enable pixel blending for better representation of the down sampled result image
* (otherwise nearest
* neighbor is used)
* @param output
* output buffer (single dimension array with same data type as source image data array).
* If set to <code>null</code> a new array is allocated.
* @return output buffer containing the down scaled version of input image data.
*/
public static Object downscaleBy2(Object input, int sizeX, int sizeY, boolean signed, boolean filter, Object output)
{
if (input == null)
return output;
final ArrayType arrayType = ArrayUtil.getArrayType(input);
final Object result = ArrayUtil.allocIfNull(output, arrayType, (sizeX / 2) * (sizeY / 2));
switch (arrayType.getDataType().getJavaType())
{
case BYTE:
downscaleBy2((byte[]) input, sizeX, sizeY, signed, filter, (byte[]) result);
break;
case SHORT:
downscaleBy2((short[]) input, sizeX, sizeY, signed, filter, (short[]) result);
break;
case INT:
downscaleBy2((int[]) input, sizeX, sizeY, signed, filter, (int[]) result);
break;
case FLOAT:
downscaleBy2((float[]) input, sizeX, sizeY, filter, (float[]) result);
break;
case DOUBLE:
downscaleBy2((double[]) input, sizeX, sizeY, filter, (double[]) result);
break;
}
return result;
}
/**
* Returns a down scaled by a factor of 2 of the input image data (X and Y resolution are
* divided by 2).<br>
* This function is specifically optimized for factor 2 down scaling.
*
* @param input
* input image data array (single dimension)
* @param sizeX
* width of source image
* @param sizeY
* height of source image
* @param signed
* consider input byte data as signed (only meaningful when filter is enabled)
* @param filter
* enable pixel blending for better representation of the down sampled result image
* (otherwise nearest
* neighbor is used)
* @return output buffer containing the down scaled version of input image data.
*/
public static Object downscaleBy2(Object input, int sizeX, int sizeY, boolean signed, boolean filter)
{
return downscaleBy2(input, sizeX, sizeY, signed, filter, null);
}
/**
* Returns a down scaled by a factor of 2 of the input image (X and Y resolution are divided by
* 2).<br>
* This function is specifically optimized for factor 2 down scaling.
*
* @param input
* input image
* @param filter
* enable pixel blending for better representation of the down sampled result image
* (otherwise nearest
* neighbor is used)
* @param output
* output image receiving the result (should be of same type as input image with X and Y
* resolution divided
* by 2). If set to <code>null</code> a new image is created.
*/
public static IcyBufferedImage downscaleBy2(IcyBufferedImage input, boolean filter, IcyBufferedImage output)
{
if (input == null)
return output;
final IcyBufferedImage result;
final int sizeX = input.getSizeX();
final int sizeY = input.getSizeY();
final int sizeC = input.getSizeC();
if (output != null)
result = output;
else
// create an empty image with specified size and current colormodel description
result = new IcyBufferedImage(sizeX / 2, sizeY / 2, input.getIcyColorModel());
for (int c = 0; c < sizeC; c++)
downscaleBy2(input.getDataXY(c), sizeX, sizeY, input.isSignedDataType(), filter, result.getDataXY(c));
return result;
}
/**
* Returns a down scaled by a factor of 2 of the input image (X and Y resolution are divided by
* 2).<br>
* This function is specifically optimized for factor 2 down scaling.
*
* @param input
* input image
* @param filter
* enable pixel blending for better representation of the down sampled result image
* (otherwise nearest
* neighbor is used)
*/
public static IcyBufferedImage downscaleBy2(IcyBufferedImage input, boolean filter)
{
return downscaleBy2(input, filter, null);
}
/**
* Return a copy of the source image with specified size, alignment rules and filter type.
*
* @param source
* source image
* @param resizeContent
* indicate if content should be resized or not (empty area are 0 filled)
* @param xAlign
* horizontal image alignment (SwingConstants.LEFT / CENTER / RIGHT)<br>
* (used only if resizeContent is false)
* @param yAlign
* vertical image alignment (SwingConstants.TOP / CENTER / BOTTOM)<br>
* (used only if resizeContent is false)
* @param filterType
* filter method used for scale (used only if resizeContent is true)
*/
public static IcyBufferedImage scale(IcyBufferedImage source, int width, int height, boolean resizeContent,
int xAlign, int yAlign, FilterType filterType)
{
if (source == null)
return null;
final IcyBufferedImage result;
final int srcW = source.getWidth();
final int srcH = source.getHeight();
final boolean resize = resizeContent && ((width != srcW) || (height != srcH));
// no content resize ?
if (!resize)
{
final int xt;
final int yt;
// calculate translation values
final int dx = width - srcW;
switch (xAlign)
{
default:
case SwingConstants.LEFT:
xt = 0;
break;
case SwingConstants.CENTER:
xt = dx / 2;
break;
case SwingConstants.RIGHT:
xt = dx;
break;
}
final int dy = height - srcH;
switch (yAlign)
{
default:
case SwingConstants.TOP:
yt = 0;
break;
case SwingConstants.CENTER:
yt = dy / 2;
break;
case SwingConstants.BOTTOM:
yt = dy;
break;
}
// create an empty image with specified size and current colormodel description
result = new IcyBufferedImage(width, height, source.getIcyColorModel());
// copy data from current image to specified destination
result.copyData(source, null, new Point(xt, yt));
}
else
{
final Float xScale = Float.valueOf((float) width / srcW);
final Float yScale = Float.valueOf((float) height / srcH);
final Interpolation interpolation;
switch (filterType)
{
default:
case NEAREST:
interpolation = Interpolation.getInstance(Interpolation.INTERP_NEAREST);
break;
case BILINEAR:
interpolation = Interpolation.getInstance(Interpolation.INTERP_BILINEAR);
break;
case BICUBIC:
interpolation = Interpolation.getInstance(Interpolation.INTERP_BICUBIC);
break;
}
// use JAI scaler (use a copy to avoid source alteration)
final RenderedOp renderedOp = ScaleDescriptor.create(getCopy(source), xScale, yScale, Float.valueOf(0f),
Float.valueOf(0f), interpolation, new RenderingHints(JAI.KEY_BORDER_EXTENDER,
BorderExtender.createInstance(BorderExtender.BORDER_COPY)));
result = IcyBufferedImage.createFrom(renderedOp, source.isSignedDataType());
}
return result;
}
/**
* Return a copy of the image with specified size.<br>
* By default the FilterType.BILINEAR is used as filter method if resizeContent is true
*
* @param source
* source image
* @param resizeContent
* indicate if content should be resized or not (empty area are 0 filled)
* @param xAlign
* horizontal image alignment (SwingConstants.LEFT / CENTER / RIGHT)<br>
* (used only if resizeContent is false)
* @param yAlign
* vertical image alignment (SwingConstants.TOP / CENTER / BOTTOM)<br>
* (used only if resizeContent is false)
*/
public static IcyBufferedImage scale(IcyBufferedImage source, int width, int height, boolean resizeContent,
int xAlign, int yAlign)
{
return scale(source, width, height, resizeContent, xAlign, yAlign, FilterType.BILINEAR);
}
/**
* Return a copy of the image with specified size<br>
*
* @param source
* source image
* @param filterType
* filter method used for scale (used only if resizeContent is true)
*/
public static IcyBufferedImage scale(IcyBufferedImage source, int width, int height, FilterType filterType)
{
return scale(source, width, height, true, 0, 0, filterType);
}
/**
* Return a copy of the image with specified size.<br>
* By default the FilterType.BILINEAR is used as filter method.
*/
public static IcyBufferedImage scale(IcyBufferedImage source, int width, int height)
{
return scale(source, width, height, FilterType.BILINEAR);
}
/**
* Translate image internal data of specified channel by the specified (x,y) vector.<br>
* Data going "outside" image bounds is lost.
*/
public static void translate(IcyBufferedImage source, int dx, int dy, int channel)
{
if (source == null)
return;
// nothing to do
if ((dx == 0) && (dy == 0))
return;
final int sizeX = source.getSizeX();
final int sizeY = source.getSizeY();
// limit to sizeX / sizeY
final int adx = Math.min(Math.max(-sizeX, dx), sizeX);
final int ady = Math.min(Math.max(-sizeY, dy), sizeY);
final int fromFill;
final int toFill;
final int fromCopy;
final int toCopy;
final int wCopy;
if (adx < 0)
{
fromCopy = -adx;
toCopy = 0;
wCopy = sizeX + adx;
fromFill = wCopy;
toFill = sizeX;
}
else
{
fromFill = 0;
toFill = adx;
fromCopy = fromFill;
toCopy = toFill;
wCopy = sizeX - adx;
}
final Object data = source.getDataXY(channel);
if (ady < 0)
{
final int hCopy = sizeY + ady;
int toOffset = 0;
int fromOffset = -ady * sizeX;
// copy
for (int y = 0; y < hCopy; y++)
{
// copy first
Array1DUtil.innerCopy(data, fromOffset + fromCopy, toOffset + toCopy, wCopy);
// then fill
Array1DUtil.fill(data, toOffset + fromFill, toOffset + toFill, 0d);
// adjust offset
fromOffset += sizeX;
toOffset += sizeX;
}
// fill
Array1DUtil.fill(data, toOffset, fromOffset, 0d);
}
else
{
final int hCopy = sizeY - ady;
int toOffset = (sizeY - 1) * sizeX;
int fromOffset = toOffset - (ady * sizeX);
// copy
for (int y = 0; y < hCopy; y++)
{
// copy first
Array1DUtil.innerCopy(data, fromOffset + fromCopy, toOffset + toCopy, wCopy);
// then fill
Array1DUtil.fill(data, toOffset + fromFill, toOffset + toFill, 0d);
// adjust offset
fromOffset -= sizeX;
toOffset -= sizeX;
}
// fill
Array1DUtil.fill(data, 0, 0 + (ady * sizeX), 0d);
}
// notify data changed
source.dataChanged();
}
/**
* Translate image internal data (all channels) by the specified (x,y) vector.<br>
* Data going "outside" image bounds is lost.
*/
public static void translate(IcyBufferedImage source, int dx, int dy)
{
if (source != null)
{
final int sizeC = source.getSizeC();
source.beginUpdate();
try
{
for (int c = 0; c < sizeC; c++)
translate(source, dx, dy, c);
}
finally
{
source.endUpdate();
}
}
}
}