/* * 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(); } } } }